Plotlyを使ってインタラクティブなグラフを作る - ドロップダウンメニューを使って描画するデータの属性を変える話
Plotlyを使ってデータ可視化をやってみた話。
今回はドロップダウンメニューで描画するデータの属性を変えられるグラフを作っていきます。Dropdown Menus in Pythonにはいくつかサンプルコードがあるんだが、今回やることはそのどれとも微妙に違う。強いて言えばupdate-dropdownのイメージが近いが、サンプルコードではドロップダウンによってグラフのアノテーションを変えているのに対し、自分がやりたいのは描画するデータを変えるということ。
やりたいこと
Scatter Plotを描きたい
データの属性ごとに描画したい
データの属性はドロップダウンメニューで選びたい
参考
最近ドハマリしているEscape from TarkovというFPSに登場する弾薬の性能を可視化した。
PlotlyのHTML出力機能を使って、Bulmaで見た目を整えたペライチのページを作ってみた。TarkovAmmoPlot
(脱線)Escape from Tarkov
そこそこの広さのあるマップで他のプレイヤーやNPCと戦いつつ、換金できるアイテムや武器のパーツを集めたり、クエストをこなしていくという、FPSと風来のシレンやトルネコみたいなローグライクを組み合わせたようなゲーム性を持つ。『弱者のためのゲームではない』らしく、脚に弾を喰らえば走れなくなるし、腕を撃たれれば銃を持つ手が震えるというハードコアなFPS。そして倒されたときの装備が他プレイヤーに奪われれば返ってこない。つらい。本当につらい。
このゲームでは(反動の強弱やカスタマイズによる操作性の上昇はあるものの)基本的に銃は弾を撃つための筒に過ぎず、弾が本体といっても過言ではない。弾にはいろいろな種類があり、アーマーへ与えるダメージ量や貫通力が異なる。しかしなぜかゲーム内では性能が見れない。Wikiにいけば表形式でまとめられているが、ぱっと見で判断しづらいので可視化するWebページを作ってみた、ということ。
🍻今回のテーマ🍻
EFTに登場する弾薬の性能を可視化すると言われて嬉しい人はほぼいないので、今回はみんな大好きクラフトビールのデータをテーマにやっていく 🍻
完成版のソースコードはこちら ↓
解説
データの構造
abv
, ibu
, id
, name
, style
, brewery_id
, ounces
という7つの列からなっている。今回はアルコール度数を表すabv
と苦さの指標であるibu
の2つの軸で可視化し、style
ごとに描画できるようにする。
データの準備とDataFrame作成
import pandas as pd URL = "https://github.com/nickhould/craft-beers-dataset/blob/master/data/processed/beers.csv" dfs = pd.read_html(URL) origin_df = dfs[0] len(origin_df)
>>> 2410
pd.read_html
では指定したURLやファイルにあるHTMLテーブルを読み出して、DataFrameのリストで返す。そして今回は1件中1件目を使う。
データ絞り込み
2410行あるので、ビールのStyleで絞り込む。
len(origin_df["style"].unique())
>>> 100
Styleは重複なしで100件ある。中身を見て、以下の3種類を独断と偏見で選んだ。
styles = ("American Stout", "English India Pale Ale (IPA)", "American Barleywine") df = origin_df[origin_df["style"].isin(styles)] len(df)
>>> 55
55件にまで絞ることができた。Colabの方を見れもらえばわかるように、ibu
にNaNが入っているデータがあるのでそれらを除去する。
df = df.dropna(subset=["ibu"]) len(df)
>>> 25
さらに半分くらいになった。
データの絞り込みはこれでおわり。ColabではStyleでソートして一覧しているが、ここでは割愛。
Scatter Plotを描く
Plotlyにはより扱いやすい高レベルなAPIとしてPlotly Expressが用意されているが、今回の目的には適していないためGraph Objectsを使う。
import plotly.graph_objects as go fig = go.Figure() # American Stout plot_df = df.loc[df["style"] == styles[0]] title = styles[0] fig.add_trace( go.Scatter( x=plot_df["abv"].apply(lambda x: x*100), y=plot_df["ibu"], mode="markers+text", text=plot_df["name"], textposition="top center", marker=dict(size=20), name=styles[0], ) )
このコードではX軸をabv、Y軸をibuに設定し、マーカーとテキストをプロットしている。テキストはマーカーの(今回は)上部(top center)に表示されるもので、DataFrameのname列のデータを割り当てている。
X軸のabvは分かりやすいように実数からパーセントにしている。
ドロップダウンに対応する
基本的な流れは以下の通り。
Figureオブジェクトに
add_trace()
でScatterを追加していくupdatemenus
を設定するupdate_layout()
でレイアウトにupdatemenus
を反映する
updatemenus
参考:
layout.updatemenus | Python | Plotly
Dropdown Menus | Python | Plotly
辞書形式で、主に以下のパラメータを設定する。
active
初期状態でどのボタンにフォーカスが当たっているか。-1もしくは後述するbuttons
の長さ以上のインデックスを指定すると空白になる。
あくまでフォーカスが当たっているだけで、描画されるデータは関連しない。
type
ボタンの種類を設定する。dropdown
と buttons
がある。
デフォルトはdropdown
。
buttons
ボタンを押したときの振る舞いを設定する最も重要なパラメータ。形式は辞書のリスト。
各ボタンにはlabel
(ボタンのラベル), method
(ボタンを押した際の挙動), args
(プロットするオブジェクトに渡すパラメータ)などが設定できる。
method
には restyle
(データもしくはデータの属性を変更する), relayout
(レイアウトを変更する), update
(データとデータの属性を変更する), animate
(アニメーションの開始もしくは停止)がある。今回はupdate
を選択している。
args
が特に重要。ここで visible
を設定することで、そのボタンを押したときにどのプロットを表示するか制御できる。他にもプロットのタイトル(title
)もここで制御できる。Google Colabではtitleが動的に変更できないっぽいので今回は割愛している。
updatemenus = [{ "active": 0, "type": "dropdown", "buttons": [ dict(label=styles[0], method="update", args=[{"visible": [True, False, False]}]), # American Stoutだけ表示する dict(label=styles[1], method="update", args=[{"visible": [False, True, False]}]), # IPAだけ表示する dict(label=styles[2], method="update", args=[{"visible": [False, False, True]}]), # American Berleywineだけ表示する ] }]
fig = go.Figure() # American Stout plot_df = df.loc[df["style"] == styles[0]] title = styles[0] fig.add_trace( go.Scatter( x=plot_df["abv"].apply(lambda x: x*100), y=plot_df["ibu"], mode="markers+text", text=plot_df["name"], textposition="top center", marker=dict(size=20), name=styles[0], visible=True, # 初期表示するのでTrue ) ) # IPA plot_df = df.loc[df["style"] == styles[1]] fig.add_trace( go.Scatter( x=plot_df["abv"].apply(lambda x: x*100), y=plot_df["ibu"], mode="markers+text", text=plot_df["name"], textposition="top center", marker=dict(size=20), name=styles[1], visible=False, # 選択されたときに表示するので初期ではFalse ) ) # American Barleywine plot_df = df.loc[df["style"] == styles[2]] fig.add_trace( go.Scatter( x=plot_df["abv"].apply(lambda x: x*100), y=plot_df["ibu"], mode="markers+text", textposition="top center", text=plot_df["name"], marker=dict(size=20), name=styles[2], visible=False, # 選択されたときに表示するので初期ではFalse ) ) updatemenus = [{ "active": 0, "type": "dropdown", "buttons": [ dict(label=styles[0], method="update", args=[{"visible": [True, False, False]}]), # American Stoutだけ表示する dict(label=styles[1], method="update", args=[{"visible": [False, True, False]}]), # IPAだけ表示する dict(label=styles[2], method="update", args=[{"visible": [False, False, True]}]), # American Berleywineだけ表示する ] }] fig.update_layout( xaxis_title="abv", yaxis_title="ibu", updatemenus=updatemenus ) fig.show()
複数のプロットを作ったり、buttons
を書くのが冗長になることがわかる。今回は3つの属性に絞っているのでまあ目を瞑るとして、さらに増えると面倒になる。そのあたりはプロットを作る関数、buttons
を作る関数を書いて対応できる。Google Colabのサンプルコードでは、そのあたりも書いているのでチェックしてみてほしい。
完成したインタラクティブなグラフはこんな感じになる。
HTML出力
Figure
オブジェクトのwrite_html()
でHTMLとして出力できる。
参考: plotly.io.write_html — 4.13.0 documentation
full_html=False, include_plotlyjs=False
を設定すれば純粋にプロット部分だけのdivタグが出力されるので、以下のようなJinja2テンプレートに埋め込みやすくなる。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>インタラクティブなグラフ</title> <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> </head> <body> {{ plot|safe }} </body> </html>