Espartaco

“Is that to say we are against Free Trade? No, we are for Free Trade, because by Free Trade all economical laws, with their most astounding contradictions, will act upon a larger scale, upon the territory of the whole earth; and because from the uniting of all these contradictions in a single group, where they will stand face to face, will result the struggle which will itself eventuate in the emancipation of the proletariat.”

Karl Heinrich Marx · Marx-Engels Collected Works, Vol. VI, p. 290

EnglishEspañol

CONSTRUCTION AND INTERPRETATION OF THE SILHOUETTE COEFFICIENT IN K-MEANS GROUPINGS

Marxist Philosophy of Science · Quantitative methods

Constructing and Interpreting the Silhouette Coefficient in k-means

Cohesion, separation, and choosing the number of clusters: foundations, calculations, and reproducible diagnostics.

Originally published:
Revised:

The quality of a clustering cannot be understood by counting its groups. It is examined through the relationships drawing each observation toward its assigned cluster and toward alternatives. The silhouette turns that comparison into an interpretable measure when its construction and limits are made explicit.

01. What is being validated?

Creating clusters and justifying them are different tasks. An algorithm can return a partition even when the data contain no clusters useful for the research question. The computational result, the geometric criterion used to assess it, and the interpretation of the phenomenon therefore need to be distinguished.

The silhouette coefficient, introduced by Rousseeuw (1987), examines how close an observation is to its assigned cluster relative to an alternative. It diagnoses a partition under the chosen dissimilarities; it does not establish, on its own, that the groups represent natural classes.

ApproachQuestionExample
Internal validationWhat structure does the partition display under the data and dissimilarities?Silhouette; Calinski–Harabasz or Davies–Bouldin indices.
External validationHow does the partition relate to an independent reference classification?Adjusted Rand index when comparable reference labels are available.
Relative comparisonWhich solution is preferable among specified alternatives?Comparing silhouettes across candidate values of k.

These perspectives can overlap: an internal measure can be used to compare solutions. External labels are not automatically the truth about the phenomenon, and their availability does not imply that the number of geometric clusters must match the number of classes.

02. What k-means optimizes

Let x1, …, xn be numerical vectors and C1, …, Ck a partition into nonempty clusters. In standard k-means, each center μg is the arithmetic mean of the observations assigned to its cluster. The objective is to reduce within-cluster sum of squares:

W(k) = ∑g=1k ∑i∈Cg ‖xi − μg‖2,μg = (1 / |Cg|) ∑i∈Cg xi.

Common algorithms can terminate at local optima. Multiple initializations reduce dependence on a single starting choice, but provide no universal guarantee of a global optimum. In R, nstart specifies how many initial sets of centers are tried (Hartigan & Wong, 1979; R Documentation, n.d.-b).

The learned centroids should be distinguished from configuration choices: k, the initialization strategy, the number of attempts, and the iteration limit. The random seed controls reproducibility; it is not a substantive property of the data.

Euclidean geometry is part of standard k-means. If another dissimilarity is needed, a compatible procedure must be justified, such as k-medoids for appropriate dissimilarity matrices. Evaluating an existing partition with a different dissimilarity is possible, but changes the diagnostic question and must be reported.

03. Constructing the silhouette coefficient

Fix a partition and a dissimilarity d(i, j) that is nonnegative, symmetric, and zero on the diagonal. The classical construction uses pairwise dissimilarities rather than replacing each cluster with its centroid. Here we use Euclidean distance, which reduces to the absolute difference in one dimension.

1. Average within-cluster dissimilarity: a(i)

If C(i) is the cluster containing observation i and contains at least two observations, average the dissimilarities from i to its other members. The self-distance is excluded:

a(i) = ∑j∈C(i), j≠i d(i, j)|C(i)| − 1.

2. Dissimilarity to each alternative cluster

For every cluster C that does not contain i, calculate a separate average:

d̄(i, C) = ∑j∈C d(i, j)|C|.

3. Choosing the neighboring cluster: b(i)

The smallest of those averages defines b(i). Average within each alternative cluster first, then take the minimum:

b(i) = minC≠C(i) d̄(i, C).

The neighboring cluster depends on observation i. It is not necessarily found by locating the nearest external point, the closest centroid, or the cluster closest to C(i) as a whole. If several alternatives tie for the minimum average, b(i) remains defined even though the neighbor label may not be unique.

4. Normalization

s(i) = b(i) − a(i)max{a(i), b(i)},   −1 ≤ s(i) ≤ 1.

The numerator expresses the advantage of the current assignment over the alternative; the denominator places the values on a bounded scale. When the denominator is positive, an equivalent expression makes the sign explicit:

ConditionEquivalent expressionReading
a(i) < b(i)s(i) = 1 − a(i)/b(i)The assigned cluster is closer on average.
a(i) = b(i) > 0s(i) = 0Equal average dissimilarity to the two options.
a(i) > b(i)s(i) = b(i)/a(i) − 1The alternative cluster is closer on average.

The formula and the singleton convention correspond to the classical silhouette implemented in cluster::silhouette() (R Documentation, n.d.-a).

Multiplying every dissimilarity by the same positive constant leaves s(i) unchanged: the common factor cancels between numerator and denominator. Scaling individual features differently can change both the partition and its evaluation.

04. A worked example: from distances to the score

Consider nine one-dimensional observations and a fixed partition: A = {0, 1, 2}, B = {6, 7, 8}, and C = {12, 13, 14}. These are synthetic data with no empirical interpretation attached. We use d(i, j) = |xi − xj|.

Hand-worked example: three groups on a lineA: 0, 1, 2. B: 6, 7, 8. C: 12, 13, 14. i = 2; a(i) = 1.5; b(i) = 5; s(i) = 0.70. 2026-11-11T17:01:32.009716 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ 0 1 2 6 7 8 12 13 14 i = 2 A = {np.int64(0), np.int64(1), np.int64(2)} B = {np.int64(6), np.int64(7), np.int64(8)} C = {np.int64(12), np.int64(13), np.int64(14)}
Figure 1. Positions of the nine observations. Marker shapes distinguish clusters; the arrow identifies observation x = 2.

For x = 2, the assigned cluster is A. The table contains every relevant distance:

DestinationObservationsDistances from x = 2Average
A, excluding itself0, 12, 1a(i) = (2 + 1)/2 = 1.5
B6, 7, 84, 5, 6d̄(i, B) = 15/3 = 5
C12, 13, 1410, 11, 12d̄(i, C) = 33/3 = 11
b(i) = min{5, 11} = 5,s(i) = (5 − 1.5)/5 = 0.70.

The observation has lower average dissimilarity to its assigned cluster than to B, the closest alternative. The score 0.70 expresses that normalized relationship; it is neither a membership probability nor an accuracy of 70%.

How zero and negative scores arise

Now change only the third point in A: A = {0, 1, t}. B and C retain their values. Each row below represents a different modified dataset, with labels held fixed:

ta(t)b(t)s(t)
21.550.700000
3.753.253.250.000000
54.52−0.555556
s(5) = (2 − 4.5)/4.5 = −5/9 ≈ −0.555556.

At t = 3.75, the two relevant average dissimilarities are equal. At t = 5, B is closer on average than A. These cases diagnose specified partitions; they are not presented as outcomes of refitting k-means.

05. Individual, cluster-level, and overall interpretation

An s(i) close to 1 indicates high relative cohesion compared with the neighboring cluster; around zero, the average dissimilarities are similar; a negative score indicates that the alternative is closer on average. These readings depend on the data, features, and chosen dissimilarity.

A negative silhouette invites closer examination. It does not, by itself, prove a programming error or require reassignment: the k-means objective and the silhouette criterion differ. Nor does it establish that the observation is an outlier.

s̄g = (1 / ng) ∑i∈Cg s(i),s̄ = (1 / n) ∑i=1n s(i)= ∑g=1k (ng / n) s̄g.

The overall mean weights each cluster by its size. Taking an unweighted average of cluster means answers another question and agrees with the overall mean only in particular cases, such as equally sized clusters.

Table 3. Complete results for the original hand-worked example.
xClustera(i)b(i)s(i)
0A1.57.00.785714
1A1.06.00.833333
2A1.55.00.700000
6B1.55.00.700000
7B1.06.00.833333
8B1.55.00.700000
12C1.55.00.700000
13C1.06.00.833333
14C1.57.00.785714

The cluster means are s̄A = 0.773016, s̄B = 0.744444, and s̄C = 0.773016. Because each cluster contains three observations, s̄ = 481/630 ≈ 0.763492.

Individual silhouettes sorted within clustersReference mean = 0.763492; moving the third point to x = 5 gives s(5) = -0.555556. Dashed lines indicate each partition mean. Each horizontal bar represents one observation. 2026-11-11T17:01:32.105839 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ −1 0 1 s(i) A · 1 A · 0 A · 2 B · 7 B · 8 B · 6 C · 13 C · 14 C · 12 Mean = 0.76349 Reference partition −1 0 1 s(i) A · 1 A · 0 A · 5 B · 7 B · 8 B · 6 C · 13 C · 14 C · 12 Mean = 0.56036 Moved point: x = 5
Figure 2. Silhouettes sorted within each cluster. Left: the reference partition. Right: the modified dataset with x = 5. Each bar represents an observation; dashed lines indicate the mean of each partition.

Read the plot by examining bar lengths, negative scores, cluster sizes, and differences between clusters. The display reveals the distribution summarized by a single mean (Rousseeuw, 1987).

06. Comparing k without treating the maximum as truth

For a candidate set K, one can select the partition with the largest mean silhouette:

k̂ ∈ arg maxk∈K s̄(k).

This selection is relative to K, the data, preprocessing, and fitting procedure. It neither confirms that clusters exist nor assesses k = 1. When several solutions score similarly, retain them for graphical and substantive examination.

To avoid the symmetric ties in the hand-worked example, now use a different synthetic dataset: {0, 0.8, 2, 6, 7.3, 8, 12, 13.2, 15.5}. All values share one unit. The table reports solutions minimizing W(k) for this small one-dimensional problem; the code includes an exhaustive check over contiguous partitions.

Table 4. Second example: these scores must not be confused with the hand-worked mean of 0.763492.
kW(k)Mean silhouetteMinimum sizeSingleton clusters
267.4550000.58209830
310.4133330.72121130
44.8066670.62866111
52.9916670.49125612
61.2850000.35336413
Comparing k: silhouette and sum of squaresFor the irregular nine-point example, mean silhouettes for k = 2, 3, 4, 5, 6 are 0.582098, 0.721211, 0.628661, 0.491256, 0.353364. Within-cluster sum of squares decreases throughout. 2026-11-11T17:01:32.286744 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ 2 3 4 5 6 k 0.0 0.2 0.4 0.6 0.8 1.0 Mean silhouette k = 3 0.72121 2 3 4 5 6 k 0 10 20 30 40 50 60 70 Within-cluster sum of squares
Figure 3. In the second example, W(k) decreases as k increases, while the mean silhouette reaches its maximum among the candidates at k = 3, with a score of 0.721211.

For k = 3, the clusters are {0, 0.8, 2}, {6, 7.3, 8}, and {12, 13.2, 15.5}. Here, further reductions in W(k) from splitting the data are not accompanied by higher silhouettes. Singleton clusters in later solutions contribute zero silhouette by convention.

CriterionWhat it examinesHow to use it
Elbow in W(k)Marginal improvement as the number of centroids increases.Look for slope changes; a distinct elbow may be absent.
SilhouetteRelative cohesion against the best alternative cluster.Compare means and within-cluster distributions.
Initialization sensitivityVariation when random seeds change.Repeat complete fits while keeping preprocessing fixed.
Sampling stability and interpretationPersistence and relevance of clusters for the problem.Examine resampling, profiles, and domain knowledge.

These criteria provide complementary evidence. Sensitivity to the seed and stability under changes to the sample are different properties: repeating random seeds does not estimate sampling uncertainty.

07. A reproducible, audited implementation in R

The following script contains the data, an independent implementation of the formula, checks of the hand-worked example, fitting with multiple initializations, and comparison of k = 2, …, 6. For each k, kmeans() retains the best fit according to within-cluster sum of squares, not silhouette (R Documentation, n.d.-b).

The code also checks the minimum sum of squares by enumeration for this small univariate case. That audit exploits n = 9 and the ordered line; it should not be generalized into an efficient strategy for large multivariate problems. Numerical cluster labels can be permuted without changing the partition.

# Silhouette coefficient: worked examples and k-means comparison
# Original article: 2022-04-21 | Revision: 2026-11-11
# Dependency: install.packages("cluster") if it is not already available.
if (!requireNamespace("cluster", quietly = TRUE)) {
  stop('Install the cluster package first: install.packages("cluster")')
}

# Independent calculation from a complete dissimilarity matrix.
silhouette_manual <- function(labels, D) {
  D <- as.matrix(D)
  n <- length(labels)
  stopifnot(n >= 3L, !anyNA(labels), is.numeric(D),
            all(dim(D) == c(n, n)), all(is.finite(D)),
            all(D >= 0), max(abs(D - t(D))) < 1e-10,
            max(abs(diag(D))) < 1e-10)
  groups <- unique(labels)
  stopifnot(length(groups) >= 2L, length(groups) <= n - 1L)
  s <- numeric(n)
  for (i in seq_len(n)) {
    own <- which(labels == labels[i])
    if (length(own) == 1L) {
      s[i] <- 0
      next
    }
    a <- mean(D[i, setdiff(own, i)])
    other <- groups[groups != labels[i]]
    means <- vapply(other, function(g) mean(D[i, labels == g]), numeric(1))
    b <- min(means)
    s[i] <- if (max(a, b) == 0) 0 else (b - a) / max(a, b)
  }
  s
}

# 1. Exact hand-worked example; distances are absolute differences.
x_hand <- matrix(c(0, 1, 2, 6, 7, 8, 12, 13, 14), ncol = 1)
labels_hand <- rep(1:3, each = 3)
D_hand <- dist(x_hand, method = "euclidean")
s_hand <- cluster::silhouette(labels_hand, D_hand)
stopifnot(max(abs(silhouette_manual(labels_hand, D_hand) -
                  s_hand[, "sil_width"])) < 1e-12,
          abs(s_hand[3, "sil_width"] - 0.7) < 1e-12,
          abs(mean(s_hand[, "sil_width"]) - 481 / 630) < 1e-12)
print(data.frame(x = x_hand[, 1], cluster = labels_hand,
                 silhouette = round(s_hand[, "sil_width"], 6)))

# Moving only the third point creates positive, zero and negative examples.
for (t in c(2, 3.75, 5)) {
  xt <- x_hand
  xt[3, 1] <- t
  actual <- cluster::silhouette(labels_hand, dist(xt))[3, "sil_width"]
  a <- mean(abs(t - c(0, 1)))
  b <- min(mean(abs(t - c(6, 7, 8))), mean(abs(t - c(12, 13, 14))))
  expected <- (b - a) / max(a, b)
  stopifnot(abs(actual - expected) < 1e-12)
  cat(sprintf("t = %.2f; a = %.2f; b = %.2f; s = %.6f\n", t, a, b, actual))
}

# Check singleton, coincident-point and zero-denominator conventions.
for (case in list(
  list(x = c(0, 1, 10), labels = c(1L, 1L, 2L)),
  list(x = c(0, 0, 0, 0), labels = c(1L, 1L, 2L, 2L)),
  list(x = c(0, 0, 10, 10), labels = c(1L, 1L, 2L, 2L))
)) {
  dd <- dist(matrix(case$x, ncol = 1))
  reference <- cluster::silhouette(case$labels, dd)[, "sil_width"]
  stopifnot(all(is.finite(reference)),
            max(abs(silhouette_manual(case$labels, dd) - reference)) < 1e-12)
}

# 2. Irregular synthetic data: avoids the ties of the hand-worked example.
x <- matrix(c(0, 0.8, 2, 6, 7.3, 8, 12, 13.2, 15.5), ncol = 1,
            dimnames = list(paste0("P", 1:9), "x"))
stopifnot(is.numeric(x), all(is.finite(x)), nrow(unique(x)) >= 6L)
# All observations use one common unit. No scaling is needed here.
# For multivariate data, justify feature selection and scaling before fitting.
# x <- scale(x)  # Optional; do not standardize constant columns.
D <- dist(x, method = "euclidean")
ks <- 2:6
models <- vector("list", length(ks))
silhouettes <- vector("list", length(ks))
rows <- vector("list", length(ks))
for (j in seq_along(ks)) {
  k <- ks[j]
  set.seed(20220421L + k)
  fit <- kmeans(x, centers = k, nstart = 100, iter.max = 100,
                algorithm = "Hartigan-Wong")
  if (!is.null(fit$ifault) && fit$ifault != 0L) stop("k-means did not converge")
  s <- cluster::silhouette(fit$cluster, D)
  stopifnot(max(abs(silhouette_manual(fit$cluster, D) -
                    s[, "sil_width"])) < 1e-12)
  models[[j]] <- fit
  silhouettes[[j]] <- s
  rows[[j]] <- data.frame(k = k, within_ss = fit$tot.withinss,
                          mean_silhouette = mean(s[, "sil_width"]),
                          negative_fraction = mean(s[, "sil_width"] < 0),
                          smallest_cluster = min(fit$size),
                          singleton_clusters = sum(fit$size == 1L))
}
results <- do.call(rbind, rows)
print(results, digits = 7, row.names = FALSE)
best <- which.max(results$mean_silhouette)
cat("Selected k among the candidates:", results$k[best], "\n")
print(summary(silhouettes[[best]]))

# Audit the small ONE-DIMENSIONAL example against all contiguous partitions.
# This is feasible only because n = 9; it is not a general-purpose workflow.
# For sorted univariate squared-error k-means, a global optimum is contiguous.
x_sorted <- sort(x[, 1])
exact_sse <- vapply(ks, function(k) {
  cuts <- combn(seq_len(length(x_sorted) - 1L), k - 1L)
  min(apply(cuts, 2, function(cut) {
    starts <- c(1L, cut + 1L)
    ends <- c(cut, length(x_sorted))
    sum(vapply(seq_len(k), function(g) {
      z <- x_sorted[starts[g]:ends[g]]
      sum((z - mean(z))^2)
    }, numeric(1)))
  }))
}, numeric(1))
stopifnot(max(abs(results$within_ss - exact_sse)) < 1e-10,
          results$k[best] == 3L,
          abs(results$mean_silhouette[best] - 0.7212109713028838) < 1e-12)

# 3. Repeat the entire fitting procedure with different seeds.
# These ranges describe initialization sensitivity, not sampling uncertainty.
seeds <- 1:20
stability <- do.call(rbind, lapply(ks, function(k) {
  means <- vapply(seeds, function(seed) {
    set.seed(seed)
    fit <- kmeans(x, centers = k, nstart = 100, iter.max = 100)
    mean(cluster::silhouette(fit$cluster, D)[, "sil_width"])
  }, numeric(1))
  data.frame(k = k, minimum = min(means), median = median(means),
             maximum = max(means))
}))
print(stability, digits = 7, row.names = FALSE)

# 4. Export the complete analysis to the current working directory.
write.csv(results, "silhouette_k_comparison.csv", row.names = FALSE)
write.csv(stability, "silhouette_initialization_sensitivity.csv", row.names = FALSE)
write.csv(data.frame(id = rownames(x), x = x[, 1],
                     cluster = models[[best]]$cluster,
                     silhouette = silhouettes[[best]][, "sil_width"]),
          "silhouette_individual_results.csv", row.names = FALSE)
paper <- "#fffdf7"
ink <- "#2c2418"
copper <- "#8b4513"
pdf("silhouette_diagnostics.pdf", width = 7, height = 6, bg = paper)
par(col = ink, col.axis = ink, col.lab = ink, col.main = ink,
    fg = ink, bg = paper)
plot(results$k, results$mean_silhouette, type = "b", pch = 19,
     col = copper, xlab = "Number of clusters (k)",
     ylab = "Mean silhouette", ylim = c(0, 1))
plot(silhouettes[[best]], col = copper, border = NA,
     main = paste("Silhouette | k =", results$k[best]))
abline(v = results$mean_silhouette[best], lty = 2, col = ink)
plot(results$k, results$within_ss, type = "b", pch = 19,
     col = copper, xlab = "Number of clusters (k)",
     ylab = "Within-cluster sum of squares")
dev.off()
writeLines(capture.output(sessionInfo()), "silhouette_session_info.txt")

In addition to the displayed tables, the script writes three CSV files, a PDF containing diagnostic plots, and a session-information file. Files are created in R’s current working directory. The twenty-seed repetition describes initialization sensitivity in this example; it is not a confidence interval.

08. Limitations and choices that must be reported

The silhouette tends to favor convex, separated clusters under the chosen geometry. Elongated, chained, or density-defined structures may score poorly even when they matter for the problem. Comparing algorithms therefore requires a shared interpretation of what constitutes a cluster (scikit-learn developers, n.d.).

  • Features and scales. Units and weights change dissimilarities. Standardization is an analytical choice, not an automatic correction appropriate for every problem.
  • Outliers and unequal sizes. Examine their effect on averages and small clusters. Report cluster means, sizes, and problematic observations alongside the overall mean.
  • High dimensionality. Assess whether the selected distances retain useful contrasts. A projection used for visualization does not replace the geometry used to fit and evaluate the model.
  • Candidates and comparability. Use the same observations and dissimilarity for a direct comparison; document any changes.
  • Computational cost. A full distance matrix requires memory of order n². Sampling can reduce the diagnostic cost, but specify whether averages use only the subsample or all observations as references.
  • Absence of structure. Examine suitable reference scenarios or stability before assigning meaning to a maximum. An available partition is not equivalent to confirmed empirical structure.

When using resampling, describe observation selection, refitting, and the comparison between partitions. Cluster labels are arbitrary: directly comparing their numerical values does not measure stability.

09. Mathematical note: cluster geometry and topology

This revision preserves the mathematical interest of Nabi (2022) while clarifying its vocabulary. Similar terminology does not justify identifying different definitions.

In clusteringIn topology or algebraNecessary distinction
Geometric cohesion or compactnessCompactness: every open cover has a finite subcover.Every finite set is compact, even when its points are far apart.
Separation between clustersSeparability: existence of a countable dense subset.It does not quantify between-cluster distance; ℝᵖ is separable in its usual topology.
Neighborhood connectivityConnectedness: absence of a separation into disjoint nonempty relatively open sets.A neighbor graph and the topology of the observed point set are different constructions.
A subset of observationsVector subspace: contains zero and is closed under addition and scalar multiplication.An arbitrary cluster of observations need not satisfy these conditions.

Two examples make the difference explicit. {0, 1} and {0, 1000} are compact subsets of ℝ, although their diameters are 1 and 1000. Every finite set of distinct points in ℝᵖ also has the discrete relative topology: a sufficiently small ball isolates each point. With more than one point, the set is disconnected even when it looks like a very tight cloud.

For a subset C of a topological space, a relevant separation would be C = U ∪ V, where U and V are nonempty, disjoint, relatively open subsets of C: U ∩ V = ∅. Their union reconstructs C; their intersection records shared elements. C is connected when no such separation exists.

An open Euclidean ball contains points whose distance from its center is less than its radius; the sphere consists of points at exactly that distance. Neither definition alone turns a statistical cluster into a connected region.

These distinctions use definitions of metric spaces and compactness ({cite(‘lebl’,’Lebl, n.d.’)}). Rigorous links to topology require explicitly defining an additional structure, such as a graph or a union of balls, and then studying its properties.

10. How to report the result

A useful report identifies the data and features, preprocessing, algorithm and options, evaluation dissimilarity, candidate values of k, and sensitivity of the result. It accompanies the overall mean with cluster sizes and cluster-level diagnostics.

For nine synthetic one-dimensional observations, k = 2, …, 6 were compared using k-means and Euclidean distance. The largest mean silhouette among the candidates was 0.721211 at k = 3. This choice was interpreted alongside within-cluster sum of squares, cluster sizes, and individual diagnostics.

The conclusion states which evidence supports the selected solution and under what conditions. Its ultimate usefulness depends on whether the clusters help answer the research question.

11. References

Follow the blogSeguí al blog

Comments

Leave a Comment/Deja un Comentario