Panda: come posso dividere il testo in una colonna in più righe?


135

Sto lavorando con un grande file CSV e la penultima colonna contiene una stringa di testo che voglio dividere per un delimitatore specifico. Mi chiedevo se esiste un modo semplice per farlo usando Panda o 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

Voglio dividere per lo spazio (' ')e quindi i due punti (':')nella Seatblockscolonna, ma ogni cella comporterebbe un numero diverso di colonne. Ho una funzione per riorganizzare le colonne in modo che la Seatblockscolonna sia alla fine del foglio, ma non sono sicuro di cosa fare da lì. Posso farlo in Excel con la text-to-columnsfunzione integrata e una macro veloce, ma il mio set di dati ha troppi record da gestire per Excel.

Alla fine, voglio prendere dischi come quelli di John Lennon e creare più linee, con le informazioni di ogni gruppo di posti su una linea separata.


questa grande domanda riguarda FlatMap in Panda, che al momento non esiste
cdarlint

Risposte:


203

Ciò divide i blocchi di sede per spazio e dà a ciascuno la propria fila.

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

Oppure, per dare ciascuna stringa separata da due punti nella propria colonna:

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

Questo è un po 'brutto, ma forse qualcuno entrerà in azione con una soluzione più carina.


7
@DanAllan fornisce un indice alla serie quando si applica; diventeranno nomi di colonne
Jeff

4
Mentre questo risponde alla domanda, vale la pena ricordare che (probabilmente) split () crea un elenco per ogni riga, che fa esplodere le dimensioni di DataFramemolto rapidamente. Nel mio caso, l'esecuzione del codice su una tabella di ~ 200M ha comportato un utilizzo della memoria di ~ 10G (+ swap ...).
David Nemeskey,

1
Anche se non sono sicuro che sia a causa di split(), perché semplicemente reduce()'ing attraverso la colonna funziona come un fascino. Il problema quindi potrebbe risiedere in stack()...
David Nemeskey,

4
Ricevo l'errore NameError: name 'Series' is not definedper questo. da dove Seriesdovrebbe venire? EDIT: non importa, dovrebbe essere pandas.Seriespoiché si riferisce all'articolo dapandas
user5359531

2
Sì, @ user5359531. I from pandas import Seriesper comodità / brevità.
Dan Allan,

52

A differenza di Dan, considero la sua risposta piuttosto elegante ... ma sfortunatamente è anche molto inefficiente. Quindi, dato che la domanda menzionava "un file CSV di grandi dimensioni" , lasciatemi suggerire di provare in una soluzione di Dan della shell:

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()"

... rispetto a questa 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 questo:

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()"

Il secondo si astiene semplicemente dall'assegnare 100.000 serie e questo è sufficiente per renderlo circa 10 volte più veloce. Ma la terza soluzione, che in qualche modo ironicamente spreca molte chiamate a str.split () (viene chiamata una volta per colonna per riga, quindi tre volte di più rispetto alle altre due soluzioni), è circa 40 volte più veloce della prima, perché evita persino di istanziare le 100000 liste. E sì, è certamente un po 'brutto ...

EDIT: questa risposta suggerisce come usare "to_list ()" ed evitare la necessità di un lambda. Il risultato è qualcosa di simile

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()"

che è persino più efficiente della terza soluzione e sicuramente molto più elegante.

EDIT: il ancora più semplice

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()"

funziona anche ed è quasi altrettanto efficiente.

EDIT: ancora più semplice ! E gestisce NaNs (ma meno efficiente):

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

Sto avendo un piccolo problema con la quantità di memoria che questo metodo consuma e mi chiedo se potresti darmi un piccolo consiglio. Ho un DataFrame che contiene circa 8000 righe, ognuna con una stringa contenente 9216 numeri interi a 8 bit delimitati da spazi. Si tratta di circa 75 MB, ma quando applico letteralmente l'ultima soluzione, Python mangia 2 GB di memoria. Puoi indicarmi la direzione di una fonte che potrebbe dirmi perché questo è e cosa posso fare per aggirarlo? Grazie.
castle-bravo,

1
Hai un sacco di liste e stringhe molto piccole, che è più o meno il caso peggiore per l'utilizzo della memoria in Python (e il passaggio intermedio ".split (). Tolist ()" produce oggetti Python puri). Quello che probabilmente farei al posto tuo sarebbe quello di scaricare il DataFrame in un file e quindi aprirlo come CSV con read_csv (..., sep = ''). Ma per rimanere in tema: la prima soluzione (insieme alla terza, che tuttavia dovrebbe essere terribilmente lenta) potrebbe essere quella che ti offre il minor utilizzo di memoria tra le 4, dato che hai un numero relativamente piccolo di file relativamente lunghe.
Pietro Battiston,

Ehi Pietro, ho provato il tuo suggerimento di salvare su un file e ricaricarlo, e ha funzionato abbastanza bene. Mi sono imbattuto in alcuni problemi quando ho provato a farlo in un oggetto StringIO e una bella soluzione al mio problema è stata pubblicata qui .
castle-bravo,

3
Il tuo ultimo suggerimento tolist()è perfetto. Nel mio caso volevo solo uno dei dati nell'elenco e sono stato in grado di aggiungere direttamente una singola colonna al mio df esistente usando .ix:df['newCol'] = pd.DataFrame(df.col.str.split().tolist()).ix[:,2]
fantabolous,

Ah, stavo avendo problemi a far funzionare tutto questo inizialmente - qualcosa per obect of type 'float' has no len()cui era sconcertante, fino a quando ho realizzato che alcune delle mie file avevano NaNin esse, al contrario str.
dwanderson,

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

Un'altra soluzione simile con il concatenamento è l'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 nella colonna NON ci sono NaNvalori, la soluzione più veloce è usare la listcomprensione con il DataFramecostruttore:

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

Ma se la colonna contiene NaNfunziona solo str.splitcon un parametro expand=Trueche restituisce DataFrame( documentazione ) e spiega perché è più 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

Forse vale la pena ricordare che è necessariamente necessaria l' expand=Trueopzione con cui lavorare pandas.DataFramesdurante l'utilizzo, .str.split()ad esempio.
holzkohlengrill,

@holzkohlengrill - grazie per il commento, lo aggiungo per rispondere.
jezrael,

@jezrael, mi ci vuole molto tempo per eseguire questo codice, è quello previsto. Come lo faccio esattamente più velocemente? Se lo metto in un ciclo for come: for x in df [Seablocks] [: 100] per farlo solo su un sottoinsieme e poi concatenarlo su questi sottoinsiemi, funzionerà?
bernando_vialli,

2

Un altro approccio sarebbe come questo:

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

1

Può anche usare groupby () senza bisogno di unirsi e impilare ().

Usa i dati di esempio sopra:

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

Grazie in anticipo. Come ho potuto usare il codice sopra dividendo due colonne in modo corrispondente. Ad esempio: 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B .. Il risultato dovrebbe essere: 0 31316 Lennon, John 25 F01 300 1:13:36:1,12 Ae riga successiva 0 31316 Lennon, John 25 F01 300 1:13:37:1,13 B
Krithi.S

@ Krithi.S, provo a capire la domanda. Vuoi dire che le due colonne devono avere lo stesso numero di membri dopo la divisione? Quali sono i risultati previsti per 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B, C?
Ben2018,

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.