Extract and Visualize Individual Heartbeats#

This example can be referenced by citing the package.

This example shows how to use NeuroKit to extract and visualize the QRS complexes (individual heartbeats) from an electrocardiogram (ECG).

# Load NeuroKit and other useful packages
import neurokit2 as nk
import numpy as np
import matplotlib.pyplot as plt
# This "decorative" cell should be hidden from the docs once this is implemented:
# https://github.com/microsoft/vscode-jupyter/issues/1182 
plt.rcParams['figure.figsize'] = [15, 5]  # Bigger images
plt.rcParams['font.size']= 14

Extract the cleaned ECG signal#

In this example, we will use a simulated ECG signal. However, you can use any of your signal (for instance, extracted from the dataframe using the read_acqknowledge().

# Simulate 30 seconds of ECG Signal (recorded at 250 samples / second)
ecg_signal = nk.ecg_simulate(duration=30, sampling_rate=250)

Once you have a raw ECG signal in the shape of a vector (i.e., a one-dimensional array), or a list, you can use ecg_process() to process it.

Note: It is critical that you specify the correct sampling rate of your signal throughout many processing functions, as this allows NeuroKit to have a time reference.

# Automatically process the (raw) ECG signal
signals, info = nk.ecg_process(ecg_signal, sampling_rate=250)

This function outputs two elements, a dataframe containing the different signals (raw, cleaned, etc.) and a dictionary containing various additional information (peaks location, …).

Extract R-peaks location#

The processing function does two important things for our purpose: 1) it cleans the signal and 2) it detects the location of the R-peaks. Let’s extract these from the output.

# Extract clean ECG and R-peaks location
rpeaks = info["ECG_R_Peaks"]
cleaned_ecg = signals["ECG_Clean"]

Great. We can visualize the R-peaks location in the signal to make sure it got detected correctly by marking their location in the signal.

# Visualize R-peaks in ECG signal
plot = nk.events_plot(rpeaks, cleaned_ecg)
../../_images/277ce891897f193d169efa1ad500215c04a0096aa46508464513e7e64dcea1f2.png

Once that we know where the R-peaks are located, we can create windows of signal around them (of a length of for instance 1 second, ranging from 400 ms before the R-peak), which we can refer to as epochs.

Segment the signal around the heart beats#

You can now epoch all these individual heart beats, synchronized by their R peaks with the ecg_segment() function.

# Plotting all the heart beats
epochs = nk.ecg_segment(cleaned_ecg, rpeaks=None, sampling_rate=250, show=True)
../../_images/f06dec000495cc4419e530f2376b23c6ae7826c6b2584de1553d51f45bf6b6f8.png

This create a dictionary of dataframes for each ‘epoch’ (in this case, each heart beat).

Advanced Plotting#

This section is written for a more advanced purpose of plotting and visualizing all the heartbeats segments. The code below uses packages other than NeuroKit2 to manually set the colour gradient of the signals and to create a more interactive experience for the user - by hovering your cursor over each signal, an annotation of the signal corresponding to the heart beat index is shown.

Custom colors and legend#

Here, we define a function to create the epochs. It takes in cleaned as the cleaned signal dataframe, and peaks as the array of R-peaks locations.

# Define a function to create epochs
def extract_heartbeats(cleaned, peaks, sampling_rate=None): 
    heartbeats = nk.epochs_create(cleaned, 
                                  events=peaks, 
                                  epochs_start=-0.3, 
                                  epochs_end=0.4, 
                                  sampling_rate=sampling_rate)
    heartbeats = nk.epochs_to_df(heartbeats)
    return heartbeats
    
heartbeats = extract_heartbeats(cleaned_ecg, peaks=rpeaks, sampling_rate=250)
heartbeats.head()
Signal Index Label Time
0 -0.197596 138 1 -0.300000
1 -0.191932 139 1 -0.295977
2 -0.186218 140 1 -0.291954
3 -0.180394 141 1 -0.287931
4 -0.174388 142 1 -0.283908

We then pivot the dataframe so that each column corresponds to the signal values of one channel, or Label.

heartbeats_pivoted = heartbeats.pivot(index='Time', columns='Label', values='Signal')
heartbeats_pivoted.head()
Label 1 10 11 12 13 14 15 16 17 18 ... 31 32 33 34 4 5 6 7 8 9
Time
-0.300000 -0.197596 -0.130136 -0.136317 -0.139936 -0.132997 -0.138664 -0.144837 -0.137267 -0.140114 -0.138768 ... -0.135290 -0.167876 -0.025205 -0.344087 -0.117783 -0.140104 -0.139753 -0.141687 -0.143025 -0.132586
-0.295977 -0.191932 -0.129020 -0.135007 -0.139127 -0.132291 -0.137883 -0.143724 -0.136445 -0.139123 -0.137999 ... -0.134431 -0.166293 -0.024575 -0.344010 -0.116980 -0.139258 -0.138742 -0.140819 -0.142537 -0.131762
-0.291954 -0.186218 -0.127736 -0.133500 -0.138070 -0.131406 -0.136994 -0.142438 -0.135494 -0.137993 -0.137086 ... -0.133379 -0.164538 -0.023844 -0.343784 -0.116074 -0.138266 -0.137536 -0.139727 -0.141889 -0.130877
-0.287931 -0.180394 -0.126239 -0.131725 -0.136699 -0.130292 -0.135904 -0.140914 -0.134365 -0.136697 -0.136004 ... -0.132075 -0.162562 -0.022989 -0.343375 -0.115015 -0.137101 -0.136076 -0.138353 -0.141056 -0.129900
-0.283908 -0.174388 -0.124478 -0.129595 -0.134939 -0.128889 -0.134500 -0.139074 -0.132984 -0.135197 -0.134721 ... -0.130439 -0.160297 -0.021974 -0.342735 -0.113742 -0.135729 -0.134276 -0.136628 -0.140011 -0.128778

5 rows × 34 columns

# Prepare figure
fig, ax = plt.subplots()

ax.set_title("Individual Heart Beats")
ax.set_xlabel("Time (seconds)")

# Aesthetics
labels = list(heartbeats_pivoted)
labels = ['Channel ' + x for x in labels] # Set labels for each signal
cmap = iter(plt.cm.YlOrRd(np.linspace(0,1, int(heartbeats["Label"].nunique())))) # Get color map
lines = [] # Create empty list to contain the plot of each signal

for i, x, color in zip(labels, heartbeats_pivoted, cmap):
    line, = ax.plot(heartbeats_pivoted[x], label='%s' % i, color=color)
    lines.append(line)
../../_images/cdc8cb2bdd5366389a6650b8cc55ca17d9c38c26def2a1bc0cb3c828091b75cc.png

Interactivity#

This section of the code incorporates the aesthetics and interactivity of the plot produced. Unfortunately, the interactivity is not active in this example but it should work in your console! As you hover your cursor over each signal, annotation of the channel that produced it is shown. You will need to uncomment the code below.

Note: you need to install the mplcursors package for the interactive part (pip install mplcursors)

# # Import packages
# import ipywidgets as widgets
# from ipywidgets import interact, interact_manual

# import mplcursors

# # Obtain hover cursor
# mplcursors.cursor(lines, hover=True, highlight=True).connect("add", lambda sel: sel.annotation.set_text(sel.artist.get_label())) 
# # Return figure
# fig