crabbymetrics
  • Home
  • API
    • API Overview
    • Regression And GLMs
    • Survival / Event-Time
    • Causal Inference And Panels
    • MPE_CBPS
    • Hypothesis Testing And Utilities
    • Transforms
    • Estimation Interfaces
  • Internals
  • Regression
    • OLS
    • ABC OLS
    • Anytime-Valid Confidence Sequences
    • Ridge
    • Bagged Polynomial Regression
    • Fixed Effects OLS
    • ElasticNet
    • Logit
    • Multinomial Logit
    • Poisson
    • MLE Prediction Interface
    • Survival / Recurrent Events
    • GMM
    • MEstimator Poisson
  • Causal / Panels
    • Balancing Weights
    • Chronos LTV Balancing
    • Cressie-Read And Rényi Balancing
    • EPLM
    • Average Derivative
    • Double ML And AIPW
    • Richer Regression
    • TwoSLS
    • FE Counterfactual Imputation
    • Synthetic Control
    • Synthetic DID
    • Augmented Balancing For Panel Data
    • Horizontal Panel Ridge
    • Matrix Completion
    • Interactive Fixed Effects
    • Staggered Panel Event Study
    • Joint Hypothesis Tests
    • Dynamic Treatment Effects
  • Transforms
    • PCA And Kernel Basis
    • Sparse Factor Rotations
  • Ablations
    • Variance Estimators
    • Semiparametric Estimator Comparisons
    • Two-Period Semiparametric DID
    • Bridging Finite And Superpopulation
    • Lecture-Motivated Panel Fixes
    • Panel Estimator DGP Comparisons
    • Same Root Panel Case Studies
    • Randomized Sketching And Least Squares
    • Estimator Scaling And References
  • Optimization
    • Optimizers
    • GMM With Optimizers
  • Ding
    • Chapter Index
    • Foundations (1-4)
    • Design And Adjustment (5-8)
    • Finite And Superpopulation (9)
    • Observational Studies (11-13, 27)
    • Instrumental Variables (21, 23)

On this page

  • 1 Where it fits
  • 2 Weight objective
  • 3 Implementation walkthrough
  • 4 Inference and diagnostics
  • 5 Performance and numerical behavior
  • 6 Python API
  • 7 Minimal example
  • 8 summary() contract

SyntheticControl

Single-treated-unit donor weighting

1 Where it fits

Group: Causal inference

SyntheticControl fits nonnegative donor weights that sum to one, minimizing pre-treatment imbalance between the treated path and a convex combination of donor paths:

\min_{w\ge 0,\;1'w=1}\|y_{\mathrm{treated,pre}} - Y_{\mathrm{donor,pre}}w\|_2^2.

It is the lower-level single-path API; the panel estimators use the newer fit(Y, W) contract.

2 Weight objective

For pre-treatment donor matrix D\in\mathbb R^{T_0\times J} and treated path y, the class solves

\hat w = \arg\min_{w\in\Delta_J} \frac{1}{2T_0}\|Dw-y\|_2^2, \qquad \Delta_J=\{w:w_j\geq0,\ \mathbf1'w=1\}.

The active-set solver operates directly on the closed simplex, so exact zero weights are permitted. There is no intercept, predictor weighting, or pre-period weighting.

3 Implementation walkthrough

  1. fit() clears prior fitted state and validates finite, nonempty, aligned arrays and a positive iteration budget.
  2. Under \mathbf1'w=1, Dw-y=(D-y\mathbf1')w. The implementation centers every donor on the treated path and scales by the largest absolute residual entry before forming its Gram matrix. This preserves the optimizer while reducing cancellation from a large shared outcome level.
  3. Starting with uniform weights, the solver obtains an equality-constrained direction on the current free set from a KKT system. It tries LU and falls back to SVD when needed. A step stops at the first nonnegativity boundary; active constraints are released when their reduced gradients permit descent.
  4. A small direction alone is not success. The final simplex vector must pass a KKT residual check; iteration exhaustion or a failed numerical solve raises an error. With one donor, the equality constraint determines weight one.
  5. predict() validates finite inputs and the fitted donor count, then applies the stored weights. summary() reports RMSE and the objective in the original outcome units, together with diagnostics for the scaled optimization problem.
  6. The bootstrap resamples pre-period rows in pairs and reruns the complete fit. A failed replicate aborts instead of silently using unconverged weights.

The softmax/L-BFGS parameterization used in v0.9.0 could become nearly flat away from the constrained optimum. Version 0.9.1 replaces it; previously fitted synthetic controls should be refitted.

4 Inference and diagnostics

summary() includes weights, pre_rmse, objective, iterations, converged, stop_reason, scaled_kkt_residual, and feasibility_error. The objective is half the mean squared pre-treatment error. The KKT residual belongs to the internally scaled quadratic problem, not the raw outcome scale.

The class receives only donor and treated fitting paths, so it does not itself calculate a post-treatment ATT. Bootstrap weight draws resample time rows as iid pairs: they do not preserve time dependence and are not formal treatment-effect confidence intervals.

5 Performance and numerical behavior

Forming the Gram matrix costs O(T_0J^2) with O(J^2) auxiliary storage in addition to the donor matrix. An active-set step solves a dense system whose dimension is the current free set plus its equality multiplier. Runtime depends on donor count and the number of constraint changes, and bootstrap multiplies that work. Redundant donors can leave weights nonunique even when the fitted path and objective agree. The solver does not claim identification of a unique donor-weight vector in that case.

6 Python API

Constructor: cm.SyntheticControl

Call fit(donors, treated) where donors is (n_periods, n_donors) and treated is the treated pre-period vector. predict(donors) applies the learned weights to a donor matrix. summary() reports weights and pre-fit RMSE.

Code
print(inspect.signature(cm.SyntheticControl))
(max_iterations=500)
Code
cls = cm.SyntheticControl
display(HTML(html_table(["Public method"], public_methods(cls))))
Public method
SyntheticControl(max_iterations=500)
bootstrap(self, /, n_bootstrap, seed=None)
fit(self, /, donors, treated)
predict(self, /, donors)
summary(self, /)

7 Minimal example

rng = np.random.default_rng(15)
donors = rng.normal(size=(40, 4))
w_true = np.array([0.45, 0.25, 0.2, 0.1])
treated = donors @ w_true + rng.normal(scale=0.02, size=40)
model = cm.SyntheticControl(max_iterations=500)
model.fit(donors, treated)
print(model.summary()['weights'])
print(model.predict(donors[-3:]))
[0.45260903 0.25299334 0.19448106 0.09991657]
[ 0.54589871  0.43772002 -0.93213338]

8 summary() contract

The table below is generated by fitting the live class in this repository and then inspecting summary(). Shapes are shown because most values are plain NumPy arrays or scalars.

Code
rng = np.random.default_rng(115)
donors = rng.normal(size=(30, 4))
treated = donors @ np.array([0.45, 0.25, 0.2, 0.1])
model = cm.SyntheticControl(max_iterations=300)
model.fit(donors, treated)
summary = model.summary()
display(HTML(html_table(["summary() key", "shape"], summary_shape_rows(summary))))
summary() key shape
weights (4,)
pre_rmse ()
converged ()
iterations ()
stop_reason ()
solver ()
objective ()
scaled_kkt_residual ()
feasibility_error ()

crabbymetrics 0.9.1

 
  • v0.9.1 release

  • v0.9 migration

  • Reproduction