Confronta due data.frames per trovare le righe in data.frame 1 che non sono presenti in data.frame 2


161

Ho i seguenti 2 data.frames:

a1 <- data.frame(a = 1:5, b=letters[1:5])
a2 <- data.frame(a = 1:3, b=letters[1:3])

Voglio trovare la riga a1 che ha a2 no.

Esiste una funzione integrata per questo tipo di operazione?

(ps: ho scritto una soluzione per questo, sono semplicemente curioso di sapere se qualcuno ha già creato un codice più elaborato)

Ecco la mia soluzione:

a1 <- data.frame(a = 1:5, b=letters[1:5])
a2 <- data.frame(a = 1:3, b=letters[1:3])

rows.in.a1.that.are.not.in.a2  <- function(a1,a2)
{
    a1.vec <- apply(a1, 1, paste, collapse = "")
    a2.vec <- apply(a2, 1, paste, collapse = "")
    a1.without.a2.rows <- a1[!a1.vec %in% a2.vec,]
    return(a1.without.a2.rows)
}
rows.in.a1.that.are.not.in.a2(a1,a2)

Risposte:


88

Questo non risponde direttamente alla tua domanda, ma ti darà gli elementi che sono in comune. Questo può essere fatto con il pacchetto di Paul Murrell compare:

library(compare)
a1 <- data.frame(a = 1:5, b = letters[1:5])
a2 <- data.frame(a = 1:3, b = letters[1:3])
comparison <- compare(a1,a2,allowAll=TRUE)
comparison$tM
#  a b
#1 1 a
#2 2 b
#3 3 c

La funzione compareoffre molta flessibilità in termini di quale tipo di confronto è consentito (ad esempio, cambiare l'ordine degli elementi di ciascun vettore, cambiare l'ordine e i nomi delle variabili, accorciare le variabili, cambiare il caso delle stringhe). Da questo, dovresti essere in grado di capire cosa mancava dall'uno o dall'altro. Ad esempio (questo non è molto elegante):

difference <-
   data.frame(lapply(1:ncol(a1),function(i)setdiff(a1[,i],comparison$tM[,i])))
colnames(difference) <- colnames(a1)
difference
#  a b
#1 4 d
#2 5 e

3
Trovo questa funzione confusa. Ho pensato che avrebbe funzionato per me, ma sembra funzionare come mostrato sopra se un set contiene righe identicamente corrispondenti dell'altro set. Considerate questo caso: a2 <- data.frame(a = c(1:3, 1), b = c(letters[1:3], "c")). Lascia a1lo stesso. Ora prova il confronto. Non mi è chiaro nemmeno nel leggere le opzioni quale sia il modo corretto di elencare solo gli elementi comuni.
Hendy,

148

SQLDF fornisce una buona soluzione

a1 <- data.frame(a = 1:5, b=letters[1:5])
a2 <- data.frame(a = 1:3, b=letters[1:3])

require(sqldf)

a1NotIna2 <- sqldf('SELECT * FROM a1 EXCEPT SELECT * FROM a2')

E le righe che si trovano in entrambi i frame di dati:

a1Ina2 <- sqldf('SELECT * FROM a1 INTERSECT SELECT * FROM a2')

La nuova versione di dplyrha una funzione, anti_joinproprio per questo tipo di confronti

require(dplyr) 
anti_join(a1,a2)

E semi_joinper filtrare le righe a1che sono anche ina2

semi_join(a1,a2)

18
Grazie per anti_joine semi_join!
drastega,

c'è un motivo per cui anti_join restituirebbe un DF nullo, così come sqldf, ma le funzioni identiche (a1, a2) e all.equal () lo contraddirebbero?
3pitt

Volevo solo aggiungere che anti_join e semi_join non avrebbero funzionato in alcuni casi come il mio. Stavo ottenendo "Errore: le colonne devono essere 1d vettori o elenchi atomici" per il mio frame di dati. Forse potrei elaborare i miei dati in modo che queste funzioni funzionino. Sqldf ha funzionato subito fuori dal cancello!
Akshay Gaur,

@AkshayGaur dovrebbe essere solo un formato di dati o un problema di pulizia dei dati; sqldf è solo sql, tutto è pre-elaborato per essere come DB nromal in modo da poter eseguire sql sui dati.
Stucash

75

In dplyr :

setdiff(a1,a2)

Fondamentalmente, setdiff(bigFrame, smallFrame)ti dà i record extra nella prima tabella.

In SQLverse questo si chiama a

Sinistra escluso Join Venn Diagram

Per una buona descrizione di tutte le opzioni di join e argomenti impostati, questo è uno dei migliori riassunti che ho visto messi insieme fino ad oggi: http://www.vertabelo.com/blog/technical-articles/sql-joins

Ma torniamo a questa domanda: ecco i risultati del setdiff()codice quando si utilizzano i dati del PO:

> a1
  a b
1 1 a
2 2 b
3 3 c
4 4 d
5 5 e

> a2
  a b
1 1 a
2 2 b
3 3 c

> setdiff(a1,a2)
  a b
1 4 d
2 5 e

O anche anti_join(a1,a2)otterrai gli stessi risultati.
Per maggiori informazioni: https://www.rstudio.com/wp-content/uploads/2015/02/data-wrangling-cheatsheet.pdf


2
Dal momento che l'OP richiede elementi a1che non sono presenti a2, non vuoi usare qualcosa del genere semi_join(a1, a2, by = c('a','b'))? Nella risposta di "Rickard", vedo che è semi_joinstato suggerito.
steveb,

Sicuro! Un'altra ottima scelta, anche; in particolare se disponi di frame di dati con solo una chiave di join e nomi di colonna diversi.
leerssej,

setdiff è di lubridate :: setdiff e non di library (dplyr)
mtelesha,

@mtelesha - Hmm, i documenti e il codice sorgente di dplyr mostrano che si trova lì: ( dplyr.tidyverse.org/reference/setops.html , github.com/tidyverse/dplyr/blob/master/R/sets. ). Inoltre, quando la libreria dplyr viene caricata, segnala persino di mascherare la setdiff()funzione di base che funziona su due vettori: stat.ethz.ch/R-manual/R-devel/library/base/html/sets.html . Forse hai caricato la libreria lubridate dopo dplyr e la sta suggerendo come fonte nell'elenco tablplete?
leerssej,

1
C'è un conflitto tra lubridate e dplyr, vedi github.com/tidyverse/lubridate/issues/693
slhck

39

Certamente non è efficiente per questo scopo particolare, ma ciò che faccio spesso in queste situazioni è inserire variabili indicatore in ciascun data.frame e quindi unire:

a1$included_a1 <- TRUE
a2$included_a2 <- TRUE
res <- merge(a1, a2, all=TRUE)

i valori mancanti in included_a1 noteranno quali righe mancano in a1. allo stesso modo per a2.

Un problema con la tua soluzione è che gli ordini di colonna devono corrispondere. Un altro problema è che è facile immaginare situazioni in cui le righe sono codificate come le stesse quando in realtà sono diverse. Il vantaggio dell'utilizzo di merge è che si ottiene gratuitamente tutto il controllo degli errori necessario per una buona soluzione.


Quindi ... nella ricerca di un valore mancante, si crea un altro valore mancante ... Come si trovano i valori mancanti in included_a1? : - /
Louis Maddox,

1
usa is.na () e subset, o dplyr :: filter
Eduardo Leoni,

Grazie per aver insegnato un modo senza installare una nuova libreria!
Rodrigo,

27

Ho scritto un pacchetto ( https://github.com/alexsanjoseph/compareDF ) poiché ho avuto lo stesso problema.

  > df1 <- data.frame(a = 1:5, b=letters[1:5], row = 1:5)
  > df2 <- data.frame(a = 1:3, b=letters[1:3], row = 1:3)
  > df_compare = compare_df(df1, df2, "row")

  > df_compare$comparison_df
    row chng_type a b
  1   4         + 4 d
  2   5         + 5 e

Un esempio più complicato:

library(compareDF)
df1 = data.frame(id1 = c("Mazda RX4", "Mazda RX4 Wag", "Datsun 710",
                         "Hornet 4 Drive", "Duster 360", "Merc 240D"),
                 id2 = c("Maz", "Maz", "Dat", "Hor", "Dus", "Mer"),
                 hp = c(110, 110, 181, 110, 245, 62),
                 cyl = c(6, 6, 4, 6, 8, 4),
                 qsec = c(16.46, 17.02, 33.00, 19.44, 15.84, 20.00))

df2 = data.frame(id1 = c("Mazda RX4", "Mazda RX4 Wag", "Datsun 710",
                         "Hornet 4 Drive", " Hornet Sportabout", "Valiant"),
                 id2 = c("Maz", "Maz", "Dat", "Hor", "Dus", "Val"),
                 hp = c(110, 110, 93, 110, 175, 105),
                 cyl = c(6, 6, 4, 6, 8, 6),
                 qsec = c(16.46, 17.02, 18.61, 19.44, 17.02, 20.22))

> df_compare$comparison_df
    grp chng_type                id1 id2  hp cyl  qsec
  1   1         -  Hornet Sportabout Dus 175   8 17.02
  2   2         +         Datsun 710 Dat 181   4 33.00
  3   2         -         Datsun 710 Dat  93   4 18.61
  4   3         +         Duster 360 Dus 245   8 15.84
  5   7         +          Merc 240D Mer  62   4 20.00
  6   8         -            Valiant Val 105   6 20.22

Il pacchetto ha anche un comando html_output per un controllo rapido

df_compare $ html_output inserisci qui la descrizione dell'immagine


il tuo compareDF è esattamente ciò di cui ho bisogno e ho fatto un buon lavoro con piccoli set.Tuttavia: 1) Non funziona con un set 50 Milioni di righe con 3 colonne (diciamo) dice che è esaurito con 32 GB di RAM. 2) Vedo anche che HTML richiede del tempo per scrivere, lo stesso output può essere inviato al file TEXT?
Profondo

1) Sì, 50 milioni di righe sono MOLTI dati, solo per essere conservati in memoria;). Sono consapevole che non è eccezionale con set di dati di grandi dimensioni, quindi potresti dover eseguire una sorta di chunking. 2) puoi dare l'argomento - limit_html = 0, per evitare che venga stampato su HTML. Lo stesso output è in compare_output $ compare_df che è possibile scrivere su una fule CSV / TEXT usando le funzioni R native.
Alex Joseph,

Grazie per la tua risposta @Alex Joseph, ci proverò e ti farò sapere come va.
Profondo

Ciao @Alex Joseph, grazie per l'input che il formato del testo ha funzionato ma ha trovato un problema, sollevato sotto: stackoverflow.com/questions/54880218/…
Deep

Non può gestire diversi numeri di colonne. Ho ricevuto un erroreThe two data frames have different columns!
PeyM87

14

È possibile utilizzare il daffpacchetto (che avvolge la daff.jslibreria utilizzando il V8pacchetto ):

library(daff)

diff_data(data_ref = a2,
          data = a1)

produce il seguente oggetto differenza:

Daff Comparison: ‘a2’ vs. ‘a1’ 
  First 6 and last 6 patch lines:
   @@   a   b
1 ... ... ...
2       3   c
3 +++   4   d
4 +++   5   e
5 ... ... ...
6 ... ... ...
7       3   c
8 +++   4   d
9 +++   5   e

Il formato diff tabulare è descritto qui e dovrebbe essere piuttosto autoesplicativo. Le righe con +++nella prima colonna @@sono quelle nuove in a1e non presenti in a2.

L'oggetto differenza può essere utilizzato per patch_data()memorizzare la differenza a scopo di documentazione utilizzando write_diff()o per visualizzare la differenza utilizzandorender_diff() :

render_diff(
    diff_data(data_ref = a2,
              data = a1)
)

genera un output HTML pulito:

inserisci qui la descrizione dell'immagine


10

Utilizzando il diffobjpacchetto:

library(diffobj)

diffPrint(a1, a2)
diffObj(a1, a2)

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine


10

Ho adattato la mergefunzione per ottenere questa funzionalità. Su frame di dati più grandi utilizza meno memoria rispetto alla soluzione di unione completa. E posso giocare con i nomi delle colonne chiave.

Un'altra soluzione è utilizzare la libreria prob.

#  Derived from src/library/base/R/merge.R
#  Part of the R package, http://www.R-project.org
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  A copy of the GNU General Public License is available at
#  http://www.r-project.org/Licenses/

XinY <-
    function(x, y, by = intersect(names(x), names(y)), by.x = by, by.y = by,
             notin = FALSE, incomparables = NULL,
             ...)
{
    fix.by <- function(by, df)
    {
        ## fix up 'by' to be a valid set of cols by number: 0 is row.names
        if(is.null(by)) by <- numeric(0L)
        by <- as.vector(by)
        nc <- ncol(df)
        if(is.character(by))
            by <- match(by, c("row.names", names(df))) - 1L
        else if(is.numeric(by)) {
            if(any(by < 0L) || any(by > nc))
                stop("'by' must match numbers of columns")
        } else if(is.logical(by)) {
            if(length(by) != nc) stop("'by' must match number of columns")
            by <- seq_along(by)[by]
        } else stop("'by' must specify column(s) as numbers, names or logical")
        if(any(is.na(by))) stop("'by' must specify valid column(s)")
        unique(by)
    }

    nx <- nrow(x <- as.data.frame(x)); ny <- nrow(y <- as.data.frame(y))
    by.x <- fix.by(by.x, x)
    by.y <- fix.by(by.y, y)
    if((l.b <- length(by.x)) != length(by.y))
        stop("'by.x' and 'by.y' specify different numbers of columns")
    if(l.b == 0L) {
        ## was: stop("no columns to match on")
        ## returns x
        x
    }
    else {
        if(any(by.x == 0L)) {
            x <- cbind(Row.names = I(row.names(x)), x)
            by.x <- by.x + 1L
        }
        if(any(by.y == 0L)) {
            y <- cbind(Row.names = I(row.names(y)), y)
            by.y <- by.y + 1L
        }
        ## create keys from 'by' columns:
        if(l.b == 1L) {                  # (be faster)
            bx <- x[, by.x]; if(is.factor(bx)) bx <- as.character(bx)
            by <- y[, by.y]; if(is.factor(by)) by <- as.character(by)
        } else {
            ## Do these together for consistency in as.character.
            ## Use same set of names.
            bx <- x[, by.x, drop=FALSE]; by <- y[, by.y, drop=FALSE]
            names(bx) <- names(by) <- paste("V", seq_len(ncol(bx)), sep="")
            bz <- do.call("paste", c(rbind(bx, by), sep = "\r"))
            bx <- bz[seq_len(nx)]
            by <- bz[nx + seq_len(ny)]
        }
        comm <- match(bx, by, 0L)
        if (notin) {
            res <- x[comm == 0,]
        } else {
            res <- x[comm > 0,]
        }
    }
    ## avoid a copy
    ## row.names(res) <- NULL
    attr(res, "row.names") <- .set_row_names(nrow(res))
    res
}


XnotinY <-
    function(x, y, by = intersect(names(x), names(y)), by.x = by, by.y = by,
             notin = TRUE, incomparables = NULL,
             ...)
{
    XinY(x,y,by,by.x,by.y,notin,incomparables)
}

7

I dati di esempio non hanno duplicati, ma la soluzione li gestisce automaticamente. Ciò significa che potenzialmente alcune delle risposte non corrisponderanno ai risultati della tua funzione in caso di duplicati.
Ecco la mia soluzione che indirizza i duplicati nello stesso modo del tuo. Ridimensiona anche alla grande!

a1 <- data.frame(a = 1:5, b=letters[1:5])
a2 <- data.frame(a = 1:3, b=letters[1:3])
rows.in.a1.that.are.not.in.a2  <- function(a1,a2)
{
    a1.vec <- apply(a1, 1, paste, collapse = "")
    a2.vec <- apply(a2, 1, paste, collapse = "")
    a1.without.a2.rows <- a1[!a1.vec %in% a2.vec,]
    return(a1.without.a2.rows)
}

library(data.table)
setDT(a1)
setDT(a2)

# no duplicates - as in example code
r <- fsetdiff(a1, a2)
all.equal(r, rows.in.a1.that.are.not.in.a2(a1,a2))
#[1] TRUE

# handling duplicates - make some duplicates
a1 <- rbind(a1, a1, a1)
a2 <- rbind(a2, a2, a2)
r <- fsetdiff(a1, a2, all = TRUE)
all.equal(r, rows.in.a1.that.are.not.in.a2(a1,a2))
#[1] TRUE

Richiede data.table 1.9.8+


2

Forse è troppo semplicistico, ma ho usato questa soluzione e la trovo molto utile quando ho una chiave primaria che posso usare per confrontare i set di dati. Spero possa essere d'aiuto.

a1 <- data.frame(a = 1:5, b = letters[1:5])
a2 <- data.frame(a = 1:3, b = letters[1:3])
different.names <- (!a1$a %in% a2$a)
not.in.a2 <- a1[different.names,]

In che modo è diverso da ciò che OP ha già provato? Hai usato lo stesso identico codice di Tal per confrontare una singola colonna anziché l'intera riga (che era il requisito)
David Arenburg,

1

Ancora un'altra soluzione basata su match_df in plyr. Ecco il match_df di plyr:

match_df <- function (x, y, on = NULL) 
{
    if (is.null(on)) {
        on <- intersect(names(x), names(y))
        message("Matching on: ", paste(on, collapse = ", "))
    }
    keys <- join.keys(x, y, on)
    x[keys$x %in% keys$y, , drop = FALSE]
}

Possiamo modificarlo per negare:

library(plyr)
negate_match_df <- function (x, y, on = NULL) 
{
    if (is.null(on)) {
        on <- intersect(names(x), names(y))
        message("Matching on: ", paste(on, collapse = ", "))
    }
    keys <- join.keys(x, y, on)
    x[!(keys$x %in% keys$y), , drop = FALSE]
}

Poi:

diff <- negate_match_df(a1,a2)

1

Utilizzando subset:

missing<-subset(a1, !(a %in% a2$a))

Questa risposta funziona per lo scenario del PO. Che dire del caso più generale quando la variabile "a" corrisponde tra i due data.frames ("a1" e "a2"), ma la variabile "b" no?
Bryan F,

1

Il codice seguente utilizza entrambi data.tablee fastmatchper una maggiore velocità.

library("data.table")
library("fastmatch")

a1 <- setDT(data.frame(a = 1:5, b=letters[1:5]))
a2 <- setDT(data.frame(a = 1:3, b=letters[1:3]))

compare_rows <- a1$a %fin% a2$a
# the %fin% function comes from the `fastmatch` package

added_rows <- a1[which(compare_rows == FALSE)]

added_rows

#    a b
# 1: 4 d
# 2: 5 e
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.