Skip to content
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions doc/changes/dev/13710.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a preprocessing example showing how to explore epoch quality before rejection using robust statistics (peak-to-peak amplitude, variance, and kurtosis) inspired by FASTER (Nolan et al., 2010) and Delorme et al. (2007), by `Aman Srivastava`_.
21 changes: 21 additions & 0 deletions doc/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@ @article{GramfortEtAl2013a
number = {267}
}
% everything else
@article{NolanEtAl2010,
author = {Nolan, H. and Whelan, R. and Reilly, R. B.},
title = {{FASTER}: Fully Automated Statistical Thresholding for {EEG} artifact Rejection},
journal = {Journal of Neuroscience Methods},
year = {2010},
volume = {192},
number = {1},
pages = {152--162},
doi = {10.1016/j.jneumeth.2010.07.015},
}

@article{DelormeEtAl2007,
author = {Delorme, A. and Sejnowski, T. and Makeig, S.},
title = {Enhanced detection of artifacts in {EEG} data using higher-order statistics and independent component analysis},
journal = {NeuroImage},
year = {2007},
volume = {34},
number = {4},
pages = {1443--1449},
doi = {10.1016/j.neuroimage.2006.11.004},
}
@article{AblinEtAl2018,
author = {Ablin, Pierre and Cardoso, Jean-Francois and Gramfort, Alexandre},
doi = {10.1109/TSP.2018.2844203},
Expand Down
114 changes: 114 additions & 0 deletions examples/preprocessing/plot_epoch_quality.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""
.. _ex-epoch-quality:

========================================
Exploring epoch quality before rejection
========================================

This example shows an approach for identifying epochs containing potential artifacts and
rejecting these bad epochs. We compute per-epoch outlier scores from peak-to-peak
amplitude, variance, and kurtosis — inspired by FASTER :footcite:`NolanEtAl2010` and
:footcite:t:`DelormeEtAl2007` — and use them to rank epochs from cleanest to noisiest to
inform rejection decisions.
"""
# Authors: Aman Srivastava
#
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.

# %%
import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import kurtosis

import mne
from mne.datasets import eegbci

print(__doc__)

# %%
# Load the EEGBCI dataset and create epochs
# -----------------------------------------
raw_fname = eegbci.load_data(subjects=3, runs=(3,))[0]
raw = mne.io.read_raw(raw_fname, preload=True)
eegbci.standardize(raw)
montage = mne.channels.make_standard_montage("standard_1005")
raw.set_montage(montage)

events, event_id = mne.events_from_annotations(raw)
epochs = mne.Epochs(raw, events, tmin=-0.2, tmax=0.5, preload=True, baseline=(None, 0))

# %%
# Compute per-epoch outlier scores
# --------------------------------
# Peak-to-peak amplitude, variance, and kurtosis are computed per epoch. Each feature is
# z-scored robustly using median absolute deviation across epochs, and averaged into a
# single outlier score normalised between [0, 1]. Scores close to 1 indicate a likely
# presence of artifacts in the epoch.

data = epochs.get_data() # (n_epochs, n_channels, n_times)

ptp = np.ptp(data, axis=-1).mean(axis=-1)
var = data.var(axis=-1).mean(axis=-1)
kurt = np.array([kurtosis(data[i].ravel()) for i in range(len(data))])

features = np.column_stack([ptp, var, kurt])
median = np.median(features, axis=0)
mad = np.median(np.abs(features - median), axis=0) + 1e-10
z = np.abs((features - median) / mad)

raw_score = z.mean(axis=-1)
scores = (raw_score - raw_score.min()) / (raw_score.max() - raw_score.min() + 1e-10)

# %%
# Determining outlier epochs
# --------------------------
# Below, epochs are ranked from cleanest to noisiest. We need to find an appropriate
# threshold to flag those epochs likely containing artifacts. In the plot, we show two
# example thresholds: a more lenient threshold of 0.8; and a stricter threshold of 0.6.
fig, ax = plt.subplots(layout="constrained")
sorted_idx = np.argsort(scores)
ax.bar(np.arange(len(scores)), scores[sorted_idx], color="steelblue")
ax.axhline(0.8, color="red", linestyle="--", label="More lenient threshold (0.8)")
ax.axhline(0.6, color="orange", linestyle="--", label="Stricter threshold (0.6)")
ax.set(
xlabel="Epoch (sorted by score)",
ylabel="Outlier score",
title="Epoch quality scores (0 = clean, 1 = likely artifact)",
)
ax.legend()

for threshold in [0.8, 0.6]:
bad_epochs = np.where(scores > threshold)[0]
print(
f"Threshold {threshold}: {len(bad_epochs)} epochs flagged "
f"out of {len(epochs)} total"
)

# %%
# Epochs flagged by the thresholds can be inspected using the :meth:`~mne.Epochs.plot`
# method. First, we show those epochs with the worst scores (≥ 0.8), containing a number
# of amplitude spikes.
epochs[np.where(scores >= 0.8)[0]].plot(title="Scores ≥ 0.8", scalings=dict(eeg=70e-6))
# %%
# In contrast, the threshold of 0.6 captures epochs with less severe artifact activity,
# which may be overly conservative to exclude from the analysis.
epochs[np.where((scores >= 0.6) & (scores < 0.8))[0]].plot(
title="0.6 ≤ scores < 0.8", scalings=dict(eeg=70e-6)
)

# %%
# Identify and handle suspicious epochs
# --------------------------------------
# Epochs scoring above the threshold can be inspected visually using
# :meth:`mne.Epochs.plot`, or dropped directly using
# :meth:`mne.Epochs.drop`. The threshold to use to flag epochs as outliers varies
# depending on the dataset and analysis goals, and inspecting the flagged epochs is a
# crucial step in identifying the optimal threshold.
epochs.drop(np.where(scores >= 0.8)[0])
print(f"Epochs remaining after dropping scores ≥ 0.8: {len(epochs)}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now there should be a brief point on actually dropping the flagged epochs.

# %%
# References
# ----------
# .. footbibliography::
Loading