Compare dois DataFrames e produza suas diferenças lado a lado

162

Estou tentando destacar exatamente o que mudou entre dois quadros de dados.

Suponha que eu tenha dois quadros de dados do 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

Meu objetivo é gerar uma tabela HTML que:

  1. Identifica as linhas que foram alteradas (podem ser int, float, boolean, string)
  2. Gera linhas com os mesmos valores OLD e NEW (idealmente em uma tabela HTML) para que o consumidor possa ver claramente o que mudou entre dois quadros de dados:

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

Suponho que eu poderia fazer uma comparação linha por linha e coluna por coluna, mas existe uma maneira mais fácil?

céu
fonte
No pandas 1.1, você pode fazer isso facilmente com uma única chamada de função -df.compare .
cs95 2/07

Respostas:

153

A primeira parte é semelhante a Constantine, você pode obter o booleano de quais linhas estão vazias *:

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

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

Então podemos ver quais entradas foram alteradas:

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

Aqui a primeira entrada é o índice e a segunda as colunas que foram alteradas.

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 df2compartilhe o mesmo índice aqui. Para superar essa ambiguidade, você pode garantir que apenas analise os rótulos compartilhados usando df1.index & df2.index, mas acho que deixarei isso como um exercício.

Andy Hayden
fonte
2
Acredito que "compartilhe o mesmo índice" significa "verifique se o índice está classificado" ... isso comparará o que for primeiro df1a ser o primeiro a df2entrar, independentemente do valor do índice. JFYI, caso eu não seja a única pessoa para a qual isso não era óbvio. ; D Obrigado!
Dmn
12
Se a pontuação for igual a nanem df1 e df1, esta função relatará que ela foi alterada de nanpara nan. Isso ocorre porque np.nan != np.nanretorna True.
precisa
2
@kungfujam está certo. Além disso, se os valores que estão sendo comparados são Nenhum você receberá falsas diferenças lá também
FistOfFury
Só para ficar claro - I ilustrar o problema com esta solução e proporcionar um fácil de usar função que corrige o problema abaixo
James Owers
1
['row', 'col'] é preferível a ['id', 'col'], conforme alterado.index.names, porque não são ids, mas linhas.
Naoki fujita
87

Destacando a diferença entre dois DataFrames

É possível usar a propriedade de estilo DataFrame para destacar a cor de fundo das células onde há uma diferença.

Usando os dados de exemplo da pergunta original

A primeira etapa é concatenar os DataFrames horizontalmente com a concatfunção e distinguir cada quadro com o keysparâmetro:

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

insira a descrição da imagem aqui

Provavelmente é mais fácil trocar os níveis das colunas e colocar os mesmos nomes de colunas um ao lado do outro:

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

insira a descrição da imagem aqui

Agora, é muito mais fácil identificar as diferenças nos quadros. Porém, podemos ir além e usar a stylepropriedade para destacar as células que são diferentes. Definimos uma função personalizada para fazer isso, que você pode ver nesta parte da documentação .

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)

insira a descrição da imagem aqui

Isso destacará as células que possuem valores ausentes. Você pode preenchê-los ou fornecer lógica extra para que eles não fiquem realçados.

Ted Petrou
fonte
1
Você sabe como é possível colorir 'Primeiro' e 'Segundo' em cores diferentes?
Aturegano
1
É possível selecionar apenas linhas diferentes? Nesse caso, como seleciono a segunda e a terceira linha sem selecionar a primeira linha (111)?
Shantanuo
1
@ Shantanuo, sim, basta editar o método final paradf_final[(df != df2).any(1)].style.apply(highlight_diff, axis=None)
anmol
3
Essa implementação está demorando mais tempo ao comparar quadros de dados com 26 mil linhas e 400 colunas. Existe alguma maneira de acelerar isso?
codeslord
42

Essa resposta simplesmente estende o @Andy Hayden, tornando-o resiliente para quando os campos numéricos são nane agrupando-o em uma função.

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)

Portanto, com seus dados (levemente editados para ter um NaN na coluna de pontuação):

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)

Resultado:

                from           to
id  col                          
112 score       1.11         1.21
113 isEnrolled  True        False
    Comment           On vacation
James Owers
fonte
Adicionei código para cuidar de pequenas diferenças no tipo de dados, o que geraria um erro, se você não o explicasse.
Roobie Nuby 30/03/19
E se eu não tiver linhas idênticas nos dois lados para comparar?
Kishor kumar R
@KishorkumarR então você deve mesmo fora as linhas em primeiro lugar, através da detecção de linhas adicionadas à nova trama de dados e linhas removido da velha trama de dados
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)

impressões

                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
fonte
3
Solução muito boa, muito mais compacta que a minha!
Andy Hayden
1
@ AndyHayden: Não estou totalmente à vontade com esta solução; parece funcionar apenas quando o índice é um índice multinível. Se eu tentar usando apenas idcomo o índice, em seguida, df.groupby(level='id')gera um erro, e eu não sei porquê ...
unutbu
19

Eu enfrentei esse problema, mas encontrei uma resposta antes de encontrar este post:

Com base na resposta da unutbu, carregue seus dados ...

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

... defina sua função diff ...

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

Então você pode simplesmente usar um painel 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

A propósito, se você estiver no IPython Notebook, poderá usar uma função diff colorida para fornecer cores, dependendo se as células forem diferentes, iguais ou nulas esquerda / direita:

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
fonte
(No Python comum, não no notebook iPython) é possível incluir my_panel = pd.Panel(dict(df1=df1,df2=df2))dentro da função report_diff()? Quero dizer, é possível fazer isso: print report_diff(df1,df2)e obter a mesma saída que sua declaração de impressão?
22715 Edesz
pd.Panel(dict(df1=df1,df2=df2)).apply(report_diff, axis=0)- isso é incrível!!!
MaxU
5
Os painéis estão obsoletos! Alguma idéia de como portar isso?
Denfromufa # 9/17
@denfromufa Tomei um acerto ao atualizá-lo na minha resposta: stackoverflow.com/a/49038417/7607701 #
Aaron N. Brock
9

Se seus dois quadros de dados tiverem os mesmos IDs, descobrir o que mudou é realmente muito fácil. Apenas fazer frame1 != frame2lhe dará um DataFrame booleano, onde cada Trueum deles é alterado. Com isso, é possível obter facilmente o índice de cada linha alterada changedids = frame1.index[np.any(frame1 != frame2,axis=1)].

cge
fonte
6

Uma abordagem diferente usando 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)

Resultado:

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

Depois de brincar com a resposta do @ journois, consegui fazê-lo funcionar usando o MultiIndex em vez do Panel devido à privação do Panel .

Primeiro, crie alguns dados fictícios:

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

Em seguida, defina sua função diff , neste caso, usarei a da resposta dele report_diffa mesma:

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

Então, vou concatenar os dados em um quadro de dados 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, finalmente, vou aplicar o report_diffgrupo abaixo de cada coluna:

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

Isso gera:

         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 isso é tudo!

Aaron N. Brock
fonte
3

Estendendo a resposta do @cge, que é bem legal para obter mais legibilidade do 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('')

Exemplo completo de demonstração:

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

Aqui está outra maneira de selecionar e mesclar:

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

Aqui está o mesmo de uma captura de tela do Jupyter:

insira a descrição da imagem aqui

Aziz Alto
fonte
0

pandas> = 1.1: DataFrame.compare

Com o pandas 1.1, você pode essencialmente replicar a saída de Ted Petrou com uma única chamada de função. Exemplo retirado dos 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

Aqui, "self" refere-se ao dataFrame do LHS, enquanto "outro" é o DataFrame do RHS. Por padrão, valores iguais são substituídos por NaNs para que você possa se concentrar apenas nas diferenças. Se você deseja mostrar valores iguais também, 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

Você também pode alterar o eixo de comparação 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

Isso compara valores em linhas, em vez de em colunas.

cs95
fonte
Nota: o pandas 1.1 ainda é experimental e está disponível apenas através da construção de uma sandbox de desenvolvimento .
cs95 2/07
-1

Uma função que encontra diferença assimétrica entre dois quadros de dados é implementada abaixo: (Com base na diferença definida 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"')

Exemplo:

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

importar pandas como pd importar numpy como np

df = pd.read_excel ('D: \ HARISH \ DATA SCIENCE \ 1 MEU treinamento \ AMOSTRA DE DADOS E 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], eixo = 1)

new.head ()

** TIPO DE JOGADOR TIPO DE JOGADOR

0 David Warner Batsman ... MS Dhoni Capitão

1 Bhuvaneshwar Kumar Bowler ... Ravindra Jadeja Polivalente

2 Manish Pandey Batsman ... Suresh Raina Polivalente

3 Rashid Khan Arman Bowler ... Kedar Jadhav Polivalente

4 Shikhar Dhawan Batsman ... Dwayne Bravo Polivalente

LIXO HARISH
fonte
LEITOR DE TIPO JOGADOR TIPO 0 David Warner batedor MS Dhoni Capitão 1 Bhuvaneshwar Kumar Bowler Ravindra Jadeja polivalente 2 Manish Pandey batedor Suresh Raina polivalente 3 Rashid Khan Arman Bowler Cedar Jadhav polivalente 4 Shikhar Dhawan batedor Dwayne Bravo polivalente
HARISH TRASH
Olá Harish, formate sua resposta um pouco mais, caso contrário, é muito difícil de ler :)
Markus