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:
Actual: the model prediction with real spend data.
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):
where the counterfactual spend zeroes out only the evaluation period:
Marginal incrementality (small perturbation):
where the perturbed spend scales only the evaluation period:
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.
Link functions#
The counterfactual is applied to spend and evaluated on
channel_contribution, which lives in the linear predictor
\(\mu_t = \text{base}_t + \sum_c v_{t,c}\) – not on the response
scale. Turning a change in \(v_{t,m}\) into a change in
\(\hat{Y}_t = \text{inv}(\mu_t)\,s\) is link-dependent, and is the job
of an IncrementalReducer:
link="identity"(IdentityLinkReducer) – the response is additive in the media contributions, the base term cancels, and the increment is \(s \sum_t \Delta_{t,m}\). Per-channel increments are independent of the baseline and of each other, and they sum to the total media increment.link="log"(LogLinkReducer) – the response is multiplicative, so the base term does not cancel and the increment is \(\sum_t \hat{Y}_t [\exp(\Delta_{t,m}) - 1]\). Per-channel increments depend on the baseline, the controls and the other channels, and they do not sum to the total media increment. This is a property of the model, not of the estimator: the paper’s derivation of \(\text{ROAS}_m\) assumes an additive response, and that assumption does not survive a non-linear link.
Because the increment is formed per posterior draw and only then aggregated, credible intervals are correct under both links.
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:
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_dataand has not implementedincrementality_spec()raisesNotImplementedError. 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
|
Increment reducer for an additive response ( |
Map a linear-predictor perturbation to a response-scale increment. |
|
|
Incrementality and counterfactual analysis for MMM models. |
|
Increment reducer for a multiplicative response ( |