Skip to content

Commit

Permalink
Added percentile and median functions (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
SmetDenis committed Aug 8, 2023
1 parent 3f39852 commit 0a9bfee
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 8 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down
53 changes: 52 additions & 1 deletion src/Stats.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public static function mean(?array $values): float

$count = \count($values);

return $sum / $count;
return \round($sum / $count, 9);
}

/**
Expand Down Expand Up @@ -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);
}
}
125 changes: 120 additions & 5 deletions tests/StatsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]));
}
}

0 comments on commit 0a9bfee

Please sign in to comment.