稀疏数据框的直接更新(替换)速度缓慢且效率低下。

9
我试图读取几十万个JSON文件,并最终将它们转换为dplyr对象。但是,这些JSON文件不是简单的键值对解析,需要进行大量预处理。预处理已编码且效率相当高。但是,我遇到的挑战是如何高效地将每个记录加载到单个对象(data.table或dplyr对象)中。
这是非常稀疏的数据,我将有超过2000个变量,其中大部分都将缺失。每个记录可能设置了一百个变量。这些变量将是字符、逻辑和数字的混合,我知道每个变量的模式。
我认为避免R复制每次更新对象(或一次添加一行)的最佳方法是创建一个空数据框,然后在从JSON文件中提取特定字段后更新它们。但是,在数据框中执行此操作非常缓慢,转移到data.table或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
}

我认为每列的模式可能已经在每次更新时设置和重置,但我也尝试过通过预设每个列类型来解决,但这并没有帮助。

对于我来说,使用数据框架(在上面被注释掉)运行此示例需要约22秒,转换为数据表则只需5秒。我希望有人知道在底层发生了什么,并能提供一种更快的方法来填充数据表。


你是否已经将所有的JSON文件预解析成列表,还是在循环中逐个解析? - MrFlick
1个回答

16

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

首先,创建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)
}))

现在我们已经有了正确的类型设置。接下来,我认为应该是i >= 402L & i < 801L。否则,你正在将前401列分配为逻辑列,然后将前801列分配为数值列,考虑到你提前知道列的类型,这没有太多意义,对吧?

其次,在执行names(.) <-时:

这行代码是:

names(sparse.val) <- paste0("A", sparse.cols)

这行代码创建了一个副本,实际上并不必要。因此我们将删除这一行。

第三,时间消耗较长的for循环:

for(x in names(sparse.val)) {
    val=sparse.val[[x]]
    outdf[i, x:=val]
}

它实际上并没有做你想象中的事情。它并没有将val的值分配给赋给x的名称。相反,它每次都会(覆)写一个名为x的列。检查你的输出。


这不是优化的一部分。这只是让你知道你实际上想要在这里做什么。

for(x in names(sparse.val)) {
    val=sparse.val[[x]]
    outdf[i, (x) := val]
}

注意在x周围的(。现在,它将被评估,并且x中包含的值将是val的值要分配到的列。我明白这有点微妙。但是,这是必要的,因为它允许可能创建列xDT[, x := val],其中您实际上希望将val分配给x


回到优化问题,好消息是,您耗时的for循环可以简化为:

set(outdf, i=i, j=paste0("A", sparse.cols), value = sparse.val)

这就是 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
}

通过这样做,您的解决方案在我的系统上花费9.84秒,而上面的函数只需0.34秒,这是 ~29倍的改进。我认为这是您要寻找的结果。请验证一下。

希望对您有所帮助。


1
这大大改善了时间,而set()不是代码中最慢的调用。你在设置一个名为“x”的列上是正确的,我发现另一个关于名称中空格的缺陷(已分离和修复)。此外,你在首次创建数据表时使用的方法非常优雅,也有助于简化代码。谢谢! - jjacobs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接