⚠️ AI-generated draft — needs human validation
This issue was written by an AI agent from materials produced during the
hasadna Open Bus hackathon (July 2026). The underlying analysis was built fast,
under hackathon conditions, and has not been peer-reviewed. Figures,
endpoint behaviour and conclusions all need independent verification before
anyone acts on them or quotes them publicly.
Please validate before implementing. Corrections very welcome.
The one-sentence version
Nobody can tell you what time a bus actually reached a given stop — the API doesn't
store it — so every analysis re-derives it from raw GPS by hand, at ~1–2 minutes per
line, and they all derive it slightly differently. Compute it once in the ETL and
store it.
Background — why this is a problem
The Stride API has a gtfs_ride_stop__arrival_time field, and a
/siri_ride_stops/list endpoint with a nearest_siri_vehicle_location_* block that
looks exactly like "where the bus was when it reached this stop". These are empty.
Checked live on 2026-08-16, 1,000 consecutive rows from /siri_ride_stops/list for
service date 2026-08-10:
| column |
populated |
nearest_siri_vehicle_location_id |
0 / 1000 |
nearest_siri_vehicle_location__recorded_at_time |
0 / 1000 |
gtfs_stop_id |
0 / 1000 |
gtfs_ride_stop__arrival_time |
0 / 1000 |
Reproduce:
curl -s "https://open-bus-stride-api.hasadna.org.il/siri_ride_stops/list?limit=1000\
&siri_rides__scheduled_start_time_from=2026-08-10T00:00:00%2B03:00\
&siri_rides__scheduled_start_time_to=2026-08-10T23:59:59%2B03:00"
So the only actual-position data available is the raw GPS ping stream from
/siri_vehicle_locations/list — a location roughly once a minute per vehicle. To
find out when a bus reached a stop, you have to take the stop's coordinates, find the
vehicle's closest approach, and interpolate between the two bracketing pings.
Every analysis in this milestone that talks about time at a stop does this
independently. Segment reliability (#1782), the Marey diagram (#1783), the
segment×hour heatmap (#1784), headway decay (#1789) and bunching (#1790) all
re-implement the same interpolation. The hackathon write-up reports this costs ~1–2
minutes per line and forces aggressive caching — (this figure is from the write-up
and was not re-measured here).
Two consequences, and the second is worse than the first:
- It's slow. Nothing built on it can be interactive.
- Everyone's numbers differ. Each consumer picks its own match-distance
threshold and its own handling of the awkward cases, so two דאטאבוס charts can
legitimately disagree about the same bus.
What to build
Compute derived stop arrival times once, in the ETL, and store them. Per
(ride, stop):
The last two are not optional extras. They are what let a consumer decide whether to
trust a value. The hackathon work used 150 m as a loose-match warning, 300 m
as "drop this stop entirely", and a 2-minute ping gap as a coarse-timing warning.
Storing the raw measurements rather than a boolean lets consumers set their own bar.
Two artifacts that must be handled explicitly
Both of these were found the hard way during the hackathon and will silently corrupt
results if ignored:
- Terminal dwell. At the origin stop the bus is parked at the kerb for several
minutes. "Closest approach" lands in the middle of that idle period, which inflates
the first segment's duration. The origin should resolve to departure — the last
ping while still in the stop's vicinity — not to closest approach.
- Coincident junction stops. Stops metres apart with identical scheduled times can
match the same ping, or match out of order. Needs a forward-constrained monotonic
search: stop k may only match a ping at or after the one matched for stop k−1.
Non-positive segment durations get dropped.
Why it's worth your time
This is the shared bottleneck under most of the analysis tickets in this batch. It is
the difference between "a notebook someone ran once during a hackathon" and "a chart
that loads when a journalist opens the page". It also makes every downstream metric
consistent, which matters more than speed if these numbers are ever quoted publicly.
Arguably it should be built before the other scale-up tickets rather than after —
they are all waiting on it.
Getting started
- Where the code goes: this is upstream of this repo — the ETL lives in
hasadna/open-bus-stride-etl and
the API in hasadna/open-bus-stride-api.
Worth opening a discussion there before writing code.
- Reference implementation of the interpolation:
analyses/bus_arrival_reliability.py,
with the method written up in
algorithms/bus-arrival-reliability.md.
- Skills: Python, SQL, some geospatial comfort (point-to-point distance,
interpolation between timestamps). No ML, no heavy maths.
- Good first step that isn't the whole thing: confirm on a handful of lines that
the derived arrival times match reality — e.g. against a timetable app or a manual
GPS trace read. Nobody has validated the interpolation against ground truth, and
everything else rests on it.
Related tickets
Blocks or would speed up: #1782, #1783, #1784, #1789, #1790, #1799, #1800
Related data-quality issues: #1775 (SIRI→GTFS matching), #1778 (first ping ≠ departure)
Credit & provenance
Found during the hasadna Open Bus hackathon, July 2026 — analysis by the hackathon team.
· Method, evidence and caveats: algorithms/upstream-issues.md
· Original work: https://github.com/hasadna/open-bus-hackathon-26
· Issue drafts and the full defect list: algorithms/upstream-issues.md
Rewritten 2026-08-16 to be readable without the hackathon materials. The null-column
table was measured live against the production API on that date; the ~1–2 min/line
cost figure is from the hackathon write-up and was not re-measured.
The one-sentence version
Nobody can tell you what time a bus actually reached a given stop — the API doesn't
store it — so every analysis re-derives it from raw GPS by hand, at ~1–2 minutes per
line, and they all derive it slightly differently. Compute it once in the ETL and
store it.
Background — why this is a problem
The Stride API has a
gtfs_ride_stop__arrival_timefield, and a/siri_ride_stops/listendpoint with anearest_siri_vehicle_location_*block thatlooks exactly like "where the bus was when it reached this stop". These are empty.
Checked live on 2026-08-16, 1,000 consecutive rows from
/siri_ride_stops/listforservice date 2026-08-10:
nearest_siri_vehicle_location_idnearest_siri_vehicle_location__recorded_at_timegtfs_stop_idgtfs_ride_stop__arrival_timeReproduce:
So the only actual-position data available is the raw GPS ping stream from
/siri_vehicle_locations/list— a location roughly once a minute per vehicle. Tofind out when a bus reached a stop, you have to take the stop's coordinates, find the
vehicle's closest approach, and interpolate between the two bracketing pings.
Every analysis in this milestone that talks about time at a stop does this
independently. Segment reliability (#1782), the Marey diagram (#1783), the
segment×hour heatmap (#1784), headway decay (#1789) and bunching (#1790) all
re-implement the same interpolation. The hackathon write-up reports this costs ~1–2
minutes per line and forces aggressive caching — (this figure is from the write-up
and was not re-measured here).
Two consequences, and the second is worse than the first:
threshold and its own handling of the awkward cases, so two דאטאבוס charts can
legitimately disagree about the same bus.
What to build
Compute derived stop arrival times once, in the ETL, and store them. Per
(ride, stop):derived_arrival_time— interpolated moment of closest approachmatch_distance_m— how close the vehicle actually got to the stopping_gap_s— seconds between the two bracketing GPS pingsThe last two are not optional extras. They are what let a consumer decide whether to
trust a value. The hackathon work used 150 m as a loose-match warning, 300 m
as "drop this stop entirely", and a 2-minute ping gap as a coarse-timing warning.
Storing the raw measurements rather than a boolean lets consumers set their own bar.
Two artifacts that must be handled explicitly
Both of these were found the hard way during the hackathon and will silently corrupt
results if ignored:
minutes. "Closest approach" lands in the middle of that idle period, which inflates
the first segment's duration. The origin should resolve to departure — the last
ping while still in the stop's vicinity — not to closest approach.
match the same ping, or match out of order. Needs a forward-constrained monotonic
search: stop k may only match a ping at or after the one matched for stop k−1.
Non-positive segment durations get dropped.
Why it's worth your time
This is the shared bottleneck under most of the analysis tickets in this batch. It is
the difference between "a notebook someone ran once during a hackathon" and "a chart
that loads when a journalist opens the page". It also makes every downstream metric
consistent, which matters more than speed if these numbers are ever quoted publicly.
Arguably it should be built before the other scale-up tickets rather than after —
they are all waiting on it.
Getting started
hasadna/open-bus-stride-etlandthe API in
hasadna/open-bus-stride-api.Worth opening a discussion there before writing code.
analyses/bus_arrival_reliability.py,with the method written up in
algorithms/bus-arrival-reliability.md.interpolation between timestamps). No ML, no heavy maths.
the derived arrival times match reality — e.g. against a timetable app or a manual
GPS trace read. Nobody has validated the interpolation against ground truth, and
everything else rests on it.
Related tickets
Blocks or would speed up: #1782, #1783, #1784, #1789, #1790, #1799, #1800
Related data-quality issues: #1775 (SIRI→GTFS matching), #1778 (first ping ≠ departure)
Credit & provenance
Found during the hasadna Open Bus hackathon, July 2026 — analysis by the hackathon team.
· Method, evidence and caveats:
algorithms/upstream-issues.md· Original work: https://github.com/hasadna/open-bus-hackathon-26
· Issue drafts and the full defect list:
algorithms/upstream-issues.mdRewritten 2026-08-16 to be readable without the hackathon materials. The null-column
table was measured live against the production API on that date; the ~1–2 min/line
cost figure is from the hackathon write-up and was not re-measured.