Data Drift Detection: Keeping ML Models Honest in Production
Your model was accurate when you deployed it. Six months later, it is quietly making bad predictions. Data drift is usually why. Here is how to detect it before users notice.
Machine learning models have a fundamental assumption baked in: the data they will see in production looks like the data they were trained on. This assumption breaks. Gradually, then suddenly.
Understanding drift - what it is, how to detect it, and what to do about it - is one of the most practically important skills in ML engineering.
Three Types of Drift
Covariate drift (X drift): The input distribution changes. Your churn model was trained on data from 2023, when your average customer was 28 years old. By 2025, your growth strategy shifted, and your average customer is now 45. The features fed to the model look different from training time.
P_train(X) ≠ P_prod(X) ← covariate drift
P_train(Y|X) = P_prod(Y|X) ← relationship is still valid
The relationship between features and outcome has not changed - a customer with these feature values still has this churn probability. But now you are seeing many more customers with feature values your model has less confidence on.
Concept drift (Y|X drift): The relationship between inputs and outputs changes. What made a customer churn in 2023 (bad support) is different from what makes them churn in 2025 (price sensitivity). Your model learned the old relationship.
P_train(Y|X) ≠ P_prod(Y|X) ← concept drift
This is harder to detect and requires ground truth labels to measure directly.
Label drift: The distribution of outcomes changes. In training, 10% of customers churned. In production, market conditions have changed and 25% are churning. Your model's threshold is calibrated for 10%.
Detecting Covariate Drift
The most tractable drift to detect - you do not need labels.
Statistical tests on individual features:
pythonimport numpy as np from scipy import stats from evidently import ColumnMapping from evidently.report import Report from evidently.metric_preset import DataDriftPreset def detect_feature_drift(train_df, production_df, feature_columns, threshold=0.05): """ Returns dict of {feature: (statistic, p_value, is_drifted)} Uses KS test for continuous features. """ results = {} for col in feature_columns: train_values = train_df[col].dropna() prod_values = production_df[col].dropna() # Kolmogorov-Smirnov test: are these from the same distribution? statistic, p_value = stats.ks_2samp(train_values, prod_values) is_drifted = p_value < threshold results[col] = { "statistic": statistic, "p_value": p_value, "drifted": is_drifted, "train_mean": train_values.mean(), "prod_mean": prod_values.mean(), "train_std": train_values.std(), "prod_std": prod_values.std() } return results drift_report = detect_feature_drift(X_train, X_production_last_30d, feature_names) drifted_features = [k for k, v in drift_report.items() if v['drifted']] print(f"Drifted features: {drifted_features}")
Population Stability Index (PSI) - the industry standard:
PSI is widely used in financial services. It quantifies how much a feature distribution has shifted:
pythondef calculate_psi(expected, actual, buckets=10): """ PSI < 0.1: stable PSI 0.1-0.2: slight shift, monitor PSI > 0.2: significant drift, investigate """ # Create buckets based on expected distribution breakpoints = np.percentile(expected, np.linspace(0, 100, buckets + 1)) breakpoints[0] = -np.inf breakpoints[-1] = np.inf # Count observations in each bucket expected_counts = np.histogram(expected, bins=breakpoints)[0] actual_counts = np.histogram(actual, bins=breakpoints)[0] # Convert to proportions, avoid division by zero expected_props = np.where(expected_counts == 0, 0.0001, expected_counts / len(expected)) actual_props = np.where(actual_counts == 0, 0.0001, actual_counts / len(actual)) psi = np.sum((actual_props - expected_props) * np.log(actual_props / expected_props)) return psi for feature in feature_names: psi = calculate_psi(X_train[feature], X_production[feature]) status = "STABLE" if psi < 0.1 else ("MONITOR" if psi < 0.2 else "DRIFT") print(f"{feature}: PSI={psi:.3f} [{status}]")
Monitoring Prediction Distribution
Even without labels, watch your model's output distribution:
pythonimport pandas as pd from collections import defaultdict class PredictionMonitor: def __init__(self, baseline_predictions): self.baseline_preds = baseline_predictions self.baseline_mean = np.mean(baseline_predictions) self.baseline_std = np.std(baseline_predictions) self.recent_preds = [] def record_prediction(self, prediction: float): self.recent_preds.append(prediction) # Alert if window is large enough to evaluate if len(self.recent_preds) >= 1000: recent_mean = np.mean(self.recent_preds[-1000:]) # Alert if mean shifts by more than 2 standard deviations z_score = abs(recent_mean - self.baseline_mean) / self.baseline_std if z_score > 2: self._alert(recent_mean, z_score) def _alert(self, current_mean, z_score): print(f"ALERT: Prediction distribution shift detected!") print(f"Baseline mean: {self.baseline_mean:.3f}") print(f"Current mean: {current_mean:.3f}") print(f"Z-score: {z_score:.2f}")
Detecting Concept Drift with Labels
This requires ground truth, which often arrives with delay. A customer might churn 30 days after their prediction. Build a delayed evaluation pipeline:
pythondef evaluate_delayed_ground_truth(predictions_log, ground_truth_log, window_days=30): """ Join predictions from 30 days ago with labels that are now available. """ cutoff_date = pd.Timestamp.now() - pd.Timedelta(days=window_days) # Get predictions made before the cutoff historical_preds = predictions_log[ predictions_log['prediction_time'] <= cutoff_date ] # Join with ground truth evaluation_df = historical_preds.merge( ground_truth_log[['customer_id', 'churned']], on='customer_id', how='inner' ) current_auc = roc_auc_score(evaluation_df['churned'], evaluation_df['predicted_probability']) print(f"Rolling 30-day AUC: {current_auc:.3f}") return current_auc
Plot this AUC over time. A declining trend is concept drift. A sudden drop is usually a data pipeline bug.
A Practical Monitoring Architecture
Production API
│
├── Log request features + predictions to structured log
│
▼
Event stream (Kafka / SQS) or batch log collection
│
▼
Daily drift job:
- Load training baseline statistics
- Compute PSI / KS for each feature on last 24h of production data
- Compute prediction distribution shift
- Write results to metrics DB
│
▼
Alerting (Grafana + PagerDuty or Datadog monitors)
- PSI > 0.2 on any critical feature → Slack alert
- Prediction mean shift > 2σ → Slack alert
- Rolling AUC drop > 0.05 → incident ticket
When to Retrain
Not all drift requires immediate retraining. Guidelines:
| Signal | Action |
|---|---|
| PSI > 0.2 on a feature | Investigate source system; retrain if root cause is real distribution shift |
| Prediction mean shift > 2σ | Shadow-test a retrained model; promote if better |
| Rolling AUC drop > 0.05 over 2 weeks | Trigger retraining pipeline immediately |
| Sudden AUC drop > 0.1 | Likely a data pipeline bug, not drift - investigate before retraining |
Automated retraining (trigger retrain when drift threshold is crossed) is tempting but fragile. A data pipeline bug can trigger unnecessary retrains. Start with alert-to-human, graduate to automated retraining once your data pipelines are stable.
Tooling Options
| Tool | Best for | Cost |
|---|---|---|
| Evidently AI | Feature drift reports, quick setup | Open source |
| WhyLogs | Production logging + profiling | Open source |
| Arize AI | Full ML observability platform | Paid (free tier) |
| Grafana + custom PSI job | Existing infra, maximum control | Open source |
| AWS SageMaker Model Monitor | AWS-native deployments | Paid |
Start with Evidently or a custom PSI calculation before investing in a paid platform.
Common Mistakes
Monitoring only infrastructure metrics and missing feature drift. CPU utilization, memory usage, and request latency tell you whether your serving infrastructure is healthy, but they are blind to the data quality problems that most often degrade model performance in production. A model can serve thousands of requests per second at normal latency while receiving completely out-of-distribution inputs. Feature-level distribution monitoring is the only way to detect data drift before it becomes a prediction quality problem.
Using absolute thresholds instead of relative drift magnitude. A feature whose distribution shifts by 5% in absolute PSI terms is very different depending on whether it was previously stable or was already drifting. Relative drift magnitude - comparing the rate of change to the historical baseline variance - gives more actionable signals than fixed absolute thresholds applied uniformly across all features.
Alerting on every minor drift event without severity tiers. If every small distribution shift triggers an alert, on-call engineers quickly learn to ignore drift alerts, which means critical drift events go unnoticed. Tier your alerts: informational for minor drift, warning for moderate drift approaching the model's training distribution boundary, and critical for severe drift that likely warrants immediate investigation or rollback.
What to Practice Next
- Implement Population Stability Index (PSI) for one numerical feature in a pipeline you own; plot PSI over time on a week of production data and identify the threshold at which you would escalate to an alert.
- Define a three-tier severity schema (low/medium/high) for feature drift alerts with explicit PSI thresholds for each tier; write the alerting rule in pseudocode or as a monitoring config.
- Instrument a model serving pipeline to log raw feature distributions to a time-series store; verify that you can reconstruct the feature histogram for any hour in the past 30 days.
Stay in the loop
Get new ML/AI lessons in your inbox.
No account needed. We will send curriculum updates, launch notes, and practical learning resources.
Related Posts
More postsOpen-Weight and Small Models in 2026: When to Self-Host
Open-weight models are competitive, small models run on a phone, and the API-for-everything default is no longer obviously right. Here is a decision framework for self-hosting versus API, where small models win, what mixture-of-experts changes about the parameter count, and the hybrid most teams end up with.
ML Model to Production: A Complete Walkthrough
Most ML models die in notebooks. Walk through the full path from trained model to live API endpoint serving real traffic - packaging, containerizing, deploying, and monitoring.
Model Versioning with MLflow: Practical Guide
Without model versioning, you cannot reproduce results, roll back broken deployments, or compare experiments. MLflow gives you a practical registry - here is how to use it well.