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