Skip to content

Cluster Analysis: k-Means in Code

Why this matters

The first clustering bridge explained the idea:

data with no answer labels -> groups that seem similar

This page makes that idea visible with one small k-means run.

The goal is not to build the best customer segmentation model. The goal is to read simple code and output, then connect the output back to the theory:

numeric features -> scaled feature space -> cluster labels and centroids

Mental model

k-means asks you to choose how many clusters you want.

Then it tries to place that many center points, called centroids, so each row is close to one of them.

For this tiny example, imagine a cafe with six customers:

visits per month
average spend in euros

k-means will not know customer names, preferences, or true customer types. It only sees the two numeric feature columns.

Core ideas

  • k-means uses numeric features as input.
  • The feature table is usually called X.
  • Scaling puts numeric features on comparable ranges before distance is used.
  • n_clusters=3 asks k-means to find three groups.
  • fit_predict(X_scaled) learns the centroids and returns one cluster label per row.
  • Cluster labels such as 0, 1, and 2 are arbitrary identifiers.
  • cluster_centers_ stores the learned centroids in the scaled feature space.
  • Inertia measures how far points are from their assigned centroids.
  • Silhouette score compares how close points are to their own cluster versus nearby other clusters.
  • Lower inertia alone does not prove that the clustering is more useful.

One tiny k-means run

This invented customer dataset has six rows and two numeric feature columns.

import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

customers = pd.DataFrame({
    "customer": ["A", "B", "C", "D", "E", "F"],
    "visits_per_month": [2, 3, 8, 9, 2, 3],
    "avg_spend_eur": [6, 7, 14, 15, 28, 30],
})

X = customers[["visits_per_month", "avg_spend_eur"]]

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

kmeans = KMeans(n_clusters=3, n_init=10, random_state=0)
customers["cluster"] = kmeans.fit_predict(X_scaled)

print(customers)
print("\nInertia:", round(kmeans.inertia_, 2))

One possible output:

  customer  visits_per_month  avg_spend_eur  cluster
0        A                 2              6        0
1        B                 3              7        0
2        C                 8             14        1
3        D                 9             15        1
4        E                 2             28        2
5        F                 3             30        2

Inertia: 0.22

How to read the output

The input rows did not disappear

The output still has the same six customers:

A, B, C, D, E, F

k-means did not remove rows. It added a new cluster assignment for each row.

The cluster numbers are names

The output says:

A -> cluster 0
B -> cluster 0
C -> cluster 1
D -> cluster 1
E -> cluster 2
F -> cluster 2

Do not read this as:

cluster 0 is first
cluster 1 is better
cluster 2 is more important

The numbers are just identifiers. The useful interpretation comes from inspecting the rows:

cluster 0 -> low visits, low spend
cluster 1 -> frequent visits, moderate spend
cluster 2 -> low visits, high spend

Those names are human interpretations. k-means only returned the numbers.

Scaling is part of the practical pattern

The code uses:

X_scaled = scaler.fit_transform(X)

This matters because k-means uses distance.

In the raw table, spend has values like 6, 14, and 30, while visits has values like 2, 3, and 9. Without scaling, the larger spend numbers can dominate distance just because of their units.

The practical habit is:

choose numeric features -> scale them -> run distance-based clustering

Scaling does not make the clusters automatically correct. It makes the distance calculation less accidentally controlled by units.

Inertia tells you compactness

The output prints:

Inertia: 0.22

In plain language:

lower inertia -> points are closer to their assigned centroids
higher inertia -> points are more spread out inside clusters

This value is useful when comparing runs on the same scaled data.

It is not useful as an absolute score by itself. A small inertia number does not automatically mean the clusters are meaningful customer types.

Reading the centroids

The learned centroids live in the scaled feature space. For interpretation, it is often easier to convert them back to the original units:

centers_original = scaler.inverse_transform(kmeans.cluster_centers_)

centers = pd.DataFrame(
    centers_original,
    columns=["visits_per_month", "avg_spend_eur"],
)

print(centers.round(1))

One possible output:

   visits_per_month  avg_spend_eur
0               2.5            6.5
1               8.5           14.5
2               2.5           29.0

Read these as the approximate centers of the discovered groups:

cluster 0 center -> about 2.5 visits, 6.5 euros
cluster 1 center -> about 8.5 visits, 14.5 euros
cluster 2 center -> about 2.5 visits, 29.0 euros

The centroid is not necessarily a real customer. It is the average position of the customers assigned to that cluster.

What if k changes?

k-means needs the number of clusters before training.

The code above chose:

n_clusters=3

To see why this choice matters, run the same scaled data with several values of k:

for k in range(1, 5):
    model = KMeans(n_clusters=k, n_init=10, random_state=0)
    model.fit(X_scaled)
    print(k, round(model.inertia_, 2))

One possible output:

1 12.00
2 5.31
3 0.22
4 0.13

The important pattern:

more clusters usually lowers inertia

That is why the lowest inertia is not enough. If you keep adding clusters, each point can get closer to a centroid, but the result may stop being useful.

For this tiny table, k=3 gives a simple interpretation:

low visits, low spend
frequent visits, moderate spend
low visits, high spend

k=4 has lower inertia, but it starts splitting an already tiny group. That may be too detailed for the practical question.

Justifying a good k

In an exam, "justify a good number of clusters" means you should connect a metric to a decision.

Do not only write:

I choose k=3.

Write the evidence pattern:

I tried several k values. Inertia dropped strongly up to k=3 and only slightly after that, so k=3 is the elbow. The silhouette score is also highest at k=3, which means points are closer to their own cluster than to other clusters. Therefore k=3 is a reasonable choice.

Inertia evidence

Use inertia to look for an elbow:

k  inertia
1  12.00
2   5.31
3   0.22
4   0.13

The big improvement happens by k=3.

After that, inertia still improves, but only a little:

k=3 -> 0.22
k=4 -> 0.13

That supports k=3 because the fourth cluster adds detail without changing the structure much.

Silhouette evidence

Silhouette score asks a different question:

Is each point closer to its own cluster than to the next-best other cluster?

Useful reading rule:

near +1 -> well separated
near  0 -> between clusters
near -1 -> probably assigned to the wrong cluster

In code:

from sklearn.metrics import silhouette_score

for k in range(2, 5):
    model = KMeans(n_clusters=k, n_init=10, random_state=0)
    labels = model.fit_predict(X_scaled)
    score = silhouette_score(X_scaled, labels)
    print(k, round(model.inertia_, 2), round(score, 2))

One possible output:

k  inertia  silhouette
2     5.31        0.52
3     0.22        0.84
4     0.13        0.56

This supports k=3 because:

inertia has its elbow at k=3
silhouette is highest at k=3
the three clusters have a simple interpretation

A safe exam answer template

Use this structure when the prompt says to justify the number of clusters:

I compared k = ... using inertia and/or silhouette score.
Inertia suggests k = ... because ...
Silhouette suggests k = ... because ...
I choose k = ... because the metric evidence and the cluster interpretation agree.

If the metrics disagree, say that directly:

The metrics do not give a single clear answer. I would treat k=... and k=... as candidates, inspect the cluster plots or centroids, and choose the simpler value unless the extra cluster has a meaningful interpretation.

How this connects to the full lesson

The full Cluster Analysis lesson uses larger examples:

synthetic blobs
Iris flowers
TF-IDF document vectors
half-moon data for DBSCAN

The same reading habit applies:

features going in        -> what similarity means
scaling or representation -> what distance sees
cluster labels coming out -> arbitrary group IDs
centroids or other output -> clues for interpretation
evaluation pattern        -> evidence, not proof

Common traps

Do not treat cluster numbers as real categories

Cluster 0 is not automatically a real customer type. Inspect the rows and centroids before naming a group.

Do not skip scaling by habit

For distance-based clustering, raw units can control the result accidentally.

Do not choose k from the lowest inertia alone

Inertia usually decreases when k increases. Use the pattern plus the practical meaning of the groups.

Do not quote silhouette without interpretation

A higher silhouette score is evidence for better separation, but you still need to explain what the chosen clusters mean.

Do not overinterpret tiny examples

This six-row table is a teaching example. Real clustering needs more data, better feature choices, and careful validation.

Check yourself

What does fit_predict(X_scaled) return?

One cluster label for each input row.

Why does the code scale the customer features before k-means?

k-means uses distance, and scaling prevents one feature from dominating just because its raw numbers are larger.

What does a centroid represent?

The center position of a cluster. It is usually an average point, not necessarily a real observation.

Why is the lowest inertia not automatically the best clustering?

Adding more clusters usually lowers inertia, even when the extra detail is not useful.

What does a high silhouette score suggest?

Points are generally closer to their own cluster than to nearby other clusters, so the clusters are better separated.

How would you justify choosing k=3 in the tiny customer example?

Inertia drops strongly up to k=3 and only slightly after that, silhouette is highest at k=3, and the three clusters have a simple interpretation.

Who gives semantic names like 'low visits, high spend' to the clusters?

A human analyst does. k-means only returns arbitrary cluster labels.

Next

Next: Cluster Analysis

The full lesson expands from this small k-means example to synthetic data, Iris clustering, text clustering, hierarchical clustering, and DBSCAN.