He visto muchas respuestas publicadas a preguntas sobre Stack Overflow relacionadas con el uso del método apply . También he visto a usuarios comentar debajo de ellos diciendo que "la apply es lenta y debe evitarse".
He leído muchos artículos sobre el tema del rendimiento que explican que la apply es lenta. También he visto un descargo de responsabilidad en los documentos sobre cómo apply es simplemente una función de conveniencia para pasar UDF (parece que no puedo encontrar eso ahora). Por lo tanto, el consenso general es que se debe evitar la apply si es posible. Sin embargo, esto plantea las siguientes preguntas:
apply es tan malo, ¿por qué está en la API?apply de forma gratuita?apply sea bueno (mejor que otras soluciones posibles)?apply , la función de conveniencia que nunca necesitóComenzamos abordando las preguntas en el OP, una por una.
"Si
applyes tan malo, ¿por qué está en la API?"
DataFrame.apply y Series.apply son funciones de conveniencia definidas en el objeto DataFrame y Series respectivamente. apply acepta cualquier función definida por el usuario que aplique una transformación/agregación en un DataFrame. apply es efectivamente una bala de plata que hace todo lo que cualquier función panda existente no puede hacer.
Algunas de las cosas que se apply pueden hacer:
axis=1 ) o en forma de columna ( axis=0 ) en un marco de datosagg o transform en estos casos)result_type )....Entre otros. Para obtener más información, consulte Aplicación de funciones por filas o columnas en la documentación.
Entonces, con todas estas características, ¿por qué la apply es mala? Es porque la apply es lenta . Pandas no hace suposiciones sobre la naturaleza de su función, por lo que aplica iterativamente su función a cada fila/columna según sea necesario. Además, el manejo de todas las situaciones anteriores significa que la apply incurre en una sobrecarga importante en cada iteración. Además, apply consume mucha más memoria, lo cual es un desafío para las aplicaciones con límites de memoria.
Hay muy pocas situaciones en las que apply es apropiado para usar (más sobre eso a continuación). Si no está seguro de si debería usar apply , probablemente no debería hacerlo.
Abordemos la siguiente pregunta.
"¿Cómo y cuándo debo hacer que mi código se
applyde forma gratuita?"
Para reformular, aquí hay algunas situaciones comunes en las que querrá deshacerse de las llamadas para apply .
Si está trabajando con datos numéricos, es probable que ya exista una función cython vectorizada que hace exactamente lo que está tratando de hacer (si no es así, haga una pregunta en Stack Overflow o abra una solicitud de función en GitHub).
Compare el rendimiento de apply para una operación de suma simple.
df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]}) df AB 0 9 12 1 4 7 2 2 5 3 1 4<!- ->
df.apply(np.sum) A 16 B 28 dtype: int64 df.sum() A 16 B 28 dtype: int64En cuanto al rendimiento, no hay comparación, el equivalente citonizado es mucho más rápido. No hay necesidad de un gráfico, porque la diferencia es obvia incluso para los datos de los juguetes.
%timeit df.apply(np.sum) %timeit df.sum() 2.22 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 471 µs ± 8.16 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) Incluso si habilita el paso de matrices sin formato con el argumento raw formato, sigue siendo el doble de lento.
%timeit df.apply(np.sum, raw=True) 840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)Otro ejemplo:
df.apply(lambda x: x.max() - x.min()) A 8 B 8 dtype: int64 df.max() - df.min() A 8 B 8 dtype: int64 %timeit df.apply(lambda x: x.max() - x.min()) %timeit df.max() - df.min() 2.43 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 1.23 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)En general, busque alternativas vectorizadas si es posible.
Pandas proporciona funciones de cadena "vectorizadas" en la mayoría de las situaciones, pero hay casos raros en los que esas funciones no ... "se aplican", por así decirlo.
Un problema común es verificar si un valor en una columna está presente en otra columna de la misma fila.
df = pd.DataFrame({ 'Name': ['mickey', 'donald', 'minnie'], 'Title': ['wonderland', "welcome to donald's castle", 'Minnie mouse clubhouse'], 'Value': [20, 10, 86]}) df Name Value Title 0 mickey 20 wonderland 1 donald 10 welcome to donald's castle 2 minnie 86 Minnie mouse clubhouseEsto debería devolver la fila segunda y tercera fila, ya que "donald" y "minnie" están presentes en sus respectivas columnas "Título".
Usando apply, esto se haría usando
df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1) 0 False 1 True 2 True dtype: bool df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)] Name Title Value 1 donald welcome to donald's castle 10 2 minnie Minnie mouse clubhouse 86Sin embargo, existe una mejor solución utilizando listas de comprensión.
df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]] Name Title Value 1 donald welcome to donald's castle 10 2 minnie Minnie mouse clubhouse 86<!- ->
%timeit df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)] %timeit df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]] 2.85 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 788 µs ± 16.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) Lo que hay que tener en cuenta aquí es que las rutinas iterativas resultan ser más rápidas que apply , debido a la menor sobrecarga. Si necesita manejar NaN y tipos de d no válidos, puede desarrollar esto usando una función personalizada que luego puede llamar con argumentos dentro de la lista de comprensión.
Para obtener más información sobre cuándo las listas de comprensión deben considerarse una buena opción, consulte mi artículo: ¿Son realmente malos los bucles for en pandas? ¿Cuándo debería importarme? .
Nota
Las operaciones de fecha y hora también tienen versiones vectorizadas. Entonces, por ejemplo, debería preferirpd.to_datetime(df['date']), por ejemplo,df['date'].apply(pd.to_datetime).Lea más en los documentos .
s = pd.Series([[1, 2]] * 3) s 0 [1, 2] 1 [1, 2] 2 [1, 2] dtype: object La gente se siente tentada a usar apply(pd.Series) . Esto es horrible en términos de rendimiento.
s.apply(pd.Series) 0 1 0 1 2 1 1 2 2 1 2Una mejor opción es enumerar la columna y pasarla a pd.DataFrame.
pd.DataFrame(s.tolist()) 0 1 0 1 2 1 1 2 2 1 2<!- ->
%timeit s.apply(pd.Series) %timeit pd.DataFrame(s.tolist()) 2.65 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 816 µs ± 40.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)Por último,
"¿Hay alguna situación en la que
applysea bueno?"
Aplicar es una función de conveniencia, por lo que hay situaciones en las que los gastos generales son lo suficientemente insignificantes como para perdonar. Realmente depende de cuántas veces se llame a la función.
Funciones que están vectorizadas para series, pero no DataFrames
¿Qué sucede si desea aplicar una operación de cadena en varias columnas? ¿Qué sucede si desea convertir varias columnas a fecha y hora? Estas funciones están vectorizadas solo para Series, por lo que deben aplicarse sobre cada columna en la que desea convertir/operar.
df = pd.DataFrame( pd.date_range('2018-12-31','2019-01-31', freq='2D').date.astype(str).reshape(-1, 2), columns=['date1', 'date2']) df date1 date2 0 2018-12-31 2019-01-02 1 2019-01-04 2019-01-06 2 2019-01-08 2019-01-10 3 2019-01-12 2019-01-14 4 2019-01-16 2019-01-18 5 2019-01-20 2019-01-22 6 2019-01-24 2019-01-26 7 2019-01-28 2019-01-30 df.dtypes date1 object date2 object dtype: object Este es un caso admisible para apply :
df.apply(pd.to_datetime, errors='coerce').dtypes date1 datetime64[ns] date2 datetime64[ns] dtype: object Tenga en cuenta que también tendría sentido stack , o simplemente usar un bucle explícito. Todas estas opciones son un poco más rápidas que usar apply , pero la diferencia es lo suficientemente pequeña como para perdonarla.
%timeit df.apply(pd.to_datetime, errors='coerce') %timeit pd.to_datetime(df.stack(), errors='coerce').unstack() %timeit pd.concat([pd.to_datetime(df[c], errors='coerce') for c in df], axis=1) %timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce') 5.49 ms ± 247 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 3.94 ms ± 48.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 3.16 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 2.41 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)Puede hacer un caso similar para otras operaciones, como operaciones de cadena o conversión a categoría.
u = df.apply(lambda x: x.str.contains(...)) v = df.apply(lambda x: x.astype(category))v/s
u = pd.concat([df[c].str.contains(...) for c in df], axis=1) v = df.copy() for c in df: v[c] = df[c].astype(category)Y así...
str : astype versus apply Esto parece una idiosincrasia de la API. El uso de apply para convertir enteros de una serie en una cadena es comparable (y, en ocasiones, más rápido) que el uso de astype .
El gráfico se trazó utilizando la biblioteca perfplot .
import perfplot perfplot.show( setup=lambda n: pd.Series(np.random.randint(0, n, n)), kernels=[ lambda s: s.astype(str), lambda s: s.apply(str) ], labels=['astype', 'apply'], n_range=[2**k for k in range(1, 20)], xlabel='N', logx=True, logy=True, equality_check=lambda x, y: (x == y).all()) Con flotantes, veo que el astype es consistentemente tan rápido o ligeramente más rápido que apply . Esto tiene que ver con el hecho de que los datos en la prueba son de tipo entero.
GroupBy con transformaciones encadenadas GroupBy.apply no se ha discutido hasta ahora, pero GroupBy.apply también es una función de conveniencia iterativa para manejar cualquier cosa que las funciones GroupBy existentes no manejen.
Un requisito común es realizar un GroupBy y luego dos operaciones principales, como un "cumsum retrasado":
df = pd.DataFrame({"A": list('aabcccddee'), "B": [12, 7, 5, 4, 5, 4, 3, 2, 1, 10]}) df AB 0 a 12 1 a 7 2 b 5 3 c 4 4 c 5 5 c 4 6 d 3 7 d 2 8 e 1 9 e 10<!- ->
Necesitaría dos llamadas grupales sucesivas aquí:
df.groupby('A').B.cumsum().groupby(df.A).shift() 0 NaN 1 12.0 2 NaN 3 NaN 4 4.0 5 9.0 6 NaN 7 3.0 8 NaN 9 1.0 Name: B, dtype: float64 Usando apply , puede acortar esto a una sola llamada.
df.groupby('A').B.apply(lambda x: x.cumsum().shift()) 0 NaN 1 12.0 2 NaN 3 NaN 4 4.0 5 9.0 6 NaN 7 3.0 8 NaN 9 1.0 Name: B, dtype: float64 Es muy difícil cuantificar el rendimiento porque depende de los datos. Pero, en general, apply es una solución aceptable si el objetivo es reducir una llamada groupby (porque groupby también es bastante costoso).
Además de las advertencias mencionadas anteriormente, también vale la pena mencionar que apply opera en la primera fila (o columna) dos veces. Esto se hace para determinar si la función tiene efectos secundarios. De lo contrario, apply puede usar una ruta rápida para evaluar el resultado; de lo contrario, volverá a una implementación lenta.
df = pd.DataFrame({ 'A': [1, 2], 'B': ['x', 'y'] }) def func(x): print(x['A']) return x df.apply(func, axis=1) # 1 # 1 # 2 AB 0 1 x 1 2 y Este comportamiento también se ve en GroupBy.apply en versiones de pandas <0.25 (se corrigió para 0.25, consulte aquí para obtener más información ).
apply son iguales El siguiente cuadro sugiere cuándo considerar apply 1 . Verde significa posiblemente eficiente; rojo evitar.
Algo de esto es intuitivo: pd.Series.apply es un bucle por filas a nivel de Python, lo mismo ocurre con pd.DataFrame.apply por filas ( axis=1 ). Los malos usos de estos son muchos y muy variados. El otro post trata de ellos con más profundidad. Las soluciones populares son utilizar métodos vectorizados, listas de comprensión (supone datos limpios) o herramientas eficientes como el constructor pd.DataFrame (por ejemplo, para evitar apply(pd.Series) ).
Si está utilizando pd.DataFrame.apply por filas, especificar raw=True (siempre que sea posible) suele ser beneficioso. En esta etapa, numba suele ser una mejor opción.
GroupBy.apply : generalmente favorecido La repetición de operaciones groupby para evitar apply perjudicará el rendimiento. GroupBy.apply generalmente está bien aquí, siempre que los métodos que usa en su función personalizada estén vectorizados. A veces, no existe un método nativo de Pandas para una agregación grupal que desee aplicar. En este caso, para un pequeño número de grupos, la apply con una función personalizada aún puede ofrecer un rendimiento razonable.
pd.DataFrame.apply columna-sabio: una bolsa mixta pd.DataFrame.apply columna-sabio ( axis=0 ) es un caso interesante. Para una pequeña cantidad de filas versus una gran cantidad de columnas, casi siempre es costoso. Para una gran cantidad de filas en relación con las columnas, el caso más común, a veces puede ver mejoras significativas en el rendimiento al usar apply :
# Python 3.7, Pandas 0.23.4 np.random.seed(0) df = pd.DataFrame(np.random.random((10**7, 3))) # Scenario_1, many rows df = pd.DataFrame(np.random.random((10**4, 10**3))) # Scenario_2, many columns # Scenario_1 | Scenario_2 %timeit df.sum() # 800 ms | 109 ms %timeit df.apply(pd.Series.sum) # 568 ms | 325 ms %timeit df.max() - df.min() # 1.63 s | 314 ms %timeit df.apply(lambda x: x.max() - x.min()) # 838 ms | 473 ms %timeit df.mean() # 108 ms | 94.4 ms %timeit df.apply(pd.Series.mean) # 276 ms | 233 ms1 Hay excepciones, pero generalmente son marginales o poco comunes. Un par de ejemplos:
df['col'].apply(str) puede superar ligeramente a df['col'].astype(str) .df.apply(pd.to_datetime) trabajando en cadenas no se escala bien con filas en comparación con un bucle for normal.Para axis=1 (es decir, funciones por fila), puede usar la siguiente función en lugar de apply . Me pregunto por qué este no es el comportamiento de los pandas . (No probado con índices compuestos, pero parece ser mucho más rápido que apply )
def faster_df_apply(df, func): cols = list(df.columns) data, index = [], [] for row in df.itertuples(index=True): row_dict = {f:v for f,v in zip(cols, row[1:])} data.append(func(row_dict)) index.append(row[0]) return pd.Series(data, index=index)