Inspect your first EEG recording#

Estimated reading time:4 minutes

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")
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()
EEG
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)