Utilities¶
General-purpose helpers used across the package. Grouped by purpose:
- Data-generating processes —
simulate_nonpaired_scores,simulate_paired_scores: generate paired and non-paired binary outcomes from configurable true effects. Used by the BFDA harness and the operating-characteristics notebooks. - Frequentist baseline tests —
fisher_exact_nonpaired_test,mcnemar_paired_test: classical tests that serve as the frequentist baseline in the operating-characteristics analyses (non-paired, paired). - Bayes Factor Design Analysis (BFDA) —
bfda_simulate,bfda_power_curve,find_n_for_power,plot_bfda_power,plot_bfda_sensitivity: Monte-Carlo sample-size planning under a Bayes-factor decision rule. See the sample-size planning guide. - Decision helpers —
bf10_to_ph0: convert a Bayes factorBF₁₀into the posterior probability of the null under a given priorπ_H₀.
utils
¶
binarize_if_needed(y, threshold=0.5, *, name='y', verbose=False)
¶
Return a 0/1 float64 array, binarising continuous inputs in [0, 1].
The package's models all operate on binary 0/1 data, but users often
have continuous scores in [0, 1] (e.g. predicted probabilities,
classifier confidences). This helper provides a single, consistent
coercion across the non-paired and paired classes:
- If every entry of
yis already exactly0or1(after casting to float), the array is returned unchanged. - Otherwise, values strictly outside
[0, 1]or anyNaNraise :class:ValueErrorimmediately — we never silently clip or truncate. - Otherwise,
yis binarised as(y >= threshold).astype(float).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
ArrayLike
|
Input scores. Any array-like accepted by :func: |
required |
threshold
|
float
|
Cut-off for binarisation. Values |
0.5
|
name
|
str
|
Name of the variable in error / warning messages
(e.g. |
'y'
|
verbose
|
bool
|
If |
False
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in bayesprop/utils/utils.py
simulate_nonpaired_scores(N=200, theta_A=0.75, theta_B=0.6, seed=0, rng=None)
¶
Simulate independent binary outcomes for a non-paired A/B test.
Each group is sampled independently from a Bernoulli distribution with the specified success probability.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
N
|
int
|
Number of observations per group. |
200
|
theta_A
|
float
|
True success probability for model A. |
0.75
|
theta_B
|
float
|
True success probability for model B. |
0.6
|
seed
|
int
|
Random seed for reproducibility. |
0
|
rng
|
Generator | None
|
Optional pre-seeded RNG; if provided, seed is ignored. |
None
|
Returns:
| Type | Description |
|---|---|
NonPairedSimResult
|
class: |
NonPairedSimResult
|
|
Source code in bayesprop/utils/utils.py
simulate_paired_scores(N=200, theta_A=0.75, theta_B=0.6, sigma_theta=0.0, seed=0, rng=None)
¶
Simulate paired binary outcomes from a logistic DGP.
Matches the paired model: y_A ~ Bern(σ(μ + δ_A)),
y_B ~ Bern(σ(μ)), where μ = logit(theta_B) and
δ_A = logit(theta_A) − logit(theta_B).
When sigma_theta > 0 each item i additionally receives a
random effect ε_i ~ N(0, sigma_theta) so that
θ_i = μ + ε_i (useful for more realistic BFDA simulations).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
N
|
int
|
Number of paired observations. |
200
|
theta_A
|
float
|
True success probability for model A. |
0.75
|
theta_B
|
float
|
True success probability for model B. |
0.6
|
sigma_theta
|
float
|
SD of optional per-item random effects
( |
0.0
|
seed
|
int
|
Random seed for reproducibility. |
0
|
rng
|
Generator | None
|
Optional pre-seeded RNG; if provided, seed is ignored. |
None
|
Returns:
| Type | Description |
|---|---|
PairedSimResult
|
class: |
PairedSimResult
|
|
PairedSimResult
|
|
Source code in bayesprop/utils/utils.py
fisher_exact_nonpaired_test(y_A, y_B, *, alternative='two-sided')
¶
Fisher's exact test for two independent proportions.
Frequentist baseline that mirrors the data contract of
:class:bayesprop.resources.bayes_nonpaired.NonPairedBayesPropTest
but returns a classical p-value instead of a Bayes factor. Useful
as a calibration reference for operating-characteristic /
Bayes-factor design analyses (e.g. overlaying a frequentist power
curve onto the Bayesian three-way OC plot).
The test is exact under H₀: p_A = p_B (no large-sample
approximation), so it remains valid at small n and at boundary
success rates p near 0 or 1, where the Wald Z and Pearson
chi-squared tests are unreliable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_A
|
ArrayLike
|
Binary outcomes (0/1) for group A. Non-binary inputs are rejected — binarise upstream if your scores are continuous. |
required |
y_B
|
ArrayLike
|
Binary outcomes (0/1) for group B. |
required |
alternative
|
Literal['two-sided', 'greater', 'less']
|
|
'two-sided'
|
Returns:
| Type | Description |
|---|---|
FrequentistTestResult
|
class: |
FrequentistTestResult
|
ratio, and cell counts. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If either array contains values outside |
Source code in bayesprop/utils/utils.py
mcnemar_paired_test(y_A, y_B, *, alternative='two-sided', exact=None)
¶
McNemar's test for two paired (matched) proportions.
Frequentist baseline that mirrors the data contract of
:class:bayesprop.resources.bayes_paired_laplace.PairedBayesPropTest
but returns a classical p-value instead of a Bayes factor. Useful
as a calibration reference for paired operating-characteristic /
Bayes-factor design analyses.
Given two binary arrays y_A and y_B of equal length, the
test conditions on the discordant pairs:
b = #{i : y_A[i] = 1, y_B[i] = 0}c = #{i : y_A[i] = 0, y_B[i] = 1}
Under H_0: p_A = p_B and conditional on b + c, b follows
Binomial(b + c, 0.5). The two-sided p-value is computed as
2 · min(P(B ≤ b), P(B ≥ b)) clipped to [0, 1] from an
exact binomial calculation when b + c is small (default
threshold 25, matching :func:scipy.stats.contingency.mcnemar's
convention), and from the standard chi-squared approximation
otherwise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_A
|
ArrayLike
|
Binary outcomes (0/1) for arm A. Non-binary inputs raise. |
required |
y_B
|
ArrayLike
|
Binary outcomes (0/1) for arm B. Must be the same length
as |
required |
alternative
|
Literal['two-sided', 'greater', 'less']
|
|
'two-sided'
|
exact
|
bool | None
|
If |
None
|
Returns:
| Type | Description |
|---|---|
FrequentistTestResult
|
class: |
FrequentistTestResult
|
odds ratio |
FrequentistTestResult
|
marginal counts. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If either array contains non-binary values or the two arrays have different lengths. |
Source code in bayesprop/utils/utils.py
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 | |
bfda_simulate(data_generator, decision_fn, sample_sizes, n_sim=500, seed=42)
¶
Generic BFDA engine -- works with any data-generating process and decision rule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_generator
|
Callable[[Generator, int], tuple[ndarray, ndarray]]
|
Callable(rng, n) -> (y_A, y_B). Generates one simulated dataset of size n per group using the provided RNG. |
required |
decision_fn
|
Callable[[ndarray, ndarray], bool]
|
Callable(y_A, y_B) -> bool. Returns |
required |
sample_sizes
|
list[int]
|
List of per-group sample sizes to evaluate. |
required |
n_sim
|
int
|
Number of simulated datasets per sample size. |
500
|
seed
|
int
|
Random seed for reproducibility. |
42
|
Returns:
| Type | Description |
|---|---|
dict[int, float]
|
Dictionary mapping sample size -> P(decisive outcome). |
Source code in bayesprop/utils/utils.py
bf10_to_ph0(bf_10, prior_H0=0.5)
¶
Convert BF_10 to posterior probability of H0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bf_10
|
float
|
Bayes factor in favour of H1. |
required |
prior_H0
|
float
|
Prior probability of H0. |
0.5
|
Returns:
| Type | Description |
|---|---|
float
|
P(H0 | data). |
Source code in bayesprop/utils/utils.py
bfda_power_curve(theta_A_true, theta_B_true, sample_sizes, design='nonpaired', decision_rule='bayes_factor', *, bf_threshold=3.0, ph0_threshold=0.05, prior_H0=0.5, rope=(-0.02, 0.02), ci_mass=0.95, n_sim=500, seed=42, alpha0=1.0, beta0=1.0, sigma_theta=2.0, prior_sigma_delta=1.0, prior_sigma_mu=2.0, n_iter=1000, burn_in=300, n_chains=2)
¶
Unified Bayes Factor Design Analysis for any design × decision-rule.
Simulates datasets under a known effect and estimates the probability that a given Bayesian decision rule will reject H0 as a function of sample size (i.e. Bayesian "power").
Supported combinations:
+----------------+----------------+--------------------+--------+
| design | decision_rule | key threshold | fast? |
+================+================+====================+========+
| nonpaired | bayes_factor | bf_threshold | yes |
| nonpaired | posterior_null | ph0_threshold | yes |
| nonpaired | rope | rope | medium |
| paired | bayes_factor | bf_threshold | slow |
| paired | posterior_null | ph0_threshold | slow |
| paired | rope | rope | slow |
+----------------+----------------+--------------------+--------+
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
theta_A_true
|
float
|
Assumed true success rate for model A. |
required |
theta_B_true
|
float
|
Assumed true success rate for model B. |
required |
sample_sizes
|
list[int]
|
List of per-group sample sizes to evaluate. |
required |
design
|
str
|
|
'nonpaired'
|
decision_rule
|
DecisionRuleType
|
|
'bayes_factor'
|
bf_threshold
|
float
|
BF_10 threshold for decisive evidence ( |
3.0
|
ph0_threshold
|
float
|
Reject H0 when P(H0|data) < this ( |
0.05
|
prior_H0
|
float
|
Prior probability of H0 ( |
0.5
|
rope
|
tuple[float, float]
|
(lower, upper) bounds of the ROPE ( |
(-0.02, 0.02)
|
ci_mass
|
float
|
Credible interval mass for ROPE analysis ( |
0.95
|
n_sim
|
int
|
Number of simulated datasets per sample size. |
500
|
seed
|
int
|
Random seed for reproducibility. |
42
|
alpha0
|
float
|
Prior Beta alpha parameter (non-paired only). |
1.0
|
beta0
|
float
|
Prior Beta beta parameter (non-paired only). |
1.0
|
sigma_theta
|
float
|
SD of the shared latent item effect (paired DGP). |
2.0
|
prior_sigma_delta
|
float
|
SD of N(0, σ) prior on delta_A (paired only). |
1.0
|
prior_sigma_mu
|
float
|
SD of N(0, σ) prior on mu (paired only). |
2.0
|
n_iter
|
int
|
Total Gibbs iterations per chain (paired only). |
1000
|
burn_in
|
int
|
Warm-up iterations per chain (paired only). |
300
|
n_chains
|
int
|
Number of MCMC chains per dataset (paired only). |
2
|
Returns:
| Type | Description |
|---|---|
dict[int, float]
|
Dictionary mapping sample size -> P(decisive outcome). |
Source code in bayesprop/utils/utils.py
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 | |
find_n_for_power(power_curve, target_power=0.8)
¶
Interpolate the sample size needed to achieve a target power level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
power_curve
|
dict[int, float]
|
Dictionary mapping sample size -> power (from BFDA). |
required |
target_power
|
float
|
Desired power level (default 0.80). |
0.8
|
Returns:
| Type | Description |
|---|---|
float | None
|
Interpolated sample size, or |
Source code in bayesprop/utils/utils.py
plot_bfda_power(power_curve, theta_A_true, theta_B_true, bf_threshold=3.0, target_power=0.8, title=None, ax=None)
¶
Plot a BFDA power curve with 80%/95% reference lines.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
power_curve
|
dict[int, float]
|
Dictionary mapping sample size -> power. |
required |
theta_A_true
|
float
|
Assumed true rate for model A (for title). |
required |
theta_B_true
|
float
|
Assumed true rate for model B (for title). |
required |
bf_threshold
|
float
|
BF_10 threshold used (for y-axis label). |
3.0
|
target_power
|
float
|
Power level to highlight via interpolation. |
0.8
|
title
|
str | None
|
Optional custom title. |
None
|
ax
|
Axes | None
|
Optional matplotlib Axes to plot on. |
None
|
Returns:
| Type | Description |
|---|---|
Figure
|
The matplotlib Figure. |
Source code in bayesprop/utils/utils.py
plot_bfda_sensitivity(theta_A_true, theta_B_true, sample_sizes, thresholds=None, n_sim=500, seed=42, design='nonpaired', title=None, ax=None, **kwargs)
¶
Plot BFDA power curves for multiple BF_10 thresholds.
Works for both paired and non-paired designs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
theta_A_true
|
float
|
Assumed true success rate for model A. |
required |
theta_B_true
|
float
|
Assumed true success rate for model B. |
required |
sample_sizes
|
list[int]
|
List of per-group sample sizes to evaluate. |
required |
thresholds
|
list[float] | None
|
BF_10 thresholds to compare (default: [3, 6, 10]). |
None
|
n_sim
|
int
|
Number of simulated datasets per sample size. |
500
|
seed
|
int
|
Random seed for reproducibility. |
42
|
design
|
str
|
|
'nonpaired'
|
title
|
str | None
|
Optional custom title. |
None
|
ax
|
Axes | None
|
Optional matplotlib Axes to plot on. |
None
|
**kwargs
|
Any
|
Extra arguments forwarded to :func: |
{}
|
Returns:
| Type | Description |
|---|---|
Figure
|
The matplotlib Figure. |
Source code in bayesprop/utils/utils.py
838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 | |