Extend the model with effects

Use the existing effect interfaces when ordinary controls and seasonal settings cannot express the required linear-predictor contribution. Custom structure requires a statistical rationale as well as a graph that compiles.

model.add_mu_effect(effect) registers a MuEffect before graph construction. An effect defines data creation, its contribution and prediction-time data updates. Built-in interfaces include ControlMuEffect, MediaMuEffect, FourierEffect, LinearTrendEffect and EventAdditiveEffect. model.add_events provides a separate event-registration interface. Check each constructor rather than borrowing an AMMM3 effect signature.

In YAML, effects holds class specifications and extra_vars selects auxiliary columns to retain as xarray variables. Without extra_vars, an unused DataFrame column is not automatically retained in the canonical training dataset.

Counterfactual completeness

A media-dependent effect can carry part of the response to changing channel_data. For incrementality, such an effect must declare an IncrementalitySpec; otherwise the operation raises rather than silently omitting the effect. Effects unrelated to media remain in the baseline. Channel mixing and temporal reach can require joint rather than independent channel contrasts. A declaration must describe the actual dependency, not merely suppress validation.

The default optimiser response may also omit media response routed through a custom effect. Select and verify the complete response variable explicitly. Effects must implement their own serialisation and any supplementary idata groups needed to reconstruct them. Test save/load and prediction with changed inputs, not just in-sample graph construction.

FE and CRE reject custom mean effects. For a fully custom PyMC model, reuse transformations where appropriate, but do not assume MMM’s automatic scaling, persistence, plotting or calibration contracts apply to that new graph.

Retain and serialise an auxiliary control effect

Use a built-in ControlMuEffect when a named auxiliary variable needs its own coefficient and persisted effect definition. This standalone example retains promotion_z explicitly, builds the graph and checks effect serialisation; it does not fit or claim the promotion coefficient is causal.

import numpy as np
import pandas as pd
from pymc_extras.prior import Prior
from ammm.mmm import MMM, GeometricAdstock, LogisticSaturation, ControlMuEffect
from ammm.mmm.data_conversion import to_mmm_dataset

frame = pd.DataFrame({
    "date": pd.date_range("2025-01-06", periods=12, freq="W-MON"),
    "tv": np.linspace(10, 40, 12),
    "promotion_z": [-1.0, 1.0] * 6,
    "revenue": np.linspace(50, 70, 12),
})
training = to_mmm_dataset(
    frame, date_column="date", channel_columns=["tv"],
    target_column="revenue", extra_vars=["promotion_z"],
)
effect = ControlMuEffect(
    data_vars=["promotion_z"], prefix="promotion",
    prior=Prior("Normal", mu=0, sigma=0.2),
)
rebuilt_effect = ControlMuEffect.from_dict(effect.to_dict())
effect_model = MMM(
    date_column="date", target_column="revenue", channel_columns=["tv"],
    adstock=GeometricAdstock(l_max=1), saturation=LogisticSaturation(),
)
effect_model.add_mu_effect(rebuilt_effect)
effect_model.build_model(training)
assert "promotion_effect_contribution" in effect_model.model.named_vars

The effect contributes in scaled target units and updates its named data at prediction time; future canonical datasets must retain promotion_z on the required date/panel coordinates. Its prior and data-variable declaration are serialised by to_dict/from_dict (src/ammm/mmm/additive_effect.py:433, src/ammm/mmm/additive_effect.py:579). For a fitted model, use the standard save/load route and compare predictions on changed inputs after loading; the graph-only check above does not exercise fitted-state persistence (src/ammm/mmm/mmm.py:925).

In a full YAML configuration whose input has the same columns, use:

extra_vars: [promotion_z]
effects:
  - class: ammm.mmm.ControlMuEffect
    kwargs:
      data_vars: [promotion_z]
      prefix: promotion
      prior:
        class: pymc_extras.prior.Prior
        args: [Normal]
        kwargs: {mu: 0, sigma: 0.2}

The builder converts those extra columns to xarray variables and attaches the effect before graph construction (src/ammm/mmm/builders/yaml.py:172). Do not also include promotion_z in control_columns unless you deliberately want two contributions for the same input and can identify them separately.

Implementation reference at 7cb7f20: src/ammm/mmm/additive_effect.py:341, src/ammm/mmm/spend_reach.py:135.