Visualizing Raw EEG using MNE#
In this section we will see how to plot various attributes of a raw EEG data file using functions and methods provided by the MNE library. THis includes viewing data over time, over the scalp, and also plotting electrode locations on the scalp. We will use the same data file that we used in the previous section.
import mne
mne.set_log_level('error')
import numpy as np
import matplotlib.pyplot as plt
raw = mne.io.read_raw_brainvision('data/sub-001/sub-001.vhdr',
preload=True)
---------------------------------------------------------------------------
ImportError Traceback (most recent call last)
Cell In[1], line 1
----> 1 import mne
2 mne.set_log_level('error')
4 import numpy as np
File ~/miniforge3/envs/neural_data_science/lib/python3.12/site-packages/mne/__init__.py:22
19 __version__ = '0.16.2'
21 # have to import verbose first since it's needed by many things
---> 22 from .utils import (set_log_level, set_log_file, verbose, set_config,
23 get_config, get_config_path, set_cache_dir,
24 set_memmap_min_size, grand_average, sys_info, open_docs)
25 from .io.pick import (pick_types, pick_channels,
26 pick_channels_regexp, pick_channels_forward,
27 pick_types_forward, pick_channels_cov,
28 pick_channels_evoked, pick_info)
29 from .io.base import concatenate_raws
File ~/miniforge3/envs/neural_data_science/lib/python3.12/site-packages/lazy_loader/__init__.py:82, in attach.<locals>.__getattr__(name)
80 elif name in attr_to_modules:
81 submod_path = f"{package_name}.{attr_to_modules[name]}"
---> 82 submod = importlib.import_module(submod_path)
83 attr = getattr(submod, name)
85 # If the attribute lives in a file (module) with the same
86 # name as the attribute, ensure that the attribute and *not*
87 # the module is accessible on the package.
File ~/miniforge3/envs/neural_data_science/lib/python3.12/importlib/__init__.py:90, in import_module(name, package)
88 break
89 level += 1
---> 90 return _bootstrap._gcd_import(name[level:], package, level)
File ~/miniforge3/envs/neural_data_science/lib/python3.12/site-packages/mne/utils/_logging.py:20
16 from typing import Any, Callable, TypeVar
18 from decorator import FunctionMaker
---> 20 from .docs import fill_doc
22 logger = logging.getLogger("mne") # one selection here used across mne-python
23 logger.propagate = False # don't propagate (in case of multiple imports)
File ~/miniforge3/envs/neural_data_science/lib/python3.12/site-packages/mne/utils/docs.py:17
13 from copy import deepcopy
15 from decorator import FunctionMaker
---> 17 from ..defaults import HEAD_SIZE_DEFAULT
18 from ._bunch import BunchConst
20 # # # WARNING # # #
21 # This list must also be updated in doc/_templates/autosummary/class.rst if it
22 # is changed here!
ImportError: cannot import name 'HEAD_SIZE_DEFAULT' from 'mne.defaults' (/Users/aaron/miniforge3/envs/neural_data_science/lib/python3.12/site-packages/mne/defaults.py)
Descriptive statistics on channels#
We saw preivously how to get metadata from the raw
file using the .info
property. Another useful way of peeking into a raw file’s data is to use the .describe()
method to see the names of each channel, and the range of values in each channel.
raw.describe()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[2], line 1
----> 1 raw.describe()
NameError: name 'raw' is not defined
Set Montage#
The raw file contains the 10-20 system labels for each channel, but it doesn’t provide information about the location of each electrode on the scalp. Internally, MNE can represent the location of each channel in a 3D Cartesian coordinate system. This is necessary to plot the locations of the channels on the scalp, which is extremely useful when visualizing EEG data. The set of coordinates for each channel is called the montage
of the raw file. We can set the montage of the raw file to the 10-20 system using the set_montage()
method. This will add the location information to the raw file. MNE provies a large set of standard montages for many commercial EEG systems. For the present data, the EEG caps were manufactured by a company named Easycap, and so we load the montage for the Easycap system.
raw.set_montage('easycap-M1')
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[3], line 1
----> 1 raw.set_montage('easycap-M1')
NameError: name 'raw' is not defined
The coordinates for each location are stored in the 'dig'
attribute of the info
object. The dig
attribute is a list of objects of the MNE type mne.io._digitization.DigPoint
. Each electrode location is stored as a set of (x, y, z) Cartesian coordinates, i.e., positions in a 3D grid, where x is the left-right dimension, y is posterior-anterior, and z is inferior-posterior. The (0, 0, 0) location is inside the head, located in a plane defined by the bridge of the nose (called the nasion) and the left and right ear canals. As shown in the figure below, x goes from left (negative) to right (positive), y goes from posterior to anterior, and z goes from inferior to superior. In general, you won’t need to work with this coordinate system directly, but it’s useful to know how the data are represented.
raw.info['dig']
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[4], line 1
----> 1 raw.info['dig']
NameError: name 'raw' is not defined
Note above that the list of sensor locations does not contain the names of each sensor — the channel names we saw above. In general MNE’s data structures are designed to assume that channels are listed in the same order in all attributes of the data structure. So, the order of labels in the list of channel names from raw.info['ch_names']
corresponds to the order of the channel locations in raw.info['dig']
.
raw.info['ch_names']
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[5], line 1
----> 1 raw.info['ch_names']
NameError: name 'raw' is not defined
View electrode positions#
MNE provides tools for viewing channel locations in both 2D and 3D.
Tip
Always remember to use plt.show()
, or put a semicolon after an MNE plot command. Otherwise the plot will be drawn twice.
raw.plot_sensors(show_names=True)
plt.show()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[6], line 1
----> 1 raw.plot_sensors(show_names=True)
2 plt.show()
NameError: name 'raw' is not defined
Interactive Plot of Continuous EEG#
We could add more code to the above cell to do things like label the axes, add a title to indicate what electrode the data came from, make the x axis actually represent units of time, etc.. However, the great value of using MNE is that the developers have created customized functions for doing most everything you could ever want with EEG, with a lot less typing that if you were working with raw NumPy arrays and Matplotlib.
For example, MNE data classes like Raw
have a .plot
method that allows you to generate a nice-looking plot that automatically does all the hard work of formatting it appropriately. This function actually creates an interactive plot in a new window (which is why you won’t see output in this notebook file), which allows you to scroll through the entire continuous dataset (unfortunately this doesn’t work if you’re running Jupyter notebooks via a cloud service, but should work fine if you followed the instructions at the start of this course and are using VS Code).
The black area at the bottom shows the entire raw EEG recording, with a green vertical bar near the start indicating the position in the file that is currently plotted, and a drag bar to scroll through the data. The other colored vertical lines mark the times of event codes sent by the stimulus presentation program, which mark events of experimental interest.
Event codes (Markers / Triggers)#
Event codes — often called markers or triggers are codes that are sent by the stimulus presentation program to mark the times of events of experimental interest. The event codes are stored in the .vmrk
file in Brain Vision format. When we import the raw data into MNE, these are stored in an attribute called ._annotations
. Although we can access that directly (e.g., raw._annotations
), again MNE provides tools that makes this easier and generate more interpretable output: the events_from_annotations()
function:
mne.events_from_annotations(raw)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[7], line 1
----> 1 mne.events_from_annotations(raw)
NameError: name 'mne' is not defined
The result includes two outputs. The first is a NumPy array that has three columns, with one row for each event code in the data. The first column indicates the time of the event, and the last column stores the code associated with that event. These event codes are always represented as integers (since it is a NumPy array of integers).
The second output is a dictionary mapping labels for each event code, to the numerical codes themselves. Usually these are not very informative in the raw data. In a later lesson, we will learn how to map the numerical codes to meaningful labels based on the experimental design. For now, it’s simply important to understand how the event codes are stored in the Raw
data object.
One important thing to know is that the event codes are stored in the same units as the data, so in this case, the event codes are in units of samples, not milliseconds. MNE knows this and operates accordingly, but if you want to know the time of an event in milliseconds, you need to convert it yourself based on the sampling rate. In this case, our sampling rate is 500 Hz, if we wanted to see the event timings in milliseconds we can divide the event time by the sampling rate, and multiplying by 1000. We can do this below by first assigning the two outputs of mne.events_from_annotations()
to two variables, and then converting the values in the first column of the events `array`` to milliseconds:
events, event_dict = mne.events_from_annotations(raw)
# MNE expects the events array in terms of samples, so don't change it
# Create a copy of the events array and convert the first column to ms
events_ms = events.copy()
events_ms[:, 0] = events_ms[:, 0] / 500 * 1000
events_ms[:10]
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[8], line 1
----> 1 events, event_dict = mne.events_from_annotations(raw)
3 # MNE expects the events array in terms of samples, so don't change it
4 # Create a copy of the events array and convert the first column to ms
5 events_ms = events.copy()
NameError: name 'mne' is not defined
Again, it’s generally better to work with MNE objects using MNE’s own functions and methods, but sometimes it’s important to understand the events
array. For example, there may be circumstances when you need to modify event codes. For example, we might want to separately average trials on which participants made correct and incorrect responses. In that case, we would need to change the event codes for stimuli followed by incorrect responses to something different than the event codes for correct responses.
raw.describe()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[9], line 1
----> 1 raw.describe()
NameError: name 'raw' is not defined
Summary#
MNE Provides a number of methods for Raw
objects that allow us to access and visualize aspects of both the metadata — including channel locations — and the data itself. However, in practice these are useful things to be familiar with, and they can be used when you’re working with data from a system or format that you’re not familiar with to get a better idea of the structure of the data. But in routine usage, within the context of a specific experiment we can expect that all of the data have the same structure. In the next series of lessons we’ll walk through the steps that are routinely applied to any EEG data set, in order to prepare it for ERP analysis.