コードでパンダapply()をいつ使用したいですか?


110

Stack OverflowでPandasメソッドの使用に関する質問に投稿された多くの回答を見てきましたapply。また、「apply遅く、避けた方がいい」とのコメントをユーザーから見たことがあります。

パフォーマンスapplyが遅いという説明のある記事をたくさん読んだことがあります。また、ドキュメントでapplyUDFを渡すための単純な便利な関数についての免責事項も確認しました(現在、それを見つけることができないようです)。したがって、一般的なコンセンサスは、apply可能であれば回避する必要があるということです。ただし、これにより次の質問が生じます。

  1. applyひどい場合は、なぜAPIにあるのですか?
  2. いつ、どのようにしてコードをapplyフリーにする必要がありますか?
  3. どんな状況で、これまで存在しapplyている良い(他の可能な解決策よりも良い)は?

1
returns.add(1).apply(np.log)vs. np.log(returns.add(1)は、apply一般的にわずかに速くなるケースです。これは、下のjppの図の右下の緑色のボックスです。
Alexander

@アレクサンダーありがとう。これらの状況を徹底的に指摘しませんでしたが、知っておくと役に立ちます。
cs95

回答:


107

apply、あなたが決して必要としなかった便利な機能

まず、OPの質問に1つずつ対処します。

Ifは適用さそう悪いです、そしてなぜそれがAPIであります?

DataFrame.applyおよびSeries.applyは、それぞれDataFrameオブジェクトとSeriesオブジェクトで定義された便利な関数です。applyDataFrameに変換/集約を適用するユーザー定義関数を受け入れます。apply効果的には、既存のパンダ機能では実行できないことをすべて実行する、特効薬です。

いくつかのことapplyができます:

  • DataFrameまたはシリーズでユーザー定義関数を実行する
  • DataFrameに行単位(axis=1)または列単位()のいずれかで関数を適用するaxis=0
  • 関数を適用しながらインデックスアライメントを実行する
  • ユーザー定義関数を使用して集計を実行します(ただし、通常、aggまたはtransformこれらの場合に使用します)。
  • 要素ごとの変換を実行する
  • 集計結果を元の行にブロードキャストします(result_type引数を参照)。
  • ユーザー定義関数に渡す位置/キーワード引数を受け入れます。

...とりわけ。詳細については、ドキュメントの「行単位または列単位の関数アプリケーション」を参照してください。

では、これらすべての機能を備えているのに、なぜapply悪いのでしょうか。それはあるためapplyである 遅いです。Pandasは関数の性質について何も想定していないため、必要に応じて関数を各行/列に繰り返し適用します。さらに、上記のすべての状況を処理することは、apply各反復でいくつかの大きなオーバーヘッドが発生することを意味します。さらに、applyより多くのメモリを消費しますが、これはメモリ制限のあるアプリケーションにとっては課題です。

apply使用するのが適切な状況はほとんどありません(詳細は以下を参照)。を使用する必要があるかどうかわからない場合は、使用しapplyないでください。


次の質問に取り組みましょう。

いつ、どのようにしてコードを適用しないようにする必要がありますか?

言い換えると、への呼び出しを削除する必要がある一般的な状況を以下に示しますapply

数値データ

数値データを使用している場合は、実行しようとしていることを正確に実行するベクトル化されたcython関数がすでにある可能性があります(そうでない場合は、Stack Overflowで質問するか、GitHubで機能リクエストを開いてください)。

apply単純な加算演算のパフォーマンスを比較してください。

df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
df

   A   B
0  9  12
1  4   7
2  2   5
3  1   4

df.apply(np.sum)

A    16
B    28
dtype: int64

df.sum()

A    16
B    28
dtype: int64

パフォーマンスに関しては、比較はありません。cythonizedの同等物ははるかに高速です。おもちゃのデータでも明らかな違いがあるので、グラフは必要ありません。

%timeit df.apply(np.sum)
%timeit df.sum()
2.22 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
471 µs ± 8.16 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

raw引数を指定して生の配列を渡すことができる場合でも、速度は2倍になります。

%timeit df.apply(np.sum, raw=True)
840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

もう一つの例:

df.apply(lambda x: x.max() - x.min())

A    8
B    8
dtype: int64

df.max() - df.min()

A    8
B    8
dtype: int64

%timeit df.apply(lambda x: x.max() - x.min())
%timeit df.max() - df.min()

2.43 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.23 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

一般に、可能であれば、ベクトル化された代替案を探します。

文字列/正規表現

Pandasは、ほとんどの状況で「ベクトル化された」文字列関数を提供しますが、これらの関数が機能しない...いわゆる「適用」されるまれなケースがあります。

一般的な問題は、列の値が同じ行の別の列に存在するかどうかを確認することです。

df = pd.DataFrame({
    'Name': ['mickey', 'donald', 'minnie'],
    'Title': ['wonderland', "welcome to donald's castle", 'Minnie mouse clubhouse'],
    'Value': [20, 10, 86]})
df

     Name  Value                       Title
0  mickey     20                  wonderland
1  donald     10  welcome to donald's castle
2  minnie     86      Minnie mouse clubhouse

「donald」と「minnie」がそれぞれの「タイトル」列に存在するため、これは2番目と3番目の行を返すはずです。

適用を使用すると、これは

df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)

0    False
1     True
2     True
dtype: bool

df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]

     Name                       Title  Value
1  donald  welcome to donald's castle     10
2  minnie      Minnie mouse clubhouse     86

ただし、リスト内包表記を使用するより良い解決策があります。

df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]

     Name                       Title  Value
1  donald  welcome to donald's castle     10
2  minnie      Minnie mouse clubhouse     86

%timeit df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
%timeit df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]

2.85 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
788 µs ± 16.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

ここで注意すべきapplyことは、オーバーヘッドが少ないため、反復ルーチンがたまたまの処理よりも高速になることです。NaNおよび無効なdtypeを処理する必要がある場合は、カスタム関数を使用してこれを構築し、リスト内包内の引数を使用して呼び出すことができます。

リスト内包表記を適切なオプションと見なす必要がある場合の詳細については、私の記述を参照してください。パンダのあるループの場合-いつ気にする必要がありますか?


日付および日時の操作にも、ベクトル化されたバージョンがあります。したがって、たとえば、あなたが選ぶべきpd.to_datetime(df['date'])、と言う、オーバー、df['date'].apply(pd.to_datetime)

詳細については、ドキュメントをご覧ください 。

よくある落とし穴:リストの列を分解する

s = pd.Series([[1, 2]] * 3)
s

0    [1, 2]
1    [1, 2]
2    [1, 2]
dtype: object

人々は使いたくなりますapply(pd.Series)。これはパフォーマンスの点で恐ろしいことです。

s.apply(pd.Series)

   0  1
0  1  2
1  1  2
2  1  2

より良いオプションは、列をリスト化してpd.DataFrameに渡すことです。

pd.DataFrame(s.tolist())

   0  1
0  1  2
1  1  2
2  1  2

%timeit s.apply(pd.Series)
%timeit pd.DataFrame(s.tolist())

2.65 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
816 µs ± 40.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

最後に、

良いところ apply はありますか?

適用便利な機能なので、そこにあるのオーバーヘッドが許す無視できる十分ある状況は。これは、関数が呼び出される回数に本当に依存します。

データフレームではなくシリーズ用にベクトル化された関数
複数の列に文字列操作を適用したい場合はどうでしょうか?複数の列を日時に変換する場合はどうでしょうか?これらの関数はシリーズ専用にベクトル化されているため、変換/操作する各列に適用する必要があります。

df = pd.DataFrame(
         pd.date_range('2018-12-31','2019-01-31', freq='2D').date.astype(str).reshape(-1, 2), 
         columns=['date1', 'date2'])
df

       date1      date2
0 2018-12-31 2019-01-02
1 2019-01-04 2019-01-06
2 2019-01-08 2019-01-10
3 2019-01-12 2019-01-14
4 2019-01-16 2019-01-18
5 2019-01-20 2019-01-22
6 2019-01-24 2019-01-26
7 2019-01-28 2019-01-30

df.dtypes

date1    object
date2    object
dtype: object

これは次の場合に許容されapplyます:

df.apply(pd.to_datetime, errors='coerce').dtypes

date1    datetime64[ns]
date2    datetime64[ns]
dtype: object

に意味がstackあるか、明示的なループを使用することに注意してください。これらのオプションはすべて、を使用するよりもわずかに高速ですapplyが、違いは許されるほど小さいものです。

%timeit df.apply(pd.to_datetime, errors='coerce')
%timeit pd.to_datetime(df.stack(), errors='coerce').unstack()
%timeit pd.concat([pd.to_datetime(df[c], errors='coerce') for c in df], axis=1)
%timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')

5.49 ms ± 247 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.94 ms ± 48.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.16 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.41 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

文字列操作やカテゴリへの変換など、他の操作についても同様のケースを作成できます。

u = df.apply(lambda x: x.str.contains(...))
v = df.apply(lambda x: x.astype(category))

v / s

u = pd.concat([df[c].str.contains(...) for c in df], axis=1)
v = df.copy()
for c in df:
    v[c] = df[c].astype(category)

等々...

シリーズの変換strastypeapply

これはAPIの特異性のようです。を使用applyしてSeriesの整数を文字列に変換することは、を使用することと同等です(場合によってはより高速です)astype

ここに画像の説明を入力してくださいperfplotライブラリ を使用してグラフをプロットしました。

import perfplot

perfplot.show(
    setup=lambda n: pd.Series(np.random.randint(0, n, n)),
    kernels=[
        lambda s: s.astype(str),
        lambda s: s.apply(str)
    ],
    labels=['astype', 'apply'],
    n_range=[2**k for k in range(1, 20)],
    xlabel='N',
    logx=True,
    logy=True,
    equality_check=lambda x, y: (x == y).all())

フロートを使用すると、astypeがと常に同じか、わずかに速いことがわかりapplyます。これは、テストのデータが整数型であることと関係があります。

GroupBy 連鎖変換による操作

GroupBy.applyこれまで説明していませんがGroupBy.apply、既存のGroupBy関数では処理できないものを処理するための反復型の便利な関数でもあります。

一般的な要件の1つは、GroupByを実行してから、「遅れ累積」などの2つの主要な操作を実行することです。

df = pd.DataFrame({"A": list('aabcccddee'), "B": [12, 7, 5, 4, 5, 4, 3, 2, 1, 10]})
df

   A   B
0  a  12
1  a   7
2  b   5
3  c   4
4  c   5
5  c   4
6  d   3
7  d   2
8  e   1
9  e  10

ここでは、2つの連続したgroupby呼び出しが必要です。

df.groupby('A').B.cumsum().groupby(df.A).shift()

0     NaN
1    12.0
2     NaN
3     NaN
4     4.0
5     9.0
6     NaN
7     3.0
8     NaN
9     1.0
Name: B, dtype: float64

を使用するとapply、これを1つの呼び出しに短縮できます。

df.groupby('A').B.apply(lambda x: x.cumsum().shift())

0     NaN
1    12.0
2     NaN
3     NaN
4     4.0
5     9.0
6     NaN
7     3.0
8     NaN
9     1.0
Name: B, dtype: float64

データに依存するため、パフォーマンスを定量化することは非常に困難です。しかし、一般的にapplyは、目的がgroupby通話を減らすことである場合は許容可能なソリューションです(これgroupbyも非常に高価であるため)。


その他の警告

上記の警告以外applyに、最初の行(または列)を2回操作することにも言及する価値があります。これは、関数に副作用があるかどうかを判断するために行われます。そうでない場合apply、結果を評価するために高速パスを使用できる可能性があります。そうでない場合、低速の実装にフォールバックします。

df = pd.DataFrame({
    'A': [1, 2],
    'B': ['x', 'y']
})

def func(x):
    print(x['A'])
    return x

df.apply(func, axis=1)

# 1
# 1
# 2
   A  B
0  1  x
1  2  y

この動作は、GroupBy.apply0.25未満のパンダバージョンでも見られます(0.25で修正されました。詳細については、こちらを参照してください)。


私たちは注意する必要があると思います。 %timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')最初の反復の後は確かに変換datetimeするのではるかに速くなります... datetime
jpp

@jpp同じ懸念がありました。ただし、どちらの方法でも線形スキャンを実行する必要があります。文字列でto_datetimeを呼び出すのは、datetimeオブジェクトで呼び出すのと同じくらい高速です。野球のタイミングは同じです。別の方法としては、主要なポイントから離れた、時間制限のあるソリューションごとに、いくつかの事前コピー手順を実装します。しかし、それは正当な懸念事項です。
cs95

to_datetime文字列の呼び出しは... datetimeオブジェクトの場合と同じくらい高速です」..本当に?applyvs forループのタイミングにデータフレームの作成(固定コスト)を含めましたが、その差ははるかに小さくなっています。
jpp

@jppええと、それは私の(確かに限られた)テストから得たものです。確かにデータにもよりますが、説明の都合上、「本気で気にしないでください」というのが一般的な考え方です。
cs95

1
@ cs95、明けましておめでとうございます!
jpp

48

すべてapplyのが同じではない

以下の表は、いつ検討すべきかを示していますapply1。緑はおそらく効率的であることを意味します。赤は避けます。

ここに画像の説明を入力してください

これの一部は直感的ですpd.Series.apply。Pythonレベルの行単位のループであり、pd.DataFrame.apply行単位(axis=1)です。これらの誤用は数多くあり、多岐にわたります。もう1つの投稿では、それらについて詳しく説明しています。一般的な解決策は、ベクトル化されたメソッド、リスト内包表記(クリーンなデータを想定)、またはpd.DataFrameコンストラクターなどの効率的なツール(たとえばを回避するapply(pd.Series))を使用することです。

pd.DataFrame.apply行単位で使用している場合、raw=True(可能な場合)指定することはしばしば有益です。この段階でnumbaは、通常はが適切です。

GroupBy.apply:一般的に好まれる

繰り返す groupby回避applyするために操作をと、パフォーマンスが低下します。GroupBy.applyカスタム関数で使用するメソッド自体がベクトル化されている限り、通常はここで問題ありません。適用したいグループごとの集約にネイティブのPandasメソッドがない場合があります。この場合、applyカスタム関数を持つ少数のグループでも、妥当なパフォーマンスを提供できます。

pd.DataFrame.apply 列方向:混合バッグ

pd.DataFrame.apply列ごとの(axis=0)は興味深いケースです。少数の行と多数の列では、ほとんどの場合コストがかかります。列に比べて行数が多い場合、より一般的なケースでは、を使用してパフォーマンスが大幅に向上ことありますapply

# Python 3.7, Pandas 0.23.4
np.random.seed(0)
df = pd.DataFrame(np.random.random((10**7, 3)))     # Scenario_1, many rows
df = pd.DataFrame(np.random.random((10**4, 10**3))) # Scenario_2, many columns

                                               # Scenario_1  | Scenario_2
%timeit df.sum()                               # 800 ms      | 109 ms
%timeit df.apply(pd.Series.sum)                # 568 ms      | 325 ms

%timeit df.max() - df.min()                    # 1.63 s      | 314 ms
%timeit df.apply(lambda x: x.max() - x.min())  # 838 ms      | 473 ms

%timeit df.mean()                              # 108 ms      | 94.4 ms
%timeit df.apply(pd.Series.mean)               # 276 ms      | 233 ms

1例外はありますが、これらは通常わずかなものか珍しいものです。いくつかの例:

  1. df['col'].apply(str) わずかに上回る可能性があります df['col'].astype(str)いるます。
  2. df.apply(pd.to_datetime)文字列での作業は、通常のforループと比較して行でうまくスケーリングしません。

2
ピッチしてくれてありがとう、複数の見方を理解してください:) +1
cs95

1
@coldspeed、ありがとうございます。投稿にそれほど問題はありません(私の対するベンチマークの矛盾は別ですが、入力または設定に基づく可能性があります)。問題を見る別の方法があると感じただけです。
1

@jpp私は、あなたが優れたフローチャートを常にガイダンスとして使用しました。今日まで、行のapply方が私のソリューションよりもはるかに高速であることがわかりましたany。これについて何か考えはありますか?
Stef

1
@jpp:そうです:1mio行の場合、x 100列anyはの約100倍高速ですapply。これは、2000行x 1000列で最初のテストを実行したもので、ここでapplyは2倍の速さでしたany
Stef

1
@jppプレゼンテーション/記事であなたの画像を使用したいと思います。よろしいですか?私は明らかに出典について言及します。ありがとう
Erfan

3

以下のためにaxis=1(つまり、行ごとの機能)次に、あなただけの代わりに、以下の機能を使用することができますapply。なぜこれがpandas振る舞いではないのでしょうか。(複合インデックスでのテストは行われていませんが、それよりもはるかに高速のようですapply

def faster_df_apply(df, func):
    cols = list(df.columns)
    data, index = [], []
    for row in df.itertuples(index=True):
        row_dict = {f:v for f,v in zip(cols, row[1:])}
        data.append(func(row_dict))
        index.append(row[0])
    return pd.Series(data, index=index)

これにより、場合によってはパフォーマンスが向上することを知り、非常に驚​​きました。列の値のサブセットがそれぞれ異なる複数のことを実行する必要がある場合、これは特に役立ちました。「すべての適用は同じではない」という回答は、いつ役立つかを理解するのに役立つ場合がありますが、データのサンプルでテストすることはそれほど難しくありません。
デンソン、

いくつかの指針:パフォーマンスについては、リスト内包表記はforループよりも優れています。zip(df, row[1:])ここで十分です。本当に、この段階で、numbafuncが数値計算かどうかを検討してください。説明については、この回答を参照してください。
jpp

@jpp-より良い機能がある場合は共有してください。これは私の分析からかなり最適に近いと思います。はいnumbaはより高速です。これは(奇妙に遅い)faster_df_applyと同等であるがそれよりも高速なものが欲しい人向けですDataFrame.apply
ピートカチョッピ

2

こんな状況はありますか apply良いありますか?はい、時々。

タスク:Unicode文字列をデコードします。

import numpy as np
import pandas as pd
import unidecode

s = pd.Series(['mañana','Ceñía'])
s.head()
0    mañana
1     Ceñía


s.apply(unidecode.unidecode)
0    manana
1     Cenia

更新
はの使用をapply推奨するものでNumPyはなく、は上記の状況に対処できないため、の良い候補である可能性があると考えていましたpandas apply。しかし、@ jppによるリマインダーのおかげで、わかりやすいolリストの理解を忘れていました。


うーん、ダメ。これはどのようにより良いです[unidecode.unidecode(x) for x in s]list(map(unidecode.unidecode, s))
19

1
それはすでにパンダシリーズだったので、私は適用を使いたくなりました、そうです、そうです、適用よりもリストコンプを使用するほうがいいですが、反対投票は少し厳しいです、私は主張していませんでしたapply、これは良いかもしれないと思っただけです使用事例。
astro123
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.