1

A function random.random() generates random float numbers in range [0, 1), a half-open range. On the other hand, a function uniform(a, b) generates numbers in range [a, b] or [a, b) (cannot be specified explicitly) according docs. I am looking for Python function to generate random float numbers only in range [a, b) (without upper bound).

I looked at several questions e.g. to this one but I didn't find answer.

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
illuminato
  • 1,057
  • 1
  • 11
  • 33
  • Are you referring to the mathematical convention of using ] and ) to mean 'up to and including' and 'up to but not including'? – Bill Aug 14 '22 at 01:04
  • @Bill Yes, correct – illuminato Aug 14 '22 at 01:05
  • So, if b is 10, say, what are you expecting the highest number in the range [1, 10) to be? For example, do you want it to be the next lowest number in the systems floating point representation below 10? E.g. `10 - np.finfo("float64").eps*5` – Bill Aug 14 '22 at 01:13
  • 2
    In Python 3.9 you can use `nextafter` to get the previous floating point value from your upper bound: `math.nextafter(x, -math.inf)`. Then use that smaller value as the inclusive upper bound. – Tom Karzes Aug 14 '22 at 01:14
  • @Bill Yes, next lowest number e.g. `9,999` – illuminato Aug 14 '22 at 01:18
  • Thanks @Tom Karzes, I didn't know about `nextafter`. There is also a numpy version of it so this should work: `np.random.uniform(a, np.nextafter(b, a))` – Bill Aug 14 '22 at 01:18
  • @illuminato did you see this question: [Is it possible to generate a random number in python on a completely open interval or one that is closed on the high end?](https://stackoverflow.com/q/52107703/1609514) – Bill Aug 14 '22 at 01:21
  • @Tom. If your implementation is one where it doesn't include upper value, reducing upper value will mean there'll be one value you want but will never get. – paxdiablo Aug 14 '22 at 01:28
  • @paxdiablo That was assuming that `uniform` included the upper bound. I don't know if it does or not. – Tom Karzes Aug 14 '22 at 02:09

5 Answers5

5

If you have a random function such as uniform() which may give a half-open or fully-open range (and you don't know which), probably the simplest way to ensure it gives a half-open range is just to filter out the high values.

In other words, something like:

def half_open_uniform(a, b):
    # raise exception if a == b ?
    result = uniform(a, b)
    while result == b:
        result = uniform(a, b)
    return result

If uniform() already returns a half-open value, the while loop will never run and you'll get back a value you want.

If the value it returns is fully-open and your upper and lower bounds have a decent spread, the vast majority of cases will also not activate the while loop.

If the upper and lower bounds are very close, the probability of getting the upper bound increases but, even then, it will most likely just run once.

If the two bounds are identical, that probabilty rises sharply to 100%. But calling something like half_open_uniform(1, 1) can be caught with a simple pre-check at the start of the function, probably raising an exception (see the comment).


By way of example, consider the following code which will choose random.uniform() if no arguments are provided, and that filter function given if any arguments are:

import random
import sys

def half_open_uniform(a, b):
    result = random.uniform(a, b)
    while result == b:
        result = random.uniform(a, b)
    return result

fn = half_open_uniform if len(sys.argv) > 1 else random.uniform
upper = 1.0000000000001
count = 0
while True:
    count += 1
    x = fn(1, upper)
    if x == upper:
        print(">>>", count, x)
        break
    if count % 100000 == 0:
        print(">>>", count, x)

You can see that, with the uniform one (running with no argument), you get the upper bound occasionally with the first number on each line being the number of iterations before the upper was delivered (each of these lines is the result of a single run):

>>> 452 1.0000000000001
>>> 321 1.0000000000001
>>> 766 1.0000000000001
>>> 387 1.0000000000001
>>> 889 1.0000000000001
>>> 616 1.0000000000001
>>> 82 1.0000000000001
>>> 357 1.0000000000001
>>> 2443 1.0000000000001
>>> 71 1.0000000000001

On the other hand, running with an argument (using the filter function), it just keeps on going for a great deal of time:

>>> 100000 1.0000000000000604
>>> 200000 1.0000000000000642
>>> 300000 1.0000000000000466
>>> 400000 1.0000000000000304
>>> 500000 1.000000000000074
>>> 600000 1.0000000000000007
>>> 700000 1.0000000000000346
>>> 800000 1.0000000000000826
>>> 900000 1.0000000000000588
>>> 1000000 1.0000000000000773
:
>>> 49700000 1.0000000000000449
>>> 49800000 1.0000000000000446
>>> 49900000 1.0000000000000027
>>> 50000000 1.0000000000000786
:

That's fifty million calls without getting back the upper value. And, of course, it was still going. I let it get to a little over two billion iterations before I got bored :-)

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • Little bit risky for production grade code with while loop but raising an exception is better. – illuminato Aug 14 '22 at 18:31
  • Risk is minimal, especially if the distribution is truly uniform. Note the suggested exception was only for `a == b`, the loop becomes infinite at that point. Every other scenario, the loop handles. – paxdiablo Aug 14 '22 at 22:08
1

From the documentation, it looks like numpy.random.uniform actually does what you want:

Samples are uniformly distributed over the half-open interval [low, high) (includes low, but excludes high)

Solution:

import numpy as np

x = np.random.uniform(a, b)
Bill
  • 10,323
  • 10
  • 62
  • 85
  • 4
    Actually, if you read further in the documentation under the `high` argument, it says "The high limit may be included in the returned array of floats due to floating-point rounding in the equation `low + (high-low) * random_sample()`." This seems to contradict the statement above from the documentation. – Bill Aug 14 '22 at 02:51
0
import random
def rand_float(a, b):
    return random.random() * (b-a) + a

This function should work. It utilizes the fact that random.random() returns [0, 1) and just scales it up according to the desired range, then applying an offset so that it will begin in the correct place.

Example:

print(rand_float(1, 5.3)) # could give 3.6544643 or 4.2999 but not 5.3

This doesn't take into account floating-point precision issues, but for most cases it will work.

Luke B
  • 2,075
  • 2
  • 18
  • 26
  • This answer is sort of repeating the original question and the docs linked in it. According to the docs, this is exactly the implementation used by `uniform`. So why re-implement `uniform`? This question was not "is there a "most cases" solution?"—they started their question by mentioning that solution, and asked if there was a more robust solution. – Dan Getz Aug 14 '22 at 01:28
  • Wow, I didn't see that this was the literal implementation used in the docs. It looks like Bill has a perfect solution. – Luke B Aug 14 '22 at 01:36
0

Unless your b is exceedingly close to a, it is extremely unlikely that b will ever be the exact output of your random number generator (unless the generator is itself bad), so I would recommend accepting a tiny bias and just returning any relevant value, e.g. a, if the result happens to be b. For example, assuming gen(a, b) is how you are generating values between a and b,

def closed_open_rand(a, b):
    r = gen(a, b)
    if r == b:
        return a
    return r

This will introduce a tiny bias towards the value a that you probably don't care about. If you really care about avoiding this tiny bias, you can instead give up determinism:

def closed_open_rand(a, b):
    while (r := gen(a, b)) == b:
        pass
    return r

This method avoids the tiny bias, but could theoretically be an infinite loop if your generator is bad.

If you really care about both avoiding tiny bias and maintaining determinism, then you need to look into more sophisticated methods.

mCoding
  • 4,059
  • 1
  • 5
  • 11
0

You could do something like this:

from random import choice
from numpy import arange

start = 0
stop = 1
precision = 0.00001
choice(arange(start, stop, precision))

where choice picks randomly a number from a float range. Stop is excluded since it uses a range.

Andreas
  • 8,694
  • 3
  • 14
  • 38