Spaces:
Running
title: Gridlock Traffic Intelligence
emoji: π¦
colorFrom: blue
colorTo: green
sdk: docker
app_port: 8000
pinned: false
license: mit
Gridlock β Event-Driven Congestion Forecasting & Resource Recommendation
Forecasting the traffic impact of planned and unplanned road events in Bengaluru (from the anonymised Astram event log) and turning those forecasts into concrete operational recommendations: manpower, barricading and diversion.
The dataset ships with no ready-made "impact" label, so the core of this project is (a) engineering defensible targets, (b) ruthless leakage control, and (c) skew-aware modelling and evaluation.
1. Problem framing
How can historical and real-time data be used to forecast event-related traffic impact and recommend optimal manpower, barricading and diversion plans?
"Impact" is decomposed into three forecastable targets, each computable only from information available when an event is first reported:
| Task | Target | Type | Drives |
|---|---|---|---|
| T1 Road closure | y_road_closure β will the event need a closure / diversion? |
binary (β7% positive) | barricading + diversion |
| T2 Priority | y_high_priority β High vs Low operational priority |
binary (β62% positive) | manpower tier |
| T3 Duration | y_duration_min β how long until cleared (minutes) |
regression (heavy-tailed) | manpower + interval |
Three separately tuned models over one shared, leakage-safe feature pipeline (multi-task by construction, not a single fragile multi-head net).
2. Why this is not a trivial classification task
- Severe class skew β closures are ~7% of events, so accuracy is useless. The whole evaluation is skew-aware (PR-AUC / average precision, F-beta, MCC, balanced accuracy, Brier calibration).
- No target column β duration is reconstructed by coalescing
resolved β closed β endtimestamps minus start, then cleaned of non-positive and automated-batch-closure rows. - Leakage everywhere β many columns are only filled in after the event is
resolved. The single biggest trap: the end-point coordinates and
route_pathare populated only when a closure/diversion segment is drawn, sohas_end_pointalone "predicts" closure at AP β 0.98. These are treated as leakage and excluded (see below). - Multilingual free text β descriptions mix English, transliterated Kannada and native Kannada script, often stating the impact directly ("road closed", "slow moment", "traffic normal"). Encoded with a multilingual sentence-transformer plus an interpretable bilingual lexicon.
- Bimodal duration β minor incidents clear in minutesβhours; construction runs for daysβweeks. Handled with a log target, winsorised point model and uncapped quantile models for honest prediction intervals.
3. Leakage control (the most important part)
Columns are partitioned in src/config.py:
ID_COLUMNSβ opaque identifiers, dropped.LEAKAGE_COLUMNSβ known only after the event unfolds; never features. Includesstatus, all resolution timestamps,resolved_at_*,comment, and criticallyendlatitude/endlongitude/end_address/route_path(the closure/diversion geometry β a consequence of the decision we predict).- History features (
hist_hotspot_count,loc_event_density,same_loc_cause_hist,same_day_loc_reports) are computed strictly causally β each row only sees earlier-reported events. - All fitted transforms (categorical vocabularies, embedding PCA, numeric medians, calibration, decision thresholds) are learned on training rows only; the chronological test set is never touched until final scoring.
- The literal
corridorcolumn is excluded from the priority model only (PRIORITY_EXCLUDE_FEATURES) because it makes that label a trivial 1-field lookup.
Note on priority (T2): even without
corridor, priority is ~deterministic from location (it essentially encodes "is this on a designated priority corridor?"), which is legitimately knowable at report time. So its high score reflects an genuinely easy geofencing task, not leakage β verified via feature-importance and per-junction purity checks. The hard ML problems are closure (T1) and duration (T3).
4. Pipeline
raw CSV
ββ data_loading.py read as strings, strip whitespace
ββ cleaning.py parse datetimes, fix coords, flag auto-batch closures
ββ targets.py build y_road_closure / y_high_priority / y_duration_min
ββ feature_engineering temporal + cyclical + spatial(geo_cluster, causal hotspot)
β + recurrence + causal target-rate + bilingual lexicon + missingness
ββ text_features.py multilingual sentence-transformer embeddings (cached)
ββ preprocessing.py train-fitted assembly: native categs + per-task embedding PCA
ββ splits.py chronological train/test + time-series & stratified folds
ββ models.py Optuna-tuned LightGBM + XGBoost + CatBoost
β β OOF logistic stack β isotonic calibration β F-beta threshold
β duration: log point model + p10/p50/p90 quantile intervals
ββ evaluate.py skew-aware metrics + operating points + PR/calibration/SHAP
ββ train.py / train_best.py full 3-task run / focused best closure model
ββ predict.py / recommend.py inference β manpower / barricading / diversion
Modelling highlights
- Stacked ensemble of three decorrelated gradient-boosters combined by a logistic meta-learner trained on out-of-fold predictions.
- Imbalance handled at the threshold, not the loss. Counter-intuitively, the
textbook
scale_pos_weight = neg/pos(β12.4 here) hurt ranking β it inflates recall but distorts the probability surface, dropping PR-AUC. We instead leave the loss unweighted (scale_pos_weight = 1) and absorb the skew purely in the decision threshold, which lifted test PR-AUC 0.302 β 0.317. The threshold itself is a policy choice β src/train_best.py reports the recall-, F1-, F2- and MCC-optimal operating points so the control room picks where to sit on the curve (e.g. max-recall to never miss a closure vs. MCC-optimal for balance). - Task-specific embedding width. The 384-dim multilingual embeddings are the
single strongest signal, so the PCA width is tuned per task. Closure trains
on all rows and keeps 96 components (validated PR-AUC 0.31 β 0.33); duration
has only ~2.5k labelled rows, where wide projections overfit, so it keeps a
narrow 32 (
CLOSURE_EMB_PCA_COMPONENTSvsEMBED_PCA_COMPONENTS). This alone is the largest single lever on closure ranking. - Causal target-rate features. Past-only, empirical-Bayes-smoothed closure
rates per cause / corridor / police-station / geo-cluster / zone / pincode
(
shift(1)so a row never sees its own label) give the rare closure target far more signal than the static category, and a rolling "ambient duration level" tracks the heavy non-stationarity in clearance times. The accumulated history is persisted (history.parquet) so inference reproduces the exact training-time encodings. Together with the wider embedding the deployed closure model reaches PR-AUC 0.326 and duration log-RΒ² 0.251. - Probability calibration (isotonic) so the recommendation thresholds act on trustworthy probabilities.
- Uncertainty for duration: quantile models give an 80% prediction interval, not just a point estimate.
5. Results (chronological hold-out)
Train: 2023-11-09 β 2024-03-14 (nβ6446) Β· Test: 2024-03-14 β 2024-04-08 (nβ1611). Full metrics in reports/metrics.json; figures in reports/figures.
T1 β Road closure (test positive rate 7.2%; accuracy is meaningless here):
| Metric | Value | Read as |
|---|---|---|
| Average precision (PR-AUC) | 0.326 | 4.5Γ the 7.2% base rate |
| ROC-AUC | 0.835 | ranks closures well above non-closures |
| Recall | 0.655 | catches ~2/3 of real closures⦠|
| Precision | 0.277 | β¦at the deliberately recall-favoured F2 threshold (0.10) |
| F2 / MCC | 0.515 / 0.360 | recall-weighted; see operating points below |
| Brier | 0.057 | calibrated probabilities |
Confusion @ deployed threshold: TP 76, FP 198, FN 40, TN 1297 β i.e. of 116 real closures, 76 are pre-flagged for barricading while only 40 are missed. Base learners (OOF AP): LightGBM 0.380, XGBoost 0.392, CatBoost 0.389 β the stack combines them to OOF AP 0.396.
The threshold is a policy choice, not a fixed model property. A single recall-favoured threshold makes MCC look low even though the ranking (PR-AUC) is unchanged. The deployed model's full trade-off (in reports/metrics.json; src/train_best.py prints the same for a focused re-tune):
Operating point Threshold Recall Precision F1 F2 MCC MCC-optimal (balanced) 0.213 0.509 0.399 0.447 0.482 0.402 F2-optimal (recall-leaning) 0.097 0.655 0.277 0.390 0.515 0.360 Recall β₯ 0.8 (never miss) 0.057 0.828 0.151 0.255 0.436 0.247 So the same model scores MCC β 0.40 at its balanced operating point β the headline 0.36 is simply measured where recall is deliberately favoured.
T2 β Priority (genuinely easy report-time geofencing, see note above):
| Metric | Value |
|---|---|
| Average precision | 0.984 |
| F1 / balanced-acc | 0.963 / 0.946 |
| MCC / ROC-AUC | 0.901 / 0.981 |
| Brier | 0.039 |
T3 β Duration (heavy-tailed minutes; 630 valid test events):
| Metric | Value | Note |
|---|---|---|
| Median abs. error | 74 min | typical incident error |
| MAE / RMSE | 2411 / 5581 min | inflated by multi-week construction tail |
| MAE (log scale) | 1.44 | the honest central-tendency error |
| RΒ² (log scale) | 0.251 | honest fit on the heavy-tailed target (raw-minute RΒ² 0.09 is dominated by 140-day outliers) |
| 80% interval coverage | 0.78 | conformalized β β nominal 80% |
| Median interval width | 119 min | actionable uncertainty band |
The closure model's value is best read as lift over the 7% base rate at high recall β exactly what a control room needs to pre-stage barricades. The duration model's median 73-min error and calibrated 80% interval give a usable clearance estimate, while the (expected) large MAE honestly reflects the days-long construction tail.
6. Usage
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# Build everything and train (Optuna-tuned). Artifacts land in models/, reports/.
python -m src.train
# Fast smoke run (skip tuning) / offline text features:
GRIDLOCK_NO_TUNE=1 python -m src.train
GRIDLOCK_NO_TRANSFORMER=1 python -m src.train
# Focused best closure model + full operating-point trade-off table:
python -m src.train_best # tuned; saves closure_model_best.joblib
GRIDLOCK_CLOSURE_SPW=3 python -m src.train_best # trade some AP for more recall
# Inference + operational recommendations on raw event rows:
python -m src.predict
from src.predict import predict_events
from src.data_loading import load_raw
recs = predict_events(load_raw().tail(20))
print(recs[["closure_probability", "manpower_tier", "officers_suggested",
"barricading", "diversion", "expected_duration_min"]])
7. Repository layout
src/ config, loading, cleaning, targets, features, text, splits,
preprocessing, models, evaluate, predict, recommend, train
data/raw/ astram_events.csv (copy of the provided file)
data/processed/ cleaned + feature parquets, cached embeddings
models/ trained artifacts (*.joblib)
reports/ metrics.json + figures/ (PR, calibration, SHAP)
8. Possible extensions
External holiday/festival calendar and weather joins; survival analysis for still-active (right-censored) events; an online-updating hotspot feed; a serving API / control-room dashboard.