Сравните два DataFrames и выведите их различия бок о бок

163

Я пытаюсь выделить именно то, что изменилось между двумя кадрами.

Предположим, у меня есть два кадра данных 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. Выводит строки с одинаковыми, старыми и новыми значениями (в идеале в таблицу 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"
    

Я полагаю, я мог бы сделать сравнение строка за строкой и столбец за столбцом, но есть ли более простой способ?

небо
источник
Из панд 1.1 вы можете легко сделать это с помощью одного вызова функции -df.compare .
cs95

Ответы:

153

Первая часть похожа на Константина, вы можете получить логическое значение, строки которого пусты *:

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, но я думаю, что я оставлю это как упражнение.

Энди Хейден
источник
2
Я считаю, что «использовать один и тот же индекс» означает «убедиться, что индекс отсортирован» ... это будет сравнивать то, что находится первым, df1с тем, что находится первым df2, независимо от значения индекса. JFYI на случай, если я не единственный человек, для которого это не было очевидно. ; D Спасибо!
ДМН
12
Если оценка равна nanкак в df1, так и в df1, эта функция сообщит, что она изменилась с nanна nan. Это потому что np.nan != np.nanвозвращается True.
Джеймс Оверс
2
@kungfujam прав. Кроме того, если сравниваемые значения равны None, вы также получите ложные различия
FistOfFury
Просто чтобы прояснить
ситуацию
1
['row', 'col'] предпочтительнее, чем ['id', 'col'], как change.index.names, потому что это не идентификаторы, а строки.
Наоки Фудзита
88

Подчеркивая разницу между двумя фреймами данных

Можно использовать свойство стиля DataFrame, чтобы выделить цвет фона ячеек, где есть разница.

Используя данные примера из исходного вопроса

Первым шагом является объединение DataFrames по горизонтали с 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)

введите описание изображения здесь

Это будет подсвечивать ячейки, которые имеют пропущенные значения. Вы можете либо заполнить их, либо предоставить дополнительную логику, чтобы они не выделялись.

Тед Петру
источник
1
Знаете ли вы, как можно покрасить «Первый» и «Второй» в разные цвета?
aturegano
1
Можно ли выбрать только разные строки? В этом случае, как выбрать второй и третий ряд без выбора первого ряда (111)?
Shantanuo
1
@shantanuo, да, просто отредактируйте последний метод дляdf_final[(df != df2).any(1)].style.apply(highlight_diff, axis=None)
anmol
3
Эта реализация занимает больше времени при сравнении кадров данных с 26K строк и 400 столбцов. Есть ли способ ускорить его?
кодовый повелитель
42

Этот ответ просто расширяет @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, затем вы должны сначала выровнять строки, обнаружив добавленные строки в новом фрейме данных и удалив строки из старого фрейма данных
Sabre
22
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
unutbu
источник
3
Очень хорошее решение, гораздо более компактное, чем у меня!
Энди Хейден
1
@AndyHayden: я не совсем доволен этим решением; Кажется, он работает только тогда, когда индекс является многоуровневым. Если я пытаюсь использовать только idв качестве индекса, df.groupby(level='id')возникает ошибка, и я не уверен, почему ...
unutbu
19

Я столкнулся с этой проблемой, но нашел ответ, прежде чем найти этот пост:

На основании ответа 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])

... определить вашу функцию сравнения ...

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

Затем вы можете просто использовать Panel для вывода:

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, вы можете использовать цветную функцию сравнения, чтобы задавать цвета в зависимости от того, являются ли ячейки разными, равными или левыми / правыми нулевыми:

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))
journois
источник
(В обычном Python, а не в записной книжке iPython) возможно ли включение my_panel = pd.Panel(dict(df1=df1,df2=df2))внутри функции report_diff()? Я имею в виду, возможно ли сделать это: print report_diff(df1,df2)и получить тот же вывод, что и ваш оператор печати?
Edesz
pd.Panel(dict(df1=df1,df2=df2)).apply(report_diff, axis=0)- это круто!!!
MaxU
5
Панели устарели! Есть идеи, как это портировать?
денфромуфа
@denfromufa Я попробовал обновить его в своем ответе: stackoverflow.com/a/49038417/7607701
Аарон Н. Брок
9

Если ваши два кадра данных имеют одинаковые идентификаторы, то выяснить, что изменилось, довольно просто. Простое выполнение frame1 != frame2даст вам логический DataFrame, где каждый Trueявляется данными, которые изменились. Исходя из этого, вы можете легко получить индекс каждой измененной строки, выполнив changedids = frame1.index[np.any(frame1 != frame2,axis=1)].

КГЭ
источник
6

Другой подход с использованием 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
юр
источник
3

После возни с ответом @ journois, я смог заставить его работать, используя MultiIndex вместо Panel из-за ограничения 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'],
})

Затем определите свою функцию сравнения , в этом случае я буду использовать ту, которая из его ответа 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

И это все!

Аарон Н. Брок
источник
3

Расширение ответа @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('')
Hubbitus
источник
1

Вот еще один способ с помощью выбора и слияния:

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:

введите описание изображения здесь

Азиз Альто
источник
0

панды> = 1.1: DataFrame.compare

С pandas 1.1 вы можете по существу реплицировать вывод Теда Петру одним вызовом функции. Пример взят из документации:

pd.__version__
# '1.1.0.dev0+2004.g8d10bfb6f'

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, а «other» - к фрейму данных RHS. По умолчанию равные значения заменяются на 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

Это сравнивает значения по строкам, а не по столбцам.

cs95
источник
Примечание: pandas 1.1 все еще экспериментальная и доступна только при создании тестовой среды разработки .
CS95
-1

Функция, которая находит несимметричное различие между двумя фреймами данных, реализована ниже: (на основе разницы в наборах для панд ) GIST: 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: /programming/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
Мехмет Онер Ялчин
источник
-1

импортировать панд как pd импортировать numpy как np

df = pd.read_excel ('D: \ HARISH \ DATA SCIENCE \ 1 MY Training \ SAMPLE DATA & projs \ CRICKET DATA \ СПИСОК ИГРОКОВ IPL \ СПИСОК ИГРОКОВ IPL _ 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 ()

** ТИП ИГРОКА ТИП ИГРОКА

0 Дэвид Уорнер Бэтсмен ... М.С. Дони Капитан

1 Бхуванешвар Кумар Боулер ... Ravindra Jadeja All-Rounder

2 Маниш Пандей Бэтсмен ... Суреш Райна, универсал

3 Рашид-хан Арман Боулер ... Кедар Джадхав Универсал

4 Shikhar Dhawan Batsman .... Dwayne Bravo All-Rounder

HARISH TRASH
источник
ИГРОК Тип плеера ТИП 0 Дэвид Уорнер Бэтсмен МС Дони Капитан 1 Bhuvaneshwar Кумар Боулер Равиндра Джейджа все руки 2 Маниш Пандей Бэтсмен Суреш Райна все руки 3 Рашид Хан Арман Боулер Кедар Джадхав все руки 4 Шикхар Дгаван Бэтсмен Дуэйн Браво все руки
Harish TRASH
Здравствуйте, Хариш, пожалуйста, отформатируйте свой ответ немного больше, иначе его довольно сложно прочитать :)
Маркус