Confronta due DataFrame e mostra le loro differenze fianco a fianco


163

Sto cercando di evidenziare esattamente cosa è cambiato tra due frame di dati.

Supponiamo che io abbia due dataframe 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

Il mio obiettivo è produrre una tabella HTML che:

  1. Identifica le righe che sono state modificate (potrebbero essere int, float, boolean, string)
  2. Emette righe con gli stessi, VECCHI e NUOVI valori (idealmente in una tabella HTML) in modo che il consumatore possa vedere chiaramente cosa è cambiato tra due frame di dati:

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

Suppongo di poter fare un confronto riga per riga e colonna per colonna, ma esiste un modo più semplice?


Da Panda 1.1 puoi farlo facilmente con una singola chiamata di funzione -df.compare .
cs95,

Risposte:


153

La prima parte è simile a Costantino, puoi ottenere il booleano di cui le righe sono vuote *:

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

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

Quindi possiamo vedere quali voci sono cambiate:

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

Qui la prima voce è l'indice e la seconda le colonne che sono state modificate.

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

* Nota: è importante df1e df2condividi qui lo stesso indice. Per superare questa ambiguità, puoi assicurarti di guardare solo le etichette condivise usando df1.index & df2.index, ma penso che lo lascerò come un esercizio.


2
Credo che "condividi lo stesso indice" significa "assicurati che l'indice sia ordinato" ... questo confronterà tutto ciò che è primo in df1quello che è primo df2, indipendentemente dal valore dell'indice. JFYI nel caso in cui non fossi l'unica persona per cui questo non era ovvio. ; D Grazie!
dmn

12
Se il punteggio è uguale nansia in df1 che in df1, questa funzione lo segnalerà come modificato da nana nan. Questo perché np.nan != np.nanritorna True.
James Owers,

2
@kungfujam ha ragione. Inoltre, se i valori confrontati sono Nessuno, otterrai anche false differenze
FistOfFury

Giusto per essere chiari - Illustro il problema con questa soluzione e fornisco una funzione facile da usare che risolve il problema di seguito
James Owers

1
['row', 'col'] è preferibile di ['id', 'col'] come change.index.names, perché non è id, ma righe.
naoki fujita,

88

Evidenziando la differenza tra due DataFrame

È possibile utilizzare la proprietà di stile DataFrame per evidenziare il colore di sfondo delle celle in cui vi è una differenza.

Utilizzando i dati di esempio della domanda originale

Il primo passo è concatenare i DataFrames in orizzontale con la concatfunzione e distinguere ogni frame con il keysparametro:

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

inserisci qui la descrizione dell'immagine

Probabilmente è più facile scambiare i livelli di colonna e mettere gli stessi nomi di colonna uno accanto all'altro:

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

inserisci qui la descrizione dell'immagine

Ora è molto più facile individuare le differenze nei frame. Ma possiamo andare oltre e usare la styleproprietà per evidenziare le celle che sono diverse. Definiamo una funzione personalizzata per fare ciò che puoi vedere in questa parte della documentazione .

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)

inserisci qui la descrizione dell'immagine

Ciò evidenzierà le celle che hanno entrambi valori mancanti. Puoi riempirli o fornire una logica aggiuntiva in modo che non vengano evidenziati.


1
Sai come è possibile colorare sia "Primo" che "Secondo" in diversi colori?
Aturegano,

1
È possibile selezionare solo righe diverse? In questo caso come posso selezionare la seconda e la terza riga senza selezionare la prima riga (111)?
shantanuo,

1
@shantanuo, sì, basta modificare il metodo finale indf_final[(df != df2).any(1)].style.apply(highlight_diff, axis=None)
anmol il

3
Questa implementazione impiega più tempo a confrontare i frame di dati con 26K righe e 400 colonne. C'è un modo per accelerarlo?
codeslord,

42

Questa risposta estende semplicemente @Andy Hayden, rendendola resistente ai campi numerici nane racchiudendola in una funzione.

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)

Quindi con i tuoi dati (leggermente modificato per avere un NaN nella colonna dei punteggi):

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)

Produzione:

                from           to
id  col                          
112 score       1.11         1.21
113 isEnrolled  True        False
    Comment           On vacation

Ho aggiunto del codice per occuparmi delle differenze minori nel tipo di dati, il che genererebbe un errore se non lo rendessi conto.
Roobie Nuby,

Cosa succede se non ho file identiche su entrambi i lati per confrontare?
Kishor kumar R,

@KishorkumarR quindi dovresti prima uniformare le righe, rilevando le righe aggiunte al nuovo frame di dati e le righe rimosse dal vecchio frame di dati
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)

stampe

                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
Soluzione molto bella, molto più compatta della mia!
Andy Hayden,

1
@AndyHayden: non mi sento completamente a mio agio con questa soluzione; sembra funzionare solo quando l'indice è un indice multilivello. Se provo a utilizzare solo idcome indice, quindi df.groupby(level='id')genera un errore e non sono sicuro del perché ...
unutbu

19

Ho riscontrato questo problema, ma ho trovato una risposta prima di trovare questo post:

Basato sulla risposta di unutbu, carica i tuoi dati ...

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

... definisci la tua funzione diff ...

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

Quindi puoi semplicemente usare un pannello per concludere:

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

A proposito, se ti trovi in ​​IPython Notebook, potresti usare una funzione diff colorata per dare colori a seconda che le celle siano diverse, uguali o nulle sinistra / destra:

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

(Nel normale Python, non nel notebook iPython) è possibile includere my_panel = pd.Panel(dict(df1=df1,df2=df2))nella funzione report_diff()? Voglio dire, è possibile farlo: print report_diff(df1,df2)e ottenere lo stesso output della tua dichiarazione di stampa?
edesz,

pd.Panel(dict(df1=df1,df2=df2)).apply(report_diff, axis=0)- Questo e spettacolare!!!
MaxU

5
I pannelli sono obsoleti! Qualche idea su come portarlo?
denfromufa,

@denfromufa Mi sono preso una buona dose per aggiornarlo nella mia risposta: stackoverflow.com/a/49038417/7607701
Aaron N. Brock,

9

Se i tuoi due frame di dati hanno gli stessi ID, scoprire che cosa è cambiato è piuttosto semplice. Basta fare frame1 != frame2ti darà un DataFrame booleano in cui ognuno Trueè un dato che è cambiato. Da ciò, puoi facilmente ottenere l'indice di ogni riga modificata facendo changedids = frame1.index[np.any(frame1 != frame2,axis=1)].


6

Un approccio diverso utilizzando concat e 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)

Produzione:

       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

Dopo trafficando con @ risposta di journois, ero in grado di farlo funzionare utilizzando MultiIndex invece di Pannello a causa di deprication del pannello .

Innanzitutto, crea alcuni dati fittizi:

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

Quindi, definisci la tua funzione diff , in questo caso userò quella della sua risposta che report_diffrimane la stessa:

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

Quindi, ho intenzione di concatenare i dati in un frame di dati 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:]]

E infine applicherò in report_diffbasso ogni gruppo di colonne:

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

Questo produce:

         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

E questo è tutto!


3

Estendere la risposta di @cge, che è piuttosto interessante per una maggiore leggibilità del risultato:

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

Esempio dimostrativo completo:

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

Ecco un altro modo usando select and merge:

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

Ecco la stessa cosa da uno screenshot di Jupyter:

inserisci qui la descrizione dell'immagine


0

panda> = 1.1: DataFrame.compare

Con Panda 1.1, potresti essenzialmente replicare l'output di Ted Petrou con una singola chiamata di funzione. Esempio tratto dai documenti:

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

Qui, "self" si riferisce al dataframe LHS, mentre "altro" è il DataFrame RHS. Per impostazione predefinita, i valori uguali vengono sostituiti con NaN in modo da poter concentrarsi solo sulle differenze. Se si desidera mostrare anche valori uguali, utilizzare

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

Puoi anche cambiare l'asse di confronto usando 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

Questo confronta i valori per riga, anziché per colonna.


Nota: Panda 1.1 è ancora sperimentale ed è disponibile solo creando una sandbox di sviluppo .
cs95,

-1

Una funzione che trova la differenza asimmetrica tra due frame di dati è implementata di seguito: (Basato sulla differenza impostata per i panda ) 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"')

Esempio:

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

import panda come pd import numpy as np

df = pd.read_excel ('D: \ HARISH \ DATA SCIENCE \ 1 MY Training \ SAMPLE DATA & projs \ DATA CRICKET \ ELENCO GIOCATORI IPL \ ELENCO GIOCATORI 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

nuovo = pd.concat ([srh, csk], axis = 1)

new.head ()

** TIPO DI GIOCATORE TIPO DI GIOCATORE

0 David Warner Batsman ... MS Dhoni Captain

1 bombetta Bhuvaneshwar Kumar ... Ravindra Jadeja tuttofare

2 Manish Pandey Batsman ... Suresh Raina tuttofare

3 Rashid Khan Arman Bowler ... Kedar Jadhav tuttofare

4 Shikhar Dhawan Batsman .... Dwayne Bravo tuttofare


TIPO DI GIOCATORE TIPO DI GIOCATORE 0 David Warner Batsman MS Dhoni Capitano 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
Round TRASH

Ciao Harish, ti preghiamo di formattare la tua risposta un po 'di più, altrimenti è abbastanza difficile da leggere :)
Markus
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.