Strange behavior involving dates - "origin must be supplied"

Ben*_*Ben 1 r date data.table

I have a data.table like so

dt <- data.table(x=as.Date(c("2014-1-1", "2015-1-1", "2016-1-1")), y=as.Date(c(NA, "2015-6-1", NA)))
dt
            x          y
1: 2014-01-01       <NA>
2: 2015-01-01 2015-06-01
3: 2016-01-01       <NA>
Run Code Online (Sandbox Code Playgroud)

I want to add a column z which is equal to y where y is not NA, and x otherwise.

dt[, z:=ifelse(is.na(y), x, y)]
dt
            x          y     z
1: 2014-01-01       <NA> 16071
2: 2015-01-01 2015-06-01 16587
3: 2016-01-01       <NA> 16801
Run Code Online (Sandbox Code Playgroud)

But for some reason the above statement casts z to numeric. If I try to convert it to a date with as.Date I get an error

dt[, z:=as.Date(ifelse(is.na(y), x, y))]
Error in as.Date.numeric(ifelse(is.na(y), x, y)) : 'origin' must be supplied
Run Code Online (Sandbox Code Playgroud)

What gives and how do I accomplish what I'm trying to do?

Uwe*_*Uwe 7

这个老问题现在已经被浏览了一万多次。

虽然它有一个公认的答案,但我觉得这个问题值得

  • 一个真正的data.table解决方案,
  • 解释为什么Date失败ifelse()
  • 为什么该replace()方法返回错误的结果。

data.table 方法

使用data.table,ifelse()replace()可以写成两个链式赋值操作,其中第二个使用子集:

dt[, z := y][is.na(z), z := x][]
Run Code Online (Sandbox Code Playgroud)
            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
Run Code Online (Sandbox Code Playgroud)

第一个赋值操作z通过复制列来创建一个新y列。第二分配操作修改z 代替通过复制的内容x只有那些行,其中zNA

或者,我们可以复制xfirst 并z用非NA y值替换值:

dt <- copy(dt_orig)   # use a fresh copy of dt
dt[, z := x][!is.na(y), z := y][]
Run Code Online (Sandbox Code Playgroud)

如果 中有很多NA值,后者可能更有效y

replace()方法中的错误

弗兰克建议使用C8H10N4O2 在编辑他的答案时使用它replace()而不是ifelse()它。不幸的是,这两个代码不仅会生成警告,而且只会返回错误的结果

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)][]
Run Code Online (Sandbox Code Playgroud)
            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
Run Code Online (Sandbox Code Playgroud)

z3 行中的值已从x第 2 行中复制,这是错误的。相反,它应该是从第 3 行复制的。

这里发生了什么?帮助页面上replace(x, list, values)

replace在替换值x与给定的指标list在给出的values

在我们的示例中,list获取行索引1, 3values获取2014-01-01, 2015-01-01, 2016-01-01。不同的长度是警告消息的原因。并且很明显的是,第二索引在list该第3行是通过在第二值替换values2015-01-01

正确使用replace()requires 子集x

dt <- copy(dt_orig)   # use a fresh copy of dt
dt[, z := replace(y, is.na(y), x[is.na(y)])][]
Run Code Online (Sandbox Code Playgroud)

这产生

            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
Run Code Online (Sandbox Code Playgroud)

没有任何警告。

为什么Date失败ifelse()

帮助页面上ifelse(test, yes, no)有一个很长的警告部分,它开始

结果的模式可能取决于test[...]的值,并且结果的类属性 [...] 取自test和 ,可能不适用于从yes和 中选择的值 no

有时最好使用诸如

(tmp <- yes; tmp[!test] <- no[!test]; tmp)
Run Code Online (Sandbox Code Playgroud)

将此建议应用于我们的示例

dt <- copy(dt_orig)   # use a fresh copy of dt
dt[, z := {tmp <- x; tmp[!is.na(y)] <- y[!is.na(y)]; tmp}][]
Run Code Online (Sandbox Code Playgroud)

我们确实得到

            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
Run Code Online (Sandbox Code Playgroud)

数据

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)))
Run Code Online (Sandbox Code Playgroud)

基准

由于现在有 5 种不同的方法可用,我想知道最快的方法是什么。运行时间可能取决于行数,也取决于 中NA值的份额y

因此,包中的press()函数bench用于研究这两个参数对基准测试结果的影响。

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()
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明