When two packages disagree about a model they both implement, check the equation before you check the sampler.
Drop SUTVA and the bias has a closed form
\[Y_{1t} - \sum_j \alpha_j Y_{jt} \;=\; \underbrace{\xi_{0t}}_{\text{what we want}}
\;-\; \underbrace{\sum_j \alpha_j \xi^{c}_{jt}}_{\text{what we get for free}}\]
The bias is a product of two things: how much weight a donor carries, and how contaminated it is.
A filthy donor with zero weight is harmless. A slightly dirty one carrying half the counterfactual is not.
No \(\beta\). No factors. No error variances. They cancel.
The effects depend on \((\alpha, \rho, \mathbf{w}, W)\) and the observed data. Nothing else.
So the model’s weak identification has exactly one address: \(\rho\).
Weak identification you can locate is a manageable problem. Weak identification spread across a nuisance block is not.
One call runs both samplers and post-processes the spillovers
result = SCSPILL({**panel.config_kwargs(),"m_iter": 500_000,"burn": 250_000,"seed": 20251022,}).fit()result.att # −16.87result.effects_detail.att_scm # −15.68, the ρ = 0 comparator, freeresult.rho_hat # 0.316result.spillover_panel # 31 × 38: who else was treated
Stage 2 comes out of the Stage-3 fit at no extra cost. The model is nested.
The spatial parameter is clearly above zero
0.316
\(\widehat{\rho}\), 95% CrI [0.231, 0.403]. The interval excludes zero. The data reject \(\rho = 0\) — and with it, SUTVA on the donor pool.
The two parameters leaning on one contiguity channel are the hard ones
Parameter
Effective sample size (from 250,000 draws)
\(\sigma^2\)
204,095
\(\alpha\) (donor weights)
10,000 – 26,000
\(\beta\) (retail price)
388
\(\rho\)
137
\(\rho\) is the only parameter drawn by random-walk Metropolis rather than from a closed-form conditional, so consecutive draws are heavily correlated. The posterior itself is tight — SD 0.043 on a support 1.9 wide.
Slow mixing is not weak identification, and neither is a defect in the software.
Almost the entire spillover lands on one state
Nevada −5.50 · Idaho −0.49 · Utah −0.49 · every other donor under 0.06 packs.
The leak runs the opposite way to the obvious hypothesis
−5.50
Nevada’s sales came in below its no-treatment path. That looks like the campaign crossing the border, not the cigarettes — but the estimate cannot name the mechanism.
Which means the classical estimate was biased toward zero
Donor
\(\alpha_j\)
\(\xi^{c}_j\)
\(\alpha_j \xi^{c}_j\)
Nevada
0.200
−5.50
−1.098
Utah
0.036
−0.49
−0.018
Idaho
0.012
−0.49
−0.006
Sum (all 38 donors)
−1.130
Purged minus contaminated: −1.186. The identity holds to 0.06 packs.
The residual is bookkeeping: the plug-in uses \(\widehat{\alpha}\); the purged ATT averages over paired draws.
Negative spillovers on positively-weighted donors push the estimate toward zero. Modelling the leak makes the effect larger.
The R edition’s interval is 33 times too narrow — and the R specification reproduces that exactly
Back in the R specification: ATT −16.286 against −16.590, \(\widehat{\rho}\) 0.2282 against 0.2226, effective sample size 3.27 against 2.93.
Independent code, different language, reproducing the pathology — including an effective sample size of 3.
Chain length was never the problem — the interval was incomplete
Specification
Iterations
Width
ESS(\(\rho\))
scspill, R spec
5,000
0.482
3.3
scspill, R spec
500,000
0.702
66.9
scspill, corrected
500,000
12.713
136.8
A hundred times the iterations widens the interval by 45%. Propagating \(\alpha\) widens it by 1,700%.
Effective sample size asks whether the interval is reliable. propagate_alpha asks whether it is complete. The published interval failed both.
Report the effective sample size beside every credible interval, or the interval is decoration.
A memory layout was doing part of the modelling
Departure 1: the covariate array was indexed as \((N, T, K)\) when it was laid out as \((T, N, K)\).
Nothing crashes. Nothing looks wrong. Each state’s price is silently matched to another state’s sales.
It is also the one departure with no escape hatch worth using.
The only way out is to drop the covariates — one specification error for another.
Departures 5 and 6 were different: nobody spotted them. A Geweke joint-distribution test did.
The most dangerous bugs are the ones that return plausible numbers.
Artefacts shrink; errors do not
max \(|z|\) falls 3.48 → 2.50 and rejections go 1 → 0 as the chain grows tenfold. That is slow mixing, not an incoherent conditional.
The only prior that moves the answer is the one nobody calls a prior
Sweeping \(a_0\), \(b_0\) and the step size moves \(\rho\) by 0.07. Truncating the support moves it by 0.32.
The strongest objection — and the answer
Objection. The SAR layer does not make anything causal that was not causal before. You supplied the graph; a different graph gives different spillovers.
Response. Correct, and the post says so. But the alternative is not “no assumption” — it is \(\rho = 0\), imposed silently and never reported. Making the assumption a parameter is what lets the data reject it.
The choice is not between assuming and not assuming. It is between an assumption you can test and one you cannot.
What Survives Every Relaxation
Act III
Every stage of the ladder agrees on the sign and the scale
Two libraries, four prior structures, one spatial layer, one independent third implementation.
What Proposition 99 cost, and who else paid
−16.87
packs per capita per year for California — and −5.50 for a state that never voted on it
Four things survive this deck
The effect is robust — −15.7 to −18.8 packs, every relaxation.
The donor pool is not — 5 donors or 26, depending on the constraint.
SUTVA is false here — 1.19 packs for California, 5.50 for Nevada.
The interval was the real error, not the point estimate.
Everything here is runnable today
Full tutorial — carlos-mendez.org/post/python_sc_bayes_spatial
R edition — carlos-mendez.org/post/r_sc_bayes_spatial
Run it — Colab notebook, or the Quarto bundle with a hermetic .venv
Web app — interactive \(\rho\) slider, donor comparator, trace diagnostics
Data dictionary — the panel and both spatial objects