Contents

1 Introduction

LineagePulse is a differential expression algorithm for single-cell RNA-seq (scRNA-seq) data. LineagePulse is based on zero-inflated negative binomial noise model and can capture both discrete and continuous population structures: Discrete population structures are groups of cells (e.g. condition of an experiment or tSNE clusters). Continous population structures can for example be pseudotemporal orderings of cells or temporal orderings of cells. The main use and novelty of LineagePulse lies in its ability to fit gene expression trajectories on pseudotemporal orderings of cells well. Note that LineagePulse does not infer a pseudotemporal ordering but is a downstream analytic tool to analyse gene expression trajectories on a given pseudotemporal ordering (such as from diffusion pseudotime or monocle2).

To run LineagPulse on scRNA-seq data, the user needs to use a minimal input parameter set for the wrapper function runLineagePulse, which then performs all normalisation, model fitting and differential expression analysis steps without any more user interaction required:

Additionally, one can provide:

Lastly, the experienced user who has a solid grasp of the mathematical and algorithmic basis of LineagePulse may change the defaults of these advanced input options:

2 Differential expression analysis

Here, we present a differential expression analysis scenario on a longitudinal ordering. The differential expression results are in a data frame which can be accessed from the output object via list like properties ($). The core differential expression analysis result are p-value and false-discovery-rate corrected p-value of differential expression which are the result of a gene-wise hypothesis test of a non-constant expression model (impulse, splines or groups) versus a constant expression model.

library(LineagePulse)
lsSimulatedData <- simulateContinuousDataSet(
  scaNCells = 100,
  scaNConst = 10,
  scaNLin = 10,
  scaNImp = 10,
  scaMumax = 100,
  scaSDMuAmplitude = 3,
  vecNormConstExternal=NULL,
  vecDispExternal=rep(20, 30),
  vecGeneWiseDropoutRates = rep(0.1, 30))
## Draw mean trajectories
## Setting size factors uniformly =1
## Draw dispersion
## Simulate negative binomial noise
## Simulate drop-out
objLP <- runLineagePulse(
  counts = lsSimulatedData$counts,
  dfAnnotation = lsSimulatedData$annot)
## LineagePulse for count data: v1.21.0
## --- Data preprocessing
## # 0 out of 100 cells did not have a continuous covariate and were excluded.
## # 0 out of 30 genes did not contain non-zero observations and are excluded from analysis.
## # 0 out of 100 cells did not contain non-zero observations and are excluded from analysis.
## --- Compute normalisation constants:
## # All size factors are set to one.
## --- Fit ZINB model for both H1 and H0.
## ### a) Fit ZINB model A (H0: mu=constant disp=constant) with noise model.
## #  .   Initialisation: ll -25835.427735371
## # 1.  Iteration with ll   -13580.4563543946 in 0.02 min.
## # 2.  Iteration with ll   -13545.5012826231 in 0.03 min.
## # 3.  Iteration with ll   -13545.5012709603 in 0.02 min.
## Finished fitting zero-inflated negative binomial model A with noise model in 0.1 min.
## ### b) Fit ZINB model B (H1: mu=splines disp=constant).
## #  .   Initialisation: ll -14992.0933009712
## # 1.  Iteration with ll   -13131.467437459 in 0.02 min.
## Finished fitting zero-inflated negative binomial model B in 0.03 min.
## ### c) Fit NB model A (H0: mu=constant disp=constant).
## #  .   Initialisation: ll -14837.7220533234
## # 1.  Iteration with ll   -14615.7441459006 in 0.01 min.
## Finished fitting NB model B in 0.02 min.
## ### d) Fit NB model B (H1: mu=splines disp=constant).
## #  .   Initialisation: ll -15009.9308718258
## # 1.  Iteration with ll   -14516.2074850154 in 0.02 min.
## Finished fitting NB model B in 0.03 min.
## Time elapsed during ZINB fitting: 0.22 min
## --- Run differential expression analysis.
## Finished runLineagePulse().
head(objLP$dfResults)
##          gene         p      padj  mean_H0      p_nb   padj_nb df_full_zinb
## gene_1 gene_1 0.2462125 0.3887566 37.65990 0.2462125 0.9961428            7
## gene_2 gene_2 0.9480626 0.9807544 76.25004 0.9480626 0.9961428            7
## gene_3 gene_3 0.2966068 0.4136265 51.23973 0.2966068 0.9961428            7
## gene_4 gene_4 0.3033261 0.4136265 54.95003 0.3033261 0.9961428            7
## gene_5 gene_5 0.8078616 0.9289335 43.85998 0.8078616 0.9961428            7
## gene_6 gene_6 0.7163055 0.8953819 23.08000 0.7163055 0.9961428            7
##        df_red_zinb df_full_nb df_red_nb loglik_full_zinb loglik_red_zinb
## gene_1           2          7         2        -416.3913       -419.7273
## gene_2           2          7         2        -532.2083       -532.7915
## gene_3           2          7         2        -439.8704       -442.9204
## gene_4           2          7         2        -432.2398       -435.2547
## gene_5           2          7         2        -421.4772       -422.6218
## gene_6           2          7         2        -379.8961       -381.3432
##        loglik_full_nb loglik_red_nb allZero
## gene_1      -460.3226     -461.2396   FALSE
## gene_2      -532.4752     -533.0584   FALSE
## gene_3      -488.7408     -489.9752   FALSE
## gene_4      -500.6961     -500.8805   FALSE
## gene_5      -474.7200     -475.4183   FALSE
## gene_6      -411.1330     -411.9886   FALSE

In addition to the raw p-values, one may be interested in further details of the expression models such as shape of the expression mean as a function of pseudotime, log fold changes (LFC) and global expression trends as function of pseudotime. We address each of these follow-up questions with separate sections in the following. Note that all of these follow-up questions are answered based on the model that were fit to compute the p-value of differential expression. Therefore, once runLineagePulse() was called once, no further model fitting is required.

# Further inspection of results ## Plot gene-wise trajectories

Multiple options are available for gene-wise expression trajectory plotting: Observations can be coloured by the posterior probability of drop-out (boolColourByDropout). Observations can be normalized based on the alternative expression model or taken as raw observerations for the scatter plot (boolH1NormCounts). Lineage contours can be added to aid visual interpretation of non-uniform population density in pseudotime related effects (boolLineageContour). Log counts can be displayed instead of counts if the fold changes are large (boolLogPlot). In any case, the output object of the gene-wise expression trajectors plotting function plotGene is a ggplot2 object which can then be printed or modified.

# plot the gene with the lowest p-value of differential expression
gplotExprProfile <- plotGene(
objLP = objLP, boolLogPlot = FALSE,
strGeneID = objLP$dfResults[which.min(objLP$dfResults$p),]$gene,
boolLineageContour = FALSE)
gplotExprProfile

The function plotGene also shows the H1 model fit under a negative binomial noise model (“H1(NB)”) as a reference to show what the model fit looks like if drop-out is not accounted for.

2.1 Manual analysis of expression trajectories

LineagePulse provides the user with parameter extraction functions that allow the user to interact directly with the raw model fits for analytic tasks or questions not addressed above.

# extract the mean parameter fits per cell of the gene with the lowest p-value.
matMeanParamFit <- getFitsMean(
    lsMuModel = lsMuModelH1(objLP),
    vecGeneIDs = objLP$dfResults[which.min(objLP$dfResults$p),]$gene)
cat("Minimum fitted mean parameter: ", round(min(matMeanParamFit),1) )
## Minimum fitted mean parameter:  90.1
cat("Mean fitted mean parameter: ", round(mean(matMeanParamFit),1) )
## Mean fitted mean parameter:  231.3

2.2 Fold changes

Given a discrete population structure, such as tSNE cluster or experimental conditions, a fold change is the ratio of the mean expression value of both groups. The definition of a fold change is less clear if a continous expression trajector is considered: Of interest may be for example the fold change from the first to the last cell on the expression trajectory or from the minimum to the maximum expression value. Note that in both cases, we compute fold changes on the model fit of the expression mean parameter which is corrected for noise and therefore more stable than the estimate based on the raw expression count observation.

# first, extract the model fits for a given gene again
vecMeanParamFit <- getFitsMean(
    lsMuModel = lsMuModelH1(objLP),
    vecGeneIDs = objLP$dfResults[which.min(objLP$dfResults$p),]$gene)
# compute log2-fold change from first to last cell on trajectory
idxFirstCell <- which.min(dfAnnotationProc(objLP)$pseudotime)
idxLastCell <- which.max(dfAnnotationProc(objLP)$pseudotime)
cat("LFC first to last cell on trajectory: ",
    round( (log(vecMeanParamFit[idxLastCell]) - 
                log(vecMeanParamFit[idxFirstCell])) / log(2) ,1) )
## LFC first to last cell on trajectory:
# compute log2-fold change from minimum to maximum value of expression trajectory
cat("LFC minimum to maximum expression value of model fit: ", 
    round( (log(max(vecMeanParamFit)) - 
                log(min(vecMeanParamFit))) / log(2),1) )
## LFC minimum to maximum expression value of model fit:  2.1

2.3 Global expression profiles

Global expression profiles or expression profiles across large groups of genes can be visualised via heatmaps of expression z-scores. One could extract the expression mean parameter fits as described above and create such heatmaps from scratch. LineaegePulse also offers a wrapper for creating such a heatmap:

# create heatmap with all differentially expressed genes
lsHeatmaps <- sortGeneTrajectories(
    vecIDs = objLP$dfResults[which(objLP$dfResults$padj < 0.01),]$gene,
    lsMuModel = lsMuModelH1(objLP),
    dirHeatmap=NULL)
print(lsHeatmaps$hmGeneSorted)

3 Session information

sessionInfo()
## R version 4.3.0 RC (2023-04-18 r84287)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Ubuntu 22.04.2 LTS
## 
## Matrix products: default
## BLAS:   /home/biocbuild/bbs-3.18-bioc/R/lib/libRblas.so 
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.10.0
## 
## locale:
##  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=en_GB              LC_COLLATE=C              
##  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
##  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
## 
## time zone: America/New_York
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] LineagePulse_1.21.0 BiocStyle_2.29.0   
## 
## loaded via a namespace (and not attached):
##  [1] tidyselect_1.2.0            dplyr_1.1.2                
##  [3] farver_2.1.1                bitops_1.0-7               
##  [5] fastmap_1.1.1               SingleCellExperiment_1.23.0
##  [7] RCurl_1.98-1.12             digest_0.6.31              
##  [9] lifecycle_1.0.3             cluster_2.1.4              
## [11] Cairo_1.6-0                 magrittr_2.0.3             
## [13] compiler_4.3.0              rlang_1.1.0                
## [15] sass_0.4.5                  tools_4.3.0                
## [17] utf8_1.2.3                  yaml_2.3.7                 
## [19] knitr_1.42                  labeling_0.4.2             
## [21] DelayedArray_0.27.0         RColorBrewer_1.1-3         
## [23] BiocParallel_1.35.0         KernSmooth_2.23-20         
## [25] withr_2.5.0                 BiocGenerics_0.47.0        
## [27] grid_4.3.0                  stats4_4.3.0               
## [29] fansi_1.0.4                 caTools_1.18.2             
## [31] colorspace_2.1-0            ggplot2_3.4.2              
## [33] scales_1.2.1                gtools_3.9.4               
## [35] iterators_1.0.14            SummarizedExperiment_1.31.0
## [37] cli_3.6.1                   rmarkdown_2.21             
## [39] crayon_1.5.2                generics_0.1.3             
## [41] rjson_0.2.21                cachem_1.0.7               
## [43] zlibbioc_1.47.0             splines_4.3.0              
## [45] parallel_4.3.0              BiocManager_1.30.20        
## [47] XVector_0.41.0              matrixStats_0.63.0         
## [49] vctrs_0.6.2                 Matrix_1.5-4               
## [51] jsonlite_1.8.4              bookdown_0.33              
## [53] IRanges_2.35.0              GetoptLong_1.0.5           
## [55] S4Vectors_0.39.0            clue_0.3-64                
## [57] magick_2.7.4                foreach_1.5.2              
## [59] jquerylib_0.1.4             glue_1.6.2                 
## [61] codetools_0.2-19            shape_1.4.6                
## [63] gtable_0.3.3                GenomeInfoDb_1.37.0        
## [65] GenomicRanges_1.53.0        ComplexHeatmap_2.17.0      
## [67] munsell_0.5.0               tibble_3.2.1               
## [69] pillar_1.9.0                htmltools_0.5.5            
## [71] gplots_3.1.3                GenomeInfoDbData_1.2.10    
## [73] circlize_0.4.15             R6_2.5.1                   
## [75] doParallel_1.0.17           evaluate_0.20              
## [77] lattice_0.21-8              Biobase_2.61.0             
## [79] highr_0.10                  png_0.1-8                  
## [81] bslib_0.4.2                 Rcpp_1.0.10                
## [83] xfun_0.39                   MatrixGenerics_1.13.0      
## [85] pkgconfig_2.0.3             GlobalOptions_0.1.2