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 i
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:
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
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).
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.