If you’ve used Python, or any high-level programming language for that matter, it’s easy to take for granted that you can use it to generate random numbers. For example, you can print a random integer from 1 through 10 with the following snippet:
| | import random |
| | |
| | print(random.randint(1, 10)) |
But here’s a fundamental, almost philosophical, question: how can a computer choose a random number? Here’s what I mean.
Let’s talk about randomness as it appears in the world. If I ask my friend to choose a random number between 1 and 10, is the number they choose truly random? Perhaps there’s some psychology involved in the number they choose. In fact, some studies show that people choose 7 more often than any other number. While there’s debate as to why that’s so, it indicates that a human choosing a random number is not as random as we might think. After all, if the process was truly random, 7 should be as common a choice as any other number.
Let’s consider possibly the most “classic” of all random activities: flipping a coin. Is it truly random to flip a coin to see whether it lands on heads or tails? In theory, physicists should be able to predict the result by measuring the strength one used to flip the coin, plus the weight of the coin, plus the air pressure, plus the hardness of the surface on which the coin lands, plus a few other factors. Indeed, if we want to get really philosophical, we can wonder whether anything in the world is truly random.
Despite these arguments, we can still define randomness in a way that is good enough for our purposes.
Here’s the deal. Sure, it might be technically possible to predict the outcome of a coin toss. However, it’s really difficult to measure all the relevant factors—and you and I certainly aren’t going to do so. Therefore, we can consider flipping a coin to be unpredictable enough to be called random. And so, we can say that an event is random if it’s difficult to predict the outcome.
But now let’s get back to computers. A computer doesn’t flip coins and certainly doesn’t use original thinking to generate a random number “off the top of its head.” A computer is what’s called deterministic, meaning that it’s only capable of receiving and executing instructions we give it. So, if we give the computer an algorithm to generate a random number, say, using mathematical calculations, we’ll still be able to predict exactly what number the computer will choose if we know the algorithm. It won’t be random at all.
This is a real head-scratcher. Is it even possible for a computer to choose a random number?
It turns out that there are two different ways in which a computer generates random numbers: with a True Random Number Generator (TRNG) or a Pseudorandom Number Generator (PRNG). We’ll take a look at both, starting with TRNG.
A TRNG uses a highly unpredictable external source rather than an algorithm to provide a random number. That is, since a computer is completely deterministic and cannot come up with a random number on its own, the computer relies on this outside source to do so.
This outside source could be anything in nature or the environment that is so hard to predict that we consider it random. For example, we could connect the computer to a secret office in a basement in South Dakota (the epicenter of secret basement offices). Each time the computer needs a random number, it sends a signal to that office, and someone there will roll a die, which we’d consider a random event. The number that the die lands on is then sent back to the computer.
Of course, this is completely impractical. However, here are some TRNGs that are used in real life. In truth, some of them may sound almost as ridiculous as our South Dakota example, yet they are truly used in the real world.
You know how if you set a radio to the wrong channel, you hear nothing but static noise? This static is generated by lightning strikes and other natural processes. One TRNG technique is to connect the computer to a radio that’s set to a channel that picks up this static. The computer then picks out the frequency of sound at a precise moment in time and uses that frequency to generate the random number. As crazy as this sounds, this is currently one of the more popular TRNGs out there.
Another real-life TRNG approach is to hook a computer up to a camera that takes a snapshot of a lava lamp, and generates a random number based on the exact formation of the “lava” at that given moment. I kid you not.
Yet another TRNG connects the computer to decaying radioactive material. It’s hard to predict the exact way in which an atomic nucleus decays, so why not use that for generating random numbers? (Don’t try this at home.)
These are considered true random number generators because the random numbers are based on processes that are too difficult to predict. They’re the best source for generating the “randomest” numbers.
However, there are two major disadvantages to TRNGs. First, they’re not always practical, as you can imagine. Second, it can be relatively slow to generate a random number based on an external process, at least compared with PRNGs, which we’ll look at soon.
Interestingly, there exist online TRNG services that allow you to connect your code to their TRNG devices via a web API. That is, they have computers hooked up to lava lamps or radioactive material or whatever, and all you have to do is connect to their servers and grab a truly random number that they generate. While this may help with the practicality issue, making a web request is way slower than having your computer generate a random number internally.
Because of these issues of practicality and speed, Python uses a PRNG rather than a TRNG. On that note, let’s look at how PRNGs work.
My dictionary tells me that the prefix pseudo means “false, not genuine, a sham.” Indeed, pseudorandom numbers are just that—a total sham. They’re not random at all; they only seem random. Yet, this is exactly the approach that Python and most other programming languages use to generate “random” numbers. Let’s see how this works.
The main idea behind PRNGs is that they have a prepared sequence of numbers that, at first glance, seem random. For example:
| | [5, 9, 1, 0, 2, 3, 4, 8, 0, 7, 2, 0, 0, 4, 9, 7, 0, 5, 0, 2] |
When looking at these numbers at a glance, they do look kind of random. At least, there’s no predictable pattern.
With a PRNG, each time we ask the computer to generate a “random” number, it will use this sequence. The first time we execute code that generates a random number, the computer will return the first number from the sequence. In our example, this would be the number 5. The next time we ask the computer to generate a random number, it will return 9 since it’s the next integer in the sequence. And the third time we ask for a random number, the computer will return 1. You get the idea.
When the computer reaches the end of the sequence (the final 2 in our example), it will then start all over again at the beginning of the sequence. There are 20 integers in the example sequence, so when we generate a random number for the 21st time, we’ll get back a 5 since it’s the first number in the sequence.
If you’re horrified at this algorithm for generating random numbers, you should be. This isn’t random at all! This approach is entirely deterministic; we know exactly what the computer will “randomly” choose next. Even worse, there are major problems with this sequence of numbers. For one thing, there are significantly more instances of 0 than any other number. And did you notice that there are no 6s at all?
Now, we can improve things a bit if we create a better sequence. Here are a few ideas that are crucial for a decent sequence:
One integer shouldn’t be considerably more prevalent than the other integers. Each integer should appear the same number of times or at least close to it. This attribute is known as uniformity.
The numbers should be “mixed up.” If the sequence were 1, 2, 3, 4, and so on, it wouldn’t even seem random. Even though pseudorandom numbers are, by definition, not truly random, we could at least try to make them look random. We call this attribute lacking pattern. That is, the sequence should lack any discernible pattern.
The sequence should have a lot more integers. The previous example has only 20 integers. Even if there’s no pattern within the 20 numbers themselves, there still ends up being a pattern in that the same 20 sequence numbers keep repeating themselves! After a while, a user of our software may begin to notice this pattern. But what if we had 100 integers? That would be a more difficult pattern to detect. The number of integers in the sequence before it begins again is called the period. A sequence with a long period is one that has many numbers in it, so it’ll take a while until the sequence starts over from the beginning again.
With this in mind, here’s a better-looking sequence:
| | [4, 7, 2, 0, 0, 1, 8, 5, 1, 8, 3, 0, 7, 4, 7, 0, 3, 9, 2, 6, |
| | 9, 1, 3, 3, 8, 9, 5, 4, 3, 1, 6, 8, 2, 9, 2, 0, 6, 5, 5, 9, |
| | 6, 8, 5, 2, 2, 9, 7, 9, 8, 7, 6, 6, 0, 8, 5, 5, 7, 7, 4, 4, |
| | 9, 8, 1, 7, 0, 3, 6, 1, 1, 4, 1, 9, 0, 9, 4, 2, 1, 5, 6, 4, |
| | 2, 7, 3, 3, 6, 0, 6, 7, 2, 5, 4, 5, 3, 1, 8, 4, 8, 2, 3, 0] |
This pattern contains 100 numbers, lacks pattern, and is uniform. Is it perfect? Far from it. But it’s a lot better than our first sequence.
As contrived as this all seems, this is pretty much how PRNGs work. Since a computer can’t generate a true random number, it fakes it. But it fakes it well enough to make the numbers seem random to the user.
The concept of pseudorandom number generation is starting to take shape. But here’s a new monkey wrench: so far, we’ve only been dealing with random numbers from 0 through 9. But what if we want to generate a number that was, say, 7 digits long?
Indeed, Python’s random() method typically produces a number with a whole bunch of digits. Interestingly, the number generated is a float between 0 and 1. However, it has many digits beyond the decimal point. For example:
| | >>> random.random() |
| | 0.5339767180649452 |
And herein lies the problem. If we want uniformity to the point where every possible float occurs at least once within our sequence, we’d need to prepopulate a sequence containing trillions of numbers. That’s a lot of memory to take up to enable a programming language to generate a random number.
Because of this, PRNGs don’t prepopulate a long sequence of numbers for the computer to cycle through. Instead, they use a clever trick to achieve a similar result. That is, the PRNG only has to remember the pseudorandom number it generated previously, and then it calculates the next number based on the previous number.
In other words, there is indeed a prepopulated sequence. But we don’t have to tell the computer the entire sequence up front. Instead, the computer figures out the next number based on the previous number.
Here’s a simplified example to demonstrate the basic concept.
Let’s make up a formula for generating random numbers between 0 and 210. It’ll go something like this:
Yes, this sounds like a calculation that I pulled out of a hat, and that’s because I did. But let’s see what happens.
If the previous number generated by the computer was 73, we’ll apply our calculation:
| | (73 + 12549) % 211 = 173 |
Therefore, 173 will be the next pseudorandom number.
Now, the next time the computer generates a number after this, the computer will grab the previous number (173) and apply the same calculation to generate a new pseudorandom number:
| | (173 + 12549) % 211 = 62 |
And so on.
This type of PRNG is called a Linear Congruential Generator, or LCG. The name isn’t terribly important, but you can impress your family and friends by saying that you spent your day creating your own Linear Congruential Generator.
There are a couple of other items I need to point out. You can’t pull any old LCG formula out of your hat, like I admittedly did, as it takes mathematical expertise to choose the “best” formula. For example, the number we’re dividing by (which in our example is 211) must be a prime number. You’ll read more about this soon.
There’s also another loose end to tie up. We now have a method to generate the next random number in our sequence. But how do we determine the first number?
For this, the programming language provides a starting value, which is called the seed. The seed can be any arbitrary number hardcoded straight into the programming language and used the first time the computer is asked to generate a random number. From then on, the sequence continues based on the LCG’s math formula. Many languages, including Python, use the current time to determine the seed.
I whipped up the following code to serve as a basic LCG so that we can see what an LCG implementation looks like:
| | def generate_random_sequence(seed): |
| | sequence = [] |
| | previous_number = seed |
| | |
| | while True: |
| | next_number_in_sequence = (previous_number + 12549) % 211 |
| | if next_number_in_sequence in sequence: |
| | break |
| | else: |
| | sequence.append(next_number_in_sequence) |
| | previous_number = next_number_in_sequence |
| | |
| | return sequence |
To run this code, I’ll call the generate_random_sequence function and pass in an arbitrary seed of 999:
| | print(generate_random_sequence(999)) |
This produces the following sequence:
| | [44, 144, 33, 133, 22, 122, 11, 111, 0, 100, 200, 89, |
| | 189, 78, 178, 67, 167, 56, 156, 45, 145, 34, 134, 23, |
| | 123, 12, 112, 1, 101, 201, 90, 190, 79, 179, 68, 168, |
| | 57, 157, 46, 146, 35, 135, 24, 124, 13, 113, 2, 102, |
| | 202, 91, 191, 80, 180, 69, 169, 58, 158, 47, 147, 36, |
| | 136, 25, 125, 14, 114, 3, 103, 203, 92, 192, 81, 181, |
| | 70, 170, 59, 159, 48, 148, 37, 137, 26, 126, 15, 115, |
| | 4, 104, 204, 93, 193, 82, 182, 71, 171, 60, 160, 49, |
| | 149, 38, 138, 27, 127, 16, 116, 5, 105, 205, 94, 194, |
| | 83, 183, 72, 172, 61, 161, 50, 150, 39, 139, 28, 128, |
| | 17, 117, 6, 106, 206, 95, 195, 84, 184, 73, 173, 62, |
| | 162, 51, 151, 40, 140, 29, 129, 18, 118, 7, 107, 207, |
| | 96, 196, 85, 185, 74, 174, 63, 163, 52, 152, 41, 141, |
| | 30, 130, 19, 119, 8, 108, 208, 97, 197, 86, 186, 75, |
| | 175, 64, 164, 53, 153, 42, 142, 31, 131, 20, 120, 9, |
| | 109, 209, 98, 198, 87, 187, 76, 176, 65, 165, 54, 154, |
| | 43, 143, 32, 132, 21, 121, 10, 110, 210, 99, 199, 88, |
| | 188, 77, 177, 66, 166, 55, 155] |
Conveniently, this sequence contains all the numbers from 0 to 210 with each number occurring once. It turns out that this sequence has great uniformity and a period of 210.
However, this sequence is not so great when it comes to lacking pattern. Look at every pair of consecutive numbers: 44 and 144, 33 and 133, 22 and 122. That’s a pattern if I’ve ever seen one.
Even without mathematical expertise, we can at least play with the numbers of the LCG formula to see if we can get a better pattern. If I change the formula to divide by 111, which is not a prime number, we get this result:
| | [6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72, 78, 84, |
| | 90, 96, 102, 108, 3, 9, 15, 21, 27, 33, 39, 45, 51, 57, |
| | 63, 69, 75, 81, 87, 93, 99, 105, 0] |
The first thing that jumps out at me is that this sequence has a short period.
In truth, this happens because when we get to the final number, 0, and apply the formula to it, we get:
| | (0 + 12549) % 111 = 6 |
We get 6, but you’ll notice that 6 is the first number in the sequence, so the sequence starts over from the beginning. That is, once the “current” generated number is 6, the next number must be 12 since that’s what our LCG formula dictates. We’re forced to return to the beginning of our sequence.
So, for an LCG to uniformly generate every number in a range of, say, 0 through 101, the LCG must generate each of those numbers once and only once before beginning the sequence all over again. The sequence will “short circuit” as soon as we generate a number we’ve already generated before, and we are forced to jump back to that point in the sequence.
Now, if you were to ask Python to generate an integer between 0 and 9 using random.randint(0, 9), it’s possible to get the same result twice in a row. But then the sequence would get locked into generating that same number over and over again. If, for example, the LCG formula says that the previous result of 4 produces another 4, then there’s no way to ever stop generating a 4!
The answer, though, is that when you execute random.randint(0, 9), under the hood, Python is generating a much longer number using random.random(), such as 0.5339767180649452, like you saw earlier. Python then does something along the lines of returning the final digit, like the 2 in our example.
However, when the computer generates the next number, it’s performing the LCG formula on 0.5339767180649452. This could produce a number like 0.11364654704325492, whose final digit also happens to be a 2. So it may appear as if you received the same number twice, but in truth, Python has generated a completely different number with many more digits. It’s just that both numbers happen to end with a 2.
This explanation might make sense for generating numbers between 0 and 9, but what about generating numbers between, say, 116 and 865? Indeed, the math that Python uses is slightly more complex than simply grabbing a final digit, but in any case, Python first generates a longer number and then converts it into a number in the range you’re looking for.
You saw that our previous LCG formula of (x + 12549) % 211 produced a sequence with a distinct pattern, which is problematic. Let’s tweak our formula again, this time using the numbers: (previous_number + 1223) % 227.
This time, we get:
| | [179, 40, 128, 216, 77, 165, 26, 114, 202, 63, 151, |
| | 12, 100, 188, 49, 137, 225, 86, 174, 35, 123, 211, 72, 160, |
| | 21, 109, 197, 58, 146, 7, 95, 183, 44, 132, 220, 81, 169, |
| | 30, 118, 206, 67, 155, 16, 104, 192, 53, 141, 2, |
| | 90, 178, 39, 127, 215, 76, 164, 25, 113, 201, 62, 150, |
| | 11, 99, 187, 48, 136, 224, 85, 173, 34, 122, 210, 71, 159, |
| | 20, 108, 196, 57, 145, 6, 94, 182, 43, 131, 219, 80, 168, |
| | 29, 117, 205, 66, 154, 15, 103, 191, 52, 140, 1, 89, 177, |
| | 38, 126, 214, 75, 163, 24, 112, 200, 61, 149, 10, 98, 186, |
| | 47, 135, 223, 84, 172, 33, 121, 209, 70, 158, 19, 107, 195, |
| | 56, 144, 5, 93, 181, 42, 130, 218, 79, 167, 28, 116, 204, |
| | 65, 153, 14, 102, 190, 51, 139, 0, 88, 176, 37, 125, 213, |
| | 74, 162, 23, 111, 199, 60, 148, 9, 97, 185, 46, 134, 222, |
| | 83, 171, 32, 120, 208, 69, 157, 18, 106, 194, 55, 143, 4, |
| | 92, 180, 41, 129, 217, 78, 166, 27, 115, 203, 64, 152, 13, |
| | 101, 189, 50, 138, 226, 87, 175, 36, 124, 212, 73, 161, 22, |
| | 110, 198, 59, 147, 8, 96, 184, 45, 133, 221, 82, 170, 31, |
| | 119, 207, 68, 156, 17, 105, 193, 54, 142, 3, 91] |
This has all numbers from 0 through 226. We have good uniformity, a longish period, and a pattern that is more difficult to detect. Now, there is a pattern (can you find it?), but since it’s harder to see, our sequence has a greater appearance of randomness than before.
In the end, we managed to create a sequence of seemingly mixed-up numbers without having to store the entire sequence. Instead, we simply allow the LCG to do the work of computing the next number. The trick is to find the right formula, which takes mathematical expertise.
Luckily, mathematicians and computer scientists have developed a whole slew of PRNGs, some of which are LCGs and some that use other types of math. These PRNGs do the heavy lifting for us, and some have awesome names like Threefish, Philox, Mersenne Twister, and Blum Blum Shub. As of this writing, Python uses the Mersenne (pronounced mer-SEN) Twister, which is a type of LCG.
The Mersenne Twister is impressive, as it uniformly generates long numbers and also has a super-long period of about (219937). That’s a massive load of numbers before the sequence wraps around to the beginning again.
Another important attribute of the Mersenne Twister is that a computer can execute its calculations quickly. Some LCGs are so involved that it may take the computer a stretch of time to compute the next number. While the Mersenne Twister is itself a complex formula, it’s one that a computer can execute with great speed. This is important, as you’ll see in the next section that an operation like shuffling an array has to generate many random numbers in succession. If generating a single random number is not fast, shuffling an entire array can become way too slow.