Anomaly detection sounds fancy. In my work, 80% of it is simple stuff. A z-score, a rolling window, done.
The problem
You have a metric. Revenue, signups, errors, whatever. You want to know when something is off. Not just “number went up” — number went up more than it should have.
Method 1 — Simple z-score
Compare today to the historical mean and std:
z_score = (current_value - historical_mean) / historical_std
is_anomaly = abs(z_score) > 3
Use it for: stable metrics, no strong seasonality.
Does not handle: trends, weekly patterns.
Method 2 — Rolling stats
Let the baseline move with the data:
rolling_mean = df["metric"].rolling(window=28).mean()
rolling_std = df["metric"].rolling(window=28).std()
z_score = (df["metric"] - rolling_mean) / rolling_std
Use it for: metrics with gradual drift.
Still bad at: seasonality.
Method 3 — Decompose the series
Split into trend, seasonal, residual. Flag the residual.
from statsmodels.tsa.seasonal import seasonal_decompose
result = seasonal_decompose(df["metric"], period=7)
residuals = result.resid
# z-score on the residuals
Use it for: clear weekly or monthly patterns.
Method 4 — Prophet
Prophet gives you intervals out of the box:
from prophet import Prophet
model = Prophet(interval_width=0.99)
model.fit(df)
forecast = model.predict(df)
# Anything outside yhat_upper/yhat_lower is a candidate anomaly
Use it for: messy seasonality, holidays.
What actually matters
- Start simple. Z-scores solve more problems than people want to admit. I start there every time.
- Tune the threshold. 3-sigma is a default, not a rule. If you hate false positives, move it.
- Handle missing data. Detectors hate gaps. Fill them on purpose, not by accident.
- Alert fatigue is real. Better to miss a few than cry wolf every day. I learned this the hard way.
- Check before you alert. Most “anomalies” have boring explanations.
The meta-problem
The hard part of anomaly detection is not the math. It is knowing what counts as an anomaly you would actually act on.
Work backwards: “If we saw this alert, what would we do?” If the answer is nothing, do not build the alert.