본문으로 건너뛰기
cccskills
무료GitHub에 공개

spatial-transcriptomics-analysis-squidpy

Squidpy spatial statistics on annotated spatial transcriptomics: build the spatial graph (Visium grid, kNN, radius, Delaunay), neighbourhood enrichment z-scores, distance-resolved co-occurrence, interaction matrix and centrality scores, Ripley's L/F/G point patterns, spatially variable genes by Moran's I / Geary's C, and permutation ligand-receptor tests (ligrec, OmniPath). Works on Visium spots and single-cell-resolution data (Xenium, MERFISH, CosMx, Slide-seq). Needs cluster or cell type labels for all but the spatially variable gene step. For an end-to-end ligand-receptor workflow use spatial-transcriptomics-cell-cell-communication.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

실행 점수 미검증

설치 방법 보기

포함된 파일(1)

  • SKILL.md16.3 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Squidpy Spatial Analysis

Overview

Graph and point-pattern statistics that describe how cell types (or spot clusters) are arranged in tissue and which genes follow a spatial pattern. Each Core API module answers one question and exports a CSV:

QuestionModule
Which cell types sit next to each other more than chance?2 Neighbourhood enrichment
At what distance do two types co-occur?3 Co-occurrence
How many contacts between types; which types are central?4 Interaction matrix + centrality
Is a type clustered, random or dispersed?5 Ripley's statistics
Which genes are spatially patterned?6 Spatial autocorrelation
Which LR pairs are specific to a cluster pair?7 Ligand-receptor test

When to Use

  • Testing which cell types are neighbours more or less often than chance
  • Measuring the distance range over which two cell types stay together
  • Deciding whether a cell type is clustered, random or evenly dispersed in tissue
  • Finding spatially variable genes by Moran's I or Geary's C
  • Running a quick permutation ligand-receptor test between labels
  • Use spatial-transcriptomics-cell-cell-communication instead for a multi-method LIANA ligand-receptor ranking with a spatial adjacency filter
  • For tissue regions without labels, use spatial-domain-detection first; for cell labels use spatial-transcriptomics-cell-type-annotation

Prerequisites

  • Input: AnnData with obsm["spatial"] and log-normalised X
  • Labels: a categorical column in obs (cell types for single-cell data; spot clusters or domains for Visium - spots are mixtures, so read results as region-level)
  • Network: Module 7 downloads OmniPath interactions
  • Versions: squidpy >= 1.6. sq.gr.spatial_neighbors is deprecated from 1.7 in favour of spatial_neighbors_knn/_radius/_delaunay/_grid; it still works through 1.8
# Install only what `import` reports missing. `--pypi` keeps the env's scanpy/numba
# stack; a conda add can swap it and bare pip can shadow it.
pixi add --pypi squidpy omnipath scanpy
pixi add --pypi liana        # only for the local-LR-list recipe

Quick Start

import scanpy as sc
import squidpy as sq

adata = sc.read_h5ad("celltype_annotated.h5ad")
adata.obs["cell_type"] = adata.obs["cell_type"].astype("category")
sq.gr.spatial_neighbors(adata, coord_type="generic", n_neighs=10)     # Visium: coord_type="grid", n_rings=1
sq.gr.nhood_enrichment(adata, cluster_key="cell_type", n_perms=1000, seed=0)
sq.pl.nhood_enrichment(adata, cluster_key="cell_type", save="nhood_quick.png")

Core API

Module 1: Build the Spatial Graph

All graph-based modules (2, 4, 6) read obsp["spatial_connectivities"].

import numpy as np
import pandas as pd
import scanpy as sc
import squidpy as sq
import matplotlib.pyplot as plt

adata = sc.read_h5ad("celltype_annotated.h5ad")
CLUSTER_KEY = "cell_type"
adata.obs[CLUSTER_KEY] = adata.obs[CLUSTER_KEY].astype("category")
cats = adata.obs[CLUSTER_KEY].cat.categories

PLATFORM = "single_cell"      # "visium" for Visium spots
if PLATFORM == "visium":
    sq.gr.spatial_neighbors(adata, coord_type="grid", n_neighs=6, n_rings=1)   # hexagonal ring
else:
    sq.gr.spatial_neighbors(adata, coord_type="generic", n_neighs=10)
    # alternatives: delaunay=True (natural neighbours) or radius=30 (same units as coordinates)

deg = np.asarray(adata.obsp["spatial_connectivities"].sum(1)).ravel()
print(f"{adata.n_obs} nodes, median degree {np.median(deg):.0f}, isolated {(deg == 0).sum()}")

Module 2: Neighbourhood Enrichment

Permutation test of how often two labels are neighbours versus shuffled labels:

sq.gr.nhood_enrichment(adata, cluster_key=CLUSTER_KEY, n_perms=1000, seed=0)
z = pd.DataFrame(adata.uns[f"{CLUSTER_KEY}_nhood_enrichment"]["zscore"], index=cats, columns=cats)
z.to_csv("nhood_enrichment_z.csv")
sq.pl.nhood_enrichment(adata, cluster_key=CLUSTER_KEY, method="average", figsize=(8, 8),
                       save="nhood_enrichment.png")

pairs = z.where(np.triu(np.ones(z.shape, bool), k=1)).stack().sort_values()
print("most co-located:\n", pairs.tail(8).round(1))
print("most avoiding:\n", pairs.head(8).round(1))

z > 2: neighbours more often than chance; z < -2: avoid each other. The diagonal measures self-clustering.

Module 3: Co-occurrence Across Distances

Ratio of P(type B | within distance d of type A) to P(type B). Memory grows with cell count squared, so subsample large data:

MAX_CELLS = 20000
sub = adata if adata.n_obs <= MAX_CELLS else sc.pp.subsample(adata, n_obs=MAX_CELLS, random_state=0, copy=True)
occ, interval = sq.gr.co_occurrence(sub, cluster_key=CLUSTER_KEY, interval=50, copy=True)
# occ: n_clusters x n_clusters x n_distances; distance bins = interval[1:]
dist = interval[1:] if len(interval) == occ.shape[2] + 1 else interval
sub_cats = sub.obs[CLUSTER_KEY].cat.categories
rows = [(a, b, d, occ[i, j, k]) for i, a in enumerate(sub_cats) for j, b in enumerate(sub_cats)
        for k, d in enumerate(dist)]
co = pd.DataFrame(rows, columns=["anchor", "other", "distance", "ratio"])
co.to_csv("co_occurrence.csv", index=False)

ANCHOR = sub_cats[0]           # set to the cell type of interest
sq.pl.co_occurrence(sub, cluster_key=CLUSTER_KEY, clusters=[ANCHOR], figsize=(8, 4),
                    save="co_occurrence.png")

Ratio > 1 at short distance = attraction; the distance where it returns to 1 is the spatial range of the association. Older squidpy versions also take n_splits to reduce memory.

Module 4: Interaction Matrix and Centrality

im = sq.gr.interaction_matrix(adata, cluster_key=CLUSTER_KEY, normalized=True, copy=True)
im = pd.DataFrame(im, index=cats, columns=cats)          # row-normalised share of each type's edges
im.to_csv("interaction_matrix.csv")
sq.pl.interaction_matrix(adata, cluster_key=CLUSTER_KEY, save="interaction_matrix.png")  # recomputes uns if needed

cent = sq.gr.centrality_scores(adata, cluster_key=CLUSTER_KEY, copy=True)
cent.to_csv("centrality_scores.csv")
print(cent.sort_values("closeness_centrality", ascending=False).round(3))

The interaction matrix counts raw contacts (dominated by abundant types); use Module 2 for enrichment over chance. Centrality: degree_centrality = share of graph connections, closeness_centrality = how central a type is in the tissue graph, average_clustering = how tightly its cells group.

Module 5: Ripley's Statistics

MODE = "L"                     # "L" clustering scale, "G" nearest-neighbour, "F" empty space
rip = sq.gr.ripley(adata, cluster_key=CLUSTER_KEY, mode=MODE, n_simulations=100, seed=0, copy=True)
stat = rip[f"{MODE}_stat"]     # columns: bins, <cluster_key>, stats
stat.to_csv(f"ripley_{MODE}.csv", index=False)
fig, ax = plt.subplots(figsize=(8, 5))
for ct, d in stat.groupby(CLUSTER_KEY, observed=True):
    ax.plot(d["bins"], d["stats"], label=ct)
band = rip["sims_stat"].groupby("bins")["stats"]      # columns: bins, simulations, stats
lo, hi = band.min(), band.max()
ax.fill_between(lo.index, lo, hi, color="grey", alpha=0.3, label="random (simulated)")
ax.set_xlabel("distance"); ax.set_ylabel(f"Ripley's {MODE}"); ax.legend(fontsize=7)
plt.tight_layout(); plt.savefig(f"ripley_{MODE}.png", dpi=150); plt.close()

For L, a curve above the simulated band = clustered at that distance, below = dispersed (regular). rip["pvalues"] (cluster x bin) tests each curve against the simulations.

Module 6: Spatially Variable Genes

GENES = None                   # None -> highly variable genes; or a list of genes
if GENES is None:
    if "highly_variable" not in adata.var:
        sc.pp.highly_variable_genes(adata, n_top_genes=min(2000, adata.n_vars))
    GENES = adata.var_names[adata.var["highly_variable"]].tolist()

sq.gr.spatial_autocorr(adata, mode="moran", genes=GENES, n_perms=100, n_jobs=4, seed=0)
moran = adata.uns["moranI"].sort_values("I", ascending=False)
moran.to_csv("moran_i.csv")
sig = moran[moran["pval_norm_fdr_bh"] < 0.05]
print(f"{len(sig)} of {len(moran)} genes spatially autocorrelated (FDR < 0.05)")
print(sig.head(15)[["I", "pval_norm_fdr_bh"]])
sq.pl.spatial_scatter(adata, color=sig.index[:4].tolist(), shape=None, size=2, save="top_svg.png")

Moran's I near 1 = smooth spatial pattern, near 0 = random. For a targeted panel (< ~1,000 genes) test all genes with GENES = adata.var_names.tolist(). mode="geary" writes uns["gearyC"] (C near 0 = clustered, 1 = random). On Visium use sq.pl.spatial_scatter with the default shape to draw spots over the image.

Module 7: Ligand-Receptor Permutation Test

CellPhoneDB-style test with OmniPath LR pairs, between labels:

res = sq.gr.ligrec(adata, cluster_key=CLUSTER_KEY, n_perms=1000, threshold=0.1, use_raw=False,
                   corr_method="fdr_bh", seed=0, copy=True,
                   transmitter_params={"categories": "ligand"}, receiver_params={"categories": "receptor"})

def to_long(df, name):
    d = df.copy()
    d.columns = [f"{a}|{b}" for a, b in d.columns]
    d.index = [f"{a}|{b}" for a, b in d.index]
    return d.stack().rename(name).rename_axis(["lr", "pair"]).reset_index()

lr = to_long(res["means"], "mean").merge(to_long(res["pvalues"], "pval"), on=["lr", "pair"])
lr[["ligand", "receptor"]] = lr["lr"].str.split("|", n=1, expand=True)
lr[["sender", "receiver"]] = lr["pair"].str.split("|", n=1, expand=True)
hits = lr[(lr["pval"] < 0.05) & (lr["mean"] > 0)].sort_values("mean", ascending=False)
hits.to_csv("ligrec_significant.csv", index=False)
print(f"{len(hits)} significant LR x cluster pair combinations")
print(hits.head(15)[["sender", "receiver", "ligand", "receptor", "mean", "pval"]])

Means index = (ligand, receptor), columns = (sender, receiver). For a consensus across several LR methods and spatial filtering use spatial-transcriptomics-cell-cell-communication.

Common Workflows

Workflow: Cell Type Arrangement Report

Graph, enrichment, contact counts and the distance range of the strongest pair, in one pass:

sq.gr.spatial_neighbors(adata, coord_type="generic", n_neighs=10)
sq.gr.nhood_enrichment(adata, cluster_key=CLUSTER_KEY, n_perms=1000, seed=0)
z = pd.DataFrame(adata.uns[f"{CLUSTER_KEY}_nhood_enrichment"]["zscore"], index=cats, columns=cats)
im = pd.DataFrame(sq.gr.interaction_matrix(adata, cluster_key=CLUSTER_KEY, normalized=True, copy=True),
                  index=cats, columns=cats)
pairs = z.where(np.triu(np.ones(z.shape, bool), k=1)).stack()
a, b = pairs.idxmax()
report = pd.DataFrame({"z": pairs, "contact_share": [im.loc[x, y] for x, y in pairs.index]})
report.sort_values("z", ascending=False).to_csv("arrangement_report.csv")

sub = adata if adata.n_obs <= 20000 else sc.pp.subsample(adata, n_obs=20000, random_state=0, copy=True)
occ, interval = sq.gr.co_occurrence(sub, cluster_key=CLUSTER_KEY, interval=50, copy=True)
sc_ = list(sub.obs[CLUSTER_KEY].cat.categories)
dist = interval[1:] if len(interval) == occ.shape[2] + 1 else interval
print(f"strongest pair: {a} - {b} (z = {pairs.max():.1f})")
print(pd.Series(occ[sc_.index(a), sc_.index(b)], index=np.round(dist, 1)).head(10).round(2))

Workflow: Compare Arrangement Between Samples

Neighbourhood z-scores are computed per sample (graphs must not connect cells across sections), then compared:

SAMPLE_KEY = "sample"
zs = {}
for s in adata.obs[SAMPLE_KEY].unique():
    a_s = adata[adata.obs[SAMPLE_KEY] == s].copy()
    a_s.obs[CLUSTER_KEY] = a_s.obs[CLUSTER_KEY].cat.remove_unused_categories()
    sq.gr.spatial_neighbors(a_s, coord_type="generic", n_neighs=10)
    sq.gr.nhood_enrichment(a_s, cluster_key=CLUSTER_KEY, n_perms=1000, seed=0)
    c = a_s.obs[CLUSTER_KEY].cat.categories
    zs[s] = pd.DataFrame(a_s.uns[f"{CLUSTER_KEY}_nhood_enrichment"]["zscore"], index=c, columns=c).stack()
zdf = pd.DataFrame(zs)
zdf.to_csv("nhood_z_by_sample.csv")
print(zdf.assign(spread=zdf.max(axis=1) - zdf.min(axis=1)).sort_values("spread", ascending=False).head(15).round(1))

With one section per condition the difference is descriptive; replicate sections are needed to call a change.

Key Parameters

ParameterModuleDefaultEffect
n_neighs / radius / delaunay110 / - / FalseDefines "neighbour"; drives Steps 2, 4, 6
n_rings11Visium hex rings (1 = 6 neighbours)
n_perms2, 6, 71000 / 100 / 1000Permutation precision
interval350Number of distance bins
MODE5"L"Ripley's function
threshold70.1Min fraction of a cluster expressing each gene

Common Recipes

Recipe: Visium - Use Expression Clusters as Labels

Spots have no cell type; cluster them on expression (or use domains from spatial-domain-detection) and run the modules on those labels:

sc.pp.pca(adata, n_comps=30)
sc.pp.neighbors(adata)
sc.tl.leiden(adata, resolution=0.5, key_added="spot_cluster", flavor="igraph", n_iterations=2)
sq.gr.spatial_neighbors(adata, coord_type="grid", n_rings=1)
sq.gr.nhood_enrichment(adata, cluster_key="spot_cluster", n_perms=1000, seed=0)
sq.pl.spatial_scatter(adata, color="spot_cluster", save="spot_clusters.png")   # spots over the H&E image

Recipe: Ligrec with a Local LR List (no OmniPath download)

Pass interactions as a DataFrame with source (ligand) and target (receptor) columns, for example from LIANA's consensus resource:

import liana as li

pairs_df = li.rs.select_resource("consensus").rename(columns={"ligand": "source", "receptor": "target"})
pairs_df = pairs_df[~pairs_df["source"].str.contains("_") & ~pairs_df["target"].str.contains("_")]   # simple genes only
res = sq.gr.ligrec(adata, cluster_key=CLUSTER_KEY, interactions=pairs_df[["source", "target"]],
                   n_perms=1000, threshold=0.1, use_raw=False, corr_method="fdr_bh", seed=0, copy=True)
print(res["pvalues"].shape)

Complex (multi-subunit) pairs are dropped here; Module 7 with OmniPath or the CellPhoneDB route of spatial-transcriptomics-cell-cell-communication handles complexes.

Expected Outputs

  • nhood_enrichment_z.csv, figures/nhood_enrichment.png
  • co_occurrence.csv, figures/co_occurrence.png
  • interaction_matrix.csv, centrality_scores.csv
  • ripley_L.csv, ripley_L.png
  • moran_i.csv, figures/top_svg.png
  • ligrec_significant.csv

Troubleshooting

ProblemCauseFix
KeyError: spatial_connectivitiesModule 1 not run on this objectRun sq.gr.spatial_neighbors first
ValueError on coord_type="visium"Not a valid valueUse coord_type="grid" for Visium
Co-occurrence out of memoryToo many cellsLower MAX_CELLS
Many isolated nodesradius too small for the coordinate unitsCheck units (pixels vs microns); use kNN
Ligrec fails to downloadNo internet / omnipath missingpixi add --pypi omnipath; or pass interactions= a DataFrame with source/target columns
Neighbourhood z-scores extreme for rare typesFew cellsMerge rare types or interpret with counts
Deprecation warning for spatial_neighborssquidpy >= 1.7Switch to sq.gr.spatial_neighbors_knn etc.

Related Skills

  • spatial-transcriptomics-cell-cell-communication - LIANA/CellPhoneDB ranking filtered by neighbourhood enrichment, local LR scores, MISTy
  • spatial-transcriptomics-cell-type-annotation - labels and niches for single-cell data
  • spatial-domain-detection - spatial domains to use as CLUSTER_KEY

References

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

API + Python SDK for ordering cell-free protein expression and binding assays. Submit sequences for expression (10–100 µg), measure binding affinity (KD) against targets, track status, and retrieve results programmatically — no wet-lab setup. Built for ML-guided directed evolution and antibody/nanobody optimization. Requires Adaptyv account and API key.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

jaechang-hits/SciAgent-Skills3822026년 10월 22일 업데이트

aeon

무료

scikit-learn compatible Python toolkit for time series ML: classify, cluster, regress, segment, transform with 30+ algorithms (ROCKET, InceptionTime, KNN-DTW, HIVE-COTE, WEASEL). Handles panel, multivariate, and unequal-length series. Maintained successor to sktime. Alternatives: sktime (larger ecosystem), tslearn (fewer algorithms), catch22 (features only).

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

jaechang-hits/SciAgent-Skills3822026년 10월 22일 업데이트

AiZynthFinder retrosynthetic route planning (CASP) from AstraZeneca Molecular AI. Monte Carlo tree search guided by a template-based neural expansion policy recursively disconnects a target SMILES until precursors are found in a purchasable stock. Covers config.yml (v4 format), aizynthcli batch screening, the AiZynthFinder/AiZynthExpander Python API, one-step disconnections, custom stocks via smiles2stock, scorers, Retro*/breadth-first/DFPN search alternatives, and reading output.json.gz / trees.json. Use for synthesis route planning, synthesizability screening, and building-block/precursor search. For reaction barriers use neb-irc-activation-energy; for 2D reaction scheme drawing use rdkit-chemdraw-cdxml.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

jaechang-hits/SciAgent-Skills3822026년 10월 22일 업데이트

Access AlphaFold DB's 200M+ predicted structures by UniProt ID. Download PDB/mmCIF, analyze pLDDT/PAE, bulk-fetch proteomes via Google Cloud. For experimental structures use PDB; for prediction use ColabFold or ESMFold.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

jaechang-hits/SciAgent-Skills3822026년 10월 22일 업데이트

Annotated matrices for single-cell genomics. Stores X with obs/var metadata, layers, embeddings (obsm/varm), graphs (obsp/varp), uns. Use for .h5ad/.zarr I/O, concatenation, scverse integration. For analysis use scanpy; for probabilistic models use scvi-tools.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

jaechang-hits/SciAgent-Skills3822026년 10월 22일 업데이트

GRN inference from expression via GRNBoost2 (gradient boosting) or GENIE3 (Random Forest). Load matrix, filter by TFs, infer TF-target-importance links, save network. Dask-parallelized to single-cell scale. Core SCENIC component.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

jaechang-hits/SciAgent-Skills3822026년 10월 22일 업데이트

jaechang-hits의 스킬 모두 보기

이 스킬의 문제 신고