versiones guia español descargar actualizar python string performance pandas python-internals

python - guia - Conversión de una serie de pasos en cadenas: ¿por qué se aplica mucho más rápido que un astype?



qgis manual (2)

Comencemos con un poco de consejos generales: si está interesado en encontrar los cuellos de botella del código Python, puede usar un generador de perfiles para encontrar las funciones / partes que consumen la mayor parte del tiempo. En este caso, uso un perfilador de línea porque realmente puede ver la implementación y el tiempo empleado en cada línea.

Sin embargo, estas herramientas no funcionan con C o Cython de forma predeterminada. Dado que CPython (que es el intérprete de Python que estoy usando), NumPy y los pandas hacen un uso intensivo de C y Cython, habrá un límite de cuánto llegaré con la creación de perfiles.

En realidad: probablemente se podría extender el perfilado al código de Cython y probablemente también al código C al compilarlo con símbolos de depuración y seguimiento, sin embargo, no es una tarea fácil compilar estas bibliotecas, así que no haré eso (pero si a alguien le gusta hacerlo que la documentación de Cython incluye una página sobre cómo crear un perfil de código de Cython ).

Pero veamos hasta dónde puedo llegar:

Código de línea de Python código

Voy a usar line-profiler y un cuaderno de Jupyter aquí:

%load_ext line_profiler import numpy as np import pandas as pd x = pd.Series(np.random.randint(0, 100, 100000))

Perfil x.astype

%lprun -f x.astype x.astype(str)

Line # Hits Time Per Hit % Time Line Contents ============================================================== 87 @wraps(func) 88 def wrapper(*args, **kwargs): 89 1 12 12.0 0.0 old_arg_value = kwargs.pop(old_arg_name, None) 90 1 5 5.0 0.0 if old_arg_value is not None: 91 if mapping is not None: ... 118 1 663354 663354.0 100.0 return func(*args, **kwargs)

Así que eso es simplemente un decorador y el 100% del tiempo se gasta en la función decorada. Así que vamos a perfilar la función decorada:

%lprun -f x.astype.__wrapped__ x.astype(str)

Line # Hits Time Per Hit % Time Line Contents ============================================================== 3896 @deprecate_kwarg(old_arg_name=''raise_on_error'', new_arg_name=''errors'', 3897 mapping={True: ''raise'', False: ''ignore''}) 3898 def astype(self, dtype, copy=True, errors=''raise'', **kwargs): 3899 """ ... 3975 """ 3976 1 28 28.0 0.0 if is_dict_like(dtype): 3977 if self.ndim == 1: # i.e. Series ... 4001 4002 # else, only a single dtype is given 4003 1 14 14.0 0.0 new_data = self._data.astype(dtype=dtype, copy=copy, errors=errors, 4004 1 685863 685863.0 99.9 **kwargs) 4005 1 340 340.0 0.0 return self._constructor(new_data).__finalize__(self)

Source

Una vez más, una línea es el cuello de botella, así que _data.astype método _data.astype :

%lprun -f x._data.astype x.astype(str)

Line # Hits Time Per Hit % Time Line Contents ============================================================== 3461 def astype(self, dtype, **kwargs): 3462 1 695866 695866.0 100.0 return self.apply(''astype'', dtype=dtype, **kwargs)

Bien, otro delegado, veamos lo que _data.apply hace:

%lprun -f x._data.apply x.astype(str)

Line # Hits Time Per Hit % Time Line Contents ============================================================== 3251 def apply(self, f, axes=None, filter=None, do_integrity_check=False, 3252 consolidate=True, **kwargs): 3253 """ ... 3271 """ 3272 3273 1 12 12.0 0.0 result_blocks = [] ... 3309 3310 1 10 10.0 0.0 aligned_args = dict((k, kwargs[k]) 3311 1 29 29.0 0.0 for k in align_keys 3312 if hasattr(kwargs[k], ''reindex_axis'')) 3313 3314 2 28 14.0 0.0 for b in self.blocks: ... 3329 1 674974 674974.0 100.0 applied = getattr(b, f)(**kwargs) 3330 1 30 30.0 0.0 result_blocks = _extend_blocks(applied, result_blocks) 3331 3332 1 10 10.0 0.0 if len(result_blocks) == 0: 3333 return self.make_empty(axes or self.axes) 3334 1 10 10.0 0.0 bm = self.__class__(result_blocks, axes or self.axes, 3335 1 76 76.0 0.0 do_integrity_check=do_integrity_check) 3336 1 13 13.0 0.0 bm._consolidate_inplace() 3337 1 7 7.0 0.0 return bm

Source

Y de nuevo ... una llamada de función está tomando todo el tiempo, esta vez es x._data.blocks[0].astype :

%lprun -f x._data.blocks[0].astype x.astype(str)

Line # Hits Time Per Hit % Time Line Contents ============================================================== 542 def astype(self, dtype, copy=False, errors=''raise'', values=None, **kwargs): 543 1 18 18.0 0.0 return self._astype(dtype, copy=copy, errors=errors, values=values, 544 1 671092 671092.0 100.0 **kwargs)

.. que es otro delegado ...

%lprun -f x._data.blocks[0]._astype x.astype(str)

Line # Hits Time Per Hit % Time Line Contents ============================================================== 546 def _astype(self, dtype, copy=False, errors=''raise'', values=None, 547 klass=None, mgr=None, **kwargs): 548 """ ... 557 """ 558 1 11 11.0 0.0 errors_legal_values = (''raise'', ''ignore'') 559 560 1 8 8.0 0.0 if errors not in errors_legal_values: 561 invalid_arg = ("Expected value of kwarg ''errors'' to be one of {}. " 562 "Supplied value is ''{}''".format( 563 list(errors_legal_values), errors)) 564 raise ValueError(invalid_arg) 565 566 1 23 23.0 0.0 if inspect.isclass(dtype) and issubclass(dtype, ExtensionDtype): 567 msg = ("Expected an instance of {}, but got the class instead. " 568 "Try instantiating ''dtype''.".format(dtype.__name__)) 569 raise TypeError(msg) 570 571 # may need to convert to categorical 572 # this is only called for non-categoricals 573 1 72 72.0 0.0 if self.is_categorical_astype(dtype): ... 595 596 # astype processing 597 1 16 16.0 0.0 dtype = np.dtype(dtype) 598 1 19 19.0 0.0 if self.dtype == dtype: ... 603 1 8 8.0 0.0 if klass is None: 604 1 13 13.0 0.0 if dtype == np.object_: 605 klass = ObjectBlock 606 1 6 6.0 0.0 try: 607 # force the copy here 608 1 7 7.0 0.0 if values is None: 609 610 1 8 8.0 0.0 if issubclass(dtype.type, 611 1 14 14.0 0.0 (compat.text_type, compat.string_types)): 612 613 # use native type formatting for datetime/tz/timedelta 614 1 15 15.0 0.0 if self.is_datelike: 615 values = self.to_native_types() 616 617 # astype formatting 618 else: 619 1 8 8.0 0.0 values = self.values 620 621 else: 622 values = self.get_values(dtype=dtype) 623 624 # _astype_nansafe works fine with 1-d only 625 1 665777 665777.0 99.9 values = astype_nansafe(values.ravel(), dtype, copy=True) 626 1 32 32.0 0.0 values = values.reshape(self.shape) 627 628 1 17 17.0 0.0 newb = make_block(values, placement=self.mgr_locs, dtype=dtype, 629 1 269 269.0 0.0 klass=klass) 630 except: 631 if errors == ''raise'': 632 raise 633 newb = self.copy() if copy else self 634 635 1 8 8.0 0.0 if newb.is_numeric and self.is_numeric: ... 642 1 6 6.0 0.0 return newb

Source

... está bien, todavía no está allí. Vamos a ver astype_nansafe :

%lprun -f pd.core.internals.astype_nansafe x.astype(str)

Line # Hits Time Per Hit % Time Line Contents ============================================================== 640 def astype_nansafe(arr, dtype, copy=True): 641 """ return a view if copy is False, but 642 need to be very careful as the result shape could change! """ 643 1 13 13.0 0.0 if not isinstance(dtype, np.dtype): 644 dtype = pandas_dtype(dtype) 645 646 1 8 8.0 0.0 if issubclass(dtype.type, text_type): 647 # in Py3 that''s str, in Py2 that''s unicode 648 1 663317 663317.0 100.0 return lib.astype_unicode(arr.ravel()).reshape(arr.shape) ...

Source

Nuevamente, una es una línea que toma el 100%, así que voy a ir más allá:

%lprun -f pd.core.dtypes.cast.lib.astype_unicode x.astype(str) UserWarning: Could not extract a code object for the object <built-in function astype_unicode>

Bien, encontramos una built-in function , eso significa que es una función C. En este caso es una función de Cython. Pero significa que no podemos profundizar más con el perfilador de líneas. Así que voy a parar aquí por ahora.

Perfilado x.apply

%lprun -f x.apply x.apply(str)

Line # Hits Time Per Hit % Time Line Contents ============================================================== 2426 def apply(self, func, convert_dtype=True, args=(), **kwds): 2427 """ ... 2523 """ 2524 1 84 84.0 0.0 if len(self) == 0: 2525 return self._constructor(dtype=self.dtype, 2526 index=self.index).__finalize__(self) 2527 2528 # dispatch to agg 2529 1 11 11.0 0.0 if isinstance(func, (list, dict)): 2530 return self.aggregate(func, *args, **kwds) 2531 2532 # if we are a string, try to dispatch 2533 1 12 12.0 0.0 if isinstance(func, compat.string_types): 2534 return self._try_aggregate_string_function(func, *args, **kwds) 2535 2536 # handle ufuncs and lambdas 2537 1 7 7.0 0.0 if kwds or args and not isinstance(func, np.ufunc): 2538 f = lambda x: func(x, *args, **kwds) 2539 else: 2540 1 6 6.0 0.0 f = func 2541 2542 1 154 154.0 0.1 with np.errstate(all=''ignore''): 2543 1 11 11.0 0.0 if isinstance(f, np.ufunc): 2544 return f(self) 2545 2546 # row-wise access 2547 1 188 188.0 0.1 if is_extension_type(self.dtype): 2548 mapped = self._values.map(f) 2549 else: 2550 1 6238 6238.0 3.3 values = self.asobject 2551 1 181910 181910.0 95.5 mapped = lib.map_infer(values, f, convert=convert_dtype) 2552 2553 1 28 28.0 0.0 if len(mapped) and isinstance(mapped[0], Series): 2554 from pandas.core.frame import DataFrame 2555 return DataFrame(mapped.tolist(), index=self.index) 2556 else: 2557 1 19 19.0 0.0 return self._constructor(mapped, 2558 1 1870 1870.0 1.0 index=self.index).__finalize__(self)

Source

De nuevo, es una función que lleva la mayor parte del tiempo: lib.map_infer ...

%lprun -f pd.core.series.lib.map_infer x.apply(str)

Could not extract a code object for the object <built-in function map_infer>

Bueno, esa es otra función de Cython.

Esta vez hay otro colaborador (aunque menos significativo) con ~ 3%: values = self.asobject . Pero ignoraré esto por ahora, porque estamos interesados ​​en los principales contribuyentes.

Entrando en C / Cython

Las funciones llamadas por astype

Esta es la función astype_unicode :

cpdef ndarray[object] astype_unicode(ndarray arr): cdef: Py_ssize_t i, n = arr.size ndarray[object] result = np.empty(n, dtype=object) for i in range(n): # we can use the unsafe version because we know `result` is mutable # since it was created from `np.empty` util.set_value_at_unsafe(result, i, unicode(arr[i])) return result

Source

Esta función utiliza este ayudante:

cdef inline set_value_at_unsafe(ndarray arr, object loc, object value): cdef: Py_ssize_t i, sz if is_float_object(loc): casted = int(loc) if casted == loc: loc = casted i = <Py_ssize_t> loc sz = cnp.PyArray_SIZE(arr) if i < 0: i += sz elif i >= sz: raise IndexError(''index out of bounds'') assign_value_1d(arr, i, value)

Source

Que a su vez utiliza esta función C:

PANDAS_INLINE int assign_value_1d(PyArrayObject* ap, Py_ssize_t _i, PyObject* v) { npy_intp i = (npy_intp)_i; char* item = (char*)PyArray_DATA(ap) + i * PyArray_STRIDE(ap, 0); return PyArray_DESCR(ap)->f->setitem(v, item, ap); }

Source

Funciones llamadas por apply

Esta es la implementación de la función map_infer :

def map_infer(ndarray arr, object f, bint convert=1): cdef: Py_ssize_t i, n ndarray[object] result object val n = len(arr) result = np.empty(n, dtype=object) for i in range(n): val = f(util.get_value_at(arr, i)) # unbox 0-dim arrays, GH #690 if is_array(val) and PyArray_NDIM(val) == 0: # is there a faster way to unbox? val = val.item() result[i] = val if convert: return maybe_convert_objects(result, try_float=0, convert_datetime=0, convert_timedelta=0) return result

Source

Con este ayudante:

cdef inline object get_value_at(ndarray arr, object loc): cdef: Py_ssize_t i, sz int casted if is_float_object(loc): casted = int(loc) if casted == loc: loc = casted i = <Py_ssize_t> loc sz = cnp.PyArray_SIZE(arr) if i < 0 and sz > 0: i += sz elif i >= sz or sz == 0: raise IndexError(''index out of bounds'') return get_value_1d(arr, i)

Source

Que utiliza esta función C:

PANDAS_INLINE PyObject* get_value_1d(PyArrayObject* ap, Py_ssize_t i) { char* item = (char*)PyArray_DATA(ap) + i * PyArray_STRIDE(ap, 0); return PyArray_Scalar(item, PyArray_DESCR(ap), (PyObject*)ap); }

Source

Algunas reflexiones sobre el código Cython.

Hay algunas diferencias entre los códigos de Cython que se llaman eventualmente.

La tomada por astype usa unicode mientras que la ruta de apply usa la función pasada. Veamos si eso hace una diferencia (nuevamente, IPython / Jupyter hace que sea muy fácil compilar el código Cython):

%load_ext cython %%cython import numpy as np cimport numpy as np cpdef object func_called_by_astype(np.ndarray arr): cdef np.ndarray[object] ret = np.empty(arr.size, dtype=object) for i in range(arr.size): ret[i] = unicode(arr[i]) return ret cpdef object func_called_by_apply(np.ndarray arr, object f): cdef np.ndarray[object] ret = np.empty(arr.size, dtype=object) for i in range(arr.size): ret[i] = f(arr[i]) return ret

Sincronización:

import numpy as np arr = np.random.randint(0, 10000, 1000000) %timeit func_called_by_astype(arr) 514 ms ± 11.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit func_called_by_apply(arr, str) 632 ms ± 43.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Está bien, hay una diferencia pero está mal , en realidad indicaría que apply sería un poco más lento .

¿Pero recuerda la llamada de objeto que mencioné anteriormente en la función de apply ? ¿Podría ser esa la razón? Veamos:

import numpy as np arr = np.random.randint(0, 10000, 1000000) %timeit func_called_by_astype(arr) 557 ms ± 33.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit func_called_by_apply(arr.astype(object), str) 317 ms ± 13.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Ahora se ve mejor. La conversión a una matriz de objetos hizo que la función llamada aplicara mucho más rápido. Hay una razón simple para esto: str es una función de Python y generalmente son mucho más rápidas si ya tiene objetos de Python y NumPy (o Pandas) no necesita crear un contenedor de Python para el valor almacenado en la matriz (que es generalmente no es un objeto de Python, excepto cuando la matriz es de object dtype).

Sin embargo, eso no explica la gran diferencia que has visto. Mi sospecha es que en realidad hay una diferencia adicional en las formas en que se repiten las matrices y se establecen los elementos en el resultado. Muy probablemente el:

val = f(util.get_value_at(arr, i)) if is_array(val) and PyArray_NDIM(val) == 0: val = val.item() result[i] = val

parte de la función map_infer es más rápida que:

for i in range(n): # we can use the unsafe version because we know `result` is mutable # since it was created from `np.empty` util.set_value_at_unsafe(result, i, unicode(arr[i]))

que es llamado por el astype(str) . Los comentarios de la primera función parecen indicar que el escritor de map_infer realmente intentó hacer el código lo más rápido posible (vea el comentario sobre "¿hay una manera más rápida de desempaquetar?" Mientras que la otra tal vez fue escrita sin especial cuidado rendimiento, pero eso es sólo una conjetura.

También en mi computadora estoy bastante cerca del rendimiento de x.astype(str) y x.apply(str) ya:

import numpy as np arr = np.random.randint(0, 100, 1000000) s = pd.Series(arr) %timeit s.astype(str) 535 ms ± 23.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit func_called_by_astype(arr) 547 ms ± 21.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit s.apply(str) 216 ms ± 8.48 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit func_called_by_apply(arr.astype(object), str) 272 ms ± 12.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Tenga en cuenta que también he comprobado algunas otras variantes que devuelven un resultado diferente:

%timeit s.values.astype(str) # array of strings 407 ms ± 8.56 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit list(map(str, s.values.tolist())) # list of strings 184 ms ± 5.02 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Curiosamente, el bucle de Python con la list y el map parece ser el más rápido en mi computadora.

En realidad hice un pequeño punto de referencia que incluye la trama

import pandas as pd import simple_benchmark def Series_astype(series): return series.astype(str) def Series_apply(series): return series.apply(str) def Series_tolist_map(series): return list(map(str, series.values.tolist())) def Series_values_astype(series): return series.values.astype(str) arguments = {2**i: pd.Series(np.random.randint(0, 100, 2**i)) for i in range(2, 20)} b = simple_benchmark.benchmark( [Series_astype, Series_apply, Series_tolist_map, Series_values_astype], arguments, argument_name=''Series size'' ) %matplotlib notebook b.plot()

Tenga en cuenta que es un gráfico log-log debido a la amplia gama de tamaños que cubrí en el punto de referencia. Sin embargo, más bajo significa más rápido aquí.

Los resultados pueden ser diferentes para las diferentes versiones de Python / NumPy / Pandas. Así que si quieres compararlo, estas son mis versiones:

Versions -------- Python 3.6.5 NumPy 1.14.2 Pandas 0.22.0

Tengo un pandas.Series contienen enteros, pero necesito convertirlos en cadenas para algunas herramientas pandas.Series . Supongamos que tengo un objeto de la Series :

import numpy as np import pandas as pd x = pd.Series(np.random.randint(0, 100, 1000000))

En StackOverflow y otros sitios web, he visto a la mayoría de las personas argumentar que la mejor manera de hacerlo es:

%% timeit x = x.astype(str)

Esto lleva unos 2 segundos.

Cuando uso x = x.apply(str) , solo toma 0.2 segundos.

¿Por qué x.astype(str) tan lento? ¿Debería la forma recomendada ser x.apply(str) ?

Estoy principalmente interesado en el comportamiento de python 3 para esto.


Actuación

Vale la pena ver el rendimiento real antes de comenzar cualquier investigación, ya que, contrariamente a la opinión popular, la list(map(str, x)) parece ser más lenta que x.apply(str) .

import pandas as pd, numpy as np ### Versions: Pandas 0.20.3, Numpy 1.13.1, Python 3.6.2 ### x = pd.Series(np.random.randint(0, 100, 100000)) %timeit x.apply(str) # 42ms (1) %timeit x.map(str) # 42ms (2) %timeit x.astype(str) # 559ms (3) %timeit [str(i) for i in x] # 566ms (4) %timeit list(map(str, x)) # 536ms (5) %timeit x.values.astype(str) # 25ms (6)

Puntos a destacar:

  1. (5) es ligeramente más rápido que (3) / (4), lo cual esperamos a medida que se mueva más trabajo a C [suponiendo que no se utilice la función lambda ].
  2. (6) es, con mucho, el más rápido.
  3. (1) / (2) son similares.
  4. (3) / (4) son similares.

¿Por qué es rápido x.map / x.apply?

Esto parece ser porque usa el código Cython de compilación rápida:

cpdef ndarray[object] astype_str(ndarray arr): cdef: Py_ssize_t i, n = arr.size ndarray[object] result = np.empty(n, dtype=object) for i in range(n): # we can use the unsafe version because we know `result` is mutable # since it was created from `np.empty` util.set_value_at_unsafe(result, i, str(arr[i])) return result

¿Por qué x.astype (str) es lento?

Pandas aplica str a cada elemento de la serie, sin usar el Cython anterior.

Por lo tanto, el rendimiento es comparable a [str(i) for i in x] / list(map(str, x)) .

¿Por qué es tan rápido x.values.astype (str)?

Numpy no aplica una función en cada elemento de la matriz. Una descripción de esto encontré:

Si hizo s.values.astype(str) lo que obtiene es un objeto que contiene int . Este es el numpy realiza la conversión, mientras que los pandas iteran sobre cada elemento y llaman a str(item) en él. Entonces, si lo hace en un tipo de s.astype(str) , tiene un objeto que contiene str .

Hay una razón técnica por la que la versión numpy no se ha implementado en el caso de no nulos.