Artifact rejection strategies

Amplitude and flatness criteria, autoreject, annotation-based rejection, quantifying data loss, and exclusion criteria set before looking at effects.

~50 min Widget: w-threshold-tuner Notebook: nb-2-5-rejection

Prerequisites: L2.4 · Filtering in practice

1 claim on this page is unverified. TODO(confirm) marks a specific statement the author has not yet checked against a primary source. Everything else on this page has been reviewed. Treat a marked claim as provisional and go to the cited source rather than quoting the sentence.

Objectives

  • Apply amplitude, peak-to-peak and flatness criteria
  • Use autoreject (local and global)
  • Annotate segments and quantify data loss
  • Set subject exclusion criteria before looking at effects

Why this matters

Rejection is the one preprocessing step that changes how much data each condition contributes, and therefore the one that can create an effect out of nothing. A threshold is not a cleanliness setting: it is a filter on trials, and if the conditions differ in how often they trip it — because one is longer, more arousing, more likely to provoke a blink — you have compared a well-estimated average with a badly-estimated one. This lesson gives you criteria, a way to measure the damage, and the rule that keeps rejection from becoming an analysis choice.

Concepts

What rejection is for

Some segments of EEG are not measurements of brain activity: an electrode popped, the participant moved, the amplifier saturated, a lead came loose. Averaging them in adds variance at best and a systematic deflection at worst. Rejection removes them. It is not a substitute for artifact correction (L2.6, L2.7), which keeps the trial and removes the artifact’s contribution — the two are complementary, and the usual order is to correct what can be corrected (blinks, line noise) and reject what cannot (pops, movement, saturation).

Threshold criteria

Three criteria cover most of what a threshold can catch, and each answers a different question:

  • Peak-to-peak amplitude within the epoch. The difference between the maximum and minimum of a channel inside the epoch window. This is the workhorse: it catches pops, movement and drift-within-epoch, and it is insensitive to a DC offset, which absolute amplitude is not.
  • Absolute amplitude. Maximum absolute value. Catches saturation and huge excursions; more sensitive than peak-to-peak to a baseline that is simply offset.
  • Flatness. Peak-to-peak below a small value: a channel that is not moving at all is disconnected, not quiet.

In MNE these are the reject, flat and reject_tmin/reject_tmax arguments to Epochs, expressed per channel type in volts:

epochs = mne.Epochs(raw, events, event_id, tmin=-0.2, tmax=0.8,
                    reject=dict(eeg=100e-6),      # peak-to-peak, µV → V
                    flat=dict(eeg=1e-6),
                    reject_by_annotation=True, preload=True)
print(epochs.drop_log)                            # why each dropped epoch was dropped

Two things about the numbers. First, there is no universal threshold: an appropriate peak-to-peak criterion depends on the amplifier, the reference, the filtering already applied, the montage and the population (children and patient groups move more). Anyone who gives you a number without those caveats is giving you their lab’s number. Second, the threshold interacts with everything upstream: the same epochs rejected at a fixed criterion before and after a high-pass, or before and after an average reference, are different sets. Fix the pipeline, then set the threshold.

epochs.drop_log is the object that makes this auditable: it records, per epoch, which channel and which criterion caused the drop. It is the raw material for the QC report of L2.8 and for the per-condition table below.

Autoreject: thresholds estimated from the data

Choosing thresholds by eye does not scale and does not reproduce. autoreject (Jas et al., 2017) estimates them instead, by cross-validation: it searches over candidate peak-to-peak thresholds and picks those that minimise the error of the resulting average on held-out trials.

It comes in two forms:

  • Global (get_rejection_threshold, or AutoReject in its global mode) estimates one threshold for the whole recording — a direct, data-driven replacement for a hand-picked number.
  • Local (AutoReject) estimates a threshold per channel and then, for each epoch, decides between three outcomes: keep it, repair it by interpolating the few worst channels in that epoch, or reject it if too many channels are bad. The per-epoch interpolation is what makes it “local”, and it recovers trials that a global threshold would have thrown away for one bad channel.

The honest caveats: it is a cross-validated fit, so it costs time and needs enough epochs to fit on; its per-epoch interpolation shares every limitation of interpolation in L2.2 (no new information, and a rank cost that is now epoch-dependent); and it optimises the average’s error, which is the right objective for an ERP and not obviously the right one for a single-trial or time-frequency analysis. It removes the arbitrariness of the threshold, not the need to report what it did.

Annotation-based rejection

Not every artifact is aligned to an epoch. A minute of movement in the middle of a run, a stretch where a lead was reattached, a segment of drowsiness — these are properties of the continuous recording, and the place to record them is an annotation:

raw.set_annotations(raw.annotations + mne.Annotations(onset=[123.4], duration=[8.0],
                                                      description=['BAD_movement']))

Any annotation whose description starts with BAD_ is honoured by Epochs(..., reject_by_annotation=True) and by raw.filter() (which will not filter across it). The advantages over deleting the data are decisive: the timing of everything else is unchanged, the decision is visible in the file, and it is reversible. Deleting a segment by slicing and re-concatenating creates a discontinuity that a later filter will ring across (L2.4) and destroys the mapping between event samples and time.

Annotation is also where the label_source rule of §4.5 bites: automatic segment detection produces candidates, and a candidate is not an expert judgment until someone looked.

Data loss is a number you report

Every rejection scheme trades trials for cleanliness, and the trade has a known shape: the noise in an average falls as the square root of the number of trials, so throwing away 20% of trials costs about 10% of your signal-to-noise ratio — while removing a handful of genuinely contaminated trials can improve it a great deal. Somewhere between those, more rejection starts making the average worse.

So report, per subject and per condition: the number of trials presented, the number rejected, the percentage, and the reason (which criterion, which channel). The per-condition breakdown is not optional — it is the entire subject of the next section.

Condition-biased rejection

Here is the failure mode. Condition A is the rare, attended, task-relevant stimulus; condition B is the frequent standard. Participants blink more after the rare stimulus, or move, or the trials are longer. A fixed threshold therefore rejects more A trials than B trials. The A average is now built from fewer trials than the B average, so it is noisier — and several common measurements are biased by noise in a direction, not just made imprecise: peak amplitude, in particular, is inflated when noise is higher, because the peak-picking operation takes the maximum (L3.3, pf-peak-amplitude-noise-bias). The result is a difference between conditions that is a difference in trial counts.

What to do:

  1. Measure it. Build the per-condition rejection table for every subject, every time. A difference of a few percent is life; a difference of 20 percentage points is a finding about your pipeline, not about the brain.
  2. Set the criterion condition-blind. Estimate thresholds on all trials pooled, never per condition, and never after looking at the effect.
  3. Consider matching trial counts when the imbalance is large — subsample the better-preserved condition to the worse one (with a seed, and reported), so that the two averages have the same noise level. This costs power and buys comparability; it is a choice to state, not a default.
  4. Prefer measurements that are robust to unequal noise — mean amplitude over an a-priori window rather than peak amplitude (L3.3) — and report the standardized measurement error per condition (L3.5) so a reader can see the noise difference you could not remove.
Judgment call

Write the rejection criteria and the subject-exclusion rule down before you see the group effect, and keep the per-condition table in the QC report. The test of whether rejection was an analysis choice is simple: could you have arrived at a different threshold by looking at the result? If yes, the threshold needs to be pre-specified.

Subject exclusion

The same logic scales to whole recordings. A defensible exclusion rule states, in advance: the maximum fraction of trials that may be rejected in any condition; the minimum number of trials per condition needed for the measurement you plan (L3.5 gives the trial-count argument); the maximum fraction of interpolated channels (L2.2); and any hard technical failure (missing events, wrong sampling rate, a run that ended early). Applied blind to the effect, it is a criterion. Applied after the group analysis, it is a degree of freedom (L6.4).

The data behind this lesson

  • The widget serves one ds-erpcore P3 subject’s epochs over both conditions, with per-epoch peak-to-peak, maximum-absolute and flatness values precomputed for every channel, so that the rejection counts it shows you are the real counts, not counts over a display subset. ERP CORE is CC BY 4.0, open access and per-subject downloadable; TODO(confirm): the author mirrors the entry into the catalog registry and signs off the dataset page (§10.11 item 8).
  • The notebook runs the same comparison with autoreject against fixed thresholds and produces the per-condition rejection table.

Explore

Threshold tuner — move the rejection threshold and watch the per-condition counts and the resulting ERP

mode: default Open lab page →
Loading Threshold tuner — move the rejection threshold and watch the per-condition counts and the resulting ERP…

What to look for

Data: ds-erpcore , subject sub-001, run task-P3, -0.19921875–0.80078125 s · license CC-BY-SA-4.0 · DOI TODO(confirm) · labels: algorithmic · a cropped, re-referenced or filtered derivative of the source recording. Share-alike. This asset is derived from a source whose licence requires that anything built from it carry the same licence. If you reuse it, distribute your version under CC-BY-SA-4.0 and keep the attribution below.
Kappenman, E., Farrens, J., Zhang, W., Stewart, A. X., & Luck, S. J. (2020). ERP CORE: An Open Resource for Human Event-related Potential Research. PsyArXiv. Paper DOI 10.31234/osf.io/4azqm. Dataset DOI 10.18112/openneuro.ds003069.v1.0.0, https://osf.io/thsqg/. Licensed CC-BY-SA-4.0; this is a derived asset and is distributed under the same licence (data/directory.yaml license_decision, section 13 item 15).

Start with the threshold far too high (nothing rejected) and walk it down, watching three things at once: the total percentage rejected, the difference between the two conditions’ percentages, and the ERP. You are looking for the range where the average visibly improves and the counts stay balanced — and for the point below which the difference between conditions starts to grow. The exercise asks you to find that second point.

Practice

Artifact rejection strategies: a peak-to-peak threshold sweep, autoreject against fixed thresholds, and the per-condition rejection table nb-2-5-rejection

Level 2 ~7 min
notebooks/L2/nb-2-5-rejection.ipynb

Downloads from ds-erpcore.

Open in Colab Download Read it here

The notebook compares autoreject (global and local) with fixed thresholds on ds-erpcore P3 data and prints the per-condition rejection table, the resulting trial counts and the effect on the difference wave. Its final cell prints the numbers this lesson’s exercise asks for.

Exercises

Exercise ex-2-5-biased-threshold

Numeric

Using the threshold tuner, find the peak-to-peak rejection threshold at which one condition loses at least 20 percentage points more of its trials than the other. Report that threshold.

µV

Accepted within ±5 µV.

Exercise ex-2-5-consequence

Free response

You have a threshold that rejects 20 percentage points more trials in one condition than the other. What does that do to the comparison you were going to run, and what would you do instead?

Pitfalls

Pitfall

Rejection that differs by condition

Symptom
One condition loses 30% of trials; effect appears or disappears.
Cause

A fixed rejection criterion is not condition-neutral, because the conditions are not equally likely to trip it. Rare, attended or task-relevant stimuli provoke more blinks and more movement; conditions differ in trial length, in arousal, in how often the participant responds. So the same threshold rejects more trials in one condition than the other.

Detect
  • Build the per-condition rejection table for every subject, every run: trials presented, trials rejected, percentage, and the criterion and channel responsible. epochs.droplog holds all of it. - Plot the percentage rejected per condition against the threshold, and look at the difference between the two curves rather than the total. - Compare the effect computed on the full trial set with the eff…
Fix
  • Set the criterion condition-blind: estimate it on pooled trials, never per condition, and fix it before looking at the effect. Pre-specify it alongside the subject-exclusion rule. - Prefer a data-driven threshold (autoreject, fitted on pooled trials) to a hand-picked one, and report what it chose. - Report the per-condition rejection table in the QC report and in the paper — not just the total…

Full entry with example →

In other tools

In other toolsEEGLAB · FieldTrip — names only

The equivalents of what this lesson does, for a reader who works in another toolbox. Function names only: their own documentation is the place to learn how to call them.

EEGLAB

  • pop_eegthreshEEGLAB
  • pop_rejkurtEEGLAB

FieldTrip

  • ft_rejectvisualFieldTrip
  • ft_artifact_thresholdFieldTrip

Names checked 2026-09-18 against EEGLAB 2026.0.0 (plugins at the versions in EEGLAB’s own plugin list) and FieldTrip 20251218.

Reading

  1. Jas et al. (2017). Autoreject. unverified