Automating HRV analysis: RHRVEasy

RHRV Team

2024-09-30

RHRVEasy automates all steps of a Heart Rate Variability (HRV) analysis, including data processing, indices calculation, and statistical analysis. It takes as input a list of folders, each containing the recordings of a same population. It calculates time, frequency, and nonlinear domain HRV indices, and then it applies hypothesis test, and corrects the significance levels. If there are more than two experimental groups and statistically significant differences are found, it performs a post-hoc analysis to find out which groups have the differences.

0. Set up required to run this tutorial

This tutorial uses the recordings of the Normal Sinus Rhythm RR Interval Database (hereinafter referred to as NSR_DB), a subset of the RR interval time series from healthy subjects (referred to as HEALTHY_DB), and the Congestive Heart Failure RR Interval Database (referred to as CHF_DB). The former two databases comprise data from healthy individuals, while the latter consists of recordings from patients with severe cardiac pathology. Consequently, significant disparities in numerous HRV indices are anticipated between the healthy databases and the CHF_DB.

The three databases are available in the GitHub repository for the book “Heart Rate Variability Analysis with the R package RHRV”, under the data/Chapter8 folder, within the data/Chapter8 directory. To execute this tutorial, download this folder to your local machine and define the following variables:

library("RHRV")

basePath <- "book_data"  # adjust as needed
NSR_DB <- file.path(basePath, "normal")
CHF_DB <- file.path(basePath, "chf")
HEALTHY_DB <- file.path(basePath, "healthy")

RHRVEasy permits creating an Excel spreadsheet with all the HRV indices calculated for each recording. The following variable must contain the folder on the local machine where the Excel spreadsheet is to be saved:

spreadsheetPath <- basePath

1. Time and frequency analysis

RHRVEasy enables the user to carry out a full HRV analysis by just invoking a function with a single mandatory parameter: a list with the folders containing the recordings of the experimental groups. This list must have at least two folders. Each folder must contain all the RR recordings of the same experimental group and no additional files, as RHRVEasy will try to open all the files within these folders. The name that will be used to refer to each experimental group within RHRVEasy will be the name of the folder in which its recordings are located.

The following function call computes the time and frequency indices for the NSR_DB and CHF_DB databases, and performs a statistical comparison of each index correcting the significance level with the Bonferroni method. Note the use of the nJobs to use several cores and parallelize the computations. With nJobs = -1, it uses all available cores; if an integer greater than 0 is indicated, it uses the number of cores indicated by the integer.

easyAnalysis <- RHRVEasy(folders = c(NSR_DB, CHF_DB), nJobs = -1)

When the returned object is displayed in the console, it shows which indices present statistically significant differences:

print(easyAnalysis)
## Significant differences in SDNN (Kruskal-Wallis rank sum test, bonferroni p-value = 1.117154e-07):
##   chf's mean95% CI: (61.91503, 94.0085) [Bootstrap CI without adjustment]
##   normal's mean95% CI: (131.1187, 148.1985) [Bootstrap CI without adjustment]
## 
## Significant differences in SDANN (Kruskal-Wallis rank sum test, bonferroni p-value = 3.799696e-07):
##   chf's mean95% CI: (48.19527, 80.0444) [Bootstrap CI without adjustment]
##   normal's mean95% CI: (122.0759, 139.05) [Bootstrap CI without adjustment]
## 
## Significant differences in SDNNIDX (Kruskal-Wallis rank sum test, bonferroni p-value = 0.01426098):
##   chf's mean95% CI: (29.96821, 47.6446) [Bootstrap CI without adjustment]
##   normal's mean95% CI: (47.0144, 54.5201) [Bootstrap CI without adjustment]
## 
## Significant differences in IRRR (Kruskal-Wallis rank sum test, bonferroni p-value = 1.492754e-07):
##   chf's mean95% CI: (78.67064, 124.1918) [Bootstrap CI without adjustment]
##   normal's mean95% CI: (189.5291, 215.7118) [Bootstrap CI without adjustment]
## 
## Significant differences in TINN (Kruskal-Wallis rank sum test, bonferroni p-value = 1.452872e-06):
##   chf's mean95% CI: (243.1949, 373.8965) [Bootstrap CI without adjustment]
##   normal's mean95% CI: (511.0544, 586.6332) [Bootstrap CI without adjustment]
## 
## Significant differences in HRVi (Kruskal-Wallis rank sum test, bonferroni p-value = 1.452872e-06):
##   chf's mean95% CI: (15.96148, 23.78737) [Bootstrap CI without adjustment]
##   normal's mean95% CI: (32.80169, 37.58583) [Bootstrap CI without adjustment]
## 
## Significant differences in ULF (Kruskal-Wallis rank sum test, bonferroni p-value = 1.74099e-08):
##   chf's mean95% CI: (1182.117, 4410.562) [Bootstrap CI without adjustment]
##   normal's mean95% CI: (7215.618, 9824.658) [Bootstrap CI without adjustment]
## 
## Significant differences in VLF (Kruskal-Wallis rank sum test, bonferroni p-value = 0.002535127):
##   chf's mean95% CI: (52.21509, 135.5065) [Bootstrap CI without adjustment]
##   normal's mean95% CI: (131.5723, 175.2834) [Bootstrap CI without adjustment]

All computed indices, as well as all p-values resulting from all comparisons, are stored in data.frames contained in the object. Two different sets of p-values are available; the ones obtained before (p.value) and after (adj.p.value) applying the significance level correction:

# HRVIndices
head(easyAnalysis$HRVIndices)
## # A tibble: 6 × 16
##   file       group  SDNN SDANN SDNNIDX pNN50  SDSD rMSSD  IRRR MADRR  TINN  HRVi
##   <chr>      <fct> <dbl> <dbl>   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 chf201_rr… chf    75.5  52.9    49.6  2.03  20.2  20.2  93.8  7.81  358. 22.9 
## 2 chf202_rr… chf    88.5  75.8    39.6  6.13  34.7  34.7 117.  15.6   350. 22.4 
## 3 chf203_rr… chf    38.8  30.9    21.7  1.20  17.3  17.3  46.9  7.81  170. 10.9 
## 4 chf204_rr… chf    55.1  39.1    36.0  4.84  33.0  33.0  70.3  7.81  237. 15.2 
## 5 chf205_rr… chf    34.9  26.1    19.5  1.97  23.7  23.7  46.9  7.81  169. 10.8 
## 6 chf206_rr… chf    41.2  34.9    14.8  2.02  18.9  18.9  31.2  7.81  122.  7.79
## # ℹ 4 more variables: ULF <dbl>, VLF <dbl>, LF <dbl>, HF <dbl>
# Statistical analysis
head(easyAnalysis$stats)
## # A tibble: 6 × 4
##   HRVIndex method                             p.value adj.p.value
##   <chr>    <chr>                                <dbl>       <dbl>
## 1 SDNN     Kruskal-Wallis rank sum test 0.00000000798 0.000000112
## 2 SDANN    Kruskal-Wallis rank sum test 0.0000000271  0.000000380
## 3 SDNNIDX  Kruskal-Wallis rank sum test 0.00102       0.0143     
## 4 pNN50    Kruskal-Wallis rank sum test 0.774         1          
## 5 SDSD     Kruskal-Wallis rank sum test 0.0891        1          
## 6 rMSSD    Kruskal-Wallis rank sum test 0.0891        1

The format parameter specifies the format in which the RR intervals are stored. All formats supported by the RHRV package can be used: WFDB, ASCII, RR, Polar, Suunto, EDFPlus or Ambit (check the RHRV website for more information). The default format is RR, where the beat distances in seconds are stored in a single column of an ASCII file. This is the format of the three databases used in this tutorial.

By default, the frequency analysis is performed using the Fourier transform. It is also possible to use the Wavelet transform pasing the value 'wavelet' to the typeAnalysis parameter (check the paper “García, C. A., Otero, A., Vila, X., & Márquez, D. G. (2013). A new algorithm for wavelet-based heart rate variability analysis. Biomedical Signal Processing and Control, 8(6), 542-550” for details):

easyAnalysisWavelet <- RHRVEasy(
  folders = c(NSR_DB, CHF_DB), 
  typeAnalysis = 'wavelet', 
  n_jobs = -1
)

Note that the significant indices are the same as the previous ones.

2. Correction of the significance level

Given that multiple statistical tests are performed on several HRV indices, a correction of the significance level should be applied. The Bonferroni method is used by default. This behavior can be overridden with the parameter correctionMethod of RHRVEasy. The possible values of this parameter besides bonferroni are holm, hochberg, hommel, BH (Benjamini & Hochberg), fdr (false discovery rate), BY (Benjamini & Yekutieli), and none (indicating that no correction is to be made). Furthermore, there is no need to recompute the HRV indices to apply a different correction method, but the RHRVEasyStats function can be used to this end. The confidence level can also be changed using the significance parameter (in both RHRVEasy and RHRVEasyStats functions).

easyAnalysisFDR <- RHRVEasyStats(easyAnalysis, correctionMethod =  'fdr')
pValues <- merge(
  easyAnalysis$stats, 
  easyAnalysisFDR$stats,
  by = setdiff(names(easyAnalysis$stats), "adj.p.value"),
  suffixes = c(".bonf", ".fdr")
)
#Let us compare the p-values obtained with different correction methods 
print(
  head(
    pValues[, c("HRVIndex", "p.value", "adj.p.value.bonf", "adj.p.value.fdr")]
  )
) 
##   HRVIndex      p.value adj.p.value.bonf adj.p.value.fdr
## 1       HF 5.601495e-01     1.000000e+00    6.032380e-01
## 2     HRVi 1.037766e-07     1.452872e-06    2.421454e-07
## 3     IRRR 1.066253e-08     1.492754e-07    4.975847e-08
## 4       LF 1.651479e-02     2.312071e-01    2.568968e-02
## 5    MADRR 6.319903e-02     8.847864e-01    8.847864e-02
## 6    pNN50 7.744691e-01     1.000000e+00    7.744691e-01

3. Saving the indices to an Excel spreadsheet

If the argument saveHRVindicesInPath is specified when invoking the function RHRVEasy, an Excel spreadsheet with all the HRV indices calculated for each recording will be created in the path specified by this parameter. The name of the spreadsheet generated is “<group 1 name>Vs<group 2 name> .xlsx”:

easyAnalysis <- RHRVEasy(folders = c(NSR_DB, CHF_DB), 
                         saveHRVIndicesInPath = spreadsheetPath)

This spreadsheet can also be generated from the object returned by RHRVEasy by calling the function SaveHRVIndices.

SaveHRVIndices(easyAnalysis, saveHRVIndicesInPath = spreadsheetPath)

4. Comparing more than two experimental groups

If the analysis involves three or more groups, when statistically significant differences are found among them it does not necessarily mean that there are statistically significant differences between all pairs of groups. In such a scenario post-hoc tests are used to find which pairs of groups present differences:

#Comparison of the three databases
easyAnalysis3 <- RHRVEasy(
  folders = c(NSR_DB, CHF_DB, HEALTHY_DB),
  nJobs = -1
)
print(easyAnalysis3)
## Significant differences in SDNN (Kruskal-Wallis rank sum test, bonferroni p-value = 3.543622e-07):
##   Significant differences in the post-hoc tests (Dunn's all-pairs test + bonferroni-p-value adjustment):
##       group1  group2 adj.p.value
##     1 healthy chf    0.00799    
##     2 normal  chf    0.000000282
##     ----------------------------
##     chf's mean95% CI: (63.20538, 92.2515) [Bootstrap CI without adjustment]
##     healthy's mean95% CI: (123.242, 158.269) [Bootstrap CI without adjustment]
##     normal's mean95% CI: (131.665, 147.9961) [Bootstrap CI without adjustment]
## 
## Significant differences in SDANN (Kruskal-Wallis rank sum test, bonferroni p-value = 1.345688e-06):
##   Significant differences in the post-hoc tests (Dunn's all-pairs test + bonferroni-p-value adjustment):
##       group1 group2 adj.p.value
##     1 normal chf    0.000000403
##     ---------------------------
##     chf's mean95% CI: (47.61222, 81.42191) [Bootstrap CI without adjustment]
##     healthy's mean95% CI: (105.1872, 134.0331) [Bootstrap CI without adjustment]
##     normal's mean95% CI: (120.4753, 138.5329) [Bootstrap CI without adjustment]
## 
## Significant differences in SDNNIDX (Kruskal-Wallis rank sum test, bonferroni p-value = 0.001063849):
##   Significant differences in the post-hoc tests (Dunn's all-pairs test + bonferroni-p-value adjustment):
##       group1  group2 adj.p.value
##     1 healthy chf        0.00111
##     ----------------------------
##     chf's mean95% CI: (29.1345, 47.73994) [Bootstrap CI without adjustment]
##     healthy's mean95% CI: (56.23389, 74.9991) [Bootstrap CI without adjustment]
##     normal's mean95% CI: (47.0101, 54.33106) [Bootstrap CI without adjustment]
## 
## Significant differences in IRRR (Kruskal-Wallis rank sum test, bonferroni p-value = 3.688167e-07):
##   Significant differences in the post-hoc tests (Dunn's all-pairs test + bonferroni-p-value adjustment):
##       group1  group2 adj.p.value
##     1 healthy chf    0.00395    
##     2 normal  chf    0.000000425
##     ----------------------------
##     chf's mean95% CI: (77.3305, 124.7238) [Bootstrap CI without adjustment]
##     healthy's mean95% CI: (179.9086, 234.5556) [Bootstrap CI without adjustment]
##     normal's mean95% CI: (187.6484, 215.9975) [Bootstrap CI without adjustment]
## 
## Significant differences in MADRR (Kruskal-Wallis rank sum test, bonferroni p-value = 0.006224158):
##   Significant differences in the post-hoc tests (Dunn's all-pairs test + bonferroni-p-value adjustment):
##       group1  group2 adj.p.value
##     1 healthy chf        0.00237
##     ----------------------------
##     chf's mean95% CI: (8.62069, 11.85345) [Bootstrap CI without adjustment]
##     healthy's mean95% CI: (16.55556, 24.66667) [Bootstrap CI without adjustment]
##     normal's mean95% CI: (11.28472, 14.03356) [Bootstrap CI without adjustment]
## 
## Significant differences in TINN (Kruskal-Wallis rank sum test, bonferroni p-value = 1.350844e-06):
##   Significant differences in the post-hoc tests (Dunn's all-pairs test + bonferroni-p-value adjustment):
##       group1  group2 adj.p.value
##     1 healthy chf     0.000933  
##     2 normal  chf     0.00000519
##     ----------------------------
##     chf's mean95% CI: (244.0477, 371.3618) [Bootstrap CI without adjustment]
##     healthy's mean95% CI: (533.6798, 701.4795) [Bootstrap CI without adjustment]
##     normal's mean95% CI: (511.6379, 586.4394) [Bootstrap CI without adjustment]
## 
## Significant differences in HRVi (Kruskal-Wallis rank sum test, bonferroni p-value = 1.350844e-06):
##   Significant differences in the post-hoc tests (Dunn's all-pairs test + bonferroni-p-value adjustment):
##       group1  group2 adj.p.value
##     1 healthy chf     0.000933  
##     2 normal  chf     0.00000519
##     ----------------------------
##     chf's mean95% CI: (15.85798, 23.7487) [Bootstrap CI without adjustment]
##     healthy's mean95% CI: (34.45, 45.19331) [Bootstrap CI without adjustment]
##     normal's mean95% CI: (32.68737, 37.61479) [Bootstrap CI without adjustment]
## 
## Significant differences in ULF (Kruskal-Wallis rank sum test, bonferroni p-value = 5.860632e-08):
##   Significant differences in the post-hoc tests (Dunn's all-pairs test + bonferroni-p-value adjustment):
##       group1 group2  adj.p.value
##     1 normal chf    0.0000000162
##     ----------------------------
##     chf's mean95% CI: (1075.296, 4358.885) [Bootstrap CI without adjustment]
##     healthy's mean95% CI: (4995.594, 8167.694) [Bootstrap CI without adjustment]
##     normal's mean95% CI: (7063.468, 9898.164) [Bootstrap CI without adjustment]
## 
## Significant differences in VLF (Kruskal-Wallis rank sum test, bonferroni p-value = 0.0005669878):
##   Significant differences in the post-hoc tests (Dunn's all-pairs test + bonferroni-p-value adjustment):
##       group1  group2 adj.p.value
##     1 healthy chf        0.00239
##     2 normal  chf        0.00977
##     ----------------------------
##     chf's mean95% CI: (54.04686, 134.9712) [Bootstrap CI without adjustment]
##     healthy's mean95% CI: (171.6335, 340.8925) [Bootstrap CI without adjustment]
##     normal's mean95% CI: (130.0847, 177.0061) [Bootstrap CI without adjustment]

Note that the stats data.frame now contains a column named pairwise storing the results of the post-hoc analysis for those indices where the omnibus test has been significant:

print(head(easyAnalysis3$stats))
## # A tibble: 6 × 5
##   HRVIndex method                            p.value adj.p.value pairwise
##   <chr>    <chr>                               <dbl>       <dbl> <list>  
## 1 SDNN     Kruskal-Wallis rank sum test 0.0000000253 0.000000354 <tibble>
## 2 SDANN    Kruskal-Wallis rank sum test 0.0000000961 0.00000135  <tibble>
## 3 SDNNIDX  Kruskal-Wallis rank sum test 0.0000760    0.00106     <tibble>
## 4 pNN50    Kruskal-Wallis rank sum test 0.0186       0.260       <NULL>  
## 5 SDSD     Kruskal-Wallis rank sum test 0.0301       0.421       <NULL>  
## 6 rMSSD    Kruskal-Wallis rank sum test 0.0301       0.421       <NULL>
# Let's print the post-hoc comparisons for "SDNN"
print(head(easyAnalysis3$stats$pairwise[[1]]))
## # A tibble: 3 × 6
##   HRVIndex group1  group2  method                     p.value adj.p.value
##   <chr>    <chr>   <chr>   <chr>                        <dbl>       <dbl>
## 1 SDNN     healthy chf     Dunn's all-pairs test 0.000296     0.00799    
## 2 SDNN     normal  chf     Dunn's all-pairs test 0.0000000104 0.000000282
## 3 SDNN     normal  healthy Dunn's all-pairs test 0.861        1

5. Overwriting default parameters

Any parameter of any RHRV function can be specified as an additional parameter of the RHRVEasy function; in this case, the default value used for that parameter will be overwritten by the one specified for the user. The default values used in the RHRVEasy package are the same as those used in the RHRV package. For more information about the parameters available you can consult the RHRV website. For example, the following analysis modifies the the limits of the ULF, VLF, LF and HF spectral bands, and uses an interpolation frequency (freqhr) of 2 Hz:

easyAnalysisOverwritten <- RHRVEasy(folders = c(NSR_DB, CHF_DB),
                                    freqhr = 2, 
                                    ULFmin = 0, ULFmax = 0.02, 
                                    VLFmin = 0.02,  VLFmax = 0.07, 
                                    LFmin = 0.07, LFmax = 0.20, 
                                    HFmin = 0.20, HFmax = 0.5)

6. Nonlinear analysis

The calculation of the nonlinear indices requires considerable computational resources, specially the Recurrence Quantification Analysis (RQA). Whereas in a typical HRV analysis the computation of all the time and frequency domain indices for a few dozens of recordings often completes within a few minutes, the computation of the nonlinear indices could last many hours. That’s why the boolean parameters nonLinear and doRQA are set to FALSE by default. If these parameters are not changed, only time and frequency indices will be calculated, as in the previous sections.

Warning: the following sentence, will take several hours to execute on a medium to high performance PC. You may reproduce the results of the paper by running this chunk of code.

fullAnalysis <- RHRVEasy(
  folders = c(NSR_DB, CHF_DB, HEALTHY_DB),
  nJobs = -1,
  nonLinear =  TRUE, 
  doRQA = TRUE,
  saveHRVIndicesInPath = spreadsheetPath
)