HOWTO · R
How to Perform K-Means Clustering in Base R
This article shows how to perform K-means clustering using base R functions. All steps, including the elbow bend plot to choose K, and the PCA scatter plot when there are more than two variables, are demonstrated in base R.
On this page
Use base R’s kmeans() to group numeric observations, inspect the resulting cluster assignments and tot.withinss, and compare candidate values of K with an elbow plot. For two variables, inspect the input observations in a scatter plot; for more variables, use prcomp() to view a two-component projection colored by the fitted assignments. The examples below use a reproducible synthetic data set.
About K-Means Clustering in R
K-means is an unsupervised method: it partitions observations into a chosen number of groups using distances to cluster centers. It does not use a dependent or target variable. Its input should contain numeric measurements; a factor remains categorical even if its levels are represented by numbers, so convert a factor only when those codes represent meaningful quantitative values.
K-means assigns every observation to a cluster and can be sensitive to outliers. It is most useful when groups are reasonably compact and separated; strongly overlapping groups may not have a meaningful partition. Consider whether this distance-based method fits the variables and question before interpreting its result.
the kmeans() Function in R
Base R provides kmeans(x, centers, nstart) for fitting a model. x is a numeric matrix or data frame, centers is either the requested number of clusters or initial center values, and nstart controls how many random sets of starting centers are tried when centers is a number.
Each initialization can lead to a different local solution. With numeric centers, nstart lets kmeans() keep the attempted fit with the lowest total within-cluster sum of squares; it does not guarantee a global optimum. Larger values can make the result less dependent on one starting configuration, at additional computation cost.
K-Means Model in R
This self-contained example creates three groups of two-variable observations, fits a model, and displays its assignments and total within-cluster sum of squares. A cluster label is an arbitrary identifier: the numbers do not show that a grouping is correct, and their order can change between fits.
set.seed(4987)
M1 <- rnorm(13)
set.seed(9874)
M2 <- rnorm(11) + 3
set.seed(8749)
N1 <- runif(13)
set.seed(7498)
N2 <- runif(11) - 2
set.seed(6237)
M3 <- rnorm(14) - 6
set.seed(2376)
N3 <- runif(14) + 1
DF <- rbind(
data.frame(M = M1, N = N1),
data.frame(M = M2, N = N2),
data.frame(M = M3, N = N3)
)
set.seed(9944)
km_1 <- kmeans(DF, centers = 3, nstart = 20)
km_1$cluster
km_1$tot.withinss
[1] 1 1 1 1 1 1 1 1 1 1 1 1 1 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2
[1] 46.40382
the Number of Clusters (K)
The number of groups is not usually known in advance. Compare candidate values of K by fitting a model for each and plotting tot.withinss. The minimum achievable within-cluster sum of squares cannot increase as K grows, because adding a cluster can preserve the previous partition. In practice, scores from separate heuristic kmeans() fits may fluctuate as they reach different local solutions, even with multiple starts, so select a useful trade-off rather than simply choosing the smallest observed score. The elbow is a heuristic: look for a bend after which adding clusters produces smaller improvements, then consider whether that partition is useful for the analysis.
The following standalone example draws the within-cluster sum of squares for candidate values from 1 through 20, matching the range in the figure. Its shape can suggest a value to investigate; it does not establish ground truth. For this constructed data, three is a reasonable candidate to compare with the domain context.
set.seed(4987)
M1 <- rnorm(13)
set.seed(9874)
M2 <- rnorm(11) + 3
set.seed(8749)
N1 <- runif(13)
set.seed(7498)
N2 <- runif(11) - 2
set.seed(6237)
M3 <- rnorm(14) - 6
set.seed(2376)
N3 <- runif(14) + 1
DF <- rbind(
data.frame(M = M1, N = N1),
data.frame(M = M2, N = N2),
data.frame(M = M3, N = N3)
)
set.seed(8520)
candidate_k <- 1:20
within_ss <- vapply(candidate_k, function(k) {
kmeans(DF, centers = k, nstart = 18)$tot.withinss
}, numeric(1))
plot(candidate_k, within_ss, type = "b", xlab = "K", ylab = "Total within-cluster sum of squares")
The existing elbow figure illustrates this comparison. Treat the bend as a prompt for further analysis, not proof that K = 3 is universally or uniquely correct.
Visualize the Clusters in R
For two variables, first inspect the input observations in a scatter plot. This uncolored view shows their positions before interpreting any fitted cluster assignments; it does not depict model labels.
set.seed(4987)
M1 <- rnorm(13)
set.seed(9874)
M2 <- rnorm(11) + 3
set.seed(8749)
N1 <- runif(13)
set.seed(7498)
N2 <- runif(11) - 2
set.seed(6237)
M3 <- rnorm(14) - 6
set.seed(2376)
N3 <- runif(14) + 1
DF <- rbind(
data.frame(M = M1, N = N1),
data.frame(M = M2, N = N2),
data.frame(M = M3, N = N3)
)
plot(DF$M, DF$N, xlab = "M", ylab = "N")
To show the fitted assignments directly, fit kmeans() to the same two-variable data and use km_1$cluster as the col value for each observation. The color at row i therefore corresponds to the assignment for row i; the colors identify model labels, not ranked groups.
set.seed(4987)
M1 <- rnorm(13)
set.seed(9874)
M2 <- rnorm(11) + 3
set.seed(8749)
N1 <- runif(13)
set.seed(7498)
N2 <- runif(11) - 2
set.seed(6237)
M3 <- rnorm(14) - 6
set.seed(2376)
N3 <- runif(14) + 1
DF <- rbind(
data.frame(M = M1, N = N1),
data.frame(M = M2, N = N2),
data.frame(M = M3, N = N3)
)
set.seed(9944)
km_1 <- kmeans(DF, centers = 3, nstart = 20)
plot(DF$M, DF$N, col = km_1$cluster, pch = 19,
xlab = "M", ylab = "N")
When the data has more than two variables, prcomp() can project it onto its first two principal components for a two-dimensional view. The plot compresses the higher-dimensional data, so distances and separation in the projection may not represent every aspect of the original space. This example adds a third numeric variable before fitting and plotting the projection; the horizontal axis is labeled PC1, as in the figure, and the vertical axis is left unlabeled.
set.seed(4987)
M1 <- rnorm(13)
set.seed(9874)
M2 <- rnorm(11) + 3
set.seed(8749)
N1 <- runif(13)
set.seed(7498)
N2 <- runif(11) - 2
set.seed(6237)
M3 <- rnorm(14) - 6
set.seed(2376)
N3 <- runif(14) + 1
O1 <- rnorm(13, 10, 2)
O2 <- rnorm(11, 5, 3)
O3 <- rnorm(14, 0, 2)
DF <- rbind(
data.frame(M = M1, N = N1, O = O1),
data.frame(M = M2, N = N2, O = O2),
data.frame(M = M3, N = N3, O = O3)
)
set.seed(9944)
km_2 <- kmeans(DF, centers = 3, nstart = 20)
pca_mod <- prcomp(DF)
plot(pca_mod$x[, 1:2], col = km_2$cluster, pch = 19, cex = 1.5,
xlab = "PC1", ylab = "")
Other Practical Considerations
- Use
set.seed()before fitting when you need repeatable random initializations in the same R environment. kmeans()requires complete numeric input. If dropping rows with missing values is appropriate for the analysis,na.omit()can remove incomplete observations; check how many rows remain and whether omission changes the population represented.- Variables on larger numeric scales can dominate Euclidean distances.
scale()centers and scales columns, which may help when their units or ranges are not meant to carry different weights. Scaling is a modeling choice, not a requirement; retain the original scale when its differences are meaningful. - If you cluster scaled data, use the same transformed data for the PCA view when you want the plot to reflect the distances used in fitting.
References
The book An Introduction to Statistical Learning introduces K-means clustering using R.