Ефективний спосіб застосувати декілька фільтрів до панд DataFrame або Series


148

У мене є сценарій, коли користувач хоче застосувати кілька фільтрів до об'єкта 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 на основі вхідного словника. Однак я припускаю, що всі роботи для цієї серії можуть бути легко розширені до DataFrame.


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

Мені цікаво, чи можуть панди робити подібні речі, як data.table в R: df [col1 <1 ,,] [col2> = 1]
xappppp

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

Відповіді:


245

Панди (і нуме) дозволяють здійснювати булеву індексацію , яка буде набагато ефективнішою:

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

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)])
Кузенбо

39

Умови ланцюга створюють довгі лінії, які відлякують 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) кожна з окремих умов працює на всіх початкових даних. І все-таки я вважаю, що це буде досить ефективним для багатьох застосувань, і це дуже легко читається.

Ви також можете здійснити диз'юнкцію (в якій має бути істинною лише одна з умов), використовуючи 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 )], не впевнений , що відбувається.
користувач5359531

Знайшли рішення про помилку в іншому місці. data[conjunction(*conditions_list)]працює після упаковки фреймів даних у список та розпакування списку на місці
user5359531

1
Я просто залишив коментар до вищезгаданої відповіді з набагато більш тонкою версією, а потім помітив вашу відповідь. Дуже чисто, мені це дуже подобається!
dwanderson

Це чудова відповідь!
Charlie Crown

1
Я використовував: df[f_2 & f_3 & f_4 & f_5 ]з f_2 = df["a"] >= 0і т. д. Не потрібно в цій функції ... (приємно використовувати функцію вищого порядку, хоча ...)
А. Рабус

19

Найпростіші з усіх рішень:

Використання:

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

Ще один приклад : для фільтрування фрейму даних за значеннями, що належать до лютого 2018 року, використовуйте наведений нижче код

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

Я використовую змінну замість постійної. помилка отримання. df [df []] [df []] дає попередження, але дає правильну відповідь.
Nguai al

8

Оскільки оновлення панд 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

2

Чому б цього не зробити?

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

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

Наприклад

list = [1, 0]
df[df.col1.isin(list)]
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.