dfbayes 0.1.0__tar.gz

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.
Files changed (40) hide show
  1. dfbayes-0.1.0/.gitignore +16 -0
  2. dfbayes-0.1.0/.python-version +1 -0
  3. dfbayes-0.1.0/AUTHORS +16 -0
  4. dfbayes-0.1.0/COVERAGE.md +220 -0
  5. dfbayes-0.1.0/DECISION.md +298 -0
  6. dfbayes-0.1.0/LICENSE +338 -0
  7. dfbayes-0.1.0/NOTICE +22 -0
  8. dfbayes-0.1.0/PKG-INFO +271 -0
  9. dfbayes-0.1.0/README.md +235 -0
  10. dfbayes-0.1.0/justfile +32 -0
  11. dfbayes-0.1.0/pyproject.toml +83 -0
  12. dfbayes-0.1.0/src/dfbayes/__init__.py +93 -0
  13. dfbayes-0.1.0/src/dfbayes/_core.py +208 -0
  14. dfbayes-0.1.0/src/dfbayes/beta.py +209 -0
  15. dfbayes-0.1.0/src/dfbayes/binomial.py +57 -0
  16. dfbayes-0.1.0/src/dfbayes/concordance.py +219 -0
  17. dfbayes-0.1.0/src/dfbayes/contrast.py +191 -0
  18. dfbayes-0.1.0/src/dfbayes/mann_whitney.py +396 -0
  19. dfbayes-0.1.0/src/dfbayes/mcnemar.py +80 -0
  20. dfbayes-0.1.0/src/dfbayes/median_test.py +89 -0
  21. dfbayes-0.1.0/src/dfbayes/plotting.py +287 -0
  22. dfbayes-0.1.0/src/dfbayes/py.typed +0 -0
  23. dfbayes-0.1.0/src/dfbayes/results.py +890 -0
  24. dfbayes-0.1.0/src/dfbayes/sign_test.py +105 -0
  25. dfbayes-0.1.0/src/dfbayes/simulation.py +507 -0
  26. dfbayes-0.1.0/src/dfbayes/wilcoxon.py +340 -0
  27. dfbayes-0.1.0/tests/__init__.py +1 -0
  28. dfbayes-0.1.0/tests/_support.py +171 -0
  29. dfbayes-0.1.0/tests/cases.py +306 -0
  30. dfbayes-0.1.0/tests/fixtures/fixtures.json +1 -0
  31. dfbayes-0.1.0/tests/fixtures/generate.R +847 -0
  32. dfbayes-0.1.0/tests/fixtures/inputs.json +1 -0
  33. dfbayes-0.1.0/tests/fixtures/stochastic.json +1 -0
  34. dfbayes-0.1.0/tests/test_deterministic.py +44 -0
  35. dfbayes-0.1.0/tests/test_package.py +21 -0
  36. dfbayes-0.1.0/tests/test_plotting.py +139 -0
  37. dfbayes-0.1.0/tests/test_results.py +408 -0
  38. dfbayes-0.1.0/tests/test_simulation.py +413 -0
  39. dfbayes-0.1.0/tests/test_stochastic.py +377 -0
  40. dfbayes-0.1.0/uv.lock +1785 -0
@@ -0,0 +1,16 @@
1
+ # Python
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .pytest_cache/
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+
10
+ # Upstream R sources are read-only inputs, not part of the port
11
+ upstream/
12
+
13
+ # R / fixture scratch
14
+ .Rlib/
15
+ Rplots.pdf
16
+ tests/fixtures/*.tmp
@@ -0,0 +1 @@
1
+ 3.12
dfbayes-0.1.0/AUTHORS ADDED
@@ -0,0 +1,16 @@
1
+ Upstream DFBA
2
+ =============
3
+
4
+ Daniel H. Barch <daniel.barch@tufts.edu>
5
+ Richard A. Chechile <richard.chechile@tufts.edu>
6
+ Tufts University
7
+
8
+ CRAN package DFBA 0.1.0, published 2023-12-13:
9
+ https://CRAN.R-project.org/package=DFBA
10
+
11
+ Python port
12
+ ===========
13
+
14
+ dfbayes contributors
15
+
16
+ See NOTICE for provenance and licence details.
@@ -0,0 +1,220 @@
1
+ # Coverage map — R source branch to test
2
+
3
+ Fixture case names refer to `tests/fixtures/fixtures.json`; stochastic case
4
+ names refer to `tests/fixtures/stochastic.json`. The deterministic suite runs
5
+ every fixture in `tests/test_deterministic.py`; the stochastic suite is
6
+ `tests/test_stochastic.py`; Phase 2 model/power behaviour is in
7
+ `tests/test_simulation.py`.
8
+
9
+ `upstream/` is read-only. Expected values are produced by running R DFBA 0.1.0,
10
+ never by hand.
11
+
12
+ ## `dfba_binomial` (`dfba_binomial.R`)
13
+
14
+ | R branch | Tests |
15
+ | --- | --- |
16
+ | default uniform prior | `binomial_default`, `binomial_zero_zero` |
17
+ | non-default prior (`a0`, `b0`) | `binomial_prior_2_3`, `binomial_jeffreys_99` |
18
+ | `prob_interval` 0.5 / 0.99 / 0.999 | `binomial_half_interval`, `binomial_jeffreys_99`, `binomial_interval_999` |
19
+ | all successes / all failures | `binomial_all_success`, `binomial_all_fail` |
20
+ | large counts | `binomial_large` |
21
+ | negative / non-integer counts | `binomial_err_n1_neg`, `binomial_err_n2_neg`, `binomial_err_n1_nonint`, `binomial_err_n2_nonint` |
22
+ | non-positive / infinite / NA prior | `binomial_err_a0_zero`, `binomial_err_b0_zero`, `binomial_err_a0_inf`, `binomial_err_b0_inf`, `binomial_err_a0_na`, `binomial_err_b0_na` |
23
+ | NA counts | `binomial_err_n1_na`, `binomial_err_n2_na` |
24
+ | `prob_interval` at 0 / 1 / NA | `binomial_err_pi_zero`, `binomial_err_pi_one`, `binomial_err_pi_na` |
25
+
26
+ ## `dfba_beta_descriptive` (`dfba_beta_descriptive.R`)
27
+
28
+ | R branch | Tests |
29
+ | --- | --- |
30
+ | regular mode/HDI | `bd_38_55`, `bd_38_55_90`, `bd_38_55_99`, `bd_large` |
31
+ | mode undefined (`a==1 & b==1`, or both ≤ 1) | `bd_1_1`, `bd_2_2`, `bd_1_2` |
32
+ | mode defined with one shape ≤ 1 | `bd_half_2`, `bd_2_half` |
33
+ | `prob_interval` 0.5 | `bd_interval_half` |
34
+ | non-positive / infinite / NA shape | `bd_err_a_zero`, `bd_err_b_zero`, `bd_err_a_inf`, `bd_err_b_inf`, `bd_err_a_na`, `bd_err_b_na` |
35
+ | `prob_interval` < 0 / > 1 / NA | `bd_err_pi_neg`, `bd_err_pi_gt1`, `bd_err_pi_na` |
36
+
37
+ ## `dfba_beta_bayes_factor` (`dfba_beta_bayes_factor.R`)
38
+
39
+ | R branch | Tests |
40
+ | --- | --- |
41
+ | interval H0 | `bf_interval`, `bf_interval_narrow`, `bf_interval_jeffreys`, `bf_interval_full` |
42
+ | point H0 | `bf_point`, `bf_point_jeffreys`, `bf_point_zero`, `bf_point_one` |
43
+ | point density 0 → `BF10=NaN` / `BF01=Inf` | `bf_point_zero`, `bf_point_one` |
44
+ | interval H0 = (0,1) → `BF10=NaN` / `BF01=Inf` | `bf_interval_full` |
45
+ | bad method | `bf_err_method` |
46
+ | point H0 length / out of range | `bf_err_point_len2`, `bf_err_point_gt1`, `bf_err_point_neg` |
47
+ | interval H0 length / range / order | `bf_err_interval_len1`, `bf_err_interval_gt1`, `bf_err_interval_order` |
48
+ | prior validation and posterior below prior | `bf_err_a0_zero`, `bf_err_a_post_lt_a0`, `bf_err_b_post_lt_b0`, `bf_err_a_post_na` |
49
+
50
+ ## `dfba_sign_test` (`dfba_sign_test.R`)
51
+
52
+ | R branch | Tests |
53
+ | --- | --- |
54
+ | positive/negative sign counts | `sign_default`, `sign_jeffreys_99` |
55
+ | zero differences removed | `sign_with_zeros`, `sign_three_pairs` |
56
+ | pairwise NA removal | `sign_with_na`, `sign_nan` |
57
+ | `prob_interval=0` | `sign_pi_zero` |
58
+ | BF always uses the uniform prior (upstream bug) | `sign_jeffreys_99` |
59
+ | length mismatch / too few / all trivial | `sign_err_length`, `sign_err_too_few`, `sign_err_all_zero` |
60
+ | NA prior / invalid interval | `sign_err_a0_na`, `sign_err_b0_na`, `sign_err_pi_neg`, `sign_err_pi_gt1`, `sign_err_pi_na` |
61
+
62
+ ## `dfba_median_test` (`dfba_median_test.R`)
63
+
64
+ | R branch | Tests |
65
+ | --- | --- |
66
+ | E > C and C > E counts | `med_example`, `med_jeffreys` |
67
+ | ties | `med_ties`, `med_all_equal` |
68
+ | independent NA removal | `med_na` |
69
+ | one/both vectors empty | `med_one_empty`, `med_both_empty` |
70
+ | invalid prior | `med_err_a0_na`, `med_err_b0_na`, `med_err_a0_zero`, `med_err_b0_inf` |
71
+
72
+ ## `dfba_gamma` (`dfba_gamma.R`)
73
+
74
+ | R branch | Tests |
75
+ | --- | --- |
76
+ | matrix/table input | `gamma_example`, `gamma_table`, `gamma_2x2` |
77
+ | zero cells | `gamma_zeros` |
78
+ | 1×1, 1×N, N×1 | `gamma_1x1`, `gamma_1xN`, `gamma_Nx1` |
79
+ | NA cell → `invalid 'times' argument` | `gamma_na` |
80
+ | non-matrix input | `gamma_not_matrix` |
81
+ | invalid prior / interval | `gamma_err_a0_na`, `gamma_err_b0_zero`, `gamma_err_pi_99` |
82
+
83
+ ## `dfba_mcnemar` (`dfba_mcnemar.R`)
84
+
85
+ | R branch | Tests |
86
+ | --- | --- |
87
+ | `n_01 > n_10`, `n_10 > n_01` | `mc_default`, `mc_all_01`, `mc_all_10` |
88
+ | both zero | `mc_zero_zero` |
89
+ | large counts and cancellation | `mc_large` |
90
+ | Jeffreys prior / 99% interval | `mc_jeffreys_99` |
91
+ | negative / non-integer / NA counts | `mc_err_neg`, `mc_err_nonint`, `mc_err_na` |
92
+ | invalid prior / interval | `mc_err_a0_na`, `mc_err_pi_zero`, `mc_err_pi_one`, `mc_err_pi_na` |
93
+
94
+ ## `dfba_bivariate_concordance` (`dfba_bivariate_concordance.R`)
95
+
96
+ | R branch | Tests |
97
+ | --- | --- |
98
+ | `nc`, `nd`, `tau` from concordant/discordant pairs | `bc_example`, `bc_jeffreys`, `bc_pi_99` |
99
+ | tied pairs removed from `nc`/`nd` | `bc_ties_doc` |
100
+ | all equal / empty / single pair / two pairs | `bc_all_equal`, `bc_empty`, `bc_single_pair`, `bc_two_pairs` |
101
+ | pairwise NA row removal warning | `bc_na_rows` |
102
+ | NA `prob_interval` propagates | `bc_pi_na` |
103
+ | `fitting_parameters` 0 / 2 / 3 / NA | `bc_star_0`, `bc_star_2`, `bc_star_3`, `bc_star_na` |
104
+ | invalid prior / unequal lengths | `bc_err_a0_na`, `bc_err_b0_zero`, `bc_err_length` |
105
+
106
+ The star class has R's duplicated `prob_interval` slot; see `DECISION.md`.
107
+
108
+ ## `dfba_mann_whitney` (`dfba_mann_whitney.R`)
109
+
110
+ | R branch | Tests |
111
+ | --- | --- |
112
+ | `method=NULL`, `nH >= 20` → large | `mw_large_example`, `test_method_none_selects_at_the_nH_20_boundary[20-20-large]` |
113
+ | `method=NULL`, `nH < 20` → small | `test_method_none_selects_at_the_nH_20_boundary[19-21-small]`; forced small `mw_small_branch_*` |
114
+ | explicit `method="large"` | `mw_large_forced_small_data`, `mw_large_forced_19` |
115
+ | explicit `method="small"` | `mw_small_branch_a`, `mw_small_branch_b` (stochastic) |
116
+ | large branch `xs >= .5` | `mw_large_example`, `mw_large_jeffreys`, `mw_large_99` |
117
+ | large branch `xs < .5` (swap a/b) | `mw_large_reversed` |
118
+ | `nH` exactly at the 20 boundary | `mw_large_nH_boundary` |
119
+ | independent NA cell removal + messages | `mw_large_na` |
120
+ | all values equal → `missing value where TRUE/FALSE needed` | `mw_large_all_equal` |
121
+ | small branch A (`length(E)*length(C) > Ntot`) | `mw_small_branch_a` (stochastic, frozen `fomega`) |
122
+ | small branch B (`length(E)*length(C) <= Ntot`) | `mw_small_branch_b` (stochastic, frozen `fomega`) |
123
+ | `samples < 10000` message → warning | `mw_large_low_samples` |
124
+ | invalid prior / interval / method | `mw_err_a0_na`, `mw_err_b0_zero`, `mw_err_pi_neg`, `mw_err_pi_gt1`, `mw_err_pi_na`, `mw_err_method` |
125
+ | empty E / C | `mw_err_empty_e`, `mw_err_empty_c` |
126
+ | analytic null at `omega=0.5` (branch A and B) | `test_mw_fomega_matches_null_branch_a/b` |
127
+ | sampler determinism | `test_mw_sampler_is_deterministic` |
128
+ | public wrapper shape/range | `test_mw_small_public_wrapper` |
129
+
130
+ ## `dfba_wilcoxon` (`dfba_wilcoxon.R`)
131
+
132
+ | R branch | Tests |
133
+ | --- | --- |
134
+ | `method=NULL`, `n > 24` → large | `wil_large_example` |
135
+ | explicit `method="large"` | `wil_large_forced_small` |
136
+ | explicit `method="small"` | `wil_small` (stochastic, frozen `fphi`) |
137
+ | ties get average ranks | `wil_large_ties` |
138
+ | zero/trivial differences removed | `wil_large_zero_diffs` |
139
+ | pairwise NA removal | `wil_large_na` |
140
+ | Jeffreys prior / 99% interval | `wil_large_jeffreys`, `wil_large_99` |
141
+ | `samples < 10000` message → warning | `wil_large_low_samples` |
142
+ | length mismatch / too few / all trivial | `wil_err_length`, `wil_err_too_few`, `wil_err_all_equal` |
143
+ | invalid prior / interval / method | `wil_err_a0_na`, `wil_err_b0_zero`, `wil_err_pi_neg`, `wil_err_pi_gt1`, `wil_err_pi_na`, `wil_err_method` |
144
+ | analytic null at `phi=0.5` (Poisson binomial) | `test_wilcoxon_fphi_matches_poisson_binomial_null` |
145
+ | sampler determinism | `test_wilcoxon_sampler_is_deterministic` |
146
+ | average tie ranks used by `tpos`/`tneg` | `test_average_ranks_matches_scipy_including_ties` |
147
+ | public wrapper shape/range | `test_wilcoxon_small_public_wrapper` |
148
+
149
+ ## `dfba_beta_contrast` (`dfba_beta_contrast.R`)
150
+
151
+ | R branch | Tests |
152
+ | --- | --- |
153
+ | two conditions, `delta`/`delta0` frozen | `beta_contrast_2` (stochastic) |
154
+ | four conditions / orthogonal contrast | `beta_contrast_4` (stochastic) |
155
+ | `prH1==1 | priorprH1==0` shortcut | `_finish_beta_contrast` unit path; public smoke |
156
+ | `samples < 10000` / NA samples | `bcon_err_samples_low`, `bcon_err_samples_na` |
157
+ | vector length mismatch | `bcon_err_length` |
158
+ | non-integer / NA / negative counts | `bcon_err_n1_nonint`, `bcon_err_n1_na`, `bcon_err_n2_neg` |
159
+ | invalid prior | `bcon_err_a0_zero`, `bcon_err_a0_inf`, `bcon_err_a0_na`, `bcon_err_b0_na` |
160
+ | positive/negative contrast sums | `bcon_err_contrast_pos`, `bcon_err_contrast_neg` |
161
+ | NA contrast / invalid interval | `bcon_err_contrast_na`, `bcon_err_pi_neg`, `bcon_err_pi_na` |
162
+ | `a0_vec`/`b0_vec` length-1 broadcast (documented divergence) | public wrapper + `DECISION.md` |
163
+ | sampler determinism | `test_beta_contrast_sampler_is_deterministic` |
164
+ | public wrapper shape/range | `test_beta_contrast_public_wrapper` |
165
+
166
+ ## `dfba_sim_data` (`dfba_sim_data.R`)
167
+
168
+ | R branch | Tests |
169
+ | --- | --- |
170
+ | negative delta | `sim_err_delta_neg` |
171
+ | `n < 20` / non-integer | `sim_err_n_lt20`, `sim_err_n_nonint` |
172
+ | invalid model / design | `sim_err_model`, `sim_err_design` |
173
+ | negative block effect | `sim_err_block_neg` |
174
+ | invalid prior | `sim_err_a0_zero` |
175
+ | model shape validation | `sim_err_shape_normal`, `sim_err_shape_pareto` |
176
+ | independent summary (t + Mann-Whitney) against frozen R E/C | `sim_frozen_independent` |
177
+ | paired summary (t + Wilcoxon) against frozen R E/C | `sim_frozen_paired` |
178
+ | nine model parameterisations | `test_model_means`, `test_cauchy_scale_and_location`, `test_pareto_support_and_median` |
179
+ | paired block effect cancels in differences | `test_sim_data_paired_block_effect_cancels_in_differences` |
180
+ | independent block effects change both conditions | `test_sim_data_independent_block_effect_changes_values` |
181
+ | `block_max=0` does not consume RNG state (R's `runif(n,0,0)` behaviour) | `test_zero_block_max_does_not_advance_the_generator` |
182
+ | pinned-seed reproducibility | `test_sim_data_is_reproducible_with_a_seed` |
183
+ | output shape, design, ranges | `test_sim_data_returns_both_conditions` |
184
+
185
+ ## `dfba_power_curve` (`dfba_power_curve.R`)
186
+
187
+ | R branch | Tests |
188
+ | --- | --- |
189
+ | negative `delta_step` | `power_err_delta_step_neg` |
190
+ | `n < 20` | `power_err_n_lt20` |
191
+ | invalid model / design / effect criterion / prior / block | `power_err_model`, `power_err_design`, `power_err_effect_crit`, `power_err_a0_zero`, `power_err_block_neg` |
192
+ | 21-point delta grid and 21 power rows | `test_power_curve_structure_and_reproducibility` |
193
+ | `n` rounded like R | `test_power_curve_rounds_n_like_r` |
194
+ | `samples` errors for NA/NaN/Inf/negative | `test_samples_rejected_where_r_also_errors` |
195
+ | `samples` rounding to zero (deliberate divergence) | `test_samples_rounding_to_zero_are_rejected_divergence` |
196
+ | `samples` rounded like R | `test_samples_are_rounded_like_r` |
197
+ | pinned-seed reproducibility | `test_power_curve_structure_and_reproducibility` |
198
+ | summary fast path equals full analysis | `test_summary_fast_path_matches_full_analysis` |
199
+ | power loop shares the draw path with `dfba_sim_data` | `test_power_loop_uses_the_public_draw_path` |
200
+ | output table and pandas export | `test_output_table_is_pandas_friendly`, `test_power_result_helpers_and_batch_dataframe` |
201
+ | plotting | `test_power_curve_plot` |
202
+
203
+ ## `dfba_bayes_vs_t_power` (`dfba_bayes_vs_t_power.R`)
204
+
205
+ | R branch | Tests |
206
+ | --- | --- |
207
+ | negative delta | `bvt_err_delta_neg` |
208
+ | `n_min < 20` / non-integer | `bvt_err_n_min_lt20`, `bvt_err_n_min_nonint` |
209
+ | invalid prior / model / design / effect criterion | `bvt_err_a0_zero`, `bvt_err_model`, `bvt_err_design`, `bvt_err_effect_crit` |
210
+ | 11 sample sizes from `n_min` to `n_min+50` | `test_bayes_vs_t_power_structure_and_reproducibility`, `test_bayes_vs_t_power_sample_size_grid_respects_n_min` |
211
+ | `samples` validation (shared `_check_nsims`) | `test_samples_rejected_where_r_also_errors`, `test_samples_rounding_to_zero_are_rejected_divergence`, `test_samples_are_rounded_like_r` |
212
+ | pinned-seed reproducibility | `test_bayes_vs_t_power_structure_and_reproducibility` |
213
+ | plotting | `test_bayes_vs_t_power_plot` |
214
+
215
+ ## `methods-*.R`
216
+
217
+ | R source | Coverage |
218
+ | --- | --- |
219
+ | `show` methods | `summary()`/`str()` field order in `tests/test_results.py` |
220
+ | `plot` methods | Python-native `result.plot()` dispatch in `tests/test_plotting.py`; not byte-compatible with R base graphics |
@@ -0,0 +1,298 @@
1
+ # Decision record — `dfbayes`
2
+
3
+ ## Upstream pin
4
+
5
+ - Package: **DFBA**
6
+ - Version: **0.1.0**
7
+ - CRAN publication: 2023-12-13
8
+ - Tarball: `upstream/DFBA_0.1.0.tar.gz` (vendored read-only)
9
+ - Installed from source with `R CMD INSTALL`; R 4.6.1 at `/usr/bin/R`.
10
+ - Upstream licence: **GPL-2** (no `LICENSE` file). This port is
11
+ **GPL-2.0-only**.
12
+
13
+ `testthat` is not installed in the fixture environment; the fixture generator
14
+ uses base R plus `jsonlite` and does not require `testthat`. The upstream
15
+ `tests/testthat/` suite was read as a behavioural oracle, but our fixtures come
16
+ from calling the installed R functions directly.
17
+
18
+ ## Distribution and import name
19
+
20
+ `dfba` on PyPI is an unrelated dynamic-flux-balance package. This port is
21
+ distributed and imported as **`dfbayes`**. `dfbayes` returned 404 on PyPI when
22
+ checked on 2026-10-31.
23
+
24
+ ## Target language: pure Python + numpy
25
+
26
+ Confirmed. No Rust, no PyO3, no compiled extension. The reasons are:
27
+
28
+ 1. Upstream has no compiled code at all (`NeedsCompilation: no`), so there is
29
+ no kernel to translate and no performance gap to close.
30
+ 2. The compute that does exist is Monte Carlo, which vectorises. Rust would buy
31
+ a constant factor at best, not an order of magnitude.
32
+ 3. The API returns 18 S4 list-subclass result objects with 15–20 fields each.
33
+ An FFI boundary would marshal a 20-field struct per call for functions whose
34
+ actual compute is microseconds. Net negative.
35
+ 4. Pure Python ships as a single `py3-none-any` wheel. `mixtime`'s
36
+ `DECISION.md` documents the maturin/abi3/universal2 cost we would otherwise
37
+ repeat.
38
+
39
+ Note: "no quadrature/optimisation" is true but is *not* the reason Python wins.
40
+ The heavy paths are Monte Carlo:
41
+
42
+ - `dfba_mann_whitney(method="small")`: 200-point omega grid × `samples`
43
+ replications, each drawing `rexp(n_E)` and `rexp(n_C)` and doing an
44
+ `n_E × n_C` comparison loop.
45
+ - `dfba_wilcoxon(method="small")`: same structure.
46
+ - `dfba_beta_contrast`: `samples` × K individual `rbeta(1, ...)` draws.
47
+ - `dfba_power_curve` / `dfba_bayes_vs_t_power`: 21 × `nsims` full simulations.
48
+
49
+ The R original makes roughly 12 million R-level `rexp()` calls in the
50
+ Mann-Whitney small path. The port draws the whole replication batch at once and
51
+ uses broadcasting for the comparison matrix; it does not transliterate the
52
+ loops line-for-line.
53
+
54
+ ## NA policy
55
+
56
+ R distinguishes logical `NA` (structural) from `NA_real_` (propagated), and
57
+ `is.na()` is TRUE for both `NA` and `NaN`.
58
+
59
+ - **Inputs:** `None` and `NaN` are both treated as missing, matching `is.na()`.
60
+ Scalar validation mirrors R's `if (...)` branches and raises `ValueError`.
61
+ - **Outputs:** all output NA is **`None`**. Genuine `0/0` remains
62
+ `float("nan")`. This preserves the NA-vs-NaN distinction that affects
63
+ arithmetic, but collapses R's logical-vs-double NA distinction; Python has no
64
+ typed NA. `dfbayes.is_na()` treats `None` and `NaN` as missing.
65
+ - **Deletion semantics are per function** (see `AGENTS.md`):
66
+ pairwise/listwise drop for `dfba_sign_test`, `dfba_wilcoxon`, and
67
+ `dfba_bivariate_concordance`; independent per-vector drop for
68
+ `dfba_median_test` and `dfba_mann_whitney`.
69
+ - `dfba_bivariate_concordance(prob_interval=NA)` is not validated by R; the
70
+ interval fields become NA. The port returns `None` there.
71
+ - `dfba_gamma` with an NA cell raises, because R's `rep(..., times=NA)`
72
+ raises `invalid 'times' argument`. The port mirrors that error.
73
+
74
+ ## RNG policy: (a) freeze the narrowest intermediate
75
+
76
+ R's `rexp()` uses inversion from `unif_rand()`; `rbeta()` uses a different
77
+ algorithm again. Bit-exact reproduction is a multi-day subproject and buys
78
+ nothing: R's own small-sample output already varies with `samples`, so
79
+ cross-language `set.seed()` reproducibility is not a property users rely on.
80
+
81
+ Policy:
82
+
83
+ - `dfba_mann_whitney(method="small")` → freeze `fomega` (length-200 counts).
84
+ - `dfba_wilcoxon(method="small")` → freeze `fphi` (length-200 counts).
85
+ - `dfba_beta_contrast` → freeze `delta` and `delta0`.
86
+ - `dfba_power_curve` / `dfba_bayes_vs_t_power` → the simulation loop is
87
+ implemented directly; its aggregation is checked structurally (21 or 11
88
+ grid points, proportions in [0,1], exact grid spacing), and the per-dataset
89
+ summary is replayed against a frozen R E/C pair. Numeric power values are
90
+ not fixture-pinned because they depend on the RNG.
91
+ - `dfba_sim_data` → each of the nine models is checked against its analytic
92
+ mean/median or support under a pinned numpy seed. The t-test and large-sample
93
+ posterior are replayed exactly against R for a frozen E/C pair.
94
+ - The sampling wrappers use numpy. They are **not** claimed to match R, and the
95
+ package does not ship or depend on an R RNG emulator.
96
+ Two tests per stochastic function: (1) analytic-vs-Monte-Carlo empirical
97
+ probability within ~5 standard errors at the null value (`omega=0.5` uses the
98
+ exact Mann-Whitney U distribution; `phi=0.5` uses the Poisson binomial);
99
+ (2) same seed → identical output, different seed → different output.
100
+ - `dfba_beta_contrast` uses R's `quantile()` default type 7;
101
+ `numpy.percentile` matches it.
102
+
103
+ Pinned seeds are recorded with each fixture case in
104
+ `tests/fixtures/stochastic.json`:
105
+
106
+ | case | seed | frozen intermediate |
107
+ | --- | ---: | --- |
108
+ | `beta_contrast_2` | 20261030 | `delta`, `delta0` (10000 each) |
109
+ | `beta_contrast_4` | 20261031 | `delta`, `delta0` (10000 each) |
110
+ | `mw_small_branch_a` | 20261032 | `fomega` (200 bins) |
111
+ | `mw_small_branch_b` | 20261033 | `fomega` (200 bins) |
112
+ | `wil_small` | 20261034 | `fphi` (200 bins) |
113
+
114
+ The two Mann-Whitney cases sit on opposite sides of R's branch test
115
+ `length(E)*length(C) > UE + UC`:
116
+
117
+ - branch A: `E = C = c(1,1,2,2)` → 16 comparisons, `Ntot = 8`;
118
+ - branch B: `E = c(2,4,6)`, `C = c(1,3,5,7,9)` → 15 comparisons,
119
+ `Ntot = 15`.
120
+
121
+ Branch A counts only the first `Ntot` row-major comparisons; branch B counts
122
+ all of them. Both are preserved and replay-tested against the frozen `fomega`.
123
+
124
+ ## Phase 2 simulation models
125
+
126
+ `dfba_sim_data` draws with numpy's `Generator`; R's `set.seed()` state is not
127
+ reproduced. Each model uses R's parameterisation:
128
+
129
+ | model | C draw | E draw |
130
+ | --- | --- | --- |
131
+ | `normal` | `N(0, shape1)` | `N(delta, shape2)` |
132
+ | `weibull` | `Weibull(shape1, 1)` | `delta + Weibull(shape2, 1)` |
133
+ | `cauchy` | `Cauchy(0, shape1)` | `Cauchy(delta, shape2)` |
134
+ | `lognormal` | `LogNormal(0, shape1)` | `LogNormal(delta, shape2)` |
135
+ | `chisquare` | `chi2(shape1)` | `delta + chi2(shape2)` |
136
+ | `logistic` | `Logistic(0, shape1)` | `Logistic(delta, shape2)` |
137
+ | `exponential` | `Exp(rate = shape1)` | `delta + Exp(rate = shape2)` |
138
+ | `gumbel` | inverse transform on `U in [1e-5, 1-1e-5]` | same, offset by `delta` |
139
+ | `pareto` | inverse transform, `xm=1`, `alpha=1.16*shape1` | inverse transform, `xm=1+delta`, `alpha=1.16*shape2` |
140
+
141
+ The block effect is `U(0, block_max)`. Paired designs add the same block to
142
+ both conditions; independent designs add separate blocks. The one-tailed
143
+ p-value uses Student's t with `2n-2` degrees of freedom (independent) or `n-1`
144
+ (paired). The Bayesian probability is the `prH1` from the large-sample
145
+ Mann-Whitney or Wilcoxon branch, matching R.
146
+
147
+ `dfba_power_curve` evaluates 21 deltas from 0 to `20*delta_step`;
148
+ `dfba_bayes_vs_t_power` evaluates 11 sample sizes from `n_min` to `n_min+50` in
149
+ steps of five. Both accept an optional numpy `Generator` via `rng`, which is a
150
+ Python extension.
151
+
152
+ ## Tolerances
153
+
154
+ `pbeta`/`qbeta` are TOMS 708 in R and Boost in scipy. Agreement is expected
155
+ around 1e-12, not bit-identical. Tolerances are derived from the fixtures by
156
+ measuring the observed relative/absolute difference, then rounded up to a
157
+ documented bound. They are never loosened to make a test pass; a larger
158
+ difference is a port bug or a documented divergence.
159
+
160
+ The suite default is `rtol=1e-11, atol=1e-12`. Observed maxima outside the
161
+ Mann-Whitney large branch are below `3e-12` relative, driven by the final
162
+ `pbeta`/`qbeta` calls. Two exceptions are documented and handled explicitly in
163
+ `tests/_support.py`:
164
+
165
+ - `mw_large*` `BF10`: `rtol=1e-9`. R's large-branch polynomial is evaluated by
166
+ subtracting O(1e3) terms to leave an O(1e-2) result, and `BF10` is then a
167
+ ratio of two beta tails. The observed maximum relative difference is
168
+ `2.40e-11`, so `1e-9` is a conservative bound, not a license to drift.
169
+ - `mc_large` `BF10interval`/`postH1`: the relative difference is `7.4e-8`
170
+ because the value itself is `2.99e-9`, formed by cancellation. The absolute
171
+ difference is about `2.2e-16`, well inside the shared `atol=1e-12`; no
172
+ field-specific `rtol` is needed.
173
+
174
+ R's `^` on doubles is repeated multiplication, not libm `pow`; the port uses
175
+ `r_pow()` where R's one-ulp behaviour is observable (`mann_whitney` polynomial
176
+ and `beta` variance).
177
+
178
+ ## Documented divergences and deliberate choices
179
+
180
+ These are intentional and covered by tests; none changes a deterministic
181
+ numeric result.
182
+
183
+ 1. **Progress output.** R's small-sample functions emit `\r...% complete` via
184
+ `message()` unless `hide_progress=TRUE`. The port emits no progress output by
185
+ default and has no `\r` rewriting. This is display-only.
186
+ 2. **R `message()` → Python `UserWarning`.** Sample-size advice and the
187
+ NA-cell notes become `UserWarning`s, preserving the text. `dfba_beta_contrast`
188
+ raises `ValueError` for `samples < 10000` exactly as R stops; NA `samples`
189
+ raises `missing value where TRUE/FALSE needed`.
190
+ 3. **`a0_vec`/`b0_vec` recycling in `dfba_beta_contrast`.** R silently recycles
191
+ mismatched lengths through vector arithmetic. The port broadcasts length 1
192
+ and rejects other mismatched lengths with `ValueError` rather than silently
193
+ recycling.
194
+ 4. **Star concordance duplicate field.** R's `dfba_bivariate_concordance_out`
195
+ with `fitting_parameters` has two `prob_interval` slots. The dataclass has
196
+ one field; `R_FIELDS` records the duplicate so `summary()` can reproduce R's
197
+ field order.
198
+ 5. **Logical NA.** `dfba_beta_descriptive` returns logical `NA` for an
199
+ undefined mode/HDI. Python has no typed NA, so these are `None`; see the NA
200
+ policy above.
201
+ 6. **`dfba_sign_test` Bayes factor prior.** Upstream computes the BF with the
202
+ default uniform prior even when the caller sets `a0`/`b0`. The port
203
+ reproduces that bug; the posterior interval still uses the caller's prior.
204
+ 7. **`dfba_gamma` matrix expansion.** R expands a table/matrix row-major
205
+ (`x` = row, `y` = column) and uses `int(count)` per cell. Python accepts a
206
+ 2-D array and indexes it explicitly; column-major storage is not assumed.
207
+ 8. **`plot()` and text `show()`.** The 15 R `plot()` S4 methods are not
208
+ ported. In their place the package offers an optional, Python-native
209
+ matplotlib layer (`result.plot()`) that returns an `Axes`; it is a
210
+ usability extension, not a reproduction of R's base-graphics output.
211
+ `summary()` reproduces field names/order, not R's exact `sprintf` layout.
212
+ 9. **Result helpers.** `to_dict()`, `to_json()`, `to_dataframe()`, `bf10`,
213
+ `credible_interval`, `posterior_mean`, `posterior_median`,
214
+ `probability_h1`, `interpret()`, `_repr_html_()`, and
215
+ `results_to_dataframe()` are additions, not R API. `to_json()` maps
216
+ non-finite values to `null` because JSON has no NaN or infinity.
217
+ `to_dataframe()` omits array slots unless `include_arrays=True`.
218
+ 10. **Phase 2 progress and RNG.** `hide_progress` is accepted but no `\r...%`
219
+ progress output is emitted, matching the Phase 1 policy. The simulation
220
+ functions accept an optional numpy `Generator` (`rng=`) for reproducibility;
221
+ R only offers `set.seed()`. Sampling is not bit-compatible with R.
222
+ 11. **`dfba_power_curve` result shape.** The R documentation mentions a
223
+ `delta_vec` slot, but the R implementation does not add one to the returned
224
+ list; the values live in `outputdf$delta_value`. The port follows the
225
+ implementation. `PowerCurveOut.delta_vec` is a Python convenience property,
226
+ not an R slot.
227
+ 12. **Phase 2 `samples` validation.** R computes `nsims <- round(samples)` and
228
+ then `rep(NA, nsims)` without validating. R 4.6.1 behaves as follows for
229
+ both `dfba_power_curve` and `dfba_bayes_vs_t_power`:
230
+
231
+ | `samples` input | R behaviour |
232
+ | --- | --- |
233
+ | `NA`, `NaN`, `Inf`, `-Inf` | error: `invalid 'times' argument` |
234
+ | `-0.6`, `-1`, any value rounding negative | error: `invalid 'times' argument` |
235
+ | `-0.5`, `-0.4`, `0`, `0.4`, `0.5` (rounds to 0) | returns `nsims = 0` with `NaN`/`Inf` power from a pathological `1:0` loop |
236
+ | `0.6` and above | accepted; uses R's banker's rounding (`0.6 -> 1`, `1.5 -> 2`, `2.5 -> 2`) |
237
+
238
+ The port matches the first two rows. It deliberately rejects the
239
+ rounds-to-zero row with `samples must be a positive integer` instead of
240
+ returning undefined `NaN`/`Inf` power. This is an intentional behaviour
241
+ change for inputs R "accepts", documented here rather than reproduced.
242
+ Fractions that round to at least one follow R. `None` is the port's NA
243
+ convention and raises the same `invalid 'times' argument` as `NA`.
244
+ 13. **Power-loop summary fast path.** `dfba_power_curve` and
245
+ `dfba_bayes_vs_t_power` only consume `pvalue` and `prH1` from each
246
+ simulated data set. R still calls the full analysis, which computes the
247
+ posterior median, equal-tailed interval and HDI for every replication. The
248
+ port factors the closed-form posterior parameters into helpers and exposes
249
+ `_wilcoxon_prh1` / `_mann_whitney_prh1`, which skip that unused interval
250
+ work. The public analysis functions keep their full output. A differential
251
+ test (`test_summary_fast_path_matches_full_analysis`) pins the fast path to
252
+ the public functions. Measured on the development machine at
253
+ `samples=1000`, `dfba_power_curve` fell from 86.6 s to 5.5 s (paired) and
254
+ 76.2 s to 3.5 s (independent); `dfba_bayes_vs_t_power` fell from 41.9 s to
255
+ 3.0 s and 32.6 s to 2.1 s. No fixture or tolerance was changed.
256
+ 14. **Zero-width block effect does not consume RNG state.** R's
257
+ `runif(n, 0, 0)` returns zeros without advancing `.Random.seed`; numpy's
258
+ `Generator.uniform(0, 0, n)` does advance. `dfba_sim_data` now skips the
259
+ block draw when `block_max == 0` (the default), so a caller-supplied
260
+ `Generator` moves in lockstep with what R's stream would do. The returned
261
+ values are unchanged because the skipped block was identically zero. This
262
+ is stream-position fidelity only; the port still does not claim
263
+ bit-compatible draws. Test: `test_zero_block_max_does_not_advance_the_generator`.
264
+ 15. **Student-t tail without the `rv_continuous` dispatch.** `_summarise`
265
+ computes the one-tailed p-value with `scipy.special.stdtr(df, -t)`, which
266
+ is exactly what `scipy.stats.t.sf(t, df)` calls internally, but without the
267
+ frozen-distribution argument handling. The result is bit-identical
268
+ (`t.sf` and `stdtr` agree exactly at every tested `t`/`df`, including
269
+ `±Inf` and `NaN`). At `samples=500`, `dfba_power_curve` fell from 2.43 s to
270
+ 1.86 s (paired) and 1.54 s to 0.94 s (independent), process CPU, best of
271
+ three; the test suite and all fixtures are unchanged.
272
+ 16. **Average ranks without `scipy.stats.rankdata`.** The Wilcoxon signed-rank
273
+ statistic needs R's `rank(..., ties.method = "average")`. `_average_ranks`
274
+ implements it with `argsort` plus a vectorised tie collapse and a no-tie
275
+ fast path. It is bit-identical to `scipy.stats.rankdata(method="average")`
276
+ on 2,000 randomised arrays, including heavy integer ties, repeated values
277
+ and empty/singleton inputs. At `samples=500`, the paired power curve fell
278
+ from 1.86 s to 0.83 s; the independent path is unchanged. A snapshot of 120
279
+ large and 120 small Wilcoxon calls (with tied data), 18 power curves and 4
280
+ `dfba_bayes_vs_t_power` calls is byte-identical before and after.
281
+ 17. **Power loops share the draw path without re-validating.** The power
282
+ functions validate their constant arguments once, then call the private
283
+ `_simulate` helper for every replication instead of `dfba_sim_data`. It
284
+ performs the same draw, block effect and summary but skips repeated
285
+ argument checks and result-object allocation. `dfba_sim_data` delegates to
286
+ the same helper, so there is still a single implementation. At
287
+ `samples=500`, the paired power curve fell from 0.83 s to 0.60 s and the
288
+ independent one from 0.84 s to 0.64 s. The 240-call Wilcoxon/power/Bayes-vs-t
289
+ snapshot in item 16 is byte-identical. Test:
290
+ `test_power_loop_uses_the_public_draw_path` pins the shared path and RNG
291
+ stream position.
292
+
293
+ ## Out of scope
294
+
295
+ - The 15 R `plot()` S4 methods. The Python-native plotting layer above replaces
296
+ them for Python users but does not reproduce R's base-graphics output.
297
+ - `summary()`/`show()` byte-for-byte text formatting. Field names and order are
298
+ matched; the R `sprintf`/`cat` layout is not reproduced exactly.