17
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

17
13
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?