Spaces:
Running
Running
| """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() | |