ml-models / Inference and deployment / Monitoring and drift core

Monitoring and drift

A deployed model is tested once and then meets data that keeps changing. This page separates the three ways the world drifts away from the training set (covariate, label and concept drift), shows what can be watched before the labels arrive and what only the labels reveal, works the population stability index and the Kolmogorov–Smirnov test by hand, and turns the numbers into retraining triggers, all on one simulated model over five weeks.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. Three kinds of shift
  5. What to watch
  6. The population stability index
  7. The two-sample Kolmogorov–Smirnov test
  8. Delayed and partial labels
  9. Correcting label shift
  10. Retraining triggers
  11. Worked example
  12. PSI on a shift of 0.3
  13. KS on the same shift, and on a tiny one with ten times the data
  14. Five weeks, one decision rule
  15. Recalibrating instead of retraining
  16. Prior correction and maturing labels
  17. Implementation
  18. Tradeoffs
  19. Connections
  20. Read first
  21. Read next
  22. References

Problem

Every guarantee from held-out evaluation assumes the test set comes from the same distribution as the data the model will see. After deployment that assumption decays. Customers change, a competitor launches, a sensor is recalibrated, an upstream team renames a field, a pandemic moves everyone online. The model keeps returning numbers with the same confidence, and nothing in the serving path fails: a server answering in 20 ms with stale probabilities looks perfectly healthy.

The labels that would show the problem often come late or never: a loan defaults months after approval, a fraud chargeback arrives weeks after the purchase, a recommendation that wasn't clicked says little about the one that wasn't shown. Monitoring is the practice of catching these changes from whatever signals are available, telling apart the ones that hurt the model from the ones that don't, and deciding when to recalibrate, retrain, or look for a bug.

Intuition

A classifier is an estimate of P(y | x), learned from examples drawn from a joint P(x, y). That joint can change in three ways that matter differently. If only the inputs move, P(x), into regions the model saw rarely, its answers are still right where it learned well and become guesses where it didn't. If only how common each outcome is changes, P(y), every probability is off by the same factor in the odds, and a one-number correction fixes it. If the relationship itself changes, P(y | x), the model is answering a question that no longer has the same answer, and only new labels can say how.

So there are two kinds of signals. Inputs and predictions are available immediately for every request and show that something changed. Outcomes arrive later, for some of the requests, and show whether the model is wrong. A good monitor uses the cheap signals as an early warning and the expensive ones as the verdict.

Mechanics

Three kinds of shift

Write the joint two ways: P(x, y) = P(y | x) P(x) = P(x | y) P(y). Following Moreno-Torres et al. (2012), each kind of shift keeps one factor fixed and changes the other:

  • Covariate shift: P(x) changes, P(y | x) does not. A credit model trained mostly on salaried applicants starts seeing freelancers. If the model learned P(y | x) well everywhere, it stays correct; its errors grow only where the new inputs are rare in training. Shimodaira (2000) showed training can be corrected by weighting each example by Pnew(x) / Ptrain(x).
  • Label shift (prior shift): P(y) changes, P(x | y) does not. A disease becomes more common, but a sick patient's symptoms look the same. By Bayes' rule the correct posterior moves by a fixed factor in the odds, so the model's probabilities can be corrected without retraining (Saerens et al., 2002).
  • Concept drift: P(y | x) changes. Fraudsters adapt to the rules, a feature's meaning changes, a policy change makes the same applicant riskier. The model's mapping is now wrong, and since the inputs may not move at all, nothing short of new labels reveals it.

In time, drift can be sudden (a code release), gradual (tastes changing), recurring (seasons, weekends), or a one-off outlier. Gama et al. (2014) survey the detectors and adaptation strategies for each; models that learn from every new example are covered under online learning.

What to watch

Three monitoring signals and when each arrives Requests flow left to right: inputs x enter the model, which emits a prediction p, and much later the outcome y arrives and is joined back to the logged prediction. Under each stage is what it lets you check. Inputs, available at once: missing values, ranges and categories, and per-feature drift by PSI or the KS test. Predictions, available at once: the score distribution and the mean predicted rate. Outcomes, delayed by days to months and only for some requests: calibration, Brier score, log loss, AUC, and the gap between predicted and observed rates. inputs x every request, now model → p̂ every request, now outcome y some requests, later delay data checks nulls, ranges, new categories PSI, KS per feature prediction drift score distribution mean predicted rate share above threshold performance reliability, Brier log loss, AUC predicted vs observed early warning: something changed verdict: is the model wrong?

The first line of defence is the plain data checks: the share of nulls, values outside the training range, unseen categories, a feature that is suddenly constant. Most production "drift" is a broken pipeline, not a changing world, and these checks catch it the same day (Breck et al., 2017). Then come distribution tests per feature and on the model's output. The output is the most useful single series: it is one number per request, it summarizes every feature the model uses, weighted by how much the model uses it, and its mean is the base rate the model believes in, which can be compared with the observed rate once labels mature.

The population stability index

Split a feature into bins, usually deciles of the reference (training or launch) data, so each holds 10% of it. Let ei be the reference share in bin i and ai the current share. Then

PSI = Σi (ai − ei) · ln(ai / ei)

Every term is non-negative, since ai − ei and ln(ai/ei) always have the same sign. PSI is the sum of the two KL divergences, KL(a ‖ e) + KL(e ‖ a), over the binned distributions, so it is symmetric. The usual reading, a convention from credit scoring with no single canonical source, is below 0.1 stable, 0.1 to 0.25 worth watching, above 0.25 a significant shift. A bin that is empty in either distribution makes its term infinite, so implementations clip shares to a small ε; the result then depends on ε.

The two-sample Kolmogorov–Smirnov test

For a continuous feature, compare the empirical CDFs Fref and Fcur directly, with no bins:

D = supx | Fref(x) − Fcur(x) |

Under the null hypothesis that both samples come from one distribution, √(nm/(n+m)) · D follows the Kolmogorov distribution for large samples, so the test rejects at level α when D exceeds c(α) · √((n+m)/(nm)) with c(α) = √(−ln(α/2) / 2), 1.358 at α = 0.05. The p-value is the series 2 Σk≥1 (−1)k−1 e−2k²λ², with λ = (√Ne + 0.12 + 0.11/√Ne) · D and Ne = nm/(n+m) (Stephens' correction). The catch is that the critical value shrinks like 1/√n: with enough traffic every real-world feature "fails" the test, so KS answers "is there any difference?", not "is the difference big enough to matter?". PSI is a size, KS a significance; dashboards usually alert on a size.

Delayed and partial labels

When labels arrive over weeks, the most recent cohorts always look better than they are: only the quick outcomes are in. Comparing this week's observed rate with last quarter's fully matured one finds a false improvement every time. Compare cohorts at the same age, wait for a maturity window, or model the arrival curve. Labels can also be biased by the model itself: a rejected loan never shows whether it would have defaulted, and a fraud model only gets labels for the cases it flagged for review. Those gaps are filled with a small random holdout that is served without the model's decision, which also keeps the evaluation honest for causal questions.

Correcting label shift

If P(x | y) is fixed and the positive rate moves from π to π′, Bayes' rule gives the new posterior from the old one:

p′ = p·(π′/π) / ( p·(π′/π) + (1 − p)·((1 − π′)/(1 − π)) )

In log-odds this adds the constant ln(π′/(1−π′)) − ln(π/(1−π)) to every prediction. π′ itself is unknown without labels, but can be estimated from the predictions on new data: Saerens et al. (2002) iterate EM between the corrected posteriors and their mean, and Lipton et al. (2018) solve a linear system using the model's confusion matrix from validation (black-box shift estimation), which also detects label shift with a statistical test.

Retraining triggers

  • Schedule. Retrain daily, weekly or monthly on a sliding window. Simple and predictable, wasteful when nothing changed, too slow after a sudden break.
  • Input drift. PSI above the action threshold on an important feature or on the score. Fast, but covariate shift often doesn't hurt, so this trigger should open an investigation, not ship a model.
  • Performance. A matured-label metric (Brier, log loss, AUC, calibration gap) crosses a threshold or a drift detector fires. The signal that matters, late by the label delay.
  • Calibration only. When ranking holds (AUC unchanged) but the probabilities are off, refit a calibrator on recent labeled data: two parameters, minutes, and no new model to validate.

Whatever fires, the new model goes through the same evaluation, shadow or canary rollout as the first one; an automatic retrain that trains on a broken feed ships the bug.

ComponentShape/ParamsMeaningSource
PSIbin shares e, a of length BSymmetric binned KL between reference and current; a size of shiftFormula above
KS statistic Dsamples of size n, mLargest gap between empirical CDFs; a test of any shiftFormula above (scipy.stats.ks_2samp)
calibration_curven_bins=5, strategy="uniform"Observed rate and mean prediction per score bin, empty bins droppedcalibration.py
brier_score_lossscalarMean squared error of the probabilities; a strictly proper score_classification.py
log_lossscalarMean negative log-likelihood of the outcomes_classification.py
roc_auc_scorescalarRanking quality; unchanged by a monotone recalibration_ranking.py
confusion_matrixK × KThe validation matrix black-box shift estimation inverts_classification.py
CalibratedClassifierCVmethod="sigmoid" (default)Refits a calibrator on recent labelscalibration.py
FrozenEstimator wrappinga fitted modelCalibrate a deployed model without refitting it; all given data goes to the calibratorcalibration.py
_sigmoid_calibrationslope a, intercept bPlatt's two-parameter fit, with smoothed targetscalibration.py

Worked example

One feature x, standard normal at launch. The true outcome probability is σ(2x − 1), and the deployed model learned it exactly: p̂ = σ(2x − 1). Each week is 2000 evenly spaced quantiles of that week's input distribution, an idealized sample with no noise, and each point's outcome is scored by its true probability, so every number below is the expected value, not one random draw. Monitoring uses the launch deciles of x as PSI bins.

PSI on a shift of 0.3

Reference x ~ N(0, 1) and current x ~ N(0.3, 1), 2000 points each, binned at the reference deciles:

BinReference eCurrent aa − eln(a/e)Term
10.1000.0570-0.0430-0.5620.0242
20.1000.0700-0.0300-0.3570.0107
30.1000.0780-0.0220-0.2480.0055
40.1000.0850-0.0150-0.1630.0024
50.1000.0925-0.0075-0.0780.0006
60.1000.0990-0.0010-0.0100.0000
70.1000.10750.00750.0720.0005
80.1000.11750.01750.1610.0028
90.1000.13050.03050.2660.0081
100.1000.16300.06300.4890.0308
Sum110.0856

Mass moves out of the low bins into the high ones, and the two tails contribute most: 0.024 and 0.031 of the 0.0856. The PSI is below the 0.1 watch level. For two unit normals a distance δ apart, each KL direction is δ²/2, so the unbinned value is δ² = 0.09; binning can only lose information, and ten bins give 0.0856.

Reference and current shares per decile bin Ten pairs of bars, one per reference decile. Every reference bar is at 0.10. The current bars rise steadily from 0.057 in the lowest bin to 0.163 in the highest, so the shifted data has fewer points on the left and more on the right. The PSI term under each pair is largest at the two ends. 0.10 0 1 0.024 2 0.011 3 0.005 4 0.002 5 0.001 6 0.000 7 0.001 8 0.003 9 0.008 10 0.031 reference e current a PSI terms below the bins

KS on the same shift, and on a tiny one with ten times the data

On the same two samples the largest CDF gap is D = 0.1195, near the midpoint of the two means, where the gap between two unit normals peaks at 2Φ(δ/2) − 1 = 0.1192. The critical value at α = 0.05 is 1.358 · √((2000 + 2000) / (2000 · 2000)) = 0.0429, and the p-value is 6.3e-13: rejected decisively, while PSI said "stable".

Now shift by only 0.05 with 20000 points per sample. D = 0.0200 against a critical value of 0.0136, p = 0.00068: still rejected. The PSI is 0.0024, about a fortieth of the watch level. At production volumes the test flags shifts far too small to change any prediction, which is why KS is used to rank features by how much they moved, and PSI or an effect size to decide whether to act.

Five weeks, one decision rule

Each week, the monitor computes the PSI of x against launch, then, once labels mature, the mean predicted and observed rates, the Brier score next to the best achievable Brier (that of the true probabilities), and the largest gap in a five-bin reliability table. The rule: recalibrate or retrain if any reliability gap exceeds 0.02; else investigate inputs if PSI ≥ 0.25; else watch if PSI ≥ 0.1.

WeekWhat changedPSI(x)Predicted rateObserved rateBrierBest BrierMax gapDecision
1baseline0.00000.35230.35230.14050.14050.000ok
2small input shift0.00940.38080.38080.14430.14430.000ok
3covariate shift0.34010.53030.53030.15100.15100.000investigate inputs
4concept drift0.00000.35230.30330.19460.17790.238recalibrate or retrain
5base-rate drift0.00000.35230.42480.15550.14860.121recalibrate or retrain

Since each point's expected Brier is (p̂ − p)² + p(1 − p), the gap between the two Brier columns is exactly the mean squared error of the probabilities: zero means the model is right.

  • Week 3, covariate shift. x moves to N(0.6, 1). PSI is 0.340, a "significant shift", and the mean prediction jumps from 0.352 to 0.530. Yet the observed rate matches it and Brier equals the best possible: the model still knows P(y | x). The alarm is right that the world changed and wrong that the model broke. Prediction drift without a calibration gap is the signature of covariate shift.
  • Week 4, concept drift. The truth becomes σ(x − 1): the feature now matters half as much. The inputs are identical, so PSI is 0.0 and the predictions are unchanged, and nothing fires until labels arrive. Then the observed rate is 0.303 against 0.352 predicted, and the top bin is off by 0.238.
  • Week 5, base-rate drift. The truth becomes σ(2x − 0.5): every case is 0.5 more likely in log-odds, exactly the form a change of prior takes. Again PSI is 0.0; only labels show the observed rate rising to 0.425.
Reliability diagrams for the baseline, concept-drift and base-rate-drift weeks Mean predicted probability on the horizontal axis and observed rate on the vertical, five uniform bins. The baseline week's points lie on the diagonal. The concept-drift week's points fall below it at high scores, the top bin predicting 0.895 where 0.657 happened, and above it in the bottom bin: the model is overconfident in both directions. The base-rate-drift week's points lie above the diagonal everywhere: the model underpredicts every bin. 0 1 mean predicted 0 1 week 1, baseline: on the diagonal week 4, concept drift: flattened week 5, base-rate drift: above top bin, week 4 predicted 0.895 observed 0.657 233 of 2000 cases
Week 4 binCasesMean predictedObservedGap
18470.0800.139-0.060
23870.2930.2800.013
32840.4970.3760.120
42490.6990.4820.217
52330.8950.6570.238

Recalibrating instead of retraining

Both label-detected weeks keep the model's ranking: the true probability is still increasing in the model's logit z = 2x − 1. So fit Platt scaling, σ(a·z + b), on the week's labels (here by Newton's method, with each point's true probability as a soft target):

WeekabBrier beforeBrier afterBest Brier
concept drift0.500-0.5000.19460.17790.1779
base-rate drift1.0000.5000.15550.14860.1486

The fits recover the drift exactly. For concept drift the truth's logit is x − 1 = 0.5·z − 0.5; for base-rate drift it is z + 0.5. Two parameters fitted in milliseconds bring Brier back to the best achievable. This works only because the drift here is monotone in z; if the new relationship had reordered cases (a different feature now mattering), no calibrator could fix it, AUC would drop, and the model would need retraining.

Prior correction and maturing labels

Label shift with a known prior: a model trained where 10% of cases were positive is deployed where 30% are. The odds factor is (0.3/0.1) / (0.7/0.9) = 27/7, adding ln(27/7) ≈ 1.350 to every log-odds:

Model's p0.20.50.8
Corrected p′0.4910.7940.939

A case the model calls a coin flip is 0.794 = 27/34 likely in the new population.

Delayed labels: suppose the baseline week's positives are reported over five weeks, with this share known after each:

Weeks since prediction12345
Share of positives reported20%50%80%95%100%
Apparent positive rate0.0700.1760.2820.3350.352

The model predicts 0.352 and is exactly right, but a week after deployment it appears to overpredict fivefold. Unlabeled cases are not negatives yet; compare each cohort with the reference at the same age.

And the empty-bin trap: with reference shares 0.25, 0.25, 0.25, 0.25 and current shares 0.4, 0.3, 0.3, 0.0, the last term is (0 − 0.25) · ln 0 = +∞. Clipping to ε = 0.0001 gives 1.955 for that term and PSI = 2.044, a number set mostly by the choice of ε. Report an emptied bin as its own alert.

Input PSI is 0.34 on the most important feature, and the matured calibration is perfect. Retrain?

Not because of this. It is covariate shift the model handles, as in week 3. Check the pipeline for a bug, check performance in the newly common region, and keep watching; retraining on the new mix may help a little but is not urgent.

Implementation

A batch monitoring job: PSI and KS per feature against a reference snapshot, and, for the cohort whose labels have matured, calibration metrics and a refit calibrator for the frozen deployed model.

import numpy as np
from scipy.stats import ks_2samp
from sklearn.calibration import CalibratedClassifierCV, calibration_curve
from sklearn.frozen import FrozenEstimator
from sklearn.metrics import brier_score_loss, log_loss, roc_auc_score

def psi(reference, current, bins=10, eps=1e-4):
    edges = np.quantile(reference, np.linspace(0, 1, bins + 1)[1:-1])
    e = np.bincount(np.searchsorted(edges, reference, side="right"), minlength=bins) / len(reference)
    a = np.bincount(np.searchsorted(edges, current, side="right"), minlength=bins) / len(current)
    empty = (a == 0) | (e == 0)
    e, a = np.clip(e, eps, None), np.clip(a, eps, None)
    return float(np.sum((a - e) * np.log(a / e))), bool(empty.any())

for name in features:                          # inputs: every request, now
    value, emptied = psi(X_ref[name], X_week[name])
    d, p = ks_2samp(X_ref[name], X_week[name])
    report(name, psi=value, emptied=emptied, ks=d, ks_p=p)

scores = model.predict_proba(X_mature)[:, 1]   # outcomes: the matured cohort
frac_pos, mean_pred = calibration_curve(y_mature, scores, n_bins=5)
report("model", auc=roc_auc_score(y_mature, scores),
       brier=brier_score_loss(y_mature, scores), log_loss=log_loss(y_mature, scores),
       max_gap=np.abs(frac_pos - mean_pred).max(),
       rate_gap=scores.mean() - y_mature.mean())

recalibrated = CalibratedClassifierCV(FrozenEstimator(model), method="sigmoid")
recalibrated.fit(X_mature, y_mature)           # two parameters; the model is not refit

FrozenEstimator makes CalibratedClassifierCV use all the data for the calibrator and leave the model alone. scikit-learn's _sigmoid_calibration parameterizes Platt as σ(−(a·s + b)) and smooths the 0/1 targets, so its a has the opposite sign of the one above. The recalibrated model should itself be scored on a later cohort before it replaces the old one. Thresholds and reference windows belong in configuration next to the model version, since a retrained model needs a new reference snapshot: its own training data.

Tradeoffs

  • Sensitivity vs alert fatigue. Hundreds of features times daily tests produce false alarms every day; a team that learns to ignore them misses the real one. Alert on the score and a few key features, use sizes rather than p-values, and require persistence (two windows in a row).
  • PSI vs KS vs model-based detectors. PSI is easy to explain and handles categories but depends on the bins and ε. KS needs no bins but is univariate and over-sensitive at volume. Training a classifier to tell reference from current data (a domain classifier) catches multivariate shift that no single feature shows; Rabanser et al. (2019) found that testing a dimension-reduced representation, or the model's own outputs, works well for high-dimensional inputs.
  • Reference window. Comparing with training data detects everything since launch; comparing with last week detects sudden changes but lets slow drift through unnoticed. Seasonal data needs last year's same week.
  • Label delay vs label quality. Early, partial labels give a fast but biased verdict; waiting for maturity is accurate but slow. Proxy outcomes (a click for a purchase) are faster still and drift on their own.
  • Recalibrate vs retrain vs adapt online. Recalibration is cheap and safe but fixes only monotone errors. Retraining fixes concept drift but needs fresh labels, validation and a rollout. Online updates adapt fastest and can also learn a corrupted feed within hours.
  • Feedback loops. A deployed model changes the data it later trains on: approved loans are the only labeled loans, recommended items get the clicks. Sculley et al. (2015) list these hidden loops among ML systems' technical debts; a small randomized holdout is the usual remedy.
Why can't any input-only monitor detect week 4's concept drift?

Concept drift changes P(y | x) and can leave P(x) exactly as it was, as here: PSI is 0 and the predictions are identical. Only outcomes carry information about P(y | x), so only labeled data can show it.

Your KS test rejects on every feature, every day, at p < 0.001. What do you do?

Look at the effect sizes. With millions of rows, the test detects differences too small to matter; switch the alert to PSI or D above a size threshold, and use the p-values only to rank which features moved most.

The predicted rate is stable but AUC on matured labels fell from 0.82 to 0.71. Will recalibration help?

No. A monotone calibrator keeps the ranking, so it cannot change AUC; the model ranks cases worse than before. That is concept drift beyond a rescaling, and it needs retraining on recent labeled data.

Connections

Read first

Read next

No page builds on this one yet.

References

  • Moreno-Torres, Raeder, Alaiz-Rodríguez, Chawla and Herrera. A Unifying View on Dataset Shift in Classification. Pattern Recognition, 2012.
  • Shimodaira. Improving Predictive Inference under Covariate Shift by Weighting the Log-Likelihood Function. Journal of Statistical Planning and Inference, 2000.
  • Saerens, Latinne and Decaestecker. Adjusting the Outputs of a Classifier to New a Priori Probabilities: A Simple Procedure. Neural Computation, 2002.
  • Lipton, Wang and Smola. Detecting and Correcting for Label Shift with Black Box Predictors. ICML 2018. arXiv:1802.03916
  • Rabanser, Günnemann and Lipton. Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift. NeurIPS 2019. arXiv:1810.11953
  • Gama, Žliobaitė, Bifet, Pechenizkiy and Bouchachia. A Survey on Concept Drift Adaptation. ACM Computing Surveys, 2014.
  • Sculley et al. Hidden Technical Debt in Machine Learning Systems. NeurIPS 2015.
  • Breck, Cai, Nielsen, Salib and Sculley. The ML Test Score: A Rubric for ML Production Readiness and Technical Debt Reduction. IEEE Big Data 2017.
  • Platt. Probabilistic Outputs for Support Vector Machines and Comparisons to Regularized Likelihood Methods. In Advances in Large Margin Classifiers, MIT Press, 2000.
  • scikit-learn 1.9.1: calibration_curve, CalibratedClassifierCV, brier_score_loss, log_loss.