¿Cómo puedo utilizar la función apply() para una sola columna?

Resuelto Amani asked hace 8 años • 8 respuestas

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?

Amani avatar Jan 23 '16 17:01 Amani
Aceptado

Dado un marco de datos de muestra dfcomo:

   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
Fabio Lamanna avatar Jan 23 '2016 10:01 Fabio Lamanna

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
George Petrov avatar Jan 23 '2016 10:01 George Petrov

Dado el siguiente marco de datos dfy 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 yde 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 > 5es verdadera pero la segunda condición 7 > 8es 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 en pd.Series, pero acepta dict y pd.Seriescomo entrada. Usar map()con una función es casi intercambiable con usar apply(). Puede ser más rápido que apply(). 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 compatible pd.Seriesy siempre devolverá un marco de datos. Sin embargo, puede ser más rápido. La documentación dice : " En la implementación actual, applymapse llama funcdos 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 para apply(). Tiene un comportamiento similar sólo en los casos de uso más básicos. No funciona con el complex_function. Aún lo necesita apply(), como puede ver en el siguiente ejemplo. El principal caso de uso assign()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:

  1. 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 ejemplo complex_functionpodría refactorizarse de esta manera.

  2. 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.

  3. Utilice raw=Trueel parámetro. En teoría, esto debería mejorar el rendimiento de apply() 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.

  4. 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í.

  5. Intentar/fallar/repetir. Como se mencionó anteriormente, map()puede applymap()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.

above_c_level avatar Jul 18 '2020 12:07 above_c_level

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
Mike Müller avatar Jan 23 '2016 10:01 Mike Müller

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 applylo hace).

Esto es posible usando assign: es válido para assignlas 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.

Thibaut Dubernet avatar Jun 26 '2019 08:06 Thibaut Dubernet