Lesson 15
Model Monitoring
Detect data drift and concept drift after deployment, monitor prediction distributions over time, and know when to retrain.
Deploying a model is not the end of the ML workflow. Production data changes over time: the feature distributions shift (data drift) or the relationship between features and labels changes (concept drift). Either way, accuracy falls without retraining.
Simulating data drift
Data drift is a change in the input feature distribution. If a model was trained on experienced employees and then deployed against new hires, the feature means shift, and the model's predictions become unreliable.
import numpy as np
import pandas as pd
from numpy.random import default_rng
rng = default_rng(42)
n = 300
experience = rng.integers(1, 21, n)
certifications = rng.integers(0, 7, n)
overtime = rng.integers(0, 45, n)
education = rng.choice([10, 12, 14, 16, 18], n, p=[0.05, 0.25, 0.35, 0.25, 0.10])
noise = rng.normal(0, 4500, n)
salary = (20000 + experience * 3200 + (education - 10) * 1500 +
certifications * 1200 + overtime * 80 + noise).round(-2)
salary = np.clip(salary, 18000, 160000).astype(int)
senior = (salary >= 65650).astype(int)
top = ((experience >= 15) & (certifications >= 5)).astype(int)
employees = pd.DataFrame(dict(
experience=experience, certifications=certifications,
overtime=overtime, education=education, salary=salary,
senior=senior, top=top,
))
import numpy as np
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
X = employees[["experience", "certifications", "overtime", "education"]].values
y = employees["senior"].values
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
clf = GradientBoostingClassifier(n_estimators=100, random_state=0).fit(X_train, y_train)
# Simulate distribution shift: new hires, 5 years less experience on average
X_shifted = X_test.copy()
X_shifted[:, 0] = np.clip(X_shifted[:, 0] - 5, 1, 20)
print(f"original experience mean: {X_test[:, 0].mean():.1f}")
print(f"shifted experience mean: {X_shifted[:, 0].mean():.1f}")
print(f"original % predicted senior: {(clf.predict(X_test) == 1).mean():.2f}")
print(f"shifted % predicted senior: {(clf.predict(X_shifted) == 1).mean():.2f}")Output:
original experience mean: 10.5
shifted experience mean: 6.2
original % predicted senior: 0.48
shifted % predicted senior: 0.23The model predicts fewer seniors on the shifted data, even though no label changed. This is data drift in action.
Monitoring with population stability index
A simple drift detector compares the mean and standard deviation of each feature in training versus the latest production data. A large difference signals drift. Formal tests (Kolmogorov-Smirnov, PSI) make this quantitative.
import numpy as np
import pandas as pd
from numpy.random import default_rng
rng = default_rng(42)
n = 300
experience = rng.integers(1, 21, n)
certifications = rng.integers(0, 7, n)
overtime = rng.integers(0, 45, n)
education = rng.choice([10, 12, 14, 16, 18], n, p=[0.05, 0.25, 0.35, 0.25, 0.10])
noise = rng.normal(0, 4500, n)
salary = (20000 + experience * 3200 + (education - 10) * 1500 +
certifications * 1200 + overtime * 80 + noise).round(-2)
salary = np.clip(salary, 18000, 160000).astype(int)
senior = (salary >= 65650).astype(int)
top = ((experience >= 15) & (certifications >= 5)).astype(int)
employees = pd.DataFrame(dict(
experience=experience, certifications=certifications,
overtime=overtime, education=education, salary=salary,
senior=senior, top=top,
))
import numpy as np
from sklearn.model_selection import train_test_split
X = employees[["experience", "certifications", "overtime", "education"]].values
y = employees["senior"].values
X_train, X_test, _, _ = train_test_split(X, y, test_size=0.2, random_state=42)
# Simulate a shifted production window
X_shifted = X_test.copy()
X_shifted[:, 0] = np.clip(X_shifted[:, 0] - 5, 1, 20)
names = ["experience", "certifications", "overtime", "education"]
print(f"{'feature':15s} train_mean prod_mean delta")
for i, name in enumerate(names):
train_m = X_train[:, i].mean()
prod_m = X_shifted[:, i].mean()
print(f"{name:15s} {train_m:9.2f} {prod_m:9.2f} {abs(train_m - prod_m):5.2f}")Output:
feature train_mean prod_mean delta
experience 10.30 6.70 3.60
certifications 2.98 3.15 0.17
overtime 22.40 23.60 1.20
education 14.22 14.10 0.12When to retrain
A practical retraining trigger is a drop in a tracked metric (accuracy, F1, or RMSE on labelled production samples) beyond a threshold. Without labels, use the prediction distribution: if the fraction of positive predictions drifts significantly from the training baseline, trigger a review.
A simple strategy:
- Save the model's output distribution on a labelled holdout at deploy time.
- Weekly, score the holdout with the live model.
- If the score drops more than 2–3 percent, retrain on new data.
Key takeaways
- Data drift: the input feature distribution changes after deployment
- Concept drift: the relationship between features and labels changes
- Monitor feature means and prediction distributions in production
- A significant drop in a tracked metric is a retraining trigger
- The ML workflow is a loop that continues after deployment
Loading playground…
Check your understanding
Model Monitoring Quiz
What is data drift?
1 / 10