【问题标题】:Increase paste speed of two columns in data.table in R (reproducible)提高 R 中 data.table 中两列的粘贴速度(可重现)
【发布时间】:2018-12-02 12:52:52
【问题描述】:

我有这样的数据:

library(data.table)
NN = 10000000
set.seed(32040)
DT <- data.table(
  col = 1:10000000,
  timestamp = 1521872652 + sample(7000001, NN, replace = TRUE)
)

我正在尝试将唯一的年份和星期提取为代码,以便对重复项进行排序(真实数据表具有 userID 以及更多内容)。我有一个有效的当前解决方案(如下),但是在从日期列中唯一地粘贴几周和年份的部分上它很慢。使用anytime 包创建日期并从lubridate 中提取weekyear 仍然非常快。有人可以帮我加快速度吗?谢谢!

我的慢代码(有效,但我想加快速度)

library(anytime)
library(lubridate)
tz<-"Africa/Addis_Ababa"
DT$localtime<-  anytime(DT$timestamp, tz=tz) ###Lightning fast
DT$weekuni <- paste(year(DT$localtime),week(DT$localtime),sep="") ###super slow

我的测试表明是 paste 杀死了我:

非常快的anytime 转换到日期:

system.time(DT$localtime<-  anytime(DT$timestamp, tz=tz)) ###Lightning fast
       user  system elapsed 
      0.264   0.417   0.933 

lubridate 周和年从日期的快速转换,但慢 paste

> system.time(DT$weekuni1 <- week(DT$localtime)) ###super slow
   user  system elapsed 
  1.203   0.188   1.400 
> system.time(DT$weekuni2 <- year(DT$localtime))
   user  system elapsed 
  1.229   0.189   1.427 
> system.time(DT$weekuni <- paste0(DT$weekuni1,dt$weekuni2))
   user  system elapsed 
 14.652   0.344  15.483

【问题讨论】:

  • pkg:data.table 中已经有一个yearweek 函数。如果效率是一个问题,覆盖那些似乎是不明智的。并且函数拼写为paste,但这种用法与[paste]标签定义不一致。我原以为使用 data.table 中的 := 函数将是可行的方法,而不是使用 $.data.frame
  • 你的建议有点快,但仍然比我认为的要慢,因为时间列来自任何时间:&gt; system.time(dt &lt;- dt[,weekuni:=paste0(year(dt$localtime),week(dt$localtime)),]) user system elapsed 6.285 0.412 6.739
  • 好吧,您正在制作两个额外的向量,然后在附加它们之前将它们强制转换为字符。我不知道为什么这应该是闪电般的速度。
  • 您应该注意加载包时出现的屏蔽消息。
  • 润滑年/周函数可能会更快。我们依赖 POSIXlt 转换,这可能很慢。

标签: r performance time data.table


【解决方案1】:

我使用 format 而不是 paste 使您的代码运行速度提高了大约 50%。

首先,我不确定anytime 对您的用例有何意义,因为我们几乎可以立即将时间戳放入POSIXct 结构中:

DT[ , localtime := .POSIXct(timestamp, tz = tz)]

接下来,我在 ?strptime 上搜索了基于 ISO 周的格式化代码以获得:

DT[ , weekuni := format(localtime, format = '%G%V')]

我不能 100% 确定这将始终与 paste(year, week) 相同,但它是针对您的测试数据的;如果它们之间存在差异,您应该询问这对您是否真的很重要。

我能想到的唯一可能更快的是对时间戳本身使用整数运算。如果Africa/Addis_Ababa 时区在您的示例时间范围内没有对其 UTC 偏移量进行任何调整(不幸的是,看起来 Africa/Addis_Ababa 观察夏令时,因此 UTC 偏移量在 2 到 3 小时之间变化,这使得这更加容易整数算术方法要困难得多)


作为记录,使用 data.table::yeardata.table::week 的速度与此处使用的方法差不多,但是它使用的“年”和“周”定义与 lubridate 不同(默认情况下使用上面 %G%V 所做的 ISO 年/周)。

data.table 还没有isoyear 实现,并且data.table::isoweeklubridate::week 慢很多。

【讨论】:

    【解决方案2】:

    如果您愿意仅根据日期定义一年-一周,您可以获得快 20 倍的解决方案:

    library(data.table)
    NN = 10000000
    # NN = 1e4
    set.seed(32040)
    DT <- data.table(
      col = seq_len(NN),
      timestamp = 1521872652 + sample(7000001, NN, replace = TRUE)
    )
    DT1 <- copy(DT)
    
    DT2 <- copy(DT)
    tz <- "Africa/Addis_Ababa"
    
    old <- function(DT) {
      DT$localtime<-  anytime::anytime(DT$timestamp, tz=tz) ###Lightning fast
      DT$weekuni <- paste(lubridate::year(DT$localtime), lubridate::week(DT$localtime), sep="")
      DT[, timestamp := NULL]
      DT[, .(col, localtime, weekuni)]
    }
    
    new <- function(DT) {
      DT[ , localtime := anytime::anytime(timestamp, tz = tz)]
      DT[, Date := as.Date(localtime)]
      DT[, weekuni := paste0(lubridate::year(.BY[[1L]]), lubridate::week(.BY[[1L]])),
         keyby = "Date"]
      DT[, Date := NULL]
      # DT[, timestamp := NULL]
      DT[order(col), .(col, localtime, weekuni)]
    }
    
    bench::mark(old(DT1), new(DT2), check = FALSE, filter_gc = FALSE)
    #> # A tibble: 2 x 10
    #>   expression     min    mean median    max `itr/sec` mem_alloc  n_gc n_itr
    #>   <chr>      <bch:t> <bch:t> <bch:> <bch:>     <dbl> <bch:byt> <dbl> <int>
    #> 1 old(DT1)    22.39s  22.39s 22.39s 22.39s    0.0447    2.28GB     5     1
    #> 2 new(DT2)     1.13s   1.13s  1.13s  1.13s    0.888   878.12MB     1     1
    #> # ... with 1 more variable: total_time <bch:tm>
    

    reprex package (v0.2.0) 于 2018 年 6 月 23 日创建。

    即使你不这样做,你仍然可以通过每个日期只使用一次paste 来获得 10 倍的加速:

    library(data.table)
    NN = 1e7
    # NN = 1e4
    set.seed(32040)
    DT <- data.table(
      col = seq_len(NN),
      timestamp = 1521872652 + sample(7000001, NN, replace = TRUE)
    )
    DT1 <- copy(DT)
    
    DT2 <- copy(DT)
    DT3 <- copy(DT)
    tz <- "Africa/Addis_Ababa"
    
    old <- function(DT) {
      DT$localtime<-  anytime::anytime(DT$timestamp, tz=tz) ###Lightning fast
      DT$weekuni <- paste(lubridate::year(DT$localtime), lubridate::week(DT$localtime), sep="")
      DT[, timestamp := NULL]
      DT[, .(col, weekuni)]
    }
    
    new <- function(DT) {
      DT[ , Date := anytime::anydate(timestamp, tz = tz)]
      DT[, weekuni := paste0(lubridate::year(.BY[[1L]]), lubridate::week(.BY[[1L]])),
         keyby = "Date"]
      DT[, Date := NULL]
      # DT[, timestamp := NULL]
      setorderv(DT[, .(col, weekuni)], "col")
    }
    
    
    bench::mark(old(DT1), new(DT2), check = TRUE, filter_gc = FALSE)
    #> # A tibble: 2 x 10
    #>   expression     min    mean median    max `itr/sec` mem_alloc  n_gc n_itr
    #>   <chr>      <bch:t> <bch:t> <bch:> <bch:>     <dbl> <bch:byt> <dbl> <int>
    #> 1 old(DT1)     22.2s   22.2s  22.2s  22.2s    0.0450    2.21GB     4     1
    #> 2 new(DT2)      2.8s    2.8s   2.8s   2.8s    0.357     1.42GB     3     1
    #> # ... with 1 more variable: total_time <bch:tm>
    

    【讨论】:

    • 应该小心包含as.Date 的时间,因为as.Date.POSIXct 包含as.POSIXlt.POSIXct 步骤,我认为这是很多事情的瓶颈。 OTOH OP 可能还有其他理由保留 date 字段,在这种情况下,转换是沉没成本。
    猜你喜欢
    • 2013-08-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-27
    • 2019-10-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多