比较两个DataFrame,并将它们的差异并列输出。

232
我正在尝试突出显示两个数据框之间的确切差异。
假设我有两个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 表格,该表格:

  1. 识别已更改的行(可以是整数、浮点数、布尔值或字符串)
  2. 输出具有相同、OLD 和 NEW 值的行(最好以 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"

我可以逐行逐列进行比较,但是否有更简便的方法呢?


3
从pandas 1.1开始,你可以轻松使用一个名为df.compare的函数来完成这个操作。 - cs95
6
注意:要进行比较,数据框必须具有完全相同的形状。因此,如果您试图找出行是否已添加或删除,则无法成功。 - MrR
16个回答

177
第一部分与Constantine相似,您可以获得哪些行是空的布尔值*:
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

* 注意:在这里,df1df2共享相同的索引非常重要。为了解决这种不确定性,您可以确保只查看使用df1.index& df2.index共享标签,但我认为我会把这留给您作为一项练习。


4
我认为“共享相同的索引”意味着“确保索引已排序”......这将比较df1中的第一个元素和df2中的第一个元素,而不考虑索引的值。 顺便提一下,以防我不是唯一一个没有明白的人;D 谢谢! - dmn
16
如果在df1和df2中score的值都等于nan,这个函数将报告score的值已从nan变为nan。这是因为np.nan != np.nan返回的结果为True。 - James Owers
3
@kungfujam是正确的。此外,如果要比较的值是None,则在那里也会得到虚假的差异。 - FistOfFury
1
只是为了明确 - 我用这个解决方案说明问题,并提供一个易于使用的函数来解决问题。请参见下面的链接:https://dev59.com/omQn5IYBdhLWcg3wGT8X#38421614 - James Owers
1
使用['row', 'col']比使用['id', 'col']更好,因为它不是ID,而是行。这样更符合命名规范。 - naoki fujita

139

突出显示两个数据框之间的差异

可以使用DataFrame样式属性来突出显示存在差异的单元格的背景颜色。

使用原问题的示例数据

第一步是使用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)

在此输入图像描述

这将突出显示同时存在缺失值的单元格。您可以填充它们或提供额外的逻辑,使其不被突出显示。


2
你知道是否可能以不同的颜色着色'First'和'Second'吗? - aturegano
1
有没有可能只选择不同的行?在这种情况下,如何选择第二行和第三行而不选择第一行(111)? - shantanuo
1
@shantanuo,是的,只需要编辑最终方法为df_final[(df != df2).any(1)].style.apply(highlight_diff, axis=None) - anmol
3
比较包含26K行和400列的数据框实现起来需要较长时间。 有什么方法可以加快速度吗? - codeslord
由于某些原因,这在日期方面无法正常工作。你能帮我解决一下吗? - JQTs
显示剩余2条评论

60

这个答案简单地扩展了 @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):

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

我添加了代码来处理数据类型的小差异,如果你没有考虑到它,就会抛出错误。 - Roobie Nuby
1
如果两边没有相同的行可以进行比较,该怎么办? - Kishor kumar R
1
@KishorkumarR 然后您应该先将行数平均分配,通过检测到新数据帧中添加的行和从旧数据帧中删除的行来实现。 - Saber
1
很棒的答案。我发现如果由于之前的转换导致列顺序错位,添加以下内容会很有帮助。df1 = df1.reindex(sorted(df1.columns), axis=1) df2 = df2.reindex(sorted(df2.columns), axis=1) - callpete
3
当索引标签不同时,此函数会出现ValueError: Can only compare identically-labeled DataFrame objects错误。如果该函数也考虑到这种情况,它将更加强大和有效。 - Tommy

28
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.StringIO(texts[0]), widths=[5,7,25,21,20])
df2 = pd.read_fwf(io.StringIO(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

3
非常好的解决方案,比我的要简洁得多! - Andy Hayden
3
@AndyHayden: 我对这个解决方案并不完全满意;它似乎只适用于多级索引。如果我尝试将只有'id'作为索引,那么df.groupby(level='id')会报错,我不确定原因是什么... - unutbu

23

pandas >= 1.1:DataFrame.compare

使用 pandas 1.1 及以上版本,您可以通过单个函数调用实现类似 Ted Petrou 的输出。 示例取自文档:

pd.__version__
# '1.1.0'

df1.compare(df2)

  score       isEnrolled       Comment             
   self other       self other    self        other
1  1.11  1.21        NaN   NaN     NaN          NaN
2   NaN   NaN        1.0   0.0     NaN  On vacation

这里,“self”指的是左侧数据帧,而“other”是右侧数据帧。默认情况下,相等的值将被替换为NaN,以便你只关注差异。如果你想要显示相等的值,使用

df1.compare(df2, keep_equal=True, keep_shape=True) 

  score       isEnrolled           Comment             
   self other       self  other       self        other
1  1.11  1.21      False  False  Graduated    Graduated
2  4.12  4.12       True  False        NaN  On vacation

您还可以使用 align_axis 更改比较的轴:

df1.compare(df2, align_axis='index')

         score  isEnrolled      Comment
1 self    1.11         NaN          NaN
  other   1.21         NaN          NaN
2 self     NaN         1.0          NaN
  other    NaN         0.0  On vacation

这将逐行比较值,而不是按列比较。


我有类似的情况,但问题是我的df_all来自于连接2个或更多个df(df1,df2),我需要跟踪df_all中的更改并确定哪个dfs(df1或df2)发生了更改。你能指出这个问题是否已经得到解答吗?我在任何地方都找不到它。 - M_S_N

22

我曾面临这个问题,但在发现这篇文章之前就找到了答案:

根据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.StringIO(texts[0]), widths=[5,7,25,17,20], parse_dates=[4])
df2 = pd.read_fwf(io.StringIO(texts[1]), widths=[5,7,25,17,20], parse_dates=[4])

...定义你的diff函数...

def report_diff(x):
    return x[0] if x[0] == x[1] else '{} | {}'.format(*x)

那么你可以简单地使用面板来总结:

my_panel = pd.Panel(dict(df1=df1,df2=df2))
print my_panel.apply(report_diff, axis=0)

#          id  Name        score    isEnrolled                       Date
#0        111  Jack   nan | 2.17          True        2013-05-01 12:00:00
#1        112  Nick  1.11 | 1.21         False  2013-05-12 15:05:23 | NaT
#2  nan | nan   Zoe         4.12  True | False  NaT | 2013-05-01 12:00:00

顺便提一下,如果你在IPython Notebook中,你可能会喜欢使用一个带有颜色的diff函数,可以根据单元格是否不同、相等或左/右为空来显示颜色:

from IPython.display import HTML
pd.options.display.max_colwidth = 500  # You need this, otherwise pandas
#                          will limit your HTML strings to 50 characters

def report_diff(x):
    if x[0]==x[1]:
        return unicode(x[0].__str__())
    elif pd.isnull(x[0]) and pd.isnull(x[1]):
        return u'<table style="background-color:#00ff00;font-weight:bold;">'+\
            '<tr><td>%s</td></tr><tr><td>%s</td></tr></table>' % ('nan', 'nan')
    elif pd.isnull(x[0]) and ~pd.isnull(x[1]):
        return u'<table style="background-color:#ffff00;font-weight:bold;">'+\
            '<tr><td>%s</td></tr><tr><td>%s</td></tr></table>' % ('nan', x[1])
    elif ~pd.isnull(x[0]) and pd.isnull(x[1]):
        return u'<table style="background-color:#0000ff;font-weight:bold;">'+\
            '<tr><td>%s</td></tr><tr><td>%s</td></tr></table>' % (x[0],'nan')
    else:
        return u'<table style="background-color:#ff0000;font-weight:bold;">'+\
            '<tr><td>%s</td></tr><tr><td>%s</td></tr></table>' % (x[0], x[1])

HTML(my_panel.apply(report_diff, axis=0).to_html(escape=False))

еңЁеёёи§„зҡ„PythonдёӯпјҢжҳҜеҗҰеҸҜд»ҘеңЁеҮҪж•°report_diff()дёӯеҢ…еҗ«my_panel = pd.Panel(dict(df1=df1,df2=df2))пјҹжҲ‘зҡ„ж„ҸжҖқжҳҜпјҢжҳҜеҗҰеҸҜд»Ҙиҝҷж ·еҒҡпјҡprint report_diff(df1,df2)并иҺ·еҫ—дёҺжӮЁзҡ„жү“еҚ°иҜӯеҸҘзӣёеҗҢзҡ„иҫ“еҮәпјҹ - edesz
pd.Panel(dict(df1=df1,df2=df2)).apply(report_diff, axis=0) - 这太棒了!!! - MaxU - stand with Ukraine
6
面板已被弃用!有什么方法可以移植这个? - denfromufa
@denfromufa,我在我的答案中试着更新了它:https://dev59.com/omQn5IYBdhLWcg3wGT8X#49038417 - Aaron N. Brock

12

使用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

11

如果两个数据框中具有相同的id,则找出更改内容实际上很容易。只需执行frame1 != frame2即可获得一个布尔值数据框,其中每个True是发生更改的数据。从那里,您可以通过执行changedids = frame1.index[np.any(frame1 != frame2,axis=1)]轻松获取每个更改行的索引。


6

在尝试了@journois的答案后,由于Panel 已被弃用,我成功地使用MultiIndex代替Panel来实现它。

首先,创建一些虚拟数据:

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)

然后,我将把数据连接成一个MultiIndex数据帧:
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

就是这样了!


请将 df_final 替换为 df_all。 - Luka Banfi

4

延伸@cge的答案,这对于结果的可读性非常有帮助:

a[a != b][np.any(a != b, axis=1)].join(pd.DataFrame('a<->b', index=a.index, columns=['a<=>b'])).join(
        b[a != b][np.any(a != b, axis=1)]
        ,rsuffix='_b', how='outer'
).fillna('')

完整演示示例:

import numpy as np, pandas as pd

a = pd.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(pd.DataFrame('a<->b', index=a.index, columns=['a<=>b'])).join(
        b[a != b][np.any(a != b, axis=1)]
        ,rsuffix='_b', how='outer'
).fillna('')

示例结果: 在此输入图片描述

在线演示


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接