# Gridlock — Model Report & Implementation Guide A complete account of everything built for the Astram event dataset: the predicted variables, the models behind each, every metric on the chronological hold-out, which implementation is best for each task, and the exact single commands to reproduce the best models. - **Dataset:** `astram_events.csv` — 8,173 raw → 8,057 clean Bengaluru traffic events, 9 Nov 2023 → 8 Apr 2024 (150-day span, 46 raw columns). - **Two independent implementations:** 1. **Main pipeline** (`src/`, run with `python -m src.train`) — three operational targets over one shared, leakage-safe feature pipeline. 2. **Standalone hotspot model** (`hotspot_model.py`) — a fourth, newly-engineered forward-looking target, fully self-contained. - **Evaluation discipline (all tasks):** chronological train→test split (train on the past, test on the future), skew-aware metrics, leakage-controlled features, probability calibration, and decision thresholds chosen on held-out data only. --- ## 0. TL;DR — the four tasks at a glance | # | Predicted variable | Type (positive rate) | Best implementation | Headline metric | Single command | |---|--------------------|----------------------|---------------------|-----------------|----------------| | **T1** | `y_road_closure` — will the event need a closure/diversion? | Binary (7.2%) | Main pipeline | **PR-AUC 0.326** (4.5× base), MCC 0.40 @ balanced | `python -m src.train` | | **T2** | `y_high_priority` — High vs Low operational priority | Binary (62.2%) | Main pipeline | **PR-AUC 0.984**, MCC 0.90 | `python -m src.train` | | **T3** | `y_duration_min` — minutes until cleared | Regression (heavy-tailed) | Main pipeline | **log-R² 0.251**, median AE 74 min | `python -m src.train` | | **T4** | `y_hotspot` — will this ~110 m spot reoffend (≥2 events in 14 days)? | Binary (14.1%) | Standalone hotspot model | **PR-AUC 0.441** (2.8× base), recall 0.88 | `python hotspot_model.py train` | > **Get every best model in one line:** > ```bash > python -m src.train && python hotspot_model.py train > ``` > The first command trains and deploys the best **T1/T2/T3** models; the second > trains and deploys the best **T4** model. Details in [§6](#6-how-to-get-the-best-models). --- ## 1. The two implementations ### 1A. Main pipeline — `src/` (`python -m src.train`) Three **separately tuned models over one shared feature pipeline** (multi-task by construction, not a single fragile multi-head net). Each model is a **stacked ensemble**: ``` Optuna-tuned LightGBM ┐ Optuna-tuned XGBoost ├─► logistic meta-learner on out-of-fold preds Optuna-tuned CatBoost ┘ └─► isotonic calibration └─► decision threshold ``` - **Duration** uses the same ensemble on a `log1p` target for the point estimate, plus **p10/p50/p90 quantile models** with a conformal correction for an honest 80% prediction interval. - **Shared features:** temporal + cyclical encodings, spatial bins (`geo_cluster` KMeans), causal (past-only) hotspot/recurrence counts, empirical-Bayes causal target-rate encodings, multilingual sentence-transformer embeddings (per-task PCA width), a bilingual impact lexicon, and missingness flags. ### 1B. Standalone hotspot model — `hotspot_model.py` A single self-contained file (no import from `src/`) with its own cleaning, target, features, training and prediction CLI. One **calibrated LightGBM** classifier on a strictly causal feature set, with an isotonic calibrator and a recall-favouring F2 threshold. Artifacts live under `hotspot_artifacts/`. --- ## 2. T1 — Road closure (`y_road_closure`) **What it predicts:** at report time, will this event require a road closure or diversion? Drives **barricading and diversion** planning. **Model:** stacked LightGBM + XGBoost + CatBoost → logistic meta → isotonic calibration → F2 threshold. Embedding PCA width **96** (closure-specific). **Test set:** n = 1,611, positive rate **7.2%** (≈116 real closures). | 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 recall-favoured deployed threshold (0.10) | | F2 / F1 | 0.515 / 0.390 | recall-weighted | | MCC | 0.360 | at deployed threshold (0.40 at balanced point) | | Balanced accuracy | 0.761 | | | Brier | 0.057 | well-calibrated probabilities | | Confusion @ deploy | TP 76 · FP 198 · FN 40 · TN 1297 | 76/116 closures pre-flagged, 40 missed | **Base learners (OOF AP):** LightGBM 0.380 · XGBoost 0.392 · CatBoost 0.389 → stack **0.396**. **The threshold is a policy choice** — the same model, scored at different operating points (from [reports/metrics.json](reports/metrics.json)): | 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, deployed) | 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 | **Why it's hard (and honest):** the strongest raw closure predictors (end-point coordinates / `route_path`) are *consequences* of the closure decision and were removed as leakage; closure is partly discretionary; and there is real temporal drift (OOF AP 0.40 → test 0.33). The PR-AUC of 0.326 is genuine signal at 4.5× the base rate. --- ## 3. T2 — Priority (`y_high_priority`) **What it predicts:** High vs Low operational priority. Drives the **manpower tier**. **Model:** same stacked ensemble; `corridor` deliberately **excluded** (it makes the label a trivial one-field lookup). Embedding PCA width 32. **Test set:** n = 1,611, positive rate **62.2%**. | Metric | Value | |--------|-------| | **Average precision (PR-AUC)** | **0.984** | | ROC-AUC | 0.980 | | F1 | 0.963 | | Precision / Recall | 0.951 / 0.975 | | Balanced accuracy | 0.946 | | MCC | 0.901 | | Brier | 0.040 | | Confusion @ deploy (thr 0.38) | TP 977 · FP 50 · FN 25 · TN 559 | **Base learners (OOF AP):** LightGBM 0.991 · XGBoost 0.988 · CatBoost 0.986 → stack **0.989**. **Note:** even without `corridor`, priority is near-deterministic from *location* (it encodes "is this on a designated priority corridor?"), which is legitimately knowable at report time. The high score reflects a **genuinely easy geofencing task, not leakage** — verified via feature importance and per-junction purity. This is the "good" task with little headroom left. --- ## 4. T3 — Duration (`y_duration_min`) **What it predicts:** minutes from start until the event is cleared (reconstructed by coalescing `resolved → closed → end` minus start, cleaned of non-positive and automated batch-closure rows). Drives the **manpower estimate + clearance interval**. **Model:** stacked ensemble on a `log1p` target (winsorised p99) for the point estimate, plus p10/p50/p90 quantile models with conformal correction for the interval. Embedding PCA width 32. **Test set:** n = 630 valid events (train n = 2,568). | Metric | Value | Note | |--------|-------|------| | **R² (log scale)** | **0.251** | honest fit on the heavy-tailed target | | Median absolute error | **74 min** | typical incident error | | MAE (log scale) | 1.44 | central-tendency error | | MAE / RMSE (raw min) | 2411 / 5581 | inflated by the multi-week construction tail | | R² (raw min) | 0.092 | misleading — dominated by 140-day outliers | | MAPE | 17.2 | | | 80% interval coverage | **0.78** | conformalized → ≈ nominal 80% | | Median interval width | 119 min | actionable uncertainty band | **Why two R² numbers:** raw-minute R² (0.09) is dominated by a handful of days-to-weeks construction events; the **log-scale R² (0.251)** is the honest measure of central-tendency fit, and the 74-min median error is what a control room actually experiences on normal incidents. --- ## 5. T4 — Chronic hotspot early warning (`y_hotspot`) — NEW **What it predicts (engineered from scratch):** at the moment an event is reported at a location, will that **same ~110 m spot generate ≥ 2 more events within the next 14 days?** This is a *recurring-hotspot early warning* — instead of repeatedly firefighting the same junction/pothole/water-logging spot, the control room gets a flag to send a **root-cause fix** (drainage, resurfacing, a permanent marshal). **Model:** single calibrated LightGBM (isotonic + F2 threshold), built entirely on strictly past-only causal features. **Test set:** n = 1,383, base rate **15.6%** (overall positive rate 14.1% across 6,914 labelable rows). | Metric | Value | Read as | |--------|-------|---------| | **Average precision (PR-AUC)** | **0.441** | **2.8× the 15.6% base rate** | | ROC-AUC | 0.790 | | | Brier | 0.106 | | | Recall (deployed F2, thr 0.071) | **0.875** | catches 189 of 216 emerging hotspots | | Precision (deployed) | 0.259 | early-warning favours recall | | F1 / MCC (deployed) | 0.400 / 0.299 | | | Confusion @ deploy | TP 189 · FP 541 · FN 27 · TN 626 | only 27 emerging hotspots missed | Alternative balanced operating point (MCC-optimal, thr 0.138): recall 0.593, precision 0.414, MCC **0.381**, F2 0.546. **Leakage safety:** every feature uses only strictly-earlier events; the label uses only strictly-later events (disjoint time windows); right-censored rows (without a full 14-day future) are dropped from train/test. **Top features** (gain): `junc_win30`, `zone_win30`, `ps_win30`, `police_station`, lat/long, `loc_days_since_first/last`, `area_to_loc_ratio`, `loc_rate_per_day`. **Face validity:** the highest-risk events are recurring **construction at HAL Old Airport** (metro work) and **potholes at J.P. Nagar** — exactly the chronic spots worth a permanent fix. Honest caveat: brand-new locations (no prior history) almost never turn chronic (6 positives in 383 first-ever sites), and the model correctly assigns them low risk rather than inventing signal. --- ## 6. How to get the best models ### Best implementation per task | Task | Best implementation | Command | Why | |------|---------------------|---------|-----| | **T1 Road closure** | Main pipeline (`src/`) | `python -m src.train` | Deploys the spw = 1 + PCA-96 config (PR-AUC 0.326), the best validated closure model. | | **T2 Priority** | Main pipeline (`src/`) | `python -m src.train` | Only and best implementation (PR-AUC 0.984). | | **T3 Duration** | Main pipeline (`src/`) | `python -m src.train` | Only and best implementation (log-R² 0.251 + calibrated interval). | | **T4 Chronic hotspot** | Standalone (`hotspot_model.py`) | `python hotspot_model.py train` | Self-contained forward-looking model (PR-AUC 0.441). | ### Single command for the best models ```bash # from the repo root, with the venv active: source .venv/bin/activate # Best T1 + T2 + T3 (Optuna-tuned) AND best T4, one line: python -m src.train && python hotspot_model.py train ``` - `python -m src.train` → writes the 6 main model artifacts to `models/`, metrics to [reports/metrics.json](reports/metrics.json), and figures to `reports/figures/`. - `python hotspot_model.py train` → writes `hotspot_artifacts/hotspot_bundle.joblib`, `hotspot_history.parquet`, and `hotspot_metrics.json`. ### Inference (after training) ```bash # T1/T2/T3 — closure prob, manpower tier, officers, barricading, diversion, duration interval: python -m src.predict # T4 — score events and list the top emerging hotspots: python hotspot_model.py predict ``` ### Useful variants ```bash GRIDLOCK_NO_TUNE=1 python -m src.train # fast smoke run (skips Optuna) GRIDLOCK_NO_TRANSFORMER=1 python -m src.train # offline TF-IDF text fallback python -m src.train_best # focused closure model + operating-point table GRIDLOCK_CLOSURE_SPW=3 python -m src.train_best # trade some PR-AUC for more closure recall ``` --- ## 7. Leakage control (applies to all tasks) - **`LEAKAGE_COLUMNS`** — anything known only after the event unfolds (`status`, all resolution timestamps, `resolved_at_*`, `comment`) is never a feature. The biggest trap: **end-point coordinates / `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 excluded as leakage. - **Causal features only** — every history/recurrence/target-rate feature sees strictly earlier events (`shift(1)` / append-after accumulators). - **Train-only fitting** — categorical vocabularies, embedding PCA, calibration and thresholds are learned on training rows; the chronological test set is untouched until final scoring. - **`corridor` excluded from T2 only** — it makes priority a trivial lookup. --- ## 8. Artifact map | Path | Produced by | Contents | |------|-------------|----------| | `models/*.joblib` (6) | `python -m src.train` | T1/T2/T3 models, preprocessors, calibrators, thresholds | | `models/closure_model_best.joblib` | `python -m src.train_best` | focused closure model | | `models/geo_kmeans.joblib` | `python -m src.train` | spatial bin assignment for inference | | `data/processed/history.parquet` | `python -m src.train` | causal target-rate history for inference | | [reports/metrics.json](reports/metrics.json) | `python -m src.train` | full T1/T2/T3 metrics + operating points | | `reports/figures/*.png` | `python -m src.train` | PR, calibration, SHAP | | `hotspot_artifacts/hotspot_bundle.joblib` | `python hotspot_model.py train` | T4 model + isotonic + threshold + dtypes | | `hotspot_artifacts/hotspot_history.parquet` | `python hotspot_model.py train` | event log for causal feature replay | | `hotspot_artifacts/hotspot_metrics.json` | `python hotspot_model.py train` | full T4 metrics + operating points | --- ## 9. Summary - **Four predicted variables**, each engineered from a dataset that ships with no ready-made label, each forecastable strictly from report-time information. - **T2 (priority)** is the saturated "easy" task (0.984); **T1 (closure)** and **T3 (duration)** are the genuinely hard operational tasks, improved to PR-AUC 0.326 and log-R² 0.251 through per-task embedding width and causal target-rate features; **T4 (chronic hotspot)** is a new forward-looking early-warning model at PR-AUC 0.441 with 0.88 recall. - **One command per implementation** reproduces every best model: `python -m src.train && python hotspot_model.py train`.