1. ホーム
  2. python

[解決済み] 既存の列から新しい列を作成するためのPandas applyとnp.vectorizeの性能比較

2022-09-22 08:25:37

質問

Pandasのデータフレームを使用しており、既存のカラムの関数として新しいカラムを作成したいと考えています。との間の速度の違いについて良い議論を見たことがありません。 df.apply()np.vectorize() の2種類があり、こちらでお聞きしようと思いました。

パンダについて apply() 関数は遅いです。私が測定したところ(以下にいくつかの実験で示します)では np.vectorize() を使うのは、DataFrame 関数を使うよりも 25 倍(またはそれ以上)速いです。 apply() は、少なくとも私の 2016 MacBook Pro 上では。 これは予想された結果なのでしょうか、またその理由は?

例えば、次のようなデータフレームがあるとします。 N の行があるとします。

N = 10
A_list = np.random.randint(1, 100, N)
B_list = np.random.randint(1, 100, N)
df = pd.DataFrame({'A': A_list, 'B': B_list})
df.head()
#     A   B
# 0  78  50
# 1  23  91
# 2  55  62
# 3  82  64
# 4  99  80

さらに、2つのカラムの関数として新しいカラムを作成したいとします。 AB . 以下の例では、簡単な関数である divide() . この関数を適用するには df.apply() または np.vectorize() :

def divide(a, b):
    if b == 0:
        return 0.0
    return float(a)/b

df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)

df['result2'] = np.vectorize(divide)(df['A'], df['B'])

df.head()
#     A   B    result   result2
# 0  78  50  1.560000  1.560000
# 1  23  91  0.252747  0.252747
# 2  55  62  0.887097  0.887097
# 3  82  64  1.281250  1.281250
# 4  99  80  1.237500  1.237500

もし私が N を 100 万以上の現実的なサイズにすると、次のようになります。 np.vectorize() が 25 倍以上速いことがわかります。 df.apply() .

以下は完全なベンチマークコードです。

import pandas as pd
import numpy as np
import time

def divide(a, b):
    if b == 0:
        return 0.0
    return float(a)/b

for N in [1000, 10000, 100000, 1000000, 10000000]:    

    print ''
    A_list = np.random.randint(1, 100, N)
    B_list = np.random.randint(1, 100, N)
    df = pd.DataFrame({'A': A_list, 'B': B_list})

    start_epoch_sec = int(time.time())
    df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)
    end_epoch_sec = int(time.time())
    result_apply = end_epoch_sec - start_epoch_sec

    start_epoch_sec = int(time.time())
    df['result2'] = np.vectorize(divide)(df['A'], df['B'])
    end_epoch_sec = int(time.time())
    result_vectorize = end_epoch_sec - start_epoch_sec


    print 'N=%d, df.apply: %d sec, np.vectorize: %d sec' % \
            (N, result_apply, result_vectorize)

    # Make sure results from df.apply and np.vectorize match.
    assert(df['result'].equals(df['result2']))

結果は以下のようになります。

N=1000, df.apply: 0 sec, np.vectorize: 0 sec

N=10000, df.apply: 1 sec, np.vectorize: 0 sec

N=100000, df.apply: 2 sec, np.vectorize: 0 sec

N=1000000, df.apply: 24 sec, np.vectorize: 1 sec

N=10000000, df.apply: 262 sec, np.vectorize: 4 sec

もし np.vectorize() が一般的に常に df.apply() であるならば、なぜ np.vectorize() はもっと言及されないのでしょうか?私は、StackOverflow の投稿の中で唯一 df.apply() のようなものです。

pandasは他のカラムの値に基づいて新しいカラムを作成します。

Pandasの 'apply' 関数を複数のカラムに使用するにはどうすればよいですか?

Pandasのデータフレームの2つの列に関数を適用する方法

どのように解決するのですか?

私は 開始 は、PandasとNumPyの配列のパワーが高性能であることに由来していると言うことです。 ベクトル化された 計算から得られるものです。 1 ベクトル化された計算の全体のポイントは、高度に最適化されたCコードに計算を移動し、連続したメモリブロックを利用することによって、Pythonレベルのループを回避することです。 2

Pythonレベルのループ

ここで、いくつかのタイミングを見ることができます。以下は すべて を生成するPythonレベルのループです。 pd.Series , np.ndarray または list オブジェクトが同じ値を含んでいます。データフレーム内の系列への割り当ての目的では、結果は同等です。

# Python 3.6.5, NumPy 1.14.3, Pandas 0.23.0

np.random.seed(0)
N = 10**5

%timeit list(map(divide, df['A'], df['B']))                                   # 43.9 ms
%timeit np.vectorize(divide)(df['A'], df['B'])                                # 48.1 ms
%timeit [divide(a, b) for a, b in zip(df['A'], df['B'])]                      # 49.4 ms
%timeit [divide(a, b) for a, b in df[['A', 'B']].itertuples(index=False)]     # 112 ms
%timeit df.apply(lambda row: divide(*row), axis=1, raw=True)                  # 760 ms
%timeit df.apply(lambda row: divide(row['A'], row['B']), axis=1)              # 4.83 s
%timeit [divide(row['A'], row['B']) for _, row in df[['A', 'B']].iterrows()]  # 11.6 s

いくつかの収穫がありました。

  1. tuple -に基づくメソッド (最初の 4 つ) は pd.Series -ベースのメソッド (最後の 3 つ) よりも 1 倍効率的です。
  2. np.vectorize , リスト内包 +. zipmap メソッド、つまり上位3つのメソッドはすべてほぼ同じ性能を持っています。これは、これらのメソッドが tuple は、Pandasのオーバーヘッドを回避するために pd.DataFrame.itertuples .
  3. を使用することにより、大幅な速度向上があります。 raw=Truepd.DataFrame.apply を付けるかどうか。このオプションは、NumPyの配列をカスタム関数に送り込む際に、代わりに pd.Series オブジェクトの代わりにNumpyの配列をカスタム関数に送ります。

pd.DataFrame.apply : 単なるループ

見るために まさに を見るには、関数を修正すればよいでしょう。

def foo(row):
    print(type(row))
    assert False  # because you only need to see this once
df.apply(lambda row: foo(row), axis=1)

出力します。 <class 'pandas.core.series.Series'> . Pandasの系列オブジェクトを作成し、渡し、問い合わせることは、NumPyの配列と比較して大きなオーバーヘッドを伴います。これは驚くことではありません。Pandasの系列は、インデックス、値、属性などを保持するための適切な量の雛形を含んでいます。

もう一度、同じ演習を raw=True でもう一度同じことをしてみてください。 <class 'numpy.ndarray'> . これら全てはドキュメントに記載されていますが、実際に見た方がより説得力があります。

np.vectorize : 偽のベクトル化

のドキュメントは np.vectorize には以下のような注意書きがあります。

ベクトル化された関数が評価するのは pyfunc の連続したタプルに対して評価します。 を評価します。 ただし、numpyのブロードキャストルールを使用します。

入力配列は同じ大きさなので、quot;broadcasting rules"はここでは関係ない。との並列は map は有益です。 map のバージョンはほとんど同じ性能だからです。その ソースコード は何が起こっているかを示しています。 np.vectorize は入力関数を ユニバーサル関数 ("ufunc") に変換します。 np.frompyfunc . キャッシュなどの最適化もあり、多少のパフォーマンス向上は見込めます。

要するに np.vectorize は、Pythonレベルのループである を実行しますが pd.DataFrame.apply は膨大なオーバーヘッドを追加します。JIT コンパイルはありません。 numba で見られるような JIT コンパイルはありません (後述)。それは は単なる便宜的なもので .

真のベクトル化: あなたは べきこと を使用します。

なぜ上記のような違いがどこにも書かれていないのでしょうか。なぜなら、真にベクトル化された計算のパフォーマンスは、それらを無関係にするからです。

%timeit np.where(df['B'] == 0, 0, df['A'] / df['B'])       # 1.17 ms
%timeit (df['A'] / df['B']).replace([np.inf, -np.inf], 0)  # 1.96 ms

はい、これは上記のループ状のソリューションのうち最も速いものよりも ~40 倍も速いです。これらのどちらかは許容範囲内です。私の意見では、1つ目は簡潔で読みやすく、効率的です。他の方法だけを見て、例えば numba のように、パフォーマンスが重要で、これがボトルネックの一部である場合のみ、他のメソッドに目を向けてください。

numba.njit : より大きな効率

ループの場合 が実行可能であると考えられる場合、通常は numba を介して最適化されます。

確かに numba マイクロ秒 . 面倒な作業をしなければ、これよりはるかに効率的になることは難しいでしょう。

from numba import njit

@njit
def divide(a, b):
    res = np.empty(a.shape)
    for i in range(len(a)):
        if b[i] != 0:
            res[i] = a[i] / b[i]
        else:
            res[i] = 0
    return res

%timeit divide(df['A'].values, df['B'].values)  # 717 µs

使用方法 @njit(parallel=True) を使うことで、より大きな配列のためのさらなるブーストを提供できるかもしれません。


1 数値型は以下の通りです。 int , float , datetime , bool , category . それらは を除く object d型であり、連続したメモリブロックに保持することができます。

2 NumPyの演算がPythonに比べて効率的である理由は、少なくとも2つあります。

  • Pythonではすべてがオブジェクトです。これには、Cとは異なり、数値も含まれます。したがって、Pythonの型は、ネイティブCの型には存在しないオーバーヘッドを持ちます。
  • NumPyのメソッドは通常C言語ベースです。加えて、最適化されたアルゴリズム は可能な限り使用されます。