cell-communication
Overview
Cell-cell communication (CCC) inference predicts which cell types signal to which
others by scoring co-expression of known ligand-receptor (L-R) pairs across
annotated cell populations. Input is an annotated .h5ad with cell-type labels
in adata.obs and log-normalized expression; output is a ranked table of
source -> target L-R interactions.
Tool landscape (2026):
- LIANA+ (
liana, Python) - recommended default. Runs multiple methods
(CellPhoneDB, NATMI, Connectome, logFC, SingleCellSignalR, CellChat,
geometric mean) and returns a robust consensus rank aggregate. scverse-native
(AnnData/MuData), also handles spatial and multi-condition data.
- CellPhoneDB (v5, Python) - permutation-test statistical method; also DEG-based
and receptor-activity (CellSign) modes. LIANA reimplements its core scoring.
- CellChat (v2, R only - GitHub) - pathway-level signaling
probabilities plus strong built-in visualization (chord, hierarchy, river plots).
Use from R/Seurat; not a Python package.
- squidpy
sq.gr.ligrec (Python) - CellPhoneDB-style permutation test inside the
scverse spatial stack; convenient when already in squidpy. For spatially-resolved
proximity use the spatialdata-squidpy skill.
Installation
LIANA+ requires Python 3.10-3.13. Latest release: liana 1.8.1 (July 2026).
uv pip install liana # core (AnnData/scanpy)
uv pip install "liana[extras]" # tensor-cell2cell / MOFA+ multi-condition tools
CellChat is R-based (install in R):
if (!requireNamespace("devtools", quietly = TRUE)) install.packages("devtools")
devtools::install_github("jinworks/CellChat") # not on CRAN or Bioconductor
Core workflow (LIANA on an h5ad)
import scanpy as sc
import liana as li
# 1. Load an ANNOTATED, log-normalized AnnData (cell types already assigned).
adata = sc.read_h5ad("annotated.h5ad")
# adata.X should be log1p-normalized counts (NOT raw counts, NOT scaled/z-scored).
# adata.obs["cell_type"] holds the labels to group by.
# 2. Run the consensus method (runs several methods, aggregates ranks).
li.mt.rank_aggregate(
adata,
groupby="cell_type", # cell-type column in adata.obs
resource_name="consensus", # human; use "mouseconsensus" for mouse
expr_prop=0.1, # drop L-R where either gene is expressed in <10% of a cluster
use_raw=False, # read adata.X; set True to read adata.raw
verbose=True,
)
# 3. Results are a DataFrame in adata.uns["liana_res"].
res = adata.uns["liana_res"]
print(res.columns.tolist())
# source, target, ligand_complex, receptor_complex, magnitude_rank, specificity_rank, ...
# 4. Top robust interactions: lower ranks = more relevant. Sort/filter, then inspect.
top = (res[res["specificity_rank"] <= 0.05]
.sort_values("magnitude_rank")
.head(20))
print(top[["source", "target", "ligand_complex", "receptor_complex",
"magnitude_rank", "specificity_rank"]])
# 5a. Dotplot: colour = magnitude (expression strength), size = specificity.
li.pl.dotplot(
adata=adata,
colour="magnitude_rank",
size="specificity_rank",
inverse_colour=True, # invert so stronger interactions look "hotter"
inverse_size=True, # invert so more-specific interactions look larger
source_labels=["Monocyte", "T cell"], # subset senders (optional)
target_labels=["B cell", "NK"], # subset receivers (optional)
top_n=15,
orderby="magnitude_rank",
orderby_ascending=True,
filter_fun=lambda x: x["specificity_rank"] <= 0.05,
figure_size=(9, 7),
)
# 5b. Chord / network view: circular source->target network of interactions.
li.pl.circle_plot(adata=adata, groupby="cell_type")
# li.pl.tileplot(...) is another summary view.
# List options if unsure of names:
li.mt.show_methods() # individual methods (cellphonedb, natmi, cellchat, ...)
li.rs.show_resources() # available L-R resources (consensus, mouseconsensus, ...)
Run an individual method instead of the consensus with e.g. li.mt.cellphonedb(adata, groupby="cell_type", ...); it writes the same adata.uns["liana_res"].
Gotchas / best practices
- Annotate first. CCC is only meaningful over trustworthy cell-type labels.
Do QC, clustering, and annotation before this step (see related skills). Garbage or
mixed clusters produce meaningless L-R calls.
- Feed log-normalized data, not raw counts and not scaled data. LIANA reads
adata.X (with use_raw=False) or adata.raw; it expects normalize_total +
log1p values. Z-scored/sc.pp.scaled matrices give wrong scores.
- Magnitude vs specificity - interpret both. Magnitude reflects how strongly
the L-R pair is co-expressed in a
source->target pair (is the interaction present
and how strong). Specificity reflects how uniquely that pair stands out for those
two cell types versus all other pairs. A strong-magnitude interaction can be
non-specific (housekeeping ligands). Prioritize hits that are both high-magnitude
and high-specificity. In the aggregate, magnitude_rank/specificity_rank are
aggregated ranks in [0,1] where lower = more relevant; sort ascending and filter
specificity_rank <= 0.05.
source -> target is directional. source = the ligand-expressing (sender)
cell type; target = the receptor-expressing (receiver). A->B and B->A are distinct.
- Human vs mouse resources.
resource_name="consensus" is human; use
"mouseconsensus" for mouse. For other species or a mixed reference, translate via
orthologs with li.rs.get_hcop_orthologs() / li.rs.translate_resource(). Never run
a human resource on mouse gene symbols.
- Comparing conditions/batches. Do not just pool samples. Run per sample/condition
and compare, or use LIANA+'s multi-sample tools (
by_sample scoring, plus
tensor-cell2cell / MOFA+ under li.multi in the [extras] install) to decompose
condition-specific communication. Confounded batches inflate apparent signaling; keep
batch structure explicit. See the LIANA+ docs for the current multi-condition API.
expr_prop and small clusters. Very small cell-type clusters and the
expr_prop threshold (default 0.1) heavily affect which pairs survive; report the
threshold and cluster sizes used.
- Don't over-invent signatures. For exact/advanced arguments (spatial
bivariate,
MistyData, multi-view) consult the LIANA+ docs: https://liana-py.readthedocs.io
CellChat's chord/pathway plots and CellPhoneDB v5 modes live in their own docs.
Use this vs related skills
Run scanpy / scrna-preprocessing-clustering (QC, normalize, cluster) and
cell-annotation (assign cell types) first to produce the annotated .h5ad; use
spatialdata-squidpy when you need spatially-resolved ligand-receptor proximity rather
than label-based CCC.
1---2name: cell-communication3description: Cell-cell / ligand-receptor communication analysis for single-cell data using LIANA+ (recommended consensus default), CellPhoneDB, CellChat (R), and squidpy's ligrec. Use for inferring cell-cell communication, ligand-receptor pairs, source->target signaling from an annotated .h5ad. Trigger terms - "cell-cell communication", "ligand-receptor", "CellPhoneDB", "CellChat", "LIANA", "cell interaction", "ligrec".4license: MIT5---67# cell-communication89## Overview1011Cell-cell communication (CCC) inference predicts which cell types signal to which12others by scoring co-expression of known ligand-receptor (L-R) pairs across13annotated cell populations. Input is an **annotated `.h5ad`** with cell-type labels14in `adata.obs` and log-normalized expression; output is a ranked table of15`source -> target` L-R interactions.1617Tool landscape (2026):1819- **LIANA+** (`liana`, Python) - **recommended default.** Runs multiple methods20 (CellPhoneDB, NATMI, Connectome, logFC, SingleCellSignalR, CellChat,21 geometric mean) and returns a robust **consensus rank aggregate**. scverse-native22 (AnnData/MuData), also handles spatial and multi-condition data.23- **CellPhoneDB** (v5, Python) - permutation-test statistical method; also DEG-based24 and receptor-activity (CellSign) modes. LIANA reimplements its core scoring.25- **CellChat** (v2, **R only** - GitHub) - pathway-level signaling26 probabilities plus strong built-in visualization (chord, hierarchy, river plots).27 Use from R/Seurat; not a Python package.28- **squidpy `sq.gr.ligrec`** (Python) - CellPhoneDB-style permutation test inside the29 scverse spatial stack; convenient when already in squidpy. For spatially-resolved30 proximity use the `spatialdata-squidpy` skill.3132## Installation3334LIANA+ requires **Python 3.10-3.13**. Latest release: **liana 1.8.1** (July 2026).3536```bash37uv pip install liana # core (AnnData/scanpy)38uv pip install "liana[extras]" # tensor-cell2cell / MOFA+ multi-condition tools39```4041CellChat is R-based (install in R):42```r43if (!requireNamespace("devtools", quietly = TRUE)) install.packages("devtools")44devtools::install_github("jinworks/CellChat") # not on CRAN or Bioconductor45```4647## Core workflow (LIANA on an h5ad)4849```python50import scanpy as sc51import liana as li5253# 1. Load an ANNOTATED, log-normalized AnnData (cell types already assigned).54adata = sc.read_h5ad("annotated.h5ad")55# adata.X should be log1p-normalized counts (NOT raw counts, NOT scaled/z-scored).56# adata.obs["cell_type"] holds the labels to group by.5758# 2. Run the consensus method (runs several methods, aggregates ranks).59li.mt.rank_aggregate(60 adata,61 groupby="cell_type", # cell-type column in adata.obs62 resource_name="consensus", # human; use "mouseconsensus" for mouse63 expr_prop=0.1, # drop L-R where either gene is expressed in <10% of a cluster64 use_raw=False, # read adata.X; set True to read adata.raw65 verbose=True,66)6768# 3. Results are a DataFrame in adata.uns["liana_res"].69res = adata.uns["liana_res"]70print(res.columns.tolist())71# source, target, ligand_complex, receptor_complex, magnitude_rank, specificity_rank, ...7273# 4. Top robust interactions: lower ranks = more relevant. Sort/filter, then inspect.74top = (res[res["specificity_rank"] <= 0.05]75 .sort_values("magnitude_rank")76 .head(20))77print(top[["source", "target", "ligand_complex", "receptor_complex",78 "magnitude_rank", "specificity_rank"]])7980# 5a. Dotplot: colour = magnitude (expression strength), size = specificity.81li.pl.dotplot(82 adata=adata,83 colour="magnitude_rank",84 size="specificity_rank",85 inverse_colour=True, # invert so stronger interactions look "hotter"86 inverse_size=True, # invert so more-specific interactions look larger87 source_labels=["Monocyte", "T cell"], # subset senders (optional)88 target_labels=["B cell", "NK"], # subset receivers (optional)89 top_n=15,90 orderby="magnitude_rank",91 orderby_ascending=True,92 filter_fun=lambda x: x["specificity_rank"] <= 0.05,93 figure_size=(9, 7),94)9596# 5b. Chord / network view: circular source->target network of interactions.97li.pl.circle_plot(adata=adata, groupby="cell_type")98# li.pl.tileplot(...) is another summary view.99100# List options if unsure of names:101li.mt.show_methods() # individual methods (cellphonedb, natmi, cellchat, ...)102li.rs.show_resources() # available L-R resources (consensus, mouseconsensus, ...)103```104105Run an individual method instead of the consensus with e.g. `li.mt.cellphonedb(adata, groupby="cell_type", ...)`; it writes the same `adata.uns["liana_res"]`.106107## Gotchas / best practices108109- **Annotate first.** CCC is only meaningful over trustworthy cell-type labels.110 Do QC, clustering, and annotation before this step (see related skills). Garbage or111 mixed clusters produce meaningless L-R calls.112- **Feed log-normalized data, not raw counts and not scaled data.** LIANA reads113 `adata.X` (with `use_raw=False`) or `adata.raw`; it expects `normalize_total` +114 `log1p` values. Z-scored/`sc.pp.scale`d matrices give wrong scores.115- **Magnitude vs specificity - interpret both.** *Magnitude* reflects how strongly116 the L-R pair is co-expressed in a `source->target` pair (is the interaction present117 and how strong). *Specificity* reflects how uniquely that pair stands out for those118 two cell types versus all other pairs. A strong-magnitude interaction can be119 non-specific (housekeeping ligands). Prioritize hits that are both high-magnitude120 and high-specificity. In the aggregate, `magnitude_rank`/`specificity_rank` are121 aggregated ranks in [0,1] where **lower = more relevant**; sort ascending and filter122 `specificity_rank <= 0.05`.123- **`source -> target` is directional.** `source` = the ligand-expressing (sender)124 cell type; `target` = the receptor-expressing (receiver). A->B and B->A are distinct.125- **Human vs mouse resources.** `resource_name="consensus"` is human; use126 `"mouseconsensus"` for mouse. For other species or a mixed reference, translate via127 orthologs with `li.rs.get_hcop_orthologs()` / `li.rs.translate_resource()`. Never run128 a human resource on mouse gene symbols.129- **Comparing conditions/batches.** Do not just pool samples. Run per sample/condition130 and compare, or use LIANA+'s multi-sample tools (`by_sample` scoring, plus131 tensor-cell2cell / MOFA+ under `li.multi` in the `[extras]` install) to decompose132 condition-specific communication. Confounded batches inflate apparent signaling; keep133 batch structure explicit. See the LIANA+ docs for the current multi-condition API.134- **`expr_prop` and small clusters.** Very small cell-type clusters and the135 `expr_prop` threshold (default 0.1) heavily affect which pairs survive; report the136 threshold and cluster sizes used.137- **Don't over-invent signatures.** For exact/advanced arguments (spatial `bivariate`,138 `MistyData`, multi-view) consult the LIANA+ docs: https://liana-py.readthedocs.io139 CellChat's chord/pathway plots and CellPhoneDB v5 modes live in their own docs.140141## Use this vs related skills142143Run `scanpy` / `scrna-preprocessing-clustering` (QC, normalize, cluster) and144`cell-annotation` (assign cell types) **first** to produce the annotated `.h5ad`; use145`spatialdata-squidpy` when you need spatially-resolved ligand-receptor proximity rather146than label-based CCC.