Picture memory

This builds on the P300 and WR example.

Design * study-test recognition memory * Study phase: 50 pictures were presented for 2s and judged “like” or “dislike”, indicated by a button press * Test phase: The same 50 studied items were presented with 50 distractor items in pseudo-random order and judged “old” or “new”. * The 50 studied and 50 distractor items are counterbalanced across subjects so the individual pictures are studied for half the participants and distractors for the other half.

Automated code tagging * tag individual stimulus items are * stimulus categories * response categories * local (within-trial) stimulus-response contigent tags: “old”, “new”, “hit”, “miss”, “correct rejection”, “false alarm”

Event table modification for custom code tagging * non-local (across-phase) response-contingent tags * separate codemaps and event tables are constructed for the study and test phases * DM analysis: align the study phase items with subsequent test phase “hit”, “miss” responses * Memory x preference: align the test phase items with previous study phase “like”, “dislike” responses

See Appendex at the end of the end of the notebook for source code to generate the code maps.

[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
[2]:
# set filenames
crw = MDE_HOME / "mkdig/sub000pm.crw"    # EEG recording
log = MDE_HOME / "mkdig/sub000pm.x.log"  # events
yhdr = MDE_HOME / "mkpy/sub000pm.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
pm_h5_f = DOCS_DATA / "sub000pm.h5"

mkh5 EEG data, event code log, header information

[3]:
# convert to HDF5
pm_h5 = mkh5.mkh5(pm_h5_f)
pm_h5.reset_all()
pm_h5.create_mkdata("sub000", crw, log, yhdr)

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

# calibrate
pts, pulse, lo, hi, ccode = 5, 10, -40, 40, 0
pm_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/mkpy/mkh5.py:3666: UserWarning: negative event code(s) found for cal condition code 0 -16384
  warnings.warn(msg)
Found cals in /sub000/dblock_3
Calibrating block /sub000/dblock_0 of 4: (95232,)
Calibrating block /sub000/dblock_1 of 4: (34048,)
Calibrating block /sub000/dblock_2 of 4: (139008,)
Calibrating block /sub000/dblock_3 of 4: (28416,)

codemaps: study and test phase generated programmatically from a table of item information

item information

Begin with the item-specific information, gathered somehow.

Here awk extracts a flat text file directly from the actual stimulus presentation files.

condition_id tracks animacy and item_id tracks the item.

jpg is the image file prefix for human readability and scn file tracks the version of the stimulus presentation file which counterbalance the 50 study items so of the 100 test items, across subjects, each individual picture appears equally often as previously presented or not.

[4]:
%%bash

echo "study phase head"
head ${MDE_HOME}/mkpy/pm_item_id_by_scn.tsv

echo "test phase tail"
tail ${MDE_HOME}/mkpy/pm_item_id_by_scn.tsv


study phase head
           ccode           phase    condition_id         item_id             jpg             scn
               2           study               2             154        necklace         studyp1
               2           study               2             107            bell         studyp1
               2           study               1             146         leopard         studyp1
               2           study               1             157            peas         studyp1
               2           study               2             151            moon         studyp1
               2           study               2             143          jacket         studyp1
               2           study               2             147         lighter         studyp1
               2           study               2             141        htarblon         studyp1
               2           study               2             149            lock         studyp1
test phase tail
               1            test               1             144        kilerwal          testp4
               1            test               2             187           table          testp4
               1            test               4             111        briefcas          testp4
               1            test               4             197         whistle          testp4
               1            test               4             200          wrench          testp4
               1            test               4             152        motrcyle          testp4
               1            test               2             120           chair          testp4
               1            test               3             186            swan          testp4
               1            test               4             167        refridge          testp4
               1            test               2             194          vacuum          testp4
[5]:
# read the item information table
pm_items = pd.read_csv(
    MDE_HOME / "mkpy/pm_item_id_by_scn.tsv",
    delim_whitespace=True
).query("scn in ['studyp1', 'testp1']")

display(pm_items.head())
display(pm_items.tail())
ccode phase condition_id item_id jpg scn
0 2 study 2 154 necklace studyp1
1 2 study 2 107 bell studyp1
2 2 study 1 146 leopard studyp1
3 2 study 1 157 peas studyp1
4 2 study 2 151 moon studyp1
ccode phase condition_id item_id jpg scn
295 1 test 4 198 wineglas testp1
296 1 test 4 125 crayon testp1
297 1 test 3 175 sheep testp1
298 1 test 4 184 stove testp1
299 1 test 2 105 barn testp1

study phase codemap

  • define a code pattern: tags template

  • plug in the individual items

  • save the result

Note that many-to-many mapping. Many codes (1, 2) are mapped to many tags, e.g., “like”, “dislike”

[6]:
# study phase codemape file name
pm_study_codemap_f = MDE_HOME / "mkpy/pm_study_codemap.tsv"

pm_study_codemap_cols = ["regexp", "study_bin_id", "animacy", "study_response", ] + list(pm_items.columns)

pm_study_codemap = pd.read_csv(pm_study_codemap_f, sep="\t")
display(pm_study_codemap.shape)
display(pm_study_codemap)
(702, 10)
regexp study_bin_id animacy study_response ccode phase condition_id item_id jpg scn
0 (#[1234]) 0 cal cal 0 study 0 -1 cal cal
1 (#[12]) 200 _any _any 2 study 2 -1 _any _any
2 (#[1]) 8 (154) 1040 2100 animate like 2 study 2 154 necklace studyp1
3 (#[1]) 8 1040 (154) 2101 animate like 2 study 2 154 necklace studyp1
4 (#[1]) 1040 8 (154) 2102 animate like 2 study 2 154 necklace studyp1
... ... ... ... ... ... ... ... ... ... ...
697 (#[2]) 1040 8 (159) 2202 inanimate like 2 study 2 159 pen studyp1
698 (#[2]) 8 (159) 2064 2210 inanimate dislike 2 study 2 159 pen studyp1
699 (#[2]) 8 2064 (159) 2211 inanimate dislike 2 study 2 159 pen studyp1
700 (#[2]) 2064 8 (159) 2212 inanimate dislike 2 study 2 159 pen studyp1
701 (#[2]) 8 (159) (?!(1040|2064)) 2203 inanimate no_response 2 study 2 159 pen studyp1

702 rows × 10 columns

1. study phase get_event_table(codemap)

[7]:
pm_study_event_table = pm_h5.get_event_table(pm_study_codemap_f)
searching codes in: sub000/dblock_0
/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 pm_study_codemap.tsv 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)
searching codes in: sub000/dblock_1
searching codes in: sub000/dblock_2
searching codes in: sub000/dblock_3

inspect the study phase event table

[8]:
print("study phase (shape):", pm_study_event_table.shape)
print("study phase columns:", pm_study_event_table.columns.to_list())

# select some columns to show
example_columns = [
    "dblock_path", "dblock_ticks", "log_evcodes", "log_ccodes", "log_flags",
    "regexp", "match_code",
    "phase", "study_bin_id", "study_response",
]

# first few stimulus events, coded for response
display(pm_study_event_table[example_columns].query("study_bin_id > 2000").head())

for is_anchor in [True, False]:
    print("is_anchor: ", is_anchor)
    events = pm_study_event_table.query("is_anchor == @is_anchor ")
    print(events.shape)
    display(
        pd.crosstab(
            [
                events.data_group,
                events.ccode,
                events.study_bin_id,
                events.study_response
            ],
            [
                events.log_flags
            ],
            margins=True
        )
    )
study phase (shape): (359, 33)
study phase columns: ['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', 'study_bin_id', 'animacy', 'study_response', 'ccode', 'phase', 'condition_id', 'item_id', 'jpg', 'scn']
dblock_path dblock_ticks log_evcodes log_ccodes log_flags regexp match_code phase study_bin_id study_response
100 sub000/dblock_0 834 2 2 32 (#[2]) 8 (154) 1040 2 study 2200 like
101 sub000/dblock_0 1410 154 2 0 (#[2]) 8 (154) 1040 154 study 2200 like
102 sub000/dblock_0 3390 2 2 0 (#[2]) 8 (107) 1040 2 study 2200 like
103 sub000/dblock_0 3969 107 2 0 (#[2]) 8 (107) 1040 107 study 2200 like
104 sub000/dblock_0 5532 1 2 0 (#[1]) 8 (146) 1040 1 study 2100 like
is_anchor:  True
(309, 33)
log_flags 0 32 64 All
data_group ccode study_bin_id study_response
sub000 0 0 cal 208 0 1 209
2 200 _any 48 2 0 50
2100 like 9 0 0 9
2101 like 1 0 0 1
2102 like 1 0 0 1
2103 no_response 1 0 0 1
2110 dislike 3 0 0 3
2200 like 22 2 0 24
2210 dislike 11 0 0 11
All 304 4 1 309
is_anchor:  False
(50, 33)
log_flags 0 All
data_group ccode study_bin_id study_response
sub000 2 2100 like 9 9
2101 like 1 1
2102 like 1 1
2103 no_response 1 1
2110 dislike 3 3
2200 like 24 24
2210 dislike 11 11
All 50 50

test phase codemap

  • define the template

  • plug in the individual items

  • save the result

[9]:
# test phase codemap name
pm_test_codemap_f = MDE_HOME / "mkpy/pm_test_codemap.tsv"

# test phase codemap column names
pm_test_codemap_cols = [
    "regexp", "test_bin_id", "animacy", "stimulus", "test_response", "accuracy"
] + list(pm_items.columns)

pm_test_codemap = pd.read_csv(pm_test_codemap_f, sep="\t")
display(pm_test_codemap.shape)
display(pm_test_codemap)
(706, 12)
regexp test_bin_id animacy stimulus test_response accuracy ccode phase condition_id item_id jpg scn
0 (#[1234]) 0 cal cal cal cal 0 test cal -1 cal cal
1 (#[1234]) 10 _any _any _any _any 1 test _any -1 _any _any
2 (#[1]) 11 animate distractor _any _any 1 test 1 -1 _any _any
3 (#[2]) 12 inanimate distractor _any _any 1 test 2 -1 _any _any
4 (#[3]) 13 animate studied _any _any 1 test 3 -1 _any _any
... ... ... ... ... ... ... ... ... ... ... ... ...
701 (#2) 2064 8 (105) 1202 inanimate distractor new cr 1 test 2 105 barn testp1
702 (#2) 8 (105) 1040 1210 inanimate distractor old fa 1 test 2 105 barn testp1
703 (#2) 8 1040 (105) 1211 inanimate distractor old fa 1 test 2 105 barn testp1
704 (#2) 1040 8 (105) 1212 inanimate distractor old fa 1 test 2 105 barn testp1
705 (#2) 8 (105) (?!(2064|1040)) 1203 inanimate distractor none nr 1 test 2 105 barn testp1

706 rows × 12 columns

test phase get_event_table(codemap)

[10]:
pm_test_event_table = pm_h5.get_event_table(pm_test_codemap_f)
searching codes in: sub000/dblock_0
/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 pm_test_codemap.tsv 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)
searching codes in: sub000/dblock_1
searching codes in: sub000/dblock_2
searching codes in: sub000/dblock_3

inspect test phase event table

[11]:
print("test phase (shape):", pm_test_event_table.shape)
print("test phase columns:", pm_test_event_table.columns)

# select some columns to show
example_columns = [
    "dblock_path", "dblock_ticks", "log_evcodes", "log_ccodes", "log_flags",
    "regexp", "match_code",
    "phase", "test_bin_id", "test_response", "accuracy",
]


# first few stimulus events, coded for response
display(pm_test_event_table[example_columns].query("test_bin_id > 1000").head())

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

for is_anchor in [True, False]:
    print("is_anchor: ", is_anchor)
    events = pm_test_event_table.query("is_anchor == @is_anchor ")

    display(
        pd.crosstab(
            [
                events.data_group,
                # events.ccode,
                events.test_bin_id,
                events.animacy,
                events.stimulus,
                events.test_response
            ],
            [
                events.accuracy
            ],
            margins=True
        )
    )
test phase (shape): (609, 35)
test phase columns: 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',
       'test_bin_id', 'animacy', 'stimulus', 'test_response', 'accuracy',
       'ccode', 'phase', 'condition_id', 'item_id', 'jpg', 'scn'],
      dtype='object')
dblock_path dblock_ticks log_evcodes log_ccodes log_flags regexp match_code phase test_bin_id test_response accuracy
204 sub000/dblock_1 611 1 1 0 (#1) 2064 8 (127) 1 test 1102 new cr
205 sub000/dblock_1 1061 127 1 0 (#1) 2064 8 (127) 127 test 1102 new cr
206 sub000/dblock_1 2062 4 1 0 (#4) 8 (129) 1040 4 test 1400 old hit
207 sub000/dblock_1 2516 129 1 0 (#4) 8 (129) 1040 129 test 1400 old hit
208 sub000/dblock_1 3767 2 1 0 (#2) 8 (185) 2064 2 test 1200 new cr
dblock_path dblock_ticks log_evcodes log_ccodes log_flags regexp match_code phase test_bin_id test_response accuracy
854 sub000/dblock_3 27315 4 0 0 (#[1234]) 4 test 0 cal cal
855 sub000/dblock_3 27444 2 0 0 (#[1234]) 2 test 0 cal cal
856 sub000/dblock_3 27573 3 0 0 (#[1234]) 3 test 0 cal cal
857 sub000/dblock_3 27703 4 0 0 (#[1234]) 4 test 0 cal cal
858 sub000/dblock_3 27832 2 0 0 (#[1234]) 2 test 0 cal cal
is_anchor:  True
accuracy _any cal cr fa hit All
data_group test_bin_id animacy stimulus test_response
sub000 0 cal cal cal 0 209 0 0 0 209
10 _any _any _any 100 0 0 0 0 100
11 animate distractor _any 15 0 0 0 0 15
12 inanimate distractor _any 35 0 0 0 0 35
13 animate studied _any 15 0 0 0 0 15
14 inanimate studied _any 35 0 0 0 0 35
1100 animate distractor new 0 0 14 0 0 14
1102 animate distractor new 0 0 1 0 0 1
1200 inanimate distractor new 0 0 32 0 0 32
1201 inanimate distractor new 0 0 1 0 0 1
1202 inanimate distractor new 0 0 1 0 0 1
1210 inanimate distractor old 0 0 0 1 0 1
1300 animate studied old 0 0 0 0 15 15
1400 inanimate studied old 0 0 0 0 35 35
All 200 209 49 1 50 509
is_anchor:  False
accuracy cr fa hit All
data_group test_bin_id animacy stimulus test_response
sub000 1100 animate distractor new 14 0 0 14
1102 animate distractor new 1 0 0 1
1200 inanimate distractor new 32 0 0 32
1201 inanimate distractor new 1 0 0 1
1202 inanimate distractor new 1 0 0 1
1210 inanimate distractor old 0 1 0 1
1300 animate studied old 0 0 15 15
1400 inanimate studied old 0 0 35 35
All 49 1 50 100

Prune study phase event table for epochs and tag with test phase responses

The study events were double counted as stim only and again as stim + response in order to verify code mapping

Now drop redundant stim-only events

[12]:
# ----------------------------------------------
# prune event tables to unique single trials
# ----------------------------------------------

# study phase response tagged single trials are coded with study table bin id > 2000
pm_study_events_for_epochs = pm_study_event_table.query(
    "is_anchor==True and study_bin_id >= 2000"
).copy().set_index("item_id").sort_index()

# test phase response-tagged single trials are coded with test table bin id > 1000
pm_test_events_for_epochs = pm_test_event_table.query(
    "is_anchor==True and test_bin_id >= 1000"
).copy().set_index("item_id").sort_index()
[13]:
# --------------------------------------------------------
# update study phase events with test phase responses
# --------------------------------------------------------

# just for summary display ...
display_cols = ["match_code", "anchor_code", "log_evcodes", "phase", "study_response"]

# test response tags
test_s_r_cols = ["test_response", "accuracy"]

print("Study phase items before ...")
display(pm_study_events_for_epochs.shape)
display(pm_study_events_for_epochs[display_cols].head())

# align the test phase subsequent responses with the study phase items
pm_study_events_for_epochs = (
    pm_study_events_for_epochs
    .join(
        pm_test_events_for_epochs[test_s_r_cols],
        how="left",
        on="item_id",
    )
)

print("Study phase items after joining test phase responses ...")
display(pm_study_events_for_epochs.shape)
display(pm_study_events_for_epochs[display_cols + test_s_r_cols].head())
Study phase items before ...
(50, 32)
match_code anchor_code log_evcodes phase study_response
item_id
101 1 1 1 study dislike
103 2 2 2 study dislike
106 1 1 1 study like
107 2 2 2 study like
112 2 2 2 study dislike
Study phase items after joining test phase responses ...
(50, 34)
match_code anchor_code log_evcodes phase study_response test_response accuracy
item_id
101 1 1 1 study dislike old hit
103 2 2 2 study dislike old hit
106 1 1 1 study like old hit
107 2 2 2 study like old hit
112 2 2 2 study dislike old hit
[14]:
# --------------------------------------------------------
# update test phase event table with study phase responses
# ---------------------------------------------------------

# just for summary display ...
display_cols = ["match_code", "anchor_code", "log_evcodes", "phase"]


# study response tags to map to the other phase
study_s_r_cols = ["study_response"]


print("Test phase items before:", pm_test_events_for_epochs.shape)
display(pm_test_events_for_epochs[display_cols].head(12))

# align the study phase like/dislike responses with the test phase items
pm_test_events_for_epochs = (
    pm_test_events_for_epochs
    .join(
        pm_study_events_for_epochs["study_response"],
        how="left",
        on="item_id",
    )
)

print("Test phase items after", pm_test_events_for_epochs.shape)
display(pm_test_events_for_epochs[display_cols + study_s_r_cols].head(12))
Test phase items before: (100, 34)
match_code anchor_code log_evcodes phase
item_id
101 3 3 3 test
102 1 1 1 test
103 4 4 4 test
104 2 2 2 test
105 2 2 2 test
106 3 3 3 test
107 4 4 4 test
108 1 1 1 test
109 2 2 2 test
110 1 1 1 test
111 2 2 2 test
112 4 4 4 test
Test phase items after (100, 35)
match_code anchor_code log_evcodes phase study_response
item_id
101 3 3 3 test dislike
102 1 1 1 test NaN
103 4 4 4 test dislike
104 2 2 2 test NaN
105 2 2 2 test NaN
106 3 3 3 test like
107 4 4 4 test like
108 1 1 1 test NaN
109 2 2 2 test NaN
110 1 1 1 test NaN
111 2 2 2 test NaN
112 4 4 4 test dislike

study phase set_epochs(name, pre, post)

[15]:
pm_h5.set_epochs("study_ms1500", pm_study_events_for_epochs, -750, 750)
Sanitizing event table data types for mkh5 epochs table ...

study phase export_epochs(name)

[16]:
pm_study_epochs_f = DOCS_DATA / "sub000pm.study_ms1500.epochs.feather"
pm_h5.export_epochs("study_ms1500", pm_study_epochs_f, file_format="feather")

test phase set_epochs(name, pre, post)

[17]:
pm_h5.set_epochs("test_ms1500", pm_test_events_for_epochs, -750, 750)
Sanitizing event table data types for mkh5 epochs table ...

test phase export_epochs(name)

[18]:
pm_test_epochs_f = DOCS_DATA / "sub000pm.test_ms1500.epochs.feather"
pm_h5.export_epochs("test_ms1500", pm_test_epochs_f, file_format="feather")

Analyze the epochs

  • Is there a difference between like and dislike at study?

  • Is there a difference between old and new at test?

  • If so is it the same for liked and disliked?

[19]:
# matplotlib line colors, background, fonts
plt.style.use("bmh")

Study phase time-domain average ERPs

[20]:
# load and sanitize epoch_id for pandas index
pm_study_epochs = pd.read_feather(pm_study_epochs_f)
pm_study_epochs['epoch_id'] = pm_study_epochs['epoch_id'].astype('int')

# exclude epochs flagged for artifacts
pm_study_epochs = epf.drop_bad_epochs(
    pm_study_epochs,
    bads_column="log_flags",
    epoch_id="epoch_id",
    time="match_time"
)


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

print("After excluding EEG artifacts")
display(
    pd.crosstab(
        [
            pm_study_good_events.ccode,
            pm_study_good_events.animacy,
            pm_study_good_events.study_response,
        ],
        [
            pm_study_good_events.log_flags
        ],
        margins=True
    )
)

# for illustration ...
midline = ["MiPf", "MiCe", "MiPa", "MiOc"]

# select COLUMNS: epoch index, timestamps, event tags, and midline EEG columns
midline_epochs = pm_study_epochs[["epoch_id", "match_time", "animacy", "study_response"] + midline]

# select ROWS: use tag values to pick and choose
midline_epochs = midline_epochs.query("study_response in ['like', 'dislike']")

# center each channel
midline_epochs = epf.center_eeg(
    midline_epochs,
    midline,
    -750, 0,
    epoch_id="epoch_id",
    time="match_time"
)
After excluding EEG artifacts
log_flags 0 All
ccode animacy study_response
2 animate dislike 3 3
like 11 11
no_response 1 1
inanimate dislike 11 11
like 22 22
All 48 48
[21]:
for grp in [["animacy"], ["study_response"], ["animacy", "study_response"]]:

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

    # plot
    f, axs = plt.subplots(1, 4, figsize=(14,8), sharex=True)
    for rep, erp in midline_erps.groupby(grp):
        for axi, chan in enumerate(midline):

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

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

            # channel
            axs[axi].set(xlim=(-15, 20), xlabel=chan)

    axs[0].legend(loc="lower right")
    axs[0].set_title(f"Study phase:\n{':'.join(grp)}", fontsize=12, loc="left")
    f.tight_layout()

_images/pm_37_0.png
_images/pm_37_1.png
_images/pm_37_2.png

test phase time-domain average ERPs

[22]:
# load and sanitize epoch_id for pandas index
pm_test_epochs = pd.read_feather(pm_test_epochs_f)
pm_test_epochs['epoch_id'] = pm_test_epochs['epoch_id'].astype('int')
print(pm_test_epochs.columns)

# drop EEG epochs tagged as bad
pm_test_epochs = epf.drop_bad_epochs(
    pm_test_epochs,
    bads_column="log_flags",
    epoch_id="epoch_id",
    time="match_time"
)

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

print("After excluding EEG artifacts")
display(
    pd.crosstab(
        [
            pm_test_good_events.ccode,
            pm_test_good_events.animacy,
            pm_test_good_events.test_response,
        ],
        [
            pm_test_good_events.log_flags
        ],
        margins=True
    )
)
Index(['epoch_id', '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', 'test_bin_id', 'animacy', 'stimulus',
       'test_response', 'accuracy', 'ccode', 'phase', 'condition_id', 'jpg',
       'scn', 'study_response', 'match_time', 'anchor_time',
       'anchor_time_delta', 'diti_t_0', 'diti_hop', 'diti_len', 'pygarv',
       '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'],
      dtype='object')
After excluding EEG artifacts
log_flags 0 All
ccode animacy test_response
1 animate new 14 14
old 13 13
inanimate new 31 31
old 35 35
All 93 93
[23]:
# for illustration ...
midline = ["MiPf", "MiCe", "MiPa", "MiOc"]

# select COLUMNS: epoch index, timestamps, event tags, and midline EEG columns
midline_epochs = pm_test_epochs[["epoch_id", "match_time", "animacy", "test_response"] + midline]

# select ROWS: use tag values to pick and choose
midline_epochs = midline_epochs.query("test_response in ['old', 'new']")

# center each channel
midline_epochs = epf.center_eeg(
    midline_epochs,
    midline,
    -750, 0,
    epoch_id="epoch_id",
    time="match_time"
)
[24]:
# plot separately and together
for grp in [["animacy"], ["test_response"], ["animacy", "test_response"]]:

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

    # plot
    f, axs = plt.subplots(1, 4, figsize=(14,8))

    for (rep), erp in midline_erps.groupby(grp):
        for axi, chan in enumerate(midline):

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

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

            axs[axi].set(xlim=(-10, 10), xlabel=chan)


    axs[0].legend(loc="lower right")
    axs[0].set_title(f"test phase:\n{':'.join(grp)}", fontsize=12)
    f.tight_layout()

_images/pm_41_0.png
_images/pm_41_1.png
_images/pm_41_2.png

Appendix

Python code to generate the study and test code maps from the stimulus files

[25]:
%%bash

cat ${MDE_HOME}/scripts/make_pm_codemaps.py
"""demonstrate programmatic codemap generation"""

import re
import pandas as pd
from make_zenodo_files import MDE_HOME

# stim info scraped from .scn scenario files
PM_ITEM_ID_BY_SCN_F = MDE_HOME / "mkpy/pm_item_id_by_scn.tsv"

# study phase codemap
PM_STUDY_CODEMAP_F = MDE_HOME / "mkpy/pm_study_codemap.tsv"

# test phase codemap
PM_TEST_CODEMAP_F = MDE_HOME / "mkpy/pm_test_codemap.tsv"


def make_pm_item_id_by_scn():
    """scrape stimuli out of the presentation files and extract code information"""

    stim_split_re = re.compile(
        r"(?P<soa>\d+)\s+(?P<dur>\d+)\s+(?P<evcode>\d+)\s+"
        r"(?P<type>....)=(?P<stim>.+)[\.]"
    )
    scn_paths = sorted(MDE_HOME.glob("mkstim/pictmem/*.scn"))

    header_str = "".join(
        [
            f"{col:>16}"
            for col in ["ccode", "phase", "condition_id", "item_id", "jpg", "scn"]
        ]
    )
    items = [header_str]
    for scn_path in scn_paths:
        with open(scn_path, "r") as _fh:
            scn = scn_path.stem
            phase = None
            ccode = None
            for phase, ccode in [("study", 2), ("test", 1)]:
                if phase in str(scn_path):
                    break
            assert phase is not None and ccode is not None
            for line in _fh.readlines():
                fields = stim_split_re.match(line)
                if fields is None:
                    continue
                evcode = int(fields["evcode"])

                if evcode > 0 and evcode <= 4:
                    assert fields["type"] == "jpeg"
                    stim = fields["stim"]
                    cond = evcode

                if evcode > 100:
                    item = evcode
                    items.append(
                        "".join(
                            [
                                f"{val:>16}"
                                for val in [ccode, phase, cond, item, stim, scn]
                            ]
                        )
                    )

    with open(PM_ITEM_ID_BY_SCN_F, "w") as _fh:
        _fh.write("\n".join(items))
        _fh.write("\n")


def make_pm_study_phase_codemap():
    """ Study phase """

    # read the item information table
    pm_items = pd.read_csv(PM_ITEM_ID_BY_SCN_F, delim_whitespace=True).query(
        "scn in ['studyp1', 'testp1']"
    )

    # code map
    pm_study_codemap_cols = [
        "regexp",
        "study_bin_id",
        "animacy",
        "study_response",
    ] + list(pm_items.columns)

    # stimulus-response tag template as a Python dictionary
    # The key:val pair says "this code sequence gets these tags"
    # The ITEM_ID string will be replaced by the actual 3-digit item number

    study_code_tags = {
        "(#[12]) 8 (ITEM_ID) 1040": (2000, "like"),
        "(#[12]) 8 1040 (ITEM_ID)": (2001, "like"),
        "(#[12]) 1040 8 (ITEM_ID)": (2002, "like"),
        "(#[12]) 8 (ITEM_ID) 2064": (2100, "dislike"),
        "(#[12]) 8 2064 (ITEM_ID)": (2101, "dislike"),
        "(#[12]) 2064 8 (ITEM_ID)": (2102, "dislike"),
        "(#[12]) 8 (ITEM_ID) (?!(1040|2064))": (2003, "no_response"),
    }

    # the new 4-digit "study_bin_id" tag re-codes the match event 1 or
    # 2 with more information
    #
    #  phase animacy response response_timing
    #    phase: study=2
    #    animacy: 1=animate, 2=inanimate
    #    response(0=like, 1=dislike)
    #    response timing: 0=prompted,1,2 anticipation, 3=no response)
    #
    study_code_tags = {
        # animate
        "(#[1]) 8 (ITEM_ID) 1040": (2100, "animate", "like"),
        "(#[1]) 8 1040 (ITEM_ID)": (2101, "animate", "like"),
        "(#[1]) 1040 8 (ITEM_ID)": (2102, "animate", "like"),
        "(#[1]) 8 (ITEM_ID) 2064": (2110, "animate", "dislike"),
        "(#[1]) 8 2064 (ITEM_ID)": (2111, "animate", "dislike"),
        "(#[1]) 2064 8 (ITEM_ID)": (2112, "animate", "dislike"),
        "(#[1]) 8 (ITEM_ID) (?!(1040|2064))": (2103, "animate", "no_response"),
        # inanimate
        "(#[2]) 8 (ITEM_ID) 1040": (2200, "inanimate", "like"),
        "(#[2]) 8 1040 (ITEM_ID)": (2201, "inanimate", "like"),
        "(#[2]) 1040 8 (ITEM_ID)": (2202, "inanimate", "like"),
        "(#[2]) 8 (ITEM_ID) 2064": (2210, "inanimate", "dislike"),
        "(#[2]) 8 2064 (ITEM_ID)": (2211, "inanimate", "dislike"),
        "(#[2]) 2064 8 (ITEM_ID)": (2212, "inanimate", "dislike"),
        "(#[2]) 8 (ITEM_ID) (?!(1040|2064))": (2203, "inanimate", "no_response"),
    }

    #
    # Build a list of codemap lines.

    # The first line says *any* code matching 1 or 2 gets the tags 200, "_any", 2, ... etc.
    # This tags all matching stimulus events, it is not contingent the response.
    # It is not necessary but it is useful here, we will see why shortly.
    study_code_map = [
        ("(#[1234])", 0, "cal", "cal", 0, "study", 0, -1, "cal", "cal"),
        ("(#[12])", 200, "_any", "_any", 2, "study", 2, -1, "_any", "_any"),
    ]

    # plug each row of the pictmem item info into the template and append the
    # result to the list of codemap lines
    for idx, row in pm_items.query("phase == 'study'").iterrows():
        for pattern, tags in study_code_tags.items():
            code_tags = (
                pattern.replace(
                    "ITEM_ID", str(row.item_id)
                ),  # current item number goes in the template
                *(str(t) for t in tags),
                *(str(c) for c in row),  # this adds the rest of the item to the tags
            )
            study_code_map.append(code_tags)

    # convert the list of lines to a pandas.DataFrame and save as a tab separated text file
    pm_study_codemap = pd.DataFrame(study_code_map, columns=pm_study_codemap_cols)
    pm_study_codemap.to_csv(PM_STUDY_CODEMAP_F, sep="\t", index=False)

    print(pm_study_codemap.shape)
    print(pm_study_codemap)


def make_pm_test_phase_codemap():

    # read the item information table
    pm_items = pd.read_csv(PM_ITEM_ID_BY_SCN_F, delim_whitespace=True).query(
        "scn in ['studyp1', 'testp1']"
    )

    # test phase codemap column names
    pm_test_codemap_cols = [
        "regexp",
        "test_bin_id",
        "animacy",
        "stimulus",
        "test_response",
        "accuracy",
    ] + list(pm_items.columns)

    # test phase template: stimulus, old/new response (include pre-prompt anticipations)
    test_code_tags = {
        # new stim animate
        "(#1) 8 (ITEM_ID) 2064": (1100, "animate", "distractor", "new", "cr"),
        "(#1) 8 2064 (ITEM_ID)": (1101, "animate", "distractor", "new", "cr"),
        "(#1) 2064 8 (ITEM_ID)": (1102, "animate", "distractor", "new", "cr"),
        "(#1) 8 (ITEM_ID) 1040": (1110, "animate", "distractor", "old", "fa"),
        "(#1) 8 1040 (ITEM_ID)": (1111, "animate", "distractor", "old", "fa"),
        "(#1) 1040 8 (ITEM_ID)": (1112, "animate", "distractor", "old", "fa"),
        "(#1) 8 (ITEM_ID) (?!(2064|1040))": (
            1103,
            "animate",
            "distractor",
            "none",
            "nr",
        ),
        # new stim inanimate
        "(#2) 8 (ITEM_ID) 2064": (1200, "inanimate", "distractor", "new", "cr"),
        "(#2) 8 2064 (ITEM_ID)": (1201, "inanimate", "distractor", "new", "cr"),
        "(#2) 2064 8 (ITEM_ID)": (1202, "inanimate", "distractor", "new", "cr"),
        "(#2) 8 (ITEM_ID) 1040": (1210, "inanimate", "distractor", "old", "fa"),
        "(#2) 8 1040 (ITEM_ID)": (1211, "inanimate", "distractor", "old", "fa"),
        "(#2) 1040 8 (ITEM_ID)": (1212, "inanimate", "distractor", "old", "fa"),
        "(#2) 8 (ITEM_ID) (?!(2064|1040))": (
            1203,
            "inanimate",
            "distractor",
            "none",
            "nr",
        ),
        # old stim animate
        "(#3) 8 (ITEM_ID) 1040": (1300, "animate", "studied", "old", "hit"),
        "(#3) 8 1040 (ITEM_ID)": (1301, "animate", "studied", "old", "hit"),
        "(#3) 1040 8 (ITEM_ID)": (1302, "animate", "studied", "old", "hit"),
        "(#3) 8 (ITEM_ID) 2064": (1310, "animate", "studied", "new", "miss"),
        "(#3) 8 2064 (ITEM_ID)": (1311, "animate", "studied", "new", "miss"),
        "(#3) 2064 8 (ITEM_ID)": (1312, "animate", "studied", "new", "miss"),
        "(#3) 8 (ITEM_ID) (?!(2064|1040))": (1303, "animate", "studied", "none", "nr"),
        # old stim inanimate
        "(#4) 8 (ITEM_ID) 1040": (1400, "inanimate", "studied", "old", "hit"),
        "(#4) 8 1040 (ITEM_ID)": (1401, "inanimate", "studied", "old", "hit"),
        "(#4) 1040 8 (ITEM_ID)": (1402, "inanimate", "studied", "old", "hit"),
        "(#4) 8 (ITEM_ID) 2064": (1410, "inanimate", "studied", "new", "miss"),
        "(#4) 8 2064 (ITEM_ID)": (1411, "inanimate", "studied", "new", "miss"),
        "(#4) 2064 8 (ITEM_ID)": (1412, "inanimate", "studied", "new", "miss"),
        "(#4) 8 (ITEM_ID) (?!(2064|1040))": (
            1403,
            "inanimate",
            "studied",
            "none",
            "nr",
        ),
    }

    # initialize the code map to tag stimulus codes, not response contingent
    test_code_map = [
        (
            "(#[1234])",
            0,
            "cal",
            "cal",
            "cal",
            "cal",
            0,
            "test",
            "cal",
            "-1",
            "cal",
            "cal",
        ),
        (
            "(#[1234])",
            10,
            "_any",
            "_any",
            "_any",
            "_any",
            1,
            "test",
            "_any",
            "-1",
            "_any",
            "_any",
        ),
        (
            "(#[1])",
            11,
            "animate",
            "distractor",
            "_any",
            "_any",
            1,
            "test",
            1,
            "-1",
            "_any",
            "_any",
        ),
        (
            "(#[2])",
            12,
            "inanimate",
            "distractor",
            "_any",
            "_any",
            1,
            "test",
            2,
            "-1",
            "_any",
            "_any",
        ),
        (
            "(#[3])",
            13,
            "animate",
            "studied",
            "_any",
            "_any",
            1,
            "test",
            3,
            "-1",
            "_any",
            "_any",
        ),
        (
            "(#[4])",
            14,
            "inanimate",
            "studied",
            "_any",
            "_any",
            1,
            "test",
            4,
            "-1",
            "_any",
            "_any",
        ),
    ]

    # iterate through the item info and plug the item number into the template lines
    for idx, row in pm_items.query("phase == 'test'").iterrows():
        for pattern, tags in test_code_tags.items():
            # condition_id is 1, 2, 3, or 4 only plug into the relevant template lines.
            if re.match(r"^\(#" + str(row.condition_id), pattern):
                code_tags = (
                    pattern.replace("ITEM_ID", str(row.item_id)),
                    tags[0],
                    *(str(t) for t in tags[1:]),
                    *(str(c) for c in row),
                )
                test_code_map.append(code_tags)

    pm_test_codemap = pd.DataFrame(test_code_map, columns=pm_test_codemap_cols)

    # write test demo phase codemap
    pm_test_codemap.to_csv(PM_TEST_CODEMAP_F, sep="\t", index=False)

    print(pm_test_codemap.shape)
    print(pm_test_codemap)


if __name__ == "__main__":
    make_pm_item_id_by_scn()
    make_pm_study_phase_codemap()
    make_pm_test_phase_codemap()