Cómo aplicar una función a dos columnas del marco de datos Pandas

368

Supongamos que tengo un dfque tiene columnas de 'ID', 'col_1', 'col_2'. Y defino una función:

f = lambda x, y : my_function_expression.

Ahora quiero aplicar la fa df's dos columnas 'col_1', 'col_2'para calcular elemento gota una nueva columna 'col_3', algo así como:

df['col_3'] = df[['col_1','col_2']].apply(f)  
# Pandas gives : TypeError: ('<lambda>() takes exactly 2 arguments (1 given)'

Cómo hacer ?

** ** Agregue una muestra detallada como a continuación ***

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

#df['col_3'] = df[['col_1','col_2']].apply(get_sublist,axis=1)
# expect above to output df as below 

  ID  col_1  col_2            col_3
0  1      0      1       ['a', 'b']
1  2      2      4  ['c', 'd', 'e']
2  3      3      5  ['d', 'e', 'f']
bicho grande
fuente
44
¿puede aplicar f directamente a las columnas: df ['col_3'] = f (df ['col_1'], df ['col_2'])
btel
1
sería útil saber qué festá haciendo
tehmisvh
2
no, df ['col_3'] = f (df ['col_1'], df ['col_2']) no funciona. Para f solo acepta entradas escalares, no entradas de vectores. OK, puedes asumir f = lambda x, y: x + y. (por supuesto, mi verdadera f no es tan simple, de lo contrario puedo directamente df ['col_3'] = df ['col_1'] + df ['col_2'])
bigbug
1
Encontré un Q&A relacionado en la siguiente URL, pero mi problema es calcular una nueva columna por dos columnas existentes, no 2 de 1. stackoverflow.com/questions/12356501/…
bigbug
Creo que mi respuesta stackoverflow.com/a/52854800/5447172 responde a esto de la manera más Pythonic / Pandanic, sin soluciones o indexación numérica. Produce exactamente la salida que necesita en su ejemplo.
ajrwhite

Respuestas:

291

Aquí hay un ejemplo de uso applyen el marco de datos, con el que estoy llamando axis = 1.

Tenga en cuenta que la diferencia es que, en lugar de tratar de pasar dos valores a la función f, reescriba la función para aceptar un objeto Serie pandas y luego indexe la Serie para obtener los valores necesarios.

In [49]: df
Out[49]: 
          0         1
0  1.000000  0.000000
1 -0.494375  0.570994
2  1.000000  0.000000
3  1.876360 -0.229738
4  1.000000  0.000000

In [50]: def f(x):    
   ....:  return x[0] + x[1]  
   ....:  

In [51]: df.apply(f, axis=1) #passes a Series object, row-wise
Out[51]: 
0    1.000000
1    0.076619
2    1.000000
3    1.646622
4    1.000000

Dependiendo de su caso de uso, a veces es útil crear un groupobjeto pandas y luego usarlo applyen el grupo.

Un hombre
fuente
Sí, intenté usar apply, pero no puedo encontrar la expresión de sintaxis válida. Y si cada fila de df es única, ¿sigue usando groupby?
bigbug
Agregué un ejemplo a mi respuesta, espero que haga lo que estás buscando. De lo contrario, proporcione una función de ejemplo más específica, ya que sumse resuelve con éxito mediante cualquiera de los métodos sugeridos hasta ahora.
Aman el
1
¿Podrías pegar tu código? Reescribo la función: def get_sublist (x): return mylist [x [1]: x [2] + 1] y df ['col_3'] = df.apply (get_sublist, axis = 1) da 'ValueError: operandos podrían no se transmitirá junto con formas (2) (3) '
bigbug
3
@Aman: con Pandas versión 0.14.1 (y posiblemente anterior), el uso también puede usar una expresión lambda. Dale al dfobjeto que definiste, otro enfoque (con resultados equivalentes) es df.apply(lambda x: x[0] + x[1], axis = 1).
Jubbles
2
@CanCeylan, puede usar los nombres de columna en la función en lugar de índices, entonces no necesita preocuparse por el cambio de orden u obtener el índice por nombre, por ejemplo, consulte stackoverflow.com/questions/13021654/…
Davos
167

Hay una forma limpia y de una línea de hacer esto en Pandas:

df['col_3'] = df.apply(lambda x: f(x.col_1, x.col_2), axis=1)

Esto permite fser una función definida por el usuario con múltiples valores de entrada y utiliza nombres de columna (seguros) en lugar de índices numéricos (inseguros) para acceder a las columnas.

Ejemplo con datos (basado en la pregunta original):

import pandas as pd

df = pd.DataFrame({'ID':['1', '2', '3'], 'col_1': [0, 2, 3], 'col_2':[1, 4, 5]})
mylist = ['a', 'b', 'c', 'd', 'e', 'f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

df['col_3'] = df.apply(lambda x: get_sublist(x.col_1, x.col_2), axis=1)

Salida de print(df):

  ID  col_1  col_2      col_3
0  1      0      1     [a, b]
1  2      2      4  [c, d, e]
2  3      3      5  [d, e, f]

Si los nombres de sus columnas contienen espacios o comparten un nombre con un atributo de marco de datos existente, puede indexar entre corchetes:

df['col_3'] = df.apply(lambda x: f(x['col 1'], x['col 2']), axis=1)
ajrwhite
fuente
2
Tenga en cuenta que si se usa axis=1y se llama namesu columna, en realidad no devolverá los datos de su columna, sino el index. Similar a obtener el nameen a groupby(). Resolví esto cambiando el nombre de mi columna.
Tom Hemmes
2
¡ESO ES TODO! Simplemente no me di cuenta de que podría insertar funciones definidas por el usuario con múltiples parámetros de entrada en lambdas. Es importante tener en cuenta (creo) que está utilizando DF.apply () en lugar de Series.apply (). Esto le permite indexar el df usando las dos columnas que desea, y pasar la columna completa a la función, pero debido a que está utilizando apply (), aplica la función en forma de elementos en toda la columna. ¡Brillante! ¡Gracias por tu publicación!
Archivo de datos
1
¡FINALMENTE! ¡Salvaste mi día!
Mysterio
Creo que la forma sugerida de hacer esto es df.loc [:, 'new col'] = df.apply .....
valearner
@valearner No creo que haya ninguna razón para preferir .locen el ejemplo. Puede ser necesario si adapta esto a otra configuración del problema (por ejemplo, trabajar con sectores).
ajrwhite
86

Una solución simple es:

df['col_3'] = df[['col_1','col_2']].apply(lambda x: f(*x), axis=1)
sjm
fuente
1
¿Cómo es esta respuesta diferente al enfoque en la pregunta: df ['col_3'] = df [['col_1', 'col_2']]. aplica (f) solo para confirmar, el enfoque en la pregunta no funcionó porque el póster no especificó este eje = 1, el valor predeterminado es axis = 0?
Perdido1
1
Esta respuesta es comparable a la respuesta de @ Anman pero un poco más ingeniosa. Está construyendo una función anónima que toma un iterable y lo desempaqueta antes de pasarlo a la función f.
tiao
39

Una pregunta interesante! mi respuesta de la siguiente manera:

import pandas as pd

def sublst(row):
    return lst[row['J1']:row['J2']]

df = pd.DataFrame({'ID':['1','2','3'], 'J1': [0,2,3], 'J2':[1,4,5]})
print df
lst = ['a','b','c','d','e','f']

df['J3'] = df.apply(sublst,axis=1)
print df

Salida:

  ID  J1  J2
0  1   0   1
1  2   2   4
2  3   3   5
  ID  J1  J2      J3
0  1   0   1     [a]
1  2   2   4  [c, d]
2  3   3   5  [d, e]

Cambié el nombre de la columna a ID, J1, J2, J3 para asegurar ID <J1 <J2 <J3, por lo que la columna se muestra en la secuencia correcta.

Una versión más breve:

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'J1': [0,2,3], 'J2':[1,4,5]})
print df
lst = ['a','b','c','d','e','f']

df['J3'] = df.apply(lambda row:lst[row['J1']:row['J2']],axis=1)
print df

fuente
23

El método que está buscando es Series.combine. Sin embargo, parece que se debe tener cuidado con los tipos de datos. En su ejemplo, usted (como hice cuando probé la respuesta) ingenuamente llamaría

df['col_3'] = df.col_1.combine(df.col_2, func=get_sublist)

Sin embargo, esto arroja el error:

ValueError: setting an array element with a sequence.

Mi mejor suposición es que parece esperar que el resultado sea del mismo tipo que la serie que llama al método (df.col_1 aquí). Sin embargo, lo siguiente funciona:

df['col_3'] = df.col_1.astype(object).combine(df.col_2, func=get_sublist)

df

   ID   col_1   col_2   col_3
0   1   0   1   [a, b]
1   2   2   4   [c, d, e]
2   3   3   5   [d, e, f]
JoeCondron
fuente
12

La forma en que ha escrito f necesita dos entradas. Si observa el mensaje de error, dice que no está proporcionando dos entradas a f, solo una. El mensaje de error es correcto.
La falta de coincidencia se debe a que df [['' col1 ',' col2 ']] devuelve un único marco de datos con dos columnas, no dos columnas separadas.

Debe cambiar su f para que tome una sola entrada, mantenga el marco de datos anterior como entrada, luego divídalo en x, y dentro del cuerpo de la función. Luego haga lo que necesite y devuelva un valor único.

Necesita esta firma de función porque la sintaxis es .apply (f) Por lo tanto, f necesita tomar la única cosa = marco de datos y no dos cosas, que es lo que su f actual espera.

Como no ha proporcionado el cuerpo de f, no puedo ayudarlo en más detalles, pero esto debería proporcionar la salida sin cambiar fundamentalmente su código o usar algunos otros métodos en lugar de aplicar

Nitina
fuente
12

Voy a votar por np.vectorize. Le permite disparar más de x número de columnas y no tratar con el marco de datos en la función, por lo que es ideal para funciones que no controla o hacer algo como enviar 2 columnas y una constante en una función (es decir, col_1, col_2, 'foo').

import numpy as np
import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

#df['col_3'] = df[['col_1','col_2']].apply(get_sublist,axis=1)
# expect above to output df as below 

df.loc[:,'col_3'] = np.vectorize(get_sublist, otypes=["O"]) (df['col_1'], df['col_2'])


df

ID  col_1   col_2   col_3
0   1   0   1   [a, b]
1   2   2   4   [c, d, e]
2   3   3   5   [d, e, f]
Trae Wallace
fuente
1
Esto realmente no responde la pregunta usando pandas.
mnky9800n
18
La pregunta es "Cómo aplicar una función a dos columnas del marco de datos de Pandas" y no "Cómo aplicar una función a dos columnas del marco de datos de Pandas utilizando solo métodos de Pandas" y numpy es una dependencia de Pandas, por lo que debe tenerlo instalado de todos modos, Esto parece una extraña objeción.
Trae Wallace
12

Devolver una lista de applyes una operación peligrosa ya que no se garantiza que el objeto resultante sea una Serie o un Marco de datos. Y se pueden plantear excepciones en ciertos casos. Veamos un ejemplo simple:

df = pd.DataFrame(data=np.random.randint(0, 5, (5,3)),
                  columns=['a', 'b', 'c'])
df
   a  b  c
0  4  0  0
1  2  0  1
2  2  2  2
3  1  2  2
4  3  0  0

Hay tres resultados posibles al devolver una lista de apply

1) Si la longitud de la lista devuelta no es igual al número de columnas, se devuelve una serie de listas.

df.apply(lambda x: list(range(2)), axis=1)  # returns a Series
0    [0, 1]
1    [0, 1]
2    [0, 1]
3    [0, 1]
4    [0, 1]
dtype: object

2) Cuando la longitud de la lista devuelta es igual al número de columnas, se devuelve un DataFrame y cada columna obtiene el valor correspondiente en la lista.

df.apply(lambda x: list(range(3)), axis=1) # returns a DataFrame
   a  b  c
0  0  1  2
1  0  1  2
2  0  1  2
3  0  1  2
4  0  1  2

3) Si la longitud de la lista devuelta es igual al número de columnas para la primera fila pero tiene al menos una fila donde la lista tiene un número diferente de elementos que el número de columnas, se genera un ValueError.

i = 0
def f(x):
    global i
    if i == 0:
        i += 1
        return list(range(3))
    return list(range(4))

df.apply(f, axis=1) 
ValueError: Shape of passed values is (5, 4), indices imply (5, 3)

Respondiendo el problema sin aplicar

Usar applycon axis = 1 es muy lento. Es posible obtener un rendimiento mucho mejor (especialmente en conjuntos de datos más grandes) con métodos iterativos básicos.

Crear un marco de datos más grande

df1 = df.sample(100000, replace=True).reset_index(drop=True)

Tiempos

# apply is slow with axis=1
%timeit df1.apply(lambda x: mylist[x['col_1']: x['col_2']+1], axis=1)
2.59 s ± 76.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# zip - similar to @Thomas
%timeit [mylist[v1:v2+1] for v1, v2 in zip(df1.col_1, df1.col_2)]  
29.5 ms ± 534 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

@Thomas respuesta

%timeit list(map(get_sublist, df1['col_1'],df1['col_2']))
34 ms ± 459 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Ted Petrou
fuente
1
Es bueno ver respuestas tan detalladas desde donde es posible aprender.
Andrea Moro
7

Estoy seguro de que esto no es tan rápido como las soluciones que usan operaciones Pandas o Numpy, pero si no desea reescribir su función, puede usar map. Usando los datos de ejemplo originales:

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

df['col_3'] = list(map(get_sublist,df['col_1'],df['col_2']))
#In Python 2 don't convert above to list

Podríamos pasar tantos argumentos como quisiéramos a la función de esta manera. La salida es lo que queríamos

ID  col_1  col_2      col_3
0  1      0      1     [a, b]
1  2      2      4  [c, d, e]
2  3      3      5  [d, e, f]
Thomas
fuente
1
En realidad, esto es mucho más rápido que las respuestas que usan applyconaxis=1
Ted Petrou
2

Mi ejemplo a tus preguntas:

def get_sublist(row, col1, col2):
    return mylist[row[col1]:row[col2]+1]
df.apply(get_sublist, axis=1, col1='col_1', col2='col_2')
Qing Liu
fuente
2

Si tiene un gran conjunto de datos, puede usar una forma fácil pero más rápida (tiempo de ejecución) de hacerlo usando más rápido:

import pandas as pd
import swifter

def fnc(m,x,c):
    return m*x+c

df = pd.DataFrame({"m": [1,2,3,4,5,6], "c": [1,1,1,1,1,1], "x":[5,3,6,2,6,1]})
df["y"] = df.swifter.apply(lambda x: fnc(x.m, x.x, x.c), axis=1)
durjoy
fuente
1

Supongo que no desea cambiar la get_sublistfunción, y solo quiere usar el applymétodo DataFrame para hacer el trabajo. Para obtener el resultado que desea, he escrito dos funciones de ayuda: get_sublist_listy unlist. Como sugiere el nombre de la función, primero obtenga la lista de sublistas, luego extraiga esa sublista de esa lista. Finalmente, necesitamos llamar a la applyfunción para aplicar esas dos funciones al df[['col_1','col_2']]DataFrame posteriormente.

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

def get_sublist_list(cols):
    return [get_sublist(cols[0],cols[1])]

def unlist(list_of_lists):
    return list_of_lists[0]

df['col_3'] = df[['col_1','col_2']].apply(get_sublist_list,axis=1).apply(unlist)

df

Si no utiliza []para encerrar la get_sublistfunción, la get_sublist_listfunción devolverá una lista simple, se elevará ValueError: could not broadcast input array from shape (3) into shape (2), como había mencionado @Ted Petrou.

Allenyllee
fuente