Operadores lógicos para indexação booleana no Pandas

153

Estou trabalhando com índice booleano no Pandas. A questão é por que a afirmação:

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

funciona bem enquanto

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

sai com erro?

Exemplo:

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()
user2988577
fonte
6
Isso ocorre porque matrizes numpy e séries de pandas usam os operadores bit a bit em vez de lógicos, pois você está comparando cada elemento da matriz / série com outro. Portanto, não faz sentido usar o operador lógico nessa situação. veja relacionado: stackoverflow.com/questions/8632033/…
EdChum
9
Em Python and != &. O andoperador no Python não pode ser substituído, enquanto o &operador ( __and__) pode. Daí a escolha do uso &em numpy e pandas.
Steven Rumbalski

Respostas:

209

Quando voce diz

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

Você está pedindo implicitamente ao Python que converta (a['x']==1)e (a['y']==10)valores booleanos.

Matrizes NumPy (de comprimento maior que 1) e objetos Pandas como Series não possuem um valor booleano - em outras palavras, eles aumentam

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

quando usado como um valor booleano. Isso porque não está claro quando deve ser Verdadeiro ou Falso . Alguns usuários podem assumir que são True se tiverem um comprimento diferente de zero, como uma lista Python. Outros podem desejar que isso seja verdadeiro apenas se todos os seus elementos forem verdadeiros. Outros podem querer que seja True se algum de seus elementos for True.

Como existem muitas expectativas conflitantes, os designers do NumPy e Pandas se recusam a adivinhar e, em vez disso, aumentam um ValueError.

Em vez disso, você deve ser explícito, chamando o empty(), all()ou any()método para indicar qual comportamento você deseja.

Nesse caso, no entanto, parece que você não deseja avaliação booleana, deseja lógica e elementos em termos de elementos . É isso que o &operador binário executa:

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

retorna uma matriz booleana.


A propósito, como observa alexpmil , os parênteses são obrigatórios, pois &têm uma precedência de operador mais alta que ==. Sem os parênteses, a['x']==1 & a['y']==10seria avaliado como o a['x'] == (1 & a['y']) == 10que por sua vez seria equivalente à comparação encadeada (a['x'] == (1 & a['y'])) and ((1 & a['y']) == 10). Essa é uma expressão do formulário Series and Series. O uso de andcom duas séries ativaria novamente o mesmo ValueErrorque acima. É por isso que os parênteses são obrigatórios.

unutbu
fonte
3
matrizes numpy possuem essa propriedade se tiverem o comprimento um. Apenas os desenvolvedores de pandas (teimosamente) se recusam a adivinhar: p
Andy Hayden
4
'&' Não possui a mesma curva ambígua que 'e'? Como é que, quando se trata de '&', de repente todos os usuários concordam que deve ser em termos de elementos, enquanto quando vêem 'e', ​​suas expectativas variam?
Indominus
16
@ Indominus: A própria linguagem Python requer que a expressão x and ydesencadeie a avaliação de bool(x)e bool(y). Python "primeiro avalia x; se xfor falso, seu valor é retornado; caso contrário, yé avaliado e o valor resultante é retornado." Portanto, a sintaxe x and ynão pode ser usada para lógica lógica de elemento e desde apenas xou ypode ser retornada. Por outro lado, x & ygatilhos x.__and__(y)e o __and__método podem ser definidos para retornar o que quisermos.
unutbu
2
Importante observar: os parênteses em torno da ==cláusula são obrigatórios . a['x']==1 & a['y']==10retorna o mesmo erro da pergunta.
Alex P. Miller
1
Para que serve "|"?
Euler_Salter
62

TLDR; Operadores lógicos em Pandas são &, |e ~, e parênteses (...)é importante!

Python é and, ore notoperadores lógicos são projetados para trabalhar com escalares. Portanto, o Pandas teve que fazer um melhor e substituir os operadores bit a bit para obter a versão vetorizada (elemento a elemento) dessa funcionalidade.

Portanto, o seguinte em python ( exp1e exp2são expressões que avaliam um resultado booleano) ...

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

... será traduzido para ...

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

para pandas.

Se no processo de execução da operação lógica você obtiver a ValueError, precisará usar parênteses para agrupar:

(exp1) op (exp2)

Por exemplo,

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

E assim por diante.


Indexação booleana : Uma operação comum é calcular máscaras booleanas através de condições lógicas para filtrar os dados. Pandas fornece três operadores:&para lógicas E,|para OR lógico, e~para lógicas não.

Considere a seguinte configuração:

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

AND lógico

Por dfacima, digamos que você gostaria de retornar todas as linhas onde a <5 e B> 5. Isso é feito por máscaras para cada condição de computação separadamente, e Anding eles.

&Operador Bitwise sobrecarregado
Antes de continuar, observe este trecho específico dos documentos, que declaram

Outra operação comum é o uso de vetores booleanos para filtrar os dados. Os operadores são: |para or, &para ande ~para not. Eles devem ser agrupados usando parênteses , pois, por padrão, o Python avaliará uma expressão df.A > 2 & df.B < 3como df.A > (2 & df.B) < 3, enquanto a ordem de avaliação desejada é (df.A > 2) & (df.B < 3).

Portanto, com isso em mente, o elemento elemento lógico AND pode ser implementado com o operador bit a bit &:

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

E a etapa de filtragem subsequente é simplesmente,

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

   A  B  C
1  3  7  9
3  4  7  6

Os parênteses são usados ​​para substituir a ordem de precedência padrão dos operadores bit a bit, que têm maior precedência sobre os operadores condicionais <e >. Consulte a seção Precedência do operador nos documentos python.

Se você não usar parênteses, a expressão será avaliada incorretamente. Por exemplo, se você tentar acidentalmente algo como

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

É analisado como

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

O que se torna,

df['A'] < something_you_dont_want > 5

Que se torna (consulte os documentos python sobre comparação de operadores encadeados ),

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

O que se torna,

# Both operands are Series...
something_else_you_dont_want1 and something_else_you_dont_want2

Que lança

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

Então, não cometa esse erro! 1

Evitando o agrupamento de parênteses
A correção é realmente bastante simples. A maioria dos operadores possui um método associado correspondente para DataFrames. Se as máscaras individuais forem construídas usando funções em vez de operadores condicionais, você não precisará mais agrupar por parens para especificar a ordem de avaliação:

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

Veja a seção Comparações flexíveis. . Para resumir, temos

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

Outra opção para evitar parênteses é usar DataFrame.query(ou eval):

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

   A  B  C
1  3  7  9
3  4  7  6

Eu documentei extensivamentequery e evalem Avaliação de Expressão Dinâmica em pandas usando pd.eval () .

operator.and_
Permite que você execute esta operação de maneira funcional. Chama internamente Series.__and__que corresponde ao operador bit a bit.

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

Você normalmente não precisará disso, mas é útil saber.

Generalizando: np.logical_and(e logical_and.reduce)
Outra alternativa é usar np.logical_and, que também não precisa de agrupamento de parênteses:

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é um ufunc (Universal Functions) , e a maioria dos ufuncs tem um reducemétodo. Isso significa que é mais fácil generalizar logical_andse você tiver várias máscaras para AND. Por exemplo, para máscaras AND m1e m2e m3com &, você teria que fazer

m1 & m2 & m3

No entanto, uma opção mais fácil é

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

Isso é poderoso, porque permite que você desenvolva uma lógica mais complexa (por exemplo, gerando dinamicamente máscaras em uma compreensão de lista e adicionando todas elas):

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 - Eu sei que estou falando sério sobre esse ponto, mas por favor, tenha paciência comigo. Este é um muito , muito erro de principiante comum, e deve ser explicado muito bem.


OR lógico

Pelo dfexposto, suponha que você gostaria de retornar todas as linhas em que A == 3 ou B == 7.

Bitwise Sobrecarregado |

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

Se ainda não o fez, leia também a seção Lógica E acima, todas as advertências se aplicam aqui.

Como alternativa, esta operação pode ser especificada com

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_
Chamadas Series.__or__sob o capô.

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
Para duas condições, use 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

Para várias máscaras, use 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

NÃO lógico

Dada uma máscara, como

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

Se você precisar inverter todos os valores booleanos (para que o resultado final seja [False, False, True]), poderá usar qualquer um dos métodos abaixo.

Bit a bit ~

~mask

0    False
1    False
2     True
dtype: bool

Novamente, as expressões precisam estar entre parênteses.

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

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

Isso chama internamente

mask.__invert__()

0    False
1    False
2     True
dtype: bool

Mas não o use diretamente.

operator.inv
Chama internamente __invert__a série.

operator.inv(mask)

0    False
1    False
2     True
dtype: bool

np.logical_not
Essa é a variante numpy.

np.logical_not(mask)

0    False
1    False
2     True
dtype: bool

Observe que np.logical_andpode ser substituído por np.bitwise_and, logical_orcom bitwise_ore logical_notcom invert.

cs95
fonte
@ cs95 no TLDR, para OR booleano por elemento, você recomenda usar |, o que é equivalente a numpy.bitwise_or, em vez de numpy.logical_or. Posso perguntar por que? Não foi numpy.logical_orprojetado especificamente para esta tarefa? Por que adicionar o ônus de fazê-lo bit a bit para cada par de elementos?
flow2k
@ flow2k você pode citar o texto relevante, por favor? Não consigo encontrar o que você está se referindo. FWIW Eu mantenho que lógico_ * é o equivalente funcional correto dos operadores.
cs95
@ cs95 Estou me referindo à primeira linha da resposta: "TLDR; operadores lógicos no Pandas são &, | e ~".
flow2k
@ flow2k Está literalmente na documentação : "Outra operação comum é o uso de vetores booleanos para filtrar os dados. Os operadores são: | para ou, & para e, e ~ e não".
cs95
@ cs95, ok, acabei de ler esta seção, e ela é usada |para operação booleana em elementos. Mas para mim, essa documentação é mais um "tutorial" e, por outro lado, acho que essas referências à API estão mais próximas da fonte da verdade: numpy.bitwise_or e numpy.logical_or - então estou tentando entender o que é descrito aqui.
flow2k
4

Operadores lógicos para indexação booleana no Pandas

É importante perceber que você não pode usar nenhum dos operadores lógicos do Python ( and,or ou not) em pandas.Seriesou pandas.DataFrames (da mesma forma que você não pode usá-los em numpy.arrays com mais de um elemento). A razão pela qual você não pode usá-los é porque eles implicitamente chamam boolseus operandos, o que gera uma exceção, porque essas estruturas de dados decidiram que o booleano de uma matriz é ambíguo:

>>> 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().

Eu cobri isso mais amplamente na minha resposta ao "Valor da verdade de uma série é ambíguo. Use a.empty, a.bool (), a.item (), a.any () ou a.all ()" Q + A .

Funções lógicas do NumPys

No entanto, o NumPy fornece equivalentes operacionais por elemento a esses operadores como funções que podem ser usadas numpy.array,pandas.Series , pandas.DataFrame, ou qualquer outro (em conformidade) numpy.arraysubclasse:

Então, essencialmente, deve-se usar (assumindo df1 e df2sendo panda DataFrames):

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

Funções bit a bit e operadores bit a bit para booleanos

No entanto, se você tiver um array NumPy booleano, pandas Series ou pandas DataFrames, também poderá usar o funções bit a bit em elementos (para os booleanos, eles são - ou pelo menos devem ser - indistinguíveis das funções lógicas):

Normalmente, os operadores são usados. No entanto, quando combinado com operadores de comparação, é necessário lembrar a comparação entre parênteses, porque os operadores bit a bit têm uma precedência mais alta que os operadores de comparação :

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

Isso pode ser irritante porque os operadores lógicos do Python têm uma precedência mais baixa do que os operadores de comparação; portanto, você normalmente escreve a < 10 and b > 10(em quea e bsão, por exemplo, números inteiros simples) e não precisa de parênteses.

Diferenças entre operações lógicas e bit a bit (em não booleanos)

É realmente importante enfatizar que operações lógicas e de bits são equivalentes apenas para matrizes NumPy booleanas (e Series e DataFrames booleanos). Se estes não contiverem booleanos, as operações fornecerão resultados diferentes. Vou incluir exemplos usando matrizes NumPy, mas os resultados serão semelhantes para as estruturas de dados do 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)

E como o NumPy (e da mesma forma os pandas) faz coisas diferentes para os índices booleano ( matrizes de índice booleano ou "mascarado" ) e inteiro ( matrizes de índice ), os resultados da indexação também serão diferentes:

>>> 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])

Tabela de resumo

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              |        ~

Onde o operador lógico não funciona para matrizes NumPy , pandas Series e pandas DataFrames. Os outros trabalham nessas estruturas de dados (e objetos simples do Python) e trabalham em elementos. No entanto, tenha cuidado com o invertido bit a bit no Python simples, boolporque o bool será interpretado como número inteiro nesse contexto (por exemplo, ~Falseretornos -1e ~Trueretornos -2).

MSeifert
fonte