Pandasのパフォーマンスは、既存の列から新しい列を作成するためにnp.vectorizeに適用されます


81

Pandasデータフレームを使用していて、既存の列の関数として新しい列を作成したいと思います。私はとの速度差の良い議論を見ていないdf.apply()np.vectorize()私は、私はここに求めるだろうと思ったので、。

パンダのapply()機能は遅いです。私が測定したもの(いくつかの実験で以下に示す)から、少なくとも私の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

さらに、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

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()、なぜされnp.vectorize()、より言及されていませんか?次のdf.apply()ような、に関連するStackOverflowの投稿のみが表示されます。

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

パンダの「適用」機能を複数の列に使用するにはどうすればよいですか?

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


私はあなたの質問の詳細を掘り下げませんでしたnp.vectorizeが、基本的にはpythonforループ(これは便利なメソッドです)でapplyあり、ラムダはpython時間でもあります
roganjosh 2018年

「np.vectorize()が一般に常にdf.apply()よりも高速である場合、なぜnp.vectorize()がこれ以上言及されないのですか?」必要がないapply限り、行ごとに使用するべきではなく、明らかにベクトル化された関数は、ベクトル化されていない関数よりもパフォーマンスが優れているためです。
PMende 2018年

1
@PMendeですnp.vectorizeが、ベクトル化されていません。これはよく知られている誤称です
roganjosh 2018年

1
@PMende、確かに、私は他のことを暗示していませんでした。実装に関する意見をタイミングから導き出すべきではありません。はい、彼らは洞察力があります。しかし、彼らはあなたに真実ではないことを推測させることができます。
jpp 2018年

3
@PMendeはパンダの.strアクセサーと遊んでいます。多くの場合、リスト内包表記よりも時間がかかります。想定しすぎです。
roganjosh 2018年

回答:


115

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

Pythonレベルのループ

これで、いくつかのタイミングを見ることができます。以下にすべてのいずれかで生成するPythonレベルループpd.Seriesnp.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)。
  2. np.vectorize、リスト内包表記+zipmapメソッド、つまり上位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のブロードキャストルールを使用することを除いて、pythonmap関数のように入力配列の連続するタプルを評価します。

入力配列の次元が同じであるため、ここでは「ブロードキャストルール」は関係ありません。上記mapmapバージョンのパフォーマンスはほぼ同じであるため、との並列処理は有益です。ソースコードの何が起こっているかを示している:np.vectorizeあなたの入力機能は、に変換ユニバーサル機能を経由して(「ufunc」) np.frompyfunc。キャッシュなどの最適化がいくつかあり、パフォーマンスの向上につながる可能性があります。

要するに、np.vectorizePythonレベルのループ行うべきことを実行しますがpd.DataFrame.apply、分厚いオーバーヘッドが追加されます。一緒に見られるJITコンパイルはありません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つの数値型が含まれます:intfloatdatetimeboolcategory。それら objectdtypeを除外し、連続したメモリブロックに保持できます。

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

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

1
@jpp:parallel引数付きのデコレータを使用すると、。@njit(parallel=True)だけでなくさらに改善されます@njit。おそらくあなたもそれを追加することができます。
シェルドール2018年

1
b [i]!= 0をダブルチェックします。通常のPythonとNumbaの動作は、0をチェックしてエラーをスローすることです。これはSIMDのベクトル化を壊す可能性が高く、通常は実行速度に大きな影響を及ぼします。ただし、Numba内でこれを@njit(error_model = 'numpy')に変更して、この0による除算の二重チェックを回避できます。np.emptyを使用してメモリを割り当て、elseステートメント内で結果を0に設定することもお勧めします。
max9111 2018年

1
error_model numpyは、プロセッサが0-> NaNで除算したものを使用します。少なくともNumba0.41devでは、両方のバージョンでSIMDベクトル化が使用されています。ここで説明するようにあなたはこれをチェックすることができますnumba.pydata.org/numba-doc/dev/user/faq.html(1.16.2.3を。私のループはベクトル化されていないのはなぜ?)私は単にあなたの関数にelseステートメントを追加します(RES [ i] =0。)そしてnp.emptyでメモリを割り当てます。これをerror_model = 'numpy'と組み合わせると、パフォーマンスが約20%向上します。古いNumbaバージョンでは、パフォーマンスへの影響が大きかった...
max9111 2018年

2
@ stackoverflowuser2010、「任意の関数に対する」普遍的な答えはありません。プログラミング/アルゴリズムを理解することの一部である、適切な仕事のための適切なツールを選択する必要があります。
JPP

1
ハッピーホリデー!
cs 9519

5

関数が複雑になるほど(つまり、関数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はufunc、を呼び出すと、Python関数をnumpyオブジェクトに変換しようとしますnp.vectorize。これがどのように行われるのか、私は実際にはわかりません。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

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)

それをsize=1000000(100万)までクランクするとどうなりますか?
stackoverflowuser2010 2018年

2
ここでのあなたの主張は間違っていると私はかなり確信しています。今のところ、そのステートメントをコードでバックアップすることはできません。うまくいけば、他の誰かが
バックアップ

@ stackoverflowuser2010実際のベクトル化されたアプローチとともに、更新しました。
PMende 2018年

0

私はPythonを初めて使用します。しかし、以下の例では、「apply」は「vectorize」よりも高速に動作するようです。または、何かが足りません。

 import numpy as np
 import pandas as pd

 B = np.random.rand(1000,1000)
 fn = np.vectorize(lambda l: 1/(1-np.exp(-l)))
 print(fn(B))

 B = pd.DataFrame(np.random.rand(1000,1000))
 fn = lambda l: 1/(1-np.exp(-l))
 print(B.apply(fn))
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.