apply
、あなたが決して必要としなかった便利な機能
まず、OPの質問に1つずつ対処します。
「Ifは適用さそう悪いです、そしてなぜそれがAPIであります?」
DataFrame.apply
およびSeries.apply
は、それぞれDataFrameオブジェクトとSeriesオブジェクトで定義された便利な関数です。apply
DataFrameに変換/集約を適用するユーザー定義関数を受け入れます。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)
等々...
シリーズの変換str
:astype
対apply
これは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.apply
0.25未満のパンダバージョンでも見られます(0.25で修正されました。詳細については、こちらを参照してください)。
returns.add(1).apply(np.log)
vs.np.log(returns.add(1)
は、apply
一般的にわずかに速くなるケースです。これは、下のjppの図の右下の緑色のボックスです。