How we measured the ridership impact of CTA's Frequent Network, and what we found across eight separate analyses.
In 2025, CTA designated 20 bus routes as its Frequent Network, promising 10-minute headways during service hours. We set out to answer two questions: Did ridership increase on these routes? And are the promised headways being delivered?
For ridership, we use difference-in-differences (DiD), a causal inference method that compares changes over time between treated routes (Frequent Network) and control routes (similar non-FN routes). For headways, we collect real-time vehicle positions from CTA's Bus Tracker API and compare observed stop intervals against the GTFS schedule.
CTA rolled out the Frequent Network in four phases:
| Phase | Launch | Routes | Post-launch data |
|---|---|---|---|
| Phase 1 | March 23, 2025 | J14, 34, 47, 54, 60, 63, 79, 95 | ~8 months |
| Phase 2 | June 15, 2025 | 4, 20, 49, 66 | ~5 months |
| Phase 3 | August 17, 2025 | 53, 55, 77, 82 | ~3.5 months |
| Phase 4 | December 21, 2025 | 9, 12, 72, 81 | None |
Phase 2 launched June 15, 2025, and Phase 3 launched August 17, 2025, per CTA press releases.
Daily boardings by route from the Chicago Data Portal SODA API (dataset jyb9-n7fm). Public, no authentication required. Our data covers January 2023 through November 30, 2025 — 122,084 route-day observations across 131 routes.
CTA Bus Tracker API (getvehicles endpoint), polled every 5 minutes for all 20 Frequent Network routes via a Cloudflare Worker cron trigger and stored in Cloudflare D1 between Feb 16 and Mar 30, 2026. Collection ran during Frequent Network service hours (weekdays 6 AM–9 PM, weekends 9 AM–9 PM Chicago time).
Collection history: Early prototyping (Feb 11–16, 2026) used local polling at 60-second intervals and then GitHub Actions at 30-minute intervals. On Feb 17, 2026, the database was cleaned to contain only data from the current 5-minute Worker collector, ensuring a consistent sampling methodology across the entire dataset. The early data is archived locally for algorithm validation purposes.
See the headway analysis page for current results and caveats.
CTA GTFS static feed, which provides planned stop times, trip schedules, and route geometry (shapes). Used for scheduled headway computation and the route map.
Phase 4 launched on December 21, 2025, but the Chicago Data Portal ridership data ends on November 30, 2025. This means there are zero post-launch ridership observations for Phase 4 routes.
Including these routes in a DiD analysis creates artificial -100% year-over-year changes and severely biases aggregate estimates downward. All causal analyses in this project (notebooks 03, 06, 07, and 08) exclude Phase 4 routes from the treatment group.
Phase 4 routes are still shown on the map and in the headway analysis, but they are excluded from all ridership impact estimates.
DiD compares the change in ridership for treated routes (Frequent Network) against the change for untreated control routes over the same period. If both groups would have followed the same trend absent treatment, the difference in their changes estimates the causal effect.
Control routes are selected from non-FN routes based on pre-treatment ridership similarity. For each phase, already-treated routes from earlier phases are excluded from the control pool to prevent contamination.
A simple pooled DiD across all routes produces a negative estimate (-565 rides/day, -5.8%). This is misleading for two reasons:
Our solution: analyze each phase separately (notebook 06), use the Callaway-Sant'Anna staggered estimator (notebook 07), and confirm with regression fixed effects (notebook 08).
Confidence intervals for the phase-level DiD are computed via cluster bootstrap (1,000 iterations, resampled at the route level). This accounts for within-route correlation in daily ridership observations. With only 16 treated and ~15 control routes, the small sample makes asymptotic standard errors unreliable.
We ran eight separate analyses, each stress-testing the findings from a different angle. The table below summarizes all eight notebooks; details on each follow.
| # | Analysis | Key Finding |
|---|---|---|
| 01 | Ridership Exploration Initial EDA of all 20 routes |
13/16 Phase 1–3 routes gained riders YoY; pooled DiD misleadingly negative at -5.8% |
| 02 | Headway Exploration Scheduled vs. observed headways |
Schedule: 99.8% ≤ 10 min. See current observed results → |
| 03 | Phases 1–3 Only Excludes Phase 4 |
Cleaner data, but pooled DiD still negative (-4.6%) due to staggered treatment bias |
| 04 | Without Route #79 Tests if worst performer drives results |
Excluding #79 makes DiD worse (-8.4%), ruling out single-route explanation |
| 05 | Ridership Share FN share of total CTA ridership |
FN share grows +0.26 to +0.86 pp per phase; total ridership also growing |
| 06 | DiD by Phase Phase-level DiD with bootstrap CIs |
P1: +428/day (+5.9%), P2: +514 (+4.2%), P3: +1,142 (+10.4%) — all positive |
| 07 | Staggered DiD Callaway-Sant'Anna estimator |
Overall ATT: +429 rides/day, statistically significant |
| 08 | Regression DiD OLS with route + time fixed effects |
+236 rides/day (p=0.002); placebo test passes (p=0.93) |
The initial exploratory analysis covering all 20 Frequent Network routes and 131 total CTA bus routes. Pulls daily ridership from the Chicago Data Portal SODA API (Jan 2023 – Nov 2025), visualizes trends by route and phase, and computes a simple pooled 2×2 DiD.
Key finding: 13 of 16 Phase 1–3 routes show positive year-over-year ridership changes. J14 Jeffery Jump leads at +26.1%. However, the pooled DiD is negative (-565 rides/day) because control routes grew faster than treated routes on average — and Phase 4's zero post-data contaminates the estimate.
Assesses whether Frequent Network routes actually achieve the promised 10-minute headways. Compares scheduled headways from GTFS against observed headways from CTA Bus Tracker API data.
On paper, all 20 routes schedule 97–100% of headways at 10 minutes or less. Arrival detection uses a crossing+interpolation algorithm that identifies when each vehicle's pdist (distance along route) crosses a reference point, with arrival times linearly interpolated between bounding observations. See the headway analysis page for current results.
Re-runs the ridership analysis from notebook 01 but excludes Phase 4 routes entirely, since they have zero post-launch data. This gives a cleaner before/after comparison for 16 routes.
The pooled DiD estimate improves slightly to -443 rides/day (-4.6%), but remains negative. This notebook demonstrates that simply removing Phase 4 isn't enough — the fundamental problem is pooling across staggered treatment phases.
Route #79 (79th Street) had the worst year-over-year performance among Phase 1 routes at -6.2%, and carries the highest baseline ridership (~15,500 rides/day). This notebook tests whether removing this single outlier flips the result.
Finding: excluding Route #79 actually worsens the pooled DiD estimate to -795 rides/day (-8.4%). This rules out the hypothesis that one underperforming route is dragging down the result. The negative pooled estimate reflects a genuine methodological issue with pooling staggered treatments, not a single bad route.
A descriptive (non-causal) analysis showing the Frequent Network's share of total CTA bus ridership over time. If FN routes are gaining riders, their share of the system total should grow.
Results: FN share grows after each phase launch — +0.26 percentage points for Phase 1, +0.51 pp for Phase 2, and +0.86 pp for Phase 3 (year-over-year). Importantly, total CTA bus ridership also grew during this period, meaning the rising FN share represents genuine ridership growth, not just riders switching from other routes.
This is purely descriptive and doesn't control for confounders. See notebooks 06–08 for causal estimates.
The core analysis featured on the main page. Runs separate 2×2 DiD for each phase, using only never-treated routes as controls. This eliminates both Phase 4 contamination and staggered treatment bias.
Results: All three phases show positive and statistically significant ridership gains.
Confidence intervals are computed via cluster bootstrap (1,000 iterations at the route level).
Implements the Callaway & Sant'Anna (2021) estimator, which is specifically designed for settings with staggered treatment timing. Unlike standard TWFE, it avoids using already-treated units as controls for later cohorts.
The overall average treatment effect on the treated (ATT) is +429 rides/day, statistically significant. Group-level estimates are consistent with notebook 06: Phase 1 (+372), Phase 2 (+431), Phase 3 (+764). An event study shows effects growing over time, reaching +726–793 rides/day at 6–8 months post-treatment.
This is the most econometrically rigorous approach and independently confirms the phase-level findings.
A standard regression approach: OLS with route and time (year-month) fixed effects, plus a treatment indicator. Provides traditional standard errors and p-values.
The basic specification estimates +236 rides/day with homoskedastic standard errors (p=0.002, significant). With cluster-robust standard errors at the route level, p=0.22 — not significant at 5%, reflecting the reality that 31 routes provide limited statistical power.
Critically, a placebo test (applying fake treatment one year earlier) produces a near-zero estimate of -17 rides/day with p=0.925. This confirms there were no pre-existing differential trends between treated and control routes — a key assumption of DiD.
We ran three different causal estimation strategies (notebooks 06, 07, and 08). All three agree that the Frequent Network had a positive effect on ridership. We feature the phase-level DiD (notebook 06) on the main page for three reasons:
Each phase gets its own independent before-and-after comparison with its own control group. Readers can see that Phase 1, Phase 2, and Phase 3 are each individually positive — the finding doesn't depend on aggregation hiding variation across phases.
A simple “compare treated vs. control before and after” is easier for a general audience to grasp than Callaway-Sant’Anna group-time average treatment effects or regression coefficients with clustered standard errors.
The front page shows Phase 1 at +428, Phase 2 at +514, Phase 3 at +1,142 rides per day. A single aggregate ATT (+429 from notebook 07) loses this detail and obscures the fact that later phases show larger effects, possibly because the Frequent Network brand was better established.
Callaway-Sant’Anna (notebook 07) produces an overall ATT of +429 rides/day that is statistically significant. It is the most econometrically rigorous approach for staggered treatment settings and independently confirms the phase-level findings. Its main value is as a robustness check.
OLS regression (notebook 08) estimates +236 rides/day with homoskedastic standard errors (p=0.002, significant), but cluster-robust standard errors yield p=0.22 — not significant at the 5% level with only 31 routes. The most valuable output is the placebo test: applying fake treatment one year earlier produces a near-zero estimate (p=0.93), confirming there were no pre-existing differential trends between treated and control routes.
In short: all three methods point in the same direction. Phase-level DiD shows it most clearly. This page exists so readers who want the full statistical picture can evaluate all eight analyses.
The headway analysis went through several iterations before arriving at its current methodology. This section documents what changed, when, and why — both for transparency and because the process itself illustrates the challenges of measuring transit performance from real-time API data.
Data collection began on February 11, 2026, using a local Python script (headway_collector.py) that polled CTA’s Bus Tracker API every 60 seconds for all 20 Frequent Network routes. Vehicle positions were stored in a local SQLite database. Over three days, this produced 464,674 position records — dense, high-quality data with roughly 60 observations per vehicle per hour.
At this polling frequency, arrival detection worked well. The original algorithm looked for vehicles whose pdist (distance along route in feet) fell within a ±500-foot window around a reference stop. With observations every 60 seconds, a bus traveling at ~1,000 feet per minute would typically have at least one observation inside the 1,000-foot detection window as it passed through.
The limitation was operational: the script had to run continuously on a local machine, which wasn’t sustainable for long-term monitoring.
To automate collection, we moved to a GitHub Actions cron job that polled every 30 minutes and wrote positions to Cloudflare D1 via the REST API. This solved the operational problem but created a data quality problem: at 30-minute intervals, a bus covers roughly 30,000 feet between observations. The 1,000-foot detection window was almost never hit. Over three days, this produced only 15,414 records with very few usable arrival detections.
On February 16, we deployed a Cloudflare Worker with a cron trigger that polls every 5 minutes during service hours and writes directly to D1 via native binding. This was a significant improvement over GitHub Actions: faster execution, no cold start overhead, and a 5-minute interval that captures bus movements at a much finer granularity.
But a problem remained: even at 5-minute polling, a bus covers roughly 5,000–7,500 feet between observations. The original ±500-foot detection window was still too narrow. Most buses were jumping from well before the reference point to well past it in a single polling interval, never appearing “inside” the window.
To decide how to handle the three eras of data with different polling frequencies, we consulted three large language models (ChatGPT, Claude, and Gemini) with a detailed description of the data and algorithm. All three agreed that Era 2 data was essentially unusable. But Gemini identified the critical insight: the detection algorithm itself had a fundamental mismatch with 5-minute polling.
The math was straightforward. A bus traveling at ~17 mph covers ~1,300 feet per minute, or about 6,600 feet in 5 minutes. The detection window was 1,000 feet wide (±500 feet). The probability of a bus having an observation inside that window during a 5-minute poll was roughly 1,000 / 6,600 ≈ 15%. In practice, validation against the dense Era 1 data confirmed an approximately 80% false negative rate — four out of five actual arrivals went undetected.
This explained why the initial headway results showed only ~59% adherence to the 10-minute promise, and why Route 47 appeared to have zero arrivals despite buses clearly running.
The solution was to stop asking “is the bus inside the window?” and instead ask “did the bus cross the reference point between these two observations?”
The new algorithm examines consecutive observation pairs for each vehicle. If pdist was below the reference point in one observation and at or above it in the next, a crossing occurred. The exact arrival time is then linearly interpolated:
fraction = (reference - prev_pdist) / (curr_pdist - prev_pdist)
arrival_time = prev_time + fraction × (curr_time - prev_time)
For example, if a bus is at pdist 15,000 at 2:00 PM and pdist 22,000 at 2:05 PM, with a reference point at 20,000:
This assumes roughly constant speed between observations — an imperfect but reasonable approximation over 5-minute intervals. The interpolation is much more accurate than not detecting the arrival at all.
A 30-minute minimum gap between same-vehicle arrivals prevents false duplicates from GPS jitter, where pdist oscillates near the reference point without the bus actually making a new trip.
To verify the new algorithm, we used the dense Era 1 data (60-second polling) as ground truth. We ran the crossing algorithm at full resolution, then downsampled the same data to 5-minute intervals and re-ran it. Comparing the results:
| Metric | 60-second (ground truth) | 5-minute (simulated) | Difference |
|---|---|---|---|
| Arrivals detected | 7,345 | 6,968 | 94.9% detection rate |
| Mean adherence (≤ 10 min) | 92.2% | 90.8% | −1.5 pp |
| Routes with arrivals | 20 / 20 | 20 / 20 | — |
The 5-minute resolution captures 94.9% of arrivals and produces adherence metrics within 1.5 percentage points of ground truth across all 20 routes. This was deemed acceptable for ongoing monitoring. The validation script (scripts/validate_algorithm.py) is included in the repository.
With the algorithm fixed, we cleaned the D1 database to ensure methodological consistency:
data/headway.db for validation purposes.The daily analysis pipeline (scripts/update_headways.py) reads from D1 and updates the headway page automatically. As the Worker continues to collect data, the headway results will become increasingly robust.