Source code for mmm_framework.config.reach_frequency

"""Reach & frequency channel modeling (#141).

A media channel can be declared *reach/frequency-measured*: its column is
**reach** (distinct people reached per period) and its effect is
``reach · g(frequency)`` where ``g`` is a **frequency-saturation** curve —
diminishing returns to added exposures (the "3+ frequency wearout"). This lets
the model answer the core planning question *"buy more reach or more
frequency?"*, which raw impressions (reach × frequency collapsed to a volume)
cannot.
"""

from __future__ import annotations

from enum import Enum

from pydantic import BaseModel, Field


[docs] class FrequencyResponse(str, Enum): """Shape of the frequency-saturation curve ``g(f)`` (``f`` = avg frequency, normalized by its mean so the shape prior is scale-free).""" #: ``g(f) = 1 - exp(-k f)`` — diminishing returns from the first exposure, #: monotone, asymptotes to 1. The safe planner default. EXPONENTIAL = "exponential" #: ``g(f) = f^s / (f^s + h^s)`` — S-shaped, models a *minimum effective #: frequency* threshold (low frequency is nearly wasted, then it kicks in). HILL = "hill"
[docs] class ReachFrequencyConfig(BaseModel): """Declare a channel as reach modulated by a frequency-saturation curve. The named ``channel``'s media column is treated as **reach**. The ``frequency_column`` is a per-period average-frequency series supplied in the control block — it is pulled out (not double-counted as a linear control) and used only to build the frequency gain. The channel's effect becomes ``beta · sat(adstock(reach · g(frequency)))`` — the standard media pipeline with the input re-mixed to *effective reach*. Off by default (``ModelConfig.reach_frequency = []``): a channel is an ordinary volume channel, byte-identical to today (R0.1/R0.2). """ #: Media channel whose column is reach (must be a modeled channel). channel: str #: Per-period average-frequency series (must be a control column). frequency_column: str #: Frequency-saturation shape. response: FrequencyResponse = FrequencyResponse.EXPONENTIAL #: Prior scale on the shape parameter (``k`` for exponential; the slope / #: half-saturation priors for Hill). Larger ⇒ faster frequency saturation #: a priori. frequency_prior_scale: float = Field(default=1.0, gt=0) model_config = {"extra": "forbid"}