pandas: Bagaimana cara membagi teks dalam satu kolom menjadi beberapa baris?

137

Saya bekerja dengan file csv besar dan di sebelah kolom terakhir memiliki string teks yang ingin saya pisahkan dengan pembatas tertentu. Saya bertanya-tanya apakah ada cara sederhana untuk melakukan ini menggunakan panda atau 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

Saya ingin membagi spasi (' ')dan kemudian titik dua (':')di Seatblockskolom, tetapi setiap sel akan menghasilkan jumlah kolom yang berbeda. Saya memiliki fungsi untuk mengatur ulang kolom sehingga Seatblockskolom berada di akhir lembar, tetapi saya tidak yakin apa yang harus dilakukan dari sana. Saya dapat melakukannya di excel dengan text-to-columnsfungsi bawaan dan makro cepat, tetapi kumpulan data saya memiliki terlalu banyak catatan untuk ditangani oleh excel.

Pada akhirnya, saya ingin membuat catatan seperti John Lennon dan membuat beberapa baris, dengan info dari setiap set kursi di baris terpisah.

Bradley
sumber
pertanyaan bagus ini terkait dengan FlatMap di panda, yang saat ini tidak ada
cdarlint

Jawaban:

211

Ini membagi Seatblock dengan ruang dan memberikan barisnya masing-masing.

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

Atau, untuk memberikan setiap string yang dipisahkan oleh titik dua di kolomnya sendiri:

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

Ini sedikit jelek, tapi mungkin seseorang akan setuju dengan solusi yang lebih cantik.

Dan Allan
sumber
7
@DanAllan memberikan indeks ke Seri saat Anda menerapkan; mereka akan menjadi nama kolom
Jeff
4
Meskipun ini menjawab pertanyaan, perlu disebutkan bahwa (mungkin) split () membuat daftar untuk setiap baris, yang memperbesar ukuran dengan DataFramesangat cepat. Dalam kasus saya, menjalankan kode pada tabel ~ 200M menghasilkan penggunaan ~ 10G memori (+ swap ...).
David Nemeskey
1
Meskipun saya tidak yakin itu karena split(), karena hanya reduce()melalui kolom berfungsi seperti pesona. Masalahnya mungkin terletak pada stack()...
David Nemeskey
5
Saya mendapatkan kesalahan NameError: name 'Series' is not defineduntuk ini. darimana Seriesasalnya? EDIT: tidak apa-apa, seharusnya pandas.Serieskarena ini mengacu pada item daripandas
user5359531
2
Ya, @ user5359531. I from pandas import Seriesuntuk kenyamanan / singkatnya.
Dan Allan
54

Beda dari Dan, saya anggap jawabannya cukup elegan ... tapi sayangnya itu juga sangat tidak efisien. Jadi, karena pertanyaan menyebutkan "file csv besar" , izinkan saya menyarankan untuk mencoba solusi shell 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()"

... dibandingkan dengan alternatif ini:

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

... dan ini:

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

Yang kedua hanya menahan diri dari mengalokasikan 100.000 Seri, dan ini cukup untuk membuatnya sekitar 10 kali lebih cepat. Tetapi solusi ketiga, yang ironisnya membuang banyak panggilan ke str.split () (ini disebut sekali per kolom per baris, jadi tiga kali lebih banyak daripada dua solusi lainnya), adalah sekitar 40 kali lebih cepat dari yang pertama, karena bahkan menghindari untuk memasukkan 100 000 daftar. Dan ya, ini pasti sedikit jelek ...

EDIT: jawaban ini menyarankan cara menggunakan "to_list ()" dan untuk menghindari kebutuhan lambda. Hasilnya seperti ini

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

yang bahkan lebih efisien daripada solusi ketiga, dan tentunya jauh lebih elegan.

EDIT: bahkan lebih sederhana

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

bekerja juga, dan hampir seefisien.

EDIT: bahkan lebih sederhana ! Dan menangani NaN (tetapi kurang efisien):

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
sumber
Saya mengalami sedikit masalah dengan jumlah memori yang digunakan metode ini dan saya ingin tahu apakah Anda dapat memberi saya sedikit saran. Saya memiliki DataFrame yang berisi sekitar 8000 baris, masing-masing dengan string berisi bilangan bulat 8-bit 8-bit yang dibatasi spasi 9216. Ini kira-kira 75MB, tetapi ketika saya menerapkan solusi terakhir secara verbatim, Python memakan 2GB memori saya. Dapatkah Anda mengarahkan saya ke suatu sumber yang akan memberi tahu saya mengapa ini terjadi, dan apa yang dapat saya lakukan untuk mengatasinya? Terima kasih.
castle-bravo
1
Anda memiliki banyak daftar dan string yang sangat kecil, yang kurang lebih merupakan kasus terburuk untuk penggunaan memori di python (dan langkah perantara ".split (). Tolist ()" menghasilkan objek python murni). Apa yang mungkin saya lakukan di tempat Anda adalah membuang DataFrame ke file, dan kemudian membukanya sebagai csv dengan read_csv (..., sep = ''). Tetapi untuk tetap pada topik: solusi pertama (bersama dengan yang ketiga, yang bagaimanapun seharusnya sangat lambat) mungkin menawarkan Anda penggunaan memori terendah di antara 4, karena Anda memiliki jumlah baris yang relatif kecil yang relatif panjang.
Pietro Battiston
Hai Pietro, saya mencoba saran Anda untuk menyimpan ke file dan memuat ulang, dan itu bekerja dengan cukup baik. Saya mengalami beberapa masalah ketika saya mencoba melakukan ini di objek StringIO, dan solusi yang bagus untuk masalah saya telah diposting di sini .
castle-bravo
3
Saran terakhir Anda tolist()sempurna. Dalam kasus saya, saya hanya menginginkan salah satu bagian data dalam daftar dan dapat langsung menambahkan satu kolom ke df saya yang ada dengan menggunakan .ix:df['newCol'] = pd.DataFrame(df.col.str.split().tolist()).ix[:,2]
fantabolous
Ahh, saya mengalami kesulitan untuk membuat ini bekerja pada awalnya - sesuatu obect of type 'float' has no len()yang membingungkan, sampai saya menyadari beberapa baris saya ada NaNdi dalamnya, bukan 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

Solusi serupa lainnya dengan rantai adalah penggunaan reset_indexdan 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

Jika dalam kolom TIDAK NaN nilai, solusi tercepat adalah menggunakan listpemahaman dengan DataFramekonstruktor:

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

Tetapi jika kolom berisi NaNhanya berfungsi str.splitdengan parameter expand=Trueyang mengembalikan DataFrame( dokumentasi ), dan itu menjelaskan mengapa lebih lambat:

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
sumber
Mungkin perlu disebutkan bahwa Anda perlu expand=Trueopsi untuk bekerja pandas.DataFramessaat menggunakan .str.split()misalnya.
holzkohlengrill
@holzkohlengrill - terima kasih atas komentarnya, saya tambahkan untuk menjawab.
jezrael
@jezrael, saya butuh waktu lama untuk mengeksekusi kode ini, itu yang diharapkan. Bagaimana tepatnya cara membuatnya lebih cepat? JIKA saya memasukkannya ke dalam loop for seperti: for x di df [Seablocks] [: 100] untuk hanya melakukannya pada subset dan kemudian menggabungkannya pada subset ini, apakah itu akan berhasil?
bernando_vialli
2

Pendekatan lain akan seperti ini:

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

Bisa juga menggunakan groupby () tanpa perlu join dan stack ().

Gunakan contoh data di atas:

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
sumber
Terima kasih sebelumnya. Bagaimana saya bisa menggunakan kode di atas dengan membagi dua kolom secara berurutan. Untuk Contoh: 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B .. Hasilnya harus: 0 31316 Lennon, John 25 F01 300 1:13:36:1,12 Adan baris berikutnya 0 31316 Lennon, John 25 F01 300 1:13:37:1,13 B
Krithi.S
@ Krithi.S, saya mencoba memahami pertanyaan itu. Apakah maksud Anda kedua kolom harus memiliki jumlah anggota yang sama setelah pemisahan? Apa hasil yang Anda harapkan untuk 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B, C?
Ben2018