import numpy as np
import matplotlib.pyplot as plt
from scipy.io import loadmat
from scipy.signal import detrend
from scipy.stats import zscore, chi2
from armorf import armorf
Chapter 28
Chapter 28
Analyzing Neural Time Series Data
Python code for Chapter 28 – 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
Figure 28.1
=(10, 8))
plt.figure(figsize
# - Stationary-like process - #
= [np.random.randn()]
x for i in range(29):
* x[i])) + np.random.randn())
x.append(np.exp(np.cos(np.pi
221)
plt.subplot('mo-', linewidth=1, markerfacecolor='k', markersize=6)
plt.plot(x, 0, 31])
plt.xlim(['x_t = e^cos(pi*x_t-1) + randn'])
plt.legend(['Stationary autoregressive process')
plt.title(
= [np.random.randn(), np.random.randn()]
x for i in range(28):
0.2 * x[i+1] - 0.4 * x[i] + np.random.randn())
x.append(
222)
plt.subplot('mo-', linewidth=1, markerfacecolor='k', markersize=6)
plt.plot(x, 0, 31])
plt.xlim(['x_t = .2x_t-1 - .4x_t-2 + randn'])
plt.legend(['Stationary autoregressive process')
plt.title(
# - Non-stationary process - #
= [1]
x for i in range(29):
1.1 * x[i] + np.random.randn())
x.append(
223)
plt.subplot('kp-', linewidth=1, markerfacecolor='g', markersize=10)
plt.plot(x, 0, 31])
plt.xlim(['Non-stationary autoregressive process')
plt.title('x_t = 1.1*x_t-1 + randn'])
plt.legend([
= [1, 1.5]
x for i in range(28):
1.2 * x[i] - 0.3 * x[i+1] + np.random.randn())
x.append(
224)
plt.subplot('kp-', linewidth=1, markerfacecolor='g', markersize=10)
plt.plot(x, 0, 31])
plt.xlim(['x_t = -0.3*x_t-1 + 1.2*x_t-2 + randn'])
plt.legend(['Non-stationary autoregressive process')
plt.title(
plt.tight_layout() plt.show()
Figure 28.2
=(10, 8))
plt.figure(figsize
# - Stationary-like process - #
# define X
= [np.random.randn(), np.random.randn()]
x for i in range(28):
0.2 * x[i+1] - 0.4 * x[i] + np.random.randn())
x.append(
# define y
= [np.random.rand(), np.random.rand()] # random initial conditions
y for i in range(len(x)-2):
0.25 * y[i+1] - 0.8 * x[i] + 1.5 * x[i+1] + np.random.randn())
y.append(
221)
plt.subplot('mo-', linewidth=1, markerfacecolor='k', markersize=6)
plt.plot(x, 'go-', linewidth=1, markerfacecolor='k', markersize=6)
plt.plot(y, 0, 31])
plt.xlim(['Stationary bivariate autoregression')
plt.title('x_t = .2x_t-1 - .4x_t-2 + randn', 'y=0.25*y_t-1 - 0.8*x_t-2 + 1.5*x_t-1 + randn'])
plt.legend([
# - Non-stationary process - #
# define X
= [1, 1.5]
x for i in range(28):
1.2 * x[i] - 0.3 * x[i+1] + np.random.randn())
x.append(
# define y
= [np.random.rand(), np.random.rand()] # random initial conditions
y for i in range(len(x)-2):
0.25 * y[i+1] - 1.2 * x[i] + np.random.randn())
y.append(
222)
plt.subplot('mo-', linewidth=1, markerfacecolor='k', markersize=6)
plt.plot(x, 'go-', linewidth=1, markerfacecolor='k', markersize=6)
plt.plot(y, 0, 31])
plt.xlim(['Non-stationary bivariate autoregression')
plt.title('x_t = -.3x_t-1 - 1.2x_t-2 + randn', 'y=0.25*y_t-1 - 0.8*x_t-2 + 1.5*x_t-1 + randn'])
plt.legend([
plt.tight_layout() plt.show()
Figure 28.3
Note: this cell takes a while to run
# Load sample EEG data
= loadmat('../data/sampleEEGdata.mat')['EEG'][0, 0]
EEG
# Define channels for Granger prediction
= 'O1'
chan1name = 'F5'
chan2name
# Granger prediction parameters
= 200 # in ms
timewin = 27 # in ms
order
# Temporal down-sample results (but not data!)
= np.arange(-400, 1001, 20) # in ms
times2save
# Convert parameters to indices
= round(timewin / (1000 / EEG['srate'][0, 0]))
timewin_points = round(order / (1000 / EEG['srate'][0, 0]))
order_points
# Find the index of those channels
= EEG['chanlocs'][0]['labels'] == chan1name
chan1 = EEG['chanlocs'][0]['labels'] == chan2name
chan2
# Remove ERP from selected electrodes to improve stationarity
= EEG['data'][[np.where(chan1)[0][0], np.where(chan2)[0][0]], :, :] - np.mean(EEG['data'][[np.where(chan1)[0][0], np.where(chan2)[0][0]], :, :], axis=2)[..., np.newaxis]
eegdata
# Convert requested times to indices
= np.array([np.argmin(np.abs(EEG['times'][0] - t)) for t in times2save])
times2saveidx
# Initialize
= np.zeros(len(times2save))
x2y = np.zeros(len(times2save))
y2x = np.zeros((len(times2save), 15)) # Bayes info criteria (hard-coded to order=15)
bic
# Loop over time points
for timei in range(len(times2save)):
# Data from all trials in this time window
= eegdata[:, times2saveidx[timei] - timewin_points//2:times2saveidx[timei] + timewin_points//2 - ((timewin_points+1)%2) + 1, :].copy()
tempdata
# Detrend and zscore all data
for triali in range(EEG['trials'][0, 0]):
0, :, triali] = zscore(detrend(tempdata[0, :, triali], axis=0), ddof=1, axis=0)
tempdata[1, :, triali] = zscore(detrend(tempdata[1, :, triali], axis=0), ddof=1, axis=0)
tempdata[
# Reshape tempdata for VAR
= np.reshape(tempdata, (2, timewin_points * EEG['trials'][0][0]), 'F')
tempdata
# Fit AR models
= armorf(tempdata[0, :][np.newaxis, :], EEG['trials'][0][0], timewin_points, order_points)
Ax, Ex, _ = armorf(tempdata[1, :][np.newaxis, :], EEG['trials'][0][0], timewin_points, order_points)
Ay, Ey, _ = armorf(tempdata, EEG['trials'][0][0], timewin_points, order_points)
Axy, E, _
# Compute Granger causality values as log ratio of error variances
= np.log(Ex / E[0, 0])
y2x[timei] = np.log(Ey / E[1, 1])
x2y[timei]
# Compute BIC for optimal model order at each time point
# This code is used for the following cell
for bici in range(bic.shape[1]):
# Run model
= armorf(tempdata, EEG['trials'][0][0], timewin_points, bici+1)
Axy, E, _ # Computer Bayes Information Criteria
= np.log(np.linalg.det(E)) + (np.log(len(tempdata[0])) * (bici+1) * 2 ** 2) / len(tempdata[0])
bic[timei, bici]
# Plot Granger causality over time
plt.figure()=f'GP: {chan1name} -> {chan2name}')
plt.plot(times2save, x2y, label'r', label=f'GP: {chan2name} -> {chan1name}')
plt.plot(times2save, y2x,
plt.legend()f'Window length: {timewin} ms, order: {order} ms')
plt.title(-400, 1000])
plt.xlim(['Time (ms)')
plt.xlabel('Granger prediction estimate')
plt.ylabel( plt.show()
Figure 28.4
# Plot BIC
=(10, 4))
plt.figure(figsize
121)
plt.subplot(= np.mean(bic, axis=0)
mean_bic 1, bic.shape[1] + 1)) * (1000 / EEG['srate'][0, 0]), mean_bic, '--.')
plt.plot((np.arange('Order (converted to ms)')
plt.xlabel('Mean BIC over all time points')
plt.ylabel(
= np.argmin(mean_bic) + 1
bestbic_idx * (1000 / EEG['srate'][0, 0]), mean_bic[bestbic_idx-1], 'mo', markersize=15)
plt.plot(bestbic_idx f'Optimal order is {bestbic_idx} ({bestbic_idx * (1000 / EEG["srate"][0, 0])} ms)')
plt.title(
122)
plt.subplot(= np.argmin(bic, axis=1) + 1
bic_per_timepoint * (1000 / EEG['srate'][0, 0]), '--.')
plt.plot(times2save, bic_per_timepoint 'Time (ms)')
plt.xlabel('Optimal order (converted to ms)')
plt.ylabel('Optimal order (in ms) at each time point')
plt.title(
plt.tight_layout() plt.show()
Figure 28.5
# Define frequency range
= 10 # in Hz, using a minimum of 10 Hz because of 200-ms window
min_freq = 40
max_freq
= 15
order_points
= np.logspace(np.log10(min_freq), np.log10(max_freq), 15)
frequencies
# Initialize
= np.zeros((2, len(frequencies), len(times2save)))
tf_granger
# Loop over time points
for timei in range(len(times2save)):
# Data from all trials in this time window
= eegdata[:, times2saveidx[timei] - timewin_points//2:times2saveidx[timei] + timewin_points//2 - ((timewin_points+1)%2) + 1, :].copy()
tempdata
# Detrend and zscore all data
for triali in range(EEG['trials'][0, 0]):
0, :, triali] = zscore(detrend(tempdata[0, :, triali], axis=0), ddof=1, axis=0)
tempdata[1, :, triali] = zscore(detrend(tempdata[1, :, triali], axis=0), ddof=1, axis=0)
tempdata[
# Reshape tempdata for VAR
= np.reshape(tempdata, (2, timewin_points * EEG['trials'][0][0]), 'F')
tempdata
# Fit AR models
= armorf(tempdata[0, :][np.newaxis, :], EEG['trials'][0][0], timewin_points, order_points)
Ax, Ex, _ = armorf(tempdata[1, :][np.newaxis, :], EEG['trials'][0][0], timewin_points, order_points)
Ay, Ey, _ = armorf(tempdata, EEG['trials'][0][0], timewin_points, order_points)
Axy, E, _
# corrected covariance
= E[1, 1] - E[0, 1]**2/E[0, 0]
eyx = E[0, 0] - E[1, 0]**2/E[1, 1]
exy = len(E)
N
# Compute Granger causality for each frequency
for fi, freq in enumerate(frequencies):
# Compute the transfer function matrix (Fourier transform of the VAR coefficients)
= np.eye(N, dtype=complex)
H for m in range(order_points):
+= Axy[:, m*N:(m+1)*N] * np.exp(-1j * (m+1) * 2 * np.pi * freq / EEG['srate'][0, 0])
H
# Compute the spectral density matrix
= np.linalg.inv(H)
Hi = np.linalg.solve(H, E) @ Hi.T / EEG['srate'][0, 0]
S
# Compute Granger causality
0, fi, timei] = np.log(np.abs(S[1, 1]) / np.abs(S[1, 1] - (Hi[1, 0] * exy * Hi[1, 0].conj()) / EEG['srate'][0, 0]))
tf_granger[1, fi, timei] = np.log(np.abs(S[0, 0]) / np.abs(S[0, 0] - (Hi[0, 1] * eyx * Hi[0, 1].conj()) / EEG['srate'][0, 0]))
tf_granger[
# Plot time-frequency Granger causality
=(12, 6))
plt.figure(figsize211)
plt.subplot(0, :, :], 40, cmap='viridis', vmin=0, vmax=0.025)
plt.contourf(times2save, frequencies, tf_granger[
plt.colorbar()f'{chan1name} -> {chan2name}')
plt.title(
212)
plt.subplot(1, :, :], 40, cmap='viridis', vmin=0, vmax=0.025)
plt.contourf(times2save, frequencies, tf_granger[
plt.colorbar()f'{chan2name} -> {chan1name}')
plt.title(
plt.tight_layout() plt.show()
Figure 28.6
# Plot of cycles per frequency
=(10, 10))
plt.figure(figsizefor fi, freq in enumerate(frequencies):
4, 4, fi+1)
plt.subplot(= np.real(np.exp(-1j * np.arange(1, order_points+1) * 2 * np.pi * freq / EEG['srate'][0, 0]))
cycles 1, order_points+1)) * (1000 / EEG['srate'][0, 0]), cycles)
plt.plot((np.arange(0.5, 1.015 * order_points * (1000 / EEG['srate'][0, 0])])
plt.xlim([-1.1, 1.1])
plt.ylim([f'{round(freq)} Hz')
plt.title('Time (ms)')
plt.xlabel(
plt.tight_layout() plt.show()
Figure 28.7
plt.figure()
# Select electrode to plot
= EEG['chanlocs'][0]['labels']=='O2'
electrode2plot = np.squeeze(np.mean(EEG['data'][electrode2plot, :, :], axis=2))
erp 211)
plt.subplot('times'][0], np.squeeze(EEG['data'][electrode2plot, :, 0]),label='single trial')
plt.plot(EEG['times'][0], np.squeeze(EEG['data'][electrode2plot, :, 0]) - erp, 'r', label='single trial - ERP')
plt.plot(EEG['times'][0], erp, 'k', label='ERP')
plt.plot(EEG[-200, 1200])
plt.xlim([
plt.legend()f'Data from electrode {EEG["chanlocs"][0]["labels"][electrode2plot]}')
plt.title(
plt.gca().invert_yaxis()
# Select another electrode to plot
= EEG['chanlocs'][0]['labels']=='AFz'
electrode2plot = np.squeeze(np.mean(EEG['data'][electrode2plot, :, :], axis=2))
erp 212)
plt.subplot('times'][0], np.squeeze(EEG['data'][electrode2plot, :, 0]), label='single trial')
plt.plot(EEG['times'][0], np.squeeze(EEG['data'][electrode2plot, :, 0]) - erp, 'r', label='single trial - ERP')
plt.plot(EEG['times'][0], erp, 'k', label='ERP')
plt.plot(EEG[-200, 1200])
plt.xlim([
plt.legend()f'Data from electrode {EEG["chanlocs"][0]["labels"][electrode2plot]}')
plt.title(
plt.gca().invert_yaxis()
plt.tight_layout() plt.show()
Figure 28.8
# Baseline time window
= [-400, -100]
baseline_period
# Convert to indices
= [np.argmin(np.abs(times2save - bp)) for bp in baseline_period]
baseidx
# Plot as % changes from baseline
plt.figure()100 * (x2y - np.mean(x2y[baseidx[0]:baseidx[1]+1])) / np.mean(x2y[baseidx[0]:baseidx[1]+1]), label=f'GC: {chan1name} -> {chan2name}')
plt.plot(times2save, 100 * (y2x - np.mean(y2x[baseidx[0]:baseidx[1]+1])) / np.mean(y2x[baseidx[0]:baseidx[1]+1]), 'r', label=f'GC: {chan2name} -> {chan1name}')
plt.plot(times2save,
plt.legend()-400, 1000])
plt.xlim(['Time (ms)')
plt.xlabel('Granger prediction estimate (% change from baseline)')
plt.ylabel(
plt.show()
# Convert time-frequency domain to percent change
= tf_granger.copy()
tf_grangerPC for i in range(2):
= np.mean(tf_grangerPC[i, :, baseidx[0]:baseidx[1]+1], axis=1)
meangranger = 100 * (tf_grangerPC[i, :, :] - meangranger[:, None]) / meangranger[:, None]
tf_grangerPC[i, :, :]
# Plot time-frequency Granger causality percent change
=(12, 6))
plt.figure(figsizefor i in range(2):
2, 1, i+1)
plt.subplot(40, cmap='viridis', vmin=-150, vmax=150)
plt.contourf(times2save, frequencies, tf_grangerPC[i, :, :],
plt.colorbar()if i == 0:
f'{chan1name} -> {chan2name}; percent change')
plt.title(else:
f'{chan2name} -> {chan1name}; percent change')
plt.title('Frequency (Hz)')
plt.ylabel('Time (ms)')
plt.xlabel(
plt.tight_layout() plt.show()
Figure 28.9
plt.figure()
# Generate two chi-square distributed random numbers
= chi2.rvs(2, size=1000)
d1 = chi2.rvs(2, size=1000)
d2
# Get histograms
= np.histogram(d1, bins=50)
y1, x1 = np.histogram(d2, bins=50)
y2, x2 = np.histogram(d1 - d2, bins=50)
y3, x3
221)
plt.subplot(-1], y1, 'k')
plt.plot(x1[:-1], y2, 'r')
plt.plot(x2[:0, 20])
plt.xlim([0, max(y1.max(), y2.max())])
plt.ylim([
223)
plt.subplot(-1], y3)
plt.plot(x3[:-10, 10])
plt.xlim([0, 140])
plt.ylim([
# Once more, with new distributions
= chi2.rvs(7, size=1000)
d1 = chi2.rvs(7, size=1000)
d2
# Get histograms
= np.histogram(d1, bins=50)
y1, x1 = np.histogram(d2, bins=50)
y2, x2 = np.histogram(d1 - d2, bins=50)
y3, x3
222)
plt.subplot(-1], y1, 'k')
plt.plot(x1[:-1], y2, 'r')
plt.plot(x2[:0, 30])
plt.xlim([0, 80])
plt.ylim([
224)
plt.subplot(-1], y3)
plt.plot(x3[:-20, 20])
plt.xlim([0, 80])
plt.ylim([
plt.tight_layout() plt.show()