PRP basics¶
Loading in PRPs¶
Once you have the Python environment set up and EEG data processed, you're ready to load in and take a look at the PRPs. Open an analysis script/notebook (should be in the same directory as README.md and the "neuraspeech" folder) and activate the Python environment you installed. In VS Code, for example, choose the environment for the kernel in the top right. Then, import neuraspeech and run PRPData.
Here is an example from a small dataset with 2 subjects. Each subject completed the EEG continuous speech listening experiment in 2 sessions, in 2 presentation modes (monaural, binaural), and at 2 intensity levels (60 dB, 75 dB). This gives 2 (subjects) x 2 (sessions) x 2 (presentation modes) x 2 (intensity levels) = 16 data files.
import neuraspeech as ns
path_to_data = 'examples/dataset'
path_to_montage_file = 'examples/dataset/montage.csv'
prps = ns.PRPData(
data_path = path_to_data,
sr = 128, # Sampling rate of processed EEG
time_win = (0.0, 0.5), # Time window of the PRP relative to phoneme onset
eeg_fieldname = 'eegs', # Fildename of EEG data in the .mat files
phoneme_fieldname = 'phones', # Fildename of phoneme lables in the .mat files
montage_path = path_to_montage_file
)
100%|██████████| 16/16 [00:06<00:00, 2.48it/s]
Done.
By default, PRPData assumes that the data are named following the BIDS convention. That is, information such as subject ID, condition code, etc. are stored as key-value pairs separated by undercores "_" and "-", respectively. Again, for example, data from subject 01 completing the Alice listening task in noise condition may be saved as "sub-01_task-alice_cond-noise.mat". If you use a different file naming convention, you can supply a custom file naming parsing function (see below).
Also, it is assumed that the EEG sampling rate is 128 Hz with time window from 0.0 to 0.5 s and the fieldnames of EEG and phoneme labels in the .mat files are 'eegs' and 'phones'. So unless any of these is different for your data, you can leave these arguments out and don't have to specify them.
Print out data summary:
prps.data_summary()
== PRP dataset summary == Key Type Values ---------------------------------------------------------------------------------------------------- filename Factor sub-01_ses-1_task-60bi_acq-2.mat:37... (16 cells) sub Factor 01:296, 02:296 ses Factor 1:296, 2:296 task Factor 60bi:148, 60mo:148, 75bi:148, 75mo:148 acq Factor 2:148, 1:148, 3:148, 4:148 phoneme Factor AA:16, AE:16, AH:16, AO:16, AW:16, AY:16, B:16, CH:16, D:16, DH:16... (37 cells) manner Factor Vowel:224, Stop:128, Fricative:128, Nasal-approximant:112 PRP NDVar 64 time, 32 sensor; -0.000121755 - 3.32726e-05 ---------------------------------------------------------------------------------------------------- Dataset: 592 cases
In addition to the metadata, it also tells you how many PRPs there are for each phoneme and manner-of-articulation category.
The dataset is stored as a Dataset object from the eelbrain package. It can be accessed using the .get_data() method. All PRPs are stored (as a NDVar object) with the variable name 'PRP' in the dataset.
prp_data = prps.get_data()
print(prp_data.head())
# filename sub ses task acq phoneme manner ----------------------------------------------------------------------------------- 0 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 AA Vowel 1 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 AE Vowel 2 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 AH Vowel 3 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 AO Vowel 4 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 AW Vowel 5 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 AY Vowel 6 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 B Stop 7 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 CH Stop 8 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 D Stop 9 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 DH Fricative ----------------------------------------------------------------------------------- NDVars: PRP
print(prp_data['PRP'])
<NDVar 'PRP': 592 case, 64 time, 32 sensor>
Here, it's telling you that there are totally 592 PRPs, each being an array with 64 time samples and 32 sensors/electrodes.
Subsetting¶
Sometimes you may want to drop some PRP data before the analysis. The toolbox has functions that allow you to do this:
# Subset to remove unwatned subjects and phonemes
filter = {
'sub': ['02'],
'phoneme': ['AH', 'TH', 'Y', 'UH', 'Q', 'CH', 'JH']
}
prps.subset_dataset(filter, exclude = True)
prps.data_summary()
== PRP dataset summary == Key Type Values ------------------------------------------------------------------------------------------------ filename Factor sub-01_ses-1_task-60bi_acq-2.mat:31... (8 cells) sub Factor 01:248 ses Factor 1:124, 2:124 task Factor 60bi:62, 60mo:62, 75bi:62, 75mo:62 acq Factor 2:62, 1:62, 3:62, 4:62 phoneme Factor AA:8, AE:8, AO:8, AW:8, AY:8, B:8, D:8, DH:8, EH:8, ER:8, EY:8... (31 cells) manner Factor Vowel:96, Stop:48, Fricative:56, Nasal-approximant:48 PRP NDVar 64 time, 32 sensor; -9.18619e-06 - 1.03316e-05 ------------------------------------------------------------------------------------------------ Dataset: 248 cases
By setting exclude = True in the subset_dataset() method, you tell the PRP dataset to "exclude" everything in the filter, so now there are no PRPs from subject 02 and those phonemes. Alternatively, you can do the other way around to keep only items in the filter by using exclude = False:
# This further reduces the PRP dataset to just PRPs in the vowel and stop categories
filter = {
'manner': ['Vowel', 'Stop']
}
prps.subset_dataset(filter, exclude = False)
prps.data_summary()
== PRP dataset summary == Key Type Values -------------------------------------------------------------------------------------------------- filename Factor sub-01_ses-1_task-60bi_acq-2.mat:18... (8 cells) sub Factor 01:144 ses Factor 1:72, 2:72 task Factor 60bi:36, 60mo:36, 75bi:36, 75mo:36 acq Factor 2:36, 1:36, 3:36, 4:36 phoneme Factor AA:8, AE:8, AO:8, AW:8, AY:8, EH:8, ER:8, EY:8, IH:8, IY:8, OW:8... (18 cells) manner Factor Vowel:96, Stop:48 PRP NDVar 64 time, 32 sensor; -9.18619e-06 - 9.62185e-06 -------------------------------------------------------------------------------------------------- Dataset: 144 cases
Combining datasets¶
You can use the combine() function to combine different datasets (this should work for both PRPData and FStatistic):
# Make 3 example PRP datasets to combine
import copy
prps_1 = copy.deepcopy(prps)
prps_2 = copy.deepcopy(prps)
prps_3 = copy.deepcopy(prps)
combined_prps = ns.combine(
[prps_1, prps_2, prps_3],
labels = ['Dataset_1', 'Dataset_2', 'Dataset_3'], # Optional. Labels to indicate source dataset of each row
label_col = 'Dataset_name' # Name of the label column to add
)
combined_prps.data_summary()
== PRP dataset summary == Key Type Values --------------------------------------------------------------------------------------------------- filename Factor sub-01_ses-1_task-60bi_acq-2.mat:54... (8 cells) sub Factor 01:432 ses Factor 1:216, 2:216 task Factor 60bi:108, 60mo:108, 75bi:108, 75mo:108 acq Factor 2:108, 1:108, 3:108, 4:108 phoneme Factor AA:24, AE:24, AO:24, AW:24, AY:24, EH:24, ER:24, EY:24, IH:24... (18 cells) manner Factor Vowel:288, Stop:144 PRP NDVar 64 time, 32 sensor; -9.18619e-06 - 9.62185e-06 Dataset_name Factor Dataset_1:144, Dataset_2:144, Dataset_3:144 --------------------------------------------------------------------------------------------------- Dataset: 432 cases
prps.data['sub']
Factor(['01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', '01', <... N=144>], name='sub')
Saving & loading PRPs¶
You can save the PRP data to (as .pickle file) disk and load it again next time. This can be useful when your dataset is large with many EEG files and would take some time to extract PRPs:
# Load the PRPs again
prps = ns.PRPData(
data_path = path_to_data,
montage_path = path_to_montage_file
)
# Apply filter to remove unwanted phonemes
filter = {'phoneme': ['AH', 'TH', 'Y', 'UH', 'Q', 'CH', 'JH']}
prps.subset_dataset(filter, exclude = True)
# Save the data
prps.save('examples/saved/prps_saved.pkl')
100%|██████████| 16/16 [00:06<00:00, 2.53it/s]
Done. Saved to examples/saved/prps_saved.pkl
# Load it again
prps = ns.PRPData.from_pickle('examples/saved/prps_saved.pkl')
Loaded attributes from examples/saved/prps_saved.pkl
Plotting PRPs¶
# Fronto-central channels
fronto_central_chs = ['AF3', 'AF4', 'Fz', 'F3', 'F4', 'FC1', 'FC2']
# Plot the averaged PRP for each manner, divided by the factors in split_by
prps.plot_manner_prps(
split_by = ['task'], # Factor(s) used to divide the figure. Separate subplot for level of the 'task' factor
line_by = 'manner', # Within each suplot, do separate PRP lines for each manner category (averaged across phonemes of the same manner)
title_prefix = '(FC)', # Optional, append a prefix to the title of each subplot
subset_sensors = fronto_central_chs, # Plot the average PRPs in this subset of electrodes
equal_y_scale = True, # Ensure that the y-scale is the same across subplots
fig_path = 'examples/figures/prps_fig.svg' # Optional. Save the figure to disk by giving its path
)
Included sensors: ['AF3', 'AF4', 'Fz', 'F3', 'F4', 'FC1', 'FC2']
By default it'll also show a topomap indicating which electrodes are used to plot the PRPs (red dots).
You can include more than one factor in split_by so that the function will plot manner PRPs for each combination of the levels of the factors:
prps.plot_manner_prps(
split_by = ['task', 'ses'], # Separate subplot for each combination of 'task' and 'ses' (session 1 or 2)
line_by = 'manner',
title_prefix = '(FC)',
subset_sensors = fronto_central_chs,
equal_y_scale = True,
show_topo = False # You can also turn off the topomap marking the plotted electrodes
)
Included sensors: ['AF3', 'AF4', 'Fz', 'F3', 'F4', 'FC1', 'FC2']
You can also draw vertical lines to mark certain time points:
# Define the time points and their corresponding text labels
mark = {'time': [0.05, 0.12, 0.23, 0.4], 'label': ['R1', 'R2', 'R3', 'R4']}
prps.plot_manner_prps(
split_by = ['task'],
line_by = 'manner',
title_prefix = '(FC)',
subset_sensors = fronto_central_chs,
mark = mark,
equal_y_scale = True
)
Included sensors: ['AF3', 'AF4', 'Fz', 'F3', 'F4', 'FC1', 'FC2']
And add standard errors around the mean curve:
prps.plot_manner_prps(
split_by = ['task'],
line_by = 'manner',
title_prefix = '(FC)',
subset_sensors = fronto_central_chs,
se = 1, # Number of SEs to plot around mean
se_alpha = 0.25, # Transparency of SE bands
equal_y_scale = True
)
Included sensors: ['AF3', 'AF4', 'Fz', 'F3', 'F4', 'FC1', 'FC2']
Extracting PRP amplitudes¶
Another useful function is get_values(). For PRPData, this allows you to extract the amplitudes at specific electrodes or time points/intervals from the internal PRP data. The returned dataframe can be used for subsequent analyses such as running statistical tests or training a machine-learning classifer (e.g., predict phoneme manner from the PRP amplitudes at specific time points or electrodes). Below are a few usage examples.
In this example, we extracted the amplitude values at a single time point (50 ms), summarized across electrodes listed in fronto_central_chs. By default summary_func = 'mean', meaning that we'll average across the amplitude values across sensors if summarize_sensors = True. Other available summary functions are 'min', 'max', and 'sum'. The average amplitude values for the PRPs are in the x0 of the retunred dataframe.
prp_amp_df = prps.get_values(
times = 0.05,
subset_sensors = fronto_central_chs, # Default is None and all electrodes will be included.
summarize_sensors = True, # Default is True.
summary_func = 'mean'
)
prp_amp_df.head(3)
| filename | sub | ses | task | acq | phoneme | manner | x0 | |
|---|---|---|---|---|---|---|---|---|
| 0 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AA | Vowel | -3.230879e-07 |
| 1 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AE | Vowel | 7.516904e-07 |
| 2 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AO | Vowel | 5.462134e-07 |
You can also supply a list of time points of interest and give each of time a more interpretable column name using output_col_labels:
prp_amp_df = prps.get_values(
times = [0.05, 0.11, 0.20],
subset_sensors = fronto_central_chs,
summarize_sensors = True,
summary_func = 'mean',
output_col_labels = ['R1', 'R2', 'R3']
)
prp_amp_df.head(3)
| filename | sub | ses | task | acq | phoneme | manner | R1 | R2 | R3 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AA | Vowel | -3.230879e-07 | 8.947557e-08 | 1.032721e-06 |
| 1 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AE | Vowel | 7.516904e-07 | 2.856267e-07 | 6.638414e-07 |
| 2 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AO | Vowel | 5.462134e-07 | -4.878900e-07 | 1.423067e-07 |
The times can also be intervals using tuples. In this example, we set summarize_time = True and average the amplitudes in 20-ms windows at R2 and R3:
prp_amp_df = prps.get_values(
times = [0.05, (0.10, 0.12), (0.19, 0.21)],
subset_sensors = fronto_central_chs,
summarize_sensors = True,
summarize_time = True, # Default is True
summary_func = 'mean',
output_col_labels = ['R1', 'R2', 'R3']
)
prp_amp_df.head(3)
| filename | sub | ses | task | acq | phoneme | manner | R1 | R2 | R3 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AA | Vowel | -3.230879e-07 | 5.897666e-08 | 1.149120e-06 |
| 1 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AE | Vowel | 7.516904e-07 | 3.266181e-07 | 6.606404e-07 |
| 2 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AO | Vowel | 5.462134e-07 | -4.192475e-07 | 7.937825e-08 |
If we set summarize_time = False, the summary function won't apply and we'll get amplitude values at each time sample within each time window. These individual time steps are indicated by the suffixes like _t0, _t1, etc.
prp_amp_df = prps.get_values(
times = [0.05, (0.10, 0.12), (0.19, 0.21)],
subset_sensors = fronto_central_chs,
summarize_sensors = True,
summarize_time = False,
summary_func = 'mean',
output_col_labels = ['R1', 'R2', 'R3']
)
prp_amp_df.head(3)
| filename | sub | ses | task | acq | phoneme | manner | R1 | R2_t0 | R2_t1 | R2_t2 | R3_t0 | R3_t1 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AA | Vowel | -3.230879e-07 | -1.189356e-07 | 8.947557e-08 | 2.063900e-07 | 1.265519e-06 | 1.032721e-06 |
| 1 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AE | Vowel | 7.516904e-07 | 4.029984e-07 | 2.856267e-07 | 2.912292e-07 | 6.574395e-07 | 6.638414e-07 |
| 2 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AO | Vowel | 5.462134e-07 | -3.934959e-07 | -4.878900e-07 | -3.763565e-07 | 1.644984e-08 | 1.423067e-07 |
So if you want to extract amplitudes over the entire duration of PRPs, averaged across fronto_central_chs, you can set a long continuous time interval from 0.0 to 0.5 s using times = (0.0, 0.5) and summarize_time = False:
prp_amp_df = prps.get_values(
times = (0.0, 0.5),
subset_sensors = fronto_central_chs,
summarize_sensors = True,
summarize_time = False,
summary_func = 'mean'
)
prp_amp_df.head(3)
| filename | sub | ses | task | acq | phoneme | manner | x0_t0 | x0_t1 | x0_t2 | ... | x0_t54 | x0_t55 | x0_t56 | x0_t57 | x0_t58 | x0_t59 | x0_t60 | x0_t61 | x0_t62 | x0_t63 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AA | Vowel | -1.329540e-07 | 1.934834e-07 | 3.267872e-07 | ... | 2.547433e-07 | 1.778489e-07 | 1.435099e-07 | 1.765034e-07 | 2.941042e-07 | 4.749168e-07 | 6.790940e-07 | 8.379049e-07 | 8.780326e-07 | 7.462857e-07 |
| 1 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AE | Vowel | 3.058108e-08 | 4.995545e-08 | 8.560086e-08 | ... | -6.546889e-07 | -5.344407e-07 | -2.544637e-07 | 7.210964e-08 | 3.157831e-07 | 3.840106e-07 | 2.566018e-07 | -8.263945e-09 | -3.176756e-07 | -5.728460e-07 |
| 2 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AO | Vowel | -6.592528e-07 | -6.657782e-07 | -5.982707e-07 | ... | 8.136910e-07 | 6.664765e-07 | 5.052684e-07 | 3.276658e-07 | 1.461521e-07 | 6.347341e-09 | -5.055015e-08 | -1.809129e-08 | 5.754361e-08 | 1.089469e-07 |
3 rows × 71 columns
Similarly, if we set summarize_sensors = False, we'll get amplitude values at individual electrodes appearing as separate columns:
prp_amp_df = prps.get_values(
times = [0.05, (0.10, 0.12), (0.19, 0.21)],
subset_sensors = ['Cz', 'Fz', 'Oz'], # Use these 3 electrodes for example
summarize_sensors = False,
summarize_time = True,
summary_func = 'mean',
output_col_labels = ['R1', 'R2', 'R3']
)
prp_amp_df.head(3)
| filename | sub | ses | task | acq | phoneme | manner | R1_Cz | R1_Fz | R1_Oz | R2_Cz | R2_Fz | R2_Oz | R3_Cz | R3_Fz | R3_Oz | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AA | Vowel | 1.942694e-07 | -2.612044e-07 | -7.517175e-07 | -1.041146e-06 | -2.412378e-07 | -0.000001 | -4.149416e-07 | 3.576717e-07 | -1.489215e-06 |
| 1 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AE | Vowel | 6.153197e-07 | 8.105184e-07 | -1.825531e-06 | 4.584019e-07 | 2.358443e-07 | -0.000002 | 5.479022e-07 | 3.531864e-07 | -1.893833e-06 |
| 2 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AO | Vowel | 6.887054e-07 | 6.998426e-07 | -2.929385e-07 | -5.297552e-07 | -2.366920e-07 | 0.000001 | -1.105885e-06 | 2.526893e-07 | 6.250017e-07 |
If both summarize_time and summarize_senosrs are False, we'll extract values at individual sensors and individual time points, and the output dataframe can wide with many columns:
prp_amp_df = prps.get_values(
times = [0.05, (0.10, 0.12), (0.19, 0.21)],
subset_sensors = ['Cz', 'Fz', 'Oz'], # Use these 3 electrodes for example
summarize_sensors = False,
summarize_time = False,
summary_func = 'mean',
output_col_labels = ['R1', 'R2', 'R3']
)
prp_amp_df.head(3)
| filename | sub | ses | task | acq | phoneme | manner | R1_Cz | R1_Fz | R1_Oz | ... | R2_t1_Oz | R2_t2_Cz | R2_t2_Fz | R2_t2_Oz | R3_t0_Cz | R3_t0_Fz | R3_t0_Oz | R3_t1_Cz | R3_t1_Fz | R3_t1_Oz | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AA | Vowel | 1.942694e-07 | -2.612044e-07 | -7.517175e-07 | ... | -0.000001 | -7.463753e-07 | -9.256759e-08 | -0.000001 | -4.011817e-07 | 4.644346e-07 | -1.590194e-06 | -4.287014e-07 | 2.509087e-07 | -1.388235e-06 |
| 1 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AE | Vowel | 6.153197e-07 | 8.105184e-07 | -1.825531e-06 | ... | -0.000002 | 4.901450e-07 | 1.943750e-07 | -0.000002 | 5.258701e-07 | 3.467719e-07 | -1.955544e-06 | 5.699343e-07 | 3.596009e-07 | -1.832123e-06 |
| 2 | sub-01_ses-1_task-60bi_acq-2.mat | 01 | 1 | 60bi | 2 | AO | Vowel | 6.887054e-07 | 6.998426e-07 | -2.929385e-07 | ... | 0.000002 | -5.367173e-07 | -1.613366e-07 | 0.000001 | -1.033115e-06 | 2.315253e-07 | 6.938648e-07 | -1.178654e-06 | 2.738532e-07 | 5.561385e-07 |
3 rows × 25 columns
Finally, by default get_values() return a Pandas dataframe. By setting return_dataframe = False, nothing will be returned and the extracted values will be saved as columns to the internal PRP dataset:
prps.get_values(
times = [0.05, (0.10, 0.12), (0.19, 0.21)],
subset_sensors = fronto_central_chs,
summarize_sensors = True,
summarize_time = True,
summary_func = 'mean',
output_col_labels = ['R1', 'R2', 'R3'],
return_dataframe = False
)
prp_data = prps.get_data()
print(prp_data.head())
Adding extracted values to the data object. # filename sub ses task acq phoneme manner R1 R2 R3 ----------------------------------------------------------------------------------------------------------------------------- 0 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 AA Vowel -3.2309e-07 5.8977e-08 1.1491e-06 1 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 AE Vowel 7.5169e-07 3.2662e-07 6.6064e-07 2 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 AO Vowel 5.4621e-07 -4.1925e-07 7.9378e-08 3 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 AW Vowel 1.1759e-06 -5.4042e-08 1.28e-07 4 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 AY Vowel 5.549e-07 -8.7097e-07 2.9084e-09 5 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 B Stop 4.0048e-07 7.7149e-07 4.6152e-07 6 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 D Stop 5.8449e-07 1.8902e-07 -3.6974e-07 7 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 DH Fricative -3.1538e-07 2.5974e-07 2.641e-07 8 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 EH Vowel 6.7521e-07 1.0127e-06 6.6264e-07 9 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60bi 2 ER Vowel 3.5533e-07 5.7984e-08 -2.5965e-07 ----------------------------------------------------------------------------------------------------------------------------- NDVars: PRP
Miscellaneous¶
1. Custom file name parsing¶
BY default, PRPData assumes that metadata of EEG are encoded in the file names in the BIDS format. You can define a custom file name parsing function to override this behavoior. In the example above, the "task" factor consists of both intensity and presentation mode information (e.g., "60bi"). If you want to have separate variabls indicating intensity and presentation mode in the output, you can create a function like the following and pass it to PRPData. Essentially, all this function needs to do is just return a dictionary where key-value pairs are factor names and their corresponding values. This utility allows you to define your own rules for understanding the file names, making it more convenient for subsequent analyses
# Custom function to parse filenames
import os
def parse_filename(fn):
fn = os.path.basename(fn)
sub, ses, task, acq = fn.split('_')
sub = sub.split('-')[-1]
ses = ses.split('-')[-1]
task = task.split('-')[-1]
intensity = task[0:2]
pres = task[-2:]
return {'sub': sub, 'ses': ses, 'int': intensity, 'pres': pres}
prps = ns.PRPData(
data_path = path_to_data,
montage_path = path_to_montage_file,
metadata_fn = parse_filename # Pass that custom function here
)
100%|██████████| 16/16 [00:06<00:00, 2.52it/s]
Done.
# Note that now "task" is replaced with "int" and "pres", encoding information about intensity level and presentation mode, respectively.
prps.data_summary()
== PRP dataset summary == Key Type Values ---------------------------------------------------------------------------------------------------- filename Factor sub-01_ses-1_task-60bi_acq-2.mat:37... (16 cells) sub Factor 01:296, 02:296 ses Factor 1:296, 2:296 int Factor 60:296, 75:296 pres Factor bi:296, mo:296 phoneme Factor AA:16, AE:16, AH:16, AO:16, AW:16, AY:16, B:16, CH:16, D:16, DH:16... (37 cells) manner Factor Vowel:224, Stop:128, Fricative:128, Nasal-approximant:112 PRP NDVar 64 time, 32 sensor; -0.000121755 - 3.32726e-05 ---------------------------------------------------------------------------------------------------- Dataset: 592 cases
Saved to examples/saved/prps_saved.pkl
2. Recoding values¶
Values in any columns of the data in PRPData can be recoded using the recode() method:
# Recode levels "pres" and "int" using a dictionary
recode_pres = {'bi': 'Binaural',
'mo': 'Monaural'}
prps.recode('pres', recode_pres)
recode_int= {'60': '60_dB',
'75': '75_dB'}
prps.recode('int', recode_int)
print(prps.get_data().head())
# filename sub ses int pres phoneme manner ----------------------------------------------------------------------------------------- 0 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60_dB Binaural AA Vowel 1 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60_dB Binaural AE Vowel 2 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60_dB Binaural AO Vowel 3 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60_dB Binaural AW Vowel 4 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60_dB Binaural AY Vowel 5 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60_dB Binaural B Stop 6 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60_dB Binaural D Stop 7 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60_dB Binaural DH Fricative 8 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60_dB Binaural EH Vowel 9 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60_dB Binaural ER Vowel ----------------------------------------------------------------------------------------- NDVars: PRP
3. Changing line style of PRP curves¶
You can change the linewidth and colors of manner PRPs in the plot like this:
# New color mapping for manner PRPs
new_colors = {
'Vowel': '#8338EC',
'Stop': '#FF006E',
'Fricative': '#FB5607',
'Nasal-approximant': '#FFBE0B'
}
prps.plot_manner_prps(
split_by = ['pres', 'int'],
line_by = 'manner',
title_prefix = '(FC)',
subset_sensors = fronto_central_chs,
equal_y_scale = True,
se = 1,
lw = 4.5, # Default linewidth is 3.0, change to 4.5 to make it thicker
line_colors = new_colors,
show_topo = False
)
Included sensors: ['AF3', 'AF4', 'Fz', 'F3', 'F4', 'FC1', 'FC2']
4. Better plot aesthetics¶
You can set plot aesthetics and layout globally with plt.rcParams.update():
import matplotlib.pyplot as plt
plt.rcParams.update({
# Font
'font.family': 'Arial',
# Tick label sizes
'xtick.labelsize': 14,
'ytick.labelsize': 14,
# Axis label sizes
'axes.labelsize': 16,
# Title size
'axes.titlesize': 16,
# Remove top/right spines globally
'axes.spines.top': False,
'axes.spines.right': False,
# Optional: cleaner tick appearance
'xtick.direction': 'out',
'ytick.direction': 'out',
# Optional: make saved figures cleaner
'savefig.bbox': 'tight',
'savefig.dpi': 300,
})
prps.plot_manner_prps(
split_by = ['task'],
line_by = 'manner',
title_prefix = '(FC)',
subset_sensors = fronto_central_chs,
se = 1,
se_alpha = 0.25,
equal_y_scale = True
)
Included sensors: ['AF3', 'AF4', 'Fz', 'F3', 'F4', 'FC1', 'FC2']