ipywidgetsとBokeh使ってインタラクティブな可視化をする

# なぜ対話型なのか

何らかのパラメータが入ったグラフを出力したい場合、パラメータを変化させる度に確認する方法としては下記のような方法が考えられます。

1. 出力関数の引数を都度入力してグラフを出力
2. 変化させたいパラメータの全てのグラフを出力
3. 対話型でパラメータを設定してグラフを出力

`1`の方法は入力が少し面倒です。IPythonのコンソール上ならまだしも、`Jupyter notebook`で関数を実行して、実行した関数の引数を書き換えて・・・はい、面倒ですね。  
`2`の方法はどうでしょうか。2〜3個のグラフを一度に出力して比較するのは十分実用的と言えそうです。  
しかし、みたいパラメータが100個あるような状況では人間の目で確認するのが大変そうです。  
ということで、`3`の方法を実装してみます。  

# ipywidgetsのインストール

`Jupyter notebook`で対話型のUIを実装するには`ipywidgets`を使うのがお手軽です。  
インストール方法は下記のとおりです。

## pipの場合

```bash
pip install ipywidgets
jupyter nbextension enable --py widgetsnbextension
```

## condaの場合

```bash
conda install -c conda-forge ipywidgets
```

# ipywidgets.interact

目的の関数に`ipywidgets.interact`をデコレートするだけで、対話型のUIを作ることが可能です。  
下記の例では株価を取得し、n日間の終値の移動平均をグラフに出力しています。  
`interact`の引数に数値を与えると、`Jupyter notebook`上にスライダーが表示され、これを調整することで移動平均のパラメータが変化します。  
`n=(5, 30)`は最小値を`5`、最大値を`30`に設定しています。  
`n=10`と、特に制限を設けずにデフォルト値のみ設定する方法もあります。  

```python
%matplotlib inline

from pandas_datareader.data import DataReader
from ipywidgets import interact

price = DataReader('^GSPC', 'yahoo', start='2016-01-01', end='2016-12-31')


@interact(n=(5, 30))
def plot_rolling_mean(n):
    price['Adj Close'].plot()
    price['Adj Close'].rolling(n).mean().plot()
```

![rolling_mean.gif](https://qiita-image-store.s3.amazonaws.com/0/22023/42c45589-1647-85a6-87bb-5e8f34b1af7c.gif)

このように、UIで視覚的にパラメータの適正値を探すことが可能となります。  

もう一つの例として、グラフの種類を変更してみます。  
`interact`の引数にタプルやリストを与えるとドロップダウンメニューが表示されます。  
これを利用してUIでグラフの種類を変更してみます。

```python
@interact(kind=['line', 'area'])
def plot_line_or_band(kind):
    price['Adj Close'].plot(kind=kind)
```

![chart_type.gif](https://qiita-image-store.s3.amazonaws.com/0/22023/6bac06d1-a158-1355-dca3-030d3608acc3.gif)

このように予めUIと選択肢を用意しておくことで、プログラムがわからない相手に対しても、直感的に操作を促すことが可能となります。

# ipywidgetsをも少し詳しく

[ドキュメント](https://ipywidgets.readthedocs.io/en/latest/index.html)にもありますが、`ipywidgets`が提供しているUIの一覧は`ipywidgets.widgets.Widget.widget_types`から参照可能です。

```python
import ipywidgets as widgets
widgets.Widget.widget_types
```

これだけあります・・・

```py
{'Jupyter.Accordion': ipywidgets.widgets.widget_selectioncontainer.Accordion,
 'Jupyter.BoundedFloatText': ipywidgets.widgets.widget_float.BoundedFloatText,
 'Jupyter.BoundedIntText': ipywidgets.widgets.widget_int.BoundedIntText,
 'Jupyter.Box': ipywidgets.widgets.widget_box.Box,
 'Jupyter.Button': ipywidgets.widgets.widget_button.Button,
 'Jupyter.Checkbox': ipywidgets.widgets.widget_bool.Checkbox,
 'Jupyter.ColorPicker': ipywidgets.widgets.widget_color.ColorPicker,
 'Jupyter.Controller': ipywidgets.widgets.widget_controller.Controller,
 'Jupyter.ControllerAxis': ipywidgets.widgets.widget_controller.Axis,
 'Jupyter.ControllerButton': ipywidgets.widgets.widget_controller.Button,
 'Jupyter.Dropdown': ipywidgets.widgets.widget_selection.Dropdown,
 'Jupyter.FlexBox': ipywidgets.widgets.widget_box.FlexBox,
 'Jupyter.FloatProgress': ipywidgets.widgets.widget_float.FloatProgress,
 'Jupyter.FloatRangeSlider': ipywidgets.widgets.widget_float.FloatRangeSlider,
 'Jupyter.FloatSlider': ipywidgets.widgets.widget_float.FloatSlider,
 'Jupyter.FloatText': ipywidgets.widgets.widget_float.FloatText,
 'Jupyter.HTML': ipywidgets.widgets.widget_string.HTML,
 'Jupyter.Image': ipywidgets.widgets.widget_image.Image,
 'Jupyter.IntProgress': ipywidgets.widgets.widget_int.IntProgress,
 'Jupyter.IntRangeSlider': ipywidgets.widgets.widget_int.IntRangeSlider,
 'Jupyter.IntSlider': ipywidgets.widgets.widget_int.IntSlider,
 'Jupyter.IntText': ipywidgets.widgets.widget_int.IntText,
 'Jupyter.Label': ipywidgets.widgets.widget_string.Label,
 'Jupyter.PlaceProxy': ipywidgets.widgets.widget_box.PlaceProxy,
 'Jupyter.Play': ipywidgets.widgets.widget_int.Play,
 'Jupyter.Proxy': ipywidgets.widgets.widget_box.Proxy,
 'Jupyter.RadioButtons': ipywidgets.widgets.widget_selection.RadioButtons,
 'Jupyter.Select': ipywidgets.widgets.widget_selection.Select,
 'Jupyter.SelectMultiple': ipywidgets.widgets.widget_selection.SelectMultiple,
 'Jupyter.SelectionSlider': ipywidgets.widgets.widget_selection.SelectionSlider,
 'Jupyter.Tab': ipywidgets.widgets.widget_selectioncontainer.Tab,
 'Jupyter.Text': ipywidgets.widgets.widget_string.Text,
 'Jupyter.Textarea': ipywidgets.widgets.widget_string.Textarea,
 'Jupyter.ToggleButton': ipywidgets.widgets.widget_bool.ToggleButton,
 'Jupyter.ToggleButtons': ipywidgets.widgets.widget_selection.ToggleButtons,
 'Jupyter.Valid': ipywidgets.widgets.widget_bool.Valid,
 'jupyter.DirectionalLink': ipywidgets.widgets.widget_link.DirectionalLink,
 'jupyter.Link': ipywidgets.widgets.widget_link.Link}
 ```

流石にこれを全て説明する体力はないので、一例として、`ipywidgets.widgets.Dropdown`を使ってみます。

```py
from IPython.display import display
d = widgets.Dropdown(options=['red', 'green', 'blue'], value='blue')


def on_value_change(change):
    print(change['new'])


d.observe(on_value_change, names='value')

display(d)
```

`IPython.display.display`にてドロップダウンを表示し、`observe`メソッドにてイベントをハンドリングしています。  
キーワード引数`names`には`on_value_change`関数に渡すプロパティが渡されます。通常は`'value'`を使用します。    
ドロップダウンの値が変更されると、`on_value_change`が呼び出され、`'value'`プロパティの値が出力されます。  

# Bokehのpush_notebookと組み合わせてみる

`Bokeh`には`bokeh.io.push_notebook`という便利なメソッドがあり、既に出力されているグラフをハンドリングして、内容の変更をpushさせることができます。  
前述したドロップダウンを使用して、グラフオブジェクトの色を動的に変更してみます。

```python
from IPython.display import display
import ipywidgets as widgets
from bokeh.io import output_notebook, push_notebook
from bokeh.plotting import figure, show

d = widgets.Dropdown(options=['red', 'green', 'blue'], value='blue')


def on_value_change(change):
    r.glyph.fill_color = change['new']
    push_notebook(handle=t)


d.observe(on_value_change, names='value')

p = figure(width=250, height=250)
r = p.circle(1, 1, size=20, line_color=None)

output_notebook()

display(d)
t = show(p, notebook_handle=True)
```

![change_color.gif](https://qiita-image-store.s3.amazonaws.com/0/22023/1b5213a8-878d-4b5b-d6b7-c8383409e4a6.gif)

ここでのポイントは下記の2点です。

* `t = show(p, notebook_handle=True)`と`show`メソッドのキーワード引数に`notebook_handle=True`を設定し、グラフをハンドリング可能な状態にしていること
* `push_notebook(handle=t)`にて上記で設定したハンドラに対して変更をpushしていること

このように`Bokeh`では`Jupyter notebook`上のグラフオブジェクトに対して変更を加えることが可能です。  
前述の`matplotlib`ではグラフ全体の再描画の処理が走りましたが、`Bokeh`では必要な部分のみを変更できるため、動的なグラフの処理を軽くすることができます。

`Bokeh`は`matplotlib`と比較すると、後発である分、最初から`Jupyter notebook`を意識した機能があり、`Jupyter notebook`との相性がよい可視化ツールであると言えそうです。

# おまけ(という名の本編)

ここまでが前置きでした。  
本記事はjupyter notebook Advent Calendar 2016 16日目です。  
15日目が[やきうの人](http://shinyorke.hatenablog.com/entry/2016/12/11/203146)だったので、続けてやきうネタをやってみます。  

先程のグラフ上の円の位置を動的に変化させて、上中下に投げ分けができるピッチャーがいるという前提で、簡易的なピッチングマシンを作ってみます。

```python
from random import randint
from bokeh.io import output_notebook, push_notebook
from bokeh.plotting import figure, show
from IPython.display import display
import ipywidgets as widgets


STRIKE_ZONE_HEIGHT = 750  # ストライクゾーンの高さ
STRIKE_ZONE_WIDTH = 432  # ストライクゾーンの幅
STRIKE_ZONE_DIV = 3  # 3分割して投げ分ける
zone_low = (0, int(STRIKE_ZONE_HEIGHT / STRIKE_ZONE_DIV))  # 低めのレンジ
zone_mid = (zone_low[1], zone_low[1] * 2)  # 真ん中のレンジ
zone_high = (zone_mid[1], STRIKE_ZONE_HEIGHT)  # 高めのレンジ


# コースを決定
def get_cause(zone):
    return randint(0, STRIKE_ZONE_WIDTH), randint(*zone)


# 決められたコースに投げる
def pitch(zone):
    x, y = get_cause(zone)
    r.data_source.data['x'] = [x]
    r.data_source.data['y'] = [y]
    # ここがポイント！指定したハンドルにpush
    push_notebook(handle=t)


# ボタンをクリックしたときの動作
def on_button_clicked(b):
    cause_dict = {'High': zone_high, 'Mid': zone_mid, 'Low': zone_low}
    pitch(cause_dict[b.description])

# ボタンオブジェクトを作成
h = widgets.Button(description="High")
m = widgets.Button(description="Mid")
l = widgets.Button(description="Low")

# クリックしたときのイベントをハンドリング
h.on_click(on_button_clicked)
m.on_click(on_button_clicked)
l.on_click(on_button_clicked)


output_notebook()


p = figure(
    width=250,
    height=250,
    x_range=(0, STRIKE_ZONE_WIDTH),
    y_range=(0, STRIKE_ZONE_HEIGHT))
# 初期値
r = p.circle([STRIKE_ZONE_WIDTH / 2], [STRIKE_ZONE_HEIGHT / 2], size=20)
# notebook_handle=Trueをつけることで、後で操作できるように
t = show(p, notebook_handle=True)
display(h)
display(m)
display(l)
```

![pitch.gif](https://qiita-image-store.s3.amazonaws.com/0/22023/eba6af32-4731-0527-9d47-cf7fbaeac682.gif)

ボタンに関しては`on_click`というイベントハンドラで制御を渡すことができます。  
詳細については[Widget Eventsのドキュメント](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html)をご確認ください。  
今回は乱数でコースの座標を決めましたが実データを突っ込んで球種や球速の情報を加えれば、野球の実況サイトに見劣りしないオレオレ一球速報を実装することができると思います。  
実データのとり方は[前の人](http://shinyorke.hatenablog.com/entry/2016/12/11/203146)がやってくれたので、われこそはという方は是非作ってみてください。  

# まとめ

少し長くなりましたが、今回お伝えしたかったことをまとめるとこんな感じになります。  

* `interact`をデコレートするだけで超かんたんにUIを作れる
* 少し凝ったやつは`widgets`から目的のものを自作できる
* `Bokeh`の`push_notebook`を使うと動的なグラフを作りやすくなる

`ipywidgets`と`Bokeh`は`Jupyter notebook`との相性がよいツールですが、認知度が低い印象です。  
これを機に使ってみようかなと思っていただけた方がいれば幸いです。