Как сделать более быстрые операции со столбцами списка внутри data.table

Из-за проблем с памятью (и скоростью) я надеялся выполнить некоторые вычисления внутри таблицы data.table вместо того, чтобы делать их вне ее.

Следующий код имеет 100 000 строк, но я работаю с 40 миллионами строк.

library(tictoc)
library(data.table) # version 1.11.8
library(purrr)
library(furrr)
plan(multiprocess)

veryfing_function <- function(vec1, vec2){
  vector <- as.vector(outer(vec1, vec2, paste0))
  split(vector, ceiling(seq_along(vector)/length(vec1)))
}


dt <- data.table(letters = replicate(1e6, sample(letters[1:5], 3, TRUE), simplify = FALSE),
                 numbers = replicate(1e6, sample(letters[6:10], 3, TRUE), simplify = FALSE))



tic()
result1 <- future_map2(dt$letters, dt$numbers, veryfing_function)
toc()


tic()
result2 <- mapply(veryfing_function, dt$letters, dt$numbers, SIMPLIFY = FALSE)
toc()



tic()
dt[, result := future_map2(letters, numbers, veryfing_function)]
toc()


tic()
dt[, result2 := mapply(veryfing_function, letters, numbers, SIMPLIFY = FALSE)]
toc()

Вывод одинаков для всех вариантов и ожидается. Тесты были:

26 с 72 с 38 с 105 с, поэтому я не видел преимуществ в использовании функций из таблицы данных или использовании mapply.

Моя главная проблема - память, которая не решается с помощью решения future_map2.

Я сейчас пользуюсь Windows, поэтому я надеялся найти решение для скорости, отличной от mclapply, возможно, какой-то хитрости с data.table я не вижу (использование ключей для списков не поддерживается)

2 ответа

Решение

Это действительно вопрос о типах памяти и хранения данных. Все мое обсуждение будет посвящено 100 000 элементов данных, чтобы все не затухало.

Давайте рассмотрим вектор длиной 100 000 против списка, содержащего 100 000 отдельных элементов.

object.size(rep(1L, 1E5))
#400048 bytes
object.size(replicate(1E5, 1, simplify = F))
#6400048 bytes

Мы получили от 0,4 МБ до 6,4 МБ, просто сохранив данные по-разному! При применении этого к вашей функции Map(veryfing_function, ...) и только 1E5 элементов:

dt <- data.table(letters = replicate(1e5, sample(letters[1:5], 3, TRUE), simplify = FALSE),
                 numbers = replicate(1e5, sample(letters[6:10], 3, TRUE), simplify = FALSE))

tic()
result2 <- Map(veryfing_function, dt[['letters']], dt[['numbers']])
toc()
# 11.93 sec elapsed
object.size(result2)
# 109,769,872 bytes
#example return:
[[1000]]
[[1000]]$`1`
[1] "cg" "bg" "cg"

[[1000]]$`2`
[1] "ch" "bh" "ch"

[[1000]]$`3`
[1] "ch" "bh" "ch"

Мы могли бы сделать простую модификацию вашей функции, чтобы вернуть неназванные списки вместо разделения, и мы сохраним немного памяти как split() кажется, дает именованные списки, и я не думаю, что нам нужно имя:

verifying_function2 <- function(vec1, vec2) {
  vector <- outer(vec1, vec2, paste0) #not as.vector
  lapply(seq_len(ncol(vector)), function(i) vector[, i]) #no need to split, just return a list
}

tic()
result2_mod <- Map(verifying_function2, dt[['letters']], dt[['numbers']])
toc()
# 2.86 sec elapsed
object.size(result2_mod)
# 73,769,872 bytes

#example_output
[[1000]]
[[1000]][[1]]
[1] "cg" "bg" "cg"

[[1000]][[2]]
[1] "ch" "bh" "ch"

[[1000]][[3]]
[1] "ch" "bh" "ch"

Следующий шаг - зачем вообще возвращать список. Я использую lapply() в измененной функции просто доберитесь до вашего вывода. Потерять lapply() вместо этого будет список матриц, которые, я думаю, будут полезны:

tic()
result2_mod2 <- Map(function(x,y) outer(x, y, paste0), dt[['letters']], dt[['numbers']])
toc()
# 1.66 sec elapsed
object.size(result2_mod2)
# 68,570,336 bytes

#example output:
[[1000]]
     [,1] [,2] [,3]
[1,] "cg" "ch" "ch"
[2,] "bg" "bh" "bh"
[3,] "cg" "ch" "ch"

Последний логический шаг - просто вернуть матрицу. Обратите внимание, что все это время мы боролись с упрощением mapply(..., simplify = F) что эквивалентно Map(),

tic()
result2_mod3 <- mapply(function(x,y) outer(x, y, paste0), dt[['letters']], dt[['numbers']])
toc()
# 1.3 sec elapsed
object.size(result2_mod3)
# 7,201,616 bytes

Если вам нужна некоторая размерность, вы можете преобразовать большую матрицу в трехмерный массив:

tic()
result2_mod3_arr <- array(as.vector(result2_mod3), dim = c(3,3,1E5))
toc()
# 0.02 sec elapsed
result2_mod3_arr[,,1000]
     [,1] [,2] [,3]
[1,] "cg" "ch" "ch"
[2,] "bg" "bh" "bh"
[3,] "cg" "ch" "ch"
object.size(result2_mod3_arr)
# 7,201,624 bytes

Я также посмотрел на ответ @ marbel - он быстрее и занимает немного больше памяти. Мой подход, скорее всего, выиграет, если dt Перечислите что-нибудь еще раньше.

tic()
dt1 = as.data.table(do.call(rbind, dt[['letters']]))
dt2 = as.data.table(do.call(rbind, dt[['numbers']]))

res = data.table()

combs = expand.grid(names(dt1), names(dt2), stringsAsFactors=FALSE)

set(res, j=paste0(combs[,1], combs[,2]), value=paste0( dt1[, get(combs[,1])], dt2[, get(combs[,2])] ) )
toc()
# 0.14 sec elapsed
object.size(res)
# 7,215,384 bytes

tl; dr - преобразовать ваш объект в матрицу или data.frame, чтобы облегчить вашу память. Также имеет смысл, что data.table версии вашей функции занимают больше времени - вероятно, больше накладных расходов, чем просто прямое применение mapply(),

Это другой подход к проблеме, но я считаю, что это может быть полезно.

Вывод отличается, так что я не уверен без дополнительной информации, будет ли он служить вашей конкретной проблеме, но здесь все идет, надеюсь, это поможет!

Время составляет 1,165 секунды против 87 секунд сопоставления.

vec1 = replicate(1e6, sample(letters[1:5], 3, TRUE), simplify = FALSE)
vec2 = replicate(1e6, sample(letters[6:10], 3, TRUE), simplify = FALSE)

dt <- data.table(v1 = vec1, v2 = vec2)
dt1 = as.data.table(do.call(rbind, vec1))
dt2 = as.data.table(do.call(rbind, vec2))
res = data.table()

tic()
cols1 = names(dt1)
cols2 = names(dt2)
combs = expand.grid(cols1, cols2, stringsAsFactors=FALSE)

for(i in 1:nrow(combs)){
  vars = combs[i, ]
  set(res, j=paste0(vars[,1], vars[,2]), value=paste0( dt1[, get(vars[,1])], dt2[, get(vars[,2])] ) )
}
toc()
Другие вопросы по тегам