A Brief Post About K-Means
A few weeks ago I read chapter 4 of the book "Introduction to Applied Linear Algebra" [1], called "Clustering," and I found it so clear and simple. Chapter 4 introduces the clustering concept only based on vectors and distance (subjects treated in chapters 1 and 3, respectively) through the canonical example of clustering models: k-means.
In this post I want to make a little review of the chapter and implement k-means in python.
Clustering
The idea of clustering is to partition a set of vectors into groups based on a distance measurement. An intuitive way to think about it is a 2D-table where each observation (row) is a vector, and we want to assign it to a cluster based on some similarity measurement. So the goal is to add a new categorical variable (column) to the table with possible group values.
To draw the concept and use an example of the chapter, imagine we are in a hospital and we have a table with measurements of a feature vector for each patient. A clustering method could help separate patients into similar groups and get insights based on these groups. Maybe we could then assign labels and give different diagnosis procedures, and therefore be more effective instead of giving a unique entrance diagnosis.
To formalize what I describe above:
- : a parameter specifying the number of groups that we want to assign.
- : the categorical variable with the group assignation, i.e. a vector with the size of the number of observations.
- : a set of indices that represent vectors assigned to group .
- : a n-vector corresponding to group , this vector has the same length as the vectors we want to assign a cluster.
The similarity within a group is given by the distance between vectors of a group and the group representative vector . All members share the fact that the distance to this group representative () is the minimal one with respect to other group representatives' vectors.
A simple measurement of distance is the euclidean norm defined as:
"""
x: a float numpy 1-d array
y: a float numpy 1-d array
---
Return the euclidean norm between x and y
"""
return
The clustering objective function () measures the quality of choice in cluster assignments. A better cluster assignment of is one where the squared euclidean norm is lower.
Given the nature of the problem, finding an optimal solution for is really hard because it depends on both the group assignation () and the choice of group representatives (). Instead, we can find a feasible solution for minimising , a suboptimal one (local optimum), by solving a sequence of simpler optimization problems in an iterative approach.
k-means algorithm

Now the way the chapter shows how to assign these groups is through the k-means algorithm. The best description for k-means is encapsulated in three steps:
-
Initialize with a fixed set of representatives for (pick random observations of the data as representatives, for example).
-
Now we reduce the problem to just cluster assignations, and we can also see this problem as subproblems (one for each observation). Just look at which group representative has the minimal distance to vector , and assign it to vector .
-
The step before gives us a vector with the group assignation () of each observation (). Remember that is a vector of length equal to the number of observations we have in the data. Now, based on , we come back to the problem of finding a set of group representatives . This step gives k-means its name, because now we update the set of group representatives by computing the mean of the assigned cluster vectors from step 2.
I put the image of Ouroboros above as a graphical analogy, because k-means is self-contained and also iterative — steps 2 and 3 repeat themselves. In each iteration a new assignation of clusters is given, followed by a redefinition of group representatives. But, opposite to the symbolic meaning of Ouroboros, the flow of time is not endless: the process of repetition ends when the algorithm converges to a solution.
When does k-means reach a solution? Convergence occurs when there isn't a variation in the group assignation from the previous iteration ( is exactly the same as ).
The implementation of the three previous steps is very straightforward
using numpy in python:
"""
df: a 2D numpy array
K: number of group representatives
num_iter: number of iterations to repeat steps 2 and 3
---
Return the cluster assignment (c), group representatives (z), and the
cost function of each iteration (J)
"""
=
# initialize cluster representatives (STEP 1)
=
=
=
= 0
= 0
# solve the N subproblems of cluster assignation (STEP 2)
=
=
+= ** 2
# cost function evolution by iteration
# update group representatives, take the mean based on c (STEP 3)
=
# check convergence status
# reached convergence
break
# stop the process at iteration number -> iter_counter
break
=
+= 1
return , ,
A good way to understand k-means is visually and step by step! So I wrote
k_means with the argument num_iter, which allows stopping the process at
a certain iteration and getting the results at that point.
We can play with the algorithm and show the evolution of centroid definition and cluster assignation, but before that we need some data. We generate two groups of random data from two bivariate normal distributions with the following parameters:
A simple dataset with 2 features helps us to visualize it easily.
Additionally, we know a priori that there are two underlying groups, since
these are defined by the two different distributions. If we look at the
next plot, both samples are well separated into different groups. This is a
very basic setting, but the purpose is to illustrate how k_means works.
So k_means is blind to the colours, and its goal is to uncover the samples
generated by the two underlying distributions based only on the data.
Pictorially, the process looks like this:
For the cost side, decays rapidly and converges around a cost value of 36:
>
Additional resources
kmeans.py— the script used to regenerate the plots and the animation above in pure Python. The original 2018 version of this post was an .Rmd (R + reticulate) that ran this samek_meansimplementation but rendered every plot through ggplot2, stitching the per-iteration PNGs into a GIF with an external tool.
References
- Boyd, S. & Vandenberghe, L. (2018). Introduction to Applied Linear Algebra: Vectors, Matrices, and Least Squares. Cambridge University Press.