避免日期操作中的舍入陷阱的最佳实践

时间:2017-11-21 21:09:24

标签: r date datetime lubridate

我正在进行一些日期/时间操作,并在转换日期时遇到可解释但令人不愉快的往返问题 - >时间 - >约会。我已经通过在适当的点进行四舍五入来暂时克服这个问题,但我想知道是否有更好的日期处理方法会更清晰。我正在使用base-R和lubridate函数的混合。

tl; dr 是一种很好的,简单的方法,可以从十进制日期(YYYY.fff)转换为Date类(并返回),而无需通过POSIXt并产生回合 - 关闭(和潜在的时区)并发症??

从1918年的几天开始,作为单独的年/月/日列(不是我的问题的关键部分,但它是我的管道恰好开始的地方):

library(lubridate)
dd <- data.frame(year=1918,month=9,day=1:12)

转换年/月/日 - &gt;日期 - &gt;时间:

dd <- transform(dd,
                time=decimal_date(make_date(year, month, day)))

由于结果,结果时间向量的连续差异不完全是1:这是可以理解的,但会导致问题。

table(diff(dd$time)*365)
## 0.999999999985448  1.00000000006844 
##                 9                 2 

现在假设我转换回日期:日期在午夜之前或之后(在任何一个方向关闭<1秒):

d2 <- lubridate::date_decimal(dd$time)
#  [1] "1918-09-01 00:00:00 UTC" "1918-09-02 00:00:00 UTC"
#  [3] "1918-09-03 00:00:00 UTC" "1918-09-03 23:59:59 UTC"
#  [5] "1918-09-04 23:59:59 UTC" "1918-09-05 23:59:59 UTC"
#  [7] "1918-09-07 00:00:00 UTC" "1918-09-08 00:00:00 UTC"
#  [9] "1918-09-09 00:00:00 UTC" "1918-09-09 23:59:59 UTC"
# [11] "1918-09-10 23:59:59 UTC" "1918-09-12 00:00:00 UTC"

如果我现在想要日期(而不是POSIXct对象)我可以使用as.Date(),但令我沮丧的是 as.Date()截断而不是舍入 ......

tt <- as.Date(d2)
## [1] "1918-09-01" "1918-09-02" "1918-09-03" "1918-09-03" "1918-09-04"
## [6] "1918-09-05" "1918-09-07" "1918-09-08" "1918-09-09" "1918-09-09"
##[11] "1918-09-10" "1918-09-12"

所以差异现在是0/1/2天:

table(diff(tt))
# 0 1 2 
# 2 7 2 

我可以先解决这个问题来解决这个问题:

table(diff(as.Date(round(d2))))
## 1 
## 11

但我想知道是否有更好的方法(例如将POSIXct保留在我的管道中并保持日期......

根据this R-help desk article from 2004由Grothendieck和Petzoldt的建议:

  

在考虑使用哪个班级时,总是如此   选择最不复杂的类来支持   应用。也就是说,如果可能的话,使用Date,否则使用   chron并以其他方式使用POSIX类。这样的策略将大大减少出错的可能性并提高应用程序的可靠性。

本文中的扩展表显示了如何在DatechronPOSIXct之间进行翻译,但不包括小数时间作为候选人之一...

2 个答案:

答案 0 :(得分:6)

如果可能的话,似乎最好避免从十进制时间转换回来。

从日期转换为十进制日期时,还需要考虑时间。由于Date没有与之关联的特定时间,因此decimal_date固有地认为它是00:00:00

但是,如果我们只关心日期(而不是时间),我们可以假设有时间。可以说,当天中午(12:00:00)与当天的开始(00:00:00)一样好。这将使转换回Date更加可靠,因为我们不在午夜标记,几秒钟关闭不会影响输出。其中一种方法是将12*60*60/(365*24*60*60)添加到dd$time

dd$time2 = dd$time + 12*60*60/(365*24*60*60)
data.frame(dd[1:3],
           "00:00:00" = as.Date(date_decimal(dd$time)),
           "12:00:00" = as.Date(date_decimal(dd$time2)),
           check.names = FALSE)
#   year month day        00:00:00        12:00:00
#1  1918     9   1      1918-09-01      1918-09-01
#2  1918     9   2      1918-09-02      1918-09-02
#3  1918     9   3      1918-09-03      1918-09-03
#4  1918     9   4      1918-09-03      1918-09-04
#5  1918     9   5      1918-09-04      1918-09-05
#6  1918     9   6      1918-09-05      1918-09-06
#7  1918     9   7      1918-09-07      1918-09-07
#8  1918     9   8      1918-09-08      1918-09-08
#9  1918     9   9      1918-09-09      1918-09-09
#10 1918     9  10      1918-09-09      1918-09-10
#11 1918     9  11      1918-09-10      1918-09-11
#12 1918     9  12      1918-09-12      1918-09-12
     

但应注意,以这种方式获得的小数时间值会有所不同。

答案 1 :(得分:3)

lubridate::decimal_date()正在返回numeric。如果我理解正确,那么问题是如何将numeric转换为Date,并在不通过POSIXct的情况下适当地进行调整。

as.Date(1L, origin = '1970-01-01')告诉我们,我们可以为as.Date提供自某些指定来源以来的天数,并立即转换为日期类型。知道这一点,我们可以完全跳过年份部分并将其设置为原点。然后我们可以将小数日期转换为天数:

as.Date((dd$time-trunc(dd$time)) * 365, origin = "1918-01-01")

所以,像这样的函数可能会起作用(至少在没有闰日的情况下数年):

date_decimal2 <- function(decimal_date) {
  years <- trunc(decimal_date)
  origins <- paste0(years, "-01-01")
  # c.f. https://stackoverflow.com/questions/14449166/dates-with-lapply-and-sapply
  do.call(c, mapply(as.Date.numeric, x = (decimal_date-years) * 365, origin = origins, SIMPLIFY = FALSE))
}

旁注:我承认自己因为试图在1970年前的日期处理起源而试图移动一个兔子洞。我发现进一步的起源偏离了目标日期,结果越奇怪(而不是以闰日似乎很容易解释的方式)。由于原点很灵活,我决定将其定位在目标值之上。对于闰日,秒,以及其他任何奇怪的时间,我们都可以自己动手。 =)