既存の列から新しい列を作成するための Pandas の apply と np.vectorize のパフォーマンス 質問する

既存の列から新しい列を作成するための Pandas の apply と np.vectorize のパフォーマンス 質問する

私は Pandas データフレームを使用しており、既存の列の関数として新しい列を作成したいと考えています。df.apply()との速度の違いに関する適切な議論を見たことがないのでnp.vectorize()、ここで質問しようと思いました。

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

たとえば、次の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

Aさらに、2 つの列との関数として新しい列を作成したいとしますB。以下の例では、単純な関数 を使用します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

N100 万以上の現実的なサイズに増やすと、 は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()、 についてはなぜあまり言及されないのでしょうか? に関連する StackOverflow の投稿は、次のようなものnp.vectorize()しか見かけません。df.apply()

pandasは他の列の値に基づいて新しい列を作成します

Pandas の「apply」関数を複数の列に使用するにはどうすればいいですか?

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

ベストアンサー1

私はします始めるPandasとNumPy配列の威力は、高性能なベクトル化された数値配列の計算。1ベクトル化された計算の全体的なポイントは、計算を高度に最適化された C コードに移動し、連続したメモリ ブロックを利用することで、Python レベルのループを回避することです。2

Pythonレベルのループ

ここで、タイミングを見てみましょう。以下は全てpd.Series、 、np.ndarrayまたは同じ値を含むオブジェクトを生成する Python レベルのループ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. ベースの方法(最初の 4 つ) は、 ベースの方法 (最後の 3 つ)tupleよりも 1 倍効率的です。pd.Series
  2. np.vectorize、リスト内包+zipメソッドmap、つまり上位3つは、すべてほぼ同じパフォーマンスを示しています。これは、tuple そしてからいくつかのパンダのオーバーヘッドをバイパスしますpd.DataFrame.itertuples
  3. raw=True使用する場合と使用しない場合とでは、速度が大幅に向上しますpd.DataFrame.apply。このオプションは、オブジェクトではなく NumPy 配列をカスタム関数に渡しますpd.Series

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

見るその通りPandas が渡すオブジェクトに応じて、関数を簡単に修正できます。

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 のブロードキャスト ルールを使用することを除いて、Python の map 関数のように入力配列の連続するタプルを評価します。

入力配列は同じ次元を持つため、「ブロードキャストルール」はここでは無関係です。上のバージョンはほぼ同じパフォーマンスであるmapため、との類似点は参考になります。mapソースコード何が起こっているかを表示します。np.vectorize入力した関数をユニバーサル機能("ufunc")経由np.frompyfuncキャッシュなどの最適化が行われており、パフォーマンスの向上につながる可能性があります。

つまり、np.vectorizePythonレベルのループが行うことはすべき実行しますが、pd.DataFrame.apply大きなオーバーヘッドが追加されます。numba(下記参照)。ただの便利さ

真のベクトル化:すべき使用

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

%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 倍高速です。どちらも許容範囲です。私の意見では、最初のソリューションが簡潔で読みやすく、効率的です。numbaパフォーマンスが重要で、これがボトルネックの一部である場合のみ、以下の他の方法を検討してください。

numba.njit: 効率性の向上

ループの場合実行可能と見なされる場合、通常は、numba基盤となる NumPy 配列を介して最適化され、可能な限り C に移行します。

確かに、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などfloatdatetimeありますboolcategory除外する objectdtype であり、連続したメモリ ブロックに保持できます。

2 NumPy 操作が Python よりも効率的である理由は少なくとも 2 つあります。

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

おすすめ記事