
【身近なデータ分析】Apple Watchの心拍数データをR言語で分析してみた~後編~
「身近なデータ分析」シリーズ。このシリーズは、データサイエンスに関心を持ちプログラミング言語をある程度学習した方が、実際にデータを分析してみようと思った際に、その参考例となりうることを目指したものです。
前編では、今回検証する仮説「睡眠時間と翌日の心拍数には負の相関関係がある」の概要、そしてApple Watch で記録されたデータの取り込み方・簡単な可視化に関して紹介しましたが(【身近なデータ分析】Apple Watchの心拍数データをR言語で分析してみた~前編~)、後編では、特徴的なデータの加工に関する手順と分析結果に関してお話しできればと思います。
睡眠時間データの加工
睡眠時間のデータ(HKCategoryTypeIdentifierSleepAnalysis)に関しては、そのままでは非常に扱いにくいものになっています。
このデータの主要なカラムを抜粋すると、睡眠がスタートした時のタイムスタンプと睡眠が終わった時のタイムスタンプの2列になりますが、日単位で集計されたものではありません。また、睡眠は夜0時をまたぐ場合も多いため、単純に日ごとの集計をしてしまうと、夜ごとの睡眠時間の集計とはならないのです。
加工前のテーブルイメージ(sleep_base)

※Apple ヘルスケアデータでは、標準のカラム名がstartDate, endDateとなっている一方、データ型はyyyy/mm/dd hh:mm:ss となっています。このため、本記事のテーブルイメージでは、カラム名をそれぞれstartDate -> start_time、endDate -> end_timeに変換しています。
そこで1日の境界線を自ら新たに設定し、起床した日付を基準にすべてのデータを紐づけることで、日別に睡眠時間を集計できるように加工します。今回は17時00分を境界線に設定しています(このあたりは各自のライフサイクルによって変化させるのがよい項目です)。変換後のテーブルイメージは以下の通りです。
変換後テーブルイメージ(sleep_set_day)

こうした日付データの加工に便利なR言語のパッケージがlubridateです。
lubridateの各関数を用いることで、タイムスタンプから取得したい単位のデータを持ってくることができます。例えば上記の加工において、ある時点(timestamp)が17時00よりも前であるかを判定するためには、hour(timestamp) < 17 とすればよいです。
またlubridateでは、日付の足し算・引き算を比較的簡単な記述でおこなうことができます。例えば、ある時点のちょうど1日後のタイムスタンプを取得したければ、timestamp + days(1) とすればよいです。
##境界に設定した時間から24時までの睡眠に対して翌日の日付を、0時から境界に設定した時間までの睡眠に対して当日の日付を紐づける
boundary_hour <- 17
sleep_set_day <- sleep_base %>%
dplyr::mutate(start_time_for_check = if_else(hour(start_time) < boundary_hour,
as_date(start_time), as_date(start_time + days(1)))) %>%
dplyr::mutate(end_time_for_check = if_else(hour(end_time) < boundary_hour,
as_date(end_time), as_date(end_time + days(1)))) %>%
dplyr::filter(start_time_for_check == end_time_for_check) %>% #境界線を跨ぐイレギュラーな睡眠データは除外する
dplyr::rename(wakeup_day = end_time_for_check) %>%
dplyr::select(c('start_time','end_time','wakeup_day'))
起床した日付を基準にすべての睡眠時間のデータを紐づけることができれば、あとはそれぞれの行単位で睡眠時間を計算して(これは、lubridateパッケージのtime_length()関数を用います)、日別に合計すれば、一夜ごとの睡眠時間を算出することができます。
##睡眠時間データを日別のデータに改変する
sleep_by_day <- sleep_set_day %>%
dplyr::mutate(sleep_seconds = time_length(interval(start = start_time, end = end_time),unit = "second")) %>%
dplyr::group_by(wakeup_day) %>%
dplyr::summarise(sleep_sum = sum(sleep_seconds), fall_time = min(start_time), wake_time = max(end_time), .groups = "drop")
エクササイズデータと心拍数データの加工
心拍数は運動中や運動を終えた直後には上昇し、その日の運動量は日平均の心拍数に大きな影響を与えると考えられます。しかしながら、旅行やお出かけなどで活動量が多い日は必然的に早起きになっているという可能性を考えて、今回は安静時の心拍数のみを検証の対象としたいと思います。計測時のラグも考慮して、Apple Watchでエクササイズが記録された時間の前後5分の心拍数データを除外する処理をおこないます。
エクササイズ時間 HKQuantityIdentifierAppleExerciseTime に関しても、睡眠時間と同様主要なカラムを抜粋すると、基本的に開始のタイムスタンプと終了のタイムスタンプの2列となります。ただし、睡眠時間のデータとは異なり1分間で1レコードが生成される仕様になっています。ひとつのエクササイズ単位で1レコードを持つテーブルにするには、少々加工が必要です(※加工の詳細は紙面の都合上割愛します)。今回は、加工後のテーブルを例に挙げて説明していきます。
加工前のテーブルイメージ

エクササイズ単位で1レコードとなった加工後のテーブルイメージ(exer_base)

また、心拍数のデータ(HKQuantityTypeIdentifierHeartRate)にて用いるのは、記録された時点のカラム(startDate=endDate)と値を表現するカラム(value)の2列です。
心拍数データのテーブルイメージ(hr_base)

※カラム名をstartDate -> record_timeに変換しています。
今回は、エクササイズ前後の時間帯に該当する心拍数のデータを除外することが目的であるため、心拍数のデータ1つ1つが、それぞれエクササイズのデータにある区間±5分の中にあるかどうかを判定できればよいことになります。R言語のlubridateパッケージでは、こうしたある時点がインターバルの中にあるか外にあるかの判定についても容易におこなうことができます。
具体的な処理としては、下の表のように、心拍数のデータに対してその日に記録されたエクササイズのデータをすべて紐づけたのちに、record_time %within% interval(ex_start_time, ex_end_time)とすれば、インターバル判定が可能です(record_timeがインターバル内にあれば1を返します)。
紐づけ後のテーブルイメージ(hr_add_flag)

##誤差として考慮する時間分をエクササイズ前後に加えておく
exer_exclude_minutes <- 5
exer_for_join <- exer_base %>%
dplyr::mutate(ex_start_time = start_time - minutes(exer_exclude_minutes), ex_end_time = end_time + minutes(exer_exclude_minutes)) %>%
dplyr::mutate(record_date = as_date(ex_start_time)) %>% #日を跨いでエクササイズしているデータがある場合、別途処理が必要
dplyr::select(c('record_date','ex_start_time','ex_end_time'))
hr_add_flag <- hr_base %>%
dplyr::mutate(record_date = as_date(record_time)) %>%
dplyr::left_join(exer_for_join, by='record_date') %>% #日を跨いでエクササイズしているデータがある場合、別途処理が必要
dplyr::mutate(flag = if_else(record_time %within% interval(ex_start_time,ex_end_time),1,0)) %>%
tidyr::replace_na(list(flag=0)) #エクササイズがまったく記録されていない日
時点ごとに重複したデータとなっているため、最後に、時点ごとにflagの最大値をとります。
##誤差として考慮する時間分をエクササイズ前後に加えておく
hr_add_flag_agg <- hr_add_flag %>%
dplyr::group_by(record_date,record_time, value) %>%
dplyr::summarise(flag = max(flag), .groups = 'drop')
前編の最後でお見せした心拍数のプロットを、エクササイズの有無で色分けすると下の図のようになります。エクササイズ時間中に記録されているデータは当然ながら心拍数が高い傾向にあることがわかります。※このあとの分析には、「運動時以外」のデータのみを使用します。

仮説の検証
前編で述べた通り、今回の仮説は「睡眠時間が短い日の翌日は心拍数が高い傾向にある」=「睡眠時間と翌日の心拍数には負の相関関係がある」というものです。データがそろったので実際に確かめていくことにします。横軸に睡眠時間、縦軸に起床後6時間までの安静時平均心拍数をプロットすると以下のようになります。

グラフで右下がりの傾向が見られると仮説を支持する形になります。しかしながら、その傾向は見られないようです。
念のため、関連の程度を定量的に表現することができる指標である相関係数も算出しておきます。値の絶対値が関連の大きさを表すのですが(絶対値の最小値0 / 最大値1)、 -0.02という値でした。この結果から仮説が支持されたと主張するのは難しそうです。
今回のAppleWatchの心拍数データから、睡眠時間が短くなった次の日の影響を読み取ることはできませんでした。しかしながら、次の日の胸のドキドキ感が気のせいだとは少し考えにくく、今回は不正確なデータが多かったからではないかと私は考えています。具体的な検討は次の章にて説明しています。
結果を得たあと何をするか
実際に案件などでデータを分析する際は、一度相関係数を出してそれで終わりということはほとんどなく、データやロジックの問題を改善したうえで再分析を実施したり、探索的に追加で分析をおこなったりすることが普通です。探索の内容によっては、追加でデータを用意する必要も生じます。
具体的な検討内容としては、
データにノイズが含まれていないか、想定外のデータが分析に含まれていないか
考慮する他の要因の影響を除けていないなど、ロジックに問題がないか
他の分析手法を使えないか
などが挙げられます。
例えば、睡眠時間のデータを改めて眺めてみると、どうやら二度寝の就寝時間がかなり遅い場合にはその二度目の睡眠がデータとして記録されないことがあるらしいことがわかりました。したがって、実際は睡眠中であるにもかかわらず、データ上は起床後として扱われた心拍数データが存在する可能性があります。今回は時間と紙面の都合上割愛しますが、どの日がそのケースに該当したかを他のデータを用いるなどして特定し、そうしたデータを除外するなどの加工をおこなえば、結果が変わる可能性があります。もっとも、今回に限らず実際のデータには多くのデータが含まれていることが往々にしてあるため、それらをひとつひとつ処理するための地道な作業が、データ分析には必要不可欠といえるでしょう。
以上のように、問題を見つけたとしてもすべてを解決できるとは限りませんが、可能な限り対処し、より分析を精緻化していくことが求められます。
まとめ
今回は、Apple Watchのヘルスケアデータを用いて、データの読み込みから結果の考察までという分析の一連の流れを説明しました。睡眠時間や心拍数といったスマートウォッチのデータは、身近なデータでもあり、実際の分析に挑戦するには絶好の題材であると思います。こちらの記事を参考に、データ分析を楽しんでいただければ幸いです。
(書き手:T.K.)
少しでもお役に立てましたら「スキ♡」を押していただけると励みになります!
▼コーポレートサイトはこちら
▼リクルートサイトはこちら