incrementality#

Incrementality and counterfactual analysis for Marketing Mix Models.

This module provides functionality to compute incremental channel contributions using counterfactual analysis, properly accounting for adstock carryover effects.

Concept#

Incrementality measures the causal impact of a marketing channel by comparing two scenarios:

  1. Actual: the model prediction with real spend data.

  2. Counterfactual: the model prediction with spend removed or perturbed.

The difference between these two predictions is the incremental contribution of that channel. Because MMMs include adstock transformations, spend at time t affects outcomes at t, t + 1, …, t + l_max. A naïve element-wise comparison ignores this temporal attribution; this module handles it correctly by extending the evaluation window to capture both carry-in and carry-out effects.

Total incrementality (zero-out counterfactual):

\[\Delta Y_m = \sum_{t=t_0}^{t_1 + L - 1} \bigl[\hat{Y}_t(x;\,\Omega) - \hat{Y}_t(x^{\text{cf}};\,\Omega)\bigr]\]

where the counterfactual spend zeroes out only the evaluation period:

\[\begin{split}x^{\text{cf}}_{s,m} = \begin{cases} 0 & s \in [t_0,\, t_1] \\ x_{s,m} & \text{otherwise} \end{cases}\end{split}\]

Marginal incrementality (small perturbation):

\[\delta Y_m = \sum_{t=t_0}^{t_1 + L - 1} \bigl[\hat{Y}_t(\tilde{x};\,\Omega) - \hat{Y}_t(x;\,\Omega)\bigr]\]

where the perturbed spend scales only the evaluation period:

\[\begin{split}\tilde{x}_{s,m} = \begin{cases} \alpha\, x_{s,m} & s \in [t_0,\, t_1] \\ x_{s,m} & \text{otherwise} \end{cases}\end{split}\]

Here m is the channel, x the spend vector, L the adstock window length (l_max), Ω the posterior parameter samples, and α the counterfactual_spend_factor. Spend outside \([t_0, t_1]\) is always kept at its actual value so that adstock carry-in is correctly accounted for.

The intervention is on spend, not on a channel’s effect. With \(\alpha = 0\) the two agree only if the saturation sends zero spend to zero contribution; otherwise (or with time_varying_media) a residual effect survives the counterfactual. To remove a component’s effect directly, see compute_counterfactual_contributions_dataset().

Incrementality is a general-purpose building block. Dividing incremental contribution by spend gives ROAS (Return on Ad Spend) when the model’s target variable is revenue; taking the reciprocal (spend / contribution) gives CAC (Customer Acquisition Cost) when the target is customer count. The same logic applies to any target variable.

Mediated effects#

Spend does not always reach the response through channel_contribution alone. A mu_effect can read channel_data itself – a funnel mediator, where upper-funnel spend creates demand, demand drives lower-funnel spend, and only that converts – and then part of the incremental response travels through the effect. Such an effect is included in the increment, additively in the linear predictor:

\[\Delta \mu_t = \Delta v_{t,m} + \sum_j \Delta e_{t,j}\]

which is then handed to the same IncrementalReducer as before. The reducers are untouched by mediation: they convert a change in the linear predictor into a change in the response, and do not care how many nodes that change was collected from.

Three things follow, and they are why mediation is not free:

  • Effects must opt in. An effect whose contribution depends on channel_data and has not implemented incrementality_spec() raises NotImplementedError. Ignoring it would report the direct path as if it were the total. Effects that do not depend on spend – trends, events, seasonality – are part of the baseline, cancel in the difference, and are skipped without being asked anything.

  • One counterfactual per channel. Without mediation a single all-channels perturbation is enough, because \(v_{t,c}\) depends on channel c’s spend alone and column m of that one evaluation is channel m’s counterfactual. A funnel sums over channels inside a nonlinear transform, so no per-channel column survives and each channel needs its own perturbation.

  • The window gets longer. A mediated path that chains a second adstock behind the model’s own outlives it, so the evaluation window is sized for the longest path spend can take – measured on the graph, not declared.

Both the measurement and the check that the increment is complete live in spend_reach, which reads them off single-date spend perturbations. This module asks it for a window length and a mode and otherwise knows nothing about either.

Estimands#

Incrementality.compute_incremental_contribution() is a unilateral intervention: each channel’s number answers “what changes if this channel’s spend is scaled by counterfactual_spend_factor, holding the others at their actual spend”. At the default factor = 0 that is the familiar leave-one-out question, “what would we lose without this channel”; at 0.5 it is a halving and at 1.01 a one-percent increase, and neither is a leave-one-out. Incrementality.compute_joint_incremental_contribution() applies the same factor to every channel at once and answers “how much does media drive in total”.

Both intervene on spend, which is not the same as removing a channel’s term from the model: at factor = 0 they coincide only where zero spend produces zero contribution, as it does for a saturation through the origin but not for one with an intercept.

The two estimands agree only when the response is additive in the channels, that is under link="identity" with no channel-dependent effect. Otherwise they differ by the interaction between the channels, and the sign of the gap is not fixed: at factor = 0 with strictly positive contributions the unilateral numbers sum to more than the joint, because interaction mass is counted by every channel that touches it, but with contributions of mixed sign, or with factor > 1, the gap can go the other way. Summing per-channel increments is not a way to get a total either way, and the gap is a property of the model rather than an error in either number.

Examples#

Compute quarterly incremental contributions:

incremental = mmm.incrementality.compute_incremental_contribution(
    frequency="quarterly",
    start_date="2024-01-01",
    end_date="2024-12-31",
)

Compute quarterly ROAS (when target variable is revenue):

roas = mmm.incrementality.contribution_over_spend(
    frequency="quarterly",
    start_date="2024-01-01",
    end_date="2024-12-31",
)

Compute monthly CAC (when target variable is customer count):

cac = mmm.incrementality.spend_over_contribution(
    frequency="monthly",
)

Compute marginal ROAS (return on next dollar):

mroas = mmm.incrementality.marginal_contribution_over_spend(
    frequency="quarterly",
)

References#

Google MMM Paper: https://storage.googleapis.com/gweb-research2023-media/pubtools/3806.pdf

Classes

IdentityLinkReducer(scale)

Increment reducer for an additive response (link="identity").

IncrementalReducer()

Map a linear-predictor perturbation to a response-scale increment.

Incrementality(model[, idata, data])

Incrementality and counterfactual analysis for MMM models.

LogLinkReducer(baseline_response)

Increment reducer for a multiplicative response (link="log").