Repository navigation
Count trials in geometric, as the docstring and NumPy do - #196
vlad-perevezentsev wants to merge 7 commits into
Conversation
viRngGeometric returns the number of failures, with support 0, 1, 2, ... and mean 1 / p - 1, and that value was returned unchanged. The docstring documents the NumPy convention instead: "supported on the positive integers, k = 1, 2, ...", a PMF of (1 - p)^(k - 1) * p, and an example answering "How many trials succeeded after a single run?" with p itself. Measured over 200k samples at p = 0.35, the frequency of 1 was p * (1 - p) = 0.2287, and p = 0.3487 was the frequency of 0. This is not an edge case of p == 1, which merely made the mismatch obvious by returning 0 where NumPy returns 1: every p was off by one. Adding the successful trial fixes the whole distribution, so the special case for p == 1 now fills ones. Values that would overflow int are left as they are; they are already meaningless, as MKL writes INT_MIN once 1 / p exceeds the range of its int output. Samples for a given seed therefore grow by one, which the regression test records. The new test pins the convention itself rather than a stream.
antonwolfy
left a comment
There was a problem hiding this comment.
Thanks, the fix looks right to me. I built it and compared against NumPy for p from 1.0 down to 1e-6, with scalar and array p, broadcasting and every brng: min, P(1), mean and variance all match now, and the full suite passes.
Could you add "Closes #182" to the description? That's the issue reporting this exact bug. Also, #195 touches the same changelog section and test_random.py, so whichever of the two lands second will need a rebase.
Per review: the guard never fired. MKL writes INT_MIN on overflow, and INT_MIN < INT_MAX, so the +1 turned it into INT_MIN + 1, which contradicts the comment. Measured over 500k draws at p from 1e-9 down to 1e-12, the only negative value present was INT_MIN + 1 and INT_MAX never appeared; with the sign check the marker now survives unchanged. The comment also explains why total and out exist: the chunk loop advances res and decrements len, so without them the +1 pass would only cover the last chunk for outputs above MKL_INT_MAX, which nothing tests.
Per review: assert_array_equal ignores dtypes by default, so the int32 expectation was not actually checked and an int64 or float result would have passed. strict=True also pins the shape and needs NumPy 1.24, below the 1.26.4 minimum.
Per review: "with a fixed seed they change accordingly" mostly repeated "one larger", so the entry now says how to get the previous samples back.
Same point as the review on gh-196: assert_array_equal ignores dtypes by default, so the int32 expectations were not actually checked and an int64 or float result would have passed. strict=True also pins the shape and needs NumPy 1.24, below the 1.26.4 minimum.
There was a problem hiding this comment.
Thanks, this looks good now. I rebuilt 05fe07f and compared it against NumPy: chi-square against the exact PMF passes for p from 0.999 to 1e-5 across all brngs, scalar and array p and interfaces.numpy_random, p == 1 returns 1s in every form, and the output is exactly the old stream plus one, with no extra randomness consumed and MKL's INT_MIN overflow marker kept unchanged.
Two small things:
- The description still doesn't have "Closes #182" (GitHub shows no linked issue), so #182 would stay open after the merge.
- A correction to my overflow comment: the wrapped positive values I mentioned around
p = 1e-10don't exist. Re-measuring, every overflowed draw is exactlyINT_MINand the positive ones are genuine draws below 2^31, so the new comment is accurate as written.
Per review: the +1 pass skips negatives so the marker survives, but nothing checked it. At p = 1e-300 every draw overflows, so the assertion is exact rather than statistical; measured as all INT_MIN for p of 1e-100, 1e-300 and the smallest subnormal, on both the scalar and the array path.
viRngGeometricreturns the number of failures before the first success, with support0, 1, 2, ...and mean1 / p - 1, and that value was returned unchanged. The docstring documents the NumPy convention instead: "supported on the positive integers, k = 1, 2, ...", a PMF of(1 - p)^(k - 1) * p, and an example answering "How many trials succeeded after a single run?" withp.geometric(1.0)returning0where NumPy returns1is just the most visible symptom: everypwas off by one. Adding the successful trial fixes the whole distribution, and the special case forp == 1now fills ones.Closes #182
Changed
geometricsamples are one larger than before for everyp, so results for a fixed seed change accordingly. All other streams are unaffected.P(1), mean) rather than a stream.Notes
intare left as they are — MKL already writesINT_MINonce1 / pexceeds the range of itsintoutput.