Claude Skill

alterlab-squidpy-spatial

Analyzes spatial transcriptomics with squidpy (1.8.x) on AnnData and SpatialData objects, routing platforms correctly: Visium spots use spatial_neighbors(coord_type='grid') and pair with deconvolution, while Xenium/MERFISH single-cell data use coord_type='generic'/Delaunay neighb

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download alterlab-ieu-alterlab-academic-skills-skills_bioinformatics_alterlab-squidpy-spatial-e4836c0.zip · 13 KB
Part of alterlab-ieu/alterlab-academic-skills — 94 skills

Install

skills CLI npx skills add https://github.com/AlterLab-IEU/AlterLab-Academic-Skills/tree/main/skills/bioinformatics/alterlab-squidpy-spatial
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install alterlab-ieu-alterlab-academic-skills@llmmart
Git git clone https://github.com/AlterLab-IEU/AlterLab-Academic-Skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole alterlab-ieu/alterlab-academic-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Squidpy: Spatial Transcriptomics

Squidpy is the scverse toolkit for spatially-resolved omics, built on AnnData and SpatialData. It answers questions a non-spatial scRNA-seq pipeline cannot: which cell types sit next to which (neighborhood enrichment), how cell-type pairs co-occur across distance (co-occurrence), which genes vary across tissue space (Moran's I / spatially variable genes), and what ligand-receptor signalling is plausible (ligrec). This skill does the spatial analysis; it hands non-spatial QC/clustering to alterlab-scanpy and spot deconvolution to alterlab-scvi-tools.

When to Use This Skill

Use when the request involves:

  • Spatial transcriptomics / spatially-resolved omics on Visium, Visium HD, Xenium, MERFISH/MERSCOPE, or CosMx data.
  • Building a spatial neighbor graph and running neighborhood enrichment, co-occurrence, interaction matrix, Ripley's statistics, or centrality scores.
  • Finding spatially variable genes via Moran's I (spatial_autocorr) or Sepal.
  • Ligand-receptor analysis in a spatial context (ligrec).
  • Identifying spatial niches / tissue domains (calculate_niche_*: neighborhood profile, UTAG, CellCharter, SpatialLeiden).
  • Reading platform output into AnnData/SpatialData and choosing the right coord_type for the platform.

Does NOT Trigger

Request Route to
Non-spatial scRNA-seq QC, normalization, PCA/UMAP, Leiden clustering, marker genes alterlab-scanpy
Spot deconvolution / mapping cell types to Visium spots (destVI, Tangram), probabilistic batch correction/integration alterlab-scvi-tools (see its references/models-spatial.md)
Building/slicing/concatenating .h5ad AnnData objects, layer & obsm wrangling (no spatial analysis) alterlab-anndata
RNA-velocity / trajectory dynamics alterlab-scvelo
Bulk RNA-seq differential expression from a count matrix alterlab-pydeseq2
Raw FASTQ → expression matrix (read alignment/quantification) alterlab-rnaseq-quant
Diversity / ecology statistics on a feature table alterlab-scikit-bio

If the user wants the whole pipeline ("cluster my Xenium data, then find which cell types are neighbors"), run the scanpy clustering step under alterlab-scanpy first, then return here for the spatial graph and enrichment.

The One Decision That Matters: Platform → coord_type

Squidpy's spatial graph depends on the measurement geometry. Getting coord_type wrong silently produces a meaningless graph. (All parameter behavior below is from the squidpy 1.8 sq.gr.spatial_neighbors API.)

Platform Resolution Builder Pair with
Visium spot (multi-cell, hex grid) coord_type="grid", n_neighs=6, n_rings=1..2 deconvolution → alterlab-scvi-tools
Visium HD 2/8/16 µm bins (square grid) coord_type="grid" (square lattice) binning choice up front
Xenium / MERFISH / CosMx single cell coord_type="generic", delaunay=True (or n_neighs=k) direct cell-type analysis
  • coord_type=None auto-picks "grid" only when spatial is in adata.uns with n_neighs=6 (the Visium signature); otherwise it falls back to "generic". Set coord_type explicitly rather than relying on auto-detection.
  • delaunay=True is only used when coord_type="generic"; it builds the graph from a Delaunay triangulation instead of k-nearest spots. n_rings is only used for coord_type="grid".
  • Squidpy 1.8 also exposes the builders directly — sq.gr.spatial_neighbors_grid, _knn, _radius, _delaunay — each with only the parameters that apply to it. Prefer them when you know the geometry: sq.gr.spatial_neighbors(..., delaunay=True) silently ignores n_neighs, which is a common source of "my k did nothing".

Loading Data (pick the reader for the platform)

import squidpy as sq
import scanpy as sc

# Visium (legacy spot data) — squidpy's own reader, returns AnnData
adata = sq.read.visium("path/to/visium_outs/")

# Vizgen MERSCOPE / Nanostring CosMx via squidpy readers
adata = sq.read.vizgen("path/to/merscope/", counts_file="cell_by_gene.csv",
                       meta_file="cell_metadata.csv")
adata = sq.read.nanostring("path/to/cosmx/", counts_file="exprMat_file.csv",
                           meta_file="metadata_file.csv", fov_file="fov_positions.csv")

For Xenium and Visium HD, use the spatialdata-io readers (squidpy has no sq.read.xenium) and operate on a SpatialData object:

from spatialdata_io import xenium, visium_hd, merscope
sdata = xenium("path/to/xenium_outs/")        # 10x Xenium
sdata = visium_hd("path/to/visium_hd_outs/")  # 10x Visium HD
sdata = merscope("path/to/merscope/")         # Vizgen MERSCOPE

spatialdata-io reader names are verified against the spatialdata-io stable API. Squidpy 1.8 accepts SpatialData objects directly; see references/spatialdata_io.md for the SpatialData ↔ AnnData (table) flow.

Standard Spatial Workflow

QC, normalization, HVGs, PCA, neighbors, Leiden, and sc.tl.umap are scanpy steps — run them via alterlab-scanpy. Once you have clusters / cell-type labels, do the spatial part here.

import squidpy as sq

# 1. Build the spatial neighbor graph (choose coord_type per the table above)
sq.gr.spatial_neighbors(adata, coord_type="generic", delaunay=True)   # Xenium/MERFISH
# sq.gr.spatial_neighbors(adata, coord_type="grid", n_neighs=6)       # Visium

# 2. Neighborhood enrichment: which cluster pairs are spatially adjacent?
sq.gr.nhood_enrichment(adata, cluster_key="leiden")
sq.pl.nhood_enrichment(adata, cluster_key="leiden")

# 3. Co-occurrence across distance
sq.gr.co_occurrence(adata, cluster_key="leiden")
sq.pl.co_occurrence(adata, cluster_key="leiden", clusters="0")

# 4. Spatially variable genes via Moran's I
sq.gr.spatial_autocorr(adata, mode="moran")
svgs = adata.uns["moranI"].head(20)   # ranked by Moran's I

# 5. Ligand-receptor interaction (Omnipath-backed)
sq.gr.ligrec(adata, cluster_key="leiden")
# 6. Spatial niches / tissue domains — cluster cells by their neighborhood, not
#    just their own expression. Requires a spatial graph (step 1) already built.
sq.gr.calculate_niche_neighborhood(adata, groups="leiden", resolutions=[0.5, 1.0])
# other flavors: calculate_niche_utag, calculate_niche_cellcharter,
#                calculate_niche_spatialleiden

The generic sq.gr.calculate_niche(..., flavor=...) dispatcher still works but is deprecated for removal in squidpy 1.9 — call the flavor-specific function, whose signature only carries the parameters that flavor actually uses. Niches answer a different question from nhood_enrichment: enrichment asks which labelled cell types sit together on average, niches assign each cell to a recurring tissue microenvironment.

Other graph statistics: sq.gr.interaction_matrix, sq.gr.centrality_scores, sq.gr.ripley (clustering/dispersion vs. CSR), and sq.gr.sepal (an alternative spatially-variable-gene test). Visualize tissue with sq.pl.spatial_scatter (spot/point) or sq.pl.spatial_segment (segmented cells); sq.pl.nhood_enrichment_dotplot and sq.pl.var_by_distance (expression as a function of distance to an anchor) are the other two plots worth knowing. For image features on H&E/IF, the sq.im module (process, segment, calculate_image_features) operates on an ImageContainer.

Helper script — build the graph and run the core statistics in one call:

uv run python skills/bioinformatics/alterlab-squidpy-spatial/scripts/spatial_neighborhood.py \
    clustered.h5ad --platform xenium --cluster-key leiden --out spatial_report.json

See scripts/spatial_neighborhood.py --help. It chooses coord_type from --platform, runs spatial_neighbors, nhood_enrichment, co_occurrence, and spatial_autocorr, and writes a JSON summary (top spatially variable genes + the enrichment z-score matrix) plus the updated .h5ad.

Deeper References

  • references/platform_routing.md — full platform→coord_type decision table, the n_neighs/n_rings/delaunay parameter semantics, and per-platform gotchas.
  • references/analysis_recipes.md — copy-paste recipes for each sq.gr / sq.pl function with the parameters that matter and how to read the outputs.
  • references/spatialdata_io.md — reading Xenium / Visium HD / MERSCOPE into SpatialData and getting the AnnData table squidpy operates on.

Self-Check Before Reporting

  • Did you set coord_type to match the platform (grid for Visium, generic for single-cell)? A wrong graph invalidates every downstream statistic.
  • Did clustering/QC run under alterlab-scanpy (this skill assumes labels exist)?
  • For Visium spot data, did you flag that deconvolution (alterlab-scvi-tools) is needed before cell-type-level claims — spots are multi-cell?
  • Are nhood_enrichment z-scores reported with the permutation context, not as raw counts?

Part of the AlterLab Academic Skills suite.

Files (alterlab-academic-skills)
  • evals
    • evals.json 5.6 KB
      {
        "skill": "alterlab-squidpy-spatial",
        "evals": [
          {
            "id": "xenium-neighborhood-enrichment",
            "prompt": "I have a 10x Xenium run with cells already clustered into cell types. I want to know which cell types tend to sit next to each other in the tissue — is there spatial co-localization between my tumor and immune clusters?",
            "expected_output": "Invokes alterlab-squidpy-spatial. Recognizes Xenium as single-cell resolution, builds the graph with sq.gr.spatial_neighbors(coord_type='generic', delaunay=True), then runs sq.gr.nhood_enrichment on the cluster_key and reports the permutation z-score matrix (positive z = spatial attraction/co-localization, negative = avoidance). Does NOT use coord_type='grid' (that is for Visium spots).",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "nhood_enrichment" },
              { "type": "behavior", "value": "Chooses coord_type='generic' (Delaunay) for single-cell Xenium data and reports neighborhood-enrichment z-scores with their permutation context, not raw adjacency counts." }
            ]
          },
          {
            "id": "visium-spatial-variable-genes",
            "prompt": "From my Visium slide I want the list of genes that vary across tissue space — spatially variable genes ranked by Moran's I. Walk me through it in squidpy.",
            "expected_output": "Invokes alterlab-squidpy-spatial. Builds a Visium spot graph with sq.gr.spatial_neighbors(coord_type='grid', n_neighs=6), runs sq.gr.spatial_autocorr(mode='moran'), and returns genes ranked by Moran's I from adata.uns['moranI']. Notes Visium spots are multi-cell and that cell-type-level claims require deconvolution via alterlab-scvi-tools.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "spatial_autocorr" },
              { "type": "behavior", "value": "Uses coord_type='grid' for Visium and ranks spatially variable genes by Moran's I via spatial_autocorr, flagging that Visium spots are multi-cell." }
            ]
          },
          {
            "id": "merfish-cooccurrence-distance",
            "prompt": "Using squidpy on my MERFISH data, how do I measure whether two cell types co-occur as a function of distance, and also build the neighbor graph correctly for single-molecule imaging data?",
            "expected_output": "Invokes alterlab-squidpy-spatial. Treats MERFISH as single-cell (coord_type='generic', delaunay=True or n_neighs), runs sq.gr.co_occurrence over the cluster_key to get the co-occurrence probability ratio across radial distance, and visualizes with sq.pl.co_occurrence.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "co_occurrence" }
            ]
          },
          {
            "id": "spatial-ligand-receptor",
            "prompt": "I want to run a ligand-receptor interaction analysis between my annotated clusters in spatial transcriptomics data using squidpy. Which function and how do I read the output?",
            "expected_output": "Invokes alterlab-squidpy-spatial. Uses sq.gr.ligrec with the cluster_key (Omnipath-backed permutation test of L-R co-expression between cluster pairs) and sq.pl.ligrec to visualize, explaining the means/p-values output per ligand-receptor pair per cluster pair.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "ligrec" }
            ]
          },
          {
            "id": "near-miss-alterlab-scanpy",
            "prompt": "I just loaded a Xenium dataset into AnnData. Run the standard QC, normalize, find highly variable genes, PCA, build the kNN graph, and Leiden-cluster the cells so I have cell types.",
            "expected_output": "Does NOT invoke this skill; defers to alterlab-scanpy. The request is the standard non-spatial single-cell pipeline (QC, normalization, HVGs, PCA, sc.pp.neighbors, Leiden) that produces the cluster labels; squidpy's spatial graph and neighborhood statistics come afterward. alterlab-squidpy-spatial assumes labels already exist.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "alterlab-scanpy" }
            ]
          },
          {
            "id": "near-miss-alterlab-scvi-tools",
            "prompt": "My Visium spots are mixtures of cells. I have a matching scRNA-seq reference with cell-type labels and want to deconvolve each spot into cell-type proportions using destVI.",
            "expected_output": "Does NOT invoke this skill; defers to alterlab-scvi-tools. Spot deconvolution / mapping cell-type proportions onto Visium spots (destVI, Tangram) is a deep generative modeling task covered by alterlab-scvi-tools (references/models-spatial.md). alterlab-squidpy-spatial does neighborhood/graph statistics, not deconvolution.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "alterlab-scvi-tools" }
            ]
          },
          {
            "id": "near-miss-alterlab-anndata",
            "prompt": "I have several .h5ad files from different Visium slides. Help me concatenate them into one AnnData, align the var index, and move the raw counts into a layer — no analysis yet, just the data structure.",
            "expected_output": "Does NOT invoke this skill; defers to alterlab-anndata. The request is pure AnnData object construction — concatenation, var-index alignment, layer management — with no spatial analysis. alterlab-anndata is the data-format skill; alterlab-squidpy-spatial only runs once the object is ready and labels exist.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "alterlab-anndata" }
            ]
          }
        ]
      }
      
  • references
    • analysis_recipes.md 3.8 KB
      # Squidpy Analysis Recipes
      
      Copy-paste recipes for the core `sq.gr` graph statistics and their `sq.pl`
      visualizations, with the parameters that matter and how to read each output. These
      assume an AnnData (or SpatialData `table`) with cluster/cell-type labels in
      `adata.obs[cluster_key]` and a spatial graph already built (see
      `platform_routing.md`). Function names are verified against the squidpy 1.8 API.
      
      ## 1. Spatial neighbor graph (prerequisite)
      
      ```python
      import squidpy as sq
      
      # Single-cell (Xenium / MERFISH / CosMx)
      sq.gr.spatial_neighbors(adata, coord_type="generic", delaunay=True)
      
      # Visium spots
      sq.gr.spatial_neighbors(adata, coord_type="grid", n_neighs=6, n_rings=1)
      ```
      
      Writes `adata.obsp["spatial_connectivities"]` and `adata.obsp["spatial_distances"]`,
      plus `adata.uns["spatial_neighbors"]`.
      
      ## 2. Neighborhood enrichment
      
      ```python
      sq.gr.nhood_enrichment(adata, cluster_key="leiden")
      sq.pl.nhood_enrichment(adata, cluster_key="leiden")
      z = adata.uns["leiden_nhood_enrichment"]["zscore"]   # cluster x cluster z-scores
      ```
      
      Permutation z-score per cluster pair: positive = adjacent more than chance
      (attraction), negative = avoidance. Report with the permutation context.
      
      ## 3. Co-occurrence across distance
      
      ```python
      sq.gr.co_occurrence(adata, cluster_key="leiden")
      sq.pl.co_occurrence(adata, cluster_key="leiden", clusters="0")
      ```
      
      Computes the co-occurrence probability ratio of cluster pairs as a function of
      radial distance. Use it when you care about *how adjacency decays with distance*,
      not just binary neighbor counts. Stored in `adata.uns["leiden_co_occurrence"]`.
      
      ## 4. Spatially variable genes — Moran's I
      
      ```python
      sq.gr.spatial_autocorr(adata, mode="moran")
      top = adata.uns["moranI"].sort_values("I", ascending=False).head(20)
      ```
      
      `mode="moran"` writes the `adata.uns["moranI"]` table (Moran's I, p-value, FDR per
      gene); `mode="geary"` writes `gearyC`. High positive Moran's I = strong spatial
      autocorrelation = a spatially variable gene. Restrict to highly variable genes
      first to cut runtime.
      
      ### Alternative: Sepal
      
      ```python
      sq.gr.sepal(adata, max_neighs=6)   # max_neighs=6 hex (Visium) / 4 square grid
      ```
      
      `sq.gr.sepal` identifies spatially variable genes via a diffusion-based score; it is
      an alternative to Moran's I, useful as a cross-check.
      
      ## 5. Ligand-receptor analysis
      
      ```python
      sq.gr.ligrec(adata, cluster_key="leiden")
      sq.pl.ligrec(adata, cluster_key="leiden")
      ```
      
      Tests ligand-receptor co-expression between cluster pairs using an Omnipath-backed
      interaction database (a permutation test in the spirit of CellPhoneDB). Returns
      means and p-values per L-R pair per cluster pair.
      
      ## 6. Other graph statistics
      
      ```python
      sq.gr.interaction_matrix(adata, cluster_key="leiden")   # raw inter-cluster edge counts
      sq.gr.centrality_scores(adata, cluster_key="leiden")    # degree / closeness / clustering per cluster
      sq.gr.ripley(adata, cluster_key="leiden", mode="L")     # Ripley's L vs. complete spatial randomness
      ```
      
      `ripley` (modes `"F"`, `"G"`, `"L"`) tests whether a cluster is more clustered or
      dispersed than complete spatial randomness. `centrality_scores` summarizes each
      cluster's position in the spatial graph.
      
      ## 7. Visualizing tissue
      
      ```python
      sq.pl.spatial_scatter(adata, color="leiden")    # spot/point overlay (Visium, points)
      sq.pl.spatial_segment(adata, color="leiden", seg_cell_id="cell_id")  # segmented cells (Xenium)
      ```
      
      ## 8. Image features (optional, H&E / IF)
      
      ```python
      from squidpy.im import ImageContainer
      img = ImageContainer("tissue_image.tif")
      sq.im.process(img, layer="image", method="smooth")
      sq.im.segment(img, layer="image", method="watershed")
      sq.im.calculate_image_features(adata, img, features="summary")
      ```
      
      The `sq.im` module operates on an `ImageContainer` and writes per-observation image
      features back into `adata.obs` for joint expression+morphology analysis. Skip this
      entirely for pure transcriptomic neighborhood work.
      
    • platform_routing.md 3.6 KB
      # Platform → coord_type Routing
      
      The single most consequential choice in a squidpy analysis is how the spatial
      neighbor graph is built, because every downstream statistic (`nhood_enrichment`,
      `co_occurrence`, `spatial_autocorr`, `ripley`, `interaction_matrix`,
      `centrality_scores`) consumes that graph. The right builder depends on the
      measurement geometry of the platform. All parameter semantics below are from the
      squidpy 1.8 `sq.gr.spatial_neighbors` API.
      
      ## Decision table
      
      | Platform | Spatial unit | Geometry | `coord_type` | Key args | Notes |
      |----------|--------------|----------|--------------|----------|-------|
      | Visium | spot (10–100s of cells) | hexagonal grid | `"grid"` | `n_neighs=6`, `n_rings=1` (or `2`) | 6 immediate neighbors per spot; raise `n_rings` for wider neighborhoods |
      | Visium HD | 2/8/16 µm bin | square grid | `"grid"` | `n_neighs=4` or `6` | square lattice; pick the bin size before graphing |
      | Xenium | single cell | irregular points | `"generic"` | `delaunay=True` | Delaunay triangulation gives adjacency without a k cutoff |
      | MERFISH / MERSCOPE | single cell | irregular points | `"generic"` | `delaunay=True` or `n_neighs=k` | k-NN (`delaunay=False`) when you want a fixed degree |
      | CosMx (Nanostring) | single cell | irregular points | `"generic"` | `delaunay=True` | same as Xenium/MERFISH |
      
      ## Parameter semantics (squidpy 1.8 `sq.gr.spatial_neighbors`)
      
      - **`coord_type`** — `"grid"`, `"generic"`, or `None`.
        - `None` auto-selects `"grid"` **only** when `spatial` is present in `adata.uns`
          with `n_neighs == 6` (the Visium signature); otherwise it uses `"generic"`.
        - Do not rely on auto-detection — pass `coord_type` explicitly so the geometry is
          unambiguous and reproducible.
      - **`n_neighs`** (default `6`) — for `"grid"`, the number of neighboring tiles; for
        `"generic"`, the number of nearest neighbors, applied only when `delaunay=False`.
      - **`n_rings`** (default `1`) — number of rings of neighbors; **only used for
        `coord_type="grid"`**. `n_rings=2` includes second-shell spots.
      - **`delaunay`** (default `False`) — build the graph from a Delaunay triangulation;
        **only used for `coord_type="generic"`**. Preferred for single-cell platforms
        because it adapts to local density without a fixed `k`.
      
      ## Per-platform gotchas
      
      - **Visium spots are multi-cell.** Any cell-type-level claim from raw Visium needs
        **deconvolution first** (destVI / Tangram via `alterlab-scvi-tools`,
        `references/models-spatial.md`). Squidpy graph statistics on undeconvolved spots
        describe *spot-cluster* adjacency, not single-cell adjacency.
      - **Single-cell platforms (Xenium/MERFISH/CosMx)** carry true per-cell coordinates,
        so `nhood_enrichment` and `co_occurrence` are directly interpretable at cell-type
        resolution — no deconvolution step.
      - **Visium HD** must have its bin size chosen before analysis; 2 µm bins are near
        single-cell but sparse, 8/16 µm bins trade resolution for counts.
      - **Clusters/labels must already exist.** `nhood_enrichment`, `co_occurrence`,
        `interaction_matrix`, `centrality_scores`, and `ligrec` all take a `cluster_key`
        pointing at a categorical `adata.obs` column produced upstream (Leiden via
        `alterlab-scanpy`, or imported cell-type annotations).
      
      ## Reading `nhood_enrichment` output
      
      `sq.gr.nhood_enrichment` runs a permutation test and stores a **z-score** matrix in
      `adata.uns[f"{cluster_key}_nhood_enrichment"]["zscore"]`. Positive z = the two
      clusters are adjacent more often than expected under the permuted null (attraction);
      negative z = avoidance. Always report z-scores with that permutation context, never
      raw adjacency counts.
      
    • spatialdata_io.md 2.8 KB
      # Reading Modern Platforms with spatialdata-io
      
      Squidpy 1.8 requires `spatialdata>=0.7.2` and accepts `SpatialData` objects, which
      is the recommended representation for single-cell-resolution platforms (Xenium,
      Visium HD) that carry images, shapes, and points alongside the expression table.
      Squidpy's own `sq.read` module covers `visium`, `vizgen`, and `nanostring` only —
      **there is no `sq.read.xenium`** — so Xenium and Visium HD come in through
      `spatialdata-io`. Reader names below are verified against the spatialdata-io stable
      API.
      
      ## Readers
      
      ```python
      from spatialdata_io import xenium, visium, visium_hd, merscope
      
      sdata = xenium("path/to/xenium_outs/")        # 10x Genomics Xenium
      sdata = visium("path/to/visium_outs/")        # 10x Genomics Visium (spatialdata form)
      sdata = visium_hd("path/to/visium_hd_outs/")  # 10x Genomics Visium HD
      sdata = merscope("path/to/merscope/")         # Vizgen MERSCOPE
      ```
      
      Each returns a `SpatialData` object — a container of elements: `images`, `labels`
      (segmentation masks), `shapes` (cell/nucleus boundaries), `points` (transcript
      locations), and `tables` (the AnnData expression table(s)).
      
      ## Getting the AnnData table squidpy operates on
      
      Squidpy graph functions run on the expression `table` (an AnnData) inside the
      `SpatialData` object. Access it via the `tables` mapping:
      
      ```python
      adata = sdata.tables["table"]            # the AnnData squidpy analyses use
      # ... run scanpy QC/clustering (alterlab-scanpy) to populate adata.obs["leiden"] ...
      import squidpy as sq
      sq.gr.spatial_neighbors(adata, coord_type="generic", delaunay=True)  # Xenium = single cell
      sq.gr.nhood_enrichment(adata, cluster_key="leiden")
      ```
      
      The exact key under `tables` depends on the reader/dataset; inspect `sdata.tables`
      (a dict-like mapping) to find it. Spatial coordinates live in `adata.obsm["spatial"]`,
      which `spatial_neighbors` reads by default (`spatial_key="spatial"`).
      
      ## When to stay in AnnData vs. SpatialData
      
      - **Visium (legacy spot data):** `sq.read.visium(...)` returns a plain AnnData that
        already has `adata.uns["spatial"]` and `adata.obsm["spatial"]`; you can skip
        SpatialData entirely. Use `coord_type="grid"`.
      - **Xenium / Visium HD / large MERSCOPE:** prefer `spatialdata-io` + `SpatialData`
        so images, segmentation shapes, and transcript points stay aligned; extract the
        `table` for squidpy graph statistics. Use `coord_type="generic"` (Xenium/MERSCOPE)
        or `"grid"` (Visium HD bins) per `platform_routing.md`.
      
      ## Hand-offs
      
      - Non-spatial QC, normalization, PCA/UMAP, Leiden clustering, marker genes on the
        extracted `table` → `alterlab-scanpy`.
      - Spot deconvolution for Visium (destVI / Tangram) → `alterlab-scvi-tools`
        (`references/models-spatial.md`).
      - Pure `.h5ad` AnnData construction/slicing with no spatial analysis →
        `alterlab-anndata`.
      
  • scripts
    • spatial_neighborhood.py 6.4 KB
      #!/usr/bin/env python3
      """
      Spatial neighborhood analysis for squidpy (1.8.x).
      
      Given a clustered AnnData (.h5ad) that already carries a categorical cluster /
      cell-type column in ``adata.obs`` (produce it upstream with alterlab-scanpy), this
      script:
      
        1. builds the spatial neighbor graph with the *correct* ``coord_type`` for the
           platform (the one decision that invalidates everything downstream if wrong),
        2. runs ``sq.gr.nhood_enrichment`` (permutation z-scores per cluster pair),
        3. runs ``sq.gr.co_occurrence`` (adjacency vs. radial distance),
        4. runs ``sq.gr.spatial_autocorr`` (Moran's I -> spatially variable genes),
      
      then writes a compact JSON summary and (optionally) the updated .h5ad.
      
      Platform -> coord_type (squidpy 1.8 sq.gr.spatial_neighbors semantics):
        visium / visium_hd        -> coord_type="grid"     (n_neighs / n_rings)
        xenium / merfish / cosmx  -> coord_type="generic"  (delaunay=True)
      
      This runs locally and sends no data anywhere. squidpy + scanpy must be installed
      (``uv run --with squidpy python ...`` or a project env). For large single-cell
      panels this is a good candidate to run on local compute rather than burning API
      calls.
      
      Usage:
          uv run python spatial_neighborhood.py clustered.h5ad \
              --platform xenium --cluster-key leiden --out spatial_report.json
      """
      from __future__ import annotations
      
      import argparse
      import json
      import sys
      
      # Platform -> (coord_type, kwargs for sq.gr.spatial_neighbors)
      GRID_PLATFORMS = {"visium", "visium_hd"}
      GENERIC_PLATFORMS = {"xenium", "merfish", "merscope", "cosmx"}
      
      
      def graph_kwargs(platform: str, n_neighs: int, n_rings: int):
          """Return the spatial_neighbors kwargs for the platform."""
          platform = platform.lower()
          if platform in GRID_PLATFORMS:
              return {"coord_type": "grid", "n_neighs": n_neighs, "n_rings": n_rings}
          if platform in GENERIC_PLATFORMS:
              # Delaunay adapts to local density without a fixed k -> preferred single-cell.
              return {"coord_type": "generic", "delaunay": True}
          raise ValueError(
              f"unknown platform {platform!r}; "
              f"expected one of {sorted(GRID_PLATFORMS | GENERIC_PLATFORMS)}"
          )
      
      
      def main(argv: list[str] | None = None) -> int:
          p = argparse.ArgumentParser(
              description="Build a squidpy spatial graph and run core neighborhood statistics."
          )
          p.add_argument("h5ad", help="Path to a clustered AnnData .h5ad (labels in obs).")
          p.add_argument(
              "--platform",
              required=True,
              help="visium | visium_hd | xenium | merfish | merscope | cosmx",
          )
          p.add_argument(
              "--cluster-key",
              default="leiden",
              help="Categorical obs column with cluster / cell-type labels (default: leiden).",
          )
          p.add_argument("--n-neighs", type=int, default=6, help="grid n_neighs (default: 6).")
          p.add_argument("--n-rings", type=int, default=1, help="grid n_rings (default: 1).")
          p.add_argument(
              "--moran-top", type=int, default=20, help="How many top SVGs to report (default: 20)."
          )
          p.add_argument("--out", default=None, help="Write JSON summary here (default: stdout).")
          p.add_argument(
              "--write-h5ad", default=None, help="Optionally write the updated AnnData here."
          )
          args = p.parse_args(argv)
      
          try:
              import scanpy as sc
              import squidpy as sq
          except ImportError as exc:  # pragma: no cover - dependency guard
              print(
                  f"ERROR: this script needs scanpy + squidpy installed ({exc}). "
                  "Try: uv run --with squidpy python " + __file__,
                  file=sys.stderr,
              )
              return 2
      
          try:
              kwargs = graph_kwargs(args.platform, args.n_neighs, args.n_rings)
          except ValueError as exc:
              print(f"ERROR: {exc}", file=sys.stderr)
              return 2
      
          adata = sc.read_h5ad(args.h5ad)
      
          if args.cluster_key not in adata.obs:
              print(
                  f"ERROR: cluster key {args.cluster_key!r} not found in adata.obs "
                  f"(have: {list(adata.obs.columns)}). Cluster upstream with alterlab-scanpy.",
                  file=sys.stderr,
              )
              return 2
      
          # 1. spatial graph (the consequential choice)
          sq.gr.spatial_neighbors(adata, **kwargs)
      
          # 2. neighborhood enrichment (permutation z-scores)
          sq.gr.nhood_enrichment(adata, cluster_key=args.cluster_key)
          nhood = adata.uns.get(f"{args.cluster_key}_nhood_enrichment", {})
          zscore = nhood.get("zscore")
          categories = list(adata.obs[args.cluster_key].cat.categories) if hasattr(
              adata.obs[args.cluster_key], "cat"
          ) else sorted(set(map(str, adata.obs[args.cluster_key])))
      
          # 3. co-occurrence across distance
          sq.gr.co_occurrence(adata, cluster_key=args.cluster_key)
      
          # 4. spatially variable genes via Moran's I
          sq.gr.spatial_autocorr(adata, mode="moran")
          moran = adata.uns.get("moranI")
          top_svgs = []
          if moran is not None:
              moran_sorted = moran.sort_values("I", ascending=False).head(args.moran_top)
              for gene, row in moran_sorted.iterrows():
                  entry = {"gene": str(gene), "morans_I": float(row["I"])}
                  if "pval_norm" in moran_sorted.columns:
                      entry["pval_norm"] = float(row["pval_norm"])
                  if "pval_norm_fdr_bh" in moran_sorted.columns:
                      entry["fdr_bh"] = float(row["pval_norm_fdr_bh"])
                  top_svgs.append(entry)
      
          report = {
              "tool": "alterlab-squidpy-spatial/spatial_neighborhood.py",
              "version": "1.0.0",
              "input": args.h5ad,
              "platform": args.platform.lower(),
              "spatial_neighbors_kwargs": kwargs,
              "cluster_key": args.cluster_key,
              "n_obs": int(adata.n_obs),
              "n_vars": int(adata.n_vars),
              "clusters": categories,
              "nhood_enrichment_zscore": (
                  zscore.tolist() if zscore is not None else None
              ),
              "top_spatially_variable_genes": top_svgs,
              "notes": (
                  "z-scores are permutation-test based: positive = spatial attraction, "
                  "negative = avoidance. For Visium spot data, deconvolve "
                  "(alterlab-scvi-tools) before cell-type-level claims."
              ),
          }
      
          if args.write_h5ad:
              adata.write(args.write_h5ad)
              report["written_h5ad"] = args.write_h5ad
      
          payload = json.dumps(report, indent=2)
          if args.out:
              with open(args.out, "w") as fh:
                  fh.write(payload)
              print(f"Wrote {args.out}", file=sys.stderr)
          else:
              print(payload)
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
  • SKILL.md 10.2 KB
    ---
    name: alterlab-squidpy-spatial
    description: "Analyzes spatial transcriptomics with squidpy (1.8.x) on AnnData and SpatialData objects, routing platforms correctly: Visium spots use spatial_neighbors(coord_type='grid') and pair with deconvolution, while Xenium/MERFISH single-cell data use coord_type='generic'/Delaunay neighbors and spatialdata-io readers (xenium, visium_hd, merscope). Runs sq.gr.spatial_neighbors, nhood_enrichment, co_occurrence, spatial_autocorr (Moran's I for spatially variable genes), ripley, and ligrec. Use when the user wants spatial transcriptomics, squidpy, Visium/Xenium/MERFISH analysis, neighborhood enrichment, co-occurrence, or spatially variable genes; QC/clustering uses alterlab-scanpy and spot deconvolution (destVI/Tangram) uses alterlab-scvi-tools. Part of the AlterLab Academic Skills suite."
    license: MIT
    allowed-tools: Read Write Edit Bash(python:*) Bash(uv:*)
    compatibility: "Self-contained — runs under `uv run python` with squidpy (1.8.x, current 1.8.3 as of 2026-09; needs spatialdata>=0.7.2, spatialdata-plot>=0.3.3, scanpy>=1.9.3, anndata>=0.9, Python>=3.12) installed; no API key or account required."
    metadata:
        skill-author: AlterLab
        version: "1.1.0"
        last_updated: "2026-09-23"
    ---
    
    # Squidpy: Spatial Transcriptomics
    
    Squidpy is the scverse toolkit for spatially-resolved omics, built on AnnData and
    SpatialData. It answers questions a non-spatial scRNA-seq pipeline cannot: *which
    cell types sit next to which* (neighborhood enrichment), *how cell-type pairs
    co-occur across distance* (co-occurrence), *which genes vary across tissue space*
    (Moran's I / spatially variable genes), and *what ligand-receptor signalling is
    plausible* (ligrec). This skill does the spatial analysis; it hands non-spatial
    QC/clustering to `alterlab-scanpy` and spot deconvolution to `alterlab-scvi-tools`.
    
    ## When to Use This Skill
    
    Use when the request involves:
    - Spatial transcriptomics / spatially-resolved omics on **Visium, Visium HD, Xenium,
      MERFISH/MERSCOPE, or CosMx** data.
    - Building a **spatial neighbor graph** and running **neighborhood enrichment**,
      **co-occurrence**, **interaction matrix**, **Ripley's statistics**, or
      **centrality scores**.
    - Finding **spatially variable genes** via Moran's I (`spatial_autocorr`) or Sepal.
    - **Ligand-receptor** analysis in a spatial context (`ligrec`).
    - Identifying **spatial niches / tissue domains** (`calculate_niche_*`: neighborhood
      profile, UTAG, CellCharter, SpatialLeiden).
    - Reading platform output into AnnData/SpatialData and choosing the right
      `coord_type` for the platform.
    
    ### Does NOT Trigger
    
    | Request | Route to |
    |---------|----------|
    | Non-spatial scRNA-seq QC, normalization, PCA/UMAP, Leiden clustering, marker genes | `alterlab-scanpy` |
    | Spot **deconvolution** / mapping cell types to Visium spots (destVI, Tangram), probabilistic batch correction/integration | `alterlab-scvi-tools` (see its `references/models-spatial.md`) |
    | Building/slicing/concatenating `.h5ad` AnnData objects, layer & obsm wrangling (no spatial analysis) | `alterlab-anndata` |
    | RNA-velocity / trajectory dynamics | `alterlab-scvelo` |
    | Bulk RNA-seq **differential expression** from a count matrix | `alterlab-pydeseq2` |
    | Raw FASTQ → expression matrix (read alignment/quantification) | `alterlab-rnaseq-quant` |
    | Diversity / ecology statistics on a feature table | `alterlab-scikit-bio` |
    
    If the user wants the *whole* pipeline ("cluster my Xenium data, then find which
    cell types are neighbors"), run the scanpy clustering step under `alterlab-scanpy`
    first, then return here for the spatial graph and enrichment.
    
    ## The One Decision That Matters: Platform → coord_type
    
    Squidpy's spatial graph depends on the measurement geometry. Getting `coord_type`
    wrong silently produces a meaningless graph. (All parameter behavior below is from
    the squidpy 1.8 `sq.gr.spatial_neighbors` API.)
    
    | Platform | Resolution | Builder | Pair with |
    |----------|-----------|---------|-----------|
    | **Visium** | spot (multi-cell, hex grid) | `coord_type="grid"`, `n_neighs=6`, `n_rings=1..2` | deconvolution → `alterlab-scvi-tools` |
    | **Visium HD** | 2/8/16 µm bins (square grid) | `coord_type="grid"` (square lattice) | binning choice up front |
    | **Xenium / MERFISH / CosMx** | single cell | `coord_type="generic"`, `delaunay=True` (or `n_neighs=k`) | direct cell-type analysis |
    
    - `coord_type=None` auto-picks `"grid"` only when `spatial` is in `adata.uns` with
      `n_neighs=6` (the Visium signature); otherwise it falls back to `"generic"`. **Set
      `coord_type` explicitly** rather than relying on auto-detection.
    - `delaunay=True` is only used when `coord_type="generic"`; it builds the graph from
      a Delaunay triangulation instead of k-nearest spots. `n_rings` is only used for
      `coord_type="grid"`.
    - Squidpy 1.8 also exposes the builders directly — `sq.gr.spatial_neighbors_grid`,
      `_knn`, `_radius`, `_delaunay` — each with only the parameters that apply to it.
      Prefer them when you know the geometry: `sq.gr.spatial_neighbors(..., delaunay=True)`
      silently ignores `n_neighs`, which is a common source of "my k did nothing".
    
    ## Loading Data (pick the reader for the platform)
    
    ```python
    import squidpy as sq
    import scanpy as sc
    
    # Visium (legacy spot data) — squidpy's own reader, returns AnnData
    adata = sq.read.visium("path/to/visium_outs/")
    
    # Vizgen MERSCOPE / Nanostring CosMx via squidpy readers
    adata = sq.read.vizgen("path/to/merscope/", counts_file="cell_by_gene.csv",
                           meta_file="cell_metadata.csv")
    adata = sq.read.nanostring("path/to/cosmx/", counts_file="exprMat_file.csv",
                               meta_file="metadata_file.csv", fov_file="fov_positions.csv")
    ```
    
    For **Xenium and Visium HD**, use the **`spatialdata-io`** readers (squidpy has no
    `sq.read.xenium`) and operate on a `SpatialData` object:
    
    ```python
    from spatialdata_io import xenium, visium_hd, merscope
    sdata = xenium("path/to/xenium_outs/")        # 10x Xenium
    sdata = visium_hd("path/to/visium_hd_outs/")  # 10x Visium HD
    sdata = merscope("path/to/merscope/")         # Vizgen MERSCOPE
    ```
    
    `spatialdata-io` reader names are verified against the spatialdata-io stable API.
    Squidpy 1.8 accepts SpatialData objects directly; see
    `references/spatialdata_io.md` for the SpatialData ↔ AnnData (table) flow.
    
    ## Standard Spatial Workflow
    
    QC, normalization, HVGs, PCA, neighbors, Leiden, and `sc.tl.umap` are **scanpy**
    steps — run them via `alterlab-scanpy`. Once you have clusters / cell-type labels,
    do the spatial part here.
    
    ```python
    import squidpy as sq
    
    # 1. Build the spatial neighbor graph (choose coord_type per the table above)
    sq.gr.spatial_neighbors(adata, coord_type="generic", delaunay=True)   # Xenium/MERFISH
    # sq.gr.spatial_neighbors(adata, coord_type="grid", n_neighs=6)       # Visium
    
    # 2. Neighborhood enrichment: which cluster pairs are spatially adjacent?
    sq.gr.nhood_enrichment(adata, cluster_key="leiden")
    sq.pl.nhood_enrichment(adata, cluster_key="leiden")
    
    # 3. Co-occurrence across distance
    sq.gr.co_occurrence(adata, cluster_key="leiden")
    sq.pl.co_occurrence(adata, cluster_key="leiden", clusters="0")
    
    # 4. Spatially variable genes via Moran's I
    sq.gr.spatial_autocorr(adata, mode="moran")
    svgs = adata.uns["moranI"].head(20)   # ranked by Moran's I
    
    # 5. Ligand-receptor interaction (Omnipath-backed)
    sq.gr.ligrec(adata, cluster_key="leiden")
    ```
    
    ```python
    # 6. Spatial niches / tissue domains — cluster cells by their neighborhood, not
    #    just their own expression. Requires a spatial graph (step 1) already built.
    sq.gr.calculate_niche_neighborhood(adata, groups="leiden", resolutions=[0.5, 1.0])
    # other flavors: calculate_niche_utag, calculate_niche_cellcharter,
    #                calculate_niche_spatialleiden
    ```
    
    The generic `sq.gr.calculate_niche(..., flavor=...)` dispatcher still works but is
    deprecated for removal in squidpy 1.9 — call the flavor-specific function, whose
    signature only carries the parameters that flavor actually uses. Niches answer a
    different question from `nhood_enrichment`: enrichment asks *which labelled cell types
    sit together on average*, niches assign *each cell to a recurring tissue
    microenvironment*.
    
    Other graph statistics: `sq.gr.interaction_matrix`, `sq.gr.centrality_scores`,
    `sq.gr.ripley` (clustering/dispersion vs. CSR), and `sq.gr.sepal` (an alternative
    spatially-variable-gene test). Visualize tissue with `sq.pl.spatial_scatter`
    (spot/point) or `sq.pl.spatial_segment` (segmented cells);
    `sq.pl.nhood_enrichment_dotplot` and `sq.pl.var_by_distance` (expression as a function
    of distance to an anchor) are the other two plots worth knowing. For image features on
    H&E/IF, the `sq.im` module (`process`, `segment`, `calculate_image_features`)
    operates on an `ImageContainer`.
    
    **Helper script** — build the graph and run the core statistics in one call:
    
    ```bash
    uv run python skills/bioinformatics/alterlab-squidpy-spatial/scripts/spatial_neighborhood.py \
        clustered.h5ad --platform xenium --cluster-key leiden --out spatial_report.json
    ```
    
    See `scripts/spatial_neighborhood.py --help`. It chooses `coord_type` from
    `--platform`, runs `spatial_neighbors`, `nhood_enrichment`, `co_occurrence`, and
    `spatial_autocorr`, and writes a JSON summary (top spatially variable genes + the
    enrichment z-score matrix) plus the updated `.h5ad`.
    
    ## Deeper References
    
    - `references/platform_routing.md` — full platform→`coord_type` decision table, the
      `n_neighs`/`n_rings`/`delaunay` parameter semantics, and per-platform gotchas.
    - `references/analysis_recipes.md` — copy-paste recipes for each `sq.gr` / `sq.pl`
      function with the parameters that matter and how to read the outputs.
    - `references/spatialdata_io.md` — reading Xenium / Visium HD / MERSCOPE into
      SpatialData and getting the AnnData `table` squidpy operates on.
    
    ## Self-Check Before Reporting
    
    - Did you set `coord_type` to match the platform (grid for Visium, generic for
      single-cell)? A wrong graph invalidates every downstream statistic.
    - Did clustering/QC run under `alterlab-scanpy` (this skill assumes labels exist)?
    - For Visium spot data, did you flag that **deconvolution** (`alterlab-scvi-tools`)
      is needed before cell-type-level claims — spots are multi-cell?
    - Are `nhood_enrichment` z-scores reported with the permutation context, not as raw
      counts?
    
    Part of the AlterLab Academic Skills suite.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related