You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
🛑 On hold — do not start implementing
The metric this ticket proposes to build has a design problem that is not yet
resolved (see "Why this is on hold" below). The ticket is kept open because
the question it asks is worth answering; the method it proposes is probably
the wrong way to answer it. If you want to work on this, the useful contribution
right now is deciding what the metric should be — not writing the code.
Background — what this is about, from scratch
You do not need to have followed the hackathon to read this. Here is the whole story.
There is no "cancelled" flag in the Israeli bus data. A cancellation has to be
inferred. The Stride API has an endpoint, /rides_execution/list, that returns one
row per planned ride with two columns that matter: planned_start_time and actual_start_time. If actual_start_time is null, that planned ride has no
recorded execution — which is usually a cancellation.
During the hackathon, someone built a reliability score on top of that. The idea:
for a given bus line, look at the last 15 service days. A day where every planned
ride ran counts as 1. A day with even one cancellation counts as 0. The score is good_days / days_scored. It is deliberately harsh — the argument was that "did my
line have a clean day" is closer to lived passenger experience than "99.4% of trips
operated".
Method 3 is one sentence of a comment — # method 3: script that compares average score per bus company, and provide a visualization. There is no spec beyond that.
The obvious reading is: run method 2 for every operator, average each operator's
line scores, rank the operators.
That is what this ticket originally proposed. It should probably not be built as
described. Here is why.
A day without cancellation is a binary question. You don't know if the outage was
one single missing ride, or a 50% of all planned rides, for example. So it can flag
operators with constant small misses as worse than operators with frequent severe
outages, just because the first is smeared upon more calendar dates.
This was checked against the live API and it is correct. 52 lines across 9
operators, 15-day window (2026-08-01 → 2026-08-15), scoring logic copied from the
hackathon implementation, with a volume-weighted rate (1 − cancelled/planned)
computed from the same rows for comparison:
1. Two lines with near-identical real service, scored 6.7× apart:
line
rides/day
trips actually operated
binary score
תנופה 11
26
89.7%
0.47
בית שמש אקספרס 12
113
87.8%
0.07
The line that runs more of its trips scores worse, because it has more chances
per day to miss one.
2. Lines with an identical score delivering very different service. Five sampled
lines scored exactly 0.20. Their actual operated-trip rates ranged from 83.4%
(גלים 494) to 96.4% (סופרבוס 1) — a 13-point spread compressed into one number.
Two lines of the same operator, גלים 492 and 494, both score 0.20 while running
94.1% and 83.4% of their trips.
3. The score tracks frequency, not just reliability. Spearman correlation between
the binary score and rides/day is −0.69.
rides/day
lines
mean binary score
mean trips operated
< 20
17
0.90
99.2%
20–59
16
0.67
95.5%
60–119
10
0.41
95.6%
≥ 120
6
0.46
98.3%
A ~1 percentage-point difference in delivered service between the top and bottom
band shows up as a ~0.5 difference on a 0–1 headline score.
4. And that propagates straight into the operator ranking this ticket asks for.
Averaging each operator's line scores — literally what method 3 does — produced this:
operator
lines sampled
mean-of-binary (what method 3 publishes)
trips actually operated
median rides/day
מועצה אזורית גולן
6
0.96
99.6%
11
גי.בי.טורס
6
0.83
99.4%
34
תבל
2
0.77
99.8%
245
סופרבוס
10
0.72
97.1%
35
גלים
10
0.70
98.0%
47
תנופה
5
0.53
85.2%
26
בית שמש אקספרס
10
0.37
95.1%
96
תנופה delivers 10 percentage points less service than בית שמש אקספרס and ranks
above it. The ranking is inverted for the two worst operators in the sample,
because תנופה's average is propped up by two low-frequency lines that score 1.00 and
0.93. A national table built this way would partly be measuring which operators run
rural low-frequency routes, and publishing it as an accountability metric.
In fairness to the metric — it is not noise. Within a single frequency band it
discriminates cleanly: at ~230 rides/day, תבל lines 1 and 3 score 0.80/0.73 (99.8%
operated) while סופרבוס lines 1 and 71 score 0.20/0.27 (96.4% operated). Cancellations
also genuinely cluster on particular days — סופרבוס line 1 scores 0.20 where an
independent-failure model predicts ~0.00005, so "some days are bad days" is a real
effect the metric captures. The problem is specifically cross-line and
cross-operator comparison, which is the only thing method 3 does.
What would actually be worth building
Pick one of these and say so on the ticket before starting:
Option A — publish a volume-weighted execution rate instead (recommended).
Pooled 1 − Σcancelled / Σplanned per operator. It is computable from the exact same
API rows at zero extra cost, it is comparable across operators, and it is what most
transit regulators publish. This is what the hackathon author themselves recommended
in the write-up's own criticism section. The binary score can stay as a secondary
"clean days" column, clearly labelled as not comparable across lines.
Option B — keep the binary score but make it comparable. Band lines by frequency
and only ever compare within a band, or normalise against an expected clean-day rate
given the line's trip count. Harder, and it still throws away the severity
information @arielvino points out.
Option C — close this ticket. Methods 1 and 2 (#1795, #1796) are per-line
diagnostics and stand on their own merits. If nobody wants to defend a
cross-operator ranking, that is a legitimate outcome and worth recording.
Blocker: feed gaps are not cancellations, and the existing guard misses them
Whatever metric is chosen, this has to be solved first, because it can manufacture a
"worst operator in Israel" out of nothing.
The hackathon code guards against this with no_actuals = (cancelled == planned) —
if a line reports zero actuals for the entire window, it is drawn grey rather than
scored 0.00. That guard is not sufficient. Verified live in the same window:
כרמלית (operator_ref 20) — 2,040 planned rides, 2,040 "cancelled". Confirmed to
have zero rows in /siri_rides/list for the window. It is the Haifa funicular;
it has no SIRI feed. Caught by the guard.
כפיר (operator_ref 21) — 4,490 planned, 4,341 "cancelled" → 3.3% operated.
But it does return rows from /siri_rides/list, so cancelled == planned is false and the guard does not fire. It would be scored 0.00 and ranked dead
last in a national table.
תנופה line 41 — 847 planned, 846 "cancelled" (0.1%). Same problem, one line
rather than a whole operator.
3 of the 52 lines sampled were in this state. Whether the root cause is feed coverage
or the SIRI→GTFS matching described in #1775 was not established here — that is
itself worth investigating. Related: #1780 (operators absent from SIRI), #1788 / #1802 (coverage measurement).
A workable floor: exclude an operator-day whose operated rate is below some threshold
(the sampled genuinely-bad lines bottom out around 68%, while the feed-dead ones are
under 4% — there is a wide gap to put a line in), and cross-check against
network-wide SIRI volume for that day.
Getting started
Skills: Python or TypeScript, plus a willingness to argue about metric design.
The statistics involved are averages and rank correlations — nothing exotic.
The data, hands-on:/rides_execution/list requires all four of line_ref, operator_ref, date_from, date_to. Resolve line_refs from /gtfs_routes/list for the window you are scoring — one "line" is many line_refs (line 480 was 2
in Nov 2025 and 8 in Jul 2026), and the set changes over time. No API key needed;
the checked-in .env already points at production.
Do not average None as 0.0. The reference code returns None (not 0.0) for
"no planned rides in the window" specifically so this averaging step cannot go
wrong. Lines with no planned rides must be excluded from the denominator, not
scored zero.
Related tickets
#1795 (method 1, per line) · #1796 (method 2, per operator) · #1794 (worst days) · #1775, #1780 (why actuals go missing) · #1803 (pre-aggregation, the shared cost
bottleneck)
Rewritten 2026-08-16 to be readable standalone, and to record the live-API check of @arielvino's objection. The measurements in "Why this is on hold" were run by an AI
agent against the production Stride API on 2026-08-16 and are reproducible from the
description above — but they are a 52-line sample of small operators, not a census,
and have not been independently reproduced.
Background — what this is about, from scratch
You do not need to have followed the hackathon to read this. Here is the whole story.
There is no "cancelled" flag in the Israeli bus data. A cancellation has to be
inferred. The Stride API has an endpoint,
/rides_execution/list, that returns onerow per planned ride with two columns that matter:
planned_start_timeandactual_start_time. Ifactual_start_timeis null, that planned ride has norecorded execution — which is usually a cancellation.
During the hackathon, someone built a reliability score on top of that. The idea:
for a given bus line, look at the last 15 service days. A day where every planned
ride ran counts as 1. A day with even one cancellation counts as 0. The score is
good_days / days_scored. It is deliberately harsh — the argument was that "did myline have a clean day" is closer to lived passenger experience than "99.4% of trips
operated".
That work defined three methods, in a code comment
(
orion/days_with_no_cancellations.py:6-11):Method 3 is one sentence of a comment —
# method 3: script that compares average score per bus company, and provide a visualization. There is no spec beyond that.The obvious reading is: run method 2 for every operator, average each operator's
line scores, rank the operators.
That is what this ticket originally proposed. It should probably not be built as
described. Here is why.
Why this is on hold
@arielvino raised the objection that a
binary per-day metric is arbitrary and misleading:
This was checked against the live API and it is correct. 52 lines across 9
operators, 15-day window (2026-08-01 → 2026-08-15), scoring logic copied from the
hackathon implementation, with a volume-weighted rate (
1 − cancelled/planned)computed from the same rows for comparison:
1. Two lines with near-identical real service, scored 6.7× apart:
The line that runs more of its trips scores worse, because it has more chances
per day to miss one.
2. Lines with an identical score delivering very different service. Five sampled
lines scored exactly 0.20. Their actual operated-trip rates ranged from 83.4%
(גלים 494) to 96.4% (סופרבוס 1) — a 13-point spread compressed into one number.
Two lines of the same operator, גלים 492 and 494, both score 0.20 while running
94.1% and 83.4% of their trips.
3. The score tracks frequency, not just reliability. Spearman correlation between
the binary score and rides/day is −0.69.
A ~1 percentage-point difference in delivered service between the top and bottom
band shows up as a ~0.5 difference on a 0–1 headline score.
4. And that propagates straight into the operator ranking this ticket asks for.
Averaging each operator's line scores — literally what method 3 does — produced this:
תנופה delivers 10 percentage points less service than בית שמש אקספרס and ranks
above it. The ranking is inverted for the two worst operators in the sample,
because תנופה's average is propped up by two low-frequency lines that score 1.00 and
0.93. A national table built this way would partly be measuring which operators run
rural low-frequency routes, and publishing it as an accountability metric.
In fairness to the metric — it is not noise. Within a single frequency band it
discriminates cleanly: at ~230 rides/day, תבל lines 1 and 3 score 0.80/0.73 (99.8%
operated) while סופרבוס lines 1 and 71 score 0.20/0.27 (96.4% operated). Cancellations
also genuinely cluster on particular days — סופרבוס line 1 scores 0.20 where an
independent-failure model predicts ~0.00005, so "some days are bad days" is a real
effect the metric captures. The problem is specifically cross-line and
cross-operator comparison, which is the only thing method 3 does.
What would actually be worth building
Pick one of these and say so on the ticket before starting:
Option A — publish a volume-weighted execution rate instead (recommended).
Pooled
1 − Σcancelled / Σplannedper operator. It is computable from the exact sameAPI rows at zero extra cost, it is comparable across operators, and it is what most
transit regulators publish. This is what the hackathon author themselves recommended
in the write-up's own criticism section. The binary score can stay as a secondary
"clean days" column, clearly labelled as not comparable across lines.
Option B — keep the binary score but make it comparable. Band lines by frequency
and only ever compare within a band, or normalise against an expected clean-day rate
given the line's trip count. Harder, and it still throws away the severity
information @arielvino points out.
Option C — close this ticket. Methods 1 and 2 (#1795, #1796) are per-line
diagnostics and stand on their own merits. If nobody wants to defend a
cross-operator ranking, that is a legitimate outcome and worth recording.
Blocker: feed gaps are not cancellations, and the existing guard misses them
Whatever metric is chosen, this has to be solved first, because it can manufacture a
"worst operator in Israel" out of nothing.
The hackathon code guards against this with
no_actuals = (cancelled == planned)—if a line reports zero actuals for the entire window, it is drawn grey rather than
scored 0.00. That guard is not sufficient. Verified live in the same window:
have zero rows in
/siri_rides/listfor the window. It is the Haifa funicular;it has no SIRI feed. Caught by the guard.
But it does return rows from
/siri_rides/list, socancelled == plannedisfalse and the guard does not fire. It would be scored 0.00 and ranked dead
last in a national table.
rather than a whole operator.
3 of the 52 lines sampled were in this state. Whether the root cause is feed coverage
or the SIRI→GTFS matching described in #1775 was not established here — that is
itself worth investigating. Related: #1780 (operators absent from SIRI), #1788 /
#1802 (coverage measurement).
A workable floor: exclude an operator-day whose operated rate is below some threshold
(the sampled genuinely-bad lines bottom out around 68%, while the feed-dead ones are
under 4% — there is a wide gap to put a line in), and cross-check against
network-wide SIRI volume for that day.
Getting started
The statistics involved are averages and rank correlations — nothing exotic.
/rides_execution/listrequires all four ofline_ref,operator_ref,date_from,date_to. Resolveline_refs from/gtfs_routes/listfor the window you are scoring — one "line" is many
line_refs (line 480 was 2in Nov 2025 and 8 in Jul 2026), and the set changes over time. No API key needed;
the checked-in
.envalready points at production.orion/days_with_no_cancellations.py— methods 1 and 2, ~380 lines, with the five live data caveats documented inline.
Read
orion/days_with_no_cancellations.mdfirst; it explains why
/rides_execution/listwas chosen over two alternatives.line_ref. ~80 for a small company, ~1,240for אגד. A national ranking is a batch job, not a live query — which is part of
why Pre-aggregate derived arrival times so these analyses can run interactively #1803 (pre-aggregation) matters.
Noneas 0.0. The reference code returnsNone(not0.0) for"no planned rides in the window" specifically so this averaging step cannot go
wrong. Lines with no planned rides must be excluded from the denominator, not
scored zero.
Related tickets
#1795 (method 1, per line) · #1796 (method 2, per operator) · #1794 (worst days) ·
#1775, #1780 (why actuals go missing) · #1803 (pre-aggregation, the shared cost
bottleneck)
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 standalone, and to record the live-API check of
@arielvino's objection. The measurements in "Why this is on hold" were run by an AI
agent against the production Stride API on 2026-08-16 and are reproducible from the
description above — but they are a 52-line sample of small operators, not a census,
and have not been independently reproduced.