Разделение строковых ячеек двух столбцов pandas dataframe с несовпадающим количеством составных элементов
Есть:
df = pd.DataFrame({'Program': ['RG', 'RG', 'Set'],
'Number': [0, 1, 2],
'Id': [['100; 500'], ['100; 500'], 1],
'Values': [0.7, ['0.9; 0.5'], 0.4]
})
Необходимо получить:
df = pd.DataFrame({'Program': ['RG', 'RG', 'RG', 'RG', 'Set'],
'Number': [0, 0, 1, 1, 2],
'Id': [100, 500, 100, 500, 1],
'Values': [0.7, 0, 0.9, 0.5, 0.4]
})
Если разбивать каждую ячейку по разделителю отдельно, то будет присвоение значений 'Values' к 'Id', которых быть не должно и увеличивается количество строк из-за этого:
df['Id'] = df['Id'].str.split(';')
df = df.explode('Id')
df['Values'] = df['Values'].str.split(';')
df = df.explode('Values')
Как разбить df, чтобы не потерять привязку 'Id' к 'Values' и лишние строки не добавлять?
Ответы (2 шт):
Вот, при такой исходной структуре
df = pd.DataFrame({'Program': ['RG', 'RG', 'Set'],
'Number': [0, 1, 2],
'Id': [[100, 500], [100, 500], 1],
'Values': [[0.7, 0], [0.9, 0.5], 0.4]
})
df = df.explode(['Id', 'Values'])
получается, как вы хотите
Program Number Id Values
0 RG 0 100 0.7
0 RG 0 500 0
1 RG 1 100 0.9
1 RG 1 500 0.5
2 Set 2 1 0.4
Но для этого, строковое представление списков надо превратить в реальные списки и сделать их равной длины. Но для такой операции, мне кажется, в pandas нет встроенного механизма
pandas 2.2.3
Предположим, что в интересующих колонках ['Id', 'Values'] находятся данные двух типов: либо число, либо строка внутри списка, представляющая собой перечень чисел, разделенных точкой с запятой:
df = pd.DataFrame(
{
"Program": ["RG", "RG", "SET", "BAR"],
"Number": [10, 20, 30, 40],
"Id": [["100; 200"], ["300; 400"], 1, 5],
"Values": [0.7, ["0.9; 0.5"], 0.4, ["0.1; 0.2; 0.3"]],
}
)
# Program Number Id Values
# 0 'RG' 10 ['100; 200'] 0.7
# 1 'RG' 20 ['300; 400'] ['0.9; 0.5']
# 2 'SET' 30 1 0.4
# 3 'BAR' 40 5 ['0.1; 0.2; 0.3']
Сначала нужно привести данные к одному виду. В данном случае это будут строки:
tied_cols = ['Id', 'Values']
data = df.drop(columns=tied_cols).assign(
Id = df['Id'].explode().astype(str),
Values = df['Values'].explode().astype(str),
)
# Program Number Id Values
# 0 'RG' 10 '100; 200' '0.7'
# 1 'RG' 20 '300; 400' '0.9; 0.5'
# 2 'SET' 30 '1' '0.4'
# 3 'BAR' 40 '5' '0.1; 0.2; 0.3'
Теперь есть минимум два пути:
Мы можем применить
explodeодновременно к обоим столбцам, предварительно позаботившись, чтобы совпадало количество элементов в ячейках одной строки согласно требованию метода: "...their list-like data on same row of the frame must have matching length".Расщепить столбцы отдельно и соединить результат с помощью
join,concatилиmerge, предварительно добавив внутреннюю нумерацию по каждому исходному индексу данных (после расщепления индекс строки будет продублирован для каждого элемента исходной ячейки, и эти стоки мы дополнительно нумеруем начиная от нуля).
Первый подход можем реализовать, добавив, например, недостающие хвосты к строкам перед расщеплением:
sep = '; '
answer = data.drop(columns=tied_cols).join(
data[tied_cols].add(
data[tied_cols]
.map(lambda s: s.count(sep))
.apply(lambda rec: rec.max() - rec, axis='columns')
.map(lambda x: f'{sep}0'*x)
)
.map(lambda x: x.split(sep))
.explode(tied_cols)
.astype({'Id': int, 'Values': float})
)
# Program Number Id Values
# 0 'RG' 10 100 0.7
# 0 'RG' 10 200 0.0
# 1 'RG' 20 300 0.9
# 1 'RG' 20 400 0.5
# 2 'SET' 30 1 0.4
# 3 'BAR' 40 5 0.1
# 3 'BAR' 40 0 0.2
# 3 'BAR' 40 0 0.3
Второй подход даёт возможность заполнить недостающие звенья после соединения:
def split_and_explode(column: pd.Series, sep=None):
x = column.str.split(sep).explode().to_frame()
in_group_index = x.groupby(level=0).cumcount()
return x.set_index(in_group_index, append=True)
print(pd.concat(
[
split_and_explode(data["Id"], sep),
split_and_explode(data["Values"], sep),
],
axis="columns",
))
# Id Values
# 0 0 '100' '0.7'
# 1 '200' NaN
# 1 0 '300' '0.9'
# 1 '400' '0.5'
# 2 0 '1' '0.4'
# 3 0 '5' '0.1'
# 1 NaN '0.2'
# 2 NaN '0.3'
# ^^^
# Обратите внимание на дополнительный индекс
# на втором уровне, благодаря которому происходит
# правильная последовательность соединения
Если недостающие звенья действительно должны быть нулями, применяем fillna(0), ну а если предполагается, что одно значение распространяется на всю группу, то ffill(). Соединение с основным телом данных производим через join, предварительно сбросив вспомогательный индекс и сохранив исходный:
answer = data.drop(columns=tied_cols).join(
pd.concat(
[split_and_explode(data["Id"], sep), split_and_explode(data["Values"], sep)],
axis="columns",
)
.reset_index(level=-1, drop=True)
.fillna('0')
.astype({"Id": int, "Values": float})
)
# Program Number Id Values
# 0 'RG' 10 100 0.7
# 0 'RG' 10 200 0.0
# 1 'RG' 20 300 0.9
# 1 'RG' 20 400 0.5
# 2 'SET' 30 1 0.4
# 3 'BAR' 40 5 0.1
# 3 'BAR' 40 0 0.2
# 3 'BAR' 40 0 0.3
Важно! Позаботьтесь о том, чтобы строки входной таблицы имели уникальные индексы. Это нужно для правильного группирования развернутых ячеек и корректной работы join.