Machine Learning Algorithms

Ch.68: Agglomerative Hierarchical Clustering

By Ayush Arora••14 min read

Inspired by: YouTube

The K-means post ended with a working algorithm: pick KK, place random centroids, repeat assign-then-move until nothing changes. That algorithm has a real weakness worth seeing directly before reaching for its replacement.

1. Where K-Means Actually Fails

K-means clusters by distance to a centroid, which quietly bakes in an assumption: clusters are roughly round blobs. Hand it a few other shapes and that assumption breaks:

Four scatter plots, each colored by K-means output. Concentric circles get split in half rather than by ring. Two crescent moons get split roughly in half by a vertical line rather than by moon. An elongated diagonal streak of points gets cut across its short axis into two colors, misgrouping points that belong together. A noisy scattering of outlier points gets absorbed into whichever of the two main blobs is nearest, rather than being left unclustered.

In every panel, K-means does exactly what it's built to do, minimize distance to a centroid, and in every panel that's the wrong objective for the actual shape of the data. Concentric circles need to be split by ring, not by a line through the middle. The crescents need to be split by moon. The elongated cluster is one group that happens to be stretched, not two groups cut across the middle. And K-means has no concept of "this point doesn't belong to any cluster," so stray noise gets forced in wherever it's nearest.

K-means works best specifically on spherical (or hyperspherical, in higher dimensions) clusters. Two other families of algorithms exist to handle the cases it doesn't: density-based clustering (DBSCAN, covered separately) and hierarchical clustering, the subject of this post.

2. Agglomerative vs Divisive

Hierarchical clustering comes in two directions. Agglomerative clustering works bottom-up: start by treating every single point as its own cluster, then repeatedly merge the two closest clusters until only one remains. Divisive clustering works top-down: start with all points in one cluster, then repeatedly split the least cohesive cluster until every point is alone.

Agglomerative is the one actually used in practice, for a fairly simple reason: deciding which two clusters are closest and merging them is a much better-defined operation than deciding how to best split a cluster into two. The rest of this post covers agglomerative clustering specifically.

3. The Core Idea: Merge the Closest Pair, Repeat

Six points, clustered by hand, to build the intuition before any code:

Left panel: six labeled points P1 through P6 scattered in 2D, P1 and P2 isolated near the top-left, P3, P4, P5, P6 forming a looser group toward the bottom-right. Right panel: the resulting dendrogram, showing P4 and P5 merge first at the smallest distance, then P1 and P2 merge independently, then P3 joins the P4-P5 pair, then P6 joins that group, and finally everything merges into one cluster at the top.

Every point starts as its own cluster, six clusters for six points. The two closest points, P4 and P5, merge first. Separately, P1 and P2 are also close, so they merge too. Now there's a cluster of ; P3 turns out to be closer to that cluster than to anything else, so it joins, forming . P6 is next closest to that group, so it joins too. Finally, the only two clusters left, and , merge into one.

Every merge gets recorded, and that record, drawn as a tree, is called a dendrogram. The height at which two clusters merge represents how far apart they were when they merged. Reading the dendrogram back tells the full merge history: P4-P5 were the closest pair overall, P1-P2 merged independently and later than P4-P5, and the last, highest merge (combining the two main branches) was the least natural one, forced only because the algorithm runs until a single cluster remains.

4. The Algorithm, as Pseudocode

Stated generally, for nn points:

  1. Build a proximity matrix: an n×nn \times n table where entry (i,j)(i, j) is the distance between point ii and point jj. The diagonal is all zeros, since every point's distance to itself is zero.
  2. Treat every point as its own cluster.
  3. Loop: find the two closest clusters in the current proximity matrix, merge them into one, and update the matrix, removing the two merged rows/columns and adding one new row/column for the merged cluster.
  4. Stop once only one cluster remains.

Step 3's update is where this gets genuinely tricky. The distance between two points is unambiguous, Euclidean distance between two coordinates. The distance between two clusters, each potentially containing many points, is not: "how far apart are these two groups?" has more than one reasonable answer, and the different answers are exactly what separate the different types of agglomerative clustering.

5. Four Ways to Measure Inter-Cluster Distance

Given two clusters, each with potentially many points, scikit-learn's AgglomerativeClustering supports four strategies (the linkage parameter) for turning all the pairwise point-to-point distances between the two clusters into a single number:

Written out, Ward's merge cost for two clusters AA and BB, with centroids μA\mu_A and μB\mu_B, and μA∪B\mu_{A \cup B} the centroid of all their points combined, is exactly that three-part recipe:

Δ(A,B)=∑x∈A∪B∥x−μA∪B∥2  −  ∑x∈A∥x−μA∥2  −  ∑x∈B∥x−μB∥2\Delta(A, B) = \sum_{x \in A \cup B} \lVert x - \mu_{A \cup B} \rVert^2 \;-\; \sum_{x \in A} \lVert x - \mu_A \rVert^2 \;-\; \sum_{x \in B} \lVert x - \mu_B \rVert^2

The first term is the spread of the merged cluster around its shared centroid; the other two are the spread each cluster already had on its own. With a little algebra, the whole expression collapses into a form that only needs the two centroids and the two cluster sizes nAn_A and nBn_B:

Δ(A,B)=nA nBnA+nB ∥μA−μB∥2\Delta(A, B) = \frac{n_A \, n_B}{n_A + n_B} \, \lVert \mu_A - \mu_B \rVert^2

This second form shows what Ward actually rewards. The ∥μA−μB∥2\lVert \mu_A - \mu_B \rVert^2 part says clusters whose centers are close are cheap to merge, like any other linkage. The nAnBnA+nB\frac{n_A n_B}{n_A + n_B} weight is the new part: it grows with cluster size, so two big clusters cost more to merge than two small ones at the same centroid distance. That pushes Ward toward absorbing small clusters first and producing clusters of similar size. One detail if you read the dendrogram's merge heights: SciPy's linkage(..., method="ward") reports 2 Δ(A,B)\sqrt{2\,\Delta(A, B)} as the height rather than Δ(A,B)\Delta(A, B) itself. Since that's a monotonic rescaling, the merge order and the shape of the tree are unchanged; only the numbers on the vertical axis differ.

The same two clusters, with each method's defining computation drawn directly on top of the points:

Four scatter plots of the same two clusters, one blue (5 points) and one red (5 points), with all 25 cross-cluster pairs drawn faintly as background lines in each panel. Single link panel: the single closest pair across the two clusters is highlighted in bold green, labeled 'min distance'. Complete link panel: the single farthest pair is highlighted instead, labeled 'max distance'. Average link panel: all 25 pairwise lines are drawn solid green with the mean distance labeled. Ward panel: both clusters' individual centroids are marked with triangles, the combined centroid of all 10 points is marked with a star, and every point connects to that combined centroid with a thin orange line.

Single and complete link both reduce the entire relationship between two clusters down to one specific pair of points, just different pairs (the nearest and the farthest, respectively), while discarding every other distance in the panel's faint background mesh. Average link keeps all of them, just flattened into a single mean. Ward works differently again: instead of picking or averaging point-to-point distances at all, it asks how far every point in both clusters sits from their one shared (combined) centroid, which is a direct stand-in for how spread out the merged cluster would become.

The chaining failure mode is worth seeing directly, since it's single link's defining weakness. Two well-separated blobs connected by a thin, dense trail of bridge points:

Two scatter plots of the same data: two blobs of points connected by a straight, evenly-spaced trail of bridge points. In the single-link panel, nearly every point is colored the same single color, with one lone point in a different color, showing the bridge chained everything into one cluster. In the complete-link panel, the two blobs are colored distinctly and separately, cleanly split despite the bridge.

Single link, asked for 2 clusters, produces one giant cluster covering almost everything plus a single leftover point, exactly the chaining failure: each bridge point is close to its neighbor, so the merge chain never has a reason to stop at the gap between the two blobs. Complete link, run on the identical data with the identical request for 2 clusters, recovers the two blobs cleanly, because a bridge point being close to one blob doesn't make the two blobs' farthest points close to each other.

None of the four linkage types is simply "better," they fail in different ways on different data, which is itself the point: hierarchical clustering's flexibility comes from choosing among them, not from one universally correct default (even though Ward is scikit-learn's default and a reasonable starting point).

6. Choosing the Number of Clusters from the Dendrogram

Agglomerative clustering, run to completion, always ends at exactly one cluster, so "how many clusters are there" isn't answered by the algorithm itself any more than it was for K-means. The dendrogram makes the answer readable directly: find the tallest vertical line that no horizontal line crosses, and cut through it.

A dendrogram built from 200 customers' annual income and spending score, colored into five groups below a dashed red horizontal cut line. The cut sits in the tall gap between the last few high-level merges and the many lower-level merges, and crossing exactly five vertical branches at that height.

The cut line sits in the largest unobstructed vertical gap, the four highest merges in this dendrogram happen at heights roughly 406, 395, 263, and 246, then the next merge drops all the way down to about 114, a large jump. Cutting anywhere in that gap crosses exactly 5 of the tree's branches, so 5 is the natural number of clusters this dendrogram is suggesting. The vertical distance at any merge point is exactly the inter-cluster distance from Section 5, the same quantity single link, complete link, average link, or Ward's method was computing all along.

7. Full Example: Segmenting Mall Customers

The classic dataset for this algorithm is a 200-row mall customer dataset ([CustomerID, Genre, Age, Annual Income, Spending Score]), using only the last two columns:

import pandas as pd
import scipy.cluster.hierarchy as shc
from sklearn.cluster import AgglomerativeClustering
 
customer_data = pd.read_csv("shopping_data.csv")
data = customer_data.iloc[:, 3:5].values  # Annual Income, Spending Score
 
dend = shc.dendrogram(shc.linkage(data, method="ward"))
A wide dendrogram built from 200 customers using ward linkage on their annual income and spending score, branching down from a single root into many small leaf-level merges, with the overall shape suggesting roughly 5 well-separated groups.

Applying the cut rule from Section 6 to this exact dendrogram gives K=5K=5:

cluster = AgglomerativeClustering(n_clusters=5, metric="euclidean", linkage="ward")
labels = cluster.fit_predict(data)

(Older scikit-learn versions name this parameter affinity instead of metric; both set the point-to-point distance measure, with euclidean as the default.)

Scatter plot of 200 mall customers plotted by annual income (x-axis) against spending score (y-axis), colored into 5 clearly separated clusters: low income/high spending, low income/low spending, mid income/mid spending, high income/high spending, and high income/low spending.

Five groups fall out cleanly: customers with low income but high spending, low income and low spending, mid income and mid spending clustered densely in the center, high income with high spending, and high income with low spending, a genuinely useful segmentation for a marketing team deciding who to target with which offer, with no manual boundary-drawing required.

A natural follow-up exercise: run the same recipe on a countries-of-the-world dataset (GDP, population, surface area, and similar columns), and see which countries end up closest to any one country of interest in the resulting dendrogram. That exercise doesn't have a companion notebook here, there's no dataset-specific trick involved beyond everything covered above, just a different CSV through the same shc.linkage and AgglomerativeClustering calls.

8. Benefits and the One Big Limitation

Two genuine advantages over K-means: hierarchical clustering handles a much wider variety of cluster shapes (choosing single, complete, average, or Ward linkage to suit the data, rather than being locked into one geometric assumption), and the dendrogram itself is a reusable artifact, it records, for every point, exactly which other points it's closest to and in what order, information a K-means run simply doesn't produce.

The cost is memory. The proximity matrix is n×nn \times n, every point's distance to every other point, and that grows quadratically with no way around it. For n=106n = 10^6 points, the matrix needs on the order of 101210^{12} entries; stored as 8-byte floating point numbers, that's roughly 8×10128 \times 10^{12} bytes, on the order of several terabytes, in RAM, for the distances alone. Past a dataset of a few tens of thousands of rows, this algorithm simply stops being usable, which is the real reason it isn't the default choice for large-scale clustering the way K-means or DBSCAN are.

9. What to Take Away