Compare dos data.frames para encontrar las filas en data.frame 1 que no están presentes en data.frame 2


161

Tengo los siguientes 2 data.frames:

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

Quiero encontrar la fila a1 que a2 no tiene.

¿Existe una función integrada para este tipo de operación?

(PD: escribí una solución para ello, simplemente tengo curiosidad por saber si alguien ya hizo un código más elaborado)

Aquí está mi solución:

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)

Respuestas:


88

Esto no responde su pregunta directamente, pero le dará los elementos que son comunes. Esto se puede hacer con el paquete de 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 función comparele brinda mucha flexibilidad en términos de qué tipo de comparaciones están permitidas (por ejemplo, cambiar el orden de los elementos de cada vector, cambiar el orden y los nombres de las variables, acortar las variables, cambiar el caso de las cadenas). A partir de esto, deberías poder descubrir lo que faltaba en uno u otro. Por ejemplo (esto no es muy 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
Encuentro esta función confusa. Pensé que funcionaría para mí, pero parece que solo funciona como se muestra arriba si un conjunto contiene filas idénticas del otro conjunto. Considere este caso: a2 <- data.frame(a = c(1:3, 1), b = c(letters[1:3], "c")). Deja a1lo mismo. Ahora prueba la comparación. No me resulta claro, incluso al leer las opciones, cuál es la forma correcta de enumerar solo elementos comunes.
Hendy

148

SQLDF proporciona una buena solución

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

Y las filas que están en ambos marcos de datos:

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

La nueva versión de dplyrtiene una función anti_join, para exactamente este tipo de comparaciones

require(dplyr) 
anti_join(a1,a2)

Y semi_joinpara filtrar filas en a1que también están ena2

semi_join(a1,a2)

18
Gracias por anti_joiny semi_join!
drastega

¿hay alguna razón por la cual anti_join devolvería un DF nulo, como haría sqldf, pero las funciones idénticas (a1, a2) y all.equal () lo contradecirían?
3pitt

Solo quería agregar aquí que anti_join y semi_join no funcionarían en algunos casos como el mío. Estaba recibiendo "Error: las columnas deben ser 1d vectores atómicos o listas" para mi marco de datos. Tal vez podría procesar mis datos para que estas funciones funcionen. ¡Sqldf trabajó directamente desde la puerta!
Akshay Gaur

@AkshayGaur debería ser solo un formato de datos o un problema de limpieza de datos; sqldf es simplemente sql, todo está preprocesado para ser como DB nromal, de modo que podríamos ejecutar sql en los datos.
stucash

75

En dplyr :

setdiff(a1,a2)

Básicamente, setdiff(bigFrame, smallFrame)te da los registros adicionales en la primera tabla.

En el SQLverse esto se llama un

Izquierda excluyendo diagrama de Venn de Join

Para obtener buenas descripciones de todas las opciones de unión y establecer temas, este es uno de los mejores resúmenes que he visto hasta la fecha: http://www.vertabelo.com/blog/technical-articles/sql-joins

Pero volviendo a esta pregunta, estos son los resultados del setdiff()código cuando se utilizan los datos del OP:

> 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 incluso anti_join(a1,a2)obtendrá los mismos resultados.
Para más información: https://www.rstudio.com/wp-content/uploads/2015/02/data-wrangling-cheatsheet.pdf


2
Dado que el OP pide elementos a1que no están en a2, ¿no quieres usar algo como semi_join(a1, a2, by = c('a','b'))? En la respuesta de "Rickard", veo que semi_joinse sugirió.
steveb

¡Por supuesto! Otra gran opción, también; especialmente si tiene marcos de datos con solo una clave de combinación y diferentes nombres de columna.
leerssej

setdiff es de lubridate :: setdiff y no de la biblioteca (dplyr)
mtelesha

@mtelesha - Hmm, los documentos y el código fuente de dplyr muestran que está allí: ( dplyr.tidyverse.org/reference/setops.html , github.com/tidyverse/dplyr/blob/master/R/sets. ). Además, cuando se carga la biblioteca dplyr, incluso informa que enmascara la setdiff()función base que funciona en dos vectores: stat.ethz.ch/R-manual/R-devel/library/base/html/sets.html . ¿Tal vez ha cargado la biblioteca lubridate después de dplyr y está sugiriéndola como la fuente en la lista completa?
leerssej

1
Existe un conflicto entre lubridate y dplyr, vea github.com/tidyverse/lubridate/issues/693
slhck

39

Ciertamente no es eficiente para este propósito en particular, pero lo que hago a menudo en estas situaciones es insertar variables indicadoras en cada marco de datos y luego fusionarlas:

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

los valores faltantes en included_a1 indicarán qué filas faltan en a1. de manera similar para a2.

Un problema con su solución es que los pedidos de columna deben coincidir. Otro problema es que es fácil imaginar situaciones en las que las filas están codificadas de la misma manera cuando en realidad son diferentes. La ventaja de usar merge es que obtiene de forma gratuita todas las comprobaciones de errores necesarias para una buena solución.


Entonces ... al buscar un valor perdido, creas otro valor perdido ... ¿Cómo encuentras los valores faltantes included_a1? : - /
Louis Maddox

1
use is.na () y subconjunto, o dplyr :: filter
Eduardo Leoni

¡Gracias por enseñar un camino sin instalar una nueva biblioteca!
Rodrigo

27

Escribí un paquete ( https://github.com/alexsanjoseph/compareDF ) ya que tuve el mismo 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 ejemplo más complicado:

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

El paquete también tiene un comando html_output para una verificación rápida

df_compare $ html_output ingrese la descripción de la imagen aquí


su compareDF es exactamente lo que necesito, y he hecho un buen trabajo con conjuntos pequeños. Sin embargo: 1) No funciona con un conjunto de 50 millones de filas con 3 columnas (por ejemplo) dice que no tiene memoria con 32 GB de RAM. 2) También veo que HTML tarda un tiempo en escribirse, ¿se puede enviar la misma salida al archivo TEXT?
Profundo

1) Sí, 50 millones de filas son MUCHOS datos, solo para guardarlos en la memoria;). Soy consciente de que no es excelente con grandes conjuntos de datos, por lo que es posible que tenga que hacer algún tipo de fragmentación. 2) puede dar el argumento - limit_html = 0, para evitar que se imprima en un HTML. La misma salida se encuentra en compare_output $ compare_df, que puede escribir en un archivo CSV / TEXT utilizando funciones nativas de R.
Alex Joseph

Gracias por su respuesta @Alex Joseph, lo intentaré y le haré saber cómo va.
Profundo

Hola @Alex Joseph, gracias por la entrada, el formato de texto funcionó pero encontró un problema, lo planteó en: stackoverflow.com/questions/54880218/…
Profundo

No puede manejar diferentes números de columnas. Recibí un errorThe two data frames have different columns!
PeyM87

14

Puede usar el daffpaquete (que envuelve la daff.jsbiblioteca usando el V8paquete ):

library(daff)

diff_data(data_ref = a2,
          data = a1)

produce el siguiente objeto de diferencia:

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

El formato de diferencias se describe en el formato de diferencias de resaltador Coopy para tablas y debe explicarse bastante por sí mismo. Las líneas con +++en la primera columna @@son las que son nuevas a1y no están presentes en a2.

El objeto de diferencia se puede usar para patch_data()almacenar la diferencia con fines de documentación usando write_diff()o para visualizar la diferencia usandorender_diff() :

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

genera una salida HTML ordenada:

ingrese la descripción de la imagen aquí


10

Usando el diffobjpaquete:

library(diffobj)

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

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí


10

Adapté la mergefunción para obtener esta funcionalidad. En marcos de datos más grandes, utiliza menos memoria que la solución de fusión completa. Y puedo jugar con los nombres de las columnas clave.

Otra solución es usar la biblioteca 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

Sus datos de ejemplo no tienen duplicados, pero su solución los maneja automáticamente. Esto significa que potencialmente algunas de las respuestas no coincidirán con los resultados de su función en caso de duplicados.
Aquí está mi solución, cuya dirección se duplica de la misma manera que la suya. ¡También escala muy bien!

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

Necesita data.table 1.9.8+


2

Tal vez sea demasiado simplista, pero utilicé esta solución y la encuentro muy útil cuando tengo una clave principal que puedo usar para comparar conjuntos de datos. Espero que pueda ayudar.

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

¿Cómo es esto diferente de lo que OP ya intentó? Usó exactamente el mismo código como Tal para comparar una sola columna en lugar de la fila completa (que era el requisito)
David Arenburg,

1

Otra solución más basada en match_df en plyr. Aquí está el match_df de 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]
}

Podemos modificarlo para negar:

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

Luego:

diff <- negate_match_df(a1,a2)

1

Utilizando subset:

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

Esta respuesta funciona para el escenario del OP. ¿Qué pasa con el caso más general cuando la variable "a" coincide entre los dos data.frames ("a1" y "a2"), pero la variable "b" no?
Bryan F

1

El siguiente código usa ambos data.tabley fastmatchpara aumentar la velocidad.

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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.