Posted at

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

More than 1 year has passed since last update.

@simonritchieさんのpandasで複数カラムを参照して高速に1行1行値を調整する際のメモを読みました。

このような場合、私は各カラムを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つを使いますので、予めインポートしておきます。

import pandas as pd

import numpy as np


テストデータの作成

これは上述のpandasで複数カラムを参照して高速に1行1行値を調整する際のメモに従って、以下の通りに作成します。

同じデータを2つ(df1とdf2)用意しています。

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

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()




apple_price
orange_price
melon_price




0
151
115
237


1
78
90
286


2
76
74
199


3
76
99
212


4
86
81
261


パフォーマンス比較方法

「上で作成したテストデータについて500万行すべてをfor文で処理する」を1エポックとして、10エポック実行して得られた1回あたりの平均処理時間です。

具体的には %%timeit -n 10 でタイムを計測しています。


1. @shimonritchieさんの方法

pandasで複数カラムを参照して高速に1行1行値を調整する際のメモに従います。


詳細はこちらをご覧下さい。

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

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の場合のみを計測していました。

マシン構成の違いによる規模感を見るために実行します。

%timeit get_fruit_type(index=0)

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

今回は500万行についてfor文を回して計測します。

こちらの方がより実際の状況に近くなります。

%%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で処理

apple_price_array = df2.apple_price.values

orange_price_array = df2.orange_price.values
melon_price_array = df2.melon_price.values

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

%%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で処理

apple_price_list = df2.apple_price.tolist()

orange_price_list = df2.orange_price.tolist()
melon_price_list = df2.melon_price.tolist()

%%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) 程度遅くなる。

%%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もそんなに変わらないと認識していたことは誤りということが分かったのは勉強になりました。