Source code for spatialvi.external.ppi._model

"""Prediction-powered inference utilities.

The :class:`PPIInference` class provides static methods for computing
prediction-powered confidence intervals and point estimates using the
``ppi_py`` package. Prediction-powered inference allows researchers to
leverage machine learning predictions to reduce the number of labelled
examples required for statistical inference while maintaining rigorous
error control.

This is particularly useful in spatial transcriptomics when you have:
- A small number of cells with gold-standard labels
- A large number of cells with model predictions
- Want to make statistically valid inferences about population parameters

References
----------
Angelopoulos et al. (2023) "Prediction-Powered Inference"
https://arxiv.org/abs/2301.09633
"""

from __future__ import annotations

import logging
from collections.abc import Iterable

import numpy as np

logger = logging.getLogger(__name__)

try:
    from ppi_py import (
        ppi_mean_ci,
        ppi_mean_pointestimate,
        ppi_ols_ci,
        ppi_ols_pointestimate,
    )
except ImportError:
    ppi_mean_ci = None
    ppi_mean_pointestimate = None
    ppi_ols_ci = None
    ppi_ols_pointestimate = None


[docs] class PPIInference: """Static methods for prediction-powered inference. This class does not derive from the spatial model base because PPI operates on numerical arrays rather than AnnData objects. It exposes simple wrappers around commonly used estimands in the ``ppi_py`` package, raising informative errors if the package is not installed. Examples -------- >>> import numpy as np >>> from spatialvi.external import PPIInference >>> # Small labeled sample >>> y_labeled = np.array([1.2, 0.8, 1.1, 0.9, 1.0]) >>> yhat_labeled = np.array([1.1, 0.9, 1.0, 0.85, 1.05]) >>> # Large unlabeled sample with predictions >>> yhat_unlabeled = np.random.normal(1.0, 0.2, 1000) >>> # Compute confidence interval for mean >>> ci = PPIInference.mean_ci(y_labeled, yhat_labeled, yhat_unlabeled) >>> print(f"95% CI for mean: [{ci[0]:.3f}, {ci[1]:.3f}]") """ @staticmethod def _require_ppi() -> None: if ppi_mean_ci is None: raise ImportError("The 'ppi_py' package is not installed. Install it with ``pip install ppi-python``.")
[docs] @staticmethod def mean_ci( y: Iterable[float], yhat: Iterable[float], yhat_unlabeled: Iterable[float], alpha: float = 0.1, ) -> tuple[float, float]: """Compute a prediction-powered confidence interval for the mean. Parameters ---------- y True labels for the labelled examples. yhat Predictions for the labelled examples. yhat_unlabeled Predictions for the unlabelled examples. alpha Error rate (the confidence level is 1-alpha). Returns ------- ci_low, ci_high Lower and upper bounds of the confidence interval. """ PPIInference._require_ppi() y_arr = np.asarray(y) yhat_arr = np.asarray(yhat) yhat_unlabeled_arr = np.asarray(yhat_unlabeled) return ppi_mean_ci(y_arr, yhat_arr, yhat_unlabeled_arr, alpha=alpha)
[docs] @staticmethod def mean_pointestimate( y: Iterable[float], yhat: Iterable[float], yhat_unlabeled: Iterable[float], ) -> float: """Compute a prediction-powered point estimate for the mean. Parameters ---------- y True labels for the labelled examples. yhat Predictions for the labelled examples. yhat_unlabeled Predictions for the unlabelled examples. Returns ------- estimate The estimated mean of the outcome variable. """ PPIInference._require_ppi() y_arr = np.asarray(y) yhat_arr = np.asarray(yhat) yhat_unlabeled_arr = np.asarray(yhat_unlabeled) return ppi_mean_pointestimate(y_arr, yhat_arr, yhat_unlabeled_arr)
[docs] @staticmethod def ols_ci( X: Iterable[Iterable[float]], y: Iterable[float], yhat: Iterable[float], X_unlabeled: Iterable[Iterable[float]], yhat_unlabeled: Iterable[float], alpha: float = 0.1, ) -> tuple[np.ndarray, np.ndarray]: """Compute prediction-powered CI for OLS regression coefficients. Parameters ---------- X Labelled design matrix (n_l x p). y Labelled response vector. yhat Predictions for the labelled examples. X_unlabeled Unlabelled design matrix (n_u x p). yhat_unlabeled Predictions on the unlabelled data. alpha Error rate (confidence level is 1-alpha). Returns ------- ci_low, ci_high Lower and upper bounds for each regression coefficient. """ PPIInference._require_ppi() X_arr = np.asarray(X) y_arr = np.asarray(y) yhat_arr = np.asarray(yhat) X_unlabeled_arr = np.asarray(X_unlabeled) yhat_unlabeled_arr = np.asarray(yhat_unlabeled) return ppi_ols_ci(X_arr, y_arr, yhat_arr, X_unlabeled_arr, yhat_unlabeled_arr, alpha=alpha)
[docs] @staticmethod def ols_pointestimate( X: Iterable[Iterable[float]], y: Iterable[float], yhat: Iterable[float], X_unlabeled: Iterable[Iterable[float]], yhat_unlabeled: Iterable[float], ) -> np.ndarray: """Compute prediction-powered point estimates for OLS coefficients. Parameters ---------- X Labelled design matrix (n_l x p). y Labelled response vector. yhat Predictions for the labelled examples. X_unlabeled Unlabelled design matrix (n_u x p). yhat_unlabeled Predictions on the unlabelled data. Returns ------- coefficients Point estimates for each regression coefficient. """ PPIInference._require_ppi() X_arr = np.asarray(X) y_arr = np.asarray(y) yhat_arr = np.asarray(yhat) X_unlabeled_arr = np.asarray(X_unlabeled) yhat_unlabeled_arr = np.asarray(yhat_unlabeled) return ppi_ols_pointestimate(X_arr, y_arr, yhat_arr, X_unlabeled_arr, yhat_unlabeled_arr)
[docs] @staticmethod def classical_mean_ci( y: Iterable[float], alpha: float = 0.1, ) -> tuple[float, float]: """Compute classical confidence interval for the mean (no predictions). This is a utility method for comparison with PPI intervals. Parameters ---------- y Sample values. alpha Error rate (confidence level is 1-alpha). Returns ------- ci_low, ci_high Lower and upper bounds of the confidence interval. """ from scipy import stats y_arr = np.asarray(y) n = len(y_arr) mean = np.mean(y_arr) se = np.std(y_arr, ddof=1) / np.sqrt(n) t_val = stats.t.ppf(1 - alpha / 2, df=n - 1) return (mean - t_val * se, mean + t_val * se)