PSI vs KL Divergence for Lapse Drift Detection

Detecting lapse drift means measuring how far this quarter’s observed surrender experience has moved from the assumed lapse distribution baked into the reserve model, and the two statistics actuaries reach for first are the Population Stability Index and the Kullback–Leibler divergence. This page compares them head to head for lapse monitoring and shows when each is the right instrument, sitting inside Dynamic Threshold Tuning for Assumption Drift and the broader Assumption Validation & Rule Engine Design discipline that turns those measurements into automated rules.

The Problem

A lapse assumption is only credible for as long as the experience underneath it holds shape. When policyholders start surrendering earlier, later, or in different durational bands than the pricing curve predicted, the deterministic and stochastic reserves quietly drift off basis, yet no single policy record raises an alarm — the shift lives in the shape of the distribution, not in any one lapse. A drift detector therefore has to compare two binned distributions: the observed lapse counts across policy-year bands against the assumed lapse rates the Policy Lapse & Surrender Assumption Engines emit. The question is which divergence measure to compute over those bins. The Population Stability Index gives a symmetric, bounded-in-practice number with industry-blessed action thresholds; the Kullback–Leibler divergence gives an asymmetric, information-theoretic quantity that is unforgiving of bins the baseline said should be nearly empty. Choosing wrongly means either a detector that never trips or one that screams at every quarter’s sampling noise.

Defining the Two Statistics

Both measures start from the same inputs: an observed proportion aia_i of lapses in bin ii and an expected proportion eie_i from the assumed curve, with iai=iei=1\sum_i a_i = \sum_i e_i = 1.

The Population Stability Index is

PSI=i=1k(aiei)ln ⁣aiei.\text{PSI} = \sum_{i=1}^{k} (a_i - e_i)\,\ln\!\frac{a_i}{e_i}.

The Kullback–Leibler divergence of the observed distribution AA from the expected distribution EE is

DKL(AE)=i=1kailn ⁣aiei.D_{\mathrm{KL}}(A \,\|\, E) = \sum_{i=1}^{k} a_i \,\ln\!\frac{a_i}{e_i}.

The algebra reveals the family resemblance: PSI is exactly the symmetrized KL divergence, PSI=DKL(AE)+DKL(EA)\text{PSI} = D_{\mathrm{KL}}(A\|E) + D_{\mathrm{KL}}(E\|A). That single identity explains most of the practical differences below — PSI cannot tell you which direction the drift ran, but it also cannot be gamed by which distribution you happen to label “actual.”

A Minimal Working Example

The snippet below computes both statistics over the same binned lapse experience. It floors empty bins, renormalises, and prints an action verdict against the standard PSI bands. It runs as-is on NumPy:

import numpy as np

EPS = 1e-6  # floor so a genuinely empty bin cannot send the logarithm to infinity


def _to_proportions(counts, floor=EPS):
    counts = np.asarray(counts, dtype=float)
    proportions = counts / counts.sum()
    floored = np.clip(proportions, floor, None)
    return floored / floored.sum()  # renormalise after flooring


def population_stability_index(actual_counts, expected_counts):
    a = _to_proportions(actual_counts)
    e = _to_proportions(expected_counts)
    return float(np.sum((a - e) * np.log(a / e)))


def kl_divergence(actual_counts, expected_counts):
    p = _to_proportions(actual_counts)
    q = _to_proportions(expected_counts)
    return float(np.sum(p * np.log(p / q)))


def drift_verdict(psi):
    if psi >= 0.25:
        return "recalibrate"
    if psi >= 0.10:
        return "watch"
    return "stable"


if __name__ == "__main__":
    # Observed surrender counts by policy-year band vs the assumed lapse curve.
    duration_bands = ["yr1", "yr2", "yr3", "yr4_5", "yr6_10", "yr11_plus"]
    observed_lapses = np.array([420, 310, 260, 350, 300, 150])
    assumed_lapses = np.array([500, 300, 240, 300, 260, 120])

    psi = population_stability_index(observed_lapses, assumed_lapses)
    kl_fwd = kl_divergence(observed_lapses, assumed_lapses)
    kl_rev = kl_divergence(assumed_lapses, observed_lapses)

    print(f"PSI            = {psi:.4f}")
    print(f"KL(obs||assm)  = {kl_fwd:.4f}")
    print(f"KL(assm||obs)  = {kl_rev:.4f}")
    print(f"Cross-check    = {kl_fwd + kl_rev:.4f} (equals PSI)")
    print(f"Verdict        = {drift_verdict(psi)}")
Comparing PSI and KL divergence as parallel lapse drift detectors Binned observed versus assumed lapse rates split into two parallel statistics — the Population Stability Index with 0.1 and 0.25 bands, and the Kullback-Leibler divergence, asymmetric and information-theoretic — that both converge on one drift verdict gate. Binned lapse rates observed vs assumed PSI (symmetric) 0.10 watch / 0.25 recalibrate KL divergence asymmetric, directional Drift verdict stable / watch / recalibrate
Two divergence statistics computed over the same binned lapse experience, feeding one action gate.

How It Works, Block by Block

_to_proportions is the shared foundation and the shared danger. Both statistics are defined on probability distributions, not raw counts, so every path first divides by the total and floors near-zero cells. The floor matters more for KL than for PSI: a bin the assumed curve put at zero makes ln(ai/ei)\ln(a_i/e_i) diverge, and without the EPS clamp a single unexpected late-duration surrender would return inf. Renormalising after flooring keeps the vector a genuine distribution so the two measures stay comparable.

population_stability_index returns a number you can act on without a lookup table. The industry has converged on three bands — below 0.10 the distribution is stable, 0.10 to 0.25 warrants watching, and 0.25 or above signals a shift material enough to recalibrate. Because PSI is symmetric, it does not matter whether this quarter’s experience or the assumed curve is passed first; the magnitude is identical. That stability across argument order is exactly what makes PSI convenient to bolt onto an automated monitor, where a human is not present to reason about direction.

kl_divergence keeps the direction the PSI throws away. Passing (observed, assumed) measures the surprise of seeing the observed experience under the assumed model — the quantity that answers “how badly does my pricing curve explain what actually happened.” Swap the arguments and you get a different number, because KL is not a metric. That asymmetry is a feature when you care specifically about experience-under-assumption, and the cross-check line proves the identity: forward plus reverse KL reconstructs the PSI to the last decimal.

drift_verdict is where a statistic becomes a rule. Mapping the PSI onto the three action bands is the smallest possible bridge from measurement to governance. In production this threshold is not a hard-coded constant but a tuned parameter — the Building a Python Rule Engine for Policy Lapse Assumptions pattern shows how to make it a versioned rule rather than a magic number.

Edge Cases and Production Hardening

Empty and near-empty bins destabilise KL before they touch PSI. Late-duration bands often carry only a handful of exposures, so a single surrender swings aia_i wildly and the KL logarithm amplifies it. Merge sparse tail bins until each holds enough exposure to be meaningful, or widen the band definitions before either statistic is computed:

import numpy as np


def merge_sparse_bins(counts, min_exposure=30):
    counts = list(np.asarray(counts, dtype=float))
    merged = []
    carry = 0.0
    for c in counts:
        carry += c
        if carry >= min_exposure:
            merged.append(carry)
            carry = 0.0
    if carry > 0:  # fold any leftover into the final band
        if merged:
            merged[-1] += carry
        else:
            merged.append(carry)
    return np.array(merged)

Both the observed and assumed vectors must be merged with the same boundaries, or the bins stop lining up and the divergence becomes meaningless.

Binning choice dominates the result. Ten equal-width duration bands and six actuarially meaningful bands can hand back PSI values that differ by a factor of two on identical data, because collapsing bins hides within-bin movement. Fix the bin edges to the durational structure of the lapse assumption itself — the same knots the pricing curve uses — and version them alongside the assumption so a drift reading is always reproducible against a known partition.

Small samples inflate both statistics upward. With only a few hundred exposures, sampling noise alone produces a positive PSI even when the true distribution is unchanged, so a fixed 0.10 threshold trips on quiet quarters. Establish a noise floor by bootstrapping: resample the assumed distribution at the observed exposure count a few thousand times, compute the PSI each time, and set the “watch” band at a high percentile of that null distribution rather than at the textbook 0.10. This is the dynamic side of threshold tuning — the constant becomes a function of exposure.

Recommendation

Default to PSI for routine automated lapse monitoring. Its symmetry, its bounded practical range, and its three widely recognised action bands make it legible to reviewers and trivial to wire into a rule engine, and because it is the symmetrized KL divergence it already contains the information KL carries. Reach for KL divergence when direction is the question — when you specifically need to quantify how poorly the assumed curve explains emerging experience, or when feeding an information-theoretic model-selection step. In practice, compute both: report the PSI for the action decision and the forward KL as the directional diagnostic that tells the actuary which tail moved.

Compliance Note

Whichever statistic drives the gate, ASOP No. 25 (Credibility Procedures) and ASOP No. 56 (Modeling) expect the choice of drift measure, its thresholds, and its binning to be documented and reproducible rather than improvised each valuation. Recording the bin edges, the exposure count, and the tuned threshold alongside every PSI reading is what lets an appointed actuary defend a “no recalibration required” conclusion under review, and it feeds the same assumption-governance trail that the broader Assumption Validation & Rule Engine Design discipline maintains for every experience study.

Frequently Asked Questions

Is PSI just a symmetric version of KL divergence?

Yes. The Population Stability Index equals the forward KL divergence plus the reverse KL divergence between the same two binned distributions, which is why PSI gives the same magnitude regardless of which distribution you label as actual. KL divergence alone is asymmetric and keeps the direction of the drift that PSI discards.

What PSI value means my lapse assumption has drifted?

The industry convention is that a PSI below 0.10 indicates a stable distribution, 0.10 to 0.25 warrants monitoring, and 0.25 or above signals a shift material enough to justify recalibrating the assumption. These bands are starting points; with small exposure counts you should raise the thresholds to a bootstrapped noise floor so ordinary sampling variation does not trip the detector.

Why does KL divergence sometimes return infinity on lapse data?

KL divergence divides by the expected proportion inside a logarithm, so any bin the assumed curve set to zero while the observed data is non-zero drives the term to infinity. Flooring every proportion to a small epsilon and renormalising, or merging sparse late-duration bins before computing the statistic, keeps the value finite and stable.

Should I choose the number of bins before or after seeing the experience?

Fix the bin edges before seeing the experience, and anchor them to the durational structure of the lapse assumption itself rather than to equal-width buckets. Choosing bins after the fact lets the partition be tuned toward or away from a drift signal, which makes the reading impossible to reproduce and undermines any governance conclusion drawn from it.

Up a level: Dynamic Threshold Tuning for Assumption Drift · Assumption Validation & Rule Engine Design


Regulatory references: Actuarial Standards Board ASOP No. 25 (Credibility Procedures) and ASOP No. 56 (Modeling).