SplitTestAnalyzer::getMathRandom()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace BenTools\SplitTestAnalyzer;
4
5
final class SplitTestAnalyzer implements \IteratorAggregate
6
{
7
8
    const DEFAULT_SAMPLES = 5000;
9
10
    /**
11
     * @var int
12
     */
13
    private $numSamples;
14
15
    /**
16
     * @var Variation[]
17
     */
18
    private $variations = [];
19
20
    /**
21
     * @var array
22
     */
23
    private $result;
24
25
    /**
26
     * BayesianPerformance constructor.
27
     * @param int       $numSamples
28
     */
29
    private function __construct(int $numSamples = self::DEFAULT_SAMPLES)
30
    {
31
        $this->numSamples = $numSamples;
32
    }
33
34
    /**
35
     * @param Variation[] ...$variations
36
     * @throws \InvalidArgumentException
37
     */
38
    private function setVariations(Variation ...$variations)
39
    {
40
        foreach ($variations as $variation) {
41
            if (array_key_exists($variation->getKey(), $this->variations)) {
0 ignored issues
show
Bug introduced by
The method getKey cannot be called on $variation (of type array<integer,object<Ben...estAnalyzer\Variation>>).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
42
                throw new \InvalidArgumentException(sprintf('Variation %s already exists into the stack.', $variation->getKey()));
0 ignored issues
show
Bug introduced by
The method getKey cannot be called on $variation (of type array<integer,object<Ben...estAnalyzer\Variation>>).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
43
            }
44
45
            $this->variations[$variation->getKey()] = $variation;
0 ignored issues
show
Bug introduced by
The method getKey cannot be called on $variation (of type array<integer,object<Ben...estAnalyzer\Variation>>).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
46
        }
47
    }
48
49
    /**
50
     * @param int       $numSamples
51
     * @return SplitTestAnalyzer
52
     */
53
    public static function create(int $numSamples = self::DEFAULT_SAMPLES): self
54
    {
55
        return new self($numSamples);
56
    }
57
58
    /**
59
     * @param Variation[] ...$variations
60
     * @return SplitTestAnalyzer
61
     * @throws \InvalidArgumentException
62
     */
63
    public function withVariations(Variation ...$variations): self
64
    {
65
        if (count($variations) < 2) {
66
            throw new \InvalidArgumentException("At least 2 variations to compare are expected.");
67
        }
68
        $object = ([] === $this->variations) ? $this : clone $this;
69
        $object->reset();
70
        $object->setVariations(...$variations);
71
        return $object;
72
    }
73
74
    /**
75
     * @param Variation $variation
76
     * @return float
77
     */
78
    public function getVariationProbability(Variation $variation): float
79
    {
80
        $this->check($variation);
81
        $this->computeProbabilities();
82
        return $this->result[$variation->getKey()];
83
    }
84
85
    /**
86
     * @param int $sort
87
     * @return Variation[]
88
     */
89
    public function getOrderedVariations(int $sort = SORT_DESC): array
90
    {
91
        $this->computeProbabilities();
92
        $variations = $this->variations;
93
        uasort($variations, function (Variation $variationA, Variation $variationB) use ($sort) {
94
            if (SORT_DESC === $sort) {
95
                return $this->getVariationProbability($variationB) <=> $this->getVariationProbability($variationA);
96
            } else {
97
                return $this->getVariationProbability($variationA) <=> $this->getVariationProbability($variationB);
98
            }
99
        });
100
101
        return $variations;
102
    }
103
104
    /**
105
     * @return Variation|null
106
     */
107
    public function getBestVariation(): ?Variation
108
    {
109
        $variations = $this->getOrderedVariations(SORT_DESC);
110
        $best = reset($variations);
111
        if ($best instanceof Variation && 0 === $best->getNbSuccesses()) {
112
            return null;
113
        }
114
        return $best;
115
    }
116
117
    /**
118
     * @return array
119
     */
120
    public function getResult()
121
    {
122
        $this->computeProbabilities();
123
        return $this->result;
124
    }
125
126
    public function getIterator()
127
    {
128
        $result = $this->getResult();
129
        yield from $result;
130
    }
131
132
    /**
133
     * @return mixed
134
     * @throws \Exception
135
     * @throws \InvalidArgumentException
136
     */
137
    private function computeProbabilities()
138
    {
139
        if (count($this->variations) < 2) {
140
            throw new \InvalidArgumentException("At least 2 variations to compare are expected.");
141
        }
142
143
        if (null !== $this->result) {
144
            return;
145
        }
146
        $variations = $this->variations;
147
        $winnerIndex = null;
148
        $numRows = count($variations);
149
        $winCount = array_combine(array_keys($variations), array_fill(0, $numRows, 0));
150
151
        for ($i = 0; $i < $this->numSamples; $i++) {
152
            $winnerValue = 0;
153
154
            foreach ($variations as $v => $variation) {
155
                $x = $this->getSample($variation->getNbSuccesses(), $variation->getNbFailures());
156
                if ($x > $winnerValue) {
157
                    $winnerIndex = $v;
158
                    $winnerValue = $x;
159
                }
160
            }
161
162
            if (null !== $winnerIndex) {
163
                $winCount[$winnerIndex]++;
164
            }
165
        }
166
167
        foreach ($variations as $v => $variation) {
168
            $this->result[$v] = round($winCount[$v] / ($this->numSamples / 100));
169
        }
170
    }
171
172
    /**
173
     * @param Variation[] ...$variations
174
     * @throws \InvalidArgumentException
175
     */
176
    private function check(Variation ...$variations)
177
    {
178
        foreach ($variations as $variation) {
179
            if (!in_array($variation, $this->variations)) {
180
                throw new \InvalidArgumentException(sprintf('Variation %s not found in the stack.', $variation->getKey()));
0 ignored issues
show
Bug introduced by
The method getKey cannot be called on $variation (of type array<integer,object<Ben...estAnalyzer\Variation>>).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
181
            }
182
        }
183
    }
184
185
    /**
186
     * @return float
187
     * @throws \Exception
188
     */
189
    private function getMathRandom(): float
190
    {
191
        return (float) random_int(0, PHP_INT_MAX) / PHP_INT_MAX;
192
    }
193
194
    /**
195
     * @return float
196
     * @throws \Exception
197
     */
198
    private function getRandn(): float
199
    {
200
        $u = $v = $x = $y = $q = null;
0 ignored issues
show
Unused Code introduced by
$q is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
Unused Code introduced by
$y is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
Unused Code introduced by
$x is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
Unused Code introduced by
$v is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
Unused Code introduced by
$u is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
201
        do {
202
            $u = $this->getMathRandom();
203
            $v = 1.7156 * ($this->getMathRandom() - 0.5);
204
            $x = $u - 0.449871;
205
            $y = abs($v) + 0.386595;
206
            $q = $x * $x + $y * (0.19600 * $y - 0.25472 * $x);
207
        } while ($q > 0.27597 && ($q > 0.27846 || $v * $v > -4 * log($u) * $u * $u));
208
        return $v / $u;
209
    }
210
211
    /**
212
     * @param float $shape
213
     * @return float
214
     * @throws \Exception
215
     */
216
    private function getRandg(float $shape): float
217
    {
218
        if (0.0 === $shape) {
219
            return 0;
220
        }
221
222
        $oalph = $shape;
223
        $a1 = $a2 = $u = $v = $x = null;
0 ignored issues
show
Unused Code introduced by
$x is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
Unused Code introduced by
$v is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
Unused Code introduced by
$u is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
Unused Code introduced by
$a2 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
Unused Code introduced by
$a1 is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
224
225
        if (!$shape) {
226
            $shape = 1;
227
        }
228
        if ($shape < 1) {
229
            $shape += 1;
230
        }
231
232
        $a1 = $shape - 1 / 3;
233
        $a2 = 1 / sqrt(9 * $a1);
234
        do {
235
            do {
236
                $x = $this->getRandn();
237
                $v = 1 + $a2 * $x;
238
            } while ($v <= 0);
239
            $v = $v * $v * $v;
240
            $u = $this->getMathRandom();
241
        } while ($u > 1 - 0.331 * pow($x, 4) &&
242
        log($u) > 0.5 * $x * $x + $a1 * (1 - $v + log($v)));
243
244
        // alpha > 1
245
        if ($shape == $oalph) {
246
            return $a1 * $v;
247
        }
248
        // alpha < 1
249
        do {
250
            $u = $this->getMathRandom();
251
        } while ($u === 0);
252
        return pow($u, 1 / $oalph) * $a1 * $v;
253
    }
254
255
    /**
256
     * @param float $alpha
257
     * @param float $beta
258
     * @return float
259
     * @throws \Exception
260
     */
261
    private function getSample(float $alpha, float $beta): float
262
    {
263
        if (0.0 === $alpha) {
264
            return 0.0;
265
        }
266
        $u = $this->getRandg($alpha);
267
        return $u / ($u + $this->getRandg($beta));
268
    }
269
270
    private function reset()
271
    {
272
        $this->variations = [];
273
        $this->result = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $result.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
274
    }
275
}
276