Вывод разницы в двух кадрах данных Pandas бок о бок-выделение разницы
Я пытаюсь выделить именно то, что изменилось между двумя таблиц данных.
Предположим, у меня есть два фрейма данных Python Pandas:
"StudentRoster Jan-1":
id Name score isEnrolled Comment
111 Jack 2.17 True He was late to class
112 Nick 1.11 False Graduated
113 Zoe 4.12 True
"StudentRoster Jan-2":
id Name score isEnrolled Comment
111 Jack 2.17 True He was late to class
112 Nick 1.21 False Graduated
113 Zoe 4.12 False On vacation
моя цель-вывести таблицу HTML, которая:
- определяет строки, которые изменились (может быть int, float, boolean, string)
-
выводит строки с одинаковыми, старыми и новыми значениями (в идеале в таблицу HTML), чтобы потребитель мог четко видеть, что изменилось между двумя кадрами данных:
"StudentRoster Difference Jan-1 - Jan-2": id Name score isEnrolled Comment 112 Nick was 1.11| now 1.21 False Graduated 113 Zoe 4.12 was True | now False was "" | now "On vacation"
Я полагаю, что я мог бы сделать сравнение строк и столбцов по столбцам, но есть ли более простой способ?
11 ответов:
первая часть похожа на Константина, вы можете получить логическое значение из строки пусты*:
In [21]: ne = (df1 != df2).any(1) In [22]: ne Out[22]: 0 False 1 True 2 True dtype: bool
затем мы можем видеть, какие записи изменились:
In [23]: ne_stacked = (df1 != df2).stack() In [24]: changed = ne_stacked[ne_stacked] In [25]: changed.index.names = ['id', 'col'] In [26]: changed Out[26]: id col 1 score True 2 isEnrolled True Comment True dtype: bool
здесь первая запись-это индекс, а вторая-столбцы, которые были изменены.
In [27]: difference_locations = np.where(df1 != df2) In [28]: changed_from = df1.values[difference_locations] In [29]: changed_to = df2.values[difference_locations] In [30]: pd.DataFrame({'from': changed_from, 'to': changed_to}, index=changed.index) Out[30]: from to id col 1 score 1.11 1.21 2 isEnrolled True False Comment None On vacation
* Примечание: важно, чтобы
df1
иdf2
поделитесь тем же индексом здесь. Чтобы преодолеть эту двусмысленность, вы можете убедиться, что смотрите только на общие метки с помощьюdf1.index & df2.index
, но я думаю, что оставлю это как упражнение.
выделение разницы между двумя кадрами данных
можно использовать свойство стиля фрейма данных, чтобы выделить цвет фона ячеек, где есть разница.
используя пример данных из исходного вопроса
первым шагом является объединение фреймов данных по горизонтали с
concat
функция и различать каждый кадр сkeys
параметр:df_all = pd.concat([df.set_index('id'), df2.set_index('id')], axis='columns', keys=['First', 'Second']) df_all
вероятно, проще поменять местами уровни столбцов и поместить одни и те же имена столбцов рядом друг с другом:
df_final = df_all.swaplevel(axis='columns')[df.columns[1:]] df_final
теперь, его гораздо легче обнаружить различия в рамках. Но, мы можем пойти дальше и использовать
style
свойства, выделите ячейки, которые отличаются. Мы определяем пользовательскую функцию для этого, которую вы можете увидеть в эта часть документация.def highlight_diff(data, color='yellow'): attr = 'background-color: {}'.format(color) other = data.xs('First', axis='columns', level=-1) return pd.DataFrame(np.where(data.ne(other, level=0), attr, ''), index=data.index, columns=data.columns) df_final.style.apply(highlight_diff, axis=None)
это выделит ячейки, которые оба имеют пропущенные значения. Вы можете либо заполнить их, либо предоставить дополнительную логику, чтобы они не выделялись.
этот ответ просто расширяет @Andy Hayden, делая его устойчивым к тому, когда числовые поля
nan
, и обернуть его в функцию.import pandas as pd import numpy as np def diff_pd(df1, df2): """Identify differences between two pandas DataFrames""" assert (df1.columns == df2.columns).all(), \ "DataFrame column names are different" if any(df1.dtypes != df2.dtypes): "Data Types are different, trying to convert" df2 = df2.astype(df1.dtypes) if df1.equals(df2): return None else: # need to account for np.nan != np.nan returning True diff_mask = (df1 != df2) & ~(df1.isnull() & df2.isnull()) ne_stacked = diff_mask.stack() changed = ne_stacked[ne_stacked] changed.index.names = ['id', 'col'] difference_locations = np.where(diff_mask) changed_from = df1.values[difference_locations] changed_to = df2.values[difference_locations] return pd.DataFrame({'from': changed_from, 'to': changed_to}, index=changed.index)
Итак, с вашими данными (немного отредактировано, чтобы иметь NaN в столбце score):
import sys if sys.version_info[0] < 3: from StringIO import StringIO else: from io import StringIO DF1 = StringIO("""id Name score isEnrolled Comment 111 Jack 2.17 True "He was late to class" 112 Nick 1.11 False "Graduated" 113 Zoe NaN True " " """) DF2 = StringIO("""id Name score isEnrolled Comment 111 Jack 2.17 True "He was late to class" 112 Nick 1.21 False "Graduated" 113 Zoe NaN False "On vacation" """) df1 = pd.read_table(DF1, sep='\s+', index_col='id') df2 = pd.read_table(DF2, sep='\s+', index_col='id') diff_pd(df1, df2)
выход:
from to id col 112 score 1.11 1.21 113 isEnrolled True False Comment On vacation
я столкнулся с этой проблемой, но нашел ответ, прежде чем найти этот пост :
на основе ответа unutbu, загрузите свои данные...
import pandas as pd import io texts = ['''\ id Name score isEnrolled Date 111 Jack True 2013-05-01 12:00:00 112 Nick 1.11 False 2013-05-12 15:05:23 Zoe 4.12 True ''', '''\ id Name score isEnrolled Date 111 Jack 2.17 True 2013-05-01 12:00:00 112 Nick 1.21 False Zoe 4.12 False 2013-05-01 12:00:00'''] df1 = pd.read_fwf(io.BytesIO(texts[0]), widths=[5,7,25,17,20], parse_dates=[4]) df2 = pd.read_fwf(io.BytesIO(texts[1]), widths=[5,7,25,17,20], parse_dates=[4])
...определите ваш diff
import pandas as pd import io texts = ['''\ id Name score isEnrolled Comment 111 Jack 2.17 True He was late to class 112 Nick 1.11 False Graduated 113 Zoe 4.12 True ''', '''\ id Name score isEnrolled Comment 111 Jack 2.17 True He was late to class 112 Nick 1.21 False Graduated 113 Zoe 4.12 False On vacation'''] df1 = pd.read_fwf(io.BytesIO(texts[0]), widths=[5,7,25,21,20]) df2 = pd.read_fwf(io.BytesIO(texts[1]), widths=[5,7,25,21,20]) df = pd.concat([df1,df2]) print(df) # id Name score isEnrolled Comment # 0 111 Jack 2.17 True He was late to class # 1 112 Nick 1.11 False Graduated # 2 113 Zoe 4.12 True NaN # 0 111 Jack 2.17 True He was late to class # 1 112 Nick 1.21 False Graduated # 2 113 Zoe 4.12 False On vacation df.set_index(['id', 'Name'], inplace=True) print(df) # score isEnrolled Comment # id Name # 111 Jack 2.17 True He was late to class # 112 Nick 1.11 False Graduated # 113 Zoe 4.12 True NaN # 111 Jack 2.17 True He was late to class # 112 Nick 1.21 False Graduated # 113 Zoe 4.12 False On vacation def report_diff(x): return x[0] if x[0] == x[1] else '{} | {}'.format(*x) changes = df.groupby(level=['id', 'Name']).agg(report_diff) print(changes)
печать
score isEnrolled Comment id Name 111 Jack 2.17 True He was late to class 112 Nick 1.11 | 1.21 False Graduated 113 Zoe 4.12 True | False nan | On vacation
Если ваши два фрейма данных имеют одинаковые идентификаторы в них, то выяснить, что изменилось на самом деле довольно легко. Просто делаю
frame1 != frame2
даст вам логический фрейм данных, где каждыйTrue
- Это данные, которые изменились. Из этого вы можете легко получить индекс каждой измененной строки, выполнивchangedids = frame1.index[np.any(frame1 != frame2,axis=1)]
.
другой подход с использованием concat и drop_duplicates:
import sys if sys.version_info[0] < 3: from StringIO import StringIO else: from io import StringIO import pandas as pd DF1 = StringIO("""id Name score isEnrolled Comment 111 Jack 2.17 True "He was late to class" 112 Nick 1.11 False "Graduated" 113 Zoe NaN True " " """) DF2 = StringIO("""id Name score isEnrolled Comment 111 Jack 2.17 True "He was late to class" 112 Nick 1.21 False "Graduated" 113 Zoe NaN False "On vacation" """) df1 = pd.read_table(DF1, sep='\s+', index_col='id') df2 = pd.read_table(DF2, sep='\s+', index_col='id') #%% dictionary = {1:df1,2:df2} df=pd.concat(dictionary) df.drop_duplicates(keep=False)
выход:
Name score isEnrolled Comment id 1 112 Nick 1.11 False Graduated 113 Zoe NaN True 2 112 Nick 1.21 False Graduated 113 Zoe NaN False On vacation
расширение ответа @cge, что довольно круто для большей читаемости результата:
a[a != b][np.any(a != b, axis=1)].join(DataFrame('a<->b', index=a.index, columns=['a<=>b'])).join( b[a != b][np.any(a != b, axis=1)] ,rsuffix='_b', how='outer' ).fillna('')
полный демонстрационный пример:
a = DataFrame(np.random.randn(7,3), columns=list('ABC')) b = a.copy() b.iloc[0,2] = np.nan b.iloc[1,0] = 7 b.iloc[3,1] = 77 b.iloc[4,2] = 777 a[a != b][np.any(a != b, axis=1)].join(DataFrame('a<->b', index=a.index, columns=['a<=>b'])).join( b[a != b][np.any(a != b, axis=1)] ,rsuffix='_b', how='outer' ).fillna('')
после возиться с ответом @journois, я смог заставить его работать с помощью MultiIndex вместо панели из-за группы deprication.
во-первых, создайте некоторые фиктивные данные:
df1 = pd.DataFrame({ 'id': ['111', '222', '333', '444', '555'], 'let': ['a', 'b', 'c', 'd', 'e'], 'num': ['1', '2', '3', '4', '5'] }) df2 = pd.DataFrame({ 'id': ['111', '222', '333', '444', '666'], 'let': ['a', 'b', 'c', 'D', 'f'], 'num': ['1', '2', 'Three', '4', '6'], })
затем определите ваш diff функция, в этом случае я буду использовать один из его ответа
report_diff
остается той же:def report_diff(x): return x[0] if x[0] == x[1] else '{} | {}'.format(*x)
затем я собираюсь объединить данные в Многоиндексный фрейм данных:
df_all = pd.concat( [df1.set_index('id'), df2.set_index('id')], axis='columns', keys=['df1', 'df2'], join='outer' ) df_all = df_all.swaplevel(axis='columns')[df1.columns[1:]]
и наконец, я собираюсь применить
report_diff
вниз по каждой группе столбцов:df_final.groupby(level=0, axis=1).apply(lambda frame: frame.apply(report_diff, axis=1))
вот результаты:
let num 111 a 1 222 b 2 333 c 3 | Three 444 d | D 4 555 e | nan 5 | nan 666 nan | f nan | 6
и это все!
вот еще один способ с помощью select и merge:
In [6]: # first lets create some dummy dataframes with some column(s) different ...: df1 = pd.DataFrame({'a': range(-5,0), 'b': range(10,15), 'c': range(20,25)}) ...: df2 = pd.DataFrame({'a': range(-5,0), 'b': range(10,15), 'c': [20] + list(range(101,105))}) In [7]: df1 Out[7]: a b c 0 -5 10 20 1 -4 11 21 2 -3 12 22 3 -2 13 23 4 -1 14 24 In [8]: df2 Out[8]: a b c 0 -5 10 20 1 -4 11 101 2 -3 12 102 3 -2 13 103 4 -1 14 104 In [10]: # make condition over the columns you want to comapre ...: condition = df1['c'] != df2['c'] ...: ...: # select rows from each dataframe where the condition holds ...: diff1 = df1[condition] ...: diff2 = df2[condition] In [11]: # merge the selected rows (dataframes) with some suffixes (optional) ...: diff1.merge(diff2, on=['a','b'], suffixes=('_before', '_after')) Out[11]: a b c_before c_after 0 -4 11 21 101 1 -3 12 22 102 2 -2 13 23 103 3 -1 14 24 104
вот то же самое из скриншота Jupyter:
ниже реализована функция, которая находит асимметричную разницу между двумя фреймами данных: (На основе установить разницу для панд) Суть: https://gist.github.com/oneryalcin/68cf25f536a25e65f0b3c84f9c118e03
def diff_df(df1, df2, how="left"): """ Find Difference of rows for given two dataframes this function is not symmetric, means diff(x, y) != diff(y, x) however diff(x, y, how='left') == diff(y, x, how='right') Ref: https://stackoverflow.com/questions/18180763/set-difference-for-pandas/40209800#40209800 """ if (df1.columns != df2.columns).any(): raise ValueError("Two dataframe columns must match") if df1.equals(df2): return None elif how == 'right': return pd.concat([df2, df1, df1]).drop_duplicates(keep=False) elif how == 'left': return pd.concat([df1, df2, df2]).drop_duplicates(keep=False) else: raise ValueError('how parameter supports only "left" or "right keywords"')
пример:
df1 = pd.DataFrame(d1) Out[1]: Comment Name isEnrolled score 0 He was late to class Jack True 2.17 1 Graduated Nick False 1.11 2 Zoe True 4.12 df2 = pd.DataFrame(d2) Out[2]: Comment Name isEnrolled score 0 He was late to class Jack True 2.17 1 On vacation Zoe True 4.12 diff_df(df1, df2) Out[3]: Comment Name isEnrolled score 1 Graduated Nick False 1.11 2 Zoe True 4.12 diff_df(df2, df1) Out[4]: Comment Name isEnrolled score 1 On vacation Zoe True 4.12 # This gives the same result as above diff_df(df1, df2, how='right') Out[22]: Comment Name isEnrolled score 1 On vacation Zoe True 4.12