Metadata-Version: 2.4
Name: okunfa
Version: 0.1.1
Summary: Synthetic control from scratch: the ADH estimator with the placebo inference that makes it honest
Author-email: Kenny Obidele <obidelek19@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/Kenny0bi/okunfa
Project-URL: Issues, https://github.com/Kenny0bi/okunfa/issues
Keywords: causal-inference,synthetic-control,econometrics,policy-evaluation,placebo-tests
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: numpy>=1.24
Requires-Dist: scipy>=1.10

# 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](https://raw.githubusercontent.com/Kenny0bi/okunfa/main/assets/ghost.svg)

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](https://raw.githubusercontent.com/Kenny0bi/okunfa/main/assets/recipe.gif)

(Source: [assets/manim_recipe.py](https://raw.githubusercontent.com/Kenny0bi/okunfa/main/assets/manim_recipe.py), video in
[assets/recipe.mp4](https://raw.githubusercontent.com/Kenny0bi/okunfa/main/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](https://raw.githubusercontent.com/Kenny0bi/okunfa/main/assets/storm.svg)

![One in thirty-nine](https://raw.githubusercontent.com/Kenny0bi/okunfa/main/assets/rank.svg)

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](https://raw.githubusercontent.com/Kenny0bi/okunfa/main/assets/stress.svg)

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

```python
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

```bash
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

- [okunfa/core.py](https://github.com/Kenny0bi/okunfa/blob/main/okunfa/core.py) the nested estimator: simplex QP
  inside, Nelder-Mead over V outside
- [okunfa/inference.py](https://github.com/Kenny0bi/okunfa/blob/main/okunfa/inference.py) in-space placebos,
  permutation p-values, leave-one-out, in-time placebo
- [okunfa/datasets.py](https://github.com/Kenny0bi/okunfa/blob/main/okunfa/datasets.py) the Prop 99 panel and the
  exact ADH predictor design
- [benchmarks/repro_prop99.py](https://github.com/Kenny0bi/okunfa/blob/main/benchmarks/repro_prop99.py) the
  reproduction, staged and resumable, everything into repro.json

## 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.
