StatsFragments

Python, R, Rust, 統計, 機械学習とか

Python pandas のデータを Highcharts/Highstock + Jupyter でプロットしたい

R を使っている方はご存知だと思うが、R には {htmlwidgets} というパッケージがあり、R 上のデータを任意の Javascript ライブラリを使ってプロットすることが比較的カンタンにできる。{htmlwidgets} って何?という方には こちらの説明がわかりやすい。

同じことを Python + pandas を使ってやりたい。サンプルとして利用する Javascript ライブラリは 上の資料と同じく HighchartsHighstock にする。

www.highcharts.com

補足 pandas-highcharts という Python パッケージもあるが、このエントリでは任意の Javascript ライブラリで使えるであろう方法を記載する。

Highcharts でのプロット

以降の操作は Jupyter Notebook 上で行う。まずは必要パッケージをロードする。

import numpy as np
import pandas as pd
from IPython.display import HTML

続けて、HighchartsHighstock を読み込む。これは %%html magic を使うのが楽。

%%html
    <script src="http://code.highcharts.com/highcharts.js"></script>
    <script src="http://code.highcharts.com/stock/highstock.js"></script>
    <script src="http://code.highcharts.com/modules/exporting.js"></script>

Highcharts でプロットする準備ができたため、%%html magic を使って サンプル Your first chart に記載のサンプルをプロットしてみる。これでプロット時のスクリプト / データ構造が確認できる。

%%html
    <div id="container" style="width:100%; height:400px;"></div>
    <script>
        plot = function () { 
            $('#container').highcharts({
                chart: {
                    type: 'bar'
                },
                title: {
                    text: 'Fruit Consumption'
                },
                xAxis: {
                    categories: ['Apples', 'Bananas', 'Oranges']
                },
                yAxis: {
                    title: {
                        text: 'Fruit eaten'
                    }
                },
                series: [{
                    name: 'Jane',
                    data: [1, 0, 4]
                }, {
                    name: 'John',
                    data: [5, 7, 3]
                }]
            });
        };
        plot();
    </script>

f:id:sinhrks:20150613152238p:plain

補足 Jupyter から実行すればアニメーションする。

pandas のデータをプロットしたい場合は、上のスクリプトと同じように .highcharts() の引数にあわせた形式でデータを渡し、HTML としてレンダリングしてやればうまくいきそうだ。pandas で元データとなる DataFrame を定義して、上のスクリプトの形式に変換していく。

df = pd.DataFrame({'Jane': [1, 0, 4], 'John': [5, 7, 3]},
                  index=['Apples', 'Bananas', 'Oranges'])
df
#          Jane  John
# Apples      1     5
# Bananas     0     7
# Oranges     4     3

スクリプトは文字列結合で作ってもよいが、引数となる json 形式に対応する辞書型のデータをつくってからjson.dumps したほうが簡単だろう。DataFrame.to_json でデータを直接 変換できると楽なのだが、フォーマットが違うため無理そうだ。

# NG!
df.to_json()
# '{"Jane":{"Apples":1,"Bananas":0,"Oranges":4},
#   "John":{"Apples":5,"Bananas":7,"Oranges":3}}'

そのため、個々の要素ごとに変換を考えていく。うまいこと辞書ができたら json.dumps する。

[{'name': c, 'data': col.tolist()} for c, col in df.iteritems()]
# [{'data': [1, 0, 4], 'name': 'Jane'}, {'data': [5, 7, 3], 'name': 'John'}]

chartdict = {'chart': {'type': 'bar'},
             'title': {'text': 'Fruit Consumption'},
             'xAxis': {'categories': df.index.tolist()}, 
             'yAxis': {'title': {'text': 'Fruit eaten'}},
             'series': [{'name': c, 'data': col.tolist()} for c, col in df.iteritems()]
            }

import json
json.dumps(chartdict)
# '{"series": [{"data": [1, 0, 4], "name": "Jane"}, {"data": [5, 7, 3], "name": "John"}],
#   "yAxis": {"title": {"text": "Fruit eaten"}}, "chart": {"type": "bar"},
#   "xAxis": {"categories": ["Apples", "Bananas", "Oranges"]},
#   "title": {"text": "Fruit Consumption"}}'

あとは 必要な HTML / Javascript のテンプレートを文字列として作って format すればよい。

template = """
<script src="http://code.highcharts.com/highcharts.js"></script>
<script src="http://code.highcharts.com/modules/exporting.js"></script>
<div id="{chart}" style="width:100%; height:400px;"></div>
<script type="text/javascript">
plot = function () {{
    $("#{chart}").highcharts({data});
}};
plot();
</script>
"""
HTML(template.format(chart='container2', data=json.dumps(chartdict)))
# 略

Highstock でのプロット

同様に、Highstock へプロットすることもできる。サンプル Single line seriespandas のデータを使ってプロットしてみる。

補足 株価の取得には以下のパッケージを使う。

import japandas as jpd
toyota = jpd.DataReader(7203, 'yahoojp', start='2015-01-01')
toyota.head()
#               始値    高値    安値    終値       出来高  調整後終値*
# 日付                                                  
# 2015-01-05  7565  7575  7416  7507   9515300    7507
# 2015-01-06  7322  7391  7300  7300  12387900    7300
# 2015-01-07  7256  7485  7255  7407  11465400    7407
# 2015-01-08  7500  7556  7495  7554  10054500    7554
# 2015-01-09  7630  7666  7561  7609  10425400    7609

Highstock に渡すデータの形式は以下のサイトがわかりやすい。引数としては [UNIX時間(ミリ秒), 始値, 高値, 安値, 終値] を入れ子のリストにして渡せばよいようだ。

時刻は UNIX時間で渡す必要があるので、少し操作が必要だ。上で取得した DataFrame は日時型の Index である DatetimeIndex を持っている。DatetimeIndexUNIXエポックを基準とした現在時刻をナノ秒で保存しているため、int 型に変換して 1000000 で割ればUNIX時間(ミリ秒)となる。したがって、Highstock へ渡すデータは以下のようにして作れる。

toyota.index.astype(int) / 1000000
# array([1420416000000, 1420502400000, 1420588800000, 1420675200000,
#        1420761600000, 1421107200000, 1421193600000, 1421280000000,
#        ....
#        1433721600000, 1433808000000, 1433894400000, 1433980800000,
#        1434067200000])

toyota['time'] = toyota.index.astype(int) / 1000000
toyota[['time', u'始値', u'高値', u'安値', u'終値']].values.tolist()
# [[1420416000000, 7565, 7575, 7416, 7507],
#  [1420502400000, 7322, 7391, 7300, 7300],
#  ...
#  [1433980800000, 8250, 8326, 8241, 8322],
#  [1434067200000, 8387, 8394, 8329, 8394]]

描画する。アニメーションも含め、うまく動いているようだ。

chartdict = {'rangeSelector': {'selected': 1},
             'title': {'text' : 'Stock Price'},
             'series': [{'name' : u'トヨタ',
                         'data': toyota[['time', u'始値', u'高値', u'安値', u'終値']].values.tolist(),
                         'tooltip': {'valueDecimals': 2}}]}

template = """
<div id="{chart}" style="width:100%; height:400px;"></div>
<script type="text/javascript">
plot = function () {{
    $('#{chart}').highcharts('StockChart', {data});
}};
plot();
</script>
HTML(template.format(chart='container3', data=json.dumps(chartdict)))

f:id:sinhrks:20150613145918p:plain

まとめ

pandas のデータを任意の Javascript ライブラリでプロットする際には、

  1. 当該の Javascript ライブラリが利用するデータ構造を確認する
  2. そのデータ構造にあうように pandas のデータを変換し、辞書型を作る
  3. json.dumpsスクリプトに渡す

とやればだいたいできると思う。

補足 ここで今 話題の spyre 上でもプロットしたいな?と思ったのだが、spyre で普通に HTML としてレンダリングするとうまく動かない。spyre が内部で使っている cherrypy も DOM を書き換えているのが原因かと思っているのだが、自分にはそのあたりの知識がまったくないのでよくわからない。できた方がいれば教えてください。

sinhrks.hatenablog.com

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理