Hierarchical Clustering

Hierarchical clustering builds a full tree of merges, letting you pick the number of clusters after the fact and inspect structure at every scale. This page covers agglomerative clustering, linkage choices, scaling limits and practical code.


1. Building a Hierarchy

Agglomerative Clustering and the Dendrogram A B C D E F G points in feature space A B C D E F G cut -> 3 clusters merge height = linkage distance

Hierarchical clustering produces a nested tree of clusters instead of a single flat partition. The common agglomerative (bottom-up) form starts with every point as its own cluster and repeatedly merges the two closest clusters until one remains. The merge history is drawn as a dendrogram: the height of each join is the distance at which the merge happened. Cutting the tree at a height (or asking for n clusters) yields a flat clustering, and you can choose the cut after seeing the structure — no need to fix k in advance as with K-Means.

Divisive (top-down) clustering splits recursively instead; bisecting K-Means is a practical version. Agglomerative is far more common.

2. Linkage Criteria

Linkage Criteria: How Cluster Distance Is Measured Single min pairwise chains, finds snakes Complete max pairwise compact, outlier-sensitive Average mean pairwise middle ground Ward increase in SSE K-Means-like, default

The distance between two clusters is defined by the linkage:

3. Complexity

The naive algorithm needs the full n × n distance matrix: O(n2) memory and O(n2) to O(n3) time depending on linkage and implementation (SciPy uses efficient O(n2) algorithms for the standard linkages). In practice that limits plain agglomerative clustering to tens of thousands of points. Options for larger data: provide a sparse connectivity graph (k-nearest-neighbors) so only local merges are considered, cluster a sample or K-Means micro-clusters first, or use BIRCH or HDBSCAN.

4. Example with SciPy and scikit-learn

import numpy as np
from scipy.cluster.hierarchy import linkage, fcluster, dendrogram
from sklearn.cluster import AgglomerativeClustering
from sklearn.datasets import make_blobs
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score

X, _ = make_blobs(n_samples=500, centers=4, random_state=1)
X = StandardScaler().fit_transform(X)

# SciPy: full merge tree, choose the cut afterwards
Z = linkage(X, method="ward")                  # (n-1) x 4 merge table
labels = fcluster(Z, t=4, criterion="maxclust")
# or cut by height: fcluster(Z, t=5.0, criterion="distance")
# dendrogram(Z, truncate_mode="lastp", p=20)   # plot with matplotlib

# scikit-learn equivalent, with an optional connectivity constraint
from sklearn.neighbors import kneighbors_graph
conn = kneighbors_graph(X, n_neighbors=10, include_self=False)
agg = AgglomerativeClustering(n_clusters=4, linkage="ward", connectivity=conn).fit(X)
print(silhouette_score(X, agg.labels_).round(3))

# Average linkage with cosine distance, e.g. for embeddings
agg_cos = AgglomerativeClustering(n_clusters=None, distance_threshold=0.4,
                                  metric="cosine", linkage="average").fit(X)
print("clusters found:", agg_cos.n_clusters_)

5. When to Use It

Pitfalls: merges are greedy and never undone, so an early bad merge propagates; results depend heavily on linkage and scaling; and dendrograms on more than a few hundred leaves need truncation to be readable.

6. Worked Example: Ward Linkage by Hand

Ward Merges on Five 1-D Points A=1 B=2 merge, cost 0.5 D=9 E=10 merge, cost 0.5 AB + C=4 cost 4.17 ABC + DE cost 61.6 Cut between heights 4.17 and 61.6 to get two clusters: {1, 2, 4} and {9, 10} largest vertical gap in the dendrogram = most natural cut

Take five one-dimensional points: A=1, B=2, C=4, D=9, E=10. Ward's merge cost for clusters P and Q is the increase in total within-cluster sum of squares, Δ = (|P||Q| / (|P|+|Q|)) · (meanP − meanQ)2.

The shape is what matters: two cheap merges, one moderate merge, and one very expensive merge. The huge jump before the last merge says the data has two well-separated groups, and cutting anywhere in that gap returns {1, 2, 4} and {9, 10}. With single linkage the same data merges in the same order here, but on elongated or noisy data the orders diverge, which is why linkage choice matters.

7. Cutting the Tree and Validating

scikit-learn and SciPy offer two ways to get a flat clustering: ask for n_clusters, or set a distance_threshold (SciPy: fcluster(Z, t, criterion="distance")). Thresholds are more interpretable when the distance has meaning, such as a cosine distance of 0.15 for near-duplicate text. Useful checks:

8. Hierarchical vs Other Methods

Agglomerative (Ward)K-MeansHDBSCAN
OutputFull tree; flat cut on demandOne flat partitionCluster tree condensed to flat clusters + noise
DeterministicYes (ties aside)Depends on initializationYes
MetricsWard needs Euclidean; others anyEuclideanAny metric
MemoryO(n2)O(nk)Roughly O(n) with spatial indexes in low dimensions
OutliersForced into clusters (single linkage isolates them late)Pull centroidsLabeled as noise

Typical real-world uses follow from the tree output: building product or document taxonomies that people browse level by level, ordering rows and columns of gene-expression or correlation heatmaps (the clustered heatmap), grouping near-duplicate records for entity resolution with a distance threshold, and phylogenetics, where average linkage (UPGMA) has a long history. In each case the value is the nested structure, not just one flat partition.

A common hybrid for large data: run MiniBatchKMeans with a few hundred to a few thousand centers, then cluster the centers hierarchically. You keep the interpretable tree while the expensive step runs on micro-clusters.

9. Pitfalls

Further reading: Ward, J. (1963), "Hierarchical grouping to optimize an objective function"; Müllner, D. (2011), "Modern hierarchical, agglomerative clustering algorithms" (arXiv:1109.2378); Murtagh & Contreras (2012), "Algorithms for hierarchical clustering: an overview". Related: Clustering overview, DBSCAN.