ECG Analysis - Heart Rate Variability Parameters
Difficulty Level:
Tags extract☁ecg☁hrv

Using an analogy with the programming paradigms, electrophysiological signals can be viewed as objects containing lots of information inside. However obtatining knowledge from an object is only possible by accessing its attributes (characteristics).

In signal processing there is an identical logic, so, for extracting knowledge from signals (our objects), we need to identify their intrinsic characteristics (parameters).

The following description explains how to extract some parameters from ECG, commonly used for heart rate variability analysis (HRV).

List of HRV analysis parameters:
Minimum, Maximum and Average RR Interval;
Minimum, Maximum and Average Heart Rate (BPM);
SDNN;
rmsSD;
NN20, pNN20;
NN50, pNN50;
Power inside ULF, VLF, LF and HF Frequency Bands;
SD1, SD2, SD1 / SD2;


1 - Importation of the needed packages

In [1]:
# biosignalsnotebooks own package for loading and plotting the acquired data
import biosignalsnotebooks as bsnb

# Scientific packages
from numpy import linspace, max, min, average, std, array, diff, fabs, sqrt, power, round
from scipy.integrate import simps

2 - Load of acquired ECG data

In [2]:
# Load of data
data, header = bsnb.load_signal("ecg_5_min", get_header=True)

3 - Identification of the channel used during acquisition

In [3]:
channel = list(data.keys())[0]
In [4]:
print (" Channel: " + str(channel) + " of " + str(header["device name"]) + " device")
 Channel: CH1 of b'00:07:80:58:9B:3F' device

4 - Storage of sampling frequency and acquired data inside variables

In [5]:
# Sampling frequency and acquired data
fs = header["sampling rate"]

# Signal Samples
signal = data[channel]
time = linspace(0, len(signal) / fs, len(signal))

5 - Generation of tachogram

Tachogram defines the fundamental structure from where all parameters will be extracted.

In [6]:
tachogram_data, tachogram_time = bsnb.tachogram(signal, fs, signal=True, out_seconds=True)
In [7]:
bsnb.plot(tachogram_time, tachogram_data, x_axis_label='Time (s)', y_axis_label='Cardiac Cycle (s)', title="Tachogram",  x_range=(0, time[-1]))

6 - Removal of ectopic beats

A commonly accepted definition for ectopic beats establishes that a cardiac cycle that differs in at least 20 % of the duration of the previous one, can be considered an ectopic beat that should be removed.

In [8]:
tachogram_data_NN, tachogram_time_NN = bsnb.remove_ectopy(tachogram_data, tachogram_time)
bpm_data = (1 / array(tachogram_data_NN)) * 60

7 - Comparison between the tachograms obtained before and after ectopic beat removal

In [9]:
bsnb.plot_post_ecto_rem_tachogram(tachogram_time, tachogram_data, tachogram_time_NN, tachogram_data_NN)

We can conclude that there are not ectopic beats in the present acquisition

8 - Extraction of Parameters

8.1 - Time Parameters

In [10]:
# Maximum, Minimum and Average RR Interval
max_rr = max(tachogram_data_NN)
min_rr = min(tachogram_data_NN)
avg_rr = average(tachogram_data_NN)

# Maximum, Minimum and Average Heart Rate
max_hr = 1 / min_rr # Cycles per second
max_bpm = max_hr * 60 # BPM

min_hr = 1 / max_rr # Cycles per second
min_bpm = min_hr * 60 # BPM

avg_hr = 1 / avg_rr # Cyles per second
avg_bpm = avg_hr * 60 # BPM

# SDNN
sdnn = std(tachogram_data_NN)

time_param_dict = {"Maximum RR": max_rr, "Minimum RR": min_rr, "Average RR": avg_rr, "Maximum BPM": max_bpm, "Minimum BPM": min_bpm, "Average BPM": avg_bpm, "SDNN": sdnn}
In [11]:
print ("[Maximum RR, Minimum RR, Average RR] = [" + str(max_rr) + ", " + str(min_rr) + ", " + str(avg_rr) + "] s")
print ("[Maximum BPM, Minimum BPM, Average BPM] = [" + str(max_bpm) + ", " + str(min_bpm) + ", " + str(avg_bpm) + "] BPM")
[Maximum RR, Minimum RR, Average RR] = [0.9230030766769346, 0.6710022366741271, 0.7767506411168555] s
[Maximum BPM, Minimum BPM, Average BPM] = [89.41847988077431, 65.00520043336859, 77.24486704475537] BPM
In [12]:
bsnb.plot_hrv_parameters(tachogram_time, tachogram_data, time_param_dict)

8.2 - Poincaré Parameters

In [13]:
# Auxiliary Structures
tachogram_diff = diff(tachogram_data)
tachogram_diff_abs = fabs(tachogram_diff)
sdsd = std(tachogram_diff)
rr_i = tachogram_data[:-1]
rr_i_plus_1 = tachogram_data[1:]

# Poincaré Parameters
sd1 = sqrt(0.5 * power(sdsd, 2))
sd2 = sqrt(2 * power(sdnn, 2) - power(sd1, 2))
sd1_sd2 = sd1 / sd2
In [14]:
print ("[SD1, SD2] = [" + str(sd1) + ", " + str(sd2) + "] s")
print ("SD1/SD2 = " + str(sd1_sd2))
[SD1, SD2] = [0.016523103999584482, 0.05624870449741531] s
SD1/SD2 = 0.293750836525377
In [15]:
bsnb.plot_poincare(tachogram_data)
Out[15]:
[Figure(id='1771', ...)]

8.3 - Frequency Parameters

In [16]:
# Auxiliary Structures
freqs, power_spect = bsnb.psd(tachogram_time, tachogram_data) # Power spectrum.

# Frequemcy Parameters
freq_bands = {"ulf_band": [0.00, 0.003], "vlf_band": [0.003, 0.04], "lf_band": [0.04, 0.15], "hf_band": [0.15, 0.40]}
power_values = {}
total_power = 0

band_keys = freq_bands.keys()
for band in band_keys:
    freq_band = freq_bands[band]
    freq_samples_inside_band = [freq for freq in freqs if freq >= freq_band[0] and freq <= freq_band[1]]
    power_samples_inside_band = [p for p, freq in zip(power_spect, freqs) if freq >= freq_band[0] and freq <= freq_band[1]]
    power = round(simps(power_samples_inside_band, freq_samples_inside_band), 5)
    
    # Storage of power inside each band
    power_values[band] = power
    
    # Total power update
    total_power = total_power + power
In [17]:
print ("Power in [ULF, VLF, LF, HF] Bands = [" + str(power_values["ulf_band"]) + ", " + str(power_values["vlf_band"]) + ", " + str(power_values["lf_band"]) + ", " + str(power_values["hf_band"]) + "] s\u00B2")
print ("Total Power = " + str(total_power) + " s\u00B2")
Power in [ULF, VLF, LF, HF] Bands = [0.0, 0.00084, 0.00104, 0.00014] s²
Total Power = 0.00202 s²
In [18]:
bsnb.plot_hrv_power_bands(freqs, power_spect)

Additional Temporal Parameters

In [19]:
# Number of RR intervals that have a difference in duration, from the previous one, of at least 20 ms
nn20 = sum(1 for i in tachogram_diff_abs if i > 0.02)
pnn20 = int(float(nn20) / len(tachogram_diff_abs) * 100) # Percentage value.

# Number of RR intervals that have a difference in duration, from the previous one, of at least 50 ms
nn50 = sum(1 for i in tachogram_diff_abs if i > 0.05)
pnn50 = int(float(nn50) / len(tachogram_diff_abs) * 100) # Percentage value.
In [20]:
print ("[NN20, pNN20, NN50, pNN50] = [" + str(nn20) + ", " + str(pnn20) + " %, " + str(nn50) + ", " + str(pnn50) + " %]")
[NN20, pNN20, NN50, pNN50] = [139, 36 %, 12, 3 %]

This procedure can be automatically done by hrv_parameters function in extract module of biosignalsnotebooks package

In [21]:
dictParameters = bsnb.hrv_parameters(signal, fs, signal=True)
print (dictParameters)
{'MaxRR': 0.9230030766769346, 'MinRR': 0.6710022366741271, 'AvgRR': 0.7767506411168555, 'MaxBPM': 89.41847988077431, 'MinBPM': 65.00520043336859, 'AvgBPM': 77.24486704475537, 'SDNN': 0.041454370839627, 'SD1': 0.016523103999584482, 'SD2': 0.05624870449741531, 'SD1/SD2': 0.293750836525377, 'NN20': 139, 'pNN20': 36, 'NN50': 12, 'pNN50': 3, 'ULF_Power': 0.0, 'VLF_Power': 0.00084, 'LF_Power': 0.00104, 'HF_Power': 0.00014, 'LF_HF_Ratio': 7.428571428571429, 'Total_Power': 0.00202}

This set of parameters reveals interesting information about ECG signal, however you can extract much more features during your signal processing journey !

We hope that you have enjoyed this guide. biosignalsnotebooks is an environment in continuous expansion, so don"t stop your journey and learn more with the remaining Notebooks !

In [22]:
bsnb.css_style_apply()
.................... CSS Style Applied to Jupyter Notebook .........................
Out[22]: