1"""Train the numpy MLP, fit the two baselines, evaluate on the held-out test week,2and export the predicted zone-to-zone time matrices by hour.3 4 python train_nn.py5"""6import json7 8import numpy as np9 10from delivery import config as C11from delivery import plots as P12from delivery.baselines import DistanceLinear, HistoricalAverage, NNPredictor13from delivery.data import load_processed14from delivery.eval import metrics, metrics_by_band15from delivery.nn import TrainConfig, train16from delivery.travel_time import TimeModel, build_all_weekdays17 18 19def main():20 trips, zones, dist = load_processed()21 Z = len(zones)22 tr, va, te = (trips[trips.split == s] for s in ("train", "val", "test"))23 print(f"train {len(tr):,} val {len(va):,} test {len(te):,} zones {Z}")24 25 print("training NN (numpy MLP, Adam, early stopping)")26 cfg = TrainConfig(seed=C.SEED)27 model = train(tr, va, Z, dist, cfg)28 C.MODELS.mkdir(exist_ok=True)29 model.save(C.MODELS / "mlp.npz")30 31 hist = HistoricalAverage(Z).fit(tr)32 lin = DistanceLinear(dist).fit(tr)33 nn = NNPredictor(model)34 args = [te[c].values for c in ("pu", "do", "hour", "weekday")]35 preds = {"NN (MLP)": nn.predict(*args), "hist-avg (pair,hour)": hist.predict(*args),36 "distance-linear": lin.predict(*args)}37 preds_all = dict(preds, **{"global mean": np.full(len(te), tr.duration_min.mean())})38 y = te.duration_min.values39 res = {"test": {k: metrics(y, p) for k, p in preds_all.items()},40 "by_band": {k: metrics_by_band(te, p) for k, p in preds.items()},41 "train_config": cfg.__dict__, "epochs_run": len(model.history),42 "best_val_mae": float(min(h[2] for h in model.history)),43 "distance_linear_coef": {"intercept": float(lin.intercept_), "slope": float(lin.slope_)}}44 # rare night cells: where hist-avg had to back off45 rare = hist.counts[te.pu.values, te["do"].values, te.hour.values] < hist.min_count46 res["rare_cells"] = {"test_trips_in_rare_cells": int(rare.sum()),47 **{k: metrics(y[rare], p[rare]) for k, p in preds.items()}} if rare.any() else {}48 print("\n test-week results (minutes)")49 print(f" {'model':<24}{'MAE':>8}{'RMSE':>8}{'bias':>8}")50 for k, m in res["test"].items():51 print(f" {k:<24}{m['MAE']:8.3f}{m['RMSE']:8.3f}{m['bias']:8.3f}")52 53 C.OUTPUTS.mkdir(exist_ok=True)54 (C.OUTPUTS / "nn_results.json").write_text(json.dumps(res, indent=2))55 (C.ROOT / "report" / "results").mkdir(parents=True, exist_ok=True)56 (C.ROOT / "report" / "results" / "nn_results.json").write_text(json.dumps(res, indent=2))57 58 # time matrices (taxi minutes; the planner multiplies by VAN_FACTOR)59 mats = {name: build_all_weekdays(p, Z) for name, p in60 (("nn", nn), ("hist", hist), ("dist", lin))}61 np.savez_compressed(C.MODELS / "time_matrices.npz", **mats)62 print("FIFO check (|M[h+1]-M[h]| < 60):", {k: TimeModel(m[1]).is_fifo() for k, m in mats.items()})63 with open(C.OUTPUTS / "time_matrix_nn_tuesday.csv", "w") as f:64 f.write("hour,pu_idx,do_idx,pu_zone,do_zone,minutes\n")65 names = zones.sort_values("idx").zone.tolist()66 for h in range(24):67 for i in range(Z):68 for j in range(Z):69 f.write(f"{h},{i},{j},\"{names[i]}\",\"{names[j]}\",{mats['nn'][1, h, i, j]:.2f}\n")70 71 P.learning_curve(model.history, C.OUTPUTS / "nn_learning_curve.png")72 P.mae_by_hour(te, preds, C.OUTPUTS / "nn_mae_by_hour.png")73 P.pred_vs_actual(y, preds["NN (MLP)"], C.OUTPUTS / "nn_pred_vs_actual.png")74 P.matrix_heatmaps(mats["nn"][1], zones, C.OUTPUTS / "time_matrix_nn.png")75 print("saved models/mlp.npz, models/time_matrices.npz and figures in outputs/")76 77 78if __name__ == "__main__":79 main()