Inference after Hierarchical and k-means clustering
Source:vignettes/hac-km-clustering.Rmd
hac-km-clustering.RmdIntroduction
This vignette demonstrates how to use test.clusters.hc()
and test.clusters.km() functions from the PCIdep package to
perform post-clustering inference on hierarchical agglomerative
clustering (HAC) and k-means clustering
results. We’ll use the Palmer penguins dataset as an example.
This vignette is an adaptation of Lucy L. Gao’s great tutorial.
The Palmer Penguins Data
The Palmer Penguins dataset contains measurements of penguin species collected from the Palmer Station in Antarctica. It includes data on body measurements (bill length, flipper length, body mass) and other characteristics for three penguin species: Adelie, Chinstrap, and Gentoo. This dataset is a useful toy example for demonstrating clustering and classification techniques.
Reference: Horst, A. M., Hill, A. P., & Gorman, K. B. (2020). palmerpenguins: Palmer Archipelago (Antarctica) penguin data. Retrieved from https://github.com/allisonhorst/palmerpenguins
library(palmerpenguins)
#>
#> Attaching package: 'palmerpenguins'
#> The following objects are masked from 'package:datasets':
#>
#> penguins, penguins_raw
data(penguins)
head(penguins)
#> # A tibble: 6 × 8
#> species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
#> <fct> <fct> <dbl> <dbl> <int> <int>
#> 1 Adelie Torgersen 39.1 18.7 181 3750
#> 2 Adelie Torgersen 39.5 17.4 186 3800
#> 3 Adelie Torgersen 40.3 18 195 3250
#> 4 Adelie Torgersen NA NA NA NA
#> 5 Adelie Torgersen 36.7 19.3 193 3450
#> 6 Adelie Torgersen 39.3 20.6 190 3650
#> # ℹ 2 more variables: sex <fct>, year <int>We are interested in classifying individuals based on bill and flipper length, using species as the ground truth for evaluating the clustering results.
data_penguins <- penguins[penguins$sex == "female", c("bill_length_mm", "flipper_length_mm", "species")]
data_penguins <- na.omit(data_penguins)The scatter plot below illustrates the distribution of female penguins in the bill length vs. flipper length space, colored by species. It provides an initial visual assessment of how well these two measurements separate the three penguin species, motivating the use of clustering.
library(ggplot2)
ggplot(data_penguins, aes(x = bill_length_mm, y = flipper_length_mm, color = species)) +
geom_point() +
theme_bw() +
labs(title = "Female Penguins: Bill Length vs Flipper Length")+
labs(x = "Bill Length (mm)", y = "Flipper Length (mm)", color = "Species")
Inference after Hierarchical Clustering with
test.clusters.hc()
Performing HAC
Hierarchical Agglomerative Clustering (HAC) starts with each
observation as its own cluster and iteratively merges the closest
clusters according to a linkage criterion. Here, we use single linkage
(single). After building the dendrogram, we cut it at K=5 to obtain five groups.
# Perform hierarchical clustering
X <- data_penguins[, c("bill_length_mm", "flipper_length_mm")] # Clustering based on bill length and flipper length
hc_result <- hclust(dist(X), method = "ward.D") # HAC with Ward linkage
# Cut the dendrogram to get cluster assignments
num_clusters <- 5
hc_clusters <- cutree(hc_result, k = num_clusters)
# Scatter plot: color by species and use markers for HAC clusters
ggplot(data_penguins, aes(
x = bill_length_mm,
y = flipper_length_mm,
color = species,
shape = factor(hc_clusters)
)) +
geom_point(size = 2.5, alpha = 0.85) +
theme_bw() +
labs(
title = "Female Penguins: Species colors and HAC cluster markers",
x = "Bill Length (mm)",
y = "Flipper Length (mm)",
color = "Species",
shape = "HAC cluster"
)
The previous plot shows that some species were correctly classified into a single cluster, like Gentoo, but we also have different species classified into the same cluster (cluster 1) and two clusters containing individuals from the same species (2 and 3). This motivates the use of post-clustering inference to assess whether the retrieved clusters correspond to the known ground truth.
Covariance estimation is needed for post-clustering inference
The data model in PCIdep allows for an arbitrary
covariance matrix describing dependency between features. Since the true
covariance matrix \boldsymbol{\Sigma}
is unknown, we estimate it while maintaining statistical guarantees.
There are two options to estimate \boldsymbol{\Sigma} in PCIdep,
detailed below. These hold for all clustering algorithms implemented in
the package, including HAC (test.clusters.hc()), k-means (test.clusters.km()) and
any custom clustering algorithm (test.clusters.MC()).
1. Sample splitting
Data can be split into two sub-samples. One sub-sample is used to estimate \boldsymbol{\Sigma} and the other is used for clustering and inference. This approach is valid under the assumption of independence between observations. To implement this, set
sample_split = TRUEin the testing function. The user can also specify the number of observations to use for covariance estimation via thenYparameter, which defaults to half of the total sample size.
⚠ Important
If sample
splitting is performed internally (setting sample_split =
TRUE), clustering (and testing) will be performed on a random
subset of X. Consequently, any further analysis or
representation of the retrieved clusters should be performed on such
data subset. To obtain the data subset used for clustering and
inference, set return_X_clus = TRUE when calling the
function. This will include the data matrix used for clustering in the
returned list under the name X_clus. If preferred, the user
can also perform sample splitting prior to calling the function, and
provide the covariance estimation sample via the Y
parameter (see next section). In this case, clustering and inference
will be performed on the full data matrix X.
2. Provide an independent sample
If an independent sample is available, it can be provided directly to the testing function via the
Yparameter. Then,Ywill be used to estimate \boldsymbol{\Sigma} while the full sampleXis used for clustering and inference. When an i.i.d. sample is available, this approach is more powerful than sample splitting since it uses the full data for clustering and inference, while still providing valid covariance estimation.
💡 Dependent
observations
This functionality allows introducing an
i.i.d. sample for estimation when observations are not independent, that
is, whose covariance structure is encoded via a known matrix \mathbf{U}\neq\mathbb{I}_n. A matrix \mathbf{U} of size n_Y \times n_Y, where n_Y is the number of observations in the
i.i.d. copy, must be provided via the UY parameter.
Alternatively, its inverse (precision matrix) can be supplied via the
precUY parameter. If neither of UY or
precUY is provided, the function will assume independence
between observations in the i.i.d. copy and set UY to the
identity matrix.
Testing cluster differences with
test.clusters.hc()
Now we are interested in assessing whether the difference between the
retrieved clusters are statistically significant. To do so, we use the
test.clusters.hc() function.
📝 Note
The
test.clusters.hc() function does not require the clustering
to be performed beforehand. It performs the hierarchical clustering
automatically given a linkage method and a number of clusters, so the
previous code was only illustrative. However, if the user prefers to
perform clustering beforehand, they can provide a
stats::hclust() or fastcluster::hclust()
object via the hcl parameter, as well as the squared
distance matrix via the dismat parameter. In this case, the
function will not perform clustering and will directly test the provided
clusters. This can be useful when multiple cluster pairs from the same
partition or dendrogram are to be tested.
Here, if we assume that the dependence between flipper and bill length is the same across sexes, we can use the male penguins data to estimate \boldsymbol{\Sigma}:
Y <- penguins[penguins$sex == "male", c("bill_length_mm", "flipper_length_mm")]
Y <- na.omit(Y) # i.i.d. sample for covariance estimationThen, we run test.clusters.hc(), setting the following
parameters:
-
X: data matrix used for clustering and inference. -
Y: independent sample used for covariance estimation. -
NC: number of clusters to retrieve from the dendrogram. -
clusters: pair of cluster labels to compare (e.g.,c(1, 2)). -
linkage: HAC linkage method (here,"ward.D"). -
U: covariance matrix of observation-level dependence. Here observations are assumed independent, soUis set to the identity matrix, which is the default.
For details on the parameters and their usage, please refer to the
test.clusters.hc documentation.
# Test the HAC clusters
# Difference between clusters 4 and 2 (different species)
hc_test_42 <- PCIdep::test.clusters.hc(X=X, Y=Y, NC = 5, clusters = c(4, 2), linkage = "ward.D")
#> U is not provided: observations are considered independent with unit variance.
#> Sigma not provided: plugging an over-estimate.
hc_test_42$pvalue
#> [1] 0.0001055019
# Difference between clusters 2 and 3 (same species)
hc_test_23 <- PCIdep::test.clusters.hc(X=X, Y=Y, NC = 5, clusters = c(2, 3), linkage = "ward.D")
#> U is not provided: observations are considered independent with unit variance.
#> Sigma not provided: plugging an over-estimate.
hc_test_23$pvalue
#> [1] 0.8907002The results correspond to the expected behavior: the difference between clusters 4 and 2 is significant (as they contain different species), while the difference between clusters 2 and 3 is not significant (as they contain individuals from the same species).
k-means Clustering with test.clusters.km()
The same post-clustering inference analysis can also be performed using the k-means clustering algorithm. First, we classify the data into 3 groups with k-means. The resulting partition is shown below.
Performing k-means on the penguins data
# Perform k-means clustering
km_result <- kmeans(data_penguins[, c("bill_length_mm", "flipper_length_mm")], centers = 3)
# Extract cluster assignments
km_clusters <- km_result$cluster
# Scatter plot: color by species and use markers for k-means clusters
ggplot(data_penguins, aes(
x = bill_length_mm,
y = flipper_length_mm,
color = species,
shape = factor(km_clusters)
)) +
geom_point(size = 2.5, alpha = 0.85) +
theme_bw() +
labs(
title = "Female Penguins: Species colors and k-means cluster markers",
x = "Bill Length (mm)",
y = "Flipper Length (mm)",
color = "Species",
shape = "k-means cluster"
)
Testing k-means clusters with test.clusters.km()
The test.clusters.km() function performs post-clustering
inference for k-means clustering
results. It has similar parameters to test.clusters.hc(),
with the exception of the clustering algorithm specific parameters.
Instead of specifying a linkage method, the user can specify:
📝 Note
For
test.clusters.km(), the user can also provide an
KmeansInference::kmeans_inference() object via the
km_at_cl parameter. In this case, the function will not
perform k-means again and will directly test the provided
clustering result.
-
itermax: The maximum number of iterations allowed for the k-means algorithm to converge. The default is 10. -
tol: The convergence threshold for the k-means algorithm. The default is 1e-6.
The remaining parameters are the same as in
test.clusters.hc(). To use test.clusters.km(),
simply replace the function name and specify the desired parameters. For
example, to test the difference between clusters 1 and 2, we can
run:
# Test the k-means clusters
km_test_12 <- PCIdep::test.clusters.km(X=X, Y=Y, NC = 3, clusters = c(1, 2)) # Using male penguins data to estimate covariance
#> U is not provided: observations are considered independent with unit variance.
#> Sigma not provided: plugging an over-estimate.
km_test_12$pvalue
#> [1] 0.8053609💡 Statistical power of
test.clusters.km()
Post-clustering inference
with k-means is generally less
powerful than with HAC or other clustering algorithms. This is due
to how the k-means p-value has been defined. The k-means p-value in PCIdep is an
extension of that proposed by Chen and Witten,
originally implemented for independent observations and features in the
R package KMeansInference.
The analytical tractability of this p-value requires conditioning on more
information than in the HAC case, which comes at a price in conditional
power. We refer the reader to Chen and Witten
(2023) for a detailed discussion, or to the power analyses presented
in the PCIdep paper.
Summary
In this vignette, we demonstrated how to perform post-clustering
inference using the test.clusters.hc() and
test.clusters.km() functions from the PCIdep
package. For more details on the methods and assumptions underlying
these functions, please refer to the package documentation and the PCIdep paper.