R 中大规模时间序列操作的性能属性主要是 xts 和 data.table

问题描述

我正在开展一个包含大型时间序列数据集的新项目,相关计算将根据这些数据集输入 shiny 应用程序。因此,我对效率感兴趣。这些操作通常仅限于基本期间的转换和风险指标的后续汇总统计

我正在研究使用哪个库/方法来构建计算脚本。目前,我对 xtsdata.table 没问题。尽管我可以使用 quantmodTTR 库,但我对在生产中部署黑盒函数犹豫不决,并且更愿意保持完整的可追溯性。

到目前为止,我已经进行了以下基准测试,其中将 data.frame 的每日价格转换为每月回报。目前使用的包是 xtsdata.tablequantmod(作为参考)。代码粘贴在下面,但也可以在 GitHub 上找到。

基准代码

# Simple return exercise: Daily Prices to Monthly Returns
# Input: Nx2 data.frame with columns (N days,price) 
# Output: Mx2 object with columns (M months,return)
# Three different functions: 1. xts,2. data.table,3. quantmod

rm(list = ls()); gc()

library(data.table) 
library(zoo)
library(xts)
library(ggplot2)
library(quantmod)

# Asset params
spot = 100
r = 0.01
sigma = 0.02
N = 1e5

# Input data: Nx2 data.frame (date,price)
pmat = data.frame( 
    date = seq.Date(as.Date('1970-01-01'),by = 1,length.out = N),price = spot * exp(cumsum((r - 0.5 * sigma**2) * 1/N + (sigma * (sqrt(1/N)) * rnorm(N,mean = 0,sd = 1))))
)

# Output functions

      # 1. xts standalone 
      xtsfun = function(mat){
        xtsdf = as.xts(mat[,2],order.by = mat[,1])
        eom_prices = to.monthly(xtsdf)[,4]
        mret = eom_prices/lag.xts(eom_prices) - 1; mret[1] = eom_prices[1]/xtsdf[1] - 1
        mret
      }
      
      # 2. data.table standalone 
      dtfun = function(mat){
        dt = setNames(as.data.table(mat),c('V1','V2'))
        dt[,.(EOM = last(V2)),.(Month = as.yearmon(V1))][,.(Month,Return = EOM/shift(EOM,fill = first(mat[,2])) - 1)]
      }
      
      # 3. quantmod (black Box library)
      qmfun = function(mat){
        qmdf = as.xts(mat[,1])
        monthlyReturn(qmdf)
      }

# Check 1 == 2 == 3:
all.equal(
    unlist(dtfun(pmat[1:1000,])[,Return]),as.numeric(xtsfun(pmat[1:1000,])),as.numeric(qmfun(pmat[1:1000,scale = NULL
)
    
# Benchmark
library(microbenchmark)
gc()

mbm = microbenchmark(
  xts = xtsfun(pmat),data.table = dtfun(pmat),quantmod = qmfun(pmat),times = 50
)

mbm

结果

对于 N = 1e5,三种方法的表现相似:

Unit: milliseconds
       expr      min       lq     mean   median       uq       max neval
        xts 20.62520 22.93372 25.14445 23.84235 27.25468  39.29402    50
 data.table 21.23984 22.29121 27.28266 24.05491 26.25416  98.35812    50
   quantmod 14.21228 16.71663 19.54709 17.19368 19.38106 102.56189    50

然而,对于 N = 1e6,我观察到 data.table 的显着性能差异:

Unit: milliseconds
       expr       min        lq      mean    median        uq       max neval
        xts  296.8969  380.7494  408.7696  397.4292  431.1306  759.7227    50
 data.table 1562.3613 1637.8787 1669.8513 1651.4729 1688.2312 1969.4942    50
   quantmod  144.1901  244.2427  278.7676  268.4302  331.4777  418.7951    50

我很好奇是什么导致了这个结果,特别是因为 data.table 通常在大 N 上表现出色。当然,dtfun 可能只是写得不好(我非常感谢任何代码改进),但我使用其他方法获得了类似的结果,包括 EOM 日期的自联接和每日返回的 cumprod

xts 和/或 quantmod 是否受益于任何可大规模提高其性能的内部 rcpp 或 eqv 调用?最后,如果您知道用于大规模 TS 的任何其他具有竞争力的独立解决方案(base?、dplyr?),我都听得一清二楚。

解决方法

答案在于 themesdata.table 处理。本质上,它采用相对较慢的 date 格式。当改用基于整数的 ISOdate 分组时,结果偏向于 date

我已使用 data.tablexts 的更新解决方案更新了 TSBenchmark 存储库。我非常感谢 Joshua UlrichMatt Dowle 提供的改进,他们值得全部赞扬。