Models decay when the data distribution shifts. PSI (Population Stability Index) detects changes in feature distributions. The KS test checks if two samples came from the same distribution. Monitor these to know when to retrain.
- Baseline: Save reference distributions from your training data (histograms, quantiles per feature)
- Schedule: Run PSI or KS test on each feature weekly (or per batch in streaming)
- Alert thresholds: PSI > 0.1 = moderate drift, PSI > 0.25 = significant — trigger investigation
- Investigate: Check if drift is in inputs (covariate shift) or in the target (concept drift)
- Retrain or adapt: If performance has degraded, retrain on recent data. For gradual drift, use a sliding training window
- Platforms such as Uber’s Michelangelo track feature and prediction distributions for every deployed model, and alert or retrain when they shift
- COVID-19 shifted income, spending and employment features at once, and many credit and demand models degraded within weeks — drift can be sudden and hit every feature together
- Gradual (seasonal) drift is normal — build it into the retraining schedule. Sudden drift (a pandemic, a regulatory change) needs an immediate response
pip install scipy pandas numpy
import numpy as np
from scipy.stats import ks_2samp
def psi(ref, new, bins=10):
edges = np.histogram_bin_edges(ref, bins=bins)
p = np.histogram(ref, bins=edges)[0] / len(ref) + 1e-6
q = np.histogram(new, bins=edges)[0] / len(new) + 1e-6
return np.sum((p - q) * np.log(p / q))
feature = 'V14'
ref = train_df[feature].dropna().to_numpy()
new = production_df[feature].dropna().to_numpy()
ks_stat, p_value = ks_2samp(ref, new)
print({'psi': psi(ref, new), 'ks': ks_stat, 'p_value': p_value})
A classifier that catches 80% of positives with 10% false alarms, unchanged. When 10% of cases are positive its precision is 0.471; when 20% are, 0.667. Nothing about the features’ distribution within each class moved — only the share of positives (label shift) — yet a key metric changed by 20 points.
- Monitoring input features alone misses label shift and concept drift; track outcomes when they arrive.
- Covariate shift, label shift and concept shift need different fixes; diagnose which one you have first (Moreno-Torres et al. 2012).
- Thresholds tuned under one prevalence are wrong under another; re-tune when the base rate moves.
tpr, fpr = 0.80, 0.10 # the model's behaviour within each class, unchanged
def precision(prevalence):
return tpr * prevalence / (tpr * prevalence + fpr * (1 - prevalence))
before, after = precision(0.10), precision(0.20) # only the share of positives changedAssumes import numpy as np and import pandas as pd (and SciPy where imported). Each snippet simulates or makes up its own data, as the comments say.
The same idea with a library
# Python — PSI & KS test from scipy.stats import ks_2samp import numpy as np def psi(ref, new, bins=10): edges = np.histogram_bin_edges(ref, bins=bins) p = np.histogram(ref, bins=edges)[0] / len(ref) + 1e-6 q = np.histogram(new, bins=edges)[0] / len(new) + 1e-6 return np.sum((p - q) * np.log(p / q)) # KS test stat, p_val = ks_2samp(ref_data, new_data) print(f"KS stat: {stat:.3f}, p: {p_val:.4f}")
- J. G. Moreno-Torres, T. Raeder, R. Alaiz-Rodríguez, N. V. Chawla & F. Herrera, “A Unifying View on Dataset Shift in Classification”, Pattern Recognition 45(1), 2012
- Z. C. Lipton, Y.-X. Wang & A. Smola, “Detecting and Correcting for Label Shift with Black Box Predictors”, ICML, 2018
- Dataset Shift in Machine Learning, J. Quiñonero-Candela, M. Sugiyama, A. Schwaighofer & N. D. Lawrence (eds.), MIT Press, 2009
Last reviewed October 2026 · How this site checks its content · Report a mistake