Rounding random numbers

Saturday, April 02, 2022

Lately, I’ve been rewriting this site to build on Jekyll. As I was working on the Computers are Useless page, decided to rewrite a Java applet in Javascript. (Since not very few people can see the applet because modern browsers no longer support them.)

While writing the JS version of Computers are Useless, an interesting note in the MDN page for Math.random.

Note: It might be tempting to use Math.round() to accomplish that, but doing so would cause your random numbers to follow a non-uniform distribution, which may not be acceptable for your needs.

Intuitively, it makes sense – for a given range, rounding would distribute the frequency for the lower half to the lowest value, while the higher half goes to the highest value. In effect, it should make it so that the extremities of the numerical range would have a lower (half?) frequency than the rest of the range.

Let’s actually experiment by trying the two methods. The easiest way to try is to write code that generates random integers between 0 and 9. One method is to use Math.random, while the other is to use the technique written in the MDN article.

Once we generate the random numbers, we’ll create a histogram, or a distribution of which numbers were encountered how many times. This will reveal the frequencies of the values generated by the two methods.

Below is output from a Javascript to demonstrate this. (It’s Javascript running live on your browser! 😉️)

Here’s the code that was used to produce the above output.

const counts = new Map();
for (i = 0; i < 100000; i++) {
  const value = Math.round(Math.random() * 9);
  if (!counts.has(value)) {
    counts.set(value, 0);
  }
  counts.set(value, counts.get(value) + 1);
}

You’ll notice that 0 and 9 only get half of the counts as the other numbers. This is because we’re naively using Math.round. For example 0 will get selected only when the value is between 0.0 and 0.5, while 1 will be selected when the value is between 0.5 and 1.5. Likewise, 9 is selected only when the value is between 9.0 and 9.5. That’s why the “edges” get only half of the counts.

That gives us an observation – if we want to use Math.round, we’ll need to take into account the 0.5 that the edges are missing. We could make the Math.round take a range of -0.5 through 9.5 so we’d get an even distribution.

Here’s the output of an amended piece of code:

And here’s the amended code:

const counts2 = new Map();
for (i = 0; i < 100000; i++) {
  const value = Math.round((Math.random() * 10) - 0.5);
  if (!counts2.has(value)) {
    counts2.set(value, 0);
  }
  counts2.set(value, counts2.get(value) + 1);
}