Прямое обновление (замена) разреженного кадра данных является медленным и неэффективным

Я пытаюсь читать в нескольких сотнях тысяч файлов JSON и в конечном итоге превращать их в объект dplyr. Но файлы JSON - это не просто синтаксический анализ ключа и требуют много предварительной обработки. Предварительная обработка кодируется и достаточно хороша для эффективности. Но проблема, с которой я сталкиваюсь, заключается в том, чтобы эффективно загружать каждую запись в один объект (data.table или dplyr object).

Это очень редкие данные, у меня будет более 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
}

Я думал, что режим каждого столбца может быть установлен и reset с каждым обновлением, но я также пробовал это, предварительно задав каждый тип столбца, и это не помогло.

Для меня, запуск этого примера с data.frame(закомментированный выше) занимает около 22 секунд, преобразование в таблицу данных составляет 5 секунд. Я надеялся, что кто-то знает, что происходит под обложками, и может обеспечить более быстрый способ заполнения таблицы данных здесь.

Ответ 1

Я следую вашему коду, кроме той части, где вы строите 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(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.


Возвращаясь к оптимизации, хорошей новостью является то, что ваше трудоемкое for-loop просто:

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 раз. Я думаю, что это результат, который вы ищете. Пожалуйста, проверьте его.

НТН