Gridlock / src /targets.py
parvmittal07's picture
Initial Demo complete
01eb82e
Raw
History Blame Contribute Delete
2.91 kB
"""Target engineering for the three prediction tasks.
T1 y_road_closure : bool -> barricading / diversion need (imbalanced ~7%)
T2 y_high_priority : bool -> manpower tier (priority == "High")
T3 y_duration_min : float -> impact duration in minutes (regression)
Duration is derived from post-event timestamps (the only place it exists) but
those timestamps are NEVER used as model features. The duration label is built
by coalescing resolved -> closed -> end (most reliable first). Rows that were
auto-closed by the nightly batch job, are still active, or have a non-positive
duration are marked invalid and excluded from regression training only.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from . import config as C
def _coalesced_end(df: pd.DataFrame) -> pd.Series:
"""Best available clearance timestamp: resolved > closed > end."""
cols = ["resolved_datetime", "closed_datetime", "end_datetime"]
present = [c for c in cols if c in df.columns]
return df[present].bfill(axis=1).iloc[:, 0]
def build_targets(df: pd.DataFrame, save: bool = True) -> pd.DataFrame:
df = df.copy()
# ---- T1: road closure (barricading / diversion) --------------------- #
df[C.TARGET_CLOSURE] = df["requires_road_closure"].fillna(False).astype(int)
# ---- T2: high priority (manpower tier) ------------------------------ #
df[C.TARGET_PRIORITY] = (
df["priority"].astype(str).str.strip().str.lower().eq("high").astype(int)
)
# ---- T3: impact duration (minutes) ---------------------------------- #
end = _coalesced_end(df)
duration = (end - df["start_datetime"]).dt.total_seconds() / 60.0
valid = (
duration.gt(C.DURATION_MIN_MINUTES)
& (~df["auto_resolved_flag"].fillna(False))
& df["status"].astype(str).str.lower().ne("active")
)
df[C.TARGET_DURATION] = np.where(valid, duration, np.nan)
df["duration_valid"] = valid.astype(bool)
if save:
df.to_parquet(C.CLEAN_PARQUET, index=False)
return df
def winsorized_log_duration(y_min: pd.Series, upper_cap: float | None = None):
"""Return (log1p target, fitted upper cap). Cap is computed ONLY on the
values passed in (the trainer passes the training split to avoid leakage).
"""
y = y_min.dropna()
if upper_cap is None:
upper_cap = float(np.quantile(y, C.DURATION_WINSOR_UPPER_Q))
capped = np.clip(y_min, a_min=None, a_max=upper_cap)
return np.log1p(capped), upper_cap
if __name__ == "__main__": # pragma: no cover
d = pd.read_parquet(C.CLEAN_PARQUET)
d = build_targets(d)
print("closure positive rate:", d[C.TARGET_CLOSURE].mean().round(4))
print("high-priority rate :", d[C.TARGET_PRIORITY].mean().round(4))
print("duration valid rows :", int(d["duration_valid"].sum()))
print(d.loc[d.duration_valid, C.TARGET_DURATION].describe().round(1))