11  Lab 6: Batch correction

Notes

The estimated time for this lab is around 1h20.

Aims
  • Process pancreas scRNAseq datasets from different technologies.
  • Merge the datasets and analyse them without batch correction.
  • Use Seurat to perform batch correction using canonical correlation analysis (CCA) and mutual nearest neighbors (MNN).
  • Use LIGER to perform batch correction using integrative non-negative matrix factorization.

In this lab, we will look at different single cell RNA-seq datasets collected from pancreatic islets. We will look at how different batch correction methods affect our data analysis.

11.1 Read in pancreas expression matrices

Four different datasets are provided in the ~/Share/batch_correction/ directory. These datasets were collected using different single cell RNA-seq technologies.

Question

Import the four datasets into R. What is the size and sparsity of each dataset?

R
celseq.data <- round(read.table("~/Share/batch_correction/pancreas_multi_celseq_expression_matrix.txt.gz"))
celseq2.data <- round(read.table("~/Share/batch_correction/pancreas_multi_celseq2_expression_matrix.txt.gz"))
fluidigmc1.data <- round(read.table("~/Share/batch_correction/pancreas_multi_fluidigmc1_expression_matrix.txt.gz"))
smartseq2.data <- round(read.table("~/Share/batch_correction/pancreas_multi_smartseq2_expression_matrix.txt.gz"))

11.2 Analyze each pancreas dataset without batch correction

We will first analyze each dataset separately to see if there are any differences between the datasets.

Question

What is the size of each single cell RNA-seq dataset?

Briefly describe the technology used to collect each dataset.

Which datasets do you expect to be different and which do you expect to be similar?

R
dim(celseq.data)
[1] 20148  1728
dim(celseq2.data)
[1] 19140  3072
dim(fluidigmc1.data)
[1] 25463   638
dim(smartseq2.data)
[1] 26179  3514
Question

Create a Seurat object for each dataset, and look at the distributions of number of genes per cell.

R
library(SingleCellExperiment)
Loading required package: SummarizedExperiment
Loading required package: MatrixGenerics
Loading required package: matrixStats

Attaching package: 'MatrixGenerics'
The following objects are masked from 'package:matrixStats':

    colAlls, colAnyNAs, colAnys, colAvgsPerRowSet, colCollapse, colCounts, colCummaxs, colCummins, colCumprods, colCumsums, colDiffs, colIQRDiffs, colIQRs, colLogSumExps, colMadDiffs, colMads, colMaxs, colMeans2,
    colMedians, colMins, colOrderStats, colProds, colQuantiles, colRanges, colRanks, colSdDiffs, colSds, colSums2, colTabulates, colVarDiffs, colVars, colWeightedMads, colWeightedMeans, colWeightedMedians,
    colWeightedSds, colWeightedVars, rowAlls, rowAnyNAs, rowAnys, rowAvgsPerColSet, rowCollapse, rowCounts, rowCummaxs, rowCummins, rowCumprods, rowCumsums, rowDiffs, rowIQRDiffs, rowIQRs, rowLogSumExps,
    rowMadDiffs, rowMads, rowMaxs, rowMeans2, rowMedians, rowMins, rowOrderStats, rowProds, rowQuantiles, rowRanges, rowRanks, rowSdDiffs, rowSds, rowSums2, rowTabulates, rowVarDiffs, rowVars, rowWeightedMads,
    rowWeightedMeans, rowWeightedMedians, rowWeightedSds, rowWeightedVars
Loading required package: GenomicRanges
Loading required package: stats4
Loading required package: BiocGenerics
Loading required package: generics

Attaching package: 'generics'
The following objects are masked from 'package:base':

    as.difftime, as.factor, as.ordered, intersect, is.element, setdiff, setequal, union

Attaching package: 'BiocGenerics'
The following objects are masked from 'package:stats':

    IQR, mad, sd, var, xtabs
The following objects are masked from 'package:base':

    anyDuplicated, aperm, append, as.data.frame, basename, cbind, colnames, dirname, do.call, duplicated, eval, evalq, Filter, Find, get, grep, grepl, is.unsorted, lapply, Map, mapply, match, mget, order, paste,
    pmax, pmax.int, pmin, pmin.int, Position, rank, rbind, Reduce, rownames, sapply, saveRDS, table, tapply, unique, unsplit, which.max, which.min
Loading required package: S4Vectors

Attaching package: 'S4Vectors'
The following object is masked from 'package:utils':

    findMatches
The following objects are masked from 'package:base':

    expand.grid, I, unname
Loading required package: IRanges
Loading required package: Seqinfo
Loading required package: Biobase
Welcome to Bioconductor

    Vignettes contain introductory material; view with 'browseVignettes()'. To cite Bioconductor, see 'citation("Biobase")', and for packages 'citation("pkgname")'.

Attaching package: 'Biobase'
The following object is masked from 'package:MatrixGenerics':

    rowMedians
The following objects are masked from 'package:matrixStats':

    anyMissing, rowMedians
library(ggplot2)

celseq_raw <- SingleCellExperiment(assay = list(counts = celseq.data)) |> scuttle::quickPerCellQC()
celseq2_raw <- SingleCellExperiment(assay = list(counts = celseq2.data)) |> scuttle::quickPerCellQC()
fluidigmc1_raw <- SingleCellExperiment(assay = list(counts = fluidigmc1.data)) |> scuttle::quickPerCellQC()
smartseq2_raw <- SingleCellExperiment(assay = list(counts = smartseq2.data)) |> scuttle::quickPerCellQC()
celseq_raw$tech <- "celseq"
celseq2_raw$tech <- "celseq2"
fluidigmc1_raw$tech <- "fluidigmc1"
smartseq2_raw$tech <- "smartseq2"

df <- rbind(
    data.frame(tech = celseq_raw$tech, nGenes = celseq_raw$detected, totalUMIs = celseq_raw$sum),
    data.frame(tech = celseq2_raw$tech, nGenes = celseq2_raw$detected, totalUMIs = celseq2_raw$sum),
    data.frame(tech = fluidigmc1_raw$tech, nGenes = fluidigmc1_raw$detected, totalUMIs = fluidigmc1_raw$sum),
    data.frame(tech = smartseq2_raw$tech, nGenes = smartseq2_raw$detected, totalUMIs = smartseq2_raw$sum) 
)

ggplot(df, aes(x = tech, y = nGenes)) +
    geom_violin(trim = TRUE) +
    geom_boxplot(width = 0.1, position = position_dodge(0.9), outlier.shape = NA) +
    theme_minimal() +
    ylab("Number of genes detected per cell") +
    xlab("Technology")

ggplot(df, aes(x = tech, y = log10(totalUMIs))) +
    geom_violin(trim = TRUE) +
    geom_boxplot(width = 0.1, position = position_dodge(0.9), outlier.shape = NA) +
    theme_minimal() +
    ylab("Total number of UMIs detected per cell") +
    xlab("Technology")

Now we will subset the data to remove cells with low gene counts.

Question

Subset the data to remove cells with:

  • fewer than 1750 genes for CEL-Seq;
  • fewer than 2500 genes for CEL-Seq2;
  • fewer than 2500 genes for SMART-Seq2.
R
celseq <- celseq_raw[ , celseq_raw$detected > 1750 ]
celseq2 <- celseq2_raw[ , celseq2_raw$detected > 2500 ]
smartseq2 <- smartseq2_raw[ , smartseq2_raw$detected > 2500 ]
fluidigmc1 <- fluidigmc1_raw

Now we will subsample each dataset to only have 500 cells. This is to speed up subsequent analyses. Often when we are setting up our analysis, we work with a subset of the data to make iteration across analysis decisions faster, and once we have finalized how we want to do the analysis, we work with the full dataset.

Question
  • Within metadata dataframe, save what technology each dataset was generated with.
  • Randomly subsample each full dataset to 500 cells.
R
celseq <- celseq[ , sample(colnames(celseq), 500) ]
celseq2 <- celseq2[ , sample(colnames(celseq2), 500) ]
fluidigmc1 <- fluidigmc1[ , sample(colnames(fluidigmc1), 500) ]
smartseq2 <- smartseq2[ , sample(colnames(smartseq2), 500) ]

Now we will subset each dataset to only retain the common genes across all four datasets.

Question
  • Using Reduce, find the common genes across all four datasets.
  • Subset each dataset to only retain these common genes.
R
common_genes <- Reduce(intersect, list(
    rownames(celseq),
    rownames(celseq2),
    rownames(fluidigmc1),
    rownames(smartseq2)
))

celseq <- celseq[common_genes, ]
celseq2 <- celseq2[common_genes, ]
fluidigmc1 <- fluidigmc1[common_genes, ]
smartseq2 <- smartseq2[common_genes, ]

11.3 Individual processing

Like what we have done in previous labs, we will now normalize, identify variable genes, and perform PCA embedding.

R
process_batch <- function(sce) {
    clusters <- scran::quickCluster(sce)
    sce <- scran::computeSumFactors(sce, clusters = clusters)
    sce <- scuttle::logNormCounts(sce)
    dec <- scran::modelGeneVar(sce)
    hvgs <- scran::getTopHVGs(dec, n = 2000)
    rowData(sce)$highly_variable <- rownames(sce) %in% hvgs
    sce <- scater::runPCA(sce, subset_row = hvgs, ncomponents = 50)
    return(sce)
}
celseq <- process_batch(celseq)
celseq2 <- process_batch(celseq2)
fluidigmc1 <- process_batch(fluidigmc1)
smartseq2 <- process_batch(smartseq2)

11.4 Batch correction with MNN

We will now use MNN to see to what extent it can remove potential batch effects. The integration workflow for SingleCellExperiment objects requires the identification of shared variable genes.

Why would we need such a requirement?

Question
R
shared_hvgs <- Reduce(intersect, list(
    rownames(celseq)[rowData(celseq)$highly_variable],
    rownames(celseq2)[rowData(celseq2)$highly_variable],
    rownames(fluidigmc1)[rowData(fluidigmc1)$highly_variable],
    rownames(smartseq2)[rowData(smartseq2)$highly_variable]
))

length(shared_hvgs)
[1] 397

Now that the shared set of highly variable genes has been identified, we can perform MNN batch correction.

Question
  • Use the fastMNN function to perform MNN batch correction across the four datasets.
R
library(batchelor)

merged_mnn <- fastMNN(
    celseq,
    celseq2,
    fluidigmc1,
    smartseq2,
    batch = c(celseq$tech, celseq2$tech, fluidigmc1$tech, smartseq2$tech),
    subset.row = shared_hvgs,
    d = 50,
    correct.all = TRUE
)

merged_mnn
class: SingleCellExperiment 
dim: 15731 2000 
metadata(2): merge.info pca.info
assays(1): reconstructed
rownames(15731): A1BG A1CF ... ZZEF1 ZZZ3
rowData names(1): rotation
colnames(2000): D3ex_6 D3en1_30 ... HP1525301T2D_G15 HP1502401_K15
colData names(1): batch
reducedDimNames(1): corrected
mainExpName: NULL
altExpNames(0):
assay(merged_mnn, "reconstructed")[1:5, 1:5]
<5 x 5> LowRankMatrix object of type "double":
              D3ex_6      D3en1_30    D17TGFB_81      D1713_49        D71_55
A1BG   -1.880365e-02 -1.752763e-02 -1.950988e-02 -2.263767e-02  9.125304e-03
A1CF   -1.597865e-02 -2.145396e-02 -2.654471e-02 -2.456507e-02 -2.493597e-02
A2ML1  -2.379921e-03 -5.210582e-03 -4.799030e-03 -3.080544e-03 -3.325405e-03
A2M    -2.335084e-03 -1.327261e-02 -1.107461e-02 -1.785528e-03 -1.765205e-03
A4GALT -3.074631e-03 -2.516310e-03 -8.735587e-05  8.094972e-05 -3.008913e-03
dim(assay(merged_mnn, "reconstructed"))
[1] 15731  2000
range(assay(merged_mnn, "reconstructed"))
[1] -0.1184722  0.2827918
mean(assay(merged_mnn, "reconstructed"))
[1] -0.005222021
  • Add back the raw counts from each dataset to the merged object for downstream analysis.
R
assay(merged_mnn, "counts") <- cbind(
    assay(celseq, "counts"),
    assay(celseq2, "counts"),
    assay(fluidigmc1, "counts"),
    assay(smartseq2, "counts")
)

Let’s look at how well the batches have mixed after MNN correction.

Loading required package: scuttle
merged_mnn <- scater::runUMAP(merged_mnn, dimred = "corrected", n_neighbors = 15, min_dist = 0.5)
plotUMAP(merged_mnn, colour_by = "batch")

11.5 Batch correction: integrative non-negative matrix factorization (NMF)

Here we use integrative non-negative matrix factorization (NMF/LIGER) to see to what extent it can remove potential batch effects.

The important parameters in the batch correction are the number of factors (k), the penalty parameter (lambda), and the clustering resolution. The number of factors sets the number of factors (consisting of shared and dataset-specific factors) used in factorizing the matrix. The penalty parameter sets the balance between factors shared across the batches and factors specific to the individual batches. The default setting of lambda=5.0 is usually used by the Macosko lab. Resolution=1.0 is used in the Louvain clustering of the shared neighbor factors that have been quantile normalized.

R
ob.list <- list("celseq" = celseq_raw, "celseq2" = celseq2_raw, "fluidigmc1" = fluidigmc1_raw, "smartseq2" = smartseq2_raw)

# Create a LIGER object with raw counts data from each batch.
library(rliger)
data.liger <- createLiger(lapply(ob.list, function(x) as.matrix(assay(x, "counts"))))

# Normalize gene expression for each batch.
data.liger <- rliger::normalize(data.liger)

# Find variable genes for LIGER analysis. Identify variable genes that are variable across most samples.
data.liger <- selectGenes(data.liger)
Question

Scale the gene expression across the datasets.

R
data.liger <- scaleNotCenter(data.liger)

Next, we will run the integrative non-negative matrix factorization.

Question

Use the runIntegration function to perform the integrative non-negative matrix factorization.

R
data.liger <- runIntegration(data.liger, k = 30)

What do matrices H, V, and W represent, and what are their dimensions?

R
dim(getMatrix(data.liger, "H")$celseq)
dim(getMatrix(data.liger, "V")$celseq)
dim(getMatrix(data.liger, "W"))

Next, do normalization and clustering of cells in shared nearest factor space.

Question

Do quantile normalization, cluster quantile normalized data

R
data.liger <- quantileNorm(data.liger, resolution = 1)
data.liger <- runCluster(data.liger)

What are the dimensions of H.norm. What does this represent?

R
dim(getMatrix(data.liger, "H.norm"))

Let’s see what the liger data looks like mapped onto a UMAP visualization.

Question

Run UMAP on the quantile normalized data and visualize the clusters.

R
merged_liger <- SingleCellExperiment(
    colData = cellMeta(data.liger),
    assay = list(), 
    reducedDims = list(liger = getMatrix(data.liger, "H.norm"))
)

merged_liger <- scater::runUMAP(merged_liger, dimred = 'liger', n_neighbors = 15, min_dist = 0.5)
plotUMAP(merged_liger, colour_by = "dataset")

11.6 Clustering and differential gene expression

Clustering can now be performed over the integrated data (using merged_mnn here). Here we use the Louvain clustering algorithm.

Then, differential gene expression is done across each batch, and the p-values are combined. (requires metap package installation). Note that we use the original expression data in all visualization and DE tests. We should never use values from the integrated data in DE tests, as they violate assumptions of independence among samples that are required for DE tests!

Question
  • Perform clustering on the MNN integrated data.
R
set.seed(42)
ig <- scran::buildSNNGraph(merged_mnn, use.dimred = "corrected")
clusters <- igraph::cluster_louvain(ig)$membership
merged_mnn$cluster <- factor(clusters)

table(merged_mnn$cluster, merged_mnn$batch)
   
      1   2   3   4
  1 130  55  14  47
  2 156  56  29  85
  3  74 105 208  65
  4  89 193 195 208
  5  28  39  19  36
  6  13  29  24  19
  7  10  23  11  40
  • Aggregate cells across batches and clusters to prepare pseudobulk data.
R
merged_mnn_sce <- scuttle::aggregateAcrossCells(merged_mnn, use.assay.type = "counts", ids = paste0(merged_mnn$batch, "_", merged_mnn$cluster))
merged_mnn_se <- as(merged_mnn_sce, "SummarizedExperiment")
rownames(merged_mnn_se) <- rownames(merged_mnn)
  • Perform differential gene expression analysis across clusters.

Attaching package: 'DESeq2'
The following object is masked from 'package:scater':

    fpkm
dds <- DESeqDataSet(merged_mnn_se, design = ~ batch + cluster)
converting counts to integer mode
  the design formula contains one or more numeric variables with integer values,
  specifying a model with increasing fold change for higher values.
  did you mean for this to be a factor? if so, first convert
  this variable to a factor using the factor() function
dds <- DESeq(dds)
estimating size factors
estimating dispersions
gene-wise dispersion estimates
mean-dispersion relationship
final dispersion estimates
fitting model and testing
results(dds)
log2 fold change (MLE): cluster 7 vs 1 
Wald test p-value: cluster 7 vs 1 
DataFrame with 15731 rows and 6 columns
        baseMean log2FoldChange     lfcSE       stat      pvalue        padj
       <numeric>      <numeric> <numeric>  <numeric>   <numeric>   <numeric>
A1BG   159.23838       3.286605  1.155480   2.844363  0.00445002   0.0279264
A1CF   295.07579       0.887998  0.613208   1.448119  0.14758386   0.3665995
A2ML1    7.01102      -0.409769  1.951685  -0.209956  0.83370171   0.9363838
A2M    166.13753      -5.319744  1.747873  -3.043552  0.00233803   0.0166203
A4GALT  16.15219      -2.845327  1.663240  -1.710714  0.08713391   0.2565733
...          ...            ...       ...        ...         ...         ...
ZYG11A   15.7647      -0.170908  2.199476 -0.0777038 9.38064e-01 0.976885098
ZYG11B  341.8235       0.972751  0.398965  2.4381840 1.47613e-02 0.070017264
ZYX     324.4365      -3.874309  0.968198 -4.0015672 6.29243e-05 0.000768896
ZZEF1   182.5351       1.126124  0.510961  2.2039321 2.75291e-02 0.112493033
ZZZ3    274.4154      -0.342310  0.764085 -0.4479998 6.54153e-01 0.851313980

11.7 Session info

─ Session info ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.5.1 (2025-06-13)
 os       Ubuntu 24.04.2 LTS
 system   x86_64, linux-gnu
 ui       X11
 language (EN)
 collate  en_IN.utf8
 ctype    en_IN.utf8
 tz       Europe/Paris
 date     2025-11-20
 pandoc   3.1.3 @ /usr/bin/ (via rmarkdown)
 quarto   1.5.55 @ /opt/quarto/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 package              * version  date (UTC) lib source
 abind                  1.4-8    2024-09-12 [1] CRAN (R 4.5.1)
 batchelor            * 1.26.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 beachmat               2.26.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 beeswarm               0.4.0    2021-06-01 [1] CRAN (R 4.5.1)
 Biobase              * 2.70.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 BiocGenerics         * 0.56.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 BiocNeighbors          2.4.0    2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 BiocParallel           1.44.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 BiocSingular           1.26.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 bluster                1.20.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 cachem                 1.1.0    2024-05-16 [1] CRAN (R 4.5.1)
 cli                    3.6.5    2025-04-23 [1] CRAN (R 4.5.1)
 cluster                2.1.8.1  2025-03-12 [2] CRAN (R 4.5.1)
 codetools              0.2-20   2024-03-31 [2] CRAN (R 4.5.1)
 cowplot                1.2.0    2025-07-07 [1] CRAN (R 4.5.1)
 DelayedArray           0.36.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 DelayedMatrixStats     1.32.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 DESeq2               * 1.49.3   2025-07-25 [1] Bioconductor 3.22 (R 4.5.1)
 devtools               2.4.6    2025-10-03 [1] CRAN (R 4.5.1)
 dichromat              2.0-0.1  2022-05-02 [1] CRAN (R 4.5.1)
 digest                 0.6.38   2025-11-12 [1] CRAN (R 4.5.1)
 dplyr                  1.1.4    2023-11-17 [1] CRAN (R 4.5.1)
 dqrng                  0.4.1    2024-05-28 [1] CRAN (R 4.5.1)
 edgeR                  4.8.0    2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 ellipsis               0.3.2    2021-04-29 [1] CRAN (R 4.5.1)
 evaluate               1.0.5    2025-08-27 [1] CRAN (R 4.5.1)
 farver                 2.1.2    2024-05-13 [1] CRAN (R 4.5.1)
 fastmap                1.2.0    2024-05-15 [1] CRAN (R 4.5.1)
 FNN                    1.1.4.1  2024-09-22 [1] CRAN (R 4.5.1)
 fs                     1.6.6    2025-04-12 [1] CRAN (R 4.5.1)
 generics             * 0.1.4    2025-05-09 [1] CRAN (R 4.5.1)
 GenomicRanges        * 1.62.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 ggbeeswarm             0.7.2    2023-04-29 [1] CRAN (R 4.5.1)
 ggplot2              * 4.0.1    2025-11-14 [1] CRAN (R 4.5.1)
 ggrepel                0.9.6    2024-09-07 [1] CRAN (R 4.5.1)
 glue                   1.8.0    2024-09-30 [1] CRAN (R 4.5.1)
 gridExtra              2.3      2017-09-09 [1] CRAN (R 4.5.1)
 gtable                 0.3.6    2024-10-25 [1] CRAN (R 4.5.1)
 htmltools              0.5.8.1  2024-04-04 [1] CRAN (R 4.5.1)
 htmlwidgets            1.6.4    2023-12-06 [1] CRAN (R 4.5.1)
 igraph                 2.2.1    2025-10-27 [1] CRAN (R 4.5.1)
 IRanges              * 2.44.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 irlba                  2.3.5.1  2022-10-03 [1] CRAN (R 4.5.1)
 jsonlite               2.0.0    2025-03-27 [1] CRAN (R 4.5.1)
 knitr                  1.50     2025-03-16 [1] CRAN (R 4.5.1)
 labeling               0.4.3    2023-08-29 [1] CRAN (R 4.5.1)
 lattice                0.22-7   2025-04-02 [2] CRAN (R 4.5.1)
 lifecycle              1.0.4    2023-11-07 [1] CRAN (R 4.5.1)
 limma                  3.66.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 locfit                 1.5-9.12 2025-03-05 [1] CRAN (R 4.5.0)
 magrittr               2.0.4    2025-09-12 [1] CRAN (R 4.5.1)
 Matrix                 1.7-3    2025-03-11 [1] CRAN (R 4.5.1)
 MatrixGenerics       * 1.22.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 matrixStats          * 1.5.0    2025-01-07 [1] CRAN (R 4.5.0)
 memoise                2.0.1    2021-11-26 [1] CRAN (R 4.5.1)
 metapod                1.18.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 pillar                 1.11.1   2025-09-17 [1] CRAN (R 4.5.1)
 pkgbuild               1.4.8    2025-05-26 [1] CRAN (R 4.5.1)
 pkgconfig              2.0.3    2019-09-22 [1] CRAN (R 4.5.1)
 pkgload                1.4.1    2025-09-23 [1] CRAN (R 4.5.1)
 purrr                  1.2.0    2025-11-04 [1] CRAN (R 4.5.1)
 R6                     2.6.1    2025-02-15 [1] CRAN (R 4.5.0)
 RColorBrewer           1.1-3    2022-04-03 [1] CRAN (R 4.5.1)
 Rcpp                   1.1.0    2025-07-02 [1] CRAN (R 4.5.1)
 remotes                2.5.0    2024-03-17 [1] CRAN (R 4.5.1)
 ResidualMatrix         1.20.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 rlang                  1.1.6    2025-04-11 [1] CRAN (R 4.5.1)
 rmarkdown              2.30     2025-09-28 [1] CRAN (R 4.5.1)
 RSpectra               0.16-2   2024-07-18 [1] CRAN (R 4.5.1)
 rsvd                   1.0.5    2021-04-16 [1] CRAN (R 4.5.1)
 S4Arrays               1.10.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 S4Vectors            * 0.48.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 S7                     0.2.0    2024-11-07 [1] CRAN (R 4.5.1)
 ScaledMatrix           1.18.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 scales                 1.4.0    2025-04-24 [1] CRAN (R 4.5.1)
 scater               * 1.38.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 scran                  1.38.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 scuttle              * 1.20.0   2025-10-30 [1] Bioconductor 3.22 (R 4.5.1)
 Seqinfo              * 1.0.0    2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 sessioninfo            1.2.3    2025-02-05 [1] CRAN (R 4.5.0)
 SingleCellExperiment * 1.32.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 SparseArray            1.10.1   2025-10-31 [1] Bioconductor 3.22 (R 4.5.1)
 sparseMatrixStats      1.22.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 statmod                1.5.1    2025-10-09 [1] CRAN (R 4.5.1)
 SummarizedExperiment * 1.40.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)
 tibble                 3.3.0    2025-06-08 [1] CRAN (R 4.5.1)
 tidyselect             1.2.1    2024-03-11 [1] CRAN (R 4.5.1)
 usethis                3.2.1    2025-09-06 [1] CRAN (R 4.5.1)
 uwot                   0.2.4    2025-11-10 [1] CRAN (R 4.5.1)
 vctrs                  0.6.5    2023-12-01 [1] CRAN (R 4.5.1)
 vipor                  0.4.7    2023-12-18 [1] CRAN (R 4.5.1)
 viridis                0.6.5    2024-01-29 [1] CRAN (R 4.5.1)
 viridisLite            0.4.2    2023-05-02 [1] CRAN (R 4.5.1)
 withr                  3.0.2    2024-10-28 [1] CRAN (R 4.5.1)
 xfun                   0.54     2025-10-30 [1] CRAN (R 4.5.1)
 XVector                0.50.0   2025-10-29 [1] Bioconductor 3.22 (R 4.5.1)

 [1] /home/rsg/R/x86_64-pc-linux-gnu-library/4.5
 [2] /opt/R/4.5.1/lib/R/library
 * ── Packages attached to the search path.

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────