¿Cómo puedo utilizar la función apply() para una sola columna?
Tengo un marco de datos de pandas con varias columnas. Quiero cambiar los valores solo de la primera columna sin afectar las otras columnas. ¿ Cómo puedo hacer eso usando apply()
pandas?
Dado un marco de datos de muestra df
como:
a b
0 1 2
1 2 3
2 3 4
3 4 5
lo que quieres es:
df['a'] = df['a'].apply(lambda x: x + 1)
que devuelve:
a b
0 2 2
1 3 3
2 4 4
3 5 5
Para una sola columna es mejor usar map()
, así:
df = pd.DataFrame([{'a': 15, 'b': 15, 'c': 5}, {'a': 20, 'b': 10, 'c': 7}, {'a': 25, 'b': 30, 'c': 9}])
a b c
0 15 15 5
1 20 10 7
2 25 30 9
df['a'] = df['a'].map(lambda a: a / 2.)
a b c
0 7.5 15 5
1 10.0 10 7
2 12.5 30 9
Dado el siguiente marco de datos df
y la función complex_function
,
import pandas as pd
def complex_function(x, y=0):
if x > 5 and x > y:
return 1
else:
return 2
df = pd.DataFrame(data={'col1': [1, 4, 6, 2, 7], 'col2': [6, 7, 1, 2, 8]})
col1 col2
0 1 6
1 4 7
2 6 1
3 2 2
4 7 8
Hay varias soluciones para usar apply()
en una sola columna. A continuación los explicaré detalladamente.
I. Solución sencilla
La solución sencilla es la de @Fabio Lamanna:
df['col1'] = df['col1'].apply(complex_function)
Producción:
col1 col2
0 2 6
1 2 7
2 1 1
3 2 2
4 1 8
Sólo se modifica la primera columna, la segunda columna no se modifica. La solución es hermosa. Es solo una línea de código y se lee casi como en inglés: "Tome 'col1' y aplíquele la función complex_function " .
Sin embargo, si necesita datos de otra columna, por ejemplo, 'col2', no funcionará. Si desea pasar los valores de 'col2' a la variable y
de complex_function
, necesita algo más.
II. Solución utilizando todo el marco de datos.
Alternativamente, puede usar todo el marco de datos como se describe en esta publicación SO o en esta :
df['col1'] = df.apply(lambda x: complex_function(x['col1']), axis=1)
o si prefieres (como yo) una solución sin función lambda:
def apply_complex_function(x):
return complex_function(x['col1'])
df['col1'] = df.apply(apply_complex_function, axis=1)
En esta solución suceden muchas cosas que es necesario explicar. La apply()
función funciona en pd.Series
y pd.DataFrame
. Pero no puedes usarlo df['col1'] = df.apply(complex_function).loc[:, 'col1']
porque arrojaría un archivo ValueError
.
Por lo tanto, debe indicar qué columna utilizar. Para complicar las cosas, la apply()
función solo acepta invocables . Para solucionar esto, es necesario definir una función (lambda) con la columna x['col1']
como argumento; es decir, envolvemos la información de la columna en otra función.
Desafortunadamente, el valor predeterminado del parámetro del eje es cero ( axis=0
), lo que significa que intentará ejecutarse en columnas y no en filas. Esto no fue un problema en la primera solución, porque le dimos apply()
un archivo pd.Series
. Pero ahora la entrada es un marco de datos y debemos ser explícitos ( axis=1
). (Me pregunto con qué frecuencia olvido esto).
Si se prefiere la versión con función lambda o sin ella es subjetivo. En mi opinión, la línea de código es lo suficientemente complicada como para leerla incluso sin una función lambda incluida. Solo necesita la función (lambda) como contenedor. Es sólo un código repetitivo. Al lector no debería molestarle eso.
Ahora puedes modificar esta solución fácilmente para tener en cuenta la segunda columna:
def apply_complex_function(x):
return complex_function(x['col1'], x['col2'])
df['col1'] = df.apply(apply_complex_function, axis=1)
Producción:
col1 col2
0 2 6
1 2 7
2 1 1
3 2 2
4 2 8
En el índice 4 el valor ha cambiado de 1 a 2, porque la primera condición 7 > 5
es verdadera pero la segunda condición 7 > 8
es falsa.
Tenga en cuenta que sólo necesitaba cambiar la primera línea de código (es decir, la función) y no la segunda línea.
Nota al margen
Nunca pongas la información de la columna en tu función.
def bad_idea(x):
return x['col1'] ** 2
¡Al hacer esto, hace que una función general dependa del nombre de una columna! Esta es una mala idea, porque la próxima vez que desee utilizar esta función, no podrá hacerlo. Peor aún: tal vez cambie el nombre de una columna en un marco de datos diferente solo para que funcione con su función existente. (He estado allí, lo he hecho. ¡Es una pendiente resbaladiza!)
III. Soluciones alternativas sin usar.apply()
Aunque el OP pidió específicamente una solución con apply()
, se sugirieron soluciones alternativas. Por ejemplo, la respuesta de @George Petrov sugirió usar map()
; la respuesta propuesta por @Thibaut Dubernet assign()
.
Estoy totalmente de acuerdo en que rara vez apply()
es la mejor solución porque no apply()
está vectorizada . Es una operación de elementos con costosas llamadas a funciones y gastos generales de pd.Series
.
Una razón para usarlo apply()
es que desea utilizar una función existente y el rendimiento no es un problema. O su función es tan compleja que no existe una versión vectorizada.
Otra razón para usarlo apply()
es en combinación con groupby() . Tenga en cuenta que DataFrame.apply()
y GroupBy.apply()
son funciones diferentes.
Por tanto, tiene sentido considerar algunas alternativas:
map()
solo funciona enpd.Series
, pero acepta dict ypd.Series
como entrada. Usarmap()
con una función es casi intercambiable con usarapply()
. Puede ser más rápido queapply()
. Consulte esta publicación SO para obtener más detalles.df['col1'] = df['col1'].map(complex_function)
applymap()
es casi idéntico para los marcos de datos. No es compatiblepd.Series
y siempre devolverá un marco de datos. Sin embargo, puede ser más rápido. La documentación dice : " En la implementación actual,applymap
se llamafunc
dos veces a la primera columna/fila para decidir si puede tomar una ruta de código rápida o lenta " . Pero si el rendimiento realmente cuenta, deberías buscar una ruta alternativa.df['col1'] = df.applymap(complex_function).loc[:, 'col1']
assign()
no es un reemplazo factible paraapply()
. Tiene un comportamiento similar sólo en los casos de uso más básicos. No funciona con elcomplex_function
. Aún lo necesitaapply()
, como puede ver en el siguiente ejemplo. El principal caso de usoassign()
es el encadenamiento de métodos , porque devuelve el marco de datos sin cambiar el marco de datos original.df['col1'] = df.assign(col1=df.col1.apply(complex_function))
Anexo: ¿Cómo acelerar apply()
?
Solo lo menciono aquí porque fue sugerido por otras respuestas, por ejemplo, @durjoy. La lista no es exhaustiva:
No utilice
apply()
. Esto no es una broma. Para la mayoría de las operaciones numéricas, existe un método vectorizado en pandas. Los bloques if/else a menudo se pueden refactorizar con una combinación de indexación booleana y.loc
. Mi ejemplocomplex_function
podría refactorizarse de esta manera.Refactorizar a Cython. Si tiene una ecuación compleja y los parámetros de la ecuación están en su marco de datos, esta podría ser una buena idea. Consulte la guía oficial del usuario de pandas para obtener más información.
Utilice
raw=True
el parámetro. En teoría, esto debería mejorar el rendimiento deapply()
si solo está aplicando una función de reducción de NumPypd.Series
, porque se elimina la sobrecarga de . Por supuesto, su función tiene que aceptar un ndarray. Tienes que refactorizar tu función a NumPy. Al hacer esto, obtendrá un gran aumento de rendimiento.Utilice paquetes de terceros. Lo primero que debes probar es Numba . No sé más rápido mencionado por @durjoy; y probablemente vale la pena mencionar muchos otros paquetes aquí.
Intentar/fallar/repetir. Como se mencionó anteriormente,
map()
puedeapplymap()
ser más rápido, según el caso de uso. Simplemente cronometra las diferentes versiones y elige la más rápida. Este enfoque es el más tedioso y con el menor aumento de rendimiento.
No necesitas ninguna función. Puedes trabajar en una columna completa directamente.
Datos de ejemplo:
>>> df = pd.DataFrame({'a': [100, 1000], 'b': [200, 2000], 'c': [300, 3000]})
>>> df
a b c
0 100 200 300
1 1000 2000 3000
La mitad de todos los valores de la columna a
:
>>> df.a = df.a / 2
>>> df
a b c
0 50 200 300
1 500 2000 3000
Aunque las respuestas dadas son correctas, modifican el marco de datos inicial, lo cual no siempre es deseable (y, dado que el OP solicitó ejemplos "usando apply
", es posible que quisieran una versión que devuelva un nuevo marco de datos, como apply
lo hace).
Esto es posible usando assign
: es válido para assign
las columnas existentes, como indica la documentación (el énfasis es mío):
Asigne nuevas columnas a un DataFrame.
Devuelve un nuevo objeto con todas las columnas originales además de las nuevas. Las columnas existentes que se reasignen se sobrescribirán .
En breve:
In [1]: import pandas as pd
In [2]: df = pd.DataFrame([{'a': 15, 'b': 15, 'c': 5}, {'a': 20, 'b': 10, 'c': 7}, {'a': 25, 'b': 30, 'c': 9}])
In [3]: df.assign(a=lambda df: df.a / 2)
Out[3]:
a b c
0 7.5 15 5
1 10.0 10 7
2 12.5 30 9
In [4]: df
Out[4]:
a b c
0 15 15 5
1 20 10 7
2 25 30 9
Tenga en cuenta que a la función se le pasará todo el marco de datos, no solo la columna que desea modificar, por lo que deberá asegurarse de seleccionar la columna correcta en su lambda.