Панды: Как разделить текст в столбце на несколько строк?

135

Я работаю с большим CSV-файлом, и рядом с последним столбцом есть строка текста, которую я хочу разделить по определенному разделителю. Мне было интересно, если есть простой способ сделать это с помощью панд или питона?

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

Я хочу разделить пробел, (' ')а затем двоеточие (':')в Seatblocksстолбце, но каждая ячейка приведет к разному количеству столбцов. У меня есть функция для перестановки столбцов, чтобы Seatblocksстолбец находился в конце листа, но я не уверен, что делать дальше. Я могу сделать это в Excel с помощью встроенной text-to-columnsфункции и быстрого макроса, но в моем наборе данных слишком много записей для обработки Excel.

В конечном счете, я хочу взять такие записи Джона Леннона и создать несколько строк с информацией о каждом наборе мест на отдельной строке.

Брэдли
источник
этот великий вопрос относится к FlatMap в пандах, которого в настоящее время не существует
cdarlint

Ответы:

203

Это разделяет блоки сидений по пробелам и дает каждому свой ряд.

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

Или, чтобы дать каждой строке, разделенной двоеточием, в своем собственном столбце:

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

Это немного некрасиво, но, возможно, кто-то подойдет к более хорошему решению.

Дэн Аллан
источник
7
@DanAllan дать индекс серии, когда вы подаете заявку; они станут именами столбцов
Джефф
4
Хотя это отвечает на вопрос, стоит упомянуть, что (вероятно) split () создает список для каждой строки, который DataFrameочень быстро увеличивает размер . В моем случае выполнение кода в таблице ~ 200M привело к использованию памяти ~ 10G (+ swap ...).
Дэвид Немески
1
Хотя я не уверен, что это из-за того split(), что простое reduce()прохождение колонки работает как шарм. Тогда проблема может заключаться в stack()...
Дэвид Немески
4
Я получаю ошибку NameError: name 'Series' is not definedза это. откуда Seriesвзяться? РЕДАКТИРОВАТЬ: не имеет значения, это должно быть, pandas.Seriesтак как он ссылается на элемент отpandas
user5359531
2
Да, @ user5359531. Я from pandas import Seriesдля удобства / краткости.
Дэн Аллан
52

В отличие от Дэна, я считаю его ответ довольно элегантным ... но, к сожалению, он также очень и очень неэффективен. Итак, поскольку в вопросе упоминается «большой CSV-файл» , позвольте мне предложить попробовать в оболочке решение Дэна:

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

... по сравнению с этой альтернативой:

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

... и это:

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

Второй просто воздерживается от выделения 100 000 Series, и этого достаточно, чтобы сделать это примерно в 10 раз быстрее. Но третье решение, которое несколько иронично тратит впустую много вызовов str.split () (оно вызывается один раз на столбец на строку, то есть в три раза больше, чем для двух других решений), примерно в 40 раз быстрее, чем первое, потому что он даже избегает экземпляров 100 000 списков. И да, это, конечно, немного некрасиво ...

РЕДАКТИРОВАТЬ: этот ответ предлагает, как использовать "to_list ()" и избежать необходимости лямбда. Результат примерно такой

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

который даже более эффективен, чем третье решение, и, конечно, гораздо более элегантен.

РЕДАКТИРОВАТЬ: еще проще

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

работает тоже, и почти так же эффективно.

РЕДАКТИРОВАТЬ: еще проще ! И обрабатывает NaNs (но менее эффективно):

time python -c "import pandas as pd;
df = pd.DataFrame(['a b c']*100000, columns=['col']);
print df.col.str.split(expand=True).head()"
Пьетро Баттистон
источник
У меня небольшие проблемы с объемом памяти, который потребляет этот метод, и мне интересно, не могли бы вы дать мне небольшой совет. У меня есть DataFrame, который содержит около 8000 строк, каждая из которых содержит строку, содержащую 8 8-разрядных целых чисел, разделенных пробелами. Это примерно 75 МБ, но когда я применяю последнее решение дословно, Python съедает 2 ГБ памяти. Можете ли вы указать мне направление на какой-нибудь источник, который скажет мне, почему это так, и что я могу сделать, чтобы обойти это? Спасибо.
замок-браво
1
У вас есть много списков и очень маленьких строк, что является более или менее худшим случаем использования памяти в python (и промежуточный шаг ".split (). Tolist ()" создает чистые объекты python). На вашем месте я бы, вероятно, сделал бы сброс DataFrame в файл, а затем открыл его как csv с read_csv (..., sep = ''). Но стоит остановиться на теме: первое решение (вместе с третьим, которое, однако, должно быть очень медленным), может быть тем, которое предлагает вам самое низкое использование памяти среди 4, поскольку у вас относительно небольшое количество относительно длинных строк.
Пьетро Баттистон
Привет, Пьетро, ​​я попробовал твоё предложение сохранить файл и загрузить его заново, но это сработало довольно хорошо. Я столкнулся с некоторыми проблемами, когда попытался сделать это в объекте StringIO, и здесь было опубликовано хорошее решение моей проблемы .
замок-браво
3
Ваше последнее предложение tolist()идеально. В моем случае мне нужна была только одна из частей данных в списке, и я мог напрямую добавить один столбец к моей существующей df, используя .ix:df['newCol'] = pd.DataFrame(df.col.str.split().tolist()).ix[:,2]
fantabolous
Ах, у меня были проблемы с тем, чтобы сначала это сработало - что-то, obect of type 'float' has no len()что сбивало с толку, пока я не понял, что некоторые из моих рядов были NaNв них, в отличие от str.
Двандерсон
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

Другое подобное решение с цепочкой - это использование reset_indexи 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

Если в столбце НЕ указаны NaNзначения, самым быстрым решением является использование listпонимания с DataFrameконструктором:

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

Но если столбец содержит NaNтолько работает str.splitс параметром, expand=Trueкоторый возвращает DataFrame( документация ), и это объясняет, почему он медленнее:

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
источник
Возможно, стоит упомянуть, что вам обязательно понадобится expand=Trueопция, с которой можно работать, например, pandas.DataFramesпри использовании .str.split().
holzkohlengrill
@holzkohlengrill - спасибо за комментарий, я добавляю его в ответ.
Джезраэль
@ Jezrael, это занимает очень много времени, чтобы выполнить этот код, это то, что ожидалось. Как именно я могу сделать это быстрее? Если я поместил его в цикл for, например: для x в df [Seablocks] [: 100] сделать это только на подмножестве, а затем объединить на этих подмножествах, это будет работать?
bernando_vialli
2

Другой подход будет выглядеть так:

temp = df['Seatblocks'].str.split(' ')
data = data.reindex(data.index.repeat(temp.apply(len)))
data['new_Seatblocks'] = np.hstack(temp)
Бхарат Саху
источник
1

Также можно использовать groupby () без необходимости объединения и stack ().

Используйте приведенный выше пример данных:

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
источник
Заранее спасибо. Как я мог использовать приведенный выше код, разделив два столбца соответственно. Например: 0 31316 Леннон, Джон 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B .. Результат должен быть: 0 31316 Lennon, John 25 F01 300 1:13:36:1,12 Aи следующая строка 0 31316 Lennon, John 25 F01 300 1:13:37:1,13 B
Krithi.S
@ Krithi.S, я пытаюсь понять вопрос. Вы хотите сказать, что после разделения столбцы должны содержать одинаковое количество элементов? Каковы ваши ожидаемые результаты для 0 31316 Леннон, Джон 25 F01 300 1: 13: 36: 1,12 1: 13: 37: 1,13 A, B, C?
Ben2018