---
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.
