如何在 data.table 中进行更快的列表列操作

How to do faster list-column operations inside data.table

由于内存(和速度)问题,我希望在 data.table 内部进行一些计算,而不是在其外部进行。

以下代码有 100.000 行,但我处理的是 4000 万行。

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 秒 ,所以我认为使用 data.table 中的函数或使用 mapply 没有任何优势。

我主要担心的是内存问题,future_map2 解决方案没有解决这个问题。

我现在正在使用 Windows,所以我希望找到一个除了 mclapply 之外的速度解决方案,也许是一些我没有看到的 data.table 技巧(不支持键控列表)

这是解决问题的另一种方法,但我相信它很有用。

输出不同,所以在没有更多信息的情况下我不确定它是否能解决您的具体问题,但就到此为止,希望对您有所帮助!

时间为 1.165 秒,而 mapply 为 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()

这真的是一个关于内存和数据存储类型的问题。我的所有讨论都将针对 100,000 个数据元素,这样一切都不会陷入困境。

让我们检查一下长度为 100,000 的向量与包含 100,000 个独立元素的列表。

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

仅通过以不同方式存储数据,我们就从 0.4 MB 减少到 6.4 MB!!将此应用于您的函数时 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"

我们可以对您的函数做一个简单的修改,使 return 未命名列表而不是拆分,我们可以节省一点内存,因为 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"

下一步就是为什么return一个列表的列表了。我在修改后的函数中使用 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"

最后一个合乎逻辑的步骤是 return 一个矩阵。请注意,我们一直在反对使用等同于 Map()mapply(..., simplify = F) 进行简化。

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

如果你想要一些维度,你可以将大矩阵转换为 3D 数组:

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().

相比,开销可能更多