Aplicar a função pandas à coluna para criar várias novas colunas?

215

Como fazer isso em pandas:

Eu tenho uma função extract_text_featuresem uma única coluna de texto, retornando várias colunas de saída. Especificamente, a função retorna 6 valores.

A função funciona, no entanto, parece não haver nenhum tipo de retorno adequado (pandas DataFrame / numpy array / lista Python), de modo que a saída possa ser atribuída corretamente df.ix[: ,10:16] = df.textcol.map(extract_text_features)

Então, acho que preciso voltar a iterar com isso df.iterrows(), de acordo com isso ?

ATUALIZAÇÃO: A iteração df.iterrows()é pelo menos 20x mais lenta, então eu me rendi e dividi a função em seis .map(lambda ...)chamadas distintas .

ATUALIZAÇÃO 2: esta pergunta foi feita em torno da v0.11.0 . Portanto, grande parte das perguntas e respostas não são muito relevantes.

smci
fonte
1
Eu não acho que você pode fazer a atribuição múltipla da maneira que você tê-lo escrito: df.ix[: ,10:16]. Eu acho que você terá que mergeseus recursos no conjunto de dados.
usar o seguinte código
1
Para aqueles que querem uma solução muito mais performant verificar este abaixo que não usaapply
Ted Petrou
A maioria das operações numéricas com pandas pode ser vetorizada - isso significa que elas são muito mais rápidas que a iteração convencional. OTOH, algumas operações (como string e regex) são inerentemente difíceis de vetorizar. Nesse caso, é importante entender como fazer um loop sobre seus dados. Para obter mais informações sobre quando e como fazer loop em seus dados, leia Para loops com Pandas - Quando devo me importar? .
cs95
@ Coldspeed: a questão principal não era escolher qual era o desempenho mais alto dentre várias opções, estava combatendo a sintaxe dos pandas para que isso funcionasse, em torno da v0.11.0 .
smci 4/01/19
Na verdade, o comentário é destinado a futuros leitores que estão procurando soluções iterativas, que não sabem melhor ou que sabem o que estão fazendo.
cs95

Respostas:

109

Com base na resposta do usuário1827356, você pode fazer a atribuição de uma só 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

EDIT: Esteja ciente do enorme consumo de memória e baixa velocidade: https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/ !

Zelazny7
fonte
2
por curiosidade, espera-se que consuma muita memória fazendo isso? Eu estou fazendo isso em um dataframe que contém 2.5mil linhas, e eu quase tive problemas de memória (também é muito mais lento do que retornar apenas uma coluna).
precisa saber é o seguinte
2
'df.join (df.textcol.apply (lambda s: pd.Series ({' feature1 ': s + 1,' feature2 ': s-1})))' seria uma opção melhor, eu acho.
Shivam K. Thakkar
@ShivamKThakkar, por que você acha que sua sugestão seria uma opção melhor? Você acha que seria mais eficiente ou teria menos custo de memória?
tsando
1
Por favor, considere a velocidade e a memória necessária: ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply
Make42
189

Eu costumo fazer isso 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
fonte
8
Mas o que você faz se tiver 50 colunas adicionadas assim em vez de 6?
Max
14
@maxtemp = list(zip(*df['num'].map(powers))); for i, c in enumerate(columns): df[c] = temp[c]
ostrokach
8
@ostrokach Eu acho que você quis dizer for i, c in enumerate(columns): df[c] = temp[i]. Graças a isso, eu realmente tenho o objetivo de enumerate: D
rocarvaj
4
Essa é, de longe, a solução mais elegante e legível que encontrei para isso. A menos que você esteja tendo problemas de desempenho, o idioma zip(*df['col'].map(function))provavelmente é o caminho a percorrer.
François Leblanc
84

Isto é o que eu fiz no passado

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

Edição para integridade

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
user1827356
fonte
concat () parece mais simples que merge () para conectar as novas colunas ao dataframe original.
cominho
2
boa resposta, você não precisa usar um dict ou uma mesclagem se especificar as colunas fora da aplicaçãodf[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2'))
Matt
66

Esta é a maneira correta e mais fácil de fazer isso para 95% dos 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
fonte
você não deve escrever: df = df.apply (exemplo (DF), eixo = 1) me corrija se eu estiver errado, eu sou apenas um novato
user299791
1
@ user299791, Não, neste caso, você está tratando o exemplo como um objeto de primeira classe, passando a própria função. Esta função será aplicada a cada linha.
Michael David Watson
oi Michael, sua resposta me ajudou no meu problema. Definitivamente, sua solução é melhor que o método df.assign () original dos pandas, porque é uma vez por coluna. Usando assign (), se você deseja criar 2 novas colunas, é necessário usar o df1 para trabalhar no df para obter a nova coluna1, depois usar o df2 para trabalhar no df1 para criar a segunda nova coluna ... isso é bastante monótono. Mas seu método salvou minha vida !!! Obrigado!!!
commentallez-vous
1
Isso não executa o código de atribuição da coluna uma vez por linha? Não seria melhor retornar pd.Series({k:v})e serializar a atribuição da coluna como na resposta de Ewan?
Denis de Bernardy
29

Em 2018, eu uso apply()com 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
fonte
6
É assim que se faz hoje em dia!
Make42
1
Isso funcionou imediatamente em 2020, enquanto muitas outras perguntas não. Também não usa o pd.Series que é sempre bom em relação a problemas de desempenho
Théo Rubenach
1
Esta é uma boa solução. O único problema é que você não pode escolher o nome para as duas colunas adicionadas recentemente. Você precisa fazer o df.rename mais tarde (colunas = {0: 'col1', 1: 'col2'})
pedram bashiri
2
@pedrambashiri Se a função que você passa para df.applyretornar a dict, as colunas serão nomeadas de acordo com as teclas.
Seb
24

Apenas use 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
fonte
4
Ajuda a ressaltar que a opção é nova na 0,23 . A pergunta foi feita novamente em 0,11
smci 8/06/19
Bom, isso é simples e ainda funciona perfeitamente. Este é o que eu estava procurando. Obrigado
Isaac Sim
Duplica uma resposta anterior: stackoverflow.com/a/52363890/823470
tar
22

Resumo: se você deseja criar apenas algumas colunas, usedf[['new_col1','new_col2']] = df[['data1','data2']].apply( function_of_your_choosing(x), axis=1)

Para esta solução, o número de novas colunas que você está criando deve ser igual ao número de colunas que você usa como entrada para a função .apply (). Se você quiser fazer outra coisa, dê uma olhada nas outras respostas.

Detalhes Digamos que você tenha um quadro de dados de duas colunas. A primeira coluna é a altura de uma pessoa quando ela tem 10 anos; o segundo é a altura da pessoa quando ela tem 20 anos.

Suponha que você precise calcular a média das alturas de cada pessoa e a soma das alturas de cada pessoa. São dois valores por cada linha.

Você pode fazer isso através da seguinte função, que será aplicada em breve:

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]

Você pode usar esta função da seguinte maneira:

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

(Para ficar claro: essa função de aplicação recebe os valores de cada linha no dataframe subconjunto e retorna uma lista.)

No entanto, se você fizer isso:

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

você criará uma nova coluna que contém as listas [média, soma], que você provavelmente gostaria de evitar, porque isso exigiria outro Lambda / Apply.

Em vez disso, você deseja dividir cada valor em sua própria coluna. Para fazer isso, você pode criar duas colunas ao mesmo tempo:

df[['Mean','Sum']] = df[['height_at_age_10','height_at_age_20']]
.apply(mean_and_sum(x),axis=1)
Evan W.
fonte
4
Para os pandas 0.23, você precisará usar a sintaxe:df["mean"], df["sum"] = df[['height_at_age_10','height_at_age_20']] .apply(mean_and_sum(x),axis=1)
SummerEla
Esta função pode gerar erro. A função de retorno deve ser return pd.Series([mean,sum])
Kanishk Mair
22

Para mim, isso funcionou:

Entrada df

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

Função

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

Crie 2 novas colunas:

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

Resultado:

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

Procurei várias maneiras de fazer isso e o método mostrado aqui (retornando uma série de pandas) não parece ser mais eficiente.

Se começarmos com um quadro de dados grande, de dados aleatórios:

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

O exemplo mostrado aqui:

# 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 loops, o melhor de 3: 2,77 s por loop

Um 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 loops, o melhor de 3: 8,85 ms por loop

Pela minha conta, é muito mais eficiente pegar uma série de tuplas e depois convertê-las em um DataFrame. Eu ficaria interessado em ouvir o pensamento das pessoas, se houver um erro no meu trabalho.

RFox
fonte
Isso é realmente útil! Eu obtive uma aceleração de 30x em comparação com a função que retorna métodos de série.
Pushkar Nimkar
9

A solução aceita será extremamente lenta para muitos dados. A solução com o maior número de votos positivos é um pouco difícil de ler e também lenta com os dados numéricos. Se cada nova coluna puder ser calculada independentemente das outras, eu atribuiria cada uma delas diretamente sem usar apply.

Exemplo com dados de caracteres falsos

Crie 100.000 seqüências de caracteres em um 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 desejássemos extrair alguns recursos de texto, como feito na pergunta original. Por exemplo, vamos extrair o primeiro caractere, contar a ocorrência da letra 'e' e colocar em maiúscula a 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

Horários

%%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)

Surpreendentemente, você pode obter um melhor desempenho percorrendo 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)

Outro exemplo com dados numéricos falsos

Crie 1 milhão de números aleatórios e teste a powersfunção de cima.

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)

A atribuição de cada coluna é 25x mais rápida e muito legível:

%%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)

Fiz uma resposta semelhante com mais detalhes aqui sobre por que applynormalmente não é o caminho a percorrer.

Ted Petrou
fonte
8

Postaram a mesma resposta em duas outras perguntas semelhantes. A maneira como prefiro fazer isso é agrupar os valores de retorno da função em uma série:

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

E, em seguida, use apply da seguinte maneira para criar colunas separadas:

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

você pode retornar a linha inteira em vez de valores:

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

onde a função retorna a linha

def extract_text_features(row):
      row['new_col1'] = value1
      row['new_col2'] = value2
      return row
Saket Bajaj
fonte
Não, eu não deseja aplicar extract_text_featuresa cada coluna do df, apenas para a coluna de textodf.textcol
SMCI
-2
def myfunc(a):
    return a * a

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

Isso funcionou para mim. Nova coluna será criada com os dados da coluna antiga processados.

user2902302
fonte
2
Isso não retorna 'várias novas colunas'
pedram bashiri 21/04
Isso não retorna 'várias novas colunas', portanto não responde à pergunta. Você poderia excluí-lo?
smci