Module 10 · The Professional Stack & Capstone ⏱ 16 min

Deployment and Monitoring Judgment

By the end of this lesson you will be able to:
  • Explain why a model's accuracy and latency change between the notebook and production
  • Measure the slow tail of latency with percentiles and a simple drift score by hand
  • Decide when to roll back a deployed model and how to ship a risky one safely

A model that scored 0.94 accuracy in your notebook can ship and report 0.71 in production, and nothing about the model changed. The notebook lied - not on purpose, but because it never paid the costs real deployment charges. It trained and scored on the same clean file, with no network, no serialization, and no users feeding it data the likes of which it has never seen. The moment a model leaves the notebook, four new forces act on it: every prediction now waits on latency, every prediction now costs money, the world drifts away from the data it was trained on, and any mismatch between how you trained and how you serve corrupts the inputs silently. This lesson is about judging those forces - when to act, and how to recover when something goes wrong.

flowchart LR
  N["notebook:<br/>trains and scores"] --> F["freeze preprocessing<br/>with the model"]
  F --> S["serve behind<br/>an API"]
  S --> M["monitor live inputs<br/>and predictions"]
  M -->|"drift or metric drop"| R["roll back to<br/>last good version"]
  R --> S
  style R fill:#b91c1c,color:#fff
  style M fill:#3776ab,color:#fff
The honest production loop: freeze preprocessing with the model, serve it, then watch live inputs and metrics so a drift or accuracy drop triggers a rollback before users notice.

Latency: the notebook never queued

In the notebook, scoring one row takes 5ms and that is the end of the story. In production that same row travels over a network, gets serialized, waits behind other requests, loads the model into memory if it was cold, and gets deserialized on the way back. The 5ms becomes 50 or 200. Averages hide the worst cases, so production latency is measured in percentiles: p50 is the median request, the experience most users get, and p99 is the slowest one in a hundred, the request that breaks autocomplete or triggers a timeout. A model whose p50 is fine but whose p99 is 800ms is not fine for an interactive feature. Budget for the tail, not the middle.

Percentiles by hand on a sorted latency sample. p50 hides the one slow request; p99 exposes it. Press Run.
def percentile(sorted_vals, p):
    # nearest-rank percentile on an already-sorted list
    k = max(0, int(round(p / 100 * len(sorted_vals))) - 1)
    return sorted_vals[k]

latencies = sorted([5, 8, 6, 7, 9, 100, 8, 7, 6, 9])
print("p50", percentile(latencies, 50))
print("p99", percentile(latencies, 99))

Read the tail, not the average

That p99 of 100 tells you something the median of 7 hides entirely: one request in a hundred hits a value more than ten times the typical one. In a batch job that is noise; in a user-facing API it is the difference between a snappy page and a timeout. Production dashboards watch p99, and sometimes p99.9, precisely because the average forgives the slow tail. When a latency budget slips, the fix is rarely the model itself - it is the cold-start that loads it, the serialization that round-trips it, or a queue building up behind a busy instance. Measure the tail first, then decide whether the model or the plumbing is to blame.

Drift: the world moves on

A model is frozen at training time, but the world it scores keeps changing. Drift is the gap between the data you trained on and the data arriving now, and it comes in two flavors. Covariate drift means the inputs shifted - a feature that used to average 12 now averages 18, because your users, your sensors, or your market changed. The model's internal mapping may still be correct, but it is being queried at inputs it rarely saw. Concept drift is nastier: the relationship itself flipped. The same input that used to signal 'will repay' now signals 'will default', because the economy or the fraud pattern changed. Both decay accuracy, but they demand different fixes - one is a retrain, the other is a rethink.

flowchart TD
  D["what drifted?"] -->|"inputs shifted"| CV["covariate drift:<br/>feature distribution moved"]
  D -->|"relationship flipped"| CC["concept drift:<br/>input-to-label map changed"]
  CV --> R1["retrain on<br/>newer data"]
  CC --> R2["mapping broke:<br/>redesign features"]
  style CV fill:#b45309,color:#fff
  style CC fill:#b91c1c,color:#fff
Two kinds of drift, two fixes. Covariate drift moves the inputs and is fixed by retraining on newer data; concept drift breaks the input-to-label mapping and forces a redesign.
A drift score by hand: how far has the serve distribution's center moved, measured in training standard deviations?
def mean(xs):
    return sum(xs) / len(xs)

def drift_score(train, serve):
    t = mean(train)
    spread = (mean([(x - t) ** 2 for x in train])) ** 0.5
    return abs(t - mean(serve)) / spread

train = [1, 2, 3, 4, 5]
serve = [4, 5, 6, 7, 8]
print(round(drift_score(train, serve), 2))

How much drift is too much?

The drift score is a distance, not a verdict. A value of 0.2 means the serve distribution's center has moved two tenths of a training standard deviation away - small enough to be normal noise. A score above 1 means the inputs have moved further than the training data typically spreads, which is a strong signal the model is now answering outside its comfort zone. Teams set a threshold and alert when the rolling drift score crosses it, then investigate before accuracy visibly drops. The threshold is a judgment call, calibrated on how much accuracy you can afford to lose before acting. The discipline is measuring drift continuously, because it never announces itself - it shows up as a slow, quiet decay in the metrics you forgot to watch.

Exercise

What does this print? The training mean is 3 and its standard deviation is sqrt(2); the serve mean is 6.

def mean(xs):
    return sum(xs) / len(xs)

def drift_score(train, serve):
    t = mean(train)
    spread = (mean([(x - t) ** 2 for x in train])) ** 0.5
    return abs(t - mean(serve)) / spread

print(round(drift_score([1, 2, 3, 4, 5], [4, 5, 6, 7, 8]), 2))
Exercise

Write should_rollback(history, drop_threshold) returning True when the model should roll back. history is a list of daily accuracy values in chronological order (oldest first). Roll back when the latest value has dropped by more than drop_threshold below the first (baseline) value.

def should_rollback(history, drop_threshold):
    # roll back if the latest value dropped more than drop_threshold below history[0]
    pass
Exercise

A model was trained on STANDARDIZED features - each value x became (x - mean) / scale. But the serve path sends raw values to the model: standardize returns its input unchanged, so the model sees features on the wrong scale. Fix standardize to apply the training transform.

def standardize(value, mean, scale):
    return value   # BUG: sends the raw value; model expects (value - mean) / scale
Exercise

Your credit model's accuracy has fallen for a month. The applicant pool's average debt-to-income ratio climbed from 0.30 to 0.42 over the same period - the inputs shifted, but people with a given ratio still default at the same rate as before. Which drift is this?

Exercise

You are deploying a new ranking model that you are not yet sure beats the old one. Which strategy ships it safely while you find out?

Recap

  • A model's accuracy and latency change in production because the notebook never paid for network, serialization, cold-starts, or unseen data.
  • Measure latency with percentiles - watch p99, the slow tail, not the average that forgives it.
  • Covariate drift moves the inputs (retrain); concept drift breaks the input-to-label mapping (redesign). A drift score turns the shift into a number you can alert on.
  • Training-serving skew - feeding the model differently-preprocessed features at serve time - is the most common cause of a silent collapse; freeze preprocessing with the model.
  • Ship risky models with a canary or shadow, and roll back to the last good version the moment a monitored metric drops past its threshold.

The capstone ties it together: one dataset, taken honestly from split to a leakage-free scikit-learn pipeline, with metrics that would survive production.

Checkpoint quiz

A model scores in 5ms in your notebook but takes 180ms per request in production. What most likely explains the gap?

Your daily accuracy monitor reads 0.91, 0.90, 0.89, 0.72. What is the best first action?

Go deeper — technical resources