Opérateurs logiques pour l'indexation booléenne dans Pandas

153

Je travaille avec un index booléen dans Pandas. La question est de savoir pourquoi la déclaration:

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

fonctionne bien alors que

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

quitte avec une erreur?

Exemple:

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
la source
6
Cela est dû au fait que les tableaux numpy et les séries pandas utilisent les opérateurs binaires plutôt que logiques, car vous comparez chaque élément du tableau / série avec un autre. Il n'a donc pas de sens d'utiliser l'opérateur logique dans cette situation. voir en rapport: stackoverflow.com/questions/8632033/…
EdChum
9
En Python and != &. L' andopérateur en Python ne peut pas être remplacé, alors que l' &opérateur ( __and__) le peut. D'où le choix de l'utilisation &dans numpy et pandas.
Steven Rumbalski

Réponses:

209

Quand tu dis

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

Vous demandez implicitement à Python de convertir (a['x']==1)et (a['y']==10)en valeurs booléennes.

Les tableaux NumPy (de longueur supérieure à 1) et les objets Pandas tels que Series n'ont pas de valeur booléenne - en d'autres termes, ils augmentent

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

lorsqu'il est utilisé comme valeur booléenne. C'est parce qu'il n'est pas clair quand il devrait être vrai ou faux . Certains utilisateurs peuvent supposer qu'ils sont True s'ils ont une longueur non nulle, comme une liste Python. D'autres pourraient désirer que ce soit vrai seulement si tous ses éléments sont vrais. D'autres pourraient vouloir qu'il soit vrai si l' un de ses éléments est vrai.

Parce qu'il y a tellement d'attentes contradictoires, les concepteurs de NumPy et de Pandas refusent de deviner et lèvent à la place une ValueError.

Au lieu de cela, vous devez être explicite, en appelant la méthode empty(), all()ou any()pour indiquer le comportement souhaité.

Dans ce cas, cependant, il semble que vous ne vouliez pas d'évaluation booléenne, vous vouliez des éléments logiques et. C'est ce que fait l' &opérateur binaire:

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

renvoie un tableau booléen.


À propos, comme le note alexpmil , les parenthèses sont obligatoires car elles &ont une priorité d'opérateur plus élevée que ==. Sans les parenthèses, a['x']==1 & a['y']==10serait évalué comme a['x'] == (1 & a['y']) == 10ce qui serait à son tour équivalent à la comparaison chaînée (a['x'] == (1 & a['y'])) and ((1 & a['y']) == 10). C'est une expression de la forme Series and Series. L'utilisation de andavec deux séries déclencherait à nouveau la même chose ValueErrorque ci-dessus. C'est pourquoi les parenthèses sont obligatoires.

unutbu
la source
3
Les tableaux numpy ont cette propriété s'ils sont de longueur un. Seuls les développeurs de pandas refusent (obstinément) de deviner: p
Andy Hayden
4
«&» N'a-t-il pas la même courbe ambiguë que «et»? Comment se fait-il qu'en ce qui concerne «&», tous les utilisateurs s'accordent soudainement à dire que cela devrait être élémentaire, alors que lorsqu'ils voient «et», leurs attentes varient?
Indominus
16
@Indominus: Le langage Python lui-même exige que l'expression x and ydéclenche l'évaluation de bool(x)et bool(y). Python "évalue d'abord x; si xest faux, sa valeur est renvoyée; sinon, yest évaluée et la valeur résultante est renvoyée." Ainsi, la syntaxe x and yne peut pas être utilisée pour la logique élémentaire et puisque seulement xou ypeut être retournée. En revanche, les x & ydéclencheurs x.__and__(y)et la __and__méthode peuvent être définis pour renvoyer tout ce que nous voulons.
unutbu
2
Important à noter: les parenthèses autour de la ==clause sont obligatoires . a['x']==1 & a['y']==10renvoie la même erreur que dans la question.
Alex P. Miller
1
À quoi sert "|"?
Euler_Salter
62

TLDR; Les opérateurs logiques dans Pandas sont &, |et ~, et les parenthèses (...)sont importantes!

Python and, oret les notopérateurs logiques sont conçus pour fonctionner avec des scalaires. Les Pandas ont donc dû faire mieux et remplacer les opérateurs binaires pour obtenir une version vectorisée (élément par élément) de cette fonctionnalité.

Donc ce qui suit en python ( exp1et ce exp2sont des expressions qui s'évaluent en un résultat booléen) ...

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

... se traduira par ...

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

pour les pandas.

Si, lors de l'exécution d'une opération logique, vous obtenez un ValueError, vous devez utiliser des parenthèses pour le regroupement:

(exp1) op (exp2)

Par exemple,

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

Etc.


Indexation booléenne : une opération courante consiste à calculer des masques booléens via des conditions logiques pour filtrer les données. Pandas fournit trois opérateurs:&pour ET logique,|pour OU logique et~pour NON logique.

Considérez la configuration suivante:

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

ET logique

Pour dfci-dessus, disons que vous souhaitez renvoyer toutes les lignes où A <5 et B> 5. Cela se fait en calculant les masques pour chaque condition séparément, et en les AND.

&Opérateur bit à bit surchargé
Avant de continuer, veuillez prendre note de cet extrait particulier de la documentation, qui indique

Une autre opération courante est l'utilisation de vecteurs booléens pour filtrer les données. Les opérateurs sont: |pour or, &pour andet ~pour not. Celles-ci doivent être regroupées en utilisant des parenthèses , car par défaut Python évaluera une expression telle df.A > 2 & df.B < 3que df.A > (2 & df.B) < 3, alors que l'ordre d'évaluation souhaité est (df.A > 2) & (df.B < 3).

Donc, dans cet esprit, ET logique élément par élément peut être implémenté avec l'opérateur bit à 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

Et l'étape de filtrage suivante est simplement,

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

   A  B  C
1  3  7  9
3  4  7  6

Les parenthèses sont utilisées pour remplacer l'ordre de priorité par défaut des opérateurs au niveau du bit, qui ont une priorité plus élevée sur les opérateurs conditionnels <et >. Voir la section sur la priorité des opérateurs dans la documentation python.

Si vous n'utilisez pas de parenthèses, l'expression est évaluée de manière incorrecte. Par exemple, si vous tentez accidentellement quelque chose comme

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

Il est analysé comme

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

Qui devient,

df['A'] < something_you_dont_want > 5

Ce qui devient (voir la documentation python sur la comparaison d'opérateurs chaînés ),

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

Qui devient,

# Both operands are Series...
something_else_you_dont_want1 and something_else_you_dont_want2

Qui jette

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

Alors, ne faites pas cette erreur! 1

Éviter le regroupement des parenthèses
Le correctif est en fait assez simple. La plupart des opérateurs ont une méthode liée correspondante pour DataFrames. Si les masques individuels sont créés à l'aide de fonctions au lieu d'opérateurs conditionnels, vous n'aurez plus besoin de regrouper par parenthèses pour spécifier l'ordre d'évaluation:

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

Voir la section sur les comparaisons flexibles. . Pour résumer, nous avons

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

Une autre option pour éviter les parenthèses consiste à utiliser DataFrame.query(ou eval):

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

   A  B  C
1  3  7  9
3  4  7  6

J'ai largement documenté queryet evalen évaluation d'expression dynamique chez les pandas en utilisant pd.eval () .

operator.and_
Vous permet d'effectuer cette opération de manière fonctionnelle. Appelle en interne Series.__and__qui correspond à l'opérateur au niveau du 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

Vous n'en aurez généralement pas besoin, mais il est utile de le savoir.

Généralisation: np.logical_and(et logical_and.reduce)
Une autre alternative consiste à utiliser np.logical_and, qui ne nécessite pas non plus de regroupement de parenthèses:

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_andest un ufunc (Universal Functions) , et la plupart des ufuncs ont une reduceméthode. Cela signifie qu'il est plus facile de généraliser avec logical_andsi vous avez plusieurs masques à AND. Par exemple, pour les masques AND m1et m2et m3avec &, vous devrez faire

m1 & m2 & m3

Cependant, une option plus simple est

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

C'est puissant, car cela vous permet de construire sur cela avec une logique plus complexe (par exemple, générer dynamiquement des masques dans une compréhension de liste et les ajouter tous):

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 - Je sais que je persiste sur ce point, mais soyez indulgents avec moi. C'est une erreur très , très courante du débutant, et doit être expliquée très en détail.


OU logique

Pour ce qui dfprécède, disons que vous souhaitez renvoyer toutes les lignes où A == 3 ou B == 7.

Surchargé au niveau du bit |

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

Si vous ne l'avez pas encore fait, veuillez également lire la section sur la logique ET ci - dessus, toutes les mises en garde s'appliquent ici.

Alternativement, cette opération peut être spécifiée avec

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_
Appels Series.__or__sous le capot.

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
Pour deux conditions, utilisez 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

Pour plusieurs masques, utilisez 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

NON logique

Étant donné un masque, tel que

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

Si vous devez inverser chaque valeur booléenne (pour que le résultat final soit [False, False, True]), vous pouvez utiliser l'une des méthodes ci-dessous.

Bitwise ~

~mask

0    False
1    False
2     True
dtype: bool

Encore une fois, les expressions doivent être mises entre parenthèses.

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

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

Cela appelle en interne

mask.__invert__()

0    False
1    False
2     True
dtype: bool

Mais ne l'utilisez pas directement.

operator.inv
Fait appel __invert__à la série en interne .

operator.inv(mask)

0    False
1    False
2     True
dtype: bool

np.logical_not
C'est la variante numpy.

np.logical_not(mask)

0    False
1    False
2     True
dtype: bool

Remarque, np.logical_andpeut être remplacé par np.bitwise_and, logical_orpar bitwise_oret logical_notpar invert.

cs95
la source
@ cs95 dans le TLDR, pour OR booléen élément par élément, vous préconisez d'utiliser |, ce qui équivaut à numpy.bitwise_or, au lieu de numpy.logical_or. Puis-je demander pourquoi? N'est-il pas numpy.logical_orconçu spécifiquement pour cette tâche? Pourquoi ajouter le fardeau de le faire au niveau du bit pour chaque paire d'éléments?
flow2k
@ flow2k pouvez-vous citer le texte pertinent s'il vous plaît? Je ne trouve pas de quoi vous parlez. FWIW Je maintiens que logical_ * est l'équivalent fonctionnel correct des opérateurs.
cs95
@ cs95 Je fais référence à la première ligne de la réponse: "TLDR; Les opérateurs logiques dans Pandas sont &, | et ~".
flow2k
@ flow2k C'est littéralement dans la documentation : "Une autre opération courante est l'utilisation de vecteurs booléens pour filtrer les données. Les opérateurs sont: | pour ou, & pour et, et ~ pour non."
cs95
@ cs95, ok, je viens de lire cette section, et elle l'utilise |pour une opération booléenne élément par élément. Mais pour moi, cette documentation est plus un "tutoriel", et en revanche, je pense que ces références API sont plus proches de la source de vérité: numpy.bitwise_or et numpy.logical_or - donc j'essaie de comprendre ce qui est décrit ici.
flow2k
4

Opérateurs logiques pour l'indexation booléenne dans Pandas

Il est important de comprendre que vous ne pouvez utiliser aucun des opérateurs logiques Python ( and, orou not) sur pandas.Seriesou pandas.DataFrames (de même, vous ne pouvez pas les utiliser sur numpy.arrays avec plus d'un élément). La raison pour laquelle vous ne pouvez pas les utiliser est parce qu'ils appellent implicitement boolleurs opérandes qui lèvent une exception car ces structures de données ont décidé que le booléen d'un tableau est ambigu:

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

J'ai couvert cela plus en détail dans ma réponse à la "La valeur de vérité d'une série est ambiguë. Utilisez a.empty, a.bool (), a.item (), a.any () ou a.all ()" Q + A .

Fonctions logiques NumPys

Cependant NumPy fournit des équivalents de fonctionnement par élément à ces opérateurs comme des fonctions qui peuvent être utilisées sur numpy.array, pandas.Series, pandas.DataFrame, ou de tout autre (conforme) numpy.arraySous - classe:

Donc, essentiellement, on devrait utiliser (en supposant que df1et df2sont des pandas DataFrames):

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

Fonctions au niveau du bit et opérateurs au niveau du bit pour les booléens

Cependant, dans le cas où vous avez un tableau NumPy booléen, une série pandas ou des DataFrames pandas, vous pouvez également utiliser les fonctions binaires élément par élément (pour les booléens, elles sont - ou du moins devraient être - indiscernables des fonctions logiques):

En général, les opérateurs sont utilisés. Cependant, lorsqu'ils sont combinés avec des opérateurs de comparaison, il ne faut pas oublier de mettre la comparaison entre parenthèses car les opérateurs au niveau du bit ont une priorité plus élevée que les opérateurs de comparaison :

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

Cela peut être irritant car les opérateurs logiques Python ont une priorité plus faible que les opérateurs de comparaison, donc vous écrivez normalement a < 10 and b > 10(où aet bsont par exemple des entiers simples) et n'avez pas besoin de la parenthèse.

Différences entre les opérations logiques et binaires (sur les non-booléens)

Il est vraiment important de souligner que les opérations binaires et logiques ne sont équivalentes que pour les tableaux booléens NumPy (et les séries booléennes et DataFrames). Si ceux-ci ne contiennent pas de booléens, les opérations donneront des résultats différents. J'inclurai des exemples utilisant des tableaux NumPy mais les résultats seront similaires pour les structures de données 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)

Et comme NumPy (et de même les pandas) fait des choses différentes pour les indices booléens ( tableaux d'index booléens ou «masques» ) et entiers ( tableaux d'index ), les résultats de l'indexation seront également différents:

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

Sommaire

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

l'opérateur logique ne fonctionne pas pour les tableaux NumPy , pandas Series et pandas DataFrames. Les autres fonctionnent sur ces structures de données (et sur des objets Python simples) et fonctionnent élément par élément. Cependant, soyez prudent avec l'inversion au niveau du bit sur les Python simples boolcar le booléen sera interprété comme des entiers dans ce contexte (par exemple, les ~Falseretours -1et les ~Trueretours -2).

MSeifert
la source