CCCマーケティング TECH Labの Tech Blog

TECH Labスタッフによる格闘記録やマーケティング界隈についての記事など

PlotlyとDashを使ってPythonだけでインタラクティブなダッシュボードを作りました。

こんにちは、技術開発ユニットの三浦です。

2022年は「はんだづけ」が出来るようになりたいな・・・と考えています。以前温度湿度センサーを買って、それが自分ではんだづけをしないといけないものだったのですが、上手く出来なくて無駄にしてしまったことがありました。それ以来少し苦手意識が出来てしまって、はんだづけが必要なセンサーなどを避けるようにしていたのですが、それだと出来ることも限られてしまいます。なので何か「これを作ろう!」というテーマを決めて、はんだづけをするきっかけをどんどん増やしていって、少しずつ上手になっていきたいなと思います。

さて、最近イベントログを集計する機会がありました。抽出条件が複雑だったため、PythonのPandasを使って抽出から集計までの処理をプログラムで組んで、結果をcsvファイルで出力する、という対応を取りました。csvで出力したファイルはその後、必要に応じてExcelや可視化ツールに取り込んでレポート作成などに使用します。

このcsvファイルを別のアプリケーションに連携するステップにはいつも気を使います。ミスが発生しやすいからです。新しいファイルに差し替えないといけないのに、差し替えが漏れてしまい、間違ったデータでレポートを作ってしまった、という失敗をしたことがあって、それ以来可視化ツールにファイルを取り込む際にはファイルのタイムスタンプやファイルサイズを何度も確認するようにしています。

しかし、そもそもデータの抽出と可視化の環境が分かれていることが問題であって、抽出から可視化までのプロセスを一気通貫で出来ればそのミスを減らすことが出来ます。ケースによって色んな選択肢はあるものの、今回はPythonのPlotlyDashというライブラリを使ってダッシュボードを作りました。こんな感じのダッシュボードがPythonだけで作れてしまいます。

f:id:miu4930:20220117131835p:plain
Dinnerタイムの来店状況

f:id:miu4930:20220117131918p:plain
Lunchタイムの来店状況

Plotly+Dash

PlotlyDashはどちらもPlotly社が提供しているオープンソースのPythonのライブラリです。

Plotly Python Graphing Library | Python | Plotly

Dash Documentation & User Guide | Plotly

Plotlyがグラフを作成するライブラリ、Dashがデータ可視化用のWebアプリを作るためのライブラリ(フレームワーク)です。Plotlyで作ったグラフをDashでダッシュボードの中に組み込んであげる、そんな流れになります。

ダッシュボード作成までの手順

手順は以下のようになります。

  1. Plotlyでグラフを作る
  2. Dashでダッシュボードのレイアウトを作る
  3. Dashcallbackを作ってインタラクティブな動きを付ける

使用するデータ

Plotlyに用意されているサンプルデータplotly.data.tipsを使用しました。このデータはあるレストランで働くウェイターが数か月に渡り、受け取ったチップに関する情報を記録したデータとのことです。

import plotly
plotly.data.tips().head()

f:id:miu4930:20220117133355p:plain
tipsデータ

作るダッシュボード

ここでは以下のようなダッシュボードを作ります。

f:id:miu4930:20220117140727p:plain
Tipsデータを可視化するダッシュボード

左に来店者(支払者)の性別人数カウントのPie Chart、右に来店者の喫煙/非喫煙者の有無の曜日別カウントのBar Chartを表示しています。左上のRadioボタンで"Lunch"/"Dinner"を切り替えてそれぞれの営業時間の集計結果に切り替えることが出来ます。

ダッシュボードを作っていきます

前準備

最初に必要なライブラリのインストールをします。

pip install jupyter pandas plotly dash

さらにJupyter notebook上でDashアプリ開発が出来るようになるJupyter Dashをインストールします。

github.com

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()

結果

f:id:miu4930:20220117141331p:plain
Bar Chart

  • 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()

結果

f:id:miu4930:20220117141348p:plain
Pie Chart

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ボタンを選んでも何も起こりません。

Dashcallbackを作ってインタラクティブな動きを付ける

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()

まとめ

PlotlyDashを使うことでいい感じのデータ可視化用のダッシュボードを作ることが出来ました。見栄えの調整などはまだまだ手を付けられていないのですが、ちゃんと調整することでかなりカッコいいダッシュボードが出来そうです。そのままレポートにも使えそうなダッシュボードが、分析作業と同じ環境で作れてしまうのは、作業のミスも減らせていいな、と思います。ぜひ試してみてください!