Operating Characteristics (Paired)¶
Monte-Carlo evaluation of the paired-Laplace Bayesian procedure's
frequentist operating characteristics — three-way decision rates, CI
coverage on Δ = p_A − p_B, matched-α McNemar baselines, and
sequential stopping-time distributions. The companion notebook is
src/notebooks/operating_characteristics_paired_laplace.ipynb; the
narrative is in Frequentist Evaluation — Paired Laplace.
operation_characteristics_paired
¶
Operating-characteristic / BFDA simulation harness for the paired-Laplace model.
This module mirrors :mod:bayesprop.utils.operation_characteristics
but evaluates the paired Bayesian procedure built around
:class:bayesprop.resources.bayes_paired_laplace.PairedBayesPropTest
and its sequential variant
:class:SequentialPairedBayesPropTest.
A paired binary design has the same three frequentist concerns as the
non-paired one — Type-I rate, three-way decision rates, credible-interval
coverage — plus a sequential variant. The analysis model is
y_A ~ Bern(σ(μ + δ_A)), y_B ~ Bern(σ(μ)), with prior
δ_A ~ N(0, σ_δ²). The Savage–Dickey BF tests H_0: δ_A = 0,
which is equivalent to H_0: p_A = p_B.
For an apples-to-apples comparison with the non-paired analysis (and
with the user's intuition), the public API takes the true marginal
probabilities (p_A, p_B) on the probability scale and inverts
the sigmoid internally to recover the analysis-model intercept
μ = logit(p_B) and effect δ_A = logit(p_A) − logit(p_B). This
parametrisation is the one expected by
:func:simulate_paired_scores when δ_B = 0 and σ_θ = 0.
Three public entry points cover the workflow:
- :func:
simulate_fixed_n_paired— one(p_A, p_B, n)cell of the OC grid, returning the Bayes three-way decision rates, the 95 % CI coverage ofΔ = p_A − p_B, and the per-replicate McNemar exact p-values needed for a frequentist baseline. - :func:
grid_fixed_n_paired— sweeps :func:simulate_fixed_n_pairedover an arbitrary list of(p_A, p_B)pairs. - :func:
simulate_sequential_paired— empirical stopping-time distribution of :class:SequentialPairedBayesPropTestat a single(p_A, p_B)point.
The matched-α helper :func:matched_calibration_alpha and the Wilson
band helper :func:wilson_band are parametrisation-free and are
re-exported from the non-paired module so callers don't have to
import from two places.
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_fixed_n_paired(p_A, p_B, n, n_sim, rng, *, prior_sigma_delta=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 paired dataset of size
nvia :func:simulate_paired_scoreswiththeta_A = p_Aandtheta_B = p_B(σ_θ = 0); - fit :class:
PairedBayesPropTestwith priorδ_A ~ N(0, prior_sigma_delta²); - compute the Savage–Dickey BF on
δ_A = 0and classify the result with :func:classify_bf(configurablebf_upper/bf_lower); - optionally check whether the 95 % credible interval on
Δ = p_A − p_Bcovers the true effect (track_ci=True); - run :func:
mcnemar_paired_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 arm A. |
required |
p_B
|
float
|
True success probability for arm B (= the analysis-model intercept on the probability scale). |
required |
n
|
int
|
Number of paired observations. |
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 |
prior_sigma_delta
|
float
|
Standard deviation of the |
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 Laplace draws per replicate. 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]
|
decision rates, summing to 1), |
tuple[dict[str, float], ndarray]
|
95 % CI coverage of |
tuple[dict[str, float], ndarray]
|
and the echoes |
tuple[dict[str, float], ndarray]
|
|
tuple[dict[str, float], ndarray]
|
length |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in bayesprop/utils/operation_characteristics_paired.py
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 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
grid_fixed_n_paired(grid, n, n_sim, seed, *, prior_sigma_delta=1.0, bf_upper=3.0, bf_lower=1.0 / 3.0, n_samples_mc=4000, track_ci=True)
¶
Sweep :func:simulate_fixed_n_paired 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
|
Number of paired observations (constant across grid cells). |
required |
n_sim
|
int
|
Replicates per grid cell. |
required |
seed
|
int
|
Top-level random seed. |
required |
prior_sigma_delta
|
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]
|
— needed to derive the matched calibration α via |
tuple[DataFrame, ndarray]
|
func: |
Source code in bayesprop/utils/operation_characteristics_paired.py
simulate_sequential_paired(p_A, p_B, n_sim, rng, *, prior_sigma_delta=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 paired-Laplace test.
For each of n_sim replicates we run a fresh
:class:SequentialPairedBayesPropTest, stream batches of size
batch_size of paired Bernoulli observations from
:func:simulate_paired_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 arm A. |
required |
p_B
|
float
|
True success probability for arm B. |
required |
n_sim
|
int
|
Number of independent sequential trajectories. |
required |
rng
|
Generator
|
Pre-seeded NumPy generator. |
required |
prior_sigma_delta
|
float
|
Prior SD on |
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. |
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 Laplace draws per look. |
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]
|
are quantiles of the per-arm stopping sample size. |
dict[str, float]
|
|
dict[str, float]
|
|
dict[str, float]
|
fractions of trials whose final classification (via |
dict[str, float]
|
func: |
Source code in bayesprop/utils/operation_characteristics_paired.py
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 | |