Clustering Algorithms — K-Means, DBSCAN & Hierarchical
y to predict against. Section 4 removes that entirely — there's no label, just X, and the job is to find STRUCTURE inside it. This lesson covers three real clustering algorithms: KMeans revisited properly (K-Means++ initialization, the elbow method, and its real limitations), DBSCAN (density-based, decides its own number of clusters, and naturally flags outliers as noise), and AgglomerativeClustering / scipy.cluster.hierarchy.dendrogram (hierarchical clustering and how to actually read a dendrogram). By the end you'll know which one to reach for, and why.
From Predicting to Grouping
Sections 1–3 were all SUPERVISED learning: every row of training data came with a correct answer attached, and every metric (accuracy, RMSE, R²) worked by comparing a prediction against that known answer. Clustering is different in a fundamental way — there is no y_train at all. The algorithm only sees feature rows and has to decide, on its own, which rows "belong together."
That changes what "success" even means. There's no ground-truth cluster label to check predictions against, so evaluating a clustering result (Section 5 of this lesson) works completely differently from evaluating a classifier or regressor.
K-Means Revisited — Centroids, K-Means++, and the Elbow Method
K-Means partitions data into k clusters, each represented by a centroid — the mean position of every point currently assigned to it. The algorithm alternates between two steps until the centroids stop moving: assign every point to its nearest centroid, then recompute each centroid as the mean of its newly assigned points.
from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler X_scaled = StandardScaler().fit_transform(X) kmeans = KMeans(n_clusters=4, init="k-means++", n_init=10, random_state=42) labels = kmeans.fit_predict(X_scaled) print(f"Cluster centers shape: {kmeans.cluster_centers_.shape}") print(f"Inertia (WCSS): {kmeans.inertia_:.1f}") # Cluster centers shape: (4, 2) # Inertia (WCSS): 187.3
k centroids at completely random data points — a bad draw can leave two centroids right next to each other, or none near a real cluster, leading to a poor final result. K-Means++ (the sklearn default, init="k-means++") instead picks the first centroid randomly, then picks each next one with probability proportional to its squared distance from the centroids already chosen — spreading the initial centroids apart on purpose. Combined with n_init=10 (running the whole thing 10 times from different K-Means++ starts and keeping the best-inertia result), this makes landing in a bad local optimum far less likely than plain random initialization.import matplotlib.pyplot as plt inertias = [] k_range = range(1, 11) for k in k_range: km = KMeans(n_clusters=k, init="k-means++", n_init=10, random_state=42) km.fit(X_scaled) inertias.append(km.inertia_) plt.plot(k_range, inertias, marker="o") plt.xlabel("Number of clusters (k)") plt.ylabel("Inertia (WCSS)") plt.title("Elbow Method") plt.show()
Each additional cluster always lowers inertia — with enough clusters, every point could get its own centroid and inertia hits zero. The elbow method looks for the k where adding another cluster stops buying a big inertia improvement — here, the drop from k=3 to k=4 (-49%) is much larger than the drop from k=4 to k=5 (-18%), so k=4 is the illustrative "elbow."
k clusters, even genuine outliers. Both of these are exactly the gaps DBSCAN, next, is built to fill.DBSCAN — Density-Based Clustering
DBSCAN (Density-Based Spatial Clustering of Applications with Noise) takes a completely different approach: instead of measuring distance to a centroid, it looks at local point DENSITY. A cluster is a region where points are packed closely together, separated from other such regions by areas of low density.
eps of each other.eps to count as a dense "core" point.Every point ends up in one of three categories: a core point has at least min_samples neighbors within eps; a border point is within eps of a core point but doesn't have enough neighbors itself; and a noise point is neither — it's isolated, and DBSCAN labels it -1 rather than forcing it into a cluster.
from sklearn.cluster import DBSCAN dbscan = DBSCAN(eps=0.5, min_samples=5) labels = dbscan.fit_predict(X_scaled) n_clusters = len(set(labels)) - (1 if -1 in labels else 0) n_noise = list(labels).count(-1) print(f"Clusters found: {n_clusters}") print(f"Noise points: {n_noise}") # Clusters found: 3 # Noise points: 14
n_clusters isn't even a parameter — it discovers however many dense regions actually exist from eps and min_samples alone. And because it explicitly separates noise from real clusters, it can find arbitrarily shaped (non-spherical, non-convex) clusters that K-Means would slice straight through. The trade-off: DBSCAN's result is sensitive to how eps and min_samples are set, and a single global eps struggles when different regions of the data have very different densities.Hierarchical / Agglomerative Clustering — Dendrograms
Agglomerative clustering builds a hierarchy from the bottom up: it starts with every point as its own cluster, then repeatedly merges the two closest clusters together, one merge at a time, until only one giant cluster remains. That entire merge history is drawn as a dendrogram — a tree diagram where the height of each merge shows how far apart the two things being joined were.
from scipy.cluster.hierarchy import dendrogram, linkage import matplotlib.pyplot as plt Z = linkage(X_scaled, method="ward") plt.figure(figsize=(10, 5)) dendrogram(Z) plt.xlabel("Sample index") plt.ylabel("Distance") plt.title("Hierarchical Clustering Dendrogram") plt.show()
Getting actual cluster assignments back — rather than just a picture — is what sklearn.cluster.AgglomerativeClustering is for. It runs the same bottom-up merging internally, and returns labels for a chosen number of clusters directly, without a separate dendrogram-reading step.
from sklearn.cluster import AgglomerativeClustering agg = AgglomerativeClustering(n_clusters=4, linkage="ward") labels = agg.fit_predict(X_scaled) print(f"Cluster sizes: {[list(labels).count(i) for i in set(labels)]}") # Cluster sizes: [12, 9, 15, 6]
linkage="ward" (used above) merges whichever pair of clusters increases total within-cluster variance the least — it tends to produce compact, evenly-sized clusters and is a common default. "complete" uses the maximum distance between any two points in the two clusters (tends toward tight, compact clusters). "average" uses the mean pairwise distance. "single" uses the minimum distance (can chain long, straggly clusters together). Both scipy.cluster.hierarchy.linkage and AgglomerativeClustering accept the same linkage choices.Evaluating Clusters Without Labels — Silhouette Score
Every metric in Section 3 of this course (accuracy, F1, RMSE, R²) needed a true label to compare against. Clustering has none — so a different kind of metric is needed, one that judges cluster quality using only the data and the assigned labels themselves.
from sklearn.metrics import silhouette_score score = silhouette_score(X_scaled, labels) print(f"Silhouette score: {score:.3f}") # Silhouette score: 0.612 -> reasonably well-separated, compact clusters
silhouette_score can be computed for every candidate k in a loop, and the k with the HIGHEST average silhouette score is a strong candidate for the "right" number of clusters. Unlike inertia, silhouette score doesn't automatically improve as k grows, so it doesn't need an "elbow" to be read visually — the single best value is usually clear. It works for DBSCAN and AgglomerativeClustering results too, as long as noise points (label -1) are excluded first.Choosing the Right Algorithm
All three algorithms cluster the same kind of data, but they make different trade-offs — the right choice depends on what's known about the data going in.
| Algorithm | Needs k upfront? | Non-spherical shapes? | Handles noise/outliers? | Scales to large n? |
|---|---|---|---|---|
| KMeans | Yes | No — assumes convex clusters | No — every point assigned | Yes, scales well |
| DBSCAN | No — finds it automatically | Yes | Yes — labels noise as -1 | Moderate — sensitive to eps |
| Agglomerative | Optional — cut the dendrogram | Depends on linkage | No — every point assigned | Poor on very large n |
StandardScaler before clustering — as every code example in this lesson already does — isn't optional.Lesson Summary
Use any numeric dataset with at least 2 features — a scaled subset of a dataset from an earlier lesson works fine, or generate one with sklearn.datasets.make_blobs.
Loop
k from 1 to 10, record inertia for each, and plot the elbow curve from Section 2. Pick a k, fit a final KMeans(init="k-means++", n_init=10), and print silhouette_score for the result.
Fit
DBSCAN on the same scaled data with a couple of different eps values (try values both smaller and larger than your first guess). How does the number of clusters and the number of noise points (-1 labels) change as eps grows?
Use
scipy.cluster.hierarchy.linkage and dendrogram to visualize the same data. Then fit AgglomerativeClustering with the same number of clusters K-Means used. Do the three algorithms broadly agree on which points belong together, or do they disagree? Write 2–3 sentences on what you saw.
💡 Show hints if you're stuck
- Task 1:
from sklearn.datasets import make_blobs; X, _ = make_blobs(n_samples=300, centers=4, random_state=42)generates a quick clusterable toy dataset if you don't have one handy. - Task 2: A very small
epstends to label almost everything as noise; a very largeepstends to merge everything into one giant cluster. The "right" value is usually somewhere in between. - Task 3: Comparing cluster assignments across algorithms directly is easiest with
pd.crosstab(kmeans_labels, agg_labels)— a strong diagonal means the two algorithms mostly agree.