25
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

グラフ上でマウスドラッグしてデータの詳細を表示させたい

Posted at

やりたいこと

グラフ上で範囲を選択して、その範囲に含まれているデータの詳細を別テーブルなどで表示させたいです。
01_アウトプットイメージ_.jpg

できたもの

(gif)
02_output.gif

HoloViewsSelection1Dを活用して実現できました。

解説

上記をつくる過程を紹介しながら、解説をします。
過去の記事で紹介したようなHoloViewsやPanelの解説と一部重複しますが、ご承知おきください。

コード

import pandas as pd
import panel as pn
import holoviews as hv
from sklearn.datasets import load_iris
iris = load_iris()

def initialize():
  df = pd.DataFrame(iris.data, columns=iris.feature_names)
  df['species'] = iris.target_names[iris.target]
  return df

# ホバーツールのインポート
from bokeh.models import HoverTool

# 初期化
df = initialize()
# 列名変更(列名に()が含まれるとホバーツールの表示おかしくなるため)
df.columns = [column.replace(" ", "_").replace("(", "").replace(")", "") for column in df.columns]

# # Panelを使うためのおまじない
pn.extension()
# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# Selectウィジェットの定義
x_ax = pn.widgets.Select(name= "X軸", options=list(df.columns), width=150)
y_ax = pn.widgets.Select(name= "Y軸", options=list(df.columns), width=150)
size_ax = pn.widgets.Select(name= "サイズ", options=list(df.columns), width=150)
color_ax = pn.widgets.Select(name= "カラー", options=list(df.columns), width=150)
# Selectウィジェットの初期値指定(すべて同じにせず一つずつずらす)
for i, widget in enumerate([x_ax, y_ax, size_ax, color_ax]):
  widget.value = widget.options[min(i, len(widget.options))]

# グラフ出力パネル定義(データ未入力だとエラー出るので仮グラフデータ格納)
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

# ホバーツールの設定
TOOLTIPS = [(e, "@{" + e + "}") for e in list(df.columns)]

# グラフ表示用関数
def display_graph(event):
    # 関数外でも使用するグラフデータ+選択データをglobal宣言
    global scatter
    global selection

    # サイズを規定する列を追加
    def minmax_norm(df_input):
      return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
    df["size"] = (minmax_norm(df[size_ax.value]) + 1) * 5

    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims=[x_ax.value], vdims=[y_ax.value] + list(df.columns))  # hoverで表示するデータはすべてvdimsに追加する必要あり
    scatter = scatter.opts(color=color_ax.value, cmap="Category10", legend_position="right", 
                       width=550, height=400, fontscale=1.5, size="size", 
                       show_grid=True, title="Iris_data",
                       tools=[HoverTool(tooltips=TOOLTIPS), "box_select"],
                       active_tools=["box_select"]
                       )

    # 選択された領域のデータを取得・格納
    selection = hv.streams.Selection1D(source= scatter)

    # 選択された領域が変わるたびに実行される関数
    @pn.depends(selection.param.index, watch=True)
    def selected(selected_index):
        # データフレームの更新と表示
        selection_info.value = sdf.query("index in @selected_index").drop("size", axis=1)
        selection_info.visible = True
        
    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True

# 選択データ表示テーブルの定義
selection_info = pn.widgets.DataFrame(df, width=400, height=400, visible=False, row_height=25, autosize_mode="fit_viewport")

# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力", width=50, margin = [30, 10, 10, 10])
display_graph_but.on_click(display_graph)

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane, selection_info)

環境

  • Windows10 + Jupyterlab
    (Google Colabでも実装可能ですが、重い+謎の警告出るためローカル環境推奨)
  • Python 3.9.1
  • HoloViews 1.14.5
  • Panel 0.12.1

インポートと使用データの取得

ライブラリとデータの準備を行ないます。
使用するデータですが、sklearnを使ってみんな大好きIRISデータをダウンロードします。

import pandas as pd
import panel as pn
import holoviews as hv
from sklearn.datasets import load_iris
iris = load_iris()

def initialize():
  df = pd.DataFrame(iris.data, columns=iris.feature_names)
  df['species'] = iris.target_names[iris.target]
  return df

pandas,sklearnの他、holoviews, panel, bokehは必須のライブラリなので、未インストールの場合はpipコマンドでインストールしておきます(ここでは割愛)。

基本のグラフをHoloviewsで書く

# 初期化
df = initialize()

# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# 描画するグラフデータの生成
scatter = hv.Scatter(df, kdims=["sepal length (cm)"], vdims=["petal length (cm)"]) 
scatter = scatter.opts(width=500, height=400, fontscale=1.5, size=5,
                   show_grid=True, title="Iris_data")

# 表示
scatter

HoloViewsを使用する場合、下記のように毎回セルごとにおまじないをする必要があります

# HoloViewsのバックエンド指定
hv.extension("bokeh")

グラフを描画します。第一引数に使用するデータ(今回の場合dfというDataFrame)を指定し、kdims(=Key Dimensions)にX軸にあたる要素、vdims(=Value Dimensions)にY軸にあたる要素をリストで指定します。

# 描画するグラフデータの生成
scatter = hv.Scatter(df, kdims=["sepal length (cm)"], vdims=["petal length (cm)"])

その後、.opts()メソッドを使用してグラフの様々な設定を変更。

scatter = scatter.opts(width=550, height=400, fontscale=1.5, size=5,
                   show_grid=True, title="Iris_data"
)

表示は作成したインスタンスをそのまま書くだけで、ノート上で表示してくれます(DataFrameなどと同じ感覚)

# 表示
scatter

ここまでの結果
  ↓
10_default_holoviews.JPG

グラフの色を変更する

この章までで書くコード(クリックして表示)
# 初期化
df = initialize()

# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# 描画するグラフデータの生成
scatter = hv.Scatter(df, kdims=["sepal length (cm)"], vdims=["petal length (cm)", "species"]) 
scatter = scatter.opts(color="species", cmap="Category10", legend_position="right", 
                   width=550, height=400, fontscale=1.5, size=5,
                   show_grid=True, title="Iris_data",
                   )

# 表示
scatter

データ内の特定の列の情報をもとにグラフの色を設定したい場合、vdimsに指定したい列名を追加した上で、optsメソッドでcolor引数にその列名を追加します。数値でもカテゴリデータでも同じ書き方でOKです。

# 描画するグラフデータの生成
scatter = hv.Scatter(df, kdims=["sepal length (cm)"], vdims=["petal length (cm)", "species"])
scatter = scatter.opts(color="species", cmap="Category10", legend_position="right",
                   width=550, height=400, fontscale=1.5, size=5,

また、optsメソッドのcmapでカラーマップを、legend_positionで凡例の表示位置を指定できます。

ここまでの結果
  ↓
11_withcolor_holoviews.JPG

グラフの各データの表示サイズを変更する

この章までで書くコード(クリックして表示)
# 初期化
df = initialize()

# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# サイズを規定する列を追加
def minmax_norm(df_input):
  return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
df["size"] = (minmax_norm(df["petal width (cm)"]) + 1) * 5

# 描画するグラフデータの生成
scatter = hv.Scatter(df, kdims=["sepal length (cm)"], vdims=["petal length (cm)", "species", "size"]) 
scatter = scatter.opts(color="species", cmap="Category10", legend_position="right", 
                   width=550, height=400, fontscale=1.5, size="size", 
                   show_grid=True, title="Iris_data",
                   )

# 表示
scatter

バブルチャートのように、ある列のデータから各点のサイズを変更することもできます。
色の時と同じような指定が可能なのですが、データの数値をそのまま当てはめてしまうと、点が異常に大きくなってしまうため、所望のサイズになるよう数値を変換する必要があります。
やり方は色々ありますが、今回はPandasの列(Series)を受け取って0~1で正規化した列を返す関数を作り、その数値を加算乗算して所望の数値に変換する方法を採用しました。

# サイズを規定する列を追加
def minmax_norm(df_input):
  return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
df["size"] = (minmax_norm(df["petal width (cm)"]) + 1) * 5

size列を作成したら、あとは色の時と同じように追加した列名(ここではsize)をvdimsに追加し、optsメソッドのsize引数に指定するだけです。

# 描画するグラフデータの生成
scatter = hv.Scatter(df, kdims=["sepal length (cm)"], vdims=["petal length (cm)", "species", "size"])
scatter = scatter.opts(color="species", cmap="Category10", legend_position="right",
                   width=550, height=400, fontscale=1.5, size="size",

ここまでの結果
  ↓
12_withsize_holoviews.JPG

プルダウンで各軸に指定する項目を選べるようにする

この章までで書くコード(クリックして表示)
# 初期化
df = initialize()

# # Panelを使うためのおまじない
pn.extension()
# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# Selectウィジェットの定義
x_ax = pn.widgets.Select(name= "X軸", options=list(df.columns), width=150)
y_ax = pn.widgets.Select(name= "Y軸", options=list(df.columns), width=150)
size_ax = pn.widgets.Select(name= "サイズ", options=list(df.columns), width=150)
color_ax = pn.widgets.Select(name= "カラー", options=list(df.columns), width=150)
# Selectウィジェットの初期値指定(すべて同じにせず一つずつずらす)
for i, widget in enumerate([x_ax, y_ax, size_ax, color_ax]):
  widget.value = widget.options[min(i, len(widget.options))]

# グラフ出力パネル定義(データ未入力だとエラー出るので仮グラフデータ格納)
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

# グラフ表示用関数
def display_graph(event):
    # 関数外でも使用するグラフデータ+選択データをglobal宣言
    global scatter

    # サイズを規定する列を追加
    def minmax_norm(df_input):
      return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
    df["size"] = (minmax_norm(df[size_ax.value]) + 1) * 5

    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims=[x_ax.value], vdims=[y_ax.value, color_ax, "size"])
    scatter = scatter.opts(color=color_ax.value, cmap="Category10", legend_position="right", 
                       width=550, height=400, fontscale=1.5, size="size", 
                       show_grid=True, title="Iris_data",
                       )

    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True

# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力", width=50, margin = [30, 10, 10, 10])
display_graph_but.on_click(display_graph)

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane)

ここら辺からpanelというライブラリを使用してインタラクティブ要素を追加していきます。
まず、プルダウンを追加してユーザーが自由に軸の項目を選択できるようにします。

準備

panelは、使用前にpn.extension()を実行する必要があります。

# Panelを使うためのおまじない
pn.extension()

軸の項目を選択するプルダウンの作成

panelを使用する準備ができたら、まず各軸を選択できるSelect(プルダウンで選択肢を選択させる)ウィジェットのインスタンスをpn.widgets.Select()で作成します。
options引数にリストを与えると、選択肢として表示されるようになります。
今回はシンプルにdf.columnsをリストに変換して渡しました(データ型を無視しているので項目によってはエラーがでます)

# Selectウィジェットの定義
x_ax = pn.widgets.Select(name= "X軸", options=list(df.columns), width=150)
y_ax = pn.widgets.Select(name= "Y軸", options=list(df.columns), width=150)
size_ax = pn.widgets.Select(name= "サイズ", options=list(df.columns), width=150)
color_ax = pn.widgets.Select(name= "カラー", options=list(df.columns), width=150)

ウィジェットの名前やサイズはお好みで。

インスタンスを生成出来たら、実際にウィジェットを配置してみます。
pn.Column()を使用すれば、縦方向にウィジェットを配置できます。

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane)

今回、少し窮屈に感じたのでmarginを指定して間隔を空けています。

あと、試行錯誤を繰り返す中で毎回軸項目を選択しなおすのが面倒だったので、Selectの初期値を一つずつズらして別の項目が初期値になるように設定しています。

# Selectウィジェットの初期値指定(すべて同じにせず一つずつずらす)
for i, widget in enumerate([x_ax, y_ax, size_ax, color_ax]):
  widget.value = widget.options[min(i, len(widget.options))]

グラフ表示ボタンの作成

次に、グラフを表示させるためのボタンを作っていきます。
まず、pn.widgets.Button()メソッドでButtonインスタンスを生成します。
name引数には、ボタンに表示させたいテキストを指定します。

# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力", width=50, margin = [30, 10, 10, 10])

widthなどのボタンのサイズ指定はお好みで。
また、Selectウィジェットとも少し間隔を空けたいので、margin引数にリストを指定して少し細かくマージンを設定しています。
marginに与えるリストの中はHTMLと同様で[上、右、下、左]の順となります。0時スタートの時計回りと考えれば覚えやすいですかね。

表示はSelectウィジェットの下に配置するので、pn.Columnの中に生成したインスタンスを追加します。

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane)

ボタンを押下したときの動作の指定

続いて、Buttonを押下したときの動作を指定していきます。
まず、defで引数にeventを指定した関数を作ります。

# グラフ表示用関数
def display_graph(event):

その中で実際に動作させたい処理を書いていきます。
今回の場合、まず前項まで書いてきたグラフ描画のコードを移植します。
そこから、Selectウィジェットで選択されている値でグラフを描画するようにしたいので、列名を直接指定していた箇所をSelectウィジェット.valueで選択されている項目を受け取れるようにします。
sizeは正規化の際にsize.valueを渡すので、optsメソッドのsize引数には固定の"size"を渡しておけばOKです。

# グラフ表示用関数
def display_graph(event):
        :
    # サイズを規定する列を追加
    def minmax_norm(df_input):
      return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
    df["size"] = (minmax_norm(df[size_ax.value]) + 1) * 5
        :
    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims=[x_ax.value, ], vdims=[y_ax.value, color_ax, "size"])
    scatter = scatter.opts(color=color_ax.value, cmap="Category10", legend_position="right",
                       width=550, height=400, fontscale=1.5, size="size",
                       show_grid=True, title="Iris_data",
                       )

扱うscatterは関数外でも使用するため、globalで宣言をしておきます。

# グラフ表示用関数
def display_graph(event):
    # 関数外でも使用するグラフデータ+選択データをglobal宣言
    global scatter

処理をする関数をつくれたら、先程作ったButtonインスタンスに.on_click()メソッドを使って処理を登録します。

# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力", width=50, margin = [30, 10, 10, 10])
display_graph_but.on_click(display_graph)

グラフ表示領域の作成

次にグラフを表示させる領域を作っていきます。
HoloViewsグラフの表示領域を作るには、pn.pane.HoloViews()メソッドを使用します。

# グラフ出力パネル定義(データ未入力だとエラー出るので仮グラフデータ格納)
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

しかし、これをvisible=False(見えない状態)にしても、HoloViewsインスタンスを与えていないとエラーになってしまいます。なので、適当なオブジェクトを指定することでエラーを回避しています。

# グラフ出力パネル定義(データ未入力だとエラー出るので仮グラフデータ格納)
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

表示するパネルが用意できたら、先ほど作成したグラフ描画関数に、このパネルで表示するグラフを変更し、さらに非表示を解除する処理を加えます。

# グラフ表示用関数
def display_graph(event):
        :
    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True

グラフ表示パネルは、pn.Row()を使用してこれまで作成してきたウィジェットの横に配置をします。
尚、pn.Rowとpn.Columnは組み合わせて使用可能です。

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane)

ここまでの結果(gif)
  ↓
13_interactive_trim.gif

オンマウスでデータを表示する機能(ホバーツール)追加

この章までで書くコード(クリックして表示)
# ホバーツールのインポート
from bokeh.models import HoverTool

# 初期化
df = initialize()
# 列名変更(列名に()が含まれるとホバーツールの表示おかしくなるため)
df.columns = [column.replace(" ", "_").replace("(", "").replace(")", "") for column in df.columns]

# # Panelを使うためのおまじない
pn.extension()
# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# Selectウィジェットの定義
x_ax = pn.widgets.Select(name= "X軸", options=list(df.columns), width=150)
y_ax = pn.widgets.Select(name= "Y軸", options=list(df.columns), width=150)
size_ax = pn.widgets.Select(name= "サイズ", options=list(df.columns), width=150)
color_ax = pn.widgets.Select(name= "カラー", options=list(df.columns), width=150)
# Selectウィジェットの初期値指定(すべて同じにせず一つずつずらす)
for i, widget in enumerate([x_ax, y_ax, size_ax, color_ax]):
  widget.value = widget.options[min(i, len(widget.options))]

# データ未入力だとウィジェットエラー出るので仮のグラフデータ格納
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

# ホバーツールの設定
TOOLTIPS = [(e, "@{" + e + "}") for e in list(df.columns)]

# グラフ表示用関数
def display_graph(event):
    # 関数外でも使用するグラフデータ+選択データをglobal宣言
    global scatter

    # サイズを規定する列を追加
    def minmax_norm(df_input):
      return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
    df["size"] = (minmax_norm(df[size_ax.value]) + 1) * 5

    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims=[x_ax.value], vdims=[y_ax.value] + list(df.columns))  # hoverで表示するデータはすべてvdimsに追加する必要あり
    scatter = scatter.opts(color=color_ax.value, cmap="Category10", legend_position="right", 
                       width=550, height=400, fontscale=1.5, size="size", 
                       show_grid=True, title="Iris_data",
                       tools=[HoverTool(tooltips=TOOLTIPS)]
                       )

    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True

# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力", width=50, margin = [30, 10, 10, 10])
display_graph_but.on_click(display_graph)

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane)

折角なので、オンマウスしたときに詳細情報が表示される機能(ホバーツール)を追加します。

まず、bokeh.modelsの中のHoverToolをインポートします。

# ホバーツールのインポート
from bokeh.models import HoverTool

次に、オンマウスしたときに表示させたい内容を記述します。
一行に表示させたい情報をタプルにし、それを表示させたい情報分リストにまとめて渡します。(今回はリスト内包表記で記述)
@列名で各要素における列名のデータを参照することができます。
ただ、@で参照する列名に一部の記号や日本語などが使用されている場合、列名を{}で囲う必要があるので注意が必要です。

# ホバーツールの設定
TOOLTIPS = [(e, "@{" + e + "}") for e in list(df.columns)]

あとはグラフの.opts()メソッドの中でtoolsにリストでHoverTool()を指定します。
そして、そのHoverToolの引数として先ほど作成した表示させたい情報(今回の場合TOOLTIPS)を指定すればOKです。

    scatter = scatter.opts(…
        :
                       tools=[HoverTool(tooltips=TOOLTIPS)]
                       )

もし@で参照した情報の一部が????となり上手く表示されない場合、以下のような処置で解決できる可能性があります。
・vdimsに参照する列名を追加する
@で参照する列はvdimsに追加する必要があるみたいなので、list(df.columns)としてdfのすべての列をvdimsに追加してしまいます。ただ、vdimsは先頭の要素が自動的にy軸の項目になるので、先にy_ax.valueを渡した後に他の列名を追加で渡しています。

    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims=[x_ax.value], vdims=[y_ax.value] + list(df.columns)) # hoverで表示するデータはすべてvdimsに追加する必要あり

・列名で一部記号を削除
列名に()が使用されているとその列を参照したときに????と表示されてしまうみたいです。全角に置換してみたり、別の種類の括弧を使用してもダメでした。
今回は素直に括弧を削除して対応しました。

# 列名変更(列名に()が含まれるとホバーツールの表示おかしくなるため)
df.columns = [column.replace(" ", "_").replace("(", "").replace(")", "") for column in df.columns]

ここまでの結果
  ↓
14_hovertool.JPG

Selection1Dによる選択領域の取得

この時点でのコード(クリックして表示)
# ホバーツールのインポート
from bokeh.models import HoverTool

# 初期化
df = initialize()
# 列名変更(列名に()が含まれるとホバーツールの表示おかしくなるため)
df.columns = [column.replace(" ", "_").replace("(", "").replace(")", "") for column in df.columns]

# # Panelを使うためのおまじない
pn.extension()
# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# Selectウィジェットの定義
x_ax = pn.widgets.Select(name= "X軸", options=list(df.columns), width=150)
y_ax = pn.widgets.Select(name= "Y軸", options=list(df.columns), width=150)
size_ax = pn.widgets.Select(name= "サイズ", options=list(df.columns), width=150)
color_ax = pn.widgets.Select(name= "カラー", options=list(df.columns), width=150)
# Selectウィジェットの初期値指定(すべて同じにせず一つずつずらす)
for i, widget in enumerate([x_ax, y_ax, size_ax, color_ax]):
  widget.value = widget.options[min(i, len(widget.options))]

# グラフ出力パネル定義(データ未入力だとエラー出るので仮グラフデータ格納)
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

# ホバーツールの設定
TOOLTIPS = [(e, "@{" + e + "}") for e in list(df.columns)]

# グラフ表示用関数
def display_graph(event):
    # 関数外でも使用するグラフデータ+選択データをglobal宣言
    global scatter
    global selection

    # サイズを規定する列を追加
    def minmax_norm(df_input):
      return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
    df["size"] = (minmax_norm(df[size_ax.value]) + 1) * 5

    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims=[x_ax.value], vdims=[y_ax.value] + list(df.columns))  # hoverで表示するデータはすべてvdimsに追加する必要あり
    scatter = scatter.opts(color=color_ax.value, cmap="Category10", legend_position="right", 
                       width=550, height=400, fontscale=1.5, size="size", 
                       show_grid=True, title="Iris_data",
                       tools=[HoverTool(tooltips=TOOLTIPS), "box_select"],
                       active_tools=["box_select"]
                       )

    # 選択された領域のデータを取得・格納
    selection = hv.streams.Selection1D(source= scatter)

    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True

# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力", width=50, margin = [30, 10, 10, 10])
display_graph_but.on_click(display_graph)

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane)

ようやく、本投稿のタイトル回収ができる項目に来ました。
グラフ上をマウスドラッグで範囲指定し、その中に含まれるデータを参照していきます。

まず、グラフにボックス選択ツールを追加します。
先ほどHoverToolを追加した、.optsメソッドのtoolsの中にbox_selectを追加するだけでOK。
(投げ縄ツールを追加したい場合はlasso_selectを追加します)

    scatter = scatter.opts(…
        :
                       tools=[HoverTool(tooltips=TOOLTIPS), "box_select"],
                       active_tools=["box_select"]
                       )

また、active_toolsにリストでbox_selectを追加しておけば、ボックス選択ツールが初期状態から自動でONにすることが出来ます。

各種選択ツールの準備が出来たら、ツールで選択した範囲の情報にアクセスできるようにします。
方法はシンプルです。グラフ描画処理の中で、hv.streams.Selection1Dを定義し、引数sourceに参照するグラフオブジェクトを指定するだけです。

# グラフ表示用関数
def display_graph(event):
        :
    # 選択された領域のデータを取得・格納
    selection = hv.streams.Selection1D(source=scatter)
        :

これでselectionに選択範囲の情報が格納されるようになりました。

試しに適当な範囲をツールで選択し、別セルでselectionのインデックスを表示させてみます。
Selection1Dオブジェクトのインデックスを参照したい場合、.indexで参照可能です。

15_box_select.JPG
  ↓
16_preview_selection_index.JPG

選択データをリアルタイムで別パネルに出力

完成したコード(クリックして表示)
# ホバーツールのインポート
from bokeh.models import HoverTool

# 初期化
df = initialize()
# 列名変更(列名に()が含まれるとホバーツールの表示おかしくなるため)
df.columns = [column.replace(" ", "_").replace("(", "").replace(")", "") for column in df.columns]

# # Panelを使うためのおまじない
pn.extension()
# HoloViewsのバックエンド指定
hv.extension("bokeh")  # 他のセルで一度hv.extension("bokeh")を実行してしまっている場合は、以降のセルでこの一行が必要になる。

# Selectウィジェットの定義
x_ax = pn.widgets.Select(name= "X軸", options=list(df.columns), width=150)
y_ax = pn.widgets.Select(name= "Y軸", options=list(df.columns), width=150)
size_ax = pn.widgets.Select(name= "サイズ", options=list(df.columns), width=150)
color_ax = pn.widgets.Select(name= "カラー", options=list(df.columns), width=150)
# Selectウィジェットの初期値指定(すべて同じにせず一つずつずらす)
for i, widget in enumerate([x_ax, y_ax, size_ax, color_ax]):
  widget.value = widget.options[min(i, len(widget.options))]

# グラフ出力パネル定義(データ未入力だとエラー出るので仮グラフデータ格納)
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

# ホバーツールの設定
TOOLTIPS = [(e, "@{" + e + "}") for e in list(df.columns)]

# グラフ表示用関数
def display_graph(event):
    # 関数外でも使用するグラフデータ+選択データをglobal宣言
    global scatter
    global selection

    # サイズを規定する列を追加
    def minmax_norm(df_input):
      return (df_input - df_input.min()) / ( df_input.max() - df_input.min())
    df["size"] = (minmax_norm(df[size_ax.value]) + 1) * 5

    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims=[x_ax.value], vdims=[y_ax.value] + list(df.columns))  # hoverで表示するデータはすべてvdimsに追加する必要あり
    scatter = scatter.opts(color=color_ax.value, cmap="Category10", legend_position="right", 
                       width=550, height=400, fontscale=1.5, size="size", 
                       show_grid=True, title="Iris_data",
                       tools=[HoverTool(tooltips=TOOLTIPS), "box_select"],
                       active_tools=["box_select"]
                       )

    # 選択された領域のデータを取得・格納
    selection = hv.streams.Selection1D(source= scatter)

    # 選択された領域が変わるたびに実行される関数
    @pn.depends(selection.param.index, watch=True)
    def selected(selected_index):
        # データフレームの更新と表示
        selection_info.value = sdf.query("index in @selected_index").drop("size", axis=1)
        selection_info.visible = True
        
    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True

# 選択データ表示テーブルの定義
selection_info = pn.widgets.DataFrame(df, width=400, height=400, visible=False, row_height=25, autosize_mode="fit_viewport")

# グラフ表示用ボタンの定義
display_graph_but = pn.widgets.Button(name="グラフ出力", width=50, margin = [30, 10, 10, 10])
display_graph_but.on_click(display_graph)

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane, selection_info)

最後の仕上げです。
グラフ上で選択ツールによりデータが選択される度に、テーブルに詳細情報を表示・更新されるようにします。

まず、詳細情報を表示するテーブルを定義します。
テーブルにはpn.widgets.DataFrame()を使用します。第一引数にDataFrameを与えればテーブルで表示することが出来ます。

# 選択データ表示テーブルの定義
selection_info = pn.widgets.DataFrame(df, width=400, height=400, visible=False, row_height=25, autosize_mode="fit_viewport")

panelにはwidgetsとpaneにそれぞれDataFrameを表示するウィジェットが存在します。表示ならpaneのDataFrameを使用した方が良さそうなのですが、いかんせんこれが使いづらい。。。(列幅を自動調整できないなど)
なので、今回はwidgets.DataFrameを採用しました。

表示はこれまでと同様なので解説は割愛します。

# 表示
pn.Row(pn.Column(x_ax, y_ax, size_ax, color_ax, display_graph_but, margin=10), graph_pane, selection_info)

テーブルが用意できたら、selection.indexが更新されるたびに(=選択ツールで選択されるたびに)実行される関数を作成します。
何かの値の変化に反応して関数を実行させるには、@pn.depends()デコレータで関数をデコレートします。デコレータの第一引数には監視されるオブジェクト.param.監視される変数を指定します。
そしてその変化を常に監視したいのでwatch=Trueにします。

# グラフ表示用関数
def display_graph(event):
        :
    # 選択された領域が変わるたびに実行される関数
    @pn.depends(selection.param.index, watch=True)
    def selected(selected_index):
        # データフレームの更新と表示
        :

(この処理はselectionを参照するため、そのselectionが確実に定義されているグラフ描画関数の中で記述します(そうしないとエラーになる))

続いて、デコレートされる関数の中身を書いていきます。
関数宣言時の引数にはデコレータを書くときに指定した値(今回の場合は、選択されたデータのindex)を受け取る変数を指定します。
そして、dfをそのindexでフィルタし、不要な列(size列)を削除してから、先ほど作ったテーブルのvalueとして指定します。
ついでにテーブルの非表示解除も同じタイミングで行なっておきます。

    # 選択された領域が変わるたびに実行される関数
    @pn.depends(selection.param.index, watch=True)
    def selected(selected_index):
        # データフレームの更新と表示
        selection_info.value = df.query("index in @￰selected_index").drop("size", axis=1)
        selection_info.visible = True

以上で完成です!
  ↓
17_viewing_table.JPG

所感・今後

  • HoloViewsもPanelも便利なので、もっと認知度上がってほしいと思いもあって今回の記事を書きました。
  • 今回は散布図による例を作ってみましたが、ヒストグラムやヒートマップでも可能。
    (気が向けばこちらも今後記事にしてみるかも)
  • qiitaで記事を書くとき、コード内で強調表示などをしたかったので、noteを乱用してみました。少しでも見やすくなっていれば幸いです。
25
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?