| Title: | Compare DID, SDID, Matrix Completion and the Triply Robust Panel Estimator |
|---|---|
| Description: | A comparison toolkit for binary-treatment panel causal inference. Runs difference-in-differences (two-way fixed effects), synthetic difference-in-differences, synthetic control, matrix completion, and the Triply RObust Panel (TROP) estimator of Athey, Imbens, Qu and Viviano (2026) <doi:10.1002/jae.70061> on the same data, and returns their average treatment effects on a single tidy schema with shared plots. TROP, DID and matrix completion are implemented natively; synthetic difference-in-differences and synthetic control are obtained through the 'synthdid' package, and an alternative matrix-completion / interactive fixed-effects estimator through 'gsynth'. This is an unofficial, independent implementation and is not affiliated with or endorsed by the authors of the TROP estimator. |
| Authors: | Takuma Iwasaki [aut, cre] |
| Maintainer: | Takuma Iwasaki <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0.9006 |
| Built: | 2026-10-08 10:43:44 UTC |
| Source: | https://github.com/takuma1102/cfcompare |
Brings the output of trop(), panel_compare(), a synthdid estimate,
or any object with an estimate into the common cf_att_tbl schema, so
results computed elsewhere can be slotted into the same comparison and plots.
as_att(x, ...)as_att(x, ...)
x |
An object to tidy. |
... |
Passed to methods (e.g. |
A cf_att_tbl (a data.frame).
Point estimate and (where available) confidence interval for each method, on a single axis – the quick visual comparison this package is built for.
## S3 method for class 'cf_comparison' autoplot(object, ...) ## S3 method for class 'cf_att_tbl' autoplot(object, ...)## S3 method for class 'cf_comparison' autoplot(object, ...) ## S3 method for class 'cf_att_tbl' autoplot(object, ...)
object |
A |
... |
Unused. |
A ggplot2 object.
df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, seed = 1) cmp <- panel_compare(df, "y", "w", "id", "t", methods = c("DID", "MC", "TROP")) autoplot(cmp)df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, seed = 1) cmp <- panel_compare(df, "y", "w", "id", "t", methods = c("DID", "MC", "TROP")) autoplot(cmp)
One line per estimator; the y axis is on a log scale (as in the paper). Lower is better.
## S3 method for class 'cf_rmse_curve' autoplot(object, log_y = TRUE, ...)## S3 method for class 'cf_rmse_curve' autoplot(object, log_y = TRUE, ...)
object |
A |
log_y |
Logical; log10 y axis (default |
... |
Unused. |
A ggplot object.
Autoplot for paired RMSE curves
## S3 method for class 'cf_rmse_curves' autoplot(object, combined = FALSE, log_y = TRUE, ...)## S3 method for class 'cf_rmse_curves' autoplot(object, combined = FALSE, log_y = TRUE, ...)
object |
A |
combined |
|
log_y |
Logical; log10 y axis. |
... |
Unused. |
A list of two ggplots, or one combined ggplot.
Visualises a panel_rmse() result as a ranked bar chart (lowest RMSE first),
with +/- 1 standard-error whiskers across placebo runs. This is the
cross-model RMSE comparison from the paper.
## S3 method for class 'cf_rmse_tbl' autoplot(object, ...)## S3 method for class 'cf_rmse_tbl' autoplot(object, ...)
object |
A |
... |
Unused. |
A ggplot object.
Forest plot of a compare_se_modes() result: every row carries the same TROP
point estimate and only the confidence interval differs between resampling
schemes. Zero is always kept in view and marked with a dashed reference line,
since whether the interval covers zero is the quantity of interest.
## S3 method for class 'cf_se_comparison' autoplot(object, ...)## S3 method for class 'cf_se_comparison' autoplot(object, ...)
object |
A |
... |
Unused. |
A ggplot2 object.
Cells are coloured by cross-validation loss (darker = better out-of-sample
fit) and annotated with the ATT estimate at that penalty pair; the
CV-selected cell is outlined. The two swept penalties (see the axes
argument of trop_sensitivity()) form the x and y axes, and the fixed
penalty is reported in the subtitle.
## S3 method for class 'cf_trop_grid' autoplot(object, ...)## S3 method for class 'cf_trop_grid' autoplot(object, ...)
object |
A |
... |
Unused. |
A ggplot object.
Draws the treated-unit average observed path against the estimated untreated
(counterfactual) path, in the style of the synthdid plot: a dashed line
marks the last pre-treatment period, the post-treatment gap between the two lines is
the estimated effect, and – since TROP carries explicit time weights – the
time weights are drawn as a
filled band along the bottom (as in synthdid's time-weight strip) to
show which periods the counterfactual leans on. The band shows weights, not
observation counts; heights are scaled to the largest weight, and with
the weights are uniform and the band is flat.
## S3 method for class 'trop' autoplot(object, show_weights = TRUE, show_se = FALSE, ...)## S3 method for class 'trop' autoplot(object, show_weights = TRUE, show_se = FALSE, ...)
object |
A |
show_weights |
Logical; draw the time-weight band along the bottom. |
show_se |
Logical; append the fit's standard-error method to the subtitle.
Defaults to |
... |
Unused. |
This is a point-estimate trajectory: it shows no standard errors or
confidence intervals (the post-treatment gap is left unshaded so it is not
mistaken for a confidence band). To visualise the fit's uncertainty, use
trop_event_study() and call autoplot() on the result, which draws
per-period estimates with pointwise confidence bars. Accordingly the subtitle
reports only the selected penalties by default; set show_se = TRUE to also
note which standard-error method the fit used.
A ggplot object.
trop_event_study() and its autoplot() method for per-period
effects with confidence bars.
df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 3, seed = 1) autoplot(trop(df, "y", "w", "id", "t", control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)))df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 3, seed = 1) autoplot(trop(df, "y", "w", "id", "t", control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)))
Plots the per-period treatment effects from trop_event_study() against event
time, with pointwise confidence-interval bars, a solid line at zero, and a
dotted line marking treatment onset. When pre-treatment periods are present
they are drawn as distinct placebo / pre-trend points (open markers) so the
pre-period profile can be read at a glance. The SE method is named at the end
of the subtitle.
## S3 method for class 'trop_event_study' autoplot(object, ...)## S3 method for class 'trop_event_study' autoplot(object, ...)
object |
A |
... |
Unused. |
A ggplot object.
df <- sim_panel(N = 16, T = 9, n_treated = 4, t0 = 6, att = 3, seed = 1) fit <- trop(df, "y", "w", "id", "t", control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) autoplot(trop_event_study(fit, se = "bootstrap", control = trop_control(n_boot = 20L, seed = 1)))df <- sim_panel(N = 16, T = 9, n_treated = 4, t0 = 6, att = 3, seed = 1) fit <- trop(df, "y", "w", "id", "t", control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) autoplot(trop_event_study(fit, se = "bootstrap", control = trop_control(n_boot = 20L, seed = 1)))
Histogram of the placebo-null ATT distribution with the observed ATT drawn as a solid reference line and zero as a dashed line.
## S3 method for class 'trop_placebo_test' autoplot(object, ...)## S3 method for class 'trop_placebo_test' autoplot(object, ...)
object |
A |
... |
Unused. |
A ggplot2 object.
Row-binds several ATT results — trop() fits, panel_compare() results,
synthdid estimates, existing cf_att_tbls, or anything as_att()
understands — into a single cf_att_tbl ready for autoplot() or
plot_counterfactual(). Each argument is passed through as_att() first, so
multi-row inputs (e.g. a panel_compare() result) keep all their rows, and
columns are aligned automatically. This replaces the manual
rbind(as.data.frame(...), ...) + class(...) <- "cf_att_tbl" dance.
bind_att(...)bind_att(...)
... |
Objects coercible by |
Names given to the arguments become method labels, overriding the label
already on a single-row result; a name on a multi-row input is ignored with
a warning (it cannot sensibly relabel several methods at once). For per-object
control of other fields such as outcome, coerce that object with as_att()
first and pass the result in unnamed.
A cf_att_tbl (a data.frame) with the inputs stacked row-wise.
as_att(), panel_compare(), compare_se_modes()
df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) lam <- list(time = 0.1, unit = 0.5, nn = 2) # fixed penalties (fast example) f_pooled <- trop(df, "y", "w", "id", "t", anchor = "pooled", lambda = lam, se = "none") f_per_cell <- trop(df, "y", "w", "id", "t", anchor = "per_cell", lambda = lam, se = "none") bind_att(pooled = f_pooled, per_cell = f_per_cell)df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) lam <- list(time = 0.1, unit = 0.5, nn = 2) # fixed penalties (fast example) f_pooled <- trop(df, "y", "w", "id", "t", anchor = "pooled", lambda = lam, se = "none") f_per_cell <- trop(df, "y", "w", "id", "t", anchor = "per_cell", lambda = lam, se = "none") bind_att(pooled = f_pooled, per_cell = f_per_cell)
Refits the TROP estimator under several inference methods — the unit-level
stratified block bootstrap and/or the leave-one-treated-unit-out jackknife —
and returns the estimate, standard error and confidence interval from each on
the common cf_att_tbl schema. The penalties are chosen once (a
single se = "none" fit — by cross-validation, or fixed up front with
lambda) and reused for every inference method, exactly as trop_ablation()
reuses one fit's penalties, so the ATT is
identical across rows and only the uncertainty changes. This is the table to
reach for when checking how sensitive the confidence interval is to the choice
of resampling scheme — it is an inference-mode comparison, not an
estimator comparison, so all rows share one point estimate by construction.
compare_se_modes( data, outcome, treatment, unit, time, covariates = NULL, lambda = NULL, se = c("bootstrap", "jackknife"), anchor = "pooled", control = trop_control(), labels = NULL )compare_se_modes( data, outcome, treatment, unit, time, covariates = NULL, lambda = NULL, se = c("bootstrap", "jackknife"), anchor = "pooled", control = trop_control(), labels = NULL )
data, outcome, treatment, unit, time, covariates
|
Passed to |
lambda |
Optional named list |
se |
Character vector of inference methods to compare; any of
|
anchor |
Weight anchoring, as in |
control |
A |
labels |
Optional |
An object of class cf_se_comparison (extending cf_att_tbl) with one
row per inference method, sharing a common point estimate. If a method does
not apply to the design (for example jackknife with a single treated unit) it
still contributes a row, carrying the shared estimate with NA uncertainty
and an explanatory note.
trop(), trop_ablation(), bind_att()
df <- sim_panel(N = 16, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) compare_se_modes(df, "y", "w", "id", "t", control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) # Fix the penalties (skip cross-validation), as with trop()'s `lambda`: compare_se_modes(df, "y", "w", "id", "t", lambda = list(time = 0, unit = 0, nn = Inf), control = trop_control(n_boot = 20L))df <- sim_panel(N = 16, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) compare_se_modes(df, "y", "w", "id", "t", control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) # Fix the penalties (skip cross-validation), as with trop()'s `lambda`: compare_se_modes(df, "y", "w", "id", "t", lambda = list(time = 0, unit = 0, nn = Inf), control = trop_control(n_boot = 20L))
Returns a method's estimate of the untreated potential outcome as
an N x T matrix on the panel's unit/time grid. This gives every estimator a
common interface, so counterfactuals – and the unit-period prediction RMSE
built on them (panel_rmse() with metric = "prediction") – can be compared
on the same footing.
counterfactual_matrix(object, ...) predict_counterfactual(object, ...)counterfactual_matrix(object, ...) predict_counterfactual(object, ...)
object |
A fitted object: a |
... |
Unused; for method extensibility. |
Coverage of the returned matrix differs by method. The native engines
(trop(), and the DID/MC/TROP fits inside panel_compare()) return a fitted
value for every cell. Methods whose model only defines a counterfactual for
the treated units – DIFP, and (when the optional package is installed) SDID,
SC and gsynth – fill the treated rows and leave control cells NA (a
control's untreated outcome is simply its observed value).
predict_counterfactual() is an alias for counterfactual_matrix().
For a single fit, an N x T numeric matrix (with NA where the
method does not define a counterfactual). For a cf_comparison, a named
list of such matrices, one per method.
panel_rmse(), trop(), panel_compare()
df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, seed = 1) fit <- trop(df, "y", "w", "id", "t", lambda = list(time = 0.1, unit = 0.5, nn = 2), se = "none") M <- counterfactual_matrix(fit) dim(M)df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, seed = 1) fit <- trop(df, "y", "w", "id", "t", lambda = list(time = 0.1, unit = 0.5, nn = 2), se = "none") M <- counterfactual_matrix(fit) dim(M)
Produces a publication-quality table from a trop_ablation() result. The
"latex" output uses booktabs rules and math-mode penalty headers,
matching the style of the paper's tables (load the booktabs LaTeX package).
## S3 method for class 'trop_ablation' format( x, output = c("latex", "markdown"), digits = 3, caption = "TROP penalty ablation", label = "tab:trop-ablation", ... )## S3 method for class 'trop_ablation' format( x, output = c("latex", "markdown"), digits = 3, caption = "TROP penalty ablation", label = "tab:trop-ablation", ... )
x |
A |
output |
|
digits |
Number of decimal places. |
caption, label
|
Table caption and cross-reference label (LaTeX only). |
... |
Unused. |
A character vector of lines (paste-ready). Combine with
cat(..., sep = "\n") or writeLines().
trop_ablation()
df <- sim_panel(N = 14, T = 8, n_treated = 3, t0 = 5, att = 2, seed = 1) ab <- trop_ablation(df, "y", "w", "id", "t", lambda_full = list(time = 0.1, unit = 0.5, nn = 2)) writeLines(format(ab, "markdown"))df <- sim_panel(N = 14, T = 8, n_treated = 3, t0 = 5, att = 2, seed = 1) ab <- trop_ablation(df, "y", "w", "id", "t", lambda_full = list(time = 0.1, unit = 0.5, nn = 2)) writeLines(format(ab, "markdown"))
Runs a set of panel estimators on a single long panel and returns their average-treatment-effect-on-the-treated (ATT) estimates on a common tidy schema, so applied researchers can compare them at a glance. DID, MC and TROP are computed natively (no external dependencies); SDID and SC are routed through the synthdid package when available, and an alternative MC/IFE can be routed through gsynth. Methods whose optional package is missing, or that do not apply to the design, are skipped with a message.
panel_compare( data, outcome, treatment, unit, time, covariates = NULL, methods = c("TROP", "DID", "SC", "MC", "SDID", "DIFP"), exclude = NULL, anchor = "auto", se = c("auto", "jackknife", "bootstrap", "none"), control = trop_control(), verbose = FALSE )panel_compare( data, outcome, treatment, unit, time, covariates = NULL, methods = c("TROP", "DID", "SC", "MC", "SDID", "DIFP"), exclude = NULL, anchor = "auto", se = c("auto", "jackknife", "bootstrap", "none"), control = trop_control(), verbose = FALSE )
data |
A long |
outcome, treatment, unit, time
|
Column names (strings). |
covariates |
Optional character vector of time-varying covariate column
names. Passed to the native engines ( |
methods |
Character vector of methods to run. Any of |
exclude |
Optional character vector of methods to drop from |
anchor |
Weight anchoring for TROP; see |
se |
Standard-error method for the native engines; see |
control |
A list of solver/CV settings from |
verbose |
Logical; print CV progress. |
An object of class cf_comparison: a list with the tidy table
att (class cf_att_tbl), per-method counterfactual matrices
counterfactual, the native fit objects fits, and the reshaped panel.
trop(), autoplot.cf_comparison(), plot_counterfactual()
# panel_compare() fits TROP with cross-validated penalties (random held-out # cells), so it is wrapped in \donttest to keep R CMD check fast df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, seed = 3) cmp <- panel_compare(df, "y", "w", "id", "t", methods = c("DID", "MC", "TROP"), se = "none", control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) cmp$att# panel_compare() fits TROP with cross-validated penalties (random held-out # cells), so it is wrapped in \donttest to keep R CMD check fast df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, seed = 3) cmp <- panel_compare(df, "y", "w", "id", "t", methods = c("DID", "MC", "TROP"), se = "none", control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) cmp$att
Converts a long panel data.frame into the N x T outcome (Y) and
treatment (W) matrices used by trop_matrix(), and reports the treated rows
and the width of the treated block. It is the data-frame-to-matrix companion
to trop_matrix(): rather than building Y, W, treated_units and
treated_periods by hand, call panel_matrices() and pass the pieces
straight through.
panel_matrices(data, outcome, treatment, unit, time)panel_matrices(data, outcome, treatment, unit, time)
data |
A long |
outcome, treatment, unit, time
|
Column names (strings). |
A list with components:
YN x T outcome matrix (rows = units, columns = times).
WN x T 0/1 treatment matrix on the same grid.
units,times
the sorted unit and time labels.
treated_unitsinteger row indices of units with any treated cell.
treated_periodsnumber of periods in which any treated unit is active, i.e. the width of the post block.
df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) pm <- panel_matrices(df, "y", "w", "id", "t") str(pm) # Feed straight into the matrix-in entry point (treated rows/periods inferred). trop_matrix(pm$Y, pm$W, lambda_unit = 0.1, lambda_time = 0.1, lambda_nn = Inf)df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) pm <- panel_matrices(df, "y", "w", "id", "t") str(pm) # Feed straight into the matrix-in entry point (treated rows/periods inferred). trop_matrix(pm$Y, pm$W, lambda_unit = 0.1, lambda_time = 0.1, lambda_nn = Inf)
Compares estimators by how well each does on held-out control data, following the "random blocks" placebo idea of the doubly/triply robust panel estimator paper. Two scoring rules are available:
panel_rmse( data, outcome, treatment, unit, time, methods = c("DID", "SC", "SDID", "MC", "TROP", "DIFP"), exclude = NULL, metric = c("placebo", "prediction"), horizon = 10L, n_pseudo = 10L, n_runs = 10L, control = trop_control(), seed = NULL, verbose = FALSE )panel_rmse( data, outcome, treatment, unit, time, methods = c("DID", "SC", "SDID", "MC", "TROP", "DIFP"), exclude = NULL, metric = c("placebo", "prediction"), horizon = 10L, n_pseudo = 10L, n_runs = 10L, control = trop_control(), seed = NULL, verbose = FALSE )
data |
A long |
outcome, treatment, unit, time
|
Column names (strings). |
methods |
Methods to compare; subset of |
exclude |
Optional character vector of methods to drop from |
metric |
|
horizon |
Number of final periods held out per placebo cohort. |
n_pseudo |
Number of placebo (pseudo-treated) control units per run. |
n_runs |
Number of placebo runs to average over. |
control |
A list of solver/CV controls from |
seed |
Optional integer seed for reproducible placebo draws. |
verbose |
Logical; print progress. |
metric = "placebo" (default): in each run a random set of control units is
given a placebo block treatment in the final horizon periods (true effect
zero), every method is estimated on that control-only panel, and the score is
sqrt(mean(ATT^2)) across runs. This works for every method – native
(DID, MC, TROP) and wrapped (SDID/SC via synthdid, gsynth,
augsynth, CS = Callaway & Sant'Anna via did).
metric = "prediction": per-cell one-step-ahead held-out RMSE (the paper's
Table-1 unit-period counterfactual prediction error). A block of control
cells is held out and each method predicts the untreated outcome there via
its counterfactual matrix (counterfactual_matrix()). Supported for the
native methods (DID, MC, TROP), for DIFP, and – when synthdid
is installed – for SDID and SC. The remaining wrapped methods
(gsynth, augsynth, CS) are reported as NA for this metric; use
metric = "placebo" for them.
Lower is better. Native methods are tuned once on the real data (DID is
parameter-free; MC selects lambda_nn; TROP selects the full triplet by
cross-validation) and reused across runs. Wrapped methods are skipped with a
note when their package is missing or the design does not apply.
A cf_rmse_tbl (a data.frame) with one row per method and columns
method, rmse, rmse_se, n_runs, engine, note.
Athey, S., Imbens, G. W., Qu, Z., & Viviano, D. (2025/2026). Triply/Doubly Robust Panel Estimators.
panel_compare(), autoplot.cf_rmse_tbl()
df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) r <- panel_rmse(df, "y", "w", "id", "t", methods = c("DID", "TROP"), horizon = 2, n_pseudo = 3, n_runs = 2, control = trop_control(n_cv_cells = 5L, cv_cycles = 1L), seed = 1) r autoplot(r)df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) r <- panel_rmse(df, "y", "w", "id", "t", methods = c("DID", "TROP"), horizon = 2, n_pseudo = 3, n_runs = 2, control = trop_control(n_cv_cells = 5L, cv_cycles = 1L), seed = 1) r autoplot(r)
For each native engine in a comparison (DID, MC, TROP), draws the average outcome over the treated units against the predicted control counterfactual, to show how the methods extrapolate through the post-treatment period.
plot_counterfactual(x, methods = NULL)plot_counterfactual(x, methods = NULL)
x |
A |
methods |
Optional subset of methods to draw. |
A ggplot2 object.
Draws the cross-validation-loss surface and the ATT surface from a
trop_sensitivity() grid as two separate, full-width base-graphics plots,
each with a colour key (legend) on the right. Unlike autoplot.cf_trop_grid()
— a single ggplot2 heatmap that packs both into one panel — this view
keeps the surfaces apart so neither is squeezed, and needs no ggplot2. The
surface axes follow the swept penalties recorded on the grid (see the axes
argument of trop_sensitivity()).
plot_trop_surfaces( grid, which = c("both", "cv_loss", "att"), ask = interactive(), ... )plot_trop_surfaces( grid, which = c("both", "cv_loss", "att"), ask = interactive(), ... )
grid |
A |
which |
Which surface(s) to draw: |
ask |
Logical; when drawing both on an interactive device, prompt before
the second figure. Defaults to |
... |
Further arguments passed to |
Invisibly, a list with the cv_loss and att surface matrices
(rows = y-axis penalty, columns = x-axis penalty).
trop_sensitivity(), autoplot.cf_trop_grid()
df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) g <- trop_sensitivity(df, "y", "w", "id", "t", lambda_time = c(0, 0.25, 1), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) plot_trop_surfaces(g, which = "cv_loss")df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) g <- trop_sensitivity(df, "y", "w", "id", "t", lambda_time = c(0, 0.25, 1), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) plot_trop_surfaces(g, which = "cv_loss")
Plot one or both estimation-RMSE curves
## S3 method for class 'cf_rmse_curve' plot(x, log_y = TRUE, file = NULL, width = 8, height = 5, ...) ## S3 method for class 'cf_rmse_curves' plot( x, combined = FALSE, log_y = TRUE, file = NULL, width = NULL, height = NULL, ... )## S3 method for class 'cf_rmse_curve' plot(x, log_y = TRUE, file = NULL, width = 8, height = 5, ...) ## S3 method for class 'cf_rmse_curves' plot( x, combined = FALSE, log_y = TRUE, file = NULL, width = NULL, height = NULL, ... )
x |
A |
log_y |
Logical; log10 y axis (default |
file |
Optional path. If given, the figure is written to a PNG at
|
width, height
|
Figure size in inches. Defaults are deliberately generous (single 8x5; combined 12x5). |
... |
Unused. |
combined |
For |
The input, invisibly.
Sweeps one design dimension – the number of control units ("n_control"),
pre-treatment periods ("n_pre"), treated units ("n_treated") or
post-treatment periods ("n_post") – and, at each value, runs a
semi-synthetic Monte Carlo: panels are drawn from a latent factor model in
which treatment is selected on the factor loadings (so plain DID/TWFE is
biased), a known constant effect att is imposed, every estimator is run, and
the estimation RMSE against the known truth is recorded. The result is the
data behind the paper-style "RMSE vs. N_control / T_pre" line plot, one line
per estimator. With a dense values grid and enough n_runs the curves are
smooth, as in the paper.
rmse_curve( vary = c("n_control", "n_pre", "n_treated", "n_post"), values = NULL, n_runs = 500L, methods = c("DID", "SDID", "SC", "MC", "DIFP", "TROP"), exclude = NULL, n_control = 60L, n_treated = 8L, n_pre = 16L, n_post = 6L, rank = 4L, att = 2, noise = 1, ar = 0.4, trend_sd = 0.05, anchor = "pooled", control = trop_control(), seed = 1L, parallel = FALSE, progress = interactive(), verbose = FALSE )rmse_curve( vary = c("n_control", "n_pre", "n_treated", "n_post"), values = NULL, n_runs = 500L, methods = c("DID", "SDID", "SC", "MC", "DIFP", "TROP"), exclude = NULL, n_control = 60L, n_treated = 8L, n_pre = 16L, n_post = 6L, rank = 4L, att = 2, noise = 1, ar = 0.4, trend_sd = 0.05, anchor = "pooled", control = trop_control(), seed = 1L, parallel = FALSE, progress = interactive(), verbose = FALSE )
vary |
Which dimension to sweep: |
values |
Integer vector of values for the swept dimension. Defaults to
|
n_runs |
Monte Carlo replications per value (higher = smoother; the paper
uses ~1000). Default 500. Note: with the dense default grid and six
estimators this is a large simulation (thousands of fits per panel); reduce
|
methods |
Estimators to include; any subset of the |
exclude |
Optional character vector of methods to drop from |
n_control, n_treated, n_pre, n_post
|
Base design; the dimension named in
|
rank, att, noise
|
Number of latent factors, the imposed (true) ATT, and the idiosyncratic-noise scale. |
ar |
AR(1) coefficient of the idiosyncratic errors (serial correlation). |
trend_sd |
SD of the heterogeneous unit-specific linear trend slopes (non-parallel trends); treatment is also selected on this slope. |
anchor |
Estimation anchor for the native ATT ( |
control |
Solver/CV controls from |
seed |
Base integer seed (each replication uses a distinct offset). |
parallel |
Logical; if |
progress |
Logical; print a rough progress indicator (completed fits, a
percentage, elapsed time and an ETA) periodically while the curve runs.
Helpful for the heavy large- |
verbose |
Logical; additionally print a line as each swept value
finishes. Independent of |
This is a simulation diagnostic on synthetic data; it does not take a user
panel. To run a curve on your own data, draw panels with sim_semisynthetic()
and call panel_compare() in a loop.
A cf_rmse_curve (a data.frame) with columns method, x
(the swept value), rmse, bias, n_runs. The swept-dimension label is in
attr(., "vary").
rmse_curves(), panel_rmse(), autoplot.cf_rmse_curve()
# quick look (small grid + few reps); raise n_runs/values for paper quality cc <- rmse_curve("n_control", values = c(12, 16), n_runs = 2, n_pre = 6, n_post = 3, methods = c("DID", "TROP"), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) autoplot(cc)# quick look (small grid + few reps); raise n_runs/values for paper quality cc <- rmse_curve("n_control", values = c(12, 16), n_runs = 2, n_pre = 6, n_post = 3, methods = c("DID", "TROP"), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) autoplot(cc)
Convenience wrapper that runs rmse_curve() for both the number of control
units and the number of pre-treatment periods and bundles the two curves, so
they can be plotted individually (default) or side by side (combined = TRUE),
as in the paper's two-panel figure.
rmse_curves(values_control = NULL, values_pre = NULL, ...)rmse_curves(values_control = NULL, values_pre = NULL, ...)
values_control, values_pre
|
Optional grids for each sweep (see
|
... |
Passed to |
A cf_rmse_curves object: a list with $n_control and $n_pre,
each a cf_rmse_curve.
rmse_curve(), autoplot.cf_rmse_curves()
# defaults are a large simulation; use a small grid + few reps for a quick look g <- rmse_curves(values_control = c(12, 16), values_pre = c(5, 8), n_post = 3, n_runs = 2, methods = c("DID", "TROP"), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) plot(g) # two separate figures (default) # combined = TRUE needs the optional 'patchwork' package: if (requireNamespace("patchwork", quietly = TRUE)) plot(g, combined = TRUE) # side-by-side, paper-style# defaults are a large simulation; use a small grid + few reps for a quick look g <- rmse_curves(values_control = c(12, 16), values_pre = c(5, 8), n_post = 3, n_runs = 2, methods = c("DID", "TROP"), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) plot(g) # two separate figures (default) # combined = TRUE needs the optional 'patchwork' package: if (requireNamespace("patchwork", quietly = TRUE)) plot(g, combined = TRUE) # side-by-side, paper-style
Returns the cross-validation-selected penalties of a trop_sensitivity()
grid — the grid cell minimising the CV loss — as a list(time=, unit=, nn=), the same shape trop() accepts for its lambda argument. So the
data-driven choice on a grid can be fed straight back into a fit, e.g.
trop(..., lambda = selected_lambda(g)), without digging the values out of
attr(g, "selected") by hand.
selected_lambda(grid)selected_lambda(grid)
grid |
A |
A named list with the selected time, unit and nn penalties. The
full selected row (including the ATT and CV loss there) remains available in
attr(grid, "selected").
trop_sensitivity(), surface_matrix(), trop()
df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) g <- trop_sensitivity(df, "y", "w", "id", "t", lambda_time = c(0, 0.25, 1), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) selected_lambda(g)df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) g <- trop_sensitivity(df, "y", "w", "id", "t", lambda_time = c(0, 0.25, 1), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) selected_lambda(g)
Generates a long panel whose untreated potential outcomes follow an
interactive-fixed-effects (factor) model on top of two-way fixed effects,
in the spirit of the data-generating processes in Athey, Imbens, Qu &
Viviano (2026). A block treatment is applied to n_treated units from period
t0 onward, with a constant additive effect att.
sim_panel( N = 30, T = 20, n_treated = 5, t0 = NULL, rank = 3L, att = 1, noise = 0.5, seed = NULL, n_cov = 0L, phi = NULL, confounding = 0 )sim_panel( N = 30, T = 20, n_treated = 5, t0 = NULL, rank = 3L, att = 1, noise = 0.5, seed = NULL, n_cov = 0L, phi = NULL, confounding = 0 )
N |
Number of units. |
T |
Number of periods. |
n_treated |
Number of treated units. |
t0 |
First treated period (block design). |
rank |
Number of latent factors. |
att |
True treatment effect added to treated cells. |
noise |
Standard deviation of idiosyncratic noise. |
seed |
Optional RNG seed. |
n_cov |
Number of time-varying covariates to generate (default |
phi |
Optional numeric coefficient vector for the covariates (length
|
confounding |
Non-negative strength of selection-on-confounders in the
treatment assignment. With |
A long data.frame with columns id, t, y, w, and the noiseless
counterfactual y0 (useful for evaluating estimators). When n_cov > 0 it
additionally carries covariate columns x1, ..., x{n_cov}, and the true
coefficients are stored on attr(., "phi").
df <- sim_panel(N = 16, T = 9, n_treated = 3, t0 = 6, seed = 42) head(df) # Two covariates with known coefficients, recoverable by trop(covariates=): dc <- sim_panel(N = 16, T = 9, n_treated = 3, t0 = 6, n_cov = 2, phi = c(1.2, -0.7), seed = 1) attr(dc, "phi")df <- sim_panel(N = 16, T = 9, n_treated = 3, t0 = 6, seed = 42) head(df) # Two covariates with known coefficients, recoverable by trop(covariates=): dc <- sim_panel(N = 16, T = 9, n_treated = 3, t0 = 6, n_cov = 2, phi = c(1.2, -0.7), seed = 1) attr(dc, "phi")
Takes a real long panel, uses its outcomes (optionally smoothed through a
low-rank-plus-two-way-fixed-effects fit) as the untreated potential outcomes
Y(0), and imposes a known treatment effect on a chosen block of units and
periods. Because the baseline is real data but the effect is known, the result
is a ground-truth benchmark that "closely matches" the real setting – the
style of semi-synthetic experiment used to evaluate panel estimators in
Athey, Imbens, Qu & Viviano (2026). Pair it with panel_compare() or
panel_rmse() to score estimators against the truth.
sim_semisynthetic( data, outcome, unit, time, n_treated, t0 = NULL, att = 1, effect = NULL, baseline = c("observed", "lowrank"), lambda_nn = NULL, noise = 0, seed = NULL )sim_semisynthetic( data, outcome, unit, time, n_treated, t0 = NULL, att = 1, effect = NULL, baseline = c("observed", "lowrank"), lambda_nn = NULL, noise = 0, seed = NULL )
data |
A real long |
outcome, unit, time
|
Column names (strings). |
n_treated |
Number of units to assign to the placebo treated group (sampled at random from all units). |
t0 |
First treated period (block design). Defaults to about three quarters of the way through the panel. |
att |
Constant additive treatment effect imposed on treated cells. Ignored
if |
effect |
Optional per-treated-period effect: a single number, or a numeric
vector of length |
baseline |
|
lambda_nn |
Nuclear-norm penalty for the |
noise |
For |
seed |
Optional RNG seed. |
A long data.frame with columns id, t, y, w, y0 (the imposed
untreated potential outcome) and tau (the true effect, 0 off treatment).
sim_panel(), panel_compare(), panel_rmse()
real <- sim_panel(N = 16, T = 10, n_treated = 0L, att = 0, seed = 1) ss <- sim_semisynthetic(real, "y", "id", "t", n_treated = 6, t0 = 8, att = 3, seed = 2) mean(ss$tau[ss$w == 1]) # true ATT = 3real <- sim_panel(N = 16, T = 10, n_treated = 0L, att = 0, seed = 1) ss <- sim_semisynthetic(real, "y", "id", "t", n_treated = 6, t0 = 8, att = 3, seed = 2) mean(ss$tau[ss$w == 1]) # true ATT = 3
Reshapes a long trop_sensitivity() grid into a wide matrix of one quantity
— the cross-validation loss or the ATT — laid out over the two swept
penalties (see the axes argument of trop_sensitivity()). Rows are the
y-axis penalty, columns the x-axis penalty; the matrix dimnames are named
after the two penalty columns so the layout is self-describing. This is the
same reshape plot_trop_surfaces() performs internally, exposed so the
surfaces can be inspected or post-processed without re-deriving them with
xtabs().
surface_matrix(grid, value = c("cv_loss", "att"))surface_matrix(grid, value = c("cv_loss", "att"))
grid |
A |
value |
Which quantity to lay out: |
A numeric matrix (y-axis penalty in rows, x-axis penalty in columns).
trop_sensitivity(), selected_lambda(), plot_trop_surfaces()
df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) g <- trop_sensitivity(df, "y", "w", "id", "t", lambda_time = c(0, 0.25, 1), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) surface_matrix(g, "cv_loss") surface_matrix(g, "att")df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) g <- trop_sensitivity(df, "y", "w", "id", "t", lambda_time = c(0, 0.25, 1), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) surface_matrix(g, "cv_loss") surface_matrix(g, "att")
Fits the TROP estimator of Athey, Imbens, Qu & Viviano (2026) on a long panel. TROP combines a low-rank-plus-two-way-fixed-effects outcome model with exponential-decay unit weights (upweighting controls similar to the treated) and time weights (upweighting periods near the treated periods). Penalties are chosen by leave-one-out cross-validation on the control cells. The estimator nests DID/TWFE, matrix completion and synthetic-control-type weighting as special cases.
trop( data, outcome, treatment, unit, time, covariates = NULL, lambda = NULL, standardize = FALSE, anchor = c("auto", "per_cell", "pooled"), se = c("bootstrap", "auto", "jackknife", "none"), grids = NULL, control = trop_control(), verbose = FALSE )trop( data, outcome, treatment, unit, time, covariates = NULL, lambda = NULL, standardize = FALSE, anchor = c("auto", "per_cell", "pooled"), se = c("bootstrap", "auto", "jackknife", "none"), grids = NULL, control = trop_control(), verbose = FALSE )
data |
A long |
outcome, treatment, unit, time
|
Column names (strings). |
covariates |
Optional character vector of column names for time-varying
covariates. When supplied, the low-rank term is augmented additively as
|
lambda |
Optional named list |
standardize |
Logical; if |
anchor |
How weights are anchored to treated cells: |
se |
Standard-error method: |
grids |
Optional list of penalty grids; see |
control |
A list of solver/CV settings from |
verbose |
Logical; print CV progress. |
An object of class trop: a list with the ATT estimate,
std.error, conf.low/conf.high, the selected penalties lambda,
any covariate coefficients phi, per-cell effects, the estimated
counterfactual matrix, weights, the reshaped panel, and scaling (the
outcome centre/scale used internally; the identity unless
standardize = TRUE). All effect and outcome-level components are on the
raw outcome scale; panel$Y and lambda are on the fitting scale.
Athey, S., Imbens, G. W., Qu, Z., & Viviano, D. (2026). Triply Robust Panel Estimators. Journal of Applied Econometrics. doi:10.1002/jae.70061.
df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, seed = 1) # fixed penalties keep this example fast and deterministic; omit `lambda` # to choose (time, unit, nn) by cross-validation fit <- trop(df, "y", "w", "id", "t", lambda = list(time = 0.1, unit = 0.5, nn = 2), se = "none") fitdf <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, seed = 1) # fixed penalties keep this example fast and deterministic; omit `lambda` # to choose (time, unit, nn) by cross-validation fit <- trop(df, "y", "w", "id", "t", lambda = list(time = 0.1, unit = 0.5, nn = 2), se = "none") fit
Refits TROP under a sequence of penalty constraints to show how the ATT moves
as the estimator is stripped back towards matrix completion and
difference-in-differences. This is the robustness exercise behind Table 5 of
Athey, Imbens, Qu & Viviano (2026): constraining lambda_unit and/or
lambda_time to zero (dropping unit/time weights) and/or lambda_nn to
infinity (dropping the low-rank regression adjustment).
trop_ablation( data, outcome, treatment, unit, time, covariates = NULL, control = trop_control(), anchor = "pooled", se = "none", lambda_full = NULL, rmse = FALSE, horizon = NULL, n_pseudo = NULL, n_runs = 200L ) ## S3 method for class 'trop_ablation' print(x, digits = 3, ...) ## S3 method for class 'trop_ablation' plot(x, file = NULL, width = NULL, height = NULL, res = 200, digits = 3, ...)trop_ablation( data, outcome, treatment, unit, time, covariates = NULL, control = trop_control(), anchor = "pooled", se = "none", lambda_full = NULL, rmse = FALSE, horizon = NULL, n_pseudo = NULL, n_runs = 200L ) ## S3 method for class 'trop_ablation' print(x, digits = 3, ...) ## S3 method for class 'trop_ablation' plot(x, file = NULL, width = NULL, height = NULL, res = 200, digits = 3, ...)
data, outcome, treatment, unit, time, covariates
|
Passed to |
control |
A |
anchor |
Weight anchoring, as in |
se |
Standard-error method for every specification (default |
lambda_full |
Optional named list |
rmse |
Logical; if |
horizon, n_pseudo
|
Placebo design used when |
n_runs |
Number of placebo draws when |
x |
A |
digits |
Number of decimal places for the penalties and estimates. |
... |
Unused. |
file |
Optional output path. When supplied, the table is rendered to that
file instead of the active graphics device; the format is taken from the
extension ( |
width, height
|
Figure size in inches. Defaults adapt to the number of rows and columns. |
res |
Resolution in PPI for the |
The full specification's penalties are taken from a single cross-validated
trop() fit; every constrained specification then reuses those values where
they are not constrained, so the rows are directly comparable. The result is a
table object: print it for a clean console table, or format() it to
paste-ready LaTeX (booktabs) or Markdown. It is also an ordinary
data.frame, so it can be passed to knitr::kable(), gt::gt(), etc.
An object of class trop_ablation (a data.frame) with one row per
specification: spec, the three penalties lambda_time/lambda_unit/
lambda_nn, estimate, std.error, conf.low/conf.high, and rank.
When rmse = TRUE it also has rmse and rmse_rel (RMSE relative to the
full TROP).
Athey, S., Imbens, G. W., Qu, Z., & Viviano, D. (2026). Triply Robust Panel Estimators. Journal of Applied Econometrics. doi:10.1002/jae.70061.
trop(); format() for paste-ready LaTeX/Markdown output
df <- sim_panel(N = 14, T = 8, n_treated = 3, t0 = 5, att = 2, seed = 1) # fixed base penalties keep the example fast; omit `lambda_full` to select # them by cross-validation ab <- trop_ablation(df, "y", "w", "id", "t", lambda_full = list(time = 0.1, unit = 0.5, nn = 2)) ab # clean console table cat(format(ab, "latex"), sep = "\n") # paste-ready LaTeX (booktabs)df <- sim_panel(N = 14, T = 8, n_treated = 3, t0 = 5, att = 2, seed = 1) # fixed base penalties keep the example fast; omit `lambda_full` to select # them by cross-validation ab <- trop_ablation(df, "y", "w", "id", "t", lambda_full = list(time = 0.1, unit = 0.5, nn = 2)) ab # clean console table cat(format(ab, "latex"), sep = "\n") # paste-ready LaTeX (booktabs)
trop()
Control settings for trop()
trop_control( max_iter = 2000L, tol = 1e-06, n_cv_cells = 120L, cv_cycles = 2L, max_cells = 60L, conf_level = 0.95, n_boot = 200L, boot_ci = c("percentile", "normal"), svd = c("truncated", "full"), cv_method = c("loocv", "placebo"), n_placebo = 30L, nn_floor = "auto", nn_floor_scale = 0.001, workers = 1L, seed = NULL )trop_control( max_iter = 2000L, tol = 1e-06, n_cv_cells = 120L, cv_cycles = 2L, max_cells = 60L, conf_level = 0.95, n_boot = 200L, boot_ci = c("percentile", "normal"), svd = c("truncated", "full"), cv_method = c("loocv", "placebo"), n_placebo = 30L, nn_floor = "auto", nn_floor_scale = 0.001, workers = 1L, seed = NULL )
max_iter |
Maximum solver iterations. Higher by default because small
|
tol |
Reference solver convergence tolerance. The applied tolerance is
tightened automatically as |
n_cv_cells |
Number of control cells sampled for the CV criterion. Set
to |
cv_cycles |
Number of coordinate-descent cycles in penalty selection. |
max_cells |
Threshold for |
conf_level |
Confidence level for intervals. |
n_boot |
Number of replications for the bootstrap standard error
( |
boot_ci |
Bootstrap confidence-interval type: |
svd |
Singular-value decomposition used by the soft-impute solver:
|
cv_method |
Penalty cross-validation criterion. |
n_placebo |
Number of placebo assignments drawn when
|
nn_floor |
Small non-negative stabilising constant added to every
finite |
nn_floor_scale |
Relative constant used when |
workers |
Number of parallel workers for the embarrassingly parallel
loops (cross-validation cells, and the bootstrap / jackknife
replicates). |
seed |
Optional seed for CV cell sampling (reproducibility). |
A list of control parameters.
Decomposes a fitted trop() ATT into per-period treatment effects indexed by
event time (periods relative to each unit's first treated period) and attaches
pointwise standard errors and confidence intervals. The same resampling used
for the overall SE is reused: each resample's per-cell effects are aggregated
by event time rather than into a single mean, so no extra modelling
assumptions are introduced. With pre_periods = TRUE the pre-treatment gaps
are returned as placebo / pre-trend points (a flat, near-zero pre-period
profile supports the design).
trop_event_study( object, se = c("bootstrap", "jackknife", "none"), pre_periods = TRUE, control = NULL, ... )trop_event_study( object, se = c("bootstrap", "jackknife", "none"), pre_periods = TRUE, control = NULL, ... )
object |
A |
se |
Standard-error method for the per-period effects: |
pre_periods |
Logical; include pre-treatment event times as placebo /
pre-trend points (default |
control |
A list of solver/CV/bootstrap settings from |
... |
Unused. |
Because the treated composition varies across bootstrap resamples and each event time draws on relatively few cells, the per-period intervals are wider than the overall ATT interval and cover each point individually (they are not simultaneous bands).
An object of class trop_event_study: a list whose estimates is a
data.frame with one row per event time (event_time, estimate,
std.error, conf.low, conf.high, n_cells, period), plus the overall
att, the se.method, and metadata.
Athey, S., Imbens, G. W., Qu, Z., & Viviano, D. (2026). Triply Robust Panel Estimators. Journal of Applied Econometrics. doi:10.1002/jae.70061.
autoplot.trop_event_study() to plot the result.
df <- sim_panel(N = 16, T = 9, n_treated = 4, t0 = 6, att = 3, seed = 1) fit <- trop(df, "y", "w", "id", "t", control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) es <- trop_event_study(fit, se = "bootstrap", pre_periods = TRUE, control = trop_control(n_boot = 20L, seed = 1)) esdf <- sim_panel(N = 16, T = 9, n_treated = 4, t0 = 6, att = 3, seed = 1) fit <- trop(df, "y", "w", "id", "t", control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) es <- trop_event_study(fit, se = "bootstrap", pre_periods = TRUE, control = trop_control(n_boot = 20L, seed = 1)) es
A thin, matrix-in wrapper around the TROP working model, written independently
from the paper. The matrix-in form is convenient for numerically comparing
trop() against other matrix-based implementations on identical inputs. For
data-frame input, cross-validated penalty selection, inference and the
multi-estimator comparison, use trop() and panel_compare() instead.
trop_matrix( Y, W, treated_units = NULL, lambda_unit, lambda_time, lambda_nn, treated_periods = NULL, control = trop_control() )trop_matrix( Y, W, treated_units = NULL, lambda_unit, lambda_time, lambda_nn, treated_periods = NULL, control = trop_control() )
Y |
N x T outcome matrix. |
W |
N x T 0/1 treatment matrix (1 = actively treated cell). |
treated_units |
Integer row indices of the treated units, used to anchor
the unit weights. Defaults to |
lambda_unit, lambda_time
|
Non-negative decay parameters for the unit and time weights. |
lambda_nn |
Nuclear-norm penalty; use |
treated_periods |
Number of final columns treated as the post block, used
to build the pre-period mask and the time-distance centre. Defaults to
|
control |
A list of solver controls from |
The untreated outcome model is fitted on the control cells with the supplied
weights, and the returned effect is the average over treated cells of
Y - alpha - beta - L, exactly as in the paper's eq. (2). With
lambda_nn = Inf the low-rank term is dropped and the fit is weighted two-way
fixed effects, matching the reference to numerical tolerance. With a finite
lambda_nn the nuclear-norm term is solved by this package's
proximal-gradient / soft-impute routine, which targets the same convex program
as eq. (2) (loss sum_js w (Y - alpha - beta - L)^2, with no 1/2 factor);
lambda_nn is therefore on the same scale as the official Python and Stata
packages, and finite-penalty results agree with them to solver tolerance.
trop_matrix() always uses the pooled weighting scheme anchored to the centre
of the treated block: it takes no anchor argument and has no per-cell mode.
It therefore computes the same pooled, block-centre estimator as the official
Python package's TROP_TWFE_average (implemented independently), and exists as
a like-for-like matrix-in cross-check against that pooled reference.
trop(..., anchor = "pooled") uses the same convention (unit distances to the
average treated trajectory, time distances to the centre of the treated
block), so on a trailing-block design the two agree to solver tolerance for
the same penalties. For the per-cell estimator (the paper's eq. (12) /
Algorithm 2), use trop(..., anchor = "per_cell"), which shares the
data-frame core .trop_att().
A single numeric ATT estimate.
Athey, S., Imbens, G. W., Qu, Z., & Viviano, D. (2026). Triply Robust Panel Estimators. Journal of Applied Econometrics. doi:10.1002/jae.70061.
df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) Y <- matrix(0, max(df$id), max(df$t)); W <- Y for (k in seq_len(nrow(df))) { Y[df$id[k], df$t[k]] <- df$y[k] W[df$id[k], df$t[k]] <- df$w[k] } tu <- which(rowSums(W) > 0) trop_matrix(Y, W, tu, 0.1, 0.1, Inf, treated_periods = 4) # treated_units and treated_periods are inferred from W when omitted, so the # matrices produced by panel_matrices() can be fed in directly. pm <- panel_matrices(df, "y", "w", "id", "t") trop_matrix(pm$Y, pm$W, lambda_unit = 0.1, lambda_time = 0.1, lambda_nn = Inf)df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) Y <- matrix(0, max(df$id), max(df$t)); W <- Y for (k in seq_len(nrow(df))) { Y[df$id[k], df$t[k]] <- df$y[k] W[df$id[k], df$t[k]] <- df$w[k] } tu <- which(rowSums(W) > 0) trop_matrix(Y, W, tu, 0.1, 0.1, Inf, treated_periods = 4) # treated_units and treated_periods are inferred from W when omitted, so the # matrices produced by panel_matrices() can be fed in directly. pm <- panel_matrices(df, "y", "w", "id", "t") trop_matrix(pm$Y, pm$W, lambda_unit = 0.1, lambda_time = 0.1, lambda_nn = Inf)
Assesses whether a fitted trop() ATT is distinguishable from zero by
comparing it to a placebo null distribution. Control units are repeatedly
relabelled as pseudo-treated — using the same number of treated units and
the same treatment timing as the real design, so block, staggered and
non-absorbing patterns are all reproduced faithfully — and the estimator is
refit at the fit's cross-validated penalties (the true effect is zero by
construction). The observed ATT is then placed against that null.
trop_placebo_test( object, B = 500L, alternative = c("two.sided", "greater", "less"), control = NULL ) ## S3 method for class 'trop_placebo_test' print(x, digits = 4, ...)trop_placebo_test( object, B = 500L, alternative = c("two.sided", "greater", "less"), control = NULL ) ## S3 method for class 'trop_placebo_test' print(x, digits = 4, ...)
object |
A |
B |
Number of placebo assignments to draw. |
alternative |
Direction of the test: |
control |
A |
x |
A |
digits |
Number of significant digits to print. |
... |
Unused. |
This is a randomisation/permutation test, not a standard error: it answers
"could a TROP design like mine produce an effect this large when there is none?"
For interval estimates use the bootstrap or jackknife (see trop() and
compare_se_modes()).
An object of class trop_placebo_test (a list) with the observed
ATT, the vector of placebo ATTs, the p.value, the placebo-null mean
and sd, a central null interval null.low/null.high, alternative,
B, and n_treated_units. It has print() and autoplot() methods.
df <- sim_panel(N = 16, T = 9, n_treated = 3, t0 = 6, att = 3, seed = 1) fit <- trop(df, "y", "w", "id", "t", se = "none", control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) pt <- trop_placebo_test(fit, B = 50) ptdf <- sim_panel(N = 16, T = 9, n_treated = 3, t0 = 6, att = 3, seed = 1) fit <- trop(df, "y", "w", "id", "t", se = "none", control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) pt <- trop_placebo_test(fit, B = 50) pt
Sweeps any two of the three TROP penalties — ,
and — against each other while
holding the third fixed, and records at each grid point both the resulting
ATT estimate and the leave-one-out cross-validation loss. This is the data
behind the diagnostic heatmap: colour the cells by CV loss to see where the
data-driven choice lands, and read the ATT off each cell to judge how
sensitive the estimate is to the penalties. Use axes to choose which two
penalties go on the x and y axes; the remaining (fixed) penalty is reported
in the heatmap subtitle.
trop_sensitivity( data, outcome, treatment, unit, time, lambda_time = NULL, lambda_nn = NULL, lambda_unit = NULL, axes = c("nn", "time"), anchor = "pooled", control = trop_control(), seed = NULL, verbose = FALSE )trop_sensitivity( data, outcome, treatment, unit, time, lambda_time = NULL, lambda_nn = NULL, lambda_unit = NULL, axes = c("nn", "time"), anchor = "pooled", control = trop_control(), seed = NULL, verbose = FALSE )
data |
A long |
outcome, treatment, unit, time
|
Column names (strings). |
lambda_time, lambda_nn, lambda_unit
|
Penalty inputs. For a penalty that
is on an axis (see |
axes |
Length-2 character vector naming the two penalties to sweep, as
|
anchor |
Estimation anchor passed to the ATT computation
( |
control |
A list of solver/CV controls from |
seed |
Optional integer seed for CV-cell sampling. |
verbose |
Logical; print progress. |
A cf_trop_grid (a data.frame) with columns lambda_time,
lambda_nn, lambda_unit, att, cv_loss. The two swept penalties vary
across rows; the fixed penalty is constant. The grid point minimising
cv_loss (the data-driven choice) is stored in attr(., "selected"), and
the chosen layout in attr(., "axes") (named x/y) and
attr(., "fixed").
trop(), autoplot.cf_trop_grid()
df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) g <- trop_sensitivity(df, "y", "w", "id", "t", lambda_time = c(0, 0.1, 0.5), lambda_nn = c(2, 5), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) autoplot(g) # Sweep the unit penalty against the nuclear-norm penalty instead, holding # the time penalty fixed (chosen by CV): g2 <- trop_sensitivity(df, "y", "w", "id", "t", axes = c("nn", "unit"), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) autoplot(g2)df <- sim_panel(N = 14, T = 9, n_treated = 3, t0 = 6, att = 2, seed = 1) g <- trop_sensitivity(df, "y", "w", "id", "t", lambda_time = c(0, 0.1, 0.5), lambda_nn = c(2, 5), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) autoplot(g) # Sweep the unit penalty against the nuclear-norm penalty instead, holding # the time penalty fixed (chosen by CV): g2 <- trop_sensitivity(df, "y", "w", "id", "t", axes = c("nn", "unit"), control = trop_control(n_cv_cells = 5L, cv_cycles = 1L)) autoplot(g2)
Convenience accessor for the ground-truth average treatment effect on the
treated of a simulated panel. It reads the per-cell true effect — the tau
column produced by sim_semisynthetic(), or y - y0 for a sim_panel()
panel — and averages it over the actively treated cells, giving the number
estimators are trying to recover. Pair it with panel_compare() or
panel_rmse() to score estimates against the truth without re-deriving it by
hand.
true_att(data, treatment = "w", tau = "tau")true_att(data, treatment = "w", tau = "tau")
data |
A long |
treatment |
Name of the 0/1 treatment column (default |
tau |
Name of the true-effect column (default |
A single numeric: the true ATT averaged over treated cells.
sim_semisynthetic(), sim_panel(), panel_compare()
real <- sim_panel(N = 16, T = 10, n_treated = 0L, att = 0, seed = 1) ss <- sim_semisynthetic(real, "y", "id", "t", n_treated = 6, t0 = 8, att = 3, seed = 2) true_att(ss) # 3real <- sim_panel(N = 16, T = 10, n_treated = 0L, att = 0, seed = 1) ss <- sim_semisynthetic(real, "y", "id", "t", n_treated = 6, t0 = 8, att = 3, seed = 2) true_att(ss) # 3