scCS.multicomparison¶
multicomparison.py — MultiScorer: multi-condition (3+) commitment score analysis for scCS.
Extends the pairwise PairScorer to handle 3 or more experimental conditions (e.g., multiple drug treatments, time points, genotypes).
Architecture¶
- MultiScorer
Wraps SingleScorer. Pools all conditions for embedding, then scores each condition separately using cell masks on the shared embedding.
- Tier 1 — Core multi-condition API
score_all_conditions() : dict[condition -> CommitmentScoreResult]
- Tier 2 — Omnibus + post-hoc statistical comparison
compare_omnibus() : Kruskal-Wallis / ANOVA per fate compare_posthoc() : Dunn / Tukey / Conover pairwise post-hoc compute_pairwise_deltas() : ΔCS for ALL condition pairs with bootstrap CI
- Tier 3 — Advanced
- fit_mixed_model()linear mixed-effects model on per-cell
fate affinity scores via statsmodels MixedLM
fit_mixed_model_contrasts() : LMM with custom condition contrasts trajectory_shift() : KS test + Wasserstein distance on
pseudotime distributions per fate arm
plot_trajectory_shift() : visualization of pseudotime distributions
Usage¶
>>> mscorer = scCS.MultiScorer(
... adata,
... root='17',
... branches=['homeostatic', 'activated'],
... condition_obs_key='treatment',
... obs_key='leiden',
... )
>>> mscorer.build_embedding(ordering_metric='pseudotime')
>>> mscorer.fit()
>>> results = mscorer.score_all_conditions()
>>> omnibus = mscorer.compare_omnibus(results)
>>> posthoc = mscorer.compare_posthoc(results, omnibus_results=omnibus)
>>> deltas = mscorer.compute_pairwise_deltas()
>>> shift = mscorer.trajectory_shift(results)
Classes¶
RNA velocity commitment scorer for experiments with 3+ conditions. |
Module Contents¶
- class scCS.multicomparison.MultiScorer(adata, root: str, branches: List[str], condition_obs_key: str, obs_key: str = 'leiden', n_angle_bins: int = 36, sector_method: Literal['centroid', 'equal'] = 'centroid', copy: bool = False)[source]¶
RNA velocity commitment scorer for experiments with 3+ conditions.
Builds a SHARED star embedding on the pooled data from all conditions, then scores each condition separately. This ensures arm geometry is identical across conditions, making CS values directly comparable.
Provides tiered statistical testing: - Tier 2: Omnibus tests (Kruskal-Wallis / ANOVA) followed by
post-hoc pairwise comparisons (Dunn / Tukey / Conover).
Tier 3: Mixed-effects models with custom contrasts, trajectory shift analysis.
- Parameters:
adata (AnnData) – Full single-cell dataset containing all conditions.
root (str) – Label of the progenitor/root cluster in adata.obs[obs_key].
branches (list of str) – Labels of the k terminal fate clusters.
condition_obs_key (str) – Column in adata.obs with condition labels (e.g., ‘treatment’). Must contain at least 3 unique values.
obs_key (str) – Column in adata.obs with cluster labels. Default: ‘leiden’.
n_angle_bins (int) – Number of angular bins. Default: 36.
sector_method ({'centroid', 'equal'}) – Sector definition strategy.
copy (bool) – Work on a copy of adata.
- Raises:
ValueError – If condition_obs_key has fewer than 3 unique values. For 2 conditions, use PairScorer instead.
Examples
>>> mscorer = MultiScorer( ... adata, ... root='17', ... branches=['homeostatic', 'activated'], ... condition_obs_key='treatment', ... obs_key='leiden', ... ) >>> mscorer.build_embedding(ordering_metric='pseudotime') >>> mscorer.fit() >>> results = mscorer.score_all_conditions() >>> omnibus = mscorer.compare_omnibus(results) >>> posthoc = mscorer.compare_posthoc(results, omnibus_results=omnibus)
- build_embedding(ordering_metric: str | numpy.ndarray = 'pseudotime', invert_ordering: bool = False, scale_ordering: bool = False, arm_scale: float = 10.0, jitter: float = 0.3, seed: int = 42, arm_norm: str = 'global', verbose: bool = True) MultiScorer[source]¶
Build the shared star embedding on pooled data from all conditions.
The embedding is built on ALL cells (all conditions pooled), ensuring that arm geometry is identical across conditions.
- Parameters:
ordering_metric (str or np.ndarray) – See SingleScorer.build_embedding().
invert_ordering (bool)
scale_ordering (bool)
arm_scale (float)
jitter (float)
seed (int)
verbose (bool)
- Return type:
self
- refit_pseudotime(scale_01: bool = True, arm_scale: float = 10.0, jitter: float = 0.3, seed: int = 42, arm_norm: str = 'global', verbose: bool = True) MultiScorer[source]¶
Rebuild the shared embedding using subset-local pseudotime.
See SingleScorer.refit_pseudotime().
- fit(verbose: bool = True) MultiScorer[source]¶
Fit the shared FateMap and project velocity.
Must be called after build_embedding().
- Return type:
self
- score_all_conditions(cell_level: bool = True, k_nn: int | None = None, n_bootstrap: int = 0, bootstrap_ci: float = 0.95, verbose: bool = True) Dict[str, scCS.scores.CommitmentScoreResult][source]¶
Compute commitment scores separately for each condition.
Uses the shared embedding and FateMap. Each condition’s cells are masked from the shared adata_sub, so arm geometry is identical.
- Parameters:
cell_level (bool) – Compute per-cell fate affinity scores.
k_nn (int, optional) – NN-smoothed entropy neighbors.
n_bootstrap (int) – Bootstrap replicates for CI. 0 = disabled.
bootstrap_ci (float) – CI level for bootstrap.
verbose (bool)
- Returns:
dict
- Return type:
condition_label -> CommitmentScoreResult
- compare_omnibus(results: Dict[str, scCS.scores.CommitmentScoreResult], test: Literal['kruskal', 'anova'] = 'kruskal', pval_threshold: float = 0.05, verbose: bool = True) pandas.DataFrame[source]¶
Omnibus test across all conditions per fate.
For each fate arm, tests whether per-cell affinity scores differ across ALL conditions simultaneously.
‘kruskal’: Kruskal-Wallis H test (non-parametric, recommended default)
‘anova’: One-way ANOVA (parametric, assumes normality)
- Parameters:
results (dict) – Output of score_all_conditions() with cell_level=True.
test ({'kruskal', 'anova'}) – Statistical test to use. Default: ‘kruskal’.
pval_threshold (float) – Significance threshold for flagging. Default 0.05.
verbose (bool)
- Returns:
fate, test, statistic, pval, pval_adj, significant, n_conditions
- Return type:
pd.DataFrame with columns
- compare_posthoc(results: Dict[str, scCS.scores.CommitmentScoreResult], omnibus_results: pandas.DataFrame | None = None, method: Literal['dunn', 'tukey', 'conover'] = 'dunn', pval_correction: Literal['fdr', 'bonferroni', 'holm'] = 'fdr', pval_threshold: float = 0.05, verbose: bool = True) pandas.DataFrame[source]¶
Post-hoc pairwise comparisons across conditions per fate.
Only meaningful after an omnibus test rejects H0. If omnibus_results is provided, post-hoc is only run for fates where omnibus p < threshold.
- - 'dunn': Dunn's test with rank-based comparisons (non-parametric,
recommended with Kruskal-Wallis). Uses scikit-posthocs.
- - 'tukey': Tukey HSD (parametric, for balanced designs, with ANOVA).
- - 'conover': Conover-Iman test (more powerful than Dunn, non-parametric).
Uses scikit-posthocs.
- Multiple testing correction applied across all pairwise comparisons
- within each fate arm.
- Parameters:
results (dict) – Output of score_all_conditions() with cell_level=True.
omnibus_results (pd.DataFrame, optional) – Output of compare_omnibus(). If provided, post-hoc is only run for fates where omnibus pval_adj < pval_threshold.
method ({'dunn', 'tukey', 'conover'}) – Post-hoc test method. Default: ‘dunn’.
pval_correction ({'fdr', 'bonferroni', 'holm'}) – Multiple testing correction method. Default: ‘fdr’.
pval_threshold (float) – Significance threshold. Default 0.05.
verbose (bool)
- Returns:
fate, comparison, method, statistic, pval, pval_adj, significant, mean_A, mean_B, delta_mean
- Return type:
pd.DataFrame with columns
- compute_pairwise_deltas(n_bootstrap: int = 500, ci: float = 0.95, seed: int = 42, verbose: bool = True) Dict[Tuple[str, str], Dict][source]¶
Compute ΔCS for ALL condition pairs with bootstrap CI.
Unlike PairScorer.compute_delta_CS() which takes two specific conditions, this computes delta for every pair in the condition set.
- Parameters:
n_bootstrap (int) – Number of bootstrap replicates. Default 500.
ci (float) – Confidence interval level. Default 0.95.
seed (int)
verbose (bool)
- Returns:
(same structure as PairScorer.compute_delta_CS() output).
- Return type:
dict mapping (cond_a, cond_b) -> delta_result dict
- fit_mixed_model(results: Dict[str, scCS.scores.CommitmentScoreResult], replicate_key: str | None = None, ref_condition: str | None = None, verbose: bool = True) pandas.DataFrame[source]¶
Linear mixed-effects model on per-cell fate affinity scores.
Models per-cell fate affinity as a function of condition (fixed effect) with optional sample/replicate as a random effect.
- Model (per fate j):
affinity_ij ~ condition_i + (1 | sample_id_i)
Uses statsmodels MixedLM.
- Parameters:
results (dict) – Output of score_all_conditions(cell_level=True).
replicate_key (str, optional) – Column in adata_sub.obs with sample/replicate IDs.
ref_condition (str, optional) – Reference condition for the fixed effect.
verbose (bool)
- Returns:
fate, condition, coef, std_err, z_score, pval, pval_adj, ci_low, ci_high, significant
- Return type:
pd.DataFrame with columns
- fit_mixed_model_contrasts(results: Dict[str, scCS.scores.CommitmentScoreResult], contrasts: List[Tuple[str, str]] | None = None, replicate_key: str | None = None, ref_condition: str | None = None, pval_threshold: float = 0.05, verbose: bool = True) pandas.DataFrame[source]¶
Linear mixed-effects model with custom condition contrasts.
Extends fit_mixed_model() to test specific condition comparisons within the LMM framework (more powerful than separate models).
If contrasts is None, tests each condition vs ref_condition. If contrasts is provided, tests each specified pair, e.g.:
[(‘drug_A’, ‘control’), (‘drug_B’, ‘control’), (‘drug_A’, ‘drug_B’)]
Uses statsmodels MixedLM with Wald tests on contrast coefficients.
- Parameters:
results (dict) – Output of score_all_conditions(cell_level=True).
contrasts (list of (str, str), optional) – Pairs of conditions to compare. If None, all conditions vs ref_condition are tested.
replicate_key (str, optional) – Column in adata_sub.obs with sample/replicate IDs.
ref_condition (str, optional) – Reference condition. Required when contrasts is None.
pval_threshold (float) – Significance threshold. Default 0.05.
verbose (bool)
- Returns:
fate, contrast, coef, std_err, z_score, pval, pval_adj, significant
- Return type:
pd.DataFrame with columns
- trajectory_shift(results: Dict[str, scCS.scores.CommitmentScoreResult], pseudotime_key: str = 'sccs_pseudotime', n_bootstrap: int = 500, seed: int = 42, verbose: bool = True) pandas.DataFrame[source]¶
Test whether pseudotime distributions differ across conditions per fate arm.
For each fate arm and each pair of conditions, computes: - Kolmogorov-Smirnov (KS) statistic and p-value - Wasserstein distance (Earth Mover’s Distance) - Bootstrap CI on the Wasserstein distance
- Parameters:
results (dict) – Output of score_all_conditions().
pseudotime_key (str) – Column in adata_sub.obs with pseudotime values.
n_bootstrap (int) – Bootstrap replicates for Wasserstein CI. Default 500.
seed (int)
verbose (bool)
- Returns:
fate, comparison, ks_stat, ks_pval, wasserstein, wasserstein_ci_low, wasserstein_ci_high, mean_pt_A, mean_pt_B, delta_mean_pt, significant
- Return type:
pd.DataFrame with columns
- plot_trajectory_shift(shift_df: pandas.DataFrame, pseudotime_key: str = 'sccs_pseudotime', color_map: Dict[str, str] | None = None, figsize: Tuple[float, float] | None = None, title: str | None = None, save_path: str | None = None) matplotlib.figure.Figure[source]¶
Visualize pseudotime distributions per condition per fate arm.
Produces a grid of KDE plots: one row per fate arm, one column per pairwise comparison. Overlaid KDEs show how pseudotime distributions shift between conditions.
- Parameters:
shift_df (pd.DataFrame) – Output of trajectory_shift().
pseudotime_key (str)
color_map (dict, optional)
figsize (tuple, optional)
title (str, optional)
save_path (str, optional)
- Returns:
fig
- Return type:
matplotlib Figure
- plot_omnibus_summary(omnibus_df: pandas.DataFrame, results: Dict[str, scCS.scores.CommitmentScoreResult], posthoc_df: pandas.DataFrame | None = None, figsize: Tuple[float, float] | None = None, save_path: str | None = None, vmin: float | None = None, vmax: float | None = None) matplotlib.figure.Figure[source]¶
Summary heatmap: fates × conditions showing omnibus significance.
Left panel: heatmap of mean per-cell affinity per fate per condition, annotated with omnibus p-value stars. Right panel (if posthoc provided): significant pairwise comparisons.
- Parameters:
omnibus_df (pd.DataFrame) – Output of compare_omnibus().
results (dict) – Output of score_all_conditions().
posthoc_df (pd.DataFrame, optional) – Output of compare_posthoc().
figsize (tuple, optional)
save_path (str, optional)
vmin (float, optional) – Color limits for the mean-affinity heatmap. If both are
None(default), they are derived from the finite values of the affinity matrix so the colormap spans the actual data range. Set explicitly to pin a fixed scale across figures.vmax (float, optional) – Color limits for the mean-affinity heatmap. If both are
None(default), they are derived from the finite values of the affinity matrix so the colormap spans the actual data range. Set explicitly to pin a fixed scale across figures.
- Returns:
fig
- Return type:
matplotlib Figure
- plot_posthoc_heatmap(posthoc_df: pandas.DataFrame, fate: str | None = None, figsize: Tuple[float, float] | None = None, save_path: str | None = None) matplotlib.figure.Figure[source]¶
Condition × condition heatmap of post-hoc p-values for a given fate.
Lower triangle: p-values. Upper triangle: delta mean affinity. Annotated with significance stars.
- Parameters:
posthoc_df (pd.DataFrame) – Output of compare_posthoc().
fate (str, optional) – Which fate to plot. If None, uses the first fate with significant results.
figsize (tuple, optional)
save_path (str, optional)
- Returns:
fig
- Return type:
matplotlib Figure
- plot_pairwise_delta_grid(delta_results: Dict[Tuple[str, str], Dict], figsize_per_panel: Tuple[float, float] = (4, 4), cmap: str = 'RdBu_r', save_path: str | None = None) matplotlib.figure.Figure[source]¶
Grid of ΔCS heatmaps for all condition pairs.
Each panel shows ΔnCS = nCS_A − nCS_B for one condition pair, with bootstrap CI half-width annotated below each entry. Inherits the same layout as
scCS.plot.plot_delta_cs_heatmap()but renders all pairs on a single shared figure.- Parameters:
delta_results (dict) – Output of compute_pairwise_deltas().
figsize_per_panel (tuple)
cmap (str) – Diverging colormap. Default ‘RdBu_r’.
save_path (str, optional)
- Returns:
fig
- Return type:
matplotlib Figure
- transfer_labels(results: Dict[str, scCS.scores.CommitmentScoreResult], prefix: str = 'cs_') None[source]¶
Transfer per-cell commitment scores to the full adata for all conditions.
Calls SingleScorer.transfer_labels() for each condition’s result, writing condition-specific columns to adata.obs.
- Parameters:
results (dict) – Output of score_all_conditions(cell_level=True).
prefix (str) – Column prefix. Default: ‘cs_’.
- plot_star(result: scCS.scores.CommitmentScoreResult, **kwargs)[source]¶
Radial star embedding plot.
- plot_star_grid(results: Dict[str, scCS.scores.CommitmentScoreResult], color_map: Dict[str, str] | None = None, figsize_per_panel: Tuple[float, float] = (6, 6), save_path: str | None = None) matplotlib.figure.Figure[source]¶
Side-by-side star embedding plots, one per condition.
- plot_rose_grid(results: Dict[str, scCS.scores.CommitmentScoreResult], color_map: Dict[str, str] | None = None, figsize_per_panel: Tuple[float, float] = (5, 5), title: str | None = None, save_path: str | None = None) matplotlib.figure.Figure[source]¶
Grid of polar rose plots — one per condition.
- plot_affinity_distributions(results: Dict[str, scCS.scores.CommitmentScoreResult], plot_type: Literal['violin', 'box', 'strip'] = 'violin', color_map: Dict[str, str] | None = None, figsize: Tuple[float, float] | None = None, title: str | None = None, save_path: str | None = None) matplotlib.figure.Figure[source]¶
Violin/box plots of per-cell fate affinity scores by condition.
- plot_delta_cs_heatmap(delta_result: dict, **kwargs) matplotlib.figure.Figure[source]¶
Heatmap of ΔCS = nCS_A − nCS_B with CI annotation.
- plot_compare_conditions_bar(results: Dict[str, scCS.scores.CommitmentScoreResult], **kwargs) matplotlib.figure.Figure[source]¶
Grouped bar chart of nCS per condition.
- plot_commitment_vector_radar(results: Dict[str, scCS.scores.CommitmentScoreResult], **kwargs) matplotlib.figure.Figure[source]¶
Radar / spider chart of commitment vectors per condition.
- property scorer: scCS.single.SingleScorer | None[source]¶
The internal SingleScorer used for embedding and scoring.