Spatial omics workflows with SpatialData and Squidpy alongside scanpy, anndata, and napari-viz. Use when working with Visium, Xenium, CosMx, MERFISH, Slide-seq, spatial transcriptomics, spatial proteomics, tissue images linked to AnnData, spatial neighbor graphs, spatial autocorrelation, ligand-receptor proximity, image features, or napari-spatialdata.
8 stars
0 votes
0 copies
1 view
Added September 12, 2026
datapythongobashexpress
Security analysis
A96/100
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Spatialdata Squidpy?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/stanfish06-spatialdata-squidpy)
---
name: spatialdata-squidpy
description: Spatial omics workflows with SpatialData and Squidpy alongside scanpy, anndata, and napari-viz. Use when working with Visium, Xenium, CosMx, MERFISH, Slide-seq, spatial transcriptomics, spatial proteomics, tissue images linked to AnnData, spatial neighbor graphs, spatial autocorrelation, ligand-receptor proximity, image features, or napari-spatialdata.
---
# SpatialData + Squidpy
Use this skill for spatial omics analysis in the scverse ecosystem. Prefer it when a task combines expression matrices, spatial coordinates, segmentation labels, microscopy/pathology images, or platform-specific outputs from 10x Visium/Xenium, NanoString CosMx, Vizgen MERFISH, Slide-seq, or related assays.
Reference versions: `spatialdata` 0.8.0, `spatialdata-io` 0.7.1, `squidpy` 1.8.3.
```bash
pip install spatialdata spatialdata-io squidpy
```
## Routing
- Use `scanpy` for ordinary scRNA-seq preprocessing and clustering.
- Use `anndata` when the main issue is object structure, layers, backed mode, or h5ad I/O.
- Use this skill when spatial coordinates, images, regions, or spatial statistics matter.
- Use `napari-viz` for headless rendering/inspection of microscopy volumes or labels.
## The SpatialData Object
A `SpatialData` object holds five element dicts plus a set of named coordinate systems. Elements are not merged into one array; they are co-registered by transformations.
| Attribute | Holds | Type per element |
|---|---|---|
| `sdata.images` | microscopy / H&E rasters | `DataArray` (single scale) or `DataTree` (multiscale) |
| `sdata.labels` | segmentation masks | `DataArray` / `DataTree`, integer-valued |
| `sdata.points` | transcript locations | dask `DataFrame` |
| `sdata.shapes` | circles / polygons | `GeoDataFrame` |
| `sdata.tables` | expression + obs | `AnnData` |
| `sdata.coordinate_systems` | registered systems | `list[str]`, `"global"` by default |
A table is linked to the element it annotates by `adata.uns["spatialdata_attrs"]`: `region` (element name), `region_key` (obs column naming the region), `instance_key` (obs column matching label ids / shape index). `sdata.tables` is a dict — a SpatialData object can carry several tables annotating different elements.
## Reading Platform Data
`spatialdata_io` readers return a `SpatialData` object directly. Every reader takes the run directory as its first argument.
```python
import spatialdata_io as sdio
sdata = sdio.xenium("path/to/xenium_run") # cells+nucleus boundaries, labels, transcripts, morphology images, table
sdata = sdio.visium("path/to/visium_out") # spots as circles, hires/lowres images, table
sdata = sdio.visium_hd("path/to/vishd_out", bin_size=[8, 16])
sdata = sdio.merscope("path/to/merscope_out") # z_layers=3 by default
sdata = sdio.cosmx("path/to/cosmx_out")
```
Other readers: `codex`, `curio`, `dbit`, `macsima`, `mcmicro`, `seqfish`, `steinbock`, `stereoseq`, plus `generic` / `image` / `geojson` for loose files and `xenium_aligned_image(image_path, alignment_file)` for post-hoc aligned images.
Reading a platform run is slow and parses many files. Convert once to Zarr, then read the Zarr from then on:
```python
import spatialdata as sd
sdata.write("run.zarr") # overwrite=False by default; raises if the store exists
sdata = sd.read_zarr("run.zarr")
sdata = sd.read_zarr("run.zarr", selection=("images", "tables")) # partial read
```
After adding an element to a Zarr-backed object, `sdata.write_element("clusters")` persists just that element. Rewriting an element that Dask is already backing from the same store fails — write the object to a new path instead.
## Coordinate Systems and Transformations
This is where spatial analyses go wrong. Elements do not share one pixel grid — each carries a transformation per coordinate system, and the system decides what units your coordinates are in.
```python
from spatialdata.transformations import (
get_transformation, set_transformation, Identity, Scale, Translation, Affine, Sequence,
)
get_transformation(sdata.images["morphology_focus"]) # to "global"
get_transformation(sdata.images["morphology_focus"], get_all=True) # dict per coordinate system
# register the image into a micron-scale system (0.2125 um/px is the Xenium pixel size)
set_transformation(
sdata.images["morphology_focus"],
Scale([0.2125, 0.2125], axes=("y", "x")),
to_coordinate_system="microns",
)
print(sdata.coordinate_systems) # ['global', 'microns']
```
Check the physical extent before overlaying anything:
```python
sd.get_extent(sdata, coordinate_system="global") # {'y': (0.0, 512.0), 'x': (0.0, 512.0)}
```
`sdata.transform_element_to_coordinate_system("morphology_focus", "microns")` takes the element *name* and returns the element with coordinates actually resampled; `sdata.filter_by_coordinate_system("microns")` drops elements not registered there.
## Cropping and Querying
```python
crop = sd.bounding_box_query(
sdata,
axes=("y", "x"),
min_coordinate=[0, 0],
max_coordinate=[256, 256],
target_coordinate_system="global",
)
```
Every element in `crop` is cut consistently and the table is filtered to the surviving instances (`filter_table=True` by default). `sd.polygon_query(...)` does the same for an arbitrary shape; both are also reachable as `sdata.query.bounding_box(...)` / `sdata.query.polygon(...)`.
## Linking Tables to Elements
```python
# pull a table column onto the element it annotates
values = sd.get_values("channel_0_sum", sdata=sdata, element_name="blobs_labels", table_name="table")
# align element rows and table rows for joint work
elements, table = sd.join_spatialelement_table(
sdata=sdata, spatial_element_names="blobs_labels", table_name="table", how="left"
)
```
Attaching a new table requires the annotation metadata, or it will not write:
```python
from spatialdata.models import TableModel
adata = TableModel.parse(
adata, region="cell_labels", region_key="region", instance_key="cell_id"
)
sdata.tables["clusters"] = adata
```
## Spatial Graphs (Squidpy)
`sq.gr.spatial_neighbors` is **deprecated** since squidpy 1.7.0 and is removed in 1.9.0. Use the mode-specific builders. Each takes an `AnnData` or a `SpatialData` — with a SpatialData, name the element and the table:
```python
import squidpy as sq
# cell-resolved platforms (Xenium, MERFISH, CosMx): kNN or Delaunay
sq.gr.spatial_neighbors_knn(
sdata,
elements_to_coordinate_systems={"cell_labels": "global"},
table_key="table",
n_neighs=6,
)
# Visium-style lattice: grid mode, n_rings controls how far out
sq.gr.spatial_neighbors_grid(adata, n_neighs=6, n_rings=2)
# physical interaction scale rather than a fixed k
sq.gr.spatial_neighbors_radius(adata, radius=30.0)
sq.gr.spatial_neighbors_delaunay(adata)
```
Grid mode assumes an approximately regular lattice — on a Xenium point cloud its ring distances are meaningless. Multi-slide objects need `library_key=` so edges are not built across slides. Results land in `table.obsp["spatial_connectivities"]` and `table.obsp["spatial_distances"]`.
## Spatial Statistics
```python
sq.gr.spatial_autocorr(sdata, table_key="table", mode="moran", n_perms=100, seed=0)
# -> table.uns["moranI"]: I, pval_norm, pval_sim, pval_z_sim + fdr_bh-corrected columns
sq.gr.spatial_autocorr(sdata, table_key="table", mode="geary")
sq.gr.nhood_enrichment(sdata, cluster_key="cluster", table_key="table", n_perms=1000, seed=0)
# -> table.uns["cluster_nhood_enrichment"]
sq.gr.co_occurrence(sdata, cluster_key="cluster", table_key="table", interval=50)
sq.gr.ripley(adata, cluster_key="cluster", mode="L")
sq.gr.ligrec(adata, cluster_key="cluster")
```
`spatial_autocorr` already applies `corr_method="fdr_bh"`; read `pval_norm_fdr_bh` / `pval_sim_fdr_bh`, not the raw p-values. `nhood_enrichment` and `spatial_autocorr` are permutation tests — set `seed=` or results are not reproducible.
Plotting: `sq.pl.nhood_enrichment`, `sq.pl.co_occurrence`, `sq.pl.centrality_scores`, `sq.pl.interaction_matrix`, `sq.pl.ripley`, `sq.pl.ligrec`, `sq.pl.spatial_scatter`, `sq.pl.spatial_segment`, `sq.pl.var_by_distance`.
## Core Workflow
1. Read the run with the platform reader, then `sdata.write("run.zarr")` once and work from the Zarr.
2. Inspect `print(sdata)` — element names, shapes, and the coordinate systems each is registered in.
3. Confirm the units. `get_transformation(..., get_all=True)` and `sd.get_extent(...)` before any overlay or radius.
4. Preprocess the table with Scanpy (QC, normalize, HVGs, PCA, neighbors, UMAP, clustering) — expression preprocessing is not spatial.
5. Build the spatial graph with the mode that matches the geometry (grid for Visium, kNN/Delaunay for cell-resolved).
6. Run the spatial statistics, with `seed=` set.
7. Crop with `bounding_box_query` / `polygon_query` rather than slicing arrays by hand.
8. Write results back: `sdata.write(...)` for the whole object, `sdata.write_element(...)` for a newly added element.
## Quality Checks
- Confirm whether coordinates are pixels, microns, array indices, or platform-specific units; a radius in the wrong system is silently wrong, not an error.
- Check image origin and axis orientation before overlaying labels or points — `sd.get_extent` per coordinate system.
- `region` / `region_key` / `instance_key` must be set on every table, or the table will not write and cannot be joined to its element.
- Keep tissue masks and segmentation labels versioned; most downstream errors come from silent coordinate or mask mismatch.
- Stratify spatial statistics by sample/slide (`library_key=`); do not pool slides unless batch and geometry are comparable.
- Report neighborhood graph parameters — mode, `n_neighs`/`radius`/`n_rings`, coordinate system — because they strongly affect spatial-enrichment calls.
- Permutation-based results (`nhood_enrichment`, `spatial_autocorr`, `ligrec`) need `seed=` and `n_perms=` reported.