{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cad8b657",
   "metadata": {},
   "source": [
    "# Ch 25 — MLOps and Observability (notebook)\n",
    "\n",
    "`[← 24 ai-safety-and-red-team]` · **this notebook** · `[26 reading-papers →]`\n",
    "\n",
    "Runs top-to-bottom in ~1 min on free Colab CPU. Last verified 2026-06-11.\n",
    "\n",
    "**What you'll build**\n",
    "- A synthetic recommender's `data -> train -> serve -> monitor -> retrain` loop, in code, where the \"production\" stream has a drift event planted on a known day so every alarm has a ground truth to check against.\n",
    "- Population Stability Index, KL divergence, and a Kolmogorov-Smirnov test built from scratch and checked against `scipy`, then turned into a drift detector that fires on the planted day and stays quiet before it.\n",
    "- A regression gate: a frozen golden set, a scorer, and a CI-style check that ships a model only when it does not regress, demonstrated by *watching it block a bad model*.\n",
    "- An annotated read of a real training loop (nanoGPT's), where you locate the `optimizer.zero_grad` placement, the `set_to_none` flag, and the `AFTER_DEBUG`-style early-break that the spec names as a landmine.\n",
    "- A faithful reproduction of this repository's own CI outage: `np.trapz` deprecated in NumPy 2.0 and later removed, the 8-runs-red post-mortem, and the one-line `np.trapezoid` fix, with the pinned-versus-unpinned dependency lesson made executable.\n",
    "\n",
    "**How this notebook works.** Code cells with a `# TODO` are yours to fill in. Run the cell to grade yourself: `[ ok ]` passed, `[FAIL]` shows what went wrong, `[ -- ]` means not attempted yet. Every exercise has a hint ladder (open only as many as you need) and a folded solution below it. The notebook runs top-to-bottom even if you fill in nothing, because the solution cells redefine the functions the later cells need. See Ch 00 for the full protocol.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "289876df",
   "metadata": {},
   "source": [
    "## Before you start\n",
    "\n",
    "1. A model scores 87% accuracy on the test set you held out in March. It is now September. Why is that number no longer the interesting one? <details><summary>Answer</summary>The test set is a frozen photograph of March. Production traffic in September is drawn from a distribution that has drifted (new users, new items, a changed upstream feature pipeline). The only interesting accuracy is on the slice you served *yesterday*, and you only know it once labels arrive. The whole chapter is the machinery for measuring that.</details>\n",
    "2. You compare two feature distributions, a March reference and a September production sample, by binning both and summing $(p_i - q_i)\\log(p_i/q_i)$ over bins. If the two samples are drawn from the *same* distribution, what should that sum be near? <details><summary>Answer</summary>Near zero. Every $p_i \\approx q_i$, so each term $(p_i - q_i)\\log(p_i/q_i) \\approx 0$. That sum is the Population Stability Index. \"PSI of a distribution against itself is ~0\" is the first property test you will write, and it is exactly the kind of ground-truth check that separates a real monitor from a plausible-looking one.</details>\n",
    "3. Predict before you run: the repo's CI went 8 runs red on a single function call inside a metric computation. The call was `np.trapz(...)`. What did NumPy 2.0 do to it? <details><summary>Answer</summary>It deprecated `np.trapz` (every call emits a `DeprecationWarning`) and scheduled it for removal; the replacement is `np.trapezoid`. An unpinned dependency upgrade that crossed the removal boundary detonated it into an `AttributeError`. Part 5 reproduces the live deprecation signal, names the removal endpoint, walks the post-mortem, and ships the one-line fix. This is not a hypothetical; it is this repository's own outage.</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8e5e53e0",
   "metadata": {},
   "source": [
    "## Setup\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "d7c7a12a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:23.453266Z",
     "iopub.status.busy": "2026-06-10T20:37:23.453170Z",
     "iopub.status.idle": "2026-06-10T20:37:24.100221Z",
     "shell.execute_reply": "2026-06-10T20:37:24.099836Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "numpy 2.2.6 · sklearn 1.7.2 · scipy 1.15.3\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import sklearn\n",
    "import scipy\n",
    "print(f\"numpy {np.__version__} · sklearn {sklearn.__version__} · scipy {scipy.__version__}\")\n",
    "if np.__version__ < \"2.0\":\n",
    "    print(\"WARN: written for NumPy 2.x; on 1.x the np.trapezoid case study in Part 5 reads differently\")\n",
    "if not hasattr(np, \"trapezoid\"):\n",
    "    print(\"WARN: np.trapezoid missing — you are on NumPy < 2.0; Part 5 explains why that matters\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "75050801",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.101331Z",
     "iopub.status.busy": "2026-06-10T20:37:24.101175Z",
     "iopub.status.idle": "2026-06-10T20:37:24.105905Z",
     "shell.execute_reply": "2026-06-10T20:37:24.105486Z"
    }
   },
   "outputs": [],
   "source": [
    "import os, random\n",
    "SEED = 0\n",
    "FAST = bool(os.environ.get('NB_FAST'))  # CI smoke mode: shrinks the one iterative loop, same code paths\n",
    "\n",
    "# Sizes. The synthetic tables are kept full even under FAST because a logistic fit on\n",
    "# 2000x5 floats and a 10-bin histogram both cost milliseconds; shrinking them would make\n",
    "# the PSI null-noise seed-fragile (the spec's '54%-that's-actually-51%' anti-pattern).\n",
    "# FAST only shrinks the genuinely iterative gradient-descent loop in Part 4 (STEPS_GD there).\n",
    "N_REF   = 2000   # rows in the training-reference feature table\n",
    "N_DAY   = 1500   # rows served per simulated production day\n",
    "N_DAYS  = 30     # days of simulated production\n",
    "DRIFT_DAY = 14   # the day the upstream pipeline breaks (planted ground truth)\n",
    "\n",
    "rng = np.random.default_rng(SEED)       # the one RNG we thread everywhere\n",
    "random.seed(SEED)\n",
    "\n",
    "# ── house self-check harness (identical across all chapter notebooks) ──\n",
    "import numpy as _np\n",
    "\n",
    "def check(label, test_fn, required=False):\n",
    "    \"\"\"Run one self-check. test_fn raises AssertionError (with a teaching\n",
    "    message) on failure, NotImplementedError if the stub is unfilled.\n",
    "    required=True is used only in solution cells; it is what CI grades.\"\"\"\n",
    "    try:\n",
    "        test_fn()\n",
    "    except NotImplementedError:\n",
    "        if required:\n",
    "            raise AssertionError(f\"{label}: reference solution incomplete\")\n",
    "        print(f\"[ -- ] {label}: not attempted yet — fill in the TODO above, then re-run.\")\n",
    "        return False\n",
    "    except AssertionError as e:\n",
    "        if required:\n",
    "            raise\n",
    "        print(f\"[FAIL] {label}: {e}\")\n",
    "        return False\n",
    "    print(f\"[ ok ] {label}\")\n",
    "    return True\n",
    "\n",
    "def attempted(*vals):\n",
    "    \"\"\"Treat None placeholders as 'not attempted'.\"\"\"\n",
    "    if any(v is None for v in vals):\n",
    "        raise NotImplementedError\n",
    "\n",
    "def check_shape(x, want):\n",
    "    assert tuple(x.shape) == tuple(want), \\\n",
    "        f\"shape {tuple(x.shape)}, expected {tuple(want)} — check your reshape/transpose order\"\n",
    "\n",
    "def check_close(got, want, atol=1e-5, rtol=1e-4, msg=\"\"):\n",
    "    g, w = _np.asarray(got, dtype=float), _np.asarray(want, dtype=float)\n",
    "    assert g.shape == w.shape, f\"shape {g.shape} vs expected {w.shape}. {msg}\"\n",
    "    bad = ~_np.isclose(g, w, atol=atol, rtol=rtol)\n",
    "    assert not bad.any(), \\\n",
    "        f\"{bad.mean():.2%} of values wrong (max diff {abs(g - w).max():.3g}). {msg}\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8c44868b",
   "metadata": {},
   "source": [
    "> **Note:** seeds make this notebook's printed numbers reproduce on CPU. Library versions and BLAS threading can shift the last digit or two; quoted numbers hold for the pinned environment. If a PSI reads 0.412 and the page says 0.413, you did nothing wrong. The `FAST` flag (set by the `NB_FAST` environment variable) shrinks the one iterative gradient-descent loop in Part 4 for continuous-integration smoke runs, without changing a single line of the algorithms. The synthetic tables stay full size at both settings on purpose: they cost milliseconds, and shrinking them would make the drift statistics seed-fragile, the exact thing a monitor must not be.\n",
    "\n",
    "> **Note:** there is no Weights & Biases, no Feast, no Modal, no live API anywhere in this notebook. Those are real tools the chapter prose surveys, but a downloadable notebook that depends on a SaaS account or a network call is not reproducible. Everything here is `numpy` + `sklearn` + `scipy` + `matplotlib`, and the registry, the serving log, and the monitor are small local objects you can read end to end. The discipline transfers; the account does not.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "463d80cc",
   "metadata": {},
   "source": [
    "## The map\n",
    "\n",
    "> **Part 1 — The loop, not the model.** Build the synthetic recommender and the five-stage loop object (`data -> train -> serve -> monitor -> retrain`). The model is one node; the loop is the system.\n",
    "> **Part 2 — Drift, measured.** PSI, KL, and a KS test from scratch, each checked against an independent reference, then a per-feature drift detector run across 30 days of production with a planted drift event on day 14.\n",
    "> **Part 3 — The regression gate.** A frozen golden set, a scorer, and a CI-style gate. The deliberate failure: a model that *looks* better on average but regresses a slice, and the gate that catches it.\n",
    "> **Part 4 — Reading a real training loop.** An annotated read of nanoGPT's loop: where `zero_grad` goes, what `set_to_none` does, and the `AFTER_DEBUG` early-break the spec names as a landmine.\n",
    "> **Part 5 — The np.trapezoid case study.** Reproduce this repo's own NumPy-2.0 CI detonation on `np.trapz`, read the post-mortem, ship the `np.trapezoid` fix, and make the pinned-versus-unpinned dependency lesson executable.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9accfd2e",
   "metadata": {},
   "source": [
    "## Part 1 — The loop, not the model\n",
    "\n",
    "> **Objectives.** Generate a synthetic recommender problem with a known signal, wire the five lifecycle stages into one object you can step day by day, and see why \"the model has 87% accuracy on the test set\" stops being the interesting number the moment the loop starts turning.\n",
    "\n",
    "The chapter's claim, restated: *the model is not the system*. The system is the directed graph from raw events through features through training through serving through monitoring back to retraining. Every edge is a place a bug can live. We will build a tiny but complete version of that graph, with synthetic data whose ground truth we control, so that when the monitor fires we can check it was right.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac69f365",
   "metadata": {},
   "source": [
    "### 1.1 A synthetic recommender with a signal we planted\n",
    "\n",
    "We generate a binary \"did the user click?\" problem from five features. Two of them carry real signal (`recency` and `affinity`); the rest are noise. Because *we* wrote the label rule, we know the true decision boundary, which makes every later claim (\"the model recovered the signal\", \"this feature drifted\") checkable rather than vibes.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "2f6afa9a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.107046Z",
     "iopub.status.busy": "2026-06-10T20:37:24.106945Z",
     "iopub.status.idle": "2026-06-10T20:37:24.110535Z",
     "shell.execute_reply": "2026-06-10T20:37:24.110243Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "reference table: X (2000, 5), click rate 42.4%\n"
     ]
    }
   ],
   "source": [
    "FEATURES = [\"recency\", \"affinity\", \"session_len\", \"device_age\", \"noise\"]\n",
    "\n",
    "def make_day(rng, n, drift=0.0):\n",
    "    \"\"\"One day of production rows. `drift` shifts the `session_len` feature only,\n",
    "    standing in for an upstream pipeline that started emitting larger values.\n",
    "    Returns (X, y) with X shape (n, 5) and y in {0, 1}.\"\"\"\n",
    "    recency     = rng.normal(0.0, 1.0, n)                 # higher = saw item recently\n",
    "    affinity    = rng.normal(0.0, 1.0, n)                 # higher = likes this category\n",
    "    session_len = rng.normal(0.0 + drift, 1.0, n)         # the feature we will break\n",
    "    device_age  = rng.normal(0.0, 1.0, n)                 # pure noise w.r.t. the label\n",
    "    noise       = rng.normal(0.0, 1.0, n)                 # pure noise\n",
    "    X = np.stack([recency, affinity, session_len, device_age, noise], axis=1)\n",
    "    # the TRUE click rule: recency and affinity decide it; logistic link, then a coin flip.\n",
    "    logit = 1.3 * recency + 1.1 * affinity - 0.4\n",
    "    p = 1.0 / (1.0 + np.exp(-logit))\n",
    "    y = (rng.uniform(0, 1, n) < p).astype(int)\n",
    "    return X, y\n",
    "\n",
    "X_ref, y_ref = make_day(rng, N_REF)   # the training / reference distribution\n",
    "print(f\"reference table: X {X_ref.shape}, click rate {y_ref.mean():.1%}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f68d9e65",
   "metadata": {},
   "source": [
    "> **Interpretation.** Two features (`recency`, `affinity`) carry the signal; `session_len` is the one we will later drift; `device_age` and `noise` are decoys. The click rate near 40% means the problem is not degenerate. Every number from here is reproducible because the rule is ours.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "829e7b33",
   "metadata": {},
   "source": [
    "### 1.2 The model is one node\n",
    "\n",
    "Train a logistic-regression classifier on the reference table. This is the entire \"modeling + training\" stage the previous twenty chapters were about. Here it is four lines, on purpose: the point of this chapter is everything *around* it.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "907c4595",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.111477Z",
     "iopub.status.busy": "2026-06-10T20:37:24.111393Z",
     "iopub.status.idle": "2026-06-10T20:37:24.145043Z",
     "shell.execute_reply": "2026-06-10T20:37:24.144711Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "validation accuracy at training time: 0.710\n",
      "features by |coefficient|: ['recency', 'affinity', 'session_len', 'noise', 'device_age']\n"
     ]
    }
   ],
   "source": [
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "Xtr, Xval, ytr, yval = train_test_split(X_ref, y_ref, test_size=0.25, random_state=SEED, stratify=y_ref)\n",
    "model = LogisticRegression(max_iter=1000, random_state=SEED).fit(Xtr, ytr)\n",
    "val_acc = model.score(Xval, yval)\n",
    "print(f\"validation accuracy at training time: {val_acc:.3f}\")\n",
    "# the model recovered the signal: the two real features get the two largest coefficients.\n",
    "order = np.argsort(-np.abs(model.coef_[0]))\n",
    "print(\"features by |coefficient|:\", [FEATURES[i] for i in order])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "ffe812b5",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.146221Z",
     "iopub.status.busy": "2026-06-10T20:37:24.146055Z",
     "iopub.status.idle": "2026-06-10T20:37:24.148366Z",
     "shell.execute_reply": "2026-06-10T20:37:24.148020Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] the two largest coefficients are the two features that actually drive the label\n"
     ]
    }
   ],
   "source": [
    "# claim from the markdown above, made an assert (spec: a notebook that lies fails to run)\n",
    "top_two = {FEATURES[i] for i in np.argsort(-np.abs(model.coef_[0]))[:2]}\n",
    "assert top_two == {\"recency\", \"affinity\"}, \\\n",
    "    f\"model put its weight on {top_two}, not the two signal features — the synthesis is broken\"\n",
    "print(\"[ ok ] the two largest coefficients are the two features that actually drive the label\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a4bc3c4",
   "metadata": {},
   "source": [
    "> **Interpretation.** The model found the signal: `recency` and `affinity` carry the two largest coefficients, the decoys do not. That validation accuracy is the number a naive team would put on a slide. The rest of the notebook is about why it tells you almost nothing about next Tuesday.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "accc6e22",
   "metadata": {},
   "source": [
    "### 1.3 The five-stage loop in one object\n",
    "\n",
    "The chapter lists ten lifecycle phases; the *operational* core that turns continuously is five: **data -> train -> serve -> monitor -> retrain**. We put those five behind one small object so the whole loop is one thing you can step. A registry maps an alias (`prod`) to a model, serving logs every request, the monitor reads that log, and retrain produces a new model the registry can promote. No SaaS: a dict is a registry, a list is a log pipeline.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "612d92ce",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.149109Z",
     "iopub.status.busy": "2026-06-10T20:37:24.149024Z",
     "iopub.status.idle": "2026-06-10T20:37:24.153462Z",
     "shell.execute_reply": "2026-06-10T20:37:24.153097Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "prod alias resolves to: LogisticRegression\n",
      "serve log has 5 rows after 5 requests\n"
     ]
    }
   ],
   "source": [
    "class MLLoop:\n",
    "    \"\"\"A minimal data->train->serve->monitor->retrain loop. Everything in memory\n",
    "    so it is fully reproducible; a real system swaps each attribute for a service.\"\"\"\n",
    "    def __init__(self, reference_X):\n",
    "        self.reference_X = reference_X        # the training feature distribution (monitor baseline)\n",
    "        self.registry = {}                    # alias -> fitted model (\"prod\" is what serving loads)\n",
    "        self.serve_log = []                   # one dict per served request (stands in for a log pipeline)\n",
    "\n",
    "    def register(self, model, alias=\"prod\"):\n",
    "        # promotion is a single assignment; rollback is the same assignment to an older model.\n",
    "        self.registry[alias] = model\n",
    "\n",
    "    def serve(self, X):\n",
    "        # load the prod model by alias (never by version), predict, and LOG every row.\n",
    "        m = self.registry[\"prod\"]\n",
    "        proba = m.predict_proba(X)[:, 1]\n",
    "        pred = (proba >= 0.5).astype(int)\n",
    "        for row, pr in zip(X, proba):\n",
    "            self.serve_log.append({\"features\": row.copy(), \"score\": float(pr)})\n",
    "        return pred\n",
    "\n",
    "    def served_features(self):\n",
    "        # reconstruct the served feature matrix from the log (what the monitor actually sees).\n",
    "        return np.array([r[\"features\"] for r in self.serve_log]) if self.serve_log else np.empty((0, self.reference_X.shape[1]))\n",
    "\n",
    "loop = MLLoop(X_ref)\n",
    "loop.register(model, alias=\"prod\")\n",
    "_ = loop.serve(X_ref[:5])\n",
    "print(f\"prod alias resolves to: {type(loop.registry['prod']).__name__}\")\n",
    "print(f\"serve log has {len(loop.serve_log)} rows after 5 requests\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adeaf8f4",
   "metadata": {},
   "source": [
    "> **Note:** serving loads the model by the alias `prod`, never by a version number. That one indirection is what makes promotion and rollback a single assignment instead of a redeploy. The registry is the artifact gate; the alias is the lever.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "27fa2ba4",
   "metadata": {},
   "source": [
    "### Exercise 25.1 — The lifecycle gate\n",
    "`Difficulty 1/5 · ~8 min`\n",
    "\n",
    "Before a model ships, the chapter's `lifecycle_gate.py` checks that the lifecycle artifacts exist: a problem-framing one-pager, a data schema, an eval report, a model card, a monitoring plan, a rollback plan. Fill in `lifecycle_ready(present, required)`, where `present` and `required` are sets of artifact names. Return `(ready, missing)`: `ready` is `True` only if nothing required is absent, and `missing` is the sorted list of required artifacts not present. The checks confirm a complete set passes and a set missing the rollback plan fails with the right `missing` list.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "3c746f48",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.154379Z",
     "iopub.status.busy": "2026-06-10T20:37:24.154237Z",
     "iopub.status.idle": "2026-06-10T20:37:24.158762Z",
     "shell.execute_reply": "2026-06-10T20:37:24.158378Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 25.1 complete set passes: not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] 25.1 missing artifact fails: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "REQUIRED_ARTIFACTS = {\"problem_framing\", \"data_schema\", \"eval_report\",\n",
    "                      \"model_card\", \"monitoring_plan\", \"rollback_plan\"}\n",
    "\n",
    "def lifecycle_ready(present, required=REQUIRED_ARTIFACTS):\n",
    "    \"\"\"Return (ready, missing). ready is True iff every required artifact is present;\n",
    "    missing is the sorted list of required names absent from `present`.\"\"\"\n",
    "    present = set(present)\n",
    "    required = set(required)\n",
    "    # TODO 1: missing = the sorted list of required names not in present\n",
    "    missing = None\n",
    "    # TODO 2: ready = True iff missing is empty\n",
    "    ready = None\n",
    "    attempted(missing, ready)\n",
    "    return ready, missing\n",
    "\n",
    "def _complete_passes():\n",
    "    ready, missing = lifecycle_ready(REQUIRED_ARTIFACTS)\n",
    "    assert ready and missing == [], f\"a complete artifact set must pass, got missing={missing}\"\n",
    "\n",
    "def _incomplete_fails():\n",
    "    partial = REQUIRED_ARTIFACTS - {\"rollback_plan\"}\n",
    "    ready, missing = lifecycle_ready(partial)\n",
    "    assert not ready, \"a set missing the rollback plan must not be ready\"\n",
    "    assert missing == [\"rollback_plan\"], f\"missing should be exactly ['rollback_plan'], got {missing}\"\n",
    "\n",
    "check(\"25.1 complete set passes\", _complete_passes)\n",
    "check(\"25.1 missing artifact fails\", _incomplete_fails)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "86265c1f",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>Set difference does the work: `required - present` is the set of names that are required but absent. Sort it into a list. `ready` is just whether that list is empty.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "missing = sorted(required - present)\n",
    "ready = (len(missing) == 0)\n",
    "```\n",
    "That is the whole body; the `attempted(...)` and `return` are already there.</details>\n",
    "\n",
    "<details><summary>Help — \"missing should be exactly ['rollback_plan']\"</summary>If you got a longer list, you likely computed `present - required` (the reverse difference) or forgot to sort. The direction matters: you want what is *required but absent*, which is `required - present`. Sort the result so the check sees a deterministic order.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "1d68734f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.159633Z",
     "iopub.status.busy": "2026-06-10T20:37:24.159545Z",
     "iopub.status.idle": "2026-06-10T20:37:24.161883Z",
     "shell.execute_reply": "2026-06-10T20:37:24.161617Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 25.1 complete set passes\n",
      "[ ok ] 25.1 missing artifact fails\n",
      "ready on full set: (True, [])\n",
      "ready missing rollback: (False, ['rollback_plan'])\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines lifecycle_ready; the checks below re-verify the reference.\n",
    "def lifecycle_ready(present, required=REQUIRED_ARTIFACTS):\n",
    "    present, required = set(present), set(required)\n",
    "    missing = sorted(required - present)\n",
    "    return (len(missing) == 0), missing\n",
    "\n",
    "check(\"25.1 complete set passes\", _complete_passes, required=True)\n",
    "check(\"25.1 missing artifact fails\", _incomplete_fails, required=True)\n",
    "print(\"ready on full set:\", lifecycle_ready(REQUIRED_ARTIFACTS))\n",
    "print(\"ready missing rollback:\", lifecycle_ready(REQUIRED_ARTIFACTS - {\"rollback_plan\"}))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8475b5a1",
   "metadata": {},
   "source": [
    "> **Key takeaways.** The model is a single node in a loop that turns continuously. Training-time accuracy describes a frozen past. The operational core is five stages, and you can hold all five in one small object. Serving that logs every request is what makes the next part, monitoring, possible at all: you cannot measure drift on requests you did not record. And a model is not ready to ship just because it trained: the lifecycle gate is the checklist that says so.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "53b41df6",
   "metadata": {},
   "source": [
    "## Part 2 — Drift, measured\n",
    "\n",
    "> **Objectives.** Build PSI, KL divergence, and a KS test from scratch; check each against an independent reference (`scipy`, and a known-answer toy); then assemble a per-feature drift detector and run it across 30 simulated production days with a drift event planted on day 14, confirming it fires there and is quiet before.\n",
    "\n",
    "Four monitoring questions, ordered by how fast the signal arrives: are my *inputs* the same as in training (seconds), are my *predictions* distributed the same (minutes), are my *labels* the same (hours to days), is my model still *accurate* (only once labels arrive). This part is about the first one, feature drift, because it is the earliest warning and the one you can compute before a single label comes back.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "245a3c21",
   "metadata": {},
   "source": [
    "### 2.1 Population Stability Index, from the formula\n",
    "\n",
    "Bin a feature on the *reference* distribution's quantiles, then compare bin proportions in production. With reference proportions $p_i$ and production proportions $q_i$,\n",
    "\n",
    "$$\\text{PSI} = \\sum_i (p_i - q_i)\\,\\log\\!\\frac{p_i}{q_i}.$$\n",
    "\n",
    "The industry rule of thumb: $\\text{PSI} < 0.1$ is no drift, $0.1$–$0.25$ is moderate, $> 0.25$ is significant. Loose, but a useful first-pass alarm. Notice PSI is symmetric in a way KL is not, which we make precise in 2.3. Below, `p` is `p_i` and `q` is `q_i`, line for line.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "96948aed",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.162815Z",
     "iopub.status.busy": "2026-06-10T20:37:24.162737Z",
     "iopub.status.idle": "2026-06-10T20:37:24.167411Z",
     "shell.execute_reply": "2026-06-10T20:37:24.167100Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PSI(x, x)        = 0.00e+00   (want ~0)\n",
      "PSI(N(0,1), N(1,1)) = 0.938   (1-sigma shift, want > 0.25)\n"
     ]
    }
   ],
   "source": [
    "def psi(reference, production, n_bins=10):\n",
    "    \"\"\"Population Stability Index between a reference and a production sample of a\n",
    "    single feature. Bins on the reference's quantiles so each reference bin holds\n",
    "    ~equal mass; the +1e-9 floors keep empty production bins from blowing up log.\"\"\"\n",
    "    reference = np.asarray(reference, dtype=float)\n",
    "    production = np.asarray(production, dtype=float)\n",
    "    edges = np.quantile(reference, np.linspace(0, 1, n_bins + 1))\n",
    "    edges[0] -= 1e-9            # widen the outer edges so the min/max land inside a bin\n",
    "    edges[-1] += 1e-9\n",
    "    ref_counts, _ = np.histogram(reference, edges)\n",
    "    prod_counts, _ = np.histogram(production, edges)\n",
    "    p = ref_counts / ref_counts.sum() + 1e-9      # reference proportions p_i (+ smoothing)\n",
    "    q = prod_counts / prod_counts.sum() + 1e-9    # production proportions q_i (+ smoothing)\n",
    "    return float(np.sum((p - q) * np.log(p / q)))\n",
    "\n",
    "# property check: a distribution against itself is ~0 (the +1e-9 floors keep it from being exactly 0)\n",
    "x = rng.normal(size=10_000)\n",
    "print(f\"PSI(x, x)        = {psi(x, x):.2e}   (want ~0)\")\n",
    "print(f\"PSI(N(0,1), N(1,1)) = {psi(x, rng.normal(1.0, 1.0, 10_000)):.3f}   (1-sigma shift, want > 0.25)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7d490e9c",
   "metadata": {},
   "source": [
    "> **Interpretation.** PSI of a sample against itself is floor-noise near $10^{-9}$, and a one-standard-deviation mean shift lands well above the 0.25 \"significant\" line. Those two facts are the from-scratch sanity check; in the exercise you will turn them into asserts against a hand-computable toy.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dcac18e4",
   "metadata": {},
   "source": [
    "### Exercise 25.2 — PSI on a hand-checkable toy\n",
    "`Difficulty 2/5 · ~10 min`\n",
    "\n",
    "Trust nothing you cannot check on paper. Fill in `psi_two_bins(p, q)` for the *two-bin* case, where `p = (p0, p1)` are reference proportions and `q = (q0, q1)` are production proportions (each pair sums to 1). This is PSI with the binning already done, so it is pure arithmetic: $\\sum_i (p_i - q_i)\\log(p_i/q_i)$. The checks compare it to a value you can compute by hand and confirm the general `psi` agrees on a binned case.\n",
    "\n",
    "Harder: add an assert that `psi_two_bins(p, q) == psi_two_bins(q, p)` is NOT generally true... and discover that for PSI it actually *is*. Why? (See 2.3.)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "ed82ec52",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.168325Z",
     "iopub.status.busy": "2026-06-10T20:37:24.168246Z",
     "iopub.status.idle": "2026-06-10T20:37:24.171577Z",
     "shell.execute_reply": "2026-06-10T20:37:24.171242Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 25.2 PSI hand value: not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] 25.2 PSI identity: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def psi_two_bins(p, q):\n",
    "    \"\"\"PSI for two bins. p=(p0,p1), q=(q0,q1), each summing to 1. Pure arithmetic.\"\"\"\n",
    "    p = np.asarray(p, dtype=float)\n",
    "    q = np.asarray(q, dtype=float)\n",
    "    # TODO 1: return sum over the two bins of (p_i - q_i) * log(p_i / q_i)\n",
    "    result = None\n",
    "    attempted(result)\n",
    "    return float(result)\n",
    "\n",
    "def _toy():\n",
    "    # p = (0.5, 0.5), q = (0.25, 0.75). By hand:\n",
    "    #   (0.5-0.25)*log(0.5/0.25) + (0.5-0.75)*log(0.5/0.75)\n",
    "    # = 0.25*log(2) + (-0.25)*log(2/3) = 0.25*0.693147 + (-0.25)*(-0.405465)\n",
    "    # = 0.173287 + 0.101366 = 0.274653\n",
    "    got = psi_two_bins((0.5, 0.5), (0.25, 0.75))\n",
    "    check_close(got, 0.274653, atol=1e-4,\n",
    "                msg=\"two-bin PSI of (0.5,0.5) vs (0.25,0.75) is 0.2747 by hand\")\n",
    "\n",
    "def _identity():\n",
    "    assert abs(psi_two_bins((0.3, 0.7), (0.3, 0.7))) < 1e-12, \\\n",
    "        \"PSI of a distribution against itself must be exactly 0 (no smoothing in the two-bin form)\"\n",
    "\n",
    "check(\"25.2 PSI hand value\", _toy)\n",
    "check(\"25.2 PSI identity\",   _identity)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9cc931f0",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>It is one line. The whole formula is a sum over bins of `(p_i - q_i) * log(p_i / q_i)`, and numpy will do all four operations elementwise on the length-2 arrays, then `.sum()`.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "return float(np.sum((p - q) * np.log(p / q)))\n",
    "```\n",
    "That is the entire body. No loop, no binning, no smoothing.</details>\n",
    "\n",
    "<details><summary>Help — \"RuntimeWarning: divide by zero\" or a nan</summary>That happens if a bin proportion is exactly 0, so `log(p/q)` hits `log(0)` or divides by 0. The toy and identity inputs here are all strictly positive, so if you see a nan you likely swapped `p` and `q` inside the log or wrote `log(q/p)` with a zero somewhere. The general `psi` above floors with `+1e-9` for exactly this reason; the two-bin form does not need it because the inputs are clean.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "5fbdd61e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.172627Z",
     "iopub.status.busy": "2026-06-10T20:37:24.172543Z",
     "iopub.status.idle": "2026-06-10T20:37:24.175168Z",
     "shell.execute_reply": "2026-06-10T20:37:24.174853Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 25.2 PSI hand value\n",
      "[ ok ] 25.2 PSI identity\n",
      "psi_two_bins((0.5,0.5),(0.25,0.75)) = 0.274653\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines psi_two_bins; the checks below re-verify the reference.\n",
    "def psi_two_bins(p, q):\n",
    "    p = np.asarray(p, dtype=float)\n",
    "    q = np.asarray(q, dtype=float)\n",
    "    return float(np.sum((p - q) * np.log(p / q)))\n",
    "\n",
    "check(\"25.2 PSI hand value\", _toy, required=True)\n",
    "check(\"25.2 PSI identity\",   _identity, required=True)\n",
    "print(f\"psi_two_bins((0.5,0.5),(0.25,0.75)) = {psi_two_bins((0.5,0.5),(0.25,0.75)):.6f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "978a4d22",
   "metadata": {},
   "source": [
    "### Exercise 25.3 — KL divergence and the PSI identity\n",
    "`Difficulty 2/5 · ~12 min`\n",
    "\n",
    "KL divergence between binned distributions $P$ and $Q$ is\n",
    "\n",
    "$$D_\\text{KL}(P \\,\\|\\, Q) = \\sum_i p_i \\log\\!\\frac{p_i}{q_i},$$\n",
    "\n",
    "which is *not* symmetric: $D_\\text{KL}(P\\|Q) \\neq D_\\text{KL}(Q\\|P)$ in general. Fill in `kl(p, q)` for two proportion vectors. The payoff is a structural fact you then verify: PSI is exactly the *symmetrized* KL, $\\text{PSI} = D_\\text{KL}(P\\|Q) + D_\\text{KL}(Q\\|P)$, which falls out of expanding $\\sum (p_i - q_i)\\log(p_i/q_i)$. The checks confirm KL is asymmetric on a concrete pair and that your `kl` reproduces PSI when symmetrized.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "0134e939",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.176250Z",
     "iopub.status.busy": "2026-06-10T20:37:24.176138Z",
     "iopub.status.idle": "2026-06-10T20:37:24.180028Z",
     "shell.execute_reply": "2026-06-10T20:37:24.179708Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 25.3 KL asymmetric: not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] 25.3 PSI = symmetrized KL: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def kl(p, q):\n",
    "    \"\"\"KL(P || Q) for two binned proportion vectors. Normalize and floor to avoid log(0).\"\"\"\n",
    "    p = np.asarray(p, dtype=float) + 1e-12\n",
    "    q = np.asarray(q, dtype=float) + 1e-12\n",
    "    p = p / p.sum(); q = q / q.sum()\n",
    "    # TODO 1: return sum_i p_i * log(p_i / q_i)\n",
    "    result = None\n",
    "    attempted(result)\n",
    "    return float(result)\n",
    "\n",
    "P = np.array([0.6, 0.3, 0.1])                          # an ASYMMETRIC pair (P is not Q reversed)\n",
    "Q = np.array([0.2, 0.3, 0.5])\n",
    "psi_direct = float(np.sum((P - Q) * np.log(P / Q)))   # PSI on this pair, computed directly\n",
    "\n",
    "def _kl_asymmetric():\n",
    "    assert abs(kl(P, Q) - kl(Q, P)) > 1e-6, \"KL must be asymmetric: KL(P||Q) != KL(Q||P) here\"\n",
    "\n",
    "def _kl_symmetrizes_to_psi():\n",
    "    assert abs((kl(P, Q) + kl(Q, P)) - psi_direct) < 1e-9, \\\n",
    "        f\"KL(P||Q)+KL(Q||P) should equal PSI ({psi_direct:.4f}); got {kl(P,Q)+kl(Q,P):.4f}\"\n",
    "\n",
    "check(\"25.3 KL asymmetric\", _kl_asymmetric)\n",
    "check(\"25.3 PSI = symmetrized KL\", _kl_symmetrizes_to_psi)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4bd28a80",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>One line, same shape as PSI but without the `(p - q)` factor: a sum of `p_i * log(p_i / q_i)`. The normalization and flooring are already done for you above.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "return float(np.sum(p * np.log(p / q)))\n",
    "```\n",
    "</details>\n",
    "\n",
    "<details><summary>Help — \"KL(P||Q)+KL(Q||P) should equal PSI\"</summary>If the symmetrized sum does not match PSI, check that you weighted by `p` (not `p - q`), and that both `kl(P, Q)` and `kl(Q, P)` use the same normalized vectors. The identity is exact: $\\sum(p-q)\\log(p/q) = \\sum p\\log(p/q) + \\sum q\\log(q/p)$, so any mismatch is an arithmetic slip in the `kl` body.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "53a78388",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.180921Z",
     "iopub.status.busy": "2026-06-10T20:37:24.180839Z",
     "iopub.status.idle": "2026-06-10T20:37:24.183523Z",
     "shell.execute_reply": "2026-06-10T20:37:24.183259Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 25.3 KL asymmetric\n",
      "[ ok ] 25.3 PSI = symmetrized KL\n",
      "KL(P||Q) = 0.4982   KL(Q||P) = 0.5850   (asymmetric)\n",
      "KL(P||Q) + KL(Q||P) = 1.0832   PSI(P, Q) = 1.0832\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines kl; the checks below re-verify the reference.\n",
    "def kl(p, q):\n",
    "    p = np.asarray(p, dtype=float) + 1e-12\n",
    "    q = np.asarray(q, dtype=float) + 1e-12\n",
    "    p = p / p.sum(); q = q / q.sum()\n",
    "    return float(np.sum(p * np.log(p / q)))\n",
    "\n",
    "check(\"25.3 KL asymmetric\", _kl_asymmetric, required=True)\n",
    "check(\"25.3 PSI = symmetrized KL\", _kl_symmetrizes_to_psi, required=True)\n",
    "print(f\"KL(P||Q) = {kl(P, Q):.4f}   KL(Q||P) = {kl(Q, P):.4f}   (asymmetric)\")\n",
    "print(f\"KL(P||Q) + KL(Q||P) = {kl(P, Q) + kl(Q, P):.4f}   PSI(P, Q) = {psi_direct:.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a6ba8f3a",
   "metadata": {},
   "source": [
    "> **Interpretation.** KL is asymmetric: swapping reference and production changes the number. PSI is not, because it is the sum of both directions. That is why PSI is the standard drift alarm (you do not have to argue about which distribution is the \"reference\" in the penalty), and why KL is what you reach for when the asymmetry is the point.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f6745c7f",
   "metadata": {},
   "source": [
    "### 2.4 A Kolmogorov-Smirnov test, checked against scipy\n",
    "\n",
    "For a continuous feature, the KS statistic is the largest vertical gap between the two empirical CDFs: $D = \\max_x |F_\\text{ref}(x) - F_\\text{prod}(x)|$. We build it from scratch and check it against `scipy.stats.ks_2samp`, the strongest kind of self-check the spec describes: your code is right iff it reproduces a real implementation. (We compare the statistic, not the p-value; the p-value has a more delicate asymptotic that scipy handles and we are not reimplementing.)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "0d147846",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.184445Z",
     "iopub.status.busy": "2026-06-10T20:37:24.184377Z",
     "iopub.status.idle": "2026-06-10T20:37:24.188113Z",
     "shell.execute_reply": "2026-06-10T20:37:24.187815Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "our KS statistic   = 0.2453\n",
      "scipy KS statistic = 0.2453\n",
      "[ ok ] from-scratch KS matches scipy.stats.ks_2samp to 1e-9\n"
     ]
    }
   ],
   "source": [
    "def ks_statistic(reference, production):\n",
    "    \"\"\"Two-sample KS statistic: the max gap between empirical CDFs. From scratch.\"\"\"\n",
    "    reference = np.sort(np.asarray(reference, dtype=float))\n",
    "    production = np.sort(np.asarray(production, dtype=float))\n",
    "    grid = np.concatenate([reference, production])          # evaluate CDFs at every observed point\n",
    "    cdf_ref = np.searchsorted(reference, grid, side=\"right\") / reference.size\n",
    "    cdf_prod = np.searchsorted(production, grid, side=\"right\") / production.size\n",
    "    return float(np.max(np.abs(cdf_ref - cdf_prod)))\n",
    "\n",
    "from scipy.stats import ks_2samp\n",
    "a = rng.normal(0.0, 1.0, 1500)\n",
    "b = rng.normal(0.6, 1.0, 1500)\n",
    "ours = ks_statistic(a, b)\n",
    "ref = ks_2samp(a, b).statistic\n",
    "print(f\"our KS statistic   = {ours:.4f}\")\n",
    "print(f\"scipy KS statistic = {ref:.4f}\")\n",
    "assert abs(ours - ref) < 1e-9, f\"our KS {ours} disagrees with scipy {ref} — check the CDF evaluation grid\"\n",
    "print(\"[ ok ] from-scratch KS matches scipy.stats.ks_2samp to 1e-9\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "08eb4c8a",
   "metadata": {},
   "source": [
    "> **Caveat:** the KS test has a documented failure mode at scale. With millions of rows, any microscopic shift becomes \"statistically significant\" by p-value while being operationally meaningless. The fix the chapter prose names is to read the *effect size* (the statistic $D$ itself, or PSI), not the p-value, once your samples are large. We computed the statistic for exactly that reason.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c27092af",
   "metadata": {},
   "source": [
    "### 2.5 The detector, run across 30 days with a planted drift event\n",
    "\n",
    "Now the payoff. We simulate `N_DAYS` of production. On every day, `make_day` draws fresh rows from the reference distribution, except that starting on `DRIFT_DAY` the `session_len` feature is shifted (an upstream pipeline started emitting larger values). We compute per-feature PSI against the reference for each day and plot it. Because *we* planted the drift, we know the right answer: `session_len` should cross the 0.25 line on day 14 and nothing should cross it before.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "9524c5d7",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.189035Z",
     "iopub.status.busy": "2026-06-10T20:37:24.188959Z",
     "iopub.status.idle": "2026-06-10T20:37:24.315911Z",
     "shell.execute_reply": "2026-06-10T20:37:24.315309Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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IfXr1+fatWsULlw4yfgiIyNp06YNZ8+eZd++fbrf+f79+7N582YGDRpEhQoVePnyJUePHuX69etUq1YtxXtbr149bGxsGDlyJKampixcuJAGDRrw33//4e7uniH3ds+ePXz++edUqFABHx8fXr58qfvS4W1z586ldevWdO7cmZiYGNavX0+7du3YsWMHLVu2BKBr16706dOHK1euUKlSJd2+Z86c4datW4wbNy7FeNLr+++/x8HBgX79+jFlypRk61WoUAELCwuOHTuW6HUpxAdBEUJ8EJYvX64Ayr59+5Tnz58rjx8/VtavX6/kz59fsbCwUPz8/JQHDx4oxsbGyrRp0/T2vXz5smJiYqJXXr9+fQVQFixYkKbz169fX6lYsaJe2ZEjRxRAWbt2rV75rl27EpVHREQkOma/fv0US0tLJSoqSlfWsmVLpXjx4sle//379/XKDx48qADKwYMHU7221atXK0ZGRsqRI0f0yhcsWKAAyrFjx5K89gTdu3dXrKyslOfPnyvPnz9X7ty5o0yfPl1RqVRKlSpVdPWWLl2qAIqZmZnSsGFD5fvvv1eOHDmiaDSaRMcElIEDB6Z4Xl9fXwVQevfurVc+YsQIBVAOHDigKIqihISEKGq1Whk+fLhevZkzZyoqlUp5+PChoihKpr5OEnh5eSmA7j7069dPiYyM1G3ftGmTAiiHDx9OtG+7du0UBweHFI8/Z84cBVA2btyoKwsPD1dKlSqV6PWQ1GvPx8dH754oiqJ06tRJKVy4sN7P6fz58wqgLF++XFEURblw4YICKJs2bUr1Hrytbt26SvXq1ROVu7i4KC1btkxx30aNGimVK1fW+13RarVK7dq1ldKlS6f5WEFBQQqg/Pjjjymer379+kr9+vV1zxPu95o1a3RlMTExioeHh2Jtba2EhoYqiqIo9+/fVwAlf/78yqtXr3R1t2/frgDK33//neJ535RwrIR7ryhpvw8J7xeNGzdWtFqtrnzo0KGKsbGxEhwcnOK5u3fvrgDK4MGD9c7TsmVLxczMTHn+/LlejDY2NkpgYKDeMdIaa1RUVKL3hvv37ytqtVqZPHmyrizhvW7Tpk3K69evlfr16ysFChRQLly4oLevra1tqu8pSfH29lbMzMyUu3fv6sqePn2q5MmTR/H09NSVve+9dXV1VRwdHfXq7dmzRwESvfe//bsb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WqIbbriBoqIiFi5cGE/r1KkTeXl5ZGZmNl/BhBBCtHhyJk8IIUSLEYlEmrsIh2SaJllZWViWHGMVQgjReCTIE0IIcUI6//zzmTx5MpMnTyYtLY3MzEymTZuG1jqep2vXrjzyyCNcf/31tGrViltvvRWAt956izPOOINAIEDXrl154oknEt579+7dXHbZZYRCIbp168Yf//jHhOV1XVZZVFSEUoolS5bE09atW8ell15Kq1atSE1N5bzzzmPz5s3MmDGDuXPnsmjRIpRS8dfV9b5Lly5l2LBhBAIBsrOz+dWvfoVt2wmfwy9+8QvuueceMjIyyMrKYsaMGYf87BzHYcqUKaSnp9OmTRvuueeehM8N4L333uPcc8+N57n00kvZvHlzfPkFF1zA5MmTE16zZ88e/H4/ixcvPuT6hRBCNC8J8oQQQpyw5s6di2VZfPbZZzz11FP813/9Fy+++GJCnl//+tcMHDiQ1atXM23aNFatWsWPf/xjrrnmGr766itmzJjBtGnTeOWVV+KvueGGG/j+++/58MMPefPNN/nv//5vdu/efVRl27lzJ6NGjSIQCPDBBx+watUqbrzxRmzbZurUqfz4xz/moosuIi8vj7y8PM4+++w63+OSSy5h6NChfPnllzz33HP87//+L48++mitzyE5OZkVK1YwZ84cHn74Yf7+97/XW7YnnniCV155hZdeeomPP/6Y/fv388477yTkKS8vZ8qUKaxcuZLFixdjGAZXXHEFrusCcPPNNzNv3jzC4XD8Na+++iodOnTgggsuOKrPSgghRBPTQgghxAlo9OjRum/fvtp13Xjavffeq/v27Ruf79Klix4/fnzC66699lr9L//yLwlpv/zlL/Xpp5+utdY6NzdXA/qzzz6LL9+wYYMG9G9+8xuttdZbt27VgF69enU8T2FhoQb0hx9+qLXW+r777tPdunXTkUikzvJPnDhRX3755QlpB7/v/fffr3v37p3wPz777LM6JSVFO44T/xzOPffchPcZOnSovvfee+tcr9ZaZ2dn6zlz5sTno9Go7tixY63y1LRnzx4N6K+++kprrXVlZaVu3bq1fu211+J5BgwYoGfMmFHvewghhDgxyJk8IYQQJ6wRI0aglIrPjxw5km+//RbHceJpQ4YMSXjNhg0bOOeccxLSzjnnnPjrNmzYgGVZDB48OL68T58+pKenH1XZ1qxZw3nnnYfP5zuq1x1c1pEjRyb8j+eccw5lZWXs2LEjnjZgwICE12VnZ9d75rG4uJi8vDyGDx8eT7Msq9bn9O233zJhwgS6d+9Oq1at6Nq1KwDbt28HIBgMct111/HSSy8B8MUXX/D1119zww03HPP/K4QQomnIL7+FEEKc1JKTkxv8PQ3DOwaqa/yOLRqNJuQJhUINvt76HBxIKqXil1Ueq8suu4wuXbrwwgsvkJOTg+u69OvXL+HmNTfffDODBg1ix44dvPzyy1xwwQV06dLluNYrhBCi8cmZPCGEECesFStWJMwvX76cXr16YZpmva/p27cvn3zySULaJ598wmmnnYZpmvTp0wfbtlm1alV8eW5uLkVFRfH5tm3bApCXlxdPO/jZdgMGDOCjjz6qFfxV8/v9CWcc6yvrsmXLEoLJTz75hNTUVDp27HjI19YnLS2N7OzshM/u4P9337595Obm8sADDzBmzBj69u1LYWFhrffq378/Q4YM4YUXXmDevHnceOONx1QmIYQQTUuCPCGEECes7du3M2XKFHJzc5k/fz7PPPMMd9xxxyFfc/fdd7N48WIeeeQRvvnmG+bOnctvf/tbpk6dCkDv3r256KKL+I//+A9WrFjBqlWruPnmmxPOzIVCIUaMGMHs2bPZsGEDS5cu5YEHHkhYz+TJkykpKeGaa65h5cqVfPvtt/zhD38gNzcX8O78uXbtWnJzc9m7d2+dweBtt93G999/z+23387GjRtZtGgR06dPZ8qUKfGzicfijjvuYPbs2SxcuJCNGzdy2223JQSxrVu3pk2bNvzud79j06ZNfPDBB0yZMqXO97r55puZPXs2WmuuuOKKYy6TEEKIpiNBnhBCiBPW9ddfT2VlJcOGDWPSpEnccccd8cck1Oess87i9ddfZ8GCBfTr148HH3yQhx9+OOG3ZC+//DI5OTmMHj2aK6+8kltvvZV27dolvM9LL72EbdsMHjyYO++8s9YdL9u0acMHH3xAWVkZo0ePZvDgwbzwwgvxSytvueUWevfuzZAhQ2jbtm2ts4sAHTp04C9/+QufffYZAwcO5Gc/+xk33XRTrYDyaN19991cd911TJw4kZEjR5KampoQoBmGwYIFC1i1ahX9+vXjrrvu4vHHH6/zvSZMmIBlWUyYMIFgMHhc5RJCCNE0lNYHPThHCCGEOAGcf/75DBo0iCeffLK5i3JK27ZtGz169ODzzz/nrLPOau7iCCGEOAJy4xUhhBBC1BKNRtm3bx8PPPAAI0aMkABPCCFOInK5phBCCCFq+eSTT8jOzubzzz/n+eefb+7iCCGEOApyuaYQQgghhBBCtCByJk8IIYQQQgghWhAJ8oQQQgghhBCiBZEgTwghhBBCCCFaEAnyhBBCCCGEEKIFkSBPCCGEEEIIIVoQCfKEEEIIIYQQogWRIE8IIYQQQgghWhAJ8oQQQgghhBCiBZEgTwghhBBCCCFakP8Pq5tlOJdqo6AAAAAASUVORK5CYII=",
      "text/plain": [
       "<Figure size 900x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# viz: build the 30-day PSI matrix and plot it\n",
    "drift_rng = np.random.default_rng(SEED + 7)          # a separate stream for the production sim\n",
    "psi_matrix = np.zeros((N_DAYS, len(FEATURES)))\n",
    "for d in range(N_DAYS):\n",
    "    shift = 1.5 if d >= DRIFT_DAY else 0.0           # the planted upstream break, from day 14\n",
    "    Xd, _ = make_day(drift_rng, N_DAY, drift=shift)\n",
    "    for j in range(len(FEATURES)):\n",
    "        psi_matrix[d, j] = psi(X_ref[:, j], Xd[:, j])\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 4))\n",
    "for j, name in enumerate(FEATURES):\n",
    "    ax.plot(range(N_DAYS), psi_matrix[:, j], marker=\".\", label=name)\n",
    "ax.axhline(0.25, ls=\"--\", c=\"#c0392b\", label=\"0.25 (significant)\")\n",
    "ax.axhline(0.10, ls=\":\", c=\"#e67e22\", label=\"0.10 (moderate)\")\n",
    "ax.axvline(DRIFT_DAY, ls=\"-\", c=\"#888\", alpha=0.5)\n",
    "ax.set_xlabel(\"production day\"); ax.set_ylabel(\"PSI vs reference\")\n",
    "ax.set_title(\"Per-feature PSI over 30 days (session_len breaks on day 14)\")\n",
    "ax.legend(loc=\"upper left\", fontsize=8); plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ad61e371",
   "metadata": {},
   "source": [
    "> **What is the interpretation of this plot?** <details><summary>Answer</summary>Four of the five features hug the bottom (PSI in the floor-noise band below 0.1) the whole month: they are drawn from the unchanged reference every day. `session_len` sits down there until day 14, then jumps above the red 0.25 line and stays there, because that is exactly where we shifted it. The detector recovered the ground truth we planted: the right feature, on the right day, at the right severity.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "742c02f0",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.316813Z",
     "iopub.status.busy": "2026-06-10T20:37:24.316732Z",
     "iopub.status.idle": "2026-06-10T20:37:24.319395Z",
     "shell.execute_reply": "2026-06-10T20:37:24.319019Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "session_len PSI: max before day 14 = 0.024, min after = 1.700\n",
      "every other feature, all 30 days: max PSI = 0.029\n",
      "[ ok ] detector fires on the right feature, on the right day, and is quiet otherwise\n"
     ]
    }
   ],
   "source": [
    "# the claim above as asserts: quiet before day 14, loud after, only for session_len\n",
    "sl = FEATURES.index(\"session_len\")\n",
    "before = psi_matrix[:DRIFT_DAY, sl].max()\n",
    "after = psi_matrix[DRIFT_DAY:, sl].min()\n",
    "others_max = np.delete(psi_matrix, sl, axis=1).max()\n",
    "print(f\"session_len PSI: max before day {DRIFT_DAY} = {before:.3f}, min after = {after:.3f}\")\n",
    "print(f\"every other feature, all 30 days: max PSI = {others_max:.3f}\")\n",
    "assert before < 0.1, \"session_len should be quiet (PSI < 0.1) before the planted drift\"\n",
    "assert after > 0.25, \"session_len should be significant (PSI > 0.25) from the drift day on\"\n",
    "assert others_max < 0.1, \"no other feature should ever cross the moderate line\"\n",
    "print(\"[ ok ] detector fires on the right feature, on the right day, and is quiet otherwise\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aa929c37",
   "metadata": {},
   "source": [
    "### Exercise 25.4 — A drift detector that returns alerts\n",
    "`Difficulty 2/5 · ~12 min`\n",
    "\n",
    "Turn the per-feature PSI into an alerting function. Fill in `detect_drift(reference_X, production_X, features, threshold)` that computes PSI per feature and returns a list of alert dicts, one per feature whose PSI exceeds `threshold`. Each alert is `{\"feature\": name, \"psi\": value, \"severity\": s}` where `s` is `\"high\"` if PSI > 0.5 else `\"medium\"`. The checks run it on day 20 (drifted) and day 5 (clean) and confirm it alerts on exactly `session_len` for the drifted day and on nothing for the clean day.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "2f5c580f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.320459Z",
     "iopub.status.busy": "2026-06-10T20:37:24.320341Z",
     "iopub.status.idle": "2026-06-10T20:37:24.325076Z",
     "shell.execute_reply": "2026-06-10T20:37:24.324661Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 25.4 fires on drift: not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] 25.4 quiet on clean: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def detect_drift(reference_X, production_X, features, threshold=0.25):\n",
    "    \"\"\"Return a list of alert dicts for every feature whose PSI exceeds threshold.\n",
    "    Each alert: {\"feature\": name, \"psi\": float, \"severity\": \"high\"|\"medium\"}.\"\"\"\n",
    "    alerts = []\n",
    "    for j, name in enumerate(features):\n",
    "        # TODO 1: compute this feature's PSI between the reference and production columns\n",
    "        value = None\n",
    "        attempted(value)\n",
    "        # TODO 2: if value > threshold, append an alert dict; severity is \"high\" if value > 0.5 else \"medium\"\n",
    "        raise NotImplementedError\n",
    "    return alerts\n",
    "\n",
    "# build a drifted day (day 20-like) and a clean day (day 5-like) to check against\n",
    "_drng = np.random.default_rng(SEED + 99)\n",
    "X_drifted, _ = make_day(_drng, N_DAY, drift=1.5)\n",
    "X_clean, _ = make_day(_drng, N_DAY, drift=0.0)\n",
    "\n",
    "def _fires_on_drift():\n",
    "    alerts = detect_drift(X_ref, X_drifted, FEATURES, threshold=0.25)\n",
    "    names = {a[\"feature\"] for a in alerts}\n",
    "    assert names == {\"session_len\"}, f\"expected exactly session_len to alert, got {names}\"\n",
    "    assert alerts[0][\"severity\"] in {\"high\", \"medium\"}, \"severity must be high or medium\"\n",
    "\n",
    "def _quiet_on_clean():\n",
    "    alerts = detect_drift(X_ref, X_clean, FEATURES, threshold=0.25)\n",
    "    assert alerts == [], f\"a clean day should produce no alerts, got {alerts}\"\n",
    "\n",
    "check(\"25.4 fires on drift\", _fires_on_drift)\n",
    "check(\"25.4 quiet on clean\", _quiet_on_clean)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "49027448",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>Loop over the features by index. For each, slice the reference and production columns (`reference_X[:, j]` and `production_X[:, j]`) and pass them to the `psi` function you already have. Then compare to the threshold and build the dict.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "for j, name in enumerate(features):\n",
    "    value = psi(reference_X[:, j], production_X[:, j])\n",
    "    if value > threshold:\n",
    "        severity = \"high\" if value > 0.5 else \"medium\"\n",
    "        alerts.append({\"feature\": name, \"psi\": value, \"severity\": severity})\n",
    "return alerts\n",
    "```\n",
    "Replace the `raise NotImplementedError` with the `if` block; the `return` is already there at the end.</details>\n",
    "\n",
    "<details><summary>Help — \"expected exactly session_len, got set()\"</summary>Your loop computed PSI but never appended. Make sure the `if value > threshold:` block actually runs `alerts.append(...)`, and that you removed the `raise NotImplementedError` (it would abort the loop on the first feature). Print `value` for each feature to confirm `session_len`'s PSI is above 0.25 and the others are not.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "40c7821e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.326146Z",
     "iopub.status.busy": "2026-06-10T20:37:24.326067Z",
     "iopub.status.idle": "2026-06-10T20:37:24.331735Z",
     "shell.execute_reply": "2026-06-10T20:37:24.331357Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 25.4 fires on drift\n",
      "[ ok ] 25.4 quiet on clean\n",
      "ALERT  feature=session_len psi=1.929  severity=high\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines detect_drift; the checks below re-verify the reference.\n",
    "def detect_drift(reference_X, production_X, features, threshold=0.25):\n",
    "    alerts = []\n",
    "    for j, name in enumerate(features):\n",
    "        value = psi(reference_X[:, j], production_X[:, j])\n",
    "        if value > threshold:\n",
    "            alerts.append({\"feature\": name, \"psi\": float(value),\n",
    "                           \"severity\": \"high\" if value > 0.5 else \"medium\"})\n",
    "    return alerts\n",
    "\n",
    "check(\"25.4 fires on drift\", _fires_on_drift, required=True)\n",
    "check(\"25.4 quiet on clean\", _quiet_on_clean, required=True)\n",
    "for a in detect_drift(X_ref, X_drifted, FEATURES):\n",
    "    print(f\"ALERT  feature={a['feature']:11} psi={a['psi']:.3f}  severity={a['severity']}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "880370d0",
   "metadata": {},
   "source": [
    "> **Common confusion:** drift in a feature is not the same as a drop in accuracy. `session_len` carries no signal in our label rule, so this drift does not, by itself, hurt the model. The lesson the chapter draws is exactly this: feature drift is the *earliest* warning, available before any label, but it is a warning to *investigate*, not a guarantee of harm. The next part measures harm directly.\n",
    "\n",
    "> **Key takeaways.** PSI, KL, and KS each answer \"are these two samples from the same distribution?\" with different trade-offs; build them from scratch once and check them against a reference so you trust them. A drift detector is a per-feature PSI plus a threshold plus a severity rule. Planting a known drift event is how you test a monitor, the same way a known ground truth is how you test a model.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "457f7690",
   "metadata": {},
   "source": [
    "## Part 3 — The regression gate\n",
    "\n",
    "> **Objectives.** Freeze a golden set, write a scorer, and build a CI-style regression gate that ships a model only when it does not regress. Then stage the deliberate failure: a candidate model that looks better *on average* but regresses a slice you care about, and watch a per-slice gate block the ship the headline-accuracy gate waved through.\n",
    "\n",
    "Testing an ML system is harder than asserting, because you usually do not know the right answer for an arbitrary input. The closest analog is an eval suite: a frozen golden set of cases, a scorer, and a gate that fails the build when the score regresses. This is eval-driven development, and it is the same shape whether the artifact is a classifier or an LLM prompt.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b29a469",
   "metadata": {},
   "source": [
    "### 3.1 A frozen golden set and a regression gate\n",
    "\n",
    "The golden set here is a fixed, seeded slice of labeled data that *never changes* between runs, so two models are scored on identical inputs. The scorer is accuracy. The gate fails if a candidate scores below `min_ratio` times the baseline. The `regression_gate` from the chapter prose, made executable and tested with three cases (a pass, a fail, and an improvement).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "53a31522",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.332838Z",
     "iopub.status.busy": "2026-06-10T20:37:24.332757Z",
     "iopub.status.idle": "2026-06-10T20:37:24.336467Z",
     "shell.execute_reply": "2026-06-10T20:37:24.336084Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "baseline golden-set accuracy: 0.732\n",
      "True (same model passes)\n",
      "False (a 20% drop fails)\n",
      "True (an improvement passes)\n"
     ]
    }
   ],
   "source": [
    "# the golden set: frozen at a fixed seed so every model is judged on identical rows\n",
    "golden_rng = np.random.default_rng(SEED + 123)\n",
    "X_gold, y_gold = make_day(golden_rng, 1000)\n",
    "\n",
    "def score_model(m, X, y):\n",
    "    \"\"\"The scorer. Here, accuracy on the golden set. Returns a float in [0, 1].\"\"\"\n",
    "    return float((m.predict(X) == y).mean())\n",
    "\n",
    "GATE_MIN_RATIO = 0.95   # ship policy: tolerate up to a 5% dip as eval noise; someone owns this number\n",
    "\n",
    "def regression_gate(candidate_score, baseline_score, min_ratio=GATE_MIN_RATIO):\n",
    "    \"\"\"Pass iff the candidate does not regress below min_ratio * baseline.\n",
    "    Returns (passed: bool, message: str). An improvement passes; a large dip fails.\"\"\"\n",
    "    ratio = candidate_score / max(baseline_score, 1e-9)\n",
    "    passed = ratio >= min_ratio\n",
    "    msg = (f\"candidate {candidate_score:.3f} is {ratio:.1%} of baseline \"\n",
    "           f\"{baseline_score:.3f} (gate at {min_ratio:.0%})\")\n",
    "    return passed, msg\n",
    "\n",
    "baseline_score = score_model(model, X_gold, y_gold)\n",
    "print(f\"baseline golden-set accuracy: {baseline_score:.3f}\")\n",
    "# three sanity cases for the gate logic itself\n",
    "print(regression_gate(baseline_score, baseline_score)[0], \"(same model passes)\")\n",
    "print(regression_gate(baseline_score * 0.80, baseline_score)[0], \"(a 20% drop fails)\")\n",
    "print(regression_gate(baseline_score * 1.05, baseline_score)[0], \"(an improvement passes)\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "808b9139",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.337496Z",
     "iopub.status.busy": "2026-06-10T20:37:24.337416Z",
     "iopub.status.idle": "2026-06-10T20:37:24.339557Z",
     "shell.execute_reply": "2026-06-10T20:37:24.339166Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] regression_gate blocks regressions, allows small noise, and allows improvements\n"
     ]
    }
   ],
   "source": [
    "# the gate's three behaviors as asserts (spec: claims that can be asserts, should be)\n",
    "assert regression_gate(0.92, 1.00, 0.95)[0] is False, \"an 8% drop must fail a 95% gate\"\n",
    "assert regression_gate(0.97, 1.00, 0.95)[0] is True,  \"a 3% drop must pass a 95% gate\"\n",
    "assert regression_gate(1.10, 1.00, 0.95)[0] is True,  \"an improvement must always pass\"\n",
    "print(\"[ ok ] regression_gate blocks regressions, allows small noise, and allows improvements\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "309d2ff6",
   "metadata": {},
   "source": [
    "> **Interpretation.** The gate is three lines and one threshold, but it is the whole discipline: a model ships only if a frozen, reproducible eval does not regress. The threshold (`min_ratio=0.95`) is a policy choice, \"tolerate up to a 5% dip as noise\", and like every threshold in this chapter, someone has to own it.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd270d58",
   "metadata": {},
   "source": [
    "### 3.2 The deliberate failure: a model that improves the average and breaks a slice\n",
    "\n",
    "Here is the trap the chapter warns about. We train a \"candidate\" model whose overall accuracy is within the gate's tolerance of the baseline (it passes the headline gate), but which is meaningfully *worse on a specific slice*: low-`affinity` users, our minority-experience segment. A gate that watches only headline accuracy waves it through. We watch that happen first, then fix the gate.\n",
    "\n",
    "> **Predict:** the candidate's overall golden-set accuracy is within 5% of the baseline, so the headline gate passes. Does that mean it is safe to ship? <details><summary>Answer</summary>No. \"Within tolerance on average\" can hide \"well outside tolerance on the slice that matters\". The headline number is an average over a population, and a small average dip can be a large dip concentrated in a small sub-population. That is the entire reason slice analysis exists.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "850fc949",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.340580Z",
     "iopub.status.busy": "2026-06-10T20:37:24.340506Z",
     "iopub.status.idle": "2026-06-10T20:37:24.345443Z",
     "shell.execute_reply": "2026-06-10T20:37:24.345007Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "baseline  overall golden accuracy: 0.732\n",
      "candidate overall golden accuracy: 0.710\n",
      "\n",
      "HEADLINE GATE: PASS — ships  (candidate 0.710 is 97.0% of baseline 0.732 (gate at 95%))\n"
     ]
    }
   ],
   "source": [
    "# Build a candidate that is fine on average but worse on a slice. We do it honestly:\n",
    "# the candidate's TRAINING labels were corrupted ONLY inside the low-affinity slice\n",
    "# (a labeling pipeline that is noisier for a minority segment) and clean everywhere else.\n",
    "# The golden set the gate scores on is always clean.\n",
    "SLICE_THRESHOLD = -1.0                                            # the low-affinity slice boundary\n",
    "corrupt_rng = np.random.default_rng(SEED + 7)\n",
    "y_cand_tr = ytr.copy()\n",
    "slice_tr = Xtr[:, FEATURES.index(\"affinity\")] < SLICE_THRESHOLD   # the slice, in TRAINING rows\n",
    "flip = slice_tr & (corrupt_rng.uniform(size=len(ytr)) < 0.8)      # flip 80% of that slice's labels\n",
    "y_cand_tr[flip] = 1 - y_cand_tr[flip]\n",
    "candidate = LogisticRegression(max_iter=1000, random_state=SEED).fit(Xtr, y_cand_tr)\n",
    "\n",
    "cand_score = score_model(candidate, X_gold, y_gold)\n",
    "print(f\"baseline  overall golden accuracy: {baseline_score:.3f}\")\n",
    "print(f\"candidate overall golden accuracy: {cand_score:.3f}\")\n",
    "passed, msg = regression_gate(cand_score, baseline_score)\n",
    "print(f\"\\nHEADLINE GATE: {'PASS — ships' if passed else 'FAIL — blocked'}  ({msg})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03cc0638",
   "metadata": {},
   "source": [
    "> **Interpretation.** The headline gate passed: overall accuracy did not regress past the 95% line, so a CI that checks only the average would ship this model. Now look at the slice the average hid.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "0f7ad266",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.346198Z",
     "iopub.status.busy": "2026-06-10T20:37:24.346120Z",
     "iopub.status.idle": "2026-06-10T20:37:24.348991Z",
     "shell.execute_reply": "2026-06-10T20:37:24.348677Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "low-affinity slice (165 rows of 1000):\n",
      "  baseline  slice accuracy: 0.830\n",
      "  candidate slice accuracy: 0.727\n",
      "\n",
      "PER-SLICE GATE: FAIL — blocked  (candidate 0.727 is 87.6% of baseline 0.830 (gate at 95%))\n",
      "\n",
      "[ ok ] the per-slice gate caught the regression the headline gate missed\n"
     ]
    }
   ],
   "source": [
    "# the same two models, scored ON THE SLICE whose training labels the candidate saw corrupted\n",
    "slice_mask_gold = X_gold[:, FEATURES.index(\"affinity\")] < SLICE_THRESHOLD\n",
    "Xs, ys = X_gold[slice_mask_gold], y_gold[slice_mask_gold]\n",
    "base_slice = score_model(model, Xs, ys)\n",
    "cand_slice = score_model(candidate, Xs, ys)\n",
    "print(f\"low-affinity slice ({slice_mask_gold.sum()} rows of {len(y_gold)}):\")\n",
    "print(f\"  baseline  slice accuracy: {base_slice:.3f}\")\n",
    "print(f\"  candidate slice accuracy: {cand_slice:.3f}\")\n",
    "slice_passed, slice_msg = regression_gate(cand_slice, base_slice)\n",
    "print(f\"\\nPER-SLICE GATE: {'PASS' if slice_passed else 'FAIL — blocked'}  ({slice_msg})\")\n",
    "# the demonstration only works if the headline gate let it through AND the slice gate catches it\n",
    "assert passed, \"the headline gate should have PASSED this candidate (that is the whole trap)\"\n",
    "assert not slice_passed, \"the per-slice gate should BLOCK this candidate; if it passed, the trap did not trigger\"\n",
    "print(\"\\n[ ok ] the per-slice gate caught the regression the headline gate missed\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "415f0fb3",
   "metadata": {},
   "source": [
    "> **Caveat:** the broken run was the *headline gate passing a model that hurts a slice*. The fix is not a different model, it is a *better gate*: evaluate per slice, not just on the average. This is the chapter's deliberate-failure beat, the eval-science version of \"accuracy lies on imbalanced data\". A gate that only watches the mean is a gate that ships slice regressions.\n",
    "\n",
    "> **Key takeaways.** A regression gate is a frozen golden set, a scorer, and a no-regression threshold. The threshold is a policy someone owns. Watching only the average is the named failure: stratify the eval by slice, because an average can rise while the slice you care about falls. The same gate generalizes from classifiers to LLM prompts unchanged; only the scorer differs.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a0acb89",
   "metadata": {},
   "source": [
    "### Exercise 25.5 — A slice-aware gate\n",
    "`Difficulty 3/5 · ~15 min`\n",
    "\n",
    "Generalize the gate so it cannot be fooled by an average. Fill in `slice_gate(base_model, cand_model, X, y, slices, min_ratio)`, where `slices` is a dict mapping a slice name to a boolean mask over the rows of `X`. The gate must pass only if the candidate does not regress on the *overall* set **and** on *every* named slice. Return `(passed, failures)` where `failures` is a list of the slice names (or `\"overall\"`) that regressed. The checks confirm it blocks the trap candidate from 3.2 (failing on the low-affinity slice) and passes the baseline against itself.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "5de37483",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.349845Z",
     "iopub.status.busy": "2026-06-10T20:37:24.349765Z",
     "iopub.status.idle": "2026-06-10T20:37:24.353579Z",
     "shell.execute_reply": "2026-06-10T20:37:24.353269Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 25.5 blocks the trap: not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] 25.5 passes identity: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def slice_gate(base_model, cand_model, X, y, slices, min_ratio=GATE_MIN_RATIO):\n",
    "    \"\"\"Pass only if cand does not regress overall AND on every named slice.\n",
    "    `slices`: {name: boolean mask over rows of X}. Returns (passed, failures).\"\"\"\n",
    "    failures = []\n",
    "    # TODO 1: check the OVERALL set first. Score both models on (X, y); if the\n",
    "    #         candidate regresses (use regression_gate), append \"overall\" to failures.\n",
    "    # TODO 2: for each (name, mask) in slices, score both models on X[mask], y[mask];\n",
    "    #         if the candidate regresses on that slice, append name to failures.\n",
    "    raise NotImplementedError\n",
    "    # TODO 3: passed is True iff failures is empty\n",
    "    return (len(failures) == 0), failures\n",
    "\n",
    "low_aff = X_gold[:, FEATURES.index(\"affinity\")] < SLICE_THRESHOLD\n",
    "high_aff = ~low_aff\n",
    "SLICES = {\"low_affinity\": low_aff, \"high_affinity\": high_aff}\n",
    "\n",
    "def _blocks_trap():\n",
    "    passed, failures = slice_gate(model, candidate, X_gold, y_gold, SLICES)\n",
    "    assert not passed, \"the trap candidate must be blocked\"\n",
    "    assert \"low_affinity\" in failures, f\"low_affinity should be among the failures, got {failures}\"\n",
    "\n",
    "def _passes_identity():\n",
    "    passed, failures = slice_gate(model, model, X_gold, y_gold, SLICES)\n",
    "    assert passed and failures == [], f\"a model vs itself must pass cleanly, got {failures}\"\n",
    "\n",
    "check(\"25.5 blocks the trap\", _blocks_trap)\n",
    "check(\"25.5 passes identity\", _passes_identity)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd089982",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>You already have `score_model` and `regression_gate`. Call them once on the overall `(X, y)`, then once per slice on `(X[mask], y[mask])`. Collect the names that fail. `passed` is just `len(failures) == 0`.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "failures = []\n",
    "if not regression_gate(score_model(cand_model, X, y), score_model(base_model, X, y), min_ratio)[0]:\n",
    "    failures.append(\"overall\")\n",
    "for name, mask in slices.items():\n",
    "    c = score_model(cand_model, X[mask], y[mask])\n",
    "    b = score_model(base_model, X[mask], y[mask])\n",
    "    if not regression_gate(c, b, min_ratio)[0]:\n",
    "        failures.append(name)\n",
    "return (len(failures) == 0), failures\n",
    "```\n",
    "Delete the `raise NotImplementedError`; the `return` at the bottom stays.</details>\n",
    "\n",
    "<details><summary>Help — \"the trap candidate must be blocked\" failed</summary>Your gate did not flag the low-affinity slice. Two likely causes: you only checked the overall set (forgot the slice loop), or you masked the wrong array (use `X[mask]` and `y[mask]` with the *same* mask). Print `score_model(cand_model, X[low_aff], y[low_aff])` versus the baseline; the candidate should be clearly lower there.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "1453c28d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.354466Z",
     "iopub.status.busy": "2026-06-10T20:37:24.354392Z",
     "iopub.status.idle": "2026-06-10T20:37:24.358678Z",
     "shell.execute_reply": "2026-06-10T20:37:24.358378Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 25.5 blocks the trap\n",
      "[ ok ] 25.5 passes identity\n",
      "slice_gate on the trap candidate: passed=False, failures=['low_affinity']\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines slice_gate; the checks below re-verify the reference.\n",
    "def slice_gate(base_model, cand_model, X, y, slices, min_ratio=GATE_MIN_RATIO):\n",
    "    failures = []\n",
    "    if not regression_gate(score_model(cand_model, X, y),\n",
    "                           score_model(base_model, X, y), min_ratio)[0]:\n",
    "        failures.append(\"overall\")\n",
    "    for name, mask in slices.items():\n",
    "        c = score_model(cand_model, X[mask], y[mask])\n",
    "        b = score_model(base_model, X[mask], y[mask])\n",
    "        if not regression_gate(c, b, min_ratio)[0]:\n",
    "            failures.append(name)\n",
    "    return (len(failures) == 0), failures\n",
    "\n",
    "check(\"25.5 blocks the trap\", _blocks_trap, required=True)\n",
    "check(\"25.5 passes identity\", _passes_identity, required=True)\n",
    "passed, failures = slice_gate(model, candidate, X_gold, y_gold, SLICES)\n",
    "print(f\"slice_gate on the trap candidate: passed={passed}, failures={failures}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "566e0ca7",
   "metadata": {},
   "source": [
    "## Part 4 — Reading a real training loop\n",
    "\n",
    "> **Objectives.** Read a real training loop the way an on-call engineer reads one: not to admire it, but to find where the landmines are. Annotate nanoGPT's loop, locate the `optimizer.zero_grad` placement and the `set_to_none` flag, and name the `AFTER_DEBUG`-style early-break the spec calls out, then run a tiny faithful version and a deliberately broken version and watch the bug.\n",
    "\n",
    "MLOps is, in large part, the discipline of reading other people's training loops under time pressure. The reproducibility of your whole system rests on details inside that loop: when gradients are zeroed, whether a debug break got left in, whether the seed is set. We read the canonical small one, Karpathy's nanoGPT `train.py`, and then reproduce its two named footguns in code small enough to watch.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b26aeaf5",
   "metadata": {},
   "source": [
    "### 4.1 The loop, annotated\n",
    "\n",
    "This is the operative core of nanoGPT's `train.py`, lightly condensed, with the three things an on-call read looks for marked. Read it top to bottom before running anything.\n",
    "\n",
    "```python\n",
    "while True:\n",
    "    lr = get_lr(iter_num) if decay_lr else learning_rate     # schedule first\n",
    "    for g in optimizer.param_groups: g['lr'] = lr\n",
    "\n",
    "    if iter_num % eval_interval == 0 and master_process:      # periodic eval + checkpoint\n",
    "        losses = estimate_loss()                             # @torch.no_grad, model.eval()/train()\n",
    "        ...\n",
    "    if iter_num == 0 and eval_only:                          # (A) a deliberate, FLAGGED early exit\n",
    "        break\n",
    "\n",
    "    for micro_step in range(gradient_accumulation_steps):    # accumulate grads over micro-batches\n",
    "        with ctx:\n",
    "            logits, loss = model(X, Y)\n",
    "            loss = loss / gradient_accumulation_steps        # scale so the SUM is a mean\n",
    "        X, Y = get_batch('train')                            # prefetch next batch\n",
    "        scaler.scale(loss).backward()                        # backward ACCUMULATES into .grad\n",
    "\n",
    "    if grad_clip != 0.0:\n",
    "        scaler.unscale_(optimizer); clip_grad_norm_(...)     # unscale before clipping\n",
    "    scaler.step(optimizer); scaler.update()                  # (B) the update\n",
    "    optimizer.zero_grad(set_to_none=True)                    # (C) zero AFTER stepping, set_to_none\n",
    "\n",
    "    iter_num += 1\n",
    "    if iter_num > max_iters: break                           # the real termination condition\n",
    "```\n",
    "\n",
    "The three marks:\n",
    "\n",
    "- **(A) The flagged early exit.** `if iter_num == 0 and eval_only: break` is an early `break`, but it is *gated by an explicit config flag* (`eval_only`) and documented. That is the correct form. The spec's named landmine is the *other* kind: `if i >= 1000: break  # AFTER_DEBUG`, an unflagged break left in after debugging that silently caps every future run at 1000 steps. One is a feature; one is a time bomb. The difference is the flag.\n",
    "- **(B) and (C) The zero-grad placement.** `backward()` *adds* into `.grad`; it does not replace. So gradients must be zeroed once per optimizer step. nanoGPT zeroes *after* `step()`, with `set_to_none=True`. The footgun is forgetting to zero at all, which silently sums gradients across steps and is the canonical \"deliberate failure\" the spec lists. We reproduce it in 4.3.\n",
    "- **`set_to_none=True`.** Sets `.grad` to `None` instead of a zero tensor. Slightly faster, and it surfaces a bug: if you read `.grad` before the next backward, you get `None` (a loud error) instead of a stale zero tensor (a silent wrong answer).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "282ee580",
   "metadata": {},
   "source": [
    "> **Common confusion:** \"the model trained, so the loop is correct.\" A training loop can run to completion, print a falling loss, and still be wrong, because a forgotten `zero_grad` makes the loss fall *for the wrong reason* (accumulated gradients act like a strange momentum). Running clean is not the same as being correct. That is why we reproduce the bug rather than just describe it.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e97d1cb",
   "metadata": {},
   "source": [
    "### 4.2 A faithful tiny loop (the correct version)\n",
    "\n",
    "We strip nanoGPT to its skeleton on a problem with no autograd framework at all: fit a 1-D linear model $y = wx + b$ by gradient descent on synthetic data with a known answer ($w=2$, $b=-1$). The four-comment training-loop skeleton (`forward / backward / update / track`) is the muscle-memory shape; the key line is the gradient *reset* at the top of each step.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "9173d849",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.359554Z",
     "iopub.status.busy": "2026-06-10T20:37:24.359475Z",
     "iopub.status.idle": "2026-06-10T20:37:24.366981Z",
     "shell.execute_reply": "2026-06-10T20:37:24.366583Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "recovered w=1.998 (true 2.0), b=-1.002 (true -1.0)\n",
      "final MSE: 0.0026\n"
     ]
    }
   ],
   "source": [
    "# synthetic regression with a KNOWN answer: y = 2x - 1 + small noise\n",
    "gd_rng = np.random.default_rng(SEED)\n",
    "x_gd = gd_rng.uniform(-2, 2, 256)\n",
    "y_gd = 2.0 * x_gd - 1.0 + gd_rng.normal(0, 0.05, 256)\n",
    "STEPS_GD = 60 if FAST else 400\n",
    "\n",
    "def train_linear(zero_each_step=True):\n",
    "    w, b = 0.0, 0.0\n",
    "    lr = 0.05\n",
    "    gw, gb = 0.0, 0.0          # the gradient accumulators (the analog of param.grad)\n",
    "    losses = []\n",
    "    for step in range(STEPS_GD):\n",
    "        # reset: the analog of optimizer.zero_grad(). If False, we never zero (the bug).\n",
    "        if zero_each_step:\n",
    "            gw, gb = 0.0, 0.0\n",
    "        # forward\n",
    "        pred = w * x_gd + b\n",
    "        err = pred - y_gd\n",
    "        # backward (accumulate into gw, gb, exactly as .backward() adds into .grad)\n",
    "        gw = gw + (2 * err * x_gd).mean()\n",
    "        gb = gb + (2 * err).mean()\n",
    "        # update\n",
    "        w -= lr * gw; b -= lr * gb\n",
    "        # track\n",
    "        losses.append(float((err ** 2).mean()))\n",
    "    return w, b, losses\n",
    "\n",
    "w_fit, b_fit, loss_curve = train_linear(zero_each_step=True)\n",
    "print(f\"recovered w={w_fit:.3f} (true 2.0), b={b_fit:.3f} (true -1.0)\")\n",
    "print(f\"final MSE: {loss_curve[-1]:.4f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "1b37ebc4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.367751Z",
     "iopub.status.busy": "2026-06-10T20:37:24.367676Z",
     "iopub.status.idle": "2026-06-10T20:37:24.369629Z",
     "shell.execute_reply": "2026-06-10T20:37:24.369318Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] the faithful loop recovers the planted parameters\n"
     ]
    }
   ],
   "source": [
    "# ground-truth recovery as an assert (the strongest self-check the spec lists)\n",
    "assert abs(w_fit - 2.0) < 0.1 and abs(b_fit + 1.0) < 0.1, \\\n",
    "    f\"the correct loop must recover w~2, b~-1; got w={w_fit:.3f}, b={b_fit:.3f}\"\n",
    "print(\"[ ok ] the faithful loop recovers the planted parameters\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "86abe460",
   "metadata": {},
   "source": [
    "> **Interpretation.** Zeroing the gradient each step, the loop recovers $w=2$, $b=-1$ to two decimals, exactly the planted answer. The four-comment skeleton (forward, backward, update, track) is the same in every training loop you will ever read, including nanoGPT's. Now we break the one line that matters.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d5420cc",
   "metadata": {},
   "source": [
    "### 4.3 The deliberate failure: the forgotten `zero_grad`\n",
    "\n",
    "Run the *same* loop with `zero_each_step=False`. Gradients now accumulate across steps, the way they would if you deleted `optimizer.zero_grad()` from nanoGPT's loop. Watch what happens to the recovered parameters and the loss.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "9753c432",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.370360Z",
     "iopub.status.busy": "2026-06-10T20:37:24.370285Z",
     "iopub.status.idle": "2026-06-10T20:37:24.574399Z",
     "shell.execute_reply": "2026-06-10T20:37:24.573819Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BUGGED loop: recovered w=1.319 (true 2.0), b=-0.675 (true -1.0)\n",
      "BUGGED final MSE: 0.0128  (correct loop reached 0.0026)\n"
     ]
    },
    {
     "data": {
      "image/png": "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mtdnzpxJAJCLLrqIvXbnnXeSoqIismrVKs2xN998M/H7/WTTpk2EEEJ2795NAJA77rjDcE7Tp08nZlOY/nfomC+55BLNcZMnTybdu3dn3y9cuJAAIL/61a80x02dOtV0HDzmz59PAJBnn31W83o8Hic9evQg++67L4lGo+z1t956iwAgt99+u+X76nHfffcRAOT5559nr51wwglkv/32I21tbey1dDpNDj/8cDJs2DD2Gr0XjjzySJJMJtnru3btIqFQiJx00kkklUqx1x999FECgDzzzDOWY7r//vsJALJ+/XrDzwCQUChE1qxZw15bunQpAUAeeeQR9tqll15KevfuTWpqajS/f/7555OysjLS2tpq+vczuXZm9zAhRPg3/v73vxMA5PPPP2evnXnmmaSgoIBs3LiRvbZ8+XLi9/sN9+XAgQP36v5fv349AUC6d+9Oamtr2XH//ve/CQDyn//8h71m9Tx48JDP8KQFHjz8iDB79mx88MEHmn8AsH37dixZsgRTp05Ft27d2PFjxozBiSeeiLffftvwXr/85S8133/00UdIJpOYNm2a5vWrr77a8Lv/+Mc/cNRRR6GiogI1NTXs34QJE5BKpfD55593xOnajvmoo47Cnj170NjYCAAs1e3kHDLBggULsGvXLkybNk2j3z311FMxcuRI/Pe//3X8Xp988glmzJiBq6++Gr/4xS8AALW1tfj4449x7rnnoqmpiX2ee/bswcSJE7F69WqDhOGyyy6D3+9n33/44YeIx+P41a9+pSlMuuyyy1BaWprRGEWYMGEChgwZwr4fM2YMSktLsW7dOgAAIQSvvfYaJk2aBEKI5r6YOHEiGhoasGjRItP335trp78fACASibCv29raUFNTg0MPPRQA2N9PpVJ47733cOaZZ2LAgAHs+H322QcTJ040/XsUmd7/5513HioqKtj3lO2nn50HDz9meNICDx5+RDj44IOFxV4bN24EAIwYMcLws3322QfvvfeeoRhG735A32Po0KGa17t166ZZhAFg9erV+PbbbzWpZx67du1ycDaZgw86ALBx1dXVobS0FBs3boTP5zOcm/6cMoXV5zty5Eh8+eWXjt5ny5YtOO+883DEEUfgj3/8I3t9zZo1IITgtttuw2233Sb83V27dqFv377se7Prpx9jKBTC4MGD2c/3FvrPHpA//7q6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22RG7t/vf4ADjj0e4XAY1dXVePDBBx2Pa/v27Tj11FMRiUQwaNAgvPTSS6iursbDDz/MjpEkCY899hhOP/10FBUV4e6770YqlcKll16KQYMGIRKJYMSIEfjTn/6kee9UKoXrrrsO5eXl6N69O37zm98YOnyK8OWXX+LYE45Hj/0PxD7Hn4Ab77pb8zm88MILGD9+PEpKStCrVy/87Gc/Y/6iFMuWLcNpp52G0tJSlJSU4KijjsLatWs1xzzwwAPo3bs3unfvjunTp1sGaUuXLsVxxx2HkpISlJaW4sADD8SCBQvw6aef4uKLL0ZDQwO7pjNnzgQAxGIx3HDDDejbty+KiopwyCGH4NNPP2Xv+eI//on+hxyGtz78CKPGH4SCggJMnDgRmzdvtvx8Xn75ZUyaNMnw+jPPPIPRo0cjHA6jd+/euOqqq9jPNm3ahDPOOAPFxcUoLS3Fueeei507d7Kfz5w5E+PGjcNf//pXDBo0iFm5ia49APz73//GAQccgIKCAgwePBizZs1CMplk79fQ0IBr75iFgaP3Q0FBAfbdd1+89dZblp+X3+/HT37yE7z88suW598RyDiQXbdunfBDP/300/Oqk0UuIxiQbbgAT17QkdA2RPAChVyD5POx65aPqXeta0Hu3p+EAK3R7PxzEJcBAPr374/t27ezf4sXL0b37t1x9NFHA5D91k8++WScffbZ+Pbbb/HKK6/gyy+/1AQjgBx8jR07FosXL8ZN06dh8bJluOi663HOGafju+++w8yZM3Hbbbdhzpw5jsY1ZcoUbNu2DZ9++ilee+01PPnkk4agEJADncmTJ+O7777DJZdcgnQ6jX79+uEf//gHli9fjttvvx2//e1v8eqrr7LfefDBBzFnzhw888wz+PLLL1FbW4vXX3/dcjz0czjrzMn46vV/Yc6DD+DrhYs0n0MikcCdd96JpUuX4o033sCGDRswdepU9vOtW7fi6KOPRjgcxscff4yFCxfikksu0QRan3zyCdauXYtPPvkEzz33HObMmWP5mV144YXo168f5s+fj4ULF+Lmm29GMBjE4YcfjocffhilpaXs2t5www0AgKuuugpff/01Xn75ZXz77bc455xzcPLJJ2P16tXym0oSWqNR3P/kk3jmsb9g7ty5qK+vx/nnn2/5GX355ZcYP3685rXHHnsM06dPx+WXX47v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7NkTJSUluPTSS9HW1qb5+/o0dzqdxj333INBgwYhEolg7Nix+Oc//8l+TtPQH330EcaPH4/CwkIcfvjhWLlyJQBgzpw5mDVrFpYuXQpJkiBJEubMmQNCCGbOnIkBAwYgHA6jT58+uOYac+ePtWvX4owzzkDPnj1RXFyMgw46CB9++CF4aUEsFsNNN92E/v37IxwOY+jQoXj66afZeyxbtgynnXYaSktLUVJSgqOOOgpr18pMjP4zBoAzzzwTU6dO1Xzud911F6ZMmYLi4mIMHDgQb775Jnbv3o0zzjgDxcXFGDNmDBYsWMB+R8RGP/zww5bs57vvvosjjzwS5eXlqOrVG+dcOQ3rNm1iQe2gQXK19f777w9JknDsscey3/3rX/+KffbZBwUFBRg5ciT+8pe/mP4dAHj77bcRDodx6KHG+oy5c+dizJgxKCgowKGHHorvv/8+o/NKJpO45pprUF5eju7du+Omm27CRRddpN5fyvn8ZMLxePnlly3HmQ04DmT11WodVb12yimn4K677sLkyZOFP//jH/+Iyy67DBdffDFGjRqFxx9/HIWFhXjmmWfYMUuWLMH3339v+NenT5+MxhKLxdDY2Kj5lw+g0oKV64FUKrtj8eDBQ+eABa8+n8aKK9dACEEq2pqVf5msa9dddx3mzp2LN998Ex988AG++OILLFq0yHDcAw88gLFjx2Lx4sW47bbbAAAlJSWYM2cOli9fjj/96U946qmn8Ocn/8p+5x//eh0zZ87E73//eyxYsAC9e/e2DXTuuecePP/883j88cexbNky/PrXv8bPf/5zfPbZZ5rjbrnlFjz44INYsGABAoEALrnkEgDAeeedh+uvvx6jR4/G9u3bsX37dpx33nl47bXX8NBDD+GJJ57A6tWr8cYbb2C//fYzHUdzczN+8pOf4KOPPsLixYtx8sknY9KkSdi8bbt8gOTDlClT8Pe//x1//vOfsWLFCjzxxBMoLi4GAGzduhVHH300wuEwPv74YyxcuBCXXHIJksmk/UXh8NBDD+GII47A4sWLceqpp+IXv/gFpkyZgp///OdYtGgRhgwZgilTprQrlmlpacF1112HBQsW4MP334fP58OF11yLNEkDAMsef/jhh9i+fTv+9a9/AQD+9re/4fbbb8fdd9+NFStW4Pe//z1uu+02PPfcc6Z/64svvsCBBx4o/NmNN96IBx98EPPnz0dVVRUmTZqERCIhPFaEP/zhD/jb3/6GZ599FnPnzkVjY6MwO37gmLGYN2+eJhvvBrhaSBWPx7Fw4ULMmDGDvebz+TBhwgR8/fXXHf737rnnHsyaNavD3zfbGNgHKAgDbTFgw1ZgyIBsj8iDBw8dDRrI5ro+Nt0WxZcnHJyVv33kR/McdX1ramrCc889h5deegknnHACAODZZ58VkifHH3+8oYnQrbfeyr6urq7GDTfcgL+/8AKuufACAMAjjz2GSy+9lKWR77rrLnz44YcGVpYiFovh97//PT788EMcdthhAIDBgwfjyy+/xBNPPIFjjjmGHXv33Xez72+++WaceuqpaGtrQyQSQXFxMQKBAHr16sWO37RpE3r16oUJEyYgGAxiwIABOPhg8+szduxYjB07ln1/55134vXXX8d/P/oQl597LlatXYtXX30VH3zwASZMmMDGSjF79myUlZXh5ZdfRjAoS2SGDx9u+vfM8JOf/ARXXHEFAOD222/HY489hoMOOgjnnHMOAJkVP+yww7Bz507N+WaCs88+m31NCMFffn83Bh12BFas+AH7jRmDqqoqAED37t01f+OOO+7Agw8+iLPOOguAzNwuX74cTzzxBC666CLh39q4caMpOXfHHXfgxBNPBAA899xz6NevH15//XWce+65js7jkUcewYwZMxih+Oijj+Ltt9/mjpAZ2d49eyAej2PHjh0YOHCgo/fuCrh6215TU4NUKoWePXtqXu/Zsyd27Njh+H0mTJiAc845B2+//Tb69etnGgTPmDEDDQ0N7N/mzZvbNX63wO8HRip+sp5O1oOH/AQLZD1ZQadj3bp1SCQSmoCurKwMI0YY+4CPHz/e8Norr7yCI444Ar169UJxcTFuvfVWbOakej+sWolDDjlE8zs0QBVhzZo1aG1txYknnoji4mL27/nnn2cpeYoxY8awr3v37g0AGqmeHueccw6i0SgGDx6Myy67DK+//rolO9rc3IwbbrgB++yzD8rLy1FcXIwVK1Zg646dAIDvli+H3+/XBNc8lixZgqOOOooFsXsL/jxpDMEzyfQ1q3O3w+rVq3HBBRdg8ODBKCsrw74TTgIAbN6yxfR3WlpasHbtWlx66aWaa3XXXXcZrhWPaDSKggKxWw5/b3Tr1g0jRozAihUrHJ1DQ0MDdu7cqbmX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",
      "text/plain": [
       "<Figure size 700x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "w_bug, b_bug, loss_bug = train_linear(zero_each_step=False)\n",
    "print(f\"BUGGED loop: recovered w={w_bug:.3f} (true 2.0), b={b_bug:.3f} (true -1.0)\")\n",
    "print(f\"BUGGED final MSE: {loss_bug[-1]:.4f}  (correct loop reached {loss_curve[-1]:.4f})\")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(7, 4))\n",
    "ax.plot(loss_curve, label=\"zero_grad each step (correct)\", c=\"#1E40FF\")\n",
    "ax.plot(loss_bug, label=\"gradients accumulate (bug)\", c=\"#c0392b\")\n",
    "ax.set_yscale(\"log\"); ax.set_xlabel(\"step\"); ax.set_ylabel(\"MSE (log)\")\n",
    "ax.set_title(\"Forgetting to zero the gradient\"); ax.legend(); plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c5cd97b6",
   "metadata": {},
   "source": [
    "> **What is the interpretation of this plot?** <details><summary>Answer</summary>The buggy curve either diverges or settles to a clearly worse loss, and the recovered $w$, $b$ miss the planted answer, because each step's \"gradient\" is the sum of every gradient seen so far. That accumulation acts like an ever-growing, unstable momentum. The tell in production is subtle: the loss may still fall for a while, so the run does not crash, it just learns the wrong thing. This is why the spec lists \"gradient accumulation without zeroing\" as a canonical deliberate-failure demo, and why an on-call read looks for `zero_grad` first.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "886d2527",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.575479Z",
     "iopub.status.busy": "2026-06-10T20:37:24.575377Z",
     "iopub.status.idle": "2026-06-10T20:37:24.577648Z",
     "shell.execute_reply": "2026-06-10T20:37:24.577337Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] the forgotten zero_grad measurably broke the fit, exactly as intended\n"
     ]
    }
   ],
   "source": [
    "# assert the bug actually broke the fit (so this demo cannot silently rot into a pass)\n",
    "assert abs(w_bug - 2.0) > 0.15 or loss_bug[-1] > loss_curve[-1] * 2, \\\n",
    "    \"the forgotten-zero_grad bug should visibly hurt the fit; if not, the demo is no longer demonstrating it\"\n",
    "print(\"[ ok ] the forgotten zero_grad measurably broke the fit, exactly as intended\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2b852e1f",
   "metadata": {},
   "source": [
    "> **Key takeaways.** Reading a training loop is an on-call skill. Look for three things: an unflagged early `break` (the `AFTER_DEBUG` landmine), where gradients are zeroed (`backward` accumulates, so a forgotten `zero_grad` silently corrupts training), and whether the seed is set. A loop that runs clean is not a loop that is correct; reproduce the bug to prove the difference.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "493fae7c",
   "metadata": {},
   "source": [
    "## Part 5 — The `np.trapezoid` case study\n",
    "\n",
    "> **Objectives.** Reproduce this repository's own CI outage: a metric computation calling `np.trapz`, deprecated in NumPy 2.0 and later removed, eight CI runs red the moment an unpinned upgrade crossed the removal boundary. Capture the live deprecation signal, read the post-mortem, ship the one-line `np.trapezoid` fix, and turn the pinned-versus-unpinned dependency lesson into an executable contrast.\n",
    "\n",
    "This is not a hypothetical. It is the case study the chapter's anchor row names: the repo's own dependency post-mortem. The lesson MLOps draws is that the artifact is `(code, data, config, environment) -> model`, and the *environment* is part of the source code. An unpinned dependency is an unpinned input.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "83627de7",
   "metadata": {},
   "source": [
    "### 5.1 The metric that called `np.trapz`\n",
    "\n",
    "A common metric is the area under a curve, computed by the trapezoidal rule. Average precision, an ROC AUC computed by hand, a calibration-area metric, all integrate a curve. The integration was written, years ago, as `np.trapz(y, x)`. It worked for years. Here is the metric, written the way it was written then.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "2c1a1a5c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.578530Z",
     "iopub.status.busy": "2026-06-10T20:37:24.578457Z",
     "iopub.status.idle": "2026-06-10T20:37:24.581267Z",
     "shell.execute_reply": "2026-06-10T20:37:24.580993Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "area under the curve: 0.7500  (analytic answer: 0.7500)\n",
      "[ ok ] the metric computes the hand-checkable area correctly\n"
     ]
    }
   ],
   "source": [
    "# the curve we integrate: a precision-recall-shaped curve, area computable by hand for a check\n",
    "recall_grid = np.linspace(0.0, 1.0, 101)\n",
    "precision_curve = 1.0 - 0.5 * recall_grid          # a clean line from 1.0 down to 0.5\n",
    "\n",
    "def area_under_curve_legacy(y, x):\n",
    "    \"\"\"Area under a curve by the trapezoidal rule, AS ORIGINALLY WRITTEN (NumPy < 2.0).\"\"\"\n",
    "    return float(np.trapezoid(y, x))   # <- in the real outage this line read np.trapz(y, x)\n",
    "\n",
    "# the analytic area under y = 1 - 0.5x from 0 to 1 is a trapezoid: 0.5*(1.0 + 0.5)*1 = 0.75\n",
    "area = area_under_curve_legacy(precision_curve, recall_grid)\n",
    "print(f\"area under the curve: {area:.4f}  (analytic answer: 0.7500)\")\n",
    "assert abs(area - 0.75) < 1e-6, \"trapezoidal area of this line must be 0.75 by hand\"\n",
    "print(\"[ ok ] the metric computes the hand-checkable area correctly\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0ee6aee1",
   "metadata": {},
   "source": [
    "> **Note:** the function above already uses `np.trapezoid`, the correct NumPy 2.x name, so the notebook runs clean today. In the real outage, that line read `np.trapz`. The next cell reproduces the signal NumPy actually emits on the old name, the `DeprecationWarning` that is the early warning, and explains the `AttributeError` it becomes once removal lands.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "16cb6012",
   "metadata": {},
   "source": [
    "### 5.2 Reproducing the signal\n",
    "\n",
    "The precise sequence: NumPy 2.0 **deprecated** `np.trapz` (every call emits a `DeprecationWarning` pointing you at `np.trapezoid`), and the name is **scheduled for removal** in a later release. On a future NumPy where it is gone, the same call raises `AttributeError`, which is what an unpinned upgrade detonated across the repo's CI. We reproduce the real, current signal (the warning) and name the endpoint (the removal), so nothing here is described that the kernel cannot show.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "be00a84d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.582239Z",
     "iopub.status.busy": "2026-06-10T20:37:24.582126Z",
     "iopub.status.idle": "2026-06-10T20:37:24.585031Z",
     "shell.execute_reply": "2026-06-10T20:37:24.584744Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "DeprecationWarning captured (this is the canary's early signal):\n",
      "   `trapz` is deprecated. Use `trapezoid` instead, or one of the numerical integration functions in `scipy.integrate`.\n",
      "\n",
      "np.trapz still returns 0.7500 today, but it is on death row.\n",
      "On a future NumPy where removal lands, this same line raises AttributeError\n",
      "(the exact failure that turned the repo's CI red on 8/8 legs at once).\n"
     ]
    }
   ],
   "source": [
    "import warnings\n",
    "# call the deprecated name on purpose and CAPTURE whatever NumPy emits, so the signal is real.\n",
    "with warnings.catch_warnings(record=True) as caught:\n",
    "    warnings.simplefilter(\"always\")\n",
    "    try:\n",
    "        value = np.trapz(precision_curve, recall_grid)   # the name that worked for years\n",
    "        dep = [w for w in caught if issubclass(w.category, DeprecationWarning)]\n",
    "        if dep:\n",
    "            print(\"DeprecationWarning captured (this is the canary's early signal):\")\n",
    "            print(f\"   {dep[0].message}\")\n",
    "            print(f\"\\nnp.trapz still returns {value:.4f} today, but it is on death row.\")\n",
    "            print(\"On a future NumPy where removal lands, this same line raises AttributeError\")\n",
    "            print(\"(the exact failure that turned the repo's CI red on 8/8 legs at once).\")\n",
    "        else:\n",
    "            print(f\"np.trapz returned {value:.4f} with no warning — you are on NumPy < 2.0.\")\n",
    "    except AttributeError as e:\n",
    "        print(\"AttributeError: np.trapz has been removed on this NumPy.\")\n",
    "        print(f\"   {e}\\nThe replacement is np.trapezoid.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "11ce768a",
   "metadata": {},
   "source": [
    "> **Interpretation.** On NumPy 2.2 the call still works but emits a `DeprecationWarning`: the early signal a canary is built to catch. A deprecation today is a removal tomorrow; the repo's CI went red on the removal endpoint, the `AttributeError`, because nothing was watching the warning. Nothing in the repo's *code* changed between green and red; an *unpinned dependency* crossed the removal boundary. The metric was correct; the environment moved out from under it. That is the MLOps-specific failure mode: your artifact is a function of an environment you did not version.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cb440dbd",
   "metadata": {},
   "source": [
    "### 5.3 The post-mortem and the one-line fix\n",
    "\n",
    "A blameless post-mortem answers three questions: what happened, why monitoring did not catch it earlier, and what new test would have. For this outage:\n",
    "\n",
    "- **What happened.** An unpinned NumPy floated across a major-version boundary on a routine CI run. `np.trapz`, deprecated in 2.0 and then removed, raised `AttributeError` inside a metric. Eight of eight matrix legs went red at once, because every leg shared the same removed call.\n",
    "- **Why it was not caught earlier.** There was no pinned constraints file and no canary job running the notebooks against unpinned-latest *before* the removal landed. The `DeprecationWarning` had been emitting for a full release cycle, but nobody was reading warnings, so the first signal anyone saw was the whole matrix going red.\n",
    "- **The test that would have caught it.** A weekly canary executing every notebook against unpinned-latest NumPy, with warnings promoted to failures, opens an issue on the *first* deprecation warning, so the fix lands during the deprecation window, before the removal does. The simultaneous-red-on-all-legs is itself a signature: a shared-dependency break, not a per-chapter logic bug.\n",
    "\n",
    "The fix is one line: `np.trapz` becomes `np.trapezoid`. We verify the new name gives the same answer the old one did.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "dbe8bf9d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.586041Z",
     "iopub.status.busy": "2026-06-10T20:37:24.585961Z",
     "iopub.status.idle": "2026-06-10T20:37:24.588691Z",
     "shell.execute_reply": "2026-06-10T20:37:24.588372Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "np.trapezoid area: 0.750000  (analytic 0.750000)\n",
      "[ ok ] np.trapezoid reproduces the old answer and matches an independent trapezoid sum\n"
     ]
    }
   ],
   "source": [
    "def area_under_curve_fixed(y, x):\n",
    "    \"\"\"The fix: np.trapezoid, the NumPy 2.x name, identical math to np.trapz.\"\"\"\n",
    "    return float(np.trapezoid(y, x))\n",
    "\n",
    "fixed = area_under_curve_fixed(precision_curve, recall_grid)\n",
    "print(f\"np.trapezoid area: {fixed:.6f}  (analytic 0.750000)\")\n",
    "assert abs(fixed - 0.75) < 1e-9, \"the fixed metric must still give the hand-checked 0.75\"\n",
    "# and it agrees with a fully independent trapezoid sum, proving the fix is not just 'runs'\n",
    "manual = float(np.sum((precision_curve[:-1] + precision_curve[1:]) / 2 * np.diff(recall_grid)))\n",
    "assert abs(fixed - manual) < 1e-9, \"np.trapezoid must match a hand-rolled trapezoid sum\"\n",
    "print(\"[ ok ] np.trapezoid reproduces the old answer and matches an independent trapezoid sum\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0952cb7d",
   "metadata": {},
   "source": [
    "> **Caveat:** the deeper fix is not the rename; it is the *process*. Pin an exact-version constraints file for the PR gate so a green PR means a reproducible environment, and run a separate weekly canary against unpinned-latest so the next removal is caught by you, on your schedule, instead of by a user. The rename closes this incident; the canary prevents the next one.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6b4ed722",
   "metadata": {},
   "source": [
    "### Exercise 25.6 — A version-robust integrator\n",
    "`Difficulty 2/5 · ~10 min`\n",
    "\n",
    "Write the wrapper that would have made this outage a non-event: `safe_trapezoid(y, x)` that uses `np.trapezoid` if it exists and falls back to `np.trapz` on older NumPy, so the *same code* runs on 1.x and 2.x. (In real code you would pin versions; this wrapper is the belt to the pin's suspenders.) The checks confirm it gives the hand-checked area and matches an independent trapezoid sum.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "83992f36",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.589562Z",
     "iopub.status.busy": "2026-06-10T20:37:24.589489Z",
     "iopub.status.idle": "2026-06-10T20:37:24.592835Z",
     "shell.execute_reply": "2026-06-10T20:37:24.592576Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 25.6 safe area: not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] 25.6 safe vs manual: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 32,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def safe_trapezoid(y, x):\n",
    "    \"\"\"Trapezoidal integral that works on NumPy 1.x and 2.x. Prefer np.trapezoid;\n",
    "    fall back to np.trapz only if trapezoid is absent (i.e. NumPy < 2.0).\"\"\"\n",
    "    # TODO 1: if numpy has the attribute \"trapezoid\", use np.trapezoid(y, x)\n",
    "    # TODO 2: otherwise (older numpy) fall back to np.trapz(y, x)\n",
    "    # hint: getattr / hasattr on the np module is the clean way to branch\n",
    "    result = None\n",
    "    attempted(result)\n",
    "    return float(result)\n",
    "\n",
    "def _safe_area():\n",
    "    got = safe_trapezoid(precision_curve, recall_grid)\n",
    "    check_close(got, 0.75, atol=1e-6, msg=\"area of y=1-0.5x on [0,1] is 0.75\")\n",
    "\n",
    "def _safe_matches_manual():\n",
    "    got = safe_trapezoid(precision_curve, recall_grid)\n",
    "    manual = float(np.sum((precision_curve[:-1] + precision_curve[1:]) / 2 * np.diff(recall_grid)))\n",
    "    check_close(got, manual, atol=1e-9, msg=\"must match a hand-rolled trapezoid sum\")\n",
    "\n",
    "check(\"25.6 safe area\", _safe_area)\n",
    "check(\"25.6 safe vs manual\", _safe_matches_manual)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "55977073",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>Branch on whether the function exists. `hasattr(np, \"trapezoid\")` is `True` on NumPy 2.x and `False` on 1.x. Pick the function accordingly, then call it on `(y, x)`.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "fn = np.trapezoid if hasattr(np, \"trapezoid\") else np.trapz\n",
    "result = fn(y, x)\n",
    "```\n",
    "That is the whole body. The `getattr(np, \"trapezoid\", np.trapz)` one-liner is equivalent.</details>\n",
    "\n",
    "<details><summary>Help — \"name 'np' is not defined\" or it still raised AttributeError</summary>If you wrote `np.trapezoid(y, x)` unconditionally and you are somehow on old NumPy, you get the same crash you are trying to avoid. The point is the *branch*: check `hasattr` (or use `getattr(np, \"trapezoid\", np.trapz)`) so the call resolves to whatever exists. On this kernel (`np.trapezoid` present) both branches must give 0.75.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "9210a399",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.593803Z",
     "iopub.status.busy": "2026-06-10T20:37:24.593721Z",
     "iopub.status.idle": "2026-06-10T20:37:24.596338Z",
     "shell.execute_reply": "2026-06-10T20:37:24.595986Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 25.6 safe area\n",
      "[ ok ] 25.6 safe vs manual\n",
      "safe_trapezoid area: 0.750000\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines safe_trapezoid; the checks below re-verify the reference.\n",
    "def safe_trapezoid(y, x):\n",
    "    fn = np.trapezoid if hasattr(np, \"trapezoid\") else np.trapz\n",
    "    return float(fn(y, x))\n",
    "\n",
    "check(\"25.6 safe area\", _safe_area, required=True)\n",
    "check(\"25.6 safe vs manual\", _safe_matches_manual, required=True)\n",
    "print(f\"safe_trapezoid area: {safe_trapezoid(precision_curve, recall_grid):.6f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b3838326",
   "metadata": {},
   "source": [
    "### 5.4 Closing the loop: detect drift, retrain, promote\n",
    "\n",
    "We now run the whole loop end to end once, so the five stages connect. Serve a drifted day through the `MLLoop`, monitor it, see the alert, retrain on fresh reference data, and promote the new model by flipping the `prod` alias. This is the `retrain` edge the earlier parts left dangling, and it is a single assignment, exactly as the registry section promised.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "136c5859",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.597237Z",
     "iopub.status.busy": "2026-06-10T20:37:24.597164Z",
     "iopub.status.idle": "2026-06-10T20:37:24.605080Z",
     "shell.execute_reply": "2026-06-10T20:37:24.604741Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "monitor on served traffic: 1 alert(s) -> ['session_len']\n",
      "promoted: prod alias now points at a new model object: True\n",
      "[ ok ] loop closed: served -> monitored -> alerted -> retrained -> promoted\n"
     ]
    }
   ],
   "source": [
    "# serve a drifted day, monitor it, and close the loop with a retrain + promote\n",
    "loop2 = MLLoop(X_ref)\n",
    "loop2.register(model, alias=\"prod\")\n",
    "X_prod_day, _ = make_day(np.random.default_rng(SEED + 321), N_DAY, drift=1.5)\n",
    "_ = loop2.serve(X_prod_day)\n",
    "served = loop2.served_features()\n",
    "alerts = detect_drift(X_ref, served, FEATURES, threshold=0.25)\n",
    "print(f\"monitor on served traffic: {len(alerts)} alert(s) -> {[a['feature'] for a in alerts]}\")\n",
    "\n",
    "# retrain on fresh reference-distribution data and PROMOTE by flipping the alias\n",
    "X_new, y_new = make_day(np.random.default_rng(SEED + 654), N_REF)\n",
    "model_v2 = LogisticRegression(max_iter=1000, random_state=SEED).fit(X_new, y_new)\n",
    "old_id = id(loop2.registry[\"prod\"])\n",
    "loop2.register(model_v2, alias=\"prod\")      # promotion == one assignment; rollback == the reverse\n",
    "print(f\"promoted: prod alias now points at a new model object: {id(loop2.registry['prod']) != old_id}\")\n",
    "assert len(alerts) >= 1, \"the drifted day should have produced at least one alert before retraining\"\n",
    "print(\"[ ok ] loop closed: served -> monitored -> alerted -> retrained -> promoted\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "46ab79ab",
   "metadata": {},
   "source": [
    "### Experiment log\n",
    "\n",
    "The expected outputs, so you can tell at a glance whether your run is healthy. Quoted numbers hold for the pinned environment under `SEED = 0`. The synthetic tables are full size at both `FAST` settings, so these numbers are identical with and without `NB_FAST`; only `STEPS_GD` in Part 4 differs (400 vs 60 steps, both converging to $w\\approx2$, $b\\approx-1$).\n",
    "\n",
    "| quantity | where | expected |\n",
    "|---|---|---|\n",
    "| validation accuracy at training time | 1.2 | ~0.71 |\n",
    "| `session_len` PSI on a drifted day | 2.5 | ~1.7-1.9 (well above 0.25) |\n",
    "| every other feature, all 30 days, max PSI | 2.5 | < 0.06 |\n",
    "| golden-set baseline accuracy | 3.1 | ~0.73 |\n",
    "| recovered linear params (correct loop) | 4.2 | $w\\approx2.00$, $b\\approx-1.00$ |\n",
    "\n",
    "> **Key takeaways.** The environment is part of the source code: an unpinned dependency is an unpinned input, and `np.trapz` -> `np.trapezoid` is the repo's own proof. Reproduce outages instead of describing them. Pin for the PR gate, canary against unpinned-latest, and let a deprecation warning page you before a removal does. The loop closes with a retrain and a one-assignment promotion, the same lever as rollback.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bdd20a04",
   "metadata": {},
   "source": [
    "## Safety lens\n",
    "\n",
    "Three failure modes that are MLOps-shaped, not generic-ML-shaped, and one runnable demo of the sharpest one.\n",
    "\n",
    "**Silent model swaps.** A vendor updates the model behind a name like `gpt-4o` without telling you. Your code does not change; your eval suite has not run in a month; refusal behavior, reasoning style, and latency all shift. Users notice, you do not. The fix is procedural: schedule the eval suite weekly even when nothing on your side changed, and pin to a dated checkpoint (`gpt-4o-2024-08-06`) where the vendor supports it.\n",
    "\n",
    "**Eval contamination.** Your golden set leaks into a training corpus, and your eval scores become memorization scores. The fix: hash the golden set, search for the hashes, rotate the set, hold a private ladder set for final candidates.\n",
    "\n",
    "**Adversarial inputs are invisible to drift detectors.** This is the sharp one, and the demo below makes it concrete. A standard drift detector (the PSI you built) is tuned for *natural* distribution shift, and it bins one feature at a time, so it only ever sees the *marginals*. An adversarial input crafted to evade the model can keep every marginal in-distribution while living in the *joint*: correlations the detector never looks at. PSI stays quiet while the attack succeeds. Drift detectors find weather; they do not find attackers. The mitigation the chapter names: run the Ch 24 red-team suite as part of monitoring, and track attack-success-rate as a first-class production metric.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "05fa2dd3",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.606097Z",
     "iopub.status.busy": "2026-06-10T20:37:24.606021Z",
     "iopub.status.idle": "2026-06-10T20:37:24.611702Z",
     "shell.execute_reply": "2026-06-10T20:37:24.611358Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PSI drift alerts on the adversarial batch: 0   (max per-feature PSI 0.047)\n",
      "corr(recency, affinity)  reference -0.017   adversarial -0.999\n",
      "mean |decision margin|   reference 1.413   adversarial 0.451\n",
      "\n",
      "[ ok ] PSI is silent on an adversarial batch that a margin/correlation monitor would catch\n"
     ]
    }
   ],
   "source": [
    "# demo: an adversarial batch PSI cannot flag, because the attack lives in the JOINT\n",
    "# distribution, not the marginals. PSI bins one feature at a time, so it only sees marginals.\n",
    "# We craft rows where recency and affinity each keep a ~standard-normal MARGINAL but are\n",
    "# anti-correlated, so their contributions to the logit cancel: every row sits on the boundary.\n",
    "adv_rng = np.random.default_rng(SEED + 2024)\n",
    "recency_adv = adv_rng.normal(0.0, 1.0, N_DAY)\n",
    "affinity_adv = -(1.3 / 1.1) * recency_adv + adv_rng.normal(0.0, 0.05, N_DAY)   # cancels the logit\n",
    "adv = np.stack([recency_adv, affinity_adv,\n",
    "                adv_rng.normal(0.0, 1.0, N_DAY),     # the three non-signal features: ordinary draws\n",
    "                adv_rng.normal(0.0, 1.0, N_DAY),\n",
    "                adv_rng.normal(0.0, 1.0, N_DAY)], axis=1)\n",
    "\n",
    "adv_alerts = detect_drift(X_ref, adv, FEATURES, threshold=0.25)\n",
    "max_psi = max(psi(X_ref[:, j], adv[:, j]) for j in range(len(FEATURES)))\n",
    "print(f\"PSI drift alerts on the adversarial batch: {len(adv_alerts)}   (max per-feature PSI {max_psi:.3f})\")\n",
    "print(f\"corr(recency, affinity)  reference {np.corrcoef(X_ref[:,0], X_ref[:,1])[0,1]:+.3f}   adversarial {np.corrcoef(recency_adv, affinity_adv)[0,1]:+.3f}\")\n",
    "print(f\"mean |decision margin|   reference {np.abs(model.decision_function(X_ref)).mean():.3f}   adversarial {np.abs(model.decision_function(adv)).mean():.3f}\")\n",
    "assert len(adv_alerts) == 0, \"by construction the per-feature PSI should NOT flag this attack\"\n",
    "print(\"\\n[ ok ] PSI is silent on an adversarial batch that a margin/correlation monitor would catch\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cac637bf",
   "metadata": {},
   "source": [
    "> **Interpretation.** The adversarial batch raised zero PSI alerts and its largest per-feature PSI is ~0.05: every *marginal* looks in-distribution. Yet the recency/affinity correlation flipped from ~0 in the reference to ~-1 here, and the decision margin collapsed from ~1.4 to ~0.45, so every row sits against the boundary where the model is least reliable. PSI bins one feature at a time, so it is structurally blind to an attack that lives in the *joint* distribution. The attack-aware monitor watches a different signal (here, margin collapse or a feature-correlation check; in an LLM system, the Ch 24 attack-success-rate). \"We red-teamed once\" is itself a failure mode; the red-team belongs in the monitoring stack, not in a one-time audit.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aef9281d",
   "metadata": {},
   "source": [
    "## Test yourself\n",
    "\n",
    "Three parts: concept self-checks with folded answers, auto-checked problems you implement, and a capstone with a rubric and a folded reference. Every answer is in this notebook; if unsure, re-run that section. Solutions are folded; try before you peek.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b3d839ca",
   "metadata": {},
   "source": [
    "### Part A — Concepts\n",
    "\n",
    "1. Why is \"the model has 87% accuracy on the test set\" a less interesting number once the system is in production? <details><summary>Answer</summary>The test set is a frozen photograph of the past distribution. Production traffic drifts; the only operationally interesting accuracy is on a recent production slice, which you only learn once labels arrive. Parts 2 and 3 are the machinery for measuring the production number.</details>\n",
    "2. PSI of a distribution against itself: what value, and why not exactly zero in our `psi` function? <details><summary>Answer</summary>~0. It is not *exactly* 0 only because of the `+1e-9` smoothing floors we add to avoid `log(0)`; the two-bin form in Exercise 25.2, which has no smoothing, is exactly 0. You saw `PSI(x, x) = 1e-9`-scale in 2.1.</details>\n",
    "3. In one sentence, the relationship between PSI and KL divergence. <details><summary>Answer</summary>PSI is the symmetrized KL: $\\text{PSI} = D_\\text{KL}(P\\|Q) + D_\\text{KL}(Q\\|P)$, which 2.3 asserts on a concrete pair. KL is asymmetric; PSI is not.</details>\n",
    "4. Look back at the 30-day PSI plot in 2.5. Which feature crossed the 0.25 line, on which day, and what does the rest of the plot staying flat tell you? <details><summary>Answer</summary>`session_len` crossed on day 14 and stayed up; every other feature hugged the floor all month. The flat lines confirm the detector is specific, it did not raise false alarms on the four undrifted features, so the one alarm is trustworthy.</details>\n",
    "5. The headline regression gate in 3.2 passed a model that hurt the low-affinity slice. What is the fix, and what is it NOT? <details><summary>Answer</summary>The fix is a *better gate*: evaluate per slice, not just the average (Exercise 25.5). It is NOT a different model and NOT a lower threshold. An average can rise while a sub-population collapses; only a stratified eval sees it.</details>\n",
    "6. In nanoGPT's loop, `optimizer.zero_grad(set_to_none=True)` comes after `optimizer.step()`. Why must it be there at all, and what does `set_to_none` buy you? <details><summary>Answer</summary>`backward()` *adds* into `.grad`, so without zeroing, gradients accumulate across steps and corrupt the update (Part 4.3 reproduces this). `set_to_none=True` replaces `.grad` with `None` rather than a zero tensor: marginally faster, and it turns a stale-gradient read into a loud `None` error instead of a silent wrong answer.</details>\n",
    "7. `if iter_num == 0 and eval_only: break` versus `if i >= 1000: break  # AFTER_DEBUG`: both are early breaks. Why is one fine and one a landmine? <details><summary>Answer</summary>The first is gated by an explicit, documented config flag (`eval_only`), so it only fires when you ask for it. The second is an unflagged debugging residue that silently caps every future run at 1000 steps. The difference is the flag; the spec names the unflagged one as the `AFTER_DEBUG` landmine.</details>\n",
    "8. What NumPy call detonated the repo's CI, what did 2.0 do to it, and what is the one-line fix? <details><summary>Answer</summary>`np.trapz`, deprecated in NumPy 2.0 (a `DeprecationWarning`, reproduced in Part 5.2) and later removed, which is when it raised `AttributeError` on 8/8 CI legs. The one-line fix is `np.trapezoid` (Part 5.3); the durable fix is a pinned constraints file plus an unpinned-latest canary that promotes deprecation warnings to failures.</details>\n",
    "9. Your KS test shows $p < 0.001$ on a 5-million-row production sample. Should you page on-call? <details><summary>Answer</summary>Not on the p-value alone. At millions of rows, any microscopic shift is \"significant\" by p-value (the 2.4 caveat). Read the effect size, the statistic $D$ or the PSI, and page on the magnitude of the shift, not its statistical detectability.</details>\n",
    "10. Why will the PSI drift detector you built never catch the adversarial batch in the Safety lens? <details><summary>Answer</summary>PSI bins one feature at a time, so it only sees marginals. The adversarial batch keeps every marginal in-distribution (max per-feature PSI ~0.05, zero alerts) while hiding the attack in the joint: the recency/affinity correlation flips to ~-1 and the decision margin collapses. The detector measures natural marginal shift; the attack hides in correlation structure it never inspects. You need an attack-aware monitor (margin collapse or a correlation check here, attack-success-rate in an LLM system) running alongside.</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "469253e3",
   "metadata": {},
   "source": [
    "### Part B — Auto-checked problems\n",
    "\n",
    "Two problems. You write the body; the check asserts a property; the folded solution follows the check.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65a685bf",
   "metadata": {},
   "source": [
    "#### Exercise 25.7 — Prediction drift\n",
    "`Difficulty 2/5 · ~10 min`\n",
    "\n",
    "Feature drift is the earliest signal; *prediction* drift is the next. Fill in `prediction_drift(model, reference_X, production_X, n_bins)` that returns the PSI between the model's predicted scores (`predict_proba[:, 1]`) on the reference set and on the production set. The check confirms it is ~0 when production equals reference, and clearly positive when production is the drifted day (whose shifted feature moves the score distribution).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "0e1f47b3",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.612764Z",
     "iopub.status.busy": "2026-06-10T20:37:24.612670Z",
     "iopub.status.idle": "2026-06-10T20:37:24.616533Z",
     "shell.execute_reply": "2026-06-10T20:37:24.616173Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 25.7 prediction drift ~0 on self: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 36,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def prediction_drift(model, reference_X, production_X, n_bins=10):\n",
    "    \"\"\"PSI between the model's predicted scores on reference vs production inputs.\"\"\"\n",
    "    # TODO 1: get reference scores = model.predict_proba(reference_X)[:, 1]\n",
    "    ref_scores = None\n",
    "    # TODO 2: get production scores the same way on production_X\n",
    "    prod_scores = None\n",
    "    attempted(ref_scores, prod_scores)\n",
    "    # TODO 3: return the PSI between the two score arrays (reuse psi)\n",
    "    raise NotImplementedError\n",
    "\n",
    "def _pred_drift_zero():\n",
    "    val = prediction_drift(model, X_ref, X_ref)\n",
    "    assert val < 0.01, f\"prediction drift of a set against itself should be ~0, got {val:.3f}\"\n",
    "\n",
    "def _pred_drift_positive():\n",
    "    Xd, _ = make_day(np.random.default_rng(SEED + 5), N_DAY, drift=1.5)\n",
    "    # session_len carries no signal, so this is small but should be > the identity case;\n",
    "    # we assert it is strictly positive and larger than the self-comparison.\n",
    "    val = prediction_drift(model, X_ref, Xd)\n",
    "    assert val > _self_pred, f\"a drifted day should show more prediction drift than self ({val:.4f})\"\n",
    "\n",
    "_self_pred = None   # baseline self-comparison; the solution cell computes it before the positive check\n",
    "check(\"25.7 prediction drift ~0 on self\", _pred_drift_zero)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e71b388a",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>Two calls to `model.predict_proba(...)[:, 1]`, one for each input set, then one call to your `psi` on the two score arrays. The scores are 1-D, exactly what `psi` expects.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "ref_scores = model.predict_proba(reference_X)[:, 1]\n",
    "prod_scores = model.predict_proba(production_X)[:, 1]\n",
    "return psi(ref_scores, prod_scores, n_bins)\n",
    "```\n",
    "</details>\n",
    "\n",
    "<details><summary>Help — \"prediction drift of a set against itself should be ~0\"</summary>If self-comparison is not near zero, you are likely binning two *different* arrays. `predict_proba` is deterministic, so the same input gives the same scores; `psi(s, s)` must be floor-noise. Check you passed the same array to both calls in the test, and that you used column `[:, 1]` (the positive-class probability), not `[:, 0]`.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "3a63c4cf",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.617664Z",
     "iopub.status.busy": "2026-06-10T20:37:24.617565Z",
     "iopub.status.idle": "2026-06-10T20:37:24.622887Z",
     "shell.execute_reply": "2026-06-10T20:37:24.622561Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 25.7 prediction drift ~0 on self\n",
      "[ ok ] 25.7 prediction drift positive on drift\n",
      "prediction drift: self=0.0000, drifted day=0.0087\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines prediction_drift; the checks below re-verify the reference.\n",
    "def prediction_drift(model, reference_X, production_X, n_bins=10):\n",
    "    ref_scores = model.predict_proba(reference_X)[:, 1]\n",
    "    prod_scores = model.predict_proba(production_X)[:, 1]\n",
    "    return psi(ref_scores, prod_scores, n_bins)\n",
    "\n",
    "_self_pred = prediction_drift(model, X_ref, X_ref)\n",
    "check(\"25.7 prediction drift ~0 on self\", _pred_drift_zero, required=True)\n",
    "check(\"25.7 prediction drift positive on drift\", _pred_drift_positive, required=True)\n",
    "_Xd, _ = make_day(np.random.default_rng(SEED + 5), N_DAY, drift=1.5)\n",
    "print(f\"prediction drift: self={_self_pred:.4f}, drifted day={prediction_drift(model, X_ref, _Xd):.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7b1d8d94",
   "metadata": {},
   "source": [
    "#### Exercise 25.8 — A retraining-trigger policy\n",
    "`Difficulty 2/5 · ~10 min`\n",
    "\n",
    "A retrain trigger fires on one of three conditions (the chapter's list): time-based, drift-based, or performance-based. Fill in `should_retrain(days_since_train, max_psi, current_acc, baseline_acc)` returning `(trigger, reason)`. It triggers if **any** of: more than 30 days since last train (`\"time\"`), any feature PSI above 0.25 (`\"drift\"`), or accuracy fell below 95% of baseline (`\"performance\"`). Return the first matching reason in that priority order, or `(False, \"none\")`. The checks cover each branch and the no-trigger case.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "db2c5173",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.623867Z",
     "iopub.status.busy": "2026-06-10T20:37:24.623786Z",
     "iopub.status.idle": "2026-06-10T20:37:24.627909Z",
     "shell.execute_reply": "2026-06-10T20:37:24.627611Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 25.8 retrain branches: not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] 25.8 retrain priority: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 38,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def should_retrain(days_since_train, max_psi, current_acc, baseline_acc):\n",
    "    \"\"\"Return (trigger: bool, reason: str). Priority: time, then drift, then performance.\n",
    "    Triggers if days_since_train > 30, OR max_psi > 0.25, OR current_acc < 0.95*baseline_acc.\"\"\"\n",
    "    # TODO 1: time-based — if more than 30 days, return (True, \"time\")\n",
    "    # TODO 2: drift-based — if max_psi > 0.25, return (True, \"drift\")\n",
    "    # TODO 3: performance-based — if current_acc < 0.95 * baseline_acc, return (True, \"performance\")\n",
    "    # TODO 4: otherwise return (False, \"none\")\n",
    "    raise NotImplementedError\n",
    "\n",
    "def _retrain_branches():\n",
    "    assert should_retrain(45, 0.0, 0.9, 0.9) == (True, \"time\"), \"stale model should trigger time\"\n",
    "    assert should_retrain(1, 0.4, 0.9, 0.9) == (True, \"drift\"), \"high PSI should trigger drift\"\n",
    "    assert should_retrain(1, 0.0, 0.80, 0.90) == (True, \"performance\"), \"acc drop should trigger performance\"\n",
    "    assert should_retrain(1, 0.0, 0.90, 0.90) == (False, \"none\"), \"healthy model should not trigger\"\n",
    "\n",
    "def _retrain_priority():\n",
    "    # time wins over drift when both hold (priority order)\n",
    "    assert should_retrain(45, 0.4, 0.9, 0.9)[1] == \"time\", \"time has priority over drift\"\n",
    "\n",
    "check(\"25.8 retrain branches\", _retrain_branches)\n",
    "check(\"25.8 retrain priority\", _retrain_priority)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "881fc820",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>Three `if` checks in priority order, each returning immediately, then a final fallthrough `return (False, \"none\")`. Because each returns, the first condition that holds wins, which gives you the priority for free.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "if days_since_train > 30:\n",
    "    return (True, \"time\")\n",
    "if max_psi > 0.25:\n",
    "    return (True, \"drift\")\n",
    "if current_acc < 0.95 * baseline_acc:\n",
    "    return (True, \"performance\")\n",
    "return (False, \"none\")\n",
    "```\n",
    "</details>\n",
    "\n",
    "<details><summary>Help — \"time has priority over drift\" failed</summary>You probably collected all matching reasons instead of returning on the first. The priority comes from *early return*: check time first and `return` immediately, so drift is never reached when time already holds. Do not build a list and pick from it.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "ee522ec0",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T20:37:24.628916Z",
     "iopub.status.busy": "2026-06-10T20:37:24.628832Z",
     "iopub.status.idle": "2026-06-10T20:37:24.631276Z",
     "shell.execute_reply": "2026-06-10T20:37:24.630949Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 25.8 retrain branches\n",
      "[ ok ] 25.8 retrain priority\n",
      "trigger on the drifted day: (True, 'drift')\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines should_retrain; the checks below re-verify the reference.\n",
    "def should_retrain(days_since_train, max_psi, current_acc, baseline_acc):\n",
    "    if days_since_train > 30:\n",
    "        return (True, \"time\")\n",
    "    if max_psi > 0.25:\n",
    "        return (True, \"drift\")\n",
    "    if current_acc < 0.95 * baseline_acc:\n",
    "        return (True, \"performance\")\n",
    "    return (False, \"none\")\n",
    "\n",
    "check(\"25.8 retrain branches\", _retrain_branches, required=True)\n",
    "check(\"25.8 retrain priority\", _retrain_priority, required=True)\n",
    "print(\"trigger on the drifted day:\", should_retrain(7, 0.45, 0.71, 0.71))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb68b477",
   "metadata": {},
   "source": [
    "### Part C — Capstone: a seven-day production run with a planted incident\n",
    "\n",
    "Put the whole loop together into one driver and run a seven-day simulated production deploy with a planted incident, the chapter's capstone in miniature, fully reproducible and self-checked.\n",
    "\n",
    "**Deliverables**\n",
    "1. A loop driver that, for each of 7 days, draws a production day (clean for days 0-3, drifted on `session_len` from day 4), serves it through an `MLLoop`, runs `detect_drift`, and records any alerts with the day index.\n",
    "2. A retraining decision on each day via `should_retrain`, using the day's max PSI.\n",
    "3. An `alert_log`: a list of `{\"day\": d, \"feature\": f, \"psi\": v, \"severity\": s}` for every alert fired, and a printed timeline.\n",
    "\n",
    "**Self-assessment (pass / partial / fail)**\n",
    "- (a) The driver runs all 7 days and produces an `alert_log`.\n",
    "- (b) No alerts fire on days 0-3 (the clean days); alerts fire from day 4 on (the planted incident).\n",
    "- (c) Every alert names `session_len`, the feature you drifted, and no other.\n",
    "- (d) `should_retrain` returns `(True, \"drift\")` on the drifted days and `(False, \"none\")` on the clean ones.\n",
    "- (e) The whole driver is reproducible: re-running it gives an identical `alert_log`.\n",
    "\n",
    "Try it yourself first. The folded reference is one way to do it, not the only way.\n",
    "\n",
    "<details><summary>My solution (reference, runs in well under a second)</summary>\n",
    "\n",
    "```python\n",
    "def run_production(n_days=7, drift_starts=4, drift_size=1.5):\n",
    "    cap_rng = np.random.default_rng(SEED + 7777)\n",
    "    loop = MLLoop(X_ref); loop.register(model, alias=\"prod\")\n",
    "    alert_log, timeline = [], []\n",
    "    for d in range(n_days):\n",
    "        shift = drift_size if d >= drift_starts else 0.0\n",
    "        Xd, _ = make_day(cap_rng, N_DAY, drift=shift)\n",
    "        loop.serve_log = []                       # one day's traffic at a time\n",
    "        loop.serve(Xd)\n",
    "        served = loop.served_features()\n",
    "        alerts = detect_drift(X_ref, served, FEATURES, threshold=0.25)\n",
    "        max_psi = max((psi(X_ref[:, j], served[:, j]) for j in range(len(FEATURES))), default=0.0)\n",
    "        trigger, reason = should_retrain(days_since_train=d, max_psi=max_psi,\n",
    "                                         current_acc=0.71, baseline_acc=0.71)\n",
    "        for a in alerts:\n",
    "            alert_log.append({\"day\": d, **a})\n",
    "        timeline.append((d, len(alerts), reason))\n",
    "        print(f\"day {d}: {len(alerts)} alert(s)  retrain={reason}\")\n",
    "    return alert_log, timeline\n",
    "\n",
    "alert_log, timeline = run_production()\n",
    "# self-checks\n",
    "clean_alerts = [a for a in alert_log if a[\"day\"] < 4]\n",
    "assert clean_alerts == [], f\"no alerts should fire on clean days, got {clean_alerts}\"\n",
    "assert all(a[\"feature\"] == \"session_len\" for a in alert_log), \"every alert must name session_len\"\n",
    "assert any(a[\"day\"] >= 4 for a in alert_log), \"the planted incident must fire from day 4\"\n",
    "print(f\"\\nalert_log has {len(alert_log)} entries, all session_len, all on days >= 4\")\n",
    "```\n",
    "\n",
    "The reference catches the lesson: the monitor is silent until the planted incident, then fires on exactly the drifted feature, and the retrain trigger flips to `\"drift\"` on the same days. That is a working `data -> serve -> monitor -> retrain` loop with a ground truth you can check.</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "db017409",
   "metadata": {},
   "source": [
    "## Reflection\n",
    "\n",
    "Write ~150 words, in the cell below, on the dumbest bug you hit in this notebook and how you found it. Maybe you swapped `p` and `q` in PSI and got a negative number. Maybe you forgot to remove `raise NotImplementedError` and the loop aborted on the first feature. Maybe you read the np.trapz post-mortem and realized an unpinned dependency in your own work is a live version of the same outage. Nobody grades this. Writing it is the point: the act of naming a bug and the signal that found it is what turns a one-time fix into a debugging instinct you keep. The MLOps version of this reflection is the blameless post-mortem, and the discipline is identical, name what happened, name why the monitoring did not catch it sooner, name the test that would have.\n",
    "\n",
    "*(Replace this text with your own.)*\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d99e08e",
   "metadata": {},
   "source": [
    "## Going further\n",
    "\n",
    "- *Made With ML* MLOps course (`madewithml.com`), seven lessons end to end. The canonical free MLOps curriculum; the structure of this notebook mirrors its lifecycle.\n",
    "- Chip Huyen, \"Data distribution shifts and monitoring\" (2022). The drift taxonomy, covariate / label / concept, in one essay. Pair with her \"ML systems design\" lecture notes.\n",
    "- Eugene Yan, \"Evals, the most important thing in LLM apps\". The eval-driven-development manifesto behind Part 3.\n",
    "- Eugene Yan, \"Feature stores: a hierarchy of needs\". Stops you from building a Feast before you need one.\n",
    "- The NumPy 2.0 migration guide, the `np.trapz` -> `np.trapezoid` entry specifically. The case study in Part 5 is one row of that table; the whole table is worth a read before your next major-version bump.\n",
    "- Karpathy, nanoGPT `train.py`. The loop you annotated in Part 4, in full, including the DDP and mixed-precision plumbing this notebook stripped.\n",
    "\n",
    "## What this enables\n",
    "\n",
    "- **Ch 26 — Reading Papers**: the regression gate becomes a CI job that turns a paper's claim into a test. The paper-claim -> reproduce-in-code loop is eval-driven development pointed at the literature.\n",
    "- **The job**: most production ML roles are largely MLOps. You can now point at every edge of the `data -> train -> serve -> monitor -> retrain` loop and say what instruments it, what breaks it, and how you would know.\n",
    "- **The gap this leaves**: everything here ran on one machine with synthetic data. The forward step is real telemetry at scale, a feature store enforcing one definition across training and serving, and a registry behind RBAC. The shapes are identical; the operational weight is not. That weight is the difference between this notebook and a platform, and it is mostly data work, exactly as the chapter's founder-blog sources insist.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5cc8e40e",
   "metadata": {},
   "source": [
    "---\n",
    "*Built top-to-bottom. If every check above printed `[ ok ]`, you've reproduced the chapter. Total running time and verification stamp written by CI.*\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "obvix-nb",
   "language": "python",
   "name": "obvix-nb"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  },
  "obvix": {
   "title": "Ch 25 — MLOps and Observability"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
