Skip to contents

Overview

This tutorial provides a step-by-step workflow for applying the SpatialEcoTyper framework and a pre-trained deconvolution model to infer the abundances of spatial ecotypes from bulk transcriptomic data, including bulk RNA-seq and Visium spatial transcriptomics data.

First load required packages for this vignette

Inferring SE levels from bulk RNA-seq data

We will use bulk RNA-seq data from TCGA melanoma (SKCM) samples for demonstration. The gene expression matrix is available at SKCM_RNASeqV2.geneExp.tsv, which is a Transcripts Per Million (TPM) matrix obtained from the PanCanAtlas.

Loading bulk expression of TCGA melanoma samples

url <- "https://spatialecotyper.stanford.edu/inc/inc.public.vignettes.php?file=SKCM_RNASeqV2.geneExp.tsv"
download.file(url, destfile = "SKCM_RNASeqV2.geneExp.tsv", mode = "wb", method = "curl")
bulkdata <- fread("SKCM_RNASeqV2.geneExp.tsv", sep = "\t", data.table = FALSE)
rownames(bulkdata) <- bulkdata[, 1] ## Set the first column as row names
bulkdata <- bulkdata[, -1] ## Drop the first column
head(bulkdata[, 1:5]) ## TPM matrix
##       TCGA.3N.A9WB.06 TCGA.3N.A9WC.06 TCGA.3N.A9WD.06 TCGA.BF.A1PU.01
## A1BG         381.0660         195.182        360.8790        176.3990
## A1CF           0.0000           0.000          0.7092          0.0000
## A2BP1          0.0000           0.000          6.3830          1.2987
## A2LD1        250.1980         160.755         97.1986        163.2340
## A2M         2209.5200      169237.000      18257.9000       6716.4500
## A2ML1          7.2698           0.000          0.0000          7.7922
##       TCGA.BF.A1PV.01
## A1BG         216.8470
## A1CF           0.0000
## A2BP1          0.0000
## A2LD1         60.8727
## A2M         1740.5800
## A2ML1          0.5977

Loading pre-trained model

Users must provide a pre-trained Non-Negative Matrix Factorization (NMF) model through the W argument as a numeric matrix.

For standard analyses using the nine spatial ecotypes defined in our study, the pre-trained model file (SE_deconvolution_model.rds) can be downloaded from Spatial EcoTyper web platform. Alternatively, you can upload your bulk expression data directly to the web platform to run the deconvolution.

If you have identified new spatial ecotypes, you can train a custom deconvolution model (to derive a custom W matrix) using your own dataset, following Tutorial 7: NMF Model Development for Spatial Ecotype Deconvolution from Bulk Gene Expression Data.

# Note: Load the downloaded file into R using readRDS() to extract the W matrix before passing it to the function
nmf_model = readRDS("SE_deconvolution_model.rds")

SE deconvolution

The DeconvoluteSE function infers SE abundances in bulk tissue samples. By default, the predicted abundances of SEs and NonSE components for each sample are normalized to sum to 1. To disable this normalization, set sum2one = FALSE.

Before prediction, DeconvoluteSE automatically preprocesses the input data. If the maximum expression value exceeds 80, the function applies a log₂ transformation. It then standardizes each gene to have a mean of 0 and a standard deviation of 1. This standardization can be disabled by setting scale = FALSE. For reliable standardization, we recommend using at least 20 samples. For smaller datasets, we recommend combining the samples with a larger reference dataset during normalization and then applying DeconvoluteSE to the normalized data.

Deconvolution

sefracs <- DeconvoluteSE(bulkdata, W = nmf_model)

Compare SE abundances across sample groups

For demonstration purposes, here we compare the SE abundances between primary and metastatic melanoma samples.

sefracs = as.data.frame(sefracs)
sefracs$Group = NA
sefracs$Group[grepl("01$", rownames(sefracs))] = "Primary"
sefracs$Group[grepl("06$", rownames(sefracs))] = "Metastatic"

ggplot(sefracs[!is.na(sefracs$Group), ], aes(Group, SE1)) + 
  geom_boxplot(width = 0.5, outlier.shape = NA) + 
  ggpubr::stat_compare_means(comparisons = list(c("Primary", "Metastatic"))) +
  geom_jitter(width = 0.1, size = 0.2) + 
  theme_classic(base_size = 14)

Inferring SE levels from Visium data

Here, we will demonstrate how to infer SE levels in Visium spots using a breast cancer sample. The expression data can be accessed from: Visium_Breast_Cancer_filtered_feature_bc_matrix.h5, which was obtained from 10x Genomics datasets.

Loading data

First, download and load the expression data into R.

if(!"hdf5r" %in% installed.packages()) BiocManager::install("hdf5r")
require("hdf5r")
url <- "https://spatialecotyper.stanford.edu/inc/inc.public.vignettes.php?file=Visium_Breast_Cancer_filtered_feature_bc_matrix.h5"
download.file(url, destfile = "Visium_Breast_Cancer_filtered_feature_bc_matrix.h5", mode = "wb")
# Load Visium gene expression matrix. Rows are genes, columns are spots.
visiumdat <- Read10X_h5("Visium_Breast_Cancer_filtered_feature_bc_matrix.h5")
# normalize the expression data
visiumdat <- NormalizeData(visiumdat)

meta <- read.csv("https://spatialecotyper.stanford.edu/inc/inc.public.vignettes.php?file=Visium_Breast_Cancer_tissue_positions_list.csv", 
                 header = FALSE, row.names = 1)
colnames(meta) <- c("tissue", "row", "col", "imagerow", "imagecol")
meta <- meta[colnames(visiumdat), ]
head(meta)
##                    tissue row col imagerow imagecol
## AAACAAGTATCTCCCA-1      1  50 102    15632    17782
## AAACACCAATAACTGC-1      1  59  19    17734     6447
## AAACAGAGCGACTCCT-1      1  14  94     7079    16716
## AAACAGGGTCTATATT-1      1  47  13    14882     5637
## AAACAGTGTTCCTGGG-1      1  73  43    21069     9712
## AAACATTTCCCGGATT-1      1  61  97    18242    17091

SE deconvolution

The process of SE deconvolution for Visium data is the same as for bulk RNA-seq data described above, using the DeconvoluteSE function.

sefracs <- DeconvoluteSE(visiumdat, W = nmf_model, scale = TRUE)
head(sefracs)
##                         NonSE        SE1        SE2        SE3        SE4
## AAACAAGTATCTCCCA-1 0.05232099 0.05041149 0.10012837 0.07571055 0.12156545
## AAACACCAATAACTGC-1 0.18364314 0.09826717 0.09954508 0.15312700 0.17172335
## AAACAGAGCGACTCCT-1 0.07892363 0.18628624 0.05614935 0.14666072 0.08793592
## AAACAGGGTCTATATT-1 0.01745807 0.10309868 0.06961478 0.17559241 0.05575361
## AAACAGTGTTCCTGGG-1 0.15862994 0.02678254 0.23718907 0.01112368 0.08659772
## AAACATTTCCCGGATT-1 0.23539977 0.08190824 0.10742201 0.10051098 0.04179825
##                           SE5        SE6         SE7        SE8        SE9
## AAACAAGTATCTCCCA-1 0.23954159 0.03325773 0.094210882 0.12824410 0.10460885
## AAACACCAATAACTGC-1 0.04421240 0.03536673 0.032214566 0.07797078 0.10392979
## AAACAGAGCGACTCCT-1 0.05905418 0.08619887 0.191601732 0.02817472 0.07901464
## AAACAGGGTCTATATT-1 0.06850337 0.10667711 0.160825782 0.19010269 0.05237349
## AAACAGTGTTCCTGGG-1 0.24190862 0.07533104 0.007060598 0.06890341 0.08647337
## AAACATTTCCCGGATT-1 0.06918666 0.17898566 0.041112012 0.06446834 0.07920809

Visualizing SE abundances across Visium spots

The SpatialView function can be used to visualize SE levels across Visium spots.

## attach the spatial coordinates to the matrix for visualization
gg = as.data.frame(sefracs)
gg = cbind(meta[match(rownames(gg), rownames(meta)), c("imagerow", "imagecol")], gg)
gg$imagerow = -gg$imagerow

colors = rev(RColorBrewer::brewer.pal(11, "Spectral"))
plist = lapply(colnames(gg)[-(1:3)], function(se){
  p = SpatialView(gg, by = se, X = "imagecol", Y = "imagerow", pt.size = 0.2) + 
    coord_fixed() + scale_color_gradientn(colors = colors) + 
    theme(legend.position = "none", plot.title = element_text(hjust = 0.5)) +
    labs(title = se)
  p
})
p = patchwork::wrap_plots(plist, nrow = 3)
plot(p)

The SmoothSEAbundances function can be used to smooth SE levels across Visium spots by taking average SE levels across k nearest neighbor spots.

gg_smoothed = SmoothSEAbundances(gg[, -(1:2)], spot_coords = gg[, 1:2],
                                 include.self = TRUE, k = 7,
                                 X = "imagecol", Y = "imagerow")

colors = rev(RColorBrewer::brewer.pal(11, "Spectral"))
plist = lapply(colnames(gg_smoothed)[-(1:3)], function(se){
  p = SpatialView(gg_smoothed, by = se, 
                  X = "imagecol", Y = "imagerow", pt.size = 0.2) + 
    coord_fixed() + scale_color_gradientn(colors = colors) + 
    theme(legend.position = "none", plot.title = element_text(hjust = 0.5)) +
    labs(title = se)
  p
})
p = patchwork::wrap_plots(plist, nrow = 3)
plot(p)

Aggregating SE abundances across Visium spots

To compute sample-level SE abundances, Spatial EcoTyper calculates a weighted average of SE abundances across all Visium spots, where each spot is weighted by its inferred number of cells. The resulting SE abundances are then normalized to sum to 1.

ncells = InferNCells(visiumdat)

se_abundance = colSums(sefracs * ncells)
se_abundance = se_abundance / sum(se_abundance)

se_abundance
##      NonSE        SE1        SE2        SE3        SE4        SE5        SE6 
## 0.14040332 0.10273023 0.10877977 0.07961834 0.08949085 0.10648313 0.06536730 
##        SE7        SE8        SE9 
## 0.10937745 0.09877298 0.09897662

Session info

The session info allows users to replicate the exact environment and identify potential discrepancies in package versions or configurations that might be causing problems.

## R version 4.4.1 (2024-06-14)
## Platform: aarch64-apple-darwin20
## Running under: macOS 26.6.1
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
## LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0
## 
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
## 
## time zone: America/Los_Angeles
## tzcode source: internal
## 
## attached base packages:
## [1] parallel  stats     graphics  grDevices utils     datasets  methods  
## [8] base     
## 
## other attached packages:
##  [1] hdf5r_1.3.11          dplyr_1.2.1           SpatialEcoTyper_1.0.5
##  [4] pals_1.9              ggplot2_4.0.3         NMF_0.28             
##  [7] Biobase_2.64.0        BiocGenerics_0.50.0   cluster_2.1.6        
## [10] rngtools_1.5.2        registry_0.5-1        RANN_2.6.2           
## [13] Matrix_1.7-0          data.table_1.18.4     Seurat_5.1.0         
## [16] SeuratObject_5.0.2    sp_2.1-4             
## 
## loaded via a namespace (and not attached):
##   [1] RcppAnnoy_0.0.22       splines_4.4.1          later_1.3.2           
##   [4] tibble_3.3.1           polyclip_1.10-7        fastDummies_1.7.4     
##   [7] lifecycle_1.0.5        rstatix_0.7.2          sf_1.1-0              
##  [10] doParallel_1.0.17      globals_0.16.3         lattice_0.22-6        
##  [13] MASS_7.3-60.2          backports_1.5.0        magrittr_2.0.5        
##  [16] plotly_4.10.4          sass_0.4.9             rmarkdown_2.28        
##  [19] jquerylib_0.1.4        yaml_2.3.10            httpuv_1.6.15         
##  [22] sctransform_0.4.1      spam_2.10-0            spatstat.sparse_3.1-0 
##  [25] reticulate_1.39.0      cowplot_1.1.3          mapproj_1.2.11        
##  [28] pbapply_1.7-2          DBI_1.3.0              RColorBrewer_1.1-3    
##  [31] maps_3.4.2             abind_1.4-8            Rtsne_0.17            
##  [34] purrr_1.0.2            circlize_0.4.18        IRanges_2.38.1        
##  [37] S4Vectors_0.42.1       ggrepel_0.9.6          irlba_2.3.5.1         
##  [40] listenv_0.9.1          spatstat.utils_3.1-0   units_1.0-1           
##  [43] goftest_1.2-3          RSpectra_0.16-2        spatstat.random_3.3-1 
##  [46] fitdistrplus_1.2-1     parallelly_1.38.0      pkgdown_2.1.0         
##  [49] leiden_0.4.3.1         codetools_0.2-20       tidyselect_1.2.1      
##  [52] shape_1.4.6.1          farver_2.1.2           matrixStats_1.5.0     
##  [55] stats4_4.4.1           spatstat.explore_3.3-2 jsonlite_1.8.8        
##  [58] GetoptLong_1.1.1       e1071_1.7-16           Formula_1.2-5         
##  [61] progressr_0.14.0       ggridges_0.5.6         survival_3.6-4        
##  [64] iterators_1.0.14       systemfonts_1.1.0      foreach_1.5.2         
##  [67] tools_4.4.1            ragg_1.3.2             ica_1.0-3             
##  [70] Rcpp_1.1.2             glue_1.8.1             gridExtra_2.3.1       
##  [73] xfun_0.52              withr_3.0.3            BiocManager_1.30.25   
##  [76] fastmap_1.2.0          boot_1.3-30            spData_2.3.4          
##  [79] digest_0.6.39          R6_2.6.1               mime_0.12             
##  [82] wk_0.9.5               textshaping_0.4.0      colorspace_2.1-2      
##  [85] scattermore_1.2        tensor_1.5             dichromat_2.0-0.1     
##  [88] spatstat.data_3.1-2    tidyr_1.3.1            generics_0.1.4        
##  [91] class_7.3-22           httr_1.4.7             htmlwidgets_1.6.4     
##  [94] spdep_1.4-2            uwot_0.2.2             pkgconfig_2.0.3       
##  [97] gtable_0.3.6           ComplexHeatmap_2.20.0  lmtest_0.9-40         
## [100] S7_0.2.2               htmltools_0.5.8.1      carData_3.0-5         
## [103] dotCall64_1.1-1        clue_0.3-68            scales_1.4.0          
## [106] png_0.1-9              spatstat.univar_3.0-1  knitr_1.48            
## [109] rstudioapi_0.16.0      reshape2_1.4.4         rjson_0.2.23          
## [112] nlme_3.1-164           proxy_0.4-27           cachem_1.1.0          
## [115] zoo_1.8-12             GlobalOptions_0.1.4    stringr_1.5.1         
## [118] KernSmooth_2.23-24     miniUI_0.1.1.1         s2_1.1.9              
## [121] desc_1.4.3             pillar_1.11.1          grid_4.4.1            
## [124] vctrs_0.7.3            ggpubr_0.6.0           promises_1.3.0        
## [127] car_3.1-3              xtable_1.8-4           evaluate_0.24.0       
## [130] cli_3.6.6              compiler_4.4.1         rlang_1.3.0           
## [133] crayon_1.5.3           ggsignif_0.6.4         future.apply_1.11.2   
## [136] labeling_0.4.3         classInt_0.4-11        plyr_1.8.9            
## [139] fs_1.6.4               stringi_1.8.4          viridisLite_0.4.3     
## [142] deldir_2.0-4           gridBase_0.4-7         lazyeval_0.2.2        
## [145] spatstat.geom_3.3-2    RcppHNSW_0.6.0         patchwork_1.2.0       
## [148] bit64_4.5.2            future_1.34.0          shiny_1.9.1           
## [151] highr_0.11             ROCR_1.0-11            broom_1.0.8           
## [154] igraph_2.0.3           bslib_0.8.0            bit_4.5.0