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.
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
Measured — nbs/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
- Match the estimand. A go-dark window measures average window ROAS; a small flighting delta measures marginal. File it where it happened.
- Prefer the likelihood route when the goal is correcting this model against this (possibly confounded) history; use prior-route calibration for portable evidence libraries.
- 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.
- Expect surgery, not absolution. Untested channels keep their biases — which is why the next notebook builds a portfolio.
- 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.