Логические операторы для логического индексирования в Pandas

Я работаю с логическим индексом в Pandas. Вопрос в том, почему утверждение:

a[(a['some_column']==some_number) & (a['some_other_column']==some_other_number)]

работает нормально, тогда как

a[(a['some_column']==some_number) and (a['some_other_column']==some_other_number)]

выходит с ошибкой?

Пример:

a=pd.DataFrame({'x':[1,1],'y':[10,20]})

In: a[(a['x']==1)&(a['y']==10)]
Out:    x   y
     0  1  10

In: a[(a['x']==1) and (a['y']==10)]
Out: ValueError: The truth value of an array with more than one element is ambiguous.     Use a.any() or a.all()

Ответ 1

Когда вы говорите

(a['x']==1) and (a['y']==10)

Вы неявно просите Python преобразовать (a['x']==1) и (a['y']==10) в логические значения.

Массивы NumPy (длиной более 1) и Pandas объекты, такие как Series, не имеют логического значения - другими словами, они поднимают

ValueError: The truth value of an array is ambiguous. Use a.empty, a.any() or a.all().

при использовании в качестве логического значения. Это потому, что его неясно, когда оно должно быть True или False. Некоторые пользователи могут предположить, что они True, если они имеют ненулевую длину, например, список Python. Другие могут пожелать, чтобы он был истинным, только если все его элементы True. Другие могут хотеть, чтобы он был Истинным, если любой его элементов True.

Поскольку существует так много противоречивых ожиданий, разработчики NumPy и Pandas отказываются угадывать, а вместо этого повышают ValueError.

Вместо этого вы должны быть явным, вызывая метод empty(), all() или any(), чтобы указать, какое поведение вы желаете.

В этом случае, однако, похоже, что вам не нужна логическая оценка, вы хотите логически element-wise. Это то, что выполняет двоичный оператор &:

(a['x']==1) & (a['y']==10)

возвращает логический массив.


Кстати, как alexpmil отмечает, скобки являются обязательными, поскольку & имеет более высокий приоритет оператора, чем ==. Без круглых скобок a['x']==1 & a['y']==10 будет оцениваться как a['x'] == (1 & a['y']) == 10, который, в свою очередь, будет эквивалентен сопоставленному сравнению (a['x'] == (1 & a['y'])) and ((1 & a['y']) == 10). Это выражение формы Series and Series. Использование and с двумя рядами снова приведет к тому же ValueError, что и выше. Поэтому круглые скобки являются обязательными.

Ответ 2

TL;DR; Логические операторы в Pandas являются &, |и ~, и круглые скобки (...) важны!

Python and or и not логические операторы предназначены для работы со скалярами. Поэтому Pandas пришлось сделать лучше и переопределить побитовые операторы, чтобы получить векторизованную (поэлементную) версию этой функциональности.

Итак, следующее в python (выражения exp1 и exp2 - это выражения, которые приводят к логическому результату)...

exp1 and exp2              # Logical AND
exp1 or exp2               # Logical OR
not exp1                   # Logical NOT

... будет переводить на...

exp1 & exp2                # Element-wise logical AND
exp1 | exp2                # Element-wise logical OR
~exp1                      # Element-wise logical NOT

для панд.

Если в процессе выполнения логической операции вы получаете ValueError, тогда вам нужно использовать скобки для группировки:

(exp1) op (exp2)

Например,

(df['col1'] == x) & (df['col2'] == y) 

И так далее.


Булево индексирование. Распространенной операцией является вычисление логических масок с помощью логических условий для фильтрации данных. Pandas предоставляет три оператора: & для логического И, | для логического ИЛИ и ~ для логического НЕ.

Рассмотрим следующую настройку:

np.random.seed(0)
df = pd.DataFrame(np.random.choice(10, (5, 3)), columns=list('ABC'))
df

   A  B  C
0  5  0  3
1  3  7  9
2  3  5  2
3  4  7  6
4  8  8  1

Логическое И

Для вышеприведенного df, скажем, вы хотите вернуть все строки, где A <5 и B> 5. Это делается путем вычисления масок для каждого условия в отдельности и выполнения AND для них.

Перегрузка побитовая & оператор
Прежде чем продолжить, пожалуйста, обратите внимание на эту конкретную выдержку из документов, в которой говорится

Другой распространенной операцией является использование логических векторов для фильтрации данных. Операторы: | для or, & для and, и ~ для not. Они должны быть сгруппированы с помощью круглых скобок, так как по умолчанию Python оценивает выражение, такое как df.A > 2 & df.B < 3 как df.A > (2 & df.B) < 3, в то время как желаемый порядок оценки равен (df.A > 2) & (df.B < 3).

Таким образом, с учетом этого, поэлементное логическое И может быть реализовано с помощью побитового оператора &:

df['A'] < 5

0    False
1     True
2     True
3     True
4    False
Name: A, dtype: bool

df['B'] > 5

0    False
1     True
2    False
3     True
4     True
Name: B, dtype: bool

(df['A'] < 5) & (df['B'] > 5)

0    False
1     True
2    False
3     True
4    False
dtype: bool

И последующий этап фильтрации просто,

df[(df['A'] < 5) & (df['B'] > 5)]

   A  B  C
1  3  7  9
3  4  7  6

Скобки используются для переопределения порядка приоритета по умолчанию для побитовых операторов, которые имеют более высокий приоритет над условными операторами < и >. Смотрите раздел Приоритет оператора в документации по Python.

Если вы не используете скобки, выражение оценивается неправильно. Например, если вы случайно попытались сделать что-то

df['A'] < 5 & df['B'] > 5

Разбирается как

df['A'] < (5 & df['B']) > 5

Который становится,

df['A'] < something_you_dont_want > 5

Который становится (см. Документы по питону по сравнению цепочечных операторов),

(df['A'] < something_you_dont_want) and (something_you_dont_want > 5)

Который становится,

# Both operands are Series...
something_else_you_dont_want1 and something_else_you_dont_want2

Какие броски

ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().

Так что, не делайте эту ошибку! 1

Избегание группировки скобок
Исправление на самом деле довольно просто. Большинство операторов имеют соответствующий связанный метод для DataFrames. Если отдельные маски создаются с использованием функций вместо условных операторов, вам больше не нужно группировать по пареням, чтобы указать порядок оценки:

df['A'].lt(5)

0     True
1     True
2     True
3     True
4    False
Name: A, dtype: bool

df['B'].gt(5)

0    False
1     True
2    False
3     True
4     True
Name: B, dtype: bool

df['A'].lt(5) & df['B'].gt(5)

0    False
1     True
2    False
3     True
4    False
dtype: bool

Смотрите раздел Гибкие сравнения. , Подводя итог, мы имеем

╒════╤════════════╤════════════╕
│    │ Operator   │ Function   │
╞════╪════════════╪════════════╡
│  0 │ >          │ gt         │
├────┼────────────┼────────────┤
│  1 │ >=         │ ge         │
├────┼────────────┼────────────┤
│  2 │ <          │ lt         │
├────┼────────────┼────────────┤
│  3 │ <=         │ le         │
├────┼────────────┼────────────┤
│  4 │ ==         │ eq         │
├────┼────────────┼────────────┤
│  5 │ !=         │ ne         │
╘════╧════════════╧════════════╛

Другой способ избежать скобок - использовать DataFrame.query (или eval):

df.query('A < 5 and B > 5')

   A  B  C
1  3  7  9
3  4  7  6

Я хорошо документирован query и eval в динамической оценке экспрессии в панде с помощью pd.eval().

operator.and_
Позволяет выполнять эту операцию функциональным образом. Внутренне вызывает Series.__and__ что соответствует побитовому оператору.

import operator 

operator.and_(df['A'] < 5, df['B'] > 5)
# Same as,
# (df['A'] < 5).__and__(df['B'] > 5) 

0    False
1     True
2    False
3     True
4    False
dtype: bool

df[operator.and_(df['A'] < 5, df['B'] > 5)]

   A  B  C
1  3  7  9
3  4  7  6

Обычно это вам не понадобится, но это полезно знать.

Обобщение: np.logical_andlogical_and.reduce)
Другой альтернативой является использование np.logical_and, которое также не требует группировки скобок:

np.logical_and(df['A'] < 5, df['B'] > 5)

0    False
1     True
2    False
3     True
4    False
Name: A, dtype: bool

df[np.logical_and(df['A'] < 5, df['B'] > 5)]

   A  B  C
1  3  7  9
3  4  7  6

np.logical_and является ufunc (универсальные функции), и большинство ufuncs есть reduce метод. Это означает, что легче обобщать с помощью logical_and если у вас есть несколько масок для AND. Например, для масок AND m1 и m2 и m3 с & вам придется сделать

m1 & m2 & m3

Тем не менее, более простой вариант

np.logical_and.reduce([m1, m2, m3])

Это мощно, потому что позволяет вам строить поверх этого более сложную логику (например, динамически генерировать маски в понимании списка и добавлять их все):

import operator

cols = ['A', 'B']
ops = [np.less, np.greater]
values = [5, 5]

m = np.logical_and.reduce([op(df[c], v) for op, c, v in zip(ops, cols, values)])
m 
# array([False,  True, False,  True, False])

df[m]
   A  B  C
1  3  7  9
3  4  7  6

1 - Я знаю, что я об этом говорю, но, пожалуйста, потерпите меня.Это очень, очень распространенная ошибка новичка, и должна быть объяснена очень тщательно.


Логическое ИЛИ

Для приведенного выше df, скажем, вы хотите вернуть все строки, где A == 3 или B == 7.

Перегружен побитовой |

df['A'] == 3

0    False
1     True
2     True
3    False
4    False
Name: A, dtype: bool

df['B'] == 7

0    False
1     True
2    False
3     True
4    False
Name: B, dtype: bool

(df['A'] == 3) | (df['B'] == 7)

0    False
1     True
2     True
3     True
4    False
dtype: bool

df[(df['A'] == 3) | (df['B'] == 7)]

   A  B  C
1  3  7  9
2  3  5  2
3  4  7  6

Если вы еще этого не сделали, ознакомьтесь также с разделом " Логическое И" выше, здесь действуют все предостережения.

В качестве альтернативы, эта операция может быть указана с

df[df['A'].eq(3) | df['B'].eq(7)]

   A  B  C
1  3  7  9
2  3  5  2
3  4  7  6

operator.or_
Series.__or__ звонков Series.__or__ под капотом.

operator.or_(df['A'] == 3, df['B'] == 7)
# Same as,
# (df['A'] == 3).__or__(df['B'] == 7)

0    False
1     True
2     True
3     True
4    False
dtype: bool

df[operator.or_(df['A'] == 3, df['B'] == 7)]

   A  B  C
1  3  7  9
2  3  5  2
3  4  7  6

np.logical_or
Для двух условий используйте logical_or:

np.logical_or(df['A'] == 3, df['B'] == 7)

0    False
1     True
2     True
3     True
4    False
Name: A, dtype: bool

df[np.logical_or(df['A'] == 3, df['B'] == 7)]

   A  B  C
1  3  7  9
2  3  5  2
3  4  7  6

Для нескольких масок используйте logical_or.reduce:

np.logical_or.reduce([df['A'] == 3, df['B'] == 7])
# array([False,  True,  True,  True, False])

df[np.logical_or.reduce([df['A'] == 3, df['B'] == 7])]

   A  B  C
1  3  7  9
2  3  5  2
3  4  7  6

Логическое НЕ

Учитывая маску, такую как

mask = pd.Series([True, True, False])

Если вам нужно инвертировать каждое логическое значение (чтобы конечный результат был [False, False, True]), то вы можете использовать любой из методов ниже.

Побитовый ~

~mask

0    False
1    False
2     True
dtype: bool

Опять же, выражения должны быть заключены в скобки.

~(df['A'] == 3)

0     True
1    False
2    False
3     True
4     True
Name: A, dtype: bool

Это внутренне называет

mask.__invert__()

0    False
1    False
2     True
dtype: bool

Но не используйте его напрямую.

operator.inv
Внутренне звонки __invert__ в серии.

operator.inv(mask)

0    False
1    False
2     True
dtype: bool

np.logical_not
Это вариант NumPy.

np.logical_not(mask)

0    False
1    False
2     True
dtype: bool

Обратите внимание, что np.logical_and можно заменить на np.bitwise_and, logical_or с помощью bitwise_or и logical_not с помощью invert.

Ответ 3

Логические операторы для логического индексирования в Pandas

Важно понимать, что вы не можете использовать любые логические операторы Python (and or или not) в pandas.Series или pandas.DataFrame (аналогично, вы не можете использовать их в numpy.array с более чем одним элементом). Причина, по которой вы не можете использовать их, заключается в том, что они неявно вызывают bool для своих операндов, который выдает исключение, потому что эти структуры данных решили, что логическое значение массива неоднозначно:

>>> import numpy as np
>>> import pandas as pd
>>> arr = np.array([1,2,3])
>>> s = pd.Series([1,2,3])
>>> df = pd.DataFrame([1,2,3])
>>> bool(arr)
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
>>> bool(s)
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
>>> bool(df)
ValueError: The truth value of a DataFrame is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().

Я рассказал об этом более подробно в своем ответе на "Истинное значение Серии неоднозначно. Используйте a.empty, a.bool(), a.item(), a.any() или a.all()" Q + А

Логические функции NumPys

Однако NumPy предоставляет поэлементные рабочие эквиваленты этим операторам в качестве функций, которые можно использовать в numpy.array, pandas.Series, pandas.DataFrame или любом другом (соответствующем) подклассе numpy.array:

Таким образом, по сути, следует использовать (предполагая, что df1 и df2 - панды DataFrames):

np.logical_and(df1, df2)
np.logical_or(df1, df2)
np.logical_not(df1)
np.logical_xor(df1, df2)

Побитовые функции и побитовые операторы для логических значений

Однако, если у вас есть логический массив NumPy, pandas Series или pandas DataFrames, вы также можете использовать поэлементные побитовые функции (для логических значений они - или, по крайней мере, должны - неотличимы от логических функций):

  • поразрядно и: np.bitwise_and или оператор &
  • побитовый или: np.bitwise_or или | оператор
  • поразрядно: np.invert (или псевдоним np.bitwise_not) или оператор ~
  • побитовый xor: np.bitwise_xor или оператор ^

Обычно используются операторы. Однако в сочетании с операторами сравнения нужно помнить, что сравнение следует заключать в круглые скобки, поскольку побитовые операторы имеют более высокий приоритет, чем операторы сравнения:

(df1 < 10) | (df2 > 10)  # instead of the wrong df1 < 10 | df2 > 10

Это может раздражать, потому что логические операторы Python имеют меньший приоритет, чем операторы сравнения, поэтому вы обычно пишете a < 10 and b > 10 (где a и b, например, простые целые числа) и не нуждаетесь в скобках.

Различия между логическими и побитовыми операциями (для не булевых)

Очень важно подчеркнуть, что битовые и логические операции эквивалентны только для логических массивов NumPy (и логических серий и фреймов данных). Если они не содержат логических значений, то операции будут давать разные результаты. Я приведу примеры использования массивов NumPy, но результаты будут аналогичны для структур данных pandas:

>>> import numpy as np
>>> a1 = np.array([0, 0, 1, 1])
>>> a2 = np.array([0, 1, 0, 1])

>>> np.logical_and(a1, a2)
array([False, False, False,  True])
>>> np.bitwise_and(a1, a2)
array([0, 0, 0, 1], dtype=int32)

А поскольку NumPy (и аналогично pandas) выполняет разные функции для булевых (булевых или "маскирующих" индексных массивов) и целочисленных (индексных массивов) индексов, результаты индексации также будут разными:

>>> a3 = np.array([1, 2, 3, 4])

>>> a3[np.logical_and(a1, a2)]
array([4])
>>> a3[np.bitwise_and(a1, a2)]
array([1, 1, 1, 2])

Таблица результатов

Logical operator | NumPy logical function | NumPy bitwise function | Bitwise operator
-------------------------------------------------------------------------------------
       and       |  np.logical_and        | np.bitwise_and         |        &
-------------------------------------------------------------------------------------
       or        |  np.logical_or         | np.bitwise_or          |        |
-------------------------------------------------------------------------------------
                 |  np.logical_xor        | np.bitwise_xor         |        ^
-------------------------------------------------------------------------------------
       not       |  np.logical_not        | np.invert              |        ~

Где логический оператор не работает для массивов NumPy, панд Series и панд DataFrames. Другие работают над этими структурами данных (и обычными объектами Python) и работают поэлементно. Однако будьте осторожны с побитовым инвертированием на обычном Python bool потому что в этом контексте bool будет интерпретироваться как целые числа (например, ~False возвращает -1 и ~True возвращает -2).