Define epochs according to event order

Dear group members,

I have the following event dictionary:

event_dict = {'False': 1, 'Hit': 2, 'MP1': 3, 'MP2': 4, 'MP3': 5, 
              'M_False': 6, 'M_Hit': 7, 'OGT': 8, 'PMC': 9, 'S  4': 10, 
              'actiCAP Data On': 11, 'boundary': 12}

and I want to create epochs that contain three events (‘Hit’: 2, '‘M_Hit’: 7, ‘OGT’: 8). All three events should be contained in the same Epoch.

So I first epoch my data passing the entire event_dict:

epochs = mne.Epochs(raw_bandpass_ica, events, event_id=event_dict, 
                    tmin=-0.2, tmax=6, reject=reject_criteria, preload=True)

And then, in order to create my desired epoch, I tried this:

OGT = mne.pick_events(events, include=[8, 2, 7])
epochs['OGT'].plot(events=OGT, event_id=event_dict, 
                   event_color=dict(OGT='red', Hit='blue', M_Hit='green'))

However, the output I got was Epochs containing some of the three events and not all at the same time.

Do you know how could I define Epochs containing concomitant events?

I would appreciate any help

Best regards,

Bruno

Hello, this can be achieved using Epochs metadata. I am currently working on some code that will allow you to do just what you want. I can share it here sometime later today.

Hi Richard. That would be great! Thanks for the message.

So here is a function that can help you achieve what you want:

from typing import Dict, Tuple

import numpy as np
import pandas as pd


def gen_epochs_metadata(
    *,
    events: np.ndarray,
    event_id: Dict[str, int],
    tmin: float,
    tmax: float,
    sfreq: float
) -> Tuple[pd.DataFrame, np.ndarray]:
    """Generate a default set of Epochs.metadata, and subset the events array.

    Parameters
    ----------
    events : array, shape (m, 3)
        The events array.
    event_id : dict
        The mapping from event names (keys) to event IDs (values). This
        function assumes that only event IDs listed here will be used to
        create `~mne.Epochs`, that is, the number of rows of the generated
        metadata will equal the number of events with the specified IDs.
    tmin : float
        Start of the epoch in seconds, relative to event time.
    tmax : float
        End of the epoch in seconds, relative to event time.
    sfreq : float
        The sampling frequency during data acquisiton.

    Returns
    -------
    metadata : pd.DataFrame
        The pre-assambled metadata.
    events : array, shape (n, 3 )
        The events corresponding to the generated metadata, i.e. one event per
        row.
    """
    # First and last sample of each epoch, relative to event onset
    start_sample = int(round(tmin * sfreq))
    stop_sample = int(round(tmax * sfreq)) + 1

    # Make indexing easier
    events_df = pd.DataFrame(events, columns=('sample', 'prev_id', 'id'))
    id_to_name_map = dict(zip(event_id.values(), event_id.keys()))

    unlabeled_events = sorted(set(events_df['id']) - set(event_id.values()))
    columns = ['event_name',
               *event_id.keys(),
               *[f'event_{e}' for e in unlabeled_events],
               *[f'{e}_time' for e in event_id.keys()],
               *[f'event_{e}_time' for e in unlabeled_events]]

    # The events that will be used to create Epochs
    target_events = events_df.loc[events_df['id'].isin(event_id.values()), :]

    # Prepare & condition the metadata DataFrame
    data = np.empty((len(target_events),
                    len(columns)))
    metadata = pd.DataFrame(data=data, columns=columns, index=target_events.index)
    metadata[metadata.columns[0]] = ''

    for col in metadata.columns[1:-(len(event_id) + len(unlabeled_events))]:
        metadata[col] = False

    for col in metadata.columns[-(len(event_id) + len(unlabeled_events)):]:
        metadata[col] = np.nan

    # We're all set, let's iterate over ALL events; whenever we encounter a
    # "target event", we fill the respective cells in the metadata.
    for idx, event in events_df.iterrows():
        if event['id'] not in event_id.values():
            continue

        metadata.loc[idx, 'event_name'] = id_to_name_map[event['id']]

        # Determine which events fall into the current epoch
        epoch_start_sample = event['sample'] + start_sample
        epoch_stop_sample = event['sample'] + stop_sample
        events_in_epoch = events_df.loc[
            (events_df['sample'] >= epoch_start_sample) &
            (events_df['sample'] <= epoch_stop_sample), :]

        assert not events_in_epoch.empty

        target_event_sample = event['sample']

        for _, epochs_event in events_in_epoch.iterrows():
            if epochs_event['id'] in id_to_name_map:
                event_col_name = id_to_name_map[epochs_event['id']]
            else:
                event_col_name = f'event_{epochs_event["id"]}'

            event_time_col_name = f'{event_col_name}_time'

            event_sample = epochs_event['sample'] - target_event_sample
            event_time = event_sample / sfreq
            event_time = 0 if np.isclose(event_time, 0) else event_time

            metadata.loc[idx, event_col_name] = True
            metadata.loc[idx, event_time_col_name] = event_time

    events = events[np.in1d(events[:, 2], list(event_id.values()))]

    return metadata, events

It generates metadata and a subset of the events array you can use to create Epochs. Here’s an example on how to use it:

import os
import mne

sample_data_folder = mne.datasets.sample.data_path()
sample_data_raw_file = os.path.join(sample_data_folder, 'MEG', 'sample',
                                    'sample_audvis_raw.fif')
raw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False).crop(tmax=120)
tmax = 1
tmin = -0.5
sfreq = raw.info['sfreq']

all_events = mne.find_events(raw, stim_channel='STI 014')
event_id = {'auditory/left': 1, 'auditory/right': 2, 'visual/left': 3,
            'visual/right': 4, 'face': 5, 'button': 32}

metadata, events = gen_epochs_metadata(events=all_events, event_id=event_id,
                                       tmin=tmin, tmax=tmax, sfreq=sfreq)

epochs = mne.Epochs(raw, events, tmin=tmin, tmax=tmax, event_id=event_id,
                    preload=True, metadata=metadata)

Let’s inspect the metadata:

epochs.metadata

As you can see, there’s one row per epoch. The first column, event_name, specifies the name of the event that was used to create this respective epoch. The other columns contain boolean values (i.e., True and False), indicating whether the event from the column title occurred in that very epoch. In this example, in the first epoch, we had both an auditory/right and a visual/left event.

Farther to the right there are columns listing the time of the events in seconds relative to the event that was used to create the epoch (i.e., the one from the event_name column).

You can now use this metadata to query and select subsets of your epochs. For example, to select all epochs that contained both, the auditory/left and the visual/left events, you would do:

epochs['`auditory/left` and `visual/left`']

Output:

<Epochs |  40 events (all good), -0.499488 - 1.00064 sec, baseline [-0.499488, 0] sec, ~106.8 MB, data loaded, with metadata,
 'button': 1
 'visual/left': 39>

See the MNE docs for more info on how to work with metadata.

Let’s run the same query directly on the metadata object:

epochs.metadata.query('`auditory/left` and `visual/left`')

As you can see, the selection indeed only contains epochs where auditory/left and visual/left triggers were recorded.

Note that in the example above, I had to enclose the column names in the metadata query in backticks so the query operation would be able to deal with the forward slashes in the column names.

3 Likes

Wow, thanks a lot. I’m sure you’ve put a lot of effort in answering this question! It was really helpful!

2 Likes