Heartbeat evoked potentials in MNE

Hello all,

I’m working on a project where participants were alternating between resting state and answering probes about the contents of their thoughts during the resting state—we have recording from EEG and ECG channels. I’d like to analyze the heartbeat evoked potential (HEP) during the period before onset of a thought probe to test whether HEPs are different for different thought patterns.

I’m new to MNE and EEG analysis generally, and I wonder whether there’s an easy way to go about this analysis with the existing MNE API. This analysis would seem to require having nested epochs—I would first epoch ~5.5 s prior to thought probe onset, and then, within that epoch, create epochs extending some number of seconds after each heartbeat. The ideal result would be having epochs around each heartbeat be tagged according to their associated thought probe, as I’ll need to average over hearbeat epochs within each pre-thought-probe period, then analyze my probe data to determine how to average across probes in the experiment (ie, based on the self-reported thought patterns).

I was able to use methods from MNE to create epochs for probe events and heartbeat events separately, but I’m stumped about how to combine. I’ve approached the problem in two ways:

  • I took each probe epoch, convert back to a mne.Raw, then run mne.preprocessing.create_ecg_epochs (as this function will only accept an mne.Raw object). The problem with this approach is that the zero-locked probe-relative timing in the probe epoched data seems to be lost when converting the epoch to raw (the first sample starts at time 0).
  • Since I can compute events separately for probe onsets and heartbeats, I could loop through the heartbeat onsets and assess whether they fall within the pre-probe window, then update the event label, then try to construct epochs based on these new labels. I haven’t tried to implement this approach yet, but my sense is it would require nested loops and be inefficient, and I figured I’d reach out here before trying this approach.

Any insight you could offer about doing such an HEP analysis in MNE would be greatly appreciated!

I think approach 2 ended up working with decent efficiency (using some Numpy). In case others are interested:

# Get experiment events
events, event_id = mne.events_from_annotations(raw)
# Extract only relevant events
q1_samples = events[events[:, 2] == 6][:, 0]

# Find ecg events
ecg_events, _, _ = find_ecg_events(raw)
ecg_samples = ecg_events[:, 0]

# Custom bins around experiment events
q1_bins = [(x - (5.5*raw.info['sfreq']), x) for x in q1_samples]

bin_indices = np.empty_like(ecg_samples, dtype=object)

# Record which bin each ecg event falls into
for i, (start, end) in enumerate(q1_bins):
    indices = (ecg_samples >= start) & (ecg_samples < end)
    bin_indices[indices] = i

# Build new events and event_id
mask = [x is not None for x in bin_indices]
ecg_samples_relevant = ecg_samples[mask]
ecg_q1_indices = bin_indices[mask] + 1
values = set(ecg_q1_indices)
keys = ['Probe' + str(x) for x in values]
event_id = {k: v for k, v in zip(keys, values)}
events = np.column_stack([ecg_samples_relevant, np.zeros_like(ecg_q1_indices),

# Epoch and save
baseline = (-.1, 0)
tmin = -0.1
tmax = 1
epochs = mne.Epochs(raw,
                    events = events,
                    event_id = event_id,
                    tmin = tmin,
                    tmax = tmax,
                    baseline = baseline,
                    preload = True)

epochs.to_data_frame().to_csv('hot_damn.csv', index=False)

1 Like

sorry I didn’t see this post early enough to give any suggestions, but I’m glad you found yourself a way that worked! FWIW I probably would have suggested something that I think is very close to your “approach 2” that you ended up doing:

  1. find ECG events
  2. for each ECG event, decide which question ID it is related to (if any)
  3. recreate an events array using the ECG event timestamps and the question IDs
  4. create epochs from that new events array

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.