v2.0.0
Loading...
Searching...
No Matches
sts_ttest.cpp
Go to the documentation of this file.
1//=============================================================================================================
24
25//=============================================================================================================
26// INCLUDES
27//=============================================================================================================
28
29#include "sts_ttest.h"
30
31#include <cmath>
32#include <algorithm>
33#include <QtGlobal>
34#include <random>
35#include <set>
36#include <vector>
37
38//=============================================================================================================
39// USED NAMESPACES
40//=============================================================================================================
41
42using namespace STSLIB;
43using namespace Eigen;
44
45//=============================================================================================================
46// DEFINE METHODS
47//=============================================================================================================
48
49StatsTtestResult StatsTtest::oneSample(const MatrixXd& data, double mu, StatsTailType tail)
50{
51 const int n = static_cast<int>(data.rows());
52 const int nVars = static_cast<int>(data.cols());
53 const int df = n - 1;
54
55 // Column means
56 RowVectorXd means = data.colwise().mean();
57
58 // Column standard deviations
59 MatrixXd centered = data.rowwise() - means;
60 RowVectorXd stddev = (centered.colwise().squaredNorm() / static_cast<double>(df)).array().sqrt();
61
62 // t-statistics: (mean - mu) / (std / sqrt(n))
63 double sqrtN = std::sqrt(static_cast<double>(n));
64 RowVectorXd tstat = (means.array() - mu) / (stddev.array() / sqrtN);
65
66 // p-values
67 MatrixXd matPval(1, nVars);
68 for (int j = 0; j < nVars; ++j) {
69 matPval(0, j) = tToPval(tstat(j), df, tail);
70 }
71
72 StatsTtestResult result;
73 result.matTstat = tstat;
74 result.matPval = matPval;
75 result.degreesOfFreedom = df;
76 return result;
77}
78
79//=============================================================================================================
80
81StatsTtestResult StatsTtest::paired(const MatrixXd& dataA, const MatrixXd& dataB, StatsTailType tail)
82{
83 return oneSample(dataA - dataB, 0.0, tail);
84}
85
86//=============================================================================================================
87
88StatsTtestResult StatsTtest::permutation(const MatrixXd& data, int nPermutations, StatsTailType tail, unsigned int seed)
89{
90 // Adapted from mne.stats.permutation_t_test / _get_1samp_orders / _max_stat (MNE-Python, BSD-3-Clause).
91 const int n = static_cast<int>(data.rows());
92 const RowVectorXd x2 = data.array().square().colwise().mean();
93 const double dofScaling = std::sqrt(n / (n - 1.0));
94 const double sqrtN = std::sqrt(static_cast<double>(n));
95 const auto tValues = [&](const RowVectorXd& mu) -> RowVectorXd {
96 return mu.array() / ((x2.array() - mu.array().square()).sqrt() * dofScaling / sqrtN);
97 };
98 const RowVectorXd tObs = tValues(data.colwise().mean());
99
100 // Sign flips as n-bit numbers, sample 0 in the most significant bit; a two-sided test never flips sample 0
101 const int bits = n - (tail == StatsTailType::Both ? 1 : 0);
102 if (n < 2 || bits > 63) {
103 qWarning("StatsTtest::permutation: needs 2 to 63 samples, got %d.", n);
104 return StatsTtestResult();
105 }
106 const quint64 maxPerms = (quint64(1) << bits) - 1;
107 std::vector<quint64> orders;
108 if (static_cast<quint64>(std::max(nPermutations, 0)) > maxPerms) {
109 for (quint64 s = 1; s <= maxPerms; ++s)
110 orders.push_back(s);
111 } else {
112 std::mt19937_64 gen(seed);
113 std::uniform_int_distribution<quint64> pick(1, maxPerms);
114 std::set<quint64> seen;
115 while (static_cast<int>(orders.size()) < nPermutations - 1) {
116 const quint64 s = pick(gen);
117 if (seen.insert(s).second)
118 orders.push_back(s);
119 }
120 }
121
122 std::vector<double> h0;
123 h0.reserve(orders.size() + 1);
124 for (quint64 s : orders) {
125 VectorXd signs(n);
126 for (int i = 0; i < n; ++i)
127 signs(i) = ((s >> (n - 1 - i)) & 1) ? -1.0 : 1.0;
128 h0.push_back(tValues(signs.transpose() * data / n).cwiseAbs().maxCoeff());
129 }
130 h0.push_back(tObs.cwiseAbs().maxCoeff());
131
132 MatrixXd pvals(1, tObs.size());
133 for (int j = 0; j < tObs.size(); ++j) {
134 const double t = tObs(j);
135 const auto count = std::count_if(h0.begin(), h0.end(), [&](double h) {
136 return tail == StatsTailType::Both ? h >= std::abs(t) : tail == StatsTailType::Right ? h >= t
137 : -h <= t;
138 });
139 pvals(0, j) = static_cast<double>(count) / static_cast<double>(h0.size());
140 }
141
142 StatsTtestResult result;
143 result.matTstat = tObs;
144 result.matPval = pvals;
145 result.degreesOfFreedom = n - 1;
146 return result;
147}
148
149//=============================================================================================================
150
151StatsTtestResult StatsTtest::independent(const MatrixXd& dataA, const MatrixXd& dataB, StatsTailType tail)
152{
153 const int nA = static_cast<int>(dataA.rows());
154 const int nB = static_cast<int>(dataB.rows());
155 const int nVars = static_cast<int>(dataA.cols());
156 const int df = nA + nB - 2;
157
158 RowVectorXd meanA = dataA.colwise().mean();
159 RowVectorXd meanB = dataB.colwise().mean();
160
161 // Pooled variance
162 MatrixXd centA = dataA.rowwise() - meanA;
163 MatrixXd centB = dataB.rowwise() - meanB;
164 RowVectorXd ssA = centA.colwise().squaredNorm();
165 RowVectorXd ssB = centB.colwise().squaredNorm();
166 RowVectorXd pooledVar = (ssA + ssB) / static_cast<double>(df);
167
168 // t-statistic
169 double invN = 1.0 / static_cast<double>(nA) + 1.0 / static_cast<double>(nB);
170 RowVectorXd tstat = (meanA - meanB).array() / (pooledVar.array() * invN).sqrt();
171
172 // p-values
173 MatrixXd matPval(1, nVars);
174 for (int j = 0; j < nVars; ++j) {
175 matPval(0, j) = tToPval(tstat(j), df, tail);
176 }
177
178 StatsTtestResult result;
179 result.matTstat = tstat;
180 result.matPval = matPval;
181 result.degreesOfFreedom = df;
182 return result;
183}
184
185//=============================================================================================================
186
187double StatsTtest::tCdf(double t, int df)
188{
189 // P(T <= t) for t-distribution with df degrees of freedom.
190 // Using: P(T <= t) = 1 - 0.5 * I_x(df/2, 1/2) where x = df/(df + t^2) [for t >= 0]
191 // For t < 0: P(T <= t) = 0.5 * I_x(df/2, 1/2) where x = df/(df + t^2)
192 double x = static_cast<double>(df) / (static_cast<double>(df) + t * t);
193 double iBeta = regularizedBeta(x, static_cast<double>(df) / 2.0, 0.5);
194 if (t >= 0.0) {
195 return 1.0 - 0.5 * iBeta;
196 } else {
197 return 0.5 * iBeta;
198 }
199}
200
201//=============================================================================================================
202
203double StatsTtest::tToPval(double t, int df, StatsTailType tail)
204{
205 // Each tail directly from I_x(df/2, 1/2) = P(|T| >= |t|), so p-values below 1e-16 are not lost to 1 - CDF
206 const double x = static_cast<double>(df) / (static_cast<double>(df) + t * t);
207 const double twoSided = regularizedBeta(x, static_cast<double>(df) / 2.0, 0.5);
208 switch (tail) {
210 return t < 0.0 ? 0.5 * twoSided : 1.0 - 0.5 * twoSided;
212 return t >= 0.0 ? 0.5 * twoSided : 1.0 - 0.5 * twoSided;
214 default:
215 return twoSided;
216 }
217}
218
219//=============================================================================================================
220
221double StatsTtest::regularizedBeta(double x, double a, double b)
222{
223 // I_x(a,b) = x^a * (1-x)^b / (a * B(a,b)) * CF(x,a,b)
224 // where CF is the continued fraction expansion.
225 // For x > (a+1)/(a+b+2), use the identity I_x(a,b) = 1 - I_{1-x}(b,a).
226 if (x <= 0.0)
227 return 0.0;
228 if (x >= 1.0)
229 return 1.0;
230
231 if (x > (a + 1.0) / (a + b + 2.0)) {
232 return 1.0 - regularizedBeta(1.0 - x, b, a);
233 }
234
235 double lnPre = a * std::log(x) + b * std::log(1.0 - x) - logBeta(a, b) - std::log(a);
236 return std::exp(lnPre) * betaCf(x, a, b);
237}
238
239//=============================================================================================================
240
241double StatsTtest::betaCf(double x, double a, double b)
242{
243 // Continued fraction for I_x(a,b) using Lentz's algorithm.
244 const int maxIter = 200;
245 const double eps = 1.0e-12;
246 const double tiny = 1.0e-30;
247
248 double qab = a + b;
249 double qap = a + 1.0;
250 double qam = a - 1.0;
251
252 double c = 1.0;
253 double d = 1.0 - qab * x / qap;
254 if (std::fabs(d) < tiny)
255 d = tiny;
256 d = 1.0 / d;
257 double h = d;
258
259 for (int m = 1; m <= maxIter; ++m) {
260 double m2 = 2.0 * m;
261
262 // Even step
263 double aa = m * (b - m) * x / ((qam + m2) * (a + m2));
264 d = 1.0 + aa * d;
265 if (std::fabs(d) < tiny)
266 d = tiny;
267 c = 1.0 + aa / c;
268 if (std::fabs(c) < tiny)
269 c = tiny;
270 d = 1.0 / d;
271 h *= d * c;
272
273 // Odd step
274 aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
275 d = 1.0 + aa * d;
276 if (std::fabs(d) < tiny)
277 d = tiny;
278 c = 1.0 + aa / c;
279 if (std::fabs(c) < tiny)
280 c = tiny;
281 d = 1.0 / d;
282 double del = d * c;
283 h *= del;
284
285 if (std::fabs(del - 1.0) < eps)
286 break;
287 }
288 return h;
289}
290
291//=============================================================================================================
292
293double StatsTtest::logBeta(double a, double b)
294{
295 return std::lgamma(a) + std::lgamma(b) - std::lgamma(a + b);
296}
Frequentist Student's t-tests with exact p-values via the regularised incomplete beta function.
Statistical testing (t-tests, F-tests, cluster permutation, multiple comparison correction).
StatsTailType
Direction of the alternative hypothesis for a t- or F-test (left, right, or two-sided).
Definition sts_types.h:38
Per-call output of a Student t-test: t-statistics, p-values and degrees of freedom.
Definition sts_ttest.h:65
Eigen::MatrixXd matPval
Definition sts_ttest.h:67
Eigen::MatrixXd matTstat
Definition sts_ttest.h:66
static double regularizedBeta(double x, double a, double b)
static StatsTtestResult paired(const Eigen::MatrixXd &dataA, const Eigen::MatrixXd &dataB, StatsTailType tail=StatsTailType::Both)
Definition sts_ttest.cpp:81
static StatsTtestResult permutation(const Eigen::MatrixXd &data, int nPermutations=10000, StatsTailType tail=StatsTailType::Both, unsigned int seed=0)
Definition sts_ttest.cpp:88
static double tCdf(double t, int df)
static StatsTtestResult oneSample(const Eigen::MatrixXd &data, double mu=0.0, StatsTailType tail=StatsTailType::Both)
Definition sts_ttest.cpp:49
static StatsTtestResult independent(const Eigen::MatrixXd &dataA, const Eigen::MatrixXd &dataB, StatsTailType tail=StatsTailType::Both)