skxperiments 0.1.0.dev0__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.
- skxperiments/__init__.py +5 -0
- skxperiments/core/__init__.py +42 -0
- skxperiments/core/assignment.py +589 -0
- skxperiments/core/base.py +512 -0
- skxperiments/core/exceptions.py +145 -0
- skxperiments/core/potential_outcomes.py +168 -0
- skxperiments/core/results.py +624 -0
- skxperiments/design/__init__.py +22 -0
- skxperiments/design/balance.py +182 -0
- skxperiments/design/blocked_crd.py +157 -0
- skxperiments/design/crd.py +162 -0
- skxperiments/design/factorial.py +174 -0
- skxperiments/design/power.py +233 -0
- skxperiments/design/rerandomized_crd.py +319 -0
- skxperiments/diagnostics/__init__.py +21 -0
- skxperiments/diagnostics/aa_test.py +277 -0
- skxperiments/diagnostics/balance_report.py +224 -0
- skxperiments/diagnostics/srm.py +327 -0
- skxperiments/estimators/__init__.py +23 -0
- skxperiments/estimators/blocked_difference_in_means.py +197 -0
- skxperiments/estimators/cuped.py +280 -0
- skxperiments/estimators/difference_in_means.py +161 -0
- skxperiments/estimators/factorial_estimator.py +213 -0
- skxperiments/estimators/lin_estimator.py +298 -0
- skxperiments/inference/__init__.py +17 -0
- skxperiments/inference/bootstrap.py +450 -0
- skxperiments/inference/multiple.py +365 -0
- skxperiments/inference/neyman.py +386 -0
- skxperiments/inference/randomization_test.py +319 -0
- skxperiments/pipeline.py +366 -0
- skxperiments/reporting/__init__.py +30 -0
- skxperiments/reporting/plots.py +411 -0
- skxperiments/reporting/summary.py +185 -0
- skxperiments-0.1.0.dev0.dist-info/METADATA +272 -0
- skxperiments-0.1.0.dev0.dist-info/RECORD +36 -0
- skxperiments-0.1.0.dev0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Bootstrap confidence intervals for superpopulation inference.
|
|
2
|
+
|
|
3
|
+
Implements ``BootstrapCI``: wraps any scalar estimator and approximates
|
|
4
|
+
the sampling distribution of its ATE by resampling units **with
|
|
5
|
+
replacement within each treatment arm** (within each block-by-arm
|
|
6
|
+
stratum for blocked designs), then forms a percentile or BCa confidence
|
|
7
|
+
interval.
|
|
8
|
+
|
|
9
|
+
Unlike ``RandomizationTest`` (which permutes the treatment assignment
|
|
10
|
+
under Fisher's sharp null) and ``NeymanCI`` (finite-population variance),
|
|
11
|
+
the bootstrap treats the observed arms as samples drawn from
|
|
12
|
+
superpopulations and ignores the randomization mechanism. It is therefore
|
|
13
|
+
the library's explicit superpopulation method.
|
|
14
|
+
|
|
15
|
+
References
|
|
16
|
+
----------
|
|
17
|
+
Efron, B. (1987). Better bootstrap confidence intervals. Journal of the
|
|
18
|
+
American Statistical Association, 82(397), 171-185.
|
|
19
|
+
Efron, B., & Tibshirani, R. J. (1993). An Introduction to the Bootstrap.
|
|
20
|
+
Chapman & Hall.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
from scipy.stats import norm
|
|
25
|
+
|
|
26
|
+
from skxperiments.core.assignment import (
|
|
27
|
+
BlockedAssignment,
|
|
28
|
+
CRDAssignment,
|
|
29
|
+
)
|
|
30
|
+
from skxperiments.core.base import BaseEstimator, BaseInference
|
|
31
|
+
from skxperiments.core.exceptions import (
|
|
32
|
+
InsufficientDataError,
|
|
33
|
+
InvalidDesignError,
|
|
34
|
+
)
|
|
35
|
+
from skxperiments.core.results import Results
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BootstrapCI(BaseInference):
|
|
39
|
+
"""Bootstrap confidence intervals for superpopulation inference.
|
|
40
|
+
|
|
41
|
+
Wraps a scalar estimator and approximates the sampling distribution of
|
|
42
|
+
its ATE by resampling units with replacement, preserving the size of
|
|
43
|
+
each treatment arm (and, for blocked designs, of each block-by-arm
|
|
44
|
+
stratum). Two interval methods are supported:
|
|
45
|
+
|
|
46
|
+
- ``"percentile"``: the ``alpha/2`` and ``1 - alpha/2`` empirical
|
|
47
|
+
quantiles of the bootstrap distribution.
|
|
48
|
+
- ``"bca"`` (default): bias-corrected and accelerated interval
|
|
49
|
+
(Efron 1987), correcting for median bias (``z0``) and for a
|
|
50
|
+
non-constant standard error (acceleration ``a``, estimated by the
|
|
51
|
+
leave-one-out jackknife).
|
|
52
|
+
|
|
53
|
+
The estimator is treated as a black box: each resample is turned back
|
|
54
|
+
into an ``Assignment`` of the original type and the estimator is
|
|
55
|
+
refitted on it. Any estimator producing a scalar ``Results.ate`` is
|
|
56
|
+
therefore supported (``DifferenceInMeans``, ``BlockedDifferenceInMeans``,
|
|
57
|
+
``LinEstimator``, ``CUPED``).
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
estimator : BaseEstimator
|
|
62
|
+
Causal estimator producing a scalar ATE (``Results.ate``). Need
|
|
63
|
+
not be pre-fitted: ``fit`` refits it on the supplied assignment
|
|
64
|
+
and on every resample. Estimators producing multi-effect
|
|
65
|
+
``Results`` (``Results.ate is None``) are rejected.
|
|
66
|
+
method : {"bca", "percentile"}, optional
|
|
67
|
+
Interval method, by default ``"bca"``.
|
|
68
|
+
n_resamples : int, optional
|
|
69
|
+
Number of bootstrap resamples, by default 10_000. Must be a
|
|
70
|
+
positive integer.
|
|
71
|
+
alpha : float, optional
|
|
72
|
+
Significance level; the interval is two-sided at level
|
|
73
|
+
``1 - alpha``, by default 0.05.
|
|
74
|
+
seed : int or None, optional
|
|
75
|
+
Random seed for reproducibility. The same ``seed`` produces the
|
|
76
|
+
same ``bootstrap_distribution_`` and interval. By default None.
|
|
77
|
+
|
|
78
|
+
Attributes
|
|
79
|
+
----------
|
|
80
|
+
assignment_ : CRDAssignment or BlockedAssignment
|
|
81
|
+
The assignment passed to ``fit``.
|
|
82
|
+
observed_statistic_ : float
|
|
83
|
+
ATE on the original assignment, captured before resampling.
|
|
84
|
+
bootstrap_distribution_ : np.ndarray
|
|
85
|
+
Array of resampled ATEs. Length equals ``n_resamples``.
|
|
86
|
+
se_ : float
|
|
87
|
+
Bootstrap standard error: ``std(bootstrap_distribution_, ddof=1)``.
|
|
88
|
+
ci_ : tuple of float
|
|
89
|
+
The ``(1 - alpha)`` confidence interval bounds.
|
|
90
|
+
p_value_ : float
|
|
91
|
+
Approximate two-sided bootstrap p-value (achieved significance
|
|
92
|
+
level) for ``H0: ATE = 0``.
|
|
93
|
+
|
|
94
|
+
Notes
|
|
95
|
+
-----
|
|
96
|
+
**Superpopulation scope.** ``BootstrapCI`` always reports
|
|
97
|
+
``inference_mode="superpopulation"`` in ``Results.extra``, regardless
|
|
98
|
+
of any ``inference_mode`` the wrapped estimator may have written: the
|
|
99
|
+
bootstrap is a superpopulation procedure by construction.
|
|
100
|
+
|
|
101
|
+
**Resampling scheme.** Resampling is stratified by arm (CRD) or by
|
|
102
|
+
block-by-arm (blocked), preserving each stratum's size and so the
|
|
103
|
+
fixed-margin structure of the design. Each block-by-arm stratum must
|
|
104
|
+
contain at least 2 units; matched-pair blocked designs (one treated
|
|
105
|
+
and one control per block) are not supported by the within-stratum
|
|
106
|
+
bootstrap in v1 and raise ``InsufficientDataError``.
|
|
107
|
+
|
|
108
|
+
**Rerandomization.** A ``CRDAssignment`` from ``ReRandomizedCRD`` is
|
|
109
|
+
accepted, but the bootstrap ignores the Mahalanobis acceptance
|
|
110
|
+
criterion (it does not re-randomize). The resulting interval may be
|
|
111
|
+
conservative; for inference that respects rerandomization use
|
|
112
|
+
``RandomizationTest``.
|
|
113
|
+
|
|
114
|
+
**BCa cost.** The acceleration estimate adds a leave-one-out jackknife
|
|
115
|
+
of ``n_obs`` estimator refits on top of the ``n_resamples`` bootstrap
|
|
116
|
+
refits. The bias-correction is undefined when no (or every) resampled
|
|
117
|
+
estimate falls below the observed estimate; in that degenerate case
|
|
118
|
+
``fit`` raises ``InvalidDesignError`` suggesting ``method="percentile"``.
|
|
119
|
+
|
|
120
|
+
Examples
|
|
121
|
+
--------
|
|
122
|
+
>>> from skxperiments.design.crd import CRD
|
|
123
|
+
>>> from skxperiments.estimators.difference_in_means import (
|
|
124
|
+
... DifferenceInMeans,
|
|
125
|
+
... )
|
|
126
|
+
>>> from skxperiments.inference.bootstrap import BootstrapCI
|
|
127
|
+
>>> design = CRD(p=0.5, seed=42)
|
|
128
|
+
>>> assignment = design.randomize(df) # doctest: +SKIP
|
|
129
|
+
>>> dim = DifferenceInMeans(outcome_col="y")
|
|
130
|
+
>>> ci = BootstrapCI(estimator=dim, method="bca", seed=0)
|
|
131
|
+
>>> result = ci.fit(assignment).estimate() # doctest: +SKIP
|
|
132
|
+
>>> result.ci # doctest: +SKIP
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
_VALID_METHODS = ("bca", "percentile")
|
|
136
|
+
|
|
137
|
+
def __init__(
|
|
138
|
+
self,
|
|
139
|
+
estimator: BaseEstimator,
|
|
140
|
+
method: str = "bca",
|
|
141
|
+
n_resamples: int = 10_000,
|
|
142
|
+
alpha: float = 0.05,
|
|
143
|
+
seed: int | None = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
if not isinstance(estimator, BaseEstimator):
|
|
146
|
+
raise InvalidDesignError(
|
|
147
|
+
f"estimator must be an instance of BaseEstimator, got "
|
|
148
|
+
f"{type(estimator).__name__}."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if method not in self._VALID_METHODS:
|
|
152
|
+
raise InvalidDesignError(
|
|
153
|
+
f"method must be one of {self._VALID_METHODS}, got "
|
|
154
|
+
f"{method!r}."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if not isinstance(n_resamples, int) or isinstance(n_resamples, bool):
|
|
158
|
+
raise InvalidDesignError(
|
|
159
|
+
f"n_resamples must be an integer, got "
|
|
160
|
+
f"{type(n_resamples).__name__}."
|
|
161
|
+
)
|
|
162
|
+
if n_resamples <= 0:
|
|
163
|
+
raise InvalidDesignError(
|
|
164
|
+
f"n_resamples must be > 0, got {n_resamples}."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if not isinstance(alpha, (int, float)) or isinstance(alpha, bool):
|
|
168
|
+
raise InvalidDesignError(
|
|
169
|
+
f"alpha must be a float in (0, 1), got "
|
|
170
|
+
f"{type(alpha).__name__}."
|
|
171
|
+
)
|
|
172
|
+
if not (0.0 < alpha < 1.0):
|
|
173
|
+
raise InvalidDesignError(
|
|
174
|
+
f"alpha must be in (0, 1), got {alpha}."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
self.estimator = estimator
|
|
178
|
+
self.method = method
|
|
179
|
+
self.n_resamples = n_resamples
|
|
180
|
+
self.alpha = alpha
|
|
181
|
+
self.seed = seed
|
|
182
|
+
|
|
183
|
+
def fit(
|
|
184
|
+
self,
|
|
185
|
+
assignment: CRDAssignment | BlockedAssignment,
|
|
186
|
+
) -> "BootstrapCI":
|
|
187
|
+
"""Run the bootstrap and compute the confidence interval.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
assignment : CRDAssignment or BlockedAssignment
|
|
192
|
+
Observed assignment. ``FactorialAssignment`` is rejected with
|
|
193
|
+
``DesignEstimatorMismatch``.
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
BootstrapCI
|
|
198
|
+
Returns self.
|
|
199
|
+
|
|
200
|
+
Raises
|
|
201
|
+
------
|
|
202
|
+
DesignEstimatorMismatch
|
|
203
|
+
If ``assignment`` is not a ``CRDAssignment`` or
|
|
204
|
+
``BlockedAssignment``.
|
|
205
|
+
InvalidDesignError
|
|
206
|
+
If the estimator returns a multi-effect ``Results``
|
|
207
|
+
(``Results.ate is None``), or if the BCa bias-correction is
|
|
208
|
+
undefined (use ``method="percentile"``).
|
|
209
|
+
InsufficientDataError
|
|
210
|
+
If any arm (CRD) or block-by-arm stratum (blocked) has fewer
|
|
211
|
+
than 2 units.
|
|
212
|
+
"""
|
|
213
|
+
self._validate_assignment_type(
|
|
214
|
+
assignment, (CRDAssignment, BlockedAssignment)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Refit on the original assignment for the observed statistic.
|
|
218
|
+
self.estimator.fit(assignment)
|
|
219
|
+
base_results = self.estimator.estimate()
|
|
220
|
+
|
|
221
|
+
if base_results.ate is None:
|
|
222
|
+
raise InvalidDesignError(
|
|
223
|
+
"BootstrapCI supports only estimators producing a scalar "
|
|
224
|
+
"ATE (Results.ate). The supplied estimator "
|
|
225
|
+
f"({type(self.estimator).__name__}) produced a multi-effect "
|
|
226
|
+
"Results (Results.effects). Multi-effect support is planned "
|
|
227
|
+
"for v2."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
observed = float(base_results.ate)
|
|
231
|
+
|
|
232
|
+
# Capture metadata BEFORE the resampling loop, so estimate() does
|
|
233
|
+
# not depend on the post-loop state of self.estimator.
|
|
234
|
+
self._n_obs = base_results.n_obs
|
|
235
|
+
self._n_treated = base_results.n_treated
|
|
236
|
+
self._n_control = base_results.n_control
|
|
237
|
+
self._estimator_name = base_results.estimator_name
|
|
238
|
+
self._design_name = base_results.design_name
|
|
239
|
+
|
|
240
|
+
strata = self._get_strata(assignment)
|
|
241
|
+
|
|
242
|
+
rng = np.random.default_rng(self.seed)
|
|
243
|
+
distribution = np.empty(self.n_resamples, dtype=float)
|
|
244
|
+
for b in range(self.n_resamples):
|
|
245
|
+
idx = np.concatenate(
|
|
246
|
+
[rng.choice(s, size=len(s), replace=True) for s in strata]
|
|
247
|
+
)
|
|
248
|
+
distribution[b] = self._compute_statistic(assignment, idx)
|
|
249
|
+
|
|
250
|
+
se = float(np.std(distribution, ddof=1))
|
|
251
|
+
|
|
252
|
+
if self.method == "bca":
|
|
253
|
+
ci_lower, ci_upper, z0, accel = self._bca_interval(
|
|
254
|
+
assignment, distribution, observed
|
|
255
|
+
)
|
|
256
|
+
self._bias_correction = z0
|
|
257
|
+
self._acceleration = accel
|
|
258
|
+
else:
|
|
259
|
+
lo, hi = np.quantile(
|
|
260
|
+
distribution, [self.alpha / 2.0, 1.0 - self.alpha / 2.0]
|
|
261
|
+
)
|
|
262
|
+
ci_lower, ci_upper = float(lo), float(hi)
|
|
263
|
+
self._bias_correction = None
|
|
264
|
+
self._acceleration = None
|
|
265
|
+
|
|
266
|
+
# Two-sided achieved significance level for H0: ATE = 0.
|
|
267
|
+
p_less = float(np.mean(distribution <= 0.0))
|
|
268
|
+
p_greater = float(np.mean(distribution >= 0.0))
|
|
269
|
+
p_value = min(1.0, 2.0 * min(p_less, p_greater))
|
|
270
|
+
|
|
271
|
+
self.assignment_: CRDAssignment | BlockedAssignment = assignment
|
|
272
|
+
self.observed_statistic_: float = observed
|
|
273
|
+
self.bootstrap_distribution_: np.ndarray = distribution
|
|
274
|
+
self.se_: float = se
|
|
275
|
+
self.ci_: tuple[float, float] = (ci_lower, ci_upper)
|
|
276
|
+
self.p_value_: float = p_value
|
|
277
|
+
|
|
278
|
+
return self
|
|
279
|
+
|
|
280
|
+
def _get_strata(
|
|
281
|
+
self,
|
|
282
|
+
assignment: CRDAssignment | BlockedAssignment,
|
|
283
|
+
) -> list[np.ndarray]:
|
|
284
|
+
"""Return iloc positions for each resampling stratum.
|
|
285
|
+
|
|
286
|
+
CRD: [treated, control]. Blocked: one stratum per block-by-arm.
|
|
287
|
+
Each stratum must contain at least 2 units.
|
|
288
|
+
"""
|
|
289
|
+
if isinstance(assignment, BlockedAssignment):
|
|
290
|
+
data = assignment.data_
|
|
291
|
+
treat = data[assignment.treatment_col_].values
|
|
292
|
+
blocks = data[assignment.block_col_].values
|
|
293
|
+
strata: list[np.ndarray] = []
|
|
294
|
+
for block_val in assignment.block_sizes_:
|
|
295
|
+
for arm in (1, 0):
|
|
296
|
+
idx = np.where((blocks == block_val) & (treat == arm))[0]
|
|
297
|
+
arm_name = "treated" if arm == 1 else "control"
|
|
298
|
+
if len(idx) < 2:
|
|
299
|
+
raise InsufficientDataError(
|
|
300
|
+
context=(
|
|
301
|
+
f"BootstrapCI resampling ({arm_name} arm in "
|
|
302
|
+
f"block '{block_val}')"
|
|
303
|
+
),
|
|
304
|
+
minimum=2,
|
|
305
|
+
received=len(idx),
|
|
306
|
+
)
|
|
307
|
+
strata.append(idx)
|
|
308
|
+
return strata
|
|
309
|
+
|
|
310
|
+
treated = assignment.treated_ids()
|
|
311
|
+
control = assignment.control_ids()
|
|
312
|
+
if len(treated) < 2:
|
|
313
|
+
raise InsufficientDataError(
|
|
314
|
+
context="BootstrapCI resampling (treated arm)",
|
|
315
|
+
minimum=2,
|
|
316
|
+
received=len(treated),
|
|
317
|
+
)
|
|
318
|
+
if len(control) < 2:
|
|
319
|
+
raise InsufficientDataError(
|
|
320
|
+
context="BootstrapCI resampling (control arm)",
|
|
321
|
+
minimum=2,
|
|
322
|
+
received=len(control),
|
|
323
|
+
)
|
|
324
|
+
return [treated, control]
|
|
325
|
+
|
|
326
|
+
def _compute_statistic(
|
|
327
|
+
self,
|
|
328
|
+
assignment: CRDAssignment | BlockedAssignment,
|
|
329
|
+
idx: np.ndarray,
|
|
330
|
+
) -> float:
|
|
331
|
+
"""Refit the estimator on the units selected by ``idx`` (iloc)."""
|
|
332
|
+
df_rs = assignment.data_.iloc[idx].reset_index(drop=True)
|
|
333
|
+
|
|
334
|
+
if isinstance(assignment, BlockedAssignment):
|
|
335
|
+
block_sizes = (
|
|
336
|
+
df_rs.groupby(assignment.block_col_).size().to_dict()
|
|
337
|
+
)
|
|
338
|
+
resampled: CRDAssignment | BlockedAssignment = BlockedAssignment(
|
|
339
|
+
data=df_rs,
|
|
340
|
+
treatment_col=assignment.treatment_col_,
|
|
341
|
+
design=None,
|
|
342
|
+
block_col=assignment.block_col_,
|
|
343
|
+
block_sizes=block_sizes,
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
resampled = CRDAssignment(
|
|
347
|
+
data=df_rs,
|
|
348
|
+
treatment_col=assignment.treatment_col_,
|
|
349
|
+
design=None,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
self.estimator.fit(resampled)
|
|
353
|
+
return float(self.estimator.estimate().ate)
|
|
354
|
+
|
|
355
|
+
def _bca_interval(
|
|
356
|
+
self,
|
|
357
|
+
assignment: CRDAssignment | BlockedAssignment,
|
|
358
|
+
distribution: np.ndarray,
|
|
359
|
+
observed: float,
|
|
360
|
+
) -> tuple[float, float, float, float]:
|
|
361
|
+
"""Compute the BCa interval bounds, bias-correction, and acceleration."""
|
|
362
|
+
# Bias-correction z0 from the fraction of resamples below observed.
|
|
363
|
+
prop_less = float(np.mean(distribution < observed))
|
|
364
|
+
if prop_less <= 0.0 or prop_less >= 1.0:
|
|
365
|
+
raise InvalidDesignError(
|
|
366
|
+
"BootstrapCI: the BCa bias-correction is undefined because "
|
|
367
|
+
"no (or every) bootstrap estimate falls below the observed "
|
|
368
|
+
"estimate. This usually means a degenerate or tiny sample. "
|
|
369
|
+
"Use method='percentile' instead."
|
|
370
|
+
)
|
|
371
|
+
z0 = float(norm.ppf(prop_less))
|
|
372
|
+
|
|
373
|
+
# Acceleration via the leave-one-out jackknife.
|
|
374
|
+
n = assignment.n_units_
|
|
375
|
+
all_idx = np.arange(n)
|
|
376
|
+
jack = np.empty(n, dtype=float)
|
|
377
|
+
for i in range(n):
|
|
378
|
+
jack[i] = self._compute_statistic(
|
|
379
|
+
assignment, np.delete(all_idx, i)
|
|
380
|
+
)
|
|
381
|
+
jack_mean = jack.mean()
|
|
382
|
+
diffs = jack_mean - jack
|
|
383
|
+
denom = 6.0 * float(np.sum(diffs**2)) ** 1.5
|
|
384
|
+
accel = 0.0 if denom == 0.0 else float(np.sum(diffs**3)) / denom
|
|
385
|
+
|
|
386
|
+
z_lo = norm.ppf(self.alpha / 2.0)
|
|
387
|
+
z_hi = norm.ppf(1.0 - self.alpha / 2.0)
|
|
388
|
+
|
|
389
|
+
def _adjust(z: float) -> float:
|
|
390
|
+
denom_z = 1.0 - accel * (z0 + z)
|
|
391
|
+
return float(norm.cdf(z0 + (z0 + z) / denom_z))
|
|
392
|
+
|
|
393
|
+
p_lo = _adjust(z_lo)
|
|
394
|
+
p_hi = _adjust(z_hi)
|
|
395
|
+
|
|
396
|
+
if not (np.isfinite(p_lo) and np.isfinite(p_hi)):
|
|
397
|
+
raise InvalidDesignError(
|
|
398
|
+
"BootstrapCI: the BCa adjustment produced non-finite "
|
|
399
|
+
"percentiles (acceleration drove the denominator to zero). "
|
|
400
|
+
"Use method='percentile' instead."
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
lo, hi = np.quantile(distribution, sorted([p_lo, p_hi]))
|
|
404
|
+
return float(lo), float(hi), z0, accel
|
|
405
|
+
|
|
406
|
+
def estimate(self) -> Results:
|
|
407
|
+
"""Return a Results object with the ATE, SE, CI, and p-value.
|
|
408
|
+
|
|
409
|
+
Returns
|
|
410
|
+
-------
|
|
411
|
+
Results
|
|
412
|
+
Results with ``ate`` (observed), ``se`` (bootstrap standard
|
|
413
|
+
error), ``ci`` (percentile or BCa bounds), ``p_value``
|
|
414
|
+
(two-sided achieved significance level), ``alpha``,
|
|
415
|
+
``inference_name="BootstrapCI"``, and ``extra`` containing
|
|
416
|
+
``method``, ``n_resamples``, ``bootstrap_distribution``,
|
|
417
|
+
``inference_mode="superpopulation"`` and, for BCa,
|
|
418
|
+
``bias_correction`` and ``acceleration``.
|
|
419
|
+
|
|
420
|
+
Raises
|
|
421
|
+
------
|
|
422
|
+
NotFittedError
|
|
423
|
+
If ``fit`` has not been called.
|
|
424
|
+
"""
|
|
425
|
+
self._check_is_fitted()
|
|
426
|
+
|
|
427
|
+
extra: dict = {
|
|
428
|
+
"method": self.method,
|
|
429
|
+
"n_resamples": self.n_resamples,
|
|
430
|
+
"bootstrap_distribution": self.bootstrap_distribution_,
|
|
431
|
+
"inference_mode": "superpopulation",
|
|
432
|
+
}
|
|
433
|
+
if self.method == "bca":
|
|
434
|
+
extra["bias_correction"] = self._bias_correction
|
|
435
|
+
extra["acceleration"] = self._acceleration
|
|
436
|
+
|
|
437
|
+
return Results(
|
|
438
|
+
ate=self.observed_statistic_,
|
|
439
|
+
se=self.se_,
|
|
440
|
+
ci=self.ci_,
|
|
441
|
+
p_value=self.p_value_,
|
|
442
|
+
alpha=self.alpha,
|
|
443
|
+
n_obs=self._n_obs,
|
|
444
|
+
n_treated=self._n_treated,
|
|
445
|
+
n_control=self._n_control,
|
|
446
|
+
estimator_name=self._estimator_name,
|
|
447
|
+
design_name=self._design_name,
|
|
448
|
+
inference_name=type(self).__name__,
|
|
449
|
+
extra=extra,
|
|
450
|
+
)
|