Weighted Returns With PHP

Posted on

Getting Started

I was researching into the idea of randomly selecting array elements based on varying weights, i was looking for something like;

<?php
  $arr = ['RED' => 50, 'BLUE' => 25, 'GREEN' => 25];
  return_me_a_true_random($arr);

From this I would expect red to be returned around 50% of the time, blue around a quarter, and green around a quarter also.

I came across this on stack overflow and wanted to test how true the outcome is.

Implementation

The Code

function getRandomWeightedElement(array $weightedValues) {
  $rand = mt_rand(1, (int) array_sum($weightedValues));

  foreach ($weightedValues as $key => $value) {
    $rand -= $value;
    if ($rand <= 0) {
      return $key;
    }
  }
}

The Test

I decided to test this PHPUnit, so I created the following test.

require './vendor/autoload.php';
require './random-weighted.php';

class RandomTest extends PHPUnit_Framework_TestCase
{
    protected $iterations;

    public function setUp()
    {
        parent::setUp();
        $this->iterations = 1000;
    }

    /**
     * Helper method to see if result is roughly in the correct percentage outcome
     */
    public function isInbetween($number, $min, $max)
    {
        if($number > $min && $number < $max) {
            return true;
        }
        return false;
    }

    /**
     * @test
     */
    public function testWeightedArray()
    {
        // these are the weighted results
        $arr = ['RED' => 50, 'GREEN' => 25, 'BLUE' => 25];
        // results array
        $results = ['RED' => 0, 'GREEN' => 0, 'BLUE' => 0];

        // loop through 1000 times to get a true repesentation of results
        for($i = 0; $i < $this->iterations; $i++){
            //execute random
            $color = getRandomWeightedElement($arr);
            // store results
            $results[$color]++;
        }

        // Test for the outcome of red, somewhere around 45% - 55%
        $redMin = ($this->iterations / 2) - ($this->iterations * 0.10);
        //var_dump($redMin);exit;
        $redMax = ($this->iterations / 2) + ($this->iterations * 0.10);
        //var_dump($redMax);exit;
        $redResult = $this->isInbetween($results['RED'], $redMin, $redMax);
        $this->assertTrue($redResult);

        // Test for the outcome of green, somewhere around 20% - 30%
        $greenMin = ($this->iterations / 4) - ($this->iterations * 0.10);
        //var_dump($redMin);exit;
        $greenMax = ($this->iterations / 4) + ($this->iterations * 0.10);
        //var_dump($redMax);exit;
        $greenResult = $this->isInbetween($results['GREEN'], $greenMin, $greenMax);
        $this->assertTrue($greenResult);

        // Test for the outcome of blue, somewhere around 20% - 30%
        $blueMin = ($this->iterations / 4) - ($this->iterations * 0.10);
        //var_dump($redMin);exit;
        $blueMax = ($this->iterations / 4) + ($this->iterations * 0.10);
        //var_dump($redMax);exit;
        $blueResult = $this->isInbetween($results['BLUE'], $blueMin, $blueMax);
        $this->assertTrue($blueResult);

    }
}

In essence I set the weights associated to each of the array items and ran the function 10,000 times. I then examined the results.

The Results My results were the following;

  • Red: 497
  • Green: 272
  • Blue: 231

So this does work as expected, and the more you run this function the more accurate it gets.