こんにちは、技術開発ユニットの三浦です。
2022年は「はんだづけ」が出来るようになりたいな・・・と考えています。以前温度湿度センサーを買って、それが自分ではんだづけをしないといけないものだったのですが、上手く出来なくて無駄にしてしまったことがありました。それ以来少し苦手意識が出来てしまって、はんだづけが必要なセンサーなどを避けるようにしていたのですが、それだと出来ることも限られてしまいます。なので何か「これを作ろう!」というテーマを決めて、はんだづけをするきっかけをどんどん増やしていって、少しずつ上手になっていきたいなと思います。
さて、最近イベントログを集計する機会がありました。抽出条件が複雑だったため、PythonのPandas
を使って抽出から集計までの処理をプログラムで組んで、結果をcsvファイルで出力する、という対応を取りました。csvで出力したファイルはその後、必要に応じてExcelや可視化ツールに取り込んでレポート作成などに使用します。
このcsvファイルを別のアプリケーションに連携するステップにはいつも気を使います。ミスが発生しやすいからです。新しいファイルに差し替えないといけないのに、差し替えが漏れてしまい、間違ったデータでレポートを作ってしまった、という失敗をしたことがあって、それ以来可視化ツールにファイルを取り込む際にはファイルのタイムスタンプやファイルサイズを何度も確認するようにしています。
しかし、そもそもデータの抽出と可視化の環境が分かれていることが問題であって、抽出から可視化までのプロセスを一気通貫で出来ればそのミスを減らすことが出来ます。ケースによって色んな選択肢はあるものの、今回はPythonのPlotly
とDash
というライブラリを使ってダッシュボードを作りました。こんな感じのダッシュボードがPythonだけで作れてしまいます。
Plotly
+Dash
Plotly
とDash
はどちらもPlotly社が提供しているオープンソースのPythonのライブラリです。
Plotly Python Graphing Library | Python | Plotly
Dash Documentation & User Guide | Plotly
Plotly
がグラフを作成するライブラリ、Dash
がデータ可視化用のWebアプリを作るためのライブラリ(フレームワーク)です。Plotly
で作ったグラフをDash
でダッシュボードの中に組み込んであげる、そんな流れになります。
ダッシュボード作成までの手順
手順は以下のようになります。
Plotly
でグラフを作るDash
でダッシュボードのレイアウトを作るDash
でcallback
を作ってインタラクティブな動きを付ける
使用するデータ
Plotly
に用意されているサンプルデータplotly.data.tips
を使用しました。このデータはあるレストランで働くウェイターが数か月に渡り、受け取ったチップに関する情報を記録したデータとのことです。
import plotly
plotly.data.tips().head()
作るダッシュボード
ここでは以下のようなダッシュボードを作ります。
左に来店者(支払者)の性別人数カウントのPie Chart、右に来店者の喫煙/非喫煙者の有無の曜日別カウントのBar Chartを表示しています。左上のRadioボタンで"Lunch"/"Dinner"を切り替えてそれぞれの営業時間の集計結果に切り替えることが出来ます。
ダッシュボードを作っていきます
前準備
最初に必要なライブラリのインストールをします。
pip install jupyter pandas plotly dash
さらにJupyter notebook上でDash
アプリ開発が出来るようになるJupyter Dash
をインストールします。
pip install jupyter_dash
続いてデータのロードやグラフ描画用のセッティングなど。
import plotly import pandas as pd #設定 #Tipping dataの読み込み #https://vincentarelbundock.github.io/Rdatasets/doc/reshape2/tips.html #来店カウント用に'count'カラムを追加 data = plotly.data.tips() data['count'] = 1 #曜日別の来店カウントの基礎データ #可視化対象のデータに依存して出現曜日が異なることを避けるために用意 bar_base_data = pd.Series([0]) bar_base_data = bar_base_data.repeat(7) bar_base_data.set_axis( ['Mon','Tue','Wed','Thur','Fri','Sat','Sun'], axis=0, inplace=True ) bar_base_data.index.name = 'day' bar_base_data.rename('count',inplace=True) #Bar ChartやPie Chartで使用するcolor定義 color_dict = { 'sex':{ 'Female':'LightCoral', 'Male':'LightBlue' }, 'smoker':{ 'Yes':'Navy', 'No':'Orange' } }
bar_base_data
は曜日でBar Chartを描く際、集計軸によってはデータが存在しない曜日が発生することがあり、その度にBar Chartのx軸が変化してしまうことを避けるために用意しました。
color_dict
はグラフの系列の色を固定するために用意しました。
Plotly
でグラフを作る
Bar ChartとPie Chartを作ります。それぞれ関数にまとめておくと、後でメンテナンスがしやすくなります。
- Bar Chart
def draw_visiter_count_bar_chart(data, group_name, group_labels): """曜日別の来店合計を示すBar Chartを表示するFigureを作成。 Args: data: 元データ(plotly.data.tipsを想定) group_name: グラフの系列対象のカラム名('sex'か'smoker'を想定) group_labels: 系列名のリスト Returns: Bar Chartを表示するFigureオブジェクト. """ data = data.groupby(by=[group_name, 'day']).sum().loc[:,'count'] #系列ごとにBar Chartを作る figures = [] for label in group_labels: #グラフ形状をFixするための処理 _data = (bar_base_data + data[label]).fillna(0) _data = _data.reindex(index=bar_base_data.index) figures.append( go.Bar( name=label, x=_data.index, y=_data, marker_color=color_dict[group_name][label]) ) fig = go.Figure(data=figures) #Stacked Bar Chartで表示する fig.update_layout( title={'text':'Week Day Visiter:' + group_name}, barmode='stack' ) return fig #テスト draw_visiter_count_bar_chart(data, 'smoker', ['Yes','No']).show()
結果
- Pie Chart
def draw_visiter_count_pie_chart(data, group_name, group_labels): """来店合計を示すPie Chartを表示するFigureを作成。 Args: data: 元データ(plotly.data.tipsを想定) group_name: グラフの系列対象のカラム名('sex'か'smoker'を想定) group_labels: 系列名のリスト Returns: Pie Chartを表示するFigureオブジェクト. """ data = data.groupby(by=group_name).sum().loc[:,'count'] fig = go.Figure( data=[ go.Pie( labels=data.index, values=data, marker_colors= [color_dict[group_name][i] for i in data.index] ) ] ) fig.update_traces(textinfo='value') fig.update_layout(title={'text':'Total Visiter:' + group_name}) return fig #テスト draw_visiter_count_pie_chart(data, 'sex', ['Female','Male']).show()
結果
Dash
でダッシュボードのレイアウトを作る
Dash
を使ってダッシュボードのレイアウトを作ります。Dash
ではHTMLの要素をdash.html
を使って作ります。例えばhtml.H1(['Tips Data Explorer'])
を呼び出すと、<h1>Tips Data Explorer</h1>
というHTML要素がダッシュボードに作られます。さらに純粋なHTML要素だけでなく、JavascriptやReact.jsによるインタラクティブに動作する要素も作ることが出来ます(dash.dcc
)。この要素にPlotly
で作ったグラフ(Figure
オブジェクト)を組み込んであげます。コードは以下のようになります。
from jupyter_dash import JupyterDash from dash import dcc from dash import html external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = JupyterDash(__name__, external_stylesheets=external_stylesheets) # Create server variable with Flask server object for use with gunicorn server = app.server app.layout = html.Div([ #Title html.H1([ 'Tips Data Explorer' ]), #Radio Button dcc.RadioItems( id='time_selector', options=[ {'label':'Lunch', 'value':'Lunch'}, {'label':'Dinner', 'value':'Dinner'} ], value='Dinner', labelStyle={'display':'inline-block'} #Radio Buttonを横並びにする ), #Pie Chart #グラフを横並びにするため、グラフをDivで囲み、inline-blockにする html.Div( style={'width':'40%', 'display':'inline-block'}, children=[ dcc.Graph( id='sex_pie_chart', figure=draw_visiter_count_pie_chart(data, 'sex', ['Female','Male']) ) ] ), #Bar Chart html.Div( style={'width':'60%', 'display':'inline-block'}, children=[ dcc.Graph( id='smoker_bar_chart', figure=draw_visiter_count_bar_chart(data, 'smoker', ['Yes','No']) ) ] ) ]) #Server起動 app.run_server()
上手く起動出来るとDash app running on http://127.0.0.1:8050/
という出力が表示されるので、ここに表示されているURLにアクセスするとPlotly
のグラフが組み込まれたダッシュボードにアクセスすることが出来ます。この状態ではRadioボタンを選んでも何も起こりません。
Dash
でcallback
を作ってインタラクティブな動きを付ける
Radioボタンで"Lunch", "Dinner"を選ぶとそれぞれの営業時間の表示に切り替わるようにします。Dash
では要素のプロパティを受け取り、処理を行って、その結果を別の要素のプロパティに反映することが出来ます。処理を行う関数の前に@app.callback
デコレータを付けてあげます。
#Radio Buttonの値を変更した時のcallback @app.callback( Output('sex_pie_chart', 'figure'), Output('smoker_bar_chart', 'figure'), Input('time_selector', 'value')) def update_figures(input_value): #input_value = "Lunch" or "Dinner" _data = data.query('time=="{}"'.format(input_value)) return \ draw_visiter_count_pie_chart(_data, 'sex', ['Female','Male']),\ draw_visiter_count_bar_chart(_data, 'smoker', ['Yes','No'])
dash.dependencies.Input('time_selector', 'value')
はid='time_selector'
の要素(ここではdcc.RadioItems
)の値を受け取り、その値を関数update_figures
の引数input_value
に入れて関数の処理を行った後、関数の返り値をdash.dependencies.Output('sex_pie_chart', 'figure')
とdash.dependencies.Output('smoker_bar_chart', 'figure')
でそれぞれid='sex_pie_chart'
とid='smoker_bar_chart'
の要素のfigure
プロパティにセットします。このようにしてRadioボタンで選択した値に応じてグラフを変化させることが出来ます。
ダッシュボードを作成するコード全体は以下のようになります。
from jupyter_dash import JupyterDash from dash import dcc from dash import html from dash.dependencies import Input, Output external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = JupyterDash(__name__, external_stylesheets=external_stylesheets) # Create server variable with Flask server object for use with gunicorn server = app.server app.layout = html.Div([ #Title html.H1([ 'Tips Data Explorer' ]), #Radio Button dcc.RadioItems( id='time_selector', options=[ {'label':'Lunch', 'value':'Lunch'}, {'label':'Dinner', 'value':'Dinner'} ], value='Dinner', labelStyle={'display':'inline-block'} #Radio Buttonを横並びにする ), #Pie Chart #グラフを横並びにするため、グラフをDivで囲み、inline-blockにする html.Div( style={'width':'40%', 'display':'inline-block'}, children=[ dcc.Graph( id='sex_pie_chart', #figure=draw_visiter_count_pie_chart(data, 'sex', ['Female','Male']) ) ] ), #Bar Chart html.Div( style={'width':'60%', 'display':'inline-block'}, children=[ dcc.Graph( id='smoker_bar_chart', #figure=draw_visiter_count_bar_chart(data, 'smoker', ['Yes','No']) ) ] ) ]) #Radio Buttonの値を変更した時のcallback @app.callback( Output('sex_pie_chart', 'figure'), Output('smoker_bar_chart', 'figure'), Input('time_selector', 'value')) def update_figures(input_value): #input_value = "Lunch" or "Dinner" _data = data.query('time=="{}"'.format(input_value)) return \ draw_visiter_count_pie_chart(_data, 'sex', ['Female','Male']),\ draw_visiter_count_bar_chart(_data, 'smoker', ['Yes','No']) #Server起動 app.run_server()
まとめ
Plotly
とDash
を使うことでいい感じのデータ可視化用のダッシュボードを作ることが出来ました。見栄えの調整などはまだまだ手を付けられていないのですが、ちゃんと調整することでかなりカッコいいダッシュボードが出来そうです。そのままレポートにも使えそうなダッシュボードが、分析作業と同じ環境で作れてしまうのは、作業のミスも減らせていいな、と思います。ぜひ試してみてください!