体重と体脂肪率の記録からローソク足チャートを作成する

毎日体組成計に乗って体重と体脂肪率を記録してデータが蓄積されてきたので、自分の成果を客観的に分析できるようにローソク足チャートにしてみます。チャートから傾向を見れるようになってモチベーションが上がると嬉しいです。

ローソク足チャートの作り方を確認する

様々なチャートを作成できるライブラリのbokeh(https://docs.bokeh.org/en/latest/docs/gallery.html)を使用してローソク足チャートを作成します。まずはどのようなデータが必要になるのかサンプルコードを見て使用方法を確認します。サンプルコードはこちら(https://docs.bokeh.org/en/latest/docs/gallery/candlestick.html)を確認します。

サンプルコードにあるMSFTのデータについて確認します。以下のようなcsv形式のデータになっていました。

Date,Open,High,Low,Close,Volume,Adj Close
2000-03-01,89.62,94.09,88.94,90.81,106889800,33.68
2000-03-02,91.81,95.37,91.12,93.37,106932600,34.63
2000-03-03,94.75,98.87,93.87,96.12,101435200,35.65
2000-03-06,96.0,97.37,90.12,90.62,93609400,33.61
2000-03-07,96.12,97.5,91.94,92.87,135061000,34.45
2000-03-08,93.81,96.19,91.0,95.56,94290000,35.44

Date/Open/High/Low/Closeのデータが必要ということが確認できました。体重・体脂肪率のデータは計測した日時と合わせて体重・体脂肪率になっているので、ローソク足チャートが作れるデータに変換する必要があります。

データを整形・変換する

計測したデータは日々シンプル・ダイエットというアプリに記録しています。このアプリはGoogleドライブにデータをエクスポートすることができます。データは以下のようなcsv形式になっています。

date,weight,bodyfat
2020/07/01 5:38:15,60.4,17
2020/07/02 5:13:11,61.2,16.4
2020/07/03 5:19:42,60.3,19.3
2020/07/03 5:21:49,60.4,18.4
2020/07/04 5:55:58,60.8,16.9
2020/07/04 23:47:52,61.8,15.3

体重と体脂肪率はそれぞれ別のローソク足チャートにしたいので、データを分けます。できていないときもありますが、一日に2回以上測定している日もあるので一日毎にOpen/High/Low/Closeのデータとなるように整形します。また、一日の終わりといっても0時を超えて計測していることもあるので、朝5時以降に計測したデータがその日の始値となるようにします。

import pands as pd
import datetime

if __name__ == '__main__':
    df = pd.read_csv('./SimpleWeight-Export.csv')

    #夜中の測定を前日の終値とするため5時間引いておく
    #(朝5時以前の測定は前日の測定結果にする)
    df['date'] = pd.to_datetime(df['date']) - datetime.timedelta(hours=5)
    df['date'] = df['date'].dt.date

    conv_data = {}

    #同じ日のデータをリストにまとめる
    for index, row in df.iterrows():
        day = row['date']
        if conv_data.get(day, None) == None:
            dairy_data = {'weight':[], 'bodyfat':[]}
        else:
            dairy_data = conv_data[day]

        dairy_data['weight'].append(row.weight)
        dairy_data['bodyfat'].append(row.bodyfat)
        conv_data[day] = dairy_data

    weight_data = []
    bodyfat_data = []

    #ローソク足チャートに必要なデータに変換する
    for day,value in conv_data.items():
        weight = {'date': day,
                  'open': value['weight'][0],
                  'high': max(value['weight']),
                  'low': min(value['weight']),
                  'close': value['weight'][-1],
                  }
        weight_data.append(weight)

        bodyfat = {'date': day,
                  'open': value['bodyfat'][0],
                  'high': max(value['bodyfat']),
                  'low': min(value['bodyfat']),
                  'close': value['bodyfat'][-1],
                  }
        bodyfat_data.append(bodyfat)

始値と終値から陽線、陰線を判断する

サンプルコードと同じように体重と体脂肪率のデータの始値と終値を比較して陽線/陰線を描くためのデータを作成します。

    w_df = pd.DataFrame(weight_data)
    b_df = pd.DataFrame(bodyfat_data)

    w_inc = w_df.close > w_df.open
    w_dec = w_df.open > w_df.close

    b_inc = b_df.close > b_df.open
    b_dec = b_df.open > b_df.close

ローソク足チャートを描画する

体重のチャートと体脂肪率のチャートは横に並べて別々に表示します。綺麗なコードではありませんがやりたいことは以下のコードで実現します。

from bokeh.plotting import figure, output_file, show
from bokeh.layouts import row as bkrow

if __name__ == '__main__':
    (中略)

    w = 12*60*60*1000
    TOOLS = 'pan,wheel_zoom,box_zoom,reset,save'

    p_wei = figure(x_axis_type='datetime', tools=TOOLS, plot_width=500, plot_height=500, title='Weight chart')
    p_wei.grid.grid_line_alpha = 0.3
    p_wei.background_fill_color = 'black'

    p_wei.segment(w_df.date, w_df.high, w_df.date, w_df.low, color='black')
    p_wei.vbar(w_df.date[w_inc], w, w_df.open[w_inc], w_df.close[w_inc], fill_color='mediumseagreen', line_color='mediumseagreen')
    p_wei.vbar(w_df.date[w_dec], w, w_df.open[w_dec], w_df.close[w_dec], fill_color='tomato', line_color='tomato')

    p_fat = figure(x_axis_type='datetime', tools=TOOLS, plot_width=500, plot_height=500, title='Bodyfat chart')
    p_fat.grid.grid_line_alpha = 0.3
    p_fat.background_fill_color = 'black'

    p_fat.segment(b_df.date, b_df.high, b_df.date, b_df.low, color='black')
    p_fat.vbar(b_df.date[b_inc], w, b_df.open[b_inc], b_df.close[b_inc], fill_color='mediumseagreen', line_color='mediumseagreen')
    p_fat.vbar(b_df.date[b_dec], w, b_df.open[b_dec], b_df.close[b_dec], fill_color='tomato', line_color='tomato')

    output_file('bodycomp_candlestick.html', title='body composition')
    show(bkrow(p_wei, p_fat))

これらを実行すると以下のようなチャートが表示されます。

サンプルコードと違う点としては、チャートを2つ並べて配置している点です。体重と体脂肪率のそれぞれのbokeh.plotting.figureオブジェクトをbokeh.layouts.rowを使いLayoutオブジェクトを作成しています。

移動平均線を追加する

ローソク足チャートだけでは見にくいので移動平均線も加えてみます。株式だと、20/50/100/200等の移動平均を用いると思います。市場の営業日数が概ね年間200日ということで、200日の移動平均線が概ね直近1年間の平均値、100日が半年、50日が四半期、20日が1ヶ月というように考えます。それに対して、体重計には毎日乗るので、7日(1週間)、30日(1ヶ月)、90日(3ヶ月)の移動平均を計算します。

移動平均線を追加するのはとても簡単で、以下のコードを追加するだけです。

    p_wei.line(w_df.date, w_df.close.rolling(7, min_periods=1).mean(), color='orangered', legend_label='7 SMA')
    p_wei.line(w_df.date, w_df.close.rolling(30, min_periods=10).mean(), color='olive', legend_label='30 SMA')
    p_wei.line(w_df.date, w_df.close.rolling(90, min_periods=30).mean(), color='dodgerblue', legend_label='90 SMA')

    p_fat.line(b_df.date, b_df.close.rolling(7, min_periods=1).mean(), color='orangered', legend_label='7 SMA')
    p_fat.line(b_df.date, b_df.close.rolling(30, min_periods=10).mean(), color='olive', legend_label='30 SMA')
    p_fat.line(b_df.date, b_df.close.rolling(90, min_periods=30).mean(), color='dodgerblue', legend_label='90 SMA')

実行すると以下の図のように移動平均線がローソク足チャートに重なって表示されます。

移動平均を各日の終値で計算しているのも影響しているのかもしれませんが、移動平均線を追加してわかったことがあります。体重はすべての移動平均線が下降していることが確認できます。一方で体脂肪率は、7日移動平均線と30に日移動平均線の乖離がなくなってきていることと、7日の移動平均線は上昇に転じていることが確認できます。

移動平均乖離率を表示する

作成したローソク足チャートでは毎日の数値変動が多くて見にくいので、30日移動平均から現在値がどの程度離れているかを移動平均乖離率を表示してみます。

移動平均乖離率は以下の式で計算できます。

移動平均乖離率 = (終値 - 移動平均値) ÷ 移動平均値 ✕ 100

移動平均乖離率はローソク足チャートの下に表示します。bokeh.plotting.figureのインスタンスを2つ追加し、レイアウトも変更していきます。

from bokeh.layouts import column as bkcolumn

if __name__ == '__main__':
    (中略)
    p_wei_madr = figure(x_axis_type='datetime', plot_width=500, plot_height=150, x_range=p_wei.x_range, title='移動平均乖離率')
    df_wei_madr = pd.DataFrame((w_df.close - w_df.close.rolling(30, min_periods=10).mean()) * 100/w_df.close.rolling(30, min_periods=10).mean())
    p_wei_madr.line(w_df.date, 0, color='red', line_width=2)
    p_wei_madr.line(w_df.date, df_wei_madr.close, color='deepskyblue', line_width=1.5)
    p_wei_madr.background_fill_color = 'black'
    p_wei_madr.grid.grid_line_alpha = 0.3

    p_fat_madr = figure(x_axis_type='datetime', plot_width=500, plot_height=150, x_range=p_fat.x_range, title='移動平均乖離率')
    df_fat_madr = pd.DataFrame((b_df.close - b_df.close.rolling(30, min_periods=10).mean()) * 100/b_df.close.rolling(30, min_periods=10).mean())
    p_fat_madr.line(w_df.date, 0, color='red', line_width=2)
    p_fat_madr.line(w_df.date, df_fat_madr.close, color='deepskyblue', line_width=1.5)
    p_fat_madr.background_fill_color = 'black'
    p_fat_madr.grid.grid_line_alpha = 0.3

    output_file('bodycomp_candlestick.html', title='body composition')
    show(bkrow(bkcolumn(p_wei,p_wei_madr), bkcolumn(p_fat,p_fat_madr)))

実行結果はこのようになります。

最終的なコード

import pandas as pd
import datetime
from bokeh.plotting import figure, output_file, show
from bokeh.layouts import row as bkrow
from bokeh.layouts import column as bkcolumn

def make_chandlechart(data, key):
    dairy_data = []
    for day,value in data.items():
        _data = {'date': day,
                 'open': value[key][0],
                 'high': max(value[key]),
                 'low': min(value[key]),
                 'close': value[key][-1],
                 }
        dairy_data.append(_data)

    _df = pd.DataFrame(dairy_data)

    inc = _df.close > _df.open
    dec = _df.open > _df.close

    W = 12*60*60*1000
    TOOLS = 'pan,wheel_zoom,box_zoom,reset,save'

    p = figure(x_axis_type='datetime', tools=TOOLS, plot_width=500, plot_height=500, title='Weight chart')
    p.grid.grid_line_alpha = 0.3
    p.background_fill_color = 'black'

    p.segment(_df.date, _df.high, _df.date, _df.low, color='black')
    p.vbar(_df.date[inc], W, _df.open[inc], _df.close[inc], fill_color='mediumseagreen', line_color='mediumseagreen')
    p.vbar(_df.date[dec], W, _df.open[dec], _df.close[dec], fill_color='tomato', line_color='tomato')

    p.line(_df.date, _df.close.rolling(7, min_periods=1).mean(), color='orangered', legend_label='7 SMA')
    p.line(_df.date, _df.close.rolling(30, min_periods=10).mean(), color='olive', legend_label='30 SMA')
    p.line(_df.date, _df.close.rolling(90, min_periods=30).mean(), color='dodgerblue', legend_label='90 SMA')

    p_madr = figure(x_axis_type='datetime', plot_width=500, plot_height=150, x_range=p.x_range, title='移動平均乖離率')
    df_madr = pd.DataFrame((_df.close - _df.close.rolling(30, min_periods=10).mean()) * 100/_df.close.rolling(30, min_periods=10).mean())
    p_madr.line(_df.date, 0, color='red', line_width=2)
    p_madr.line(_df.date, df_madr.close, color='deepskyblue', line_width=1.5)
    p_madr.background_fill_color = 'black'
    p_madr.grid.grid_line_alpha = 0.3

    return bkcolumn(p,p_madr)

if __name__ == '__main__':
    df = pd.read_csv('./SimpleWeight-Export.csv')
    df['date'] = pd.to_datetime(df['date']) - datetime.timedelta(hours=5)
    df['date'] = df['date'].dt.date

    conv_data = {}
    for index, row in df.iterrows():
        day = row['date']
        if conv_data.get(day, None) == None:
            dairy_data = {'weight':[], 'bodyfat':[]}
        else:
            dairy_data = conv_data[day]

        dairy_data['weight'].append(row.weight)
        dairy_data['bodyfat'].append(row.bodyfat)
        conv_data[day] = dairy_data

    wei_chart = make_chandlechart(conv_data, 'weight')
    fat_chart = make_chandlechart(conv_data, 'bodyfat')

    output_file('bodycomp_candlestick.html', title='body composition')
    show(bkrow(wei_chart, fat_chart))

体重も体脂肪率も同じ処理をしているだけなので共通処理をまとめました。もっと改善できる余地はあるかもしれませんが、今後修正していきたいと思います。

さいごに

簡単にオシレータを追加できることはわかりましたが、新しい気づきは得られませんでした。オシレータを追加するよりも上値と下値のトレンドラインを引いてみた方が違いがわかりやすかったかもしれません。体重チャートは上値も下値も下降トレンドですが、体脂肪率チャートでは上値が下がっていく一方で下値が切り上がっているのがわかるので体脂肪率は上昇トレンドに変わってしまいそうです。本人の努力次第ではあるのですが…。

数値を見える化しながらモチベーション維持していけることを期待します。