¿Aplicar la función de pandas a la columna para crear múltiples columnas nuevas?

215

Cómo hacer esto en pandas:

Tengo una función extract_text_featuresen una sola columna de texto, devolviendo múltiples columnas de salida. Específicamente, la función devuelve 6 valores.

La función funciona, sin embargo, no parece haber ningún tipo de retorno adecuado (pandas DataFrame / numpy array / Python list) de modo que la salida se pueda asignar correctamente df.ix[: ,10:16] = df.textcol.map(extract_text_features)

Entonces, creo que tengo que volver a iterar df.iterrows(), ¿según esto ?

ACTUALIZACIÓN: Iterar con df.iterrows()es al menos 20 veces más lento, así que me entregué y dividí la función en seis .map(lambda ...)llamadas distintas .

ACTUALIZACIÓN 2: esta pregunta se hizo alrededor de v0.11.0 . Por lo tanto, gran parte de las preguntas y respuestas no son demasiado relevantes.

smci
fuente
1
Creo que no se puede hacer la asignación múltiple de la manera que lo tienes escrito: df.ix[: ,10:16]. Creo que tendrás que mergeincluir tus funciones en el conjunto de datos.
Zelazny7
1
Para aquellos que desean una solución mucho más eficiente, consulte esta a continuación, que no utilizaapply
Ted Petrou el
La mayoría de las operaciones numéricas con pandas se pueden vectorizar, esto significa que son mucho más rápidas que la iteración convencional. OTOH, algunas operaciones (como string y regex) son inherentemente difíciles de vectorizar. En este caso, es importante entender cómo recorrer sus datos. Para obtener más información sobre cuándo y cómo se debe recorrer sus datos, lea Para ver los bucles con Pandas: ¿cuándo debería importarme? .
cs95
@coldspeed: el problema principal no era elegir cuál era el mayor rendimiento entre varias opciones, era luchar contra la sintaxis de los pandas para que esto funcionara, alrededor de v0.11.0 .
smci
De hecho, el comentario está dirigido a futuros lectores que buscan soluciones iterativas, que no conocen mejor o que saben lo que están haciendo.
cs95

Respuestas:

109

Partiendo de la respuesta del usuario1827356, puede hacer la tarea de una sola vez usando df.merge:

df.merge(df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1})), 
    left_index=True, right_index=True)

    textcol  feature1  feature2
0  0.772692  1.772692 -0.227308
1  0.857210  1.857210 -0.142790
2  0.065639  1.065639 -0.934361
3  0.819160  1.819160 -0.180840
4  0.088212  1.088212 -0.911788

EDITAR: Tenga en cuenta el gran consumo de memoria y la baja velocidad: https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/ !

Zelazny7
fuente
2
solo por curiosidad, ¿se espera que use mucha memoria al hacer esto? Estoy haciendo esto en un marco de datos que contiene 2.5mil filas, y casi me encuentro con problemas de memoria (también es mucho más lento que devolver solo 1 columna).
Jeffrey04
2
'df.join (df.textcol.apply (lambda s: pd.Series ({' feature1 ': s + 1,' feature2 ': s-1})))' sería una mejor opción, creo.
Shivam K. Thakkar
@ShivamKThakkar ¿por qué crees que tu sugerencia sería una mejor opción? ¿Sería más eficiente lo que piensas o tendría menos costo de memoria?
tsando
1
Tenga en cuenta la velocidad y la memoria requerida: ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply
Make42
189

Usualmente hago esto usando zip:

>>> df = pd.DataFrame([[i] for i in range(10)], columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5
6    6
7    7
8    8
9    9

>>> def powers(x):
>>>     return x, x**2, x**3, x**4, x**5, x**6

>>> df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
>>>     zip(*df['num'].map(powers))

>>> df
        num     p1      p2      p3      p4      p5      p6
0       0       0       0       0       0       0       0
1       1       1       1       1       1       1       1
2       2       2       4       8       16      32      64
3       3       3       9       27      81      243     729
4       4       4       16      64      256     1024    4096
5       5       5       25      125     625     3125    15625
6       6       6       36      216     1296    7776    46656
7       7       7       49      343     2401    16807   117649
8       8       8       64      512     4096    32768   262144
9       9       9       81      729     6561    59049   531441
Ostrokach
fuente
8
Pero, ¿qué haces si tienes 50 columnas agregadas como esta en lugar de 6?
max
14
@maxtemp = list(zip(*df['num'].map(powers))); for i, c in enumerate(columns): df[c] = temp[c]
ostrokach
8
@ostrokach Creo que te refieres for i, c in enumerate(columns): df[c] = temp[i]. Gracias a esto, realmente obtuve el propósito de enumerate: D
rocarvaj
44
Esta es, con mucho, la solución más elegante y legible que he encontrado para esto. A menos que tenga problemas de rendimiento, el idioma zip(*df['col'].map(function))es probablemente el camino a seguir.
François Leblanc
84

Esto es lo que he hecho en el pasado.

df = pd.DataFrame({'textcol' : np.random.rand(5)})

df
    textcol
0  0.626524
1  0.119967
2  0.803650
3  0.100880
4  0.017859

df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))
   feature1  feature2
0  1.626524 -0.373476
1  1.119967 -0.880033
2  1.803650 -0.196350
3  1.100880 -0.899120
4  1.017859 -0.982141

Edición para completar

pd.concat([df, df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))], axis=1)
    textcol feature1  feature2
0  0.626524 1.626524 -0.373476
1  0.119967 1.119967 -0.880033
2  0.803650 1.803650 -0.196350
3  0.100880 1.100880 -0.899120
4  0.017859 1.017859 -0.982141
usuario1827356
fuente
concat () parece más simple que merge () para conectar los nuevos cols al marco de datos original.
comino
2
buena respuesta, no necesita usar un dict o una fusión si especifica las columnas fuera de la aplicacióndf[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2'))
Matt
66

Esta es la forma correcta y más fácil de lograr esto para el 95% de los casos de uso:

>>> df = pd.DataFrame(zip(*[range(10)]), columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5

>>> def example(x):
...     x['p1'] = x['num']**2
...     x['p2'] = x['num']**3
...     x['p3'] = x['num']**4
...     return x

>>> df = df.apply(example, axis=1)
>>> df
    num  p1  p2  p3
0    0   0   0    0
1    1   1   1    1
2    2   4   8   16
3    3   9  27   81
4    4  16  64  256
Michael David Watson
fuente
no deberías escribir: df = df.apply (ejemplo (df), axis = 1) corrígeme si estoy equivocado, solo soy un novato
user299791
1
@ user299791, No, en este caso está tratando el ejemplo como un objeto de primera clase, por lo que está pasando la función en sí. Esta función se aplicará a cada fila.
Michael David Watson
hola Michael, tu respuesta me ayudó en mi problema. Definitivamente su solución es mejor que el método original pandas 'df.assign (), porque esto es una vez por columna. Usando asignar (), si desea crear 2 nuevas columnas, debe usar df1 para trabajar en df para obtener una nueva columna1, luego use df2 para trabajar en df1 para crear la segunda nueva columna ... esto es bastante monótono. ¡Pero tu método me salvó la vida! ¡¡¡Gracias!!!
commentallez-vous
1
¿No ejecutará el código de asignación de columna una vez por fila? ¿No sería mejor devolver pd.Series({k:v})ay serializar la asignación de columna como en la respuesta de Ewan?
Denis de Bernardy
29

En 2018, uso apply()con argumentoresult_type='expand'

>>> appiled_df = df.apply(lambda row: fn(row.text), axis='columns', result_type='expand')
>>> df = pd.concat([df, appiled_df], axis='columns')
Ben
fuente
66
¡Así es como lo haces hoy en día!
Make42
1
Esto funcionó de fábrica en 2020, mientras que muchas otras preguntas no. Tampoco usa lo pd.Series que siempre es bueno con respecto a los problemas de rendimiento
Théo Rubenach
1
Esta es una buena solución. El único problema es que no puede elegir el nombre de las 2 columnas recién agregadas. Luego debe hacer df.rename (columnas = {0: 'col1', 1: 'col2'})
pedram bashiri
2
@pedrambashiri Si la función a la que pasa df.applydevuelve a dict, las columnas aparecerán nombradas de acuerdo con las teclas.
Seb
24

Solo usa result_type="expand"

df = pd.DataFrame(np.random.randint(0,10,(10,2)), columns=["random", "a"])
df[["sq_a","cube_a"]] = df.apply(lambda x: [x.a**2, x.a**3], axis=1, result_type="expand")
Abhishek
fuente
44
Ayuda a señalar que la opción es nueva en 0.23 . La pregunta se hizo nuevamente en 0.11
smci
Agradable, esto es simple y aún funciona perfectamente. Este es el que estaba buscando. Gracias
Isaac Sim
Duplica una respuesta anterior: stackoverflow.com/a/52363890/823470
tar
22

Resumen: si solo desea crear unas pocas columnas, usedf[['new_col1','new_col2']] = df[['data1','data2']].apply( function_of_your_choosing(x), axis=1)

Para esta solución, el número de columnas nuevas que está creando debe ser igual al número de columnas que usa como entrada para la función .apply (). Si quieres hacer otra cosa, mira las otras respuestas.

Detalles Supongamos que tiene un marco de datos de dos columnas. La primera columna es la altura de una persona cuando tiene 10 años; el segundo es la altura de dicha persona cuando tiene 20 años.

Suponga que necesita calcular tanto la media de las alturas de cada persona como la suma de las alturas de cada persona. Eso es dos valores por cada fila.

Puede hacerlo a través de la siguiente función, que pronto se aplicará:

def mean_and_sum(x):
    """
    Calculates the mean and sum of two heights.
    Parameters:
    :x -- the values in the row this function is applied to. Could also work on a list or a tuple.
    """

    sum=x[0]+x[1]
    mean=sum/2
    return [mean,sum]

Puede usar esta función así:

 df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

(Para ser claros: esta función de aplicación toma los valores de cada fila en el marco de datos subconjunto y devuelve una lista).

Sin embargo, si haces esto:

df['Mean_&_Sum'] = df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

creará 1 nueva columna que contenga las listas [mean, sum], que probablemente querría evitar, porque eso requeriría otra Lambda / Apply.

En cambio, desea dividir cada valor en su propia columna. Para hacer esto, puede crear dos columnas a la vez:

df[['Mean','Sum']] = df[['height_at_age_10','height_at_age_20']]
.apply(mean_and_sum(x),axis=1)
Evan W.
fuente
44
Para pandas 0.23, deberá usar la sintaxis:df["mean"], df["sum"] = df[['height_at_age_10','height_at_age_20']] .apply(mean_and_sum(x),axis=1)
SummerEla
Esta función puede generar un error. La función de retorno debe ser return pd.Series([mean,sum])
Kanishk Mair
22

Para mí esto funcionó:

Entrada df

df = pd.DataFrame({'col x': [1,2,3]})
   col x
0      1
1      2
2      3

Función

def f(x):
    return pd.Series([x*x, x*x*x])

Crea 2 nuevas columnas:

df[['square x', 'cube x']] = df['col x'].apply(f)

Salida:

   col x  square x  cube x
0      1         1       1
1      2         4       8
2      3         9      27
Joe
fuente
13

He buscado varias formas de hacer esto y el método que se muestra aquí (devolver una serie de pandas) no parece ser el más eficiente.

Si comenzamos con un marco de datos más grande de datos aleatorios:

# Setup a dataframe of random numbers and create a 
df = pd.DataFrame(np.random.randn(10000,3),columns=list('ABC'))
df['D'] = df.apply(lambda r: ':'.join(map(str, (r.A, r.B, r.C))), axis=1)
columns = 'new_a', 'new_b', 'new_c'

El ejemplo que se muestra aquí:

# Create the dataframe by returning a series
def method_b(v):
    return pd.Series({k: v for k, v in zip(columns, v.split(':'))})
%timeit -n10 -r3 df.D.apply(method_b)

10 bucles, lo mejor de 3: 2.77 s por bucle

Un método alternativo:

# Create a dataframe from a series of tuples
def method_a(v):
    return v.split(':')
%timeit -n10 -r3 pd.DataFrame(df.D.apply(method_a).tolist(), columns=columns)

10 bucles, lo mejor de 3: 8.85 ms por bucle

Según mis cálculos, es mucho más eficiente tomar una serie de tuplas y luego convertirlas en un DataFrame. Sin embargo, me interesaría escuchar el pensamiento de las personas si hay un error en mi trabajo.

RFox
fuente
¡Esto es realmente útil! Obtuve una aceleración de 30x en comparación con los métodos de series de retorno de funciones.
Pushkar Nimkar
9

La solución aceptada será extremadamente lenta para muchos datos. La solución con el mayor número de votos positivos es un poco difícil de leer y también lenta con datos numéricos. Si cada nueva columna se puede calcular independientemente de las demás, simplemente asignaría cada una de ellas directamente sin usarlas apply.

Ejemplo con datos de personajes falsos

Crea 100,000 cadenas en un DataFrame

df = pd.DataFrame(np.random.choice(['he jumped', 'she ran', 'they hiked'],
                                   size=100000, replace=True),
                  columns=['words'])
df.head()
        words
0     she ran
1     she ran
2  they hiked
3  they hiked
4  they hiked

Digamos que queríamos extraer algunas características del texto como se hizo en la pregunta original. Por ejemplo, extraigamos el primer carácter, cuentemos la aparición de la letra 'e' y capitalicemos la frase.

df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
df.head()
        words first  count_e         cap
0     she ran     s        1     She ran
1     she ran     s        1     She ran
2  they hiked     t        2  They hiked
3  they hiked     t        2  They hiked
4  they hiked     t        2  They hiked

Tiempos

%%timeit
df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
127 ms ± 585 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

def extract_text_features(x):
    return x[0], x.count('e'), x.capitalize()

%timeit df['first'], df['count_e'], df['cap'] = zip(*df['words'].apply(extract_text_features))
101 ms ± 2.96 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Sorprendentemente, puede obtener un mejor rendimiento al recorrer cada valor

%%timeit
a,b,c = [], [], []
for s in df['words']:
    a.append(s[0]), b.append(s.count('e')), c.append(s.capitalize())

df['first'] = a
df['count_e'] = b
df['cap'] = c
79.1 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Otro ejemplo con datos numéricos falsos

Crea 1 millón de números aleatorios y prueba la powersfunción desde arriba.

df = pd.DataFrame(np.random.rand(1000000), columns=['num'])


def powers(x):
    return x, x**2, x**3, x**4, x**5, x**6

%%timeit
df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
       zip(*df['num'].map(powers))
1.35 s ± 83.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Asignar cada columna es 25 veces más rápido y muy legible:

%%timeit 
df['p1'] = df['num'] ** 1
df['p2'] = df['num'] ** 2
df['p3'] = df['num'] ** 3
df['p4'] = df['num'] ** 4
df['p5'] = df['num'] ** 5
df['p6'] = df['num'] ** 6
51.6 ms ± 1.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Hice una respuesta similar con más detalles aquí sobre por qué applygeneralmente no es el camino a seguir.

Ted Petrou
fuente
8

Han publicado la misma respuesta en otras dos preguntas similares. La forma en que prefiero hacer esto es envolver los valores de retorno de la función en una serie:

def f(x):
    return pd.Series([x**2, x**3])

Y luego use aplicar de la siguiente manera para crear columnas separadas:

df[['x**2','x**3']] = df.apply(lambda row: f(row['x']), axis=1)
Dmytro Bugayev
fuente
1

puede devolver la fila completa en lugar de valores:

df = df.apply(extract_text_features,axis = 1)

donde la función devuelve la fila

def extract_text_features(row):
      row['new_col1'] = value1
      row['new_col2'] = value2
      return row
Saket Bajaj
fuente
No, no quiero aplicar extract_text_featuresa todas las columnas del df, solo a la columna de textodf.textcol
smci
-2
def myfunc(a):
    return a * a

df['New Column'] = df['oldcolumn'].map(myfunc))

Esto funcionó para mí. Se creará una nueva columna con datos procesados ​​de la columna anterior.

usuario2902302
fuente
2
Esto no devuelve 'múltiples columnas nuevas'
pedram bashiri
Esto no devuelve 'múltiples columnas nuevas', por lo que no responde la pregunta. ¿Podría por favor eliminarlo?
smci