Machine Learning Essentials

Hierarchical & DBSCAN Clustering


Plot the GPS coordinates of every reported burglary in a city over six months. The points are not scattered evenly. They form dense knots along certain streets, thin trails connecting them, and a scatter of isolated incidents in the suburbs. You want to find the hotspots.

Ask k-means for six clusters and it will give you six. But every point gets assigned to one, including the isolated suburban incidents that belong to nothing. The hotspots along a curved high street get chopped in half, because k-means draws straight boundaries between centres. And the number six was your guess — the data may contain eleven hotspots, or three.

The problem is that k-means answers a specific question: given that there are exactly k round groups covering all the data, where are they? Sometimes that is the right question. For hotspots it is the wrong one on all three counts — you do not know the number, the shapes are not round, and not every point belongs to a group.

Two families of algorithms drop different parts of those assumptions.

Two answers to the burglary hotspot problemHierarchical• Builds the whole tree, k chosen later• Height in the tree shows separation• Single linkage chainsdense trails together• O(n squared) memorycaps it near 10,000 rowsDBSCAN• Clusters are denseregions, k never named• Isolated suburban incidents become noise• Finds elongated shapes along streets• Fails when density varies between areas
Only DBSCAN has a category for points that belong to nothing, which is exactly what a scatter of isolated incidents is.

Hierarchical clustering: build the whole tree, decide later

Instead of committing to a number of clusters, build every possible number at once.

The bottom-up (agglomerative) version:

  1. Start with every point as its own cluster. With 200 points you have 200 clusters.
  2. Find the two closest clusters and merge them. Now 199.
  3. Repeat until everything is in one cluster.

Record every merge and you get a dendrogram — a tree whose leaves are individual points and whose height at each junction is the distance at which those two groups joined.

Text
distance   | 12|                    ┌──────────────┐   |                    │              │  8|              ┌─────┴───┐          │   |              │         │          │  4|          ┌───┴──┐   ┌──┴──┐    ┌──┴──┐   |          │      │   │     │    │     │  0|   ───────A──────B───C─────D────E─────F───                cut here (dist 6) -> 3 clusters: {A,B} {C,D} {E,F}                cut here (dist 10) -> 2 clusters: {A,B,C,D} {E,F}

The dendrogram contains every clustering from 1 to n. You choose kk afterwards by drawing a horizontal line and counting the vertical lines it crosses — which is a genuinely different workflow from k-means, where changing kk means refitting.

Linkage: distance between clusters, not points

Step 2 says "find the two closest clusters", which requires deciding what distance between two groups means. This choice changes the results more than anything else.

Suppose cluster P = {(0,0), (1,0)} and cluster Q = {(4,0), (9,0)}. The pairwise distances are 4, 9, 3, and 8.

LinkageDefinitionValue hereBehaviour
SingleClosest pair3Finds long, snaking shapes; prone to chaining
CompleteFurthest pair9Produces compact, roughly equal clusters; sensitive to outliers
AverageMean of all pairs6A compromise; reasonably robust
WardIncrease in total within-cluster variance from merging—Minimises variance like k-means; usually the best default

Ward linkage is different in kind. Rather than measuring a distance, it asks: of all available merges, which one increases total within-cluster sum of squares the least? That is the same objective k-means minimises, approached from the bottom up, and it is why Ward tends to give compact, similar-sized clusters. It requires Euclidean distance; the others accept any metric.

The chaining problem

Single linkage merges on the closest pair, so a thin bridge of intermediate points can fuse two obviously separate groups. Two dense blobs 20 units apart, with a line of stragglers spaced 2 units apart running between them, will merge into one cluster — every merge along the chain is a small step, and the algorithm never notices it has walked across the gap.

This is sometimes exactly what you want, since it lets single linkage discover elongated shapes no other linkage finds. More often it is a failure mode, and it is the main reason Ward or average linkage are the safer defaults.

Python
import scipy.cluster.hierarchy as schfrom sklearn.preprocessing import StandardScalerfrom sklearn.cluster import AgglomerativeClusteringimport matplotlib.pyplot as pltX_scaled = StandardScaler().fit_transform(X)linkage = sch.linkage(X_scaled, method="ward")sch.dendrogram(linkage, truncate_mode="lastp", p=30)   # last 30 merges onlyplt.ylabel("merge distance")# then cut the treemodel = AgglomerativeClustering(n_clusters=4, linkage="ward")labels = model.fit_predict(X_scaled)

truncate_mode="lastp" matters in practice. A dendrogram of 5,000 points is an unreadable black smear at the bottom; showing only the final merges gives you the part that carries the decision.

Reading the tree for k

Look for the longest vertical stretch that no horizontal line crosses. A long gap means those clusters stayed separate over a wide range of distances — they are robustly distinct. Cutting in the middle of that gap gives a stable clustering. If merge heights rise smoothly with no notable gap, the data has no strong hierarchy and any cut is arbitrary.

The cost

Agglomerative clustering computes and maintains distances between all pairs: O(n2)O(n^2) memory and typically O(n2log⁡n)O(n^2 \log n) time. At 10,000 points the distance matrix holds 50 million entries — workable. At 100,000 points it is 5 billion, roughly 40 GB, and the approach is simply unavailable. Hierarchical clustering is for datasets up to a few tens of thousands of rows.

Hierarchical clustering does not ask you for k. It asks you for a notion of distance between groups, and then shows you every possible k so that you can choose with the structure in front of you.

DBSCAN: clusters are dense regions, everything else is noise

DBSCAN drops a different assumption. It does not partition the data at all. Some points belong to clusters; some are labelled noise and belong to nothing.

Two parameters:

  • eps (ε\varepsilon) — the radius defining "nearby".
  • min_samples — how many points must be within that radius for a region to count as dense.

Every point is then one of three kinds:

TypeDefinitionRole
CoreHas at least min_samples points within eps (including itself)Forms the interior of a cluster and can extend it
BorderWithin eps of a core point, but not itself dense enoughJoins a cluster but cannot extend it further
NoiseNeitherLabelled −1; belongs to no cluster

The algorithm picks an unvisited core point, claims everything within eps, then repeats from each newly claimed core point, spreading outward until the density runs out. Then it starts a new cluster elsewhere.

Because clusters grow by spreading through dense regions rather than radiating from a centre, they can be any shape at all — crescents, rings, long winding streets. The burglary hotspot along a curved high street is exactly what DBSCAN is good at.

A concrete count

With eps=1.5 and min_samples=4, take a point that has 6 neighbours within 1.5 units: it is a core point. A point on the edge with 2 neighbours, one of which is that core point: it is a border point, joins the cluster, but the search stops there. A point with 0 neighbours within 1.5 units: noise.

Change min_samples to 8 and the first point (6 neighbours) is no longer core. If nothing else nearby qualifies either, an entire cluster of 30 points becomes noise. DBSCAN is sensitive to these parameters in a way that is not gradual — small changes can restructure the result completely.

Choosing eps without guessing

There is a standard method. For every point, compute the distance to its kk-th nearest neighbour (with kk = min_samples), sort those distances, and plot them. Points inside clusters have small kk-th-neighbour distances; noise points have large ones. The plot therefore stays flat and then bends sharply upward, and the distance at the bend is a good eps.

Python
import numpy as npfrom sklearn.neighbors import NearestNeighborsimport matplotlib.pyplot as pltmin_samples = 8nn = NearestNeighbors(n_neighbors=min_samples).fit(X_scaled)distances, _ = nn.kneighbors(X_scaled)kth = np.sort(distances[:, -1])plt.plot(kth)plt.ylabel(f"distance to {min_samples}th nearest neighbour")plt.xlabel("points, sorted")# eps = the y-value at the knee

For min_samples, a common starting rule is twice the number of dimensions, raised if the data is noisy. Higher values produce fewer, denser clusters and more noise.

Python
from sklearn.cluster import DBSCANdb = DBSCAN(eps=0.6, min_samples=8).fit(X_scaled)labels = db.labels_n_clusters = len(set(labels)) - (1 if -1 in labels else 0)n_noise = (labels == -1).sum()print(f"{n_clusters} clusters, {n_noise} noise points "      f"({n_noise / len(labels):.1%})")

Always print the noise fraction. If 70% of your data is noise, eps is too small. If 0% is, it is probably too large and DBSCAN has degenerated into "one big cluster".

Where DBSCAN breaks

Varying density. This is the fundamental limitation. A single eps defines a single density threshold for the whole dataset. If your data has one tight cluster and one diffuse one, no single value works: an eps small enough to separate the tight cluster from its neighbours will shatter the diffuse cluster into noise, and one large enough to hold the diffuse cluster together will merge the tight ones.

HDBSCAN is the standard answer — it runs DBSCAN across all density thresholds and extracts the clusters that persist longest, which removes eps from your responsibilities entirely and handles mixed densities well. It is available as sklearn.cluster.HDBSCAN in recent versions.

High dimensions. DBSCAN depends on distance, and in high dimensions distances between all pairs of points converge towards each other. Beyond roughly 10–15 features, "within eps" stops distinguishing anything useful and DBSCAN either finds one cluster or all noise. Reduce dimensionality first.

Border-point ambiguity. A border point reachable from two clusters is assigned to whichever was processed first, so results can differ slightly with row order. Minor, but worth knowing if you need exact reproducibility.

Choosing between the three

K-MeansHierarchicalDBSCAN
Must specify k?YesNo — choose after seeing the treeNo
Cluster shapesRound onlyDepends on linkageAny shape
Handles outliersNo — forces assignmentNo — forces assignmentYes, explicitly
Varying densityPoorlyModeratelyPoorly (use HDBSCAN)
Practical size limitMillions~10–50k rows~100k–1M rows
ComplexityO(nkpi)O(nkpi)O(n2log⁡n)O(n^2 \log n)O(nlog⁡n)O(n \log n) with an index
Deterministic?No — depends on initYesNearly (border points aside)
Assigns new points?Yes — nearest centreNo — must refitNo — must refit
Key parameterklinkage, cut heighteps, min_samples

The "assigns new points" row has real operational consequences. K-means produces centroids, so classifying tomorrow's customer is a distance calculation. Hierarchical clustering and DBSCAN produce no such artefact — a new point requires rerunning the whole algorithm, or training a separate classifier on the cluster labels to serve as a predictor.

A practical decision path

  1. Do you need to assign new points cheaply, forever? K-means, or fit a classifier on top of whatever labels you produce.
  2. Are there outliers that genuinely belong to nothing? DBSCAN or HDBSCAN. Forcing anomalies into clusters distorts every cluster they land in.
  3. Are the shapes non-convex — curves, rings, streets? DBSCAN.
  4. Do you want to see structure at multiple levels of granularity? Hierarchical. Retail category trees and taxonomies are natural fits.
  5. Millions of rows? MiniBatchKMeans; the others will not finish.
  6. No idea? Run all three, compare silhouette scores, and — more importantly — look at the cluster profiles and see which set a domain expert recognises.

All three on the same data

Python
import pandas as pdimport numpy as npfrom sklearn.preprocessing import StandardScalerfrom sklearn.cluster import KMeans, AgglomerativeClustering, DBSCANfrom sklearn.metrics import silhouette_scoreX_scaled = StandardScaler().fit_transform(df[features])runs = {    "kmeans-4": KMeans(n_clusters=4, n_init=10, random_state=42),    "ward-4":   AgglomerativeClustering(n_clusters=4, linkage="ward"),    "dbscan":   DBSCAN(eps=0.6, min_samples=8),}for name, algo in runs.items():    labels = algo.fit_predict(X_scaled)    mask = labels != -1                     # exclude noise from the score    n_clusters = len(set(labels[mask]))    if n_clusters > 1:        sil = silhouette_score(X_scaled[mask], labels[mask])    else:        sil = float("nan")    print(f"{name:10s} clusters={n_clusters}  "          f"noise={(~mask).sum():5d}  silhouette={sil:.3f}")

The mask line is not optional. Passing DBSCAN's −1 labels straight into silhouette_score treats "noise" as a cluster, which it is not — noise points are scattered everywhere, so their internal cohesion is terrible and the score is dragged down for a reason that has nothing to do with cluster quality.

A representative result on customer data with a few genuine outliers:

MethodClustersNoiseSilhouetteWhat happened
k-means, k=4400.42Balanced groups; 30 extreme spenders distorted one centre
Ward, k=4400.44Similar structure, one cluster noticeably smaller
DBSCAN34120.58Isolated the outliers; the remaining clusters are much cleaner

DBSCAN's higher silhouette here is partly earned and partly definitional — removing 412 awkward points from the evaluation makes the rest look tidier. The honest question is whether those 412 are noise you should exclude or a genuine fourth segment you have just deleted. Look at them before deciding: if they share a coherent profile, they are a small cluster DBSCAN's density threshold missed, not noise.

What this means when you build something

Pick the algorithm by the shape of your question rather than by which one is fashionable.

If you need a fixed number of groups you will act on repeatedly, and new customers must be assignable tomorrow without retraining, k-means is the operationally correct choice even when another method scores better, because centroids are a deployable artefact and a dendrogram is not.

If your data contains points that genuinely belong to nothing — fraud, sensor faults, one-off events — use DBSCAN or HDBSCAN, and treat the noise label as a result rather than a failure. Being told "these 412 rows do not fit any pattern" is often more valuable than being told which arbitrary group they were forced into.

And whatever you run, look at the noise fraction, the cluster sizes, and the silhouette before you look at the cluster profiles. A method that returns one cluster containing 97% of the data and six containing four points each has not found structure; it has found a parameter setting. Adjusting that setting before you start writing up the segments will save you from a very confident presentation of nothing.