44#include <QtConcurrent>
45#include <QRandomGenerator>
64 const QVector<MatrixXd>& dataA,
65 const QVector<MatrixXd>& dataB,
66 const SparseMatrix<int>& adjacency,
74 const int nA = dataA.size();
77 MatrixXd tObs = computeTMap(dataA, dataB);
81 double threshold = inverseTCdf(clusterAlpha, df);
84 auto [clusterIds, clusterStats] = findClusters(tObs, threshold, adjacency, tail);
87 QVector<MatrixXd> allData;
88 allData.reserve(nA + dataB.size());
89 for (
const auto& m : dataA) allData.append(m);
90 for (
const auto& m : dataB) allData.append(m);
93 QVector<double> nullDist(nPermutations);
96 QVector<int> permIndices(nPermutations);
97 std::iota(permIndices.begin(), permIndices.end(), 0);
99 std::function<double(
int)> permuteFunc = [&](int) ->
double {
100 return permuteOnce(allData, nA, adjacency, threshold, tail);
103 QFuture<double> future = QtConcurrent::mapped(permIndices, permuteFunc);
104 future.waitForFinished();
106 for (
int i = 0; i < nPermutations; ++i) {
107 nullDist[i] = future.resultAt(i);
111 std::sort(nullDist.begin(), nullDist.end());
114 QVector<double> clusterPvals(clusterStats.size());
115 for (
int c = 0; c < clusterStats.size(); ++c) {
116 double obsStat = std::fabs(clusterStats[c]);
118 for (
int p = 0; p < nPermutations; ++p) {
119 if (nullDist[p] >= obsStat) {
123 clusterPvals[c] =
static_cast<double>(count) /
static_cast<double>(nPermutations);
137MatrixXd StatsCluster::computeTMap(
const QVector<MatrixXd>& dataA,
const QVector<MatrixXd>& dataB)
139 const int nSubjects = dataA.size();
140 const int nChannels =
static_cast<int>(dataA[0].rows());
141 const int nTimes =
static_cast<int>(dataA[0].cols());
145 MatrixXd sumDiff = MatrixXd::Zero(nChannels, nTimes);
146 MatrixXd sumDiffSq = MatrixXd::Zero(nChannels, nTimes);
148 for (
int s = 0; s < nSubjects; ++s) {
149 MatrixXd diff = dataA[s] - dataB[s];
151 sumDiffSq += diff.cwiseProduct(diff);
154 double n =
static_cast<double>(nSubjects);
155 MatrixXd mean = sumDiff / n;
156 MatrixXd variance = (sumDiffSq - sumDiff.cwiseProduct(sumDiff) / n) / (n - 1.0);
159 variance = variance.cwiseMax(1.0e-30);
161 MatrixXd tMap = mean.array() / (variance.array().sqrt() / std::sqrt(n));
167QPair<MatrixXi, QVector<double>> StatsCluster::findClusters(
168 const MatrixXd& tMap,
170 const SparseMatrix<int>& adjacency,
173 const int nChannels =
static_cast<int>(tMap.rows());
174 const int nTimes =
static_cast<int>(tMap.cols());
176 MatrixXi clusterIds = MatrixXi::Zero(nChannels, nTimes);
177 QVector<double> clusterStats;
178 int currentClusterId = 0;
181 auto bfsClusters = [&](
bool positive) {
183 MatrixXi visited = MatrixXi::Zero(nChannels, nTimes);
185 for (
int ch = 0; ch < nChannels; ++ch) {
186 for (
int t = 0; t < nTimes; ++t) {
187 double val = tMap(ch, t);
188 bool suprathreshold = positive ? (val > threshold) : (val < -threshold);
190 if (!suprathreshold || visited(ch, t))
continue;
194 double clusterSum = 0.0;
195 std::queue<std::pair<int, int>> queue;
199 while (!queue.empty()) {
200 auto [curCh, curT] = queue.front();
203 clusterIds(curCh, curT) = positive ? currentClusterId : -currentClusterId;
204 clusterSum += tMap(curCh, curT);
207 for (
int dt = -1; dt <= 1; dt += 2) {
209 if (nt < 0 || nt >= nTimes)
continue;
210 double nval = tMap(curCh, nt);
211 bool nSupra = positive ? (nval > threshold) : (nval < -threshold);
212 if (nSupra && !visited(curCh, nt)) {
213 visited(curCh, nt) = 1;
214 queue.push({curCh, nt});
219 for (SparseMatrix<int>::InnerIterator it(adjacency, curCh); it; ++it) {
220 int nCh =
static_cast<int>(it.row());
221 double nval = tMap(nCh, curT);
222 bool nSupra = positive ? (nval > threshold) : (nval < -threshold);
223 if (nSupra && !visited(nCh, curT)) {
224 visited(nCh, curT) = 1;
225 queue.push({nCh, curT});
230 clusterStats.append(clusterSum);
242 return {clusterIds, clusterStats};
247double StatsCluster::permuteOnce(
248 const QVector<MatrixXd>& allData,
250 const SparseMatrix<int>& adjacency,
254 const int nTotal = allData.size();
257 std::vector<int> indices(nTotal);
258 std::iota(indices.begin(), indices.end(), 0);
261 QRandomGenerator rng(QRandomGenerator::global()->generate());
262 for (
int i = nTotal - 1; i > 0; --i) {
263 int j =
static_cast<int>(rng.bounded(i + 1));
264 std::swap(indices[i], indices[j]);
268 QVector<MatrixXd> permA, permB;
270 permB.reserve(nTotal - nA);
271 for (
int i = 0; i < nTotal; ++i) {
273 permA.append(allData[indices[i]]);
275 permB.append(allData[indices[i]]);
280 MatrixXd tMap = computeTMap(permA, permB);
281 auto [clusterIds, clusterStats] = findClusters(tMap, threshold, adjacency, tail);
284 double maxStat = 0.0;
285 for (
double s : clusterStats) {
286 double absS = std::fabs(s);
287 if (absS > maxStat) {
296double StatsCluster::inverseTCdf(
double p,
int df)
303 double targetCdf = 1.0 - p / 2.0;
308 for (
int iter = 0; iter < 100; ++iter) {
309 double mid = (lo + hi) / 2.0;
311 if (cdf < targetCdf) {
318 return (lo + hi) / 2.0;
323MatrixXd StatsCluster::computeOneSampleTMap(
const QVector<MatrixXd>& data)
325 const int nSubjects = data.size();
326 const int nVertices =
static_cast<int>(data[0].rows());
327 const int nTimes =
static_cast<int>(data[0].cols());
329 MatrixXd sum = MatrixXd::Zero(nVertices, nTimes);
330 MatrixXd sumSq = MatrixXd::Zero(nVertices, nTimes);
332 for (
int s = 0; s < nSubjects; ++s) {
334 sumSq += data[s].cwiseProduct(data[s]);
337 double n =
static_cast<double>(nSubjects);
338 MatrixXd mean = sum / n;
339 MatrixXd variance = (sumSq - sum.cwiseProduct(sum) / n) / (n - 1.0);
340 variance = variance.cwiseMax(1.0e-30);
342 MatrixXd tMap = mean.array() / (variance.array().sqrt() / std::sqrt(n));
348MatrixXd StatsCluster::computeFMap(
const QVector<QVector<MatrixXd>>& conditions)
350 const int nConditions = conditions.size();
351 const int nVertices =
static_cast<int>(conditions[0][0].rows());
352 const int nTimes =
static_cast<int>(conditions[0][0].cols());
356 for (
int c = 0; c < nConditions; ++c) {
357 nTotal += conditions[c].size();
361 MatrixXd grandSum = MatrixXd::Zero(nVertices, nTimes);
362 for (
int c = 0; c < nConditions; ++c) {
363 for (
int s = 0; s < conditions[c].size(); ++s) {
364 grandSum += conditions[c][s];
367 MatrixXd grandMean = grandSum /
static_cast<double>(nTotal);
370 MatrixXd ssBetween = MatrixXd::Zero(nVertices, nTimes);
371 MatrixXd ssWithin = MatrixXd::Zero(nVertices, nTimes);
373 for (
int c = 0; c < nConditions; ++c) {
374 int nc = conditions[c].size();
375 MatrixXd condSum = MatrixXd::Zero(nVertices, nTimes);
376 for (
int s = 0; s < nc; ++s) {
377 condSum += conditions[c][s];
379 MatrixXd condMean = condSum /
static_cast<double>(nc);
381 MatrixXd diff = condMean - grandMean;
382 ssBetween +=
static_cast<double>(nc) * diff.cwiseProduct(diff);
384 for (
int s = 0; s < nc; ++s) {
385 MatrixXd residual = conditions[c][s] - condMean;
386 ssWithin += residual.cwiseProduct(residual);
390 int dfBetween = nConditions - 1;
391 int dfWithin = nTotal - nConditions;
393 MatrixXd msBetween = ssBetween /
static_cast<double>(dfBetween);
394 MatrixXd msWithin = ssWithin /
static_cast<double>(dfWithin);
395 msWithin = msWithin.cwiseMax(1.0e-30);
397 MatrixXd fMap = msBetween.array() / msWithin.array();
403QPair<MatrixXi, QVector<double>> StatsCluster::findClustersFlat(
404 const MatrixXd& statMap,
406 const SparseMatrix<int>& adjacency,
409 const int nVertices =
static_cast<int>(statMap.rows());
410 const int nTimes =
static_cast<int>(statMap.cols());
411 const int nTotal = nVertices * nTimes;
413 MatrixXi clusterIds = MatrixXi::Zero(nVertices, nTimes);
414 QVector<double> clusterStats;
415 int currentClusterId = 0;
418 std::vector<bool> visited(nTotal,
false);
420 for (
int idx = 0; idx < nTotal; ++idx) {
421 int v = idx / nTimes;
422 int t = idx % nTimes;
423 double val = statMap(v, t);
425 bool suprathreshold = positiveOnly ? (val > threshold) : (val < -threshold);
426 if (!suprathreshold || visited[idx])
continue;
429 double clusterSum = 0.0;
430 std::queue<int> queue;
434 while (!queue.empty()) {
435 int curIdx = queue.front();
438 int curV = curIdx / nTimes;
439 int curT = curIdx % nTimes;
441 clusterIds(curV, curT) = positiveOnly ? currentClusterId : -currentClusterId;
442 clusterSum += statMap(curV, curT);
445 for (SparseMatrix<int>::InnerIterator it(adjacency, curIdx); it; ++it) {
446 int nIdx = static_cast<int>(it.row());
447 if (visited[nIdx]) continue;
448 int nV = nIdx / nTimes;
449 int nT = nIdx % nTimes;
450 double nval = statMap(nV, nT);
451 bool nSupra = positiveOnly ? (nval > threshold) : (nval < -threshold);
453 visited[nIdx] = true;
459 clusterStats.append(clusterSum);
462 return {clusterIds, clusterStats};
467double StatsCluster::permuteOnceOneSample(
468 const QVector<MatrixXd>& data,
469 const SparseMatrix<int>& adjacency,
473 const int nSubjects = data.size();
476 QRandomGenerator rng(QRandomGenerator::global()->generate());
477 QVector<int> signs(nSubjects);
478 for (
int s = 0; s < nSubjects; ++s) {
479 signs[s] = rng.bounded(2) == 0 ? 1 : -1;
483 QVector<MatrixXd> flipped(nSubjects);
484 for (
int s = 0; s < nSubjects; ++s) {
485 flipped[s] = data[s] *
static_cast<double>(signs[s]);
489 MatrixXd tMap = computeOneSampleTMap(flipped);
491 double maxStat = 0.0;
494 auto [ids, stats] = findClustersFlat(tMap, threshold, adjacency,
true);
495 for (
double s : stats) {
496 if (std::fabs(s) > maxStat) maxStat = std::fabs(s);
500 auto [ids, stats] = findClustersFlat(tMap, threshold, adjacency,
false);
501 for (
double s : stats) {
502 if (std::fabs(s) > maxStat) maxStat = std::fabs(s);
511double StatsCluster::permuteOnceFTest(
512 const QVector<MatrixXd>& allData,
513 const QVector<int>& groupSizes,
514 const SparseMatrix<int>& adjacency,
517 const int nTotal = allData.size();
520 std::vector<int> indices(nTotal);
521 std::iota(indices.begin(), indices.end(), 0);
523 QRandomGenerator rng(QRandomGenerator::global()->generate());
524 for (
int i = nTotal - 1; i > 0; --i) {
525 int j =
static_cast<int>(rng.bounded(i + 1));
526 std::swap(indices[i], indices[j]);
530 QVector<QVector<MatrixXd>> permConditions;
532 for (
int c = 0; c < groupSizes.size(); ++c) {
533 QVector<MatrixXd> group;
534 group.reserve(groupSizes[c]);
535 for (
int s = 0; s < groupSizes[c]; ++s) {
536 group.append(allData[indices[offset + s]]);
538 permConditions.append(group);
539 offset += groupSizes[c];
543 MatrixXd fMap = computeFMap(permConditions);
544 auto [ids, stats] = findClustersFlat(fMap, threshold, adjacency,
true);
546 double maxStat = 0.0;
547 for (
double s : stats) {
548 if (s > maxStat) maxStat = s;
556 const QVector<MatrixXd>& data,
557 const SparseMatrix<int>& adjacency,
563 MatrixXd tObs = computeOneSampleTMap(data);
566 MatrixXi clusterIdsPos, clusterIdsNeg;
567 QVector<double> clusterStats;
570 auto [ids, stats] = findClustersFlat(tObs, threshold, adjacency,
true);
572 clusterStats.append(stats);
575 auto [ids, stats] = findClustersFlat(tObs, threshold, adjacency,
false);
577 clusterStats.append(stats);
581 MatrixXi clusterIds = MatrixXi::Zero(tObs.rows(), tObs.cols());
582 if (clusterIdsPos.size() > 0) clusterIds += clusterIdsPos;
583 if (clusterIdsNeg.size() > 0) clusterIds += clusterIdsNeg;
586 QVector<int> permIndices(nPermutations);
587 std::iota(permIndices.begin(), permIndices.end(), 0);
589 std::function<double(
int)> permuteFunc = [&](int) ->
double {
590 return permuteOnceOneSample(data, adjacency, threshold, tail);
593 QFuture<double> future = QtConcurrent::mapped(permIndices, permuteFunc);
594 future.waitForFinished();
596 QVector<double> nullDist(nPermutations);
597 for (
int i = 0; i < nPermutations; ++i) {
598 nullDist[i] = future.resultAt(i);
600 std::sort(nullDist.begin(), nullDist.end());
603 QVector<double> clusterPvals(clusterStats.size());
604 for (
int c = 0; c < clusterStats.size(); ++c) {
605 double obsStat = std::fabs(clusterStats[c]);
607 for (
int p = 0; p < nPermutations; ++p) {
608 if (nullDist[p] >= obsStat) {
612 clusterPvals[c] =
static_cast<double>(count) /
static_cast<double>(nPermutations);
627 const QVector<QVector<MatrixXd>>& conditions,
628 const SparseMatrix<int>& adjacency,
633 MatrixXd fObs = computeFMap(conditions);
636 auto [clusterIds, clusterStats] = findClustersFlat(fObs, threshold, adjacency,
true);
639 QVector<MatrixXd> allData;
640 QVector<int> groupSizes;
641 for (
int c = 0; c < conditions.size(); ++c) {
642 groupSizes.append(conditions[c].size());
643 for (
int s = 0; s < conditions[c].size(); ++s) {
644 allData.append(conditions[c][s]);
649 QVector<int> permIndices(nPermutations);
650 std::iota(permIndices.begin(), permIndices.end(), 0);
652 std::function<double(
int)> permuteFunc = [&](int) ->
double {
653 return permuteOnceFTest(allData, groupSizes, adjacency, threshold);
656 QFuture<double> future = QtConcurrent::mapped(permIndices, permuteFunc);
657 future.waitForFinished();
659 QVector<double> nullDist(nPermutations);
660 for (
int i = 0; i < nPermutations; ++i) {
661 nullDist[i] = future.resultAt(i);
663 std::sort(nullDist.begin(), nullDist.end());
666 QVector<double> clusterPvals(clusterStats.size());
667 for (
int c = 0; c < clusterStats.size(); ++c) {
668 double obsStat = clusterStats[c];
670 for (
int p = 0; p < nPermutations; ++p) {
671 if (nullDist[p] >= obsStat) {
675 clusterPvals[c] =
static_cast<double>(count) /
static_cast<double>(nPermutations);
690 const MatrixXd& statMap,
691 const SparseMatrix<int>& adjacency,
696 const int nVertices =
static_cast<int>(statMap.rows());
697 const int nTimes =
static_cast<int>(statMap.cols());
698 const int nTotal = nVertices * nTimes;
700 MatrixXd tfceMap = MatrixXd::Zero(nVertices, nTimes);
703 auto runTfce = [&](
const MatrixXd& absMap,
bool isPositive) {
704 double maxVal = absMap.maxCoeff();
705 if (maxVal <= 0.0)
return;
707 double dh = maxVal /
static_cast<double>(nSteps);
709 for (
int step = 1; step <= nSteps; ++step) {
710 double h = dh *
static_cast<double>(step);
713 std::vector<bool> visited(nTotal,
false);
715 for (
int idx = 0; idx < nTotal; ++idx) {
716 int v = idx / nTimes;
717 int t = idx % nTimes;
719 if (absMap(v, t) < h || visited[idx])
continue;
722 std::vector<int> cluster;
723 std::queue<int> queue;
727 while (!queue.empty()) {
728 int curIdx = queue.front();
730 cluster.push_back(curIdx);
732 for (SparseMatrix<int>::InnerIterator it(adjacency, curIdx); it; ++it) {
733 int nIdx =
static_cast<int>(it.row());
734 if (visited[nIdx])
continue;
735 int nV = nIdx / nTimes;
736 int nT = nIdx % nTimes;
737 if (absMap(nV, nT) >= h) {
738 visited[nIdx] =
true;
745 double extent =
static_cast<double>(cluster.size());
746 double contribution = std::pow(extent, E) * std::pow(h, H) * dh;
748 for (
int cIdx : cluster) {
749 int cv = cIdx / nTimes;
750 int ct = cIdx % nTimes;
752 tfceMap(cv, ct) += contribution;
754 tfceMap(cv, ct) -= contribution;
762 MatrixXd posMap = statMap.cwiseMax(0.0);
763 runTfce(posMap,
true);
766 MatrixXd negMap = (-statMap).cwiseMax(0.0);
767 runTfce(negMap,
false);
Frequentist Student's t-tests with exact p-values via the regularised incomplete beta function.
One-way ANOVA F-test with exact p-values for comparing two or more independent groups.
Maris-Oostenveld cluster-mass permutation tests and Threshold-Free Cluster Enhancement for M/EEG infe...
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).
Per-call output of a cluster permutation test: observed statistic map, cluster masses,...
Eigen::MatrixXi matClusterIds
QVector< double > vecClusterStats
QVector< double > vecClusterPvals
static StatsClusterResult permutationTest(const QVector< Eigen::MatrixXd > &dataA, const QVector< Eigen::MatrixXd > &dataB, const Eigen::SparseMatrix< int > &adjacency, int nPermutations=1024, double clusterAlpha=0.05, double pThreshold=0.05, StatsTailType tail=StatsTailType::Both)
static Eigen::MatrixXd tfce(const Eigen::MatrixXd &statMap, const Eigen::SparseMatrix< int > &adjacency, double E=0.5, double H=2.0, int nSteps=100)
static StatsClusterResult oneSamplePermutationTest(const QVector< Eigen::MatrixXd > &data, const Eigen::SparseMatrix< int > &adjacency, double threshold, int nPermutations, StatsTailType tail)
static StatsClusterResult fTestPermutationTest(const QVector< QVector< Eigen::MatrixXd > > &conditions, const Eigen::SparseMatrix< int > &adjacency, double threshold, int nPermutations)
static double tCdf(double t, int df)