34
\$\begingroup\$

I have an input of two values, let's say 1 and 10. Lesser numbers should overall occur more often in the output than higher numbers, while there is a peak in the probability at #2:

1: very common
2: extremely common       
3: very common     
4: common              
5: somewhat common  
...                  
10: extremely rare   

How do I achieve this? My code until now looks like this:

function getValue($min, $max) {
    return random($min, $max);
}
\$\endgroup\$
10
  • 3
    \$\begingroup\$ Really interesting question. Check out this post: stackoverflow.com/questions/4420502/… \$\endgroup\$
    – jgallant
    Commented Feb 18, 2016 at 12:47
  • 2
    \$\begingroup\$ ...and this post. \$\endgroup\$
    – Vaillancourt
    Commented Feb 18, 2016 at 12:53
  • 2
    \$\begingroup\$ Do you just want to get whole numbers? Or any number in the range e.g. 0.5, 7.9834... \$\endgroup\$
    – Malrig
    Commented Feb 18, 2016 at 13:22
  • 6
    \$\begingroup\$ Define "very common" vs. "extremely common". If you generate a million numbers how many of each do you want in the result (roughly). \$\endgroup\$ Commented Feb 18, 2016 at 13:51
  • 1
    \$\begingroup\$ I give a gentle introduction to the mathematics of generating an arbitrary probability distribution when given a uniform probability distribution here: ericlippert.com/2012/02/21/generating-random-non-uniform-data -- the code is in C# but you can adapt the mathematics to the language of your choice. \$\endgroup\$ Commented Feb 18, 2016 at 17:40

12 Answers 12

56
\$\begingroup\$

Assuming you have a random() function that returns a uniformly-distributed numeric value in the interval [0, 1)...

(I see the edit attempt to "fix" the mismatched bracket above, but this is deliberate and carries specific meaning)

random() - random()

Gives a distribution that peaks at 0 and falls off toward -1 and 1.

abs(random() - random())

Peaks around 0 and falls off toward 1

 floor(abs(random() - random()) * (1 + max - min) + min)

Gives you a random number between min and max, with outputs closer to min being more common, falling off linearly toward the max.


Note as pointed out by Logan Pickup in the comments below, there's a slight mathematical artifact here if you're using this to generate continuous random numbers (ie. without the floor): the only way to get zero out of abs is to start with zero, but you can get a value of epsilon if the input is either positive or negative epsilon. So you have about half the chance of getting exactly zero as you have of getting the next larger representable number. If you're using this with floor after the abs to generate random integers, then this little quirk will be swamped and not noticeably affect the result.


Using inverse transform sampling you can get this same linear distribution (without the 0 artifact) with one random sample via the formula:

1 - sqrt(1 - random())

(and we can apply the same scale/offset/floor approach to get a corresponding discrete distribution)

You can find methods to get custom-shaped probability distributions in this answer or the links suggested in comments above.

\$\endgroup\$
9
  • 6
    \$\begingroup\$ Ooh! That's clever! \$\endgroup\$
    – Vaillancourt
    Commented Feb 18, 2016 at 13:45
  • 2
    \$\begingroup\$ @user1306322 because random() is uniformly distributed on [0, 1), so random() * 2 - 1 is uniformly distributed on [-1, 1) - it doesn't have the non-uniformity OP has asked for. By taking two independent random rolls a and b, we introduce some non-uniformity: there are more ways to get 0 (all cases a == b) than there are to get 1 (only a == 1, b == 0 fits the bill - this is a simplification since 1 is actually outside the range of this function, but the extreme is easier to talk about) \$\endgroup\$
    – DMGregory
    Commented Feb 18, 2016 at 15:47
  • \$\begingroup\$ To expand on this answer: this trick works with random() returning float/double but it doesn't work if you use random integers, because the probability of returning min will be about half of what it should be. For continuous or high-resolution outputs, the probability of returning min is approximately 0 anyway so it doesn't matter. \$\endgroup\$ Commented Feb 18, 2016 at 19:10
  • \$\begingroup\$ @DietrichEpp I've updated the rounding function to floor, which should keep the distribution linear, including for the min & max buckets. \$\endgroup\$
    – DMGregory
    Commented Feb 18, 2016 at 19:38
  • 2
    \$\begingroup\$ Not that I'm complaining about the upvotes, but don't forget to spread the love a bit. ;) This was just a quick off-the-cuff answer - the power-based approaches in some of the other answers offer more flexibility in the falloff of the probability distribution, with similar simplicity. \$\endgroup\$
    – DMGregory
    Commented Feb 19, 2016 at 15:51
37
\$\begingroup\$

Let's say "rand()" gives you a random number between 0 and 1 (inclusive).

pow(rand(), 2)

will give you an answer between 0 and 1 (inclusive), but the result is more likely to be close to zero, following a quadratic curve.

pow(rand(), 0.5)

will give you an answer between 0 and 1 (inclusive), but the result is more likely to be close to one, following a square root curve.

And since the min and max of the output are still 0 and 1, this distribution can be mapped to any range (given min, max, and the exponent p) like so:

result = min + (max - min) * pow(rand(), p)

Using different values of p can bias you towards the minimum or maximum value depending on your needs: graph of x raised to different powers (example of mapping a 0-1 range by raising x to different powers. From top to bottom: 1/4, 1/2, 2, 4)

Experiment with different powers to find what works for you. Just remember that bigger powers greater than one will bias you towards the minimum value, and smaller powers less than one but greater than zero will bias you towards the maximum value.

You can also swap max and min to flip the distribution of results, and if you want integer results, I'd use floor(x), increase max by one, and either use a rand() function that is exclusive at the top end (won't return 1.0) or clamp the final output to the range to filter out that rare case.

\$\endgroup\$
1
  • 4
    \$\begingroup\$ This is the answer I was going to offer, only better illustrated +1! \$\endgroup\$
    – Wren
    Commented Feb 19, 2016 at 16:51
10
\$\begingroup\$

Why not use a system similar to advantage/disadvantage as used in DnD 5e?

It boils down to: disadvantage: roll 2 (or any number of) dice and keep the lowest. advantage: roll 2 (or any number of) dice and keep the highest.

for 1 out of 2 dice this gives a linear chance decreasing as you get higher:

example: http://anydice.com/program/1227

for one out of more dice this gives a curve:

example: http://anydice.com/program/585d

And picking more dice out of the set favors numbers in the lower-middle of your range.

I hope this helps

\$\endgroup\$
1
  • \$\begingroup\$ Was going to post this, but you beat me. :) Also, you might check out this rpg.SE post that uses a very similar method. \$\endgroup\$
    – MichaelS
    Commented Feb 19, 2016 at 0:40
6
\$\begingroup\$

You can use a local variable to save a random call:

function F(min, max) {
  var t = random(); //Assuming this returns a value between 0 and 1
  return min + (max - min) * t * t;
}

This makes a non linear distribution where values close to min are more frequent. You can force even more extremal distribution by inserting t = t*t or user a pow with other values.

\$\endgroup\$
4
  • \$\begingroup\$ "to save a random call"? Save from what? If the algorithm needs two calls, it needs to calls, and if it needs one call, it needs one call. I'm not sure why you'd "save" a call to random. \$\endgroup\$
    – Vaillancourt
    Commented Feb 18, 2016 at 14:49
  • 1
    \$\begingroup\$ @Vaillancourt To save one call to random() as compared to the formula in my answer which takes the difference of two calls. \$\endgroup\$
    – DMGregory
    Commented Feb 18, 2016 at 14:56
  • 3
    \$\begingroup\$ Or just do return min + (max - min) * pow(random(), 2). Still +1 for smart use of Uniform Product Distribution. \$\endgroup\$
    – Dorus
    Commented Feb 18, 2016 at 15:16
  • \$\begingroup\$ @Vaillancourt Some PRNGs are computationally intensive. But then pow() may be as well, depending on floating-point performance of a particular CPU and optimizing power of the compiler. Benchmark. \$\endgroup\$ Commented Feb 19, 2016 at 23:07
5
\$\begingroup\$

A naive and not efficient algorithm that would give you a linear distribution:

vector values;
int iterations = 1;
for ( val = max; val >= min; --val ) {
  for ( i = 1; i <= iterations ; ++i )
    values.push_back( val );

  ++iterations;
}

// The goal is to have in your values vector something like this for min = 1 and max = 4:
// [1, 1, 1, 1, 2, 2, 2, 3, 3, 4]

return values[random(0, values.size() - 1)];

This will give you what you need.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ nice idea, but really inefficient for bigger number ranges. :( \$\endgroup\$
    – Standard
    Commented Feb 18, 2016 at 12:42
  • \$\begingroup\$ @wernersbacher you're absolutely right ;) That's why I said it in the first sentence! I'm sure someone else will come up with a better solution. \$\endgroup\$
    – Vaillancourt
    Commented Feb 18, 2016 at 12:43
  • \$\begingroup\$ We can bring it to O(n) I think. First loop, i=0; for(n=min to max){i+=n; values[n]=i} (If I did it right, you have an array with {1, (1+2), (1+2+3), etc.}.) Switch to for(n=max to min) so it's what the OP wants {max, (max+[max-1]), etc.}. Next, roll r=random(1, i) (inclusive, and i should equal the last value in the values array). Then, for(n=1 to values.length){if(i<=values[n]){return n}}. This way rolling 1 to a million just does 2 million iterations, instead of a million million. \$\endgroup\$
    – MichaelS
    Commented Feb 19, 2016 at 0:55
  • 1
    \$\begingroup\$ The generation of the list may be inefficient, but if its done once and stored as a static, its not so bad. The line return values[random(0, values.size() - 1)]; isn't inefficient. \$\endgroup\$
    – Octopus
    Commented Feb 19, 2016 at 17:38
5
\$\begingroup\$

If this is for a game, often you want to tweak values intuitively rather than rely on some existing distribution.

A good way to do this is to pick numbers from an array, and duplicate the values you want more than one. For example, here's some python code that does that. The nice thing about it is that you can see pretty easily the probability of the different options.

import random
values = [
1, 1, 1, 1, 1, 1,
2, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 3,
4, 4, 4, 4,
5, 5, 5, 5,
6, 6, 6,
7, 7,
8,
9,
10
]
random.choice(values)

Obviously this only makes sense with a small number of values.

Alternatively, if you have a probability distribution in mind you can use Inverse Transform Sampling. It can take the uniform random number from 0 to 1 and map it so that it matches any probability distribution. It's requires some maths, but if you have some good libraries they should be able to help you out.

\$\endgroup\$
3
  • \$\begingroup\$ This is effectively Vaillancourt's answer, but without the method to generate the desired table without specifying every entry explicitly. \$\endgroup\$
    – DMGregory
    Commented Feb 19, 2016 at 0:16
  • 4
    \$\begingroup\$ @DMGregory: Two differences. First, this one is more time efficient (the data already exists at runtime, and doesn't need to be created). Second, you can tweak the curve precisely as you see fit, instead of trying to come up with some formula or logic that approximates the curve you're going for. \$\endgroup\$
    – MichaelS
    Commented Feb 19, 2016 at 0:46
  • \$\begingroup\$ The curve here only matches what you want only if the probabilities of each numbers are a small integer ratio with each other. There are many cases where you may want a non-integer distribution probabilities. In this particular implementation, for example, 8,9,10 have the exact same probability of occurring. \$\endgroup\$
    – Lie Ryan
    Commented Feb 20, 2016 at 11:39
2
\$\begingroup\$

If you have uniformly distributed random numbers, you can use those as the parameter to a mathematical function that has a exponential-like shape, so that most numbers in the random range yield a low value. The initial random input can be normalized, then scaled to your range, so the function is well-behaved.

For example, the following function, if given normalized values from 0 to 1 as input:

f(x)= 10 * 2^((x-1.4)*10)+0.1

You can tweak the parameters to make this cover the whole output range from 0 to 1. And if you need higher numbers, just multiply or offset the result.

I didn't check this mathematically, but it seems good enough experimentally.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ I like this answer because it made me understand what the functions of the other answers actually do mathematically. \$\endgroup\$ Commented Feb 19, 2016 at 22:36
2
\$\begingroup\$

I recommend using the function:

f(s,g) = round(s+R^n(g-s))

where s and g represent your minimum and maximum values, respectively, R is a random number between 0 and 1 (inclusive) and n is a pre-determined constant. The notation ||x|| represents the nearest integer to x.

An example in PHP 5:

function biasedRandom($min, $max) {
  $power = 2;
  # lcg_value() returns a random number between 0 and 1
  $bias  = pow(lcg_value(), $power);
  return round($min + ($max-$min) * $bias);
}

In an IPython notebook I ran my function 1000 times each for different powers. It generated this graph, where each value (x-axis) is plotted against the probability of it occurring (y-axis):

p=2 p=4 p=8 Higher powers have steeper graphs

The graph shows that as p, the power which we raise the random number to, increases, the 'steepness' of the graph also increases.

That is to say, a higher value of p will make the smallest value, in this case 1, to have a much higher probability of occurring than the highest value, 10. See the green line, where p equals 8.

In the case of the blue line, where p is only equal to 2, the bias is far less extreme, and all numbers are closer in probability.

This animation demonstrates how the probabilities of each value change with different values of p:

https://www.desmos.com/calculator/9pi4bwnc7y

I used this python function to generate each of the biased random numbers used as data in all of the following graphs.

def biasedRandom(smallest, largest, p): return int(round(smallest + (largest - smallest) * pow(random,p)))

These box-plots each show the distribution of 500 values generated by my function, with powers of (from top to bottom) 2, then 2.5 then 4.

box-plots

The box-plots clearly show that smaller numbers are more common than larger numbers (the medians are all less than 5.5), and that there is a higher density of values towards the lower end of the spectrum. However, one can also see how high values (all the way up to ten) still emerge sometimes.

If we compare the box-plots we can again see that a lower power (the top-most box-plot has the lowest power, in this case 2) leads to a more even spread of values, with high values appearing more frequently and low values appearing less frequently than when using higher powers.

Without rounding the function's final result, we can obtain the following visualisation, which shows clearly the higher density of smaller numbers. Values are plotted as they are obtained by the function.

https://jsfiddle.net/theonlygusti/zexhrL2p/1/
The above image shows the distribution of results for powers 8, 16 and 32. The layout is intended to represent a number line, with 1 on the far left and 10 on the right.

\$\endgroup\$
2
  • \$\begingroup\$ This function seems to be exactly what I want for my own use-case, so thanks for that! I noticed that p=0 always produces the maximum value for the range (expected, since x^0=1), and higher values for p bias towards lower values for the result. However, p=1 gets odd. While overall it's close to an even distribution of values across the range, the minimum and maximum values are each approximately half as likely as the rest of the values in the range, and I'm not sure why. Screenshot of my Mathematica notebook demonstrating the phenomenon: i.imgur.com/wByvP0s.png \$\endgroup\$
    – Brian S
    Commented Jun 21, 2019 at 23:58
  • 1
    \$\begingroup\$ @BrianS I know why and I know how to fix it: you will find that the probability of getting a min (or equivalently, a max) value outputted is exactly half of getting any other value. That is because the function I proposed just rounds a distribution of real (decimal) numbers, and that means anything between e.g. (in your example) 0.5 to 1.5 will be outputted as 1, but to get 0 outputted you half as many possible values: only 0 to 0.5. To fix this you can use Round[min - 0.5 + (max-min + 1) * bias]. \$\endgroup\$
    – minseong
    Commented Jun 22, 2019 at 0:42
1
\$\begingroup\$

Couple other ideas:

Folded Normal Distribution.

enter image description here

This is achieved by generating a standard Gaussian random, then running it through abs(). Values greater than 1 are possible, feel free to deal with it however you choose; clamp it, re-randomize, ignore.

Wald Distribution.

Also known as the Inverse Gaussian.

enter image description here

0 as a result has a very low probability, while values near 0 have a very high probability, trailing off towards infinity (see above about clamping).

\$\endgroup\$
4
  • \$\begingroup\$ Really cool concepts \$\endgroup\$
    – minseong
    Commented Feb 20, 2016 at 15:58
  • \$\begingroup\$ @theonlygusti Yup, I actually implemented the Wald distribution in the Custom Ore Gen mod for Minecraft so I could use it in my ore distributions. Long, generally straight veins with rare, but hard, corners. \$\endgroup\$ Commented Feb 20, 2016 at 17:35
  • \$\begingroup\$ That's pretty cool, but surely the fact that y=0 when x=0 would have complicated its implementation into your mod? Doesn't the slope down towards the origin mess anything up? \$\endgroup\$
    – minseong
    Commented Feb 20, 2016 at 22:12
  • \$\begingroup\$ @theonlygusti The random is a relative modifier, not an absolute one. Also, remember that Y here is a probability value, not a return value. X is the return value from the rand_wald() function. \$\endgroup\$ Commented Feb 21, 2016 at 0:47
0
\$\begingroup\$

The important thing is to re-map the function from 0-1 to what you desire. A big part of this depends on just what distribution you want from it. I suspect you want something that has an exponential decaying type curve in it, although you have to really ask yourself what you want the distribution to look like. A couple of possibilities are an exponential decay function, and a inverse function, although in both cases the edge cases need to be well studied. I'm not going to give you the code, but rather the formulas. Assume R is a random number between 0-1.

Inverse Linear:

while (R<.1) {
    R*=10;
}
return ceil(10-(1/(R))

Exponential Decay:

// Will likely need to work on some edge cases
return 10-10*(e-e^R)

To test it, I suggest that instead of using real random numbers, iterate through, say, every 0.01 value of R, and see if the distribution is correct. If there are points that lie outside your range, find a way to remove them. Good luck!

\$\endgroup\$
0
\$\begingroup\$

One can use a simple formula like so:

function getValue(min,max) {
    return min-1+Math.pow((max-min+1),Math.random());
}

When given min as 1 and max as 10 this generates a number from 1.0 to 10.0 where 10.0 is rare. Remember Math.random() generates a uniform random number from 0.0 to 1.0

Here is some R code to show you the results:

(function(min,max){ 
    x=min-1+(max-min+1)^runif(10000); 
    print(summary(x)); 
    hist(x)
 })(4,28)

histogram

\$\endgroup\$
2
  • \$\begingroup\$ Interesting concept, but doesnt this achieve the opposite effect? 1111 1111(max) XOR 0000 0001(rand) would give 1111 1110 right? the most significant bit has a 50% chance of staying up thus there would be a tendency of producing high numbers, also max doesn't limit the outcome of rand. I like the concept though. Would & achieve the desired effect? \$\endgroup\$
    – Niels
    Commented Feb 19, 2016 at 11:58
  • \$\begingroup\$ I meant hat to be the power operator. I've been doing a lot of R programming recently. I changed it to POW(x,y) \$\endgroup\$
    – Chris
    Commented Feb 19, 2016 at 14:18
-1
\$\begingroup\$

You could call the random function twice:

 function getValue($min, $max) {
     return random($min,random($min, $max));
 }

For numbers 1 - 5, the probabilities for getValue(1,5) are
1 = 137 out of 300
2 = 72 out of 300
3 = 47 out of 300
4 = 27 out of 300
5 = 12 out of 300

\$\endgroup\$
1
  • \$\begingroup\$ This is effectively a subset of user2645227's answer. \$\endgroup\$
    – DMGregory
    Commented Feb 19, 2016 at 0:18

Not the answer you're looking for? Browse other questions tagged .