#python #pandas #dataframe
Вопрос:
Я пытаюсь построить функцию сравнения между старой и новой версиями таблицы. Единственная константа-это количество и название столбцов. Количество строк может измениться при появлении новых записей в новой версии таблицы. Результатом должна быть обновленная таблица, в которой новые записи принимаются в качестве дополнительных строк, а удаленные записи должны оставаться в таблице и помечаться как «неактивные» в созданном для этой цели столбце (новые записи помечаются как «новые»). Могут быть изменения одной записи подряд. Для этого строка должна быть обновлена, а измененное значение должно быть «старое значение новое значение», и строка должна быть помечена как «отредактированная».
Мне нужно использовать панд для этой цели, и я попробовал merge(frame_old, frame_new, how='outer', indicator=True)
. Здесь я автоматически создаю столбец статуса (содержащий «новый», «отредактированный» и «неактивный») -> > _merged
. Но, естественно, нет никаких функций для распознавания отредактированных записей …
Потому что проще объяснить функцию на примере, который я подготовил:
Стол старый
import pandas as pd
dict_old= {
'ID' : ["IQL_000", "SID_002", "OLA_056"],
'Text' : ["foo", "hello", "a descirption"],
'Number' : ["0991", "1231", "333"]
}
frame_old= pd.DataFrame(dict_old)
Таблица новая
import pandas as pd
dict_new= {
'ID' : ["IQL_000", "SID_002", "PPA_344"],
'Text' : ["foo", "world", "nothing"],
'Number' : ["0991", "1231", "978"]
}
frame_new= pd.DataFrame(dict_new)
Таблица обновлена
import pandas as pd
dict_updated= {
'ID' : ["IQL_000", "SID_002", "OLA_056", "PPA_344"],
'Text' : ["foo", "OLD:hello, NEW:world", "a descirption", "nothing"],
'Number' : ["0991", "1231", "333", "978"],
'Status' : ["active", "edited", "inactive", "new"]
}
frame_updated= pd.DataFrame(dict_updated)
Комментарии:
1. Если вы укажете
on="ID"
во время слияния, вы получите дваText
столбца, которые можно сравнить.
Ответ №1:
- Используется
pandas.merge
для объединения старой и новой таблиц. groupby
в столбце «Идентификатор» и используйте пользовательскиеagg
функции.- Назначьте необходимый «Статус» с помощью
map
.
frame_old = pd.DataFrame({'ID' : ["IQL_000", "SID_002", "OLA_056"],'Text' : ["foo", "hello", "a descirption"],'Number' : ["0991", "1231", "333"]})
frame_new = pd.DataFrame({'ID' : ["IQL_000", "SID_002", "PPA_344"], 'Text' : ["foo", "world", "nothing"], 'Number' : ["0991", "1231", "978"]})
merged = pd.merge(frame_old, frame_new, indicator=True, how="outer", suffixes=("_old", "_new"))
output = merged.groupby("ID").agg({"Text": lambda x: f"OLD: {x.iat[0]} NEW: {x.iat[-1]}" if len(x.unique())>1 else x,
"Number": lambda x: f"OLD: {x.iat[0]} NEW: {x.iat[-1]}" if len(x.unique())>1 else x.iat[-1],
"_merge": lambda x: x if len(x.unique())==1 else "edited"})
output["Status"] = output["_merge"].map({"left_only": "inactive",
"right_only": "new",
"both": "active",
"edited": "edited"})
output = output[["Text", "Number", "Status"]].reset_index()
>>> output
ID Text Number Status
0 IQL_000 foo 0991 active
1 OLA_056 a descirption 333 inactive
2 PPA_344 nothing 978 new
3 SID_002 OLD: hello NEW: world 1231 edited
Комментарии:
1. Спасибо вам за ваш ответ! Хотя значение числа не было изменено, изменение в этом столбце также возможно. Когда я применяю ту же лямбда-функцию для Numer вместо
"last"
, я получаю следующий вывод для отредактированной строки:3 SID_002 OLD: hello NEW: world [] edited
. Примечание: Я еще не изменил ни одного номера.2. @Pm740 — Я отредактировал свой ответ, чтобы учесть и это тоже!
Ответ №2:
С вашего DataFrames
, мы можем начать с применения merge
к ним (здесь мы показываем индикатор, чтобы быть уверенными в происхождении данных) :
>>> frame_merged = frame_old.merge(frame_new, how='outer', indicator=True)
>>> frame_merged
ID Text Number _merge
0 IQL_000 foo 0991 both
1 SID_002 hello 1231 left_only
2 OLA_056 a descirption 333 left_only
3 SID_002 world 1231 right_only
4 PPA_344 nothing 978 right_only
Затем, используя groupby
примененное к list
, мы получили ожидаемый результат, зная, что последний элемент списка является текущим значением :
>>> frame_text = frame_merged.groupby(['ID'])['Text'].apply(list).reset_index()
>>> frame_text
ID Number Text
0 IQL_000 0991 [foo]
1 OLA_056 333 [a descirption]
2 PPA_344 978 [nothing]
3 SID_002 1231 [hello, world]
Для Status
столбца мы можем применить аналогичную стратегию, используя _merge
столбец, созданный ранее :
>>> frame_edited = frame_merged.groupby(['ID', 'Number'])['_merge'].apply(list).reset_index()
>>> frame_edited['_merge'] = frame_edited['_merge'].astype(str)
>>> frame_edited
ID Number _merge
0 IQL_000 0991 ['both']
1 OLA_056 333 ['left_only']
2 PPA_344 978 ['right_only']
3 SID_002 1231 ['left_only', 'right_only']
Затем мы можем использовать numpy
для переписывания разыскиваемого Status
:
>>> import numpy as np
>>> condlist = [frame_edited['_merge'] == "['both']",
... frame_edited['_merge'] == "['left_only']",
... frame_edited['_merge'] == "['right_only']",
... frame_edited['_merge'] == "['left_only', 'right_only']",]
>>> choicelist = ['active',
... 'inactive',
... 'new',
... 'edited']
>>> frame_edited['Status'] = np.select(condlist, choicelist, default=np.nan)
>>> frame_edited = frame_edited.drop(['_merge'], axis=1)
>>> frame_edited
ID Number Status
0 IQL_000 0991 active
1 OLA_056 333 inactive
2 PPA_344 978 new
3 SID_002 1231 edited
Чтобы получить ожидаемый результат, мы можем объединить эти два DataFrames
вот так :
>>> pd.merge(frame_merged, frame_edited, how="inner", left_on=['ID', 'Number'], right_on=['ID', 'Number'])
ID Number Text Status
0 IQL_000 0991 [foo] active
1 OLA_056 333 [a descirption] inactive
2 PPA_344 978 [nothing] new
3 SID_002 1231 [hello, world] edited
Чтобы добавить OLD/NEW
метки, мы можем использовать следующую функцию :
def add_prefixe(row):
if len(row['Text']) == 2:
return ["OLD:" row['Text'][0], "NEW:" row["Text"][1]]
else:
return row['Text']
df = df.assign(Text = df.apply(add_prefixe, axis=1))
Тогда мы, наконец, получим :
>>> df
ID Number Text Status
0 IQL_000 0991 [foo] active
1 OLA_056 333 [a descirption] inactive
2 PPA_344 978 [nothing] new
3 SID_002 1231 [OLD:hello, NEW:world] edited
Комментарии:
1. Это неверно. В строке с индексом 2 «ничего» — это новое значение, но первый элемент списка
2. Действительно, я исправил свой ответ. Спасибо @not_speshal !
3. Результат все тот же
4. Я уточнил, что последним элементом списка является текущее значение. Я собираюсь добавить ярлык «старый/новый», чтобы избежать какой-либо путаницы.