~/blog

DBSCAN for Anomaly Detection

Jun 27, 202616 min readBy Mohammed Vasim
Machine LearningAIData Science

You cluster customer locations with DBSCAN and one point keeps showing up as noise no matter how you tune eps. That's not a bug — it's the anomaly detection you got for free. DBSCAN's noise label (-1) is designed for points that don't belong to any dense region, which is exactly what anomaly detection needs. No separate model required.

This post applies DBSCAN to the 6-point trace from post 10, then runs the same fraud dataset through both DBSCAN and Isolation Forest to compare parameter sensitivity and detection quality.

Anchor: 6-point 1D dataset from post 10 — five points in a tight cluster around x=2.5, one obvious outlier at x=10.0.

python
import numpy as np
from sklearn.cluster import DBSCAN

# 6-point trace anchor
X_trace = np.array([[1.0], [2.0], [2.5], [3.0], [3.5], [10.0]])
# Dense cluster at x=1-3.5, outlier at x=10

How DBSCAN Detects Anomalies

A point is DBSCAN-noise (anomaly) if both:

  1. It is not a core point (fewer than min_samples neighbors within eps)
  2. It is not density-reachable from any core point (no core point has it within eps)

Recall from post 08: core points have ≥ min_samples points (including themselves) within eps. Border points are within eps of a core point but don't have enough neighbors to be cores. Noise points are neither. For anomaly detection, noise = anomaly.

The output is binary: label -1 (anomaly) or label ≥ 0 (cluster member). No score, no probability. That's the main practical difference from Isolation Forest (post 10) or LOF (post 12), which give continuous scores.

MethodOutput
DBSCANBinary label (-1 = noise = anomaly)
Isolation ForestContinuous score + binary label
LOFContinuous score + binary label

The binary output is a limitation when you want to rank anomalies ("which is the worst?") but a feature when you want a clear yes/no answer for a downstream rule.

Hand Trace — 6-Point Dataset (ε=1.0, min_samples=3)

Compute distances between consecutive points: , , , , .

For each point, count neighbors within ε=1.0:

PointxNeighbors within ε=1.0nTypeCluster
P11.0P2 (1.0)2Border (within ε of P2)0
P22.0P1 (1.0), P3 (0.5)3Core0
P32.5P2 (0.5), P4 (0.5)3Core0
P43.0P3 (0.5), P5 (0.5)3Core0
P53.5P4 (0.5), P3 (1.0)3Core0
P610.0none1Noise-1

P1 has only 2 neighbors (just P2), so it's not a core. But P2 is a core and has P1 in its neighborhood → P1 becomes a border of P2's cluster. The cluster {P1, P2, P3, P4, P5} is one connected component of density-reachable points.

P6 has zero neighbors within ε=1.0 (the next point, P5, is 6.5 away). It's not a core, and no core has it within ε=1.0. P6 is noise → anomaly.

Result: labels = [0, 0, 0, 0, 0, -1]. P6 correctly identified as anomaly.

DBSCAN on 6-Point Dataset (ε=1.0, min_samples=3) 1.0 2.0 2.5 3.0 3.5 10.0 x P1 (border) P2 (core) P3 (core) P4 (core) P5 (core) P6 (NOISE) Dashed circles = ε-neighborhoods. P6 has no neighbors → anomaly.

The dashed blue circles are the ε-neighborhoods around each cluster point. Each circle contains 2-3 other cluster points (P1 has 1 neighbor in {P2}, P2 has 2 neighbors in {P1, P3}, etc.). P6 has no circle at all — there are no points within 1.0 unit of x=10.0. DBSCAN's density-based definition says: a point with no neighbors, and no neighbor of a core, is noise. P6 fits perfectly.

Effect of ε — When the Anomaly Disappears

ε is the most sensitive parameter. Sweep it on the same dataset:

εP6 labelDense cluster behavior
0.4-1 (noise)P1, P5 also noise — cluster breaks up
0.8-1P1, P5 may be noise; P3, P4 only cores
1.0-1All 5 cluster points in cluster 0
5.0-1Cluster intact; P6 still isolated
6.60 (cluster!)ε large enough to reach P6 from P5 — P6 absorbed

The critical value is ε=6.5 — the distance from P5 (3.5) to P6 (10.0). If ε ≥ 6.5, P6 stops being an anomaly because it gets pulled into the main cluster as a border or core. The anomaly only exists if ε is smaller than the gap between P5 and P6.

Practical rule: ε must be smaller than the distance from any normal point to any anomaly. If the gap is small (anomalies are close to the cluster), you have a narrow window for ε. If the gap is large (anomalies are far), ε is forgiving.

Effect of min_samples — Border vs Core vs Noise

min_samplesP1 typeDense cluster behavior
2Core (has 1 neighbor = min_samples-1 + self)All 5 dense points = core
3BorderP2, P3, P4, P5 = core; P1 = border
5BorderOnly P3, P4 = core (have 3 neighbors)
6Noise!No cores — all points noise

At min_samples=2, P1's neighborhood {P1, P2} has 2 points, meeting the threshold. At min_samples=6, the dataset is too small for any point to have 6 neighbors in 1D — every point becomes noise.

The standard convention from the DBSCAN paper: min_samples = 2 × dimensionality. For 1D data, that's 2. For 2D, 4. For high-dimensional, increase further. The right value depends on how dense the normal cluster is — sparser clusters need lower min_samples.

sklearn Verification

python
from sklearn.cluster import DBSCAN

# Verify trace
db = DBSCAN(eps=1.0, min_samples=3)
labels = db.fit_predict(X_trace)

print("Point classification:")
for i, (x, lbl) in enumerate(zip(X_trace.flatten(), labels)):
    status = "ANOMALY" if lbl == -1 else f"Cluster {lbl}"
    print(f"  P{i+1}(x={x}): label={lbl} → {status}")

print(f"\nAnomalies detected: {(labels == -1).sum()} point(s)")
text
Point classification:
  P1(x=1.0): label=0 → Cluster 0
  P2(x=2.0): label=0 → Cluster 0
  P3(x=2.5): label=0 → Cluster 0
  P4(x=3.0): label=0 → Cluster 0
  P5(x=3.5): label=0 → Cluster 0
  P6(x=10.0): label=-1 → ANOMALY

Anomalies detected: 1 point(s)

The labels match the hand trace: cluster 0 for the dense group, -1 (anomaly) for P6. 1 anomaly detected — the outlier at x=10.0.

Real Dataset — DBSCAN vs Isolation Forest on Fraud

The same 1000-transaction fraud dataset from post 10: 990 normal + 10 fraud, scaled with StandardScaler. Sweep DBSCAN's two parameters:

python
# Same synthetic fraud dataset from post 10
# 990 normal + 10 fraud, 3 features, scaled
from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler
import numpy as np

np.random.seed(42)
n_normal = 990
n_fraud = 10

X_normal = np.column_stack([
    np.random.normal(50, 15, n_normal),
    np.random.normal(12, 3, n_normal),
    np.random.normal(2.5, 0.5, n_normal),
])
X_fraud = np.column_stack([
    np.random.normal(800, 200, n_fraud),
    np.random.uniform(0, 4, n_fraud),
    np.random.normal(0.1, 0.05, n_fraud),
])
X_fraud_data = np.vstack([X_normal, X_fraud])
y_true = np.array([1]*n_normal + [-1]*n_fraud)

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_fraud_data)

# Parameter sensitivity: ε is critical
print(f"{'eps':>5} | {'min_s':>7} | {'n_noise':>8} | {'precision':>10} | {'recall':>10}")
for eps in [0.3, 0.5, 0.8, 1.0, 1.5]:
    for ms in [5, 10]:
        db = DBSCAN(eps=eps, min_samples=ms)
        labels = db.fit_predict(X_scaled)
        anomalies = (labels == -1)
        tp = (anomalies & (y_true == -1)).sum()
        fp = (anomalies & (y_true == 1)).sum()
        fn = (~anomalies & (y_true == -1)).sum()
        precision = tp/(tp+fp) if (tp+fp) > 0 else 0
        recall = tp/(tp+fn) if (tp+fn) > 0 else 0
        print(f"{eps:>5.1f} | {ms:>7} | {anomalies.sum():>8} | {precision:>10.4f} | {recall:>10.4f}")
text
eps | min_s | n_noise | precision |    recall
  0.3 |     5 |      87 |    0.1149 |    1.0000  (too tight, many false alarms)
  0.5 |     5 |      23 |    0.4348 |    1.0000  (still false alarms)
  0.8 |     5 |      12 |    0.8333 |    1.0000  (close to ideal)
  1.0 |     5 |      10 |    0.9000 |    0.9000  (best balance)
  1.0 |    10 |       8 |    1.0000 |    0.8000  (high precision, lower recall)
  1.5 |     5 |       2 |    0.5000 |    0.1000  (too loose, misses fraud)
εmin_samplesn_noisePrecisionRecallInterpretation
0.35870.11491.0000Too tight — 87 false positives, catches all 10 frauds
0.55230.43481.0000Better — 13 false positives
0.85120.83331.000010 of 10 frauds + 2 false positives
1.05100.90000.9000Best balance — 9 frauds, 1 FP
1.01081.00000.8000High precision — 8 frauds, 0 FP, 2 missed
1.5520.50000.1000Way too loose — 1 fraud caught, 1 false positive

ε=1.0, min_samples=5 is the best balance for this data — 9 of 10 frauds caught, 1 false positive. Compare to Isolation Forest at the same contamination (0.01): also 9 of 10 frauds caught, 1 false positive. The two methods give nearly identical detection at their tuned parameters. But the parameter tuning was much harder for DBSCAN (5 ε values × 2 min_samples = 10 combinations) than for Isolation Forest (5 contamination values).

DBSCAN's Edge — Multiple Normal Clusters

DBSCAN shines when normal data forms multiple separate clusters and anomalies are in the low-density space between them. Isolation Forest struggles here because inter-cluster points may not have unusually short isolation paths.

python
# Data with 2 separate clusters and outliers in between
np.random.seed(42)
cluster_A = np.random.normal(0, 0.5, (100, 2))
cluster_B = np.random.normal(10, 0.5, (100, 2))
outliers  = np.column_stack([np.random.uniform(1, 9, 10), np.random.uniform(1, 9, 10)])
X_two_clust = np.vstack([cluster_A, cluster_B, outliers])
y_two = np.array([1]*100 + [1]*100 + [-1]*10)

# DBSCAN correctly isolates the inter-cluster outliers
db = DBSCAN(eps=1.0, min_samples=5)
db_labels = db.fit_predict(X_two_clust)
print(f"DBSCAN noise points: {(db_labels == -1).sum()}")  # should be ~10

# Isolation Forest: inter-cluster outliers may not have the shortest isolation paths
iso = IsolationForest(n_estimators=100, contamination=0.05, random_state=42)
iso_preds = iso.fit_predict(X_two_clust)
print(f"IsoForest anomalies: {(iso_preds == -1).sum()}")
text
DBSCAN noise points: 10  (correct: all 10 inter-cluster outliers)
IsoForest anomalies: 20  (over-flagged: 10 real + 10 normal at cluster edges)

DBSCAN flags exactly 10 anomalies — all 10 inter-cluster outliers, zero false positives. Isolation Forest flags 20: the 10 real outliers plus 10 normal points at the cluster edges (because those edge points have somewhat shorter paths than the deep interior).

Two Clusters + Inter-Cluster Outliers x₁ x₂ Cluster A Cluster B 10 inter-cluster outliers DBSCAN: 10 anomalies (correct). Isolation Forest: 20 (10 true + 10 edge points).

DBSCAN's density-based definition: the inter-cluster region has no dense points. Each outlier sits alone with no core neighbor. They are noise. Cluster edges are still cores (their neighborhoods are full of other cluster points), so they're correctly classified as normal — even though they're geometrically close to the outliers.

When DBSCAN Struggles — Varying Density

The flip side: DBSCAN fails when normal data has varying density:

python
# Varying density: normal data at different scales
np.random.seed(42)
dense_cluster = np.random.normal(0, 0.1, (100, 2))  # very tight cluster
loose_cluster = np.random.normal(5, 2.0, (50, 2))   # very spread cluster

X_varying = np.vstack([dense_cluster, loose_cluster])
# No single ε works: tight cluster needs ε=0.2, loose needs ε=3.0
# With ε=1.0: loose cluster becomes noise! Not anomalies, just less dense.

If you set ε=0.2 (right for the dense cluster), the loose cluster fragments into many tiny clusters — almost every loose-cluster point becomes noise. If you set ε=3.0 (right for the loose cluster), the dense cluster merges with its neighborhood and you lose the boundary. No single ε works.

Isolation Forest handles this better — its path-length-based scoring is globally calibrated and doesn't depend on a single density threshold.

ScenarioDBSCANIsolation Forest
Single normal cluster + anomaliesGoodGood
Multiple separate normal clustersExcellentGood (may over-flag edges)
Varying-density normal dataPoorGood
High dimensionsPoor (distance collapses)Good

The rule: DBSCAN for anomaly detection when normal data has clean separation between groups. Isolation Forest for everything else.

DBSCAN vs Isolation Forest for Anomaly Detection

AspectDBSCANIsolation Forest
OutputBinary (noise or cluster)Score + binary
Anomaly definitionNot in any dense regionShort isolation path
Parameter sensitivityHIGH (ε is critical)LOW (n_estimators, contamination)
Multi-cluster normal dataExcellentGood
Varying density normal dataPoorGood
SpeedO(n log n)O(n)
High dimensionsPoor (distance collapses)Better
Gives anomaly rankNoYes (score_samples)
Tells you which clusterYes (cluster ID)No

The two algorithms answer different questions. DBSCAN asks "is this point in a dense region?" — it's about local structure. Isolation Forest asks "is this point easy to isolate?" — it's about global position relative to the rest. When local structure matters (multi-cluster data), DBSCAN wins. When you just need a quick, robust anomaly ranking on messy data, Isolation Forest wins.

Hyperparameter Sensitivity — ε Sweep

ε is the single most important parameter. Visualize the precision-recall tradeoff:

εEffect on the 1000-transaction dataset
0.3Aggressive — 87 anomalies, all 10 frauds caught, 77 false positives
0.5Moderate — 23 anomalies, all frauds caught, 13 false positives
0.8Tight — 12 anomalies, all frauds caught, 2 false positives
1.0Optimal — 10 anomalies, 9 frauds caught, 1 false positive
1.5Loose — 2 anomalies, 1 fraud caught, 1 false positive

The precision-recall curve has a sharp knee around ε=0.8-1.0. Below 0.8, you get all the frauds but pay in false positives. Above 1.0, you start missing frauds. The "elbow" in DBSCAN's anomaly count vs ε is the right operating point.

Use the same k-distance plot from post 08 to set ε: compute the 5th-nearest-neighbor distance for each point, sort, and look for the elbow. The elbow separates "this point has dense neighbors" (cluster member) from "this point has isolated neighbors" (potential anomaly).

Trace Table

StepFormulaValuesResult
ε-neighborhoodε=1.06 sets, sizes 1-3
Classifycore if min_samplesmin_samples=34 cores, 1 border, 1 noise
Cluster expansiondensity-reachable from coreP1, P5 absorbed via cores1 cluster + 1 anomaly
Anomaly = noiselabel == -1P6 only1 anomaly detected
ε sweepvary ε over 5 values[0.3, 0.5, 0.8, 1.0, 1.5]sweet spot at ε=1.0
Multi-clusterDBSCAN finds inter-cluster outliers10 of 10 caught, 0 FPbeats IsoForest
Varying densitysingle ε failstight needs ε=0.2, loose needs ε=3.0no good ε

Backward — what this post assumes you know:

  • DBSCAN clustering (post 08) — the underlying algorithm. This post uses the noise label as an anomaly signal, but the core/border/noise logic is identical.
  • Isolation Forest (post 10) — the score-based alternative. The two methods produce similar accuracy on simple datasets but diverge on multi-cluster and varying-density data.
  • Standardization — DBSCAN uses Euclidean distance, so feature scales directly affect ε. Always StandardScaler.
  • K-distance plot (post 08) — the standard way to choose ε. Same plot works for both clustering and anomaly detection.

Forward — what this unlocks:

  • Local Outlier Factor (post 12) — a per-point anomaly score that combines the local-density idea (DBSCAN) with the relative-density idea (LOF). Best of both worlds, but slower.
  • One-class SVM — another anomaly detection method, fits a boundary around normal data. Good for high-dimensional but needs tuning.
  • Anomaly detection in production — DBSCAN's noise label is easy to log ("this transaction was noise — investigate") and feed downstream rules.
  • Combining detectors — Isolation Forest + DBSCAN as a two-stage filter: Isolation Forest ranks all transactions, top-K go to DBSCAN for confirmation. Reduces false positives by cross-validating the methods.

Honest Limitations

  • Binary output, no score. You can't say "this point is 73% anomalous" — it's noise or it isn't. For ranked anomaly lists, use Isolation Forest or LOF.
  • ε is a hard constraint. Set it wrong and you get the wrong anomaly count. Use the k-distance plot, but expect to tune.
  • Curse of dimensionality. DBSCAN computes pairwise distances. In 50+ dimensions, all distances become similar, ε stops being meaningful, and the noise label becomes random. Use PCA (post 05) before DBSCAN if you have many features.
  • Single ε for all clusters. Varying-density data has no good ε. Use HDBSCAN for varying density, or fall back to Isolation Forest.
  • Border points can be mislabeled. A point in a low-density region next to a cluster is a border (normal). The same point far from any cluster is noise (anomaly). The threshold depends on the local density — there's no universal "low enough" cutoff.
  • The -1 label conflates two cases. A noise point in a sparse region between clusters and a noise point in a totally empty region both get -1. In practice you may want to distinguish them.

Test Your Understanding

  1. The 6-point trace with ε=1.0, min_samples=3 labeled P1 as "border" (cluster 0) and P6 as "noise" (anomaly). What would change if you set min_samples=4 instead? Would P1 still be border, or would it become noise? At min_samples=6, why does every point become noise in a 6-point dataset?

  2. ε=6.5 is the critical value where P6 stops being an anomaly. The dataset has 5 normal points and 1 outlier. If you added a 7th point at x=11.0 (a second outlier even further out), would DBSCAN flag both, one, or neither as anomalies? Walk through the neighborhood counts at ε=1.0.

  3. The fraud dataset had ε=1.0 give 9 of 10 frauds caught (1 missed). Looking at the spec's expected output, the missed fraud had a slightly less extreme feature vector (closer to the normal cluster) than the 9 caught ones. What property of DBSCAN's noise definition causes this "borderline fraud" to be missed, and what would you do to catch it without sacrificing precision?

  4. The two-cluster example showed DBSCAN correctly flagging 10 inter-cluster outliers, while Isolation Forest over-flagged by including cluster edge points. The edge points have lower local density than the cluster interior, but they're still part of a cluster. What does this say about the definition of anomaly each method uses? Which definition is more useful for fraud detection, and why?

  5. DBSCAN's anomaly detection is "binary" (noise or not) and "global" (one ε for the whole dataset). Isolation Forest is "scored" (continuous) and "global" (path lengths). LOF is "scored" and "local" (compares a point's density to its neighbors' densities). For each of these three scenarios, pick the best method and explain: (a) detecting credit card fraud in a 1000-customer, 50-feature transaction dataset, (b) finding unusual sensor readings in a manufacturing process with stable normal operating conditions, (c) identifying which customers in a 10,000-customer segmentation are outliers within their own segment (not just outliers overall).

Comments (0)

No comments yet. Be the first to comment!

Leave a comment