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
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 = pd.to_datetime(mmm.model.coords["date"])
        penalty = self.penalty_provider(dates)
        pm.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 = pm.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 = pm.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 = pd.to_datetime(model.coords["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.mu_effects.append(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 pm.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 pm.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.

Classes

EventAdditiveEffect(**data)

Event effect class for the MMM.

FourierEffect(fourier[, date_dim_name])

Fourier seasonality additive effect for MMM.

LinearTrendEffect(trend, prefix[, date_dim_name])

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

Model(*args, **kwargs)

Protocol MMM.

MuEffect(*args, **kwargs)

Protocol for arbitrary additive mu effect.