# Bio Applied Spatial Transcriptomics

> Analyze Visium/Xenium/MERFISH spatial transcriptomics with Squidpy/Scanpy: QC, spatial neighbor graphs, Moran's I spatially variable genes, tissue-image plots. Use for spatial autocorrelation or SVG detection.

- Skill: `pavel-kravchenko/bio-applied-spatial-transcriptomics` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pavel-kravchenko/bio-applied-spatial-transcriptomics`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavel-kravchenko/bio-applied-spatial-transcriptomics/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: pavel-kravchenko (https://skillmd.com/u/pavel-kravchenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pavel-kravchenko/bio-applied-spatial-transcriptomics

---


# 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` (or `install.packages("Seurat")` in R)
- Familiarity with the standard scRNA-seq pipeline (normalize -> HVG -> PCA -> neighbors -> Leiden) — see `bio-single-cell-preprocessing` and `bio-single-cell-clustering`
- Basic AnnData structure (`bio-single-cell-data-io` / `anndata` skill)

## 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.

```python
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.

```python
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"]`.

```python
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.

```r
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 computing `pct_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; use `n_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_type` mismatch**: `sq.gr.spatial_neighbors(coord_type=...)` must match the platform (`"visium"` for hex-grid spots, `"generic"` with a `radius`/`n_neighs` for imaging-based data) or the graph will be topologically wrong.

## See Also
- `bio-single-cell-preprocessing` — QC/normalization shared with scRNA-seq
- `bio-single-cell-clustering` — Leiden/Louvain clustering used before spatial analysis
- `bio-spatial-transcriptomics-spatial-deconvolution` — dedicated cell-type deconvolution methods
- `bio-spatial-transcriptomics-spatial-domains` — spatial domain detection beyond expression clustering

