本文へ移動
cccskills
無料GitHub で公開

spatial-transcriptomics-cell-cell-communication

Spatial cell-cell communication / ligand-receptor (LR) interaction analysis, one skill for every route: LIANA consensus ranking between annotated cell types, CellPhoneDB v5 permutation p-values, DEG method and spatial microenvironments, squidpy neighbourhood enrichment to keep spatially adjacent sender-receiver pairs, LIANA bivariate local LR scores on the tissue (works on Visium spots without labels), pathway-focused candidate evidence tables (immune checkpoints, chemokines, Notch, Wnt, TGF-beta) with contact vs secreted classes and PubMed co-mentions, and multi-sample Tensor-cell2cell and MISTy models. Xenium/MERFISH/CosMx cells, Visium spots, scRNA-seq. Route-specific steps follow the main workflow. Needs cell type labels first (spatial-transcriptomics-cell-type-annotation or a deconvolution skill).

日本語の概要は準備中です。原文の説明を表示しています。

jaechang-hits/SciAgent-Skillsスター 384(GitHub 384 + cccskills 0)

実行スコア 未検証

インストール方法を見る

含まれるファイル(1)

  • SKILL.md36.3 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Spatial Cell-Cell Communication

Overview

Expression-based ligand-receptor (LR) inference ranks pairs that could signal; spatial data adds whether sender and receiver actually sit next to each other. This skill runs one inference (LIANA consensus by default, or CellPhoneDB statistics), puts the result into one table, keeps interactions whose cell types are spatial neighbours, scores LR co-expression at each location, and narrows the table to the user's question with an evidence class per candidate. Expression of both partners never proves signalling: every result is a candidate.

QuestionRoute
Which LR pairs between my cell types, and are the types neighboursWorkflow Steps 1-6
Permutation p-values per pair, DEG-driven or microenvironment-restricted testsRoute: CellPhoneDB v5 Statistics, in place of Step 2
Where on the tissue an LR pair is co-expressed (labels not needed)Step 5; p-value/category layers in Route: LIANA Multi-sample
Communication differences across samples or conditionsRoute: LIANA Multi-sample (by-sample ranking, Tensor-cell2cell)
What in the neighbourhood explains cell type compositionRoute: LIANA Multi-sample (MISTy)
Literature support and which candidates to validateAfter Step 6, Route: Literature Evidence

All routes are in ## Task Routes below the Workflow.

When to Use

  • Finding which annotated cell types may signal to each other in a spatial section
  • Separating interactions between neighbouring cell types from those between distant ones
  • Testing which LR pairs are specific to a sender-receiver pair with CellPhoneDB p-values
  • Mapping where on the tissue a ligand and its receptor are co-expressed, also on Visium spots
  • Asking about one pathway family (immune checkpoints, chemokines, Notch, Wnt, TGF-beta) after inference
  • Comparing communication programs across samples or conditions
  • For neighbourhood enrichment or co-occurrence alone, without LR pairs, use spatial-transcriptomics-analysis-squidpy

Prerequisites

  • Input: log-normalised expression in X (not z-scored) and obsm["spatial"]
  • Labels: a cell type column in obs for Steps 2-4 and 6 (spatial-transcriptomics-cell-type-annotation). Visium spots have no labels: Step 5 works without them; Steps 2-4 need a deconvolution first (spatial-transcriptomics-deconvolution-cell2location)
  • Genes: gene symbols; CellPhoneDB is human-only (mouse via orthologs, see its reference)
  • Python packages: liana, squidpy, scanpy; cellphonedb, cell2cell only for their routes
# 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 liana squidpy scanpy

Pre-flight Interview

decisions:
  - id: D1
    param: CELL_TYPE_KEY
    kind: required
    source: data
    ask: "Which column holds the cell types whose communication you want to study?"
    default: null

  - id: D2
    param: RESOURCE
    kind: required
    source: data
    ask: "The gene symbols look <human/mouse>: use the <human/mouse> ligand-receptor database?"
    default: "consensus for human symbols, mouseconsensus for mouse symbols"

  - id: D3
    param: METHOD
    kind: required
    source: user
    ask: "Rank interactions by agreement across several methods (LIANA consensus, recommended), or do you need CellPhoneDB permutation p-values per pair (slower, human symbols)?"
    default: "LIANA consensus"

  - id: D4
    param: QUERY
    kind: optional
    source: user
    ask: "Is there a pathway (immune checkpoints, chemokines, growth factors, Notch, Wnt, TGF-beta) or a sender -> receiver direction you care about?"
    default: "all pathways and all cell type pairs"

  - id: D5
    param: SAMPLE_KEY
    kind: required
    source: data
    skip_if: "the data holds a single sample"
    ask: "Which column identifies the samples, and which one the condition to compare?"
    default: null

  - id: D6
    param: expr_prop
    kind: optional
    source: user
    ask: "Minimum fraction of a cell type that must express each partner gene?"
    default: "0.1"

  - id: D7
    param: NEIGHBOUR_Z
    kind: optional
    source: user
    ask: "How strongly must two cell types co-locate to count as neighbours?"
    default: "neighbourhood z > 2 over 15 nearest neighbours"

  - id: D8
    param: BANDWIDTH
    kind: optional_conditional
    source: data
    ask: "The coordinates are in <microns/pixels>; how wide should the local neighbourhood be?"
    default: "from li.pp.query_bandwidth: a few to tens of neighbours per location"

  - id: D9
    param: REGION_KEY
    kind: optional_conditional
    source: data
    depends_on: [D3]
    skip_if: "METHOD is LIANA consensus"
    ask: "Should CellPhoneDB only test cell types that share a tissue region? If so, which region/niche column?"
    default: "no restriction"

  - id: D10
    param: n_perms
    kind: optional
    source: user
    ask: "How many permutations for p-values?"
    default: "1000 for neighbourhood/CellPhoneDB tests, 100 for local LR scores"

Ask D1-D3 together, with D5 when obs has a sample-like column. Offer the obs columns that look like cell type labels for D1; if none exist on single-cell-resolution data, stop and annotate first. On Visium without a deconvolution, say that only Step 5 can run.

Workflow

  1. Inputs, organism and panel coverage
  2. Rank LR pairs between cell types
  3. Harmonise results into one table
  4. Keep spatially adjacent pairs
  5. Local LR co-expression on the tissue
  6. Focus on the question and classify candidates

Step 1: Inputs and Panel Coverage

import liana as li
import numpy as np
import pandas as pd
import scanpy as sc

adata = sc.read_h5ad("celltype_annotated.h5ad")
CELL_TYPE_KEY = "cell_type"                         # D1
adata.obs[CELL_TYPE_KEY] = adata.obs[CELL_TYPE_KEY].astype("category")
print(adata.obs[CELL_TYPE_KEY].value_counts())

head = adata.var_names[:200]                         # mouse "Actb" vs human "ACTB"
n_mouse = sum(len(g) > 1 and g[0].isupper() and g[1:].islower() for g in head)
RESOURCE = "mouseconsensus" if n_mouse > len(head) / 2 else "consensus"   # D2
print(f"resource: {RESOURCE}; max X = {adata.X.max():.1f} (expect < ~15 for log data)")

resource = li.rs.select_resource(RESOURCE)
genes = set(adata.var_names)
def present(complex_):
    return all(g in genes for g in complex_.split("_"))
covered = resource[resource["ligand"].map(present) & resource["receptor"].map(present)]
print(f"LR pairs measurable: {len(covered)} of {len(resource)}")

A targeted panel (Xenium ~400 genes) measures only a small share of the resource. Report that coverage with the results: absent pairs were not measured, not ruled out.

Step 2: Rank LR Pairs Between Cell Types

LIANA consensus (magnitude_rank, specificity_rank; lower = stronger, aggregated over CellPhoneDB, CellChat, NATMI, Connectome and others). For CellPhoneDB statistics, run Route: CellPhoneDB v5 Statistics instead and continue at Step 3 with SOURCE = "cellphonedb".

li.mt.rank_aggregate(adata, groupby=CELL_TYPE_KEY, resource_name=RESOURCE,
                     expr_prop=0.1, use_raw=False, key_added="liana_res", verbose=True)
res = adata.uns["liana_res"]
res.to_csv("liana_results.csv", index=False)
print(res.sort_values("magnitude_rank").head(15)[
    ["source", "target", "ligand_complex", "receptor_complex", "magnitude_rank", "specificity_rank"]])
p = li.pl.dotplot(adata=adata, colour="magnitude_rank", size="specificity_rank",
                  inverse_colour=True, inverse_size=True, top_n=20,
                  orderby="magnitude_rank", orderby_ascending=True, figure_size=(10, 8),
                  return_fig=True)          # restrict with source_labels=[...], target_labels=[...]
p.save("liana_dotplot.png", dpi=150)
ColumnMeaning
source / targetSender (ligand) / receiver (receptor) cell type
ligand_complex / receptor_complexGenes; complexes joined by _
lr_meansMean of ligand and receptor expression
magnitude_rank / specificity_rankConsensus ranks of strength / cell type specificity

Step 3: Harmonise Results

One row per (sender, receiver, ligand, receptor) with a score and a p-value or rank, from either tool, so Steps 4 and 6 run the same way:

SOURCE = "liana"            # or "cellphonedb" (significant_long.csv from its reference)
TOP_RANK = 0.05             # LIANA magnitude_rank cutoff for "top" interactions

if SOURCE == "liana":
    r = res[res["magnitude_rank"] <= TOP_RANK]
    lr = pd.DataFrame({
        "sender": r["source"], "receiver": r["target"],
        "ligand": r["ligand_complex"], "receptor": r["receptor_complex"],
        "score": r["lr_means"], "p_or_rank": r["magnitude_rank"], "specificity": r["specificity_rank"]})
else:
    r = pd.read_csv("cellphonedb/significant_long.csv")
    r = r[r["pval"] < 0.05]
    for c in ("partner_a", "partner_b"):
        r[c] = r[c].str.replace(r"^(simple|complex):", "", regex=True)
    swap = r["receptor_a"].astype(bool) & ~r["receptor_b"].astype(bool)   # orient ligand -> receptor
    lr = pd.DataFrame({
        "sender": np.where(swap, r["cell_b"], r["cell_a"]), "receiver": np.where(swap, r["cell_a"], r["cell_b"]),
        "ligand": np.where(swap, r["partner_b"], r["partner_a"]),
        "receptor": np.where(swap, r["partner_a"], r["partner_b"]),
        "score": r["mean"], "p_or_rank": r["pval"], "specificity": r.get("score")})

lr["genes"] = (lr["ligand"] + "_" + lr["receptor"]).str.upper().str.split("_").map(set)
if SOURCE == "cellphonedb" and "genes" in r:      # subunit genes of complexes (saved as "{'ITGA4', ...}")
    import ast
    lr["genes"] = [g | (ast.literal_eval(x) if isinstance(x, str) and x.startswith("{") else set())
                   for g, x in zip(lr["genes"], r["genes"])]
lr = lr.reset_index(drop=True)
print(f"{len(lr)} rows, {lr[['ligand', 'receptor']].drop_duplicates().shape[0]} LR pairs")

For LIANA lower p_or_rank / specificity is better; for CellPhoneDB p_or_rank is a p-value and specificity the 0-100 interaction score (higher is better). CellPhoneDB complex names (e.g. integrin_a4b1_complex) are not symbols; the code adds their subunit genes.

Step 4: Keep Spatially Adjacent Pairs

import squidpy as sq

sq.gr.spatial_neighbors(adata, coord_type="generic", n_neighs=15)   # Visium: coord_type="grid", n_rings=1
sq.gr.nhood_enrichment(adata, cluster_key=CELL_TYPE_KEY, n_perms=1000, seed=0)
cats = adata.obs[CELL_TYPE_KEY].cat.categories
z = pd.DataFrame(adata.uns[f"{CELL_TYPE_KEY}_nhood_enrichment"]["zscore"], index=cats, columns=cats)
z.to_csv("nhood_enrichment_z.csv")

NEIGHBOUR_Z = 2                                                     # D7
lr["neighbour_z"] = [z.loc[s, t] if s in z.index and t in z.columns else np.nan
                     for s, t in zip(lr["sender"], lr["receiver"])]
lr["autocrine"] = lr["sender"] == lr["receiver"]
adjacent = lr[(lr["neighbour_z"] > NEIGHBOUR_Z) | lr["autocrine"]].sort_values("p_or_rank")
adjacent.drop(columns=["genes"]).to_csv("lr_adjacent_pairs.csv", index=False)
print(f"{len(adjacent)} of {len(lr)} top interactions are between neighbouring (or the same) cell types")

Autocrine pairs are always adjacent; read them separately. On Visium spots the label-based test is coarse; prefer Step 5 there. Show the co-location matrix the filter is based on:

sq.pl.nhood_enrichment(adata, cluster_key=CELL_TYPE_KEY, cmap="coolwarm", vmin=-10, vmax=10,
                       figsize=(7, 6), save="nhood_enrichment.png")

Step 5: Local LR Co-expression on the Tissue

Scores each LR pair at every location, so it also works on Visium spots without labels. Pick the bandwidth from the coordinate scale first:

spatial_neighbors = li.pp.spatial_neighbors if hasattr(li, "pp") else li.ut.spatial_neighbors  # li.ut in older liana
plot, bw_table = li.pp.query_bandwidth(coordinates=adata.obsm["spatial"], start=0, end=500, interval_n=20)
plot.save("liana_bandwidth.png", dpi=150)
print(bw_table.head(10))      # average neighbours per bandwidth: aim for a few to tens

BANDWIDTH = 100               # D8; coordinate units: ~50-100 um Xenium; Visium full-res px ~2-3 spot spacings
spatial_neighbors(adata, bandwidth=BANDWIDTH, cutoff=0.1, kernel="gaussian", set_diag=True)
lrdata = li.mt.bivariate(adata, resource_name=RESOURCE, local_name="cosine", global_name="morans",
                         n_perms=100, mask_negatives=False, add_categories=True, nz_prop=0.1,
                         use_raw=False, verbose=True)
summary = lrdata.var.sort_values("morans", ascending=False)
summary.to_csv("liana_bivariate_summary.csv")
lrdata.write_h5ad("liana_bivariate.h5ad")
print(summary.head(10))
sc.pl.embedding(lrdata, basis="spatial", color=summary.index[:4].tolist(), cmap="coolwarm", save="_lr_local.png")

var["morans"] is the global spatial co-pattern of ligand and receptor; the per-location p-values (layers["pvals"]) and categories (layers["cats"]: both high / one high / both low) are read in Route: LIANA Multi-sample.

Step 6: Focus and Classify Candidates

Narrow the table to the user's question (D4) and give each candidate an evidence class. Matching is per gene symbol, so CD4 does not hit CD40.

PATHWAYS = {
    "immune_checkpoint": ["PDCD1", "CD274", "PDCD1LG2", "CTLA4", "CD28", "CD80", "CD86",
                          "LAG3", "HAVCR2", "LGALS9", "TIGIT", "PVR", "NECTIN2"],
    "chemokine": ["CXCL*", "CCL*", "CX3CL1", "XCL*", "CXCR*", "CCR*", "CX3CR1", "XCR1", "ACKR*"],
    "growth_factor": ["EGF", "EGFR", "ERBB*", "VEGF*", "KDR", "FLT1", "FLT4", "FGF*", "FGFR*",
                      "PDGF*", "HGF", "MET", "IGF1", "IGF1R"],
    "notch": ["NOTCH*", "JAG1", "JAG2", "DLL1", "DLL3", "DLL4"],
    "wnt": ["WNT*", "FZD*", "LRP5", "LRP6", "RSPO*", "LGR*"],
    "tgfb_bmp": ["TGFB*", "TGFBR*", "BMP*", "BMPR*", "ACVR*", "GDF*"],
}
def matches(genes, patterns):
    return any(g.startswith(p[:-1]) if p.endswith("*") else g == p for g in genes for p in patterns)
for name, pats in PATHWAYS.items():
    lr[name] = lr["genes"].map(lambda g: matches(g, pats))
lr["pathway"] = lr[list(PATHWAYS)].apply(lambda row: ",".join(k for k, v in row.items() if v), axis=1)

QUERY = list(PATHWAYS)                               # D4, e.g. ["immune_checkpoint", "chemokine"]
SENDERS, RECEIVERS = None, None                      # e.g. {"Tumor cell"}, {"T cell"}
q = lr[lr[QUERY].any(axis=1)]
if SENDERS: q = q[q["sender"].isin(SENDERS)]
if RECEIVERS: q = q[q["receiver"].isin(RECEIVERS)]

CONTACT = {"CD274", "PDCD1", "PDCD1LG2", "CTLA4", "CD28", "CD80", "CD86", "JAG1", "JAG2",
           "DLL1", "DLL3", "DLL4", "NOTCH1", "NOTCH2", "NOTCH3", "NOTCH4", "TIGIT", "PVR", "NECTIN2"}
def evidence_class(row):
    if pd.isna(row["neighbour_z"]):
        return "no spatial test"
    if row["neighbour_z"] > NEIGHBOUR_Z or row["autocrine"]:
        return "co-located"
    return "unlikely (contact, not co-located)" if row["ligand"].split("_")[0] in CONTACT else "possible paracrine"
q = q.assign(evidence_class=q.apply(evidence_class, axis=1))
ev = q.drop(columns=["genes"]).sort_values(["p_or_rank", "score"], ascending=[True, False])
ev.to_csv("lr_candidate_evidence.csv", index=False)
print(ev.groupby(["pathway", "evidence_class"]).size())

CONTACT is a starting list of membrane-bound partners; extend it for the pathways in QUERY. Plot the top candidates:

import matplotlib.pyplot as plt

top = ev.head(30).copy()
top["lr"] = top["ligand"] + " -> " + top["receptor"]
top["pair"] = top["sender"].astype(str) + " -> " + top["receiver"].astype(str)
fig, ax = plt.subplots(figsize=(max(8, 0.4 * top["pair"].nunique()), max(6, 0.3 * top["lr"].nunique())))
s = ax.scatter(top["pair"], top["lr"], c=top["neighbour_z"], cmap="coolwarm", vmin=-5, vmax=5,
               s=30 + 200 * top["score"] / top["score"].max())
plt.colorbar(s, label="neighbourhood z (sender-receiver)"); plt.xticks(rotation=90)
plt.tight_layout(); plt.savefig("lr_candidates.png", dpi=150); plt.close()
Evidence patternInterpretation
Top-ranked / significant, co-locatedStrong candidate; check literature (Route: Literature Evidence)
Top-ranked, not co-located, secreted ligandPossible paracrine signal; weaker
Top-ranked, not co-located, contact-dependentUnlikely in this tissue
Only one partner above expr_propNot supported

State validation routes (spatial protein staining, receptor blocking, perturbation) for the top candidates.

Task Routes

Each route continues the Workflow session (same variable names). Only the calls that differ from the Workflow, or that fail silently when written another way, are shown.

Route: CellPhoneDB v5 Statistics

Scores each curated LR pair, including multi-subunit complexes, for every ordered pair of cell types; the statistical method tests specificity by shuffling labels. Run it in place of Step 2, write cellphonedb/significant_long.csv, then continue at Step 3 with SOURCE = "cellphonedb". Install pixi add --pypi cellphonedb.

MethodCallUse when
Simplecpdb_analysis_methodQuick look, no statistics
Statisticalcpdb_statistical_analysis_methodDiscovery; permutation p-values
DEG-basedcpdb_degs_analysis_methodInteractions driven by genes up in one cell type; result has relevant_interactions instead of pvalues

Inputs and gotchas:

  • Database: without GitHub access, download cellphonedb.zip from the ventolab/cellphonedb-data releases by hand into CPDB_DIR.
  • counts.h5ad holds normalised (log) expression, not z-scored, with human HGNC symbols. Cell type names must not contain |, the result separator.
  • Mouse: map symbols 1:1 to human first: li.rs.get_hcop_orthologs(target_organism="mouse", columns=["human_symbol", "mouse_symbol"], min_evidence=3), keep rows unique on both columns, rename var_names.
  • Microenvironments (D9): a cell_type / microenvironment table built from a region column, a type counting as present above ~2% of the region's cells; only pairs of co-located types are tested.
  • DEG method: degs.tsv with cluster / gene columns of up-regulated genes (e.g. rank_genes_groups, adj p < 0.05, logFC > 0.25).
import os
from cellphonedb.src.core.methods import cpdb_statistical_analysis_method
from cellphonedb.utils import db_utils

CPDB_DIR = os.path.abspath("ref_cache/cellphonedb/v5.0.0")
CPDB_ZIP = os.path.join(CPDB_DIR, "cellphonedb.zip")
if not os.path.exists(CPDB_ZIP):
    os.makedirs(CPDB_DIR, exist_ok=True)
    db_utils.download_database(CPDB_DIR, "v5.0.0")
OUT = "cellphonedb"
os.makedirs(OUT, exist_ok=True)
sc.AnnData(X=adata.X, obs=pd.DataFrame(index=adata.obs_names),
           var=pd.DataFrame(index=adata.var_names)).write_h5ad(f"{OUT}/counts.h5ad")
pd.DataFrame({"barcode_sample": adata.obs_names, "cell_type": adata.obs[CELL_TYPE_KEY].astype(str).values}
             ).to_csv(f"{OUT}/meta.tsv", sep="\t", index=False)

res = cpdb_statistical_analysis_method.call(
    cpdb_file_path=CPDB_ZIP, meta_file_path=f"{OUT}/meta.tsv", counts_file_path=f"{OUT}/counts.h5ad",
    counts_data="hgnc_symbol", microenvs_file_path=None,      # or f"{OUT}/microenvs.tsv"
    score_interactions=True, iterations=1000, threshold=0.1, threads=4, debug_seed=42, pvalue=0.05,
    subsampling=False,             # >~100k cells: True + subsampling_num_cells, subsampling_log=False
    output_path=OUT, output_suffix="stat")

Long table read by Step 3 (cell_a expresses partner_a; Step 3 orients ligand -> receptor from receptor_a / receptor_b):

sig = res["significant_means"]
labels = set(adata.obs[CELL_TYPE_KEY].astype(str))
pair_cols = [c for c in sig.columns if c.count("|") == 1 and set(c.split("|")) <= labels]
meta_cols = [c for c in ["id_cp_interaction", "interacting_pair", "partner_a", "partner_b", "secreted",
                         "receptor_a", "receptor_b"] if c in sig.columns]
long = sig.melt(id_vars=meta_cols, value_vars=pair_cols, var_name="pair", value_name="mean").dropna(subset=["mean"])
pv = res["pvalues"].melt(id_vars=["id_cp_interaction"], value_vars=pair_cols, var_name="pair", value_name="pval")
long = long.merge(pv, on=["id_cp_interaction", "pair"], how="left")
long[["cell_a", "cell_b"]] = long["pair"].str.split("|", expand=True)
if "interaction_scores" in res:
    sc_long = res["interaction_scores"].melt(id_vars=["id_cp_interaction"], value_vars=pair_cols,
                                             var_name="pair", value_name="score")
    long = long.merge(sc_long, on=["id_cp_interaction", "pair"], how="left")
long["genes"] = long["id_cp_interaction"].map(
    res["deconvoluted"].groupby("id_cp_interaction")["gene_name"].apply(lambda s: set(s.dropna())))
long.to_csv(f"{OUT}/significant_long.csv", index=False)
SignalMeaning
Low p-value, high meanExpressed and specific to this cell type pair - strongest candidates
High mean, p >= 0.05Broadly expressed; not specific to this pair
High interaction_scores (0-100)Partners specific to these two types relative to all types
secreted == TrueLigand can act at a distance; membrane-bound pairs need contact

Two conditions: run the statistical method per condition with the same database and parameters and compare the significant sets; this is descriptive, since cell numbers per type differ between conditions and change each test's power. ktplotspy (plot_cpdb_heatmap, plot_cpdb) draws publication-style figures from res.

Route: LIANA Multi-sample, Local Score Layers, MISTy

Uses adata, CELL_TYPE_KEY, RESOURCE, BANDWIDTH and lrdata from the Workflow. Functions follow the current li.pp / li.ms namespaces (older releases: li.ut / li.multi).

  • Per sample and condition (D5): li.mt.rank_aggregate.by_sample(adata, groupby=CELL_TYPE_KEY, sample_key=SAMPLE_KEY, resource_name=RESOURCE, expr_prop=0.1, use_raw=False, n_perms=None, return_all_lrs=True), then pivot magnitude_rank by condition (median per interaction). With few samples per condition this is descriptive; no p-values.
  • Tensor-cell2cell (pixi add --pypi cell2cell, several samples per condition): factorises samples x LR x sender x receiver into patterns. Test a factor's context loadings between conditions across samples (Mann-Whitney) before calling it condition-specific; with few samples, exploratory.
  • Local score layers of lrdata (Step 5): X local score per location (ligand^receptor), layers["pvals"] per-location permutation p-value, layers["cats"] 1 = both high, -1 = one high one low, 0 = both low; var["morans"] / var["morans_pvals"] global co-pattern. Plot layer="cats" to separate true co-localisation (1) from one-partner regions (-1).
  • MISTy: for each target (e.g. cell2location proportions) how well it is predicted by the same spot (intra), direct neighbours (juxta) and the wider area (para). gain_R2 = what the spatial views add over intra; interactions with view juxta/para name the predicting neighbours. Pass extra=lrdata[:, top_lr] to ask whether local LR activity predicts composition beyond composition itself.
  • One method instead of the consensus: li.mt.cellphonedb(adata, groupby=CELL_TYPE_KEY, resource_name=RESOURCE, expr_prop=0.1, use_raw=False, n_perms=1000, seed=0, key_added="cpdb_res") gives cellphone_pvals without installing cellphonedb.
import cell2cell as c2c
from collections import defaultdict

SAMPLE_KEY, CONDITION_KEY = "sample", "condition"          # D5
li.mt.rank_aggregate.by_sample(adata, groupby=CELL_TYPE_KEY, sample_key=SAMPLE_KEY, resource_name=RESOURCE,
                               expr_prop=0.1, use_raw=False, n_perms=None, return_all_lrs=True)
tensor = li.ms.to_tensor_c2c(adata, sample_key=SAMPLE_KEY, score_key="magnitude_rank", how="outer_cells")
ctx = adata.obs[[SAMPLE_KEY, CONDITION_KEY]].drop_duplicates()
context_dict = defaultdict(lambda: "Unknown", zip(ctx[SAMPLE_KEY], ctx[CONDITION_KEY]))
meta = c2c.tensor.generate_tensor_metadata(interaction_tensor=tensor, metadata_dicts=[context_dict, None, None, None],
                                           fill_with_order_elements=True)
tensor = c2c.analysis.run_tensor_cell2cell_pipeline(
    tensor, meta, copy_tensor=True, rank=None, tf_optimization="regular", random_state=0, device="cpu",
    elbow_metric="error", upper_rank=15, tf_init="random", sample_col="Element", group_col="Category",
    output_fig=False)                         # rank=None: elbow search (slow); or fix e.g. 6
contexts = tensor.factors["Contexts"]         # also "Ligand-Receptor Pairs", "Sender Cells", "Receiver Cells"
from liana.method import genericMistyData
from liana.method.sp import LinearModel

adata.obsm["compositions"] = pd.read_csv("cell2location_proportions.csv", index_col=0).loc[adata.obs_names]
comps = li.pp.obsm_to_adata(adata, "compositions")
misty = genericMistyData(intra=comps, cutoff=0.05, bandwidth=BANDWIDTH, n_neighs=6)
misty(model=LinearModel, k_cv=10, seed=1337, bypass_intra=False, verbose=True)   # or RandomForestModel
misty.uns["target_metrics"].to_csv("misty_target_metrics.csv", index=False)       # intra_R2, multi_R2, gain_R2
misty.uns["interactions"].to_csv("misty_interactions.csv", index=False)           # target, predictor, view, importances

Route: Literature Evidence for Candidates

Adds PubMed support to the Step 6 candidate table (ev). NCBI E-utilities need no key below 3 requests per second; cap the number of pairs queried. The judgment is made from the evidence table, not by the code.

import time
import requests

EUTILS = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
CONTEXT = ""        # optional, e.g. "lung cancer" to count context-specific co-mentions

def pubmed_count(ligand, receptor):
    a, b = ligand.split("_")[0], receptor.split("_")[0]   # first subunit for complexes
    term = f"{a}[tiab] AND {b}[tiab]" + (f" AND ({CONTEXT})" if CONTEXT else "")
    r = requests.get(EUTILS, params={"db": "pubmed", "term": term, "retmode": "json", "retmax": 0}, timeout=30)
    r.raise_for_status(); time.sleep(0.4)
    return int(r.json()["esearchresult"]["count"])

pairs = ev[["ligand", "receptor"]].drop_duplicates().head(50)
pairs["pubmed_n"] = [pubmed_count(l, r) for l, r in zip(pairs["ligand"], pairs["receptor"])]
ev = ev.merge(pairs, on=["ligand", "receptor"], how="left")
ev["final_class"] = [("strong, known" if (n or 0) > 0 else "novel, co-located") if c == "co-located" else c
                     for c, n in zip(ev["evidence_class"], ev["pubmed_n"])]
ev.to_csv("lr_candidate_evidence.csv", index=False)

A zero count with strong statistics is a novelty candidate, not an error. To read the papers, fetch abstracts with efetch.fcgi?db=pubmed&id=<ids>&rettype=abstract&retmode=text for the ids of an esearch with retmax=5&sort=relevance.

Key Parameters

ParameterDefaultEffect
expr_prop0.1Min fraction of a cell type expressing each gene
TOP_RANK0.05LIANA magnitude_rank cutoff for top interactions
NEIGHBOUR_Z2How strongly two types must co-locate
n_neighs15Size of the spatial neighbourhood (cells)
BANDWIDTH100Spatial kernel for local scores, in coordinate units
nz_prop0.1Min fraction of locations expressing each partner (local scores)
QUERY / SENDERS / RECEIVERSallRestricts the candidate table to the question
threshold (CellPhoneDB)0.1Min fraction of a cell type expressing each partner gene
iterations (CellPhoneDB)1000Label permutations; fewer = coarser p-values
pvalue0.05Significance cutoff for significant_means
MIN_FRAC (microenvs)0.02Presence cutoff for a type in a region
DEG cutoffs (CellPhoneDB DEG method)adj p < 0.05, logFC > 0.25Which genes count as up-regulated
subsampling_num_cells1/3 of cellsGeometric sketching size for large datasets
rank (Tensor-cell2cell)None (elbow)Number of communication patterns; fix 3-6 when the elbow search is slow
how (Tensor-cell2cell)"outer_cells"Keep cell types missing in some samples
bandwidth (MISTy)BANDWIDTHPara view kernel width, in coordinate units
k_cv (MISTy)10Cross-validation folds per target
CONTEXTemptyRestricts literature counts to a disease/tissue
Pairs queried50Number of PubMed requests
Request delay0.4 sStays under the 3 requests/second limit

Common Recipes

Recipe: Sender-Receiver Count Map

import seaborn as sns

topn = res[(res["magnitude_rank"] <= 0.05) & (res["specificity_rank"] <= 0.05)]
counts = topn.groupby(["source", "target"]).size().unstack(fill_value=0)
fig, ax = plt.subplots(figsize=(9, 8))
sns.heatmap(counts, cmap="YlOrRd", annot=True, fmt="d", ax=ax)
ax.set_xlabel("receiver"); ax.set_ylabel("sender")
plt.tight_layout(); plt.savefig("liana_sender_receiver_counts.png", dpi=150); plt.close()

Recipe: Distance Range of One Cell Type Pair

Neighbourhood enrichment is a yes/no at one scale; co-occurrence shows over which distance two types stay together:

ANCHOR = "Macrophage"                                  # a category of CELL_TYPE_KEY
sub = adata[np.random.default_rng(0).choice(adata.n_obs, min(adata.n_obs, 20000), replace=False)].copy()
occ, interval = sq.gr.co_occurrence(sub, cluster_key=CELL_TYPE_KEY, interval=50, copy=True)
sub_cats = list(sub.obs[CELL_TYPE_KEY].cat.categories)
dist = interval[1:] if len(interval) == occ.shape[2] + 1 else interval
curve = pd.DataFrame(occ[sub_cats.index(ANCHOR)].T, index=np.round(dist, 1), columns=sub_cats)
print(curve.iloc[:10].round(2))          # ratio > 1 at short distance = attraction

Recipe: Visium - Link Local LR Scores to Deconvolved Cell Types

abund = pd.read_csv("cell2location_abundance.csv", index_col=0).loc[lrdata.obs_names]
lr_top = summary.index[:20]
local = pd.DataFrame(np.asarray(lrdata[:, lr_top].X.todense()) if hasattr(lrdata.X, "todense")
                     else lrdata[:, lr_top].X, index=lrdata.obs_names, columns=lr_top)
corr = pd.DataFrame({ct: local.corrwith(abund[ct]) for ct in abund.columns}).round(2)
corr.to_csv("lr_local_vs_abundance.csv")
print(corr.apply(lambda r: ", ".join(f"{k} {v}" for k, v in r.nlargest(3).items()), axis=1))

Recipe: Are Ligand and Receptor Actually Expressed?

Aggregated scores can hide that only a few cells express a partner:

def frac_expr(cell_type, gene):
    if gene not in adata.var_names:
        return np.nan
    x = adata[adata.obs[CELL_TYPE_KEY] == cell_type, gene].X
    x = x.toarray() if hasattr(x, "toarray") else np.asarray(x)
    return float((x > 0).mean())

ev_top = ev.head(30).copy()
ev_top["ligand_frac_sender"] = [frac_expr(s, l.split("_")[0]) for s, l in zip(ev_top["sender"], ev_top["ligand"])]
ev_top["receptor_frac_receiver"] = [frac_expr(r, rc.split("_")[0]) for r, rc in zip(ev_top["receiver"], ev_top["receptor"])]
print(ev_top[["sender", "receiver", "ligand", "receptor", "ligand_frac_sender", "receptor_frac_receiver"]].round(2))

Expected Outputs

  • liana_results.csv, liana_dotplot.png - LR ranking per sender-receiver pair
  • nhood_enrichment_z.csv, nhood_enrichment.png - cell type co-location z-scores
  • lr_adjacent_pairs.csv - top interactions whose cell types are neighbours
  • liana_bandwidth.png, liana_bivariate_summary.csv, liana_bivariate.h5ad - local LR scores
  • lr_candidate_evidence.csv, lr_candidates.png - candidates with pathway and evidence class
  • CellPhoneDB: ref_cache/cellphonedb/v5.0.0/cellphonedb.zip (cached database), cellphonedb/meta.tsv and counts.h5ad (inputs), cellphonedb/statistical_analysis_*_stat.txt (means, pvalues, significant_means, deconvoluted, interaction_scores), cellphonedb/significant_long.csv (read by Step 3)
  • LIANA multi-sample / MISTy: tensor.factors (context, LR, sender, receiver loadings), misty_target_metrics.csv, misty_interactions.csv
  • Literature: pubmed_n and final_class columns added to lr_candidate_evidence.csv

Troubleshooting

ProblemCauseFix
Very few resultsTargeted panel lacks LR genesReport coverage (Step 1); do not extrapolate
KeyError on cell typeColumn not categorical, or label spelling differs between results and adataastype("category"); use the same label column for inference and Step 4
All local scores near 0Bandwidth smaller than spot/cell spacingRe-read the query_bandwidth table; raise BANDWIDTH
Mouse genes not matchedHuman resourceRESOURCE = "mouseconsensus"
Results dominated by housekeeping-like pairsData was z-scored or not normalisedUse normalised (log) expression
Slow permutationsMany cellsLower n_perms for exploration, report it
AttributeError: module 'liana' has no attribute 'pp' / msOlder lianaStep 5 falls back to li.ut; elsewhere use li.ut.* / li.multi.*, or upgrade liana
Pathway query returns nothingPathway genes not on the panel, or mouse symbolsCheck adata.var_names; genes are upper-cased for matching
Almost no interactionsMouse or Ensembl IDs in var_namesMap to human HGNC symbols (ortholog step of the CellPhoneDB route)
Some clusters/cell_types in microenvironments file are not present in metadataLabel mismatch between filesBuild both files from the same cell type column
Very slow / out of memoryMany cellssubsampling=True with subsampling_num_cells; raise threads
download_database failsNo network access to GitHubDownload cellphonedb.zip from the release page manually into CPDB_DIR
Pair columns missing a cell typeType below threshold for all genes or absent from microenvsCheck value_counts; lower MIN_FRAC
Tensor factorization failsToo few samples or cell types missing in many sampleshow="outer_cells", fix rank low (3-4), add samples
MISTy juxta view errorsquidpy missingpixi add --pypi squidpy
KeyError aligning compositionsSpot IDs differ between filesAlign on adata.obs_names before obsm
PubMed HTTP 429Too many requestsIncrease time.sleep; lower the number of pairs
Literature counts inflatedGene symbol is a common word (e.g. MET, KIT)Add CONTEXT or check abstracts
Complex pairs never matchCellPhoneDB complex names instead of genesUse the subunit genes from Step 3

Bundled Resources

Merged from four former skills: spatial-transcriptomics-cell-cell-communication (Steps 1-2, 4-5, recipes), cellphonedb-cell-cell-interaction (Route: CellPhoneDB v5 Statistics; its adjacency recipe is Step 4), liana-spatial-cell-cell-communication (Steps 1-2 and 5, Route: LIANA Multi-sample) and spatial-single-cell-ligand-receptor-discovery (Steps 3, 4, 6, expression-fraction recipe, Route: Literature Evidence).

Related Skills

  • spatial-transcriptomics-cell-type-annotation - labels for Steps 2-4
  • spatial-transcriptomics-deconvolution-cell2location - abundances for Visium
  • spatial-mapping-tangram - per-spot proportions or per-nucleus labels from a reference
  • spatial-domain-detection - region labels for CellPhoneDB microenvironments
  • spatial-transcriptomics-analysis-squidpy - further spatial statistics and squidpy ligrec

References

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは 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-Skills3842026年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-Skills3842026年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-Skills3842026年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-Skills3842026年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-Skills3842026年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-Skills3842026年10月22日 更新

jaechang-hits のスキルをすべて見る

このスキルの問題を報告する