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
cbps/inference/asyvar.py
ADDED
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Asymptotic Variance Estimation for ATE
|
|
3
|
+
=======================================
|
|
4
|
+
|
|
5
|
+
Variance estimation and confidence interval construction for IPTW estimators
|
|
6
|
+
of the average treatment effect (ATE) under binary treatment.
|
|
7
|
+
|
|
8
|
+
Two variance formulas are implemented:
|
|
9
|
+
|
|
10
|
+
- **oCBPS**: Semiparametric efficiency bound (Corollary 2.2 of Fan et al., 2022),
|
|
11
|
+
which attains the Hahn (1998) efficiency bound when both the propensity score
|
|
12
|
+
and outcome models are correctly specified.
|
|
13
|
+
|
|
14
|
+
- **CBPS**: Full sandwich variance (Theorem 2.1 of Fan et al., 2022), accounting
|
|
15
|
+
for estimation uncertainty in propensity score parameters via the GMM
|
|
16
|
+
asymptotic variance formula.
|
|
17
|
+
|
|
18
|
+
References
|
|
19
|
+
----------
|
|
20
|
+
Fan, J., Imai, K., Lee, I., Liu, H., Ning, Y., and Yang, X. (2022).
|
|
21
|
+
Optimal covariate balancing conditions in propensity score estimation.
|
|
22
|
+
Journal of Business & Economic Statistics, 41(1), 97-110.
|
|
23
|
+
https://doi.org/10.1080/07350015.2021.2002159
|
|
24
|
+
|
|
25
|
+
Hahn, J. (1998). On the role of the propensity score in efficient
|
|
26
|
+
semiparametric estimation of average treatment effects.
|
|
27
|
+
Econometrica, 66(2), 315-331.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
import scipy.stats
|
|
32
|
+
import scipy.linalg
|
|
33
|
+
from sklearn.linear_model import LinearRegression
|
|
34
|
+
from typing import Optional, Dict, Any
|
|
35
|
+
import warnings
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def asy_var(
|
|
39
|
+
Y: np.ndarray,
|
|
40
|
+
Y_1_hat: Optional[np.ndarray] = None,
|
|
41
|
+
Y_0_hat: Optional[np.ndarray] = None,
|
|
42
|
+
CBPS_obj: Optional[Dict[str, Any]] = None,
|
|
43
|
+
method: str = "CBPS",
|
|
44
|
+
X: Optional[np.ndarray] = None,
|
|
45
|
+
TL: Optional[np.ndarray] = None,
|
|
46
|
+
pi: Optional[np.ndarray] = None,
|
|
47
|
+
mu: Optional[float] = None,
|
|
48
|
+
CI: float = 0.95,
|
|
49
|
+
use_observed_y: bool = False
|
|
50
|
+
) -> Dict[str, Any]:
|
|
51
|
+
"""
|
|
52
|
+
Estimate asymptotic variance and confidence intervals for ATE.
|
|
53
|
+
|
|
54
|
+
Computes the asymptotic variance of the IPTW estimator for the average
|
|
55
|
+
treatment effect under binary treatment. Two methods are available:
|
|
56
|
+
the semiparametric efficiency bound (oCBPS) and the full sandwich
|
|
57
|
+
formula (CBPS).
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
Y : ndarray of shape (n,)
|
|
62
|
+
Observed outcome values.
|
|
63
|
+
Y_1_hat : ndarray of shape (n,), optional
|
|
64
|
+
Predicted potential outcomes under treatment, E[Y(1)|X]. If None,
|
|
65
|
+
fitted via OLS on the treatment group.
|
|
66
|
+
Y_0_hat : ndarray of shape (n,), optional
|
|
67
|
+
Predicted potential outcomes under control, E[Y(0)|X]. If None,
|
|
68
|
+
fitted via OLS on the control group.
|
|
69
|
+
CBPS_obj : dict or CBPSResults, optional
|
|
70
|
+
Fitted CBPS object containing 'x', 'y', 'fitted_values', and
|
|
71
|
+
'coefficients' attributes.
|
|
72
|
+
method : {'CBPS', 'oCBPS'}, default='CBPS'
|
|
73
|
+
Variance estimation method:
|
|
74
|
+
|
|
75
|
+
- 'CBPS': Full sandwich formula (Theorem 2.1 of Fan et al., 2022)
|
|
76
|
+
- 'oCBPS': Semiparametric efficiency bound (Corollary 2.2)
|
|
77
|
+
|
|
78
|
+
X : ndarray of shape (n, p), optional
|
|
79
|
+
Covariate matrix with intercept column. Extracted from CBPS_obj
|
|
80
|
+
if not provided.
|
|
81
|
+
TL : ndarray of shape (n,), optional
|
|
82
|
+
Binary treatment indicator (0/1). Extracted from CBPS_obj if not
|
|
83
|
+
provided.
|
|
84
|
+
pi : ndarray of shape (n,), optional
|
|
85
|
+
Estimated propensity scores in (0, 1). Extracted from CBPS_obj
|
|
86
|
+
if not provided.
|
|
87
|
+
mu : float, optional
|
|
88
|
+
ATE point estimate. If None, computed via AIPW (Augmented IPW).
|
|
89
|
+
Note: The variance formula is the semiparametric efficiency bound,
|
|
90
|
+
which is valid for AIPW but NOT for simple IPTW.
|
|
91
|
+
CI : float, default=0.95
|
|
92
|
+
Confidence level for the interval, in (0, 1).
|
|
93
|
+
use_observed_y : bool, default=False
|
|
94
|
+
Sigma_mu computation method:
|
|
95
|
+
|
|
96
|
+
- False (default): Use predicted values Y_1_hat, Y_0_hat.
|
|
97
|
+
This matches R CBPS package behavior and is recommended.
|
|
98
|
+
- True: Use observed Y values. This is an experimental option
|
|
99
|
+
not implemented in the R package.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
dict
|
|
104
|
+
Dictionary with keys:
|
|
105
|
+
|
|
106
|
+
- 'mu.hat': ATE point estimate
|
|
107
|
+
- 'asy.var': Asymptotic variance of sqrt(n) * (mu_hat - mu)
|
|
108
|
+
- 'var': Finite-sample variance (asy.var / n)
|
|
109
|
+
- 'std.err': Standard error
|
|
110
|
+
- 'CI.mu.hat': ndarray of shape (2,), confidence interval bounds
|
|
111
|
+
|
|
112
|
+
See Also
|
|
113
|
+
--------
|
|
114
|
+
cbps.AsyVar : Public interface accepting CBPSResults objects.
|
|
115
|
+
vcov_outcome : Variance for continuous treatment weighted regressions.
|
|
116
|
+
|
|
117
|
+
Notes
|
|
118
|
+
-----
|
|
119
|
+
**Important**: This function uses the AIPW (Augmented Inverse Probability
|
|
120
|
+
Weighting) estimator for the ATE point estimate, NOT simple IPTW. This is
|
|
121
|
+
because the variance formula (semiparametric efficiency bound) is the
|
|
122
|
+
variance of AIPW, not IPTW.
|
|
123
|
+
|
|
124
|
+
The AIPW estimator is:
|
|
125
|
+
|
|
126
|
+
.. math::
|
|
127
|
+
|
|
128
|
+
\\hat{\\mu}_{AIPW} = \\frac{1}{n}\\sum_{i=1}^{n}\\left[
|
|
129
|
+
\\hat{m}_1(X_i) - \\hat{m}_0(X_i) +
|
|
130
|
+
\\frac{T_i(Y_i - \\hat{m}_1(X_i))}{\\pi_i} -
|
|
131
|
+
\\frac{(1-T_i)(Y_i - \\hat{m}_0(X_i))}{1-\\pi_i}
|
|
132
|
+
\\right]
|
|
133
|
+
|
|
134
|
+
where :math:`\\hat{m}_1(X) = E[Y(1)|X]` and :math:`\\hat{m}_0(X) = E[Y(0)|X]`
|
|
135
|
+
are fitted via OLS on the treatment and control groups respectively.
|
|
136
|
+
|
|
137
|
+
The oCBPS variance attains the semiparametric efficiency bound
|
|
138
|
+
(Hahn, 1998, Theorem 1; Fan et al., 2022, Corollary 2.2, Eq. 2.6):
|
|
139
|
+
|
|
140
|
+
.. math::
|
|
141
|
+
|
|
142
|
+
V_{\\text{opt}} = E\\left[\\frac{\\text{Var}(Y(1)|X)}{\\pi(X)} +
|
|
143
|
+
\\frac{\\text{Var}(Y(0)|X)}{1-\\pi(X)} + (L(X) - \\mu)^2\\right]
|
|
144
|
+
|
|
145
|
+
where L(X) = E[Y(1) - Y(0) | X] is the conditional average treatment
|
|
146
|
+
effect.
|
|
147
|
+
|
|
148
|
+
The CBPS variance uses the sandwich formula (Fan et al., 2022,
|
|
149
|
+
Theorem 2.1, Eq. 2.4):
|
|
150
|
+
|
|
151
|
+
.. math::
|
|
152
|
+
|
|
153
|
+
V = \\bar{H}^{*T} \\Sigma \\bar{H}^*
|
|
154
|
+
|
|
155
|
+
where :math:`\\bar{H}^* = (1, H_y^{*T})^T` and :math:`\\Sigma` is the
|
|
156
|
+
joint variance-covariance matrix (Eq. 2.3) that accounts for
|
|
157
|
+
propensity score estimation uncertainty.
|
|
158
|
+
|
|
159
|
+
**Historical Note**: The original R CBPS package uses simple IPTW for the
|
|
160
|
+
point estimate. This Python implementation corrects this mismatch to ensure
|
|
161
|
+
the variance formula matches the estimator.
|
|
162
|
+
|
|
163
|
+
Examples
|
|
164
|
+
--------
|
|
165
|
+
>>> from cbps import CBPS, AsyVar
|
|
166
|
+
>>> fit = CBPS('treat ~ age + education', data=df, att=0)
|
|
167
|
+
>>> result = AsyVar(Y=df['outcome'], CBPS_obj=fit, method='oCBPS')
|
|
168
|
+
>>> print(f"ATE: {result['mu.hat']:.3f} (SE: {result['std.err']:.3f})")
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
# Parameter extraction (order: X -> TL -> pi -> mu)
|
|
172
|
+
def _get_attr(obj, key):
|
|
173
|
+
if isinstance(obj, dict):
|
|
174
|
+
return obj[key]
|
|
175
|
+
return getattr(obj, key)
|
|
176
|
+
|
|
177
|
+
# Extract X
|
|
178
|
+
if X is None:
|
|
179
|
+
if CBPS_obj is None:
|
|
180
|
+
raise ValueError("Must specify X or CBPS_obj")
|
|
181
|
+
X = _get_attr(CBPS_obj, 'x')
|
|
182
|
+
|
|
183
|
+
# Extract TL
|
|
184
|
+
if TL is None:
|
|
185
|
+
if CBPS_obj is None:
|
|
186
|
+
raise ValueError("Must specify TL or CBPS_obj")
|
|
187
|
+
TL = _get_attr(CBPS_obj, 'y')
|
|
188
|
+
|
|
189
|
+
# Extract pi
|
|
190
|
+
if pi is None:
|
|
191
|
+
if CBPS_obj is None:
|
|
192
|
+
raise ValueError("Must specify pi or CBPS_obj")
|
|
193
|
+
pi = _get_attr(CBPS_obj, 'fitted_values')
|
|
194
|
+
|
|
195
|
+
# Dimension and range validation
|
|
196
|
+
|
|
197
|
+
n = len(Y)
|
|
198
|
+
p = X.shape[1]
|
|
199
|
+
|
|
200
|
+
# Dimension checks
|
|
201
|
+
if X.shape[0] != n:
|
|
202
|
+
raise ValueError(f"X row count ({X.shape[0]}) does not match Y length ({n})")
|
|
203
|
+
if len(TL) != n:
|
|
204
|
+
raise ValueError(f"TL length ({len(TL)}) does not match Y length ({n})")
|
|
205
|
+
if len(pi) != n:
|
|
206
|
+
raise ValueError(f"pi length ({len(pi)}) does not match Y length ({n})")
|
|
207
|
+
|
|
208
|
+
# Propensity score range validation
|
|
209
|
+
if not np.all((pi >= 0) & (pi <= 1)):
|
|
210
|
+
raise ValueError("Propensity score pi must be in [0, 1] interval")
|
|
211
|
+
if np.any((pi <= 0) | (pi >= 1)):
|
|
212
|
+
warnings.warn(
|
|
213
|
+
"Propensity scores contain boundary values (0 or 1); "
|
|
214
|
+
"variance estimate may be Inf or NaN.",
|
|
215
|
+
UserWarning
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Treatment indicator validation
|
|
219
|
+
if not np.all((TL == 0) | (TL == 1)):
|
|
220
|
+
unique_tl = np.unique(TL)
|
|
221
|
+
if len(unique_tl) > 4:
|
|
222
|
+
raise ValueError(
|
|
223
|
+
"asy_var supports binary treatment only. "
|
|
224
|
+
f"Detected {len(unique_tl)} unique treatment values. "
|
|
225
|
+
"For continuous treatment, use vcov_outcome()."
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
warnings.warn(
|
|
229
|
+
f"Treatment indicator TL is not standard 0/1 coding "
|
|
230
|
+
f"(values: {unique_tl}). "
|
|
231
|
+
"Results may be invalid for non-binary treatment.",
|
|
232
|
+
UserWarning
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if not (0 < CI < 1):
|
|
236
|
+
raise ValueError(f"Confidence level CI must be in (0, 1), got {CI}")
|
|
237
|
+
if method not in ["CBPS", "oCBPS"]:
|
|
238
|
+
raise ValueError(f"method must be 'CBPS' or 'oCBPS', got '{method}'")
|
|
239
|
+
|
|
240
|
+
# Sample size requirement: n_group > p for residual variance estimation
|
|
241
|
+
n_1 = np.sum(TL == 1)
|
|
242
|
+
n_0 = np.sum(TL == 0)
|
|
243
|
+
if n_1 <= p or n_0 <= p:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"Group sizes must exceed covariate dimension p. "
|
|
246
|
+
f"Current: n_1={n_1}, n_0={n_0}, p={p}."
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Extreme propensity score warning
|
|
250
|
+
eps = 1e-6
|
|
251
|
+
if np.any(pi <= eps) or np.any(pi >= 1 - eps):
|
|
252
|
+
warnings.warn(
|
|
253
|
+
f"Extreme propensity scores detected "
|
|
254
|
+
f"(min={pi.min():.6f}, max={pi.max():.6f}); "
|
|
255
|
+
"numerical instability may occur.",
|
|
256
|
+
RuntimeWarning
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Fit outcome models for potential outcomes FIRST (needed for AIPW)
|
|
260
|
+
Y_1_hat, Y_0_hat = _fit_outcome_models(Y, X, TL, Y_1_hat, Y_0_hat, p)
|
|
261
|
+
|
|
262
|
+
# Compute ATE via AIPW if not provided
|
|
263
|
+
# CRITICAL FIX: Use AIPW (Augmented IPW) instead of simple IPTW
|
|
264
|
+
# The efficiency bound variance formula is for AIPW, not IPTW!
|
|
265
|
+
# AIPW formula: (1/n) Σ [m₁(X) - m₀(X) + T(Y - m₁(X))/π - (1-T)(Y - m₀(X))/(1-π)]
|
|
266
|
+
if mu is None:
|
|
267
|
+
if CBPS_obj is None:
|
|
268
|
+
raise ValueError("Must specify mu or CBPS_obj")
|
|
269
|
+
# AIPW estimator (doubly robust, matches efficiency bound variance)
|
|
270
|
+
mu = np.mean(
|
|
271
|
+
Y_1_hat - Y_0_hat +
|
|
272
|
+
TL * (Y - Y_1_hat) / pi -
|
|
273
|
+
(1 - TL) * (Y - Y_0_hat) / (1 - pi)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Auxiliary estimates: L(X) = CATE, K(X) = E[Y(0)|X]
|
|
277
|
+
L_hat = Y_1_hat - Y_0_hat
|
|
278
|
+
K_hat = Y_0_hat
|
|
279
|
+
|
|
280
|
+
# Dispatch to method-specific variance computation
|
|
281
|
+
if method == "oCBPS":
|
|
282
|
+
result = _compute_asy_var_ocbps(
|
|
283
|
+
Y, Y_1_hat, Y_0_hat, TL, pi, mu, L_hat, n, n_1, n_0, p, CI
|
|
284
|
+
)
|
|
285
|
+
elif method == "CBPS":
|
|
286
|
+
# CBPS method requires propensity score coefficients
|
|
287
|
+
if CBPS_obj is None:
|
|
288
|
+
raise ValueError("CBPS method requires CBPS_obj to obtain coefficients")
|
|
289
|
+
if isinstance(CBPS_obj, dict):
|
|
290
|
+
if 'coefficients' not in CBPS_obj:
|
|
291
|
+
raise ValueError("CBPS_obj must contain 'coefficients' key")
|
|
292
|
+
else:
|
|
293
|
+
if not hasattr(CBPS_obj, 'coefficients'):
|
|
294
|
+
raise ValueError("CBPS_obj must have 'coefficients' attribute")
|
|
295
|
+
|
|
296
|
+
beta = _get_attr(CBPS_obj, 'coefficients')
|
|
297
|
+
# Ensure beta is 1D array
|
|
298
|
+
if beta.ndim == 2:
|
|
299
|
+
beta = beta.ravel()
|
|
300
|
+
result = _compute_asy_var_cbps(
|
|
301
|
+
Y, Y_1_hat, Y_0_hat, X, TL, pi, mu, L_hat, K_hat, beta, n, p, CI,
|
|
302
|
+
use_observed_y
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _fit_outcome_models(
|
|
309
|
+
Y: np.ndarray,
|
|
310
|
+
X: np.ndarray,
|
|
311
|
+
TL: np.ndarray,
|
|
312
|
+
Y_1_hat: Optional[np.ndarray],
|
|
313
|
+
Y_0_hat: Optional[np.ndarray],
|
|
314
|
+
p: int
|
|
315
|
+
) -> tuple:
|
|
316
|
+
"""
|
|
317
|
+
Fit outcome models E[Y(1)|X] and E[Y(0)|X] via OLS.
|
|
318
|
+
|
|
319
|
+
Parameters
|
|
320
|
+
----------
|
|
321
|
+
Y : ndarray of shape (n,)
|
|
322
|
+
Observed outcomes.
|
|
323
|
+
X : ndarray of shape (n, p)
|
|
324
|
+
Covariates with intercept column.
|
|
325
|
+
TL : ndarray of shape (n,)
|
|
326
|
+
Treatment indicator.
|
|
327
|
+
Y_1_hat, Y_0_hat : ndarray or None
|
|
328
|
+
Pre-computed predictions; fitted here if None.
|
|
329
|
+
p : int
|
|
330
|
+
Number of covariates including intercept.
|
|
331
|
+
|
|
332
|
+
Returns
|
|
333
|
+
-------
|
|
334
|
+
Y_1_hat, Y_0_hat : ndarray of shape (n,)
|
|
335
|
+
Full-sample predictions of potential outcomes.
|
|
336
|
+
"""
|
|
337
|
+
# Fit E[Y(1)|X] on treatment group
|
|
338
|
+
if Y_1_hat is None:
|
|
339
|
+
X_1 = X[TL == 1]
|
|
340
|
+
Y_1 = Y[TL == 1]
|
|
341
|
+
model_1 = LinearRegression(fit_intercept=False).fit(X_1, Y_1)
|
|
342
|
+
Y_1_hat = X @ model_1.coef_
|
|
343
|
+
|
|
344
|
+
# Fit E[Y(0)|X] on control group
|
|
345
|
+
if Y_0_hat is None:
|
|
346
|
+
X_0 = X[TL == 0]
|
|
347
|
+
Y_0 = Y[TL == 0]
|
|
348
|
+
model_0 = LinearRegression(fit_intercept=False).fit(X_0, Y_0)
|
|
349
|
+
Y_0_hat = X @ model_0.coef_
|
|
350
|
+
|
|
351
|
+
return Y_1_hat, Y_0_hat
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _compute_asy_var_ocbps(
|
|
355
|
+
Y: np.ndarray,
|
|
356
|
+
Y_1_hat: np.ndarray,
|
|
357
|
+
Y_0_hat: np.ndarray,
|
|
358
|
+
TL: np.ndarray,
|
|
359
|
+
pi: np.ndarray,
|
|
360
|
+
mu: float,
|
|
361
|
+
L_hat: np.ndarray,
|
|
362
|
+
n: int,
|
|
363
|
+
n_1: int,
|
|
364
|
+
n_0: int,
|
|
365
|
+
p: int,
|
|
366
|
+
CI: float
|
|
367
|
+
) -> Dict[str, Any]:
|
|
368
|
+
"""
|
|
369
|
+
Compute variance using the semiparametric efficiency bound.
|
|
370
|
+
|
|
371
|
+
Implements Corollary 2.2 and Eq. 2.6 of Fan et al. (2022), which
|
|
372
|
+
attains the Hahn (1998) efficiency bound.
|
|
373
|
+
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
Y : ndarray of shape (n,)
|
|
377
|
+
Observed outcomes.
|
|
378
|
+
Y_1_hat, Y_0_hat : ndarray of shape (n,)
|
|
379
|
+
Predicted potential outcomes.
|
|
380
|
+
TL : ndarray of shape (n,)
|
|
381
|
+
Treatment indicator.
|
|
382
|
+
pi : ndarray of shape (n,)
|
|
383
|
+
Propensity scores.
|
|
384
|
+
mu : float
|
|
385
|
+
ATE point estimate.
|
|
386
|
+
L_hat : ndarray of shape (n,)
|
|
387
|
+
Estimated CATE, L(X) = E[Y(1) - Y(0) | X].
|
|
388
|
+
n, n_1, n_0 : int
|
|
389
|
+
Sample sizes (total, treated, control).
|
|
390
|
+
p : int
|
|
391
|
+
Number of covariates including intercept.
|
|
392
|
+
CI : float
|
|
393
|
+
Confidence level.
|
|
394
|
+
|
|
395
|
+
Returns
|
|
396
|
+
-------
|
|
397
|
+
dict
|
|
398
|
+
Keys: 'mu.hat', 'asy.var', 'var', 'std.err', 'CI.mu.hat'.
|
|
399
|
+
"""
|
|
400
|
+
# Estimate conditional variances via residuals
|
|
401
|
+
residuals_1 = (Y - Y_1_hat) * TL
|
|
402
|
+
sigma_hat_1_squared = np.sum(residuals_1**2) / (n_1 - p)
|
|
403
|
+
|
|
404
|
+
residuals_0 = (Y - Y_0_hat) * (1 - TL)
|
|
405
|
+
sigma_hat_0_squared = np.sum(residuals_0**2) / (n_0 - p)
|
|
406
|
+
|
|
407
|
+
# Semiparametric efficiency bound (Eq. 2.6)
|
|
408
|
+
asy_var = np.mean(
|
|
409
|
+
sigma_hat_1_squared / pi +
|
|
410
|
+
sigma_hat_0_squared / (1 - pi) +
|
|
411
|
+
(L_hat - mu)**2
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Finite-sample variance and standard error
|
|
415
|
+
var = asy_var / n
|
|
416
|
+
std_err = np.sqrt(var)
|
|
417
|
+
|
|
418
|
+
# Confidence interval
|
|
419
|
+
z_alpha = scipy.stats.norm.ppf(1 - (1 - CI) / 2)
|
|
420
|
+
diff = z_alpha * std_err
|
|
421
|
+
lower = mu - diff
|
|
422
|
+
upper = mu + diff
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
'mu.hat': mu,
|
|
426
|
+
'asy.var': asy_var,
|
|
427
|
+
'var': var,
|
|
428
|
+
'std.err': std_err,
|
|
429
|
+
'CI.mu.hat': np.array([lower, upper])
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _compute_asy_var_cbps(
|
|
434
|
+
Y: np.ndarray,
|
|
435
|
+
Y_1_hat: np.ndarray,
|
|
436
|
+
Y_0_hat: np.ndarray,
|
|
437
|
+
X: np.ndarray,
|
|
438
|
+
TL: np.ndarray,
|
|
439
|
+
pi: np.ndarray,
|
|
440
|
+
mu: float,
|
|
441
|
+
L_hat: np.ndarray,
|
|
442
|
+
K_hat: np.ndarray,
|
|
443
|
+
beta: np.ndarray,
|
|
444
|
+
n: int,
|
|
445
|
+
p: int,
|
|
446
|
+
CI: float,
|
|
447
|
+
use_observed_y: bool = False
|
|
448
|
+
) -> Dict[str, Any]:
|
|
449
|
+
"""
|
|
450
|
+
Compute variance using the sandwich formula.
|
|
451
|
+
|
|
452
|
+
Implements Theorem 2.1 of Fan et al. (2022). The asymptotic variance
|
|
453
|
+
is V = H_bar' Sigma H_bar (Eq. 2.4), where Sigma is the joint
|
|
454
|
+
variance-covariance matrix defined in Eq. 2.3.
|
|
455
|
+
|
|
456
|
+
Parameters
|
|
457
|
+
----------
|
|
458
|
+
Y : ndarray of shape (n,)
|
|
459
|
+
Observed outcomes.
|
|
460
|
+
Y_1_hat, Y_0_hat : ndarray of shape (n,)
|
|
461
|
+
Predicted potential outcomes.
|
|
462
|
+
X : ndarray of shape (n, p)
|
|
463
|
+
Covariate matrix with intercept.
|
|
464
|
+
TL : ndarray of shape (n,)
|
|
465
|
+
Treatment indicator.
|
|
466
|
+
pi : ndarray of shape (n,)
|
|
467
|
+
Propensity scores.
|
|
468
|
+
mu : float
|
|
469
|
+
ATE point estimate.
|
|
470
|
+
L_hat : ndarray of shape (n,)
|
|
471
|
+
Estimated CATE, L(X) = E[Y(1) - Y(0) | X].
|
|
472
|
+
K_hat : ndarray of shape (n,)
|
|
473
|
+
Estimated baseline mean, K(X) = E[Y(0)|X].
|
|
474
|
+
beta : ndarray of shape (p,)
|
|
475
|
+
Propensity score coefficients.
|
|
476
|
+
n : int
|
|
477
|
+
Sample size.
|
|
478
|
+
p : int
|
|
479
|
+
Number of covariates including intercept.
|
|
480
|
+
CI : float
|
|
481
|
+
Confidence level.
|
|
482
|
+
use_observed_y : bool
|
|
483
|
+
Use observed Y (True, experimental) or predicted values (False,
|
|
484
|
+
matches R package) in Sigma_mu.
|
|
485
|
+
|
|
486
|
+
Returns
|
|
487
|
+
-------
|
|
488
|
+
dict
|
|
489
|
+
Keys: 'mu.hat', 'asy.var', 'var', 'std.err', 'CI.mu.hat'.
|
|
490
|
+
"""
|
|
491
|
+
# Common denominator pi(1-pi)
|
|
492
|
+
denom = pi * (1 - pi)
|
|
493
|
+
|
|
494
|
+
# Omega = var(g) = E[XX' / (pi(1-pi))] per Eq. 2.3
|
|
495
|
+
omega_hat = np.mean(
|
|
496
|
+
X[:, :, None] * X[:, None, :] / denom[:, None, None],
|
|
497
|
+
axis=0
|
|
498
|
+
)
|
|
499
|
+
omega_hat = (omega_hat + omega_hat.T) / 2 # Symmetrize
|
|
500
|
+
|
|
501
|
+
# Sigma_mu = var(mu_beta) per Eq. 2.3
|
|
502
|
+
# CRITICAL FIX: The formula is E[Y(1)^2/pi + Y(0)^2/(1-pi)] - mu^2
|
|
503
|
+
# where E[Y(1)^2|X] = Var(Y(1)|X) + E[Y(1)|X]^2 = sigma_1^2 + m_1(X)^2
|
|
504
|
+
# The original implementation only used m_1(X)^2, missing the variance term!
|
|
505
|
+
#
|
|
506
|
+
# Correct formula: Sigma_mu = E[sigma_1^2/pi + sigma_0^2/(1-pi)]
|
|
507
|
+
# + E[m_1^2/pi + m_0^2/(1-pi)] - mu^2
|
|
508
|
+
|
|
509
|
+
# Estimate conditional variances (same as oCBPS method)
|
|
510
|
+
n_1 = np.sum(TL == 1)
|
|
511
|
+
n_0 = np.sum(TL == 0)
|
|
512
|
+
residuals_1 = (Y - Y_1_hat) * TL
|
|
513
|
+
sigma_1_sq = np.sum(residuals_1**2) / max(n_1 - p, 1)
|
|
514
|
+
residuals_0 = (Y - Y_0_hat) * (1 - TL)
|
|
515
|
+
sigma_0_sq = np.sum(residuals_0**2) / max(n_0 - p, 1)
|
|
516
|
+
|
|
517
|
+
if use_observed_y:
|
|
518
|
+
# Use observed Y values: E[T*Y^2/pi + (1-T)*Y^2/(1-pi)]
|
|
519
|
+
Sigma_mu_hat = np.mean(TL * Y**2 / pi + (1 - TL) * Y**2 / (1 - pi)) - mu**2
|
|
520
|
+
else:
|
|
521
|
+
# Use predicted values with estimated conditional variances
|
|
522
|
+
# E[(sigma_1^2 + m_1^2)/pi + (sigma_0^2 + m_0^2)/(1-pi)] - mu^2
|
|
523
|
+
Sigma_mu_hat = np.mean(
|
|
524
|
+
(sigma_1_sq + Y_1_hat**2) / pi +
|
|
525
|
+
(sigma_0_sq + Y_0_hat**2) / (1 - pi)
|
|
526
|
+
) - mu**2
|
|
527
|
+
|
|
528
|
+
# cov(mu_beta, g_beta) per Eq. 2.3
|
|
529
|
+
cov_hat = np.mean(
|
|
530
|
+
X * (K_hat + (1 - pi) * L_hat)[:, None] / denom[:, None],
|
|
531
|
+
axis=0
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# H_y^* = d(mu)/d(beta), the Jacobian defined after Eq. 2.2
|
|
535
|
+
eta = X @ beta
|
|
536
|
+
prop_modified = pi / (1.0 + np.exp(eta))
|
|
537
|
+
derivative = prop_modified[:, None] * X
|
|
538
|
+
factor = (K_hat + (1 - pi) * L_hat) / denom
|
|
539
|
+
H_0_hat = -np.mean(derivative * factor[:, None], axis=0)
|
|
540
|
+
|
|
541
|
+
# H_f^* = d(g)/d(beta), the Jacobian defined after Eq. 2.2
|
|
542
|
+
H_f_hat = -np.mean(
|
|
543
|
+
X[:, :, None] * (derivative / denom[:, None])[:, None, :], axis=0
|
|
544
|
+
)
|
|
545
|
+
H_f_hat = (H_f_hat + H_f_hat.T) / 2 # Symmetrize
|
|
546
|
+
|
|
547
|
+
# Check condition numbers
|
|
548
|
+
cond_Hf = np.linalg.cond(H_f_hat)
|
|
549
|
+
cond_Om = np.linalg.cond(omega_hat)
|
|
550
|
+
if cond_Hf > 1e10 or cond_Om > 1e10:
|
|
551
|
+
warnings.warn(
|
|
552
|
+
f"Ill-conditioned matrices: cond(H_f)={cond_Hf:.2e}, "
|
|
553
|
+
f"cond(Omega)={cond_Om:.2e}.",
|
|
554
|
+
RuntimeWarning
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Assemble V = H_bar' Sigma H_bar per Theorem 2.1
|
|
558
|
+
# Computed as: Sigma_mu + H_y' Sigma_beta H_y + 2 H_y' Sigma_mu_beta
|
|
559
|
+
term1 = Sigma_mu_hat
|
|
560
|
+
|
|
561
|
+
# H_y' Sigma_beta H_y where Sigma_beta = (H_f' Omega^{-1} H_f)^{-1}
|
|
562
|
+
try:
|
|
563
|
+
A = scipy.linalg.solve(omega_hat, H_f_hat, assume_a='sym')
|
|
564
|
+
except Exception:
|
|
565
|
+
A = scipy.linalg.pinvh(omega_hat) @ H_f_hat
|
|
566
|
+
G = H_f_hat.T @ A
|
|
567
|
+
G = (G + G.T) / 2
|
|
568
|
+
|
|
569
|
+
try:
|
|
570
|
+
sol = scipy.linalg.solve(G, H_0_hat, assume_a='sym', check_finite=False)
|
|
571
|
+
term2 = float(H_0_hat.T @ sol)
|
|
572
|
+
except Exception:
|
|
573
|
+
G_pinv = scipy.linalg.pinvh(G)
|
|
574
|
+
term2 = float(H_0_hat.T @ (G_pinv @ H_0_hat))
|
|
575
|
+
|
|
576
|
+
# 2 H_y' Sigma_mu_beta (note: Sigma_mu_beta has negative sign in Eq. 2.3)
|
|
577
|
+
try:
|
|
578
|
+
sol_hf = scipy.linalg.solve(
|
|
579
|
+
H_f_hat, cov_hat, assume_a='sym', check_finite=False
|
|
580
|
+
)
|
|
581
|
+
except Exception:
|
|
582
|
+
Hf_pinv = np.linalg.pinv(H_f_hat)
|
|
583
|
+
sol_hf = Hf_pinv @ cov_hat
|
|
584
|
+
term3 = float(2.0 * (H_0_hat.T @ sol_hf))
|
|
585
|
+
|
|
586
|
+
# Asymptotic variance
|
|
587
|
+
asy_var = float(term1 + term2 - term3)
|
|
588
|
+
|
|
589
|
+
# Handle negative variance (numerical artifact)
|
|
590
|
+
if asy_var < 0:
|
|
591
|
+
if asy_var > -1e-12:
|
|
592
|
+
warnings.warn(
|
|
593
|
+
f"Asymptotic variance slightly negative ({asy_var:.2e}); clipped to 0.",
|
|
594
|
+
RuntimeWarning,
|
|
595
|
+
stacklevel=2
|
|
596
|
+
)
|
|
597
|
+
asy_var = 0.0
|
|
598
|
+
else:
|
|
599
|
+
warnings.warn(
|
|
600
|
+
f"Asymptotic variance negative ({asy_var:.6f}). "
|
|
601
|
+
"Consider using method='oCBPS' or bootstrap inference.",
|
|
602
|
+
RuntimeWarning,
|
|
603
|
+
stacklevel=2
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
# Finite-sample variance and confidence interval
|
|
607
|
+
var = asy_var / n
|
|
608
|
+
std_err = np.sqrt(var)
|
|
609
|
+
|
|
610
|
+
z_alpha = scipy.stats.norm.ppf(1 - (1 - CI) / 2)
|
|
611
|
+
diff = z_alpha * std_err
|
|
612
|
+
lower = mu - diff
|
|
613
|
+
upper = mu + diff
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
'mu.hat': mu,
|
|
617
|
+
'asy.var': asy_var,
|
|
618
|
+
'var': var,
|
|
619
|
+
'std.err': std_err,
|
|
620
|
+
'CI.mu.hat': np.array([lower, upper])
|
|
621
|
+
}
|