VeryFitProの歩数データをヒストグラムでプロットする

概要

苦労した点

  • plt.hist()のrangeパラメータを指定しておらず、バーの幅がまちまちになった
  • 基本的にデータを採取すること自体を忘れていることがある

ややこしい点

plt.hist()は基本的に一次元の配列データをカウントしてくれる。が、端末自体がヒストグラム的に歩数をカウントしてくれている場合は、データを元に戻す必要がある。例えば、07:00~07:15の間に100歩歩いたとすると、"07:00"というデータを100個複製して配列に追加するとplt.hist()が正しく描画してくれる (バーの幅を15分間隔に設定する必要は別途あり、plt.hist()側が行いたいのは、x軸に対応した値=時刻データ、の頻度をカウントするということだから)。ちゃんと調べれば何もしなくとも正しく描画するための機能はあるのかもしれないが関数を書いた。

教訓

基本的にあらゆる物事は数値で観察されるべきだと感じた。数値で表現されないすべてのものは主観(多分)。

ソースコード (下準備 - 日付関係)

# dater.py
import numpy as np
import pandas as pd
from datetime import datetime as dt
from datetime import timedelta as tdelta


def dateobj(s):
    """日付文字列をdateオブジェクトに変換する"""
    return dt.strptime(s, "%Y-%m-%d").date()


def change_hms(ds, hms):
    """日付文字列の時分秒を書き換える"""
    sep = " "
    ymd_hms = ds.split(sep)
    ymd_hms[1] = hms
    return sep.join(ymd_hms)


def tostart(ds):
    """その日付の00:00:00に時刻を設定する"""
    hms = "00:00:00"
    return change_hms(ds, hms)


def toend(ds):
    """その日付の23:59:59に時刻を設定する"""
    hms = "23:59:59"
    return change_hms(ds, hms)


def tostartend(ds):
    """
    その日付の00:00:00と23:59:59の日付文字列のペアを生成する
    """
    start = tostart(ds)
    end = toend(ds)
    return (start, end)


def topercentage(ds):
    """一日の何%が経過したかに変換する"""
    dtobj = datetimeobj(ds)
    sec = dtobj.hour * 60 * 60
    sec += dtobj.minute * 60
    sec += dtobj.second
    total_sec = 86400
    return sec / total_sec


def toseq(ds_from, ds_to):
    """連続する日付文字列を得る"""
    dt_from_to = [dateobj(ds) for ds in (ds_from, ds_to)]
    dt_from = dt_from_to[0]
    dt_to = dt_from_to[1]

    # 1日差
    one_day = tdelta(days=1)

    # ds_fromからds_toまで連番の日付文字列を生成
    dates = [str(dt_from)]
    cur = dt_from
    while dt_from <= cur and cur < dt_to:
        cur = cur + one_day
        dates.append(str(cur))

    return dates


def get_todays(df, ds):
    """指定日時のデータのみを取り出す (VeryFitPro専用)"""

    # 指定日の00:00:00と23:59:59の文字列を取得
    hms = "00:00:00"
    startstr, endstr = tostartend(" ".join([ds, hms]))

    # "start_date"列を取り出す
    dt = pd.to_datetime(df["start_date"])
    mask_from = dt >= startstr
    mask_to = dt <= endstr
    df_todays = df[mask_from & mask_to]

    return df_todays

ソースコード (下準備 - xmlファイルを読み込む)

# data_loader.py
import numpy as np
import pandas as pd
import xml.etree.ElementTree as et


def _get_root_from(fpath):
    """
    xmlファイル (ヘルスケア、VeryFitPro) のルート要素を取得する
    """

    # xmlファイルを読み込む
    f = open(fpath)
    contents = f.read()
    f.close()

    # ルート要素を取得する
    root = et.fromstring(contents)

    return root


def _get_by_type(dtype, root):
    """指定のtype属性を持つタグを全て取得する"""
    records = []
    for child in root:
        if child.get("type") == dtype:
            records.append(child)
    return records


def _remove_gap(ds):
    """+900等の時差データを除外する"""
    parts = ds.split(" ")
    parts = parts[:-1]
    return " ".join(parts)


def _get_records(dtype, root):
    """
    対応したtype属性を持つレコードだけを持つDataFrameを取得する
    """

    # 対応するtypeのレコードをすべて取得
    records = _get_by_type(dtype, root)

    # np.ndarray→pd.DataFrame
    np_records = np.array([
        [
            _remove_gap(r.get("startDate")), 
            _remove_gap(r.get("endDate")), 
            r.get("value")
        ]
        for r in records
    ])
    df_records = pd.DataFrame(np_records)

    # カラムを設定する
    columns = ["start_date", "end_date", "value"]
    df_records.columns = columns

    return df_records


def load_step_count(fpath):
    """歩数に関するデータを読み込む"""

    # ルート要素を取得
    root = _get_root_from(fpath)

    # 対応したtype属性を持つデータを取得 & カラム設定
    dtype = "HKQuantityTypeIdentifierStepCount"
    df_step_count = _get_records(dtype, root)

    return df_step_count

ソースコード (歩数をヒストグラムにプロットする)

import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns

# ※この他に上記のコードをインポートしている


def duplicate(a, b):
    """
    ヒストグラムのプロット用に値を複製する

    pyplotにおけるhist()は値のカウントからを担うため
    歩数分だけ時刻データを複製する必要がある
    """
    vals = []
    for i, v1 in enumerate(a):
        v2 = b[i]
        for n in range(v1):
            vals.append(v2)    # v2 (時刻) がv1 (歩数) 回だけ登場する
    return vals


def plot_hist(datestr, df, subplot=False, plot_counter=None,
    grid=True):
    """ヒストグラムを描画する (plt.show()の直前まで)"""

    # 指定日のデータだけを取り出す
    df_todays = get_todays(df, datestr)

    # 時刻を一日における経過時間 (%) に変換する
    h = 24.0
    to_hour = np.vectorize(lambda ds: topercentage(ds) * h)
    np_hours = to_hour(df_todays["start_date"].values)

    # 歩数データを数値に変換する
    to_i = np.vectorize(lambda step: int(step))
    step_count = to_i(df_todays["value"].values)

    # pyplotのヒストグラムでは値の頻度をカウントするので
    # 歩数だけの時刻データを一次元配列に追加していく必要がある
    dup_hours = duplicate(step_count.tolist(), np_hours.tolist())

    # plt.subplot()を使う場合 (plt.show()はここでは呼び出さない)
    if subplot:
        plt.subplot(4, 2, plot_counter)    # ※最大8日間とする

    # グリッドを表示する
    if grid:
        plt.grid(True, alpha=0.4)

    # 目盛りの間隔を指定する
    plt.xticks(np.arange(0.0, 24.0, 1.0))

    # ヒストグラムを描画する
    # 15分間隔なので24時間で96区分
    plt.hist(dup_hours, bins=96, range=(0.0, 24.0))

    # 値の範囲を設定する
    plt.xlim((0, 24))    # 0時~24時
    plt.ylim((0, 1000))    # ※15分当たりの最大歩数 (目安) でよい

    # 軸のラベルを設定する
    plt.xlabel("hour")
    plt.ylabel("step count")

    # タイトルとして日付を表示する (括弧内はトータルの歩数)
    step_count_ttl = np.sum(step_count)
    title = "".join([datestr, " (", str(step_count_ttl), ")"])
    plt.title(title)

    plt.tight_layout()


def plot_step_counts(fpath):
    """指定範囲の日時の歩数をプロットする"""

    # 歩数データを読み込む
    df_step_count = load_step_count(fpath)

    # 日付の範囲を指定する (※plt.subplot()の関係で8日分まで)
    datestr_from = "2017-07-28"
    datestr_to = "2017-08-03"

    # 連番の日付文字列を取得する
    datestrs = toseq(datestr_from, datestr_to)

    # 歩数をヒストグラムでプロットする
    plot_steps = True
    if plot_steps:
        for i, ds in enumerate(datestrs):
            plot_hist(ds, df_step_count, 
                subplot=True, plot_counter=i+1)

        plt.tight_layout()
        plt.show()


if __name__ == '__main__':

    # データファイルのパス
    fpath = "../data/ios_health_care/20170803.xml"

    # 指定範囲の日時の歩数をプロットする
    plot_step_counts(fpath)

動作結果

f:id:zdassen:20170803180807p:plain