Gridlock / README.md
parvmittal07's picture
Initial Demo complete
01eb82e
|
Raw
History Blame Contribute Delete
12.7 kB
metadata
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 β†’ end timestamps 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_path are populated only when a closure/diversion segment is drawn, so has_end_point alone "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. Includes status, all resolution timestamps, resolved_at_*, comment, and critically endlatitude / 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 corridor column 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_COMPONENTS vs EMBED_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.