在R中,从数据框的所有行生成成对数据框。

3

我有一个名为df的数据框,有4列,包含800万个观测值:

name <- c("Pablo", "Christina", "Steve", "Diego", "Ali", "Brit", "Ruth", "Mia", "David", "Dylan")
year <- seq(2000, 2009, 1)
v1 <- sample(1:10, 10, replace=T)
v2 <- sample(1:10, 10, replace=T)
df <- data.frame(year, v1)

> df
        name year v1 v2
1      Pablo 2000  2  9
2  Christina 2001  5  3
3      Steve 2002  8  9
4      Diego 2003  7  6
5        Ali 2004  2  4
6       Brit 2005  1  1
7       Ruth 2006 10  9
8        Mia 2007  6  7
9      David 2008 10  9
10     Dylan 2009  3  2

我需要生成一个名为outputdata.frame,其中包含df中所有行的两两组合,如下所示:
 >output
   name year v1 v2    name_2 year_2 v1_2 v2_2
1 Pablo 2000  2  9 Christina   2001    5    3
2 Pablo 2000  2  9     Steve   2002    8    9
3 Pablo 2000  2  9     Diego   2003    7    6
etc.  

什么是最快的方法来做到这一点?

1
喜欢使用 idx <- t(combn(seq_len(nrow(df)), 2));cbind(df[idx[,1],], df[idx[,2],]) 吗? - lukeA
3
使用tidyr包中的crossing函数,对df数据框进行自身的交叉操作。 - alistaire
@alistaire 对!但第一行包含重复项(即Pablo-Pablo)。是否可能在没有重复项的情况下生成此输出? - wake_wake
1
使用 cbind.data.frame 或将其包装在 as.data.frame 中。之后,您可以使用 names(df)<-c("col1", "col2", .....) 重命名列。 - lukeA
@lukeA 谢谢!这在样本上的效果很好。但是对于真实数据的较大子集,会出现有关大小问题的错误:“Error in matrix(r, nrow = len.r, ncol = count) : invalid 'ncol' value (too large or NA) In addition: Warning message: In combn(seq_len(nrow(data1)), 2) : NAs introduced by coercion to integer range”。 - wake_wake
显示剩余2条评论
5个回答

6

tidyr::crossing函数返回所有观测值的组合,但需要使用setNames或类似函数设置变量名。如果不想包含自身匹配,则可以在任何唯一的ID列上调用dplyr::filter函数来删除它们。

library(tidyverse)

df_crossed <- df %>% 
    setNames(paste0(names(.), '_2')) %>% 
    crossing(df) %>% 
    filter(name != name_2)

head(df_crossed)
##   name_2 year_2 v1_2 v2_2      name year v1 v2
## 1  Pablo   2000    5    5 Christina 2001  7  3
## 2  Pablo   2000    5    5     Steve 2002  1  9
## 3  Pablo   2000    5    5     Diego 2003  2  8
## 4  Pablo   2000    5    5       Ali 2004  9  5
## 5  Pablo   2000    5    5      Brit 2005  8  5
## 6  Pablo   2000    5    5      Ruth 2006  8  1

另一种解决命名问题的方法是在crossing之后使用janitor::clean_names,尽管这需要额外安装一个包。

1
谢谢Alistaire!这在我的数据的较小子集上运行得很好。但是一旦子集变得更大,R就会尝试分配一个大小为1338 GB的向量...不确定该如何处理 :-) - wake_wake
1
是的,行数所需的阶乘也在其中。它还给出了帕布罗-克里斯蒂娜和克里斯蒂娜-帕布罗,即排列而非组合。使用“combn”进行索引将允许更有限的集合(如果这是您所需要的),但它仍然会变得非常快。您可能不需要复制数据;您可以将一行中的函数应用于其他行,并仅存储结果。也许需要一个新问题。 - alistaire

3
希望这能给帖子所有者想要的结果。
name <- c("Pablo", "Christina", "Steve", "Diego", "Ali", "Brit", "Ruth", "Mia", "David", "Dylan")
year <- seq(2000, 2009, 1)
v1 <- sample(1:10, 10, replace=T)
v2 <- sample(1:10, 10, replace=T)
df <- data.frame(name, year, v1, v2, stringsAsFactors=FALSE)
print(df)
rows = nrow(df)
n <- rows * (rows - 1) / 2
ndf <- data.frame(
    name1=character(n),year1=numeric(n), v1_1=numeric(n),v2_1=numeric(n),
    name2=character(n),year2=numeric(n), v1_2=numeric(n),v2_2=numeric(n),
    stringsAsFactors=FALSE
)
k <- 1
for (i in 1:(rows-1))
{
    for (j in (i+1):rows)
    {
        ndf[k,] <- c(df[i,], df[j,])
        k <- k + 1
    }
}
print(ndf)

#        name year v1 v2
#1      Pablo 2000  4  9
#2  Christina 2001  2  1
#3      Steve 2002  2  9
#4      Diego 2003  5  5
#5        Ali 2004 10  4
#6       Brit 2005  5  2
#7       Ruth 2006  7 10
#8        Mia 2007  6  7
#9      David 2008  4 10
#10     Dylan 2009  7  3

#       name1 year1 v1_1 v2_1     name2 year2 v1_2 v2_2
#1      Pablo  2000    4    9 Christina  2001    2    1
#2      Pablo  2000    4    9     Steve  2002    2    9
#3      Pablo  2000    4    9     Diego  2003    5    5
#4      Pablo  2000    4    9       Ali  2004   10    4
#5      Pablo  2000    4    9      Brit  2005    5    2
#6      Pablo  2000    4    9      Ruth  2006    7   10
#7      Pablo  2000    4    9       Mia  2007    6    7
#8      Pablo  2000    4    9     David  2008    4   10
#9      Pablo  2000    4    9     Dylan  2009    7    3
#10 Christina  2001    2    1     Steve  2002    2    9
#...

该方法不包括互相重复的数据,即Christeina 2001 2 1 Pablo 2000 4 9不被包括在内。如果要包括互相重复的数据,则两个for循环都需要从1到rows进行,而在内部循环中需要跳过i == j的情况。当然,ndf的大小也需要重新计算。 - Shiping

2

不要加入噪音,但考虑在同一数据框上使用merge进行基本的R交叉连接,并过滤掉反向重复项。请注意,在过滤之前进行交叉连接将返回800万x 800万条记录的数据集,因此希望您的RAM足以支持此操作。

df <- data.frame(name = c("Pablo", "Christina", "Steve", "Diego", "Ali",
                          "Brit", "Ruth", "Mia", "David", "Dylan"), 
                 year = seq(2000, 2009, 1),
                 v1 =sample(1:10, 10, replace=T), 
                 v2 =sample(1:10, 10, replace=T),
                 stringsAsFactors = FALSE)

# MERGE ON KEY, THEN REMOVE KEY COL
df$key <- 1
dfm <- merge(df, df, by="key")[,-1]   

# FILTER OUT SAME NAME AND REVERSE DUPS, THEN RENAME COLUMNS
dfm <- setNames(dfm[(dfm$name.x < dfm$name.y),], 
                c("name_p1", "year_p1", "V1_p1", "V2_p1",
                  "name_p2", "year_p2", "V1_p2", "V2_p2"))

# ALL PABLO PAIRINGS 
dfm[dfm$name_p1=='Pablo' | dfm$name_p2=='Pablo',]

#      name_p1 year_p1 V1_p1 V2_p1 name_p2 year_p2 V1_p2 V2_p2
# 3      Pablo    2000     7     8   Steve    2002     3     1
# 7      Pablo    2000     7     8    Ruth    2006     8     4
# 11 Christina    2001    10    10   Pablo    2000     7     8
# 31     Diego    2003     4     9   Pablo    2000     7     8
# 41       Ali    2004     5     3   Pablo    2000     7     8
# 51      Brit    2005     2     4   Pablo    2000     7     8
# 71       Mia    2007     7     7   Pablo    2000     7     8
# 81     David    2008     1     7   Pablo    2000     7     8
# 91     Dylan    2009     9     2   Pablo    2000     7     8

如果这个大数据集是从符合SQL标准的数据库中获取的,我可以提供对应的SQL查询语句,这样过滤器就可以在连接过程中运行而不是之后单独运行,这可能更有效。

1
您可以使用 data.table 将名称列与自身进行交叉连接,并删除重复情况。这将导致一个较小的结构,用于合并数据,而不是进行完整的合并,然后进行过滤。您可以通过两次合并添加其余数据:一次合并与第一个名称列相关联的数据,再一次合并与第二个列相关联的数据。
name <- c("Pablo", "Christina", "Steve", "Diego", "Ali", "Brit", "Ruth", "Mia", "David", "Dylan")
year <- seq(2000, 2009, 1)
v1 <- sample(1:10, 10, replace=T)
v2 <- sample(1:10, 10, replace=T)
# stringsAsFactors = FALSE in order for pmin to work properly
df <- data.frame(name, year, v1, v2, stringsAsFactors = FALSE) 

library(data.table)
setDT(df)
setkey(df)

# cross-join name column to itself while removing duplicates and redundancies
name_cj <- setnames(
  CJ(df[, name], df[, name])[V1 < V2], # taking a hint from Parfait's clever solution
  c("name1", "name2"))

# perform 2 merges, once for the 1st name column and
# again for the 2nd name colum
name_cj <- merge(
  merge(name_cj, df, by.x = "name1", by.y = "name"),
  df,
  by.x = "name2", by.y = "name", suffixes = c("_1", "_2"))

# reorder columns as desired with setorder()
head(name_cj)
#      name2     name1 year_1 v1_1 v2_1 year_2 v1_2 v2_2
#1:      Brit       Ali   2004    3    8   2005    4    5
#2: Christina       Ali   2004    3    8   2001    9    8
#3: Christina      Brit   2005    4    5   2001    9    8
#4:     David       Ali   2004    3    8   2008    5    2
#5:     David      Brit   2005    4    5   2008    5    2
#6:     David Christina   2001    9    8   2008    5    2

1

这个扩展@alistaires的解决方案展示了一个交叉矩阵用作索引。按照所述的问题想要完整的交叉输出,这将非常庞大(对于800万个项目来说有6400万行),因此实际上没有绕过内存要求的方法。但是,如果这个在现实世界中的应用是处理子集,那么这里展示的索引技术可能是减少内存使用的一种方式。可能只交叉整数在交叉操作期间使用更少的内存。

library(dplyr)
library(tidyr)
crossed <- as.matrix(crossing(1:nrow(df), 1:nrow(df)))
# bind and name in one step (may be inefficient) so that filter can be applied in one step
output <- as.data.frame(cbind(df[crossed[, 1],], 
                              data.frame(name_2 = df[crossed[, 2], 1],
                                         year_2 = df[crossed[, 2], 2],
                                         v1_2   = df[crossed[, 2], 3],
                                         v2_2   = df[crossed[, 2], 4]) )) %>%
           filter(!(name == name_2 & year == year_2))

# estimated sized for 8 million rows gine this 10 row sample
format(object.size(output) / (10 / 8e6), units="MB")
#[1] "5304 Mb"

1
as.data.frame(cbind(...))是一个常常导致类型问题的不良习惯。只需使用data.frame即可。 - alistaire

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