Note
Go to the end to download the full example code or to run this example in your browser via Binder.
Inspect your first EEG recording#
Open one recording and inspect its real voltage traces and spectrum.
These real Nakanishi2015 SSVEP recordings are distributed as the processed
nm000118 release
(study).
Filtering, downsampling and latency handling were already applied; do not
shift the event onsets again. CPU is sufficient. Internet is needed for the
first download; EEGDASH_CACHE_DIR keeps downloads across runs.
The explicit subset uses 1 participant(s), about 7.0 MB of signal files.
Before you start#
Install EEGDash and its dependencies. Tutorial 00 introduces the catalogue
query, but this script runs independently. You will need basic NumPy indexing.
The useful output is an MNE Raw object whose channels, units and event
labels you have inspected before making training windows.
import os
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
from eegdash import EEGDashDataset
1. Load and inspect the selected recordings#
The dataset constructor retrieves descriptions; accessing recording.raw
opens the signal file and acquires it if absent from the cache. Count
dataset.datasets to count recordings. The concatenated dataset’s length
has a different meaning and should not be used as the recording count.
SSVEP means a response to repeated visual stimulation. Here annotation
strings name the attended flicker frequency in Hz. Sorting with key=float
keeps numerical frequency order; sorting strings or relying on automatic
integer event codes need not do that. The channel and rate checks establish
the array contract that the next tutorials reuse.
cache_dir = Path(os.environ.get("EEGDASH_CACHE_DIR", ".eegdash_cache"))
subjects = ["1"]
dataset = EEGDashDataset(
cache_dir=cache_dir,
dataset="nm000118",
subject=subjects,
session="0",
run="0",
task="ssvep",
n_jobs=1,
)
assert len(dataset.datasets) == len(subjects)
print(dataset.description[["subject", "session", "run"]])
raw = dataset.datasets[0].raw
sfreq = raw.info["sfreq"]
channel_names = raw.ch_names
class_names = sorted(set(raw.annotations.description), key=float)
mapping = {name: index for index, name in enumerate(class_names)}
assert len(mapping) == 12
for recording in dataset.datasets:
assert recording.raw.ch_names == channel_names
assert recording.raw.info["sfreq"] == sfreq
assert set(recording.raw.annotations.description) == set(mapping)
print(f"Channels: {channel_names}; sampling rate: {sfreq} Hz")
print("Observed stimulus frequencies (Hz):", class_names)
subject session run
0 1 0 0
Downloading sub-1_ses-0_task-ssvep_run-0_eeg.set: 0%| | 0.00/6.71M [00:00<?, ?B/s]
Downloading sub-1_ses-0_task-ssvep_run-0_eeg.set: 0%| | 0.00/6.71M [00:00<?, ?B/s]
Downloading sub-1_ses-0_task-ssvep_run-0_eeg.set: 15%|█▍ | 1.00M/6.71M [00:00<00:03, 1.66MB/s]
Downloading sub-1_ses-0_task-ssvep_run-0_eeg.set: 45%|████▍ | 3.00M/6.71M [00:00<00:00, 3.92MB/s]
Downloading sub-1_ses-0_task-ssvep_run-0_eeg.set: 100%|██████████| 6.71M/6.71M [00:00<00:00, 7.18MB/s]
[09/11/26 17:43:09] INFO HTTP Request: GET _client.py:1025
https://data.nemar.org/nm000118/
"HTTP/1.1 200 OK"
[09/11/26 17:43:10] INFO HTTP Request: GET _client.py:1025
https://data.nemar.org/nm000118/v1.
0.2/manifest.json "HTTP/1.1 200 OK"
INFO HTTP Request: GET _client.py:1025
https://raw.githubusercontent.com/n
emarDatasets/nm000118/v1.0.2/sub-1/
ses-0/eeg/sub-1_ses-0_task-ssvep_ru
n-0_channels.tsv "HTTP/1.1 200 OK"
INFO HTTP Request: GET _client.py:1025
https://raw.githubusercontent.com/n
emarDatasets/nm000118/v1.0.2/sub-1/
ses-0/eeg/sub-1_ses-0_task-ssvep_ru
n-0_events.tsv "HTTP/1.1 200 OK"
INFO HTTP Request: GET _client.py:1025
https://raw.githubusercontent.com/n
emarDatasets/nm000118/v1.0.2/sub-1/
ses-0/eeg/sub-1_ses-0_task-ssvep_ru
n-0_events.json "HTTP/1.1 200 OK"
INFO HTTP Request: GET _client.py:1025
https://raw.githubusercontent.com/n
emarDatasets/nm000118/v1.0.2/sub-1/
ses-0/eeg/sub-1_ses-0_task-ssvep_ru
n-0_eeg.json "HTTP/1.1 200 OK"
INFO HTTP Request: GET _client.py:1025
https://raw.githubusercontent.com/n
emarDatasets/nm000118/v1.0.2/datase
t_description.json "HTTP/1.1 200
OK"
[09/11/26 17:43:11] INFO HTTP Request: GET _client.py:1025
https://raw.githubusercontent.com/n
emarDatasets/nm000118/v1.0.2/partic
ipants.tsv "HTTP/1.1 200 OK"
INFO HTTP Request: GET _client.py:1025
https://raw.githubusercontent.com/n
emarDatasets/nm000118/v1.0.2/partic
ipants.json "HTTP/1.1 200 OK"
INFO HTTP Request: GET _client.py:1025
https://raw.githubusercontent.com/n
emarDatasets/nm000118/v1.0.2/README
.md "HTTP/1.1 200 OK"
[09/11/26 17:43:12] INFO HTTP Request: GET _client.py:1025
https://raw.githubusercontent.com/n
emarDatasets/nm000118/v1.0.2/.bidsi
gnore "HTTP/1.1 200 OK"
Channels: ['PO7', 'PO3', 'POz', 'PO4', 'PO8', 'O1', 'Oz', 'O2']; sampling rate: 256.0 Hz
Observed stimulus frequencies (Hz): ['9.25', '9.75', '10.25', '10.75', '11.25', '11.75', '12.25', '12.75', '13.25', '13.75', '14.25', '14.75']
2. Inspect voltage and annotations#
get_data returns (channels, samples). At 256 Hz, four seconds contain
1,024 samples, and stop is exclusive. The plotted transpose places samples
on the horizontal axis and gives each channel its own line. Multiplying by
1e6 changes display units from volts to microvolts, not the cached signal.
Inspect the annotations table alongside the trace: an event description tells you the stimulus condition, whereas a waveform shows the recorded response. A finite-array assertion catches invalid numerical values; it does not rule out artifacts, clipping or a poorly connected electrode.
print(raw)
print(raw.annotations.to_data_frame().head())
signal = raw.get_data(picks="eeg", start=0, stop=int(4 * sfreq))
assert signal.shape[0] == len(channel_names) and np.isfinite(signal).all()
fig, ax = plt.subplots(figsize=(9, 4), layout="constrained")
ax.plot(np.arange(signal.shape[1]) / sfreq, signal.T * 1e6)
ax.set(xlabel="Time (s)", ylabel="Voltage (µV)", title="Recorded first trial")

<RawEEGLAB | sub-1_ses-0_task-ssvep_run-0_eeg.set, 8 x 218520 (853.6 s), ~15 KiB, data not loaded>
onset ... HED
0 1970-01-01 00:00:00.343750 ... Sensory-event, Experimental-stimulus, Visual-p...
1 1970-01-01 00:00:05.085938 ... Sensory-event, Experimental-stimulus, Visual-p...
2 1970-01-01 00:00:09.828125 ... Sensory-event, Experimental-stimulus, Visual-p...
3 1970-01-01 00:00:14.570312 ... Sensory-event, Experimental-stimulus, Visual-p...
4 1970-01-01 00:00:19.312500 ... Sensory-event, Experimental-stimulus, Visual-p...
[5 rows x 4 columns]
[Text(0.5, 18.503588541666666, 'Time (s)'), Text(18.503588541666666, 0.5, 'Voltage (µV)'), Text(0.5, 1.0, 'Recorded first trial')]
3. Inspect the supplied channel geometry and spectrum#
The averaged power spectral density summarizes how signal power is distributed across frequency over the recording. The 40 Hz display limit focuses on the low-frequency range and does not apply another filter. A spectral peak can suggest a rhythm or stimulus response, but this whole-recording average mixes all twelve attended frequencies and cannot validate a class label by itself.
Channel coordinates describe where sensors were placed; channel names and order determine which signal is which. Neither an attractive montage nor a smooth spectrum replaces inspection of the individual trial voltages.
print("Supplied montage:", raw.get_montage())
raw.compute_psd(fmax=40, picks="eeg").plot(average=True, show=False)
plt.show()

Supplied montage: <DigMontage | 0 extras (headshape), 0 HPIs, 3 fiducials, 8 channels>
Effective window size : 8.000 (s)
Plotting power spectral density (dB=True).
Try another observed trial by changing the start and stop sample indices. The release concatenates trials; their order is not acquisition chronology.
Continue with event-labelled windows#
Tutorial 02 converts this same recording into four-second windows and shows where its annotation-derived label appears in a DataLoader batch. When inspecting another trial manually, derive its start from the annotation onset rather than assuming four-second trials are adjacent with no remainder.
Total running time of the script: (0 minutes 5.750 seconds)