query examples data consultas python pandas dataframe eval

python - examples - query pandas data frame



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

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

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 documentation 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 documentation , 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''))

documentation - 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 , sintaxis 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 documentation se puede encontrar una lista que detalla todas las características y la sintaxis 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 sintaxis 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 sintaxis: 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) 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

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 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 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 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 se establece inplace sin un objetivo, 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 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

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

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

...¡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] A B C D 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") A B C D 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 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

Pero no lo hagas

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