- Execute the k-means algorithm by hand and explain why it converges to a local minimum of the within-cluster sum of squares
- Choose the number of clusters using the elbow method, silhouette scores and domain constraints
- Compare k-means, hierarchical clustering, DBSCAN and Gaussian mixture models by the cluster shapes and data conditions each handles
- Derive principal component analysis as variance maximization and connect it to the eigendecomposition of the covariance matrix and the SVD
- Interpret explained-variance ratios to choose how many principal components to keep
- Explain what t-SNE and UMAP preserve and identify the common misreadings of their plots
Before this: The ML Workflow: Framing, Data, Splits and Leakage, Linear Algebra for AI, Probability and Statistics for AI
Most of the world's data has no labels. Customer logs, sensor streams, documents and images arrive without anyone saying which group they belong to or which features matter. Unsupervised learning finds structure in that data on its own: clustering groups similar points, and dimensionality reduction compresses many features into a few that keep the important variation. Practitioners use these tools for customer segmentation, anomaly screening, compressing features before a supervised model, deduplicating data, and, very often, simply looking at a high-dimensional dataset before deciding what to do with it.
Intuition: sorting a pile without instructions
Imagine someone dumps a thousand photos on a table and asks you to "organize them." Nobody tells you the categories. You might group them by who is in them, by location, or by colour. Each choice is a different notion of similarity, and each produces a different, equally valid grouping. That is the central fact of unsupervised learning: the algorithm finds the structure that its similarity measure and assumptions can see. There is no ground truth to score against, so judgment about the distance metric, the scaling of features, and the shape of clusters you expect matters more than in supervised learning.
Dimensionality reduction is the same pile viewed differently. If every photo is described by a million pixel values, but most of the variation comes from a few factors (lighting, pose, who is in the frame), you can describe each photo with a handful of numbers and lose little. PCA finds the directions of greatest variation; t-SNE and UMAP try to keep neighbours close in a 2D picture.
k-means clustering
The objective
Given points and a number of clusters , k-means looks for cluster assignments and centroids that minimize the within-cluster sum of squares (WCSS, called inertia in scikit-learn):
Finding the global minimum is NP-hard in general, so we use Lloyd's algorithm, which alternates two steps:
- Assignment step: give each point to its nearest centroid, .
- Update step: move each centroid to the mean of its assigned points, , where is the set of points assigned to cluster .
Each step can only lower or leave it unchanged: the assignment step picks the best centroid for each point given fixed centroids, and the mean is the point that minimizes squared distance to a set. Since there are finitely many assignments, the algorithm stops after a finite number of iterations, but only at a local minimum. That is why implementations run several random restarts (n_init) and keep the best, and why k-means++ initialization (pick each new initial centroid with probability proportional to its squared distance from the nearest centroid already chosen) is the standard default.
Worked iteration
Six points in 2D: , , , , , . Use with a deliberately poor start: , .
Iteration 1, assignment. Squared distances to :
| Point | to | to | Cluster |
|---|---|---|---|
| A (1,1) | 0 | 1 | 1 |
| B (2,1) | 1 | 0 | 2 |
| C (1,2) | 1 | 2 | 1 |
| D (5,4) | 25 | 18 | 2 |
| E (6,5) | 41 | 32 | 2 |
| F (5,6) | 41 | 34 | 2 |
.
Iteration 1, update. . .
Iteration 2, assignment. For B: , , so B moves to cluster 1. A, C stay in cluster 1; D, E, F stay in cluster 2. With these assignments, evaluated against the current centroids, .
Iteration 2, update. , .
Iteration 3. No point changes cluster, so the algorithm has converged. Final . The objective fell , exactly as the monotonicity argument promises.
Choosing k
There is no universally correct ; use several signals together.
- Elbow method. Plot against . The best achievable never increases as grows (with distinct points it is zero), so look for the "elbow" where extra clusters stop buying much reduction. Elbows are often ambiguous.
- Silhouette score. For point , let be its mean distance to other points in its own cluster and the mean distance to points in the nearest other cluster. Then Values near 1 mean well-separated, near 0 mean on a boundary, negative means probably misassigned. The mean silhouette over all points is a standard model-selection score. In the example above, for A: , , so .
- Gap statistic, BIC (for mixtures), stability across resamples, and most importantly the use case: a marketing team that can run four campaigns needs four segments, whatever the elbow says.
import numpy as np
from sklearn.datasets import make_blobs
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
X, _ = make_blobs(n_samples=600, centers=4, cluster_std=0.8, random_state=0)
X = StandardScaler().fit_transform(X) # k-means is distance-based: scale first
for k in range(2, 8):
km = KMeans(n_clusters=k, n_init=10, random_state=0).fit(X)
print(k, round(km.inertia_, 1), round(silhouette_score(X, km.labels_), 3))
The loop prints inertia and mean silhouette for each ; on this synthetic data the silhouette peaks at the true value of 4. Note StandardScaler: k-means uses Euclidean distance, so a feature measured in dollars will swamp one measured in years unless you scale.
What k-means assumes
k-means implicitly assumes clusters are roughly spherical, similar in size and similar in spread, because it assigns by distance to a single centre. It fails on elongated clusters, nested rings, and clusters of very different densities, and it must assign every point, including outliers, which drag centroids around.
Hierarchical clustering
Agglomerative clustering starts with every point as its own cluster and repeatedly merges the two closest clusters until one remains. The full merge history is a tree called a dendrogram; cutting it at a height gives a flat clustering, so you can inspect many values of from one run. The key choice is the linkage, the definition of distance between clusters:
| Linkage | Distance between clusters | Behaviour |
|---|---|---|
| Single | closest pair of points | finds chained, irregular shapes; prone to "chaining" through noise |
| Complete | farthest pair of points | compact clusters of similar diameter |
| Average | mean of all pairwise distances | a compromise |
| Ward | increase in total within-cluster variance from merging | similar to k-means; the usual default |
Agglomerative clustering needs pairwise distances, so naive implementations cost memory and at least time; it is practical for thousands to tens of thousands of points, not millions. Its advantages are the dendrogram, the freedom to use any distance (for example edit distance between strings), and determinism.
DBSCAN: density-based clustering
DBSCAN (Ester, Kriegel, Sander and Xu, 1996) defines clusters as dense regions separated by sparse ones. It has two parameters: a radius and a count min_samples. A core point has at least min_samples points within distance (counting itself in scikit-learn). Clusters are formed by connecting core points that lie within of one another, plus the non-core border points within reach of them. Everything else is labelled noise (label -1 in scikit-learn).
Strengths: no need to choose , arbitrary cluster shapes (rings, crescents), and explicit outlier detection. Weaknesses: one global cannot fit clusters of very different densities, and choosing is fiddly. A common heuristic is the k-distance plot: sort every point's distance to its min_samples-th nearest neighbour and pick near the knee. HDBSCAN, available as sklearn.cluster.HDBSCAN in recent scikit-learn versions, removes the single- limitation by building a hierarchy over density levels and keeping the most stable clusters; it is usually the better first try for density-based clustering.
Gaussian mixture models
A Gaussian mixture model (GMM) is a probabilistic model that says each point was generated by first picking a component with probability , then drawing from a Gaussian :
It is fitted with the expectation-maximization (EM) algorithm. The E-step computes each point's responsibility , a soft assignment. The M-step re-estimates , , as responsibility-weighted averages. Like k-means it converges to a local optimum of its objective (here the log-likelihood). In fact k-means is the limiting case of a GMM with equal, spherical covariances shrinking to zero, so soft assignments become hard.
GMMs add three things k-means lacks: soft membership (a customer can be 70% one segment), elliptical clusters of different sizes and orientations via full covariance matrices, and a likelihood, which allows model selection with the Bayesian information criterion (BIC) and lets you flag low-density points as anomalies. The price is more parameters (a full covariance has entries per component), so in high dimensions use covariance_type="diag" or reduce dimensionality first.
Dimensionality reduction with PCA
Derivation
Centre the data so each feature has mean zero, and stack it in a matrix . The sample covariance matrix is (many texts use ; the directions found are identical). We want the unit vector such that projecting the data onto it, , has maximum variance:
Add a Lagrange multiplier : . Setting the gradient with respect to to zero gives , that is
So must be an eigenvector of , and the variance it captures is . The best direction is the eigenvector with the largest eigenvalue; this is the first principal component. Repeating the argument with the extra constraint that each new direction is orthogonal to the earlier ones yields the remaining eigenvectors in decreasing order of eigenvalue. Because is symmetric and positive semidefinite, its eigenvectors can be chosen orthogonal and its eigenvalues are real and non-negative.
The SVD route. Write the centred data as (singular value decomposition). Then , so the columns of are the principal directions and the eigenvalues are . Libraries compute PCA through the SVD of rather than forming , because it is more numerically stable; for very large data they use randomized SVD.
Explained variance
The fraction of total variance captured by component is
A common rule is to keep the smallest number of components whose cumulative ratio passes a target such as 90% or 95%, or to look for an elbow in the scree plot of .
Worked example
Four centred points: . Using :
- , ,
- .
So . The characteristic equation gives , so , . The eigenvectors are and . The first component explains of the variance. Projecting onto gives ; projecting gives , because that point lies entirely along .
import numpy as np
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
X, y = load_digits(return_X_y=True) # 1,797 images, 64 pixel features
pca = PCA().fit(X)
cum = np.cumsum(pca.explained_variance_ratio_)
k95 = int(np.argmax(cum >= 0.95)) + 1
print(f"components for 95% variance: {k95} of {X.shape[1]}")
Z = PCA(n_components=k95).fit_transform(X) # compressed features
print(Z.shape)
On the digits dataset roughly 30 of 64 components keep 95% of the variance (the exact count prints when you run it). Two cautions: PCA is scale-sensitive, so standardize features measured in different units, and PCA is linear, so it cannot unroll curved structure.
t-SNE and UMAP: visualization, not measurement
t-SNE (van der Maaten and Hinton, 2008) converts distances in the original space into neighbour probabilities, using a Gaussian around each point whose width is set by the perplexity (roughly the effective number of neighbours, commonly 5 to 50). It then places points in 2D so that a heavy-tailed Student-t distribution over 2D distances matches those probabilities, minimizing the KL divergence between the two. The heavy tail lets moderately distant points spread apart, which produces the characteristic well-separated islands.
UMAP (McInnes, Healy and Melville, 2018) builds a weighted k-nearest-neighbour graph, interprets it as a fuzzy topological structure, and optimizes a low-dimensional layout with a cross-entropy objective. Its main knobs are n_neighbors (local versus global emphasis) and min_dist (how tightly points pack). It is usually faster than exact t-SNE, scales to millions of points, can transform new data, and tends to keep somewhat more global arrangement, although neither method guarantees global geometry.
Both are excellent for looking: spotting that labelled classes separate, finding mislabelled examples, seeing whether embeddings from a model group by topic. Both are dangerous for measuring:
- Cluster sizes mean little. t-SNE expands dense clusters and contracts sparse ones.
- Distances between clusters mean little. Two islands far apart on the plot may not be far apart in the data.
- Hyperparameters change the picture. Different perplexities or
n_neighborsvalues can produce visibly different layouts from the same data, and random noise can form apparent clusters at low perplexity. - Runs differ. Different seeds give different layouts. Always try several.
- Do not cluster on the 2D output and present the clusters as findings; cluster in the original or PCA space and use the plot to illustrate.
A standard recipe is PCA to about 50 dimensions first (removes noise and speeds things up), then t-SNE or UMAP to 2D.
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
X, y = load_digits(return_X_y=True)
X50 = PCA(n_components=50, random_state=0).fit_transform(X)
for perp in (5, 30, 50): # compare several perplexities
emb = TSNE(n_components=2, perplexity=perp, init="pca",
random_state=0).fit_transform(X50)
print(perp, emb.shape) # plot emb coloured by y
UMAP lives in the separate umap-learn package (import umap; umap.UMAP(n_neighbors=15, min_dist=0.1).fit_transform(X50)).
Evaluating clusters
Without labels, use internal metrics: silhouette, Davies-Bouldin (lower is better), Calinski-Harabasz (higher is better). They reward compact, separated clusters and therefore favour k-means-like shapes, so they can mislead for DBSCAN on non-convex data. When some labels exist, external metrics such as the adjusted Rand index and normalized mutual information compare a clustering with them. The most important check is qualitative: inspect samples from each cluster and ask whether a domain expert would find the grouping useful, and test stability by re-running on bootstrap samples.
Common pitfalls
- Not scaling features before k-means, hierarchical clustering, DBSCAN or PCA, so the feature with the biggest units dominates.
- Trusting one k-means run. Use multiple initializations (
n_init) and a fixedrandom_statefor reproducibility. - Forcing k-means onto non-spherical data. Rings and crescents need DBSCAN, HDBSCAN, spectral clustering or a GMM.
- Treating the elbow as objective. It is a heuristic; combine it with silhouette, stability and business constraints.
- Reading t-SNE or UMAP plots as maps. Cluster sizes and inter-cluster gaps are not reliable quantities.
- Fitting PCA on all data before a train/test split. PCA is a learned transformation; fit it on training data only, ideally inside a pipeline.
- High-dimensional distances. In hundreds of dimensions, distances concentrate and nearest neighbours become less meaningful. Reduce dimensions or use a representation (such as learned embeddings with cosine similarity) built for the task.
In practice
- Start by looking. PCA to 2 components and a UMAP or t-SNE plot are cheap first diagnostics on any new dataset or set of embeddings.
- Segmentation: standardize, try k-means for a few values with silhouette scores, then check with a GMM if soft membership or elliptical segments make sense. Profile each segment with feature means so stakeholders can name it.
- Anomaly screening: DBSCAN/HDBSCAN noise labels and low GMM likelihoods are useful first-pass flags, covered further in the anomaly detection lesson.
- Embeddings: modern pipelines cluster vectors from neural encoders (text or image embeddings). Normalize them and use cosine distance (for k-means, L2-normalizing first makes Euclidean distance equivalent in ranking to cosine).
- Compression: PCA before a supervised model can remove noise and collinearity, but tree ensembles rarely benefit; test it with cross-validation rather than assuming.
- Scale: for millions of points use
MiniBatchKMeans, randomized PCA, and approximate nearest-neighbour libraries.
Key takeaways
- k-means alternates nearest-centroid assignment and mean updates, monotonically reducing within-cluster sum of squares to a local minimum, so use k-means++ and several restarts.
- Choose the number of clusters with several signals (elbow, silhouette, BIC, stability) plus the business constraint, never a single heuristic.
- Hierarchical clustering gives a dendrogram for any k, DBSCAN/HDBSCAN finds arbitrary shapes and labels noise, and Gaussian mixtures give soft, elliptical, likelihood-based clusters.
- PCA finds orthogonal directions of maximum variance: the eigenvectors of the covariance matrix, computed in practice through the SVD, with eigenvalues giving explained variance.
- t-SNE and UMAP are visualization tools whose cluster sizes and inter-cluster distances are not trustworthy measurements.
- Distance-based methods require feature scaling, and learned transforms like PCA must be fitted on training data only.
Terms in this lesson
- Inertia (WCSS)
- The sum of squared distances from each point to its assigned cluster centroid, the objective k-means minimizes.
- k-means++
- An initialization that picks each new starting centroid with probability proportional to its squared distance from the nearest centroid already chosen.
- Silhouette coefficient
- For a point, (b - a) / max(a, b), where a is its mean distance to its own cluster and b its mean distance to the nearest other cluster.
- Dendrogram
- A tree recording the sequence and heights of merges in hierarchical clustering, which can be cut at any level to produce a flat clustering.
- Core point
- In DBSCAN, a point with at least min_samples points within radius epsilon.
- Responsibility
- In a Gaussian mixture, the posterior probability that a given point was generated by a given component.
- Principal component
- A unit direction of maximal remaining variance, equal to an eigenvector of the data covariance matrix.
- Explained variance ratio
- A component's eigenvalue divided by the sum of all eigenvalues, the share of total variance it captures.
- Perplexity
- The t-SNE parameter that sets each point's Gaussian neighbourhood width, roughly the effective number of neighbours considered.
Check your understanding
- 1.
Why does Lloyd's k-means algorithm always terminate?
- 2.
Your data contains two concentric rings plus scattered noise points. Which method is the best first choice?
- 3.
A covariance matrix has eigenvalues 6, 3, 1. What fraction of variance do the first two principal components explain?
- 4.
On a t-SNE plot, cluster A appears three times larger than cluster B and far from cluster C. What can you safely conclude?
- 5.
For a point, the mean distance to its own cluster is 2 and to the nearest other cluster is 5. What is its silhouette value?
Exercises
Work each problem before opening the solution.
- Exercise 1Core
Run k-means by hand on the 1D points with and initial centroids , . Show each assignment and update step until convergence and report the final inertia.
Show solution
Iteration 1. Assign: 1 to (distance 0 vs 2); 2 is equidistant (1 vs 1) so break ties toward the lower index, cluster 1; 3, 10, 11, 20 go to . Update: , .
Iteration 2. Assign: 1, 2 to ; 3 is 1.5 from and 8 from , so cluster 1; 10, 11, 20 to . Update: , .
Iteration 3. Assign: 1, 2, 3 to ; 10 is 8 from and 3.67 from , cluster 2; 11 and 20 cluster 2. No change, so converged.
Inertia: cluster 1: . Cluster 2: (exactly ). Total .
Note that 20 is an outlier that pulls up; a 3-cluster solution {1,2,3}, {10,11}, {20} would have inertia 2.5, which illustrates why inertia always falls as k grows.
- Exercise 2Core
Compute the principal components and explained-variance ratios of the centred 2D data using the covariance. Then project the point onto the first component.
Show solution
. . .
is already diagonal, so the eigenvectors are the axes: with and with .
Explained variance ratios: and .
Projection of onto : . When features are already uncorrelated, PCA simply sorts them by variance, which is why scaling matters: if you standardized both features, each would have variance 1 and PCA would have no preferred direction.
- Exercise 3Core
Write scikit-learn code that compares k-means (k=2) and DBSCAN on
make_moons(n_samples=500, noise=0.06, random_state=0)using the adjusted Rand index against the true labels. Explain the result you expect.Show solution
from sklearn.datasets import make_moons from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans, DBSCAN from sklearn.metrics import adjusted_rand_score X, y = make_moons(n_samples=500, noise=0.06, random_state=0) X = StandardScaler().fit_transform(X) km = KMeans(n_clusters=2, n_init=10, random_state=0).fit_predict(X) db = DBSCAN(eps=0.3, min_samples=5).fit_predict(X) print("k-means ARI:", round(adjusted_rand_score(y, km), 3)) print("DBSCAN ARI:", round(adjusted_rand_score(y, db), 3)) print("DBSCAN clusters:", sorted(set(db)))Expected: k-means splits the moons with a roughly straight boundary, cutting each crescent, so its ARI is well below 1 (typically around 0.2 to 0.5). DBSCAN follows each dense crescent and should recover them almost perfectly (ARI near 1), possibly labelling a few points as noise (-1). If DBSCAN returns one cluster, eps is too large; if it returns many small clusters or lots of noise, eps is too small; a k-distance plot helps choose it.
- Exercise 4Challenge
Show that for a fixed set of points , the point minimizing is the mean, and use this to prove the k-means update step never increases the objective. Then explain why a Gaussian mixture with shared covariance reduces to k-means as .
Show solution
Minimizer. . Gradient: . Setting it to zero gives . The Hessian is , positive definite, so this is the unique global minimum.
Update step. The objective decomposes as . With assignments fixed, each inner sum depends only on , and replacing with the mean of minimizes it. So each term cannot increase, hence cannot increase.
GMM limit. With equal weights and covariance , the responsibility is . Dividing numerator and denominator by the term for the nearest centroid, every other term contains , which goes to 0 as when . So for the nearest centroid and 0 otherwise: the E-step becomes the hard assignment step. The M-step for means with 0/1 responsibilities is the cluster mean, which is the k-means update.
Further reading
- bookJames, Witten, Hastie, Tibshirani (and Taylor for the Python edition), An Introduction to Statistical Learning, Chapter 12: Unsupervised Learning (2nd ed. 2021; Python ed. 2023)
- papervan der Maaten & Hinton, Visualizing Data using t-SNE (JMLR, 2008)
- paperMcInnes, Healy & Melville, UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction (2018)
- articleWattenberg, Viégas & Johnson, How to Use t-SNE Effectively (Distill, 2016)
- docsscikit-learn API reference: sklearn.cluster
Go further
Sources
Fact-checked and last reviewed 2026-11-05