DEVELOPMENT

Posted on December 3, 2014 by: Sean Eshbaugh, Back-End Developer

When generating sample data, it’s not uncommon to need data with some sort of weighted distribution. For such sampling, `Math.random()` (or your language’s random number function) can be a bit too random. Today, we’ll be taking a look at a simple and extensible method for generating weighted random numbers in JavaScript.

Before we examine any code, let’s take a look at what the main `WeightedRandom` function expects as its sole argument:

```[
{"value": 1, "weight": 1},
{"value": 2, "weight": 1},
{"value": 3, "weight": 2}
]
```

Nothing too complicated, at least at first glance. Just an array of objects, each one having `value` and `weight` properties. `weight` is expected to be an integer. `value` can be anything we want, but for the purposes of the demonstrations below, we will just use integers or functions that return integers. It’s important to note that either `value` or `weight` can be a function. If `weight` is a function, it will be evaluated when we create a new `WeightedRandom` object, and likewise if `value` is a function, it will be evaluated whenever `next` is called.

Now that we know what to give `WeightedRandom`, let’s take a look at the function itself:

```function WeightedRandom(shape) {
var weightedRandom, valueFunction, weight, i, j;

weightedRandom = this;

weightedRandom.samples = [];

for (i = 0; i < shape.length; i += 1) {
if (shape[i] && shape[i].hasOwnProperty("value") && shape[i].hasOwnProperty("weight")) {
valueFunction = getValueFunction(shape[i].value);

weight = getWeight(shape[i].weight);

for (j = 0; j < weight; j += 1) {
weightedRandom.samples.push(valueFunction);
}
}
}
}
```

For each element in the `shape` array, `WeightedRandom` calls `getValueFunction`, passing in the `value`. If it’s a function, it returns itself; otherwise, it gets a wrapper function (we’ll see why when we look at the `Next` function) that returns the value. After that, the `weight` of each value is calculated (if necessary) by `getWeight`. This allows us to pass a function as the `weight`. Finally, the `valueFunction` is added to the `samples` array `weight` number of times. That last part is the most important piece of this method of generating weighted random numbers. The `sample` array ends up containing the values from which we want to take a sample, but the number of times each appears in the array is equal to its weight. Relatively “heavier” values appear more times, and “lighter” values appear fewer times. This brings us to the `next` function, which we use to actually get our random numbers.

```WeightedRandom.prototype.next = function() {
var weightedRandom;

weightedRandom = this;

return weightedRandom.samples[Math.floor(Math.random() * weightedRandom.samples.length)]();
};
```

Since all the interesting work has already been done, `Next` just randomly selects a sample value function and then calls it to get our sample value. This is why `WeightedRandom` created a wrapper function, if necessary, for each value. By doing so, we can keep `next` simple.

The entire source for `WeightedRandom` can be seen here.

Unweighted Random Number Graphs

Unweighted Example

Here is a graph of the distribution of 1,000 random integers between 1 and 10. Since there’s no weighting to the randomness, you can see that the distribution is mostly flat. You can expect some variation, but that’s simply because randomness is clumpy.

Results (1,000 Samples):

Weighted Random Number Graphs

Weighted Example 1

This example shows how we can use `WeightedRandom` to generate a bimodal distribution.

Shape:

```shape = [
{"value": 10, "weight": 1},
{"value": 20, "weight": 2},
{"value": 30, "weight": 3},
{"value": 40, "weight": 2},
{"value": 50, "weight": 1},
{"value": 60, "weight": 1},
{"value": 70, "weight": 1},
{"value": 80, "weight": 3},
{"value": 90, "weight": 4},
{"value": 100, "weight": 2}
];
```

Results (1,000 Samples):

Weighted Example 2

Below is a slightly more complex shape for our random numbers. The values toward the ends of the list are weighted more strongly, but 0, the value in the middle, is weighted using a function. This function returns the current time in milliseconds since the Unix Epoch modulo 12. This means that the weight of the value 0 varies with time. Try pressing the “Resample” button many times to see the number of samples that end up being 0 jump around.

Shape:

```shape = [
{"value": -9, "weight": 10},
{"value": -8, "weight": 9},
{"value": -7, "weight": 8},
{"value": -6, "weight": 7},
{"value": -5, "weight": 6},
{"value": -4, "weight": 5},
{"value": -3, "weight": 4},
{"value": -2, "weight": 3},
{"value": -1, "weight": 2},
{"value": 0, "weight": function() { return Date.now() % 12; }},
{"value": 1, "weight": 2},
{"value": 2, "weight": 3},
{"value": 3, "weight": 4},
{"value": 4, "weight": 5},
{"value": 5, "weight": 6},
{"value": 6, "weight": 7},
{"value": 7, "weight": 8},
{"value": 8, "weight": 9},
{"value": 9, "weight": 10}
];
```

Results (1,000 Samples):

Weighted Example 3

Finally, the last example lets you create your own weighted distribution. Just click “Add Value,” enter an integer for the value and an integer for the weight, and then click “Resample.”

Shape:

TL;DR
Weighted Random Numbers
Resources

Up Next:

More Than a Mommy Blog