Clusters defined by crowding rather than by distance to a centre, which is why it finds shapes k-means cannot.
Overview
What k-means cannot do
Set the data control above to Two crescents. K-means, which assigns each point to the nearest of *k* centres, can't separate these: the boundary it draws is always a straight line between two centroids, and no straight line separates two interleaved crescents.
DBSCAN separates them right away, and does it without being told there are two.
DBSCAN: Density-Based Clustering
This module needs JavaScript: the
numbers are computed in the page rather than recorded.
Worth knowing
A core point has at least minPts neighbours within eps. Clusters grow outward from core points.
Points reachable from a core point but not core themselves are border points. Everything else is noise.
You never say how many clusters to find. The density parameters decide, and the count falls out.
Because clusters spread through connected dense regions, they can be any shape at all — which is what k-means cannot do.
DBSCAN: Density-Based Clustering
Clustering by crowding rather than by distance to a centre. The two params that decide all.
The definitions
All follows from two params. eps, a radius, and minPts, a count — and three meanings:
Core point. Has at least minPts points within eps of it, including itself. It sits in a crowded place.
Border point. Within eps of a core point. But Not crowded enough to be one itself. It's on the edge of a cluster.
Noise. Neither. It belongs to no cluster, and DBSCAN says so rather than forcing it a place.
A cluster is then a maximal set of points connected through core points. Start at a core point, take all within eps. For each of those that is also a core point, take *its* zone too, repeating until nothing new arrives.
This is why clusters can be any shape. The cluster spreads wherever the data is dense, following the crescent around its curve. At no point does it ask how far any of it's from a centre.
The two parameters
Drag eps in the visualisation and watch the count.
Too small, and no point has minPts neighbours. All is noise, or the data shatters into many small clusters.
Too large, and all is within reach of all else. The two crescents merge into one cluster. This Happens just above 0.25 on this data.
The band in between is where DBSCAN works, and on the crescents it's truly narrow. That sensitivity is the honest weakness of the method.
minPts is less delicate. Larger values demand denser cores, making fewer, tighter clusters and more noise. A common starting rule is minPts = 2 * number of dims, and at least 3.
For eps, the standard technique is a k-distance plot: for each point, measure the distance to its minPts-th nearest neighbour, sort those distances and plot them. The curve has a knee where distances start rising sharply, and that knee is a reasonable eps. It's the point at which you leave the dense region and start crossing gaps.
What it gives you that k-means does not
No k. The number of clusters is found, not supplied.
Any shapes. Crescents, rings, spirals, any of it connected and dense.
An clear noise label. K-means assigns each point to some cluster, so an outlier is silently absorbed and drags a centroid with it. DBSCAN sets it aside. Switch the data to blobs and the scattered points stay hollow.
Where it fails
Varying density. This is the real limitation. One eps applies all over, so a dataset with one dense cluster and one sparse cluster can't be handled: the eps that finds the sparse one merges the dense one into its surroundings. HDBSCAN exists just for this, building a hierarchy over eps values and selecting per cluster.
High dims. Distances focus as dimensionality grows, so "zone" stops meaning much. Reduce dims first.
Scale sensitivity.eps is an abs distance. So A feature measured in thousands dominates one measured in units. Scale first — this isn't optional.
Cost. Naively O(n²) because of the zone queries. A spatial index brings it to about O(n log n) in low dims.
Core, border and noise, counted
The three meanings become concrete the moment you apply them. Take eps = 1.5, minPts = 3, and a point whose 1.5-radius zone is being checked:
point
neighbours within eps (incl. itself)
type
in a dense cluster
5
core (≥ minPts)
on a cluster edge
2, but one is a core point
border
off on its own
1
noise
A cluster is then all reachable through core points: start at a core point, absorb its zone, and for each neighbour that is *also* core, absorb its zone too, until nothing new arrives. That chain-through-cores rule is why DBSCAN follows a crescent around its curve. It never measures distance to a centre, only local crowding — and why it labels the lone point noise instead of forcing it into a cluster and dragging a centroid toward it, as k-means must. The cost is that one eps applies all over: raise it past the gap between two groups and they merge, drop it below the spacing of a sparse cluster and that whole cluster turns noise. On data with one dense and one sparse cluster no single eps works, which is just what HDBSCAN was built to fix.
Choosing eps isn't guesswork either: the standard technique is a k-distance plot, where you measure each point's distance to its minPts-th nearest neighbour, sort those distances and look for the knee. The point where they start rising sharply is where you leave the dense region and begin crossing gaps, and it makes a defensible eps. minPts is the less delicate of the two, with a common starting rule of twice the number of dims. The method's real ceiling is high dimensionality: distances focus as dims grow until "zone" stops meaning much, so reducing dims first isn't optional, and neither is scaling, since eps is a single abs distance applied to each feature at once.
Where it goes wrong with DBSCAN
Not scaling the features.eps turns meaningless.
Tuning eps by trying numbers. Use a k-distance plot. It takes a minute and gives a defensible value.
Expecting it to work on varying density. Reach for HDBSCAN instead.
Reading noise as failure. Labelling outliers as noise is the feature, not a bug to fix.
Questions people ask about DBSCAN
How do I choose eps? Plot each point's distance to its k-th nearest neighbour, sorted, and pick the value at the bend of the curve. Domain knowledge about what counts as close helps too.
How do I choose min_samples? A common start is twice the number of features, or at least 4 or 5 for 2D data. Raise it for noisier data so that small chance groupings do not become clusters.
What does a label of -1 mean? It marks noise, a point that is neither a core point nor within eps of one. DBSCAN does not force every point into a cluster.
Does DBSCAN need feature scaling? Yes. Eps is a single distance applied to all features, so a large-scale feature will dominate unless you standardise first.
Why does DBSCAN struggle with varied densities? One eps cannot fit both a tight cluster and a loose one. Either the loose cluster is split into noise or the tight clusters merge. HDBSCAN handles this better.
Is DBSCAN better than k-means? It is better for odd shapes and for flagging outliers, and you do not give it k. K-means is faster and gives every point a cluster, which suits round, similar-sized groups.
Clusters by density, and the points left over
DBSCAN finds shapes k-means can't, decides the number of clusters itself. Labels outliers as noise instead of forcing them a place.
example_01.pyscikit-learn
import numpy as np
from sklearn.cluster import DBSCAN, KMeans
from sklearn.datasets import make_moons
from sklearn.metrics import adjusted_rand_score
X, y = make_moons(n_samples=600, noise=0.07, random_state=0)
db = DBSCAN(eps=0.2, min_samples=5).fit(X)
km = KMeans(n_clusters=2, n_init=10, random_state=0).fit(X)
print("two interleaving crescents -- not round, not separable by a line:")
print(" DBSCAN found %d clusters, agreement %.4f"
% (len(set(db.labels_)) - (1 if -1 in db.labels_ else 0),
adjusted_rand_score(y, db.labels_)))
print(" k-means told to find 2, agreement %.4f" % adjusted_rand_score(y, km.labels_))
print(" k-means splits them down the middle. it can only draw straight lines.")
print()
Xn = np.vstack([X, np.random.default_rng(0).uniform(-2, 3, (40, 2))])
db2 = DBSCAN(eps=0.2, min_samples=5).fit(Xn)
print("add 40 scattered junk points:")
print(" DBSCAN labelled %d of them noise (label -1)" % (db2.labels_ == -1).sum())
print(" k-means has no such label. every point joins a cluster.")
print()
print("eps is the radius that defines 'nearby'. it decides everything:")
print("%8s %12s %10s" % ("eps", "clusters", "noise"))
for eps in (0.05, 0.10, 0.20, 0.40, 1.00):
d = DBSCAN(eps=eps, min_samples=5).fit(X)
n_clusters = len(set(d.labels_)) - (1 if -1 in d.labels_ else 0)
print("%8.2f %12d %10d" % (eps, n_clusters, (d.labels_ == -1).sum()))
print(" too small: everything is noise. too large: one blob.")
Output
The next block shows how to choose eps from the data and how min_samples affects the result.
example_02.pyscikit-learn
import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons, make_blobs
from sklearn.neighbors import NearestNeighbors
X, _ = make_moons(n_samples=600, noise=0.07, random_state=0)
print("choosing eps -- distance to the 5th nearest neighbour:")
d, _ = NearestNeighbors(n_neighbors=5).fit(X).kneighbors(X)
k_dist = np.sort(d[:, -1])
for q in (50, 75, 90, 95, 99):
print(" %2dth percentile: %.4f" % (q, np.percentile(k_dist, q)))
print(" the elbow points at roughly 0.10, and eps=0.10 recovers both crescents.")
print(" 0.20 also works. you want a plateau, not one correct number.")
print()
print("min_samples sets how crowded a neighbourhood must be:")
for ms in (3, 5, 10, 30, 60, 120):
d2 = DBSCAN(eps=0.2, min_samples=ms).fit(X)
print(" min_samples=%2d -> %d clusters, %d noise"
% (ms, len(set(d2.labels_)) - (1 if -1 in d2.labels_ else 0),
(d2.labels_ == -1).sum()))
print(" raise it far enough and even the crescents stop counting as dense.")
print()
print("where it struggles: clusters of very different density.")
Xa, _ = make_blobs(n_samples=[300, 300], centers=[[0, 0], [6, 6]],
cluster_std=[0.3, 2.0], random_state=0)
for eps in (0.3, 0.8, 1.5):
d3 = DBSCAN(eps=eps, min_samples=5).fit(Xa)
print(" eps=%.1f -> %d clusters, %d noise"
% (eps, len(set(d3.labels_)) - (1 if -1 in d3.labels_ else 0),
(d3.labels_ == -1).sum()))
Output
Check yourself
0 of 3
Answer without scrolling back up.
Why can DBSCAN separate two interleaved crescents when k-means cannot?
K-means assigns each point to the nearest centroid, so the boundary is always a straight line. DBSCAN never asks how far anything is from a centre.
What is a border point?
Core points have at least minPts neighbours within eps and drive the expansion; border points get swept in by them; everything else is noise.
What is DBSCAN's main structural weakness?
The eps that finds a sparse cluster merges a dense one into its surroundings. HDBSCAN builds a hierarchy over eps values to handle exactly this.
Cheat sheet
DBSCAN: Density-Based Clustering
Set epsilon and minPts, then watch DBSCAN grow clusters by density. See how it finds arbitrary shapes, labels noise, and never asks you to choose k.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.