Porównaj dwa data.frames, aby znaleźć wiersze w data.frame 1, których nie ma w data.frame 2


161

Mam następujące 2 data.frames:

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

Chcę znaleźć wiersz a1, w którym a2 nie.

Czy istnieje wbudowana funkcja dla tego typu operacji?

(ps: napisałem rozwiązanie na to, jestem po prostu ciekawy, czy ktoś już zrobił bardziej spreparowany kod)

Oto moje rozwiązanie:

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)

Odpowiedzi:


88

To nie jest bezpośrednią odpowiedzią na twoje pytanie, ale daje ci elementy, które są wspólne. Można to zrobić za pomocą pakietu Paula Murrella 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

Funkcja comparedaje dużą elastyczność w zakresie tego, jakiego rodzaju porównania są dozwolone (np. Zmiana kolejności elementów każdego wektora, zmiana kolejności i nazw zmiennych, skracanie zmiennych, zmiana wielkości liter w łańcuchach). Na tej podstawie powinieneś być w stanie dowiedzieć się, czego brakowało w jednym lub drugim. Na przykład (to nie jest zbyt eleganckie):

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
Uważam, że ta funkcja jest myląca. Myślałem, że to zadziała, ale wygląda na to, że działa tak, jak pokazano powyżej, jeśli jeden zestaw zawiera identycznie pasujące wiersze z drugiego zestawu. Rozważyć tę sprawę: a2 <- data.frame(a = c(1:3, 1), b = c(letters[1:3], "c")). Zostaw a1to samo. Teraz spróbuj porównania. Nawet po przeczytaniu opcji nie jest dla mnie jasne, jak należy podać tylko wspólne elementy.
Hendy,

148

SQLDF zapewnia ładne rozwiązanie

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

Oraz wiersze, które znajdują się w obu ramkach danych:

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

Nowa wersja dplyrma funkcję anti_join, dla dokładnie tego rodzaju porównań

require(dplyr) 
anti_join(a1,a2)

I semi_joinfiltrować wiersze a1, które również znajdują się wa2

semi_join(a1,a2)

18
Dzięki za anti_joini semi_join!
drastega

Czy istnieje powód, dla którego anti_join zwróciłoby null DF, podobnie jak sqldf, ale identyczne funkcje (a1, a2) i all.equal () zaprzeczają temu?
3pitt

Chciałem tylko dodać, że anti_join i semi_join nie będą działać w niektórych przypadkach, takich jak mój. Otrzymałem komunikat „Błąd: kolumny muszą być wektorami atomowymi lub listami 1d” dla mojej ramki danych. Może mógłbym przetworzyć moje dane, aby te funkcje działały. Sqldf działał zaraz po wyjściu z bramy!
Akshay Gaur

@AkshayGaur powinien to być tylko problem z formatem danych lub czyszczeniem danych; sqldf to po prostu sql, wszystko jest wstępnie przetwarzane, aby być jak nromal DB, tak że możemy po prostu uruchomić sql na danych.
stucash

75

W dplyr :

setdiff(a1,a2)

Zasadniczo setdiff(bigFrame, smallFrame)dostajesz dodatkowe rekordy w pierwszej tabeli.

W SQLverse nazywa się to

Po lewej z wyłączeniem diagramu Join Venna

Aby uzyskać dobry opis wszystkich opcji dołączania i ustalonych tematów, jest to jedno z najlepszych podsumowań, jakie do tej pory widziałem: http://www.vertabelo.com/blog/technical-articles/sql-joins

Ale wracając do tego pytania - oto wyniki dla setdiff()kodu podczas korzystania z danych 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

Albo nawet anti_join(a1,a2)przyniesie Ci takie same wyniki.
Więcej informacji: https://www.rstudio.com/wp-content/uploads/2015/02/data-wrangling-cheatsheet.pdf


2
Ponieważ PO prosi o elementy a1, których nie ma w a2środku, nie chcesz użyć czegoś takiego jak semi_join(a1, a2, by = c('a','b'))? W odpowiedzi „Rickarda” widzę, że semi_joinzostało to zasugerowane.
steveb

Pewnie! Kolejny świetny wybór; szczególnie jeśli masz ramki danych zawierające tylko klucz łączenia i różne nazwy kolumn.
leerssej

setdiff pochodzi z lubridate :: setdiff, a nie z biblioteki (dplyr)
mtelesha

@mtelesha - Hmm, dokumentacja i kod źródłowy dplyr pokazują, że tam jest: ( dplyr.tidyverse.org/reference/setops.html , github.com/tidyverse/dplyr/blob/master/R/sets. ). Dodatkowo po załadowaniu biblioteki dplyr zgłasza ona nawet maskowanie setdiff()funkcji bazowej, która działa na dwóch wektorach: stat.ethz.ch/R-manual/R-devel/library/base/html/sets.html . Może załadowałeś bibliotekę lubridate po dplyr i sugeruje to jako źródło w liście tabcomplete?
leerssej

1
Istnieje konflikt między lubridate a dplyr, patrz github.com/tidyverse/lubridate/issues/693
slhck

39

Z pewnością nie jest to skuteczne w tym konkretnym celu, ale często w takich sytuacjach wstawiam zmienne wskaźnikowe do każdej ramki data.frame, a następnie łączę:

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

brakujące wartości w include_a1 będą wskazywać, których wierszy brakuje w a1. podobnie dla a2.

Jednym z problemów z rozwiązaniem jest to, że kolejność kolumn musi być zgodna. Innym problemem jest to, że łatwo wyobrazić sobie sytuacje, w których wiersze są kodowane tak samo, podczas gdy w rzeczywistości są różne. Zaletą korzystania z funkcji scalania jest to, że otrzymujesz bezpłatnie wszystkie funkcje sprawdzania błędów, które są niezbędne do dobrego rozwiązania.


Więc ... szukając brakującej wartości, tworzysz kolejną brakującą wartość ... Jak znajdujesz brakujące wartości w included_a1? : - /
Louis Maddox,

1
use is.na () and subset lub dplyr :: filter
Eduardo Leoni

Dziękujemy za nauczenie sposobu bez instalowania nowej biblioteki!
Rodrigo

27

Napisałem pakiet ( https://github.com/alexsanjoseph/compareDF ), ponieważ miałem ten sam problem.

  > 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

Bardziej skomplikowany przykład:

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

Pakiet zawiera również polecenie html_output do szybkiego sprawdzenia

df_compare $ html_output wprowadź opis obrazu tutaj


Twój plik compareDF jest dokładnie tym, czego potrzebuję i wykonałem dobrą robotę z małymi zestawami, jednak: 1) Nie działa z zestawem 50 milionów wierszy z 3 kolumnami (powiedzmy), że brakuje pamięci z 32 GB RAM. 2) Widzę również, że napisanie HTML zajmuje trochę czasu, czy to samo wyjście można wysłać do pliku TEXT?
Głęboki

1) Tak, 50 milionów wierszy to DUŻO danych, tylko do przechowywania w pamięci;). Zdaję sobie sprawę, że nie jest to świetne z dużymi zbiorami danych, więc być może będziesz musiał zrobić jakiś fragment. 2) możesz podać argument - limit_html = 0, aby uniknąć drukowania do HTML. Te same dane wyjściowe znajdują się w compare_output $ compare_df, które można zapisać w module CSV / TEXT przy użyciu natywnych funkcji języka R.
Alex Joseph

Dziękuję za odpowiedź @Alex Joseph, spróbuję i dam znać, jak to idzie.
Głęboki

Cześć @Alex Joseph, dziękuję za dane wejściowe, format tekstu działał, ale znalazłem problem, podniosłem go pod: stackoverflow.com/questions/54880218/…
Głęboko

Nie obsługuje różnej liczby kolumn. The two data frames have different columns!
Wystąpił

14

Możesz użyć daffpakietu (który otacza daff.jsbibliotekę za pomocą V8pakietu ):

library(daff)

diff_data(data_ref = a2,
          data = a1)

generuje następujący obiekt różnicy:

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

Format diff jest opisany w formacie różnic wyróżniających Coopy'ego dla tabel i powinien być dość oczywisty. Wiersze z +++w pierwszej kolumnie @@to te, które są nowe a1i nieobecne w a2.

Obiekt różnicy może służyć do patch_data()przechowywania różnic w celach dokumentacyjnych przy użyciu write_diff()lub do wizualizacji różnicy za pomocąrender_diff() :

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

generuje schludny wynik HTML:

wprowadź opis obrazu tutaj


10

Korzystanie z diffobjpakietu:

library(diffobj)

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

wprowadź opis obrazu tutaj

wprowadź opis obrazu tutaj


10

Dostosowałem merge funkcję, aby uzyskać tę funkcjonalność. W przypadku większych ramek danych zużywa mniej pamięci niż rozwiązanie pełnego scalania. Mogę bawić się nazwami kluczowych kolumn.

Innym rozwiązaniem jest skorzystanie z biblioteki 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

Twoje przykładowe dane nie mają żadnych duplikatów, ale Twoje rozwiązanie obsługuje je automatycznie. Oznacza to, że potencjalnie niektóre odpowiedzi nie będą pasować do wyników Twojej funkcji w przypadku duplikatów.
Oto moje rozwiązanie, w którym adres jest taki sam jak twój. Świetnie się skaluje!

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

Potrzebuje data.table 1.9.8+


2

Może jest to zbyt uproszczone, ale zastosowałem to rozwiązanie i uważam je za bardzo przydatne, gdy mam klucz podstawowy, którego mogę użyć do porównania zestawów danych. Mam nadzieję, że to pomoże.

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

Czym różni się to od tego, co już wypróbowano w OP?
Użyłeś

1

Kolejne rozwiązanie oparte na match_df w plyr. Oto plik Plyr's match_df:

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

Możemy go zmodyfikować, aby zanegować:

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

Następnie:

diff <- negate_match_df(a1,a2)

1

Używając subset:

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

Ta odpowiedź działa w scenariuszu PO. A co z bardziej ogólnym przypadkiem, gdy zmienna „a” pasuje między dwiema ramkami data.frames („a1” i „a2”), ale zmienna „b” nie?
Bryan F

1

Poniższy kod używa obu data.tablei fastmatchdla zwiększenia szybkości.

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.