Comparez deux data.frames pour trouver les lignes dans data.frame 1 qui ne sont pas présentes dans data.frame 2

161

J'ai les 2 data.frames suivants:

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

Je veux trouver la ligne a1 que a2 n'a pas.

Existe-t-il une fonction intégrée pour ce type d'opération?

(ps: j'ai écrit une solution pour cela, je suis simplement curieux de savoir si quelqu'un a déjà fait un code plus élaboré)

Voici ma solution:

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)
Tal Galili
la source

Réponses:

88

Cela ne répond pas directement à votre question, mais cela vous donnera les éléments qui sont en commun. Cela peut être fait avec le package 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 fonction comparevous offre une grande flexibilité quant au type de comparaisons autorisées (par exemple, changer l'ordre des éléments de chaque vecteur, changer l'ordre et les noms des variables, raccourcir les variables, changer la casse des chaînes). À partir de là, vous devriez pouvoir comprendre ce qui manquait à l'un ou à l'autre. Par exemple (ce n'est pas très élégant):

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
nullglob
la source
3
Je trouve cette fonction déroutante. Je pensais que cela fonctionnerait pour moi, mais cela ne semble fonctionner comme indiqué ci-dessus que si un ensemble contient des lignes identiques de l'autre ensemble. Considérez ce cas: a2 <- data.frame(a = c(1:3, 1), b = c(letters[1:3], "c")). Laissez a1le même. Maintenant, essayez la comparaison. Même en lisant les options, je ne vois pas clairement quelle est la bonne façon de ne lister que les éléments communs.
Hendy
148

SQLDF fournit une belle solution

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

Et les lignes qui sont dans les deux blocs de données:

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

La nouvelle version de dplyra une fonction anti_join,, pour exactement ce genre de comparaisons

require(dplyr) 
anti_join(a1,a2)

Et semi_joinpour filtrer les lignes a1qui sont également ena2

semi_join(a1,a2)
Rickard
la source
18
Merci pour anti_joinet semi_join!
drastega
y a-t-il une raison pour laquelle anti_join renverrait un DF nul, comme le ferait sqldf, mais les fonctions identiques (a1, a2) et all.equal () contrediraient cela?
3pitt
Je voulais juste ajouter ici que anti_join et semi_join ne fonctionneraient pas dans certains cas comme le mien. J'obtenais "Erreur: les colonnes doivent être des vecteurs ou des listes atomiques 1d" pour ma trame de données. Peut-être que je pourrais traiter mes données pour que ces fonctions fonctionnent. Sqldf a fonctionné dès la sortie de la porte!
Akshay Gaur
@AkshayGaur cela devrait être juste un problème de format de données ou de nettoyage de données; sqldf est juste sql tout est prétraité pour ressembler à nromal DB de sorte que nous pourrions simplement exécuter sql sur les données.
stucash
75

Dans dplyr :

setdiff(a1,a2)

Fondamentalement, setdiff(bigFrame, smallFrame)vous obtenez les enregistrements supplémentaires dans la première table.

Dans le SQLverse, cela s'appelle un

Gauche sans rejoindre le diagramme de Venn

Pour une bonne description de toutes les options de jointure et des sujets définis, c'est l'un des meilleurs résumés que j'ai vu assemblés à ce jour: http://www.vertabelo.com/blog/technical-articles/sql-joins

Mais revenons à cette question - voici les résultats pour le setdiff()code lors de l'utilisation des données de l'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

Ou même anti_join(a1,a2)vous obtiendrez les mêmes résultats.
Pour plus d'informations: https://www.rstudio.com/wp-content/uploads/2015/02/data-wrangling-cheatsheet.pdf

leerssej
la source
2
Puisque l'OP demande des éléments a1qui ne le sont pas a2, ne voulez-vous pas utiliser quelque chose comme semi_join(a1, a2, by = c('a','b'))? Dans la réponse de «Rickard», je vois que cela a semi_joinété suggéré.
steveb
Sûr! Un autre excellent choix, aussi; en particulier si vous avez des dataframes avec uniquement une clé de jointure et des noms de colonnes différents.
leerssej
setdiff vient de lubridate :: setdiff et non de la bibliothèque (dplyr)
mtelesha
@mtelesha - Hmm, la documentation et le code source de dplyr le montrent: ( dplyr.tidyverse.org/reference/setops.html , github.com/tidyverse/dplyr/blob/master/R/sets. ). De plus, lorsque la bibliothèque dplyr est chargée, elle signale même le masquage de la setdiff()fonction de base qui fonctionne sur deux vecteurs: stat.ethz.ch/R-manual/R-devel/library/base/html/sets.html . Peut-être avez-vous chargé la bibliothèque lubridate après dplyr et la suggère-t-elle comme source dans la liste tabcomplete?
leerssej
1
Il y a un conflit entre lubridate et dplyr, voir github.com/tidyverse/lubridate/issues/693
slhck
39

Ce n'est certainement pas efficace dans ce but précis, mais ce que je fais souvent dans ces situations est d'insérer des variables indicatrices dans chaque data.frame puis de fusionner:

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

les valeurs manquantes dans included_a1 noteront les lignes manquantes dans a1. de même pour a2.

Un problème avec votre solution est que les ordres de colonnes doivent correspondre. Un autre problème est qu'il est facile d'imaginer des situations où les lignes sont codées de la même manière alors qu'en fait sont différentes. L'avantage d'utiliser la fusion est que vous obtenez gratuitement toutes les vérifications d'erreurs nécessaires à une bonne solution.

Eduardo Leoni
la source
Alors ... en cherchant une valeur manquante, vous créez une autre valeur manquante ... Comment trouvez-vous la ou les valeurs manquantes included_a1? : - /
Louis Maddox
1
utilisez is.na () et subset, ou dplyr :: filter
Eduardo Leoni
Merci d'avoir enseigné un moyen sans installer une nouvelle bibliothèque!
Rodrigo
27

J'ai écrit un package ( https://github.com/alexsanjoseph/compareDF ) car j'ai eu le même problème.

  > 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 exemple plus compliqué:

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

Le package a également une commande html_output pour une vérification rapide

df_compare $ html_output entrez la description de l'image ici

Alex Joseph
la source
votre compareDF est exactement ce dont j'ai besoin, et j'ai fait du bon travail avec de petits ensembles.Cependant: 1) Ne fonctionne pas avec un ensemble de 50 millions de lignes avec 3 colonnes (disons), il dit qu'il n'y a plus de mémoire avec 32 Go de RAM. 2) Je vois aussi que HTML prend un certain temps à écrire, la même sortie peut-elle être envoyée dans un fichier TEXT?
Profond
1) Ouais 50 millions de lignes, c'est BEAUCOUP de données, juste à garder en mémoire;). Je suis conscient que ce n'est pas génial avec les grands ensembles de données, vous devrez peut-être faire une sorte de segmentation. 2) vous pouvez donner l'argument - limit_html = 0, pour éviter qu'il s'imprime dans un HTML. La même sortie se trouve dans compare_output $ comparaison_df que vous pouvez écrire dans un fichier CSV / TEXT en utilisant des fonctions R natives.
Alex Joseph
Merci pour votre réponse @ Alex Joseph, je vais essayer et vous dire comment ça se passe.
Profond
Salut @Alex Joseph, merci pour l'entrée, le format de texte a fonctionné mais a trouvé un problème, l'a soulevé sous: stackoverflow.com/questions/54880218/...
Profonde
Il ne peut pas gérer différents nombres de colonnes. J'ai eu une erreurThe two data frames have different columns!
PeyM87
14

Vous pouvez utiliser le daffpackage (qui enveloppe la daff.jsbibliothèque à l'aide du V8package ):

library(daff)

diff_data(data_ref = a2,
          data = a1)

produit l'objet de différence suivant:

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

Le format de diff tabulaire est décrit ici et devrait être assez explicite. Les lignes avec +++dans la première colonne @@sont celles qui sont nouvelles a1et non présentes dans a2.

L'objet difference peut être utilisé pour patch_data()stocker la différence à des fins de documentation en utilisant write_diff()ou pour visualiser la différence en utilisantrender_diff() :

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

génère une sortie HTML soignée:

entrez la description de l'image ici

Salim B
la source
10

Utilisation du diffobjpackage:

library(diffobj)

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

entrez la description de l'image ici

entrez la description de l'image ici

zx8754
la source
10

J'ai adapté le merge fonction pour obtenir cette fonctionnalité. Sur des données plus volumineuses, elle utilise moins de mémoire que la solution de fusion complète. Et je peux jouer avec les noms des colonnes clés.

Une autre solution consiste à utiliser la bibliothèque 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)
}
Henrico
la source
7

Vos données d'exemple n'ont pas de doublons, mais votre solution les gère automatiquement. Cela signifie que potentiellement certaines des réponses ne correspondent pas aux résultats de votre fonction en cas de doublons.
Voici ma solution dont l'adresse se duplique de la même manière que la vôtre. Il évolue également très 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

Il a besoin de data.table 1.9.8+

jangorecki
la source
2

C'est peut-être trop simpliste, mais j'ai utilisé cette solution et je la trouve très utile lorsque j'ai une clé primaire que je peux utiliser pour comparer des ensembles de données. J'espère que cela peut vous aider.

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,]
Kenia Sousa
la source
En quoi est-ce différent de ce que OP a déjà essayé? Vous avez utilisé exactement le même code que Tal pour comparer une seule colonne au lieu de la ligne entière (ce qui était obligatoire)
David Arenburg
1

Encore une autre solution basée sur match_df dans plyr. Voici le 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]
}

Nous pouvons le modifier pour annuler:

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

Ensuite:

diff <- negate_match_df(a1,a2)
chrisendres
la source
1

Utilisation subset:

missing<-subset(a1, !(a %in% a2$a))
Emilie
la source
Cette réponse fonctionne pour le scénario du PO. Qu'en est-il du cas plus général où la variable "a" correspond entre les deux data.frames ("a1" et "a2"), mais pas la variable "b"?
Bryan F
1

Le code suivant utilise à la fois data.tableet fastmatchpour une vitesse accrue.

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
embrasser
la source