When the coin flip has to move up a level
Lumen wants to test a connected-TV burst - a two-month brand campaign running on CTV in a handful of US metros. You cannot randomize which shopper "sees TV"; the delivery is inherently geographic, and post-cookie you cannot even track it cleanly at the user level. So you randomize or select regions instead: treat a subset, hold the rest out, and reconstruct what the treated regions would have done without the burst. Session 8 teaches the two credible ways to build that counterfactual - Google's CausalImpact and the synthetic control method (SCM) - and, just as important, how to earn trust in the estimate with a good pre-period fit and placebo inference.
Geo experiments - randomize the region, not the person 6 min live
A geo experiment makes the region the unit of randomization. You split markets (DMAs, states, metros) into treated and control, run the intervention in the treated markets only, and compare aggregate outcomes. This is the right tool when treatment cannot be delivered per user - broadcast and CTV media, pricing, a policy change - or when privacy loss (Safari/Firefox/ATT) has gutted user-level tracking. The cost is fewer units: you might have 9 regions, not 64,000 users, so the analysis leans hard on modeling the counterfactual well.
LiveTwo named traditions - do not conflate them3 min▶
There are two distinct lineages here, and mixing up their names is a classic tell that someone learned this from a blog post.
- Google - Geo experiments (Vaver & Koehler 2011) plus CausalImpact (Brodersen et al. 2015), which uses a Bayesian structural time-series model to forecast the counterfactual from control markets.
- Meta - GeoLift, which is built on the synthetic control method.
Self-studyThe donor pool must be clean3 min read▶
Every method here reconstructs the treated region from untreated regions - the donor pool. That only works if the donors were genuinely unaffected by the treatment. If Lumen's CTV burst in California spills into neighbouring Nevada (shared media market, cross-border shoppers), Nevada is contaminated and cannot be a donor. This is the geo-level echo of SUTVA (no interference) from b3: a spillover onto a "control" region quietly deflates your measured effect.
Lumen's first geo test failed review because a treated metro and its "control" neighbour shared a DMA - the TV signal crossed the line. The fix was to drop adjacent regions from the donor pool and add a geographic buffer. Cleaner donors, smaller pool, more honest number.
Synthetic control - build a twin from a weighted donor pool 5 min live
The synthetic control method (Abadie & Gardeazabal 2003; Abadie, Diamond & Hainmueller 2010) builds a synthetic treated unit as a weighted average of untreated donor units. The weights (non-negative, summing to 1) are chosen so the synthetic unit tracks the treated unit's outcome as closely as possible before treatment. After the intervention, the gap between the real treated line and its synthetic twin is the estimated effect. A tight pre-treatment fit is the whole credibility test - if the synthetic cannot match the treated region before anything happened, you have no reason to trust it afterward.
Self-studyPlacebo tests = permutation inference3 min read▶
With only a handful of regions, classical standard errors are shaky. SCM borrows an idea from permutation testing: pretend each donor region was the treated one, run the whole SCM on it, and collect its post-period gap. That gives you a distribution of "placebo" effects under no real treatment. If your genuine treated region's gap is extreme relative to that placebo distribution, the effect is unlikely to be noise. A common summary is the ratio of post- to pre-period fit error: the real unit should sit in the tail.
Assemble the geo panel and pick treated vs donors ★ 8 min · everyone
Lumen has 9 US regions in the geo field and 52 weeks of pre-period spend_weekly revenue. We treat a subset with a CTV burst and hold the rest out as the donor pool. First, shape the data into a region-by-week revenue panel.
Pivot weekly revenue into a wide panel - one column per region, one row per week - and mark the intervention week:
import pandas as pd, numpy as np
sw = pd.read_parquet("lumen_spend_weekly.parquet") # week, channel, geo, revenue, ...
panel = (sw.groupby(["week", "geo"], as_index=False)["revenue"].sum()
.pivot(index="week", columns="geo", values="revenue")
.sort_index())
INTERVENTION = pd.Timestamp("2026-04-06") # CTV burst goes live
pre = panel.loc[panel.index < INTERVENTION] # 52 pre-period weeks
post = panel.loc[panel.index >= INTERVENTION]
print(panel.shape, "regions:", list(panel.columns))
Choose the treated region and a clean donor pool - drop any region that could receive spillover from the treated media market:
treated = "US-CA" # got the CTV burst
adjacent = ["US-NV"] # shares a media market -> contaminated
donors = [g for g in panel.columns
if g not in [treated] + adjacent] # clean donor pool
print("treated:", treated, "| donors:", donors)
# sanity: donors must be UNAFFECTED by the burst (no spillover) - SUTVA at the geo level
Eyeball the pre-period correlations between the treated region and each donor. Good donors move with the treated region before treatment - that is what makes a tight synthetic fit possible.
Fit the counterfactual and read the lift ★ 9 min · everyone
Two credible ways to build the counterfactual. Fit CausalImpact (a Bayesian structural time-series forecast from the donors) and a classic synthetic control with pysyncon, then compare their estimated lift and interval.
CausalImpact - forecast what US-CA would have earned without the burst from the donor series:
from causalimpact import CausalImpact # tfcausalimpact / pycausalimpact # response = treated region first, then donor covariates data = panel[[treated] + donors].copy() pre_period = [str(panel.index.min().date()), "2026-03-30"] post_period = ["2026-04-06", str(panel.index.max().date())] ci = CausalImpact(data, pre_period, post_period) print(ci.summary()) # abs + relative effect, 95% credible interval # ci.plot() -> observed vs counterfactual, pointwise gap, cumulative lift
Synthetic control with pysyncon - solve for donor weights that match the pre-period, then read the post-period gap:
from pysyncon import Dataprep, Synth
long = panel.reset_index().melt(id_vars="week", var_name="geo", value_name="revenue")
prep = Dataprep(
foo=long, predictors=["revenue"], predictors_op="mean",
dependent="revenue", unit_variable="geo", time_variable="week",
treatment_identifier=treated, controls_identifier=donors,
time_predictors_prior=list(pre.index), time_optimize_ssr=list(pre.index),
)
synth = Synth(); synth.fit(dataprep=prep)
print(synth.weights()) # donor weights, sum to 1
att = synth.att(time_period=list(post.index))
print("estimated ATT (weekly lift):", att) # gap between real and synthetic
Read both numbers the same way: the cumulative post-period gap is the estimated incremental revenue from the burst. Report it with the interval and - critically - the pre-period fit quality. If CausalImpact and pysyncon roughly agree, that convergence is itself reassuring.
Lumen's finance team will not fund a channel on "revenue went up." They will fund "the synthetic twin says US-CA would have made about $X without CTV; it made about $Y; the $Y-minus-$X gap sits outside the placebo distribution." That sentence is what SCM buys you.
Validate it - pre-fit and placebo permutation ★ 7 min · everyone
An SCM estimate is only as good as its checks. Confirm the pre-period fit is tight, then run the placebo test: treat each donor as if it were the treated unit and see whether the real effect is extreme against that distribution.
Check pre-period fit with RMSPE - the synthetic must track the treated region before the burst:
def rmspe(actual, synthetic):
return np.sqrt(np.mean((actual - synthetic) ** 2))
pre_fit = rmspe(pre[treated].values, synth_pre_values) # from the fitted model
print(f"pre-period RMSPE = {pre_fit:.1f}")
# rule of thumb: pre-RMSPE should be small vs the revenue scale.
# if it is large, the synthetic never matched -> do NOT trust the post gap
Placebo permutation - refit SCM pretending each donor was treated, then compare gaps:
placebo_gaps = {}
for g in donors:
fake_donors = [d for d in donors if d != g]
gap = fit_scm_gap(panel, treated=g, donors=fake_donors, # your demo-2 fit, reused
pre=pre.index, post=post.index)
placebo_gaps[g] = gap # post-period effect if "treated"
real_gap = fit_scm_gap(panel, treated=treated, donors=donors,
pre=pre.index, post=post.index)
# ratio of post- to pre-period RMSPE puts every unit on one scale
ranks = sorted([real_gap] + list(placebo_gaps.values()), reverse=True)
p_perm = (ranks.index(real_gap) + 1) / (len(ranks))
print(f"permutation p ≈ {p_perm:.3f} (real effect should sit in the tail)")
If the real region's gap is the most extreme (or near it) among all placebos, the burst effect is unlikely to be an artifact. If a placebo donor shows an equally large "effect", your model is picking up noise - tighten the donor pool or the pre-period.
This week ◐ 40 min total
- Swap the donor pool. Drop your two highest-weight donors and refit. Watch the estimate and the pre-period fit move - this is how sensitive SCM is to the pool, and why donor choice is a decision you must defend.
- Placebo on an untreated region. Pick a donor that got no burst, treat it as the treated unit, and run the full pipeline. The effect should be near zero - if it is not, your model is over-fitting.
- Compare the two engines. Put the CausalImpact cumulative lift and the pysyncon ATT side by side. Where they disagree, explain why (BSTS priors vs convex donor weights).
- Optional: re-run with a deliberately contaminated donor (add back the adjacent region) and show how spillover shrinks the measured effect - the SUTVA violation, quantified.
Three questions before you go 🎯 ◐ 90 seconds
1 · In the synthetic control method, the donor weights are chosen to...
Weights (non-negative, summing to 1) are fit on the pre-period so the synthetic twin tracks the treated unit before treatment. The post-period gap is then read as the effect - a tight pre-fit is the credibility test.
2 · A placebo test in SCM works by...
Permutation inference: refit SCM treating each untreated donor as the treated unit to build a distribution of placebo effects. If the genuine treated region's gap sits in the tail, the effect is unlikely to be noise.
3 · Which statement is correct about the tooling?
GeoLift is Meta's and built on SCM. Google's lineage is Vaver-Koehler geo experiments plus Brodersen's CausalImpact (Bayesian structural time series). There is no "Google GeoLift".
What this session covers
Geo + synthetic control is a genuine gap on every platform we surveyed - we teach it here from the primary sources. The deep econometric proofs stay in the papers and the Udemy econometrics course.