Dimensionality reduction and batch effect removal using NewWave

Installation

First of all we need to install NewWave:

if(!requireNamespace("BiocManager", quietly = TRUE))
    install.packages("BiocManager")
BiocManager::install("NewWave")
suppressPackageStartupMessages(
  {library(SingleCellExperiment)
library(splatter)
library(irlba)
library(Rtsne)
library(ggplot2)
library(mclust)
library(NewWave)}
)

Introduction

NewWave is a new package that assumes a Negative Binomial distributions for dimensionality reduction and batch effect removal. In order to reduce the memory consumption it uses a PSOCK cluster combined with the R package SharedObject that allow to share a matrix between different cores without memory duplication. Thanks to that we can massively parallelize the estimation process with huge benefit in terms of time consumption. We can reduce even more the time consumption using some minibatch approaches on the different steps of the optimization.

I am going to show how to use NewWave with example data generated with Splatter.

params <- newSplatParams()
N=500
set.seed(1234)
data <- splatSimulateGroups(params,batchCells=c(N/2,N/2),
                           group.prob = rep(0.1,10),
                           de.prob = 0.2,
                           verbose = FALSE) 

Now we have a dataset with 500 cells and 10000 genes, I will use only the 500 most variable genes. NewWave takes as input raw data, not normalized.

set.seed(12359)
hvg <- rowVars(counts(data))
names(hvg) <- rownames(counts(data))
data <- data[names(sort(hvg,decreasing=TRUE))[1:500],]

As you can see there is a variable called batch in the colData section.

colData(data)
#> DataFrame with 500 rows and 4 columns
#>                Cell       Batch    Group ExpLibSize
#>         <character> <character> <factor>  <numeric>
#> Cell1         Cell1      Batch1   Group6    53137.4
#> Cell2         Cell2      Batch1   Group6    54886.9
#> Cell3         Cell3      Batch1   Group1    53528.5
#> Cell4         Cell4      Batch1   Group4    39425.3
#> Cell5         Cell5      Batch1   Group5    56190.6
#> ...             ...         ...      ...        ...
#> Cell496     Cell496      Batch2  Group1     62124.6
#> Cell497     Cell497      Batch2  Group5     64474.8
#> Cell498     Cell498      Batch2  Group1     76160.7
#> Cell499     Cell499      Batch2  Group6     58216.5
#> Cell500     Cell500      Batch2  Group10    76840.7

IMPORTANT: For batch effecr removal the batch variable must be a factor

data$Batch <- as.factor(data$Batch)

We also have a variable called Group that represent the cell type labels.

We can see the how the cells are distributed between group and batch

pca <- prcomp_irlba(t(counts(data)),n=10)
plot_data <-data.frame(Rtsne(pca$x)$Y)
plot_data$batch <- data$Batch
plot_data$group <- data$Group
ggplot(plot_data, aes(x=X1,y=X2,col=group, shape=batch))+ geom_point()

There is a clear batch effect between the cells.

Let’s try to correct it.

NewWave

I am going to show different implementation and the suggested way to use them with the given hardware.

Some advise:

  • Verbose option has default FALSE, in this vignette I will change it for explanatory intentions, don’t do it with big dataset because it can sensibly slower the computation
  • There are no concern about the dimension of mini-batches, I always used the 10% of the observations

Standard usage

This is the way to insert the batch variable, in the same manner can be inserted other cell-related variable and if you need some gene related variable those can be inserted in V.

res <- newWave(data,X = "~Batch", K=10, verbose = TRUE)
#> Time of setup
#>    user  system elapsed 
#>   0.008   0.000   0.307 
#> Time of initialization
#>    user  system elapsed 
#>   0.042   0.008   0.424
#> Iteration 1
#> penalized log-likelihood = -1299311.81955987
#> Time of dispersion optimization
#>    user  system elapsed 
#>   0.488   0.040   0.476
#> after optimize dispersion = -1060081.47902202
#> Time of right optimization
#>    user  system elapsed 
#>   0.000   0.000   4.478
#> after right optimization= -1059356.42663925
#> after orthogonalization = -1059356.4081748
#> Time of left optimization
#>    user  system elapsed 
#>   0.022   0.151   4.141
#> after left optimization= -1059119.83902116
#> after orthogonalization = -1059119.83749642
#> Iteration 2
#> penalized log-likelihood = -1059119.83749642
#> Time of dispersion optimization
#>    user  system elapsed 
#>   0.588   0.271   0.528
#> after optimize dispersion = -1059114.70973843
#> Time of right optimization
#>    user  system elapsed 
#>   0.001   0.000   3.729
#> after right optimization= -1059090.51538885
#> after orthogonalization = -1059090.51375732
#> Time of left optimization
#>    user  system elapsed 
#>   0.033   0.149   3.027
#> after left optimization= -1059081.78171917
#> after orthogonalization = -1059081.78168529

In order to make it faster you can increase the number of cores using “children” parameter:

res2 <- newWave(data,X = "~Batch", K=10, verbose = TRUE, children=2)
#> Time of setup
#>    user  system elapsed 
#>   0.004   0.004   3.306 
#> Time of initialization
#>    user  system elapsed 
#>   0.025   0.016   0.388
#> Iteration 1
#> penalized log-likelihood = -1299311.81957527
#> Time of dispersion optimization
#>    user  system elapsed 
#>   0.462   0.060   0.467
#> after optimize dispersion = -1060081.47793163
#> Time of right optimization
#>    user  system elapsed 
#>   0.001   0.000   2.297
#> after right optimization= -1059356.42355383
#> after orthogonalization = -1059356.40509602
#> Time of left optimization
#>    user  system elapsed 
#>   0.013   0.121   2.100
#> after left optimization= -1059119.83783846
#> after orthogonalization = -1059119.83630154
#> Iteration 2
#> penalized log-likelihood = -1059119.83630154
#> Time of dispersion optimization
#>    user  system elapsed 
#>   0.601   0.259   0.526
#> after optimize dispersion = -1059114.70823989
#> Time of right optimization
#>    user  system elapsed 
#>   0.000   0.000   1.854
#> after right optimization= -1059090.49895145
#> after orthogonalization = -1059090.49732985
#> Time of left optimization
#>    user  system elapsed 
#>   0.033   0.117   1.616
#> after left optimization= -1059081.73611584
#> after orthogonalization = -1059081.73608243

Commonwise dispersion and minibatch approaches

If you do not have an high number of cores to run newWave this is the fastest way to run. The optimization process is done by three process itereated until convercence.

  • Optimization of the dispersion parameters
  • Optimization of the gene related parameters
  • Optimization of the cell related parameters

Each of these three steps can be accelerated using mini batch, the number of observation is settled with these parameters:

  • n_gene_disp : Number of genes to use in the dispersion optimization
  • n_cell_par : Number of cells to use in the cells related parameters optimization
  • n_gene_par : Number of genes to use in the genes related parameters optimization
res3 <- newWave(data,X = "~Batch", verbose = TRUE,K=10, children=2,
                n_gene_disp = 100, n_gene_par = 100, n_cell_par = 100)
#> Time of setup
#>    user  system elapsed 
#>   0.008   0.001   0.318 
#> Time of initialization
#>    user  system elapsed 
#>   0.035   0.002   0.393
#> Iteration 1
#> penalized log-likelihood = -1299311.81973691
#> Time of dispersion optimization
#>    user  system elapsed 
#>   0.485   0.048   0.472
#> after optimize dispersion = -1060081.47941011
#> Time of right optimization
#>    user  system elapsed 
#>   0.001   0.000   2.307
#> after right optimization= -1059356.41433338
#> after orthogonalization = -1059356.39588769
#> Time of left optimization
#>    user  system elapsed 
#>   0.021   0.130   2.096
#> after left optimization= -1059119.83477829
#> after orthogonalization = -1059119.83325776
#> Iteration 2
#> penalized log-likelihood = -1059119.83325776
#> Time of dispersion optimization
#>    user  system elapsed 
#>   0.235   0.297   0.198
#> after optimize dispersion = -1059119.83325776
#> Time of right optimization
#>    user  system elapsed 
#>   0.001   0.000   0.374
#> after right optimization= -1059115.48091595
#> after orthogonalization = -1059115.48041529
#> Time of left optimization
#>    user  system elapsed 
#>   0.025   0.118   0.230
#> after left optimization= -1059115.30909742
#> after orthogonalization = -1059115.30907801

Genewise dispersion mini-batch

If you have a lot of core disposable or you want to estimate a genewise dispersion parameter this is the fastes configuration:

res3 <- newWave(data,X = "~Batch", verbose = TRUE,K=10, children=2,
                n_gene_par = 100, n_cell_par = 100, commondispersion = FALSE)
#> Time of setup
#>    user  system elapsed 
#>   0.008   0.000   0.305 
#> Time of initialization
#>    user  system elapsed 
#>   0.035   0.031   0.384
#> Iteration 1
#> penalized log-likelihood = -1299311.81953365
#> Time of dispersion optimization
#>    user  system elapsed 
#>   0.469   0.050   0.467
#> after optimize dispersion = -1060081.47915702
#> Time of right optimization
#>    user  system elapsed 
#>   0.001   0.000   2.287
#> after right optimization= -1059356.41268146
#> after orthogonalization = -1059356.39422696
#> Time of left optimization
#>    user  system elapsed 
#>   0.003   0.084   2.042
#> after left optimization= -1059119.83638921
#> after orthogonalization = -1059119.83486031
#> Iteration 2
#> penalized log-likelihood = -1059119.83486031
#> Time of dispersion optimization
#>    user  system elapsed 
#>   0.098   0.256   0.403
#> after optimize dispersion = -1055831.44916828
#> Time of right optimization
#>    user  system elapsed 
#>   0.000   0.000   0.385
#> after right optimization= -1055827.00313058
#> after orthogonalization = -1055827.00270835
#> Time of left optimization
#>    user  system elapsed 
#>   0.015   0.133   0.466
#> after left optimization= -1055799.34721882
#> after orthogonalization = -1055799.34648337
#> Iteration 3
#> penalized log-likelihood = -1055799.34648337
#> Time of dispersion optimization
#>    user  system elapsed 
#>   0.104   0.245   0.202
#> after optimize dispersion = -1055799.35297686
#> Time of right optimization
#>    user  system elapsed 
#>   0.001   0.000   0.378
#> after right optimization= -1055795.65965097
#> after orthogonalization = -1055795.65939319
#> Time of left optimization
#>    user  system elapsed 
#>   0.034   0.097   0.447
#> after left optimization= -1055768.76193191
#> after orthogonalization = -1055768.76111416

NB: do not use n_gene_disp in this case, it will slower the computation.

Now I can use the latent dimension rapresentation for visualization purpose:

latent <- reducedDim(res)

tsne_latent <- data.frame(Rtsne(latent)$Y)
tsne_latent$batch <- data$Batch
tsne_latent$group <- data$Group
ggplot(tsne_latent, aes(x=X1,y=X2,col=group, shape=batch))+ geom_point()

or for clustering:

cluster <- kmeans(latent, 10)

adjustedRandIndex(cluster$cluster, data$Group)
#> [1] 0.8750416

Session Information

sessionInfo()
#> R version 4.4.1 (2024-06-14)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.1 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        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: Etc/UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats4    stats     graphics  grDevices utils     datasets  methods  
#> [8] base     
#> 
#> other attached packages:
#>  [1] NewWave_1.15.0              mclust_6.1.1               
#>  [3] ggplot2_3.5.1               Rtsne_0.17                 
#>  [5] irlba_2.3.5.1               Matrix_1.7-0               
#>  [7] splatter_1.29.0             SingleCellExperiment_1.27.2
#>  [9] SummarizedExperiment_1.35.1 Biobase_2.65.1             
#> [11] GenomicRanges_1.57.1        GenomeInfoDb_1.41.1        
#> [13] IRanges_2.39.2              S4Vectors_0.43.2           
#> [15] BiocGenerics_0.51.1         MatrixGenerics_1.17.0      
#> [17] matrixStats_1.4.0           rmarkdown_2.28             
#> 
#> loaded via a namespace (and not attached):
#>  [1] gtable_0.3.5            xfun_0.47               bslib_0.8.0            
#>  [4] lattice_0.22-6          vctrs_0.6.5             tools_4.4.1            
#>  [7] parallel_4.4.1          tibble_3.2.1            fansi_1.0.6            
#> [10] highr_0.11              pkgconfig_2.0.3         SharedObject_1.19.1    
#> [13] checkmate_2.3.2         lifecycle_1.0.4         GenomeInfoDbData_1.2.12
#> [16] farver_2.1.2            compiler_4.4.1          munsell_0.5.1          
#> [19] codetools_0.2-20        htmltools_0.5.8.1       sys_3.4.2              
#> [22] buildtools_1.0.0        sass_0.4.9              yaml_2.3.10            
#> [25] pillar_1.9.0            crayon_1.5.3            jquerylib_0.1.4        
#> [28] BiocParallel_1.39.0     DelayedArray_0.31.11    cachem_1.1.0           
#> [31] abind_1.4-5             rsvd_1.0.5              locfit_1.5-9.10        
#> [34] digest_0.6.37           BiocSingular_1.21.2     labeling_0.4.3         
#> [37] maketools_1.3.0         fastmap_1.2.0           grid_4.4.1             
#> [40] colorspace_2.1-1        cli_3.6.3               SparseArray_1.5.31     
#> [43] magrittr_2.0.3          S4Arrays_1.5.7          utf8_1.2.4             
#> [46] withr_3.0.1             UCSC.utils_1.1.0        scales_1.3.0           
#> [49] backports_1.5.0         XVector_0.45.0          httr_1.4.7             
#> [52] beachmat_2.21.6         ScaledMatrix_1.13.0     evaluate_0.24.0        
#> [55] knitr_1.48              rlang_1.1.4             Rcpp_1.0.13            
#> [58] glue_1.7.0              jsonlite_1.8.8          R6_2.5.1               
#> [61] zlibbioc_1.51.1