A bahn.de-style train connection search that shows the median arrival delay of the last 7 days (window selectable up to 30) for every connection, so you can book the one that actually runs on time. "Booking" deep-links to the real bahn.de page pre-filled with the journey.
www.bahn.de/web/api) provides journey options including transfers and prices — the same API the bahn.de website uses. Station autocomplete is answered from the local delay data, falling back to that API for stations without delay history.pipeline/merge_delays.py unions all five countries into the served table.plan + fchg) — the same field the pipeline stores, so the answer does not change later. Optional: set DB_API_KEY/DB_CLIENT_ID (e.g. in a .env); without them the site simply stops at the last ingested day.Prerequisites:
curl -LsSf https://astral.sh/uv/install.sh | sh)The delay pipeline imports the parser from the deutsche-bahn-data submodule, so it must be checked out:
git clone --recurse-submodules https://github.com/sha2nkt/delay_bahn.git
cd delay_bahn
If you already cloned without --recurse-submodules:
git submodule update --init --recursive
uv sync
This creates .venv/ and installs everything from uv.lock (FastAPI, DuckDB, pandas, huggingface-hub, ...).
The app needs the merged delay table before it can show delay stats — it opens data/delays.duckdb (built by merge_delays.py), or falls back to loading data/delays.parquet into memory if the db file is absent:
uv run python pipeline/build_delay_db.py # full window (default: 31 days)
uv run python pipeline/build_delay_db.py --days 3 # quick smoke run
uv run python pipeline/merge_delays.py # write data/delays.parquet + the data/delays.duckdb the app opens
This downloads raw parquet files from the HuggingFace dataset into data/raw_data/ (~5.5 GB for the full window; data/de/delays.parquet adds another ~720 MB), parses each day once into the data/de/parsed/ cache, and merges them into data/de/delays.parquet. Re-run it daily to stay fresh — already-downloaded days are skipped and only days with new raw files are re-parsed. No HuggingFace account or token is needed; the dataset is public. merge_delays.py then combines the per-country tables (DE alone is fine) into data/delays.parquet and materializes the sorted data/delays.duckdb the app serves from.
uv run uvicorn app.main:app --port 8000
Open http://localhost:8000, search a connection (e.g. Berlin Hbf → München Hbf), sort by "Wenigste Verspätung".
| Path | Purpose |
|---|---|
pipeline/build_delay_db.py | HF download + XML parse → data/de/delays.parquet |
pipeline/build_ch_days.py, pipeline/fr_poller.py, pipeline/consolidate_fr.py | Swiss and French per-day producers |
pipeline/at_poller.py, pipeline/consolidate_at.py, pipeline/build_at_stations.py | Austrian per-day producer (ÖBB HAFAS board poller + curated station list) |
pipeline/nl_poller.py, pipeline/consolidate_nl.py, pipeline/build_nl_stations.py, pipeline/seed_nl_archive.py | Dutch per-day producer (OVapi GTFS-RT poller + station crosswalk + archive seeder) |
pipeline/it_poller.py, pipeline/consolidate_it.py, pipeline/build_it_stations.py | Italian per-day producer (ViaggiaTreno run tracking + station crosswalk) |
pipeline/merge_delays.py | unions the per-country tables → data/delays.parquet + data/delays.duckdb |
app/bahn_api.py | async client for the bahn.de web API |
app/delays.py | DuckDB delay-stats lookup (the core matching query) |
app/live_delays.py | live same-day lookups via the DB Timetables API |
app/main.py | FastAPI endpoints /api/locations, /api/journeys + static serving |
static/ | vanilla HTML/CSS/JS frontend |
deutsche-bahn-data/ | git submodule: data collection project whose parser and dataset we reuse |
data/ | gitignored: raw parquet mirror + delays.parquet + delays.duckdb |
feature_list.md — what the product does, feature by feature, with statusprogress.md — current state snapshot, verification status, known limitationslog.md — append-only change log (newest entry last; never rewrite old entries)This project is licensed under the Creative Commons Attribution-NonCommercial 4.0 International license (CC BY-NC 4.0) — see LICENSE for the full text. You may share and adapt this work for non-commercial purposes with attribution; commercial use requires separate permission.
The external data sources listed above remain under their own licenses (ODbL, CC BY 4.0, CC0, and provider terms).
Python
51.3%
JavaScript
27.7%
HTML
11.1%
CSS
9.1%
A bahn.de-style train connection search that shows the median arrival delay of the last 7 days (window selectable up to 30) for every connection, so you can book the one that actually runs on time. "Booking" deep-links to the real bahn.de page pre-filled with the journey.
www.bahn.de/web/api) provides journey options including transfers and prices — the same API the bahn.de website uses. Station autocomplete is answered from the local delay data, falling back to that API for stations without delay history.pipeline/merge_delays.py unions all five countries into the served table.plan + fchg) — the same field the pipeline stores, so the answer does not change later. Optional: set DB_API_KEY/DB_CLIENT_ID (e.g. in a .env); without them the site simply stops at the last ingested day.Prerequisites:
curl -LsSf https://astral.sh/uv/install.sh | sh)The delay pipeline imports the parser from the deutsche-bahn-data submodule, so it must be checked out:
git clone --recurse-submodules https://github.com/sha2nkt/delay_bahn.git
cd delay_bahn
If you already cloned without --recurse-submodules:
git submodule update --init --recursive
uv sync
This creates .venv/ and installs everything from uv.lock (FastAPI, DuckDB, pandas, huggingface-hub, ...).
The app needs the merged delay table before it can show delay stats — it opens data/delays.duckdb (built by merge_delays.py), or falls back to loading data/delays.parquet into memory if the db file is absent:
uv run python pipeline/build_delay_db.py # full window (default: 31 days)
uv run python pipeline/build_delay_db.py --days 3 # quick smoke run
uv run python pipeline/merge_delays.py # write data/delays.parquet + the data/delays.duckdb the app opens
This downloads raw parquet files from the HuggingFace dataset into data/raw_data/ (~5.5 GB for the full window; data/de/delays.parquet adds another ~720 MB), parses each day once into the data/de/parsed/ cache, and merges them into data/de/delays.parquet. Re-run it daily to stay fresh — already-downloaded days are skipped and only days with new raw files are re-parsed. No HuggingFace account or token is needed; the dataset is public. merge_delays.py then combines the per-country tables (DE alone is fine) into data/delays.parquet and materializes the sorted data/delays.duckdb the app serves from.
uv run uvicorn app.main:app --port 8000
Open http://localhost:8000, search a connection (e.g. Berlin Hbf → München Hbf), sort by "Wenigste Verspätung".
| Path | Purpose |
|---|---|
pipeline/build_delay_db.py | HF download + XML parse → data/de/delays.parquet |
pipeline/build_ch_days.py, pipeline/fr_poller.py, pipeline/consolidate_fr.py | Swiss and French per-day producers |
pipeline/at_poller.py, pipeline/consolidate_at.py, pipeline/build_at_stations.py | Austrian per-day producer (ÖBB HAFAS board poller + curated station list) |
pipeline/nl_poller.py, pipeline/consolidate_nl.py, pipeline/build_nl_stations.py, pipeline/seed_nl_archive.py | Dutch per-day producer (OVapi GTFS-RT poller + station crosswalk + archive seeder) |
pipeline/it_poller.py, pipeline/consolidate_it.py, pipeline/build_it_stations.py | Italian per-day producer (ViaggiaTreno run tracking + station crosswalk) |
pipeline/merge_delays.py | unions the per-country tables → data/delays.parquet + data/delays.duckdb |
app/bahn_api.py | async client for the bahn.de web API |
app/delays.py | DuckDB delay-stats lookup (the core matching query) |
app/live_delays.py | live same-day lookups via the DB Timetables API |
app/main.py | FastAPI endpoints /api/locations, /api/journeys + static serving |
static/ | vanilla HTML/CSS/JS frontend |
deutsche-bahn-data/ | git submodule: data collection project whose parser and dataset we reuse |
data/ | gitignored: raw parquet mirror + delays.parquet + delays.duckdb |
feature_list.md — what the product does, feature by feature, with statusprogress.md — current state snapshot, verification status, known limitationslog.md — append-only change log (newest entry last; never rewrite old entries)This project is licensed under the Creative Commons Attribution-NonCommercial 4.0 International license (CC BY-NC 4.0) — see LICENSE for the full text. You may share and adapt this work for non-commercial purposes with attribution; commercial use requires separate permission.
The external data sources listed above remain under their own licenses (ODbL, CC BY 4.0, CC0, and provider terms).
Python
51.3%
JavaScript
27.7%
HTML
11.1%
CSS
9.1%