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.46 vs 0.39
the default 12-week TV design's MDE (ROAS) vs the model-anchored expected readout — 3.7× apart: as specified, the test buys an "inconclusive"
±21% × 16 wk
the optimizer's recommended TV flighting — MDE 0.63 ROAS, power shortfall 0.02
$0
dollars at risk in the budget-neutral flighting window; P(net KPI loss) 0%
0.71
power shortfall of the best national Print candidate (MDE 4.7 ROAS) — the engine's answer is "change the mode"

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.

Measurednbs/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 wk1.030.40
±19%11 wk0.760.17
±21%16 wk0.630.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)
TV0.460.904
Search0.210.452
Print0.260.513

Measuredcooldown_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

  1. Anchor before you run. If the model's expected readout sits below the design's MDE, redesign or don't run.
  2. Prefer budget-neutral contrasts — flighting turns measurement into a rearrangement of dollars you were spending anyway.
  3. Let the engine say no. An underpowered mode (national Print) is a changed design, not a smaller ambition.
  4. Respect cool-downs. Carryover makes the testing calendar a scheduling problem, not a wishlist.
  5. 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.