Skip to content

Backport #221 to release-1: avoid Int overflow in signrank and wilcox - #230

Merged
andreasnoack merged 6 commits into
release-1from
backport-pr221-release-1
Sep 4, 2026
Merged

andreasnoack merged 6 commits into
release-1from
backport-pr221-release-1

Conversation

@andreasnoack

@andreasnoack andreasnoack commented Sep 4, 2026 •

Copy link
Copy Markdown
Member

Backports #221 ("Avoid Int overflow in signrank and wilcox by normalising in place") onto the release-1 branch, for a v1.5.3 release.

Commits

The four commits of #221 are cherry-picked with git cherry-pick -x, so each carries a (cherry picked from commit …) provenance line:

  • f511a7c ← 4a538b2 — Avoid Int overflow in signrank and wilcox by normalising in place
  • d2d5b5a ← 7776c46 — Drop the exact binomial branch from the wilcox normaliser
  • 431e67a ← 9910d20 — Work in units of a power of two in the wilcox recurrence
  • 9f1bcf5 ← 64d880c — Address review comments on code comments

Plus one branch-specific commit:

  • 85d3789 — Bump version from 1.5.2 to 1.5.3

Conflicts resolved during the cherry-pick

  • src/distrs/signrank.jl, src/distrs/wilcox.jl — applied cleanly; identical to Avoid Int overflow in signrank and wilcox by normalising in place #221's final state.
  • src/StatsFuns.jl — the SpecialFunctions import line differs from master on release-1 (no erf), so logabsbinomial was merged into this branch's line by hand.
  • test/rmath.jl — release-1 still uses the RFunctions-based test harness (master's test/rmath.jl was later rewritten to call Rmath directly). The new signrank/wilcox testsets anchor on lines that exist on this branch and apply cleanly; a using Rmath: Rmath import was added for the Rmath.* reference calls, and Avoid Int overflow in signrank and wilcox by normalising in place #221's test_inv change (dropping the Sys.islinux() special case) was reapplied to this branch's older rmathcomp structure.
  • test/misc.jl — the transient inv_binomial testset is added by the first commit and removed by the second, netting to no change, as upstream.

Fixes

Verified locally that the previously broken cases now agree with the Rmath reference: signrank at n = 72/200, wilcox at (34,34)/(30,36), and the rescaled seed at (524,556). See #221 for the full analysis.

@devmotion

Copy link
Copy Markdown
Member

targeting a v1.5.2 release (version already bumped on release-1).

That seems wrong? Neither is the version bumped on release-1 (AFAICT) nor does this PR target 1.5.2 (it bumps the version to 1.5.3).

How did you create the backport? I think using git cherry-pick -x should make it a bit clearer.

Both distributions accumulated combinatorial counts in `Int` and divided by
the total count at the end. Those counts grow like `2^n` and
`binomial(nx + ny, nx)`, so they wrapped around and made `signrankcdf` return
silently invalid probabilities from `n = 72`, while `wilcoxcdf` threw an
`OverflowError` from `nx + ny = 68`.

Both recursions are linear and homogeneous, so the normaliser can be folded
into the recursion instead of applied to its result, and the counts are never
formed:

- `signrankDP` halves at every step. Halving is exact in binary floating
  point and every count below `2^53` adds exactly, so the entries are
  bit-identical to the old ones divided by `2^n` for all `n <= 62`, i.e.
  across the whole range in which the old implementation was valid.
- `wilcox_partitions` is seeded with `1 / binomial(nx + ny, nx)` rather than
  with `1`, and hence returns the probabilities themselves. It is renamed
  `wilcox_probabilities` accordingly. The seed uses `binomial` while the
  coefficient is below `2^53`, where it is exact, and `logabsbinomial`, which
  evaluates the coefficient as a beta function, above that.

Agreement with Rmath across the support is now ~1e-15 up to at least `n = 200`
and `nx + ny = 205`, and the pdfs sum to one again.

Fixes #219

(cherry picked from commit 4a538b2)
`inv_binomial` evaluated both `logabsbinomial` and `binomial` whenever the
coefficient was small enough for the latter, and only used one of them. The
exactly rounded reciprocal it bought is worth a few ulp in the cdf, which does
not justify the branch, so the normaliser is now always evaluated as a beta
function via `logabsbinomial`.

The one thing that did depend on those last bits was the comparison of
`wilcoxinvcdf` against `Rmath.qwilcox`. The inverses locate a discontinuity by
searching for the first argument at which the cdf reaches `q`, and the `q`
tested are themselves cdf values, so the outcome is decided by the last bit of
the cdf; nudging the normaliser by a single ulp moves the answer in either
direction. That is not a property to design around, and it is not what the
package does elsewhere: as of #205 the natively implemented discrete
distributions test their inverses by round tripping through their own cdf
rather than against the Rmath quantile functions. `signrank` and `wilcox` are
the only other native discrete implementations, they already have such round
trips, so they are excluded from the Rmath inverse comparison outright. That
also removes the `Sys.islinux()` special case, which was working around the
same instability on one platform.

Also drop the test that checked `exp(36.7) < 2^53` by brute force, which only
restated the threshold it was given, and cover the asymmetric cases from #220,
where the counts overflowed well before `binomial(nx + ny, nx)` did.

(cherry picked from commit 7776c46)
The seed `1 / binomial(nx + ny, nx)` is the probability of the least likely
outcome, so it leaves the normal range long before the probabilities of
interest do: it is subnormal from `nx + ny = 1030` and zero from
`nx + ny = 1090`. Since the recurrence is homogeneous, a seed of zero zeroes
every probability, so `wilcoxcdf` returned a plausible but wrong 0.776804 at
`nx = ny = 540` and 0.0 from `nx = ny = 545`, where the answer is 0.5 either
way by symmetry. The counts these replaced threw an `OverflowError` there, so
this was a loud failure turning into a silent one.

The recurrence is homogeneous, so the unit it works in cannot affect the
result. It now seeds with `2^shift / binomial(nx + ny, nx)`, with `shift`
chosen to place the seed in the normal range, and `ldexp` removes the unit
from the scalar result. `shift` is zero whenever the seed is already normal,
which leaves every input that worked before bit for bit unchanged.

This moves the limit from `nx + ny = 1090` to about `nx + ny = 2050`, where the
scaled values would overflow instead. The recurrence is O(U^2) in
`U = nx * ny / 2`, so evaluating the cdf at the new limit takes minutes, and
runtime rather than representability is now what stops you.

Verified by shift invariance: recomputing with deliberately different units
agrees to 1e-13, and the midpoints at `nx = ny = 540` and `560` now agree with
the normal approximation to six digits.

(cherry picked from commit 9910d20)
Drop the claim that `logabsbinomial` evaluates the coefficient as a beta
function — how it computes the logarithm is its own business — and the claim
that `shift = 0` leaves results bit for bit unchanged, which compared against
an intermediate state of this PR and means nothing in the merged code. Qualify
the corresponding statement in the signrank comment: halving reproduces the
counts divided by 2^n exactly only while every count stays below 2^53.

(cherry picked from commit 64d880c)
@andreasnoack
andreasnoack force-pushed the backport-pr221-release-1 branch from 116c810 to 85d3789 Compare September 4, 2026 07:26
@andreasnoack andreasnoack changed the title Backport #221: avoid Int overflow in signrank and wilcox Backport #221 to release-1: avoid Int overflow in signrank and wilcox Sep 4, 2026
@andreasnoack

Copy link
Copy Markdown
Member Author

Done. I've updated the PR to use the -x option and the PR text. I've also enabled CI on release branches.

@codecov

codecov Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.65217% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 69.90%. Comparing base (814aa47) to head (3b4cdf0).
⚠️ Report is 1 commits behind head on release-1.

Files with missing lines Patch % Lines
src/distrs/wilcox.jl 92.85% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##           release-1     #230      +/-   ##
=============================================
+ Coverage      69.71%   69.90%   +0.18%     
=============================================
  Files             16       16              
  Lines            799      804       +5     
=============================================
+ Hits             557      562       +5     
  Misses           242      242              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@devmotion devmotion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

The uncovered line is an error message and was also flagged by codecov in #221.

@andreasnoack
andreasnoack merged commit 8790a82 into release-1 Sep 4, 2026
15 checks passed
@andreasnoack
andreasnoack deleted the backport-pr221-release-1 branch September 4, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants