pandas:是否可以使用任意长的布尔条件过滤数据框?

8

如果您已经确定了如何过滤数据框,那么解决方案就很简单:

df[(df.A == 1) & (df.B == 1)]

但是,如果您正在接受用户输入,并且事先不知道用户想要使用多少个条件呢?例如,用户想要筛选列 [A, B, C] == 1 的数据框。是否可以像这样做:

def filterIt(*args, value):
    return df[(df.*args == value)]

如果用户调用 filterIt(A, B, C, value=1),则返回:

df[(df.A == 1) & (df.B == 1) & (df.C == 1)]

可能是重复问题:https://dev59.com/9mct5IYBdhLWcg3wuftb - chishaku
https://dev59.com/YGYr5IYBdhLWcg3whKit - chishaku
你是否总是想将不同的列与相同的值进行比较?(在这种情况下为1?) - joris
4个回答

5
我认为最优雅的方法是使用 df.query(),您可以构建一个包含所有条件的字符串,例如:
import pandas as pd
import numpy as np

cols = {}
for col in ('A', 'B', 'C', 'D', 'E'):
    cols[col] = np.random.randint(1, 5, 20)
df = pd.DataFrame(cols)

def filter_df(df, filter_cols, value):
    conditions = []
    for col in filter_cols:
        conditions.append('{c} == {v}'.format(c=col, v=value))
    query_expr = ' and '.join(conditions)
    print('querying with: {q}'.format(q=query_expr))
    return df.query(query_expr)

输出示例(由于数据随机生成,您的结果可能会有所不同):

filter_df(df, ['A', 'B'], 1)
querying with: A == 1 and B == 1
    A  B  C  D  E
6   1  1  1  2  1
11  1  1  2  3  4

5

这里有另一种方法。它更清洁,性能更好,并且具有一个优点,即columns可以为空(在这种情况下,整个数据框将被返回)。

def filter(df, value, *columns):
    return df.loc[df.loc[:, columns].eq(value).all(axis=1)]

说明

  1. values = df.loc[:, columns] 只选择我们感兴趣的列。
  2. masks = values.eq(value) 给出一个布尔数据框,指示与目标值相等。
  3. mask = masks.all(axis=1) 对列应用AND(返回索引掩码)。请注意,您可以使用masks.any(axis=1)进行OR运算。
  4. return df.loc[mask] 将索引掩码应用于数据框。

演示

import numpy as np
import pandas as pd

df = pd.DataFrame(np.random.randint(0, 2, (100, 3)), columns=list('ABC'))

# both columns
assert np.all(filter(df, 1, 'A', 'B') == df[(df.A == 1) & (df.B == 1)])

# no columns
assert np.all(filter(df, 1) == df)

# different values per column
assert np.all(filter(df, [1, 0], 'A', 'B') == df[(df.A == 1) & (df.B == 0)])

备选方案

对于少量列(< 5),以下解决方案基于 steven's answer,比上述方案更具性能,但灵活性较差。原样使用时,它将无法处理空的columns集,并且不能针对每个列使用不同的值。

from operator import and_

def filter(df, value, *columns):
    return df.loc[reduce(and_, (df[column] == value for column in columns))]

通过键检索 Series 对象(df[column])比在列子集周围构建 DataFrame 对象(df.loc[:, columns])要快得多。

In [4]: %timeit df['A'] == 1
100 loops, best of 3: 17.3 ms per loop

In [5]: %timeit df.loc[:, ['A']] == 1
10 loops, best of 3: 48.6 ms per loop

然而,当处理更多的列时,这种加速变得微不足道。瓶颈变成了将掩码进行AND运算,对于此操作,reduce(and_, ...)比Pandas内置的all(axis=1)要慢得多。


1
这很凌乱,但似乎能够工作。
import operator

def filterIt(value,args):
    stuff = [getattr(b,thing) == value for thing in args]
    return reduce(operator.and_, stuff)

a = {'A':[1,2,3],'B':[2,2,2],'C':[3,2,1]}
b = pd.DataFrame(a)
filterIt(2,['A','B','C'])

0    False
1     True
2    False
dtype: bool



(b.A == 2) & (b.B == 2) & (b.C ==2)

0    False
1     True
2    False
dtype: bool

你要找的是 return reduce(operator.and_, stuff)(而不是 for 循环)。 - Igor Raush

1

感谢大家的帮助。在了解了df.query()之后,我想到了与Marius类似的东西:

def makeQuery(cols, equivalence=True, *args):
operator = ' == ' if equivalence else ' != '
query = ''
for arg in args:
    for col in cols:
        query = query + "({}{}{})".format(col, operator, arg) + ' & '

return query[:-3]


query = makeQuery([A, B, C], False, 1, 2)

查询的内容是一个字符串:

(A != 1) & (B != 1) & (C != 1) & (A != 2) & (B != 2) & (C != 2) 

可以传递给 df.query(query) 的内容。

1
'&'.join('({}{}{})'.format(col, operator, arg) for col in cols) 可以替换你的内部循环。 - Igor Raush

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