PROPSC — Treatment Effects on Proportions with Synthetic Controls#
When to use it#
Reach for PROPSC when the outcome is not a single number but a composition — a vector of proportions that sum to a whole. Party vote shares that sum to 100 percent, a budget split across spending categories, market share across brands, or a turnout-versus-abstention breakdown are all compositional. The defining feature is a sum constraint: if one share goes up under treatment, another must come down, so the treatment effects on the components must sum to zero.
The usual practice is to run a separate synthetic control for each component. The trouble is that each fit picks its own donor weights, so every component is compared against a different synthetic control. The estimated effects then need not sum to zero, which is logically incoherent for shares of a whole — an artefact of the method, not the data. PROPSC removes the incoherence by fitting a single set of donor (and, for synthetic difference-in-differences, time) weights jointly across all components, so every share is compared against the same synthetic control. This “constant control comparison” makes the estimated effects sum to zero by construction, following Bogatyrev and Stoetzer (2026).
If you only care about one component in isolation and the confounders are specific to that component, a single-outcome estimator such as Synthetic Difference-in-Differences (SDID) or Synthetic Control with Multiple Outcomes (SCMO) may be preferable. PROPSC is for when you want a coherent read of the whole composition at once.
Notation#
We observe a balanced panel of \(N\) units over \(T\) periods. The outcome for unit \(i\) at time \(t\) is a vector of \(K\) proportions \((y_{it1}, \dots, y_{itK})\) with \(0 \le y_{itk} \le 1\) and \(\sum_{k=1}^{K} y_{itk} = 1\). A block of treated units switches on simultaneously in the last \(T - T_0\) periods; the remaining \(N_0\) control units are never treated. The target is the average treatment effect on the treated for each component,
where \(y_{iTk}(d)\) is the potential outcome under treatment status \(d\). Because the proportions sum to one in both potential states, the effects obey the sum constraint \(\sum_{k=1}^{K} \tau_k = 0\).
PROPSC estimates each \(\tau_k\) with the synthetic-DID double difference using common unit weights \(\omega_j\) (over the \(N_0\) controls, on the simplex) and common time weights \(\lambda_t\) (over the \(T_0\) pre-periods), shared across all \(K\) components:
The weights are chosen to fit the pre-treatment trajectories jointly across all components, so the same \(\omega\) and \(\lambda\) enter every \(\hat{\tau}_k\).
Assumptions#
Compositional outcome. The \(K\) components are non-negative and sum to a constant (one) in every unit-time cell.
Remark. Code an “other” or “abstention” category explicitly so the vector is complete; the sum-to-zero coherence only holds for the full composition.
Simultaneous adoption. All treated units adopt in the same period with no reversals, and the control units are never treated (a single treatment block).
Remark. Staggered adoption is out of scope for PROPSC; use Synthetic Difference-in-Differences (SDID) or Sequential Synthetic Difference-in-Differences (Sequential SDiD) for a single outcome under staggered timing.
Pre-treatment fit. A convex, common combination of control units can track the treated units’ pre-treatment paths across all components jointly.
Remark. Common weights need only fit the composition as a whole well; exact fit for every single component is not required and rarely achievable.
No anticipation / stable composition. Potential outcomes in the pre-period are unaffected by the (future) treatment, and the set of components is fixed.
Remark. A newly formed party with no past votes can still be carried as a component coded zero pre-treatment; the common weights borrow strength from the other components.
Inference and diagnostics#
The default standard errors are the fixed-weights jackknife of Arkhangelsky et
al. (2021), adapted to the multivariate estimator: unit weights are held fixed
(renormalised over the retained controls) and the \(K\)-vector of effects is
recomputed on each leave-one-unit-out panel. Set inference="none" to skip
it. The jackknife is undefined with a single treated unit (it returns NaN);
the paper also discusses bootstrap and placebo alternatives.
The headline diagnostic is sum_constraint — the sum of the estimated effects
across components, which is zero to machine precision by construction and is a
direct check that the composition is coherent.
Example#
A one-draw compositional panel from a latent-factor DGP: \(K = 3\) proportions built by a softmax of unit-and-time latent factors, with a treatment bump on the first component for a block of treated units. PROPSC recovers the three effects and reports a sum of zero.
import numpy as np
import pandas as pd
from mlsynth import PROPSC
rng = np.random.default_rng(0)
N, T, K, N0, T0 = 30, 8, 3, 24, 5
load = rng.standard_normal((N, K))
rows = []
for i in range(N):
for t in range(T):
lat = 0.4 * load[i] + 0.15 * t * np.arange(K)
treated = i >= N0
if treated and t >= T0:
lat[0] += 1.0 # true effect on component 1
p = np.exp(lat - lat.max())
p = p / p.sum()
row = {"unit": f"u{i:02d}", "time": t,
"treat": int(treated and t >= T0)}
for k in range(K):
row[f"share{k + 1}"] = float(p[k])
rows.append(row)
df = pd.DataFrame(rows)
res = PROPSC({
"df": df, "outcomes": ["share1", "share2", "share3"],
"treat": "treat", "unitid": "unit", "time": "time",
"method": "sdid", "display_graphs": False,
}).fit()
print("effects per component:", np.round(res.att_vector, 4))
print("sum of effects (coherence):", round(res.sum_constraint, 12))
print("effect on share1:", round(res.att, 4), "95% CI:", res.att_ci)
The positive effect lands on share1 (the component that was bumped) and is
offset by declines in the others, so sum_constraint is zero.
Verification#
PROPSC reproduces the authors’ R package propsdid cell by cell. The
benchmark benchmarks/cases/propsc_spain.py installs propsdid at a pinned
commit and diffs PROPSC.fit() against a live run on the paper’s Spain “Just
Transition” panel, reproducing Table 2 (common-weights column) to roughly
\(10^{-11}\). See PROPSC — Treatment Effects on Proportions (Bogatyrev & Stoetzer 2026) for the full validation,
including the Poland application.
Core API#
- class mlsynth.PROPSC(config: PROPSCConfig | dict)#
Bases:
objectCompositional common-weights SC/SDID estimator.
- Parameters:
config (PROPSCConfig or dict) – Validated configuration. Beyond the common fields (
df,treat,unitid,time,display_graphs,save, colors), PROPSC readsoutcomes(theKproportion columns),method(sdid/sc/did),target(which proportion drives the flat accessors), andinference.
References
Bogatyrev, K., & Stoetzer, L. F. (2026). Estimating Treatment Effects on Proportions with Synthetic Controls. Political Analysis.
- fit() PROPSCResults#
Assemble the panel, fit the common weights, and return results.
- Returns:
PROPSCResults – Observational report. The flat accessors (
att/att_ci/counterfactual/gap/donor_weights/pre_rmse) resolve to thetargetproportion; the full compositional output is inproportions/att_vector/se_vector/sum_constraint.
- class mlsynth.config_models.PROPSCConfig(*, df: ~pandas.DataFrame, outcome: str, treat: str, unitid: str, time: str, display_graphs: bool = True, save: bool | str = False, counterfactual_color: ~typing.List[str] = <factory>, treated_color: str = 'black', plot: ~mlsynth.config_models.PlotConfig = <factory>, outcomes: ~typing.Annotated[~typing.List[str], ~annotated_types.MinLen(min_length=2)], method: ~typing.Literal['sdid', 'sc', 'did'] = 'sdid', target: str | None = None, inference: ~typing.Literal['jackknife', 'none'] = 'jackknife')#
Bases:
BaseEstimatorConfigConfiguration for the compositional common-weights SC/SDID estimator.
Beyond the common fields (
df,treat,unitid,time,display_graphs,save, colors), PROPSC readsoutcomes(theKproportion columns forming the composition),method(sdid/sc/did),target(which proportion drives the flat accessors), andinference.- inference: Literal['jackknife', 'none']#
- method: Literal['sdid', 'sc', 'did']#
- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid'}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mlsynth.utils.propsc_helpers.structures.PROPSCResults(*, effects: EffectsResults | None = None, fit_diagnostics: FitDiagnosticsResults | None = None, time_series: TimeSeriesResults | None = None, weights: WeightsResults | None = None, inference: InferenceResults | None = None, method_details: MethodDetailsResults | None = None, sub_method_results: Dict[str, Any] | None = None, additional_outputs: Dict[str, Any] | None = None, raw_results: Dict[str, Any] | None = None, execution_summary: Dict[str, Any] | None = None, plot_config: PlotConfig | None = None, proportions: Tuple[PropscProportionFit, ...], att_vector: ndarray, se_vector: ndarray, sum_constraint: float, method: str, target: str, time_weights: ndarray)#
Bases:
BaseEstimatorResultsPublic
PROPSC.fit()return container.An
EffectResult: the standardized sub-models (effects/time_series/weights/inference/fit_diagnostics/method_details) are populated for the target proportion, so the flat accessors (att/att_ci/counterfactual/gap/donor_weights/pre_rmse) resolve through the base contract. The full compositional output stays in the typed fields below.- Parameters:
proportions (Tuple[PropscProportionFit, …]) – Per-proportion effects and trajectories, in outcome order.
att_vector (np.ndarray) – The
KATTs, aligned withproportions.se_vector (np.ndarray) – The
Kjackknife standard errors.sum_constraint (float) –
sum(att_vector); zero to machine precision by construction.method (str) –
"sdid","sc", or"did".target (str) – Name of the proportion driving the flat accessors.
time_weights (np.ndarray) – Common time weights (empty for
sc).
- att_vector: np.ndarray#
- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'frozen': True, 'json_encoders': {<class 'numpy.ndarray'>: <function BaseEstimatorResults.Config.<lambda>>}}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- proportions: Tuple[PropscProportionFit, ...]#
- se_vector: np.ndarray#
- time_weights: np.ndarray#
References#
Bogatyrev, K., and L. F. Stoetzer (2026). “Estimating Treatment Effects on Proportions with Synthetic Controls.” Political Analysis.
Arkhangelsky, D., S. Athey, D. A. Hirshberg, G. W. Imbens, and S. Wager (2021). “Synthetic Difference-in-Differences.” American Economic Review 111(12).