
scSidekick - Pathway Analysis: Single-Cell Scoring & Enrichment
10_pathway_enrichment.RmdOverview
This vignette covers three complementary approaches to pathway analysis at the single-cell level:
| Function | What it does |
|---|---|
RunSCssGSEA() |
Score every cell against gene sets; test for group differences |
PlotPathwayButterfly() |
Map cells onto a 2-D axis defined by exactly 4 pathways |
RunEnrichment() |
Over-representation analysis (ORA) on a gene list or DEG table |
These tools answer different questions:
- RunSCssGSEA: “Which pathways are differentially active between my cell types, at the single-cell level?”
- PlotPathwayButterfly: “Where does each cell sit in a 4-pathway state space? Do my cell types polarize toward specific states?”
- RunEnrichment: “Given my list of significant DEGs, which pathways are over-represented?”
Part 1: RunSCssGSEA
1.1 Overview
RunSCssGSEA() computes a pathway activity score for
every cell using either GSVA’s ssGSEA algorithm or UCell, then tests for
significant differences between groups using ANOVA or Kruskal-Wallis.
Results are visualized as a mean z-score heatmap (pathways x groups) and
optional per-pathway boxplots.
Gene-set input modes (pick one):
| Mode | Parameter | Example |
|---|---|---|
| 1. Custom gene list | gene_sets |
list(MySet = c("GENE1","GENE2")) |
| 2. DEG data frame | deg_df |
RunWilcoxAUC or FindMarkers output |
| 3. MSigDB collection | gene_set_library |
"H" (Hallmark) |
| 4. MSigDB search | search_terms |
c("interferon","apoptosis") |
Modes 3 and 4 can be combined. To pull from multiple
MSigDB collections at once (instead of the single
gene_set_library/gene_set_subcategory pair),
use pathway_sets - the same named-list argument
RunGSEA() uses, e.g.
list(Hallmark = list(category = "H"), KEGG = list(category = "C2", subcategory = "CP:KEGG")).
When supplied, pathway_sets takes priority over
gene_set_library/gene_set_subcategory.
1.2 Setup
library(scSidekick)
library(Seurat)
library(SeuratData)
data("bmcite")
bmcite <- UpdateSeuratObject(bmcite)
bmcite <- PrepObject(bmcite,
variables = c("celltype.l1", "celltype.l2", "donor"),
group.by = "celltype.l1",
output_dir = tempdir(),
object_name = "bmcite")1.3 Mode 1: Custom gene sets
Pass a named list of gene vectors. Each element becomes one row in
the heatmap. search_terms also filters these custom set
names when supplied, exactly as it does for
gene_set_library/pathway_sets (Modes 3/4) - it
is not ignored just because the gene sets were entered manually.
RunSCssGSEA(bmcite,
group.by = "celltype.l1",
gene_sets = list(
T_effector = c("GZMB", "PRF1", "IFNG", "TNF"),
T_exhausted = c("PDCD1", "HAVCR2", "LAG3", "TIGIT"),
NK_cytotoxic = c("NCR1", "KLRB1", "GNLY", "NKG7")
),
output_dir = tempdir(),
object_name = "bmcite")
# search_terms narrows the custom list down to matching names only
RunSCssGSEA(bmcite,
group.by = "celltype.l1",
gene_sets = list(
T_effector = c("GZMB", "PRF1", "IFNG", "TNF"),
T_exhausted = c("PDCD1", "HAVCR2", "LAG3", "TIGIT"),
NK_cytotoxic = c("NCR1", "KLRB1", "GNLY", "NKG7")
),
search_terms = "T_", # keeps only T_effector and T_exhausted
output_dir = tempdir(),
object_name = "bmcite")1.4 Mode 2: DEG data frame
Build gene sets automatically from a DEG table. Each group’s top DEGs
become one gene set. The deg_group_column can be a single
column (groups are heatmap rows) or two columns (the first column is
used to split rows on the heatmap).
# Get DEGs first
markers <- RunWilcoxAUC(bmcite, group.by = "celltype.l1")
RunSCssGSEA(bmcite,
group.by = "celltype.l1",
deg_df = markers,
deg_gene_column = "feature",
deg_group_column = "group",
deg_fc_column = "logFC",
deg_padj_column = "padj",
deg_padj_cutoff = 0.05,
deg_top_n = 30L,
output_dir = tempdir(),
object_name = "bmcite")1.5 Mode 3: MSigDB collection
Fetch an entire MSigDB collection and score all cells against it.
Large collections (e.g. GO:BP with
thousands of sets) should be filtered with search_terms or
min.size to keep the run tractable.
RunSCssGSEA(bmcite,
group.by = "celltype.l1",
gene_set_library = "H", # Hallmark
min.size = 10L,
output_dir = tempdir(),
object_name = "bmcite")1.6 Mode 4: MSigDB keyword search
Search gene-set names with case-insensitive regex. Pass a character vector for OR logic, or a list of vectors for AND-groups.
# OR: any set mentioning "interferon" OR "apoptosis"
RunSCssGSEA(bmcite,
group.by = "celltype.l1",
search_terms = c("interferon", "apoptosis"),
pathway_sets = list(Hallmark = list(category = "H")),
output_dir = tempdir(),
object_name = "bmcite")
# AND-group: sets mentioning BOTH "T_CELL" AND "ACTIVATION"
RunSCssGSEA(bmcite,
group.by = "celltype.l1",
search_terms = list(c("T_CELL", "ACTIVATION")),
pathway_sets = list(
C7 = list(category = "C7", subcategory = "IMMUNESIGDB")),
output_dir = tempdir(),
object_name = "bmcite")1.7 Switching to UCell backend
The default scoring backend is GSVA’s ssGSEA. Use
method = "ucell" for the UCell rank-based algorithm, which
scales better to large datasets and avoids the GSVA memory overhead.
RunSCssGSEA(bmcite,
group.by = "celltype.l1",
gene_set_library = "H",
method = "ucell",
ucell_max_rank = 1500L,
output_dir = tempdir(),
object_name = "bmcite")1.8 Downsampling large datasets
For objects with >50,000 cells, pass downsample to
cap the number of cells per group before scoring.
RunSCssGSEA(large_obj,
group.by = "celltype",
gene_set_library = "H",
downsample = 5000L,
output_dir = "./Figures/Pathways")1.9 Customizing heatmap colors
The default is a 5-stop diverging palette centered at 0 (z = -2 to
+2). Override with any circlize::colorRamp2 object.
# Classic blue-white-red
col_bwr <- circlize::colorRamp2(c(-2, -1, 0, 1, 2),
c("#2166ac", "#92c5de", "white",
"#f4a582", "#d6604d"))
RunSCssGSEA(bmcite,
group.by = "celltype.l1",
gene_set_library = "H",
heatmap_colors = col_bwr,
output_dir = tempdir(),
object_name = "bmcite")1.10 Resuming a run
resume = TRUE reloads a previously saved score CSV
checkpoint, skipping re-scoring for the cells already processed.
RunSCssGSEA(bmcite,
group.by = "celltype.l1",
gene_set_library = "H",
resume = TRUE,
output_dir = tempdir(),
object_name = "bmcite")RunSCssGSEA parameter reference
| Parameter | Type | Default | Description |
|---|---|---|---|
seurat_object |
Seurat | required | Annotated Seurat object |
group.by |
character | required | Metadata column for grouping |
split.by |
character or NULL
|
NULL |
Run independently per level |
subset_cells |
logical | FALSE |
Subset cells before analysis |
subset_by |
character or NULL
|
NULL |
Metadata column to subset on (NULL →
subset_values are barcodes) |
subset_values |
character or NULL
|
NULL |
Levels or barcodes to keep when
subset_cells = TRUE
|
gene_sets |
named list or NULL
|
NULL |
Mode 1: custom gene lists. Filtered by search_terms
too, if supplied |
deg_df |
data frame or NULL
|
NULL |
Mode 2: DEG table |
gene_set_library |
character or NULL
|
NULL |
Mode 3: single MSigDB collection code. Ignored if
pathway_sets supplied |
gene_set_subcategory |
character or NULL
|
NULL |
Sub-collection code, paired with gene_set_library
|
pathway_sets |
named list or NULL
|
NULL |
Mode 3/4: multiple MSigDB collections at once (same format as
RunGSEA()); takes priority over
gene_set_library/gene_set_subcategory
|
search_terms |
character/list or NULL
|
NULL |
Mode 4: keyword filter (also filters custom
gene_sets) |
method |
character | "gsva" |
Scoring backend: "gsva" or "ucell"
|
assay |
character | "RNA" |
Seurat assay to score ("data" layer). Use
"sketch" for a Seurat v5 sketch-subsampled assay |
downsample |
integer or NULL
|
NULL |
Max cells per group |
min.size |
integer | 5 |
Min gene-set size after intersection |
ssgsea.norm |
logical | TRUE |
GSVA only: normalize ssGSEA scores to [0,1]
|
ucell_max_rank |
integer | 1500 |
UCell only: top genes considered |
sig_group_by |
character or NULL
|
NULL |
Metadata column for significance testing, if different from
group.by
|
p_cutoff |
numeric | 0.05 |
Significance threshold for heatmap rows (raw or adjusted, per
use.padj) |
use.padj |
logical | TRUE |
Use BH-adjusted p-value (vs. raw p) for p_cutoff and
heatmap stars |
show_only_significant |
logical | TRUE |
Show only significant pathways |
pathways |
character or NULL
|
NULL |
Manually override which pathways are shown (bypasses
show_only_significant) |
split_colors |
named list/vector or NULL
|
NULL |
Custom colors for split.by levels |
heatmap_colors |
colorRamp2 or NULL
|
NULL |
Custom color function |
heatmap_params |
named list | list(...) |
Extra args to ComplexHeatmap::Heatmap()
|
add_boxplots |
logical | TRUE |
Per-pathway boxplots |
add_pvalues |
logical | TRUE |
Add raw (unadjusted) t-test p-value brackets to boxplots |
add_to_object |
logical | FALSE |
Store scores back into seurat_object@meta.data
|
resume |
logical | FALSE |
Reload checkpoint CSV |
resume_folder |
character or NULL
|
NULL |
Explicit run folder to resume from (bypasses fingerprint matching) |
timestamp |
logical | TRUE |
Create a timestamped subfolder under output_dir for
this run |
caffeinate |
logical | FALSE |
Prevent system sleep |
Part 2: PlotPathwayButterfly
2.1 Overview
PlotPathwayButterfly() maps every cell onto a 2-D
coordinate system defined by exactly four pathways
arranged in quadrants. This makes cell state polarization visually
immediate: cells that are high in the top-left pathway cluster in the
upper-left of the plot; cells sitting at the center are in
transition.
Quadrant layout:
top-left | top-right
---------|----------
bottom-left | bottom-right
The Y-axis separates the dominant axis (top vs bottom pathways); the X-axis gives the left-right balance within whichever axis dominates for each cell.
Output layout: one large overview panel (all cells
colored by group.by) flanked by one small panel per group
level (that group highlighted in color; remaining cells in gray).
This visualization is adapted from the scrabble R package (Suva Lab).
2.2 Basic call with MSigDB Hallmark pathways
Supply the four pathway names as they appear in the MSigDB
collection. The pathway_sets argument specifies which
collection to search.
PlotPathwayButterfly(
seurat_object = bmcite,
group.by = "celltype.l1",
quadrant_order = c(
"HALLMARK_INTERFERON_GAMMA_RESPONSE", # bottom-left
"HALLMARK_INFLAMMATORY_RESPONSE", # bottom-right
"HALLMARK_G2M_CHECKPOINT", # top-left
"HALLMARK_MYC_TARGETS_V1" # top-right
),
pathway_sets = list(Hallmark = list(category = "H")),
output_dir = tempdir()
)2.3 Custom gene sets
When you have your own gene sets (not from MSigDB), pass them
directly via gene_sets. The names must match
quadrant_order.
PlotPathwayButterfly(
seurat_object = bmcite,
group.by = "celltype.l1",
gene_sets = list(
Cytotoxic = c("GZMB", "PRF1", "IFNG"),
NaiveLike = c("CCR7", "SELL", "TCF7"),
Exhausted = c("PDCD1", "LAG3", "HAVCR2"),
Regulatory = c("FOXP3", "IL2RA", "CTLA4")
),
quadrant_order = c("NaiveLike", "Cytotoxic", "Regulatory", "Exhausted"),
output_dir = tempdir()
)The names in quadrant_order map to the four quadrant
positions: bottom-left, bottom-right, top-left,
top-right.
2.4 Custom corner labels
By default, corner labels are auto-shortened from the pathway name
(prefix and underscores removed). Override with
quadrant_labels for cleaner display.
PlotPathwayButterfly(
seurat_object = bmcite,
group.by = "celltype.l1",
gene_sets = list(
Cytotoxic = c("GZMB", "PRF1", "IFNG"),
NaiveLike = c("CCR7", "SELL", "TCF7"),
Exhausted = c("PDCD1", "LAG3", "HAVCR2"),
Regulatory = c("FOXP3", "IL2RA", "CTLA4")
),
quadrant_order = c("NaiveLike", "Cytotoxic", "Regulatory", "Exhausted"),
quadrant_labels = c("Naive", "Cytotoxic", # bl, br
"Treg", "Exhausted"), # tl, tr
output_dir = tempdir()
)2.5 Split by donor or condition
split.by produces a separate figure per split level,
each with its own axis limits. Use this to compare butterfly
distributions across donors or conditions.
PlotPathwayButterfly(
seurat_object = bmcite,
group.by = "celltype.l1",
split.by = "donor",
gene_sets = list(
Cytotoxic = c("GZMB", "PRF1", "IFNG"),
NaiveLike = c("CCR7", "SELL", "TCF7"),
Exhausted = c("PDCD1", "LAG3", "HAVCR2"),
Regulatory = c("FOXP3", "IL2RA", "CTLA4")
),
quadrant_order = c("NaiveLike", "Cytotoxic", "Regulatory", "Exhausted"),
output_dir = tempdir()
)2.6 Handling large datasets
For objects with >50,000 cells, enable subsampling to keep scoring fast:
PlotPathwayButterfly(
seurat_object = large_obj,
group.by = "celltype",
quadrant_order = c("HALLMARK_MYC_TARGETS_V1",
"HALLMARK_E2F_TARGETS",
"HALLMARK_INTERFERON_GAMMA_RESPONSE",
"HALLMARK_INFLAMMATORY_RESPONSE"),
subsample = TRUE,
n_cells = 30000L,
output_dir = "./Figures/Butterfly"
)Without subsampling, the function emits a warning at
subsample_warn_at = 50000 cells (adjustable).
2.7 Return value
PlotPathwayButterfly invisibly returns a named list (one
element per split level). Each element contains the raw coordinates and
scores.
res <- PlotPathwayButterfly(
seurat_object = bmcite,
group.by = "celltype.l1",
gene_sets = list(
Cytotoxic = c("GZMB", "PRF1", "IFNG"),
NaiveLike = c("CCR7", "SELL", "TCF7"),
Exhausted = c("PDCD1", "LAG3", "HAVCR2"),
Regulatory = c("FOXP3", "IL2RA", "CTLA4")
),
quadrant_order = c("NaiveLike", "Cytotoxic", "Regulatory", "Exhausted")
)
# Hierarchy coordinates (X, Y) for each cell
head(res$All$hierarchy)
# Raw pathway scores (cells x pathways)
head(res$All$scores)PlotPathwayButterfly parameter reference
| Parameter | Type | Default | Description |
|---|---|---|---|
seurat_object |
Seurat | required | Annotated Seurat object |
group.by |
character | required | Metadata column for coloring |
quadrant_order |
character(4) | required | Four pathway names: bottom-left, bottom-right, top-left, top-right |
split.by |
character or NULL
|
NULL |
One figure per level |
gene_sets |
named list or NULL
|
NULL |
Custom gene sets; NULL = fetch from MSigDB |
pathway_sets |
named list | Hallmark | MSigDB collections when gene_sets = NULL
|
species |
character | "Homo sapiens" |
Species for msigdbr
|
quadrant_labels |
character(4) or NULL
|
NULL |
Corner display labels; NULL = auto-shortened pathway
name |
group_colors |
named character or NULL
|
NULL |
Color map for group.by levels |
assay |
character | "RNA" |
Assay to extract counts from |
layer |
character | "counts" |
Layer/slot name |
subsample |
logical | FALSE |
Randomly sample cells before scoring |
n_cells |
integer | 20000 |
Cells to sample when subsample = TRUE
|
center |
logical | TRUE |
Column-center scores before hierarchy transform |
log_scale |
logical | TRUE |
Apply log2 scaling to hierarchy coordinates |
overview_size |
numeric | 2 |
Width of overview panel relative to one small panel column |
point_size |
numeric | 0.3 |
Point size |
point_alpha |
numeric | 0.6 |
Point alpha |
width |
numeric or NULL
|
NULL |
PDF width (auto-sized when NULL) |
height |
numeric or NULL
|
NULL |
PDF height (auto-sized when NULL) |
output_dir |
character or NULL
|
NULL |
Save directory; NULL = print only |
file_name |
character or NULL
|
NULL |
Override the auto-built saved file name |
timestamp |
logical | FALSE |
Append a _YYYYMMDD-HHMMSS stamp to the saved file
name(s) so repeated runs are versioned instead of overwritten |
caffeinate |
logical | FALSE |
Prevent system sleep |
Part 3: RunEnrichment
3.1 Overview
RunEnrichment() runs over-representation analysis (ORA)
via enrichR on a gene list or on significant DEGs from a data frame. It
automatically calls PlotEnrichment() to save dot plots of
enriched terms.
ORA is simpler than GSEA but answers a different question: given this gene list, which pathways contain more of these genes than expected by chance? No ranking is used - just presence or absence.
3.2 From a gene list
Pass a character vector of gene symbols directly.
RunEnrichment(
features = c("GZMB", "PRF1", "IFNG", "TNF", "FASLG", "NKG7"),
databases = c("MSigDB_Hallmark_2020",
"GO_Biological_Process_2023",
"KEGG_2021_Human"),
output_dir = tempdir()
)Run enrichR::listEnrichrDbs() for all available database
names.
3.3 From a DEG table
Pass a DEG data frame; RunEnrichment filters for
significant DEGs before testing. Columns are auto-detected from common
RunWilcoxAUC/FindMarkers naming.
markers <- RunWilcoxAUC(bmcite, group.by = "celltype.l1")
RunEnrichment(
deg_df = markers,
direction = "up", # "up", "down", or "both"
fc_cutoff = 0.25,
padj_cutoff = 0.05,
databases = c("MSigDB_Hallmark_2020", "GO_Biological_Process_2023"),
output_dir = tempdir(),
object_name = "bmcite"
)3.4 Per-group enrichment
When the DEG table has a group_column,
RunEnrichment tests each group’s DEGs separately and saves
one plot per group.
RunEnrichment(
deg_df = markers,
group_column = "group",
direction = "up",
databases = c("MSigDB_Hallmark_2020"),
top_n_plot = 15L,
output_dir = tempdir(),
object_name = "bmcite"
)3.5 Choosing databases
The default databases are MSigDB_Hallmark_2020,
GO_Biological_Process_2023, and
KEGG_2021_Human. You can test any databases available in
enrichR.
# List all available enrichR databases
enrichR::listEnrichrDbs()
RunEnrichment(
features = c("GZMB", "PRF1", "IFNG"),
databases = c("Reactome_2022",
"WikiPathway_2023_Human",
"CellMarker_Augmented_2021"),
output_dir = tempdir()
)3.6 Return value
RunEnrichment returns a nested list:
[[group]][[database]] containing enrichR result data
frames.
res <- RunEnrichment(
deg_df = markers,
group_column = "group",
direction = "up",
databases = c("MSigDB_Hallmark_2020"),
output_dir = tempdir()
)
# Enrichment results for one group and database
head(res$`CD4 T`$MSigDB_Hallmark_2020)RunEnrichment parameter reference
| Parameter | Type | Default | Description |
|---|---|---|---|
features |
character or NULL
|
NULL |
Gene list for ORA; use with OR deg_df
|
deg_df |
data frame or NULL
|
NULL |
DEG table; genes filtered before testing |
gene_column |
character or NULL
|
NULL |
Gene column in deg_df; auto-detected if
NULL
|
fc_column |
character or NULL
|
NULL |
logFC column; auto-detected if NULL
|
padj_column |
character or NULL
|
NULL |
Adjusted p-value column; auto-detected if NULL
|
group_column |
character or NULL
|
NULL |
Group column in deg_df; NULL = test all
together |
fc_cutoff |
numeric | 0.25 |
Minimum absolute logFC |
padj_cutoff |
numeric | 0.05 |
Maximum adjusted p-value |
direction |
character | "up" |
Which DEGs: "up", "down", or
"both"
|
databases |
character vector | Hallmark + GO:BP + KEGG | enrichR databases to query |
top_n_plot |
integer | 20 |
Maximum terms per database to plot |
plot_type |
character | "dot" |
Plot type passed to PlotEnrichment
|
padj_cutoff_plot |
numeric | 0.05 |
Significance cutoff for plot filtering |
output_dir |
character or NULL
|
NULL |
Save directory; NULL = print only |
object_name |
character | "" |
Prefix for output file names |
subset_name |
character | "" |
Optional subset label for file names |
See also
- Vignette 08:
RunGSEA- rank-based GSEA on the full Seurat object - Vignette 09:
RunGSEA_pseudobulk- sample-level pseudobulk GSEA with limma-voom - Vignette 05:
GroupHeatmap/PlotDotPlot- gene-level expression visualizations - Vignette 11:
RunCellChat- pathway-level cell-cell communication