【问题标题】:complex data.table subset and vectorised maniulation复杂的data.table子集和向量操作
【发布时间】:2015-03-28 18:52:20
【问题描述】:

好的,我有一个使用 data.frames 构建的复杂函数,为了加快它的速度,我转向了 data.table。我对此完全陌生,所以我很困惑。无论如何,我已经为我想做的事情做了一个简单得多的玩具示例,但我无法弄清楚如何将其转换为 data.table 格式。下面是data.frame形式的例子:

    rows <- 10
    data1 <- data.frame(   id =1:rows,
                    a = seq(0.2, 0.55, length.out = rows),
                  b = seq(0.35, 0.7, length.out = rows),
                  c = seq(0.4, 0.83, length.out = rows),
                  d = seq(0.6, 0.87, length.out = rows),
                  e = seq(0.7, 0.99, length.out = rows),
                  f = seq(0.52, 0.90, length.out = rows)             
    )
    DT1 <- data.table(data1) #for later

    data2 <- data.frame(   id =3:1,
                   a = rep(3, 3),
                   d = rep(2, 3),
                   f = rep(1, 3)
    )
    m.names <- c("a", "d", "f")

    data1[match(data2$id, data1$id),m.names] <- data1[match(data2$id, data1$id),m.names] + data2[match(data2$id, data1$id),m.names]

所以请注意,在最后一步中,我希望在预先存在的数字和新数据之间执行加法,并将其向量化到多个列中。

在 data.table 格式中,我只做到了这一点:

    DT1[id %in% data2$id, m.names, with=FALSE]

这会选择我想要添加的值,但之后我就迷路了。我将不胜感激!

编辑:

好的,我已经弄清楚了其中的一部分 - 我可以使用上面的最后一行代码来实现矢量化加法部分,使用 data2 来存储添加的值,如下所示:

    data2[,m.names] <- data2[,m.names] + data.frame(DT1[id %in% data2$id, m.names, with=FALSE])

即使有 250 万行(在 DT1 中)和 data2 中的 10,000 行和 6 个匹配列,这也只需要 0.004 秒,但我仍然需要将新的 data2 分配给数据 1 中适当的动态分配列

【问题讨论】:

  • 如果您不介意手动调用列名,可以进行简单的二进制连接,例如 setkey(setDT(data1), id) ; data1[data2, ":="(a = a + i.a,d = d + i.d,f = f + i.f)]
  • @DavidArenburg 您的解决方案更好、更紧凑。
  • @akrun 你的解决方案更通用,所以我会把它留在那里,让 OP 决定
  • 感谢大卫,但在我的真实数据中不起作用。我不能使用 a,d,f 我需要使用 m.names[1] 因为列数可能会有所不同 - 使用 m.names[] 构造似乎不起作用..第二点在玩具数据中匹配版本出现得更快(并且在我使用 fmatch 的真实代码中)
  • 好的,我找到了一种通过矢量化进行添加的快速方法 - 请参阅我的编辑 - 但我仍然需要使用分配的变量列名将结果打包回 data1 ??

标签: r data.table


【解决方案1】:

这是另一种方式,使用devel version data.table v1.9.5:

require(data.table) ## v1.9.5+
setDT(data1)        ## data1 is now a data.table

cols1 = c("a", "d", "f")
cols2 = paste0("i.", cols1)
setkey(data1, id)   ## setkey and prepare for join
data1[data2, (cols1) := mapply(`+`, mget(cols1), mget(cols2), SIMPLIFY=FALSE)]
#     id         a         b         c    d         e         f
#  1:  1 3.2000000 0.3500000 0.4000000 2.60 0.7000000 1.5200000
#  2:  2 3.2388889 0.3888889 0.4477778 2.63 0.7322222 1.5622222
#  3:  3 3.2777778 0.4277778 0.4955556 2.66 0.7644444 1.6044444
#  4:  4 0.3166667 0.4666667 0.5433333 0.69 0.7966667 0.6466667
#  5:  5 0.3555556 0.5055556 0.5911111 0.72 0.8288889 0.6888889
#  6:  6 0.3944444 0.5444444 0.6388889 0.75 0.8611111 0.7311111
#  7:  7 0.4333333 0.5833333 0.6866667 0.78 0.8933333 0.7733333
#  8:  8 0.4722222 0.6222222 0.7344444 0.81 0.9255556 0.8155556
#  9:  9 0.5111111 0.6611111 0.7822222 0.84 0.9577778 0.8577778
# 10: 10 0.5500000 0.7000000 0.8300000 0.87 0.9900000 0.9000000

x[i] 形式的 join 在键列 id 上执行。对于data2 的id 列的每一行,在data1 中找到对应的匹配行。例如,对于来自 data2 的id = 2,匹配行是 data1 中的第 2 行。

一旦我们找到所有匹配的行,我们就会评估j 中的表达式,它通过添加mget(cols1) 和mget(cols2) 中的值来更新col1 中提供的data1 列.

cols2 是使用 i. 前缀生成的,它从 data.table i 获取值——这里是 data2。

HTH

【讨论】:

  • 看起来不错,但需要开发版吗?我正在 v1.9.4 下尝试它,它给了我错误:Error: value for ‘a’ not found
【解决方案2】:

一种方法是在for 循环中使用set,因为这涉及多个列。将第二个数据集转换为“data.table”(DT2),使用“id”列设置键,并使用“data1”连接。为数据集中的“m.names”列(“indx1”)和连接后创建的i. 列(“indx2”)创建列索引向量。使用for 循环,set 'm.names' 列中的NA 元素为'0',然后根据'indx1' 和'indx2' 对相应的列求和。

DT2 <- as.data.table(data2)
DTNew <- setkey(DT2, id)[data1]
indx1 <- match(m.names, names(DTNew))
indx2 <- grep('i\\.', names(DTNew))

for(k in seq_along(indx1)){
  set(DTNew, i=which(is.na(DTNew[[indx1[k]]])), j= indx1[k], value=0)
  set(DTNew, i=NULL, j= indx2[k], value = DTNew[[indx1[k]]]+
                                      DTNew[[indx2[k]]])
  }

 res <- DTNew[,2:4 := NULL]
 setnames(res, names(data1))

检查修改后的“data1”

  data1[match(data2$id, data1$id),m.names] <- data1[match(data2$id, 
       data1$id),m.names] + data2[match(data2$id, data1$id),m.names]
  all.equal(setDF(res), data1)
  #[1] TRUE

基准测试

On a 1e6 dataset, 

set.seed(24)
data1 <- cbind(id=1:1e6,as.data.frame(matrix(rnorm(1e6*10), ncol=10, 
         dimnames=list(NULL, letters[1:10])) ))
 set.seed(46)
 data2 <- data.frame(id= sample(1:1000, 100, replace=FALSE), 
             a= rnorm(100), d=rnorm(100), f= rnorm(100))
 m.names <- c("a", "d", "f")

 DT2 <- as.data.table(data2)
 system.time({
   DTNew <- setkey(DT2, id)[data1]
   indx1 <- match(m.names, names(DTNew))
   indx2 <- grep('i\\.', names(DTNew))

   for(k in seq_along(indx1)){
   set(DTNew, i=which(is.na(DTNew[[indx1[k]]])), j= indx1[k], value=0)
   set(DTNew, i=NULL, j= indx2[k], value = DTNew[[indx1[k]]]+
                                  DTNew[[indx2[k]]])
   }

  res <- DTNew[,2:4 := NULL]
  setnames(res, names(data1))
  })

 # user  system elapsed 
 # 0.082   0.005   0.086 

【讨论】:

  • 我必须一行一行地把它带到船上才能得到它——但我突然觉得这有很多步骤,而且可能很慢?
  • @user2498193 set 对于大型数据集通常非常快。您可以查看?set 中比较不同方法的示例。关于多步,两步是创建索引,对速度影响不大。二进制连接,set,然后将额外的列分配给 NULL 很快。
  • 这就是我现在正在做的事情——在 1e6 数据集上比你的快 40%:system.time({ DT1 &lt;- data.table(data1) setkey(DT1, id) ; setkey(DT2, id) text &lt;- NULL for(i in 1:length(m.names)){ text &lt;- paste0(text, m.names[i], " = ", m.names[i], " + i.", m.names[i], ", ") } expr &lt;- parse(text = paste0("\":=\"(", substr(text, 1, nchar(text)-2), ")" )) res2 &lt;- DT1[DT2, eval(expr)] }) 但identical(res, res2) 由于某种原因失败了,我无法轻易发现
  • @user2498193 谢谢,无论如何,这是做同样事情的另一种方式,尽管我会使用eval(parse 作为最后的手段。如果任何属性不同,identical 可能会失败..
  • @user2498193 这个链接可能会给一些想法talkstats.com/showthread.php/20974-Why-is-eval-parse-bad
【解决方案3】:

好的,感谢@David Arenburg 的建议。我对它进行了一些修改,以获得以下我首选的解决方案

    text <- NULL
    for(i in 1:length(m.names)){
        text <- paste0(text, m.names[i], " = ", m.names[i], " + i.", m.names[i], ", ")
    }
    expr <- parse(text = paste0("\":=\"(", substr(text, 1, nchar(text)-2), ")" ))

    res2 <- DT1[data2, eval(expr)]

【讨论】:

  • 我认为这里不需要for 循环,paste0 是矢量化的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-06-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-15
  • 2019-12-03
  • 2017-03-18
相关资源
最近更新 更多