Source code for mmm_framework.transforms.saturation

"""Saturation curve transformations for marketing response modeling.

Saturation curves model diminishing returns from marketing activities.
As spend increases, the incremental effect decreases, eventually
reaching a saturation point where additional spend has minimal impact.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

if TYPE_CHECKING:
    from numpy.typing import NDArray


[docs] def logistic_saturation(x: NDArray[np.floating], lam: float) -> NDArray[np.floating]: """ Apply logistic saturation transformation. Implements the transformation: f(x) = 1 - exp(-lam * x) for x >= 0, with negative values clipped to 0. This creates an S-shaped response curve that: - Starts at 0 when x=0 - Increases rapidly for small x - Asymptotically approaches 1 as x -> infinity Parameters ---------- x : NDArray[np.floating] Input values (e.g., normalized media spend). Negative values are clipped to 0. lam : float Saturation rate parameter. Higher values cause faster saturation. lam > 0 is required for valid behavior. Returns ------- NDArray[np.floating] Saturated values in the range [0, 1). Examples -------- >>> import numpy as np >>> from mmm_framework.transforms import logistic_saturation >>> >>> x = np.array([0.0, 0.5, 1.0, 2.0, 5.0, 10.0]) >>> saturated = logistic_saturation(x, lam=1.0) >>> print(saturated.round(3)) [0. 0.393 0.632 0.865 0.993 1. ] Notes ----- This is sometimes called "exponential saturation" in the literature. The half-saturation point (where f(x) = 0.5) occurs at x = ln(2)/lam. For modeling purposes, the input x is typically normalized (e.g., by dividing by max spend) so that lam can be interpreted consistently across channels. See Also -------- Hill saturation is another common choice, implemented in the PyMC model via pm.math operations. """ return 1.0 - np.exp(-lam * np.clip(x, 0, None))
[docs] def root_saturation(x: NDArray[np.floating], exponent: float) -> NDArray[np.floating]: """Apply root / power saturation transformation. Implements the transformation: f(x) = x ** exponent for x >= 0, with negative values clipped to 0. With ``0 < exponent < 1`` this is the classic concave power-response curve — diminishing returns that fall off as a fixed power of (adstocked, normalized) spend. ``exponent = 1`` is linear; ``exponent > 1`` is convex (increasing returns, not saturation). Parameters ---------- x : NDArray[np.floating] Input values (e.g., normalized media spend). Negative values are clipped to 0. exponent : float The power ``k``. Use ``0 < k < 1`` for a saturating (concave) curve. Returns ------- NDArray[np.floating] Saturated values ``x ** exponent``. Examples -------- >>> import numpy as np >>> from mmm_framework.transforms import root_saturation >>> >>> x = np.array([0.0, 0.25, 0.5, 1.0]) >>> root_saturation(x, exponent=0.5).round(3) array([0. , 0.5 , 0.707, 1. ]) Notes ----- Unlike the logistic/Hill forms this curve has no finite asymptote; it is typically applied to spend normalized into ``[0, 1]`` so ``f`` stays in ``[0, 1]``. The marginal ``f'(x) = k * x**(k-1)`` is unbounded at ``x = 0`` for ``k < 1``, which the model's in-graph form guards against by clamping ``x`` away from 0. See Also -------- logistic_saturation : exponential-CDF saturation ``1 - exp(-lam * x)``. """ return np.clip(x, 0, None) ** exponent