【问题标题】:Compare two DataFrames and output their differences side-by-side比较两个 DataFrame 并并排输出它们的差异
【发布时间】:2013-06-10 07:30:23
【问题描述】:

我试图准确强调两个数据框之间的变化。

假设我有两个 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. 标识已更改的行(可以是 int、float、boolean、string)
  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"
    

我想我可以逐行逐列比较,但是有没有更简单的方法?

【问题讨论】:

  • 从 pandas 1.1 开始,您可以使用 single function call - df.compare 轻松做到这一点。
  • 注意:为了比较工作,数据框需要具有相同的形状。因此,如果您试图找出是否已添加或删除了一行,那么您就不走运了。

标签: python pandas dataframe


【解决方案1】:

第一部分和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 的共享标签,但我想我将把它留作练习。

【讨论】:

  • 我相信“共享相同的索引”意味着“确保索引已排序”...这将比较 df1 中的第一个内容与 df2 中的第一个内容,无论指数的价值。 JFYI,以防我不是唯一一个不明显的人。 ;D 谢谢!
  • 如果 df1 和 df1 中的分数等于 nan,此函数将报告它已从 nan 更改为 nan。这是因为np.nan != np.nan 返回True
  • @kungfujam 是对的。此外,如果要比较的值是 None 你也会在那里得到错误的差异
  • 为了清楚起见 - 我用这个解决方案说明了问题并提供了一个易于使用的功能来解决问题below
  • ['row', 'col'] 作为 changed.index.names 比 ['id','col'] 更可取,因为它不是 id,而是行。
【解决方案2】:

突出两个 DataFrame 之间的区别

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

使用原始问题中的示例数据

第一步是用concat函数将DataFrames水平连接起来,并用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 属性来突出显示不同的单元格。我们定义了一个自定义函数来执行此操作,您可以在 this part of the documentation 中看到。

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)

这将突出显示都具有缺失值的单元格。您可以填充它们或提供额外的逻辑,这样它们就不会被突出显示。

【讨论】:

  • 你知道如何将'First'和'Second'都涂成不同的颜色吗?
  • 是否可以只选择不同的行?在这种情况下如何选择第二行和第三行而不选择第一行(111)?
  • @shantanuo,是的,只需将最终方法编辑为df_final[(df != df2).any(1)].style.apply(highlight_diff, axis=None)
  • 在比较具有 26K 行和 400 列的数据帧时,此实现需要更长的时间。有什么办法可以加快速度?
  • @Ted Petrou 您的代码非常非常漂亮,但它不适用于我的文件。我有 2 个不同数量的原始文件
【解决方案3】:

这个答案只是扩展了@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

【讨论】:

  • 我添加了代码来处理数据类型的细微差异,如果您不考虑它会引发错误。
  • 如果我没有相同的行来比较怎么办?
  • @KishorkumarR 那么你应该首先通过检测添加到新数据帧的行并从旧数据帧中删除行来平衡行
  • 很好的答案。我发现添加以下内容很有帮助,以防列顺序由于先前的转换而未对齐。 df1 = df1.reindex(sorted(df1.columns), axis=1) df2 = df2.reindex(sorted(df2.columns), axis=1)
  • 当索引标签与ValueError: Can only compare identically-labeled DataFrame objects 不同时,这个函数会崩溃——如果它也考虑到这种情况,这个函数会更强大或更强大。
【解决方案4】:
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

【讨论】:

  • 非常好的解决方案,比我的更紧凑!
  • @AndyHayden:我对这个解决方案并不完全满意;它似乎仅在索引是多级索引时才有效。如果我尝试只使用id 作为索引,那么df.groupby(level='id') 会引发错误,我不知道为什么......
【解决方案5】:

我遇到过这个问题,但在找到这篇文章之前找到了答案:

根据 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 函数 根据单元格是否不同、相等或左/右 null 来赋予颜色:

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 中,而不是 iPython 笔记本中)是否可以在函数 report_diff() 中包含 my_panel = pd.Panel(dict(df1=df1,df2=df2))?我的意思是,是否可以这样做:print report_diff(df1,df2) 并获得与您的打印语句相同的输出?
  • pd.Panel(dict(df1=df1,df2=df2)).apply(report_diff, axis=0) - 这太棒了!!!
  • 面板已弃用!知道如何移植吗?
  • @denfromufa 我在我的回答中尝试更新它:stackoverflow.com/a/49038417/7607701
【解决方案6】:

熊猫 >= 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”指的是 LHS DataFrame,而“other”是指 RHS DataFrame。默认情况下,相等的值被替换为 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) 变了。你能指出这个问题是否已经回答,我在任何地方都找不到。
【解决方案7】:

如果您的两个数据帧中的 id 相同,那么找出更改的内容实际上非常容易。只需执行 frame1 != frame2 将为您提供一个布尔数据框,其中每个 True 是已更改的数据。由此,您可以通过 changedids = frame1.index[np.any(frame1 != frame2,axis=1)] 轻松获取每个更改行的索引。

【讨论】:

    【解决方案8】:

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

    【讨论】:

      【解决方案9】:

      在摆弄@journois 的答案之后,由于Panel's deprication,我能够使用 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
      

      仅此而已!

      【讨论】:

        【解决方案10】:

        扩展@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('')
        

        【讨论】:

          【解决方案11】:

          这是另一种使用选择和合并的方式:

          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 屏幕截图的相同内容:

          【讨论】:

            【解决方案12】:

            如果您发现此线程试图在测试中比较数据名气,请查看assert_frame_equal 方法:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.testing.assert_frame_equal.html

            【讨论】:

              【解决方案13】:

              下面实现了一个查找两个数据帧之间不对称差异的函数: (基于set difference for pandas) 要点: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
              

              【讨论】:

                【解决方案14】:
                import pandas as pd
                import numpy as np
                
                df = pd.read_excel('D:\\HARISH\\DATA SCIENCE\\1 MY Training\\SAMPLE DATA & projs\\CRICKET DATA\\IPL PLAYER LIST\\IPL PLAYER LIST _ harish.xlsx')
                
                
                df1= srh = df[df['TEAM'].str.contains("SRH")]
                df2 = csk = df[df['TEAM'].str.contains("CSK")]   
                
                srh = srh.iloc[:,0:2]
                csk = csk.iloc[:,0:2]
                
                csk = csk.reset_index(drop=True)
                csk
                
                srh = srh.reset_index(drop=True)
                srh
                
                new = pd.concat([srh, csk], axis=1)
                
                new.head()
                
                ** 
                               PLAYER     TYPE           PLAYER         TYPE
                
                0        David Warner  Batsman    ...        MS Dhoni      Captain
                
                1  Bhuvaneshwar Kumar   Bowler  ...    Ravindra Jadeja  All-Rounder
                
                2       Manish Pandey  Batsman   ...   Suresh Raina  All-Rounder
                
                3   Rashid Khan Arman   Bowler     ...   Kedar Jadhav  All-Rounder
                
                4      Shikhar Dhawan  Batsman    ....    Dwayne Bravo  All-Rounder
                

                【讨论】:

                • 你好,Harish,请多格式化你的答案,否则它很难阅读:)
                猜你喜欢
                • 1970-01-01
                • 2020-07-25
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2017-12-11
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多