como dividir a coluna de tuplas no dataframe do pandas?

88

Eu tenho um dataframe do pandas (este é apenas um pequeno pedaço)

>>> d1
   y norm test  y norm train  len(y_train)  len(y_test)  \
0    64.904368    116.151232          1645          549   
1    70.852681    112.639876          1645          549   

                                    SVR RBF  \
0   (35.652207342877873, 22.95533537448393)   
1  (39.563683797747622, 27.382483096332511)   

                                        LCV  \
0  (19.365430594452338, 13.880062435173587)   
1  (19.099614489458364, 14.018867136617146)   

                                   RIDGE CV  \
0  (4.2907610988480362, 12.416745648065584)   
1    (4.18864306788194, 12.980833914392477)   

                                         RF  \
0   (9.9484841581029428, 16.46902345373697)   
1  (10.139848213735391, 16.282141345406522)   

                                           GB  \
0  (0.012816232716538605, 15.950164822266007)   
1  (0.012814519804493328, 15.305745202851712)   

                                             ET DATA  
0  (0.00034337162272515505, 16.284800366214057)  j2m  
1  (0.00024811554516431878, 15.556506191784194)  j2m  
>>> 

Quero dividir todas as colunas que contêm tuplas. Por exemplo, quero substituir a coluna LCVpelas colunas LCV-ae LCV-b.

Como eu posso fazer isso?

Donbeo
fonte

Respostas:

159

Você pode fazer isso pd.DataFrame(col.tolist())nessa coluna:

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

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

In [4]: df['b'].tolist()                                                                                                                                                        
Out[4]: [(1, 2), (3, 4)]

In [5]: pd.DataFrame(df['b'].tolist(), index=df.index)                                                                                                                                          
Out[5]: 
   0  1
0  1  2
1  3  4

In [6]: df[['b1', 'b2']] = pd.DataFrame(df['b'].tolist(), index=df.index)                                                                                                                       

In [7]: df                                                                                                                                                                      
Out[7]: 
   a       b  b1  b2
0  1  (1, 2)   1   2
1  2  (3, 4)   3   4

Nota: em uma versão anterior, esta resposta recomendada para usar em df['b'].apply(pd.Series)vez de pd.DataFrame(df['b'].tolist(), index=df.index). Isso funciona bem (porque faz de cada tupla uma série, que é então vista como uma linha de um quadro de dados), mas é mais lento / usa mais memória do que a tolistversão, conforme observado pelas outras respostas aqui (graças a @denfromufa) .
Eu atualizei esta resposta para ter certeza de que a resposta mais visível tem a melhor solução.

Joris
fonte
2
existe uma maneira de automatizar devido ao grande número de colunas?
Donbeo
Não diretamente, eu acho. Mas você pode facilmente escrever uma função para ele usando o código acima (+ removendo o original)
joris
Se você tiver um grande número de colunas, convém 'organizar' seus dados: vita.had.co.nz/papers/tidy-data.html Você pode fazer isso usando a função derreter.
Axel de
.apply (pd.Series) funciona bem, mas para grandes conjuntos de dados consome muita memória e pode causar erro de memória
Yury Wallet
26

Em conjuntos de dados muito maiores, descobri que .apply()alguns pedidos são mais lentos do quepd.DataFrame(df['b'].values.tolist(), index=df.index)

Este problema de desempenho foi resolvido no GitHub, embora eu não concorde com esta decisão:

https://github.com/pandas-dev/pandas/issues/11615

EDITAR: com base nesta resposta: https://stackoverflow.com/a/44196843/2230844

denfromufa
fonte
5
pd.DataFrame(df['b'].tolist())sem o .valuesparece funcionar muito bem também. (E obrigado, sua solução é muito mais rápida do que .apply())
Swier
Eu estava preocupado com a captura de índice, daí o uso explícito de .values.
denfromufa de
1
solução por @denfromufa funciona super rápido df [['b1', 'b2']] = pd.DataFrame (df ['b']. values.tolist (), index = df.index) e não causa nenhum erro de memória (como em comparação com .apply (pd.Series))
Yury Wallet
17

O stracessador que está disponível para pandas.Seriesobjetos de dtype == objecté, na verdade, um iterável.

Suponha que pandas.DataFrame df:

df = pd.DataFrame(dict(col=[*zip('abcdefghij', range(10, 101, 10))]))

df

        col
0   (a, 10)
1   (b, 20)
2   (c, 30)
3   (d, 40)
4   (e, 50)
5   (f, 60)
6   (g, 70)
7   (h, 80)
8   (i, 90)
9  (j, 100)

Podemos testar se é um iterável

from collections import Iterable

isinstance(df.col.str, Iterable)

True

Podemos então atribuir a partir dele como fazemos com outros iteráveis:

var0, var1 = 'xy'
print(var0, var1)

x y

Solução mais simples

Então, em uma linha, podemos atribuir as duas colunas

df['a'], df['b'] = df.col.str

df

        col  a    b
0   (a, 10)  a   10
1   (b, 20)  b   20
2   (c, 30)  c   30
3   (d, 40)  d   40
4   (e, 50)  e   50
5   (f, 60)  f   60
6   (g, 70)  g   70
7   (h, 80)  h   80
8   (i, 90)  i   90
9  (j, 100)  j  100

Solução mais rápida

Apenas um pouco mais complicado, podemos usar zip para criar um iterável semelhante

df['c'], df['d'] = zip(*df.col)

df

        col  a    b  c    d
0   (a, 10)  a   10  a   10
1   (b, 20)  b   20  b   20
2   (c, 30)  c   30  c   30
3   (d, 40)  d   40  d   40
4   (e, 50)  e   50  e   50
5   (f, 60)  f   60  f   60
6   (g, 70)  g   70  g   70
7   (h, 80)  h   80  h   80
8   (i, 90)  i   90  i   90
9  (j, 100)  j  100  j  100

Na linha

Significando, não modifique o existente df
Isso funciona porque assignleva argumentos de palavra-chave onde as palavras-chave são os nomes de coluna novos (ou existentes) e os valores serão os valores da nova coluna. Você pode usar um dicionário e descompactá-lo **e fazê-lo funcionar como os argumentos de palavra-chave. Portanto, esta é uma maneira inteligente de atribuir uma nova coluna chamada 'g'que é o primeiro item do df.col.striterável e 'h'que é o segundo item do df.col.striterável.

df.assign(**dict(zip('gh', df.col.str)))

        col  g    h
0   (a, 10)  a   10
1   (b, 20)  b   20
2   (c, 30)  c   30
3   (d, 40)  d   40
4   (e, 50)  e   50
5   (f, 60)  f   60
6   (g, 70)  g   70
7   (h, 80)  h   80
8   (i, 90)  i   90
9  (j, 100)  j  100

Minha versão da listabordagem

Com compreensão de lista moderna e descompactação variável.
Nota: também inline usandojoin

df.join(pd.DataFrame([*df.col], df.index, [*'ef']))

        col  g    h
0   (a, 10)  a   10
1   (b, 20)  b   20
2   (c, 30)  c   30
3   (d, 40)  d   40
4   (e, 50)  e   50
5   (f, 60)  f   60
6   (g, 70)  g   70
7   (h, 80)  h   80
8   (i, 90)  i   90
9  (j, 100)  j  100

A versão mutante seria

df[['e', 'f']] = pd.DataFrame([*df.col], df.index)

Teste de Tempo Ingênuo

DataFrame curto

Use um definido acima

%timeit df.assign(**dict(zip('gh', df.col.str)))
%timeit df.assign(**dict(zip('gh', zip(*df.col))))
%timeit df.join(pd.DataFrame([*df.col], df.index, [*'gh']))

1.16 ms ± 21.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
635 µs ± 18.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
795 µs ± 42.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Long DataFrame

10 ^ 3 vezes maior

df = pd.concat([df] * 1000, ignore_index=True)

%timeit df.assign(**dict(zip('gh', df.col.str)))
%timeit df.assign(**dict(zip('gh', zip(*df.col))))
%timeit df.join(pd.DataFrame([*df.col], df.index, [*'gh']))

11.4 ms ± 1.53 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.1 ms ± 41.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.33 ms ± 35.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
piRSquared
fonte
2
Considere adicionar TL; DR: df['a'], df['b'] = df.col.str:)
mirekphd
11

Acho que uma maneira mais simples é:

>>> import pandas as pd
>>> df = pd.DataFrame({'a':[1,2], 'b':[(1,2), (3,4)]}) 
>>> df
   a       b
0  1  (1, 2)
1  2  (3, 4)
>>> df['b_a']=df['b'].str[0]
>>> df['b_b']=df['b'].str[1]
>>> df
   a       b  b_a  b_b
0  1  (1, 2)    1    2
1  2  (3, 4)    3    4
Jinhua Wang
fonte
1
Esta solução é de fato muito mais simples
ApplePie
@jinhuawang parece que é um hack no topo da strrepresentação de um pd.Seriesobjeto. Você pode explicar como isso funciona ?!
denfromufa de
Eu acho que é apenas como o objeto str funciona? você pode acessar o objeto array com str
Jinhua Wang
E se algumas das linhas tiverem tuplas com um número diferente de valores?
mammykins de
Eu acho que este deve ser o aceito. É mais 'pandas-onic' ... se isso for alguma coisa.
Natacha
8

Eu sei que isso é de um tempo atrás, mas uma ressalva da segunda solução:

pd.DataFrame(df['b'].values.tolist())

é que ele descartará explicitamente o índice e adicionará um índice sequencial padrão, enquanto a resposta aceita

apply(pd.Series)

não, pois o resultado de apply manterá o índice de linha. Embora a ordem seja inicialmente mantida a partir da matriz original, os pandas tentarão combinar os indicadores dos dois dataframes.

Isso pode ser muito importante se você estiver tentando definir as linhas em um array indexado numericamente, e o pandas automaticamente tentará combinar o índice do novo array com o antigo, causando alguma distorção na ordem.

Uma solução híbrida melhor seria definir o índice do dataframe original para o novo, ou seja,

pd.DataFrame(df['b'].values.tolist(), index=df.index)

O que manterá a velocidade de uso do segundo método, garantindo que a ordem e a indexação sejam mantidas no resultado.

Mike
fonte
Editei minha resposta com base em sua observação de indexação, obrigado!
denfromufa de