Comparez deux DataFrames et affichez leurs différences côte à côte

163

J'essaie de mettre en évidence exactement ce qui a changé entre deux dataframes.

Supposons que j'ai deux dataframes 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

Mon objectif est de générer un tableau HTML qui:

  1. Identifie les lignes qui ont changé (peuvent être int, float, boolean, string)
  2. Sort les lignes avec les mêmes valeurs, OLD et NEW (idéalement dans un tableau HTML) afin que le consommateur puisse voir clairement ce qui a changé entre deux dataframes:

    "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"

Je suppose que je pourrais faire une comparaison ligne par ligne et colonne par colonne, mais y a-t-il un moyen plus simple?

ciel
la source
Depuis pandas 1.1, vous pouvez facilement le faire avec un seul appel de fonction -df.compare .
cs95 le

Réponses:

153

La première partie est similaire à Constantine, vous pouvez obtenir le booléen dont les lignes sont vides *:

In [21]: ne = (df1 != df2).any(1)

In [22]: ne
Out[22]:
0    False
1     True
2     True
dtype: bool

Ensuite, nous pouvons voir quelles entrées ont changé:

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

Ici, la première entrée est l'index et la seconde les colonnes qui ont été modifiées.

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

* Note: il est important que df1et df2partager le même indice ici. Pour surmonter cette ambiguïté, vous pouvez vous assurer de ne regarder que les étiquettes partagées en utilisant df1.index & df2.index, mais je pense que je vais laisser cela comme un exercice.

Andy Hayden
la source
2
Je crois que "partager le même index" signifie "s'assurer que l'index est trié" ... cela comparera ce qui est le premier df1à ce qui est le premier df2, quelle que soit la valeur de l'indice. JFYI au cas où je ne serais pas la seule personne pour laquelle ce n'était pas évident. ; D Merci!
dmn
12
Si le score est égal à nandans df1 et df1, cette fonction le signalera comme étant passé de nanà nan. C'est parce que np.nan != np.nanrevient True.
James Owers
2
@kungfujam a raison. De plus, si les valeurs comparées sont None, vous obtiendrez également de fausses différences
FistOfFury
Juste pour être clair - j'illustre le problème avec cette solution et fournit une fonction facile à utiliser qui résout le problème ci
James Owers
1
['row', 'col'] est préférable à ['id', 'col'] comme changed.index.names, car ce ne sont pas des identifiants, mais des lignes.
naoki fujita
88

Mettre en évidence la différence entre deux DataFrames

Il est possible d'utiliser la propriété de style DataFrame pour mettre en évidence la couleur d'arrière-plan des cellules où il y a une différence.

Utilisation des exemples de données de la question d'origine

La première étape consiste à concaténer les DataFrames horizontalement avec la concatfonction et à distinguer chaque image avec le keysparamètre:

df_all = pd.concat([df.set_index('id'), df2.set_index('id')], 
                   axis='columns', keys=['First', 'Second'])
df_all

entrez la description de l'image ici

Il est probablement plus facile d'échanger les niveaux de colonne et de placer les mêmes noms de colonne les uns à côté des autres:

df_final = df_all.swaplevel(axis='columns')[df.columns[1:]]
df_final

entrez la description de l'image ici

Maintenant, il est beaucoup plus facile de repérer les différences dans les cadres. Mais, on peut aller plus loin et utiliser la stylepropriété pour mettre en évidence les cellules qui sont différentes. Nous définissons une fonction personnalisée pour ce faire que vous pouvez voir dans cette partie de la 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)

entrez la description de l'image ici

Cela mettra en évidence les cellules qui ont toutes deux des valeurs manquantes. Vous pouvez les remplir ou fournir une logique supplémentaire afin qu'ils ne soient pas mis en évidence.

Ted Petrou
la source
1
Savez-vous comment il est possible de colorer à la fois «Première» et «Deuxième» de différentes couleurs?
aturegano
1
Est-il possible de sélectionner uniquement des lignes différentes? Dans ce cas, comment sélectionner la deuxième et la troisième ligne sans sélectionner la première ligne (111)?
shantanuo
1
@shantanuo, oui, modifiez simplement la méthode finale àdf_final[(df != df2).any(1)].style.apply(highlight_diff, axis=None)
anmol
3
Cette implémentation prend plus de temps lors de la comparaison de dataframes avec 26K lignes et 400 colonnes. Y a-t-il un moyen de l'accélérer?
codeslord
42

Cette réponse étend simplement celle de @Andy Hayden, le rendant résilient aux champs numériques nanet l'enveloppant dans une fonction.

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)

Donc avec vos données (légèrement modifiées pour avoir un NaN dans la colonne 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)

Production:

                from           to
id  col                          
112 score       1.11         1.21
113 isEnrolled  True        False
    Comment           On vacation
James Owers
la source
J'ai ajouté du code pour prendre en compte les différences mineures dans le type de données, ce qui générerait une erreur si vous n'en teniez pas compte.
Roobie Nuby
Que faire si je n'ai pas de lignes identiques de chaque côté à comparer?
Kishor kumar R
@KishorkumarR, vous devriez d'abord égaliser les lignes, en détectant les lignes ajoutées au nouveau dataframe, et supprimez les lignes de l'ancienne dataframe
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)

impressions

                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
la source
3
Très belle solution, bien plus compacte que la mienne!
Andy Hayden
1
@AndyHayden: Je ne suis pas entièrement à l'aise avec cette solution; il semble fonctionner uniquement lorsque l'index est un index à plusieurs niveaux. Si j'essaie de n'utiliser que idcomme index, df.groupby(level='id')cela
génère
19

J'ai rencontré ce problème, mais j'ai trouvé une réponse avant de trouver ce message:

Sur la base de la réponse d'unutbu, chargez vos données ...

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])

... définissez votre fonction diff ...

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

Ensuite, vous pouvez simplement utiliser un panneau pour conclure:

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

À propos, si vous êtes dans IPython Notebook, vous pouvez utiliser une fonction de différence colorée pour donner des couleurs selon que les cellules sont différentes, égales ou nulles gauche / droite:

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
la source
(En Python standard, pas dans un notebook iPython) est-il possible d'inclure my_panel = pd.Panel(dict(df1=df1,df2=df2))dans la fonction report_diff()? Je veux dire, est-il possible de faire ceci: print report_diff(df1,df2)et obtenir le même résultat que votre instruction d'impression?
edesz
pd.Panel(dict(df1=df1,df2=df2)).apply(report_diff, axis=0)- C'est génial!!!
MaxU
5
Les panneaux sont obsolètes! Une idée comment porter ça?
denfromufa
@denfromufa J'ai tenté de le mettre à jour dans ma réponse: stackoverflow.com/a/49038417/7607701
Aaron
9

Si vos deux dataframes ont les mêmes identifiants, il est en fait assez facile de découvrir ce qui a changé. frame1 != frame2Faire juste vous donnera un DataFrame booléen où chacun Trueest des données qui ont changé. À partir de là, vous pouvez facilement obtenir l'index de chaque ligne modifiée en faisant changedids = frame1.index[np.any(frame1 != frame2,axis=1)].

cge
la source
6

Une approche différente utilisant concat et 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)

Production:

       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
jur
la source
3

Après avoir bidouillé la réponse de @ journois, j'ai pu la faire fonctionner en utilisant MultiIndex au lieu de Panel en raison de la dépréciation de Panel .

Tout d'abord, créez des données factices:

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'],
})

Ensuite, définissez votre fonction diff , dans ce cas j'utiliserai celle de sa réponse report_diffreste la même:

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

Ensuite, je vais concaténer les données dans une trame de données 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:]]

Et enfin, je vais appliquer le report_diffbas de chaque groupe de colonnes:

df_final.groupby(level=0, axis=1).apply(lambda frame: frame.apply(report_diff, axis=1))

Cela produit:

         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

Et c'est tout!

Aaron N. Brock
la source
3

Extension de la réponse de @cge, ce qui est plutôt cool pour plus de lisibilité du résultat:

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

Exemple de démonstration complète:

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
la source
1

Voici une autre façon d'utiliser la sélection et la fusion:

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

Voici la même chose à partir d'une capture d'écran de Jupyter:

entrez la description de l'image ici

Aziz Alto
la source
0

pandas> = 1,1: DataFrame.compare

Avec pandas 1.1, vous pourriez essentiellement répliquer la sortie de Ted Petrou avec un seul appel de fonction. Exemple tiré de la documentation:

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

Ici, "self" fait référence au dataFrame LHS, tandis que "other" est le DataFrame RHS. Par défaut, les valeurs égales sont remplacées par NaN afin que vous puissiez vous concentrer uniquement sur les différences. Si vous souhaitez également afficher des valeurs égales, utilisez

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

Vous pouvez également modifier l'axe de comparaison en utilisant 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

Cela compare les valeurs par ligne et non par colonne.

cs95
la source
Remarque: pandas 1.1 est encore expérimental et n'est disponible qu'en créant un sandbox de développement .
cs95 le
-1

Une fonction qui trouve la différence asymétrique entre deux trames de données est implémentée ci-dessous: (Basé sur la différence définie pour les pandas ) 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"')

Exemple:

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
Mehmet Öner Yalçın
la source
-1

importer des pandas comme pd importer numpy comme 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

nouveau = pd.concat ([srh, csk], axe = 1)

new.head ()

** TYPE DE JOUEUR TYPE DE JOUEUR

0 David Warner Batsman ... MS Dhoni Capitaine

1 Bhuvaneshwar Kumar Bowler ... Ravindra Jadeja polyvalent

2 Manish Pandey Batsman ... Suresh Raina polyvalent

3 Rashid Khan Arman Bowler ... Kedar Jadhav polyvalent

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

HARISH TRASH
la source
LECTEUR DE TYPE LECTEUR TYPE 0 David Warner Batteur MS Dhoni Capitaine 1 Bhuvaneshwar Kumar Bowler Ravindra Jadeja All-Rounder 2 Manish Pandey Batteur Suresh Raina All-Rounder 3 Rashid Khan Arman Bowler Kedar Jadhav All-Rounder 4 Shikhar Dhawan Batteur Dwayne Bravo All-Rounder
HARISH TRASH
Bonjour Harish, veuillez formater un peu plus votre réponse, sinon c'est assez difficile à lire :)
Markus