The previous section walked-through the pre-processing and transformation of the count data. Here, for completeness, we list the minimal steps required to process the data prior to differential expression analysis.
# Read the sample information into a data frame
sampleinfo <- read_tsv("data/SampleInfo.txt")
# Read the data into R
seqdata <- read_tsv("data/GSE60450_Lactation.featureCounts", comment = "#")
# Transform the data to matrix of counts
countdata <- as.data.frame(seqdata) %>%
column_to_rownames("Geneid") %>% # turn the geneid column into rownames
rename_all(str_remove, ".bam") %>% # remove the ".bam" from the column names
select(sampleinfo$Sample) %>% # keep sample columns using sampleinfo
as.matrix()
# filter the data to remove genes with few counts
keep <- rowSums(countdata) > 5
countdata <- countdata[keep,]
Alternatively we can load the `objects with the RData file we created in the pre-processing tutorial.
# load the RData object we created in the previous session
load("Robjects/preprocessing.RData")
ls()
[1] "countdata" "sampleinfo" "wibble"
dim(countdata)
[1] 22013 12
sampleinfo
First load the packages we need.
library(tidyverse)
library(DESeq2)
Now that we are happy that that the quality of the data looks good, we can proceed to testing for differentially expressed genes. There are a number of packages to analyse RNA-Seq data. Most people use DESeq2 or edgeR. They are both equally applicable. There is an informative and honest blog post here by Mike Love, one of the authors of DESeq2, about deciding which to use.
We will use DESeq2 for the rest of this practical.
First we need to create a design model formula for our analysis. DESeq2
will use this to generate the model matrix, as we have seen in the linear models lecture.
We have two variables: “status”" and “cell type”. We will fit two models under two assumptions: no interaction and interaction of these two factors.
Let’s start with the model with only main effects, that is no interaction. The main assumption here is that the effect of the status is the same in both type of cells.
# Use the standard R 'formula' syntax for an additive model
design <- as.formula(~ CellType + Status)
What does this look like as a model matrix?
modelMatrix <- model.matrix(design, data = sampleinfo)
modelMatrix
(Intercept) CellTypeluminal Statuspregnant Statusvirgin
1 1 0 0 1
2 1 0 0 1
3 1 0 1 0
4 1 0 1 0
5 1 0 0 0
6 1 0 0 0
7 1 1 0 1
8 1 1 0 1
9 1 1 1 0
10 1 1 1 0
11 1 1 0 0
12 1 1 0 0
attr(,"assign")
[1] 0 1 2 2
attr(,"contrasts")
attr(,"contrasts")$CellType
[1] "contr.treatment"
attr(,"contrasts")$Status
[1] "contr.treatment"
It would be nice if virgin
were the the base line/intercept. To get R to use virgin
as the intercept we need to use a factor
. Let’s set factor levels on Status to use virgin as the intercept.
sampleinfo$Status <- factor(sampleinfo$Status,
levels = c("virgin", "pregnant", "lactate"))
modelMatrix <- model.matrix(design, data = sampleinfo)
modelMatrix
(Intercept) CellTypeluminal Statuspregnant Statuslactate
1 1 0 0 0
2 1 0 0 0
3 1 0 1 0
4 1 0 1 0
5 1 0 0 1
6 1 0 0 1
7 1 1 0 0
8 1 1 0 0
9 1 1 1 0
10 1 1 1 0
11 1 1 0 1
12 1 1 0 1
attr(,"assign")
[1] 0 1 2 2
attr(,"contrasts")
attr(,"contrasts")$CellType
[1] "contr.treatment"
attr(,"contrasts")$Status
[1] "contr.treatment"
We don’t actually need to pass DESeq2
the model matrix, instead we pass it the design formula and the sampleinfo
it will build the matrix itself.
# create the DESeqDataSet object
ddsObj.raw <- DESeqDataSetFromMatrix(countData = countdata,
colData = sampleinfo,
design = design)
some variables in design formula are characters, converting to factors
Let’s plot a PCA from vst
transformed data. Can you anticipate if the interaction term will be important?
vstcounts <- vst(ddsObj.raw, blind=TRUE)
plotPCA(vstcounts, intgroup=c("Status", "CellType"))
DESeq2
work flowThe main DESeq2
work flow is carried out in 3 steps:
First, Calculate the “median ratio” normalisation size factors…
ddsObj <- estimateSizeFactors(ddsObj.raw)
… then estimate dispersion …
ddsObj <- estimateDispersions(ddsObj)
gene-wise dispersion estimates
mean-dispersion relationship
final dispersion estimates
… finally, apply Negative Binomial GLM fitting and calculate Wald statistics
ddsObj <- nbinomWaldTest(ddsObj)
DESeq
commandIn practice the 3 steps above can be performed in a single step using the DESeq
wrapper function. Performing the three steps separately is useful if you wish to alter the default parameters of one or more steps, otherwise the DESeq
function is fine.
# Run DESeq
ddsObj <- DESeq(ddsObj.raw)
estimating size factors
estimating dispersions
gene-wise dispersion estimates
mean-dispersion relationship
final dispersion estimates
fitting model and testing
We can generate a table of differential expression results from the DDS object using the results
function of DESeq2.
res <- results(ddsObj, alpha=0.05)
head(res)
log2 fold change (MLE): Status lactate vs virgin
Wald test p-value: Status lactate vs virgin
DataFrame with 6 rows and 6 columns
baseMean log2FoldChange lfcSE
<numeric> <numeric> <numeric>
ENSMUSG00000051951 193.628316332322 0.685666258944297 0.756712220192481
ENSMUSG00000102331 0.552133378452342 1.82614307383877 2.73990068528126
ENSMUSG00000025900 2.09442070179461 -3.02782655402916 1.54150999536423
ENSMUSG00000025902 52.4208304161579 -0.703252517806886 0.417356581488203
ENSMUSG00000098104 0.684213614702782 0.201738946690919 1.68661571665909
ENSMUSG00000103922 27.5871076674379 1.63592009157741 0.669531803485365
stat pvalue padj
<numeric> <numeric> <numeric>
ENSMUSG00000051951 0.906112311454264 0.364876409573908 0.520197122671947
ENSMUSG00000102331 0.666499732508119 0.505091734596282 NA
ENSMUSG00000025900 -1.96419521322256 0.0495074577244765 0.117366437063074
ENSMUSG00000025902 -1.68501600070434 0.0919854844526348 0.189956255812803
ENSMUSG00000098104 0.119611684332298 0.904790763421973 NA
ENSMUSG00000103922 2.44337921374511 0.0145504392990085 0.0446034399345576
You will notice that some of the adjusted p-values (padj
) are NA. Remember in Session 2 we said that there is no need to pre-filter the genes as DESeq2 will do this through a process it calls ‘independent filtering’. The genes with NA
are the ones DESeq2
has filtered out.
From DESeq2
manual: “The results function of the DESeq2
package performs independent filtering by default using the mean of normalized counts as a filter statistic. A threshold on the filter statistic is found which optimizes the number of adjusted p values lower than a [specified] significance level”.
The default significance level for independent filtering is 0.1
, however, you should set this to the FDR cut off you are planning to use. We will use 0.05
- this was the purpose of the alpha
argument in the previous command.
results
The results
function has returned the results for the contrast “lactate vs virgin”. Let’s have a look at the model matrix to understand why DESeq2
has given us this particular contrast.
modelMatrix
(Intercept) CellTypeluminal Statuspregnant Statuslactate
1 1 0 0 0
2 1 0 0 0
3 1 0 1 0
4 1 0 1 0
5 1 0 0 1
6 1 0 0 1
7 1 1 0 0
8 1 1 0 0
9 1 1 1 0
10 1 1 1 0
11 1 1 0 1
12 1 1 0 1
attr(,"assign")
[1] 0 1 2 2
attr(,"contrasts")
attr(,"contrasts")$CellType
[1] "contr.treatment"
attr(,"contrasts")$Status
[1] "contr.treatment"
By default, results
has returned the contrast encoded by the final column in the model matrix. DESeq2
has the command resultsNames
that allows us to view the contrasts that are available directly from the model matrix.
resultsNames(ddsObj)
[1] "Intercept" "CellType_luminal_vs_basal"
[3] "Status_pregnant_vs_virgin" "Status_lactate_vs_virgin"
Let’s just rename res
so that we know which contrast results it contains.
resLvV <- res
rm(res)
If we want a different contrast we can just pass the results
function the name of the design matrix column that encodes it. Let’s retrieve the results for pregant versus virgin
resPvV <- results(ddsObj,
name="Status_pregnant_vs_virgin",
alpha = 0.05)
resPvV
log2 fold change (MLE): Status pregnant vs virgin
Wald test p-value: Status pregnant vs virgin
DataFrame with 22013 rows and 6 columns
baseMean log2FoldChange lfcSE
<numeric> <numeric> <numeric>
ENSMUSG00000051951 193.628316332322 -0.954402462919142 0.758163669842639
ENSMUSG00000102331 0.552133378452342 0.793605030301116 2.76321615794719
ENSMUSG00000025900 2.09442070179461 -0.422312119520873 1.28960999953244
ENSMUSG00000025902 52.4208304161579 -0.372373337372365 0.399035417679523
ENSMUSG00000098104 0.684213614702782 -1.47471635546294 1.79948663916206
... ... ... ...
ENSMUSG00000064367 54055.8663105821 0.344588799853028 0.164346846848978
ENSMUSG00000064368 13454.3113895505 0.426945077829144 0.156109922741008
ENSMUSG00000064370 111885.216173771 0.415862003187525 0.225727285622729
ENSMUSG00000064372 396.00873428211 0.262372947574106 0.261696769419487
ENSMUSG00000095742 632.014515495881 -0.191806454656634 0.355891725136564
stat pvalue padj
<numeric> <numeric> <numeric>
ENSMUSG00000051951 -1.25883433997468 0.208090174010857 0.411522854118992
ENSMUSG00000102331 0.287203383643606 0.773956595640506 NA
ENSMUSG00000025900 -0.327472739567765 0.743310358008101 NA
ENSMUSG00000025902 -0.933183674616645 0.350725142291883 0.564484945558628
ENSMUSG00000098104 -0.819520591800363 0.412489458421745 NA
... ... ... ...
ENSMUSG00000064367 2.09671683065316 0.036018649711218 0.124182953553429
ENSMUSG00000064368 2.73490032108632 0.006239916868708 0.0341430901523808
ENSMUSG00000064370 1.84232048881578 0.0654282845131489 0.190916096242412
ENSMUSG00000064372 1.002583823087 0.316061704203191 0.530023878511752
ENSMUSG00000095742 -0.538946092615762 0.589924051630021 0.762012734103729
Let’s get the top 100 genes by adjusted p-value
topGenesPvV <- as.data.frame(resPvV) %>%
rownames_to_column("GeneID") %>%
arrange(padj) %>%
head(100)
topGenesPvV
Challenge 1
Obtain results for luminal vs basal and find the top 200 genes. Call the new results object
resBvL
.
Suppose we want to find differentially expressed genes between pregnant and lactate. We don’t have a parameter that explicitly will allow us to test that hypothesis. We need to provide a contrast.
resultsNames(ddsObj)
[1] "Intercept" "CellType_luminal_vs_basal"
[3] "Status_pregnant_vs_virgin" "Status_lactate_vs_virgin"
resPvL <- results(ddsObj,
contrast=c("Status", "pregnant", "lactate"),
alpha = 0.05)
resPvL
log2 fold change (MLE): Status pregnant vs lactate
Wald test p-value: Status pregnant vs lactate
DataFrame with 22013 rows and 6 columns
baseMean log2FoldChange lfcSE
<numeric> <numeric> <numeric>
ENSMUSG00000051951 193.628316332322 -1.64006872186344 0.76011381700911
ENSMUSG00000102331 0.552133378452342 -1.03253804353765 2.70726895584589
ENSMUSG00000025900 2.09442070179461 2.60551443450829 1.58113233162031
ENSMUSG00000025902 52.4208304161579 0.330879180434521 0.42458801011868
ENSMUSG00000098104 0.684213614702782 -1.67645530215386 1.83315246550495
... ... ... ...
ENSMUSG00000064367 54055.8663105821 -0.302824789042765 0.164348578199341
ENSMUSG00000064368 13454.3113895505 -0.2514951308436 0.156113680277036
ENSMUSG00000064370 111885.216173771 -0.196775657448966 0.225728145700564
ENSMUSG00000064372 396.00873428211 -0.0171568600400709 0.262491263103216
ENSMUSG00000095742 632.014515495881 0.280640913843416 0.357511558787644
stat pvalue padj
<numeric> <numeric> <numeric>
ENSMUSG00000051951 -2.15766203055849 0.0309541180695751 0.106766273830347
ENSMUSG00000102331 -0.381394704544615 0.702910389782284 NA
ENSMUSG00000025900 1.64787879066277 0.0993775464909503 NA
ENSMUSG00000025902 0.779294686964981 0.43580614370291 0.636130155679824
ENSMUSG00000098104 -0.91452038698378 0.360443470413369 NA
... ... ... ...
ENSMUSG00000064367 -1.84257626296872 0.0653909022821323 0.183136793004815
ENSMUSG00000064368 -1.6109743258714 0.107185318448968 0.258163585175872
ENSMUSG00000064370 -0.871737358397456 0.383351674855471 0.588905143356533
ENSMUSG00000064372 -0.0653616422780692 0.947886063772488 0.973183515621002
ENSMUSG00000095742 0.784984168889803 0.432462850616724 0.633552881520095
Suppose we thought that maybe status
were irrelevant and really the only differences might be between cell types. We could fit a simpler model, this would give us more degrees of freedom and therefore more power, but how would we know if it was a better model of not? We can compare the two models using the “log ratio test” (LRT).
designC <- as.formula(~ CellType )
# Compare the designs
ddsObjC <- DESeq(ddsObj, test="LRT", reduced=designC)
using pre-existing size factors
estimating dispersions
found already estimated dispersions, replacing these
gene-wise dispersion estimates
mean-dispersion relationship
final dispersion estimates
fitting model and testing
resCvCS <- results(ddsObjC)
resCvCS
log2 fold change (MLE): Status lactate vs virgin
LRT p-value: '~ CellType + Status' vs '~ CellType'
DataFrame with 22013 rows and 6 columns
baseMean log2FoldChange lfcSE
<numeric> <numeric> <numeric>
ENSMUSG00000051951 193.628316332322 0.685666258944297 0.756712220192481
ENSMUSG00000102331 0.552133378452342 1.82614307383877 2.73990068528126
ENSMUSG00000025900 2.09442070179461 -3.02782655402916 1.54150999536423
ENSMUSG00000025902 52.4208304161579 -0.703252517806886 0.417356581488203
ENSMUSG00000098104 0.684213614702782 0.201738946690919 1.68661571665909
... ... ... ...
ENSMUSG00000064367 54055.8663105821 0.647413588895793 0.164350605695024
ENSMUSG00000064368 13454.3113895505 0.678440208672744 0.156129934723267
ENSMUSG00000064370 111885.216173771 0.612637660636491 0.225729381046246
ENSMUSG00000064372 396.00873428211 0.279529807614177 0.262569324325279
ENSMUSG00000095742 632.014515495881 -0.47244736850005 0.357092689972414
stat pvalue padj
<numeric> <numeric> <numeric>
ENSMUSG00000051951 4.40903494919066 0.110303736820745 0.203011375932541
ENSMUSG00000102331 0.397659679657469 0.819689359948631 NA
ENSMUSG00000025900 4.68578354201197 0.0960494831334411 0.182314817580936
ENSMUSG00000025902 2.89099132419601 0.235629255115333 0.361374169977365
ENSMUSG00000098104 1.1310712566686 0.568055798474907 NA
... ... ... ...
ENSMUSG00000064367 15.2088899461407 0.000498231876884956 0.00246460256311396
ENSMUSG00000064368 18.6633472727525 8.85738725215165e-05 0.000552135624089729
ENSMUSG00000064370 7.28491634375843 0.0261878904433618 0.0653923895110851
ENSMUSG00000064372 1.38080328700443 0.501374654748133 0.622036554157161
ENSMUSG00000095742 1.63512832055875 0.441505783095712 0.569776660113684
The null hypothesis is that there is no significant difference between the two models, i.e. the simpler model is sufficient to explain the variation in gene expression between the samples. If the the adjusted p-value for a gene passes a significance threshold (e.g. padj < 0.05) then we should consider using the more complex model for this gene. In practice we would usually choose one model or the other and apply it to all genes.
Challenge 2
When we looked at the PCA it did seem that an interaction model might be warranted. Let’s test that.
1.Fit a model with interaction. 2. Use the LRT to compare the two models.
3. Is the number of replicates good enough to include the interaction?
4. Is the interaction needed in the model?
save(resLvV, ddsObj, sampleinfo, file="results/DE.RData")