Everything so far has been supervised: the data carried a label we learned to predict. Unsupervised learning removes the label entirely and asks the algorithm to discover structure on its own. This chapter covers the two pillars of unsupervised work — dimensionality reduction (led by PCA) and clustering (K-means, hierarchical, and DBSCAN) — plus the curse of dimensionality that motivates both.
In supervised learning the target guides the model. In unsupervised learning there is no target; the algorithm must find patterns from the input alone. Two broad families exist:
Unsupervised methods are also the standard pre-processing companions to supervised pipelines — clusters can become new features, and reduced dimensions can become the input to a classifier.
Intuition built in 2D and 3D fails as the number of features grows. Consider a unit cube in d dimensions: the fraction of its volume near the boundary grows explosively with d. Similarly, in high-dimensional space almost every point is far from every other point, so distances stop being informative and "nearest" loses meaning.
The remedy is to keep only the features that matter, either by selecting a subset of the originals or by extracting new, smaller features from them.
Both are dimensionality reduction; they differ in whether the surviving features are original or synthesized.
Principal Component Analysis (PCA) rotates the data to a new coordinate system aligned with its directions of greatest spread, then keeps only the first few coordinates.
Picture a scatter of points shaped like a stretched ellipse. PCA finds the ellipse's long axis (component 1) and short axis (component 2). Dropping component 2 collapses the ellipse onto a line, losing little because the short axis held little spread.
Formally, PCA seeks a unit vector w such that projecting every point onto w maximizes the variance of the projected values. The projection of a point x onto w is the scalar dot product x . w, and the variance of those projections over the dataset is:
Var(proj) = w.T * S * w
where S is the covariance matrix of the centered data and w.T is the transpose of w. Maximizing this variance subject to w having unit length turns out to be an eigen-problem: the optimal w is the eigenvector of S belonging to its largest eigenvalue. The eigenvalue tells you how much variance that component explains.
The recipe for PCA is short:
S of the centered data.S into eigenvectors w1, w2, ..., wk and eigenvalues l1, l2, ..., lk.m eigenvectors are the first m principal components.m vectors to get the reduced representation.The ratio l1 / sum(l) tells you what fraction of the total variance the first component captures. In practice you keep enough components to cover 90–95% of the total variance.
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
pca = PCA(n_components=2) # keep two components
X_reduced = pca.fit_transform(X_scaled)
print(pca.explained_variance_ratio_) # share of variance per component
Scale the features before PCA — otherwise a feature measured in large units dominates the covariance matrix regardless of how informative it is. The reduced two-column matrix can now be plotted directly as a scatter of points.
K-means partitions the data into K clusters, each represented by its center (the mean of its points). The algorithm alternates two steps until nothing moves:
Geometrically you are sliding cluster centers around until each center sits at the "center of mass" of its territory. The final clusters are convex regions bounded by straight lines — K-means cannot find crescent or donut shapes.
K-means needs K as input, and choosing K is part of the job.
K-means starts from random centers, and a bad start can park a center in an empty region or leave two centers fighting over one cluster while another cluster is ignored. The result is a poor local minimum. Two defenses exist:
init="k-means++") in scikit-learn.from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=3, init="k-means++", n_init=10, random_state=42)
labels = kmeans.fit_predict(X_scaled)
print(kmeans.inertia_) # total within-cluster distance
print(kmeans.cluster_centers_) # the final centers
The labels array holds each point's cluster id, ready for analysis, plotting, or as a new categorical feature.
Hierarchical clustering builds a tree of clusters rather than a flat partition. There are two directions:
The tree of merges is called a dendrogram. To get a flat partition, cut the dendrogram horizontally at some height — clusters below the cut become the final groups. The choice of "distance between clusters" (linkage) matters: single linkage joins by the closest pair (good for elongated shapes, sensitive to noise), complete linkage by the farthest pair (compact clusters), and Ward's method by the increase in within-cluster variance.
from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.cluster import AgglomerativeClustering
agg = AgglomerativeClustering(n_clusters=3, linkage="ward")
labels = agg.fit_predict(X_scaled)
Z = linkage(X_scaled, method="ward") # for the dendrogram
dendrogram(Z)
Read the dendrogram from the bottom up: tall vertical gaps between groups indicate natural separations; the height where you cut determines the number of clusters.
DBSCAN (Density-Based Spatial Clustering of Applications with Noise) defines clusters by density instead of center distance. Two parameters control it:
eps: the radius of the neighborhood examined around each point.min_samples: how many points must live inside that radius for the point to count as a core point.The algorithm labels every point as core, border, or noise. Core points have at least min_samples neighbors within eps; border points are within eps of a core point but have too few neighbors; everything else is noise. A cluster is the set of points reachable by hopping between core points, together with their borders.
Because DBSCAN follows density, it finds shapes that center-based methods cannot:
Noise points are returned with the label -1, which makes DBSCAN a natural tool for anomaly detection (the next chapter uses this trick).
eps is hard to choose and is sensitive to scaling — standardize features first; fails when clusters have very different densities, because one eps cannot serve both; needs more data to discover small clusters.from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler
X_std = StandardScaler().fit_transform(X)
db = DBSCAN(eps=0.5, min_samples=5)
labels = db.fit_predict(X_std)
print(set(labels)) # a -1 label marks noise points
Try a small range of eps values and check how many points become noise and how many clusters appear; eps around 0.3–0.6 is a sensible starting zone for standardized data.
The silhouette score judges a clustering without any labels. For each point i let a(i) be the mean distance to the other points in its own cluster and b(i) the mean distance to the points of the nearest other cluster. The silhouette of point i is:
s(i) = (b(i) - a(i)) / max(a(i), b(i))
s(i) ~ 1: the point is much closer to its own cluster than to any other — perfect placement.s(i) ~ 0: the point sits on the boundary between two clusters.s(i) < 0: the point is probably in the wrong cluster.Averaging s(i) over all points gives a single number you can use to compare different K values (K-means), different linkages, or different eps values (DBSCAN), independent of the algorithm's own internal loss.
sklearn.datasets.make_blobs and make_moons. Run K-means, AgglomerativeClustering, and DBSCAN on both. Which algorithm recovers the true shapes of the moons, and which one fails? Compute the silhouette score for each result and explain whether the score agrees with what your eyes tell you.