Focused practice · 11 min read

Python k-Means Exercises: Assign, Update, Measure

k-means becomes less mysterious when assignment, update, and evaluation are separate exercises. These examples use tiny two-dimensional points so every centroid can be checked by hand.

Exercise: compute squared Euclidean distance

Squared distance preserves nearest-neighbor ordering and avoids a square root. Use Euclidean distance only when the actual unit matters for reporting.

def squared_distance(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b))

print(squared_distance((0, 0), (3, 4)))

25 square units

Exercise: assign every point

Assignment is a list comprehension over points. Each point chooses the index of its nearest centroid. Add the index to the key so ties resolve deterministically.

def distance(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5

points = [(0, 0), (0, 1), (5, 5), (5, 6)]
centroids = [(0, 0), (5, 5)]
labels = [min(range(len(centroids)), key=lambda i: (distance(point, centroids[i]), i)) for point in points]
print(labels)

Two points per cluster

Exercise: update one centroid

A centroid is the coordinate-wise mean of its members. Compute the mean only after all assignments are complete; otherwise the update uses a half-finished clustering.

points = [(0, 0), (0, 1), (5, 5), (5, 6)]
labels = [0, 0, 1, 1]
members = [point for point, label in zip(points, labels) if label == 0]
centroid = (
    sum(x for x, _ in members) / len(members),
    sum(y for _, y in members) / len(members),
)
print(tuple(round(value, 1) for value in centroid))

The mean of cluster 0

Exercise: measure inertia

Inertia is the sum of squared distances from each point to its own centroid. Lower is tighter, but it always falls when k increases, so never choose k by inertia alone.

def squared_distance(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b))

points = [(0, 0), (0, 1), (5, 5), (5, 6)]
labels = [0, 0, 1, 1]
centroids = [(0, 0.5), (5, 5.5)]
inertia = sum(squared_distance(point, centroids[label]) for point, label in zip(points, labels))
print(round(inertia, 1))

How tight is the clustering?

Exercise: iterate and evaluate

Run assignment and update until the labels stop changing. Then report both the labels and inertia. Try several initializations because the loop only guarantees a local optimum.

def distance(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5

points = [(0, 0), (0, 1), (5, 5), (5, 6)]
centroids = [(0, 0), (5, 5)]
labels = None
for _ in range(10):
    new_labels = [min(range(len(centroids)), key=lambda i: (distance(point, centroids[i]), i)) for point in points]
    new_centroids = []
    for index in range(len(centroids)):
        members = [point for point, label in zip(points, new_labels) if label == index]
        new_centroids.append((
            sum(x for x, _ in members) / len(members),
            sum(y for _, y in members) / len(members),
        ) if members else centroids[index])
    if new_labels == labels:
        break
    labels = new_labels
    centroids = new_centroids
inertia = sum(distance(point, centroids[label]) ** 2 for point, label in zip(points, labels))
print(labels)
print(round(inertia, 1))

Stop when assignments settle

Your practice checklist

Common questions

How do I choose k?

There is no universal rule. Use domain knowledge, inspect inertia across values of k, and evaluate whether the clusters are useful for the actual task.

Why do I get different clusters each run?

k-means finds a local optimum that depends on initialization. Use a fixed random seed or run several starts and keep the best inertia.

Does scaling change k-means?

Yes. Distance treats every coordinate equally, so a feature with a larger numeric range can dominate the assignment.

Practice the k-means loop

12 questions on distance, assignment, centroid updates, inertia, scaling, and convergence.

Open practice

Keep reading: