Función vectorizada de pandas cumsum versus numpy

Al responder a la pregunta Vectorizar el cálculo de un Marco de datos de Pandas , noté un problema interesante con respecto al rendimiento.

Tenía la impresión de que funciones df.min() como df.min() , df.mean() , df.cumsum() , etc. Sin embargo, estoy viendo una gran discrepancia en el rendimiento entre df.cumsum() y una alternativa numpy .

Dado que los pandas usan una gran numpy arreglos en su infraestructura, esperaba que el rendimiento fuera más cercano. Intenté investigar el código fuente de df.cumsum() pero lo encontré intratable. ¿Alguien puede explicar por qué es mucho más lento?

Visto desde la respuesta de @HYRY, el problema se reduce a la pregunta de por qué los dos comandos siguientes dan una discrepancia tan grande en los tiempos:

 import pandas as pd, numpy as np df_a = pd.DataFrame(np.arange(1,1000*1000+1).reshape(1000,1000)) %timeit pd.DataFrame(np.nancumsum(df_a.values)) # 4.18 ms %timeit df_a.cumsum() # 15.7 ms 

(Temporización realizada por uno de los comentaristas, ya que mi número v1.11 no tiene nancumsum).

Parece que hay un par de cosas que no valen nada aquí.

Primero, df_a.cumsum() defecto es axis=0 (Pandas no tiene el concepto de sumr todo el DataFrame en una llamada), mientras que NumPy call por defecto es axis=None . Entonces, al especificar un eje en una operación y alisar efectivamente la otra, estás comparando manzanas con naranjas.

Dicho esto, hay tres llamadas que podrías comparar:

 >>> np.cumsum(df_a, axis=0) >>> df_a.cumsum() >>> val.cumsum(axis=0) # val = df_a.values 

donde, en la llamada final, val es la matriz NumPy subyacente y no contamos para obtener el atributo .values en tiempo de ejecución.

Entonces, si está trabajando en el shell de IPython, %prun perfil de línea con %prun :

 >>> %prun -q -T pdcumsum.txt df_a.cumsum() >>> val = df_a.values >>> %prun -q -T ndarraycumsum.txt val.cumsum(axis=0) >>> %prun -q -T df_npcumsum.txt np.cumsum(df_a, axis=0) 

-T guarda la salida en texto para que pueda ver los tres combinados entre sí. Esto es lo que terminas con:

  • df_a.cumsum() : 186 llamadas de función, .022 segundos. 0.013 de eso se gasta en numpy.ndarray.cumsum() . (Supongo que si no hay NaN, no se necesita nancumsum() , pero no me cite). Otra parte se gasta en copiar la matriz.
  • val.cumsum(axis=0) : 5 llamadas a funciones, 0.020 segundos. No se realiza ninguna copia (aunque esto no es una operación in situ).
  • np.cumsum(df_a, axis=0) : 204 llamadas a funciones, 0.026 segundos. Basta con decir que pasar un objeto Pandas a una función NumPy de nivel superior parece invocar el método equivalente en el objeto Pandas, que pasa por un montón de sobrecarga y luego vuelve a llamar a la función NumPy.

Ahora, a diferencia de %timeit , solo estás haciendo 1 llamada aquí, como lo harías en %time , por lo que no me apoyaría demasiado en las diferencias de tiempo relativas con %prun ; tal vez la comparación de las llamadas de función interna es lo que es útil. Pero en este caso, cuando se especifica el mismo eje para ambos, las diferencias de tiempo no son realmente tan drásticas, incluso si el número de llamadas realizadas por Pandas empequeñece el de NumPy. En otras palabras, en este caso el tiempo de las tres llamadas está dominado por np.ndarray.cumsum() , y las llamadas Pandas auxiliares no consumen mucho tiempo. Hay otros casos en los que las llamadas de Pandas auxiliares consumen mucho más tiempo de ejecución, pero este no parece ser uno de ellos.

Imagen general, como lo reconoció Wes McKinney,

Las operaciones bastante simples, desde la indexación hasta las estadísticas de resumen, pueden pasar a través de múltiples capas de andamios antes de alcanzar el nivel más bajo de cómputos.

con el compromiso de ser la flexibilidad y el aumento de la funcionalidad, podría argumentar.

Un último detalle: dentro de NumPy, puede evitar una pequeña sobrecarga llamando al método de instancia ndarray.cumsum() lugar de a la función de nivel superior np.cumsum() , porque la última simplemente termina enrutando a la primera. Pero como dijo un sabio, la optimización prematura es la raíz de todo mal.


Para referencia:

 >>> pd.__version__, np.__version__ ('0.22.0', '1.14.0') 

Las pandas pueden tratar con NaN, puedes verificar la diferencia al:

 a = np.random.randn(1000000) %timeit np.nancumsum(a) %timeit np.cumsum(a) 

salidas:

 9.02 ms ± 189 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 4.37 ms ± 18.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)