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)
135 iRank = 1; // at least 1
136 }
137 iRank = std::min(iRank, p);
138
139 // Zero out eigenvalues below rank (keep top-k)
140 // Eigenvalues are in ascending order, so zero out [0, p-iRank)
141 for (int i = 0; i < p - iRank; ++i) {
142 evals(i) = 0.0;
143 }
144
145 // Reconstruct: V * diag(evals) * V^T
146 MatrixXd covPca = evecs * evals.asDiagonal() * evecs.transpose();
147
148 return {covPca, static_cast<double>(iRank)};
149}
150
151//=============================================================================================================
152
153std::pair<MatrixXd, double> StsCovEstimators::factorAnalysis(const MatrixXd& matData,
154 int iNFactors,
155 int iMaxIter,
156 double dTol)
157{
158 // matData is (p × n), already zero-centred; skigen expects (n × p)
159 Skigen::FactorAnalysis<double> fa(iNFactors, iMaxIter, dTol);
160 fa.fit(matData.transpose());
161 return {fa.covariance(), fa.log_likelihood()};
162}
163
164//=============================================================================================================
165
166double StsCovEstimators::gaussianLogLikelihood(const MatrixXd& matTestData,
167 const MatrixXd& matCov)
168{
169 const int p = static_cast<int>(matTestData.rows());
170 const int n = static_cast<int>(matTestData.cols());
171
172 // Eigen decomposition of covariance
173 SelfAdjointEigenSolver<MatrixXd> solver(matCov);
174 VectorXd evals = solver.eigenvalues().array().max(1e-30);
175 MatrixXd evecs = solver.eigenvectors();
176
177 // log|Σ|
178 double logDet = evals.array().log().sum();
179
180 // Σ^{-1}
181 MatrixXd covInv = evecs * evals.array().inverse().matrix().asDiagonal() * evecs.transpose();
182
183 // Test sample covariance (1/n)
184 MatrixXd Stest = (matTestData * matTestData.transpose()) / static_cast<double>(n);
185
186 // trace(Σ^{-1} * S_test)
187 double trInvS = (covInv * Stest).trace();
188
189 // Average log-likelihood per sample
190 return -0.5 * (static_cast<double>(p) * std::log(2.0 * M_PI) + logDet + trInvS);
191}
192
193//=============================================================================================================
194
195std::pair<MatrixXd, double> StsCovEstimators::autoSelect(const MatrixXd& matData,
196 int iNFolds)
197{
198 const int n = static_cast<int>(matData.cols());
199
200 if (iNFolds < 2)
201 iNFolds = 2;
202 if (iNFolds > n)
203 iNFolds = n;
204
205 // Create fold indices (simple sequential split)
206 std::vector<int> indices(static_cast<size_t>(n));
207 std::iota(indices.begin(), indices.end(), 0);
208
209 // Shuffle for randomised folds
210 std::mt19937 gen(42);
211 std::shuffle(indices.begin(), indices.end(), gen);
212
213 // Method names for indexing: 0=empirical, 1=shrunk, 2=oas, 3=diag_fixed, 4=pca, 5=fa
214 const int nMethods = 6;
215 std::vector<double> avgLL(static_cast<size_t>(nMethods), 0.0);
216
217 const int foldSize = n / iNFolds;
218
219 for (int fold = 0; fold < iNFolds; ++fold) {
220 // Split into train and test
221 int testStart = fold * foldSize;
222 int testEnd = (fold == iNFolds - 1) ? n : (fold + 1) * foldSize;
223 int nTest = testEnd - testStart;
224 int nTrain = n - nTest;
225
226 MatrixXd trainData(matData.rows(), nTrain);
227 MatrixXd testData(matData.rows(), nTest);
228
229 int trainIdx = 0;
230 int testIdx = 0;
231 for (int i = 0; i < n; ++i) {
232 int col = indices[static_cast<size_t>(i)];
233 if (i >= testStart && i < testEnd) {
234 testData.col(testIdx++) = matData.col(col);
235 } else {
236 trainData.col(trainIdx++) = matData.col(col);
237 }
238 }
239
240 // Zero-mean train and test independently
241 trainData.colwise() -= trainData.rowwise().mean();
242 testData.colwise() -= testData.rowwise().mean();
243
244 // Fit each method on train, evaluate on test
245 // 0: empirical
246 {
247 MatrixXd cov = (trainData * trainData.transpose()) / static_cast<double>(nTrain);
248 // Small regularisation to avoid singular matrix
249 cov.diagonal().array() += 1e-10 * cov.trace() / static_cast<double>(cov.rows());
250 avgLL[0] += gaussianLogLikelihood(testData, cov);
251 }
252 // 1: shrunk (Ledoit-Wolf)
253 {
254 auto [cov, alpha] = ledoitWolf(trainData);
255 avgLL[1] += gaussianLogLikelihood(testData, cov);
256 }
257 // 2: OAS
258 {
259 auto [cov, rho] = oas(trainData);
260 avgLL[2] += gaussianLogLikelihood(testData, cov);
261 }
262 // 3: diagonal_fixed
263 {
264 auto [cov, reg] = diagonalFixed(trainData);
265 avgLL[3] += gaussianLogLikelihood(testData, cov);
266 }
267 // 4: PCA
268 {
269 auto [cov, rank] = pca(trainData);
270 // PCA can produce singular matrix — regularise for LL computation
271 cov.diagonal().array() += 1e-10 * cov.trace() / static_cast<double>(cov.rows());
272 avgLL[4] += gaussianLogLikelihood(testData, cov);
273 }
274 // 5: Factor Analysis
275 {
276 auto [cov, ll] = factorAnalysis(trainData);
277 avgLL[5] += gaussianLogLikelihood(testData, cov);
278 }
279 }
280
281 // Average across folds
282 for (int m = 0; m < nMethods; ++m) {
283 avgLL[static_cast<size_t>(m)] /= static_cast<double>(iNFolds);
284 }
285
286 // Find best method
287 int bestMethod = 0;
288 double bestLL = avgLL[0];
289 for (int m = 1; m < nMethods; ++m) {
290 if (avgLL[static_cast<size_t>(m)] > bestLL) {
291 bestLL = avgLL[static_cast<size_t>(m)];
292 bestMethod = m;
293 }
294 }
295
296 // Re-fit best method on full data
297 std::pair<MatrixXd, double> result;
298 switch (bestMethod) {
299 case 0: {
300 MatrixXd cov = (matData * matData.transpose()) / static_cast<double>(n);
301 cov.diagonal().array() += 1e-10 * cov.trace() / static_cast<double>(cov.rows());
302 result = {cov, static_cast<double>(bestMethod)};
303 break;
304 }
305 case 1:
306 result = ledoitWolf(matData);
307 result.second = static_cast<double>(bestMethod);
308 break;
309 case 2:
310 result = oas(matData);
311 result.second = static_cast<double>(bestMethod);
312 break;
313 case 3:
314 result = diagonalFixed(matData);
315 result.second = static_cast<double>(bestMethod);
316 break;
317 case 4:
318 result = pca(matData);
319 result.second = static_cast<double>(bestMethod);
320 break;
321 case 5:
322 result = factorAnalysis(matData);
323 result.second = static_cast<double>(bestMethod);
324 break;
325 default:
326 result = ledoitWolf(matData);
327 result.second = 1.0;
328 break;
329 }
330
331 return result;
332}
Eigen::Matrix3f S
#define M_PI
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.