Interpret contributions and returns
Describe model outputs by their estimand, units and comparison. A fitted channel component, a spend-removal contrast and a predictive outcome are different quantities even when plotted on the same scale.
Contributions
For an additive Normal model, components sum to the conditional mean, while observed outcomes also contain residual variation. Other likelihoods can change that reconciliation. Under the log link, individual channel-removal contrasts are generally non-additive. See the response contract.
model.summary.contributions() defaults to channel components. Specify
component="controls", "seasonality" or "baseline" when those components
exist. Aggregate posterior draws over dates and units before computing an
interval for a total; adding marginal interval endpoints does not yield the
interval of the total.
ROAS and marginal return
Return on advertising spend (ROAS) has revenue per currency units when the outcome is revenue and the denominator is monetary spend. A conversions outcome instead gives conversions per currency. Impressions are not money; provide and validate the required cost conversion before using monetary labels.
model.summary.roas() defaults to method="elementwise". Explicit
method="incremental" uses the incrementality path. These methods have different
counterfactual and carryover semantics, so do not compare tables solely by a
shared ROAS heading.
Continue the fitted monetary-channel Python quickstart:
incremental_return = model.incrementality.contribution_over_spend(
frequency="all_time", include_carryover=True,
)
marginal_return = model.incrementality.marginal_contribution_over_spend(
frequency="all_time", spend_increase_pct=0.01, include_carryover=True,
)
The marginal method uses a finite spend perturbation, whose size and baseline matter. History and scoring windows also matter: zero in-window spend can coexist with response from earlier exposure. Undefined ratios need explicit handling; do not replace them with a claimed zero effect. Average return does not by itself rank the next unit of spend, and neither return measure establishes causality.
The incrementality API defaults to median-based contrasts under a log link;
choose central_tendency="mean" where the API supports the intended expected
response. Joint removal of channels and the sum of separate removals can differ,
particularly with nonlinear links and cross-channel effects.
Response and sensitivity curves
Saturation curves describe a component’s response, not a guarantee of the total business outcome under a changed policy. Check observed spend support, carryover, controls and baseline assumptions. A posterior-input sweep keeps parameters fixed at their fitted draws; it does not incorporate a future policy changing the data-generating process.
Report uncertainty with the interval definition, model and scenario identifiers, and evidence limitations. Use identification guidance for causal wording and scenario recipes for retained comparisons.
Compare ratio aggregation explicitly
Even with the same component numerator, averaging date-level ratios differs
from dividing totals. For two periods with spend [10, 90] and contribution
[20, 90], the average ratio is 1.5, whereas the ratio of totals is 1.1. Neither
calculation changes a component into an incremental causal effect.
import numpy as np
spend = np.array([10.0, 90.0])
contribution = np.array([20.0, 90.0])
assert np.isclose((contribution / spend).mean(), 1.5)
assert np.isclose(contribution.sum() / spend.sum(), 1.1)
For the fitted monetary-channel quickstart, choose the implemented method and aggregation explicitly. The first table divides aggregated components by spend; the second evaluates model-implied spend-removal contrasts with carryover.
component_return = model.summary.roas(method="elementwise", frequency="all_time")
contrast_return = model.summary.roas(
method="incremental", frequency="all_time", include_carryover=True,
)
print(component_return)
print(contrast_return)
Aggregation happens before the elementwise ratio when a non-original frequency
is supplied. Incremental-only options are rejected for the elementwise method
(src/ammm/mmm/summary/_factory_contributions.py:67). Keep zero-spend handling,
window and units visible and aggregate within each posterior draw before taking
quantiles. For nonlinear links, separate removal contrasts need not add up to a
joint intervention (src/ammm/mmm/incrementality.py:271).
Read the three curve families
| Curve | Input axis | Quantity and limit |
|---|---|---|
| Adstock | time since exposure in observation periods | Carryover kernel under fitted parameters; does not include the full outcome model |
| Saturation | Scaled x, with original-unit input in runner tables | Original-scale component response; compare with observed input support and selected transformation order |
| Forward pass | sweep multiplier of observed channel history | Original-scale contribution under scaled history at retained posterior draws; no alternative-prior refit |
The runner computes these at src/ammm/pipeline/stages/core.py:819 and includes
sweep=1 exactly. Its marginal value at that point divides the sweep derivative
by total original channel input; it differs from the incrementality API’s finite
1% spend perturbation (src/ammm/pipeline/stages/core.py:948,
src/ammm/mmm/incrementality.py:1423). Use the
output schema to distinguish
94% equal-tailed curve intervals from summary-facade intervals and parameter
summary defaults. Do not infer monetary ROAS if the channel inputs are impressions.
Implementation reference at 7cb7f20: src/ammm/mmm/incrementality.py:271, src/ammm/mmm/summary/_factory_contributions.py:67.