🧬 Section 4 · Unsupervised Learning 🟡 Intermediate MODULE 17

Clustering Algorithms — K-Means, DBSCAN & Hierarchical

⏱️ 32 min read
📖 K-Means++, DBSCAN & Hierarchical Clustering
🧩 4 Quiz Questions
🏗️ 1 Challenge · 3 tasks
Your progress in Section 425%
🎯 What you'll learn: Every model since Lesson 1 had a target 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.

🛍️
Customer Segmentation
Group customers by behavior for targeted marketing — this section's own capstone project.
🗂️
Data Exploration
Discover natural groupings in a new dataset before deciding how to model it.
🚨
Outlier Discovery
DBSCAN's "noise" points are a preview of Lesson 19's dedicated anomaly-detection lesson.
📝
If K-Means already looks familiar, that's fine
K-Means is often the very first unsupervised algorithm people meet in an introductory statistics or data-science course, so Section 2 below moves quickly through the basic loop and spends most of its time on two things a first pass usually skips: K-Means++ initialization and exactly where K-Means breaks.

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.

What K-Means minimizes — inertia / within-cluster sum of squares (WCSS) inertia  =  Σ_k  Σ_{x ∈ cluster k}  ‖x - center_k‖² The sum, over every cluster, of the squared distance from each point to its own cluster's centroid. Lower inertia means tighter, more compact clusters.
kmeans_basic.py
PYTHON
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
What K-Means++ actually changes about initialization
Plain K-Means starts by dropping 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.
elbow_method.py
PYTHON
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()
Illustrative inertia by k — the "elbow" is where the drop flattens
k = 1
960
baseline
k = 2
598
-38%
k = 3
367
-39%
k = 4
187
-49% ⬅ elbow
k = 5
154
-18%
k = 6
130
-15%

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."

⚠️
Where K-Means actually breaks
K-Means implicitly assumes clusters are roughly convex and spherical, with comparable size and density — because it only ever measures distance to a single centroid point. That assumption fails on elongated, crescent-shaped, or nested-ring clusters, and on clusters of very different densities: K-Means will confidently draw a straight-line boundary through the middle of a shape it can't represent well. It also has no concept of "noise" — every single point gets forced into one of the 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 (ε)
The radius of the neighborhood searched around each point. Two points are "neighbors" if they're within eps of each other.
🔢
min_samples
The minimum number of neighbors (including itself) a point needs within 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.

dbscan_example.py
PYTHON
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
Illustrative DBSCAN result — core, border, and noise points
Two dense regions form clusters; scattered points that never reach min_samples neighbors are labeled -1 (noise), shown in red.
Cluster 0
Cluster 1
Noise (label -1)
The two things K-Means can't do that DBSCAN does natively
DBSCAN never asks for a number of clusters up front — 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.

dendrogram_scipy.py
PYTHON
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()
Illustrative dendrogram — 8 samples merging bottom-up
Leaves at the bottom are individual points. Each U-shape is a merge; height = distance at which that merge happened. Cutting horizontally (dashed red line) at a chosen height reads off a cluster count — cutting here gives 4 clusters.
cut → 4 clusters

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.

agglomerative_sklearn.py
PYTHON
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 decides HOW "distance between two clusters" is measured
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.

Silhouette score, for a single point i s(i)  =  (b(i) - a(i)) / max(a(i), b(i)) a(i) = mean distance from i to other points in its OWN cluster (lower is tighter). b(i) = mean distance from i to points in the NEAREST other cluster (higher is more separated). Averaged over every point, the score ranges from -1 to +1.
silhouette_score.py
PYTHON
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
🟢
Close to +1
Points sit well inside their own cluster and far from neighboring ones — a strong clustering.
🟡
Close to 0
Points sit near the boundary between two clusters — ambiguous, overlapping structure.
🔴
Negative
Points are, on average, closer to a different cluster than their own — likely mislabeled.
Silhouette score can pick k too — a second opinion alongside the elbow
Just like Section 2's elbow loop, 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.

K-Means vs. DBSCAN vs. Hierarchical / Agglomerative
AlgorithmNeeds k upfront?Non-spherical shapes?Handles noise/outliers?Scales to large n?
KMeansYesNo — assumes convex clustersNo — every point assignedYes, scales well
DBSCANNo — finds it automaticallyYesYes — labels noise as -1Moderate — sensitive to eps
AgglomerativeOptional — cut the dendrogramDepends on linkageNo — every point assignedPoor on very large n
Pick K-Means
Roughly round, evenly-sized clusters, a large dataset, and speed matters.
🌫️
Pick DBSCAN
Unknown cluster count, irregular shapes, and outliers need to be flagged rather than forced in.
🌳
Pick Hierarchical
A small-to-medium dataset where seeing the FULL merge hierarchy (the dendrogram itself) is valuable, not just one flat answer.
⚠️
All three still need scaled features
K-Means, DBSCAN, and Agglomerative Clustering are all distance-based, exactly like KNN and SVM from Section 2 of this course. An unscaled feature with a large numeric range (like income in dollars) will dominate the distance calculation over a feature with a small range (like age), silently distorting every one of these algorithms. StandardScaler before clustering — as every code example in this lesson already does — isn't optional.

Lesson Summary

KMeans assigns points to the nearest of k centroids; K-Means++ spreads out initial centroids to avoid bad local optima; the elbow method picks k from inertia.
K-Means assumes convex, spherical clusters and forces every point into one — it has no concept of noise.
DBSCAN groups by density (eps, min_samples), finds its own cluster count, and labels outliers -1 as noise.
Agglomerative/hierarchical clustering merges bottom-up; a dendrogram shows the full merge history, cut at any height for a cluster count.
silhouette_score evaluates clustering quality without labels — no ground truth needed, unlike Section 3's supervised metrics.
🧩 Knowledge Check — Lesson 17
4 questions on K-Means++, DBSCAN, hierarchical clustering, and choosing between them.
1. What does K-Means++ initialization change compared to plain random centroid placement?
2. In DBSCAN, what does a point labeled -1 represent?
3. In a dendrogram, what does the HEIGHT at which two branches merge represent?
4. A dataset has an unknown number of natural groups, irregular non-round shapes, and a handful of genuine outliers that shouldn't be forced into any group. Which algorithm fits best?
💪
Try It Yourself — Lesson 17
Run and compare all three clustering algorithms · Intermediate Level

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.

Task 1: Run the elbow method and fit K-Means 📉

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.
Task 2: Run DBSCAN and count the noise points 🌫️

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?
Task 3: Draw a dendrogram and compare all three 🌳

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 eps tends to label almost everything as noise; a very large eps tends 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.
Finished this lesson?
Mark it complete to track your progress.
🎉

Lesson 17 Complete!

You can now fit and reason about K-Means (with K-Means++ and the elbow method), DBSCAN, and Agglomerative Clustering, evaluate a clustering with silhouette score, and choose the right algorithm for a given dataset's shape and noise. Next up: PCA, for when the dataset has too many features to work with directly.

Module 17 of 24 Section 4 — Unsupervised Learning