Anomaly Detection

Anomaly detection is about finding the rare points that do not belong, usually with few or no labels. This page surveys the main method families, explains Isolation Forest and LOF, compares detectors in code, and covers what it takes to run detection in production.


1. What Counts as an Anomaly

Three Kinds of Anomalies Point Contextual Collective normal value, wrong time flat run where noise is expected far from everything

Anomaly detection (outlier or novelty detection) finds observations that do not fit the pattern of the rest: fraudulent transactions, failing machines, intrusions, data-quality bugs. Labels are usually scarce and anomalies are rare and varied, so most methods are unsupervised or semi-supervised (trained on data assumed to be normal) and output a score rather than a class. Two settings are worth separating: outlier detection (training data is contaminated, find the odd ones) and novelty detection (training data is clean, flag new points that differ).

2. Families of Methods

FamilyIdeaExamples
StatisticalLow probability under a fitted distributionz-score, robust MAD, Gaussian / GMM likelihood, Mahalanobis distance
Distance / densityFar from neighbors or in sparse regionsk-NN distance, Local Outlier Factor (LOF), DBSCAN noise
IsolationEasy to separate with random splitsIsolation Forest, Extended IF
BoundaryLearn a tight region around normal dataOne-Class SVM, Deep SVDD
ReconstructionLarge error when compressed and rebuiltPCA residual, autoencoders
Forecast residualLarge deviation from a predictionSeasonal decomposition, ARIMA / Prophet residuals

LOF compares a point's local density to that of its neighbors: a score near 1 is normal, well above 1 means the point is sparser than its neighborhood. That makes it work when clusters have different densities, where a global distance threshold fails.

3. Isolation Forest

Isolation Forest: Outliers Isolate in Fewer Splits 2 random cuts isolate the outlier inlier needs many cuts

Isolation Forest (Liu, Ting and Zhou 2008) builds many random trees, each splitting on a random feature at a random value between its min and max. Anomalies are few and different, so they end up isolated in a leaf after few splits. The score is based on the average path length E[h(x)] normalized by the expected path length of an unsuccessful binary-search-tree lookup, c(n): s(x) = 2-E[h(x)] / c(n). Scores near 1 are anomalous, well below 0.5 normal. It trains on small subsamples (256 by default), is linear in data size, needs no distance computations, and is the usual first choice on tabular data.

4. Example: Comparing Detectors

import numpy as np
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import LocalOutlierFactor
from sklearn.svm import OneClassSVM
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import roc_auc_score, average_precision_score

rng = np.random.default_rng(0)
normal = rng.normal(0, 1, size=(2000, 6))
anom = rng.uniform(-6, 6, size=(40, 6))
X = np.vstack([normal, anom]); y = np.r_[np.zeros(2000), np.ones(40)]
X = StandardScaler().fit_transform(X)

models = {
    "iforest": IsolationForest(n_estimators=300, contamination="auto", random_state=0),
    "lof":     LocalOutlierFactor(n_neighbors=35, novelty=False),
    "ocsvm":   OneClassSVM(nu=0.02, gamma="scale"),
}
for name, m in models.items():
    if name == "lof":
        m.fit_predict(X); score = -m.negative_outlier_factor_
    else:
        m.fit(X); score = -m.score_samples(X)          # higher = more anomalous
    print(f"{name:8s} ROC-AUC={roc_auc_score(y, score):.3f} "
          f"PR-AUC={average_precision_score(y, score):.3f}")

# Turn scores into alerts by budget, not by a magic contamination value
threshold = np.quantile(score, 0.99)                   # top 1% -> review queue

Evaluate with precision-recall AUC or precision at the top-k alerts, not accuracy: with 2% anomalies, predicting "all normal" is 98% accurate and useless.

5. Running It in Production

Production Anomaly Detection Pipeline Features windows, ratios Scaler fit on normal Detector IForest / LOF / AE Score threshold @ alert budget Review labels feed back analyst feedback becomes labels -> semi-supervised or supervised model

6. Common Pitfalls

7. Statistical Baselines, Worked Through

Start simple, because a well-chosen statistic is often good enough and always easy to explain. Take eight readings: 10, 11, 12, 11, 10, 12, 11 and 50. The mean is 15.9 and the standard deviation 12.9, so the z-score of 50 is only (50 − 15.9) / 12.9 ≈ 2.6, below the common cut-off of 3. The outlier inflated both the mean and the spread it is measured against, an effect called masking. The robust alternative uses the median (11) and the median absolute deviation (MAD, here 1). The modified z-score 0.6745 × (50 − 11) / 1 ≈ 26 flags the point unmistakably; Iglewicz and Hoaglin suggest treating modified z-scores above 3.5 as potential outliers.

For several correlated features, the multivariate analogue is the Mahalanobis distance √((x − μ)T Σ−1 (x − μ)), which measures distance in units of the data's own covariance, so a point that is ordinary on each feature separately but has an unusual combination (tall and very light) still stands out. Estimate μ and Σ robustly, for example with the minimum covariance determinant (sklearn.covariance.EllipticEnvelope), or the anomalies will again mask themselves.

8. Local Outlier Factor in Detail

Why LOF Uses Local Density dense cluster sparse cluster p Ratio, not distance p is close in absolute terms, but far relative to its dense neighbours: LOF(p) well above 1 sparse-cluster points have large k-distances too, but so do their neighbours: LOF near 1

LOF (Breunig et al., 2000) defines, for each point, the k-distance to its k-th nearest neighbour, and a reachability distance from p to o as max(k-distance(o), d(p, o)), which smooths out tiny distances inside dense groups. The local reachability density of p is the inverse of its mean reachability distance to its k neighbours. Finally, LOF(p) is the average ratio of its neighbours' densities to its own. A point as dense as its neighbours scores about 1; a point five times sparser than its neighbourhood scores about 5. Typical n_neighbors is 20; it should exceed the size of any small group of anomalies you want detected, or such a group becomes its own normal neighbourhood. In scikit-learn, use novelty=True to score unseen data.

Isolation Forest scores can be read the same concrete way. With the default subsample of 256, the normalising constant c(256) ≈ 10.2. A point isolated after 4 splits on average scores 2−4/10.2 ≈ 0.76; one needing 10 splits scores about 0.51; one needing 12 scores about 0.44 and looks normal.

9. Choosing a Detector

DataReasonable first choiceNotes
One metric, roughly stableRobust z-score (median / MAD)Explainable; add seasonal baselines for time-of-day effects
Tabular, mixed featuresIsolation ForestFast, few assumptions; encode categoricals sensibly
Clusters of different densityLOFScale features; quadratic-ish cost on large data without indexes
Images, audio, logs as sequencesAutoencoder or pretrained-embedding + k-NN distanceTrain on normal data only for novelty detection
Metrics over timeForecast residuals (seasonal model) with robust thresholdsHandle holidays and deploys as known events
Some labeled incidentsSupervised model plus an unsupervised detectorSupervised catches known patterns, unsupervised the new ones

Ensembles of detectors, combined after converting each score to a rank or percentile, are often more robust than any single method, because different detectors fail on different anomaly types. The PyOD library provides consistent interfaces for many detectors and makes this easy to try.

10. Evaluation and Alerting

Threshold by Alert Budget Score every event gets an anomaly score Rank sort descending per time window Budget top N the team can review per day Label reviewers mark true / false Learn precision@N, retrain, tune

When labels exist, report precision-recall AUC and precision at the alert budget, and look at recall per anomaly type rather than overall. For time series, decide whether you score points or events: an incident spanning 30 minutes is one event, and detecting it at minute 2 is very different from minute 28, so report detection delay. Be cautious with "point-adjusted" scoring used in some time-series benchmarks, which counts a whole anomalous segment as detected if any single point is flagged and can make even random scores look strong.

When labels do not exist, inject synthetic anomalies with known properties (spikes, level shifts, swapped fields) to sanity-check recall, and review a sample of the top-scoring events with domain experts. Operationally, group related alerts, suppress repeats, and route each alert with the features that drove it. Track alert volume, reviewer precision and time-to-acknowledge as first-class metrics; a detector that is accurate but ignored because it is noisy has no value. Typical deployments include payment fraud, account takeover, network intrusion, manufacturing quality, predictive maintenance from vibration and temperature sensors, and data-pipeline monitoring (row counts, null rates, schema changes). See time-series forecasting for the residual-based approach.

Further reading: Chandola, Banerjee & Kumar, "Anomaly Detection: A Survey" (ACM Computing Surveys, 2009); Liu, Ting & Zhou, "Isolation Forest" (ICDM 2008); Breunig, Kriegel, Ng & Sander, "LOF: Identifying Density-Based Local Outliers" (SIGMOD 2000); Iglewicz & Hoaglin, How to Detect and Handle Outliers (1993); Ruff et al., "Deep One-Class Classification" (ICML 2018); Zhao, Nasrullah & Li, "PyOD" (JMLR, 2019).