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
- Scale features before computing distances.
- Assign all points before updating any centroid.
- Resolve distance ties deterministically.
- Handle empty clusters explicitly.
- Report inertia with k and compare several initializations.
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.
Keep reading: