Description
Describe the bug
I think the implementation of the PLI metric is wrong. The standard definition is:
PLI=|mean(sign(difference_in_instantaneous_phase))|
Note that for a signal of N time samples, this PLI value is obtained as the average of N values. The MNE implementation of this metric follows the implementation of Ortiz and instead of computing this metric in the time domain (e.g. using the Hilbert transform to estimate the instantaneous phase) computes it in the frequency domain using the cross-spectrum. An issue arise when this passage to the frequency domain is used to analyze the PLI results at the individual frequency bands. For example, the default behavior of spectral_connectivity()
is faverage=False
, which therefore report a value for each frequency. In this case, the PLI estimate that was the absolute-mean-value of N samples become the absolute-mean-value of 1 sample... so the PLI estimates reduce to 0 or 1 (where it was originally a value taken from the {0/N, 1/N, 2/N, ..., N/N}). Now, if we might think that we would recover the correct values using faverage=True
, but we don't because this computes the mean-absolute-value, not the absolute-mean-value... Here is a part of the spectral_connectivity()
code:
for method, n_args in zip(con_methods, n_comp_args):
# future estimators will need to be handled here
if n_args == 3:
# compute all scores at once
method.compute_con(slice(0, n_cons), n_epochs)
elif n_args == 5:
# compute scores block-wise to save memory
for i in range(0, n_cons, block_size):
con_idx = slice(i, i + block_size)
psd_xx = psd[idx_map[0][con_idx]]
psd_yy = psd[idx_map[1][con_idx]]
method.compute_con(con_idx, n_epochs, psd_xx, psd_yy)
else:
raise RuntimeError('This should never happen.')
# get the connectivity scores
this_con = method.con_scores
if this_con.shape[0] != n_cons:
raise ValueError('First dimension of connectivity scores must be '
'the same as the number of connections')
if faverage:
if this_con.shape[1] != n_freqs:
raise ValueError('2nd dimension of connectivity scores must '
'be the same as the number of frequencies')
con_shape = (n_cons, n_bands) + this_con.shape[2:]
this_con_bands = np.empty(con_shape, dtype=this_con.dtype)
for band_idx in range(n_bands):
this_con_bands[:, band_idx] =\
np.mean(this_con[:, freq_idx_bands[band_idx]], axis=1)
this_con = this_con_bands
Note that the faverage
part comes after the call to compute_con()
which, in the case of _PLIEst
already applied the absolute operator. So, whereas in the definition, you average N values taken in {-1, 0, 1}, here you average N values taken in {0, 1}.
If I am not mistaken, the function "appears" to work when using epochs because then it does the mean(signs(X))
across the M epochs, before computing the absolute value. But remember that the stuff in X was initially N values saying whether the time-lag at every N time samples was positive or negative. Now X values are still saying whether the phase-difference is positive or negative, but you traded the ability to check it at every time point to have it localized in frequencies (uncertainty principle), so you have just one value per epochs and you average it across M epochs. I might have interesting properties on its own, but I don't think it is still fair to call this a PLI measure, i.e. average_across_epochs(abs(average_across_x)) != average_across_x(abs(average_across_epochs)) (where x can be either time or frequency, depending on whether you transform from temporal representation to frequency).