The Signal That Beats the Label by a Month

The Signal That Beats the Label by a Month

I built a telecom churn model once against eight thousand synthetic subscribers over a hundred and twenty simulated days, scoring on features like average monthly data usage and support tickets in the trailing thirty days. On day forty, a new discounted data plan launched and shifted the distribution of average data usage for a large subscriber segment. No corresponding change to the churn label, because a churn label only becomes observable thirty-plus days after a subscriber actually cancels.

The problem

Without a check on the feature distributions themselves, the only signal I had was a drop in realized accuracy once enough post-shift churn labels accumulated. That surfaced around day seventy, a full thirty days after the true shift started. For thirty days, the model kept scoring subscribers against a picture of "normal usage" that had already stopped being true, mis-scoring retention-offer eligibility the entire time, and nobody knew, because nothing was watching the inputs. The team only found out the slow way, watching outcomes accumulate, which meant watching the damage accumulate too.

That thirty-day gap is structural, built into the calendar itself rather than any one scenario. Accuracy monitoring needs ground truth, and ground truth needs time to happen. A churn label needs a subscriber to actually churn. A fraud label needs a chargeback dispute to resolve, which can take weeks or months. A demand-forecast label needs the forecasted season to actually end before you know how wrong you were. Any model whose label lags its prediction by design has this exact blind spot built in, and the length of the blind spot is set by how long the label takes to arrive, not by how bad the underlying problem is.

The pattern

The fix doesn't touch labels at all. I run a daily per-feature Population Stability Index check that compares that day's live feature distribution against the training-set baseline distribution. A PSI reading above 0.2 gets flagged as moderate drift, above 0.3 as high drift and alerting. Because PSI only needs feature values, not outcomes, it can fire days or weeks before enough labels even exist to catch the identical problem the slow way.

Running that check against the same churn scenario, the target is a catch on the shifted feature within about five days of day forty, cutting days-to-detection to under ten against the twenty-five-plus days the unmonitored version takes. That closes most of the window during which subscribers who should have gotten a retention offer would otherwise have been mis-scored. PSI itself isn't something I invented. Evidently AI's comparison of five drift-detection statistics, PSI, the Kolmogorov-Smirnov test, Wasserstein distance, KL divergence, and Jensen-Shannon distance, confirms PSI as a standard, low-sensitivity-by-design metric with an industry-standard threshold convention, and AWS SageMaker Model Monitor's own data-capture-to-baseline-to-constraints-to-violation-report pipeline is an independent implementation of the same underlying shape: compute a summary statistic, compare it against a stored baseline, alert on divergence.

Design considerations

Don't let the name fool you, because I nearly did. What I built here is pure signaling, not gating. I never made it block scoring, block promotion, or block anything else. It watches and alerts, full stop. A more honest name would put "signal" where "gate" currently sits, and I'd reserve the word "gate" for something that actually stops a decision when the reading crosses threshold. If you want drift detection to actually change what happens next, you need to wire the alert into something downstream that acts on it. PSI by itself just tells you something moved.

There's a second, quieter distinction buried in how I run this check that took me longer to notice than it should have. My daily comparison runs against the training-set baseline every single day. That's technically closer to what some vendor documentation calls a skew check, comparing serving against training, than to a drift check in the strict sense, comparing production against an earlier production window. The two catch different failures. A skew check catches a pipeline-code divergence that exists from the moment of deployment and never changes, a categorical encoding difference, a units mismatch, something wrong from day one. A drift-over-time check catches an environmental change that develops after deployment, which is what I actually cared about in this churn scenario. I'm using training-baseline comparison to catch a drift-shaped problem, and that works here, but it's worth knowing which question you're actually asking before you assume the answer generalizes to a different failure mode.

The threshold convention itself isn't universal, and I'd flag that plainly rather than treat 0.2 and 0.3 as gospel. Evidently's own convention runs 0.1 and 0.2 rather than the 0.2 and 0.3 I use. Neither is wrong, they're calibration choices, and the right threshold depends on how sensitive your specific feature is and how expensive a false alarm is to chase down. Tune it too loose and you miss the shift for weeks. Tune it too tight and you spend your week chasing alerts that resolve to nothing, which is its own kind of expensive, because the next real alert gets the same skeptical shrug the last ten false ones did.