additive_effect#

Additive effects for the multidimensional Marketing Mix Model.

Example of a custom additive effect#

  1. Custom negative-effect component (added as a MuEffect)

import numpy as np
import pandas as pd
import pymc as pm
import pymc.dims as pmd
from pymc_extras.prior import create_dim_handler

# A simple custom effect that penalizes certain dates/segments with a
# negative-only coefficient. This is not a "control" in the MMM sense, so
# give it a different name/prefix to avoid clashing with built-in controls.
class PenaltyEffect:
    '''Example MuEffect that applies a negative coefficient to a user-specified pattern.
    '''

    def __init__(self, name: str, penalty_provider):
        self.name = name
        self.penalty_provider = penalty_provider

    def create_data(self, mmm):
        # Produce penalty values aligned with model dates (and optional extra dims)
        dates = safe_to_datetime(mmm.model.coords["date"], "date")
        penalty = self.penalty_provider(dates)
        pmd.Data(f"{self.name}_penalty", penalty, dims=("date", *mmm.dims))

    def create_effect(self, mmm):
        model = mmm.model
        penalty = model[f"{self.name}_penalty"]  # dims: (date, *mmm.dims)

        # Negative-only coefficient per extra dims, broadcast over date
        coef = pmd.TruncatedNormal(f"{self.name}_coef", mu=-0.5, sigma=-0.05, lower=-1.0, upper=0.0, dims=mmm.dims)

        dim_handler = create_dim_handler(("date", *mmm.dims))
        effect = pmd.Deterministic(
            f"{self.name}_effect_contribution",
            dim_handler(coef, mmm.dims) * penalty,
            dims=("date", *mmm.dims),
        )
        return effect  # Must have dims ("date", *mmm.dims)

    def set_data(self, mmm, model, X):
        # Update to future dates during posterior predictive
        dates = safe_to_datetime(model.coords["date"], "date")
        penalty = self.penalty_provider(dates)
        pm.set_data({f"{self.name}_penalty": penalty}, model=model)

Usage
-----
# Example weekend penalty (Sat/Sun = 1, else 0), applied per geo if present
weekend_penalty = PenaltyEffect(
    name="brand_penalty",
    penalty_provider=lambda dates: pd.Series(dates)
    .dt.dayofweek.isin([5, 6])
    .astype(float)
    .to_numpy()[:, None]  # if mmm.dims == ("geo",), broadcast over geo
)

# Build your MMM as usual (with channels, etc.), then add the effect before build/fit:
# mmm = MMM(...)
# mmm.add_mu_effect(weekend_penalty)
# mmm.build_model(X, y)
# mmm.fit(X, y, ...)
# At prediction time, the effect updates itself via set_data.

How it works#

  • Mu effects follow a simple protocol: create_data(mmm), create_effect(mmm), and set_data(mmm, model, X).

  • During MMM.build_model(...), each effect’s create_data is called first to introduce any needed pmd.Data. Then create_effect must return a tensor with dims ("date", *mmm.dims) that is added additively to the model mean.

  • During posterior predictive, set_data is called with the cloned PyMC model and the new coordinates; update any pmd.Data you created using pm.set_data.

Tips for custom components#

  • Use unique variable prefixes to avoid name clashes with built-in pieces like controls. Do not call your component “control”; choose a distinct name/prefix.

  • Follow the patterns used by the provided effects in this module (e.g., FourierEffect, LinearTrendEffect, EventAdditiveEffect):

    • In create_data, derive and register any required inputs into the model.

    • In create_effect, construct PyTensor expressions and return a contribution with dims ("date", *mmm.dims). If you need broadcasting, use pymc_extras.prior.create_dim_handler as shown above.

    • In set_data, update the data variables when dates/dims change.

Built-in data-referencing effects#

The module provides ready-to-use MuEffect subclasses that read data directly from the training xr.Dataset.

DataVarMuEffect

Abstract base for effects that reference named variables in the Dataset. Subclasses implement create_effect; create_data and set_data are provided.

MediaMuEffect(DataVarMuEffect)

Applies a MediaTransformation (adstock + saturation) to a named media variable, then aggregates over channel_dim.

ControlMuEffect(DataVarMuEffect)

Applies a configurable prior coefficient to each control variable, automatically summing extra dimensions.

Example: multi-granularity media and controls#

from pymc_marketing.mmm import MMM, GeometricAdstock, LogisticSaturation
from pymc_marketing.mmm.additive_effect import (
    MediaMuEffect,
    ControlMuEffect,
)
from pymc_marketing.mmm.media_transformation import MediaTransformation

# X is an xr.Dataset with:
#   media_product:      (date, product, product-channel)
#   media_geo:          (date, geo, geo-channel)
#   control_national:   (date,)
#   control_product:    (date, product)

mmm = (
    MMM(
        date_column="date",
        channel_columns=["tv", "digital"],
        dims=("product", "geo"),
        adstock=GeometricAdstock(l_max=8),
        saturation=LogisticSaturation(),
    )
    .add_mu_effect(
        MediaMuEffect(
            data_vars=["media_product"],
            media_transformation=MediaTransformation(
                adstock=GeometricAdstock(l_max=8),
                saturation=LogisticSaturation(),
                adstock_first=True,
                dims=("product", "product-channel"),
            ),
            channel_dim="product-channel",
            prefix="product_media",
        )
    )
    .add_mu_effect(
        MediaMuEffect(
            data_vars=["media_geo"],
            media_transformation=MediaTransformation(
                adstock=GeometricAdstock(l_max=8),
                saturation=LogisticSaturation(),
                adstock_first=True,
                dims=("geo", "geo-channel"),
            ),
            channel_dim="geo-channel",
            prefix="geo_media",
        )
    )
    .add_mu_effect(
        ControlMuEffect(
            data_vars=["control_national"],
            prefix="national_ctrl",
        )
    )
    .add_mu_effect(
        ControlMuEffect(
            data_vars=["control_product"],
            prefix="product_ctrl",
        )
    )
)

mmm.fit(X, y)

Each grain uses a distinct dimension name ("product-channel" vs "geo-channel") to avoid xarray’s coordinate union and the NaN values it would produce. ControlMuEffect uses a scalar Prior("Normal", ...) by default, broadcasting across all dimensions; pass Prior("Normal", mu=0, sigma=2, dims="product") for per-product coefficients.

Note

MediaMuEffect does not apply any automatic scaling. Media data should be pre-scaled (e.g. max-scaling) before being placed in the xr.Dataset, or users can create a custom MuEffect that wraps MediaMuEffect with scaling logic.

Functions

safe_to_datetime(coords_values[, ...])

Safely convert coordinates to datetime, with validation.

Classes

ControlMuEffect(**data)

Effect that applies a user-configurable prior to each control variable.

DataVarMuEffect(**data)

MuEffect that reads its data from the xarray Dataset.

EventAdditiveEffect(**data)

Event effect class for the MMM.

FourierEffect(**data)

Fourier seasonality additive effect for MMM.

LinearTrendEffect(**data)

Wrapper for LinearTrend to use with MMM's MuEffect protocol.

MediaMuEffect(**data)

Effect that applies a media transformation to a data variable.

Model(*args, **kwargs)

Protocol MMM.

MuEffect(**data)

Abstract base class for arbitrary additive mu effects.