You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Removed 'WORK IN PROGRESS' note and improved clarity in explanations throughout the document. Updated function descriptions and added details on data handling.
Copy file name to clipboardExpand all lines: week3/gene_expression.qmd
+53-36Lines changed: 53 additions & 36 deletions
Original file line number
Diff line number
Diff line change
@@ -16,13 +16,7 @@ execute:
16
16
message: false
17
17
---
18
18
19
-
::: callout-caution
20
-
### WORK IN PROGRESS
21
-
22
-
This chapter is not yet finished, come back later for the final product
23
-
:::
24
-
25
-
## Introduction
19
+
# Introduction
26
20
27
21
Gene expression analyses can shed light on all kinds of biological processes and environmental responses, through identifying genes that are activated or silenced. In this session we will analyze RNA-seq gene expression data from *Arabidopsis thaliana* plants exposed to salt and heat stress and a combination of salt and heat stress.
28
22
@@ -50,13 +44,20 @@ First we need to install and load some packages:
50
44
if (!requireNamespace("BiocManager", quietly = TRUE))
51
45
install.packages("BiocManager")
52
46
53
-
BiocManager::install(c(
54
-
"ExpressionAtlas",
55
-
"SummarizedExperiment",
56
-
"edgeR",
57
-
"pheatmap",
58
-
"tidyverse",
59
-
))
47
+
if (!requireNamespace("ExpressionAtlas", quietly = TRUE))
48
+
BiocManager::install("ExpressionAtlas")
49
+
50
+
if (!requireNamespace("SummarizedExperiment", quietly = TRUE))
51
+
BiocManager::install("SummarizedExperiment")
52
+
53
+
if (!requireNamespace("edgeR", quietly = TRUE))
54
+
BiocManager::install("edgeR")
55
+
56
+
if (!requireNamespace("pheatmap", quietly = TRUE))
57
+
BiocManager::install("pheatmap")
58
+
59
+
if (!requireNamespace("tidyverse", quietly = TRUE))
@@ -74,19 +75,25 @@ we can download the gene expression data directly from the EMBL-EBI Expression A
74
75
exp <- getAtlasExperiment("E-GEOD-72806")
75
76
```
76
77
77
-
This returns a list (SimpleList) of data structures, in this case only one, let's extract that:
78
+
This returns a list (actually a `SimpleList`) of data structures, in this case only one, let's extract that:
78
79
79
80
```{r}
80
81
se <- exp[[1]]
81
82
```
82
83
83
-
To further explore the resulting data structure, use the `str()` and `show()` functions to get more information about the content.
84
+
::: {#exr-class structure}
85
+
To learn more about the resulting data in `se` use the `class()` function to see what kind we are dealing with.
84
86
85
-
To learn more about the data structure use the `class()` function to see what kind of data we are dealing with.
87
+
```{r}
88
+
89
+
```
90
+
91
+
To further explore the structure of `se`, use the `str()` function to get more information about the content.
86
92
87
93
```{r}
88
-
class(se)
94
+
89
95
```
96
+
:::
90
97
91
98
It is a `RangedSummarizedExperiment` from the package [SummarizedExperiment](https://bioconductor.org/help/course-materials/2019/BSS2019/04_Practical_CoreApproachesInBioconductor.html). Try this to get more information about these:
92
99
@@ -95,6 +102,7 @@ It is a `RangedSummarizedExperiment` from the package [SummarizedExperiment](htt
95
102
?SummarizedExperiment
96
103
```
97
104
105
+
## Storing the expression data in Hierarchical Data Format files
98
106
The Expression Atlas website can be unresponsive at times, so best to store the data structure locally. Because it is a complex data set, we cannot simply save it as a csv file. Instead, we can use the [Hierarchical Data Format (HDF)](https://en.wikipedia.org/wiki/Hierarchical_Data_Format) file format. In the `HDF5Array` library there are specific functions to store and load `SummarizedExperiment` files:
Using the `dir()` function you can check that a directory was created called **E-GEOD-72806**.
106
114
107
115
From now on we can load the data as:
108
-
109
116
```{r}
110
117
se = loadHDF5SummarizedExperiment(dir="E-GEOD-72806")
111
118
```
112
119
113
120
::: {#exr-create_load_function}
114
-
Create a function called `load_atlas_se` in your **rnaseq_functions.R** script that returns a SummarizedExperiment either from disk or directly from the EMBL-EBI Expression Atlas. The function should take the accession as input and a Boolean (TRUE/FALSE) to determine whether to load the data from disk or from the website. The default value for the Boolean should be TRUE.
121
+
Create a function called `load_atlas_se` in your **rnaseq_functions.R** script that returns a `RangedSummarizedExperiment` object either from disk or directly from the EMBL-EBI Expression Atlas. The function should take the accession as input and a Boolean (TRUE/FALSE) to determine whether to load the data from disk or from the website. The default value for the Boolean should be TRUE.
The different kinds ofdata in the **se** object are stored in different slots. To see which these are, you can use the **slotNames()** method on the **se** object. You can access these slots use the **\@** character. For instance, to see how the raw sequencing data were filtered you can look in the **metadata** slot, which is a list, and then the **filtering** element like this:
131
+
The different kinds of data in the **se** object are stored in different slots. To see which these are, you can use the **slotNames()** method on the **se** object. You can access these slots use the **\@** character. For instance, to see how the raw sequencing data were filtered you can look in the **metadata** slot, which returns a list, and then the **filtering** element like this:
125
132
126
133
```{r}
127
134
se@metadata$filtering
@@ -141,14 +148,14 @@ To find the proper accessor function, the help text for the class is usually the
141
148
142
149
## Exploring the count data
143
150
144
-
To get the actual expression data in the RangedSummarizedExperiment, we can use the assay() method:
151
+
To get the actual expression data in the `RangedSummarizedExperiment`, we can use the `assay()` function:
145
152
146
153
```{r}
147
154
counts <- assay(se) # genes x samples
148
155
counts
149
156
```
150
157
151
-
This returns a matrix with rows representing genes and columns representing samples. The class of counts is `DelayedMatrix`, which behaves like a regular numeric matrix, but computations are performed lazily. This means that transformations (such as log-transformation or scaling) are not executed immediately; instead, they are stored and applied only when the data are accessed. This allows efficient handling of large datasets that may reside on disk rather than in computer memory. To view the counts matrix in Rstudio, try the following commands:
158
+
This returns a matrix with rows representing genes and columns representing samples. The class of counts is `DelayedMatrix`, which behaves like a regular numeric matrix, but computations are performed lazily. This means for instance that operations like log-transformation or scaling are not executed immediately; instead, they are applied only when the data are accessed. This allows efficient handling of large datasets that may reside on disk rather than in computer memory. To view the counts matrix in RStudio, try the following commands:
152
159
153
160
```{r}
154
161
View(counts)
@@ -161,21 +168,26 @@ Let's look at the dimensions of counts:
161
168
dim(counts)
162
169
```
163
170
164
-
There are more than 32000 genes and 12 samples with various treatments. The columns are the samples, to get information about the samples we can use the colData method:
171
+
There are more than 32000 genes and 12 samples with various treatments. The columns are the samples, to get information about the samples we can use the `colData()` function:
165
172
166
173
```{r}
167
174
coldata <- colData(se) # sample level information
168
175
coldata
169
176
```
170
177
171
-
The samples have non-descriptive names like "SRR2302908", but the **environment_stress** column tells us which stress the sample was subjected to. The other columns do not help to differentiate between the samples. We can clean up the coldata DataFrame to only keep the enviromental_stress column. You probably did not notice it, but coldata is a Bioconductor DataFrame object and not a base R data.frame. We can fix that in the same command:
178
+
The samples have non-descriptive names like "SRR2302908", but the **environment_stress** column tells us which stress the sample was subjected to. The other columns do not help to differentiate between the samples. We can clean up the coldata DataFrame to only keep the enviromental_stress column. You probably did not notice it, but coldata is a Bioconductor `DataFrame` object and not a base R `data.frame`. We can fix that in the same command:
172
179
173
180
```{r}
174
-
coldata <- as.data.frame(coldata[, "environmental_stress", drop = FALSE])
175
-
181
+
coldata <- as.data.frame(coldata[, "environmental_stress", drop = FALSE])
176
182
```
177
183
178
-
Now to get some idea of the gene expression values (the counts) for the RNA-seq experiment, we can first sum the counts per sample (so per column). The `apply()` function works for this, where we apply the `sum()` function to the columns. `DelayedArray` objects also have a specific function to do the same called: `colSums()`
184
+
::: {.callout-tip appearance="simple"}
185
+
In case you wonder about the `drop = FALSE` in `coldata[, "environmental_stress", drop = FALSE]`:
186
+
By selecting only the "environmental_stress" column, we get a `data.frame` with only one column. Default R behavior is to 'simplify' this to a vector `c()`, which means we lose the row names which represent the sample labels.
187
+
`drop = FALSE` prevents this default behavior, so the result is still a `data.frame`.
188
+
:::
189
+
190
+
Now to get some idea of the gene expression values (the counts) for the RNA-seq experiment, we can first sum the counts per sample (so per column). We can use the `apply()` function for this, by allowing us to apply the `sum()` function to each column. `DelayedArray` objects also have a specific function called `colSums()` to do the same.
179
191
180
192
```{r}
181
193
apply(counts,2,sum) # or colSums(counts)
@@ -189,9 +201,9 @@ The `apply(X, MARGIN, FUN)` function performs the function given by FUN on all r
189
201
190
202
In the Suzuki et al. paper we can read that they generated on average 14 million sequencing reads per sample, the sums you see here are after filtering to remove bad quality reads. Do the sums match the number from the paper?
191
203
192
-
One sequencing read corresponds to one mRNA (fragment), so they can be used to determine the abundance of the mRNAs and quantify the expression of the different Arabidopsis genes.
204
+
One sequencing read corresponds to one mRNA (fragment), so they can be used to determine the abundance of the different mRNAs and quantify the expression of each Arabidopsis gene.
193
205
194
-
To get an idea of the overall properties of the counts, we can plot their distribution. For this we look at the counts of all genes and samples together, so let's flatten the counts into one vector:
206
+
To get an idea of the overall properties of the counts, we can plot their distribution. For this we look at the counts of all genes and samples together, so let's flatten the `counts` matrix into one vector:
This is not very informative, is it? A common trick to bring values in the same range is to performa a log transformation of the counts. We can plot the log10 transformed counts with a simple addition:
225
+
This is not very informative, is it? That is because there is very large difference in expression between genes. A common trick to bring values in the same range is to performa a log transformation of the counts. We can plot the log10 transformed counts with a simple addition:
214
226
215
227
```{r}
216
228
ggplot(df_expr, aes(x = expr)) +
@@ -348,7 +360,7 @@ The total number of counts (reads) per sample is more a technical than a biologi
348
360
If we look at the correlation between the counts for the top 10 most highly expressed genes with the library size, some are very strongly correlated.
349
361
350
362
```{r}
351
-
apply(mat_top, 1, cor, y = libsize, method = "pearson")
363
+
apply(mat_top, 1, cor, y = libsize, method = "pearson")
352
364
```
353
365
354
366
::: {.callout-tip appearance="simple"}
@@ -369,7 +381,7 @@ In practice, this does not remove all bias, so more sophisticated normalization
369
381
370
382
A normalization method that addresses this is called TMM, or "Trimmed Mean of M-values". This specifically ignores genes that change a lot between different samples in calculating a scaling factor. It is good to realize that TMM only works well if the majority of genes do not change much in expression.
371
383
372
-
We can perform TMM normalization step-by-step, but we can also use a library that was designed for RNA-seq analyses called **edgeR**.
384
+
We can perform TMM normalization step-by-step, but we can also use an R library that was designed for RNA-seq analyses called **edgeR**.
373
385
374
386
```{r}
375
387
library(edgeR)
@@ -389,12 +401,17 @@ barplot(
389
401
390
402
Did that help?
391
403
392
-
We will come back to edgeR on Wednesday when we will use it to find which genes are significantly up- or down-regulated in response to the stress treatments. These genes are called Differentially Expressed Genes or DEG, and the analysis is called Differential Gene Expression or DGE.
404
+
We will come back to `edgeR` on Wednesday when we will use it to find which genes are significantly up- or down-regulated in response to the stress treatments. These genes are called Differentially Expressed Genes or **DEG**, and the analysis is called Differential Gene Expression or **DGE**.
393
405
394
406
::: {#exr-create_function_get_normalized_counts}
395
-
Create a function called **get_normalized_counts** in your **rnaseq_functions.R** script that does the following: -extract the counts from a SummarizedExperiment -filters out lowly expressed genes -normalizes the counts using edgeR's `cpm()` function -optionally log transforms the counts
407
+
Create a function called **get_normalized_counts** in your **rnaseq_functions.R** script that does the following:
408
+
409
+
- extract the counts from a `RangedSummarizedExperiment`
410
+
- removes lowly expressed genes
411
+
- normalizes the counts using edgeR's `cpm()` function
412
+
- optionally log transforms the counts
396
413
397
-
It should take as input a **SummarizedExperiment** object, a logical value **log** indicating whether the counts should be log2 transformed (default TRUE) and two numeric values that are used for filtering: **min_count** and **min_samples**. These should have default values 10 and 3. It should return the cpm values:
414
+
It should take as input a **RangedSummarizedExperiment** object, a logical value **log** indicating whether the counts should be log2 transformed (default TRUE) and two numeric values that are used for filtering: **min_count** and **min_samples**. These should have default values 10 and 3. It should return the cpm values:
0 commit comments