Estoy usando marcos de datos de Pandas y quiero crear una nueva columna en función de las columnas existentes. No he visto una buena discusión sobre la diferencia de velocidad entre df.apply()
y np.vectorize()
, así que pensé en preguntar aquí.
La apply()
función Pandas es lenta. Por lo que medí (que se muestra a continuación en algunos experimentos), usar np.vectorize()
es 25 veces más rápido (o más) que usar la función DataFrame apply()
, al menos en mi MacBook Pro 2016. ¿Es este un resultado esperado y por qué?
Por ejemplo, supongamos que tengo el siguiente marco de datos con N
filas:
N = 10
A_list = np.random.randint(1, 100, N)
B_list = np.random.randint(1, 100, N)
df = pd.DataFrame({'A': A_list, 'B': B_list})
df.head()
# A B
# 0 78 50
# 1 23 91
# 2 55 62
# 3 82 64
# 4 99 80
Supongamos además que quiero crear una nueva columna en función de las dos columnas A
y B
. En el siguiente ejemplo, usaré una función simple divide()
. Para aplicar la función, puedo usar df.apply()
o np.vectorize()
:
def divide(a, b):
if b == 0:
return 0.0
return float(a)/b
df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)
df['result2'] = np.vectorize(divide)(df['A'], df['B'])
df.head()
# A B result result2
# 0 78 50 1.560000 1.560000
# 1 23 91 0.252747 0.252747
# 2 55 62 0.887097 0.887097
# 3 82 64 1.281250 1.281250
# 4 99 80 1.237500 1.237500
Si aumento N
a tamaños del mundo real como 1 millón o más, entonces observo que np.vectorize()
es 25 veces más rápido o más que df.apply()
.
A continuación se muestra un código completo de evaluación comparativa:
import pandas as pd
import numpy as np
import time
def divide(a, b):
if b == 0:
return 0.0
return float(a)/b
for N in [1000, 10000, 100000, 1000000, 10000000]:
print ''
A_list = np.random.randint(1, 100, N)
B_list = np.random.randint(1, 100, N)
df = pd.DataFrame({'A': A_list, 'B': B_list})
start_epoch_sec = int(time.time())
df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)
end_epoch_sec = int(time.time())
result_apply = end_epoch_sec - start_epoch_sec
start_epoch_sec = int(time.time())
df['result2'] = np.vectorize(divide)(df['A'], df['B'])
end_epoch_sec = int(time.time())
result_vectorize = end_epoch_sec - start_epoch_sec
print 'N=%d, df.apply: %d sec, np.vectorize: %d sec' % \
(N, result_apply, result_vectorize)
# Make sure results from df.apply and np.vectorize match.
assert(df['result'].equals(df['result2']))
Los resultados se muestran a continuación:
N=1000, df.apply: 0 sec, np.vectorize: 0 sec
N=10000, df.apply: 1 sec, np.vectorize: 0 sec
N=100000, df.apply: 2 sec, np.vectorize: 0 sec
N=1000000, df.apply: 24 sec, np.vectorize: 1 sec
N=10000000, df.apply: 262 sec, np.vectorize: 4 sec
Si np.vectorize()
en general siempre es más rápido que df.apply()
, ¿por qué np.vectorize()
no se menciona más? Solo veo publicaciones de StackOverflow relacionadas con df.apply()
, como:
los pandas crean una nueva columna basada en valores de otras columnas
¿Cómo uso la función 'aplicar' de Pandas a varias columnas?
Cómo aplicar una función a dos columnas del marco de datos de Pandas
np.vectorize
es básicamente unfor
bucle de Python (es un método de conveniencia) yapply
con una lambda también está en tiempo de Pythonapply
fila por fila a menos que sea necesario, y obviamente una función vectorizada superará a una no vectorizada.np.vectorize
no está vectorizado. Es un nombre inapropiado bien conocido.str
. Son más lentos que las listas por comprensión en muchos casos. Asumimos demasiado.Respuestas:
Voy a empezar diciendo que el poder de pandas y NumPy matrices se deriva de alto rendimiento vectorizados cálculos sobre matrices numéricas. 1 El objetivo de los cálculos vectorizados es evitar bucles a nivel de Python moviendo los cálculos a un código C altamente optimizado y utilizando bloques de memoria contiguos. 2
Bucles a nivel de Python
Ahora podemos ver algunos tiempos. Estos son todos los bucles Python nivel que producen ya sea
pd.Series
,np.ndarray
olist
los objetos que contienen los mismos valores. A los efectos de la asignación a una serie dentro de un marco de datos, los resultados son comparables.# Python 3.6.5, NumPy 1.14.3, Pandas 0.23.0 np.random.seed(0) N = 10**5 %timeit list(map(divide, df['A'], df['B'])) # 43.9 ms %timeit np.vectorize(divide)(df['A'], df['B']) # 48.1 ms %timeit [divide(a, b) for a, b in zip(df['A'], df['B'])] # 49.4 ms %timeit [divide(a, b) for a, b in df[['A', 'B']].itertuples(index=False)] # 112 ms %timeit df.apply(lambda row: divide(*row), axis=1, raw=True) # 760 ms %timeit df.apply(lambda row: divide(row['A'], row['B']), axis=1) # 4.83 s %timeit [divide(row['A'], row['B']) for _, row in df[['A', 'B']].iterrows()] # 11.6 s
Algunas conclusiones:
tuple
métodos basados en-(los primeros 4) son un factor más eficiente que lospd.Series
métodos basados en-(los últimos 3).np.vectorize
, comprensión de listas +zip
ymap
métodos, es decir, los 3 primeros, todos tienen aproximadamente el mismo rendimiento. Esto se debe a que usantuple
y evitan algunas sobrecargas de Pandaspd.DataFrame.itertuples
.raw=True
conpd.DataFrame.apply
versus sin. Esta opción alimenta matrices NumPy a la función personalizada en lugar depd.Series
objetos.pd.DataFrame.apply
: solo otro buclePara ver exactamente los objetos por los que pasa Pandas, puede modificar su función de manera trivial:
def foo(row): print(type(row)) assert False # because you only need to see this once df.apply(lambda row: foo(row), axis=1)
Salida:
<class 'pandas.core.series.Series'>
. Crear, pasar y consultar un objeto de la serie Pandas conlleva una sobrecarga significativa en relación con las matrices NumPy. Esto no debería ser una sorpresa: la serie Pandas incluye una cantidad decente de andamios para contener un índice, valores, atributos, etc.Vuelve a hacer el mismo ejercicio con
raw=True
y verás<class 'numpy.ndarray'>
. Todo esto se describe en la documentación, pero verlo es más convincente.np.vectorize
: vectorización falsaLos documentos para
np.vectorize
tienen la siguiente nota:Las "reglas de transmisión" son irrelevantes aquí, ya que las matrices de entrada tienen las mismas dimensiones. El paralelo
map
es instructivo, ya que lamap
versión anterior tiene un rendimiento casi idéntico. El código fuente muestra lo que está sucediendo:np.vectorize
convierte su función de entrada en una función Universal ("ufunc") a través denp.frompyfunc
. Hay alguna optimización, por ejemplo, almacenamiento en caché, que puede conducir a una mejora en el rendimiento.En resumen,
np.vectorize
hace lo que debería hacer un bucle de nivel de Python , peropd.DataFrame.apply
agrega una sobrecarga considerable. No hay compilación JIT con la que veasnumba
(ver más abajo). Es solo una conveniencia .Verdadera vectorización: lo que debe usar
¿Por qué no se mencionan las diferencias anteriores en ninguna parte? Porque el rendimiento de cálculos verdaderamente vectorizados los hace irrelevantes:
%timeit np.where(df['B'] == 0, 0, df['A'] / df['B']) # 1.17 ms %timeit (df['A'] / df['B']).replace([np.inf, -np.inf], 0) # 1.96 ms
Sí, eso es ~ 40 veces más rápido que la más rápida de las soluciones descabelladas anteriores. Cualquiera de estos es aceptable. En mi opinión, el primero es conciso, legible y eficiente. Solo mire otros métodos, por ejemplo, a
numba
continuación, si el rendimiento es crítico y esto es parte de su cuello de botella.numba.njit
: mayor eficienciaCuando los bucles se consideran viables, generalmente se optimizan mediante
numba
matrices NumPy subyacentes para moverse tanto como sea posible a C.De hecho,
numba
mejora el rendimiento a microsegundos . Sin un trabajo engorroso, será difícil ser mucho más eficiente que esto.from numba import njit @njit def divide(a, b): res = np.empty(a.shape) for i in range(len(a)): if b[i] != 0: res[i] = a[i] / b[i] else: res[i] = 0 return res %timeit divide(df['A'].values, df['B'].values) # 717 µs
El uso
@njit(parallel=True)
puede proporcionar un impulso adicional para arreglos más grandes.1 tipos numéricos incluyen:
int
,float
,datetime
,bool
,category
. Se excluyenobject
dtype y pueden realizarse en bloques de memoria contiguos.2 Hay al menos 2 razones por las que las operaciones de NumPy son eficientes frente a Python:
fuente
parallel
argumento@njit(parallel=True)
me da una mejora adicional sobre solo@njit
. Quizás puedas agregar eso también.Cuanto más complejas se vuelvan sus funciones (es decir, cuanto menos se
numpy
puedan mover a sus propios componentes internos), más verá que el rendimiento no será tan diferente. Por ejemplo:name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=100000)) def parse_name(name): if name.lower().startswith('a'): return 'A' elif name.lower().startswith('e'): return 'E' elif name.lower().startswith('i'): return 'I' elif name.lower().startswith('o'): return 'O' elif name.lower().startswith('u'): return 'U' return name parse_name_vec = np.vectorize(parse_name)
Haciendo algunos tiempos:
Usando Aplicar
Resultados:
76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Utilizando
np.vectorize
Resultados:
77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Numpy intenta convertir las funciones de Python en numerosos
ufunc
objetos cuando llamasnp.vectorize
. Cómo hace esto, en realidad no lo sé: tendrías que indagar más en los aspectos internos de numpy de lo que estoy dispuesto a cajero automático. Dicho esto, parece hacer un mejor trabajo en funciones simplemente numéricas que esta función basada en cadenas aquí.Aumentando el tamaño hasta 1,000,000:
name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))
apply
Resultados:
769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
np.vectorize
Resultados:
794 ms ± 4.85 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Una forma mejor ( vectorizada ) con
np.select
:cases = [ name_series.str.lower().str.startswith('a'), name_series.str.lower().str.startswith('e'), name_series.str.lower().str.startswith('i'), name_series.str.lower().str.startswith('o'), name_series.str.lower().str.startswith('u') ] replacements = 'A E I O U'.split()
Tiempos:
Resultados:
67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
fuente
size=1000000
(1 millón)?Soy nuevo en Python. Pero en el siguiente ejemplo, 'aplicar' parece funcionar más rápido que 'vectorizar', o me falta algo.
import numpy as np import pandas as pd B = np.random.rand(1000,1000) fn = np.vectorize(lambda l: 1/(1-np.exp(-l))) print(fn(B)) B = pd.DataFrame(np.random.rand(1000,1000)) fn = lambda l: 1/(1-np.exp(-l)) print(B.apply(fn))
fuente