Machine Learning · Insper

MLOps

Fundamental approach

What exactly do we ship, how does a request reach it safely, and how do we notice that the world has changed?

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 first four rows are this part; the last one opens the advanced approach.

The path of this part

  1. Package the modelthe whole pipeline, pickle risks, pinned versions
  2. Serve predictionsbatch or online, a schema in front, an endpoint
  3. Reproduce and versionseeds, data fingerprints
  4. Monitor driftthree shifts, PSI and KS by hand, late labels
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?
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
area under the ROC (receiver operating characteristic) curve

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.
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 (an API, application programming interface)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

HTTP: HyperText Transfer Protocol, the protocol of the web.


# 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.
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 (Data Version Control), 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).

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

\(x\): the inputs of a request; \(y\): its label, the outcome the model predicts.

PSI, by hand: one bin at a time

PSI (population stability index): bins cut at the training quartiles, \(e_i\) = expected share, \(a_i\) = this week's share. Predict. Which bin contributes most, and does the sum pass 0.25?

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 (sd). With no shift at all, PSI is not exactly zero: two samples never have identical histograms.

Usual reading (Siddiqi, 2006): below 0.1 no meaningful change · 0.1 to 0.25 worth watching · above 0.25 significant.

Simulator: move the production distribution

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

The KS statistic, by hand

KS (Kolmogorov–Smirnov): \(D = \max_x |F_{\text{ref}}(x) - F_{\text{cur}}(x)|\), \(F\) = share of the sample at or below \(x\). Reference 1–5, current 3–7. Predict. At which \(x\) is the gap largest?

Both statistics on 5,000 rows

Left: shares of reference and current data in the 10 decile bins of the reference, the current data piling into the top bins, PSI 0.25; right: the two cumulative distributions with the largest gap D = 0.21 marked
The mean moved by half a standard deviation: PSI 0.25 (left, bin by bin) and \(D = 0.21\) (right, the largest vertical gap). Both are magnitudes: how far the distribution moved.

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.

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: reject the impossible, warn on the new, answer with the version.
  3. Version code, data (a fingerprint), environment, seeds and configuration.
  4. Measure shifts with magnitudes: PSI bin by bin, the KS statistic as the largest gap.
  5. Monitor inputs and scores at once, outcomes later; only labels reveal concept drift.
symptomwhat to do
a broken request gets a normal-looking probabilitya schema: reject or warn
an input's PSI is above 0.25look upstream before retraining
inputs stable, accuracy falling once labels arriveconcept drift: outcome metrics, audited labels
Fundamental lab · about 45 min, in pairs

Shipping and monitoring a bank-marketing model, steps 1 to 8

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 without labels (7), then with them (8). At every step, answer the Predict before running.

Simulations: the leak, in production (step 2) and where the PSI of contact comes from (step 7).

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

Pace: steps 1 and 2 and the first simulation in 12 min; steps 3 to 6 in 15 min; step 7 and the second in 13 min; step 8 in 5 min.

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