Pandas DataFrameで、別の列の値に基づいて条件付きで新しい列を作成したい。私のアプリケーションでは、DataFrameには通常、数百万行と一意の条件値の数があります。は小さく、1のオーダーです。パフォーマンスは非常に重要です:新しい列を生成する最速の方法は何ですか?
以下にサンプルケースを作成し、すでにさまざまな方法を試して比較しました。この例では、条件付き入力は、列label
(ここでは_1, 2, 3
_の1つ)の値に基づくディクショナリルックアップによって表されます。
_lookup_dict = {
1: 100, # arbitrary
2: 200, # arbitrary
3: 300, # arbitrary
}
_
次に、DataFrameが次のように入力されることを期待します。
_ label output
0 3 300
1 2 200
2 3 300
3 3 300
4 2 200
5 2 200
6 1 100
7 1 100
_
以下は、1,000万行でテストされた6つの異なるメソッドです(テストコードのパラメーターNlines
)。
pandas.groupby().apply()
pandas.groupby().indices.items()
pandas.Series.map
_numpy.select
_完全なコードは、すべてのメソッドのランタイムとともに、回答の最後にあります。すべてのメソッドの出力は、パフォーマンスが比較される前に等しいとアサートされます。
pandas.groupby().apply()
label
でpandas.groupby()
を使用し、次にapply()
を使用して各ブロックに同じ値を入力します。
_def fill_output(r):
''' called by groupby().apply(): all r.label values are the same '''
r.loc[:, 'output'] = lookup_dict[r.iloc[0]['label']]
return r
df = df.groupby('label').apply(fill_output)
_
私は得る
_>>> method_1_groupby ran in 2.29s (average over 3 iterations)
_
Groupby()。apply()が最初のグループで2回実行され、使用するコードパスが決定されることに注意してください( パンダ#2936 を参照)。これにより、少数のグループの処理が遅くなる可能性があります。方法1で最初のダミーグループを追加できるように騙しましたが、あまり改善されませんでした。
pandas.groupby().indices.items()
2つ目はバリアントです。apply
を使用する代わりに、groupby().indices.items()
を使用してインデックスに直接アクセスします。これは最終的に方法1の2倍の速度になり、これは私が長い間使用してきた方法です。
_dgb = df.groupby('label')
for label, idx in dgb.indices.items():
df.loc[idx, 'output'] = lookup_dict[label]
_
入手した:
_method_2_indices ran in 1.21s (average over 3 iterations)
_
pandas.Series.map
_Pandas.Series.map を使用しました。
_df['output'] = df.label.map(lookup_dict.get)
_
ルックアップされた値の数が行の数と同等である同様のケースで、非常に良い結果が得られました。この場合、map
は方法1の2倍遅くなります。
method_3_mapは3.07秒で実行されました(3回の反復の平均)
ルックアップ値の数が少ないためだと思いますが、実装方法に問題がある可能性があります。
4番目の方法は非常に単純です。すべてのラベルをループして、DataFrameの一致する部分を選択するだけです。
_for label, value in lookup_dict.items():
df.loc[df.label == label, 'output'] = value
_
しかし、驚いたことに、以前の場合よりもはるかに高速な結果が得られました。 Pandasはここで_df.label == label
_と3回比較する必要があるため、groupby
ベースのソリューションはこれよりも高速であると期待していました。結果は私が間違っていることを証明しています。
_method_4_forloop ran in 0.54s (average over 3 iterations)
_
numpy.select
_5番目の方法は、これに基づいてnumpy select
関数を使用します StackOverflow answer 。
_conditions = [df.label == k for k in lookup_dict.keys()]
choices = list(lookup_dict.values())
df['output'] = np.select(conditions, choices)
_
これにより、最良の結果が得られます。
_method_5_select ran in 0.29s (average over 3 iterations)
_
最終的に、私は方法6でnumba
アプローチを試しました。
例のために、条件付き入力値はコンパイルされた関数のハードコードです。 Numbaにランタイム定数としてリストを与える方法がわかりません:
_@jit(int64[:](int64[:]), nopython=True)
def hardcoded_conditional_filling(column):
output = np.zeros_like(column)
i = 0
for c in column:
if c == 1:
output[i] = 100
Elif c == 2:
output[i] = 200
Elif c == 3:
output[i] = 300
i += 1
return output
df['output'] = hardcoded_conditional_filling(df.label.values)
_
方法5よりも50%速く、最高の時間になりました。
_method_6_numba ran in 0.19s (average over 3 iterations)
_
上記の理由により、これを実装していません。パフォーマンスを大幅に低下させることなく、実行時定数としてNumbaにリストを与える方法がわかりません。
_import pandas as pd
import numpy as np
from timeit import timeit
from numba import jit, int64
lookup_dict = {
1: 100, # arbitrary
2: 200, # arbitrary
3: 300, # arbitrary
}
Nlines = int(1e7)
# Generate
label = np.round(np.random.Rand(Nlines)*2+1).astype(np.int64)
df0 = pd.DataFrame(label, columns=['label'])
# Now the goal is to assign the look_up_dict values to a new column 'output'
# based on the value of label
# Method 1
# using groupby().apply()
def method_1_groupby(df):
def fill_output(r):
''' called by groupby().apply(): all r.label values are the same '''
#print(r.iloc[0]['label']) # activate to reveal the #2936 issue in Pandas
r.loc[:, 'output'] = lookup_dict[r.iloc[0]['label']]
return r
df = df.groupby('label').apply(fill_output)
return df
def method_2_indices(df):
dgb = df.groupby('label')
for label, idx in dgb.indices.items():
df.loc[idx, 'output'] = lookup_dict[label]
return df
def method_3_map(df):
df['output'] = df.label.map(lookup_dict.get)
return df
def method_4_forloop(df):
''' naive '''
for label, value in lookup_dict.items():
df.loc[df.label == label, 'output'] = value
return df
def method_5_select(df):
''' Based on answer from
https://stackoverflow.com/a/19913845/5622825
'''
conditions = [df.label == k for k in lookup_dict.keys()]
choices = list(lookup_dict.values())
df['output'] = np.select(conditions, choices)
return df
def method_6_numba(df):
''' This works, but it is hardcoded and i don't really know how
to make it compile with list as runtime constants'''
@jit(int64[:](int64[:]), nopython=True)
def hardcoded_conditional_filling(column):
output = np.zeros_like(column)
i = 0
for c in column:
if c == 1:
output[i] = 100
Elif c == 2:
output[i] = 200
Elif c == 3:
output[i] = 300
i += 1
return output
df['output'] = hardcoded_conditional_filling(df.label.values)
return df
df1 = method_1_groupby(df0)
df2 = method_2_indices(df0.copy())
df3 = method_3_map(df0.copy())
df4 = method_4_forloop(df0.copy())
df5 = method_5_select(df0.copy())
df6 = method_6_numba(df0.copy())
# make sure we havent modified the input (would bias the results)
assert 'output' not in df0.columns
# Test validity
assert (df1 == df2).all().all()
assert (df1 == df3).all().all()
assert (df1 == df4).all().all()
assert (df1 == df5).all().all()
assert (df1 == df6).all().all()
# Compare performances
Nites = 3
print('Compare performances for {0:.1g} lines'.format(Nlines))
print('-'*30)
for method in [
'method_1_groupby', 'method_2_indices',
'method_3_map', 'method_4_forloop',
'method_5_select', 'method_6_numba']:
print('{0} ran in {1:.2f}s (average over {2} iterations)'.format(
method,
timeit("{0}(df)".format(method), setup="from __main__ import df0, {0}; df=df0.copy()".format(method), number=Nites)/Nites,
Nites))
_
出力:
_Compare performances for 1e+07 lines
------------------------------
method_1_groupby ran in 2.29s (average over 3 iterations)
method_2_indices ran in 1.21s (average over 3 iterations)
method_3_map ran in 3.07s (average over 3 iterations)
method_4_forloop ran in 0.54s (average over 3 iterations)
method_5_select ran in 0.29s (average over 3 iterations)
method_6_numba ran in 0.19s (average over 3 iterations)
_
より良いパフォーマンスを生み出すことができる他のソリューションに興味があります。私はもともとPandasベースのメソッドを探していましたが、numba/cythonベースのソリューションも受け入れます。
追加 Chrisbのメソッド 比較のために:
_def method_3b_mapdirect(df):
''' Suggested by https://stackoverflow.com/a/51388828/5622825'''
df['output'] = df.label.map(lookup_dict)
return df
def method_7_take(df):
''' Based on answer from
https://stackoverflow.com/a/19913845/5622825
Exploiting that labels are continuous integers
'''
lookup_arr = np.array(list(lookup_dict.values()))
df['output'] = lookup_arr.take(df['label'] - 1)
return df
_
ランタイム:
_method_3_mapdirect ran in 0.23s (average over 3 iterations)
method_7_take ran in 0.11s (average over 3 iterations)
_
これにより、#3は他のどの方法よりも速く(#6は別として)、最もエレガントでもあります。ユーザーケースに互換性がある場合は、#7を使用してください。
.map
(#3)は、これを行うための慣用的な方法だと思いますが、.get
を渡さないでください。辞書を単独で使用すると、かなり大幅な改善が見られるはずです。
df = pd.DataFrame({'label': np.random.randint(, 4, size=1000000, dtype='i8')})
%timeit df['output'] = df.label.map(lookup_dict.get)
261 ms ± 12.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit df['output'] = df.label.map(lookup_dict)
69.6 ms ± 3.08 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
条件の数が少なく、比較が安価な場合(つまり、intとルックアップテーブル)、値(4、特に5)の直接比較は.map
よりも高速ですが、これは常に当てはまるとは限りません。文字列のセットがある場合。
ルックアップラベルが実際に連続した整数である場合は、これを利用して、take
を使用してルックアップできます。これはnumbaとほぼ同じ速度である必要があります。これは基本的に可能な限り高速だと思います-同等のものをcythonで書くことはできますが、速くはなりません。
%%timeit
lookup_arr = np.array(list(lookup_dict.values()))
df['output'] = lookup_arr.take(df['label'] - 1)
8.68 ms ± 332 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)