Skip to content

API Reference

Top-level API

Run bootstrap estimation.

Parameters

data : array-like or pandas Series/DataFrame Observed sample. statistic : callable (array) -> float. If vectorized=True, must accept (array_2d, axis=1) -> array_1d. method : str One of: bca, percentile, basic, studentized, poisson, bernoulli, bayesian, subsampling, mbb, cbb, stationary, tapered, sieve, wild, cluster, strata. ci_method : str or None CI construction for generator-based methods that do not define a specialized interval: "percentile" or "basic". Defaults to "percentile". Bayesian, Bernoulli, and subsampling intervals are not configurable through this parameter. vectorized : bool For percentile, basic, and BCa methods, call statistic as statistic(batch, axis=1). Other methods reject this option. n_jobs : int Parallelism for jackknife in BCa (effective only for n >= 2000).

Other Parameters

weighted_statistic : callable Required for custom Bayesian-bootstrap statistics. Called as weighted_statistic(data, weights) for each Dirichlet draw. subsample_size : int Number of observations in each subsample. rate : float Convergence-rate exponent for subsampling. 0.5 means root-n. prob : float Inclusion probability for Bernoulli subsampling; strictly between 0 and 1. n_inner : int Number of inner resamples per outer sample for the studentized method. Defaults to 100.

Source code in src/bootstrapx/api.py
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
def bootstrap(
    data: Any,
    statistic: Callable[..., float],
    *,
    method: str = "bca",
    n_resamples: int = 9999,
    batch_size: int | None = None,
    confidence_level: float = 0.95,
    ci_method: str | None = None,
    backend: str = "auto",
    random_state: int | np.random.Generator | None = None,
    vectorized: bool = False,
    n_jobs: int = 1,
    **kwargs: Any,
) -> BootstrapResult:
    """Run bootstrap estimation.

    Parameters
    ----------
    data : array-like or pandas Series/DataFrame
        Observed sample.
    statistic : callable
        ``(array) -> float``.  If ``vectorized=True``, must accept
        ``(array_2d, axis=1) -> array_1d``.
    method : str
        One of: bca, percentile, basic, studentized, poisson, bernoulli,
        bayesian, subsampling, mbb, cbb, stationary, tapered, sieve,
        wild, cluster, strata.
    ci_method : str or None
        CI construction for generator-based methods that do not define a
        specialized interval: ``"percentile"`` or ``"basic"``. Defaults to
        ``"percentile"``. Bayesian, Bernoulli, and subsampling intervals are
        not configurable through this parameter.
    vectorized : bool
        For percentile, basic, and BCa methods, call ``statistic`` as
        ``statistic(batch, axis=1)``. Other methods reject this option.
    n_jobs : int
        Parallelism for jackknife in BCa (effective only for n >= 2000).

    Other Parameters
    ----------------
    weighted_statistic : callable
        Required for custom Bayesian-bootstrap statistics. Called as
        ``weighted_statistic(data, weights)`` for each Dirichlet draw.
    subsample_size : int
        Number of observations in each subsample.
    rate : float
        Convergence-rate exponent for subsampling. ``0.5`` means root-n.
    prob : float
        Inclusion probability for Bernoulli subsampling; strictly between 0 and 1.
    n_inner : int
        Number of inner resamples per outer sample for the studentized method.
        Defaults to 100.
    """
    if not isinstance(method, str):
        raise TypeError("method must be a string.")
    if ci_method is not None and not isinstance(ci_method, str):
        raise TypeError("ci_method must be a string or None.")
    if not isinstance(vectorized, bool):
        raise TypeError("vectorized must be a boolean.")
    if not isinstance(backend, str):
        raise TypeError("backend must be a string.")
    if not callable(statistic):
        raise TypeError("statistic must be callable.")

    method = method.lower().strip()
    ci_method = ci_method.lower().strip() if ci_method is not None else None
    if method not in _ALL_METHODS:
        raise ValueError(f"Unknown method {method!r}. Choose from {sorted(_ALL_METHODS)}.")
    if vectorized and method not in {"percentile", "basic", "bca"}:
        raise ValueError(
            "vectorized=True is supported only for percentile, basic, and bca methods."
        )

    arr = validate_data(data, allow_2d=(method in _HIER_METHODS))
    n = arr.shape[0]
    validate_bootstrap_params(
        method=method,
        n_observations=n,
        n_resamples=n_resamples,
        batch_size=batch_size,
        confidence_level=confidence_level,
        ci_method=ci_method,
        n_jobs=n_jobs,
        kwargs=kwargs,
    )
    validate_random_state(random_state)

    rng: np.random.Generator = (
        random_state
        if isinstance(random_state, np.random.Generator)
        else np.random.default_rng(random_state)
    )

    if batch_size is None:
        batch_size = auto_batch_size(n, n_resamples)

    backend_kind = resolve_backend(backend)
    theta_hat = float(statistic(arr))
    if not np.isfinite(theta_hat):
        raise ValueError("statistic must return a finite scalar value for the observed data.")

    result_extra: dict[str, Any] = {}
    result_standard_error: float | None = None
    boot_stats: FloatArray

    if method == "studentized":
        n_inner = int(kwargs.get("n_inner", 100))
        boot_stats = np.empty(n_resamples, dtype=np.float64)
        boot_se = np.empty(n_resamples, dtype=np.float64)
        for i in range(n_resamples):
            outer_idx = rng.integers(0, n, size=n)
            sample = arr[outer_idx]
            boot_stats[i] = float(statistic(sample))
            inner_idx = rng.integers(0, n, size=(n_inner, n))
            inner_vals = np.array([float(statistic(sample[inner_idx[k]])) for k in range(n_inner)])
            boot_se[i] = float(np.std(inner_vals, ddof=1))
        boot_stats = validate_bootstrap_distribution(boot_stats, n_resamples)
        if not np.all(np.isfinite(boot_se)):
            raise ValueError("statistic returned NaN or inf during studentized bootstrap.")
        ci = studentized_interval(
            arr,
            statistic,
            theta_hat,
            boot_stats,
            boot_se,
            confidence_level,
        )
        result_extra["n_inner"] = n_inner

    elif method in _CI_CAPABLE:
        boot_stats = apply_statistic_batched(
            arr,
            statistic,
            batch_size,
            n_resamples,
            backend_kind,
            rng,
            vectorized=vectorized,
        )
        boot_stats = validate_bootstrap_distribution(boot_stats, n_resamples)

        if method == "percentile":
            ci = percentile_interval(boot_stats, confidence_level)

        elif method == "basic":
            ci = basic_interval(boot_stats, theta_hat, confidence_level)

        elif method == "bca":
            ci = bca_interval(
                boot_stats,
                arr,
                statistic,
                theta_hat,
                confidence_level,
                n_jobs=n_jobs,
            )

    else:
        if method == "bayesian":
            weighted_statistic = _resolve_weighted_statistic(
                statistic, kwargs.get("weighted_statistic")
            )
            boot_stats_list = _collect_bayesian(
                bayesian_resample(arr, n_resamples, batch_size, rng), weighted_statistic
            )

        elif method == "poisson":
            boot_stats_list = _collect_weighted(
                poisson_resample(arr, n_resamples, batch_size, rng), statistic, arr
            )

        elif method == "bernoulli":
            prob = float(kwargs.get("prob", 0.5))
            boot_stats, subset_sizes = _collect_bernoulli(
                bernoulli_resample(arr, n_resamples, batch_size, rng, prob=prob), statistic
            )

        elif method == "cluster":
            cids = kwargs["cluster_ids"]
            boot_stats_list = _collect_arrays(
                cluster_resample(arr, np.asarray(cids), n_resamples, batch_size, rng), statistic
            )

        elif method == "strata":
            sids = kwargs["strata_ids"]
            boot_stats_list = _collect_arrays(
                strata_resample(arr, np.asarray(sids), n_resamples, batch_size, rng), statistic
            )

        elif method == "subsampling":
            ss = kwargs.get("subsample_size")
            boot_stats_list = _collect_arrays(
                subsampling_resample(arr, n_resamples, batch_size, rng, subsample_size=ss),
                statistic,
            )

        elif method == "mbb":
            bl = int(kwargs.get("block_length", 10))
            boot_stats_list = _collect_arrays(
                mbb_resample(arr, n_resamples, batch_size, rng, block_length=bl), statistic
            )

        elif method == "cbb":
            bl = int(kwargs.get("block_length", 10))
            boot_stats_list = _collect_arrays(
                cbb_resample(arr, n_resamples, batch_size, rng, block_length=bl), statistic
            )

        elif method == "stationary":
            mb = float(kwargs.get("mean_block", 10.0))
            boot_stats_list = _collect_arrays(
                stationary_resample(arr, n_resamples, batch_size, rng, mean_block=mb), statistic
            )

        elif method == "tapered":
            bl = int(kwargs.get("block_length", 10))
            tp = str(kwargs.get("taper", "tukey"))
            boot_stats_list = _collect_arrays(
                tapered_block_resample(
                    arr,
                    n_resamples,
                    batch_size,
                    rng,
                    block_length=bl,
                    taper=tp,
                ),
                statistic,
            )

        elif method == "sieve":
            ar = kwargs.get("ar_order")
            boot_stats_list = _collect_arrays(
                sieve_resample(arr, n_resamples, batch_size, rng, ar_order=ar), statistic
            )

        elif method == "wild":
            fit = kwargs.get("fitted")
            dist = str(kwargs.get("distribution", "rademacher"))
            boot_stats_list = _collect_arrays(
                wild_resample(arr, n_resamples, batch_size, rng, fitted=fit, distribution=dist),
                statistic,
            )

        else:
            raise ValueError(f"Method {method!r} not implemented.")

        if method != "bernoulli":
            boot_stats = np.array(boot_stats_list, dtype=np.float64)

        boot_stats = validate_bootstrap_distribution(boot_stats, n_resamples)

        if method == "bayesian":
            ci = percentile_interval(boot_stats, confidence_level)
            ci.method = "bayesian"
            result_extra["interval_type"] = "credible"
        elif method == "subsampling":
            subsample_size = int(kwargs.get("subsample_size") or max(1, np.sqrt(n)))
            rate = float(kwargs.get("rate", 0.5))
            scale_subsample = float(subsample_size**rate)
            scale_n = float(n**rate)
            root_stats = scale_subsample * (boot_stats - theta_hat)
            ci = root_interval(
                root_stats,
                theta_hat,
                scale_n,
                confidence_level,
                method="subsampling",
            )
            result_standard_error = float(np.std(root_stats, ddof=1) / scale_n)
            result_extra.update(
                {"subsample_size": subsample_size, "rate": rate, "root_distribution": root_stats}
            )
        elif method == "bernoulli":
            fractions = subset_sizes / n
            root_stats = np.sqrt(subset_sizes / (1.0 - fractions)) * (boot_stats - theta_hat)
            ci = root_interval(
                root_stats,
                theta_hat,
                np.sqrt(n),
                confidence_level,
                method="bernoulli",
            )
            result_standard_error = float(np.std(root_stats, ddof=1) / np.sqrt(n))
            result_extra.update(
                {"prob": prob, "subset_sizes": subset_sizes, "root_distribution": root_stats}
            )
        else:
            _ci_method = ci_method or "percentile"
            if _ci_method == "basic":
                ci = basic_interval(boot_stats, theta_hat, confidence_level)
            else:
                ci = percentile_interval(boot_stats, confidence_level)

    return BootstrapResult(
        confidence_interval=ci,
        bootstrap_distribution=boot_stats,
        theta_hat=theta_hat,
        standard_error=(
            result_standard_error
            if result_standard_error is not None
            else float(np.std(boot_stats, ddof=1))
        ),
        n_resamples=len(boot_stats),
        method=method,
        extra=result_extra,
    )

Container for bootstrap estimation results.

Source code in src/bootstrapx/api.py
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
@dataclass
class BootstrapResult:
    """Container for bootstrap estimation results."""

    confidence_interval: ConfidenceInterval
    bootstrap_distribution: FloatArray
    theta_hat: float
    standard_error: float
    n_resamples: int
    method: str
    extra: dict[str, Any] = field(default_factory=dict)

    def __repr__(self) -> str:
        ci = self.confidence_interval
        return (
            f"BootstrapResult(method={self.method!r}, "
            f"theta_hat={self.theta_hat:.6g}, "
            f"se={self.standard_error:.6g}, "
            f"CI=[{ci.low:.6g}, {ci.high:.6g}])"
        )

    def to_dict(self, *, include_distribution: bool = False) -> dict[str, Any]:
        """Return a compact summary mapping of the bootstrap result.

        The potentially large bootstrap distribution is excluded by default.
        Set ``include_distribution=True`` when it is needed for serialization
        or downstream analysis. Arrays and ``extra`` metadata are copied so
        callers cannot mutate the result through the returned dictionary.
        """
        summary: dict[str, Any] = {
            "theta_hat": self.theta_hat,
            "standard_error": self.standard_error,
            "ci_low": self.confidence_interval.low,
            "ci_high": self.confidence_interval.high,
            "ci_method": self.confidence_interval.method,
            "method": self.method,
            "n_resamples": self.n_resamples,
            "extra": deepcopy(self.extra),
        }
        if include_distribution:
            summary["bootstrap_distribution"] = self.bootstrap_distribution.copy()
        return summary

    def to_frame(self) -> Any:
        """Return a one-row pandas DataFrame with the result summary.

        The bootstrap distribution is intentionally omitted to keep the frame
        compact. Use :meth:`to_dict` with ``include_distribution=True`` when
        the complete distribution is required.
        """
        try:
            import pandas as pd
        except ImportError as exc:
            raise ImportError(
                "pandas is required for BootstrapResult.to_frame(). "
                "Install with: pip install 'bootstrapx-lib[pandas]'"
            ) from exc
        return pd.DataFrame([self.to_dict()])

to_dict(*, include_distribution=False)

Return a compact summary mapping of the bootstrap result.

The potentially large bootstrap distribution is excluded by default. Set include_distribution=True when it is needed for serialization or downstream analysis. Arrays and extra metadata are copied so callers cannot mutate the result through the returned dictionary.

Source code in src/bootstrapx/api.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def to_dict(self, *, include_distribution: bool = False) -> dict[str, Any]:
    """Return a compact summary mapping of the bootstrap result.

    The potentially large bootstrap distribution is excluded by default.
    Set ``include_distribution=True`` when it is needed for serialization
    or downstream analysis. Arrays and ``extra`` metadata are copied so
    callers cannot mutate the result through the returned dictionary.
    """
    summary: dict[str, Any] = {
        "theta_hat": self.theta_hat,
        "standard_error": self.standard_error,
        "ci_low": self.confidence_interval.low,
        "ci_high": self.confidence_interval.high,
        "ci_method": self.confidence_interval.method,
        "method": self.method,
        "n_resamples": self.n_resamples,
        "extra": deepcopy(self.extra),
    }
    if include_distribution:
        summary["bootstrap_distribution"] = self.bootstrap_distribution.copy()
    return summary

to_frame()

Return a one-row pandas DataFrame with the result summary.

The bootstrap distribution is intentionally omitted to keep the frame compact. Use :meth:to_dict with include_distribution=True when the complete distribution is required.

Source code in src/bootstrapx/api.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def to_frame(self) -> Any:
    """Return a one-row pandas DataFrame with the result summary.

    The bootstrap distribution is intentionally omitted to keep the frame
    compact. Use :meth:`to_dict` with ``include_distribution=True`` when
    the complete distribution is required.
    """
    try:
        import pandas as pd
    except ImportError as exc:
        raise ImportError(
            "pandas is required for BootstrapResult.to_frame(). "
            "Install with: pip install 'bootstrapx-lib[pandas]'"
        ) from exc
    return pd.DataFrame([self.to_dict()])

Experiment comparisons

Bootstrap an effect between control and treatment samples.

The observed effect and every resampled effect are calculated as effect(statistic(control), statistic(treatment)). Independent samples are resampled separately. With paired=True, both samples use the same resampled indices. Supplying cluster IDs resamples complete clusters within each arm and is mutually exclusive with paired analysis.

Parameters

control, treatment : array-like Finite one-dimensional samples, or numeric matrices with allow_2d=True. Matrix rows are observations and columns are jointly observed features, always resampled together. Their order defines the direction of every built-in effect. DataFrames are converted to NumPy; matrix DataFrames must have unique column labels; two DataFrames must have identical labels and order. statistic : callable Scalar function applied separately to each arm, array -> float. For matrix input, receives a 2-D array and must still return one scalar. effect : {"difference", "ratio", "relative_lift"} or callable Transformation of the two arm statistics. A callable receives (control_statistic, treatment_statistic) and returns one scalar. Difference is treatment - control; ratio is treatment / control; relative lift is (treatment - control) / control. method : {"percentile", "basic", "bca"} Confidence-interval construction. BCa is not automatically more accurate in finite samples. Ratio metrics with skewed data, correlated numerator/denominator components, or few clusters can materially undercover; compare methods against domain-relevant simulations and treat results with few independent units cautiously. paired : bool Resample corresponding rows together. The samples must have equal length and cluster IDs cannot be supplied. Pairing is positional: pandas indices are not used to align samples. allow_2d : bool Explicitly enable multicolumn input for composite scalar metrics. Defaults to False, preserving the one-dimensional input contract. Both arms must have the same dimensionality and feature count. control_cluster_ids, treatment_cluster_ids : array-like or None One cluster identifier per row. Both arrays are required for clustered analysis; complete clusters are resampled independently within each experiment arm. Labels must be scalar, non-missing, finite when numeric, and mutually comparable within each arm. Mixed string/numeric labels are rejected rather than coerced into one type. control_unit_ids, treatment_unit_ids : array-like or None Optional globally consistent identifiers for one-row-per-unit input. Both are required together and must be unique within each arm. Independent arms must be disjoint; paired arms must match in row order. Cannot be combined with cluster IDs. No IDs are stored in the result. Omitting IDs leaves correspondence/assignment validation to the caller. metric_name, effect_unit : str or None Optional non-empty reporting labels, for example "revenue/order" and "USD/order" for a difference. Labels do not transform values or verify the metric definition. Ratios/lifts are dimensionless. n_resamples : int Number of bootstrap effects. batch_size : int or None Number of resamples processed per technical batch. Changing it does not change a seeded bootstrap distribution. confidence_level : float Requested interval level strictly between zero and one. random_state : int, numpy.random.Generator, or None Reproducible random-state source.

Returns

TwoSampleBootstrapResult Arm estimates, observed effect, interval, standard error, bootstrap distribution, and experiment-design metadata.

Notes

Ratio and relative-lift effects are rejected if the observed or any resampled control statistic is zero. Near-zero denominators are allowed because any fixed tolerance would depend on measurement units, but they can produce unstable intervals. No invalid replicates are silently discarded.

Source code in src/bootstrapx/comparison.py
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
def bootstrap_two_sample(
    control: Any,
    treatment: Any,
    statistic: Statistic,
    *,
    effect: str | Effect = "difference",
    method: str = "bca",
    paired: bool = False,
    allow_2d: bool = False,
    control_cluster_ids: Any | None = None,
    treatment_cluster_ids: Any | None = None,
    control_unit_ids: Any | None = None,
    treatment_unit_ids: Any | None = None,
    metric_name: str | None = None,
    effect_unit: str | None = None,
    n_resamples: int = 9999,
    batch_size: int | None = None,
    confidence_level: float = 0.95,
    random_state: int | np.random.Generator | None = None,
) -> TwoSampleBootstrapResult:
    """Bootstrap an effect between control and treatment samples.

    The observed effect and every resampled effect are calculated as
    ``effect(statistic(control), statistic(treatment))``. Independent samples
    are resampled separately. With ``paired=True``, both samples use the same
    resampled indices. Supplying cluster IDs resamples complete clusters within
    each arm and is mutually exclusive with paired analysis.

    Parameters
    ----------
    control, treatment : array-like
        Finite one-dimensional samples, or numeric matrices with
        ``allow_2d=True``. Matrix rows are observations and columns are jointly
        observed features, always resampled together. Their order defines the
        direction of every built-in effect. DataFrames are converted to NumPy;
        matrix DataFrames must have unique column labels; two DataFrames must
        have identical labels and order.
    statistic : callable
        Scalar function applied separately to each arm, ``array -> float``.
        For matrix input, receives a 2-D array and must still return one scalar.
    effect : {"difference", "ratio", "relative_lift"} or callable
        Transformation of the two arm statistics. A callable receives
        ``(control_statistic, treatment_statistic)`` and returns one scalar.
        Difference is ``treatment - control``; ratio is
        ``treatment / control``; relative lift is
        ``(treatment - control) / control``.
    method : {"percentile", "basic", "bca"}
        Confidence-interval construction. BCa is not automatically more
        accurate in finite samples. Ratio metrics with skewed data, correlated
        numerator/denominator components, or few clusters can materially
        undercover; compare methods against domain-relevant simulations and
        treat results with few independent units cautiously.
    paired : bool
        Resample corresponding rows together. The samples must have equal
        length and cluster IDs cannot be supplied.
        Pairing is positional: pandas indices are not used to align samples.
    allow_2d : bool
        Explicitly enable multicolumn input for composite scalar metrics.
        Defaults to False, preserving the one-dimensional input contract.
        Both arms must have the same dimensionality and feature count.
    control_cluster_ids, treatment_cluster_ids : array-like or None
        One cluster identifier per row. Both arrays are required for clustered
        analysis; complete clusters are resampled independently within each
        experiment arm. Labels must be scalar, non-missing, finite when numeric,
        and mutually comparable within each arm. Mixed string/numeric labels
        are rejected rather than coerced into one type.
    control_unit_ids, treatment_unit_ids : array-like or None
        Optional globally consistent identifiers for one-row-per-unit input.
        Both are required together and must be unique within each arm.
        Independent arms must be disjoint; paired arms must match in row order.
        Cannot be combined with cluster IDs. No IDs are stored in the result.
        Omitting IDs leaves correspondence/assignment validation to the caller.
    metric_name, effect_unit : str or None
        Optional non-empty reporting labels, for example ``"revenue/order"``
        and ``"USD/order"`` for a difference. Labels do not transform values or
        verify the metric definition. Ratios/lifts are dimensionless.
    n_resamples : int
        Number of bootstrap effects.
    batch_size : int or None
        Number of resamples processed per technical batch. Changing it does
        not change a seeded bootstrap distribution.
    confidence_level : float
        Requested interval level strictly between zero and one.
    random_state : int, numpy.random.Generator, or None
        Reproducible random-state source.

    Returns
    -------
    TwoSampleBootstrapResult
        Arm estimates, observed effect, interval, standard error, bootstrap
        distribution, and experiment-design metadata.

    Notes
    -----
    Ratio and relative-lift effects are rejected if the observed or any
    resampled control statistic is zero. Near-zero denominators are allowed
    because any fixed tolerance would depend on measurement units, but they
    can produce unstable intervals. No invalid replicates are silently
    discarded.
    """
    if not callable(statistic):
        raise TypeError("statistic must be callable.")
    if not isinstance(method, str):
        raise TypeError("method must be a string.")
    if not isinstance(paired, bool):
        raise TypeError("paired must be a boolean.")
    if not isinstance(allow_2d, bool):
        raise TypeError("allow_2d must be a boolean.")
    for name, label in (("metric_name", metric_name), ("effect_unit", effect_unit)):
        if label is not None:
            if not isinstance(label, str):
                raise TypeError(f"{name} must be a string or None.")
            if not label.strip():
                raise ValueError(f"{name} must be non-empty when provided.")

    method = method.lower().strip()
    if method not in _METHODS:
        raise ValueError(f"Unknown method {method!r}. Choose from {sorted(_METHODS)}.")
    effect_function, effect_name = _resolve_effect(effect)
    control_array, treatment_array = _validate_comparison_samples(
        control, treatment, allow_2d=allow_2d
    )

    cluster_mode = control_cluster_ids is not None or treatment_cluster_ids is not None
    if cluster_mode and (control_cluster_ids is None or treatment_cluster_ids is None):
        raise ValueError("control_cluster_ids and treatment_cluster_ids must be provided together.")
    if paired and cluster_mode:
        raise ValueError("paired=True cannot be combined with cluster IDs.")
    if paired and len(control_array) != len(treatment_array):
        raise ValueError("paired samples must contain the same number of observations.")
    unit_ids_validated = _validate_design_unit_ids(
        control_unit_ids,
        treatment_unit_ids,
        n_control=len(control_array),
        n_treatment=len(treatment_array),
        paired=paired,
        cluster_mode=cluster_mode,
    )

    control_ids: AnyArray | None = None
    treatment_ids: AnyArray | None = None
    if cluster_mode:
        control_ids = _validate_cluster_ids(
            control_cluster_ids, len(control_array), name="control_cluster_ids"
        )
        treatment_ids = _validate_cluster_ids(
            treatment_cluster_ids, len(treatment_array), name="treatment_cluster_ids"
        )

    n_units_control = len(np.unique(control_ids)) if control_ids is not None else len(control_array)
    n_units_treatment = (
        len(np.unique(treatment_ids)) if treatment_ids is not None else len(treatment_array)
    )
    if method == "bca" and min(n_units_control, n_units_treatment) < 3:
        unit_name = "clusters" if cluster_mode else "observations"
        raise ValueError(f"BCa requires at least three {unit_name} in each sample.")

    validate_bootstrap_params(
        method=method,
        n_observations=min(len(control_array), len(treatment_array)),
        n_resamples=n_resamples,
        batch_size=batch_size,
        confidence_level=confidence_level,
        ci_method=None,
        n_jobs=1,
        kwargs={},
    )
    validate_random_state(random_state)
    if batch_size is None:
        batch_size = auto_batch_size(control_array.size + treatment_array.size, n_resamples)

    control_estimate = _evaluate_statistic(statistic, control_array)
    treatment_estimate = _evaluate_statistic(statistic, treatment_array)
    estimate = _evaluate_effect(effect_function, control_estimate, treatment_estimate)

    if cluster_mode:
        assert control_ids is not None and treatment_ids is not None
        distribution = _cluster_distribution(
            control_array,
            treatment_array,
            control_ids,
            treatment_ids,
            statistic,
            effect_function,
            n_resamples,
            batch_size,
            random_state,
        )
    else:
        distribution = _iid_distribution(
            control_array,
            treatment_array,
            statistic,
            effect_function,
            n_resamples,
            batch_size,
            random_state,
            paired=paired,
        )
    distribution = validate_bootstrap_distribution(distribution, n_resamples)

    if method == "percentile":
        interval = percentile_interval(distribution, confidence_level)
    elif method == "basic":
        interval = basic_interval(distribution, estimate, confidence_level)
    else:
        jackknife_groups = _loo_effects(
            control_array,
            treatment_array,
            statistic,
            effect_function,
            paired=paired,
            control_cluster_ids=control_ids,
            treatment_cluster_ids=treatment_ids,
        )
        interval = bca_interval_from_jackknife(
            distribution,
            estimate,
            jackknife_groups,
            confidence_level,
        )

    from bootstrapx import __version__

    metadata = {
        "metric_name": metric_name or getattr(statistic, "__name__", type(statistic).__name__),
        "effect_unit": effect_unit,
        "resampling_unit": "cluster" if cluster_mode else "pair" if paired else "row",
        "n_control_units": n_units_control,
        "n_treatment_units": n_units_treatment,
        "n_features": control_array.shape[1] if control_array.ndim == 2 else 1,
        "unit_ids_validated": unit_ids_validated,
        "confidence_level": float(confidence_level),
        "batch_size": int(batch_size),
        "seed": int(random_state) if isinstance(random_state, int | np.integer) else None,
        "random_state_kind": "generator"
        if isinstance(random_state, np.random.Generator)
        else "seed"
        if random_state is not None
        else "unseeded",
        "package_version": __version__,
    }
    return TwoSampleBootstrapResult(
        confidence_interval=interval,
        bootstrap_distribution=distribution,
        estimate=estimate,
        control_estimate=control_estimate,
        treatment_estimate=treatment_estimate,
        standard_error=float(np.std(distribution, ddof=1)),
        n_resamples=n_resamples,
        method=method,
        effect=effect_name,
        paired=paired,
        resampling="cluster" if cluster_mode else "iid",
        n_control=len(control_array),
        n_treatment=len(treatment_array),
        n_control_clusters=n_units_control if cluster_mode else None,
        n_treatment_clusters=n_units_treatment if cluster_mode else None,
        metadata=metadata,
    )

Result of a control/treatment bootstrap comparison.

Source code in src/bootstrapx/comparison.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
@dataclass
class TwoSampleBootstrapResult:
    """Result of a control/treatment bootstrap comparison."""

    confidence_interval: ConfidenceInterval
    bootstrap_distribution: FloatArray
    estimate: float
    control_estimate: float
    treatment_estimate: float
    standard_error: float
    n_resamples: int
    method: str
    effect: str
    paired: bool
    resampling: str
    n_control: int
    n_treatment: int
    n_control_clusters: int | None = None
    n_treatment_clusters: int | None = None
    extra: dict[str, Any] = field(default_factory=dict)
    metadata: dict[str, Any] = field(default_factory=dict)

    @property
    def theta_hat(self) -> float:
        """Alias for the observed effect estimate."""
        return self.estimate

    def __repr__(self) -> str:
        ci = self.confidence_interval
        return (
            f"TwoSampleBootstrapResult(effect={self.effect!r}, "
            f"estimate={self.estimate:.6g}, method={self.method!r}, "
            f"CI=[{ci.low:.6g}, {ci.high:.6g}])"
        )

    def to_dict(self, *, include_distribution: bool = False) -> dict[str, Any]:
        """Return a compact, mutation-safe comparison summary."""
        summary: dict[str, Any] = {
            "estimate": self.estimate,
            "control_estimate": self.control_estimate,
            "treatment_estimate": self.treatment_estimate,
            "standard_error": self.standard_error,
            "ci_low": self.confidence_interval.low,
            "ci_high": self.confidence_interval.high,
            "ci_method": self.confidence_interval.method,
            "method": self.method,
            "effect": self.effect,
            "paired": self.paired,
            "resampling": self.resampling,
            "n_control": self.n_control,
            "n_treatment": self.n_treatment,
            "n_control_clusters": self.n_control_clusters,
            "n_treatment_clusters": self.n_treatment_clusters,
            "n_resamples": self.n_resamples,
            "extra": deepcopy(self.extra),
            "metadata": deepcopy(self.metadata),
        }
        if include_distribution:
            summary["bootstrap_distribution"] = self.bootstrap_distribution.copy()
        return summary

    def to_frame(self) -> Any:
        """Return a one-row pandas DataFrame without the full distribution."""
        try:
            import pandas as pd
        except ImportError as exc:
            raise ImportError(
                "pandas is required for TwoSampleBootstrapResult.to_frame(). "
                "Install with: pip install 'bootstrapx-lib[pandas]'"
            ) from exc
        return pd.DataFrame([self.to_dict()])

theta_hat property

Alias for the observed effect estimate.

to_dict(*, include_distribution=False)

Return a compact, mutation-safe comparison summary.

Source code in src/bootstrapx/comparison.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def to_dict(self, *, include_distribution: bool = False) -> dict[str, Any]:
    """Return a compact, mutation-safe comparison summary."""
    summary: dict[str, Any] = {
        "estimate": self.estimate,
        "control_estimate": self.control_estimate,
        "treatment_estimate": self.treatment_estimate,
        "standard_error": self.standard_error,
        "ci_low": self.confidence_interval.low,
        "ci_high": self.confidence_interval.high,
        "ci_method": self.confidence_interval.method,
        "method": self.method,
        "effect": self.effect,
        "paired": self.paired,
        "resampling": self.resampling,
        "n_control": self.n_control,
        "n_treatment": self.n_treatment,
        "n_control_clusters": self.n_control_clusters,
        "n_treatment_clusters": self.n_treatment_clusters,
        "n_resamples": self.n_resamples,
        "extra": deepcopy(self.extra),
        "metadata": deepcopy(self.metadata),
    }
    if include_distribution:
        summary["bootstrap_distribution"] = self.bootstrap_distribution.copy()
    return summary

to_frame()

Return a one-row pandas DataFrame without the full distribution.

Source code in src/bootstrapx/comparison.py
 97
 98
 99
100
101
102
103
104
105
106
def to_frame(self) -> Any:
    """Return a one-row pandas DataFrame without the full distribution."""
    try:
        import pandas as pd
    except ImportError as exc:
        raise ImportError(
            "pandas is required for TwoSampleBootstrapResult.to_frame(). "
            "Install with: pip install 'bootstrapx-lib[pandas]'"
        ) from exc
    return pd.DataFrame([self.to_dict()])

Composite metric helper

Added in 0.6.0. See Composite metrics.

Compute sum(sample[:, numerator]) / sum(sample[:, denominator]).

Column positions are non-negative integers. This is not the mean of row-wise ratios, and it does not compare experiment arms: use it as the statistic in bootstrap_two_sample(..., allow_2d=True). The callable receives a numeric NumPy matrix, including when the original input is a DataFrame. A zero total denominator or non-finite value raises an error; no observations or replicates are discarded or stabilized.

Source code in src/bootstrapx/stats/metrics.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@dataclass(frozen=True)
class RatioOfSums:
    """Compute ``sum(sample[:, numerator]) / sum(sample[:, denominator])``.

    Column positions are non-negative integers. This is not the mean of
    row-wise ratios, and it does not compare experiment arms: use it as the
    ``statistic`` in ``bootstrap_two_sample(..., allow_2d=True)``. The callable
    receives a numeric NumPy matrix, including when the original input is a
    DataFrame. A zero total denominator or non-finite value raises an error;
    no observations or replicates are discarded or stabilized.
    """

    numerator: int = 0
    denominator: int = 1

    def __post_init__(self) -> None:
        for name in ("numerator", "denominator"):
            index = getattr(self, name)
            if isinstance(index, bool) or not isinstance(index, int | np.integer):
                raise TypeError(f"{name} must be an integer column position.")
            if index < 0:
                raise ValueError(f"{name} must be a non-negative column position.")

    def __call__(self, sample: NDArray[np.float64]) -> float:
        values = np.asarray(sample, dtype=np.float64)
        if values.ndim != 2:
            raise ValueError("RatioOfSums requires a two-dimensional numeric sample.")
        if values.shape[0] == 0:
            raise ValueError("RatioOfSums requires at least one observation.")
        if max(self.numerator, self.denominator) >= values.shape[1]:
            raise ValueError("RatioOfSums column positions exceed the sample's feature count.")
        if not np.isfinite(values).all():
            raise ValueError("RatioOfSums requires finite values.")
        with np.errstate(over="ignore", invalid="ignore", divide="ignore"):
            numerator = float(np.sum(values[:, self.numerator]))
            denominator = float(np.sum(values[:, self.denominator]))
            if not np.isfinite(numerator) or not np.isfinite(denominator):
                raise ValueError("RatioOfSums requires finite column sums.")
            if denominator == 0.0:
                raise ValueError("RatioOfSums is undefined because the denominator sum is zero.")
            result = numerator / denominator
        if not np.isfinite(result):
            raise ValueError("RatioOfSums must return a finite value.")
        return result
Source code in src/bootstrapx/stats/confidence.py
28
29
30
31
32
33
34
35
36
37
38
39
@dataclass
class ConfidenceInterval:
    low: float
    high: float
    method: str

    def __contains__(self, value: float) -> bool:
        return self.low <= value <= self.high

    @property
    def width(self) -> float:
        return self.high - self.low

BootstrapResult.to_dict() excludes the potentially large bootstrap distribution by default. Pass include_distribution=True when the full array is required. BootstrapResult.to_frame() returns a compact one-row pandas DataFrame.

TwoSampleBootstrapResult follows the same compact-export policy and adds arm estimates, effect/design metadata, sample sizes, and optional cluster counts.

Integrations

Bases: BaseCrossValidator

Bootstrap cross-validator compatible with scikit-learn's CV API.

Generates n_splits bootstrap train/test splits. Each training set is a bootstrap resample of size n (with replacement); the test set contains the out-of-bag (OOB) observations not selected for training.

Parameters

n_splits : int, default=200 Number of bootstrap iterations. random_state : int or np.random.Generator or None Seed for reproducibility.

Notes

  • Usable with cross_val_score, cross_validate, GridSearchCV.
  • OOB test set size ≈ 0.368 × n per split (Poisson approximation).
  • Independent rows only: non-None groups are rejected. This splitter does not provide group-aware or time-series-safe validation.
  • For the 0.632 bootstrap estimator, average 0.368 * train_score + 0.632 * oob_score across splits.
Source code in src/bootstrapx/compat/sklearn_cv.py
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
class BootstrapCV(BaseCrossValidator):  # type: ignore[misc]
    """Bootstrap cross-validator compatible with scikit-learn's CV API.

    Generates ``n_splits`` bootstrap train/test splits. Each training set
    is a bootstrap resample of size ``n`` (with replacement); the test set
    contains the out-of-bag (OOB) observations not selected for training.

    Parameters
    ----------
    n_splits : int, default=200
        Number of bootstrap iterations.
    random_state : int or np.random.Generator or None
        Seed for reproducibility.

    Notes
    -----
    - Usable with ``cross_val_score``, ``cross_validate``, ``GridSearchCV``.
    - OOB test set size ≈ 0.368 × n per split (Poisson approximation).
    - Independent rows only: non-None ``groups`` are rejected. This splitter
      does not provide group-aware or time-series-safe validation.
    - For the 0.632 bootstrap estimator, average
      ``0.368 * train_score + 0.632 * oob_score`` across splits.
    """

    def __init__(self, n_splits: int = 200, random_state: int | np.random.Generator | None = None):
        if isinstance(n_splits, bool) or not isinstance(n_splits, int | np.integer):
            raise TypeError("n_splits must be an integer.")
        if n_splits < 1:
            raise ValueError("n_splits must be at least 1.")
        if (
            random_state is not None
            and not isinstance(random_state, np.random.Generator)
            and (isinstance(random_state, bool) or not isinstance(random_state, int | np.integer))
        ):
            raise TypeError("random_state must be an integer, numpy Generator, or None.")
        self.n_splits = n_splits
        self.random_state = random_state

    def split(
        self, X: Any, y: Any = None, groups: Any = None
    ) -> Generator[tuple[IntArray, IntArray], None, None]:
        if groups is not None:
            raise ValueError(
                "BootstrapCV does not support groups. Use a group-aware "
                "cross-validator for repeated observations per entity."
            )
        X, y, groups = indexable(X, y, groups)
        n = len(X)
        if n < 2:
            raise ValueError("BootstrapCV requires at least 2 observations.")
        rng = (
            self.random_state
            if isinstance(self.random_state, np.random.Generator)
            else np.random.default_rng(self.random_state)
        )
        all_idx = np.arange(n)
        yielded = 0
        while yielded < self.n_splits:
            train = rng.integers(0, n, size=n)
            test = np.setdiff1d(all_idx, train)
            if len(test) == 0:
                continue
            yielded += 1
            yield train, test

    def get_n_splits(self, X: Any = None, y: Any = None, groups: Any = None) -> int:
        return self.n_splits

    def _iter_test_indices(
        self, X: Any = None, y: Any = None, groups: Any = None
    ) -> Generator[IntArray, None, None]:
        for _, test in self.split(X, y, groups):
            yield test

Accessor registered as pd.Series.bootstrap.

Source code in src/bootstrapx/compat/pandas_accessor.py
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
class _BootstrapSeriesAccessor:
    """Accessor registered as ``pd.Series.bootstrap``."""

    def __init__(self, obj: pd.Series):
        self._obj = obj

    def ci(
        self,
        statistic: Callable[..., float],
        *,
        method: str = "bca",
        n_resamples: int = 9999,
        confidence_level: float = 0.95,
        random_state: int | None = None,
        **kwargs: Any,
    ) -> BootstrapResult:
        """Run bootstrap and return a :class:`~bootstrapx.BootstrapResult`."""
        return bootstrap(
            self._obj,
            statistic,
            method=method,
            n_resamples=n_resamples,
            confidence_level=confidence_level,
            random_state=random_state,
            **kwargs,
        )

    def bca(
        self,
        statistic: Callable[..., float],
        n_resamples: int = 9999,
        confidence_level: float = 0.95,
        random_state: int | None = None,
    ) -> BootstrapResult:
        """Shortcut for ``method='bca'``."""
        return self.ci(
            statistic,
            method="bca",
            n_resamples=n_resamples,
            confidence_level=confidence_level,
            random_state=random_state,
        )

    def percentile(
        self,
        statistic: Callable[..., float],
        n_resamples: int = 9999,
        confidence_level: float = 0.95,
        random_state: int | None = None,
    ) -> BootstrapResult:
        """Shortcut for ``method='percentile'``."""
        return self.ci(
            statistic,
            method="percentile",
            n_resamples=n_resamples,
            confidence_level=confidence_level,
            random_state=random_state,
        )

bca(statistic, n_resamples=9999, confidence_level=0.95, random_state=None)

Shortcut for method='bca'.

Source code in src/bootstrapx/compat/pandas_accessor.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def bca(
    self,
    statistic: Callable[..., float],
    n_resamples: int = 9999,
    confidence_level: float = 0.95,
    random_state: int | None = None,
) -> BootstrapResult:
    """Shortcut for ``method='bca'``."""
    return self.ci(
        statistic,
        method="bca",
        n_resamples=n_resamples,
        confidence_level=confidence_level,
        random_state=random_state,
    )

ci(statistic, *, method='bca', n_resamples=9999, confidence_level=0.95, random_state=None, **kwargs)

Run bootstrap and return a :class:~bootstrapx.BootstrapResult.

Source code in src/bootstrapx/compat/pandas_accessor.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def ci(
    self,
    statistic: Callable[..., float],
    *,
    method: str = "bca",
    n_resamples: int = 9999,
    confidence_level: float = 0.95,
    random_state: int | None = None,
    **kwargs: Any,
) -> BootstrapResult:
    """Run bootstrap and return a :class:`~bootstrapx.BootstrapResult`."""
    return bootstrap(
        self._obj,
        statistic,
        method=method,
        n_resamples=n_resamples,
        confidence_level=confidence_level,
        random_state=random_state,
        **kwargs,
    )

percentile(statistic, n_resamples=9999, confidence_level=0.95, random_state=None)

Shortcut for method='percentile'.

Source code in src/bootstrapx/compat/pandas_accessor.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def percentile(
    self,
    statistic: Callable[..., float],
    n_resamples: int = 9999,
    confidence_level: float = 0.95,
    random_state: int | None = None,
) -> BootstrapResult:
    """Shortcut for ``method='percentile'``."""
    return self.ci(
        statistic,
        method="percentile",
        n_resamples=n_resamples,
        confidence_level=confidence_level,
        random_state=random_state,
    )

Accessor registered as pd.DataFrame.bootstrap.

Applies bootstrap column-wise and returns a summary DataFrame.

Source code in src/bootstrapx/compat/pandas_accessor.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
class _BootstrapDataFrameAccessor:
    """Accessor registered as ``pd.DataFrame.bootstrap``.

    Applies bootstrap column-wise and returns a summary DataFrame.
    """

    def __init__(self, obj: pd.DataFrame):
        self._obj = obj

    def summary(
        self,
        statistic: Callable[..., float],
        *,
        method: str = "bca",
        n_resamples: int = 9999,
        confidence_level: float = 0.95,
        random_state: int | None = None,
        **kwargs: Any,
    ) -> pd.DataFrame:
        """Return a DataFrame with bootstrap CI summary for each column.

        Returns
        -------
        pd.DataFrame
            Index: column names of the original DataFrame.
            Columns: ``theta_hat``, ``ci_low``, ``ci_high``, ``se``, ``method``.
        """
        rows = []
        for col in self._obj.columns:
            r = bootstrap(
                self._obj[col],
                statistic,
                method=method,
                n_resamples=n_resamples,
                confidence_level=confidence_level,
                random_state=random_state,
                **kwargs,
            )
            rows.append(
                {
                    "column": col,
                    "theta_hat": r.theta_hat,
                    "ci_low": r.confidence_interval.low,
                    "ci_high": r.confidence_interval.high,
                    "se": r.standard_error,
                    "method": r.method,
                }
            )
        return pd.DataFrame(rows).set_index("column")

    def ci(
        self,
        statistic: Callable[..., float],
        *,
        method: str = "bca",
        n_resamples: int = 9999,
        confidence_level: float = 0.95,
        random_state: int | None = None,
        **kwargs: Any,
    ) -> pd.DataFrame:
        """Alias for :meth:`summary`."""
        return self.summary(
            statistic,
            method=method,
            n_resamples=n_resamples,
            confidence_level=confidence_level,
            random_state=random_state,
            **kwargs,
        )

ci(statistic, *, method='bca', n_resamples=9999, confidence_level=0.95, random_state=None, **kwargs)

Alias for :meth:summary.

Source code in src/bootstrapx/compat/pandas_accessor.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def ci(
    self,
    statistic: Callable[..., float],
    *,
    method: str = "bca",
    n_resamples: int = 9999,
    confidence_level: float = 0.95,
    random_state: int | None = None,
    **kwargs: Any,
) -> pd.DataFrame:
    """Alias for :meth:`summary`."""
    return self.summary(
        statistic,
        method=method,
        n_resamples=n_resamples,
        confidence_level=confidence_level,
        random_state=random_state,
        **kwargs,
    )

summary(statistic, *, method='bca', n_resamples=9999, confidence_level=0.95, random_state=None, **kwargs)

Return a DataFrame with bootstrap CI summary for each column.

Returns

pd.DataFrame Index: column names of the original DataFrame. Columns: theta_hat, ci_low, ci_high, se, method.

Source code in src/bootstrapx/compat/pandas_accessor.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def summary(
    self,
    statistic: Callable[..., float],
    *,
    method: str = "bca",
    n_resamples: int = 9999,
    confidence_level: float = 0.95,
    random_state: int | None = None,
    **kwargs: Any,
) -> pd.DataFrame:
    """Return a DataFrame with bootstrap CI summary for each column.

    Returns
    -------
    pd.DataFrame
        Index: column names of the original DataFrame.
        Columns: ``theta_hat``, ``ci_low``, ``ci_high``, ``se``, ``method``.
    """
    rows = []
    for col in self._obj.columns:
        r = bootstrap(
            self._obj[col],
            statistic,
            method=method,
            n_resamples=n_resamples,
            confidence_level=confidence_level,
            random_state=random_state,
            **kwargs,
        )
        rows.append(
            {
                "column": col,
                "theta_hat": r.theta_hat,
                "ci_low": r.confidence_interval.low,
                "ci_high": r.confidence_interval.high,
                "se": r.standard_error,
                "method": r.method,
            }
        )
    return pd.DataFrame(rows).set_index("column")