Лучшие практики для избежания округления 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
, но не включает десятичное время в качестве одного из кандидатов...
Ответы
Ответ 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 года. Я обнаружил, что дальнейшее происхождение сдвинулось с намеченной даты, тем более странным получились результаты (а не так, как это было легко объяснено височными днями). Поскольку происхождение является гибким, я решил настроить его прямо поверх целевых значений. Для високосных дней, секунд и любого другого времени странности для нас, на вашей собственной голове. =)