大容量データもサクサク動くPlotly！「plotly-resampler」で楽々高速化！

# 1. はじめに

Python のプロットツールとして、matplotlib の代わりに、下の図のようなブラウザ上でぐりぐりと操作できる [Plotly](https://plotly.com/python/) が便利で使うことが多いのですが、
![defo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/b0e40d71-3962-d157-ae5b-4f9482732b21.gif)
大容量データを一度にプロットすると、非常に重くなるのが気になっていました。

これまで描画のときだけデータを粗くするなど工夫してきたのですが、毎回大変なので、なにか便利なものはないかなーと探していたら、よいカスタムライブラリを見つけたので、この記事で紹介したいと思います！

https://github.com/predict-idlab/plotly-resampler

# 2. 通常のプロットサンプル

まず、比較のために、1000万のサンプルデータを作成して、通常の方法で Plotly でプロットしてみます。

コードはこちらです。

```python
import pandas as pd
import numpy as np
from plotly.subplots import make_subplots
import plotly.graph_objects as go

def create_data():
    # データの作成
    x = np.arange(10_000_000)
    noisy_sine = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000
    noisy_cos = (3 + np.cos(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000
    return pd.DataFrame(data={"sine": noisy_sine + 2, "cosine": noisy_cos})

def plot_data(df):
    # サブプロットの作成
    fig = make_subplots(rows=2, cols=1, shared_xaxes=True)
    # 最初のデータセットを追加
    fig.add_trace(go.Scatter(y=df["sine"], name="sine"), row=1, col=1)
    # 2番目のデータセットを追加
    fig.add_trace(go.Scatter(y=df["cosine"], name="cosine"), row=2, col=1)
    fig.update_layout(autosize=False, width=1200, height=600)
    return fig

def main():
    # データの取得
    df = create_data()
    # データのプロット
    fig = plot_data(df)
    # plotを表示する
    fig.show()
```

これを実行したのが以下の動画になります。
![normal_plot6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/bcaf11d6-2536-34a2-9673-8cdc972a59f7.gif)
プログラムを実行してから画面が表示されるまで30秒以上待つ上に、そのあとのズームやパンなどの操作も、画面が固まったり、応答が遅かったり、全体的にもっさりとした感じです。

# 3. plotly-resampler を使ったサンプル

今度は、同じデータを plotly-resampler を使用してプロットしてみます。
先にライブラリのインストールをお願いします。

```
pip install plotly-resampler
```

```python
import pandas as pd
import numpy as np
from plotly.subplots import make_subplots
import plotly.graph_objects as go
from plotly_resampler import register_plotly_resampler
from threading import Timer
import webbrowser

def create_data():
    # データの作成
    x = np.arange(10_000_000)
    noisy_sine = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000
    noisy_cos = (3 + np.cos(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000
    return pd.DataFrame(data={"sine": noisy_sine + 2, "cosine": noisy_cos})

def plot_data(df):
    # サブプロットの作成
    fig = make_subplots(rows=2, cols=1, shared_xaxes=True)
    # 最初のデータセットを追加
    fig.add_trace(go.Scatter(y=df["sine"], name="sine"), row=1, col=1)
    # 2番目のデータセットを追加
    fig.add_trace(go.Scatter(y=df["cosine"], name="cosine"), row=2, col=1)
    fig.update_layout(autosize=False, width=1200, height=600)
    return fig

def open_browser(port):
    # ブラウザを開く（省いてもOK）
    webbrowser.open_new(f"http://localhost:{port}")

def main():
    # resamplerの登録
    register_plotly_resampler(mode='auto')
    # データの取得
    df = create_data()
    # データのプロット
    fig = plot_data(df)
    # 自動的にブラウザを開く（省いてもOK）
    port = 8447
    Timer(1, lambda: open_browser(port)).start()
    # dashにplotを表示する(fig.show()の代わりに使う)
    fig.show_dash(mode='inline', port=port)

if __name__ == "__main__":
    main()
```

こちらのコードを実行した動画が下記になります。
![resample_plot7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/0ebb3065-78e7-b431-bfd3-463c287657cc.gif)
プログラムを実行してから画面が表示されるまでの時間が3秒程度と、先ほどの約1/10になり、同じように操作したときの応答も非常にサクサクになりました！

表示範囲に応じてプロット点数が調整されているようで、描画が粗くなっている部分もありますが、素早く目的のデータを見つけて拡大できるようになりました。いいですね！

# 4. 使い方の補足と注意点

詳しい使い方は、[公式リポジトリ](https://github.com/predict-idlab/plotly-resampler) に丁寧な説明があります。

* plotly-resampler の使い方の基本

    plotly の代わりに plotly-resampler を使用したい場合は、以下のコードのように冒頭で `register_plotly_resampler(mode='auto')` 関数を定義したあと、通常のプロットコードを書いて、最後に `fig.show()` の代わりに `fig.show_dash()` でプロットを出力します。

    ```python
    from plotly_resampler import register_plotly_resampler

    # resamplerの登録
    register_plotly_resampler(mode='auto')

    # 通常のplotlyのプロットコード
    (省略)

    # dashにplotを表示する
    fig.show_dash(mode='inline')
    ```

    上記例は、自動で plotly-resampler を適用するAutoモードですが、細かくカスタムするために、Manual適用する方法もリポジトリで紹介されています。

:::note info
plotly-resampler はWebフレームワークの Dash の上で実行されます。大容量データでもサクサク動くかわりに、plotlyのように静的なHTMLとしてブラウザだけで実行して見るということはできなくなります。
:::

* リポジトリの [examples](https://github.com/predict-idlab/plotly-resampler/tree/main/examples) にサンプルコードがたくさんありますが、plotly-resampler はPlotly のすべてのグラフや機能に対応しているわけではないようでした。例えば、（私がPlotlyでよく使う）[Range Slider 機能](https://plotly.com/python/range-slider/) は対応していませんでした。

*  Plotly をWebフレームワークである Streamlitから使用することが多いのですが、plotly-resampler は Streamlit には公式で対応していません。
   
   * 一応 [こちら](https://github.com/predict-idlab/plotly-resampler/issues/77)の issue に実行方法が書かれていて、実行はできたのですが、ウィジェットとプロットを連動させる方法がよくわかりませんでした。
   * 開発者は Dash の愛好家のようで、Dash の例はたくさんあるので、Webアプリから plotly-resampler を使いたいのなら ChatGPT に手伝ってもらって Dash から使う方が簡単かもしれません。

# 5. おわりに

どんな状況でも使えるというわけではないですが、サクッと試せるので、同じ悩みを持つ方はぜひ使ってみてください！