pandasのデータフレームを使用したfor文において脱iterrows()を試みたら実行時間が約70倍高速化した話

:::note info
実行時間を高速化する別の記述方法×２を追記しています。（2021/08/20）
:::

## 背景

[pandas](https://pandas.pydata.org/)のデータフレームを使用したfor文で、データフレームを行ごとに取得して…という処理を書いていました。しかし、実現したい内容に大して現実的でない時間がかかってしまうことが判明しました。いろいろ調べたり確認してみると、以下コードに時間がかかってしまっていることがわかりました。

```py:時間がかかってしまっているコード
for id, row in df.iterrows():
```

調べてみると`.iterrows()`に時間がかかるようです。とにかく脱`iterrows()`を試みて実行時間を高速化する必要がありました。

その中でリストに変換する方法（`df.values`）がありました。しかし、これは`row`の型が変わってしまい、一筋縄ではいきません。具体的には、`row['特定のカラム名']`と記述できていたものを、`row[1]`などリストのインデックスを使わなければいけなくなります（`1`は`特定のカラム名`に対応するインデックス）。後から見返した時に「この数字何！？あっリストのインデックスか！なんのデータなんだろう？」となってしまいます。

以上を踏まえて、個人的に「これがいいんじゃないか？」と思える改善案を見つけました。

## 結論

例えば、👇の場合…

```py:改善前
for id, row in df.iterrows():
    # something to do
    _ = row['特定のカラム名']
```

👇のようにするとOK👍

```py:改善後
# preparation
map_colmun_and_index = {}
for index, colmun in enumerate(df.columns, 0):
    map_colmun_and_index[colmun] = index

for row in df.values:
    # something to do
    _ = row[map_colmun_and_index['特定のカラム名']]
```

簡単にまとめると…

- 実行時間の高速化
  - `df.values`でリスト化したものでfor文を実施
- `iterrows()`を使用するメリットの一部を残す＆改善コストの削減
  - 事前にカラム名とリスト化した場合のインデックスを紐付け、マッピング
  - マッピングを使って、カラム名でリストの値の参照を実現

これにより、約70倍（※）の高速化が実現されます㊗️
（※：後述する実験の条件において算出）

念のため、差分で見たい人向けに、差分も紹介します。

```diff:改善前→改善後
+ map_colmun_and_index = {}
+ for index, colmun in enumerate(df.columns, 0):
+     map_colmun_and_index[colmun] = index
+
- for id, row in df.iterrows():
+ for row in df.values:
    # something to do
-     _ = row['特定のカラム名']
+     _ = row[map_colmun_and_index['特定のカラム名']]
```

## 実験

結論を先に紹介しましたが以下を確認するために実験した様子も紹介します。

- 本当に高速化が実現する？
- その効果は何倍？

### 実験概要

- 先に紹介した方法（改善後）と高速化を試みる前にiterrows()を使っていた方法（改善前）を比較
- 10,000回実施した実行時間の中央値から何倍の高速化が実現したかを評価

### 実験環境

- [Google Colaboratory](https://colab.research.google.com/notebooks/intro.ipynb?hl=ja)

### 実験＆結果

[scikit-learn](https://scikit-learn.org/stable/)よりiris（アヤメ）のデータを使用します。まずはデータを取得し、データフレームに変換します。

```py:scikit-learnよりirisのデータを取得・データフレームに変換
from sklearn.datasets import load_iris

iris = load_iris()
iris_df = pd.DataFrame(iris.data, columns=iris.feature_names)
```

行・列の長さは150、4でした。

```py:行・列の長さを確認
iris_df.shape
```

```py:行・列の長さを確認（結果）
(150, 4)
```

データの中身（上位5件）は以下の通りです。

```py:データの中身（上位5件）を確認
iris_df.head()
```

|sepal length (cm)|sepal width (cm)|petal length (cm)|petal width (cm)|
|----------------:|---------------:|----------------:|---------------:|
|              5.1|             3.5|              1.4|             0.2|
|              4.9|             3.0|              1.4|             0.2|
|              4.7|             3.2|              1.3|             0.2|
|              4.6|             3.1|              1.5|             0.2|
|              5.0|             3.6|              1.4|             0.2|

それでは、実験を実施します。

```py:実験
import time

results_pre_kaizen = []
results_post_kaizen = []

for _ in range(10000):
    start_time = time.time()
    for id, row in iris_df.iterrows():
        # something to do
        _ = row['sepal length (cm)']
    results_pre_kaizen.append(time.time() - start_time)
    
    start_time = time.time()
    # preparation
    map_colmun_and_index = {}
    for index, colmun in enumerate(iris_df.columns, 0):
        map_colmun_and_index[colmun] = index

    for row in iris_df.values:
        # something to do
        _ = row[dict_hoge['sepal length (cm)']]
    results_post_kaizen.append(time.time() - start_time)
```

実行結果×10,000をデータフレームに変換し、基本統計量を確認します。

```py:実行結果×10,000を基本統計量で確認
df = pd.DataFrame({"改善前":results_pre_kaizen,"改善後":results_post_kaizen}).describe()
df
```

||   改善前   |   改善後   |
|:-----------|-----------:|-----------:|
|count|10000.000000|10000.000000|
|mean|    0.008074|    0.000115|
|std|    0.000852|    0.000029|
|min|    0.007478|    0.000087|
|25%|    0.007632|    0.000105|
|50%|    0.007767|    0.000112|
|75%|    0.008166|    0.000118|
|max|    0.019278|    0.001674|

基本統計量における全ての値において、実行時間は改善前より改善後の方が高速であることがわかりました。

最後に中央値（50%）からどれだけの高速化が実現できたのかを確認します。

```py:
df["改善前"]["50%"]/df["改善後"]["50%"]
```

```
69.06794055201699
```

約70(≒69.06794055201699)倍の高速化が実現されたことがわかりました！

## おわりに

「pandasのデータフレームを使用したfor文において脱iterrows()を試みたら実行時間が約70倍高速化した話」を紹介しました。似たような悩み抱く方に響けば嬉しいです。より良い改善案や助言などがありましたら教えてください！

## 追記（2021/08/20）

実行時間を高速化する別の記述方法×２を紹介します。本記事を公開後に発見した記述方法ということもあり、上述した記述方法より、差分が少なく、理解も容易かと思われます。

### 辞書に変換する方法

辞書に変換することで、カラム名による値の指定が実現可能です。

```py:改善後（辞書に変換）
for row in df.to_dict(orient="records"):
    # something to do
     _ = row['特定のカラム名']
```

念のため、差分で見たい人向けに、差分も紹介します。

```diff:改善前→改善後（辞書に変換）
- for id, row in df.iterrows():
+ for row in df.to_dict(orient="records"):
    # something to do
     _ = row['特定のカラム名']
```

### df4loopを利用する方法

これまでの経緯から「可読性を損なわず脱`iterrows()`を試みることはできないか」と考えました。そこで、df4loopというライブラリ（OSS）を開発し、リリースしました。

https://github.com/daikikatsuragawa/df4loop

Pypiで公開しています。

https://pypi.org/project/df4loop/

df4loop自体はデータフレームに関する諸々のサポートを目的としてます。中でも`DFIterator`というクラスに脱`iterrows()`を目指した実装がなされています。

Pypiで公開されているため、以下でインストールが可能です。

```sh
pip install df4loop
```

iterrows()におけるindexが必要な場合は以下のような記述で実現可能です。

```py:改善後（df4loopを利用①）
# preparation
from df4loop import DFIterator
df_iterator = DFIterator(df)

for index, row in df_iterator.iterrows():
    # something to do
     _ = row['特定のカラム名']
```

`iterrows()`における`index`が必要な場合は以下のような記述で実現可能です。`return_indexes`の値を`False`にしています。

```py:改善後（df4loopを利用②）
# preparation
from df4loop import DFIterator
df_iterator = DFIterator(df)

for row in df_iterator.iterrows(return_indexes=False):
    # something to do
     _ = row['特定のカラム名']
```

念のため、差分で見たい人向けに、差分も紹介します。

```diff:改善前→改善後（df4loopを利用①）
+ from df4loop import DFIterator
+ df_iterator = DFIterator(df)
- for id, row in df.iterrows():
+ for index, row in df_iterator.iterrows():
    # something to do
     _ = row['特定のカラム名']
```

```diff:改善前→改善後（df4loopを利用②）
+ from df4loop import DFIterator
+ df_iterator = DFIterator(df)
- for id, row in df.iterrows():
+ for row in df_iterator.iterrows(return_indexes=False):
    # something to do
     _ = row['特定のカラム名']
```

参考までに、以下はKaggleのNotebookでdf4loopを利用した例です。

https://www.kaggle.com/daikikatsuragawa/df4loop-makes-df-iterrows-users-happy