Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
592f079
add Epochs.score_quality() for data-driven epoch quality scoring
aman-coder03 Mar 1, 2026
db8a176
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 1, 2026
c846793
DOC: Fix encoding of changelog file
aman-coder03 Mar 1, 2026
c76aa84
adding example for exploring epoch quality before rejection
aman-coder03 Mar 3, 2026
518b6b1
updating newfeature.rst file
aman-coder03 Mar 3, 2026
d7b5581
remove score_quality method, keep example only per review feedback
aman-coder03 Mar 6, 2026
926f501
updating .rst file
aman-coder03 Mar 6, 2026
3fdddec
rename changelog file to match PR number
aman-coder03 Mar 6, 2026
08d9cf8
add footcite references and update bib
aman-coder03 Mar 10, 2026
d1f02d8
Merge branch 'main' into enh-epoch-score-quality
aman-coder03 Mar 12, 2026
7d8f333
build docs
tsbinns Mar 12, 2026
c8b6554
Merge branch 'main' into enh-epoch-score-quality
aman-coder03 Mar 12, 2026
460c466
restructure as how-to guide
aman-coder03 Mar 12, 2026
00580c2
Merge branch 'main' into enh-epoch-score-quality
aman-coder03 Mar 16, 2026
0559a7f
Merge branch 'main' into enh-epoch-score-quality
aman-coder03 Mar 17, 2026
36edf2e
switching to EEGBCI
aman-coder03 Mar 18, 2026
4ba980a
update thresholds
aman-coder03 Mar 19, 2026
7c48140
address review comments
aman-coder03 Mar 22, 2026
efa9e2d
Merge branch 'main' into enh-epoch-score-quality
aman-coder03 Apr 2, 2026
6dd7c67
Merge branch 'main' into enh-epoch-score-quality
aman-coder03 Apr 4, 2026
f81a04c
Update thresholds
tsbinns Apr 6, 2026
3d5628a
Merge branch 'main' into enh-epoch-score-quality
aman-coder03 Apr 7, 2026
412ec98
Minor text update [skip azp][skip actions]
tsbinns Apr 7, 2026
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
120 changes: 120 additions & 0 deletions examples/preprocessing/plot_epoch_quality.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""
.. _ex-epoch-quality:

=========================================
Exploring epoch quality before rejection
=========================================
Comment thread
tsbinns marked this conversation as resolved.
Outdated

This example shows how to identify potentially artifactual epochs before
calling :meth:`mne.Epochs.drop_bad`. We compute per-epoch outlier scores
from peak-to-peak amplitude, variance, and kurtosis — inspired by FASTER
:footcite:t:`NolanEtAl2010` and :footcite:t:`DelormeEtAl2007` — and use
them to rank epochs from cleanest to noisiest before making any rejection
decisions.
Comment thread
tsbinns marked this conversation as resolved.
Outdated
"""
# 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
# ------------------------------------------
Comment thread
tsbinns marked this conversation as resolved.
Outdated
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 (MAD)
Comment thread
CarinaFo marked this conversation as resolved.
Outdated
# across epochs and averaged into a single outlier score, normalised
# between [0, 1]. Scores close to 1 indicate likely artifacts.
Comment thread
tsbinns marked this conversation as resolved.
Outdated

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)

# %%
# Plot epoch quality scores
# --------------------------
# Epochs are ranked from cleanest to noisiest. The dashed lines show two
# example thresholds — demonstrating the quality-quantity trade-off when
# deciding how many epochs to reject.
Comment thread
tsbinns marked this conversation as resolved.
Outdated
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="Strict threshold (0.8)")
ax.axhline(0.6, color="orange", linestyle="--", label="Lenient threshold (0.6)")
Comment thread
tsbinns marked this conversation as resolved.
Outdated
ax.set(
xlabel="Epoch (sorted by score)",
ylabel="Outlier score",
title="Epoch quality scores (0 = clean, 1 = likely artifact)",
)
ax.legend()

# %%
# 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 should be adapted based on
# your data and how many epochs you can afford to lose.
Comment thread
tsbinns marked this conversation as resolved.
Outdated
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"
)

# %%
# Plot epochs at different thresholds
# -------------------------------------
# The worst-scoring epoch (strict threshold) clearly contains an artifact.
# An epoch from the lenient threshold may look less obvious — illustrating
# why tuning the threshold matters for the quality-quantity trade-off.
worst_idx = np.argmax(scores)
epochs[worst_idx].plot(
title=f"Strict threshold — worst epoch "
f"(index {worst_idx}, score={scores[worst_idx]:.2f})",
scalings=dict(eeg=100e-6),
)

lenient_idx = np.where(scores > 0.6)[0]
lenient_idx = lenient_idx[lenient_idx != worst_idx][0]
epochs[lenient_idx].plot(
title=f"Lenient threshold — borderline epoch "
f"(index {lenient_idx}, score={scores[lenient_idx]:.2f})",
scalings=dict(eeg=100e-6),
)
Comment thread
tsbinns marked this conversation as resolved.

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