Étant donné deux DataFrames
np.random.seed(0)
df1 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df2 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df1
A B C D
0 5 0 3 3
1 7 9 3 5
2 2 4 7 6
3 8 8 1 6
4 7 7 8 1
df2
A B C D
0 5 9 8 9
1 4 3 0 3
2 5 0 2 3
3 8 1 3 3
4 3 7 0 1
Je voudrais effectuer un calcul arithmétique sur une ou plusieurs colonnes en utilisant pd.eval
. Plus précisément, je voudrais porter le code suivant:
x = 5
df2['D'] = df1['A'] + (df1['B'] * x)
... pour coder avec eval
. La raison d’utiliser eval
est que j’aimerais automatiser de nombreux flux de travail. Par conséquent, leur création dynamique me sera utile.
J'essaie de mieux comprendre les arguments engine
et parser
afin de déterminer la meilleure façon de résoudre mon problème. J'ai parcouru le documentation mais la différence ne m'a pas été éclaircie.
df2
?x
comme argument dans l'expression de chaîne?Cette réponse plonge dans les différentes fonctionnalités offertes par pd.eval
, df.query
et df.eval
.
Configuration
Les exemples impliqueront ces DataFrames (sauf indication contraire).
_np.random.seed(0)
df1 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df2 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df3 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df4 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
_
pandas.eval
- Le "Manuel manquant"Remarque
Parmi les trois fonctions en discussion, _pd.eval
_ est la plus importante. _df.eval
_ et _df.query
_ appelez _pd.eval
_ sous le capot. Le comportement et l'utilisation sont plus ou moins cohérents entre les trois fonctions, avec quelques variations sémantiques mineures qui seront mises en évidence ultérieurement. Cette section présente les fonctionnalités communes aux trois fonctions. Elle inclut (sans toutefois s'y limiter) la syntaxe autorisée , les règles de priorité et le mot clé . arguments.
_pd.eval
_ peut évaluer des expressions arithmétiques pouvant être constituées de variables et/ou de littéraux. Ces expressions doivent être transmises sous forme de chaînes. Donc, pour répondre à la question comme indiqué, vous pouvez le faire
_x = 5
pd.eval("df1.A + (df1.B * x)")
_
Quelques points à noter ici:
df1
_, _df2
_ et x
font référence à des variables de l'espace de noms global. Elles sont sélectionnées par eval
lors de l'analyse syntaxique de l'expression."df1['A'] + (df1['B'] * x)"
pour le même effet.Je traiterai de la question spécifique de la réaffectation dans la section expliquant l'attribut _target=...
_ ci-dessous. Mais pour l'instant, voici des exemples plus simples d'opérations valides avec _pd.eval
_:
_pd.eval("df1.A + df2.A") # Valid, returns a pd.Series object
pd.eval("abs(df1) ** .5") # Valid, returns a pd.DataFrame object
_
...etc. Les expressions conditionnelles sont également prises en charge de la même manière. Les instructions ci-dessous sont toutes des expressions valides et seront évaluées par le moteur.
_pd.eval("df1 > df2")
pd.eval("df1 > 5")
pd.eval("df1 < df2 and df3 < df4")
pd.eval("df1 in [1, 2, 3]")
pd.eval("1 < 2 < 3")
_
Une liste détaillant toutes les fonctionnalités et la syntaxe prises en charge est disponible dans le documentation . En résumé,
- Opérations arithmétiques sauf pour les opérateurs décalage gauche (_
<<
_) et droit (_>>
_), par exemple, _df + 2 * pi / s ** 4 % 42
_ - the_golden_ratio- Opérations de comparaison, y compris les comparaisons chaînées, par exemple, _
2 < df < df2
_- Opérations booléennes, par exemple, _
df < df2 and df3 < df4
_ ou _not df_bool
_list
etTuple
littéraux, par exemple, _[1, 2]
_ ou _(1, 2)
_- Accès aux attributs, par exemple, _
df.a
_- Expressions souscrites, par exemple, _
df[0]
_- Évaluation de variable simple, par exemple,
pd.eval('df')
(ce n'est pas très utile)- Fonctions mathématiques: sin, cos, exp, log, expm1, log1p, sqrt, sinh, cosh, tanh, arcsin, arccos, arctan, arctosh, arccosh, arcsinh, arctanh, abs et arctan2.
Cette section de la documentation spécifie également les règles de syntaxe non prises en charge, notamment set
/dict
littéraux, instructions if-else, boucles et compréhensions et expressions génératrices.
Dans la liste, il est évident que vous pouvez également transmettre des expressions impliquant l’index, telles que
_pd.eval('df1.A * (df1.index > 1)')
_
parser=...
__pd.eval
_ prend en charge deux options d'analyse différentes lors de l'analyse syntaxique de la chaîne d'expression afin de générer l'arbre de syntaxe: pandas
et python
. La principale différence entre les deux est soulignée par des règles de priorité légèrement différentes.
En utilisant l’analyseur par défaut pandas
, les opérateurs au niveau des bits surchargés _&
_ et _|
_ qui implémentent des opérations AND et OR vectorisées avec les objets pandas auront le Même priorité que and
et or
. Alors,
_pd.eval("(df1 > df2) & (df3 < df4)")
_
Sera le même que
_pd.eval("df1 > df2 & df3 < df4")
# pd.eval("df1 > df2 & df3 < df4", parser='pandas')
_
Et aussi la même chose que
_pd.eval("df1 > df2 and df3 < df4")
_
Ici, les parenthèses sont nécessaires. Pour faire cela de manière conventionnelle, les parens seraient tenus de remplacer la priorité plus élevée des opérateurs au niveau du bit:
_(df1 > df2) & (df3 < df4)
_
Sans cela, on se retrouve avec
_df1 > df2 & df3 < df4
ValueError: The truth value of a DataFrame is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
_
Utilisez _parser='python'
_ si vous souhaitez maintenir la cohérence avec les règles de priorité des opérateurs réels de python lors de l'évaluation de la chaîne.
_pd.eval("(df1 > df2) & (df3 < df4)", parser='python')
_
L’autre différence entre les deux types d’analyseurs est la sémantique des opérateurs _==
_ et _!=
_ avec les nœuds list et Tuple, qui ont la même sémantique que in
et _not in
_ , lors de l’utilisation de l’analyseur _'pandas'
_. Par exemple,
_pd.eval("df1 == [1, 2, 3]")
_
Est valide et fonctionnera avec la même sémantique que
_pd.eval("df1 in [1, 2, 3]")
_
OTOH, pd.eval("df1 == [1, 2, 3]", parser='python')
générera une erreur NotImplementedError
.
engine=...
_Il existe deux options: numexpr
(valeur par défaut) et python
. L'option numexpr
utilise le moteur numexpr qui est optimisé pour la performance.
Avec le backend _'python'
_, votre expression est évaluée comme si vous la passiez simplement à la fonction eval
de python. Vous avez la possibilité d'en faire plus à l'intérieur des expressions, telles que les opérations sur les chaînes, par exemple.
_df = pd.DataFrame({'A': ['abc', 'def', 'abacus']})
pd.eval('df.A.str.contains("ab")', engine='python')
0 True
1 False
2 True
Name: A, dtype: bool
_
Malheureusement, cette méthode n'offre aucun avantage en performances par rapport au moteur numexpr
et il existe très peu de mesures de sécurité pour garantir que les expressions dangereuses ne sont pas évaluées. UTILISEZ AT VOTRE PROPRE RISQUE ! Il n'est généralement pas recommandé de changer cette option en _'python'
_ sauf si vous savez ce que vous faites.
local_dict
_ et _global_dict
_ argumentsParfois, il est utile de fournir des valeurs pour les variables utilisées dans les expressions, mais non définies dans votre espace de noms. Vous pouvez passer un dictionnaire à _local_dict
_
Par exemple,
_pd.eval("df1 > thresh")
UndefinedVariableError: name 'thresh' is not defined
_
Cela échoue car thresh
n'est pas défini. Cependant, cela fonctionne:
_pd.eval("df1 > thresh", local_dict={'thresh': 10})
_
Ceci est utile lorsque vous avez des variables à fournir à partir d'un dictionnaire. Alternativement, avec le moteur _'python'
_, vous pouvez simplement faire ceci:
_mydict = {'thresh': 5}
# Dictionary values with *string* keys cannot be accessed without
# using the 'python' engine.
pd.eval('df1 > mydict["thresh"]', engine='python')
_
Mais cela risque d’être beaucoup plus lent que l’utilisation du moteur _'numexpr'
_ et la transmission d’un dictionnaire à _local_dict
_ ou _global_dict
_. J'espère que cela devrait constituer un argument convaincant pour l'utilisation de ces paramètres.
target
(+ inplace
) et les expressions d'assignationCe n’est pas souvent une exigence car il existe généralement des méthodes plus simples, mais vous pouvez affecter le résultat de _pd.eval
_ à un objet implémentant ___getitem__
_ tel que dict
s, et (vous avez deviné). it) DataFrames.
Considérons l'exemple dans la question
_x = 5 df2['D'] = df1['A'] + (df1['B'] * x)
_
Pour affecter une colonne "D" à _df2
_, nous faisons
_pd.eval('D = df1.A + (df1.B * x)', target=df2)
A B C D
0 5 9 8 5
1 4 3 0 52
2 5 0 2 22
3 8 1 3 48
4 3 7 0 42
_
Ce n'est pas une modification sur place de _df2
_ (mais cela peut être ... continuer à lire). Prenons un autre exemple:
_pd.eval('df1.A + df2.A')
0 10
1 11
2 7
3 16
4 10
dtype: int32
_
Si vous vouliez (par exemple) réassigner cela à un DataFrame, vous pouvez utiliser l'argument target
comme suit:
_df = pd.DataFrame(columns=list('FBGH'), index=df1.index)
df
F B G H
0 NaN NaN NaN NaN
1 NaN NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN NaN NaN NaN
4 NaN NaN NaN NaN
df = pd.eval('B = df1.A + df2.A', target=df)
# Similar to
# df = df.assign(B=pd.eval('df1.A + df2.A'))
df
F B G H
0 NaN 10 NaN NaN
1 NaN 11 NaN NaN
2 NaN 7 NaN NaN
3 NaN 16 NaN NaN
4 NaN 10 NaN NaN
_
Si vous souhaitez effectuer une mutation sur place sur df
, définissez _inplace=True
_.
_pd.eval('B = df1.A + df2.A', target=df, inplace=True)
# Similar to
# df['B'] = pd.eval('df1.A + df2.A')
df
F B G H
0 NaN 10 NaN NaN
1 NaN 11 NaN NaN
2 NaN 7 NaN NaN
3 NaN 16 NaN NaN
4 NaN 10 NaN NaN
_
Si inplace
est défini sans cible, un ValueError
est levé.
Bien que l’argument target
soit amusant à jouer, vous aurez rarement besoin de l’utiliser.
Si vous voulez faire cela avec _df.eval
_, vous utiliseriez une expression impliquant une affectation:
_df = df.eval("B = @df1.A + @df2.A")
# df.eval("B = @df1.A + @df2.A", inplace=True)
df
F B G H
0 NaN 10 NaN NaN
1 NaN 11 NaN NaN
2 NaN 7 NaN NaN
3 NaN 16 NaN NaN
4 NaN 10 NaN NaN
_
Remarque
L’une des utilisations non prévues de _pd.eval
_ est l’analyse de chaînes littérales d’une manière très similaire à _ast.literal_eval
_:
_pd.eval("[1, 2, 3]")
array([1, 2, 3], dtype=object)
_
Il peut également analyser les listes imbriquées avec le moteur _'python'
_:
_pd.eval("[[1, 2, 3], [4, 5], [10]]", engine='python')
[[1, 2, 3], [4, 5], [10]]
_
Et des listes de chaînes:
_pd.eval(["[1, 2, 3]", "[4, 5]", "[10]"], engine='python')
[[1, 2, 3], [4, 5], [10]]
_
Le problème, cependant, concerne les listes dont la longueur est supérieure à 100:
_pd.eval(["[1]"] * 100, engine='python') # Works
pd.eval(["[1]"] * 101, engine='python')
AttributeError: 'PandasExprVisitor' object has no attribute 'visit_Ellipsis'
_
Vous pouvez trouver plus d'informations sur cette erreur, les causes, les correctifs et les solutions de contournement ici .
DataFrame.eval
- Une juxtaposition avec _pandas.eval
_Comme mentionné ci-dessus, _df.eval
_ appelle _pd.eval
_ sous le capot. Le code source v0.2 montre ceci:
_def eval(self, expr, inplace=False, **kwargs):
from pandas.core.computation.eval import eval as _eval
inplace = validate_bool_kwarg(inplace, 'inplace')
resolvers = kwargs.pop('resolvers', None)
kwargs['level'] = kwargs.pop('level', 0) + 1
if resolvers is None:
index_resolvers = self._get_index_resolvers()
resolvers = dict(self.iteritems()), index_resolvers
if 'target' not in kwargs:
kwargs['target'] = self
kwargs['resolvers'] = kwargs.get('resolvers', ()) + Tuple(resolvers)
return _eval(expr, inplace=inplace, **kwargs)
_
eval
crée des arguments, effectue une petite validation et les transmet à _pd.eval
_.
Pour en savoir plus, vous pouvez lire sur: quand utiliser DataFrame.eval () contre pandas.eval () ou python eval ()
Pour les requêtes dynamiques associées à des DataFrames entiers, vous devriez préférer _pd.eval
_. Par exemple, il n'existe pas de moyen simple de spécifier l'équivalent de pd.eval("df1 + df2")
lorsque vous appelez _df1.eval
_ ou _df2.eval
_.
Une autre différence majeure concerne le mode d'accès aux colonnes. Par exemple, pour ajouter deux colonnes "A" et "B" dans _df1
_, vous devez appeler _pd.eval
_ avec l'expression suivante:
_pd.eval("df1.A + df1.B")
_
Avec df.eval, vous devez uniquement fournir les noms de colonne:
_df1.eval("A + B")
_
Étant donné que, dans le contexte de _df1
_, il est clair que "A" et "B" se rapportent à des noms de colonne.
Vous pouvez également faire référence à l'index et aux colonnes en utilisant index
(à moins que l'index ne soit nommé, auquel cas vous utiliseriez le nom).
_df1.eval("A + index")
_
Ou, plus généralement, pour tout DataFrame avec un index ayant 1 ou plusieurs niveaux, vous pouvez vous référer au kth niveau de l'index dans une expression utilisant la variable "ilevel_k" qui signifie "jendex au niveau k ". IOW, l’expression ci-dessus peut être écrite sous la forme df1.eval("A + ilevel_0")
.
Ces règles s'appliquent également à query
.
Les variables fournies dans les expressions doivent être précédées du symbole "@" afin d'éviter toute confusion avec les noms de colonne.
_A = 5
df1.eval("A > @A")
_
Il en va de même pour query
.
Il va sans dire que vos noms de colonne doivent suivre les règles de nommage d'identifiant valide dans python pour être accessibles à l'intérieur de eval
. Voir ici pour une liste de règles sur les identifiants de nommage.
Un fait peu connu est que eval
prend en charge les expressions multilignes qui traitent des affectations. Par exemple, pour créer deux nouvelles colonnes "E" et "F" dans df1 sur la base d'opérations arithmétiques sur certaines colonnes, et une troisième colonne "G" basée sur les "E" et "F" créés précédemment, nous pouvons procéder
_df1.eval("""
E = A + B
F = @df2.A + @df2.B
G = E >= F
""")
A B C D E F G
0 5 0 3 3 5 14 False
1 7 9 3 5 16 7 True
2 2 4 7 6 6 5 True
3 8 8 1 6 16 9 True
4 7 7 8 1 14 10 True
_
... Nifty! Cependant, notez que ceci n'est pas supporté par query
.
eval
v/s query
- Mot finalIl est utile de penser à _df.query
_ en tant que fonction qui utilise _pd.eval
_ en tant que sous-programme.
En règle générale, query
(comme son nom l'indique) est utilisé pour évaluer les expressions conditionnelles (c'est-à-dire les expressions qui donnent des valeurs True/False) et renvoyer les lignes correspondant au résultat True
. Le résultat de l'expression est ensuite transmis à loc
(dans la plupart des cas) pour renvoyer les lignes satisfaisant l'expression. Selon la documentation,
Le résultat de l'évaluation de cette expression est d'abord transmis à _
DataFrame.loc
_. Si cela échoue à cause d'une clé multidimensionnelle (par exemple, un DataFrame), le résultat sera transmis àDataFrame.__getitem__()
.Cette méthode utilise la fonction
pandas.eval()
de niveau supérieur pour évaluer la requête transmise.
En termes de similitude, query
et _df.eval
_ sont identiques dans la manière dont ils accèdent aux noms de colonne et aux variables.
Comme mentionné ci-dessus, cette différence essentielle entre les deux réside dans la manière dont ils gèrent le résultat de l'expression. Cela devient évident lorsque vous exécutez une expression via ces deux fonctions. Par exemple, considérons
_df1.A
0 5
1 7
2 2
3 8
4 7
Name: A, dtype: int32
df1.B
0 9
1 3
2 0
3 1
4 7
Name: B, dtype: int32
_
Pour obtenir toutes les lignes où "A"> = "B" dans _df1
_, nous utiliserions eval
comme ceci:
_m = df1.eval("A >= B")
m
0 True
1 False
2 False
3 True
4 True
dtype: bool
_
m
représente le résultat intermédiaire généré en évaluant l'expression "A> = B". Nous utilisons ensuite le masque pour filtrer _df1
_:
_df1[m]
# df1.loc[m]
A B C D
0 5 0 3 3
3 8 8 1 6
4 7 7 8 1
_
Cependant, avec query
, le résultat intermédiaire "m" est directement passé à loc
, de sorte qu'avec query
, il vous suffira de faire
_df1.query("A >= B")
A B C D
0 5 0 3 3
3 8 8 1 6
4 7 7 8 1
_
Performance sage, il est exactement identique.
_df1_big = pd.concat([df1] * 100000, ignore_index=True)
%timeit df1_big[df1_big.eval("A >= B")]
%timeit df1_big.query("A >= B")
14.7 ms ± 33.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
14.7 ms ± 24.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
_
Mais ce dernier est plus concis et exprime la même opération en une seule étape.
Notez que vous pouvez aussi faire des choses bizarres avec query
comme ceci (pour renvoyer toutes les lignes indexées par df1.index)
_df1.query("index")
# Same as df1.loc[df1.index] # Pointless,... I know
A B C D
0 5 0 3 3
1 7 9 3 5
2 2 4 7 6
3 8 8 1 6
4 7 7 8 1
_
Mais non.
Ligne de fond: veuillez utiliser query
pour interroger ou filtrer des lignes en fonction d'une expression conditionnelle.
Très bon tutoriel déjà, mais n'oubliez pas qu'avant de se lancer énormément dans l'utilisation de eval/query
attiré par sa syntaxe plus simple, des problèmes de performances graves se posent si votre jeu de données compte moins de 15 000 lignes.
Dans ce cas, utilisez simplement df.loc[mask1, mask2]
.
Voir: https://pandas.pydata.org/pandas-docs/version/0.22/enhancingperf.html#enhancingperf-eval