API Reference

This page documents all public classes and functions in scCS. Full source-linked documentation is also available in the autoapi section in the sidebar.

SingleScorer

Single-condition analysis. Wraps an AnnData object and exposes all scoring and plotting methods.

class scCS.SingleScorer(adata, root: str, branches: List[str], obs_key: str = 'leiden', n_angle_bins: int = 36, sector_method: Literal['centroid', 'equal'] = 'centroid', copy: bool = False)[source]

Bases: _BaseScorer

RNA velocity commitment scorer with radial star embedding.

Computes commitment scores for a k-furcation defined by a single user-supplied bifurcation cluster and k terminal fate clusters.

The embedding places the bifurcation cluster at the origin and arranges each fate on its own radial arm, with cells ordered by differentiation level (pseudotime, CytoTRACE2, or custom score).

Parameters:
  • adata (AnnData) – Single-cell dataset.

  • root (str) – Label of the progenitor/root cluster in adata.obs[obs_key]. Example: ‘17’ (leiden cluster 17)

  • branches (list of str) – Labels of the k terminal fate clusters. Example: [‘Monocyte’, ‘DC’, ‘Neutrophil’]

  • obs_key (str) – Column in adata.obs with cluster labels. Default: ‘leiden’.

  • n_angle_bins (int) – Number of angular bins for commitment scoring. Default: 36 (10° each).

  • sector_method ({'centroid', 'equal'}) –

    How to define angular sectors: - ‘centroid’: anchor sectors to the direction from origin to each

    fate centroid in the star embedding (recommended).

    • ’equal’: divide [0°, 360°] into k equal sectors.

  • copy (bool) – Work on a copy of adata.

Examples

# k=2 bifurcation scorer = SingleScorer(

adata, root=’17’, branches=[‘homeostatic’, ‘activated’], obs_key=’leiden’,

) scorer.build_embedding(ordering_metric=’pseudotime’) scorer.fit() result = scorer.score() scorer.plot_star(result)

# k=3 with CytoTRACE2 scorer = SingleScorer(

adata, root=’5’, branches=[‘FateA’, ‘FateB’, ‘FateC’], obs_key=’cell_type’,

) scorer.build_embedding(ordering_metric=’cytotrace’) scorer.fit() result = scorer.score() scorer.plot_star(result)

compute_velocity(mode: str = 'dynamical', n_top_genes: int = 2000, n_pcs: int = 30, n_neighbors: int = 30, min_shared_counts: int = 20, verbose: bool = True) SingleScorer[source]

Run the full scVelo RNA velocity pipeline.

Call this if adata does not yet have velocity vectors. Requires ‘spliced’ and ‘unspliced’ layers.

Parameters:
  • mode ({'dynamical', 'stochastic', 'steady_state'})

  • n_top_genes (int)

  • n_pcs (int)

  • n_neighbors (int)

  • min_shared_counts (int)

  • verbose (bool)

Return type:

self

fit(verbose: bool = True) SingleScorer[source]

Build the FateMap from the user-supplied cluster labels.

Must be called after build_embedding().

This step: 1. Validates that root and branches

exist in adata.obs[obs_key].

  1. Computes fate centroids in the X_sccs embedding.

  2. Extracts velocity vectors if not already loaded.

Return type:

self

get_deg_drivers(n_top_genes: int = 50, pval_threshold: float = 0.05, logfc_threshold: float = 0.25) dict[source]

Find DEGs for each fate arm vs the bifurcation cluster (Wilcoxon).

Parameters:
  • n_top_genes (int)

  • pval_threshold (float)

  • logfc_threshold (float)

Returns:

dict

Return type:

fate_name -> DataFrame[gene, logfoldchange, pval, pval_adj, significant]

get_enrichment(deg_drivers: dict, gene_sets: List[str] | None = None, organism: str = 'mouse', pval_threshold: float = 0.05, logfc_threshold: float = 0.25, plot: bool = True, n_top_pathways: int = 15) dict[source]

Run pathway enrichment on DEG driver genes per fate arm.

Parameters:
  • deg_drivers (dict) – Output of get_deg_drivers().

  • gene_sets (list of str, optional)

  • organism (str)

  • pval_threshold (float)

  • logfc_threshold (float)

  • plot (bool)

  • n_top_pathways (int)

Returns:

dict

Return type:

fate_name -> {‘up’: DataFrame, ‘down’: DataFrame}

get_velocity_drivers(n_top_genes: int = 50) dict[source]

Rank genes by mean scVelo velocity in each fate arm.

Parameters:

n_top_genes (int) – Number of top driver genes to print per fate.

Returns:

dict

Return type:

fate_name -> DataFrame[gene, mean_velocity, rank]

get_velocity_fate_drivers(result: CommitmentScoreResult, n_top_genes: int = 50, pval_threshold: float = 0.05) dict[source]

Identify driver genes by correlating gene velocity with fate affinity.

Parameters:
  • result (CommitmentScoreResult) – Output of scorer.score(cell_level=True).

  • n_top_genes (int)

  • pval_threshold (float)

Returns:

dict – mean_velocity, delta_velocity, significant]

Return type:

fate_name -> DataFrame[gene, spearman_r, pval, pval_adj,

classmethod load(path: str, adata) SingleScorer[source]

Load a scorer from a pickle file.

Parameters:
  • path (str) – Path to a file saved by save().

  • adata (AnnData) – The full dataset (same object originally passed to SingleScorer.__init__). Not stored in the pickle.

Return type:

SingleScorer

plot_commitment_bar(result: CommitmentScoreResult, **kwargs)[source]

Bar chart of unCS vs nCS per fate pair.

plot_commitment_heatmap(result: CommitmentScoreResult, **kwargs)[source]

Per-cell fate affinity heatmap.

plot_nn_entropy_elbow(**kwargs)[source]

Elbow plots for choosing k_nn for NN-smoothed entropy.

Parameters:
Returns:

fig

Return type:

matplotlib Figure

plot_pairwise_cs(result: CommitmentScoreResult, **kwargs)[source]

Heatmap of pairwise normalized commitment scores.

plot_rose(result: CommitmentScoreResult, **kwargs)[source]

Rose/polar plot of cumulative magnitudes per angular bin.

plot_star(result: CommitmentScoreResult, **kwargs)[source]

Radial star embedding plot — primary visualization.

plot_subset_comparison(subset_results: dict, **kwargs)[source]

Compare commitment scores across subsets.

save(path: str) None[source]

Serialize scorer state to a pickle file.

Saves the embedding, FateMap, velocity vectors, and configuration. The full adata is NOT saved (too large); pass it again to load().

Parameters:

path (str) – Destination file path (e.g., 'scorer.pkl').

score(cell_mask: ndarray | None = None, cell_level: bool = True, k_nn: int | None = None, n_bootstrap: int = 0, bootstrap_ci: float = 0.95, bootstrap_seed: int = 42, verbose: bool = True, write_to_obs: bool = True) CommitmentScoreResult[source]

Compute commitment scores for the full population or a subset.

Parameters:
  • cell_mask (np.ndarray of bool, shape (n_sub_cells,), optional) – Boolean mask over adata_sub cells (NOT the full adata). If provided, only cells where mask=True contribute to the population-level score (M_bin, M_sector, unCS, nCS). Per-cell scores are still computed for all cells.

  • cell_level (bool) – Whether to compute per-cell fate affinity scores.

  • k_nn (int, optional) – If set, compute NN-smoothed per-cell entropy using this many nearest neighbors in the scCS embedding (X_sccs).

  • n_bootstrap (int) – Number of bootstrap replicates for CS confidence intervals. 0 (default) disables bootstrapping. Recommended: 500.

  • bootstrap_ci (float) – Confidence interval level for bootstrap. Default 0.95 (95% CI).

  • bootstrap_seed (int) – Random seed for bootstrap resampling.

  • verbose (bool)

  • write_to_obs (bool) – If True (default), write per-cell scores to adata_sub.obs.

Return type:

CommitmentScoreResult

score_per_subset(split_by: str, cell_level: bool = False, n_bootstrap: int = 0, verbose: bool = False) dict[source]

Compute commitment scores separately for each value of split_by.

Useful for comparing commitment across conditions, time points, or trajectory directions.

Parameters:
  • split_by (str) – Column in adata_sub.obs to split by.

  • cell_level (bool)

  • n_bootstrap (int) – Bootstrap replicates for CI. 0 = disabled.

  • verbose (bool)

Return type:

dict mapping subset_value -> CommitmentScoreResult

transfer_labels(adata, result: CommitmentScoreResult, prefix: str = 'cs_') None[source]

Write per-cell commitment scores back to the full adata.

After scoring, per-cell fate affinities, dominant fate, and entropy are stored in adata_sub.obs. This method transfers those columns to the full adata so they can be used in downstream analyses.

Cells not in the embedding subset receive NaN for numeric columns and ‘unassigned’ for categorical columns.

Parameters:
  • adata (AnnData) – The full dataset (same object passed to SingleScorer.__init__).

  • result (CommitmentScoreResult) – Output of scorer.score(cell_level=True).

  • prefix (str) – Column prefix. Default: ‘cs_’.

PairScorer

Pairwise comparison (exactly 2 conditions). Builds a shared star embedding on pooled data, then scores each condition separately. Provides statistical comparison, mixed-effects modeling, and trajectory shift analysis.

class scCS.PairScorer(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]

Bases: object

RNA velocity commitment scorer for pairwise (2-condition) experiments.

Builds a SHARED star embedding on the pooled data from both conditions, then scores each condition separately. This ensures arm geometry is identical across conditions, making CS values directly comparable.

Parameters:
  • adata (AnnData) – Full single-cell dataset containing both 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 exactly 2 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 or more than 2 unique values. For 3+ conditions, use MultiScorer instead.

Examples

>>> pscorer = PairScorer(
...     adata,
...     root='17',
...     branches=['homeostatic', 'activated'],
...     condition_obs_key='treatment',
...     obs_key='leiden',
... )
>>> pscorer.build_embedding(ordering_metric='pseudotime')
>>> pscorer.fit()
>>> results = pscorer.score_all_conditions()
>>> delta = pscorer.compute_delta_CS('control', 'treated')
>>> stats = pscorer.compare_conditions(results)
property adata_sub

The embedding subset (from the internal SingleScorer).

build_embedding(ordering_metric: str | 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) PairScorer[source]

Build the shared star embedding on pooled data from both conditions.

The embedding is built on ALL cells (both 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

compare_conditions(results: Dict[str, CommitmentScoreResult], test: Literal['permutation', 'kruskal'] = 'permutation', n_permutations: int = 1000, pval_threshold: float = 0.05, seed: int = 42, verbose: bool = True) DataFrame[source]

Statistical comparison of per-cell fate affinity scores across conditions.

For PairScorer (k=2 conditions), the default test is a permutation test: shuffle condition labels, recompute mean per-cell affinity difference, and get an empirical null distribution.

Parameters:
  • results (dict) – Output of score_all_conditions() with cell_level=True.

  • test ({'permutation', 'kruskal'}) – Statistical test to use. Default: ‘permutation’ (recommended for k=2).

  • n_permutations (int) – Number of permutations for the permutation test. Default 1000.

  • pval_threshold (float) – Significance threshold. Default 0.05.

  • seed (int)

  • verbose (bool)

Returns:

fate, test, statistic, pval, pval_adj, significant [+ comparison column for pairwise tests]

Return type:

pd.DataFrame with columns

compute_delta_CS(condition_a: str, condition_b: str, n_bootstrap: int = 500, ci: float = 0.95, seed: int = 42, verbose: bool = True) Dict[source]

Compute ΔCS = nCS_A − nCS_B with bootstrap confidence intervals.

For each pair of fates (i, j), computes the difference in normalized commitment score between condition A and condition B, with a bootstrap CI obtained by resampling cells within each condition.

Parameters:
  • condition_a (str) – Condition labels (must be in self.conditions).

  • condition_b (str) – Condition labels (must be in self.conditions).

  • n_bootstrap (int) – Number of bootstrap replicates. Default 500.

  • ci (float) – Confidence interval level. Default 0.95.

  • seed (int)

  • verbose (bool)

Returns:

‘delta_nCS’ : np.ndarray (k, k) — nCS_A − nCS_B ‘ci_low’ : np.ndarray (k, k) — lower CI bound on delta ‘ci_high’ : np.ndarray (k, k) — upper CI bound on delta ‘nCS_A’ : np.ndarray (k, k) — nCS for condition A ‘nCS_B’ : np.ndarray (k, k) — nCS for condition B ‘fate_names’ : list of str ‘condition_a’: str ‘condition_b’: str ‘n_bootstrap’: int ‘ci_level’ : float

Return type:

dict with keys

fit(verbose: bool = True) PairScorer[source]

Fit the shared FateMap and project velocity.

Must be called after build_embedding().

Return type:

self

fit_mixed_model(results: Dict[str, CommitmentScoreResult], replicate_key: str | None = None, ref_condition: str | None = None, verbose: bool = True) 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

property is_fitted: bool
plot_affinity_distributions(results: Dict[str, 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) Figure[source]

Violin/box plots of per-cell fate affinity scores by condition.

One panel per fate, showing the distribution of per-cell affinity scores split by condition.

Parameters:
  • results (dict) – Output of score_all_conditions(cell_level=True).

  • plot_type ({'violin', 'box', 'strip'})

  • color_map (dict, optional) – condition_label -> hex color.

  • figsize (tuple, optional)

  • title (str, optional)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

plot_commitment_vector_radar(results: Dict[str, CommitmentScoreResult], **kwargs) Figure[source]

Radar / spider chart of commitment vectors per condition.

Parameters:
Returns:

fig

Return type:

matplotlib Figure

plot_compare_conditions_bar(results: Dict[str, CommitmentScoreResult], **kwargs) Figure[source]

Grouped bar chart of nCS per condition.

Parameters:
Returns:

fig

Return type:

matplotlib Figure

plot_delta_cs_heatmap(delta_result: dict, **kwargs) Figure[source]

Heatmap of ΔCS = nCS_A − nCS_B with CI annotation.

Parameters:
Returns:

fig

Return type:

matplotlib Figure

plot_rose_grid(results: Dict[str, 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) Figure[source]

Grid of polar rose plots — one per condition.

All panels share the same radial scale, making magnitudes directly comparable across conditions.

Parameters:
  • results (dict)

  • color_map (dict, optional)

  • figsize_per_panel (tuple)

  • title (str, optional)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

plot_star(result: CommitmentScoreResult, **kwargs)[source]

Radial star embedding plot.

plot_star_grid(results: Dict[str, CommitmentScoreResult], color_map: Dict[str, str] | None = None, figsize_per_panel: Tuple[float, float] = (6, 6), save_path: str | None = None) Figure[source]

Side-by-side star embedding plots, one per condition.

All panels share the same arm geometry and color scale.

Parameters:
  • results (dict)

  • color_map (dict, optional)

  • figsize_per_panel (tuple)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

plot_trajectory_shift(shift_df: 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) Figure[source]

Visualize pseudotime distributions per condition per fate arm.

Produces a grid of KDE plots: one row per fate arm. 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

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) PairScorer[source]

Rebuild the shared embedding using subset-local pseudotime.

See SingleScorer.refit_pseudotime().

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, 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

property scorer: SingleScorer | None

The internal SingleScorer used for embedding and scoring.

trajectory_shift(results: Dict[str, CommitmentScoreResult], pseudotime_key: str = 'sccs_pseudotime', n_bootstrap: int = 500, seed: int = 42, verbose: bool = True) DataFrame[source]

Test whether pseudotime distributions differ across conditions per fate arm.

For each fate arm, 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

transfer_labels(results: Dict[str, 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_’.

MultiScorer

Multi-condition comparison (3+ conditions). Same shared-embedding approach as PairScorer, plus tiered statistical testing: omnibus tests followed by post-hoc pairwise comparisons, all-pairs delta-CS, and mixed-model contrasts.

class scCS.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]

Bases: object

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)
property adata_sub

The embedding subset (from the internal SingleScorer).

build_embedding(ordering_metric: str | 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

compare_omnibus(results: Dict[str, CommitmentScoreResult], test: Literal['kruskal', 'anova'] = 'kruskal', pval_threshold: float = 0.05, verbose: bool = True) 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, CommitmentScoreResult], omnibus_results: DataFrame | None = None, method: Literal['dunn', 'tukey', 'conover'] = 'dunn', pval_correction: Literal['fdr', 'bonferroni', 'holm'] = 'fdr', pval_threshold: float = 0.05, verbose: bool = True) 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(verbose: bool = True) MultiScorer[source]

Fit the shared FateMap and project velocity.

Must be called after build_embedding().

Return type:

self

fit_mixed_model(results: Dict[str, CommitmentScoreResult], replicate_key: str | None = None, ref_condition: str | None = None, verbose: bool = True) 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, 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) 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

property is_fitted: bool
plot_affinity_distributions(results: Dict[str, 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) Figure[source]

Violin/box plots of per-cell fate affinity scores by condition.

plot_commitment_vector_radar(results: Dict[str, CommitmentScoreResult], **kwargs) Figure[source]

Radar / spider chart of commitment vectors per condition.

plot_compare_conditions_bar(results: Dict[str, CommitmentScoreResult], **kwargs) Figure[source]

Grouped bar chart of nCS per condition.

plot_delta_cs_heatmap(delta_result: dict, **kwargs) Figure[source]

Heatmap of ΔCS = nCS_A − nCS_B with CI annotation.

plot_omnibus_summary(omnibus_df: DataFrame, results: Dict[str, CommitmentScoreResult], posthoc_df: DataFrame | None = None, figsize: Tuple[float, float] | None = None, save_path: str | None = None, vmin: float | None = None, vmax: float | None = None) 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_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) 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

plot_posthoc_heatmap(posthoc_df: DataFrame, fate: str | None = None, figsize: Tuple[float, float] | None = None, save_path: str | None = None) 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_rose_grid(results: Dict[str, 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) Figure[source]

Grid of polar rose plots — one per condition.

plot_star(result: CommitmentScoreResult, **kwargs)[source]

Radial star embedding plot.

plot_star_grid(results: Dict[str, CommitmentScoreResult], color_map: Dict[str, str] | None = None, figsize_per_panel: Tuple[float, float] = (6, 6), save_path: str | None = None) Figure[source]

Side-by-side star embedding plots, one per condition.

plot_trajectory_shift(shift_df: 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) 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

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().

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, 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

property scorer: SingleScorer | None

The internal SingleScorer used for embedding and scoring.

trajectory_shift(results: Dict[str, CommitmentScoreResult], pseudotime_key: str = 'sccs_pseudotime', n_bootstrap: int = 500, seed: int = 42, verbose: bool = True) 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

transfer_labels(results: Dict[str, 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_’.

CommitmentScoreResult

Dataclass returned by scorer.score(). Contains all computed scores, matrices, and metadata.

class scCS.CommitmentScoreResult(fate_names: List[str], M_bin: ndarray, bin_edges: ndarray, sectors: List[List[int]], M_sector: ndarray, n_cells_per_fate: ndarray, commitment_vector: ndarray, population_entropy: float, mean_cell_entropy: float, per_fate_entropy: ndarray, pairwise_unCS: ndarray, pairwise_nCS: ndarray, cell_scores: ndarray | None = None, fate_angles: ndarray | None = None, cell_obs_names: ndarray | None = None, nn_cell_entropy: ndarray | None = None, nn_k: int | None = None, bootstrap_ci: Dict[str, Any] | None = None)[source]

Bases: object

Container for all commitment score outputs.

fate_names
Type:

list of str

M_bin

Cumulative magnitude per angular bin.

Type:

np.ndarray, shape (n_bins,)

bin_edges
Type:

np.ndarray, shape (n_bins + 1,)

sectors
Type:

list of k lists of bin indices

M_sector

Cumulative magnitude per fate sector.

Type:

np.ndarray, shape (k,)

n_cells_per_fate
Type:

np.ndarray, shape (k,)

commitment_vector

Normalized (sums to 1).

Type:

np.ndarray, shape (k,)

population_entropy

Normalized Shannon entropy of the aggregate commitment vector in [0, 1]. Single scalar. See compute_population_entropy().

Type:

float

mean_cell_entropy

Mean normalized per-cell Shannon entropy in [0, 1]. See compute_mean_cell_entropy(). NaN when cell_level=False.

Type:

float

per_fate_entropy

Mean binary cell entropy for each fate individually. per_fate_entropy[j] = mean over cells of H_bin(s_ij, 1-s_ij). See compute_per_fate_cell_entropy(). All-NaN array when cell_level=False.

Type:

np.ndarray, shape (k,)

pairwise_unCS
Type:

np.ndarray, shape (k, k)

pairwise_nCS
Type:

np.ndarray, shape (k, k)

cell_scores
Type:

np.ndarray, shape (n_cells, k), optional

fate_angles

Angle (degrees) of each fate axis in the radial embedding.

Type:

np.ndarray, shape (k,), optional

nn_cell_entropy

NN-smoothed per-cell entropy. Set when k_nn > 0 in score(). Also written to adata_sub.obs[‘cs_nn_entropy’].

Type:

np.ndarray, shape (n_cells,), optional

nn_k

The k_nn value used to compute nn_cell_entropy.

Type:

int, optional

dominant_fate

Fate with highest M_sector.

Type:

str

M_bin: ndarray
M_sector: ndarray
bin_edges: ndarray
bootstrap_ci: Dict[str, Any] | None = None
cell_obs_names: ndarray | None = None
cell_scores: ndarray | None = None
property commitment_entropy: float

Alias for population_entropy (deprecated, use mean_cell_entropy).

commitment_vector: ndarray
property dominant_fate: str
fate_angles: ndarray | None = None
fate_names: List[str]
property k: int
mean_cell_entropy: float
n_cells_per_fate: ndarray
nn_cell_entropy: ndarray | None = None
nn_k: int | None = None
pairwise_nCS: ndarray
pairwise_to_dataframe(normalized: bool = True) DataFrame[source]

Pairwise CS matrix as a labeled DataFrame.

pairwise_unCS: ndarray
per_fate_entropy: ndarray
population_entropy: float
sectors: List[List[int]]
summary() str[source]
to_dataframe() DataFrame[source]

Summary DataFrame with one row per fate.

FateMap

Stores the fate topology: which cells belong to which arm, centroids, and arm angles.

class scCS.FateMap(root: str, fate_names: List[str], fate_centroids: ndarray, root_centroid: ndarray, root_cells: ndarray, fate_cell_indices: List[ndarray], arm_angles_deg: ndarray, obs_key: str)[source]

Bases: object

Standardized description of k cell fates for commitment scoring.

root

Label of the progenitor/root cluster supplied by the user.

Type:

str

fate_names

Human-readable labels for each terminal fate (length k).

Type:

list of str

fate_centroids

Mean 2D position of each fate’s cells in the scCS embedding.

Type:

np.ndarray, shape (k, 2)

root_centroid

Mean 2D position of the bifurcation cluster cells. In the scCS star embedding this is always near (0, 0).

Type:

np.ndarray, shape (2,)

root_cells

Indices of bifurcation cluster cells in adata.

Type:

np.ndarray of int

fate_cell_indices

Per-fate arrays of cell indices.

Type:

list of np.ndarray

arm_angles_deg

Angle (degrees) of each fate’s radial arm in the star embedding.

Type:

np.ndarray, shape (k,)

obs_key

The obs column used for cluster labels.

Type:

str

k

Number of fates (read-only property).

Type:

int

arm_angles_deg: ndarray
fate_cell_indices: List[ndarray]
fate_centroids: ndarray
fate_names: List[str]
property k: int
obs_key: str
root: str
root_cells: ndarray
root_centroid: ndarray
summary() str[source]

Plotting Functions

All plotting functions accept a color_map dict (fate name → hex color) to preserve your original scanpy/Seurat cluster colors.

Single-condition plots:

scCS.plot_star_embedding(adata, result: CommitmentScoreResult, color_by: str = 'fate', figsize: Tuple[float, float] = (8, 8), point_size: float = 8.0, alpha: float = 0.75, arm_color: str = '#CCCCCC', arm_linewidth: float = 1.5, arm_linestyle: str = '--', show_arm_labels: bool = True, show_velocity: bool = False, velocity_scale: float = 1.0, color_map: Dict[str, str] | None = None, title: str | None = None, vmin: float | None = None, vmax: float | None = None, cmap: str | None = None, ax: Axes | None = None, save_path: str | None = None) Figure[source]

Radial star embedding plot — the primary scCS visualization.

Draws the X_sccs embedding with: - Radial arm axes (dashed lines from origin to each fate tip) - Fate labels at the arm tips - Cells colored by fate, pseudotime, entropy, or per-fate affinity - Optional velocity arrows

Parameters:
  • adata (AnnData) – Must have X_sccs in obsm.

  • result (CommitmentScoreResult)

  • color_by (str) –

    What to color cells by: - "fate" — cluster/arm assignment (default) - "pseudotime" — reads sccs_pseudotime then velocity_pseudotime - "entropy" — per-cell commitment entropy (cs_entropy) - "nn_entropy" — NN-smoothed entropy (cs_nn_entropy;

    requires score(k_nn=...))

    • a fate name — per-cell affinity (cs_{fate}; requires score(cell_level=True))

    • any other str — auto-detected numeric or categorical column in adata.obs

  • figsize (tuple)

  • point_size (float)

  • alpha (float)

  • arm_color (str) – Color of the radial arm guide lines.

  • arm_linewidth (float)

  • arm_linestyle (str)

  • show_arm_labels (bool) – Draw fate name labels at arm tips.

  • show_velocity (bool) – Overlay velocity arrows (requires velocity_sccs in obsm).

  • velocity_scale (float) – Scale factor for velocity arrows.

  • title (str, optional)

  • vmin (float, optional) – Color-scale limits for numeric color_by modes. Defaults to the finite data range, so structure is always visible regardless of the absolute entropy/affinity scale. Pass explicit values to pin limits for cross-figure comparison.

  • vmax (float, optional) – Color-scale limits for numeric color_by modes. Defaults to the finite data range, so structure is always visible regardless of the absolute entropy/affinity scale. Pass explicit values to pin limits for cross-figure comparison.

  • cmap (str, optional) – Matplotlib colormap name. Defaults: "RdYlBu_r" for entropy, "viridis" for pseudotime/generic numeric, "Blues" for per-fate affinity.

  • ax (matplotlib Axes, optional)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

scCS.plot_star_panels(adata, result: CommitmentScoreResult, panels: List[str] | None = None, figsize_per_panel: Tuple[float, float] = (6, 6), point_size: float = 6.0, alpha: float = 0.75, color_map: Dict[str, str] | None = None, save_path: str | None = None) Figure[source]

Multi-panel star embedding: one panel per coloring scheme.

Default panels: fate assignment, pseudotime, entropy, + one per fate.

Parameters:
  • adata (AnnData)

  • result (CommitmentScoreResult)

  • panels (list of str, optional) – List of color_by values. Defaults to [‘fate’, ‘pseudotime’, ‘entropy’] + fate_names.

  • figsize_per_panel (tuple)

  • point_size (float)

  • alpha (float)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

scCS.plot_commitment_bar(result: CommitmentScoreResult, ref_fate: str | None = None, mode: str = 'auto', color_map: Dict[str, str] | None = None, title: str | None = None, figsize: Tuple[float, float] | None = None, save_path: str | None = None) Figure[source]

Bar chart of unCS and nCS for all k populations.

For a k-furcation, produces k subplots — one per reference fate. Each subplot shows unCS (solid) and nCS (hatched) for all other k-1 fates relative to that reference. This way every population is shown as both a query and a reference, and nothing is hidden.

For k=2 a single subplot is produced (equivalent to the old behaviour).

Parameters:
  • result (CommitmentScoreResult)

  • ref_fate (str, optional) – If given, produce only a single subplot using this fate as reference. Useful when you want a focused comparison.

  • mode (str) – Kept for backward compatibility; ignored.

  • color_map (dict, optional) – Mapping of fate name → hex color.

  • title (str, optional) – Overall figure title.

  • figsize (tuple, optional) – Per-subplot size (w, h). Total figure width scales with k.

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

Plot gene expression trends along a chosen commitment axis.

Cells are binned along the x-axis and mean expression per bin is plotted with a LOWESS smooth.

Parameters:
  • adata (AnnData) – Must contain the same cells as result.

  • result (CommitmentScoreResult)

  • genes (list of str) – Gene names to plot. Must be present in adata.var_names.

  • fate (str, optional) – Which fate to use as the reference. Defaults to the fate with the highest M_sector.

  • x_axis (str) –

    What to use as the x-axis for binning: - 'affinity' : per-cell fate affinity score for fate

    (0 → 1, from compute_cell_scores).

    • 'pseudotime'velocity_pseudotime from adata.obs

      (or sccs_pseudotime if available via compute_local_pseudotime()).

    • 'radial_distance': Euclidean distance from origin in X_sccs

      (arm position, 0 = progenitor, arm_scale = tip).

    Default: 'affinity'.

  • n_bins (int) – Number of bins along the x-axis.

  • layer (str, optional) – AnnData layer to use for expression. Defaults to adata.X.

  • smooth (bool) – Whether to overlay a LOWESS smoothed curve.

  • smooth_frac (float) – LOWESS smoothing fraction (0–1).

  • color_map (dict, optional) – Fate name → hex color. Used to color the smoothed line.

  • figsize (tuple, optional)

  • ncols (int) – Number of columns in the subplot grid.

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

scCS.plot_rose(result: CommitmentScoreResult, title: str = 'Cumulative Velocity Magnitude by Direction', figsize: Tuple[float, float] = (7, 7), show_sectors: bool = True, color_map: Dict[str, str] | None = None, ax: Axes | None = None, save_path: str | None = None) Figure[source]

Polar rose plot of cumulative velocity magnitudes per angular bin.

Each bin shows the total velocity magnitude pointing in that direction. Fate sectors are shaded with distinct colors.

Parameters:
  • result (CommitmentScoreResult)

  • title (str)

  • figsize (tuple)

  • show_sectors (bool)

  • ax (matplotlib Axes (polar), optional)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

scCS.plot_pairwise_cs(result: CommitmentScoreResult, normalized: bool = True, title: str | None = None, figsize: Tuple[float, float] | None = None, cmap: str = 'RdBu_r', save_path: str | None = None) Figure[source]

Heatmap of pairwise commitment scores.

Entry [i, j] = CS(fate_i relative to fate_j). Values > 1 indicate stronger commitment to fate_i than fate_j. Color scale is log2-transformed for readability.

Parameters:
  • result (CommitmentScoreResult)

  • normalized (bool) – Use nCS (True) or unCS (False).

  • title (str, optional)

  • figsize (tuple, optional)

  • cmap (str)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

scCS.plot_commitment_heatmap(result: CommitmentScoreResult, cell_scores: ndarray | None = None, max_cells: int = 500, title: str = 'Per-Cell Fate Affinity', figsize: Tuple[float, float] | None = None, save_path: str | None = None) Figure[source]

Heatmap of per-cell fate affinity scores (cells × fates).

Parameters:
  • result (CommitmentScoreResult)

  • cell_scores (np.ndarray, shape (n_cells, k), optional) – If None, uses result.cell_scores.

  • max_cells (int) – Subsample to this many cells for readability.

  • title (str)

  • figsize (tuple, optional)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

scCS.plot_subset_comparison(subset_results: dict, ref_fate: str | None = None, normalized: bool = True, title: str = 'Commitment Score by Subset', figsize: Tuple[float, float] = (8, 4), save_path: str | None = None) Figure[source]

Compare commitment scores across multiple subsets.

Subsets whose chosen reference pair yields inf (e.g. progenitor-only subsets with no fate-arm cells, so pairwise_nCS is undefined) are rendered as gray hatched placeholders at zero height with an "inf" annotation, instead of silently producing empty bars.

Parameters:
  • subset_results (dict) – Mapping of subset_name -> CommitmentScoreResult (from SingleScorer.score_per_subset).

  • ref_fate (str, optional) – Reference fate for the CS column. If None, use the fate with smallest sector magnitude (most likely to be present in all subsets).

  • normalized (bool) – If True use pairwise_nCS, else pairwise_unCS.

  • title (str)

  • figsize (tuple)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

scCS.plot_nn_entropy_elbow(scorer, k_nn_range: List[int] | range = range(5, 51, 5), color_map: Dict[str, str] | None = None, figsize: Tuple[float, float] = (12, 5), title: str | None = None, save_path: str | None = None) Figure[source]

Elbow plots for choosing the optimal number of nearest neighbors (k_nn).

Sweeps over k_nn_range, computing NN-smoothed cell entropy at each k, and produces two side-by-side subplots:

  • Left: mean NN entropy across all cells vs k_nn.

  • Right: mean NN entropy per fate arm vs k_nn (one line per fate).

Use these plots to identify the elbow — the k_nn where entropy stabilizes, indicating that additional smoothing no longer changes the signal.

Parameters:
  • scorer (SingleScorer) – A fitted scorer with build_embedding() and fit() already called. No prior score() call is needed — cell scores are recomputed internally from the velocity vectors.

  • k_nn_range (list or range) – k_nn values to sweep. Default: 5, 10, 15, …, 50.

  • color_map (dict, optional) – Fate name -> hex color. Falls back to the default FATE_PALETTE.

  • figsize (tuple)

  • title (str, optional) – Overall figure title. Defaults to “NN Entropy Elbow”.

  • save_path (str, optional) – If provided, save figure to this path.

Returns:

fig

Return type:

matplotlib Figure

Examples

>>> scorer.build_embedding(differentiation_metric='pseudotime')
>>> scorer.fit()
>>> result = scorer.score(compute_cell_level=True)
>>> fig = scorer.plot_nn_entropy_elbow()

Multi-condition plots (PairScorer + MultiScorer):

scCS.plot_rose_grid(results: Dict[str, 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) Figure[source]

Grid of polar rose plots — one per condition.

All panels share the same radial scale (max of all M_bin.max() across conditions), making magnitudes directly comparable. Fate sectors are shaded with FATE_PALETTE colors (consistent with single-condition plot_rose).

Parameters:
  • results (dict) – Mapping of condition_label -> CommitmentScoreResult (output of PairScorer.score_all_conditions()).

  • color_map (dict, optional) – fate_name -> hex color. Falls back to FATE_PALETTE.

  • figsize_per_panel (tuple) – Size of each polar subplot.

  • title (str, optional) – Overall figure title.

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

scCS.plot_delta_cs_heatmap(delta_result: dict, title: str | None = None, figsize: Tuple[float, float] | None = None, cmap: str = 'RdBu_r', save_path: str | None = None) Figure[source]

Heatmap of ΔCS = nCS_A − nCS_B with CI annotation.

Entry [i, j] = nCS_A(i÷j) − nCS_B(i÷j). Positive values (red) mean condition A has stronger commitment of fate i relative to fate j. Cells are annotated with Δ ± CI_half.

Parameters:
  • delta_result (dict) – Output of PairScorer.compute_delta_CS().

  • title (str, optional)

  • figsize (tuple, optional)

  • cmap (str) – Diverging colormap. Default: ‘RdBu_r’.

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

scCS.plot_compare_conditions_bar(results: Dict[str, CommitmentScoreResult], ref_fate: str | None = None, color_map: Dict[str, str] | None = None, title: str | None = None, figsize: Tuple[float, float] | None = None, save_path: str | None = None) Figure[source]

Grouped bar chart of nCS per condition.

For each fate pair (query ÷ reference), one group of bars — one bar per condition, colored by CONDITION_PALETTE. A horizontal dashed line at CS = 1 marks the neutral point.

Parameters:
  • results (dict) – Mapping of condition_label -> CommitmentScoreResult (output of PairScorer.score_all_conditions()).

  • ref_fate (str, optional) – Reference fate for the denominator. If None, uses the fate with the lowest mean M_sector across conditions.

  • color_map (dict, optional) – condition_label -> hex color. Falls back to CONDITION_PALETTE.

  • title (str, optional)

  • figsize (tuple, optional)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

scCS.plot_commitment_vector_radar(results: Dict[str, CommitmentScoreResult], color_map: Dict[str, str] | None = None, title: str | None = None, figsize: Tuple[float, float] = (6, 6), save_path: str | None = None) Figure[source]

Radar / spider chart of commitment vectors per condition.

Each condition is one closed polygon. Axes = fate names (k spokes). Values = commitment_vector (sums to 1). Conditions colored by CONDITION_PALETTE.

For k < 3, falls back to a grouped bar chart with a warning.

Parameters:
  • results (dict) – Mapping of condition_label -> CommitmentScoreResult (output of PairScorer.score_all_conditions()).

  • color_map (dict, optional) – condition_label -> hex color. Falls back to CONDITION_PALETTE.

  • title (str, optional)

  • figsize (tuple)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

MultiScorer-specific plots:

scCS.plot_omnibus_summary(omnibus_df, results: Dict[str, CommitmentScoreResult], posthoc_df=None, figsize: Tuple[float, float] | None = None, save_path: str | None = None) 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_df provided): significant pairwise comparisons as a connectivity grid.

Parameters:
  • omnibus_df (pd.DataFrame) – Output of MultiScorer.compare_omnibus(). Columns: fate, test, statistic, pval, pval_adj, significant.

  • results (dict) – Mapping of condition_label -> CommitmentScoreResult (output of MultiScorer.score_all_conditions()).

  • posthoc_df (pd.DataFrame, optional) – Output of MultiScorer.compare_posthoc(). If provided, right panel shows post-hoc significance grid.

  • figsize (tuple, optional)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

scCS.plot_posthoc_heatmap(posthoc_df, fate: str | None = None, figsize: Tuple[float, float] | None = None, save_path: str | None = None) Figure[source]

Condition × condition heatmap of post-hoc p-values for a given fate.

Lower triangle: p-values (color intensity). Upper triangle: delta mean affinity. Annotated with significance stars.

Parameters:
  • posthoc_df (pd.DataFrame) –

    Output of MultiScorer.compare_posthoc(). Columns: fate, comparison, method, statistic, pval, pval_adj,

    significant, mean_A, mean_B, delta_mean.

  • fate (str, optional) – Which fate to plot. If None, uses the first fate in posthoc_df.

  • figsize (tuple, optional)

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

scCS.plot_pairwise_delta_grid(delta_results: Dict[Tuple[str, str], dict], figsize_per_panel: Tuple[float, float] = (4, 4), save_path: str | None = None) Figure[source]

Grid of ΔCS heatmaps for all condition pairs.

Each panel shows the ΔnCS heatmap for one condition pair, using the same layout as plot_delta_cs_heatmap().

Parameters:
  • delta_results (dict) – Output of MultiScorer.compute_pairwise_deltas(). Mapping of (cond_a, cond_b) -> delta_result dict.

  • figsize_per_panel (tuple) – Size of each subplot.

  • save_path (str, optional)

Returns:

fig

Return type:

matplotlib Figure

Embedding

scCS.build_star_embedding(adata, root: str, branches: List[str], obs_key: str = 'leiden', ordering_metric: str | ndarray = 'pseudotime', invert_ordering: bool = False, arm_scale: float = 10.0, jitter: float = 0.3, seed: int = 42, arm_norm: str = 'global') AnnData[source]

Build the radial star embedding on a subset of adata.

Only cells belonging to the bifurcation cluster or a terminal fate cluster are included. All other populations are excluded entirely.

Parameters:
  • adata (AnnData) – Full dataset. Will NOT be modified.

  • root (str) – Label of the progenitor/bifurcation cluster in adata.obs[obs_key]. These cells are placed at the origin.

  • branches (list of str) – Labels of the k terminal fate populations. Each gets one radial arm.

  • obs_key (str) – Column in adata.obs with cluster labels.

  • ordering_metric (str or np.ndarray) – How to order cells along each arm: - ‘pseudotime’ : uses adata.obs[‘velocity_pseudotime’] (computed if absent) - ‘cytotrace’ : uses adata.obs[‘cytotrace2_score’] (must be pre-computed) - any str : uses adata.obs[ordering_metric] directly - np.ndarray : per-cell scores, shape (n_cells,) for the FULL adata Higher value = more differentiated = farther from center.

  • invert_ordering (bool) – If True, invert the metric so that high values map to the center (use for metrics where high = less differentiated, e.g. raw CytoTRACE2).

  • arm_scale (float) – Maximum radial distance (length of each arm).

  • jitter (float) – Gaussian noise added perpendicular to each arm to avoid overplotting.

  • seed (int) – Random seed for jitter.

  • arm_norm ({"global", "per_arm"}, default "global") –

    How to normalize the ordering metric onto the radial arms. The rescale formula (s - s_min) / (s_max - s_min) * arm_scale is only applied to fate cells (bifurcation cells sit at the origin); s_min and s_max are computed from fate cells only in both modes since v0.7.4, so the closest fate cell always maps to r 0 and the furthest to r arm_scale.

    • "global" (default, v0.7.3+): compute one (s_min, s_max) = (fate_scores.min(), fate_scores.max()) over all fate cells and apply uniformly to every arm. Arms whose cells span shorter pseudotime intervals stay visibly shorter. Preserves the relative ordering of cells across arms — if Alpha cells span a wider pseudotime range than Delta cells, the Alpha arm extends further. Biologically meaningful: arm length reflects how far each fate has differentiated from the progenitor on a shared scale.

    • "per_arm" (legacy, pre-v0.7.3 default): each arm gets its own (s_min, s_max) = (fate_mask_scores.min(), fate_mask_scores.max()) and is mapped to [0, arm_scale] independently. All arms reach the full arm_scale regardless of how compressed/extended their pseudotime range is. Provided for reproducibility of older plots.

    Changed in version 0.7.4: Both modes now compute s_min/s_max from fate cells only, instead of including bifurcation cells. This removes a visible gap between the origin and the start of each arm in v0.7.3 "global" mode.

Returns:

adata_sub – Subset containing ONLY bifurcation + terminal fate cells. Star embedding stored in adata_sub.obsm[‘X_sccs’]. Metadata stored in adata_sub.uns[‘sccs’].

Return type:

AnnData

scCS.compute_local_pseudotime(adata_sub, adata_full, scale_01: bool = True, verbose: bool = True) ndarray[source]

Recompute velocity pseudotime on the subset’s induced subgraph.

When build_star_embedding uses ordering_metric='pseudotime', the pseudotime is resolved on the full adata before subsetting. This means the pseudotime range within the bifurcation+fate subset is compressed and non-uniform: cells that span the full differentiation axis in the subset may all cluster near 0 or 1 on the arm, leaving large empty stretches.

This function extracts the velocity_graph submatrix for the subset cells, recomputes pseudotime locally, and optionally scales it to [0, 1]. The result is stored in adata_sub.obs['sccs_pseudotime'] and returned as an array.

Call this after build_embedding() and before (or instead of) using the full-adata pseudotime for arm ordering. To rebuild the embedding with the corrected pseudotime, pass the returned array as a custom metric:

scorer.build_embedding(ordering_metric='pseudotime')
pt_sub = compute_local_pseudotime(scorer.adata_sub, adata)
scorer.build_embedding(ordering_metric=pt_sub_full)
# where pt_sub_full is the subset scores mapped back to full adata indices

Alternatively, use the convenience method SingleScorer.refit_pseudotime().

Parameters:
  • adata_sub (AnnData) – Subset returned by build_star_embedding(). Must have uns['sccs']['parent_indices'] set (done automatically).

  • adata_full (AnnData) – Full dataset with intact uns['velocity_graph'].

  • scale_01 (bool) – If True (default), min-max scale the recomputed pseudotime to [0, 1] within the subset. This ensures cells span the full arm length regardless of where the subset sits in the global pseudotime range. If False, the raw pseudotime values are returned (useful when you want to compare absolute pseudotime across conditions).

  • verbose (bool)

Returns:

pt_sub – Subset-local pseudotime, stored in adata_sub.obs['sccs_pseudotime'].

Return type:

np.ndarray, shape (n_sub_cells,)

scCS.scale_metric_01(scores: ndarray) ndarray[source]

Min-max scale a per-cell metric to [0, 1].

Useful for normalizing any differentiation metric (pseudotime, CytoTRACE2, pathway score, etc.) before passing it to build_star_embedding so that cells span the full arm length uniformly.

Parameters:

scores (np.ndarray, shape (n_cells,)) – Per-cell metric values. NaN values are preserved.

Returns:

scaled – Values in [0, 1]. Returns zeros if all values are identical.

Return type:

np.ndarray, shape (n_cells,)

Fate Detection

scCS.build_fate_map(adata, root: str, branches: List[str], obs_key: str = 'leiden', verbose: bool = True) FateMap[source]

Build a FateMap from user-supplied cluster labels.

This is the only fate-detection strategy in scCS. The user explicitly names the bifurcation cluster and all terminal fate clusters.

Parameters:
  • adata (AnnData) – Must have X_sccs in obsm (built by build_star_embedding).

  • root (str) – Label of the progenitor cluster in adata.obs[obs_key]. Example: ‘17’ (leiden cluster 17)

  • branches (list of str) – Labels of the k terminal fate clusters. Example: [‘Monocyte’, ‘DC’, ‘Neutrophil’]

  • obs_key (str) – Column in adata.obs with cluster labels.

  • verbose (bool)

Return type:

FateMap

Driver Genes

Three complementary approaches to identify fate-driving genes:

scCS.get_velocity_drivers(adata_sub, fate_names: List[str], obs_key: str, root: str, n_top_genes: int = 50) Dict[str, DataFrame][source]

Rank genes by mean scVelo velocity in each fate arm’s cells.

Parameters:
  • adata_sub (AnnData) – Subset containing only bifurcation + terminal fate cells. Must have the ‘velocity’ layer (from scVelo).

  • fate_names (list of str) – Terminal fate cluster labels.

  • obs_key (str) – Column in adata_sub.obs with cluster labels.

  • root (str) – Label of the progenitor cluster (used for context only).

  • n_top_genes (int) – Number of top driver genes to print per fate.

Returns:

dict – Sorted by mean_velocity descending (most upregulated first).

Return type:

fate_name -> DataFrame with columns [gene, mean_velocity, rank]

scCS.get_deg_drivers(adata_sub, fate_names: List[str], obs_key: str, root: str, n_top_genes: int = 50, pval_threshold: float = 0.05, logfc_threshold: float = 0.25) Dict[str, DataFrame][source]

Find DEGs for each fate arm vs the bifurcation cluster (Wilcoxon).

For each fate arm, compares arm cells against progenitor (bifurcation) cells using a Wilcoxon rank-sum test via scanpy.

Parameters:
  • adata_sub (AnnData) – Subset containing only bifurcation + terminal fate cells.

  • fate_names (list of str) – Terminal fate cluster labels.

  • obs_key (str) – Column in adata_sub.obs with cluster labels.

  • root (str) – Label of the progenitor cluster (reference group).

  • n_top_genes (int) – Number of top significant DEGs to print per fate.

  • pval_threshold (float) – Adjusted p-value threshold for significance.

  • logfc_threshold (float) – Minimum absolute log fold-change for significance.

Returns:

dict – [gene, logfoldchange, pval, pval_adj, significant] Sorted by logfoldchange descending.

Return type:

fate_name -> DataFrame with columns:

scCS.get_velocity_fate_drivers(adata_sub, cell_scores: ndarray, fate_names: List[str], obs_key: str, root: str, n_top_genes: int = 50, pval_threshold: float = 0.05, min_cells: int = 10) Dict[str, DataFrame][source]

Identify driver genes by correlating gene velocity with fate affinity.

For each fate arm, computes the Spearman correlation between each gene’s velocity (from the ‘velocity’ layer) and the cell’s fate affinity score (from cell_scores[:, j]). Genes with high positive Spearman correlation are being upregulated specifically as cells commit to that fate — a stronger signal than mean velocity alone, because it filters out genes that are fast everywhere.

Algorithm

  1. For each fate j, extract velocity matrix V (n_cells × n_genes).

  2. Extract fate affinity vector a (n_cells,) = cell_scores[:, j].

  3. Compute Spearman correlation between a and each gene’s velocity column.

  4. Compute FDR-corrected p-values (Benjamini-Hochberg via statsmodels).

  5. Return DataFrame sorted by spearman_r descending.

param adata_sub:

Subset containing only bifurcation + terminal fate cells. Must have the ‘velocity’ layer (from scVelo).

type adata_sub:

AnnData

param cell_scores:

Per-cell fate affinity scores from CommitmentScoreResult.cell_scores.

type cell_scores:

np.ndarray, shape (n_cells, k)

param fate_names:

Terminal fate cluster labels (length k).

type fate_names:

list of str

param obs_key:

Column in adata_sub.obs with cluster labels.

type obs_key:

str

param root:

Label of the progenitor cluster.

type root:

str

param n_top_genes:

Number of top driver genes to print per fate.

type n_top_genes:

int

param pval_threshold:

FDR-adjusted p-value threshold for significance.

type pval_threshold:

float

param min_cells:

Minimum number of cells required to compute correlations.

type min_cells:

int

returns:

dict

[gene, spearman_r, pval, pval_adj, mean_velocity, delta_velocity,

significant]

Sorted by spearman_r descending.

rtype:

fate_name -> DataFrame with columns:

Pathway Enrichment

scCS.run_enrichment_per_fate(deg_drivers: Dict[str, DataFrame], fate_names: List[str] | None = None, gene_sets: List[str] | None = None, organism: str = 'mouse', pval_threshold: float = 0.05, logfc_threshold: float = 0.25, plot: bool = True, n_top_pathways: int = 15) Dict[str, Dict[str, DataFrame]][source]

Run Enrichr ORA on DEG driver genes for each fate arm.

Runs separately for up-regulated and down-regulated genes. Requires gseapy >= 1.0.

Parameters:
  • deg_drivers (dict) – Output of get_deg_drivers(). fate_name -> DataFrame[gene, logfoldchange, pval, pval_adj, significant]

  • fate_names (list of str, optional) – Terminal fate cluster labels (determines iteration order). If omitted (default None), the fate names are inferred from deg_drivers.keys() in their natural insertion order. If provided but missing entries that appear in deg_drivers, a warning is emitted and only the intersection is used.

  • gene_sets (list of str, optional) – Enrichr gene set library names. Defaults to KEGG + GO BP + Reactome for the specified organism.

  • organism (str) – ‘mouse’ or ‘human’. Used for default gene sets and Enrichr organism.

  • pval_threshold (float) – Adjusted p-value threshold for reporting enriched terms.

  • logfc_threshold (float) – Minimum absolute logFC used to split up/down gene lists.

  • plot (bool) – If True, generate dot plots per fate per direction.

  • n_top_pathways (int) – Number of top enriched terms to show in dot plots.

Returns:

dict – Each DataFrame has columns: [Gene_set, Term, Overlap, P-value, Adjusted P-value, Genes] Sorted by Adjusted P-value ascending. Empty DataFrame if no significant terms found.

Return type:

fate_name -> {‘up’: DataFrame, ‘down’: DataFrame}

scCS.export_enrichment_tables(enrichment_results: Dict[str, Dict[str, DataFrame]], output_dir: str = '.', prefix: str = 'enrichment') List[str][source]

Save enrichment result DataFrames to CSV files.

Parameters:
  • enrichment_results (dict) – Output of run_enrichment_per_fate().

  • output_dir (str) – Directory to save files.

  • prefix (str) – Filename prefix.

Returns:

list of str

Return type:

paths of saved files.

Core Math — Entropy

scCS.compute_population_entropy(p_vec: ndarray) float[source]

Shannon entropy of the aggregate commitment vector, normalized to [0, 1].

Operates on the population-level commitment vector p_vec = M_sector / sum(M_sector), which reflects how total velocity mass is distributed across fate sectors.

H_pop = 0 => all velocity mass concentrated in one sector. H_pop = 1 => velocity mass uniformly spread across all sectors.

Warning

This metric can be misleading when cells are split between fates. A population where 50 % of cells strongly commit to fate A and 50 % strongly commit to fate B will yield H_pop ≈ 1 (maximum uncertainty), even though every individual cell is decisive. Use compute_mean_cell_entropy() as the primary commitment metric.

Parameters:

p_vec (np.ndarray, shape (k,)) – Normalized commitment vector (sums to 1).

Return type:

float in [0, 1]

scCS.compute_mean_cell_entropy(cell_scores: ndarray) float[source]

Mean per-cell Shannon entropy of fate-affinity scores, normalized to [0, 1].

For each cell i, computes the normalized Shannon entropy of its row-normalized fate-affinity vector s_i = cell_scores[i, :]:

h_i = -sum_j( s_ij * log(s_ij) ) / log(k)

and returns the mean over all cells:

H_cell = mean_i( h_i )

This is the recommended primary entropy metric because it measures individual cell commitment uncertainty rather than population-level velocity-mass balance.

Interpretation

H_cell ≈ 0 => cells are individually decisive (each cell strongly

favors one fate). Occurs in committed populations regardless of whether cells split between fates.

H_cell ≈ 1 => cells are individually undecided (each cell’s velocity

points equally toward all fates). Occurs in genuinely uncommitted / progenitor-like populations.

Contrast with compute_population_entropy()

A population split 50/50 between two strongly committed sub-groups gives: - H_pop ≈ 1.0 (misleadingly high — velocity mass is balanced) - H_cell ≈ 0.0 (correctly low — each cell is individually committed)

param cell_scores:

Per-cell fate-affinity matrix, row-normalized to sum to 1. Typically the output of compute_cell_scores().

type cell_scores:

np.ndarray, shape (n_cells, k)

returns:

Mean normalized per-cell entropy. Returns 0.0 for a single cell or single fate.

rtype:

float in [0, 1]

scCS.compute_per_fate_cell_entropy(cell_scores: ndarray) ndarray[source]

Per-fate mean binary cell entropy of fate-affinity scores.

For each fate j, treats each cell’s affinity score s_ij as a binary distribution [s_ij, 1 - s_ij] and computes the normalized binary Shannon entropy, then averages over all cells:

h_j = mean_i[ H_bin(s_ij) ]
    = mean_i[ -(s_ij * log(s_ij) + (1-s_ij) * log(1-s_ij)) / log(2) ]

Interpretation

h_j ≈ 0 => cells are sharply decisive about fate j (either strongly

committed or strongly not committed).

h_j ≈ 1 => cells are ambiguous about fate j (scores cluster near 0.5).

This is the per-fate analogue of compute_mean_cell_entropy().

param cell_scores:

Per-cell fate-affinity matrix, row-normalized to sum to 1. Typically the output of compute_cell_scores().

type cell_scores:

np.ndarray, shape (n_cells, k)

returns:

per_fate_entropy – Mean binary entropy for each fate. Returns zeros for k=0 or n=0.

rtype:

np.ndarray, shape (k,)

scCS.compute_nn_cell_entropy(cell_scores: ndarray, coords: ndarray, k_nn: int) ndarray[source]

NN-smoothed per-cell commitment entropy in the scCS embedding.

For each cell i: 1. Find its k_nn nearest neighbors in coords (X_sccs, 2D). 2. Average cell_scores over those neighbors (including cell i itself). 3. Compute normalized k-way Shannon entropy on the smoothed scores.

This removes single-cell velocity noise while preserving local commitment structure. Use scCS.plot.plot_nn_entropy_elbow() to choose k_nn.

Parameters:
  • cell_scores (np.ndarray, shape (n_cells, k)) – Per-cell fate-affinity matrix from compute_cell_scores().

  • coords (np.ndarray, shape (n_cells, 2)) – 2D scCS embedding coordinates (adata_sub.obsm['X_sccs']).

  • k_nn (int) – Number of nearest neighbors to average over (excluding self). Self is always included, so the effective window is k_nn + 1 cells.

Returns:

nn_entropy – Normalized Shannon entropy of the NN-smoothed fate scores per cell, in [0, 1].

Return type:

np.ndarray, shape (n_cells,)

Core Math — Scores

scCS.compute_unCS(M_sector_i: float, M_sector_j: float) float[source]

Unnormalized commitment score of fate i relative to fate j (Eq. 8).

unCS > 1 => population is more committed to fate i than fate j.

Parameters:
  • M_sector_i (float) – Cumulative magnitudes for fates i and j.

  • M_sector_j (float) – Cumulative magnitudes for fates i and j.

Return type:

float (inf if M_sector_j == 0)

scCS.compute_nCS(M_sector_i: float, M_sector_j: float, n_cells_i: int, n_cells_j: int) float[source]

Cell-number-normalized commitment score (Eq. 9).

nCS = (M_sector_i / M_sector_j) * (n_cells_j / n_cells_i)

Parameters:
  • M_sector_i (float)

  • M_sector_j (float)

  • n_cells_i (int) – Number of cells in each population / trajectory arm.

  • n_cells_j (int) – Number of cells in each population / trajectory arm.

Return type:

float

scCS.compute_commitment_vector(M_sector: ndarray) ndarray[source]

Normalize sector magnitudes to a probability-like commitment vector.

Parameters:

M_sector (np.ndarray, shape (k,))

Returns:

p_vec – Sums to 1. All-zero input returns uniform distribution.

Return type:

np.ndarray, shape (k,)

scCS.compute_pairwise_cs_matrix(M_sector: ndarray, n_cells_per_fate: ndarray | None = None, normalized: bool = True) ndarray[source]

Compute full k x k pairwise commitment score matrix.

Entry [i, j] = CS(i relative to j). Diagonal is 1.0.

Parameters:
  • M_sector (np.ndarray, shape (k,))

  • n_cells_per_fate (np.ndarray, shape (k,), optional) – If provided and normalized=True, computes nCS; else unCS.

  • normalized (bool)

Returns:

cs_matrix

Return type:

np.ndarray, shape (k, k)

scCS.compute_cell_scores(vx: ndarray, vy: ndarray, fate_centroids: ndarray, root_centroid: ndarray, mag_weight: bool = True, mag_threshold_pct: float = 5.0) ndarray[source]

Per-cell fate affinity: magnitude-weighted cosine similarity to fate direction.

For each cell i and fate j, computes:

raw_score(i, j) = dot(unit_v_i, unit_d_j)

where unit_d_j is the unit vector from root_centroid to fate_centroid_j.

Scores are shifted to [0, 1] via (score + 1) / 2, then optionally weighted by the cell’s velocity magnitude (normalized to [0, 1]). Cells with velocity magnitude below mag_threshold_pct percentile are down-weighted toward the uniform distribution (1/k), reducing noise from near-zero-velocity cells (typically progenitors at the origin).

Parameters:
  • vx (np.ndarray, shape (n_cells,))

  • vy (np.ndarray, shape (n_cells,))

  • fate_centroids (np.ndarray, shape (k, 2))

  • root_centroid (np.ndarray, shape (2,))

  • mag_weight (bool) – If True (default), weight each cell’s score by its normalized velocity magnitude. Low-magnitude cells are pulled toward the uniform distribution (1/k), reducing noise from near-stationary cells. If False, all cells contribute equally (original behavior).

  • mag_threshold_pct (float) – Percentile of velocity magnitudes below which cells are considered near-stationary and receive zero weight (replaced by 1/k). Default: 5th percentile. Only used when mag_weight=True.

Returns:

cell_scores – Per-cell affinity for each fate, row-normalized to sum to 1. Low-magnitude cells have scores close to 1/k (uniform).

Return type:

np.ndarray, shape (n_cells, k)

scCS.compute_magnitudes(vx: ndarray, vy: ndarray) ndarray[source]

Euclidean norm of 2D velocity vectors (Eq. 1).

Parameters:
  • vx (array-like, shape (n_cells,)) – x and y components of velocity vectors.

  • vy (array-like, shape (n_cells,)) – x and y components of velocity vectors.

Returns:

magnitudes – Non-negative magnitudes; NaN inputs yield NaN.

Return type:

np.ndarray, shape (n_cells,)

scCS.compute_angles(vx: ndarray, vy: ndarray) ndarray[source]

Angle of each velocity vector in [0, 360) degrees (Eq. 2-3).

Parameters:
  • vx (array-like, shape (n_cells,))

  • vy (array-like, shape (n_cells,))

Returns:

angles_deg – Angles in degrees, range [0, 360). NaN for zero-magnitude vectors.

Return type:

np.ndarray, shape (n_cells,)

scCS.bin_angles(angles_deg: ndarray, magnitudes: ndarray, n_bins: int = 36) Tuple[ndarray, ndarray][source]

Discretize angles and accumulate magnitudes per bin (Eq. 4-6).

Parameters:
  • angles_deg (np.ndarray, shape (n_cells,)) – Angles in [0, 360). NaN values are ignored.

  • magnitudes (np.ndarray, shape (n_cells,)) – Per-cell magnitudes. NaN values are ignored.

  • n_bins (int) – Number of angular bins. Default 36 (10° each, as in manuscript).

Returns:

  • bin_edges (np.ndarray, shape (n_bins + 1,)) – Bin edge angles in degrees.

  • M_bin (np.ndarray, shape (n_bins,)) – Cumulative magnitude per bin.

scCS.equal_sectors(k: int, n_bins: int = 36) List[List[int]][source]

Divide n_bins into k equal contiguous sectors.

Parameters:
  • k (int) – Number of fates / sectors.

  • n_bins (int) – Total number of angular bins.

Returns:

sectors – Each inner list contains the bin indices belonging to that sector.

Return type:

list of lists

scCS.centroid_sectors(fate_centroids: ndarray, root_centroid: ndarray, n_bins: int = 36) Tuple[List[List[int]], ndarray][source]

Define sectors anchored to fate centroid directions from the root.

Each sector is centered on the angle from the root to the corresponding fate centroid. Sector boundaries are placed at the midpoints between adjacent fate angles.

Parameters:
  • fate_centroids (np.ndarray, shape (k, 2)) – 2D embedding coordinates of each fate centroid.

  • root_centroid (np.ndarray, shape (2,)) – 2D embedding coordinate of the root / progenitor centroid.

  • n_bins (int) – Number of angular bins.

Returns:

  • sectors (list of k lists of bin indices)

  • fate_angles (np.ndarray, shape (k,)) – Central angle (degrees) for each fate.

scCS.compute_sector_magnitudes(M_bin: ndarray, sectors: List[List[int]]) ndarray[source]

Sum M_bin values within each sector (Eq. 7).

Parameters:
  • M_bin (np.ndarray, shape (n_bins,))

  • sectors (list of k lists of bin indices)

Returns:

M_sector

Return type:

np.ndarray, shape (k,)

scCS.bootstrap_cs(vx: ndarray, vy: ndarray, sectors: List[List[int]], n_cells_per_fate: ndarray, n_bins: int = 36, n_bootstrap: int = 500, ci: float = 0.95, seed: int = 42, normalized: bool = True, stratified: bool = False, fate_cell_indices: Sequence | None = None) Dict[source]

Bootstrap confidence intervals for pairwise commitment scores.

Resamples cells with replacement n_bootstrap times, recomputes unCS and nCS for each bootstrap replicate, and returns the empirical CI bounds.

Parameters:
  • vx (np.ndarray, shape (n_cells,)) – Velocity components in scCS space.

  • vy (np.ndarray, shape (n_cells,)) – Velocity components in scCS space.

  • sectors (list of k lists of bin indices) – Sector definition from centroid_sectors() or equal_sectors().

  • n_cells_per_fate (np.ndarray, shape (k,)) – Number of cells per fate arm (used for nCS normalization).

  • n_bins (int) – Number of angular bins.

  • n_bootstrap (int) – Number of bootstrap replicates. Default 500.

  • ci (float) – Confidence interval level. Default 0.95 (95% CI).

  • seed (int)

  • normalized (bool) – If True, return CI for nCS; if False, for unCS.

  • stratified (bool) – If True, resample cells within each fate arm separately (preserving arm cell counts), then concatenate. Prevents bootstrap replicates with very few cells in one arm. Requires fate_cell_indices. Default False (uniform resampling, original behavior).

  • fate_cell_indices (sequence of array-like, optional) – List of k arrays, each containing the integer indices of cells belonging to that fate arm. Required when stratified=True. Typically fate_map.fate_cell_indices.

Returns:

‘mean’ : np.ndarray (k, k) — mean CS across replicates ‘ci_low’ : np.ndarray (k, k) — lower CI bound ‘ci_high’: np.ndarray (k, k) — upper CI bound ‘std’ : np.ndarray (k, k) — standard deviation across replicates ‘n_bootstrap’: int ‘ci_level’: float

Return type:

dict with keys