|
1 | 1 | function result = granger(x, y, fs, varargin) |
2 | | -% GRANGER Bivariate Granger causality between two time series |
| 2 | +% GRANGER Bivariate Granger causality between two fNIRS time series |
3 | 3 | % |
4 | | -% Tests whether past values of x improve the prediction of y beyond |
5 | | -% what past values of y alone provide. Uses autoregressive modeling |
6 | | -% with an F-test on residual variance reduction. |
| 4 | +% Tests whether past values of x improve the prediction of y beyond what |
| 5 | +% past values of y alone provide. Uses OLS lag-regression with an F-test on |
| 6 | +% residual variance reduction (Granger 1969, Geweke 1982). |
| 7 | +% |
| 8 | +% IMPORTANT CAVEAT: Granger causality on fNIRS signals does NOT establish |
| 9 | +% neural causality. Direction inferred from the F-statistic can be manufactured |
| 10 | +% by (1) differences in hemodynamic response latency between brain regions |
| 11 | +% (the region with the faster HRF appears to "cause" the slower one even with |
| 12 | +% simultaneous neural activity), (2) a common systemic or physiological driver |
| 13 | +% (cardiac pulsation, Mayer waves, respiration, global signal) that reaches |
| 14 | +% different channels with different temporal profiles, and (3) spatially |
| 15 | +% structured shared noise (motion artifacts, superficial physiology). Interpret |
| 16 | +% results as directed temporal predictability within the measured haemodynamic |
| 17 | +% signal, not as directional neural influence. |
| 18 | +% |
| 19 | +% AR model order selection: by default ModelOrder=5 (fixed). Set |
| 20 | +% ModelOrder='auto' to select the order by BIC over 1..round(fs/2), matching |
| 21 | +% the approach in fitGLM. BIC is preferred over AIC at fNIRS run lengths to |
| 22 | +% avoid overfitting (Schwarz 1978). In sliding-window mode with 'auto', the |
| 23 | +% order is selected on the full signal first and then applied uniformly to |
| 24 | +% all windows so that windowed F-statistics are comparable. The selected order |
| 25 | +% is stored in result.modelOrder. |
| 26 | +% |
| 27 | +% References: |
| 28 | +% Granger, C. W. J. (1969). Investigating causal relations by econometric |
| 29 | +% models and cross-spectral methods. Econometrica, 37(3), 424-438. |
| 30 | +% |
| 31 | +% Geweke, J. (1982). Measurement of linear dependence and feedback between |
| 32 | +% multiple time series. Journal of the American Statistical Association, |
| 33 | +% 77(378), 304-313. |
| 34 | +% |
| 35 | +% Schwarz, G. (1978). Estimating the Dimension of a Model. The Annals of |
| 36 | +% Statistics, 6(2), 461-464. DOI: 10.1214/aos/1176344136 |
| 37 | +% |
| 38 | +% Akaike, H. (1974). A new look at the statistical model identification. |
| 39 | +% IEEE Transactions on Automatic Control, 19(6), 716-723. |
| 40 | +% DOI: 10.1109/tac.1974.1100705 |
7 | 41 | % |
8 | 42 | % Syntax: |
9 | 43 | % result = exploreFNIRS.coupling.granger(x, y, fs) |
10 | 44 | % result = exploreFNIRS.coupling.granger(x, y, fs, 'ModelOrder', 10) |
| 45 | +% result = exploreFNIRS.coupling.granger(x, y, fs, 'ModelOrder', 'auto') |
11 | 46 | % result = exploreFNIRS.coupling.granger(x, y, fs, 'WindowSize', 30) |
12 | 47 | % |
13 | 48 | % Inputs: |
14 | 49 | % x - [T x 1] time series (candidate cause) |
15 | 50 | % y - [T x 1] time series (candidate effect) |
16 | | -% fs - Sampling frequency (Hz) |
| 51 | +% fs - Sampling frequency in Hz [scalar] |
17 | 52 | % |
18 | 53 | % Name-Value Parameters: |
19 | | -% ModelOrder - Number of lags for AR model (default: 5) |
20 | | -% WindowSize - Sliding window duration in seconds (default: 0, full signal) |
21 | | -% WindowStep - Step size in seconds (default: WindowSize/2, 50% overlap) |
| 54 | +% 'ModelOrder' - Number of lags for the AR model (default: 5) |
| 55 | +% May also be the string 'auto', in which case BIC is |
| 56 | +% minimised over candidate orders 1..round(fs/2). In |
| 57 | +% sliding-window mode, order selection is done on the full |
| 58 | +% signal and the selected order is applied to all windows. |
| 59 | +% 'WindowSize' - Sliding window duration in seconds (default: 0, full signal) |
| 60 | +% Set to 0 to use the full signal. |
| 61 | +% 'WindowStep' - Step size in seconds (default: WindowSize/2, 50% overlap) |
22 | 62 | % |
23 | 63 | % Outputs: |
24 | 64 | % result - Struct with fields: |
|
27 | 67 | % .direction - 'x->y' |
28 | 68 | % .method - 'granger' |
29 | 69 | % .windowed - true if sliding window was used |
| 70 | +% .modelOrder - AR model order used (scalar; always present) |
30 | 71 | % .windowTimes - [W x 1] center times (windowed only) |
31 | 72 | % |
32 | 73 | % Algorithm: |
33 | | -% Restricted model: y(t) = sum_k a_k * y(t-k) + e_r |
| 74 | +% Restricted model: y(t) = sum_k a_k * y(t-k) + e_r (p params) |
34 | 75 | % Unrestricted model: y(t) = sum_k a_k * y(t-k) + sum_k b_k * x(t-k) + e_u |
35 | | -% F = ((RSS_r - RSS_u) / p) / (RSS_u / (T - 2p - 1)) |
| 76 | +% (2p params) |
| 77 | +% Neither model fits an intercept, so the F-test denominator dof is |
| 78 | +% nObs - 2p (nObs minus the unrestricted model's parameter count only -- |
| 79 | +% NOT nObs - 2p - 1, which would double-subtract a constant term that is |
| 80 | +% never estimated): |
| 81 | +% F = ((RSS_r - RSS_u) / p) / (RSS_u / (nObs - 2p)), nObs = T - p |
36 | 82 | % |
37 | | -% References: |
38 | | -% Granger, C. W. J. (1969). Investigating causal relations by econometric |
39 | | -% models and cross-spectral methods. Econometrica, 37(3), 424-438. |
| 83 | +% OLS lag regression is used (backslash solve). BIC for order selection: |
| 84 | +% BIC(p) = T_eff*log(sigma2_u) + 2*p*log(T_eff) |
| 85 | +% where sigma2_u = RSS_u / T_eff from the unrestricted model, and the penalty |
| 86 | +% counts the 2*p parameters of that model (p lags each of y and x). |
40 | 87 | % |
41 | | -% Geweke, J. (1982). Measurement of linear dependence and feedback between |
42 | | -% multiple time series. Journal of the American Statistical Association, |
43 | | -% 77(378), 304-313. |
| 88 | +% Example: |
| 89 | +% data = pf2.import.sampleData(); |
| 90 | +% proc = processFNIRS2(data); |
| 91 | +% result = exploreFNIRS.coupling.granger(proc.HbO(:,1), proc.HbO(:,2), proc.fs); |
| 92 | +% fprintf('F = %.3f, p = %.4f\n', result.value, result.pvalue); |
| 93 | +% |
| 94 | +% % Automatic order selection |
| 95 | +% result = exploreFNIRS.coupling.granger(proc.HbO(:,1), proc.HbO(:,2), proc.fs, ... |
| 96 | +% 'ModelOrder', 'auto'); |
| 97 | +% fprintf('Selected order: %d\n', result.modelOrder); |
| 98 | +% |
| 99 | +% Notes: |
| 100 | +% - Direction of apparent causality on fNIRS is NOT neural causality; see |
| 101 | +% the caveat in the extended description above. |
| 102 | +% - When T_valid <= 3*maxOrder (T is too short relative to the candidate |
| 103 | +% range), 'auto' order selection is skipped and order 1 is used with a |
| 104 | +% warning. |
44 | 105 | % |
45 | | -% See also: exploreFNIRS.coupling.transferEntropy, exploreFNIRS.coupling.pearson |
| 106 | +% See also: exploreFNIRS.coupling.wcoherence, exploreFNIRS.coupling.pearson, |
| 107 | +% exploreFNIRS.coupling.transferEntropy |
46 | 108 |
|
47 | 109 | p = inputParser; |
48 | 110 | addRequired(p, 'x', @(v) isnumeric(v) && isvector(v)); |
49 | 111 | addRequired(p, 'y', @(v) isnumeric(v) && isvector(v)); |
50 | 112 | addRequired(p, 'fs', @(v) isnumeric(v) && isscalar(v) && v > 0); |
51 | | - addParameter(p, 'ModelOrder', 5, @(v) isnumeric(v) && isscalar(v) && v >= 1); |
| 113 | + addParameter(p, 'ModelOrder', 5, @(v) (isnumeric(v) && isscalar(v) && v >= 1) || ... |
| 114 | + (ischar(v) && strcmpi(v, 'auto'))); |
52 | 115 | addParameter(p, 'WindowSize', 0, @(v) isnumeric(v) && isscalar(v) && v >= 0); |
53 | 116 | addParameter(p, 'WindowStep', 0, @(v) isnumeric(v) && isscalar(v) && v >= 0); |
54 | 117 | parse(p, x, y, fs, varargin{:}); |
|
60 | 123 | error('exploreFNIRS:coupling:granger', 'x and y must have equal length'); |
61 | 124 | end |
62 | 125 |
|
| 126 | + % Resolve 'auto' model order on the full signal (before windowing) |
| 127 | + autoOrder = ischar(opts.ModelOrder) && strcmpi(opts.ModelOrder, 'auto'); |
| 128 | + if autoOrder |
| 129 | + maxCand = min(round(fs / 2), floor(length(x) / 4)); |
| 130 | + maxCand = max(maxCand, 1); |
| 131 | + |
| 132 | + % Guard: need at least 3*maxCand valid samples to run order selection |
| 133 | + validMask = ~isnan(x) & ~isnan(y); |
| 134 | + [~, segLen] = longestRun(validMask); |
| 135 | + if segLen <= 3 * maxCand |
| 136 | + warning('exploreFNIRS:coupling:granger:autoOrderShortSignal', ... |
| 137 | + ['Signal is too short (valid=%d samples) for BIC order ' ... |
| 138 | + 'selection up to order %d (need > %d). Using order 1.'], ... |
| 139 | + segLen, maxCand, 3 * maxCand); |
| 140 | + modelOrder = 1; |
| 141 | + else |
| 142 | + modelOrder = selectOrderBIC(x, y, maxCand); |
| 143 | + end |
| 144 | + else |
| 145 | + modelOrder = opts.ModelOrder; |
| 146 | + end |
| 147 | + |
63 | 148 | winSamples = round(opts.WindowSize * fs); |
64 | 149 |
|
65 | 150 | if winSamples <= 0 || winSamples >= length(x) |
66 | 151 | % Full-signal mode |
67 | | - [fStat, pval] = computeGranger(x, y, opts.ModelOrder); |
| 152 | + [fStat, pval] = computeGranger(x, y, modelOrder); |
68 | 153 |
|
69 | 154 | result.value = fStat; |
70 | 155 | result.pvalue = pval; |
71 | 156 | result.direction = 'x->y'; |
72 | 157 | result.method = 'granger'; |
73 | 158 | result.windowed = false; |
| 159 | + result.modelOrder = modelOrder; |
74 | 160 | else |
75 | | - % Sliding window mode |
| 161 | + % Sliding window mode: order is fixed (selected on full signal above) |
76 | 162 | stepSamples = round(opts.WindowStep * fs); |
77 | 163 | if stepSamples <= 0 |
78 | 164 | stepSamples = max(1, round(winSamples / 2)); |
|
90 | 176 | idx = starts(w):(starts(w) + winSamples - 1); |
91 | 177 | xw = x(idx); |
92 | 178 | yw = y(idx); |
93 | | - [fVals(w), pVals(w)] = computeGranger(xw, yw, opts.ModelOrder); |
| 179 | + [fVals(w), pVals(w)] = computeGranger(xw, yw, modelOrder); |
94 | 180 | winTimes(w) = (starts(w) + winSamples/2 - 1) / fs; |
95 | 181 | end |
96 | 182 |
|
|
99 | 185 | result.direction = 'x->y'; |
100 | 186 | result.method = 'granger'; |
101 | 187 | result.windowed = true; |
| 188 | + result.modelOrder = modelOrder; |
102 | 189 | result.windowTimes = winTimes; |
103 | 190 | end |
104 | 191 | end |
105 | 192 |
|
| 193 | +%%_Subfunctions_________________________________________________________ |
106 | 194 |
|
107 | 195 | function [fStat, pval] = computeGranger(x, y, order) |
108 | | -% Compute Granger F-statistic for x -> y |
| 196 | +% COMPUTEGRANGER Compute Granger F-statistic for x -> y |
| 197 | +% |
| 198 | +% Inputs: |
| 199 | +% x - [T x 1] candidate cause |
| 200 | +% y - [T x 1] candidate effect |
| 201 | +% order - AR model order [scalar] |
| 202 | +% |
| 203 | +% Outputs: |
| 204 | +% fStat - F-statistic [scalar] |
| 205 | +% pval - p-value [scalar] |
109 | 206 |
|
110 | 207 | T = length(y); |
111 | 208 | if T <= 2 * order + 1 |
|
150 | 247 | Xu(:, order + k) = x((order + 1 - k):(T - k)); |
151 | 248 | end |
152 | 249 |
|
153 | | - % Solve via backslash |
| 250 | + % Solve via backslash (OLS) |
154 | 251 | betaR = Xr \ Y; |
155 | 252 | betaU = Xu \ Y; |
156 | 253 |
|
|
160 | 257 | rssR = sum(residR .^ 2); |
161 | 258 | rssU = sum(residU .^ 2); |
162 | 259 |
|
163 | | - % F-statistic |
| 260 | + % F-statistic. dfDen = nObs - (# unrestricted-model parameters). Neither |
| 261 | + % the restricted model (order params: y-lags) nor the unrestricted model |
| 262 | + % (2*order params: y-lags + x-lags) fits an intercept, so no additional |
| 263 | + % "-1" is subtracted for a constant term that was never estimated. |
164 | 264 | dfNum = order; |
165 | | - dfDen = nObs - 2 * order - 1; |
| 265 | + dfDen = nObs - 2 * order; |
166 | 266 |
|
167 | 267 | if dfDen <= 0 || rssU <= 0 |
168 | 268 | fStat = NaN; |
|
178 | 278 | end |
179 | 279 |
|
180 | 280 |
|
| 281 | +function order = selectOrderBIC(x, y, maxOrder) |
| 282 | +% SELECTORDERBIC Select Granger model order by BIC on OLS residuals |
| 283 | +% |
| 284 | +% For each candidate order p = 1..maxOrder, fits the unrestricted OLS model |
| 285 | +% (y lags + x lags) and computes: |
| 286 | +% BIC(p) = T_eff * log(RSS_u / T_eff) + 2*p * log(T_eff) |
| 287 | +% (the 2*p penalty counts the unrestricted model's 2*p parameters). The order |
| 288 | +% minimising BIC is returned. |
| 289 | +% |
| 290 | +% BIC is derived from OLS residual variance (not Yule-Walker), matching the |
| 291 | +% OLS backslash inference used in computeGranger. T_eff = nObs - 2*p. |
| 292 | +% |
| 293 | +% Inputs: |
| 294 | +% x - [T x 1] candidate cause |
| 295 | +% y - [T x 1] candidate effect |
| 296 | +% maxOrder - Maximum candidate order [scalar] |
| 297 | +% |
| 298 | +% Outputs: |
| 299 | +% order - Selected AR order [scalar] |
| 300 | + |
| 301 | + % Use longest valid segment for order selection |
| 302 | + valid = ~isnan(x) & ~isnan(y); |
| 303 | + [segStart, segLen] = longestRun(valid); |
| 304 | + if segLen == 0 |
| 305 | + order = 1; |
| 306 | + return; |
| 307 | + end |
| 308 | + x = x(segStart:segStart + segLen - 1); |
| 309 | + y = y(segStart:segStart + segLen - 1); |
| 310 | + T = length(y); |
| 311 | + |
| 312 | + bicVals = nan(maxOrder, 1); |
| 313 | + |
| 314 | + for q = 1:maxOrder |
| 315 | + nObs = T - q; |
| 316 | + dfDen = nObs - 2 * q - 1; |
| 317 | + if dfDen <= 0 |
| 318 | + break; |
| 319 | + end |
| 320 | + |
| 321 | + Y = y((q + 1):T); |
| 322 | + |
| 323 | + % Unrestricted model lag matrix |
| 324 | + Xu = zeros(nObs, 2 * q); |
| 325 | + for k = 1:q |
| 326 | + Xu(:, k) = y((q + 1 - k):(T - k)); |
| 327 | + Xu(:, q + k) = x((q + 1 - k):(T - k)); |
| 328 | + end |
| 329 | + |
| 330 | + betaU = Xu \ Y; |
| 331 | + residU = Y - Xu * betaU; |
| 332 | + rssU = sum(residU .^ 2); |
| 333 | + |
| 334 | + if rssU <= 0 || ~isfinite(rssU) |
| 335 | + continue; |
| 336 | + end |
| 337 | + |
| 338 | + sigma2 = rssU / nObs; |
| 339 | + % The unrestricted model has 2*q parameters (q lags each of y and x), so |
| 340 | + % the BIC complexity penalty must count 2*q, not q; using q biased |
| 341 | + % selection toward larger lag orders. |
| 342 | + bicVals(q) = nObs * log(sigma2) + 2 * q * log(nObs); |
| 343 | + end |
| 344 | + |
| 345 | + if all(isnan(bicVals)) |
| 346 | + warning('exploreFNIRS:coupling:granger:autoOrderFallback', ... |
| 347 | + 'BIC order selection produced all NaN; falling back to order 1.'); |
| 348 | + order = 1; |
| 349 | + return; |
| 350 | + end |
| 351 | + |
| 352 | + [~, order] = min(bicVals); |
| 353 | +end |
| 354 | + |
| 355 | + |
181 | 356 | function [start, len] = longestRun(mask) |
182 | | -% Find the start index and length of the longest contiguous run of true values |
| 357 | +% LONGESTRUN Find the start index and length of the longest true run |
| 358 | +% |
| 359 | +% Inputs: |
| 360 | +% mask - Logical vector [T x 1] |
| 361 | +% |
| 362 | +% Outputs: |
| 363 | +% start - Start index of longest run [scalar] |
| 364 | +% len - Length of longest run [scalar] |
| 365 | + |
183 | 366 | d = diff([0; mask(:); 0]); |
184 | 367 | starts = find(d == 1); |
185 | 368 | ends = find(d == -1) - 1; |
|
0 commit comments