Differential Connectivity Analysis
Zaoqu Liu
2026-01-23
Source:vignettes/differential-analysis.Rmd
differential-analysis.RmdOverview
Differential connectivity analysis enables comparison of cell-cell communication networks between conditions (e.g., disease vs. healthy, treated vs. control). This vignette demonstrates the complete workflow for identifying altered signaling pathways.
Workflow
┌─────────────────┐ ┌─────────────────┐
│ Condition 1 │ │ Condition 2 │
│ (Reference) │ │ (Test) │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│CreateConnectome │ │CreateConnectome │
└────────┬────────┘ └────────┬────────┘
│ │
└───────────┬───────────┘
▼
┌───────────────────────┐
│DifferentialConnectome │
└───────────┬───────────┘
│
┌───────────┴───────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Visualization │ │ Statistics │
└─────────────────┘ └─────────────────┘
Step 1: Prepare Data
Split by Condition
library(Seurat)
library(Connectome)
# Method 1: SplitObject
seurat_list <- SplitObject(seurat_obj, split.by = "condition")
seurat_ctrl <- seurat_list[["control"]]
seurat_treat <- seurat_list[["treatment"]]
# Method 2: EvenSplit (balanced sampling)
seurat_list <- EvenSplit(seurat_obj, split.by = "condition")
seurat_ctrl <- seurat_list[["control"]]
seurat_treat <- seurat_list[["treatment"]]Why EvenSplit?
EvenSplit() ensures each cell type has equal
representation across conditions, preventing bias from unequal cell
numbers:
# Without EvenSplit: potential bias
# Control: 1000 T cells, 500 B cells
# Treatment: 200 T cells, 800 B cells
# With EvenSplit: balanced
# Control: 200 T cells, 500 B cells
# Treatment: 200 T cells, 500 B cellsStep 2: Create Individual Connectomes
# Reference connectome (control)
conn_ctrl <- CreateConnectome(
object = seurat_ctrl,
species = "human",
LR.database = "fantom5",
min.cells.per.ident = 30,
p.values = FALSE # Optional for differential analysis
)
# Test connectome (treatment)
conn_treat <- CreateConnectome(
object = seurat_treat,
species = "human",
LR.database = "fantom5",
min.cells.per.ident = 30,
p.values = FALSE
)Important: Both connectomes must have: - Same cell type identities - Same ligand-receptor pairs - Matching edge identifiers
Step 3: Compute Differential Connectome
diff_conn <- DifferentialConnectome(
connect.ref = conn_ctrl,
connect.test = conn_treat,
min.pct = 0.1
)Output Columns
| Column | Description |
|---|---|
ligand.norm.lfc |
Log2 fold change of ligand expression |
recept.norm.lfc |
Log2 fold change of receptor expression |
weight.norm.lfc |
Log2 fold change of edge weight |
pct.source.1 |
% source cells expressing ligand (reference) |
pct.source.2 |
% source cells expressing ligand (test) |
pct.target.1 |
% target cells expressing receptor (reference) |
pct.target.2 |
% target cells expressing receptor (test) |
score |
Perturbation score = |
Step 4: Interpret Results
Perturbation Score
The score captures edges where both ligand and receptor are differentially expressed:
- High score: Strong coordinated change
- Score = 0: No change or unilateral change
Example Interpretation
# Top perturbed edges
top_edges <- diff_conn[order(-diff_conn$score), ][1:20, ]
# Upregulated signaling (both components increased)
upregulated <- subset(diff_conn,
ligand.norm.lfc > 0 & recept.norm.lfc > 0 & score > 1)
# Downregulated signaling (both components decreased)
downregulated <- subset(diff_conn,
ligand.norm.lfc < 0 & recept.norm.lfc < 0 & score > 1)
# Rewired signaling (opposite direction changes)
rewired <- subset(diff_conn,
(ligand.norm.lfc > 0 & recept.norm.lfc < 0) |
(ligand.norm.lfc < 0 & recept.norm.lfc > 0))Step 5: Visualization
Circos Diagram
CircosDiff(
diff_conn,
min.score = 1,
min.pct = 0.1,
sources.include = NULL, # All sources
targets.include = NULL, # All targets
title = "Differential Connectivity: Treatment vs Control"
)Edge Dot Plot
DiffEdgeDotPlot(
diff_conn,
min.score = 0.5,
features = c("VEGFA", "IL6", "TNF", "CXCL12")
)Scoring Heatmap
# Unaligned view (separate ligand/receptor panels)
DifferentialScoringPlot(
diff_conn,
min.score = 0.5,
aligned = FALSE
)
# Aligned view (edge-matched)
DifferentialScoringPlot(
diff_conn,
sources.include = c("Fibroblast", "Macrophage"),
targets.include = c("Epithelial", "Endothelial"),
min.score = 0.5,
aligned = TRUE
)Advanced Analysis
Mode-Specific Changes
# Analyze by signaling mode
modes <- unique(diff_conn$mode)
mode_summary <- data.frame()
for (m in modes) {
subset_m <- diff_conn[diff_conn$mode == m, ]
mode_summary <- rbind(mode_summary, data.frame(
mode = m,
n_edges = nrow(subset_m),
mean_score = mean(subset_m$score, na.rm = TRUE),
max_score = max(subset_m$score, na.rm = TRUE),
n_upregulated = sum(subset_m$ligand.norm.lfc > 0 &
subset_m$recept.norm.lfc > 0, na.rm = TRUE),
n_downregulated = sum(subset_m$ligand.norm.lfc < 0 &
subset_m$recept.norm.lfc < 0, na.rm = TRUE)
))
}
# Sort by mean perturbation score
mode_summary <- mode_summary[order(-mode_summary$mean_score), ]
head(mode_summary, 10)Cell Type-Specific Changes
# Identify most affected cell types (as senders)
sender_changes <- aggregate(score ~ source, data = diff_conn,
FUN = function(x) c(mean = mean(x), max = max(x)))
# Identify most affected cell types (as receivers)
receiver_changes <- aggregate(score ~ target, data = diff_conn,
FUN = function(x) c(mean = mean(x), max = max(x)))Export Results
# Export significant differential edges
sig_edges <- subset(diff_conn, score > 1)
write.csv(sig_edges, "differential_edges.csv", row.names = FALSE)
# Export for Cytoscape
cytoscape_format <- diff_conn[, c("source", "target", "ligand", "receptor",
"ligand.norm.lfc", "recept.norm.lfc", "score")]
write.csv(cytoscape_format, "cytoscape_import.csv", row.names = FALSE)Best Practices
Sample Size
- Minimum 30 cells per identity per condition
- Use
EvenSplit()for balanced comparisons - Consider bootstrapping for small samples
Filtering Strategy
# Stringent: High confidence changes
high_conf <- subset(diff_conn,
score > 2 &
(pct.source.1 > 0.1 | pct.source.2 > 0.1) &
(pct.target.1 > 0.1 | pct.target.2 > 0.1)
)
# Discovery: Explore all potential changes
discovery <- subset(diff_conn,
score > 0.5 &
(pct.source.1 > 0.05 | pct.source.2 > 0.05)
)Handling Infinite Values
When expression goes from 0 to positive (or vice versa), fold change is infinite. Connectome handles this automatically:
# Automatic handling in visualization functions
CircosDiff(diff_conn, infinity.to.max = TRUE)
DiffEdgeDotPlot(diff_conn, infinity.to.max = TRUE)Common Pitfalls
- Batch effects: Ensure conditions are not confounded with batches
- Cell type composition: Changes in cell proportions can affect results
- Pseudoreplication: Multiple samples from same individual should be aggregated
- Multiple testing: Consider adjusting score thresholds for genome-wide comparisons
Session Info
sessionInfo()
#> R version 4.4.0 (2024-04-24)
#> Platform: aarch64-apple-darwin20
#> Running under: macOS 15.6.1
#>
#> Matrix products: default
#> BLAS: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.0
#>
#> locale:
#> [1] C
#>
#> time zone: Asia/Shanghai
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> loaded via a namespace (and not attached):
#> [1] digest_0.6.39 desc_1.4.3 R6_2.6.1 fastmap_1.2.0
#> [5] xfun_0.56 cachem_1.1.0 knitr_1.51 htmltools_0.5.9
#> [9] rmarkdown_2.30 lifecycle_1.0.5 cli_3.6.5 sass_0.4.10
#> [13] pkgdown_2.1.3 textshaping_1.0.4 jquerylib_0.1.4 systemfonts_1.3.1
#> [17] compiler_4.4.0 tools_4.4.0 ragg_1.5.0 bslib_0.9.0
#> [21] evaluate_1.0.5 yaml_2.3.12 otel_0.2.0 jsonlite_2.0.0
#> [25] rlang_1.1.7 fs_1.6.6 htmlwidgets_1.6.4