cbps 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cbps/__init__.py +3462 -0
- cbps/constants.py +46 -0
- cbps/core/__init__.py +93 -0
- cbps/core/cbps_binary.py +1943 -0
- cbps/core/cbps_continuous.py +945 -0
- cbps/core/cbps_multitreat.py +1123 -0
- cbps/core/cbps_optimal.py +507 -0
- cbps/core/results.py +1447 -0
- cbps/data/Blackwell.csv +571 -0
- cbps/data/LaLonde.csv +3213 -0
- cbps/data/npcbps_continuous_sim.csv +501 -0
- cbps/data/nsw.csv +723 -0
- cbps/data/nsw_dw.csv +446 -0
- cbps/data/political_ads_urban_niebler.csv +16266 -0
- cbps/data/psid_controls.csv +2491 -0
- cbps/data/psid_controls2.csv +254 -0
- cbps/data/psid_controls3.csv +129 -0
- cbps/data/simulation_dgp1_seed12345.csv +201 -0
- cbps/data/simulation_dgp2_seed12345.csv +201 -0
- cbps/data/simulation_dgp3_seed12345.csv +201 -0
- cbps/data/simulation_dgp4_seed12345.csv +201 -0
- cbps/datasets/__init__.py +78 -0
- cbps/datasets/blackwell.py +112 -0
- cbps/datasets/continuous.py +223 -0
- cbps/datasets/lalonde.py +272 -0
- cbps/datasets/npcbps_sim.py +101 -0
- cbps/diagnostics/__init__.py +101 -0
- cbps/diagnostics/balance.py +760 -0
- cbps/diagnostics/balance_cbmsm_addon.py +162 -0
- cbps/diagnostics/continuous_diagnostics.py +259 -0
- cbps/diagnostics/normality.py +173 -0
- cbps/diagnostics/ocbps_conditions.py +197 -0
- cbps/diagnostics/overlap.py +198 -0
- cbps/diagnostics/plots.py +1193 -0
- cbps/diagnostics/weights_diag.py +205 -0
- cbps/highdim/__init__.py +84 -0
- cbps/highdim/gmm_loss.py +340 -0
- cbps/highdim/hdcbps.py +1078 -0
- cbps/highdim/lasso_utils.py +498 -0
- cbps/highdim/weight_funcs.py +298 -0
- cbps/inference/__init__.py +42 -0
- cbps/inference/asyvar.py +621 -0
- cbps/inference/vcov_outcome.py +217 -0
- cbps/iv/__init__.py +48 -0
- cbps/iv/cbiv.py +2603 -0
- cbps/logging_config.py +45 -0
- cbps/msm/__init__.py +45 -0
- cbps/msm/cbmsm.py +1871 -0
- cbps/msm/rank_diagnostics.py +112 -0
- cbps/nonparametric/__init__.py +58 -0
- cbps/nonparametric/cholesky_whitening.py +232 -0
- cbps/nonparametric/empirical_likelihood.py +339 -0
- cbps/nonparametric/npcbps.py +1036 -0
- cbps/nonparametric/taylor_approx.py +207 -0
- cbps/py.typed +0 -0
- cbps/sklearn/__init__.py +42 -0
- cbps/sklearn/estimator.py +378 -0
- cbps/utils/__init__.py +82 -0
- cbps/utils/formula.py +415 -0
- cbps/utils/helpers.py +378 -0
- cbps/utils/numerics.py +438 -0
- cbps/utils/r_compat.py +109 -0
- cbps/utils/validation.py +224 -0
- cbps/utils/variance_transform.py +483 -0
- cbps/utils/weights.py +586 -0
- cbps-0.2.0.dist-info/METADATA +1090 -0
- cbps-0.2.0.dist-info/RECORD +70 -0
- cbps-0.2.0.dist-info/WHEEL +5 -0
- cbps-0.2.0.dist-info/licenses/LICENSE +661 -0
- cbps-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optimal Covariate Balancing Propensity Score (oCBPS)
|
|
3
|
+
====================================================
|
|
4
|
+
|
|
5
|
+
This module implements optimal CBPS (oCBPS) that extends the standard
|
|
6
|
+
CBPS by incorporating dual balancing conditions for improved efficiency
|
|
7
|
+
and robustness through the framework of Fan et al. (2022).
|
|
8
|
+
|
|
9
|
+
The implementation achieves double robustness and semiparametric efficiency
|
|
10
|
+
by separating the covariate balancing conditions for baseline outcome models
|
|
11
|
+
and treatment effect heterogeneity models.
|
|
12
|
+
|
|
13
|
+
Key Innovations
|
|
14
|
+
---------------
|
|
15
|
+
1. **Dual Balancing Conditions** (Fan 2022 Eq. 3.2-3.3):
|
|
16
|
+
- g1_baseline: Balance covariates h1 related to E(Y(0)|X)
|
|
17
|
+
- g2_diff: Balance covariates h2 related to E(Y(1)-Y(0)|X)
|
|
18
|
+
|
|
19
|
+
2. **Double Robustness** (Theorem 3.1):
|
|
20
|
+
Consistent if either the propensity score model or outcome model is correct.
|
|
21
|
+
|
|
22
|
+
3. **Semiparametric Efficiency** (Corollary 3.2):
|
|
23
|
+
Achieves Hahn 1998 efficiency bound when both models are correct and m=q.
|
|
24
|
+
|
|
25
|
+
Implementation Notes
|
|
26
|
+
--------------------
|
|
27
|
+
- Only supports att=0 (ATE estimation)
|
|
28
|
+
- No sample_weights parameter (oCBPS does not support sampling weights)
|
|
29
|
+
- Dual initialization optimization for robust convergence
|
|
30
|
+
|
|
31
|
+
References
|
|
32
|
+
----------
|
|
33
|
+
.. [1] Fan, Jianqing, Kosuke Imai, Inbeom Lee, Han Liu, Yang Ning,
|
|
34
|
+
and Xiaolin Yang. 2022.
|
|
35
|
+
"Optimal Covariate Balancing Conditions in Propensity Score
|
|
36
|
+
Estimation."
|
|
37
|
+
Journal of Business & Economic Statistics, 41(1), 97-110.
|
|
38
|
+
https://doi.org/10.1080/07350015.2021.2002159
|
|
39
|
+
https://imai.fas.harvard.edu/research/CBPStheory.html
|
|
40
|
+
|
|
41
|
+
.. [2] Imai, Kosuke and Marc Ratkovic. 2014.
|
|
42
|
+
"Covariate Balancing Propensity Score."
|
|
43
|
+
Journal of the Royal Statistical Society, Series B.
|
|
44
|
+
DOI:10.1111/rssb.12027
|
|
45
|
+
|
|
46
|
+
Examples
|
|
47
|
+
--------
|
|
48
|
+
>>> from cbps import CBPS
|
|
49
|
+
>>> from cbps.datasets import load_lalonde
|
|
50
|
+
>>>
|
|
51
|
+
>>> # Load LaLonde data
|
|
52
|
+
>>> lalonde = load_lalonde()
|
|
53
|
+
>>>
|
|
54
|
+
>>> # oCBPS estimation with dual formula specification
|
|
55
|
+
>>> # Note: require m1 + m2 + 1 >= k where k is number of parameters
|
|
56
|
+
>>> fit = CBPS(
|
|
57
|
+
... formula='treat ~ age + educ + re75 + re74',
|
|
58
|
+
... data=lalonde,
|
|
59
|
+
... baseline_formula='~age + educ + re75 + re74',
|
|
60
|
+
... diff_formula='~I(re75==0)',
|
|
61
|
+
... att=0 # oCBPS only supports ATE
|
|
62
|
+
... )
|
|
63
|
+
>>>
|
|
64
|
+
>>> # View results
|
|
65
|
+
>>> print(fit.summary())
|
|
66
|
+
>>> print(f"J-statistic: {fit.J:.6f}")
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
from typing import Any, Dict, Optional
|
|
70
|
+
import warnings
|
|
71
|
+
|
|
72
|
+
import numpy as np
|
|
73
|
+
import scipy.linalg
|
|
74
|
+
import scipy.special
|
|
75
|
+
import scipy.optimize
|
|
76
|
+
import statsmodels.api as sm
|
|
77
|
+
|
|
78
|
+
# Import generalized inverse function from cbps_binary
|
|
79
|
+
from .cbps_binary import _r_ginv
|
|
80
|
+
|
|
81
|
+
# Constants
|
|
82
|
+
PROBS_MIN = 1e-6 # Probability clipping threshold for numerical stability
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _gmm_func1(
|
|
86
|
+
beta_curr: np.ndarray,
|
|
87
|
+
X: np.ndarray,
|
|
88
|
+
treat: np.ndarray,
|
|
89
|
+
baseline_X: np.ndarray,
|
|
90
|
+
diff_X: np.ndarray,
|
|
91
|
+
invV: Optional[np.ndarray] = None,
|
|
92
|
+
option: Optional[str] = None
|
|
93
|
+
) -> Dict[str, Any]:
|
|
94
|
+
"""
|
|
95
|
+
GMM objective function for optimal CBPS with dual balancing conditions.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
beta_curr : np.ndarray
|
|
100
|
+
Current propensity score coefficients (k-dimensional vector).
|
|
101
|
+
X : np.ndarray
|
|
102
|
+
Covariate matrix (n x k, including intercept column).
|
|
103
|
+
treat : np.ndarray
|
|
104
|
+
Binary treatment vector (0/1 encoded).
|
|
105
|
+
baseline_X : np.ndarray
|
|
106
|
+
Design matrix from baseline formula (n x m1).
|
|
107
|
+
diff_X : np.ndarray
|
|
108
|
+
Design matrix from diff formula (n x m2).
|
|
109
|
+
invV : np.ndarray, optional
|
|
110
|
+
Precomputed inverse of V matrix (for two-step GMM).
|
|
111
|
+
option : str, optional
|
|
112
|
+
None for dual balancing (oCBPS standard), "CBPS" for single balancing
|
|
113
|
+
(used in pre-optimization).
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
dict
|
|
118
|
+
Dictionary with keys:
|
|
119
|
+
- 'loss': GMM loss value (quadratic form)
|
|
120
|
+
- 'invV': Generalized inverse of the V matrix
|
|
121
|
+
|
|
122
|
+
Notes
|
|
123
|
+
-----
|
|
124
|
+
The dual balancing conditions (Fan et al. 2022, Eq. 3.2-3.3):
|
|
125
|
+
|
|
126
|
+
- g1_baseline: Balance covariates related to E(Y(0)|X)
|
|
127
|
+
- g2_diff: Balance covariates related to E(Y(1)-Y(0)|X)
|
|
128
|
+
|
|
129
|
+
When option="CBPS", uses standard single balance condition for pre-optimization.
|
|
130
|
+
"""
|
|
131
|
+
# Step 1: Sample size
|
|
132
|
+
n = X.shape[0]
|
|
133
|
+
|
|
134
|
+
# Step 2: Compute propensity scores
|
|
135
|
+
theta_curr = X @ beta_curr
|
|
136
|
+
probs_curr = scipy.special.expit(theta_curr)
|
|
137
|
+
|
|
138
|
+
# Sequential clipping (upper bound then lower bound)
|
|
139
|
+
probs_curr = np.minimum(1 - PROBS_MIN, probs_curr)
|
|
140
|
+
probs_curr = np.maximum(PROBS_MIN, probs_curr)
|
|
141
|
+
|
|
142
|
+
# Step 3: Compute ATE weights
|
|
143
|
+
w_curr = treat / probs_curr - (1 - treat) / (1 - probs_curr)
|
|
144
|
+
|
|
145
|
+
# Step 4: Construct moment conditions based on option
|
|
146
|
+
if option is None:
|
|
147
|
+
# Dual balancing conditions (oCBPS standard)
|
|
148
|
+
# Construct X1new: intercept + baseline covariates
|
|
149
|
+
X1new = np.column_stack([X[:, 0], baseline_X])
|
|
150
|
+
|
|
151
|
+
# g1_baseline balance condition
|
|
152
|
+
w_curr_del1 = (1/n) * (X1new.T @ w_curr)
|
|
153
|
+
|
|
154
|
+
# g2_diff weights
|
|
155
|
+
w_curr3 = treat / probs_curr - 1
|
|
156
|
+
|
|
157
|
+
# g2_diff balance condition
|
|
158
|
+
w_curr_del3 = (1/n) * (diff_X.T @ w_curr3)
|
|
159
|
+
|
|
160
|
+
# Concatenate dual balance conditions
|
|
161
|
+
gbar = np.concatenate([w_curr_del1, w_curr_del3])
|
|
162
|
+
|
|
163
|
+
elif option == "CBPS":
|
|
164
|
+
# Single balance condition (for pre-optimization)
|
|
165
|
+
# Standard ATE balance using full X matrix
|
|
166
|
+
w_curr_del = (1/n) * (X.T @ w_curr)
|
|
167
|
+
gbar = w_curr_del
|
|
168
|
+
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError(f"Unknown option: {option}")
|
|
171
|
+
|
|
172
|
+
# Step 5: Compute covariance matrix V and its inverse
|
|
173
|
+
if invV is None:
|
|
174
|
+
# Reconstruct X1new (not defined when option="CBPS")
|
|
175
|
+
X1new = np.column_stack([X[:, 0], baseline_X])
|
|
176
|
+
|
|
177
|
+
# Block 1: V11
|
|
178
|
+
factor_1 = ((1 - probs_curr) * probs_curr)**(-0.5)
|
|
179
|
+
X_1 = X1new * factor_1[:, None]
|
|
180
|
+
|
|
181
|
+
# Block 2: V22
|
|
182
|
+
factor_2 = (1/probs_curr - 1)**0.5
|
|
183
|
+
X_2 = diff_X * factor_2[:, None]
|
|
184
|
+
|
|
185
|
+
# Block 3: V12
|
|
186
|
+
X_1_1 = X1new * (probs_curr**(-0.5))[:, None]
|
|
187
|
+
|
|
188
|
+
# Block 4: V21
|
|
189
|
+
X_1_2 = diff_X * (probs_curr**(-0.5))[:, None]
|
|
190
|
+
|
|
191
|
+
# Assemble V matrix
|
|
192
|
+
V11 = (1/n) * (X_1.T @ X_1)
|
|
193
|
+
V12 = (1/n) * (X_1_1.T @ X_1_2)
|
|
194
|
+
V21 = (1/n) * (X_1_2.T @ X_1_1)
|
|
195
|
+
V22 = (1/n) * (X_2.T @ X_2)
|
|
196
|
+
|
|
197
|
+
V = np.block([[V11, V12],
|
|
198
|
+
[V21, V22]])
|
|
199
|
+
|
|
200
|
+
# Generalized inverse
|
|
201
|
+
invV_g = _r_ginv(V)
|
|
202
|
+
else:
|
|
203
|
+
invV_g = invV
|
|
204
|
+
|
|
205
|
+
# Step 6: Compute GMM loss (quadratic form)
|
|
206
|
+
loss = float(gbar.T @ invV_g @ gbar)
|
|
207
|
+
|
|
208
|
+
return {'loss': loss, 'invV': invV_g}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _gmm_loss1(beta: np.ndarray, *args, **kwargs) -> float:
|
|
212
|
+
"""
|
|
213
|
+
GMM loss function wrapper for scipy.optimize.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
beta : np.ndarray
|
|
218
|
+
Propensity score coefficients.
|
|
219
|
+
*args, **kwargs
|
|
220
|
+
Arguments passed to _gmm_func1.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
float
|
|
225
|
+
GMM loss value.
|
|
226
|
+
"""
|
|
227
|
+
return _gmm_func1(beta, *args, **kwargs)['loss']
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def cbps_optimal_2treat(
|
|
231
|
+
treat: np.ndarray,
|
|
232
|
+
X: np.ndarray,
|
|
233
|
+
baseline_X: np.ndarray,
|
|
234
|
+
diff_X: np.ndarray,
|
|
235
|
+
iterations: int = 1000,
|
|
236
|
+
att: int = 0,
|
|
237
|
+
standardize: bool = True
|
|
238
|
+
) -> Dict[str, Any]:
|
|
239
|
+
"""
|
|
240
|
+
Optimal CBPS for binary treatments with double robustness and efficiency.
|
|
241
|
+
|
|
242
|
+
Implements the optimal covariate balancing conditions from Fan et al.
|
|
243
|
+
(2022), achieving double robustness and semiparametric efficiency by
|
|
244
|
+
separating balance conditions for baseline outcome and treatment effect
|
|
245
|
+
heterogeneity models.
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
treat : np.ndarray
|
|
250
|
+
Binary treatment vector (0/1 encoded, n-dimensional).
|
|
251
|
+
X : np.ndarray
|
|
252
|
+
Covariate matrix including intercept (n x k).
|
|
253
|
+
baseline_X : np.ndarray
|
|
254
|
+
Design matrix from baseline formula (h1 covariates, n x m1).
|
|
255
|
+
Zero-variance columns should be filtered before calling.
|
|
256
|
+
diff_X : np.ndarray
|
|
257
|
+
Design matrix from diff formula (h2 covariates, n x m2).
|
|
258
|
+
Zero-variance columns should be filtered before calling.
|
|
259
|
+
iterations : int, default 1000
|
|
260
|
+
Maximum BFGS iterations.
|
|
261
|
+
att : int, default 0
|
|
262
|
+
Estimand target. Only att=0 (ATE) is supported for oCBPS.
|
|
263
|
+
standardize : bool, default True
|
|
264
|
+
Whether to standardize weights.
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
dict
|
|
269
|
+
Dictionary containing:
|
|
270
|
+
- coefficients: Coefficient matrix (k x 1)
|
|
271
|
+
- fitted_values: Propensity scores (n-dimensional)
|
|
272
|
+
- linear_predictor: Linear predictor X @ beta (n-dimensional)
|
|
273
|
+
- deviance: Negative 2 times log-likelihood
|
|
274
|
+
- weights: Optimal weights (n-dimensional)
|
|
275
|
+
- y: Treatment vector (n-dimensional)
|
|
276
|
+
- x: Covariate matrix (n x k)
|
|
277
|
+
- converged: Convergence flag (bool)
|
|
278
|
+
- J: J-statistic (float)
|
|
279
|
+
- var: Variance-covariance matrix (k x k)
|
|
280
|
+
- mle_J: MLE baseline J-statistic (float)
|
|
281
|
+
|
|
282
|
+
Raises
|
|
283
|
+
------
|
|
284
|
+
ValueError
|
|
285
|
+
If baseline and diff model dimensions are incompatible.
|
|
286
|
+
|
|
287
|
+
Notes
|
|
288
|
+
-----
|
|
289
|
+
**Key Features:**
|
|
290
|
+
|
|
291
|
+
- Only supports att=0 (ATE estimation)
|
|
292
|
+
- No sample_weights parameter (oCBPS does not support sampling weights)
|
|
293
|
+
- Dual initialization optimization for robust convergence
|
|
294
|
+
|
|
295
|
+
**Dual Balancing Conditions** (Fan 2022 Eq. 3.2-3.3):
|
|
296
|
+
|
|
297
|
+
- g1_baseline: (T/π - (1-T)/(1-π)) h1(X) = 0, balances E(Y(0)|X)
|
|
298
|
+
- g2_diff: (T/π - 1) h2(X) = 0, balances E(Y(1)-Y(0)|X)
|
|
299
|
+
|
|
300
|
+
**Double Robustness** (Theorem 3.1):
|
|
301
|
+
Consistent if either the propensity score model or outcome model is correct.
|
|
302
|
+
|
|
303
|
+
**Semiparametric Efficiency** (Corollary 3.2):
|
|
304
|
+
Achieves Hahn 1998 efficiency bound when both models are correct and m=q.
|
|
305
|
+
|
|
306
|
+
References
|
|
307
|
+
----------
|
|
308
|
+
.. [1] Fan et al. (2022). Optimal Covariate Balancing Conditions in
|
|
309
|
+
Propensity Score Estimation. Journal of Business & Economic
|
|
310
|
+
Statistics, 41(1), 97-110. https://doi.org/10.1080/07350015.2021.2002159
|
|
311
|
+
|
|
312
|
+
Examples
|
|
313
|
+
--------
|
|
314
|
+
>>> # See module-level documentation for complete examples
|
|
315
|
+
"""
|
|
316
|
+
# Initialize constants
|
|
317
|
+
n = X.shape[0]
|
|
318
|
+
|
|
319
|
+
# Determine identification status
|
|
320
|
+
m1 = baseline_X.shape[1]
|
|
321
|
+
m2 = diff_X.shape[1]
|
|
322
|
+
k = X.shape[1]
|
|
323
|
+
|
|
324
|
+
if m1 + m2 + 1 > k:
|
|
325
|
+
bal_only = 3 # Over-identified: m1 + m2 + 1 > q
|
|
326
|
+
xcov = None
|
|
327
|
+
elif m1 + m2 + 1 == k:
|
|
328
|
+
bal_only = 1 # Exactly identified: m1 + m2 + 1 = q
|
|
329
|
+
xcov = np.eye(m1 + m2 + 1)
|
|
330
|
+
else:
|
|
331
|
+
raise ValueError("Invalid baseline and diff models.")
|
|
332
|
+
|
|
333
|
+
# Dual initialization: GLM and CBPS pre-optimization paths
|
|
334
|
+
|
|
335
|
+
# GLM initial values
|
|
336
|
+
glm_model = sm.GLM(treat, X, family=sm.families.Binomial())
|
|
337
|
+
glm_result = glm_model.fit()
|
|
338
|
+
glm_beta_curr = glm_result.params.copy()
|
|
339
|
+
glm_beta_curr[np.isnan(glm_beta_curr)] = 0
|
|
340
|
+
|
|
341
|
+
# CBPS pre-optimization initial values
|
|
342
|
+
# Precompute simplified inverse matrix for pre-optimization
|
|
343
|
+
invV2 = scipy.linalg.pinv(X.T @ X)
|
|
344
|
+
|
|
345
|
+
# CBPS pre-optimization (single balance condition)
|
|
346
|
+
def gmm_loss_for_preopt(beta):
|
|
347
|
+
return _gmm_func1(beta, X, treat, baseline_X, diff_X,
|
|
348
|
+
invV=invV2, option="CBPS")['loss']
|
|
349
|
+
|
|
350
|
+
cbps_preopt = scipy.optimize.minimize(
|
|
351
|
+
gmm_loss_for_preopt,
|
|
352
|
+
glm_beta_curr,
|
|
353
|
+
method='BFGS'
|
|
354
|
+
)
|
|
355
|
+
cbps_beta_curr = cbps_preopt.x
|
|
356
|
+
|
|
357
|
+
# GMM optimization branching
|
|
358
|
+
gmm_init = glm_beta_curr
|
|
359
|
+
|
|
360
|
+
if bal_only == 1:
|
|
361
|
+
# Exactly identified
|
|
362
|
+
opt_bal = scipy.optimize.minimize(
|
|
363
|
+
lambda b: _gmm_func1(b, X, treat, baseline_X, diff_X, invV=xcov)['loss'],
|
|
364
|
+
gmm_init,
|
|
365
|
+
method='BFGS'
|
|
366
|
+
)
|
|
367
|
+
opt1 = opt_bal
|
|
368
|
+
|
|
369
|
+
elif bal_only == 3:
|
|
370
|
+
# Over-identified
|
|
371
|
+
# GMM loss function (recompute invV each iteration)
|
|
372
|
+
def gmm_loss_std(beta):
|
|
373
|
+
return _gmm_func1(beta, X, treat, baseline_X, diff_X)['loss']
|
|
374
|
+
|
|
375
|
+
# GLM path optimization
|
|
376
|
+
gmm_glm_init = scipy.optimize.minimize(
|
|
377
|
+
gmm_loss_std,
|
|
378
|
+
glm_beta_curr,
|
|
379
|
+
method='BFGS',
|
|
380
|
+
options={'maxiter': iterations}
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# CBPS pre-optimization path
|
|
384
|
+
gmm_cbps_init = scipy.optimize.minimize(
|
|
385
|
+
gmm_loss_std,
|
|
386
|
+
cbps_beta_curr,
|
|
387
|
+
method='BFGS',
|
|
388
|
+
options={'maxiter': iterations}
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Select best initialization
|
|
392
|
+
if gmm_glm_init.fun < gmm_cbps_init.fun:
|
|
393
|
+
opt1 = gmm_glm_init
|
|
394
|
+
else:
|
|
395
|
+
opt1 = gmm_cbps_init
|
|
396
|
+
|
|
397
|
+
# Compute probabilities and weights
|
|
398
|
+
|
|
399
|
+
# Optimal coefficients
|
|
400
|
+
beta_opt = opt1.x
|
|
401
|
+
|
|
402
|
+
# Optimal propensity scores
|
|
403
|
+
theta_opt = X @ beta_opt
|
|
404
|
+
probs_opt = scipy.special.expit(theta_opt)
|
|
405
|
+
probs_opt = np.minimum(1 - PROBS_MIN, probs_opt)
|
|
406
|
+
probs_opt = np.maximum(PROBS_MIN, probs_opt)
|
|
407
|
+
|
|
408
|
+
# ATE weights (simplified form)
|
|
409
|
+
w_opt = np.abs((probs_opt - 1 + treat)**(-1))
|
|
410
|
+
|
|
411
|
+
# Weight standardization
|
|
412
|
+
if standardize:
|
|
413
|
+
norm1 = np.sum((treat == 1) / probs_opt)
|
|
414
|
+
norm2 = np.sum((treat == 0) / (1 - probs_opt))
|
|
415
|
+
else:
|
|
416
|
+
norm1 = norm2 = 1.0
|
|
417
|
+
|
|
418
|
+
w_opt = ((treat == 1) / probs_opt / norm1 +
|
|
419
|
+
(treat == 0) / (1 - probs_opt) / norm2)
|
|
420
|
+
|
|
421
|
+
# Compute variance-covariance matrix
|
|
422
|
+
|
|
423
|
+
# Construct X1new (required for vcov computation)
|
|
424
|
+
X1new = np.column_stack([X[:, 0], baseline_X])
|
|
425
|
+
|
|
426
|
+
# G matrix construction
|
|
427
|
+
factor_1 = np.sqrt(np.abs(treat - probs_opt) / (probs_opt * (1 - probs_opt)))
|
|
428
|
+
XG_1 = -X * factor_1[:, None]
|
|
429
|
+
XG_12 = -X1new * factor_1[:, None]
|
|
430
|
+
XW_1 = X1new * ((probs_opt - 1 + treat)**(-1))[:, None]
|
|
431
|
+
|
|
432
|
+
factor_2 = np.sqrt(treat * (1 - probs_opt) / probs_opt)
|
|
433
|
+
XG_2 = -X * factor_2[:, None]
|
|
434
|
+
XG_22 = -diff_X * factor_2[:, None]
|
|
435
|
+
XW_2 = diff_X * ((treat / probs_opt - 1))[:, None]
|
|
436
|
+
|
|
437
|
+
# W1 matrix
|
|
438
|
+
W1 = np.vstack([XW_1.T, XW_2.T])
|
|
439
|
+
|
|
440
|
+
# G matrix
|
|
441
|
+
G = np.column_stack([
|
|
442
|
+
(XG_1.T @ XG_12) / n,
|
|
443
|
+
(XG_2.T @ XG_22) / n
|
|
444
|
+
])
|
|
445
|
+
|
|
446
|
+
# Omega outer product
|
|
447
|
+
Omega = (W1 @ W1.T) / n
|
|
448
|
+
|
|
449
|
+
# Sandwich variance formula
|
|
450
|
+
gmm_result = _gmm_func1(beta_opt, X, treat, baseline_X, diff_X, invV=None)
|
|
451
|
+
W = gmm_result['invV']
|
|
452
|
+
|
|
453
|
+
GWG_inv = _r_ginv(G @ W @ G.T)
|
|
454
|
+
vcov = GWG_inv @ G @ W @ Omega @ W.T @ G.T @ GWG_inv
|
|
455
|
+
|
|
456
|
+
# Construct return object
|
|
457
|
+
|
|
458
|
+
# J-statistic
|
|
459
|
+
J_opt = _gmm_func1(beta_opt, X, treat, baseline_X, diff_X, invV=None)['loss']
|
|
460
|
+
|
|
461
|
+
# Deviance
|
|
462
|
+
deviance = -2 * np.sum(
|
|
463
|
+
treat * np.log(probs_opt) + (1 - treat) * np.log(1 - probs_opt)
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# MLE baseline J-statistic
|
|
467
|
+
glm1_coef = glm_beta_curr
|
|
468
|
+
mle_J = _gmm_func1(glm1_coef, X, treat, baseline_X, diff_X)['loss']
|
|
469
|
+
|
|
470
|
+
# Build output dictionary
|
|
471
|
+
output = {
|
|
472
|
+
'coefficients': beta_opt.reshape(-1, 1), # k x 1 matrix
|
|
473
|
+
'fitted_values': probs_opt,
|
|
474
|
+
'linear_predictor': theta_opt,
|
|
475
|
+
'deviance': deviance,
|
|
476
|
+
'weights': w_opt,
|
|
477
|
+
'y': treat,
|
|
478
|
+
'x': X,
|
|
479
|
+
'converged': opt1.success,
|
|
480
|
+
'J': J_opt,
|
|
481
|
+
'var': vcov,
|
|
482
|
+
'mle_J': mle_J
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# ========== oCBPS Condition Verification (P1-18/21) ==========
|
|
486
|
+
# Verify observable necessary conditions for oCBPS validity.
|
|
487
|
+
try:
|
|
488
|
+
from cbps.diagnostics.ocbps_conditions import verify_ocbps_conditions
|
|
489
|
+
conditions = verify_ocbps_conditions(output, X, treat)
|
|
490
|
+
output['ocbps_conditions'] = conditions
|
|
491
|
+
|
|
492
|
+
if not conditions['all_conditions_met']:
|
|
493
|
+
warn_msgs = conditions.get('warnings', [])
|
|
494
|
+
if warn_msgs:
|
|
495
|
+
warnings.warn(
|
|
496
|
+
"oCBPS efficiency conditions not fully met: "
|
|
497
|
+
+ "; ".join(warn_msgs),
|
|
498
|
+
UserWarning
|
|
499
|
+
)
|
|
500
|
+
except (ImportError, Exception) as e:
|
|
501
|
+
# Diagnostics should never block estimation
|
|
502
|
+
output['ocbps_conditions'] = {
|
|
503
|
+
'error': str(e),
|
|
504
|
+
'all_conditions_met': None
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return output
|