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.