Intuition What this page is
The parent note gave you two drift tools — PSI (bin-by-bin) and the KS statistic (CDF gap). But a tool is only trusted once you've watched it survive every awkward input : distributions that grew, that shrank, that stayed still, an empty bin, a perfect match, a real-world alert, and an exam trap. This page marches through that whole grid.
Before any symbol appears, let us re-earn the two words we lean on constantly.
Definition What "PSI" stands for
PSI = P opulation S tability I ndex — a single number answering "how much did this feature's distribution move between training and now?" Small PSI = stable population; large PSI = the population shifted. We build the whole formula from scratch below.
Definition The three things a distribution can do to a bin
Picture a histogram: the feature range chopped into slots ("bins"), each slot holding a fraction of the data (a number between 0 and 1, all slots summing to 1).
==e i == = the e xpected fraction in bin i — from the reference (training-time) data.
==a i == = the a ctual fraction in bin i — from the new (live) data.
A bin can grow (a i > e i ), shrink (a i < e i ), or stay (a i = e i ). PSI is built so all three behave sensibly.
ln is doing here, in plain words
ln ( r ) ("natural log of r ") asks: "how many doublings-ish is r ?" — but the only facts we need:
ln ( 1 ) = 0 (no change → zero).
ln ( bigger than 1 ) > 0 (grew → positive).
ln ( between 0 and 1 ) < 0 (shrank → negative).
So ln ( a i / e i ) carries the sign of the change , and ( a i − e i ) also carries the sign of the change. Multiply two same-signed numbers → always positive. That is the whole trick behind PSI = ∑ i ( a i − e i ) ln ( a i / e i ) ≥ 0 .
Every drift computation this topic can throw at you lands in one of these cells. The examples below are labelled by cell so you can see the coverage.
Cell
What makes it tricky
Covered by
A. All bins grow/shrink cleanly
ordinary case, mixed signs
Ex 1
B. A bin stays identical
one term must vanish (ln 1 = 0 )
Ex 2
C. Perfect match, PSI = 0
the degenerate "no drift" floor
Ex 2
D1. Empty actual bin (a i = 0 )
ln 0 = − ∞ — needs a fix
Ex 3
D2. Empty reference bin (e i = 0 )
divide-by-zero a i / e i — needs a fix
Ex 3b
E. KS, shift right
offset CDFs, sup at the step
Ex 4
F. KS, identical samples
D = 0 degenerate
Ex 5
G. KS, spread change (same mean)
mean unchanged, shape changed
Ex 6
H. Real-world word problem
which tool? interpret the number
Ex 7
I. Exam twist — drift ≠ retrain
reasoning trap, not arithmetic
Ex 8
Ex 1 — Cell A: clean mixed drift
Reference e = [ 0.5 , 0.3 , 0.2 ] ; new a = [ 0.3 , 0.3 , 0.4 ] .
Forecast: Bin 1 shrank, bin 2 stayed, bin 3 grew. Guess: a moderate PSI, somewhere near the 0.1 –0.25 band. Write your guess down.
Step 1 — bin 1: ( 0.3 − 0.5 ) ln ( 0.3/0.5 ) = ( − 0.2 ) ( ln 0.6 ) = ( − 0.2 ) ( − 0.5108 ) = 0.1022 .
Why this step? Bin shrank, so both factors are negative; their product is positive — drift adds , never cancels.
Step 2 — bin 2: ( 0.3 − 0.3 ) ln ( 0.3/0.3 ) = ( 0 ) ( ln 1 ) = ( 0 ) ( 0 ) = 0 .
Why this step? Identical bin: the difference factor is 0 . It contributes nothing — exactly what "no movement here" should mean.
Step 3 — bin 3: ( 0.4 − 0.2 ) ln ( 0.4/0.2 ) = ( 0.2 ) ( ln 2 ) = ( 0.2 ) ( 0.6931 ) = 0.1386 .
Why this step? Bin grew, both factors positive → positive term. The biggest mover dominates the score.
Step 4 — sum: 0.1022 + 0 + 0.1386 = 0.2408 .
Why this conclusion? 0.2408 sits in the 0.1 –0.25 band → moderate shift, investigate . Forecast confirmed.
Verify: Both fraction lists sum to 1.0 (sanity: they are valid distributions). Every term was ≥ 0 , matching PSI ≥ 0 .
Ex 2 — Cells B & C: the zero floor
Reference e = [ 0.6 , 0.4 ] ; new a = [ 0.6 , 0.4 ] (nothing changed at all).
Forecast: No bin moved. If PSI has any self-respect it must be exactly 0 .
Step 1 — bin 1: ( 0.6 − 0.6 ) ln ( 0.6/0.6 ) = ( 0 ) ( 0 ) = 0 .
Step 2 — bin 2: ( 0.4 − 0.4 ) ln ( 0.4/0.4 ) = ( 0 ) ( 0 ) = 0 .
Why these steps? When a i = e i , the difference is 0 and the log-ratio is ln 1 = 0 . Both factors are zero — belt and suspenders.
Step 3 — sum: 0 + 0 = 0 .
Why this conclusion? This is the floor of PSI (< 0.1 → stable). It proves the claim "PSI = 0 iff the distributions are identical." Any nonzero PSI is a genuine flag.
Verify: 0 < 0.1 → stable , no action. Correct.
Ex 3 — Cell D1: empty actual bin (a i = 0 , ln 0 = − ∞ )
Reference e = [ 0.5 , 0.5 ] ; new a = [ 1.0 , 0.0 ] — the second bin emptied out in production.
Forecast: Bin 2 went to zero. Naively ln ( a 2 / e 2 ) = ln ( 0/0.5 ) = ln ( 0 ) = − ∞ , so ( a 2 − e 2 ) ln ( … ) = ( − 0.5 ) ( − ∞ ) = + ∞ . A raw PSI would be infinite — useless. So we must patch the zero.
Step 1 — spot the singularity: the ln 0 = − ∞ term appears because the actual bin is empty.
Why this step? ln is undefined at 0 ; one empty actual bin would crash the metric.
Step 2 — apply the standard fix (floor + renormalise): replace every zero fraction with a tiny floor ε = 0.0001 , then renormalise each side so its fractions sum back to 1 . Here new becomes [ 1.0 , 0.0001 ] with sum 1.0001 ; dividing each entry by 1.0001 renormalises to [ 0.99990 , 0.00009998 ] . Reference has no zeros, so it stays [ 0.5 , 0.5 ] .
Why this step? Flooring keeps the log finite; renormalising restores the "fractions sum to 1" property so the two sides remain comparable distributions. The shift is negligible, so the answer barely moves.
Step 3 — bin 1: ( 0.9999 − 0.5 ) ln ( 0.9999/0.5 ) = ( 0.4999 ) ( ln 1.9998 ) = ( 0.4999 ) ( 0.6930 ) = 0.3465 .
Why this step? The healthy, over-full bin contributes a modest positive term — the "normal" part of the drift.
Step 4 — bin 2: ( 0.00009998 − 0.5 ) ln ( 0.00009998/0.5 ) = ( − 0.4999 ) ( ln 0.00019996 ) = ( − 0.4999 ) ( − 8.517 ) = 4.258 .
Why this step? Emptying a bin is massive drift, and the smoothed term correctly reports a huge (but finite) contribution.
Step 5 — sum: 0.3465 + 4.258 = 4.604 .
Why this conclusion? ≫ 0.25 → major drift . A vanished bin is one of the strongest possible signals — often a broken pipeline , not organic drift. (See the Ex 8 trap.)
Verify: Finite because of ε ; the value grows without bound as ε → 0 , matching the "− ∞ " forecast. The renormalised new fractions sum to 1 , and the healthy bin still gave a modest ≈ 0.3465 .
Ex 3b — Cell D2: empty reference bin (e i = 0 , divide-by-zero)
Reference e = [ 0.5 , 0.5 , 0.0 ] ; new a = [ 0.4 , 0.3 , 0.3 ] — a new category appeared live that never existed in training (its reference fraction is 0 ).
Forecast: Bin 3 has e 3 = 0 , so the raw ratio a 3 / e 3 = 0.3/0 = + ∞ and ln ( + ∞ ) = + ∞ — again infinite. The mirror of Ex 3: the singularity now comes from the denominator . Same floor-and-renormalise fix. Expect a large PSI, because a brand-new category is major drift.
Step 1 — spot the singularity: the divide-by-zero a 3 / e 3 appears because the reference bin is empty.
Why this step? Division by 0 is undefined; one empty reference bin would crash the metric — the opposite failure mode to Ex 3.
Step 2 — floor + renormalise the reference : replace e 3 = 0 with ε = 0.0001 : reference [ 0.5 , 0.5 , 0.0001 ] sums to 1.0001 ; renormalise to [ 0.499950 , 0.499950 , 0.00009999 ] . New has no zeros, so it stays [ 0.4 , 0.3 , 0.3 ] .
Why this step? Flooring the denominator keeps the ratio finite; renormalising keeps reference a valid distribution.
Step 3 — bin 1: ( 0.4 − 0.49995 ) ln ( 0.4/0.49995 ) = ( − 0.09995 ) ( ln 0.80008 ) = ( − 0.09995 ) ( − 0.22304 ) = 0.02229 .
Why this step? Bin 1 shrank a little; both factors negative → small positive contribution.
Step 4 — bin 2: ( 0.3 − 0.49995 ) ln ( 0.3/0.49995 ) = ( − 0.19995 ) ( ln 0.60006 ) = ( − 0.19995 ) ( − 0.51073 ) = 0.10212 .
Why this step? Bin 2 shrank more → a bigger positive term.
Step 5 — bin 3: ( 0.3 − 0.00009999 ) ln ( 0.3/0.00009999 ) = ( 0.29990 ) ( ln 3000.3 ) = ( 0.29990 ) ( 8.00664 ) = 2.40119 .
Why this step? The brand-new category exploded from ~0 to 0.3 — the smoothed term reports a huge (finite) contribution, exactly as forecast.
Step 6 — sum: 0.02229 + 0.10212 + 2.40119 = 2.5256 .
Why this conclusion? ≫ 0.25 → major drift , dominated by the new category. In practice a never-before-seen category usually means a schema/pipeline change or a genuine new segment — investigate before retraining (Ex 8).
Verify: Both sides sum to 1 after renormalising; the value is finite thanks to ε , and grows without bound as ε → 0 , matching the "+ ∞ " forecast.
To read these, we only need the CDF ("cumulative distribution function").
Definition Empirical CDF, from zero
Line your n sample numbers up on the number line. The empirical CDF ==F ( x ) == is simply "what fraction of my samples are ≤ x ?" — a staircase that climbs by 1/ n at each data point, from 0 up to 1 .
The KS statistic ==D == is the biggest vertical gap between two such staircases:
D = sup x F ref ( x ) − F new ( x )
sup ("supremum") just means "the largest value it reaches". We use the whole CDF because it captures the entire shape, and the max gap because it is the worst-case disagreement between the two distributions.
Intuition Where the max gap can sit (used in every KS example)
On staircases, both CDFs are flat between data points, so the gap only changes at a data point (a jump of one staircase). Therefore the sup is always attained at some data point of either sample — so to find D you only need to scan the combined set of all data points from both samples, checking the gap just after each jump.
Figure 1 below draws the Ex 4 CDFs so you can see the sup gap. The cyan staircase is F ref (samples 1 , 2 , 3 , 4 ); the white staircase is F new (samples 3 , 4 , 5 , 6 , shifted right by 2). The amber arrow marks the largest vertical gap at x = 3 : F ref = 0.75 , F new = 0.25 , so D = 0.5 .
Ex 4 — Cell E: shift-right (the classic drift)
Reference [1,2,3,4], new [3,4,5,6]. Both size n = 4 .
Forecast: The new sample is the old one slid right by 2 . The staircases run parallel but offset — expect a big gap around the middle, roughly 0.5 .
Step 1 — build F ref : at x = 1 , 2 , 3 , 4 it reads 0.25 , 0.5 , 0.75 , 1.0 .
Why this step? Each of 4 points adds 1/4 = 0.25 to the staircase.
Step 2 — build F new : the values 3 , 4 , 5 , 6 give F new = 0.25 at x = 3 , 0.5 at 4 , 0.75 at 5 , 1.0 at 6 ; and F new = 0 for x < 3 .
Why this step? We need both staircases on the same axis before we can subtract them; F new stays flat at 0 until its first data point at x = 3 .
Step 3 — scan the combined data points { 1 , 2 , 3 , 4 , 5 , 6 } : the widest gap is at x = 3 , where F ref = 0.75 but F new = 0.25 , gap = 0.5 . (At x = 1 , 2 the gap is 0.25 , 0.5 ; from x = 4 onward it shrinks back.)
Why this step? sup sits at a data point of either sample, so scanning the combined set guarantees we don't miss it.
Step 4: D = 0.5 .
Why this conclusion? A gap of half the total height is a clear distribution shift — matches the forecast.
Verify: D ∈ [ 0 , 1 ] ✓. Recompute over the combined points gives max = 0.5 . See Figure 1 above (amber arrow = the sup gap).
Ex 5 — Cell F: identical samples (degenerate, D = 0 )
Reference [2,4,6,8], new [2,4,6,8].
Forecast: Same numbers → same staircase → zero gap everywhere. D must be 0 .
Step 1: F ref and F new are the identical staircase 0.25 , 0.5 , 0.75 , 1.0 at 2 , 4 , 6 , 8 .
Why this step? Both samples are the same four numbers, so each staircase climbs by 1/4 at exactly the same x positions — constructing them first lets us compare heights point by point.
Step 2: At every combined data point, ∣ F ref − F new ∣ = 0 .
Why this step? Subtracting a staircase from its twin gives 0 pointwise.
Step 3: D = sup x 0 = 0 .
Why this conclusion? This is the KS floor , the exact analogue of PSI = 0 : no evidence of drift.
Verify: D = 0 ≤ any threshold → no alarm. Correct.
Ex 6 — Cell G: same mean, different spread
Reference [3,4,5,6] (mean 4.5 ), new [1,4,5,8] (mean 4.5 ). The mean is unchanged , but the new data is more spread out.
Forecast: A mean-only check would miss this. But KS reads the whole shape, so it should still catch the spread change — expect a small-to-moderate D .
Step 1 — F ref : at x = 3 , 4 , 5 , 6 : 0.25 , 0.5 , 0.75 , 1.0 .
Why this step? Building the reference staircase first gives us the baseline shape (four tightly-packed points from 3 to 6) against which the spread-out new data is measured.
Step 2 — F new : points 1 , 4 , 5 , 8 → F new = 0.25 at x = 1 , 0.5 at 4 , 0.75 at 5 , 1.0 at 8 .
Why this step? We put F new on the same axis ; its first jump is far left (at 1 ) and its last far right (at 8 ) — the wide tails are exactly where a spread change will reveal itself.
Step 3 — scan the combined data points { 1 , 3 , 4 , 5 , 6 , 8 } (both samples, not just reference):
x = 1 : F ref = 0 (no ref point ≤ 1 ), F new = 0.25 → gap 0.25 .
x = 3 : F ref = 0.25 , F new = 0.25 → gap 0 .
x = 4 : 0.5 vs 0.5 → gap 0 .
x = 5 : 0.75 vs 0.75 → gap 0 .
x = 6 : F ref = 1.0 , F new = 0.75 (the 8 not yet counted) → gap 0.25 .
x = 8 : 1.0 vs 1.0 → gap 0 .
Why this step? We must include the new-only points x = 1 and x = 8 : at x = 1 the new staircase has already jumped while the reference hasn't, giving a gap of 0.25 — tied with the largest. Scanning only the reference points would still find 0.25 here, but checking x = 1 confirms no larger gap hides in the left tail.
Step 4: D = 0.25 .
Why this conclusion? Non-zero → KS detects the variance shift a mean-check would miss . This is why we prefer full-distribution tools.
Verify: Means equal (4.5 = 4.5 ) yet D = 0.25 > 0 — confirms KS sees shape, not just centre. Max over the full combined set is 0.25 .
Ex 7 — Cell H: real-world word problem (which tool?)
A fraud model's transaction_amount feature is monitored. Reference vs live proportions across three bins (low / mid / high):
e = [ 0.7 , 0.2 , 0.1 ] , a = [ 0.5 , 0.2 , 0.3 ] . Labels (was it fraud?) take 60 days to confirm. Which tool, and what do you do today?
Forecast: Labels are delayed, so live accuracy is unavailable now → we need an input-drift proxy . Bins are discrete fractions → PSI fits. "High amount" tripled its share; guess: moderate-to-major PSI.
Step 1 — pick the tool: PSI on the binned feature, because ground truth is 60 days away (delayed-label problem from the parent note).
Why this step? Input drift is an early proxy when performance can't yet be measured.
Step 2 — bin low: ( 0.5 − 0.7 ) ln ( 0.5/0.7 ) = ( − 0.2 ) ( ln 0.7143 ) = ( − 0.2 ) ( − 0.3365 ) = 0.0673 .
Why this step? The low-amount bin shrank; both factors negative → a small positive contribution. We compute per bin so we can later see which bin drives the alert.
Step 3 — bin mid: ( 0.2 − 0.2 ) ln ( 0.2/0.2 ) = ( 0 ) ( ln 1 ) = 0 .
Why this step? The mid bin did not move, so its term vanishes (ln 1 = 0 ) — it contributes nothing and is not the cause.
Step 4 — bin high: ( 0.3 − 0.1 ) ln ( 0.3/0.1 ) = ( 0.2 ) ( ln 3 ) = ( 0.2 ) ( 1.0986 ) = 0.2197 .
Why this step? The high-amount bin tripled; both factors positive → the dominant term, flagging where the drift lives.
Step 5 — sum: 0.0673 + 0 + 0.2197 = 0.2870 .
Why this conclusion? 0.287 > 0.25 → major input drift , driven by the "high amount" bin.
Step 6 — decide: Raise an alert and investigate the high-amount bin — do not auto-retrain, because we have no evidence yet that P ( Y ∣ X ) changed (concept drift). Could be a new product, or a broken currency conversion.
Verify: Fractions sum to 1.0 both sides ✓. PSI = 0.287 ✓. See Data drift and concept drift and Feature stores and data validation for the null/units checks.
Ex 8 — Cell I: the exam twist (drift ≠ retrain)
Prompt: "Feature income shows PSI = 4.6 overnight (like Ex 3, a bin emptied). A junior engineer schedules an immediate retrain. Justify accept or reject in two sentences."
Forecast: A PSI of 4.6 is astronomically high — too clean for organic behaviour. Suspect a data bug , not a real world-shift.
Step 1 — read the magnitude: organic drift rarely empties a whole bin overnight; PSI ≫ 0.25 instantly screams pipeline failure (nulls, unit change, schema break).
Why this step? The parent's mistake box "data drift always means retrain" applies exactly here.
Step 2 — the correct action: reject the auto-retrain. First confirm the feed: are income values now null or in the wrong currency? Retraining on garbage bakes the bug in.
Why this conclusion? Retraining only helps genuine concept drift (P ( Y ∣ X ) change confirmed by dropped labels/performance) — not a broken input pipe.
Verify: Consistent with Ex 3's finite-but-huge PSI and with Model retraining and CI-CD pipelines : retrain triggers should gate on performance , not raw input PSI alone.
Recall Cover the answers
What does PSI stand for, and what does it measure? ::: Population Stability Index; how much a feature's distribution moved between reference and live data.
Why does a "stayed the same" bin add 0 to PSI? ::: Both factors are 0 : ( a i − e i ) = 0 and ln ( a i / e i ) = ln 1 = 0 .
What two singularities break PSI on an empty bin, and the fix? ::: ln 0 = − ∞ (empty actual bin, Ex 3) and divide-by-zero a i /0 (empty reference bin, Ex 3b); fix by flooring zeros with ε then renormalising.
Which combined set must you scan to find the KS sup gap? ::: All data points of both samples (including new-only points), not just the reference points.
Which drift did Ex 6 catch that a mean-check misses? ::: A spread/variance change with the mean unchanged.
When labels are delayed, which family of metric do you rely on? ::: Input/prediction drift proxies (e.g. PSI), not live accuracy.
Does a huge overnight PSI justify auto-retrain? ::: No — suspect a broken pipeline first; retrain only on confirmed concept drift.
"Grow-Shrink-Stay, and mind the Zero" — every PSI term is ≥ 0 (grow & shrink both add, stay adds nothing), and only an empty bin (on either side) can break it — so floor your zeros.