Attica traffic lab: bus-GPS map-matching and speed profiles, traffic-light cycle detection validated on pNEUMA drone data, SUMO what-ifs, count forecasting and LLM extraction of Greek traffic decrees
Python
0
1 commits
updated Sep 26, 2026
A research lab for understanding and predicting road traffic in Attica (Athens and Piraeus, Greece) from open and free-tier data: a fleet of small collectors that record feeds which keep no history, and analysis code that turns them into road speeds, timetable delays, signal timings, simulations, forecasts and structured traffic rules.
Athens publishes no live road-speed feed, no signal timing plans and no traffic rules as data; its open loop-detector feed stopped in 2026. The lab tests how far public bus GPS, municipal counters, drone datasets, official decisions and a local LLM can close that gap.
Status: research code, first pass. The numbers below come from a few days of collection (September 2026) and are labelled with their sample sizes. Nothing here is production software.
| Path | What |
|---|---|
collectors/ | 24/7 collectors for the "collector host" (stdlib Python; config and keys from environment variables) |
analysis/ | analysis scripts for the "analysis machine": map-matching, speeds, delays, signals, SUMO, forecasts |
analysis/trafficsim/ | SUMO simulation package (network, demand, scenarios, scoring, health gate, job runner, Optuna calibration) |
analysis/simtools/ | simulation diagnostics and DUA helper scripts |
analysis/tests/ | unit tests (pytest) and an opt-in SUMO smoke test |
deploy/ | systemd unit templates, an example crontab, the nightly sync script |
results/ | small derived result files and figures (see "Results") |
DATA.md | every data source with licence / terms and citation |
analysis/signals.py, analysis/pneuma_signals.py, analysis/pneuma_busval.py, analysis/pneuma.py
OASA buses report their position every ~20–30 s. For every OSM-signalised junction on a bus route, a pass that has two standing fixes 5–120 m before the junction and then a fix past it is a "queued at red, then released" event. Events are pooled per junction × approach across all lines, corrected for the release lag of the queue position, and their times are folded modulo candidate cycles C = 50…160 s. The C that maximises the weighted mean resultant length R (Rayleigh statistic) is the cycle; a near-tie rule prefers common plan lengths {80, 90, 100, 120, 135} s.
Ground truth comes from the pNEUMA drone dataset (EPFL, 2018, ~1.0 M vehicle crossings):
signals.py is a Greenwood–Durand series truncated after its first
correction term; it goes negative for strong peaks and ignores that the best of 111 candidate cycles was picked.
Use Zar's (1999) approximation p = exp(sqrt(1 + 4n + 4(n² − R_n²)) − (1 + 2n)) with R_n = nR, and a
look-elsewhere (max-over-grid) null. With a Monte-Carlo max-R null, 96 of 133 series stay significant at
0.05, and every exotic cycle (59, 94, 109, 131 s …) falls out, including both overlap misses: the detector knows
when it does not know. (These two corrections were computed in a separate check and are not yet patched into
signals.py.)analysis/osm_edges.py, analysis/bus_speeds.py, analysis/calibrate.py, analysis/profiles.py,
analysis/gtfs_delay.py, analysis/pick_calib_points.py
ln(car) = a + b·ln(bus) against TomTom Flow Segment readings at 24 well-covered
edges (468 pairs, 21 points, 3 days): leave-one-day-out MAE 14.5 → 10.1 km/h (pooled model
ln car = 1.413 + 0.679 ln bus). Per-class fits are not yet stable. No TomTom readings are published here.analysis/sim_piraeus.py, analysis/trafficsim/, analysis/demand_v2.py, analysis/build_ff_net.py,
analysis/junction_delay.py, analysis/sim_eval.py, analysis/sumo_smoke.sh, analysis/simtools/
SUMO 1.27 mesoscopic models built from OSM (netconvert with guessed/joined signals), demand from an in-house
hourly activity model (population, jobs, POIs, visitors, freight) with one global scale k, calibrated against
aggregated TomTom congestion levels.
analysis/piraeus_model.py, analysis/f1_forecast.py; figures in results/piraeus_forecast/
Hourly vehicle counts from five Municipality of Piraeus counters (10-min bins since 01/2026), rolling-origin test over the last 8 weeks: LightGBM (Poisson; calendar, Greek holidays, school periods, events, same-hour-last-week lag) reaches WAPE 21.4 % vs 26.3 % for "same hour last week" and 35.6 % for the hour-of-week mean. The gap is largest in the 15-August week (26.8 % vs 40.8 %). Weather makes it slightly worse, and football matches at the nearby stadiums do not move these counters (median ratio 0.989 within ±3 h of 41 games): the gates follow port operations.
collectors/diavgeia.py, collectors/rules_hunter.py, collectors/events/ (events.py, events_llm.py,
events_geo.py), analysis/rules_edges.py, analysis/build_street_index.py, analysis/diavgeia_orgs.py,
analysis/bench_gliner.py, analysis/qwen_scout.py, analysis/scout_verify.py
temperature 0, response_format: json_schema) extracts dated
disruption events (events_llm.py: schema + system prompt) and standing city rules (rules_hunter.py: 19 rule
types, status, vehicles, days, time windows, validity, confidence and a ≤ 200-char verbatim quote).analysis/sunglare.py
NOAA solar-position algorithm (with refraction) against the bearing of every main-road edge direction in Attica (238,686 edges, 384,754 edge-directions): glare = sun 0–20° above the horizon and within ±25° of the heading. Glare hits 2.34 % of edge-direction × week × 15-min slots, and winter has ~24 % more glare slots than midsummer. First check against evening bus speeds: no measurable effect (ratio 0.997 vs 0.991) — an honest null.
Activity model (activity_*.py, census_pop.py, elstat_pop.py, visitors.py, poi_extract.py,
overture_*.py, fsq_attica.py), static edge features (edge_features.py, eff_lanes_model.py,
pneuma_lanes.py), TomTom tile/incident map-matching to OSM edges (tiles_to_edges.py, incidents_to_edges.py),
street markets (laiki_edges.py), air-quality patterns as long-history ground truth (airq*.py: NOx increments track
traffic per day, not per hour), micromobility (gbfs_report.py), Mapillary vs OSM signals (mapillary_signals.py),
OSRM time-sliced matrices (osrm_speeds.py, osrm_matrix.py, zone_matrix.py).
| Path | Content |
|---|---|
results/signals/pneuma_cycles.png, pneuma_junctions.csv, pneuma_timing.csv | per-junction cycles and green/red splits derived from pNEUMA (CC BY 4.0) |
results/signals/busval_summary.csv, busval_bias.csv | bus-degradation grid and release lag by queue position |
results/piraeus_forecast/ | forecast metrics per week/model, run log, hour-of-week / monthly / backtest figures |
results/airq/ | EEA air-quality patterns and the counter-vs-NOx check |
results/sim/scenarios_first_pass.json | first-pass scenario totals (single seed, completed-trip accounting — see §3 for why that is biased) |
Requirements: Python ≥ 3.12. Collectors: pip install -r requirements-collectors.txt (mostly stdlib). Analysis:
pip install -r requirements-analysis.txt (includes eclipse-sumo).
cp .env.example .env # fill in only the keys you have; never commit .env
set -a; . ./.env; set +a
# collector host
python3 collectors/keycheck.py # checks each key with one cheap call, never prints keys
python3 collectors/collector.py # bus GPS + motorway detectors + weather (runs forever)
deploy/run_collector.sh tomtom_tiles.py status # any other collector, with .env loaded
# see deploy/crontab.example and deploy/*.service for the full schedule
# analysis machine ($DATA_DIR holds everything heavy)
deploy/sync_from_collector.sh # nightly pull of finished collector days
cd analysis
python osm_extract.py $DATA_DIR/greece-latest.osm.pbf $DATA_DIR/osm
python osm_edges.py $DATA_DIR/greece-latest.osm.pbf $DATA_DIR/osm/edges.parquet
python bus_speeds.py all && python signals.py 10
python pneuma.py convert --hz 5 # pNEUMA zip -> parquet (needs the dataset, see DATA.md)
python pneuma_signals.py crossings && python pneuma_signals.py timing
python -m pytest -q # unit tests (< 2 s); -m smoke runs a small SUMO net
Configuration is by environment variable only: COLLECTOR_DIR, DATA_DIR, COLLECTOR_HOST / COLLECTOR_PATH,
OASA_AOI / OASA_FOCUS, SIM_AREA / SIM_BBOX, SIM_VENV_BIN, LLM endpoints and the API keys listed in
.env.example. Heavy pipelines need their input data (see DATA.md) and will not run on a fresh checkout.
See DATA.md for every source with its licence/terms and required citation. In short: OpenStreetMap (ODbL), pNEUMA (CC BY 4.0, cite Barmpounakis & Geroliminis 2020), OASA telematics and data.gov.gr open data, Diavgeia public acts, Open-Meteo/ERA5 (CC BY 4.0), EEA air quality; TomTom, HERE and airport data were used only under their free-tier terms and are not redistributed.
Research direction, questions, source selection, rules and verification: John Vrachnis, as an MSc-level research project at the University of Piraeus (Department of Informatics). The code was written with AI assistance: Claude Code agents implemented most scripts under John's direction and review, and a local Qwen model performs the LLM extraction steps. Git history of the original working copy does not record per-line authorship.
Data credits: "Data source: pNEUMA – open-traffic.epfl.ch" (Barmpounakis & Geroliminis, 2020); © OpenStreetMap contributors; OASA; Municipality of Piraeus / data.gov.gr; Diavgeia; Open-Meteo.com; EEA.
Code: MIT (see LICENSE). Data: not included; each source keeps its own licence (DATA.md).
Attica traffic lab: bus-GPS map-matching and speed profiles, traffic-light cycle detection validated on pNEUMA drone data, SUMO what-ifs, count forecasting and LLM extraction of Greek traffic decrees
Python
0
1 commits
updated Sep 26, 2026
A research lab for understanding and predicting road traffic in Attica (Athens and Piraeus, Greece) from open and free-tier data: a fleet of small collectors that record feeds which keep no history, and analysis code that turns them into road speeds, timetable delays, signal timings, simulations, forecasts and structured traffic rules.
Athens publishes no live road-speed feed, no signal timing plans and no traffic rules as data; its open loop-detector feed stopped in 2026. The lab tests how far public bus GPS, municipal counters, drone datasets, official decisions and a local LLM can close that gap.
Status: research code, first pass. The numbers below come from a few days of collection (September 2026) and are labelled with their sample sizes. Nothing here is production software.
| Path | What |
|---|---|
collectors/ | 24/7 collectors for the "collector host" (stdlib Python; config and keys from environment variables) |
analysis/ | analysis scripts for the "analysis machine": map-matching, speeds, delays, signals, SUMO, forecasts |
analysis/trafficsim/ | SUMO simulation package (network, demand, scenarios, scoring, health gate, job runner, Optuna calibration) |
analysis/simtools/ | simulation diagnostics and DUA helper scripts |
analysis/tests/ | unit tests (pytest) and an opt-in SUMO smoke test |
deploy/ | systemd unit templates, an example crontab, the nightly sync script |
results/ | small derived result files and figures (see "Results") |
DATA.md | every data source with licence / terms and citation |
analysis/signals.py, analysis/pneuma_signals.py, analysis/pneuma_busval.py, analysis/pneuma.py
OASA buses report their position every ~20–30 s. For every OSM-signalised junction on a bus route, a pass that has two standing fixes 5–120 m before the junction and then a fix past it is a "queued at red, then released" event. Events are pooled per junction × approach across all lines, corrected for the release lag of the queue position, and their times are folded modulo candidate cycles C = 50…160 s. The C that maximises the weighted mean resultant length R (Rayleigh statistic) is the cycle; a near-tie rule prefers common plan lengths {80, 90, 100, 120, 135} s.
Ground truth comes from the pNEUMA drone dataset (EPFL, 2018, ~1.0 M vehicle crossings):
signals.py is a Greenwood–Durand series truncated after its first
correction term; it goes negative for strong peaks and ignores that the best of 111 candidate cycles was picked.
Use Zar's (1999) approximation p = exp(sqrt(1 + 4n + 4(n² − R_n²)) − (1 + 2n)) with R_n = nR, and a
look-elsewhere (max-over-grid) null. With a Monte-Carlo max-R null, 96 of 133 series stay significant at
0.05, and every exotic cycle (59, 94, 109, 131 s …) falls out, including both overlap misses: the detector knows
when it does not know. (These two corrections were computed in a separate check and are not yet patched into
signals.py.)analysis/osm_edges.py, analysis/bus_speeds.py, analysis/calibrate.py, analysis/profiles.py,
analysis/gtfs_delay.py, analysis/pick_calib_points.py
ln(car) = a + b·ln(bus) against TomTom Flow Segment readings at 24 well-covered
edges (468 pairs, 21 points, 3 days): leave-one-day-out MAE 14.5 → 10.1 km/h (pooled model
ln car = 1.413 + 0.679 ln bus). Per-class fits are not yet stable. No TomTom readings are published here.analysis/sim_piraeus.py, analysis/trafficsim/, analysis/demand_v2.py, analysis/build_ff_net.py,
analysis/junction_delay.py, analysis/sim_eval.py, analysis/sumo_smoke.sh, analysis/simtools/
SUMO 1.27 mesoscopic models built from OSM (netconvert with guessed/joined signals), demand from an in-house
hourly activity model (population, jobs, POIs, visitors, freight) with one global scale k, calibrated against
aggregated TomTom congestion levels.
analysis/piraeus_model.py, analysis/f1_forecast.py; figures in results/piraeus_forecast/
Hourly vehicle counts from five Municipality of Piraeus counters (10-min bins since 01/2026), rolling-origin test over the last 8 weeks: LightGBM (Poisson; calendar, Greek holidays, school periods, events, same-hour-last-week lag) reaches WAPE 21.4 % vs 26.3 % for "same hour last week" and 35.6 % for the hour-of-week mean. The gap is largest in the 15-August week (26.8 % vs 40.8 %). Weather makes it slightly worse, and football matches at the nearby stadiums do not move these counters (median ratio 0.989 within ±3 h of 41 games): the gates follow port operations.
collectors/diavgeia.py, collectors/rules_hunter.py, collectors/events/ (events.py, events_llm.py,
events_geo.py), analysis/rules_edges.py, analysis/build_street_index.py, analysis/diavgeia_orgs.py,
analysis/bench_gliner.py, analysis/qwen_scout.py, analysis/scout_verify.py
temperature 0, response_format: json_schema) extracts dated
disruption events (events_llm.py: schema + system prompt) and standing city rules (rules_hunter.py: 19 rule
types, status, vehicles, days, time windows, validity, confidence and a ≤ 200-char verbatim quote).analysis/sunglare.py
NOAA solar-position algorithm (with refraction) against the bearing of every main-road edge direction in Attica (238,686 edges, 384,754 edge-directions): glare = sun 0–20° above the horizon and within ±25° of the heading. Glare hits 2.34 % of edge-direction × week × 15-min slots, and winter has ~24 % more glare slots than midsummer. First check against evening bus speeds: no measurable effect (ratio 0.997 vs 0.991) — an honest null.
Activity model (activity_*.py, census_pop.py, elstat_pop.py, visitors.py, poi_extract.py,
overture_*.py, fsq_attica.py), static edge features (edge_features.py, eff_lanes_model.py,
pneuma_lanes.py), TomTom tile/incident map-matching to OSM edges (tiles_to_edges.py, incidents_to_edges.py),
street markets (laiki_edges.py), air-quality patterns as long-history ground truth (airq*.py: NOx increments track
traffic per day, not per hour), micromobility (gbfs_report.py), Mapillary vs OSM signals (mapillary_signals.py),
OSRM time-sliced matrices (osrm_speeds.py, osrm_matrix.py, zone_matrix.py).
| Path | Content |
|---|---|
results/signals/pneuma_cycles.png, pneuma_junctions.csv, pneuma_timing.csv | per-junction cycles and green/red splits derived from pNEUMA (CC BY 4.0) |
results/signals/busval_summary.csv, busval_bias.csv | bus-degradation grid and release lag by queue position |
results/piraeus_forecast/ | forecast metrics per week/model, run log, hour-of-week / monthly / backtest figures |
results/airq/ | EEA air-quality patterns and the counter-vs-NOx check |
results/sim/scenarios_first_pass.json | first-pass scenario totals (single seed, completed-trip accounting — see §3 for why that is biased) |
Requirements: Python ≥ 3.12. Collectors: pip install -r requirements-collectors.txt (mostly stdlib). Analysis:
pip install -r requirements-analysis.txt (includes eclipse-sumo).
cp .env.example .env # fill in only the keys you have; never commit .env
set -a; . ./.env; set +a
# collector host
python3 collectors/keycheck.py # checks each key with one cheap call, never prints keys
python3 collectors/collector.py # bus GPS + motorway detectors + weather (runs forever)
deploy/run_collector.sh tomtom_tiles.py status # any other collector, with .env loaded
# see deploy/crontab.example and deploy/*.service for the full schedule
# analysis machine ($DATA_DIR holds everything heavy)
deploy/sync_from_collector.sh # nightly pull of finished collector days
cd analysis
python osm_extract.py $DATA_DIR/greece-latest.osm.pbf $DATA_DIR/osm
python osm_edges.py $DATA_DIR/greece-latest.osm.pbf $DATA_DIR/osm/edges.parquet
python bus_speeds.py all && python signals.py 10
python pneuma.py convert --hz 5 # pNEUMA zip -> parquet (needs the dataset, see DATA.md)
python pneuma_signals.py crossings && python pneuma_signals.py timing
python -m pytest -q # unit tests (< 2 s); -m smoke runs a small SUMO net
Configuration is by environment variable only: COLLECTOR_DIR, DATA_DIR, COLLECTOR_HOST / COLLECTOR_PATH,
OASA_AOI / OASA_FOCUS, SIM_AREA / SIM_BBOX, SIM_VENV_BIN, LLM endpoints and the API keys listed in
.env.example. Heavy pipelines need their input data (see DATA.md) and will not run on a fresh checkout.
See DATA.md for every source with its licence/terms and required citation. In short: OpenStreetMap (ODbL), pNEUMA (CC BY 4.0, cite Barmpounakis & Geroliminis 2020), OASA telematics and data.gov.gr open data, Diavgeia public acts, Open-Meteo/ERA5 (CC BY 4.0), EEA air quality; TomTom, HERE and airport data were used only under their free-tier terms and are not redistributed.
Research direction, questions, source selection, rules and verification: John Vrachnis, as an MSc-level research project at the University of Piraeus (Department of Informatics). The code was written with AI assistance: Claude Code agents implemented most scripts under John's direction and review, and a local Qwen model performs the LLM extraction steps. Git history of the original working copy does not record per-line authorship.
Data credits: "Data source: pNEUMA – open-traffic.epfl.ch" (Barmpounakis & Geroliminis, 2020); © OpenStreetMap contributors; OASA; Municipality of Piraeus / data.gov.gr; Diavgeia; Open-Meteo.com; EEA.
Code: MIT (see LICENSE). Data: not included; each source keeps its own licence (DATA.md).