【问题标题】:Using two grouping designations to create one 'combined' grouping variable使用两个分组名称创建一个“组合”分组变量
【发布时间】:2016-08-08 03:11:29
【问题描述】:

给定一个data.frame:

df <- data.frame(grp1 = c(1,1,1,2,2,2,3,3,3,4,4,4),
                 grp2 = c(1,2,3,3,4,5,6,7,8,6,9,10))

#> df
#   grp1 grp2
#1     1    1
#2     1    2
#3     1    3
#4     2    3
#5     2    4
#6     2    5
#7     3    6
#8     3    7
#9     3    8
#10    4    6
#11    4    9
#12    4   10

两个列都是分组变量,因此已知列 grp1 中的所有 1 都被分组在一起,以此类推,所有 2 都被分组,等等。grp2 也是如此。已知所有 1 相同,所有 2 相同。

因此,如果我们查看第 3 行和第 4 行,根据第 1 列,我们知道前 3 行可以组合在一起,而后 3 行可以组合在一起。那么由于第 3 行和第 4 行共享相同的grp2 值,我们知道实际上所有 6 行都可以组合在一起。

基于相同的逻辑,我们可以看到最后六行也可以组合在一起(因为第 7 行和第 10 行共享相同的grp2)。

除了编写一组相当复杂的for() 循环之外,还有更直接的方法吗?我还没有想到一个。

我希望获得的最终输出类似于:

# > df
#    grp1 grp2 combinedGrp
# 1     1    1           1
# 2     1    2           1
# 3     1    3           1
# 4     2    3           1
# 5     2    4           1
# 6     2    5           1
# 7     3    6           2
# 8     3    7           2
# 9     3    8           2
# 10    4    6           2
# 11    4    9           2
# 12    4   10           2

感谢您对此主题的任何指导!

【问题讨论】:

    标签: r algorithm


    【解决方案1】:

    希望这个解决方案对您有所帮助:

    假设:df是根据grp1排序的。

    ## split dataset using values of grp1
    split_df <- split.default(df$grp2,df$grp1)
    
    parent <- vector('integer',length(split_df))
    
    ## find out which combinations have values of grp2 in common
    for (i in seq(1,length(split_df)-1)){
        for (j in seq(i+1,length(split_df))){
            inter <- intersect(split_df[[i]],split_df[[j]])
    
            if (length(inter) > 0){
                parent[j] <- i
            }
        }
    }
    
    ans <- vector('list',length(split_df))
    
    index <- which(parent == 0)
    
    ## index contains indices of elements that have no element common
    for (i in seq_along(index)){
        ans[[index[i]]] <- rep(i,length(split_df[[i]]))
    }
    
    rest_index <- seq(1,length(split_df))[-index]
    
    for (i in rest_index){
        val <- ans[[parent[i]]][1]
        ans[[i]] <- rep(val,length(split_df[[i]]))
    }
    
    df$combinedGrp <- unlist(ans)
    
    df
    
       grp1 grp2 combinedGrp
    1     1    1           1
    2     1    2           1
    3     1    3           1
    4     2    3           1
    5     2    4           1
    6     2    5           1
    7     3    6           2
    8     3    7           2
    9     3    8           2
    10    4    6           2
    11    4    9           2
    12    4   10           2
    

    【讨论】:

    • 仅供参考,您可以使用seq_len(length(split_df)-1) 作为迭代器,而不是seq(1,length(split_df)-1)。这样一来,如果 split_df 的长度恰好为 1(因为它的计算结果为 c(1,0),很烦人),你最终不会得到 seq(1, 0)
    • @Frank 感谢您的启发。虽然在这种情况下,如果 split_df 的长度变为 1,则不需要任何计算,并且 if 语句可以检查那个东西,非常感谢。以后遇到这种情况我会先考虑seq_len选项。
    • 奇怪的是,当用我的 1000 行真实数据集进行测试时,这种方法创建了一个 unlist(ans) ,它低 1005 个项目。你能想到会发生这种情况的任何原因吗?
    【解决方案2】:

    我会根据连接的组件定义一个图和标签节点:

    gmap = unique(stack(df))
    gmap$node = seq_len(nrow(gmap))
    
    oldcols = unique(gmap$ind)
    newcols = paste0("node_", oldcols)
    df[ newcols ] = lapply(oldcols, function(i)  with(gmap[gmap$ind == i, ], 
      node[ match(df[[i]], values) ]
    ))
    
    library(igraph)
    g = graph_from_edgelist(cbind(df$node_grp1, df$node_grp2), directed = FALSE)
    gmap$group = components(g)$membership
    
    df$group = gmap$group[ match(df$node_grp1, gmap$node) ]
    
    
       grp1 grp2 node_grp1 node_grp2 group
    1     1    1         1         5     1
    2     1    2         1         6     1
    3     1    3         1         7     1
    4     2    3         2         7     1
    5     2    4         2         8     1
    6     2    5         2         9     1
    7     3    6         3        10     2
    8     3    7         3        11     2
    9     3    8         3        12     2
    10    4    6         4        10     2
    11    4    9         4        13     2
    12    4   10         4        14     2
    

    grp1grp2 的每个唯一元素都是一个节点,df 的每一行都是一条边。

    【讨论】:

    • 我将此标记为正确,因为它适用于提供的数据。我的问题(此响应和使用 igraph 提交的所有其他响应)是它们不适用于我的真实数据集。对于 igraph 的答案,我的 newcols 最终大部分都是 NA 导致 graph_from_edgelist() 失败。你能想出这是什么原因吗?所有分组都是非 NA。有许多独特的条目我预计不能被分组。这会是个问题吗?
    【解决方案3】:

    一种方法是通过一个矩阵来定义基于组成员身份的行之间的链接。

    这种方法与 @Frank 的图形答案有关,但使用邻接矩阵而不是使用边来定义图形。这种方法的一个优点是它可以立即处理具有相同代码的许多 > 2 个分组列。 (只要您编写灵活确定链接的函数。)缺点是您需要在行之间进行所有成对比较来构造矩阵,因此对于非常长的向量可能会很慢。照原样,@Frank 的答案对于非常长的数据或只有两列的情况会更好。

    步骤是

    1. 根据组比较行并将这些行定义为链接(即创建图表)
    2. 确定由 1 中的链接定义的图的连通分量。

    你可以做 2 几种方法。下面我展示了一种蛮力方法,您 2a) 折叠链接,直到使用矩阵乘法达到稳定的链接结构,以及 2b) 使用 hclustcutree 将链接结构转换为因子。您还可以在从矩阵创建的图形上使用igraph::clusters

    1. 构造行之间的邻接矩阵(成对链接的矩阵) (即,如果它们在同一组中,则矩阵条目为 1,否则为 0)。首先制作一个判断两行是否链接的辅助函数

    linked_rows <- function(data){
      ## helper function
      ## returns a _function_ to compare two rows of data
      ##  based on group membership.
    
      ## Use Vectorize so it works even on vectors of indices
      Vectorize(function(i, j) {
        ## numeric: 1= i and j have overlapping group membership
        common <- vapply(names(data), function(name)
                         data[i, name] == data[j, name],
                         FUN.VALUE=FALSE)
        as.numeric(any(common))
      })
    }
    

    我在outer中使用它来构造一个矩阵,

    rows <- 1:nrow(df)
    A <- outer(rows, rows, linked_rows(df)) 
    

    2a. 将 2 度链接折叠为 1 度链接。也就是说,如果行通过中间节点链接但不直接链接,则通过定义它们之间的链接将它们集中在同一组中。

    一次迭代涉及:i) 矩阵相乘得到 A 的平方,以及 ii) 将方阵中的任何非零项设置为 1(就好像它是一阶成对链接)

    ## define as a function to use below
    lump_links <- function(A) {
      A <- A %*% A
      A[A > 0] <- 1
      A
    }
    

    重复此操作直到链接稳定

    oldA <- 0
    i <- 0
    while (any(oldA != A)) {
      oldA <- A
      A <- lump_links(A)
    }
    

    2b. 使用A 中的稳定链接结构来定义组(图的连接组件)。您可以通过多种方式做到这一点。

    一种方法是先定义一个距离对象,然后使用hclustcutree。如果您考虑一下,我们希望将链接 (A[i,j] == 1) 定义为距离 0。因此步骤是 a) 将链接定义为 dist 对象中的距离 0,b) em> 从 dist 对象构造一棵树,c) 在零高度(即零距离)处切割树:

    df$combinedGrp <- cutree(hclust(as.dist(1 - A)), h = 0)
    df
    

    在实践中,您可以将步骤 1 - 2 编码在使用帮助器 lump_linkslinked_rows 的单个函数中:

    lump <- function(df) {
      rows <- 1:nrow(df)
      A <- outer(rows, rows, linked_rows(df))
    
      oldA <- 0
      while (any(oldA != A)) {
        oldA <- A
        A <- lump_links(A)
      }
      df$combinedGrp <- cutree(hclust(as.dist(1 - A)), h = 0)
      df
    }
    

    这适用于原始 df 以及 @rawr 答案中的结构

    df <- data.frame(grp1 = c(1,1,1,2,2,2,3,3,3,4,4,4,5,5,6,7,8,9),
                     grp2 = c(1,2,3,3,4,5,6,7,8,6,9,10,11,3,12,3,6,12))
    lump(df)
    
       grp1 grp2 combinedGrp
    1     1    1           1
    2     1    2           1
    3     1    3           1
    4     2    3           1
    5     2    4           1
    6     2    5           1
    7     3    6           2
    8     3    7           2
    9     3    8           2
    10    4    6           2
    11    4    9           2
    12    4   10           2
    13    5   11           1
    14    5    3           1
    15    6   12           3
    16    7    3           1
    17    8    6           2
    18    9   12           3
    

    PS

    这是一个使用igraph的版本,这使得与@Frank的答案的联系更加清晰:

      lump2 <- function(df) {
          rows <- 1:nrow(df)
          A <- outer(rows, rows, linked_rows(df))
          cluster_A <- igraph::clusters(igraph::graph.adjacency(A))
          df$combinedGrp <- cluster_A$membership
          df
        }
    

    【讨论】:

    • 是的,实际上有一种算法可以从邻接矩阵中找到连接的组件:math.stackexchange.com/q/1106870 我用扩展示例测试了我的代码,它似乎扩展到 3+ 个组就好了,通过方式。
    • 谢谢。我也进行了编辑以澄清,但我的意思是> 2列。当然,您的方法在那里也可以正常工作,但需要对创建边缘列表的代码进行一些编辑
    • 到目前为止,这是唯一适用于我的现实生活数据集的解决方案。问题是它需要的时间太长了。测试表明,100 行需要 5 秒,1k 行需要 3 分钟,2k 行需要 20 分钟。真实数据集目前是 56k 行,每次运行闪亮的应用程序时都会更新,所以我希望更快一些。看起来我只需要使用两种分组方法中的一种。不过还是谢谢
    • 嗯,我不知道@AndrewTaylor。如果您对适用于您的真实数据的 igraph 解决方案感兴趣,您可能需要使用新的示例数据发布一个新问题,以显示这些方法失败的地方。 (哦,打算在我的回答下回复。)
    【解决方案4】:

    基于https://stackoverflow.com/a/35773701/2152245,我使用了igraph 的不同实现,因为我已经拥有来自st_intersects()sf 多边形邻接矩阵:

    library(igraph)
    library(sf)
    # Use example data
    nc <- st_read(system.file("shape/nc.shp", package="sf"))
    nc <- nc[-sample(1:nrow(nc),nrow(nc)*.75),] #drop some polygons
    # Find intersetions
    b <- st_intersects(nc, sparse = F)
    g  <- graph.adjacency(b)
    clu <- components(g)
    gr <- groups(clu)
    # Quick loop to assign the groups
    for(i in 1:nrow(nc)){
        for(j in 1:length(gr)){
          if(i %in% gr[[j]]){
            nc[i,'group'] <- j
          }
        }
      }
    # Make a new sfc object
    nc_un <- group_by(nc, group) %>% 
        summarize(BIR74 = mean(BIR74), do_union = TRUE)
    plot(nc_un['BIR74'])
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-12-13
      • 2017-05-10
      • 2018-03-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-17
      相关资源
      最近更新 更多