Assume I want to choose a number from 1-10 at random, but there are weights to each number.
1 - 15% chance
2 - 15% chance
3 - 12% chance
4 - 12% chance
5 - 10% chance
6 - 10% chance
7 - 8% chance
8 - 8% chance
9 - 5% chance
10 - 5% chance
How would I go about coding this up in PHP?
I assume your percentages add up to 100%?
Build an array with
15 times a '1' value,
15 times a '2' value,
...
10 times a '6' value,
8 times a '7' value,
...
5 times 1 '10' value
You'll end up with a single array which contains 100 elements.
Pick an element randomly (and pop it from the array).
an example echoing value with OPs weight with the below class:
echo 1+Rand::get_weighted_rand(array(15,15,12,12,10,10,8,8,5,5));
and the class:
class Rand
{
/*
* generates a random value based on weight
* #RETURN MIXED: returns the key of an array element
* #PARAM $a ARRAY:
* the array key is the value returned and the array value is the weight
* if the values sum up to less than 100 than the last element of the array
* is the default value when the number is out of the range of other values
* #PARAM $p INT: number of digits after decimal
*
* i.e array(1=>20, 'foo'=>80): has an 80 chance of returning Foo
* i.e array('bar'=>0.5, 2=>1, 'default'=>0), 1: 98.5% chance of returning default
*/
public static function get_weighted_rand($a, $p=0)
{
if(array_sum($a)>100)
return FALSE;#total must be less than 100
$p=pow(10, $p+2);
$n=mt_rand(1,$p)*(100/$p);
$range=100;
foreach($a as $k=>$v)
{
$range-=$v;
if($n>$range)
return $k;
}
#returning default value
end($a);
return key($a);
}
}
If your weights are in percentages, pick a random number between 0 and 100, then iteratively subtract the percentages until you cross zero:
<?php
function getWeightedRandom() {
$weights = array(15, 15, 12, ...); // these should add up to 100
$r = rand(0, 99);
for ($i=0; $i<count($weights); $i++) {
$r -= $weights[$i];
if ($r < 0)
return $i+1;
}
}
?>
This has the added benefit of supporting non-integer weights.
Put them all multiple times in array, e.g. the 1 15 times, the 3 12 times and so on.
Then pick a random number from that array.
$array = array_merge (array_fill (0, 15, 1), array_fill (0, 15, 2), array_fill (0, 12, 3), array_fill (0, 12, 4), array_fill (0, 10, 5), array_fill (0, 10, 6), array_fill (0, 8, 7), array_fill (0, 8, 8), array_fill (0, 5, 9), array_fill (0, 5, 10));
$random_number = array_rand ($array);
Related
I am trying to create a random number generator which has the following features:
Each one is unique
Numbers are either 1 or 49 and all in between
Ordered from lowest to highest
This is what I have so far
$numbers = rand(1, 49)." ".rand(1, 49)." ".rand(1, 49)." ".rand(1, 49)." ".rand(1, 49)." ".rand(1, 49);
echo "Your Lucky Lotto Numbers Are: ".$numbers;
Im just not quite sure how to go about ordering them, plus the numbers being unique.
Just create an array with all the numbers from 1 to 49, and start randomly removing elements. Leave only the number of elements you need. That way, they are already in order, and are definitely unique.
Example:
$values = range(1,49);
while(count($values)>6) {
unset($values[array_rand($values)]);
}
print "Your results: ".implode(', ',$values);
$numbers = range(1, 49);
shuffle($numbers);
$numbers = array_slice($numbers, 0, 5);
sort($numbers);
foreach ($i=0; $i<6; $i++) {
echo $numbers[$i]." ";
}
Range & Shuffle.
You should use an array for this:
$numbers = range(1, 49); //generate the array
shuffle($numbers); //shuffle the array
$numbers = array_slice($numbers, 0, 6); // cut the array in the appopriate length
echo "Your Lucky Lotto Numbers Are: ";
print_r(asort($numbers)); //sort and print
Some useful literature:
Arrays in php: http://php.net/manual/en/language.types.array.php
Loop through array elements using foreach: http://php.net/manual/en/control-structures.foreach.php
Sorting in PHP: http://php.net/manual/en/array.sorting.php
Take an array with your numbers, take 6 random numbers (or shuffle array, pick first 6), sort your picked numbers. Not highly efficient, but should definitely work:
$numbers = range(1, 49);
shuffle($numbers);
$picks = array_slice($numbers, 0, 6);
sort($picks);
There are at least 3 ways how to do this:
1) generate non-existing numbers
You create array and add random numbers to it. If newly generated number already is in that array, generate another one (and so on until it is unique, check it in some while cycle), then sort it. i would recommend this
2) increment for all existing lower numbers
With each new random number, you lower the maximum by 1, then when adding this number, increment it for each lower number in you array
for example, when you have range 1-49 (inclusive) and numbers [4, 8, 6, 15], you generate number from 1-45 (inclusive), cause there are only 45 free numbers left. Lets say you generated 8, that means 8ht free number, and since 4, 8 and 6 are already there increment that three times and you got 11, however this requires your array to be sorted all the time.
3) same as #hexblot said
I would choose 1 cause its simple and you dont get much same selections if the amount of number is "lot" smaller then the number range (i.e. 5 numbers from 1-49 - 10% isnt much)
a little extensive maybe, but this will give you 6 unique sorted numbers
$arr = array();
while ( count($arr) < 6 ) {
$x = mt_rand(1,49);
if ( !in_array($x,$arr) ) {
$arr[] = $x;
asort($arr);
}
}
foreach($arr as $x){
echo $x." ";
}
I have produced an array of prime factors of a number - this is supposed to be the hard part! However, in order to create a list of the divisors of the same number the prime factors need to be combined in every which possible way. Something that I'm struggling to do with php.
For example I have an array of:
2
2
2
3
3
41
53
...for the number 156456; multiply them all together and you get back to the number. What I need to do is to multiply all of the duos together e.g. 2x2, 2x3, 2x53 etc and then all of the triplets together and so on until I finally multiply the 7 blocks of six together.
As you can see this will give a very large array with all of the divisors in, 4, 6, 8, 9, 12 etc. with many duplicates. I just can't seem to get from the array I have above to this array of divisors that I want. It's a case of multiplying out every possible combination of elements in the array, is there a php function for this, my search so far has been fruitless?
After reading this page: http://mathcentral.uregina.ca/QQ/database/QQ.02.06/joe1.html, I tried to build something that might work, It may not be the most efficient solution and its also limited to count($primes) <= 32 on 32 bit systems. If you need more, feel free to use a Bitset:
$primes = Array(2, 2, 2, 3, 3, 41, 53);
$num_primes = count($primes); // 7, if this is over 32, it won't work on 32bit systems
$divisors = Array();
// number of possible combinations
$limit = pow(2, $num_primes) - 1; // 127
// count a number up and use the binary
// representation to say which index is
// part of the current divisor
for($number = 0; $number <= $limit; $number++) {
$divisor = 1;
// only multiply activated bits in $number to the divisor
for($i = 0; $i < $num_primes; $i++) {
$divisor *= ($number >> $i) & 1 ? $primes[$i] : 1;
}
$divisors[] = $divisor;
}
echo implode(", ", array_unique($divisors));
This results into the following divisors:
1, 2, 4, 8, 3, 6, 12, 24, 9, 18, 36, 72, 41, 82, 164, 328, 123, 246, 492,
984, 369, 738, 1476, 2952, 53, 106, 212, 424, 159, 318, 636, 1272, 477,
954, 1908, 3816, 2173, 4346, 8692, 17384, 6519, 13038, 26076, 52152, 19557,
39114, 78228, 156456
To find all divisors you need to multiply each prime factor with each other in every possible combination. To do this I calculate the number of possible combinations ($limit). If you now count a number up to this limit the binary representation looks something like this:
7 bit
<----->
0000000 0
0000001 1
0000010 2
0000011 3
0000100 4
0000101 5
0000110 6
0000111 7
0001000 8
0001001 9
...
1111110 126
1111111 127
The current binary representation of $number represents which indexes of $primes are used to calculate the current $divisor. To show this better let's say $number = 5, which is 0000101 in binary. And the calculation for $divisor would be 2 * 1 * 2 * 1 * 1 * 1 * 1 = 4. Only the first and the third bit is set, so only the first and the third element in the array is used for the calculation.
I hope this makes it a little bit clearer.
I need to total the number of clicks over 10 links on my page and then figure out the percentage of people that clicked each. This is easy division, but how do I make sure that I get a round 100% at the end.
I want to use the below code, but am worried that a situation could arise where the percentages do not tally to 100% as this function simply removes the numbers after the period.
function percent($num_amount, $num_total) {
$count1 = $num_amount / $num_total;
$count2 = $count1 * 100;
$count = number_format($count2, 0);
echo $count;
}
Any advice would be much appreciated.
Instead of calculating one percentage in your function you could pass all your results as an array and process it as a whole. After calculating all the percentages and rounding them make a check to see if they total 100. If not, then adjust the largest value to force them all to total 100. Adjusting the largest value will make sure your results are skewed as little as possible.
The array in my example would total 100.02 before making the adjustment.
function percent(array $numbers)
{
$result = array();
$total = array_sum($numbers);
foreach($numbers as $key => $number){
$result[$key] = round(($number/$total) * 100, 2);
}
$sum = array_sum($result);//This is 100.02 with my example array.
if(100 !== $sum){
$maxKeys = array_keys($result, max($result));
$result[$maxKeys[0]] = 100 - ($sum - max($result));
}
return $result;
}
$numbers = array(10.2, 22.36, 50.10, 27.9, 95.67, 3.71, 9.733, 4.6, 33.33, 33.33);
$percentages = percent($numbers);
var_dump($percentages);
var_dump(array_sum($percentages));
Output:-
array (size=10)
0 => float 3.51
1 => float 7.69
2 => float 17.22
3 => float 9.59
4 => float 32.86
5 => float 1.28
6 => float 3.35
7 => float 1.58
8 => float 11.46
9 => float 11.46
float 100
This will also work with an associative array as the function parameter. The keys will be preserved.
These figures could now be presented in a table, graph or chart and will always give you a total of 100%;
What you want to do is this.
Total the number of clicks across the board, then divide each number by the total.
For example:
1134
5391
2374
2887
In this case, four buttons, with a total of 11786 clicks, so:
1134 / 11786 = 0.09621....
5391 / 11786 = 0.45740....
2374 / 11786 = 0.20142....
2887 / 11786 = 0.24495....
Then for each division, round the result to 'two decimal points', so the first result:
0.09621.... becomes 0.10
because the 3rd point is 5 or above, it would remain at 0.09 if the 3rd point was below 5.
Once you have all of the results rounded, multiply each by 100 then add them up.
The ending result will always be 100.
Should warn you however that depending on how you use each individual percentage, when you round them, any result less that 0.05 will become 0%, unless you keep the value before you round it so you can declare it as a percentage less than 1.
I think you want to use ceil() or round() .
Since these are floating point numbers, there is room for error. Be careful how you round, and be sure that you don't independently calculate the last remaining percentages. Simply subtract the total of what you have from 1 or 100.
Make sure you dont calculate separate sides of the equation, sum one side, then subtract the other from 1 or 100 or however you are handling your percentages.
I run into this quite a bit and have a hack for it.
$percentages = array(
'1' => 87.5,
'2' => 12.5,
'3' => 0,
'4' => 0,
'5' => 0
);
If you round those percentages for output, you will end up with 88% and 13% (101%)
round($percentages['1']);
round($percentages['2']);
// 88
// 13
So here is the code I use to fix it.
$checkTotal = array_sum($percentages);
$max = max(array_keys($percentages));
if ($checkTotal > 100) {
$percentages[$max] = $percentages[$max] - 1;
}
if ($checkTotal < 100) {
$percentages[$max] = $percentages[$max] + 1;
}
If it is 100, do nothing.
If it is less than 100, add 1 to equal 100
If it is over 100, subtract 1 to equal 100
I have an array like
$keywords = array('apple'=>10,'orange'=>2,'grape'=>12);
I want to randomly pick one of the "Key" from the array. However the probability distribution should be such that probability of picking an element should be proportional to it's value.
Add all values (10+2+12 is 24); get a random number in the range [0, 24), and pick the corresponding element depending on whether the number lies in [0, 10), [10, 12), or [12, 24).
I'd do it like this:
$probabilities = array('apple'=>50, 'orange'=>20, 'banana'=>10);
function random_probability($probabilities) {
$rand = rand(0, array_sum($probabilities));
do {
$sum = array_sum($probabilities);
if($rand <= $sum && $rand >= $sum - end($probabilities)) {
return key($probabilities);
}
} while(array_pop($probabilities));
}
An O(log(n)) approach (this is ripped directly from an answer to a very similar question):
The usual technique is to transform the array into an array of cumulative sums:
[10 60 5 25] --> [10 70 75 100]
Pick a random number in the range from zero up to the cumulative total (in the example: 0 <= x < 100). Then, use bisection on the cumulative array to locate the index into the original array:
Random variable x Index in the Cumulative Array Value in Original Array
----------------- ----------------------------- ----------------------
0 <= x < 10 0 10
10 <= x < 70 1 60
70 <= x < 75 2 5
75 <= x < 100 3 25
For example, if the random variable x is 4, bisecting the cumulative array gives a position index of 0 which corresponds to 10 in the original array.
And, if the random variable x is 72, bisecting the cumulative array gives a position index of 2 which corresponds to 5 in the original array.
I know how to generate a random number in PHP but lets say I want a random number between 1-10 but I want more 3,4,5's then 8,9,10's. How is this possible? I would post what I have tried but honestly, I don't even know where to start.
Based on #Allain's answer/link, I worked up this quick function in PHP. You will have to modify it if you want to use non-integer weighting.
/**
* getRandomWeightedElement()
* Utility function for getting random values with weighting.
* Pass in an associative array, such as array('A'=>5, 'B'=>45, 'C'=>50)
* An array like this means that "A" has a 5% chance of being selected, "B" 45%, and "C" 50%.
* The return value is the array key, A, B, or C in this case. Note that the values assigned
* do not have to be percentages. The values are simply relative to each other. If one value
* weight was 2, and the other weight of 1, the value with the weight of 2 has about a 66%
* chance of being selected. Also note that weights should be integers.
*
* #param array $weightedValues
*/
function getRandomWeightedElement(array $weightedValues) {
$rand = mt_rand(1, (int) array_sum($weightedValues));
foreach ($weightedValues as $key => $value) {
$rand -= $value;
if ($rand <= 0) {
return $key;
}
}
}
For an efficient random number skewed consistently towards one end of the scale:
Choose a continuous random number between 0..1
Raise to a power γ, to bias it. 1 is unweighted, lower gives more of the higher numbers and vice versa
Scale to desired range and round to integer
eg. in PHP (untested):
function weightedrand($min, $max, $gamma) {
$offset= $max-$min+1;
return floor($min+pow(lcg_value(), $gamma)*$offset);
}
echo(weightedrand(1, 10, 1.5));
There's a pretty good tutorial for you.
Basically:
Sum the weights of all the numbers.
Pick a random number less than that
subtract the weights in order until the result is negative and return that number if it is.
This tutorial walks you through it, in PHP, with multiple cut and paste solutions. Note that this routine is slightly modified from what you'll find on that page, as a result of the comment below.
A function taken from the post:
/**
* weighted_random_simple()
* Pick a random item based on weights.
*
* #param array $values Array of elements to choose from
* #param array $weights An array of weights. Weight must be a positive number.
* #return mixed Selected element.
*/
function weighted_random_simple($values, $weights){
$count = count($values);
$i = 0;
$n = 0;
$num = mt_rand(1, array_sum($weights));
while($i < $count){
$n += $weights[$i];
if($n >= $num){
break;
}
$i++;
}
return $values[$i];
}
/**
* #param array $weightedValues
* #return string
*/
function getRandomWeightedElement(array $weightedValues)
{
$array = array();
foreach ($weightedValues as $key => $weight) {
$array = array_merge(array_fill(0, $weight, $key), $array);
}
return $array[array_rand($array)];
}
getRandomWeightedElement(array('A'=>10, 'B'=>90));
This is very easy method. How get random weighted element. I fill array variable $key. I get $key to array $weight x. After that, use array_rand to array. And I have random value ;).
Plain and fair.
Just copy/paste and test it.
/**
* Return weighted probability
* #param (array) prob=>item
* #return key
*/
function weightedRand($stream) {
$pos = mt_rand(1,array_sum(array_keys($stream)));
$em = 0;
foreach ($stream as $k => $v) {
$em += $k;
if ($em >= $pos)
return $v;
}
}
$item['30'] = 'I have more chances than everybody :]';
$item['10'] = 'I have good chances';
$item['1'] = 'I\'m difficult to appear...';
for ($i = 1; $i <= 10; $i++) {
echo weightedRand($item).'<br />';
}
Edit: Added missing bracket at the end.
You can use weightedChoice from Non-standard PHP library. It accepts a list of pairs (item, weight) to have the possibility to work with items that can't be array keys. You can use pairs function to convert array(item => weight) to the needed format.
use function \nspl\a\pairs;
use function \nspl\rnd\weightedChoice;
$weights = pairs(array(
1 => 10,
2 => 15,
3 => 15,
4 => 15,
5 => 15,
6 => 10,
7 => 5,
8 => 5,
9 => 5,
10 => 5
));
$number = weightedChoice($weights);
In this example, 2-5 will appear 3 times more often than 7-10.
i used Brad's answar and changed it a little to fit my situation and add more flexibility
i have an array with array value
$products = [
['id'=>1,'name'=> 'product1' , 'chance'=>2] ,
['id'=>2,'name'=> 'product2' , 'chance'=>7]
]
first i shuffle the products array
shuffle($products );
then you can pass it to the function
function getRandomWeightedElement(array $products) {
$chancesSum = 0;
foreach ($products as $product){
$chancesSum += (int) $product['chance'];
}
$rand = mt_rand(1, $chancesSum);
$range = 0;
foreach ($products as $product) {
$range += (int) $product['chance'];
$compare = $rand - $range;
if ($compare <= 0){
return (int) $product['id'];
}
}}
Since I used IainMH's solution, I may as well share my PHP code:
<pre><?php
// Set total number of iterations
$total = 1716;
// Set array of random number
$arr = array(1, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5);
$arr2 = array(0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 5);
// Print out random numbers
for ($i=0; $i<$total; $i++){
// Pick random array index
$rand = array_rand($arr);
$rand2 = array_rand($arr2);
// Print array values
print $arr[$rand] . "\t" . $arr2[$rand2] . "\r\n";
}
?></pre>
I just released a class to perform weighted sorting easily.
It's based on the same algorithm mentioned in Brad's and Allain's answers, and is optimized for speed, unit-tested for uniform distribution, and supports elements of any PHP type.
Using it is simple. Instantiate it:
$picker = new Brick\Random\RandomPicker();
Then add elements as an array of weighted values (only if your elements are strings or integers):
$picker->addElements([
'foo' => 25,
'bar' => 50,
'baz' => 100
]);
Or use individual calls to addElement(). This method supports any kind of PHP values as elements (strings, numbers, objects, ...), as opposed to the array approach:
$picker->addElement($object1, $weight1);
$picker->addElement($object2, $weight2);
Then get a random element:
$element = $picker->getRandomElement();
The probability of getting one of the elements depends on its associated weight. The only restriction is that weights must be integers.
Many of the answers on this page seem to use array bloating, excessive iteration, a library, or a hard-to-read process. Of course, everyone thinks their own baby is the cutest, but I honestly think my approach is lean, simple and easy to read/modify...
Per the OP, I will create an array of values (declared as keys) from 1 to 10, with 3, 4, and 5 having double the weight of the other values (declared as values).
$values_and_weights=array(
1=>1,
2=>1,
3=>2,
4=>2,
5=>2,
6=>1,
7=>1,
8=>1,
9=>1,
10=>1
);
If you are only going to make one random selection and/or your array is relatively small* (do your own benchmarking to be sure), this is probably your best bet:
$pick=mt_rand(1,array_sum($values_and_weights));
$x=0;
foreach($values_and_weights as $val=>$wgt){
if(($x+=$wgt)>=$pick){
echo "$val";
break;
}
}
This approach involves no array modification and probably won't need to iterate the entire array (but may).
On the other hand, if you are going to make more than one random selection on the array and/or your array is sufficiently large* (do your own benchmarking to be sure), restructuring the array may be better.
The cost in memory for generating a new array will be increasingly justified as:
array size increases and
number of random selections increases.
The new array requires the replacement of "weight" with a "limit" for each value by adding the previous element's weight to the current element's weight.
Then flip the array so that the limits are the array keys and the values are the array values.
The logic is: the selected value will have the lowest limit that is >= $pick.
// Declare new array using array_walk one-liner:
array_walk($values_and_weights,function($v,$k)use(&$limits_and_values,&$x){$limits_and_values[$x+=$v]=$k;});
//Alternative declaration method - 4-liner, foreach() loop:
/*$x=0;
foreach($values_and_weights as $val=>$wgt){
$limits_and_values[$x+=$wgt]=$val;
}*/
var_export($limits_and_values);
Creates this array:
array (
1 => 1,
2 => 2,
4 => 3,
6 => 4,
8 => 5,
9 => 6,
10 => 7,
11 => 8,
12 => 9,
13 => 10,
)
Now to generate the random $pick and select the value:
// $x (from walk/loop) is the same as writing: end($limits_and_values); $x=key($limits_and_values);
$pick=mt_rand(1,$x); // pull random integer between 1 and highest limit/key
while(!isset($limits_and_values[$pick])){++$pick;} // smallest possible loop to find key
echo $limits_and_values[$pick]; // this is your random (weighted) value
This approach is brilliant because isset() is very fast and the maximum number of isset() calls in the while loop can only be as many as the largest weight (not to be confused with limit) in the array. For this case, maximum iterations = 2!
THIS APPROACH NEVER NEEDS TO ITERATE THE ENTIRE ARRAY
I used this:
mt_rand($min, mt_rand($min, $max));
it give more lower values and less higher values, since the more the value is high the more is cutted out by one of the mt_rand
The probability is linearly increasing in the lower values, forming a square diagonal (see maths lower)
PRO: easy and strightforward
CON: maybe too simple so not enough weightable or balanceable for some use case
Maths:
let i index of i-nth value from min to max,
let P(i) the probability of obtaining the i-nth value,
let N=max-min:
P(i)=(1+N-i)/sum(1,N)
Since N is equals for all terms:
P(i) is proportional to N-i
so, in facts, the probability is linearly increasing in the lower values, forming a square diagonal
Variants:
you can write variants:
mt_rand($min, mt_rand(1, mt_rand(1, $max))); //value more given in low part
mt_rand(mt_rand($min, $max), $max); //mirrored, more upper values than lower
...
function getBucketFromWeights($values) {
$total = $currentTotal = $bucket = 0;
foreach ($values as $amount) {
$total += $amount;
}
$rand = mt_rand(0, $total-1);
foreach ($values as $amount) {
$currentTotal += $amount;
if ($rand => $currentTotal) {
$bucket++;
}
else {
break;
}
}
return $bucket;
}
I ugh modified this from an answer here Picking random element by user defined weights
After I wrote this I saw someone else had an even more elegant answer. He he he he.