P3 Auditory oddball

This builds on the P50 example.

Experimental design

  • Stimuli are 2 different auditory tones presented in random order, with higher probability (“standard, .66) and lower probability (”target”, .33).

  • One tone is higher frequency, the other is lower frequency. There are two blocks of trials, each tone serves as the standard one block, the target in the other.

  • The task is to respond to the target tones by pressing a button.

Event code tagging

  • The codemap patterns tag the stimulus and response event sequences for stimulus type, tone type, and response accuracy.

  • EEG quality control flags (log_evcodes > 0) are used to exclude EEG trials prior to analysis

[1]:
import os
import sys
from pathlib import Path
import re
import numpy as np
import pandas as pd
import mkpy
import spudtr

from matplotlib import pyplot as plt
from mkpy import mkh5
from spudtr import epf

# path wrangling for nbsphinx
if "MDE_HOME" in os.environ.keys():
    MDE_HOME = Path(os.environ["MDE_HOME"])
else:
    from conf import MDE_HOME

DOCS_DATA = MDE_HOME / "docs/_data"

print(os.environ["CONDA_DEFAULT_ENV"])
print(sys.version)
for pkg in [np, pd, mkpy, spudtr]:
    print(pkg.__name__, pkg.__version__, pkg.__file__)
mkconda_dev_py39_053022
3.9.13 | packaged by conda-forge | (main, May 27 2022, 16:56:21)
[GCC 10.3.0]
numpy 1.21.6 /home/turbach/miniconda39/envs/mkconda_dev_py39_053022/lib/python3.9/site-packages/numpy/__init__.py
pandas 1.1.5 /home/turbach/miniconda39/envs/mkconda_dev_py39_053022/lib/python3.9/site-packages/pandas/__init__.py
mkpy 0.2.7 /mnt/cube/home/turbach/TPU_Projects/mkpy/mkpy/__init__.py
spudtr 0.1.0 /home/turbach/miniconda39/envs/mkconda_dev_py39_053022/lib/python3.9/site-packages/spudtr/__init__.py

mkh5 EEG data, event code log, header information

[2]:
# set filenames
crw = MDE_HOME / "mkdig/sub000p3.crw"    # EEG recording
log = MDE_HOME / "mkdig/sub000p3.x.log"  # events
yhdr = MDE_HOME / "mkpy/sub000p3.yhdr"   # extra header info

# set calibration data filenames
cals_crw = MDE_HOME / "mkdig/sub000c.crw"
cals_log = MDE_HOME / "mkdig/sub000c.x.log"
cals_yhdr = MDE_HOME / "mkpy/sub000c.yhdr"

# HDF5 file with EEG recording, events, and header
p3_h5_f = DOCS_DATA / "sub000p3.h5"
print(p3_h5_f)

# convert to HDF5
p3_h5 = mkh5.mkh5(p3_h5_f)

p3_h5.reset_all()
p3_h5.create_mkdata("sub000", crw, log, yhdr)

# add calibration data
p3_h5.append_mkdata("sub000", cals_crw, cals_log, cals_yhdr)

# calibrate
pts, pulse, lo, hi, ccode = 5, 10, -40, 40, 0
p3_h5.calibrate_mkdata(
    "sub000",  # data group to calibrate with these cal pulses
    n_points=pts,  # pts to average
    cal_size=pulse,  # uV
    lo_cursor=lo,  # lo_cursor ms
    hi_cursor=hi,  # hi_cursor ms
    cal_ccode=ccode,  # condition code
)
/mnt/cube/home/turbach/TPU_Projects/mkpy_data_examples/docs/_data/sub000p3.h5
/mnt/cube/home/turbach/TPU_Projects/mkpy/mkpy/mkh5.py:3666: UserWarning: negative event code(s) found for cal condition code 0 -16384
  warnings.warn(msg)
Found cals in /sub000/dblock_4
Calibrating block /sub000/dblock_0 of 5: (31232,)
Calibrating block /sub000/dblock_1 of 5: (32768,)
Calibrating block /sub000/dblock_2 of 5: (31744,)
Calibrating block /sub000/dblock_3 of 5: (32512,)
Calibrating block /sub000/dblock_4 of 5: (28416,)

codemap

[3]:
%%bash
head -26 ${MDE_HOME}/mkpy/p3_codemap.ytbl
# mkpy YAML codemap
# rows and columns must align, regexp is a mandatory column, all else is whatever.
#
# Note the regular expression negation *NON CAPTURNG* group (?!1040)
# to pattern-match "anything except 1040", i.e., an omitted response,
# as in (#10) (?!1040). Depending on the stimulus type, an ommited
# response is "correct" or "incorrect"

---
name: p3

columns:
  [regexp,          ccode,  instrument,   bin,    tone,  stim,       accuracy,    acc_type ]

rows:
  - ['(#\d+)',          0,         cal,     0,    cal,    cal,        cal,         cal      ]
  - ['(#11) 1040',      1,         eeg,     3,     hi,    target,     correct,     hit      ]
  - ['(#10) (?!1040)',  1,         eeg,     4,     lo,    standard,   correct,     cr       ]
  - ['(#11) (?!1040)',  1,         eeg,     5,     hi,    target,     incorrect,   miss     ]
  - ['(#10) 1040',      1,         eeg,     6,     lo,    standard,   incorrect,   fa       ]

  - ['(#21) 1040',      1,         eeg,     9,     lo,    target,     correct,      hit     ]
  - ['(#20) (?!1040)',  1,         eeg,    10,     hi,    standard,   correct,      cr      ]
  - ['(#21) (?!1040)',  1,         eeg,    11,     lo,    target,     incorrect,    miss    ]
  - ['(#20) 1040',      1,         eeg,    12,     hi,    standard,   incorrect,    fa      ]

1. get_event_table(codemap)

[4]:
p3_event_table = p3_h5.get_event_table(MDE_HOME / "mkpy/p3_codemap.ytbl")
searching codes in: sub000/dblock_0
searching codes in: sub000/dblock_1
searching codes in: sub000/dblock_2
searching codes in: sub000/dblock_3
searching codes in: sub000/dblock_4
/mnt/cube/home/turbach/TPU_Projects/mkpy/mkpy/mkh5.py:1059: UserWarning:
As of mkpy 0.2.0 to match events with a codemap regexp pattern, the
ccode column in p3_codemap.ytbl must also match the log_ccode
in the datablock. If this behavior is not desired, delete or rename
the ccode column in the codemap.
  warnings.warn(msg)

inspect the event table

[5]:
print(p3_event_table.shape)
print(p3_event_table.columns)

# select some columns to show
example_columns = [
    "dblock_path", "dblock_ticks", "log_evcodes", "log_ccodes", "log_flags",
    "regexp", "match_code", "instrument", "bin", "tone", "stim", "acc_type",
]

# first few stimulus events
display(p3_event_table[example_columns].head())

# last few calibration pulse events
display(p3_event_table[example_columns].tail())

pd.crosstab(
    [
        p3_event_table.instrument,
        p3_event_table.ccode,
        p3_event_table.stim,
        p3_event_table.accuracy,
        p3_event_table.acc_type,
        p3_event_table.tone,
        p3_event_table.bin,
    ],
    [
        p3_event_table.log_flags
    ],
    margins=True
)
(601, 31)
Index(['data_group', 'dblock_path', 'dblock_tick_idx', 'dblock_ticks',
       'crw_ticks', 'raw_evcodes', 'log_evcodes', 'log_ccodes', 'log_flags',
       'epoch_match_tick_delta', 'epoch_ticks', 'dblock_srate', 'match_group',
       'idx', 'dlim', 'anchor_str', 'match_str', 'anchor_code', 'match_code',
       'anchor_tick', 'match_tick', 'anchor_tick_delta', 'is_anchor', 'regexp',
       'ccode', 'instrument', 'bin', 'tone', 'stim', 'accuracy', 'acc_type'],
      dtype='object')
dblock_path dblock_ticks log_evcodes log_ccodes log_flags regexp match_code instrument bin tone stim acc_type
125 sub000/dblock_0 1468 11 1 0 (#11) 1040 11 eeg 3 hi target hit
126 sub000/dblock_0 2396 11 1 0 (#11) 1040 11 eeg 3 hi target hit
127 sub000/dblock_0 2943 11 1 0 (#11) 1040 11 eeg 3 hi target hit
128 sub000/dblock_0 3308 11 1 0 (#11) 1040 11 eeg 3 hi target hit
129 sub000/dblock_0 4574 11 1 0 (#11) 1040 11 eeg 3 hi target hit
dblock_path dblock_ticks log_evcodes log_ccodes log_flags regexp match_code instrument bin tone stim acc_type
1088 sub000/dblock_4 27315 4 0 0 (#\d+) 4 cal 0 cal cal cal
1089 sub000/dblock_4 27444 2 0 0 (#\d+) 2 cal 0 cal cal cal
1090 sub000/dblock_4 27573 3 0 0 (#\d+) 3 cal 0 cal cal cal
1091 sub000/dblock_4 27703 4 0 0 (#\d+) 4 cal 0 cal cal cal
1092 sub000/dblock_4 27832 2 0 0 (#\d+) 2 cal 0 cal cal cal
[5]:
log_flags 0 32 48 64 All
instrument ccode stim accuracy acc_type tone bin
cal 0 cal cal cal cal 0 208 0 0 1 209
eeg 1 standard correct cr hi 10 142 3 1 0 146
lo 4 138 7 1 0 146
target correct hit hi 3 23 12 15 0 50
lo 9 31 11 8 0 50
All 542 33 25 1 601

2. set_epochs(name, pre, post)

Stash the the event table events and tags with a name and epoch interval boundaries.

[6]:
p3_h5.set_epochs("ms1500", p3_event_table, -750, 750)
Sanitizing event table data types for mkh5 epochs table ...
/mnt/cube/home/turbach/TPU_Projects/mkpy/mkpy/mkh5.py:1515: UserWarning: data error: pre-stimulus interval is out of bounds left ... skipping epoch (392, b'sub000', b'sub000/dblock_4', 0, 26, 26, 2, 2, 0, 64, -187, 375, 250., 1, 0, 1, b'2', b'2', 2, 2, 26, 26, 0, True, b'(#\\d+)', 0, b'cal', 0, b'cal', b'cal', b'cal', b'cal', 0, 0, 0, 26, -187, 375)
  warnings.warn(

3. export_epochs(name, file_format=…)

Export the time-stamped, tagged, fixed-length segments of EEG data for analysis as HDF5, pandas HDF5 or feather

[7]:
p3_epochs_f = DOCS_DATA / "sub000p3.ms1500.epochs.feather"
p3_h5.export_epochs("ms1500", p3_epochs_f, file_format="feather")

analyze the epochs

  • Is there a difference between standard and targets?

  • If so, is the effect the same for high and low tone standards and targers?

[8]:
p3_epochs = pd.read_feather(p3_epochs_f)
# sanitize Epoch_idx for pandas index
p3_epochs['epoch_id'] = p3_epochs['epoch_id'].astype('int')

# 26 cap + EOG and A2 channel labels
p3_chans = [col for col in p3_epochs.columns if re.match(r"^[MLRAHlr]\w{1,3}$", col)]
print(f"n channels: {len(p3_chans)}\nchannels:{p3_chans}")


# lookup the epochs with stim events at time == 0) and clean artifact flags (= 0)
good_epoch_ids = p3_epochs.query("match_time == 0 and log_flags == 0").epoch_id

# select just the good epochs
p3_epochs = p3_epochs.query("epoch_id in @good_epoch_ids").copy()

# check the good event counts after dropping artifacts
p3_good_events = p3_epochs.query("match_time == 0")

print("After excluding EEG artifacts")
pd.crosstab(
    [
        p3_good_events.instrument,
        p3_good_events.ccode,
        p3_good_events.stim,
        p3_good_events.accuracy,
        p3_good_events.acc_type,
        p3_good_events.tone,
        p3_good_events.bin,
        p3_good_events.log_evcodes,
    ],
    [
        p3_good_events.log_flags
    ],
    margins=True
)


n channels: 32
channels:['lle', 'lhz', 'MiPf', 'LLPf', 'RLPf', 'LMPf', 'RMPf', 'LDFr', 'RDFr', 'LLFr', 'RLFr', 'LMFr', 'RMFr', 'LMCe', 'RMCe', 'MiCe', 'MiPa', 'LDCe', 'RDCe', 'LDPa', 'RDPa', 'LMOc', 'RMOc', 'LLTe', 'RLTe', 'LLOc', 'RLOc', 'MiOc', 'A2', 'HEOG', 'rle', 'rhz']
After excluding EEG artifacts
[8]:
log_flags 0 All
instrument ccode stim accuracy acc_type tone bin log_evcodes
cal 0 cal cal cal cal 0 1 49 49
2 47 47
3 56 56
4 56 56
eeg 1 standard correct cr hi 10 20 142 142
lo 4 10 138 138
target correct hit hi 3 11 23 23
lo 9 21 31 31
All 542 542
[9]:
# for illustration ...
midline = ["MiPf", "MiCe", "MiPa", "MiOc"]

# select COLUMNS: epoch index, timestamps, event tags, and midline EEG columns
midline_epochs = p3_epochs[["epoch_id", "match_time", "stim", "tone"] + midline]

# select ROWS: use tag values to pick and choose
midline_epochs = midline_epochs.query("stim in ['standard', 'target']")

# center each channel
midline_epochs = epf.center_eeg(
    midline_epochs,
    midline,
    -750, 0,
    epoch_id="epoch_id",
    time="match_time"
)

time-domain average midline ERPs

[10]:
# compute domain average by stim type
midline_erps = midline_epochs.groupby(
    ["stim", "match_time"]
).mean().reset_index()

# plot
f, axs = plt.subplots(4, 1, figsize=(8,12), sharey=True)

for (stim), erp in midline_erps.groupby(['stim']):
    for axi, chan in enumerate(midline):

        # mark onset
        axs[axi].axvline(0, color='gray')

        axs[axi].plot(
            erp["match_time"],
            erp[chan],
            label=f"{stim}",
            lw=2,
        )

        # channel
        axs[axi].set_title(chan, loc='left')

axs[0].legend()
axs[0].set_title("Auditory oddball P300")
f.tight_layout()

_images/p3_18_0.png
[11]:
# average by stim type
midline_erps = midline_epochs.groupby(["stim", "tone", "match_time"]).mean().reset_index()

# plot
f, axs = plt.subplots(4, 1, figsize=(8,12), sharey=True)

for (stim, tone), erp in midline_erps.groupby(['stim', "tone"]):
    for axi, chan in enumerate(midline):

        # mark onset
        axs[axi].axvline(0, color='gray')

        axs[axi].plot(
            erp["match_time"],
            erp[chan],
            label=f"{stim} {tone}",
            lw=2,
        )

        # channel
        axs[axi].set_title(chan, loc='left')

axs[0].legend()
axs[0].set_title("Auditory oddball P300")
f.tight_layout()
_images/p3_19_0.png