--- title: "ppsynth: partial pooling for synthetic control with multiple outcomes" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{ppsynth: partial pooling for synthetic control with multiple outcomes} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 5) ``` `ppsynth` implements the estimator in Grier (2026), "Correlated Weights: Partial Pooling for Synthetic Control with Multiple Outcomes". It is the R counterpart of the Stata command `ppsynth`, with the same options, the same returned quantities and the same printed display, and its engines are certified against the Python engine the Stata command calls. A study that applies the synthetic control method to several outcomes faces a choice: fit each outcome its own donor weights, or impose one common set on all of them. `ppsynth()` makes the middle available. Each outcome gets its own weights, their dispersion around a common centroid is penalized, no single penalty is ever selected (the estimator aggregates over a grid by leave-one-out exponential weighting), and the resulting mass profile reports how much pooling the data support. ## The worked example: German reunification The package ships the panel of Section 5A of the paper: five outcomes for 21 OECD countries, 1970-2004, Germany (`id = 8`) treated from 1990. ```{r} library(ppsynth) data(germany_panel) outs <- c("proprights", "money", "inttrade", "regulation", "csh_g") fit <- ppsynth(germany_panel, outcomes = outs, id = "id", time = "year", trunit = 8, trperiod = 1990) fit ``` This mirrors the Stata call ``` ppsynth proprights money inttrade regulation csh_g, trunit(8) trperiod(1990) id(id) time(year) ``` and prints the same display: the per-outcome mean post-treatment effects and pre-RMSPE, then the realized weight pooling of the final estimator and the middle 50% of predictive mass on the pooling scale. The lambda-grid mass profile (the Stata `massprofile` option) is one argument away: ```{r} print(fit, massprofile = TRUE) ``` The mass profile is an interior hump: the data reject common weights but do not endorse fully separate fits either. The per-outcome donor weights (Stata's `r(gamma)`) and the effects: ```{r} round(fit$gamma, 2) fit$tau ``` ## Graphs ```{r} plot(fit, which = "paths") # treated (solid) vs synthetic (dash), one panel per outcome plot(fit, which = "mass") # aggregation mass over the lambda grid ``` ## Placebo inference `inference = TRUE` reruns the entire pipeline, aggregation included, for each donor treated as a placebo, and reports per-outcome and joint p-values under the Phipson-Smyth convention by default (`convention = "strict"` or `likesynth = TRUE` give the synth-style strict count). On this panel the 21 re-estimations take a minute or two on one core; `cores` spreads them across cores without changing any number. ```{r, eval = FALSE} inf <- ppsynth(germany_panel, outcomes = outs, id = "id", time = "year", trunit = 8, trperiod = 1990, inference = TRUE, cores = 2) print(inf, effects = TRUE) plot(inf, which = "gaps") ``` ## Matrix interface `fit_ppsynth()` and `placebo_inference()` take the data as lists of matrices, exactly like the Python functions of the same name, and return the same fields. ```{r} Y_tr <- lapply(outs, function(v) germany_panel[[v]][germany_panel$id == 8]) Y_dn <- lapply(outs, function(v) sapply(setdiff(sort(unique(germany_panel$id)), 8), function(u) germany_panel[[v]][germany_panel$id == u])) eng <- fit_ppsynth(Y_tr, Y_dn, T0 = 20) round(eng$agg_mass, 3) ``` ## Engines and certification Two engines run the same algorithm with the same solver settings: `engine = "cpp"` (the default) is a compiled inner solver mirroring the numba engine behind the Stata command, and `engine = "r"` is a pure-R reference solver. Every returned quantity of both engines is compared against the Python engines' output on the package's fixtures (`inst/certification/fixtures`, generated by `tools/make_fixtures.py`); the measured deviations are recorded in `inst/certification/results` and the package tests enforce them.