Introduction

We recommend to use DESeq2 on sequencing time course data with the following settings: Firstly, we recommend the usage of a natural cubic spline basis with less degrees of freedom than the number of sampled time points. Secondly, we recommend less stringent dispersion outlier calling to avoid missing differentially expressed genes with observations that are zero. Thirdly, we recommend the usage of condition-wise batch correction in case-control analysis. Below, we supply example code for this adapted DESeq2 model on simulated data in the case-only and the case-control scenario both with and without batch correction:

Case-only differential expression analysis

Without batch correction:

suppressPackageStartupMessages(library(ImpulseDE2))
suppressPackageStartupMessages(library(splines))
suppressPackageStartupMessages(library(DESeq2))
suppressPackageStartupMessages(library(BiocParallel))
## simulate data
lsSim <- simulateDataSetImpulseDE2(
    vecTimePointsA=rep(1:6, 3),
    vecTimePointsB=NULL,
    vecBatchesA=rep("B_NULL", 6*3),
    vecBatchesB=NULL,
    scaNConst=100,
    scaNImp=50,
    scaNLin=50,
    scaNSig=50,
    scaNRand=0,
    scaMumax=1000,
    scaSeedInit = 1)
## run DESeq2
dfAnnotationDESeq2 <- lsSim$dfAnnotation
# create a natural cubic spline basis with 4 degrees of freedom
matTimeSplineBasis <- ns(
    dfAnnotationDESeq2$Time, df=4)
colnames(matTimeSplineBasis) <- 
    paste0("spline", seq(1, dim(matTimeSplineBasis)[2]))
# add spline basis into annotation data frame
dfAnnotationDESeq2 <- cbind(dfAnnotationDESeq2, matTimeSplineBasis)
# create DESeq2 object with spline basis vectors as individual linear predictors
dds <- DESeqDataSetFromMatrix(
    countData = lsSim$matObservedCounts,
    colData = dfAnnotationDESeq2,
    design = ~spline1 + spline2 + spline3 + spline4)
## converting counts to integer mode
# estimate dispersion with less stringent dispersion outlier handling
# the outlier calling is controlled via outlierSD of estimateDispersionsMAP()
dds <- estimateSizeFactors(dds)
dds <- estimateDispersionsGeneEst(dds)
dds <- estimateDispersionsFit(dds)
## -- note: fitType='parametric', but the dispersion trend was not well captured by the
##    function: y = a/x + b, and a local regression fit was automatically substituted.
##    specify fitType='local' or 'mean' to avoid this message next time.
dds <- estimateDispersionsMAP(dds, outlierSD = 10)
# perform a log-likelihood ratio test
dds <- nbinomLRT(dds, full = ~spline1 + spline2 + spline3 + spline4,
                 reduced = ~1)
# extract results
print(head( results(dds) ))
## log2 fold change (MLE): spline4 
## LRT p-value: '~ spline1 + spline2 + spline3 + spline4' vs '~ 1' 
## DataFrame with 6 rows and 6 columns
##        baseMean log2FoldChange     lfcSE       stat      pvalue       padj
##       <numeric>      <numeric> <numeric>  <numeric>   <numeric>  <numeric>
## gene1  300.1373    -0.07164473 0.3442949  1.0974737 0.894672827 0.95177960
## gene2  366.7376    -0.43020095 0.2489286 16.1016613 0.002885739 0.01145135
## gene3  588.3016    -0.25562817 0.4340152  0.6982777 0.951541147 0.98097820
## gene4  952.4941     0.34247246 0.3869531  2.9938291 0.558858622 0.73923098
## gene5  209.4934    -0.20025409 0.3405095  3.4749894 0.481691432 0.68035513
## gene6  977.1628     0.05911703 0.2799341 11.5164745 0.021333532 0.06666729

With batch correction:

suppressPackageStartupMessages(library(ImpulseDE2))
suppressPackageStartupMessages(library(splines))
suppressPackageStartupMessages(library(DESeq2))
suppressPackageStartupMessages(library(BiocParallel))
## simulate data
lsSim <- simulateDataSetImpulseDE2(
    vecTimePointsA=rep(1:6, 3),
    vecTimePointsB=NULL,
    vecBatchesA=do.call(c, lapply(
        seq(1,3), function(i) rep(paste0("B",i), 6) )),
    vecBatchesB=NULL,
    scaMuBatchEffect=1,
    scaSDBatchEffect=0.2,
    scaNConst=100,
    scaNImp=50,
    scaNLin=50,
    scaNSig=50,
    scaNRand=0,
    scaMumax=1000,
    scaSeedInit = 1)
## run DESeq2
dfAnnotationDESeq2 <- lsSim$dfAnnotation
# create a natural cubic spline basis with 4 degrees of freedom
matTimeSplineBasis <- ns(
    dfAnnotationDESeq2$Time, df=4)
colnames(matTimeSplineBasis) <- 
    paste0("spline", seq(1, dim(matTimeSplineBasis)[2]))
# add spline basis into annotation data frame
dfAnnotationDESeq2 <- cbind(dfAnnotationDESeq2, matTimeSplineBasis)
# create DESeq2 object with spline basis vectors as individual linear predictors
dds <- DESeqDataSetFromMatrix(
    countData = lsSim$matObservedCounts,
    colData = dfAnnotationDESeq2,
    design = ~Batch + spline1 + spline2 + spline3 + spline4)
## converting counts to integer mode
## Warning in DESeqDataSet(se, design = design, ignoreRank): some variables in
## design formula are characters, converting to factors
# estimate dispersion with less stringent dispersion outlier handling
# the outlier calling is controlled via outlierSD of estimateDispersionsMAP()
dds <- estimateSizeFactors(dds)
dds <- estimateDispersionsGeneEst(dds)
dds <- estimateDispersionsFit(dds)
## -- note: fitType='parametric', but the dispersion trend was not well captured by the
##    function: y = a/x + b, and a local regression fit was automatically substituted.
##    specify fitType='local' or 'mean' to avoid this message next time.
dds <- estimateDispersionsMAP(dds, outlierSD = 10)
# perform a log-likelihood ratio test
dds <- nbinomLRT(dds, full = ~Batch + spline1 + spline2 + spline3 + spline4,
                 reduced = ~Batch)
# extract results
print(head( results(dds) ))
## log2 fold change (MLE): spline4 
## LRT p-value: '~ Batch + spline1 + spline2 + spline3 + spline4' vs '~ Batch' 
## DataFrame with 6 rows and 6 columns
##        baseMean log2FoldChange     lfcSE       stat     pvalue       padj
##       <numeric>      <numeric> <numeric>  <numeric>  <numeric>  <numeric>
## gene1  332.2238   0.0879040309 0.2924181  0.9434907 0.91824014 0.96535590
## gene2  422.1556   0.9170551773 0.3212048 13.1827163 0.01041667 0.03364355
## gene3  780.6804   0.2937970080 0.3761408  0.9062017 0.92366941 0.96535590
## gene4 1211.7727  -0.4093646633 0.3156516  5.8675786 0.20925825 0.35368283
## gene5  239.4681  -0.1719123830 0.3563992  1.8938956 0.75526629 0.89486528
## gene6 1102.1868   0.0006391691 0.2693686 12.9481841 0.01153177 0.03649294

Case-control differential expression analysis

Without batch correction:

suppressPackageStartupMessages(library(ImpulseDE2))
suppressPackageStartupMessages(library(splines))
suppressPackageStartupMessages(library(DESeq2))
suppressPackageStartupMessages(library(BiocParallel))
## simulate data
lsSim <- simulateDataSetImpulseDE2(
    vecTimePointsA=rep(1:6, 3),
    vecTimePointsB=rep(1:6, 3),
    vecBatchesA=rep("B_NULL", 6*3),
    vecBatchesB=rep("B_NULL", 6*3),
    scaNConst=100,
    scaNImp=50,
    scaNLin=50,
    scaNSig=50,
    scaNRand=0,
    scaMumax=1000,
    scaSeedInit = 1)
## run DESeq2
dfAnnotationDESeq2 <- lsSim$dfAnnotation
# create a natural cubic spline basis with 4 degrees of freedom
matTimeSplineBasis <- ns(
    dfAnnotationDESeq2$Time, df=4)
colnames(matTimeSplineBasis) <- 
    paste0("spline", seq(1, dim(matTimeSplineBasis)[2]))
# add spline basis into annotation data frame
dfAnnotationDESeq2 <- cbind(dfAnnotationDESeq2, matTimeSplineBasis)
# create DESeq2 object with spline basis vectors as individual linear predictors
dds <- DESeqDataSetFromMatrix(
    countData = lsSim$matObservedCounts,
    colData = dfAnnotationDESeq2,
    design = ~Condition + Condition:spline1 + Condition:spline2 + 
        Condition:spline3 + Condition:spline4)
## converting counts to integer mode
## Warning in DESeqDataSet(se, design = design, ignoreRank): some variables in
## design formula are characters, converting to factors
# estimate dispersion with less stringent dispersion outlier handling
# the outlier calling is controlled via outlierSD of estimateDispersionsMAP()
dds <- estimateSizeFactors(dds)
dds <- estimateDispersionsGeneEst(dds)
dds <- estimateDispersionsFit(dds)
## -- note: fitType='parametric', but the dispersion trend was not well captured by the
##    function: y = a/x + b, and a local regression fit was automatically substituted.
##    specify fitType='local' or 'mean' to avoid this message next time.
dds <- estimateDispersionsMAP(dds, outlierSD = 10)
# perform a log-likelihood ratio test
dds <- nbinomLRT(dds, full = ~Condition + Condition:spline1 + Condition:spline2 + 
                     Condition:spline3 + Condition:spline4,
                 reduced = ~spline1 + spline2 + spline3 + spline4)
# extract results
print(head( results(dds) ))
## log2 fold change (MLE): Conditioncontrol.spline4 
## LRT p-value: '~ Condition + Condition:spline1 + Condition:spline2 + Condition:spline3 + Condition:spline4' vs '~ spline1 + spline2 + spline3 + spline4' 
## DataFrame with 6 rows and 6 columns
##        baseMean log2FoldChange     lfcSE      stat     pvalue       padj
##       <numeric>      <numeric> <numeric> <numeric>  <numeric>  <numeric>
## gene1  253.1908     -0.1318652 0.3924393  7.732633 0.17159937 0.40121224
## gene2  388.7622     -0.3678561 0.3906130  8.315500 0.13968363 0.34920908
## gene3  589.7440      0.6272186 0.3207137 13.386425 0.02001451 0.07942264
## gene4  915.7521      0.5030112 0.3572772  6.974332 0.22255627 0.46365890
## gene5  192.9654      0.4259325 0.3920925  6.767761 0.23849309 0.48251801
## gene6  908.7567     -0.3362782 0.3145794  5.713672 0.33508413 0.58180898

With batch correction:

suppressPackageStartupMessages(library(ImpulseDE2))
suppressPackageStartupMessages(library(splines))
suppressPackageStartupMessages(library(DESeq2))
suppressPackageStartupMessages(library(BiocParallel))
## simulate data
lsSim <- simulateDataSetImpulseDE2(
    vecTimePointsA=rep(1:6, 3),
    vecTimePointsB=rep(1:6, 3),
    vecBatchesA=do.call(c, lapply(
        seq(1,3), function(i) rep(paste0("B",i), 6) )),
    vecBatchesB=do.call(c, lapply(
        seq(1,3), function(i) rep(paste0("B",i), 6) )),
    scaMuBatchEffect=1,
    scaSDBatchEffect=0.2,
    scaNConst=100,
    scaNImp=50,
    scaNLin=50,
    scaNSig=50,
    scaNRand=0,
    scaMumax=1000,
    scaSeedInit = 1)
## run DESeq2
dfAnnotationDESeq2 <- lsSim$dfAnnotation
# create a natural cubic spline basis with 4 degrees of freedom
matTimeSplineBasis <- ns(
    dfAnnotationDESeq2$Time, df=4)
colnames(matTimeSplineBasis) <- 
    paste0("spline", seq(1, dim(matTimeSplineBasis)[2]))
# add spline basis into annotation data frame
dfAnnotationDESeq2 <- cbind(dfAnnotationDESeq2, matTimeSplineBasis)
# create DESeq2 object with spline basis vectors as individual linear predictors
dds <- DESeqDataSetFromMatrix(
    countData = lsSim$matObservedCounts,
    colData = dfAnnotationDESeq2,
    design = ~Condition + Condition:spline1 + Condition:spline2 + 
        Condition:spline3 + Condition:spline4 + Condition:Batch)
## converting counts to integer mode
## Warning in DESeqDataSet(se, design = design, ignoreRank): some variables in
## design formula are characters, converting to factors
# estimate dispersion with less stringent dispersion outlier handling
# the outlier calling is controlled via outlierSD of estimateDispersionsMAP()
dds <- estimateSizeFactors(dds)
dds <- estimateDispersionsGeneEst(dds)
dds <- estimateDispersionsFit(dds)
dds <- estimateDispersionsMAP(dds, outlierSD = 10)
# perform a log-likelihood ratio test
dds <- nbinomLRT(dds, full = ~Condition + Condition:spline1 + Condition:spline2 + 
                     Condition:spline3 + Condition:spline4 + Condition:Batch,
                 reduced = ~spline1 + spline2 + spline3 + spline4 + Batch)
# extract results
print(head( results(dds) ))
## log2 fold change (MLE): Conditioncontrol.BatchB3 
## LRT p-value: '~ Condition + Condition:spline1 + Condition:spline2 + Condition:spline3 + Condition:spline4 + Condition:Batch' vs '~ spline1 + spline2 + spline3 + spline4 + Batch' 
## DataFrame with 6 rows and 6 columns
##        baseMean log2FoldChange     lfcSE      stat     pvalue      padj
##       <numeric>      <numeric> <numeric> <numeric>  <numeric> <numeric>
## gene1  255.5417     0.07621837 0.2898356 11.563099 0.11587328 0.3656402
## gene2  389.9618     0.03258513 0.2737443 12.353518 0.08951636 0.3108207
## gene3  595.1599    -0.06389875 0.2317662 10.649002 0.15467904 0.4210152
## gene4  925.8095    -0.02921463 0.2457753  6.665662 0.46450065 0.7826958
## gene5  200.5167    -0.33910048 0.2928535 13.963302 0.05183793 0.2124505
## gene6  911.3917     0.25502562 0.2275118  7.573716 0.37167789 0.6659283