ML Monitoring Report
Flat isometric illustration of a teal area chart with a tall peak, deep dip and rising arrow tip over a blue dotted grid platform.
drift-detection

PSI vs CSI: Formula, Thresholds and Model Stability

Compare PSI vs CSI for model monitoring: population and characteristic stability index formulas, threshold caveats, and examples of offsetting drift.

By ML Monitoring Report Editorial · · Updated · 8 min read

In scorecard terminology, PSI and CSI apply the same stability formula to different variables. The Population Stability Index summarizes movement in the model’s score distribution. The Characteristic Stability Index summarizes movement in an individual input characteristic. Comparing them helps locate distribution changes; it does not establish which feature caused a performance change.

The distinction matters because general ML tooling collapsed it. When a monitoring platform reports “PSI per feature,” that is CSI under a borrowed name. Nothing breaks, but the moment you sit in a model validation meeting with a credit-risk team, the two terms mean specific and different things, and the report has to say which one it computed.

For unstructured inputs and retrieval-based applications, monitoring tabular models vs LLM systems explains which stability checks need different instruments.

PSI vs CSI: same formula, different input

Both are the discrete symmetrised Kullback-Leibler divergence between a reference distribution and a current one:

index = Σ_b (Actual_b - Expected_b) * ln(Actual_b / Expected_b)

Expected_b is the share of the reference sample in bin b, Actual_b the share of the current sample. Feed it the score band distribution and you have PSI. Feed it one characteristic’s binned distribution and you have CSI for that characteristic. A 2025 arXiv paper on information-theoretic credit risk modeling shows the statistic is exactly Jeffreys divergence: non-negative, zero only at equality, and symmetric in the two samples.

The formula, the binning tradeoffs, the epsilon smoothing that keeps empty bins from producing infinities, and the code to compute it are all covered in population stability index explained. Everything below assumes that machinery and focuses on what changes when you point it at inputs instead of outputs.

A worked PSI and CSI example with offsetting drift

This hypothetical scorecard illustrates the arithmetic; it is not a measured production result. Take ten equal-frequency bands from a reference sample, so every band holds 10% of the population. Suppose the current score distribution looks like this:

Score bandExpectedActualContribution
1 (worst)10%11%0.0010
210%11%0.0010
310%11%0.0010
410%10%0.0000
510%10%0.0000
610%10%0.0000
710%10%0.0000
810%9%0.0011
910%9%0.0011
10 (best)10%9%0.0011

PSI = 0.006. That is as green as a stability metric gets. Now compute CSI on two of the characteristics feeding that score.

Credit utilisation:

UtilisationExpectedActualContribution
0-10%22%10%0.0946
10-30%26%18%0.0294
30-50%20%20%0.0000
50-70%16%22%0.0191
70-90%10%19%0.0578
90%+6%11%0.0303

CSI = 0.231. Verified income band, over the same window:

Income bandExpectedActualContribution
Under 30k18%10%0.0470
30-50k24%18%0.0173
50-80k26%26%0.0000
80-120k20%28%0.0269
120k+12%18%0.0243

CSI = 0.116.

The applicant pool became materially more leveraged and materially higher earning at the same time. Inside the scorecard those two movements push the score in opposite directions and very nearly cancel. PSI reports a stable population. The population is not stable; it is a different population that happens to average out to the same score distribution, and its default behaviour has no obligation to average out the same way.

This is the entire argument for computing both. PSI alone cannot distinguish “nothing changed” from “several things changed and offset.”

Reading the two together

PSI (score)CSI (characteristics)What it usually meansAction
LowLowNo large shift in the monitored marginal distributionsKeep checking labeled outcomes and important segments
LowOne or more highOffsetting shifts, or a characteristic with low weight in the modelInvestigate the characteristic; check whether its weight makes the score insensitive to it
HighOne highA candidate feature shift to investigateTrace the feature’s pipeline and check other causes
HighAll lowChanged scoring logic or changed relationships between inputsCheck deployments, transformations, score scaling, and joint distributions

The bottom row does not identify a single cause. Stable per-feature marginals can hide changed relationships between features. Also check for changes in the model artifact or score scaling, and compare transformation paths for training-serving skew.

Note also what neither metric can see. Both compare marginal distributions of things you can measure without labels. Neither can tell you whether the relationship between characteristics and default has changed, which is concept drift, and in credit risk it is the drift with the longest detection lag because ground truth arrives months later. Strategies for that blind window are in monitoring models when ground truth is late.

Thresholds, and how much to trust them

The convention inherited from credit scorecard practice is the same for both:

  • Under 0.10 — no meaningful shift
  • 0.10 to 0.25 — moderate shift, investigate and document
  • 0.25 and above — significant shift, treat as a trigger for revalidation

Fiddler’s writeup uses a tighter 0.2 upper cutoff, which is the useful reminder that these are industry convention rather than derived quantities. Two practical adjustments are worth making. First, use feature importance and affected-segment evidence to prioritize investigation; a low model weight is not proof that a change can be ignored. Second, binning affects both indices, so state the bin definitions and smoothing policy in a reproducible report. Choosing between PSI, KS, and calibration error for a given monitoring question is worked through in choosing monitoring metrics.

Computing both from one function

There is no separate CSI implementation. There is one stability function and two call sites, and the only discipline that matters is freezing the bin edges from the reference sample:

import numpy as np


def finite_sample(values, name):
    values = np.asarray(values, dtype=float)
    if values.ndim != 1 or values.size == 0 or not np.isfinite(values).all():
        raise ValueError(f"{name} must be a nonempty, finite, 1-D sample")
    return values


def stability_index(expected, actual, edges, eps=1e-6):
    """PSI for scores, CSI for one numeric characteristic."""
    expected = finite_sample(expected, "expected")
    actual = finite_sample(actual, "actual")
    if not np.isfinite(eps) or not 0 < eps < 1:
        raise ValueError("eps must be finite and between 0 and 1")
    edges = np.asarray(edges, dtype=float).copy()
    if (edges.ndim != 1 or edges.size < 3 or np.isnan(edges).any()
            or not np.isfinite(edges[1:-1]).all()
            or not (edges[1:] > edges[:-1]).all()):
        raise ValueError("edges must increase strictly with finite interior bounds")
    # Preserve frozen interior boundaries; include both overflow tails.
    edges[0], edges[-1] = -np.inf, np.inf
    exp_counts = np.histogram(expected, edges)[0]
    act_counts = np.histogram(actual, edges)[0]
    exp_pct = exp_counts / exp_counts.sum()
    act_pct = act_counts / act_counts.sum()
    exp_pct = np.clip(exp_pct, eps, None)
    act_pct = np.clip(act_pct, eps, None)
    exp_pct /= exp_pct.sum()
    act_pct /= act_pct.sum()
    return float(np.sum((act_pct - exp_pct) * np.log(act_pct / exp_pct)))


def score_edges(reference_scores, bands=10):
    reference_scores = finite_sample(reference_scores, "reference_scores")
    if isinstance(bands, bool) or not isinstance(bands, (int, np.integer)) or bands < 2:
        raise ValueError("bands must be an integer of at least 2")
    if reference_scores.min() == reference_scores.max():
        raise ValueError("constant reference: use an explicit value-change check")
    quantiles = np.linspace(0, 100, bands + 1)[1:-1]
    interior = np.unique(np.percentile(reference_scores, quantiles))
    return np.r_[-np.inf, interior, np.inf]


# Hypothetical score samples, including two values beyond the reference range.
ref_scores = np.arange(10, dtype=float)
cur_scores = np.r_[ref_scores, 100.0, 100.0]
edges = score_edges(ref_scores)
assert np.histogram(ref_scores, edges)[0].sum() == ref_scores.size == 10
assert np.histogram(cur_scores, edges)[0].sum() == cur_scores.size == 12
print(round(stability_index(ref_scores, cur_scores, edges), 6))  # 0.164792

For numeric feature DataFrames, freeze one set of edges per characteristic and reuse it:

frozen_edges = {name: score_edges(ref_df[name]) for name in characteristics}
psi = stability_index(ref_scores, cur_scores, score_edges(ref_scores))
csi = {
    name: stability_index(ref_df[name], cur_df[name], frozen_edges[name])
    for name in characteristics
}

The infinite outer edges retain every finite observation, including both overflow tails. Repeated quantiles are collapsed, smoothing is followed by normalization, and empty or nonfinite samples are rejected. A constant reference needs a separate value-change check. These examples are for numeric values; categorical characteristics need counts over a shared set of categories instead of quantile edges.

Recomputing edges from each new batch is the failure that makes a stability report useless: both samples get binned to look uniform against themselves, and the index collapses toward zero no matter how far the distributions actually moved. Store the edges alongside the model artefact so the reference used in month eighteen is the one the model was validated against.

What the 2026 supervisory guidance changed

If your model sits inside a regulated institution, the reference point moved this year. On 17 April 2026 the Federal Reserve, OCC and FDIC issued SR 26-2, Revised Guidance on Model Risk Management, which supersedes and replaces SR 11-7, the 2011 letter that most model monitoring policy still cites, along with the 2021 BSA/AML model statement.

Two points matter for a stability report. First, the revised guidance describes ongoing model monitoring as evaluating “the extent to which a model is performing as expected given potential changes in products, exposures, activities, clients, data relevance, or market conditions” — which is a description of exactly what a CSI panel measures, and a reason to keep the per-characteristic detail in the report rather than only the headline PSI. Second, it states that the frequency and scope of monitoring reports depend on the nature of the model, the availability of new data, and model materiality, and it frames the whole document as a risk-based approach tailored to the organisation’s model risk profile rather than a uniform standard. The letter itself is scoped to institutions above 30 billion dollars in total assets, though it notes it can still be relevant below that where model exposure is significant.

Tiering models by materiality and then setting monitoring depth per tier is the same structure the ML model monitoring framework recommends for non-regulated systems, which is a rare case of supervisory guidance and engineering practice converging on the same answer.

Caveats

Neither index is a performance metric. A stable population can still be scored badly, and an unstable one can still be scored well. Pair both with realised discrimination and calibration once outcomes land.

High-cardinality characteristics need grouping. A variable with hundreds of levels becomes hundreds of near-empty bins, and its CSI is mostly sampling noise.

Empty bins need an explicit epsilon. A zero on either side sends the log term to infinity or silently drops a bin that should have flagged. Whatever epsilon you pick changes the number, so document it in the report.

Reference choice is a policy decision. A frozen validation snapshot measures drift away from the model’s design population; a rolling window measures month-over-month change and will not notice a slow two-year slide. Regulated monitoring generally wants the frozen snapshot, with a rolling view alongside it rather than instead of it.

Seasonality can explain distribution movement. Compare with an equivalent seasonal period as well as the fixed reference before deciding whether a shift warrants action. The broader set of things that masquerade as drift is catalogued in types of data drift.

Sources

  1. SR 26-2: Revised Guidance on Model Risk Management (Board of Governors of the Federal Reserve System, April 17, 2026)
  2. SR 11-7: Guidance on Model Risk Management (April 4, 2011, superseded)
  3. Measuring Data Drift with the Population Stability Index — Fiddler AI
  4. An Information-Theoretic Framework for Credit Risk Modeling (arXiv:2509.09855)
#population-stability-index#psi#csi#credit-risk #model-monitoring #model-risk-management
Subscribe

ML Monitoring Report — in your inbox

Production ML monitoring, drift, and reliability. Sent only when there is something worth sending.

No spam. Unsubscribe anytime.

Related