Skip to main content

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 treated unit indices and the treat_time (first post-treatment column).
  • Staggered adoption (DiD family): a per-unit treat_start array giving each unit's first-treated period; use -1 or None for 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):

SC fit time vs panel size

The per-fit win compounds under inference — a full in-space placebo test runs one fit per donor (here 200), multithreaded:

panelkit vs reference wall-clock

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

panelkit-0.1.0.tar.gz (86.4 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

panelkit-0.1.0-cp39-abi3-win_amd64.whl (276.1 kB view details)

Uploaded CPython 3.9+Windows x86-64

panelkit-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (436.6 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

panelkit-0.1.0-cp39-abi3-macosx_11_0_arm64.whl (370.0 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

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

Hashes for panelkit-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a259d5648003b10a01e9458db37a4781aa0988083262d7b06179207312284eaa
MD5 68c5da00cab362e83dbf121b7450c33f
BLAKE2b-256 85c731debae4188076e2109d7038dc6b1bd53549093fe827599e8b463260fd33

See more details on using hashes here.

Provenance

The following attestation bundles were made for panelkit-0.1.0.tar.gz:

Publisher: release.yml on cacoleman16/panelkit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for panelkit-0.1.0-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 c37173d9ed7e6d46becdfc4fc455e06f3bf5a1c2c38925337cac4e267f903731
MD5 d9b5ced1bdfa977b70c1fef02ce1b7c2
BLAKE2b-256 41e0c6d1f94b829a80006d2ed00f80da167959a027521ee9aeba73064bbb7c3d

See more details on using hashes here.

Provenance

The following attestation bundles were made for panelkit-0.1.0-cp39-abi3-win_amd64.whl:

Publisher: release.yml on cacoleman16/panelkit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file panelkit-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for panelkit-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 0469039cf26114d9d503b65d3644081990bfec2e764d9666051d7a304b5145a7
MD5 f307e2c226db9dfb9764ea69b638899b
BLAKE2b-256 548aaa1b15b56a7b9bb290a82234479939d202fbd3b8064282130e8f9d35a7d6

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file panelkit-0.1.0-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for panelkit-0.1.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 d239cf7ac00f5b9c5b044ce355f461ff907b7e86c00aee55cc541fbd1a378ead
MD5 d26e4298a3f3ece8f7e464c5bcd8a131
BLAKE2b-256 5742b46c4684de55780eb8718467ff3594f28a85c3cf50f751f722f467139dd2

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page