Como aplicar uma função a duas colunas do quadro de dados do Pandas

368

Suponha que eu tenho um dfque tem colunas de 'ID', 'col_1', 'col_2'. E eu defino uma função:

f = lambda x, y : my_function_expression.

Agora eu quero aplicar o fa dfduas colunas 's 'col_1', 'col_2'para calcular elemento-wise uma nova coluna 'col_3', um pouco como:

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

Como fazer ?

** Adicione a amostra detalhada como abaixo ***

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']
Grande erro
fonte
4
você pode aplicar f diretamente às colunas: df ['col_3'] = f (df ['col_1'], df ['col_2']) '
btel
11
Seria útil saber o que festá fazendo
tehmisvh
2
não, df ['col_3'] = f (df ['col_1'], df ['col_2']) não funciona. Pois f aceita apenas entradas escalares, não entradas vetoriais. OK, você pode assumir f = lambda x, y: x + y. (claro, a minha verdadeira f não é tão simples, caso contrário, eu posso diretamente df [ 'col_3'] = df [ 'col_1'] + df [ 'col_2'])
bigbug
11
Encontrei uma seção de perguntas e respostas relacionadas no URL abaixo, mas meu problema é calcular uma nova coluna por duas colunas existentes, e não 2 de 1. stackoverflow.com/questions/12356501/...
bigbug
Acho que minha resposta stackoverflow.com/a/52854800/5447172 responde da maneira mais pitônica / pandana, sem soluções alternativas ou indexação numérica. Produz exatamente a saída necessária no seu exemplo.
ajrwhite

Respostas:

291

Aqui está um exemplo usando applyo dataframe, com o qual estou chamando axis = 1.

Observe que a diferença é que, em vez de tentar passar dois valores para a função f, reescreva a função para aceitar um objeto da série pandas e, em seguida, indexe a série para obter os valores necessários.

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

Dependendo do seu caso de uso, às vezes é útil criar um groupobjeto pandas e usá-lo applyno grupo.

Um homem
fonte
Sim, tentei usar o Apply, mas não consigo encontrar a expressão de sintaxe válida. E se cada linha de df for única, ainda use groupby?
bigbug
Adicionado um exemplo à minha resposta, espero que isso faça o que você está procurando. Caso contrário, forneça uma função de exemplo mais específica, pois sumfoi resolvida com sucesso por qualquer um dos métodos sugeridos até o momento.
Aman
11
Você poderia colar seu código? Eu reescrevo a função: def get_sublist (x): retorna mylist [x [1]: x [2] + 1] e df ['col_3'] = df.apply (get_sublist, axis = 1) fornece 'ValueError: os operandos poderiam não será transmitido em conjunto com formas (2) (3)'
bigbug
3
@ Aman: com a versão 0.14.1 do Pandas (e possivelmente anterior), o uso também pode usar uma expressão lambda. Dê ao dfobjeto que você definiu, outra abordagem (com resultados equivalentes) é df.apply(lambda x: x[0] + x[1], axis = 1).
Jubbles
2
@ CanCeylan, você pode simplesmente usar os nomes das colunas na função em vez dos índices, para não precisar se preocupar com a mudança de ordem ou obter o índice pelo nome, por exemplo, consulte stackoverflow.com/questions/13021654/…
Davos
167

Existe uma maneira limpa e de uma linha de fazer isso no Pandas:

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

Isso permite fser uma função definida pelo usuário com vários valores de entrada e usa nomes de coluna (seguros) em vez de índices numéricos (não seguros) para acessar as colunas.

Exemplo com dados (com base na pergunta 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)

Saída 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]

Se os nomes das colunas contiverem espaços ou compartilharem um nome com um atributo de quadro de dados existente, você poderá indexar com colchetes:

df['col_3'] = df.apply(lambda x: f(x['col 1'], x['col 2']), axis=1)
ajrwhite
fonte
2
Observe que se a axis=1coluna using and you for chamada, nameela não retornará realmente os dados da coluna, mas sim index. Semelhante a obter o nameem um groupby(). Eu resolvi isso renomeando minha coluna.
Tom Hemmes 22/05/19
2
É ISSO! Eu simplesmente não sabia que era possível inserir funções definidas pelo usuário com vários parâmetros de entrada nos lambdas. É importante observar (eu acho) que você está usando DF.apply () em vez de Series.apply (). Isso permite que você indexe o df usando as duas colunas desejadas e passe a coluna inteira para a função, mas, como você está usando o apply (), aplica a função de maneira elementar em toda a coluna. Brilhante! Obrigado por postar!
Data-phile
11
FINALMENTE! Você salvou meu dia!
Mysterio
Eu acredito que a maneira sugerida de fazer isso é df.loc [:, 'new col'] = df.apply .....
valearner
@valearner Acho que não há razão para preferir .loco exemplo. Pode ser necessário se você adaptar isso a outra configuração de problema (por exemplo, trabalhando com fatias).
ajrwhite 17/02
86

Uma solução simples é:

df['col_3'] = df[['col_1','col_2']].apply(lambda x: f(*x), axis=1)
sjm
fonte
11
como essa resposta é diferente da abordagem da pergunta: df ['col_3'] = df [['col_1', 'col_2']]. apply (f) apenas para confirmar, a abordagem na pergunta não funcionou porque o método poster não especificou este eixo = 1, o padrão é eixo = 0?
Lost1
11
Esta resposta é comparável à resposta de @ Anman, mas um pouco mais superficial. Ele está construindo uma função anônima que pega uma iterável e a descompacta antes de passar para a função f.
tiao
39

Uma pergunta interessante! minha resposta como abaixo:

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

Resultado:

  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]

Alterei o nome da coluna para ID, J1, J2, J3 para garantir a identificação <J1 <J2 <J3, para que a coluna seja exibida na seqüência correta.

Mais uma versão resumida:

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

fonte
23

O método que você está procurando é o Series.combine. No entanto, parece que alguns cuidados devem ser tomados em relação aos tipos de dados. No seu exemplo, você (como eu fiz ao testar a resposta) chamava ingenuamente

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

No entanto, isso gera o erro:

ValueError: setting an array element with a sequence.

Meu melhor palpite é que parece esperar que o resultado seja do mesmo tipo que a série que chama o método (df.col_1 aqui). No entanto, o seguinte 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
fonte
12

O modo como você escreveu f precisa de duas entradas. Se você olhar para a mensagem de erro, ela diz que você não está fornecendo duas entradas para f, apenas uma. A mensagem de erro está correta.
A incompatibilidade ocorre porque df [['col1', 'col2']] retorna um único quadro de dados com duas colunas, não duas colunas separadas.

Você precisa alterar seu f para que ele receba uma única entrada, mantenha o quadro de dados acima como entrada e divida-o em x, y dentro do corpo da função. Em seguida, faça o que precisar e retorne um único valor.

Você precisa dessa assinatura de função porque a sintaxe é .apply (f) Portanto, f precisa pegar a única coisa = dataframe e não duas coisas que é o que seu f atual espera.

Como você não forneceu o corpo de f, não posso ajudar em mais detalhes - mas isso deve fornecer a saída sem alterar fundamentalmente seu código ou usar outros métodos, em vez de aplicar

Nitin
fonte
12

Vou votar em np.vectorize. Ele permite que você fotografe mais de um número x de colunas e não lide com o quadro de dados na função, por isso é ótimo para funções que você não controla ou faz algo como enviar 2 colunas e uma constante em uma função (por exemplo, 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
fonte
11
Isso realmente não responde à pergunta usando pandas.
Mnky9800n
18
A pergunta é "Como aplicar uma função a duas colunas do quadro de dados do Pandas" e não "Como aplicar uma função a duas colunas do quadro de dados do Pandas usando apenas métodos do Pandas" e numpy é uma dependência do Pandas; portanto, você precisa instalá-lo de qualquer maneira, então isso parece uma objeção estranha.
Trae Wallace
12

Retornar uma lista de applyé uma operação perigosa, pois não é garantido que o objeto resultante seja uma Série ou um DataFrame. E exceções podem ser levantadas em certos casos. Vamos percorrer um exemplo simples:

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

Existem três resultados possíveis com o retorno de uma lista de apply

1) Se o comprimento da lista retornada não for igual ao número de colunas, uma Série de listas será retornada.

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) Quando o comprimento da lista retornada é igual ao número de colunas, um DataFrame é retornado e cada coluna obtém o valor correspondente na 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) Se o comprimento da lista retornada for igual ao número de colunas da primeira linha, mas tiver pelo menos uma linha em que a lista tenha um número diferente de elementos que o número de colunas, um ValueError será gerado.

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)

Respondendo ao problema sem aplicar

Usar applycom o eixo = 1 é muito lento. É possível obter um desempenho muito melhor (especialmente em conjuntos de dados maiores) com métodos iterativos básicos.

Crie um quadro de dados maior

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

Horários

# 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 answer

%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
fonte
11
É bom ver respostas tão detalhadas de onde é possível aprender.
Andrea Moro
7

Tenho certeza de que isso não é tão rápido quanto as soluções que usam operações Pandas ou Numpy, mas se você não deseja reescrever sua função, pode usar o mapa. Usando os dados de exemplo originais -

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

Poderíamos passar tantos argumentos quanto desejássemos para a função dessa maneira. A saída é o 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
fonte
11
Este é realmente muito mais rápido essas respostas que o uso applycomaxis=1
Ted Petrou
2

Meu exemplo para suas perguntas:

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

Se você tiver um conjunto de dados enorme, poderá usar uma maneira fácil, mas mais rápida (tempo de execução) de fazer isso usando o swifter:

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
fonte
1

Suponho que você não queira alterar a get_sublistfunção e apenas queira usar o applymétodo do DataFrame para fazer o trabalho. Para obter o resultado desejado, escrevi duas funções de ajuda: get_sublist_liste unlist. Como o nome da função sugere, primeiro obtenha a lista de sub-listas, depois extraia-os da lista. Finalmente, precisamos chamar applyfunction para aplicar essas duas funções ao 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

Se você não usar []a get_sublistfunção, get_sublist_listela retornará uma lista simples e aumentará ValueError: could not broadcast input array from shape (3) into shape (2), como @Ted Petrou havia mencionado.

allenyllee
fonte