Spatial Transcriptomics
When to Use
- Loading or QC-ing 10x Visium, Xenium, MERFISH, seqFISH+, Slide-seq, or Stereo-seq data with spatial coordinates.
- Detecting spatially variable genes (SVGs) or testing spatial autocorrelation (Moran's I, Geary's C).
- Building a spatial neighbor graph from spot/cell coordinates for downstream spatial statistics.
- Visualizing clusters or gene expression overlaid on a tissue histology image.
- Deconvolving multi-cell Visium spots into cell-type proportions using an scRNA-seq reference.
Version Compatibility
- Python: squidpy >=1.4, scanpy >=1.10, anndata >=0.10, Python >=3.10
- R: Seurat >=5.0 (
Load10X_Spatial,SCTransform,FindSpatiallyVariableFeatures)
Prerequisites
pip install squidpy scanpy leidenalg(orinstall.packages("Seurat")in R)- Familiarity with the standard scRNA-seq pipeline (normalize -> HVG -> PCA -> neighbors -> Leiden) — see
bio-single-cell-preprocessingandbio-single-cell-clustering - Basic AnnData structure (
bio-single-cell-data-io/anndataskill)
Platform Comparison
| Platform | Resolution | Type | Key feature |
|---|---|---|---|
| 10x Visium | 55 um (~10-20 cells/spot) | Capture-based | H&E co-registration |
| 10x Xenium | ~10 um (single-cell) | In situ sequencing | Targeted ~400 genes |
| MERFISH | Sub-cellular | In situ imaging | Error-robust barcoding |
| Slide-seq v2 | ~10 um | Capture-based | High-res bead array |
| Stereo-seq | 500 nm | Capture-based | Ultra-high resolution |
AnnData spatial slots: adata.obsm["spatial"] (n_spots, 2 pixel coords), adata.uns["spatial"] (tissue image + scale factors), adata.obsp["spatial_connectivities"] (spatial neighbor graph, after sq.gr.spatial_neighbors).
Core Workflow
Goal: Load a Visium dataset, QC-filter spots, and cluster on expression (ignoring spatial coordinates). Approach: reuse the standard scanpy pipeline — spatial coordinates only matter starting at the neighbor-graph step.
import scanpy as sc
import squidpy as sq
def load_and_cluster_visium(min_counts=200, min_cells=5, n_top_genes=2000, resolution=0.5):
"""Load the public Visium mouse-brain dataset, QC-filter, normalize, and Leiden-cluster.
Returns the processed AnnData with `obs["leiden"]` cluster labels.
"""
adata = sq.datasets.visium_hne_adata()
# QC metrics (mitochondrial fraction uses mouse "mt-" prefix; use "MT-" for human)
adata.var["mt"] = adata.var_names.str.startswith("mt-")
sc.pp.calculate_qc_metrics(adata, qc_vars=["mt"], inplace=True)
sc.pp.filter_cells(adata, min_counts=min_counts)
sc.pp.filter_genes(adata, min_cells=min_cells)
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
sc.pp.highly_variable_genes(adata, n_top_genes=n_top_genes, subset=False)
sc.pp.pca(adata, n_comps=50, use_highly_variable=True)
sc.pp.neighbors(adata, n_pcs=30)
sc.tl.leiden(adata, resolution=resolution, key_added="leiden")
return adata
Goal: Build the spatial neighbor graph and detect spatially variable genes (SVGs).
Approach: sq.gr.spatial_neighbors connects spots by physical proximity, then sq.gr.spatial_autocorr(mode="moran") computes Moran's I with a permutation p-value per gene.
def detect_spatially_variable_genes(adata, n_genes=500, n_perms=100, n_rings=1):
"""Build a spatial neighbor graph and rank genes by Moran's I spatial autocorrelation.
n_perms=100 gives approximate p-values for exploration; use n_perms=1000 for
publication-quality results (permutation testing is required because spatial
data violates the independence assumption of parametric tests).
"""
sq.gr.spatial_neighbors(adata, coord_type="visium", n_rings=n_rings)
sq.gr.spatial_autocorr(
adata, mode="moran", genes=adata.var_names[:n_genes], n_perms=n_perms
)
moran = adata.uns["moranI"].sort_values("I", ascending=False)
return moran # columns: I, pval_norm, pval_sim, pval_sim_fdr_bh (top I ~= +1 -> spatial clusters)
Goal: Visualize Leiden clusters and top SVGs overlaid on the tissue image.
Approach: sq.pl.spatial_scatter renders points on top of the H&E image stored in adata.uns["spatial"].
def plot_clusters_and_svgs(adata, moran, n_top=6):
"""Plot spatial clusters and the top-N spatially variable genes on the tissue image."""
sq.pl.spatial_scatter(adata, color="leiden", size=1.4)
top_svgs = moran.head(n_top).index.tolist()
sq.pl.spatial_scatter(adata, color=top_svgs, ncols=3, size=1.4, img_alpha=0.4)
return top_svgs
R Equivalent (Seurat)
Goal: Load a Visium sample, cluster, and find spatially variable features in R.
Approach: Seurat's spatial workflow mirrors the Scanpy/Squidpy one, with FindSpatiallyVariableFeatures(method = "moransi") as the SVG-detection step.
library(Seurat)
## Load 10x Space Ranger output (expects filtered_feature_bc_matrix.h5 + spatial/ dir)
brain <- Load10X_Spatial(data.dir = "visium_sample/")
brain <- SCTransform(brain, assay = "Spatial", verbose = FALSE)
brain <- RunPCA(brain, assay = "SCT", verbose = FALSE)
brain <- FindNeighbors(brain, reduction = "pca", dims = 1:30)
brain <- FindClusters(brain, resolution = 0.5)
brain <- RunUMAP(brain, reduction = "pca", dims = 1:30)
SpatialDimPlot(brain, label = TRUE, label.size = 3)
## Moran's I-based spatially variable genes (equivalent to sq.gr.spatial_autocorr)
brain <- FindSpatiallyVariableFeatures(
brain, assay = "SCT", features = VariableFeatures(brain)[1:500],
selection.method = "moransi"
)
top_svgs <- head(SpatiallyVariableFeatures(brain, selection.method = "moransi"), 6)
SpatialFeaturePlot(brain, features = top_svgs, ncol = 3)
Cell-type Deconvolution (Visium)
Each Visium spot captures ~10-20 cells; deconvolution estimates per-spot cell-type composition from an scRNA-seq reference.
| Tool | Approach | Reference needed |
|---|---|---|
| RCTD (spacexr, R) | Poisson GLM | Yes (scRNA-seq) |
| cell2location | Negative binomial + hierarchical Bayesian | Yes |
| Stereoscope | Probabilistic (scvi-tools based) | Yes |
| NNLS | Non-negative least squares on marker genes | Yes (marker genes) |
Result is typically stored as adata.obsm["cell_type_proportions"] (n_spots x n_types), plottable with sq.pl.spatial_scatter(adata, color=proportions.columns).
Pitfalls
- Mitochondrial prefix: use
"mt-"for mouse,"MT-"for human when computingpct_counts_mt— wrong case silently zeroes out the QC metric. - Batch effects: check for batch confounding across sections/slides before interpreting spatial patterns as biological.
- Permutation count:
n_perms=100(Python) gives approximate p-values only; usen_perms=1000+ (or R equivalent) for publication. - Spot vs. cell resolution: Visium/Slide-seq spots contain multiple cells — cluster labels and SVGs describe local tissue neighborhoods, not single cells, unless deconvolved.
- Deconvolution reference quality: results are highly sensitive to how well the scRNA-seq reference's cell types match the tissue and technology.
coord_typemismatch:sq.gr.spatial_neighbors(coord_type=...)must match the platform ("visium"for hex-grid spots,"generic"with aradius/n_neighsfor imaging-based data) or the graph will be topologically wrong.
See Also
bio-single-cell-preprocessing— QC/normalization shared with scRNA-seqbio-single-cell-clustering— Leiden/Louvain clustering used before spatial analysisbio-spatial-transcriptomics-spatial-deconvolution— dedicated cell-type deconvolution methodsbio-spatial-transcriptomics-spatial-domains— spatial domain detection beyond expression clustering