Rough
Rough

Reputation: 366

Pick random value by weight php

I'm about to create "lottary system."

Take a look at my table:

userid-lottaryid-amount
1 -------- 1 ----  1
2 -------- 1 ---- 10
3 -------- 1 ---- 15
4 -------- 1 ---- 20

I want to choose a winner. and another person for second place.

I just can't select a winner randomly because 4th user has 20 tickets and 1st user has only one. So I need to generate random results by weight in order to be more fair.

I found php function below but I couldn't figure out how to use it.

      function weighted_random_simple($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]; 

  }

    $values = array('1', '10', '20', '100');
    $weights = array(1, 10, 20, 100);

    echo weighted_random_simple($values, $weights);

I must fetch userid colomn as array to $values and amount colomn to $weights. But I couln't.

Here is my code so far:

    $query = $handler->prepare("SELECT 

      `cvu`.`lottaryid` as `lottaryid`, 
      `cvu`.`userid` as `userid`, 
      `cvu`.`amount` as `amount`, 

      `members`.`id` as `members_memberid`, 
      `members`.`username` as `username`

      FROM `lottariesandmembers` as `cvu`

      LEFT JOIN `members` as `members` ON `cvu`.`userid` = `members`.`id`  WHERE `cvu`.`lottaryid` = 2");
    $query->bindParam(':lottaryid', $lottaryid, PDO::PARAM_INT);
    $query->execute();



    while($r = $query->fetch()) {

        for ( $count=1 ; $count <= $r["amount"] ; $count++ ) {

            $abcprint = "$r[userid].$count - $r[username] - <br>";

            echo "$abcprint";

        }


    } 

This code I have, only lists users as many times as their amount. For example:

1.1 user1
2.1 user2
2.2 user2
2.3 user2
..
2.10 user2
3.1 user3
..
3.15 user3
4.1 user4
..
4.20 user4

and so on.. But I'm stuck how to pick a winner on that list.

I would like to merge those codes and create this little script, if you would like to help me.

I'm also open for brainstorm if you see the solution on the other way around.

Upvotes: 4

Views: 657

Answers (4)

IDontKnow
IDontKnow

Reputation: 355

  /**
   * 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;
      }
    }
  }

Here is an efficient and flexible function. But You have to modify it if you want to use non-integer weighting.

Upvotes: 2

Progrock
Progrock

Reputation: 7485

This isn't very elegant but should work for smallish lotteries.

It just constructs a massive array and picks an element at random.

Think of having a massive hat full of slips. Each holder gets their stake in 'slips' and each are labelled with their id. i.e. Ten slips with the holder's name 'a', 20 slips with 'b' and so on...

<?php

$holder_totals = array(
    'a' => '10',
    'b' => '20',
    'c' => '20',
    'd' => '50'
);

$big_hat = array();
foreach($holder_totals as $holder_id => $total) {
    $holder_hat = array_fill(0, intval($total), $holder_id);
    $big_hat    = array_merge($big_hat, $holder_hat);
}

// Drum roll
foreach (range(1,4) as $n) {
    $random_key = array_rand($big_hat);
    printf("Winner %d is %s.\n", $n, $big_hat[$random_key]);
    unset($big_hat[$random_key]); // Remove winning slip
}

Sample output:

Winner 1 is d.
Winner 2 is c.
Winner 3 is d.
Winner 4 is b.

Big hat looks like this:

Array
(
    [0] => a
    [1] => a
    [2] => a
    [3] => a
    [4] => a
    [5] => a
    [6] => a
    [7] => a
    [8] => a
    [9] => a
    [10] => b
    [11] => b
    [12] => b
    [13] => b
    [14] => b
    ... and so on...
)

Upvotes: 2

Don&#39;t Panic
Don&#39;t Panic

Reputation: 41820

Instead of printing out the values as you are doing, you can just build a large array, and then choose a value randomly from that array.

while($r = $query->fetch()) {
    for ( $i=0; $i <= $r["amount"]; $i++ ) {
        // Add the user into the array as many times as they have tickets
        $tickets[] = $r['userid'];
    }
}

// select the first place winner
$first = $tickets[mt_rand(0, count($tickets) - 1)];

// remove the first place winner from the array
$tickets = array_values(array_filter($tickets, function($x) use ($first) { 
    return $x != $first; 
}));

// select the second place winner
$second = $tickets[mt_rand(0, count($tickets) - 1)];

I'm sure there is a more efficient way to do this using math, but I need to think about it a bit more...

Upvotes: 2

Ihor Burlachenko
Ihor Burlachenko

Reputation: 4905

You can use weightedChoice function from my library nspl.

use function \nspl\rnd\weightedChoice;

// building your query here

$pairs = [];
while($r = $query->fetch()) {
    $pairs[] = [$r['userid'], $r['amount']];
}

$winnerId = weightedChoice($pairs);

You can install the library with composer:

composer require ihor/nspl

Or you can simply reuse weightedChoice code from GitHub:

/**
 * Returns a random element from a non-empty sequence of items with associated weights
 *
 * @param array $weightPairs List of pairs [[item, weight], ...]
 * @return mixed
 */
function weightedChoice(array $weightPairs)
{
    if (!$weightPairs) {
        throw new \InvalidArgumentException('Weight pairs are empty');
    }

    $total = array_reduce($weightPairs, function($sum, $v) { return $sum + $v[1]; });
    $r = mt_rand(1, $total);

    reset($weightPairs);
    $acc = current($weightPairs)[1];
    while ($acc < $r && next($weightPairs)) {
        $acc += current($weightPairs)[1];
    }

    return current($weightPairs)[0];
}

Upvotes: 1

Related Questions