7

I would like to return an array which has a set of unique elements randomly distributed according to custom frequency. My real world use-case is the repetition of carousel images based on a qualitative weighting of how popular those images are.

E.g. suppose I have 5 elements with weights:

A, 20% B, 50% C, 80% D, 10%

I would like to write a function that, given a length, tries to approximate a sequence such that C will appear eight times more often than D; D will appear 5 times less often than B; A will appear three times less often than C.

Plato
  • 10,812
  • 2
  • 41
  • 61
metalaureate
  • 7,572
  • 9
  • 54
  • 93
  • Should your percentages sum to 100? –  May 12 '15 at 23:59
  • One way is to say for each 10% i insert 1 number in the domain of the random generated numbers. For example : A --> 1 or 2 , B --> 3 4 5 6 7, D --> 8 , C --> ... Depending on accuracy you can add 1 number in the domain for each 1%, 5%, 10%, ... – Kevin May 13 '15 at 00:01
  • Side note: if you use more common name "random weighted distribution" you'd get plenty of answers in all languages you ever new existed :) – Alexei Levenkov May 13 '15 at 00:03
  • Weighted random numbers: http://stackoverflow.com/questions/8435183/generate-a-weighted-random-number – Drakes May 13 '15 at 00:20
  • Might it be more effective to change how *long* a single image is displayed based on its popularity? For instance, show A for 2 seconds, B for 5 seconds, C for 8 seconds, and D for 1 second? – prekolna May 13 '15 at 00:21

4 Answers4

11

C will appear eight times more often than D; D will appear 5 times less often than B; A will appear three times less often than C.

You can do that with a weighted array of your elements:

var elems = ["A", "B", "C", "D"];
var weights = [2, 5, 8, 1]; // weight of each element above
var totalWeight = weights.reduce(add, 0); // get total weight (in this case, 16)

function add(a, b) { return a + b; } // helper function

var weighedElems = [];
var currentElem = 0;
while (currentElem < elems.length) {
  for (i = 0; i < weights[currentElem]; i++)
    weighedElems[weighedElems.length] = elems[currentElem];
  currentElem++;
}

console.log(weighedElems);

This will produce an array like

["A", "A", "B", "B", "B", "B", "B", "C", "C", "C", "C", "C", "C", "C", "C", "D"]

so you can choose randomly from that like so

var rnd = Math.floor(Math.random() * totalWeight);
console.log(weighedElems[rnd]);

Resources:

Community
  • 1
  • 1
Drakes
  • 23,254
  • 3
  • 51
  • 94
4

Let's say you take your distribution numbers as an array of objects, like this:

var items = [
    {item: "A", weight: 20}, 
    {item: "B", weight: 50}, 
    {item: "C", weight: 80},
    {item: "D", weight: 10}
];

This removes any assumption that your weights add up to 100% - they might be click-counts, or votes, or any other value you like. Then you can do this:

function weightedSelect(items) {
    // Get the total, and make the weights cummulative
    var total = items.reduce(function(sum, item){
        item.weight = item.weight + sum;
        return item.weight;
    },0);

    var r = Math.random() * total;

    // Can't use .forEach() here because we want early termination
    for (var i = 0; i < items.length; i++) {
         if (r < items[i].weight)
             return items[i].item;
    }
}

I'm not sure how this compares to the other implementations for efficiency, but it's concise.

S McCrohan
  • 6,663
  • 1
  • 30
  • 39
  • This cannot be right. Just testing the function shows `D` is selected way too often, which should not be with a weight of 10. Accumulating the total and assigning at each step doesn't make any sense. – James Wilkins May 13 '23 at 08:25
2

To expand on a_gupta's answer:

function pick_bin(binProbabilities){     // e.g. [0.1, 0.3, 0.3, 0.3]
  var cumulative = [];                   // e.g. [0.1, 0.4, 0.7, 1]
  var accumulator = 0;

  // Iterating over an array with forEach:
  binProbabilities.forEach(function(item, index){
    var prob = Number(item);
    accumulator += prob;
    cumulative[index] = accumulator;
  })

  if(accumulator !== 1){
    throw new Error('Sum of binProbabilities must equal 1')
  }

  var n = binProbabilities.length;
  var rFloat = Math.random();

  // Iterating over an array with for:
  for(var i=0; i<n; i++){
    var pcI = cumulative[i];      // cumulative probability of this index
    if(pcI >= rFloat){            // Found the first bin fitting the random number
      console.log(i);
      return i;
    }
  }
}

pick_bin([1]); // returns 0 every time
pick_bin([.5, .5]) // returns 0,1 50/50
pick_bin([0.1, 0.3, 0.3, 0.3])

followup for your > 100% example, you can recalculate the weights to make them equal 1 (for a valid probability)

Desired weightings:     20% 50% 80% 10%
Sum these weights:      20 + 50 + 80 + 10 = 160
Divide each by the sum: 2/16, 5/16, 8/16, 1/16
Now they sum to 1
Plato
  • 10,812
  • 2
  • 41
  • 61
1

There is a very simple solution. The random() method returns a number between 0 and 1 inclusive.

Eg if the number returned is > 0.2, then output C (ie 80% chance).

A. Gupta
  • 54
  • 3
  • Yes. But, your question is a bit vague. If all the percentages must add up to 100, then this is essentially what you are looking for. Really, the only sort of trick is knowing about the random() method. If they don't have to add up to 100, the logic is a little more complicated. – A. Gupta May 13 '15 at 00:18
  • 1
    If the number returned is > 0.2, and you return C, then how will you ever output B? – Drakes May 13 '15 at 00:20