Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions Maths/Mean.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
125 changes: 125 additions & 0 deletions Maths/Median.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
67 changes: 67 additions & 0 deletions tests/Maths/MathsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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));
Expand Down