pandas: ¿Cómo divido el texto de una columna en varias filas?

135

Estoy trabajando con un archivo csv grande y la penúltima columna tiene una cadena de texto que quiero dividir por un delimitador específico. Me preguntaba si hay una manera simple de hacer esto usando pandas o 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

Quiero dividir por el espacio (' ')y luego los dos puntos (':')en la Seatblockscolumna, pero cada celda daría como resultado un número diferente de columnas. Tengo una función para reorganizar las columnas para que la Seatblockscolumna esté al final de la hoja, pero no estoy seguro de qué hacer a partir de ahí. Puedo hacerlo en Excel con la text-to-columnsfunción integrada y una macro rápida, pero mi conjunto de datos tiene demasiados registros para que Excel los maneje.

En última instancia, quiero tomar registros como los de John Lennon y crear varias líneas, con la información de cada conjunto de asientos en una línea separada.

Bradley
fuente
Esta gran pregunta se refiere a FlatMap en pandas, que actualmente no existe
cdarlint

Respuestas:

203

Esto divide los bloques de asiento por espacio y le da a cada uno su propia fila.

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

O, para dar a cada cadena separada por dos puntos en su propia columna:

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

Esto es un poco feo, pero tal vez alguien intervenga con una solución más bonita.

Dan Allan
fuente
77
@DanAllan le da un índice a la Serie cuando realiza la solicitud; se convertirán en nombres de columna
Jeff
44
Si bien esto responde a la pregunta, vale la pena mencionar que (probablemente) split () crea una lista para cada fila, que explota el tamaño de la DataFramemuy rápidamente. En mi caso, ejecutar el código en una tabla de ~ 200M resultó en el uso de ~ 10G de memoria (+ intercambio ...).
David Nemeskey
1
Aunque no estoy seguro de que se deba a eso split(), porque simplemente reduce()atravesar la columna funciona de maravilla. El problema puede estar en stack()...
David Nemeskey
44
Recibo el error NameError: name 'Series' is not definedpara esto. de donde se Seriessupone que debe venir? EDITAR: no importa, debería ser pandas.Seriesya que se refiere al artículo depandas
user5359531
2
Sí, @ user5359531. Yo from pandas import Seriespor conveniencia / brevedad.
Dan Allan
52

A diferencia de Dan, considero que su respuesta es bastante elegante ... pero desafortunadamente también es muy ineficiente. Entonces, dado que la pregunta menciona "un gran archivo csv" , permítame sugerirle que intente en un shell la solución 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()"

... comparado con esta alternativa:

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

... y esto:

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

El segundo simplemente se abstiene de asignar la Serie 100 000, y esto es suficiente para hacerlo alrededor de 10 veces más rápido. Pero la tercera solución, que irónicamente desperdicia muchas llamadas a str.split () (se llama una vez por columna por fila, por lo que es tres veces más que para las otras dos soluciones), es aproximadamente 40 veces más rápida que la primera, porque incluso evita tener una instancia de las 100 000 listas. Y sí, ciertamente es un poco feo ...

EDITAR: esta respuesta sugiere cómo usar "to_list ()" y evitar la necesidad de una lambda. El resultado es algo como

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

que es incluso más eficiente que la tercera solución, y ciertamente mucho más elegante.

EDITAR: el aún más 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()"

también funciona y es casi tan eficiente.

EDITAR: aún más simple ! Y maneja NaNs (pero menos eficiente):

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
fuente
Tengo un pequeño problema con la cantidad de memoria que consume este método y me pregunto si podrías darme un pequeño consejo. Tengo un DataFrame que contiene aproximadamente 8000 filas, cada una con una cadena que contiene 9216 enteros delimitados por espacios de 8 bits. Esto es aproximadamente 75 MB, pero cuando aplico la última solución literalmente, Python consume 2 GB de mi memoria. ¿Puede señalarme en la dirección de alguna fuente que me diga por qué es esto y qué puedo hacer para evitarlo? Gracias.
castillo-bravo
1
Tiene muchas listas y cadenas muy pequeñas, que es más o menos el peor caso para el uso de memoria en python (y el paso intermedio ".split (). Tolist ()" produce objetos de python puros). Lo que probablemente haría en su lugar sería volcar el DataFrame en un archivo y luego abrirlo como csv con read_csv (..., sep = ''). Pero para seguir con el tema: la primera solución (junto con la tercera, que sin embargo debería ser extremadamente lenta) puede ser la que le ofrece el menor uso de memoria entre las 4, ya que tiene un número relativamente pequeño de filas relativamente largas.
Pietro Battiston
Hola Pietro, probé tu sugerencia de guardar en un archivo y volver a cargarlo, y funcionó bastante bien. Me encontré con algunos problemas cuando intenté hacer esto en un objeto StringIO, y una buena solución a mi problema se ha publicado aquí .
castle-bravo
3
Su última sugerencia de tolist()es perfecta. En mi caso, solo quería una de las piezas de datos en la lista y pude agregar directamente una sola columna a mi df existente usando .ix:df['newCol'] = pd.DataFrame(df.col.str.split().tolist()).ix[:,2]
fantabolous
Ahh, tenía problemas para hacer que esto funcionara al principio, algo sobre lo obect of type 'float' has no len()que era desconcertante, hasta que me di cuenta de que algunas de mis filas tenían NaNen ellas, en lugar de hacerlo 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

Otra solución similar con el encadenamiento es el uso reset_indexy 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 en la columna NO hay NaNvalores, la solución más rápida es usar la listcomprensión con el DataFrameconstructor:

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

Pero si la columna contiene NaNsolo funciona str.splitcon el parámetro expand=Trueque devuelve DataFrame( documentación ), y explica por qué es más lento:

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
fuente
Quizás valga la pena mencionar que necesariamente necesita la expand=Trueopción de trabajar pandas.DataFramesmientras se usa, .str.split()por ejemplo.
holzkohlengrill
@holzkohlengrill - gracias por tu comentario, lo agrego para responder.
Israel
@jezrael, me está tomando mucho tiempo ejecutar este código, es lo que esperaba. ¿Cómo lo hago exactamente más rápido? SI lo pongo en un bucle for como: for x en df [Seablocks] [: 100] para hacerlo solo en un subconjunto y luego concatenar en estos subconjuntos, ¿funcionará?
bernando_vialli
2

Otro enfoque sería así:

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

También puede usar groupby () sin necesidad de unirse y apilar ().

Utilice los datos de ejemplo anteriores:

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
fuente
Gracias por adelantado. Cómo podría usar el código anterior dividiendo dos columnas de forma correlativa. Por ejemplo: 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B .. El resultado debería ser: 0 31316 Lennon, John 25 F01 300 1:13:36:1,12 Ay la siguiente línea 0 31316 Lennon, John 25 F01 300 1:13:37:1,13 B
Krithi.S
@ Krithi.S, trato de entender la pregunta. ¿Quiere decir que las dos columnas deben tener el mismo número de miembros después de dividirse? ¿Cuáles son sus resultados esperados para 0 31316 Lennon, John 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B, C?
Ben2018