Completed
Push — master ( bccb8a...b28953 )
by BENOIT
02:24
created

SplitTestAnalyzer::getIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
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
106
     */
107
    public function getBestVariation(): Variation
108
    {
109
        $variations = $this->getOrderedVariations(SORT_DESC);
110
        return reset($variations);
111
    }
112
113
    /**
114
     * @return array
115
     */
116
    public function getResult()
117
    {
118
        $this->computeProbabilities();
119
        return $this->result;
120
    }
121
122
    public function getIterator()
123
    {
124
        $result = $this->getResult();
125
        yield from $result;
126
    }
127
128
    /**
129
     * @return mixed
130
     * @throws \Exception
131
     * @throws \InvalidArgumentException
132
     */
133
    private function computeProbabilities()
134
    {
135
        if (count($this->variations) < 2) {
136
            throw new \InvalidArgumentException("At least 2 variations to compare are expected.");
137
        }
138
139
        if (null !== $this->result) {
140
            return;
141
        }
142
        $variations = $this->variations;
143
        $winnerIndex = null;
144
        $numRows = count($variations);
145
        $winCount = array_combine(array_keys($variations), array_fill(0, $numRows, 0));
146
147
        for ($i = 0; $i < $this->numSamples; $i++) {
148
            $winnerValue = 0;
149
150
            foreach ($variations as $v => $variation) {
151
                $x = $this->getSample($variation->getNbSuccesses(), $variation->getNbFailures());
152
                if ($x > $winnerValue) {
153
                    $winnerIndex = $v;
154
                    $winnerValue = $x;
155
                }
156
            }
157
            $winCount[$winnerIndex]++;
158
        }
159
160
        foreach ($variations as $v => $variation) {
161
            $this->result[$v] = round($winCount[$v] / ($this->numSamples / 100));
162
        }
163
    }
164
165
    /**
166
     * @param Variation[] ...$variations
167
     * @throws \InvalidArgumentException
168
     */
169
    private function check(Variation ...$variations)
170
    {
171
        foreach ($variations as $variation) {
172
            if (!in_array($variation, $this->variations)) {
173
                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...
174
            }
175
        }
176
    }
177
178
    /**
179
     * @return float
180
     * @throws \Exception
181
     */
182
    private function getMathRandom(): float
183
    {
184
        return (float) random_int(0, PHP_INT_MAX) / PHP_INT_MAX;
185
    }
186
187
    /**
188
     * @return float
189
     * @throws \Exception
190
     */
191
    private function getRandn(): float
192
    {
193
        $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...
194
        do {
195
            $u = $this->getMathRandom();
196
            $v = 1.7156 * ($this->getMathRandom() - 0.5);
197
            $x = $u - 0.449871;
198
            $y = abs($v) + 0.386595;
199
            $q = $x * $x + $y * (0.19600 * $y - 0.25472 * $x);
200
        } while ($q > 0.27597 && ($q > 0.27846 || $v * $v > -4 * log($u) * $u * $u));
201
        return $v / $u;
202
    }
203
204
    /**
205
     * @param float $shape
206
     * @return float
207
     * @throws \Exception
208
     */
209
    private function getRandg(float $shape): float
210
    {
211
        if (0.0 === $shape) {
212
            return 0;
213
        }
214
215
        $oalph = $shape;
216
        $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...
217
218
        if (!$shape) {
219
            $shape = 1;
220
        }
221
        if ($shape < 1) {
222
            $shape += 1;
223
        }
224
225
        $a1 = $shape - 1 / 3;
226
        $a2 = 1 / sqrt(9 * $a1);
227
        do {
228
            do {
229
                $x = $this->getRandn();
230
                $v = 1 + $a2 * $x;
231
            } while ($v <= 0);
232
            $v = $v * $v * $v;
233
            $u = $this->getMathRandom();
234
        } while ($u > 1 - 0.331 * pow($x, 4) &&
235
        log($u) > 0.5 * $x * $x + $a1 * (1 - $v + log($v)));
236
237
        // alpha > 1
238
        if ($shape == $oalph) {
239
            return $a1 * $v;
240
        }
241
        // alpha < 1
242
        do {
243
            $u = $this->getMathRandom();
244
        } while ($u === 0);
245
        return pow($u, 1 / $oalph) * $a1 * $v;
246
    }
247
248
    /**
249
     * @param float $alpha
250
     * @param float $beta
251
     * @return float
252
     * @throws \Exception
253
     */
254
    private function getSample(float $alpha, float $beta): float
255
    {
256
        $u = $this->getRandg($alpha);
257
        return $u / ($u + $this->getRandg($beta));
258
    }
259
260
    private function reset()
261
    {
262
        $this->variations = [];
263
        $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...
264
    }
265
}
266