Source code for mmm_framework.mmm_extensions.components.priors
"""
Prior factory functions for MMM Extensions.
These functions create PyMC prior distributions for various
model parameters with appropriate configurations.
"""
from __future__ import annotations
import pymc as pm
import pytensor.tensor as pt
[docs]
def create_adstock_prior(
name: str,
prior_type: str = "beta",
**kwargs,
) -> pt.TensorVariable:
"""
Create adstock decay prior.
Args:
name: Parameter name.
prior_type: Prior type, either "beta" or "uniform".
**kwargs: Additional prior parameters.
Returns:
Prior random variable.
"""
if prior_type == "beta":
alpha = kwargs.get("alpha", 2)
beta = kwargs.get("beta", 2)
return pm.Beta(name, alpha=alpha, beta=beta)
elif prior_type == "uniform":
return pm.Uniform(name, lower=0, upper=1)
else:
raise ValueError(f"Unknown prior type: {prior_type}")
[docs]
def create_saturation_prior(
name: str,
saturation_type: str = "logistic",
**kwargs,
) -> dict[str, pt.TensorVariable]:
"""
Create saturation parameter priors.
Args:
name: Base parameter name.
saturation_type: Saturation type, either "logistic" or "hill".
**kwargs: Prior hyperparameters.
Returns:
Dictionary of prior random variables.
"""
if saturation_type == "logistic":
lam_alpha = kwargs.get("lam_alpha", 3)
lam_beta = kwargs.get("lam_beta", 1)
return {"lam": pm.Gamma(f"{name}_lam", alpha=lam_alpha, beta=lam_beta)}
elif saturation_type == "hill":
# Data-anchored half-saturation point (equifinality guardrail, opt-in):
# when explicit ``kappa_lower``/``kappa_upper`` bounds are supplied
# (e.g. from SaturationConfig.compute_kappa_bounds_from_data), bound
# kappa to the observed-spend range so the Hill "elbow" cannot drift
# outside the data's support. Otherwise keep the weakly-informative
# Beta(2, 2) so existing fits are unchanged.
kappa_lower = kwargs.get("kappa_lower")
kappa_upper = kwargs.get("kappa_upper")
if kappa_lower is not None and kappa_upper is not None:
if not (kappa_upper > kappa_lower):
raise ValueError(
"kappa_upper must exceed kappa_lower for data-anchored kappa; "
f"got lower={kappa_lower}, upper={kappa_upper}."
)
kappa = pm.Uniform(
f"{name}_kappa", lower=float(kappa_lower), upper=float(kappa_upper)
)
else:
kappa = pm.Beta(f"{name}_kappa", alpha=2, beta=2)
return {
"kappa": kappa,
"slope": pm.Gamma(f"{name}_slope", alpha=3, beta=1),
}
else:
raise ValueError(f"Unknown saturation type: {saturation_type}")
[docs]
def create_effect_prior(
name: str,
constrained: str = "none",
mu: float = 0.0,
sigma: float = 1.0,
dims: str | tuple | None = None,
) -> pt.TensorVariable:
"""
Create effect coefficient prior.
Parameters
----------
name : str
Parameter name
constrained : str
"none", "positive", "negative"
mu : float
Prior mean (for unconstrained)
sigma : float
Prior scale
dims : str | tuple | None
PyMC dimensions
Returns
-------
TensorVariable
Prior random variable
"""
kwargs = {"sigma": sigma}
if dims is not None:
kwargs["dims"] = dims
if constrained == "positive":
return pm.HalfNormal(name, **kwargs)
elif constrained == "negative":
return -pm.HalfNormal(name, **kwargs)
else:
kwargs["mu"] = mu
return pm.Normal(name, **kwargs)
__all__ = [
"create_adstock_prior",
"create_saturation_prior",
"create_effect_prior",
]