Hierarchical and Agglomerative Clustering
K-Means requires you to fix K before you start. But what if you genuinely do not know how many clusters exist? Hierarchical clustering builds a tree of nested groupings — a dendrogram — that lets you choose K after the fact by 'cutting' the tree at any level. There are two flavours: agglomerative (bottom-up, merge small clusters into larger ones) and divisive (top-down, split a big cluster into smaller ones). Scikit-learn implements agglomerative clustering, which is the most practical and widely used.
Linkage Criteria — How to Measure Cluster Distances
Once individual points are merged into clusters, we need a rule for the distance between two clusters. Linkage criteria differ in which pair of points they measure. Single linkage uses the minimum distance between any point in cluster A and any point in cluster B — this produces long, chain-like clusters and is susceptible to outlier noise. Complete linkage uses the maximum distance — clusters must be close across their entire extent, producing more compact, sphere-like groups. Average linkage uses the mean of all pairwise distances — a balanced compromise. Ward linkage (the scikit-learn default) minimises the total within-cluster variance when merging — it tends to produce equally sized, compact clusters and is usually the best starting choice.
💡 Linkage rule of thumb: Ward linkage works well for most tabular datasets and is the default in scikit-learn's AgglomerativeClustering. Single linkage is best for detecting outliers or elongated chains. Complete linkage is robust to outliers. Average linkage sits between the two extremes.
Reading a Dendrogram
A dendrogram plots individual samples on the x-axis and merge height (distance at which two clusters merged) on the y-axis. Two leaves joined low on the y-axis are very similar; branches joined high indicate merges between dissimilar groups. To choose K, draw a horizontal cut line at a height where the vertical lines it crosses are long — long vertical lines indicate a big jump in merge distance, suggesting that cutting there separates genuinely different groups. The number of vertical lines the cut crosses equals K.
import numpy as np
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.datasets import make_blobs
from sklearn.preprocessing import StandardScaler
# Simulated player stats dataset (small for dendrogram visibility)
X, _ = make_blobs(n_samples=30, centers=3, cluster_std=0.8, random_state=42)
X = StandardScaler().fit_transform(X)
# Compute linkage matrix using Ward method
Z = linkage(X, method='ward')
# Plot dendrogram
plt.figure(figsize=(14, 5))
dendrogram(Z, leaf_rotation=90, leaf_font_size=9, color_threshold=5.0)
plt.axhline(y=5.0, color='red', linestyle='--', label='Cut line (K=3)')
plt.title('Dendrogram — Agglomerative Clustering (Ward Linkage)')
plt.xlabel('Sample index')
plt.ylabel('Merge distance')
plt.legend()
plt.tight_layout()
plt.show()
print(f"Linkage matrix shape: {Z.shape}")
print(f"Last 5 merges (height):\n{Z[-5:, 2].round(3)}")
AgglomerativeClustering in Scikit-learn
Scikit-learn's `AgglomerativeClustering` follows the standard `fit/predict` API but with one quirk: it does not implement `predict` for new points by default (it is a transductive method — it labels the training points only). If you need to assign new points, you either refit on the full dataset or use a K-Means model initialised from the agglomerative cluster centres. Key parameters are `n_clusters` (K, required unless `distance_threshold` is set), `linkage` (`ward`, `complete`, `average`, `single`), and `metric` (distance measure, defaults to `euclidean`; `ward` requires Euclidean).
from sklearn.cluster import AgglomerativeClustering
from sklearn.datasets import make_blobs
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
import numpy as np
# Simulated IPL player performance data
X, _ = make_blobs(n_samples=150, centers=4, cluster_std=1.0, random_state=7)
X = StandardScaler().fit_transform(X)
# Agglomerative clustering with Ward linkage
agg = AgglomerativeClustering(n_clusters=4, linkage='ward')
labels = agg.fit_predict(X)
print(f"Cluster sizes : {np.bincount(labels)}")
print(f"Silhouette : {silhouette_score(X, labels):.4f}")
# Compare linkage methods
for method in ['ward', 'complete', 'average', 'single']:
if method == 'ward':
agg_tmp = AgglomerativeClustering(n_clusters=4, linkage=method)
else:
agg_tmp = AgglomerativeClustering(n_clusters=4, linkage=method,
metric='euclidean')
lbl = agg_tmp.fit_predict(X)
sil = silhouette_score(X, lbl)
print(f" linkage={method:8s} → silhouette={sil:.4f} sizes={np.bincount(lbl)}")
Distance Threshold Mode — Letting the Data Choose K
Instead of specifying `n_clusters`, you can set `distance_threshold` and leave `n_clusters=None`. The algorithm merges clusters only up to that threshold distance, so the final number of clusters is determined by the data rather than by your choice. This is useful in exploratory analysis when you want a data-driven cut. After fitting, `agg.n_clusters_` tells you how many clusters were formed. Combine this with a dendrogram to pick a sensible threshold visually.
from sklearn.cluster import AgglomerativeClustering
from sklearn.datasets import make_blobs
from sklearn.preprocessing import StandardScaler
import numpy as np
X, _ = make_blobs(n_samples=120, centers=4, cluster_std=0.9, random_state=42)
X = StandardScaler().fit_transform(X)
# Let distance_threshold decide K
agg = AgglomerativeClustering(n_clusters=None, linkage='ward',
distance_threshold=4.5,
compute_full_tree=True)
labels = agg.fit_predict(X)
print(f"Discovered K : {agg.n_clusters_}")
print(f"Cluster sizes : {np.bincount(labels)}")
Comparing K-Means vs Agglomerative
K-Means is faster (O(NKI) vs O(N² log N) for agglomerative) and scales to millions of rows. Agglomerative clustering needs no K upfront, produces an interpretable dendrogram, and handles non-Euclidean distances (with average/complete linkage). For large datasets (N > 10 K), K-Means or Mini-Batch K-Means is usually preferred. For small datasets (N < 5 K) where interpretability matters, agglomerative clustering with a dendrogram often gives better insight. Agglomerative also tends to form more natural-looking clusters when the data is not globular, though DBSCAN (Lesson 27) surpasses both for non-convex shapes.
⚠️ Agglomerative clustering is O(N² log N) in memory and time. On a dataset of 50,000 rows the full linkage matrix alone requires ~10 GB RAM. Use it only for small-to-medium datasets, or subsample large ones before building the dendrogram.
Full Worked Example: Player Archetype Discovery
import numpy as np
import pandas as pd
from sklearn.cluster import AgglomerativeClustering
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
from scipy.cluster.hierarchy import dendrogram, linkage
import matplotlib.pyplot as plt
# Simulated batting + bowling features for 60 T20 players
np.random.seed(99)
n = 60
df = pd.DataFrame({
'batting_avg': np.random.normal(28, 12, n).clip(0),
'strike_rate': np.random.normal(130, 25, n).clip(60),
'bowling_economy': np.random.normal(8.0, 2.0, n).clip(4),
'wickets_per_game':np.random.exponential(0.8, n),
'fielding_rating': np.random.uniform(5, 10, n),
})
# Scale
scaler = StandardScaler()
X = scaler.fit_transform(df.values)
# Dendrogram to pick K
Z = linkage(X, method='ward')
plt.figure(figsize=(16, 4))
dendrogram(Z, leaf_rotation=90, color_threshold=6)
plt.axhline(6, color='red', ls='--', label='Cut → K=4')
plt.title('Player Archetype Dendrogram'); plt.legend(); plt.tight_layout(); plt.show()
# Fit chosen K
agg = AgglomerativeClustering(n_clusters=4, linkage='ward')
df['archetype'] = agg.fit_predict(X)
print("\nMean profile per archetype:")
print(df.groupby('archetype').mean().round(2))
print(f"\nSilhouette: {silhouette_score(X, df['archetype']):.4f}")
- Agglomerative clustering builds a dendrogram by iteratively merging the two closest clusters — no K needed upfront.
- Ward linkage (default) minimises total within-cluster variance and is the best general-purpose choice for tabular data.
- Cut the dendrogram at a height where long vertical lines are crossed — the number of lines crossed equals K.
- Use `distance_threshold` instead of `n_clusters` when you want the data to determine K automatically.
- Agglomerative clustering does not natively predict new points — refit on full data or use K-Means for deployment.
- Prefer K-Means for datasets > 10 K rows; agglomerative clustering suits smaller exploratory analyses where dendrograms add interpretability.