pythonのベンチマークツール『perfplot』『benchit』の使い方

# pythonのベンチマークツール『perfplot』『benchit』の使い方

pythonのベンチマークツール、[perfplot](https://github.com/nschloe/perfplot)と[benchit](https://github.com/droyed/benchit)の使い方のざっくり解説です。
どちらのライブラリも、主に「`pd.DataFrame`および`np.ndarray`を整形する複数の関数の実行速度の比較」のために作られました。これらのツールを使うと、「横軸に整形する対象の`pd.DataFrame`および`np.ndarray`の長さ、縦軸に各関数の実行速度」をとった折れ線グラフを簡単に作ることができます。
もちろん、`pd.DataFrame`および`np.ndarray`以外を受け取る関数に対しても使うことができます。

## 0. 例題

[pandas小技: 複数の文字列のいずれかを含む (OR + 部分一致) 検索したい - け日記](https://ohke.hateblo.jp/entry/2020/03/07/000000)より。

> 例えば↓のテーブルから 東京都または千葉県 を含む値だけをフィルタする場合に、pandasではどう書くかです。
>
> |       | name           |
> | ----: | :------------- |
> | **0** | 東京都葛飾区   |
> | **1** | 神奈川県川崎市 |
> | **2** | 埼玉県蕨市     |
> | **3** | 千葉県蘇我市   |

```python
import pandas as pd

df = pd.DataFrame({'name': ['東京都葛飾区', '神奈川県川崎市', '埼玉県蕨市', '千葉県蘇我市']})

function(df)
```

> |       | name           |
> | ----: | :------------- |
> | **0** | 東京都葛飾区   |
> | **3** | 千葉県蘇我市   |

となる関数を作る。

いろいろな方法が考えられる。

```python
def by_contains(df):
    idx = df['name'].str.contains(r'東京都|千葉県')
    return df[idx]


def by_startswith(df):
    idx = df['name'].str.startswith('東京都') | df['name'].str.startswith('千葉県')
    return df[idx]


def by_str_startswith(df):
    idx = [s.startswith('東京都') or s.startswith('千葉県')
           for s in df['name'].tolist()]
    return df[idx]


def by_np_isin(df):
    idx = np.isin(df['name'].to_numpy('<U3'), ['東京都', '千葉県'])
    return df[idx]


def by_np_or(df):
    arr = df['name'].to_numpy('<U3')
    idx = (arr == '東京都') | (arr == '千葉県')
    return df[idx]
```

さてここで、これらの関数のどれが最も速いのか？

## 1. perfplotの使い方

[GitHub - nschloe/perfplot: Performance plots for Python code](https://github.com/nschloe/perfplot)

`pip install perfplot`でインストール。

### 時間計測方法

時間計測には`perfplot.bench()`関数を用いる。測定結果として`PerfplotData`オブジェクトが返ってくる。

以下は必須パラメータ。

`setup`引数：「整数を１つ受け取って「計測したい関数に渡すインプットデータ」を作る関数」を渡す。データフレームの場合は、サンプルとして小さい`df`を作った後に以下を渡す事が多い。

- `lambda n: df.sample(n, replace=True, random_state=0).reset_index(drop=True)`
- `lambda n: pd.concat([df]*n, ignore_index=True, axis=0)`

`n_range`引数：`setup`関数にわたす`n`の数のリストを渡す。`10 ** np.arange(5)`とかでOK。

`kernels`引数：計測したい関数のリストを渡す。

`equality_check`引数：`True`/`False`を返す関数を指定して、各関数が等しい結果を返しているかどうかを確認し、異なる結果を返している場合はエラーを出す。以下のどれかでOK。

- 関数の結果が等しいかどうかを比較しない場合：`None`
- `pd.DataFrame`を返す関数の結果が等しいかどうかを比較する場合：`lambda df1, df2: df1.equals(df2)`
- `np.ndarray`を返す関数の結果が等しいかどうかを比較する場合：`np.allclose`（デフォルト）

以下はオプションのパラメータ。

`labels`引数：グラフに表示する関数名のリスト。`kernels`に渡したリストと同じ長さであること。指定しない場合は関数名がそのまま使われる。

`xlabel`引数：横軸（`n`）のラベル名。

### プロット出力方法

`PerfplotData.show()`メソッドで実行時間を比較した折れ線グラフを表示する。

また、`PerfplotData.save()`メソッドでプロットを画像として保存する。

### 実行例

```python
import numpy as np
import perfplot

ppdata = perfplot.bench(
    setup=lambda n: df.sample(n, replace=True, random_state=0).reset_index(drop=True),
    n_range=10 ** np.arange(7),
    kernels=[
        by_contains,
        by_startswith,
        by_str_startswith,
        by_np_isin,
        by_np_or
    ],
    equality_check=lambda df1, df2: df1.equals(df2),
    xlabel='len(df)'
)

ppdata.save('perfplot.png', time_unit='s')
```

> ![perfplot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394333/660db4aa-bbfe-b7ea-d48e-a6eeecc6637e.png)


`PerfplotData.save()`メソッドの`relative_to`引数に、`kernels`引数に渡したリストに対応する整数インデックスを与えると、縦軸が相対速度になる。

```python
ppdata.save('perfplot2.png', time_unit='s', relative_to=2)
```

> ![perfplot2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394333/b3492d4b-d4c0-7774-defd-520887eb5949.png)


## 2. benchitの使い方

[GitHub - droyed/benchit: Benchmarking tools for Python](https://github.com/droyed/benchit)

`pip install benchit`でインストール。

### 時間計測方法

時間計測には`benchit.timings()`関数を用いる。測定結果として`BenchmarkObj`オブジェクトが返ってくる。

以下は必須パラメータ。

`funcs`引数：計測したい関数のリストを渡す。perfplotの`kernels`引数と同じ。

`inputs`引数：計測したい関数に渡すインプットデータのリストを渡す。perfplotの`setup`と`n_range`引数に該当する。

### プロット出力方法

`BenchmarkObj.plot()`メソッドで`matplotlib.figure.Figure`オブジェクトが返されるので、受け取った`Figure`オブジェクトの`.show()`メソッドおよび`.savefig()`メソッドでプロットの出力・保存が可能。

### 実行例

```python
import numpy as np
import benchit

funcs = [
    by_contains,
    by_startswith,
    by_str_startswith,
    by_np_isin,
    by_np_or
]
inputs = [df.sample(n, replace=True, random_state=0).reset_index(drop=True)
          for n in 10 ** np.arange(7)]

bmobj = benchit.timings(funcs, inputs)

bmobj.plot(figsize=(10, 6), logx=True).savefig('benchit.png')
```

> ![benchit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394333/a48528ac-1adc-c6dd-07a2-dcda1eac78e0.png)