一种好的方法是使用for
循环。它不是逐行操作,而是针对"列'X'中有NA
的所有行",一次性地对cols
中的每列进行操作。
for (i in seq_along(cols)[-1]) {
prevcol <- cols[i-1]
thiscol <- cols[i]
dt[is.na(get(thiscol)), (thiscol) := fcoalesce(get(thiscol), get(prevcol)) ]
}
dt
诚然,使用get(.)
并不是完美的,但我认为它通常还可以。
另一种方法,速度大致相同(取决于数据大小):
dt[, (cols) := Reduce(function(prev,this) fcoalesce(this, prev), .SD, accumulate = TRUE), .SDcols = cols]
基准测试,因为您说在2M行的情况下,性能很重要。
我将使用2M行,并更新随机化 NA
的方法。
library(data.table)
set.seed(456)
n <- 2e6
dt <- data.table(a = sample(1:4,n, replace=T), b = sample(1:4,n, replace=T), c = sample(1:4,n, replace=T), d = sample(1:4,n, replace=T), e = sample(1:4,n, replace=T), f = sample(1:4,n, replace=T), g = sample(1:4,n, replace=T), h = sample(1:4,n, replace=T), i = sample(1:4,n, replace=T), j = sample(1:4,n, replace=T), xx = sample(1:4,n, replace=T))
mtx <- cbind(sample(nrow(dt), ceiling(n*11/20), replace=TRUE), sample(ncol(dt), ceiling(n*11/20), replace=TRUE))
mtx <- mtx[!duplicated(mtx),]
dt[mtx] <- NA
head(dt)
不幸的是,transpose
方法失败了:
system.time({
dt2 = transpose(dt)
setnafill(dt2, type = 'locf')
dt2 = transpose(dt2)
setnames(dt2, names(dt))
})
但是for
循环(以及顺带一提的Reduce
)可以正常工作:
cols <- setdiff(names(dt),"N")
system.time({
for (i in seq_along(cols)[-1]) {
prevcol <- cols[i-1]
thiscol <- cols[i]
dt[is.na(get(thiscol)), (thiscol) := fcoalesce(get(thiscol), get(prevcol)) ]
}
})
head(dt)
如果我将问题集简化为60万行,那么两种方法都可以工作。(我不知道我的系统的翻转点是多少...可能是100万,谁知道呢,我只是想将它们并排比较。)使用n < - 6e5
并生成dt
,我看到以下数据和简单计时:
head(dt)
sum(is.na(dt))
system.time({
dt2 = transpose(dt)
setnafill(dt2, type = 'locf')
dt2 = transpose(dt2)
setnames(dt2, names(dt))
})
sum(is.na(dt))
sum(is.na(dt2))
cols <- setdiff(names(dt),"N")
system.time({
for (i in seq_along(cols)[-1]) {
prevcol <- cols[i-1]
thiscol <- cols[i]
dt[is.na(get(thiscol)), (thiscol) := fcoalesce(get(thiscol), get(prevcol)) ]
}
})
identical(dt, dt2)
system.time({
dt[, (cols) := Reduce(function(prev,this) fcoalesce(this,prev), .SD, accumulate = TRUE), .SDcols = cols]
})
identical(dt, dt2)
一个更加强大的基准测试工具,例如
bench::mark
,需要在每一次执行时复制
dt
,这会稍微拖慢速度。虽然这个额外的开销不是很大,但还是存在的。
bench::mark(copy(dt))
这仍然是额外的。因此,我将对transpose
代码进行两次比较,一次带有额外操作,一次没有,以更好地与for
和reduce
答案进行更加公正的比较。(注意,bench :: mark
的默认操作是验证所有输出是否相同
。这可以禁用,但我没有这样做,因此所有代码块返回相同的结果。)
bench::mark(
transpose1 = {
dt2 = transpose(dt)
setnafill(dt2, type = 'locf')
dt2 = transpose(dt2)
setnames(dt2, names(dt))
dt2
},
transpose2 = {
dt0 = copy(dt)
dt2 = transpose(dt0)
setnafill(dt2, type = 'locf')
dt2 = transpose(dt2)
setnames(dt2, names(dt0))
dt2
},
forloop = {
dt0 <- copy(dt)
for (i in seq_along(cols)[-1]) {
prevcol <- cols[i-1]
thiscol <- cols[i]
dt0[is.na(get(thiscol)), (thiscol) := fcoalesce(get(thiscol), get(prevcol)) ]
}
dt0
},
reduce = {
dt0 <- copy(dt)
dt0[, (cols) := Reduce(function(prev,this) fcoalesce(this,prev), .SD, accumulate = TRUE), .SDcols = cols]
},
min_iterations = 10)
从这里开始:
- 时间:将时间标准化为毫秒后,4840毫秒与48毫秒相比较差;且
- 内存:1.28GB 与 71MB 相比较差。
(编辑以将基准测试的最小迭代次数增加到10。)
data.table
中唯一的解决方法,我会感到惊讶的,因此我会等待一段时间。 - Samdata.table
真正关注的是性能和内存效率(这两者非常相互关联),虽然这两种方法看起来并不完全符合data.table
的规范,但它们比data.table::transpose
的答案在时间上快了 100 倍,在内存分配方面快了 18 倍。 - r2evans