Edit: I've rewritten the question in hopes that the goal is a little clearer.
This is an extended question to this question here, and I really like the function provided in this answer.
In the answer above, one is able to set the probability of hitting an extreme, with higher numbers producing a higher probability of getting lower numbers and vice-versa. The issue is that I must set the probabilities for 3 groups. Those groups are Lowest Value (LV), Highest Value (HV), and Middle Value (MV). However, to simplify the request, we can consider EVP=HVP=LVP
.
Given any range, the HV/LV should appear based on the specified EVP and, as you progress/degress through the range from each extreme, the probability of the next value in the range will increase, or decrease, based on the distance between EVP and MVP.
Using an example range of 1-6, with 1 and 6 being weighted at 5% (EVP), the probability spread would be 1/6 is 5%, 2/4 is 15%, and 3/4 is 30% (MVP), totalling 100%. The reverse should also be possible, swapping EVP and MVP should produce an inverse of the graph below.
Here's an image that I hope will convey the results expected from the given example.
Middle weighted:
Bonus: It would be most excellent if I was able to set HVP and LVP separately producing a result similar to the graph below (Note: The graph is not accurate to specification above).
Middle weighted (bonus):
Thanks!
A general technique when generating non-uniform random number is using rejection sampling. Even though it may be ineffective in this case you still should know how to do this, because it works for any density function you provide.
$density
here is a density function accepting a floating point number between zero and one as argument and returning a value smaller then$max
. For your example this density function could be:An example call then would be:
Rejection sampling is especially useful if you can't easily calculate the inverse of the distribution. As in this case it is pretty easy to calculate the inverse using inverse transform sampling is probably the better choice. But that is already covered in Jon's answer.
PS: The above implementation is general and thus uses a random value between 0 and 1. By building a function that only works for your approach everything get's easier:
Since I'm stuck at home today because of the flu :( I decided to try and figure this out for you. Essentially what you're asking for is some sort of interpolation. I used the easiest (linear) and these are my results and code. The code is kind of messy and I may fix it in the upcoming days..
The results are, I think, exactly what you're looking for. In the case of:
I got the following (final) array:
Namely,
1
should happen 12% of the time,5
22%,9
31%, and10
35% of the time. Lets graph it:It looks promising, but lets try something crazier...
In this case,
3
should occur 50% of the time, and steeply decrease into6
. Lets see what happens! This is the array (in retrospect, I should have sorted these arrays):And lets look at the picture:
It looks like it works :)
I hope I was able to solve your problem (or at least point you in the right direction). Note that my code currently has a number of stipulations. Namely, the initial nodes you provide MUST have probabilities that add up to 100% or you may get some wonky behavior.
Also, the code is kind of messy but the concepts are relatively simple. Some other cool stuff would be to try and instead of using linear interpolation, use some other kind, which would give you more interesting results!
Algorithm
To avoid confusion I'll just show exactly how the algorithm works. I give PHP a
$node
array that's in the form ofinteger => frequency in percentage
and ends up looking something likearray( 1 => 22, 3 => 50, 6 => 2, 7 => 16, 10 => 10)
, which istest 2
from above.Test 2
basically says that you want 5 control nodes placed at1, 3, 6, 7, and 10
with the frequencies of22%, 50%, 2%, 16%, and 10%
respectively. First, I need to see exactly where I need to do the interpolation. For example, I don't need to do it between6
and7
, but I do need to do it between1
and3
(we need to interpolate2
) and7
and10
(where we need to interpolate8
and9
).The interpolation between
1 -> 3
has(3 - 1) - 1 = 1
steps and should be inserted atkey[2]
in the original array. The value (%
) for the1 -> 3
interpolation isabs($a - $b) / $steps
which translates to the absolute value of the%
of1
minus the%
of2
, divided bysteps + 1
which, in our case, happens to equal14
. We need to see if the function is increasing or decreasing (hello Calculus). If the function is increasing we keep adding the step%
to the new interpolation array until we filled all of our empty spots (if the function is decreasing, we subtract the step% value
. Since we only need to fill one spot, we return2 => 36
(22 + 14 = 36
).We combine the arrays and the result is
(1 => 22, 2 => 36, 3 => 50, 6 => 2, 7 => 16, 10 => 10)
. The program interpolated2
, which was a percent value that we didn't explicitly declare.In the case of
7 -> 10
, there are 2 steps, the step percentage is2
which comes from(16-10) / (3 + 1) = 2
. The function is decreasing, so we need to subtract2
repeatedly. The final interpolated array is(8 => 14, 9 => 12)
. We combine all of the arrays and voila.The following image shows the green (initial values) and the red (interpolated values). You may have to "view image" to see the whole thing clearly. You'll notice that I use
±
since the algorithm needs to figure out if we're supposed to be increasing or decreasing over a certain period.This code should probably be written in a more OOP paradigm. I play a lot with array keys (for example, I need to pass
$k
so it's easier to combine arrays once I return them frominterpolate($a, $b, $steps, $k)
because they automatically have the right keys. This is just a PHP idiosyncrasy and in retrospect, I should have probably went with a more readable OOP approach to begin with.This is my last edit, I promise :) Since I love playing with Excel, this shows how the percentages normalize once the numbers are interpolated. This is important to see, especially considering that in your first picture, what you're showing is somewhat of a mathematical impossibility.
Test 1
Test 2
You'll notice that the percentages dampen significantly to accommodate the interpolation. Your second graph in reality would look more like this:
In this graph, I weighed
1 = > 1, 5 => 98, 10 => 1
and you see the extremes of the dampening effect. After all, percentages, by definition have to add up to 100! It's just important to realize that the dampening effect is directly proportional to the number of steps between extremes.Quick and dirty way in C#:
Test code:
Output (with iterations = 1000000):
Looks like:
Assuming you can cope with whole numbers for the percentages, just assign each value between 0 and 99 a result - e.g. 0-9 could have a result of 1 and 95-99 could have a result of 6 (to give your 10%=1 and 5%=6 scenario). Once you've got that translation function (however you achieve that - there are various approaches you could use) you just need to generate a random number in the range 0-99 and translate it to the result.
Your question isn't really clear in terms of the code you want (or even which language - C# or PHP?) but hopefully that will help.
Here's some C# code which will let you get any bias you like, within reason - you don't have to express it as percentages, but you can do:
So for example, you could use
which will give a 10% chance for each of 1-5, and a 50% chance of getting a 6.
I haven't tried it, but I think this might work:
And call it like this (using your second example):
First you need to characterize your current random number generator. In the case of PHP, the rand() function returns a nice flat profile - so there's no pre-processing required.
The remap the output distribution function so the area under it is unity and the range starts at 0. Then calculate its integral. Store the integral (e.g. as an array of values). Then when you need a random number matchnig the profile, first get a random number between 0 and 1 from the built-in generator, then find the Y coordinate on the integral where the X coordinate is the value you generated. Finally, scale the value to the desired range (e.g. if looking for a value between 0 and 10, multiply by 10, if looking for a value between -8 and +8, mutliply by 16 and subtract 8).
If your random number generator does not generate a flat profile, then the simplest approach would be to convert it to a flat profile using the reverse of the method above.