K-means is one of the most classic unsupervised learning algorithms in machine learning. It relies on no human labels, and instead divides data into clusters automatically based on the distances between samples.
This article works directly from two real files:
Iris.csv: the classic iris datasetIris_sort_K_mean.c: a C implementation of K-means you can compile and run directly
The focus here is not only on getting the program to run, but on making these points clear:
- Why K-means is designed the way it is
- What each function in the code is actually responsible for
- Why standardisation, initialisation and repeated restarts directly affect the result
- How to turn the final clustering result into a readable conclusion
1. What K-means is
The goal of K-means is:
Given a dataset and a cluster count
K, divide every sample intoKgroups so that samples within a group are as close together as possible.
It repeats two things continuously:
- Assign each sample to the nearest cluster centre
- Recompute the cluster centres from the new grouping
It is easier to follow if you think of it as a process of continuous correction:
Guess a few centres, let the samples find their nearest centre; once the samples are assigned, move each centre to the average position of its members.
2. Why the Iris dataset suits learning clustering
Iris.csv holds 150 samples. Each sample carries 4 numeric features:
- sepal length
SepalLengthCm - sepal width
SepalWidthCm - petal length
PetalLengthCm - petal width
PetalWidthCm
It also carries true labels:
Iris-setosaIris-versicolorIris-virginica
One key point deserves emphasis: K-means does not use these labels while clustering. It sees only the numeric features and the distances between samples. The labels are kept purely to interpret the result after clustering finishes.
That makes this example particularly good for learning two things:
- how an algorithm groups data with no labels available
- how far the resulting clusters sit from the true classification
3. What this C program does overall
Iris_sort_K_mean.c is not a minimal version consisting of one for loop and a distance function; it splits the full K-means execution path into several readable functions. The overall flow is:
- Read the CSV and build the sample array
- Standardise every feature
- Initialise the cluster centres with K-means++
- Repeat “assign samples / update centres”
- Restart randomly many times and keep the round with the smallest SSE
- Print the cluster distribution, the label distribution and each sample’s cluster
Iris_sort_K_mean.c: load the data, then standardise, initialise, iterate, compute SSE, and finally keep the best clustering result.4. How samples are represented in the program
Each sample is stored in a Sample struct:
typedef struct {
int id;
double x[FEATURES];
double x_scaled[FEATURES];
char species[32];
int cluster;
} Sample;
Every field has a clear responsibility:
id: sample number, matching the first CSV columnx: the raw 4-dimensional featuresx_scaled: the standardised 4-dimensional featuresspecies: the true species labelcluster: the cluster number the program finally assigns
The benefit of this design is that raw data, preprocessed data, true labels and clustering results are all stored separately, so when reading the code later you never confuse the values used for computation with the values used for display.
5. Step one: reading the CSV data
The program reads Iris.csv line by line through load_iris(). It skips the header first, then uses sscanf() to split each line into an id, 4 numeric values and a species string.
You can think of this function as a text-to-struct process:
- the file holds one line of comma-separated text
- after reading it becomes one
Sample - all samples end up in the array
data[]
The entry point accepts a data file path from the command line:
const char *filename = (argc > 1) ? argv[1] : "Iris.csv";
So you can run it directly:
./iris_kmeans Iris.csv
and later switch to a different CSV without editing the source.
6. Step two: why standardisation is essential
The core of K-means is comparing distances. If the numeric scales of different features vary widely, the feature with larger values carries more weight in the Euclidean distance.
An intuitive example: if one dimension routinely varies by 5 units while another usually varies by only 0.2 units, the first will look “more important” in the distance calculation. That does not necessarily match what we intended.
So the program runs standardize_features() before clustering, using the standard score:
x_scaled = (x - mean) / std
Internally that function does three things:
- compute the mean
meanof each feature column - compute the standard deviation
stdof each feature column - convert each sample’s raw values into standardised values
After standardisation every feature is pulled back to roughly “mean 0, spread near 1”. The four features then influence the distance calculation more evenly.
7. Step three: why initialisation cannot be arbitrary
If K-means simply picks 3 random samples as centres, the algorithm still runs, but the result is usually unstable. The reasons are:
- the initial centres may sit too close together
- some regions may have no centre covering them at the start
- the algorithm converges early to a poor local solution
To reduce this problem, the program uses the K-means++ idea inside init_centroids():
- pick one centre at random first
- draw the later centres preferentially from samples further away from the existing centres
The initial centres then tend to be more spread out, closer to the intuition that each cluster should claim a position first.
At the code level this step is not difficult: the program computes each sample’s minimum distance to the existing centres, then samples with a weight proportional to that distance. The further away a point is, the higher its chance of being chosen.
8. Step four: how samples get assigned to clusters
assign_clusters() does the single most central thing in K-means: for each sample, compute its distance to every centre and assign it to the nearest one.
The distance function it calls is:
double distance_sq(const double a[], const double b[])
Note that this computes the squared Euclidean distance, not the true distance after taking a square root. That is entirely reasonable, because:
- deciding which is nearer only requires comparing the squared values
- skipping the square root avoids some pointless computation
The return value has another key role: it tells the main loop whether any sample changed cluster this round. If one did, the algorithm has not converged; if none did, the current result is stable.
9. Step five: why the centres are recomputed
Once samples have been regrouped, the old centre positions are usually no longer reasonable. The new cluster members are now fixed, so the centre should move to their average position.
update_centroids() does exactly that:
- collect which samples belong to each cluster
- sum the 4 features across all samples in each cluster
- divide by the number of samples in that cluster to get the new centre coordinates
This is what “means” in the name refers to: each cluster is represented by the mean centre of the samples inside it.
If a cluster temporarily holds no samples, the code skips that update to avoid a division by zero. This is a very common boundary-case guard in real programs.
10. Step six: when the algorithm stops
The program sets two stopping conditions:
- if no sample changed cluster this round, it has converged and can stop
- if the iteration count reaches
MAX_ITER, it must also stop, to prevent an infinite loop in extreme cases
#define MAX_ITER 1000
For a small dataset like Iris, 1000 is far beyond what is needed. Normally it converges within a few dozen rounds.
11. Why it runs 2000 times
Even with K-means++, the result is still affected by random initialisation. So the program does not run once; it defines:
#define RESTARTS 2000
That is, the program runs K-means 2000 independent times. It computes an SSE at the end of each round and keeps the best result.
SSE means:
the sum of squared distances from every sample to the centre it belongs to.
A smaller SSE generally indicates tighter clusters. “Restart many times and keep the best SSE” is therefore a very common engineering practice.
12. What the program prints
When Iris_sort_K_mean.c finishes, it prints three kinds of information:
- the iteration count and SSE of the best result
- the cluster centres in standardised space
- the sample count and true label distribution of each cluster
A typical result looks like this. The shipped program prints its headings in Chinese, so the output is reproduced exactly as it appears in a terminal:
K-means 重启次数: 2000
最佳结果迭代次数: 6
最佳 SSE: 140.965817
Cluster 0: total=53, setosa=0, versicolor=39, virginica=14
Cluster 1: total=50, setosa=50, versicolor=0, virginica=0
Cluster 2: total=47, setosa=0, versicolor=11, virginica=36
The three Chinese headings read, in order: “K-means restarts: 2000”, “iterations of the best result: 6” and “best SSE: 140.965817”.
This result shows that:
setosacan be separated out almost cleanlyversicolorandvirginicaoverlap to some degree
This is the most classic phenomenon in the Iris dataset: setosa is highly separable, while the other two classes overlap more easily in feature space.
13. Plotting the result makes it clearer
Numbers alone are not intuitive enough. To make the cluster boundaries easier to understand, the samples are projected onto a two-dimensional plane with PetalLengthCm on the horizontal axis and PetalWidthCm on the vertical axis, coloured by K-means cluster number.
The plot shows three things quickly:
- the clearly separated small cluster corresponds to
setosa - the other two groups are broadly separable, but their boundaries overlap
- K-means handles roughly spherical, well-separated clusters best, and has no magic correction for overlapping regions
In other words, K-means learns a distance structure, not the definition of a species. That is why clustering results do not necessarily match the true classification.
14. The parameters most worth changing yourself
If you download this program, three parameters are most worth experimenting with:
K: the cluster count, currently set to 3RESTARTS: the restart count; larger is more stable but takes longerMAX_ITER: the maximum iterations of a single run
A few experiments to try:
- change
Kto 2 or 4 and see how the cluster structure changes - reduce
RESTARTSand observe whether SSE fluctuates more - remove standardisation and see whether the result degrades noticeably
Experiments like these are far more useful than memorising definitions, because you actually see how algorithm parameters and data preprocessing change the outcome.
15. What the download area provides
The download area now holds a complete set of study materials:
Iris.csv: the raw datasetIris_sort_K_mean.c: the tidied C implementationiris-kmeans-flowchart.svg: the flowchartiris-kmeans-cluster-visual.svg: the clustering visualisationiris-kmeans-materials.zip: everything packaged for download
16. How to compile and run it
On macOS or Linux you can compile it directly:
gcc Iris_sort_K_mean.c -lm -o iris_kmeans
./iris_kmeans Iris.csv
If the data file sits in the same directory as the program, you can also just run:
./iris_kmeans
The program will try to read Iris.csv from the current directory by default.
17. What this example is really worth learning
Treating this article as algorithm study material, what is worth taking away is not any single line of C, but these ideas:
- K-means is fundamentally “assign to the nearest centre, update the centre to the mean”
- distance-based algorithms depend heavily on data preprocessing, and standardisation is critical
- K-means++ noticeably improves initialisation quality
- clustering algorithms with randomness usually need multiple restarts before choosing the best
- the final result must be interpreted alongside the output distribution and the visualisation
18. Experiment audit table
K-means output can easily look finished, but unsupervised learning needs its experimental conditions recorded even more carefully. The table below is for re-checking whether this Iris clustering experiment genuinely explains its result, rather than only presenting an attractive scatter plot.
| Audit item | What this experiment does | Why it affects the conclusion |
|---|---|---|
| Feature scaling | Standardises all 4 numeric features. | Euclidean distance is dominated by large-scale features, so unscaled results are not directly comparable. |
| Initialisation | Uses K-means++ to pick more widely spread initial centres. | Initial centres that sit too close lead to poor local solutions, changing both SSE and the cluster distribution. |
| Random restarts | RESTARTS = 2000, keeping the result with the smallest SSE. |
A single run cannot represent stable behaviour; multiple restarts reduce the role of chance. |
| Comparison against true labels | species is not used during clustering, only counted at output time. |
This separates two questions: unsupervised cluster structure, and true species classification. |
| Two-dimensional visualisation | Projects a scatter plot using petal length and petal width. | The plot shows only two dimensions and cannot replace the SSE explanation in the full four-dimensional space. |
| Failure modes | States that versicolor and virginica overlap. |
Clustering cannot magically recover labels; overlapping regions need explaining through distributions and samples. |
19. Summary
Working from Iris.csv and Iris_sort_K_mean.c together, the complete K-means flow becomes very clear:
- read the data
- standardise it
- initialise the centres with K-means++
- repeat “assign samples / update centres” until convergence
- run multiple restarts and select the result with the smallest SSE
For a beginner, the greatest value of this example is that it keeps the core logic of the algorithm while staying close enough to a real program. You do not only understand the concept; you see how the concept turns into code and results.
If you are just starting with unsupervised learning, I would strongly suggest compiling it once yourself, changing a few parameters, and reading the flowchart and the scatter plot alongside the output. That builds real intuition far better than reading the algorithm definition once.