Skip to main content

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") becomes est.weights; attr(est, "setup") becomes est.setup.
  • weights$lambda is weights.lambda_ (lambda is a Python keyword). weights["lambda"] also works, as do the aliases weights.time and weights.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. Here repr stays cheap and computes no standard error; use tau.format() for the R-style one-liner or tau.summary() for the full report.
  • Column selectors in panel_matrices are 1-based integers or names, as in R.
  • Resampling takes an explicit random_state rather 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


Download files

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

Source Distribution

synthdid_py-0.1.0.tar.gz (154.8 kB view details)

Uploaded Source

Built Distribution

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

synthdid_py-0.1.0-py3-none-any.whl (142.9 kB view details)

Uploaded Python 3

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

Hashes for synthdid_py-0.1.0.tar.gz
Algorithm Hash digest
SHA256 785fc118c2cc3f381917978325cef704bb82785d736703aee0afda6675073b12
MD5 07d867fb8b43be49e6edc1f22ae848d6
BLAKE2b-256 d0ea2dfdd2e45579d11c0295e05c7861ee1e12d261418d6fb6b7f3c08197a236

See more details on using hashes here.

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

Hashes for synthdid_py-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 71b5c1886d9b723a95403fb907bab8685db5f99dcb62ed6beb393b2b020d6a40
MD5 f7fa820ba45a02eaf98ca78543be1346
BLAKE2b-256 c4ba55ff4ff4b27871f032a9b23706b1e6e51bf619fbdb701f76ceb107e4c660

See more details on using hashes here.

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