1

I have a method that needs to select a random float val from a range of between st and ed, but I only want one of the endpoints included. (Or, in other terms, (st, ed] or [st, ed).) My current solution is:

import random
val = None
st, ed = 0, 360
  ## In application, what I want is an angle, but I've made it generic as an example.
while not (st <= val < ed): # which of st or ed to reject is up to you.
    val = random.uniform(st, ed)

...but is there a way (by argument or notation) to tell random.uniform (or a function like it) to include or exclude one endpoint or another? Perhaps NumPy has an answer?

I am aware that the odds of getting two equivalent values in a range of floats with thirteen places of precision, that spans more than (or exactly) one integer is basically nil, but I'd like to know if only to better understand the tools themselves.

Augusta
  • 7,171
  • 5
  • 24
  • 39
  • 1
    the upper bound is not included – Padraic Cunningham Apr 17 '15 at 20:51
  • 1
    @PadraicCunningham Not according to the documentation: "Get a random number in the range [a, b) or [a, b] depending on rounding" – Ella Sharakanski Apr 17 '15 at 20:52
  • ...Man, how could I have missed that? Now I feel really stupid. ._. Thanks for letting me know. – Augusta Apr 17 '15 at 20:53
  • 1
    Actually I've checked the implementation of uniform and it is just: `return a + (b-a) * self.random()` so @PadraicCunningham is right and the documentation is wrong, since the documentation for random is "Return the next random floating point number in the range [0.0, 1.0)" – Ella Sharakanski Apr 17 '15 at 20:57
  • 2
    @EllaShar unless I'm mistaken that's what "depending on rounding" means; since floating point arithmetic is imprecise it's possible to get `b` from `random.uniform(a, b)` for some values of `a` and `b`. – dimo414 Apr 17 '15 at 20:59
  • @dimo414 I was wondering about these 'missing' values in JuniorCompressor's answer, too. I suppose if having these multiplied-over values is that important (and it probably is not going to be), I'd just run something like `val = st; for n in range(int(ed-st)): val += random.random()`? – Augusta Apr 17 '15 at 21:04
  • Wait, that's actually even worse, probabilistically... :s – Augusta Apr 17 '15 at 21:06
  • 1
    Unless you're performing a precise error analysis of your code, accidentally including or not including the endpoints of the range is likely to be insignificant compared to all the other sources of error in standard floating-point calculations. – user2357112 Apr 17 '15 at 21:11
  • 2
    If you take into account rounding errors then `uniform` is not actually uniform, is it? – Ella Sharakanski Apr 17 '15 at 21:12
  • 2
    Also, an explicit example of a `uniform` call that includes the right bound: `random.uniform(2.**53, 2.**53+2)` – user2357112 Apr 17 '15 at 21:13
  • At first I was embarrassed by misreading the docs and asking a question with an obvious, RTFM answer, but now I'm kind of happy I did. – Augusta Apr 17 '15 at 21:24
  • 1
    @Augusta when it comes to floating point arithmetic, just about nothing is "obvious" ;) – dimo414 Apr 17 '15 at 21:29
  • @dimo414 So I've only begun to learn! :D – Augusta Apr 17 '15 at 23:17

2 Answers2

1

Broadly speaking, your solution seems reasonable. If you have an RNG providing a range of values larger than the range you need, you discard the invalid results and keep computing. Obviously there's a danger that this could run forever if your invalid range is too large, but ultimately that's a sacrifice you may just have to make.

This is, in fact, how Python generates a random value in the first place. From random._randbelow():

k = n.bit_length()  # don't use (n-1) here because n can be 1
r = getrandbits(k)          # 0 <= r < 2**k
while r >= n:
    r = getrandbits(k)
return r

It keeps generating a random integer below 2**k until the result is below the desired n. Just like what you're trying to do.

That said, for your specific case you shouldn't need to do this - worrying about endpoints of floating point ranges is somewhat futile.

First, as @user2357112 points out, considering open vs. closed endpoints of a uniform distribution is somewhat of a pedantic exercise. On the real number line (or any subset), the probability of randomly selecting exactly your endpoints is 0. Furthermore with floating point numbers the state of affairs gets worse, not better. Since floating point cannot precisely represent all real numbers, it's entirely possible (even likely) either of your endpoints don't exist, and therefore cannot ever be returned, regardless of your specified bounds.

@EllaShar has an interesting idea with bit-twiddling, but I wouldn't recommend it in practice. If you really need exact control of the bounds, stick to your original solution, it's simpler, more readable, and more clearly correct.


As an aside, if you have an RNG that returns numbers in the range [a, b) and you want to get numbers in the range (a, b], you can simply negate your range, e.g.

-random.randrange(-b, -a)
dimo414
  • 47,227
  • 18
  • 148
  • 244
0

To generate a number that's in [st, ed) even after rounding, you can do random.uniform(st, closest_smaller_than_ed).

To get the closest number to ed which is smaller than ed use (see this):

numpy.nextafter(ed, ed - 1)

You may want to check that ed != st to avoid generating a number not in [st, ed).

Similarly, to get val in (st, ed] do:

if st == ed:
    return ed
st_adjusted = numpy.nextafter(st, st + 1)
return random.uniform(st_adjusted, ed)

EDIT:

The comment by dimo414 is correct, there are values of ed that for them, the probability of getting closest_smaller_than_ed will be zero. Proof:

(I'll call closest_smaller_than_ed by the name adj_ed)

The method random.uniform(st, adj_ed) equals to st + (adj_ed - st) * random.rand(). The maximum here is reached when random.rand() gives the closest number that's smaller than 1, which is numpy.nextafter(1, 0).

So the question is, is there an ed value such that st + (adj_ed - st) * numpy.nextafter(1, 0) < adj_ed. If there is such an ed, then for this ed there is no chance of getting adj_ed using my method, since the largest number we can get is smaller than adj_ed.

The answer is yes, there is such an ed: st = 0; ed = 360, as the OP suggested.

This gives:

>>> st = 0
>>> ed = 360
>>> adj_ed = numpy.nextafter(ed, ed - 1)
>>> adj_1 = numpy.nextafter(1, 0)
>>> st + (adj_ed - st) * adj_1
359.99999999999989
>>> adj_ed
359.99999999999994
>>> st + (adj_ed - st) * adj_1 == numpy.nextafter(adj_ed, adj_ed - 1)
True

EDIT2:

I have a new idea, but I'm not sure it solves everything.

What if we first check that the biggest value that random.uniform(st, ed) can give is ed (if it isn't then everything is OK and there is nothing to solve). And only then we use random.uniform(st, closest_smaller_than_ed) as I suggested before.

def does_uniform_include_edge(st, ed):
    adj_1 = numpy.nextafter(1, 0)
    return st + (ed - st) * adj_1 == ed

def uniform_without_edge(st, ed):
    if does_uniform_include_edge(st, ed):
        adj_ed = numpy.nextafter(ed, ed - 1)
        # if there is an ed such that does_uniform_include_edge(st, adj_ed) is False then this solution is wrong. Is there?
        return random.uniform(st, adj_ed)
    return random.uniform(st, ed)

print(uniform_without_edge(st, ed))

For st = 0; ed = 360 we have does_uniform_include_edge(st, ed) == False so we just return random.uniform(st, ed).

Community
  • 1
  • 1
Ella Sharakanski
  • 2,683
  • 3
  • 27
  • 47
  • 1
    But if `closest_smaller_than_ed` isn't rounded by `uniform`, it will be excluded from the range of results incorrectly, no? – dimo414 Apr 17 '15 at 23:32
  • It's a clever solution, to be sure. It *probably* works. But it's difficult to be sure, and it's easy to accidentally do wrong. OP's solution, while slightly less efficient in the worst case, is simpler and easier to mentally verify. – dimo414 Apr 18 '15 at 14:20
  • I'd really like to prove it. Or to prove it's wrong. Meanwhile, it's an overkill but you could use the OP's solution in cases mine doesn't work - just insert this: `if not does_uniform_include_edge(st, adj_ed): OP's solution`. – Ella Sharakanski Apr 18 '15 at 15:05