将多个过滤器应用于 pandas DataFrame 或 Series 的有效方法
Posted
技术标签:
【中文标题】将多个过滤器应用于 pandas DataFrame 或 Series 的有效方法【英文标题】:Efficient way to apply multiple filters to pandas DataFrame or Series 【发布时间】:2012-11-16 15:47:19 【问题描述】:我有一个场景,用户想要将多个过滤器应用于 Pandas DataFrame 或 Series 对象。本质上,我想有效地将用户在运行时指定的一组过滤(比较操作)链接在一起。
过滤器应该是相加的(也就是每个应用都应该缩小结果)。
我目前正在使用reindex()
,但这每次都会创建一个新对象并复制基础数据(如果我正确理解文档的话)。因此,在过滤大型 Series 或 DataFrame 时,这可能非常低效。
我认为使用apply()
、map()
或类似的东西可能会更好。虽然我对 Pandas 还很陌生,但我仍然想把所有东西都包起来。
TL;DR
我想获取以下形式的字典并将每个操作应用于给定的 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。
【问题讨论】:
另外,我完全意识到这种解决问题的方法可能还很遥远。所以也许重新思考整个方法会很有用。我只是想让用户在运行时指定一组过滤操作并执行它们。 我想知道 pandas 是否可以做与 R 中的 data.table 类似的事情:df[col1=1]df.query
和 pd.eval
似乎很适合您的用例。有关pd.eval()
系列函数、它们的特性和用例的信息,请访问Dynamic Expression Evaluation in pandas using pd.eval()。
【参考方案1】:
Pandas(和 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 has a query method 对于这些用例,假设列名是有效的标识符,以下工作(并且对于大帧可能更有效,因为它在幕后使用 numexpr):
In [21]: df.query('col1 <= 1 & 1 <= col1')
Out[21]:
col1 col2
1 1 11
【讨论】:
你的权利,布尔值更有效,因为它不会复制数据。但是,我的情况比您的示例更棘手。我收到的输入是一个字典,定义了要应用的过滤器。我的示例可以执行类似df[(ge(df['col1'], 1) & le(df['col1'], 1)]
的操作。对我来说,真正的问题是带有过滤器的字典可能包含很多运算符,并且将它们链接在一起很麻烦。也许我可以将每个中间布尔数组添加到一个大数组中,然后使用map
将and
运算符应用于它们?
@durden2.0 我添加了一个辅助函数的想法,我认为它与您正在寻找的类似:)
这看起来非常接近我的想法!谢谢你的例子。为什么f()
需要取*b
而不仅仅是b
?这样f()
的用户是否仍然可以使用logical_and()
的可选out
参数?这就引出了另一个小问题。通过out()
传入数组与使用从logical_and()
返回的数组相比,性能优势/权衡是什么?再次感谢!
没关系,我看的不够近。 *b
是必要的,因为您要传递两个数组 b1
和 b2
,并且在调用 logical_and
时需要将它们解包。然而,另一个问题仍然存在。通过out
参数将数组传递给logical_and()
与仅使用其返回值相比,是否有性能优势?
@dwanderson 您可以将条件列表传递给 np.logical_and.reduce 以获得多个条件。示例:np.logical_and.reduce([df['a']==3, df['b']>10,df['c'].isin(1,3,5)])【参考方案2】:
链接条件会产生长行,pep8 不鼓励这样做。 使用 .query 方法强制使用字符串,这很强大但不符合 Python 标准,而且不是很动态。
一旦每个过滤器都到位,一种方法是
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 处理。
请注意,这仍然有一些冗余:a)快捷方式不会发生在全局级别 b)每个单独的条件都在整个初始数据上运行。不过,我希望这对于许多应用程序来说足够高效,而且可读性很强。
您也可以通过使用np.logical_or
来进行析取(其中只需满足一个条件):
import numpy as np
import functools
def disjunction(*conditions):
return functools.reduce(np.logical_or, conditions)
c_1 = data.col1 == True
c_2 = data.col2 < 64
c_3 = data.col3 != 4
data_filtered = data[disjunction(c_1,c_2,c_3)]
【讨论】:
有没有办法针对可变数量的条件实现这一点?我尝试将每个c_1
、c_2
、c_3
、...c_n
附加到列表中,然后传递data[conjunction(conditions_list)]
但收到错误ValueError: Item wrong length 5 instead of 37.
也尝试过data[conjunction(*conditions_list)]
,但我得到了不同的结果比data[conjunction(c_1, c_2, c_3, ... c_n )]
,不知道发生了什么。
在别处找到了错误的解决方案。 data[conjunction(*conditions_list)]
在将数据帧打包成列表后确实有效,unpacking the list in place
我刚刚在上面的答案上留下了一个更草率的版本的评论,然后注意到了你的答案。很干净,很喜欢!
这是一个很好的答案!
我用过:df[f_2 & f_3 & f_4 & f_5 ]
和 f_2 = df["a"] >= 0
等。不需要那个函数......(虽然很好地使用了高阶函数......)【参考方案3】:
为什么不这样做?
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”已被过滤,其中 a >=2。
这比运算符链接稍快(键入时间,而不是性能)。您当然可以将导入放在文件的顶部。
【讨论】:
【参考方案4】:最简单的解决方案:
用途:
filtered_df = df[(df['col1'] >= 1) & (df['col1'] <= 5)]
另一个示例,要过滤数据帧中属于 2018 年 2 月的值,请使用以下代码
filtered_df = df[(df['year'] == 2018) & (df['month'] == 2)]
【讨论】:
我使用的是变量而不是常量。得到错误。 df[df[ ]][df[ ]] 给出警告信息但给出正确答案。【参考方案5】:由于pandas 0.22 update,比较选项如下:
gt(大于) lt(小于) eq(等于) ne(不等于) ge(大于等于)还有更多。这些函数返回布尔数组。让我们看看如何使用它们:
# 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 between 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
【讨论】:
【参考方案6】:e 还可以根据不在列表或任何可迭代的列的值选择行。我们将像以前一样创建布尔变量,但现在我们将通过将 ~ 放在前面来否定布尔变量。
例如
list = [1, 0]
df[df.col1.isin(list)]
【讨论】:
【参考方案7】:如果您想检查多个列的任何/所有值,您可以这样做:
df[(df[['HomeTeam', 'AwayTeam']] == 'Fulham').any(axis=1)]
【讨论】:
以上是关于将多个过滤器应用于 pandas DataFrame 或 Series 的有效方法的主要内容,如果未能解决你的问题,请参考以下文章
pandas一些基本操作(DataFram和Series)_1
pandas一些基本操作(DataFram和Series)_3