Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
38 / 38 |
|
100.00% |
12 / 12 |
CRAP | |
100.00% |
1 / 1 |
| ChiSquaredDistribution | |
100.00% |
38 / 38 |
|
100.00% |
12 / 12 |
22 | |
100.00% |
1 / 1 |
| testHypothesis | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
8 | |||
| getDegreesOfFreedom | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| getPdf | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| getCdf | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getMean | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getMedian | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getVariance | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getStandardDeviation | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getMgf | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| getSkewness | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getExKurtosis | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Jingga |
| 4 | * |
| 5 | * PHP Version 8.1 |
| 6 | * |
| 7 | * @package phpOMS\Math\Stochastic\Distribution |
| 8 | * @copyright Dennis Eichhorn |
| 9 | * @license OMS License 2.0 |
| 10 | * @version 1.0.0 |
| 11 | * @link https://jingga.app |
| 12 | */ |
| 13 | declare(strict_types=1); |
| 14 | |
| 15 | namespace phpOMS\Math\Stochastic\Distribution; |
| 16 | |
| 17 | use phpOMS\Math\Functions\Gamma; |
| 18 | |
| 19 | /** |
| 20 | * Chi squared distribution. |
| 21 | * |
| 22 | * Test if two variables are similarly distributed (goodness fit, test dependency of variables) |
| 23 | * |
| 24 | * @package phpOMS\Math\Stochastic\Distribution |
| 25 | * @license OMS License 2.0 |
| 26 | * @link https://jingga.app |
| 27 | * @since 1.0.0 |
| 28 | */ |
| 29 | final class ChiSquaredDistribution |
| 30 | { |
| 31 | /** |
| 32 | * Chi square table. |
| 33 | * |
| 34 | * @var array<int, array<string, float>> |
| 35 | * @since 1.0.0 |
| 36 | */ |
| 37 | public const TABLE = [ |
| 38 | 1 => ['0.995' => 0.000, '0.99' => 0.000, '0.975' => 0.001, '0.95' => 0.004, '0.90' => 0.016, '0.10' => 2.706, '0.05' => 3.841, '0.025' => 5.024, '0.01' => 6.635, '0.005' => 7.879], |
| 39 | 2 => ['0.995' => 0.010, '0.99' => 0.020, '0.975' => 0.051, '0.95' => 0.103, '0.90' => 0.211, '0.10' => 4.605, '0.05' => 5.991, '0.025' => 7.378, '0.01' => 9.210, '0.005' => 10.597], |
| 40 | 3 => ['0.995' => 0.072, '0.99' => 0.115, '0.975' => 0.216, '0.95' => 0.352, '0.90' => 0.584, '0.10' => 6.251, '0.05' => 7.815, '0.025' => 9.348, '0.01' => 11.345, '0.005' => 12.838], |
| 41 | 4 => ['0.995' => 0.207, '0.99' => 0.297, '0.975' => 0.484, '0.95' => 0.711, '0.90' => 1.064, '0.10' => 7.779, '0.05' => 9.488, '0.025' => 11.143, '0.01' => 13.277, '0.005' => 14.860], |
| 42 | 5 => ['0.995' => 0.412, '0.99' => 0.554, '0.975' => 0.831, '0.95' => 1.145, '0.90' => 1.610, '0.10' => 9.236, '0.05' => 11.070, '0.025' => 12.833, '0.01' => 15.086, '0.005' => 16.750], |
| 43 | 6 => ['0.995' => 0.676, '0.99' => 0.872, '0.975' => 1.237, '0.95' => 1.635, '0.90' => 2.204, '0.10' => 10.645, '0.05' => 12.592, '0.025' => 14.449, '0.01' => 16.812, '0.005' => 18.548], |
| 44 | 7 => ['0.995' => 0.989, '0.99' => 1.239, '0.975' => 1.690, '0.95' => 2.167, '0.90' => 2.833, '0.10' => 12.017, '0.05' => 14.067, '0.025' => 16.013, '0.01' => 18.475, '0.005' => 20.278], |
| 45 | 8 => ['0.995' => 1.344, '0.99' => 1.646, '0.975' => 2.180, '0.95' => 2.733, '0.90' => 3.490, '0.10' => 13.362, '0.05' => 15.507, '0.025' => 17.535, '0.01' => 20.090, '0.005' => 21.955], |
| 46 | 9 => ['0.995' => 1.735, '0.99' => 2.088, '0.975' => 2.700, '0.95' => 3.325, '0.90' => 4.168, '0.10' => 14.684, '0.05' => 16.919, '0.025' => 19.023, '0.01' => 21.666, '0.005' => 23.589], |
| 47 | 10 => ['0.995' => 2.156, '0.99' => 2.558, '0.975' => 3.247, '0.95' => 3.940, '0.90' => 4.865, '0.10' => 15.987, '0.05' => 18.307, '0.025' => 20.483, '0.01' => 23.209, '0.005' => 25.188], |
| 48 | 11 => ['0.995' => 2.603, '0.99' => 3.053, '0.975' => 3.816, '0.95' => 4.575, '0.90' => 5.578, '0.10' => 17.275, '0.05' => 19.675, '0.025' => 21.920, '0.01' => 24.725, '0.005' => 26.757], |
| 49 | 12 => ['0.995' => 3.074, '0.99' => 3.571, '0.975' => 4.404, '0.95' => 5.226, '0.90' => 6.304, '0.10' => 18.549, '0.05' => 21.026, '0.025' => 23.337, '0.01' => 26.217, '0.005' => 28.300], |
| 50 | 13 => ['0.995' => 3.565, '0.99' => 4.107, '0.975' => 5.009, '0.95' => 5.892, '0.90' => 7.042, '0.10' => 19.812, '0.05' => 22.362, '0.025' => 24.736, '0.01' => 27.688, '0.005' => 29.819], |
| 51 | 14 => ['0.995' => 4.075, '0.99' => 4.660, '0.975' => 5.629, '0.95' => 6.571, '0.90' => 7.790, '0.10' => 21.064, '0.05' => 23.685, '0.025' => 26.119, '0.01' => 29.141, '0.005' => 31.319], |
| 52 | 15 => ['0.995' => 4.601, '0.99' => 5.229, '0.975' => 6.262, '0.95' => 7.261, '0.90' => 8.547, '0.10' => 22.307, '0.05' => 24.996, '0.025' => 27.488, '0.01' => 30.578, '0.005' => 32.801], |
| 53 | 16 => ['0.995' => 5.142, '0.99' => 5.812, '0.975' => 6.908, '0.95' => 7.962, '0.90' => 9.312, '0.10' => 23.542, '0.05' => 26.296, '0.025' => 28.845, '0.01' => 32.000, '0.005' => 34.267], |
| 54 | 17 => ['0.995' => 5.697, '0.99' => 6.408, '0.975' => 7.564, '0.95' => 8.672, '0.90' => 10.085, '0.10' => 24.769, '0.05' => 27.587, '0.025' => 30.191, '0.01' => 33.409, '0.005' => 35.718], |
| 55 | 18 => ['0.995' => 6.265, '0.99' => 7.015, '0.975' => 8.231, '0.95' => 9.390, '0.90' => 10.865, '0.10' => 25.989, '0.05' => 28.869, '0.025' => 31.526, '0.01' => 34.805, '0.005' => 37.156], |
| 56 | 19 => ['0.995' => 6.844, '0.99' => 7.633, '0.975' => 8.907, '0.95' => 10.117, '0.90' => 11.651, '0.10' => 27.204, '0.05' => 30.144, '0.025' => 32.852, '0.01' => 36.191, '0.005' => 38.582], |
| 57 | 20 => ['0.995' => 7.434, '0.99' => 8.260, '0.975' => 9.591, '0.95' => 10.851, '0.90' => 12.443, '0.10' => 28.412, '0.05' => 31.410, '0.025' => 34.170, '0.01' => 37.566, '0.005' => 39.997], |
| 58 | 21 => ['0.995' => 8.034, '0.99' => 8.897, '0.975' => 10.283, '0.95' => 11.591, '0.90' => 13.240, '0.10' => 29.615, '0.05' => 32.671, '0.025' => 35.479, '0.01' => 38.932, '0.005' => 41.401], |
| 59 | 22 => ['0.995' => 8.643, '0.99' => 9.542, '0.975' => 10.982, '0.95' => 12.338, '0.90' => 14.041, '0.10' => 30.813, '0.05' => 33.924, '0.025' => 36.781, '0.01' => 40.289, '0.005' => 42.796], |
| 60 | 23 => ['0.995' => 9.260, '0.99' => 10.196, '0.975' => 11.689, '0.95' => 13.091, '0.90' => 14.848, '0.10' => 32.007, '0.05' => 35.172, '0.025' => 38.076, '0.01' => 41.638, '0.005' => 44.181], |
| 61 | 24 => ['0.995' => 9.886, '0.99' => 10.856, '0.975' => 12.401, '0.95' => 13.848, '0.90' => 15.659, '0.10' => 33.196, '0.05' => 36.415, '0.025' => 39.364, '0.01' => 42.980, '0.005' => 45.559], |
| 62 | 25 => ['0.995' => 10.520, '0.99' => 11.524, '0.975' => 13.120, '0.95' => 14.611, '0.90' => 16.473, '0.10' => 34.382, '0.05' => 37.652, '0.025' => 40.646, '0.01' => 44.314, '0.005' => 46.928], |
| 63 | 26 => ['0.995' => 11.160, '0.99' => 12.198, '0.975' => 13.844, '0.95' => 15.379, '0.90' => 17.292, '0.10' => 35.563, '0.05' => 38.885, '0.025' => 41.923, '0.01' => 45.642, '0.005' => 48.290], |
| 64 | 27 => ['0.995' => 11.808, '0.99' => 12.879, '0.975' => 14.573, '0.95' => 16.151, '0.90' => 18.114, '0.10' => 36.741, '0.05' => 40.113, '0.025' => 43.195, '0.01' => 46.963, '0.005' => 49.645], |
| 65 | 28 => ['0.995' => 12.461, '0.99' => 13.565, '0.975' => 15.308, '0.95' => 16.928, '0.90' => 18.939, '0.10' => 37.916, '0.05' => 41.337, '0.025' => 44.461, '0.01' => 48.278, '0.005' => 50.993], |
| 66 | 29 => ['0.995' => 13.121, '0.99' => 14.256, '0.975' => 16.047, '0.95' => 17.708, '0.90' => 19.768, '0.10' => 39.087, '0.05' => 42.557, '0.025' => 45.722, '0.01' => 49.588, '0.005' => 52.336], |
| 67 | 30 => ['0.995' => 13.787, '0.99' => 14.953, '0.975' => 16.791, '0.95' => 18.493, '0.90' => 20.599, '0.10' => 40.256, '0.05' => 43.773, '0.025' => 46.979, '0.01' => 50.892, '0.005' => 53.672], |
| 68 | 40 => ['0.995' => 20.707, '0.99' => 22.164, '0.975' => 24.433, '0.95' => 26.509, '0.90' => 29.051, '0.10' => 51.805, '0.05' => 55.758, '0.025' => 59.342, '0.01' => 63.691, '0.005' => 66.766], |
| 69 | 50 => ['0.995' => 27.991, '0.99' => 29.707, '0.975' => 32.357, '0.95' => 34.764, '0.90' => 37.689, '0.10' => 63.167, '0.05' => 67.505, '0.025' => 71.420, '0.01' => 76.154, '0.005' => 79.490], |
| 70 | 60 => ['0.995' => 35.534, '0.99' => 37.485, '0.975' => 40.482, '0.95' => 43.188, '0.90' => 46.459, '0.10' => 74.397, '0.05' => 79.082, '0.025' => 83.298, '0.01' => 88.379, '0.005' => 91.952], |
| 71 | 70 => ['0.995' => 43.275, '0.99' => 45.442, '0.975' => 48.758, '0.95' => 51.739, '0.90' => 55.329, '0.10' => 85.527, '0.05' => 90.531, '0.025' => 95.023, '0.01' => 100.425, '0.005' => 104.215], |
| 72 | 80 => ['0.995' => 51.172, '0.99' => 53.540, '0.975' => 57.153, '0.95' => 60.391, '0.90' => 64.278, '0.10' => 96.578, '0.05' => 101.879, '0.025' => 106.629, '0.01' => 112.329, '0.005' => 116.321], |
| 73 | 90 => ['0.995' => 59.196, '0.99' => 61.754, '0.975' => 65.647, '0.95' => 69.126, '0.90' => 73.291, '0.10' => 107.565, '0.05' => 113.145, '0.025' => 118.136, '0.01' => 124.116, '0.005' => 128.299], |
| 74 | 100 => ['0.995' => 67.328, '0.99' => 70.065, '0.975' => 74.222, '0.95' => 77.929, '0.90' => 82.358, '0.10' => 118.498, '0.05' => 124.342, '0.025' => 129.561, '0.01' => 135.807, '0.005' => 140.169], |
| 75 | ]; |
| 76 | |
| 77 | /** |
| 78 | * Test hypthesis. |
| 79 | * |
| 80 | * Goodness of fit test. |
| 81 | * |
| 82 | * @param array $dataset Values |
| 83 | * @param array $expected Expected values based on probability |
| 84 | * @param float $significance Significance |
| 85 | * @param int $df Degrees of freedom (optional) |
| 86 | * |
| 87 | * @return array |
| 88 | * |
| 89 | * @throws \Exception |
| 90 | * |
| 91 | * @since 1.0.0 |
| 92 | */ |
| 93 | public static function testHypothesis(array $dataset, array $expected, float $significance = 0.05, int $df = 0) : array |
| 94 | { |
| 95 | if (($count = \count($dataset)) !== \count($expected)) { |
| 96 | throw new \Exception('Dimension'); |
| 97 | } |
| 98 | |
| 99 | $sum = 0.0; |
| 100 | |
| 101 | for ($i = 0; $i < $count; ++$i) { |
| 102 | $sum += ($dataset[$i] - $expected[$i]) ** 2 / $expected[$i]; |
| 103 | } |
| 104 | |
| 105 | $p = null; |
| 106 | |
| 107 | if ($df === 0) { |
| 108 | $df = self::getDegreesOfFreedom($dataset); |
| 109 | } |
| 110 | |
| 111 | if (!\defined('self::TABLE') || !\array_key_exists($df, self::TABLE)) { |
| 112 | throw new \Exception('Degrees of freedom not supported'); |
| 113 | } |
| 114 | |
| 115 | foreach (self::TABLE[$df] as $key => $value) { |
| 116 | if ($value > $sum) { |
| 117 | $p = $key; |
| 118 | break; |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | $p = $p ?? 0; |
| 123 | |
| 124 | return [ |
| 125 | 'Chi2' => $sum, |
| 126 | 'P' => $p, |
| 127 | 'H0' => ($p > $significance), |
| 128 | 'df' => $df, |
| 129 | ]; |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Get degrees of freedom of array. |
| 134 | * |
| 135 | * @param array $values Value matrix or vector (N or NxM) |
| 136 | * |
| 137 | * @return int |
| 138 | * |
| 139 | * @since 1.0.0 |
| 140 | */ |
| 141 | public static function getDegreesOfFreedom(array $values) : int |
| 142 | { |
| 143 | if (\is_array($first = \reset($values))) { |
| 144 | return (\count($values) - 1) * (\count($first) - 1); |
| 145 | } else { |
| 146 | return \count($values) - 1; |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | /** |
| 151 | * Get probability density function. |
| 152 | * |
| 153 | * @param float $x Value x |
| 154 | * @param int $df Degreegs of freedom |
| 155 | * |
| 156 | * @return float |
| 157 | * |
| 158 | * @throws \OutOfBoundsException |
| 159 | * |
| 160 | * @since 1.0.0 |
| 161 | */ |
| 162 | public static function getPdf(float $x, int $df) : float |
| 163 | { |
| 164 | if ($x < 0) { |
| 165 | throw new \OutOfBoundsException('Out of bounds'); |
| 166 | } |
| 167 | |
| 168 | return 1 / (\pow(2, $df / 2) * Gamma::gamma($df / 2)) * \pow($x, $df / 2 - 1) * \exp(-$x / 2); |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Get cumulative density function. |
| 173 | * |
| 174 | * @param float $x Value x |
| 175 | * @param int $df Degreegs of freedom |
| 176 | * |
| 177 | * @return float |
| 178 | * |
| 179 | * @since 1.0.0 |
| 180 | */ |
| 181 | public static function getCdf(float $x, int $df) : float |
| 182 | { |
| 183 | return 1 / Gamma::gamma($df / 2) * Gamma::incompleteGammaFirst($df / 2, $x / 2); |
| 184 | } |
| 185 | |
| 186 | /** |
| 187 | * Get mode. |
| 188 | * |
| 189 | * @param int $df Degrees of freedom |
| 190 | * |
| 191 | * @return int |
| 192 | * |
| 193 | * @since 1.0.0 |
| 194 | */ |
| 195 | public static function getMode(int $df) : int |
| 196 | { |
| 197 | return \max($df - 2, 0); |
| 198 | } |
| 199 | |
| 200 | /** |
| 201 | * Get expected value. |
| 202 | * |
| 203 | * @param int $df Degrees of freedom |
| 204 | * |
| 205 | * @return float |
| 206 | * |
| 207 | * @since 1.0.0 |
| 208 | */ |
| 209 | public static function getMean(int $df) : float |
| 210 | { |
| 211 | return $df; |
| 212 | } |
| 213 | |
| 214 | /** |
| 215 | * Get median. |
| 216 | * |
| 217 | * @param int $df Degrees of freedom |
| 218 | * |
| 219 | * @return float |
| 220 | * |
| 221 | * @since 1.0.0 |
| 222 | */ |
| 223 | public static function getMedian(int $df) : float |
| 224 | { |
| 225 | return $df * (1 - 2 / (9 * $df)) ** 3; |
| 226 | } |
| 227 | |
| 228 | /** |
| 229 | * Get variance. |
| 230 | * |
| 231 | * @param int $df Degrees of freedom |
| 232 | * |
| 233 | * @return float |
| 234 | * |
| 235 | * @since 1.0.0 |
| 236 | */ |
| 237 | public static function getVariance(int $df) : float |
| 238 | { |
| 239 | return 2 * $df; |
| 240 | } |
| 241 | |
| 242 | /** |
| 243 | * Get standard deviation. |
| 244 | * |
| 245 | * @param int $df Degrees of freedom |
| 246 | * |
| 247 | * @return float |
| 248 | * |
| 249 | * @since 1.0.0 |
| 250 | */ |
| 251 | public static function getStandardDeviation(int $df) : float |
| 252 | { |
| 253 | return \sqrt(self::getVariance($df)); |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * Get moment generating function. |
| 258 | * |
| 259 | * @param int $df Degrees of freedom |
| 260 | * @param float $t Value t |
| 261 | * |
| 262 | * @return float |
| 263 | * |
| 264 | * @throws \OutOfBoundsException |
| 265 | * |
| 266 | * @since 1.0.0 |
| 267 | */ |
| 268 | public static function getMgf(int $df, float $t) : float |
| 269 | { |
| 270 | if ($t > 0.5) { |
| 271 | throw new \OutOfBoundsException('Out of bounds'); |
| 272 | } |
| 273 | |
| 274 | return \pow(1 - 2 * $t, -$df / 2); |
| 275 | } |
| 276 | |
| 277 | /** |
| 278 | * Get skewness. |
| 279 | * |
| 280 | * @param int $df Degrees of freedom |
| 281 | * |
| 282 | * @return float |
| 283 | * |
| 284 | * @since 1.0.0 |
| 285 | */ |
| 286 | public static function getSkewness(int $df) : float |
| 287 | { |
| 288 | return \sqrt(8 / $df); |
| 289 | } |
| 290 | |
| 291 | /** |
| 292 | * Get Ex. kurtosis. |
| 293 | * |
| 294 | * @param int $df Degrees of freedom |
| 295 | * |
| 296 | * @return float |
| 297 | * |
| 298 | * @since 1.0.0 |
| 299 | */ |
| 300 | public static function getExKurtosis(int $df) : float |
| 301 | { |
| 302 | return 12 / $df; |
| 303 | } |
| 304 | } |