LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sim_shapirowilk.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_SIM_SIM_SHAPIROWILK_H
6#define LINE_API_SIM_SIM_SHAPIROWILK_H
7
8/**
9 * @file
10 * @ingroup api_sim
11 * Shapiro-Wilk test for univariate normality.
12 *
13 * Port of matlab/src/api/sim/sim_shapirowilk.m, i.e. Royston's AS R94 algorithm,
14 * valid for 3 <= n <= 5000. The statistic is
15 * W = (sum_i a_i x_(i))^2 / sum_i (x_i - xbar)^2,
16 * where x_(i) are the order statistics and a is the antisymmetric weight vector
17 * obtained by correcting the normalized expected normal order statistics
18 * m_i = Phi^{-1}((i-3/8)/(n+1/4)) in their two extreme components. Small W means
19 * departure from normality, so the test is one-sided in W and the p-value is an
20 * upper normal tail after Royston's normalizing transform, which has three
21 * branches: n = 3 exact, 4 <= n <= 11, and n >= 12.
22 *
23 * The weights are computed in double throughout. They are a function of n
24 * alone -- Phi^{-1} at fixed plotting positions plus two polynomial corrections
25 * with published five-digit coefficients -- so refining them past double would
26 * refine a constant that is only known to five digits anyway; the sample itself
27 * enters in T.
28 *
29 * Reference: J. P. Royston, "Approximating the Shapiro-Wilk W-test for
30 * Non-normality", Statistics and Computing 2, 1992; J. P. Royston, "Remark
31 * AS R94", Applied Statistics 44(4), 1995. W and the p-value agree with
32 * scipy.stats.shapiro to 5e-10 and 1.5e-7 respectively over n up to 2000.
33 */
34
35#include <algorithm>
36#include <cmath>
37#include <cstddef>
38#include <limits>
39#include <vector>
40
41#include <boost/math/constants/constants.hpp>
42
45#include "line/num/number.h"
46#include "line/util/error.h"
47
48namespace line {
49namespace sim {
50
51/** Outcome of the Shapiro-Wilk normality test. */
52template <class T>
54 T W; ///< The Shapiro-Wilk statistic
55 double pvalue = 1.0; ///< p-value, small means normality is rejected
56 double zscore = 0.0; ///< Normalized statistic, NaN when n = 3
57 bool reject = false; ///< True when pvalue < alpha
58 std::size_t nobs = 0; ///< Number of observations n
59};
60
61namespace detail {
62
63/** Royston AS R94 antisymmetric weight vector, a(n+1-i) = -a(i), zero based. */
64inline std::vector<double> shapirowilk_weights(std::size_t n) {
65 std::vector<double> a(n, 0.0);
66 if (n == 3) {
67 a[0] = -std::sqrt(0.5);
68 a[1] = 0.0;
69 a[2] = std::sqrt(0.5);
70 return a;
71 }
72
73 static const double c1[6] = {0.0, 0.221157, -0.147981, -2.071190, 4.434685, -2.706056};
74 static const double c2[6] = {0.0, 0.042981, -0.293762, -1.752461, 5.682633, -3.582633};
75
76 std::vector<double> m(n, 0.0);
77 double mm = 0.0;
78 for (std::size_t i = 0; i < n; ++i) {
79 m[i] = sim_norminv((static_cast<double>(i + 1) - 0.375) /
80 (static_cast<double>(n) + 0.25));
81 mm += m[i] * m[i];
82 }
83 const double u = 1.0 / std::sqrt(static_cast<double>(n));
84
85 // MATLAB's polyval(fliplr(c), u), i.e. the ascending-power evaluation
86 double p1 = 0.0, p2 = 0.0, uk = 1.0;
87 for (int k = 0; k < 6; ++k) {
88 p1 += c1[k] * uk;
89 p2 += c2[k] * uk;
90 uk *= u;
91 }
92
93 a = m;
94 const double an = m[n - 1] / std::sqrt(mm) + p1;
95 if (n > 5) {
96 const double anm1 = m[n - 2] / std::sqrt(mm) + p2;
97 const double phi = (mm - 2.0 * m[n - 1] * m[n - 1] - 2.0 * m[n - 2] * m[n - 2]) /
98 (1.0 - 2.0 * an * an - 2.0 * anm1 * anm1);
99 for (std::size_t i = 2; i + 2 < n; ++i) a[i] = m[i] / std::sqrt(phi);
100 a[n - 1] = an;
101 a[n - 2] = anm1;
102 a[0] = -an;
103 a[1] = -anm1;
104 } else {
105 const double phi = (mm - 2.0 * m[n - 1] * m[n - 1]) / (1.0 - 2.0 * an * an);
106 for (std::size_t i = 1; i + 1 < n; ++i) a[i] = m[i] / std::sqrt(phi);
107 a[n - 1] = an;
108 a[0] = -an;
109 }
110 return a;
111}
112
113} // namespace detail
114
115/**
116 * @brief Shapiro-Wilk test for univariate normality.
117 *
118 * @param x the sample, 3 to 5000 finite observations, order irrelevant
119 * @param alpha significance level in (0,1), 0.05 by default
120 */
121template <class T>
122ShapiroWilkResult<T> sim_shapirowilk(const std::vector<T>& x, double alpha = 0.05) {
124 "sim_shapirowilk: the weights and the p-value are transcendental, so exact "
125 "arithmetic is refused");
126 if (!(alpha > 0.0) || !(alpha < 1.0))
127 throw InputError("sim_shapirowilk: alpha must be a real scalar in (0,1)");
128
129 const std::size_t n = x.size();
130 if (n < 3)
131 throw InputError("sim_shapirowilk: at least 3 observations are required");
132 if (n > 5000)
133 throw InputError("sim_shapirowilk: the AS R94 approximation is valid up to n = 5000");
134 for (std::size_t i = 0; i < n; ++i)
135 if (!detail::num_isfinite(x[i]))
136 throw InputError("sim_shapirowilk: the sample must be finite");
137
138 std::vector<T> s(x);
139 std::sort(s.begin(), s.end());
140
142 for (std::size_t i = 0; i < n; ++i) sum += s[i];
143 const T mean = sum / num_traits<T>::from_int(static_cast<long>(n));
144 T ssd = num_traits<T>::from_int(0);
145 for (std::size_t i = 0; i < n; ++i) {
146 const T d = T(s[i] - mean);
147 ssd += T(d * d);
148 }
149 if (!(ssd > num_traits<T>::from_int(0)))
150 throw InputError("sim_shapirowilk: the sample is constant, W is undefined");
151
152 const std::vector<double> a = detail::shapirowilk_weights(n);
153 T ax = num_traits<T>::from_int(0);
154 for (std::size_t i = 0; i < n; ++i) ax += T(num_traits<T>::from_double(a[i]) * s[i]);
155
157 r.nobs = n;
158 r.W = T(T(ax * ax) / ssd);
159 const T one = num_traits<T>::from_int(1);
160 if (r.W > one) r.W = one;
161
162 const double Wd = num_traits<T>::to_double(r.W);
163 if (n == 3) {
164 // exact null distribution, W is supported on [3/4, 1]
165 const double pi = boost::math::constants::pi<double>();
166 double p = 6.0 / pi * (std::asin(std::sqrt(Wd)) - std::asin(std::sqrt(0.75)));
167 r.pvalue = std::min(std::max(p, 0.0), 1.0);
168 r.zscore = std::numeric_limits<double>::quiet_NaN();
169 } else {
170 const double nd = static_cast<double>(n);
171 double w, mu, sigma;
172 if (n <= 11) {
173 const double g = -2.273 + 0.459 * nd;
174 w = -std::log(g - std::log(1.0 - Wd));
175 mu = 0.5440 - 0.39978 * nd + 0.025054 * nd * nd - 0.0006714 * nd * nd * nd;
176 sigma = std::exp(1.3822 - 0.77857 * nd + 0.062767 * nd * nd -
177 0.0020322 * nd * nd * nd);
178 } else {
179 const double ln = std::log(nd);
180 w = std::log(1.0 - Wd);
181 mu = -1.5861 - 0.31082 * ln - 0.083751 * ln * ln + 0.0038915 * ln * ln * ln;
182 sigma = std::exp(-0.4803 - 0.082676 * ln + 0.0030302 * ln * ln);
183 }
184 r.zscore = (w - mu) / sigma;
185 r.pvalue = 1.0 - sim_normcdf(r.zscore);
186 }
187 r.reject = r.pvalue < alpha;
188 return r;
189}
190
191} // namespace sim
192} // namespace line
193
194#endif // LINE_API_SIM_SIM_SHAPIROWILK_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
double sim_norminv(double p)
Standard normal quantile function.
Definition sim_dist.h:62
double sim_normcdf(double z)
Standard normal cumulative distribution function.
Definition sim_dist.h:52
ShapiroWilkResult< T > sim_shapirowilk(const std::vector< T > &x, double alpha=0.05)
Shapiro-Wilk test for univariate normality.
Number-type abstraction for the templated API port.
Normal and Student t quantiles used by the output-analysis routines.
Shared arithmetic helpers for the templated simulation output-analysis port.
Outcome of the Shapiro-Wilk normality test.
std::size_t nobs
Number of observations n.
bool reject
True when pvalue < alpha.
double pvalue
p-value, small means normality is rejected
double zscore
Normalized statistic, NaN when n = 3.
T W
The Shapiro-Wilk statistic.