LineagePulse 1.14.0

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:

- counts the count matrix (genes x cells) which MUST NOT be normalised in any way. A valid input option is expected counts from an aligner. Note that TPM or depth normalised expected counts are NOT count data. The statistical framework of LineagePulse rests on the assumption that matCounts contain count data. The count matrix can also be supplied as the path to a .mtx file (sparse matrix format file) or as a SummarizedExperiment or SingleCellExperiment object.
- dfAnnotation data frame that contains cell-wise annotation. The rownames of dfAnnotation must be equal to the column names of matCounts as both correspond to cells. Note that if counts is a SummarizedExperiment or SingleCellExperiment object, the annotation data frame is taken to be colData(counts) if dfAnnotation is NULL. dfAnnotation must contain
- a column named “continuous” if a continuous model is fit (e.g. strMuModel is impulse or splines and expression is fit as a function of time or pseudotime coordinates)
- a column named “groups” if a discrete population model is fit (e.g. strMuModel is groups and expression is fit as a function of group assignment, e.g. clusters or experimental conditions) that contains the assignemnt of cells to these groups as strings
- columns that describe the batch structure (if any).

Additionally, one can provide:

- matPiConstPredictors a matrix of gene-specific predictors of the drop-out rate (genes x predictors). We suggest to use the log average expression (unless strDropModel is “logistic_ofMu”) and potentially parameters which may affect sequencing efficiency such as GC content of the gene.
- strMuModel the type of expression model to use as an alternative model for differential expression analysis: “impulse” for an impulse model and “splines” for a natural cubic spline model.
- vecConfoundersMu a vector of strings which corresond to column names in dfAnnotation which describe the batch structure to be corrected for.
- scaDFSplinesMu the degrees of freedom of the spline-based model for the mean parameter if strMuModel was set to “splines”.
- vecNormConstExternal cell-wise normalisation constants to be used (e.g. sequencing depth correction factors). the names of the elements have to be the column names of matCounts (cells).
- scaNProc to set the number of processes for parallelization.
- boolVerbose output basic progress reports while the wrapper functions runs.
- boolSuperVerbose output detailed progress reports for each step of the wrapper function.

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:

- vecConfoundersDisp batch variables to be used to correct the dispersion (variance).
- strDispModelFull the dispersion model to be used for the full model.
- strDispModelRed the dispersion model to be used for the reduced model.
- strDropModel the drop-out model to be used.
- strDropFitGroup the groups of cells which receive one parameterisation of the drop-out model.
- scaDFSplinesDisp the degrees of freedom of the spline-based model for the dispersion parameter if strDispModel was set to “splines”.
- boolEstimateNoiseBasedOnH0 whether to estimate the drop-out model on the null or alternative expression model. Note that setting this to FALSE strongly increases the run time.
- scaMaxEstimationCycles maximum number of drop-out and expression model estimation iteration cycles.

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.14.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 -24256.3599006978`

`## # 1. Iteration with ll -12371.426436406 in 0.03 min.`

`## # 2. Iteration with ll -12222.9139682648 in 0.03 min.`

`## # 3. Iteration with ll -12222.9128581604 in 0.03 min.`

`## Finished fitting zero-inflated negative binomial model A with noise model in 0.12 min.`

`## ### b) Fit ZINB model B (H1: mu=splines disp=constant).`

`## # . Initialisation: ll -13686.679917408`

`## # 1. Iteration with ll -11909.4252552264 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 -13526.6326200418`

`## # 1. Iteration with ll -13303.3083570498 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 -13710.4978395483`

`## # 1. Iteration with ll -13201.5070073055 in 0.02 min.`

`## Finished fitting NB model B in 0.03 min.`

`## Time elapsed during ZINB fitting: 0.24 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.47270981 0.59088726 9.610004 0.47270981 0.999735 7
## gene_2 gene_2 1.00000000 1.00000000 53.080629 1.00000000 0.999735 7
## gene_3 gene_3 0.04666243 0.08234547 50.960003 0.04666243 0.999735 7
## gene_4 gene_4 0.03029639 0.05680574 64.990296 0.03029639 0.999735 7
## gene_5 gene_5 0.54263312 0.62611513 37.639903 0.54263312 0.999735 7
## gene_6 gene_6 0.89401874 0.95787722 6.520042 0.89401874 0.999735 7
## df_red_zinb df_full_nb df_red_nb loglik_full_zinb loglik_red_zinb
## gene_1 2 7 2 -315.5329 -317.8098
## gene_2 2 7 2 -490.0873 -447.0440
## gene_3 2 7 2 -430.5354 -436.1599
## gene_4 2 7 2 -454.6326 -460.8075
## gene_5 2 7 2 -407.8062 -409.8299
## gene_6 2 7 2 -285.0177 -285.8472
## loglik_full_nb loglik_red_nb allZero
## gene_1 -315.7183 -317.9924 FALSE
## gene_2 -496.7215 -498.1108 FALSE
## gene_3 -491.2557 -492.3844 FALSE
## gene_4 -515.4270 -516.1782 FALSE
## gene_5 -453.2331 -454.3243 FALSE
## gene_6 -284.7280 -285.5322 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.

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: 15.3`

`cat("Mean fitted mean parameter: ", round(mean(matMeanParamFit),1) )`

`## Mean fitted mean parameter: 34.6`

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: 1.9`

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)
```

`sessionInfo()`

```
## R version 4.1.1 (2021-08-10)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Ubuntu 20.04.3 LTS
##
## Matrix products: default
## BLAS: /home/biocbuild/bbs-3.14-bioc/R/lib/libRblas.so
## LAPACK: /home/biocbuild/bbs-3.14-bioc/R/lib/libRlapack.so
##
## 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
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] LineagePulse_1.14.0 BiocStyle_2.22.0
##
## loaded via a namespace (and not attached):
## [1] MatrixGenerics_1.6.0 Biobase_2.54.0
## [3] sass_0.4.0 splines_4.1.1
## [5] jsonlite_1.7.2 foreach_1.5.1
## [7] gtools_3.9.2 bslib_0.3.1
## [9] assertthat_0.2.1 highr_0.9
## [11] BiocManager_1.30.16 stats4_4.1.1
## [13] GenomeInfoDbData_1.2.7 yaml_2.2.1
## [15] pillar_1.6.4 lattice_0.20-45
## [17] glue_1.4.2 digest_0.6.28
## [19] GenomicRanges_1.46.0 RColorBrewer_1.1-2
## [21] XVector_0.34.0 colorspace_2.0-2
## [23] htmltools_0.5.2 Matrix_1.3-4
## [25] pkgconfig_2.0.3 GetoptLong_1.0.5
## [27] magick_2.7.3 bookdown_0.24
## [29] zlibbioc_1.40.0 purrr_0.3.4
## [31] scales_1.1.1 BiocParallel_1.28.0
## [33] tibble_3.1.5 farver_2.1.0
## [35] generics_0.1.1 IRanges_2.28.0
## [37] ggplot2_3.3.5 ellipsis_0.3.2
## [39] SummarizedExperiment_1.24.0 BiocGenerics_0.40.0
## [41] magrittr_2.0.1 crayon_1.4.1
## [43] evaluate_0.14 fansi_0.5.0
## [45] gplots_3.1.1 doParallel_1.0.16
## [47] tools_4.1.1 GlobalOptions_0.1.2
## [49] lifecycle_1.0.1 matrixStats_0.61.0
## [51] ComplexHeatmap_2.10.0 stringr_1.4.0
## [53] S4Vectors_0.32.0 munsell_0.5.0
## [55] cluster_2.1.2 DelayedArray_0.20.0
## [57] compiler_4.1.1 jquerylib_0.1.4
## [59] GenomeInfoDb_1.30.0 caTools_1.18.2
## [61] rlang_0.4.12 grid_4.1.1
## [63] RCurl_1.98-1.5 iterators_1.0.13
## [65] rjson_0.2.20 SingleCellExperiment_1.16.0
## [67] circlize_0.4.13 labeling_0.4.2
## [69] bitops_1.0-7 rmarkdown_2.11
## [71] gtable_0.3.0 codetools_0.2-18
## [73] DBI_1.1.1 R6_2.5.1
## [75] knitr_1.36 dplyr_1.0.7
## [77] fastmap_1.1.0 utf8_1.2.2
## [79] clue_0.3-60 KernSmooth_2.23-20
## [81] shape_1.4.6 stringi_1.7.5
## [83] Rcpp_1.0.7 parallel_4.1.1
## [85] vctrs_0.3.8 png_0.1-7
## [87] tidyselect_1.1.1 xfun_0.27
```