diff --git a/Maths/Mean.php b/Maths/Mean.php index 2958a9d..c26bb1a 100644 --- a/Maths/Mean.php +++ b/Maths/Mean.php @@ -21,3 +21,68 @@ function mean(...$numbers): int|float return $total / count($numbers); } + +/** + * Calculates the mean of a discrete series. + * + * Formula: Mean = Σ(x * f) / Σf + * Where 'x' is the value and 'f' is its corresponding frequency. + * + * @param array $values The discrete values (x) + * @param array $frequencies The frequencies of the values (f) + * @return int|float Mean of the discrete series + * @throws \Exception + */ +function mean_discrete(array $values, array $frequencies): int|float +{ + if (count($values) !== count($frequencies)) { + throw new \Exception('Values and frequencies arrays must have the same length'); + } + + if (empty($values)) { + throw new \Exception('Please pass values to find mean value'); + } + + $totalFrequency = array_sum($frequencies); + + if ($totalFrequency == 0) { + throw new \Exception('Total frequency cannot be zero'); + } + + $sumProduct = 0; + for ($i = 0; $i < count($values); $i++) { + $sumProduct += $values[$i] * $frequencies[$i]; + } + + return $sumProduct / $totalFrequency; +} + +/** + * Calculates the mean of a continuous series. + * + * Formula: Mean = Σ(m * f) / Σf + * Where 'm' is the midpoint of each class interval, and 'f' is the frequency. + * + * @param array $lower_bounds The lower bounds of the class intervals + * @param array $upper_bounds The upper bounds of the class intervals + * @param array $frequencies The frequencies of the class intervals + * @return int|float Mean of the continuous series + * @throws \Exception + */ +function mean_continuous(array $lower_bounds, array $upper_bounds, array $frequencies): int|float +{ + if (count($lower_bounds) !== count($upper_bounds) || count($lower_bounds) !== count($frequencies)) { + throw new \Exception('Lower bounds, upper bounds, and frequencies arrays must have the same length'); + } + + if (empty($lower_bounds)) { + throw new \Exception('Please pass values to find mean value'); + } + + $midpoints = []; + for ($i = 0; $i < count($lower_bounds); $i++) { + $midpoints[] = ($lower_bounds[$i] + $upper_bounds[$i]) / 2; + } + + return mean_discrete($midpoints, $frequencies); +} diff --git a/Maths/Median.php b/Maths/Median.php index 8ef8625..331ac0f 100644 --- a/Maths/Median.php +++ b/Maths/Median.php @@ -25,10 +25,135 @@ function median(...$numbers): float|int sort($numbers); $length = count($numbers); + + // Using intdiv to find the middle index corresponds to the mathematical formula m = (n+1)/2. + // Since arrays are 0-indexed, intdiv($length, 2) effectively gives the middle element for odd counts, + // and the upper middle element for even counts. $middle = intdiv($length, 2); + if ($length % 2 == 0) { return ($numbers[$middle] + $numbers[$middle - 1]) / 2; } return $numbers[$middle] + 0; } + +/** + * Calculates the median of a discrete series. + * + * Formula: The value corresponding to the Cumulative Frequency (CF) + * that is just greater than or equal to N / 2, where N = Σf. + * + * @param array $values The discrete values (x) + * @param array $frequencies The frequencies of the values (f) + * @return int|float Median of the discrete series + * @throws \Exception + */ +function median_discrete(array $values, array $frequencies): int|float +{ + if (count($values) !== count($frequencies)) { + throw new \Exception('Values and frequencies arrays must have the same length'); + } + if (empty($values)) { + throw new \Exception('Please pass values to find median value'); + } + + $data = []; + for ($i = 0; $i < count($values); $i++) { + $data[] = ['value' => $values[$i], 'freq' => $frequencies[$i]]; + } + usort($data, fn($a, $b) => $a['value'] <=> $b['value']); + + $totalFrequency = array_sum($frequencies); + if ($totalFrequency == 0) { + throw new \Exception('Total frequency cannot be zero'); + } + + $cumulativeFrequency = 0; + $targetCF = $totalFrequency / 2; + $isEven = $totalFrequency % 2 === 0; + $prevValue = null; + + foreach ($data as $item) { + $cumulativeFrequency += $item['freq']; + + if ($isEven) { + if ($cumulativeFrequency > $targetCF) { + if ($cumulativeFrequency - $item['freq'] == $targetCF) { + return ($prevValue + $item['value']) / 2; + } + return $item['value']; + } + if ($cumulativeFrequency == $targetCF) { + $prevValue = $item['value']; + } + } else { + if ($cumulativeFrequency >= $targetCF) { + return $item['value']; + } + } + } + + return $data[count($data) - 1]['value']; +} + +/** + * Calculates the median of a continuous series. + * + * Formula: Median = L + (((N/2) - CF_prev) / f) * h + * Where: + * L = Lower bound of the median class + * N = Total frequency (Σf) + * CF_prev = Cumulative frequency of the class before the median class + * f = Frequency of the median class + * h = Class width (Upper bound - Lower bound) + * + * @param array $lower_bounds The lower bounds of the class intervals + * @param array $upper_bounds The upper bounds of the class intervals + * @param array $frequencies The frequencies of the class intervals + * @return int|float Median of the continuous series + * @throws \Exception + */ +function median_continuous(array $lower_bounds, array $upper_bounds, array $frequencies): int|float +{ + if (count($lower_bounds) !== count($upper_bounds) || count($lower_bounds) !== count($frequencies)) { + throw new \Exception('Lower bounds, upper bounds, and frequencies arrays must have the same length'); + } + if (empty($lower_bounds)) { + throw new \Exception('Please pass values to find median value'); + } + + $data = []; + for ($i = 0; $i < count($lower_bounds); $i++) { + $data[] = [ + 'L' => $lower_bounds[$i], + 'U' => $upper_bounds[$i], + 'f' => $frequencies[$i] + ]; + } + usort($data, fn($a, $b) => $a['L'] <=> $b['L']); + + $totalFrequency = array_sum($frequencies); + if ($totalFrequency == 0) { + throw new \Exception('Total frequency cannot be zero'); + } + + $targetCF = $totalFrequency / 2; + $cumulativeFrequency = 0; + $prevCF = 0; + + foreach ($data as $item) { + $cumulativeFrequency += $item['f']; + if ($cumulativeFrequency >= $targetCF) { + $L = $item['L']; + $f = $item['f']; + $h = $item['U'] - $item['L']; + + if ($f == 0) continue; + return $L + (($targetCF - $prevCF) / $f) * $h; + } + $prevCF = $cumulativeFrequency; + } + + return 0; +} diff --git a/tests/Maths/MathsTest.php b/tests/Maths/MathsTest.php index 1edfa22..cb6c64f 100644 --- a/tests/Maths/MathsTest.php +++ b/tests/Maths/MathsTest.php @@ -161,6 +161,44 @@ public function testMean() $this->assertEquals(-1, mean(-1)); } + public function testMeanDiscrete() + { + $this->assertEquals( + ((10 * 2) + (20 * 3) + (30 * 5)) / (2 + 3 + 5), + mean_discrete([10, 20, 30], [2, 3, 5]) + ); + + $this->assertEquals( + ((5 * 10) + (15 * 20)) / (10 + 20), + mean_discrete([5, 15], [10, 20]) + ); + } + + public function testMeanDiscreteExceptionMismatchedArrays() + { + $this->expectException(\Exception::class); + mean_discrete([1, 2], [1]); + } + + public function testMeanContinuous() + { + $this->assertEquals( + ((5 * 2) + (15 * 3) + (25 * 5)) / (2 + 3 + 5), + mean_continuous([0, 10, 20], [10, 20, 30], [2, 3, 5]) + ); + + $this->assertEquals( + ((2.5 * 10) + (7.5 * 20)) / (10 + 20), + mean_continuous([0, 5], [5, 10], [10, 20]) + ); + } + + public function testMeanContinuousExceptionMismatchedArrays() + { + $this->expectException(\Exception::class); + mean_continuous([0, 10], [10, 20], [1]); + } + public function testMedian() { $this->assertEquals(3, median(1, 2, 3, 4, 5)); @@ -175,6 +213,35 @@ public function testMedianRejectsNonNumericValues() median('a', 'b', 'c'); } + public function testMedianDiscrete() + { + // Odd total frequency (N=5) + // Values: 10(2), 20(3). CF: 2, 5. N/2 = 2.5 + // Median is the value where CF >= 2.5, which is 20. + $this->assertEquals(20, median_discrete([10, 20], [2, 3])); + + // Even total frequency (N=4) + // Values: 10(2), 20(2). CF: 2, 4. Target = 2. + // First value CF = 2 (exactly matches target). So average of 10 and 20 = 15. + $this->assertEquals(15, median_discrete([10, 20], [2, 2])); + + // Even total frequency (N=6) + // Values: 10(1), 20(4), 30(1). CF: 1, 5, 6. Target = 3. + // First value > 3 is 20 (CF=5), previous CF=1 (not 3). Median = 20. + $this->assertEquals(20, median_discrete([10, 20, 30], [1, 4, 1])); + } + + public function testMedianContinuous() + { + // N = 10, Target = 5 + // Intervals: 0-10 (f=2, CF=2) + // 10-20 (f=5, CF=7) -> Median Class + // 20-30 (f=3, CF=10) + // Formula: L = 10, f = 5, h = 10, prevCF = 2 + // Median = 10 + ((5 - 2) / 5) * 10 = 10 + (3/5)*10 = 10 + 6 = 16 + $this->assertEquals(16, median_continuous([0, 10, 20], [10, 20, 30], [2, 5, 3])); + } + public function testMode() { $this->assertEquals([3], mode(1, 2, 3, 3, 4, 5));