dfbayes 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dfbayes/__init__.py +93 -0
- dfbayes/_core.py +208 -0
- dfbayes/beta.py +209 -0
- dfbayes/binomial.py +57 -0
- dfbayes/concordance.py +219 -0
- dfbayes/contrast.py +191 -0
- dfbayes/mann_whitney.py +396 -0
- dfbayes/mcnemar.py +80 -0
- dfbayes/median_test.py +89 -0
- dfbayes/plotting.py +287 -0
- dfbayes/py.typed +0 -0
- dfbayes/results.py +890 -0
- dfbayes/sign_test.py +105 -0
- dfbayes/simulation.py +507 -0
- dfbayes/wilcoxon.py +340 -0
- dfbayes-0.1.0.dist-info/METADATA +271 -0
- dfbayes-0.1.0.dist-info/RECORD +21 -0
- dfbayes-0.1.0.dist-info/WHEEL +4 -0
- dfbayes-0.1.0.dist-info/licenses/AUTHORS +16 -0
- dfbayes-0.1.0.dist-info/licenses/LICENSE +338 -0
- dfbayes-0.1.0.dist-info/licenses/NOTICE +22 -0
dfbayes/__init__.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Distribution-free Bayesian procedures, ported from R's DFBA 0.1.0.
|
|
2
|
+
|
|
3
|
+
Phase 1 covers the eleven closed-form core functions. Phase 2 adds the
|
|
4
|
+
simulation helpers ``dfba_sim_data``, ``dfba_power_curve`` and
|
|
5
|
+
``dfba_bayes_vs_t_power``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .beta import dfba_beta_bayes_factor, dfba_beta_descriptive
|
|
11
|
+
from .binomial import dfba_binomial
|
|
12
|
+
from .concordance import dfba_bivariate_concordance, dfba_gamma
|
|
13
|
+
from .contrast import dfba_beta_contrast
|
|
14
|
+
from .mann_whitney import dfba_mann_whitney
|
|
15
|
+
from .mcnemar import dfba_mcnemar
|
|
16
|
+
from .median_test import dfba_median_test
|
|
17
|
+
from .results import (
|
|
18
|
+
BetaContrastOut,
|
|
19
|
+
BetaDescriptiveOut,
|
|
20
|
+
BetaOutputDF,
|
|
21
|
+
BinomialOut,
|
|
22
|
+
BivariateConcordanceOut,
|
|
23
|
+
BivariateConcordanceStarOut,
|
|
24
|
+
DFBAResult,
|
|
25
|
+
GammaOut,
|
|
26
|
+
IntervalBFOut,
|
|
27
|
+
MannWhitneyLargeOut,
|
|
28
|
+
MannWhitneySmallOut,
|
|
29
|
+
McnemarOut,
|
|
30
|
+
MedianTestOut,
|
|
31
|
+
OutputTable,
|
|
32
|
+
PointBFOut,
|
|
33
|
+
PowerCurveOut,
|
|
34
|
+
SignTestOut,
|
|
35
|
+
SimDataOut,
|
|
36
|
+
TPowerOut,
|
|
37
|
+
WilcoxonLargeOut,
|
|
38
|
+
WilcoxonSmallOut,
|
|
39
|
+
results_to_dataframe,
|
|
40
|
+
)
|
|
41
|
+
from .sign_test import dfba_sign_test
|
|
42
|
+
from .simulation import (
|
|
43
|
+
dfba_bayes_vs_t_power,
|
|
44
|
+
dfba_power_curve,
|
|
45
|
+
dfba_sim_data,
|
|
46
|
+
)
|
|
47
|
+
from .wilcoxon import dfba_wilcoxon
|
|
48
|
+
|
|
49
|
+
__version__ = "0.1.0"
|
|
50
|
+
|
|
51
|
+
__all__ = [ # noqa: RUF022 - grouped by kind for readability
|
|
52
|
+
"__version__",
|
|
53
|
+
# functions
|
|
54
|
+
"dfba_beta_bayes_factor",
|
|
55
|
+
"dfba_beta_contrast",
|
|
56
|
+
"dfba_beta_descriptive",
|
|
57
|
+
"dfba_binomial",
|
|
58
|
+
"dfba_bivariate_concordance",
|
|
59
|
+
"dfba_gamma",
|
|
60
|
+
"dfba_mann_whitney",
|
|
61
|
+
"dfba_mcnemar",
|
|
62
|
+
"dfba_median_test",
|
|
63
|
+
"dfba_sign_test",
|
|
64
|
+
"dfba_wilcoxon",
|
|
65
|
+
# Phase 2 simulation
|
|
66
|
+
"dfba_sim_data",
|
|
67
|
+
"dfba_power_curve",
|
|
68
|
+
"dfba_bayes_vs_t_power",
|
|
69
|
+
# result objects
|
|
70
|
+
"DFBAResult",
|
|
71
|
+
"BetaContrastOut",
|
|
72
|
+
"BetaDescriptiveOut",
|
|
73
|
+
"BetaOutputDF",
|
|
74
|
+
"BinomialOut",
|
|
75
|
+
"BivariateConcordanceOut",
|
|
76
|
+
"BivariateConcordanceStarOut",
|
|
77
|
+
"GammaOut",
|
|
78
|
+
"IntervalBFOut",
|
|
79
|
+
"MannWhitneyLargeOut",
|
|
80
|
+
"MannWhitneySmallOut",
|
|
81
|
+
"McnemarOut",
|
|
82
|
+
"MedianTestOut",
|
|
83
|
+
"OutputTable",
|
|
84
|
+
"PointBFOut",
|
|
85
|
+
"PowerCurveOut",
|
|
86
|
+
"SignTestOut",
|
|
87
|
+
"SimDataOut",
|
|
88
|
+
"TPowerOut",
|
|
89
|
+
"WilcoxonLargeOut",
|
|
90
|
+
"WilcoxonSmallOut",
|
|
91
|
+
# batch helpers
|
|
92
|
+
"results_to_dataframe",
|
|
93
|
+
]
|
dfbayes/_core.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Shared numerical and validation helpers.
|
|
2
|
+
|
|
3
|
+
The port deliberately mirrors R's scalar behaviour, including division by zero
|
|
4
|
+
(``Inf``/``-Inf``/``NaN``) and the fact that ``is.na(NaN)`` is TRUE.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from scipy.special import betainc, betaincinv, betaln
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# NA / NaN
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
def is_na(value: Any) -> bool:
|
|
20
|
+
"""R's ``is.na``: TRUE for both NA (None) and NaN."""
|
|
21
|
+
if value is None:
|
|
22
|
+
return True
|
|
23
|
+
try:
|
|
24
|
+
return bool(np.isnan(value))
|
|
25
|
+
except (TypeError, ValueError):
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def as_float_vector(values: Any, name: str = "values") -> np.ndarray:
|
|
30
|
+
"""Convert a sequence to a 1-D float array, mapping None to NaN.
|
|
31
|
+
|
|
32
|
+
This is the input-side equivalent of R's numeric vector coercion; ``None``
|
|
33
|
+
and ``NaN`` both become ``NaN`` and are treated as missing.
|
|
34
|
+
"""
|
|
35
|
+
if values is None:
|
|
36
|
+
arr = np.array([np.nan], dtype=float)
|
|
37
|
+
elif isinstance(values, np.ndarray):
|
|
38
|
+
arr = values.astype(float, copy=False)
|
|
39
|
+
if arr.ndim == 0:
|
|
40
|
+
arr = arr.reshape(1)
|
|
41
|
+
else:
|
|
42
|
+
try:
|
|
43
|
+
arr = np.asarray(list(values), dtype=float)
|
|
44
|
+
except TypeError:
|
|
45
|
+
# R treats a scalar as a length-1 vector.
|
|
46
|
+
arr = np.array([float(values)], dtype=float)
|
|
47
|
+
if arr.ndim != 1:
|
|
48
|
+
raise ValueError(f"{name} must be a one-dimensional vector")
|
|
49
|
+
return arr
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def as_scalar(value: Any, name: str) -> float:
|
|
53
|
+
"""Coerce a scalar argument to float, mapping None to NaN."""
|
|
54
|
+
if value is None:
|
|
55
|
+
return float("nan")
|
|
56
|
+
try:
|
|
57
|
+
return float(value)
|
|
58
|
+
except (TypeError, ValueError) as exc:
|
|
59
|
+
raise ValueError(f"{name} must be a single numeric value") from exc
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def rdiv(numerator: float, denominator: float) -> float:
|
|
63
|
+
"""Division with R's semantics: 0/0 -> NaN, x/0 -> +/-Inf."""
|
|
64
|
+
num = float(numerator)
|
|
65
|
+
den = float(denominator)
|
|
66
|
+
if math.isnan(num) or math.isnan(den):
|
|
67
|
+
return float("nan")
|
|
68
|
+
if den == 0.0:
|
|
69
|
+
if num == 0.0:
|
|
70
|
+
return float("nan")
|
|
71
|
+
return math.copysign(math.inf, num)
|
|
72
|
+
return num / den
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def r_pow(x: float, y: float) -> float:
|
|
76
|
+
"""R's ``^`` for the cases used upstream.
|
|
77
|
+
|
|
78
|
+
R evaluates a non-negative integer power by repeated multiplication
|
|
79
|
+
(``x^4`` is ``((x*x)*x)*x``), not libm ``pow``. The two differ by one ulp
|
|
80
|
+
often enough to matter: the Mann-Whitney polynomial subtracts terms of
|
|
81
|
+
order 1e3 to obtain a result of order 1e-2, so a one-ulp change in ``x^4``
|
|
82
|
+
is amplified to ~1e-12 in the output.
|
|
83
|
+
"""
|
|
84
|
+
xf = float(x)
|
|
85
|
+
yf = float(y)
|
|
86
|
+
if yf >= 0.0 and yf.is_integer():
|
|
87
|
+
result = 1.0
|
|
88
|
+
for _ in range(int(yf)):
|
|
89
|
+
result *= xf
|
|
90
|
+
return result
|
|
91
|
+
return xf ** yf
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def rseq(start: float, stop: float, by: float, *, n: int | None = None) -> np.ndarray:
|
|
95
|
+
"""Approximate R's ``seq`` for the fixed grids used upstream.
|
|
96
|
+
|
|
97
|
+
Upstream only uses ``seq`` with a known number of points (201, 1001, 200,
|
|
98
|
+
101), so callers pass ``n`` and we use ``linspace``. The endpoint is
|
|
99
|
+
included, matching R.
|
|
100
|
+
"""
|
|
101
|
+
if n is None:
|
|
102
|
+
raise TypeError("rseq requires n")
|
|
103
|
+
return np.linspace(start, stop, n)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Beta distribution functions (R's pbeta/qbeta/dbeta equivalents)
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def pbeta(q: float, a: float, b: float) -> float:
|
|
111
|
+
if is_na(q) or is_na(a) or is_na(b):
|
|
112
|
+
return float("nan")
|
|
113
|
+
return float(betainc(a, b, q))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def pbeta_vec(q: np.ndarray, a: float, b: float) -> np.ndarray:
|
|
117
|
+
return betainc(a, b, np.asarray(q, dtype=float))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def qbeta(p: float, a: float, b: float) -> float:
|
|
121
|
+
if is_na(p) or is_na(a) or is_na(b):
|
|
122
|
+
return float("nan")
|
|
123
|
+
return float(betaincinv(a, b, p))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def qbeta_vec(p: np.ndarray, a: float, b: float) -> np.ndarray:
|
|
127
|
+
return betaincinv(a, b, np.asarray(p, dtype=float))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def dbeta(x: float, a: float, b: float) -> float:
|
|
131
|
+
"""Beta density, including R's boundary conventions."""
|
|
132
|
+
if is_na(x) or is_na(a) or is_na(b):
|
|
133
|
+
return float("nan")
|
|
134
|
+
x = float(x)
|
|
135
|
+
if x < 0.0 or x > 1.0:
|
|
136
|
+
return 0.0
|
|
137
|
+
if x == 0.0:
|
|
138
|
+
if a < 1.0:
|
|
139
|
+
return math.inf
|
|
140
|
+
if a == 1.0:
|
|
141
|
+
return float(b)
|
|
142
|
+
return 0.0
|
|
143
|
+
if x == 1.0:
|
|
144
|
+
if b < 1.0:
|
|
145
|
+
return math.inf
|
|
146
|
+
if b == 1.0:
|
|
147
|
+
return float(a)
|
|
148
|
+
return 0.0
|
|
149
|
+
log_density = (a - 1.0) * math.log(x) + (b - 1.0) * math.log1p(-x) - float(betaln(a, b))
|
|
150
|
+
return math.exp(log_density)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Validation helpers
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def check_positive_finite(a: Any, b: Any, message: str) -> tuple[float, float]:
|
|
158
|
+
av = as_scalar(a, "a")
|
|
159
|
+
bv = as_scalar(b, "b")
|
|
160
|
+
for value in (av, bv):
|
|
161
|
+
if math.isnan(value) or math.isinf(value) or value <= 0.0:
|
|
162
|
+
raise ValueError(message)
|
|
163
|
+
return av, bv
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def check_prob_interval(value: Any, *, allow_zero_one: bool, message: str) -> float:
|
|
167
|
+
p = as_scalar(value, "prob_interval")
|
|
168
|
+
if math.isnan(p):
|
|
169
|
+
# R reaches `if (NA)` and raises a base error, not the custom stop().
|
|
170
|
+
raise ValueError("missing value where TRUE/FALSE needed")
|
|
171
|
+
if allow_zero_one:
|
|
172
|
+
if p < 0.0 or p > 1.0:
|
|
173
|
+
raise ValueError(message)
|
|
174
|
+
else:
|
|
175
|
+
if p <= 0.0 or p >= 1.0:
|
|
176
|
+
raise ValueError(message)
|
|
177
|
+
return p
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def check_nonnegative_count(
|
|
181
|
+
value: Any,
|
|
182
|
+
name: str,
|
|
183
|
+
negative_message: str,
|
|
184
|
+
integer_message: str,
|
|
185
|
+
) -> int | float:
|
|
186
|
+
"""Validate a count in R's order: negativity/NA first, integrality second.
|
|
187
|
+
|
|
188
|
+
Preserves whether the caller supplied an int or a float, matching R's
|
|
189
|
+
observable ``integer`` vs ``double`` distinction for inputs.
|
|
190
|
+
"""
|
|
191
|
+
if value is None:
|
|
192
|
+
raise ValueError(negative_message)
|
|
193
|
+
try:
|
|
194
|
+
numeric = float(value)
|
|
195
|
+
except (TypeError, ValueError) as exc:
|
|
196
|
+
raise ValueError(negative_message) from exc
|
|
197
|
+
if math.isnan(numeric) or numeric < 0.0:
|
|
198
|
+
raise ValueError(negative_message)
|
|
199
|
+
if numeric != round(numeric):
|
|
200
|
+
raise ValueError(integer_message)
|
|
201
|
+
if isinstance(value, (int, np.integer)):
|
|
202
|
+
return int(value)
|
|
203
|
+
return numeric
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def result_fields(obj: Any) -> list[tuple[str, Any]]:
|
|
207
|
+
"""Return (name, value) pairs in R slot order."""
|
|
208
|
+
return [(name, getattr(obj, name)) for name in obj.R_FIELDS]
|
dfbayes/beta.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Beta descriptive statistics and Bayes factors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ._core import (
|
|
11
|
+
as_scalar,
|
|
12
|
+
check_positive_finite,
|
|
13
|
+
check_prob_interval,
|
|
14
|
+
dbeta,
|
|
15
|
+
pbeta,
|
|
16
|
+
qbeta,
|
|
17
|
+
qbeta_vec,
|
|
18
|
+
r_pow,
|
|
19
|
+
rdiv,
|
|
20
|
+
)
|
|
21
|
+
from .results import (
|
|
22
|
+
BetaDescriptiveOut,
|
|
23
|
+
BetaOutputDF,
|
|
24
|
+
IntervalBFOut,
|
|
25
|
+
PointBFOut,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def dfba_beta_descriptive(
|
|
30
|
+
a: float,
|
|
31
|
+
b: float,
|
|
32
|
+
prob_interval: float = 0.95,
|
|
33
|
+
) -> BetaDescriptiveOut:
|
|
34
|
+
"""Descriptive statistics for a beta distribution.
|
|
35
|
+
|
|
36
|
+
Mirrors ``DFBA::dfba_beta_descriptive``. An undefined mode or HDI is
|
|
37
|
+
returned as ``None`` (R stores a logical ``NA`` there).
|
|
38
|
+
"""
|
|
39
|
+
pi = check_prob_interval(
|
|
40
|
+
prob_interval,
|
|
41
|
+
allow_zero_one=True,
|
|
42
|
+
message="The probability for the interval estimate must be between 0 and 1.",
|
|
43
|
+
)
|
|
44
|
+
av, bv = check_positive_finite(
|
|
45
|
+
a, b,
|
|
46
|
+
"Both the a and b shape parameters for a beta must be positive and finite",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
phimean = av / (av + bv)
|
|
50
|
+
phivariance = (av * bv) / (r_pow(av + bv, 2) * (av + bv + 1))
|
|
51
|
+
phimedian = qbeta(0.5, av, bv)
|
|
52
|
+
|
|
53
|
+
undefined_mode = (av == 1.0 and bv == 1.0) or av < 1.0 or bv < 1.0
|
|
54
|
+
phimode = None if undefined_mode else (av - 1.0) / (av + bv - 2.0)
|
|
55
|
+
|
|
56
|
+
qlequal = qbeta((1.0 - pi) / 2.0, av, bv)
|
|
57
|
+
qhequal = qbeta(pi + ((1.0 - pi) / 2.0), av, bv)
|
|
58
|
+
|
|
59
|
+
if undefined_mode:
|
|
60
|
+
qlmin = None
|
|
61
|
+
qhmax = None
|
|
62
|
+
else:
|
|
63
|
+
# R: seq(0, 1 - prob_interval, (1 - prob_interval)/1000)
|
|
64
|
+
alpha_l = np.linspace(0.0, 1.0 - pi, 1001)
|
|
65
|
+
ql = qbeta_vec(alpha_l, av, bv)
|
|
66
|
+
qh = qbeta_vec(pi + alpha_l, av, bv)
|
|
67
|
+
diff = qh - ql
|
|
68
|
+
index = int(np.argmin(diff)) # first index attaining the minimum
|
|
69
|
+
qlmin = float(ql[index])
|
|
70
|
+
qhmax = float(qh[index])
|
|
71
|
+
|
|
72
|
+
# R: seq(0, 1, .005) -> 201 points
|
|
73
|
+
x = np.arange(201, dtype=float) * 0.005
|
|
74
|
+
density = np.array([dbeta(float(xi), av, bv) for xi in x])
|
|
75
|
+
cumulative = np.array(
|
|
76
|
+
[pbeta(float(xi), av, bv) for xi in x]
|
|
77
|
+
)
|
|
78
|
+
outputdf = BetaOutputDF(x=x, density=density, cumulative_prob=cumulative)
|
|
79
|
+
|
|
80
|
+
return BetaDescriptiveOut(
|
|
81
|
+
a=av,
|
|
82
|
+
b=bv,
|
|
83
|
+
prob_interval=pi,
|
|
84
|
+
x_mean=phimean,
|
|
85
|
+
x_median=phimedian,
|
|
86
|
+
x_mode=phimode,
|
|
87
|
+
x_variance=phivariance,
|
|
88
|
+
eti_lower=qlequal,
|
|
89
|
+
eti_upper=qhequal,
|
|
90
|
+
hdi_lower=qlmin,
|
|
91
|
+
hdi_upper=qhmax,
|
|
92
|
+
outputdf=outputdf,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def dfba_beta_bayes_factor(
|
|
97
|
+
a_post: float,
|
|
98
|
+
b_post: float,
|
|
99
|
+
method: str,
|
|
100
|
+
H0: Any,
|
|
101
|
+
a0: float = 1.0,
|
|
102
|
+
b0: float = 1.0,
|
|
103
|
+
) -> PointBFOut | IntervalBFOut:
|
|
104
|
+
"""Bayes factor for a point or interval null hypothesis."""
|
|
105
|
+
if method != "point" and method != "interval":
|
|
106
|
+
raise ValueError("method must be either 'point' or 'interval'")
|
|
107
|
+
|
|
108
|
+
a0v, b0v = check_positive_finite(
|
|
109
|
+
a0, b0, "Both a0 and b0 must be positive and finite."
|
|
110
|
+
)
|
|
111
|
+
a_postv = as_scalar(a_post, "a_post")
|
|
112
|
+
b_postv = as_scalar(b_post, "b_post")
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
a_postv < a0v
|
|
116
|
+
or b_postv < b0v
|
|
117
|
+
or math.isnan(a_postv)
|
|
118
|
+
or math.isnan(b_postv)
|
|
119
|
+
):
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"Both a_post and b_post cannot be less than the respective a0 and b0 values"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if method == "point":
|
|
125
|
+
# R checks length(H0), so a length-1 vector is accepted as well as a
|
|
126
|
+
# scalar.
|
|
127
|
+
if np.ndim(H0) == 1 and len(H0) == 1:
|
|
128
|
+
H0 = H0[0]
|
|
129
|
+
elif np.ndim(H0) != 0:
|
|
130
|
+
raise ValueError("'H0' must be a single numeric value when method = 'point'")
|
|
131
|
+
if H0 is None:
|
|
132
|
+
raise ValueError("missing value where TRUE/FALSE needed")
|
|
133
|
+
h0 = as_scalar(H0, "H0")
|
|
134
|
+
if math.isnan(h0):
|
|
135
|
+
raise ValueError("missing value where TRUE/FALSE needed")
|
|
136
|
+
if h0 > 1.0 or h0 < 0.0:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
"H0 must be greater than or equal to 0 and less than or equal to 1"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
dprior = dbeta(h0, a0v, b0v)
|
|
142
|
+
dpost = dbeta(h0, a_postv, b_postv)
|
|
143
|
+
bf10 = math.inf if dpost == 0.0 else rdiv(dprior, dpost)
|
|
144
|
+
bf01 = math.inf if dprior == 0.0 else rdiv(dpost, dprior)
|
|
145
|
+
|
|
146
|
+
return PointBFOut(
|
|
147
|
+
method=method,
|
|
148
|
+
a_post=a_postv,
|
|
149
|
+
b_post=b_postv,
|
|
150
|
+
a0=a0v,
|
|
151
|
+
b0=b0v,
|
|
152
|
+
BF10=bf10,
|
|
153
|
+
BF01=bf01,
|
|
154
|
+
null_hypothesis=h0,
|
|
155
|
+
dpriorH0=dprior,
|
|
156
|
+
dpostH0=dpost,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# interval
|
|
160
|
+
if np.ndim(H0) != 1 or len(H0) != 2:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
"'H0' must be a vector of two values (lower and upper) when method = 'interval'"
|
|
163
|
+
)
|
|
164
|
+
if any(v is None for v in H0):
|
|
165
|
+
raise ValueError("missing value where TRUE/FALSE needed")
|
|
166
|
+
lower = as_scalar(H0[0], "H0[0]")
|
|
167
|
+
upper = as_scalar(H0[1], "H0[1]")
|
|
168
|
+
if math.isnan(lower) or math.isnan(upper):
|
|
169
|
+
raise ValueError("missing value where TRUE/FALSE needed")
|
|
170
|
+
if any(v > 1.0 or v < 0.0 for v in (lower, upper)):
|
|
171
|
+
raise ValueError(
|
|
172
|
+
"H0 interval limits must be greater than or equal to 0 and less than or equal to 1"
|
|
173
|
+
)
|
|
174
|
+
if lower >= upper:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
"When method = 'interval', H0 upper limit must be greater than H0 lower limit"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
ph0 = pbeta(upper, a0v, b0v) - pbeta(lower, a0v, b0v)
|
|
180
|
+
ph1 = 1.0 - ph0
|
|
181
|
+
post_h0 = pbeta(upper, a_postv, b_postv) - pbeta(lower, a_postv, b_postv)
|
|
182
|
+
post_h1 = 1.0 - post_h0
|
|
183
|
+
|
|
184
|
+
bf10 = (
|
|
185
|
+
math.inf
|
|
186
|
+
if post_h0 == 0.0
|
|
187
|
+
else rdiv(rdiv(1.0, post_h0) - 1.0, rdiv(1.0, ph0) - 1.0)
|
|
188
|
+
)
|
|
189
|
+
bf01 = (
|
|
190
|
+
math.inf
|
|
191
|
+
if post_h1 == 0.0
|
|
192
|
+
else rdiv(rdiv(1.0, ph0) - 1.0, rdiv(1.0, post_h0) - 1.0)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return IntervalBFOut(
|
|
196
|
+
method=method,
|
|
197
|
+
a_post=a_postv,
|
|
198
|
+
b_post=b_postv,
|
|
199
|
+
a0=a0v,
|
|
200
|
+
b0=b0v,
|
|
201
|
+
BF10=bf10,
|
|
202
|
+
BF01=bf01,
|
|
203
|
+
H0lower=lower,
|
|
204
|
+
H0upper=upper,
|
|
205
|
+
pH0=ph0,
|
|
206
|
+
pH1=ph1,
|
|
207
|
+
postH0=post_h0,
|
|
208
|
+
postH1=post_h1,
|
|
209
|
+
)
|
dfbayes/binomial.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Bayesian binomial rate inference."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._core import check_nonnegative_count, check_positive_finite, check_prob_interval
|
|
6
|
+
from .beta import dfba_beta_descriptive
|
|
7
|
+
from .results import BinomialOut
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def dfba_binomial(
|
|
11
|
+
n1: float,
|
|
12
|
+
n2: float,
|
|
13
|
+
a0: float = 1.0,
|
|
14
|
+
b0: float = 1.0,
|
|
15
|
+
prob_interval: float = 0.95,
|
|
16
|
+
) -> BinomialOut:
|
|
17
|
+
"""Bayesian inference for a binomial rate parameter.
|
|
18
|
+
|
|
19
|
+
``n1`` and ``n2`` must be non-negative integers; ``a0``/``b0`` must be
|
|
20
|
+
positive and finite. NA/NaN anywhere raises ``ValueError``, matching R.
|
|
21
|
+
"""
|
|
22
|
+
a0v, b0v = check_positive_finite(
|
|
23
|
+
a0, b0, "Both a0 and b0 must be positive and finite"
|
|
24
|
+
)
|
|
25
|
+
pi = check_prob_interval(
|
|
26
|
+
prob_interval,
|
|
27
|
+
allow_zero_one=False,
|
|
28
|
+
message="prob_interval must be greater than 0 and less than 1",
|
|
29
|
+
)
|
|
30
|
+
n1v = check_nonnegative_count(
|
|
31
|
+
n1, "n1", "Neither n1 nor n2 can be negative", "n1 and n2 must be integers"
|
|
32
|
+
)
|
|
33
|
+
n2v = check_nonnegative_count(
|
|
34
|
+
n2, "n2", "Neither n1 nor n2 can be negative", "n1 and n2 must be integers"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
a_post = n1v + a0v
|
|
38
|
+
b_post = n2v + b0v
|
|
39
|
+
|
|
40
|
+
descriptive = dfba_beta_descriptive(a_post, b_post, prob_interval=pi)
|
|
41
|
+
|
|
42
|
+
return BinomialOut(
|
|
43
|
+
n1=n1v,
|
|
44
|
+
n2=n2v,
|
|
45
|
+
a0=a0v,
|
|
46
|
+
b0=b0v,
|
|
47
|
+
prob_interval=pi,
|
|
48
|
+
a_post=a_post,
|
|
49
|
+
b_post=b_post,
|
|
50
|
+
phimean=descriptive.x_mean,
|
|
51
|
+
phimedian=descriptive.x_median,
|
|
52
|
+
phimode=descriptive.x_mode,
|
|
53
|
+
eti_lower=descriptive.eti_lower,
|
|
54
|
+
eti_upper=descriptive.eti_upper,
|
|
55
|
+
hdi_lower=descriptive.hdi_lower,
|
|
56
|
+
hdi_upper=descriptive.hdi_upper,
|
|
57
|
+
)
|