Note
Go to the end to download the full example code or to run this example in your browser via Binder.
Inspect an auditory oddball response in real EEG#
Use EEGDashDataset to load one auditory-P300 run from OpenNeuro
ds003061, inspect its actual labels and plot standard/oddball ERPs.
Subject 001, run 2 contains about 63.4 MB of signal data. Selecting the
run avoids downloading the other two recordings of this participant.
See the dataset. CPU is sufficient;
set EEGDASH_CACHE_DIR to retain downloads between runs.
The goal is to trace an observed stimulus marker into a measured response: load the recording, select the two documented event types, create aligned trials, and average each condition. An ERP is an average voltage time course; it does not assign a predicted label to a new trial. This page therefore ends with a descriptive comparison rather than a fitted classifier.
With EEGDash installed, run the blocks in order. Basic MNE Raw concepts from the first-recording page are sufficient. You will inspect the retained trial counts and array shape, a Cz waveform plot and a mean difference in microvolts.
1. Load exactly one recording and inspect its annotations#
Subject identifiers are strings: retain the leading zeros in 001.
The task and run filters bound acquisition before any samples are read.
Printing annotation counts makes the source vocabulary visible and avoids
accidentally treating responses or unrelated markers as standard stimuli.
import os
from pathlib import Path
import matplotlib.pyplot as plt
import mne
import numpy as np
import pandas as pd
from braindecode.preprocessing import RemoveCommonAverageReference, RemoveDCOffset
from eegdash import EEGDashDataset
from eegdash.features import signal_mean
cache_dir = Path(os.environ.get("EEGDASH_CACHE_DIR", ".eegdash_cache"))
dataset = EEGDashDataset(
cache_dir=cache_dir,
dataset="ds003061",
subject="001",
task="P300",
run="2",
n_jobs=1,
)
assert len(dataset.datasets) == 1
raw = dataset.datasets[0].raw.copy().load_data().pick("eeg")
print(pd.Series(raw.annotations.description).value_counts())
# Preserve the source marker spelling below, including its typo.
# These literal names are the label source, including the response qualification
# on the oddball class. Consequently the comparison concerns the selected
# response-associated oddballs, not every possible rare-stimulus outcome.
mapping = {"stimulus/standard": 1, "stimulus/oddball_with_reponse": 2}
assert set(mapping) <= set(raw.annotations.description)
assert "Cz" in raw.ch_names, "This ERP comparison requires Cz"
[09/16/26 20:46:44] INFO Auto-corrected misrouted dataset.py:561
storage.base for dataset on003061:
s3://nemar/on003061 ->
s3://openneuro.org/ds003061
Downloading sub-001_task-P300_run-2_eeg.set: 0%| | 0.00/60.5M [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_eeg.set: 0%| | 0.00/60.5M [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_eeg.set: 2%|▏ | 1.00M/60.5M [00:00<00:13, 4.70MB/s]
Downloading sub-001_task-P300_run-2_eeg.set: 30%|██▉ | 18.0M/60.5M [00:00<00:01, 33.4MB/s]
Downloading sub-001_task-P300_run-2_eeg.set: 60%|█████▉ | 36.0M/60.5M [00:00<00:00, 49.3MB/s]
Downloading sub-001_task-P300_run-2_eeg.set: 89%|████████▉ | 54.0M/60.5M [00:01<00:00, 58.5MB/s]
Downloading sub-001_task-P300_run-2_eeg.set: 100%|██████████| 60.5M/60.5M [00:01<00:00, 57.4MB/s]
Downloading sub-001_task-P300_run-2_channels.tsv: 0%| | 0.00/1.12k [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_channels.tsv: 0%| | 0.00/1.12k [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_channels.tsv: 100%|██████████| 1.12k/1.12k [00:00<00:00, 24.3kB/s]
Downloading sub-001_task-P300_run-2_events.tsv: 0%| | 0.00/44.2k [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_events.tsv: 0%| | 0.00/44.2k [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_events.tsv: 100%|██████████| 44.2k/44.2k [00:00<00:00, 879kB/s]
Downloading sub-001_task-P300_run-2_events.json: 0%| | 0.00/1.85k [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_events.json: 0%| | 0.00/1.85k [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_events.json: 100%|██████████| 1.85k/1.85k [00:00<00:00, 36.9kB/s]
Downloading sub-001_task-P300_run-2_electrodes.tsv: 0%| | 0.00/1.68k [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_electrodes.tsv: 0%| | 0.00/1.68k [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_electrodes.tsv: 100%|██████████| 1.68k/1.68k [00:00<00:00, 55.8kB/s]
Downloading sub-001_task-P300_run-2_coordsystem.json: 0%| | 0.00/97.0 [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_coordsystem.json: 0%| | 0.00/97.0 [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_coordsystem.json: 100%|██████████| 97.0/97.0 [00:00<00:00, 3.31kB/s]
Downloading sub-001_task-P300_run-2_eeg.json: 0%| | 0.00/1.34k [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_eeg.json: 0%| | 0.00/1.34k [00:00<?, ?B/s]
Downloading sub-001_task-P300_run-2_eeg.json: 100%|██████████| 1.34k/1.34k [00:00<00:00, 34.1kB/s]
Reading 0 ... 193791 = 0.000 ... 756.996 secs...
stimulus/standard 527
response 112
stimulus/noise 111
stimulus/oddball_with_reponse 111
stimulus/noise_with_reponse 1
Name: count, dtype: int64
2. Filter and epoch relative to the actual stimulus onset#
Keep the pre-stimulus baseline. We use MNE Epochs so the stated interval is independent of the annotation duration; resample after event extraction to preserve event timing on the original recording’s sample grid. EEGPrep removes the per-channel median offset and applies its common average reference. Neither operation changes sample count or rate. We verify that grid and restore the original event annotations to avoid rounding their latencies through the MNE↔EEGLAB conversion. Filtering to 0.5–30 Hz attenuates drift and faster activity while keeping a broad ERP time course. These settings affect the measured waveform; hold them fixed when comparing recordings.
Each epoch includes 100 ms before and 600 ms after the stimulus. Baseline
correction subtracts the pre-stimulus channel mean within each trial.
get_data() returns (trials, channels, time samples), with voltages in
volts. Resampling to 128 Hz changes the last dimension, not trial identity.
The printed condition counts refer to retained epochs, whereas the earlier
counts describe all annotations. Epochs can be lost at recording boundaries
or existing bad spans; missing conditions stop the example rather than
silently producing an empty average.
source_annotations = raw.annotations.copy()
source_date = raw.info["meas_date"]
source_grid = (raw.info["sfreq"], raw.n_times, raw.first_samp)
RemoveDCOffset().apply(raw)
RemoveCommonAverageReference().apply(raw)
assert (raw.info["sfreq"], raw.n_times, raw.first_samp) == source_grid
raw.set_meas_date(source_date)
raw.set_annotations(source_annotations)
raw.filter(0.5, 30.0)
events, _ = mne.events_from_annotations(raw, event_id=mapping)
epochs = mne.Epochs(
raw,
events,
event_id={"standard": 1, "oddball": 2},
tmin=-0.1,
tmax=0.6,
baseline=(-0.1, 0),
preload=True,
reject_by_annotation=True,
)
epochs.resample(128)
assert np.isfinite(epochs.get_data()).all()
assert all(len(epochs[name]) > 1 for name in epochs.event_id)
print({name: len(epochs[name]) for name in epochs.event_id})
print("Epoch shape:", epochs.get_data().shape)
/tmp/tmpuku7b8_3.set
Field 'subject' is missing from the EEG dictionnary, adding it.
Field 'group' is missing from the EEG dictionnary, adding it.
Field 'condition' is missing from the EEG dictionnary, adding it.
Field 'session' is missing from the EEG dictionnary, adding it.
Field 'comments' is missing from the EEG dictionnary, adding it.
Field 'times' is missing from the EEG dictionnary, adding it.
Field 'icaact' is missing from the EEG dictionnary, adding it.
Field 'icachansind' is missing from the EEG dictionnary, adding it.
Field 'urchanlocs' is missing from the EEG dictionnary, adding it.
Field 'urevent' is missing from the EEG dictionnary, adding it.
Field 'eventdescription' is missing from the EEG dictionnary, adding it.
Field 'epoch' is missing from the EEG dictionnary, adding it.
Field 'epochdescription' is missing from the EEG dictionnary, adding it.
Field 'stats' is missing from the EEG dictionnary, adding it.
Field 'specdata' is missing from the EEG dictionnary, adding it.
Field 'specicaact' is missing from the EEG dictionnary, adding it.
Field 'splinefile' is missing from the EEG dictionnary, adding it.
Field 'icasplinefile' is missing from the EEG dictionnary, adding it.
Field 'dipfit' is missing from the EEG dictionnary, adding it.
Field 'history' is missing from the EEG dictionnary, adding it.
Field 'saved' is missing from the EEG dictionnary, adding it.
Field 'etc' is missing from the EEG dictionnary, adding it.
Field 'datfile' is missing from the EEG dictionnary, adding it.
Field 'run' is missing from the EEG dictionnary, adding it.
Field 'roi' is missing from the EEG dictionnary, adding it.
/tmp/tmp_w8sbeq2.set
Field 'subject' is missing from the EEG dictionnary, adding it.
Field 'group' is missing from the EEG dictionnary, adding it.
Field 'condition' is missing from the EEG dictionnary, adding it.
Field 'session' is missing from the EEG dictionnary, adding it.
Field 'comments' is missing from the EEG dictionnary, adding it.
Field 'times' is missing from the EEG dictionnary, adding it.
Field 'icaact' is missing from the EEG dictionnary, adding it.
Field 'icachansind' is missing from the EEG dictionnary, adding it.
Field 'urchanlocs' is missing from the EEG dictionnary, adding it.
Field 'urevent' is missing from the EEG dictionnary, adding it.
Field 'eventdescription' is missing from the EEG dictionnary, adding it.
Field 'epoch' is missing from the EEG dictionnary, adding it.
Field 'epochdescription' is missing from the EEG dictionnary, adding it.
Field 'stats' is missing from the EEG dictionnary, adding it.
Field 'specdata' is missing from the EEG dictionnary, adding it.
Field 'specicaact' is missing from the EEG dictionnary, adding it.
Field 'splinefile' is missing from the EEG dictionnary, adding it.
Field 'icasplinefile' is missing from the EEG dictionnary, adding it.
Field 'dipfit' is missing from the EEG dictionnary, adding it.
Field 'history' is missing from the EEG dictionnary, adding it.
Field 'saved' is missing from the EEG dictionnary, adding it.
Field 'etc' is missing from the EEG dictionnary, adding it.
Field 'datfile' is missing from the EEG dictionnary, adding it.
Field 'run' is missing from the EEG dictionnary, adding it.
Field 'roi' is missing from the EEG dictionnary, adding it.
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 0.5 - 30 Hz
FIR filter parameters
---------------------
Designing a one-pass, zero-phase, non-causal bandpass filter:
- Windowed time-domain design (firwin) method
- Hamming window with 0.0194 passband ripple and 53 dB stopband attenuation
- Lower passband edge: 0.50
- Lower transition bandwidth: 0.50 Hz (-6 dB cutoff frequency: 0.25 Hz)
- Upper passband edge: 30.00 Hz
- Upper transition bandwidth: 7.50 Hz (-6 dB cutoff frequency: 33.75 Hz)
- Filter length: 1691 samples (6.605 s)
Used Annotations descriptions: ['stimulus/oddball_with_reponse', 'stimulus/standard']
Not setting metadata
638 matching events found
Applying baseline correction (mode: mean)
0 projection items activated
Using data from preloaded Raw for 638 events and 181 original time points ...
1 bad epochs dropped
{'standard': 526, 'oddball': 111}
Epoch shape: (637, 64, 90)
3. Plot the measured response at Cz#
Compare conditions within this recording. A difference in this small
sample does not by itself distinguish MMN, P3a and P3b generators.
average() reduces the trial axis separately for each condition. MNE
retains the time and channel information in an Evoked object and displays
EEG amplitudes in microvolts. More trials can make an average less noisy;
the two conditions need not have equal counts.
EEGDash’s signal_mean averages the oddball-minus-standard waveform from
250 to 400 ms at the preselected Cz channel. Positive values mean the
oddball average is more positive in that interval under this reference;
negative values are valid too. This interval mean is not a peak latency,
a significance test or evidence that a new participant will show the effect.
evokeds = {name: epochs[name].average() for name in epochs.event_id}
mne.viz.plot_compare_evokeds(evokeds, picks="Cz", show=False)
difference = mne.combine_evoked([evokeds["oddball"], evokeds["standard"]], [1, -1])
interval = (difference.times >= 0.25) & (difference.times <= 0.4)
cz = difference.ch_names.index("Cz")
print(
"Mean oddball-minus-standard at Cz, 250–400 ms (µV):",
signal_mean(difference.data[cz, interval]) * 1e6,
)
plt.show()

Mean oddball-minus-standard at Cz, 250–400 ms (µV): 3.950679283942988
4. Decide what evidence the next analysis needs#
Inspect bad channels and artifacts before treating the difference as a physiological result: existing bad annotations are respected, but no new artifact detector is fitted here. To estimate between-participant variation, repeat the same fixed analysis for additional real participants and compare their interval means; individual trials are not independent participants. For a descriptive auditory/visual comparison, run the visual tutorial with matched reference, units and time axes. Different tasks and participants prevent that plot alone from establishing a modality effect.
Total running time of the script: (0 minutes 7.468 seconds)