Skip to content

Commit 38c2f4a

Browse files
committed
Adding kurtosis() method for excess kurtosis
1 parent 29c36f6 commit 38c2f4a

8 files changed

Lines changed: 135 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## 1.2.5 - WIP
4+
- Adding `kurtosis()` method for excess kurtosis
5+
-
36
## 1.2.4 - 2026-02-21
47
- Adding `skewness()` method for adjusted Fisher-Pearson sample skewness
58
- Adding `pskewness()` method for population (biased) skewness

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ The various mathematical statistics are listed below:
7979
| `variance()` | variance for a sample (supports pre-computed mean via `xbar`) |
8080
| `skewness()` | adjusted Fisher-Pearson sample skewness |
8181
| `pskewness()` | population (biased) skewness |
82+
| `kurtosis()` | excess kurtosis (sample formula, 0 for normal distribution) |
8283
| `geometricMean()` | geometric mean |
8384
| `harmonicMean()` | harmonic mean |
8485
| `correlation()` | Pearson’s or Spearman’s rank correlation coefficient for two inputs |
@@ -342,6 +343,22 @@ use HiFolks\Statistics\Stat;
342343
$pskewness = Stat::pskewness([1, 1, 1, 1, 1, 10]);
343344
```
344345

346+
#### Stat::kurtosis ( array $data, ?int $round = null )
347+
Kurtosis measures the "tailedness" of a distribution — how much data lives in the extreme tails compared to a normal distribution. This method returns the **excess kurtosis** using the sample formula, which is the same as Excel's `KURT()` and Python's `scipy.stats.kurtosis(bias=False)`.
348+
349+
A normal distribution has excess kurtosis of 0. Positive values (leptokurtic) indicate heavier tails and more outliers. Negative values (platykurtic) indicate lighter tails and fewer outliers.
350+
351+
Requires at least 4 data points.
352+
353+
```php
354+
use HiFolks\Statistics\Stat;
355+
$kurtosis = Stat::kurtosis([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
356+
// negative (platykurtic, lighter tails than normal)
357+
358+
$kurtosis = Stat::kurtosis([1, 2, 2, 2, 2, 2, 2, 2, 2, 50]);
359+
// positive (leptokurtic, heavier tails due to outlier)
360+
```
361+
345362
#### Stat::covariance ( array $x , array $y )
346363
Covariance, static method, returns the sample covariance of two inputs *$x* and *$y*.
347364
Covariance is a measure of the joint variability of two inputs.

TODO.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
- Trimmed/Truncated mean - mean after removing outliers (top/bottom x%)
66
- Weighted median - median with weights (like fmean supports weights, but median doesn't)
7-
- Skewness - measure of asymmetry of the distribution
8-
- Kurtosis - measure of "tailedness" of the distribution
97
- Standard error of the mean (SEM)
108
- Coefficient of variation (CV) - stdev / mean, useful for comparing variability across datasets
119
- Mean absolute deviation (MAD)
@@ -49,4 +47,4 @@
4947

5048
### Notes
5149

52-
The most impactful additions would likely be skewness, kurtosis, coefficient of variation, percentile, and Spearman correlation — these are commonly needed and align well with the package's existing scope (inspired by Python's statistics module).
50+
The most impactful additions would likely be (skewness DONE), (kurtosis DONE), coefficient of variation, percentile, and Spearman correlation — these are commonly needed and align well with the package's existing scope (inspired by Python's statistics module).

examples/norm_dist.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@
8484
echo "Skewness: " . Stat::skewness($times, 4)
8585
. " (positive = right-skewed, a few slow finishers pull the tail right)"
8686
. PHP_EOL;
87+
echo "Kurtosis: " . Stat::kurtosis($times, 4)
88+
. " (positive = leptokurtic, heavy tails with outliers)"
89+
. PHP_EOL;
8790

8891
// --- Normal Distribution Model ---
8992
echo PHP_EOL . "=== Normal Distribution Model ===" . PHP_EOL . PHP_EOL;
@@ -193,3 +196,9 @@
193196
. ")"
194197
. PHP_EOL;
195198
}
199+
200+
// --- Distribution Shape ---
201+
echo PHP_EOL . "=== Distribution Shape ===" . PHP_EOL . PHP_EOL;
202+
203+
echo "Skewness: " . Stat::skewness($times, 4) . PHP_EOL;
204+
echo "Kurtosis: " . Stat::kurtosis($times, 4) . PHP_EOL;

src/Stat.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,49 @@ public static function pskewness(array $data, ?int $round = null): float
582582
return Math::round($pskewness, $round);
583583
}
584584

585+
/**
586+
* Return the excess kurtosis of the data using the sample formula.
587+
* This is the same formula used by Excel's KURT() and Python's
588+
* scipy.stats.kurtosis(bias=False, fisher=True).
589+
*
590+
* Excess kurtosis measures the "tailedness" of a distribution relative
591+
* to a normal distribution. A normal distribution has excess kurtosis 0.
592+
* Positive values (leptokurtic) indicate heavier tails and more outliers;
593+
* negative values (platykurtic) indicate lighter tails and fewer outliers.
594+
*
595+
* Formula: [n(n+1) / ((n-1)(n-2)(n-3))] * Σ((xi - x̄) / s)⁴ − [3(n-1)² / ((n-2)(n-3))]
596+
*
597+
* @param array<int|float> $data
598+
* @param int|null $round whether to round the result
599+
* @return float excess kurtosis
600+
*
601+
* @throws InvalidDataInputException if the data has fewer than 4 elements or all values are identical
602+
*/
603+
public static function kurtosis(array $data, ?int $round = null): float
604+
{
605+
$n = self::count($data);
606+
if ($n < 4) {
607+
throw new InvalidDataInputException("Kurtosis requires at least 4 data points.");
608+
}
609+
610+
$mean = self::mean($data);
611+
$stdev = self::stdev($data);
612+
613+
if ($stdev == 0) {
614+
throw new InvalidDataInputException("Kurtosis is undefined when all values are identical (standard deviation is zero).");
615+
}
616+
617+
$sumFourth = 0.0;
618+
foreach ($data as $xi) {
619+
$sumFourth += (($xi - $mean) / $stdev) ** 4;
620+
}
621+
622+
$kurtosis = ($n * ($n + 1)) / (($n - 1) * ($n - 2) * ($n - 3)) * $sumFourth
623+
- (3 * ($n - 1) ** 2) / (($n - 2) * ($n - 3));
624+
625+
return Math::round($kurtosis, $round);
626+
}
627+
585628
/**
586629
* Return the geometric mean of the numeric data.
587630
* That is the number that can replace each of these numbers so that their product

src/Statistics.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,18 @@ public function pskewness(?int $round = null): float
295295
return Stat::pskewness($this->numericalArray(), $round);
296296
}
297297

298+
/**
299+
* Return the excess kurtosis (sample formula).
300+
*
301+
* @param int|null $round whether to round the result
302+
*
303+
* @see Stat::kurtosis()
304+
*/
305+
public function kurtosis(?int $round = null): float
306+
{
307+
return Stat::kurtosis($this->numericalArray(), $round);
308+
}
309+
298310
/**
299311
* Return the geometric mean of the numeric data.
300312
*

tests/StatTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,50 @@ public function test_pskewness_less_than_skewness_for_small_samples(): void
334334
$this->assertLessThan(abs($skewness), abs($pskewness));
335335
}
336336

337+
public function test_calculates_kurtosis_normal_like(): void
338+
{
339+
// A uniform-ish symmetric dataset: excess kurtosis near 0 or negative
340+
$this->assertEqualsWithDelta(-1.2, Stat::kurtosis([1, 2, 3, 4, 5]), 0.1);
341+
}
342+
343+
public function test_calculates_kurtosis_heavy_tails(): void
344+
{
345+
// Data with outliers should have positive excess kurtosis (leptokurtic)
346+
$kurtosis = Stat::kurtosis([1, 2, 2, 2, 2, 2, 2, 2, 2, 50]);
347+
$this->assertGreaterThan(0, $kurtosis);
348+
}
349+
350+
public function test_calculates_kurtosis_light_tails(): void
351+
{
352+
// Uniform-like data should have negative excess kurtosis (platykurtic)
353+
$kurtosis = Stat::kurtosis([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
354+
$this->assertLessThan(0, $kurtosis);
355+
}
356+
357+
public function test_calculates_kurtosis_with_rounding(): void
358+
{
359+
$kurtosis = Stat::kurtosis([1, 2, 2, 2, 2, 2, 2, 2, 2, 50], 4);
360+
$this->assertEquals(round($kurtosis, 4), $kurtosis);
361+
}
362+
363+
public function test_kurtosis_with_empty_array(): void
364+
{
365+
$this->expectException(InvalidDataInputException::class);
366+
Stat::kurtosis([]);
367+
}
368+
369+
public function test_kurtosis_with_three_elements(): void
370+
{
371+
$this->expectException(InvalidDataInputException::class);
372+
Stat::kurtosis([1, 2, 3]);
373+
}
374+
375+
public function test_kurtosis_with_identical_values(): void
376+
{
377+
$this->expectException(InvalidDataInputException::class);
378+
Stat::kurtosis([5, 5, 5, 5]);
379+
}
380+
337381
public function test_calculates_geometric_mean(): void
338382
{
339383
$this->assertEquals(36, Stat::geometricMean([54, 24, 36], 2));

tests/StatisticTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ public function test_calculates_pskewness(): void
187187
$this->assertEqualsWithDelta(0.0, Statistics::make([1, 2, 3, 4, 5])->pskewness(), 1e-10);
188188
}
189189

190+
public function test_calculates_kurtosis(): void
191+
{
192+
// Uniform-like data: negative excess kurtosis
193+
$this->assertLessThan(0, Statistics::make([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])->kurtosis());
194+
}
195+
190196
public function test_calculates_geometric_mean(): void
191197
{
192198
$this->assertEquals(36, Statistics::make([54, 24, 36])->geometricMean(2));

0 commit comments

Comments
 (0)