Clustering
Clustering groups unlabelled data, and every clustering algorithm is really a guess about what shape a group is allowed to be.
- 21 min read
- 3 reading levels
- Updated
Read these first
On this page 9
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
The short answer
Clustering is sorting things into groups when nobody has told you what the groups are.
The view from the terrace
Stand on a rooftop at night and look down at the city. You see the lights bunch together. A market square glows as a tight round blob. A main road stretches away as a long thin ribbon of streetlamps. Between them sit dark fields with almost nothing.
You did not count anything. Your eyes found the groups instantly, because groups are places where things are packed close with gaps around them.
Now imagine someone insisting that every group of lights must be a circle. The market square survives that rule. The road along the highway gets chopped into six unrelated pieces.
That single disagreement is what this whole lesson is about.
Why this lesson exists
Unsupervised learning introduced the idea of grouping data with no answer key. It used k-means, an algorithm that puts a centre point in each group and pulls the nearest items towards it.
K-means is the one everybody learns first. It is also the one that insists every group is a round blob.
Most people stop there. They meet k-means, decide clustering is a solved thing, and then get strange results on real data and blame the data.
The truth is smaller and more useful. K-means is not "clustering". It is one opinion about what a group looks like. There are other opinions, and they disagree.
Three ways to decide what a group is
By centres. Pick a number of centre points. Every item belongs to whichever centre is nearest. This is k-means. Fast, easy, and it always produces round-ish groups of roughly similar size.
By crowding. A group is any patch where items are packed tightly together. Follow the crowd wherever it leads. Anything sitting alone in a gap gets marked as an oddity, rather than forced into a group. This is DBSCAN, short for density-based spatial clustering of applications with noise.
By joining up. Start with every item alone. Repeatedly join the two closest groups. Keep going until everything is one group, and record the order in which things merged. This is hierarchical clustering, and the record it leaves lets you cut the result at any number of groups afterwards.
How they differ, in one picture
Here is data shaped like two rings, one inside the other. A human sees the answer at once.
the data k-means says DBSCAN says
ooooo AAABB AAAAA
oo oo AA BB AA AA
o ooo o A A B B A BBB A
o o o o A A B B A B B A
o ooo o A A B B A BBB A
oo oo AA BB AA AA
ooooo AAABB AAAAA
one cut down the middle inner ring, outer ringK-means sliced the picture in half, because half is the best it can do with two round blobs. DBSCAN followed each ring around, because a ring is a connected patch of crowding.
Neither algorithm is broken. They were asked different questions, and they answered honestly.
Where you have already seen this
- News apps bundling every article about one match into a single card.
- Photo apps gathering the faces of one person before you name them.
- Delivery companies carving a city into zones so one rider covers one patch.
- Fraud teams flagging the card swipes that sit far from every normal pattern.
- Disease maps finding outbreak hotspots by looking for unusual crowding on a map.
Notice the shapes involved. A delivery zone is blobby, and k-means suits it. An outbreak spreading along a river is a ribbon, and it does not.
What is honestly hard here
There is no answer to check against. In classification you can count how many predictions were right. Here nothing is right. You get a grouping, and you have to judge it yourself.
The scoring numbers have opinions too. There are scores that rate a grouping without any answer key. Almost all of them reward tight round groups. So a score can rate the wrong answer above the right one. You will see that happen in the Developer section, with real numbers.
Read this twice, because it surprises people. Choosing a clustering algorithm is not a search for the best one. It is a statement about what you believe a group is in your data. If you cannot say what a group means in your problem, no algorithm will decide it for you.
Remember this
- Clustering finds groups with no labels, and every algorithm assumes a shape for those groups.
- Centres give round groups, crowding gives any shape plus outliers, joining-up gives a whole family of answers at once.
- Match the algorithm to the shape you expect, and be suspicious of any score that claims one grouping is the correct one.
What to learn next
- Unsupervised learning — k-means in full, and why scaling decides the answer.
- Decision trees — back to labelled data, and a model you can read aloud.
- Autoencoders — learning the space you should be clustering in.
Developer — Code and libraries.
Setup
pip install scikit-learn numpyEverything below is generated in memory and runs in a couple of seconds on a laptop. No downloads, no GPU.
The shape test
Two rings, one inside the other. The correct grouping is plain to a person, and we know it exactly, so we can score every algorithm against the truth.
The score is the adjusted Rand index (ARI), which measures how well two groupings agree. It is 1.0 for a perfect match and about 0.0 for a random guess.
from sklearn.cluster import DBSCAN, AgglomerativeClustering, KMeans
from sklearn.datasets import make_circles
from sklearn.metrics import adjusted_rand_score
# Two rings, one inside the other. `truth` says which ring each point came from.
X, truth = make_circles(n_samples=200, factor=0.4, noise=0.05, random_state=0)
runs = {
"k-means, k=2": KMeans(n_clusters=2, n_init=10, random_state=0),
"average linkage, k=2": AgglomerativeClustering(n_clusters=2, linkage="average"),
"single linkage, k=2": AgglomerativeClustering(n_clusters=2, linkage="single"),
"DBSCAN, eps=0.2": DBSCAN(eps=0.2, min_samples=5),
}
for name, model in runs.items():
labels = model.fit_predict(X)
groups = len(set(labels) - {-1}) # DBSCAN uses -1 to mean "outlier"
noise = int((labels == -1).sum())
print(f"{name:22s} groups={groups} noise={noise:3d} "
f"agreement with truth (ARI) {adjusted_rand_score(truth, labels):+.3f}")k-means, k=2 groups=2 noise= 0 agreement with truth (ARI) -0.005 average linkage, k=2 groups=2 noise= 0 agreement with truth (ARI) +0.106 single linkage, k=2 groups=2 noise= 0 agreement with truth (ARI) +1.000 DBSCAN, eps=0.2 groups=2 noise= 0 agreement with truth (ARI) +1.000
Read the first row carefully. K-means scored -0.005, which is what pure chance scores. Every setting was reasonable and the number of groups was correct. The algorithm still learned nothing, because a ring is not a blob.
Average linkage joins groups by the average distance between their members, so it also leans towards compact blobs. It managed +0.106, which is barely above chance.
Single linkage joins groups by their closest pair of members. That lets a group creep along a ring one neighbour at a time, and it recovered the truth exactly.
DBSCAN did the same for the same reason, and it discovered that there were two groups without being told.
Now watch the winner lose
If the lesson were "single linkage is better", this page would be misleading you. Here is a different dataset: two real groups with a thin line of stray points running between them.
import numpy as np
from sklearn.cluster import AgglomerativeClustering, KMeans
rng = np.random.default_rng(0)
left = rng.normal([0, 0], 0.35, size=(80, 2)) # one real group
right = rng.normal([6, 0], 0.35, size=(80, 2)) # the other real group
bridge = np.column_stack([np.linspace(0.9, 5.1, 14), rng.normal(0, 0.05, 14)])
Y = np.vstack([left, right, bridge]) # 174 points in total
for name, model in [
("single linkage ", AgglomerativeClustering(n_clusters=2, linkage="single")),
("average linkage", AgglomerativeClustering(n_clusters=2, linkage="average")),
("ward linkage ", AgglomerativeClustering(n_clusters=2, linkage="ward")),
("k-means ", KMeans(n_clusters=2, n_init=10, random_state=0)),
]:
labels = model.fit_predict(Y)
print(f" {name} group sizes {sorted(np.bincount(labels).tolist())}")single linkage group sizes [1, 173] average linkage group sizes [84, 90] ward linkage group sizes [84, 90] k-means group sizes [87, 87]
Single linkage returned one group of 173 and one group of 1.
The stray points formed a chain of stepping stones. Single linkage only needs one close pair to merge two groups, so it walked across the bridge and swallowed both real groups into one. The second "group" is a lone outlier at the far edge.
This failure has a name: chaining. The very property that let single linkage follow a ring is the property that lets fourteen stray points destroy the answer.
Average linkage, Ward linkage and k-means all handled this one sensibly. The algorithm that was perfect a moment ago is now the worst in the table.
There is no best clustering algorithm. There is only a match, or a mismatch, between an algorithm's idea of a group and your data's actual shape.
The score that recommends the wrong answer
Unsupervised learning introduced the silhouette score, which rates a grouping without any answer key. Higher is supposed to mean better. Here is what it says about the rings.
from sklearn.cluster import DBSCAN, KMeans
from sklearn.datasets import make_circles
from sklearn.metrics import adjusted_rand_score, silhouette_score
X, truth = make_circles(n_samples=200, factor=0.4, noise=0.05, random_state=0)
km = KMeans(n_clusters=2, n_init=10, random_state=0).fit_predict(X)
db = DBSCAN(eps=0.2, min_samples=5).fit_predict(X)
print(f"k-means ARI {adjusted_rand_score(truth, km):+.3f} "
f"silhouette {silhouette_score(X, km):+.3f}")
print(f"DBSCAN ARI {adjusted_rand_score(truth, db):+.3f} "
f"silhouette {silhouette_score(X, db):+.3f}")k-means ARI -0.005 silhouette +0.326 DBSCAN ARI +1.000 silhouette +0.153
Silhouette prefers k-means, at +0.326 against +0.153. ARI, which is allowed to see the true answer, says k-means learned nothing and DBSCAN was perfect.
The silhouette score is not buggy. It measures whether points sit close to their own group's members and far from other groups' members, using straight-line distance. A ring fails that test by construction: two points on opposite sides of the same ring are far apart.
So the score encodes the same round-blob assumption that k-means does, and it rewards its own assumption.
Practical rule: never pick a clustering algorithm using a score built on the assumption you are trying to test. Use silhouette to compare k values within one algorithm. Do not use it to choose between algorithms with different ideas of shape.
Choosing DBSCAN's eps without guessing
DBSCAN has one setting that decides everything: eps, the radius that counts as "nearby". Guessing it is the main reason people abandon DBSCAN.
The standard method is to look at how far each point sits from its min_samples-th nearest neighbour. Most points in a real group will have a small value. Outliers will have a large one.
import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_circles
from sklearn.metrics import adjusted_rand_score
from sklearn.neighbors import NearestNeighbors
X, truth = make_circles(n_samples=200, factor=0.4, noise=0.05, random_state=0)
# How far is each point from its 5th nearest neighbour?
distances, _ = NearestNeighbors(n_neighbors=5).fit(X).kneighbors(X)
fifth = np.sort(distances[:, 4])
print("distance to the 5th nearest neighbour:")
for pct in (50, 75, 90, 95, 99):
print(f" {pct}% of points are within {np.percentile(fifth, pct):.3f}")
print()
print(" eps groups noise ARI")
for eps in (0.08, 0.12, 0.15, 0.20, 0.30, 0.40):
labels = DBSCAN(eps=eps, min_samples=5).fit_predict(X)
groups = len(set(labels) - {-1})
print(f"{eps:5.2f} {groups:6d} {int((labels == -1).sum()):5d} "
f"{adjusted_rand_score(truth, labels):+.3f}")distance to the 5th nearest neighbour: 50% of points are within 0.120 75% of points are within 0.154 90% of points are within 0.179 95% of points are within 0.188 99% of points are within 0.210 eps groups noise ARI 0.08 8 122 +0.345 0.12 6 79 +0.674 0.15 14 21 +0.544 0.20 2 0 +1.000 0.30 2 0 +1.000 0.40 1 0 +0.000
The neighbour distances flatten out somewhere between 0.18 and 0.21. Setting eps a little above that flat point lands you at 0.20, which is exactly where the answer is perfect.
Study the sweep, because it is unforgiving.
- At
eps=0.08the radius is smaller than the gaps inside a ring. DBSCAN found 8 fragments and dumped 122 of 200 points into noise. - At
eps=0.15it found 14 groups. The count is not even smoothly wrong on the way up. - At
eps=0.40the radius bridges the gap between the rings, so everything becomes one group and the ARI collapses to zero.
Two of six settings work. eps is not a knob you tune casually, and the neighbour-distance table is how you avoid a blind search.
If picking eps for your data feels impossible because different regions have different densities, that is a real limitation of DBSCAN, not your mistake. HDBSCAN was built for exactly that case and ships with scikit-learn as sklearn.cluster.HDBSCAN.
Common mistakes
Clustering unscaled columns. A column measured in rupees will overwhelm a column measured in visits. This is the single most common error in clustering and it is demonstrated with code in unsupervised learning. Scale first, every time.
Treating group numbers as meaningful. Label 0 in one run may be label 1 in the next. Store the members, never the number.
Forgetting that DBSCAN produces -1. Outliers get the label -1, and it is a real label. Code that does np.bincount(labels) or indexes a colour list with the label will fail or silently mislabel your outliers.
Calling fit_predict on new data. DBSCAN and AgglomerativeClustering have no predict method, because there is no rule to apply to a new point. To assign new points you must either refit or attach them to the nearest existing group yourself. K-means and Gaussian mixtures do have predict.
Comparing algorithms with an internal score. Shown above. Silhouette rewards its own shape assumption.
Trusting a picture from t-SNE or UMAP. Those methods rearrange data for display. The gaps and the group sizes you see in the picture are partly artefacts of their settings. Confirm any grouping in the original space.
Try it yourself
Change noise=0.05 to noise=0.12 in rings.py and rerun it.
The rings get fuzzier, and they start touching. Before you run it, predict which two algorithms fail first.
Then find the largest noise value at which eps=0.20 still recovers both rings. Then rerun choose_eps.py at that noise level and check whether the neighbour-distance table still points you at a value that works. When it stops doing so, you have found the point where the groups genuinely stopped being separable — which is information about your data, not a failure of your code.
What to learn next
- Unsupervised learning — k-means in full, and why scaling decides the answer.
- Decision trees — back to labelled data, and a model you can read aloud.
- Autoencoders — learning the space you should be clustering in.
Researcher — Mathematics and papers.
The three families, formally
Centroid-based. Minimise within-cluster sum of squares. The objective, Lloyd's algorithm, k-means++ seeding, the O(nkdi) cost and the NP-hardness result are covered in the researcher block of unsupervised learning and are not repeated here.
Hierarchical (agglomerative). Start with n singleton clusters and repeatedly merge the closest pair under a linkage function d(A, B). The merge sequence forms a dendrogram; any horizontal cut yields a partition.
Density-based. Define clusters as maximal connected regions of high density, with low-density points labelled noise.
Linkage functions
The linkage function is the entire content of hierarchical clustering. Each one implies a different cluster geometry.
single d(A,B) = min_{a in A, b in B} ||a - b||
complete d(A,B) = max_{a in A, b in B} ||a - b||
average d(A,B) = (1/(|A||B|)) * SUM_{a,b} ||a - b|| (UPGMA)
centroid d(A,B) = ||mean(A) - mean(B)||
Ward d(A,B) = ( |A||B| / (|A|+|B|) ) * ||mean(A) - mean(B)||^2A,B— two candidate clusters;|A|is the number of points inA||.||— Euclidean norm
Single linkage computes the minimum spanning tree of the data and cuts its largest edges (Gower & Ross, 1969). That gives it the ability to follow arbitrarily shaped connected structures, and the chaining pathology demonstrated in the Developer section, where a thin bridge of 14 points merges two well-separated groups. The two behaviours are the same property.
Ward's criterion (Ward, 1963) merges the pair that increases total within-cluster variance least. It is the closest hierarchical analogue of k-means and inherits the same preference for spherical, similarly-sized clusters. In scikit-learn, linkage="ward" requires metric="euclidean" for exactly this reason.
All five fit the Lance-Williams recurrence (Lance & Williams, 1967), which updates distances after a merge in O(1) without revisiting the raw points:
d(A ∪ B, C) = α_A d(A,C) + α_B d(B,C) + β d(A,B) + γ |d(A,C) − d(B,C)|Each linkage is one setting of (α_A, α_B, β, γ). This recurrence is why a naive implementation costs O(n^3) time and O(n^2) memory. SLINK (Sibson, 1973) and CLINK (Defays, 1977) achieve O(n^2) for single and complete linkage; nearest-neighbour chain gives O(n^2) for any reducible linkage, which includes Ward, complete and average but excludes centroid and median linkage, whose dendrograms can contain inversions.
The O(n^2) memory requirement is the practical ceiling. A full pairwise distance matrix for n = 100{,}000 in float64 is 80 GB.
DBSCAN
Ester, Kriegel, Sander & Xu (1996) define clusters through two parameters, eps and minPts:
N_eps(p) = { q in D : dist(p,q) <= eps } the eps-neighbourhood
core point : |N_eps(p)| >= minPts
q is directly density-reachable from p if p is core and q in N_eps(p)
q is density-reachable from p if a chain of such steps exists
p and q are density-connected if some core o reaches both
cluster : a maximal density-connected set
noise : any point in no clusterProperties worth stating precisely:
kis discovered, not supplied. The number of clusters follows from the density structure.- Border points are ambiguous. A non-core point within
epsof two different core points is assigned to whichever cluster reaches it first. The result depends on data ordering. HDBSCAN removes this. - Complexity is
O(n log n)with a spatial index (R*-tree, ball tree, k-d tree) andO(n^2)without one. Index performance degrades above roughly 15–20 dimensions, at which point DBSCAN becomes quadratic. - Memory is
O(n), notO(n^2), which is its main advantage over hierarchical methods at scale.
The single global eps is DBSCAN's real limitation: it cannot represent clusters of substantially different density. HDBSCAN (Campello, Moulavi & Sander, 2013) fixes this by building a hierarchy over the mutual reachability distance
d_mreach(a,b) = max( core_k(a), core_k(b), d(a,b) )where core_k(x) is the distance from x to its k-th nearest neighbour. It then extracts a flat clustering by maximising cluster stability, defined as the persistence of a branch across the density hierarchy. This replaces eps with min_cluster_size, a parameter with a meaning practitioners can actually reason about.
OPTICS (Ankerst et al., 1999) is the intermediate step: it produces a reachability plot that encodes the clustering for all eps simultaneously.
Spectral clustering
When clusters are connected but not compact, embed before you partition. Build an affinity matrix W, typically W_ij = exp(−||x_i − x_j||^2 / (2σ^2)), and form the graph Laplacian:
L = D − W unnormalised, D_ii = SUM_j W_ij
L_sym = D^{−1/2} L D^{−1/2} Ng, Jordan & Weiss (2002)
L_rw = D^{−1} L Shi & Malik (2000)Take the eigenvectors of the k smallest eigenvalues, treat their rows as an embedding of the points, and run k-means there. The theoretical grounding is the Cheeger inequality relating the second eigenvalue of L to the normalised cut of the graph.
The relevant fact for the rings dataset: spectral clustering recovers it, because in the eigenvector embedding each ring collapses to a compact region. Cost is O(n^3) for a dense eigendecomposition, or O(n k) per iteration with Lanczos on a sparse k-nearest-neighbour graph. The result is highly sensitive to σ. von Luxburg (2007), A Tutorial on Spectral Clustering, remains the reference treatment.
Kleinberg's impossibility theorem
Kleinberg (2002), An Impossibility Theorem for Clustering, proves that no clustering function f mapping a distance function on a finite set to a partition can satisfy all three of:
- Scale invariance —
f(αd) = f(d)for allα > 0. - Richness — every partition of the set is achievable by some
d. - Consistency — shrinking within-cluster distances and stretching between-cluster distances leaves
f(d)unchanged.
Any two are satisfiable; all three are not. Single linkage cut at k clusters gives up richness. Cut at a fixed distance threshold, it gives up scale invariance. K-means with fixed k gives up richness.
The practical reading is the point this lesson makes with data: there is no algorithm-independent notion of "the correct clustering". Ackerman & Ben-David (2008) show the impossibility relaxes when applied to clustering quality measures rather than clustering functions, which is why evaluation is a more tractable formal object than the algorithm itself.
Evaluation
With ground truth. The Rand index counts agreeing pairs; the adjusted version corrects for chance agreement:
ARI = ( RI − E[RI] ) / ( max(RI) − E[RI] )RI is the fraction of point pairs that two partitions agree about, either by placing both points together in each, or by separating them in each. E[RI] is its expectation under the generalised hypergeometric model of random partitions with fixed cluster sizes (Hubert & Arabie, 1985). The correction matters: an unadjusted Rand index rises towards 1 as k grows, purely by chance.
Adjusted mutual information (Vinh, Epps & Bailey, 2010) applies the same chance correction to normalised mutual information. Their recommendation is AMI when clusters are unbalanced and small, ARI when they are large and roughly equal.
Without ground truth. Silhouette (Rousseeuw, 1987), Davies-Bouldin (1979) and Calinski-Harabasz (1974) all encode a compactness-and-separation prior under a chosen metric. The Developer section measures the consequence: silhouette rates a k-means partition with ARI = −0.005 above a DBSCAN partition with ARI = 1.000. These indices are valid only for comparing partitions produced under the same geometric assumption they encode.
For density-based results, DBCV (Moulavi et al., 2014) scores clusterings using density rather than centroid distance and does not penalise non-convex shapes.
Current practice
Three shifts matter.
Cluster in a learned space. Raw features rarely make Euclidean distance meaningful. Standard practice is to learn a representation first — self-supervised, or via an autoencoder — and cluster the embedding. Deep clustering methods such as DEC (Xie, Girshick & Farhadi, 2016) and SCAN (Van Gansbeke et al., 2020) fold the two steps together.
HDBSCAN over DBSCAN. For exploratory work on real data, min_cluster_size is a parameter people can set from domain knowledge; eps is not. This is now the pragmatic default for density-based work.
Stability as the selection criterion. Rather than optimising an internal index, resample the data, cluster each subsample, and measure how often pairs of points co-cluster (Ben-Hur, Elisseeff & Guyon, 2002; Von Luxburg, 2010). A grouping that survives resampling is evidence of structure. An internal index optimum is evidence about the index.
The honest summary for practice: decide what a group means in your problem, pick the algorithm whose assumptions match that meaning, and validate by resampling rather than by an index.
Key references
- Ward, J. H. (1963). Hierarchical Grouping to Optimize an Objective Function. JASA 58(301).
- Lance, G. N. & Williams, W. T. (1967). A General Theory of Classificatory Sorting Strategies. Computer Journal 9(4).
- Sibson, R. (1973). SLINK: An Optimally Efficient Algorithm for the Single-Link Cluster Method. Computer Journal 16(1).
- Ester, M., Kriegel, H.-P., Sander, J. & Xu, X. (1996). A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise. KDD.
- Ankerst, M. et al. (1999). OPTICS: Ordering Points To Identify the Clustering Structure. SIGMOD.
- Shi, J. & Malik, J. (2000). Normalized Cuts and Image Segmentation. IEEE TPAMI 22(8).
- Ng, A., Jordan, M. & Weiss, Y. (2002). On Spectral Clustering: Analysis and an Algorithm. NeurIPS.
- Kleinberg, J. (2002). An Impossibility Theorem for Clustering. NeurIPS.
- Hubert, L. & Arabie, P. (1985). Comparing Partitions. Journal of Classification 2(1).
- Vinh, N. X., Epps, J. & Bailey, J. (2010). Information Theoretic Measures for Clusterings Comparison. JMLR 11.
- von Luxburg, U. (2007). A Tutorial on Spectral Clustering. Statistics and Computing 17(4).
- Campello, R., Moulavi, D. & Sander, J. (2013). Density-Based Clustering Based on Hierarchical Density Estimates. PAKDD.
- Moulavi, D. et al. (2014). Density-Based Clustering Validation. SDM.
What to learn next
- Unsupervised learning — k-means in full, and why scaling decides the answer.
- Decision trees — back to labelled data, and a model you can read aloud.
- Autoencoders — learning the space you should be clustering in.