Calibrate with external evidence
Use calibration only after matching the experiment’s estimand to the supported model contrast. Document assignment assumptions, contamination and spillovers, outcome units, population, period, spend intervention and transport to the modelling data. Do not treat a calibration target as an unquestionable answer.
Lift measurements
Build the graph, then call model.add_lift_test_measurements(lift_data) before
fitting. Required columns include channel, x, delta_x, delta_y and
sigma, plus relevant panel labels and any required date information for a
time-varying specification. The model compares saturation at x and
x + delta_x, with scaling and supported media modifiers.
This is a static saturation contrast. It does not replay arbitrary treatment
and control histories or automatically translate cumulative experimental lift
into the correct delta_y. Match that contrast before passing a study estimate.
The default Gamma likelihood uses absolute lift magnitudes and compatible
spend/effect signs under the monotonic response assumption.
Only the identity link is supported. A log-link model is rejected before adding
lift data. Supported named built-in pymc.dims distributions can be restored
from saved calibration records; arbitrary callables cannot. YAML does not
support overriding the lift dist argument.
Cost per target and ROAS
Build the model and register channel_contribution with
add_original_scale_contribution_variable before calling this method
(src/ammm/mmm/_mmm_calibration.py:230).
model.add_cost_per_target_calibration(data, calibration_data) uses a Normal
measurement likelihood on a ratio of mean spend and mean contribution over
dates. Set target_per_cost=True for the reciprocal quantity, such as revenue
per unit of spend. This is a ratio of aggregates, not the average of per-date
ratios, and it is not automatically a history-removal incremental return.
Pass original monetary spend on the same training coordinates, including when
the fitted channels are impressions. Calibration rows identify channel, panel
labels, sigma and the column selected by target_column (default
cost_per_target). Supply uncertainty justified by the external analysis.
Intervals, standard errors and standard deviations are not interchangeable
without the appropriate conversion and assumptions.
Avoid counting the same observations as independent evidence twice. Examine conflicts and prior sensitivity; calibration need not narrow every interval or improve causal validity. FE and CRE reject these calibration operations.
Persistence
Supported calls and copied inputs are replayed on load. Rebuilding clears them; adding calibration after fitting requires a new fit before saving. Follow the data and lifecycle contract and retain the study’s original evidence alongside the modelling rationale.
Build, register and calibrate
This continuation uses the synthetic X and y from the Python quickstart,
but constructs a fresh ordinary model. The two measurement tables below are
illustrative; replace them with independently reviewed study estimates in the
same units. The cost-per-target example must register the original-scale media
contribution before adding its likelihood (src/ammm/mmm/_mmm_calibration.py:230).
import pandas as pd
from ammm.mmm import MMM, GeometricAdstock, LogisticSaturation
calibrated = MMM(
date_column="date", channel_columns=["tv", "social"],
adstock=GeometricAdstock(l_max=1), saturation=LogisticSaturation(),
)
calibrated.build_model(X, y)
calibrated.add_original_scale_contribution_variable(["y", "channel_contribution"])
cost_measurement = pd.DataFrame({
"channel": ["tv"], "cost_per_target": [3.0], "sigma": [0.5],
})
calibrated.add_cost_per_target_calibration(X, cost_measurement)
calibrated.fit(
X, y, nuts_sampler="pymc", chains=2, cores=1,
tune=500, draws=500, target_accept=0.95, random_seed=42,
)
Here X supplies monetary spend with exactly the training date/channel labels;
3.0 and 0.5 have currency per target-unit units. The registered contribution
has target units, and the likelihood compares the ratio of date-mean spend to
date-mean contribution. With target_per_cost=True, invert the estimand and
supply its uncertainty on that reciprocal scale; do not invert a standard error
mechanically (src/ammm/mmm/_mmm_calibration.py:171). Review diagnostics and
sensitivity to this measurement before reporting the fit.
For a lift measurement instead, start with a separate fresh model, build it and add this table before fitting the same data. Do not append it to the fitted model above unless you intend to refit with both independent evidence sources.
lift_model = MMM(
date_column="date", channel_columns=["tv", "social"],
adstock=GeometricAdstock(l_max=1), saturation=LogisticSaturation(),
)
lift_model.build_model(X, y)
lift_measurement = pd.DataFrame({
"channel": ["tv"], "x": [40.0], "delta_x": [10.0],
"delta_y": [3.0], "sigma": [0.7],
})
lift_model.add_lift_test_measurements(lift_measurement)
x and delta_x use the fitted channel’s original input units; delta_y and
sigma use original outcome units for the supported static contrast. This block
only builds the calibrated graph. Fit it using the same-data lifecycle above,
then compare against an uncalibrated fit and retain the study provenance
(src/ammm/mmm/_mmm_calibration.py:102).
The equivalent lift block in an otherwise complete YAML configuration is:
calibration:
- add_lift_test_measurements:
df_lift_test:
class: pandas.DataFrame
kwargs:
data:
channel: [tv]
x: [40.0]
delta_x: [10.0]
delta_y: [3.0]
sigma: [0.7]
The builder resolves the nested DataFrame then calls the named method; failures
are wrapped with the calibration method name. It rejects a YAML lift dist
override (src/ammm/mmm/builders/yaml.py:37). Save a fitted calibrated model with
model.save and preserve the external study report separately: replayable numeric
inputs do not establish study validity (src/ammm/mmm/persistence.py:79).
Implementation reference at 7cb7f20: src/ammm/mmm/_mmm_calibration.py:102, src/ammm/mmm/mmm.py:2870, src/ammm/mmm/builders/yaml.py:37.