import numpy as np
import matplotlib.pyplot as plt
from scipy.io import loadmat
from scipy.interpolate import griddata
from scipy.signal import filtfilt, firls
from mne import create_info, EvokedArray, find_layout
from mne.channels import make_dig_montage
# packages for interactive plots
import plotly.graph_objects as go
import plotly.io as pio
= "plotly_mimetype+notebook_connected"
pio.renderers.default from IPython.display import display, HTML
= '<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js" integrity="sha512-c3Nl8+7g4LMSTdrm621y7kf9v3SDPnhxLNhcjFJbKECVnmZHTdo+IRO05sNLTH/D3vA6u1X32ehoLC7WFVdheg==" crossorigin="anonymous"></script>'
js display(HTML(js))
Chapter 9
Chapter 9
Analyzing Neural Time Series Data
Python code for Chapter 9 – converted from original Matlab by AE Studio (and ChatGPT)
Original Matlab code by Mike X Cohen
This code accompanies the book, titled “Analyzing Neural Time Series Data” (MIT Press).
Using the code without following the book may lead to confusion, incorrect data analyses, and misinterpretations of results.
Mike X Cohen and AE Studio assume no responsibility for inappropriate or incorrect use of this code.
Import necessary libraries
Note: The following code requires the MNE library for topographical plotting. If MNE is not installed, you can install it using pip:
%pip install mne
Figure 9.1a
# Load EEG data
= loadmat('../data/sampleEEGdata.mat')['EEG'][0, 0]
EEG
# Plot a few trials from one channel
= 'FCz'
which_channel_to_plot = EEG['chanlocs'][0]['labels']==which_channel_to_plot
channel_index = [-200, 1000] # in ms
x_axis_limit = 12
num_trials2plot
= plt.subplots(int(np.ceil(num_trials2plot / np.ceil(np.sqrt(num_trials2plot)))),
fig, axs int(np.ceil(np.sqrt(num_trials2plot))), figsize=(12, 8))
= axs.flatten()
axs
for i in range(num_trials2plot):
= np.random.choice(EEG['trials'][0][0], 1)
random_trial 'times'][0], EEG['data'][channel_index, :, random_trial].flatten())
axs[i].plot(EEG[
axs[i].set_xlim(x_axis_limit)f'Trial {random_trial[0]+1}')
axs[i].set_title(
axs[i].set_yticklabels([])
plt.tight_layout() plt.show()
Figure 9.1b
=(10, 5))
plt.figure(figsize# Plot all trials
'times'][0], np.squeeze(EEG['data'][channel_index, :, :]), 'y')
plt.plot(EEG[# plot ERP (simply the average time-domain signal)
'times'][0], np.squeeze(np.mean(EEG['data'][channel_index, :, :], axis=2)), 'k')
plt.plot(EEG[-300, 1000])
plt.xlim([-60, 60])
plt.ylim([
plt.gca().invert_yaxis()
plt.show()
# Plot ERP (Event-Related Potential)
=(10, 5))
plt.figure(figsize'times'][0], np.squeeze(np.mean(EEG['data'][channel_index, :, :], axis=2)), 'k', linewidth=2)
plt.plot(EEG[0, color='k')
plt.axhline(0, color='k', linestyle=':')
plt.axvline(-300, 1000])
plt.xlim([-3, 7])
plt.ylim(['Time (ms)')
plt.xlabel('Voltage (μV)')
plt.ylabel(f'ERP (average of 99 trials) from electrode {EEG["chanlocs"][0][channel_index][0][0][0]}')
plt.title(
plt.gca().invert_yaxis() plt.show()
Figure 9.2
# Plot filtered ERPs
= 'P7'
chan2plot
= np.squeeze(np.mean(EEG['data'][EEG['chanlocs'][0]['labels']==chan2plot, :, :], axis=2))
erp
# Define filter parameters
= EEG['srate'][0][0] / 2
nyquist = 0.15 # percent
transition_width
# Filter from 0-40 Hz
= 40
filter_cutoff = [0, filter_cutoff, filter_cutoff * (1 + transition_width), nyquist] / nyquist
ffrequencies = [1, 1, 0, 0]
idealresponse = firls(101, ffrequencies, idealresponse)
filterweights = filtfilt(filterweights, 1, erp)
erp_0to40
# Filter from 0-10 Hz
= 10
filter_cutoff = [0, filter_cutoff, filter_cutoff * (1 + transition_width), nyquist] / nyquist
ffrequencies = [1, 1, 0, 0]
idealresponse = firls(101, ffrequencies, idealresponse)
filterweights = filtfilt(filterweights, 1, erp)
erp_0to10
# Filter from 5-15 Hz
= 5
filter_low = 15
filter_high = [0, filter_low * (1 - transition_width), filter_low, filter_high, filter_high * (1 + transition_width), nyquist] / nyquist
ffrequencies = [0, 0, 1, 1, 0, 0]
idealresponse = firls(round(3 * (EEG['srate'][0][0] / filter_low)) + 1, ffrequencies, idealresponse)
filterweights = filtfilt(filterweights, 1, erp)
erp_5to15
# Plot all filtered ERPs
=(8, 6))
plt.figure(figsize'times'][0], erp, 'k', label='None')
plt.plot(EEG['times'][0], erp_0to40, 'b', linewidth=2, label='0-40 Hz')
plt.plot(EEG['times'][0], erp_0to10, 'r', label='0-10 Hz')
plt.plot(EEG['times'][0], erp_5to15, 'm', label='5-15 Hz')
plt.plot(EEG[-200, 1200])
plt.xlim(['Time (ms)')
plt.xlabel('Voltage (μV)')
plt.ylabel(f'ERP from electrode {chan2plot}')
plt.title(
plt.legend()
plt.gca().invert_yaxis() plt.show()
Figure 9.3
=(10, 8))
plt.figure(figsize
# Butterfly plot
2, 1, 1)
plt.subplot('times'][0], np.squeeze(np.mean(EEG['data'], axis=2)).T)
plt.plot(EEG[-200, 1000])
plt.xlim(['Time (ms)')
plt.xlabel('Voltage (μV)')
plt.ylabel('ERP from all sensors')
plt.title(
plt.gca().invert_yaxis()
# Topographical variance plot
2, 1, 2)
plt.subplot('times'][0], np.var(np.mean(EEG['data'], axis=2), axis=0))
plt.plot(EEG[-200, 1000])
plt.xlim(['Time (ms)')
plt.xlabel('Variance (μV²)')
plt.ylabel('Topographical variance')
plt.title(
plt.tight_layout() plt.show()
Figure 9.4
= -np.mean(EEG['data'][:, 299, :], axis=1)
c = c - np.min(c)
c = c / np.max(c)
c = np.tile(c, (3, 1))
c
# create channel montage
= [chan['labels'][0] for chan in EEG['chanlocs'][0]]
chan_labels = np.vstack([-1*EEG['chanlocs']['Y'], EEG['chanlocs']['X'], EEG['chanlocs']['Z']]).T
coords # note that we flip the y-coordinate here to match the Matlab figure
# remove channels outside of head
= np.where(np.array([chan['radius'][0][0] for chan in EEG['chanlocs'][0]]) > 0.54)
exclude_chans = np.delete(coords, exclude_chans, axis=0)
coords = np.delete(chan_labels, exclude_chans)
chan_labels = np.delete(c, exclude_chans, axis=1)
c = make_dig_montage(ch_pos=dict(zip(chan_labels, coords)), coord_frame='head')
montage
# create MNE Info and Evoked object
= create_info(list(chan_labels), EEG['srate'], ch_types='eeg')
info = EvokedArray(np.zeros((len(chan_labels), 1)), info, tmin=EEG['xmin'][0][0])
evoked1
evoked1.set_montage(montage)= EvokedArray(c[0, :, np.newaxis], info, tmin=EEG['xmin'][0][0])
evoked2
evoked2.set_montage(montage)
# Plot using MNE's topomap function
= plt.subplots(1, 2, figsize=(10, 5))
fig, ax =False, contours=0, sphere=EEG['chanlocs'][0][0]['sph_radius'][0][0], cmap='gray', axes=ax[1], show=False, times=-1, time_format='', colorbar=False)
evoked2.plot_topomap(sensors=False, contours=0, sphere=EEG['chanlocs'][0][0]['sph_radius'][0][0], cmap='gray', vlim=ax[1].get_images()[0].get_clim(), axes=ax[0], show=False, times=-1, time_format='', colorbar=False)
evoked1.plot_topomap(sensors
= (find_layout(evoked1.info).pos[:, 0] - np.mean(find_layout(evoked1.info).pos[:, 0])) * 2.3 * EEG['chanlocs'][0][0]['sph_radius'][0][0]
pos_x = (find_layout(evoked1.info).pos[:, 1] - np.mean(find_layout(evoked1.info).pos[:, 0])) * 2.3 * EEG['chanlocs'][0][0]['sph_radius'][0][0]
pos_y
# Plot the sensors using scatter
for i in range(len(pos_x)):
0].scatter(pos_x[i], pos_y[i], s=20, c=[c[0, i]], marker='o', cmap='gray', vmin=0, vmax=1, edgecolors='none')
ax[
plt.tight_layout() plt.show()
Introduction to topographical plotting
def pol2cart(theta, radius):
"""Convert polar coordinates to Cartesian."""
= radius * np.cos(theta)
x = radius * np.sin(theta)
y return x, y
= 100 # in ms
timepoint2plot = 69#np.random.choice(EEG['trials'][0][0], 1)[0]
trial2plot = 20 # more-or-less arbitrary, but this is a good value
color_limit
# Convert time point from ms to index
= np.argmin(np.abs(EEG['times'][0] - timepoint2plot))
timepointidx
# Get X and Y coordinates of electrodes
= np.pi / 180 * np.array([chan['theta'] for chan in EEG['chanlocs'][0]])
th = np.array([chan['radius'] for chan in EEG['chanlocs'][0]])
radii = pol2cart(th, radii)
electrode_locs_X, electrode_locs_Y
# Flatten the electrode location arrays
= electrode_locs_X.flatten()
electrode_locs_X = electrode_locs_Y.flatten()
electrode_locs_Y
# Interpolate to get a nice surface
= 100
interpolation_level = np.linspace(min(electrode_locs_X), max(electrode_locs_X), interpolation_level)
interpX = np.linspace(min(electrode_locs_Y), max(electrode_locs_Y), interpolation_level)
interpY
# meshgrid is a function that creates 2D grid locations based on 1D inputs
= np.meshgrid(interpX, interpY)
gridX, gridY
# Let's look at these matrices
plt.figure()121)
plt.subplot(='viridis')
plt.imshow(gridX, cmap
122)
plt.subplot(
plt.imshow(gridY)
plt.tight_layout()
plt.show()
# Interpolate the data on a 2D grid
= griddata((electrode_locs_X, electrode_locs_Y), EEG['data'][:, timepointidx, trial2plot], (gridX, gridY), method='cubic')
interpolated_EEG_data
# Plot the interpolated data
= plt.subplots(1, 2, figsize=(12, 6))
fig, axs = axs[0].contourf(interpY, interpX, interpolated_EEG_data.T, 100, cmap='jet', vmin=-color_limit, vmax=color_limit)
im 0].set_title('Interpolated data in Python')
axs[
# create channel montage
= [chan['labels'][0] for chan in EEG['chanlocs'][0]]
chan_labels = np.vstack([-1*EEG['chanlocs']['Y'], EEG['chanlocs']['X'], EEG['chanlocs']['Z']]).T
coords = make_dig_montage(ch_pos=dict(zip(chan_labels, coords)), coord_frame='head')
montage
# create MNE Info and Evoked object
= create_info(chan_labels, EEG['srate'], ch_types='eeg')
info = EvokedArray(EEG['data'][:, timepointidx, trial2plot, np.newaxis], info, tmin=EEG['xmin'][0][0])
evoked
evoked.set_montage(montage)
# Plot using MNE's topomap function
=EEG['chanlocs'][0][0]['sph_radius'][0][0], cmap='jet', vlim=(-color_limit*1000000, color_limit*1000000), axes=axs[1], show=False, times=-1, time_format='', colorbar=False)
evoked.plot_topomap(sphere1].set_title('Topomap using MNE')
axs[f'Topographical data from trial {trial2plot+1}, time={round(EEG["times"][0][timepointidx])}', fontsize=16)
fig.suptitle(
plt.show()
= go.Figure(data=[go.Surface(
fig =gridY,
x=gridX,
y=interpolated_EEG_data,
z='Jet',
colorscale=-color_limit,
cmin=color_limit,
cmax=False,
showscale
)])
fig.update_layout(=dict(
scene='left-right of scalp',
xaxis_title='anterior-posterior of scalp',
yaxis_title='μV',
zaxis_title=dict(range=[np.min(interpY) * 1.1, np.max(interpY) * 1.1]),
xaxis=dict(range=[np.min(interpX) * 1.1, np.max(interpX) * 1.1]),
yaxis=dict(
camera# Set the initial camera position to the same view as the MATLAB plot
=dict(x=0, y=0, z=2.5),
eye=dict(x=0, y=1, z=0),
up
)
),='A landscape of cortical electrophysiological dynamics<br>(click and spin with mouse)',
title=500,
width=500
height
) fig.show()
Figure 9.5
= [np.argmin(np.abs(EEG['times'][0] - t)) for t in np.arange(-100, 650, 50)]
times2plot
# create channel montage
= [chan['labels'][0] for chan in EEG['chanlocs'][0]]
chan_labels = np.vstack([-1*EEG['chanlocs']['Y'], EEG['chanlocs']['X'], EEG['chanlocs']['Z']]).T
coords = make_dig_montage(ch_pos=dict(zip(chan_labels, coords)), coord_frame='head')
montage
# create MNE Info and Evoked object
= create_info(chan_labels, EEG['srate'], ch_types='eeg')
info = EvokedArray(np.mean(EEG['data'], axis=2) , info, tmin=EEG['xmin'][0][0])
evoked
evoked.set_montage(montage)
# Plot the topomaps
= plt.subplots(3, 5, figsize=(15, 10))
fig, axs for i, time_point in enumerate(times2plot):
= axs[i // 5, i % 5] # Determine the subplot position
ax # Introduce a random noise to the FC4 channel at the specific time point
'chanlocs'][0]['labels']=='FC4', time_point] = np.random.randn() * 10
evoked.data[EEG[# Plot the topomap
=EEG['chanlocs'][0][0]['sph_radius'][0][0], cmap='jet', vlim=(-8000000, 8000000), axes=ax, show=False, times=evoked.times[time_point], colorbar=False)
evoked.plot_topomap(spheref'{round(evoked.times[time_point]*1000)} ms') # Convert seconds back to milliseconds for the title
ax.set_title(
plt.tight_layout() plt.show()
Figure 9.6
= True # Change to False to sort by EEG data
useRTs
# Get RTs from each trial
= np.array([epoch['eventlatency'][0][np.where(np.array(epoch['eventlatency'][0]) == 0)[0][0] + 1][0][0] for epoch in EEG['epoch'][0]])
rts
# Find the sorting indices for RTs
if useRTs:
= np.argsort(rts)
rts_idx else:
= np.argsort(EEG['data'][46, 333, :])
rts_idx
# Plot sorted data
=(12, 8))
plt.figure(figsize'data'][46, :, rts_idx], aspect='auto', extent=[min(EEG['times'][0]), max(EEG['times'][0]), 1, EEG['trials'][0][0]], vmin=-30, vmax=30, origin="lower", cmap='viridis')
plt.imshow(EEG[-200, 1200])
plt.xlim(['Time (ms)')
plt.xlabel('Trials (sorted)')
plt.ylabel(='Voltage (μV)')
plt.colorbar(label
# Also plot the RTs on each trial
if useRTs:
1, EEG['trials'][0][0] + 1), 'k', linewidth=3)
plt.plot(rts[rts_idx], np.arange( plt.show()