Skip to content

Commit 35978cf

Browse files
NoamGaashclaude
andcommitted
Document BusAnalysis related work and start algorithms/ solution docs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent dead176 commit 35978cf

4 files changed

Lines changed: 222 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ not the shared community API. `gtfs_rides_agg`'s `group_by` only accepts the
8686
fields in `AGG_GROUP_BY_FIELDS` (`gtfs_route_date`, `gtfs_route_hour`,
8787
`operator_ref`, `day_of_week`, `line_ref`) — anything else 500s server-side,
8888
so it's validated client-side first. Prefer `gtfs_rides_agg`/`siri_rides`
89-
over paging raw endpoints when the shape fits.
89+
over paging raw endpoints when the shape fits. Known defects in the upstream
90+
data — the stored SIRI→GTFS ride link is empty since 2024-10, `siri_ride` has
91+
~2.6% duplicate journeys, `first_vehicle_location_id` is a processing-state
92+
flag rather than a transmission signal, and five operators never reach the
93+
feed at all — are documented in `docs/busanalysis.md`. Check it before
94+
building an analysis that joins planned to actual or reads those columns.
9095

9196
**Styling is shared between Python and TypeScript by convention, not by
9297
import**: `openbus_hack/theme.py`'s `SERIES_LIGHT`/`SERIES_DARK` palettes are

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,15 @@ disk and won't hammer the shared DB.
158158
| Line info lookup (e.g. line 86001) | https://markav.net/line/86001/ |
159159
| Hasadna Slack (#opendata / open-bus channels) | https://join.slack.com/t/hasadna/shared_invite/zt-458cp0v0n-GSnHzAq6F5aHeK43O4YeeA |
160160

161+
**Related work — read before trusting a number:**
162+
[`docs/busanalysis.md`](docs/busanalysis.md) summarises
163+
[lihay7/BusAnalysis](https://github.com/lihay7/BusAnalysis) (private), a national
164+
planned-vs-actual reconstruction on this same data. It documents **five defects in
165+
the upstream `stride` tables** that constrain what our analyses can honestly claim —
166+
most importantly that the stored SIRI→GTFS ride link has been **empty since October
167+
2024**, and that `first_vehicle_location_id` is a processing-state flag, not evidence
168+
a vehicle stopped transmitting.
169+
161170
---
162171

163172
## Your scratch space

algorithms/README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Algorithms — what each hackathon solution actually does
2+
3+
One file per solution. Each documents the **author**, the **algorithm**, the
4+
**reasoning** behind the design choices, the **findings** (each with a stated
5+
confidence level), and **criticism** — where the output should not be trusted,
6+
and what would have to change for it to be.
7+
8+
These are working notes for two audiences: whoever picks a POC up for
9+
consolidation upstream, and whoever has to defend a number in the
10+
presentation. Neither is served by a document that only says what worked.
11+
12+
## Index
13+
14+
| Solution | Author | Cards | What it answers |
15+
|---|---|---|---|
16+
| [bus-arrival-reliability.md](bus-arrival-reliability.md) | noamf2001 | 3 | Where is the timetable optimistic? Where does the bus lose time? Which segments break at rush hour? |
17+
| [schedule-adherence.md](schedule-adherence.md) | yuvalko1 | 3 | How much does the same departure vary day to day, geographically and per day? |
18+
| [siri-coverage.md](siri-coverage.md) | yuvalko1 | 1 | What share of planned stops actually got a real-time GPS ping, by hour? |
19+
| [gps-trace-map.md](gps-trace-map.md) | yuvalko1 | 1 | What does one real ride's raw GPS trail look like? |
20+
| [poisson-arrival-regularity.md](poisson-arrival-regularity.md) | yuvalko1 / Yuval | 1 | Does headway spacing decay into a random Poisson process downstream? **Currently broken.** |
21+
| [days-with-no-cancellations.md](days-with-no-cancellations.md) | orion | 1 (+CLI) | What fraction of the last 15 days did a line run with zero cancellations? |
22+
| [service-violations.md](service-violations.md) | team | 2 | Ghost rides, early departures, late departures — the three fineable failure modes. |
23+
| [bus-bunching.md](bus-bunching.md) | team | 1 | How evenly spaced were consecutive buses against the line's own scheduled spacing? |
24+
| [route-divergence.md](route-divergence.md) | team | 2 | Which buses strayed from the planned route, and where? |
25+
| [busline-usage-anomaly.md](busline-usage-anomaly.md) | team | 1 | Which lines carry unusually many/few riders for their peer group and hour? |
26+
| [service-by-operator.md](service-by-operator.md) | example | 1 | Planned vs actual rides per day — the worked example. |
27+
28+
Related, not a hackathon solution: [`docs/busanalysis.md`](../docs/busanalysis.md)
29+
summarises `lihay7/BusAnalysis`, an independent national reconstruction on the
30+
same data whose five upstream defect reports overlap heavily with what these
31+
cards hit.
32+
33+
## Upstream issues
34+
35+
[upstream-issues.md](upstream-issues.md) collects every defect these solutions
36+
ran into, deduplicated and written as paste-ready issue drafts, with the repo
37+
each one belongs in. That is the prep for filing against
38+
[hasadna/open-bus-map-search](https://github.com/hasadna/open-bus-map-search)
39+
and its sibling repos.
40+
41+
## Confidence levels
42+
43+
Every finding below carries one of these. They describe **how much the finding
44+
would survive scrutiny**, not how large the effect is.
45+
46+
| Level | Means |
47+
|---|---|
48+
| **High** | Directly observed in the data, on a population large enough that sampling isn't the explanation, and the mechanism is understood. Would survive being quoted in public. |
49+
| **Medium** | Observed and reproducible, but on one line / a few days / one operator, or resting on a proxy whose error is understood but not bounded. Directionally trustworthy; the exact number is not. |
50+
| **Low** | Observed once, or on a sample too small to separate from noise, or resting on a proxy that could be an artifact. Worth investigating, not worth quoting. |
51+
| **Not a finding** | An artifact of the data pipeline that was mistaken for a finding at some point during the hackathon, and is recorded here so nobody rediscovers it as real. |
52+
53+
A structural rule that applies to nearly everything here: **these are claims
54+
about the record, not about the road.** A ride SIRI never saw is
55+
indistinguishable from a ride that never ran. Where a card counts "missing"
56+
rides, the finding is about what the tracking feed contains.
57+
58+
## Shared method, shared caveats
59+
60+
Most cards converge on the same skeleton, for the same reasons — worth reading
61+
once rather than in every file:
62+
63+
- **Line resolution.** A "line" (`route_short_name` like `480`) is *not* one
64+
thing. It is many `line_ref`s — one per direction × route alternative ×
65+
operator — and the set changes over time (line 480 was 2 refs in Nov 2025 and
66+
8 in Jul 2026). Cards resolve `route_short_name → line_ref` through
67+
`/gtfs_routes/list` **for the window being analysed**, and most take the first
68+
match, which is a real simplification.
69+
- **SIRI lag.** Ingestion runs ~3 days behind live. Every card clamps its window
70+
back by `LAG_DAYS = 3`; without it, a perfectly healthy line returns zero
71+
pings and reads as a total service collapse.
72+
- **Weekend exclusion.** Israeli bus service is thin by design on Fri/Sat, so
73+
headway- and reliability-based cards drop `weekday() in (4, 5)`.
74+
- **Duplicate pings.** Overlapping SIRI snapshots repeat the same physical
75+
observation — measured at ~10% of rows. Every card that touches raw pings
76+
dedups on `(siri_ride__id, recorded_at_time, lat, lon)`.
77+
- **First-ping-as-departure.** No card observes a real "doors closed" moment.
78+
Actual departure is inferred from GPS, and the naive version of that inference
79+
is wrong in a specific, measured way — see
80+
[service-violations.md](service-violations.md), which is where the artifact
81+
was found and corrected.
82+
- **Cost caps.** These run as live dashboard cards, not batch jobs. Where an
83+
original notebook scanned 90 days, the port scans 2–21 and says so in its
84+
notes. Lower confidence is the price, and it is disclosed on the card rather
85+
than hidden.

docs/busanalysis.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# BusAnalysis — related work on the same data
2+
3+
[`lihay7/BusAnalysis`](https://github.com/lihay7/BusAnalysis) is an independent
4+
reconstruction of Israel's planned-vs-actual bus record, built on the same
5+
hasadna `stride` data this hackathon uses. It is worth knowing about here for
6+
two reasons: it answers "did the scheduled bus run?" at national scale, and it
7+
found five defects in the upstream data that affect anyone querying the same
8+
tables — including our analyses.
9+
10+
**Access:** the repo is **private** (a plain `curl` or unauthenticated fetch
11+
404s — that is permission, not a missing repo). Licence is MIT, but it is not
12+
our code: anything ported across needs `lihay7`'s sign-off first. Read it with
13+
an authenticated `gh`:
14+
15+
```bash
16+
gh repo view lihay7/BusAnalysis
17+
gh api repos/lihay7/BusAnalysis/git/trees/main?recursive=1 --jq '.tree[].path'
18+
```
19+
20+
Scope: 1,306 service days (2023-01 → 2026-07), 117.2M schedule links, ~7 GB
21+
local warehouse rebuilt from GTFS + SIRI. Created and pushed 2026-07-31.
22+
Languages are Python (~1 MB of source) and HTML (the committed dashboards).
23+
24+
---
25+
26+
## The four findings
27+
28+
| # | Finding | Fix direction |
29+
|---|---|---|
30+
| 1 | **5.2%** of scheduled departures never appear in the national tracking record (±5 min, 116.4M rides, tracked operators). Widening to ±60 min only moves it to 4.9% — so these are cancellations, not delays. | Publish the rate per line and per month **with its window stated**. |
31+
| 2 | **~1.7×** — the sparsest lines (under 8 departures/day) lose **7.8%** of departures against **4.6–4.8%** on mid-frequency lines, after the control that kills the naive version of the claim. | Protect thin lines first: one missing bus on a two-a-day line is the whole service that day. |
32+
| 3 | **5.0% vs 1.5%** — reconstructed non-execution for 2024-H1 against the figure the ministry's own electronic control published for the same period, both at ±30 min. | Compute the violation rate from the open record; the enforcement basis is already electronic. |
33+
| 4 | **4.2 minutes** median *extra* waiting beyond what the published timetable promises, nationally, every month. 11.8% of line-months run 10+ minutes over promise. | Publish a timetable the service can keep — an honest every-20 beats a broken every-10. |
34+
35+
Each number is reproducible from committed aggregates in `report/metrics/`
36+
(parquet, ~9 MB) without rebuilding the warehouse.
37+
38+
---
39+
40+
## Five upstream data defects — relevant to our analyses
41+
42+
Written up as paste-ready issues in `handoff/issues/`. These describe the
43+
`stride` database we query through [stride.py](../openbus_hack/stride.py), so
44+
they constrain what our own cards can honestly claim.
45+
46+
| ID | Defect | Why it matters to us |
47+
|---|---|---|
48+
| **F1** | The SIRI→GTFS ride-matching job has written **zero matches since October 2024**. All four match columns (`gtfs_ride_id`, `route_gtfs_ride_id`, `scheduled_time_gtfs_ride_id`, `journey_gtfs_ride_id`) fail together; the raw feed is healthy (~2.9–3.1M `siri_ride` rows/month). | Any analysis joining actual→planned via the stored link gets nothing for the last ~21 months. You have to match yourself. |
49+
| **F6** | **2.6%** of `siri_ride` rows are surplus duplicate journeys (3.93% sit in a duplicated group); rate has ~quadrupled since 2023 and is 66× skewed across operators. Downstream of the `scheduled_start_time` drift bug (hasadna #390). | Raw counts of tracking rows overstate real service. Affects anything counting rides rather than deduplicating journeys. |
50+
| **F7** | `first_vehicle_location_id` is **100% null for 18 consecutive months** (2024-12 → 2026-05, 49.4M rides); `duration_minutes` tracks it. | It is a **processing-state flag, not a property of the bus** — it encodes whether stride's enrichment job ran, not whether a vehicle transmitted. Easy to misread as a per-operator transmission metric. Don't. |
51+
| **F8** | The GTFS import sometimes keeps the previous release alongside the current one under a single `gtfs_route.date`, **doubling planned counts** on affected dates. 50 days exceed 1.5×, 36 exceed 1.8×, scattered with no era pattern. | Planned-ride denominators are ~2× too high on those dates. |
52+
| **F9** | **Five operators never reach the tracking feed** — three absent entirely across 3.5 years, two under 1% covered. 2.74M scheduled rides, ~2.3% of national planned volume. | They are unmeasurable, not failing. Counting them as cancellations is most of how 5.2% becomes 7.4%. |
53+
54+
Fixing all five moves the national figure from **7.4% → 5.2%** (and to 42% on
55+
the worst days), which is the case for treating them as defects rather than
56+
noise.
57+
58+
---
59+
60+
## How to read numbers off this data honestly
61+
62+
Four rules the project holds itself to. They apply to our cards too:
63+
64+
1. **Every rate names its time window.** There is no tolerance-free answer to
65+
"did the bus run?" A figure without a window can't be compared to one that
66+
has it.
67+
2. **These are claims about the tracking record, not about the road.** A ride
68+
the feed never saw counts as missing. No independent national bound exists
69+
on the feed's own completeness — so the phrasing is "never appears in the
70+
record", never "the bus didn't run".
71+
3. **Time differences here are scheduled-against-scheduled, not punctuality.**
72+
The record holds no observed departure time. Their M2 is a *waiting*
73+
measure; actual lateness needs stop-level data.
74+
4. **Five operators have no tracking feed at all** — excluded from every
75+
performance rate and reported as a finding in their own right (F9).
76+
77+
---
78+
79+
## How it relates to this repo
80+
81+
Same data, opposite architecture:
82+
83+
| | this repo | BusAnalysis |
84+
|---|---|---|
85+
| Data access | live paginating API client with a disk cache | ~7 GB local mirror (4 slim tables, 245M rows), not committed |
86+
| Unit of work | one `@analysis` function per person → card | a batch pipeline: mirror → per-day matcher → marts → metrics → figures |
87+
| Matching | not attempted; we lean on what the API returns | own greedy 1:1 matcher, 6 time windows, in `src/busanalysis/matching/greedy.py` |
88+
| Output | live React dashboard over FastAPI | committed self-contained HTML (`report/dashboard/index.html`, 11.6 MB; `brief.html`, one screen) |
89+
| Registry | `openbus_hack/registry.py` discovers `analyses/*` | has its own `src/busanalysis/registry.py` for the same job |
90+
91+
Layout of theirs, briefly:
92+
93+
```
94+
src/busanalysis/
95+
pipeline/ mirror, extract_day, marts, backfill
96+
matching/ greedy 1:1 matcher + validation
97+
quality/ filters
98+
metrics/ departure_fidelity, enforcement_gap, gap_series,
99+
record_integrity, city_profiles
100+
viz/ charts, dashboard, brief, city_map, heatmap
101+
tests/ substantial — test_extract_day.py alone is 42 KB
102+
report/ dashboards, figures, committed parquet aggregates
103+
handoff/ the five defect issues + QUESTIONS_FOR_HASADNA.md
104+
assistant/ research record: findings ledger, verification memos,
105+
DECISIONS.md, ATTEMPTS_AND_FAILURES.md (the dead ends)
106+
plans/ the plans each phase was executed from
107+
```
108+
109+
## Directions worth taking from it
110+
111+
- **Port finding #2 as an analysis card.** "Sparse lines lose disproportionately
112+
more service" maps cleanly onto `@analysis` + `bar_chart` over
113+
`gtfs_rides_agg`, and it is the finding that most needs a live, per-line view
114+
rather than a static national figure.
115+
- **Cross-check.** They built from an offline mirror, we query the API live. If
116+
our numbers diverge from their 5.2%, the divergence itself is informative —
117+
most likely F6/F8 territory.
118+
- **Carry the defect list upstream.** The five `handoff/issues/` write-ups are
119+
directly relevant to the consolidation this hackathon feeds into
120+
(`open-bus-stride-api`, `open-bus-pipelines`). F1 in particular claims the
121+
national "rides that didn't run" statistic has had no computable basis since
122+
October 2024.

0 commit comments

Comments
 (0)