Index Ai Academy
Phase 3 · Machine learning · Unsupervised

Clusters of any shape: DBSCAN and hierarchies

Why k-means needs round clusters, DBSCAN's core points and noise, and hierarchical clustering with its linkages.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

K-means draws its clusters as round blobs around centers. Real groupings are often other shapes: delivery addresses strung along roads, customer journeys that branch, sensor readings tracing a path, with stray points that belong nowhere. Forcing those into k round blobs gives confident nonsense. This lesson covers the two families that handle shape: density-based clustering, which follows crowded regions wherever they go and labels the rest as noise, and hierarchical clustering, which builds a whole family tree of groupings you can cut at any level.

What you'll be able to do

  • Say which cluster shapes k-means assumes, and recognize data where that assumption fails
  • Explain DBSCAN's core points, reach and noise, and tune its radius and minimum points
  • Build a hierarchy of clusters by merging, and choose a linkage that fits the shapes
  • Pick a clustering method from what you know about the data

What it covers

  1. Why k-means needs round clusters
  2. DBSCAN: clusters are crowded regions
  3. Choosing eps and min points
  4. The points within reach
  5. Hierarchical clustering: a family tree of groups
  6. Find the bug: the wrong linkage
  7. Choosing a method
Quick check
A guess first. Two interlocking crescents, like a pair of bananas, each a clear group to the eye. What does k-means with k = 2 usually do?
  1. Finds one cluster and calls the rest noise
  2. Cuts straight across both, mixing the crescents
  3. Refuses to run on curved data
  4. Finds the two crescents exactly
Show the answer

Cuts straight across both, mixing the crescents

k-means gives each point to its nearest center, so its boundary between two clusters is a straight line. A straight line can't separate interlocking crescents.

Why k-means needs round clusters

k-means minimizes each point's squared distance to its cluster's center. That has two consequences. Clusters come out roughly round and of similar spread, because a long or curved cluster has points far from any single center. And every boundary between two clusters is a straight line, halfway between their centers. On blobs that's exactly right; on crescents, rings or long strands it isn't.

from sklearn.datasets import make_moons
from sklearn.cluster import KMeans
from sklearn.metrics import adjusted_rand_score

X, true_groups = make_moons(n_samples=200, noise=0.06, random_state=0)
kmeans = KMeans(n_clusters=2, n_init=10, random_state=0).fit(X)
print("agreement with the real crescents:", round(adjusted_rand_score(true_groups, kmeans.labels_), 2))

The adjusted Rand index compares two groupings: 1 means identical, 0 means no better than chance. It ignores which number each cluster gets, so calling the crescents 1 and 0 instead of 0 and 1 still scores 1. k-means scores 0.26 here, barely better than guessing, even though the two groups are obvious to anyone looking.

DBSCAN: clusters are crowded regions

DBSCAN (density-based spatial clustering of applications with noise) defines a cluster as a crowded region, whatever its shape. It has two settings: a radius eps and a count min points.

  1. A core point has at least min points neighbors within eps of it, counting itself.
  2. A cluster starts at a core point and grows to every point within eps of it, then to every point within eps of those, as long as each step goes through a core point.
  3. Points reached this way but not crowded themselves are border points: they join the cluster without extending it. Without them, the thin outer edges of every cluster would turn into noise.
  4. Points no cluster reaches are noise, labeled -1.

You never choose the number of clusters yourself; it comes out of the data. And noise is a feature: stray points aren't forced into a group, which also makes DBSCAN a simple anomaly detector.

Quick check
Two tight groups of three points and one point on its own. What does DBSCAN return?
from sklearn.cluster import DBSCAN
points = [[0, 0], [0, 1], [1, 0], [10, 10], [10, 11], [11, 10], [5, 5]]
print(DBSCAN(eps=1.5, min_samples=3).fit(points).labels_.tolist())
  1. [0, 0, 0, 1, 1, 1, -1]
  2. [0, 0, 0, 0, 0, 0, 0]
  3. [1, 1, 1, 0, 0, 0, -1]
  4. [0, 0, 0, 1, 1, 1, 2]
Show the answer

[0, 0, 0, 1, 1, 1, -1]

Each group's points are within 1.5 of each other, so all six are core points and form clusters 0 and 1, numbered in the order found. (5, 5) has no neighbors but itself: noise, -1.

Try it now

Two crescents and a few stray points. The ring shows the radius around one point. Slide the radius from small to large.

Quick check
The same eps sweep in scikit-learn, on the 200 crescent points from the first cell. What does this print?
from sklearn.datasets import make_moons
from sklearn.cluster import DBSCAN

X, _ = make_moons(n_samples=200, noise=0.06, random_state=0)
for eps in (0.05, 0.2, 0.5):
    labels = DBSCAN(eps=eps, min_samples=5).fit(X).labels_
    print(len(set(labels) - {-1}), end=" ")
  1. 0 1 2
  2. 2 2 2
  3. 0 2 1
  4. 1 2 0
Show the answer

0 2 1

At 0.05 no point has 5 neighbors, so everything is noise and there are 0 clusters. At 0.2 the two crescents come out. At 0.5 the radius bridges them into 1.

In your own words
Why does k-means fail on two crescents, and how does DBSCAN see them?

Write a sentence or two, then compare it with a good answer in the lesson.

Choosing eps and min points

Start min points around twice the number of features (4 for two-dimensional data) and raise it if the data is noisy. For eps, a standard trick: for every point, find the distance to its k-th nearest neighbor (k = min points), sort those distances and plot them. The curve stays flat across the crowded regions, then bends sharply upward for the isolated points; eps belongs near the bend.

DBSCAN's weakness is uneven density. One eps can't fit a dense city cluster and a sparse rural one at the same time: small enough for the city and the countryside is all noise; large enough for the countryside and neighborhoods in the city merge. Its relative HDBSCAN handles that by trying every radius and keeping the clusters that persist, and is often the better default today. Scale your features first, too: eps is a distance, and a feature measured in thousands will dominate one measured in single digits.

Quick check
Your points form a very dense downtown and a sparse countryside. One DBSCAN radius gives either one merged downtown or a countryside of noise. What's the best next step?
  1. Delete the countryside points
  2. Raise min points until it works
  3. Try HDBSCAN, which considers every radius
  4. Switch to k-means with many clusters
Show the answer

Try HDBSCAN, which considers every radius

Uneven density is DBSCAN's known weakness. HDBSCAN keeps the clusters that persist across radii, so dense and sparse regions both come out.

Drill

The points within reach

Write neighbors(points, i, eps): the indexes of every point within distance eps of point i (Euclidean distance, math.dist), including i itself, in index order. This is the building block of DBSCAN.

import math


def neighbors(points, i, eps):
    return []


pts = [(0, 0), (0, 1), (3, 3), (0.5, 0.5)]
print(neighbors(pts, 0, 1.0))
In the lesson, your code is checked as you write it.
Quick check
A reminder from the k-means lesson. What does k-means need you to decide before it runs?
  1. The radius around each point
  2. Which points are noise
  3. How many clusters to find
  4. The order to merge points in
Show the answer

How many clusters to find

k is the one thing k-means can't work out for itself. DBSCAN replaces it with a radius and a minimum count, and the number of clusters follows from the data.

In your own words
What do DBSCAN's radius and minimum points control, and what's its known weakness?

Write a sentence or two, then compare it with a good answer in the lesson.

Hierarchical clustering: a family tree of groups

Agglomerative clustering starts with every point as its own cluster and repeatedly merges the two closest clusters until one remains. Recording the merges gives a tree, a dendrogram: cut it high and you get a few big groups, cut it low and many small ones, without rerunning anything.

What "closest clusters" means is the linkage, and it decides the shapes you get:

  • single: the distance between the two nearest points, one from each cluster. Follows chains and curves, so it finds the crescents, but one stray point between two groups can join them;
  • complete: the distance between the two farthest points. Gives compact, similar-sized clusters;
  • average: the average distance over all pairs, a middle ground;
  • Ward: the merge that adds the least spread. Behaves much like k-means.
from sklearn.datasets import make_moons
from sklearn.cluster import AgglomerativeClustering
from sklearn.metrics import adjusted_rand_score

X, true_groups = make_moons(n_samples=200, noise=0.06, random_state=0)
for linkage in ("single", "ward"):
    labels = AgglomerativeClustering(n_clusters=2, linkage=linkage).fit(X).labels_
    print(linkage, round(adjusted_rand_score(true_groups, labels), 2))

Single linkage follows the crescents perfectly; Ward splits them like k-means did.

To read a dendrogram, look at the heights of the merges: a big jump between one merge and the next means two well-separated groups were forced together, so cut just below the jump. Many merges at similar heights mean there's no natural number of clusters, and any cut is a choice you should be able to justify.

Drill

Find the bug: the wrong linkage

cluster_distance(a, b) should be the single-linkage distance between two clusters (lists of points): the distance between their closest pair. It's returning something else, so curved clusters never join up. Fix it.

import math


def cluster_distance(a, b):
    return max(math.dist(p, q) for p in a for q in b)


print(cluster_distance([(0, 0), (1, 0)], [(2, 0), (9, 0)]))
In the lesson, your code is checked as you write it.

Choosing a method

Start from what you know about the data. If you expect a known number of compact, similar-sized groups, k-means is fast and hard to beat. If groups can be any shape, the number is unknown, or some points belong nowhere, use DBSCAN or HDBSCAN. If you want to explore groupings at several levels, such as product families and then sub-families, hierarchical clustering's tree shows them all at once. It needs every pairwise distance, though, so beyond a few tens of thousands of points it gets slow.

Whatever you choose, check the result by looking: plot the clusters (after reducing to two dimensions if needed), read a sample from each, and compare methods with the adjusted Rand index when you have some known groups. Clustering always returns an answer; only checking tells you whether it's a useful one. A useful clustering is one someone can act on: delivery zones a dispatcher can staff, customer groups a marketer can write to.

Quick check
Delivery addresses lie along a few winding roads, with some isolated farms. Which method fits best?
  1. Ward linkage, cut into a few clusters
  2. DBSCAN: any shape, and the farms become noise
  3. k-means with one cluster per road
  4. Any of them: clustering finds the truth
Show the answer

DBSCAN: any shape, and the farms become noise

Roads are long, curved, crowded strands and farms are isolated points: exactly what density-based clustering handles. k-means and Ward look for round blobs.

In your own words
How does hierarchical clustering build groups, and what does linkage decide?

Write a sentence or two, then compare it with a good answer in the lesson.

Practice what you just read

  • 2 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 6 review cards for spaced repetition

Create a free account

Try a quiz question

In DBSCAN, what is a core point?

  1. A point with enough neighbors within eps
  2. The center point of each cluster
  3. The first point in the data set
  4. A point that sits in two clusters at once

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson