Source code for pytyche.analysis._calibrate

"""Attach an SBC-fitted calibration artifact to a posterior.

Implementation behind ``posterior.apply_calibration(calibration)`` on the
three posterior result types.  The fitted corrections are query-time
remappings — they never transform samples — so "applying" a calibration
means attaching the artifact to the posterior (``is_calibrated=True``),
not changing any array.  Sample arrays on the returned posterior are the
same objects as the original's.
"""

from __future__ import annotations

import dataclasses
from typing import TypeVar

from pytyche.analysis._contrasts import require_observed
from pytyche.bcf.config import (
    BinaryBCFResult,
    ContinuousBCFResult,
    HurdleBCFResult,
)
from pytyche.calibrate.artifact import Calibration

AnyBCFResultT = TypeVar(
    "AnyBCFResultT", HurdleBCFResult, ContinuousBCFResult, BinaryBCFResult
)


[docs] def apply_calibration( posterior: AnyBCFResultT, calibration: Calibration ) -> AnyBCFResultT: """Return a new posterior of the same type with *calibration* attached. Attach, don't transform: the artifact's corrections are query-time interval remappings, so the returned posterior shares every field of the original by reference (``dataclasses.replace`` with only ``is_calibrated=True`` and ``calibration`` changed) — no array is copied or re-derived, and the original posterior is untouched. The correction currently applies to intervals only: probabilities and expected losses are computed on the raw draws and are identical pre/post calibration; corrected CIs appear where the interval summaries are built (``analyze``). K = 2 experiments only — per-contrast recalibration for K >= 3 is not yet implemented. Args: posterior: One of the three posterior result types, carrying observed data (raises otherwise). calibration: SBC-fitted artifact whose fitted regime (``metric``, ``n_treatments``) must match the posterior's observed data. Returns: A new instance of the same result type with ``is_calibrated=True`` and ``calibration`` attached by identity. Raises: TypeError: When *calibration* is ``None`` — an artifact is required; an uncalibrated posterior is one you never applied a calibration to. (The fit entry points' ``calibration=`` kwarg only delegates here when non-None, so the default fit path never produces this error.) ValueError: When ``posterior.observed`` is ``None`` (no regime to check against), when ``calibration.applies_to(observed)`` is ``False`` (the message names each mismatched dimension), or when a hurdle posterior's ``pooling`` differs from the artifact's (a correction fitted on one pooling mode's miscalibration profile must not silently apply to the other). NotImplementedError: When the (regime-matching) posterior has K >= 3 variants — per-contrast recalibration is not yet shipped. """ if calibration is None: raise TypeError( "apply_calibration requires a Calibration artifact, got None. " "To leave a posterior uncalibrated, don't apply a calibration " "at all." ) observed = require_observed(posterior) if not calibration.applies_to(observed): mismatches = [] if observed.metric != calibration.metric: mismatches.append( f"metric (posterior {observed.metric!r}, " f"artifact {calibration.metric!r})" ) if len(observed.variants) != calibration.n_treatments: mismatches.append( f"n_treatments (posterior K={len(observed.variants)}, " f"artifact K={calibration.n_treatments})" ) raise ValueError( "calibration was fitted on a different regime than this " "posterior's observed data — mismatched " + "; ".join(mismatches) ) if ( isinstance(posterior, HurdleBCFResult) and posterior.pooling != calibration.pooling ): # Part of the regime match: the correction was fitted to one # pooling mode's miscalibration profile. Binary/continuous # posteriors have no pooling concept and skip this check. raise ValueError( f"calibration was fitted on pooling=" f"{calibration.pooling!r} sweeps but this posterior was fit " f"with pooling={posterior.pooling!r} — no silent cross-regime " "reuse." ) if len(observed.variants) >= 3: raise NotImplementedError( "apply_calibration supports K = 2 only: per-contrast R(p) " "recalibration at K >= 3 requires per-contrast SBC machinery " "that is not yet implemented." ) return dataclasses.replace( posterior, is_calibrated=True, calibration=calibration )