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.


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.


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

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 NIE są NaNwartoś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

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)

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

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.