我试图读取几十万个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
}

我以为每个列的模式可能已在每次更新时设置和重置,但是我也尝试通过预先设置每个列的类型来进行此操作,但这没有帮助。

对我而言,使用data.frame(上面注释)运行此示例大约需要22秒,而转换为data.table则需要5秒。我希望有人知道幕后的情况,并可以提供一种更快的方法来填充此处的数据表。

最佳答案

除了您构建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)
}))

现在我们有了正确的类型集。接下来,我认为应该是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中。我知道这有点微妙。但是,这是必需的,因为它允许创建x列作为DT[, x := val]的可能性,在该列中您实际上希望将val分配给x

回到优化,好消息是,您耗时的循环很简单:
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倍。我认为这是您要寻找的结果。请验证一下。

高温超导

10-07 13:00
查看更多