P4Learned Delivery Routing
1 Codebase

eval.py

delivery/eval.py · 93 lines · Download

1"""Evaluation: prediction metrics and the replay simulation on held-out real trips.2 3Replay: a plan (fixed stop sequences per van, made with some time model) is driven again,4but each leg i -> j departing at time t takes a duration drawn from REAL test-week trips with5the same (pickup zone, drop-off zone, hour), times the van factor. Pools with fewer than6`min_pool` trips back off to hour +-1, then to all hours of that zone pair. The van still7waits if early. We count late deliveries (service start after the window closes).8Orders a plan could not assign count as late (unserved).9"""10from __future__ import annotations11 12from collections import defaultdict13 14import numpy as np15 16from . import config as C17from .search import simulate18from .integration import van_problem19 20 21def metrics(y_true, y_pred) -> dict:22    e = np.asarray(y_pred) - np.asarray(y_true)23    return {"MAE": float(np.abs(e).mean()), "RMSE": float(np.sqrt((e ** 2).mean())),24            "bias": float(e.mean()), "n": int(len(e))}25 26 27def metrics_by_band(trips, pred) -> dict:28    bands = {"night 0-5": (0, 6), "morning 6-9": (6, 10), "midday 10-15": (10, 16),29             "evening 16-19": (16, 20), "late 20-23": (20, 24)}30    out = {}31    for name, (a, b) in bands.items():32        m = (trips.hour.values >= a) & (trips.hour.values < b)33        out[name] = metrics(trips.duration_min.values[m], np.asarray(pred)[m])34    return out35 36 37class ReplaySampler:38    def __init__(self, test_trips, factor: float = C.VAN_FACTOR, min_pool: int = 3):39        self.factor, self.min_pool = factor, min_pool40        self.cell = defaultdict(list)41        self.pair = defaultdict(list)42        for i, j, h, d in zip(test_trips.pu.values, test_trips["do"].values,43                              test_trips.hour.values, test_trips.duration_min.values):44            self.cell[(int(i), int(j), int(h))].append(float(d))45            self.pair[(int(i), int(j))].append(float(d))46        self.cell = {k: np.array(v) for k, v in self.cell.items()}47        self.pair = {k: np.array(v) for k, v in self.pair.items()}48        self.backoffs = defaultdict(int)49 50    def pool(self, i, j, t):51        h = int(t // 60) % 2452        p = self.cell.get((i, j, h))53        if p is not None and len(p) >= self.min_pool:54            return p55        near = [self.cell.get((i, j, hh)) for hh in (h - 1, h, h + 1)]56        near = [x for x in near if x is not None]57        if near and sum(len(x) for x in near) >= self.min_pool:58            self.backoffs["hour+-1"] += 159            return np.concatenate(near)60        if (i, j) in self.pair:61            self.backoffs["all-hours"] += 162            return self.pair[(i, j)]63        self.backoffs["reverse-pair"] += 164        return self.pair.get((j, i), np.array([10.0]))65 66    def travel_fn(self, rng):67        def f(i, j, t):68            p = self.pool(int(i), int(j), t)69            return float(p[rng.integers(len(p))]) * self.factor70        return f71 72 73def replay(inst, result, sampler: ReplaySampler, tm, n_reps: int = 30, seed: int = 0) -> dict:74    """Late deliveries when the plan's sequences are driven with real held-out durations."""75    rng = np.random.default_rng(seed)76    served = {i for r in result.routes.values() for i in r.sequence}77    unserved = len(inst.orders) - len(served)78    lates, minutes = [], []79    for _ in range(n_reps):80        fn = sampler.travel_fn(rng)81        n_late, tot = unserved, 0.082        for v in inst.vans:83            r = result.routes.get(v.id)84            if r is None or not r.sequence:85                continue86            prob = van_problem(inst, tm, v, r.sequence)87            sim = simulate(prob, list(range(len(r.sequence))), travel=fn)88            n_late += sim.n_late89            tot += sum(sim.lateness)90        lates.append(n_late); minutes.append(tot)91    return {"late_mean": float(np.mean(lates)), "late_std": float(np.std(lates)),92            "late_minutes_mean": float(np.mean(minutes)), "unserved": unserved,93            "late_frac": float(np.mean(lates)) / max(1, len(inst.orders))}