Skip to content
nonconvexcard

Drift Detection

What PSI actually measures, how to monitor an embedding distribution, and how to detect quality regressions when the labels arrive weeks late or never.

AI EngineeringGraduate⏱ 3 hours9 min readdriftPSIKL divergencemonitoringconcept drift

Four things called drift

DefinitionFour things called drift

Write P(x)P(x) for the input distribution and P(y∣x)P(y \mid x) for the relationship being modelled.

NameWhat changedExampleRemedy
Covariate shiftP(x)P(x)new customer segment asks different questionsretrain or extend coverage
Prior shiftP(y)P(y)fraud rate triplesrecalibrate thresholds
Concept driftP(y∣x)P(y \mid x)the refund policy changed, so the right answer changedupdate the corpus, retrain
Upstream corruptionxx itself is wronga field stopped being populatedfix the pipeline

The fourth is not drift at all and is the most common in practice. It is listed here because it presents identically on every monitoring dashboard, and because treating it as drift leads to retraining a model on corrupted data — which converts a fixable bug into a permanent one.

Key idea

Distinguish them by what else moved. Upstream corruption shows a discontinuity — a step change at a deploy boundary, often with a null rate jumping. Covariate shift is gradual and shows in the input distribution with the output relationship intact. Concept drift shows no input change at all and is only visible in outcomes, which is why it is the hardest and needs labels or proxies.

PSI, and what it is

DefinitionThe population stability index

Bin a variable into kk buckets. Let pip_i be the fraction of the reference sample in bucket ii and qiq_i the fraction of the current sample. Then

PSI(p,q)  =  ∑i=1k(pi−qi) ln⁡piqi.\mathrm{PSI}(p, q) \;=\; \sum_{i=1}^{k} (p_i - q_i)\,\ln\frac{p_i}{q_i} .

Buckets are built from the reference distribution — usually deciles — and held fixed thereafter.

PropositionPSI is the symmetrised KL divergence
PSI(p,q)  =  DKL(p ∥ q)  +  DKL(q ∥ p),\mathrm{PSI}(p,q) \;=\; D_{\mathrm{KL}}(p \,\|\, q) \;+\; D_{\mathrm{KL}}(q \,\|\, p) ,

the Jeffreys divergence. Consequently PSI≥0\mathrm{PSI} \ge 0, with equality if and only if pi=qip_i = q_i for every ii, and it is symmetric in its arguments.

Key idea

Knowing PSI is Jeffreys divergence explains two things practitioners hit and usually treat as quirks. It diverges when a bucket empties: qi→0q_i \to 0 with pi>0p_i > 0 sends the term to infinity, which is why implementations add an epsilon — and that epsilon silently sets the maximum detectable drift. And it is symmetric, so it cannot tell you which direction the distribution moved; that needs the per-bucket contributions, which are worth logging individually since they localise the change.

HeuristicThe conventional PSI thresholds

Heuristic — a reasoning shortcut that predicts the right answer in the regimes discussed here. It is not a proof and can fail outside them.

PSIConventional reading
<0.1< 0.1no significant change
0.1−0.250.1 - 0.25moderate change, investigate
>0.25> 0.25significant change, act

Assumptions

These figures are industry convention with no distributional derivation, and they are not scale-free: PSI depends on the number of buckets and on sample size, so the same underlying shift produces different values under 10 buckets and 20, and a small sample produces a positive PSI against an identical distribution purely from sampling noise. Calibrate thresholds by computing PSI between two samples drawn from the same period — the reference against itself — and set the alarm above the noise floor that produces. Using 0.25 without doing this is guessing.

Embedding distributions

For an LLM system the input is text, and the useful distributional handle is its embedding.

def embedding_drift(reference: np.ndarray, current: np.ndarray) -> dict:
    """Three complementary views of how an embedding cloud has moved."""
    mu_r, mu_c = reference.mean(0), current.mean(0)
    centroid_shift = np.linalg.norm(mu_c - mu_r) / np.linalg.norm(mu_r)

    # Spread: has the cloud changed shape as well as position?
    spread_ratio = current.std(0).mean() / reference.std(0).mean()

    # Coverage: what fraction of current queries land far from everything we have seen?
    d = pairwise_min_distance(current, reference)          # nearest reference neighbour
    tail = float(np.mean(d > np.quantile(pairwise_min_distance(reference, reference), 0.99)))

    return {"centroid_shift": centroid_shift, "spread_ratio": spread_ratio, "novel_fraction": tail}

The third number is the operationally useful one: the fraction of current queries that are unlike anything in the reference set. A rising novel fraction is the earliest signal that the retrieval corpus no longer covers what people are asking, and unlike PSI it points at specific examples you can read.

Alerting on many things at once

PropositionMonitoring many features guarantees false alarms

Run an independent level-α\alpha test on each of mm features every period. Under the null — no drift anywhere — the probability of at least one alarm in a single period is 1−(1−α)m1-(1-\alpha)^m, and over TT periods

Pr⁡[at least one false alarm]  =  1−(1−α)mT  →mT→∞  1.\Pr[\text{at least one false alarm}] \;=\; 1-(1-\alpha)^{mT} \;\xrightarrow[mT\to\infty]{}\; 1 .

With m=50m = 50 features at α=0.01\alpha = 0.01, the daily false-alarm probability is 1−0.9950=0.3951 - 0.99^{50} = 0.395, and over a 30-day month it is 1−0.991500≈11 - 0.99^{1500} \approx 1.

When the labels are late

HeuristicMonitor proxies when labels are late

Heuristic — a reasoning shortcut that predicts the right answer in the regimes discussed here. It is not a proof and can fail outside them.

Ground truth for a generative system arrives weeks later, incompletely, or never. These move earlier and are available immediately:

ProxyReads onTypical failure it catches
refusal rateoutputsprompt or policy change, safety filter regression
mean output lengthoutputstruncation bugs, model swap, prompt regression
tool-call rate and failure rateoutputstool schema drift, broken integration
retrieval score distributionretrievalindex staleness, embedding mismatch
"no relevant documents" rateretrievalcorpus coverage gap
user retry / rephrase ratebehaviouranswer quality, broadly
escalation-to-human ratebehaviourthe closest thing to a quality label
thumbs-down ratebehaviourquality, heavily biased in sampling

Each is weak individually. Together, and with per-segment breakdowns, they catch most regressions before any labelled data exists.

Assumptions

Every proxy is confounded. Output length moves when the prompt changes for good reasons; retry rate moves when the UI changes; thumbs-down is dominated by a small unrepresentative population. They are leading indicators for investigation, not evidence for a decision. Treating a proxy movement as proof of a regression is how teams roll back improvements.

Key idea

The one genuinely reliable answer to delayed labels is to create labels deliberately: sample a small, fixed fraction of production traffic, run it through the evaluation harness in Evaluation Harnesses, and treat the result as a continuously refreshed quality estimate. It costs a percent or two of traffic and it converts drift detection from inference to measurement.

What this does not say

Detecting drift says nothing about responding to it. Automatic retraining on drifted data is tempting and dangerous: if the drift is upstream corruption, retraining bakes the corruption into the model; if it is concept drift, the recent data may be too sparse to retrain on; and a retraining loop triggered by a monitor is a feedback system whose stability nobody has analysed. The defensible default is that drift detection pages a human and retraining is a deliberate act.

References

  • SurveyJ. Gama, I. Žliobaitė, A. Bifet, M. Pechenizkiy, A. Bouchachia (2014). A Survey on Concept Drift AdaptationThe taxonomy of drift types and detection strategies this page follows.
  • PaperA. Gretton, K. Borgwardt, M. Rasch, B. Schölkopf, A. Smola (2012). A Kernel Two-Sample TestMaximum mean discrepancy, the test used below for embedding distributions.

These works are cited as sources to read, not reproduced. All exposition, examples and problems on this site are written independently; where an argument is standard, the citation points to a canonical treatment.

Results stated on this page

  • DefinitionFour things called driftCovariate shift, prior shift, concept drift and upstream corruption have the same symptom and different remedies.
  • DefinitionThe population stability indexA binned comparison of two distributions, summing the signed difference of bin masses times the log of their ratio.
  • PropositionPSI is the symmetrised KL divergenceproof includedThe population stability index is exactly KL(p,q) plus KL(q,p), so it is non-negative and zero only when the distributions agree.
  • HeuristicThe conventional PSI thresholdsValues below 0.1, between 0.1 and 0.25, and above 0.25 are conventionally read as stable, moderate and significant, with no distributional justification.
  • PropositionMonitoring many features guarantees false alarmsproof includedIndependent tests on m features at level alpha, repeated each period, produce alarms at a rate that approaches certainty as m or the number of periods grows.
  • HeuristicMonitor proxies when labels are lateRefusal rate, output length, tool-call rate, retry rate and escalation rate move before ground truth arrives and are available immediately.