Skip to contents

Introduction

scider is a user-friendly R package providing functions to model the global density of cells in a slide of spatial transcriptomics data. All functions in the package are built on the SpatialExperiment object, allowing integration into various spatial transcriptomics-related packages from Bioconductor. After modelling density, scider supports several downstream analyses, including colocalization analysis, boundary detection and differential density analysis.

The plotting functions return ggplot objects, so figures can be further customised with any ggplot2 layer.

This vignette walks through a typical workflow on a Xenium breast cancer dataset.

Load Example Data

The example dataset is a small SpatialExperiment object (one Xenium breast cancer section) hosted as a GitHub Release asset. Download it once and cache locally:

data_url   <- "https://github.com/ChenLaboratory/example_data/releases/download/v1.0.0/spe_xenium_bc_s1rep1.rds"
cache_dir  <- tools::R_user_dir("scider", "cache")
data_file  <- file.path(cache_dir, "spe_xenium_bc_s1rep1.rds")

if (!file.exists(data_file)) {
  dir.create(cache_dir, recursive = TRUE, showWarnings = FALSE)
  download.file(data_url, data_file, mode = "wb")
}

spe <- readRDS(data_file)
spe
#> class: SpatialExperiment 
#> dim: 313 163797 
#> metadata(0):
#> assays(1): counts
#> rownames(313): ABCC11 ACTA2 ... ZEB2 ZNF562
#> rowData names(3): ID Symbol Type
#> colnames(163797): cell_1 cell_2 ... cell_167779 cell_167780
#> colData names(12): cell_id transcript_counts ... gene_counts cell_type
#> reducedDimNames(0):
#> mainExpName: NULL
#> altExpNames(0):
#> spatialCoords names(2) : x_centroid y_centroid
#> imgData names(1): sample_id

Data exploration

The Xenium data was annotated by 10X Genomics. The cell type annotation is summarized below.

table(colData(spe)$cell_type)
#> 
#>           Stromal    Invasive_Tumor              DCIS       Macrophages            Myoepi       Endothelial      CD4+_T_Cells 
#>             41422             38149             24604             12798              9938              8931              8453 
#>      CD8+_T_Cells            Others           B_Cells Perivascular-Like               DCs        Mast_Cells 
#>              6940              5769              4987               847               792               167

We can visualize the spatial distribution of all the cell types across the tissue as follows.

plotSpatial(spe, group.by = "cell_type")

Or visualize the spatial distribution of specific cell types (e.g., DCIS and Invasive Tumor).

COI <- c("DCIS", "Invasive_Tumor")
sel <- spe$cell_type %in% COI
plotSpatial(spe[, sel], group.by = "cell_type")

We can also examine the expression level of each individual gene spatially. Because the returned object is a ggplot, we can add a title with ggtitle().

plotSpatial(spe, feature = "FOXA1", type = "log") +
  ggtitle("FOXA1 expression (log-scale)")

Cell type density

scider implements grid-based density analysis for spatial transcriptomics data. We first calculate density for each cell type using function gridDensity. The calculated density and grid information are saved in the metadata of the SpatialExperiment object. The size of the grid (bin) is set by the grid.length.x parameter, and the level of smoothing is set by bandwidth.

spe <- gridDensity(spe, grid.length.x = 50, bandwidth = 50)

We can visualise the overall (all cell types) density of the whole tissue using function plotDensity. The probs parameter indicates the density percentile below which the grids are filtered (not plotted). Setting probs to 0 will plot everything including the background.

plotDensity(spe, probs = 0)

Alternatively, user can specify cutoff to filter grids with low cell density. The density values represent the expected numbers of cells at each grid.

plotDensity(spe, cutoff = 5)

We can also visualise the density of specific cell type(s).

plotDensity(spe, coi = COI)

Find Regions-of-interest (ROIs)

After obtaining grid-based density for each cell type, we can then detect regions-of-interest (ROIs) based on density of certain cell type(s). This is done by using the findROI function.

Here we identify ROIs based on the cell density of DCIS and invasive tumor. The min.density parameter filters grids with low cell density, whereas the ngrid.min gives the minimum number of grids that each ROI should have.

spe <- findROI(spe, coi = COI, min.density = 0.5, ngrid.min = 20)
#> Overwriting the probs argument. Grids are filtered by the min.density value.

The ROIs can then be visualized using plotROI.

plotROI(spe, roi = COI)

Testing relationship between cell types

After defining ROIs, we can test whether the densities of any two cell types are spatially correlated, within each ROI or overall while accounting for ROI variation. This is done with function corDensity, which uses a modified t-test (Dutilleul’s correction) that accounts for the spatial autocorrelation between neighbouring grids.

results <- corDensity(spe, roi = COI)

We can examine the correlation between each pair of cell types in each ROI.

results$ROI
#> DataFrame with 3276 rows and 9 columns
#>        celltype1      celltype2         ROI     ngrid   cor.coef         t        df     p.Pos       p.Neg
#>      <character>    <character> <character> <numeric>  <numeric> <numeric> <numeric> <numeric>   <numeric>
#> 1        Stromal Invasive tumor           1       202  -0.151757  -1.16577   57.6512  0.875746 0.124253901
#> 2        Stromal Invasive tumor           2       128  -0.777733  -4.30982   12.1338  0.999505 0.000494573
#> 3        Stromal Invasive tumor           3       248  -0.700439  -3.20855   10.6886  0.995690 0.004310468
#> 4        Stromal Invasive tumor           4       171  -0.457845  -2.54249   24.3734  0.991120 0.008880219
#> 5        Stromal Invasive tumor           5       227  -0.488500  -3.92329   49.1096  0.999864 0.000135806
#> ...          ...            ...         ...       ...        ...       ...       ...       ...         ...
#> 3272        D cs     Mast cells          38        54  0.3382772  1.827257   25.8390 0.0396169    0.960383
#> 3273        D cs     Mast cells          39       108  0.2618742  1.662013   37.5172 0.0524223    0.947578
#> 3274        D cs     Mast cells          40       132  0.0235890  0.157033   44.2912 0.4379664    0.562034
#> 3275        D cs     Mast cells          41       103  0.3117195  1.585540   23.3578 0.0631452    0.936855
#> 3276        D cs     Mast cells          42        62 -0.0201452 -0.146636   52.9616 0.5580119    0.441988

Or the correlation between each pair of cell types across all ROIs:

results$overall
#> DataFrame with 78 rows and 5 columns
#>             celltype1      celltype2   cor.coef       p.Pos       p.Neg
#>           <character>    <character>  <numeric>   <numeric>   <numeric>
#> 1             Stromal Invasive tumor -0.4230995 1.00000e+00 4.29019e-53
#> 2             Stromal           Dcis -0.5108656 1.28106e-01 8.94811e-62
#> 3             Stromal    Macrophages  0.3641173 5.08375e-32 9.99999e-01
#> 4             Stromal         Myoepi -0.0423066 1.48122e-19 4.37604e-03
#> 5             Stromal    Endothelial  0.4622224 1.09692e-55 1.00000e+00
#> ...               ...            ...        ...         ...         ...
#> 74            B cells           D cs  0.3357256 4.33879e-61    1.000000
#> 75            B cells     Mast cells  0.1900816 2.09341e-27    0.999998
#> 76  Perivascular like           D cs  0.1259144 2.18311e-11    0.999801
#> 77  Perivascular like     Mast cells  0.0859226 9.10731e-07    0.952813
#> 78               D cs     Mast cells  0.0992215 3.65284e-29    0.998556

We can visualise the statistics between each pair of cell types using function plotCorHeatmap in the ROIs:

plotCorHeatmap(results$ROI)

Or the correlation between cell type pairs across the whole slide:

plotCorHeatmap(results$overall)

Cell density contour

Another strategy to partition a tissue spatially is to construct cell density contours. This can be done using the getContour function.

The equal.cell parameter ensures roughly equal cell numbers across all density levels.

spe <- getContour(spe, coi = COI, equal.cell = TRUE)
#> Finding contour using total density of input COIs.
#> Using bins = 10 to draw contours with equal cell numbers.

The constructed contours can be visualised using plotContour.

plotContour(spe, coi = COI)

Session Information

sessionInfo()
#> R version 4.6.0 (2026-04-24 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26200)
#> 
#> Matrix products: default
#>   LAPACK version 3.12.1
#> 
#> locale:
#> [1] LC_COLLATE=English_Australia.utf8  LC_CTYPE=English_Australia.utf8    LC_MONETARY=English_Australia.utf8
#> [4] LC_NUMERIC=C                       LC_TIME=English_Australia.utf8    
#> 
#> time zone: Australia/Sydney
#> tzcode source: internal
#> 
#> attached base packages:
#> [1] stats4    stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#>  [1] ggplot2_4.0.3               SpatialExperiment_1.21.0    SingleCellExperiment_1.33.2 SummarizedExperiment_1.41.1
#>  [5] Biobase_2.71.0              GenomicRanges_1.63.2        Seqinfo_1.1.0               IRanges_2.45.0             
#>  [9] S4Vectors_0.49.2            BiocGenerics_0.57.1         generics_0.1.4              MatrixGenerics_1.23.0      
#> [13] matrixStats_1.5.0           scider_1.9.6               
#> 
#> loaded via a namespace (and not attached):
#>  [1] DBI_1.3.0              deldir_2.0-4           rlang_1.2.0            magrittr_2.0.5         snakecase_0.11.1      
#>  [6] otel_0.2.0             e1071_1.7-17           compiler_4.6.0         spatstat.geom_3.7-3    mgcv_1.9-4            
#> [11] fftwtools_0.9-11       vctrs_0.7.3            stringr_1.6.0          pkgconfig_2.0.3        fastmap_1.2.0         
#> [16] magick_2.9.1           XVector_0.51.0         labeling_0.4.3         promises_1.5.0         purrr_1.2.2           
#> [21] xfun_0.57              jsonlite_2.0.0         goftest_1.2-3          later_1.4.8            DelayedArray_0.37.1   
#> [26] spatstat.utils_3.2-2   R6_2.6.1               stringi_1.8.7          RColorBrewer_1.1-3     spatstat.data_3.1-9   
#> [31] spatstat.univar_3.1-7  lubridate_1.9.5        Rcpp_1.1.1-1.1         knitr_1.51             tensor_1.5.1          
#> [36] httpuv_1.6.17          Matrix_1.7-5           splines_4.6.0          igraph_2.3.0           timechange_0.4.0      
#> [41] tidyselect_1.2.1       abind_1.4-8            codetools_0.2-20       spatstat.random_3.4-5  spatstat.explore_3.8-0
#> [46] lattice_0.22-9         tibble_3.3.1           shiny_1.13.0           withr_3.0.3            S7_0.2.2              
#> [51] evaluate_1.0.5         sf_1.1-1               units_1.0-1            proxy_0.4-29           polyclip_1.10-7       
#> [56] pillar_1.11.1          KernSmooth_2.23-26     plotly_4.12.0          dbscan_1.2.4           scales_1.4.0          
#> [61] xtable_1.8-8           class_7.3-23           glue_1.8.1             janitor_2.2.1          pheatmap_1.0.13       
#> [66] lazyeval_0.2.3         tools_4.6.0            hexDensity_1.4.10      hexbin_1.28.5          data.table_1.18.2.1   
#> [71] grid_4.6.0             tidyr_1.3.2            nlme_3.1-169           fastmatrix_0.6-6       cli_3.6.6             
#> [76] spatstat.sparse_3.1-0  S4Arrays_1.11.1        viridisLite_0.4.3      dplyr_1.2.1            gtable_0.3.6          
#> [81] SpatialPack_0.4-1      digest_0.6.39          classInt_0.4-11        SparseArray_1.11.13    rjson_0.2.23          
#> [86] htmlwidgets_1.6.4      farver_2.1.2           htmltools_0.5.9        lifecycle_1.0.5        httr_1.4.8            
#> [91] mime_0.13