【问题标题】:How to create a new data table based on pairwise combinations of a subset of column names?如何基于列名子集的成对组合创建新数据表?
【发布时间】:2021-12-27 12:11:48
【问题描述】:

我正在尝试定义一个函数,该函数将数据框或表作为输入,具有特定数量的 ID 列(例如,2 或 3 个 ID 列),其余列是 NAME1、NAME2、...、NAMEK (数字列)。输出应该是一个数据表,由与之前相同的 ID 列加上一个额外的 ID 列组成,该列对列名(NAME1、NAME2、...)的每个唯一成对组合进行分组。此外,我们还必须根据 ID 列将数值列的实际值收集到两个新列中;具有两个 ID 列和三个数字列的示例:

ID1 <- c("A","A","A","B","B","B")
ID2 <- c(1,2,3,1,2,3)
NAME1 <- c(10,11,9,22,25,22)
NAME2 <- c(7,9,8,20,22,21)
NAME3 <- c(10,12,11,15,19,30)
DT <- data.table(ID1,ID2,NAME1,NAME2,NAME3)

我希望以 DT 作为输入的函数的输出为

ID.new <- c("NAME1 - NAME2","NAME1 - NAME2","NAME1 - NAME2", "NAME1 - NAME2",
 "NAME1 - NAME2","NAME1 - NAME2", "NAME1 - NAME3", "NAME1 - NAME3",
 "NAME1 - NAME3","NAME1 - NAME3","NAME1 - NAME3","NAME1 - NAME3",
 "NAME2 - NAME3","NAME2 - NAME3","NAME2 - NAME3","NAME2 - NAME3",
 "NAME2 - NAME3", "NAME2 - NAME3")
ID1 <- c("A","A","A","B","B","B","A","A","A","B","B","B","A","A","A","B","B","B")
ID2 <- c(1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,1,2,3)
value.left <- c(10,11,9,22,25,22,10,11,9,22,25,22,7,9,8,20,22,21)
value.right <- c(7,9,8,20,22,21,10,12,11,15,19,30,10,12,11,15,19,30)
DT.output <- data.table(ID.new,ID1,ID2,value.left,value.right)

我发现 fun() (见下文)可以完成这项工作,但对我来说太慢了:

  fun <- function(data, ID.cols){
   data <- data.table(data)
   # Which of the columns are ID columns
   ids <-  which(colnames(data) %in% ID.cols)
   # Obtain all pairwise combinations of numeric columns into a list
   numeric.combs <- combn(x = data.table(data)[,!ids, with = FALSE], m = 2, simplify = FALSE)
   id.cols <- data[,ids, with = FALSE]
   # bind the ID columns to each pairwise combination of numeric columns inside the list
   bind.columns.each.numeric.comb <- lapply(X = numeric.combs, FUN = function(x) cbind(id.cols,x)) 
   # Create generic names for the numeric columns so that rbindlist() may be applied. In addition we make a new column that groups based on which columns we are considering
   generalize <- suppressWarnings(lapply(X = bind.columns.each.numeric.comb, FUN = function(x) 
   setattr(x = x[,ID.NEW:=paste(colnames(x[,!ids,with=FALSE]),collapse=" - ")], name = 
   'names', value = c(ID.cols,"value.left","value.right","ID.NEW"))))
   return(rbindlist(l=generalize))
}

# Performance
print(microbenchmark(fun(DT,ID.cols=c("ID1","ID2")),times=1000))

有没有更快更优雅的方法来做到这一点?

【问题讨论】:

  • 是否可以使用其他包的解决方案?
  • @TarJae,是的,只要效率更高。我还没有找到比 fun() 更有效地解决这个问题的包。

标签: r data.table


【解决方案1】:

融合的自连接选项:

library(data.table)
DTlong <- melt(DT, id.vars = c("ID1", "ID2"), variable.factor = FALSE)
out <- DTlong[DTlong, on = .(ID1, ID2), allow.cartesian = TRUE
  ][variable < i.variable,
  ][, .(ID.new = paste(variable, i.variable, sep = " - "),
        ID1, ID2, value.left = value, value.right = i.value)]
out
#            ID.new    ID1   ID2 value.left value.right
#            <char> <char> <num>      <num>       <num>
#  1: NAME1 - NAME2      A     1         10           7
#  2: NAME1 - NAME2      A     2         11           9
#  3: NAME1 - NAME2      A     3          9           8
#  4: NAME1 - NAME2      B     1         22          20
#  5: NAME1 - NAME2      B     2         25          22
#  6: NAME1 - NAME2      B     3         22          21
#  7: NAME1 - NAME3      A     1         10          10
#  8: NAME2 - NAME3      A     1          7          10
#  9: NAME1 - NAME3      A     2         11          12
# 10: NAME2 - NAME3      A     2          9          12
# 11: NAME1 - NAME3      A     3          9          11
# 12: NAME2 - NAME3      A     3          8          11
# 13: NAME1 - NAME3      B     1         22          15
# 14: NAME2 - NAME3      B     1         20          15
# 15: NAME1 - NAME3      B     2         25          19
# 16: NAME2 - NAME3      B     2         22          19
# 17: NAME1 - NAME3      B     3         22          30
# 18: NAME2 - NAME3      B     3         21          30

### validation
setorder(out, ID.new, ID1, ID2)
identical(DT.output, out)
# [1] TRUE

combn 的方法当然是合理的想法,但它唯一的低效之处在于它每次组合迭代一次。也就是说,传递给combn(..., FUN=)的函数在这种情况下被调用了18次;如果您的数据更大,它将被调用更多次。但是,对于此处的merge/join,一切都以我们可以管理的向量化方式完成:merge 高效完成,过滤作为单个逻辑向量返回,paste(..) 是也是一个大向量。

公平地说,合并概念确实有其自身的低效率:由于笛卡尔连接,它最初产生 54 行。这将导致更大数据的内存耗尽问题。如果遇到这种情况,使用fuzzyjoin 并包含variable &lt; variable(LHS 与 RHS)可能会有所帮助,这应该可以减少(如果不能完全消除)问题。

最后一条建议也可以在sqldf 中完成:

sqldf::sqldf("
  select t1.variable || ' - ' || t2.variable as [ID.new], t1.ID1, t1.ID2, 
    t1.value as [value.left], t2.value as [value.right]
  from DTlong t1
    join DTlong t2 on t1.ID1=t2.ID1 and t1.ID2=t2.ID2
      and t1.variable < t2.variable")
#           ID.new ID1 ID2 value.left value.right
# 1  NAME1 - NAME2   A   1         10           7
# 2  NAME1 - NAME3   A   1         10          10
# 3  NAME1 - NAME2   A   2         11           9
# 4  NAME1 - NAME3   A   2         11          12
# 5  NAME1 - NAME2   A   3          9           8
# 6  NAME1 - NAME3   A   3          9          11
# 7  NAME1 - NAME2   B   1         22          20
# 8  NAME1 - NAME3   B   1         22          15
# 9  NAME1 - NAME2   B   2         25          22
# 10 NAME1 - NAME3   B   2         25          19
# 11 NAME1 - NAME2   B   3         22          21
# 12 NAME1 - NAME3   B   3         22          30
# 13 NAME2 - NAME3   A   1          7          10
# 14 NAME2 - NAME3   A   2          9          12
# 15 NAME2 - NAME3   A   3          8          11
# 16 NAME2 - NAME3   B   1         20          15
# 17 NAME2 - NAME3   B   2         22          19
# 18 NAME2 - NAME3   B   3         21          30

基准测试:

bench::mark(
  pernkf  = fun(DT, c("ID1", "ID2")),
  tjebo   = fun2(DT, c("ID1", "ID2")),
  r2evans = fun3(DT, c("ID1", "ID2")),  # native data.table
  r2evans2 = fun4(),                    # sqldf
  check = FALSE)
# # A tibble: 4 x 13
#   expression      min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result memory        time     gc        
#   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list> <list>        <list>   <list>    
# 1 pernkf       5.38ms   6.06ms     161.      287KB    13.2     61     5      379ms <NULL> <Rprofmem[,3~ <bch:tm~ <tibble [~
# 2 tjebo        5.08ms   5.63ms     172.      230KB     8.83    78     4      453ms <NULL> <Rprofmem[,3~ <bch:tm~ <tibble [~
# 3 r2evans      2.97ms   3.48ms     280.      170KB    11.0    127     5      454ms <NULL> <Rprofmem[,3~ <bch:tm~ <tibble [~
# 4 r2evans2    17.19ms  18.91ms      52.0     145KB    13.0     20     5      384ms <NULL> <Rprofmem[,3~ <bch:tm~ <tibble [~

sqldf 在此示例中确实会影响性能,我欢迎改进查询 :-)

【讨论】:

  • 为什么函数在combn中会被调用18次?只有三种组合?
  • 如果速度是唯一重要的事情......如果你在数据框(不是:数据表)上使用我的函数,它比你的更快。以社区 wiki 的形式查看基准测试结果。
  • 我同意速度并不总是最重要的前提;在那种语气中,我认为可读性和简单性也应该是有影响的。我很想知道我们所有的解决方案如何适用于“真实数据”(如果问题中的数据不完美地表示)。谢谢!
  • @r2evans,这是一个很好的解决方案,但据我所知,这些解决方案都不适合任意命名的 ID 列(我应该更清楚地说明这一点)。这个想法是应该接受 ID 列的任何名称,但在这里,ID1 和 ID2 由名称 (?) 使用,例如 . on = .(ID1, ID2) :)
【解决方案2】:

如果您可以使用数据框,下面将为您提供当前速度和内存效率最高的方法(请参阅基准 wiki)。

我认为使用combn() 的方法对我来说似乎是合理的。而且我真的不认为它会迭代组合 18 次,as has been purported。而且,我个人觉得这个比数据表融化版更容易阅读,但这可能是因为我不习惯data.table语法。

注意:在数据表上使用它显然效率不高。如果你真的需要 data.table,r2evans 解决方案会更好。

fun2 <- function(data, ID.cols){
  ids <-  which(colnames(data) %in% ID.cols)
  ## you can loop over the combinations directly
  new_dat <- combn(data[-ids], 2, function(x) {
    new_x <- setNames(x, paste("value", c("left", "right"), sep = "."))
    ## use paste with collapse for the ID.new
    new_x$ID.new <- paste(names(x), collapse = " - ")
    new_x
  }, simplify = FALSE)

## bind it with the old ID columns, outside the loop (bit faster)
  cbind(do.call(rbind, new_dat), data[ids])
}

fun2(DT,ID.cols = c("ID1", "ID2"))
#>    value.left value.right        ID.new ID1 ID2
#> 1          10           7 NAME1 - NAME2   A   1
#> 2          11           9 NAME1 - NAME2   A   2
#> 3           9           8 NAME1 - NAME2   A   3
#> 4          22          20 NAME1 - NAME2   B   1
#> 5          25          22 NAME1 - NAME2   B   2
#> 6          22          21 NAME1 - NAME2   B   3
#> 7          10          10 NAME1 - NAME3   A   1
#> 8          11          12 NAME1 - NAME3   A   2
#> 9           9          11 NAME1 - NAME3   A   3
#> 10         22          15 NAME1 - NAME3   B   1
#> 11         25          19 NAME1 - NAME3   B   2
#> 12         22          30 NAME1 - NAME3   B   3
#> 13          7          10 NAME2 - NAME3   A   1
#> 14          9          12 NAME2 - NAME3   A   2
#> 15          8          11 NAME2 - NAME3   A   3
#> 16         20          15 NAME2 - NAME3   B   1
#> 17         22          19 NAME2 - NAME3   B   2
#> 18         21          30 NAME2 - NAME3   B   3

有关基准,请参阅the community wiki

【讨论】:

  • 这很棒。这似乎是一种非常通用的方法,可以扩展到具有任意数量的名称列和 ID 列的“真实数据”。不过,我想知道,为什么这里的数据框方法比数据表快?
  • @Pernkf 很高兴你喜欢它。我不知道为什么会更快!我对 data.table 不是很了解,也不太了解底层结构。可能结构与数据框列表结构不同,需要在内部进行转换,这可能是一个时间限制步骤?
【解决方案3】:

基准测试,reprex。如果你真的不需要数据表,base R 似乎可以解决问题。

注意这是比较 r2evans 和 pernkf 在数据表上的函数与 tjebo 和 tarjae 在数据帧上的函数。

目前不包括 PeaceWang 建议的方法,因为它们要么无法扩展到 k 列,要么提供不正确的结果。

bench::mark(
  pernkf  = fun(DT, c("ID1", "ID2")),
  tjebo   = fun2(DF, c("ID1", "ID2")),
  r2evans = fun3(DT, c("ID1", "ID2")), 
  tarjae = fun4(DF, c("ID1", "ID2")),
  check = FALSE)

#> # A tibble: 4 × 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 pernkf       2.95ms    3.2ms     302.     2.29MB     6.33
#> 2 tjebo      359.33µs 373.85µs    2423.    18.65KB    10.5 
#> 3 r2evans      1.65ms   1.79ms     535.   756.16KB     6.30
#> 4 tarjae      26.49ms  27.74ms      34.3    4.75MB     7.35

m <- microbenchmark::microbenchmark(
  pernkf = fun(DT, ID.cols = c("ID1", "ID2")),
  r2evans = fun3(DT, ID.cols = c("ID1", "ID2")),
  tjebo = fun2(DF, ID.cols = c("ID1", "ID2")), 
  tarjae = fun4(DF, c("ID1", "ID2")),
  times = 1000
)
m
#> Unit: microseconds
#>     expr       min         lq       mean    median        uq       max neval
#>   pernkf  2885.714  3055.1450  3439.1257  3150.457  3298.404  95391.80  1000
#>  r2evans  1629.028  1739.5715  1949.8389  1829.696  1922.227  10843.33  1000
#>    tjebo   354.714   410.0975   469.1457   427.948   443.237   4344.00  1000
#>   tarjae 25854.416 26564.8420 29103.6948 27142.758 30982.328 118592.10  1000

ggplot2::autoplot(m)
#> Coordinate system already present. Adding new coordinate system, which will replace the existing one.

数据和函数

library(tidyverse)
library(data.table)

ID1 <- c("A","A","A","B","B","B")
ID2 <- c(1,2,3,1,2,3)
NAME1 <- c(10,11,9,22,25,22)
NAME2 <- c(7,9,8,20,22,21)
NAME3 <- c(10,12,11,15,19,30)
DF <- data.frame(ID1,ID2,NAME1,NAME2,NAME3)
DT <- data.table(DF)

fun <- function(data, ID.cols){
  data <- data.table(data)
  ids <-  which(colnames(data) %in% ID.cols)
  numeric.combs <- combn(x = data.table(data)[,!ids, with = FALSE], m = 2, simplify = FALSE)
  id.cols <- data[,ids, with = FALSE]
  bind.columns.each.numeric.comb <- lapply(X = numeric.combs, FUN = function(x) cbind(id.cols,x)) 
  generalize <- suppressWarnings(lapply(X = bind.columns.each.numeric.comb, FUN = function(x) 
    setattr(x = x[,ID.NEW:=paste(colnames(x[,!ids,with=FALSE]),collapse=" - ")], name = 
              'names', value = c(ID.cols,"value.left","value.right","ID.NEW"))))
  return(rbindlist(l=generalize))
}

fun2 <- function(data, ID.cols){
  ids <-  which(colnames(data) %in% ID.cols)
  new_dat <- combn(data[-ids], 2, function(x) {
    new_x <- setNames(x, paste("value", c("left", "right"), sep = "."))
    new_x$ID.new <- paste(names(x), collapse = " - ")
    new_x
  }, simplify = FALSE)
  cbind(do.call(rbind, new_dat), data[ids])
}

fun3 <- function(data, ID.cols) {
  DTlong <- melt(data, id.vars = ID.cols, variable.factor = FALSE)
  out <- DTlong[DTlong, on = .(ID1, ID2), allow.cartesian = TRUE
  ][variable < i.variable,
  ][, .(ID.new = paste(variable, i.variable, sep = " - "),
        ID1, ID2, value.left = value, value.right = i.value)]
  out
}
fun4 <- function(x, id.cols){
DT1 <- DT %>% 
  pivot_longer(
    -id.cols
  ) %>% 
  mutate(name1 = lead(name, default=last(name)),
         value1 = lead(value, default=last(value)))%>% 
  arrange(name, name1) %>% 
  group_by(name) %>% 
  mutate(n = n()) %>% 
  mutate(name_nr = parse_number(name)) %>% 
  ungroup()


DT1 %>% 
  mutate(name1 = lead(name, unique(n)*(max(name_nr)-min(name_nr)))) %>% 
  mutate(value1 = lead(value, unique(n)*(max(name_nr)-min(name_nr)))) %>% 
  slice(seq_len(first(n))) %>% 
  bind_rows(DT1 %>% 
              slice(1:(n() - unique(n))), .
  ) %>% 
  mutate(ID.new = paste(name, name1, sep = " - "), .before=1) %>% 
  select(ID.new, ID1, ID2, value.left=value, value.right = value1) %>% 
  arrange(ID.new)
}

检查解决方案是否相同:

## convert all to data frame
## column names and order need to be the same
## rows need to be sorted in the same way (caveat row names!)
preparetocompare <- function(x){
x <- data.frame(x)
names(x) <- tolower(names(x))
x <- x[c("id1", "id2", "value.left", "value.right", "id.new")]
x <- x[with(x, order(id.new, id1, id2)),]
rownames(x) <- NULL
}
compare_df <- function(...){
 # credit to https://stackoverflow.com/a/17244041/7941188
 ls_df <-  c(as.list(environment()), list(...))
 ls_compare <- lapply(ls_df, preparetocompare)
 # inspired by https://stackoverflow.com/a/18814864/7941188
 all.identical <- function(l) mapply(all.equal, head(l, 1), tail(l, -1))
 all.identical(ls_compare)
}

compare_df(fun(DT, c("ID1", "ID2")), 
           fun2(DF, c("ID1", "ID2")), 
           fun3(DT, c("ID1", "ID2")),
           fun4(DF, c("ID1", "ID2"))
           )
#> [1] TRUE TRUE TRUE

【讨论】:

    【解决方案4】:

    这篇文章由三部分组成:

    1. 原始答案(具有两个 ID 列的非 equi 自连接)
    2. 第一次编辑(具有可变 ID 列数的非 equi 自连接)
    3. 第二次编辑(6 种不同问题大小的不同方法的基准)

    原答案:具有两个 ID 列的非 equi 自连接

    为了完整起见,这里有一个解决方案,它使用了熔融数据的非等自连接(重新整形为长格式):

    library(data.table)
    mdt <- melt(DT, id.vars = c("ID1", "ID2"))
    res <- mdt[mdt, on = .(ID1, ID2, variable < variable), nomatch = NULL,
        .(ID.new = paste(x.variable, i.variable, sep = " - "), 
          ID1, ID2, value.left = x.value, value.right = i.value)]
    
    all.equal(res, DT.output, ignore.row.order = TRUE)
    
    [1] TRUE
    

    这种方法类似于r2evans' answer,但避免了笛卡尔连接。我已经避免将基准测试结果显示为对 6 行 5 列的样本数据集进行基准测试的相关性有限。

    编辑 1:具有可变 ID 列数的非 equi 自连接

    OP 已要求 ID 列的数量可以变化(事实上,ID 列的名称作为参数传递给 OP 自己的函数)。

    non-equi 自连接可以被增强以处理任意数量的 ID 列:

    library(data.table)
    id_cols <- c("ID1", "ID2")
    mdt <- melt(DT, id.vars = id_cols)
    res <- mdt[mdt, on = c(id_cols, "variable < variable"), nomatch = NULL,
               c(.(ID.new = paste(x.variable, i.variable, sep = " - "), 
                   value.left = x.value, value.right = i.value), .SD), 
               .SDcols = id_cols]
    
    all.equal(res, DT.output, ignore.col.order = TRUE, ignore.row.order = TRUE)
    
    [1] TRUE
    

    注意,在这里使用.SD 是安全的,因为.SDcols 只选择那些已经用于加入的列(由id_cols 指定)。

    编辑 2:变化问题规模的基准

    到目前为止,r2evanstjebo 提供的基准测试仅使用具有 2 个 id 列、3 个数字列和 6 行的原始数据集。由于问题规模较小,这些基准比较开销,但不能代表较大问题规模的性能。

    有 3 个不同的参数来描述问题的大小:

    1. 样本数据集DT的行数nr
    2. 数字列 nc 的数量,从中创建成对行,以及
    3. id 列数ni

    最终结果由nc * (nc - 1) / 2 * nr 行和ni + 3 列组成。

    通过使用bench 包中的press() 函数,我们可以轻松执行一组具有不同问题规模的基准测试。

    基准测试中包含 6 种不同的方法:

    所有方法都实现为使用 2 个参数调用的函数,分别是数据集 DTDF,以及具有任意 id 列名称的字符向量。

    pernkf <- function(data, ID.cols){
      data <- data.table(data)
      # Which of the columns are ID columns
      ids <-  which(colnames(data) %in% ID.cols)
      # Obtain all pairwise combinations of numeric columns into a list
      numeric.combs <- combn(x = data.table(data)[,!ids, with = FALSE], m = 2, simplify = FALSE)
      id.cols <- data[,ids, with = FALSE]
      # bind the ID columns to each pairwise combination of numeric columns inside the list
      bind.columns.each.numeric.comb <- lapply(X = numeric.combs, FUN = function(x) cbind(id.cols,x)) 
      # Create generic names for the numeric columns so that rbindlist() may be applied. In addition we make a new column that groups based on which columns we are considering
      generalize <- suppressWarnings(lapply(X = bind.columns.each.numeric.comb, FUN = function(x) 
        setattr(x = x[,ID.new:=paste(colnames(x[,!ids,with=FALSE]),collapse=" - ")], name = 
                  'names', value = c(ID.cols,"value.left","value.right","ID.new"))))
      return(rbindlist(l=generalize))
    }
    
    r2evans = \(DT, id_cols) {
      DTlong <- melt(DT, id.vars = id_cols, variable.factor = FALSE)
      DTlong[DTlong, on = c(id_cols), allow.cartesian = TRUE
      ][variable < i.variable,
      ][, .(ID.new = paste(variable, i.variable, sep = " - "), setnames(.SD, id_cols), 
            value.left = value, value.right = i.value), .SDcols = id_cols
      ]
    }
    
    tjebo <- \(data, ID.cols) {
      ids <-  which(colnames(data) %in% ID.cols)
      ## you can loop over the combinations directly
      new_dat <- combn(data[-ids], 2, function(x) {
        new_x <- setNames(x, paste("value", c("left", "right"), sep = "."))
        ## use paste with collapse for the ID.new
        new_x$ID.new <- paste(names(x), collapse = " - ")
        new_x
      }, simplify = FALSE)
      ## bind it with the old ID columns, outside the loop (bit faster)
      cbind(do.call(rbind, new_dat), data[ids])
    }
    
    nej <- \(DT, id_cols) {
      mdt <- melt(DT, id.vars = id_cols)
      mdt[mdt, on = c(id_cols, "variable < variable"), nomatch = NULL,
          .(setnames(.SD, id_cols), ID.new = paste(x.variable, i.variable, sep = " - "), 
            value.left = x.value, value.right = i.value), 
          .SDcols = id_cols]
    }
    
    dtc <- \(DT, id_cols) {
      combn(setdiff(colnames(DT), id_cols), 2, 
            \(x) DT[, ..x][, ID.new := paste(x, collapse = " - ")], 
            simplify = FALSE) |>
        rbindlist(use.names = FALSE) |>
        setnames(1:2, c("value.left", "value.right")) |>
        cbind(DT[, ..id_cols])
    }
    
    mvl <- \(DT, id_cols) {
      num_cols <- setdiff(colnames(DT), id_cols)
      combos <- combn(num_cols, 2L, simplify = TRUE)
      id_new_levels <- apply(combos, 2, paste, collapse = " - ") 
      melt(DT, measure.vars = list(combos[1L, ],combos[2L, ]), 
           value.name = c("value.left", "value.right"), variable.name = "ID.new")[
             , ID.new := as.character(`levels<-`(ID.new, id_new_levels))]
    }
    

    Peace WangTarJae 的两种方法已被省略,因为我无法将它们转换为可扩展的函数。

    在对press() 的调用中,nr 的行数从 10 变化到 100'000,数字列数 nc 从 3 变化到 10。相应地,生成的数据集的行数会有所不同从 30 到 450 万行。所有运行都使用 3 个 id 列来验证 ni 是否可扩展(不限于 2 个)。

    检查功能设置为忽略不同的行和/或列顺序,因为这些可能因不同的方法而异。

    library(bench)
    bm <- press(
      nr = c(10L, 1000L, 100000L),
      nc = c(3L, 5L, 10L),
      {
        ni <- 3L
        DT <- data.table()
        id_cols <- sprintf("ID%01i", seq(ni))
        # append id cols
        for (id in id_cols) set(DT, , id, seq(nr))
        # append data cols
        for (j in seq(nc)) {
          col_name <- sprintf("NAME%04i", j)
          set(DT, , col_name, seq(nr) + (j / 1000))
        }
        DF <- as.data.frame(DT)
        mark(
          pernkf(DT, id_cols),
          r2evans(DT, id_cols),
          tjebo(DF, id_cols),
          nej(DT, id_cols),
          dtc(DT, id_cols),
          mvl(DT, id_cols),
          check = \(x,y) all.equal(x, setDT(y), ignore.row.order = TRUE, ignore.col.order = TRUE),
          min_iterations = 3L
        )
      }
    )
    

    基准时间由

    可视化
    ggplot2::autoplot(bm)
    

    (注意对数时间刻度)。

    几乎总是,mvl() 是最快的方法。仅对于具有 3 个数字列和最多 1000 行的最小问题大小,tjebo() 稍微快一些。对于 100'000 行的大问题,dtc()pernkf() 分别是第二和第三。

    下一张图表显示了性能如何随数字列数 nc 变化:

    library(ggplot2)
    ggplot(bm) +
      aes(nc, median, colour = attr(expression, "description")) +
      geom_point() + 
      geom_line() +
      scale_x_log10() +
      labs(colour = "expression") +
      facet_wrap(~nr, scales = "free_y") +
      ggtitle("Median run time")
    

    (注意 log-log 尺度和 facets 的独立时间尺度)

    tjebo() 的运行时间使用nc 比任何其他方法增加得更快。对于某些用例,mvl() 比任何其他方法都要快一个数量级。

    一个经常被忽视的方面是内存消耗。下图显示了内存分配如何随问题大小而变化:

    ggplot(bm) +
      aes(nc, mem_alloc, colour = attr(expression, "description")) +
      geom_point() + 
      geom_line() +
      scale_x_log10() +
      labs(colour = "expression") +
      facet_wrap(~nr, scales = "free_y") +
      ggtitle("Memory allocation")
    

    (注意对数刻度和 y 轴上的独立刻度)

    每个用例的最佳和最差方法之间的内存分配差异非常大,大约是 7 到 8 倍。同样,tjebo()nc 的内存分配增长最快。对于大型问题,mvl() 分配的内存比任何其他方法都要少,然后是dtc()pernkf()

    【讨论】:

      【解决方案5】:

      UPDATE II(删除了错误的解决方案)

      现在经过真正的努力和社区的大力支持(感谢 @akrun@tjebo),我认为我有正确且可扩展的 tidyverse 解决方案: (万岁):-)

      library(tidyverse)
      
      DT1 <- DT %>% 
        pivot_longer(
          -c(ID1, ID2)
        ) %>% 
        mutate(name1 = lead(name, default=last(name)),
               value1 = lead(value, default=last(value)))%>% 
        arrange(name, name1) %>% 
        group_by(name) %>% 
        mutate(n = n()) %>% 
        mutate(name_nr = parse_number(name)) %>% 
        ungroup()
      
      DT1 %>% 
        mutate(name1 = lead(name, unique(n)*(max(name_nr)-min(name_nr)))) %>% 
        mutate(value1 = lead(value, unique(n)*(max(name_nr)-min(name_nr)))) %>% 
        slice(seq_len(first(n))) %>% 
        bind_rows(DT1 %>% 
                    slice(1:(n() - unique(n))), .
                  ) %>% 
        mutate(ID.new = paste(name, name1, sep = " - "), .before=1) %>% 
        select(ID.new, ID1, ID2, value.left=value, value.right = value1) %>% 
        arrange(ID.new)
      
      ID.new        ID1     ID2 value.left value.right
         <chr>         <chr> <dbl>      <dbl>       <dbl>
       1 NAME1 - NAME2 A         1         10           7
       2 NAME1 - NAME2 A         2         11           9
       3 NAME1 - NAME2 A         3          9           8
       4 NAME1 - NAME2 B         1         22          20
       5 NAME1 - NAME2 B         2         25          22
       6 NAME1 - NAME2 B         3         22          21
       7 NAME1 - NAME3 A         1         10          10
       8 NAME1 - NAME3 A         2         11          12
       9 NAME1 - NAME3 A         3          9          11
      10 NAME1 - NAME3 B         1         22          15
      11 NAME1 - NAME3 B         2         25          19
      12 NAME1 - NAME3 B         3         22          30
      13 NAME2 - NAME3 A         1          7          10
      14 NAME2 - NAME3 A         2          9          12
      15 NAME2 - NAME3 A         3          8          11
      16 NAME2 - NAME3 B         1         20          15
      17 NAME2 - NAME3 B         2         22          19
      18 NAME2 - NAME3 B         3         21          30
      

      【讨论】:

      • 正确。谢谢杰博。会修复的。
      • @tjebo 请看我的更新,现在有正确的版本!
      • 这个在建了一整天还没完工
      • 请看我的更新。非常感谢您的支持。
      • 干得好,为努力+1 - 唉,基准测试看起来不太有希望......请参阅基准维基
      【解决方案6】:

      注意

      这是一个鼓舞人心的想法,它不能完全满足 OP 的要求(例如,ID.new 和编号顺序),但我认为值得在这里重新记录。

      你可以先把DT转成长格式melt。 然后使用步骤-nrow(DT)shift 值以执行 减号,即NAME1 - NAME2, NAME2 - NAME3, NAME3 - NAME1

      ds = melt(DT,
                measure.vars = patterns("^NAME"),
                variable.name = c("ID.new"),
                value.name = c("value.left"))
      group_len = nrow(DT)
      ds[, ID.new := paste(ID.new,shift(ID.new, n = -group_len, type = c("cyclic")),sep = " - ")]
      ds[, value.right := shift(value.left, n = -group_len, type = c("cyclic"))]
      

      输出:

            ID1   ID2        ID.new value.left value.right
          <char> <num>        <char>      <num>       <num>
       1:      A     1 NAME1 - NAME2         10           7
       2:      A     2 NAME1 - NAME2         11           9
       3:      A     3 NAME1 - NAME2          9           8
       4:      B     1 NAME1 - NAME2         22          20
       5:      B     2 NAME1 - NAME2         25          22
       6:      B     3 NAME1 - NAME2         22          21
       7:      A     1 NAME2 - NAME3          7          10
       8:      A     2 NAME2 - NAME3          9          12
       9:      A     3 NAME2 - NAME3          8          11
      10:      B     1 NAME2 - NAME3         20          15
      11:      B     2 NAME2 - NAME3         22          19
      12:      B     3 NAME2 - NAME3         21          30
      13:      A     1 NAME3 - NAME1         10          10
      14:      A     2 NAME3 - NAME1         12          11
      15:      A     3 NAME3 - NAME1         11           9
      16:      B     1 NAME3 - NAME1         15          22
      17:      B     2 NAME3 - NAME1         19          25
      18:      B     3 NAME3 - NAME1         30          22
      

      【讨论】:

      • 我认为 OP 想要一种可扩展到 k 列的方法
      • @tjebo 当然,这是可扩展的
      • 恐怕我看不出它在当前形式下是如何可扩展的。
      • (另外,如果您想更新答案,请考虑不生成差异列,而是生成 OP 给出的输出,作为字符;)
      • @tjebo 当然,我知道你的意思(列 ID.new)。这还不是主要的难点,在寒冷的夜里行走,没有OP的回应,我有点懒得编辑。
      【解决方案7】:

      我认为由于数据结构良好,有人可能会使用以下代码(这是可扩展的,但为简单起见,我提供了一个简单的变体)

      melt(DT, measure.vars=list(c(3,3,4), c(4,5,5)))
      

      【讨论】:

      • 正如目前所写,您的答案尚不清楚。请edit 添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。你可以找到更多关于如何写好答案的信息in the help center
      • 这是人类生活的未来,被Bot 恼火:D
      • @هنروقتان,这确实是一个绝妙的主意!但是,您的答案太简洁而无法投票。请edit 回答并提供更多详细信息。
      • @هنروقتان,我已经改进了您的方法,将其包含在我的benchmark 中。对于所调查的用例,您的方法几乎总是最快且内存分配更少的方法。
      猜你喜欢
      • 2020-09-07
      • 2023-04-02
      • 2021-08-21
      • 2023-03-24
      • 1970-01-01
      • 2021-04-25
      • 1970-01-01
      • 2017-07-28
      • 1970-01-01
      相关资源
      最近更新 更多