Note
Go to the end to download the full example code or to run this example in your browser via Binder.
Extract features from real EEG trials#
Compute band powers and preserve the labels and identities needed by tutorial 42.
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 3 participant(s), about 21.1 MB of signal files.
Before you start#
Install EEGDash and its dependencies. Tutorial 02 explains windows, and
tutorial 11 explains why their participant identifiers matter. This page runs
independently, then writes plot_40_features.csv and a JSON schema for
tutorial 42. Keep both files in the same cache directory.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from braindecode.preprocessing import create_windows_from_events
from eegdash.paths import get_default_cache_dir
from eegdash import EEGDashDataset
from functools import partial
from eegdash.features import (
FeatureExtractor,
extract_features,
spectral_bands_power,
spectral_preprocessor,
)
import json
1. Load and inspect the selected recordings#
The task is SSVEP frequency classification, but here the immediate goal is a readable feature table. The three recordings share eight posterior channels and a 256 Hz rate. Numeric ordering of annotation strings provides a stable class mapping; those labels describe the attended stimulus, not eye state.
cache_dir = get_default_cache_dir()
subjects = ["1", "2", "3"]
dataset = EEGDashDataset(
cache_dir=cache_dir,
dataset="nm000118",
subject=subjects,
session="0",
run="0",
task="ssvep",
n_jobs=1,
)
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)}
2. Window the observed trials#
Four seconds gives enough samples for repeated one-second spectral segments. Equal size and stride, together with dropping the final remainder, retain one window per annotated trial. Thus feature-table rows can be traced back to a recording and start sample instead of becoming anonymous vectors. The source event lasts 4.15 seconds; its final 0.15 seconds are unused.
window_size = int(4 * sfreq)
windows = create_windows_from_events(
dataset,
mapping=mapping,
window_size_samples=window_size,
window_stride_samples=window_size,
on_last_window="drop",
preload=True,
)
metadata = windows.get_metadata().reset_index(drop=True)
y = metadata["target"].to_numpy(dtype=int)
pd.crosstab(metadata["subject"], y)
4. Persist the exact table and column contract for tutorial 42#
CSV makes the handoff inspectable without an additional storage engine.
The JSON file supplies the authoritative feature-column order, mapping and
extraction parameters. Read BIDS identifiers explicitly as strings: otherwise
a CSV reader can reinterpret "0" or a zero-padded identifier as a number.
This tutorial owns both fixed names; rerunning replaces CSV and schema together.
output_path = cache_dir / "plot_40_features.csv"
cache_dir.mkdir(parents=True, exist_ok=True)
feature_table.to_csv(output_path, index=False)
(output_path.with_suffix(".json")).write_text(
json.dumps(
{
"dataset": "nm000118",
"subjects": subjects,
"session": "0",
"run": "0",
"task": "ssvep",
"mapping": mapping,
"feature_columns": feature_columns,
"sfreq": sfreq,
"window_samples": window_size,
"bands": bands,
"channels": channel_names,
"nperseg": int(sfreq),
"noverlap": 0,
"welch_window": "hamming",
"power_units": "V^2",
},
indent=2,
)
)
print("Saved:", output_path.resolve())
Saved: /home/runner/eegdash_cache/plot_40_features.csv
5. Inspect measured feature distributions#
These are log10 powers relative to 1 V², not decibels. Outliers stay visible. Broad bands discard fine SSVEP frequency detail; weak decoding can be valid. Run tutorial 42 with the same cache to fit from the saved CSV and feature list.
feature_table[["subject", "target", "frequency_hz"] + feature_columns[:4]].head()
log_power = np.log10(np.maximum(feature_table[feature_columns], 1e-30))
fig, ax = plt.subplots(figsize=(10, 4), layout="constrained")
ax.boxplot(log_power.to_numpy(), tick_labels=feature_columns, showfliers=True)
ax.tick_params(axis="x", rotation=90)
ax.set(ylabel="log10(power / 1 V²)", title="Recorded SSVEP band/channel features")
plt.show()

Total running time of the script: (0 minutes 1.271 seconds)