Skip to content

Commit 30de2e7

Browse files
vpKumaravelnbara
andauthored
[ENH] Add a bad channel detection method using LOF algorithm (#66)
Co-authored-by: Nicolas Barascud <10333715+nbara@users.noreply.github.com>
1 parent 5e95ee5 commit 30de2e7

File tree

9 files changed

+171
-13
lines changed

9 files changed

+171
-13
lines changed

README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![codecov](https://codecov.io/gh/nbara/python-meegkit/branch/master/graph/badge.svg)](https://codecov.io/gh/nbara/python-meegkit)
44
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/nbara/python-meegkit/master)
55
[![DOI](https://zenodo.org/badge/117451752.svg)](https://zenodo.org/badge/latestdoi/117451752)
6-
[![twitter](https://img.shields.io/twitter/follow/lebababa?label=Twitter&style=flat&logo=Twitter)](https://twitter.com/intent/follow?screen_name=lebababa)
6+
[![twitter](https://img.shields.io/twitter/follow/lebababa?style=flat&logo=Twitter)](https://twitter.com/intent/follow?screen_name=lebababa)
77

88
# MEEGkit
99

@@ -54,7 +54,7 @@ Other available options are `[docs]` (which installs dependencies required to bu
5454

5555
## References
5656

57-
### 1. CCA, STAR, SNS, DSS, ZapLine, and robust detrending
57+
### 1. CCA, STAR, SNS, DSS, ZapLine, and Robust Detrending
5858

5959
This is mostly a translation of Matlab code from the [NoiseTools toolbox](http://audition.ens.fr/adc/NoiseTools/) by Alain de Cheveigné. It builds on an initial python implementation by [Pedro Alcocer](https://github.com/pealco).
6060

@@ -83,10 +83,9 @@ If you use this code, you should cite the relevant methods from the original art
8383
Journal of Neuroscience Methods, 168(1), 195202. https://doi.org/10.1016/j.jneumeth.2007.09.012
8484
[10] de Cheveigné, A., & Simon, J. Z. (2007). Denoising based on time-shift PCA.
8585
Journal of Neuroscience Methods, 165(2), 297305. https://doi.org/10.1016/j.jneumeth.2007.06.003
86-
8786
```
8887

89-
### 2. Artifact subspace reconstruction (ASR)
88+
### 2. Artifact Subspace Reconstruction (ASR)
9089

9190
The base code is inspired from the original [EEGLAB inplementation](https://github.com/sccn/clean_rawdata) [1], while the riemannian variant [2] was adapted from the [rASR toolbox](https://github.com/s4rify/rASRMatlab) by Sarah Blum.
9291

@@ -101,7 +100,7 @@ If you use this code, you should cite the relevant methods from the original art
101100
13, 141.
102101
```
103102

104-
### 3. Rhythmic entrainment source separation (RESS)
103+
### 3. Rhythmic Entrainment Source Separation (RESS)
105104

106105
The code is based on [Matlab code from Mike X. Cohen](https://mikexcohen.com/data/) [1]
107106

@@ -130,3 +129,15 @@ If you use this, you should cite the following articles:
130129
"High-speed spelling with a noninvasive brain-computer interface",
131130
Proc. Int. Natl. Acad. Sci. U. S. A, 112(44): E6058-6067, 2015.
132131
```
132+
133+
### 5. Local Outlier Factor (LOF)
134+
135+
If you use this, you should cite the following article:
136+
137+
```sql
138+
[1] Breunig M, Kriegel HP, Ng RT, Sander J. 2000. LOF: identifying density-based
139+
local outliers. SIGMOD Rec. 29, 2, 93-104. https://doi.org/10.1145/335191.335388
140+
[2] Kumaravel VP, Buiatti M, Parise E, Farella E. 2022. Adaptable and Robust
141+
EEG Bad Channel Detection Using Local Outlier Factor (LOF). Sensors (Basel).
142+
2022 Sep 27;22(19):7314. https://doi.org/10.3390/s22197314.
143+
```

doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Here is a list of the methods and techniques available in ``meegkit``:
3737
~meegkit.cca
3838
~meegkit.dss
3939
~meegkit.detrend
40+
~meegkit.lof
4041
~meegkit.ress
4142
~meegkit.sns
4243
~meegkit.star

doc/modules/meegkit.lof.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
meegkit.lof
2+
===========
3+
4+
.. automodule:: meegkit.lof
5+
6+
.. rubric:: Functions
7+
8+
.. autosummary::
9+
10+
LOF
11+
12+
13+
14+
15+
16+
17+
18+
19+
20+
21+
22+
23+

meegkit/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""M/EEG denoising utilities in python."""
22
__version__ = '0.1.3'
33

4-
from . import asr, cca, detrend, dss, sns, star, ress, trca, tspca, utils
4+
from . import asr, cca, detrend, dss, lof, sns, star, ress, trca, tspca, utils
55

6-
__all__ = ['asr', 'cca', 'detrend', 'dss', 'ress', 'sns', 'star', 'trca',
6+
__all__ = ['asr', 'cca', 'detrend', 'dss', 'lof', 'ress', 'sns', 'star', 'trca',
77
'tspca', 'utils']

meegkit/lof.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Local Outlier Factor (LOF)."""
2+
# Authors: Velu Prabhakar Kumaravel <vkumaravel@fbk.eu>
3+
# License: BSD-3-Clause
4+
5+
import logging
6+
from sklearn.neighbors import LocalOutlierFactor
7+
8+
9+
class LOF():
10+
"""Local Outlier Factor.
11+
12+
Local Outlier Factor (LOF) is an automatic, density-based outlier detection
13+
algorithm based on [1]_ and [2]_.
14+
15+
Parameters
16+
----------
17+
n_neighbours : int
18+
Number of neighbours defining the local neighbourhood.
19+
metric: str in {'euclidean', 'nan_euclidean', 'cosine',
20+
'cityblock', 'manhattan'}
21+
Metric to use for distance computation. Default is “euclidean”
22+
threshold : float
23+
Threshold to define outliers. Theoretical threshold ranges anywhere
24+
between 1.0 and any integer. Default: 1.5
25+
26+
Notes
27+
-----
28+
It is recommended to perform a CV (e.g., 10-fold) on training set to
29+
calibrate this parameter for the given M/EEG dataset.
30+
31+
See [2]_ for details.
32+
33+
References
34+
----------
35+
.. [1] Breunig M, Kriegel HP, Ng RT, Sander J.
36+
2000. LOF: identifying density-based local outliers.
37+
SIGMOD Rec. 29, 2, 93-104. https://doi.org/10.1145/335191.335388
38+
.. [2] Kumaravel VP, Buiatti M, Parise E, Farella E.
39+
2022. Adaptable and Robust EEG Bad Channel Detection Using
40+
Local Outlier Factor (LOF). Sensors (Basel). 2022 Sep 27;22(19):7314.
41+
doi: 10.3390/s22197314. PMID: 36236413; PMCID: PMC9571252.
42+
43+
"""
44+
45+
def __init__(self, n_neighbors=20, metric='euclidean',
46+
threshold=1.5, **kwargs):
47+
48+
self.n_neighbors = n_neighbors
49+
self.metric = metric
50+
self.threshold = threshold
51+
52+
def predict(self, X):
53+
"""Detect bad channels using Local Outlier Factor algorithm.
54+
55+
Parameters
56+
----------
57+
X : array, shape=(n_channels, n_samples)
58+
The data X should have been high-pass filtered.
59+
60+
Returns
61+
-------
62+
bad_channel_indices : Detected bad channel indices.
63+
64+
"""
65+
if X.ndim == 3: # in case the input data is epoched
66+
logging.warning('Expected input data with shape '
67+
'(n_channels, n_samples)')
68+
return []
69+
70+
if self.n_neighbors >= X.shape[0]:
71+
logging.warning('Number of neighbours cannot be greater than the '
72+
'number of channels')
73+
return []
74+
75+
if self.threshold < 1.0:
76+
logging.warning('Invalid threshold. Try a positive integer >= 1.0')
77+
return []
78+
79+
clf = LocalOutlierFactor(self.n_neighbors)
80+
logging.debug('[LOF] Predicting bad channels')
81+
clf.fit_predict(X)
82+
lof_scores = clf.negative_outlier_factor_
83+
bad_channel_indices = -lof_scores >= self.threshold
84+
85+
return bad_channel_indices

tests/data/lofdata.mat

97.4 MB
Binary file not shown.

tests/test_asr.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
from meegkit.utils.matrix import sliding_window
1010
from scipy import signal
1111

12-
np.random.seed(9)
13-
1412
# Data files
1513
THIS_FOLDER = os.path.dirname(os.path.abspath(__file__))
1614
# file = os.path.join(THIS_FOLDER, 'data', 'eeg_raw.fif')
@@ -175,6 +173,7 @@ def test_asr_functions(show=False, method='riemann'):
175173
@pytest.mark.parametrize(argnames='reref', argvalues=(False, True))
176174
def test_asr_class(method, reref, show=False):
177175
"""Test ASR class (simulate online use)."""
176+
np.random.default_rng(9)
178177
raw = np.load(os.path.join(THIS_FOLDER, 'data', 'eeg_raw.npy'))
179178
sfreq = 250
180179
# Train on a clean portion of data

tests/test_lof.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""LOF test."""
2+
import os
3+
4+
import numpy as np
5+
import pytest
6+
import scipy.io as sio
7+
8+
from meegkit.lof import LOF
9+
10+
np.random.seed(9)
11+
12+
# Data files
13+
THIS_FOLDER = os.path.dirname(os.path.abspath(__file__)) # data folder of MEEGKIT
14+
15+
16+
@pytest.mark.parametrize(argnames='n_neighbors', argvalues=(8, 20, 40, 2048))
17+
def test_lof(n_neighbors, show=False):
18+
mat = sio.loadmat(os.path.join(THIS_FOLDER, 'data', 'lofdata.mat'))
19+
X = mat['X']
20+
lof = LOF(n_neighbors)
21+
bad_channel_indices = lof.predict(X)
22+
print(bad_channel_indices)
23+
24+
@pytest.mark.parametrize(argnames='metric',
25+
argvalues=('euclidean', 'nan_euclidean',
26+
'cosine', 'cityblock', 'manhattan'))
27+
def test_lof2(metric, show=False):
28+
mat = sio.loadmat(os.path.join(THIS_FOLDER, 'data', 'lofdata.mat'))
29+
X = mat['X']
30+
lof = LOF(20, metric)
31+
bad_channel_indices = lof.predict(X)
32+
print(bad_channel_indices)
33+
34+
if __name__ == "__main__":
35+
pytest.main([__file__])
36+
#test_lof(20, True)
37+
#test_lof(metric='euclidean')

tests/test_tspca.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ def test_tspca_sns_dss(): # TODO
5151
y_tspca_sns_dss = fold(
5252
np.dot(unfold(y_tspca_sns), todss), y_tspca_sns.shape[0])
5353

54-
return y_tspca, y_tspca_sns, y_tspca_sns_dss
54+
# TODO do something with it
55+
assert y_tspca_sns_dss.shape == noisy_data.shape
5556

5657

5758
def test_tsr(show=True):
@@ -107,6 +108,7 @@ def test_tsr(show=True):
107108
plt.show()
108109

109110
if __name__ == '__main__':
110-
# import pytest
111-
# pytest.main([__file__])
112-
test_tsr()
111+
import pytest
112+
pytest.main([__file__])
113+
# test_tspca_sns_dss()
114+
# test_tsr()

0 commit comments

Comments
 (0)