import numpy as np
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
rng = np.random.default_rng(11)
centres = np.array([[0., 0.], [4., 4.], [-4., 4.]])
X = np.vstack([rng.normal(c, 1.0, (25, 2)) for c in centres])Hierarchical clustering
Examples
Hierarchical clustering does not need the number of clusters in advance. It builds a whole tree of clusterings, and we cut it where we like.
The algorithm
- Start with every point in its own cluster.
- Repeat until one cluster is left.
- Find the two closest clusters.
- Merge them, and record the height at which they merged.
The result is a dendrogram. Cutting it at a given height gives a clustering.
Linkage
We need a distance between two sets of points, not between two points. That choice is called the linkage.
| linkage | definition |
|---|---|
| single | \(\min_{x \in C_i, y \in C_j} d(x, y)\) |
| complete | \(\max_{x \in C_i, y \in C_j} d(x, y)\) |
| average | the mean over all pairs |
| Ward | the merge that increases the within-cluster sum of squares the least |
An example
Z = linkage(X, method="complete")
fig, ax = plt.subplots(figsize=(6.6, 3.0))
dendrogram(Z, ax=ax, no_labels=True, color_threshold=6.5,
above_threshold_color="#7a838b")
ax.axhline(6.5, color="#a8452f", lw=1.2, ls="--")
ax.set(ylabel="merge height")
plt.show()The height of a merge is the distance between the two clusters at that moment. A long vertical line means the two clusters were far apart, which is a sign of real structure. Many short merges near the bottom mean the points are all similar.
labels = fcluster(Z, t=3, criterion="maxclust")
print("cluster sizes:", np.bincount(labels)[1:])cluster sizes: [25 25 25]
The linkage matters
fig, axes = plt.subplots(1, 4, figsize=(6.8, 2.0))
for ax, method in zip(axes, ["single", "complete", "average", "ward"]):
lab = fcluster(linkage(X, method=method), t=3, criterion="maxclust")
for j in np.unique(lab):
ax.plot(X[lab == j, 0], X[lab == j, 1], "o", ms=2.5)
ax.set_title(method, fontsize=9)
ax.set(xticks=[], yticks=[])
plt.show()Single linkage tends to produce one large cluster and several tiny ones, because a single close pair is enough to merge two groups. This is called chaining.
Complete and average linkage produce more balanced clusters. Ward tends to produce clusters of similar size, which is a good default and also an assumption we may not want.
When single linkage wins
Chaining is not always bad. If the clusters are long and thin, single linkage is the only one of the four that finds them.
t = rng.uniform(0, np.pi, 120)
moon1 = np.column_stack([np.cos(t), np.sin(t)]) + rng.normal(0, 0.06, (120, 2))
moon2 = np.column_stack([1 - np.cos(t), 0.5 - np.sin(t)]) + rng.normal(0, 0.06, (120, 2))
moons = np.vstack([moon1, moon2])
fig, axes = plt.subplots(1, 2, figsize=(6.6, 2.9))
for ax, method in zip(axes, ["single", "complete"]):
lab = fcluster(linkage(moons, method=method), t=2, criterion="maxclust")
for j in np.unique(lab):
ax.plot(moons[lab == j, 0], moons[lab == j, 1], "o", ms=3)
ax.set_title(method, fontsize=9)
ax.set(xticks=[], yticks=[])
plt.show()Small decisions with big consequences
Before we can cluster anything we have made several choices. The distance measure. Whether to standardize. The linkage. Where to cut.
None of these is given by the data, and all of them change the answer. Report them, and check whether the conclusion survives a different choice.