LoginSignup
12
6

More than 1 year has passed since last update.

Apple Watch で計測したランニングの記録から機械学習で目標タイムを設定してみた

Last updated at Posted at 2021-12-17

はじめに

はじめまして、NTTドコモサービスイノベーション部1年目の内村です。
普段の業務ではドコモのネットワーク品質向上のため、ネットワーク分析業務に取組んでいます。

突然ですが、皆さんの中にもリモートワーク、オンライン授業を受けることが多く、運動不足を感じておられる方も多いのではないでしょうか。私も在宅ワークをする機会が多く部屋から出ない日もあり、運動不足を感じていました。このままではまずいと、6月からランニングを始めたのですが、最近寒くなってきたこともありただ走るだけではモチベーションが上がらない…

そこで今回はランニングをする目的作りとモチベーションアップのため、Apple Watchで記録したこれまでのランニングデータを元に自分のランニングの目標タイム設定を行ってみたいと思います。

image-20211204155040968.png

それでは、NTTドコモ R&D AdventCalendar2021(カレンダー2) 18日目楽しんで読んでいただければ幸いです。

本記事で扱う内容

  • IOS端末からヘルスケアデータの抽出と学習データの準備
  • タイムを推定するモデルの作成
  • 400mごとのタイム推定

本記事の環境

  • Apple Watch series 3 (ver 7.3)
  • iPhone 8 (ios 14.8.1)
  • Google colab

ヘルスケアデータの準備

ヘルスケアデータの取得

最初にヘルスケアデータをiPhoneから取り出します。
エクスポート自体はとても簡単でiPhoneのヘルスケアアプリから「すべてのヘルスケアデータを書き出す」をタップすることで、任意の方法で取り出すことができます。

ヘルスケアデータの構成は、下記のようになっています。「ecport.xml」には、心拍数や歩数、移動距離などあらゆるログが保存されています。「route_yyyy-MM-dd_h.mmtt.gpx」には、ランニングやハイキングなど運動中に移動した距離が、ワークアウトごとに保存されています。

apple_health_export
 ├ export.xml
 ├ export_cda.xml
 ├ /workout-routes
 ├ route_2021-06-01_7.05pm.gpx
 ├ route_2021-06-05_11.02am.gpx
 ├ ・・・・・

ヘルスケアデータの確認

export.xmlのデータは以下の形で読み込みを行います。

import pandas as pd
import xmltodict

input_path = './apple_health_export/export.xml'
with open(input_path, 'r') as xml_file:
    input_data = xmltodict.parse(xml_file.read())

records_list = input_data['HealthData']['Record']
df = pd.DataFrame(records_list)
df['@type'].unique()

実際に確認してみると、心拍数、歩数など多種多様なログが残されているのが分かります。
公式documentを調べると各@typeの説明がすぐ見つかります。試しに'HKQuantityTypeIdentifierHeartRate'を調べたところ計測値は心拍数でした。また、運動中かどうかでも心拍数を記録する@typeを分けているようです。参考にヘルスケアデータから確認できるデータの例を載せておきます。

@type 計測値
'HKQuantityTypeIdentifierHeartRate' 心拍数
'HKQuantityTypeIdentifierStepCount' 歩数
'HKQuantityTypeIdentifierWalkingSpeed' 歩行速度

ランニング中の心拍数データが見つからなかったため、今回はワークアウトデータのルートのみを利用することにします。

ワークアウトデータの確認

route_yyyy-MM-dd_h.mmtt.gpxのデータは緯度、経度、標高、時間をレコードの要素としてランニングなどのワークアウト中、ログを取り続けていようです。記事を参考に下記のコードで.gpxファイルのログを確認してみると、ほとんど毎秒ログを取っているようです。また、高さも数センチ精度で取れているようで驚きです。

import gpxpy
import gpxpy.gpx
from pytz import timezone

# タイムゾーン
dt_tz = timezone('Asia/Tokyo')
# 日時文字列形式
dt_fmt = '%Y-%m-%d %H:%M:%S'
def gpx2df(path_gpx):
  # 配列の初期化
  DateTime = []
  Lat      = []
  Lng      = []
  Alt      = []
  # GPXファイルの読み込み
  gpx_file_r = open(path_gpx, 'r')
  # GPXファイルのパース
  gpx = gpxpy.parse(gpx_file_r)
  # GPXデータの読み込み
  for track in gpx.tracks:
      for segment in track.segments:
          # ポイントデータリストの読み込み
          points = segment.points
          # ポイントデータの長さ
          N = len(points)
          # ポイントデータの読み込み
          for i in range(N):
              # ポイントデータ
              point = points[i]
              # データ抽出
              datetime = point.time.astimezone(dt_tz).strftime(dt_fmt)
              lat      = point.latitude
              lng      = point.longitude
              alt      = point.elevation
              # データ代入
              DateTime.append(datetime)
              Lat.append(lat)
              Lng.append(lng)
              Alt.append(alt)
  df = pd.DataFrame({ "datetime": DateTime, "lat":Lat, "lng":Lng, "alt":Alt})
  return df

ワークアウトデータの一部抜粋

id 時間 緯度[度] 経度[度] 標高[m]
2800 2021-09-26 10:28:26 35.335565 139.632768 3.72
2801 2021-09-26 10:28:27 35.335578 139.632793 3.83
2802 2021-09-26 10:28:28 35.335592 139.632817 3.94

実際にログをFolium(pythonの地図表示ライブラリ)で確認すると、以下のようにどの地点を通過したか簡単に分かります。こうして見るとGPSの精度は素晴らしいですね、通ったルートがほぼ正確に記録されています。

import folium
path = './apple_health_export/workout-routes/route_2021-09-26_10.54am.gpx'.format(path_dir)
df = gpx2df_2(path)
m = folium.Map()
locs = df.loc[:,["lat", "lng"]].values
folium.PolyLine(locs, color="red", weight=7).add_to(m)
m.fit_bounds([[df["lat"].min(),df["lng"].min()], [df["lat"].max(), df["lng"].min()]])
m

image-20211206081306469.png

ワークアウトデータの整形

ここまでで、ランニング中の各地点での時刻と緯度・経度の抽出までは行えましたが、学習データとしては緯度・経度から移動距離に変換する必要があります。移動距離は1つ前のレコードの緯度・経度との差分を取ることで得ることができます。また、高低差に関しても同様に差分を計算することで求めることができます。

from geopy.distance import geodesic

def latlon2m(row):
  # (緯度, 経度)
  if row.isnull().sum() < 1:
    pos_1 = (row["lat"], row["lng"])
    pos_2 = (row["lat_old"], row["lng_old"])
    dis = geodesic(pos_1, pos_2).m
  else:
    dis = 0
  return dis

def make_feature(df):
  #lat lng alt datetimeから学習用のデータセットを作成する
  df['datetime'] = pd.to_datetime(df['datetime'], format='%Y-%m-%d %H:%M:%S')
  #1.1レコード前のデータを結合
  df_ = df.shift().add_suffix("_old")
  df = pd.concat([df, df_], axis=1)
  #2.距離・高低を計算
  df["distance"] = df.apply(latlon2m, axis=1)
  df["alt_diff"] = df["alt"] - df["alt_old"]
  df["dist_total"] = df["distance"].cumsum()
  return df

ワークアウトデータから作成した特徴量の一部抜粋

id 時間 進んだ距離[m] 高低差[m] 総距離[m]
2800 2021-09-26 10:28:26 2.76 0.11 7668.3
2801 2021-09-26 10:28:27 2.69 0.11 7671.0
2802 2021-09-26 10:28:28 2.67 0.10 7673.7

学習データの準備

私の約半年間ランニングをしたログの数を確認すると、全部で58となります。
運動が足りてないせいで、データも不足してしまっています…。

無いものは仕方ないので、図のように各ワークアウトのログを400mごとに分割して集計を行い特徴量とし、400m進むのにどれだけの時間がかかるのかを推定します。

image-20211204160951548.png

def make_dataset(df):
  df = make_feature(df)
  #3.レコードをまとめて学習用データセットにする
  i=0
  df_dataset = pd.DataFrame()
  Dist = []
  Time = []
  Alt_up = []
  Alt_dwn = []
  DateTime = []
  Dist_total = []
  while True:
    if (df.iloc[-1]["dist_total"])>=(i+1)*400:
      df_ = df[(df["dist_total"]>=i*400) & (df["dist_total"]<(i+1)*400)]
      if len(df_) > 1:
        Time.append((df_["datetime"].iloc[-1] - df_["datetime"].iloc[0]).total_seconds())
        Alt_up.append(df_[df_["alt_diff"]>0]["alt_diff"].sum())
        Alt_dwn.append(df_[df_["alt_diff"]<0]["alt_diff"].sum())
        DateTime.append(pd.to_datetime(df_['datetime'], format='%Y-%m-%d %H:%M:%S').iloc[0])
        Dist_total.append(df_.iloc[0]["dist_total"])
        Dist.append(df_["distance"].sum())
      i += 1
    else:
      break
  df_dataset = pd.DataFrame({ "time": Time, "dist":Dist, "dist_total":Dist_total,"alt_up":Alt_up, "alt_dwn":Alt_dwn, "datetime":DateTime})
  return df_dataset

作成した特徴量と400m走るのにかかった時間の抜粋

id 時間[s] 進んだ距離[m] 総距離[m] 登り累計[m] 下り累計[m]
5 128.0 308.8 2001.1 0.77 -10.81
6 136.0 399.4 2402.9 0.18 -5.17
7 152.0 399.2 2802.6 7.20 -11.54

ここでは割愛しますが、GPSの誤差が大きいデータや時間、速度が明らかにおかしいデータの削除と学習に使えそうな特徴量の追加を行いました。以下が今回準備した特徴量を一部抜粋した散布図です。

ダウンロード (1).png

time:400m走るのにかかった時間[s]、dist_total:その日走った総距離[m]、alt_up:400m中の上り累計[m]、alt_dwn:400m中の下り累計[m]、month:月

400m走るのに必要な時間の中央値は131秒となりました。1km走るのに5分半程度かかることになるので、感覚的にも妥当な結果になっています。高低差を示す"alt_up"、"alt_dwn"を見ると、値の大きいところでは40m以上の上り下りの区間があることが分かります。(この登りがいつも辛い)。そして、半年間走り続けてもペースに大きな変化が無く、6月の平均が131秒、11月の平均が130秒だったのは残念なポイントでした。

まだまだ、分析を続けたいところですが、時間も無いので最後のモデル作成に進みます。

タイムの推定

これまでのところで予測に使えるデータの準備はできたので、推定モデルをサクッと準備して精度を確認したいと思います。本来なら時系列モデルを扱いランニングの途中経過を踏まえた予測をしたいのですが、諸事情(アプリを実装する時間・技術が無かった&予測した後に実際に走って精度を確かめたい)のため、今回は走る直前までに利用可能な特徴量を元にLightGBMで予測することを前提とします。

モデル作成

from sklearn.model_selection import train_test_split

#データの準備
l_test = (df_dataset["month"]==11) & (df_dataset["day"] == 17)
X_test = df_dataset[l_test].loc[:,l_feature].values
X_train = df_dataset[~l_test].loc[:,l_feature].values
y_test = df_dataset[l_test]["time"].values
y_train = df_dataset[~l_test]["time"].values
X_train, X_eval, y_train, y_eval = train_test_split(X_train, y_train, test_size=0.2, shuffle=True)

#モデルの学習
import lightgbm

params = {"metric": "rmse", "learning_rate": 0.1, "num_boost_round":10000,
          "early_stopping_rounds":100, "verbose_eval":50}
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_eval, y_eval)
gbm = lgb.train(params, lgb_train, valid_sets=lgb_eval)
y_predict = gbm.predict(X_test)

推定結果

モデルを作成・学習した後、実際に走ってみたところ、誤差は8.90秒(誤差率6.8%)でした。1kmあたりに直すと22秒の誤差となるため、あまり良い精度とはいえない結果となってしまいました。

最後にlightgbmのモデルがタイムを予測する際にどの特徴量を重視したのか確認したいと思います。やはりどれだけ上り下りががあるか、次いで距離[m]、それまでの総距離[m]が重要指標となりました。分析からも分かっていたことですが日付[月]の重要度はやはり低く、推定に大きく影響しないようです…

fti = gbm.feature_importances_
df = pd.DataFrame(fti, index = l_feature)
df
特徴量 重要度
上り累計[m] 216
下り累計[m] 216
進んだ距離[m] 192
総距離[m] 184
日付[月] 32

まとめ

今回は、Apple Watchで計測したヘルスケアデータを使いランニングの目標タイム設定にチャレンジしてみました。

程々の精度は出るだろうと思っていたのですが、その日ごとのコンディションによってタイムにばらつきがあるようで精度としてはいまいちな結果となりました。今回はワークアウトのルートデータしか活用しませんでしたが、睡眠時間や心拍数などを活用することで良い精度やコンディションの分析まで出来るのかなと思います。

個人的には初めてヘルスケアデータに触り測定項目や精度を知ることができ、楽しく分析できました。本当はApple Watchのアプリまで実装して表示しつつ走るところまでしたかったのですが、時間と技術力が足らず断念してしまいした。来年機会があれば、再チャレンジしたいと思います!!

ここまで読んでいただきありがとうございました。25日までアドベントカレンダーは続くので、引き続き楽しんでください。

では、みなさま良いクリスマスを🎄🎄🎄

参考

Apple Developer Documentation

Python3.7によるGPXファイルの読み込みと移動距離の算出 - Qiita

Folium — Folium 0.12.1 documentation

Welcome to LightGBM’s documentation! — LightGBM 3.3.1.99 documentation

12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6