diff --git a/README.md b/README.md index 104305d..228efee 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ Filter::clean(string $string): string; // Alias of "Str::clean($string, true, tr Filter::cmd(string $value): string; // Cleanup system command. -Filter::data(JBZoo\Data\Data|array $data): JBZoo\Data\Data; // Returns JSON object from array. +Filter::data(JBZoo\Data\Data|array $data): JBZoo\Data\Data; // Returns Data object from array. Filter::digits(??string $value): string; // Returns only digits chars. @@ -306,6 +306,8 @@ Filter::html(string $string): string; // Alias of "Str::htmlEnt($string)". Filter::int(?string|int|float|bool|null $value): int; // Smart convert any string to int. +Filter::json(JBZoo\Data\JSON|array $data): JBZoo\Data\JSON; // Returns JSON object from array. + Filter::low(string $string): string; // String to lower and trim. 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 Stats::mean(??array $values): float; // Returns the mean (average) value of the given values. +Stats::median(array $data): ??float; // Calculate the median of a given population. + +Stats::percentile(array $data, int|float $percentile = 95): ??float; // Calculate the percentile of a given population. + Stats::renderAverage(array $values, int $rounding = 3): string; // Render human readable string of average value and system error. 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 Sys::isHHVM(): bool; // Returns true when the runtime used is HHVM. -Sys::isPHP(string $version, string $current = '8.1.16'): bool; // Compares PHP versions. +Sys::isPHP(string $version, string $current = '8.1.22'): bool; // Compares PHP versions. Sys::isPHPDBG(): bool; // Returns true when the runtime used is PHP with the PHPDBG SAPI. diff --git a/src/Stats.php b/src/Stats.php index 2176501..832211b 100644 --- a/src/Stats.php +++ b/src/Stats.php @@ -68,7 +68,7 @@ public static function mean(?array $values): float $count = \count($values); - return $sum / $count; + return \round($sum / $count, 9); } /** @@ -153,4 +153,55 @@ public static function renderAverage(array $values, int $rounding = 3): string return "{$avg}±{$stdDev}"; } + + /** + * Render human readable string of average value and system error. + */ + public static function renderMedian(array $values, int $rounding = 3): string + { + $avg = \number_format(self::median($values), $rounding); + $stdDev = \number_format(self::stdDev($values), $rounding); + + return "{$avg}±{$stdDev}"; + } + + /** + * Calculate the percentile of a given population. + * @param float[]|int[] $data + */ + public static function percentile(array $data, float|int $percentile = 95): float + { + $count = \count($data); + if ($count === 0) { + return 0; + } + + $percent = $percentile / 100; + if ($percent < 0 || $percent > 1) { + throw new Exception("Percentile should be between 0 and 100, {$percentile} given"); + } + + $allIndex = ($count - 1) * $percent; + $intValue = (int)$allIndex; + $floatValue = $allIndex - $intValue; + + \sort($data, \SORT_NUMERIC); + + if ($intValue + 1 < $count) { + $result = $data[$intValue] + ($data[$intValue + 1] - $data[$intValue]) * $floatValue; + } else { + $result = $data[$intValue]; + } + + return \round(float($result), 6); + } + + /** + * Calculate the median of a given population. + * @param float[]|int[] $data + */ + public static function median(array $data): float + { + return self::percentile($data, 50.0); + } } diff --git a/tests/StatsTest.php b/tests/StatsTest.php index 853228d..a795cf9 100644 --- a/tests/StatsTest.php +++ b/tests/StatsTest.php @@ -29,6 +29,11 @@ public function testMean(): void isSame(1.0, Stats::mean([1])); isSame(1.0, Stats::mean([1, 1])); isSame(2.0, Stats::mean([1, 3])); + isSame(2.0, Stats::mean(['1', 3])); + isSame(2.25, Stats::mean(['1.5', 3])); + + $data = [72, 57, 66, 92, 32, 17, 146]; + isSame(68.857142857, Stats::mean($data)); } public function testStdDev(): void @@ -81,10 +86,120 @@ public function testHistogram(): void public function testRenderAverage(): void { - isSame('1.500±0.500', Stats::renderAverage([1, 2, 1, 2])); - isSame('1.5±0.5', Stats::renderAverage([1, 2, 1, 2], 1)); - isSame('1.50±0.50', Stats::renderAverage([1, 2, 1, 2], 2)); - isSame('2±1', Stats::renderAverage([1, 2, 1, 2], 0)); - isSame('2±1', Stats::renderAverage([1, 2, 1, 2], -1)); + $data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + isSame('5.500±2.872', Stats::renderAverage($data)); + isSame('5.5±2.9', Stats::renderAverage($data, 1)); + isSame('5.50±2.87', Stats::renderAverage($data, 2)); + isSame('6±3', Stats::renderAverage($data, 0)); + isSame('6±3', Stats::renderAverage($data, -1)); + + $data = [72, 57, 66, 92, 32, 17, 146]; + isSame('68.857±39.084', Stats::renderAverage($data)); + isSame('68.9±39.1', Stats::renderAverage($data, 1)); + isSame('68.86±39.08', Stats::renderAverage($data, 2)); + isSame('69±39', Stats::renderAverage($data, 0)); + isSame('69±39', Stats::renderAverage($data, -1)); + } + + public function testRenderMedian(): void + { + $data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + isSame('5.500±2.872', Stats::renderMedian($data)); + isSame('5.5±2.9', Stats::renderMedian($data, 1)); + isSame('5.50±2.87', Stats::renderMedian($data, 2)); + isSame('6±3', Stats::renderMedian($data, 0)); + isSame('6±3', Stats::renderMedian($data, -1)); + + $data = [72, 57, 66, 92, 32, 17, 146]; + isSame('66.000±39.084', Stats::renderMedian($data)); + isSame('66.0±39.1', Stats::renderMedian($data, 1)); + isSame('66.00±39.08', Stats::renderMedian($data, 2)); + isSame('66±39', Stats::renderMedian($data, 0)); + isSame('66±39', Stats::renderMedian($data, -1)); + } + + public function testPercentile(): void + { + $data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + isSame(1.0, Stats::percentile($data, 0)); + isSame(1.09, Stats::percentile($data, 1)); + isSame(1.9, Stats::percentile($data, 10)); + isSame(2.8, Stats::percentile($data, 20)); + isSame(3.7, Stats::percentile($data, 30)); + isSame(4.6, Stats::percentile($data, 40)); + isSame(5.5, Stats::percentile($data, 50)); + isSame(6.4, Stats::percentile($data, 60)); + isSame(7.3, Stats::percentile($data, 70)); + isSame(8.2, Stats::percentile($data, 80)); + isSame(9.1, Stats::percentile($data, 90)); + isSame(9.55, Stats::percentile($data)); + isSame(9.91, Stats::percentile($data, 99)); + isSame(9.9991, Stats::percentile($data, 99.99)); + isSame(10.0, Stats::percentile($data, 100)); + + $data = [72, 57, 66, 92, 32, 17, 146]; + isSame(17.0, Stats::percentile($data, 0)); + isSame(17.9, Stats::percentile($data, 1)); + isSame(26.0, Stats::percentile($data, 10)); + isSame(37.0, Stats::percentile($data, 20)); + isSame(52.0, Stats::percentile($data, 30)); + isSame(60.6, Stats::percentile($data, 40)); + isSame(66.0, Stats::percentile($data, 50)); + isSame(69.6, Stats::percentile($data, 60)); + isSame(76.0, Stats::percentile($data, 70)); + isSame(88.0, Stats::percentile($data, 80)); + isSame(113.6, Stats::percentile($data, 90)); + isSame(129.8, Stats::percentile($data)); + isSame(142.76, Stats::percentile($data, 99)); + isSame(145.9676, Stats::percentile($data, 99.99)); + isSame(146.0, Stats::percentile($data, 100)); + + isSame(0.0, Stats::percentile([], 0)); + isSame(0.0, Stats::percentile([], 90)); + isSame(0.0, Stats::percentile([0], 0)); + isSame(0.0, Stats::percentile([0], 90)); + isSame(1.0, Stats::percentile([1], 90)); + + isSame(0.0, Stats::percentile(['qwerty'], 50)); + isSame(5.5, Stats::percentile(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'], 50)); + 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)); + isSame( + 5.5, + Stats::percentile([ + 11 => '1.0', + 12 => '2.0', + 13 => '3.0', + 14 => '4.0', + 15 => '5.0', + 16 => '6.0', + 17 => '7.0', + 18 => '8.0', + 19 => '9.0', + 20 => '10.0', + ], 50), + ); + } + + public function testPercentileWithInvalidPercent1(): void + { + $this->expectException(\JBZoo\Utils\Exception::class); + $this->expectExceptionMessage('Percentile should be between 0 and 100, 146 given'); + Stats::percentile([1, 2, 3], 146); + } + + public function testPercentileWithInvalidPercent2(): void + { + $this->expectException(\JBZoo\Utils\Exception::class); + $this->expectExceptionMessage('Percentile should be between 0 and 100, -146 given'); + Stats::percentile([1, 2, 3], -146); + } + + public function testMedian(): void + { + isSame(0.0, Stats::median([])); + isSame(1.0, Stats::median([1])); + isSame(1.5, Stats::median([1, 2])); + isSame(5.5, Stats::median([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); + isSame(5.5, Stats::median([1, 1, 1, 1, 5, 6, 7, 8, 9, 10])); } }