Empirical arrival-time predictions for Deutsche Bahn trains — as a distribution, not a point estimate.
Early release. Predictions are experimental — always cross-check times and connections with DB's official apps before relying on them.
Enter from, to and a departure time (future trips included): the app finds direct trains and one-transfer connections and predicts when you will actually arrive — median arrival, an 80% credible interval, the full distribution, and per-train catch probabilities — all computed from real historical runs, not from DB's own forecast. A Deutschland-Ticket filter restricts the search to regional trains; live station boards and per-train forecasts remain available behind the list icon.
- History: the piebro/deutsche-bahn-data
archive (CC BY 4.0) provides months of real per-stop delay data collected
from DB's IRIS API.
pipeline/build_shards.pycondenses it into one small shard per train: every station it calls at, with date, planned time, arrival/departure delay, delay at the previous stop, and cancellations. - Live state: the app fetches the keyless IRIS timetable API
(
iris.noncd.db.de) for the live board — the same source DB's station displays use. - Prediction: a weighted empirical distribution over the train's past runs at your station. Without live data, past final delays are weighted by recency (30-day half-life — short on purpose: construction sites and timetable changes make old runs stale) and a same-weekday boost. When a live delay is reported, the delta model shifts each run's observed last-hop progression (final − previous stop) onto the live report, sharpened towards runs that were similarly late. Trains without history fall back to a Bayesian Normal-inverse-gamma prior per train class and time of day (closed-form Student-t predictive).
- Connections: for a journey with a transfer, the app propagates the
feeder's arrival distribution through the transfer with the law of total
probability: the passenger boards the first connecting train (in planned
order) that has not yet left, so the final arrival is a mixture
Σ_k P(board k) · P(arrival | board k)over all candidate trains towards the destination — including the case where a delayed earlier train is still catchable. Departure and arrival delays of a candidate come from the same historical run, preserving their correlation; feeder and candidates are assumed independent (documented in-app). A Deutschland-Ticket switch (on by default when the feeder is regional) restricts candidates to covered trains (RE, RB, IRE, S-Bahn, private regional operators — no ICE/IC/EC, night trains or FlixTrain). - Data updates: predictions stay fresh to within ~a day, with minimal
downloads. The daily
update-dataworkflow maintains three assets on thedatarelease:meta.json(tiny descriptor),history.zip(monthly base, rebuilt when the archive publishes a new month) andrecent.zip(rebuilt daily bypipeline/build_recent.pyfrom the archive's raw IRIS logs, covering the days newer than the newest monthly file). The in-app update checksmeta.jsonfirst and fetches only the tier that changed; the app overlays recent runs onto the base. - Future trips: IRIS publishes live plans only ~a day ahead. Beyond
that the planner reconstructs station boards from the historical
timetable (
pipeline/build_boards.py: per station, every train that recently called there with typical times, weekday pattern and last-seen date) and predicts blind — clearly labelled in the UI, since timetable changes and construction can shift planned times. - On-demand shards: trains outside the local data are fetched
individually (a few KB each) from the repo's
shardsbranch, where the workflow publishes the merged base+recent set daily. Fetched shards are cached on disk with an 18-hour refresh, so a commuter's usual connections cost one download and then work offline. - Shard format: a columnar layout (deduplicated planned times, delta-coded dates, departure stored only when it differs from arrival) cut shard size ~63% versus naive per-run JSON rows; gzip keeps decoding dependency-free (brotli would save a further ~15% at the cost of a decoder dependency).
- Backtesting:
pipeline/backtest.pywalk-forward evaluates model variants on months of archive data with proper scoring rules (CRPS, pinball loss, interval coverage). On a 12-week eval (Easter–June 2026, 91k predictions) the delta model cut live-scenario CRPS 3.2× versus ignoring live data (1.53 vs 4.83); a 30-day half-life beat 7/14/60 days; explicit holiday handling showed no benefit even across the April–June holidays. Parameters above are the backtest winners.
- Widen the data beyond the draft subset (trains calling at Augsburg Hbf / München Hbf) — the pipeline and the update mechanism already handle any station list.
- Condition on the true previous-stop live delay instead of the current station's report.
- On-device TabICL v2 (BSD-3) via ONNX Runtime as the conditional model: context = this connection's historical runs, query = today's features, output = full predictive distribution.
Toolchain is pinned with pixi; the Android SDK must be
available via local.properties or ANDROID_HOME.
pixi run ./gradlew assembleDebug # build the APK
pixi run ./gradlew testDebugUnitTest # run unit tests
pixi run -e pipeline test-pipeline # data-pipeline testsThe pipeline tests matter more than their size suggests: the nightly job's monthly path only executes when a new archive file appears, so without them a break in it stays invisible for weeks.
pixi run -e pipeline python pipeline/backtest.py \
--data-dir pipeline/data --stations 8000013,8000261 --eval-weeks 12# download monthly parquet files into pipeline/data/, then:
pixi run -e pipeline python pipeline/build_shards.py \
--data-dir pipeline/data --out-dir pipeline/output --stations 8000013,8000261
cp pipeline/output/shards/*.jgz pipeline/output/index.json app/src/main/assets/history/The bundled station list (app/src/main/assets/stations.csv) derives from
db-stations (DB open data, CC BY 4.0).
MIT — see LICENSE. Bundled data: CC BY 4.0 by Deutsche Bahn (station list and delay history).