HoloViewsとBokehでぐりぐり動かせるグラフを描く

職場でのpython導入を画策中。
エクセルで出来なくてPythonで出来ることがないかなー、と模索していたら、
**Bokeh**というライブラリを使って**インタラクティブなグラフ**が書けるとのこと。
[こちら](https://qiita.com/driller/items/53be86cea3c3201e7e0f)の記事で気になっていた**HoloViews**と合わせて色々調べたのでまとめてみた。

#インタラクティブなグラフって？
ユーザーの操作に対して応答するグラフを指す。（インタラクティブ=双方向の～という意味）
![holoviewデモ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/bf1ab2cf-9e96-0693-0301-b697e76a5e8b.gif)
こんな感じでマウスでぐりぐり動かしたり、ズームしたり、情報を表示させたりできる。

#環境
とりあえず手っ取り早く取り掛かりたかったので、GoogleColaboratoryで実装。
そのため本稿ではライブラリのインストールに関する記述は省略。
（未検証だがライブラリのインストールを行なえばJupyterNotebookでも動くはず）

#データ
下記のような初代ポケモンのデータをDataFrame形式で準備。
![20201230_085515.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/16d581f6-0cb3-eaf0-03c0-5872c68ebdd0.png)
データは公式ポケモン図鑑とポケモンWikiから引用。

#実装
##基本
基本となるグラフの書き方。今回は散布図（Scatter Graph）を書いていく。

```python
# グラフ関連ライブラリのインポート
import holoviews as hv
hv.extension('bokeh')

# 基本的な描画
poke_graph = hv.Scatter(poke_df,kdims=["height"],vdims=["weight"])
# 表示
poke_graph
```

![20201230_090817.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/81c5fa4a-a630-7f1d-58a6-e078b49c3998.png)
`hv.extension('bokeh')`でholoviewsをbokehモードに拡張。
（<font color="red">セルごとに毎回上記を記述しないとグラフが表示されない</font>）
`hv.Scatter()`で散布図インスタンスを生成。
Scatterの場合、引数には使用するデータセットと**kdims**、**vdims**を指定する。
　kdims：key dimensions。キーとなる要素。x軸のイメージ。
　vdims：value dimensions。上記キーに対応した値の要素。y軸の他、後述のオプションの値にも適用できる。

表示はpandasみたいにインスタンス名を書くだけで表示できる。
（JupyterNotebookではグラフを表示するために`output_notebook()`というおまじないが必要とのこと）


##オプションの設定
holoviewsは後からグラフに関するオプションを追加できる。

```python
hv.extension('bokeh')

# タイトルの設定
poke_graph.opts(title="ポケモンデータ")
# グリッドの描画
poke_graph.opts(show_grid=True)
# 軸ラベルの変更
poke_graph.opts(xlabel="たかさ", ylabel="おもさ")
```

![20201230_091008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/6017c96c-aa05-c0cd-d85c-4d8f8c4c93f3.png)
`インスタンス.opts(…)`で各種オプションを設定する。
分かりやすくするために個別に設定しているが、一度の`.opts`でまとめて設定することもできる。
`poke_graph.opts(title="ポケモンデータ", show_grid=True, xlabel="たかさ", ylabel="おもさ")`



マーカーに関する設定はこんな感じ

```python
hv.extension('bokeh')

# マーカーの色の設定
poke_graph.opts(color = "red")
# マーカーの縁の色の設定
poke_graph.opts(line_color = "black")
# マーカーのサイズの設定
poke_graph.opts(size=7)
# マーカーの透過率の設定
poke_graph.opts(alpha = 0.8)
```

![20201230_091145.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/305c0f27-df23-3cae-0639-9b20624221fb.png)


上記だとちょっと窮屈なので、見やすさを意識して設定を追加。

```python
hv.extension('bokeh')

# 背景色の設定
poke_graph.opts(bgcolor = "whitesmoke")
# グラフ領域の描画サイズの設定
poke_graph.opts(width=640, height=480)
# フォントサイズの一律で大きく
poke_graph.opts(fontscale=1.5)
# 軸のフォーマット変更
poke_graph.opts(xformatter="%.1f m", yformatter="%.1f kg")
```

![20201230_091500.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/00066137-51c3-b0c0-6bc2-006b63b7b07a.png)


##要素をオプションへ適用する
####マーカーサイズへの適用
ここでは、バブルチャートのように種族値合計をマーカーの大きさに適用してみる。
要素をオプションに適用する場合、<font color="red">vdimsに要素が追加されていないと表示がおかしくなる</font>。
なので、`.add_dimension()`でディメンジョンに要素を追加。

```python
poke_graph2 = poke_graph.add_dimension("種族値合計",dim_pos=2,vdim=True,dim_val = "合計")
```

第一引数に要素名を指定。`dim_pos`で何番目に要素を追加（挿入）するか指定。
（vdimsの順番がどのように影響してくるか不明だが、とりあえず新しい要素が先頭にさえならなければ良さそう。なので適当に2とかにしている。）
`vdim=True`でvdimsに追加するようにする（省略するとkdimsに追加されてしまう）。
`dim_val=`で実際に値として使用するデータを指定する。今回の場合、すでにデータセットをDataFrame形式で渡してあるので、カラム名で指定するだけでOK。データセットで指定した以外にも`dim_val=df["特殊"]`や`dim_val=poke_df["合計"]**2`のように新しい値を指定することもできる。

vdimsに追加したら、optsで引数に追加したディメンジョン名を渡すことができるようになる。

```python
poke_graph2 = poke_graph2.opts(size="種族値合計")
```

![20201230_160724.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/25c57695-964e-2a34-1530-c75047b6b310.png)
おっと。
上記のようになってしまうのは、sizeに600とか大きい値が渡されているため。
これを回避するために、値を数式で変換する。その際、<font color="red">要素を数式に適用するには`dim()`を使わないといけない</font>。

```python
from holoviews import dim
poke_graph2 = poke_graph2.opts(size=(dim("種族値合計")/poke_df["合計"].min())*5)
```

![20201230_092108.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/2e083db4-0f29-17d1-18bb-abc258942706.png)
上記の例では、各ポケモンの種族値合計の値と種族値合計の最小値の比率に5をかけて、sizeの値としている。

####マーカーカラーへの適用
要素からマーカーの色を指定することも可能。
サイズの時と同様、まずvdimsに要素を追加し、`color=`にディメンジョン名を指定する。

```python
hv.extension('bokeh')

# データ要素にタイプ1を追加
poke_graph3 = poke_graph2.add_dimension("タイプ1",dim_pos=2,vdim=True,dim_val = "タイプ1")
# マーカーの色を「タイプ1」に応じて設定
poke_graph3.opts(color="タイプ1")
# 凡例の位置を右側に設定
poke_graph3.opts(legend_position="right")
# 描画サイズの幅を再設定
poke_graph3.opts(width=800)
```

![20201230_214005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/aec7442b-f1ba-39c0-1d79-c3572f0e49e5.png)
上記はカラーが自動で振られているが、任意の色にすることも可能。
colorに渡す要素をキー、色を値にした辞書を作成し、`cmap=`に渡せばOK。

```python
color_table = {"ノーマル":"cornsilk", "ほのお":"orangered", "みず":"royalblue", "でんき":"gold",
                "くさ":"green", "こおり":"powderblue", "かくとう":"darkorange", "どく":"mediumorchid",
                "じめん":"saddlebrown", "ひこう":"lightcyan", "エスパー":"khaki", "むし":"yellowgreen",
                "いわ":"dimgrey", "ゴースト":"mediumpurple", "ドラゴン":"lightseagreen",
                "あく":"indigo", "はがね":"steelblue","フェアリー":"pink",
                }
poke_graph3.opts(color="タイプ1",cmap=color_table)
```

![20201230_092207.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/606889de-69c1-8c33-a488-f49d4b2f93fc.png)
色名はmatplotlibと同じものを使用可能。多分16進数RGB指定もできる。

##ツールチップを使う
bokehの特徴の一つ、ツールチップを使ってみる。

```python
hv.extension('bokeh')

# データ要素にポケモン名を追加
poke_graph4 = poke_graph3.add_dimension("name",dim_pos=2,vdim=True,dim_val = "name")

# ホバーツールのインポート
from bokeh.models import HoverTool
# ホバーツールの設定
TOOLTIPS = [("name","@name")]
hover = HoverTool(tooltips=TOOLTIPS)
# グラフにホバーツールの設定適用
poke_graph4 = poke_graph4.opts(plot=dict(tools=[hover]))
```

![20201230_092318.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/f0e8e42a-56f8-b3d9-b58b-b3207d370ae7.png)
ホバーツールはツールチップの一種。カーソルが合った要素の情報を表示することができる。
`HoverTool`インスタンスを生成し、引数`tooltips=`に表示したい情報をタプルのリストで渡す。
タプルの中には一行に表示する情報が記述される。複数タプルを渡せば複数行表示可能。
`"@～"`でvdimsに追加したディメンション名を指定する。<font color="red">ディメンション名が日本語の場合、`"@{攻撃}"`みたいに{}で囲まないと認識してくれないので注意</font>。
また、"@～"の他、"\$x"や"$index"等で要素の基本的な情報を表示することもできる。

また、HTML記述でより凝ったレイアウトが可能。
\<div>タグや\<table>タグのほか、\<img>タグも使用可能なので画像を表示することもできる。

```python
TOOLTIPS_2 = """
<div>
  :
</div>
"""
```

のようにHTML記述をした文字列を渡す。
文中で`<img src="@img_path">`のように@ディメンション名を文字列として渡せばその情報を表示してくれる。

サンプル（コードは冗長になったのでクリックで表示）
<details><summary>コード</summary><div>

```python
# 図鑑No.から画像アドレスを指定する関数を定義
def path_create(id):
  result = r"file:///C:\Users\aaa\Desktop\ずかん\pokemon_" + str(id).zfill(3) + ".png"
  return result

# データ要素の追加
poke_graph5 = poke_graph4.add_dimension("img_path",dim_pos=2,vdim=True,
                                        dim_val = poke_df["No."].apply(path_create))
poke_graph5 = poke_graph5.add_dimension("タイプ2",dim_pos=2,vdim=True,dim_val = "タイプ2")
poke_graph5 = poke_graph5.add_dimension("HP",dim_pos=2,vdim=True,dim_val = "HP")
poke_graph5 = poke_graph5.add_dimension("攻撃",dim_pos=2,vdim=True,dim_val = "攻撃")
poke_graph5 = poke_graph5.add_dimension("防御",dim_pos=2,vdim=True,dim_val = "防御")
poke_graph5 = poke_graph5.add_dimension("特攻",dim_pos=2,vdim=True,dim_val = "特攻")
poke_graph5 = poke_graph5.add_dimension("特防",dim_pos=2,vdim=True,dim_val = "特防")
poke_graph5 = poke_graph5.add_dimension("素早さ",dim_pos=2,vdim=True,dim_val = "素早さ")

# ツールチップの設定
TOOLTIPS_2 = """
<div style="clear:both; margin: 0px 10px 0px 10px;">
  <div style="float:left ;margin: 0px 0px 0px 0px;">
    <p style="margin: 5px 0px 5px 5px"><span style="font-size:12px; color:navy ;font-weight:bold">@name</span></p>
    <img src="@img_path" height="72" alt="" width="72"><br>
    <span style="font-size:10px">
      たかさ：@{height}{0.f} m<br>
      おもさ：@{weight}{0.f} kg<br>
    </span>
  </div>
  <div style="float:right">
    <span style="font-size:10px">
      <div style="clear:both">
        <p align="right">タイプ：&nbsp;@{タイプ1}&nbsp;&nbsp;&nbsp;@{タイプ2}</p>
      </div>
      <div style="clear:both">
        <table align="right" style="margin: 5px 0px 0px 0px">
          <tr><td align="right">ＨＰ： </td><td align="right">@HP</td></tr>
          <tr><td align="right">こうげき： </td><td align="right">@{攻撃}</td></tr>
          <tr><td align="right">ぼうぎょ： </td><td align="right">@{防御}</td></tr>
          <tr><td align="right">とくこう： </td><td align="right">@{特攻}</td></tr>
          <tr><td align="right">とくぼう： </td><td align="right">@{特防}</td></tr>
          <tr><td align="right">すばやさ： </td><td align="right">@{素早さ}</td></tr>
        </table>
      </div>
    </span>
  </div>
</div>
"""
hover2 = HoverTool(tooltips=TOOLTIPS_2)

# グラフにホバーツールの設定適用
poke_graph5 = poke_graph5.opts(plot=dict(tools=[hover2]))
```
</div></ditails>

![20201230_093518.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/2bdaadd2-fd8c-073b-6ffd-042b543b307b.png)
GoogleCoboratoryではローカル画像が表示されないため、上記のようになってしまう。
HTML形式で保存（後述）し、ファイルから開けば画像が表示される。
![20201230_093708.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/5a9417e3-c547-190f-7629-367f43f9dc77.png)
（画像は公式ポケモンずかんから拝借したものをローカルに保存して参照）

##結果をファイルに保存
Holoviews(Bokeh)のグラフはHTML形式でファイル出力することができる。
ファイル出力すれば、python環境でないPCでもグラフをグリグリすることが出来るはず。

```python
# グラフをhtmlで保存
renderer = hv.renderer('bokeh')
renderer.save(poke_graph5, 'output_poke_graph5')
```

上記の例だとpoke_graph5をoutput_poke_graph5.htmlとして出力する。
出力先はカレントディレクトリ。
GoogleColaboの場合は`drive.mount("/content/gdrive")`でグーグルドライブをマウントし、`%cd "gdrive/My Drive/任意のフォルダ"`でカレントディレクトリの変更すればよい。

#所感
Holoviewsは最初こそ書き方がいまいち掴めなかったが、kdimsとvdimsさえ理解すればシンプルな記述ができると感じた。
ただ、**現状の理解力だと職場のエクセル作業を置き換えるにはまだハードルが高いなぁ**と感じてしまう。
Holoviews（Bokeh）で出来ることはまだまだありそうなので、もっと勉強してエクセルで実現できないことを探さねば。
職場のエクセル作業をPythonに置き換えられた人の体験談とかも聞いてみたい。

#参考
+ [Pythonの可視化ツールはHoloViewsが標準になるかもしれない](https://qiita.com/driller/items/53be86cea3c3201e7e0f)（Qiita記事）
+ [HoloViews公式ユーザーガイド](https://holoviews.org/user_guide/index.html)（外部サイト・英語）
+ [ポケモンずかん（公式）](https://zukan.pokemon.co.jp/)（外部サイト）
+ [ポケモンWiki](https://wiki.ポケモン.com/wiki/%E3%83%95%E3%82%B7%E3%82%AE%E3%83%80%E3%83%8D)（外部サイト）