Source code for pystatsbio.doseresponse._common

"""Shared result types for dose-response modeling."""

from __future__ import annotations

from dataclasses import dataclass

import numpy as np
from numpy.typing import NDArray


[docs] @dataclass(frozen=True) class CurveParams: """Parameters of a fitted dose-response curve. For 4PL: bottom + (top - bottom) / (1 + (ec50/x)^hill) """ bottom: float top: float ec50: float hill: float asymmetry: float | None = None # 5PL only hormesis: float | None = None # BC.5 only model: str = "LL.4"
[docs] def predict(self, dose: NDArray[np.floating]) -> NDArray[np.floating]: """Predict response at given dose levels.""" from pystatsbio.doseresponse._models import _MODEL_MAP func, param_names = _MODEL_MAP[self.model] kwargs = {name: getattr(self, name) for name in param_names} return func(dose, **kwargs)
[docs] def to_array(self) -> NDArray[np.floating]: """Return parameter vector in model order (for Jacobian indexing).""" from pystatsbio.doseresponse._models import _MODEL_MAP _, param_names = _MODEL_MAP[self.model] return np.array([getattr(self, name) for name in param_names], dtype=np.float64)
[docs] @staticmethod def from_array(params: NDArray[np.floating], model: str) -> CurveParams: """Construct from parameter vector and model name.""" from pystatsbio.doseresponse._models import _MODEL_MAP _, param_names = _MODEL_MAP[model] d = dict(zip(param_names, params)) return CurveParams( bottom=d["bottom"], top=d["top"], ec50=d["ec50"], hill=d["hill"], asymmetry=d.get("asymmetry"), hormesis=d.get("hormesis"), model=model, )
[docs] @dataclass(frozen=True) class DoseResponseResult: """Result of fitting a single dose-response curve.""" params: CurveParams se: NDArray[np.floating] # standard errors of parameters residuals: NDArray[np.floating] rss: float aic: float bic: float converged: bool n_iter: int model: str # e.g., "LL.4", "LL.5", "W1.4" dose: NDArray[np.floating] response: NDArray[np.floating] n_obs: int jac: NDArray[np.floating] # Jacobian at solution (n_obs, n_params)
[docs] def predict(self, dose: NDArray[np.floating] | None = None) -> NDArray[np.floating]: """Predict response. If *dose* is ``None``, use the fitted dose.""" if dose is None: dose = self.dose return self.params.predict(dose)
[docs] def summary(self) -> str: """Human-readable summary, similar to R drc::summary().""" from pystatsbio.doseresponse._models import _MODEL_MAP _, param_names = _MODEL_MAP[self.model] lines = [ f"Dose-response model: {self.model}", "", "Parameter estimates:", ] p_arr = self.params.to_array() for i, name in enumerate(param_names): val = p_arr[i] se_val = self.se[i] if i < len(self.se) else float("nan") t_val = val / se_val if se_val > 0 and not np.isnan(se_val) else float("nan") lines.append(f" {name:>12s} = {val:>12.6f} (SE = {se_val:.6f}, t = {t_val:.3f})") lines.append("") lines.append(f" RSS = {self.rss:.6f}") lines.append(f" AIC = {self.aic:.2f}") lines.append(f" BIC = {self.bic:.2f}") lines.append(f" n = {self.n_obs}") lines.append(f" Converged: {self.converged}") return "\n".join(lines)
[docs] @dataclass(frozen=True) class BatchDoseResponseResult: """Result of batch-fitting dose-response curves (HTS). Each array has length n_compounds. """ ec50: NDArray[np.floating] hill: NDArray[np.floating] top: NDArray[np.floating] bottom: NDArray[np.floating] converged: NDArray[np.bool_] rss: NDArray[np.floating] n_compounds: int