import numpy as np
import matplotlib.pyplot as plt
from scipy.io import loadmat
from scipy.fft import fft, ifft
from scipy.signal import firls, filtfilt
Chapter 2
Chapter 2
Analyzing Neural Time Series Data
Python code for Chapter 2 – 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
Type %pip install my_package
in a cell to install any missing packages
Figure 2.1
# Load the sample EEG data
= loadmat('../data/sampleEEGdata.mat')['EEG'][0, 0]
EEG
# Set the number of trials to analyze
= 6 # modifiable
nTrials
# Initialize data array
= np.zeros((nTrials, EEG['pnts'][0][0]))
data
# Define wavelet parameters
= np.arange(-1, 1 + 1/EEG['srate'][0][0], 1/EEG['srate'][0][0])
wavetime = len(wavetime) + EEG['pnts'][0][0] - 1
n_conv = fft(np.exp(2 * 1j * np.pi * 10 * wavetime) * np.exp(-wavetime**2 / (2 * (5 / (2 * np.pi * 10))**2)) / 10, n_conv)
waveletfft = np.zeros((nTrials, EEG['pnts'][0][0]))
data10hz
# Plotting setup
= plt.subplots(nTrials, 3, figsize=(15, 10))
fig, axs
# Loop over trials
for triali in range(nTrials):
# Create single trial "ERP"
= 0.5 * np.sin(2 * np.pi * 6 * EEG['times'][0] / 1000 + 2 * np.pi * triali / nTrials - np.pi) + np.random.randn(EEG['pnts'][0][0]) / 6
data[triali, :] # Add non-phase-locked stimulus potential
259:360] = data[triali, 259:360] + np.sin(2 * np.pi * 10 * EEG['times'][0][259:360] / 1000 + 2 * np.pi * triali / nTrials - np.pi) + np.random.randn(101) / 5
data[triali,
# Plot data from this trial
0].plot(EEG['times'][0], data[triali, :])
axs[triali, 0].set_xlim([-250, 850])
axs[triali, 0].set_ylim([-2.2, 2.2])
axs[triali,
# Plot ERP from trial 1 to current
1].plot(EEG['times'][0], np.mean(data[:triali+1, :], axis=0))
axs[triali, 1].set_xlim([-250, 850])
axs[triali, 1].set_ylim([-2.2, 2.2])
axs[triali,
# Convolve with 10 Hz wavelet
= ifft(waveletfft * fft(data[triali, :], n_conv)) * np.sqrt(5 / (2 * np.pi * 10))
convolution_result_fft = convolution_result_fft[int(np.floor(len(wavetime)/2)) : -int(np.floor(len(wavetime)/2))]
convolution_result_fft = np.abs(convolution_result_fft)**2
data10hz[triali, :]
# Plot 10 Hz power
2].plot(EEG['times'][0], np.mean(data10hz[:triali+1, :], axis=0))
axs[triali, 2].set_xlim([-250, 850])
axs[triali, 2].set_ylim([-0.1, 0.8])
axs[triali,
plt.tight_layout() plt.show()
Figure 2.2
This code involves performing convolution with a complex Morlet wavelet, which you will learn about in Chapters 10-13.
# Define parameters for the time-frequency analysis
= 1000
srate = np.arange(0, 10 + 1/srate, 1/srate)
time = -0.5
DCoffset
# Create multi-frequency signal
= np.sin(2 * np.pi * 10 * time) # High frequency part
a = 0.1 * np.sin(2 * np.pi * 0.3 * time) + DCoffset # Low frequency part
b
# Combine signals
= a * b
data += (2 * np.sin(2 * np.pi * 3 * time) * np.sin(2 * np.pi * 0.07 * time) * 0.1 + DCoffset)
data
# Morlet wavelet convolution parameters
= 40
num_frex = 2
min_freq = 20
max_freq
= len(data)
Ldata = len(data)
Ltapr = Ldata + Ltapr - 1
Lconv1 = 2**int(np.ceil(np.log2(Lconv1)))
Lconv
= np.logspace(np.log10(min_freq), np.log10(max_freq), num_frex)
frex
# Initialize time-frequency representation matrix
= np.zeros((num_frex, len(data)))
tf = fft(data, Lconv)
datspctra
= 4 / (2 * np.pi * frex)
s = np.arange(-((len(data) - 1) / 2) / srate, ((len(data) - 2) / 2) / srate + 1/srate, 1/srate)
t
# Perform wavelet convolution
for fi in range(len(frex)):
= np.exp(2 * 1j * np.pi * frex[fi] * t) * np.exp(-t**2 / (2 * s[fi]**2))
wavelet = ifft(datspctra * fft(wavelet, Lconv), Lconv)
m = m[:Lconv1]
m = m[int(np.floor((Ltapr - 1) / 2)) : -int(np.ceil((Ltapr - 1) / 2))]
m = np.abs(m)**2
tf[fi, :]
# Plot the signals and time-frequency representation
= plt.subplots(2, 2, figsize=(12, 8))
fig, axs
0, 0].plot(a)
axs[0, 0].set_xlim([1 * 1000, 8 * 1000])
axs[0, 0].set_ylim([-1, 1])
axs[0, 0].set_xticks(np.arange(1 * 1000, 9 * 1000, 1000))
axs[0, 0].set_xticklabels(np.arange(1, 9))
axs[0, 0].set_title('10 Hz signal, DC=0')
axs[
0, 1].plot(b)
axs[0, 1].set_xlim([1 * 1000, 8 * 1000])
axs[0, 1].set_ylim([-1, 1])
axs[0, 1].set_xticks(np.arange(1 * 1000, 9 * 1000, 1000))
axs[0, 1].set_xticklabels(np.arange(1, 9))
axs[0, 1].set_title(f'.3 Hz signal, DC={DCoffset}')
axs[
1, 0].plot(data)
axs[1, 0].set_xlim([1 * 1000, 8 * 1000])
axs[1, 0].set_ylim([-1, 1])
axs[1, 0].set_xticks(np.arange(1 * 1000, 9 * 1000, 1000))
axs[1, 0].set_xticklabels(np.arange(1, 9))
axs[1, 0].set_title('Time-domain signal')
axs[
= axs[1, 1].imshow(tf, aspect='auto', extent=[1, len(data), 0, num_frex], origin='lower')
im 1, 1].set_xlim([1 * 1000, 8 * 1000])
axs[1, 1].set_yticks(np.arange(1, num_frex, 8))
axs[1, 1].set_yticklabels(np.round(frex[::8]))
axs[1, 1].set_xticks(np.arange(1 * 1000, 9 * 1000, 1000))
axs[1, 1].set_xticklabels(np.arange(1, 9))
axs[1, 1].set_title('Time-frequency representation')
axs[
plt.tight_layout() plt.show()
Figure 2.3
# Select the channel to plot
= 'Pz' # you can pick any electrode (type `EEG['chanlocs'][0]['labels']` in a new cell for all electrodes)
chan2plot
# Compute ERP (time-domain trial average from selected electrode)
= np.squeeze(np.mean(EEG['data'][EEG['chanlocs'][0]['labels']==chan2plot, :, :], axis=2))
erp
# Low-pass filter parameters
= EEG['srate'][0][0] / 2
nyquist = 40 # Hz
filter_cutoff = 0.1 # Transition width, in fraction of 1
trans_width
# Filter design
= [0, filter_cutoff, filter_cutoff * (1 + trans_width), nyquist] / nyquist
ffrequencies = [1, 1, 0, 0]
idealresponse = firls(101, ffrequencies, idealresponse)
filterweights = filtfilt(filterweights, 1, erp)
filtered_erp
# Plotting the ERP
=(10, 5))
plt.figure(figsize'times'][0], filtered_erp, 'k.-', label='256 Hz')
plt.plot(EEG[
# Down-sample and plot
= np.arange(-200, 1001, 40)
times2plot = filtered_erp[::5]
downsampled_erp = EEG['times'][0][::5]
downsampled_times 'mo-', label='50 Hz')
plt.plot(downsampled_times, downsampled_erp,
-200, 1000])
plt.xlim([-4, 10])
plt.ylim([
plt.gca().invert_yaxis()'Time (ms)')
plt.xlabel('Voltage (µV)')
plt.ylabel('ERP from electrode {}'.format(chan2plot))
plt.title(
plt.legend()
plt.show()