Logo
  • Home
  • Update history

Getting started

  • Installation

Phoneme-related potential

  • Preparing your data
  • PRP basics
  • F-statistic of manner separability
    • Calculating F-statistic
    • Plotting F-statistics
    • Extracting F-statistics
    • Statistical analyses

Temporal response function

  • Introduction
  • Getting started
  • Envelope and onset predictors
  • Preparing EEG data
  • Plotting EEG and stimuli
  • Running TRF model
  • Submitting Slurm jobs
  • Analyzing results
  • Predictors from TextGrids
  • Predictors from tables
  • Pitch predictors
Neuraspeech
  • Phoneme-related potential
  • F-statistic of manner separability
  • Edit on zc-guo/neuraspeech

F-statistic of manner separability¶

Calculating F-statistic¶

The F-statistic measures the ratio of between-category variance over within-category variance. Higher values indicate greater category separability. This metric can be computed for PRPs as a measure of neural separability of different manner-of-articulation classes. Here, we consider these 4 major sound classes: vowel, stop, fricative, and nasal-approximant. You can check the manner category of each phoneme:

In [2]:
Copied!
import neuraspeech as ns
for k, v in ns.PHON2MANNER.items():
    print(f'{k}: {v}')
import neuraspeech as ns for k, v in ns.PHON2MANNER.items(): print(f'{k}: {v}')
AA: Vowel
AE: Vowel
AH: Vowel
AO: Vowel
AW: Vowel
AY: Vowel
EH: Vowel
ER: Vowel
EY: Vowel
IH: Vowel
IY: Vowel
OW: Vowel
OY: Vowel
UH: Vowel
UW: Vowel
L: Nasal-approximant
M: Nasal-approximant
N: Nasal-approximant
NG: Nasal-approximant
R: Nasal-approximant
W: Nasal-approximant
Y: Nasal-approximant
DH: Fricative
F: Fricative
HH: Fricative
S: Fricative
SH: Fricative
V: Fricative
Z: Fricative
TH: Fricative
ZH: Fricative
B: Stop
D: Stop
G: Stop
K: Stop
P: Stop
T: Stop
CH: Stop
JH: Stop
Q: Stop

We'll use FStatistic from the toolbox to compute F-statistic. Specifically, an F-value will be computed at each electrode at each time point over the duration of PRP. By default, we compute F seaprately for each unique value of the "filename" factor, so that . In the example data, there are 2 subjects and 8 conditions (resulting from 3 binary factors). This means that there'll be 16 rows in the output dataset.

In [3]:
Copied!
# Load in the PRP data
prps = ns.PRPData.from_pickle('examples\saved\prps_saved.pkl')

# Compute F
fstats = ns.FStatistic(
    prps,                   # Pass the PRPData object here
    by = 'filename',        # Default to 'filename', meaning that the results will be computed separately for each filename value
    category = 'manner'     # Defualt to 'manner'. Name of variable indicatings cateogry labels.
    )
# Load in the PRP data prps = ns.PRPData.from_pickle('examples\saved\prps_saved.pkl') # Compute F fstats = ns.FStatistic( prps, # Pass the PRPData object here by = 'filename', # Default to 'filename', meaning that the results will be computed separately for each filename value category = 'manner' # Defualt to 'manner'. Name of variable indicatings cateogry labels. )
Loaded attributes from examples\saved\prps_saved.pkl
100%|██████████| 16/16 [01:45<00:00,  6.59s/it]
Done.

Note that the computation of F may take some time especially when the dataset is large. But just like PRPData, we can set the results to disk when the code finishes running and skip the calculation next time by loading it. Highly recommend to do this.

In [4]:
Copied!
# Save the F-stat results
fstats.save('examples/saved/fstats_saved.pkl')

# Load it again
fstats = ns.FStatistic.from_pickle('examples/saved/fstats_saved.pkl')
# Save the F-stat results fstats.save('examples/saved/fstats_saved.pkl') # Load it again fstats = ns.FStatistic.from_pickle('examples/saved/fstats_saved.pkl')
Saved to examples/saved/fstats_saved.pkl
Loaded attributes from examples/saved/fstats_saved.pkl

Like PRPData, the F-statistic results in the retunred FStatistic object are stored as a eelbrain Dataset, which can be acessed using the .get_data() method:

In [5]:
Copied!
fstats_data = fstats.get_data()
print(fstats.get_data())
fstats_data = fstats.get_data() print(fstats.get_data())
#    filename                           sub   ses   int   pres   stat  
-----------------------------------------------------------------------
0    sub-01_ses-1_task-60bi_acq-2.mat   01    1     60    bi     F-stat
1    sub-01_ses-1_task-60mo_acq-1.mat   01    1     60    mo     F-stat
2    sub-01_ses-1_task-75bi_acq-3.mat   01    1     75    bi     F-stat
3    sub-01_ses-1_task-75mo_acq-4.mat   01    1     75    mo     F-stat
4    sub-01_ses-2_task-60bi_acq-2.mat   01    2     60    bi     F-stat
5    sub-01_ses-2_task-60mo_acq-1.mat   01    2     60    mo     F-stat
6    sub-01_ses-2_task-75bi_acq-3.mat   01    2     75    bi     F-stat
7    sub-01_ses-2_task-75mo_acq-4.mat   01    2     75    mo     F-stat
8    sub-02_ses-1_task-60bi_acq-3.mat   02    1     60    bi     F-stat
9    sub-02_ses-1_task-60mo_acq-4.mat   02    1     60    mo     F-stat
10   sub-02_ses-1_task-75bi_acq-2.mat   02    1     75    bi     F-stat
11   sub-02_ses-1_task-75mo_acq-1.mat   02    1     75    mo     F-stat
12   sub-02_ses-2_task-60bi_acq-3.mat   02    2     60    bi     F-stat
13   sub-02_ses-2_task-60mo_acq-4.mat   02    2     60    mo     F-stat
14   sub-02_ses-2_task-75bi_acq-2.mat   02    2     75    bi     F-stat
15   sub-02_ses-2_task-75mo_acq-1.mat   02    2     75    mo     F-stat
-----------------------------------------------------------------------
NDVars: F

For each row (i.e., a unique filename), there is a 2-dimensional F-statistic array with shape of (no_timepoints, no_electrodes). Here we have 64 time points and 32 electrodes.

In [6]:
Copied!
print(fstats_data['F'])
print(fstats_data['F'])
<NDVar 'F': 16 case, 64 time, 32 sensor>

You can also recode the variable values as in the case of PRPData:

In [7]:
Copied!
# Recode levels "pres" and "int" using a dictionary
recode_pres = {'bi': 'Binaural', 
               'mo': 'Monaural'}

fstats.recode('pres', recode_pres)

recode_int= {'60': '60_dB',
             '75': '75_dB'}

fstats.recode('int', recode_int)

print(fstats.get_data().head())
# Recode levels "pres" and "int" using a dictionary recode_pres = {'bi': 'Binaural', 'mo': 'Monaural'} fstats.recode('pres', recode_pres) recode_int= {'60': '60_dB', '75': '75_dB'} fstats.recode('int', recode_int) print(fstats.get_data().head())
#   filename                           sub   ses   int     pres       stat  
----------------------------------------------------------------------------
0   sub-01_ses-1_task-60bi_acq-2.mat   01    1     60_dB   Binaural   F-stat
1   sub-01_ses-1_task-60mo_acq-1.mat   01    1     60_dB   Monaural   F-stat
2   sub-01_ses-1_task-75bi_acq-3.mat   01    1     75_dB   Binaural   F-stat
3   sub-01_ses-1_task-75mo_acq-4.mat   01    1     75_dB   Monaural   F-stat
4   sub-01_ses-2_task-60bi_acq-2.mat   01    2     60_dB   Binaural   F-stat
5   sub-01_ses-2_task-60mo_acq-1.mat   01    2     60_dB   Monaural   F-stat
6   sub-01_ses-2_task-75bi_acq-3.mat   01    2     75_dB   Binaural   F-stat
7   sub-01_ses-2_task-75mo_acq-4.mat   01    2     75_dB   Monaural   F-stat
8   sub-02_ses-1_task-60bi_acq-3.mat   02    1     60_dB   Binaural   F-stat
9   sub-02_ses-1_task-60mo_acq-4.mat   02    1     60_dB   Monaural   F-stat
----------------------------------------------------------------------------
NDVars: F

Plotting F-statistics¶

The usage of the plotting function of the F-statistic results is almost identical to that of PRP data. The only difference is that this time only one curve representing the F-value will be plotted in each panel.

In [13]:
Copied!
# Fronto-central channels
fronto_central_chs = ['AF3', 'AF4', 'Fz', 'F3', 'F4', 'FC1', 'FC2']

# Mark these time points and their corresponding text labels
mark = {'time': [0.05, 0.12, 0.23, 0.4], 'label': ['R1', 'R2', 'R3', 'R4']}

# Plot the averaged PRP for each manner, divided by the factors in split_by
fstats.plot_f_stats(
    split_by = ['pres', 'int'],                  # Factor(s) used to divide the figure.
    title_prefix = '(FC)',                       # Optional, append a prefix to the title of each subplot
    subset_sensors = fronto_central_chs,         # Plot the average F-stat in this subset of electrodes
    equal_y_scale = True,                        # Ensure that the y-scale is the same across subplots
    se = 1,                                      # SE around mean
    lw = 3.5,                                    # Linewidth
    mark = mark,                                 # Mark these time points
    show_topo = True,                            # Show topo of the plotted sensors
    line_colors = '#008ADC',                   # Line color, default is red,
    fig_path = 'examples/figures/fstats_fig.svg' # Optional. Save the figure to disk by giving its path
    )
# Fronto-central channels fronto_central_chs = ['AF3', 'AF4', 'Fz', 'F3', 'F4', 'FC1', 'FC2'] # Mark these time points and their corresponding text labels mark = {'time': [0.05, 0.12, 0.23, 0.4], 'label': ['R1', 'R2', 'R3', 'R4']} # Plot the averaged PRP for each manner, divided by the factors in split_by fstats.plot_f_stats( split_by = ['pres', 'int'], # Factor(s) used to divide the figure. title_prefix = '(FC)', # Optional, append a prefix to the title of each subplot subset_sensors = fronto_central_chs, # Plot the average F-stat in this subset of electrodes equal_y_scale = True, # Ensure that the y-scale is the same across subplots se = 1, # SE around mean lw = 3.5, # Linewidth mark = mark, # Mark these time points show_topo = True, # Show topo of the plotted sensors line_colors = '#008ADC', # Line color, default is red, fig_path = 'examples/figures/fstats_fig.svg' # Optional. Save the figure to disk by giving its path )
Included sensors: ['AF3', 'AF4', 'Fz', 'F3', 'F4', 'FC1', 'FC2']
No description has been provided for this image
No description has been provided for this image

Additionally, you can plot the topographies of F values at different time points:

In [14]:
Copied!
fstats.plot_f_topo(
    split_by = ['pres', 'int'],                 # Split the figure based on combinations of the levels of these factors
    times = [0.05, 0.12, 0.23],                 # Plot topomaps of F at these time points
    vlim = (0.0, 6.0),                          # Lower and upper bounds of the colormap
    cmap = 'Reds',                              # Matplitlib colormap. Default is "Reds"     
    fig_path = 'examples/figures/f_topo.svg'
    )
fstats.plot_f_topo( split_by = ['pres', 'int'], # Split the figure based on combinations of the levels of these factors times = [0.05, 0.12, 0.23], # Plot topomaps of F at these time points vlim = (0.0, 6.0), # Lower and upper bounds of the colormap cmap = 'Reds', # Matplitlib colormap. Default is "Reds" fig_path = 'examples/figures/f_topo.svg' )
No description has been provided for this image

Extracting F-statistics¶

We can also get the F-statistics at particular time points and electrodes using get_values(). The usage is exactly the same as the get_values() of PRPData (see Extracting PRP amplitudes in PRP basics), except this time it will return F-statistic value instead of PRP amplitudes. In the example below, we get F-statists averaged across fronto_central_chs electrodes within 20-ms time windows centered at 50 ms, 110 ms, 200 ms, and 400 ms and label them as R1, R2, R3, and R4:

In [15]:
Copied!
fstats_df = fstats.get_values(
    times = [(0.04, 0.06), (0.10, 0.12), (0.19, 0.21), (0.39, 0.41)], 
    subset_sensors = fronto_central_chs,
    summarize_sensors = True,
    summarize_time = True, 
    summary_func = 'mean',
    output_col_labels = ['R1', 'R2', 'R3', 'R4']
    )
fstats_df.head(3)
fstats_df = fstats.get_values( times = [(0.04, 0.06), (0.10, 0.12), (0.19, 0.21), (0.39, 0.41)], subset_sensors = fronto_central_chs, summarize_sensors = True, summarize_time = True, summary_func = 'mean', output_col_labels = ['R1', 'R2', 'R3', 'R4'] ) fstats_df.head(3)
Out[15]:
filename sub ses int pres stat R1 R2 R3 R4
0 sub-01_ses-1_task-60bi_acq-2.mat 01 1 60_dB Binaural F-stat 2.132361 0.996413 0.495814 0.475428
1 sub-01_ses-1_task-60mo_acq-1.mat 01 1 60_dB Monaural F-stat 2.223798 2.390641 0.853467 0.305553
2 sub-01_ses-1_task-75bi_acq-3.mat 01 1 75_dB Binaural F-stat 1.433542 0.927914 1.404303 0.299543

Statistical analyses¶

There are many ways to run statistical tests on manner separability or similar measures so a complete coverage is beyond the scope of this tutorial. But one quick way to test if the factors of interest have significant effects is to use a mass-univariate permutation-based approach, which allows you to identify clusters of electrodes with a significant effect and are already implemented in eelbrain. Below is an example of a mass-univariate ANOVA on our example dataset. For more details about this statistical method, see Brodbeck et al. (2023) and the eelbrain tutorial here.

In [16]:
Copied!
import eelbrain
fstats_data = fstats.get_data()
fstats_data['F'].sensor.set_adjacency(connect_dist = 1.6) # Setting adjacency defining which electrodes are neighbors
fstats_data['sub'].random = True                          # Subject as a random effect

# ANOVA: F ~ ses * int * pres
fstats_anova_res = eelbrain.testnd.ANOVA(
    'F',
    'ses * int * pres',
    data = fstats_data,
    pmin = 0.05,        # Use uncorrected p = 0.05 as threshold for forming clusters
    tstart = 0.0,
    tstop = 0.5,
    samples = 10000     # Number of permutations to speed up the example; use 10'000 when the exact p-value matters
)
fstats_anova_res.find_clusters(0.05)
import eelbrain fstats_data = fstats.get_data() fstats_data['F'].sensor.set_adjacency(connect_dist = 1.6) # Setting adjacency defining which electrodes are neighbors fstats_data['sub'].random = True # Subject as a random effect # ANOVA: F ~ ses * int * pres fstats_anova_res = eelbrain.testnd.ANOVA( 'F', 'ses * int * pres', data = fstats_data, pmin = 0.05, # Use uncorrected p = 0.05 as threshold for forming clusters tstart = 0.0, tstop = 0.5, samples = 10000 # Number of permutations to speed up the example; use 10'000 when the exact p-value matters ) fstats_anova_res.find_clusters(0.05)
Permutation test: 100%|██████████| 10000/10000 [00:08<00:00, 1168.79 permutations/s]
Out[16]:
# id tstart tstop duration n_sensors v p sig effect

If there are significant clusters, find_clusters() will print out a table listing the start/end times, number of electrodes, etc. in those clusters.

In addition to mass-univariate tests, you may also extract average F-statistics as in the fstats_df example above and run standard statistical tests, if you have specific interest periods/regions.

Previous Next

Built with MkDocs using a theme provided by Read the Docs.
zc-guo/neuraspeech « Previous Next »