Порівняйте два DataFrames та виведіть їх відмінності поряд


162

Я намагаюся виділити саме те, що змінилося між двома фреймами даних.

Припустимо, у мене є два фрейми даних 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 Дякую!
dmn

12
Якщо оцінка дорівнює nanяк df1, так і df1, ця функція повідомить про те, що вона змінилася з nanна nan. Це тому, що np.nan != np.nanповертає True.
Джеймс Оуерс

2
@kungfujam прав. Крім того, якщо значення, які порівнюються, "Немає", ви також отримаєте помилкові відмінності
FistOfFury

Просто, щоб було зрозуміло - я ілюструю проблему з цим рішенням і надаю просту у використанні функцію, яка виправляє проблему нижче
James Owers

1
['рядок', 'col'] є кращим, ніж ['id', 'col'] як змінено.index.name, оскільки це не ідентифікатори, а рядки.
naoki fujita

88

Виділення різниці між двома DataFrames

Можна виділити властивість стилю 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 стовпцями. Чи є спосіб її прискорити?
codelord

42

Ця відповідь просто поширюється на @Andy Hayden's, роблячи її стійкою до чисельних полів 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

Я додав код, щоб потурбуватися про незначні відмінності в типі даних, які призвели до помилки, якщо ви цього не зробили.
Робі Нубі

Що робити, якщо для порівняння у мене немає однакових рядків?
Кішор кумар R

@KishorkumarR тоді вам слід спочатку вирівняти рядки, виявивши додані рядки до нового фрейму даних та видаливши рядки зі старого фрейму даних
Saber

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

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)

Тоді ви можете просто використовувати панель для висновку:

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

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) чи можна включити 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 замість панелі завдяки зображенню на панелі .

Спочатку створіть кілька фіктивних даних:

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('')

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

Ось те саме із скріншоту Юпітера:

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


0

панди> = 1,1: DataFrame.compare

За допомогою панд 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 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

Тут порівнюються значення в рядках, а не в стовпцях.


Примітка. Панда 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 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], вісь = 1)

new.head ()

** ТИП ГРАЙЕРУ ТИПУ ІГРАТА

0 Девід Уорнер Бетсман ... Капітан MS Dhoni

1 Бууванешвар Кумар Боулер ... Равіндра Жадея Вселенський

2 Маніш Панді Бетсман ... Суреш Райна всебічний

3 Рашид Хан Арман Боулер ... Кедар Ядхав Вселенський

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


ГРАВЕЦЬ Тип плеєра ТИП 0 Девід Уорнер бетсмен МС Доні Капітан 1 Bhuvaneshwar Кумар Боулер Равіндра ДжейДжей все руки 2 Маніш Пандей бетсмен Суреш Райна все руки 3 Рашид Хан Арман Боулер Кедар Джадхав все руки 4 шикхара Дгаван бетсмен Дуейн Браво все руки
Harish ТРАШ

Привіт Харіш, будь ласка, відформатуйте свою відповідь трохи більше, інакше її досить важко прочитати :)
Маркус,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.