Подтвердить что ты не робот

Эффективный способ применения нескольких фильтров к pandas DataFrame или Series

У меня есть сценарий, когда пользователь хочет применить несколько фильтров к объекту Pandas DataFrame или Series. По сути, я хочу эффективно объединить кучу фильтрации (операции сравнения) вместе, которые указаны во время выполнения пользователем.

Фильтры должны быть аддитивными (ака каждая применяется, чтобы сузить результаты).

В настоящее время я использую reindex(), но каждый раз создает новый объект и копирует основные данные (если я правильно понимаю документацию). Таким образом, это может быть действительно неэффективно при фильтрации большой серии или DataFrame.

Я думаю, что использование apply(), map() или что-то подобное может быть лучше. Я новичок в Pandas, хотя все еще пытаюсь оборачивать вокруг себя все.

TL; DR

Я хочу взять словарь следующей формы и применить каждую операцию к данному объекту 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.

4b9b3361

Ответ 1

Pandas (и numpy) позволяют булевое индексирование, что будет намного более эффективным:

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

In [22]: df.query("col1 <= 1 and 1 <= df['col1']")  # use df[] syntax if not a valid identifier
Out[22]:
   col1  col2
1     1    11

Ответ 2

Условия цепочек создают длинные строки, которые не рекомендуется pep8. Использование метода .query вынуждает использовать строки, которые являются мощными, но непитоническими и не очень динамичными.

Как только каждый из фильтров находится на месте, один подход -

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.

Обратите внимание, что это все еще имеет некоторые избыточности: а) сокращение не происходит на глобальном уровне. b) Каждое из отдельных условий выполняется на всех исходных данных. Тем не менее, я ожидаю, что это будет достаточно эффективно для многих приложений, и это очень читаемо.

Ответ 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.

Это немного быстрее (время ввода, а не производительность), чем цепочка операторов. Конечно, вы могли бы вставить импорт в верхнюю часть файла.