【问题标题】:data.table: Perform efficient row-wise operation on large data.table with columns as inputdata.table:以列为输入对大型 data.table 执行高效的逐行操作
【发布时间】:2020-07-18 20:32:33
【问题描述】:

我有一个非常大的 data.table,有 1.6x10^8 行,我想在 exposureexposure.before.index 列之间执行逐行操作,如下面的示例所示。

我创建了 TI 列(即治疗强化),它指示一个非 ID 当前是否在一种或多种药物上,exposure,这与它们在每个 ID 各自第一行的任何药物不同,exposure.before.index。您可以查看我的代码并观察最终输出是否如说明的那样。

library(data.table)
DT <- data.table::data.table(ID=c("a","a","a","b","b","c","c"),
                             drugA=c(1,1,1,0,0,0,0),
                             drugB=c(0,1,1,1,0,0,0),
                             drugC=c(0,0,1,0,1,0,0))
DT[, exposure := gsub("NA\\+|\\+NA", "", do.call(paste, 
                                                 c(Map(function(x, y) names(.SD)[(NA^!x) * y], .SD, 
                                                       seq_along(.SD)), sep="+"))), .SDcols = drugA:drugC]
DT[exposure=="NA",exposure:="NONE"]
DT[,exposure.before.index:=c("drugA","drugA","drugA","drugB","drugB","NONE","NONE")]
DT[,CNT:=1:.N]
DT[!(exposure.before.index!="NONE" & exposure=="NONE"),TI:=(any(!unlist(strsplit(exposure, "[+]"))%in%unlist(strsplit(exposure.before.index, "[+]")))),by="CNT"]
DT[is.na(TI),TI:=FALSE]
DT

   ID drugA drugB drugC          exposure exposure.before.index CNT    TI
1:  a     1     0     0             drugA                 drugA   1 FALSE
2:  a     1     1     0       drugA+drugB                 drugA   2  TRUE
3:  a     1     1     1 drugA+drugB+drugC                 drugA   3  TRUE
4:  b     0     1     0             drugB                 drugB   4 FALSE
5:  b     0     0     1             drugC                 drugB   5  TRUE
6:  c     0     0     0              NONE                  NONE   6 FALSE
7:  c     0     0     0              NONE                  NONE   7 FALSE

我创建 CNT 是为了在 exposureexposure.before.index 之间应用我的函数 any(!unlist(strsplit(exposure, "[+]"))%in%unlist(strsplit(exposure.before.index, "[+]")))。由于我使用此方法的 1.6x10^8 行需要相当长的时间。当我想逐行应用某个操作/函数时,我通常会使用这种 data.table[...,by="CNT"] 技术,但我发现这对于非常大的 data.table 来说并不可靠。你们中的一些人还有其他方法比我的方法更强大吗?

我发现了与我的主题类似的其他问题,但答案并未概括为以稳健的方式对用户定义的函数应用逐行操作。

感谢任何帮助和/或建议。

【问题讨论】:

  • 嗨 theneil,你能解释一下整数列的含义吗?是否所有药物都以整数列编码?在第 2 行 exposure.before.index = "drugA",但在您的文字中,您声明“与他们之前使用的任何药物不同,exposure.before.index”。你能澄清一下吗?一般来说,字符串操作很慢,所以如果你能将你的问题简化为整数或逻辑比较,你会好很多。
  • @IanCampbell 你发现了一个错误。所以exposure.before.index 应该包含患者在第一行使用的药物。在尝试撰写此专栏时,我随机选择了药物类型,并专注于所需的输出,而不是遵循我原来的工作 data.table 中的逻辑。我现在正在编辑;希望它更有意义
  • 您能否确认drugAdrugBdrugC 是否在您的数据集中实际可用,或者它们是否只是为了在此处创建数据集而存在?如果是后者,我建议删除它们以仅包含您的实际数据集的样子。
  • @Cole 我的实际数据集按通用名称有超过 15 种不同的药物类别。包括药物的实际名称是大材小用;我创建了这个示例,以便我可以在我的实际数据集上重现它,而不管药物名称是什么。我刚刚看到你发布了一个答案。我将在当天晚些时候彻底讨论它。谢谢!!
  • 抱歉,不是命名约定,而是字段本身。如果有的话,我会将名称缩短为 A、B、C。

标签: r data.table robust rowwise


【解决方案1】:

这很难。对于这 1 亿个数据集,strsplit 的内存效率不会很高 - 每行都需要从strsplit 创建两个列表。我的建议是使用函数并跳过by = 1:.N 步骤。

exposed = function(before, after) {
  out = vector(length = length(before))
  for (i in seq_along(before)) {
    bef = before[i]
    aft = after[i]
    if (bef == "NONE" || aft == "NONE") 
      out[i] = FALSE
    else
      out[i] = any(!unlist(strsplit(aft, "[+]", fixed = TRUE), use.names = FALSE)%chin%unlist(strsplit(bef, "[+]", fixed = TRUE), use.names = FALSE))
  }
  return(out)
}

DT[, TI3 := exposed(exposure.before.index, exposure)]

> DT[, .(exposure.before.index, exposure, TI, TI3)]
   exposure.before.index          exposure    TI   TI3
1:                 drugA             drugA FALSE FALSE
2:                 drugA       drugA+drugB  TRUE  TRUE
3:                 drugA drugA+drugB+drugC  TRUE  TRUE
4:                 drugB             drugB FALSE FALSE
5:                 drugB             drugC  TRUE  TRUE
6:                  NONE              NONE FALSE FALSE
7:                  NONE              NONE FALSE FALSE

注意这里有一些优化:

  1. 使用%chin% 代替%in%,这是一个 实用函数,在字符向量上比%in% 更快
  2. 使用strsplit(..., fixed = TRUE) 进行优化 - 这不是我们使用的正则表达式。可能是最大的性能提升。
  3. unlist(..., use.names = FALSE)

下一步是将函数转换为Rcpp,此处未完成。字符串比Rcpp 中的数字更复杂(至少对我而言)。

下面是这个函数的表现。对于 7 行示例,这要快 4 倍。但是随着行数的增加,速度差异变得不那么明显了:

## 7 rows
Unit: microseconds
   expr      min       lq     mean   median       uq       max
 use_fx  375.801  395.251  662.582  409.751  431.351 21345.701
     OP 1889.901 2021.601 2211.858 2096.101 2285.201  4042.801

## 700,000 rows
Unit: seconds
   expr       min        lq      mean    median        uq       max
 use_fx  4.409595  4.409595  4.409595  4.409595  4.409595  4.409595
     OP 12.592520 12.592520 12.592520 12.592520 12.592520 12.592520

## 7,000,000 rows
Unit: seconds
   expr       min        lq      mean    median        uq       max
 use_fx  43.90979  43.90979  43.90979  43.90979  43.90979  43.90979
     OP 130.16418 130.16418 130.16418 130.16418 130.16418 130.16418

## code used:
DT_big = DT[rep(seq_len(.N), 1e5)]
microbenchmark(
  use_fx = DT_big[, TI3 := exposed(exposure.before.index, exposure)],
  OP = {
    DT_big[,CNT:=1:.N]
    DT_big[!(exposure.before.index!="NONE" & exposure=="NONE"),TI:=(any(!unlist(strsplit(exposure, "[+]")) %in% unlist(strsplit(exposure.before.index, "[+]")))),by="CNT"]
    DT_big[is.na(TI),TI:=FALSE]
  }
  , times = 1L
)

如果您对Rcpp 感兴趣,这可能会有所帮助:

https://wckdouglas.github.io/2015/05/string-manipulation

【讨论】:

  • exposure.before.index 可以有多种药物吗?根据您的代码,我假设是的,strsplit(...)
猜你喜欢
  • 2011-12-14
  • 1970-01-01
  • 2014-02-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多