python - filas - Salida de diferencia en dos marcos de datos de Pandas uno al lado del otro-destacando la diferencia
pandas dataframe (10)
Destacando la diferencia entre dos DataFrames
Es posible utilizar 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 DataFrames horizontalmente con la función concat
y distinguir cada cuadro con el parámetro keys
:
df_all = pd.concat([df.set_index(''id''), df2.set_index(''id'')],
axis=''columns'', keys=[''First'', ''Second''])
df_all
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
Ahora, es mucho más fácil detectar las diferencias en los cuadros. Pero, podemos ir más allá y usar la propiedad de style
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)
Esto resaltará las celdas que tienen valores faltantes. Puede llenarlos o proporcionar una lógica adicional para que no se destaquen.
Estoy tratando de resaltar exactamente qué cambió entre dos dataframes.
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:
- Identifica las filas que han cambiado (podría ser int, float, boolean, string)
Salidas de filas con valores iguales, ANTERIORES y NUEVOS (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 columna y columna por columna, pero ¿hay alguna manera más fácil?
Aquí hay otra forma de usar seleccionar y fusionar:
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:
Después de jugar con la respuesta de @ journois, pude hacer que funcione usando MultiIndex en lugar de Panel debido a la privación del Panel .
Primero, crea 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 report_diff
de su respuesta. report_diff
mantiene 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_diff
en cada grupo de columnas:
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!
Esta respuesta simplemente amplía la de @Andy Hayden, haciéndola resistente a cuando los campos numéricos son nan
, y 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 puntaje):
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
Extendiendo la respuesta de @cge, que es bastante bueno para una mayor legibilidad del resultado:
a[a != b][np.any(a != b, axis=1)].join(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 completo de demostración:
a = 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(DataFrame(''a<->b'', index=a.index, columns=[''a<=>b''])).join(
b[a != b][np.any(a != b, axis=1)]
,rsuffix=''_b'', how=''outer''
).fillna('''')
La primera parte es similar a Constantine, puedes obtener el booleano cuyas 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
Entonces 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 cambiado.
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 df1
y df2
compartan el mismo índice aquí. Para superar esta ambigüedad, puedes asegurarte de que solo miras las etiquetas compartidas usando df1.index & df2.index
, pero creo que lo dejo como ejercicio.
Me he enfrentado a este problema, pero encontré una respuesta antes de encontrar esta publicación:
Basado en la respuesta de unutbu, carga tus 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.BytesIO(texts[0]), widths=[5,7,25,17,20], parse_dates=[4])
df2 = pd.read_fwf(io.BytesIO(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 puedes 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ás en IPython Notebook, te puede gustar usar una función de color diff 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))
Si sus dos marcos de datos tienen los mismos identificadores, descubrir qué fue lo que cambió realmente es bastante fácil. Simplemente haciendo frame1 != frame2
le dará un DataFrame booleano donde cada True
es información que ha cambiado. A partir de ahí, puede obtener fácilmente el índice de cada fila modificada haciendo changedids = frame1.index[np.any(frame1 != frame2,axis=1)]
.
Un enfoque diferente que usa 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
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.BytesIO(texts[0]), widths=[5,7,25,21,20])
df2 = pd.read_fwf(io.BytesIO(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