Synthetic Difference-in-Differences (Arkhangelsky et al. 2021): a complete Python port of the reference R package
Project description
synthdid-py
Synthetic Difference-in-Differences for Python — a complete port of the reference R package
synthdid by Arkhangelsky, Athey, Hirshberg,
Imbens and Wager.
Everything the R package does, this does: the estimator, the Frank-Wolfe solvers, covariate adjustment, all three variance estimators, the placebo diagnostic, and the full set of plots. Plus staggered adoption, which the R package does not support.
Point estimates agree with R to the fifth decimal on the canonical example.
日本語版の README は README.ja.md にあります。
Install
pip install synthdid-py
Plots need matplotlib:
pip install "synthdid-py[plot]"
Quickstart
from synthdid import load_california_prop99, panel_matrices, synthdid_estimate
panel = panel_matrices(load_california_prop99()) # long panel -> Y, N0, T0, W
tau = synthdid_estimate(panel.Y, panel.N0, panel.T0,
unit_names=panel.units, time_labels=panel.time)
print(tau)
# synthdid_estimate: -15.6038. Effective N0/N0 = 16.4/38. Effective T0/T0 = 2.8/19. N1,T1 = 1,12.
print(tau.se(method="placebo")) # ~9.8 (California is the only treated unit)
print(tau.ci(method="placebo")) # ~(-34.8, 3.6)
tau is a float — compare it, add it, put it in a DataFrame — that also carries
.weights, .setup and .opts.
tau.summary() # estimate, se, effective sample sizes, leading weights
tau.controls() # the control units that carry the weight, sorted
tau.effect_curve() # the per-period effects that average to the estimate
tau.placebo() # refit on pre-treatment data only
tau.plot() # the trajectory + 2x2 diagram figure
Verified against the R implementation
Run on california_prop99 (39 states, 1970-2000, California treated from 1989):
| estimator | this package | R synthdid 0.0.9 |
|---|---|---|
synthdid_estimate |
−15.60383 | −15.60383 |
sc_estimate |
−19.61966 | −19.61966 |
did_estimate |
−27.34911 | −27.34911 |
The test suite also checks, on synthetic panels, every invariance the R package tests for (unit and time fixed effects, rescaling, block shifts), and validates the Frank-Wolfe solver against an independent projected-gradient solver with a duality-gap optimality certificate.
pytest # 108 tests
The three estimators
from synthdid import synthdid_estimate, sc_estimate, did_estimate
sdid = synthdid_estimate(Y, N0, T0) # unit weights AND time weights
sc = sc_estimate(Y, N0, T0) # unit weights only (synthetic control)
did = did_estimate(Y, N0, T0) # uniform weights (difference-in-differences)
Y is an N x T array whose first N0 rows are the control units and whose first T0
columns are the pre-treatment periods. A DataFrame works too, and its index and columns become
the unit and period labels.
Standard errors
| method | algorithm in the paper | when to use |
|---|---|---|
"bootstrap" |
Algorithm 2 | the default; resamples units |
"jackknife" |
Algorithm 3 | fast; not recommended for synthetic control |
"placebo" |
Algorithm 4 | the only option with a single treated unit |
tau.se(method="bootstrap", replications=200, random_state=0)
tau.vcov(method="jackknife") # 1x1 matrix, like R's vcov
tau.ci(level=0.95, method="placebo")
With one treated unit the bootstrap and jackknife are undefined and return nan — same as R.
Pass random_state for reproducible resampling.
Covariates
Pass a time-varying N x T x C array. Weights and coefficients are solved jointly, as in
Arkhangelsky et al.'s "optimized" adjustment:
panel = panel_matrices(df, unit="country", time="year",
outcome="log_gdp", treatment="dem",
covariates=["educ"])
tau = synthdid_estimate(panel.Y, panel.N0, panel.T0, X=panel.X)
tau.weights.beta # the covariate coefficients
Staggered adoption
Units that start treatment at different times — the case the R package rejects. Cohorts are
estimated separately and combined with weights proportional to treated unit-periods, following
the Stata sdid package:
from synthdid import staggered_synthdid_estimate
est = staggered_synthdid_estimate(df, unit="state", time="year",
outcome="y", treatment="d")
print(est.att) # aggregate ATT
print(est.by_cohort) # one row per adoption date
print(est.se(method="jackknife"))
est.estimates[2005].plot() # drill into one cohort
control_pool="never_treated" (default) uses only never-treated units, matching Stata.
control_pool="not_yet_treated" also admits units that adopt later, truncating each cohort's
post-period at the next adoption date so those controls stay clean.
With a single adoption date this reduces exactly to synthdid_estimate.
Plots
tau.plot() # trajectories + parallelogram + effect arrow + lambda ribbon
tau.units_plot() # each control's own diff-in-diff, sized by its weight
tau.placebo_plot() # the estimate beside a pre-treatment placebo
tau.rmse_plot() # solver convergence diagnostic
synthdid_plot({"DiD": did, "SC": sc, "SDID": sdid}) # one panel each
synthdid_plot({"DiD": did, "SC": sc, "SDID": sdid}, facet=["a"]*3) # overlaid
Every plotting function returns (fig, ax), so styling is ordinary matplotlib.
examples/quickstart.py reproduces Figure 1 of the paper.
API
| Estimation | synthdid_estimate · sc_estimate · did_estimate · staggered_synthdid_estimate |
| Inference | vcov · synthdid_se · bootstrap_se · jackknife_se · placebo_se |
| Diagnostics | summary · synthdid_controls · synthdid_placebo · synthdid_effect_curve |
| Plots | synthdid_plot · synthdid_units_plot · synthdid_placebo_plot · synthdid_rmse_plot |
| Data | panel_matrices · PanelData · random_low_rank · load_california_prop99 · load_cps · load_penn |
| Solver | sc_weight_fw · sc_weight_fw_covariates · fw_step · sparsify_function · contract3 |
Notes for people coming from R
synthdid_estimate(Y, N0, T0)returns a float subclass, not a scalar with attributes.attr(est, "weights")becomesest.weights;attr(est, "setup")becomesest.setup.weights$lambdaisweights.lambda_(lambdais a Python keyword).weights["lambda"]also works, as do the aliasesweights.timeandweights.unit.vcov()returns a 1×1 array so it stays a drop-in match;.se()gives the scalar directly.print(tau.hat)in R runs a jackknife. Herereprstays cheap and computes no standard error; usetau.format()for the R-style one-liner ortau.summary()for the full report.- Column selectors in
panel_matricesare 1-based integers or names, as in R. - Resampling takes an explicit
random_staterather than reading a global seed.
Citing
If you use this package, cite the method:
Arkhangelsky, D., Athey, S., Hirshberg, D. A., Imbens, G. W., & Wager, S. (2021). Synthetic Difference-in-Differences. American Economic Review, 111(12), 4088–4118.
License
BSD 3-Clause. This is a derivative work of the R package synthdid
(© 2019 Stanford University), which is dual-licensed GPL (≥2) | BSD 3-Clause; this port takes
the BSD option. See NOTICE for full attribution, including the Stata sdid package
whose staggered-adoption design this follows, and the sources of the bundled datasets.
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 Distribution
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 synthdid_py-0.1.0.tar.gz.
File metadata
- Download URL: synthdid_py-0.1.0.tar.gz
- Upload date:
- Size: 154.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
785fc118c2cc3f381917978325cef704bb82785d736703aee0afda6675073b12
|
|
| MD5 |
07d867fb8b43be49e6edc1f22ae848d6
|
|
| BLAKE2b-256 |
d0ea2dfdd2e45579d11c0295e05c7861ee1e12d261418d6fb6b7f3c08197a236
|
File details
Details for the file synthdid_py-0.1.0-py3-none-any.whl.
File metadata
- Download URL: synthdid_py-0.1.0-py3-none-any.whl
- Upload date:
- Size: 142.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71b5c1886d9b723a95403fb907bab8685db5f99dcb62ed6beb393b2b020d6a40
|
|
| MD5 |
f7fa820ba45a02eaf98ca78543be1346
|
|
| BLAKE2b-256 |
c4ba55ff4ff4b27871f032a9b23706b1e6e51bf619fbdb701f76ceb107e4c660
|