Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC, WIP: public function to find and return bad channels #214

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions pylossless/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,52 @@
return prop_outliers[prop_outliers > flag_crit].coords.to_index().values


def find_bads_by_threshold(epochs, threshold=5e-5):
"""Return channels with a standard deviation consistently above a fixed threshold.

Parameters
----------
inst : mne.Epochs
an instance of mne.Epochs
threshold : float
the threshold in volts. If the standard deviation of a channel's voltage
varianceat a specific epoch is above the threshold, then that channel x epoch
will be flagged as an "outlier". Default is 5e-5 (0.00005), i.e.
50 microvolts.

Returns
-------
list
a list of channel names that are considered outliers.

Notes
-----
If you are having trouble converting between exponential notation and
decimal notation, you can use the following code to convert between the two:

>>> import numpy as np
>>> threshold = 5e-5
>>> with np.printoptions(suppress=True):
... print(threshold)
0.00005

Examples
--------
>>> import mne
>>> import pylossless as ll
>>> fname = mne.datasets.sample.data_path() / "MEG/sample/sample_audvis_raw.fif"
>>> raw = mne.io.read_raw(fname, preload=True).pick("eeg")
>>> raw.apply_function(lambda x: x * 3, picks=["EEG 001"]) # Make a noisy channel
>>> epochs = mne.make_fixed_length_epochs(raw, preload=True)
>>> bad_chs = ll.pipeline.find_bads_by_threshold(epochs)
"""
bads = _threshold_volt_std(epochs, flag_dim="ch", threshold=threshold)
logger.info(

Check warning on line 270 in pylossless/pipeline.py

View check run for this annotation

Codecov / codecov/patch

pylossless/pipeline.py#L269-L270

Added lines #L269 - L270 were not covered by tests
f"Found {len(bads)} channels with high voltage variance: {bads}"
)
return _threshold_volt_std(epochs, flag_dim="ch", threshold=threshold)

Check warning on line 273 in pylossless/pipeline.py

View check run for this annotation

Codecov / codecov/patch

pylossless/pipeline.py#L273

Added line #L273 was not covered by tests


def _threshold_volt_std(epochs, flag_dim, threshold=5e-5):
"""Detect epochs or channels whose voltage std is above threshold.

Expand Down Expand Up @@ -765,9 +811,13 @@
on. You may need to assess a more appropriate value for your own data.
"""
epochs = self.get_epochs(picks=picks)
above_threshold = _threshold_volt_std(
epochs, flag_dim=flag_dim, threshold=threshold
)
# So yes this add a few LOC, but IMO it's worth it for readability
if flag_dim == "ch":
above_threshold = find_bads_by_threshold(epochs, threshold=threshold)

Check warning on line 816 in pylossless/pipeline.py

View check run for this annotation

Codecov / codecov/patch

pylossless/pipeline.py#L815-L816

Added lines #L815 - L816 were not covered by tests
else: # TODO: Implement an annotate_bads_by_threshold for epochs
above_threshold = _threshold_volt_std(

Check warning on line 818 in pylossless/pipeline.py

View check run for this annotation

Codecov / codecov/patch

pylossless/pipeline.py#L818

Added line #L818 was not covered by tests
epochs, flag_dim=flag_dim, threshold=threshold
)
self.flags[flag_dim].add_flag_cat("volt_std", above_threshold, epochs)

def find_outlier_chs(self, epochs=None, picks="eeg"):
Expand Down
Loading