29

Say, if I wanted to generate an unbiased random number between min and max, I'd do:

var rand = function(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
};

But what if I want to generate a random number between min and max but more biased towards a value N between min and max to a degree D? It's best to illustrate it with a probability curve:

enter image description here

Colonel Panic
  • 132,665
  • 89
  • 401
  • 465
c00000fd
  • 20,994
  • 29
  • 177
  • 400
  • 3
    http://en.wikipedia.org/wiki/Gaussian_function – Tesseract Mar 29 '15 at 02:53
  • possible duplicate of [Generating random numbers in Javascript in a specific range?](http://stackoverflow.com/questions/1527803/generating-random-numbers-in-javascript-in-a-specific-range) – Ryan Mar 29 '15 at 03:13
  • 8
    @ryan that is not a duplicate as it is random without bias. OP asks for a biased result. –  Mar 29 '15 at 03:19
  • Don't you need another parameter; something that controls the "spread"? – Salman A Mar 29 '15 at 11:33
  • @SalmanA: I understand that one can make it more complicated, thus I don't think it needs more parameters. – c00000fd Mar 30 '15 at 02:29
  • OP do you know about 'probability distribution' and 'density function'? You'll find them in any introduction to probability book. The language will help you express what you want precisely (and think about how to solve it). Your picture is very good, that's a density function. – Colonel Panic Mar 30 '15 at 14:51

4 Answers4

47

Here is one way:

  • Get a random number in the min-max range
  • Get a random normalized mix value
  • Mix random with bias based on random mix

Ie., in pseudo:

Variables:
  min = 0
  max = 100
  bias = 67      (N)
  influence = 1  (D) [0.0, 1.0]

Formula:
  rnd = random() x (max - min) + min
  mix = random() x influence
  value = rnd x (1 - mix) + bias x mix

The mix factor can be reduced with a secondary factor to set how much it should influence (ie. mix * factor where factor is [0, 1]).

Demo

This will plot a biased random range. The upper band has 1 as influence, the bottom 0.75 influence. Bias is here set to be at 2/3 position in the range. The bottom band is without (deliberate) bias for comparison.

var ctx = document.querySelector("canvas").getContext("2d");
ctx.fillStyle = "red"; ctx.fillRect(399,0,2,110);  // draw bias target
ctx.fillStyle = "rgba(0,0,0,0.07)";

function getRndBias(min, max, bias, influence) {
    var rnd = Math.random() * (max - min) + min,   // random in range
        mix = Math.random() * influence;           // random mixer
    return rnd * (1 - mix) + bias * mix;           // mix full range and bias
}

// plot biased result
(function loop() {
  for(var i = 0; i < 5; i++) {  // just sub-frames (speedier plot)
    ctx.fillRect( getRndBias(0, 600, 400, 1.00),  4, 2, 50);
    ctx.fillRect( getRndBias(0, 600, 400, 0.75), 55, 2, 50);
    ctx.fillRect( Math.random() * 600          ,115, 2, 35);
  }
  requestAnimationFrame(loop);
})();
<canvas width=600></canvas>
8

Fun: use the image as the density function. Sample random pixels until you get a black one, then take the x co-ordinate.

enter image description here

Code:

getPixels = require("get-pixels"); // npm install get-pixels

getPixels("distribution.png", function(err, pixels) {
  var height, r, s, width, x, y;
  if (err) {
    return;
  }
  width = pixels.shape[0];
  height = pixels.shape[1];
  while (pixels.get(x, y, 0) !== 0) {
    r = Math.random();
    s = Math.random();
    x = Math.floor(r * width);
    y = Math.floor(s * height);
  }
  return console.log(r);
});

Example output:

0.7892316638026386
0.8595335511490703
0.5459279934875667
0.9044852438382804
0.35129814594984055
0.5352215224411339
0.8271261665504426
0.4871773284394294
0.8202084102667868
0.39301465335302055

Scale to taste.

Colonel Panic
  • 132,665
  • 89
  • 401
  • 465
5

Just for fun, here's a version that relies on the Gaussian function, as mentioned in SpiderPig's comment to your question. The Gaussian function is applied to a random number between 1 and 100, where the height of the bell indicates how close the final value will be to N. I interpreted the degree D to mean how likely the final value is to be close to N, and so D corresponds to the width of the bell - the smaller D is, the less likely is the bias. Clearly, the example could be further calibrated.

(I copied Ken Fyrstenberg's canvas method to demonstrate the function.)

function randBias(min, max, N, D) {
  var a = 1,
      b = 50,
      c = D;

  var influence = Math.floor(Math.random() * (101)),
    x = Math.floor(Math.random() * (max - min + 1)) + min;

  return x > N 
         ? x + Math.floor(gauss(influence) * (N - x)) 
         : x - Math.floor(gauss(influence) * (x - N));

  function gauss(x) {
    return a * Math.exp(-(x - b) * (x - b) / (2 * c * c));
  }
}

var ctx = document.querySelector("canvas").getContext("2d");
ctx.fillStyle = "red";
ctx.fillRect(399, 0, 2, 110);
ctx.fillStyle = "rgba(0,0,0,0.07)";

(function loop() {
  for (var i = 0; i < 5; i++) {
    ctx.fillRect(randBias(0, 600, 400, 50), 4, 2, 50);
    ctx.fillRect(randBias(0, 600, 400, 10), 55, 2, 50);
    ctx.fillRect(Math.random() * 600, 115, 2, 35);
  }
  requestAnimationFrame(loop);
})();
<canvas width=600></canvas>
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • Thanks. It would really help if you added a live sample like Ken Fyrstenberg did in his post. It really helps to see the effectiveness or bias of the PRNG. – c00000fd Mar 30 '15 at 02:31
  • @c00000fd added! Thinking about the shape and location of the "bell", as well as other parameters, could help refine the results. – גלעד ברקן Mar 31 '15 at 04:44
4

Say when you use Math.floor(Math.random() * (max - min + 1)) + min;, you are actually creating a Uniform distribution. To get the data distribution in your chart, what you need is a distribution with non-zero skewness.

There are different techniques to get those kinds of distributions. Here is an example of beta distribution found on stackoverflow.


Here is the example summarized from the link:

unif = Math.random()  // The original uniform distribution.

And we can transfer it into beta distribution by doing

beta = sin(unif*pi/2)^2 // The standard beta distribution

To get the skewness shown in your chart,

beta_right = (beta > 0.5) ? 2*beta-1 : 2*(1-beta)-1;

You can change the value 1 to any else to have it skew to other value.

LumiG
  • 452
  • 1
  • 6
  • 13
  • 2
    Thanks for the explanation. While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. – Lea Cohen Mar 29 '15 at 11:43