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.
Files changed (70) hide show
  1. cbps/__init__.py +3462 -0
  2. cbps/constants.py +46 -0
  3. cbps/core/__init__.py +93 -0
  4. cbps/core/cbps_binary.py +1943 -0
  5. cbps/core/cbps_continuous.py +945 -0
  6. cbps/core/cbps_multitreat.py +1123 -0
  7. cbps/core/cbps_optimal.py +507 -0
  8. cbps/core/results.py +1447 -0
  9. cbps/data/Blackwell.csv +571 -0
  10. cbps/data/LaLonde.csv +3213 -0
  11. cbps/data/npcbps_continuous_sim.csv +501 -0
  12. cbps/data/nsw.csv +723 -0
  13. cbps/data/nsw_dw.csv +446 -0
  14. cbps/data/political_ads_urban_niebler.csv +16266 -0
  15. cbps/data/psid_controls.csv +2491 -0
  16. cbps/data/psid_controls2.csv +254 -0
  17. cbps/data/psid_controls3.csv +129 -0
  18. cbps/data/simulation_dgp1_seed12345.csv +201 -0
  19. cbps/data/simulation_dgp2_seed12345.csv +201 -0
  20. cbps/data/simulation_dgp3_seed12345.csv +201 -0
  21. cbps/data/simulation_dgp4_seed12345.csv +201 -0
  22. cbps/datasets/__init__.py +78 -0
  23. cbps/datasets/blackwell.py +112 -0
  24. cbps/datasets/continuous.py +223 -0
  25. cbps/datasets/lalonde.py +272 -0
  26. cbps/datasets/npcbps_sim.py +101 -0
  27. cbps/diagnostics/__init__.py +101 -0
  28. cbps/diagnostics/balance.py +760 -0
  29. cbps/diagnostics/balance_cbmsm_addon.py +162 -0
  30. cbps/diagnostics/continuous_diagnostics.py +259 -0
  31. cbps/diagnostics/normality.py +173 -0
  32. cbps/diagnostics/ocbps_conditions.py +197 -0
  33. cbps/diagnostics/overlap.py +198 -0
  34. cbps/diagnostics/plots.py +1193 -0
  35. cbps/diagnostics/weights_diag.py +205 -0
  36. cbps/highdim/__init__.py +84 -0
  37. cbps/highdim/gmm_loss.py +340 -0
  38. cbps/highdim/hdcbps.py +1078 -0
  39. cbps/highdim/lasso_utils.py +498 -0
  40. cbps/highdim/weight_funcs.py +298 -0
  41. cbps/inference/__init__.py +42 -0
  42. cbps/inference/asyvar.py +621 -0
  43. cbps/inference/vcov_outcome.py +217 -0
  44. cbps/iv/__init__.py +48 -0
  45. cbps/iv/cbiv.py +2603 -0
  46. cbps/logging_config.py +45 -0
  47. cbps/msm/__init__.py +45 -0
  48. cbps/msm/cbmsm.py +1871 -0
  49. cbps/msm/rank_diagnostics.py +112 -0
  50. cbps/nonparametric/__init__.py +58 -0
  51. cbps/nonparametric/cholesky_whitening.py +232 -0
  52. cbps/nonparametric/empirical_likelihood.py +339 -0
  53. cbps/nonparametric/npcbps.py +1036 -0
  54. cbps/nonparametric/taylor_approx.py +207 -0
  55. cbps/py.typed +0 -0
  56. cbps/sklearn/__init__.py +42 -0
  57. cbps/sklearn/estimator.py +378 -0
  58. cbps/utils/__init__.py +82 -0
  59. cbps/utils/formula.py +415 -0
  60. cbps/utils/helpers.py +378 -0
  61. cbps/utils/numerics.py +438 -0
  62. cbps/utils/r_compat.py +109 -0
  63. cbps/utils/validation.py +224 -0
  64. cbps/utils/variance_transform.py +483 -0
  65. cbps/utils/weights.py +586 -0
  66. cbps-0.2.0.dist-info/METADATA +1090 -0
  67. cbps-0.2.0.dist-info/RECORD +70 -0
  68. cbps-0.2.0.dist-info/WHEEL +5 -0
  69. cbps-0.2.0.dist-info/licenses/LICENSE +661 -0
  70. cbps-0.2.0.dist-info/top_level.txt +1 -0
@@ -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
+ }