pythonでjupyter notebook上でplotlyを使ってこんなことが出来ます。
plotlyで散布図をクリックしたときに何か起こせないの気持ち悪いなあ、JavaScript使わないとできないの不便だなあ、ということを1年くらい思っていました。Pythonだけでできました。私が知らなかっただけでした。
なかなか便利そうな匂いがしますが、公式のドキュメントにもあまり情報がないみたいです1。ということで、これを使えるようになることがこの記事の目標です。
とりあえずコード全体はこちらになります。以下一歩一歩見ていきます。
df = pd.read_csv('./data/articles.csv')
fig = go.FigureWidget(px.scatter(df, x='page_views_count', y='likes_count'))
n = len(df)
scatter = fig.data[0]
colors = ['blue'] * n
scatter.marker.color = colors
scatter.marker.size = [10] * n
def update_point(trace, points, selector):
c = list(scatter.marker.color)
s = list(scatter.marker.size)
for i in points.point_inds:
c[i] = 'red'
s[i] = 20
with fig.batch_update():
scatter.marker.color = c
scatter.marker.size = s
scatter.on_click(update_point)
fig
環境
Windows 11
Python 3.8.5
jupyter 1.0.02
plotly 5.7.0
使用データ
本記事はdelikaさんの下記イベントに参加しています。参加するつもりだったのですが、いつのまにかGWが終わり5月も中旬になってました。おかしいなぁ。
データはイベントで用意されているQiitaの投稿に関するダミーデータを用います3。
Widgetについて理解する
plotlyのこの機能はipywidgetsを使っています4。ipywidgetはこんな感じのことができます
import ipywidgets as widgets
def on_click_callback(clicked_button):
print('Clicked')
button = widgets.Button(description='Click me')
button.on_click(on_click_callback)
button
on_click
というメソッドで、クリックしたときに呼び出されるcallback関数を登録することができます。この例だと、ボタンをclickしたときに on_click_callback
というコールバック関数が呼び出されるわけです。
plotlyでは、go.FigureWidgetを使うことでWidgetとして動かせる(=クリックイベントを起こせる)グラフを作れます。FigureWidgetのTrace( fig.data
でアクセスできるScatterなどのgraph_objs。今回の例ではscatter
)が on_click
というメソッドを持っていて、この例のボタンと同じように機能します。
fig = go.FigureWidget(px.scatter(df, x='page_views_count', y='likes_count'))
scatter = fig.data[0]
def update_point(trace, points, selector):
pass
scatter.on_click(update_point)
callback関数を理解する
ではcallback関数を見ていきましょう。なんとなく色とサイズを変えていることはイメージできると思います。
引数から説明していきましょう。
def update_point(trace, points, selector):
c = list(scatter.marker.color)
s = list(scatter.marker.size)
for i in points.point_inds:
c[i] = 'red'
s[i] = 20
with fig.batch_update():
scatter.marker.color = c
scatter.marker.size = s
callbackの引数①:trace
traceは scatter.on_click(update_point)
の scatter
に相当するものです5。
callbackの引数②:points
こちらはクリックされたポイントの情報が入ります。 update_point
を以下のように書き換えて適当な点をクリックすると
def update_point_example(trace, points, selector):
print(points)
このような内容が出力されます。これは、398行目をクリックして、そのxの値(page_views_count)が484、yの値(likes_count)が1177ということを示しています6。
update_point
の例だと、このクリックした points
に該当する点の色とサイズを point_inds
経由で指定しています。ちなみに on_click
だと一度にクリックされるポイントの数は高々ひとつだと思うので、for文で回している理由はあまりないです7。
callbackの引数③:selector
これは今回の例では使われていませんが、先ほどと同じようにprintして中身を確かめましょう8。すると、以下が出力されます。
ctrlやaltなどが出てくるので、もしやと思ってctrlを押しながらクリックすると、 ctrl=True
に変わります。つまり、押し方よって挙動を変えられるという訳です9。
ちなみに button、buttonsは右クリックをすると値が変わりました。metaはちょっとよく分からなかったです。
batch_updateは何をしてるか
複数の処理を一度に行うために batch_update
を使います。 batch_update
がない、以下のgifを見比べると分かりやすいです。
2段階で処理がなされていることが分かります。これをいっぺんに変えるのが batch_update
の役割です。
これで、冒頭に述べた例がどういう仕組みで動いているか理解できたと思います。
注意点!!
plotlyでは fig.show()
と書くことが一般的ですが、FigureWidgetの場合は単に fig
あるいは、 display(fig)
とする必要があります(fig.show()
とすると、クリックしても何も反応しないグラフが表示されます)。
以下、色々なTipsや応用例などをご紹介していきます。
複数のtraceがある場合どうすればいいか
plotlyを使っていると、Traceが複数ある図を作るケースも多くあります。たとえば、plotly.expressで color
を指定すると複数Traceになります。先ほどみたように on_click
はTraceの中身に対して呼ぶものなので、for文で全部に対して on_clike
を定義してあげればOKです。
df['type'] = np.random.randint(0, 2, len(df))
fig = go.FigureWidget(px.histogram(df, x='likes_count', color='type'))
# len(fig.data) # 2
def update_point(trace, points, selector):
print('clicked')
for hist in fig.data:
hist.on_click(update_point)
fig
ただしひとつ罠があります。上の例だと、このようになってしまいます。
1回のクリックに対して、Traceの数ぶんだけcallback関数が呼ばれるため、ここでは2回 print('clicked')
が実行されてしまっています。
これはクリックした点や棒を持たないTraceもcallback関数を呼び出すことが原因です。したがって対処するには、if points.point_inds:
とか for point in points.point_inds:
とかで、空のpointに対しては何も動かさないようにcallback関数を書き換えてあげればOKです10。
def update_point(trace, points, selector):
if points.point_inds:
print('clicked')
figureの見た目を変える以外の使い方
いくらでもあると思いますが、自分がパッと思いついたものを羅列しておきます。他にもアイデアなどあったらぜひコメントで教えてください!
複数のグラフを連動させる
こちらで紹介されている例です。上のグラフで日付を選ぶと、その日のデータが下の左右のグラフに出力されます。
hoverで表示している内容をcopyする
plotlyの使いずらいポイントのひとつに、hoverされた情報をコピーできない点があります
pyperclipとかを使えば対処できます。
import pyperclip as pc
def update_point(trace, points, selector):
for i in points.point_inds:
pc.copy(i)
print('copied!')
urlを開く
webbrowserを使ってクリックしたら開くようにするとか、
import webbrowser
def update_point(trace, points, selector):
webbrowser.open('http://example.com')
リンク付きのHTMLを表示するとかができそうです。
from IPython.display import HTML
def update_point(trace, points, selector):
link_text = f'<a href={url}>open link</a>'
display(HTML(link_text))
一度出力を消す
printとかを多用してると溜まって見にくいので、こうすることができます(が、レスポンスは悪いのでおそらく微妙です)。これまでにクリックした一連のものを対象にしたdataframeを出力するとかができます。
from IPython.display import clear_output
def update_point(trace, points, selector):
# 一旦出力を消す
clear_output(True)
# figureを再表示
display(fig)
print('clicked')
jupyter以外で使う
ipywidgetを用いているので、jupyter以外での使用は私の知る限り難しいと思います。JupyterLabでは使用できますが、extensionとかの設定がいるかもしれません11。Google Colab だとできないみたいです12
Dashで似たようなことをやる方法は以下で紹介されています。
streamlitでは、以下を使うとできるかもですが、本記事で紹介する方法とは別物と思われます(特に確認などは出来てません汗)。
クリック以外のイベント
on_hover
scatter.on_hover(update_point)
fig
クリックでなく、ホバーしたときにアクションが起こるようになります。
on_unhover
※ on_hoverとほぼ同じなので略
on_selection
plotlyのレイアウトの dragmode
を変更して利用します。矩形で選択したものを対象にします。update_point
関数のなかでfor文を使って点を取り出していたのは、これでも同じように使えるというメリットがあります。
fig.update_layout(dragmode="select")
scatter.on_selection(update_point)
fig
on_deselect
def deselect_point(trace, points):
print('deselected')
fig.update_layout(dragmode="select")
scatter.on_deselect(deselect_point)
fig
同じくdragmodeにて、selectionが解消された時に実行されます。この場合、callback関数は選択していた対象は受け取らないため points
は空になります。また selector
も受け取らないことに注意です。
on_change
def zoom(layout, x_range):
print('xaxis updated, new range is: ', x_range)
fig.layout.on_change(zoom, 'xaxis.range')
fig
こいつだけ若干異質で、何かが変わったことに駆動されてcallback関数が実行されます。on_change
の一つ目の引数がcallback関数で、二つ目の引数に「何が変わったときにcallback関数を呼ぶか」を指定します。ここではx軸の範囲aが変わったときをトリガーにしています。
最後に
色々できて面白いですね。jupyter以外で使えなさそうなので、使いどころは難しい気もしますが。最後に、リサーチ中に発掘したplotly, matplotlibのあまり知られていなさそうな情報を共有して終わりにします!
jupyterでならmatplotlibでもインタラクティブなグラフは作れるみたいです。
しかも点自体を動かせるプロットも可能とのこと。
plotlyはお絵かきできる機能もあるんですね。
-
https://plotly.com/python/click-events/ 。こちらはこのドキュメントとほぼ同じ例です。ただし色とデータだけ変えています。 ↩
-
以下で述べる通りipywidgetsを用いているので、Google colaboratoryでは使用できないみたいです。jupyter Labでは使用できますが、extensionとかの設定がいるかもしれません (https://plotly.com/python/getting-started/#jupyterlab-support) 。 ↩
-
https://delika.io/qiita_delika_article_campaign/QiitadelikaDummy ↩
-
ipywidgetsについての詳細は「Python: ipywidgets で Jupyter に簡単な UI を作る 」などが参考になります。 ↩
-
そのため、
update_point
関数においてscatter
をtrace
で書き換えても同じ挙動をします。また、update_point
を次のように変えてクリックしてもエラーは起きません:def update_point(trace, points, selector): assert trace == scatter
↩ -
trace_name
は特に定義してないので空です。trace_index
は、f
はtraceをひとつしか持ってないので0になってます。それにしても(ダミーデータだからですが)likesの数がPV数を上回るというおそろしい記事ですね。 ↩ -
後述する
on_selection
とかだとこれがありがたいので、このまま使っています。 ↩ -
def update_point_example(trace, points, selector): print(selector)
↩ -
Windowsでやってます。Macだとctrlがcommandとかになるんですかね? ↩
-
この例だと
point
が適当な命名なのかは微妙なところですが、先ほどと変えても混乱しそうなのでこのままにしてます。 ↩ -
https://plotly.com/python/getting-started/#jupyterlab-support ↩