3

So I'm very familiar with the good old

Math.floor(Math.random() * (max - min + 1)) + min;

and this works very nicely with small numbers, however when numbers get larger this quickly becomes biased and only returns numbers one zero below it (for ex. a random number between 0 and 1e100 will almost always (every time I've tested, so several billion times since I used a for loop to generate lots of numbers) return [x]e99). And yes I waited the long time for the program to generate that many numbers, twice. By this point, it would be safe to assume that the output is always [x]e99 for all practical uses.

So next I tried this

Math.floor(Math.pow(max - min + 1, Math.random())) + min;

and while that works perfectly for huge ranges it breaks for small ones. So my question is how can do both - be able to generate both small and large random numbers without any bias (or minimal bias to the point of not being noticeable)?

Note: I'm using Decimal.js to handle numbers in the range -1e2043 < x < 1e2043 but since it is the same algorithm I displayed the vanilla JavaScript forms above to prevent confusion. I can take a vanilla answer and convert it to Decimal.js without any trouble so feel free to answer with either.

Note #2: I want to even out the odds of getting large numbers. For example 1e33 should have the same odds as 1e90 in my 0-1e100 example. But at the same time I need to support smaller numbers and ranges.

23
  • See How do I add 1 to a big integer represented as a string in JavaScript? Commented Aug 9, 2017 at 2:12
  • a billion is nothing compared to 1e100. I cannot reproduce the result always being 1e99. Example result: Math.floor(Math.random() * (1e100 + 1));, 8.942283537027985e+98 Commented Aug 9, 2017 at 2:15
  • @guest271314 not sure how that has any relevance to generating random numbers? Commented Aug 9, 2017 at 2:22
  • Generate the random numbers as in values less than cause the "e" artifact of JavaScript, that is for numbers greater than Number.MAX_SAFE_INTEGER, then create a concatenated string containing the N digits Commented Aug 9, 2017 at 2:25
  • @ASDFGerte Just tested with your code and still got only e99's. And yes a billion is nothing compared to 1e100 but the whole point of generating a random number in that range is that it won't be biased. A billion different ones with nothing under e99 is a huge problem. Even if I generated only two random numbers if they were consistently [x]e99 that would be a huge issue. Commented Aug 9, 2017 at 2:25

3 Answers 3

2

Your Problem is Precision. That's the reason you use Decimal.js in the first place. Like every other Number in JS, Math.random() supports only 53 bit of precision (Some browser even used to create only the upper 32bit of randomness). But your value 1e100 would need 333 bit of precision. So the lower 280 bit (~75 decimal places out of 100) are discarded in your formula.

But Decimal.js provides a random() method. Why don't you use that one?

function random(min, max){
    var delta = new Decimal(max).sub(min);
    return Decimal.random( +delta.log(10) ).mul(delta).add(min);
}

Another "problem" why you get so many values with e+99 is probability. For the range 0 .. 1e100 the probabilities to get some exponent are

e+99  => 90%, 
e+98  =>  9%,
e+97  =>  0.9%,
e+96  =>  0.09%,
e+95  =>  0.009%,
e+94  =>  0.0009%,
e+93  =>  0.00009%,
e+92  =>  0.000009%,
e+91  =>  0.0000009%,
e+90  =>  0.00000009%,
and so on

So if you generate ten billion numbers, statistically you'll get a single value up to 1e+90. That are the odds.

I want to even out those odds for large numbers. 1e33 should have the same odds as 1e90 for example

OK, then let's generate a 10random in the range min ... max.

function random2(min, max){
    var a = +Decimal.log10(min), 
        b = +Decimal.log10(max);
    //trying to deal with zero-values. 
    if(a === -Infinity && b === -Infinity) return 0;  //a random value between 0 and 0 ;)
    if(a === -Infinity) a = Math.min(0, b-53);
    if(b === -Infinity) b = Math.min(0, a-53);

    return Decimal.pow(10, Decimal.random(Math.abs(b-a)).mul(b-a).add(a) );
}

now the exponents are pretty much uniformly distributed, but the values are a bit skewed. Because 101 to 101.5 10 .. 33 has the same probability as 101.5 to 102 34 .. 100

Sign up to request clarification or add additional context in comments.

10 Comments

I am using the one that Decimal.js provides, although I wasn't setting the number of sig figs for it. I will try that.
Also I guess I have probably worded my question badly but I want to even out those odds for large numbers. 1e33 should have the same odds as 1e90 for example.
@LJ So you do not want uniform randomness? The formula you used gives numbers uniformly at random, but note that most numbers between 0 and 9e99 are larger than 1e99.
@LJTalbot Wanting 1e33 to be as common as 1e90 is like generating numbers between 0 and 999, but wanting to get numbers between 0 and 9 1/3 of the time, numbers between 10 and 99 1/3 of the time and numbers between 100 and 999 the other 1/3 (whereas if you generated numbers uniformly at random they would fall between 100 and 999 90% of the time). There is nothing wrong with wanting to do that, as long as you are well aware that is not uniform randomness.
@LJTalbot I've added a snippet that generates random values with uniformly distributed exponents
|
1

The issue with Math.random() * Math.pow(10, Math.floor(Math.random() * 100)); at smaller numbers is that random ranges [0, 1), meaning that when calculating the exponent separately one needs to make sure the prefix ranges [1, 10). Otherwise you want to calculate a number in [1eX, 1eX+1) but have e.g. 0.1 as prefix and end up in 1eX-1. Here is an example, maxExp is not 100 but 10 for readability of the output but easily adjustable.

let maxExp = 10;

function differentDistributionRandom() {
  let exp = Math.floor(Math.random() * (maxExp + 1)) - 1;
  if (exp < 0) return Math.random();
  else return (Math.random() * 9 + 1) * Math.pow(10, exp);
}

let counts = new Array(maxExp + 1).fill(0).map(e => []);
for (let i = 0; i < (maxExp + 1) * 1000; i++) {
  let x = differentDistributionRandom();
  counts[Math.max(0, Math.floor(Math.log10(x)) + 1)].push(x);
}

counts.forEach((e, i) => {
  console.log(`E: ${i - 1 < 0 ? "<0" : i - 1}, amount: ${e.length}, example: ${Number.isNaN(e[0]) ? "none" : e[0]}`);
});

You might see the category <0 here which is hopefully what you wanted (the cutoff point is arbitrary, here [0, 1) has the same probability as [1, 10) as [10, 100) and so on, but [0.01, 0.1) is again less likely than [0.1, 1))

If you didn't insist on base 10 you could reinterpret the pseudorandom bits from two Math.random calls as Float64 which would give a similar distribution, base 2:

function exponentDistribution() {
  let bits = [Math.random(), Math.random()];
  let buffer = new ArrayBuffer(24);
  let view = new DataView(buffer);
  
  view.setFloat64(8, bits[0]);
  view.setFloat64(16, bits[1]);
  
  //alternatively all at once with setInt32
  for (let i = 0; i < 4; i++) {
    view.setInt8(i, view.getInt8(12 + i));
    view.setInt8(i + 4, view.getInt8(20 + i));
  }
  
  return Math.abs(view.getFloat64(0));
}

let counts = new Array(11).fill(0).map(e => []);

for (let i = 0; i < (1 << 11) * 100; i++) {
  let x = exponentDistribution();
  let exp = Math.floor(Math.log2(x));
  if (exp >= -5 && exp <= 5) {
    counts[exp + 5].push(x);
  }
}

counts.forEach((e, i) => {
  console.log(`E: ${i - 5}, amount: ${e.length}, example: ${Number.isNaN(e[0]) ? "none" : e[0]}`);
});

This one obviously is bounded by the precision ends of Float64, there are some uneven parts of the distribution due to some details of IEEE754, e.g. denorms/subnorms and i did not take care of special values like Infinity. It is rather to be seen as a fun extra, a reminder of the distribution of float values. Note that the loop does 1 << 11 (2048) times a number iterations, which is about the exponent range of Float64, 11 bit, [-1022, 1023]. That's why in the example each bucket gets approximately said number (100) hits.

Comments

0

You can create the number in increments less than Number.MAX_SAFE_INTEGER, then concatenate the generated numbers to a single string

const r = () => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);

let N = "";

for (let i = 0; i < 10; i++) N += r();

document.body.appendChild(document.createTextNode(N));

console.log(/e/.test(N));

5 Comments

This has nothing to do with my question.
@LJTalbot Yes, it does, you are presently just not able to "see" the algorithm and practical application. That is ok. Not every one needs to view a question or answer at the same perspective, else you would not have asked the question itself - you would be capable of viewing all perspectives simultaneously and thus be able to resolve your own inquiries. You can select any .length of string from a string of N .length to get result without bias, as there is no formal distribution at the original string of N length, where N is for example a string having .length of 100 or more
My question is how to generate large numbers, it has nothing to do with the max safe integer. Also, what do you mean by "see"? Are you saying that no matter what answer you give eventually I'll need it so it's acceptable? And your code throws the error Cannot read property body of undefined, as I expected it to. It seems that you didn't notice that this question is about Node.js.
Also I need to generate one integer at a time, not multiple.
@LJTalbot You can generate one integer at a time using approach at Answer - by first generating a large number as a string, then selecting random groups of indexes. Using a 0-9 based number system if you want to select a single digits you can select any single random index. If you want to select a number having fifty digits you can do so as well either at a single or multiple calls to .slice() then concatenating the result. Am not invested in whether you accept the Answer or not. The Answer is only a possible option. No error is thrown at stacksnippets.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.