Note
Go to the end to download the full example code.
Plotting EEG Topomap of Alpha/Theta Ratio with MNEΒΆ
Basic STRF fitting tutorial.
MNE is a popular python toolbox for analyzing neural data, and it has a lot of visualization capabilities. In this tutorial, we show how to interface between naplib-python and mne to produce EEG topomaps.
# Author: Gavin Mischler
#
# License: MIT
import os
from os import path
import openneuro
from mne.datasets import sample
import numpy as np
import matplotlib.pyplot as plt
import mne
from mne.viz import plot_topomap
from naplib.io import load_bids
Download EEG data from OpenNeuroΒΆ
Import data and get auditory spectrogram which will be used as stimulus.
dataset = 'ds002778'
subject = 'pd6'
bids_root = path.join(path.dirname(sample.data_path()), dataset)
print(bids_root)
if not path.isdir(bids_root):
os.makedirs(bids_root)
openneuro.download(dataset=dataset, target_dir=bids_root,
include=[f'sub-{subject}'])
/home/docs/mne_data/ds002778
π Hello! This is openneuro-py 2026.1.0. Great to see you! π€
π Please report problems π€― and bugs πͺ² at
https://github.com/hoechenberger/openneuro-py/issues
π Preparing to download ds002778 β¦
π Traversing directories for ds002778 : 0 entities [00:00, ? entities/s]
π Traversing directories for ds002778 : 7 entities [00:04, 1.43 entities/s]
π Traversing directories for ds002778 : 8 entities [00:05, 1.62 entities/s]
π Traversing directories for ds002778 : 10 entities [00:05, 2.14 entities/s]
π Traversing directories for ds002778 : 14 entities [00:05, 3.68 entities/s]
π Traversing directories for ds002778 : 15 entities [00:05, 3.72 entities/s]
π Traversing directories for ds002778 : 17 entities [00:06, 4.09 entities/s]
π Traversing directories for ds002778 : 20 entities [00:06, 3.10 entities/s]
π₯ Retrieving up to 19 files (5 concurrent downloads).
Skipping participants.tsv: already downloaded.: 100%|ββββββββββ| 1.62k/1.62k [00:00<?, ?B/s]
Skipping CHANGES: already downloaded.: 100%|ββββββββββ| 379/379 [00:00<?, ?B/s]
Skipping participants.json: already downloaded.: 100%|ββββββββββ| 1.24k/1.24k [00:00<?, ?B/s]
Skipping README: already downloaded.: 100%|ββββββββββ| 4.55k/4.55k [00:00<?, ?B/s]
Skipping dataset_description.json: already downloaded.: 100%|ββββββββββ| 354/354 [00:00<?, ?B/s]
Skipping sub-pd6_ses-off_task-rest_eeg.bdf: already downloaded.: 100%|ββββββββββ| 11.5M/11.5M [00:00<?, ?B/s]
Skipping sub-pd6_ses-off_scans.tsv: already downloaded.: 100%|ββββββββββ| 75.0/75.0 [00:00<?, ?B/s]
Skipping sub-pd6_ses-off_task-rest_beh.json: already downloaded.: 100%|ββββββββββ| 433/433 [00:00<?, ?B/s]
Skipping sub-pd6_ses-off_task-rest_channels.tsv: already downloaded.: 100%|ββββββββββ| 2.22k/2.22k [00:00<?, ?B/s]
Skipping sub-pd6_ses-off_task-rest_beh.tsv: already downloaded.: 100%|ββββββββββ| 10.0/10.0 [00:00<?, ?B/s]
Skipping sub-pd6_ses-off_task-rest_eeg.json: already downloaded.: 100%|ββββββββββ| 471/471 [00:00<?, ?B/s]
Skipping sub-pd6_ses-off_task-rest_events.tsv: already downloaded.: 100%|ββββββββββ| 66.0/66.0 [00:00<?, ?B/s]
Skipping sub-pd6_ses-on_scans.tsv: already downloaded.: 100%|ββββββββββ| 74.0/74.0 [00:00<?, ?B/s]
Skipping sub-pd6_ses-on_task-rest_beh.tsv: already downloaded.: 100%|ββββββββββ| 10.0/10.0 [00:00<?, ?B/s]
Skipping sub-pd6_ses-on_task-rest_beh.json: already downloaded.: 100%|ββββββββββ| 433/433 [00:00<?, ?B/s]
Skipping sub-pd6_ses-on_task-rest_eeg.bdf: already downloaded.: 100%|ββββββββββ| 17.4M/17.4M [00:00<?, ?B/s]
Skipping sub-pd6_ses-on_task-rest_eeg.json: already downloaded.: 100%|ββββββββββ| 471/471 [00:00<?, ?B/s]
Skipping sub-pd6_ses-on_task-rest_channels.tsv: already downloaded.: 100%|ββββββββββ| 2.22k/2.22k [00:00<?, ?B/s]
Skipping sub-pd6_ses-on_task-rest_events.tsv: already downloaded.: 100%|ββββββββββ| 51.0/51.0 [00:00<?, ?B/s]
β
Finished downloading ds002778.
π§ Please enjoy your brains.
Read the data into a Data objectΒΆ
# We are only interested in the 32-channel EEG data as the responses, so select those channels
resp_channels = ['Fp1','AF3','F7','F3','FC1','FC5','T7','C3','CP1','CP5','P7',
'P3','Pz','PO3','O1','Oz','O2','PO4','P4','P8','CP6','CP2',
'C4','T8','FC6','FC2','F4','F8','AF4','Fp2','Fz','Cz']
data = load_bids(root=bids_root, subject=subject, datatype='eeg', task='rest', suffix='eeg', session='off', resp_channels=resp_channels)
Extracting BDF parameters from /home/docs/mne_data/ds002778/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_eeg.bdf...
Setting channel info structure...
Creating raw.info structure...
Reading events from /home/docs/mne_data/ds002778/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_events.tsv.
Reading channel info from /home/docs/mne_data/ds002778/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_channels.tsv.
Not fully anonymizing info - keeping hand, his_id, sex of subject_info
/home/docs/checkouts/readthedocs.org/user_builds/naplib-python/checkouts/latest/naplib/io/load_bids.py:103: RuntimeWarning: Unable to map the following column(s) to to MNE:
gender: f
MMSE: 30
NAART: 42
disease_duration: 8
rl_deficits: L OFF meds, more R ON meds
notes: Used preprocessed data from EEGLAB .mat file instead of raw data for pd on
raw = read_raw_bids(bids_path=bids_path)
NOTE: pick_channels() is a legacy function. New code should use inst.pick(...).
NOTE: pick_types() is a legacy function. New code should use inst.pick(...).
NOTE: pick_channels() is a legacy function. New code should use inst.pick(...).
NOTE: pick_types() is a legacy function. New code should use inst.pick(...).
0%| | 0/2 [00:00<?, ?it/s]
100%|ββββββββββ| 2/2 [00:00<00:00, 49.27it/s]
Compute Alpha Theta RatioΒΆ
Let's compute the Alpha/Theta Ratio in each channel. We use the log-value so that ratios above 1 are positive and ratios below 1 are negative, which makes the resulting topomap more clear.
def log_alpha_theta_ratio(response, sfreq):
'''response should be of shape (time * channels)'''
# must transpose response for mne function
alpha_psd, _ = mne.time_frequency.psd_array_welch(response.T, sfreq, fmin=8, fmax=13, verbose=False) # psd is shape (channels * freqs)
alpha_psd = alpha_psd.mean(-1)
theta_psd, _ = mne.time_frequency.psd_array_welch(response.T, sfreq, fmin=4, fmax=8, verbose=False) # psd is shape (channels * freqs)
theta_psd = theta_psd.mean(-1)
return np.log(alpha_psd / theta_psd)
alpha_theta_ratio = [log_alpha_theta_ratio(trial['resp'], trial['sfreq']) for trial in data]
Visualize Results with MNEΒΆ
The Data contains the mne_info attribute (data.mne_info) which we can use for plotting This info is an instance of mne.Info, and it contains measurement information like channel names, locations, etc, as well as other metadata
# First, we need to set the montage (i.e. the arrangement of electrodes) so that the channels can be plotted properly
# Here, we set it to the standard 10-20 system, but many options are available if the data were recorded in a different
# montage. See https://mne.tools/dev/generated/mne.channels.make_standard_montage.html for details
data.mne_info.set_montage('standard_1020')
fig, ax = plt.subplots()
ax.set_title('Trial 1 Alpha/Theta Ratio')
plot_topomap(alpha_theta_ratio[0], data.mne_info, axes=ax)
plt.show()

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