Evaluación de expresión dinámica en pandas usando pd.eval ()

Objetivo y Motivación.

eval y query son potentes, pero las funciones subestimadas en la suite de API de pandas, y su uso está lejos de estar completamente documentadas o comprendidas. Con la cantidad adecuada de cuidado, la query y la eval pueden simplificar enormemente el código, mejorar el rendimiento y convertirse en una herramienta poderosa para crear flujos de trabajo dynamics.

El objective de esta QnA canónica es brindar a los usuarios una mejor comprensión de estas funciones, analizar algunas de las características menos conocidas, cómo se usan y la mejor forma de usarlas, con ejemplos claros y fáciles de entender. Los dos temas principales que tratará este post son

  1. Comprensión engine target argumentos de engine , parser y target en pd.eval , y cómo se pueden usar para evaluar expresiones
  2. Comprender la diferencia entre pd.eval , df.eval y df.query , y cuándo es apropiada cada función para la ejecución dinámica.

Esta publicación no es un sustituto de la documentación (enlaces en la respuesta), ¡así que por favor revise eso también!


Pregunta

Enmarcaré una pregunta de tal manera que abra la discusión para varias características admitidas por eval .

Teniendo en cuenta dos 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 ABCD 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 ABCD 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 

Me gustaría realizar aritmética en una o más columnas usando pd.eval . Específicamente, me gustaría portar el siguiente código:

 x = 5 df2['D'] = df1['A'] + (df1['B'] * x) 

… para codificar utilizando eval . La razón para usar eval es que me gustaría automatizar muchos flujos de trabajo, por lo que crearlos de forma dinámica me será útil.

Estoy tratando de entender mejor los argumentos del engine y del parser para determinar la mejor manera de resolver mi problema. He revisado la documentación pero no se me aclaró la diferencia.

  1. ¿Qué argumentos se deben usar para garantizar que mi código funcione al máximo rendimiento?
  2. ¿Hay una manera de asignar el resultado de la expresión de nuevo a df2 ?
  3. Además, para hacer las cosas más complicadas, ¿cómo paso x como un argumento dentro de la expresión de cadena?

Esta respuesta se adentra en las diversas características y funcionalidades ofrecidas por pd.eval , df.query y df.eval .

Preparar
Los ejemplos incluirán estos DataFrames (a menos que se especifique lo contrario).

 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 – El “Manual que falta”

Nota
De las tres funciones que se discuten, pd.eval es la más importante. df.eval y df.query llaman pd.eval debajo del capó. El comportamiento y el uso son más o menos consistentes en las tres funciones, con algunas variaciones semánticas menores que se destacarán más adelante. Esta sección presentará la funcionalidad que es común en las tres funciones, esto incluye, pero no se limita a , syntax permitida, reglas de precedencia y argumentos de palabras clave.

pd.eval puede evaluar expresiones aritméticas que pueden consistir en variables y / o literales. Estas expresiones deben pasarse como cadenas. Por lo tanto, para responder a la pregunta como se indica, puede hacer

 x = 5 pd.eval("df1.A + (df1.B * x)") 

Algunas cosas a tener en cuenta aquí:

  1. La expresión entera es una cadena.
  2. df1 , df2 y x refieren a variables en el espacio de nombres global, estas son recogidas por eval cuando se analiza la expresión
  3. Se accede a columnas específicas utilizando el atributo accessor index. También puede usar "df1['A'] + (df1['B'] * x)" para el mismo efecto.

Abordaré el problema específico de la reasignación en la sección que explica el atributo target=... continuación. Pero por ahora, aquí hay ejemplos más simples de operaciones válidas con 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 

…y así. Las expresiones condicionales también son compatibles de la misma manera. Las siguientes declaraciones son todas expresiones válidas y serán evaluadas por el motor.

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

En la documentación se puede encontrar una lista que detalla todas las características y la syntax compatibles. En resumen,

  • Operaciones aritméticas, excepto para los operadores de desplazamiento a la izquierda ( << ) y a la derecha ( >> ), por ejemplo, df + 2 * pi / s ** 4 % 42 - the_golden_ratio
  • Operaciones de comparación, incluidas comparaciones encadenadas, por ejemplo, 2 < df < df2
  • Operaciones booleanas, por ejemplo, df < df2 and df3 < df4 o not df_bool list y literales de tuple , por ejemplo, [1, 2] o (1, 2)
  • Acceso a atributos, por ejemplo, df.a
  • Expresiones de subíndices, por ejemplo, df[0]
  • Evaluación de variable simple, por ejemplo, pd.eval('df') (esto no es muy útil)
  • Funciones matemáticas: sin, cos, exp, log, expm1, log1p, sqrt, sinh, cosh, tanh, arcsin, arccos, arctan, arccosh, arcsinh, arctanh, abs y arctan2.

Esta sección de la documentación también especifica reglas de syntax que no son compatibles, incluidos los literales set / dict , sentencias if-else, bucles y comprensiones, y expresiones generadoras.

De la lista, es obvio que también puede pasar expresiones que incluyan el índice, como

 pd.eval('df1.A * (df1.index > 1)') 

Selección de analizador: El parser=... argumento

pd.eval admite dos opciones de analizador diferentes al analizar la cadena de expresión para generar el árbol de syntax: pandas y python . La principal diferencia entre los dos se resalta mediante reglas de precedencia ligeramente diferentes.

Usando los pandas analizadores por defecto, los operadores bitwise sobrecargados & y | los cuales implementan operaciones AND y OR vectorizadas con objetos pandas tendrán la misma prioridad de operador que y 'o. Asi que,

 pd.eval("(df1 > df2) & (df3 < df4)") 

Sera lo mismo que

 pd.eval("df1 > df2 & df3 < df4") # pd.eval("df1 > df2 & df3 < df4", parser='pandas') 

Y también lo mismo que

 pd.eval("df1 > df2 and df3 < df4") 

Aquí, los paréntesis son necesarios. Para hacer esto de manera convencional, se requeriría que los parenteses anulen la precedencia más alta de los operadores a nivel de bits:

 (df1 > df2) & (df3 < df4) 

Sin eso, terminamos con

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

Utilice parser='python' si desea mantener la coherencia con las reglas de precedencia del operador real de python mientras evalúa la cadena.

 pd.eval("(df1 > df2) & (df3 < df4)", parser='python') 

La otra diferencia entre los dos tipos de analizadores es la semántica de los operadores == y != Con nodos de tupla y lista, que tienen la semántica similar a la in y not in respectivamente, cuando se usa el analizador 'pandas' . Por ejemplo,

 pd.eval("df1 == [1, 2, 3]") 

Es válido y se ejecutará con la misma semántica que

 pd.eval("df1 in [1, 2, 3]") 

OTOH, pd.eval("df1 == [1, 2, 3]", parser='python') lanzará un error NotImplementedError .

Selección de fondo: El argumento engine=...

Hay dos opciones: numexpr (predeterminado) y python . La opción numexpr usa el backend numexpr que está optimizado para el rendimiento.

Con 'python' backend 'python' , su expresión se evalúa de manera similar a simplemente pasar la expresión a la función eval de python. Tiene la flexibilidad de hacer más expresiones internas, como operaciones de cadena, por ejemplo.

 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 

Desafortunadamente, este método no ofrece beneficios de rendimiento sobre el motor numexpr , y existen muy pocas medidas de seguridad para garantizar que las expresiones peligrosas no se evalúen, ¡así que UTILICE A SU PROPIO RIESGO ! Generalmente no se recomienda cambiar esta opción a 'python' menos que sepa lo que está haciendo.

local_dict y global_dict

A veces, es útil proporcionar valores para las variables utilizadas dentro de las expresiones, pero que actualmente no están definidas en su espacio de nombres. Puedes pasar un diccionario a local_dict

Por ejemplo,

 pd.eval("df1 > thresh") UndefinedVariableError: name 'thresh' is not defined 

Esto falla porque la thresh no está definida. Sin embargo, esto funciona:

 pd.eval("df1 > x", local_dict={'thresh': 10}) 

Esto es útil cuando tiene variables para suministrar desde un diccionario. Alternativamente, con el motor 'python' , simplemente puedes hacer esto:

 mydict = {'thresh': 5} # Dictionary values with *string* keys cannot be accessed without # using the 'python' engine. pd.eval('df1 > mydict["thresh"]', engine='python') 

Pero esto posiblemente será mucho más lento que usar el motor 'numexpr' y pasar un diccionario a local_dict o global_dict . Con suerte, esto debería hacer un argumento convincente para el uso de estos parámetros.

El argumento de target (+ inplace ) y las expresiones de asignación

Esto no suele ser un requisito porque usualmente hay formas más simples de hacerlo, pero puede asignar el resultado de pd.eval a un objeto que implementa __getitem__ como dict s, y (lo adivinó) DataFrames.

Considera el ejemplo de la pregunta.

 x = 5 df2['D'] = df1['A'] + (df1['B'] * x) 

Para asignar una columna "D" a df2 , hacemos

 pd.eval('D = df1.A + (df1.B * x)', target=df2) ABCD 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 

Esta no es una modificación in situ de df2 (pero puede ser ... sigue leyendo). Considere otro ejemplo:

 pd.eval('df1.A + df2.A') 0 10 1 11 2 7 3 16 4 10 dtype: int32 

Si quisiera (por ejemplo) volver a asignar esto a un DataFrame, podría usar el argumento de target siguiente manera:

 df = pd.DataFrame(columns=list('FBGH'), index=df1.index) df FBGH 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 FBGH 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 desea realizar una mutación in situ en df , configure inplace=True .

 pd.eval('B = df1.A + df2.A', target=df, inplace=True) # Similar to # df['B'] = pd.eval('df1.A + df2.A') df FBGH 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 se establece inplace sin un objective, se ValueError un ValueError .

Si bien es divertido jugar con el argumento target , rara vez tendrá que usarlo.

Si quisiera hacer esto con df.eval , usaría una expresión que involucra una asignación:

 df = df.eval("B = @df1.A + @df2.A") # df.eval("B = @df1.A + @df2.A", inplace=True) df FBGH 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 

Nota
Uno de los usos no deseados de pd.eval es analizar cadenas literales de una manera muy similar a ast.literal_eval :

 pd.eval("[1, 2, 3]") array([1, 2, 3], dtype=object) 

También puede analizar listas anidadas con el motor 'python' :

 pd.eval("[[1, 2, 3], [4, 5], [10]]", engine='python') [[1, 2, 3], [4, 5], [10]] 

Y listas de cuerdas:

 pd.eval(["[1, 2, 3]", "[4, 5]", "[10]"], engine='python') [[1, 2, 3], [4, 5], [10]] 

El problema, sin embargo, es para listas con una longitud mayor que 10:

 pd.eval(["[1]"] * 100, engine='python') # Works pd.eval(["[1]"] * 101, engine='python') AttributeError: 'PandasExprVisitor' object has no attribute 'visit_Ellipsis' 

Puede encontrar más información sobre este error, causas, soluciones y soluciones alternativas aquí .


DataFrame.eval - Una yuxtaposición con pandas.eval

Como se mencionó anteriormente, df.eval llama a pd.eval bajo el capó. El código fuente v0.23 muestra esto:

 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 crea argumentos, hace un poco de validación y pasa los argumentos a pd.eval .

Para más información, puede leer en: cuándo usar DataFrame.eval () versus pandas.eval () o python eval ()

Diferencias de uso

Expresiones con DataFrames v / s Series Expressions

Para consultas dinámicas asociadas con DataFrames completos, debería preferir pd.eval . Por ejemplo, no hay una forma sencilla de especificar el equivalente de pd.eval("df1 + df2") cuando llama a df1.eval o df2.eval .

Especificar nombres de columna

Otra diferencia importante es cómo se accede a las columnas. Por ejemplo, para agregar dos columnas "A" y "B" en df1 , debería llamar a pd.eval con la siguiente expresión:

 pd.eval("df1.A + df1.B") 

Con df.eval, solo necesita proporcionar los nombres de columna:

 df1.eval("A + B") 

Dado que, dentro del contexto de df1 , está claro que "A" y "B" se refieren a nombres de columna.

También puede consultar el índice y las columnas utilizando el index (a menos que se nombre el índice, en cuyo caso usted usaría el nombre).

 df1.eval("A + index") 

O, más generalmente, para cualquier DataFrame con un índice que tenga 1 o más niveles, puede referirse al nivel k th del índice en una expresión usando la variable "ilevel_k" que significa " i ndex a nivel k ". IOW, la expresión anterior se puede escribir como df1.eval("A + ilevel_0") .

Estas reglas también se aplican a la query .

Acceso a las variables en el espacio de nombres local / global

Las variables suministradas dentro de las expresiones deben ir precedidas por el símbolo "@", para evitar confusiones con los nombres de las columnas.

 A = 5 df1.eval("A > @A") 

Lo mismo ocurre con la query /

No hace falta decir que los nombres de sus columnas deben seguir las reglas para que los nombres de identificadores válidos en Python sean accesibles dentro de eval . Vea aquí una lista de reglas para nombrar identificadores.

Consultas multilínea y asignación

Un hecho poco conocido es que eval soporta expresiones multilínea que se ocupan de la asignación. Por ejemplo, para crear dos nuevas columnas "E" y "F" en df1 basadas en algunas operaciones aritméticas en algunas columnas, y una tercera columna "G" basada en las "E" y "F" creadas previamente, podemos hacer

 df1.eval(""" E = A + B F = @df2.A + @df2.B G = E >= F """) ABCDEFG 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 

...¡Hábil! Sin embargo, tenga en cuenta que esto no es compatible con la query .


eval v / s query - Word final

Es df.query pensar en df.query como una función que usa pd.eval como subrutina.

Normalmente, la query (como su nombre lo indica) se usa para evaluar expresiones condicionales (es decir, expresiones que dan como resultado los valores Verdadero / Falso) y devolver las filas correspondientes al resultado True . El resultado de la expresión se pasa luego a loc (en la mayoría de los casos) para devolver las filas que satisfacen la expresión. Según la documentación,

El resultado de la evaluación de esta expresión se pasa primero a DataFrame.loc y si eso falla debido a una clave multidimensional (por ejemplo, un DataFrame), el resultado se pasará a DataFrame.__getitem__() .

Este método utiliza la función pandas.eval() nivel pandas.eval() para evaluar la consulta pasada.

En términos de similitud, query y df.eval son iguales en cuanto a cómo acceden a los nombres de columna y las variables.

Esta diferencia clave entre los dos, como se mencionó anteriormente, es cómo manejan el resultado de la expresión. Esto se vuelve obvio cuando en realidad ejecutas una expresión a través de estas dos funciones. Por ejemplo, considere

 df1.A 0 5 1 7 2 2 3 8 4 7 Name: A, dtype: int32 df2.B 0 9 1 3 2 0 3 1 4 7 Name: B, dtype: int32 

Para obtener todas las filas donde "A"> = "B" en df1 , df1 eval esta manera:

 m = df1.eval("A >= B") m 0 True 1 False 2 False 3 True 4 True dtype: bool 

m representa el resultado intermedio generado al evaluar la expresión "A> = B". Luego usamos la máscara para filtrar df1 :

 df1[m] # df1.loc[m] ABCD 0 5 0 3 3 3 8 8 1 6 4 7 7 8 1 

Sin embargo, con la query , el resultado intermedio "m" se pasa directamente a loc , por lo que con la query , simplemente debe hacer

 df1.query("A >= B") ABCD 0 5 0 3 3 3 8 8 1 6 4 7 7 8 1 

En cuanto al rendimiento, es exactamente el mismo.

 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) 

Pero este último es más conciso y expresa la misma operación en un solo paso.

Tenga en cuenta que también puede hacer cosas raras con una query como esta (para, por ejemplo, devolver todas las filas indexadas por df1.index)

 df1.query("index") # Same as df1.loc[df1.index] # Pointless,... I know ABCD 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 

Pero no lo hagas

Línea inferior: utilice la query al consultar o filtrar filas según una expresión condicional.

Ya tiene un gran tutorial, pero tenga en cuenta que antes de saltar al uso de eval/query atraído por su syntax más simple, tiene graves problemas de rendimiento si su conjunto de datos tiene menos de 15,000 filas.

En ese caso, simplemente use df.loc[mask1, mask2] .

Consulte: https://pandas.pydata.org/pandas-docs/version/0.22/enhancingperf.html#enhancingperf-eval

introduzca la descripción de la imagen aquí