Drift Detection
SkillCloud & infraDetect when model inputs or predictions shift from the training distribution using PSI and statistical tests. Use when monitoring deployed models or diagnosing performance degradation.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the Drift Detection skill
What this skill tells your AI
The instructions your AI receives, as published by ml4t/skills in validation/drift-detection/SKILL.md and read by ahel’s review.
A model trained on 2018-2022 data may silently fail when the 2023 distribution shifts. Systematic drift detection catches degradation before it becomes a drawdown.
The Problem
Financial distributions are non-stationary. Volatility regimes change, correlations spike during crises, and feature distributions shift as markets evolve. A model deployed without monitoring can trade on stale assumptions for months. The three drift types - data drift (input distributions change), concept drift (feature-target relationship changes), and prior drift (target distribution changes) - each require different detection strategies. By the time performance metrics visibly decay, the damage is already done.
The Pattern
WRONG
# Deploy model and check performance monthly - too late
model = train_model(X_train, y_train)
# ... 3 months later ...
print(f"Live Sharpe: {live_sharpe:.2f}") # Already lost money
CORRECT
import numpy as np
from scipy.stats import ks_2samp
def calculate_psi(reference, current, n_bins=10):
"""Population Stability Index between two distributions."""
breakpoints = np.percentile(reference, np.linspace(0, 100, n_bins + 1))
# Open the outer bins. Percentile edges stop at the reference min and max,
# so np.histogram silently drops every current observation outside that
# range - exactly the tail shift PSI is supposed to catch.
breakpoints[0], breakpoints[-1] = -np.inf, np.inf
ref_pct = np.histogram(reference, bins=breakpoints)[0] / len(reference)
cur_pct = np.histogram(current, bins=breakpoints)[0] / len(current)
ref_pct = np.clip(ref_pct, 1e-4, None)
cur_pct = np.clip(cur_pct, 1e-4, None)
return float(np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct)))
# Monitor every feature at each rebalance
ref_data = X_train # Training period as baseline
cur_data = X_live # Most recent window
for col_idx, col_name in enumerate(feature_names):
psi = calculate_psi(ref_data[:, col_idx], cur_data[:, col_idx])
ks_stat, ks_pval = ks_2samp(ref_data[:, col_idx], cur_data[:, col_idx])
flag = "DRIFT" if psi > 0.20 else "ok"
print(f"{col_name:>25}: PSI={psi:.3f}, KS p={ks_pval:.3f} [{flag}]")
PSI Thresholds
| PSI | Interpretation | Action |
|---|---|---|
| < 0.10 | No significant drift | Continue |
| 0.10 - 0.20 | Moderate drift | Investigate, increase monitoring |
| > 0.20 | Significant drift | Consider retraining |
Detecting Concept Drift
Feature distributions can be stable while the feature-target relationship breaks. Track rolling IC to catch this:
from scipy.stats import spearmanr
def rolling_ic(predictions, actuals, window=63):
"""Rolling rank IC to detect concept drift."""
ic_series = []
for i in range(window, len(predictions)):
ic, _ = spearmanr(predictions[i-window:i], actuals[i-window:i])
ic_series.append(ic)
return np.array(ic_series)
ic = rolling_ic(model_predictions, realized_returns)
baseline_ic = np.mean(ic[:252]) # First year as baseline
if np.mean(ic[-63:]) < baseline_ic * 0.5:
print("ALERT: IC declined >50% from baseline")
Guardrails
- Baseline period must be representative and stable - do not use crisis periods as reference
- PSI and KS test catch different things: PSI is binned (better for tails), KS is continuous
- Some drift is normal in financial data - set thresholds based on historical drift rates, not arbitrary cutoffs
- Retrain triggers should be predefined: PSI > 0.20 → flag for review; rolling IC < 50% of baseline for 63+ days → trigger retrain pipeline
- Monitor prediction distribution too, not just features - a model can produce drifted outputs from stable inputs
Production Implementation
ml4t-diagnostic provides integrated drift monitoring:
from ml4t.diagnostic.api import cross_sectional_ic_series
from ml4t.diagnostic.evaluation.drift import analyze_drift
drift = analyze_drift(reference_df, current_df, methods=["psi", "wasserstein"])
ic_df = cross_sectional_ic_series(predictions, forward_returns, entity_col="symbol")
recent_ic = ic_df.tail(63)["ic"].mean()
baseline_ic = ic_df.head(252)["ic"].mean()
print(f"Baseline IC: {baseline_ic:.4f}, Recent IC: {recent_ic:.4f}")
Checklist
- PSI computed for all key features at each rebalance
- KS test run alongside PSI for continuous distribution comparison
- Rolling IC tracked for concept drift detection
- Alert thresholds predefined (not set after observing losses)
- Retrain protocol documented and triggered automatically on drift
Signals
- GitHub stars
- 20
- Forks
- 11
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
ml4t-drift-detection- Source
- github.com/ml4t/skills