Pythonでインタラクティブなグラフのデータ選択を実現する【Dockerfile付き】

スポンサーリンク

今回はPythonを使って、散布図のプロットをインタラクティブに選択して抽出する機能を紹介します。

先日、ちょうど実験データの解析で散布図のこの領域のデータを取り出したいね、という感じで必要になり、私なりに色々調べて作ってみました。本記事で扱うのはランダムに生成したデータの散布図にしておりますので、実際に自分が解析したいデータを適宜使用して使ってみてください!

本プログラムはJupyter Notebook上で動かしています。参考までに、Dockerfileを記事の最後に載せていますので、興味のある方は見てみてください。

コード全体

コード全体は以下のようになります。このコードを実行することで、Jupyter Notebook内でグラフ上のデータを選択し、選択されたデータを可視化できるようにしています。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from ipywidgets import Output
%matplotlib widget  

fig, ax = plt.subplots(figsize=(8, 6))
# ランダムデータを100個生成
x = list(np.random.random(100))
y = list(np.random.random(100))
df = pd.DataFrame({"x": x, "y": y})

scatter = ax.plot(x, y, "o", alpha=0.3, color="blue", zorder=0.1)

# グラフの設定
ax.set_xlabel("x")
ax.set_ylabel("y")
plt.tight_layout()



filtered_indexes_list = []
mask = np.ones(len(df), dtype=bool)
mask[0 : len(df)] = False

out = Output()
display(out)

# ダブルクリック 1
# 右クリック 3
@out.capture(clear_output=True)
def on_click(event):
    ind = np.searchsorted(x, event.xdata)
    # 開始点
    if event.button == 3:
        plt.title("Selected")
        
        # とりあえずクリックされたら表示範囲の情報を得る
        x_min, x_max, y_min, y_max = plt.axis()  # グラフの現在の表示範囲を取得
        width = x_max - x_min  # 横幅
        height = y_max - y_min  # 縦幅
        x_clicked = event.xdata  # クリックしたx座標
        y_clicked = event.ydata  # クリックしたy座標
        
        # 挙動がわかりやすいようにprintしておきます。不要だったら消してください。
        print(f"(width): {width}")
        print(f"(height): {height}")
        print(f"(x_min): {x_min}")
        print(f"(x_max): {x_max}")
        print(f"(y_min): {y_min}")
        print(f"(y_max): {y_max}")
        print(f" onclick(x, y): ({x_clicked}, {y_clicked})")
        
        # x,yの表示範囲にあるデータのindexを取得
        x_indexes = [index for index, value in enumerate(x) if x_min <= value <= x_max]
        y_indexes = [index for index, value in enumerate(x) if y_min <= value <= y_max]
        
        selected_range_x = [x[i] for i in x_indexes]
        selected_range_y = [y[i] for i in y_indexes]
        
        # 表示範囲にあるx,yのデータ数
        print(len(selected_range_x))
        print(len(selected_range_y))
        
        # x, y座標が表示範囲内のデータを抽出
        condition_mask = (df["x"].between(x_min, x_max, inclusive=True)) & (df["y"].between(y_min, y_max, inclusive=True))
        filtered_indexes = df[condition_mask].index
        
        # 抽出したデータを赤い点で表示
        ax.plot(df["x"].iloc[filtered_indexes], df["y"].iloc[filtered_indexes], "o", color="red", alpha=0.9)
        fig.canvas.draw()
        
        # dfのfiltered_indexesのデータを保存
        filtered_indexes_list.append(filtered_indexes)

# ダブルクリックや右クリックのイベントをハンドリング
cid = fig.canvas.mpl_connect("button_press_event", on_click)
plt.show()

実行すると、以下のような画面が出ます。

左側に表示されているウィジェットの四角のボタンをクリックするとこのグラフの一部を拡大できます。拡大後右クリックすると、以下のような感じで、拡大されたプロットが赤くなります。これで、任意の範囲のプロットの取得が可能になります。

# 表示範囲のplotのindexがfiltered_indexes_listに格納される
filtered_indexes_list

# 以下のコードでfiltered_indexes_listに格納されたindexのplotを見ることができる
# csvに保存するなりご自由に!
df.iloc[filtered_indexes_list[0]]

コードの説明

以下、少し補足します。このコードの実行にはJupyter Widgetsというものを使用しました。

out = Output() 

ipywidgets ライブラリを使用して、Jupyter Notebook内で対話的な出力をするためのウィジェット機能を作成するコードです。Jupyter Notebookのセル内で出力をキャプチャし、その出力を操作および表示できます。out = Output() で作成した out オブジェクトを使うことで対話的な操作を実行できます。さらに以下で説明する@out.capture デコレータを使用することで、ウィジェット内でセルの出力をキャプチャしてカスタマイズできるようになります。

@out.capture(clear_output=True)
def on_click(event):

このコードでは、@out.capture デコレータを on_click関数に適用し、この関数の出力が out ウィジェット内に表示されます。つまり、この形式で「クリックされたデータを取得する」などの関数の機能を表示しているグラフに適用できるみたいですね。

cid = fig.canvas.mpl_connect("button_press_event", on_click)

Matplotlibを使用して、マウスのクリックイベントをキャッチし、指定した関数on_clickを呼び出すためのコードです。mpl_connect メソッドは、図に対して行った特定のイベント(ここでは “button_press_event”、つまりマウスボタンのクリックイベント)に対して、指定した関数(on_click 関数)を関連付けるために使用しています。on_click 関数は、マウスのボタンがクリックされたときに呼び出される関数で、マウスクリックの情報(たとえば、どのボタンがクリックされたか、クリックした位置など)を処理します。

ちなみに今回は「if event.button == 3:」で右クリックを指定していますが、ダブルクリックなら「if event.button == 1:」で指定できるみたいです。試してないですが、左クリックなら「if event.button == 2:」とかですかね?

環境構築の補足

今回のコードはdockerで動かしたJupyter Notebook内で実行しました。私が行った限りAnacondaにはJupyter Widgetsが含まれていないみたいなので、別途インストールするコードをDockerfileに書きました。以下、Dockerfileです。よかったら参考にしてみてください。

Dockerfile

FROM ubuntu:latest
RUN apt-get update && apt-get install -y \
    sudo \
    wget \
    vim
WORKDIR /opt
RUN wget https://repo.anaconda.com/archive/Anaconda3-2023.07-2-Linux-x86_64.sh && \
    sh Anaconda3-2023.07-2-Linux-x86_64.sh -b -p /opt/anaconda3 && \
    rm -f Anaconda3-2023.07-2-Linux-x86_64.sh
ENV PATH /opt/anaconda3/bin:$PATH

RUN pip install --upgrade pip

# %matplotlib widgetを使うため
RUN conda install -y -c conda-forge ipywidgets && \
    conda install -c conda-forge ipympl && \
    conda install -y -c conda-forge nodejs

WORKDIR /

CMD ["jupyter", "lab", "--ip=0.0.0.0", "--allow-root", "--LabApp.token=''"]

まとめ

ipywidgets ライブラリをしようすることで、グラフをインタラクティブに操作できると、データ解析などで便利ですね!時系列データなどへの応用も可能だと思います!最後まで読んでいただきありがとうございました!

コメント

タイトルとURLをコピーしました