scCS.scores =========== .. py:module:: scCS.scores .. autoapi-nested-parse:: scores.py — Core commitment score math engine for scCS. Implements the generalized k-furcation commitment score framework, extending the 2-state (homeostatic/activated) formulation from: Kriukov et al. (2025) "Single-cell transcriptome of myeloid cells in response to transplantation of human retinal neurons reveals reversibility of microglial activation" Mathematical framework ---------------------- Given per-cell RNA velocity vectors (vx_i, vy_i) in the scCS radial embedding: 1. magnitude_i = sqrt(vx_i^2 + vy_i^2) [Eq. 1] 2. theta_i = atan2(vy_i, vx_i) -> [0, 360) [Eq. 2-3] 3. Bin angles into N bins of width 360/N degrees [Eq. 4-5] 4. M_bin(b) = sum of magnitude_i for all cells in bin b [Eq. 6] 5. M_sector(j) = sum of M_bin(b) for b in sector j [Eq. 7] 6. unCS(i,j) = M_sector(i) / M_sector(j) [Eq. 8] 7. nCS(i,j) = unCS(i,j) * n_cells(j) / n_cells(i) [Eq. 9] Generalization to k fates: - CS_vec = [M_sector(1), ..., M_sector(k)] (raw) - p_vec = CS_vec / sum(CS_vec) (normalized) - H_pop = -sum(p_k * log(p_k)) / log(k) (population entropy) - H_cell_j = mean_i[ h_bin(s_ij) ] (per-fate cell entropy, k values) - H_nn_i = H( mean_{n in NN(i)}(cell_scores[n]) ) (NN-smoothed per-cell entropy) - cell_scores = dot(unit_velocity_i, unit_direction_to_fate_j) (per-cell) Entropy notes ------------- Three complementary entropy metrics are provided: ``compute_population_entropy(p_vec)`` → float Entropy of the aggregate commitment vector (M_sector / sum(M_sector)). Single scalar. Measures how evenly total velocity mass is distributed. **Limitation**: high for any balanced split, even if every cell is decisive. ``compute_per_fate_cell_entropy(cell_scores)`` → ndarray shape (k,) For each fate j: binary entropy of each cell's affinity toward j, averaged over all cells. h_j = mean_i[ H_bin(s_ij, 1-s_ij) ]. Tells you per-fate how individually decisive cells are toward that fate. Low h_j = cells are sharply committed (or sharply not committed) to fate j. High h_j = cells are ambiguous about fate j. ``compute_nn_cell_entropy(cell_scores, coords, k_nn)`` → ndarray shape (n_cells,) For each cell: average cell_scores over its k_nn nearest neighbors in the scCS embedding (X_sccs), then compute full k-way entropy on the smoothed scores. Spatially local smoothing removes single-cell noise. Use the elbow plots (plot_nn_entropy_elbow) to choose k_nn. Attributes ---------- .. autoapisummary:: scCS.scores.compute_commitment_entropy Classes ------- .. autoapisummary:: scCS.scores.CommitmentScoreResult Functions --------- .. autoapisummary:: scCS.scores.compute_magnitudes scCS.scores.compute_angles scCS.scores.bin_angles scCS.scores.equal_sectors scCS.scores.centroid_sectors scCS.scores.compute_sector_magnitudes scCS.scores.compute_unCS scCS.scores.compute_nCS scCS.scores.compute_commitment_vector scCS.scores.compute_population_entropy scCS.scores.compute_mean_cell_entropy scCS.scores.compute_per_fate_cell_entropy scCS.scores.compute_nn_cell_entropy scCS.scores.compute_pairwise_cs_matrix scCS.scores.compute_cell_scores scCS.scores.bootstrap_cs Module Contents --------------- .. py:function:: compute_magnitudes(vx: numpy.ndarray, vy: numpy.ndarray) -> numpy.ndarray Euclidean norm of 2D velocity vectors (Eq. 1). :param vx: x and y components of velocity vectors. :type vx: array-like, shape (n_cells,) :param vy: x and y components of velocity vectors. :type vy: array-like, shape (n_cells,) :returns: **magnitudes** -- Non-negative magnitudes; NaN inputs yield NaN. :rtype: np.ndarray, shape (n_cells,) .. py:function:: compute_angles(vx: numpy.ndarray, vy: numpy.ndarray) -> numpy.ndarray Angle of each velocity vector in [0, 360) degrees (Eq. 2-3). :param vx: :type vx: array-like, shape (n_cells,) :param vy: :type vy: array-like, shape (n_cells,) :returns: **angles_deg** -- Angles in degrees, range [0, 360). NaN for zero-magnitude vectors. :rtype: np.ndarray, shape (n_cells,) .. py:function:: bin_angles(angles_deg: numpy.ndarray, magnitudes: numpy.ndarray, n_bins: int = 36) -> Tuple[numpy.ndarray, numpy.ndarray] Discretize angles and accumulate magnitudes per bin (Eq. 4-6). :param angles_deg: Angles in [0, 360). NaN values are ignored. :type angles_deg: np.ndarray, shape (n_cells,) :param magnitudes: Per-cell magnitudes. NaN values are ignored. :type magnitudes: np.ndarray, shape (n_cells,) :param n_bins: Number of angular bins. Default 36 (10° each, as in manuscript). :type n_bins: int :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. .. py:function:: equal_sectors(k: int, n_bins: int = 36) -> List[List[int]] Divide n_bins into k equal contiguous sectors. :param k: Number of fates / sectors. :type k: int :param n_bins: Total number of angular bins. :type n_bins: int :returns: **sectors** -- Each inner list contains the bin indices belonging to that sector. :rtype: list of lists .. py:function:: centroid_sectors(fate_centroids: numpy.ndarray, root_centroid: numpy.ndarray, n_bins: int = 36) -> Tuple[List[List[int]], numpy.ndarray] 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. :param fate_centroids: 2D embedding coordinates of each fate centroid. :type fate_centroids: np.ndarray, shape (k, 2) :param root_centroid: 2D embedding coordinate of the root / progenitor centroid. :type root_centroid: np.ndarray, shape (2,) :param n_bins: Number of angular bins. :type n_bins: int :returns: * **sectors** (*list of k lists of bin indices*) * **fate_angles** (*np.ndarray, shape (k,)*) -- Central angle (degrees) for each fate. .. py:function:: compute_sector_magnitudes(M_bin: numpy.ndarray, sectors: List[List[int]]) -> numpy.ndarray Sum M_bin values within each sector (Eq. 7). :param M_bin: :type M_bin: np.ndarray, shape (n_bins,) :param sectors: :type sectors: list of k lists of bin indices :returns: **M_sector** :rtype: np.ndarray, shape (k,) .. py:function:: compute_unCS(M_sector_i: float, M_sector_j: float) -> float Unnormalized commitment score of fate i relative to fate j (Eq. 8). unCS > 1 => population is more committed to fate i than fate j. :param M_sector_i: Cumulative magnitudes for fates i and j. :type M_sector_i: float :param M_sector_j: Cumulative magnitudes for fates i and j. :type M_sector_j: float :rtype: float (inf if M_sector_j == 0) .. py:function:: compute_nCS(M_sector_i: float, M_sector_j: float, n_cells_i: int, n_cells_j: int) -> float Cell-number-normalized commitment score (Eq. 9). nCS = (M_sector_i / M_sector_j) * (n_cells_j / n_cells_i) :param M_sector_i: :type M_sector_i: float :param M_sector_j: :type M_sector_j: float :param n_cells_i: Number of cells in each population / trajectory arm. :type n_cells_i: int :param n_cells_j: Number of cells in each population / trajectory arm. :type n_cells_j: int :rtype: float .. py:function:: compute_commitment_vector(M_sector: numpy.ndarray) -> numpy.ndarray Normalize sector magnitudes to a probability-like commitment vector. :param M_sector: :type M_sector: np.ndarray, shape (k,) :returns: **p_vec** -- Sums to 1. All-zero input returns uniform distribution. :rtype: np.ndarray, shape (k,) .. py:function:: compute_population_entropy(p_vec: numpy.ndarray) -> float 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 :func:`compute_mean_cell_entropy` as the primary commitment metric. :param p_vec: Normalized commitment vector (sums to 1). :type p_vec: np.ndarray, shape (k,) :rtype: float in [0, 1] .. py:data:: compute_commitment_entropy .. py:function:: compute_mean_cell_entropy(cell_scores: numpy.ndarray) -> float 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 :func:`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 :func:`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] .. py:function:: compute_per_fate_cell_entropy(cell_scores: numpy.ndarray) -> numpy.ndarray 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 :func:`compute_mean_cell_entropy`. :param cell_scores: Per-cell fate-affinity matrix, row-normalized to sum to 1. Typically the output of :func:`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,) .. py:function:: compute_nn_cell_entropy(cell_scores: numpy.ndarray, coords: numpy.ndarray, k_nn: int) -> numpy.ndarray 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 :func:`scCS.plot.plot_nn_entropy_elbow` to choose k_nn. :param cell_scores: Per-cell fate-affinity matrix from :func:`compute_cell_scores`. :type cell_scores: np.ndarray, shape (n_cells, k) :param coords: 2D scCS embedding coordinates (``adata_sub.obsm['X_sccs']``). :type coords: np.ndarray, shape (n_cells, 2) :param k_nn: Number of nearest neighbors to average over (excluding self). Self is always included, so the effective window is k_nn + 1 cells. :type k_nn: int :returns: **nn_entropy** -- Normalized Shannon entropy of the NN-smoothed fate scores per cell, in [0, 1]. :rtype: np.ndarray, shape (n_cells,) .. py:function:: compute_pairwise_cs_matrix(M_sector: numpy.ndarray, n_cells_per_fate: Optional[numpy.ndarray] = None, normalized: bool = True) -> numpy.ndarray Compute full k x k pairwise commitment score matrix. Entry [i, j] = CS(i relative to j). Diagonal is 1.0. :param M_sector: :type M_sector: np.ndarray, shape (k,) :param n_cells_per_fate: If provided and normalized=True, computes nCS; else unCS. :type n_cells_per_fate: np.ndarray, shape (k,), optional :param normalized: :type normalized: bool :returns: **cs_matrix** :rtype: np.ndarray, shape (k, k) .. py:function:: compute_cell_scores(vx: numpy.ndarray, vy: numpy.ndarray, fate_centroids: numpy.ndarray, root_centroid: numpy.ndarray, mag_weight: bool = True, mag_threshold_pct: float = 5.0) -> numpy.ndarray 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). :param vx: :type vx: np.ndarray, shape (n_cells,) :param vy: :type vy: np.ndarray, shape (n_cells,) :param fate_centroids: :type fate_centroids: np.ndarray, shape (k, 2) :param root_centroid: :type root_centroid: np.ndarray, shape (2,) :param mag_weight: 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). :type mag_weight: bool :param mag_threshold_pct: 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. :type mag_threshold_pct: float :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). :rtype: np.ndarray, shape (n_cells, k) .. py:function:: bootstrap_cs(vx: numpy.ndarray, vy: numpy.ndarray, sectors: List[List[int]], n_cells_per_fate: numpy.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: Optional[Sequence] = None) -> Dict 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. :param vx: Velocity components in scCS space. :type vx: np.ndarray, shape (n_cells,) :param vy: Velocity components in scCS space. :type vy: np.ndarray, shape (n_cells,) :param sectors: Sector definition from centroid_sectors() or equal_sectors(). :type sectors: list of k lists of bin indices :param n_cells_per_fate: Number of cells per fate arm (used for nCS normalization). :type n_cells_per_fate: np.ndarray, shape (k,) :param n_bins: Number of angular bins. :type n_bins: int :param n_bootstrap: Number of bootstrap replicates. Default 500. :type n_bootstrap: int :param ci: Confidence interval level. Default 0.95 (95% CI). :type ci: float :param seed: :type seed: int :param normalized: If True, return CI for nCS; if False, for unCS. :type normalized: bool :param stratified: 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). :type stratified: bool :param fate_cell_indices: 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``. :type fate_cell_indices: sequence of array-like, optional :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 :rtype: dict with keys .. py:class:: CommitmentScoreResult Container for all commitment score outputs. .. attribute:: fate_names :type: list of str .. attribute:: M_bin Cumulative magnitude per angular bin. :type: np.ndarray, shape (n_bins,) .. attribute:: bin_edges :type: np.ndarray, shape (n_bins + 1,) .. attribute:: sectors :type: list of k lists of bin indices .. attribute:: M_sector Cumulative magnitude per fate sector. :type: np.ndarray, shape (k,) .. attribute:: n_cells_per_fate :type: np.ndarray, shape (k,) .. attribute:: commitment_vector Normalized (sums to 1). :type: np.ndarray, shape (k,) .. attribute:: population_entropy Normalized Shannon entropy of the aggregate commitment vector in [0, 1]. Single scalar. See :func:`compute_population_entropy`. :type: float .. attribute:: mean_cell_entropy Mean normalized per-cell Shannon entropy in [0, 1]. See :func:`compute_mean_cell_entropy`. NaN when cell_level=False. :type: float .. attribute:: 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 :func:`compute_per_fate_cell_entropy`. All-NaN array when cell_level=False. :type: np.ndarray, shape (k,) .. attribute:: pairwise_unCS :type: np.ndarray, shape (k, k) .. attribute:: pairwise_nCS :type: np.ndarray, shape (k, k) .. attribute:: cell_scores :type: np.ndarray, shape (n_cells, k), optional .. attribute:: fate_angles Angle (degrees) of each fate axis in the radial embedding. :type: np.ndarray, shape (k,), optional .. attribute:: 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 .. attribute:: nn_k The k_nn value used to compute nn_cell_entropy. :type: int, optional .. attribute:: dominant_fate Fate with highest M_sector. :type: str .. py:attribute:: fate_names :type: List[str] .. py:attribute:: M_bin :type: numpy.ndarray .. py:attribute:: bin_edges :type: numpy.ndarray .. py:attribute:: sectors :type: List[List[int]] .. py:attribute:: M_sector :type: numpy.ndarray .. py:attribute:: n_cells_per_fate :type: numpy.ndarray .. py:attribute:: commitment_vector :type: numpy.ndarray .. py:attribute:: population_entropy :type: float .. py:attribute:: mean_cell_entropy :type: float .. py:attribute:: per_fate_entropy :type: numpy.ndarray .. py:attribute:: pairwise_unCS :type: numpy.ndarray .. py:attribute:: pairwise_nCS :type: numpy.ndarray .. py:attribute:: cell_scores :type: Optional[numpy.ndarray] :value: None .. py:attribute:: fate_angles :type: Optional[numpy.ndarray] :value: None .. py:attribute:: cell_obs_names :type: Optional[numpy.ndarray] :value: None .. py:attribute:: nn_cell_entropy :type: Optional[numpy.ndarray] :value: None .. py:attribute:: nn_k :type: Optional[int] :value: None .. py:attribute:: bootstrap_ci :type: Optional[Dict[str, Any]] :value: None .. py:property:: commitment_entropy :type: float Alias for ``population_entropy`` (deprecated, use ``mean_cell_entropy``). .. py:property:: k :type: int .. py:property:: dominant_fate :type: str .. py:method:: to_dataframe() -> pandas.DataFrame Summary DataFrame with one row per fate. .. py:method:: pairwise_to_dataframe(normalized: bool = True) -> pandas.DataFrame Pairwise CS matrix as a labeled DataFrame. .. py:method:: summary() -> str