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


158

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

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

В настоящее время я использую, reindex()но при этом каждый раз создается новый объект и копируются базовые данные (если я правильно понимаю документацию). Таким образом, это может быть действительно неэффективным при фильтрации больших серий или 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 на основе входного словаря. Однако я предполагаю, что все, что работает для серии, можно легко расширить до DataFrame.


Кроме того, я полностью осознаю, что такой подход к проблеме может быть далеким. Так что, возможно, было бы полезно переосмыслить весь подход. Я просто хочу разрешить пользователям указывать набор операций фильтрации во время выполнения и выполнять их.
durden2.0

Мне интересно, могут ли панды делать то же, что и data.table в R: df [col1 <1 ,,] [col2> = 1]
xappppp 04

df.queryи pd.evalкажутся подходящими для вашего варианта использования. Для получения информации о pd.eval()семействе функций, их функциях и вариантах использования, пожалуйста, посетите Dynamic Expression Evaluation в pandas с помощью pd.eval () .
cs95

Ответы:


259

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

1
Ваше право, логическое значение более эффективно, поскольку оно не копирует данные. Однако мой сценарий немного сложнее, чем ваш пример. Я получаю словарь, определяющий, какие фильтры применять. Мой пример мог бы сделать что-то вроде df[(ge(df['col1'], 1) & le(df['col1'], 1)]. Проблема для меня в том, что словарь с фильтрами может содержать множество операторов, и объединение их в цепочку является громоздким. Может быть, я мог бы добавить каждый промежуточный логический массив в большой массив, а затем просто использовать mapдля применения andк ним оператора?
durden2.0

@ durden2.0 Я добавил идею для вспомогательной функции, которая, как мне кажется, похожа на то, что вы ищете :)
Энди Хейден

Это очень похоже на то, что я придумал! Спасибо за пример. Почему f()нужно брать, *bа не просто b? Это так, что пользователь f()может по-прежнему использовать необязательный outпараметр logical_and()? Это приводит к еще одному небольшому побочному вопросу. В чем преимущество / компромисс в производительности при передаче массива через по out()сравнению с использованием массива, возвращенного из logical_and()? Еще раз спасибо!
durden2.0

Неважно, я не смотрел достаточно близко. *bНеобходимо потому , что вы передаете два массива b1и , b2и вы должны распаковать их при вызове logical_and. Однако остаётся другой вопрос. Есть ли преимущество в производительности при передаче массива через outпараметр в logical_and()vs, просто используя его возвращаемое значение?
durden2.0

2
@dwanderson вы можете передать список условий в np.logical_and.reduce для нескольких условий. Пример: np.logical_and.reduce ([df ['a'] == 3, df ['b']> 10, df ['c']. Isin (1,3,5)])
Kuzenbo

41

Сочетание условий создает длинные линии, которые не приветствуются 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.

Обратите внимание, что это все еще имеет некоторые избыточности: 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(c1,c2,c3)]

1
Есть ли способ реализовать это для переменного количества условий? Я попытался добавления каждого 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 )], не уверен , что происходит.
user5359531

Нашел решение ошибки в другом месте. data[conjunction(*conditions_list)]действительно работает после упаковки фреймов данных в список и распаковки списка на месте
user5359531

1
Я просто оставил комментарий к приведенному выше ответу с гораздо более неряшливой версией, а затем заметил ваш ответ. Очень чисто, мне очень нравится!
dwanderson

Это отличный ответ!
Charlie Crown

1
я использовал: df[f_2 & f_3 & f_4 & f_5 ]with f_2 = df["a"] >= 0и т. д. Нет необходимости в этой функции ... (хотя хорошее использование функции высшего порядка ...)
А. Рабус

23

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

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

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

Другой пример. Чтобы отфильтровать фрейм данных для значений, принадлежащих февралю 2018 г., используйте приведенный ниже код.

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

я использую переменную вместо константы. получение ошибки. df [df []] [df []] выдает предупреждающее сообщение, но дает правильный ответ.
Нгуай аль

9

Начиная с обновления pandas 0.22 , доступны следующие параметры сравнения:

  • gt (больше чем)
  • lt (меньше чем)
  • экв (равно)
  • 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

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.

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


1

e также может выбирать строки на основе значений столбца, которых нет в списке или какой-либо итерации. Мы создадим логическую переменную, как и раньше, но теперь мы инвертируем логическую переменную, поместив ~ впереди.

Например

list = [1, 0]
df[df.col1.isin(list)]

0

Если вы хотите проверить любое / все несколько столбцов на предмет значения, вы можете сделать:

df[(df[['HomeTeam', 'AwayTeam']] == 'Fulham').any(axis=1)]
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.