Causal Inference 08: Designing the Next Experiment
The model stops being the patient and becomes the surgeon's assistant. Rung 5 designed
a test from the raw geo panel alone — deliberately, because an experiment's
validity must never depend on the model it will later repair. But its
efficiency should absolutely use the model. This page mirrors
nbs/causal/causal_08_designing_the_next_experiment.ipynb: Veranda Home's
fitted posterior (the cached rung-2 fit — designing costs posterior passes, not
refits) prices two candidate tests from
notebook 07's to-do list —
TV, the stubborn residual under-read, and Print, the
twin that inherited the pair-sum bias — and hands them opposite
verdicts.
1 — Candidate one: a TV flighting test
Veranda's data is national, so the recommended design family is budget-neutral
flighting: alternate TV spend between a high and a low level in randomized
blocks — same annual budget, honest identification from the contrast. The
design engine's default for this dataset: national_flighting,
12 weeks, spend levels [0.5, 1.5] of plan,
budget-neutral — with a minimum detectable effect of 1.46 ROAS
(the framework's power rule, \( \mathrm{MDE} = 2.8\,\hat\sigma_{\text{design}} \),
at 80% power and 5% two-sided size).
from mmm_framework.planning.design import design_experiment
from mmm_framework.planning.design_anchor import model_anchored_effect
from mmm_framework.planning.opportunity_cost import compute_opportunity_cost
design = design_experiment("veranda_weekly.csv", "Sales", "TV")
# -> national_flighting | 12 weeks | levels [0.5, 1.5] | MDE 1.46 ROAS
anchor = model_anchored_effect(mmm, design, max_draws=60, random_seed=42)
# -> the readout the test would produce: median 0.39, 90% HDI [0.25, 0.49]
cost = compute_opportunity_cost(mmm, design, max_draws=60, random_seed=42,
kpi_kind="revenue")
# -> spend_at_risk 0.0, P(KPI loss over the window) 0%
2 — What would we actually see? The model-anchored readout
The design's MDE says what is detectable. The posterior says what is expected: push the candidate schedule through the fitted response curves — a spend perturbation on the test window only, not the global curve — and read the distribution of incremental ROAS the test would measure. For the default TV design: median 0.39, 90% HDI [0.25, 0.49]. The expected readout sits far below the 1.46 MDE — as specified, this test would probably come back "inconclusive". Better to know that before spending a quarter on it.
Measured — nbs/artifacts/causal_08_design.json. What the
default 12-week TV flighting would read (median 0.39, 90% HDI [0.25, 0.49]) against
what it could detect (MDE 1.46). The optimizer's job is to pull the detection bar
down to the readout — its recommended design (section 4) lands at 0.63.
3 — What does learning cost? (Almost nothing, if you design it that way)
The posterior counterfactual of running the schedule vs business-as-usual prices the test's short-term risk: dollars at risk $0, P(net KPI loss over the window) 0% — the posterior actually expects a small positive KPI delta (+92 units) over the window, since the flighting spends the same dollars on a curve that is not linear. One honest footnote: randomized blocks against a varying baseline round to approximately neutral — the notebook's window nets to +2.2% of annual TV spend, not exactly zero. Budget-neutral flighting is still the free lunch of measurement: the same media plan, rearranged into an instrument.
4 — The Pareto choice
Candidate designs trade four objectives — MDE ↓, power shortfall ↓, short-term
cost ↓, duration ↓ — and no single design wins all four.
suggest_experiment sweeps the design space (intensity × duration), keeps
the non-dominated frontier, and recommends the knee among powered designs.
The sweep over ±30–100% intensity and 6–16 weeks:
| intensity | duration | MDE (ROAS) | power shortfall |
|---|---|---|---|
| ±13% | 6 wk | 1.03 | 0.40 |
| ±19% | 11 wk | 0.76 | 0.17 |
| ±21% | 16 wk | 0.63 | 0.02 |
Measured — the TV candidate grid from the notebook's seeded run. The recommended design: ±21% intensity × 16 weeks → MDE 0.63 ROAS, power shortfall 0.02. Note what the optimizer traded: a gentler contrast than the default's ±50%, held for longer — more weeks of a smaller, cheaper wiggle beat a short violent one.
from mmm_framework.planning.experiment_optimizer import (
suggest_experiment, cooldown_weeks)
from mmm_framework.agents.model_ops import identify_structural_parameters
sug = suggest_experiment(mmm, "veranda_weekly.csv", "Sales", "TV",
duration_min=6, duration_max=16,
intensity_min=30.0, intensity_max=100.0,
max_draws=40, random_seed=42)
rec = sug["recommended"] # ±21% x 16 weeks -> MDE 0.63, shortfall 0.02
gap = cooldown_weeks(mmm, "TV") # {'alpha': 0.46, ..., 'cooldown_weeks': 4}
ident = identify_structural_parameters(mmm, dataset_path="veranda_weekly.csv",
kpi="Sales", channel="TV", duration=24)
Measured — the design frontier for both candidates, on one axis. TV's candidates descend toward the expected readout as duration grows; the big marker is the recommended, powered pick. Print's candidates (section 5) never get anywhere near the line — the best national Print design's MDE is 3.9 ROAS, an order of magnitude above any plausible readout.
5 — The engine's most valuable answer: "no"
Now Print — the channel notebook 07 flagged as the top re-test priority. Print's national spend is a rounding error of TV's, so a national budget-neutral flighting contrast moves almost no dollars, and no candidate in the sweep comes close to powered: MDEs run 6.4 → 4.7 → 3.9 ROAS across 6/11/16 weeks, with power shortfalls stuck at 0.70–0.73 (best available: MDE 4.7, shortfall 0.71). The honest recommendation is not "run the least-bad one" — it is change the mode: a matched-pair geo holdout (rung 5's machinery) concentrates the contrast in a subset of markets, or measures the Radio+Print pair jointly. The design engine just saved a quarter's Print budget from buying an "inconclusive". A test you shouldn't run is a result too.
6 — Cool-downs: carryover contaminates the calendar
Two tests on the same channel back-to-back are not independent: the first test's adstock is still paying out during the second's window. The fitted carryover implies a washout period per channel — the minimum gap before the next test on that channel, and the minimum block length inside a flighting schedule:
| channel | fitted adstock α | half-life (weeks) | cool-down (weeks) |
|---|---|---|---|
| TV | 0.46 | 0.90 | 4 |
| Search | 0.21 | 0.45 | 2 |
| 0.26 | 0.51 | 3 |
Measured — cooldown_weeks reads each channel's fitted
adstock and returns the smallest gap after which the residual carryover falls below
5%. These numbers become scheduling constraints in
notebook 09's calendar.
7 — Identifying the curve, not just the effect
Everything above measures an average effect. Budget allocation
needs the curve — the saturation shape and the carryover — and rung 2 showed
those parameters barely move under observational data. A multi-level flighting
schedule (≥3 spend levels) is the design that can, in principle, trace the curve.
identify_structural_parameters computes, per structural parameter, the
detection power and the expected prior→posterior contraction the schedule
would buy — Fisher-information math against the fitted posterior, with a fail-closed
self-check that the forward op mirrors the graph.
Measured — expected prior→posterior contraction per structural
parameter (log scale), 12-week vs 24-week 3-level TV schedules. Detection power barely
moves (β: 0.830 → 0.833; saturation λ: 0.678 → 0.682; adstock α: ≈1.00 at both), but
contraction is roughly 10× larger at 24 weeks — and only the 24-week
commitment crosses the engine's identification bar:
identifies_anything = False at 12w, True at 24w.
Curvature knowledge is expensive; buy it deliberately or don't claim it.
📎 The honest arithmetic of curve identification
A well-built 12-week schedule detects the effect with high power yet barely contracts the structural priors. Detection asks "is the average effect nonzero?" — one number. Identification asks "which saturation curve and which carryover kernel produced it?" — a shape. The same contrast that answers the first question in 12 weeks needs 24 to start answering the second. Measure effects cheaply and often; measure curves rarely and on purpose.
8 — The design checklist this rung leaves behind
- Anchor before you run. If the model's expected readout sits below the design's MDE, redesign or don't run.
- Prefer budget-neutral contrasts — flighting turns measurement into a rearrangement of dollars you were spending anyway.
- Let the engine say no. An underpowered mode (national Print) is a changed design, not a smaller ambition.
- Respect cool-downs. Carryover makes the testing calendar a scheduling problem, not a wishlist.
- Decide which you're buying: the effect or the curve. They cost different orders of magnitude.
Check your understanding
Commit to an answer before opening each one.
The default 12-week design is perfectly valid — randomized, budget-neutral, honest MDE. Why does the engine still say "don't run it as specified"?
Because validity and efficiency are different properties. The design would produce an unbiased readout — of an effect it almost certainly cannot distinguish from zero. The model-anchored pass says the readout would land near 0.39 ROAS (90% HDI [0.25, 0.49]) while the design can only detect 1.46: the likely outcome is a six-figure "inconclusive". The anchor doesn't make the experiment depend on the model — the readout stays model-free — it just stops you from buying a coin flip. The optimizer then fixes exactly this, trading a gentler ±21% contrast held for 16 weeks to bring the MDE down to 0.63.
Why is the verdict for Print "change the mode" rather than "run the least-bad national candidate"?
Because the failure is structural, not marginal. A national budget-neutral flighting contrast can only move Print's own (tiny) budget, so even the best candidate in the sweep has an MDE of 3.9–4.7 ROAS with a power shortfall of ~0.71 — no plausible Print ROAS is detectable at any duration in the range. Running the least-bad one converts a quarter of calendar into an "inconclusive" by construction. The fix is a different mode: a matched-pair geo holdout concentrates the same contrast in a subset of markets, where the effective per-market perturbation is large enough to power. The ambition stays; the instrument changes.
The 12-week 3-level schedule has ≥83% power on TV's effect, yet identifies_anything comes back False. What's the difference between detecting and identifying?
Power measures the chance of declaring the average effect nonzero — and 12 weeks is plenty for that (β power 0.830). Identification measures whether the schedule would actually move the structural priors: expected contraction on the saturation and adstock parameters is ~0.001 at 12 weeks — the posterior would come back essentially the prior. At 24 weeks contraction grows ~10× (β 0.0064, λ 0.0077) and crosses the engine's bar. If you fund 12 weeks, claim an effect measurement; only the 24-week commitment buys a curve.
💡 Run it yourself
This page mirrors
nbs/causal/causal_08_designing_the_next_experiment.ipynb
— notebook 8 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_08_design.json,
produced by nbs/causal/causal_08_designing_the_next_experiment.ipynb.
Previous: 07 · Many Experiments.
Next: 09 · The Measurement
Program — from one test to a program: EIG, EVOI, the EVPI ceiling, and
evidence that decays while you're not looking. Source:
github.com/redam94/mmm-framework.