Quantified Self Experiments / Accuracy - Fitbit Charge 5 vs 4 - Heart Rate

## Abstract

#### What did i do?

I've measured my heart rate by using Fitbit Charge 5 and 4 simultaneously

#### How did i do it?

I've used bland altman plot approach to build agreement between 2 devices.

#### What did i learn?

Fitbit Charge 5 seems to be a biased for +0.6bpm versus Fitbit Charge 4. Devices doesnt agree well, with limits of agreement between -6.6 and 7.8 beats per minute. Trends are pretty consistent which is a good sign.

## Introduction

Heart rate is important biomarker which represents general health. Accurate measurements may provide some useful insights in sickness and disease detection.

The purpose of this experiment (n=1) is to compare heart data between 2 wristworn devices.

## Materials & Methods

#### Participants

Adult male anthropometrics was described in previous article.

#### Experimental design

From 2021-10-06 to 2021-10-12 Fitbit Charge 5 and Charge 4 was weared on same hand (left) and heart rate data were collected. There were single training session (~20 minutes of rowing) and a few ~1 hour walks.

## Results

To check heart rate agreement i've decided to build Bland-Altman plots. Heart rates were compared on same resolutions. There were total ~65000 measurements during that period.
Bland-Altman plot for 1 minute average heart rate: X and Y axis are in beats per minute

How to read this plot? Imagine we have a list of heart rate measuremens, n rows, each row contain 2 values one for FC4 and one for FC5. Then for each row we compute mean for both measures: (FC5 - FC4) / 2 and their difference (FC5 - FC4). Then we plot means on X and differences on Y.

For example, we can see that when heart rate is less than 60 bpm devices agree well (small difference FC5 - FC4). But when heart rate goes to 80 they agree less.
Black dashed line slightly above x axis indicate bias (difference between overall means). Green and red lines represents 95% limits of agreement. Here we can see 0.6 bpm bias and wide limits of agreement [-8.7,9.9]. Thats seems like a poor agreement.

Lets build a linear regression: Adjusted R-squared is a proportion of shared variance between both devices. We can see ~91% value which is not bad and is equal to correlation coefficient of 0.95. Devices seems to follow patterns pretty well, which is easily detected by visual inspection: We can check how devices agree on 5 minute periods: Limits of agreement were improved, but not too much.

## Discussion

This data analysis suggests a poor agreement for absolute heart rate measurement value on 1-min and 5-min resolution windows. On the other side devices share 91% of variation (R-squared) and correlation coefficient is 0.95 is pretty large which suggests that devices follow similar trend pretty well.

## Data availability & Information

Welcome for questions, suggestions and critics in comments below.

Original unmodified (exported) raw data for Fitbit Charge 5 here and for Fitbit Charge 4 is here.

`alpha <- 0.05source("https://blog.kto.to/uploads/r/functions.R")df.fc4.raw <- na.omit(read.json.dir("Data/fc4/"))df.fc4.raw <- df.fc4.raw[df.fc4.raw\$value.confidence > 0, ]df.fc4 <- data.frame(datetime = as.POSIXct(df.fc4.raw\$dateTime, tryFormats = c("%m/%d/%y %H:%M:%OS")), hr = df.fc4.raw\$value.bpm)df.fc5.raw <- na.omit(read.json.dir("Data/fc5/"))df.fc5.raw <- df.fc5.raw[df.fc5.raw\$value.confidence > 0, ]df.fc5 <- data.frame(datetime = as.POSIXct(df.fc5.raw\$dateTime, tryFormats = c("%m/%d/%y %H:%M:%OS")), hr = df.fc5.raw\$value.bpm)library(lubridate)library(dplyr)df.fc4 = df.fc4 %>%  mutate(datetime = floor_date(datetime, unit = "1 minute")) %>%  group_by(datetime) %>%  summarise(hr.fc4 = mean(hr, na.rm = TRUE))df.fc5 = df.fc5 %>%  mutate(datetime = floor_date(datetime, unit = "1 minute")) %>%  group_by(datetime) %>%  summarise(hr.fc5 = mean(hr, na.rm = TRUE))par(mfrow = c(2,1))plot(df.fc4\$datetime, df.fc4\$hr.fc4, pch = 20, cex = .9)plot(df.fc5\$datetime, df.fc5\$hr.fc5, pch = 20, cex = .9)df <- left_join(df.fc4, df.fc5, by=c("datetime"))df <- na.omit(df)library(blandr)library(shiny)library(boot)blandr.statistics(df\$hr.fc5, df\$hr.fc4, sig.level = 1 - alpha, LoA.mode = 1)par(mfrow = c(1,1))blandr.draw(df\$hr.fc5, df\$hr.fc4, plotter = "rplot")#blandr.output.report(df\$hr.fc5, df\$hr.fc4)df\$diff <- df\$hr.fc5 - df\$hr.fc4summary(df\$diff); quantile(df\$diff, 0.025); quantile(df\$diff, 0.975)boot.fn <- function(data, indices) { return(mean(data[indices]))} boot_results <- boot(df\$diff, R = 1000, statistic = boot.fn); boot_resultsboot.ci(boot_results, conf=1-alpha, type="perc")lm.fit <- lm(df\$hr.fc4 ~ df\$hr.fc5)summary(lm.fit)par(mfrow = c(2,2))plot(lm.fit)library(car)par(mfrow = c(1,1))avPlots(lm.fit, ellipse = TRUE)ggplotRegression(lm.fit)`

#### Statistical analysis

RStudio version 1.3.959 and R version 4.0.2.