pandasで複数カラムをfor文で処理するときの高速化Tips

@simonritchieさんの[pandasで複数カラムを参照して高速に1行1行値を調整する際のメモ](https://qiita.com/simonritchie/items/dd737a52cf32b662675c)を読みました。
このような場合、私は各カラムをlistまたはnumpy.arrayに変換して取り出してzipで処理します。
これは何かの記事でlistに取り出してから処理するのが良いということを知ったからなのですが、自分で計測しないまま何となく従っていて、実際のところはよく分かっていませんでした。
そこで、@simonritchieさんの方法と私がよく使う方法を比較してみました。
計測した方法は次の3つです。  

1. @shimonritchieさんの方法
2. カラムをnumpy.arrayに変換してzipで処理
3. カラムをlistに変換してzipで処理

# 結論　やっぱりカラムをlistに変換して取り出してzipで並列処理が速い
`%%timeit -n 10` でパフォーマンスを計測した結果は次の通りです。

|順位|方法                                  |1回あたりの処理時間 (sec)|
|----|--------------------------------------|-------------------------|
|1   |カラムをlistに変換してzipで処理       |0.583                    |
|2   |@shimonritchieさんの方法              |1.02                     |
|3   |カラムをnumpy.arrayに変換してzipで処理|2.12                     |

カラムをlistに変換してzipで回すほうが速いですね。
驚いたのは@sihmonritchieさんの方法もなかなか速いことです。正直なところ、これほど速いとは思いませんでした。
期待外れだったのはnumpy.arrayに変換してzipで回す方法です。list版と比べて3倍強遅いとは...
やはり曖昧なままにするのは良くないですね。勉強になりました。

# 必要なパッケージ
今回はnumpyとpandasの2つを使いますので、予めインポートしておきます。

```python
import pandas as pd
import numpy as np
```

# テストデータの作成
これは上述の[pandasで複数カラムを参照して高速に1行1行値を調整する際のメモ](https://qiita.com/simonritchie/items/dd737a52cf32b662675c)に従って、以下の通りに作成します。

同じデータを2つ（df1とdf2）用意しています。
@himonritchieさんの方法がデータフレームに破壊的な変更を加えているので、同じデータを2つ用意しておきます。

```python
MAX_RECORDS = 5000000
data = {
    'apple_price': np.random.randint(low=60, high=160, size=MAX_RECORDS),
    'orange_price': np.random.randint(low=70, high=140, size=MAX_RECORDS),
    'melon_price': np.random.randint(low=120, high=340, size=MAX_RECORDS)
}
df1 = pd.DataFrame(data=data, columns=['apple_price', 'orange_price', 'melon_price'])
df2 = df1.copy()

df1.head()
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>apple_price</th>
      <th>orange_price</th>
      <th>melon_price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>151</td>
      <td>115</td>
      <td>237</td>
    </tr>
    <tr>
      <th>1</th>
      <td>78</td>
      <td>90</td>
      <td>286</td>
    </tr>
    <tr>
      <th>2</th>
      <td>76</td>
      <td>74</td>
      <td>199</td>
    </tr>
    <tr>
      <th>3</th>
      <td>76</td>
      <td>99</td>
      <td>212</td>
    </tr>
    <tr>
      <th>4</th>
      <td>86</td>
      <td>81</td>
      <td>261</td>
    </tr>
  </tbody>
</table>
</div>

# パフォーマンス比較方法
「上で作成したテストデータについて500万行すべてをfor文で処理する」を1エポックとして、10エポック実行して得られた1回あたりの平均処理時間です。
具体的には `%%timeit -n 10` でタイムを計測しています。

# 1. @shimonritchieさんの方法
[pandasで複数カラムを参照して高速に1行1行値を調整する際のメモ](https://qiita.com/simonritchie/items/dd737a52cf32b662675c)に従います。  
詳細はこちらをご覧下さい。

```python
df1.reset_index(drop=True, inplace=True)
apple_price_dict = df1.apple_price.to_dict()
orange_price_dict = df1.orange_price.to_dict()
melon_price_dict = df1.melon_price.to_dict()
df1['index_val'] = df1.index
```

```python
def get_fruit_type(index):
    """
    果物の値段に応じた種別値を取得する。

    Parameters
    ----------
    index : int
        対象の行のデータフレームのインデックス。

    Returns
    -------
    fruit_type : int
        各値段に応じて、1～4までの値が設定される。
    """

    apple_price = apple_price_dict[index]
    orange_price = orange_price_dict[index]
    melon_price = melon_price_dict[index]

    if apple_price >= 120 and orange_price >= 130:
        return 1

    if apple_price <= 130 and melon_price >= 200:
        return 2

    if orange_price <= 100 and melon_price <= 300:
        return 3

    return 4
```

なぜか元の記事ではインデックスが0の場合のみを計測していました。
マシン構成の違いによる規模感を見るために実行します。

```python
%timeit get_fruit_type(index=0)
```

    155 ns ± 0.495 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

今回は500万行についてfor文を回して計測します。
こちらの方がより実際の状況に近くなります。

```python
%%timeit -n 10

for i in range(MAX_RECORDS):
    get_fruit_type(index=i)
```

    1.02 s ± 1.78 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

# 2. カラムをnp.arrayに変換して取り出し、zipで処理

```python
apple_price_array = df2.apple_price.values
orange_price_array = df2.orange_price.values
melon_price_array = df2.melon_price.values
```

```python
def get_fruit_type(apple_price, orange_price, melon_price):
    """
    果物の値段に応じた種別値を取得する。

    Parameters
    ----------
    apple_price : int
        appleの値段
    
    orange_price : int
        orangeの値段
    
    melon_price : int
        melonの値段

    Returns
    -------
    fruit_type : int
        各値段に応じて、1～4までの値が設定される。
    """

    if apple_price >= 120 and orange_price >= 130:
        return 1

    if apple_price <= 130 and melon_price >= 200:
        return 2

    if orange_price <= 100 and melon_price <= 300:
        return 3

    return 4
```

```python
%%timeit -n 10

for apple, orange, melon in zip(apple_price_array, orange_price_array, melon_price_array):
    get_fruit_type(apple, orange, melon)
```

    2.12 s ± 3.36 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

# 3. カラムをlistに変換して取り出し、zipで処理

```python
apple_price_list = df2.apple_price.tolist()
orange_price_list = df2.orange_price.tolist()
melon_price_list = df2.melon_price.tolist()
```

```python
%%timeit -n 10

for apple, orange, melon in zip(apple_price_list, orange_price_list, melon_price_list):
    get_fruit_type(apple, orange, melon)
```

    583 ms ± 961 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

### （番外編） 引数を渡す方法で処理速度に違いは出るのか？
0.074(sec) 程度遅くなる。

```python
%%timeit -n 10

for apple, orange, melon in zip(apple_price_list, orange_price_list, melon_price_list):
    get_fruit_type(apple_price=apple, orange_price=orange, melon_price=melon)
```

    657 ms ± 1.04 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

# まとめ
pandas.DataFrameで複数カラムの処理をfor文で処理する場合の高速化の方法について比較しました。
このケースではlistとして取り出してzipとforで処理するのが定石と言われており、それを確認することができました。
また、とりあえずカラムを取り出してzipとforで回すならば、listもnumpy.arrayもそんなに変わらないと認識していたことは誤りということが分かったのは勉強になりました。