TRF for Alice EEG Dataset

Estimate a TRF, starting with a *-raw.fif EEG file and stimulus *.wav files.

The data used is one subject from the Alice dataset. This assumes that the Alice dataset has already been downloded (see the repository readme).

# sphinx_gallery_thumbnail_number = 6
import eelbrain
import eelbrain.datasets._alice
from matplotlib import pyplot
import mne


# Define the dataset root; this will use ~/Data/Alice, replace it with the
# proper path if you downloaded the dataset in a different location
DATA_ROOT = eelbrain.datasets._alice.get_alice_path()

# Define some paths that will be used throughout
STIMULUS_DIR = DATA_ROOT / 'stimuli'
EEG_DIR = DATA_ROOT / 'eeg'

# Load one subject's raw EEG file
SUBJECT = 'S18'
LOW_FREQUENCY = 0.5
HIGH_FREQUENCY = 20

Load EEG data

This section loads EEG data from one subject from the Alice dataset.

raw = mne.io.read_raw(EEG_DIR / SUBJECT / f'{SUBJECT}_alice-raw.fif', preload=True)

# Filter the raw data to the desired band
raw.filter(LOW_FREQUENCY, HIGH_FREQUENCY, n_jobs=1)

# Interpolate bad channels
# This is not structly necessary for a single subject.
# However, when processing multiple subjects, it will allow comparing results across all sensors.
raw.interpolate_bads()

# Load the events embedded in the raw file as eelbrain.Dataset, a type of object that represents a data-table
events = eelbrain.load.mne.events(raw)

# Display the events table:
events
# i_start trigger event
0 6495 1 1
1 35304 5 2
2 65752 6 3
3 97408 7 4
4 132429 8 5
5 165593 9 6
6 197508 10 7
7 228988 11 8
8 257678 12 9
9 286316 2 10
10 316973 3 11
11 345086 4 12


Plot the first 5 seconds of the first trial

t0 = events[0, 'i_start'] / raw.info['sfreq']
xlim = [t0, t0 + 5]
p = eelbrain.plot.TopoButterfly(raw, t=t0 + 1, xlim=xlim, vmax=1e-4, h=3, w=10, clip='circle')
alice trf

Create a predictor

# Load the sound file corresponding to trigger 1
wav = eelbrain.load.wav(STIMULUS_DIR / f'1.wav')

# Compute the acoustic envelope
envelope = wav.envelope()

# Filter the envelope with the same parameters as the EEG data
envelope = eelbrain.filter_data(envelope, LOW_FREQUENCY, HIGH_FREQUENCY, pad='reflect')
envelope = eelbrain.resample(envelope, 100)

# Visualize the first 5 seconds
p = eelbrain.plot.UTS([wav, envelope * 2], axh=2, w=10, columns=1, xlim=5)

# Add y=0 as reference
p.add_hline(0, zorder=0)
1.wav, 1.wav

Generate the acoustic envelope for all trials in this dataset

envelopes = []
for stimulus_id in events['event']:
    wav = eelbrain.load.wav(STIMULUS_DIR / f'{stimulus_id}.wav')
    envelope = wav.envelope()
    envelope = eelbrain.filter_data(envelope, LOW_FREQUENCY, HIGH_FREQUENCY, pad='reflect')
    envelope = eelbrain.resample(envelope, 100)
    envelopes.append(envelope)

# Add the envelopes to the events table
events['envelope'] = envelopes

# Add a second predictor corresponding to acoustic onsets
events['onsets'] = [envelope.diff('time').clip(0) for envelope in envelopes]
events
# i_start trigger event envelope onsets
0 6495 1 1 <NDVar '1.wa... <NDVar '1.wa...
1 35304 5 2 <NDVar '2.wa... <NDVar '2.wa...
2 65752 6 3 <NDVar '3.wa... <NDVar '3.wa...
3 97408 7 4 <NDVar '4.wa... <NDVar '4.wa...
4 132429 8 5 <NDVar '5.wa... <NDVar '5.wa...
5 165593 9 6 <NDVar '6.wa... <NDVar '6.wa...
6 197508 10 7 <NDVar '7.wa... <NDVar '7.wa...
7 228988 11 8 <NDVar '8.wa... <NDVar '8.wa...
8 257678 12 9 <NDVar '9.wa... <NDVar '9.wa...
9 286316 2 10 <NDVar '10.w... <NDVar '10.w...
10 316973 3 11 <NDVar '11.w... <NDVar '11.w...
11 345086 4 12 <NDVar '12.w... <NDVar '12.w...


Add EEG trial data

Add EEG data for each trial. We specifically need the EEG data corresponding to each stimulus. Given that each stimulus had a slightly different duration, we need to extract EEG segments that are trimmed differently for each trial.

# Extract the stimulus duration (in seconds) from the envelopes
events['duration'] = eelbrain.Var([envelope.time.tstop for envelope in events['envelope']])
events
# i_start trigger event envelope onsets duration
0 6495 1 1 <NDVar '1.wa... <NDVar '1.wa... 57.54
1 35304 5 2 <NDVar '2.wa... <NDVar '2.wa... 60.85
2 65752 6 3 <NDVar '3.wa... <NDVar '3.wa... 63.26
3 97408 7 4 <NDVar '4.wa... <NDVar '4.wa... 69.99
4 132429 8 5 <NDVar '5.wa... <NDVar '5.wa... 66.27
5 165593 9 6 <NDVar '6.wa... <NDVar '6.wa... 63.78
6 197508 10 7 <NDVar '7.wa... <NDVar '7.wa... 62.9
7 228988 11 8 <NDVar '8.wa... <NDVar '8.wa... 57.31
8 257678 12 9 <NDVar '9.wa... <NDVar '9.wa... 57.23
9 286316 2 10 <NDVar '10.w... <NDVar '10.w... 61.27
10 316973 3 11 <NDVar '11.w... <NDVar '11.w... 56.17
11 345086 4 12 <NDVar '12.w... <NDVar '12.w... 46.98


Extract EEG data corresponding exactly to the timing of the envelopes

events['eeg'] = eelbrain.load.mne.variable_length_epochs(events, 0, tstop='duration', decim=5, adjacency='auto')
events
# i_start trigger event envelope onsets duration eeg
0 6495 1 1 <NDVar '1.wa... <NDVar '1.wa... 57.54 <NDVar: 61 s...
1 35304 5 2 <NDVar '2.wa... <NDVar '2.wa... 60.85 <NDVar: 61 s...
2 65752 6 3 <NDVar '3.wa... <NDVar '3.wa... 63.26 <NDVar: 61 s...
3 97408 7 4 <NDVar '4.wa... <NDVar '4.wa... 69.99 <NDVar: 61 s...
4 132429 8 5 <NDVar '5.wa... <NDVar '5.wa... 66.27 <NDVar: 61 s...
5 165593 9 6 <NDVar '6.wa... <NDVar '6.wa... 63.78 <NDVar: 61 s...
6 197508 10 7 <NDVar '7.wa... <NDVar '7.wa... 62.9 <NDVar: 61 s...
7 228988 11 8 <NDVar '8.wa... <NDVar '8.wa... 57.31 <NDVar: 61 s...
8 257678 12 9 <NDVar '9.wa... <NDVar '9.wa... 57.23 <NDVar: 61 s...
9 286316 2 10 <NDVar '10.w... <NDVar '10.w... 61.27 <NDVar: 61 s...
10 316973 3 11 <NDVar '11.w... <NDVar '11.w... 56.17 <NDVar: 61 s...
11 345086 4 12 <NDVar '12.w... <NDVar '12.w... 46.98 <NDVar: 61 s...


Plot the first 5 seconds of EEG of the first trial (compare above)

p = eelbrain.plot.TopoButterfly(events[0, 'eeg'], t=1.5, xlim=5, vmax=1e-4, h=3, w=10, clip='circle')
alice trf

Plot EEG alongside the representations of the sound that was presented

fig, axes = pyplot.subplots(2, 1, sharex=True, figsize=(10, 4))
p = eelbrain.plot.UTS([[events[0, 'envelope'], events[0, 'onsets']]], xlim=5, axes=axes[0])
p = eelbrain.plot.Butterfly(events[0, 'eeg'], xlim=5, vmax=1e-4, axes=axes[1])
alice trf

TRF

Estimate the brain’s response function to acoustic onsets.

trf = eelbrain.boosting('eeg', 'onsets', -0.100, 0.500, data=events, basis=0.050, partitions=4)

Plot the TRF, highlighting the topography at the global field power maximum

t = trf.h.std('sensor').argmax('time')
p = eelbrain.plot.TopoButterfly(trf.h, t=t, w=10, h=4, clip='circle')
alice trf

Alternative visualization as array image

p = eelbrain.plot.TopoArray(trf.h, t=[0.050, 0.120, 0.150], w=6, h=4, clip='circle')
alice trf

Predictive power

In order to derive an unbiased estimate of predictive power, we can use cross-validation. That means part of the data is never used while estimating the TRF, and can be used in the end to calculate how well the TRF can predict neural data. The boosting() function uses K-fold cross-validation. Cross-validation is enabled with the test=True parameter, and K is set through the partitions parameter.

trf_cv = eelbrain.boosting('eeg', 'onsets', 0, 0.500, data=events, basis=0.050, partitions=4, test=True)

Plot the predictive power across sensors, including the average across all sensors in each figure title.

title = f"Mean exp: {trf_cv.proportion_explained.mean('sensor'):.2%}"
p = eelbrain.plot.Topomap(trf_cv.proportion_explained, clip='circle', title=title)
pcb = p.plot_colorbar('Proportion explained')

title = f"Mean r: {trf_cv.r.mean('sensor'):.2}"
p = eelbrain.plot.Topomap(trf_cv.r, clip='circle', title=title)
pcb = p.plot_colorbar()
  • Mean exp: 0.25%
  • alice trf
  • Mean r: 0.046
  • alice trf

Decoding

Train an envelope decoder on the first 11 trials and use it to decode the envelope of the last trial.

# Use a larger delta to speed up training
decoder = eelbrain.boosting('envelope', 'eeg', -0.500, 0, data=events[:11], partitions=5, delta=0.05)

Now use the decoder to reconstruct the envelope of the last trial. Note that, when using the convolve() function, the time alignment is handled automatically because the kernel, decoder.h, includes a time axis (decoder.h.time) with relative delays between input and output.

# Normalize the EEG
eeg_11 = events[11, 'eeg'] / decoder.x_scale

# Predict the envelope by convolving the decoder with the EEG
y_pred = eelbrain.convolve(decoder.h, eeg_11, name='predicted envelope')

Extract the actual envelope and adjust the scale for visualization

y = events[11, 'envelope']
y = y - decoder.y_mean
y /= decoder.y_scale / y_pred.std()
y.name = 'envelope'

r = eelbrain.correlation_coefficient(y, y_pred)
p = eelbrain.plot.UTS([[y_pred, y]], w=10, h=3, xlim=10, title=f"{r=}")
r=0.2309358350597969

Visualize the decoder weights

p = eelbrain.plot.TopoArray(decoder.h, w=6, h=4, clip='circle', t=[-0.160, -0.130, -0.100])
alice trf

Total running time of the script: (0 minutes 42.465 seconds)

Gallery generated by Sphinx-Gallery