Distribution shift and drift statistics: PSI, KS and chi-square
Before this lesson: lesson 01 (data contracts: checks on one batch; this lesson compares two batches); KL divergence; Bayes' rule and hypothesis testing (p-values, the chi-square test); calibration of a classifier. Time: about 60 minutes, plus the notebook.
Why this matters #
A model in production fails silently. The API still answers in 50 ms with status 200, but the data is no longer the data the model was trained on. Monitoring must find this, and it usually has no labels to do it: fraud chargebacks, flood reports and forecast verification arrive days or weeks later (lesson 08 handles delayed labels). So we watch the inputs and the predictions.
The same problem appears in science. A weather station gets a new instrument. A satellite channel is added to the data assimilation. A climate model is used in a warmer climate than its training period. In industry, PSI comes from credit scoring, where banks check every month that the population of applicants still looks like the one the score was built on. "How would you detect drift?" is one of the most common questions in ML system design interviews.
1. Predict first #
2. The picture #
One feature, two histograms. Take one input of a rain-nowcast model, for example the relative humidity at a station. Draw two histograms: the training data (the reference) and the last week of production (the current window). Monitoring compares the two, again and again, as new data arrives.
Things go wrong in four ways, from easy to hard to see:
- The data is broken. Nulls, a unit change (percent to fraction), a renamed column. The data contract of lesson 01 catches this.
- The data is valid but distributed differently. A dry summer. Only a comparison of two distributions shows it. This lesson is about this case.
- The rule of the world changed. The same humidity now gives less rain. The inputs look normal; only the labels show it.
- The model gets other inputs online than offline for the same example. This is training-serving skew, a bug in the pipeline. Lesson 08 finds it.
Two ways to factor the data. Let be the input and the label. The joint distribution can be written in two ways:
Each kind of shift changes one factor and keeps the other. Let be the training distribution and the production distribution, and let the model predict .
| Shift | What changes | What stays | Rain example | Seen without labels? |
|---|---|---|---|---|
| covariate shift | a heat wave: more hot days than in training | yes: input drift | ||
| label shift | autumn: more rainy hours, each looks as before | yes: through and | ||
| concept drift | often | the same sky now gives less rain | no: needs labels | |
| prediction drift | — | the share of "rain" alerts doubles | yes |
The figure below is a toy world with one feature . The model was trained on the reference data and predicts when . It is never retrained.
Choose a kind of shift and move the strength. Blue is the reference, orange the current data, aqua the model's fixed rule. Top: the input density . Middle: the true rule and the model's prediction. Right: how often the model predicts 1, how often , and the accuracy. Notice: under covariate shift the accuracy drops only when the inputs move to , where the reference had almost no data and the model is wrong. Under concept drift the input density and do not move at all, but the accuracy falls.
In the figure, choose concept drift and set the strength to 1. The accuracy falls from 0.85 to 0.73. What happens to the share of predicted rain, ?
is a fixed function of , and concept drift does not change . So the distribution of is the same as before. The model makes more mistakes, but it makes them at the same rate of "rain" predictions. This is why a prediction-drift monitor cannot see pure concept drift (section 3 proves it).
3. Definitions and main results #
What each kind of shift does to a model #
- Covariate shift. The rule stays, so a model that learned it well is still right where the training data was dense. Errors grow when production inputs fall where the training data was sparse, because there the model extrapolates. Accuracy can also go up: if the inputs move far from the decision boundary, most cases become easy. In the toy world, moving to raises the accuracy from 0.85 to 0.99.
- Label shift. By Bayes' rule the posterior changes, so a calibrated model becomes miscalibrated and the best decision threshold moves (proof below).
- Concept drift. The learned rule is now wrong, even on familiar inputs. Only retraining on recent labelled data fixes it.
- Prediction drift is a symptom, not a cause. It is still a very useful signal: one low-dimensional number that reacts to exactly the input changes the model uses.
Theorem (prediction drift depends only on the inputs). Let the model be fixed. Then the distribution of is determined by alone. So pure concept drift (with unchanged) causes no prediction drift.
Proof.
- For any set of outputs, .
- The integrand uses only and . The label and do not appear.
- Under pure concept drift , so step 1 gives the same value under and .
Label shift is different: is a new mixture of the same class densities. So label shift moves , and input and prediction monitors can see it.
Label shift correction. Assume and the model is calibrated on the training distribution, so it outputs . Then
Proof.
- Bayes' rule under : .
- Label shift: . Bayes' rule under : .
- Put step 2 into step 1: .
- The last factor does not depend on . Dividing by the sum over removes it.
So the fix is: multiply each predicted class probability by and renormalise. Worked example C shows the numbers.
Estimating without production labels (black-box shift estimation)
The correction needs , but production labels are late. Lipton, Wang and Smola (2018) estimate it from the predictions alone. Under label shift, does not change (the inputs of each class look the same, and is fixed). So
The left side is measured in production. The conditional confusion matrix is measured on labelled validation data. Solve the linear system for (it needs an invertible confusion matrix). Example with two classes: true positive rate 0.8, false positive rate 0.1, and 45% of production predictions are "rain". Then , so : half of the hours are rainy, without a single production label.
A sensor change is not a pure covariate shift. If a new rain gauge reads 5% high, the measured input has a new distribution, and the same reading now means less real rain, so changed too. Input drift monitors see it; the fix is to recalibrate the input or to retrain on data from the new sensor.
Signals, by the labels they need #
| Signal | Labels? | Catches |
|---|---|---|
| service health: traffic, errors, p99 latency | no | outages, overload (lesson 05) |
| data quality: schema, nulls, ranges, volume, freshness | no | broken pipelines, unit changes |
| input drift: PSI, KS, chi-square per feature; a domain classifier | no | covariate shift and label shift |
| prediction drift: the distribution of | no | input changes that reach the output |
| performance per slice: accuracy, calibration | yes, delayed | everything, including concept drift |
Per-feature statistics look at one feature at a time. They miss a change in the relation between features (each marginal the same, the joint different). A domain classifier catches it: label reference rows 0 and production rows 1, train a classifier to separate them, and measure its AUC on held-out rows. An AUC near 0.5 means no detectable shift; its feature importances point to the features that moved. Rabanser et al. (2019) compare these methods on images.
PSI: the population stability index #
Bin a feature into bins. Let be the reference (expected) proportion and the current (actual) proportion in bin . The population stability index is
The usual reading is a rule of thumb from credit scoring, not a statistical test: below 0.1 stable, 0.1 to 0.25 a moderate shift, above 0.25 a large shift. The standard binning is quantile bins of the reference, so each .
Theorem (PSI is the symmetrised KL divergence). Let all proportions be positive. Then , and with equality only if .
Proof.
- Split each term: .
- Sum over . The first parts give ; the second parts give . This sum is also called the Jeffreys divergence.
- In each term, and have the same sign, because is increasing. So every term is , and it is only if .
Step 3 is a direct proof; it does not even need Gibbs' inequality. Three properties explain why a PSI value depends on how you compute it.
- Empty bins. If , the term is infinite. Implementations floor the proportions at a small (here ). The term is then about , so its value depends on the choice of . Quantile bins of the reference have no empty reference bin.
- Merging bins cannot increase PSI. By the log-sum inequality, , and the same holds for the other KL term. (This is the data processing inequality for KL: a coarser view cannot show more difference.) Fewer bins see less of the shift.
- The noise floor. Even with no drift, and are estimated from finite samples, so they differ a little and PSI is positive. With reference values and current values,
Why the noise floor is
- Let be the true probability of bin (the same for both samples, since there is no drift), and write with a small .
- For small , . So each term is about , and . (With a fixed reference, times this sum is Pearson's chi-square statistic.)
- A proportion from values has variance . The reference proportion (or, for quantile bins, the true mass of a reference bin) has variance about . The two samples are independent, so .
- Divide by and sum: , because .
A simulation confirms it: with , and the formula gives , and the mean PSI over 2,000 windows with no drift is .
More bins and smaller windows raise the floor. A threshold such as 0.1 has a meaning only for a fixed binning and window size. With 50 bins and windows of 200 values the floor is , already the "large shift" level.
So PSI is an effect size: it measures how much the distribution moved, plus a small bias that shrinks as and grow. It does not grow with the sample size.
Move the mean shift to 0, then make the window small and the bins many. Blue bars are the reference proportions (all ), orange bars the current , and the lower chart shows each bin's term. The gauge compares PSI with the noise floor (violet) and the levels 0.1 and 0.25 (aqua). Notice: with no shift, and , PSI is far above 0.1 from noise alone. Press New current window a few times to see how much it varies.
A feature has two bins. Reference , current . What is the PSI?
. The changes do not cancel: both terms are positive, because and always have the same sign. is only the first term.
You merge two neighbouring bins of a 10-bin PSI into one bin and recompute it on the same data. What happens?
By the log-sum inequality each KL part of PSI can only decrease when two bins are merged (the data processing inequality). A coarser binning sees less of the shift. It stays the same only if the two bins had the same ratio .
The two-sample Kolmogorov–Smirnov test #
Let and be the empirical CDFs of a reference sample of size and a current sample of size : is the fraction of reference values . The two-sample KS statistic is the largest vertical gap between them:
No binning is needed. Under (both samples from the same continuous distribution), has, for large and , the Kolmogorov distribution:
This tail probability is the asymptotic p-value. For example gives .
Move the shift and the sample size. The blue and orange steps are the empirical CDFs of the reference and current samples; the magenta bar is the largest gap . Notice: at a fixed shift, stays near its population value as grows, but grows like and the p-value goes to 0. Press New samples to see how much varies at small .
Why the p-value goes to 0. The p-value depends on and on . For a fixed true shift, stays about the same as the samples grow, while grows like . So the p-value goes to 0, even for a harmless shift. Limits of KS: it works on one feature at a time; it is most sensitive near the middle of the distribution and less in the tails (the empirical CDF is noisiest where is largest, at the median); and its asymptotic p-value is conservative (too large) for discrete data with ties.
The chi-square test for categories #
For a categorical feature (station id, weather type), build a table of counts : row 0 is the reference, row 1 the current window, one column per category from the union of both samples. The expected count under (same distribution) is (row total times column total, divided by the grand total). The statistic is
The degrees of freedom are . A category that appears only in production causes no problem: its column total is positive, so its . The approximation needs expected counts of about 5 or more, so merge rare categories into "other". For a fixed shift, grows linearly with the sample size (as does for KS), so its p-value also goes to 0.
A categorical feature "weather type" gets a new category "hail" that never appears in the reference data. Which drift statistic handles this best?
In the chi-square table the new column has a positive total, so its expected counts are positive and the test is well defined. PSI's term for an empty reference bin is about , so its value depends on the arbitrary . KS needs an order of the values, and categories have none. A data-quality rule "unknown category" is a good second check.
Significant is not important #
All three statistics are computed on samples. PSI and are effect sizes: they estimate how far apart the two distributions are. The p-values of KS and chi-square answer another question: is there any difference at all? With production volumes, the answer is almost always yes.
Slide the sample size from to , then press Simulate. The shift is fixed (here standard deviations). Top: PSI (orange) and (violet) approach their population values and then stay flat. Bottom: the chi-square p-value (orange) and the KS p-value (violet) fall far below (aqua). Notice: at both tests say "significant" with p-values below , while PSI is , far below any alert level.
Answers to section 1. (1) Yes, the p-value is tiny, but nobody should be paged. For a normal mean shift of standard deviations, , where . So , and . One simulated sample (section 5) gives and . The PSI with 10 bins is about . The test detects that the shift is real, not that it matters. (2) No. This is label shift with and . The corrected probability is (worked example C).
You compute PSI, and the KS p-value on a reference and a current sample of 1,000 values each. Then you get 10 times more data with the same proportions. What changes?
PSI and depend only on the proportions (plus a noise term that shrinks), so they are effect sizes. The p-value depends on , which grows by . In worked example A, falls from 0.037 to while stays 0.20.
4. Worked example #
A. Three statistics on one shift. A rain feature has three ordered classes: dry, light, heavy. The reference proportions are ; this month they are .
-
PSI. Term by term:
- dry: ;
- light: ;
- heavy: .
Sum: , a moderate shift close to "large". Check with the theorem: and , and .
-
KS. Cumulative proportions: reference , current . The gaps are , so .
-
Chi-square with . Counts: reference , current . Column totals , row totals 100 and 100, grand total 200. So the expected counts are in each row. The six cells give , with degrees of freedom. For 2 degrees of freedom the tail is , so .
-
KS p-value with . , so . (The data has ties, so this p-value is conservative.)
-
The same proportions with . PSI and do not change. All counts are 10 times larger, so and . For KS, and .
B. Binning. Merge light and heavy into "wet": , . , as the merging property says. The merged bin hides that heavy rain doubled.
C. Label shift correction. Training data: . Now: .
- Weights: , .
- The calibrated model says for some hour, so .
- Unnormalised: rain , dry .
- Normalise: .
An hour that looked like a coin flip in training is now 80% likely to be rainy. A threshold of 0.5 on the old output is no longer the best threshold.
5. In code #
NumPy and SciPy check the hand computations. The notebook asks you to write PSI, KS and chi-square yourself.
import numpy as np
from scipy import stats
from scipy.stats import entropy
# A. One shift, three statistics (dry, light, heavy rain).
e = np.array([0.5, 0.3, 0.2]) # reference proportions
a = np.array([0.3, 0.3, 0.4]) # current proportions
psi = np.sum((a - e) * np.log(a / e))
print(f"PSI = {psi:.4f} = KL(a||e) + KL(e||a) = {entropy(a, e):.4f} + {entropy(e, a):.4f}")
# PSI = 0.2408 = KL(a||e) + KL(e||a) = 0.1240 + 0.1168
D = np.abs(np.cumsum(a) - np.cumsum(e)).max()
print(f"D = {D:.2f}") # D = 0.20
for n in (100, 1000):
x2, p, dof, _ = stats.chi2_contingency(np.vstack([e * n, a * n]), correction=False)
lam = np.sqrt(n * n / (n + n)) * D
print(f"n = m = {n}: X2 = {x2:.2f} (dof {dof}), p = {p:.1e}; "
f"KS lambda = {lam:.2f}, p = {stats.kstwobign.sf(lam):.1e}")
# n = m = 100: X2 = 11.67 (dof 2), p = 2.9e-03; KS lambda = 1.41, p = 3.7e-02
# n = m = 1000: X2 = 116.67 (dof 2), p = 4.6e-26; KS lambda = 4.47, p = 8.5e-18
# B. Merging two bins cannot increase PSI.
e2, a2 = np.array([0.5, 0.5]), np.array([0.3, 0.7])
print(f"PSI after merging = {np.sum((a2 - e2) * np.log(a2 / e2)):.4f}") # 0.1695
# C. Label shift correction: p(rain) = 0.2 in training, q(rain) = 0.5 now.
p_y, q_y = np.array([0.8, 0.2]), np.array([0.5, 0.5]) # (dry, rain)
w = q_y / p_y
post = np.array([0.5, 0.5]) # calibrated model output p(y | x)
print("corrected q(rain | x) =", (w * post / np.sum(w * post))[1]) # 0.8
# D. Large samples: a 0.01-sigma shift is "significant" but tiny.
rng = np.random.default_rng(0)
x = rng.standard_normal(1_000_000)
y = rng.standard_normal(1_000_000) + 0.01
r = stats.ks_2samp(x, y)
print(f"D = {r.statistic:.4f}, p = {r.pvalue:.1e}") # D = 0.0043, p = 2.8e-08
stats.kstwobign is the Kolmogorov distribution of section 3; ks_2samp chooses an exact or an
asymptotic p-value by itself. correction=False turns off Yates' continuity correction, which SciPy
applies only to tables.
6. Where it shows up #
- Weather and climate. Compare a seasonal input with the same season of earlier years, not with the annual mean, or every summer looks like drift. A statistical downscaling model used in a warmer climate than its training period works under covariate shift, with a risk of extrapolation. A new satellite instrument or an upgrade of the data assimilation system shifts input statistics overnight.
- Sensor networks and seismology. An instrument replacement changes gain or response: a sensor change, which is more than covariate shift (section 3). A station is a natural slice: monitor per station.
- Medicine. A new scanner or a new hospital changes the images: covariate shift. During an epidemic the share of positive cases rises while each case looks the same: label shift, the setting that black-box shift estimation was designed for (Lipton et al., 2018).
- Credit scoring and fraud. PSI comes from credit scoring, where it is computed every month on the score and on each input. Fraud has fast concept drift, because fraudsters adapt to the model.
- Tools. Drift libraries such as Evidently and NannyML compute PSI, KS and chi-square per feature and per window, and some offer domain classifiers. They implement the formulas of this lesson; their default bins, floors and thresholds differ, so read them before you compare numbers.
7. Common mistakes #
- Alerting on p-values with production volumes. With rows every shift is significant. Alert on an effect size (PSI or ) with thresholds calibrated on stable history (lesson 08).
- Comparing PSI values computed with different bins or window sizes. The value and its noise floor depend on both.
- Reading 0.1 and 0.25 as a statistical test. They are a rule of thumb for 10 bins and large samples. For small windows, the noise floor alone can reach them.
- Thinking "no prediction drift, so the model is fine". Pure concept drift does not move ; only labels show it.
- Using PSI with an floor for a new category. The value depends on . Use chi-square, and a data-quality rule for unknown categories.
- Checking only marginals. Each feature can look stable while the joint distribution changed. Add a domain classifier or prediction drift.
- Calling every accuracy drop "drift". First check data quality and skew (lesson 08); they are cheaper to find and to fix.
8. Check yourself #
- Reference proportions , current , floor .
Compute PSI, then again with . What does this show?
Answer
With : . With : . The empty bin dominates, and its term depends on the arbitrary . A new category is better tested with chi-square (here with ), or with a data-quality rule ("unknown category"). - A calibrated model was trained where . In production (label shift).
The model outputs . What is the corrected probability?
Answer
and . Unnormalised: and . Corrected: . - With 10 quantile bins, a reference of 10,000 values and windows of 2,000 values, what is the PSI
noise floor? And with 50 bins and windows of 200 values?
Answer
. With 50 bins: , already the "large shift" level. A simulation gives an even higher mean (0.34), because many bins are empty and hit the floor, and about 80% of windows with no drift exceed 0.25. - Why does label shift change , while concept drift may not?
Answer
Under label shift : the same class densities with new weights, so the mixture changes whenever the class densities differ. Concept drift changes , which does not enter at all. - You monitor 30 numeric features with KS p-values and alert when . Traffic grows from
to rows per window. What will you see, and what should you change?
Answer
Many more alerts, because grows by a factor of 10 for the same small shifts, so tiny harmless shifts become "significant". Alert on or PSI instead, with thresholds calibrated on stable history (for example the 99.9% quantile of past window scores). - Each of two features has the same distribution as in training, but their correlation changed
sign. Which monitors can see this?
Answer
No per-feature statistic (PSI, KS, chi-square), because each marginal is unchanged. A domain classifier on both features can (its AUC rises above 0.5), and so can prediction drift if the model uses the relation between the two features.
What does covariate shift change, and what does it keep?
It changes the input distribution and keeps the rule .
What does label shift change, and what does it keep?
It changes the class proportions and keeps the class-conditional inputs .
What is concept drift, and which monitor can see it?
A change of : the same input now has another label distribution. Only performance monitoring with labels can see it.
Why does pure concept drift cause no prediction drift?
is a fixed function of , so depends only on , which concept drift does not change.
Write the PSI formula.
, with reference proportions and current proportions per bin.
PSI equals which divergence?
The symmetrised KL (Jeffreys) divergence: .
What is the PSI noise floor with no drift, for bins, reference and current values?
About . With , , : .
Define the two-sample KS statistic.
, the largest gap between the two empirical CDFs.
Why do KS p-values go to 0 at production volumes?
The p-value uses . For a fixed shift stays the same and grows like .
How many degrees of freedom has a chi-square drift test on categories?
: the table has 2 rows and columns, so .
9. Interview angle #
- "How would you detect drift without labels?" — Data quality checks first; then per-feature drift (PSI or KS for numeric, chi-square for categorical), prediction drift, and a domain classifier for joint shift. Thresholds on effect sizes, calibrated on stable history.
- "PSI or KS?" — PSI is binned and symmetric, an effect size with a known noise floor; it depends on the binning. KS needs no bins, but its p-value goes to 0 with large samples, so use itself as the score. Use chi-square for categories.
- "What kinds of distribution shift are there?" — Covariate, label and concept shift from the two factorisations of , and prediction drift as a symptom. Say which ones need labels.
- "The class balance changed in production. What do you do?" — It is label shift if each class looks the same. Estimate (from fresh labels or by black-box shift estimation), reweight the predicted probabilities by , and move the decision threshold.
10. Practice #
The notebook for this lesson runs in Colab. Each exercise has a check cell and a folded solution:
- Write
psi(reference, current, n_bins=10, eps=1e-4)with reference-quantile bins, and check that PSI equals KL + KL withscipy.stats.entropy. - Write
ks_statistic(x, y)with sorting andnp.searchsorted, and compare it withscipy.stats.ks_2samp. - Write
chi2_drift(ref_counts, cur_counts)from the expected counts, and compare it withscipy.stats.chi2_contingency. - Simulate 2,000 windows with no drift: compare the mean PSI with and take the 99.9% quantile as a calibrated threshold.
- Simulate label shift: show that a calibrated logistic regression becomes miscalibrated, and fix it with the weights .
11. Watch and read #
- Chip Huyen, Data distribution shifts and monitoring (2022): the sections "Types of Data Distribution Shifts" and "Detecting Data Distribution Shifts".
- Zhang, Lipton, Li and Smola, Dive into Deep Learning, Environment and Distribution Shift: covariate, label and concept shift, with the corrections.
- Lipton, Wang and Smola, Detecting and Correcting for Label Shift with Black Box Predictors (ICML 2018): black-box shift estimation.
- Rabanser, Günnemann and Lipton, Failing Loudly: An Empirical Study of Methods for Detecting Dataset Shift (NeurIPS 2019): per-feature tests, dimension reduction and domain classifiers compared.
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.