Continuous-Treatment Synthetic Control (CTSC)

Contents

Continuous-Treatment Synthetic Control (CTSC)#

Overview#

CTSC (Powell, D. (2022). “Synthetic Control Estimation Beyond Comparative Case Studies: Does the Minimum Wage Reduce Employment?,” Journal of Business & Economic Statistics 40(3):1302-1314) generalises the synthetic control method to settings the original was never built for: continuous and/or multi-valued treatments in panels where there is no clean treated / never-treated split.

The canonical synthetic control estimates the effect of one unit adopting a single binary policy, using never-treated units as donors. But many policy variables are continuous and adopted (and changed) by every unit – every U.S. state has a minimum wage that moves over time, so there is no “untreated” state to serve as a clean control. CTSC (the paper’s “GSC”) handles exactly this case.

Note

The paper names the estimator GSC (generalized synthetic control). mlsynth calls it CTSC to avoid collision with Xu (2017)’s differently constructed Generalized Synthetic Control (gsynth), which is an interactive-fixed-effects estimator.

What CTSC does#

  • Builds a synthetic control for every unit out of the other units’ untreated outcomes.

  • Jointly estimates a unit-specific treatment-slope vector \(\alpha_i\) (allowing fully heterogeneous marginal effects) and the synthetic-control weights.

  • Reports the population-weighted average marginal effect \(\alpha^{AE} = \sum_i \pi_i \alpha_i\).

  • Permits an interactive-fixed-effects (factor) outcome structure, so it is consistent when the treatment is correlated with unobserved factors and trends – precisely the case where two-way fixed-effects regression is badly biased.

When to use CTSC#

  • The policy variable is continuous or multi-valued (minimum wage, tax rate, dosage) rather than a 0/1 indicator.

  • Every unit is “treated” with a time-varying intensity, so there is no never-treated donor pool and comparative-case-study SC cannot be applied.

  • You suspect the treatment is correlated with unobserved trends/factors, making two-way fixed effects inconsistent.

  • You want unit-specific marginal effects (heterogeneity), not just an average.

Assumptions (and how to spot violations)#

CTSC is a much more permissive estimator than vanilla SC – it does not require a clean treated/control split, it does not assume homogeneous effects, and it does not require the analyst to specify the number of factors. But it does still rely on a small set of identifying assumptions. Each is given here together with the symptom you would see if it failed.

  1. Interactive fixed-effects DGP for the untreated outcome. The paper assumes \(Y_{it}^N = \lambda_t' \mu_i + \epsilon_{it}\), i.e. the counterfactual (no-treatment) outcome is a low-rank factor model plus mean-zero idiosyncratic noise.

    Plausibly violated when the outcome has strong unit-specific nonlinear trends, structural breaks affecting only some units, or unit-specific deterministic time polynomials that cannot be written as \(\lambda_t' \mu_i\). Diagnostic: fit an interactive-FE model (e.g. gsynth) to the pre-/non-treatment data and inspect the residuals – if there is visible unit-specific curvature left, the factor structure is misspecified.

  2. Convex-hull condition on donor loadings. For each treated-period observation of unit \(i\), its factor loading \(\mu_i\) must lie (approximately) in the convex hull of the other units’ loadings, so that a simplex-weighted combination of donors can reproduce its untreated trajectory.

    Plausibly violated when the treated unit is an outlier on level, seasonality, or factor exposure – a coastal mega-state with no interior analog, a country with idiosyncratic policy that no donor shares. Diagnostic: look at the per-unit fit weights \(\Omega_i\) returned by CTSC; units with very poor untreated fit get heavily down-weighted, and if most units fall in that bucket the hull condition is failing population-wide.

  3. Large \(T\) regime (consistency, not factor count). Powell shows the estimator is consistent as \(T \to \infty\) without the user having to specify the rank of \(\lambda_t' \mu_i\). The trade-off is that very short panels leave the simplex weights and slopes weakly identified.

    Plausibly violated when \(T\) is on the order of \(n\) or smaller (so the per-unit least squares with \(n-1\) simplex weights becomes ill-posed). Diagnostic: re-run on a longer pre-period or aggregate to a coarser frequency and check whether the average effect and weights are stable; large swings suggest \(T\) is too short.

  4. Linearity of the outcome in the treatment vector. The paper writes \(Y_{it} = Y_{it}^N + D_{it}' \alpha_i\), i.e. the treatment effect is linear in \(D_{it}\) (with unit-specific slopes \(\alpha_i\)). Multi-valued \(D_{it}\) is fine, and interactions/polynomial terms can be entered as extra columns of \(D_{it}\), but CTSC does not estimate a fully nonparametric dose-response curve.

    Plausibly violated when the dose-response is strongly nonlinear inside the support and you have not encoded the nonlinearity (e.g. sharp regime-switching, kinked schedules). Diagnostic: add a quadratic or spline term to treatment_vars and test whether the higher-order coefficient is jointly significant under the sign-flip distribution.

  5. Slope heterogeneity \(\alpha_i\) is unit-specific but time-invariant. Each unit gets its own marginal effect, but it does not drift over time within a unit.

    Plausibly violated when the marginal effect itself moves – a minimum-wage elasticity that changes after a labour-market reform, a dose-response that shifts when patient populations change. Diagnostic: split the post-treatment window in half, refit, and compare the recovered \(\alpha_i\); large within-unit drift indicates the time-invariance assumption is binding.

  6. No simultaneity in \(D_{it}\). CTSC allows \(D_{it}\) to be correlated with the factors \(\lambda_t' \mu_i\) (this is the whole point), but it still assumes \(D_{it}\) is mean-independent of the idiosyncratic shock \(\epsilon_{it}\). Reverse causality from contemporaneous \(\epsilon_{it}\) to \(D_{it}\) breaks identification.

    Plausibly violated when the policy responds within the same period to the outcome – e.g. the minimum wage being raised because employment surprised on the upside this quarter. Diagnostic: regress \(\Delta D_{it}\) on lagged residuals from a no-treatment factor fit; significant feedback is a red flag.

When not to use CTSC#

  • Single treated unit with a clean binary policy. A canonical comparative-case-study set-up (one state passes one law on one date, others never do) is exactly what vanilla SC was built for; CTSC’s per-unit weight system is unnecessary overhead and its sign-flip inference is weaker than the placebo / conformal inference available in the binary-treatment world. Use Two-Step Synthetic Control or Forward Difference-in-Differences (FDID).

  • Short panels. Powell’s consistency story is in \(T \to \infty\). With \(T \lesssim n\) the per-unit least squares with \(n-1\) simplex weights is ill-posed and the average effect can swing dramatically with the seed. Prefer Forward Difference-in-Differences (FDID) (which is designed for short panels by stepwise donor selection) or a factor estimator that explicitly regularises the rank.

  • Treated trajectory outside the donor convex hull. If the treated unit’s untreated trend cannot be expressed as a simplex combination of the donors’ untreated trends – coastal vs. interior states, an outlier sector, a country with idiosyncratic seasonality – CTSC has no fix; the per-unit \(\Omega_i\) will collapse and the average effect is dominated by a handful of well-fit units. Use Factor Model Approach (FMA) or Synthetic Control with Multiple Outcomes (SCMO) (auxiliary outcomes) to widen the donor information set before forcing a hull fit.

  • Treatment effect that drifts over time within a unit. CTSC fixes \(\alpha_i\) across time. If the dose-response is genuinely time-varying (a minimum-wage elasticity that changes after a recession, a drug whose effect attenuates with tolerance), CTSC will return an average across the post-window that masks the dynamics. Use a time-varying-effects estimator (Time-Aware Synthetic Control (TASC) for state-space dynamics, Dynamic Synthetic Control for Auto-Regressive processes (DSCAR) for autoregressive treated processes) instead.

  • Strongly nonlinear or kinked dose-response that you cannot encode. CTSC is linear in \(D_{it}\). If the policy effect has a sharp kink or saturation that no parsimonious basis expansion captures, fall back to a doubly-robust panel estimator or a changes-in-changes design.

  • Contemporaneous reverse causality from outcome to treatment. CTSC permits the treatment to be correlated with unobserved factors, but not with the same-period idiosyncratic shock. If the policy is set in response to within-period outcome surprises, you need an instrument or a timing-based identification strategy; CTSC alone is not enough.

Mathematical Formulation#

Model (paper eq. 4)#

\[Y_{it} = \lambda_t' \mu_i + D_{it}' \alpha_i + \epsilon_{it},\]

where \(D_{it}\) is a \(K\)-vector of (continuous/discrete) treatment variables, \(\lambda_t' \mu_i\) is an interactive fixed-effects (factor) term, and \(\alpha_i\) is the unit-specific slope.

Joint estimation (paper eq. 5-6)#

CTSC minimises, over slopes \(b\) and per-unit simplex weights \(\phi\),

\[\frac{1}{2nT}\sum_i \Omega_i^{-1} \sum_t \Bigl[ Y_{it} - D_{it}' b_i - \sum_{j \ne i} \phi_j^i (Y_{jt} - D_{jt}' b_j) \Bigr]^2, \quad \phi_j^i \ge 0,\ \sum_{j \ne i} \phi_j^i = 1,\]

where \(Y_{it} - D_{it}' b_i\) is unit \(i\)’s untreated outcome, its synthetic control is a convex combination of the other units’ untreated outcomes, and \(\Omega_i\) is a per-unit fit weight (a two-step measure, eq. 6) that down-weights units lacking a good synthetic control. The average effect (paper eq. 7) is \(\alpha^{AE} = \sum_i \pi_i \alpha_i\).

Inference (paper Section 4)#

Because CTSC mechanically correlates units (each control is built from the others), inference uses unit-level moment scores at the null-restricted estimate and calibrates a Wald statistic with a Rademacher (sign-flip) randomization distribution, valid under arbitrary within- and cross-unit dependence.

Implementation note#

The paper minimises the objective with Nelder-Mead over all \(nK + n(n-1)\) parameters. mlsynth exploits the biconvex structure – weighted linear least squares in the slopes for fixed weights (a single closed-form linear solve) and \(n\) independent simplex-constrained least squares in the weights for fixed slopes – and solves it by block coordinate descent. This optimises the same objective with far better stability and speed. The null-restricted fit used for inference imposes \(\sum_i \pi_i \alpha_i = a_0\) via a KKT-augmented linear system.

Calibration to the paper’s simulation#

mlsynth.utils.ctsc_helpers.simulation reproduces the paper’s Section 5 / Table 1 Monte Carlo (Models 1-4). The data-generating process is

\[Y_{it} = \beta_i d_{it} + 5 \sum_{k=1}^{2} \lambda_t^{(k)} \mu_i^{(k)} + \epsilon_{it},\]

with piecewise factor paths \(\lambda_t^{(k)}\), \(\mu_i^{(k)} \sim U(0,1)\), \(\epsilon_{it} \sim N(0, \tfrac14)\), and \(\beta_i = \sum_k \mu_i^{(k)} - \tfrac1n \sum_i \sum_k \mu_i^{(k)}\) (so the true average effect is exactly zero). The continuous treatment \(d_{it}\) is a function of the same factors, so it correlates with the interactive fixed effects.

Reproduced calibration (mlsynth, fewer Monte-Carlo draws than the paper’s 1000):

Model

CTSC mean bias (paper)

Two-way FE bias (paper)

1 (n=10)

~0.00 (0.011)

~0.80 (0.850)

2 (n=30)

~0.00 (0.005)

~0.82 (0.846)

3 (within)

~0.00 (-0.001)

~0.49 (0.423)

4 (T=20)

~0.01 (-0.002)

~0.53 (0.263)

CTSC is essentially unbiased across all models while two-way fixed effects is badly biased; the inference rejects the (true) zero null at roughly the nominal 5% rate (paper Panel B ~0.044).

from mlsynth.utils.ctsc_helpers.simulation import run_simulation

for model in (1, 2, 3, 4):
    s = run_simulation(model, n_sims=100, seed=0)
    print(f"Model {s.model}: CTSC bias={s.ctsc_mean_bias:+.3f} "
          f"MAD={s.ctsc_mad:.3f} | FE bias={s.fe_mean_bias:+.3f}")

Core API#

CTSC: Continuous-Treatment Synthetic Control (Powell 2022).

Powell, D. (2022). “Synthetic Control Estimation Beyond Comparative Case Studies: Does the Minimum Wage Reduce Employment?” Journal of Business & Economic Statistics 40(3):1302-1314.

The synthetic control method was built for a single treated unit adopting a binary policy. CTSC generalises it to settings with continuous and/or multi-valued treatments where there is no clean treated / never-treated split – every unit has a time-varying treatment (e.g. every U.S. state has a minimum wage that changes over time, so the comparative-case-study synthetic control cannot be applied).

CTSC builds a synthetic control for every unit out of the others’ untreated outcomes and jointly estimates a unit-specific treatment-slope vector \(\alpha_i\) together with the synthetic-control weights. The reported effect is the population-weighted average marginal effect \(\alpha^{AE} = \sum_i \pi_i \alpha_i\). Because the outcome model allows interactive fixed effects (factor structure) and unit-specific slopes, CTSC is consistent where two-way fixed-effects regressions are badly biased when the treatment is correlated with unobserved factors.

The paper names the estimator “GSC”; mlsynth uses CTSC to avoid collision with Xu (2017)’s differently constructed Generalized Synthetic Control (gsynth).

Implementation note#

The paper minimises the joint objective (eq. 5) with Nelder-Mead over all \(nK + n(n-1)\) parameters. mlsynth exploits the biconvex structure – weighted linear least squares in the slopes for fixed weights, and per-unit simplex-constrained least squares in the weights for fixed slopes – and solves it by block coordinate descent, which optimises the same objective far more stably. The bundled simulation module reproduces the paper’s Table 1 (Models 1-4) as a calibration check (CTSC mean bias \(\approx 0\) vs two-way FE bias \(\approx 0.85\)).

class mlsynth.estimators.ctsc.CTSC(config: CTSCConfig | dict)#

Bases: object

Continuous-Treatment Synthetic Control estimator.

Parameters:

config (CTSCConfig or dict) – Configuration object. See mlsynth.config_models.CTSCConfig.

fit() CTSCResults#

Run CTSC and return CTSCResults.

Configuration#

class mlsynth.config_models.CTSCConfig(*, 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', treatment_vars: ~typing.List[str], population_col: str | None = None, use_fit_weights: bool = True, inference: bool = True, n_draws: ~typing.Annotated[int, ~annotated_types.Ge(ge=100)] = 2000, random_state: int = 0)#

Configuration for the Continuous-Treatment Synthetic Control (CTSC).

Powell, D. (2022). “Synthetic Control Estimation Beyond Comparative Case Studies,” Journal of Business & Economic Statistics. Generalises synthetic control to continuous / multi-valued treatments with no clean treated/never-treated split; jointly estimates unit-specific treatment slopes and synthetic controls for all units. (The paper calls it “GSC”; mlsynth uses CTSC to avoid collision with Xu (2017)’s GSC.)

Parameters:
  • treatment_vars (list of str) – The K >= 1 treatment / explanatory columns (continuous or discrete). CTSC estimates an average marginal effect for each.

  • population_col (str, optional) – Time-invariant per-unit weight column for the average effect (e.g. population). Defaults to uniform weights.

  • use_fit_weights (bool) – Use the two-step per-unit fit weights Omega_i (paper eq. 6). Default True.

  • inference (bool) – Run the sign-flip Wald test of H0: alpha^AE = 0. Default True.

  • n_draws (int) – Rademacher draws for the randomization test.

  • random_state (int) – Seed for the randomization-test RNG.

Notes

The base treat field is unused by CTSC; provide the continuous / discrete treatment column(s) via treatment_vars instead. Pass any existing column name for treat to satisfy the base config.

inference: bool#
model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid'}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

n_draws: int#
population_col: str | None#
random_state: int#
treatment_vars: List[str]#
use_fit_weights: bool#

Helper Modules#

Panel ingestion for the CTSC estimator.

Unlike a binary-treatment estimator, CTSC takes one or more treatment / explanatory columns (continuous or discrete) that vary over units and time, and pivots them into a (n, T, K) array alongside the (n, T) outcome.

mlsynth.utils.ctsc_helpers.setup.prepare_ctsc_inputs(df: DataFrame, outcome: str, treatment_vars: Sequence[str], unitid: str, time: str, population_col: str | None = None) CTSCInputs#

Pivot a long panel into CTSCInputs.

Parameters:
  • df (pd.DataFrame) – Balanced long panel; one row per (unit, time).

  • outcome (str) – Outcome column.

  • treatment_vars (sequence of str) – The K >= 1 treatment / explanatory columns (continuous or discrete). CTSC estimates a marginal effect for each.

  • unitid, time (str) – Unit-id and time column names.

  • population_col (str, optional) – Time-invariant per-unit weight column for the average effect (e.g. population). Defaults to uniform weights.

Core estimation for the Continuous-Treatment Synthetic Control (CTSC).

Powell, D. (2022). “Synthetic Control Estimation Beyond Comparative Case Studies: Does the Minimum Wage Reduce Employment?” Journal of Business & Economic Statistics 40(3):1302-1314. (The paper calls the estimator “GSC”; mlsynth names it CTSC to avoid collision with Xu (2017)’s Generalized Synthetic Control.)

The estimator jointly fits, for every unit \(i\), a treatment-slope vector \(\alpha_i \in \mathbb{R}^K\) and a synthetic control over the other units’ untreated outcomes (paper eq. 5):

\[\min_{b, \phi}\ \frac{1}{2nT}\sum_i \Omega_i^{-1} \sum_t \Bigl[ Y_{it} - D_{it}' b_i - \sum_{j \ne i} \phi_j^i (Y_{jt} - D_{jt}' b_j) \Bigr]^2, \quad \phi_j^i \ge 0,\ \sum_{j \ne i} \phi_j^i = 1,\]

where \(Y_{it} - D_{it}' b_i\) is unit \(i\)’s untreated outcome and \(\Omega_i\) is a per-unit fit weight (eq. 6) that down-weights units without a good synthetic control.

The paper minimises this with Nelder-Mead over all \(nK + n(n-1)\) parameters. The objective is biconvex – a weighted linear least squares in the stacked slopes \(b\) for fixed weights \(\phi\), and \(n\) independent simplex-constrained least squares in \(\phi\) for fixed \(b\) – so mlsynth uses block coordinate descent (a single linear solve alternated with per-unit simplex QPs), which optimises the same objective far more stably.

mlsynth.utils.ctsc_helpers.estimate.fit_ctsc(Y: ndarray, D: ndarray, *, population_weights: ndarray | None = None, use_fit_weights: bool = True, restrict_ae: ndarray | None = None, omega: ndarray | None = None) dict#

Fit CTSC and return the unit-specific slopes and the average effect.

Parameters:
  • Y (np.ndarray) – Outcomes, shape (n, T).

  • D (np.ndarray) – Treatment / explanatory variables, shape (n, T, K).

  • population_weights (np.ndarray, optional) – Per-unit weights \(\pi_i\) for the average effect (eq. 7); defaults to uniform 1/n.

  • use_fit_weights (bool) – If True, use the two-step per-unit fit weights \(\Omega_i\) (eq. 6); if False, weight all units equally.

Returns:

  • dict with keys alpha (n, K) unit slopes, average_effect (K,),

  • weights (n, n) synthetic-control matrix, omega (n,) fit weights,

  • objective (float).

Wald / sign-flip inference for CTSC (Powell 2022, Section 4).

CTSC induces mechanical cross-unit correlation (each unit’s control is built from the others). The inference procedure forms unit-level moment scores at the restricted (null-imposed) estimate and calibrates a Wald statistic with a Rademacher (sign-flip) randomization distribution, permitting arbitrary within-unit and cross-unit dependence (Canay, Romano & Shaikh 2017; Powell 2019).

For treatment variable \(k\), the per-unit, per-period moment is (paper eq. 10)

\[h_{it}^{(k)} = \Bigl(D_{it}^{(k)} - \sum_{j \ne i} w_j^i D_{jt}^{(k)}\Bigr) \Bigl[ Y_{it} - D_{it}'\alpha_i - \sum_{j \ne i} w_j^i (Y_{jt} - D_{jt}'\alpha_j) \Bigr],\]

with mean zero under the null. The unit score is the time average \(s_i^{(k)} = \tfrac{1}{T}\sum_t h_{it}^{(k)}\).

This implementation uses the unit-level scores directly with the sign-flip test (a valid randomization test under sign symmetry of the unit scores); the paper’s optional PCA orthogonalisation of the scores is not applied.

mlsynth.utils.ctsc_helpers.inference.sign_flip_wald_inference(Y: ndarray, D: ndarray, pi: ndarray, omega: ndarray, *, null_value: ndarray | None = None, n_draws: int = 2000, random_state: int = 0) CTSCInference#

Run the sign-flip Wald test of H0: alpha^AE = null_value.

Re-fits CTSC under the average-effect restriction, forms the unit scores, and calibrates the Wald statistic by Rademacher sign flips. Also returns per-variable joint and marginal p-values and a score-spread standard error for the average effect.

Calibrated simulation study from Powell (2022), Section 5 / Table 1.

Reproduces Models 1-4 of the paper’s Monte Carlo, the data-generating process for which is

\[Y_{it} = \beta_i d_{it} + 5 \sum_{k=1}^{2} \lambda_t^{(k)} \mu_i^{(k)} + \epsilon_{it},\]

with the piecewise factor paths \(\lambda_t^{(1)}, \lambda_t^{(2)}\) defined below, \(\mu_i^{(k)} \sim U(0, 1)\), \(\epsilon_{it} \sim N(0, \tfrac14)\), and unit-specific effects \(\beta_i = \sum_k \mu_i^{(k)} - \tfrac1n \sum_i \sum_k \mu_i^{(k)}\) (so the true average effect is exactly zero each draw). The treatment \(d_{it}\) is continuous and a function of the same factors, so it is correlated with the interactive fixed effects – which is why two-way fixed effects is badly biased and CTSC is not.

Model variations:

Model

treatment construction

n

T

1

d = factors + U(0, 10)

10

50

2

d = factors + U(0, 10)

30

50

3

d = factors + U(0, 5) * (beta_i + 3)

10

50

4

d = factors + U(0, 10)

10

20

The headline calibration target (paper Table 1): CTSC mean bias \(\approx 0\) with small MAD/RMSE, while the two-way fixed-effects estimator has mean bias \(\approx 0.85\) (Models 1-2).

class mlsynth.utils.ctsc_helpers.simulation.SimulationSummary(model: int, n_sims: int, ctsc_mean_bias: float, ctsc_mad: float, ctsc_rmse: float, fe_mean_bias: float, fe_mad: float, fe_rmse: float)#

Monte-Carlo summary for one model (mean bias, MAD, RMSE).

ctsc_mad: float#
ctsc_mean_bias: float#
ctsc_rmse: float#
fe_mad: float#
fe_mean_bias: float#
fe_rmse: float#
model: int#
n_sims: int#
mlsynth.utils.ctsc_helpers.simulation.generate_model(model: int, rng: Generator) Tuple[ndarray, ndarray, float]#

Draw one panel from Powell (2022) Model model in {1, 2, 3, 4}.

Returns (Y, D, true_ae) where Y is (n, T), D is (n, T, 1), and true_ae is the true average effect (0).

mlsynth.utils.ctsc_helpers.simulation.run_simulation(model: int, n_sims: int = 100, *, seed: int = 0) SimulationSummary#

Run n_sims Monte-Carlo draws of model and summarise CTSC vs FE.

The true average effect is zero, so bias equals the mean estimate.

mlsynth.utils.ctsc_helpers.simulation.twoway_fe_effect(Y: ndarray, D: ndarray) float#

Two-way (unit + time) fixed-effects slope on the treatment – the biased baseline the paper compares against.

Orchestration for the CTSC estimator (Powell 2022).

mlsynth.utils.ctsc_helpers.pipeline.run_ctsc(inputs: CTSCInputs, *, use_fit_weights: bool = True, inference: bool = True, null_value: ndarray | None = None, n_draws: int = 2000, random_state: int = 0) CTSCResults#

Run CTSC and assemble CTSCResults.

Parameters:
  • inputs (CTSCInputs) – Preprocessed panel.

  • use_fit_weights (bool) – Use the two-step per-unit fit weights \(\Omega_i\) (eq. 6).

  • inference (bool) – Run the sign-flip Wald test of H0: alpha^AE = null_value.

  • null_value (np.ndarray, optional) – Per-variable null for the average effect (default zeros).

  • n_draws (int) – Rademacher draws for the randomization test.

  • random_state (int) – RNG seed.

Frozen dataclasses for the Continuous-Treatment Synthetic Control (CTSC).

Powell, D. (2022). “Synthetic Control Estimation Beyond Comparative Case Studies: Does the Minimum Wage Reduce Employment?” Journal of Business & Economic Statistics 40(3):1302-1314.

CTSC generalises synthetic control to continuous and/or multi-valued treatments in panels where there is no clean treated / never-treated split – every unit has a time-varying treatment (e.g. every U.S. state has a minimum wage that changes over time). It jointly estimates a unit-specific treatment-slope vector \(\alpha_i\) and a synthetic control over the other units’ untreated outcomes, and reports a population-weighted average marginal effect.

The paper names the estimator “GSC” (generalized synthetic control); mlsynth uses CTSC to avoid confusion with Xu (2017)’s differently constructed Generalized Synthetic Control (gsynth).

class mlsynth.utils.ctsc_helpers.structures.CTSCInference(method: str, null_value: ndarray, wald_stat: ndarray, p_value: ndarray, se: ndarray, n_draws: int)#

Wald / sign-flip inference for CTSC (paper Section 4).

Tests \(H_0: \alpha^{AE,(k)} = a_0\) per treatment variable via a Wald statistic on the orthogonal moment scores, calibrated by a Rademacher (sign-flip) randomization distribution that permits arbitrary within-unit and cross-unit dependence.

method#

"sign_flip_wald".

Type:

str

null_value#

Tested null \(a_0\) per variable, shape (K,).

Type:

np.ndarray

wald_stat#

Wald statistics per variable, shape (K,).

Type:

np.ndarray

p_value#

Randomization p-values per variable, shape (K,).

Type:

np.ndarray

se#

Standard errors of the average effect per variable, shape (K,).

Type:

np.ndarray

n_draws#

Number of Rademacher draws.

Type:

int

method: str#
n_draws: int#
null_value: ndarray#
p_value: ndarray#
se: ndarray#
wald_stat: ndarray#
class mlsynth.utils.ctsc_helpers.structures.CTSCInputs(Y: ndarray, D: ndarray, unit_names: List[Any], time_labels: ndarray, treatment_names: List[str], population_weights: ndarray)#

Preprocessed panel for CTSC.

Y#

Outcomes, shape (n, T).

Type:

np.ndarray

D#

Treatment / explanatory variables, shape (n, T, K).

Type:

np.ndarray

unit_names#

Length-n unit identifiers.

Type:

list

time_labels#

Length-T period labels.

Type:

np.ndarray

treatment_names#

Length-K names of the treatment / explanatory variables.

Type:

list of str

population_weights#

Per-unit weights \(\pi_i\) for the average effect; sum to one.

Type:

np.ndarray

D: ndarray#
property K: int#
property T: int#
Y: ndarray#
property n: int#
population_weights: ndarray#
time_labels: ndarray#
treatment_names: List[str]#
unit_names: List[Any]#
class mlsynth.utils.ctsc_helpers.structures.CTSCResults(inputs: ~mlsynth.utils.ctsc_helpers.structures.CTSCInputs, average_effect: ~numpy.ndarray, unit_effects: ~numpy.ndarray, unit_weight_matrix: ~numpy.ndarray, fit_metric: ~numpy.ndarray, objective: float, weights: ~typing.Any | None = None, inference: ~mlsynth.utils.ctsc_helpers.structures.CTSCInference | None = None, metadata: ~typing.Dict[str, ~typing.Any] = <factory>)#

Top-level container returned by mlsynth.CTSC.fit().

inputs#

Preprocessed panel.

Type:

CTSCInputs

average_effect#

Population-weighted average marginal effect, shape (K,) (paper eq. 7).

Type:

np.ndarray

unit_effects#

Unit-specific treatment slopes \(\alpha_i\), shape (n, K).

Type:

np.ndarray

unit_weight_matrix#

All-units synthetic-control weight matrix, shape (n, n); [i, i] = 0, each row non-negative and summing to one. CTSC builds a synthetic control for every unit, so there is no single treated-unit donor vector – row i is unit i’s weights.

Type:

np.ndarray

fit_metric#

Per-unit fit weights \(\Omega_i\), shape (n,) (smaller = better synthetic control; paper eq. 6).

Type:

np.ndarray

objective#

Minimised objective value.

Type:

float

weights#

Standardized weights. Since CTSC has no single treated unit, donor_weights holds the cross-unit average weight each donor receives; the full per-unit matrix is unit_weight_matrix.

Type:

WeightsResults, optional

inference#

CTSCInference when inference is run; None otherwise.

Type:

object, optional

metadata#

Free-form diagnostics.

Type:

dict

average_effect: ndarray#
fit_metric: ndarray#
inference: CTSCInference | None = None#
inputs: CTSCInputs#
metadata: Dict[str, Any]#
objective: float#
unit_effects: ndarray#
unit_weight_matrix: ndarray#
weights: Any | None = None#

Example#

import numpy as np
import pandas as pd

from mlsynth import CTSC
from mlsynth.utils.ctsc_helpers.simulation import generate_model

# A continuous-treatment panel (here from the paper's Model 1 DGP).
rng = np.random.default_rng(3)
Y, D, true_ae = generate_model(1, rng)
n, T = Y.shape
rows = [{"state": f"s{i}", "qtr": t, "emp": Y[i, t], "minwage": D[i, t, 0]}
        for i in range(n) for t in range(T)]
df = pd.DataFrame(rows)

res = CTSC({
    "df": df,
    "outcome": "emp",
    "treat": "minwage",          # placeholder for the base config
    "treatment_vars": ["minwage"],
    "unitid": "state",
    "time": "qtr",
    "inference": True,
}).fit()

print(f"average marginal effect = {res.average_effect[0]:+.4f}  "
      f"(true = {true_ae})")
print(f"sign-flip Wald p-value  = {res.inference.p_value[0]:.3f}")
# Unit-specific slopes (heterogeneity):
print("unit effects:", np.round(res.unit_effects[:, 0], 3))

References#

Abadie, A., Diamond, A., & Hainmueller, J. (2010). “Synthetic Control Methods for Comparative Case Studies.” Journal of the American Statistical Association 105(490):493-505.

Arkhangelsky, D., Athey, S., Hirshberg, D. A., Imbens, G. W., & Wager, S. (2021). “Synthetic Difference-in-Differences.” American Economic Review 111(12):4088-4118.

Canay, I. A., Romano, J. P., & Shaikh, A. M. (2017). “Randomization Tests Under an Approximate Symmetry Assumption.” Econometrica 85(3):1013-1030.

Dube, A., & Zipperer, B. (2015). “Pooling Multiple Case Studies Using Synthetic Controls: An Application to Minimum Wage Policies.” IZA DP.

Powell, D. (2022). “Synthetic Control Estimation Beyond Comparative Case Studies: Does the Minimum Wage Reduce Employment?” Journal of Business & Economic Statistics 40(3):1302-1314.

Xu, Y. (2017). “Generalized Synthetic Control Method: Causal Inference with Interactive Fixed Effects Models.” Political Analysis 25(1):57-76.