immu4989's picture
mcp: logic.py
826cbe1 verified
Raw
History Blame Contribute Delete
16.8 kB
"""Inference layer for the demo Space.
Keeps Gradio out of the modeling code: this module trains (once, cached to
disk) small models from the use-case packages and exposes three functions the
UI calls — commit-miss risk with a per-shipment SHAP breakdown, ETA quantiles
with a promise recommendation, and the budgeted-intervention economics.
Models are trained on the packages' own synthetic generators, so the Space
needs no data files and no network. Training is deliberately small (a cold
start is ~20s); results are cached under MODEL_CACHE so warm starts are
instant.
"""
from __future__ import annotations
import math
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import pandas as pd
# delivery-commit-prediction
from delivery_commit import cleaning as dc_cleaning
from delivery_commit import features as dc_features
from delivery_commit import schema as dc_schema
from delivery_commit import synthetic as dc_synthetic
from delivery_commit import train as dc_train
# eta-regression
from eta_regression import cleaning as eta_cleaning
from eta_regression import features as eta_features
from eta_regression import synthetic as eta_synthetic
from eta_regression import train as eta_train
# intervention-optimization
from intervention_opt import evaluate as iv_evaluate
from intervention_opt import synthetic as iv_synthetic
# Pre-trained models shipped with the deployed Space (built by build_models.py)
# so visitors never wait for a cold-start train. Absent in the git checkout,
# where the app trains on demand and caches instead — see _commit_models.
BUNDLED = Path(__file__).parent / "models"
MODEL_CACHE = Path(__file__).parent / ".model_cache"
# Dark palette: the app is forced to dark mode (see app.py), so figures render
# transparent with light foreground and brightened accent colors that pop on a
# dark card.
INK = "#60a5fa" # blue accent (oracle / interval)
BAD = "#f87171" # raises risk
GOOD = "#4ade80" # lowers risk / the winning policy
NEUTRAL = "#94a3b8" # inert bars
FG = "#e5e7eb" # text, ticks, labels
GRID = "#334155" # spines, gridlines
_state: dict = {"commit": None, "eta": None, "shap": None, "iv_day": None}
def _style_dark(fig, axes) -> None:
"""Make a figure blend into the dark UI: transparent canvas, light text."""
fig.patch.set_alpha(0.0)
for ax in (axes if isinstance(axes, (list, tuple)) else [axes]):
ax.set_facecolor("none")
ax.title.set_color(FG)
ax.xaxis.label.set_color(FG)
ax.yaxis.label.set_color(FG)
ax.tick_params(colors=FG)
for spine in ax.spines.values():
spine.set_color(GRID)
def _cache_dir():
"""A writable cache dir, or None. Some hosts (HF Spaces) mount the app dir
read-only, in which case we skip caching and just retrain per cold start."""
try:
MODEL_CACHE.mkdir(exist_ok=True)
probe = MODEL_CACHE / ".probe"
probe.write_text("ok")
probe.unlink()
return MODEL_CACHE
except OSError:
return None
def _try_save(save_fn, models, cache, name) -> None:
if cache is not None:
try:
save_fn(models, cache / name)
except OSError:
pass
# ---------------------------------------------------------------------------
# Model loading (train once, cache)
# ---------------------------------------------------------------------------
def _load_bundled(load_fn, name):
"""Load a pre-trained model shipped with the Space, or None if absent."""
path = BUNDLED / name
if path.exists():
try:
return load_fn(path)
except Exception: # noqa: BLE001 - version skew etc. -> fall back to training
return None
return None
def build_commit(save_to=None):
"""Train the commit-risk models (used at cold start and by build_models.py)."""
raw = dc_synthetic.make_dataset(n=14000, seed=7, messy=True)
clean, _ = dc_cleaning.clean(raw)
models, _ = dc_train.train(clean, dc_train.TrainConfig(n_estimators=120, seed=7))
if save_to is not None:
dc_train.save(models, save_to)
return models
def build_eta(save_to=None):
"""Train the ETA quantile models (used at cold start and by build_models.py)."""
raw = eta_synthetic.make_dataset(n=14000, seed=7, messy=True)
clean, _ = eta_cleaning.clean(raw)
cfg = eta_train.TrainConfig(n_estimators=120, seed=7, quantiles=eta_train.PRODUCT_QUANTILES)
models, _ = eta_train.train(clean, cfg)
if save_to is not None:
eta_train.save(models, save_to)
return models
def _commit_models():
if _state["commit"] is None:
_state["commit"] = _load_bundled(dc_train.load, "commit")
if _state["commit"] is None:
cache = _cache_dir()
try:
_state["commit"] = dc_train.load(cache / "commit") if cache else None
if _state["commit"] is None:
raise FileNotFoundError
except Exception: # noqa: BLE001 - any load failure -> retrain from generator
models = build_commit()
_try_save(dc_train.save, models, cache, "commit")
_state["commit"] = models
return _state["commit"]
def _eta_models():
if _state["eta"] is None:
_state["eta"] = _load_bundled(eta_train.load, "eta")
if _state["eta"] is None:
cache = _cache_dir()
try:
_state["eta"] = eta_train.load(cache / "eta") if cache else None
if _state["eta"] is None:
raise FileNotFoundError
except Exception: # noqa: BLE001
models = build_eta()
_try_save(eta_train.save, models, cache, "eta")
_state["eta"] = models
return _state["eta"]
def _shap_explainer():
if _state["shap"] is None:
import shap
_state["shap"] = shap.TreeExplainer(_commit_models().xgb)
return _state["shap"]
def warmup() -> None:
"""Train/load everything up front so the first user click is fast."""
_commit_models()
_eta_models()
_shap_explainer()
# ---------------------------------------------------------------------------
# Shared shipment row
# ---------------------------------------------------------------------------
def _raw_row(
distance,
service_level,
origin_cong,
dest_cong,
weather,
minutes_after_cutoff,
peak,
rural,
dest_type,
) -> pd.DataFrame:
"""Build a one-row raw shipment covering every column both models need."""
if service_level == "overnight":
promised = 1
elif service_level == "two_day":
promised = 2
else:
promised = int(min(7, max(3, math.ceil(1 + distance / 600))))
row = {
dc_schema.ID_COL: "DEMO0001",
dc_schema.DATE_COL: pd.Timestamp("2025-06-03"),
"distance_miles": float(distance),
"package_weight_lb": 6.0,
"package_volume_cuft": 0.9,
"declared_value_usd": 150.0,
"minutes_after_cutoff": float(minutes_after_cutoff),
"origin_hub_congestion": float(origin_cong),
"dest_hub_congestion": float(dest_cong),
"dest_weather_severity": int(weather),
"route_stop_density": 4.5 if dest_type == "residential" else 2.5,
"promised_transit_days": promised,
"service_level": service_level,
"origin_region": "midwest",
"dest_region": "northeast" if distance > 400 else "midwest",
"dest_type": dest_type,
"day_of_week": 1,
"is_peak_season": int(peak),
"is_rural_dest": int(rural),
"signature_required": 0,
}
return pd.DataFrame([row])
# ---------------------------------------------------------------------------
# Tab 1a: commit-miss risk + SHAP
# ---------------------------------------------------------------------------
# NB: these callbacks use named, typed parameters (not *args) on purpose — the
# demo also runs as an MCP server, and the generated tool schemas come from the
# signatures and docstrings below.
def score_commit(
distance_miles: float,
service_level: str,
origin_hub_congestion: float,
dest_hub_congestion: float,
dest_weather_severity: int,
minutes_after_cutoff: float,
is_peak_season: bool,
is_rural_dest: bool,
dest_type: str,
):
"""Score one parcel's probability of missing its delivery commitment.
Uses only induction-time information. Args: distance_miles (5-3000),
service_level (overnight|two_day|ground), origin/dest hub congestion (0-1),
dest_weather_severity (0 clear .. 3 severe), minutes_after_cutoff (pickup
relative to facility cutoff; positive = late), is_peak_season,
is_rural_dest, dest_type (residential|commercial). Returns a markdown
verdict with the miss probability and a SHAP chart of the drivers.
"""
models = _commit_models()
raw = _raw_row(
distance_miles, service_level, origin_hub_congestion, dest_hub_congestion,
dest_weather_severity, minutes_after_cutoff, is_peak_season, is_rural_dest,
dest_type,
)
clean, _ = dc_cleaning.clean(raw)
X = dc_features.to_matrix(dc_features.engineer(clean))
X = X.reindex(columns=models.feature_columns, fill_value=0.0)
prob = float(models.xgb.predict_proba(X)[:, 1][0])
contrib = pd.Series(_shap_explainer().shap_values(X)[0], index=X.columns)
top = contrib.reindex(contrib.abs().sort_values(ascending=False).head(8).index)[::-1]
fig, ax = plt.subplots(figsize=(7, 4.2))
colors = [BAD if v > 0 else GOOD for v in top.values]
ax.barh(range(len(top)), top.values, color=colors)
ax.set_yticks(range(len(top)))
ax.set_yticklabels([_pretty(c) for c in top.index], fontsize=9)
ax.axvline(0, color=FG, lw=0.8, alpha=0.6)
ax.set_xlabel("push toward missing → (log-odds contribution) ← push toward on-time")
ax.set_title("Why this shipment scored the way it did")
_style_dark(fig, ax)
fig.tight_layout()
verdict = "high risk" if prob >= 0.35 else "elevated" if prob >= 0.15 else "low risk"
label = f"## Miss probability: {prob:.0%} \n**{verdict}** — network base rate is ~11%."
return label, fig
def _pretty(col: str) -> str:
names = {
"dest_weather_severity": "destination weather",
"distance_miles": "lane distance",
"total_hub_congestion": "total hub congestion",
"origin_hub_congestion": "origin congestion",
"dest_hub_congestion": "destination congestion",
"is_peak_season": "peak season",
"is_rural_dest": "rural destination",
"late_pickup": "late pickup",
"late_pickup_minutes": "minutes late at pickup",
"minutes_after_cutoff": "pickup vs cutoff",
"miles_per_promised_day": "miles per promised day",
"route_stop_density": "route stop density",
}
if col in names:
return names[col]
if col.startswith("service_level_"):
return col.replace("service_level_", "") + " service"
if col.startswith("dest_type_"):
return col.replace("dest_type_", "") + " destination"
return col.replace("_", " ")
# ---------------------------------------------------------------------------
# Tab 1b: ETA quantiles + promise
# ---------------------------------------------------------------------------
def predict_eta(
distance_miles: float,
service_level: str,
origin_hub_congestion: float,
dest_hub_congestion: float,
dest_weather_severity: int,
minutes_after_cutoff: float,
is_peak_season: bool,
is_rural_dest: bool,
dest_type: str,
):
"""Predict one parcel's transit time as a P10/P50/P90 quantile interval.
Same induction-time inputs as score_commit. Returns a markdown summary with
the median ETA, the honest P10-P90 range, and the promise (ceil of P90 —
the transit days you can quote and keep ~9 times in 10), plus an interval
chart.
"""
models = _eta_models()
raw = _raw_row(
distance_miles, service_level, origin_hub_congestion, dest_hub_congestion,
dest_weather_severity, minutes_after_cutoff, is_peak_season, is_rural_dest,
dest_type,
)
clean, _ = eta_cleaning.clean(raw)
X = eta_features.to_matrix(eta_features.engineer(clean))
X = X.reindex(columns=models.feature_columns, fill_value=0.0)
q = eta_train.predict_quantiles(models, X, alphas=(0.1, 0.5, 0.9)).iloc[0]
p10, p50, p90 = float(q[0.1]), float(q[0.5]), float(q[0.9])
promise = math.ceil(p90)
fig, ax = plt.subplots(figsize=(7, 1.9))
ax.hlines(0, p10, p90, color=INK, lw=6, alpha=0.5)
ax.plot(p50, 0, "o", color=INK, ms=12)
for x, lab in [(p10, f"P10\n{p10:.1f}d"), (p50, f"P50\n{p50:.1f}d"), (p90, f"P90\n{p90:.1f}d")]:
ax.annotate(lab, (x, 0), textcoords="offset points", xytext=(0, 12),
ha="center", fontsize=9, color=FG)
ax.axvline(promise, color=GOOD, ls="--", lw=1.5)
ax.annotate(f"promise {promise}d", (promise, 0), textcoords="offset points",
xytext=(0, -22), ha="center", color=GOOD, fontsize=9)
ax.set_ylim(-0.6, 0.6)
ax.set_yticks([])
ax.set_xlabel("transit days")
ax.set_title("Predicted transit-time interval")
_style_dark(fig, ax)
fig.tight_layout()
label = (
f"## Median ETA: {p50:.1f} days \n"
f"Honest range **{p10:.1f}{p90:.1f} days**. Quote **{promise} days** "
f"(ceil of P90) and you keep the promise ~9 times in 10."
)
return label, fig
# ---------------------------------------------------------------------------
# Tab 2: intervention economics
# ---------------------------------------------------------------------------
def _iv_day():
if _state["iv_day"] is None:
_state["iv_day"] = iv_synthetic.make_day(n=20000, seed=7)
return _state["iv_day"]
_POLICY_LABELS = {
"none": "Do nothing",
"random": "Random spend",
"top_k_risk": "Flag the riskiest (top-K)",
"expected_value_greedy": "Expected-value greedy",
"oracle": "Oracle (perfect scores)",
}
def run_budget(budget: float):
"""Allocate a daily intervention budget across 20,000 at-risk shipments.
Compares five policies at the given budget (USD, 1000-20000): do nothing,
random spend, flag-the-riskiest (top-K), expected-value greedy, and a
perfect-scores oracle. Returns a markdown takeaway, a net-savings chart,
and the full policy comparison table (spend, misses prevented, cost
avoided, net savings, ROI, % of oracle).
"""
import tempfile
df = _iv_day()
with tempfile.TemporaryDirectory() as tmp:
comparison, _ = iv_evaluate.evaluate_all(df, budget=float(budget), out_dir=tmp)
comp = comparison.set_index("policy")
order = ["none", "random", "top_k_risk", "expected_value_greedy", "oracle"]
comp = comp.reindex([p for p in order if p in comp.index])
fig, ax = plt.subplots(figsize=(7.5, 4.2))
net = comp["net_savings_usd"].to_numpy()
colors = [
GOOD if p == "expected_value_greedy" else NEUTRAL if p != "oracle" else INK
for p in comp.index
]
ax.bar(range(len(comp)), net, color=colors)
ax.set_xticks(range(len(comp)))
ax.set_xticklabels([_POLICY_LABELS[p] for p in comp.index], rotation=20, ha="right", fontsize=9)
ax.axhline(0, color=FG, lw=0.8, alpha=0.6)
ax.set_ylabel("net savings on the day ($)")
ax.set_title(f"Every policy, same ${budget:,.0f} budget")
for i, v in enumerate(net):
va = "bottom" if v >= 0 else "top"
ax.annotate(f"${v:,.0f}", (i, v), textcoords="offset points",
xytext=(0, 4 if v >= 0 else -6), ha="center", va=va, fontsize=8, color=FG)
ax.margins(y=0.15)
_style_dark(fig, ax)
fig.tight_layout()
tbl = comp.reset_index()
tbl["policy"] = tbl["policy"].map(_POLICY_LABELS)
tbl = tbl.rename(
columns={
"policy": "Policy",
"spend_usd": "Spend $",
"misses_prevented": "Misses prevented",
"miss_cost_avoided_usd": "Cost avoided $",
"net_savings_usd": "Net savings $",
"roi": "ROI",
"pct_of_oracle": "% of oracle",
}
)
for c in ["Spend $", "Cost avoided $", "Net savings $"]:
tbl[c] = tbl[c].map(lambda x: f"{x:,.0f}")
greedy = comp.loc["expected_value_greedy"]
topk = comp.loc["top_k_risk", "net_savings_usd"]
takeaway = (
f"### Expected-value greedy nets ${greedy['net_savings_usd']:,.0f} "
f"({greedy['roi']:.1f}× ROI, {greedy['pct_of_oracle']:.0f}% of the oracle) — "
f"**${greedy['net_savings_usd'] - topk:,.0f} more** than flagging the riskiest shipments, "
f"because a 90% risk on a $5 parcel is worth less than a 40% risk on a contract pallet."
)
return takeaway, fig, tbl