Code
print(inspect.signature(cm.SyntheticControl))(max_iterations=500)
Single-treated-unit donor weighting
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.
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.
fit() clears prior fitted state and validates finite, nonempty, aligned arrays and a positive iteration budget.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.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.
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.
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.
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.
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]
summary() contractThe 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.
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 |
() |