| Title: | Updated Synthetic Difference-in-Differences Estimation and Inference |
|---|---|
| Description: | Synthetic difference-in-differences, synthetic control and difference-in-differences estimates of the average treatment effect on the treated in panel data, following Arkhangelsky, Athey, Hirshberg, Imbens and Wager (2021) <doi:10.1257/aer.20190159> and the Stata implementation of Clarke, Pailanir, Athey and Imbens (2024) <doi:10.1177/1536867X241297914>. Supports simultaneous and staggered treatment adoption, bootstrap, jackknife and placebo inference, covariate adjustment, and parallel replications in compiled code. Derived from the synthdid package by the authors of the 2021 paper. |
| Authors: | Jose J. Alcocer [aut, cre, cph] (ORCID: <https://orcid.org/0009-0005-0469-3689>), Harry G. Muttram [aut] (ORCID: <https://orcid.org/0009-0001-4241-3204>), Dmitry Arkhangelsky [ctb], Susan Athey [ctb], David A. Hirshberg [ctb], Guido W. Imbens [ctb], Stefan Wager [ctb], Stanford University [cph] |
| Maintainer: | Jose J. Alcocer <[email protected]> |
| License: | BSD_3_clause + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-10-19 13:47:28 UTC |
| Source: | https://github.com/alcocer-jj/sdid |
A dataset containing per-capita cigarette consumption (in packs). In year
1989 California imposed a Tobacco tax. The column treated is 1 from then on
for California.
data(california_prop99)data(california_prop99)
A data frame with 1209 rows and 4 variables:
US state name, character string
Year, integer
per-capita cigarette consumption, numeric
the treatment indicator 0: control, 1: treated, numeric
Abadie, Alberto, Alexis Diamond, and Jens Hainmueller. "Synthetic control methods for comparative case studies: Estimating the effect of California’s tobacco control program." Journal of the American statistical Association 105, no. 490 (2010): 493-505.
data(california_prop99) sdid(california_prop99, "PacksPerCapita", "State", "Year", "treated", vce = "placebo", seed = 1213, graph = FALSE)data(california_prop99) sdid(california_prop99, "PacksPerCapita", "State", "Year", "treated", vce = "placebo", seed = 1213, graph = FALSE)
CPS
data(CPS)data(CPS)
A data frame with 2000 rows and 8 variables.
state
year
log_wage
hours
urate
min_wage
open_carry
abort_ban
PENN
data(PENN)data(PENN)
A data frame with 5328 rows and 5 variables.
country
year
log_gdp
dem
educ
Estimates average effects on the treated (ATT) in panel data using synthetic
difference-in-differences. As in the Stata package sdid (Clarke et al.
2024), it accepts a balanced panel in long format, detects whether a
staggered treatment approach is needed, and estimates the effect with
bootstrap, jackknife, or placebo inference. Under staggered adoption,
sdid() estimates adoption-period effects, and the overall effect is a
weighted average of these with weights proportional to each cohort's number
of treated units times its number of post-treatment periods, following
Algorithm 1 of Clarke et al. (2024).
sdid( data, outcome, unit, time, treatment, vce, covariates = NULL, method = c("sdid", "sc", "did"), reps = 50, seed = NULL, level = 95, alternative = c("two.sided", "greater", "less"), covariate.method = c("optimized", "projected"), standardize = TRUE, projection.sample = c("controls", "untreated"), num.threads = NULL, graph = TRUE, quietly = FALSE, ... )sdid( data, outcome, unit, time, treatment, vce, covariates = NULL, method = c("sdid", "sc", "did"), reps = 50, seed = NULL, level = 95, alternative = c("two.sided", "greater", "less"), covariate.method = c("optimized", "projected"), standardize = TRUE, projection.sample = c("controls", "untreated"), num.threads = NULL, graph = TRUE, quietly = FALSE, ... )
data |
A balanced panel in long format, with one row per unit and period. A data frame, tibble or data.table. |
outcome, unit, time, treatment
|
Names of the columns holding the outcome, the unit identifier, the period and the treatment indicator. The indicator is 1 for a treated unit from its first treated period onward and 0 otherwise. Units first treated in the same period form a cohort, and if not every treated unit is first treated in the same period, the design is staggered. |
vce |
The method of inference, which must be given. "bootstrap"
resamples units, and "placebo" gives units never treated the
treatment pattern of the treated units, both over |
covariates |
Optional names of numeric columns of time-varying covariates to adjust for. Defaults to NULL, adjusting for none. |
method |
The estimator. "sdid" (the default) for synthetic difference-in-differences, "sc" for synthetic control or "did" for difference-in-differences. |
reps |
The number of bootstrap or placebo replications. Defaults to 50. More give a more precise standard error. |
seed |
An optional seed for the replications, so that results can be reproduced. It applies within this call only, and the state of the random number generator is restored afterward. Defaults to NULL. |
level |
The confidence level of the intervals, in percent. Defaults to 95. |
alternative |
The alternative hypothesis of the test of the effect
against zero. "two.sided" (the default) tests whether the effect
differs from zero, "greater" whether it is positive and "less"
whether it is negative. A one-sided test gives one-sided p-values
and intervals open in its direction, so that |
covariate.method |
How the covariates are adjusted for, ignored without covariates. "optimized" (the default) estimates their coefficients jointly with the weights. "projected" first regresses the outcome on the covariates and fixed effects, then estimates on the adjusted outcome, as Kranz (2022) proposes. |
standardize |
For the optimized method, TRUE (the default) to scale each covariate to mean zero and standard deviation one while solving, which keeps covariates on very different scales from stalling the solver. Coefficients are reported in the covariates' own units either way. |
projection.sample |
For the projected method, the observations its regression uses. "controls" (the default) uses the units never treated in every period. "untreated" adds the treated units' periods before treatment, a larger sample that assumes the treatment had no effect before it began. |
num.threads |
The number of threads for the replications. Defaults
to NULL, which uses the option |
graph |
TRUE (the default) to draw the trends plot of the estimate,
as |
quietly |
TRUE to print nothing, for example in a loop. By default,
|
... |
Further estimator settings: |
The result can be passed to the plot functions sdid_plot, sdid_units_plot and sdid_placebo_plot, and to sdid_placebo for an in-time placebo test. For a staggered design, the plots show one panel per cohort.
An object of class "sdid", returned invisibly. Use the methods in
sdid-methods and weights.sdid() to extract results rather than
reading the list directly. The main components are:
estimateThe ATT. In a staggered design, the weighted average of the cohort effects.
se, z, p.value, conf.int
Standard error, z statistic,
p-value and confidence interval. All are NA with
vce = "noinference", and the jackknife SE is NA when a cohort's
weight rests on a single control. One-sided intervals have an infinite
bound.
cohortsA data frame with one row per adoption period: period,
treated (number of treated units), post (number of post-treatment
periods), weight (share of the overall effect), and the cohort's
estimate, se, z, p.value, conf.low and conf.high. A block
design has a single row.
betaCovariate coefficients, or NULL without covariates. With
the optimized method and staggered adoption, a matrix with one column
per cohort.
level, alternative, vce, reps, method, design,
covariate.method, variables, call
The settings used for the
fit. design is "block" or "staggered".
setup, estimates
The panel as matrices and the per-cohort estimates used by the plot functions. These are internal and may change.
Damian Clarke, Daniel Pailañir, Susan Athey, and Guido Imbens. "On Synthetic Difference-in-Differences and Related Estimation Methods in Stata". The Stata Journal 24(4), 557-598, 2024.
Dmitry Arkhangelsky, Susan Athey, David A. Hirshberg, Guido W. Imbens, and Stefan Wager. "Synthetic Difference in Differences". American Economic Review 111(12), 4088-4118, 2021.
data(california_prop99) fit = sdid(california_prop99, "PacksPerCapita", "State", "Year", "treated", vce = "placebo", seed = 1213, graph = FALSE) confint(fit, level = 0.9)data(california_prop99) fit = sdid(california_prop99, "PacksPerCapita", "State", "Year", "treated", vce = "placebo", seed = 1213, graph = FALSE) confint(fit, level = 0.9)
Computes a placebo variant of our estimator using pre-treatment data only
sdid_placebo(estimate, treated.fraction = NULL, placebo.time = "auto")sdid_placebo(estimate, treated.fraction = NULL, placebo.time = "auto")
estimate |
The result of |
treated.fraction |
The share of the periods before treatment that
the placebo treats, counted back from the last of them. Defaults to
NULL, which uses the share of all periods that are treated. Give
either this or |
placebo.time |
The period at which the placebo treatment begins, as
it appears in the time column, such as 1981, or a date for a panel
indexed by dates. It must come before the real treatment, with at
least one period before it. Defaults to "auto", which sets it from
|
The placebo estimate. Use c() to get it as a number, print it for a
one-line summary, or plot it with sdid_placebo_plot().
Plots the treated and synthetic control trajectories of an sdid() result,
with an in-time placebo stacked below. Requires ggplot2.
sdid_placebo_plot( estimate, treated.fraction = NULL, style = c("minimal", "effects"), placebo.time = "auto", x.breaks = NULL, y.breaks = NULL )sdid_placebo_plot( estimate, treated.fraction = NULL, style = c("minimal", "effects"), placebo.time = "auto", x.breaks = NULL, y.breaks = NULL )
estimate |
The result of |
treated.fraction |
The share of the periods before treatment that
the placebo treats, as in |
style |
The plot's style, "minimal" (the default) or "effects", as
in |
placebo.time |
The period at which the placebo treatment begins, as
in |
x.breaks, y.breaks
|
Optional ticks for the time and outcome axes, as
in |
A ggplot object, which can be extended with +.
Plots the treated and synthetic control trajectories of an sdid() result,
along with the time weights, which show which pre-treatment periods were
most informative for predicting post-treatment outcomes among the controls.
sdid_plot( estimates, treated.name = "treated", control.name = "synthetic control", spaghetti.units = c(), spaghetti.matrices = NULL, lambda.comparable = FALSE, overlay = 0, lambda.plot.scale = 3, effect.curvature = 0.3, line.width = 0.5, point.size = 1, onset.alpha = 0.8, spaghetti.line.width = 0.2, spaghetti.label.size = 2, spaghetti.line.alpha = 0.3, spaghetti.label.alpha = 0.5, alpha.multiplier = NULL, style = c("minimal", "effects"), lambda.axis = TRUE, x.breaks = NULL, y.breaks = NULL )sdid_plot( estimates, treated.name = "treated", control.name = "synthetic control", spaghetti.units = c(), spaghetti.matrices = NULL, lambda.comparable = FALSE, overlay = 0, lambda.plot.scale = 3, effect.curvature = 0.3, line.width = 0.5, point.size = 1, onset.alpha = 0.8, spaghetti.line.width = 0.2, spaghetti.label.size = 2, spaghetti.line.alpha = 0.3, spaghetti.label.alpha = 0.5, alpha.multiplier = NULL, style = c("minimal", "effects"), lambda.axis = TRUE, x.breaks = NULL, y.breaks = NULL )
estimates |
The result of an sdid object or a list of sdid estimates. For a staggered sdid result, each adoption period is plotted in a stacked panel. |
treated.name |
A string conveying the name of the treated curve that appears in the legend. Defaults to "treated". |
control.name |
A string conveying the name of the control curve that appears in the legend. Defaults to 'synthetic control'. |
spaghetti.units |
A list of units to plot individually alongside the treatment and control curves. Defaults to the empty list. |
spaghetti.matrices |
A list of matrices, one per estimate, of extra trajectories to plot. Each row is a trajectory, one column per period, and row names are used as labels. |
lambda.comparable |
Logical argument where if TRUE, the lambda weight axis is scaled to range from 0 to 1. Useful for when comparing across stacked panels. Defaults to FALSE. |
overlay |
Between 0 and 1. Shifts the synthetic control toward the treated line by this fraction of their pre-treatment gap, which makes it easier to see whether they move in parallel. Defaults to 0 (no shift). A vector of values can be animated with plotly. |
lambda.plot.scale |
Height of the time-weight band: the range of the trajectories divided by this number. Defaults to 3. |
effect.curvature |
Curvature of the effect arrow in the effects style. |
line.width |
Width of the trajectory lines. |
point.size |
Size of the points in the effects style. |
onset.alpha |
Opacity of the red dashed line that marks treatment. |
spaghetti.line.width, spaghetti.label.size, spaghetti.line.alpha, spaghetti.label.alpha
|
Width, label size and opacity of the spaghetti trajectories and their labels. |
alpha.multiplier |
One number per estimate that scales its opacity, to emphasize some estimates over others. |
style |
The plot's style, "minimal" (the default) or "effects". See Details. |
lambda.axis |
Logical argument where if TRUE, each panel with a time weights' band shows the band's scale on a right-hand axis. Defaults to TRUE. |
x.breaks, y.breaks
|
Optional tick positions for the time and outcome
axes (numbers, or dates for a date axis). The axes are extended to
the outermost ticks if needed. With stacked panels, |
Two plotting styles are available: "minimal" and "effects". The default,
style = "minimal", plots both trajectories in black, with the treated line
solid and the synthetic control dashed. The time weights are plotted as a
gray band with their scale on a right-hand axis, and a red dashed line marks
treatment. style = "effects" adds a diff-in-diff diagram of the treatment
effect and fades the trajectory lines.
A staggered sdid() result is drawn as one panel per cohort, stacked in a
column and labeled by adoption period. A list of results is drawn the same
way, one panel each. To change the axis ticks, use x.breaks and y.breaks
rather than adding your own scale, which would drop the time-weight axis.
Requires ggplot2.
A ggplot object, which can be extended with +.
Synthetic difference-in-differences estimates the effect as a weighted average, over the control units, of the difference-in-differences comparison of the treated units with each control alone, using the time weights. This plot shows those comparisons with their unit weights, for an estimate or each cohort of a staggered design.
sdid_units_plot( estimates, type = c("scatter", "bars"), cohort = NULL, label.units = NULL, weighted.only = FALSE )sdid_units_plot( estimates, type = c("scatter", "bars"), cohort = NULL, label.units = NULL, weighted.only = FALSE )
estimates |
The result of |
type |
The kind of plot. "scatter" (the default) plots each control's weight against its own estimate. "bars" draws a bar from the effect to each weighted control's own estimate, ordered by weight. See Details. |
cohort |
For a staggered result, the adoption period of the one
cohort to plot, as it appears in the result's |
label.units |
Which controls to label: "all", "weighted" (positive weight only), or a number n for the n largest weights. Defaults to "all" for one panel and 10 for several. The bars only show labeled controls. |
weighted.only |
TRUE to leave controls with no weight out of the scatter. Defaults to FALSE, drawing them as open circles. |
type = "scatter" (the default) plots each control's weight against its own
estimate, with the overall effect as a red line. Controls with zero weight
are drawn as open circles. type = "bars" draws a bar from the effect to
each weighted control's estimate, largest weight first; bars to the right
pull the estimate up, bars to the left pull it down.
Requires ggplot2. If ggrepel is installed, it is used to keep labels from overlapping.
A ggplot object, which can be extended with +.
Standard generics applied to the result of sdid(). weights.sdid()
extracts the unit and time weights.
## S3 method for class 'sdid' print(x, digits = 5, ...) ## S3 method for class 'sdid' summary(object, ...) ## S3 method for class 'sdid' coef(object, ...) ## S3 method for class 'sdid' vcov(object, ...) ## S3 method for class 'sdid' confint(object, parm, level = object$level/100, ...) ## S3 method for class 'sdid' nobs(object, ...) ## S3 method for class 'sdid' plot(x, ...)## S3 method for class 'sdid' print(x, digits = 5, ...) ## S3 method for class 'sdid' summary(object, ...) ## S3 method for class 'sdid' coef(object, ...) ## S3 method for class 'sdid' vcov(object, ...) ## S3 method for class 'sdid' confint(object, parm, level = object$level/100, ...) ## S3 method for class 'sdid' nobs(object, ...) ## S3 method for class 'sdid' plot(x, ...)
x, object
|
The result of |
digits |
The number of decimals printed for estimates, standard errors and confidence bounds. Defaults to 5. |
... |
Passed to |
parm |
Not used, as the result has a single parameter, the effect, named by the treatment column. |
level |
The confidence level of the interval, as a proportion, such as 0.9. Defaults to the level of the fit. |
print()Prints the header and results tables that sdid() prints,
and returns x invisibly.
summary()Prints the same tables, then for each cohort its number
of controls and pre-treatment periods with positive weight and its five
largest unit weights, and returns object invisibly.
coef()The estimated effect, a number named by the treatment column.
vcov()Its variance, the square of the standard error, as a 1 by 1
matrix whose row and column are named by the treatment column. It is NA
without inference.
confint()A 1 by 2 matrix holding the lower and upper bounds of
the normal confidence interval at level, with the row named by the
treatment column and the columns by the bounds' percentiles. For a
one-sided alternative the interval is one-sided, open in the direction of
the alternative.
nobs()The number of observations, the number of units times the number of periods.
plot()The trends plot of sdid_plot(), a ggplot object.
Summaries of the result of sdid() in the form the broom package defines,
which modelsummary and other packages for tables of estimates use. tidy()
gives a row per estimate and glance() one row describing the data. The
methods become available once the generics package is loaded, as it is by
modelsummary and broom, so modelsummary(fit) works without further setup.
Arguments given to modelsummary that it does not use itself, such as
cohorts and labels, are passed on to tidy().
## S3 method for class 'sdid' tidy( x, conf.int = TRUE, conf.level = x$level/100, cohorts = FALSE, labels = NULL, ... ) ## S3 method for class 'sdid' glance(x, ...)## S3 method for class 'sdid' tidy( x, conf.int = TRUE, conf.level = x$level/100, cohorts = FALSE, labels = NULL, ... ) ## S3 method for class 'sdid' glance(x, ...)
x |
The result of |
conf.int |
TRUE (the default) to include the bounds of the confidence interval. |
conf.level |
The confidence level of the interval, as a proportion.
Defaults to the level of the fit. modelsummary sets it from its
|
cohorts |
TRUE to add a row for each adoption period of a staggered design, after the overall effect. Defaults to FALSE. A design with a single adoption period gets no further rows. |
labels |
Optional names for all the rows, in order, the overall
effect first and then, with |
... |
Not used. |
tidy() returns a data frame with one row per estimate and columns
term, estimate, std.error, statistic (z), p.value, and, if
conf.int = TRUE, conf.low and conf.high. These are NA without
inference.
glance() returns one row with nobs, n.units, n.periods and
covariates (comma-separated names, or NA).
data(california_prop99) fit = sdid(california_prop99, "PacksPerCapita", "State", "Year", "treated", vce = "placebo", seed = 1213, graph = FALSE, quietly = TRUE) if (requireNamespace("generics", quietly = TRUE)) { generics::tidy(fit) generics::glance(fit) }data(california_prop99) fit = sdid(california_prop99, "PacksPerCapita", "State", "Year", "treated", vce = "placebo", seed = 1213, graph = FALSE, quietly = TRUE) if (requireNamespace("generics", quietly = TRUE)) { generics::tidy(fit) generics::glance(fit) }
Unit and time weights of an sdid result
## S3 method for class 'sdid' weights(object, ...)## S3 method for class 'sdid' weights(object, ...)
object |
The result of |
... |
Not used. |
A list of two data frames. omega has a row per unit never treated
and lambda a row per period, each with a column of weights for each
adoption period. A period after a cohort's adoption has no time weight for
that cohort and is NA.