Skip to content

Commit 2a0ef3b

Browse files
NoamGaashclaude
andcommitted
Drop duplicate SIRI pings before plotting or averaging
Prompted by a good question — whether the vehicle id and timestamp had actually been checked. They had not. Checking found one of each: - Vehicle identity is fine. Across 6,326 rides in a sampled window, zero span more than one siri_ride__vehicle_ref, so grouping by siri_ride__id never blends two buses, and timestamps are monotonic within a ride. - Timestamps are not. 1,936 of 20,000 rows (9.7%) are exact duplicates of (ride, recorded_at_time, lat, lon) — the same physical observation reported in overlapping SIRI snapshots. Those duplicates were not harmless. On schedule-adherence-map each stop's position is a 1/distance-weighted average of the pings that matched it, so a duplicated ping carried its weight twice and pulled the measured route toward whatever happened to be reported twice; matched pings for the sample line drop 81 -> 73 once deduplicated. On gps-trace-map they inflated the ping count and added zero-length map segments (13 -> 12). Deduplicated at both fetch sites, with the dropped count surfaced in the card's notes rather than silently discarded. The adherence cache key is bumped so the fix actually takes effect instead of serving pre-dedup data. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 59d09cb commit 2a0ef3b

2 files changed

Lines changed: 19 additions & 1 deletion

File tree

analyses/gps_trace_map.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ def run(req: AnalysisRequest):
8181
"07:00-11:00 Israel time — try a different line/operator."],
8282
)
8383

84+
# SIRI snapshots overlap, so the same physical observation is reported more
85+
# than once — measured at ~10% of rows in a sample window. Left in, those
86+
# duplicates inflate the ping count and add zero-length map segments.
87+
# (siri_ride__id never spans two vehicle_refs — checked across 6326 rides —
88+
# so grouping by ride is safe without also keying on the plate.)
89+
before = len(pings)
90+
pings = pings.drop_duplicates(subset=["siri_ride__id", "recorded_at_time", "lat", "lon"])
91+
duplicates = before - len(pings)
92+
8493
# One ride: whichever siri_ride__id has the most pings in the window — same
8594
# pick the source notebook made ("the ride with the richest trail").
8695
ride_id = pings.groupby("siri_ride__id").size().idxmax()
@@ -129,6 +138,8 @@ def run(req: AnalysisRequest):
129138
f"Picked automatically: the ride with the most GPS pings among all "
130139
f"{pings['siri_ride__id'].nunique()} rides seen for this line/operator "
131140
"in the sampled window.",
141+
f"{duplicates:,} duplicate ping(s) dropped before plotting — the same "
142+
"vehicle, instant and position reported in overlapping SIRI snapshots.",
132143
_CREDIT,
133144
],
134145
)

analyses/schedule_adherence_average.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def _load(line_ref: str, days_back: int):
9595
matches the reference stop sequence and that have GPS. Disk-cached: this is
9696
2 API calls per scanned day."""
9797

98-
key = ("v1", line_ref, days_back)
98+
key = ("v2-dedup", line_ref, days_back)
9999

100100
def compute():
101101
ref_date = datetime.date.today() - datetime.timedelta(days=LAG_DAYS)
@@ -177,6 +177,13 @@ def compute():
177177

178178
actual = pd.DataFrame(actual_rows)
179179
actual["recorded_at_time"] = pd.to_datetime(actual["recorded_at_time"], utc=True)
180+
# Overlapping SIRI snapshots report the same physical observation more
181+
# than once (~10% of rows in a sampled window). Duplicates are not
182+
# harmless here: the map's average position is distance-weighted, and a
183+
# duplicated ping carries its weight twice, pulling the measured route
184+
# toward whatever happened to be reported twice.
185+
actual = actual.drop_duplicates(
186+
subset=["siri_ride__id", "recorded_at_time", "lat", "lon"])
180187
actual = (_largest_group(actual, "siri_ride__id")
181188
.sort_values("recorded_at_time").reset_index(drop=True))
182189
matched.append({"date": day.isoformat(), "start": w_from, "actual": actual})

0 commit comments

Comments
 (0)