直接更新(替换)稀疏数据帧是缓慢且低效的

jja*_*obs 8 r dplyr data.table

我正在尝试读取几十万个JSON文件,并最终将它们放入dplyr对象中.但是JSON文件不是简单的键值解析,它们需要大量的预处理.预处理是经过编码的,效率相当高.但我遇到的挑战是将每条记录有效地加载到单个对象(data.table或dplyr对象)中.

这是非常稀疏的数据,我将有超过2000个变量,这些变量大部分都会丢失.每条记录可能有一百个变量集.变量将是字符,逻辑和数字的混合,我知道每个变量的模式.

我认为避免R为每次更新复制对象(或一次添加一行)的最佳方法是创建一个空数据框,然后在从JSON文件中提取后更新特定字段.但是在数据框中执行此操作非常慢,移动到数据表或dplyr对象要好得多,但仍希望将其减少到几分钟而不是几小时.请参阅下面的示例:

timeMe <- function() {
  set.seed(1)
  names = paste0("A", seq(1:1200))

  # try with a data frame
  # outdf <- data.frame(matrix(NA, nrow=100, ncol=1200, dimnames=list(NULL, names)))
  # try with data table
  outdf <- data.table(matrix(NA, nrow=100, ncol=1200, dimnames=list(NULL, names)))

  for(i in seq(100)) {
    # generate 100 columns (real data is in json)
    sparse.cols <- sample(1200, 100)
    # Each record is coming in as a list
    # Each column is either a character, logical, or numeric
    sparse.val <- lapply(sparse.cols, function(i) {
      if(i < 401) {  # logical
        sample(c(TRUE, FALSE), 1) 
      } else if (i < 801) {  # numeric
        sample(seq(10), 1)
      } else { # character
        sample(LETTERS, 1)
      }
    })  # now we have a list with values to populate
    names(sparse.val) <- paste0("A", sparse.cols)

    # and here is the challenge and what takes a long time.
    # want to assign the ith row and the named column with each value
    for(x in names(sparse.val)) {
      val=sparse.val[[x]]
      # this is where the bottleneck is.
      # for data frame
      # outdf[i, x] <- val
      # for data table
      outdf[i, x:=val]
    }
  }  
  outdf
}
Run Code Online (Sandbox Code Playgroud)

我认为每个列的模式可能已经设置并重置每次更新,但我也通过预先设置每个列类型尝试了这一点,这没有帮助.

对我来说,使用data.frame(上面已注释掉)运行此示例大约需要22秒,转换为data.table为5秒.我希望有人知道幕后发生了什么,并且可以提供更快的方法来填充数据表.

Aru*_*run 14

我遵循你的代码,除了你构建的部分sparse.val.分配列的方式有一些小错误.不要忘记检查答案是否正确尝试优化:).

一,创建data.table:

由于您说您已经知道列的类型,因此预先生成正确的类型非常重要.否则,当你这样做:DT[, LHS := RHS]并且RHS类型不等于时LHS,RHS将被强制转换为LHS的类型.在您的情况下,所有数值和字符值都将转换为逻辑,因为所有列都是逻辑类型.这不是你想要的.

因此,创建矩阵将无济于事(所有列都属于同一类型)+它也很慢.相反,我会这样做:

rows = 100L
cols = 1200L
outdf <- setDT(lapply(seq_along(cols), function(i) {
    if (i < 401L) rep(NA, rows)
    else if (i >= 402L & i < 801L) rep(NA_real_, rows)
    else rep(NA_character_, rows)
}))
Run Code Online (Sandbox Code Playgroud)

现在我们有正确的类型集.接下来,我认为应该是i >= 402L & i < 801L.否则,您将前401列分配为逻辑列,然后将前801列分配为数字,如果您知道前面列的类型,则没有多大意义,对吧?

第二,做names(.) <-:

这条线:

names(sparse.val) <- paste0("A", sparse.cols)
Run Code Online (Sandbox Code Playgroud)

将创建一个副本,而不是真的有必要.因此我们将删除此行.

第三,循环耗时:

for(x in names(sparse.val)) {
    val=sparse.val[[x]]
    outdf[i, x:=val]
}
Run Code Online (Sandbox Code Playgroud)

实际上并没有做你认为它正在做的事情.它没有将值分配给分配val给的名称x.相反,它(过)写入(每次)一个名为的列x.检查你的输出.


这不是优化的一部分.这只是为了让你知道你真正想要做什么.

for(x in names(sparse.val)) {
    val=sparse.val[[x]]
    outdf[i, (x) := val]
}
Run Code Online (Sandbox Code Playgroud)

注意(周围x.现在,它将被评估,并且包含的值将是将赋值x的列val.我明白,这有点微妙.但是,这是必要的,因为它允许创建列的可能性xDT[, x := val],你居然要val被分配到x.


回到优化,好消息是,你的耗时for循环很简单:

set(outdf, i=i, j=paste0("A", sparse.cols), value = sparse.val)
Run Code Online (Sandbox Code Playgroud)

这是data.table子分配参照功能就派上用场了!

把它们放在一起:

你的最终功能如下:

timeMe2 <- function() {
    set.seed(1L)

    rows = 100L
    cols = 1200L
    outdf <- as.data.table(lapply(seq_len(cols), function(i) {
        if (i < 401L) rep(NA, rows)
        else if (i >= 402L & i < 801L) rep(NA_real_, rows)
        else sample(rep(NA_character_, rows))
    }))
    setnames(outdf, paste0("A", seq(1:1200)))

    for(i in seq(100)) {
        sparse.cols <- sample(1200L, 100L)
        sparse.val <- lapply(sparse.cols, function(i) {
            if(i < 401L) sample(c(TRUE, FALSE), 1) 
            else if (i >= 402 & i < 801L) sample(seq(10), 1)
            else sample(LETTERS, 1)
        })
        set(outdf, i=i, j=paste0("A", sparse.cols), value = sparse.val)
    }  
    outdf
}
Run Code Online (Sandbox Code Playgroud)

通过这样做,您的解决方案在我的系统上需要9.84秒,而上述功能需要0.34秒,这是〜29倍的改进.我认为这是你正在寻找的结果.请验证它.

HTH