Gridlock / app /backend /precompute.py
parvmittal07's picture
Initial Demo complete
01eb82e
Raw
History Blame Contribute Delete
11.2 kB
"""Offline precompute: score every historical event once and write the static
JSON the Map, Top-Areas and Models tabs read at runtime.
Outputs (in app/backend/generated/):
* options.json - dropdown vocab + police-station centroids + map bounds
* areas.json - per police-station aggregates (table + map markers)
* map.json - ~110 m hotspot cells + per-event points for heat layers
* metrics.json - combined model metrics for the Models tab
Run from the repo root with the venv active:
python -m app.backend.precompute
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from app.backend.inference import _json_safe, get_service, three_level_manpower # noqa: E402
GENERATED_DIR = Path(__file__).resolve().parent / "generated"
GENERATED_DIR.mkdir(parents=True, exist_ok=True)
UNKNOWN_STATION = "Unknown / No Station"
GRID_DECIMALS = 3 # ~110 m cell, matches the hotspot model's location unit
def _write(name: str, obj) -> None:
path = GENERATED_DIR / name
with open(path, "w", encoding="utf-8") as fh:
json.dump(_json_safe(obj), fh, ensure_ascii=False, separators=(",", ":"))
size_kb = path.stat().st_size / 1024
print(f" wrote {name} ({size_kb:.0f} KB)")
def build_master(service) -> pd.DataFrame:
"""One row per scored event with geo, actual labels and all predictions."""
from src.cleaning import clean
from src.data_loading import load_raw
from src.targets import build_targets
print("[precompute] loading + scoring all events (transformer embeddings)...")
raw = load_raw()
# Geo + admin + actual labels come from the hotspot loader (keeps id and all
# raw columns, types coordinates) so we can group/plot reliably.
HM = service.HM
geo = HM.load_and_clean() # id, latitude, longitude, police_station, junction, zone, event_cause, requires_road_closure(1/0), priority
# Main predictions (closure / priority / duration / manpower).
main = service.predictor.predict_frame(raw)
main = main.rename(columns={"event_id": "id"})
# Hotspot risk computed causally over the FULL frame (each event sees only
# its past) - the faithful way to replay the model, no history double-count.
feats, _, _ = HM.assemble_features(geo)
feats = HM.apply_category_dtypes(feats, service.hotspot_bundle["cat_dtypes"])[
service.hotspot_bundle["feature_cols"]
]
raw_proba = service.hotspot_bundle["model"].predict_proba(feats)[:, 1]
risk = service.hotspot_bundle["isotonic"].predict(raw_proba)
thr = float(service.hotspot_bundle["threshold"])
geo = geo.copy()
geo["hotspot_risk"] = risk
geo["hotspot_flag"] = (risk >= thr).astype(int)
# Actual clearance duration (heavy-tailed; only some rows are valid).
tdf = build_targets(clean(raw, save=False), save=False)
dur = tdf[["id", "y_duration_min", "duration_valid"]].copy()
cols_geo = [
"id", "latitude", "longitude", "police_station", "junction", "zone",
"event_cause", "requires_road_closure", "priority", "hotspot_risk",
"hotspot_flag",
]
cols_main = [
"id", "closure_probability", "high_priority_probability",
"expected_duration_min", "officers_suggested", "manpower_tier",
]
master = (
geo[cols_geo]
.merge(main[cols_main], on="id", how="inner")
.merge(dur, on="id", how="left")
)
master["manpower_level"] = master["manpower_tier"].map(three_level_manpower)
master["is_high_priority"] = (master["priority"].astype(str).str.lower() == "high").astype(int)
master["requires_road_closure"] = pd.to_numeric(
master["requires_road_closure"], errors="coerce"
).fillna(0)
master["police_station"] = master["police_station"].fillna(UNKNOWN_STATION).replace(
{"": UNKNOWN_STATION}
)
print(f"[precompute] master frame: {len(master):,} events")
return master
def _top_causes(series: pd.Series, n: int = 3) -> list[str]:
vc = series.dropna().astype(str)
vc = vc[vc.str.lower() != "nan"]
return vc.value_counts().head(n).index.tolist()
def aggregate_areas(master: pd.DataFrame) -> list[dict]:
areas = []
for name, g in master.groupby("police_station"):
n = len(g)
valid_dur = g.loc[g["duration_valid"] == True, "y_duration_min"] if "duration_valid" in g else pd.Series(dtype=float)
closure_rate = float(g["requires_road_closure"].mean())
pred_closure = float(g["closure_probability"].mean())
high_pri_rate = float(g["is_high_priority"].mean())
pred_high_pri = float(g["high_priority_probability"].mean())
avg_hotspot = float(g["hotspot_risk"].mean())
risk_score = round(
100.0 * (0.45 * pred_closure + 0.35 * avg_hotspot + 0.20 * pred_high_pri), 1
)
level_counts = g["manpower_level"].value_counts()
areas.append({
"area": name,
"n_events": int(n),
"lat": round(float(g["latitude"].median()), 5),
"lng": round(float(g["longitude"].median()), 5),
"closure_rate": round(closure_rate, 4),
"pred_closure_rate": round(pred_closure, 4),
"high_priority_rate": round(high_pri_rate, 4),
"pred_high_priority_rate": round(pred_high_pri, 4),
"avg_duration_min": round(float(valid_dur.median()), 1) if len(valid_dur) else None,
"pred_avg_duration_min": round(float(g["expected_duration_min"].median()), 1),
"avg_hotspot_risk": round(avg_hotspot, 4),
"chronic_count": int(g["hotspot_flag"].sum()),
"chronic_rate": round(float(g["hotspot_flag"].mean()), 4),
"avg_officers": round(float(g["officers_suggested"].mean()), 2),
"manpower_high": int(level_counts.get("high", 0)),
"manpower_medium": int(level_counts.get("medium", 0)),
"manpower_low": int(level_counts.get("low", 0)),
"risk_score": risk_score,
"top_causes": _top_causes(g["event_cause"]),
})
areas.sort(key=lambda a: a["n_events"], reverse=True)
return areas
def build_map(master: pd.DataFrame) -> dict:
# ~110 m hotspot cells (the model's own location unit).
m = master.dropna(subset=["latitude", "longitude"]).copy()
m["glat"] = m["latitude"].round(GRID_DECIMALS)
m["glng"] = m["longitude"].round(GRID_DECIMALS)
cells = []
for (glat, glng), g in m.groupby(["glat", "glng"]):
count = len(g)
max_risk = float(g["hotspot_risk"].max())
if count < 2 and max_risk < 0.2:
continue # skip lonely low-risk cells to keep the layer crisp
label = g["police_station"].mode().iat[0] if len(g["police_station"].mode()) else UNKNOWN_STATION
junctions = g["junction"].dropna().astype(str)
junctions = junctions[junctions.str.lower() != "nan"]
cells.append({
"lat": round(float(glat), 3),
"lng": round(float(glng), 3),
"count": int(count),
"max_risk": round(max_risk, 4),
"mean_risk": round(float(g["hotspot_risk"].mean()), 4),
"chronic_count": int(g["hotspot_flag"].sum()),
"closure_rate": round(float(g["requires_road_closure"].mean()), 4),
"label": label,
"junction": junctions.mode().iat[0] if len(junctions.mode()) else None,
"top_cause": _top_causes(g["event_cause"], 1)[0] if _top_causes(g["event_cause"], 1) else None,
})
cells.sort(key=lambda c: c["max_risk"], reverse=True)
# Per-event points for the heat layers (compact arrays: lat,lng,closure,hotspot,officers).
pts = [
[round(float(r.latitude), 4), round(float(r.longitude), 4),
round(float(r.closure_probability), 3),
round(float(r.hotspot_risk), 3) if pd.notna(r.hotspot_risk) else 0.0,
int(r.officers_suggested)]
for r in m.itertuples()
]
bounds = {
"min_lat": round(float(m["latitude"].min()), 4),
"max_lat": round(float(m["latitude"].max()), 4),
"min_lng": round(float(m["longitude"].min()), 4),
"max_lng": round(float(m["longitude"].max()), 4),
"center_lat": round(float(m["latitude"].median()), 4),
"center_lng": round(float(m["longitude"].median()), 4),
}
print(f"[precompute] map: {len(cells):,} hotspot cells, {len(pts):,} points")
return {
"point_fields": ["lat", "lng", "closure_prob", "hotspot_risk", "officers"],
"points": pts,
"hotspot_cells": cells,
"bounds": bounds,
}
def build_options(service, master: pd.DataFrame, areas: list[dict]) -> dict:
bounds = {
"center_lat": round(float(master["latitude"].median()), 4),
"center_lng": round(float(master["longitude"].median()), 4),
"min_lat": round(float(master["latitude"].min()), 4),
"max_lat": round(float(master["latitude"].max()), 4),
"min_lng": round(float(master["longitude"].min()), 4),
"max_lng": round(float(master["longitude"].max()), 4),
}
station_centroids = [
{"name": a["area"], "lat": a["lat"], "lng": a["lng"], "n_events": a["n_events"]}
for a in areas if a["area"] != UNKNOWN_STATION
]
return {
"categories": service.category_options(),
"stations": station_centroids,
"bounds": bounds,
}
def build_metrics(master: pd.DataFrame) -> dict:
from src import config as C
def _load(path: Path):
return json.loads(path.read_text()) if path.exists() else {}
reports = _load(C.REPORTS_DIR / "metrics.json")
hotspot = _load(ROOT / "hotspot_artifacts" / "hotspot_metrics.json")
closure_best = _load(C.REPORTS_DIR / "closure_best_operating_points.json")
dataset = {
"n_events_scored": int(len(master)),
"n_areas": int(master["police_station"].nunique()),
"closure_base_rate": round(float(master["requires_road_closure"].mean()), 4),
"high_priority_base_rate": round(float(master["is_high_priority"].mean()), 4),
"chronic_rate": round(float(master["hotspot_flag"].mean()), 4),
"date_span": "9 Nov 2023 - 8 Apr 2024 (~150 days)",
}
return {
"dataset": dataset,
"priority": reports.get("priority", {}),
"closure": reports.get("closure", {}),
"duration": reports.get("duration", {}),
"hotspot": hotspot,
"closure_best_operating_points": closure_best,
}
def main() -> None:
print("=" * 64)
print("Gridlock precompute")
print("=" * 64)
service = get_service()
master = build_master(service)
areas = aggregate_areas(master)
map_data = build_map(master)
options = build_options(service, master, areas)
metrics = build_metrics(master)
_write("areas.json", areas)
_write("map.json", map_data)
_write("options.json", options)
_write("metrics.json", metrics)
print("[precompute] done.")
if __name__ == "__main__":
main()