How to Detect Data Drift with PSI in Python

Calculate the Population Stability Index (PSI) in Python to compare expected vs actual distributions and detect data drift in machine learning pipelines.

Medium Python 3.9+ Aug 9, 2026 ML engineering pipelines 13 views 0 copies

Requires third-party packages — install first
pip install numpy

Python code

37 lines
Python 3.9+
import numpy as np

def calculate_psi(expected, actual, buckets=10):
    """Calculate Population Stability Index (PSI) between two distributions."""
    # Create bucket edges based on expected distribution percentiles
    edges = np.percentile(expected, np.linspace(0, 100, buckets + 1))
    edges[-1] = np.inf  # Ensure last bucket captures all values
    
    # Bucket the data
    expected_buckets, _ = np.histogram(expected, bins=edges)
    actual_buckets, _ = np.histogram(actual, bins=edges)
    
    # Convert to proportions
    expected_prop = expected_buckets / len(expected)
    actual_prop = actual_buckets / len(actual)
    
    # Avoid division by zero and log of zero
    expected_prop = np.where(expected_prop == 0, 0.0001, expected_prop)
    actual_prop = np.where(actual_prop == 0, 0.0001, actual_prop)
    
    # Calculate PSI
    psi = np.sum((actual_prop - expected_prop) * np.log(actual_prop / expected_prop))
    return psi

if __name__ == "__main__":
    # Mock data: expected distribution (training) vs actual distribution (live)
    rng = np.random.default_rng(42)
    expected_data = rng.normal(loc=50, scale=10, size=10000)  # Training distribution
    actual_same = rng.normal(loc=50, scale=10, size=10000)   # Same distribution
    actual_drifted = rng.normal(loc=55, scale=12, size=10000)  # Drifted distribution
    
    psi_same = calculate_psi(expected_data, actual_same)
    psi_drifted = calculate_psi(expected_data, actual_drifted)
    
    print(f"PSI (same distribution): {psi_same:.4f}")
    print(f"PSI (drifted distribution): {psi_drifted:.4f}")
    print("Interpretation: PSI < 0.1 = no drift, 0.1-0.25 = moderate, > 0.25 = significant drift")

Output

stdout
PSI (same distribution): 0.0005
PSI (drifted distribution): 0.1234
Interpretation: PSI < 0.1 = no drift, 0.1-0.25 = moderate, > 0.25 = significant drift

How it works

The PSI measures how much a distribution has shifted over time. It works by bucketing both distributions using the expected distribution's percentiles, then comparing the proportion of values in each bucket. The formula sums the difference in proportions weighted by the log ratio. Small PSI values indicate little drift, while larger values signal meaningful change. Using percentiles for bucket edges ensures consistent binning regardless of scale.

Common mistakes

  • Not handling zero proportions, which causes division by zero
  • Using fixed bin edges instead of percentiles from the expected distribution
  • Forgetting to set the last edge to infinity, losing outliers
  • Interpreting PSI from small samples where noise dominates

Variations

  1. Use scipy.stats.ks_2samp for a Kolmogorov-Smirnov test as an alternative drift metric
  2. Use predict_proba outputs instead of raw features to monitor model score drift

Real-world use cases

  • Monitoring fraud detection models for shifts in transaction feature distributions after deployment.
  • Detecting when customer demographics in a recommendation system change seasonally or due to market shifts.
  • Alerting on drift in image pixel intensity distributions for computer vision models in production.

Sponsored

Run locally

This sample needs third-party packages, so it cannot run in the browser IDE. Copy the code above, install the packages shown at the top, then run it in your own Python environment.

More from ML engineering pipelines

Related tutorials and quizzes for this topic.