scspill — conceptual spillover network above dark-themed observed-versus-synthetic California and donor spillover plots

scspill — Synthetic control models with spillover effects, in Python

Synthetic control when the treatment leaks: estimate the policy effect and the spillover received by every donor.

scspill is a Python package of synthetic control models with spillover effects. Classical synthetic control assumes the donors are untouched by the treatment; when a policy spills across borders or trade links, that assumption fails and the estimate is biased. Every model here drops it, and every model returns two things classical synthetic control cannot both give you: the effect on the treated unit, purged of contamination, and the spillover effect received by each donor.

Two models ship today, behind one configuration and one result type:

The model catalogue compares them, names the models on the roadmap, and marks plainly which are implemented.

🎯 Estimate

Pick a model, configure once, fit once. SCSPILL(config).fit() returns the ATT, the counterfactual path, and a time-by-donor spillover panel for either model. sar runs a two-step Bayesian sampler; cd fits an SCM per unit and tests any spillover hypothesis.

🔬 Validate

sar ships its article’s appendix machinery as first-class functions: the Geweke joint distribution test of the sampler, prior sensitivity sweeps, and prior predictive checks against nine statistics of the donor panel.

🎲 Simulate

sar’s Monte Carlo engine: plant \(\rho\) and the treatment effect on a lattice, watch classical SCM go wrong under spillovers, and score bias, RMSE, and coverage across estimators.

The models

Model method Reference Status
Bayesian spatial-autoregressive spillover SCM "sar" Sakaguchi & Tagawa (2026), Econometrics Journal Available
Synthetic control under a known spillover structure "cd" Cao & Dowd, arXiv:1902.07343v3 Available
Inclusive synthetic control "iscm" Di Stefano & Mellace (2024) Planned
Partial-interference SCG "grossi" Grossi et al. (2025), JRSS-A Planned

The remaining planned rows are a roadmap. Their method names are rejected. The model catalogue explains what each would assume, and why having several matters — the assumptions are not nested, so two models bracket the answer.

What a sar fit needs and returns

The treated unit is matched by unconstrained, horseshoe-shrunk synthetic weights \(\alpha\) (donors can be excluded, weighted negatively, or extrapolated — no simplex constraint). Spillovers travel through weights you specify: spatial_w links the treated unit to each donor, spatial_W links donors to each other, and a single scalar \(\rho\) — estimated by random-walk Metropolis inside a Gibbs sampler — scales the whole channel. At \(\rho = 0\) the method collapses exactly to the Bayesian horseshoe synthetic control.

You provide scspill returns
a long panel df (one row per unit-period) att, att_ci — the treatment effect with credible interval
a 0/1 treatment column the counterfactual path with a pointwise credible band
spatial_w, spatial_W — spatial weights by donor label the posterior of \(\rho\) (draws, interval, ESS, acceptance)
optional covariate columns a \((T \times N)\) spillover panel with credible bands per donor
MCMC diagnostics and R-style plot panels

What a cd fit needs and returns

Every unit gets an intercept-adjusted simplex synthetic control. You state which controls the treatment can reach, and how:

  • named units with free effects (affected_units);
  • one effect shared by a group (spillover_structure="homogeneous");
  • a spillover decaying with distance ("distance_decay");
  • any exposure matrix ("custom").

The model separates the direct effect from each spillover in every post-treatment period.

You provide scspill returns
a long panel df and a 0/1 treatment column (one or several treated units) effects_panel — every unit’s effect by period
the exposed units, or a structure effect_ci, pointwise_p_values — pointwise intervals and P tests
test_hypothesis(C) — any linear hypothesis, e.g. “no spillover anywhere”
kappa_test, select_structure_by_kappa() — is the structure right?
cd.sensitivity — how much a missed spillover could bias the result

Bundled case studies

  • California Proposition 99 — 39 states, 1970–2000, per-capita cigarette sales, rook-contiguity weights (scspill.data.load_california()): the sar paper’s tobacco-tax application, where the spillover lands on Nevada.
  • Proposition 99 with a known structure — Cao & Dowd’s 50 states plus DC, 1970–2000, with the 13 exposed states (scspill.data.load_california_cd()). The cd model finds leakage into Nevada, and the page sets it beside sar.
  • The 2011 Sudan secession — 34 African countries, 2000–2015, GDP per capita, bilateral-trade weights (scspill.data.load_sudan()): spillovers travel the trade network to Egypt and Kenya.

Installation

pip install scspill            # NumPy/SciPy backend
pip install "scspill[numba]"   # + JIT-compiled samplers (~10x faster)

At a glance

from scspill import SCSPILL
from scspill.data import load_california

panel = load_california()
result = SCSPILL({**panel.config_kwargs(), "m_iter": 20_000, "burn": 10_000,
                  "seed": 42}).fit()

result.att, result.att_ci          # treatment effect on California
result.rho_hat, result.rho_ci      # spillover intensity
result.spillover_panel["Nevada"]   # the effect received by Nevada
result.plot(kind="panel")          # counterfactual | effect | top spillovers
from scspill.data import load_california_cd

cd = SCSPILL(load_california_cd(51).config_kwargs()).fit()   # method="cd"
cd.effects_panel[["CA", "NV"]]     # direct effect and Nevada's spillover, by year
cd.effect_ci["NV"]                 # their pointwise 95% intervals
cd.plot(kind="spillover")          # every exposed state's effect

Validated, not just implemented

Every release is cross-validated against the authors’ R replication package (see the validation article and benchmarks/REPORT.md in the repository): the California and Sudan posteriors are matched against the frozen R credible intervals, the Monte Carlo grid against the paper’s Tables 1–2, and the sampler itself passes the Geweke joint distribution test — which also caught, and led us to fix, two internal inconsistencies in the reference implementation’s latent-factor block.

cd is checked against three independent sources (see Replicating Cao & Dowd):

  • the paper: the significance of the early years, and Figure 3’s break-even points 17.07 and 9.66;
  • the authors’ code: every P value of their Proposition 99 output, and their simulation statistics;
  • mlsynth’s implementation: to solver tolerance.

Built on

numpy · scipy · pandas · pydantic · matplotlib · optional numba — and designed to sit beside mlsynth, whose estimator architecture it shares.

Citing

If you use scspill, please cite it as:

Mendez, C., Sakaguchi, S., & Tagawa, H. (2026). Synthetic Control Models with Spillovers in Python (version 0.3.0). https://github.com/quarcs-lab/scspill

Authors: Carlos Mendez · Shosei Sakaguchi · Hayato Tagawa

CITATION.cff in the repository carries the same metadata in machine-readable form.

Acknowledgement

The library’s design, syntax, and infrastructure are based on Jared Greathouse’s mlsynth, a larger and more comprehensive package of synthetic control methods. The documentation stack follows geometrics, from the QuaRCS lab, where scspill is developed. The sar model is cross-validated against the MIT-licensed replication package for the article it implements.