Máscara de Pandas / métodos de dónde versus NumPy np.donde
A menudo uso Pandas mask
y where
métodos para una lógica más limpia al actualizar valores en una serie condicionalmente. Sin embargo, para el código relativamente crítico de rendimiento, noto una caída significativa del rendimiento en relación con numpy.where
.
Aunque estoy feliz de aceptar esto para casos específicos, estoy interesado en saber:
- Do Pandas
mask
/where
los métodos ofrecen cualquier funcionalidad adicional, aparte deinplace
/errors
/try-cast
¿parámetros? Me comprenda esos 3 parámetros, pero rara vez los use. Por ejemplo, no tengo idea de a qué se refiere el parámetrolevel
. - ¿Hay algún contra-ejemplo no trivial donde
mask
/where
¿superanumpy.where
? Si tal ejemplo existe, podría influir en cómo elijo los métodos apropiados en el futuro.
Para referencia, aquí hay algunos puntos de referencia sobre Pandas 0.19.2 / Python 3.6.0:
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
assert (df[0].mask(df[0] > 0.5, 1).values == np.where(df[0] > 0.5, 1, df[0])).all()
%timeit df[0].mask(df[0] > 0.5, 1) # 145 ms per loop
%timeit np.where(df[0] > 0.5, 1, df[0]) # 113 ms per loop
El rendimiento parece divergir más para no escalar valores:
%timeit df[0].mask(df[0] > 0.5, df[0]*2) # 338 ms per loop
%timeit np.where(df[0] > 0.5, df[0]*2, df[0]) # 153 ms per loop
1 answers
Estoy usando pandas 0.23.3 y Python 3.6, por lo que puedo ver una diferencia real en el tiempo de ejecución solo para su segundo ejemplo.
Pero vamos a investigar una versión ligeramente diferente de su segundo ejemplo (por lo que conseguimos2*df[0]
fuera del camino). Aquí está nuestra línea de base en mi máquina:
twice = df[0]*2
mask = df[0] > 0.5
%timeit np.where(mask, twice, df[0])
# 61.4 ms ± 1.51 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit df[0].mask(mask, twice)
# 143 ms ± 5.27 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
La versión de Numpy es aproximadamente 2,3 veces más rápida que la de los pandas.
Así que vamos a perfilar ambas funciones para ver la diferencia-perfilar es una buena manera de obtener el panorama general cuando uno no es muy familiarizado con la base de código: es más rápido que la depuración y menos propenso a errores que tratar de averiguar lo que está pasando con solo leer el código.
Estoy en Linux y uso perf
. Para la versión de numpy obtenemos (para la lista ver apéndice A):
>>> perf record python np_where.py
>>> perf report
Overhead Command Shared Object Symbol
68,50% python multiarray.cpython-36m-x86_64-linux-gnu.so [.] PyArray_Where
8,96% python [unknown] [k] 0xffffffff8140290c
1,57% python mtrand.cpython-36m-x86_64-linux-gnu.so [.] rk_random
Como podemos ver, la mayor parte del tiempo se gasta en PyArray_Where
- alrededor del 69%. El símbolo desconocido es una función del núcleo (de hecho clear_page
) - corro sin privilegios de root por lo que el símbolo no es resuelto.
Y para los pandas obtenemos (véase el Apéndice B para el código):
>>> perf record python pd_mask.py
>>> perf report
Overhead Command Shared Object Symbol
37,12% python interpreter.cpython-36m-x86_64-linux-gnu.so [.] vm_engine_iter_task
23,36% python libc-2.23.so [.] __memmove_ssse3_back
19,78% python [unknown] [k] 0xffffffff8140290c
3,32% python umath.cpython-36m-x86_64-linux-gnu.so [.] DOUBLE_isnan
1,48% python umath.cpython-36m-x86_64-linux-gnu.so [.] BOOL_logical_not
Una situación bastante diferente: {[28]]}
- pandas no usa
PyArray_Where
bajo el capó - el consumidor de tiempo más prominente esvm_engine_iter_task
, que es numexpr-funcionalidad. - hay un copiado de memoria pesado en curso -
__memmove_ssse3_back
utiliza aproximadamente25
% del tiempo! Probablemente algunas de las funciones del núcleo también están conectadas a los accesos a la memoria.
En realidad, pandas-0.19 utilizado PyArray_Where
bajo el capó, para la versión anterior el perf-report se vería como:
Overhead Command Shared Object Symbol
32,42% python multiarray.so [.] PyArray_Where
30,25% python libc-2.23.so [.] __memmove_ssse3_back
21,31% python [kernel.kallsyms] [k] clear_page
1,72% python [kernel.kallsyms] [k] __schedule
Así que básicamente usaría np.where
bajo el capó + algo de sobrecarga (todo lo anterior copia de datos, ver __memmove_ssse3_back
) en ese entonces.
No veo ningún escenario en el que los pandas puedan llegar a ser más rápidos que numpy en la versión 0.19 de pandas - solo agrega sobrecarga a la funcionalidad de numpy. La versión 0.23.3 de Pandas es una historia completamente diferente - aquí se utiliza numexpr-module, es muy posible que haya escenarios para los que la versión de pandas es (al menos ligeramente) más rápida.
No estoy seguro de que esta copia de memoria sea realmente necesaria/necesaria-tal vez uno incluso podría llamarlo error de rendimiento, pero simplemente no sé lo suficiente para estar seguro.
Podríamos ayudar a los pandas a no copiar, quitando algunas indirectas (pasando np.array
en lugar de pd.Series
). Por ejemplo:
%timeit df[0].mask(mask.values > 0.5, twice.values)
# 75.7 ms ± 1.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Ahora, pandas es solo un 25% más lento. El perf dice:
Overhead Command Shared Object Symbol
50,81% python interpreter.cpython-36m-x86_64-linux-gnu.so [.] vm_engine_iter_task
14,12% python [unknown] [k] 0xffffffff8140290c
9,93% python libc-2.23.so [.] __memmove_ssse3_back
4,61% python umath.cpython-36m-x86_64-linux-gnu.so [.] DOUBLE_isnan
2,01% python umath.cpython-36m-x86_64-linux-gnu.so [.] BOOL_logical_not
Mucho menos copia de datos, pero aún más que en el numpy versión que es principalmente responsable de la sobrecarga.
Mi clave toma-aways de ella:
Pandas tiene el potencial de ser al menos un poco más rápido que numpy (porque es posible ser más rápido). Sin embargo, el manejo algo opaco de la copia de datos por parte de pandas hace difícil predecir cuándo este potencial se ve eclipsado por la copia de datos (innecesaria).
Cuando el rendimiento de
where
/mask
es el cuello de botella, usaría numba / cython para mejorar el rendimiento-ver mi ingenuo intenta usar numba y cython más abajo.
La idea es tomar
np.where(df[0] > 0.5, df[0]*2, df[0])
Versión y para eliminar la necesidad de crear un temporal - es decir, df[0]*2
.
Como propuso @max9111, usando numba:
import numba as nb
@nb.njit
def nb_where(df):
n = len(df)
output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
assert(np.where(df[0] > 0.5, twice, df[0])==nb_where(df[0].values)).all()
%timeit np.where(df[0] > 0.5, df[0]*2, df[0])
# 85.1 ms ± 1.61 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit nb_where(df[0].values)
# 17.4 ms ± 673 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Que es aproximadamente factor 5 más rápido que la versión de numpy!
Y aquí está mi intento mucho menos exitoso de mejorar el rendimiento con la ayuda de Cython:
%%cython -a
cimport numpy as np
import numpy as np
cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_where(double[::1] df):
cdef int i
cdef int n = len(df)
cdef np.ndarray[np.float64_t] output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
assert (df[0].mask(df[0] > 0.5, 2*df[0]).values == cy_where(df[0].values)).all()
%timeit cy_where(df[0].values)
# 66.7± 753 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Da un 25% de aceleración. Ni claro, por qué Cython es mucho más lento que numba sin embargo.
Listados:
A: np_where.py:
import pandas as pd
import numpy as np
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
twice = df[0]*2
for _ in range(50):
np.where(df[0] > 0.5, twice, df[0])
B: pd_mask.py:
import pandas as pd
import numpy as np
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
twice = df[0]*2
mask = df[0] > 0.5
for _ in range(50):
df[0].mask(mask, twice)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-27 08:18:36