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.
- dfbayes-0.1.0/.gitignore +16 -0
- dfbayes-0.1.0/.python-version +1 -0
- dfbayes-0.1.0/AUTHORS +16 -0
- dfbayes-0.1.0/COVERAGE.md +220 -0
- dfbayes-0.1.0/DECISION.md +298 -0
- dfbayes-0.1.0/LICENSE +338 -0
- dfbayes-0.1.0/NOTICE +22 -0
- dfbayes-0.1.0/PKG-INFO +271 -0
- dfbayes-0.1.0/README.md +235 -0
- dfbayes-0.1.0/justfile +32 -0
- dfbayes-0.1.0/pyproject.toml +83 -0
- dfbayes-0.1.0/src/dfbayes/__init__.py +93 -0
- dfbayes-0.1.0/src/dfbayes/_core.py +208 -0
- dfbayes-0.1.0/src/dfbayes/beta.py +209 -0
- dfbayes-0.1.0/src/dfbayes/binomial.py +57 -0
- dfbayes-0.1.0/src/dfbayes/concordance.py +219 -0
- dfbayes-0.1.0/src/dfbayes/contrast.py +191 -0
- dfbayes-0.1.0/src/dfbayes/mann_whitney.py +396 -0
- dfbayes-0.1.0/src/dfbayes/mcnemar.py +80 -0
- dfbayes-0.1.0/src/dfbayes/median_test.py +89 -0
- dfbayes-0.1.0/src/dfbayes/plotting.py +287 -0
- dfbayes-0.1.0/src/dfbayes/py.typed +0 -0
- dfbayes-0.1.0/src/dfbayes/results.py +890 -0
- dfbayes-0.1.0/src/dfbayes/sign_test.py +105 -0
- dfbayes-0.1.0/src/dfbayes/simulation.py +507 -0
- dfbayes-0.1.0/src/dfbayes/wilcoxon.py +340 -0
- dfbayes-0.1.0/tests/__init__.py +1 -0
- dfbayes-0.1.0/tests/_support.py +171 -0
- dfbayes-0.1.0/tests/cases.py +306 -0
- dfbayes-0.1.0/tests/fixtures/fixtures.json +1 -0
- dfbayes-0.1.0/tests/fixtures/generate.R +847 -0
- dfbayes-0.1.0/tests/fixtures/inputs.json +1 -0
- dfbayes-0.1.0/tests/fixtures/stochastic.json +1 -0
- dfbayes-0.1.0/tests/test_deterministic.py +44 -0
- dfbayes-0.1.0/tests/test_package.py +21 -0
- dfbayes-0.1.0/tests/test_plotting.py +139 -0
- dfbayes-0.1.0/tests/test_results.py +408 -0
- dfbayes-0.1.0/tests/test_simulation.py +413 -0
- dfbayes-0.1.0/tests/test_stochastic.py +377 -0
- dfbayes-0.1.0/uv.lock +1785 -0
dfbayes-0.1.0/.gitignore
ADDED
|
@@ -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.
|