Подтвердить что ты не робот

Лучшие практики для избежания округления gotchas в манипуляции с датами

Я делаю манипуляции с датой и временем и испытываю объяснимые, но неприятные проблемы с круговым отключением при преобразовании даты → времени → даты. Я временно преодолел эту проблему путем округления в соответствующих точках, но мне интересно, есть ли лучшие практики для обработки данных, которые были бы более чистыми. Я использую сочетание функций base-R и lubridate.

tl; dr есть хороший, простой способ конвертировать из десятичной даты (YYYY.fff) в класс Date (и обратно) без прохождения POSIXt и выполнения округления ( и потенциально часовых поясах)

Начните с нескольких дней с 1918 года в качестве отдельных столбцов год/месяц/день (не критическая часть моей проблемы, но там, где мой конвейер запускается):

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

Преобразовать год/месяц/день → дата → время:

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 из моего конвейера и пребывание с датами...

Как было предложено этой статьей справочника R-справочника от 2004 года Гротендиком и Петцольдтом:

При рассмотрении того, какой класс использовать, всегда выберите наименее сложный класс, который будет поддерживать выражение. То есть используйте Date, если это возможно, в противном случае используйте chron и в противном случае используйте классы POSIX. Такая стратегия значительно снизит вероятность ошибки и повысит надежность вашего приложения.

В обширной таблице в этой статье показано, как перевести между Date, chron и POSIXct, но не включает десятичное время в качестве одного из кандидатов...

4b9b3361

Ответ 1

Кажется, было бы лучше избегать конвертации из десятичного времени, если это вообще возможно.

При конвертировании с даты на десятичную дату необходимо также учитывать время. Поскольку 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

Следует отметить, однако, что полученное таким образом значение десятичного времени будет различным.

Ответ 2

lubridate::decimal_date() возвращает numeric. Если я правильно вас понимаю, вопрос заключается в том, как преобразовать этот numeric в Date и сделать его круглым, без пересказки через POSIXct.

as.Date(1L, origin = '1970-01-01') показывает нам, что мы можем предоставить as.Date дни с определенного определенного источника и немедленно конвертироваться в тип 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/info/14449166/dates-with-lapply-and-sapply
  do.call(c, mapply(as.Date.numeric, x = (decimal_date-years) * 365, origin = origins, SIMPLIFY = FALSE))
}

Боковое замечание: Я признаю, что я спустился с кроличьей дырой, пытаясь переместить происхождение вокруг сделки с датой до 1970 года. Я обнаружил, что дальнейшее происхождение сдвинулось с намеченной даты, тем более странным получились результаты (а не так, как это было легко объяснено височными днями). Поскольку происхождение является гибким, я решил настроить его прямо поверх целевых значений. Для високосных дней, секунд и любого другого времени странности для нас, на вашей собственной голове. =)