python - ¿Cuándo debería querer usar pandas apply() en mi código?
performance (4)
Todas las
apply
no son iguales
La siguiente tabla sugiere cuándo considerar
apply
1
.
Verde significa posiblemente eficiente;
rojo evitar
Parte
de esto es intuitivo:
pd.Series.apply
es un bucle a nivel de Python,
pd.DataFrame.apply
filas (
axis=1
).
Los usos indebidos de estos son muchos y de gran alcance.
El otro post trata con ellos en mayor profundidad.
Las soluciones populares son el uso de métodos vectorizados, listas de comprensión (asume datos limpios), o herramientas eficientes como el constructor
pd.DataFrame
(por ejemplo, para evitar
apply(pd.Series)
).
Si está utilizando
pd.DataFrame.apply
filas, a menudo es beneficioso especificar
raw=True
(cuando sea posible).
En esta etapa, la
numba
suele ser una mejor opción.
GroupBy.apply
: generalmente favorecido
Repetir
groupby
operaciones
groupby
para evitar
apply
dañará el rendimiento.
GroupBy.apply
generalmente está bien aquí, siempre que los métodos que utilice en su función personalizada sean vectorizados.
A veces no existe un método nativo de Pandas para una agregación grupal que desee aplicar.
En este caso, para una pequeña cantidad de grupos,
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 (
axis=0
) es un caso interesante.
Para un pequeño número de filas frente a un gran número de columnas, casi siempre es costoso.
Para un gran número 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 ms
1 Hay excepciones, pero generalmente son marginales o poco frecuentes. Un par de ejemplos:
-
df[''col''].apply(str)
puede superar ligeramente adf[''col''].astype(str)
. -
df.apply(pd.to_datetime)
trabaja en cadenas no sedf.apply(pd.to_datetime)
bien con las filas en comparación con un ciclo regularfor
.
Se trata de una QnA de respuesta automática que pretende informar a los usuarios sobre las dificultades y los beneficios de la aplicación.
He visto muchas respuestas publicadas en preguntas sobre Stack Overflow que involucran el uso de aplicar.
También he visto a usuarios que comentan debajo de ellos que dicen que "
apply
es lento", y debería evitarse ".
He leído muchos artículos sobre el tema del rendimiento que explican que
apply
es lento.
También he visto un descargo de responsabilidad en los documentos acerca de cómo
apply
es simplemente una función de conveniencia para pasar UDF (parece que no puede encontrar eso ahora).
Por lo tanto, el consenso general es que se debe evitar
apply
si es posible.
Sin embargo, esto plantea las siguientes preguntas:
-
Si
apply
es tan malo, ¿por qué está en la API? -
¿Cómo y cuándo debo hacer que mi código se
apply
-free? -
¿Hay alguna vez situaciones en las que
apply
sea bueno (mejor que otras soluciones posibles)?
apply
, la función de conveniencia que nunca necesitó
Comenzamos por abordar las preguntas en el OP, una por una.
" Si aplicar es tan malo, ¿por qué está en la API? "
DataFrame.apply
y
Series.apply
son
funciones de conveniencia
definidas en DataFrame y Series object 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 lo que cualquier función de pandas existente no puede hacer.
Algunas de las cosas que se
apply
pueden hacer:
- Ejecute cualquier función definida por el usuario en un DataFrame o Series
-
Aplique una función en filas (
axis=1
) o en columnas (axis=0
) en un DataFrame - Realizar la alineación del índice mientras se aplica la función.
-
Realice la agregación con funciones definidas por el usuario (sin embargo, en estos casos preferimos
agg
otransform
) - Realizar transformaciones de elementos
- Difundir los resultados agregados a las filas originales (ver el argumento tipo de resultado).
- Acepte los argumentos de posición / palabra clave para pasar a las funciones definidas por el usuario.
...Entre otros. Para obtener más información, consulte la aplicación de función Fila o columna en la documentación.
Entonces, con todas estas características, ¿por qué se
apply
mal?
Es
porque
apply
es
lento
.
Pandas no hace suposiciones sobre la naturaleza de su función, y así
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 algunos gastos generales importantes en cada iteración.
Además,
apply
consume mucha más memoria, lo que es un desafío para las aplicaciones con límite de memoria.
Hay muy pocas situaciones en las que
apply
es apropiado usar (más sobre esto más adelante).
Si no está seguro de si debería usar
apply
, probablemente no debería.
Vamos a abordar la siguiente pregunta.
" ¿Cómo y cuándo debo hacer que mi código se aplique -free? "
Para reformular, aquí hay algunas situaciones comunes en las que querrá
deshacerse
de cualquier llamada para hacer una
apply
.
Datos numéricos
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, haga una pregunta sobre el desbordamiento de pila o abra una solicitud de función en GitHub).
Contraste el rendimiento de
apply
para una operación de adición simple.
df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
df
A B
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: int64
En 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 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 habilitas pasar matrices en bruto con el argumento en
raw
, aún es 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.
Cuerda / Regex
Pandas proporciona funciones de cadena "vectorizadas" en la mayoría de las situaciones, pero hay casos raros donde esas funciones no ... "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 clubhouse
Esto 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 86
Sin 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 son más rápidas que las que se
apply
, debido a la menor sobrecarga.
Si necesita manejar NaNs y tipos de datos no válidos, puede aprovechar esto utilizando una función personalizada a la que puede llamar con argumentos dentro de la lista de comprensión.
Para obtener más información sobre cuándo debe considerarse una lista la comprensión de las listas, consulte mi artículo: Para bucles con pandas: ¿cuándo debería importarme? .
Nota
Las operaciones de fecha y fecha / hora también tienen versiones vectorizadas. Entonces, por ejemplo, deberías preferirpd.to_datetime(df[''date''])
, over, say,df[''date''].apply(pd.to_datetime)
.Lea más en los docs .
Un error común: explotar columnas de listas
s = pd.Series([[1, 2]] * 3)
s
0 [1, 2]
1 [1, 2]
2 [1, 2]
dtype: object
Las personas están tentadas 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 2
Una mejor opción es listificar 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 situaciones donde
apply
es 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 llama 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 datetime?
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 utilizar 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 perdonar.
%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 establecer 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í...
Conversión de series a
str
:
astype
versus
apply
Esto parece una idiosincrasia de la API.
El uso de
apply
para convertir enteros en una serie a una cadena es comparable (ya veces más rápido) que usar
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 los flotadores, veo que el
astype
es siempre tan rápido o un poco más rápido de lo que se
apply
.
Entonces, esto tiene que ver con el hecho de que los datos en la prueba son de tipo entero.
GroupBy
operaciones 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 de
GroupBy
existentes no
GroupBy
.
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
A B
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ías 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
, puedes 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).
Otras advertencias
Aparte 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 algún efecto secundario.
Si no,
apply
puede ser capaz de usar una ruta rápida para evaluar el resultado, de lo contrario, se reduce 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
A B
0 1 x
1 2 y
Este comportamiento también se ve en
GroupBy.apply
en versiones de pandas <0.25 (se ha corregido para 0.25,
consulte aquí para obtener más información
).
Me gustaría añadir mis dos centavos:
¿Alguna vez hay situaciones donde aplicar es bueno? Sí a veces.
Tarea: descifrar cadenas Unicode.
import numpy as np
import pandas as pd
import unidecode
s = pd.Series([''mañana'',''Ceñía''])
s.head()
0 mañana
1 Ceñía
s.apply(unidecode.unidecode)
0 manana
1 Cenia
Actualizar
De ninguna manera abogaba por el uso de
apply
, solo pensaba, ya que el
numpy
no puede lidiar con la situación anterior, podría haber sido un buen candidato para la
pandas apply
.
Pero me estaba olvidando de la simple comprensión de la lista gracias al recordatorio de @jpp.
Para
axis=1
(es decir, funciones de 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 de lo que se
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)