tutorial index example create column python performance pandas numpy

python - index - ¿Por qué las funciones numpy son tan lentas en pandas series/dataframes?



pandas python tutorial (4)

Hay dos partes en la diferencia de rendimiento a tener en cuenta aquí:

  • Sobrecarga de Python en cada biblioteca (los pandas son de mucha ayuda)
  • Diferencia en la implementación del algoritmo numérico ( pd.clip realmente llama a np.where )

Ejecutar esto en una matriz muy pequeña debe demostrar la diferencia en la sobrecarga de Python. Para Numpy, es comprensible que sea muy pequeño, sin embargo, los pandas realizan muchas comprobaciones (valores nulos, procesamiento de argumentos más flexible, etc.) antes de llegar a la gran cantidad de crujidos. Intenté mostrar un desglose aproximado de las etapas por las que pasan los dos códigos antes de llegar al lecho rocoso del código C.

data = pd.Series(np.random.random(100))

Cuando se utiliza np.clip en un ndarray , la sobrecarga es simplemente la función numpy wrapper que llama al método del objeto:

>>> %timeit np.clip(data.values, 0.2, 0.8) # numpy wrapper, calls .clip() on the ndarray >>> %timeit data.values.clip(0.2, 0.8) # C function call 2.22 µs ± 125 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) 1.32 µs ± 20.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Los pandas gastan más tiempo en buscar casos extremos antes de llegar al algoritmo:

>>> %timeit np.clip(data, a_min=0.2, a_max=0.8) # numpy wrapper, calls .clip() on the Series >>> %timeit data.clip(lower=0.2, upper=0.8) # pandas API method >>> %timeit data._clip_with_scalar(0.2, 0.8) # lowest level python function 102 µs ± 1.54 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) 90.4 µs ± 1.01 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) 73.7 µs ± 805 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

En relación con el tiempo total, la sobrecarga de ambas bibliotecas antes de tocar el código C es bastante significativa. Para numpy, la instrucción de envoltura única toma tanto tiempo para ejecutarse como el procesamiento numérico. Pandas tiene ~ 30x más de sobrecarga justo en las primeras dos capas de llamadas a funciones.

Para aislar lo que está sucediendo a nivel de algoritmo, debemos verificar esto en una matriz más grande y comparar las mismas funciones:

>>> data = pd.Series(np.random.random(1000000)) >>> %timeit np.clip(data.values, 0.2, 0.8) >>> %timeit data.values.clip(0.2, 0.8) 2.85 ms ± 37.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 2.85 ms ± 15.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) >>> %timeit np.clip(data, a_min=0.2, a_max=0.8) >>> %timeit data.clip(lower=0.2, upper=0.8) >>> %timeit data._clip_with_scalar(0.2, 0.8) 12.3 ms ± 135 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 12.3 ms ± 115 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 12.2 ms ± 76.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

La sobrecarga python en ambos casos es ahora insignificante; el tiempo para las funciones de contenedor y la comprobación de argumentos es pequeño en relación con el tiempo de cálculo en 1 millón de valores. Sin embargo, hay una diferencia de velocidad de 3-4x que se puede atribuir a la implementación numérica. Al investigar un poco en el código fuente, vemos que la implementación de pandas de clip realmente usa np.where , no np.clip :

def clip_where(data, lower, upper): '''''' Actual implementation in pd.Series._clip_with_scalar (minus NaN handling). '''''' result = data.values result = np.where(result >= upper, upper, result) result = np.where(result <= lower, lower, result) return pd.Series(result) def clip_clip(data, lower, upper): '''''' What would happen if we used ndarray.clip instead. '''''' return pd.Series(data.values.clip(lower, upper))

El esfuerzo adicional requerido para verificar cada condición booleana por separado antes de hacer una sustitución condicional parece dar cuenta de la diferencia de velocidad. Especificar tanto la parte upper como la lower daría como resultado 4 pasadas a través de la matriz numpy (dos controles de desigualdad y dos llamadas a np.where ). La comparación de estas dos funciones muestra que la relación de velocidad 3-4x:

>>> %timeit clip_clip(data, lower=0.2, upper=0.8) >>> %timeit clip_where(data, lower=0.2, upper=0.8) 11.1 ms ± 101 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 2.97 ms ± 76.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

No estoy seguro de por qué los desarrolladores de pandas fueron con esta implementación. np.clip puede ser una función API más nueva que anteriormente requería una solución alternativa. También hay un poco más de lo que he explicado aquí, ya que los pandas verifican varios casos antes de ejecutar el algoritmo final, y esta es solo una de las implementaciones que se pueden llamar.

Considere un MWE pequeño, tomado de otra pregunta :

DateTime Data 2017-11-21 18:54:31 1 2017-11-22 02:26:48 2 2017-11-22 10:19:44 3 2017-11-22 15:11:28 6 2017-11-22 23:21:58 7 2017-11-28 14:28:28 28 2017-11-28 14:36:40 0 2017-11-28 14:59:48 1

El objetivo es recortar todos los valores con un límite superior de 1. Mi respuesta usa np.clip , que funciona bien.

np.clip(df.Data, a_min=None, a_max=1) array([1, 1, 1, 1, 1, 1, 0, 1])

O,

np.clip(df.Data.values, a_min=None, a_max=1) array([1, 1, 1, 1, 1, 1, 0, 1])

Ambos devuelven la misma respuesta. Mi pregunta es sobre el rendimiento relativo de estos dos métodos. Considerar -

df = pd.concat([df]*1000).reset_index(drop=True) %timeit np.clip(df.Data, a_min=None, a_max=1) 1000 loops, best of 3: 270 µs per loop %timeit np.clip(df.Data.values, a_min=None, a_max=1) 10000 loops, best of 3: 23.4 µs per loop

¿Por qué hay una diferencia tan grande entre los dos, simplemente llamando values a este último? En otras palabras...

¿Por qué las funciones numpy son tan lentas en los pandas?


La razón por la que el rendimiento es diferente es porque numpy primero tiende a buscar la implementación de pandas de la función utilizando getattr que haciendo lo mismo en las funciones numpy incorporadas cuando se pasa un objeto pandas.

No es el numpy sobre el objeto pandas que es lento, es la versión de los pandas.

Cuando tu lo hagas

np.clip(pd.Series([1,2,3,4,5]),a_min=None,amax=1)

_wrapfunc se llama:

# Code from source def _wrapfunc(obj, method, *args, **kwds): try: return getattr(obj, method)(*args, **kwds)

Debido al método getattr :

getattr(pd.Series([1,2,3,4,5]),''clip'')(None, 1) # Equivalent to `pd.Series([1,2,3,4,5]).clip(lower=None,upper=1)` # 0 1 # 1 1 # 2 1 # 3 1 # 4 1 # dtype: int64

Si pasas por la implementación de pandas, hay mucho trabajo de verificación previa realizado. Es la razón por la cual las funciones que tienen la implementación de pandas a través de numpy tienen tanta diferencia en la velocidad.

No solo clip, funciones como cumsum , cumprod , reshape , searchsorted , transpose y mucho más utiliza la versión panda de ellos que numpy cuando les pasas un objeto pandas.

Puede parecer molesto hacer el trabajo sobre esos objetos, pero bajo la capucha es la función de los pandas.


Sí, parece que np.clip es mucho más lento en pandas.Series que en numpy.ndarray s. Eso es correcto, pero en realidad (al menos asintomáticamente) no es tan malo. 8000 elementos aún se encuentran en el régimen, donde los factores constantes son los principales contribuyentes en el tiempo de ejecución. Creo que este es un aspecto muy importante de la pregunta, así que estoy visualizando esto (tomando prestado de otra respuesta ):

# Setup import pandas as pd import numpy as np def on_series(s): return np.clip(s, a_min=None, a_max=1) def on_values_of_series(s): return np.clip(s.values, a_min=None, a_max=1) # Timing setup timings = {on_series: [], on_values_of_series: []} sizes = [2**i for i in range(1, 26, 2)] # Timing for size in sizes: func_input = pd.Series(np.random.randint(0, 30, size=size)) for func in timings: res = %timeit -o func(func_input) timings[func].append(res) %matplotlib notebook import matplotlib.pyplot as plt import numpy as np fig, (ax1, ax2) = plt.subplots(1, 2) for func in timings: ax1.plot(sizes, [time.best for time in timings[func]], label=str(func.__name__)) ax1.set_xscale(''log'') ax1.set_yscale(''log'') ax1.set_xlabel(''size'') ax1.set_ylabel(''time [seconds]'') ax1.grid(which=''both'') ax1.legend() baseline = on_values_of_series # choose one function as baseline for func in timings: ax2.plot(sizes, [time.best / ref.best for time, ref in zip(timings[func], timings[baseline])], label=str(func.__name__)) ax2.set_yscale(''log'') ax2.set_xscale(''log'') ax2.set_xlabel(''size'') ax2.set_ylabel(''time relative to {}''.format(baseline.__name__)) ax2.grid(which=''both'') ax2.legend() plt.tight_layout()

Es un diagrama de registro y registro porque creo que esto muestra las características más importantes más claramente. Por ejemplo, muestra que np.clip en numpy.ndarray es más rápido, pero también tiene un factor constante mucho más pequeño en ese caso. ¡La diferencia para las matrices grandes es solo ~ 3! Esa sigue siendo una gran diferencia, pero mucho menos que la diferencia en arreglos pequeños.

Sin embargo, todavía no es una respuesta a la pregunta de dónde viene la diferencia de tiempo.

La solución es bastante simple: np.clip delega en el método de clip del primer argumento:

>>> np.clip?? Source: def clip(a, a_min, a_max, out=None): """ ... """ return _wrapfunc(a, ''clip'', a_min, a_max, out=out) >>> np.core.fromnumeric._wrapfunc?? Source: def _wrapfunc(obj, method, *args, **kwds): try: return getattr(obj, method)(*args, **kwds) # ... except (AttributeError, TypeError): return _wrapit(obj, method, *args, **kwds)

La línea getattr de la función _wrapfunc es la línea importante aquí, porque np.ndarray.clip y pd.Series.clip son métodos diferentes, sí, métodos completamente diferentes :

>>> np.ndarray.clip <method ''clip'' of ''numpy.ndarray'' objects> >>> pd.Series.clip <function pandas.core.generic.NDFrame.clip>

Desafortunadamente, np.ndarray.clip es una función C, por lo que es difícil pd.Series.clip un perfil, sin embargo, pd.Series.clip es una función normal de Python, por lo que es fácil pd.Series.clip un perfil. Usemos una serie de enteros 5000 aquí:

s = pd.Series(np.random.randint(0, 100, 5000))

Para np.clip en los values obtengo el siguiente perfil de línea:

%load_ext line_profiler %lprun -f np.clip -f np.core.fromnumeric._wrapfunc np.clip(s.values, a_min=None, a_max=1) Timer unit: 4.10256e-07 s Total time: 2.25641e-05 s File: numpy/core/fromnumeric.py Function: clip at line 1673 Line # Hits Time Per Hit % Time Line Contents ============================================================== 1673 def clip(a, a_min, a_max, out=None): 1674 """ ... 1726 """ 1727 1 55 55.0 100.0 return _wrapfunc(a, ''clip'', a_min, a_max, out=out) Total time: 1.51795e-05 s File: numpy/core/fromnumeric.py Function: _wrapfunc at line 55 Line # Hits Time Per Hit % Time Line Contents ============================================================== 55 def _wrapfunc(obj, method, *args, **kwds): 56 1 2 2.0 5.4 try: 57 1 35 35.0 94.6 return getattr(obj, method)(*args, **kwds) 58 59 # An AttributeError occurs if the object does not have 60 # such a method in its class. 61 62 # A TypeError occurs if the object does have such a method 63 # in its class, but its signature is not identical to that 64 # of NumPy''s. This situation has occurred in the case of 65 # a downstream library like ''pandas''. 66 except (AttributeError, TypeError): 67 return _wrapit(obj, method, *args, **kwds)

Pero para np.clip en la Series obtengo un resultado de perfil totalmente diferente:

%lprun -f np.clip -f np.core.fromnumeric._wrapfunc -f pd.Series.clip -f pd.Series._clip_with_scalar np.clip(s, a_min=None, a_max=1) Timer unit: 4.10256e-07 s Total time: 0.000823794 s File: numpy/core/fromnumeric.py Function: clip at line 1673 Line # Hits Time Per Hit % Time Line Contents ============================================================== 1673 def clip(a, a_min, a_max, out=None): 1674 """ ... 1726 """ 1727 1 2008 2008.0 100.0 return _wrapfunc(a, ''clip'', a_min, a_max, out=out) Total time: 0.00081846 s File: numpy/core/fromnumeric.py Function: _wrapfunc at line 55 Line # Hits Time Per Hit % Time Line Contents ============================================================== 55 def _wrapfunc(obj, method, *args, **kwds): 56 1 2 2.0 0.1 try: 57 1 1993 1993.0 99.9 return getattr(obj, method)(*args, **kwds) 58 59 # An AttributeError occurs if the object does not have 60 # such a method in its class. 61 62 # A TypeError occurs if the object does have such a method 63 # in its class, but its signature is not identical to that 64 # of NumPy''s. This situation has occurred in the case of 65 # a downstream library like ''pandas''. 66 except (AttributeError, TypeError): 67 return _wrapit(obj, method, *args, **kwds) Total time: 0.000804922 s File: pandas/core/generic.py Function: clip at line 4969 Line # Hits Time Per Hit % Time Line Contents ============================================================== 4969 def clip(self, lower=None, upper=None, axis=None, inplace=False, 4970 *args, **kwargs): 4971 """ ... 5021 """ 5022 1 12 12.0 0.6 if isinstance(self, ABCPanel): 5023 raise NotImplementedError("clip is not supported yet for panels") 5024 5025 1 10 10.0 0.5 inplace = validate_bool_kwarg(inplace, ''inplace'') 5026 5027 1 69 69.0 3.5 axis = nv.validate_clip_with_axis(axis, args, kwargs) 5028 5029 # GH 17276 5030 # numpy doesn''t like NaN as a clip value 5031 # so ignore 5032 1 158 158.0 8.1 if np.any(pd.isnull(lower)): 5033 1 3 3.0 0.2 lower = None 5034 1 26 26.0 1.3 if np.any(pd.isnull(upper)): 5035 upper = None 5036 5037 # GH 2747 (arguments were reversed) 5038 1 1 1.0 0.1 if lower is not None and upper is not None: 5039 if is_scalar(lower) and is_scalar(upper): 5040 lower, upper = min(lower, upper), max(lower, upper) 5041 5042 # fast-path for scalars 5043 1 1 1.0 0.1 if ((lower is None or (is_scalar(lower) and is_number(lower))) and 5044 1 28 28.0 1.4 (upper is None or (is_scalar(upper) and is_number(upper)))): 5045 1 1654 1654.0 84.3 return self._clip_with_scalar(lower, upper, inplace=inplace) 5046 5047 result = self 5048 if lower is not None: 5049 result = result.clip_lower(lower, axis, inplace=inplace) 5050 if upper is not None: 5051 if inplace: 5052 result = self 5053 result = result.clip_upper(upper, axis, inplace=inplace) 5054 5055 return result Total time: 0.000662153 s File: pandas/core/generic.py Function: _clip_with_scalar at line 4920 Line # Hits Time Per Hit % Time Line Contents ============================================================== 4920 def _clip_with_scalar(self, lower, upper, inplace=False): 4921 1 2 2.0 0.1 if ((lower is not None and np.any(isna(lower))) or 4922 1 25 25.0 1.5 (upper is not None and np.any(isna(upper)))): 4923 raise ValueError("Cannot use an NA value as a clip threshold") 4924 4925 1 22 22.0 1.4 result = self.values 4926 1 571 571.0 35.4 mask = isna(result) 4927 4928 1 95 95.0 5.9 with np.errstate(all=''ignore''): 4929 1 1 1.0 0.1 if upper is not None: 4930 1 141 141.0 8.7 result = np.where(result >= upper, upper, result) 4931 1 33 33.0 2.0 if lower is not None: 4932 result = np.where(result <= lower, lower, result) 4933 1 73 73.0 4.5 if np.any(mask): 4934 result[mask] = np.nan 4935 4936 1 90 90.0 5.6 axes_dict = self._construct_axes_dict() 4937 1 558 558.0 34.6 result = self._constructor(result, **axes_dict).__finalize__(self) 4938 4939 1 2 2.0 0.1 if inplace: 4940 self._update_inplace(result) 4941 else: 4942 1 1 1.0 0.1 return result

Dejé de ir a las subrutinas en ese punto porque ya destaca dónde el pd.Series.clip hace mucho más trabajo que el np.ndarray.clip . Simplemente compare el tiempo total de la llamada np.clip en los values (55 unidades de temporizador) a uno de los primeros controles en el método pandas.Series.clip , el if np.any(pd.isnull(lower)) (158 unidades). En ese momento, el método de los pandas ni siquiera comenzó en el clipping y ya lleva 3 veces más.

Sin embargo, varios de estos "gastos generales" se vuelven insignificantes cuando el conjunto es grande:

s = pd.Series(np.random.randint(0, 100, 1000000)) %lprun -f np.clip -f np.core.fromnumeric._wrapfunc -f pd.Series.clip -f pd.Series._clip_with_scalar np.clip(s, a_min=None, a_max=1) Timer unit: 4.10256e-07 s Total time: 0.00593476 s File: numpy/core/fromnumeric.py Function: clip at line 1673 Line # Hits Time Per Hit % Time Line Contents ============================================================== 1673 def clip(a, a_min, a_max, out=None): 1674 """ ... 1726 """ 1727 1 14466 14466.0 100.0 return _wrapfunc(a, ''clip'', a_min, a_max, out=out) Total time: 0.00592779 s File: numpy/core/fromnumeric.py Function: _wrapfunc at line 55 Line # Hits Time Per Hit % Time Line Contents ============================================================== 55 def _wrapfunc(obj, method, *args, **kwds): 56 1 1 1.0 0.0 try: 57 1 14448 14448.0 100.0 return getattr(obj, method)(*args, **kwds) 58 59 # An AttributeError occurs if the object does not have 60 # such a method in its class. 61 62 # A TypeError occurs if the object does have such a method 63 # in its class, but its signature is not identical to that 64 # of NumPy''s. This situation has occurred in the case of 65 # a downstream library like ''pandas''. 66 except (AttributeError, TypeError): 67 return _wrapit(obj, method, *args, **kwds) Total time: 0.00591302 s File: pandas/core/generic.py Function: clip at line 4969 Line # Hits Time Per Hit % Time Line Contents ============================================================== 4969 def clip(self, lower=None, upper=None, axis=None, inplace=False, 4970 *args, **kwargs): 4971 """ ... 5021 """ 5022 1 17 17.0 0.1 if isinstance(self, ABCPanel): 5023 raise NotImplementedError("clip is not supported yet for panels") 5024 5025 1 14 14.0 0.1 inplace = validate_bool_kwarg(inplace, ''inplace'') 5026 5027 1 97 97.0 0.7 axis = nv.validate_clip_with_axis(axis, args, kwargs) 5028 5029 # GH 17276 5030 # numpy doesn''t like NaN as a clip value 5031 # so ignore 5032 1 125 125.0 0.9 if np.any(pd.isnull(lower)): 5033 1 2 2.0 0.0 lower = None 5034 1 30 30.0 0.2 if np.any(pd.isnull(upper)): 5035 upper = None 5036 5037 # GH 2747 (arguments were reversed) 5038 1 2 2.0 0.0 if lower is not None and upper is not None: 5039 if is_scalar(lower) and is_scalar(upper): 5040 lower, upper = min(lower, upper), max(lower, upper) 5041 5042 # fast-path for scalars 5043 1 2 2.0 0.0 if ((lower is None or (is_scalar(lower) and is_number(lower))) and 5044 1 32 32.0 0.2 (upper is None or (is_scalar(upper) and is_number(upper)))): 5045 1 14092 14092.0 97.8 return self._clip_with_scalar(lower, upper, inplace=inplace) 5046 5047 result = self 5048 if lower is not None: 5049 result = result.clip_lower(lower, axis, inplace=inplace) 5050 if upper is not None: 5051 if inplace: 5052 result = self 5053 result = result.clip_upper(upper, axis, inplace=inplace) 5054 5055 return result Total time: 0.00575753 s File: pandas/core/generic.py Function: _clip_with_scalar at line 4920 Line # Hits Time Per Hit % Time Line Contents ============================================================== 4920 def _clip_with_scalar(self, lower, upper, inplace=False): 4921 1 2 2.0 0.0 if ((lower is not None and np.any(isna(lower))) or 4922 1 28 28.0 0.2 (upper is not None and np.any(isna(upper)))): 4923 raise ValueError("Cannot use an NA value as a clip threshold") 4924 4925 1 120 120.0 0.9 result = self.values 4926 1 3525 3525.0 25.1 mask = isna(result) 4927 4928 1 86 86.0 0.6 with np.errstate(all=''ignore''): 4929 1 2 2.0 0.0 if upper is not None: 4930 1 9314 9314.0 66.4 result = np.where(result >= upper, upper, result) 4931 1 61 61.0 0.4 if lower is not None: 4932 result = np.where(result <= lower, lower, result) 4933 1 283 283.0 2.0 if np.any(mask): 4934 result[mask] = np.nan 4935 4936 1 78 78.0 0.6 axes_dict = self._construct_axes_dict() 4937 1 532 532.0 3.8 result = self._constructor(result, **axes_dict).__finalize__(self) 4938 4939 1 2 2.0 0.0 if inplace: 4940 self._update_inplace(result) 4941 else: 4942 1 1 1.0 0.0 return result

Todavía hay múltiples llamadas a funciones, por ejemplo, isna y np.where , que toman una cantidad de tiempo significativa, pero en general esto es al menos comparable al tiempo np.ndarray.clip (eso está en el régimen donde la diferencia de tiempo es ~ 3 en mi computadora).

La comida para llevar debería ser probablemente:

  • Muchas funciones de NumPy simplemente delegan en un método del objeto pasado, por lo que puede haber grandes diferencias cuando pasas objetos diferentes.
  • El perfilado, especialmente el perfil de línea, puede ser una gran herramienta para encontrar los lugares de donde proviene la diferencia de rendimiento.
  • Siempre asegúrese de probar objetos de diferentes tamaños en tales casos. Podría estar comparando factores constantes que probablemente no importen, excepto si procesa muchas matrices pequeñas.

Versiones usadas:

Python 3.6.3 64-bit on Windows 10 Numpy 1.13.3 Pandas 0.21.1


Solo lee el código fuente, está claro.

def clip(a, a_min, a_max, out=None): """a : array_like Array containing elements to clip.""" return _wrapfunc(a, ''clip'', a_min, a_max, out=out) def _wrapfunc(obj, method, *args, **kwds): try: return getattr(obj, method)(*args, **kwds) #This situation has occurred in the case of # a downstream library like ''pandas''. except (AttributeError, TypeError): return _wrapit(obj, method, *args, **kwds) def _wrapit(obj, method, *args, **kwds): try: wrap = obj.__array_wrap__ except AttributeError: wrap = None result = getattr(asarray(obj), method)(*args, **kwds) if wrap: if not isinstance(result, mu.ndarray): result = asarray(result) result = wrap(result) return result

rectificar:

después de pandas v0.13.0_ahl1, pandas tiene su propio implemento de clip .