从一个数据框中选择未出现在另一个数据框中的行。

203

我有两个数据框:

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

我想找到a1有而a2没有的行。

是否有内置函数可以执行此类操作?

(附言:我已经为此编写了解决方案,我只是好奇是否有人已经编写了更精细的代码)

以下是我的解决方案:

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)
14个回答

185

sqldf 提供了一个不错的解决方案

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

两个数据框中都存在的行:

a1Ina2 <- sqldf('SELECT * FROM a1 INTERSECT SELECT * FROM a2')
新版本的dplyr有一个函数anti_join,用于进行这种比较。
require(dplyr) 
anti_join(a1,a2)

使用semi_join筛选在a2中也出现在a1中的行。

semi_join(a1,a2)

27
谢谢使用anti_joinsemi_join! - drastega
为什么anti_join和sqldf会返回一个空的DF,但是identical(a1,a2)和all.equal()函数却与之相矛盾? - 3pitt
只是想在这里补充一下,anti_join和semi_join在某些情况下并不适用,就像我的情况一样。我在我的数据框中得到了“错误:列必须是1d原子向量或列表”的结果。也许我可以处理我的数据,使这些函数起作用。 Sqldf一开始就可以工作! - Akshay Gaur
@AkshayGaur,这只是一个数据格式或数据清理问题;sqldf仅仅是sql,一切都预处理成普通的DB,使我们能够直接在数据上运行sql。 - stucash

108

dplyr 中:

setdiff(a1,a2)

基本上,setdiff(bigFrame, smallFrame) 可以让你得到第一个表中的额外记录。

在SQLverse中,这被称为左排除连接(Left Excluding Join):

Left Excluding Join Venn Diagram

关于所有联接选项和集合主题的良好描述,这是我见过的最好的总结之一:http://www.vertabelo.com/blog/technical-articles/sql-joins

但回到这个问题——使用OP的数据,这里是setdiff()代码的结果:

> 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

甚至 anti_join(a1, a2) 也会得到相同的结果。
更多信息请参考:https://www.rstudio.com/wp-content/uploads/2015/02/data-wrangling-cheatsheet.pdf


2
由于OP要求在a2中不存在的a1项,难道你不想使用类似于semi_join(a1, a2, by = c('a','b'))这样的东西吗?在"Rickard"的答案中,我看到了建议使用semi_join - steveb
当然可以!这也是一个很好的选择,特别是当您有仅具有连接键和不同列名称的数据框时。 - leerssej
setdiff 函数来自于 lubridate 包而不是 dplyr 包。 - mtelesha
1
@mtelesha - 嗯,dplyr 的文档和源代码显示它在那里:(https://dplyr.tidyverse.org/reference/setops.html, https://github.com/tidyverse/dplyr/blob/master/R/sets.)。此外,当加载 dplyr 库时,它甚至报告掩盖了基本的 setdiff() 函数,该函数可用于两个向量:https://stat.ethz.ch/R-manual/R-devel/library/base/html/sets.html。也许您在加载 dplyr 之后加载了 lubridate 库,并且它在 tabcomplete 列表中建议它作为来源? - leerssej
1
lubridate 和 dplyr 之间存在冲突,详见 https://github.com/tidyverse/lubridate/issues/693 - slhck
还要考虑使用 dplyr::intersection - bmc

96

虽然这并没有直接回答你的问题,但它会给你共同的元素。使用 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

函数compare提供了很大的灵活性,允许进行各种比较(例如改变每个向量元素的顺序、改变变量的顺序和名称、缩短变量、改变字符串大小写)。由此,你应该能够弄清楚一个向量与另一个向量之间的差异所在。例如(这不是非常优雅的写法):

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
我觉得这个函数很令人困惑。我以为它会对我有用,但它似乎只在一个集合包含完全匹配的行时才像上面展示的那样工作。考虑这种情况:a2 <- data.frame(a = c(1:3, 1), b = c(letters[1:3], "c"))。保持 a1 不变,现在尝试进行比较。即使阅读选项,我也不清楚列出只有共同元素的正确方式是什么。 - Hendy

45

这种方法对于这个特定的目的显然不是很有效率,但我通常在这种情况下会在每个数据框中插入指示变量,然后进行合并:

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

included_a1 中的缺失值将说明 a1 中哪些行数据是缺失的。a2 同理。

你的解决方案存在一个问题,即列的顺序必须相匹配。另一个问题是很容易想象到情况,即当实际不同时,行被编码为相同。使用合并的优点是,你可以免费获得所有必要的错误检查,以便得到良好的解决方案。


那么...在寻找缺失值时,您会创建另一个缺失值...您如何找到included_a1中的缺失值? :-/ - Louis Maddox
1
使用 is.na() 和 subset,或者 dplyr::filter。 - Eduardo Leoni
3
谢谢您教授一种不需要安装新库的方法! - Rodrigo

31

我写了一个包(https://github.com/alexsanjoseph/compareDF),因为我遇到了同样的问题。

  > 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

一个更复杂的例子:

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

该包还具有一个html_output命令,用于快速检查

df_compare$html_output 这里输入图片描述


你的compareDF正是我所需要的,并且在小数据集上表现出色。然而:1)它无法处理一个有3列、5000万行的数据集(例如),即使有32GB的内存也会提示内存不足。2)我还发现HTML输出需要一些时间,能否将相同的输出发送到文本文件中? - Deep
  1. 是的,5000万行数据确实非常多,很难全部存储在内存中。我知道这对于大型数据集来说并不理想,所以你可能需要进行某种形式的分块处理。
  2. 你可以使用参数“-limit_html = 0”来避免输出为HTML格式。相同的输出结果可以在“compare_output$comparison_df”中找到,你可以使用本地R函数将其写入CSV/TEXT文件。
- Alex Joseph
谢谢你的回复,@Alex Joseph。我会尝试一下并告诉你结果。 - Deep
嗨,@Alex Joseph,感谢您的输入。文本格式确实有效,但我发现了一个问题,在以下链接中提出了这个问题:https://stackoverflow.com/questions/54880218/compare-df-in-r-truncating-trailing-zero - Deep
1
不用担心@Lowpar - 函数的参数是df_newdf_old。如果有一个+标记,意味着df_new数据框中有它,而-标记则表示df_old数据框中有它被更改了。在上面的例子中,Datsun 710在新数据框中的值为181 HP,在旧数据框中的值为93 HP。 - Alex Joseph
显示剩余8条评论

22

你可以使用daff(它使用daff.js,并使用V8进行封装):

library(daff)

diff_data(data_ref = a2,
          data = a1)

生成以下差异对象:

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

表格差异格式的描述可以在这里找到,应该相当易于理解。 第一列中带有+++的行@@a1中新添加且不存在于a2中的行。

差异对象可用于patch_data(),使用write_diff()存储差异以供文档目的,或使用render_diff()可视化差异。

render_diff(
    diff_data(data_ref = a2,
              data = a1)
)
生成整洁的HTML输出:

enter image description here


11

使用 diffobj 包:

library(diffobj)

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

在此输入图片描述

在此输入图片描述


10

我改编了merge函数以实现此功能。在较大的数据框中,它比完整合并解决方案使用更少的内存。我可以随意更改关键列的名称。

另一种解决方案是使用库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)
}

8
您的示例数据没有任何重复项,但您的解决方案可以自动处理它们。这意味着在出现重复情况时,您的函数的某些答案可能与结果不匹配。 以下是我的解决方案,它以与您相同的方式处理重复项。同时,它还具有优秀的可扩展性!
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

需要使用 data.table 1.9.8+


3
也许这个解决方案过于简单,但是当我有一个可以用来比较数据集的主键时,我使用了这个解决方案,并发现它非常有用。希望能对你有所帮助。
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,]

这与 OP 已经尝试过的有何不同?你使用了与 Tal 相同的代码,只比较了单个列而不是整行(这是要求)。 - David Arenburg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接