web-dev-qa-db-fra.com

Évaluation de l'expression dynamique dans pandas à l'aide de pd.eval ()

É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.

  1. Quels arguments doivent être utilisés pour garantir que mon code fonctionne au maximum de ses performances?
  2. Est-il possible d'assigner le résultat de l'expression à df2?
  3. Aussi, pour rendre les choses plus compliquées, comment puis-je passer x comme argument dans l'expression de chaîne?
45
cs95

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:

  1. L'expression entière est une chaîne
  2. _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.
  3. L'accès aux colonnes spécifiques s'effectue à l'aide de l'index d'accesseur d'attribut. Vous pouvez également utiliser "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 et Tuple 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)')
_

Sélection de l'analyseur: l'argument _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.

Sélection du backend: l'argument _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_ arguments

Parfois, 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.

L'argument target (+ inplace) et les expressions d'assignation

Ce 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 dicts, 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 ()

Différences d'utilisation

Expressions avec DataFrames v/s Series Expressions

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_.

Spécification des noms de colonne

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.

Accès aux variables de l'espace de noms local/global

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.

Requêtes multilignes et affectation

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 final

Il 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.

55
cs95

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

enter image description here

3
astro123