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

CurveInput axisQuantity and limit
Adstocktime since exposure in observation periodsCarryover kernel under fitted parameters; does not include the full outcome model
SaturationScaled x, with original-unit input in runner tablesOriginal-scale component response; compare with observed input support and selected transformation order
Forward passsweep multiplier of observed channel historyOriginal-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.