Changes for version 0.298 - 2026-08-12

  • wilcox_test
    • A rewrite of `wilcox_test` against R 4.6.1, driven by R's and SciPy's own test suites rather than by cases invented here. It brings the function up to the exact conditional inference R gained in 4.6.0, fixes six bugs — two of which returned confidently wrong p-values on the *default* code path — and adds the Hodges-Lehmann estimate and confidence interval, `digits.rank`, and the Edgeworth series.
    • Everything below is checked in the new `t/wilcox_test.R.scipy.t` (3,242 tests), whose expected values are frozen literals with their provenance recorded in the file header; it needs no R and no Python to run. The full suite is 120 files and 23,149 tests, and `./test.all.perls.pl` passes on all five local perls — `5.10.1`, `5.12.5` (long double), `5.42.3`, `5.44.0` and `5.44.0-quadmath` — with no warnings on any of them.
    • Exact p-values are now computed when there are ties:
    • R 4.6.0 added exact (conditional) inference in the presence of ties, via Torsten Hothorn's implementation of the Streitberg-Röhmel shift algorithm; R's `doc/NEWS.Rd` announces it and `tests/reg-tests-1d.R` records the consequence at its degenerate one-sample cases: *"For R >= 4.6.0 warnings for exact with ties are gone."* Before that, ties ruled out an exact p-value and both R and this module fell back to the normal approximation with a warning.
    • `wilcox_test` now does what R does. When ties are present the null distribution is the conditional one given the observed ranks, and the same holds for zero differences in the signed-rank test. The warnings are gone with them.
    • This changes published answers on tied data, including R's own documented examples:
    • | case | was | is (R 4.6.1) | |---|---|---| | `?wilcox.test` man-page data, `wilcox_test(\@x, \@y)` | `0.13291945818531886` | `0.12990538872891813` | | the `airquality` Ozone example (`W = 127.5`) | `1.2080783e-04` | `6.1087351888e-05` | | `wilcox_test([1,2,2,3], [4,5,5,6], exact => 1)` | `0.02842953599879653` + a warning | `0.028571428571428571` | | `wilcox_test([1,1])` | `0.34577858615116` | `0.5` | | `wilcox_test([4,3,2], [3,2,1], paired => 1)` | `0.14891467317876567` | `0.25` |
    • Two further consequences are worth knowing about. V itself changes when zero differences are present, because the exact test ranks `|x - mu|` over every observation and only afterwards drops the ranks belonging to the zeroes, where the approximation drops the zeroes first and ranks what is left: `wilcox_test([-1, 0, 1])` gives `V = 2.5` exactly and `V = 1.5` with `exact => 0`. R's two branches differ in exactly the same way. And degenerate inputs that used to be fatal now return a result, as they must for `tests/reg-tests-1d.R` line 332 to pass: `wilcox_test([0])` gives `V = 0`, `p = 1`, and so does `wilcox_test([0,0,0,0,0])`, which SciPy pins as `test_all_zeros_exact`.
    • If you need the old numbers, `exact => 0` still asks for the approximation and is unchanged.
    • The exact upper tail was returning zero, on the default path:
    • `p_greater` was computed as `1 - CDF(q - 1)`. That subtraction cancels away every significant digit once the true p falls below `NV_EPSILON`, and then returns a flat `0`. It did not take a contrived input to reach: two perfectly separated samples of 30 apiece are inside the automatic exact branch, no `exact => 1` required.
    • | m = n | was | is | R 4.6.1 | |---|---|---|---| | 20 | `7.2544192875e-12` | `7.2544445519e-12` | `7.2544445519e-12` | | 25 | `7.8825834748e-15` | `7.9107286024e-15` | `7.9107286024e-15` | | 30 | `0` | `8.4556169461e-18` | `8.4556169461e-18` | | 49 | `0` | `3.9250145965e-29` | `3.9250145965e-29` |
    • Both tails are now summed directly. That alone is not enough for the rank-sum table, whose Gaussian-binomial recurrence is built with subtractions, so far up the support a count of `1` is the difference of numbers around `C(m+n, n)` and has already been rounded into noise. The table is folded about its centre before summing, so only well-conditioned entries are ever touched — the same thing R's `pwilcox()` does when it folds `q` about `m*n/2` and flips `lower_tail`.
    • The signed-rank tail was accurate to `n = 49` by luck (`1 - 2^-49` is exactly representable) and reached `0` from about `n = 53`; forcing `exact => 1` on `n = 120` returned `0` where R gives `1.5046327690525337e-36`, and now returns it too.
    • `int m * n` overflowed, and said the samples were identical:
    • `exact_pwilcox` took `int m, int n` and computed `int max_u = m * n`. For two separated samples of 50,000 that wraps negative, every statistic looks out of range, and the function returns `1.0`:
      • wilcox_test([1 .. 50000], [50001 .. 100000], exact => 1); # p = 1
    • Signed overflow is also undefined behaviour, so a different optimiser was entitled to do something else entirely. Sizes and indices in the exact distributions are `size_t` now, the multiplications are checked for wrap before they happen, and a table that would need more than 16 million cells is refused outright with a message naming `exact => 0` rather than attempted.
    • NaN was ranked instead of dropped:
    • `NaN` is `NA` to R, and R drops it. `looks_like_number` accepts it, `d == 0.0` is false for it, so it went into the rank buffer — and `cmp_nv3` returns `0` for every comparison involving it, which leaves `qsort` without the strict weak ordering the C standard entitles it to.
    • The visible symptom is R's own regression case, `tests/reg-tests-1d.R` line 3546, which asserts that a paired test is unaffected by pairs whose difference is `Inf - Inf`:
    • | | was | is (and R) | |---|---|---| | `1:5` vs `4*(0:4)` | `V = 1`, `p = 0.125` | `V = 1`, `p = 0.125` | | the same with `+Inf` appended to both | `V = 1`, `p = 0.0625` | `V = 1`, `p = 0.125` | | the same with `-Inf` and `+Inf` on both | `V = 2`, `p = 0.046875` | `V = 1`, `p = 0.125` |
    • `NaN` — in either sample, and however it arises — is now dropped with the other missing values. `±Inf` is not missing and is kept, since a rank test has no trouble with it; SciPy's `test_gh_11355b` pins five cases of that and they all agree.
    • An empty `y` ran a different test:
    • `wilcox_test([1,2,3], [])` fell through to the one-sample branch and returned a signed-rank result, silently answering a question nobody asked. It croaks now, with R's message.
    • `mu` was likewise unvalidated: `mu => Inf` or `mu => NaN` turned every difference into a non-number and produced a confident answer from the wreckage. Both croak now, as they do in R.
    • A dying `$SIG{__WARN__}` handler leaked the rank buffer:
    • The warnings in `wilcox_test` were emitted while the `RankInfo` and difference buffers were held as raw pointers. A `__WARN__` handler that dies — or `warnings FATAL` at the call site — longjmps straight past the `Safefree`. Under valgrind, 500 iterations of the ties path with such a handler lost 95,616 bytes in 498 blocks. Every allocation now goes through `Newx` plus `SAVEFREEPV`, the idiom `chisq_test` in the same file already used, so it is released by the save stack however the call unwinds. The same 500 iterations now report `definitely lost: 0 bytes`, as does a sweep over every croak path and every branch of the function.
    • New: `conf.int`, and a Hodges-Lehmann estimate:
    • R has returned a distribution-free confidence interval and a point estimate since PR#1150 in 2001, and `tests/reg-tests-1a.R` has guarded them ever since with Hollander & Wolfe's published numbers. `wilcox_test` now computes both, by all four of R's routes — the exact interval from the order statistics of the Walsh averages or the pairwise differences, the exact interval conditional on the observed ranks when there are ties, and the asymptotic interval from a root search:
      • my $r = wilcox_test(\@y, \@x, paired => 1, conf_int => 1);
      • $r->{estimate} == -0.46
      • $r->{conf_int} == [-0.786, -0.010]
      • $r->{conf_level} == 0.9609375
    • Those are Hollander & Wolfe (1999) 2nd ed., pp. 40 and 53, to the digit. So are the two-sample values from pp. 111 and 126: estimate `-0.305`, interval `(-0.76, 0.15)`.
    • The level a rank test can actually deliver is a step function of the data, so `conf_level` reports what was achieved rather than echoing what was asked for — `0.9609375` above, not `0.95`. `conf.level`, `tol.root` and R's alpha-doubling search for a level the data can support (with its *requested conf.level not achievable* warning) all behave as R's do.
    • New: `digits.rank`, `edgeworth`, and more of R's result fields:
    • `digits.rank` rounds each value to a given number of significant digits before ranking, so that ties are decided on the rounded values. R's man page recommends it because tie detection is an exact `==` on floating point, and its own worked example shows `(4:2)/10` against `(3:1)/10` — three differences that ought to be `0.1` and are three different doubles. Ported from R's `fprec()`, half-to-even rounding included.
    • `edgeworth => 1, 2, 3` adds up to three Edgeworth correction terms to the normal approximation, the refinement R 4.6.0 reaches through its integer `correct`. It is ignored on the exact path, and — as in R — ignored when there are ties, or when the signed-rank test dropped a zero, because the series is derived for untied ranks.
    • The result hash gains `statistic_name` (`"W"` or `"V"`, as R prints), plus `null_value` and `null_value_name`, and `estimate` / `conf_int` / `conf_level` when an interval was asked for.
    • Three deliberate differences from R:
    • Each is asserted in the test file, so that changing one later is a choice rather than a drift.
    • 1. `correct` is a boolean here. R 4.6.0 turned its `correct` into an integer `0:3`, in which numeric `0` still applies the continuity correction and only `FALSE` removes it — so in R, `correct = 0` and `correct = FALSE` are different tests. Keeping that would mean `correct => 0` no longer meaning "off", which is what it means for every other flag in this module. `correct` stays a boolean, and R's `correct = k` is `correct => 1, edgeworth => k`. 2. A zero variance is reported, not propagated. With `exact => 0` and every observation tied there is nothing to divide by. R divides anyway and returns `NaN`; this warns and returns `p = 1`. The default path no longer reaches it at all, since the exact test handles all-tied data. 3. An all-tied interval does not raise. R's one-sample code warns and hands back a `NaN` interval at level `0`; its two-sample code warns and then dies inside `uniroot` with *missing value where TRUE/FALSE needed*. We give the one-sample answer in both places.
    • There is one place where this module is simply more accurate than R. R's exact p-values on tied data come from a density it normalises entry by entry; `wilcox_test` sums the integer permutation counts and divides once. For the worst case in the corpus — an 11-against-12 tied rank sum whose p-value is exactly `4/676039` — this returns the correctly rounded double and R is `1.2e-11` high. Checked against exact rational arithmetic, and recorded in the test file rather than papered over.
    • Testing:
    • `t/wilcox_test.R.scipy.t` takes its cases from the references' own suites:
    • R's `tests/reg-tests-1a.R` (the PR#1150 Hollander & Wolfe intervals), `reg-tests-1b.R` (the Wolfgang Huber `wilcox.test(1, 2:60)` case, and the check that the asymptotic estimate does not move with `alternative`), `reg-tests-1d.R` (the six degenerate one-sample calls and the `±Inf` identities), and the man-page examples whose printed output is pinned in `tests/Examples/stats-Ex.Rout.save`.
    • SciPy 1.17.1's `TestMannWhitneyU`, whose header reads *"All magic numbers are from R wilcox.test"* — `cases_basic`, `cases_continuity`, `cases_9184`, `cases_2118`, `test_tie_correct`, `test_exact_U_equals_mean`, `test_gh_11355b` and the 30-against-20 asymptotic cases — and `TestWilcoxon`'s `test_accuracy_wilcoxon`, `test_wilcoxon_tie`, `test_onesided`, `test_exact_pval`, `test_exact_p_1`, `test_all_zeros_exact` and `test_symmetry_gh19872_gh20752`.
    • A 663-case sweep generated by `t/wilcox_test.R.scipy.R`, committed next to the test, crossing four data shapes against every alternative, `exact` state, `correct` state, `mu` and `conf.int` setting.
    • Beyond the file, 960 further randomised calls were compared against R 4.6.1 and agree everywhere except the three divergences above.
    • One lesson from getting that to pass on every NV width is worth recording: the corpus data has to be exactly representable. Whether two values tie decides which branch runs, and `1.6 - 2 - 0.5` does not land on the same value in a `double`, an x87 `long double` and a `__float128`. A corpus of one-decimal values passed on the default perl and failed on `perl-5.12.5` and quadmath with a *different statistic*, not merely a different last digit. Every generated value is now a whole number of quarters or of 1024ths. For the same reason the asymptotic interval, which is only ever pinned down to `tol.root`, is generated at `tol.root = 1e-12` rather than freezing wherever Brent's method happened to stop on one machine.
    • A compiler-warning audit of `LikeR.xs` for `-Wint-conversion`, `-Wimplicit-int`, `-Wreturn-mismatch` and `-Wdeclaration-missing-parameter-type`, and a pass tightening integer types that can only hold a count or a flag. No behaviour changed: the full suite (116 files, 18,546 tests) passes, and every function touched was diffed call-for-call against a build of the previous release, with `dnorm`, `pnorm`, one- and two-sample `ks_test`, `fisher_test`, `auc` and the set operations re-checked against R 4.6.1 and found bit-identical.
    • All four of those warnings were already clean, and stay clean on a `double`, a `long double` and a `__float128` build. Two of them cannot be tested with the GCC most systems still default to: `-Wreturn-mismatch` and `-Wdeclaration-missing-parameter-type` are GCC 14 additions — where they are errors rather than warnings — and GCC 13 rejects both as unrecognized options, so a check that appears to pass on 13 has really only skipped them.
  • Dead code removed
    • Turning the audit up to `-Wextra` found two branches that could never run, both of them a test for negativity on a value whose type is unsigned:
    • 1. `r_pow_di` takes `unsigned int n`, so its `if (n < 0) return 1.0 / r_pow_di(x, -n);` was unreachable — a leftover of R's `R_pow_di`, which takes a signed exponent. All three callers (in `K2x`, for the exact one-sample Kolmogorov-Smirnov distribution) pass a non-negative exponent, so the unsigned parameter is the correct one and the reciprocal branch simply goes. 2. `hoa2aoh` casts `HvUSEDKEYS` to `U32` and then clamps with `if (ncols < 0) ncols = 0;`.
  • Types narrowed to what they can actually hold
    • Eighteen `int`s that only ever hold 0 or 1 became `bool`, a convention the file already followed in some 219 other places; each was confirmed by reading every call site rather than by name. The flag parameters of `ft_pnhyper`, `K2l`, `c_dnorm`, `c_pnorm`, `c_pnorm_both`, `set_multiplicity` and `roc_split`, the `is_cat` field of `AnFac`, the `lower_pos` and `frac_low` locals of `auc`, `auroc`, `roc` and `bedroc`, and the return types of `mg_key` and `psmirnov_exact_test`. Several of these were already being handed a `bool` by their callers — `dnorm`'s and `pnorm`'s `log` and `lower` options, for instance — so only the helper signatures were behind. `c_pnorm_both`'s loop counter became `unsigned int`.
    • Two that look like flags and are not: `c_pnorm_both`'s `i_tail` is three-valued, and `set_multiplicity`'s `gimme` carries a Perl `G_*` context value. Both stay `int`.
    • Also six coefficient tables in `c_pnorm_both` written `const static double`, which puts the storage class after the qualifier and draws `-Wold-style-declaration`; they are now `static const double`.
  • runif argument validation, and every warning names its function
    • `runif` accepts its arguments either positionally or by name, and decided which was which by asking whether the current argument was a string *and* whether another argument followed it. A key at the end of the list therefore failed the second half of that test and fell through to the positional branch, where it was read as a number: `runif(5, 'min')` took `SvNV("min")`, which is 0, silently set `min = 0`, and returned five values. The only sign anything was wrong was perl's own `Argument "min" isn't numeric`, which does not say which function provoked it. `runif(5, bogus => 1)` went the same way, taking `bogus` as `min` and `1` as `max`. Every sibling that parses named arguments — `rbinom`, `binom_test`, `fisher_test`, `dnorm`, `pnorm` — rejects both of those.
    • `runif` now does too. A string argument is treated as a key when it is not a number, which is decidable from the key alone, so a dangling or misspelled key is an error instead of a silent coercion; a numeric string is still positional, so `runif("9")` is unchanged. Named values are checked for numerichood before use, which is what keeps perl's unattributed warning from being the diagnostic.
    • `n` is also range-checked now. It was read straight through `SvUV()`, so `runif(-1)` wrapped to 2**64-1, `av_extend()` read that back as a negative `SSize_t`, and perl died with `panic: av_extend_guts() negative count (-2)` -- which names neither the function nor the argument at fault. A negative or over-large `n` now croaks and says so. Non-integer `n` still truncates toward zero, as R's `runif()` does, and `runif(0)` still returns an empty list.
    • Separately, three warnings did not name the function emitting them, unlike every other warning in the file: one in `ks_test` (the 1-sided exact 1-sample case falling back to asymptotic) and two in `wilcox_test`'s signed-rank branch (exact p-value abandoned for ties, and for zeroes). All three now carry the prefix their siblings already had. The one warning left deliberately bare is the `warn("%s", m)` in the uninitialized-value catcher, which re-emits somebody else's warning verbatim and must not add to it.
  • Argument-stack indices are now Stack_off_t
    • `-Wextra` reported 58 `-Wsign-compare` warnings, and 33 of them were one idiom: an index declared `size_t`, `unsigned`, `unsigned int` or `unsigned short int` and then compared against `items`. `items` is neither of those — XSUB.h's `dITEMS` declares it `Stack_off_t items = (Stack_off_t)(SP - MARK)`, a *signed* type, because it is a stack-pointer difference. Every one of those comparisons was converting the signed side to unsigned.
    • The indices are now `Stack_off_t` themselves, which is the type they are compared against: 25 declarations across 23 functions — `binom_test`, `ks_test`, `wilcox_test`, `write_table`, `max`, `runif`, `quantile`, `mean`, `mode`, `sum`, `sd`, `uniq`, `var`, `t_test`, `median`, `matrix`, `fisher_test`, `power_t_test`, `var_test`, `dnorm`, `value_counts`, `prcomp` and `pnorm`. That is a retype, not a cast: writing `(size_t)items` at each comparison would silence the warning just as well, but it would be wrong the day `Stack_off_t` widens, which is exactly what it exists to allow. `t_test`'s index was `unsigned short int`, which drew no warning at all — integer promotion made the comparison signed — and was the same latent mistake regardless.
    • `Stack_off_t` arrived in perl 5.39.2 and this distribution supports 5.010, so the preamble now carries a shim typedef guarded on `PERL_STACK_OFFSET_DEFINED`, the macro perl.h defines next to the typedef. On 5.10.1 and 5.12.5 neither the macro nor the type exists and the shim supplies `I32`, which is what the stack offset was on every perl before that.
    • The 33 warnings are gone, 25 remain, and no warning category increased — verified by compiling the before and after trees and diffing the warning sets. The remaining 25 are unrelated signedness pairs (`size_t` against `ssize_t`, `IV` against `size_t`, `STRLEN` against `ssize_t`) and are left alone. The full suite passes on perl 5.10.1 and 5.12.5, the two builds that depend on the shim, as well as on 5.42.3, 5.44.0 and 5.44.0-quadmath; and 94 calls covering all 23 retyped functions — positional and named forms, bare lists against arrayrefs, `write_table`'s emitted bytes, and the odd-argument and unknown-argument croaks that this index arithmetic drives — produce identical output before and after.
  • NV was being computed at double precision on wide builds
    • Every libm call in `LikeR.xs` was written bare — `sqrt(x)`, `log(x)`, `lgamma(x)` — and C has no type-generic `<math.h>`. Those functions take a `double`, so on a perl built with `-Duselongdouble` or `-Dusequadmath` every one of them converted the `NV` down to 53 bits of mantissa, computed there, and converted the result back. Nothing warned and nothing failed to compile; the answers were simply less accurate than the perl running them. On perl-5.12.5 (`long double`), `sd(1..5)` returned exactly the double-rounded `sqrt(2.5)`, 9.5e-17 away from the value perl's own `sqrt` gives.
    • All 412 of those calls now go through `nv_*` macros that paste on the suffix for the width `NV` actually is: none for `double`, `l` for `long double`, `q` for `__float128`. The 80 `isnan`/`isinf`/`isfinite` calls became `Perl_isnan`/`Perl_isinf`/`Perl_isfinite`, which matters most where the C99 type-generic macros are absent: there `isfinite()` is a plain `double` function, and narrowing a large-but-finite long double into it reports the value as infinite rather than merely rounding it.
    • The long-double row is conditional. The `l` variants are C99 but some libms — the thinner BSD ones especially — do not ship the whole set, so `Makefile.PL` link-tests all twenty as a unit and defines `LIKER_HAVE_LONG_DOUBLE_MATH` only if every one resolves; otherwise the build falls back to the `double` functions, which is exactly what it did before and so cannot regress. `__float128` needs no probe: `<quadmath.h>` and `-lquadmath` come with the quadmath perl itself, and the built object was checked with `nm` — it references `lgammaq`, `expq`, `sqrtq` and no double-width libm symbol at all.
    • Accuracy on the long-double build, measured against values that are exact in binary or known in closed form: `sd(1..5)` is now bit-identical to perl's `sqrt(2.5)`, and `fisher_test([[3,1],[1,3]])` moves from 1.5e-16 to 6.4e-18 relative error against the exact 17/35. The remaining 6.4e-18 is an accuracy floor in that function's own summation, not a width problem — the `__float128` build lands on the same figure.
    • This costs time where the wide math is software-emulated: the suite takes 352s on the quadmath perl, against 67s when it was quietly running on hardware doubles. The other four perls are unaffected.
  • The build ran itself twice, and clobbered its own Makefile doing it
    • `make` had to be run twice or the `.so` came out stamped with the wrong version and refused to load. The cause: ExtUtils::MakeMaker scans the directory for `*.PL` files to run during the build, and `dev.Makefile.PL` — a local convenience wrapper, not part of the distribution — looks like one. It was being run mid-build as `perl dev.Makefile.PL dev.Makefile`, and since it calls `WriteMakefile()` it overwrote the real `Makefile` with its own: no `DEFINE`, no probed C99 flag, and a different `VERSION`. The second `make` then rebuilt from that. `PL_FILES => {}` turns the scan off; nothing here is generated by a `.PL` file.
    • The version half was a stale literal: the checked-in `Makefile.PL` pinned `VERSION => "0.28"` while `lib/Stats/LikeR.pm` had moved to 0.298, and `XSLoader::load()` passes `$VERSION` to a `.so` compiled with `-DXS_VERSION` from that literal. It now reads `VERSION_FROM => lib/Stats/LikeR.pm`. One `make` after `perl Makefile.PL` is enough again, and the non-quadmath builds are about a third faster for not doing the work twice.
  • Portability: Solaris, the BSDs, and vendor compilers
    • The C99 flag is now probed instead of guessed. `Makefile.PL` was selecting `-std=gnu99` on any compiler whose name matched `/\b(?:g?cc|clang)\b/`, and `$Config{cc}` is plain `cc` for Oracle Studio on Solaris and for aCC on HP-UX — both of which reject that flag outright, so the build failed there before it compiled a line. Each candidate is now trial-compiled and the first that works wins: `-std=gnu99`/`-std=c99` for gcc and clang, `-xc99=all` for Studio, `-qlanglvl=extc99` for AIX `xlc`, `-AC99` for HP-UX, and nothing at all for a compiler already in C99 mode. MSVC is skipped outright, since it warns rather than errors on switches it does not know and would make the probe settle on a no-op.
    • Two things that would have failed to compile off Linux are gone. `<strings.h>` and its `strcasecmp` — POSIX-only, absent on MSVC — are replaced by a small `str_ieq_ascii()`, which also drops the locale dependency: `tolower()` under a Turkish locale maps `I` outside ASCII, which should never decide whether `"TRUE"` matches `"true"`. And bare C99 `restrict`, used on 151 pointers here, now has an `#ifdef` mapping it to `__restrict` on MSVC and `__restrict__` on older gcc, and defining it away where no spelling exists, rather than losing the annotation.
    • `LikeR.xs` also compiles clean under strict `-std=c99` with no GNU extensions, which is the closest available local proxy for a vendor compiler.
  • Dead code: sample()'s private PRNG
    • A splitmix64 generator sat at the top of the file under a comment promising a PRNG stream separate from `Drand01()`, seeded lazily from `/dev/urandom` with a `time()^PID` fallback. None of it was true: no seeding code was ever written, no caller ever existed, and its state started at a fixed 0, so had anything called it the "random" sample would have been the same sequence in every process. `sample()` draws from `Drand01()` and always did, which is the behaviour that is wanted — `srand($seed)` governs it the way `set.seed()` governs R. The generator and its comment are removed.
  • Tests
    • Two files, 273 assertions, and both were checked against a deliberately broken build rather than merely observed to pass.
    • `t/nv_width.t` fails if the math width ever comes undone. Its sharp assertion needs no tolerance at all: `sd(1..5)` must be the identical NV to perl's `sqrt(2.5)`, which holds on any width and breaks the moment a `double` gets in the way. It is width-adaptive rather than skipped on a `double` perl, computing the NV epsilon of the running build instead of assuming one.
    • `t/scale.keywords.t` covers `scale()`'s string options — `"mean"`, `"sd"`, `"none"`, `"true"`, `"false"`, `""` and their case variants — which had no coverage at all: `t/01.t` passes only the numeric forms. Expected values come from R 4.6.1 `base::scale()` at `options(digits=17)` and are frozen in the file, so it needs no R at run time. Deleting the case fold from `str_ieq_ascii()` fails 11 of its assertions; usefully, all 11 are the "off" spellings, because an unmatched string falls through to `SvTRUE` and still means "compute it", so `"MEAN"` would keep working while `"NONE"` flipped. That is recorded in the file so the section is not trusted for more than it proves.
    • The suite is 118 files and 18,819 tests, passing on perl 5.10.1, 5.12.5, 5.42.3 (threaded), 5.44.0 and 5.44.0-quadmath, with no compiler warnings on any of them.

Documentation

Modules

Get basic statistical functions, like in R, but with Perl using XS for performance