K-means clustering: find groups and understand their centers

Find similar reading patterns without supplied group labels

A library has monthly counts of books borrowed and visits for its readers. It wants to explore whether similar records form useful groups, but nobody has assigned a correct group to each reader. This is clustering: grouping observations from their features rather than learning supplied category answers.

K-means represents each group with a centroid, the mean position of its members. It alternates assigning records to their nearest centers and moving each center to the mean of its assigned records. You choose k, the requested number of groups.

We will use six invented readers so every assignment and mean can be checked. Their obvious separation is intentional teaching data, not evidence that real readers have two natural types. Review unsupervised versus supervised learning and distance between feature records before starting if needed.

What you’ll learn

  • Assign records to nearby centers and calculate each center from its members.
  • Trace how repeated assignments and updates change the grouping.
  • Interpret inertia, silhouette, initialization and the chosen number of clusters without treating them as ground truth.
  • Use consistent feature scales and distinguish assigning a new reader from refitting the groups.

Readers are observations; crosses are group summaries

00551010Books borrowedLibrary visits

Circles: lower borrowing/visit group

Diamonds: higher borrowing/visit group

Ringed crosses: group means, not extra readers

The final two-group fit for six invented monthly records, in raw book-count and visit-count units. Different shapes identify membership without relying on color alone.

Keep features meaningful before looking for groups

Each row is one reader’s record for the same one-month period. The two columns are books borrowed and library visits. Reader letters identify records for discussion; they are not numeric inputs. There is no target column describing an agreed reader type.

Six invented monthly records
ReaderBooks borrowedLibrary visits
A11
B12
C21
D88
E89
F98

Both are counts, but treating a one-book difference like a one-visit difference is still a modelling choice. A different observation period or feature definition changes the question. A quiet month might reflect travel or a limited opening schedule, not a permanent preference.

Clustering does not require supplied correct labels, but it still requires sensible features, complete records or a justified missing-value treatment. Check missing values and unusual observations before interpreting patterns.

Fit two groups and inspect their centers

Prepare and run

Use Python with NumPy and scikit-learn in the course environment. The invented data is included; no file or download is needed.

Python · complete runnable example

import numpy as np
from sklearn.cluster import KMeans

# Invented monthly reader records: [books borrowed, library visits].
X = np.array([[1., 1.], [1., 2.], [2., 1.], [8., 8.], [8., 9.], [9., 8.]])
model = KMeans(n_clusters=2, n_init=10, random_state=4).fit(X)
centers = sorted(model.cluster_centers_.tolist())
print("Centers:", [[round(v, 2) for v in row] for row in centers])
print("First pair together:", bool(model.labels_[0] == model.labels_[1]))
print("Opposite groups:", bool(model.labels_[0] != model.labels_[3]))
print("Inertia:", round(model.inertia_, 3))

Expected output

Centers: [[1.33, 1.33], [8.33, 8.33]]
First pair together: True
Opposite groups: True
Inertia: 2.667

The Python array has six rows and two features. There is no y target array: fit(X) learns a partition from the supplied measurements. n_clusters=2 requests two centers, while n_init and random_state control repeated initializations and reproducibility.

cluster_centers_ holds the fitted means and labels_ assigns an integer cluster ID to each reference row. The first three records are together, separate from the final three. We compare label equality instead of assuming cluster zero always describes low counts.

Centers are sorted only to make the report readable. Sorting the printed list does not reorder the model’s labels or establish a new label-to-center mapping. The reported inertia will be explained after the update trace. The KMeans API documents these attributes.

Start with centers, then assign the records

To see the mechanism, temporarily supply initial centers (1, 1) and (2, 1). These are deliberately poor starting positions near the low-count readers. They are not the fitted centers printed earlier and not a claim about the library’s default initialization.

Reader A at (1, 1) has distance zero to the first center and one to the second, so it joins the first. Reader B at (1, 2) has distance one to the first and √2 to the second, so it also joins the first. Reader C at (2, 1) exactly matches the second.

Readers D, E and F are closer to the second starting center too. The first assignment therefore gives center one readers A and B, and center two readers C, D, E and F. The group numbers only identify these positions; the algorithm has not yet found the final separation.

Euclidean distance uses the gaps in both features. Squared distance selects the same nearest center without taking a square root, which is convenient in the upcoming Python trace. Equal-distance cases require a tie convention; these assignments do not contain a tie.

Update the means and assign again

For the first assigned group, A = (1, 1) and B = (1, 2). Average each column independently: books (1 + 1)/2 = 1; visits (1 + 2)/2 = 1.5. Its new center is (1, 1.5).

The other group contains C = (2, 1), D = (8, 8), E = (8, 9) and F = (9, 8). Its books mean is 27/4 = 6.75 and visits mean is 26/4 = 6.5. Its new center is (6.75, 6.5).

Now assign using those updated positions. C at (2, 1) is much closer to (1, 1.5), so it changes groups. The next means become (4/3, 4/3), approximately (1.33, 1.33), for A, B and C; and (25/3, 25/3), approximately (8.33, 8.33), for D, E and F.

Assigning again with those centers leaves the memberships unchanged. A basic assign/update procedure has reached a stable partition here. A centroid need not equal any observed reader: fractional counts are legitimate group averages, not evidence of a reader borrowing one third of a book.

Reproduce those two updates in Python

Prepare and run

Use Python with NumPy and scikit-learn in the course environment. The invented data is included; no file or download is needed.

Python · complete runnable example

import numpy as np

# Same invented reader records. Supplied starting centers for this trace.
X = np.array([[1., 1.], [1., 2.], [2., 1.], [8., 8.], [8., 9.], [9., 8.]])
centers = np.array([[1., 1.], [2., 1.]])
for step in range(1, 3):
    squared_distances = ((X[:, None, :] - centers[None, :, :]) ** 2).sum(axis=2)
    labels = squared_distances.argmin(axis=1)
    centers = np.array([X[labels == group].mean(axis=0) for group in range(2)])
    print("Update:", step)
    print("Assignments:", labels.tolist())
    print("Centers:", np.round(centers, 3).tolist())

Expected output

Update: 1
Assignments: [0, 0, 1, 1, 1, 1]
Centers: [[1.0, 1.5], [6.75, 6.5]]
Update: 2
Assignments: [0, 0, 0, 1, 1, 1]
Centers: [[1.333, 1.333], [8.333, 8.333]]

This complete teaching trace repeats the six records and supplies the starting centers explicitly. It performs only two updates. It is not a production replacement for KMeans: it lacks general stopping rules, repeated initialization, empty-cluster handling and other implementation details.

X[:, None, :] adds a comparison axis, letting NumPy subtract each center from every row. Squaring and summing the two feature differences gives one squared distance per record and center. argmin(axis=1) selects the nearest center’s position for each record.

X[labels == group] selects one group’s members and mean(axis=0) averages its two columns. Both groups are nonempty in this trace, so each mean is defined. C changes from ID one to zero after the first center update; rounding affects only the report, not the calculations.

Measure how tightly records sit around their centers

Inertia is the sum of squared distances from each record to its assigned center. K-means seeks a small value for this objective. It is not a classification accuracy or a count of correctly identified reader types.

For group A, B, C with center (4/3, 4/3), the squared distances are 2/9, 5/9 and 5/9, totaling 4/3. The upper group has the same relative arrangement and contributes another 4/3. Total inertia is 8/3, approximately 2.667.

For fixed memberships, their coordinate means minimize this squared-distance sum. Assigning to nearer centers then reduces or preserves the objective too. That explains why repeated assignment and mean updates help, but it does not prove that a particular initialization finds the globally best partition.

Inertia depends on feature units and the number of records. Changing scales can change both its magnitude and the partition. Compare candidate k values on the same records and representation; do not compare raw inertia across unrelated datasets as a universal quality score.

A repeatable result is not a uniquely correct grouping

Starting centers matter because the procedure can settle at a locally good solution. K-means++ initialization chooses starting positions with separation in mind; it is a useful default strategy, not a proof that the final solution is globally optimal.

In our fit, n_init=10 requests ten initializations and selects the run with the lowest inertia. Specifying it explicitly avoids relying on version-dependent defaults. random_state=4 makes these randomized choices repeatable for the given inputs and configuration.

A fixed seed does not establish that the groups are stable under new samples, different feature choices or changed library behavior. Inspect reasonable alternative runs and the resulting memberships. Real implementations stop based on convergence tolerances or iteration limits; a stopped run should not be described as perfectly optimized merely because it returned labels.

The K-means guide explains initialization, updates and limitations. Empty groups and too few distinct observations require implementation handling; our six-record trace intentionally avoids those edge cases.

Compare one, two and three requested groups

Prepare and run

Use Python with NumPy and scikit-learn in the course environment. The invented data is included; no file or download is needed.

Python · complete runnable example

import numpy as np
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

# Invented reader records; same representation for every candidate k.
X = np.array([[1., 1.], [1., 2.], [2., 1.], [8., 8.], [8., 9.], [9., 8.]])
for k in [1, 2, 3]:
    model = KMeans(n_clusters=k, n_init=10, random_state=4).fit(X)
    print("k:", k, "inertia:", round(model.inertia_, 3))
    if k > 1:
        print("Silhouette:", round(silhouette_score(X, model.labels_), 3))
    else:
        print("Silhouette: undefined for one cluster")

Expected output

k: 1 inertia: 149.667
Silhouette: undefined for one cluster
k: 2 inertia: 2.667
Silhouette: 0.885
k: 3 inertia: 1.833
Silhouette: 0.49

The program repeats the same records and representation for each candidate k. One cluster forces both separated regions around one shared mean, giving high inertia. Two clusters fit the obvious separation. Three lowers inertia further by subdividing one of those regions.

Silhouette compares how close a record is to its own group with how close it is to another group. Its range is −1 to 1; larger positive values indicate better separation under the chosen distance. The overall score averages the record scores. It is defined only when the number of represented clusters is between two and one fewer than the number of records.

More precisely, a record’s silhouette compares its mean distance to the other members of its own group, a, with the smallest mean distance to the members of another group, b. Its value is (b − a) / max(a, b): for a = 0.5 and b = 5, that is 0.9. These are distances to member records, not just to centroids. Scikit-learn assigns a singleton group’s record a silhouette value of zero.

For these deliberately separated points, k = 2 scores about 0.885, higher than k = 3 at about 0.490. Lower inertia alone would prefer three, while silhouette favors two. Neither proves a true number of reader types; both summarize geometry in the supplied features. The silhouette API documents its definition and requirements.

Do not let one curve name the groups for you

An elbow plot compares inertia across k values, looking for a point after which extra groups give smaller improvements. Sometimes the change is gradual and there is no clear elbow. The best possible inertia cannot increase when extra centers are allowed, but independently initialized fitted runs are not guaranteed to reach those best solutions.

Silhouette can add a separation check, but favors some geometries and may disagree with the intended use. Examine group sizes, means, representative records and sensitivity to reasonable feature or sample changes. A tiny group may be useful, an unusual record or a consequence of forcing too many centers.

Choose a grouping that serves a stated exploratory question, such as planning different library outreach messages, then assess whether those descriptions hold up on later observations. Cluster IDs do not create truth labels. A data partition can be mathematically compact yet practically unhelpful.

Apply fitted groups to new readers without moving the centers

Prepare and run

Use Python with NumPy and scikit-learn in the course environment. The invented data is included; no file or download is needed.

Python · complete runnable example

import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans

# Invented reference readers, followed by two separate hypothetical queries.
X = np.array([[1., 1.], [1., 2.], [2., 1.], [8., 8.], [8., 9.], [9., 8.]])
scaler = StandardScaler().fit(X)
prepared = scaler.transform(X)
model = KMeans(n_clusters=2, n_init=10, random_state=4).fit(prepared)
queries = scaler.transform([[2., 2.], [7., 8.]])
query_labels = model.predict(queries)
original_centers = scaler.inverse_transform(model.cluster_centers_)
print("Centers in original units:",
      [[round(v, 2) for v in row] for row in sorted(original_centers.tolist())])
print("First query joins lower group:", bool(query_labels[0] == model.labels_[0]))
print("Second query joins higher group:", bool(query_labels[1] == model.labels_[3]))

Expected output

Centers in original units: [[1.33, 1.33], [8.33, 8.33]]
First query joins lower group: True
Second query joins higher group: True

This independent program fits StandardScaler on the six reference readers and clusters their standardized features. It then transforms two new monthly records, (2, 2) and (7, 8), with those same learned scaling settings before calling predict.

predict assigns each query to a fitted center. It does not refit either the scaler or the groups, and it does not treat the query as a new group member for updating the mean. The equality checks compare each query label with an existing reference label, avoiding assumptions about numeric IDs.

inverse_transform reports the standardized-space centers back in books and visits for interpretation. In this symmetric teaching data, scaling retains the same partition; that is not true for every dataset. Review feature scaling before choosing a representation for less balanced features.

Assigning held-out or later readers lets you inspect how the existing groups behave on new observations. There is no classification accuracy without agreed reference categories. If you refit for a later month, centers and memberships may change and IDs may need matching across fits.

Describe the recorded behavior, not a permanent person type

For these records, useful descriptions are “lower monthly borrowing and visits” and “higher monthly borrowing and visits”. “Low-value readers” or “better readers” would introduce a judgment not measured by either feature. The monthly counts do not tell us motivation, need or long-term preference.

IDs zero and one are arbitrary identifiers. A later run can swap those numbers while keeping exactly the same members together. Conversely, unchanged numeric IDs do not prove unchanged groups after a refit. Compare memberships and center profiles rather than interpreting the number as a rank.

Summaries should include group size, feature ranges and unusual members, not just the mean. The segmentation project applies this idea to describing and questioning groups. Use the analysis as a prompt for investigation rather than treating an unsupervised partition as an established category system.

Your turn: add a seventh reference reader

Your task: Add (2, 2) to the reference array in this complete program and fit again. Assuming it joins the lower-count group, calculate that group’s new mean before running. Will the upper group’s mean change? How is this different from predict?

Python · complete program to modify

import numpy as np
from sklearn.cluster import KMeans

# Invented monthly reader records: [books borrowed, library visits].
X = np.array([[1., 1.], [1., 2.], [2., 1.], [8., 8.], [8., 9.], [9., 8.]])
model = KMeans(n_clusters=2, n_init=10, random_state=4).fit(X)
centers = sorted(model.cluster_centers_.tolist())
print("Centers:", [[round(v, 2) for v in row] for row in centers])
print("First pair together:", bool(model.labels_[0] == model.labels_[1]))
print("Opposite groups:", bool(model.labels_[0] != model.labels_[3]))
print("Inertia:", round(model.inertia_, 3))

Worked answer

The new first-group column totals are each 1 + 1 + 2 + 2 = 6 across four records, so the mean becomes (1.5, 1.5). The other three records still average (25/3, 25/3). Append the seventh row and fit the modified array.

Centers: [[1.5, 1.5], [8.33, 8.33]]
First pair together: True
Opposite groups: True
Inertia: 3.333

The lower group’s four squared distances now sum to 2; the upper group contributes 4/3, totaling 10/3. Refitting includes the new reader in the reference evidence and recomputes means. Calling predict on (2, 2) alone would only assign it to the old fitted center, leaving the centers unchanged.

Know when centers are the wrong description

K-means uses squared Euclidean distances around means, so it tends to favor compact groups. Curved, elongated or strongly differing-density groups can be poorly represented. It will still return a partition for many unsuitable datasets, so “the algorithm produced labels” is not evidence of meaningful clusters.

Extreme observations can pull a mean or consume a center. Investigate their provenance and meaning; deleting them only to improve a score can remove important information. Arbitrary numeric codes for categories also invent distances and averages between category names. Use a representation or method appropriate to the data.

For large datasets, MiniBatchKMeans updates from smaller batches to reduce computation, trading some fitting precision for speed. Hierarchical methods build a sequence of group merges; density-based methods can represent different shapes and identify noise under their own assumptions. We develop hierarchical clustering next rather than assuming K-means fits every task.

The chosen features, scale, observation window and population determine what the groups mean. A repeatable fit or high silhouette is a useful check, not a guarantee of stable real-world categories.

Quiz: interpret a fractional center

The fitted center is (1.33 books, 1.33 visits). What does it represent?
Choose one answer. You can try again.

Answer and explanation

A numerical summary of the assigned readers, not necessarily an observed reader.

Correct. Averaging feature values can yield fractions even when every individual observation is an integer count.

Quiz: add a query without changing the fit

You call predict for a new reader at (2, 2). What happens to the fitted centers?
Choose one answer. You can try again.

Answer and explanation

They remain fixed while the new record is assigned to one of them.

Correct. Prediction chooses a nearest fitted center. Including the record in a refit is a separate operation that can change the centers.

Quiz: choose what lower inertia proves

Requesting three groups gives lower inertia than two on these six records. What can we conclude?
Choose one answer. You can try again.

Answer and explanation

The fitted three-center representation has lower squared distances here; it does not prove three true reader types.

Correct. Extra centers allow more subdivision. Use separation, stability, meaningful summaries and the intended question alongside the objective.

Recap: assign, average, repeat and interpret

K-means assigned the invented reader records to centers and updated those centers to column means. Our trace showed a reader switching groups as the centers moved, while the final fit separated the lower and higher monthly counts.

  • Keep the task clear: clustering groups feature records without supplied correct category labels.
  • Trace the mechanism: nearest-center assignment alternates with mean updates.
  • Read the objective: inertia sums squared within-group distances; it is not accuracy.
  • Question the choices: k, initialization, scale and geometry affect the result.
  • Distinguish actions: predict assigns to fixed centers; refitting can move them.
  • Describe carefully: a centroid is a summary and an ID is a name, not a person type or a rank.

Before moving on: calculate the lower group’s mean after adding (2, 2), explain why three centers can reduce inertia and say what evidence would make a grouping useful. Next, we will build a hierarchy of groups through successive merges.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Hierarchical clustering: follow merges and choose a grouping →