从数字 YYYYMMDD 到日期再回到数字 YYYYMMDD 的最快方法

Fastest way from numeric YYYYMMDD to date and back to numeric YYYYMMDD

我需要优化的是以下过程:从yyyymmdd形式的数值开始,将其转换为日期,加上3个月(不是90天,月日必须保持不变),然后将其转换回 yyyymmdd 格式的数字。以下代码实现了这一点:

library(lubridate)
a = 20180223
b = as.numeric(gsub("-", "", 
                    x = as.character(ymd(a) %m+% months(3)),
                    fixed = TRUE))
b
20180523

但是,在我看来,将其应用于大型 data.table 时运行速度太慢。下面是在我的机器上运行 13 秒的 100 万行代码。有什么办法可以优化这个吗?我在想也许我不需要转换为日期,但我无法理解它。

library(data.table)
library(lubridate)

DT = CJ(rep(2016:2018, 1000), 1:12, 1:28)
DT[, StartDate := V1*10000 + V2*100 + V3]

system.time({
  DT[, StartDate_3M :=  as.numeric(gsub("-", "", 
                             x = as.character(ymd(StartDate) %m+% months(3)),
                             fixed = TRUE))]
}) 
   user  system elapsed 
  13.03    0.04   13.18 

这个答案是集体努力的结果,给出了很多宝贵的意见。

library(data.table)
library(lubridate)
library(microbenchmark)

DT = CJ(rep(2016:2018, 100), 1:12, 1:28)
DT[, StartDate := V1*10000 + V2*100 + V3]

我缩小了数据集,以便在我的机器上更快地完成比较。

Pre-compute期间:

diff_3m <- months(3)

这个建议的代码可以准确地满足您的需求,注意像 20180131 这样的日期不会超过一个月的月底,因此您最终会得到 20180430 因为使用了来自 lubridate.

%m+% 运算符
DT[, StartDate_3M_new := as.numeric(format(ymd(StartDate) %m+% diff_3m, "%Y%m%d"))]

我机器上的时间:

microbenchmark(
  orig = DT[, StartDate_3M_orig :=  as.numeric(gsub("-", "", 
                                               x = as.character(ymd(StartDate) %m+% months(3)),
                                               fixed = TRUE))],
  new = DT[, StartDate_3M_new := as.numeric(format(ymd(StartDate) %m+% diff_3m, "%Y%m%d"))], times=10)


Unit: milliseconds
 expr      min       lq     mean   median       uq      max neval
 orig 713.2652 734.5133 798.1475 749.9207 883.2163 912.6070    10
  new 458.8666 483.0388 523.6454 502.4709 518.7074 665.7304    10

当然我不知道这是否是 "fastest possible" 但我认为 re-implement 所有用于更快计算的小日期技巧所花费的时间可能会超过运行 这段代码所花费的时间。

编辑添加:这是@BogdanC 算术答案的清理版本(即不抛出警告)。免费带闰年!性能相似。

add_months_2 <- function(dt, n_months, month_days) {
  dt[, year := StartDate %/% 10000][
    , month := (StartDate - year * 10000) %/% 100][
    , day := StartDate %% 100][
    , new_month := c(1:12, 1:3)[month + n_months]][
    , leap_year := (!(year %% 4) & (year %% 100)) | !(year %% 400)][
    , max_d := (month_days + leap_year * c(0, 1, rep(0, 10)))[new_month]][  
    , StartDate_PlusM := year * 10000 + new_month * 100 + pmin(day, max_d)]
  dt
}

我继续尝试算术实现。不是我能写的最干净的,但它是一个很好的起点。我用于添加一个月的逻辑与 ymd() %m+% months(3) 相同,但明显的例外是不处理闰年(这也可以完成,但我的业务逻辑不需要它)。
下面的完整代码和基准测试 - 它比我最初的想法快 96%,比团队努力快 94%(我仍然非常重视)。

library(data.table)
library(lubridate)
library(microbenchmark)

DT = CJ(rep(2016:2018, 1000), 1:12, 1:28)
DT[, StartDate := V1*10000 + V2*100 + V3]

diff_3m <- months(3)

# arithmetic implementation
month_max_days <- c(31,28,31,30,31,30,31,31,30,31,30,31)

add_months <- function(dt, n_months, month_days) {
  dt[, year := StartDate %/% 10000]
  dt[, month := (StartDate - year * 10000) %/% 100]
  dt[, day := StartDate %% 100]
  dt[month + n_months <= 12, new_month := month + n_months]
  dt[month + n_months > 12, new_month := (month + n_months) ]
  dt[month + n_months > 12, new_month := new_month %% 12 ]
  dt[month + n_months <= 12, StartDate_3M := year * 10000 + new_month * 100 + pmin(day,month_days[new_month])]
  dt[month + n_months > 12, StartDate_3M := (year + (month + n_months) %/% 12) * 10000 + new_month * 100 + pmin(day,month_days[new_month])]
  dt
}

microbenchmark(
  orig = DT[, StartDate_3M_orig :=  as.numeric(gsub("-", "", 
                                                    x = as.character(ymd(StartDate) %m+% months(3)),
                                                    fixed = TRUE))],
  new = DT[, StartDate_3M_new := as.numeric(format(ymd(StartDate) %m+% diff_3m, "%Y%m%d"))],
  new_v2 = add_months(DT,3,month_max_days),
  times=10)
Unit: milliseconds
   expr        min         lq       mean     median        uq       max neval
   orig 11445.7826 12749.9895 13260.7266 13021.3065 13952.189 15247.221    10
    new  7953.0839  8643.7795 10004.3372  9484.5933 11104.017 13002.025    10
 new_v2   215.5608   309.2308   570.2348   408.0091   761.361  1196.798    10