ECoG preprocessing

This tutorial introduces a simple preprocessing pipeline to ready raw ECoG signal from an EDF for further analyses.

Open in Colab

[ ]:
!pip install mne mne_bids
[1]:
import mne
from mne_bids import BIDSPath

Download the data

We will demonstrate preprocessing for one subject’s raw ECoG data. The data is stored in a common EDF format which we’ll load and process with MNE. The raw data is stored in the dataset’s top directory, inside the ieeg data type. First, we’ll build the appropriate BIDSPath.

[2]:
bids_root = ""  # if using a local dataset, set this variable accordingly

file_path = BIDSPath(root=bids_root,
                    subject="01", task="podcast", datatype="ieeg",
                    suffix="ieeg", extension=".edf")
print(f"File path within the dataset: {file_path}")

# You only need to run this if using Colab (i.e. if you did not set bids_root to a local directory)
if not len(bids_root):
    !wget -nc https://s3.amazonaws.com/openneuro.org/ds005574/$file_path
    file_path = file_path.basename
File path within the dataset: ../../monkey/sub-01/ieeg/sub-01_task-podcast_ieeg.edf

Next, we’ll load the data with MNE read_raw_edf function:

[3]:
raw = mne.io.read_raw_edf(file_path, verbose=False)
raw
[3]:
General
Filename(s) sub-01_task-podcast_ieeg.edf
MNE object type RawEDF
Measurement date 2024-09-17 at 12:02:32 UTC
Participant <no
Experimenter Unknown
Acquisition
Duration 00:29:60 (HH:MM:SS)
Sampling frequency 512.00 Hz
Time points 921,600
Channels
EEG
Head & sensor digitization Not available
Filters
Highpass 0.00 Hz
Lowpass 256.00 Hz

Notice that the info above indicates there are 124 EEG channels. This is incorrect because there are different types of channels. When loading data with mne_bids.read_raw_bids or from a fif file, it will load correctly. In this case we will manually fix this by setting channel types depending on the channel names. This is important because we want to exclude non-ECoG data from analyses.

[4]:
def name2type(channel_name: str) -> str:
    if channel_name.startswith("DC"):
        return 'misc'
    elif channel_name == 'Pulse Rate':
        return 'misc'
    elif channel_name.startswith("EKG"):
        return 'ecg'
    else:
        return 'ecog'

channel_type_mapping = {name: name2type(name) for name in raw.ch_names}

raw = raw.set_channel_types(channel_type_mapping)
raw
/tmp/ipykernel_531495/3856841931.py:13: RuntimeWarning: The unit for channel(s) DC1, DC10, DC11, DC12, DC2, DC3, DC4, DC5, DC6, DC7, DC8, DC9 has changed from V to NA.
  raw = raw.set_channel_types(channel_type_mapping)
[4]:
General
Filename(s) sub-01_task-podcast_ieeg.edf
MNE object type RawEDF
Measurement date 2024-09-17 at 12:02:32 UTC
Participant <no
Experimenter Unknown
Acquisition
Duration 00:29:60 (HH:MM:SS)
Sampling frequency 512.00 Hz
Time points 921,600
Channels
ECG
misc
ECoG
Head & sensor digitization Not available
Filters
Highpass 0.00 Hz
Lowpass 256.00 Hz

Notice now that the channels information above displays different types.

Power spectrum

Before preprocessing, it is important to visually inspect channel data to mark electrodes with significant noise or other problems. Here, we will use a power spectrum density to visualize the frequency density per electrode. This is a convenient method of finding problematic electrodes.

[10]:
psd = raw.compute_psd(picks='ecog')
psd.plot()
Effective window size : 4.000 (s)
Plotting power spectral density (dB=True).
/tmp/ipykernel_531495/428485453.py:2: RuntimeWarning: Channel locations not available. Disabling spatial colors.
  psd.plot()
[10]:
../_images/notebooks_02-preprocessing_12_2.png

Notice the outlier channels that exhibit periodic oscillations and deviate from the 1/f pattern the majority of the channels follow. We identified these electrodes by name from an previous analyses. Here, we will mark them as bad electrodes using the Info object.

[6]:
raw.info['bads'] = ['DAMT1', 'DPMT4', 'DPMT6', 'DPMT7']
raw
[6]:
General
Filename(s) sub-01_task-podcast_ieeg.edf
MNE object type RawEDF
Measurement date 2024-09-17 at 12:02:32 UTC
Participant <no
Experimenter Unknown
Acquisition
Duration 00:29:60 (HH:MM:SS)
Sampling frequency 512.00 Hz
Time points 921,600
Channels
ECG
misc
ECoG and
Head & sensor digitization Not available
Filters
Highpass 0.00 Hz
Lowpass 256.00 Hz

Common average re-referencing

Re-referencing is a common procedure applied to EEG and ECoG signals in order to remove common noise present across electrodes. This process simply averages all good channels together into one time series, and then subtracts the average from each channel. MNE implements this process in set_eeg_reference.

[7]:
# Re-reference data
raw.load_data()  # required
rereferenced_raw = raw.set_eeg_reference(ref_channels="average", ch_type="ecog")
Reading 0 ... 921599  =      0.000 ...  1799.998 secs...
Applying average reference.
Applying a custom ('ECoG',) reference.

Notch filter powerline noise

Powerline noise is an artifact in electrophysiology that comes from the line frequency of power from a utility (60 Hz in US, 50 Hz in other parts of the world). You can see the noise in the power spectrum above as sharp peaks at 60 Hz and its harmonics. We can easily remove these frequencies using a Notch filter using MNE:

[8]:
# Notch filter power line noise
powerline_freq = raw.info.get("line_freq")
if powerline_freq is None:
    powerline_freq = 60
freqs = [powerline_freq * m for m in range(1, 4)]
notched_raw = rereferenced_raw.notch_filter(freqs=freqs, notch_widths=2)
Filtering raw data in 1 contiguous segment
Setting up band-stop filter

FIR filter parameters
---------------------
Designing a one-pass, zero-phase, non-causal bandstop filter:
- Windowed time-domain design (firwin) method
- Hamming window with 0.0194 passband ripple and 53 dB stopband attenuation
- Lower transition bandwidth: 0.50 Hz
- Upper transition bandwidth: 0.50 Hz
- Filter length: 3381 samples (6.604 s)

[Parallel(n_jobs=1)]: Done  17 tasks      | elapsed:    0.6s
[Parallel(n_jobs=1)]: Done  71 tasks      | elapsed:    2.6s

Frequency-band extraction

The signal so far is considered “cleaned” and ready for analyses. However, all other tutorials use the power modulations of the high gamma frequency band. This means we extract the signal with frequencies in the range 70–200 Hz, then apply a hilbert transform to extract power modulation as the envelope of the analytic signal. Luckily, MNE implements these steps using the filter and apply_hilbert functions:

[9]:
iir_params = dict(order=4, ftype="butter")
band_raw = notched_raw.filter(70, 200, picks="data", method="iir", iir_params=iir_params)
band_raw = band_raw.apply_hilbert(envelope=True)
band_raw
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 70 - 2e+02 Hz

IIR filter parameters
---------------------
Butterworth bandpass zero-phase (two-pass forward and reverse) non-causal filter:
- Filter order 16 (effective, after forward-backward)
- Cutoffs at 70.00, 200.00 Hz: -6.02, -6.02 dB

[9]:
General
Filename(s) sub-01_task-podcast_ieeg.edf
MNE object type RawEDF
Measurement date 2024-09-17 at 12:02:32 UTC
Participant <no
Experimenter Unknown
Acquisition
Duration 00:29:60 (HH:MM:SS)
Sampling frequency 512.00 Hz
Time points 921,600
Channels
ECG
misc
ECoG and
Head & sensor digitization Not available
Filters
Highpass 70.00 Hz
Lowpass 200.00 Hz