【问题标题】:How to do cross join in R?如何在R中进行交叉连接?
【发布时间】:2012-05-22 22:11:54
【问题描述】:

如何在 R 中实现交叉连接?我知道“合并”可以做内连接,外连接。但我不知道如何在 R 中实现交叉连接。

谢谢

【问题讨论】:

标签: r cross-join


【解决方案1】:

我不知道使用data.frame 的内置方法,但这并不难。

@danas 展示了一种简单的内置方法,但我会在此处留下我的答案,以防它对其他目的有用。

cross.join <- function(a, b) {
    idx <- expand.grid(seq(length=nrow(a)), seq(length=nrow(b)))
    cbind(a[idx[,1],], b[idx[,2],])
}

并表明它适用于一些内置数据集:

> tmp <- cross.join(mtcars, iris)
> dim(mtcars)
[1] 32 11
> dim(iris)
[1] 150   5
> dim(tmp)
[1] 4800   16
> str(tmp)
'data.frame':   4800 obs. of  16 variables:
 $ mpg         : num  21 21 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 ...
 $ cyl         : num  6 6 4 6 8 6 8 4 4 6 ...
 $ disp        : num  160 160 108 258 360 ...
 $ hp          : num  110 110 93 110 175 105 245 62 95 123 ...
 $ drat        : num  3.9 3.9 3.85 3.08 3.15 2.76 3.21 3.69 3.92 3.92 ...
 $ wt          : num  2.62 2.88 2.32 3.21 3.44 ...
 $ qsec        : num  16.5 17 18.6 19.4 17 ...
 $ vs          : num  0 0 1 1 0 1 0 1 1 1 ...
 $ am          : num  1 1 1 0 0 0 0 0 0 0 ...
 $ gear        : num  4 4 4 3 3 3 3 4 4 4 ...
 $ carb        : num  4 4 1 1 2 1 4 2 2 4 ...
 $ Sepal.Length: num  5.1 5.1 5.1 5.1 5.1 5.1 5.1 5.1 5.1 5.1 ...
 $ Sepal.Width : num  3.5 3.5 3.5 3.5 3.5 3.5 3.5 3.5 3.5 3.5 ...
 $ Petal.Length: num  1.4 1.4 1.4 1.4 1.4 1.4 1.4 1.4 1.4 1.4 ...
 $ Petal.Width : num  0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 ...
 $ Species     : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...

【讨论】:

    【解决方案2】:

    只是all=TRUE吗?

    x<-data.frame(id1=c("a","b","c"),vals1=1:3)
    y<-data.frame(id2=c("d","e","f"),vals2=4:6)
    merge(x,y,all=TRUE)
    

    来自merge的文档:

    如果 by 或两者 by.x 和 by.y 的长度为 0(长度为零的向量或 NULL),则结果 r 是 x 和 y 的笛卡尔积,即 dim(r) = c( nrow(x)*nrow(y), ncol(x) + ncol(y))。

    【讨论】:

    • 为什么是all?看不到all=FALSE(默认)会如何影响结果。还要注意merge 设置by.x = by.y = by = intersect(names(x), names(y) 所以xy 不能共享任何列名(否则你不会得到默认设置的交叉连接)。
    • 不确定为什么这是公认的解决方案。正如 cmets 中所指出的,为多个用例提供交叉连接是行不通的。
    • 仅供参考:这仅适用于 data.frames 而不适用于 data.tables
    【解决方案3】:

    如果速度是个问题,我建议查看出色的 data.table 包。在最后的示例中,它比 merge 快了约 90 倍。

    您没有提供示例数据。如果您只想获取两个(或多个单独的)列的所有组合,可以使用CJ(交叉连接):

    library(data.table)
    CJ(x=1:2,y=letters[1:3])
    #   x y
    #1: 1 a
    #2: 1 b
    #3: 1 c
    #4: 2 a
    #5: 2 b
    #6: 2 c
    

    如果你想在两个表上进行交叉连接,我还没有找到使用 CJ() 的方法。但是你仍然可以使用data.table

    x2<-data.table(id1=letters[1:3],vals1=1:3)
    y2<-data.table(id2=letters[4:7],vals2=4:7)
    
    res<-setkey(x2[,c(k=1,.SD)],k)[y2[,c(k=1,.SD)],allow.cartesian=TRUE][,k:=NULL]
    res
    #    id1 vals1 id2 vals2
    # 1:   a     1   d     4
    # 2:   b     2   d     4
    # 3:   c     3   d     4
    # 4:   a     1   e     5
    # 5:   b     2   e     5
    # 6:   c     3   e     5
    # 7:   a     1   f     6
    # 8:   b     2   f     6
    # 9:   c     3   f     6
    #10:   a     1   g     7
    #11:   b     2   g     7
    #12:   c     3   g     7
    

    res 行的解释:

    • 基本上,您将一个虚拟列(在本例中为 k)添加到一个表并将其设置为键(setkey(tablename,keycolumns)),将虚拟列添加到另一个表,然后将它们连接起来。
    • data.table 结构在连接中使用列位置而不是名称,因此您必须将虚拟列放在开头。 c(k=1,.SD) 部分是我发现在开头添加列的一种方法(默认是将它们添加到末尾)。
    • 标准 data.table 连接的格式为X[Y]。在这种情况下,X 是setkey(x2[,c(k=1,.SD)],k),Y 是y2[,c(k=1,.SD)]
    • allow.cartesian=TRUE 告诉 data.table 忽略重复的键值,并执行笛卡尔连接(以前的版本不需要这样做)
    • 最后的[,k:=NULL] 只是从结果中删除了虚拟键。

    你也可以把它变成一个函数,这样用起来更干净:

    # Version 1; easier to write:
    CJ.table.1 <- function(X,Y)
      setkey(X[,c(k=1,.SD)],k)[Y[,c(k=1,.SD)],allow.cartesian=TRUE][,k:=NULL]
    
    CJ.table.1(x2,y2)
    #    id1 vals1 id2 vals2
    # 1:   a     1   d     4
    # 2:   b     2   d     4
    # 3:   c     3   d     4
    # 4:   a     1   e     5
    # 5:   b     2   e     5
    # 6:   c     3   e     5
    # 7:   a     1   f     6
    # 8:   b     2   f     6
    # 9:   c     3   f     6
    #10:   a     1   g     7
    #11:   b     2   g     7
    #12:   c     3   g     7
    
    # Version 2; faster but messier:
    CJ.table.2 <- function(X,Y) {
      eval(parse(text=paste0("setkey(X[,c(k=1,.SD)],k)[Y[,c(k=1,.SD)],list(",paste0(unique(c(names(X),names(Y))),collapse=","),")][,k:=NULL]")))
    }
    

    以下是一些速度基准:

    # Create a bigger (but still very small) example:
    n<-1e3
    x3<-data.table(id1=1L:n,vals1=sample(letters,n,replace=T))
    y3<-data.table(id2=1L:n,vals2=sample(LETTERS,n,replace=T))
    
    library(microbenchmark)
    microbenchmark(merge=merge.data.frame(x3,y3,all=TRUE),
                   CJ.table.1=CJ.table.1(x3,y3),
                   CJ.table.2=CJ.table.2(x3,y3),
                   times=3, unit="s")
    #Unit: seconds
    #       expr        min         lq     median         uq        max neval
    #      merge 4.03710225 4.23233688 4.42757152 5.57854711 6.72952271     3
    # CJ.table.1 0.06227603 0.06264222 0.06300842 0.06701880 0.07102917     3
    # CJ.table.2 0.04740142 0.04812997 0.04885853 0.05433146 0.05980440     3
    

    请注意,这些data.table 方法比@danas.zuokas 建议的merge 方法快得多。此示例中的两个具有 1,000 行的表会生成一个具有 100 万行的交叉连接表。因此,即使您的原始表格很小,结果也会很快变大,速度变得很重要。

    最后,data.table 的最新版本要求您添加allow.cartesian=TRUE(如在 CJ.table.1 中)或指定应返回的列的名称 (CJ.table.2)。第二种方法 (CJ.table.2) 似乎更快,但如果要自动指定所有列名,则需要一些更复杂的代码。它可能不适用于重复的列名。 (请随意推荐一个更简单的 CJ.table.2 版本)

    【讨论】:

    • 不确定这是否是由于后续的包更改,但为了使其正常工作,我不得不将函数稍微修改为 CJ.table&lt;-function(X,Y) setkey(X[,c(k=1,.SD)],k)[Y[,c(k=1,.SD)],allow.cartesian=TRUE][,k:=NULL]
    • 你是对的@StephLocke,data.table 的行为自我最初的回答以来发生了变化。我已经更新了它并添加了一些时间。谢谢。
    • 确保使用的虚拟变量名称唯一的变体:CJ.table.3 &lt;- function(X,Y){ unique_name &lt;- last(make.unique(c(colnames(X),colnames(Y),"k"))) X[,c(setNames(1,unique_name),.SD)][Y[,c(setNames(1,unique_name),.SD)],on=unique_name,allow.cartesian=TRUE][,(unique_name):=NULL] }
    • 在最近更新期间 data.table 不再允许后一个版本,而是引发错误。添加allow.cartesian 可以缓解这种情况,但data.table 建议使用by = .EACHI。请注意,这三种方法提供的速度几乎与今天完全相同(基准测试在我的机器上为 1000 次以上的所有 3 种方法提供了大约 35 毫秒的时间),因此与可读性较差的版本 CJ.table.2 相比,没有性能优势。 (添加了 allow.cartesian 参数)
    • 是否有比 CJ.table.1 更快的版本适用于数据表?我有一个大规模的问题,所以即使是很小的改进也能节省很多时间!
    【解决方案4】:

    如果你想通过 data.table 来做,这是一种方法:

    cjdt <- function(a,b){
      cj = CJ(1:nrow(a),1:nrow(b))
      cbind(a[cj[[1]],],b[cj[[2]],])
    }
    
    A = data.table(ida = 1:10)
    B = data.table(idb = 1:10)
    cjdt(A,B)
    

    如上所述,如果您正在执行许多小连接,并且不需要 data.table 对象和生成它的开销,则可以通过编写 c++ 代码块来显着提高速度Rcpp之类的:

    // [[Rcpp::export]]
    NumericMatrix crossJoin(NumericVector a, NumericVector b){
      int szA = a.size(), 
          szB = b.size();
      int i,j,r;
      NumericMatrix ret(szA*szB,2);
      for(i = 0, r = 0; i < szA; i++){
        for(j = 0; j < szB; j++, r++){
          ret(r,0) = a(i);
          ret(r,1) = b(j);
        }
      }
      return ret;
    }
    

    为了比较,首先是大连接:

    C++

    n = 1
    a = runif(10000)
    b = runif(10000)
    system.time({for(i in 1:n){
      crossJoin(a,b)
    }})
    

    用户系统已过 1.033 0.424 1.462


    数据表

    system.time({for(i in 1:n){
      CJ(a,b)
    }})
    

    用户系统已过 0.602 0.569 2.452


    现在有很多小连接:

    C++

    n = 1e5
    a = runif(10)
    b = runif(10)
    system.time({for(i in 1:n){
      crossJoin(a,b)
    }})
    

    用户系统已过 0.660 0.077 0.739


    数据表

    system.time({for(i in 1:n){
      CJ(a,b)
    }})
    

    用户系统已过 26.164 0.056 26.271

    【讨论】:

    • 加入原子向量与加入data.frames完全不同。基本上,你把它简化了很多。 CJ 或您的解决方案都没有解决相关问题。
    【解决方案5】:

    使用sqldf

    x <- data.frame(id1 = c("a", "b", "c"), vals1 = 1:3)
    y <- data.frame(id2 = c("d", "e", "f"), vals2 = 4:6) 
    
    library(sqldf)
    sqldf("SELECT * FROM x
          CROSS JOIN y")
    

    输出:

      id1 vals1 id2 vals2
    1   a     1   d     4
    2   a     1   e     5
    3   a     1   f     6
    4   b     2   d     4
    5   b     2   e     5
    6   b     2   f     6
    7   c     3   d     4
    8   c     3   e     5
    9   c     3   f     6
    

    仅作记录,使用基本包,我们可以使用by=NULL 代替all=TRUE

    merge(x, y, by= NULL)
    

    【讨论】:

      【解决方案6】:

      通过使用合并函数及其可选参数:

      内连接:merge(df1, df2) 将适用于这些示例,因为 R 会通过常用变量名称自动连接帧,但您很可能希望指定 merge(df1, df2, by = "CustomerId")确保您只匹配所需的字段。如果匹配的变量在不同的数据框中有不同的名称,也可以使用 by.x 和 by.y 参数。

      Outer join: merge(x = df1, y = df2, by = "CustomerId", all = TRUE)
      
      Left outer: merge(x = df1, y = df2, by = "CustomerId", all.x = TRUE)
      
      Right outer: merge(x = df1, y = df2, by = "CustomerId", all.y = TRUE)
      
      Cross join: merge(x = df1, y = df2, by = NULL)
      

      【讨论】:

        【解决方案7】:

        我很想知道是否存在一种方便交叉连接两个 data.table 的方法。我经常这样做,最终推出了自己的功能,其他人可能会觉得有帮助

        library(data.table)
        
        cartesian_join <- function(i, j){
          # Cartesian join of two data.tables
          # If i has M rows and j has N rows, the result will have M*N rows
          # Example: cartesian_join(as.data.table(iris), as.data.table(mtcars))
        
          # Check inputs
          if(!is.data.table(i)) stop("'i' must be a data.table")
          if(!is.data.table(j)) stop("'j' must be a data.table")
          if(nrow(i) == 0) stop("'i' has 0 rows. Not sure how to handle cartesian join")
          if(nrow(j) == 0) stop("'j' has 0 rows. Not sure how to handle cartesian join")
        
          # Do the join (use a join column name that's unlikely to clash with a pre-existing column name)
          i[, MrJoinyJoin := 1L]
          j[, MrJoinyJoin := 1L]
          result <- j[i, on = "MrJoinyJoin", allow.cartesian = TRUE]
          result[, MrJoinyJoin := NULL]
          i[, MrJoinyJoin := NULL]
          j[, MrJoinyJoin := NULL]
        
          return(result[])
        }
        
        foo <- data.frame(Foo = c(1,2,3))
        foo
          Foo
        1   1
        2   2
        3   3
        
        bar <- data.frame(Bar = c("a", "b", "c"))
        bar
          Bar
        1   a
        2   b
        3   c
        
        cartesian_join(as.data.table(foo), as.data.table(bar))
           Bar Foo
        1:   a   1
        2:   b   1
        3:   c   1
        4:   a   2
        5:   b   2
        6:   c   2
        7:   a   3
        8:   b   3
        9:   c   3
        

        【讨论】:

          【解决方案8】:

          这是几年前提出的问题,但您可以使用 tidyr::crossing() 进行交叉连接。绝对是最简单的解决方案。

          library(tidyr)
          
          league <- c("MLB", "NHL", "NFL", "NBA")
          season <- c("2018", "2017")
          
          tidyr::crossing(league, season)
          #> # A tibble: 8 x 2
          #>   league season
          #>   <chr>  <chr> 
          #> 1 MLB    2017  
          #> 2 MLB    2018  
          #> 3 NBA    2017  
          #> 4 NBA    2018  
          #> 5 NFL    2017  
          #> 6 NFL    2018  
          #> 7 NHL    2017  
          #> 8 NHL    2018
          

          reprex package (v0.2.0) 于 2018 年 12 月 8 日创建。

          【讨论】:

          • 更好。您显示的行为(以向量作为输入)与base 函数expand.grid 相同。 crossing 的优势在于它适用于 data.frame 输入(以及问题的重点)。使用已接受答案中的示例 x &lt;- data.frame(id1 = c("a", "b", "c"), vals1 = 1:3); y &lt;- data.frame(id2 = c("d", "e", "f"), vals2 = 4:6),然后 crossing(x, y) 按预期工作,而 expand.grid(x, y) 失败。
          • 这似乎比merge()快得多
          【解决方案9】:

          对于 data.table 使用

          dt1[, as.list(dt2), by = names(dt1)]
          

          请注意,这仅适用于没有重复行的情况。

          【讨论】:

            【解决方案10】:

            dplyr 解决方案:

            您可以在 dplyr 联接中执行交叉联接(即left_joininner_join 等)。

            例如根据left_join的帮助: "要执行交叉连接,生成 x 和 y 的所有组合,请使用 by = character()"

            所以你可以使用类似的东西:

            df3 <- left_join(df1, df2, by = character())
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2018-05-10
              • 2023-03-23
              • 2014-03-28
              • 1970-01-01
              • 2013-08-03
              • 1970-01-01
              • 2018-08-21
              • 2013-09-04
              相关资源
              最近更新 更多