- Explain covariate shift, label shift and concept drift in terms of the joint distribution P(X, Y), and say which can be detected without labels.
- Calculate the Population Stability Index by hand and implement PSI and KS-based drift checks in Python.
- Choose monitoring signals across service health, data quality, distribution drift and model performance for a given system.
- Design performance monitoring for delayed labels using prediction logging, proxy labels, labelled samples and holdouts.
- Compare scheduled, triggered and continuous retraining and decide when retraining is the wrong response.
- Write an incident response plan and a model card with a monitoring section.
Before this: Deploying Models: From Artifact to Production Service, Model Evaluation and Metrics
A deployed model starts going stale on the day it ships. Customers change, fraudsters adapt, a partner renames a field, a sensor is recalibrated, a pandemic rewrites shopping habits. Unlike ordinary software, a model can fail silently: it keeps returning well-formed predictions that are steadily getting worse. Monitoring is how you notice, and a retraining and incident process is how you respond. This lesson covers what to watch, how to test for change statistically, what to do when labels arrive late, and how to document models so others can operate them.
Intuition: a map of a city that keeps changing
A trained model is a map drawn from a survey of the city at one point in time. Data drift is the city growing new neighbourhoods: the inputs you see now come from places the survey barely covered. Concept drift is the city renaming its streets: the same location now leads somewhere different, so the relationship between input and correct answer has changed. Your map can be fine for one and useless for the other. You can spot new neighbourhoods by looking at where travellers are coming from (inputs alone). You only learn that streets were renamed when travellers report getting lost (labels).
Kinds of drift, formally
Write the joint distribution of inputs and target as . Training data came from ; production data comes from .
| Name | What changes | What stays the same | Example | Detectable without labels? |
|---|---|---|---|---|
| Covariate shift (a kind of data drift) | A loan app launches in a new region with younger applicants | Yes | ||
| Label (prior) shift | Flu share of clinic visits jumps in winter | Partly, via the prediction distribution | ||
| Concept drift | possibly | Fraudsters change tactics, so the same transaction pattern now means fraud | Usually no, you need labels |
Terminology varies between authors; some use "data drift" for any change in and "concept drift" for any change in , which is what we do here. Drift can also be sudden (a schema change, a policy change), gradual (slowly ageing customers), recurring (seasonality) or incremental.
Important: drift in does not always hurt. If the model already handles the new region well, performance holds. And concept drift can destroy performance while every input histogram looks unchanged. That is why input-drift monitoring is a smoke detector, not a performance measurement.
What to monitor: four layers
- Service health: request rate, error rate, latency percentiles, saturation. Standard SRE monitoring, and the first thing to break.
- Data quality: schema (columns, types), null rates, out-of-range values, unseen category values, row counts per batch, freshness of upstream tables. Most "model incidents" are actually data-pipeline incidents.
- Distribution drift: of each important input feature and of the model's output scores, compared with a reference window (training data or a known-good recent window).
- Model performance: the actual metric (accuracy, AUC, calibration, precision at the operating threshold) computed when labels arrive, plus the business metric the model exists to move (approval rate, loss rate, revenue), broken down by important segments.
Detecting drift: PSI
The Population Stability Index, borrowed from credit scoring, compares a feature's binned distribution in production with a reference. Divide the reference data into bins (often deciles of the reference). Let be the fraction of reference rows in bin and the fraction of production rows in the same bin. Then
Each term is non-negative, because and always have the same sign. PSI equals the symmetric (Jeffreys) form of KL divergence between the two binned distributions: . An empty bin makes the logarithm infinite, so add a small to every fraction.
Commonly quoted industry thresholds: PSI below 0.1 means little change, 0.1 to 0.25 a moderate shift worth investigating, above 0.25 a major shift. These are conventions from practice, not statistical tests; calibrate them to your own features by looking at PSI between historical windows where you know nothing went wrong.
Worked example
A feature, applicant income, was split into quartiles on training data, so each reference bin holds . This week's applicants fall into the bins as : the population has shifted to higher incomes.
| Bin | Term | ||||
|---|---|---|---|---|---|
| 1 | 0.25 | 0.10 | −0.15 | 0.1374 | |
| 2 | 0.25 | 0.20 | −0.05 | 0.0112 | |
| 3 | 0.25 | 0.30 | +0.05 | 0.0091 | |
| 4 | 0.25 | 0.40 | +0.15 | 0.0705 |
That is in the "moderate, investigate" band, close to "major". Next steps: check whether the shift is real (a marketing campaign targeting higher earners) or an artefact (income now reported monthly instead of yearly would push nearly everything into one extreme bin). Then check whether model performance in those bins has changed.
Detecting drift: the Kolmogorov–Smirnov test
For a continuous feature, the two-sample Kolmogorov–Smirnov (KS) test compares the empirical cumulative distribution functions (ECDFs) and . Its statistic is the largest vertical gap between them:
Under the null hypothesis that both samples come from the same distribution, large is unlikely. For large samples of sizes and , the null is rejected at level when , with . With the critical value is .
The trap: production monitoring has huge samples. With a million rows per day, a KS test will flag shifts far too small to matter, and with dozens of features you will get false alarms from multiple testing alone. Practitioners therefore (a) look at the effect size ( itself, PSI, or Wasserstein distance) rather than only the p-value, (b) subsample to a fixed size, (c) correct for multiple comparisons or alert only on important features, and (d) require a shift to persist across several windows. For categorical features use a chi-squared test or PSI over categories, and alert separately on brand-new categories.
import numpy as np
from scipy.stats import ks_2samp
def psi(reference, current, bins=10, eps=1e-6):
# Bin edges from reference quantiles; open-ended outer bins catch new extremes.
edges = np.quantile(reference, np.linspace(0, 1, bins + 1))[1:-1]
e = np.bincount(np.searchsorted(edges, reference), minlength=bins) / len(reference)
a = np.bincount(np.searchsorted(edges, current), minlength=bins) / len(current)
e, a = e + eps, a + eps
return float(np.sum((a - e) * np.log(a / e)))
rng = np.random.default_rng(0)
ref = rng.lognormal(mean=10.8, sigma=0.5, size=50_000) # training-time incomes
cur = rng.lognormal(mean=11.0, sigma=0.5, size=20_000) # this week's incomes
print("PSI:", round(psi(ref, cur), 3))
res = ks_2samp(rng.choice(ref, 2_000, replace=False), rng.choice(cur, 2_000, replace=False))
print("KS D:", round(res.statistic, 3), "p-value:", res.pvalue)
The function uses quantile bins from the reference so each reference bin holds about 10% of rows, and the outer bins are open-ended so new extreme values are counted rather than dropped. A shift of 0.2 in the log-mean (about 22% higher incomes) produces a PSI of about 0.15 and, on 2,000-row subsamples, a KS statistic of about 0.15 with a p-value far below 0.001 (exact values vary with the random seed).
Multivariate drift. Per-feature tests miss changes in the relationships between features. Two practical approaches: train a domain classifier to distinguish reference rows from production rows (an AUC well above 0.5 means the distributions differ, and its feature importances show where), or compare distributions in a learned embedding space. Rabanser, Günnemann and Lipton's "Failing Loudly" study compares such methods empirically and is worth reading.
Monitor the outputs too. The distribution of predicted scores is one of the most useful drift signals: it compresses all features into what the model actually uses. A jump in the share of high-risk scores or a collapse in the predicted positive rate is often the first sign of trouble.
Performance monitoring when labels are late
Labels often arrive late or never: chargebacks take weeks, loan defaults take months or years, and recommendation "labels" are only for items shown. Strategies:
- Join predictions to labels when they arrive. Log every prediction with a request ID, timestamp, model version and the features used, then compute metrics on the labelled cohort as it matures. Report performance by prediction date, so you can see the cohort from three weeks ago once its labels are in.
- Use proxy labels: early signals that correlate with the true outcome (a manual-review verdict before the chargeback, a 30-day delinquency before the default, a click before a purchase). Validate the proxy's correlation with the real label periodically.
- Label a sample: send a small random sample of predictions to human reviewers every week. Random, not only uncertain cases, or your estimate is biased.
- Estimate performance without labels, with care. If the model is still well calibrated on the new data (plausible only under mild covariate shift), its expected accuracy on new data is the average of its confidence in its predicted class. This breaks down under concept drift, which is exactly when you most need it, so treat it as a hint.
Feedback loops complicate everything. A fraud model that blocks transactions never sees whether the blocked ones were really fraud; a recommender only gets clicks for items it chose to show. Over time the training data reflects the model's own decisions, which can lock in errors. Mitigations: keep a small randomised holdout or exploration slice where the model's decision is not applied or is randomised (within ethical and legal limits), log the propensity (probability of the action taken) so later training can correct for it, and be explicit about which outcomes are observed only for some decisions.
Alerting and retraining triggers
Good alerts are actionable, routed to an owner, and rare enough to be taken seriously.
- Page a human for service SLO breaches and data-quality failures that make predictions invalid (null rates spiking, schema change, a feature stuck at a constant).
- Open a ticket for moderate drift or slow metric decay.
- Use multiple windows (for example, alert only if PSI > 0.25 on two consecutive days) and severity tiers to reduce noise.
- Put the reference window, threshold and owner for each alert in configuration that is reviewed like code.
Retraining strategies, from simplest to most automated:
| Strategy | How it works | Good for | Risk |
|---|---|---|---|
| Scheduled | Retrain weekly or monthly on a sliding window | Steady, gradual drift | Wastes compute; may be too slow for sudden change |
| Triggered | Retrain when drift or performance crosses a threshold | Irregular change | Threshold tuning; triggers on data bugs |
| Continuous or online | Update incrementally as labels arrive | Fast-moving domains (ads, news) | Harder to validate; can learn from poisoned or buggy data quickly |
Whatever the trigger, retraining is not deployment. The new model passes the same evaluation gates and release process as any other (see the deployment lesson). And retraining cannot fix a data bug: if the drift is caused by a broken pipeline, retraining on the broken data teaches the model the bug. Diagnose first.
Incident response for ML systems
When a model misbehaves in production, borrow from site reliability engineering:
- Detect and declare. Someone owns the incident and a channel is opened.
- Mitigate first. Roll back to the previous model, switch to a rule-based fallback, raise the human-review share, or turn the feature off. Have these switches ready before you need them.
- Diagnose. Is it the service, the data, or the model? Check upstream data freshness and schema, recent deploys (code, model, features, config), and drift reports. Most incidents are found in the data.
- Fix and verify with the same evaluation gates.
- Blameless post-mortem: timeline, root cause, why detection took as long as it did, and action items (a new data check, a new alert, a test).
Model documentation: model cards
A model card (Mitchell et al., 2019) is a short, structured document shipped with a model. It records: model details (version, owner, architecture, training date); intended use and out-of-scope uses; the factors (groups, conditions, instruments) across which performance may vary; metrics and decision thresholds; evaluation and training data; quantitative results disaggregated by those factors; ethical considerations; and caveats. For operations, add the monitoring plan: which features and metrics are watched, the reference window, thresholds, the retraining policy and the rollback plan. Datasheets for datasets play the same role for training data. Several governance frameworks and regulations expect documentation of this kind (see the governance lesson); keep it versioned next to the model in the registry, not in a slide deck.
Common pitfalls
- Monitoring only inputs. Input drift is neither necessary nor sufficient for performance loss. Monitor outputs and, when labels arrive, real metrics.
- Trusting p-values at scale. Large samples make trivial shifts "significant"; use effect sizes and persistence.
- Retraining on broken data instead of fixing the pipeline.
- Reference window mismatch: comparing a December week with a training set from July and calling seasonality "drift". Use a seasonally comparable reference too.
- No prediction logging, so labels can never be joined back and incidents cannot be reconstructed.
- Aggregate-only metrics that hide a collapse in one segment (a new region, a new device type).
- Ignoring feedback loops, so the model gradually trains on its own decisions.
- Alert fatigue: dozens of noisy drift alerts that everyone mutes.
In practice
- Start with data-quality checks and output-distribution monitoring; they catch most incidents cheaply.
- Log every prediction with model version, features and a request ID, and build the label join on day one.
- Compute PSI or a distance per important feature against both the training reference and a recent rolling window; alert on persistent, large shifts.
- Track the business metric and segment-level performance, not only global accuracy.
- Keep fallbacks (previous model, rules, human review) ready, and rehearse switching to them.
- Write a model card with a monitoring section before launch, and update it with each retrain.
- Open-source tools exist for drift reports and data validation (for example Evidently, NannyML, Great Expectations), as do monitoring features in cloud ML platforms; the statistics underneath are the ones in this lesson.
Key takeaways
- Models fail silently, so production ML needs monitoring at four layers: service health, data quality, distribution drift and real performance.
- Data drift changes P(X) and can be seen without labels; concept drift changes P(Y|X) and usually needs labels to detect.
- PSI and KS quantify distribution shift, but at production scale use effect sizes and persistence rather than p-values alone.
- Log every prediction with its model version and ID so delayed labels can be joined back and cohorts evaluated by prediction date.
- Retraining is not a fix for broken data: diagnose first, mitigate with rollback or fallbacks, and send every retrained model through the release gates.
- Model cards document intended use, disaggregated results and the monitoring plan so others can operate the model safely.
Terms in this lesson
- Data drift
- A change in the distribution of model inputs P(X) between training and production.
- Concept drift
- A change in the relationship between inputs and target, P(Y|X), so that the same input now has a different correct answer.
- Label shift
- A change in the class proportions P(Y) while the class-conditional input distributions P(X|Y) stay the same.
- Population Stability Index (PSI)
- A binned measure of distribution change computed as the sum over bins of (actual − expected) × ln(actual/expected).
- Kolmogorov–Smirnov statistic
- The maximum absolute difference between two empirical cumulative distribution functions, used to test whether two samples share a distribution.
- Proxy label
- An early-available signal that correlates with the true outcome and is used to estimate performance before real labels arrive.
- Feedback loop
- A situation in which a model's decisions influence the data later used to evaluate or retrain it.
- Model card
- A short structured document describing a model's intended use, evaluation results across relevant groups, limitations and operating details.
Check your understanding
- 1.
Fraudsters change tactics so that transactions which used to be legitimate-looking are now often fraudulent, while transaction feature distributions look unchanged. What kind of drift is this?
- 2.
A feature has reference bin fractions (0.5, 0.5) and production fractions (0.4, 0.6). What is the PSI, to two decimal places?
- 3.
Your monitoring runs a KS test on every feature over a million rows per day and fires dozens of alerts daily, yet model performance is stable. What is the most likely cause?
- 4.
Loan default labels arrive after 12 months. Which approach gives the most trustworthy early estimate of model performance?
- 5.
A model's predicted positive rate drops to near zero overnight after an upstream team deployed a change. What should you do first?
Exercises
Work each problem before opening the solution.
- Exercise 1Core
Classify each scenario as covariate shift, label shift or concept drift, and say whether it could be detected from inputs alone:
- A spam filter: spammers start writing in the style of normal business email.
- A medical triage model is deployed at a hospital whose patients are on average 15 years older than in the training data; the relation between symptoms and diagnosis is unchanged.
- During a flu epidemic the share of flu cases among clinic visits triples, while flu patients' symptoms look as before.
Show solution
- Concept drift: the same textual features now map to a different label (spam that looks legitimate). Inputs alone may show little change; labels are needed.
- Covariate shift: P(X) changed (older patients), P(Y|X) did not. Detectable from inputs (the age distribution).
- Label shift: P(Y) changed, P(X|Y) did not. Partly detectable without labels: the overall input mix and the model's predicted flu rate will rise, and methods such as black-box shift estimation can estimate the new class proportions.
- Exercise 2Core
Compute the PSI for a feature with five reference bins each holding 20% of rows, and production fractions (0.15, 0.20, 0.20, 0.20, 0.25). Interpret the result using common thresholds.
Show solution
Only bins 1 and 5 differ.
Bin 1: (0.15 − 0.20) × ln(0.15/0.20) = (−0.05) × ln(0.75) = (−0.05)(−0.2877) = 0.01438.
Bin 5: (0.25 − 0.20) × ln(0.25/0.20) = (0.05) × ln(1.25) = (0.05)(0.2231) = 0.01116.
Bins 2–4 contribute 0.
PSI = 0.01438 + 0.01116 = 0.0255. Below 0.1: little change by the usual convention. No action beyond normal monitoring, though you might watch whether the tails keep moving.
- Exercise 3Core
Write a function
drift_report(ref_df, cur_df, features, psi_threshold=0.25)using pandas DataFrames that returns a list of(feature, psi)pairs for numeric features whose PSI exceeds the threshold, sorted from largest to smallest. Reuse thepsifunction from the lesson.Show solution
import numpy as np def psi(reference, current, bins=10, eps=1e-6): edges = np.quantile(reference, np.linspace(0, 1, bins + 1))[1:-1] e = np.bincount(np.searchsorted(edges, reference), minlength=bins) / len(reference) a = np.bincount(np.searchsorted(edges, current), minlength=bins) / len(current) e, a = e + eps, a + eps return float(np.sum((a - e) * np.log(a / e))) def drift_report(ref_df, cur_df, features, psi_threshold=0.25): flagged = [] for f in features: r = ref_df[f].dropna().to_numpy() c = cur_df[f].dropna().to_numpy() if len(r) == 0 or len(c) == 0: continue # report missing data separately as a data-quality issue value = psi(r, c) if value > psi_threshold: flagged.append((f, value)) return sorted(flagged, key=lambda t: t[1], reverse=True)Nulls are dropped here, so a jump in the null rate should be monitored by a separate data-quality check. Features with many tied values (e.g. lots of zeros) produce duplicate quantile edges;
searchsortedstill works, but some bins will be empty, which theepshandles. - Exercise 4Challenge
A recommender's click-through rate fell 12% over two weeks. Input PSI for all monitored features is below 0.05, the service is healthy, and no deploys happened. (a) List three plausible causes consistent with these facts. (b) Describe the investigation you would run for each. (c) Explain why the stable PSI does not rule out a model problem.
Show solution
(a) Plausible causes:
- Concept drift: user preferences changed (new season, a competitor launched, trending content changed), so the same features predict clicks less well.
- Feedback loop or catalogue change: the candidate pool changed (popular items went out of stock, new items without interaction history), so the model is ranking from a worse pool; or the model has been reinforcing a narrow set of items.
- An unmonitored input or upstream change: a feature not in the monitored set, a change in the UI or placement, or a logging change in how clicks are counted.
(b) Investigations:
- Compare model performance (e.g. AUC or NDCG) on recent logged impressions with a reference period; segment by user cohort and content category. Compare with the randomised exploration or holdout slice: if CTR fell there too, the change is in users or content, not the model.
- Check catalogue statistics (share of cold-start items, inventory) and the diversity of recommended items over time.
- Audit the full feature list, front-end release notes and the click-logging pipeline; reconcile click counts against an independent source.
(c) PSI measures only marginal input distributions. Concept drift changes P(Y|X) with P(X) unchanged, relationship changes between features are invisible to per-feature PSI, and unmonitored inputs or outcome-logging changes are not measured at all. Only outcome metrics reveal these.
- Exercise 5Challenge
Draft the monitoring section of a model card for a real-time credit-card fraud model whose chargeback labels arrive 30–60 days after a transaction. Include signals, reference windows, thresholds, owners, retraining policy and rollback plan.
Show solution
A good answer contains:
- Service: p99 latency < 250 ms and error rate < 0.1% (5-minute windows); page the on-call ML platform engineer.
- Data quality: schema checks on every request batch; null rate per feature vs 7-day baseline (alert if > 2× and > 1 percentage point); unseen merchant categories; upstream table freshness < 15 min. Page the data on-call.
- Drift: daily PSI for the top 15 features by importance and for the output score, against (1) the training reference and (2) the same week last year for seasonality. Ticket at PSI > 0.1 for 3 consecutive days; escalate at PSI > 0.25 for 2 days. Owner: the model's team.
- Decision metrics (real time): decline rate and manual-review rate per hour, compared with the same hour last week; alert outside ±20% relative.
- Performance (delayed): proxy precision from manual-review confirmations, weekly; true recall and precision at the operating threshold per transaction-week cohort once 60 days have passed; segmented by region, channel and card type.
- Feedback loop: a 0.5% holdout where low-risk declines are converted to review instead (within policy), to measure missed fraud and the precision of declines.
- Retraining: scheduled monthly on a rolling 12-month window, plus triggered if cohort recall drops > 3 points; every retrain goes through shadow and canary.
- Rollback: previous model kept deployed; automatic traffic switch on SLO breach; manual one-command rollback for metric anomalies; rule-based fallback if both models are unavailable.
- Review: card updated with each retrain, signed off by the model owner and risk.
Further reading
- paperGama, Žliobaitė, Bifet, Pechenizkiy & Bouchachia, A Survey on Concept Drift Adaptation (ACM Computing Surveys, 2014)
- paperRabanser, Günnemann & Lipton, Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift (NeurIPS 2019)
- paperMitchell et al., Model Cards for Model Reporting (FAT* 2019)
- paperLipton, Wang & Smola, Detecting and Correcting for Label Shift with Black Box Predictors (ICML 2018)
- bookBeyer, Jones, Petoff & Murphy (eds.), Site Reliability Engineering (Google, 2016)
Go further
Sources
Fact-checked and last reviewed 2026-11-05