-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
Add recipe for a version of random() with a larger population #22664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
I've asked Allen Downey to take a look at this as well. |
The code looks good. I put some tests in this Jupyter notebook: It passes a visual test that the distribution is uniform from 0 to 1. I also tried out an implementation closer to what's in the paper. Both work, but Raymond's is a bit faster. One question: in the last line, why not use |
Although it occurs to me that I see that it generates 56 bits and then shifts 3 of them away. Why not use them all?
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment about possible bias in subnormal results as in the ldexp()
version I looked at offline.
A "geometric" explanation may be intuitively helpful: this is like throwing a dart at random at [0, 1), then picking the closest representable float at or to the left. The one in the paper is like throwing the dart, but picking the closest representable float in either direction, which leaves an exact power of 2 less likely to be picked than any other float in its binade, but more likely to be picked than any float in the binade preceding it.
Either of those is justifiable, but I prefer what this code does, because it's easier to explain (nothing special about an exact power of 2).
About the slight bias in denorm cases, I really don't care - but you might 😉 .
Doc/library/random.rst
Outdated
while not x: | ||
x = getrandbits(32) | ||
exponent += x.bit_length() - 32 | ||
return mantissa * 2.0 ** exponent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiplication rounds, so when this slobbers into the denorm range, nearest/even rounding will give a slight bias toward 0 in the last retained bit. ldexp()
on Windows truncates instead, which doesn't introduce bias in the denorm cases; but I believe ldexp()
on most other platforms does round.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for this use of "slobber"
That's the The point to the shift is so that the floating multiplication is exact. Rounding would introduce numerical complications (e.g., most obviously, 1.0 would suddenly become a possible output; less obviously, the default to-nearest/even rounding could introduce bias in the last retained bit). |
Sorry, that was sloppy. The point to the shift is so that the conversion from int to float is exact. The multiplication is exact regardless (we're nowhere near the subnormal range, nor near overflow). But avoiding rounding remains the motivation. |
Okay, I switched back to using The reason for the I also considered using |
Actually, under the "closest representable float <= a truly random real in [0, 1)" view, 0 will be delivered with the right probability, provided |
Fair enough. ldexp() is specialized to assemble a float from a mantissa and an integer exponent. So, if it is well implemented, it should do at least as well and possibly better than any other way of doing it. |
FWIW, here is the test code I've been using:
That gives this output:
|
Any comments on the wording of the comment, docstring, or introductory paragraphs? I haven't yet had a chance to test its intelligibility on my students. |
About the docstring: it took me a few passes to get it. It is explained in terms of the denominator of a rational number, which makes sense. I think of it differently, in terms of equally-spaced points on the number line being mapped to floating-point values, which are not equally spaced. So some of them get chosen many times and some (in fact, a large majority) never get chosen at all. I think your way of explaining it is fine -- it's just not the way I thought of it. It's probably best to test it on an audience that's not me. |
That would be a Bad Idea. There's never a reason to "apologize" for using exact integer operations whenever possible 😉 |
It all depends on how well the reader understands the basics of floating point representations. Nobody comes to that with useful intuitions - they have to unlearn lots of what they think they "know". Already gave a visual metaphor, throwing a dart uniformly across the real [0, 1) clopen interval. If they understand that representable floats are unevenly spaced, and how, then "move to the closest one <=" should be extremely easy to picture. But if they don't understand how representable floats are distributed, you're going to need at least a page of explanation with several diagrams. |
Depending on the reader, they may find this easiest to grasp. The default
where there's no ambiguity because all operations are exact in float arithmetic.
Note that 1 / 2**1074 is the smallest non-zero positive representable float. If they understand the notation, this makes it crystal clear that it's "as uniform as possible". |
I doubt this is worth adding, but since I already wrote it ... it was a sanity check on the EDIT: replaced the code with a more compact, more uniform, branch-free work-a-like. Now it's at least close to what was in my head 😉. If there's any confusion about why this works, the key is in something that's obvious, but perhaps only in hindsight: every finite IEEE-754 double is, mathematically, an integer multiple of 2**-1074. So def slow_full_random():
m = getrandbits(1074)
# Conceptually, we want truncating ldexp(m, -1074), but we don't
# want any rounding anywhere. We need to cut `m` back to at
# most 53 significant bits so conversion to float is exact.
excess = max(m.bit_length() - 53, 0)
return ldexp(m >> excess, excess - 1074) |
Now I feel vindicated for my early draft designed to fit in a tweet ;-) |
Draft text to introduce the recipe: The default random() returns multiples of 2⁻⁵³ in the However, many floats in that interval are not possible selections. The following recipe takes a different approach. All floats in For efficiency, the actual mechanics involve calling math.ldexp
|
Thanks @rhettinger for the PR 🌮🎉.. I'm working now to backport this PR to: 3.9. |
Sorry @rhettinger, I had trouble checking out the |
Thanks @rhettinger for the PR 🌮🎉.. I'm working now to backport this PR to: 3.9. |
GH-22684 is a backport of this pull request to the 3.9 branch. |
…GH-22664) (cherry picked from commit 8b2ff4c) Co-authored-by: Raymond Hettinger <[email protected]>
No description provided.