Experience the task
You're about to do a short version of the exact task a real participant performed while wearing EEG.
You'll see two words, one at a time: first a prime word, then a target word. Decide whether the target is related in meaning to the prime. Press F if related, J if unrelated — as quickly and accurately as you can.
The task takes about a minute. Complete it to see what happened in a real participant's brain while they did the same thing.
About this data
This is real, publicly available EEG from the ERP CORE dataset (Kappenman et al., 2021), recorded during a word-pair semantic judgement task. On every trial, a prime word was followed by a target word, and the participant judged whether the target word was semantically (related, e.g. "bread" → "butter") or (unrelated, e.g. "bread" → "hammer") to the prime, ~50% of trials each.
The signal is recorded from 30 electrodes placed across the scalp using the extended 10–20 system, plus a VEOG channel near the eye to detect blinks, re-referenced to the average of the two mastoid electrodes (P9/P10). Click an electrode on the head diagram or use the dropdown to switch between them — notice how the signal looks different at each location (e.g. blinks are biggest at the front, the N400 effect is biggest towards the centre/back).
Raw signal
Filtering the signal
The problem
The raw EEG signal is contaminated by noise: slow voltage drifts from the electrodes, high-frequency electrical interference from muscles and equipment, and a persistent 60 Hz hum from the mains power supply. Occasional large deflections from eye blinks can also swamp the brain signal.
The fix — digital filters
A bandpass filter keeps only the frequencies we care about (typically 1–30 Hz for ERP work), removing slow drifts and high-frequency noise. A notch filter specifically targets the 60 Hz power-line hum. Adjust the settings and compare the signal before and after filtering.
Python code (MNE-Python)
import mne
# Load raw EEG data
raw = mne.io.read_raw_edf('recording.edf', preload=True)
# Bandpass filter: keep 0.1–30 Hz (gentle high-pass preserves slow ERP components)
raw.filter(l_freq=0.1, h_freq=30.0, method='iir')
# Notch filter: remove 60 Hz power-line noise
raw.notch_filter(freqs=60.0)
# Plot before and after
raw.plot(duration=5, scalings='auto')Blink removal
The problem — eye blinks
Even after filtering, the signal still contains large artefacts from eye blinks. The eye acts like a small battery (the cornea is positive, the retina negative). Every time you blink, the eyelid slides over this electrical dipole and creates a huge voltage swing (50–100 µV) that is picked up by EEG electrodes — especially those near the forehead.
Go back to the raw signal and switch between electrodes — notice how blinks are massive at Fp1/Fp2 (forehead), visible at Fz, and nearly invisible at Oz (back of the head).
The fix — detect and subtract
First, we detect blinks by bandpass filtering the VEOG channel to 1–10 Hz (isolating the blink frequency range) and finding positive peaks above an automatic threshold (mean + 3 SD).
Then, we remove the blink from every EEG channel using regression. Because the VEOG channel records the blink directly, we can estimate how much of that blink signal leaked into each EEG electrode and subtract it out. Channels near the eyes (Fp1, Fp2) get a large correction; channels at the back of the head (Oz) get almost none. The brain signal underneath is preserved.
Python code (MNE-Python)
import mne
# Detect blinks in the VEOG channel
eog_events = mne.preprocessing.find_eog_events(raw, ch_name='VEOG')
print(f'Detected {len(eog_events)} blinks')
# Regression-based correction: estimate blink component
# in each channel using VEOG as reference, then subtract
# (SSP projection is MNE's built-in version of this)
projs, _ = mne.preprocessing.compute_proj_eog(raw, ch_name='VEOG')
raw.add_proj(projs)
raw.apply_proj()
# For ICA-based removal (more advanced):
# ica = mne.preprocessing.ICA(n_components=15)
# ica.fit(raw)
# ica.exclude = [0] # blink component
# ica.apply(raw)Epoching — cutting the signal into trials
The problem
The brain's response to each individual stimulus is tiny — only a few millionths of a volt — completely buried in the ongoing background noise. Looking at the continuous signal, you cannot tell when a target word appeared.
The fix — epoching
We cut the filtered signal into short segments (epochs) centred on each target word's onset. Each epoch captures the brain's response to one word, from a short baseline before the word appeared to several hundred milliseconds after. Any epoch that still exceeds an amplitude threshold — a sign something noisy slipped through — is rejected.
Python code (MNE-Python)
import mne
events = mne.find_events(raw)
event_id = {'related': 1, 'unrelated': 2}
# Cut into epochs around each event
epochs = mne.Epochs(raw, events, event_id,
tmin=-0.2, tmax=0.8,
baseline=None, # no baseline yet
reject=dict(eeg=100e-6)) # reject noisy epochs
print(f'Kept {len(epochs)} of {len(events)} epochs')Baseline correction
The problem
Each epoch starts at a slightly different voltage level, depending on the ongoing brain activity at the moment the stimulus arrived. These random offsets mean that epochs are not directly comparable — a positive voltage in one epoch might just reflect a higher baseline, not a brain response.
The fix — subtract the pre-stimulus mean
For each epoch, we calculate the mean voltage during the pre-stimulus period (the baseline window before the target word appeared) and subtract it from the entire epoch. This shifts every epoch so that the pre-stimulus period sits at zero, making the post-stimulus response directly comparable across trials.
Python code (MNE-Python)
# Baseline correction is usually applied during epoching:
epochs = mne.Epochs(raw, events, event_id,
tmin=-0.2, tmax=0.8,
baseline=(-0.2, 0)) # subtract mean of -200 to 0 ms
# Or applied separately:
epochs.apply_baseline(baseline=(-0.2, 0))Averaging — revealing the ERP
The problem
Even after epoching and baseline correction, each individual trial is still dominated by noise. The brain's response on any single trial is invisible.
The fix — averaging across trials
Random noise varies from trial to trial and cancels out when we average, but the brain's consistent response — the event-related potential (ERP) — survives. Compare the averaged related and unrelated ERPs below: around 300–500 ms after the target word, the unrelated waveform dips more negative than the related one. That dip is the N400 — the brain's response to a word that doesn't fit the meaning set up by the prime. The bigger the mismatch in meaning, the bigger the N400.
Python code (MNE-Python)
# Average by condition
related_erp = epochs['related'].average()
unrelated_erp = epochs['unrelated'].average()
# Plot both ERPs overlaid
mne.viz.plot_compare_evokeds(
{'Related': related_erp, 'Unrelated': unrelated_erp},
picks='eeg')Frequency-band decomposition
The idea
The EEG signal is a mixture of oscillations at different frequencies, each linked to different brain states and cognitive processes:
Delta (1–4 Hz) — deep sleep, unconscious processing
Theta (4–8 Hz) — drowsiness, memory encoding
Alpha (8–13 Hz) — relaxed wakefulness, eyes closed
Beta (13–30 Hz) — active thinking, concentration
Gamma (30–45 Hz) — binding information, high-level processing
By decomposing the signal into these bands, we can see which brain rhythms are present and how strong each one is.
Python code (MNE-Python)
import mne
import numpy as np
# Compute power spectral density using Welch's method
spectrum = raw.compute_psd(method='welch', fmin=1, fmax=40)
spectrum.plot()
# Compute power in each frequency band
bands = {'Delta': (1, 4), 'Theta': (4, 8),
'Alpha': (8, 13), 'Beta': (13, 30), 'Gamma': (30, 45)}
for name, (fmin, fmax) in bands.items():
power = spectrum.get_data(fmin=fmin, fmax=fmax).mean()
print(f'{name}: {power:.4f} µV²/Hz')
# Filter into individual bands for visualisation
alpha_raw = raw.copy().filter(l_freq=8, h_freq=13)
alpha_raw.plot(duration=5, title='Alpha band (8–13 Hz)')