Operating Characteristics¶
Monte-Carlo evaluation of the non-paired Bayesian procedure's frequentist operating characteristics — three-way decision rates, CI coverage, matched-α frequentist baselines, and sequential stopping-time distributions.
operation_characteristics
¶
Operating-characteristic / BFDA simulation harness for the non-paired model.
This module evaluates the frequentist operating characteristics of the
Bayesian decision procedure built around
:class:bayesprop.resources.bayes_nonpaired.NonPairedBayesPropTest.
A Bayesian model has no Type-I error by itself, but the moment we wrap
it in a decision rule (BF_10 ≥ bf_upper → reject, BF_10 ≤ bf_lower
→ accept, otherwise inconclusive) the rule is a function from data to a
decision and therefore has well-defined frequentist operating
characteristics. Estimating those by Monte-Carlo simulation is the
standard "calibrated Bayes" check (Rubin 1984, Little 2006).
Three public entry points cover the workflow:
- :func:
simulate_fixed_n— one(p_A, p_B, n)cell of the OC grid, returning the Bayes three-way decision rates, the 95 % CI coverage ofΔ, and the per-replicate Fisher exact p-values needed for a frequentist baseline. - :func:
grid_fixed_n— sweeps :func:simulate_fixed_nover an arbitrary list of(p_A, p_B)pairs and returns a tidypandas.DataFrameplus the stacked(n_grid, n_sim)p-value matrix. - :func:
simulate_sequential— empirical stopping-time distribution of :class:SequentialNonPairedBayesPropTestat a single(p_A, p_B)point.
The small helper :func:matched_calibration_alpha derives the
frequentist significance level that empirically matches the Bayes rule's
Type-I rate at the null, so the two procedures can be compared at equal
false-positive rates.
All quantities are obtained through the public API of the codebase
(NonPairedBayesPropTest.fit, savage_dickey_test, classify_bf,
fisher_exact_nonpaired_test, SequentialNonPairedBayesPropTest),
so this module also serves as an end-to-end integration test of those
APIs.
simulate_fixed_n(p_A, p_B, n, n_sim, rng, *, alpha0=1.0, beta0=1.0, bf_upper=3.0, bf_lower=1.0 / 3.0, n_samples_mc=4000, track_ci=True)
¶
Monte-Carlo operating characteristics at a single (p_A, p_B, n) cell.
For each of n_sim replicates we
- generate one dataset of size
nper arm via :func:simulate_nonpaired_scores, - fit :class:
NonPairedBayesPropTestwith priorBeta(α₀, β₀), - compute the Savage–Dickey BF on
Δ = 0and classify the result with :func:classify_bf(configurablebf_upper/bf_lower), - optionally check whether the 95 % credible interval on
Δcovers the true effectp_A − p_B(track_ci=True), - run :func:
fisher_exact_nonpaired_teston the same simulated data so a frequentist baseline can be derived later.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
p_A
|
float
|
True success probability for group A. |
required |
p_B
|
float
|
True success probability for group B. |
required |
n
|
int
|
Per-arm sample size. |
required |
n_sim
|
int
|
Number of Monte-Carlo replicates. |
required |
rng
|
Generator
|
Pre-seeded NumPy generator. Threaded through the entire loop so the harness is fully deterministic given the seed. |
required |
alpha0
|
float
|
Prior alpha for both Beta(α₀, β₀) priors. |
1.0
|
beta0
|
float
|
Prior beta for both priors. |
1.0
|
bf_upper
|
float
|
Threshold above which the Bayes rule rejects |
3.0
|
bf_lower
|
float
|
Threshold below which the Bayes rule accepts |
1.0 / 3.0
|
n_samples_mc
|
int
|
Posterior Monte-Carlo draws per replicate. Only affects the 95 % CI estimate; the BF is computed analytically. |
4000
|
track_ci
|
bool
|
If False, skip the CI coverage check
( |
True
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
Tuple |
ndarray
|
keys |
tuple[dict[str, float], ndarray]
|
fractions of the three Bayes decisions, summing to 1), |
tuple[dict[str, float], ndarray]
|
|
tuple[dict[str, float], ndarray]
|
|
tuple[dict[str, float], ndarray]
|
|
tuple[dict[str, float], ndarray]
|
is a 1-D |
tuple[dict[str, float], ndarray]
|
per-replicate Fisher exact p-values. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in bayesprop/utils/operation_characteristics.py
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 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 | |
grid_fixed_n(grid, n, n_sim, seed, *, alpha0=1.0, beta0=1.0, bf_upper=3.0, bf_lower=1.0 / 3.0, n_samples_mc=4000, track_ci=True)
¶
Sweep :func:simulate_fixed_n over an arbitrary (p_A, p_B) grid.
A single deterministic np.random.Generator is created from
seed and threaded through every grid cell, so a re-run with the
same seed produces bit-identical results.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grid
|
list[tuple[float, float]]
|
List of |
required |
n
|
int
|
Per-arm sample size (constant across grid cells). |
required |
n_sim
|
int
|
Replicates per grid cell. |
required |
seed
|
int
|
Top-level random seed. |
required |
alpha0
|
float
|
See :func: |
1.0
|
beta0
|
float
|
See :func: |
1.0
|
bf_upper
|
float
|
See :func: |
3.0
|
bf_lower
|
float
|
See :func: |
1.0 / 3.0
|
n_samples_mc
|
int
|
See :func: |
4000
|
track_ci
|
bool
|
See :func: |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Tuple |
ndarray
|
|
tuple[DataFrame, ndarray]
|
|
tuple[DataFrame, ndarray]
|
|
tuple[DataFrame, ndarray]
|
is a |
tuple[DataFrame, ndarray]
|
p-values — needed to derive the matched calibration α via |
tuple[DataFrame, ndarray]
|
func: |
Source code in bayesprop/utils/operation_characteristics.py
matched_calibration_alpha(p_values, bayes_type1_rate, null_grid_index)
¶
Empirical α that matches a frequentist test's Type-I rate to the Bayes rule's.
Given a matrix of Fisher (or other) p-values produced under a grid
of true effects, the row indexed by null_grid_index corresponds
to the null case Δ = 0. We pick the p-value cutoff α whose
empirical rejection rate on that row equals
bayes_type1_rate, by reading the corresponding quantile of the
null p-value distribution. The resulting α is the fairest
like-for-like calibration when overlaying the frequentist and
Bayesian power curves.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
p_values
|
ndarray
|
|
required |
bayes_type1_rate
|
float
|
Empirical |
required |
null_grid_index
|
int
|
Row index into |
required |
Returns:
| Type | Description |
|---|---|
float
|
Matched-calibration α, clipped to |
float
|
if |
float
|
the null in this simulation), since no positive α can match. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in bayesprop/utils/operation_characteristics.py
wilson_band(rates, n_sim, *, confidence=0.95)
¶
Wilson confidence band for a vector of Monte-Carlo binomial rates.
Each entry of rates is treated as an empirical binomial proportion
k / n_sim with k = round(rate * n_sim) (the number of successes
among the n_sim simulation replicates). The Wilson score interval
is computed pointwise via
:func:scipy.stats.binomtest's proportion_ci(method="wilson").
Unlike the Wald interval, Wilson stays inside [0, 1] and retains
its nominal coverage near the boundaries (rate ≈ 0 or ≈ 1),
which is exactly where OC curves spend most of their interesting
structure. See Brown, Cai & DasGupta (2001).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rates
|
ndarray
|
1-D array of empirical rates in |
required |
n_sim
|
int
|
Number of simulation replicates each rate was averaged over. Constant across the grid in this codebase. |
required |
confidence
|
float
|
Nominal coverage, e.g. |
0.95
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Tuple |
ndarray
|
of |
tuple[ndarray, ndarray]
|
construction of the Wilson interval. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in bayesprop/utils/operation_characteristics.py
simulate_sequential(p_A, p_B, n_sim, rng, *, alpha0=1.0, beta0=1.0, bf_upper=3.0, bf_lower=1.0 / 3.0, n_min=50, n_max=600, batch_size=50, n_samples_mc=2000)
¶
Empirical stopping-time distribution of the sequential BF procedure.
For each of n_sim replicates we run a fresh
:class:SequentialNonPairedBayesPropTest, stream batches of size
batch_size of independent Bernoulli observations from
:func:simulate_nonpaired_scores, and record the per-arm sample
size at which the procedure stops. Trials that hit n_max
without the BF crossing either threshold are right-censored and
classified as "inconclusive".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
p_A
|
float
|
True success probability for group A. |
required |
p_B
|
float
|
True success probability for group B. |
required |
n_sim
|
int
|
Number of independent sequential trajectories. |
required |
rng
|
Generator
|
Pre-seeded NumPy generator. |
required |
alpha0
|
float
|
Prior alpha for both arms. |
1.0
|
beta0
|
float
|
Prior beta for both arms. |
1.0
|
bf_upper
|
float
|
Stop for |
3.0
|
bf_lower
|
float
|
Stop for |
1.0 / 3.0
|
n_min
|
int
|
Minimum per-arm sample size before any BF-based stop is allowed. Guards against unstable early BFs. |
50
|
n_max
|
int
|
Hard cap on per-arm sample size — the trajectory is terminated and the trial reported as censored. |
600
|
batch_size
|
int
|
Per-arm batch size delivered to each |
50
|
n_samples_mc
|
int
|
Posterior MC draws per look (only affects the sequential test's internal ROPE / coverage estimates; the BF is computed analytically). |
2000
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
Dict with the per-trial stopping-time statistics. Keys |
dict[str, float]
|
|
dict[str, float]
|
|
dict[str, float]
|
per-arm stopping sample size. |
dict[str, float]
|
fraction of trials that hit |
dict[str, float]
|
|
dict[str, float]
|
classification (via :func: |
dict[str, float]
|
|
Source code in bayesprop/utils/operation_characteristics.py
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 | |