Skip to content

Commit 0a9bfee

Browse files
authored
Added percentile and median functions (#43)
1 parent 3f39852 commit 0a9bfee

File tree

3 files changed

+180
-8
lines changed

3 files changed

+180
-8
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ Filter::clean(string $string): string; // Alias of "Str::clean($string, true, tr
294294

295295
Filter::cmd(string $value): string; // Cleanup system command.
296296

297-
Filter::data(JBZoo\Data\Data|array $data): JBZoo\Data\Data; // Returns JSON object from array.
297+
Filter::data(JBZoo\Data\Data|array $data): JBZoo\Data\Data; // Returns Data object from array.
298298

299299
Filter::digits(??string $value): string; // Returns only digits chars.
300300

@@ -306,6 +306,8 @@ Filter::html(string $string): string; // Alias of "Str::htmlEnt($string)".
306306

307307
Filter::int(?string|int|float|bool|null $value): int; // Smart convert any string to int.
308308

309+
Filter::json(JBZoo\Data\JSON|array $data): JBZoo\Data\JSON; // Returns JSON object from array.
310+
309311
Filter::low(string $string): string; // String to lower and trim.
310312

311313
Filter::parseLines(array|string $input): array; // Parse lines to assoc list.
@@ -495,6 +497,10 @@ Stats::linSpace(float $min, float $max, int $num = 50, bool $endpoint = true): a
495497

496498
Stats::mean(??array $values): float; // Returns the mean (average) value of the given values.
497499

500+
Stats::median(array $data): ??float; // Calculate the median of a given population.
501+
502+
Stats::percentile(array $data, int|float $percentile = 95): ??float; // Calculate the percentile of a given population.
503+
498504
Stats::renderAverage(array $values, int $rounding = 3): string; // Render human readable string of average value and system error.
499505

500506
Stats::stdDev(array $values, bool $sample = false): float; // Returns the standard deviation of a given population.
@@ -643,7 +649,7 @@ Sys::isFunc(Closure|string $funcName): bool; // Checks if function exists and ca
643649

644650
Sys::isHHVM(): bool; // Returns true when the runtime used is HHVM.
645651

646-
Sys::isPHP(string $version, string $current = '8.1.16'): bool; // Compares PHP versions.
652+
Sys::isPHP(string $version, string $current = '8.1.22'): bool; // Compares PHP versions.
647653

648654
Sys::isPHPDBG(): bool; // Returns true when the runtime used is PHP with the PHPDBG SAPI.
649655

src/Stats.php

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public static function mean(?array $values): float
6868

6969
$count = \count($values);
7070

71-
return $sum / $count;
71+
return \round($sum / $count, 9);
7272
}
7373

7474
/**
@@ -153,4 +153,55 @@ public static function renderAverage(array $values, int $rounding = 3): string
153153

154154
return "{$avg}±{$stdDev}";
155155
}
156+
157+
/**
158+
* Render human readable string of average value and system error.
159+
*/
160+
public static function renderMedian(array $values, int $rounding = 3): string
161+
{
162+
$avg = \number_format(self::median($values), $rounding);
163+
$stdDev = \number_format(self::stdDev($values), $rounding);
164+
165+
return "{$avg}±{$stdDev}";
166+
}
167+
168+
/**
169+
* Calculate the percentile of a given population.
170+
* @param float[]|int[] $data
171+
*/
172+
public static function percentile(array $data, float|int $percentile = 95): float
173+
{
174+
$count = \count($data);
175+
if ($count === 0) {
176+
return 0;
177+
}
178+
179+
$percent = $percentile / 100;
180+
if ($percent < 0 || $percent > 1) {
181+
throw new Exception("Percentile should be between 0 and 100, {$percentile} given");
182+
}
183+
184+
$allIndex = ($count - 1) * $percent;
185+
$intValue = (int)$allIndex;
186+
$floatValue = $allIndex - $intValue;
187+
188+
\sort($data, \SORT_NUMERIC);
189+
190+
if ($intValue + 1 < $count) {
191+
$result = $data[$intValue] + ($data[$intValue + 1] - $data[$intValue]) * $floatValue;
192+
} else {
193+
$result = $data[$intValue];
194+
}
195+
196+
return \round(float($result), 6);
197+
}
198+
199+
/**
200+
* Calculate the median of a given population.
201+
* @param float[]|int[] $data
202+
*/
203+
public static function median(array $data): float
204+
{
205+
return self::percentile($data, 50.0);
206+
}
156207
}

tests/StatsTest.php

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ public function testMean(): void
2929
isSame(1.0, Stats::mean([1]));
3030
isSame(1.0, Stats::mean([1, 1]));
3131
isSame(2.0, Stats::mean([1, 3]));
32+
isSame(2.0, Stats::mean(['1', 3]));
33+
isSame(2.25, Stats::mean(['1.5', 3]));
34+
35+
$data = [72, 57, 66, 92, 32, 17, 146];
36+
isSame(68.857142857, Stats::mean($data));
3237
}
3338

3439
public function testStdDev(): void
@@ -81,10 +86,120 @@ public function testHistogram(): void
8186

8287
public function testRenderAverage(): void
8388
{
84-
isSame('1.500±0.500', Stats::renderAverage([1, 2, 1, 2]));
85-
isSame('1.5±0.5', Stats::renderAverage([1, 2, 1, 2], 1));
86-
isSame('1.50±0.50', Stats::renderAverage([1, 2, 1, 2], 2));
87-
isSame('2±1', Stats::renderAverage([1, 2, 1, 2], 0));
88-
isSame('2±1', Stats::renderAverage([1, 2, 1, 2], -1));
89+
$data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
90+
isSame('5.500±2.872', Stats::renderAverage($data));
91+
isSame('5.5±2.9', Stats::renderAverage($data, 1));
92+
isSame('5.50±2.87', Stats::renderAverage($data, 2));
93+
isSame('6±3', Stats::renderAverage($data, 0));
94+
isSame('6±3', Stats::renderAverage($data, -1));
95+
96+
$data = [72, 57, 66, 92, 32, 17, 146];
97+
isSame('68.857±39.084', Stats::renderAverage($data));
98+
isSame('68.9±39.1', Stats::renderAverage($data, 1));
99+
isSame('68.86±39.08', Stats::renderAverage($data, 2));
100+
isSame('69±39', Stats::renderAverage($data, 0));
101+
isSame('69±39', Stats::renderAverage($data, -1));
102+
}
103+
104+
public function testRenderMedian(): void
105+
{
106+
$data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
107+
isSame('5.500±2.872', Stats::renderMedian($data));
108+
isSame('5.5±2.9', Stats::renderMedian($data, 1));
109+
isSame('5.50±2.87', Stats::renderMedian($data, 2));
110+
isSame('6±3', Stats::renderMedian($data, 0));
111+
isSame('6±3', Stats::renderMedian($data, -1));
112+
113+
$data = [72, 57, 66, 92, 32, 17, 146];
114+
isSame('66.000±39.084', Stats::renderMedian($data));
115+
isSame('66.0±39.1', Stats::renderMedian($data, 1));
116+
isSame('66.00±39.08', Stats::renderMedian($data, 2));
117+
isSame('66±39', Stats::renderMedian($data, 0));
118+
isSame('66±39', Stats::renderMedian($data, -1));
119+
}
120+
121+
public function testPercentile(): void
122+
{
123+
$data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
124+
isSame(1.0, Stats::percentile($data, 0));
125+
isSame(1.09, Stats::percentile($data, 1));
126+
isSame(1.9, Stats::percentile($data, 10));
127+
isSame(2.8, Stats::percentile($data, 20));
128+
isSame(3.7, Stats::percentile($data, 30));
129+
isSame(4.6, Stats::percentile($data, 40));
130+
isSame(5.5, Stats::percentile($data, 50));
131+
isSame(6.4, Stats::percentile($data, 60));
132+
isSame(7.3, Stats::percentile($data, 70));
133+
isSame(8.2, Stats::percentile($data, 80));
134+
isSame(9.1, Stats::percentile($data, 90));
135+
isSame(9.55, Stats::percentile($data));
136+
isSame(9.91, Stats::percentile($data, 99));
137+
isSame(9.9991, Stats::percentile($data, 99.99));
138+
isSame(10.0, Stats::percentile($data, 100));
139+
140+
$data = [72, 57, 66, 92, 32, 17, 146];
141+
isSame(17.0, Stats::percentile($data, 0));
142+
isSame(17.9, Stats::percentile($data, 1));
143+
isSame(26.0, Stats::percentile($data, 10));
144+
isSame(37.0, Stats::percentile($data, 20));
145+
isSame(52.0, Stats::percentile($data, 30));
146+
isSame(60.6, Stats::percentile($data, 40));
147+
isSame(66.0, Stats::percentile($data, 50));
148+
isSame(69.6, Stats::percentile($data, 60));
149+
isSame(76.0, Stats::percentile($data, 70));
150+
isSame(88.0, Stats::percentile($data, 80));
151+
isSame(113.6, Stats::percentile($data, 90));
152+
isSame(129.8, Stats::percentile($data));
153+
isSame(142.76, Stats::percentile($data, 99));
154+
isSame(145.9676, Stats::percentile($data, 99.99));
155+
isSame(146.0, Stats::percentile($data, 100));
156+
157+
isSame(0.0, Stats::percentile([], 0));
158+
isSame(0.0, Stats::percentile([], 90));
159+
isSame(0.0, Stats::percentile([0], 0));
160+
isSame(0.0, Stats::percentile([0], 90));
161+
isSame(1.0, Stats::percentile([1], 90));
162+
163+
isSame(0.0, Stats::percentile(['qwerty'], 50));
164+
isSame(5.5, Stats::percentile(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'], 50));
165+
isSame(5.5, Stats::percentile(['1.0', '2.0', '3.0', '4.0', '5.0', '6.0', '7.0', '8.0', '9.0', '10.0'], 50));
166+
isSame(
167+
5.5,
168+
Stats::percentile([
169+
11 => '1.0',
170+
12 => '2.0',
171+
13 => '3.0',
172+
14 => '4.0',
173+
15 => '5.0',
174+
16 => '6.0',
175+
17 => '7.0',
176+
18 => '8.0',
177+
19 => '9.0',
178+
20 => '10.0',
179+
], 50),
180+
);
181+
}
182+
183+
public function testPercentileWithInvalidPercent1(): void
184+
{
185+
$this->expectException(\JBZoo\Utils\Exception::class);
186+
$this->expectExceptionMessage('Percentile should be between 0 and 100, 146 given');
187+
Stats::percentile([1, 2, 3], 146);
188+
}
189+
190+
public function testPercentileWithInvalidPercent2(): void
191+
{
192+
$this->expectException(\JBZoo\Utils\Exception::class);
193+
$this->expectExceptionMessage('Percentile should be between 0 and 100, -146 given');
194+
Stats::percentile([1, 2, 3], -146);
195+
}
196+
197+
public function testMedian(): void
198+
{
199+
isSame(0.0, Stats::median([]));
200+
isSame(1.0, Stats::median([1]));
201+
isSame(1.5, Stats::median([1, 2]));
202+
isSame(5.5, Stats::median([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
203+
isSame(5.5, Stats::median([1, 1, 1, 1, 5, 6, 7, 8, 9, 10]));
89204
}
90205
}

0 commit comments

Comments
 (0)