\"Anomaly\" covers three distinct problems, and a detector built for one is close to
Scanned 9/10/2026
Install to Claude Code
npx -y skills add snoodleboot-io/prompticorn --skill anomaly-detection-techniques --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Anomaly Detection Techniques?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/snoodleboot-io-anomaly-detection-techniques)More formats (shields.io, HTML) on the badges page.
---
name: anomaly-detection-techniques
description: "\"Anomaly\" covers three distinct problems, and a detector built for one is close to"
---
# Anomaly Detection Techniques (Verbose)
## Core Patterns
### Naming the Anomaly Before Choosing a Method
"Anomaly" covers three distinct problems, and a detector built for one is close to
useless on the others.
| Type | Definition | Example | Approach |
|---|---|---|---|
| Point | A single observation is extreme | $40,000 charge on a $50/mo card | Robust z, Isolation Forest |
| Contextual | Normal in general, wrong here | 500 req/s at 4am Sunday | Deseasonalize, then score residual |
| Collective | Each point fine, the sequence is not | 200 logins, each valid, in 60s | Windowed aggregates, sequence models |
Most production incidents are contextual or collective. Teams reach for a point
detector, get poor recall, and conclude anomaly detection does not work — when the
actual failure was scoring raw values instead of residuals against an expectation.
### Robust Univariate Screening
```python
import numpy as np
def robust_z(x: np.ndarray) -> np.ndarray:
med = np.median(x)
mad = np.median(np.abs(x - med))
if mad == 0: # >50% identical values
mad = np.mean(np.abs(x - med)) or 1e-9
return 0.6745 * (x - med) / mad
flags = np.abs(robust_z(x)) > 3.5
```
The classic 3-sigma rule fails through **masking**: the outliers you are hunting
sit inside the sample used to compute σ, so a few extreme points inflate σ enough
to bring themselves back inside the threshold. Ten large outliers in a thousand
points can double σ and hide each other. Median and MAD have a 50% breakdown
point — half the data can be arbitrary before the estimate moves.
The `0.6745` constant is `Φ⁻¹(0.75)`, which makes MAD a consistent estimator of σ
under normality, so the familiar 3-ish cutoff still means roughly what you expect.
The conventional threshold is 3.5.
Heavy-tailed data (latency, revenue, file sizes) needs a log transform first, or
even the robust threshold will flag the natural right tail every time.
### Multivariate Detection
```python
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import LocalOutlierFactor
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
iso = make_pipeline(
StandardScaler(),
IsolationForest(n_estimators=300, max_samples=256,
contamination="auto", random_state=0),
)
iso.fit(X_clean) # fit on a known-good window
scores = -iso[-1].score_samples(iso[:-1].transform(X_new)) # higher = anomalous
lof = LocalOutlierFactor(n_neighbors=20, novelty=True).fit(X_clean)
lof_scores = -lof.score_samples(X_new)
```
Isolation Forest builds random trees and scores by average path length —
anomalies sit in sparse regions and get isolated in few splits. `max_samples=256`
is the paper's default and is deliberately small; subsampling reduces *swamping*,
where dense normal clusters make genuinely isolated points look ordinary.
Local Outlier Factor compares a point's local density to its neighbors'. This is
the one that catches an anomaly sitting *between* two dense clusters — globally
unremarkable, locally in a void. It requires `novelty=True` at construction to be
usable on unseen data; the default object only exposes `fit_predict` on the
training set.
Scaling matters more than method choice for distance- and density-based
detectors: an unscaled feature measured in bytes dominates every distance
computation. Fit the scaler on clean data only — one fitted on contaminated data
has its variance inflated by the anomalies, which shrinks them toward the center.
### Time Series Anomalies
```python
from statsmodels.tsa.seasonal import STL
stl = STL(series, period=24 * 7, robust=True).fit()
resid = stl.resid
score = np.abs(robust_z(resid.values))
```
Score the residual, never the level. `robust=True` uses iteratively reweighted
fitting so a spike does not get absorbed into the seasonal or trend component —
without it, a large anomaly partly explains itself away and its residual shrinks.
For a forecast-based detector the prediction interval *is* the threshold: flag
observations outside it, and let the interval widen where the model is genuinely
uncertain instead of using one global cutoff. Multiple seasonalities (daily *and*
weekly) need MSTL or explicit Fourier terms; a single `period` leaves the other
cycle in the residual and produces a recurring wave of false positives.
See time-series-preprocessing for the causality rules — a detector whose expected
value was fitted using future points looks excellent offline and detects nothing
live.
### Evaluation
```python
from sklearn.metrics import average_precision_score, precision_recall_curve
ap = average_precision_score(y_true, scores) # PR-AUC
order = np.argsort(-scores)
for k in (10, 50, 100, 500):
print(k, y_true[order[:k]].mean()) # precision@k
```
| Metric | At 0.1% base rate | Verdict |
|---|---|---|
| Accuracy | 99.9% for "always normal" | Meaningless |
| ROC-AUC | 0.95 while 95% of alerts are false | Misleading |
| PR-AUC (AP) | Tracks true/false alert ratio | Use it |
| Precision@k | Directly the operator's experience | Use it |
| Recall@fixed-alert-budget | Coverage at sustainable load | Use it |
ROC-AUC is the trap. Its x-axis is false positive *rate*, normalized by the huge
negative class, so 10,000 false positives out of 10 million negatives moves it by
0.001 — while making the alert queue 10,000 items long. PR-AUC and precision@k use
raw false positive counts and reflect what the on-call engineer actually sees.
Set `k` from the triage budget: if the team can investigate 40 alerts a day, the
only question is how many of the top 40 are real. For streaming systems, detection
latency belongs beside precision — a detector that finds an outage with 100%
precision six hours later has not detected it.
### Thresholding and Operations
`contamination` in scikit-learn sets `offset_` so that the given fraction of
training data is labeled anomalous. It does not affect the score *ranking* — so
persist the raw scores and threshold them yourself against a labeled or
operator-reviewed sample. Setting `contamination` blindly to 0.01 on data whose
true rate is 0.0001 guarantees a 100:1 false positive ratio.
Feed dismissals back. A detector without a feedback loop degrades silently: data
drifts, the score distribution shifts, and the threshold calibrated in March is
mute or screaming by September. Recalibrate on a rolling quantile of recent
scores, and exclude confirmed anomalies from the refit window.
## Common Anti-Patterns
❌ **Reporting accuracy.** At a 0.1% base rate, a detector that does nothing scores
99.9%.
✅ Precision@k and PR-AUC, with `k` set by triage capacity.
❌ **Reporting ROC-AUC as the headline.** Stays near 0.95 while nearly every alert
is false.
✅ Average precision, plus the raw confusion counts at the operating threshold.
❌ **Mean ± 3σ on skewed or contaminated data.** The outliers inflate σ and mask
themselves.
✅ Median/MAD, and log-transform heavy-tailed quantities first.
❌ **Scoring raw values on a seasonal series.** Every quiet night is an anomaly.
✅ Decompose or forecast, then score the residual.
❌ **Refitting nightly on data containing yesterday's undetected anomalies.** The
model learns them as normal and stops flagging them.
✅ Fit on a curated clean window; exclude confirmed incidents from refits.
❌ **Fitting the scaler on contaminated data.** Anomalies inflate the variance and
get pulled toward the center.
✅ Fit scaler and detector together on the clean reference period.
❌ **Trusting `contamination` to set the threshold.** It is a guess about the base
rate, not a calibration.
✅ Keep raw scores; calibrate the cutoff against reviewed samples and alert budget.
❌ **A fixed threshold that never moves.** Drift makes it mute or deafening.
✅ Rolling recalibration with alert-rate monitoring.
❌ **No feedback capture.** You never learn precision, so you cannot improve it.
✅ Record every alert's disposition; it becomes the labeled set you lacked.
## Anomaly Detection Checklist
- [ ] Anomaly type identified: point, contextual, or collective
- [ ] Seasonality and trend removed before scoring time series
- [ ] Robust statistics (median/MAD) used instead of mean/σ
- [ ] Heavy-tailed features log-transformed before thresholding
- [ ] Features scaled, with the scaler fitted on clean data only
- [ ] Multivariate method used when anomalies are combinations, not extremes
- [ ] Precision@k and PR-AUC reported; accuracy and ROC-AUC not headlined
- [ ] `k` set from actual triage capacity
- [ ] Detection latency measured for streaming detectors
- [ ] Threshold calibrated against reviewed samples, not `contamination`
- [ ] Confirmed anomalies excluded from the refit window
- [ ] Alert disposition captured as feedback and as future labels
- [ ] Score distribution monitored for drift
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!