TweedSpot

Overview

TweedSpot detects spatially variable genes (SVGs) in spatial transcriptomics data using Tweedie generalized additive models (GAMs) with a spatial smooth. The package currently focuses on an agnostic SVG workflow built around the SpatialExperiment container.

TweedSpot is intended to be useful to the broader spatial transcriptomics community, especially for analysts who want a method that models count data directly, adapts to heterogeneous mean-variance relationships, and plugs into standard downstream workflows for ranking, visualization, and interpretation of spatially variable genes. We are submitting the package to Bioconductor because we think it can serve as a broadly accessible, reproducible tool for community use and comparison across spatial datasets and analysis pipelines.

Once available on Bioconductor, the release version can be installed with:

install.packages("BiocManager")
BiocManager::install("TweedSpot")

The current development version can be installed from GitHub with:

install.packages("remotes")
remotes::install_github("himelmallick/TweedSpot", dependencies = TRUE)

This vignette covers a standard workflow:

  1. load a SpatialExperiment
  2. prefilter low-information genes
  3. fit TweedSpot
  4. extract and rank gene-level results
  5. visualize observed and fitted spatial patterns

Load packages

library(TweedSpot)
library(SpatialExperiment)
#> Loading required package: 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 object is masked from 'package:utils':
#> 
#>     data
#> 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, scale, sequence, table,
#>     tapply, transform, 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(SummarizedExperiment)
library(STexampleData)
#> Loading required package: ExperimentHub
#> Loading required package: AnnotationHub
#> Loading required package: BiocFileCache
#> Loading required package: dbplyr
#> 
#> Attaching package: 'AnnotationHub'
#> The following object is masked from 'package:Biobase':
#> 
#>     cache
library(BiocParallel)

Load example data

We use the Visium_humanDLPFC() example dataset from STexampleData. To keep the vignette lightweight, we restrict the analysis to in-tissue spots and a small subset of genes.

spe <- Visium_humanDLPFC()
#> see ?STexampleData and browseVignettes('STexampleData') for documentation
#> downloading 1 resources
#> retrieving 1 resource
#> loading from cache
spe <- spe[, colData(spe)$in_tissue == 1]
spe <- spe[seq_len(min(250, nrow(spe))), ]
spe
#> class: SpatialExperiment 
#> dim: 250 3639 
#> metadata(0):
#> assays(1): counts
#> rownames(250): ENSG00000243485 ENSG00000237613 ... ENSG00000198793
#>   ENSG00000225602
#> rowData names(3): gene_id gene_name feature_type
#> colnames(3639): AAACAAGTATCTCCCA-1 AAACAATCTACTAGCA-1 ...
#>   TTGTTTGTATTACACG-1 TTGTTTGTGTAAATTC-1
#> colData names(8): barcode_id sample_id ... reference cell_count
#> reducedDimNames(0):
#> mainExpName: NULL
#> altExpNames(0):
#> spatialCoords names(2) : pxl_col_in_fullres pxl_row_in_fullres
#> imgData names(4): sample_id image_id data scaleFactor

Prefilter features

filter_features() removes features with limited information before model fitting. Its thresholds (minimum total signal, percent and number of locations where the feature is detected, mean, and variance) make no assumption about the assay, so the same call works for genes, proteins, peaks, or metabolites. For intensity data, detection_threshold sets the value above which a feature counts as detected. Named features such as negative-control probes can be removed with exclude or exclude_pattern, and, for transcriptomics only, exclude_mito and exclude_ribo remove mitochondrial and ribosomal genes.

Two further steps are specific to TweedSpot. First, by default (model_aware = TRUE) a feature must be detected in at least as many locations as the spatial smooth has basis functions, so pass the same smooth_k you will use in tweedspot(). Second, locations can be quality controlled with min_total_per_location, min_features_per_location, outlier_nmads, max_excluded_fraction (for example the mitochondrial fraction), and location_mask. Use action = "flag" to keep everything and record the reasons in filter_keep and filter_reason columns instead.

spe <- TweedSpot::filter_features(
  input = spe,
  assay_name = "counts",
  min_total = 5,
  min_pct_locations = 5,
  min_locations = 3,
  min_mean = 0.05,
  drop_empty_locations = TRUE,
  report = TRUE
)
#> filter_features retained 83/250 features and 3638/3639 spatial locations.
#> Features failing each criterion: low_total=97, low_pct_locations=167, low_locations=87, low_mean=166, low_model_support=130.
#> Locations failing each criterion: empty=1.

spe
#> class: SpatialExperiment 
#> dim: 83 3638 
#> metadata(1): filter_features
#> assays(1): counts
#> rownames(83): ENSG00000188976 ENSG00000188290 ... ENSG00000171824
#>   ENSG00000198793
#> rowData names(3): gene_id gene_name feature_type
#> colnames(3638): AAACAAGTATCTCCCA-1 AAACAATCTACTAGCA-1 ...
#>   TTGTTTGTATTACACG-1 TTGTTTGTGTAAATTC-1
#> colData names(8): barcode_id sample_id ... reference cell_count
#> reducedDimNames(0):
#> mainExpName: NULL
#> altExpNames(0):
#> spatialCoords names(2) : pxl_col_in_fullres pxl_row_in_fullres
#> imgData names(4): sample_id image_id data scaleFactor

Fit TweedSpot

We now fit TweedSpot on the filtered dataset. For this example object, sample_id is constant across all retained spots, so we do not include it as a covariate. To keep the vignette fast and robust across build machines, this runnable example uses the Poisson mapping (tweedie_p = 1), total-count library-size offsets, serial execution, and a small smooth_k. Estimated Tweedie fits with tweedie_p = NULL and SCRAN size factors are shown below as recommended analysis options for full datasets.



bp <- SerialParam()

spe_res <- tweedspot(
  input = spe,
  assay_name = "counts",
  two_part = FALSE,
  tweedie_p = 1,
  fit_method = "REML",
  use_bam = FALSE,
  bam_discrete = TRUE,
  size_factor_method = "library_size",
  smooth_k = 5,
  BPPARAM = bp,
  verbose = TRUE
)
#> Running TweedSpot on 83 genes across 3638 spatial locations with 1 worker(s).
#> Completed TweedSpot fits for 83 feature(s).

Tweedie index parameter

TweedSpot is built on mgcv, so its Tweedie index support follows what can be represented as mgcv families. The main control is tweedie_p.

When tweedie_p = NULL, TweedSpot uses mgcv::tw() and estimates the Tweedie index during fitting. This is the default and recommended setting for raw spatial transcriptomics count data because genes can differ substantially in their mean-variance relationships.

Fixed values are also supported:

tweedie_p Family used internally Interpretation
NULL mgcv::tw() Estimated Tweedie index
0 stats::gaussian() Gaussian model for continuous approximately normal outcomes
1 stats::poisson() Poisson count model
1 < p < 2 mgcv::Tweedie(p = p) Compound Poisson-Gamma Tweedie model
2 mgcv::Tweedie(p = 2) Gamma boundary case
3 stats::inverse.gaussian() Inverse Gaussian model for strictly positive outcomes

Values between 0 and 1, values between 2 and 3, negative values, and values greater than 3 are not supported. The inverse Gaussian case requires strictly positive responses, so it should not be used on raw count assays that contain zeros.

For example, users can fit a fixed Tweedie index:

spe_res_fixed <- tweedspot(
  input = spe,
  assay_name = "counts",
  tweedie_p = 1.5,
  use_bam = TRUE,
  bam_discrete = TRUE,
  smooth_k = 10
)

Or use the Poisson and Gaussian mappings when those assumptions are desired:

spe_res_poisson <- tweedspot(
  input = spe,
  assay_name = "counts",
  tweedie_p = 1
)

spe_res_gaussian <- tweedspot(
  input = spe,
  assay_name = "counts",
  tweedie_p = 0
)

The family argument remains available for users who want to pass an mgcv family directly, but tweedie_p takes precedence when both are supplied.

For a full analysis, users may prefer the adaptive Tweedie model with SCRAN size factors:

spe_res <- tweedspot(
  input = spe,
  assay_name = "counts",
  tweedie_p = NULL,
  use_bam = TRUE,
  bam_discrete = TRUE,
  size_factor_method = "scran",
  smooth_k = 10,
  BPPARAM = BiocParallel::bpparam()
)

Extract and rank results

get_tweedspot_results() returns one row per feature, sorted by adjusted p-value (ties broken by the test statistic):

Column Meaning
feature Row name of the feature
feature_name Readable name from rowData(), when it differs from the row name
stat Test statistic of the spatial smooth
pval, padj Raw and adjusted p-value (padj_method, Benjamini-Yekutieli by default)
edf Effective degrees of freedom of the smooth (pattern complexity)
dev_expl Deviance explained by the model
p, family Fitted Tweedie variance power and family

The same values are stored in rowData(spe_res) with a tweedspot_ prefix.

res <- get_tweedspot_results(spe_res)

head(res)
#>           feature feature_name      stat pval padj      edf   dev_expl p
#> 1 ENSG00000179403         VWA1 1001.8822    0    0 3.970820 0.25511182 1
#> 2 ENSG00000049245        VAMP3  452.2555    0    0 3.948047 0.14822962 1
#> 3 ENSG00000116251        RPL22  354.1967    0    0 3.971214 0.07255624 1
#> 4 ENSG00000187730        GABRD  259.2659    0    0 3.966324 0.07398777 1
#> 5 ENSG00000162444         RBP7  258.2351    0    0 3.899639 0.15604747 1
#> 6 ENSG00000171603       CLSTN1  183.7028    0    0 3.956985 0.04926444 1
#>    family
#> 1 poisson
#> 2 poisson
#> 3 poisson
#> 4 poisson
#> 5 poisson
#> 6 poisson

top_spatial_genes() provides a convenience wrapper for retrieving the top hits by adjusted p-value, raw p-value, statistic, or deviance explained.

top_spatial_genes(spe_res, n = 10, rank_by = "padj")
#>            feature feature_name       stat pval padj      edf   dev_expl p
#> 1  ENSG00000179403         VWA1 1001.88216    0    0 3.970820 0.25511182 1
#> 2  ENSG00000049245        VAMP3  452.25548    0    0 3.948047 0.14822962 1
#> 3  ENSG00000116251        RPL22  354.19670    0    0 3.971214 0.07255624 1
#> 4  ENSG00000187730        GABRD  259.26589    0    0 3.966324 0.07398777 1
#> 5  ENSG00000162444         RBP7  258.23509    0    0 3.899639 0.15604747 1
#> 6  ENSG00000171603       CLSTN1  183.70285    0    0 3.956985 0.04926444 1
#> 7  ENSG00000187608        ISG15  160.58221    0    0 3.701830 0.04877231 1
#> 8  ENSG00000054523        KIF1B  104.85272    0    0 3.814177 0.02464114 1
#> 9  ENSG00000097021        ACOT7   87.80020    0    0 3.932911 0.02471381 1
#> 10 ENSG00000069424       KCNAB2   87.73853    0    0 3.900609 0.02477056 1
#>     family
#> 1  poisson
#> 2  poisson
#> 3  poisson
#> 4  poisson
#> 5  poisson
#> 6  poisson
#> 7  poisson
#> 8  poisson
#> 9  poisson
#> 10 poisson

Cell-type-specific spatial variation

With single-cell-resolution data, a feature can look spatially variable only because cell types are arranged in space: a marker of one cell type is high wherever that cell type sits. Setting group to a cell-type column tests each feature within each cell type instead, so only spatial variation among cells of the same type is called. Size factors and coordinates are shared across types, each type gets its own spatial smooth, and p-values are adjusted stage-wise: a feature is first screened across all types, then confirmed type by type.

As a small real example we use Slide-seqV2 data from the mouse hippocampus (Stickels et al., 2021), in which each bead carries a cell-type label from RCTD (Cable et al., 2022). To keep the example fast we take the three most common cell types, 1,500 beads of each, and the 25 most highly expressed genes.

spe_hpc <- STexampleData::SlideSeqV2_mouseHPC()
#> see ?STexampleData and browseVignettes('STexampleData') for documentation
#> downloading 1 resources
#> retrieving 1 resource
#> loading from cache
spe_hpc <- spe_hpc[, !is.na(spe_hpc$celltype)]

top_types <- names(sort(table(spe_hpc$celltype), decreasing = TRUE))[1:3]
set.seed(1)
keep_beads <- unlist(lapply(top_types, function(ct) {
  idx <- which(spe_hpc$celltype == ct)
  idx[sample.int(length(idx), min(1500L, length(idx)))]
}))
spe_hpc <- spe_hpc[, sort(keep_beads)]

# Library-size factors from all genes, before keeping only a few of them.
lib <- MatrixGenerics::colSums(counts(spe_hpc))
spe_hpc$sizeFactor <- lib / mean(lib)

spe_hpc <- filter_features(
  spe_hpc,
  min_total = 50,
  exclude_mito = TRUE,
  group = "celltype",
  smooth_k = 20
)
top_genes <- order(MatrixGenerics::rowSums(counts(spe_hpc)), decreasing = TRUE)[1:25]
spe_hpc <- spe_hpc[top_genes, ]

table(spe_hpc$celltype)
#> 
#>       Astrocyte             CA3 Oligodendrocyte 
#>            1500            1500            1500

We fit TweedSpot twice: once ignoring cell types, and once within each cell type.

overall <- tweedspot(spe_hpc, smooth_k = 20, seed = 1, BPPARAM = bp,
                     verbose = FALSE)
by_type <- tweedspot(spe_hpc, group = "celltype", smooth_k = 20, seed = 1,
                     BPPARAM = bp, verbose = FALSE)

Ignoring cell types, many genes look spatially variable. Within cell types, fewer remain: genes called only in the first analysis vary in space mainly because the cell types they mark are arranged in space.

called_overall <- rowData(overall)$tweedspot_padj < 0.05
called_within <- rowData(by_type)$tweedspot_group_sig != ""
table(overall = called_overall, within_cell_type = called_within)
#>        within_cell_type
#> overall FALSE TRUE
#>    TRUE     3   22

# Genes called overall but in no cell type: candidate composition effects
rownames(spe_hpc)[called_overall & !called_within]
#> [1] "Cpe"      "Hsp90ab1" "Ckb"

# One row per gene and cell type
res_ct <- get_tweedspot_results(by_type, group = TRUE)
head(res_ct[, c("feature", "group", "n_detected", "padj", "dev_expl")])
#>   feature           group n_detected          padj  dev_expl
#> 1  Tmsb4x             CA3       1110 2.122697e-307 0.3834169
#> 2    Hpca             CA3       1357 2.122697e-307 0.3658890
#> 3     Mbp Oligodendrocyte       1381 2.122697e-307 0.2627221
#> 4  Camk2a Oligodendrocyte        373 2.122697e-307 0.3178921
#> 5   Kif5a Oligodendrocyte       1111 2.122697e-307 0.2173208
#> 6   Ptgds       Astrocyte        383 2.122697e-307 0.2169499

# Top hits within the most common cell type
top_spatial_genes(by_type, n = 5, group = top_types[1])
#>   feature feature_name feature_index     group n_locations n_detected tested
#> 1   Ptgds        Ptgds            20 Astrocyte        1500        383   TRUE
#> 2    Hpca         Hpca             4 Astrocyte        1500        569   TRUE
#> 3   Kif5a        Kif5a            14 Astrocyte        1500        725   TRUE
#> 4  Malat1       Malat1             2 Astrocyte        1500        831   TRUE
#> 5  Camk2a       Camk2a            18 Astrocyte        1500       1043   TRUE
#>   reason     stat pval       edf   dev_expl        p  family          padj
#> 1        23.44492    0 15.603855 0.21694994 1.100000 tweedie 2.122697e-307
#> 2        18.02608    0 16.761350 0.20015487 1.100001 tweedie 2.122697e-307
#> 3        16.02098    0 11.832803 0.13301651 1.100000 tweedie 2.122697e-307
#> 4        12.74166    0  2.638292 0.02502882 1.379826 tweedie 2.122697e-307
#> 5        11.30423    0 17.458981 0.11000527 1.100000 tweedie 2.122697e-307
#>    padj_feature
#> 1 2.122697e-307
#> 2 2.122697e-307
#> 3 2.122697e-307
#> 4 2.122697e-307
#> 5 2.122697e-307

plot_feature() and plot_top_features() take the same group argument and show the model fitted on that cell type’s beads only.

best <- top_spatial_genes(by_type, n = 1, group = top_types[1])$feature
plot_feature(by_type, best, group = top_types[1])

The same argument works with any discrete location label, such as cell states or annotated regions.

Visualize spatial patterns

plot_feature() shows one feature in three aligned panels: the observed normalized signal, the TweedSpot fit on the same colour scale, and the fitted spatial effect as a log2 fold change relative to the feature’s average. It refits the model with the settings tweedspot() stored in metadata(spe_res)$tweedspot, so the figure shows exactly the model that was tested.

top_feature <- res$feature[1]
plot_feature(spe_res, top_feature)

plot_top_features() draws the spatial effects of the n_top most significant features on one shared scale, so the strength of different patterns can be compared directly.

plot_top_features(spe_res, n_top = 6)

Both functions save publication-ready files when given an output_dir, which is created if needed. The default size, 7.2 inches wide, is a two-column journal width.

plot_feature(spe_res, top_feature, output_dir = "figures", formats = c("pdf", "png"))
plot_top_features(spe_res, n_top = 9, output_dir = "figures")

Notes

  • Covariates are specified from colData(input) using a one-sided formula such as ~ batch + age + sex.
  • If colData(input)$sizeFactor is present, TweedSpot uses it as the library size adjustment. Otherwise, size_factor_method = "scran" estimates size factors with scran::computeSumFactors(). Users can set size_factor_method = "library_size" for total-count scaling.
  • filter_features() can optionally report how many features and locations were retained and how many were removed by each criterion.
  • tweedspot() can print lightweight progress messages through verbose = TRUE.

Session information

sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.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.32.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=en_US.UTF-8    
#>  [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] BiocParallel_1.47.0         STexampleData_1.21.1       
#>  [3] ExperimentHub_3.3.2         AnnotationHub_4.3.2        
#>  [5] BiocFileCache_3.3.0         dbplyr_2.6.0               
#>  [7] SpatialExperiment_1.23.0    SingleCellExperiment_1.35.2
#>  [9] SummarizedExperiment_1.43.0 Biobase_2.73.2             
#> [11] GenomicRanges_1.65.4        Seqinfo_1.3.2              
#> [13] IRanges_2.47.5              S4Vectors_0.51.10          
#> [15] BiocGenerics_0.59.12        generics_0.1.4             
#> [17] MatrixGenerics_1.25.0       matrixStats_1.5.0          
#> [19] TweedSpot_0.99.3            BiocStyle_2.41.0           
#> 
#> loaded via a namespace (and not attached):
#>  [1] DBI_1.3.0            httr2_1.3.0          rlang_1.3.0         
#>  [4] magrittr_2.0.5       otel_0.2.0           compiler_4.6.1      
#>  [7] RSQLite_3.53.3       mgcv_1.9-4           png_0.1-9           
#> [10] vctrs_0.7.3          pkgconfig_2.0.3      crayon_1.5.3        
#> [13] fastmap_1.2.0        magick_2.9.1         XVector_0.53.0      
#> [16] labeling_0.4.3       scuttle_1.23.2       rmarkdown_2.32      
#> [19] purrr_1.2.2          bit_4.6.0            xfun_0.61           
#> [22] bluster_1.23.1       cachem_1.1.0         beachmat_2.29.3     
#> [25] jsonlite_2.0.0       blob_1.3.0           DelayedArray_0.39.8 
#> [28] irlba_2.4.1          parallel_4.6.1       cluster_2.1.8.3     
#> [31] R6_2.6.1             RColorBrewer_1.1-3   bslib_0.12.0        
#> [34] limma_3.99.2         jquerylib_0.1.4      Rcpp_1.1.2          
#> [37] knitr_1.52           BiocBaseUtils_1.15.1 splines_4.6.1       
#> [40] Matrix_1.7-6         igraph_2.3.4         tidyselect_1.2.1    
#> [43] abind_1.4-8          yaml_2.3.12          codetools_0.2-20    
#> [46] curl_8.0.0           lattice_0.23-1       tibble_3.3.1        
#> [49] S7_0.2.2             withr_3.0.3          KEGGREST_1.53.6     
#> [52] evaluate_1.0.5       Biostrings_2.81.9    pillar_1.11.1       
#> [55] BiocManager_1.30.27  filelock_1.0.3       ggplot2_4.0.3       
#> [58] BiocVersion_3.24.0   scales_1.4.0         glue_1.8.1          
#> [61] metapod_1.21.0       maketools_1.3.2      tools_4.6.1         
#> [64] BiocNeighbors_2.7.3  sys_3.4.3            ScaledMatrix_1.21.0 
#> [67] locfit_1.5-9.12      buildtools_1.0.0     scran_1.41.1        
#> [70] grid_4.6.1           AnnotationDbi_1.75.2 edgeR_4.99.8        
#> [73] patchwork_1.3.2      nlme_3.1-171         BiocSingular_1.29.2 
#> [76] cli_3.6.6            rsvd_1.0.5           rappdirs_0.3.4      
#> [79] S4Arrays_1.13.2      dplyr_1.2.1          gtable_0.3.6        
#> [82] sass_0.4.10          digest_0.6.39        SparseArray_1.13.4  
#> [85] dqrng_0.4.1          farver_2.1.2         rjson_0.2.23        
#> [88] memoise_2.0.1        htmltools_0.5.9      lifecycle_1.0.5     
#> [91] httr_1.4.9           statmod_1.5.2        bit64_4.8.6