Fast, from-scratch causal-inference estimators for panel/geo experiments (SC, ASC, SDID, DiD, MC-NNM).
Project description
panelkit
Fast, from-scratch causal-inference estimators for panel / geo experiments — written in Rust, exposed to Python.
panelkit reimplements the standard panel causal-inference toolbox on top of its
own dependency-free numerical core (no BLAS/LAPACK, no ndarray, no rand), so
the whole stack is self-contained, deterministic, and fast. The numerical core
(panelkit-linalg) is a standalone crate intended to also back sibling projects
(e.g. a future time-series library).
- Fast: ~45–70× a NumPy+SciPy synthetic control on a single fit, ~1400× on a full placebo test (multithreaded). Details.
- Self-contained: the numerical core is hand-written — matmul, Cholesky, QR, a one-sided Jacobi SVD, simplex solvers, and a PRNG, with zero numeric deps.
- Reproducible: all resampling inference is bit-identical regardless of thread count (deterministic per-replicate seed substreams).
- Modern: correct under staggered adoption (Callaway-Sant'Anna, Sun-Abraham) with a Goodman-Bacon diagnostic, plus a novel conformal-pooled SC family.
Install
pip install panelkit # once published; until then, build from source:
# from a clone (needs a Rust toolchain — https://rustup.rs — and maturin):
pip install maturin numpy
maturin develop --release --manifest-path crates/pypanelkit/Cargo.toml
Data model
Every estimator takes an N × T NumPy array Y (rows = units, columns = time
periods). Treatment is specified one of two ways:
- Block treatment (SC family, MC-NNM, CP-ASC): a list of
treatedunit indices and thetreat_time(first post-treatment column). - Staggered adoption (DiD family): a per-unit
treat_startarray giving each unit's first-treated period; use-1orNonefor never-treated units.
Estimators at a glance
| class | method | treatment | best for |
|---|---|---|---|
SyntheticControl |
Abadie et al. 2010 | block | one/few treated units, transparent weights |
AugmentedSC |
Ben-Michael et al. 2021 | block | poor pre-fit (ridge bias correction) |
SyntheticDiD |
Arkhangelsky et al. 2021 | block | robust general default |
MCNNM |
Athey et al. 2021 | block | low-rank structure, many treated cells |
CPASC |
novel (this project) | block, multi-treated | conservative pooled inference, cumulative $ lift |
TWFE |
two-way FE | staggered | baseline (biased under heterogeneity) |
CallawaySantAnna |
Callaway-Sant'Anna 2021 | staggered | staggered adoption, event study |
SunAbraham |
Sun-Abraham 2021 | staggered | staggered event study (saturated) |
GoodmanBacon |
Goodman-Bacon 2021 | staggered | diagnostic: why TWFE is biased |
Examples
Synthetic Control (+ placebo inference)
import numpy as np
from panelkit import SyntheticControl
# Y: 50 units × 60 periods; unit 0 treated from period 45.
res = SyntheticControl(inference="placebo").fit(Y, treated=[0], treat_time=45)
res.att # average post-treatment effect
res.att_path # per-period effects (length T_post)
res.counterfactual # synthetic control's predicted path
res.weights # donor weights (on the simplex)
res.donor_ids # which units those weights correspond to
res.p_value # in-space placebo p-value
print(res.summary())
Synthetic DiD — the robust default
from panelkit import SyntheticDiD
res = SyntheticDiD().fit(Y, treated=[0], treat_time=45)
print(res.att) # unit + time weighted 2×2 DiD
Augmented SC and MC-NNM
from panelkit import AugmentedSC, MCNNM
AugmentedSC().fit(Y, treated=[0], treat_time=45).att # ridge-corrected SC
MCNNM().fit(Y, treated=[0], treat_time=45).att # low-rank completion, λ by CV
CP-ASC — conformal pooled SC (multiple treated units)
from panelkit import CPASC
treated = [0, 1, 2, 3, 4, 5]
res = CPASC(mode="mspe").fit(Y, treated, treat_time=22) # CP-ASC
res.att # empirical-Bayes pooled ATT
res.p_value # conformal block-permutation p-value
res.unit_att # per-unit effects
res.unit_weight # inverse-MSPE pooling weights
CPASC(mode="stratified").fit(Y, treated, 22) # Strat-CP-ASC (size-robust)
CPASC(mode="cumulative").fit(Y, treated, 22) # C-AS-CP-ASC (total-dollar target)
Difference-in-differences with staggered adoption
from panelkit import TWFE, CallawaySantAnna, SunAbraham, GoodmanBacon
# treat_start[i] = first treated period for unit i, or -1 if never treated.
cs = CallawaySantAnna().fit(Y, treat_start)
cs.att # overall ATT (cohort-size weighted)
cs.event_time # relative event times, e.g. [-5,...,-1, 0, 1,...]
cs.event_att # event-study coefficients (clean pre-trends + dynamics)
cs.event_se # influence-function standard errors
print(cs.summary())
sa = SunAbraham().fit(Y, treat_start) # interaction-weighted event study
twfe = TWFE().fit(Y, treat_start) # baseline; biased under heterogeneity
# Goodman-Bacon: why TWFE is biased — decompose it into 2×2 comparisons.
bacon = GoodmanBacon().fit(Y, treat_start)
bacon.twfe # == TWFE coefficient (Σ weightᵢ · estimateᵢ)
bacon.forbidden_weight # weight on "already-treated as control" comparisons
print(bacon.summary())
Runnable scripts live in examples/: sc_demo.py, did_demo.py,
cpasc_demo.py. See GUIDE.md for the estimand, assumptions, and
valid inference for each estimator.
Inference
| engine | use | determinism |
|---|---|---|
| placebo / permutation (in-space) | SC family, small N treated | order-independent |
| jackknife (leave-one-out) | SDID, N treated ≥ 2 | order-independent |
| multiplier (wild) bootstrap | C&S / SA influence functions | seeded substreams |
| conformal block permutation | CP-ASC, single-unit counterfactuals | order-independent |
All bootstrap/permutation engines produce bit-identical results regardless
of RAYON_NUM_THREADS, because replicate b always draws from
Xoshiro256pp::substream(seed, b). Verified in CI at 1 and 8 threads.
Performance
From-scratch Rust + multithreaded inference vs the textbook NumPy + SciPy-SLSQP
synthetic control. Numbers below are measured live by
benchmarks/make_plots.py (median over 5 panels per
size, Apple M4 Pro; absolute times vary by hardware — re-run to regenerate).
A single synthetic-control fit, across donor-pool sizes (log scale):
The per-fit win compounds under inference — a full in-space placebo test runs one fit per donor (here 200), multithreaded:
| task (200 × 130 panel) | panelkit | NumPy + SciPy-SLSQP | speedup |
|---|---|---|---|
| single SC fit | ~2.0 ms | ~120 ms | ~60× |
| full placebo (200 fits) | ~0.056 s | ~82 s | ~1467× |
Estimates are identical (ATT |Δ| ≈ 1e-11; same placebo p-value) — this is pure implementation speed, not an approximation. panelkit is also far steadier: SciPy SLSQP has occasional convergence cliffs on near-collinear donor panels (one took 9.5 s in testing), which is why the table reports the median typical case. See BENCHMARKS.md.
Architecture
A four-crate Cargo workspace with a strict dependency DAG:
linalg ← estimators ← inference ← pypanelkit (PyO3 bindings)
Only pypanelkit touches Python; linalg depends on nothing but std.
| crate | role |
|---|---|
panelkit-linalg |
Mat, GEMM/QR/Cholesky, one-sided Jacobi SVD, simplex solvers, SVT, RNG |
panelkit-estimators |
the estimators above, as functions of a Panel |
panelkit-inference |
resampling engines |
pypanelkit |
the panelkit._panelkit extension module |
Development
cargo test --workspace # Rust tests (incl. SVD cross-oracle)
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check
maturin develop --release --manifest-path crates/pypanelkit/Cargo.toml
pytest python/tests
cargo bench -p panelkit-estimators # criterion micro-benchmarks
License
MIT OR Apache-2.0.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file panelkit-0.1.0.tar.gz.
File metadata
- Download URL: panelkit-0.1.0.tar.gz
- Upload date:
- Size: 86.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a259d5648003b10a01e9458db37a4781aa0988083262d7b06179207312284eaa
|
|
| MD5 |
68c5da00cab362e83dbf121b7450c33f
|
|
| BLAKE2b-256 |
85c731debae4188076e2109d7038dc6b1bd53549093fe827599e8b463260fd33
|
Provenance
The following attestation bundles were made for panelkit-0.1.0.tar.gz:
Publisher:
release.yml on cacoleman16/panelkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
panelkit-0.1.0.tar.gz -
Subject digest:
a259d5648003b10a01e9458db37a4781aa0988083262d7b06179207312284eaa - Sigstore transparency entry: 1709020704
- Sigstore integration time:
-
Permalink:
cacoleman16/panelkit@15059d1c2d618c60c5ae3e9f113e6fe35b6b476c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/cacoleman16
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@15059d1c2d618c60c5ae3e9f113e6fe35b6b476c -
Trigger Event:
push
-
Statement type:
File details
Details for the file panelkit-0.1.0-cp39-abi3-win_amd64.whl.
File metadata
- Download URL: panelkit-0.1.0-cp39-abi3-win_amd64.whl
- Upload date:
- Size: 276.1 kB
- Tags: CPython 3.9+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c37173d9ed7e6d46becdfc4fc455e06f3bf5a1c2c38925337cac4e267f903731
|
|
| MD5 |
d9b5ced1bdfa977b70c1fef02ce1b7c2
|
|
| BLAKE2b-256 |
41e0c6d1f94b829a80006d2ed00f80da167959a027521ee9aeba73064bbb7c3d
|
Provenance
The following attestation bundles were made for panelkit-0.1.0-cp39-abi3-win_amd64.whl:
Publisher:
release.yml on cacoleman16/panelkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
panelkit-0.1.0-cp39-abi3-win_amd64.whl -
Subject digest:
c37173d9ed7e6d46becdfc4fc455e06f3bf5a1c2c38925337cac4e267f903731 - Sigstore transparency entry: 1709020761
- Sigstore integration time:
-
Permalink:
cacoleman16/panelkit@15059d1c2d618c60c5ae3e9f113e6fe35b6b476c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/cacoleman16
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@15059d1c2d618c60c5ae3e9f113e6fe35b6b476c -
Trigger Event:
push
-
Statement type:
File details
Details for the file panelkit-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: panelkit-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 436.6 kB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0469039cf26114d9d503b65d3644081990bfec2e764d9666051d7a304b5145a7
|
|
| MD5 |
f307e2c226db9dfb9764ea69b638899b
|
|
| BLAKE2b-256 |
548aaa1b15b56a7b9bb290a82234479939d202fbd3b8064282130e8f9d35a7d6
|
Provenance
The following attestation bundles were made for panelkit-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on cacoleman16/panelkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
panelkit-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
0469039cf26114d9d503b65d3644081990bfec2e764d9666051d7a304b5145a7 - Sigstore transparency entry: 1709020873
- Sigstore integration time:
-
Permalink:
cacoleman16/panelkit@15059d1c2d618c60c5ae3e9f113e6fe35b6b476c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/cacoleman16
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@15059d1c2d618c60c5ae3e9f113e6fe35b6b476c -
Trigger Event:
push
-
Statement type:
File details
Details for the file panelkit-0.1.0-cp39-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: panelkit-0.1.0-cp39-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 370.0 kB
- Tags: CPython 3.9+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d239cf7ac00f5b9c5b044ce355f461ff907b7e86c00aee55cc541fbd1a378ead
|
|
| MD5 |
d26e4298a3f3ece8f7e464c5bcd8a131
|
|
| BLAKE2b-256 |
5742b46c4684de55780eb8718467ff3594f28a85c3cf50f751f722f467139dd2
|
Provenance
The following attestation bundles were made for panelkit-0.1.0-cp39-abi3-macosx_11_0_arm64.whl:
Publisher:
release.yml on cacoleman16/panelkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
panelkit-0.1.0-cp39-abi3-macosx_11_0_arm64.whl -
Subject digest:
d239cf7ac00f5b9c5b044ce355f461ff907b7e86c00aee55cc541fbd1a378ead - Sigstore transparency entry: 1709020817
- Sigstore integration time:
-
Permalink:
cacoleman16/panelkit@15059d1c2d618c60c5ae3e9f113e6fe35b6b476c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/cacoleman16
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@15059d1c2d618c60c5ae3e9f113e6fe35b6b476c -
Trigger Event:
push
-
Statement type: