Within-participant behaviour PLS#

Within-participant behaviour PLS can be used to analyze trial-level associations between brain and behavioural data (e.g., subjective ratings). In this example, we will simulate a dataset where the strength of a stimulus is associated with the magnitude of an ERP on a trial-by-trial basis.

Simulating data#

We will simulate an experiment in which a stimulus evokes a parietally focused ERP. First, we will set up the underlying ERP timecourse.

[1]:
import mne
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import mne_plsc

np.random.seed(123)

# Set up underlying ERP timecourse
sfreq = 100
times = np.arange(-0.5, 1, 1/sfreq)
erp_timecourse = np.exp(-5*times) * times * np.sin(10*times)
erp_timecourse[times < 0] = 0
f, ax = plt.subplots()
ax.plot(times, erp_timecourse)
ax.set_title('Underlying ERP timecourse')
[1]:
Text(0.5, 1.0, 'Underlying ERP timecourse')
../_images/notebooks_within-participants_2_1.png

Next, we will set up the weights that will determine how strongly the ERP will be expressed at each channel.

[2]:
# Set up montage and info
montage = mne.channels.make_standard_montage('biosemi16')
ch_pos_dict = montage.get_positions()["ch_pos"]
ch_pos_array = np.array([ch_pos_dict[ch] for ch in montage.ch_names])
info = mne.create_info(ch_names=montage.ch_names,
                       ch_types='eeg',
                       sfreq=sfreq)
info = info.set_montage(montage)

# Function to get channel weights
center = ch_pos_dict['Pz']
dist = np.linalg.norm(ch_pos_array - center, axis=1)
chan_weights = np.exp(-(dist ** 2) / (2 * 0.075 ** 2))

f, ax = plt.subplots()
mne.viz.plot_topomap(chan_weights, info, axes=ax, show=False)
ax.set_title('Channel weights')
plt.show()
../_images/notebooks_within-participants_4_0.png

Finally, we will simulate the actual epochs per participant.

[3]:
n_ptpts = 5
trials_per_ptpt = 20
all_epochs = []
for ptpt_n in range(n_ptpts):
    # Project ERP onto channels
    data = np.outer(chan_weights, erp_timecourse)
    data = np.stack([data]*trials_per_ptpt)
    # Simulate stimulus
    stim = np.random.gamma(shape=1, scale=1, size=(trials_per_ptpt,))
    # Scale ERP by stimulus
    data = data * stim.reshape((-1, 1, 1))
    # Add noise
    noise = 0.1*np.random.normal(size=data.shape)
    data = data + noise
    # Create epochs object
    epochs = mne.EpochsArray(data=data,
                             info=info,
                             metadata=pd.DataFrame({
                                 'stim': stim.squeeze(),
                                 'cond': [0, 1]*int(trials_per_ptpt/2)}),
                             verbose=False)
    all_epochs.append(epochs)

Fitting and evaluating the model#

Having set up the data, the syntax for fitting a within-participants behaviour PLS model is similar to fitting an ordinaly within-participants PLS model. The difference is that we don’t provide a design matrix—we just specify the name(s) of the columns in each participant’s metadata containing the covariate(s).

[4]:
res = mne_plsc.fit_within_beh(data=all_epochs,
                              covariates='stim')
res.plot_lv(0)
[4]:
(<Figure size 640x480 with 3 Axes>,
 array([<Axes: xlabel='Time (s)', ylabel='Salience'>,
        <Axes: ylabel='Correlation with brain score'>], dtype=object))
../_images/notebooks_within-participants_8_1.png

Unsurprisingly, the latent variable is significant:

[5]:
res.permute(100)
print(res.summary())
Getting permutations: 100%|██████████| 5/5 [00:00<00:00, 1295.98it/s]
Permuting: 100%|██████████| 100/100 [00:00<00:00, 659.25it/s]
   LV index  singular value  variance explained   p value
0         0        7.537496                 1.0  0.009901

Cluster analysis#

Cluster analysis produces parietal clusters, as expected given how the data was simulated:

[6]:
res.bootstrap(100)
res.add_adjacency(montage_name='biosemi16')
res.cluster()
res.plot_cluster(lv_idx=0, cluster_idx=0, highlight='extent')
res.plot_cluster(lv_idx=0, cluster_idx=1, highlight='extent')
Getting participant resamples: 100%|██████████| 100/100 [00:00<00:00, 27151.11it/s]
Getting trial resamples: 100%|██████████| 5/5 [00:00<00:00, 103.56it/s]
Resampling: 100%|██████████| 100/100 [00:00<00:00, 441.40it/s]
Defaulting to all channels adjacent for ERP/ERF analysis
Clustering z-scores
Defaulting to unsigned clustering
Computing clusters for lv_idx 0...
Threshold: 2
45 clusters
[6]:
(<Figure size 640x480 with 4 Axes>,
 array([<Axes: xlabel='Time (s)', ylabel='Bootstrap ratio (z score)'>,
        <Axes: >], dtype=object))
../_images/notebooks_within-participants_12_3.png
../_images/notebooks_within-participants_12_4.png