I have three options and I need to randomly pick one (in JavaScript). But not equally random. I want there to be a 10% chance of the first option, 20% chance of the second one, and 70% chance of the third.
Here are some ways to do that.
Imagine the data is like this:
var choices = [
[10, "apples"],
[20, "oranges"],
[70, "bananas"]
];
Plus we’ve set scoped variables for stuff like iterators and random numbers and stuff. Let’s just call this conceptual code, not production code.
Test between a min and max
The first place my mind went was to generate a random number between 1-100 and then test if that number was between…
- 1-10 for apples
- 11-30 for oranges
- 31-100 for bananas
It’s a bit verbose to get those min and max values. You run one loop to iterate through all the choices. The minimum value is zero for the first option, and the sum of all the previous options for all the rest. The maximum value is the sum of the current and all previous options combined.
On each iteration of the loop, you test to see if the random number is within the range that was just generated. If so, set the final choice and break. If not, do another iteration.
function pickChoice() {
rand = Math.floor(Math.random() * 100);
choice = -1;
for (i = 0; i < choices.length; i++) {
// set up min
if (i === 0) {
min = 0;
} else {
min = 0;
// add up all the values so far
for (i2 = 0; i2 < i; i2++) {
min += choices[i2][0];
}
// one higher
min++;
}
// set up max
if (i === 0) {
max = choices[i][0];
} else {
max = 0;
// add up all the values so far
for (i2 = 0; i2 < i + 1; i2++) {
max += choices[i2][0];
}
}
if (rand >= min && rand <= max) {
choice = i;
break;
}
}
// If the choice is still -1 here, something went wrong...
};
See the Pen % chances of Array items by Chris Coyier (@chriscoyier) on CodePen.
Test if above minimum
David DeSandro had a more clever idea (of course). Give each of the options a minimum value right off the bat. The minimum for each being the sum of all previous options. So the data becomes:
- 0 for apples
- 10 for oranges
- 30 for bananas
Then generate a random number, iterate through the choices, and if the random number is higher than the minimum, set the pick to that. The pick might be set a number of times in this loop, but it will ultimately select the correct option.
function pickChoice() {
var rand = Math.random() * 100, pick, choice, i;
// go through choices, update pick if rand is above bottom percent threshold
for ( i = 0, len = choices.length; i < len; i++ ) {
choice = choices[i];
if ( rand > choice.percent ) {
pick = choice;
}
}
return pick;
}
David also set the data from the values in the HTML for his fork.
See the Pen % chances of Array items by David DeSandro (@desandro) on CodePen.
Build a new Array of all possibilities
George Papadakis made a quick demo with yet another way. Build a new array where each choice is represented a proportionally correct number of times. So with our data, it could be simplified to
- 1 item in the new array is apples
- 2 items in the new array are oranges
- 7 items in the new array are bananas
Then when you generate a random number to choose one, you just pick that right out of the new array, like choices[rand]
.
// quick'n'dirty
NodeList.prototype.map = Array.prototype.map;
var choices = [],
lis = document.querySelectorAll('li')
.map(function (item) {
var percent = parseFloat(item.querySelector('span').textContent) / 10,
label = item.querySelector('strong').textContent;
for (var i = 0; i < percent; i++) {
choices.push(label)
}
});
function pick() {
var rnd = parseInt(Math.random() * 10);
document.querySelector('div.choice').innerHTML = choices[rnd];
}
See the Pen % chances of Array items by George Papadakis (@phaistonian) on CodePen.
More?
You could probably remix ideas from all of these into other solutions. Or do this an entirely different way. It’s always fun to see different solutions to the same problem!
My real-world use-case was that I needed to randomize some ads, but base them on percentage-share not just a random split. Any one of these could do that.
Here’s some ways to do that.
Should be-
Here’re some ways to do that.
I’m not sure that “here’re” is a proper contraction? I think I’ll go with “here are”. Thanks! I’m going to bury this thread as it’s related to the content of the article.
The first indefinite article in the title is incorrect. It should be an “a” not an “an”: “Choose a Random Option based on a Range” (The “R” in Random is not silent, so it should be preceded with “a”.)
Anyway, thanks for the post.
Ah yes, an artifact of editing the title too many times. I’m going to change it to “a” and then bury this thread as it won’t be too useful for people reading the comments for stuff related to the content.
Wow; the first two comments are both grammar sticklers! I just thought I’d throw in a positive comment about the article. Thanks for the variety of examples on ways one could do this. I wasn’t sure what the practical application would be, but when you explained your reasoning for it, it clicked. Good article!
Hiya
made another version for you :-)
With comments :
It’s been golfed a bit because i enjoy doing that – 81 chars :
I was about to respond with this same basic result. Except I’d make two changes. First, rather than requiring the user to make their scale add to 100, just calculate the sum. This way 1,2,7 is weighted the same a 10,20,70 or even 1000,2000,7000. I’d also sort the array, since there’s no guarantee the user did that either. My result was
Went ahead and improved the performance side of things.
http://jsperf.com/pick-from-choices
Note that due to floating points not being quite exact I forced integers instead. Marginally faster, too. I also made one that uses Int32Array and it gives Chrome quite a nice a boost:
Otherwise I don’t know what has happened to Chrome recently, because Firefox wipes the floor with Chrome in speed in other areas.
As an added bonus both of these also work in Internet Explorer.
@Vesa; doesn’t work in IE11 :/
Thanks for the jsPerf though!
@Tom Yes, imma’s code fails, but jsperf will keep running my code above the overly huge error message. I don’t know why jsperf doesn’t detect IE11 correctly though, it ranks it as “other”.
With this problem, I’d prefer using odds to percentages. In other words, don’t force the totals to equal 100. That way I can easily add another element to the array without adjusting all of my percentages. If I wanted them all to have the same likelihood, I’d just use “100” for each of them. If I wanted one element to be twice as likely, I’d enter 200. I find this more robust, and just as easy to use.
I took your idea of odds, but I wanted to get a more performant and generic function.
One way to use this with the help of lodash:
Pen for more explanation
The first two version are broken (a bit).
The ranges are wrong, they should be
– 0-9 for apples
– 10-29 for oranges
– 30-99 for bananas
as
Math.random() * 100
is a number lower than 100 and is possibly 0.So your percentages are distorted a bit.
(Your first algorithm chooses the following options:
Math.random()choice
0-10apples
11-30oranges
31-99bananas
The first option is chosen slightly too often and the last one is chosen slightly too rarely.)
In the second version it is even possible that no choice is set (if
Math.random()
returns exactly 0).Here’s a solution that runs in O(n) time and doesn’t require the sum to be 100 (or 1 or whatever). You do not need to pre-calculate anything and the items can be in any order. In this example, “apples” would be picked 1/6th, oranges 2/6, and bananas 3/6.
I like this method.
It relies on casting booleans to numbers. If our random number is 0.1 or less, it passes none of the tests, so the index is 0; if it is between 0.1 and 0.3, it passes one test, so the index is 1; if it is greater than 0.3, it passes both tests, and so the index is 2.
So much elegant and sleek in just 3 lines. Great!
Kudos on this one. Better than mine below, hadn’t thought of that. Well done. The boolean approach is something I can see using in the future. Thanks.
Thank you. I’ll be the first to admit this is a rather quick and dirty approach, but it’s good when you only have a few items and don’t need a full-blown subroutine to do the random selection.
I think there is a simpler way, sorry it is in Google spreadsheet format, too busy today…
1* is to transform true or false in 1 or 0.
Useless brackets are kept to ease understanding.
The possible results are 1, 2 or 3.
Just substract 1 to have a Javascript table index starting at 0.
Sorry, A1 is of course the random number, i.e. =rand() in the A1 cell.
CSSConfAU 2015 NEWS you might sneak to one of our delegates http://goo.gl/9vIsH2 who attended and see what he learnt.
if (i === 0) {
min = 0;
} else {
min = 0;
Instead, I suggest…
min = 0;
min += (i > 0) ? 1 : 0;
An alternative way to do this, similar to some above probably, but it works fine enough.
Just set the maximum in the array. Really there is not often a need to do a 100 random, unless you have a more complex set of information. Even this, we could’ve probably done more economically, but for a rough go, 1-10 will work just as well. Increase or decrease the number depending on the amount of complexity.
A final thought. For the sake of reusability I’d take the function I wrote above, strip the array out of it. Pass the array to the function as a parameter. Then do some initial checks to make sure that, 1, it is a proper multi-dimensional array that we’re looking for, and 2, that it’s ordered lowest to highest by the [n][0]. If it isn’t low-high, order it low-high. Then, use the math.random based on the highest value.
If we can use lodash, here is an example:
— generate an array, with the specified number of choices of each item /
— test this out
While lodash can be nice and helpful I find this more readable:
Generates the big array in one fast step and picking a random item is made much more clear via
Math.random()
than_.sample
as “sample” didn’t ring my bell as to what it actually means until I read the docs. I think one of the downsides of libraries like lodash is the sheer number of utility functions they have as there is always a new one you haven’t heard of even if you’ve used the lib for several months. And you forget the rarely needed ones over time.Another thing I’ve noticed with functional programming advocates is that they really want to avoid simple procedural code like
while
orswitch
even when a functional solution doesn’t really make sense.I’m not sure that either approach is better; if you can read the functional approach then you’re probably good to go. I myself prefer code that is more human readable; after all the processor can go so very fast that the impediment is the human understanding of the code, not the processor’s. Given that, both approaches generate an array that is really wasteful — consider what would happen if there were 1,000,000 choices rather than 100.
However, I do favor an approach which does as much calculation up-front as possible, so that the actual picking of the item happens as fast as possible.
I do favor a procedural approach when it makes sense, because it limits the amount of code and thus it limits the number of places where you can make a mistake.
But I think we’re getting away from the original problem, which is how to pick an ad from a limited set of choices, with weighting of those choices. Anything quick and dirty that accomplishes that can be wrapped in a function, and you’ll do just fine.
I don’t find most functional code any more human readable than any other code. I think the greatest value that functional paradigm can provide is the idea of having simple functions that do just one thing. That is far more valuable than functional paradigm in itself, because you can apply that to any coding you do.
Once you add in libraries like lodash that collect a multitude of simple functions into long single lines of code you’re back to the issue of hard-to-read code as those lines are a compact jungle of lots of logic. The reading order is also different from most “regular code” as often it has to be done from end to beginning (innermost function call to outermost), which fights against the typical top to bottom row-by-row.