Getting Started with K-means Clustering: A C Implementation on the Iris Dataset
Getting Started with K-means Clustering: A C Implementation on the Iris Dataset
Search
Ask the AI

Getting Started with K-means Clustering: A C Implementation on the Iris Dataset

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 dataset
  • Iris_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:

  1. Why K-means is designed the way it is
  2. What each function in the code is actually responsible for
  3. Why standardisation, initialisation and repeated restarts directly affect the result
  4. 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 into K groups so that samples within a group are as close together as possible.

It repeats two things continuously:

  1. Assign each sample to the nearest cluster centre
  2. 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-setosa
  • Iris-versicolor
  • Iris-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:

  1. Read the CSV and build the sample array
  2. Standardise every feature
  3. Initialise the cluster centres with K-means++
  4. Repeat “assign samples / update centres”
  5. Restart randomly many times and keep the round with the smallest SSE
  6. Print the cluster distribution, the label distribution and each sample’s cluster
Iris K-means algorithm flowchart
This flowchart corresponds to the execution order in Iris_sort_K_mean.c: load the data, then standardise, initialise, iterate, compute SSE, and finally keep the best clustering result.
The most useful thing to notice in this diagram: it does not stop after running K-means once; it separates a single clustering run from the outer loop that restarts and keeps the best result. For an algorithm with random initialisation, that structure matters.

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 column
  • x: the raw 4-dimensional features
  • x_scaled: the standardised 4-dimensional features
  • species: the true species label
  • cluster: 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 current download checks the fixed header in load_iris(), splits six unquoted fields in parse_sample(), and validates the ID and four finite numbers with strtol() and strtod(). Malformed rows, duplicate IDs, unknown species and more than 150 rows cause an error rather than silently skipping or truncating data.

You can think of this function as a text-to-struct process:

  1. the file holds one line of comma-separated text
  2. after reading it becomes one Sample
  3. 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

A replacement must keep the same column order and species names, with K to 150 rows. This is not a general CSV importer: quoted commas, multiline fields and automatic missing-value imputation are unsupported.

6. Step two: how standardisation changes distance

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:

  1. compute the mean mean of each feature column
  2. compute the standard deviation std of each feature column
  3. convert each sample’s raw values into standardised values

Variance is divided by n (ddof=0). Nonconstant columns have approximately zero mean and unit standard deviation; constant columns use a divisor of 1 and become zero. Values are not restricted to [-1,1]. Each column receives a new squared-distance weight of 1/std².

Scaling chooses a distance model; it does not guarantee a better answer. All four Iris measurements are in centimetres. Scaling can increase the weight of weaker features. The measured comparison below agrees better with species labels without scaling; that is not a universal preprocessing rule.

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():

  1. pick one centre at random first
  2. 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.

Sampling weights use the minimum squared distance D² to an existing centre: D²/sum(D²), not D. The current implementation uses a [0,1) draw, a strictly greater cumulative threshold and skips zero weights, avoiding reselection of an occupied centre when the draw is exactly zero. C rand() does not guarantee identical sequences across runtimes or global optimality.

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:

  1. collect which samples belong to each cluster
  2. sum the 4 features across all samples in each cluster
  3. 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.

An empty cluster retains its old centre; it is not reseeded. K=3 therefore does not guarantee three populated groups. Three identical coordinate vectors all enter cluster 0 under the tie rule, giving SSE=0 and two empty clusters, not evidence of three meaningful populations.

10. Step six: when the algorithm stops

The program sets two stopping conditions:

  1. if no sample changed cluster this round, it has converged and can stop
  2. if the iteration count reaches MAX_ITER, it must also stop, to prevent an infinite loop in extreme cases
#define MAX_ITER 1000

The current version prints converged=yes/no. Reaching the limit is not convergence: updated centres are member means, but members need not still have the nearest centre. Executed steps are counted separately; MAX_ITER=1 reports one step and nonconvergence, rather than an incorrect two steps.

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

The program makes 2000 initialisations along one pseudorandom sequence and keeps the smallest SSE. This is an example search budget, not a minimum requirement for Iris. More restarts do not prove global optimality.

SSE means:

the sum of squared distances from every sample to the centre it belongs to.

Lower SSE improves the same objective only with fixed samples, K and scaling. Raw-centimetre and standardised SSE values cannot be directly ranked. Changing K or the dataset also changes the comparison conditions.

The intuition worth building: K-means is not an algorithm that gets it right forever given one attempt. It carries randomness by design, so trying several times is the normal strategy, not redundant work.

12. What the program prints

When Iris_sort_K_mean.c finishes, it prints three kinds of information:

  1. the iteration count and SSE of the best result
  2. the cluster centres in standardised space
  3. the sample count and true label distribution of each cluster

This excerpt comes from running the current download with ./iris_kmeans Iris.csv 42 standard on macOS with Apple clang 21.0.0. Complete output and all 150 assignments are in reference/, rather than separately invented example numbers:

K-means 重启次数: 2000
最佳结果迭代次数: 5
最佳 SSE: 140.96581663074701

Cluster 0: total=53, setosa=0, versicolor=39, virginica=14
Cluster 1: total=47, setosa=0, versicolor=11, virginica=36
Cluster 2: total=50, setosa=50, versicolor=0, virginica=0

The Chinese headings report restarts, iterations of the selected run, and SSE. A cluster number is an arbitrary identifier, not a species code.

This result shows that:

  • setosa can be separated out almost cleanly
  • versicolor and virginica overlap 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.

Two-dimensional visualisation of Iris K-means clustering
A petal projection from a historical run, illustrating shape. Colour IDs need not match the current seed=42 IDs; reference assignments define this run. A two-dimensional plot is not a four-dimensional decision boundary.

The plot shows three things quickly:

  1. the clearly separated small cluster corresponds to setosa
  2. the other two groups are broadly separable, but their boundaries overlap
  3. 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 3
  • RESTARTS: the restart count; larger is more stable but takes longer
  • MAX_ITER: the maximum iterations of a single run

A few experiments to try:

  1. change K to 2 or 4 and see how the cluster structure changes
  2. reduce RESTARTS and observe whether SSE fluctuates more
  3. hold the seed fixed and compare standard with raw without assuming which must agree better with labels

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 dataset
  • Iris_sort_K_mean.c: the tidied C implementation
  • iris-kmeans-flowchart.svg: the flowchart
  • iris-kmeans-cluster-visual.svg: the clustering visualisation
  • Download the verified experiment package: source, original CSV, checks, dependency versions and full results

16. How to compile and run it

On macOS or Linux you can compile it directly:

cc -std=c11 -O2 -Wall -Wextra -Werror -pedantic 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 default reads Iris.csv and prints a time-based seed. Reproduce with ./iris_kmeans Iris.csv 42 standard, or use ./iris_kmeans Iris.csv 42 raw for the unscaled comparison. Record input order, compiler and C runtime; a fixed seed does not promise identical cross-platform output.

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:

  1. K-means is fundamentally “assign to the nearest centre, update the centre to the mean”
  2. distance-based algorithms depend heavily on data preprocessing, and standardisation is critical
  3. K-means++ noticeably improves initialisation quality
  4. clustering algorithms with randomness usually need multiple restarts before choosing the best
  5. 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. Scaling changes distance weights. SSE values in different spaces are not directly ranked; retaining raw units depends on the task.
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. Why your SSE may differ from another Iris tutorial

Check the data before increasing restarts. The site’s CSV has SHA-256 600ac44f23c2e6e0ae37daac8ceb2baba4df963efa580eb31b3b576b28e34c55; its bytes are preserved. An element-by-element comparison with scikit-learn 1.7.2 load_iris() finds only these two differing records:

Record Id Site CSV: four features scikit-learn 1.7.2
35 4.9, 3.1, 1.5, 0.1 4.9, 3.1, 1.5, 0.2
38 4.9, 3.1, 1.5, 0.1 4.9, 3.6, 1.4, 0.1

The UCI Iris documentation also identifies these historical differences. Ids are one-based and exclude the header: the name “Iris” does not establish identical input. The check exports the scikit-learn version into a temporary directory without overwriting the original CSV.

Input and distance space SSE / best one-to-one label match ARI
Site CSV, standard 140.965817 / 125/150 0.620135
scikit-learn, standard 139.820496 / 125/150 0.620135
Site CSV, raw 78.940841 / 134/150 0.730238
scikit-learn, raw 78.851441 / 134/150 0.730238

All four runs use the same C source, seed=42, K=3 and 2000 restarts. The data version changes SSE even though label-agreement summaries are unchanged here. ARI does not depend on cluster numbering. The matched counts enumerate all 3! cluster-to-species mappings; they are not held-out classification accuracy. All 150 samples participate in clustering. Labels describe the result afterwards and do not select centres or the winning restart. Better agreement with raw distances is this experiment’s observation, not a universal preprocessing rule.

Verify individual claims instead of trusting a success message

python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements.txt
python audit_iris.py --output my-audit

The reference environment uses Python 3.13.9, NumPy 2.3.5 and scikit-learn 1.7.2. The check compiles the shipped C file, parses every assignment and full-precision centre, and uses NumPy to recalculate scaling, means, nearest centres, SSE and the contingency table. It does not substitute another C solver. Rotating only species labels leaves assignments unchanged for fixed coordinates and seed.

  • reference/download-standard-assignments.csv provides 150 traceable assignments; reference/audit.json records hashes, environment, four runs and check scope.
  • Twelve malformed CSV cases and four invalid seeds must fail. The previous program accepted NaN, printed SSE: nan and returned exit status 0; the current loader rejects it.
  • Constant columns, duplicate coordinates and empty clusters do not produce NaN. MAX_ITER=1 reports nonconvergence; K=2 and K=4 preserve assignment/centre invariants.
  • The full Iris run passes AddressSanitizer and UndefinedBehaviorSanitizer. A missing compatible compiler or sanitizer produces a failure, not a successful verification.

These checks cover the teaching dataset and listed boundaries, not global optimality for arbitrary data, identical random trajectories across platforms, or production robustness. See the K-means documentation for algorithm background and load_iris 1.7 for the dataset interface.

20. 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.

Leave a Reply

Scroll down