      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 :
# 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 :
# Load of data


3 - Identification of the channel used during acquisition

In :
channel = list(data.keys())

In :
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 :
# Sampling frequency and acquired data

# 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 :
tachogram_data, tachogram_time = bsnb.tachogram(signal, fs, signal=True, out_seconds=True)

In :
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 :
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 :
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 :
# 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 :
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 :
bsnb.plot_hrv_parameters(tachogram_time, tachogram_data, time_param_dict)


8.2 - Poincaré Parameters

In :
# 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:]

sd1 = sqrt(0.5 * power(sdsd, 2))
sd2 = sqrt(2 * power(sdnn, 2) - power(sd1, 2))
sd1_sd2 = sd1 / sd2

In :
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 :
bsnb.plot_poincare(tachogram_data)

Out:
[Figure(id='1771', ...)]

8.3 - Frequency Parameters

In :
# 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 and freq <= freq_band]
power_samples_inside_band = [p for p, freq in zip(power_spect, freqs) if freq >= freq_band and freq <= freq_band]
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 :
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 :
bsnb.plot_hrv_power_bands(freqs, power_spect)


In :
# 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 :
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 :
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 ! ☌ Project Presentation ☌ GitHub Repository ☌ How to install biosignalsnotebooks Python package ? ☌ Signal Library ☌ Notebook Categories ☌ Notebooks by Difficulty ☌ Notebooks by Signal Type ☌ Notebooks by Tag
In :
bsnb.css_style_apply()

.................... CSS Style Applied to Jupyter Notebook .........................

Out: