Как построить функцию сравнения и обновления таблиц в панд?

#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:

  1. Используется pandas.merge для объединения старой и новой таблиц.
  2. groupby в столбце «Идентификатор» и используйте пользовательские agg функции.
  3. Назначьте необходимый «Статус» с помощью 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. Я уточнил, что последним элементом списка является текущее значение. Я собираюсь добавить ярлык «старый/новый», чтобы избежать какой-либо путаницы.