Compare dos DataFrames y muestre sus diferencias una al lado de la otra

162

Estoy tratando de resaltar exactamente lo que cambió entre dos marcos de datos.

Supongamos que tengo dos marcos de datos de 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

Mi objetivo es generar una tabla HTML que:

  1. Identifica las filas que han cambiado (podría ser int, float, boolean, string)
  2. Emite filas con los mismos valores, VIEJO y NUEVO (idealmente en una tabla HTML) para que el consumidor pueda ver claramente qué cambió entre dos marcos de datos:

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

Supongo que podría hacer una comparación fila por fila y columna por columna, pero ¿hay alguna manera más fácil?

cielo
fuente
Desde pandas 1.1 puede hacer esto fácilmente con una sola llamada de función -df.compare .
cs95

Respuestas:

153

La primera parte es similar a Constantine, puede obtener el valor booleano de qué filas están vacías *:

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

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

Luego podemos ver qué entradas han cambiado:

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

Aquí la primera entrada es el índice y la segunda las columnas que se han modificado.

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: es importante que df1y df2comparta el mismo índice aquí. Para superar esta ambigüedad, puede asegurarse de que solo mira las etiquetas compartidas usando df1.index & df2.index, pero creo que lo dejaré como un ejercicio.

Andy Hayden
fuente
2
Creo que "compartir el mismo índice" significa "asegurarse de que el índice esté ordenado" ... esto comparará lo que sea primero en df1lo que sea primero df2, independientemente del valor del índice. JFYI en caso de que no sea la única persona para la que esto no era obvio. ; D Gracias!
dmn
12
Si el puntaje es igual nanen df1 y df1, esta función informará que ha cambiado de nana nan. Esto es porque np.nan != np.nanregresa True.
James Owers
2
@kungfujam tiene razón. Además, si los valores que se comparan son Ninguno, también obtendrá falsas diferencias
FistOfFury
Para que quede claro: ilustro el problema con esta solución y proporciono una función fácil de usar que soluciona el problema a continuación
James Owers
1
['row', 'col'] es preferible a ['id', 'col'] como se cambió.index.names, porque no son identificadores, sino filas.
naoki fujita
87

Destacando la diferencia entre dos DataFrames

Es posible usar la propiedad de estilo DataFrame para resaltar el color de fondo de las celdas donde hay una diferencia.

Usando los datos de ejemplo de la pregunta original

El primer paso es concatenar los marcos de datos horizontalmente con la concatfunción y distinguir cada marco con el keysparámetro:

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

ingrese la descripción de la imagen aquí

Probablemente sea más fácil intercambiar los niveles de columna y poner los mismos nombres de columna uno al lado del otro:

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

ingrese la descripción de la imagen aquí

Ahora, es mucho más fácil detectar las diferencias en los cuadros. Pero, podemos ir más allá y usar la stylepropiedad para resaltar las celdas que son diferentes. Definimos una función personalizada para hacer esto que puede ver en esta parte de la documentación .

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)

ingrese la descripción de la imagen aquí

Esto resaltará las celdas que tienen valores faltantes. Puede llenarlos o proporcionar una lógica adicional para que no se resalten.

Ted Petrou
fuente
1
¿Sabes cómo es posible colorear tanto 'Primero' como 'Segundo' en diferentes colores?
aturegano
1
¿Es posible seleccionar solo filas diferentes? En este caso, ¿cómo selecciono la segunda y la tercera fila sin seleccionar la primera fila (111)?
shantanuo
1
@shantanuo, sí, solo edite el método final paradf_final[(df != df2).any(1)].style.apply(highlight_diff, axis=None)
anmol
3
Esta implementación lleva más tiempo cuando se comparan marcos de datos con 26K filas y 400 columnas. ¿Hay alguna forma de acelerarlo?
codelord
42

Esta respuesta simplemente extiende @Andy Hayden's, haciéndolo resistente a los campos numéricos nany envolviéndolo en una función.

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)

Entonces, con sus datos (ligeramente editados para tener un NaN en la columna de puntuación):

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)

Salida:

                from           to
id  col                          
112 score       1.11         1.21
113 isEnrolled  True        False
    Comment           On vacation
James Owers
fuente
Agregué código para solucionar pequeñas diferencias en el tipo de datos, lo que arrojaría un error, si no lo contabilizaras.
Roobie Nuby
¿Qué sucede si no tengo filas idénticas en ambos lados para comparar?
Kishor kumar R
@KishorkumarR, entonces debería emparejar primero las filas, detectando filas agregadas al nuevo marco de datos y eliminando filas del marco de datos anterior
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)

huellas dactilares

                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
fuente
3
¡Muy buena solución, mucho más compacta que la mía!
Andy Hayden
1
@AndyHayden: No estoy del todo cómoda con esta solución; parece funcionar solo cuando el índice es un índice multinivel. Si trato de usar solo idcomo índice, df.groupby(level='id')aparece un error, y no estoy seguro de por qué ...
unutbu
19

Me he enfrentado a este problema, pero encontré una respuesta antes de encontrar esta publicación:

Según la respuesta de unutbu, cargue sus datos ...

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

... define tu función diff ...

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

Entonces puede simplemente usar un Panel para concluir:

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

Por cierto, si está en IPython Notebook, es posible que desee utilizar una función de diferencias de color para dar colores dependiendo de si las celdas son diferentes, iguales o nulas izquierda / derecha:

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
fuente
(En Python normal, no en el portátil iPython) ¿es posible incluirlo my_panel = pd.Panel(dict(df1=df1,df2=df2))dentro de la función report_diff()? Quiero decir, ¿es posible hacer esto: print report_diff(df1,df2)y obtener el mismo resultado que su declaración de impresión?
edesz
pd.Panel(dict(df1=df1,df2=df2)).apply(report_diff, axis=0)- esto es genial !!!
MaxU
55
Los paneles están en desuso! ¿Alguna idea de cómo portar esto?
denfromufa
@denfromufa Me animé a actualizarlo en mi respuesta: stackoverflow.com/a/49038417/7607701
Aaron N. Brock
9

Si sus dos marcos de datos tienen los mismos identificadores, descubrir qué ha cambiado es bastante fácil. Simplemente hacerlo frame1 != frame2le dará un Marco de Truedatos booleano donde cada uno es información que ha cambiado. A partir de eso, puede obtener fácilmente el índice de cada fila modificada haciendo changedids = frame1.index[np.any(frame1 != frame2,axis=1)].

cge
fuente
6

Un enfoque diferente usando concat y 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)

Salida:

       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
fuente
3

Después de jugar con la respuesta de @ journois, pude hacer que funcionara usando MultiIndex en lugar de Panel debido a la depricación de Panel .

Primero, cree algunos datos ficticios:

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

Luego, defina su función diff , en este caso usaré la de su respuesta que report_diffpermanece igual:

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

Luego, voy a concatenar los datos en un marco de datos 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:]]

Y finalmente voy a aplicar el report_diffgrupo de cada columna hacia abajo:

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

Esto 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

¡Y eso es todo!

Aaron N. Brock
fuente
3

Ampliando la respuesta de @cge, que es bastante bueno para una mayor legibilidad del resultado:

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

Ejemplo de demostración completa:

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
fuente
1

Aquí hay otra forma de usar select y 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

Aquí está lo mismo de una captura de pantalla de Jupyter:

ingrese la descripción de la imagen aquí

Aziz Alto
fuente
0

pandas> = 1.1: DataFrame.compare

Con pandas 1.1, esencialmente podría replicar la salida de Ted Petrou con una sola llamada de función. Ejemplo tomado de los documentos:

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

Aquí, "self" se refiere al LHS dataFrame, mientras que "other" es el RHS DataFrame. Por defecto, los valores iguales se reemplazan con NaN para que pueda centrarse solo en las diferencias. Si también desea mostrar valores que sean iguales, use

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

También puede cambiar el eje de comparación 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

Esto compara los valores en fila, en lugar de en columna.

cs95
fuente
Nota: pandas 1.1 todavía es experimental y solo está disponible mediante la creación de un entorno limitado de desarrollo .
cs95
-1

A continuación se implementa una función que encuentra la diferencia asimétrica entre dos marcos de datos: (Basado en la diferencia establecida para 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"')

Ejemplo:

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
fuente
-1

importar pandas como pd importar numpy como 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 ['EQUIPO']. str.contains ("SRH")] df2 = csk = df [df ['EQUIPO']. 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

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

new.head ()

** TIPO DE JUGADOR TIPO DE JUGADOR

0 David Warner Batsman ... MS Dhoni Capitán

1 Bhuvaneshwar Kumar Bowler ... Ravindra Jadeja All-Rounder

2 Manish Pandey Batsman ... Suresh Raina All-Rounder

3 Rashid Khan Arman Bowler ... Kedar Jadhav All-Rounder

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

HARISH BASURA
fuente
REPRODUCTOR DE TIPO DE JUGADORES TIPO 0 David Warner Batsman MS Dhoni capitán 1 Bhuvaneshwar Kumar Bowler Ravindra Jadeja All-Rounder 2 Manish Pandey Batsman Suresh Raina All-Rounder 3 Rashid Khan Arman Bowler Kedar Jadhav All-Rounder 4 Shikhar Dhawan Batsman Dwayne Bravo All-Rounder
HARISH BASURA
Hola Harish, formatea tu respuesta un poco más, de lo contrario es bastante difícil de leer :)
Markus