|
17 | 17 | from diff_diff._backend import ( |
18 | 18 | HAS_RUST_BACKEND, |
19 | 19 | _rust_project_simplex, |
20 | | - _rust_synthetic_weights, |
21 | 20 | _rust_sdid_unit_weights, |
22 | 21 | _rust_compute_time_weights, |
23 | 22 | _rust_compute_noise_level, |
@@ -1131,115 +1130,10 @@ def equivalence_test_trends( |
1131 | 1130 | } |
1132 | 1131 |
|
1133 | 1132 |
|
1134 | | -def compute_synthetic_weights( |
1135 | | - Y_control: np.ndarray, Y_treated: np.ndarray, lambda_reg: float = 0.0, min_weight: float = 1e-6 |
1136 | | -) -> np.ndarray: |
1137 | | - """ |
1138 | | - Compute synthetic control unit weights using constrained optimization. |
1139 | | -
|
1140 | | - Finds weights ω that minimize the squared difference between the |
1141 | | - weighted average of control unit outcomes and the treated unit outcomes |
1142 | | - during pre-treatment periods. |
1143 | | -
|
1144 | | - Parameters |
1145 | | - ---------- |
1146 | | - Y_control : np.ndarray |
1147 | | - Control unit outcomes matrix of shape (n_pre_periods, n_control_units). |
1148 | | - Each column is a control unit, each row is a pre-treatment period. |
1149 | | - Y_treated : np.ndarray |
1150 | | - Treated unit mean outcomes of shape (n_pre_periods,). |
1151 | | - Average across treated units for each pre-treatment period. |
1152 | | - lambda_reg : float, default=0.0 |
1153 | | - L2 regularization parameter. Larger values shrink weights toward |
1154 | | - uniform (1/n_control). Helps prevent overfitting when n_pre < n_control. |
1155 | | - min_weight : float, default=1e-6 |
1156 | | - Minimum weight threshold. Weights below this are set to zero. |
1157 | | -
|
1158 | | - Returns |
1159 | | - ------- |
1160 | | - np.ndarray |
1161 | | - Unit weights of shape (n_control_units,) that sum to 1. |
1162 | | -
|
1163 | | - Notes |
1164 | | - ----- |
1165 | | - Solves the quadratic program: |
1166 | | -
|
1167 | | - min_ω ||Y_treated - Y_control @ ω||² + λ||ω - 1/n||² |
1168 | | - s.t. ω >= 0, sum(ω) = 1 |
1169 | | -
|
1170 | | - Uses a simplified coordinate descent approach with projection onto simplex. |
1171 | | - """ |
1172 | | - n_pre, n_control = Y_control.shape |
1173 | | - |
1174 | | - if n_control == 0: |
1175 | | - return np.asarray([]) |
1176 | | - |
1177 | | - if n_control == 1: |
1178 | | - return np.asarray([1.0]) |
1179 | | - |
1180 | | - # Use Rust backend if available |
1181 | | - if HAS_RUST_BACKEND: |
1182 | | - Y_control = np.ascontiguousarray(Y_control, dtype=np.float64) |
1183 | | - Y_treated = np.ascontiguousarray(Y_treated, dtype=np.float64) |
1184 | | - weights = _rust_synthetic_weights( |
1185 | | - Y_control, Y_treated, lambda_reg, _OPTIMIZATION_MAX_ITER, _OPTIMIZATION_TOL |
1186 | | - ) |
1187 | | - else: |
1188 | | - # Fallback to NumPy implementation |
1189 | | - weights = _compute_synthetic_weights_numpy(Y_control, Y_treated, lambda_reg) |
1190 | | - |
1191 | | - # Set small weights to zero for interpretability |
1192 | | - weights[weights < min_weight] = 0 |
1193 | | - if np.sum(weights) > 0: |
1194 | | - weights = weights / np.sum(weights) |
1195 | | - else: |
1196 | | - # Fallback to uniform if all weights are zeroed |
1197 | | - weights = np.ones(n_control) / n_control |
1198 | | - |
1199 | | - return np.asarray(weights) |
1200 | | - |
1201 | | - |
1202 | | -def _compute_synthetic_weights_numpy( |
1203 | | - Y_control: np.ndarray, |
1204 | | - Y_treated: np.ndarray, |
1205 | | - lambda_reg: float = 0.0, |
1206 | | -) -> np.ndarray: |
1207 | | - """NumPy fallback implementation of compute_synthetic_weights.""" |
1208 | | - n_pre, n_control = Y_control.shape |
1209 | | - |
1210 | | - # Initialize with uniform weights |
1211 | | - weights = np.ones(n_control) / n_control |
1212 | | - |
1213 | | - # Precompute matrices for optimization |
1214 | | - # Objective: ||Y_treated - Y_control @ w||^2 + lambda * ||w - w_uniform||^2 |
1215 | | - # = w' @ (Y_control' @ Y_control + lambda * I) @ w - 2 * (Y_control' @ Y_treated + lambda * w_uniform)' @ w + const |
1216 | | - YtY = Y_control.T @ Y_control |
1217 | | - YtT = Y_control.T @ Y_treated |
1218 | | - w_uniform = np.ones(n_control) / n_control |
1219 | | - |
1220 | | - # Add regularization |
1221 | | - H = YtY + lambda_reg * np.eye(n_control) |
1222 | | - f = YtT + lambda_reg * w_uniform |
1223 | | - |
1224 | | - # Solve with projected gradient descent |
1225 | | - # Project onto probability simplex |
1226 | | - step_size = 1.0 / (np.linalg.norm(H, 2) + _NUMERICAL_EPS) |
1227 | | - |
1228 | | - for _ in range(_OPTIMIZATION_MAX_ITER): |
1229 | | - weights_old = weights.copy() |
1230 | | - |
1231 | | - # Gradient step: minimize ||Y - Y_control @ w||^2 |
1232 | | - grad = H @ weights - f |
1233 | | - weights = weights - step_size * grad |
1234 | | - |
1235 | | - # Project onto simplex (sum to 1, non-negative) |
1236 | | - weights = _project_simplex(weights) |
1237 | | - |
1238 | | - # Check convergence |
1239 | | - if np.linalg.norm(weights - weights_old) < _OPTIMIZATION_TOL: |
1240 | | - break |
1241 | | - |
1242 | | - return weights |
| 1133 | +# compute_synthetic_weights and _compute_synthetic_weights_numpy removed in the |
| 1134 | +# silent-failures audit post-cleanup (finding #22). The one caller |
| 1135 | +# (`diff_diff.prep.rank_control_units`) inlines Frank-Wolfe directly via |
| 1136 | +# `_sc_weight_fw`, matching R `synthdid::sc.weight.fw`. See `prep.py:990`. |
1243 | 1137 |
|
1244 | 1138 |
|
1245 | 1139 | def _project_simplex(v: np.ndarray) -> np.ndarray: |
|
0 commit comments