pandas: Wie teile ich Text in einer Spalte in mehrere Zeilen auf?

135

Ich arbeite mit einer großen CSV-Datei und die vorletzte Spalte enthält eine Textzeichenfolge, die ich durch ein bestimmtes Trennzeichen teilen möchte. Ich habe mich gefragt, ob es einen einfachen Weg gibt, dies mit Pandas oder Python zu tun.

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

Ich möchte durch das Leerzeichen (' ')und dann den Doppelpunkt (':')in der SeatblocksSpalte teilen , aber jede Zelle würde zu einer anderen Anzahl von Spalten führen. Ich habe eine Funktion zum Neuanordnen der Spalten, sodass sich die SeatblocksSpalte am Ende des Blattes befindet, bin mir aber nicht sicher, was ich von dort aus tun soll. Ich kann dies in Excel mit der integrierten text-to-columnsFunktion und einem schnellen Makro tun , aber mein Datensatz enthält zu viele Datensätze, als dass Excel sie verarbeiten könnte.

Letztendlich möchte ich Aufzeichnungen wie die von John Lennon aufnehmen und mehrere Zeilen erstellen, wobei die Informationen von jedem Sitzsatz in einer separaten Zeile stehen.

Bradley
quelle
Diese große Frage bezieht sich auf FlatMap in Pandas, die derzeit nicht existiert
cdarlint

Antworten:

203

Dies teilt die Sitzblöcke nach Raum und gibt jedem seine eigene Reihe.

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

Oder um jedem durch Doppelpunkte getrennten String eine eigene Spalte zu geben:

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

Das ist ein bisschen hässlich, aber vielleicht mischt sich jemand mit einer schöneren Lösung ein.

Dan Allan
quelle
7
@DanAllan gibt der Serie einen Index, wenn Sie sich bewerben. Sie werden Spaltennamen
Jeff
4
Während dies die Frage beantwortet, ist es erwähnenswert, dass (wahrscheinlich) split () eine Liste für jede Zeile erstellt, die die Größe der Zeile DataFramesehr schnell in die Luft sprengt . In meinem Fall führte das Ausführen des Codes in einer ~ 200M-Tabelle zu einer ~ 10G-Speichernutzung (+ Swap ...).
David Nemeskey
1
Obwohl ich nicht sicher bin, ob es daran liegt split(), weil es reduce()wie ein Zauber wirkt , einfach durch die Säule zu gehen. Das Problem kann dann in stack()...
David Nemeskey
4
Ich bekomme den Fehler NameError: name 'Series' is not defineddafür. woher Seriessoll das kommen EDIT: egal, es sollte sein, pandas.Seriesda es sich auf den Artikel vonpandas
user5359531
2
Ja, @ user5359531. Ich from pandas import Seriesfür Bequemlichkeit / Kürze.
Dan Allan
52

Anders als Dan finde ich seine Antwort ziemlich elegant ... aber leider ist sie auch sehr, sehr ineffizient. Da in der Frage "eine große CSV-Datei" erwähnt wurde , möchte ich vorschlagen, eine Shell-Lösung von Dan auszuprobieren:

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

... im Vergleich zu dieser Alternative:

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

... und das:

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

Die zweite verzichtet einfach auf die Zuweisung von 100 000 Serien, und dies reicht aus, um sie etwa zehnmal schneller zu machen. Die dritte Lösung, die ironischerweise viele Aufrufe von str.split () verschwendet (sie wird einmal pro Spalte und Zeile aufgerufen, also dreimal mehr als bei den beiden anderen Lösungen), ist etwa 40-mal schneller als die erste. weil es sogar vermeidet, die 100 000 Listen zu instanziieren. Und ja, es ist sicherlich ein bisschen hässlich ...

BEARBEITEN: Diese Antwort schlägt vor, wie man "to_list ()" verwendet und die Notwendigkeit eines Lambda vermeidet. Das Ergebnis ist so etwas wie

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

Das ist noch effizienter als die dritte Lösung und sicherlich viel eleganter.

EDIT: das noch einfacher

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

funktioniert auch und ist fast genauso effizient.

EDIT: noch einfacher ! Und behandelt NaNs (aber weniger effizient):

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
quelle
Ich habe ein kleines Problem mit der Speichermenge, die diese Methode verbraucht, und ich frage mich, ob Sie mir einen kleinen Rat geben könnten. Ich habe einen DataFrame, der ungefähr 8000 Zeilen enthält, jede mit einer Zeichenfolge, die 9216 durch Leerzeichen getrennte 8-Bit-Ganzzahlen enthält. Das sind ungefähr 75 MB, aber wenn ich die letzte Lösung wörtlich anwende, verbraucht Python 2 GB meines Speichers. Können Sie mich in die Richtung einer Quelle weisen, die mir sagt, warum dies so ist und was ich tun kann, um es zu umgehen? Vielen Dank.
Castle-Bravo
1
Sie haben viele Listen und sehr kleine Zeichenfolgen, was mehr oder weniger der schlechteste Fall für die Speichernutzung in Python ist (und der Zwischenschritt ".split (). Tolist ()" erzeugt reine Python-Objekte). Was ich wahrscheinlich an Ihrer Stelle tun würde, wäre, den DataFrame in eine Datei zu kopieren und ihn dann als csv mit read_csv (..., sep = '') zu öffnen. Aber um beim Thema zu bleiben: Die erste Lösung (zusammen mit der dritten, die jedoch sehr langsam sein sollte) bietet möglicherweise die niedrigste Speichernutzung unter den vier, da Sie eine relativ kleine Anzahl relativ langer Zeilen haben.
Pietro Battiston
Hey Pietro, ich habe Ihren Vorschlag ausprobiert, in einer Datei zu speichern und neu zu laden, und es hat ganz gut funktioniert. Ich hatte einige Probleme, als ich versuchte, dies in einem StringIO-Objekt zu tun, und eine nette Lösung für mein Problem wurde hier veröffentlicht .
Castle-Bravo
3
Ihr letzter Vorschlag von tolist()ist perfekt. In meinem Fall wollte ich nur eines der Daten in der Liste und konnte mit .ix:df['newCol'] = pd.DataFrame(df.col.str.split().tolist()).ix[:,2]
fantastabolous
Ahh, ich hatte zuerst Probleme, dies zum Laufen zu bringen - etwas, obect of type 'float' has no len()das verwirrend war, bis ich merkte, dass einige meiner Reihen NaNim Gegensatz zu ihnen waren 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

Eine andere ähnliche Lösung mit Verkettung ist Verwendung reset_indexund 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

Wenn in der Spalte KEINE NaN Werte enthalten sind, ist die schnellste Lösung das Verwendungsverständnis listmit dem 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

Aber wenn Spalte enthält NaN, funktioniert nur str.splitmit Parametern, expand=Truedie zurückgeben DataFrame( Dokumentation ), und es erklärt, warum es langsamer ist:

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
quelle
Vielleicht ist es erwähnenswert, dass Sie zum Beispiel unbedingt die expand=TrueOption benötigen, mit der Sie arbeiten, pandas.DataFrameswährend Sie sie verwenden .str.split().
holzkohlengrill
@holzkohlengrill - danke für den Kommentar, ich füge ihn zur Antwort hinzu.
Jezrael
@jezrael, ich brauche sehr lange, um diesen Code auszuführen, ist das zu erwarten. Wie genau mache ich es schneller? WENN ich es in eine for-Schleife wie: for x in df [Seablocks] [: 100] setze, um es nur für eine Teilmenge zu tun und dann für diese Teilmengen zu verketten, funktioniert das?
Bernando_vialli
2

Ein anderer Ansatz wäre wie folgt:

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

Kann auch groupby () verwenden, ohne dass Join und Stack () erforderlich sind.

Verwenden Sie die obigen Beispieldaten:

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
quelle
Danke im Voraus. Wie ich den obigen Code verwenden könnte, indem ich zwei Spalten entsprechend aufteile. Zum Beispiel: 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B. Das Ergebnis sollte sein: 0 31316 Lennon, John 25 F01 300 1:13:36:1,12 Aund nächste Zeile 0 31316 Lennon, John 25 F01 300 1:13:37:1,13 B
Krithi.S
@ Krithi.S, ich versuche die Frage zu verstehen. Meinen Sie, dass die beiden Spalten nach dem Teilen die gleiche Anzahl von Mitgliedern haben müssen? Was sind Ihre erwarteten Ergebnisse für 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B, C?
Ben2018
1

Dies scheint eine weitaus einfachere Methode zu sein als die an anderer Stelle in diesem Thread vorgeschlagenen.

geteilte Zeilen im Pandas-Datenrahmen

Timbo
quelle