Request access to the NYC Subway Delay Radar
Released for non-commercial research and journalism. Requests are reviewed manually; most are approved.
Before you dive in
Short version: free for research, journalism, teaching, and personal projects.
Please credit me, and please don't use it to make money without asking first.
The MTA's feeds are theirs, and they're public — you can get them straight
from the source without agreeing to anything. What I'm sharing is what's built
on top: the captured history the model was fit to, the learned baselines that
define "normal", the coefficients, the calibration table, and the training
protocol. That's my work, and I'm licensing it to you under CC BY-NC 4.0.
Worth saying why the coefficients count as authored rather than found: they're
the output of a set of choices — how the features are defined, excluding
alert-active periods when learning "normal", collapsing metrics to episodes,
picking the threshold on the training side only. The design is the part I made.
What you can do
Use it, study it, fine-tune it, benchmark against it, and publish what you
find — just credit Henry Williams / subway.fyi. Academic work, journalism,
teaching, and personal projects are all welcome, and if you beat these numbers
I'd love to hear about it.
The one real limit: not for commercial use
Please don't use this model, its outputs, or anything derived from it to make
money without asking me first. That covers shipping it in a paid product or
service, fine-tuning or distilling it for commercial deployment (derived
weights carry the same limit), selling predictions or analysis made with it,
and for-profit internal use.
Access here isn't a commercial licence on its own — but do get in touch if you
want one. I'm open to it.
Two caveats worth knowing
- Recall is low, by nature of the problem. It catches roughly one
disruption in eight, at about 5.75 false alarms a day. It's a screening aid
for a human reader, not a detector, and it isn't safety-certified — please
don't put it anywhere that being wrong would actually matter. - It isn't an MTA product. Unofficial and unaffiliated, so please don't
present its output as official service information.
Log in or Sign Up to review the conditions and access this model content.
NYC Subway Delay Radar
A delay-prediction model that tries to spot a subway service disruption from the public realtime feed before the MTA announces it. It runs in production on subway.fyi, evaluating every route roughly once a minute, and its full track record — misses included — is public at subway.fyi/accuracy.
The model is plain JSON logistic-regression coefficients (3.8 KB). There is
no ML runtime: scoring is twelve multiply-adds and a sigmoid, which is why it can
sit inside a lightweight poller container. predict.py in this repo is a
complete, dependency-free implementation.
The honest headline
On a single held-out window it was scored exactly once, at a threshold frozen beforehand on earlier data:
| delay radar (v2.7) | naive rule | |
|---|---|---|
| episode recall | 0.13 | 0.053 |
| precision | 0.446 | 0.242 |
| false alarms / day | 5.75 | 5.88 |
| median lead time | 43 min | — |
| holdout window | 2026-07-30 → 08-07 | same |
The naive rule is n_stations_30m >= 21, thresholded on the same validation
slice. So: at an equal false-alarm budget the model roughly doubles precision
and multiplies episode recall by ~2.5, and when it is right it fires a median of
43 minutes before the MTA posts.
Read the recall number correctly. It catches about one disruption in eight. This is a screening aid that surfaces some incidents early — not a comprehensive detector, and not something to make operational decisions on.
Two independent channels
The production radar combines the model in this repo with a second, non-learned
channel. Both write to delay_predictions in the companion dataset.
Gap channel (this model). Logistic regression over route-level headway
anomaly features: how many stations are running ≥2× their learned normal
headway, how far that has spread, whether it is growing, and how much of the
rest of the network is simultaneously anomalous. Baselines are per
(route, station, day-of-week, hour), rebuilt nightly from months of history
with alert-active periods excluded so "normal" never learns from a bad day.
Stall channel (rules, not learned). Physical evidence: one train anchored at
a platform beyond that stop's learned routine-hold time, with more trains pinned
behind it. A 9-minute hold at Canal St is normal bridge traffic; the same hold at
167 St is an emergency — so the threshold is per-platform, from
ml_stop_hold_baselines. Guards learned from production misfires: lay-up trains
parked on non-revenue tracks are rejected when other trains keep flowing through
the same platform, and ghost trips (unassigned future trips parked at origins)
are filtered using the NYCT is_assigned protobuf extension. Version
stall-v2.2-2026-08-11; knobs and their replay-derived rationale are in
poller/subway/predictor.py.
Usage
python3 predict.py
from predict import load, score, calibrate
m = load()
raw = score(m, feats) # compare against m["threshold"] (0.6339)
p = calibrate(m, raw) # empirical P(alert within 45 min) — display only
The two scales are not interchangeable. Fire on the raw score. Show a human
the calibrated one. The raw score runs ~3× overconfident: the worked example in
predict.py scores 0.82 raw, which is really a 21% chance an official alert
follows. Do not threshold on the calibrated value — that curve is deliberately
flat near the top.
Features
Twelve, all computed per (route, evaluation minute) against the learned baselines. A station is "flagged" when observed headway ≥ 2× baseline.
n_flag_now, n_stations_30m, max_ratio, med_ratio, frac_stations,
growth, buckets_active_30m, arrivals_ratio, n_other_routes_anom,
hour_sin, hour_cos, is_weekend — each documented inline in predict.py,
with the column names they carry in the published delay_predictions table.
How it was trained, and why the metrics are believable
train_delay_model.py + extract_clusters.sql reproduce the fit. The protocol
was deliberately strict, because this problem makes it very easy to fool
yourself:
- Time-split, never random. Train on buckets before 2026-07-30; holdout after. Random splits leak — adjacent 5-minute rows from one incident are nearly identical.
- Threshold picked train-side only, on the Jul 16–29 tail, at ≤5 false alarms/day maximising episode recall. The holdout was then scored once, at that frozen threshold. The table above is that single evaluation as it landed.
- Episode-level metrics, never per-row AUC. Rows within one incident are hugely autocorrelated; per-row AUC on this data is meaningless flattery. Episodes collapse per route on a 90-minute window.
- Labels timestamped by our own first observation of an MTA alert, never the
MTA's backdated
active_periodstart — otherwise the model gets credit for "predicting" something already announced. - Suppression parity between training candidates and live behaviour: a route with a currently active alert does not fire, so it must not train as if it would.
- A GBM benchmark was fit and rejected. In-sample recall 0.289 looked much better than the linear model; out-of-sample validation recall was 0.052. The simpler model shipped.
frozen_v2_holdout inside delay_model.json records the previous version
scored on the identical holdout candidates, so the improvement is a like-for-like
comparison rather than a moved goalpost.
Limitations
- Recall is low by design of the problem, not by accident. Most disruptions never produce a legible headway signature in the public feed before they are announced.
- NYC subway only, and tied to MTA GTFS-RT semantics plus the NYCT protobuf extensions. Transferring to another agency means refitting from scratch; the feature definitions may survive, the coefficients will not.
- Baselines must be live. The coefficients are worthless without
per-(route, station, dow, hour) headway baselines rebuilt from recent history.
A snapshot ships in the companion dataset (
baselines_headway). - The calibration is mildly optimistic. It was fit on the holdout window itself. Acceptable for display; do not treat it as an independent estimate.
- ~5.75 false alarms/day. Fine for a screening dashboard where a human reads the explanation. Not fine for anything automated.
- Weekend coverage is thin in the holdout: Aug 1–2 was 82–84% suppressed by active planned work, so the Aug 1 D/Q/4/6 disruption is out of scope for these numbers.
- Not safety-certified, not affiliated with or endorsed by the MTA.
Files
| file | what it is |
|---|---|
delay_model.json |
production model v2.7 — coefficients, threshold, calibration table, and the metrics above |
predict.py |
dependency-free reference scorer, mirrors production |
train_delay_model.py |
the fit, including the episode collapse and the rejected GBM benchmark |
extract_clusters.sql |
builds training candidates from the 5-minute analytics panel |
Companion dataset
digitalhen/nyc-subway-realtime
— the data this was trained on, including delay_predictions (every evaluation
this model has made, with nightly outcome labels) and baselines_headway. That
table is directly usable as a benchmark: the task is to beat these numbers on a
later window.
Citation
@misc{williams2026delayradar,
author = {Williams, Henry},
title = {NYC Subway Delay Radar: predicting subway disruptions from realtime feeds},
year = {2026},
url = {https://huggingface.co/digitalhen/nyc-subway-delay-radar},
note = {Live at https://subway.fyi}
}
Licence
CC BY-NC 4.0 — use, share, and adapt it freely, with credit to Henry Williams / subway.fyi, for non-commercial purposes.
The upstream MTA GTFS-RT feeds are the MTA's public open data, under their own terms, and aren't claimed here. The model itself — coefficients, calibration table, learned baselines, training protocol, and code — is my work, licensed to you rather than signed over.
For commercial use, including fine-tuning or distilling it for commercial deployment, please get in touch first — derived weights carry the same limit.