
Bio Workflows Spatial Pipeline
- 3 installs
- 1.1k repo stars
- Updated July 25, 2026
- gptomics/bioskills
Run an end-to-end spatial transcriptomics workflow for Visium/Xenium data covering preprocessing, domain detection, and visualization with Squidpy.
About
Orchestrates data loading, preprocessing, spatial neighbor graphs, spatial statistics, domain detection, and visualization with Squidpy. A developer uses it to analyze Visium or Xenium spatial transcriptomics data end-to-end.
- Squidpy spatial preprocessing, neighbors, and statistics
- Spatial domain detection and visualization with QC gates
Bio Workflows Spatial Pipeline by the numbers
- 3 all-time installs (skills.sh)
- Ranked #1,661 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gptomics/bioskills --skill bio-workflows-spatial-pipelineAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 1.1k |
| Last updated | July 25, 2026 |
| Repository | gptomics/bioskills ↗ |
What it does
Run an end-to-end spatial transcriptomics workflow for Visium/Xenium data covering preprocessing, domain detection, and visualization with Squidpy.
Files
Version Compatibility
Reference examples tested with: matplotlib 3.8+, numpy 1.26+, scanpy 1.10+, squidpy 1.3+
Before using code patterns, verify installed versions match. If versions differ:
- Python:
pip show <package>thenhelp(module.function)to check signatures
If code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
Spatial Transcriptomics Pipeline
"Analyze my spatial transcriptomics data end-to-end" -> Orchestrate data loading (squidpy/scanpy), QC, normalization, spatial domain detection, deconvolution (cell2location), spatial neighbor analysis, cell-cell communication, and tissue visualization.
Complete workflow for analyzing Visium, Xenium, or other spatial transcriptomics data.
Workflow Overview
Spatial data (Space Ranger output)
|
v
[1. Load Data] ---------> Read Visium/Xenium
|
v
[2. QC & Preprocessing] -> Filter, normalize
|
v
[3. Clustering] --------> Standard scRNA-seq clustering
|
v
[4. Spatial Analysis] --> Neighbors, statistics
|
v
[5. Domain Detection] --> Spatial domains
|
v
[6. Visualization] -----> Spatial plots
|
v
Annotated spatial dataPrimary Path: Squidpy + Scanpy
Step 1: Load Data
import scanpy as sc
import squidpy as sq
import numpy as np
import matplotlib.pyplot as plt
# Load Visium data (Space Ranger output)
adata = sq.read.visium('spaceranger_output/')
# Or load from specific files
adata = sc.read_10x_h5('filtered_feature_bc_matrix.h5')
adata.uns['spatial'] = ... # Add spatial info
# For Xenium
adata = sq.read.xenium('xenium_output/')
print(f'Loaded: {adata.n_obs} spots/cells, {adata.n_vars} genes')Step 2: Quality Control
# QC metrics
adata.var['mt'] = adata.var_names.str.startswith('MT-')
sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], inplace=True)
# Visualize QC
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
sc.pl.spatial(adata, color='total_counts', ax=axes[0], show=False)
sc.pl.spatial(adata, color='n_genes_by_counts', ax=axes[1], show=False)
sc.pl.spatial(adata, color='pct_counts_mt', ax=axes[2], show=False)
plt.savefig('qc_spatial.pdf')
# Filter
sc.pp.filter_cells(adata, min_counts=500)
sc.pp.filter_cells(adata, min_genes=200)
sc.pp.filter_genes(adata, min_cells=10)
adata = adata[adata.obs.pct_counts_mt < 25, :]
print(f'After QC: {adata.n_obs} spots/cells')Step 3: Normalization and Clustering
# Store raw counts
adata.layers['counts'] = adata.X.copy()
# Normalize
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
# HVGs
sc.pp.highly_variable_genes(adata, n_top_genes=2000)
# PCA and clustering
adata.raw = adata
adata = adata[:, adata.var.highly_variable]
sc.pp.scale(adata, max_value=10)
sc.tl.pca(adata, n_comps=50)
sc.pp.neighbors(adata, n_neighbors=15, n_pcs=30)
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=0.5)
# Visualize clusters in space
sc.pl.spatial(adata, color='leiden', spot_size=1.5)
plt.savefig('clusters_spatial.pdf')Step 4: Spatial Analysis
# Build spatial neighbors graph
sq.gr.spatial_neighbors(adata, coord_type='generic', n_neighs=6)
# Neighborhood enrichment (which clusters are neighbors)
sq.gr.nhood_enrichment(adata, cluster_key='leiden')
sq.pl.nhood_enrichment(adata, cluster_key='leiden')
plt.savefig('nhood_enrichment.pdf')
# Co-occurrence analysis
sq.gr.co_occurrence(adata, cluster_key='leiden')
sq.pl.co_occurrence(adata, cluster_key='leiden')
plt.savefig('co_occurrence.pdf')
# Spatially variable genes
sq.gr.spatial_autocorr(adata, mode='moran', n_perms=100, n_jobs=4)
# Top spatially variable genes
svg = adata.uns['moranI'].sort_values('I', ascending=False)
top_svg = svg.head(20).index.tolist()
print('Top spatially variable genes:', top_svg[:10])Step 5: Domain Detection
# Spatial domain detection using clustering with spatial constraints
# Option 1: Use spatial neighbors for Leiden clustering
sq.gr.spatial_neighbors(adata, coord_type='generic', n_neighs=15)
sc.tl.leiden(adata, resolution=0.3, key_added='spatial_domains',
adjacency=adata.obsp['spatial_connectivities'])
# Visualize domains
sc.pl.spatial(adata, color='spatial_domains', spot_size=1.5)
plt.savefig('spatial_domains.pdf')
# Compare transcriptomic vs spatial clusters
sc.pl.spatial(adata, color=['leiden', 'spatial_domains'], ncols=2)
plt.savefig('clusters_comparison.pdf')Step 6: Visualization
# Gene expression in space
genes = ['EPCAM', 'VIM', 'PTPRC', 'COL1A1']
sc.pl.spatial(adata, color=genes, ncols=2, spot_size=1.5, cmap='viridis')
plt.savefig('marker_genes_spatial.pdf')
# Cluster markers in space
sc.tl.rank_genes_groups(adata, 'leiden', method='wilcoxon')
sc.pl.rank_genes_groups_dotplot(adata, n_genes=5)
plt.savefig('cluster_markers.pdf')
# Save
adata.write('spatial_analyzed.h5ad')Complete Workflow Script
import scanpy as sc
import squidpy as sq
import matplotlib.pyplot as plt
import os
# Configuration
data_dir = 'spaceranger_output'
output_dir = 'spatial_results'
os.makedirs(output_dir, exist_ok=True)
os.makedirs(f'{output_dir}/plots', exist_ok=True)
# Load
print('Loading data...')
adata = sq.read.visium(data_dir)
print(f'Loaded: {adata.n_obs} spots, {adata.n_vars} genes')
# QC
print('QC filtering...')
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=500)
sc.pp.filter_genes(adata, min_cells=10)
adata = adata[adata.obs.pct_counts_mt < 25, :]
print(f'After QC: {adata.n_obs} spots')
# Normalize and cluster
print('Processing...')
adata.layers['counts'] = adata.X.copy()
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
sc.pp.highly_variable_genes(adata, n_top_genes=2000)
adata.raw = adata
adata = adata[:, adata.var.highly_variable]
sc.pp.scale(adata, max_value=10)
sc.tl.pca(adata, n_comps=50)
sc.pp.neighbors(adata, n_neighbors=15, n_pcs=30)
sc.tl.leiden(adata, resolution=0.5)
# Spatial analysis
print('Spatial analysis...')
sq.gr.spatial_neighbors(adata, coord_type='generic', n_neighs=6)
sq.gr.nhood_enrichment(adata, cluster_key='leiden')
sq.gr.spatial_autocorr(adata, mode='moran', n_perms=100)
# Plots
print('Creating plots...')
sc.pl.spatial(adata, color='leiden', spot_size=1.5, save='_clusters.pdf')
sq.pl.nhood_enrichment(adata, cluster_key='leiden', save='_nhood.pdf')
# Save
adata.write(f'{output_dir}/spatial_analyzed.h5ad')
print(f'Results saved to {output_dir}/')Related Skills
- spatial-transcriptomics/spatial-data-io - Loading formats
- spatial-transcriptomics/spatial-preprocessing - QC details
- spatial-transcriptomics/spatial-statistics - Moran's I, co-occurrence
- spatial-transcriptomics/spatial-domains - Domain detection methods
- spatial-transcriptomics/spatial-deconvolution - Cell type estimation
# Reference: matplotlib 3.8+, numpy 1.26+, scanpy 1.10+, squidpy 1.3+ | Verify API if version differs
# Complete Visium spatial transcriptomics workflow
import scanpy as sc
import squidpy as sq
import matplotlib.pyplot as plt
import numpy as np
import os
sc.settings.verbosity = 1
sc.settings.set_figure_params(dpi=100, facecolor='white')
# Configuration
data_dir = 'spaceranger_output'
output_dir = 'visium_results'
os.makedirs(output_dir, exist_ok=True)
os.makedirs(f'{output_dir}/plots', exist_ok=True)
# === Step 1: Load Data ===
print('=== Step 1: Loading Data ===')
adata = sq.read.visium(data_dir)
adata.var_names_make_unique()
print(f'Loaded: {adata.n_obs} spots, {adata.n_vars} genes')
# === Step 2: QC ===
print('=== Step 2: Quality Control ===')
adata.var['mt'] = adata.var_names.str.startswith('MT-')
adata.var['ribo'] = adata.var_names.str.startswith(('RPS', 'RPL'))
sc.pp.calculate_qc_metrics(adata, qc_vars=['mt', 'ribo'], inplace=True)
# QC plots
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
sc.pl.spatial(adata, color='total_counts', ax=axes[0, 0], show=False)
sc.pl.spatial(adata, color='n_genes_by_counts', ax=axes[0, 1], show=False)
sc.pl.spatial(adata, color='pct_counts_mt', ax=axes[0, 2], show=False)
sc.pl.violin(adata, ['total_counts', 'n_genes_by_counts', 'pct_counts_mt'],
jitter=0.4, ax=axes[1, :], show=False)
plt.tight_layout()
plt.savefig(f'{output_dir}/plots/qc_metrics.pdf')
plt.close()
# Filter
print(f'Before filtering: {adata.n_obs} spots')
sc.pp.filter_cells(adata, min_counts=500)
sc.pp.filter_cells(adata, min_genes=200)
sc.pp.filter_genes(adata, min_cells=10)
adata = adata[adata.obs.pct_counts_mt < 25, :]
print(f'After filtering: {adata.n_obs} spots')
# === Step 3: Normalization ===
print('=== Step 3: Normalization ===')
adata.layers['counts'] = adata.X.copy()
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
sc.pp.highly_variable_genes(adata, n_top_genes=2000)
print(f'HVGs: {adata.var.highly_variable.sum()}')
# === Step 4: Dimensionality Reduction & Clustering ===
print('=== Step 4: Clustering ===')
adata.raw = adata
adata = adata[:, adata.var.highly_variable]
sc.pp.scale(adata, max_value=10)
sc.tl.pca(adata, n_comps=50)
sc.pp.neighbors(adata, n_neighbors=15, n_pcs=30)
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=0.5)
# Cluster visualization
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
sc.pl.umap(adata, color='leiden', ax=axes[0], show=False)
sc.pl.spatial(adata, color='leiden', spot_size=1.5, ax=axes[1], show=False)
plt.tight_layout()
plt.savefig(f'{output_dir}/plots/clusters.pdf')
plt.close()
print(f'Clusters: {adata.obs["leiden"].nunique()}')
# === Step 5: Spatial Analysis ===
print('=== Step 5: Spatial Analysis ===')
# Spatial neighbors
sq.gr.spatial_neighbors(adata, coord_type='generic', n_neighs=6)
# Neighborhood enrichment
sq.gr.nhood_enrichment(adata, cluster_key='leiden')
sq.pl.nhood_enrichment(adata, cluster_key='leiden')
plt.savefig(f'{output_dir}/plots/nhood_enrichment.pdf')
plt.close()
# Co-occurrence
sq.gr.co_occurrence(adata, cluster_key='leiden')
sq.pl.co_occurrence(adata, cluster_key='leiden', clusters=['0', '1'])
plt.savefig(f'{output_dir}/plots/co_occurrence.pdf')
plt.close()
# Spatially variable genes
print('Finding spatially variable genes...')
sq.gr.spatial_autocorr(adata, mode='moran', n_perms=100, n_jobs=4)
svg = adata.uns['moranI'].sort_values('I', ascending=False)
top_svg = svg.head(20).index.tolist()
print(f'Top SVGs: {top_svg[:5]}')
# Plot top SVGs
sc.pl.spatial(adata, color=top_svg[:4], ncols=2, spot_size=1.5, cmap='viridis')
plt.savefig(f'{output_dir}/plots/top_svg.pdf')
plt.close()
# === Step 6: Cluster Markers ===
print('=== Step 6: Cluster Markers ===')
sc.tl.rank_genes_groups(adata, 'leiden', method='wilcoxon')
markers = sc.get.rank_genes_groups_df(adata, group=None)
markers.to_csv(f'{output_dir}/cluster_markers.csv', index=False)
# Marker plots
sc.pl.rank_genes_groups_dotplot(adata, n_genes=5, save=f'_{output_dir}/plots/markers_dotplot.pdf')
# === Step 7: Save Results ===
print('=== Step 7: Saving Results ===')
svg.to_csv(f'{output_dir}/spatially_variable_genes.csv')
adata.write(f'{output_dir}/visium_analyzed.h5ad')
print(f'\n=== Analysis Complete ===')
print(f'Results saved to: {output_dir}/')
print(f' - Processed data: visium_analyzed.h5ad')
print(f' - SVGs: spatially_variable_genes.csv')
print(f' - Markers: cluster_markers.csv')
print(f' - Plots: plots/')
Spatial Transcriptomics Pipeline - Usage Guide
Overview
This workflow analyzes spatial transcriptomics data (Visium, Xenium) from raw data to spatial domains and visualizations using Squidpy and Scanpy.
Prerequisites
pip install squidpy scanpy matplotlibQuick Start
Tell your AI agent what you want to do:
- "Analyze my Visium spatial transcriptomics data"
- "Find spatially variable genes in my tissue"
- "Identify spatial domains in my sample"
Example Prompts
Loading and QC
"Load my Space Ranger output"
"Show QC metrics on the tissue image"
Analysis
"Find spatially variable genes"
"Run neighborhood enrichment analysis"
"Detect spatial domains"
Visualization
"Plot gene expression on the tissue"
"Show clusters overlaid on the image"
Input Requirements
| Input | Format | Description |
|---|---|---|
| Space Ranger output | Directory | Visium processed data |
| Xenium output | Directory | Xenium processed data |
What the Workflow Does
1. Load Data - Read spatial data with images 2. QC - Filter low-quality spots 3. Clustering - Standard scRNA-seq pipeline 4. Spatial Analysis - Neighbors, statistics 5. Domains - Spatial domain detection 6. Visualization - Plots on tissue
Tips
- Spot size: Adjust for visualization clarity
- Resolution: Lower for fewer, larger domains
- SVGs: Check top Moran's I genes
- Deconvolution: Add for cell type estimates