Source code for vampire.anno.tl._pca

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import numpy as np
    import anndata as ad

import logging

logger = logging.getLogger(__name__)


[docs] def motif_abundance_pca( adata: ad.AnnData, layer: str | None = None, clr_transform: bool = False, n_components: int = 10, ) -> ad.AnnData: """ PCA on motif abundance percentage vectors. Row-normalises the motif abundance matrix to percentages, optionally applies a centered log-ratio (CLR) transform, then performs PCA. Results are stored in ``adata.obs``, ``adata.var``, and ``adata.uns``. Parameters ---------- adata : ad.AnnData Annotated data with motif abundance in ``X`` or ``layers``. layer : str | None, optional Layer key to use instead of ``adata.X``. clr_transform : bool, default=False If ``True``, apply a centered log-ratio transform before PCA. n_components : int, default=10 Number of principal components to compute. Returns ------- ad.AnnData The updated AnnData with PCA results. Notes ----- Stores the following fields (following scanpy conventions): - ``obsm["X_motif_abundance_pca"]`` — PC coordinates (ndarray, n_obs × n_components) - ``varm["motif_abundance_PCs"]`` — motif loadings (ndarray, n_vars × n_components) - ``uns["motif_abundance_pca"]`` — PCA metadata (variance, variance_ratio) Examples -------- >>> import vampire as vp >>> adata = vp.datasets.wdr7_hprc() >>> vp.anno.tl.motif_abundance_pca(adata) """ import numpy as np from sklearn.decomposition import PCA X = adata.X if layer is None else adata.layers[layer] if hasattr(X, "toarray"): X = X.toarray() X = np.asarray(X, dtype=float) # Row-normalise to percentages row_sums = X.sum(axis=1, keepdims=True) X_pct = np.divide(X, row_sums, out=np.zeros_like(X), where=row_sums != 0) # Optional CLR transform if clr_transform: X_pct = np.clip(X_pct, a_min=1e-10, a_max=None) log_x = np.log(X_pct) X_pct = log_x - log_x.mean(axis=1, keepdims=True) if n_components > X_pct.shape[1]: logger.warning( f"Requested n_components={n_components} exceeds number of features ({X_pct.shape[1]}). " f"Reducing n_components to {X_pct.shape[1]}." ) n_components = X_pct.shape[1] # PCA pca = PCA(n_components=n_components) pcs = pca.fit_transform(X_pct) # Store results (scanpy convention) adata.obsm["X_motif_abundance_pca"] = pcs adata.varm["motif_abundance_PCs"] = pca.components_.T adata.uns["motif_abundance_pca"] = { "variance": pca.explained_variance_.tolist(), "variance_ratio": pca.explained_variance_ratio_.tolist(), "n_components": n_components, "clr_transform": clr_transform, } logger.info( f"PCA on motif abundance: {n_components} components. " f"Explained variance: {', '.join(f'{v * 100:.1f}%' for v in pca.explained_variance_ratio_)}" ) return adata