Unsupervised learning, section 3 of 4, 4h

Clustering, hands on

By the end: You can cluster real data, choose the number of clusters with evidence, pick a method that suits the shape of the data, and judge the result without labels.

  • k-means in a few lines
  • Scaling first
  • Choosing k: elbow and silhouette
  • Density-based clustering for odd shapes
  • Judging clusters by reading them

New to unsupervised learning? Supervised learning needs examples with the right answer attached. Unsupervised learning is what you do when nobody has attached one: you hand over a pile of data and ask what structure is already in it. Clustering groups items that resemble each other; dimensionality reduction squashes data with hundreds of measurements per item down to two, so you can plot it and look at it. Why it matters

Clustering groups records that resemble each other when nobody has labelled anything. It is genuinely useful and easy to misuse, because the algorithm will hand you groups whether or not any exist. This section covers running k-means, why scaling the features first is not optional, choosing the number of groups with evidence rather than by eye, a density-based method for data that does not come in tidy blobs, and judging the result by reading what landed in each group.

k-means#

from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score

X_scaled = StandardScaler().fit_transform(X)       # otherwise the largest-valued feature dominates
for k in range(2, 11):
    labels = KMeans(n_clusters=k, n_init="auto", random_state=0).fit_predict(X_scaled)
    print(k, round(silhouette_score(X_scaled, labels), 3))

k-means places k centres and assigns each point to the nearest, then moves each centre to the middle of its points, repeating until stable. It assumes roughly round clusters of similar size. The silhouette score compares how close points are to their own cluster against the next nearest; higher is better, and it helps choose k.

The scaling line in that snippet is doing more work than it looks. Cluster 300 people on income in euros and age in years without scaling and the distance between two people is, for practical purposes, the difference in their incomes: a gap of 20,000 euros swamps a gap of 30 years. The result is three income bands whose average ages are 39.8, 39.9 and 39.9 — the age column had no effect whatsoever on a result presented as a segmentation by income and age.

import numpy as np
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

rng = np.random.default_rng(0)
income = rng.normal(50_000, 15_000, 300)
age = rng.normal(40, 12, 300)
people = np.c_[income, age]

for name, data in (("unscaled", people), ("scaled", StandardScaler().fit_transform(people))):
    labels = KMeans(n_clusters=3, n_init=10, random_state=0).fit_predict(data)
    summary = [(round(income[labels == c].mean()), round(float(age[labels == c].mean()), 1))
               for c in range(3)]
    print(name, summary)

# unscaled [(67168, 39.8), (47117, 39.9), (29254, 39.9)]
# scaled   [(66167, 41.3), (40840, 49.1), (42458, 28.2)]
Ran as shown. Unscaled, every cluster has the same average age and the segmentation is a income sort in disguise. Scaled, the three groups differ in both dimensions — high earners, older middling earners, younger middling earners — which is a result worth reading.

The n_init argument in that snippet is not decoration either. k-means starts from random centres and only finds a local optimum, so a single unlucky start can leave two centres inside one group and none in another. Run it ten times with different starts and keep the best, which is what n_init does. On six well-separated blobs, ten single-start runs gave inertias of 272, 300, 300, 300, 300, 272, 300, 2649, 3356 and 2350: three of the ten landed somewhere catastrophic, and four more settled ten per cent worse than the best.

from sklearn.datasets import make_blobs

blobs, _ = make_blobs(n_samples=600, centers=6, cluster_std=0.5, random_state=3)

singles = [round(KMeans(n_clusters=6, n_init=1, init="random", random_state=s)
                 .fit(blobs).inertia_, 1) for s in range(10)]
print(singles)          # [299.8, 300.2, 272.3, 2649.1, 299.8, 300.0, 271.9, 3355.7, 299.6, 2350.3]
print(round(KMeans(n_clusters=6, n_init=10, random_state=0).fit(blobs).inertia_, 1))
# 271.9   the best of the ten, which is what n_init gives you
Ran as shown, with purely random starts to make the effect visible. The default in scikit-learn is a smarter initialisation called k-means++ which makes bad starts much rarer, and n_init on top of it is still the reason a clustering is reproducible.

When clusters are not round#

from sklearn.cluster import HDBSCAN

labels = HDBSCAN(min_cluster_size=20).fit_predict(X_scaled)
# -1 means 'noise': points that belong to no dense region

Density-based methods find clusters of any shape and leave outliers unassigned instead of forcing them into a group. You choose a minimum cluster size rather than the number of clusters, which is often easier to reason about.

The two interleaved crescents of the two-moons dataset are the standard case where this matters, and running both methods on it also shows why you cannot pick between them with a score. k-means gets it badly wrong, with an adjusted Rand index against the true grouping of 0.25, and HDBSCAN gets it exactly right, at 1.00. The silhouette scores go the other way: 0.489 for the wrong answer and 0.330 for the right one.

from sklearn.datasets import make_moons
from sklearn.metrics import adjusted_rand_score, silhouette_score

moons, truth = make_moons(n_samples=500, noise=0.06, random_state=0)

km = KMeans(n_clusters=2, n_init=10, random_state=0).fit_predict(moons)
hd = HDBSCAN(min_cluster_size=20).fit_predict(moons)

print(round(adjusted_rand_score(truth, km), 3), round(silhouette_score(moons, km), 3))
# 0.253 0.489      wrong grouping, better silhouette
print(round(adjusted_rand_score(truth, hd), 3), round(silhouette_score(moons, hd), 3))
# 1.0 0.33         correct grouping, worse silhouette
Ran as shown. The silhouette prefers the wrong answer by a wide margin, because it measures roundness and compactness rather than correctness. Any internal score has this problem: it can only reward the shape it was defined around.

The practical rule that falls out of this: use an internal score to compare runs of the same method at different settings, never to choose between methods that make different shape assumptions. Choosing between methods is done by looking at the result.

what your data looks like                   reach for            the setting you choose
round groups, similar sizes, you know k     k-means              k
round groups, you do not know k             k-means + gap stat   k, by comparison with noise
elliptical or overlapping groups            Gaussian mixture     number of components
odd shapes, varying density, real outliers  HDBSCAN              minimum cluster size
you want a hierarchy to cut at any level    agglomerative        linkage, then where to cut
millions of rows                            MiniBatchKMeans      k, and the batch size
The right-hand column is the honest cost of each: every one of these still requires a decision you cannot derive from the data. HDBSCAN's minimum cluster size is usually the easiest of them to reason about, because it is a question about what is worth reporting rather than about geometry.

What to skip here: the dozens of other clustering algorithms, the internal indices beyond silhouette and inertia, and any attempt to prove a clustering correct. Two methods with different assumptions, an honest comparison against noise, and the discipline of naming every cluster is the whole of what this subject gives you in practice.

Judge by reading#

Scores say clusters are separated, not that they mean anything. For each cluster, look at typical members and the features that distinguish it from the rest, and try to name it. Clusters you cannot name are often artefacts of scaling or of the method.

Make that a procedure rather than an intention, because it is the step everyone skips. For each cluster print its size, the five features whose mean differs most from the overall mean in standard deviations, and the three members closest to its centre. Then write a name for it in a sentence, in a document. A cluster you cannot describe in a sentence is not a finding, and the number of clusters you can name is a better answer to the question of how many clusters there are than any score on this page.

def describe(data, labels, feature_names, X_original):
    overall = data.mean(axis=0)
    spread = data.std(axis=0)
    for c in sorted(set(labels) - {-1}):
        members = labels == c
        gap = (data[members].mean(axis=0) - overall) / spread     # in standard deviations
        top = np.argsort(-np.abs(gap))[:5]
        print(f"cluster {c}: {members.sum()} members")
        for i in top:
            print(f"   {feature_names[i]:20} {gap[i]:+.2f} sd")
        centre = data[members].mean(axis=0)
        closest = np.argsort(((data[members] - centre) ** 2).sum(axis=1))[:3]
        print("   typical members:", X_original[members][closest])
Schematic only in its inputs: pass it the scaled array you clustered, the labels, your column names and the unscaled rows to print. The standard-deviation gap is what makes the difference readable — plus 2.3 sd on nights-per-stay names a cluster in a way that a mean of 4.1 does not.

Where people go wrong

Clustering unscaled data. Income in euros and age in years produce clusters that are really just income bands.

If this comes up in an interview

How do you choose the number of clusters?

Try a range and compare internal scores such as silhouette and the elbow in within-cluster variance, then check stability across seeds and subsamples, and above all whether the clusters are interpretable and useful for the purpose. There is rarely one correct k.

Then they push further

When would k-means be the wrong choice?

When clusters are elongated or irregular, very different in size or density, or when there are many outliers. Density-based methods such as HDBSCAN, or Gaussian mixtures for elliptical clusters, fit those better.

Build this: Cluster a customer or product dataset with k-means and HDBSCAN. Choose k with silhouette, name each cluster from its members, and write down one cluster you could not name and why.

This is one section of Unsupervised learning, 16 hours across 4 sections. Read it in full to get the whole topic in order.