动态过滤pandas数据框

40

我正在尝试使用三个列的阈值来过滤 pandas 数据帧

import pandas as pd
df = pd.DataFrame({"A" : [6, 2, 10, -5, 3],
                   "B" : [2, 5, 3, 2, 6],
                   "C" : [-5, 2, 1, 8, 2]})
df = df.loc[(df.A > 0) & (df.B > 2) & (df.C > -1)].reset_index(drop = True)

df
    A  B  C
0   2  5  2
1  10  3  1
2   3  6  2

但我希望在一个函数内完成此操作,其中列名和它们的阈值以字典形式给出。这是我的第一次尝试,效果还不错。本质上,我将过滤器放在变量cond内并运行它:

df = pd.DataFrame({"A" : [6, 2, 10, -5, 3],
                   "B" : [2, 5, 3, 2, 6],
                   "C" : [-5, 2, 1, 8, 2]})
limits_dic = {"A" : 0, "B" : 2, "C" : -1}
cond = "df = df.loc["
for key in limits_dic.keys():
    cond += "(df." + key + " > " + str(limits_dic[key])+ ") & "
cond = cond[:-2] + "].reset_index(drop = True)"
exec(cond)
df
    A  B  C
0   2  5  2
1  10  3  1
2   3  6  2

现在,我把所有东西都放到一个函数里面,结果它就无法工作了(也许 exec 函数不喜欢在函数内使用!):

df = pd.DataFrame({"A" : [6, 2, 10, -5, 3],
                   "B" : [2, 5, 3, 2, 6],
                   "C" : [-5, 2, 1, 8, 2]})
limits_dic = {"A" : 0, "B" : 2, "C" : -1}
def filtering(df, limits_dic):
    cond = "df = df.loc["
    for key in limits_dic.keys():
        cond += "(df." + key + " > " + str(limits_dic[key])+ ") & "
    cond = cond[:-2] + "].reset_index(drop = True)"
    exec(cond)
    return(df)

df = filtering(df, limits_dic)
df
    A  B  C
0   6  2 -5
1   2  5  2
2  10  3  1
3  -5  2  8
4   3  6  2

我知道exec函数在函数内部使用时的行为与之不同,但不确定如何解决这个问题。此外,我想知道是否有更优雅的方法来定义一个函数来过滤两个输入:1)df和2)limits_dic = {"A":0, "B":2, "C":-1}。非常感谢您的想法。


如果更改结果的名称(cond =“df2 = df.loc [”return(locals()[ 'df2'])),它会起作用。 我尝试将字典添加到exec,但无济于事。 - bobrobbob
有关pd.eval()函数族的更多信息,其特性和用例,请访问使用pd.eval()在pandas中进行动态表达式评估 - cs95
4个回答

77

如果您想构建动态查询,有更简单的方法。这里提供一种使用列表推导和 str.join 的方法:

query = ' & '.join(['{}>{}'.format(k, v) for k, v in limits_dic.items()])

或者,使用Python 3.6+的f字符串:

query = ' & '.join([f'{k}>{v}' for k, v in limits_dic.items()])

:空段落标签。
print(query)

'A>0 & C>-1 & B>2'
将查询字符串传递给df.query函数,这正是其设计的目的:
out = df.query(query)
print(out)

    A  B  C
1   2  5  2
2  10  3  1
4   3  6  2

如果我的列名带有空格或其他奇怪的字符怎么办?

从pandas 0.25版本开始,您可以使用反引号将列名括起来,以使其正常工作:

query = ' & '.join([f'`{k}`>{v}' for k, v in limits_dic.items()])

查看这篇Stack Overflow文章获取更多信息。


如果你想要为查询获取一个布尔掩码,你也可以使用df.eval,然后在那之后进行索引操作就变得简单明了:

mask = df.eval(query)
print(mask)

0    False
1     True
2     True
3    False
4     True
dtype: bool

out = df[mask]
print(out)

    A  B  C
1   2  5  2
2  10  3  1
4   3  6  2

字符串数据

如果您需要查询使用字符串数据的列,则以上代码将需要进行轻微修改。

考虑以下示例(数据来自此答案):

df = pd.DataFrame({'gender':list('MMMFFF'),
                   'height':[4,5,4,5,5,4],
                   'age':[70,80,90,40,2,3]})

print (df)
  gender  height  age
0      M       4   70
1      M       5   80
2      M       4   90
3      F       5   40
4      F       5    2
5      F       4    3

以及列、运算符和数值列表:

column = ['height', 'age', 'gender']
equal = ['>', '>', '==']
condition = [1.68, 20, 'F']

适当的修改应该是:

query = ' & '.join(f'{i} {j} {repr(k)}' for i, j, k in zip(column, equal, condition))
df.query(query)

   age gender  height
3   40      F       5
有关pd.eval()函数族的信息、特性和用例,请访问Dynamic Expression Evaluation in pandas using pd.eval()

1
在 f-strings 中,您可以使用上面的简写 {k!r},而不是 {repr(k)}...这有助于像上面那样的长表达式。 - The Aelfinn
@Abhis 那应该长什么样子? - cs95
@cs95 如果我的列名本身带有一些运算符,例如 C > D,而我想要比较两个这样的列,那么我应该在每个列名周围添加双引号,并将整个查询放在单引号中吗? - MasayoMusic
@MasayoMusic 我认为你可以像 pandas 0.25 那样在列名中添加反引号,例如 \C > D``。 - cs95
1
关于使用“掩码”的部分回答了我一直以来对于如何在使用“loc”避免链式索引时将“查询”与列子集选择相结合的疑问。谢谢! - etotheipi
显示剩余2条评论

6
@coldspeed的版本有另一种替代方案:
conditions = None
for key, val in limit_dic.items():
    cond = df[key] > val
    if conditions is None:
        conditions = cond
    else:
        conditions = conditions & cond
print(df[conditions])

谢谢。我找不到一种方法来将在我的代码中定义的Python列表所引用的“isin”条件联接起来,以使得接受的答案生效。 - kev8484

3

一个替代品,可能更符合python编程风格:

import pandas as pd
import operator
from functools import reduce

df = pd.DataFrame({"A": [6, 2, 10, -5, 3],
                   "B": [2, 5, 3, 2, 6],
                   "C": [-5, 2, 1, 8, 2]})

limits_dic = {"A": 0, "B": 2, "C": -1}

# equiv to [df['A'] > 0, df['B'] > 2 ...]
loc_elements = [df[key] > val for key, val in limits_dic.items()]

df = df.loc[reduce(operator.and_, loc_elements)]

1

如何在不创建字符串和df.query的情况下完成此操作:

limits_dic = {"A" : 0, "B" : 2, "C" : -1}
cond = None

# Build the conjunction one clause at a time 
for key, val in limits_dic.items():
    if cond is None:
        cond = df[key] > val
    else:
        cond = cond & (df[key] > val)

df.loc[cond]

    A  B  C
0   2  5  2
1  10  3  1
2   3  6  2

请注意硬编码的 (>, &) 运算符(因为我想完全按照您的示例进行)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接