R version: R version 4.2.0 RC (2022-04-19 r82224)

Bioconductor version: 3.15

Package: 1.20.0

1 Introduction

RNA sequencing (RNA-seq) is now the most widely used high-throughput assay for measuring gene expression. In a typical RNA-seq experiment, several million reads are sequenced per sample. The reads are often aligned to the reference genome using a splice-aware aligner to identify where reads originated. Resulting alignment files are then used to compute count matrices for several analyses such as identifying differentially expressed genes. The Bioconductor project (1) has many contributed packages that specialize in analyzing this type of data and previous workflows have explained how to use them (2–4). Initial steps are typically focused on generating the count matrices. Some pre-computed matrices have been made available via the ReCount project (5) or Bioconductor Experiment data packages such as the airway dataset (6). The pre-computed count matrices in ReCount have been useful to RNA-seq methods developers and to researchers seeking to avoid the computationally intensive process of creating these matrices. In the years since ReCount was published, hundreds of new RNA-seq projects have been carried out, and researchers have shared the data publicly.

We recently uniformly processed over 70,000 publicly available human RNA-seq samples, and made the data available via the recount2 resource (7) at jhubiostatistics.shinyapps.io/recount/. Samples in recount2 are grouped by project (over 2,000) originating from the Sequence Read Archive, the Genotype-Tissue Expression study (GTEx) and the Cancer Genome Atlas (TCGA). The processed data can be accessed via the recount Bioconductor package available at bioconductor.org/packages/recount. Together, recount2 and the recount Bioconductor package should be considered a successor to ReCount.

Due to space constraints, the recount2 publication (7) did not cover how to use the recount package and other useful information for carrying out analyses with recount2 data. We describe how the count matrices in recount2 were generated. We also review the R code necessary for using the recount2 data, whose details are important because some of this code involves multiple Bioconductor packages and changing default options. We further show: (a) how to augment metadata that comes with datasets with metadata learned from natural language processing of associated papers as well as expression data (b) how to perform differential expression analyses, and (c) how to visualize the base-pair data available from recount2.

2 Analysis of RNA-seq data available at recount2

2.1 recount2 overview

The recount2 resource provides expression data summarized at different feature levels to enable novel cross-study analyses. Generally when investigators use the term expression, they think about gene expression. But more information can be extracted from RNA-seq data. Once RNA-seq reads have been aligned to the reference genome it is possible to determine the number of aligned reads overlapping each base-pair resulting in the genome base-pair coverage curve as shown in Figure 1. In the example shown in Figure 1, most of the reads overlap known exons from a gene. Those reads can be used to compute a count matrix at the exon or gene feature levels. Some reads span exon-exon junctions (jx) and while most match the annotation, some do not (jx 3 and 4). An exon-exon junction count matrix can be used to identify differentially expressed junctions, which can show which isoforms are differentially expressed given sufficient coverage. For example, junctions 2 and 5 are unique to isoform 2, while junction 6 is unique to isoform 1. The genome base-pair coverage data can be used with derfinder (8) to identify expressed regions; some of them could be unannotated exons, which together with the exon-exon junction data could help establish new isoforms.

Overview of the data available in recount2. Reads (pink boxes) aligned to the reference genome can be used to compute a base-pair coverage curve and identify exon-exon junctions (split reads). Gene and exon count matrices are generated using annotation information providing the gene (green boxes) and exon (blue boxes) coordinates together with the base-level coverage curve. The reads spanning exon-exon junctions (jx) are used to compute a third count matrix that might include unannotated junctions (jx 3 and 4). Without using annotation information, expressed regions (orange box) can be determined from the base-level coverage curve to then construct data-driven count matrices.

Figure 1: Overview of the data available in recount2
Reads (pink boxes) aligned to the reference genome can be used to compute a base-pair coverage curve and identify exon-exon junctions (split reads). Gene and exon count matrices are generated using annotation information providing the gene (green boxes) and exon (blue boxes) coordinates together with the base-level coverage curve. The reads spanning exon-exon junctions (jx) are used to compute a third count matrix that might include unannotated junctions (jx 3 and 4). Without using annotation information, expressed regions (orange box) can be determined from the base-level coverage curve to then construct data-driven count matrices.

recount2 provides gene, exon, and exon-exon junction count matrices both in text format and RangedSummarizedExperiment objects (rse) (9) as shown in Figure 2. These rse objects provide information about the expression features (for example gene IDs) and the samples. In this workflow we will explain how to add metadata to the rse objects in recount2 in order to ask biological questions. recount2 also provides coverage data in the form of bigWig files. All four features can be accessed with the recount Bioconductor package (7). recount also allows sending queries to snaptron (10) to search for specific exon-exon junctions.

recount2 provides coverage count matrices in RangedSummarizedExperiment (rse) objects. Once the rse object has been downloaded and loaded into R, the feature information is accessed with rowRanges(rse) (blue box), the counts with assays(rse)\$counts (pink box) and the sample metadata with colData(rse) (green box). The sample metadata can be expanded using add\_predictions(rse) (orange box) or with custom code (brown box) matching by a unique sample identifier such as the SRA Run ID. The rse object is inside the purple box and matching data is highlighted in each box.

Figure 2: recount2 provides coverage count matrices in RangedSummarizedExperiment (rse) objects
Once the rse object has been downloaded and loaded into R, the feature information is accessed with rowRanges(rse) (blue box), the counts with assays(rse)$counts (pink box) and the sample metadata with colData(rse) (green box). The sample metadata can be expanded using add_predictions(rse) (orange box) or with custom code (brown box) matching by a unique sample identifier such as the SRA Run ID. The rse object is inside the purple box and matching data is highlighted in each box.

2.2 Packages used in the workflow

In this workflow we will use several Bioconductor packages. To reproduce the entirety of this workflow, install the packages using the following code after installing R 3.4.x from CRAN in order to use Bioconductor version 3.5 or newer.

## Install packages from Bioconductor
if (!requireNamespace("BiocManager", quietly = TRUE)) {
      install.packages("BiocManager")
  }
BiocManager::install(c(
    "recount", "GenomicRanges", "limma", "edgeR", "DESeq2",
    "regionReport", "clusterProfiler", "org.Hs.eg.db", "gplots",
    "derfinder", "GenomicState", "bumphunter", "derfinderPlot", "sessioninfo"
))

Once they are installed, load all the packages with the following code.

library("recount")
library("GenomicRanges")
library("limma")
library("edgeR")
library("DESeq2")
library("regionReport")
library("clusterProfiler")
library("org.Hs.eg.db")
library("gplots")
library("derfinder")
library("GenomicState")
library("bumphunter")
library("derfinderPlot")
library("sessioninfo")

2.3 Coverage counts provided by recount2

The most accessible features are the gene, exon and exon-exon junction count matrices. This section explains them in greater detail. Figure 3 shows 16 RNA-seq reads, each 3 base-pairs long, and a reference genome.

RNA-seq starting data. 16 RNA-seq un-aligned RNA-seq reads 3 base-pairs long are shown (pink boxes) alongside a reference genome that is 16 base-pairs long (white box).

Figure 3: RNA-seq starting data
16 RNA-seq un-aligned RNA-seq reads 3 base-pairs long are shown (pink boxes) alongside a reference genome that is 16 base-pairs long (white box).

Reads in the recount2 resource were aligned with the splice-aware Rail-RNA aligner (11). Figure 4 shows the reads aligned to the reference genome. Some of the reads are split as they span an exon-exon junction. Two of the reads were soft clipped meaning that just a portion of the reads aligned (top left in purple).

Aligned RNA-seq reads. Spice-aware RNA-seq aligners such as Rail-RNA are able to find the coordinates to which the reads map, even if they span exon-exon junctions (connected boxes). Rail-RNA soft clips some reads (purple boxes with rough edges) such that a portion of these reads align to the reference genome.

Figure 4: Aligned RNA-seq reads
Spice-aware RNA-seq aligners such as Rail-RNA are able to find the coordinates to which the reads map, even if they span exon-exon junctions (connected boxes). Rail-RNA soft clips some reads (purple boxes with rough edges) such that a portion of these reads align to the reference genome.

In order to compute the gene and exon count matrices we first have to process the annotation, which for recount2 is Gencode v25 (CHR regions) with hg38 coordinates. Although recount can generate count matrices for other annotations using hg38 coordinates. Figure 5 shows two isoforms for a gene composed of 3 different exons.

Gene annotation. A single gene with two isoforms composed by three distinct exons (blue boxes) is illustrated. Exons 1 and 3 share the first five base-pairs while exon 2 is common to both isoforms.

Figure 5: Gene annotation
A single gene with two isoforms composed by three distinct exons (blue boxes) is illustrated. Exons 1 and 3 share the first five base-pairs while exon 2 is common to both isoforms.

The coverage curve is at base-pair resolution, so if we are interested in gene counts we have to be careful not to double count base-pairs 1 through 5 that are shared by exons 1 and 3 (Figure 5). Using the function disjoin() from GenomicRanges (12) we identified the distinct exonic sequences (disjoint exons). The following code defines the exon coordinates that match Figure 5 and the resulting disjoint exons for our example gene. The resulting disjoint exons are shown in Figure 6.

library("GenomicRanges")
exons <- GRanges("seq", IRanges(start = c(1, 1, 13), end = c(5, 8, 15)))
exons
## GRanges object with 3 ranges and 0 metadata columns:
##       seqnames    ranges strand
##          <Rle> <IRanges>  <Rle>
##   [1]      seq       1-5      *
##   [2]      seq       1-8      *
##   [3]      seq     13-15      *
##   -------
##   seqinfo: 1 sequence from an unspecified genome; no seqlengths
disjoin(exons)
## GRanges object with 3 ranges and 0 metadata columns:
##       seqnames    ranges strand
##          <Rle> <IRanges>  <Rle>
##   [1]      seq       1-5      *
##   [2]      seq       6-8      *
##   [3]      seq     13-15      *
##   -------
##   seqinfo: 1 sequence from an unspecified genome; no seqlengths
Disjoint exons. Windows of distinct exonic sequence for the example gene. Disjoint exons 1 and 2 form exon 1.

Figure 6: Disjoint exons
Windows of distinct exonic sequence for the example gene. Disjoint exons 1 and 2 form exon 1.

Now that we have disjoint exons, we can compute the base-pair coverage for each of them as shown in Figure 7. That is, for each base-pair that corresponds to exonic sequence, we compute the number of reads overlapping that given base-pair. For example, the first base-pair is covered by 3 different reads and it does not matter whether the reads themselves were soft clipped. Not all reads or bases of a read contribute information to this step, as some do not overlap known exonic sequence (light pink in Figure 7).

Base-pair coverage counting for exonic base-pairs. At each exonic base-pair we compute the number of reads overlapping that given base-pair. The first base (orange arrow) has 3 reads overlapping that base-pair. Base-pair 11 has a coverage of 3 but does not overlap known exonic sequence, so that information is not used for the gene and exon count matrices (grey arrow). If a read partially overlaps exonic sequence, only the portion that overlaps is used in the computation (see right most read).

Figure 7: Base-pair coverage counting for exonic base-pairs
At each exonic base-pair we compute the number of reads overlapping that given base-pair. The first base (orange arrow) has 3 reads overlapping that base-pair. Base-pair 11 has a coverage of 3 but does not overlap known exonic sequence, so that information is not used for the gene and exon count matrices (grey arrow). If a read partially overlaps exonic sequence, only the portion that overlaps is used in the computation (see right most read).

With base-pair coverage for the exonic sequences computed, the coverage count for each distinct exon is simply the sum of the base-pair coverage for each base in a given distinct exon. For example, the coverage count for disjoint exon 2 is \(2 + 2 + 3 = 7\) as shown in Figure 8. The gene coverage count is then \(\sum_i^n \texttt{coverage}_i\) where \(n\) is the number of exonic base-pairs for the gene and is equal to the sum of the coverage counts for its disjoint exons as shown in Figure 8.

Exon and gene coverage counts. The coverage counts for each disjoint exon are the sum of the base-pair coverage. The gene coverage count is the sum of the disjoint exons coverage counts.

Figure 8: Exon and gene coverage counts
The coverage counts for each disjoint exon are the sum of the base-pair coverage. The gene coverage count is the sum of the disjoint exons coverage counts.

For the exons, recount2 provides the disjoint exons coverage count matrix. It is possible to reconstruct the exon coverage count matrix by summing the coverage count for the disjoint exons that compose each exon. For example, the coverage count for exon 1 would be the sum of the coverage counts for disjoint exons 1 and 2, that is \(19 + 7 = 26\). Some methods might assume that double counting of the shared base-pairs was performed while others assume or recommend the opposite.

2.3.1 Scaling coverage counts

The coverage counts described previously are the ones actually included in the rse objects in recount2 instead of typical read count matrices. This is an important difference to keep in mind as most methods were developed for read count matrices. Part of the sample metadata available from recount2 includes the read length and number of mapped reads. Given a target library size (40 million reads by default), the coverage counts in recount2 can be scaled to read counts for a given library size as shown in Equation (1). Note that the resulting scaled read counts are not necessarily integers so it might be necessary to round them if a differential expression (DE) method assumes integer data.

\[\begin{equation} \frac{\sum_i^n \text{coverage}_i }{\text{Read Length}} * \frac{\text{target}}{\text{mapped}} = \text{scaled read counts} \tag{1} \end{equation}\]

From Figure 4 we know that Rail-RNA soft clipped some reads, so a more precise measure than the denominator of Equation (1) is the area under coverage (AUC) which is the sum of the coverage for all base-pairs of the genome, regardless of the annotation as shown in Figure 9. Without soft clipping reads, the AUC would be equal to the number of reads mapped multiplied by the read length. So for our example gene, the scaled counts for a library size of 20 reads would be \(\frac{36}{45} * 20 = 16\) and in general calculated with Equation (2). The following code shows how to compute the AUC given a set of aligned reads and reproduce a portion of Figure 9.

\[\begin{equation} \frac{\sum_i^n \text{coverage}_i }{\text{AUC}} * \text{target} = \text{scaled read counts} \tag{2} \end{equation}\]

## Take the example and translate it to R code
library("GenomicRanges")
reads <- GRanges("seq", IRanges(
    start = rep(
        c(1, 2, 3, 4, 5, 7, 8, 9, 10, 13, 14),
        c(3, 1, 2, 1, 2, 1, 2, 1, 2, 4, 1)
    ), width = rep(
        c(1, 3, 2, 3, 1, 2, 1, 3, 2, 3, 2, 1, 3),
        c(1, 4, 1, 2, 1, 1, 2, 2, 1, 1, 2, 1, 1)
    )
))
## Get the base-level genome coverage curve
cov <- as.integer(coverage(reads)$seq)

## AUC
sum(cov)
## [1] 45
## Code for reproducing the bottom portion of Figure 8.
pdf("base_pair_coverage.pdf", width = 20)
par(mar = c(5, 6, 4, 2) + 0.1)
plot(cov,
    type = "o", col = "violetred1", lwd = 10, ylim = c(0, 5),
    xlab = "Genome", ylab = "Coverage", cex.axis = 2, cex.lab = 3,
    bty = "n"
)
polygon(c(1, seq_len(length(cov)), length(cov)), c(0, cov, 0),
    border = NA, density = -1, col = "light blue"
)
points(seq_len(length(cov)), cov,
    col = "violetred1", type = "o",
    lwd = 10
)
dev.off()
Area under coverage (AUC). The area under coverage is the sum of the base-pair coverage for all positions in the genome regardless of the annotation. It is the area under the base-level coverage curve shown as the light blue area under the pink curve.

Figure 9: Area under coverage (AUC)
The area under coverage is the sum of the base-pair coverage for all positions in the genome regardless of the annotation. It is the area under the base-level coverage curve shown as the light blue area under the pink curve.

The recount function scale_counts() computes the scaled read counts for a target library size of 40 million reads and we highly recommend using it before doing other analyses. The following code shows how to use scale_counts() and that the resulting read counts per sample can be lower than the target size (40 million). This happens when not all mapped reads overlap known exonic base-pairs of the genome. In our example, the gene has a scaled count of 16 reads for a library size of 20 reads, meaning that 4 reads did not overlap exonic sequences.

## Check that the number of reads is less than or equal to 40 million
## after scaling.
library("recount")
rse_scaled <- scale_counts(rse_gene_SRP009615, round = FALSE)
summary(colSums(assays(rse_scaled)$counts)) / 1e6
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   22.62   29.97   34.00   31.96   34.86   36.78

2.3.2 Enriching the annotation

Data in recount2 can be used for annotation-agnostic analyses and enriching the known annotation. Just like exon and gene coverage count matrices, recount2 provides exon-exon junction count matrices. These matrices can be used to identify new isoforms (Figure 10) or identify differentially expressed isoforms. For example, exon-exon junctions 2, 5 and 6 in Figure 1 are only present in one annotated isoform. Snaptron (10) allows programatic and high-level queries of the exon-exon junction information and its graphical user interface is specially useful for visualizing this data. Inside R, the recount function snaptron_query() can be used for searching specific exon-exon junctions in recount2.

Exon-exon junctions go beyond the annotation. Reads spanning exon-exon junctions are highlighted and compared against the annotation. Three of them match the annotated junctions, but one (blue and orange read) spans an unannotated exon-exon junction with the left end matching the annotation and the right end hinting at a possible new isoform for this gene (blue and orange isoform).

Figure 10: Exon-exon junctions go beyond the annotation
Reads spanning exon-exon junctions are highlighted and compared against the annotation. Three of them match the annotated junctions, but one (blue and orange read) spans an unannotated exon-exon junction with the left end matching the annotation and the right end hinting at a possible new isoform for this gene (blue and orange isoform).

The base-pair coverage data from recount2 can be used together with derfinder (8) to identify expressed regions of the genome, providing another annotation-agnostic analysis of the expression data. Using the function expressed_regions() we can identify regions of expression based on a given data set in recount2. These regions might overlap known exons but can also provide information about intron retention events (Figure 11), improve detection of exon boundaries (Figure 12), and help identify new exons (Fig 1) or expressed sequences in intergenic regions. Using coverage_matrix() we can compute a coverage matrix based on the expressed regions or another set of genomic intervals. The resulting matrix can then be used for a DE analysis, just like the exon, gene and exon-exon junction matrices.

Intron retention events. Some reads might align with known intronic segments of the genome and provide information for exploring intron retention events (pink read). Some might support an intron retention event or a new isoform when coupled with exon-exon junction data (orange read).

Figure 11: Intron retention events
Some reads might align with known intronic segments of the genome and provide information for exploring intron retention events (pink read). Some might support an intron retention event or a new isoform when coupled with exon-exon junction data (orange read).

Exon boundaries. Reads that go beyond the known exon boundaries can inform us of whether the annotated boundaries are correct or if there was a run-off transcription event.

Figure 12: Exon boundaries
Reads that go beyond the known exon boundaries can inform us of whether the annotated boundaries are correct or if there was a run-off transcription event.

2.4 Gene-level analysis

Having reviewed how the coverage counts in recount2 were produced, we can now do a DE analysis. We will use data from 72 individuals spanning the human lifespan, split into 6 age groups with SRA accession SRP045638 (13). The function download_study() requires a SRA accession which can be found using abstract_search(). download_study() can then be used to download the gene coverage count data as well as other expression features. The files are saved in a directory named after the SRA accession, in this case SRP045638.

library("recount")

## Find the project ID by searching abstracts of studies
abstract_search("human brain development by age")
##      number_samples species
## 1296             72   human
##                                                                             abstract
## 1296 RNAseq data of 36 samples across human brain development by age group from LIBD
##        project
## 1296 SRP045638
## Download the data if it is not there
if (!file.exists(file.path("SRP045638", "rse_gene.Rdata"))) {
    download_study("SRP045638", type = "rse-gene")
}
## 2022-04-29 08:03:49 downloading file rse_gene.Rdata to SRP045638
## Check that the file was downloaded
file.exists(file.path("SRP045638", "rse_gene.Rdata"))
## [1] TRUE
## Load the data
load(file.path("SRP045638", "rse_gene.Rdata"))

The coverage count matrices are provided as RangedSummarizedExperiment objects (rse) (9). These objects store information at the feature level, the samples and the actual count matrix as shown in Figure 1 of Love et al., 2016 (3). Figure 2 shows the actual rse objects provided by recount2 and how to access the different portions of the data. Using a unique sample ID such as the SRA Run ID it is possible to expand the sample metadata. This can be done using the predicted phenotype provided by add_predictions() (14), pulling information from GEO via find_geo() and geo_characteristics(), or with custom code.

2.4.1 Metadata

Using the colData() function we can access sample metadata. More information on these metadata is provided in the supplementary material of the recount2 paper (7), and we provide a brief review here. The rse objects for SRA data sets include 21 columns with mostly technical information. The GTEx and TCGA rse objects include additional metadata as available from the raw sources. In particular, we compiled metadata for GTEx using the v6 phenotype information available at gtexportal.org, and we put together a large table of TCGA case and sample information by combining information accumulated across Seven Bridges’ Cancer Genomics Cloud and TCGAbiolinks (15).

## One row per sample, one column per phenotype variable
dim(colData(rse_gene))
## [1] 72 21
## Mostly technical variables are included
colnames(colData(rse_gene))
##  [1] "project"                                       
##  [2] "sample"                                        
##  [3] "experiment"                                    
##  [4] "run"                                           
##  [5] "read_count_as_reported_by_sra"                 
##  [6] "reads_downloaded"                              
##  [7] "proportion_of_reads_reported_by_sra_downloaded"
##  [8] "paired_end"                                    
##  [9] "sra_misreported_paired_end"                    
## [10] "mapped_read_count"                             
## [11] "auc"                                           
## [12] "sharq_beta_tissue"                             
## [13] "sharq_beta_cell_type"                          
## [14] "biosample_submission_date"                     
## [15] "biosample_publication_date"                    
## [16] "biosample_update_date"                         
## [17] "avg_read_length"                               
## [18] "geo_accession"                                 
## [19] "bigwig_file"                                   
## [20] "title"                                         
## [21] "characteristics"

2.4.1.1 Technical variables

Several of these technical variables include the number of reads as reported by SRA, the actual number of reads Rail-RNA was able to download (which might be lower in some cases), the number of reads mapped by Rail-RNA, whether the sample is paired-end or not, the coverage AUC and the average read length (times 2 for paired-end samples). Note that the sample with SRA Run ID SRR2071341 has about 240.8 million reads as reported by SRA, while it has 120.4 million spots reported in https://trace.ncbi.nlm.nih.gov/Traces/sra/?run=SRR2071341; that is because it is a paired-end sample (2 reads per spot). These details are important for those interested in writing alternative scaling functions to scale_counts().

## Input reads: number reported by SRA might be larger than number
## of reads Rail-RNA downloaded
colData(rse_gene)[
    ,
    c("read_count_as_reported_by_sra", "reads_downloaded")
]
## DataFrame with 72 rows and 2 columns
##            read_count_as_reported_by_sra reads_downloaded
##                                <integer>        <integer>
## SRR2071341                     240797206        240797206
## SRR2071345                      82266652         82266652
## SRR2071346                     132911310        132911310
## SRR2071347                      74051302         74051302
## SRR2071348                     250259914        250259914
## ...                                  ...              ...
## SRR1554541                     186250218        162403466
## SRR1554554                     140038024        121793680
## SRR1554535                     106244496         91185969
## SRR1554558                     200687480        170754145
## SRR1554553                      90579486         51803404
summary(
    colData(rse_gene)$proportion_of_reads_reported_by_sra_downloaded
)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.5719  0.9165  0.9788  0.9532  1.0000  1.0000
## AUC information used by scale_counts() by default
head(colData(rse_gene)$auc)
## [1] 22950214241  7553726235 12018044330  7041243857 24062460144 45169026301
## Alternatively, scale_scounts() can use the number of mapped reads
## and other information
colData(rse_gene)[, c(
    "mapped_read_count", "paired_end",
    "avg_read_length"
)]
## DataFrame with 72 rows and 3 columns
##            mapped_read_count paired_end avg_read_length
##                    <integer>  <logical>       <integer>
## SRR2071341         232970536       TRUE             200
## SRR2071345          78431778       TRUE             200
## SRR2071346         124493632       TRUE             200
## SRR2071347          71742875       TRUE             200
## SRR2071348         242992735       TRUE             200
## ...                      ...        ...             ...
## SRR1554541         162329325       TRUE             174
## SRR1554554         121738246       TRUE             173
## SRR1554535          91120421       TRUE             171
## SRR1554558         170648458       TRUE             170
## SRR1554553          51684462       TRUE             114

2.4.1.2 Biological information

Other metadata variables included provide more biological information, such as the SHARQ beta tissue and cell type predictions, which are based on processing the abstract of papers. This information is available for some of the SRA projects.

## SHARQ tissue predictions: not present for all studies
head(colData(rse_gene)$sharq_beta_tissue)
## [1] NA NA NA NA NA NA
head(colData(rse_gene_SRP009615)$sharq_beta_tissue)
## [1] "blood" "blood" "blood" "blood" "blood" "blood"

For some data sets we were able to find the GEO accession IDs, which we then used to create the title and characteristics variables. If present, the characteristics information can be used to create additional metadata variables by parsing the CharacterList in which it is stored. Since the input is free text, sometimes more than one type of wording is used to describe the same information, meaning that we might have to process that information in order to build a more convenient variable, such as a factor vector.

## GEO information was absent for the SRP045638 data set
colData(rse_gene)[, c("geo_accession", "title", "characteristics")]
## DataFrame with 72 rows and 3 columns
##            geo_accession       title characteristics
##              <character> <character> <CharacterList>
## SRR2071341            NA          NA              NA
## SRR2071345            NA          NA              NA
## SRR2071346            NA          NA              NA
## SRR2071347            NA          NA              NA
## SRR2071348            NA          NA              NA
## ...                  ...         ...             ...
## SRR1554541            NA          NA              NA
## SRR1554554            NA          NA              NA
## SRR1554535            NA          NA              NA
## SRR1554558            NA          NA              NA
## SRR1554553            NA          NA              NA
## GEO information for the SRP009615 data set
head(colData(rse_gene_SRP009615)$geo_accession)
## [1] "GSM836270" "GSM836271" "GSM836272" "GSM836273" "GSM847561" "GSM847562"
head(colData(rse_gene_SRP009615)$title, 2)
## [1] "K562 cells with shRNA targeting SRF gene cultured with no doxycycline (uninduced - UI), rep1." 
## [2] "K562 cells with shRNA targeting SRF gene cultured with doxycycline for 48 hours (48 hr), rep1."
head(colData(rse_gene_SRP009615)$characteristics, 2)
## CharacterList of length 2
## [[1]] cells: K562 shRNA expression: no treatment: Puromycin
## [[2]] cells: K562 shRNA expression: yes, targeting SRF treatment: Puromycin, doxycycline
## Similar but not exactly the same wording used for two different samples
colData(rse_gene_SRP009615)$characteristics[[1]]
## [1] "cells: K562"          "shRNA expression: no" "treatment: Puromycin"
colData(rse_gene_SRP009615)$characteristics[[11]]
## [1] "cell line: K562"                      
## [2] "shRNA expression: no shRNA expression"
## [3] "treatment: Puromycin"
## Extract the target information
target <- sapply(colData(rse_gene_SRP009615)$characteristics, "[", 2)
target
##  [1] "shRNA expression: no"                             
##  [2] "shRNA expression: yes, targeting SRF"             
##  [3] "shRNA expression: no"                             
##  [4] "shRNA expression: yes targeting SRF"              
##  [5] "shRNA expression: no shRNA expression"            
##  [6] "shRNA expression: expressing shRNA targeting EGR1"
##  [7] "shRNA expression: no shRNA expression"            
##  [8] "shRNA expression: expressing shRNA targeting EGR1"
##  [9] "shRNA expression: no shRNA expression"            
## [10] "shRNA expression: expressing shRNA targeting ATF3"
## [11] "shRNA expression: no shRNA expression"            
## [12] "shRNA expression: expressing shRNA targeting ATF3"
## Build a useful factor vector, set the reference level and append the result
## to the colData() slot
target_factor <- sapply(strsplit(target, "targeting "), "[", 2)
target_factor[is.na(target_factor)] <- "none"
target_factor <- factor(target_factor)
target_factor <- relevel(target_factor, "none")
target_factor
##  [1] none SRF  none SRF  none EGR1 none EGR1 none ATF3 none ATF3
## Levels: none ATF3 EGR1 SRF
colData(rse_gene_SRP009615)$target_factor <- target_factor

As shown in Figure 2, we can expand the biological metadata information by adding predictions based on RNA-seq data (14). The predictions include information about sex, sample source (cell line vs tissue), tissue and the sequencing strategy used. To add the predictions, simply use the function add_predictions() to expand the colData() slot.

## Before adding predictions
dim(colData(rse_gene))
## [1] 72 21
## Add the predictions
rse_gene <- add_predictions(rse_gene)
## 2022-04-29 08:03:55 downloading the predictions to /tmp/RtmpgT6CgH/PredictedPhenotypes_v0.0.06.rda
## Loading objects:
##   PredictedPhenotypes
## After adding the predictions
dim(colData(rse_gene))
## [1] 72 33
## Explore the variables
colData(rse_gene)[, 22:ncol(colData(rse_gene))]
## DataFrame with 72 rows and 12 columns
##            reported_sex predicted_sex accuracy_sex reported_samplesource
##                <factor>      <factor>    <numeric>              <factor>
## SRR2071341       female        female     0.862637                    NA
## SRR2071345       male          male       0.862637                    NA
## SRR2071346       male          male       0.862637                    NA
## SRR2071347       female        female     0.862637                    NA
## SRR2071348       female        female     0.862637                    NA
## ...                 ...           ...          ...                   ...
## SRR1554541       male          male       0.862637                    NA
## SRR1554554       female        female     0.862637                    NA
## SRR1554535       male          male       0.862637                    NA
## SRR1554558       female        female     0.862637                    NA
## SRR1554553       male          male       0.862637                    NA
##            predicted_samplesource accuracy_samplesource reported_tissue
##                          <factor>             <numeric>        <factor>
## SRR2071341                 tissue                    NA              NA
## SRR2071345                 tissue               0.89235              NA
## SRR2071346                 tissue                    NA              NA
## SRR2071347                 tissue                    NA              NA
## SRR2071348                 tissue                    NA              NA
## ...                           ...                   ...             ...
## SRR1554541                 tissue                    NA              NA
## SRR1554554                 tissue                    NA              NA
## SRR1554535                 tissue                    NA              NA
## SRR1554558                 tissue                    NA              NA
## SRR1554553                 tissue               0.89235              NA
##            predicted_tissue accuracy_tissue reported_sequencingstrategy
##                    <factor>       <numeric>                    <factor>
## SRR2071341            Brain        0.518825                      PAIRED
## SRR2071345            Brain        0.518825                      PAIRED
## SRR2071346            Brain        0.518825                      PAIRED
## SRR2071347            Brain        0.518825                      PAIRED
## SRR2071348            Brain        0.518825                      PAIRED
## ...                     ...             ...                         ...
## SRR1554541            Brain        0.518825                      PAIRED
## SRR1554554            Brain        0.518825                      PAIRED
## SRR1554535            Brain        0.518825                      PAIRED
## SRR1554558            Brain        0.518825                      PAIRED
## SRR1554553            Brain        0.518825                      PAIRED
##            predicted_sequencingstrategy accuracy_sequencingstrategy
##                                <factor>                   <numeric>
## SRR2071341                       PAIRED                    0.908575
## SRR2071345                       PAIRED                    0.908575
## SRR2071346                       PAIRED                    0.908575
## SRR2071347                       PAIRED                    0.908575
## SRR2071348                       PAIRED                    0.908575
## ...                                 ...                         ...
## SRR1554541                       PAIRED                    0.908575
## SRR1554554                       PAIRED                    0.908575
## SRR1554535                       PAIRED                    0.908575
## SRR1554558                       PAIRED                    0.908575
## SRR1554553                       PAIRED                    0.908575

2.4.1.3 Adding more information

Ultimately, more sample metadata information could be available elsewhere, which can be useful for analyses. This information might be provided in the paper describing the data, the SRA Run Selector or other sources. As shown in Figure 2, it is possible to append information to the colData() slot as long as there is a unique sample identifier such as the SRA Run ID.

For our example use case, project SRP045638 has a few extra biologically relevant variables via the SRA Run selector https://trace.ncbi.nlm.nih.gov/Traces/study/?acc=SRP045638. We can download that information into text file named SraRunTable.txt by default, then load it into R, sort it appropriately and then append it to the colData() slot. Below we do so for the SRP045638 project.

## Save the information from
## https://trace.ncbi.nlm.nih.gov/Traces/study/?acc=SRP045638
## to a table. We saved the file as SRP045638/SraRunTable.txt.
file.exists(file.path("SRP045638", "SraRunTable.txt"))
## [1] TRUE
## Read the table
sra <- read.csv(file.path("SRP045638", "SraRunTable.txt"),
    header = TRUE
)

## Explore it
head(sra)
##          Run     Age alignment_software..exp. Assay.Type AvgSpotLen       Bases
## 1 SRR1554533 67.7800                  TopHat2    RNA-Seq        179  6766378000
## 2 SRR1554534 40.4200                  TopHat2    RNA-Seq        179  6357169900
## 3 SRR1554535 41.5800                  TopHat2    RNA-Seq        171  9118596900
## 4 SRR1554536 44.1700                  TopHat2    RNA-Seq        184  4697126700
## 5 SRR1554537 -0.3836                  TopHat2    RNA-Seq        182 12199232600
## 6 SRR1554538 -0.4027                  TopHat2    RNA-Seq        185 14733333600
##   Biomaterial_provider  BioProject    BioSample BioSampleModel      Bytes
## 1                 LIBD PRJNA245228 SAMN02731372          Human 3744853436
## 2                 LIBD PRJNA245228 SAMN02731373          Human 3019096844
## 3                 LIBD PRJNA245228 SAMN02999518          Human 5204886934
## 4                 LIBD PRJNA245228 SAMN02999519          Human 2771744419
## 5                 LIBD PRJNA245228 SAMN02999520          Human 6485501388
## 6                 LIBD PRJNA245228 SAMN02999521          Human 7505295858
##                              Center.Name Consent DATASTORE.filetype
## 1 LIEBER INSTITUTE FOR BRAIN DEVELOPMENT  public                sra
## 2 LIEBER INSTITUTE FOR BRAIN DEVELOPMENT  public                sra
## 3 LIEBER INSTITUTE FOR BRAIN DEVELOPMENT  public                sra
## 4 LIEBER INSTITUTE FOR BRAIN DEVELOPMENT  public                sra
## 5 LIEBER INSTITUTE FOR BRAIN DEVELOPMENT  public                sra
## 6 LIEBER INSTITUTE FOR BRAIN DEVELOPMENT  public                sra
##   DATASTORE.provider               DATASTORE.region disease Experiment fraction
## 1         gs,ncbi,s3 gs.US,ncbi.public,s3.us-east-1 Control  SRX683791    total
## 2         gs,ncbi,s3 gs.US,ncbi.public,s3.us-east-1 Control  SRX683792    total
## 3         gs,ncbi,s3 gs.US,ncbi.public,s3.us-east-1 control  SRX683793    total
## 4         gs,ncbi,s3 gs.US,ncbi.public,s3.us-east-1 control  SRX683794    total
## 5         gs,ncbi,s3 gs.US,ncbi.public,s3.us-east-1 control  SRX683795    total
## 6         gs,ncbi,s3 gs.US,ncbi.public,s3.us-east-1 control  SRX683796    total
##            Instrument isolate                   Library.Name LibraryLayout
## 1 Illumina HiSeq 2000   DLPFC R2835_DLPFC_polyA_RNAseq_total        PAIRED
## 2 Illumina HiSeq 2000   DLPFC R2857_DLPFC_polyA_RNAseq_total        PAIRED
## 3 Illumina HiSeq 2000   R2869 R2869_DLPFC_polyA_RNAseq_total        PAIRED
## 4 Illumina HiSeq 2000   R3098 R3098_DLPFC_polyA_RNAseq_total        PAIRED
## 5 Illumina HiSeq 2000   R3452 R3452_DLPFC_polyA_RNAseq_total        PAIRED
## 6 Illumina HiSeq 2000   R3462 R3462_DLPFC_polyA_RNAseq_total        PAIRED
##   LibrarySelection  LibrarySource     Organism Platform race
## 1             cDNA TRANSCRIPTOMIC Homo sapiens ILLUMINA   AA
## 2             cDNA TRANSCRIPTOMIC Homo sapiens ILLUMINA   AA
## 3             cDNA TRANSCRIPTOMIC Homo sapiens ILLUMINA   AA
## 4             cDNA TRANSCRIPTOMIC Homo sapiens ILLUMINA   AA
## 5             cDNA TRANSCRIPTOMIC Homo sapiens ILLUMINA   AA
## 6             cDNA TRANSCRIPTOMIC Homo sapiens ILLUMINA   AA
##            ReleaseDate rin Sample.Name    sex SRA.Study tissue     AssemblyName
## 1 2014-11-13T00:00:00Z 8.3 R2835_DLPFC female SRP045638  DLPFC GCA_000001405.13
## 2 2014-11-13T00:00:00Z 8.4 R2857_DLPFC   male SRP045638  DLPFC GCA_000001405.13
## 3 2014-11-13T00:00:00Z 8.7 R2869_DLPFC   male SRP045638  DLPFC GCA_000001405.13
## 4 2014-11-13T00:00:00Z 5.3 R3098_DLPFC female SRP045638  DLPFC GCA_000001405.13
## 5 2014-11-13T00:00:00Z 9.6 R3452_DLPFC female SRP045638  DLPFC GCA_000001405.13
## 6 2014-11-13T00:00:00Z 6.4 R3462_DLPFC female SRP045638  DLPFC GCA_000001405.13
##   dev_stage
## 1     Fetal
## 2          
## 3          
## 4          
## 5          
## 6
## Set all column names in lower case
colnames(sra) <- tolower(colnames(sra))

## Choose some variables we want to add
sra_vars <- c(
    "sex", "race", "rin", "age", "isolate", "disease",
    "tissue"
)
stopifnot(all(sra_vars %in% colnames(sra)))

## Re-organize the SRA table based on the SRA Run IDs we have
sra <- sra[match(colData(rse_gene)$run, sra$run), ]

## Double check the order
stopifnot(identical(colData(rse_gene)$run, as.character(sra$run)))

## Append the variables of interest
colData(rse_gene) <- cbind(colData(rse_gene), sra[, sra_vars])

## Final dimensions
dim(colData(rse_gene))
## [1] 72 40
## Explore result
colData(rse_gene)[, 34:ncol(colData(rse_gene))]
## DataFrame with 72 rows and 7 columns
##                    sex        race       rin       age     isolate     disease
##            <character> <character> <numeric> <numeric> <character> <character>
## SRR2071341      female          AA       8.3   67.7800       DLPFC     Control
## SRR2071345        male          AA       8.4   40.4200       DLPFC     Control
## SRR2071346        male          AA       8.7   41.5800       R2869     control
## SRR2071347      female          AA       5.3   44.1700       R3098     control
## SRR2071348      female          AA       9.6   -0.3836       R3452     control
## ...                ...         ...       ...       ...         ...         ...
## SRR1554541        male          AA       5.7   -0.3836       R3485     control
## SRR1554554      female          AA       8.1    0.3041       R3669     control
## SRR1554535        male          AA       8.7   41.5800       R2869     control
## SRR1554558      female        CAUC       9.1   16.7000       R4028     control
## SRR1554553        male        CAUC       8.4    0.3918       R3652     control
##                 tissue
##            <character>
## SRR2071341       DLPFC
## SRR2071345       DLPFC
## SRR2071346       DLPFC
## SRR2071347       DLPFC
## SRR2071348       DLPFC
## ...                ...
## SRR1554541       DLPFC
## SRR1554554       DLPFC
## SRR1554535       DLPFC
## SRR1554558       DLPFC
## SRR1554553       DLPFC

Since we have the predicted sex as well as the reported sex via the SRA Run Selector, we can check whether they match.

table(
    "Predicted" = colData(rse_gene)$predicted_sex,
    "Observed" = colData(rse_gene)$sex
)
##             Observed
## Predicted    female male
##   female         24    4
##   male            0   44
##   Unassigned      0    0

2.4.2 DE setup

Now that we have all the metadata available we can perform a DE analysis. The original study for project SRP045638 (13) looked at differences between 6 age groups: prenatal, infant, child, teen, adult and late life. The following code creates these six age groups.

## Create the original 6 age groups
age_bins <- cut(colData(rse_gene)$age, c(-1, 0, 1, 10, 20, 50, Inf),
    include.lowest = TRUE
)
levels(age_bins) <- c(
    "prenatal", "infant", "child", "teen", "adult",
    "late life"
)
colData(rse_gene)$age_group <- age_bins

Most of the DE signal from the original study was between the prenatal and postnatal samples. To simplify the analysis, we will focus on this comparison.

## Create prenatal factor
colData(rse_gene)$prenatal <- factor(
    ifelse(colData(rse_gene)$age_group == "prenatal", "prenatal",
        "postnatal"
    ),
    levels = c("prenatal", "postnatal")
)

As we saw earlier in Figure 9, it is important to scale the coverage counts to read counts. To highlight the fact that we scaled the counts, we will use a new object name and delete the previous one. However, in practice we would simply overwrite the rse object with the output of scale_counts(rse).

## Scale counts
rse_gene_scaled <- scale_counts(rse_gene)

## To highlight that we scaled the counts
rm(rse_gene)

Having scaled the counts, we then filter out genes that are lowly expressed and extract the count matrix.

## Extract counts and filter out lowly expressed geens
counts <- assays(rse_gene_scaled)$counts
filter <- rowMeans(counts) > 0.5

2.4.3 DE analysis

Now that we have scaled the counts, there are multiple DE packages we could use, as described elsewhere (2,3). Since we have 12 samples per group, which is a moderate number, we will use limma-voom (16) due to its speed. The model we use tests for DE between prenatal and postnatal samples adjusting for sex and RIN, which is a measure of quality of the input sample. We check the data with multi-dimensional scaling plots (Figures 13 and 14) as well as the mean-variance plot (Figure 15). In a real use case we might have to explore the results with different models and perform sensitivity analyses.

library("limma")
library("edgeR")

## Build DGEList object
dge <- DGEList(counts = counts[filter, ])

## Calculate normalization factors
dge <- calcNormFactors(dge)

## Explore the data
plotMDS(dge, labels = substr(colData(rse_gene_scaled)$prenatal, 1, 2))