Causal Inference 06: Calibrating the Model

One honest experiment meets a confidently confounded posterior. The Search lift test designed in notebook 05 has run: an 8-week go-dark, read with the pre-registered calibrated estimator. The readout lands on the desk of the analyst who owns Veranda Home's national MMM — the model that over-credits Search by +43% and cannot know it. This page mirrors nbs/causal/causal_06_calibrating_the_model.ipynb: two mechanically different routes for folding the experiment into the Bayesian MMM — and they are not equivalent. A prior suggests; a likelihood commits. Then the quietest failure in applied calibration: the same number filed under the wrong estimand, and a six-figure experiment evaporating without a single warning.

+42%
Search error after Route A (informative prior) — the confounded likelihood steamrolls a prior that said "cut it in half"
+5%
Search error after Route B (in-graph likelihood) — truth back inside the 90% interval
+38%
Search error when the same readout is filed as MARGINAL ROAS — the test silently voided
+40%
Social (untested) error after calibration — surgery repairs only what was measured

1 — The patient: a confidently confounded model

The baseline is the series' rung-1 fit on the unobserved_confounding history (156 weeks, national grain): a latent demand wave drives both Search spend and sales, so the model routes demand's credit to the channel that chases it. Graded against the sealed key: Search +43% over-credited, truth outside the 90% interval — with every sampler diagnostic green. Social, the other demand-chaser, sits at +38%. TV (−18%) and Display (−27%) carry the compensating under-credit, but their intervals at least cover.

2 — The readout, and what an experiment actually measures

The go-dark window: weeks 100–111, Search off in the treated markets. The readout — generated in the notebook from the world's sealed truth plus sampling noise at the design's SE, a well-run experiment rather than an oracle — is window incremental ROAS 0.49 ± 0.08 (the world's true window ROAS: 0.56, within one SE). Note the carryover tail: the lost effect extends past the window's end, because the window's spend was still working the next month. A competent readout — and the model's in-graph estimand — accounts for it.

3 — Route A: the experiment as a prior

ExperimentCalibrator reduces the lift to the coefficient scale using the fitted model's own response shape (the design factor), derives a tight prior, and refits. The derivation does its job perfectly: from the readout it computes a coefficient target of β = 1.15 ± 0.19, against a confounded fitted mean of β ≈ 2.20 — "cut Search roughly in half."

from mmm_framework.calibration import ExperimentCalibrator
from mmm_framework.validation.results import LiftTestResult

lift = LiftTestResult(
    channel="Search", test_period=(dark_start, dark_end),
    measured_lift=readout_roas * window_spend,   # KPI units
    lift_se=readout_se * window_spend,
)
cal = ExperimentCalibrator(model)          # the fitted, confounded MMM
report = cal.derive_priors([lift])         # -> beta target 1.15 +/- 0.19
out = cal.calibrate([lift], refit=True)    # refit with the derived prior

And then the data does what data does. After the refit, Search sits at +42% — the posterior barely moved. A prior worth one experiment's information faces 156 weeks of confounded likelihood, and the likelihood wins. A prior is a suggestion.

📎 Route A is not broken — it has a different job

Prior-route calibration is built for a portable library of lift-test evidence that composes across model versions and vintages by inverse-variance. As a correction device against confounded in-sample data, though, what you need is a likelihood.

4 — Route B: the experiment as data

add_experiment_calibration attaches the readout to the graph as an observation of the model-implied window ROAS:

\[ \text{ROAS}_{\text{readout}} \;\sim\; \mathcal{N}\!\bigl(\text{ROAS}_{\text{window}}(\theta),\; \text{SE}^2\bigr) \]

Now agreement with the experiment is part of what "fitting the data" means — the sampler must find parameters that explain the history and the test, jointly re-negotiating the coefficient, the saturation curve, and the carryover.

from mmm_framework.calibration import ExperimentEstimand, ExperimentMeasurement

readout = ExperimentMeasurement(
    channel="Search", test_period=(dark_start, dark_end),
    value=0.49, se=0.08,                 # the calibrated readout from nb05
    estimand=ExperimentEstimand.ROAS,    # a go-dark measures AVERAGE window ROAS
    distribution="normal", name="search_dark",
)
model.add_experiment_calibration([readout])   # the readout becomes an observation
results = model.fit(draws=1000, tune=1000)    # jointly re-negotiates the posterior

Measurednbs/artifacts/causal_06_calibration.json. Search relative error vs the sealed truth: baseline +43%, Route A (prior) +42%, Route B (likelihood) +5% — with the 90% interval covering truth again. The wrong-estimand filing (section 5) sits at +38%. One experiment, two routes: only the likelihood commits.

Measured — calibration is surgical. Search (tested) goes +43% → +5%; Social — the other demand-chaser, untested — keeps its confounding bias almost untouched, +38% → +40%. An experiment repairs what it measured, not what you wish it had measured. That is why notebook 07 builds a portfolio.

5 — The quietest failure: the wrong estimand

Rung 2 drew the line between average ROAS (what a go-dark measures) and marginal ROAS (what the next dollar buys). Here is what happens when the analyst files this readout under the wrong one — same number 0.49, same SE 0.08, declared as marginal at +10% spend (estimand=ExperimentEstimand.MROAS, spend_lift_pct=10.0): Search lands at +38%, essentially as biased as the never-calibrated baseline.

Why so quiet? Saturation puts the marginal below the average — the confounded model's marginal ROAS near current spend already sits close to the misfiled number, so the likelihood term is satisfied without moving anything. The constraint lands non-binding. No error, no warning, no correction: the test is simply spent. Estimand bookkeeping isn't pedantry — it's the difference between an experiment that repairs the model and one that vanishes into it.

6 — Calibration discipline, in five lines

  1. Match the estimand. A go-dark window measures average window ROAS; a small flighting delta measures marginal. File it where it happened.
  2. Prefer the likelihood route when the goal is correcting this model against this (possibly confounded) history; use prior-route calibration for portable evidence libraries.
  3. Carry the SE honestly. The likelihood weighs the experiment by its precision; understating the SE overrules 156 weeks of data with 8, and overstating it buys nothing.
  4. Expect surgery, not absolution. Untested channels keep their biases — which is why the next notebook builds a portfolio.
  5. Re-check afterwards. A calibrated fit is a new model; the refutation battery and learning diagnostics still apply.

Check your understanding

Commit to an answer before opening each one.

The derived prior said "β = 1.15 ± 0.19" against a fitted β ≈ 2.20 — a precise, well-targeted correction. Why did the posterior barely move?

Because a prior competes with the likelihood on information content, and it was outgunned: one experiment's worth of information against 156 weeks of history whose confounded correlations all say "Search drives sales." The posterior is the compromise, and the compromise lands almost exactly where the data was — Search error moved from +43% to +42%. Route B avoids the contest by changing what the likelihood is: the readout becomes one more observation the parameters must explain, and Search lands at +5% with the interval covering.

Why does filing the readout as marginal ROAS produce no warning — no divergence, no worse fit, nothing?

Because the misfiled constraint is satisfiable for free. Under a saturating response curve the marginal ROAS is mechanically below the average ROAS, and the confounded model's marginal near current spend already sits close to 0.49. So the extra likelihood term is happy exactly where the posterior already was — non-binding, zero information transferred. The fit quality is unchanged, the diagnostics are green, and the only casualty is the experiment's budget. The failure is bookkeeping, so only bookkeeping can catch it.

After Route B lands Search at +5%, what does the model believe about Social — and what should the measurement program conclude?

Social stays at roughly +40% over-credited (measured: +38% → +40%). The calibration was surgical: the go-dark identified Search's window response and re-negotiated Search's parameters; it said nothing about the other channel the latent demand wave inflates. The program conclusion is the portfolio logic of notebook 07 — each confounded channel needs its own evidence, and the next test should be chosen by what it would resolve, which is notebook 08's job.

💡 Run it yourself

This page mirrors nbs/causal/causal_06_calibrating_the_model.ipynb — notebook 6 of 11 in the Causal Inference in Practice series (brand: Veranda Home; synthetic worlds with sealed answer keys; every claim graded by an assert). All numbers from nbs/artifacts/causal_06_calibration.json, produced by nbs/causal/causal_06_calibrating_the_model.ipynb. Previous: 05 · Measuring One Experiment. Next: 07 · Many Experiments — evidence as a portfolio: off-panel tests, conflicting legacy studies, and the collinear ridge snapped by a single well-placed readout. Source: github.com/redam94/mmm-framework.