Gridlock / README.md
parvmittal07's picture
Initial Demo complete
01eb82e
|
Raw
History Blame Contribute Delete
12.7 kB
---
title: Gridlock Traffic Intelligence
emoji: 🚦
colorFrom: blue
colorTo: green
sdk: docker
app_port: 8000
pinned: false
license: mit
---
<!-- The block above is read by Hugging Face Spaces (Docker SDK). It builds the
root Dockerfile and routes traffic to app_port (8000). Everything below is
rendered as the Space description. -->
# 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](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](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](reports/metrics.json); figures in
[reports/figures](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](reports/metrics.json); [src/train_best.py](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
```bash
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
```
```python
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.