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.
pip install numpy
Python code
37 linesimport 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
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
- Use scipy.stats.ks_2samp for a Kolmogorov-Smirnov test as an alternative drift metric
- 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
More from ML engineering pipelines
- Bayesian Optimization in Python: A Simplified Mock Implementation medium
- Build a Data Helper Class in Python for ML Pipelines easy
- Build a Mock Random Forest Classifier in Python easy
- Champion Challenger Deployment Mock in Python easy
- Compare Model A vs Model B Metrics in Python easy
- Create a Minimal Great Expectations Suite Mock in Python easy
Keep learning
Related tutorials and quizzes for this topic.