Note:
cfcompareis an independent R package. This is not officially endorsed or maintained by the authors of the TROP article (Athey, Imbens, Qu & Viviano, 2026). Official TROP software by the authors includes the Python packagetrop(zhaonanq/TROP-Estimator) and the Stata command (justinwaddy/TROP). Please cite the original TROP paper when using the TROP estimator. See Citation for more details.
cfcompare implements the triply robust panel estimator
(TROP) and puts it in a common R workflow with DID/TWFE, synthetic
control, matrix completion, synthetic DID, and DIFP. This package
enables not just to estimate TROP, but also to compare various ATT
estimators on the same schema.
Use cfcompare when you want to:
panel_compare();panel_rmse();trop_sensitivity and plot_trop_surfaces (for
CV-loss and ATT surfaces).Note: all plots and tables shown in this README are based on a model sample dataset, and they are provided solely as examples of the visualizations that can be created with this package.
# install.packages("pak")
pak::pak("takuma1102/cfcompare")You can also install from R-universe. This package will be submitted to CRAN as well down the line.
install.packages(
"cfcompare",
repos = c("https://takuma1102.r-universe.dev", "https://cloud.r-project.org")
)Most of the core native engines run without optional estimator
packages. Install synthdid, gsynth,
augsynth, or did for the wrapped methods you
plan to use. gsynth and did are on CRAN;
synthdid and augsynth are on GitHub only:
pak::pak(c("synth-inference/synthdid", "ebenmichael/augsynth"))library(cfcompare)
# Compare multiple estimators.
cmp <- panel_compare(
df,
outcome = "y", treatment = "w", unit = "id", time = "t",
methods = c("DID", "SDID", "SC", "MC", "DIFP", "TROP"),
se = "bootstrap", # You can also choose jackknife or "none" for running this faster.
# covariates = c("x1", "x2"),
anchor = "pooled", # faster method
# or "per-cell" (more accurate; in accordance with the original paper)
# or "auto" (choosing "per-cell" or "pooled" depending on the number of treated cells)
)
cmp$att # tidy ATT table, one row per method
autoplot(cmp) # forest plot of ATT estimates and intervals (see below for an example plot)
| Method | Engine | panel_compare() default? |
Notes |
|---|---|---|---|
TROP |
native R | yes | low-rank + two-way FE outcome model with unit/time weights |
DID |
native R | yes | two-way fixed effects |
SC |
synthdid |
yes | skipped if synthdid is unavailable or the design is
unsupported |
MC |
native R | yes | nuclear-norm matrix completion |
SDID |
synthdid |
yes | skipped if synthdid is unavailable or the design is
unsupported |
DIFP |
native R | yes | Doudchenko-Imbens-Ferman-Pinto estimator: demeaned SC with intercept |
gsynth |
gsynth |
no | optional interactive-fixed-effects / MC-style engine |
augsynth |
augsynth |
no | optional augmented synthetic control engine |
CS |
did |
no | Callaway–Sant’Anna; requires an absorbing staggered treatment |
By default, panel_compare() runs TROP,
DID, SC, MC, SDID,
and DIFP. Use methods = to specify an explicit
set, or exclude = to drop one method from the default set.
Optional engines whose package is missing, or whose design requirements
are not met (e.g., in terms of covariates), are skipped with a message
while the remaining methods still run.
cfcompare has basic functions for estimating the TROP
estimator (the official Python and Stata projects are referenced). It
also adds an R-native comparison and diagnostic layer around TROP:
| Goal | Entry point | Output |
|---|---|---|
| Compare multiple ATT estimators | panel_compare() |
cf_comparison with a common cf_att_tbl,
fits, panel data, and counterfactuals |
| Compare held-out predictive performance | panel_rmse() |
ranked RMSE table; this is separate from TROP tuning cross-validation |
| Track estimation RMSE vs design size | rmse_curve() / rmse_curves() |
sqrt(E[(tau_hat - tau)^2]) vs N, T, #treated, or #post,
with a known true ATT |
| Inspect TROP penalty sensitivity | trop_sensitivity() + autoplot() |
lambda grid with CV loss and ATT at each grid point |
| Plot CV-loss / ATT surfaces | plot_trop_surfaces() |
separate full-width CV-loss and ATT surface plots; returns surface matrices invisibly |
| Audit penalty components | trop_ablation() |
table moving from full TROP toward MC and DID by constraining penalties |
| Compare inference choices | compare_se_modes() |
one ATT estimate with bootstrap, jackknife, and/or placebo uncertainty rows |
| Reuse fitted counterfactuals | counterfactual_matrix() |
common N x T estimated untreated-outcome matrix
interface |
panel_rmse() compares estimators by how well they
predict held-out outcomes. The default metric = "placebo"
repeatedly assigns a placebo block to control units and scores the
resulting zero-effect placebo ATT.
r <- panel_rmse(
df, outcome = "y", treatment = "w", unit = "id", time = "t",
methods = c("DID", "SDID", "SC", "MC", "DIFP", "TROP"),
horizon = 4, n_pseudo = 6, n_runs = 100, seed = 1
)
r # ranked table: method, rmse, rmse_se, engine, note
autoplot(r) # see below for an example plot
metric = "prediction" scores per-cell counterfactual
prediction error.

For quick diagnostics on large panels, reduce the outer repetition
counts, such as n_runs, and use lighter TROP controls via
trop_control(n_cv_cells = , cv_cycles = , max_iter = , workers = ).
Note: This is a predictive error on held-out cells, not estimation error against a known effect. It is a different quantity from the estimation RMSE
sqrt(E[(tau_hat - tau)^2])reported byrmse_curves()andrmse_curve()(for a single-dimension plot) over Monte Carlo replications with a known true ATT. These two can rank methods differently.
Run a single TROP fit when you need the selected penalties, per-treated-cell effects, or the fitted untreated counterfactual matrix.
fit <- trop(df, "y", "w", "id", "t")
fit$lambda # CV-selected (time, unit, nn) penalties
fit$tau_cells # per-treated-cell effects
counterfactual_matrix(fit) # fitted N x T untreated-outcome matrix
autoplot(fit) # synthetic-control-style trajectory; see below
Inspect the TROP penalty surface by sweeping any two of the three
penalties (time, unit, nn) and
holding the third fixed.
g <- trop_sensitivity(
df, "y", "w", "id", "t",
axes = c("unit", "time"), # You can also choose "nn" for lambda_nn
anchor = "pooled" #
)
autoplot(g) # compact ggplot2 heatmap (see below)
surfaces <- plot_trop_surfaces(g, which = "both", ask = FALSE) (see below)
surfaces$cv_loss # matrix behind the CV-loss surface
surfaces$att # matrix behind the ATT surface

Other targeted diagnostics are available when needed.
Use trop_ablation when you want to check how RMSEs
change in accordance with the change in lambda penalties. (See also
Table 5 in the original paper.)
trop_ablation(df, "y", "w", "id", "t") # see below
Use compare_se_modes when you want to compare bootstrap
and jackknife standard errors.
compare_se_modes(df, "y", "w", "id", "t", se = c("bootstrap", "jackknife")) # see below
For event-study plots, use trop_event_study.
trop_event_study(fit, se = "bootstrap")
For placebo test relabeling control units as pseudo-treated, use
trop_placebo_test() (penalties are held fixed).
pt <- trop_placebo_test(
fit, B = 500L, alternative = "two.sided"
)
autoplot(pt) # histogram of the null; observed ATT (red solid line) and 0 (dashed); see below.
The treatment column is the active 0/1 treatment indicator for each
unit-time cell. The TROP diagnosis handles block, staggered, and
non-absorbing treatment patterns under the same schema. By contrast,
SDID and SC are block-design methods and are
skipped otherwise. CS is opt-in and requires absorbing
staggered treatment.
Native estimators support se = "bootstrap",
"jackknife", and "none" where applicable.
These are practical resampling choices for the R workflow; use the TROP
paper for formal inference conditions.
The TROP engine in this package is written in base R. For correctness
checks, it has been compared with the official Python trop
package on exactly comparable weighted-TWFE sample datasets, using
trop_control(svd = "full"), and the results agree to
numerical tolerance.
How the TROP paper, the official Python package, the official Stata
command and cfcompare handle the main implementation
choices – weight anchoring, cross-validation, standardization, inference
– and how that has changed across releases is tracked, with dated
comparison tables and notes on this package’s choices, in the article TROP
implementations compared: the paper, Python, Stata and R.
For more details, use the package help topics:
?panel_compare
?panel_rmse
?trop
?trop_sensitivity
?trop_ablation
?compare_se_modesA walk-through is in the Getting started article, and every function is documented in the reference on the package website.
Experimental. The output schema is intended to be stable, but wrapped estimator packages can change their internal objects. Pin package versions in production code.
cfcompare is an independent package to implement TROP.
If you use the TROP estimator, please cite the original paper:
Also cite the underlying packages for any wrapped estimators you use
(synthdid, gsynth, augsynth, and
did).