Why a deployed model decays
What it is
The model is a frozen description of a relationship in the world at the time it was fitted. The world keeps moving: prices change, a competitor launches, a marketing campaign brings a different population, a product change alters behaviour, an upstream schema changes, fraudsters adapt.
Why it matters
It reframes maintenance as expected rather than exceptional. A model is not a build artifact that stays correct once it passes tests; it is closer to a perishable estimate with a shelf life, and the shelf life is a property of the domain rather than of the code.
In the real world: Customer behaviour shifts after a competitor's launch, and last year's churn model slowly gets worse.
Visual example
Performance decayTwo identical launches: one domain decays in weeks, the other in years. The code did not change in either.
Decay rates, per week
fraud: -0.0085 AUC/week — crosses the floor at week 11
credit: -0.0006 AUC/week — 80 weeks to lose what fraud loses in 6
retrained: sawtooth between 0.80 and 0.82: never approaches the floor
Same architecture, same rigour. The difference is entirely the domain
Why this matters
The retraining cadence is not a matter of taste; it is set by the decay slope and the distance to the floor. Measuring the slope once tells you what the cadence has to be.
Common misconception
Wrong idea: a model degrades because something broke, so a stable system means a stable model.
Correct idea: it degrades because the world moved while the model stood still. Nothing broke on the fraud curve above — the fraudsters simply adapted, which is what they do.
import pandas as pd
# Performance by cohort month: the shape of decay, measured rather than assumed.
scored = pd.read_parquet('scored_history.parquet') # prediction, label, scored_at
scored['month'] = scored['scored_at'].dt.to_period('M')
monthly = scored.groupby('month').apply(
lambda g: pd.Series({
'n': len(g),
'auc': roc_auc_score(g['label'], g['p']),
'mean_p': g['p'].mean(),
'base_rate': g['label'].mean(),
}),
include_groups=False,
)
monthly['auc_vs_launch'] = monthly['auc'] - monthly['auc'].iloc[0]
monthly.tail(6)Decay rates differ by orders of magnitude between domains, and assuming yours is slow is the mistake. Fraud and ad ranking can degrade meaningfully in days; a medical imaging model may hold for years. Measure your own curve from the first month of production rather than adopting somebody else's retraining cadence.