I have been reading about this on various channels including here and Stack Exchange, but I'm still not sure how to choose the best approach for clustering gene expression data. As a Ph.D. molecular biologist (with no in-depth Math/Statistics background), I'm looking for a set of guidelines that one should follow for clustering. I will set the stage below for my problem and provide a reproducible example, but as a background study, I did the following which wasn't really helpful:
I performed extensive searches in SE/SO and talked to various bioinformaticians on this issue. I understand the general differences between various
hclust
methods anddistance
metrics. While I realize this my question sounds like a common one, I couldn't exactly find satisfying answers to understand the best approach for clustering RNAseq and microarray data. It seems like a lot of people have their favorite "way" of doing things and not much thought goes into understanding whichdistance metric
/clustering method
should be used and why.I read several posts about the choice of clustering methods including, this, this, this, this, and many others
My goal is to cluster samples based on their gene expression profiles and find real patterns within the dataset. Secondarily, I would also like to perform hierarchical clustering analysis for genes (variables in columns).
Couple words on data structure: Like many common RNAseq data, my real RNAseq dataset is composed of hundreds of observations (samples in rows) and thousands of genes (variables in columns). Distribution of gene expression values across samples may or may not be normal-like and the expression ranges can differ greatly. By using established methods (such as limma
or DEseq2
), I generated normalized counts in log2 scale (normalization based on the total number of transcript counts). I would like to perform clustering by using both the entire dataset and a subset of genes that I'm interested in.
I have a lengthy reproducible example below, please take a look to follow my questions (especially the comparisons towards the end are relevant).
My specific question is:
What are the most appropriate distance metric and hierarchical clustering methods for clustering samples (observations) and why? I performed hclust
with different methods
below on mock data (mtx
) and the results were highly variable. Please see the cluster tree comparisons and overall correlation
among clustering methods. I'm not sure which one to believe in.
Sorry for the long post, but in summary, I'm trying to understand the most appropriate approach for clustering gene expression data (applicable to both RNAseq and microarray) to see real patterns while avoiding patterns that might occur due to random chance.
Reproducible example
Simulate data
library(reprex)
library(pheatmap)
library(dendextend)
library(factoextra)
library(corrplot)
library(dplyr)
set.seed(123)
mtx_dims <- c(30, 500)
mtx <- matrix(rnorm(n = mtx_dims[1]*mtx_dims[2], mean = 0, sd = 4), nrow = mtx_dims[1])
mtx[, 1:10] <- mtx[ , 1:10] + 10 # blow some genes off-scale
mtx[, 11:20] <- mtx[, 11:20] + 20
mtx[, 21:30] <- mtx[, 11:20] + 30
mtx[, 31:40] <- mtx[, 11:20] + 40
mtx[, 41:50] <- mtx[, 11:20] + 50
rownames(mtx) <- paste0("sample_", 1:mtx_dims[1])
colnames(mtx) <- paste0("gene_", 1:mtx_dims[2])
rowannot <- data.frame(sample_group = sample(LETTERS[1:3], size = mtx_dims[1], replace = T))
rownames(rowannot) <- rownames(mtx)
unscaled_mtx <- mtx
mtx <- scale(mtx)
Exploratory heatmap/clustering
pheatmap(mtx,
scale = "none",
clustering_distance_rows = "euclidean",
clustering_distance_cols = "euclidean",
clustering_method = "complete",
main = "Euclidean distance (hclust method: complete)",
annotation_row = rowannot,
show_colnames = F)
pheatmap(mtx,
scale = "none",
clustering_distance_rows = "correlation",
clustering_distance_cols = "correlation",
clustering_method = "complete",
main = "Correlation distance (hclust method: complete)",
annotation_row = rowannot,
show_colnames = F)
pheatmap(unscaled_mtx,
scale = "none",
clustering_distance_rows = "euclidean",
clustering_distance_cols = "euclidean",
clustering_method = "complete",
main = "(Unscaled data) Euclidean distance (hclust method: complete)",
annotation_row = rowannot,
show_colnames = F)
Scaled mtx clustering
Euclidean Distanced_euc_mtx <- dist(mtx, method = "euclidean")
hclust_methods <- c("ward.D", "single", "complete", "average", "mcquitty",
"median", "centroid", "ward.D2")
mtx_dendlist_euc <- dendlist()
for(i in seq_along(hclust_methods)) {
hc_mtx <- hclust(d_euc_mtx, method = hclust_methods[i])
mtx_dendlist_euc <- dendlist(mtx_dendlist_euc, as.dendrogram(hc_mtx))
}
names(mtx_dendlist_euc) <- hclust_methods
mtx_dendlist_euc_cor <- cor.dendlist(mtx_dendlist_euc, method_coef = "spearman")
corrplot(mtx_dendlist_euc_cor, "pie", "lower")
mtx_dendlist_euc %>% dendlist(which = c(1,3)) %>% ladderize %>%
set("branches_k_color", k=3) %>%
tanglegram(faster = TRUE)
Pearson correlation distance
d_cor_mtx <- get_dist(mtx, method= "pearson", diag=T, upper=T)
mtx_dendlist_cor <- dendlist()
for(i in seq_along(hclust_methods)) {
hc_mtx <- hclust(d_cor_mtx, method = hclust_methods[i])
mtx_dendlist_cor <- dendlist(mtx_dendlist_cor, as.dendrogram(hc_mtx))
}
names(mtx_dendlist_cor) <- hclust_methods
mtx_dendlist_cor_cor <- cor.dendlist(mtx_dendlist_cor, method_coef = "spearman")
corrplot(mtx_dendlist_cor_cor, "pie", "lower")
mtx_dendlist_cor %>% dendlist(which = c(1,3)) %>% ladderize %>%
set("branches_k_color", k=3) %>%
tanglegram(faster = TRUE)
Unscaled mtx clustering
Euclidean Distanced_euc_mtx <- dist(unscaled_mtx, method = "euclidean")
hclust_methods <- c("ward.D", "single", "complete", "average", "mcquitty",
"median", "centroid", "ward.D2")
mtx_dendlist_euc <- dendlist()
for(i in seq_along(hclust_methods)) {
hc_mtx <- hclust(d_euc_mtx, method = hclust_methods[i])
mtx_dendlist_euc <- dendlist(mtx_dendlist_euc, as.dendrogram(hc_mtx))
}
names(mtx_dendlist_euc) <- hclust_methods
mtx_dendlist_euc_cor <- cor.dendlist(mtx_dendlist_euc, method_coef = "spearman")
corrplot(mtx_dendlist_euc_cor, "pie", "lower")
mtx_dendlist_euc %>% dendlist(which = c(1,3)) %>% ladderize %>%
set("branches_k_color", k=3) %>%
tanglegram(faster = TRUE)
Pearson correlation distance
d_cor_mtx <- get_dist(unscaled_mtx, method= "pearson", diag=T, upper=T)
mtx_dendlist_cor <- dendlist()
for(i in seq_along(hclust_methods)) {
hc_mtx <- hclust(d_cor_mtx, method = hclust_methods[i])
mtx_dendlist_cor <- dendlist(mtx_dendlist_cor, as.dendrogram(hc_mtx))
}
names(mtx_dendlist_cor) <- hclust_methods
mtx_dendlist_cor_cor <- cor.dendlist(mtx_dendlist_cor, method_coef = "spearman")
corrplot(mtx_dendlist_cor_cor, "pie", "lower")
mtx_dendlist_cor %>% dendlist(which = c(1,3)) %>% ladderize %>%
set("branches_k_color", k=3) %>%
tanglegram(faster = TRUE)
Cluster validation (using scaled matrix)
# The goal of this is to understand how many clusters are predicted by different
# clustering methods and index scores.
suppressPackageStartupMessages(library(NbClust))
indices <- c("kl", "ch",
# "hubert", "dindex", # take longer to compute and create graphical outputs
"ccc", "scott", "marriot", "trcovw",
"tracew", "friedman", "rubin", "cindex",
"db", "silhouette", "duda", "pseudot2",
"beale", "ratkowsky", "ball", "ptbiserial",
"gap", "frey", "mcclain", "gamma", "gplus",
"tau", "dunn","hartigan", "sdindex", "sdbw")
cl_methods_nb <- c("ward.D", "ward.D2", "single", "complete", "average", "mcquitty", "median", "centroid", "kmeans")
val_res <- list()
for(j in cl_methods_nb){
for(i in indices) {
# message(i)
tryCatch({
val_res[[paste(j,i, sep = "_")]] <- NbClust(data = mtx, diss = d_cor_mtx,
distance = NULL, method = j,
index=i, max.nc = 6)},
error=function(e){
# message(paste(j, i, "failed"))
})
}
}
#> Warning in pf(beale, pp, df2): NaNs produced
#> Warning in pf(beale, pp, df2): NaNs produced
#> [1] "Frey index : No clustering structure in this data set"
#> [1] "Frey index : No clustering structure in this data set"
val_res_nc <- data.frame()
for(i in names(val_res)){
method_name <- gsub("_.*", "", i)
index_name <- gsub(".*_", "", i)
if(!"Best.nc" %in% names(val_res[[i]])) next
df_int <- data.frame(method_name = method_name,
index_name = index_name,
best_nc = val_res[[i]][["Best.nc"]][1])
val_res_nc <- rbind(val_res_nc, df_int)
}
# Breakdown of cluster number as predicted various clustering
# methods and validation indices
summary(as.factor(val_res_nc$best_nc))
#> 1 2 3 4 5 6
#> 3 71 20 9 21 63
# Tabulate data
head(
val_res_nc %>%
group_by(method_name, index_name) %>%
summarize(best_nc), 10
)
#> # A tibble: 10 x 3
#> # Groups: method_name [1]
#> method_name index_name best_nc
#> <fct> <fct> <dbl>
#> 1 ward.D kl 4
#> 2 ward.D ch 2
#> 3 ward.D cindex 6
#> 4 ward.D db 6
#> 5 ward.D silhouette 6
#> 6 ward.D duda 5
#> 7 ward.D pseudot2 5
#> 8 ward.D beale 5
#> 9 ward.D ratkowsky 6
#> 10 ward.D ball 3