web-dev-qa-db-ja.com

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

Pandasデータフレームを使用しており、既存の列の関数として新しい列を作成したい。 df.apply()np.vectorize()の速度の違いについての良い議論を見たことがないので、ここで尋ねると思いました。

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

たとえば、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つの列Aおよび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

Nを100万以上のような実世界のサイズに増やすと、np.vectorize()df.apply()よりも25倍以上速いことがわかります。

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

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()がこれ以上言及されないのですか?次のような、df.apply()に関連するStackOverflowの投稿しか見ません。

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

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

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

32

開始PandasおよびNumPy配列の能力は、高性能vectorised計算から得られると言うことで数値配列。1 ベクトル化された計算のポイントは、高度に最適化されたCコードに計算を移動し、連続するメモリブロックを利用することにより、Pythonレベルのループを回避することです。2

Pythonレベルのループ

これで、いくつかのタイミングを見ることができます。以下は、all同じ値を含むpd.Seriesnp.ndarray、またはlistオブジェクトを生成するPythonレベルのループです。データフレーム内のシリーズに割り当てるために、結果は比較可能です。

# 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)よりも効率的な要因です。
  2. np.vectorize、リスト内包表記+ Zipおよびmapメソッド、つまり上位3つはすべて、ほぼ同じパフォーマンスです。これは、Tupleを使用するため、pd.DataFrame.itertuplesからPandasオーバーヘッドをバイパスするためです。
  3. raw=Truepd.DataFrame.applyとともに使用する場合と使用しない場合とで大幅に速度が向上します。このオプションは、pd.Seriesオブジェクトの代わりにNumPy配列をカスタム関数に送ります。

pd.DataFrame.apply:もう1つのループ

正確にオブジェクト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 のドキュメントには、次の注意事項があります。

ベクトル化された関数は、numpyのブロードキャストルールを使用することを除いて、pythonマップ関数のような入力配列の連続するタプルに対してpyfuncを評価します。

入力配列の次元は同じであるため、「ブロードキャストルール」はここでは無関係です。上記のmapバージョンのパフォーマンスはほぼ同じであるため、mapの並列は有益です。 ソースコード は、何が起こっているかを示します。np.vectorizeは、入力関数を niversal function ( "ufunc")に np.frompyfunc を介して変換します。改善。

要するに、np.vectorizeはPythonレベルのループがすべきことを行いますが、pd.DataFrame.applyは大きなオーバーヘッドを追加します。 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倍高速です。これらのどちらでもかまいません。私の意見では、最初は簡潔で読みやすく、効率的です。他の方法のみを見てください。パフォーマンスが重要であり、これがボトルネックの一部である場合、以下のnumba

numba.njit:効率の向上

ループが実行可能であると見なされる場合、通常は、可能な限りCに移動するために、基になるNumPy配列で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 数値タイプには、intfloatdatetimeboolcategoryが含まれます。それらはexcludeobject dtypeであり、連続したメモリブロックに保持できます。

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

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

関数が複雑になればなるほど(つまり、numpyがそれ自体の内部に移動できる量が少なくなるほど)、パフォーマンスはそれほど変わらないことがわかります。例えば:

name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=100000))

def parse_name(name):
    if name.lower().startswith('a'):
        return 'A'
    Elif name.lower().startswith('e'):
        return 'E'
    Elif name.lower().startswith('i'):
        return 'I'
    Elif name.lower().startswith('o'):
        return 'O'
    Elif name.lower().startswith('u'):
        return 'U'
    return name

parse_name_vec = np.vectorize(parse_name)

タイミングをとる:

適用の使用

%timeit name_series.apply(parse_name)

結果:

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

np.vectorizeを使用

%timeit parse_name_vec(name_series)

結果:

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

Numpyは、np.vectorizeを呼び出すと、python関数をnumpy ufuncオブジェクトに変換しようとします。これがどのように行われるかは、実際にはわかりません。ATMを使用するよりも、numpyの内部を詳しく調べる必要があります。とはいえ、ここでは、この文字列ベースの関数よりも単純な数値関数でより良い仕事をしているようです。

1,000,000までのサイズのクランキング:

name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))

apply

%timeit name_series.apply(parse_name)

結果:

769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

np.vectorize

%timeit parse_name_vec(name_series)

結果:

794 ms ± 4.85 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

np.selectを使用したより良い(vectorized)方法:

cases = [
    name_series.str.lower().str.startswith('a'), name_series.str.lower().str.startswith('e'),
    name_series.str.lower().str.startswith('i'), name_series.str.lower().str.startswith('o'),
    name_series.str.lower().str.startswith('u')
]
replacements = 'A E I O U'.split()

タイミング:

%timeit np.select(cases, replacements, default=name_series)

結果:

67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
3
PMende