这个老问题现在已经被浏览了超过一万次。
虽然它有一个公认的答案,但我觉得这个问题值得
- 正版
data.table解决方案,
- 解释为什么
Date 失败并出现ifelse() 和
- 为什么
replace() 方法会返回错误的结果。
data.table 接近
data.table、ifelse() 和 replace() 可以写成两个链式赋值操作,其中第二个使用子集:
dt[, z := y][is.na(z), z := x][]
x y z
1: 2014-01-01 <NA> 2014-01-01
2: 2015-01-01 2015-06-01 2015-06-01
3: 2016-01-01 <NA> 2016-01-01
第一个赋值操作通过复制y 列来创建一个新列z。第二个赋值操作通过将x 列的内容仅复制到z 为NA 的那些行 来修改z 就地。
或者,我们可以先复制x,然后将z 值替换为非NA y 值:
dt <- copy(dt_orig) # use a fresh copy of dt
dt[, z := x][!is.na(y), z := y][]
如果y 中有许多NA 值,后者可能会更有效。
replace() 方法中的错误
Frank has suggested 使用replace() 而不是ifelse(),后者被C8H10N4O2 in an edit of his answer 拾取。不幸的是,这两个代码不仅会产生警告,而且只是返回错误的结果:
dt <- copy(dt_orig) # use a fresh copy of dt
# C8H10N4O2's version
dt[, z := replace(y, is.na(y), x)][]
dt <- copy(dt_orig) # use a fresh copy of dt
# Frank's version
dt[, z := replace(y, which(is.na(y)), x)][]
x y z
1: 2014-01-01 <NA> 2014-01-01
2: 2015-01-01 2015-06-01 2015-06-01
3: 2016-01-01 <NA> 2015-01-01
Warning message:
In NextMethod(.Generic) :
number of items to replace is not a multiple of replacement length
第 3 行中 z 的值已从第 2 行中的 x 复制,这是错误的。相反,它应该是从第 3 行复制过来的。
这里发生了什么? replace(x, list, values) 上的帮助页面说
replace 将x 中的值替换为list 中给出的索引,这些索引由values 中给出。
在我们的示例中,list 获取行索引 1, 3,而 values 获取 2014-01-01, 2015-01-01, 2016-01-01。不同的长度是警告消息的原因。很明显,list 中的第二个索引(即第 3 行)被values 中的第二个值替换为2015-01-01。
replace() 的正确使用也需要子集x:
dt <- copy(dt_orig) # use a fresh copy of dt
dt[, z := replace(y, is.na(y), x[is.na(y)])][]
产生
x y z
1: 2014-01-01 <NA> 2014-01-01
2: 2015-01-01 2015-06-01 2015-06-01
3: 2016-01-01 <NA> 2016-01-01
没有任何警告。
为什么Date 失败并显示ifelse()
ifelse(test, yes, no) 上的帮助页面有一个很长的警告部分,开始
结果的模式可能取决于test[...]的值,结果的类属性[...]是
取自test,可能不适用于从
yes 和 no。
有时最好使用诸如
之类的结构
(tmp <- yes; tmp[!test] <- no[!test]; tmp)
将此建议应用于我们的示例
dt <- copy(dt_orig) # use a fresh copy of dt
dt[, z := {tmp <- x; tmp[!is.na(y)] <- y[!is.na(y)]; tmp}][]
我们确实得到了
x y z
1: 2014-01-01 <NA> 2014-01-01
2: 2015-01-01 2015-06-01 2015-06-01
3: 2016-01-01 <NA> 2016-01-01
数据
library(data.table) # version 1.11.4 used
dt_orig <-data.table(x = as.Date(c("2014-1-1", "2015-1-1", "2016-1-1")),
y = as.Date(c(NA, "2015-6-1", NA)))
基准测试
由于现在有 5 种不同的方法可用,我想知道最快的方法是什么。运行时间可能取决于行数,也取决于NA 值在y 中的份额。
因此,bench 包中的 press() 函数用于研究这两个参数对基准测试结果的影响。
bm <- bench::press(
n_rows = c(100, 1E4, 1E6),
na_share = c(0.1, 0.5, 0.9),
{
dt_bm <- data.table(x = as.Date("1970-01-01") + seq_len(n_rows),
y = as.Date("2970-01-01") + seq_len(n_rows))
set.seed(1L)
dt_bm[sample(seq_len(n_rows), na_share * n_rows), y := NA]
bench::mark(
ifelse = copy(dt_bm)[, z := as.Date(ifelse(is.na(y), x, y), origin="1970-01-01")][],
replace = copy(dt_bm)[, z := replace(y, is.na(y), x[is.na(y)])][],
tmp = copy(dt_bm)[, z := {tmp <- x; tmp[!is.na(y)] <- y[!is.na(y)]; tmp}][],
copy_y = copy(dt_bm)[, z := y][is.na(z), z := x][],
copy_x = copy(dt_bm)[, z := x][!is.na(y), z := y][]
)
}
)
library(ggplot2)
autoplot(bm) + theme_bw()