DBSCAN - Density-Based Clustering
Why DBSCAN is useful
K-Means assumes spherical clusters and forces every point into a cluster, even the ones that don’t really belong anywhere. DBSCAN (“Density-Based Spatial Clustering of Applications with Noise”) takes the opposite approach: it defines clusters as continuous regions of high density, so it can:
- find clusters of any shape (crescents, spirals, whatever the data actually looks like)
- mark points that don’t belong to any dense region as noise instead of forcing them into the nearest cluster
Key parameters
epseps: the radius of a point’s neighborhoodmin_samplesmin_samples: how many neighbors (including itself) a point needs withinepsepsto count as a dense region
Intuition
For every instance, DBSCAN counts how many other instances fall within epseps of it —
that’s its ε-neighborhood. A point is then:
- a core point if its ε-neighborhood contains at least
min_samplesmin_samplesinstances - a border point if it’s in the neighborhood of a core point, but isn’t dense enough to be a core point itself
- noise if it’s neither a core point nor reachable from one
All instances in a core point’s neighborhood join the same cluster, including other core points — a long chain of neighboring core points forms a single cluster, however oddly it’s shaped.
flowchart LR
P["Point"] --> Q{"Enough neighbors in eps?"}
Q -->|yes| C["Core point"]
Q -->|no| R{"Reachable from a core point?"}
R -->|yes| B["Border point"]
R -->|no| N["Noise"]
Scikit-learn example
import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
X, _ = make_moons(n_samples=300, noise=0.05, random_state=42)
tight = DBSCAN(eps=0.05, min_samples=5).fit(X)
loose = DBSCAN(eps=0.2, min_samples=5).fit(X)
for name, model in [("eps=0.05", tight), ("eps=0.2 ", loose)]:
n_clusters = len(set(model.labels_)) - (1 if -1 in model.labels_ else 0)
n_noise = int((model.labels_ == -1).sum())
print(f"{name} -> clusters: {n_clusters}, noise points: {n_noise}")import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
X, _ = make_moons(n_samples=300, noise=0.05, random_state=42)
tight = DBSCAN(eps=0.05, min_samples=5).fit(X)
loose = DBSCAN(eps=0.2, min_samples=5).fit(X)
for name, model in [("eps=0.05", tight), ("eps=0.2 ", loose)]:
n_clusters = len(set(model.labels_)) - (1 if -1 in model.labels_ else 0)
n_noise = int((model.labels_ == -1).sum())
print(f"{name} -> clusters: {n_clusters}, noise points: {n_noise}")eps=0.05 -> clusters: 7, noise points: 260
eps=0.2 -> clusters: 2, noise points: 0eps=0.05 -> clusters: 7, noise points: 260
eps=0.2 -> clusters: 2, noise points: 0Widening epseps from 0.050.05 to 0.20.2 merges the fragments into exactly the two moon
shapes the data was generated from — matching what Géron found in the book.
Predicting on new points
DBSCANDBSCAN only exposes fit_predict()fit_predict(). To classify a brand-new point, train a
KNeighborsClassifierKNeighborsClassifier on the core samples — this is the same trick the book uses:
import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
from sklearn.neighbors import KNeighborsClassifier
X, _ = make_moons(n_samples=300, noise=0.05, random_state=42)
dbscan = DBSCAN(eps=0.2, min_samples=5).fit(X)
knn = KNeighborsClassifier(n_neighbors=20)
knn.fit(dbscan.components_, dbscan.labels_[dbscan.core_sample_indices_])
X_new = np.array([[-0.5, 0.4], [0, 0.6], [1.2, -0.3], [2.5, 1.0]])
print("Predicted clusters for new points:", knn.predict(X_new))import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
from sklearn.neighbors import KNeighborsClassifier
X, _ = make_moons(n_samples=300, noise=0.05, random_state=42)
dbscan = DBSCAN(eps=0.2, min_samples=5).fit(X)
knn = KNeighborsClassifier(n_neighbors=20)
knn.fit(dbscan.components_, dbscan.labels_[dbscan.core_sample_indices_])
X_new = np.array([[-0.5, 0.4], [0, 0.6], [1.2, -0.3], [2.5, 1.0]])
print("Predicted clusters for new points:", knn.predict(X_new))Predicted clusters for new points: [1 0 0 0]Predicted clusters for new points: [1 0 0 0]Pros and cons
Pros:
- finds clusters of any shape
- detects outliers naturally, instead of forcing every point into a cluster
- just two hyperparameters (
epseps,min_samplesmin_samples) - roughly
O(m log m)O(m log m)— close to linear in the number of instances
Cons:
- choosing
epsepscan be tricky and the result is sensitive to it - struggles when clusters have very different densities — a single
epsepscan’t be “tight” for one cluster and “loose” for another - scikit-learn’s implementation can need up to
O(m²)O(m²)memory ifepsepsis large
Mini-checkpoint
If you increase epseps:
- do you get more or fewer clusters?
(Usually fewer — clusters merge, and noise points get absorbed into them.)
Visualize it
Two crescent-shaped clusters plus scattered noise. The neighborhood radius (epseps)
pulses larger and smaller: watch points flip between core (colored, dense enough)
and noise (grey) as the radius changes — this is exactly why epseps is the
parameter to tune first:
🧪 Try It Yourself
Exercise 1 – Cluster Two Moons
Exercise 2 – A Tiny eps Creates Noise
Exercise 3 – Bigger eps Merges Clusters
Next
Continue to Anomaly Detection with Isolation Forests — another way to flag rare points, this time by how easy they are to isolate rather than how dense their neighborhood is.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
