提高data.table的日期+时间拼接性能?

7

我不确定是否可以在这里提问,请告诉我是否应该在其他地方提问。

我有一个数据表,有1e6行,具有以下结构:

        V1       V2     V3
1: 03/09/2011 08:05:40 1145.0
2: 03/09/2011 08:06:01 1207.3
3: 03/09/2011 08:06:17 1198.8
4: 03/09/2011 08:06:20 1158.4
5: 03/09/2011 08:06:40 1112.2
6: 03/09/2011 08:06:59 1199.3

我正在使用以下代码将V1和V2变量转换为唯一的日期时间变量:

 system.time(DT[,`:=`(index= as.POSIXct(paste(V1,V2),
                         format='%d/%m/%Y %H:%M:%S'),
                     V1=NULL,V2=NULL)])

   user  system elapsed 
   47.47    0.16   50.27 

有没有什么方法可以提高这个转换的性能?

这里是 dput(head(DT)) 的输出:

DT <- structure(list(V1 = c("03/09/2011", "03/09/2011", "03/09/2011", 
"03/09/2011", "03/09/2011", "03/09/2011"), V2 = c("08:05:40", 
"08:06:01", "08:06:17", "08:06:20", "08:06:40", "08:06:59"), 
    V3 = c(1145, 1207.3, 1198.8, 1158.4, 1112.2, 1199.3)), .Names = c("V1", 
"V2", "V3"), class = c("data.table", "data.frame"), row.names = c(NA, 
-6L), .internal.selfref = <pointer: 0x00000000002a0788>)

2
如果您的日期是GMT并且大于1970年1月1日,您可以尝试使用“fasttime”包。 - Alex Popov
@aseidlitz 谢谢。我忘记了 fasttime。我认为它是一个很好的选择。但它在 data.table 包内部没有使用吗? - agstudy
有关在data.table中添加fasttime支持的讨论已经进行了一些,但据我所见,在R Forge上仍然是一个未解决的请求。 - Alex Popov
2个回答

6
这种方法比OP的方法快约40倍,它使用了查找表并利用了非常快速的数据表连接。此外,它利用了这样一个事实:虽然可能有1e6个日期和时间组合,但最多只能有86400个唯一时间,甚至可能还有更少的日期。最后,它完全避免了使用paste(...)
library(data.table)
library(stringr)

# create a dataset with 1MM rows
set.seed(1)
x  <- 1000*sample(1:1e5,1e6,replace=T)
dt <- data.table(id=1:1e6,
                 V1=format(as.POSIXct(x,origin="2011-01-01"),"%d/%m/%Y"),
                 V2=format(as.POSIXct(x,origin="2011-01-01"),"%H:%M:%S"),
                 V3=x)
DT <- dt

index.date <- function(dt) {
  # Edit: this change processes only times from the dataset; slightly more efficient
  V2 <- unique(dt$V2)
  dt.time <- data.table(char.time=V2,
                        int.time=as.integer(substr(V2,7,8))+
                          60*(as.integer(substr(V2,4,5))+
                                60*as.integer(substr(V2,1,2))))
  setkey(dt.time,char.time)
  # all dates from dataset
  dt.date <- data.table(char.date=unique(dt$V1), int.date=as.integer(as.POSIXct(unique(dt$V1),format="%d/%m/%Y")))
  setkey(dt.date,char.date)
  # join the dates
  setkey(dt,V1)
  dt <- dt[dt.date]
  # join the times
  setkey(dt,V2)
  dt <- dt[dt.time, nomatch=0]
  # numerical index
  dt[,int.index:=int.date+int.time]
  # POSIX date index
  dt[,index:=as.POSIXct(int.index,origin='1970-01-01')]
  # get back original order
  setkey(dt,id)
  return(dt)
}
# new approach
system.time(dt<-index.date(dt))
#   user  system elapsed 
#   2.26    0.00    2.26 


# original approach
DT <- dt
system.time(DT[,`:=`(index= as.POSIXct(paste(V1,V2),
                                       format='%d/%m/%Y %H:%M:%S'),
                     V1=NULL,V2=NULL)])
#   user  system elapsed 
#  84.33    0.06   84.52 

请注意,性能取决于有多少个唯一日期。在测试案例中,大约有1200个唯一日期。 编辑:建议使用更多的data.table语法糖编写函数并避免使用"$"进行子集提取。
index.date <- function(dt,fmt="%d/%m/%Y") {
    dt.time <- data.table(char.time = dt[,unique(V2)],key='char.time')
    dt.time[,int.time :=as.integer(substr(char.time,7,8))+
                                            60*(as.integer(substr(char.time,4,5))+
                                                        60*as.integer(substr(char.time,1,2)))]
    # all dates from dataset
    dt.date <- data.table(char.date = dt[,unique(V1)],key='char.date')
    dt.date[,int.date:=as.integer(as.POSIXct(char.date,format=fmt))]
    # join the dates
    setkey(dt,V1)
    dt <- dt[dt.date]
    # join the times
    setkey(dt,V2)
    dt <- dt[dt.time, nomatch=0]
    # numerical index
    dt[,int.index:=int.date+int.time]
    # POSIX date index
    dt[,index:=as.POSIXct.numeric(int.index,origin='1970-01-01')]
    # remove extra/temporary variables
    dt[,`:=`(int.index=NULL,int.date=NULL,int.time=NULL)]
}

对数据集进行微小更改,仅处理唯一时间;效率略有提高(从之前的3.9秒降至约2.2秒)。 - jlhoward
谢谢。使用data.table join是一个非常好的主意。我稍微修改了您的解决方案,以便更好地使用data.table语法糖。 - agstudy
@agstudy:非常干净,谢谢。但是为什么最后一条语句可以恢复原始顺序? - jlhoward
这是一种类型(我留下了旧评论)。我进行了更改。我建议我们将答案因式分解,以给出一个简短的答案。你觉得呢? - agstudy
当您使用 := 时,它会通过引用进行赋值。您不必再次分配它(使用 <-)。例如:dt.date <- dt.date[,int.date:=as.integer(as.POSIXct(char.date,format=fmt))] 可以简写为:dt.date[,int.date:=as.integer(as.POSIXct(char.date,format=fmt))] - Arun
1
这个是否考虑了闰秒? - Joshua Ulrich

2
如果您的数据中有许多重复的时间戳,您可以尝试添加,by=list(V1, V2),但必须有足够的重复来支付分割的成本。
瓶颈在于粘贴和转换,因此我认为答案是否定的。(除非您使用另一种转换为POSIX的方法)

谢谢。我忘了提到,我不应该有重复的日期(这基本上是时间序列数据)。 - agstudy
5
此外,瓶颈实际上在于转换,这需要的时间大约比粘贴步骤长40倍。如果您的日期采用真正的POSIX格式(即“1970-01-01”而不是“01/01/1970”),则可以使用Simon Urbanek的fasttime库来大幅加快速度,如此处所述 - Josh O'Brien
1
@JoshO'Brien 您关于fasttime的说法是正确的。例如,使用类似于DT [,fastPOSIXct(paste(as.Date(V1,format =“%d /%m /%Y”),V2))] 的东西可以将性能提高至少4倍。 - agstudy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接