Paired Model — Bayesian Bootstrap¶
Overview¶
The paired Bayesian-bootstrap test (Rubin, 1981) is a
nonparametric alternative to
PairedBayesPropTest(method="laplace") and
PairedBayesPropTest(method="pg"). Instead of
specifying a parametric likelihood or a prior on a logit-scale effect,
it places a flat Dirichlet "prior" over the empirical distribution of
paired differences and reads the posterior on the average treatment
effect off the resulting draws.
Use it when you want to:
- sidestep prior elicitation entirely;
- protect against model misspecification of the paired logistic likelihood;
- generate a posterior on \(\Delta = p_A - p_B\) with no latent \(\delta_A\) on the logit scale.
The price you pay is that no Savage–Dickey BF is available — there is no parametric prior on \(\Delta\) to evaluate at the null. Decisions are routed through the ROPE / posterior-mass framework, and the decision surface is intentionally lean: three quantities are enough.
Generative model¶
For paired binary observations \((y_{A,i}, y_{B,i})\) form the per-pair differences
Each posterior draw of the average treatment effect is
with \(\alpha = 1\) the standard noninformative choice. The class
exposes dirichlet_alpha as a configuration knob: values \(< 1\)
concentrate posterior mass on a small number of observations (sharper,
more bootstrap-like); values \(> 1\) smooth toward the empirical mean.
Quick start¶
import numpy as np
from bayesprop.resources.bayes_paired import PairedBayesPropTest
# Paired binary outcomes (any 0/1 arrays of equal length).
y_A = np.array([1, 1, 0, 1, 1, 0, 1, 1, 1, 0])
y_B = np.array([0, 1, 0, 0, 1, 0, 0, 1, 0, 0])
model = PairedBayesPropTest(method="bootstrap", n_samples=20_000, seed=42).fit(y_A, y_B)
s = model.summary
print(f"Posterior mean Δ = {s.mean_delta:+.4f}")
print(f"95% CI = [{s.ci_95.lower:.4f}, {s.ci_95.upper:.4f}]")
Decision API¶
The class ships only the three quantities that are well-defined directly under the BB posterior — no Bayes-factor machinery, no synthetic prior on \(H_0\):
| Quantity | Where to read it |
|---|---|
| Posterior of null — \(P(\Delta \in \text{ROPE} \mid \text{data})\) | model.rope_test().pct_in_rope |
| Posterior of superiority — \(P(p_A > p_B \mid \text{data})\) | model.summary.p_A_greater_B |
| ROPE decision (reject / accept / undecided) | model.rope_test().decision |
# (1) Posterior of null + full ROPE result
r = model.rope_test(rope=(-0.05, 0.05))
print(f"P(Δ ∈ ROPE) = {r.pct_in_rope:.3f} → {r.decision}")
print(f"95% CI for Δ = [{r.ci_lower:.3f}, {r.ci_upper:.3f}]")
# (2) Posterior of superiority — straight off the fitted summary
print(f"P(p_A > p_B | data) = {model.summary.p_A_greater_B:.3f}")
# (3) Composite decision (bayes_factor and posterior_null are None by design)
d = model.decide()
assert d.bayes_factor is None
assert d.posterior_null is None
assert d.rule == "rope"
print(d.rope.decision)
What is intentionally not exposed:
savage_dickey_test()— no parametric prior on \(\Delta\) to evaluate at the null.posterior_probability_H0()— under the BB this is justrope_test().pct_in_roperead off the posterior directly. Wrapping it would force the user to commit to a prior on \(H_0\) that has no role in the BB posterior itself, and any default flat-prior choice would be reparametrisation-non-invariant (Lindley–Jeffreys).
If you want a prior-dependent posterior probability of \(H_0\) that does react to evidence in a Bayes-factor sense, use a parametric paired model (Laplace or Pólya–Gamma) with a Savage–Dickey BF — see Paired Laplace and Paired Pólya–Gamma.
When to prefer this over the parametric paired classes¶
| Question | Use |
|---|---|
| Need a Savage–Dickey BF for a point null | PairedBayesPropTest(method="laplace") or PairedBayesPropTest(method="pg") |
| Need sequential / early-stopping support | SequentialPairedBayesPropTest |
| Worried about likelihood misspecification | PairedBayesPropTest(method="bootstrap") |
| Sample size ≤ 30 and prior elicitation is acceptable | PairedBayesPropTest(method="pg") |
| Sample size ≫ 100 and want a prior-free posterior on \(\Delta\) | PairedBayesPropTest(method="bootstrap") |
| Want frequentist OC analysis with a McNemar baseline | PairedBayesPropTest(method="laplace") (see Frequentist Evaluation — Paired Laplace) |
Plotting¶
model.plot_posteriors() # θ_A and θ_B overlay
model.plot_posterior_delta() # Δ = θ_A − θ_B with 95 % CI
plot_posteriors() shows the KDE overlay of the marginal θ_A and θ_B
posteriors. plot_posterior_delta() shows the posterior of
Δ = θ_A − θ_B on the probability scale with the 95 % CI band.
Performance notes¶
The implementation is fully vectorised — a single
rng.dirichlet(α·1_n, size=S) produces all \(S\) weight vectors at once,
followed by one matmul W @ D for the posterior draws. On the
notebook's n=10, S=50 000 example this is ~200× faster than a
per-draw Python loop. For large \(n\) the weight matrix is chunked
internally to keep peak memory below ~400 MB.
References¶
- Rubin (1981). The Bayesian Bootstrap. The Annals of Statistics, 9(1), 130–134.
- Kruschke (2018). Rejecting or accepting parameter values in Bayesian estimation. Advances in Methods and Practices in Psychological Science, 1(2), 270–280. (ROPE-based decision making.)
Inputs and binarisation¶
PairedBayesPropTest(method="bootstrap") accepts both already-binary {0, 1} inputs and
continuous scores in [0, 1]. Continuous inputs are auto-binarised at a
configurable threshold (default 0.5):
model = PairedBayesPropTest(method="bootstrap", threshold=0.5, verbose=True).fit(scores_A, scores_B)
Values strictly outside [0, 1] or NaN raise ValueError instead of
being silently truncated.
API¶
See API Reference — Paired Model (Bayesian Bootstrap) for full method-level documentation.