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.