【问题标题】:Apply a function to every specified column in a data.table and update by reference将函数应用于 data.table 中的每个指定列并通过引用更新
【发布时间】:2013-05-26 15:09:06
【问题描述】:

我有一个 data.table,我想用它对某些列执行相同的操作。这些列的名称在字符向量中给出。在这个特定示例中,我想将所有这些列乘以 -1。

一些玩具数据和一个指定相关列的向量:

library(data.table)
dt <- data.table(a = 1:3, b = 1:3, d = 1:3)
cols <- c("a", "b")

现在我正在这样做,循环遍历字符向量:

for (col in 1:length(cols)) {
   dt[ , eval(parse(text = paste0(cols[col], ":=-1*", cols[col])))]
}

有没有办法不使用 for 循环直接执行此操作?

【问题讨论】:

    标签: r data.table


    【解决方案1】:

    这似乎有效:

    dt[ , (cols) := lapply(.SD, "*", -1), .SDcols = cols]
    

    结果是

        a  b d
    1: -1 -1 1
    2: -2 -2 2
    3: -3 -3 3
    

    这里有一些技巧:

    • 因为(cols) := 中有括号,所以结果被分配给cols 中指定的列,而不是一些名为“cols”的新变量。
    • .SDcols 告诉调用我们只查看这些列,并允许我们使用 .SD,即与这些列关联的 Data 的 Subset。
    • lapply(.SD, ...).SD 上运行,这是一个列列表(如所有 data.frames 和 data.tables)。 lapply 返回一个列表,所以最后j 看起来像cols := list(...)

    编辑:正如@Arun 提到的,这是另一种可能更快的方法:

    for (j in cols) set(dt, j = j, value = -dt[[j]])
    

    【讨论】:

    • 另一种方法是使用setfor-loop。我怀疑它会更快。
    • @Arun 我已经进行了编辑。这是你的意思吗?我之前没用过set
    • +1 很好的答案。是的,对于这样的情况,我也更喜欢带有setfor 循环。
    • 是的,使用set() 似乎更快,对于我的数据集来说快了~4 倍!太棒了。
    • 谢谢,@JamesHirschorn。我不确定,但我怀疑以这种方式对列进行子集化而不是使用 .SD 会产生更多开销,无论如何,这是标准的成语,出现在 intro vignette github.com/Rdatatable/data.table/wiki/Getting-started 中,我认为成语的部分原因是避免输入两次表名。
    【解决方案2】:

    当您还想更改列的名称时,我想添加一个答案。如果您想计算多列的对数,这会非常方便,这在经验工作中很常见。

    cols <- c("a", "b")
    out_cols = paste("log", cols, sep = ".")
    dt[, c(out_cols) := lapply(.SD, function(x){log(x = x, base = exp(1))}), .SDcols = cols]
    

    【讨论】:

    • 有没有办法根据规则更改名称?例如,在 dplyr 中,您可以执行 iris %>% mutate_at(vars(matches("Sepal")), list(times_two = ~.*2)) ,它会将“_times_two”附加到新名称。
    • 我认为这不可能,但不太确定。
    • 这将添加名称为out_cols 的列,同时仍保留cols。因此,您需要通过以下任一方式来消除它们: 1) 仅要求 log.a 和 log.b:将 [,.(outcols)] 链接到末尾并通过 &lt;- 重新存储到 dt。 2) 删除带有链接[,c(cols):=NULL] 的旧列。非链式解决方案 3) 是dt[,c(cols):=...] 后跟setnames(dt, cols, newcols)
    • @mpag,没错,但对于我的实证研究用例,我大部分时间都需要数据集中的两个系列。
    【解决方案3】:

    更新:以下是一种无需 for 循环的简洁方法

    dt[,(cols):= - dt[,..cols]]
    

    这是一种简单易读的代码方式。但就性能而言,根据以下微基准测试结果,它仍然落后于 Frank 的解决方案

    mbm = microbenchmark(
      base = for (col in 1:length(cols)) {
        dt[ , eval(parse(text = paste0(cols[col], ":=-1*", cols[col])))]
      },
      franks_solution1 = dt[ , (cols) := lapply(.SD, "*", -1), .SDcols = cols],
      franks_solution2 =  for (j in cols) set(dt, j = j, value = -dt[[j]]),
      hannes_solution = dt[, c(out_cols) := lapply(.SD, function(x){log(x = x, base = exp(1))}), .SDcols = cols],
      orhans_solution = for (j in cols) dt[,(j):= -1 * dt[,  ..j]],
      orhans_solution2 = dt[,(cols):= - dt[,..cols]],
      times=1000
    )
    mbm
    
    Unit: microseconds
    expr                  min        lq      mean    median       uq       max neval
    base_solution    3874.048 4184.4070 5205.8782 4452.5090 5127.586 69641.789  1000  
    franks_solution1  313.846  349.1285  448.4770  379.8970  447.384  5654.149  1000    
    franks_solution2 1500.306 1667.6910 2041.6134 1774.3580 1961.229  9723.070  1000    
    hannes_solution   326.154  405.5385  561.8263  495.1795  576.000 12432.400  1000
    orhans_solution  3747.690 4008.8175 5029.8333 4299.4840 4933.739 35025.202  1000  
    orhans_solution2  752.000  831.5900 1061.6974  897.6405 1026.872  9913.018  1000
    

    如下图所示

    我之前的回答: 以下也有效

    for (j in cols)
      dt[,(j):= -1 * dt[,  ..j]]
    

    【讨论】:

    • 这与弗兰克一年半前的回答基本相同。
    • 谢谢,弗兰克的回答是使用 set。当我使用具有数百万行的大型 data.table 时,我看到 := 运算符优于函数
    • 我添加一个旧问题的答案的原因如下:我也有类似的问题,我在谷歌搜索时遇到了这篇文章。之后我找到了解决我的问题的方法,我认为它也适用于这里。实际上,我的建议使用了新版本的库中提供的 data.table 的新功能,该功能在提出问题时不存在。我认为分享是一个好主意,我认为其他有类似问题的人最终会在这里使用谷歌搜索。
    • 您是否使用包含 3 行的 dt 进行基准测试?
    • Hannes 的回答是进行不同的计算,因此不应该与其他人进行比较,对吧?
    【解决方案4】:

    上述解决方案似乎都不适用于按组计算。以下是我得到的最好的:

    for(col in cols)
    {
        DT[, (col) := scale(.SD[[col]], center = TRUE, scale = TRUE), g]
    }
    

    【讨论】:

      【解决方案5】:

      添加示例以基于列的字符串向量创建新列。基于 Jfly 的回答:

      dt <- data.table(a = rnorm(1:100), b = rnorm(1:100), c = rnorm(1:100), g = c(rep(1:10, 10)))
      
      col0 <- c("a", "b", "c")
      col1 <- paste0("max.", col0)  
      
      for(i in seq_along(col0)) {
        dt[, (col1[i]) := max(get(col0[i])), g]
      }
      
      dt[,.N, c("g", col1)]
      

      【讨论】:

        【解决方案6】:
        library(data.table)
        (dt <- data.table(a = 1:3, b = 1:3, d = 1:3))
        
        Hence:
        
           a b d
        1: 1 1 1
        2: 2 2 2
        3: 3 3 3
        
        Whereas (dt*(-1)) yields:
        
            a  b  d
        1: -1 -1 -1
        2: -2 -2 -2
        3: -3 -3 -3
        

        【讨论】:

        • 仅供参考,标题中的“每个指定的列”表示提问者有兴趣将其应用于列的子集(可能不是所有列)。
        • @Frank 当然!在这种情况下,OP 可以执行 dt[,c("a","b")]*(-1)。
        • 好吧,让我们完整地说dt[, cols] &lt;- dt[, cols] * (-1)
        • 似乎所需的新语法是 dt[, cols]
        【解决方案7】:

        dplyr 函数在 data.tables 上工作,所以这里有一个 dplyr 解决方案,它也“避免了 for 循环”:)

        dt %&gt;% mutate(across(all_of(cols), ~ -1 * .))

        我使用 orhan 的代码(添加行和列)对其进行了基准测试,您会看到 dplyr::mutateacross 的执行速度通常比大多数其他解决方案快,并且比使用 lapply 的 data.table 解决方案慢。

        library(data.table); library(dplyr)
        dt <- data.table(a = 1:100000, b = 1:100000, d = 1:100000) %>% 
          mutate(a2 = a, a3 = a, a4 = a, a5 = a, a6 = a)
        cols <- c("a", "b", "a2", "a3", "a4", "a5", "a6")
        
        dt %>% mutate(across(all_of(cols), ~ -1 * .))
        #>               a       b      d      a2      a3      a4      a5      a6
        #>      1:      -1      -1      1      -1      -1      -1      -1      -1
        #>      2:      -2      -2      2      -2      -2      -2      -2      -2
        #>      3:      -3      -3      3      -3      -3      -3      -3      -3
        #>      4:      -4      -4      4      -4      -4      -4      -4      -4
        #>      5:      -5      -5      5      -5      -5      -5      -5      -5
        #>     ---                                                               
        #>  99996:  -99996  -99996  99996  -99996  -99996  -99996  -99996  -99996
        #>  99997:  -99997  -99997  99997  -99997  -99997  -99997  -99997  -99997
        #>  99998:  -99998  -99998  99998  -99998  -99998  -99998  -99998  -99998
        #>  99999:  -99999  -99999  99999  -99999  -99999  -99999  -99999  -99999
        #> 100000: -100000 -100000 100000 -100000 -100000 -100000 -100000 -100000
        
        library(microbenchmark)
        mbm = microbenchmark(
          base_with_forloop = for (col in 1:length(cols)) {
            dt[ , eval(parse(text = paste0(cols[col], ":=-1*", cols[col])))]
          },
          franks_soln1_w_lapply = dt[ , (cols) := lapply(.SD, "*", -1), .SDcols = cols],
          franks_soln2_w_forloop =  for (j in cols) set(dt, j = j, value = -dt[[j]]),
          orhans_soln_w_forloop = for (j in cols) dt[,(j):= -1 * dt[,  ..j]],
          orhans_soln2 = dt[,(cols):= - dt[,..cols]],
          dplyr_soln = (dt %>% mutate(across(all_of(cols), ~ -1 * .))),
          times=1000
        )
        
        library(ggplot2)
        ggplot(mbm) +
          geom_violin(aes(x = expr, y = time)) +
          coord_flip()
        

        reprex package (v0.3.0) 于 2020 年 10 月 16 日创建

        【讨论】:

          猜你喜欢
          • 2015-07-12
          • 2013-05-22
          • 1970-01-01
          • 2015-12-15
          • 2015-06-19
          • 2013-03-18
          • 2020-08-11
          • 1970-01-01
          相关资源
          最近更新 更多