TAI BUI
Back to curriculum
BuildPython

Unsupervised Learning

No labels, no teacher, no answer key. The algorithm has to find its own structure in the data.

TIME: ~90 minutesPREREQUISITES: Phase 1 (Norms & Distances, Probability & Distributions), Phase 2 Lessons 1-6

Learning Objectives

  • Implement K-Means, DBSCAN, and Gaussian Mixture Models from scratch and compare their clustering behavior
  • Evaluate cluster quality using the silhouette score and the elbow method to select the optimal K
  • Explain when DBSCAN outperforms K-Means and identify which algorithm handles non-spherical clusters and outliers
  • Build an anomaly detection pipeline using clustering methods to flag points that deviate from normal patterns

The Problem

Every lesson up to this point has assumed labeled data: here's an input, here's the correct output. Out in the real world, that's the expensive part. A hospital sits on millions of patient records nobody has tagged with a diagnosis. An e-commerce site logs millions of sessions with no hand-labeled customer segments anywhere in sight, and a security team can stare at years of network traffic without a single confirmed example of what "anomaly" looks like on their own systems.

Unsupervised learning is what you reach for when there's no answer key: an algorithm that finds patterns without being told what to look for, grouping similar points, surfacing hidden structure, flagging what doesn't belong. Supervised learning is studying from a textbook with the answers already in the back. Unsupervised learning is staring at raw data until its shape becomes obvious.

The catch: without labels, there's no ground truth to check against. "Right" and "wrong" stop meaning anything, so you need an entirely different set of tools just to tell whether the structure you found is real, or something the algorithm imagined.

Warm-up0/3
1. A dataset has inputs but no recorded correct outputs. How does learning from it differ from the supervised learning you have already seen?Warm-up
Two small tables of rows with columns x1 and x2. The left one has a third column of outputs holding 0 and 1. The right one has a third column whose every cell says none.
Typed example values, not real data.
2. What is the Euclidean distance between the points (0, 0) and (3, 4)?Warm-up
A grid with the point (0, 0) and the point (3, 4) joined by a straight line, and a box reading distance equals a question mark.
3. What is the mean of the three points (1, 1), (3, 5) and (5, 9), averaging each coordinate separately?Warm-up
A grid with three labelled points, (1, 1), (3, 5) and (5, 9), and a box reading mean equals a pair of question marks.

The Concept

Clustering: Grouping Similar Things Together

Clustering assigns every data point to a group, a cluster, so that points inside a group look more like each other than they look like points in any other group. The hard question is always what "similar" means.

Mermaid diagram

K-Means: The Workhorse

K-Means partitions the data into exactly K clusters. Each cluster gets a centroid, its center of mass, and every point belongs to whichever centroid sits closest.

Lloyd's algorithm:

  1. Pick K random points as initial centroids
  2. Assign each data point to the nearest centroid
  3. Recompute each centroid as the mean of its assigned points
  4. Repeat steps 2-3 until assignments stop changing

Inertia, the objective function K-Means minimizes, is the total squared distance from each point to its assigned centroid. K-Means only ever finds a local minimum of that objective, never the global one, so two different random initializations can converge to two different clusterings.

Choosing K

There are two standard ways to pick K.

Elbow method: run K-Means for K = 1, 2, 3, ..., n, plot inertia against K, and look for the "elbow": the point where adding another cluster stops buying you much reduction in inertia.

Silhouette score: for each point, compare how similar it is to its own cluster (a) against the nearest other cluster (b). The silhouette coefficient is (b - a) / max(a, b), running from -1 (you're in the wrong cluster) to +1 (well-clustered). Average that across every point and you get one global score for the whole clustering.

DBSCAN: Density-Based Clustering

DBSCAN finds clusters as dense regions separated by sparse ones, so unlike K-Means, it never assumes clusters are round and never makes you commit to a K upfront.

Two parameters drive it:

  • eps: the radius of a neighborhood
  • min_samples: the minimum number of points needed to call that neighborhood dense

Three kinds of points fall out of that:

  • Core point: has at least min_samples points within eps distance
  • Border point: within eps of a core point but not itself a core point
  • Noise point: neither core nor border. These are outliers.

DBSCAN chains core points that are within eps of each other into a single cluster, lets border points tag along with the nearest core point's cluster, and leaves noise points in no cluster at all.

Its strength is finding clusters of any shape while working out the number of clusters and the outliers on its own. Its weakness shows up with clusters of very different densities: a single eps that's right for a tight cluster will swallow or miss a looser one sitting nearby.

Hierarchical Clustering

Hierarchical clustering builds a tree, a dendrogram, of nested clusters instead of committing to one flat partition.

Agglomerative (bottom-up):

  1. Start with each point as its own cluster
  2. Merge the two closest clusters
  3. Repeat until only one cluster remains
  4. Cut the dendrogram at the desired level to get K clusters

The "closeness" between clusters can be measured as:

  • Single linkage: minimum distance between any two points in the two clusters
  • Complete linkage: maximum distance between any two points
  • Average linkage: average distance between all pairs
  • Ward's method: the merge that causes the smallest increase in total within-cluster variance

Gaussian Mixture Models (GMM)

GMM gives soft assignments: instead of forcing each point into exactly one cluster the way K-Means does, it gives every point a probability of belonging to each cluster.

Under the hood, GMM assumes the data was generated by a mixture of K Gaussian distributions, each with its own mean and covariance. Fitting it means running Expectation-Maximization (EM), which alternates between:

  • E-step: compute the probability that each point belongs to each Gaussian
  • M-step: update the mean, covariance, and mixing weight of each Gaussian to maximize the likelihood of the data

Because each Gaussian carries its own covariance, GMM can stretch its clusters into ellipses instead of K-Means's spheres, and it handles overlapping clusters naturally: a point near the boundary just gets a mixed probability instead of an arbitrary hard call.

When to Use Which

Line these up and the choice usually falls out fast:

MethodBest forAvoid when
K-MeansLarge datasets, spherical clusters, known KIrregular shapes, outliers present
DBSCANUnknown K, arbitrary shapes, outlier detectionVarying densities, very high dimensions
HierarchicalSmall datasets, need dendrogram, unknown KLarge datasets (O(n^2) memory)
GMMOverlapping clusters, soft assignments neededVery large datasets, too many dimensions

Anomaly Detection with Clustering

Clustering gives you anomaly detection for free, once you know where to look:

  • K-Means: points far from any centroid are anomalies
  • DBSCAN: noise points are anomalies by definition
  • GMM: points with low probability under all Gaussians are anomalies
Check: The Concept0/28
1. What must you decide before running K-Means, which DBSCAN does not ask of you?Define
A scatter plot of 150 grey unlabeled points in the plane, next to two call lines, KMeans with a question mark inside the brackets and DBSCAN with a question mark inside the brackets.
Points from the lesson's make_blobs. The calls show no real arguments.
2. What is clustering?Define
Two scatter plots of the same 150 points. On the left all points are grey. On the right they are coloured blue, orange and green in three separate blobs.
Points from the lesson's make_blobs; colours come from a real K-Means run with K = 3.
3. In K-Means, what is a centroid?Define
A grid with five labelled blue points: (1, 2), (2, 4), (4, 3), (5, 5) and (3, 1). A box reads centroid equals a pair of question marks.
Typed example points.
4. What does inertia measure in K-Means?Define
Eight points in two groups of four, blue and orange. Dashed lines join each point to its group's centroid, C1 at (2, 2) and C2 at (8, 6). A box reads inertia equals a question mark.
Typed example points.
5. What does the silhouette score measure?Define
Left: 150 points in three coloured blobs. Right: a horizontal bar chart with one thin bar per point, grouped by colour, on an axis from -1 to 1, with a dashed vertical line at the average, 0.75.
Real K-Means run (K = 3) on the lesson's make_blobs data; per-point score computed with the lesson's rule.
6. How does the elbow method help choose K for K-Means?Define
A scatter plot of 150 grey unlabeled points, next to a box reading K equals a question mark.
Points from the lesson's make_blobs.
7. In agglomerative hierarchical clustering, what does complete linkage use to measure closeness between two clusters?Define
A plot with cluster A, three blue points near the left, and cluster B, three orange points near the right, each labelled with its coordinates. A box asks for the complete-linkage distance between A and B, shown as a question mark.
Typed example points.
8. In DBSCAN, what distinguishes a border point from a core point?Define
A scatter plot of 12 points marked three ways: seven filled blue dots in a dense patch, three blue rings around its edge, and two red crosses far away. A dashed circle in the corner shows the size of eps, 1.0, and a note gives min_samples 4.
Typed points; marker types computed from the lesson's DBSCAN rules, eps = 1.0 and min_samples = 4 chosen for illustration.
9. What makes a point a core point in DBSCAN?Define
A scatter plot of eight points with one point P in orange, surrounded by a dashed circle of radius eps with three grey points inside it and four outside. A note gives min_samples equal to 4.
Typed points; eps and min_samples chosen for illustration.
10. In the EM algorithm used to fit a Gaussian Mixture Model, what happens in the E-step versus the M-step?Define
Eight black points on a number line from 1 to 8.1 with two bell curves, A centred at 2 and B centred at 7. Below are two boxes, E-step and M-step, each followed by a question mark, with arrows between them in a loop.
Typed 1-D points; the Gaussians are starting guesses (equal weights, standard deviation 1.5).
11. What does a dendrogram show?Define
Left: six labelled points, A to F, forming three close pairs. Right: a tree diagram with the six points as leaves along the bottom, joined step by step into one tree, with numbers on its vertical axis.
Typed points; tree computed with complete linkage.
12. In this lesson's clustering methods, what counts as an anomaly?Define
A scatter plot of about 125 blue points: three blobs of different sizes, two points between blobs, and three points far from everything else.
Synthetic points, all drawn the same way.
13. K-Means fails on two interlocking half-moon shapes but DBSCAN succeeds. Why?Explain
A scatter plot of 200 grey points forming two curved crescents, one opening downward and one opening upward, hooked into each other.
Points from the lesson's make_moons (noise 0.1).
14. How does a Gaussian Mixture Model differ from K-Means in its cluster assignments?Explain
A scatter plot of 120 grey points forming two overlapping blobs, with one point P near where they meet marked in orange. Two boxes read K-Means followed by a question mark and GMM followed by a question mark.
Synthetic points.
15. Two K-Means runs on the same data with different random initial centroids end with different clusterings. Why?Explain
Two scatter plots of the same 150 points. Each has three stars marking where its starting centroids were. The final colouring differs between the two runs.
Real runs of the lesson's kmeans with seeds 0 and 1 on its make_blobs data.
16. A point has a silhouette coefficient close to +1. What does that say about it?Explain
A number line from -1 to +1 with ticks at -1, 0 and +1 and an orange marker labelled this point just below +1. Two lines of text define a and b as mean distances to the point's own cluster and to the nearest other cluster.
Schematic scale.
17. Why does GMM handle two overlapping clusters better than K-Means?Explain
A scatter plot of 140 grey points forming two blobs that overlap in the middle, with a dashed orange box drawn round the overlap zone.
Synthetic points.
18. What does hierarchical clustering give you that a single K-Means run does not?Explain
A scatter plot of eight labelled blue points, A to H: four close together at the lower left and four close together at the upper right, each side made of two tight pairs.
Typed points.
19. How does GMM identify anomalies?Explain
A scatter of 120 grey points in two blobs, each circled by a blue or orange ring marking two standard deviations of a fitted Gaussian, with a diamond at its mean. Three extra black points are labelled P, Q and R: P inside the first ring, Q in the gap between rings, R far to the right.
Synthetic points. The Gaussians are fitted with the lesson's gmm on the 120 points only; P, Q and R are added afterwards.
20. For which situation is DBSCAN a poor fit?Explain
A scatter plot of 150 grey points in three blobs with a small orange circle in the lower right showing the size of eps, next to the call DBSCAN with eps 1.5 and min_samples 5.
Points from the lesson's make_blobs; parameters from the lesson's snippet.
21. A K-Means run on customer data containing a few extreme values puts one centroid well away from the dense part of its cluster. Which property explains it?Analyze
A scatter of ten blue points: a tight group of eight near the centre-left and two points far to the upper right. A box reads centroid equals a question mark.
Typed example points.
22. You run K-Means with two different seeds and get two different clusterings with inertias of 120 and 95. What is the right conclusion?Analyze
A bar chart with two bars: seed 1 with inertia 120 and seed 2 with inertia 95.
Values from the question; the data behind them is not shown.
23. A DBSCAN run finds a tight cluster correctly but labels most of a looser neighboring cluster as noise. What is the most likely cause?Analyze
A scatter plot with a tight blue cluster on the left and a wider cloud on the right. Most points of the wider cloud are red crosses meaning noise, and a few form small green groups.
Real run of the lesson's dbscan on synthetic data. Settings are not shown.
24. A K-Means model has a smooth inertia curve and a high silhouette score, yet the product team says the segments are meaningless. What is the best next step?Analyze
A falling line chart of inertia against K from 1 to 8, a box giving a high silhouette score, and a quoted remark from the product team that the segments are meaningless.
Scores computed with the lesson's kmeans and silhouette_score on synthetic stand-in data; the quote is the question's wording.
25. You run DBSCAN for fraud detection and flag every noise point as a fraud candidate. Most flagged points turn out to be ordinary low-volume customers. What is the most likely fix?Analyze
A scatter of 213 customers as grey points, a dense group in the middle and a sparse spread toward the lower left, with 25 red crosses marking the flagged ones, many of them in the sparse area. Notes give min_samples 5 and the follow-up result.
Synthetic stand-in customer data; real run of the lesson's dbscan.
26. You must segment 2 million customer sessions into 6 groups. The clusters are roughly round and K is known. Which method fits best?Design
A scatter of grey points in six round blobs arranged in two rows of three, next to three facts: 2,000,000 sessions, 6 groups wanted, roughly round shape.
Illustrative synthetic sample; the three facts are from the question.
27. A security team has low-dimensional network features, does not know how many normal behaviors exist, and sees irregularly shaped dense regions. They want rare events flagged. Which approach fits?Design
A scatter plot of blue points: two interlocking crescents, a round blob to their right, and four points scattered far from everything else.
Synthetic stand-in for network features.
28. Customer behavior overlaps heavily between segments, and marketing wants a likelihood for each segment per customer to decide who to target. Which method fits best?Design
A scatter of 140 grey customer points forming two heavily overlapping groups, with one customer P marked in orange, and two boxes for P's segment A and segment B, each a question mark.
Synthetic points.

Build It

Step 1: K-Means from scratch

python
import math
import random
 
 
def euclidean_distance(a, b):
    return math.sqrt(sum((ai - bi) ** 2 for ai, bi in zip(a, b)))
 
 
def kmeans(data, k, max_iterations=100, seed=42):
    random.seed(seed)
    n_features = len(data[0])
 
    centroids = random.sample(data, k)
 
    for iteration in range(max_iterations):
        clusters = [[] for _ in range(k)]
        assignments = []
 
        for point in data:
            distances = [euclidean_distance(point, c) for c in centroids]
            nearest = distances.index(min(distances))
            clusters[nearest].append(point)
            assignments.append(nearest)
 
        new_centroids = []
        for cluster in clusters:
            if len(cluster) == 0:
                new_centroids.append(random.choice(data))
                continue
            centroid = [
                sum(point[j] for point in cluster) / len(cluster)
                for j in range(n_features)
            ]
            new_centroids.append(centroid)
 
        if all(
            euclidean_distance(old, new) < 1e-6
            for old, new in zip(centroids, new_centroids)
        ):
            print(f"  Converged at iteration {iteration + 1}")
            break
 
        centroids = new_centroids
 
    return assignments, centroids

Step 2: Elbow method and silhouette score

python
def compute_inertia(data, assignments, centroids):
    total = 0.0
    for point, cluster_id in zip(data, assignments):
        total += euclidean_distance(point, centroids[cluster_id]) ** 2
    return total
 
 
def silhouette_score(data, assignments):
    n = len(data)
    if n < 2:
        return 0.0
 
    clusters = {}
    for i, c in enumerate(assignments):
        clusters.setdefault(c, []).append(i)
 
    if len(clusters) < 2:
        return 0.0
 
    scores = []
    for i in range(n):
        own_cluster = assignments[i]
        own_members = [j for j in clusters[own_cluster] if j != i]
 
        if len(own_members) == 0:
            scores.append(0.0)
            continue
 
        a = sum(euclidean_distance(data[i], data[j]) for j in own_members) / len(own_members)
 
        b = float("inf")
        for cluster_id, members in clusters.items():
            if cluster_id == own_cluster:
                continue
            avg_dist = sum(euclidean_distance(data[i], data[j]) for j in members) / len(members)
            b = min(b, avg_dist)
 
        if max(a, b) == 0:
            scores.append(0.0)
        else:
            scores.append((b - a) / max(a, b))
 
    return sum(scores) / len(scores)
 
 
def find_best_k(data, max_k=10):
    print("Elbow method:")
    inertias = []
    for k in range(1, max_k + 1):
        assignments, centroids = kmeans(data, k)
        inertia = compute_inertia(data, assignments, centroids)
        inertias.append(inertia)
        print(f"  K={k}: inertia={inertia:.2f}")
 
    print("\nSilhouette scores:")
    for k in range(2, max_k + 1):
        assignments, centroids = kmeans(data, k)
        score = silhouette_score(data, assignments)
        print(f"  K={k}: silhouette={score:.4f}")
 
    return inertias

Step 3: DBSCAN from scratch

python
def dbscan(data, eps, min_samples):
    n = len(data)
    labels = [-1] * n
    cluster_id = 0
 
    def region_query(point_idx):
        neighbors = []
        for i in range(n):
            if euclidean_distance(data[point_idx], data[i]) <= eps:
                neighbors.append(i)
        return neighbors
 
    visited = [False] * n
 
    for i in range(n):
        if visited[i]:
            continue
        visited[i] = True
 
        neighbors = region_query(i)
 
        if len(neighbors) < min_samples:
            labels[i] = -1
            continue
 
        labels[i] = cluster_id
        seed_set = list(neighbors)
        seed_set.remove(i)
 
        j = 0
        while j < len(seed_set):
            q = seed_set[j]
 
            if not visited[q]:
                visited[q] = True
                q_neighbors = region_query(q)
                if len(q_neighbors) >= min_samples:
                    for nb in q_neighbors:
                        if nb not in seed_set:
                            seed_set.append(nb)
 
            if labels[q] == -1:
                labels[q] = cluster_id
 
            j += 1
 
        cluster_id += 1
 
    return labels

Step 4: Gaussian Mixture Model (EM algorithm)

python
def gmm(data, k, max_iterations=100, seed=42):
    random.seed(seed)
    n = len(data)
    d = len(data[0])
 
    indices = random.sample(range(n), k)
    means = [list(data[i]) for i in indices]
    variances = [1.0] * k
    weights = [1.0 / k] * k
 
    def gaussian_pdf(x, mean, variance):
        d = len(x)
        coeff = 1.0 / ((2 * math.pi * variance) ** (d / 2))
        exponent = -sum((xi - mi) ** 2 for xi, mi in zip(x, mean)) / (2 * variance)
        return coeff * math.exp(max(exponent, -500))
 
    for iteration in range(max_iterations):
        responsibilities = []
        for i in range(n):
            probs = []
            for j in range(k):
                probs.append(weights[j] * gaussian_pdf(data[i], means[j], variances[j]))
            total = sum(probs)
            if total == 0:
                total = 1e-300
            responsibilities.append([p / total for p in probs])
 
        old_means = [list(m) for m in means]
 
        for j in range(k):
            r_sum = sum(responsibilities[i][j] for i in range(n))
            if r_sum < 1e-10:
                continue
 
            weights[j] = r_sum / n
 
            for dim in range(d):
                means[j][dim] = sum(
                    responsibilities[i][j] * data[i][dim] for i in range(n)
                ) / r_sum
 
            variances[j] = sum(
                responsibilities[i][j]
                * sum((data[i][dim] - means[j][dim]) ** 2 for dim in range(d))
                for i in range(n)
            ) / (r_sum * d)
            variances[j] = max(variances[j], 1e-6)
 
        shift = sum(
            euclidean_distance(old_means[j], means[j]) for j in range(k)
        )
        if shift < 1e-6:
            print(f"  GMM converged at iteration {iteration + 1}")
            break
 
    assignments = []
    for i in range(n):
        assignments.append(responsibilities[i].index(max(responsibilities[i])))
 
    return assignments, means, weights, responsibilities

Step 5: Generate test data and run everything

python
def make_blobs(centers, n_per_cluster=50, spread=0.5, seed=42):
    random.seed(seed)
    data = []
    true_labels = []
    for label, (cx, cy) in enumerate(centers):
        for _ in range(n_per_cluster):
            x = cx + random.gauss(0, spread)
            y = cy + random.gauss(0, spread)
            data.append([x, y])
            true_labels.append(label)
    return data, true_labels
 
 
def make_moons(n_samples=200, noise=0.1, seed=42):
    random.seed(seed)
    data = []
    labels = []
    n_half = n_samples // 2
    for i in range(n_half):
        angle = math.pi * i / n_half
        x = math.cos(angle) + random.gauss(0, noise)
        y = math.sin(angle) + random.gauss(0, noise)
        data.append([x, y])
        labels.append(0)
    for i in range(n_half):
        angle = math.pi * i / n_half
        x = 1 - math.cos(angle) + random.gauss(0, noise)
        y = 1 - math.sin(angle) - 0.5 + random.gauss(0, noise)
        data.append([x, y])
        labels.append(1)
    return data, labels
 
 
if __name__ == "__main__":
    centers = [[2, 2], [8, 3], [5, 8]]
    data, true_labels = make_blobs(centers, n_per_cluster=50, spread=0.8)
 
    print("=== K-Means on 3 blobs ===")
    assignments, centroids = kmeans(data, k=3)
    print(f"  Centroids: {[[round(c, 2) for c in cent] for cent in centroids]}")
    sil = silhouette_score(data, assignments)
    print(f"  Silhouette score: {sil:.4f}")
 
    print("\n=== Elbow Method ===")
    find_best_k(data, max_k=6)
 
    print("\n=== DBSCAN on 3 blobs ===")
    db_labels = dbscan(data, eps=1.5, min_samples=5)
    n_clusters = len(set(db_labels) - {-1})
    n_noise = db_labels.count(-1)
    print(f"  Found {n_clusters} clusters, {n_noise} noise points")
 
    print("\n=== GMM on 3 blobs ===")
    gmm_assignments, gmm_means, gmm_weights, _ = gmm(data, k=3)
    print(f"  Means: {[[round(m, 2) for m in mean] for mean in gmm_means]}")
    print(f"  Weights: {[round(w, 3) for w in gmm_weights]}")
    gmm_sil = silhouette_score(data, gmm_assignments)
    print(f"  Silhouette score: {gmm_sil:.4f}")
 
    print("\n=== DBSCAN on moons (non-spherical clusters) ===")
    moon_data, moon_labels = make_moons(n_samples=200, noise=0.1)
    moon_db = dbscan(moon_data, eps=0.3, min_samples=5)
    n_moon_clusters = len(set(moon_db) - {-1})
    n_moon_noise = moon_db.count(-1)
    print(f"  Found {n_moon_clusters} clusters, {n_moon_noise} noise points")
 
    print("\n=== K-Means on moons (will fail to separate) ===")
    moon_km, moon_centroids = kmeans(moon_data, k=2)
    moon_sil = silhouette_score(moon_data, moon_km)
    print(f"  Silhouette score: {moon_sil:.4f}")
    print("  K-Means splits moons poorly because they are not spherical")
 
    print("\n=== Anomaly detection with DBSCAN ===")
    anomaly_data = list(data)
    anomaly_data.append([20.0, 20.0])
    anomaly_data.append([-5.0, -5.0])
    anomaly_data.append([15.0, 0.0])
    anomaly_labels = dbscan(anomaly_data, eps=1.5, min_samples=5)
    anomalies = [
        anomaly_data[i]
        for i in range(len(anomaly_labels))
        if anomaly_labels[i] == -1
    ]
    print(f"  Detected {len(anomalies)} anomalies")
    for a in anomalies[-3:]:
        print(f"    Point {[round(v, 2) for v in a]}")
Check: Build It0/7
1. In the from-scratch kmeans, a cluster ends up with no points after the assignment step. What does the code do for that cluster's centroid?Apply
A scatter with a blue blob and an orange blob, each with a diamond centroid, and a third green diamond far to the right with no points near it. Below is the lesson's loop over clusters with the argument of the first append call hidden behind question marks.
Typed scene. Code copied from the lesson with one expression hidden.
2. compute_inertia receives data [[0,0],[4,0],[10,0]], assignments [0,0,1], and centroids [[2,0],[10,0]]. What does it return?Apply
A number line from 0 to 10 with three points at 0, 4 and 10 coloured by cluster, centroid diamonds at 2 and 10, and the lesson's compute_inertia call with the three arguments listed and a question mark for the result.
Typed inputs from the question.
3. In silhouette_score, a point has a = 2 and b = 6. What value does it add to the scores list?Apply
A point on a line with a blue arrow of length 2 pointing left labelled a, towards its own cluster, and an orange arrow of length 6 pointing right labelled b, towards the nearest other cluster. Below is the line scores.append with its argument hidden.
Schematic: arrow lengths are to scale; the distances come from the question.
4. In silhouette_score, a point is the only member of its cluster, so own_members is empty. What does it contribute to the average?Apply
A scatter of six points in three clusters: three blue points, two orange points, and one green point alone in cluster 2. Beside it are lines from silhouette_score where the value appended for an empty own_members is hidden behind question marks.
Typed points; code copied from the lesson with one value hidden.
5. In dbscan, point 7 is visited first and has fewer than min_samples neighbors, so labels[7] becomes -1. Later the expansion from a core point reaches point 7. What is its final label?Apply
A scatter of 12 numbered points: points 0 to 6 in a blue group on the left, point 7 in red with a dashed circle of radius eps, and points 8 to 11 in grey just to its right. A list shows labels so far: 0 to 6 in cluster 0, point 7 at -1, 8 to 11 not visited.
Typed points; the run was checked with the lesson's dbscan (eps 1.0, min_samples 3).
6. What does the assignments list returned by the from-scratch gmm contain?Apply
A table of six rows, each giving a point index and a pair of two probabilities from a Gaussian mixture run. Beside it is the return line of gmm and an empty list of six question marks named assignments.
Values captured by running the lesson's gmm on six typed points.
7. In the anomaly demo, three points (20, 20), (-5, -5) and (15, 0) are appended to the blob data, then dbscan runs with eps=1.5 and min_samples=5. What labels do the three points get?Apply
A scatter plot of 150 grey points in three small blobs in the lower left of a larger plot, with three orange points far from them, labelled (20, 20), (-5, -5) and (15, 0). A note gives eps 1.5 and min_samples 5.
Lesson's make_blobs data plus the three appended points from its demo. dbscan is called with eps=1.5, min_samples=5.

Use It

Every one of these algorithms is a one-liner in scikit-learn:

python
from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
from sklearn.mixture import GaussianMixture
from sklearn.metrics import silhouette_score as sklearn_silhouette
 
km = KMeans(n_clusters=3, random_state=42).fit(data)
db = DBSCAN(eps=1.5, min_samples=5).fit(data)
agg = AgglomerativeClustering(n_clusters=3).fit(data)
gmm_model = GaussianMixture(n_components=3, random_state=42).fit(data)

That's the whole point of building these from scratch first: now you know exactly what's happening inside each call. K-Means iterates between assigning points and recomputing centroids. DBSCAN grows clusters outward from dense seeds. GMM alternates between expectation and maximization. scikit-learn's versions add numerical stability, smarter initialization (K-Means++), and GPU acceleration on top, but the core logic you just wrote by hand is the core logic running underneath.

Check: Use It0/1
1. What do the scikit-learn versions of K-Means, DBSCAN, and GMM add on top of the same core logic as the from-scratch versions?Explain
Two code blocks. The first has the lesson's own from-scratch calls to kmeans, dbscan and gmm. The second has the matching scikit-learn calls KMeans, DBSCAN and GaussianMixture. A question mark asks what the second adds.
Call lines copied from the lesson.

Ship It

This lesson ships two things you can reuse: working implementations of K-Means, DBSCAN, and GMM built from scratch, and outputs/skill-clustering-guide.md, a skill for choosing the right clustering algorithm based on data shape, noise, and the constraints you're working under.

Author's Note

The hard part of unsupervised learning isn't running the algorithm, it's deciding whether you should trust what it found. Silhouette score and the elbow method give you numbers, but numbers computed on ungrounded data can lie to you just as confidently as a person can. I've shipped a K-Means model with a great inertia curve and a lousy business outcome, because the metric was optimizing for tight, well-separated clusters, not clusters that meant anything to the team using them.

My rule of thumb: look at the actual points in each cluster before you look at any score, and get a domain expert to sanity-check whether the groups make sense. And if DBSCAN keeps calling half your data noise, that's not DBSCAN being picky. That's eps set too small for the density you actually have. Tune the parameter before you tune your expectations.

Exercises

  1. Implement K-Means++ initialization: instead of picking random centroids, pick the first randomly and each subsequent centroid with probability proportional to its squared distance from the nearest existing centroid. Compare convergence speed to random initialization.
  2. Add hierarchical agglomerative clustering to the code. Implement Ward's linkage and produce a dendrogram (as a nested list of merges). Cut it at different levels and compare to K-Means results.
  3. Build a simple anomaly detection pipeline: run DBSCAN and GMM on the same data, flag points that both methods agree are outliers (noise in DBSCAN, low probability in GMM). Measure the overlap and discuss when the methods disagree.

Key Terms

TermWhat people sayWhat it actually means
Clustering"Grouping similar things"Partitioning data into subsets where within-group similarity exceeds between-group similarity, measured by a specific distance metric
Centroid"The center of a cluster"The mean of all points assigned to a cluster; used by K-Means as the cluster representative
Inertia"How tight the clusters are"Sum of squared distances from each point to its assigned centroid; lower is tighter
Silhouette score"How well-separated clusters are"For each point, (b - a) / max(a, b) where a is mean intra-cluster distance and b is mean nearest-cluster distance
Core point"A point in a dense region"A point with at least min_samples neighbors within eps distance, in DBSCAN
EM algorithm"Soft K-Means"Expectation-Maximization: iteratively compute membership probabilities (E-step) and update distribution parameters (M-step)
Dendrogram"A tree of clusters"A tree diagram showing the order and distance at which clusters were merged in hierarchical clustering
Anomaly"An outlier"A data point that does not conform to the expected pattern, identified as noise by DBSCAN or low-probability by GMM

Further Reading

Code

Ships With