Skip to content

Comparative underperformance (vs quantmod & xts) for large-N time-series exercise. #4870

Description

@griipen

I built a time-series benchmark of a simple period return calculation (daily prices to monthly returns) across common libraries: https://github.com/griipen/TSBenchmark/

Curiously, data.table's competitiveness does not appear to hold at scale in this particular case; it underperforms dramatically vis-a-vis xts and quantmod for N>1e5. As I understand following a chat with the authors, it may be related to data.table's internal date handling.

From my issue at griipen/TSBenchmark#1:

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:

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

Extended results, supporting figures and full code are found in my master branch. Below are the functions for xts, quantmod and data.table for a quick reference:

      # 1. xts standalone function
      xtsfun = function(xtsdf){
        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 function
      dtfun = function(dt){
        dt[, .(EOM = last(V2)), .(Month = as.yearmon(V1))][, .(Month, Return = EOM/shift(EOM, fill = dt[, first(V2)]) - 1)]
      }
      ...
      # 5. quantmod (black box library)
      qmfun = function(xtsdf){
        monthlyReturn(xtsdf)
      }

Activity

  1. jangorecki commented on Jan 10, 2021

    @jangorecki
    Member

    Thank you for reporting. For completeness I am pasting related code below.

    library(data.table)
    N = 1e6
    spot = 100
    r = 0.01
    sigma = 0.02
    
    dt = data.table( 
      V1 = seq.Date(as.Date('1970-01-01'), by = 1, length.out = N),
      V2 = spot * exp(cumsum((r - 0.5 * sigma**2) * 1/N + (sigma * (sqrt(1/N)) * rnorm(N, mean = 0, sd = 1))))
    )
    library(zoo)
    system.time(dt[, .(EOM = last(V2)), .(Month = as.yearmon(V1))][, .(Month, Return = EOM/shift(EOM, fill = dt[, first(V2)]) - 1)])
    #   user  system elapsed 
    #  2.196   0.000   2.080 
    system.time(as.yearmon(dt$V1))
    #   user  system elapsed 
    #  2.160   0.001   2.159 

    From the timings above you can see that it is not really data.table operations takes so much time but zoo::as.yearmon. I don't think we provide high performance alternative to this zoo function. There is a pending PR #4868 but it is not yet the implementation that will address this gap because it uses POSIXlt rather than operating on numerics.

  2. ben-schwen commented on Jan 12, 2021

    @ben-schwen
    Member

    Considering benchmarks an integer solutions for Dates looks favorable (basic idea in R below). However, I do not get why @mattdowle gsub work around (from here) is so slow (maybe a Windows thing?).

    convert_date = function(x, type = "year"){
      x = as.integer(x)
      days = x - 11017
    
      years400_days = 146097
      years100_days = 36524
      years4_days = 1461
    
      years400 = days %/% years400_days
      days = days %% years400_days
    
      years100 = days %/% years100_days
      days = days %% years100_days
    
      years4 = days %/% years4_days
      days = days %% years4_days
    
      years = days %/% 365
      days = days %% 365
    
      year = 2000 + years + 4*years4 + 100*years100 + 400*years400
      year = year + (days > 305)
    
      if (type == "year"){
        return(year)
      }
      else if (type == "month"){
        days_mon = data.table(
          x = c(30, 60, 91, 121, 152, 183, 213, 244, 274, 305, 336, 365) ,
          mon = c(3:12, 1:2)
        )
        dt = data.table(days = days)
        month = days_mon[dt, on=.(x = days), roll=-Inf][["mon"]]
        return(month)
      }
    }
    
    N = 1e6
    spot = 100
    r = 0.01
    sigma = 0.02
    
    dt = data.table( 
      V1 = seq.Date(as.Date('1970-01-01'), by = 1, length.out = N),
      V2 = spot * exp(cumsum((r - 0.5 * sigma**2) * 1/N + (sigma * (sqrt(1/N)) * rnorm(N, mean = 0, sd = 1))))
    )
    
    library(zoo)
    system.time(dt[, .(EOM = last(V2)), .(Month = as.yearmon(V1))])
    #   user  system elapsed                                                
    #   1.86    0.05    1.90  
    
    system.time(dt[, .(EOM = last(V2)), .(Month = convert_date(V1,"month"), Year = convert_date(V1, "year"))])
    #   user  system elapsed                                                
    #   0.42    0.03    0.44 
    
    system.time(dt[, .(as.integer(gsub('-', '', V1)))][, .(as.integer(V1/100))])
    #   user  system elapsed                                                
    #   8.20    0.11    8.31 
    
    sessionInfo()
    # R version 4.0.2 (2020-06-22)                                           
    # Platform: x86_64-w64-mingw32/x64 (64-bit)                              
    # Running under: Windows 10 x64 (build 17763)                            
                                                                           
    # Matrix products: default                                               
                        
    # attached base packages:                                                
    # [1] stats     graphics  grDevices utils     datasets  methods   base                                                                          
                                                                           
    # other attached packages:                                               
    # [1] zoo_1.8-8         data.table_1.13.1                                
                                                                           
    # loaded via a namespace (and not attached):                             
    # [1] compiler_4.0.2  grid_4.0.2      lattice_0.20-41 
  3. jangorecki commented on Jan 13, 2021

    @jangorecki
    Member

    My guess is gsub is slow because it needs to populate global string cache with new strings that are going to be used only for as.integer.

  4. ben-schwen commented on Dec 21, 2025

    @ben-schwen
    Member

    Closing this, since it is fixed since #5300

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions