I’ve had an Apple Watch since last May, and was recently curious as to whether I could access the heart rate sensor’s data outside of the Health app. It turns out you can export your health data into an XML file and explore it with R.

Step 1: Export your data from Apple’s Health app

You’ll first need to export your health data from Apple’s Health app. First, open the Health app and tap the Health Data icon at the bottom. Tap All and then the Share icon in the top right. Tap Export to confirm that you want to export your data. It might take a few minutes to complete. Once it’s done, you’ll be prompted to share the data. You can use AirDrop to transfer it to your Mac, or simply e-mail it to yourself.

When you’ve saved the file on your computer, extract the ZIP file. Your health data records are stored in the file export.xml. Next, we’ll use R to explore the records.

Step 2: Import your data into R

I’ll be using Hadley Wickham’s xml2 package to import the XML file:

library(pander)
library(magrittr)
library(dplyr)
library(xml2)

records <- read_xml("data/export.xml") %>%
           xml_children

length(records)
[1] 1401269

Over 1.4 million records! In addition to regular heart rate measurements, there are a lot of additional health data records inside the file. Let’s see if we can figure out all the different types of records. We’ll first take a look at just a few:

records[10000:10004]
{xml_nodeset (5)}
[1] <Record type="HKQuantityTypeIdentifierHeartRate" sourceName="Jeff's  ...
[2] <Record type="HKQuantityTypeIdentifierHeartRate" sourceName="Jeff's  ...
[3] <Record type="HKQuantityTypeIdentifierHeartRate" sourceName="Jeff's  ...
[4] <Record type="HKQuantityTypeIdentifierHeartRate" sourceName="Jeff's  ...
[5] <Record type="HKQuantityTypeIdentifierHeartRate" sourceName="Jeff's  ...

It looks like each record has a type attribute, and HKQuantityTypeIdentifierHeartRate corresponds to the heart rate measurements we are after. Let’s see all the different record types and generate a count of each one:

record_types <- xml_attr(records, "type")

record_types %>%
  table(dnn="Record_type") %>%            
  as.data.frame %>%
  arrange(desc(Freq)) %>%
  mutate(Count = prettyNum(Freq, big.mark=",")) %>%
  select(-Freq) %>%
  pander
Record_type Count
HKQuantityTypeIdentifierDistanceWalkingRunning 389,841
HKQuantityTypeIdentifierActiveEnergyBurned 363,752
HKQuantityTypeIdentifierBasalEnergyBurned 277,667
HKQuantityTypeIdentifierStepCount 229,547
HKQuantityTypeIdentifierHeartRate 110,651
HKQuantityTypeIdentifierAppleExerciseTime 19,139
HKCategoryTypeIdentifierAppleStandHour 5,239
HKQuantityTypeIdentifierFlightsClimbed 3,659
HKCategoryTypeIdentifierSleepAnalysis 831
HKQuantityTypeIdentifierHeight 83
HKQuantityTypeIdentifierBodyMass 68
HKQuantityTypeIdentifierBodyFatPercentage 67
HKQuantityTypeIdentifierBodyMassIndex 67

That’s a lot of heart rate measurements! We’ll extract only the heart rate records and then view one of them to see what it contains:

heart_rate_records <- records[which(record_types == "HKQuantityTypeIdentifierHeartRate")]

single_record_attrs <- xml_attrs(heart_rate_records[10000])[[1]]

data_frame(attribute = names(single_record_attrs),
           value     = single_record_attrs) %>%
  pander
attribute value
type HKQuantityTypeIdentifierHeartRate
sourceName Jeff’s Apple Watch
unit count/min
creationDate 2015-05-31 13:14:51 -0500
startDate 2015-05-31 13:14:51 -0500
endDate 2015-05-31 13:14:51 -0500
value 103

We really only care about the date and value for each record, so let’s build a data frame and save it for use in the next step.

heart_rate_df <- data_frame(date       = as.POSIXct(xml_attr(heart_rate_records, "endDate")),
                            heart_rate = as.integer(xml_attr(heart_rate_records, "value")))
saveRDS(heart_rate_df, file="heart_rate_df.rds")

Step 3: Build a Shiny app to plot the data

To visualize the data, we’ll build a simple Shiny app that plots the heart rate data over a specified date range:

library(shiny)
library(ggplot2)
library(dplyr)
library(magrittr)
library(lubridate)

heart_rate_df <- readRDS("heart_rate_df.rds") %>%
                 mutate(date_only = format(date, "%Y-%m-%d"))

ui <- shinyUI(fluidPage(

   titlePanel("My Apple Watch heart rate data"),

   sidebarLayout(
      sidebarPanel(
         dateRangeInput("date_range", "Date range: ",
                        format="mm-dd-yyyy",
                        min=min(as.Date(heart_rate_df$date_only)),
                        max=max(as.Date(heart_rate_df$date_only)),
                        start=max(as.Date(heart_rate_df$date_only)) - days(1),
                        end=max(as.Date(heart_rate_df$date_only))
                        )
      ),
      mainPanel(
        plotOutput("heart_rate_plot"),
        tableOutput("heart_rate_table")
      )
   )
))

server <- shinyServer(function(input, output) {

  heart_rate_ranged <- reactive({
    date_ranges <- input$date_range[1:2] %>% as.Date
    date_start  <- min(date_ranges)
    date_end    <- max(date_ranges)

    heart_rate_df %>%
      filter(date_only >= date_start & date_only <= date_end)
  })

  output$heart_rate_plot <- renderPlot({
      g <- ggplot(heart_rate_ranged(), aes(x=date, y=heart_rate)) +
           geom_line() +
           theme_bw() +
           labs(x="",
                y="Heart rate (bpm)",
                title=paste0("From ", input$date_range[1], " to ", input$date_range[2]))
      g
   })

  output$heart_rate_table <- renderTable({
    heart_rate_ranged() %>%
      summarize(earliest_record    = as.character(min(date)),
                latest_record      = as.character(max(date)),
                highest_heart_rate = max(heart_rate),
                lowest_heart_rate  = min(heart_rate),
                measurements       = prettyNum(n(), big.mark=","))
  })
})

shinyApp(ui = ui, server = server)

Here’s how it looks:

shiny-app-screenshot

A simple RMarkdown document for importing and saving your heart rate data, as well as the Shiny app above, can be found at jeffjjohnston/apple_watch_heart_rate.