Skip to content

Commit fce6b50

Browse files
authored
Refactor clustering documentation and code examples
Refactor clustering section, update headings, and improve code readability.
1 parent d664347 commit fce6b50

1 file changed

Lines changed: 35 additions & 43 deletions

File tree

week3/clustering.qmd

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ execute:
1616
message: false
1717
---
1818

19-
## Clustering
19+
## Introduction
2020
In this session we continue with the gene expression dataset from <https://www.ebi.ac.uk/gxa/experiments/E-GEOD-72806/>
2121

2222
First load the proper libraries and filter/transform the data using the functions that you created yesterday. To make the functions available in the current R session, you can use the `source()` function to run the R script.
@@ -33,8 +33,6 @@ logcounts <- get_normalized_counts(se)
3333
coldata <- as.data.frame(colData(se)[, c("environmental_stress"), drop = FALSE])
3434
```
3535

36-
## Introduction
37-
3836
We have already seen that the 12 samples represent 4 treatments, with 3 replicates per treatment. We expect the replicates to have similar gene expression values. To check this, we can cluster samples based on these expression values. You were already introduced to clustering on day 2 of the course. Then you used hierarchical clustering, today we will also look at another common clustering method called *k*-means.
3937

4038
In clustering we group samples together that are similar, but how do we determine how similar samples are? We need a similarity measure. This is usually derived from a distance measure, which is inversely related to the similarity.
@@ -125,41 +123,11 @@ We can try the same for the two most variable genes:
125123
genes <- names(sort(gene_var, decreasing = TRUE))[1:2]
126124
```
127125

128-
::: {#exr-plot_2genes}
126+
::: {#exr-two_gene_scatter_plot}
129127
Now we could copy the plotting code from above, but copying a block of code is generally not a good idea, it is better to turn that into a function that you can call. So please go ahead and create a function that takes the list of genes as input, as well as the data that is needed to make the plot (logcounts and coldata). Add this function to your **rnaseq_functions.R** script
130128

131129
```{r}
132-
133-
# ---- extract expression and transpose ----
134-
expr_df <- as.data.frame(t(logcounts[genes, , drop = FALSE]))
135-
expr_df$sample <- rownames(expr_df)
136-
137-
# ---- merge expression with metadata ----
138-
df <- merge(
139-
expr_df,
140-
coldata,
141-
by = "sample",
142-
all.x = TRUE
143-
)
144-
145-
p2 <- ggplot(
146-
df,
147-
aes(
148-
x = .data[[genes[1]]],
149-
y = .data[[genes[2]]],
150-
color = environmental_stress
151-
)
152-
) +
153-
geom_point(size = 4) +
154-
geom_text(aes(label = sample), vjust = -0.8, size = 3) +
155-
labs(
156-
title = "Samples projected onto the two most variable genes",
157-
x = paste(genes[1], "(log(cpm) expression)"),
158-
y = paste(genes[2], "(log(cpm) expression)")
159-
) +
160-
theme_bw()
161-
162-
p2
130+
two_gene_scatter_plot(genes, logcounts, coldata)
163131
```
164132
:::
165133

@@ -176,7 +144,8 @@ The gene IDs are not really informative to learn more about the kinds of genes t
176144
We can use the 2-gene representation of the samples to find clusters of similar samples (like you already did on day 2 of the course). Hierarchical clustering starts with each sample in its own cluster and then step-wise merges the two most similar clusters based on a distance measure (Euclidean, correlation-based, etc.) until all samples are in one cluster. This is typically depicted as a tree/dendrogram. This merging can be stopped if a certain number of clusters is reached. How the distance is calculated depends on the distance measure, but also on the positions in the clusters the distance is calculated between. For this several so-called **agglomeration** or **linkage methods** are used that give different results. With **average linkage**, the distance between two cluster is calculated as the average of all distances between the members of both cluster. In **complete linkage**, the largest distance between any two members of two cluster is used, while **single linkage** uses the smallest distance between any two members of both clusters. You can imagine that both the distance measure as well as the linkage methods affect the clustering.
177145

178146
```{r}
179-
d_samples <- dist(df[,genes], method = "euclidean") # calculate Euclidean distance between all samples
147+
X <- df[, genes] # select only the gene expression columns for clustering
148+
d_samples <- dist(X, method = "euclidean") # calculate Euclidean distance between all samples
180149
hc_samples <- hclust(d_samples, method = "complete") # cluster using complete-linkage
181150
plot(hc_samples, hang=-1) # this actually calls plot.hclust, hang=-1 makes sure the tree starts (or ends, depending on your perspective) at 0
182151
```
@@ -308,9 +277,10 @@ df <- merge(
308277
)
309278
310279
k <- 4
280+
X <- df[, genes] # select only the gene expression columns for clustering
311281
312282
km <- kmeans(
313-
df[, genes],
283+
X,
314284
centers = k,
315285
)
316286
@@ -333,7 +303,7 @@ p2 <- ggplot(
333303
geom_point(size = 4) +
334304
geom_text(aes(label = sample), vjust = -0.8, size = 3) +
335305
labs(
336-
title = "k-means clustering in 2-gene expression space",
306+
title = "K-means clustering in 2-gene expression space",
337307
subtitle = paste("k =", k),
338308
x = paste(genes[1], "(log(cpm) expression)"),
339309
y = paste(genes[2], "(log(cpm) expression)")
@@ -349,7 +319,6 @@ We can print the cluster assignment in a table with the cluster numbers as colum
349319

350320
```{r}
351321
print(table(km$cluster))
352-
353322
```
354323

355324
Given the randomness of the initialization of the centroid locations, this might not be the best we can get.
@@ -358,12 +327,14 @@ Given the randomness of the initialization of the centroid locations, this might
358327
Write a for-loop to repeat the *K*-means clustering 10 times and report the number of samples per cluster for each repetition
359328

360329
```{r}
330+
X <- df[, genes]
331+
361332
362333
km <- kmeans(
363-
df[, genes],
334+
X,
364335
centers = k,
365336
)
366-
337+
print(table(km$cluster))
367338
368339
```
369340

@@ -378,6 +349,27 @@ Ideally we would want to run *K*-means many times and keep only the best result.
378349
```
379350
:::
380351

352+
Choosing *K* is important for *K*-means clustering. We used the number of treatments, but we can also let the data inform us of the optimal *K*. For this we use the total sum of the the squared distance of every sample to the center of its cluster, the total Within Sum of Squares or total WSS. Total WSS will be largest when *K* is 1 and smallest (zero) if we set *K* to the number of samples. If we plot a trend line of the total WSS (an elbow plot), we can look for a bend that indicates a point after which adding more clusters does not improve the total WSS as much anymore.
353+
354+
355+
356+
``` {r}
357+
set.seed(42)
358+
X <- t(logcounts) # now we take all genes into account. With the transpose t() we would cluster the genes instead of the samples.
359+
wss <- numeric(10) # vector to keep track of the total Within Sum of Squares values per K
360+
for (k in 1:10) {
361+
km <- kmeans(X, centers = k, nstart = 25)
362+
wss[k] <- km$tot.withinss # the kmeans object already has the total WSS
363+
}
364+
365+
plot(1:10, wss,
366+
type = "b", # a line plot with points
367+
pch = 19,
368+
xlab = "Number of clusters (k)",
369+
ylab = "Total within-cluster sum of squares",
370+
main = "Elbow Plot")
371+
```
372+
381373
## Clustering genes
382374

383375
Next to clustering the samples, we can also cluster the genes based on their expression patterns. If genes have similar expression, they may have related functions. We will use a different data set for this, a seed germination time series from *Arabidopsis thaliana* from this paper: ["Extensive transcriptomic and epigenomic remodelling occurs during Arabidopsis thaliana germination."](https://europepmc.org/article/MED/28911330)
@@ -442,7 +434,7 @@ for (cluster in 1:3) {
442434
main = paste("Cluster",cluster) )
443435
}
444436
```
445-
As a bonus (so not part of the examn) we can investigate which biological processes these genes are involved in to dive into the biology. For this a common approach is [Gene Ontology enrichment analysis](https://geneontology.org/docs/go-enrichment-analysis/), which basically comes down to finding biological progresses that occur more frequently in the set of genes in a cluster than would expected by chance. The code below does that for all three clusters. The dotplots show the top 20 most enriched biological processes per cluster.
437+
As a bonus (so not part of the exam) we can investigate which biological processes these genes are involved in to dive into the biology. For this a common approach is [Gene Ontology enrichment analysis](https://geneontology.org/docs/go-enrichment-analysis/), which basically comes down to finding biological progresses that occur more frequently in the set of genes in a cluster than would expected by chance. The code below does that for all three clusters. The dotplots show the top 20 most enriched biological processes per cluster.
446438

447439
``` {r}
448440
if (!requireNamespace("org.At.tair.db", quietly = TRUE))
@@ -474,4 +466,4 @@ for (cluster in 1:3) {
474466
print(dotplot(ego, showCategory = 20, title = paste("Cluster",cluster)))
475467
}
476468
```
477-
One cluster is clearly composed of seed expressed genes, another more focuessed on cell wall organisation, and one clearly enriched in photosynthesis related genes.
469+
One cluster is clearly composed of seed expressed genes, another more focused on cell wall organisation, and one clearly enriched in photosynthesis related genes.

0 commit comments

Comments
 (0)