pandas: Comment diviser le texte d'une colonne en plusieurs lignes?

135

Je travaille avec un gros fichier csv et l'avant-dernière colonne contient une chaîne de texte que je souhaite diviser par un délimiteur spécifique. Je me demandais s'il existe un moyen simple de le faire en utilisant des pandas ou 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

Je veux diviser par l'espace (' '), puis les deux points (':')dans la Seatblockscolonne, mais chaque cellule entraînerait un nombre différent de colonnes. J'ai une fonction pour réorganiser les colonnes afin que la Seatblockscolonne soit à la fin de la feuille, mais je ne sais pas quoi faire à partir de là. Je peux le faire dans Excel avec la text-to-columnsfonction intégrée et une macro rapide, mais mon ensemble de données contient trop d'enregistrements pour Excel à gérer.

En fin de compte, je veux prendre des enregistrements tels que ceux de John Lennon et créer plusieurs lignes, avec les informations de chaque ensemble de sièges sur une ligne distincte.

Bradley
la source
cette grande question concerne FlatMap dans les pandas, qui n'existe actuellement pas
cdarlint

Réponses:

203

Cela divise les sièges par espace et donne à chacun sa propre rangée.

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

Ou, pour donner à chaque chaîne séparée par deux-points dans sa propre colonne:

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

C'est un peu moche, mais peut-être que quelqu'un vous proposera une solution plus jolie.

Dan Allan
la source
7
@DanAllan donne un index à la série lorsque vous postulez; ils deviendront des noms de colonnes
Jeff
4
Bien que cela réponde à la question, il convient de mentionner que (probablement) split () crée une liste pour chaque ligne, ce qui augmente la taille du DataFramefichier très rapidement. Dans mon cas, exécuter le code sur une table de ~ 200M a entraîné une utilisation de la mémoire ~ 10G (+ swap ...).
David Nemeskey
1
Bien que je ne sois pas sûr que ce soit à cause de split(), parce que le simple fait reduce()de passer à travers la colonne fonctionne comme un charme. Le problème peut alors résider dans stack()...
David Nemeskey
4
J'obtiens l'erreur NameError: name 'Series' is not definedpour cela. d'où est Seriescensé venir? EDIT: tant pis, ça devrait l'être pandas.Seriescar il fait référence à l'article depandas
user5359531
2
Oui, @ user5359531. I from pandas import Seriespar commodité / brièveté.
Dan Allan
52

Contrairement à Dan, je trouve sa réponse assez élégante ... mais malheureusement elle est aussi très très inefficace. Donc, puisque la question mentionnait "un gros fichier csv" , laissez-moi vous suggérer d'essayer dans un shell la solution de 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()"

... par rapport à cette 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()"

... et ça:

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

Le second s'abstient simplement d'allouer 100 000 séries, et cela suffit pour le rendre environ 10 fois plus rapide. Mais la troisième solution, qui gaspille quelque peu ironiquement beaucoup d'appels à str.split () (elle est appelée une fois par colonne par ligne, donc trois fois plus que pour les deux autres solutions), est environ 40 fois plus rapide que la première, car cela évite même d'instanceser les 100 000 listes. Et oui, c'est certainement un peu moche ...

EDIT: cette réponse suggère comment utiliser "to_list ()" et éviter d'avoir besoin d'un lambda. Le résultat est quelque chose comme

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

qui est encore plus efficace que la troisième solution, et certainement beaucoup plus élégante.

EDIT: le plus simple

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

fonctionne aussi et est presque aussi efficace.

EDIT: encore plus simple ! Et gère les NaN (mais moins efficaces):

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
la source
J'ai un peu de mal avec la quantité de mémoire que cette méthode consomme et je me demande si vous pourriez me donner un petit conseil. J'ai un DataFrame qui contient environ 8000 lignes, chacune avec une chaîne contenant 9216 entiers 8 bits délimités par des espaces. Cela fait environ 75 Mo, mais lorsque j'applique la dernière solution mot pour mot, Python mange 2 Go de ma mémoire. Pouvez-vous m'indiquer une source qui me dirait pourquoi et ce que je peux faire pour contourner le problème? Merci.
castle-bravo
1
Vous avez beaucoup de listes et de très petites chaînes, ce qui est plus ou moins le pire des cas d'utilisation de la mémoire en python (et l'étape intermédiaire ".split (). Tolist ()" produit des objets python purs). Ce que je ferais probablement à votre place serait de vider le DataFrame dans un fichier, puis de l'ouvrir en tant que csv avec read_csv (..., sep = ''). Mais pour rester sur le sujet: la première solution (avec la troisième, qui devrait cependant être terriblement lente) peut être celle qui vous offre la plus faible utilisation de la mémoire parmi les 4, car vous avez un nombre relativement petit de lignes relativement longues.
Pietro Battiston
Hey Pietro, j'ai essayé votre suggestion d'enregistrer dans un fichier et de le recharger, et cela a très bien fonctionné. J'ai rencontré des problèmes lorsque j'ai essayé de le faire dans un objet StringIO, et une belle solution à mon problème a été publiée ici .
castle-bravo
3
Votre dernière suggestion tolist()est parfaite. Dans mon cas, je ne voulais qu'une des données de la liste et j'ai pu ajouter directement une seule colonne à mon df existant en utilisant .ix:df['newCol'] = pd.DataFrame(df.col.str.split().tolist()).ix[:,2]
fantastique
Ahh, j'avais du mal à faire fonctionner cela au début - quelque chose à propos de obect of type 'float' has no len()qui était déconcertant, jusqu'à ce que je réalise que certaines de mes lignes avaient NaNen eux, par opposition à 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

Une autre solution similaire avec le chaînage est l'utilisation reset_indexet 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

Si dans la colonne ne sont PAS des NaN valeurs, la solution la plus rapide est d'utiliser la listcompréhension avec le DataFrameconstructeur:

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

Mais si la colonne contient NaNne fonctionne str.splitqu'avec le paramètre expand=Truequi retourne DataFrame( documentation ), et cela explique pourquoi il est plus lent:

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
la source
Peut - être vaut la peine de mentionner ce que vous avez besoin nécessairement le expand=Truetravail d'option avec pandas.DataFramestout en utilisant .str.split()par exemple.
holzkohlengrill
@holzkohlengrill - merci pour le commentaire, je l'ajoute pour répondre.
jezrael le
@jezrael, il me faut très longtemps pour exécuter ce code, est-ce prévu. Comment puis-je le rendre plus rapide? SI je le mets dans une boucle for comme: for x in df [Seablocks] [: 100] pour le faire uniquement sur un sous-ensemble puis concaténer sur ces sous-ensembles, cela fonctionnera-t-il?
bernando_vialli
2

Une autre approche serait comme ceci:

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

Peut également utiliser groupby () sans avoir besoin de rejoindre et stack ().

Utilisez les données d'exemple ci-dessus:

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
la source
Merci d'avance. Comment je pourrais utiliser le code ci-dessus en divisant deux colonnes en conséquence. Par exemple: 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B .. Le résultat doit être: 0 31316 Lennon, John 25 F01 300 1:13:36:1,12 Aet ligne suivante 0 31316 Lennon, John 25 F01 300 1:13:37:1,13 B
Krithi.S
@ Krithi.S, j'essaye de comprendre la question. Voulez-vous dire que les deux colonnes doivent avoir le même nombre de membres après la séparation? Quels sont vos résultats attendus pour 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B, C?
Ben2018