v2.0.0
Loading...
Searching...
No Matches
sts_cov_estimators.cpp
Go to the documentation of this file.
1//=============================================================================================================
32
33//=============================================================================================================
34// INCLUDES
35//=============================================================================================================
36
37#include "sts_cov_estimators.h"
38
39//=============================================================================================================
40// SKIGEN INCLUDES
41//=============================================================================================================
42
43#include <Skigen/Covariance>
44#include <Skigen/Decomposition>
45
46//=============================================================================================================
47// STL INCLUDES
48//=============================================================================================================
49
50#include <algorithm>
51#include <cmath>
52#include <numeric>
53#include <random>
54#include <vector>
55
56//=============================================================================================================
57// EIGEN INCLUDES
58//=============================================================================================================
59
60#include <Eigen/Eigenvalues>
61
62//=============================================================================================================
63// USED NAMESPACES
64//=============================================================================================================
65
66using namespace STSLIB;
67using namespace Eigen;
68
69//=============================================================================================================
70// DEFINE MEMBER METHODS
71//=============================================================================================================
72
73std::pair<MatrixXd, double> StsCovEstimators::ledoitWolf(const MatrixXd& matData)
74{
75 // matData is (p × n), already zero-centred; skigen expects (n × p)
76 Skigen::LedoitWolf<double> lw(/*assume_centered=*/true);
77 lw.fit(matData.transpose());
78 return {lw.covariance(), lw.shrinkage()};
79}
80
81//=============================================================================================================
82
83std::pair<MatrixXd, double> StsCovEstimators::oas(const MatrixXd& matData)
84{
85 // matData is (p × n), already zero-centred; skigen expects (n × p)
86 Skigen::OAS<double> oas_est(/*assume_centered=*/true);
87 oas_est.fit(matData.transpose());
88 return {oas_est.covariance(), oas_est.shrinkage()};
89}
90
91//=============================================================================================================
92
93std::pair<MatrixXd, double> StsCovEstimators::diagonalFixed(const MatrixXd& matData,
94 double dReg)
95{
96 const int p = static_cast<int>(matData.rows());
97 const int n = static_cast<int>(matData.cols());
98
99 // Sample covariance (1/n)
100 MatrixXd cov = (matData * matData.transpose()) / static_cast<double>(n);
101
102 // Add dReg * mean_eigenvalue * I (mean_eigenvalue = trace / p)
103 const double meanEig = cov.trace() / static_cast<double>(p);
104 cov.diagonal().array() += dReg * meanEig;
105
106 return {cov, dReg};
107}
108
109//=============================================================================================================
110
111std::pair<MatrixXd, double> StsCovEstimators::pca(const MatrixXd& matData,
112 int iRank)
113{
114 const int p = static_cast<int>(matData.rows());
115 const int n = static_cast<int>(matData.cols());
116
117 // Sample covariance (1/n)
118 const MatrixXd S = (matData * matData.transpose()) / static_cast<double>(n);
119
120 // Eigen decomposition (self-adjoint → eigenvalues in ascending order)
121 SelfAdjointEigenSolver<MatrixXd> solver(S);
122 VectorXd evals = solver.eigenvalues();
123 MatrixXd evecs = solver.eigenvectors();
124
125 // Auto-detect rank: count eigenvalues > max_eval * 1e-10
126 if (iRank <= 0) {
127 const double maxEval = evals.maxCoeff();
128 const double threshold = maxEval * 1e-10;
129 iRank = 0;
130 for (int i = 0; i < p; ++i) {
131 if (evals(i) > threshold)
132 ++iRank;
133 }
134 if (iRank == 0) iRank = 1; // at least 1
135 }
136 iRank = std::min(iRank, p);
137
138 // Zero out eigenvalues below rank (keep top-k)
139 // Eigenvalues are in ascending order, so zero out [0, p-iRank)
140 for (int i = 0; i < p - iRank; ++i) {
141 evals(i) = 0.0;
142 }
143
144 // Reconstruct: V * diag(evals) * V^T
145 MatrixXd covPca = evecs * evals.asDiagonal() * evecs.transpose();
146
147 return {covPca, static_cast<double>(iRank)};
148}
149
150//=============================================================================================================
151
152std::pair<MatrixXd, double> StsCovEstimators::factorAnalysis(const MatrixXd& matData,
153 int iNFactors,
154 int iMaxIter,
155 double dTol)
156{
157 // matData is (p × n), already zero-centred; skigen expects (n × p)
158 Skigen::FactorAnalysis<double> fa(iNFactors, iMaxIter, dTol);
159 fa.fit(matData.transpose());
160 return {fa.covariance(), fa.log_likelihood()};
161}
162
163//=============================================================================================================
164
165double StsCovEstimators::gaussianLogLikelihood(const MatrixXd& matTestData,
166 const MatrixXd& matCov)
167{
168 const int p = static_cast<int>(matTestData.rows());
169 const int n = static_cast<int>(matTestData.cols());
170
171 // Eigen decomposition of covariance
172 SelfAdjointEigenSolver<MatrixXd> solver(matCov);
173 VectorXd evals = solver.eigenvalues().array().max(1e-30);
174 MatrixXd evecs = solver.eigenvectors();
175
176 // log|Σ|
177 double logDet = evals.array().log().sum();
178
179 // Σ^{-1}
180 MatrixXd covInv = evecs * evals.array().inverse().matrix().asDiagonal() * evecs.transpose();
181
182 // Test sample covariance (1/n)
183 MatrixXd Stest = (matTestData * matTestData.transpose()) / static_cast<double>(n);
184
185 // trace(Σ^{-1} * S_test)
186 double trInvS = (covInv * Stest).trace();
187
188 // Average log-likelihood per sample
189 return -0.5 * (static_cast<double>(p) * std::log(2.0 * M_PI) + logDet + trInvS);
190}
191
192//=============================================================================================================
193
194std::pair<MatrixXd, double> StsCovEstimators::autoSelect(const MatrixXd& matData,
195 int iNFolds)
196{
197 const int n = static_cast<int>(matData.cols());
198
199 if (iNFolds < 2) iNFolds = 2;
200 if (iNFolds > n) iNFolds = n;
201
202 // Create fold indices (simple sequential split)
203 std::vector<int> indices(static_cast<size_t>(n));
204 std::iota(indices.begin(), indices.end(), 0);
205
206 // Shuffle for randomised folds
207 std::mt19937 gen(42);
208 std::shuffle(indices.begin(), indices.end(), gen);
209
210 // Method names for indexing: 0=empirical, 1=shrunk, 2=oas, 3=diag_fixed, 4=pca, 5=fa
211 const int nMethods = 6;
212 std::vector<double> avgLL(static_cast<size_t>(nMethods), 0.0);
213
214 const int foldSize = n / iNFolds;
215
216 for (int fold = 0; fold < iNFolds; ++fold) {
217 // Split into train and test
218 int testStart = fold * foldSize;
219 int testEnd = (fold == iNFolds - 1) ? n : (fold + 1) * foldSize;
220 int nTest = testEnd - testStart;
221 int nTrain = n - nTest;
222
223 MatrixXd trainData(matData.rows(), nTrain);
224 MatrixXd testData(matData.rows(), nTest);
225
226 int trainIdx = 0;
227 int testIdx = 0;
228 for (int i = 0; i < n; ++i) {
229 int col = indices[static_cast<size_t>(i)];
230 if (i >= testStart && i < testEnd) {
231 testData.col(testIdx++) = matData.col(col);
232 } else {
233 trainData.col(trainIdx++) = matData.col(col);
234 }
235 }
236
237 // Zero-mean train and test independently
238 trainData.colwise() -= trainData.rowwise().mean();
239 testData.colwise() -= testData.rowwise().mean();
240
241 // Fit each method on train, evaluate on test
242 // 0: empirical
243 {
244 MatrixXd cov = (trainData * trainData.transpose()) / static_cast<double>(nTrain);
245 // Small regularisation to avoid singular matrix
246 cov.diagonal().array() += 1e-10 * cov.trace() / static_cast<double>(cov.rows());
247 avgLL[0] += gaussianLogLikelihood(testData, cov);
248 }
249 // 1: shrunk (Ledoit-Wolf)
250 {
251 auto [cov, alpha] = ledoitWolf(trainData);
252 avgLL[1] += gaussianLogLikelihood(testData, cov);
253 }
254 // 2: OAS
255 {
256 auto [cov, rho] = oas(trainData);
257 avgLL[2] += gaussianLogLikelihood(testData, cov);
258 }
259 // 3: diagonal_fixed
260 {
261 auto [cov, reg] = diagonalFixed(trainData);
262 avgLL[3] += gaussianLogLikelihood(testData, cov);
263 }
264 // 4: PCA
265 {
266 auto [cov, rank] = pca(trainData);
267 // PCA can produce singular matrix — regularise for LL computation
268 cov.diagonal().array() += 1e-10 * cov.trace() / static_cast<double>(cov.rows());
269 avgLL[4] += gaussianLogLikelihood(testData, cov);
270 }
271 // 5: Factor Analysis
272 {
273 auto [cov, ll] = factorAnalysis(trainData);
274 avgLL[5] += gaussianLogLikelihood(testData, cov);
275 }
276 }
277
278 // Average across folds
279 for (int m = 0; m < nMethods; ++m) {
280 avgLL[static_cast<size_t>(m)] /= static_cast<double>(iNFolds);
281 }
282
283 // Find best method
284 int bestMethod = 0;
285 double bestLL = avgLL[0];
286 for (int m = 1; m < nMethods; ++m) {
287 if (avgLL[static_cast<size_t>(m)] > bestLL) {
288 bestLL = avgLL[static_cast<size_t>(m)];
289 bestMethod = m;
290 }
291 }
292
293 // Re-fit best method on full data
294 std::pair<MatrixXd, double> result;
295 switch (bestMethod) {
296 case 0: {
297 MatrixXd cov = (matData * matData.transpose()) / static_cast<double>(n);
298 cov.diagonal().array() += 1e-10 * cov.trace() / static_cast<double>(cov.rows());
299 result = {cov, static_cast<double>(bestMethod)};
300 break;
301 }
302 case 1: result = ledoitWolf(matData); result.second = static_cast<double>(bestMethod); break;
303 case 2: result = oas(matData); result.second = static_cast<double>(bestMethod); break;
304 case 3: result = diagonalFixed(matData); result.second = static_cast<double>(bestMethod); break;
305 case 4: result = pca(matData); result.second = static_cast<double>(bestMethod); break;
306 case 5: result = factorAnalysis(matData); result.second = static_cast<double>(bestMethod); break;
307 default: result = ledoitWolf(matData); result.second = 1.0; break;
308 }
309
310 return result;
311}
#define M_PI
Eigen::Matrix3f S
Regularised covariance estimators for M/EEG noise covariances, matching MNE-Python's compute_covarian...
Statistical testing (t-tests, F-tests, cluster permutation, multiple comparison correction).
static std::pair< Eigen::MatrixXd, double > diagonalFixed(const Eigen::MatrixXd &matData, double dReg=0.1)
Fixed diagonal regularisation.
static std::pair< Eigen::MatrixXd, double > ledoitWolf(const Eigen::MatrixXd &matData)
Ledoit-Wolf optimal shrinkage covariance estimator.
static std::pair< Eigen::MatrixXd, double > autoSelect(const Eigen::MatrixXd &matData, int iNFolds=3)
Auto-select the best covariance estimator via cross-validation.
static std::pair< Eigen::MatrixXd, double > pca(const Eigen::MatrixXd &matData, int iRank=0)
PCA-based rank-reduced covariance estimator.
static std::pair< Eigen::MatrixXd, double > factorAnalysis(const Eigen::MatrixXd &matData, int iNFactors=0, int iMaxIter=200, double dTol=1e-6)
Factor Analysis covariance estimator via EM algorithm.
static double gaussianLogLikelihood(const Eigen::MatrixXd &matTestData, const Eigen::MatrixXd &matCov)
Gaussian log-likelihood of held-out data given a covariance model.
static std::pair< Eigen::MatrixXd, double > oas(const Eigen::MatrixXd &matData)
Oracle Approximating Shrinkage (OAS) covariance estimator.