Overview

This report explores daily resting heart rate, resilience levels,
and physical activity using Oura data.


1. Setup


2. Load Data

# Read the datasets (semicolon-delimited)
heartrate_data  <- read.csv2("heartrate.csv")
resilience_data <- read.csv2("dailyresilience.csv")
workout_data    <- read.csv2("workout.csv")

3. Process Heart Rate Data

heartrate_rest_daily <- heartrate_data %>%
  mutate(clean_date = as.Date(substr(timestamp, 1, 10))) %>%  # extract date
  filter(source == "rest") %>%                                # keep resting HR only
  group_by(clean_date) %>%
  summarise(avg_bpm = mean(bpm, na.rm = TRUE))                # daily average

4. Process Workout Data

workout_calories_daily <- workout_data %>%
  mutate(day = as.Date(day)) %>%
  group_by(day) %>%
  summarise(total_daily_calories = sum(as.numeric(calories), na.rm = TRUE)) %>%
  mutate(ActiveThreshold = total_daily_calories > 500)        # flag high-activity days

5. Process Resilience Data

resilience_data <- resilience_data %>%
  mutate(day = as.Date(day))

6. Combine All Datasets

combined <- heartrate_rest_daily %>%
  inner_join(resilience_data, by = c("clean_date" = "day")) %>%
  left_join(
    workout_calories_daily %>% select(day, ActiveThreshold),
    by = c("clean_date" = "day")
  ) %>%
  mutate(
    ActiveThreshold = replace_na(ActiveThreshold, FALSE),
    level = factor(level, levels = c("solid", "adequate", "limited"))
  )

# View a few rows to verify the join
head(combined)
## # A tibble: 6 × 6
##   clean_date avg_bpm id                       contributors level ActiveThreshold
##   <date>       <dbl> <chr>                    <chr>        <fct> <lgl>          
## 1 2025-04-22    72.6 1f7d7552-1741-4e7b-88ef… {daytime_re… adeq… TRUE           
## 2 2025-04-23    72.1 9ff55cfc-1e17-4483-8914… {daytime_re… adeq… FALSE          
## 3 2025-04-24    72.8 d5f0fe7f-78c6-4164-89df… {daytime_re… limi… FALSE          
## 4 2025-04-25    71.2 c71ccfc2-617a-4d35-9f1a… {daytime_re… adeq… TRUE           
## 5 2025-04-26    69.0 ac6bc474-733f-4b78-889f… {daytime_re… adeq… FALSE          
## 6 2025-04-27    70.8 b719a043-3ea3-4451-a3e3… {daytime_re… adeq… FALSE

7. Plot: Heart Rate and Resilience

ggplot(combined, aes(x = clean_date, y = avg_bpm)) +
  geom_point(aes(color = level), size = 2.8, alpha = 0.9) +
  geom_smooth(method = "loess", se = FALSE, color = "black", linewidth = 1.2) +
  geom_point(
    data = subset(combined, ActiveThreshold),
    aes(shape = "Active Burn > 500"),
    size = 4, stroke = 1.1, color = "black", fill = NA
  ) +
  geom_rug(
    data = subset(combined, ActiveThreshold),
    sides = "b",
    length = unit(3, "pt"),
    color = "black",
    alpha = 0.6
  ) +
  scale_color_manual(
    name = "Resilience Level",
    values = c(
      "limited"  = "red",
      "adequate" = "gold",
      "solid"    = "green4"
    )
  ) +
  scale_shape_manual(
    name = "",
    values = c("Active Burn > 500" = 1)
  ) +
  guides(color = guide_legend(order = 1),
         shape = guide_legend(order = 2)) +
  scale_x_date(
    date_breaks = "1 week",
    date_labels = "%b %d"
  ) +
  labs(
    title = "Resting Heart Rate Over Time",
    subtitle = "Colored by resilience level, with >500-calorie days highlighted",
    x = "",
    y = "Average Resting BPM"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "right",
    plot.title = element_text(face = "bold")
  )


8. Interpretation

On days labeled solid in resilience, heart rate tended to be lower.
Spikes appear around periods of sustained activity (>500 calories).
The black LOESS line gives the smoothed overall trend.