Machine Learning · Insper

MLOps

A model in a notebook creates no value

Packaging, serving, versioning, monitoring and retraining, each one measured

This lesson starts where the others ended

  • Every lesson so far ended when the test score was printed.
  • Value comes from predictions delivered reliably to the people who act on them.
  • MLOps (machine-learning operations): the engineering habits that take a fitted pipeline into production and keep it honest there.
The lab follows a bank's telemarketing model trained on calls from 2008–2009 into 2009–2010, when the share of clients who said yes went from about 7% to more than 50%. Real drift, not simulated.

The model is the small box

Sculley et al. (2015), Hidden Technical Debt in Machine Learning Systems: the code that learns is a small box surrounded by infrastructure, and most failures happen in the infrastructure.

212
lines of code in this lesson's lab
4 + 5
call fit + define the model

The rest load, check, package, serve, monitor and compare.

The loop, and what the monitor sends back

Click the buttons: what does a drift alert send back, and what does a data bug?

Every arrow can break without an error

stagefails silently when...
packagethe file loads in another library version and predicts differently
servea malformed request still gets a confident-looking probability
train againnobody can say which data, code and seed produced the model in production
monitorthe world changes and the model keeps answering as if it had not
serve vs trainthe serving code computes a feature differently from the training code

The path of this lesson

  1. Package the modelthe whole pipeline, pickle risks, pinned versions, skops
  2. Serve predictionsbatch or online, schema checks, an endpoint, latency
  3. Reproduce and versionseeds, data fingerprints, an experiment log
  4. Monitor driftmeasuring a shift, late labels, retraining
  5. Ship changes safelytraining–serving skew, feedback loops, safe rollout, maturity
Each stage answers one question: what exactly do we ship? → how does a request reach the model safely? → could we rebuild the model that is running? → how do we notice that the world changed? → what else breaks, and how do we replace a model safely?
Stage 1 · what exactly do we ship?

Packaging: the artifact is the whole pipeline

German credit (1,000 past loans): 3 numeric and 5 categorical columns → scaler + one-hot → logistic regression.


joblib.dump(pipe, path)                  # the WHOLE pipeline, one file
loaded = joblib.load(path)
np.array_equal(proba, loaded.predict_proba(X_test)[:, 1])
  

test ROC AUC

0.783

on disk

4,721
bytes

round trip

True
identical bit for bit

Why not ship the model alone?

Predict. Take the fitted LogisticRegression out of the pipeline and give it the raw table. What happens?

ValueError: X has 8 features, but LogisticRegression is expecting 30 features as input.
  
  • The model saw the 30 columns of the column transformer: 3 standardized numbers + 27 one-hot indicators.
  • Serving it alone means rebuilding those 30 columns by hand: same means, same category order, same unknowns. Any difference gives wrong predictions without an error.
Ship the pipeline: the preprocessing becomes part of the artifact.

A pickle is a program


class Innocent:
    def __reduce__(self):                 # tells pickle how to rebuild this object...
        return (print, ('this line ran while the file was being LOADED',))

obj = pickle.loads(pickle.dumps(Innocent()))   # ...and loading obeys
  

this line ran while the file was being LOADED
  
Replace print with os.system and loading runs a shell command. Load model files only from sources you would trust to run code on your machine.

Pin the versions

scikit-learn stamps its version in every saved estimator. Fake a file from another version by changing one digit of the stamp:


InconsistentVersionWarning for: ['StandardScaler', 'OneHotEncoder',
  'ColumnTransformer', 'LogisticRegression', 'Pipeline']
  

Every estimator complains, and the file still loads. It may predict differently.

  • store the versions (Python, scikit-learn, NumPy) next to the model, in a metadata file;
  • pin them in the serving environment (requirements.txt, a container image);
  • when upgrading the library, retrain and re-validate.

A safer format: skops

On loading, skops rebuilds only types from an allow-list; anything else must be trusted explicitly.

pipelineuntrusted typesloading without trust
scaler + one-hot + logistic[]loads, identical probabilities
with a custom function['__main__.cap_amount']UntrustedTypesFoundException

Outside Python, ONNX (skl2onnx) turns a fitted pipeline into a portable graph for C++, Java or JavaScript runtimes.

Stage 2 · how does a request reach the model safely?

Serving: how the model is called

patternlatencyexample
batchhoursnightly churn scores written to a database
online (API)millisecondscredit decision during checkout
streamingsecondsfraud flags on a transaction stream
edgeon the devicekeyboard prediction on a phone
Prefer batch whenever the decision can wait: cheaper, easier to monitor, easier to rerun.

The pipeline accepts garbage

Predict. One good request and four broken ones. How many of the broken ones raise an error?
requestanswer
good requestP(bad) = 0.626
age = −3P(bad) = 0.784
amount in thousandsP(bad) = 0.574
unseen purpose 'crypto'P(bad) = 0.519
housing missingValueError: columns are missing

Only one. The other three get a probability that looks exactly as trustworthy as the good one.

A schema, and a function that checks it

Write down what the model saw in training (a range per number, a list per category) and check every request first:

requeststatusmessage
good requestok0.626
age = −3rejectedage: −3 outside training range [19, 75]
amount in thousandsrejectedcredit_amount: 4 outside training range [250, 15945]
unseen purposerejectedpurpose: unknown category 'crypto'
housing missingrejectedhousing: missing
Answer with the model version, so every logged answer can be traced. Reject impossible values; warn (predict, but log) for values that are merely new.

The same function behind an HTTP endpoint


# app.py  (illustrative: run it with uvicorn, not in a notebook)
from fastapi import FastAPI
from service import predict_one           # the tested function

app = FastAPI()

@app.post('/predict')
def predict(payload: dict):
    return predict_one(payload)
  
The web layer has no decisions in it. Everything that can be wrong is in predict_one, an ordinary function you can test in a notebook.

Latency: pay per call, not per row

Predict. Scoring one row per call, or 10,000 rows in one call: how different is the cost per row?
Time per call is flat at about 1.7 ms from 1 to about 1,000 rows, then grows; the cost per row falls from 1.7 ms to 0.0006 ms

On the machine that built the page: 1.724 ms/row one at a time, 0.0006 ms/row in one batch, about 2,700×. A call costs about 1.7 ms whether it carries one row or a thousand: fixed work, not arithmetic.

Stage 3 · could we rebuild the model that is running?

Reproducibility: every ingredient has a version

ingredientwhat to recordtypical tool
codethe commit that trained itgit
dataa fingerprint and a way to get the rows backDVC, lakeFS, snapshots
environmentlibrary versionslock files, a container
randomnessevery seedrandom_state
configurationhyperparameters, features, thresholdsa config file in git
resultsmetrics, the artifacttracker, model registry

Seeds: same score, different decisions

Two random forests that differ only in their seed:

AUC

0.763 · 0.766
look like the same model

probabilities

96%
differ

decisions

18 of 250
flip
"Why was I refused yesterday and accepted today?" deserves a better answer than "we reran the script". Fix the seed; never try seeds until the test score is best.

Fingerprint the data


training data      68ad024c9661
one value changed  7eb64a1901f0
rows shuffled      a662547f0332
same data again    68ad024c9661
  
  • One changed value out of 6,000 gives a completely different fingerprint; so does another row order.
  • "Was it trained on this month's extract?" becomes a string comparison.

A hash only identifies the data; rebuilding needs the rows themselves (DVC, lakeFS, read-only snapshots).

An experiment log: one line per run, never edited

dataCCV AUCCV std
68ad024c96610.010.74950.0292
68ad024c96610.100.75990.0224
68ad024c96611.000.75220.0244
68ad024c966110.000.74420.0230
The lead of C = 0.1 over C = 1 (0.0077) is far below the fold standard deviations (about 0.02): treat them as tied. Keep the losing runs: they record how many things were tried.
Stage 4 · how do we notice that the world changed?

Monitoring: models decay silently

The service keeps answering 200 OK while its predictions get worse. Three layers: system, data quality, statistical drift.

shiftwhat changeswhat stays fixedexample
covariate\(P(x)\)\(P(y \mid x)\)the bank sells to younger clients
concept\(P(y \mid x)\)\(P(x)\)a crisis changes who repays
prior\(P(y)\)\(P(x \mid y)\)an epidemic: more patients are sick

Measuring a shift: PSI, by hand

\[ \text{PSI} = \sum_{i=1}^{B} (a_i - e_i)\,\ln\frac{a_i}{e_i} \]
bin\(e_i\)\(a_i\)\(\ln(a_i/e_i)\)term
10.250.10−0.9160.1374
20.250.20−0.2230.0112
30.250.30+0.1820.0091
40.250.40+0.4700.0705
PSI0.228

Bins cut at the training quartiles. Every term is positive.

< 0.1 no meaningful change · 0.1–0.25 worth watching · > 0.25 significant (Siddiqi, 2006)

What does PSI = 0.25 mean?

Predict. A normal feature whose mean moves. How far must it move for PSI to reach the alarm level 0.25?
mean moved by00.1 sd0.25 sd0.5 sd1 sd
PSI (10 bins)0.0050.0150.0640.2500.929

The alarm level is a move of half a standard deviation. With no shift at all, PSI is not exactly zero: two samples never have identical histograms.

Simulator: PSI, bin by bin

Predict. Turn on concept drift without moving the inputs. What happens to PSI, and to the accuracy?

Measuring a shift: the KS test

Left: shares of reference and current data in 10 decile bins, PSI 0.25; right: the two cumulative distributions with the largest gap D = 0.21 marked
\[ D = \max_x \left| F_{\text{ref}}(x) - F_{\text{cur}}(x) \right| \]

By hand, 1–5 against 3–7: \(D = 0.4\), \(p = 0.873\): not surprising with five points per side.

a shift of 0.02 sd, n =1,00010,000100,0001,000,000
KS p-value0.0970.140.000491.6e-41
PSI0.03440.00180.00040.0005
With large batches the p-value always says "different": alert on magnitudes (PSI, or \(D\)). With many features, correct for multiple tests.

The three shifts, measured

Predict. One of the three shifts is invisible to every input monitor. Which one?
PSI(x)KS paccuracyprecisionactual ratemean predicted
no change0.0040.6950.8160.7690.4620.465
covariate shift3.5050.0000.7280.7290.7200.780
concept drift0.0030.3400.6600.3290.1670.459
prior shift0.1520.0000.7940.4910.2000.339
Four panels: no change, covariate shift, concept drift and prior shift, each with the distribution of x, the true probability curve and the model's curve

Labels arrive late: monitoring at two speeds

immediately

data-quality checks, PSI or KS of each input, and the distribution of the model's own scores: one of the most effective shift detectors, for one line (Rabanser et al., 2019)

later, with the outcomes

the real metrics (AUC, precision, calibration) per batch, against the validation estimate

Churn is known after 60 days, a default after months. A refused loan never gets a repayment history: a small audited labeled sample is the only protection against concept drift.

Degrade, detect, retrain

The population moves 0.3 sd per month. Static model vs a model retrained whenever its input PSI crosses 0.25.

monthstaticmonitoredbest possible
00.8010.8010.802
2 (alert)0.7800.7800.785
60.7590.8140.830
80.6540.8220.816
120.2730.8490.850
PSI of the input grows past 4 for the static model; static accuracy falls from 0.80 to 0.27 while the retrained model tracks the best possible
The first alert fires while the static model is still fine: drift is not damage. And retraining needs labels: with a 60-day delay, the monitored model lags two months.
Stage 5 · what else breaks, and how do we replace a model safely?

Training–serving skew, measured

A new client application sends a field in different units. The test set, three ways:

AUCflaggeddecisions changedrejected by the schema
as in training0.78320.0%0.0%1%
amount in thousands0.77514.8%5.2%100%
duration in years0.7512.8%17.2%93%
The AUC hardly moves; the decisions do. The schema catches both bugs. Other sources: preprocessing reimplemented in SQL or Java, different time windows, features that know the future.

Shipping a new model safely

First an evaluation gate: beat the current model on recent labeled data neither saw, by more than the noise. Then roll out in stages:

strategyhowprotects against
shadowscores live traffic, answers only loggedcrashes, latency, strange scores
canary1–5% of trafficproblems that only show with real users
A/B testrandom split, a business metric"better AUC" that is not better outcomes
rollbackthe previous artifact stays readyeverything else, in minutes
Feedback loops: the credit model only sees repayment for loans it approved. Keep holdout traffic decided without the model.

Tests, documentation, and maturity

The ML Test Score

28 tests in 4 groups: data, model development, infrastructure, monitoring (Breck et al., 2017). The score is the minimum over the groups.

Model card

intended use, factors, metrics, training and evaluation data, ethical considerations, caveats (Mitchell et al., 2019)

A sensible ladder

  1. script + versioned artifacts
  2. CI/CD: tests before any model ships
  3. logging, dashboards, drift alerts
  4. retraining with gates and rollback

Climb when the pain justifies it.

Lab · a few seconds of computing

Shipping and monitoring a bank-marketing model

45,211 telemarketing calls, May 2008 to November 2010, in the order they were made. Deployed on 1 May 2009, followed for 19 months.

Build and validate (1–3), package and guard (4–6), monitor (7–9), retrain (10), write the model card (11).

The score PSI jumps above 0.8 from the second quarter: the cheapest and most useful single monitor.

Lab notebook (Jupyter or Colab, File → Upload notebook).

Per quarter: the actual subscription rate climbs above 50% while the static model predicts about 16% and the retrained model follows the rate

Summary

  1. Ship the whole pipeline, check the round trip, pin the versions; a pickle is a program.
  2. Put a schema in front of the model and answer with its version.
  3. Version code, data (a hash), environment, seeds and results.
  4. Monitor inputs and scores at once, outcomes later; PSI for magnitudes.
  5. Drift is not damage; concept drift needs labels.
  6. New models pass a gate and roll out in stages, with rollback ready.