"""Evaluation metrics for synthetic recovery experiments.""" from __future__ import annotations import numpy as np from scipy import stats def gamma_recovery( gamma_true: np.ndarray, gamma_pred: np.ndarray, per_gene: bool = True, ) -> float | np.ndarray: """Spearman correlation between true and predicted gamma. Parameters ---------- gamma_true, gamma_pred Shape ``(n_cells, n_genes)``. per_gene If True, compute per-gene Spearman r and return the median. If False, flatten and compute a single global r. Returns ------- float or np.ndarray Median per-gene r (if per_gene=True) or global r. """ if per_gene: n_genes = gamma_true.shape[1] rs = np.empty(n_genes, dtype=np.float64) for g in range(n_genes): gt = gamma_true[:, g] gp = gamma_pred[:, g] if gt.std() < 1e-10 or gp.std() < 1e-10: rs[g] = 0.0 else: rs[g] = stats.spearmanr(gt, gp).statistic return float(np.nanmedian(rs)) else: return float(stats.spearmanr(gamma_true.ravel(), gamma_pred.ravel()).statistic) def ci_coverage( gamma_true: np.ndarray, gamma_mean: np.ndarray, gamma_var: np.ndarray, level: float = 0.95, ) -> float: """Fraction of true gamma values within the posterior credible interval. Uses a Gaussian approximation: CI = mean +/- z * std. Parameters ---------- gamma_true Ground-truth gamma, shape ``(n_cells, n_genes)``. gamma_mean Posterior mean, same shape. gamma_var Posterior variance, same shape. level Credible interval level (e.g. 0.95). Returns ------- float Coverage fraction in [0, 1]. """ from scipy.stats import norm z = norm.ppf(0.5 + level / 2) std = np.sqrt(np.clip(gamma_var, 1e-10, None)) lower = gamma_mean - z * std upper = gamma_mean + z * std inside = (gamma_true >= lower) & (gamma_true <= upper) return float(inside.mean()) def latent_recovery( z_true: np.ndarray, z_pred: np.ndarray, ) -> float: """Mean canonical correlation (CCA) between true and predicted latents. Parameters ---------- z_true, z_pred Shape ``(n_cells, d_latent)``. Returns ------- float Mean canonical correlation across components. """ from sklearn.cross_decomposition import CCA d = min(z_true.shape[1], z_pred.shape[1]) cca = CCA(n_components=d, max_iter=500) X_c, Y_c = cca.fit_transform(z_true, z_pred) correlations = np.array( [np.corrcoef(X_c[:, i], Y_c[:, i])[0, 1] for i in range(d)] ) return float(np.mean(np.abs(correlations)))