Разделить (взорвать) pandas ввод строки данных в отдельные строки
У меня есть pandas dataframe
, в котором один столбец текстовых строк содержит значения, разделенные запятыми. Я хочу разбить каждое поле CSV и создать новую строку для каждой записи (предположим, что CSV чисты и их нужно разделить на "," ). Например, a
должен стать b
:
In [7]: a
Out[7]:
var1 var2
0 a,b,c 1
1 d,e,f 2
In [8]: b
Out[8]:
var1 var2
0 a 1
1 b 1
2 c 1
3 d 2
4 e 2
5 f 2
До сих пор я пробовал различные простые функции, но метод .apply
, кажется, принимает только одну строку в качестве возвращаемого значения, когда он используется на оси, и я не могу заставить .transform
работать. Любые предложения будут высоко оценены!
Пример данных:
from pandas import DataFrame
import numpy as np
a = DataFrame([{'var1': 'a,b,c', 'var2': 1},
{'var1': 'd,e,f', 'var2': 2}])
b = DataFrame([{'var1': 'a', 'var2': 1},
{'var1': 'b', 'var2': 1},
{'var1': 'c', 'var2': 1},
{'var1': 'd', 'var2': 2},
{'var1': 'e', 'var2': 2},
{'var1': 'f', 'var2': 2}])
Я знаю, что это не сработает, потому что мы теряем метаданные DataFrame, перейдя через numpy, но это должно дать вам представление о том, что я пытался сделать:
def fun(row):
letters = row['var1']
letters = letters.split(',')
out = np.array([row] * len(letters))
out['var1'] = letters
a['idx'] = range(a.shape[0])
z = a.groupby('idx')
z.transform(fun)
Ответы
Ответ 1
Как насчет чего-то вроде этого:
In [55]: pd.concat([Series(row['var2'], row['var1'].split(','))
for _, row in a.iterrows()]).reset_index()
Out[55]:
index 0
0 a 1
1 b 1
2 c 1
3 d 2
4 e 2
5 f 2
Затем вам просто нужно переименовать столбцы
Ответ 2
UPDATE2: более общая векторизованная функция, которая будет работать для нескольких normal
и нескольких столбцов list
def explode(df, lst_cols, fill_value='', preserve_index=False):
# make sure 'lst_cols' is list-alike
if (lst_cols is not None
and len(lst_cols) > 0
and not isinstance(lst_cols, (list, tuple, np.ndarray, pd.Series))):
lst_cols = [lst_cols]
# all columns except 'lst_cols'
idx_cols = df.columns.difference(lst_cols)
# calculate lengths of lists
lens = df[lst_cols[0]].str.len()
# preserve original index values
idx = np.repeat(df.index.values, lens)
# create "exploded" DF
res = (pd.DataFrame({
col:np.repeat(df[col].values, lens)
for col in idx_cols},
index=idx)
.assign(**{col:np.concatenate(df.loc[lens>0, col].values)
for col in lst_cols}))
# append those rows that have empty lists
if (lens == 0).any():
# at least one list in cells is empty
res = (res.append(df.loc[lens==0, idx_cols], sort=False)
.fillna(fill_value))
# revert the original index order
res = res.sort_index()
# reset index if requested
if not preserve_index:
res = res.reset_index(drop=True)
return res
Демо-версия:
Несколько столбцов list
- все столбцы list
должны иметь одинаковое количество элементов в каждой строке:
In [134]: df
Out[134]:
aaa myid num text
0 10 1 [1, 2, 3] [aa, bb, cc]
1 11 2 [] []
2 12 3 [1, 2] [cc, dd]
3 13 4 [] []
In [135]: explode(df, ['num','text'], fill_value='')
Out[135]:
aaa myid num text
0 10 1 1 aa
1 10 1 2 bb
2 10 1 3 cc
3 11 2
4 12 3 1 cc
5 12 3 2 dd
6 13 4
сохранение исходных значений индекса:
In [136]: explode(df, ['num','text'], fill_value='', preserve_index=True)
Out[136]:
aaa myid num text
0 10 1 1 aa
0 10 1 2 bb
0 10 1 3 cc
1 11 2
2 12 3 1 cc
2 12 3 2 dd
3 13 4
Настроить:
df = pd.DataFrame({
'aaa': {0: 10, 1: 11, 2: 12, 3: 13},
'myid': {0: 1, 1: 2, 2: 3, 3: 4},
'num': {0: [1, 2, 3], 1: [], 2: [1, 2], 3: []},
'text': {0: ['aa', 'bb', 'cc'], 1: [], 2: ['cc', 'dd'], 3: []}
})
Столбец CSV:
In [46]: df
Out[46]:
var1 var2 var3
0 a,b,c 1 XX
1 d,e,f,x,y 2 ZZ
In [47]: explode(df.assign(var1=df.var1.str.split(',')), 'var1')
Out[47]:
var1 var2 var3
0 a 1 XX
1 b 1 XX
2 c 1 XX
3 d 2 ZZ
4 e 2 ZZ
5 f 2 ZZ
6 x 2 ZZ
7 y 2 ZZ
используя этот маленький трюк, мы можем преобразовать CSV-подобный столбец в столбец list
:
In [48]: df.assign(var1=df.var1.str.split(','))
Out[48]:
var1 var2 var3
0 [a, b, c] 1 XX
1 [d, e, f, x, y] 2 ZZ
ОБНОВЛЕНИЕ: универсальный векторизованный подход (будет работать также для нескольких столбцов):
Оригинальный DF:
In [177]: df
Out[177]:
var1 var2 var3
0 a,b,c 1 XX
1 d,e,f,x,y 2 ZZ
Решение:
сначала давайте преобразовать строки CSV в списки:
In [178]: lst_col = 'var1'
In [179]: x = df.assign(**{lst_col:df[lst_col].str.split(',')})
In [180]: x
Out[180]:
var1 var2 var3
0 [a, b, c] 1 XX
1 [d, e, f, x, y] 2 ZZ
Теперь мы можем сделать это:
In [181]: pd.DataFrame({
...: col:np.repeat(x[col].values, x[lst_col].str.len())
...: for col in x.columns.difference([lst_col])
...: }).assign(**{lst_col:np.concatenate(x[lst_col].values)})[x.columns.tolist()]
...:
Out[181]:
var1 var2 var3
0 a 1 XX
1 b 1 XX
2 c 1 XX
3 d 2 ZZ
4 e 2 ZZ
5 f 2 ZZ
6 x 2 ZZ
7 y 2 ZZ
СТАРЫЙ ответ:
Вдохновленный решением @AFinkelstein, я хотел сделать его немного более обобщенным, чтобы его можно было применить к DF с более чем двумя столбцами и так же быстро, а почти так же быстро, как решение AFinkelstein):
In [2]: df = pd.DataFrame(
...: [{'var1': 'a,b,c', 'var2': 1, 'var3': 'XX'},
...: {'var1': 'd,e,f,x,y', 'var2': 2, 'var3': 'ZZ'}]
...: )
In [3]: df
Out[3]:
var1 var2 var3
0 a,b,c 1 XX
1 d,e,f,x,y 2 ZZ
In [4]: (df.set_index(df.columns.drop('var1',1).tolist())
...: .var1.str.split(',', expand=True)
...: .stack()
...: .reset_index()
...: .rename(columns={0:'var1'})
...: .loc[:, df.columns]
...: )
Out[4]:
var1 var2 var3
0 a 1 XX
1 b 1 XX
2 c 1 XX
3 d 2 ZZ
4 e 2 ZZ
5 f 2 ZZ
6 x 2 ZZ
7 y 2 ZZ
Ответ 3
После тяжелых экспериментов, чтобы найти что-то быстрее, чем принятый ответ, я получил это, чтобы работать. Он работал примерно в 100 раз быстрее в наборе данных, на котором я его пробовал.
Если кто-то знает способ сделать это более элегантным, во что бы то ни стало, пожалуйста, измените мой код. Я не мог найти способ, который работает без установки других столбцов, которые вы хотите сохранить в качестве индекса, а затем сбросить индекс и переименовать столбцы, но я бы предположил, что работает что-то еще.
b = DataFrame(a.var1.str.split(',').tolist(), index=a.var2).stack()
b = b.reset_index()[[0, 'var2']] # var1 variable is currently labeled 0
b.columns = ['var1', 'var2'] # renaming var1
Ответ 4
Здесь функция, которую я написал для этой общей задачи. Он более эффективен, чем методы Series
/stack
. Порядок и имена столбцов сохраняются.
def tidy_split(df, column, sep='|', keep=False):
"""
Split the values of a column and expand so the new DataFrame has one split
value per row. Filters rows where the column is missing.
Params
------
df : pandas.DataFrame
dataframe with the column to split and expand
column : str
the column to split and expand
sep : str
the string used to split the column values
keep : bool
whether to retain the presplit value as it own row
Returns
-------
pandas.DataFrame
Returns a dataframe with the same columns as `df`.
"""
indexes = list()
new_values = list()
df = df.dropna(subset=[column])
for i, presplit in enumerate(df[column].astype(str)):
values = presplit.split(sep)
if keep and len(values) > 1:
indexes.append(i)
new_values.append(presplit)
for value in values:
indexes.append(i)
new_values.append(value)
new_df = df.iloc[indexes, :].copy()
new_df[column] = new_values
return new_df
С помощью этой функции исходный вопрос будет таким же простым, как:
tidy_split(a, 'var1', sep=',')
Ответ 5
Аналогичный вопрос: pandas: Как разделить текст в столбце на несколько строк?
Вы можете сделать:
>> a=pd.DataFrame({"var1":"a,b,c d,e,f".split(),"var2":[1,2]})
>> s = a.var1.str.split(",").apply(pd.Series, 1).stack()
>> s.index = s.index.droplevel(-1)
>> del a['var1']
>> a.join(s)
var2 var1
0 1 a
0 1 b
0 1 c
1 2 d
1 2 e
1 2 f
Ответ 6
TL; DR
import pandas as pd
import numpy as np
def explode_str(df, col, sep):
s = df[col]
i = np.arange(len(s)).repeat(s.str.count(sep) + 1)
return df.iloc[i].assign(**{col: sep.join(s).split(sep)})
def explode_list(df, col):
s = df[col]
i = np.arange(len(s)).repeat(s.str.len())
return df.iloc[i].assign(**{col: np.concatenate(s)})
демонстрация
explode_str(a, 'var1', ',')
var1 var2
0 a 1
0 b 1
0 c 1
1 d 2
1 e 2
1 f 2
Позвольте создать новый dataframe d
который имеет списки
d = a.assign(var1=lambda d: d.var1.str.split(','))
explode_list(d, 'var1')
var1 var2
0 a 1
0 b 1
0 c 1
1 d 2
1 e 2
1 f 2
Общие комментарии
Я буду использовать np.arange
с repeat
для создания позиций индекса данных, которые я могу использовать с iloc
.
Вопросы-Ответы
Почему я не использую loc
?
Поскольку индекс может быть не уникальным, и использование loc
вернет каждую строку, которая соответствует запрошенному индексу.
Почему бы вам не использовать атрибуты values
и slice?
При вызове values
, если целая часть фрейма данных находится в одном сплоченном "блоке", Pandas вернет представление массива, который является "блоком". В противном случае Pandas придется собирать новый массив. При мольберке этот массив должен иметь одинаковый тип. Часто это означает возврат массива с dtype, который является object
. Используя iloc
вместо сокращения атрибута values
, я iloc
от необходимости иметь дело с этим.
Почему вы используете assign
?
Когда я использую assign
используя одно и то же имя столбца, которое я взрываю, я перезаписываю существующий столбец и сохраняю его положение в области данных.
Почему значения индекса повторяются?
В силу использования iloc
на повторных позициях полученный индекс показывает тот же повторяющийся шаблон. Один повтор для каждого элемента списка или строки.
Это можно сбросить с помощью reset_index(drop=True)
Для струнных
Я не хочу преждевременно разбить струны. Поэтому вместо этого я учитываю вхождения аргумента sep
предполагая, что если бы я разделился, длина результирующего списка была бы больше, чем количество разделителей.
Затем я использую этот sep
чтобы join
к строкам, а затем split
.
def explode_str(df, col, sep):
s = df[col]
i = np.arange(len(s)).repeat(s.str.count(sep) + 1)
return df.iloc[i].assign(**{col: sep.join(s).split(sep)})
Для списков
Подобно тому, как для строк, за исключением того, что мне не нужно подсчитывать вхождения sep
потому что он уже разделен.
Я использую concatenate
Numpy, чтобы замять списки вместе.
import pandas as pd
import numpy as np
def explode_list(df, col):
s = df[col]
i = np.arange(len(s)).repeat(s.str.len())
return df.iloc[i].assign(**{col: np.concatenate(s)})
Ответ 7
Панды> = 0,25
Методы Series и DataFrame определяют метод .explode()
, который разбивает списки на отдельные строки. См. раздел "Документы" в разделе Развертывание в виде столбца.
Поскольку у вас есть список строк, разделенных запятыми, разбейте строку на запятую, чтобы получить список элементов, а затем вызовите explode
для этого столбца.
df = pd.DataFrame({'var1': ['a,b,c', 'd,e,f'], 'var2': [1, 2]})
df
var1 var2
0 a,b,c 1
1 d,e,f 2
df.assign(var1=df['var1'].str.split(',')).explode('var1')
var1 var2
0 a 1
0 b 1
0 c 1
1 d 2
1 e 2
1 f 2
Обратите внимание, что explode
работает только с одним столбцом (пока).
NaNs и пустые списки получают лечение, которого они заслуживают, без необходимости прыгать через обручи, чтобы сделать это правильно.
df = pd.DataFrame({'var1': ['d,e,f', '', np.nan], 'var2': [1, 2, 3]})
df
var1 var2
0 d,e,f 1
1 2
2 NaN 3
df['var1'].str.split(',')
0 [d, e, f]
1 []
2 NaN
df.assign(var1=df['var1'].str.split(',')).explode('var1')
var1 var2
0 d 1
0 e 1
0 f 1
1 2 # empty list entry becomes empty string after exploding
2 NaN 3 # NaN left un-touched
Это серьезное преимущество перед решениями ravel
+ repeat
-based (которые полностью игнорируют пустые списки и задыхаются от NaN).
Ответ 8
Я придумал решение для dataframes с произвольным количеством столбцов (в то же время только разделяя по одному столбцу за раз).
def splitDataFrameList(df,target_column,separator):
''' df = dataframe to split,
target_column = the column containing the values to split
separator = the symbol used to perform the split
returns: a dataframe with each entry for the target column separated, with each element moved into a new row.
The values in the other columns are duplicated across the newly divided rows.
'''
def splitListToRows(row,row_accumulator,target_column,separator):
split_row = row[target_column].split(separator)
for s in split_row:
new_row = row.to_dict()
new_row[target_column] = s
row_accumulator.append(new_row)
new_rows = []
df.apply(splitListToRows,axis=1,args = (new_rows,target_column,separator))
new_df = pandas.DataFrame(new_rows)
return new_df
Ответ 9
Вот довольно простое сообщение, которое использует метод split
из pandas str
accessor, а затем использует NumPy для выравнивания каждой строки в один массив.
Соответствующие значения извлекаются путем повторения несоразмерного столбца правильное количество раз с помощью np.repeat
.
var1 = df.var1.str.split(',', expand=True).values.ravel()
var2 = np.repeat(df.var2.values, len(var1) / len(df))
pd.DataFrame({'var1': var1,
'var2': var2})
var1 var2
0 a 1
1 b 1
2 c 1
3 d 2
4 e 2
5 f 2
Ответ 10
Строковая функция split может принимать логический аргумент option "expand".
Вот решение, использующее этот аргумент:
(a.var1
.str.split(",",expand=True)
.set_index(a.var2)
.stack()
.reset_index(level=1, drop=True)
.reset_index()
.rename(columns={0:"var1"}))
Ответ 11
Я боролся с опытом нехватки памяти, используя различные способы взорвать мои списки, поэтому я подготовил несколько тестов, чтобы помочь мне решить, какие ответы на upvote. Я протестировал пять сценариев с различными пропорциями длины списка к количеству списков. Поделиться результатами ниже:
Время: (чем меньше, тем лучше, нажмите, чтобы посмотреть большую версию)
![Speed]()
Пиковое использование памяти: (чем меньше, тем лучше)
![Peak memory usage]()
Выводы:
- @MaxU ответ (обновление 2), кодовое имя concatenate предлагает лучшую скорость почти в каждом случае, сохраняя при этом низкое использование оперативной памяти,
- см. ответ @DMulligan (стек кодовых имен), если вам нужно обрабатывать много строк с относительно небольшими списками и позволить себе увеличить пиковую память,
- Принятый ответ @Chang хорошо работает для фреймов данных, которые имеют несколько строк, но очень большие списки.
Полная информация (функции и код бенчмаркинга) находится в этой сути GitHub. Обратите внимание, что проблема с эталонным тестом была упрощена и не включала разбиение строк в списке - что большинство решений выполнялось аналогичным образом.
Ответ 12
Основываясь на отличном решении @DMulligan, здесь представлена универсальная векторная (без циклов) функция, которая разбивает столбец данных на несколько строк и объединяет его обратно в исходный фрейм. Он также использует большую общую функцию change_column_order
из этого ответа.
def change_column_order(df, col_name, index):
cols = df.columns.tolist()
cols.remove(col_name)
cols.insert(index, col_name)
return df[cols]
def split_df(dataframe, col_name, sep):
orig_col_index = dataframe.columns.tolist().index(col_name)
orig_index_name = dataframe.index.name
orig_columns = dataframe.columns
dataframe = dataframe.reset_index() # we need a natural 0-based index for proper merge
index_col_name = (set(dataframe.columns) - set(orig_columns)).pop()
df_split = pd.DataFrame(
pd.DataFrame(dataframe[col_name].str.split(sep).tolist())
.stack().reset_index(level=1, drop=1), columns=[col_name])
df = dataframe.drop(col_name, axis=1)
df = pd.merge(df, df_split, left_index=True, right_index=True, how='inner')
df = df.set_index(index_col_name)
df.index.name = orig_index_name
# merge adds the column to the last place, so we need to move it back
return change_column_order(df, col_name, orig_col_index)
Пример:
df = pd.DataFrame([['a:b', 1, 4], ['c:d', 2, 5], ['e:f:g:h', 3, 6]],
columns=['Name', 'A', 'B'], index=[10, 12, 13])
df
Name A B
10 a:b 1 4
12 c:d 2 5
13 e:f:g:h 3 6
split_df(df, 'Name', ':')
Name A B
10 a 1 4
10 b 1 4
12 c 2 5
12 d 2 5
13 e 3 6
13 f 3 6
13 g 3 6
13 h 3 6
Обратите внимание, что он сохраняет исходный индекс и порядок столбцов. Он также работает с dataframes, которые имеют несекретный индекс.
Ответ 13
Существует возможность разбивать и разбивать фрейм данных, не меняя структуру фрейма данных
Вход:
var1 var2
0 a,b,c 1
1 d,e,f 2
#Get the indexes which are repetative with the split
df = df.reindex(df.index.repeat(df['var1'].str.split(',').apply(len)))
#Assign the split values to dataframe column
df['var1'] = np.hstack(df['var1'].drop_duplicates().str.split(','))
Out:
var1 var2
0 a 1
0 b 1
0 c 1
1 d 2
1 e 2
1 f 2
Ответ 14
обновленный ответ MaxU с поддержкой MultiIndex
def explode(df, lst_cols, fill_value='', preserve_index=False):
"""
usage:
In [134]: df
Out[134]:
aaa myid num text
0 10 1 [1, 2, 3] [aa, bb, cc]
1 11 2 [] []
2 12 3 [1, 2] [cc, dd]
3 13 4 [] []
In [135]: explode(df, ['num','text'], fill_value='')
Out[135]:
aaa myid num text
0 10 1 1 aa
1 10 1 2 bb
2 10 1 3 cc
3 11 2
4 12 3 1 cc
5 12 3 2 dd
6 13 4
"""
# make sure 'lst_cols' is list-alike
if (lst_cols is not None
and len(lst_cols) > 0
and not isinstance(lst_cols, (list, tuple, np.ndarray, pd.Series))):
lst_cols = [lst_cols]
# all columns except 'lst_cols'
idx_cols = df.columns.difference(lst_cols)
# calculate lengths of lists
lens = df[lst_cols[0]].str.len()
# preserve original index values
idx = np.repeat(df.index.values, lens)
res = (pd.DataFrame({
col:np.repeat(df[col].values, lens)
for col in idx_cols},
index=idx)
.assign(**{col:np.concatenate(df.loc[lens>0, col].values)
for col in lst_cols}))
# append those rows that have empty lists
if (lens == 0).any():
# at least one list in cells is empty
res = (res.append(df.loc[lens==0, idx_cols], sort=False)
.fillna(fill_value))
# revert the original index order
res = res.sort_index()
# reset index if requested
if not preserve_index:
res = res.reset_index(drop=True)
# if original index is MultiIndex build the dataframe from the multiindex
# create "exploded" DF
if isinstance(df.index, pd.MultiIndex):
res = res.reindex(
index=pd.MultiIndex.from_tuples(
res.index,
names=['number', 'color']
)
)
return res
Ответ 15
Я придумал следующее решение этой проблемы:
def iter_var1(d):
for _, row in d.iterrows():
for v in row["var1"].split(","):
yield (v, row["var2"])
new_a = DataFrame.from_records([i for i in iter_var1(a)],
columns=["var1", "var2"])
Ответ 16
Только что использовал jiln отличный ответ сверху, но ему нужно было расширить, чтобы разделить несколько столбцов. Думаю, я бы поделился.
def splitDataFrameList(df,target_column,separator):
''' df = dataframe to split,
target_column = the column containing the values to split
separator = the symbol used to perform the split
returns: a dataframe with each entry for the target column separated, with each element moved into a new row.
The values in the other columns are duplicated across the newly divided rows.
'''
def splitListToRows(row, row_accumulator, target_columns, separator):
split_rows = []
for target_column in target_columns:
split_rows.append(row[target_column].split(separator))
# Seperate for multiple columns
for i in range(len(split_rows[0])):
new_row = row.to_dict()
for j in range(len(split_rows)):
new_row[target_columns[j]] = split_rows[j][i]
row_accumulator.append(new_row)
new_rows = []
df.apply(splitListToRows,axis=1,args = (new_rows,target_column,separator))
new_df = pd.DataFrame(new_rows)
return new_df
Ответ 17
Другое решение, использующее пакет копирования python
import copy
new_observations = list()
def pandas_explode(df, column_to_explode):
new_observations = list()
for row in df.to_dict(orient='records'):
explode_values = row[column_to_explode]
del row[column_to_explode]
if type(explode_values) is list or type(explode_values) is tuple:
for explode_value in explode_values:
new_observation = copy.deepcopy(row)
new_observation[column_to_explode] = explode_value
new_observations.append(new_observation)
else:
new_observation = copy.deepcopy(row)
new_observation[column_to_explode] = explode_values
new_observations.append(new_observation)
return_df = pd.DataFrame(new_observations)
return return_df
df = pandas_explode(df, column_name)
Ответ 18
Следующий подход объединяет новый df с оригинальным.
a.reset_index().merge(
a['var1'].str.split(',').apply(_pd.Series).reset_index().melt('index')[['index', 'value']].dropna()
)[['value', 'var2']].rename({'value':'var1'}, axis = 1)
Ответ 19
Здесь есть много ответов, но я удивлен, что никто не упомянул встроенную функцию разнесения панд. Проверьте ссылку ниже:
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.explode.html#pandas.DataFrame.explode
По какой-то причине я не смог получить доступ к этой функции, поэтому я использовал следующий код:
import pandas_explode
pandas_explode.patch()
df_zlp_people_cnt3 = df_zlp_people_cnt2.explode('people')
![enter image description here]()
Выше приведен образец моих данных. Как видите, в колонке people было несколько человек, и я пытался ее взорвать. Код, который я дал, работает для данных типа списка. Поэтому постарайтесь получить текстовые данные через запятую в виде списка. Кроме того, поскольку в моем коде используются встроенные функции, он работает намного быстрее, чем функции custom/apply.
Примечание: вам может понадобиться установить pandas_explode с pip.