Skip to content

Latest commit

 

History

History
308 lines (237 loc) · 9.93 KB

File metadata and controls

308 lines (237 loc) · 9.93 KB

Normalisation of CAGE data by sub-sampling

The number of different genes detected when sequencing a transcriptome library increases slower and slower as reads are added: by definition the first read is always new; the second has high chances to be different from the first, etc., but after millions of them each extra read will have much higer chances to be identical to another read already sequenced. The consequence of this is that analysing the same library sequenced deeply, for instance on HiSeq, or shallowly, for instance on MiSeq, will not yield the same result in terms of gene discovery, etc. See Dave Tang's blog for a longer discussion on sequencing depth.

This tutorial is about comparing libraries on properties that are not invariant with sequencing depth. The solution presented here is to normalise the libraries to the same depth, that is, to use the same number of reads for each library. There are mainly two ways: either input a fixed number of reads in the alignment pipeline, or sub-sample a fixed number of alignments after using all the reads. This tutorial shows how do the second solution for CAGE data using R.

Busy people familiar with CAGE and R can skip the tutorial and read the manual of the rrarefy command of the vegan package.

Information and download

See the main README for general recommendations on how or what to prepare before running this tutorial.

The data downloaded here is the count of CAGE tags in the FANTOM5 CAGE peaks for all the Phase 1 libraries of the FANTOM 5 project. See the README file for more information on that file

wget --quiet --timestamping https://fantom.gsc.riken.jp/5/datafiles/latest/extra/CAGE_peaks/hg19.cage_peak_phase1and2combined_counts.osc.txt.gz
echo "0b288555ef51d1f1f9f04a2536d51a1d  hg19.cage_peak_phase1and2combined_counts.osc.txt.gz" | md5sum -c
## hg19.cage_peak_phase1and2combined_counts.osc.txt.gz: OK

Data loading and preparation in R

The table is in Order-Switchable Columns format. The read.table command in R will automatically discard its comment lines, set up column names with the header=TRUE option and row names with the row.names=1 option.

The table is large: so loading the table will take time…

osc <- read.table('hg19.cage_peak_phase1and2combined_counts.osc.txt.gz', row.names=1, header=TRUE)
dim(osc)
## [1] 201803   1829

The name of the libraries are long because they contain a plain English description of their contents. We will shorten them to their identifier. For example, counts.Adipocyte%20-%20breast%2c%20donor1.CNhs11051.11376-118A8 becomes CNhs11051. The association can be re-made using FANTOM5 SDRF files.

colnames(osc) <- regmatches(colnames(osc), regexpr('CNhs.....', colnames(osc)))

The first line, 01STAT:MAPPED, is special and contains the total number of tags for each library. The sum of all tags in all peaks is lower than this, because some tags are not in peaks. We will change the value of 01STAT:MAPPED so that the sum of the columns in our table is the total number of reads for each library.

summary(t(osc["01STAT:MAPPED",]))
##  01STAT:MAPPED     
##  Min.   :    6602  
##  1st Qu.: 1787336  
##  Median : 3229346  
##  Mean   : 3808352  
##  3rd Qu.: 5254150  
##  Max.   :16059002
osc['01STAT:MAPPED',] <- osc['01STAT:MAPPED',] - colSums(osc[-grep('01STAT:MAPPED', rownames(osc)),])
summary(colSums(osc), digits=10)
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
##     6602  1787336  3229346  3808352  5254150 16059002

Number of peaks detected and total number of tags, part 1

Let's see now how many CAGE peaks are detected per library. The command osc > 0 produces a data frame containing TRUE where a tag count was higher than zero, and FALSE otherwise. In R, since TRUE equals 1 and FALSE equals 0, the colSums command applied on this data fame of TRUE/FALSE values will then count the detected peaks. (Note: the proper name of TRUE/FALSE values is boolean.)

summary(colSums(osc > 0), digits=10)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##     360   44966   55314   54276   64576  121973

There are large variations in the number of peaks detected. Let's take the example of subcutaneous adipocytes (libraries CNhs12494, CNhs11371 and CNhs12017).

numberOfPeaks <- colSums(osc > 0)
numberOfTags <- colSums(osc)
adipocytes <- c('CNhs12494', 'CNhs11371', 'CNhs12017')
numberOfPeaks[adipocytes]
## CNhs12494 CNhs11371 CNhs12017 
##     27728     41048     54730
numberOfTags[adipocytes]
## CNhs12494 CNhs11371 CNhs12017 
##    535250   1384056   4224299

Do we see more peaks just because there were more tags ?

Sub-sampling

Here, we will remove tags from the data until each library has the same number of tags, that is, we will normalise the sequencing depth on the most shallow library.

We will use the rrarefy command of the vegan package. Unlike most R commands that work on data frames, this command uses rows by default, so we will transpose the table, sub-sample it, and transpose it again.

The sub-sampling removes tags randomly, so each time it is run it will never produce exactly the same result. However, highly expressed peaks will stay highly expressed, etc. For this tutorial, the computation is made identical across runs by setting the random number seed with the set.seed command (and resetting it with rm(.Random.seed)). Note: do not use set.seed() in your project if you do not understand well the consequences.

library(vegan)
## Loading required package: permute
## Loading required package: lattice
## This is vegan 2.6-4
set.seed(1)
minNumberOfTags <- 500000
# Let's discard the libraries that do not have enough tags.
summary(numberOfTags > minNumberOfTags)
##    Mode   FALSE    TRUE 
## logical     105    1724
osc.sub <- t(rrarefy(t(osc[,numberOfTags > minNumberOfTags]), minNumberOfTags))
rm(.Random.seed)
summary(colSums(osc.sub), digits=10)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   5e+05   5e+05   5e+05   5e+05   5e+05   5e+05

That is all ! Now all the libraries contain the same number of tags.

Number of peaks detected and total number of tags, part 2

normalisedNumberOfPeaks <- colSums(osc.sub > 0)
normalisedNumberOfPeaks[adipocytes]
## CNhs12494 CNhs11371 CNhs12017 
##     27011     28705     27507
colSums(osc.sub)[adipocytes]
## CNhs12494 CNhs11371 CNhs12017 
##     5e+05     5e+05     5e+05
barplot( log10( cbind( normalisedNumberOfPeaks[adipocytes]
                     , numberOfPeaks[adipocytes]
                     , numberOfTags[adipocytes])
              )
       , beside=TRUE
       , legend=TRUE
       , args.legend=list( x="topleft"
                         , title="library ID")
       , main="Effect of sub-sampling normalisation on number of detected peaks"
       , ylab="log10"
)

plot of chunk peakNumberBarplot

The number of detected peaks is now very similar between the three biological replicates !

Further uses of sub-sampling.

One can also use sub-sampling to compare the expression profile of CAGE peaks, normalising for the fact that low-expressed peaks will be found in less libraries. The solution would be asking a question like “what would be the profile of a promoter if only 100 reads had aligned to it ?”. This question is related to the supplementary note number 4 of the FANTOM 5 paper (Forrest et al., 2014), on ubiquitous and tissue-restricted expression, and will be the topic of a future tutorial.

Session information

sessionInfo()
## R version 4.3.0 (2023-04-21)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Debian GNU/Linux 12 (bookworm)
## 
## Matrix products: default
## BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.11.0 
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.11.0
## 
## locale:
##  [1] LC_CTYPE=en_GB.UTF-8       LC_NUMERIC=C               LC_TIME=en_GB.UTF-8        LC_COLLATE=en_GB.UTF-8     LC_MONETARY=en_GB.UTF-8   
##  [6] LC_MESSAGES=en_GB.UTF-8    LC_PAPER=en_GB.UTF-8       LC_NAME=C                  LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=en_GB.UTF-8 LC_IDENTIFICATION=C       
## 
## time zone: Etc/UTC
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] vegan_2.6-4     lattice_0.20-45 permute_0.9-7  
## 
## loaded via a namespace (and not attached):
##  [1] nlme_3.1-162    cli_3.6.1       knitr_1.43      rlang_1.1.1     xfun_0.39       highr_0.10      jsonlite_1.8.5  htmltools_0.5.5 sass_0.4.6     
## [10] rmarkdown_2.22  grid_4.3.0      evaluate_0.21   jquerylib_0.1.4 MASS_7.3-58.2   fastmap_1.1.1   yaml_2.3.7      cluster_2.1.4   compiler_4.3.0 
## [19] mgcv_1.8-41     rstudioapi_0.14 digest_0.6.31   R6_2.5.1        parallel_4.3.0  splines_4.3.0   bslib_0.5.0     Matrix_1.5-3    tools_4.3.0    
## [28] cachem_1.0.8