scPTR / src /scptr /tools /_beta.py
bryan7264's picture
Add files using upload-large-folder tool
925ee3b verified
Raw
History Blame Contribute Delete
3.46 kB
"""Beta (splicing rate) estimation from phase portrait."""
from __future__ import annotations
import numpy as np
import pandas as pd
from anndata import AnnData
from .._constants import (
SMOOTHED_UNSPLICED,
SMOOTHED_SPLICED,
BETA,
DEFAULT_BETA_QUANTILE,
)
from .._utils import get_layer, require_layers, require_obs, log_params
def estimate_beta(
adata: AnnData,
method: str = "quantile",
quantile: float = DEFAULT_BETA_QUANTILE,
groupby: str | None = None,
) -> None:
"""Estimate per-gene splicing rate beta from the u/s phase portrait.
Default fast mode uses the upper quantile of the u/s ratio per gene
as the slope estimate.
Parameters
----------
adata
Annotated data matrix with smoothed layers ``Mu`` and ``Ms``.
method
``'quantile'`` (default) — fast quantile-based estimation.
quantile
Quantile of u/s ratio to use (default 0.95).
groupby
Optional obs column for per-group estimation. When set, estimates
beta separately for each group and stores per-group values in
``adata.varm['beta_groups']``. The consensus beta (median across
groups) is stored in ``adata.var['beta']``.
"""
require_layers(adata, SMOOTHED_UNSPLICED, SMOOTHED_SPLICED)
u = get_layer(adata, SMOOTHED_UNSPLICED)
s = get_layer(adata, SMOOTHED_SPLICED)
if method != "quantile":
raise ValueError(f"Unknown method: {method!r}. Use 'quantile'.")
if groupby is not None:
require_obs(adata, groupby)
groups = adata.obs[groupby].astype(str)
unique_groups = sorted(groups.unique())
beta_groups = pd.DataFrame(
index=adata.var_names, columns=unique_groups, dtype=np.float32,
)
for grp in unique_groups:
mask = (groups == grp).values
beta_g = _quantile_beta(u[mask], s[mask], quantile)
beta_groups[grp] = beta_g.astype(np.float32)
adata.varm["beta_groups"] = beta_groups
# Consensus: median across groups
beta = np.nanmedian(beta_groups.values, axis=1)
else:
beta = _quantile_beta(u, s, quantile)
# Global clip: cap extreme beta values at the 99th percentile of
# positive betas. Genes with very sparse unspliced counts can produce
# wildly large u/s quantiles that propagate into gamma.
positive_beta = beta[beta > 0]
if len(positive_beta) > 0:
beta_cap = np.percentile(positive_beta, 99)
beta = np.clip(beta, 0, beta_cap)
adata.var[BETA] = beta.astype(np.float32)
log_params(adata, "estimate_beta", {
"method": method,
"quantile": quantile,
"groupby": groupby,
})
def _quantile_beta(
u: np.ndarray, s: np.ndarray, quantile: float
) -> np.ndarray:
"""Estimate beta as the upper quantile of u/s ratio per gene.
Parameters
----------
u : (n_obs, n_vars)
s : (n_obs, n_vars)
quantile : float
Returns
-------
beta : (n_vars,)
"""
# Avoid division by zero
s_safe = np.clip(s, 1e-6, None)
ratio = u / s_safe
# Use only cells with sufficient expression
mask = (u > 0) & (s > 1e-6)
n_vars = u.shape[1]
beta = np.zeros(n_vars, dtype=np.float64)
for g in range(n_vars):
valid = ratio[mask[:, g], g]
if len(valid) > 0:
beta[g] = np.quantile(valid, quantile)
else:
beta[g] = 0.0
return beta