ユーザーがPandas DataFrameまたはSeriesオブジェクトにいくつかのフィルターを適用したいシナリオがあります。基本的に、ユーザーが実行時に指定する一連のフィルタリング(比較操作)を効率的に連鎖させたいと思います。
フィルターは付加的である必要があります(別名、適用される各フィルターは結果を絞り込む必要があります)。
現在reindex()
を使用していますが、これは毎回新しいオブジェクトを作成し、基礎となるデータをコピーします(ドキュメントを正しく理解している場合)。そのため、大きなSeriesまたはDataFrameをフィルタリングする場合、これは非常に効率が悪い場合があります。
apply()
、map()
、または同様のものを使用する方が良いと考えています。私はPandasが初めてなので、まだすべてを頭で包もうとしています。
次の形式の辞書を取得し、各操作を特定のSeriesオブジェクトに適用し、「フィルター処理された」Seriesオブジェクトを返します。
relops = {'>=': [1], '<=': [1]}
私が現在持っているものの例から始め、単一のSeriesオブジェクトをフィルタリングします。以下は、私が現在使用している機能です。
def apply_relops(series, relops):
"""
Pass dictionary of relational operators to perform on given series object
"""
for op, vals in relops.iteritems():
op_func = ops[op]
for val in vals:
filtered = op_func(series, val)
series = series.reindex(series[filtered])
return series
ユーザーは、実行する操作を含む辞書を提供します。
>>> df = pandas.DataFrame({'col1': [0, 1, 2], 'col2': [10, 11, 12]})
>>> print df
>>> print df
col1 col2
0 0 10
1 1 11
2 2 12
>>> from operator import le, ge
>>> ops ={'>=': ge, '<=': le}
>>> apply_relops(df['col1'], {'>=': [1]})
col1
1 1
2 2
Name: col1
>>> apply_relops(df['col1'], relops = {'>=': [1], '<=': [1]})
col1
1 1
Name: col1
繰り返しになりますが、上記のアプローチの「問題」は、中間ステップのデータの不必要なコピーが多数ある可能性があることです。
また、これを拡張して、渡された辞書に演算子への列を含め、入力辞書に基づいてDataFrame全体をフィルター処理できるようにします。ただし、Seriesで機能するものはすべてDataFrameに簡単に拡張できると想定しています。
パンダ(およびnumpy)では boolean indexing を使用でき、これははるかに効率的です。
In [11]: df.loc[df['col1'] >= 1, 'col1']
Out[11]:
1 1
2 2
Name: col1
In [12]: df[df['col1'] >= 1]
Out[12]:
col1 col2
1 1 11
2 2 12
In [13]: df[(df['col1'] >= 1) & (df['col1'] <=1 )]
Out[13]:
col1 col2
1 1 11
このためのヘルパー関数を作成する場合は、次の行に沿って何かを検討してください。
In [14]: def b(x, col, op, n):
return op(x[col],n)
In [15]: def f(x, *b):
return x[(np.logical_and(*b))]
In [16]: b1 = b(df, 'col1', ge, 1)
In [17]: b2 = b(df, 'col1', le, 1)
In [18]: f(df, b1, b2)
Out[18]:
col1 col2
1 1 11
更新: pandas 0.13にはクエリメソッドがあります これらの種類のユースケースでは、列名が有効な識別子であると仮定すると、次のように機能します( numexpr =舞台裏):
In [21]: df.query('col1 <= 1 & 1 <= col1')
Out[21]:
col1 col2
1 1 11
連鎖条件は長い行を作成しますが、pep8では推奨されません。 .queryメソッドを使用すると、文字列を使用するように強制されます。これは強力ですが、Pythonではなく、あまり動的ではありません。
各フィルターを配置したら、1つのアプローチは
import numpy as np
import functools
def conjunction(*conditions):
return functools.reduce(np.logical_and, conditions)
c_1 = data.col1 == True
c_2 = data.col2 < 64
c_3 = data.col3 != 4
data_filtered = data[conjunction(c1,c2,c3)]
np.logicalは、高速で動作しますが、functools.reduceによって処理される3つ以上の引数を取りません。
これにはまだいくつかの冗長性があることに注意してください:a)ショートカットはグローバルレベルでは発生しませんb)個々の条件のそれぞれは、初期データ全体で実行されます。それでも、これは多くのアプリケーションにとって十分に効率的であり、非常に読みやすいと期待しています。
最も簡単なソリューション:
つかいます:
filtered_df = df[(df['col1'] >= 1) & (df['col1'] <= 5)]
別の例、2018年2月に属する値のデータフレームをフィルタリングするには、以下のコードを使用します
filtered_df = df[(df['year'] == 2018) & (df['month'] == 2)]
pandas 0.22 update であるため、比較オプションは次のように使用できます。
などなど。これらの関数はブール配列を返します。それらの使用方法を見てみましょう。
# sample data
df = pd.DataFrame({'col1': [0, 1, 2,3,4,5], 'col2': [10, 11, 12,13,14,15]})
# get values from col1 greater than or equals to 1
df.loc[df['col1'].ge(1),'col1']
1 1
2 2
3 3
4 4
5 5
# where co11 values is better 0 and 2
df.loc[df['col1'].between(0,2)]
col1 col2
0 0 10
1 1 11
2 2 12
# where col1 > 1
df.loc[df['col1'].gt(1)]
col1 col2
2 2 12
3 3 13
4 4 14
5 5 15
なぜこれをしないのですか?
def filt_spec(df, col, val, op):
import operator
ops = {'eq': operator.eq, 'neq': operator.ne, 'gt': operator.gt, 'ge': operator.ge, 'lt': operator.lt, 'le': operator.le}
return df[ops[op](df[col], val)]
pandas.DataFrame.filt_spec = filt_spec
デモ:
df = pd.DataFrame({'a': [1,2,3,4,5], 'b':[5,4,3,2,1]})
df.filt_spec('a', 2, 'ge')
結果:
a b
1 2 4
2 3 3
3 4 2
4 5 1
列> aがフィルタリングされていることがわかります> = 2。
これは、演算子チェーンよりもわずかに高速です(パフォーマンスではなくタイピング時間)。もちろん、インポートをファイルの先頭に置くこともできます。
eは、リストまたは反復可能な列にない列の値に基づいて行を選択することもできます。前と同じようにブール変数を作成しますが、今度は〜を先頭に配置してブール変数を無効にします。
例えば
list = [1, 0]
df[df.col1.isin(list)]