蛇ノ目の記

技術のことも。そうでないことも。

Plotlyを使ってインタラクティブなグラフを作る - ドロップダウンメニューを使って描画するデータの属性を変える話

Plotlyを使ってデータ可視化をやってみた話。

今回はドロップダウンメニューで描画するデータの属性を変えられるグラフを作っていきます。Dropdown Menus in Pythonにはいくつかサンプルコードがあるんだが、今回やることはそのどれとも微妙に違う。強いて言えばupdate-dropdownのイメージが近いが、サンプルコードではドロップダウンによってグラフのアノテーションを変えているのに対し、自分がやりたいのは描画するデータを変えるということ。

やりたいこと

  • Scatter Plotを描きたい

  • データの属性ごとに描画したい

  • データの属性はドロップダウンメニューで選びたい

参考

github.com

最近ドハマリしているEscape from TarkovというFPSに登場する弾薬の性能を可視化した。

PlotlyのHTML出力機能を使って、Bulmaで見た目を整えたペライチのページを作ってみた。TarkovAmmoPlot

テーマはBulmaswatch-darkly

(脱線)Escape from Tarkov

そこそこの広さのあるマップで他のプレイヤーやNPCと戦いつつ、換金できるアイテムや武器のパーツを集めたり、クエストをこなしていくという、FPS風来のシレントルネコみたいなローグライクを組み合わせたようなゲーム性を持つ。『弱者のためのゲームではない』らしく、脚に弾を喰らえば走れなくなるし、腕を撃たれれば銃を持つ手が震えるというハードコアなFPS。そして倒されたときの装備が他プレイヤーに奪われれば返ってこない。つらい。本当につらい。

このゲームでは(反動の強弱やカスタマイズによる操作性の上昇はあるものの)基本的に銃は弾を撃つための筒に過ぎず、弾が本体といっても過言ではない。弾にはいろいろな種類があり、アーマーへ与えるダメージ量や貫通力が異なる。しかしなぜかゲーム内では性能が見れない。Wikiにいけば表形式でまとめられているが、ぱっと見で判断しづらいので可視化するWebページを作ってみた、ということ。

🍻今回のテーマ🍻

EFTに登場する弾薬の性能を可視化すると言われて嬉しい人はほぼいないので、今回はみんな大好きクラフトビールのデータをテーマにやっていく 🍻

github.com

完成版のソースコードはこちら ↓

colab.research.google.com

解説

データの構造

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は分かりやすいように実数からパーセントにしている。

ドロップダウンに対応する

基本的な流れは以下の通り。

  1. Figureオブジェクトに add_trace() でScatterを追加していく

  2. updatemenusを設定する

  3. update_layout() でレイアウトにupdatemenus を反映する

updatemenus

参考:

layout.updatemenus | Python | Plotly

Dropdown Menus | Python | Plotly

辞書形式で、主に以下のパラメータを設定する。

  • active

初期状態でどのボタンにフォーカスが当たっているか。-1もしくは後述するbuttonsの長さ以上のインデックスを指定すると空白になる。

あくまでフォーカスが当たっているだけで、描画されるデータは関連しない。

  • type

ボタンの種類を設定する。dropdownbuttons がある。

デフォルトは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のサンプルコードでは、そのあたりも書いているのでチェックしてみてほしい。

完成したインタラクティブなグラフはこんな感じになる。

f:id:Nao_Y:20201129174037p:plain

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>