Как сравнить 2 кадра данных по заданным столбцам с помощью Groupby, где данные не организованы идеально в одном из них

#pandas #numpy #group-by #transpose #pandas-melt

Вопрос:

Проблема, которую я пытаюсь решить, состоит в том, чтобы согласовать ставки, применяемые к фактическим счетам, показанным в «Таблице счетов«, с тем, что должно быть установлено в «Таблице ставок«. Для каждой Учетной записи ее ставка может быть установлена на разных уровнях, либо на уровне Учетной записи, либо на Родительском уровне. Несколько учетных записей могут быть связаны с одним и тем же родителем, но они будут отличаться, например, в зависимости от валюты. Хотя я могу заставить его сравнивать, мое решение включает в себя много кода, который повторяется и не масштабируется, где в этом примере рассматриваются только 2 разные группировки, где я мог бы сравнить до 9 различных группировок.

Вот пример таблицы учета:

 import pandas as pd import numpy as np AccountTable = pd.DataFrame([[1234567890,456,'EUR',3.5],  [7854567890,15,'USD',2.7],  [9632587415,56,'GBP',1.4]], columns = ['Account','ParentID','Cur','Rate']) AccountTable  

Выход:

 Account ParentID Cur Rate 0 1234567890 456 EUR 3.5 1 7854567890 15 USD 2.7 2 9632587415 56 GBP 1.4  

Вот таблица ставок:

 RateTable = pd.DataFrame([['Account',1234567890,'EUR',3.5], # Rate set at account level and shuold return a match  ['ParentID',456,'EUR',3.5], # should be Unused as match found at account level  ['ParentID',15,'USD',2.7],# rate set at account level and matches   ['ParentID',15,'CAD',1.5],# CAD not in Account Table therfore unused   ['Account',9876542190,'EUR',3.5], # Account Table therfore unused   ['ParentID',56,'GBP',1.5]], # rate set on parent level but rates don't match so return should be mismatch here columns = ['Level_Type','ID','Cur','Set_Rate'])  

Выход:

 Level_Type ID Cur Set_Rate 0 Account 1234567890 EUR 3.5 1 ParentID 456 EUR 3.5 2 ParentID 15 USD 2.7 3 ParentID 15 CAD 1.5 4 Account 9876542190 EUR 3.5 5 ParentID 56 GBP 1.5  

Мои решения заключаются в следующем: я разделяю таблицу ставок на несколько фреймов данных на основе разных уровней. В данном случае 2 — Уровень учетной записи и Родительский уровень. Затем я оставил их независимо присоединяться к таблице учетных записей с помощью функции Groupby и сравнивать ставки.

 option1 = ['Account'] option2 = ['ParentID'] AccountView = RateTable[RateTable['Level_Type'].isin(option1)] ParentView = RateTable[RateTable['Level_Type'].isin(option2)] AccountView = AccountView.rename(columns={'Set_Rate':'Account_Set_Rate'}) ParentView = ParentView.rename(columns={'Set_Rate':'Parent_Set_Rate'}) AccountView = AccountView.rename(columns={'ID':'Account_ID'}) ParentView = ParentView.rename(columns={'ID':'Parent_ID'}) # new view to identify matches at Account level Only  df = pd.merge(AccountTable, AccountView, left_on=['Account','Cur'], right_on=['Account_ID','Cur'], how='left') df['Account_level_RateMatch'] = np.where(df['Rate'] == df['Account_Set_Rate'],'1','0').astype(int)   
 Account ParentID Cur Rate Level_Type Account_ID Account_Set_Rate Account_level_RateMatch 0 1234567890 456 EUR 3.5 Account 1.234568e 09 3.5 1 1 7854567890 15 USD 2.7 NaN NaN NaN 0 2 9632587415 56 GBP 1.4 NaN NaN NaN 0  

Вышесказанное повторяется, но теперь совпадает на родительском уровне:

 df = pd.merge(AccountTable, ParentView, left_on=['ParentID','Cur'], right_on=['Parent_ID','Cur'], how='left') df['Parent_level_RateMatch'] = np.where(df['Rate'] == df['Parent_Set_Rate'],'1','0').astype(int) # compare rates   

Выход:

 Account ParentID Cur Rate Level_Type Parent_ID Parent_Set_Rate Parent_level_RateMatch 0 1234567890 456 EUR 3.5 ParentID 456 3.5 1 1 7854567890 15 USD 2.7 ParentID 15 2.7 1 2 9632587415 56 GBP 1.4 ParentID 56 1.5 0  

Мне нужен лучший способ сравнить ставки по счетам с таблицей ставок, а не делать отдельные просмотры. Кроме того, логика должна быть такой, если совпадение найдено на первом уровне «Уровень учетной записи», оно останавливается на этом, и не нужно проверять следующий уровень, т. е. родительский уровень, например, в строке # 1, он совпадает как на уровне учетной записи, так и на родительском уровне.

Любые мысли или решения будут высоко оценены.

Желаемый Результат:

 Account ParentID Cur Rate IsMatch LevelFound 0 1234567890 456 EUR 3.5 1 Account 1 7854567890 15 USD 2.7 1 Parent 2 9632587415 56 GBP 1.4 0 Parent  

Комментарии:

1. @Ben. T Я добавил в исходный пост, как должен выглядеть желаемый результат

2. хорошо , допустим, для второй учетной 7854567890 записи в таблице ставок есть строка с той же учетной записью, но с неправильной ставкой. Затем есть совпадение с родителем, хотите ли вы сохранить информацию, которая не совпадает с учетной записью или с соответствующей учетной записью родителя?

3. Для учетной 7854567890 записи в таблице ставок нет соответствующей учетной записи. В этом случае в таблице учетных записей у каждой учетной записи будет родительский идентификатор, для этой учетной 7854567890 записи он равен 15. Поэтому мы ищем 15 в таблице ставок и сравниваем его ставку, которая составляет 2,7, с таблицей счетов. Обратите внимание, что в моем коде я тоже группируюсь по валюте, чтобы найти ее уникальное сопряжение. Имеет ли это смысл?

Ответ №1:

ОТРЕДАКТИРУЙТЕ решение, аналогичное оригинальному, но лучше соответствующее ожидаемому результату операции

 #define the order of the levels ordered_levels = ['Account','ParentID']   # fnd all the matching rates res = (  pd.concat(  [AccountTable  .merge(RateTable.loc[RateTable['Level_Type'].eq(lvl),#row with good level  # columns for comparision with AccountTable  ['ID','Cur','Set_Rate']]  .rename(columns={'ID':lvl, 'Cur':f'Cur_opt'}),   on=lvl, how='inner')  .query('Cur == Cur_opt') #EDIT to query same cur  .assign(LevelFound=lvl,   #EDIT if rate not the same then 0  Is_Match=lambda x: x['Rate'].eq(x['Set_Rate']).astype(int))   for lvl in ordered_levels]) # do the merge operation on each level  #EDIT for selecting first 1 if any, then first 0  .sort_values('Is_Match', ascending=False)   # keep the first matched per initial AccountTable or higher level non-match  .drop_duplicates(ordered_levels)   [AccountTable.columns.tolist()   ['LevelFound','Is_Match']]  ) print(res) # Account ParentID Cur Rate LevelFound Is_Match # 0 1234567890 456 EUR 3.5 Account 1 # 1 7854567890 15 USD 2.7 ParentID 1 # 3 9632587415 56 GBP 1.4 ParentID 0  

Оригинальное решение

Вот решение, сначала вам нужно определить порядок уровней, затем вы можете перебрать каждый из них, выбрать нужные строки RateTable , затем merge с учетной записью и сохранить только соответствующие cur и rate ( query ). concat все совпадающие данные и сохраняйте только первое совпадение для каждой учетной записи в начальной строке.

 #define the order of the levels ordered_levels = ['Account','ParentID']   # fnd all the matching rates matched = (  pd.concat(  [AccountTable  .merge(RateTable.loc[RateTable['Level_Type'].eq(lvl),#row with good level  # columns for comparision with AccountTable  ['ID','Cur','Set_Rate']]   .rename(columns={'ID':lvl, 'Cur':f'Cur_opt'}),   on=lvl, how='inner')  # keep only the matching data  .query('Cur == Cur_opt and Rate == Set_Rate')  # add the two columns for the ouput  .assign(LevelFound=opt, Is_Match=1)  for lvl in ordered_levels]) # do the merge operation on each level  .drop_duplicates(ordered_levels) # keep the first matched per initial AccountTable  [AccountTable.columns.tolist()   ['LevelFound','Is_Match']]  ) print(matched) # note that the row wihtout match is missing # Account ParentID Cur Rate LevelFound Is_Match # 0 1234567890 456 EUR 3.5 Account 1 # 1 7854567890 15 USD 2.7 ParentID 1   

Если вы хотите добавить строки без совпадения, то вы можете сделать

 res = AccountTable.merge(matched, how='left') print(res) # Account ParentID Cur Rate LevelFound Is_Match # 0 1234567890 456 EUR 3.5 Account 1.0 # 1 7854567890 15 USD 2.7 ParentID 1.0 # 2 9632587415 56 GBP 1.4 NaN NaN  

Комментарии:

1. Отлично Solution…it действительно так. Во-первых, ожидаемый результат для учетной записи 9632587415 должен быть несоответствием или » 0 » в поле Is_match, а не NaN. Хотя учетная запись не найдена на уровне учетной записи, она доступна на родительском уровне в разделе ParentID 56.

2. @AlanPaul действительно, см. решение для редактирования, действительно похожее

3. Отличная работа, Бен. Это действительно решает обозначенную проблему. Мой следующий вопрос заключается в том, насколько сложно применить условия if/else к этому коду? Вышесказанное было упрощенной версией того, что мне действительно нужно сделать. Например, если поле ставки было пустым, оно применяло бы ставку из другой таблицы. Должен ли я задать новый вопрос, поскольку этот вопрос действительно был задан?

4. В то время как внутреннее соединение было выполнено. Есть ли способ включить Set_Rate из таблицы ставок в конечный результат?

5. @AlanPaul для Set_rate просто добавьте его в последнюю строку кода, в которой выбираются выходные столбцы, чтобы [AccountTable.columns.tolist() ['LevelFound','Is_Match','Set_Rate']] внутреннее слияние было в строке, поэтому этот столбец все еще доступен. для условия if else, да, новый вопрос был бы лучше, обратитесь к этому, это тоже может помочь 🙂