CO4 Topic 14 of 25
Hierarchical clustering and DBSCAN
Two alternatives to K-means. Hierarchical clustering builds a whole tree of groupings, and DBSCAN finds dense regions of any shape and sets outliers aside.
You will be able to
- Explain agglomerative clustering and the linkage choices
- Read a dendrogram and cut it
- Define core, border and noise points
- Choose epsilon from a k-distance plot
- Compare K-means and DBSCAN on non-convex data
Read
- Textbook 1, Ch. 9, pp. 316–328
- Textbook 2, Ch. 14, pp. 501–528
Intuition
K-means needs you to choose k, and it expects round clusters. Hierarchical clustering puts off the choice of k: it merges groups step by step, and you decide later where to cut. DBSCAN thinks differently: a cluster is a crowded region, whatever its shape, and anyone far from a crowd is noise.
How it works
Agglomerative clustering. Start with each point alone, then repeatedly merge the two closest clusters.
| Linkage | Distance between clusters | Character |
|---|---|---|
| Single | Closest pair | Follows chains, sensitive to noise |
| Complete | Farthest pair | Compact clusters |
| Average | Mean of all pairs | In between |
| Ward | Merge that adds least variance | Even-sized, compact; a common default |
A dendrogram shows every merge, and its height is the merge distance. Cut it horizontally, and the number of branches you cross is the number of clusters. A long vertical gap suggests a natural cut.
Worked, single linkage. Points 1, 2, 6, 7, 20. Merge {1,2} at 1, {6,7} at 1, those two groups at 4 (2 to 6), and 20 last at 13 (7 to 20).
DBSCAN has two settings, ε (radius) and minPts:
| Point | Meaning |
|---|---|
| Core | At least minPts points within ε, counting itself |
| Border | Within ε of a core point, but not core |
| Noise | Neither |
Clusters grow by connecting core points that are within ε of each other. It finds the number of clusters by itself.
Choosing ε. Plot each point's distance to its k-th nearest neighbour, sorted. Use the height of the sharp bend.
| K-means | Hierarchical | DBSCAN | |
|---|---|---|---|
| Choose k first? | Yes | At the cut | No |
| Cluster shape | Round | Depends on linkage | Any |
| Marks outliers | No | No | Yes |
| Varying density | Fair | Fair | Poor |
Run it: shapes and students
import numpy as np
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import dendrogram, fcluster, linkage
from sklearn.cluster import DBSCAN, AgglomerativeClustering, KMeans
from sklearn.datasets import make_moons
from sklearn.impute import SimpleImputer
from sklearn.metrics import adjusted_rand_score
from sklearn.neighbors import NearestNeighbors
from sklearn.preprocessing import StandardScaler
from placement_predict import NUMERIC, load_data
X, truth = make_moons(400, noise=0.06, random_state=0)
methods = {"KMeans": KMeans(2, n_init=5, random_state=0), "Ward": AgglomerativeClustering(2, linkage="ward"),
"Single link": AgglomerativeClustering(2, linkage="single"), "DBSCAN": DBSCAN(eps=0.2, min_samples=5)}
print("Interlocking moons, compared with the true grouping (ARI, 1 is perfect)")
fig, ax = plt.subplots(2, 2, figsize=(10, 6))
ax = ax.ravel()
for i, (name, method) in enumerate(methods.items()):
labels = method.fit_predict(X)
print(f" {name:12} ARI = {adjusted_rand_score(truth, labels):.3f}, clusters = {len(set(labels) - {-1})}")
ax[i].scatter(*X.T, c=labels, s=8, cmap="viridis"); ax[i].set_title(name)
plt.tight_layout()
plt.show()
df = load_data(n=1500)
S = StandardScaler().fit_transform(SimpleImputer(strategy="median").fit_transform(df[NUMERIC]))
distances = np.sort(NearestNeighbors(n_neighbors=20).fit(S).kneighbors(S)[0][:, -1])
print(f"\nStudents: 20-NN distance at 50% of points {distances[750]:.2f}, 90% {distances[1350]:.2f}, 99% {distances[1485]:.2f}")
print("eps clusters noise points")
for eps in [1.5, 2.0, 2.5, 3.0]:
labels = DBSCAN(eps=eps, min_samples=20).fit_predict(S)
print(f"{eps:3} {len(set(labels) - {-1}):8} {(labels == -1).sum():12}")
Z = linkage(S[:60], method="ward")
print("\nCutting the small dendrogram into 3 clusters gives sizes:", np.bincount(fcluster(Z, 3, "maxclust"))[1:])
fig, ax = plt.subplots(1, 2, figsize=(11, 3.2))
dendrogram(Z, ax=ax[0], no_labels=True, color_threshold=0.7 * Z[:, 2].max()); ax[0].set(title="Ward dendrogram of 60 students", ylabel="merge distance")
ax[1].plot(distances, color="#0f766e"); ax[1].set(title="k-distance plot", xlabel="points, sorted", ylabel="distance to 20th neighbour")
plt.tight_layout()
plt.show()
On the students, the result depends heavily on ε. Small values give several groups and many noise points, and larger ones give one big crowd with a few outliers. When a result changes this much with one setting, be careful about calling the groups real. DBSCAN is most useful here for spotting unusual records.
Try this: change eps=0.2 in the moons DBSCAN to 0.05, then to 0.5. Explain what breaks each time.
Common mistakes
- Running hierarchical clustering on huge data. It needs at least n² memory for distances, so sample first.
- Forgetting to scale. ε is a distance, so units matter.
- Treating label −1 as a cluster. It means noise.
Problems
- Using the single linkage example above, list the merge heights for complete linkage.
- In 1D with ε = 1 and minPts = 3, classify points 1, 2, 3, 3.9 and 10.
- Why does DBSCAN struggle with clusters of very different density?
- Cut the dendrogram at k = 5. How do the cluster sizes change?
- Plot silhouette scores for Ward at k = 2 to 8 on the student data.
Flashcards
Say the meaning to yourself, then select the card to check.
- Ward linkage
- Merges the pair that adds the least variance.
- Dendrogram
- Tree of merges: cut it to choose the number of clusters.
- Core point
- Has at least minPts points within ε.
- DBSCAN label −1
- Noise.