Source code for mmm_framework.estimands.graph
"""In-graph estimand realization (PyTensor) — likelihood-time scalars.
This is the **in-graph** counterpart to :mod:`mmm_framework.estimands.evaluate`:
it assembles a model-implied estimand as a PyTensor expression to be compared
against an experiment measurement inside the PyMC graph. It is the canonical
home of :func:`build_estimand_expr` (subsuming
``mmm_framework.calibration.likelihood.build_estimand_expr``, which now
delegates here).
The two realizations deliberately share only :mod:`spec` and never call into
each other: this one is deterministic, integer-indexed and scale-folded
(``delta = (pert − base) * scale``), whereas the post-hoc engine is numpy,
paired/unpaired-seeded and boolean-masked. ``pytensor`` is imported lazily so
importing the package stays cheap.
The contribution-window inputs (``contrib_window`` / ``contrib_window_pert``) are
produced by the caller — the in-panel masked sum or the off-panel global-curve
evaluation (``BayesianMMM._offpanel_contribution_std``) — so the
``eval_spend`` / ``adstock_state`` branches live with the model that owns the
channel handle; this module is the estimand *algebra* over those windows.
"""
from __future__ import annotations
from typing import Any
# Estimand-kind string values (kept in sync with
# mmm_framework.calibration.likelihood.ExperimentEstimand to avoid an import
# cycle at module load; the enum is a str-enum with these values).
_CONTRIBUTION = "contribution"
_ROAS = "roas"
_MROAS = "mroas"
def _estimand_value(estimand: Any) -> str:
"""Normalize an ``ExperimentEstimand`` (or string) to its kind value."""
return estimand.value if hasattr(estimand, "value") else str(estimand)
[docs]
def build_estimand_expr(
estimand: Any,
*,
contrib_window: Any,
spend_window: float,
scale: float = 1.0,
contrib_window_pert: Any = None,
lift: float | None = None,
) -> Any:
"""Assemble a model-implied estimand from a window's contribution.
``contrib_window`` is the summed per-obs contribution over the experiment
window in *model* units; ``scale`` converts it to the estimand's natural
scale (``y_std`` for a standardized model, ``1.0`` for a raw-``y`` model).
``spend_window`` is the observed window spend (the ROAS / marginal-spend
denominator).
For ``MROAS``, ``contrib_window_pert`` is the contribution under spend scaled
by ``(1 + lift)`` within the window; the result is the incremental KPI per
incremental dollar.
Returns a PyTensor scalar. Bit-identical to the historical
``calibration.likelihood.build_estimand_expr`` (which now delegates here).
"""
kind = _estimand_value(estimand)
contribution = contrib_window * scale
if kind == _CONTRIBUTION:
return contribution
if kind == _ROAS:
return contribution / spend_window
if kind == _MROAS:
if contrib_window_pert is None or lift is None:
raise ValueError("MROAS estimand requires contrib_window_pert and lift")
delta = (contrib_window_pert - contrib_window) * scale
return delta / (lift * spend_window)
raise ValueError(f"Unknown estimand: {estimand!r}")
__all__ = ["build_estimand_expr"]