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


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

фильтры должны быть аддитивными (ака каждый примененный должен сузить результаты).

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

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

TL; DR

Я хочу взять словарь следующей формы и применить каждую операцию к данному объекту серии и вернуть "отфильтрованный" ряд объект.

relops = {'>=': [1], '<=': [1]}

Длинный Пример

Я начну с примера того, что у меня есть в настоящее время, и просто фильтрую один объект серии. Ниже приведена функция, которую я сейчас использую:

   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

опять же, "проблема" с моим вышеуказанным подходом заключается в том, что я думаю, что существует много, возможно, ненужного копирования данных для промежуточного лестница.

кроме того, я хотел бы расширить это, чтобы переданный словарь мог включать столбцы в оператор on и фильтровать весь фрейм данных на основе входного словаря. Однако я предполагаю, что все, что работает для серии, может быть легко расширено до фрейма данных.

5 71

5 ответов:

Панды (и 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

обновление: панды 0.13 имеет метод запроса для таких случаев использования, предполагая, что имена столбцов являются допустимыми идентификаторами, следующие работы (и могут быть более эффективными для больших кадров, поскольку они используют numexpr за сцены):

In [21]: df.query('col1 <= 1 & 1 <= col1')
Out[21]:
   col1  col2
1     1    11

цепочка условий создает длинные линии, которые не поощряются pep8. С помощью .метод запроса заставляет использовать строки, которые являются мощными, но неэфирными и не очень динамичными.

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

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.логика работает и работает быстро, но не принимает более двух аргументов, которые обрабатываются functools.уменьшить.

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

самое простое из всех решений:

использование:

filtered_df = df[(df['col1'] >= 1) & (df['col1'] <= 5)]

Еще Один Пример, чтобы отфильтровать фрейм данных для значений, принадлежащих Feb-2018, используйте приведенный ниже код

filtered_df = df[(df['year'] == 2018) & (df['month'] == 2)]

С панды 0.22 обновление, параметры сравнения доступны, как:

  • 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 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 " был отфильтрован, где a >=2.

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