Depending on what language or API you’re using for creating random numbers, you may run into a weird problem: you can only generate numbers between 0 and 1. Luckily, there’s an easy fix!
In short, any random number can be generated by first scaling the random number to match the desired output range and then shifting the random number by some offset. For example, to get a number in the range [0, 1) to the range [20, 30), we would compute the range of the desired distribution (i.e., 30 – 20 = 10) to get our scaling factor and then take the lower bound as our offset (i.e., 20). The resulting expression will transform our random number from the range [0, 1) to the range [20, 30): `num * 10 + 20`.
Table of Contents
Pseudorandom Number Generators
In programming, it’s common to use some form of random number generator to produce a random value. For instance, if you want a program to retrieve a random person from a list for a lottery, you would probably use a random number generator.
Unfortunately, there are very few “real” random number generators available to the average programmer. Instead, we rely on what’s called a pseudorandom number generator. A pseudorandom number generator does not pull numbers from thin air. Instead, it uses a function that is fully deterministic. As a result, given some initial seed value, we can predict the output of the pseudorandom number generator.
With that said, most pseudorandom number generators are random enough for general use. They maintain some form of randomness by relying on a constantly changing value like time as the initial seed value. That way, successive runs of the same script will almost guarantee a unique return value.
While pseudorandom number generators are great for getting a random value, they often have limitations. For example, depending on the language or library, you might only be able to generate a value between 0 and 1. Luckily, that’s all you typically need to generate any number you want.
Making Sense of a Range of Values
In mathematics, we like to talk about ranges as a set of values between two points. For example, in the previous section I mentioned that many pseudorandom number generators only produce values between 0 and 1.
That said, a range of 0 to 1 is ambiguous. Does the range include 0 and 1 or not? As is often the case in programming, we include the initial value but exclude the final value, and we indicate that as follows:
To be clear, the square brackets indicate inclusion while parentheses indicate exclusion. In other words, any number we generate will be a decimal value from 0 until 1, where 1 is not a possible return value.
With that out of the way, let’s talk about changing the distribution.
Changing a [0, 1) Distribution
To move any distribution, we have two options that we can use in tandem: shifting the distribution through addition and scaling the distribution through multiplication.
Shifting a Distribution
Let’s say that we wanted to shift our [0, 1) distribution to [1, 2). How would we go about doing that? Or in other words, what can we do to both 0 and 1 to get 1 and 2?
I’ll give you a hint! It’s addition. We can shift our entire distribution from [0, 1) to [1, 2) by adding 1 to both end points. Or to make it more explicit, we would add 1 to the random number that we generated:
>>> import random >>> random.random() 0.6620451108237024 >>> random.random() + 1 1.533041347873466
And as it turns out, we can add any number to this distribution to shift it up and down. If we wanted a range of [27, 28), we would add 27. If we wanted [-4, -3], we would subtract 4 (or add -4).
Scaling the Distribution
One of the limitations of shifting a distribution is that we can never widen or narrow it. We’re stuck with a width of 1, or are we?
Let’s once again say we wanted to shift our [0, 1) distribution to [0, 50). What can we do to both 0 and 1 to get 0 and 50?
If you guessed multiplication, you’d be right. In this case, we need to multiply both sides by 50 to get the distribution we want. Here’s what that might look like in code:
>>> import random >>> random.random() 0.7699290750233039 >>> random.random() * 50 9.924673974868725
As usual, we can change the width of our distribution however we like. For example, if we want to narrow our distribution from [0, 1) to [0, .5), we would only need to divide both sides by 2 (or multiply by .5).
Scaling and Shifting the Distribution
Scaling and shifting alone have their limitations. However, together they can create any distribution. You just have to be careful in the order in which you apply the operations. My general advice would be to multiply the distribution first before adding. Here’s a decent formula:
random_in_range = random_initial * scaling_factor + shifting_factor
For example, if we want to generate a random number representing the average weight of an apple (I know it’s a weird example), we probably want to generate values between 150 and 250 grams.
Getting the scaling factor is straightforward, we just need to calculate the range between 150 and 250 (i.e., subtract 150 from 250), which is 100.
As far as the shifting factor, we only need to calculate the difference between lower bound and 0, which is always the lower bound. In this case, our shifting factor is 150.
Put it all together and we have the following equation:
random_in_range = random_initial * 100 + 150
When plugged into the code, we’ll get a value in our expected range:
>>> import random >>> random.random() * 100 + 150 178.88152294921542 >>> random.random() * 100 + 150 180.5789905640733 >>> random.random() * 100 + 150 180.94645757862781 >>> random.random() * 100 + 150 164.5193623217517 >>> random.random() * 100 + 150 234.69377673074598
Now, that’s cool! And the best part about it is you can use it to generate any random number.
But Wait! What About Integers?
In this article, we talked about how to scale and shift a [0,1) distribution to any distribution of our liking. However, one of the limitations of the formula is that we’ll always get floating point values as a result. If we need integers, say for a lottery of 20 people, we have to do a little more work. Specifically, we need to cast the entire result to integer:
>>> import random >>> int(random.random() * 20) 19 >>> int(random.random() * 20) 4 >>> int(random.random() * 20) 1 >>> int(random.random() * 20) 15
Keep in mind that a solution like this will only work on positive values. After all, converting the values to integers will only cut of the decimal. As a result, a distribution on [-20, 0) will actually exclude -20 and include 0 with this solution. Instead, use the `math.floor()` function.
With that said, that’s all I have to say about random number distributions. As always, here are some other useful posts:
Otherwise, take care!
I've seen a lot of folks share code on Discord, but some ways are better than others. Let's compare a few of the different ways.
Today, we'll be learning recursion through the lens of from Bloom's Taxonomy, a tool educators use to make curriculum.