Skip to main content

okunfa

Synthetic control from scratch, verified against the paper that defined the method, shipped as a library.

pip install okunfa

Okunfa is Yoruba for cause. The question causal inference keeps asking is "compared to what?", and the synthetic control method (Abadie, Diamond, Hainmueller 2010) gives the most transparent answer in the toolbox: build the counterfactual as a weighted average of untreated units, weights non-negative and summing to one, chosen to match the treated unit before the intervention. The counterfactual comes with an ingredients list you can argue with.

I implemented the full estimator on numpy and scipy (no causal libraries anywhere), then pointed it at the method's founding result: California's Proposition 99 tobacco program.

The reproduction

The ghost

ADH 2010, Table 2, says synthetic California is Utah .334, Nevada .234, Montana .199, Colorado .164, Connecticut .069. This implementation, run on the same public panel with the same predictors, finds:

state published okunfa
Utah 0.334 0.335
Nevada 0.234 0.236
Montana 0.199 0.201
Colorado 0.164 0.160
Connecticut 0.069 0.068

All five within 0.005, every other state at zero, and the headline effect lands where the paper put it: California's cigarette sales about 26 packs per capita below its ghost by 2000. A test pins each published weight to within 0.01, so the reproduction cannot silently rot.

The animation

(Source: assets/manim_recipe.py, video in assets/recipe.mp4.)

The inference is the honest part

A synthetic control without placebos is just a curve that goes down. The method's own significance test is to run the identical procedure on every state that never passed the law:

The placebo storm

One in thirty-nine

California's post/pre RMSPE ratio is the most extreme of all 39 states, a permutation p-value of 1/39 = 0.026, matching the paper's inference exactly. Two more checks that the story is structural, not lucky:

Stress tests

Leave-one-out refits drop each ingredient state in turn (the gap survives losing any single leg), and an in-time placebo pretends the law passed in 1980 using only pre-1980 information (the design finds almost nothing where nothing happened).

The estimator

The paper's nested optimization, implemented directly:

  • Inner problem: given predictor importances V, the weights solve a small quadratic program on the simplex, min (X1 - X0 w)' V (X1 - X0 w), by SLSQP with an analytic gradient.
  • Outer problem: V itself is chosen to minimize pre-period outcome MSPE, by Nelder-Mead over a softmax parameterization, so every candidate V is a valid distribution by construction, with multiple restarts because the surface is not convex.

The chosen V is itself interpretable, and slightly deflating: the data puts 87% of the matching weight on retail price and most of the rest on the 1975 sales lag. The covariates everyone lists (income, beer, age structure) barely matter once the outcome lags are in, which is a known property of the method worth seeing with your own eyes.

Using it

from okunfa import SyntheticControl, in_space_placebos, permutation_pvalue
from okunfa.datasets import load_prop99, adh_design

panel, years = load_prop99()      # fetches the public panel on demand
design = adh_design(panel, years)

sc = SyntheticControl()
fit = sc.fit(**design)
print(fit.nonzero())              # [('Utah', 0.335), ('Nevada', 0.236), ...]
print(fit.gap[-1])                # about -26 packs in 2000

fits = in_space_placebos(sc, **design)
p, ratios = permutation_pvalue(fits, "California")
print(p)                          # 0.026

The API takes plain dicts and numpy arrays (unit -> outcome path, predictor -> unit -> value), so it works on any panel, not just this one.

Honest limits

  • Permutation inference with 39 units cannot say anything smaller than p = 1/39. That is a property of the design, not a bug, and it is why the paper reports exactly this number.
  • The outer optimization is non-convex; different software finds slightly different V (mine concentrates it more than the paper's table shows) while landing on the same weights and the same gap. The reproduction target is the weights and the effect, which are the identified objects.
  • Synthetic control needs a long pre-period, a treated unit inside the donor convex hull, and no spillovers onto donors. None of that is checked for you; the placebo and leave-one-out tools exist so you check it.
  • One fit takes about three minutes on my 2014 CPU (the placebo sweep is 39 of them). SLSQP in the inner loop is the cost of writing it plainly.

Reproduce it

python -m venv .venv && .venv/bin/pip install numpy scipy pytest
bash data/get_data.sh                       # or let the loader fetch it

.venv/bin/python -m pytest tests/ -q        # 6 contracts
.venv/bin/python benchmarks/repro_prop99.py # all stages, resumable
.venv/bin/python assets/make_visuals.py     # the four figures

Layout

Papers

  • Abadie, Diamond, Hainmueller (2010), Synthetic Control Methods for Comparative Case Studies: Estimating the Effect of California's Tobacco Control Program, JASA. The method and the target result.
  • Abadie & Gardeazabal (2003), The Economic Costs of Conflict. Where the idea started.
  • Abadie (2021), Using Synthetic Controls: Feasibility, Data Requirements, and Methodological Aspects, JEL. The honest-limits survey.

Metadata

Release files for okunfa 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for okunfa 0.1.1
File Size Uploaded
okunfa-0.1.1.tar.gz 13.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for okunfa 0.1.1
File Interpreter ABI Platform
okunfa-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 23.8 kB

Release files / okunfa-0.1.1.tar.gz

Download URL okunfa-0.1.1.tar.gz
Size 13.5 kB
Tags Source
SHA-256 checksum
How to use checksums
592ff42ac50a5099980e09aa8384e1021e1fc470f56319398fb04250b5055905
BLAKE2b-256 checksum
How to use checksums
21fba8ea58981e13b787efe8b098f5e9e642263ae229d223dd4c01cfb4377d5d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.11

Release files / okunfa-0.1.1-py3-none-any.whl

Download URL okunfa-0.1.1-py3-none-any.whl
Size 10.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1440f30c19967934ef10450f10a1cc58aa3e8be737c6374c6e5665a2a95b2e47
BLAKE2b-256 checksum
How to use checksums
3c78349b81776242fbbd7615be15546b7af53b3e3e030f0862a132f8ed4ec3d9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.11

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page