PHP random INT with evenly distributed order of magnitude results - php

PHP rand(min, max) between 1 and 9999 gives almost all results with 4 digits (because there are ~90% of the numbers with 4 digits). So, if I ran it 1000 times, roughly ~90% of them will probably have 4 digits.
Is there a way to generate a random INT from 1 to 9999 and that the output number have the same chance of having 1, 2, 3 or 4 digits without doing it manually?
By doing it manually I mean like this:
$digits = rand(1, 4);
$num = '';
for($i = 0; $i < $digits; $i++){
$num .= rand(0, 9);
}
$final = intval($num);

So, if I ran it 1000 times, roughly ~90% of them will probably have 4 digits.
That's exactly how uniform distributions work. There's no out of the box function to do what you're after, so you have to make some statistics magic.
What I'm thinking of is: generate a random number between 0 and 1. If it's between 0 and .25, generate another random number between 0 and 9. If it's between .25 and .5, generate another random number between 10 and 99, and so on and so forth. Then, you'd have 1/4 chance of getting each order of magnitude.
This will obviously have a bias towards the lower numbers though, since there are less of them. For example, 1 has a 25% / 10 = 2.5% chance, while 1001 has a 25% / 8998 = 0.00277% chance.
It'd go something like this:
<?php
$initial = rand(0, 100)/100;
if ($initial < .25) {
$random = rand(0, 9);
}
elseif ($initial < .5) {
$random = rand(10, 99);
}
elseif ($initial < .75) {
$random = rand(100, 999);
}
elseif ($initial >= .75) {
$random = rand(1000, 9999);
}
var_dump($random);
Demo

Related

Weighted Random Choice In PHP

I need help getting a my probability odds closer to testing results with low percentages. What I have seems to work for percentages at 1% or higher but I need it to work with very low percentages such as 0.02% (down to 4 decimals). Anything below 1% tends to end up having around a 1% probability after running tests from running 1000-100000 tests at once the results are similar.
Example Results
ID Odds Test Total Test Odds
1 60.0000 301773 60.3546%
2 30.0000 148360 29.672%
3 9.9800 44897 8.9794%
4 0.0200 4970 0.994%
Function
// $values = [1,2,3,4]
// $weights = [60.0000,30.0000,9.9800,.0200]
private function getRandom($values, $weights)
{
$count = count($values);
$i = 0;
$n = 0;
$num = mt_rand(0, array_sum($weights));
while($i < $count)
{
$n += $weights[$i];
if($n >= $num)
break;
$i++;
}
return $values[$i];
}
mt_rand returns an integer so comparing it to 0.02 is effectively the same as comparing it to 1. Hence you always get around 1% for the weights which are less than 1%. Try computing $num like this instead:
$num = mt_rand(0, array_sum($weights) * 100) / 100;
Demo on 3v4l.org

Round up decimal number for specific decimal places in PHP

I want to round up my variable if it's decimal larger than .3 and if it's lower or equal it will round down, for example if i have 1.34 it will round up to 2, if i have 1.29 it will round down to 1, and if i have 1.3 it will round down to 1. I don't know how to do this precisely, right now i'm using the round basic function like this:
$weight = $weight/1000;
if($weight < 1) $weight = 1;
else $weight = round($weight, 0, PHP_ROUND_HALF_DOWN);
If you manipulate the numbers a bit, you can figure out if the decimals are .3 or higher. You achieve this by flooring the value, and subtract that from the original value. Check if the result of that, multiplied by 10, is greater than 3. If it is, you've got something above x.3.
$number = 1.31;
$int = floor($number);
$float = $number-$int;
if ($float*10 > 3.1)
$result = ceil($number);
else
$result = $int;
echo $result; // 2
Live demo
I made you a little hack, here's the code
$weight = 5088;
$weight = $weight/1000;
if($weight < 1) {
$weight = 1;
} else {
// I get the last number (I treat the $weight as a string here)
$last_number = substr($weight, -1, 1);
// Then I get the precision (floating numbers)
$precision = strlen(substr(strrchr($weight, "."), 1));
// Then I convert it to a string so I can use some helpful string functions
$weight_str = (string) $weight;
// If the last number is less then 3
if ($last_number > 3)
// I change it to 9 I could just change it to 5 and it would work
// because round will round up if then number is 5 or greater
$weight_str[strlen($weight_str) -1] = 9;
}
}
// Then the round will round up if it's 9 or round down if it's 3 or less
$weight = round($weight_str, $precision);
echo $weight;
Maybe something like this function?
function roundImproved($value, $decimalBreakPart = 0.3) {
$whole = floor($value);
$decimal = $value - $whole;
$decimalPartLen = strlen($decimal) - 2;
return (number_format($decimal, $decimalPartLen) <= number_format($decimalBreakPart, $decimalPartLen) ? $whole : ceil($value));
}
Proof:
http://sandbox.onlinephpfunctions.com/code/d75858f175dd819de069a8a05611ac9e7053f07a
You can specify "break part" if you want.

How to get Long 19 digits random numbers at PHP?

So I need to make unique purchase number of mobile game. Each user's buying of in-app-purchase item record should be attached by unique long type 19 digits numbers from php code and stored to mysql DB.
But how can I actually make random 19 digits numbers?
I tried like this but errors.
$num_str = sprintf("%19d", mt_rand(1, 9999999999999999999));
you can also try this:
function random19() {
$number = "";
for($i=0; $i<19; $i++) {
$min = ($i == 0) ? 1:0;
$number .= mt_rand($min,9);
}
return $number;
}
echo random19();
which would output some random 19 numbers: 6416113158912395605
You have to do something like -
$num_str = sprintf("%10d", mt_rand(1, mt_getrandmax())).sprintf("%9d", mt_rand(1, mt_getrandmax()));
if (strlen($num_str) < 19)
$num_str = str_pad($num_str, 19, rand(0, 9), STR_PAD_RIGHT);
else if(strlen($num_str) > 19)
$num_str = substr($num_str, 0, 19);
echo $num_str;
The distribution of mt_rand() return values is biased towards even numbers on 64-bit builds of PHP when max is beyond 2^32. This is because if max is greater than the value returned by mt_getrandmax(), the output of the random number generator must be scaled up.
$randNumber = date("YmdHis").rand(11111,99999);

Defining percentage for random number

My rand(0,1) php function returns me the 0 and 1 randomly when I call it.
Can I define something in php, so that it makes 30% numbers will be 0 and 70% numbers will be 1 for the random calls? Does php have any built in function for this?
Sure.
$rand = (float)rand()/(float)getrandmax();
if ($rand < 0.3)
$result = 0;
else
$result = 1;
You can deal with arbitrary results and weights, too.
$weights = array(0 => 0.3, 1 => 0.2, 2 => 0.5);
$rand = (float)rand()/(float)getrandmax();
foreach ($weights as $value => $weight) {
if ($rand < $weight) {
$result = $value;
break;
}
$rand -= $weight;
}
You can do something like this:
$rand = (rand(0,9) > 6 ? 1 : 0)
rand(0,9) will produce a random number between 0 and 9, and whenever that randomly generated number is greater than 6 (which should be nearly 70% time), it will give you 1 otherwise 0...
Obviously, it seems to be the easiest solution to me, but definitely, it wont give you 1 exactly 70% times, but should be quite near to do that, if done correctly.
But, I doubt that any solution based on rand will give you 1 exactly 70% times...
Generate a new random value between 1 and 100. If the value falls below 30, then use 0, and 1 otherwise:
$probability = rand(1, 100);
if ($probability < 30) {
echo 0;
} else {
echo 1;
}
To test this theory, consider the following loop:
$arr = array();
for ($i=0; $i < 10000; $i++) {
$rand = rand(0, 1);
$probability = rand(1, 100);
if ($probability < 30) {
$arr[] = 0;
} else {
$arr[] = 1;
}
}
$c = array_count_values($arr);
echo "0 = " . $c['0'] / 10000 * 100;
echo "1 = " . $c['1'] / 10000 * 100;
Output:
0 = 29.33
1 = 70.67
Create an array with 70% 1 and 30% 0s. Then random sort it. Then start picking numbers from the beginning of the array to the end :)
$num_array = array();
for($i = 0; $i < 3; $i++) $num_array[$i] = 0;
for($i = 0; $i < 7; $i++) $num_array[$i] = 1;
shuffle($num_array);
Pros:
You'll get exactly 30% 0 and 70% 1 for any such array.
Cons: Might take longer computation time than a rand() only solution to create the initial array.
I searched for an answer to my question and this was the topic I found.
But it didn't answered my question, so I had to figure it out myself, and I did :).
I figured out that maybe this will help someone else as well.
It's regarding what you asked, but for more usage.
Basically, I use it as a "power" calculator for a random generated item (let's say a weapon). The item has a "min power" and a "max power" value in the db. And I wanted to have 80% chances to have the "power" value closer to the lower 80% of the max possible power for the item, and 20% for the highest 20% possible max power (that are stored in the db).
So, to do this I did the following:
$min = 1; // this value is normally taken from the db
$max = 30; // this value is normally taken from the db
$total_possibilities = ($max - $min) + 1;
$rand = random_int(1, 100);
if ($rand <= 80) { // 80% chances
$new_max = $max - ($total_possibilities * 0.20); // remove 20% from the max value, so you can get a number only from the lowest 80%
$new_rand = random_int($min, $new_max);
} elseif ($rand <= 100) { // 20% chances
$new_min = $min + ($total_possibilities * 0.80); // add 80% for the min value, so you can get a number only from the highest 20%
$new_rand = random_int($new_min, $max);
}
echo $new_rand; // this will be the final item power
The only problem you can have, is if the initial $min and $max variables are the same (or obviously, if the $max is bigger than the $min). This will throw an error since the random works like this ($min, $max), not the other way around.
This code can be very easily changed to have more percentages for different purposes, instead of 80% and 20% to put 40%, 40% and 20% (or whatever you need). I think the code is pretty much easy to read and understand.
Sorry if this is not helpful, but I hope it is :).
It can't do any harm either way ;).

Replace duplicate values in array with new randomly generated values

I have below a function (from a previous question that went unanswered) that creates an array with n amount of values. The sum of the array is equal to $max.
function randomDistinctPartition($n, $max) {
$partition= array();
for ($i = 1; $i < $n; $i++) {
$maxSingleNumber = $max - $n;
$partition[] = $number = rand(1, $maxSingleNumber);
$max -= $number;
}
$partition[] = $max;
return $partition;
}
For example: If I set $n = 4 and $max = 30. Then I should get the following.
array(5, 7, 10, 8);
However, this function does not take into account duplicates and 0s. What I would like - and have been trying to accomplish - is to generate an array with unique numbers that add up to my predetermined variable $max. No Duplicate numbers and No 0 and/or negative integers.
Ok, this problem actually revolves around linear sequences. With a minimum value of 1 consider the sequence:
f(n) = 1 + 2 + ... + n - 1 + n
The sum of such a sequence is equal to:
f(n) = n * (n + 1) / 2
so for n = 4, as an example, the sum is 10. That means if you're selecting 4 different numbers the minimum total with no zeroes and no negatives is 10. Now go in reverse: if you have a total of 10 and 4 numbers then there is only one combination of (1,2,3,4).
So first you need to check if your total is at least as high as this lower bound. If it is less there is no combination. If it is equal, there is precisely one combination. If it is higher it gets more complicated.
Now imagine your constraints are a total of 12 with 4 numbers. We've established that f(4) = 10. But what if the first (lowest) number is 2?
2 + 3 + 4 + 5 = 14
So the first number can't be higher than 1. You know your first number. Now you generate a sequence of 3 numbers with a total of 11 (being 12 - 1).
1 + 2 + 3 = 6
2 + 3 + 4 = 9
3 + 4 + 5 = 12
The second number has to be 2 because it can't be one. It can't be 3 because the minimum sum of three numbers starting with 3 is 12 and we have to add to 11.
Now we find two numbers that add up to 9 (12 - 1 - 2) with 3 being the lowest possible.
3 + 4 = 7
4 + 5 = 9
The third number can be 3 or 4. With the third number found the last is fixed. The two possible combinations are:
1, 2, 3, 6
1, 2, 4, 5
You can turn this into a general algorithm. Consider this recursive implementation:
$all = all_sequences(14, 4);
echo "\nAll sequences:\n\n";
foreach ($all as $arr) {
echo implode(', ', $arr) . "\n";
}
function all_sequences($total, $num, $start = 1) {
if ($num == 1) {
return array($total);
}
$max = lowest_maximum($start, $num);
$limit = (int)(($total - $max) / $num) + $start;
$ret = array();
if ($num == 2) {
for ($i = $start; $i <= $limit; $i++) {
$ret[] = array($i, $total - $i);
}
} else {
for ($i = $start; $i <= $limit; $i++) {
$sub = all_sequences($total - $i, $num - 1, $i + 1);
foreach ($sub as $arr) {
array_unshift($arr, $i);
$ret[] = $arr;
}
}
}
return $ret;
}
function lowest_maximum($start, $num) {
return sum_linear($num) + ($start - 1) * $num;
}
function sum_linear($num) {
return ($num + 1) * $num / 2;
}
Output:
All sequences:
1, 2, 3, 8
1, 2, 4, 7
1, 2, 5, 6
1, 3, 4, 6
2, 3, 4, 5
One implementation of this would be to get all the sequences and select one at random. This has the advantage of equally weighting all possible combinations, which may or may not be useful or necessary to what you're doing.
That will become unwieldy with large totals or large numbers of elements, in which case the above algorithm can be modified to return a random element in the range from $start to $limit instead of every value.
I would use 'area under triangle' formula... like cletus(!?)
Im really gonna have to start paying more attention to things...
Anyway, i think this solution is pretty elegant now, it applies the desired minimum spacing between all elements, evenly, scales the gaps (distribution) evenly to maintain the original sum and does the job non-recursively (except for the sort):
Given an array a() of random numbers of length n
Generate a sort index s()
and work on the sorted intervals a(s(0))-a(s(1)), a(s(1))-a(s(2)) etc
increase each interval by the
desired minimum separation size eg 1
(this necessarily warps their
'randomness')
decrease each interval by a factor
calculated to restore the series sum
to what it is without the added
spacing.
If we add 1 to each of a series we increase the series sum by 1 * len
1 added to each of series intervals increases sum by:
len*(len+1)/2 //( ?pascal's triangle )
Draft code:
$series($length); //the input sequence
$seriesum=sum($series); //its sum
$minsepa=1; //minimum separation
$sorti=sort_index_of($series) //sorted index - php haz function?
$sepsum=$minsepa*($length*($length+1))/2;
//sum of extra separation
$unsepfactor100=($seriesum*100)/($seriesum+sepsum);
//scale factor for original separation to maintain size
//(*100~ for integer arithmetic)
$px=series($sorti(0)); //for loop needs the value of prev serie
for($x=1 ; $x < length; $x++)
{ $tx=$series($sorti($x)); //val of serie to
$series($sorti($x))= ($minsepa*$x) //adjust relative to prev
+ $px
+ (($tx-$px)*$unsepfactor100)/100;
$px=$tx; //store for next iteration
}
all intervals are reduced by a
constant (non-random-warping-factor)
separation can be set to values other
than one
implementantions need to be carefuly
tweaked (i usualy test&'calibrate')
to accomodate rounding errors.
Probably scale everything up by ~15
then back down after. Intervals should survive if done right.
After sort index is generated, shuffle the order of indexes to duplicate values to avoid runs in the sequence of collided series.
( or just shuffle final output if order never mattered )
Shuffle indexes of dupes:
for($x=1; $x<$len; $x++)
{ if ($series($srt($x))==$series($srt($x-1)))
{ if( random(0,1) )
{ $sw= $srt($x);
$srt($x)= $srt($x-1);
$srt($x-1)= $sw;
} } }
A kind of minimal disturbance can be done to a 'random sequence' by just parting dupes by the minimum required, rather than moving them more than minimum -some 'random' amount that was sought by the question.
The code here separates every element by the min separation, whether duplicate or not, that should be kindof evenhanded, but overdone maybe. The code could be modified to only separate the dupes by looking through the series(sorti(n0:n1..len)) for them and calculating sepsum as +=minsep*(len-n) for each dupe. Then the adjustment loop just has to test again for dupe before applying adjustment.

Categories