STRAVAアプリで計測したライドデータとFitbitが計測したライドデータの比較 その1

こんにちは、ライドログ保存が趣味のTakum!です。

以前も書きかけて深追いできずにいた、気になる問題。

a.STRAVAアプリで計測したデータ と

b.Fitbitで計測し、STRAVAに連携されたデータ 

の距離がずいぶんと異なることです。本当は、心拍数も記録されたFitbitのデータを保存したいところなのですが、移動距離が短く出てしまうため、STRAVAのデータの方を保存しています。(Fitbitデータは、二重カウントにならないように、削除してます。)

今回こそ、その真相に迫ろうと思います。



分析するデータ3つ


 対象のライドは、2019/1/31の自宅から市内の駐輪場までの約11km。
 STRAVAを起動したスマホをポケットに入れ、自転車に跨り、Fitbitのbikeをスタートさせて11km走り、駐輪場に自転車を停め、歩きながらFitbitの記録を止め、STRAVAの記録を止めました。(なので、歩いている距離分、STRAVAが多少大きく出るはずです。)
 ライドの結果は、
a. STRAVAアプリ:距離11.03km タイム34:45 高度121m
b. Fitbitアプリ:距離9.39km タイム42:01 高度53m
となっており、その距離の誤差はちょっと看過できません。
ちなみに、STRAVAアプリは自動停止がON(停止中はログ保存しない)となっているため、タイムは信号待ち等で停止していた時間を除いた移動中の時間ということになります。FitbitからSTRAVAにデータ連携した後のデータがどうにか加工されているかもしれないので、連携されたデータをSTRAVAでエクスポートしたファイル(c)も含めて、検証してみたいと思います。

データの形式


まずはエクスポートされるXMLデータのフォーマットを見てみましょう。
a. STRAVAからエクスポートされるのは、GPX形式で、
<trkpt lat="緯度" lon="経度">
    <ele>標高</ele>
    <time>2019-01-30T23:22:43Z</time>
</trkpt>

のようになっています。時間は協定世界時の形式です。日本の時間に直すには、+9時間の補正が必要になります。

b. Fitbitからエクスポートされるのは、TCX形式で、
<Trackpoint>
    <Time>2019-01-31T08:22:39.000+09:00</Time>
    <Position>
        <LatitudeDegrees>緯度</LatitudeDegrees>
        <LongitudeDegrees>経度</LongitudeDegrees>
    </Position>
    <AltitudeMeters>標高</AltitudeMeters>
    <DistanceMeters>移動距離累積値</DistanceMeters>
    <HeartRateBpm>
        <Value>心拍数</Value>
    </HeartRateBpm>
</Trackpoint>
のようになっています。こちらは日本時間で記録されていますね。心拍数と、移動距離の累積値が付いています。これで何かわかりそうです。

GPXファイルをDataFrameに変換


それでは、これらのデータ、XMLでは扱いにくいので、Pythonに取り込んでみたいと思います。JupyterNotebookにて作業をしていきます。

import pandas as pd
import numpy as np
from bs4 import BeautifulSoup
import datetime

with open('Stravaで記録したGPX.gpx', 'r', encoding='utf-8') as xml_file:
    soup = BeautifulSoup(xml_file, 'xml')

trackpoints = soup.find_all('trkpt')
len(trackpoints)

1732行のデータであることが分かりました。
ここから、DataFrameに変換していきます。

points = []

for trackpoint in trackpoints:
    timestamp = trackpoint.find('time').text
    latitude  = trackpoint.attrs['lat']
    longitude = trackpoint.attrs['lon']
    altitude  = trackpoint.find('ele').text
    point = {
        'str_time': timestamp,
        'str_lat': latitude,
        'str_lng': longitude,
        'str_alt': altitude,
    }
    points.append(point)

df = pd.DataFrame(points)
df.tail()

str_altstr_latstr_lngstr_time
172752.838.2599200140.87570302019-01-30T23:22:38Z
172852.838.2599450140.87572802019-01-30T23:22:39Z
172953.038.2599800140.87576602019-01-30T23:22:41Z
173053.038.2600030140.87579302019-01-30T23:22:43Z
173153.138.2600330140.87580002019-01-30T23:22:48Z

tail()なのは、head()だと自宅の座標が出てしまうからです(-_-;)
各値は数値に見えますが、df.dtypes を見ると、object型になっているので、それぞれ変換していきます。時刻もこの時点で変換してしまいましょう。

df.loc[:, 'str_日時'] = df.loc[:, 'str_time'].apply(pd.to_datetime)+datetime.timedelta(hours=9)
df.loc[:, "str_alt"] = df.loc[:, "str_alt"].astype(np.float32)
df.loc[:, "str_lat"] = df.loc[:, "str_lat"].astype(np.float32)
df.loc[:, "str_lng"] = df.loc[:, "str_lng"].astype(np.float32)



str_altstr_latstr_lngstr_timestr_日時
172752.79999938.259918140.8757022019-01-30T23:22:38Z2019-01-31 08:22:38
172852.79999938.259945140.8757322019-01-30T23:22:39Z2019-01-31 08:22:39
172953.00000038.259979140.8757632019-01-30T23:22:41Z2019-01-31 08:22:41
173053.00000038.260002140.8757932019-01-30T23:22:43Z2019-01-31 08:22:43
173153.09999838.260033140.8757932019-01-30T23:22:48Z2019-01-31 08:22:48


インデックスを、str_日時に変更して、str_timeは削除してしまいます。

df = df.set_index('str_日時')
df = df.drop('str_time', axis=1)
df.tail()


str_altstr_latstr_lng
str_日時
2019-01-31 08:22:3852.79999938.259918140.875702
2019-01-31 08:22:3952.79999938.259945140.875732
2019-01-31 08:22:4153.00000038.259979140.875763
2019-01-31 08:22:4353.00000038.260002140.875793
2019-01-31 08:22:4853.09999838.260033140.875793

一旦完成です。pickleファイルとして保存します。

df.to_pickle('strava.pickle')

同様に、fitbitから自動連携してSTRAVAにアップされたGPXファイルもDataFrameに変換し、pickleとして保存します。
※心拍も連携されていますので、gpxtpx:hrによって値が取れます。




TCXファイルからDataFrameに変換


次は、Fitbitから直接ダウンロードしたTCXファイルをDataFrameに変換していきます。

import pandas as pd
import numpy as np
from bs4 import BeautifulSoup
import datetime

with open('fitbitで記録したTCX.tcx', 'r', encoding='utf-8') as xml_file:
    soup = BeautifulSoup(xml_file, 'xml')

trackpoints = soup.find_all('Trackpoint')

points = []

for trackpoint in trackpoints:
    timestamp = trackpoint.find('Time').text
    latitude  = trackpoint.find('LatitudeDegrees').text
    longitude = trackpoint.find('LongitudeDegrees').text
    altitude  = trackpoint.find('AltitudeMeters').text
    distance  = trackpoint.find('DistanceMeters').text
    heartrate = trackpoint.find('Value').text
    point = {
        'fit_time': timestamp,
        'fit_lat': latitude,
        'fit_lng': longitude,
        'fit_alt': altitude,
        'fit_dist': distance,
        'fit_hr': heartrate
    }
    points.append(point)

df = pd.DataFrame(points)
df.tail()


fit_altfit_distfit_hrfit_latfit_lngfit_time
239157.59388.57000000000211738.25991249084473140.875702500343322019-01-31T08:22:35.000+09:00
239258.09389.8611738.25991106033325140.87569689750672019-01-31T08:22:36.000+09:00
239358.49391.22000000000111638.25992226600647140.87570631504062019-01-31T08:22:37.000+09:00
239458.559392.6711538.25993084907532140.875715136528022019-01-31T08:22:38.000+09:00
239558.79394.2311538.25993740558624140.87572228908542019-01-31T08:22:39.000+09:00

同様に、データ型を変換し、日時をインデックスに変換します。

df.loc[:, 'fit_日時'] = df.loc[:, 'fit_time'].apply(pd.to_datetime)+datetime.timedelta(hours=9)
df.loc[:, "fit_alt"] = df.loc[:, "fit_alt"].astype(np.float32)
df.loc[:, "fit_lat"] = df.loc[:, "fit_lat"].astype(np.float32)
df.loc[:, "fit_lng"] = df.loc[:, "fit_lng"].astype(np.float32)
df.loc[:, "fit_dist"] = df.loc[:, "fit_dist"].astype(np.float32)
df.loc[:, "fit_hr"] = df.loc[:, "fit_hr"].astype(np.int)

df = df.set_index('fit_日時')
df = df.drop('fit_time', axis=1)
df.tail()

fit_altfit_distfit_hrfit_latfit_lng
fit_日時
2019-01-31 08:22:3557.5000009388.57031211738.259911140.875702
2019-01-31 08:22:3658.0000009389.86035211738.259911140.875702
2019-01-31 08:22:3758.4000029391.21972711638.259922140.875702
2019-01-31 08:22:3858.5499999392.66992211538.259930140.875717
2019-01-31 08:22:3958.7000019394.23046911538.259937140.875717


df.to_pickle('fitbit.pickle')

これで、分析する3つのpickleがそろいました。

続きのエントリー

コメント

このブログの人気の投稿

MS Azure Information Protection を入れたら右クリックの「分類して保護する」がうざい

Zwiftがいきなり楽になってしまった件(顛末)