Gridlock / src /train.py
parvmittal07's picture
Initial Demo complete
01eb82e
Raw
History Blame Contribute Delete
6.42 kB
"""End-to-end training orchestrator.
Run with ``python -m src.train``. Optional flags via environment variables:
GRIDLOCK_NO_TUNE=1 skip Optuna (fast smoke run with default params)
GRIDLOCK_NO_TRANSFORMER=1 use TF-IDF text features instead of the transformer
Produces, under models/ and reports/:
closure_model.joblib, priority_model.joblib, duration_model.joblib,
preprocessor_full.joblib, preprocessor_priority.joblib,
metrics.json, plus PR/calibration/SHAP figures in reports/figures/.
"""
from __future__ import annotations
import os
import time
import joblib
import numpy as np
import pandas as pd
from . import config as C
from . import evaluate as E
from . import models as M
from .feature_engineering import build_features, save_history
from .preprocessing import Preprocessor
from .splits import stratified_folds, temporal_split, time_series_folds, split_report
from .targets import build_targets, winsorized_log_duration
from .text_features import compute_embeddings
TUNE = os.environ.get("GRIDLOCK_NO_TUNE", "0") != "1"
def _load_dataset() -> pd.DataFrame:
df = pd.read_parquet(C.CLEAN_PARQUET)
if C.TARGET_CLOSURE not in df.columns:
df = build_targets(df, save=False)
if "event_family" not in df.columns:
df = build_features(df, save=False)
return df.reset_index(drop=True)
def _train_classification(df, emb, train_mask, test_mask, target, beta):
# Closure benefits from a wider embedding projection (more rows to fit it);
# priority is saturated and keeps the default narrow PCA.
n_emb = C.CLOSURE_EMB_PCA_COMPONENTS if target == C.TARGET_CLOSURE else None
prep = Preprocessor(target=target, n_emb_components=n_emb)
X_tr_raw, X_te_raw = df[train_mask], df[test_mask]
emb_tr, emb_te = emb[train_mask], emb[test_mask]
prep.fit(X_tr_raw, emb_tr)
X_tr = prep.transform(X_tr_raw, emb_tr)
X_te = prep.transform(X_te_raw, emb_te)
y_tr = df.loc[train_mask, target].to_numpy()
y_te = df.loc[test_mask, target].to_numpy()
oof_folds = stratified_folds(y_tr)
tune_folds = time_series_folds(len(y_tr))
cat_features = prep.categorical_feature_names
t0 = time.time()
artifact, diag = M.train_classification_task(
X_tr, y_tr, X_te, cat_features, oof_folds, tune_folds,
target=target, tune=TUNE, beta=beta,
)
test_prob = artifact.predict_proba(X_te)
metrics = E.classification_metrics(y_te, test_prob, artifact.threshold, beta=beta)
metrics["operating_points"] = E.operating_points(y_te, test_prob)
metrics["oof"] = diag
metrics["train_seconds"] = round(time.time() - t0, 1)
name = "closure" if target == C.TARGET_CLOSURE else "priority"
E.plot_pr_calibration(y_te, test_prob, name)
E.plot_shap_summary(artifact.base_models["lightgbm"], X_te, name)
return artifact, prep, metrics
def _train_duration(df, emb, train_mask, test_mask):
valid = df["duration_valid"].to_numpy() if "duration_valid" in df else df[C.TARGET_DURATION].notna().to_numpy()
tr = train_mask & valid
te = test_mask & valid
prep = Preprocessor(target=C.TARGET_DURATION)
prep.fit(df[train_mask], emb[train_mask]) # fit on all train rows for stability
X_tr = prep.transform(df[tr], emb[tr])
X_te = prep.transform(df[te], emb[te])
y_tr_min = df.loc[tr, C.TARGET_DURATION].to_numpy()
y_te_min = df.loc[te, C.TARGET_DURATION].to_numpy()
_, cap_minutes = winsorized_log_duration(pd.Series(y_tr_min)) # cap from TRAIN only
tune_folds = time_series_folds(len(y_tr_min))
t0 = time.time()
artifact, diag = M.train_duration_task(X_tr, y_tr_min, cap_minutes, tune_folds, tune=TUNE)
point = artifact.predict_minutes(X_te)
quant = artifact.predict_quantiles(X_te)
metrics = E.regression_metrics(y_te_min, point, quantile_preds=quant)
metrics["lgb_params"] = diag["lgb_params"]
metrics["train_seconds"] = round(time.time() - t0, 1)
metrics["n_train"] = int(tr.sum())
return artifact, prep, metrics
def main():
print(f"[train] tuning={'ON' if TUNE else 'OFF'} transformer={'ON' if C.USE_TRANSFORMER else 'OFF'}")
df = _load_dataset()
emb = compute_embeddings(df)
train_mask, test_mask = temporal_split(df)
print(split_report(df, train_mask, test_mask))
all_metrics = {}
print("\n[train] === road-closure classification ===")
closure_art, closure_prep, closure_m = _train_classification(
df, emb, train_mask, test_mask, C.TARGET_CLOSURE, beta=2.0)
all_metrics["closure"] = closure_m
print(f" AP={closure_m['average_precision']:.3f} (lift x{closure_m['ap_lift_over_base']:.1f}) "
f"recall={closure_m['recall']:.3f} precision={closure_m['precision']:.3f} MCC={closure_m['mcc']:.3f}")
print("\n[train] === priority classification ===")
priority_art, priority_prep, priority_m = _train_classification(
df, emb, train_mask, test_mask, C.TARGET_PRIORITY, beta=1.0)
all_metrics["priority"] = priority_m
print(f" AP={priority_m['average_precision']:.3f} F1={priority_m['f1']:.3f} "
f"bal_acc={priority_m['balanced_accuracy']:.3f} MCC={priority_m['mcc']:.3f}")
print("\n[train] === duration regression ===")
duration_art, duration_prep, duration_m = _train_duration(df, emb, train_mask, test_mask)
all_metrics["duration"] = duration_m
print(f" n_test={duration_m['n']} MAE={duration_m['mae_min']:.0f}min "
f"median_AE={duration_m['median_ae_min']:.0f}min "
f"R2_log={duration_m.get('r2_log', float('nan')):.3f} "
f"coverage80={duration_m.get('interval_coverage_80', float('nan')):.2f}")
# Persist artifacts.
joblib.dump(closure_art, C.MODELS_DIR / "closure_model.joblib")
joblib.dump(priority_art, C.MODELS_DIR / "priority_model.joblib")
joblib.dump(duration_art, C.MODELS_DIR / "duration_model.joblib")
joblib.dump(closure_prep, C.MODELS_DIR / "preprocessor_full.joblib")
joblib.dump(priority_prep, C.MODELS_DIR / "preprocessor_priority.joblib")
joblib.dump(duration_prep, C.MODELS_DIR / "preprocessor_duration.joblib")
# Persist the labeled history so inference reproduces the same past-only
# causal target-rate encodings the models trained on.
save_history(df)
E.save_metrics(all_metrics, "metrics.json")
print("\n[train] artifacts + metrics saved.")
return all_metrics
if __name__ == "__main__":
main()