Random selection in Ruby is not always about heads or tails
Picking a random value in Ruby seems pretty easy.
['heads', 'tails'].sample # => either heads or tails
Now, imagine you have a system in which you’re experimenting a few different scenarios in order to figure out which configuration is the best for your website. For example it could be as simple as the following one: on the homepage visitors are prompted to create a new account thanks to a modal with two fields: the first one for the email is on the left whereas the second one for the password is on the right.
The experiment we are interested in is the following: for half the visitors, the modal will be displayed this same way I just described, whereas for the second half it will be flipped. Our goal is to know which modal is the best one so as to attract new users.
“The modal selection is easy to code, let’s use the sample function to pick a random value and we can check which value we got later”
Right. But now, for some reason I want 90% of the users to get the first modal and 10% of the users to get the flipped modal. Maybe because I am convinced that the initial modal is the best but still I would like to reserve a little traffic to give the new modal a try. The sample function doesn’t handle this case. Of course, we can however do something like:
choice = Random.rand(10)
And then apply the flipped modal only if choice is null. Now, what if there are 6 different modals [M1, …, M6] with the following probabilities:
1% M1, 3% M2, 17% M3, 11% M4, 23% M5, 45% M6.
We can find something for sure.
Here is the first naïve suggestion I had: with a short algorithm we create the following structure (by adding up the probabilities):
1 => M1
4 (1 + 3) => M2
21 (1 + 3 + 17) => M3
32 (1 + 3 + 17 + 11) => M4
55 (1 + 3 + 17 + 11 + 23) => M5
100 (1 + 3 + 17 + 11 + 23 + 45) => M6
Then we pick a value with Random.rand(100) which gives an integer between 0 and 99. And we check if our number is strictly lower than our first number in the structure. If so then we should display the modal M1. Else we apply the same logic with the second number in the structure (instead of the first) and so on. It will stop at the worst case at the end because 99 (max value which can be picked) is lower than 100 (which is always the last number in the structure as we add up probabilities). With this algorithm, the probabilities are correct: for example M1 is displayed if the random number picked was zero (1%), M3 is displayed if it was between 4 and 20 (17%), M5 is displayed if it was between 55 and 99 (45%).
There is actually a gem to do what we’re trying to do. Why would we code something that is already existent? Let’s use this: https://github.com/ryanlecompte/weighted_randomizer
This gem will allow you to solve weighted random selection in a clean way. All you have to do is build a WeightedRandomizer object by specifying the probabilities you want. Here’s some code I wrote to give you an example of what we could do with it:
As I like to test things to check it makes sense, let me add a quick dummy test function:
We’re all set! Here are some results I got:
I hope you could learn something new with this article!