Spaces:
Running
Running
| """Generate baseline_overview.pdf for §3 from output_item50. | |
| 3 x 2 layout combining catalogue heterogeneity with mechanism: | |
| Left column (3 panels) -- raw_item_series.png style: each row is | |
| one catalogue item showing demand, | |
| served, and unmet overlaid over the | |
| full T = 52,560-day horizon. Items: | |
| I01, I20, I40 sample the catalogue. | |
| Right column (3 panels) -- internal network state for the focal | |
| item I01 in a 5-year zoom window | |
| centred on the largest macro-shock: | |
| (b) destination on-hand inventory | |
| (d) destination backlog | |
| (f) last-mile edge utilisation | |
| (PHL->NYC, BAL->NYC) with 0.95 | |
| saturation threshold | |
| The yellow band on each left panel marks the right column's window. | |
| Together the two columns expose both the catalogue's per-item | |
| heterogeneity and the mechanism that produces fill-rate drop events. | |
| """ | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| REPO = Path(__file__).resolve().parents[1] | |
| DATA = REPO / "data" / "output_item50" | |
| OUT = REPO / "results" / "figures" | |
| # ---------- styling ---------- | |
| plt.rcParams.update({ | |
| "font.family": "serif", | |
| "font.size": 10, | |
| "axes.labelsize": 9.5, | |
| "axes.titlesize": 9.5, | |
| "axes.titleweight": "bold", | |
| "xtick.labelsize": 8.5, | |
| "ytick.labelsize": 8.5, | |
| "legend.fontsize": 8.0, | |
| "mathtext.fontset": "cm", | |
| "axes.spines.top": False, | |
| "axes.spines.right": False, | |
| "axes.grid": True, | |
| "grid.alpha": 0.22, | |
| "grid.linewidth": 0.5, | |
| "lines.linewidth": 0.55, | |
| }) | |
| C_DEMAND = "#3a7cb8" | |
| C_SERVED = "#d97706" | |
| C_UNMET = "#b91c1c" | |
| C_ONHAND = "#0e7c66" | |
| C_BACKLOG = "#7c3aed" | |
| C_PHIL = "#c0392b" | |
| C_BALT = "#e67e22" | |
| C_THRESH = "#666666" | |
| C_WIN = "#f1c40f" | |
| ITEMS_LEFT = ["I01", "I02", "I03"] | |
| FOCAL = "I01" | |
| # ---------- load ---------- | |
| print("Loading daily records ...") | |
| records = pd.read_csv(DATA / "daily_records.csv") | |
| records["unmet"] = records["demand"] - records["served_from_stock"] | |
| demand_pv = records.pivot(index="day", columns="item", values="demand") | |
| served_pv = records.pivot(index="day", columns="item", | |
| values="served_from_stock") | |
| unmet_pv = records.pivot(index="day", columns="item", values="unmet") | |
| onhand_pv = records.pivot(index="day", columns="item", | |
| values="dest_on_hand_end_before_ship") | |
| backlog_pv = records.pivot(index="day", columns="item", | |
| values="dest_backlog_end_before_ship") | |
| T = demand_pv.shape[0] | |
| print(f"Horizon T = {T} days; left items: {ITEMS_LEFT}; " | |
| f"focal item: {FOCAL}") | |
| # edge utilisation | |
| edge_util = np.load(DATA / "edge_utilisation.npy") | |
| edge_list = pd.read_csv(DATA / "edge_list.csv") | |
| phil_idx = int(edge_list.index[(edge_list["from"] == "Philadelphia") | |
| & (edge_list["to"] == "NewYork")][0]) | |
| balt_idx = int(edge_list.index[(edge_list["from"] == "Baltimore") | |
| & (edge_list["to"] == "NewYork")][0]) | |
| # ---------- zoom window ---------- | |
| agg = demand_pv.sum(axis=1).values | |
| WIN = 5 * 365 | |
| roll = pd.Series(agg).rolling(WIN, center=True).mean() | |
| peak = int(roll.idxmax()) | |
| start = max(0, peak - WIN // 2) | |
| end = min(T, start + WIN) | |
| start = max(0, end - WIN) | |
| print(f"Zoom window: days {start}..{end} (peak at day {peak})") | |
| # focal-item slices | |
| foc_onhand = onhand_pv[FOCAL].values | |
| foc_backlog = backlog_pv[FOCAL].values | |
| foc_onhand_z = foc_onhand[start:end] | |
| foc_backlog_z = foc_backlog[start:end] | |
| util_phil_z = edge_util[start:end, phil_idx] | |
| util_balt_z = edge_util[start:end, balt_idx] | |
| # ---------- figure ---------- | |
| fig, axes = plt.subplots(3, 2, figsize=(13, 7.4), | |
| gridspec_kw={"hspace": 0.55, "wspace": 0.20, | |
| "width_ratios": [1.7, 1.0]}) | |
| panel_letters_left = ["a", "c", "e"] | |
| # -------- left column: per-item demand/served/unmet -------- | |
| for r, item in enumerate(ITEMS_LEFT): | |
| d = demand_pv[item].values | |
| s = served_pv[item].values | |
| u = unmet_pv[item].values | |
| fr = s.sum() / max(d.sum(), 1e-9) | |
| ymax = max(d.max(), s.max()) * 1.10 | |
| ax = axes[r, 0] | |
| ax.fill_between(np.arange(T), 0, u, color=C_UNMET, alpha=0.18, | |
| lw=0) | |
| ax.plot(np.arange(T), d, color=C_DEMAND, lw=0.45, alpha=0.85, | |
| label="demand" if r == 0 else None) | |
| ax.plot(np.arange(T), s, color=C_SERVED, lw=0.45, alpha=0.85, | |
| label="served" if r == 0 else None) | |
| ax.plot(np.arange(T), u, color=C_UNMET, lw=0.45, alpha=0.95, | |
| label="unmet" if r == 0 else None) | |
| ax.axvspan(start, end, color=C_WIN, alpha=0.22, lw=0, | |
| label="zoom window" if r == 0 else None) | |
| ax.set_xlim(0, T - 1) | |
| ax.set_ylim(0, ymax) | |
| ax.set_ylabel(f"{item} demand (units)") | |
| ax.set_title(f"({panel_letters_left[r]}) {item} full horizon " | |
| f"($T={T}$, fill rate {fr:.3f})", loc="left") | |
| if r == 0: | |
| ax.legend(loc="upper right", ncol=4, frameon=False, | |
| columnspacing=1.0, handlelength=1.4) | |
| if r == 2: | |
| ax.set_xlabel("time unit") | |
| # -------- right column: focal-item mechanism in zoom -------- | |
| # y-axis scaled to the zoom-window range, not the full horizon, so | |
| # the dynamics inside the window are readable | |
| ymax_onh = max(1.0, foc_onhand_z.max() * 1.10) | |
| ymax_bk = max(1.0, foc_backlog_z.max() * 1.10) | |
| days_z = np.arange(start, end) | |
| # (b) destination on-hand for focal item | |
| ax = axes[0, 1] | |
| ax.fill_between(days_z, 0, foc_onhand_z, color=C_ONHAND, alpha=0.18, | |
| lw=0) | |
| ax.plot(days_z, foc_onhand_z, color=C_ONHAND, lw=0.85, alpha=0.95) | |
| ax.set_xlim(start, end - 1) | |
| ax.set_ylim(0, ymax_onh) | |
| ax.set_ylabel(f"{FOCAL} on-hand") | |
| ax.set_title(f"(b) Zoom: {FOCAL} destination on-hand inventory", | |
| loc="left") | |
| # (d) destination backlog for focal item | |
| ax = axes[1, 1] | |
| ax.fill_between(days_z, 0, foc_backlog_z, color=C_BACKLOG, alpha=0.18, | |
| lw=0) | |
| ax.plot(days_z, foc_backlog_z, color=C_BACKLOG, lw=0.85, alpha=0.95) | |
| ax.set_xlim(start, end - 1) | |
| ax.set_ylim(0, ymax_bk) | |
| ax.set_ylabel(f"{FOCAL} backlog") | |
| ax.set_title(f"(d) Zoom: {FOCAL} destination backlog", loc="left") | |
| # (f) last-mile edge utilisation in zoom | |
| ax = axes[2, 1] | |
| ax.plot(days_z, util_phil_z, color=C_PHIL, lw=0.85, alpha=0.95, | |
| label=r"PHL$\to$NYC") | |
| ax.plot(days_z, util_balt_z, color=C_BALT, lw=0.85, alpha=0.85, | |
| label=r"BAL$\to$NYC") | |
| ax.axhline(0.95, color=C_THRESH, ls="--", lw=0.9, alpha=0.7, | |
| label=r"$U=0.95$") | |
| ax.set_xlim(start, end - 1) | |
| ax.set_ylim(0, 1.05) | |
| ax.set_ylabel(r"$U_{e,t}$") | |
| ax.set_xlabel("time unit") | |
| ax.set_title("(f) Zoom: last-mile edge utilisation", loc="left") | |
| ax.legend(loc="upper left", ncol=3, frameon=False, | |
| columnspacing=1.0, handlelength=1.6) | |
| plt.savefig(OUT / "baseline_overview.pdf", bbox_inches="tight") | |
| plt.savefig(OUT / "baseline_overview.png", dpi=160, | |
| bbox_inches="tight") | |
| print(f"Saved {OUT / 'baseline_overview.pdf'}") | |
| print(f"Saved {OUT / 'baseline_overview.png'}") | |