RandLib 2.0.0
Probability distributions, fitting, and heavy-tail tools for C++20
Loading...
Searching...
No Matches
gof.hpp
Go to the documentation of this file.
1#pragma once
2
6
7#include <algorithm>
8#include <cmath>
9#include <concepts>
10#include <limits>
11#include <map>
12#include <numbers>
13#include <ranges>
14#include <span>
15#include <stdexcept>
16#include <type_traits>
17#include <vector>
18
19namespace randlib
20{
21
31
33{
34 double chi_squared = 0;
35 double p_value = 1;
37 std::size_t bins = 0;
38};
39
40namespace detail
41{
42
43inline double andersonDarlingCdf(double statistic)
44{
45 if (!(statistic > 0))
46 return 0;
47 if (statistic < 2) {
48 const double polynomial =
49 2.00012 +
50 statistic *
51 (0.247105 -
52 statistic * (0.0649821 - statistic * (0.0347962 - statistic * (0.011672 - 0.00168691 * statistic))));
53 return std::clamp(std::exp(-1.2337141 / statistic) * polynomial / std::sqrt(statistic), 0.0, 1.0);
54 }
55 const double exponent =
56 1.0776 -
57 statistic *
58 (2.30695 - statistic * (0.43424 - statistic * (0.082433 - statistic * (0.008056 - 0.0003146 * statistic))));
59 return std::clamp(std::exp(-std::exp(exponent)), 0.0, 1.0);
60}
61
62inline double cvmCdfInfinite(double statistic)
63{
64 if (!(statistic > 0))
65 return 0;
66 if (statistic >= 4)
67 return 1;
68 double sum = 0;
69 for (int k = 0; k < 100; ++k) {
70 const double y = 4 * k + 1;
71 const double q = y * y / (16 * statistic);
72 const double gammaRatio = std::exp(std::lgamma(k + 0.5) - std::lgamma(k + 1.0));
73 const double term = gammaRatio * std::sqrt(y) * std::exp(-q) * math::besselK(0.25, q) /
74 (std::pow(std::numbers::pi, 1.5) * std::sqrt(statistic));
75 sum += term;
76 if (std::fabs(term) < 1e-12)
77 break;
78 }
79 return std::clamp(sum, 0.0, 1.0);
80}
81
82inline double cvmEd2(double value)
83{
84 const double z = value * value / 4;
85 return std::exp(-z) * std::pow(value / 2, 1.5) * (math::besselK(0.25, z) + math::besselK(0.75, z)) /
86 std::sqrt(std::numbers::pi);
87}
88
89inline double cvmEd3(double value)
90{
91 const double z = value * value / 4;
92 const double bessel = 2 * math::besselK(0.25, z) + 3 * math::besselK(0.75, z) - math::besselK(1.25, z);
93 return std::exp(-z) * std::pow(value / 2, 2.5) * bessel / std::sqrt(std::numbers::pi);
94}
95
96inline double cvmCorrectionTerm(int k, double statistic)
97{
98 const double m = 2 * k + 1;
99 const double sx = 2 * std::sqrt(statistic);
100 const double y1 = std::pow(statistic, 0.75);
101 const double y2 = std::pow(statistic, 1.25);
102 const double gamma1 = std::tgamma(k + 0.5);
103 const double gamma2 = std::tgamma(k + 1.5);
104 const double e1 = m * gamma1 * cvmEd2((4 * k + 3) / sx) / (9 * y1);
105 const double e2 = gamma1 * cvmEd3((4 * k + 1) / sx) / (72 * y2);
106 const double e3 = 2 * (m + 2) * gamma2 * cvmEd3((4 * k + 5) / sx) / (12 * y2);
107 const double e4 = 7 * m * gamma1 * cvmEd2((4 * k + 1) / sx) / (144 * y1);
108 const double e5 = 7 * m * gamma1 * cvmEd2((4 * k + 5) / sx) / (144 * y1);
109 return e1 + e2 + e3 + e4 + e5;
110}
111
112inline double cvmCdf(double statistic, std::size_t sampleSize)
113{
114 const double minimum = 1.0 / (12 * sampleSize);
115 const double maximum = static_cast<double>(sampleSize) / 3;
116 if (statistic <= minimum)
117 return 0;
118 if (statistic >= maximum)
119 return 1;
120 const double infinite = cvmCdfInfinite(statistic);
121 double correction = 0;
122 for (int k = 0; k < 100; ++k) {
123 const double term = -cvmCorrectionTerm(k, statistic) / (std::numbers::pi * std::tgamma(k + 1.0));
124 correction += term;
125 if (std::fabs(term) < 1e-10)
126 break;
127 }
128 return std::clamp(infinite * (1 + 1.0 / (12 * sampleSize)) + correction / sampleSize, 0.0, 1.0);
129}
130
132{
133 double observed;
134 double expected;
135};
136
137inline void mergeSparseBins(std::vector<ChiSquaredBin> &bins)
138{
139 while (bins.size() > 2) {
140 const auto sparse = std::ranges::min_element(bins, {}, &ChiSquaredBin::expected);
141 if (sparse->expected >= 5)
142 return;
143 const std::size_t index = static_cast<std::size_t>(sparse - bins.begin());
144 const std::size_t target = index == 0 ? 1 : index - 1;
145 bins[target].observed += sparse->observed;
146 bins[target].expected += sparse->expected;
147 bins.erase(sparse);
148 }
149}
150
151} // namespace detail
152
153template <class Distribution>
154 requires std::floating_point<typename Distribution::value_type>
155ContinuousGoodnessOfFit goodness_of_fit(const Distribution &distribution,
156 std::span<const typename Distribution::value_type> sample)
157{
158 if (sample.empty())
159 throw std::invalid_argument("Goodness-of-fit testing requires a non-empty sample");
160 std::vector<typename Distribution::value_type> sorted(sample.begin(), sample.end());
161 std::ranges::sort(sorted);
162 const double n = static_cast<double>(sorted.size());
163 double kolmogorovSmirnov = 0;
164 double andersonDarlingSum = 0;
165 double cramerVonMises = 1 / (12 * n);
166 const double minimumProbability = std::numeric_limits<double>::min();
167 for (std::size_t i = 0; i < sorted.size(); ++i) {
168 const double rawProbability = distribution.F(sorted[i]);
169 const double rawReverseProbability = distribution.F(sorted[sorted.size() - i - 1]);
170 if (!std::isfinite(rawProbability) || !std::isfinite(rawReverseProbability) || rawProbability < 0 ||
171 rawProbability > 1 || rawReverseProbability < 0 || rawReverseProbability > 1)
172 throw std::runtime_error("The fitted distribution returned an invalid probability");
173 const double probability = std::clamp(rawProbability, minimumProbability, 1.0);
174 const double reverseProbability =
175 std::clamp(rawReverseProbability, 0.0, 1 - std::numeric_limits<double>::epsilon());
176 kolmogorovSmirnov = std::max(kolmogorovSmirnov, (i + 1) / n - probability);
177 kolmogorovSmirnov = std::max(kolmogorovSmirnov, probability - i / n);
178 andersonDarlingSum += (2 * i + 1) * (std::log(probability) + std::log1p(-reverseProbability));
179 const double expected = (2 * i + 1) / (2 * n);
180 cramerVonMises += (probability - expected) * (probability - expected);
181 }
182
184 result.kolmogorov_smirnov = kolmogorovSmirnov;
185 result.kolmogorov_smirnov_p_value = KolmogorovSmirnovRand<double>().S(std::sqrt(n) * kolmogorovSmirnov);
186 result.anderson_darling = -n - andersonDarlingSum / n;
188 result.cramer_von_mises = cramerVonMises;
189 result.cramer_von_mises_p_value = 1 - detail::cvmCdf(cramerVonMises, sorted.size());
190 return result;
191}
192
193template <class Distribution>
194 requires std::integral<typename Distribution::value_type>
195DiscreteGoodnessOfFit goodness_of_fit(const Distribution &distribution,
196 std::span<const typename Distribution::value_type> sample,
197 int estimatedParameters = 0)
198{
199 if (sample.empty())
200 throw std::invalid_argument("Goodness-of-fit testing requires a non-empty sample");
201 if (estimatedParameters < 0)
202 throw std::invalid_argument("The number of estimated parameters should be non-negative");
203 std::map<typename Distribution::value_type, std::size_t> counts;
204 for (auto value : sample)
205 ++counts[value];
206 std::vector<detail::ChiSquaredBin> bins;
207 bins.reserve(counts.size() + 1);
208 double totalExpected = 0;
209 for (const auto &[value, observed] : counts) {
210 const double probability = distribution.P(value);
211 if (!std::isfinite(probability) || probability < 0 || probability > 1)
212 throw std::runtime_error("The fitted distribution returned an invalid probability");
213 const double expected = sample.size() * probability;
214 bins.push_back({static_cast<double>(observed), expected});
215 totalExpected += expected;
216 }
217 if (totalExpected > sample.size() * (1 + 16 * std::numeric_limits<double>::epsilon()))
218 throw std::runtime_error("The fitted distribution returned probabilities summing above one");
219 bins.push_back({0, std::max(0.0, sample.size() - totalExpected)});
221 if (std::ranges::any_of(bins, [](const auto &bin) { return bin.expected < 5; }))
222 throw std::invalid_argument("Chi-squared testing requires expected counts of at least five");
223 const int degreesOfFreedom = static_cast<int>(bins.size()) - 1 - estimatedParameters;
224 if (degreesOfFreedom <= 0)
225 throw std::invalid_argument("Too few chi-squared bins for the estimated parameter count");
226 double statistic = 0;
227 for (const auto &bin : bins) {
228 if (!(bin.expected > 0))
229 throw std::invalid_argument("The fitted distribution assigns zero probability to an observed bin");
230 const double difference = bin.observed - bin.expected;
231 statistic += difference * difference / bin.expected;
232 }
233 return {statistic, ChiSquaredRand<double>(degreesOfFreedom).S(statistic), degreesOfFreedom, bins.size()};
234}
235
236template <class Distribution, std::ranges::contiguous_range Range>
237 requires std::same_as<std::remove_cv_t<std::ranges::range_value_t<Range>>, typename Distribution::value_type>
238auto goodness_of_fit(const Distribution &distribution, const Range &sample)
239{
240 return goodness_of_fit(distribution, std::span<const typename Distribution::value_type>(sample));
241}
242
243template <class Distribution, std::ranges::contiguous_range Range>
244 requires std::integral<typename Distribution::value_type> &&
245 std::same_as<std::remove_cv_t<std::ranges::range_value_t<Range>>, typename Distribution::value_type>
246DiscreteGoodnessOfFit goodness_of_fit(const Distribution &distribution, const Range &sample, int estimatedParameters)
247{
248 return goodness_of_fit(distribution, std::span<const typename Distribution::value_type>(sample),
249 estimatedParameters);
250}
251
252} // namespace randlib
The ChiSquaredRand class Chi-squared distribution.
Definition continuous/gamma.hpp:355
double S(const RealType &x) const override
Definition continuous/gamma.hpp:485
The KolmogorovSmirnovRand class Kolmogorov-Smirnov distribution.
Definition kolmogorov_smirnov.hpp:19
double S(const RealType &x) const override
Definition kolmogorov_smirnov.hpp:161
double cvmCorrectionTerm(int k, double statistic)
Definition gof.hpp:96
double cvmCdf(double statistic, std::size_t sampleSize)
Definition gof.hpp:112
double cvmEd3(double value)
Definition gof.hpp:89
double andersonDarlingCdf(double statistic)
Definition gof.hpp:43
void mergeSparseBins(std::vector< ChiSquaredBin > &bins)
Definition gof.hpp:137
double cvmEd2(double value)
Definition gof.hpp:82
double cvmCdfInfinite(double statistic)
Definition gof.hpp:62
Real standardNormal(G &g)
Ziggurat algorithm of Marsaglia and Tsang with 256 layers and an explicit sign bit.
Definition variates.hpp:45
Real besselK(std::type_identity_t< Real > nu, Real x)
Definition special_functions.hpp:330
Definition bivariate_normal.hpp:10
ContinuousGoodnessOfFit goodness_of_fit(const Distribution &distribution, std::span< const typename Distribution::value_type > sample)
Definition gof.hpp:155
double kolmogorov_smirnov_p_value
Definition gof.hpp:25
double anderson_darling
Definition gof.hpp:26
double cramer_von_mises_p_value
Definition gof.hpp:29
double anderson_darling_p_value
Definition gof.hpp:27
double cramer_von_mises
Definition gof.hpp:28
double kolmogorov_smirnov
Definition gof.hpp:24
Definition gof.hpp:33
double p_value
Definition gof.hpp:35
std::size_t bins
Definition gof.hpp:37
int degrees_of_freedom
Definition gof.hpp:36
double chi_squared
Definition gof.hpp:34
Definition gof.hpp:132
double observed
Definition gof.hpp:133
double expected
Definition gof.hpp:134