FitBit Heart Rate Data

Updated 2025-04-27

This covers heartrate data gathered roughly every 15 seconds by the FitBit Inspire 2, model FB418.

Data

Code
# Don't use scientific notation
pd.options.display.float_format = '{:.0f}'.format

if os.path.exists("./data/heart_rate_combined.pkl"):
    df = pd.read_pickle("./data/heart_rate_combined.pkl")
else:
    file_list = glob.glob("./fitbit_data/heart_rate-*.json")
    data_frames = [pd.read_json(file) for file in file_list]
    df = pd.concat(data_frames)
    df['dateTime'] = df['dateTime'].dt.tz_localize("UTC")
    df['dateTime'] = df['dateTime'].dt.tz_convert("US/Central")
    df['year-month'] = df['dateTime'].dt.strftime("%Y-%m")
    df['year'] = df['dateTime'].dt.year
    df['month'] = df['dateTime'].dt.month
    df['day'] = df['dateTime'].dt.day
    df["bpm"] = [i["bpm"] for i in df["value"]]
    df = df.drop(columns=["value"])
    df.to_pickle("./data/heart_rate_combined.pkl")

Data is held as a series of daily json files, which are loaded and concatenated into a Pandas dataframe. The dataframe consists of the datetime in ISO format and bpm, with this analysis consisting of 9962774 datapoints. Other fields are created for ease of parsing later.

A sample of the data format is provided here:

                       dateTime year-month  year  month  day  bpm
10680 2024-06-06 23:59:38-05:00    2024-06  2024      6    6   83
10681 2024-06-06 23:59:43-05:00    2024-06  2024      6    6   82
10682 2024-06-06 23:59:58-05:00    2024-06  2024      6    6   83

Plotting:

Code
from plotnine import *
from mizani.breaks import date_breaks

# Prep daily df
daily = df.groupby(['year','month','day'])['bpm'].mean().reset_index()
daily['date'] = daily['year'].astype(str) + "-" +  daily['month'].astype(str) +  "-" + daily['day'].astype(str)
daily['date'] = pd.to_datetime(daily['date'])


# Plot
(
    ggplot(daily, aes(x="date", y="bpm", group = 1))
    + geom_line()
    + scale_x_date(breaks=date_breaks(width="3 months"),date_minor_breaks="3 months")
    + geom_smooth(span=.2)
    + labs(x = "Date", 
      y = "Heartrate in BPM", 
      title = "Daily Average Heartrate")
        + theme(
            axis_text_x=element_text(rotation=25, hjust=1),
            panel_grid=element_line(color="grey")
         )    
        + geom_vline(xintercept=[dt.date(2024,5,1)], linetype="dashed", color="grey")
        + annotate(geom="label",fill="grey",color="black",label="Moved to St. Louis", x=dt.date(2024,5,1), y=108)
)

Code
(
    ggplot(daily, aes(x="date", y="bpm", group = "year"))
    + geom_line()
    + scale_x_date(breaks=date_breaks(width="3 months"),date_minor_breaks="3 months")
    + geom_smooth(data = daily.loc[daily["year"].isin([2023,2024])], method="lm")
    + labs(x = "Date", 
      y = "Heartrate in BPM", 
      title = "Daily Average Heartrate")
        + theme(
            axis_text_x=element_text(rotation=25, hjust=1),
            panel_grid=element_line(color="grey")
         )    
        + geom_vline(xintercept=[dt.date(2024,5,1)], linetype="dashed", color="grey")
        + annotate(geom="label",fill="grey",color="black",label="Moved to St. Louis", x=dt.date(2024,5,1), y=108)
)

Code
df['hour'] = df['dateTime'].dt.hour

hourly = df.loc[df['year'] == 2024].groupby(['hour'], as_index= False)['bpm'].mean()

(
    ggplot(hourly[['hour','bpm']], aes(x="hour", y="bpm", group = 1))
    + geom_line()
        + scale_x_continuous(limits=(0,24),breaks=range(0, 24, 1))
    + labs(x = "Hour", 
      y = "Heartrate in BPM", 
      title = "Hourly Average Heartrate over 2024")
        + theme_538()
)

Between 1AM and 5AM

Code
last_month = df.loc[(df['year'] == 2024) & (df['hour'].between(0,7))].groupby(['hour'], as_index= False)['bpm'].mean()

(
    ggplot(last_month[['hour','bpm']], aes(x="hour", y="bpm", group = 1))
    + geom_line()
        + scale_x_continuous(limits=(0,7),breaks=range(0, 7, 1))
    + labs(x = "Hour", 
      y = "Heartrate in BPM", 
      title = "Average heartrate during Sleeping Hours, 2024")
        + theme_538()
)