Estoy utilizando los 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é que lo pediría aquí.
La función de Pandas apply()
es lenta. Por lo que np.vectorize()
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 dataframe 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() # AB # 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() # AB 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 incremento N
a tamaños reales como 1 millón o más, observo que np.vectorize()
es 25 veces más rápido o más que df.apply()
.
A continuación hay un código completo de benchmarking:
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()
es en general siempre más rápido que df.apply()
, entonces ¿por qué no se menciona más np.vectorize()
? 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 de “aplicación” de Pandas en varias columnas?
Cómo aplicar una función a dos columnas del dataframe de Pandas
Comenzaré diciendo que el poder de las matrices Pandas y NumPy se deriva de cálculos vectorizados de alto rendimiento en matrices numéricas. 1 Todo el punto de los cálculos vectorizados es evitar los bucles de nivel de Python moviendo los cálculos a un código C altamente optimizado y utilizando bloques de memoria contiguos. 2
Ahora podemos ver algunos tiempos. A continuación se muestran todos los bucles de nivel de Python que producen pd.Series
, np.ndarray
o objetos de list
que contienen los mismos valores. A los efectos de la asignación a una serie dentro de un dataframe, 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
Algunos puntos para llevar:
tuple
(los primeros 4) son un factor más eficiente que los métodos basados en pd.Series
(los últimos 3). np.vectorize
, lista de comprensión + zip
y métodos de map
, es decir, los 3 primeros, todos tienen aproximadamente el mismo rendimiento. Esto se debe a que usan la tuple
y evitan algunos gastos generales de Pandas de pd.DataFrame.itertuples
. raw=True
con pd.DataFrame.apply
versus sin. Esta opción alimenta matrices NumPy a la función personalizada en lugar de objetos pd.Series
. pd.DataFrame.apply
: solo otro bucle Para ver exactamente los objetos por los que pasa Pandas, puede modificar su función de forma 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:
. Crear, pasar y consultar un objeto de la serie Pandas conlleva gastos generales significativos en relación con las matrices NumPy. Esto no debería ser una sorpresa: las series Pandas incluyen una cantidad decente de andamios para contener un índice, valores, atributos, etc.
Haga el mismo ejercicio de nuevo con raw=True
y verá
. Todo esto se describe en los documentos, pero verlo es más convincente.
np.vectorize
: vectorización falsa La documentación para np.vectorize
tiene la siguiente nota:
La función vectorizada evalúa
pyfunc
sobre tuplas sucesivas de las matrices de entrada como la función de mapa python, excepto que usa las reglas de difusión de números.
Las “reglas de transmisión” son irrelevantes aquí, ya que las matrices de entrada tienen las mismas dimensiones. El paralelo al map
es instructivo, ya que la versión del map
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 de np.frompyfunc
. Existe cierta optimización, por ejemplo, el almacenamiento en caché, que puede llevar a algunas mejoras en el rendimiento.
En resumen, np.vectorize
hace lo que debería hacer un bucle de nivel de Python, pero pd.DataFrame.apply
agrega una sobrecarga gruesa. No hay una comstackción JIT que se ve con numba
(ver más abajo). Es sólo una conveniencia .
¿Por qué las diferencias anteriores no se mencionan en ninguna parte? Debido a que el rendimiento de los 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 solución más rápida de las anteriores. Cualquiera de estos son aceptables. En mi opinión, el primero es sucinto, legible y eficiente. Solo observe otros métodos, por ejemplo, numba
continuación, si el desempeño es crítico y esto es parte de su cuello de botella.
numba.njit
: mayor eficiencia Cuando los bucles se consideran viables, generalmente se optimizan a través de numba
con matrices NumPy subyacentes para moverse lo más posible a C.
De hecho, numba
mejora el rendimiento a microsegundos . Sin un trabajo engorroso, será difícil obtener 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 de @njit(parallel=True)
puede proporcionar un impulso adicional para matrices más grandes.
1 Los tipos numéricos incluyen: int
, float
, datetime
, bool
, category
. Excluyen el tipo de object
y pueden mantenerse en bloques de memoria contiguos.
2 Existen al menos 2 razones por las que las operaciones NumPy son eficientes en comparación con Python:
Cuanto más complejas sean sus funciones (es decir, cuanto menos numpy
puedan moverse a sus propias numpy
internas), 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
%timeit name_series.apply(parse_name)
Resultados:
76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Usando np.vectorize
%timeit parse_name_vec(name_series)
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 objetos ufunc ufunc
cuando llama a np.vectorize
. Cómo lo hace, realmente no lo sé, tendría que profundizar más en los aspectos internos de lo que estoy dispuesto a hacer cajeros automáticos. Dicho esto, parece que hace un mejor trabajo en funciones simplemente numéricas que esta función basada en cadenas aquí.
Poniendo en marcha el tamaño hasta 1.000.000:
name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))
apply
%timeit name_series.apply(parse_name)
Resultados:
769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
np.vectorize
%timeit parse_name_vec(name_series)
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 = 'AEIO U'.split()
Tiempos:
%timeit np.select(cases, replacements, default=name_series)
Resultados:
67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)