Pandas: crie duas novas colunas em um dataframe com valores calculados a partir de uma coluna pré-existente


100

Estou trabalhando com a biblioteca pandas e quero adicionar duas novas colunas a um dataframe dfcom n colunas (n> 0).
Essas novas colunas resultam da aplicação de uma função a uma das colunas no dataframe.

A função a ser aplicada é como:

def calculate(x):
    ...operate...
    return z, y

Um método para criar uma nova coluna para uma função que retorna apenas um valor é:

df['new_col']) = df['column_A'].map(a_function)

Então, o que eu quero, e tentei sem sucesso (*), é algo como:

(df['new_col_zetas'], df['new_col_ys']) = df['column_A'].map(calculate)

Qual poderia ser a melhor maneira de fazer isso? Eu examinei a documentação sem nenhuma pista.

** df['column_A'].map(calculate)retorna uma série pandas em que cada item consiste em uma tupla z, y. E tentar atribuir isso a duas colunas de dataframe produz um ValueError. *

Respostas:


119

Eu apenas usaria zip:

In [1]: from pandas import *

In [2]: def calculate(x):
   ...:     return x*2, x*3
   ...: 

In [3]: df = DataFrame({'a': [1,2,3], 'b': [2,3,4]})

In [4]: df
Out[4]: 
   a  b
0  1  2
1  2  3
2  3  4

In [5]: df["A1"], df["A2"] = zip(*df["a"].map(calculate))

In [6]: df
Out[6]: 
   a  b  A1  A2
0  1  2   2   3
1  2  3   4   6
2  3  4   6   9

Obrigado, ótimo, funciona. Não encontrei nada parecido com isso nos documentos de 0.8.1 ... Acho que sempre devo pensar em Séries como listas de tuplas ...
— joaquin

Existe alguma diferença em relação ao desempenho ao fazer isso? zip (* map (calcular, df ["a"])) em vez de zip (* df ["a"]. map (calcular)), que também dá (como acima) [(2, 4, 6), ( 3, 6, 9)]?
— ekta de

1
Eu recebo o seguinte aviso ao fazer a criação de uma nova coluna como essa: "SettingWithCopyWarning: Um valor está tentando ser definido em uma cópia de uma fatia de um DataFrame. Tente usar .loc [row_indexer, col_indexer] = value." Devo me preocupar com isso? pandas v.0.15
— taras

46

A principal resposta é falha em minha opinião. Felizmente, ninguém está importando em massa todos os pandas para seu namespace com from pandas import *. Além disso, o mapmétodo deve ser reservado para aqueles momentos em que é passado um dicionário ou série. Pode ter uma função, mas é para isso que applyé usado.

Então, se você deve usar a abordagem acima, eu escreveria assim

df["A1"], df["A2"] = zip(*df["a"].apply(calculate))

Na verdade, não há razão para usar o zip aqui. Você pode simplesmente fazer isso:

df["A1"], df["A2"] = calculate(df['a'])

Este segundo método também é muito mais rápido em DataFrames maiores

df = pd.DataFrame({'a': [1,2,3] * 100000, 'b': [2,3,4] * 100000})

DataFrame criado com 300.000 linhas

%timeit df["A1"], df["A2"] = calculate(df['a'])
2.65 ms ± 92.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit df["A1"], df["A2"] = zip(*df["a"].apply(calculate))
159 ms ± 5.24 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

60x mais rápido do que zip


Em geral, evite usar aplicar

Aplicar geralmente não é muito mais rápido do que iterar em uma lista Python. Vamos testar o desempenho de um loop for para fazer a mesma coisa que acima

%%timeit
A1, A2 = [], []
for val in df['a']:
    A1.append(val**2)
    A2.append(val**3)

df['A1'] = A1
df['A2'] = A2

298 ms ± 7.14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Portanto, é duas vezes mais lento, o que não é uma regressão de desempenho terrível, mas se citonizarmos o acima, obteremos um desempenho muito melhor. Supondo que você esteja usando ipython:

%load_ext cython

%%cython
cpdef power(vals):
    A1, A2 = [], []
    cdef double val
    for val in vals:
        A1.append(val**2)
        A2.append(val**3)

    return A1, A2

%timeit df['A1'], df['A2'] = power(df['a'])
72.7 ms ± 2.16 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Atribuir diretamente sem aplicar

Você pode obter melhorias de velocidade ainda maiores se usar as operações vetorizadas diretas.

%timeit df['A1'], df['A2'] = df['a'] ** 2, df['a'] ** 3
5.13 ms ± 320 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Isso aproveita as operações vetorizadas extremamente rápidas do NumPy em vez de nossos loops. Agora temos um aumento de 30x em relação ao original.


O teste de velocidade mais simples com apply

O exemplo acima deve mostrar claramente o quão lento applypode ser, mas só para ficar mais claro, vamos dar uma olhada no exemplo mais básico. Vamos elevar ao quadrado uma série de 10 milhões de números com e sem aplicação

s = pd.Series(np.random.rand(10000000))

%timeit s.apply(calc)
3.3 s ± 57.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Sem aplicar é 50x mais rápido

%timeit s ** 2
66 ms ± 2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

1
Esta é uma resposta realmente ótima. Eu queria perguntar: o que você acha do applymapcaso quando você tem que implementar uma função específica para cada elemento do dataframe?
— David

3
Embora haja alguns bons conselhos nesta resposta, acredito que o conselho principal a ser usado em func(series)vez de series.apply(func)só se aplica quando a função é totalmente definida usando operações que se comportam de maneira semelhante em um valor individual e em uma série. Esse é o caso no exemplo da primeira resposta, mas não é o caso na pergunta do OP, que está perguntando de forma mais geral sobre a aplicação de funções a colunas. 1/2
— Graham Lea

1
Por exemplo, se df é: DataFrame({'a': ['Aaron', 'Bert', 'Christopher'], 'b': ['Bold', 'Courageous', 'Distrusted']})e calcé: def calc(x): return x[0], len(x)então tdf.a.apply(calc))e calc(tdf.a)retorna coisas muito diferentes.
— Graham Lea
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.