45#include <QtConcurrent>
46#include <QRandomGenerator>
65 const QVector<MatrixXd>& dataA,
66 const QVector<MatrixXd>& dataB,
67 const SparseMatrix<int>& adjacency,
75 const int nA = dataA.size();
78 MatrixXd tObs = computeTMap(dataA, dataB);
83 double threshold = inverseTCdf(tail ==
StatsTailType::Both ? clusterAlpha : 2.0 * clusterAlpha, df);
86 auto [clusterIds, clusterStats] = findClusters(tObs, threshold, adjacency, tail);
89 QVector<MatrixXd> allData;
90 allData.reserve(nA + dataB.size());
91 for (
const auto& m : dataA)
93 for (
const auto& m : dataB)
97 QVector<double> nullDist(nPermutations);
100 QVector<int> permIndices(nPermutations);
101 std::iota(permIndices.begin(), permIndices.end(), 0);
103 std::function<double(
int)> permuteFunc = [&](int) ->
double {
104 return permuteOnce(allData, nA, adjacency, threshold, tail);
107 QFuture<double> future = QtConcurrent::mapped(permIndices, permuteFunc);
108 future.waitForFinished();
110 for (
int i = 0; i < nPermutations; ++i) {
111 nullDist[i] = future.resultAt(i);
115 std::sort(nullDist.begin(), nullDist.end());
118 QVector<double> clusterPvals(clusterStats.size());
119 for (
int c = 0; c < clusterStats.size(); ++c) {
120 double obsStat = std::fabs(clusterStats[c]);
122 for (
int p = 0; p < nPermutations; ++p) {
123 if (nullDist[p] >= obsStat) {
127 clusterPvals[c] =
static_cast<double>(count + 1) /
static_cast<double>(nPermutations + 1);
141MatrixXd StatsCluster::computeTMap(
const QVector<MatrixXd>& dataA,
const QVector<MatrixXd>& dataB)
143 const int nSubjects = dataA.size();
144 const int nChannels =
static_cast<int>(dataA[0].rows());
145 const int nTimes =
static_cast<int>(dataA[0].cols());
149 MatrixXd sumDiff = MatrixXd::Zero(nChannels, nTimes);
150 MatrixXd sumDiffSq = MatrixXd::Zero(nChannels, nTimes);
152 for (
int s = 0; s < nSubjects; ++s) {
153 MatrixXd diff = dataA[s] - dataB[s];
155 sumDiffSq += diff.cwiseProduct(diff);
158 double n =
static_cast<double>(nSubjects);
159 MatrixXd mean = sumDiff / n;
160 MatrixXd variance = (sumDiffSq - sumDiff.cwiseProduct(sumDiff) / n) / (n - 1.0);
163 variance = variance.cwiseMax(1.0e-30);
165 MatrixXd tMap = mean.array() / (variance.array().sqrt() / std::sqrt(n));
171QPair<MatrixXi, QVector<double>> StatsCluster::findClusters(
172 const MatrixXd& tMap,
174 const SparseMatrix<int>& adjacency,
177 const int nChannels =
static_cast<int>(tMap.rows());
178 const int nTimes =
static_cast<int>(tMap.cols());
180 MatrixXi clusterIds = MatrixXi::Zero(nChannels, nTimes);
181 QVector<double> clusterStats;
182 int currentClusterId = 0;
185 auto bfsClusters = [&](
bool positive) {
187 MatrixXi visited = MatrixXi::Zero(nChannels, nTimes);
189 for (
int ch = 0; ch < nChannels; ++ch) {
190 for (
int t = 0; t < nTimes; ++t) {
191 double val = tMap(ch, t);
192 bool suprathreshold = positive ? (val > threshold) : (val < -threshold);
194 if (!suprathreshold || visited(ch, t))
199 double clusterSum = 0.0;
200 std::queue<std::pair<int, int>> queue;
204 while (!queue.empty()) {
205 auto [curCh, curT] = queue.front();
208 clusterIds(curCh, curT) = positive ? currentClusterId : -currentClusterId;
209 clusterSum += tMap(curCh, curT);
212 for (
int dt = -1; dt <= 1; dt += 2) {
214 if (nt < 0 || nt >= nTimes)
216 double nval = tMap(curCh, nt);
217 bool nSupra = positive ? (nval > threshold) : (nval < -threshold);
218 if (nSupra && !visited(curCh, nt)) {
219 visited(curCh, nt) = 1;
220 queue.push({curCh, nt});
225 for (SparseMatrix<int>::InnerIterator it(adjacency, curCh); it; ++it) {
226 int nCh =
static_cast<int>(it.row());
227 double nval = tMap(nCh, curT);
228 bool nSupra = positive ? (nval > threshold) : (nval < -threshold);
229 if (nSupra && !visited(nCh, curT)) {
230 visited(nCh, curT) = 1;
231 queue.push({nCh, curT});
236 clusterStats.append(clusterSum);
248 return {clusterIds, clusterStats};
253double StatsCluster::permuteOnce(
254 const QVector<MatrixXd>& allData,
256 const SparseMatrix<int>& adjacency,
260 const int nTotal = allData.size();
263 std::vector<int> indices(nTotal);
264 std::iota(indices.begin(), indices.end(), 0);
267 QRandomGenerator rng(QRandomGenerator::global()->generate());
268 for (
int i = nTotal - 1; i > 0; --i) {
269 int j =
static_cast<int>(rng.bounded(i + 1));
270 std::swap(indices[i], indices[j]);
274 QVector<MatrixXd> permA, permB;
276 permB.reserve(nTotal - nA);
277 for (
int i = 0; i < nTotal; ++i) {
279 permA.append(allData[indices[i]]);
281 permB.append(allData[indices[i]]);
286 MatrixXd tMap = computeTMap(permA, permB);
287 auto [clusterIds, clusterStats] = findClusters(tMap, threshold, adjacency, tail);
290 double maxStat = 0.0;
291 for (
double s : clusterStats) {
292 double absS = std::fabs(s);
293 if (absS > maxStat) {
302double StatsCluster::inverseTCdf(
double p,
int df)
309 double targetCdf = 1.0 - p / 2.0;
314 for (
int iter = 0; iter < 100; ++iter) {
315 double mid = (lo + hi) / 2.0;
317 if (cdf < targetCdf) {
324 return (lo + hi) / 2.0;
329MatrixXd StatsCluster::computeOneSampleTMap(
const QVector<MatrixXd>& data)
331 const int nSubjects = data.size();
332 const int nVertices =
static_cast<int>(data[0].rows());
333 const int nTimes =
static_cast<int>(data[0].cols());
335 MatrixXd sum = MatrixXd::Zero(nVertices, nTimes);
336 MatrixXd sumSq = MatrixXd::Zero(nVertices, nTimes);
338 for (
int s = 0; s < nSubjects; ++s) {
340 sumSq += data[s].cwiseProduct(data[s]);
343 double n =
static_cast<double>(nSubjects);
344 MatrixXd mean = sum / n;
345 MatrixXd variance = (sumSq - sum.cwiseProduct(sum) / n) / (n - 1.0);
346 variance = variance.cwiseMax(1.0e-30);
348 MatrixXd tMap = mean.array() / (variance.array().sqrt() / std::sqrt(n));
354MatrixXd StatsCluster::computeFMap(
const QVector<QVector<MatrixXd>>& conditions)
356 const int nConditions = conditions.size();
357 const int nVertices =
static_cast<int>(conditions[0][0].rows());
358 const int nTimes =
static_cast<int>(conditions[0][0].cols());
362 for (
int c = 0; c < nConditions; ++c) {
363 nTotal += conditions[c].size();
367 MatrixXd grandSum = MatrixXd::Zero(nVertices, nTimes);
368 for (
int c = 0; c < nConditions; ++c) {
369 for (
int s = 0; s < conditions[c].size(); ++s) {
370 grandSum += conditions[c][s];
373 MatrixXd grandMean = grandSum /
static_cast<double>(nTotal);
376 MatrixXd ssBetween = MatrixXd::Zero(nVertices, nTimes);
377 MatrixXd ssWithin = MatrixXd::Zero(nVertices, nTimes);
379 for (
int c = 0; c < nConditions; ++c) {
380 int nc = conditions[c].size();
381 MatrixXd condSum = MatrixXd::Zero(nVertices, nTimes);
382 for (
int s = 0; s < nc; ++s) {
383 condSum += conditions[c][s];
385 MatrixXd condMean = condSum /
static_cast<double>(nc);
387 MatrixXd diff = condMean - grandMean;
388 ssBetween +=
static_cast<double>(nc) * diff.cwiseProduct(diff);
390 for (
int s = 0; s < nc; ++s) {
391 MatrixXd residual = conditions[c][s] - condMean;
392 ssWithin += residual.cwiseProduct(residual);
396 int dfBetween = nConditions - 1;
397 int dfWithin = nTotal - nConditions;
399 MatrixXd msBetween = ssBetween /
static_cast<double>(dfBetween);
400 MatrixXd msWithin = ssWithin /
static_cast<double>(dfWithin);
401 msWithin = msWithin.cwiseMax(1.0e-30);
403 MatrixXd fMap = msBetween.array() / msWithin.array();
409QPair<MatrixXi, QVector<double>> StatsCluster::findClustersFlat(
410 const MatrixXd& statMap,
412 const SparseMatrix<int>& adjacency,
415 const int nVertices =
static_cast<int>(statMap.rows());
416 const int nTimes =
static_cast<int>(statMap.cols());
417 const int nTotal = nVertices * nTimes;
419 MatrixXi clusterIds = MatrixXi::Zero(nVertices, nTimes);
420 QVector<double> clusterStats;
421 int currentClusterId = 0;
424 std::vector<bool> visited(nTotal,
false);
426 for (
int idx = 0; idx < nTotal; ++idx) {
427 int v = idx / nTimes;
428 int t = idx % nTimes;
429 double val = statMap(v, t);
431 bool suprathreshold = positiveOnly ? (val > threshold) : (val < -threshold);
432 if (!suprathreshold || visited[idx])
436 double clusterSum = 0.0;
437 std::queue<int> queue;
441 while (!queue.empty()) {
442 int curIdx = queue.front();
445 int curV = curIdx / nTimes;
446 int curT = curIdx % nTimes;
448 clusterIds(curV, curT) = positiveOnly ? currentClusterId : -currentClusterId;
449 clusterSum += statMap(curV, curT);
452 for (SparseMatrix<int>::InnerIterator it(adjacency, curIdx); it; ++it) {
453 int nIdx = static_cast<int>(it.row());
456 int nV = nIdx / nTimes;
457 int nT = nIdx % nTimes;
458 double nval = statMap(nV, nT);
459 bool nSupra = positiveOnly ? (nval > threshold) : (nval < -threshold);
461 visited[nIdx] = true;
467 clusterStats.append(clusterSum);
470 return {clusterIds, clusterStats};
475double StatsCluster::permuteOnceOneSample(
476 const QVector<MatrixXd>& data,
477 const SparseMatrix<int>& adjacency,
481 const int nSubjects = data.size();
484 QRandomGenerator rng(QRandomGenerator::global()->generate());
485 QVector<int> signs(nSubjects);
486 for (
int s = 0; s < nSubjects; ++s) {
487 signs[s] = rng.bounded(2) == 0 ? 1 : -1;
491 QVector<MatrixXd> flipped(nSubjects);
492 for (
int s = 0; s < nSubjects; ++s) {
493 flipped[s] = data[s] *
static_cast<double>(signs[s]);
497 MatrixXd tMap = computeOneSampleTMap(flipped);
499 double maxStat = 0.0;
502 auto [ids, stats] = findClustersFlat(tMap, threshold, adjacency,
true);
503 for (
double s : stats) {
504 if (std::fabs(s) > maxStat)
505 maxStat = std::fabs(s);
509 auto [ids, stats] = findClustersFlat(tMap, threshold, adjacency,
false);
510 for (
double s : stats) {
511 if (std::fabs(s) > maxStat)
512 maxStat = std::fabs(s);
521double StatsCluster::permuteOnceFTest(
522 const QVector<MatrixXd>& allData,
523 const QVector<int>& groupSizes,
524 const SparseMatrix<int>& adjacency,
527 const int nTotal = allData.size();
530 std::vector<int> indices(nTotal);
531 std::iota(indices.begin(), indices.end(), 0);
533 QRandomGenerator rng(QRandomGenerator::global()->generate());
534 for (
int i = nTotal - 1; i > 0; --i) {
535 int j =
static_cast<int>(rng.bounded(i + 1));
536 std::swap(indices[i], indices[j]);
540 QVector<QVector<MatrixXd>> permConditions;
542 for (
int c = 0; c < groupSizes.size(); ++c) {
543 QVector<MatrixXd> group;
544 group.reserve(groupSizes[c]);
545 for (
int s = 0; s < groupSizes[c]; ++s) {
546 group.append(allData[indices[offset + s]]);
548 permConditions.append(group);
549 offset += groupSizes[c];
553 MatrixXd fMap = computeFMap(permConditions);
554 auto [ids, stats] = findClustersFlat(fMap, threshold, adjacency,
true);
556 double maxStat = 0.0;
557 for (
double s : stats) {
567 const QVector<MatrixXd>& data,
568 const SparseMatrix<int>& adjacency,
574 MatrixXd tObs = computeOneSampleTMap(data);
577 MatrixXi clusterIdsPos, clusterIdsNeg;
578 QVector<double> clusterStats;
581 auto [ids, stats] = findClustersFlat(tObs, threshold, adjacency,
true);
583 clusterStats.append(stats);
586 auto [ids, stats] = findClustersFlat(tObs, threshold, adjacency,
false);
588 clusterStats.append(stats);
592 MatrixXi clusterIds = MatrixXi::Zero(tObs.rows(), tObs.cols());
593 if (clusterIdsPos.size() > 0)
594 clusterIds += clusterIdsPos;
595 if (clusterIdsNeg.size() > 0)
596 clusterIds += clusterIdsNeg;
599 QVector<int> permIndices(nPermutations);
600 std::iota(permIndices.begin(), permIndices.end(), 0);
602 std::function<double(
int)> permuteFunc = [&](int) ->
double {
603 return permuteOnceOneSample(data, adjacency, threshold, tail);
606 QFuture<double> future = QtConcurrent::mapped(permIndices, permuteFunc);
607 future.waitForFinished();
609 QVector<double> nullDist(nPermutations);
610 for (
int i = 0; i < nPermutations; ++i) {
611 nullDist[i] = future.resultAt(i);
613 std::sort(nullDist.begin(), nullDist.end());
616 QVector<double> clusterPvals(clusterStats.size());
617 for (
int c = 0; c < clusterStats.size(); ++c) {
618 double obsStat = std::fabs(clusterStats[c]);
620 for (
int p = 0; p < nPermutations; ++p) {
621 if (nullDist[p] >= obsStat) {
625 clusterPvals[c] =
static_cast<double>(count + 1) /
static_cast<double>(nPermutations + 1);
640 const QVector<QVector<MatrixXd>>& conditions,
641 const SparseMatrix<int>& adjacency,
646 MatrixXd fObs = computeFMap(conditions);
649 auto [clusterIds, clusterStats] = findClustersFlat(fObs, threshold, adjacency,
true);
652 QVector<MatrixXd> allData;
653 QVector<int> groupSizes;
654 for (
int c = 0; c < conditions.size(); ++c) {
655 groupSizes.append(conditions[c].size());
656 for (
int s = 0; s < conditions[c].size(); ++s) {
657 allData.append(conditions[c][s]);
662 QVector<int> permIndices(nPermutations);
663 std::iota(permIndices.begin(), permIndices.end(), 0);
665 std::function<double(
int)> permuteFunc = [&](int) ->
double {
666 return permuteOnceFTest(allData, groupSizes, adjacency, threshold);
669 QFuture<double> future = QtConcurrent::mapped(permIndices, permuteFunc);
670 future.waitForFinished();
672 QVector<double> nullDist(nPermutations);
673 for (
int i = 0; i < nPermutations; ++i) {
674 nullDist[i] = future.resultAt(i);
676 std::sort(nullDist.begin(), nullDist.end());
679 QVector<double> clusterPvals(clusterStats.size());
680 for (
int c = 0; c < clusterStats.size(); ++c) {
681 double obsStat = clusterStats[c];
683 for (
int p = 0; p < nPermutations; ++p) {
684 if (nullDist[p] >= obsStat) {
688 clusterPvals[c] =
static_cast<double>(count + 1) /
static_cast<double>(nPermutations + 1);
703 const MatrixXd& statMap,
704 const SparseMatrix<int>& adjacency,
709 const int nVertices =
static_cast<int>(statMap.rows());
710 const int nTimes =
static_cast<int>(statMap.cols());
711 const int nTotal = nVertices * nTimes;
713 MatrixXd tfceMap = MatrixXd::Zero(nVertices, nTimes);
716 auto runTfce = [&](
const MatrixXd& absMap,
bool isPositive) {
717 double maxVal = absMap.maxCoeff();
721 double dh = maxVal /
static_cast<double>(nSteps);
723 for (
int step = 1; step <= nSteps; ++step) {
724 double h = dh *
static_cast<double>(step);
727 std::vector<bool> visited(nTotal,
false);
729 for (
int idx = 0; idx < nTotal; ++idx) {
730 int v = idx / nTimes;
731 int t = idx % nTimes;
733 if (absMap(v, t) < h || visited[idx])
737 std::vector<int> cluster;
738 std::queue<int> queue;
742 while (!queue.empty()) {
743 int curIdx = queue.front();
745 cluster.push_back(curIdx);
747 for (SparseMatrix<int>::InnerIterator it(adjacency, curIdx); it; ++it) {
748 int nIdx =
static_cast<int>(it.row());
751 int nV = nIdx / nTimes;
752 int nT = nIdx % nTimes;
753 if (absMap(nV, nT) >= h) {
754 visited[nIdx] =
true;
761 double extent =
static_cast<double>(cluster.size());
762 double contribution = std::pow(extent, E) * std::pow(h, H) * dh;
764 for (
int cIdx : cluster) {
765 int cv = cIdx / nTimes;
766 int ct = cIdx % nTimes;
768 tfceMap(cv, ct) += contribution;
770 tfceMap(cv, ct) -= contribution;
778 MatrixXd posMap = statMap.cwiseMax(0.0);
779 runTfce(posMap,
true);
782 MatrixXd negMap = (-statMap).cwiseMax(0.0);
783 runTfce(negMap,
false);
Maris-Oostenveld cluster-mass permutation tests and Threshold-Free Cluster Enhancement for M/EEG infe...
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.
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)