pandy: Jak podzielić tekst w kolumnie na wiele wierszy?

135

Pracuję z dużym plikiem csv, a obok ostatniej kolumny znajduje się ciąg tekstu, który chcę podzielić za pomocą określonego separatora. Zastanawiałem się, czy można to zrobić w prosty sposób za pomocą pand lub Pythona?

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

Chcę podzielić przez spację, (' ')a następnie dwukropek (':')w Seatblockskolumnie, ale każda komórka spowodowałaby inną liczbę kolumn. Mam funkcję zmiany kolejności kolumn, aby Seatblockskolumna znajdowała się na końcu arkusza, ale nie jestem pewien, co dalej robić. Mogę to zrobić w programie Excel z wbudowaną text-to-columnsfunkcją i szybkim makrem, ale mój zbiór danych ma zbyt wiele rekordów, aby program Excel mógł je obsłużyć.

Ostatecznie chcę wziąć rekordy Johna Lennona i utworzyć wiele linii, z informacjami z każdego zestawu miejsc w osobnej linii.

Bradley
źródło
to świetne pytanie dotyczy FlatMap in pandas, która obecnie nie istnieje
cdarlint

Odpowiedzi:

203

Spowoduje to podzielenie bloków siedzenia według przestrzeni i nadanie każdemu osobnego rzędu.

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

Lub, aby umieścić każdy ciąg rozdzielany dwukropkami w osobnej kolumnie:

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

To trochę brzydkie, ale może ktoś wpadnie na ładniejsze rozwiązanie.

Dan Allan
źródło
7
@DanAllan podaj indeks serii podczas składania wniosku; staną się nazwami kolumn
Jeff
4
Chociaż to odpowiada na pytanie, warto wspomnieć, że (prawdopodobnie) split () tworzy listę dla każdego wiersza, co DataFramebardzo szybko powiększa rozmiar . W moim przypadku uruchomienie kodu na ~ 200M tabeli spowodowało użycie pamięci ~ 10G (+ zamiana ...).
David Nemeskey,
1
Chociaż nie jestem pewien, czy to z powodu split(), bo zwykłe reduce()przechodzenie przez kolumnę działa jak urok. Problem może wtedy leżeć w stack()...
David Nemeskey,
4
Otrzymuję błąd NameError: name 'Series' is not definedz tego powodu. skąd Seriesma pochodzić? EDYCJA: nieważne, powinno być, pandas.Seriesponieważ odnosi się do pozycji zpandas
user5359531
2
Tak, @ user5359531. I from pandas import Seriesdla wygody / zwięzłości.
Dan Allan
52

W odróżnieniu od Dana uważam jego odpowiedź za dość elegancką ... ale niestety jest też bardzo, bardzo nieefektywna. Tak więc, ponieważ pytanie dotyczyło „dużego pliku csv” , zasugeruję wypróbowanie rozwiązania powłoki Dana:

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

... w porównaniu z tą alternatywą:

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

... i to:

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

Drugi po prostu powstrzymuje się od przydzielenia 100 000 serii, a to wystarczy, aby zrobić to około 10 razy szybciej. Ale trzecie rozwiązanie, które nieco ironicznie marnuje wiele wywołań funkcji str.split () (jest wywoływane raz na kolumnę w wierszu, czyli trzy razy więcej niż w przypadku pozostałych dwóch rozwiązań), jest około 40 razy szybsze niż pierwsze, ponieważ unika nawet umieszczania 100 000 list. I tak, z pewnością jest trochę brzydki ...

EDYCJA: ta odpowiedź sugeruje, jak używać "to_list ()" i uniknąć potrzeby stosowania lambdy. Wynik jest podobny

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

które jest jeszcze wydajniejsze niż trzecie rozwiązanie, a na pewno dużo bardziej eleganckie.

EDYCJA: jeszcze prostsza

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

działa też i jest prawie tak samo wydajny.

EDYCJA: jeszcze prostsza ! I obsługuje NaN (ale mniej wydajne):

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
źródło
Mam mały problem z ilością pamięci, którą zużywa ta metoda i zastanawiam się, czy mógłbyś dać mi małą radę. Mam DataFrame, która zawiera około 8000 wierszy, każdy z ciągiem zawierającym 9216 8-bitowych liczb całkowitych rozdzielonych spacjami. To około 75 MB, ale kiedy zastosuję ostatnie rozwiązanie dosłownie, Python zjada 2 GB mojej pamięci. Czy możesz wskazać mi jakieś źródło, które powiedziałoby mi, dlaczego tak jest i co mogę zrobić, aby to obejść? Dzięki.
castle-bravo
1
Masz wiele list i bardzo małe ciągi znaków, co jest mniej więcej najgorszym przypadkiem w przypadku użycia pamięci w Pythonie (a krok pośredni „.split (). Tolist ()” tworzy czyste obiekty Pythona). To, co prawdopodobnie zrobiłbym na twoim miejscu, to zrzucić DataFrame do pliku, a następnie otworzyć go jako csv z read_csv (..., sep = ''). Pozostając jednak na temat: pierwsze rozwiązanie (razem z trzecim, które jednak powinno być strasznie wolne) może być tym, które oferuje najmniejsze zużycie pamięci spośród 4, ponieważ masz stosunkowo niewielką liczbę stosunkowo długich wierszy.
Pietro Battiston,
Hej Pietro, wypróbowałem twoją sugestię zapisania do pliku i ponownego załadowania, i zadziałało całkiem nieźle. Wpadłem w kłopoty, gdy próbowałem to zrobić w obiekcie StringIO, i ładne rozwiązanie mojego problemu została zamieszczona tutaj .
castle-bravo
3
Twoja ostatnia sugestia tolist()jest doskonała. W moim przypadku chciałem tylko jeden z fragmentów danych na liście i mogłem bezpośrednio dodać pojedynczą kolumnę do mojego istniejącego df, używając .ix:df['newCol'] = pd.DataFrame(df.col.str.split().tolist()).ix[:,2]
fantabolous
Ach, na początku miałem problemy z uruchomieniem tego - coś, obect of type 'float' has no len()co było zaskakujące, dopóki nie zdałem sobie sprawy, że niektóre z moich awantur mają NaNw nich w przeciwieństwie do 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

Innym podobnym rozwiązaniem w przypadku łączenia jest użycie reset_indexi 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

Jeśli w kolumnie NIENaNwartości, najszybszym rozwiązaniem jest użycie listzrozumienia z DataFramekonstruktorem:

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

Ale jeśli kolumna zawiera NaNdziała tylko str.splitz parametrem, expand=Truektóry zwraca DataFrame( dokumentacja ), i wyjaśnia, dlaczego jest wolniejsza:

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
źródło
Może warto wspomnieć, że koniecznie potrzebujemy expand=Truepracy z opcji pandas.DataFramespodczas korzystania .str.split()np.
holzkohlengrill
@holzkohlengrill - dziękuję za komentarz, dodaję do odpowiedzi.
jezrael
@jezrael, wykonanie tego kodu zajmuje mi bardzo dużo czasu. Jak dokładnie mam to przyspieszyć? JEŚLI wstawię to w pętlę for, taką jak: for x in df [Seablocks] [: 100], aby zrobić to tylko na podzbiorze, a następnie połączyć na tych podzbiorach, czy to zadziała?
bernando_vialli,
2

Inne podejście byłoby takie:

temp = df['Seatblocks'].str.split(' ')
data = data.reindex(data.index.repeat(temp.apply(len)))
data['new_Seatblocks'] = np.hstack(temp)
Bharat Sahu
źródło
1

Może również używać funkcji groupby () bez konieczności łączenia się i stosu ().

Użyj powyższych przykładowych danych:

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
źródło
Z góry dziękuję. Jak mogę użyć powyższego kodu, dzieląc odpowiednio dwie kolumny. Na przykład: 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B .. Wynik powinien wyglądać następująco: 0 31316 Lennon, John 25 F01 300 1:13:36:1,12 Ai następna linia 0 31316 Lennon, John 25 F01 300 1:13:37:1,13 B
Krithi.S
@ Krithi.S, próbuję zrozumieć pytanie. Czy masz na myśli to, że dwie kolumny muszą mieć taką samą liczbę członków po podzieleniu? Jakie są twoje oczekiwane wyniki dla 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B, C?
Ben2018