pandas: Como faço para dividir o texto em uma coluna em várias linhas?

135

Estou trabalhando com um arquivo csv grande e a penúltima coluna tem uma sequência de texto que eu quero dividir por um delimitador específico. Eu queria saber se existe uma maneira simples de fazer isso usando pandas ou python?

CustNum  CustomerName     ItemQty  Item   Seatblocks                 ItemExt
32363    McCartney, Paul      3     F04    2:218:10:4,6                   60
31316    Lennon, John        25     F01    1:13:36:1,12 1:13:37:1,13     300

Quero dividir pelo espaço (' ')e, em seguida, dois pontos (':')na Seatblockscoluna, mas cada célula resultaria em um número diferente de colunas. Eu tenho uma função para reorganizar as colunas para que a Seatblockscoluna esteja no final da planilha, mas não tenho certeza do que fazer a partir daí. Posso fazê-lo no excel com a text-to-columnsfunção integrada e uma macro rápida, mas meu conjunto de dados possui muitos registros para o excel manipular.

Por fim, quero gravar registros como John Lennon e criar várias linhas, com as informações de cada conjunto de assentos em uma linha separada.

Bradley
fonte
esta grande questão refere-se a FlatMap em pandas, que actualmente não existe
cdarlint

Respostas:

203

Isso divide os Seatblocks pelo espaço e dá a cada uma sua própria linha.

In [43]: df
Out[43]: 
   CustNum     CustomerName  ItemQty Item                 Seatblocks  ItemExt
0    32363  McCartney, Paul        3  F04               2:218:10:4,6       60
1    31316     Lennon, John       25  F01  1:13:36:1,12 1:13:37:1,13      300

In [44]: s = df['Seatblocks'].str.split(' ').apply(Series, 1).stack()

In [45]: s.index = s.index.droplevel(-1) # to line up with df's index

In [46]: s.name = 'Seatblocks' # needs a name to join

In [47]: s
Out[47]: 
0    2:218:10:4,6
1    1:13:36:1,12
1    1:13:37:1,13
Name: Seatblocks, dtype: object

In [48]: del df['Seatblocks']

In [49]: df.join(s)
Out[49]: 
   CustNum     CustomerName  ItemQty Item  ItemExt    Seatblocks
0    32363  McCartney, Paul        3  F04       60  2:218:10:4,6
1    31316     Lennon, John       25  F01      300  1:13:36:1,12
1    31316     Lennon, John       25  F01      300  1:13:37:1,13

Ou, para fornecer cada sequência separada por dois pontos em sua própria coluna:

In [50]: df.join(s.apply(lambda x: Series(x.split(':'))))
Out[50]: 
   CustNum     CustomerName  ItemQty Item  ItemExt  0    1   2     3
0    32363  McCartney, Paul        3  F04       60  2  218  10   4,6
1    31316     Lennon, John       25  F01      300  1   13  36  1,12
1    31316     Lennon, John       25  F01      300  1   13  37  1,13

Isso é um pouco feio, mas talvez alguém entre em contato com uma solução mais bonita.

Dan Allan
fonte
7
@DanAllan fornece um índice para a série quando você se inscreve; eles se tornarão nomes de colunas
Jeff
4
Enquanto isso responde à pergunta, vale a pena mencionar que (provavelmente) split () cria uma lista para cada linha, o que aumenta o tamanho da DataFramemuito rapidamente. No meu caso, a execução do código em uma tabela ~ 200M resultou no uso de memória ~ 10G (+ swap ...).
David Nemeskey 24/03
1
Embora eu não tenha certeza de que é por causa disso split(), simplesmente reduce()navegar pela coluna funciona como um encanto. O problema, então pode estar em stack()...
David Nemeskey
4
Estou recebendo o erro NameError: name 'Series' is not definedpara isso. de onde Seriesdeveria vir? EDIT: deixa pra lá, deve ser, pandas.Seriesjá que está se referindo ao item depandas
user5359531
2
Sim, @ user5359531. I from pandas import Seriespor conveniência / brevidade.
21816 Dan Allan
52

Diferentemente de Dan, considero sua resposta bastante elegante ... mas, infelizmente, também é muito, muito ineficiente. Portanto, desde que a pergunta mencionou "um grande arquivo csv" , deixe-me sugerir tentar em um shell a solução de Dan:

time python -c "import pandas as pd;
df = pd.DataFrame(['a b c']*100000, columns=['col']);
print df['col'].apply(lambda x : pd.Series(x.split(' '))).head()"

... comparado a esta alternativa:

time python -c "import pandas as pd;
from scipy import array, concatenate;
df = pd.DataFrame(['a b c']*100000, columns=['col']);
print pd.DataFrame(concatenate(df['col'].apply( lambda x : [x.split(' ')]))).head()"

... e isto:

time python -c "import pandas as pd;
df = pd.DataFrame(['a b c']*100000, columns=['col']);
print pd.DataFrame(dict(zip(range(3), [df['col'].apply(lambda x : x.split(' ')[i]) for i in range(3)]))).head()"

O segundo simplesmente evita alocar 100 000 Series, e isso é suficiente para torná-lo 10 vezes mais rápido. Mas a terceira solução, que ironicamente desperdiça muitas chamadas para str.split () (é chamada uma vez por coluna por linha, três vezes mais do que nas outras duas soluções), é cerca de 40 vezes mais rápida que a primeira, porque até evita instanciar as 100.000 listas. E sim, é certamente um pouco feio ...

EDIT: esta resposta sugere como usar "to_list ()" e para evitar a necessidade de um lambda. O resultado é algo como

time python -c "import pandas as pd;
df = pd.DataFrame(['a b c']*100000, columns=['col']);
print pd.DataFrame(df.col.str.split().tolist()).head()"

que é ainda mais eficiente que a terceira solução e certamente muito mais elegante.

EDIT: o ainda mais simples

time python -c "import pandas as pd;
df = pd.DataFrame(['a b c']*100000, columns=['col']);
print pd.DataFrame(list(df.col.str.split())).head()"

funciona também e é quase tão eficiente.

EDIT: ainda mais simples ! E lida com NaNs (mas menos eficiente):

time python -c "import pandas as pd;
df = pd.DataFrame(['a b c']*100000, columns=['col']);
print df.col.str.split(expand=True).head()"
Pietro Battiston
fonte
Estou tendo um pequeno problema com a quantidade de memória que esse método consome e estou me perguntando se você poderia me dar um pequeno conselho. Eu tenho um DataFrame que contém cerca de 8000 linhas, cada uma com uma seqüência de caracteres que contém 9216 inteiros de 8 bits delimitados por espaço. São aproximadamente 75 MB, mas quando aplico a última solução literalmente, o Python consome 2 GB da minha memória. Você pode me apontar na direção de alguma fonte que me diga por que isso é e o que posso fazer para contornar isso? Obrigado.
castelo-bravo
1
Você tem muitas listas e seqüências de caracteres muito pequenas, o que é mais ou menos o pior caso de uso de memória em python (e a etapa intermediária ".split (). Tolist ()" produz objetos python puros). O que eu provavelmente faria no seu lugar seria despejar o DataFrame em um arquivo e abri-lo como csv com read_csv (..., sep = ''). Mas, para permanecer no tópico: a primeira solução (juntamente com a terceira, que no entanto deve ser muito lenta) pode ser a que oferece o menor uso de memória entre as 4, pois você tem um número relativamente pequeno de linhas relativamente longas.
Pietro Battiston
Olá Pietro, tentei sua sugestão de salvar em um arquivo e recarregá-lo, e funcionou muito bem. Eu tive alguns problemas quando tentei fazer isso em um objeto StringIO, e uma boa solução para o meu problema foi postada aqui .
castelo-bravo
3
Sua última sugestão tolist()é perfeita. No meu caso eu só queria uma das peças de dados na lista e foi capaz de adicionar diretamente uma única coluna à minha df existente usando .ix:df['newCol'] = pd.DataFrame(df.col.str.split().tolist()).ix[:,2]
fantabolous
Ahh, eu estava tendo problemas para fazer isso funcionar no começo - algo sobre o obect of type 'float' has no len()que era desconcertante, até que percebi que algumas das minhas linhas estavam NaNnelas, ao contrário str.
Dwanderson 21/05
14
import pandas as pd
import numpy as np

df = pd.DataFrame({'ItemQty': {0: 3, 1: 25}, 
                   'Seatblocks': {0: '2:218:10:4,6', 1: '1:13:36:1,12 1:13:37:1,13'}, 
                   'ItemExt': {0: 60, 1: 300}, 
                   'CustomerName': {0: 'McCartney, Paul', 1: 'Lennon, John'}, 
                   'CustNum': {0: 32363, 1: 31316}, 
                   'Item': {0: 'F04', 1: 'F01'}}, 
                    columns=['CustNum','CustomerName','ItemQty','Item','Seatblocks','ItemExt'])

print (df)
   CustNum     CustomerName  ItemQty Item                 Seatblocks  ItemExt
0    32363  McCartney, Paul        3  F04               2:218:10:4,6       60
1    31316     Lennon, John       25  F01  1:13:36:1,12 1:13:37:1,13      300

Outra solução semelhante com encadeamento é o uso reset_indexe rename:

print (df.drop('Seatblocks', axis=1)
             .join
             (
             df.Seatblocks
             .str
             .split(expand=True)
             .stack()
             .reset_index(drop=True, level=1)
             .rename('Seatblocks')           
             ))

   CustNum     CustomerName  ItemQty Item  ItemExt    Seatblocks
0    32363  McCartney, Paul        3  F04       60  2:218:10:4,6
1    31316     Lennon, John       25  F01      300  1:13:36:1,12
1    31316     Lennon, John       25  F01      300  1:13:37:1,13

Se na coluna NÃO houver NaNvalores, a solução mais rápida é usar a listcompreensão com o DataFrameconstrutor:

df = pd.DataFrame(['a b c']*100000, columns=['col'])

In [141]: %timeit (pd.DataFrame(dict(zip(range(3), [df['col'].apply(lambda x : x.split(' ')[i]) for i in range(3)]))))
1 loop, best of 3: 211 ms per loop

In [142]: %timeit (pd.DataFrame(df.col.str.split().tolist()))
10 loops, best of 3: 87.8 ms per loop

In [143]: %timeit (pd.DataFrame(list(df.col.str.split())))
10 loops, best of 3: 86.1 ms per loop

In [144]: %timeit (df.col.str.split(expand=True))
10 loops, best of 3: 156 ms per loop

In [145]: %timeit (pd.DataFrame([ x.split() for x in df['col'].tolist()]))
10 loops, best of 3: 54.1 ms per loop

Mas se a coluna contém NaNapenas funciona str.splitcom o parâmetro expand=Trueque retorna DataFrame( documentação ) e explica por que é mais lento:

df = pd.DataFrame(['a b c']*10, columns=['col'])
df.loc[0] = np.nan
print (df.head())
     col
0    NaN
1  a b c
2  a b c
3  a b c
4  a b c

print (df.col.str.split(expand=True))
     0     1     2
0  NaN  None  None
1    a     b     c
2    a     b     c
3    a     b     c
4    a     b     c
5    a     b     c
6    a     b     c
7    a     b     c
8    a     b     c
9    a     b     c
jezrael
fonte
Talvez valha a pena mencionar que você necessariamente precisa da expand=Trueopção de trabalhar pandas.DataFramesenquanto estiver usando, .str.split()por exemplo.
holzkohlengrill
@holzkohlengrill - obrigado pelo comentário, adiciono-o para responder.
precisa saber é
@ jezrael, estou demorando muito tempo para executar esse código, é o esperado. Como exatamente eu faço isso mais rápido? Se eu colocá-lo em um loop for como: for x em df [Seablocks] [: 100] para fazê-lo apenas em um subconjunto e concatenar nesses subconjuntos, isso funcionará?
Bernando_vialli
2

Outra abordagem seria assim:

temp = df['Seatblocks'].str.split(' ')
data = data.reindex(data.index.repeat(temp.apply(len)))
data['new_Seatblocks'] = np.hstack(temp)
Bharat Sahu
fonte
1

Também pode usar groupby () sem a necessidade de se juntar e empilhar ().

Use os dados de exemplo acima:

import pandas as pd
import numpy as np


df = pd.DataFrame({'ItemQty': {0: 3, 1: 25}, 
                   'Seatblocks': {0: '2:218:10:4,6', 1: '1:13:36:1,12 1:13:37:1,13'}, 
                   'ItemExt': {0: 60, 1: 300}, 
                   'CustomerName': {0: 'McCartney, Paul', 1: 'Lennon, John'}, 
                   'CustNum': {0: 32363, 1: 31316}, 
                   'Item': {0: 'F04', 1: 'F01'}}, 
                    columns=['CustNum','CustomerName','ItemQty','Item','Seatblocks','ItemExt']) 
print(df)

   CustNum     CustomerName  ItemQty Item                 Seatblocks  ItemExt
0  32363    McCartney, Paul  3        F04  2:218:10:4,6               60     
1  31316    Lennon, John     25       F01  1:13:36:1,12 1:13:37:1,13  300  


#first define a function: given a Series of string, split each element into a new series
def split_series(ser,sep):
    return pd.Series(ser.str.cat(sep=sep).split(sep=sep)) 
#test the function, 
split_series(pd.Series(['a b','c']),sep=' ')
0    a
1    b
2    c
dtype: object

df2=(df.groupby(df.columns.drop('Seatblocks').tolist()) #group by all but one column
          ['Seatblocks'] #select the column to be split
          .apply(split_series,sep=' ') # split 'Seatblocks' in each group
         .reset_index(drop=True,level=-1).reset_index()) #remove extra index created

print(df2)
   CustNum     CustomerName  ItemQty Item  ItemExt    Seatblocks
0    31316     Lennon, John       25  F01      300  1:13:36:1,12
1    31316     Lennon, John       25  F01      300  1:13:37:1,13
2    32363  McCartney, Paul        3  F04       60  2:218:10:4,6
Ben2018
fonte
Desde já, obrigado. Como eu poderia usar o código acima dividindo duas colunas de forma correspondente. Por exemplo: 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B .. O resultado deve ser: 0 31316 Lennon, John 25 F01 300 1:13:36:1,12 Ae a próxima linha 0 31316 Lennon, John 25 F01 300 1:13:37:1,13 B
Krithi.S
@ Krithi.S, tento entender a pergunta. Você quer dizer que as duas colunas devem ter o mesmo número de membros após a divisão? Quais são os resultados esperados para 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B, C?
Ben2018