Indexing sequence to use for addressing an element

2019-08-12 01:00发布

问题:

There a several ways to access a specific element in a data frame, using various combinations of brackets ([ ]), and dollar signs ($). In time-sensitive functions, which one to use can be important?

Benchmarking some of the possible combinations:

library(microbenchmark)
df <- data.frame(a=1:6,b=1:6,c=1:6,d=1:6,e=1:6,f=1:6)
microbenchmark(df$c[3],
               df[3,]$c,
               df[3,3],
               df[3,][3],
               df[3,][[3]],
               df[,3][3],
               times=1e3)

yields these timings:

Unit: microseconds
         expr    min       lq      mean   median       uq      max neval
      df$c[3]  9.836  11.4505  14.03068  12.2015  12.9280 1252.854  1000
    df[3, ]$c 77.204  89.5750 100.18752  92.2445  98.6395 1351.521  1000
     df[3, 3] 15.719  18.9850  21.04074  19.6010  20.7400   82.519  1000
   df[3, ][3] 88.599 100.5920 110.59009 104.0415 110.5435  409.050  1000
 df[3, ][[3]] 75.856  87.2200  98.67104  89.9360  96.1695 1391.299  1000
   df[, 3][3] 11.639  13.4225  14.77493  13.9510  14.6905   55.172  1000

Where we see that df$c[3] is fastest, closely followed by df[,3][3]. Others are much slower.

In time sensitive appplications, I often use data tables rather than frames, because sorting and subsetting operations are typically much faster. However, addressing operations can be much slower, as we see if we repeat the above for a data.table:

library(data.table)
dt <- as.data.table(df)
microbenchmark(dt$c[3],
               dt[3,]$c,
               dt[3,3],
               dt[3,][[3]],
               times=1e3)
Unit: microseconds
         expr     min       lq      mean   median       uq      max neval
      dt$c[3]   9.503  11.4020  14.90066  12.6820  13.8950 1336.407  1000
    dt[3, ]$c 417.756 437.0495 480.26532 448.8625 463.6350 2909.038  1000
     dt[3, 3] 205.115 218.9590 238.78000 227.9575 239.1265 1554.503  1000
 dt[3, ][[3]] 414.378 435.2115 470.76853 447.1505 461.3310 1906.432  1000

My question is this: Is $[ ] guaranteed to always be the fastest addressing method, or can this depend on factors such as the types of data in the data frame (or table), the platform (OS), or the build version? If anyone can explain the reasons underlying differences in timing, and/or the pros/cons of various approaches, that would be also useful.

UPDATE

Following the suggestions in the answer from 42- the test is repeated here using more rows and with the additional syntax options from both 42- and also in the comment by A.Webb who suggested df[[3,3]] as the fastest. (note: I also tried the same test but accessing higher row numbers, but timing seems to be independent of which row is selected).

df <- data.frame(a=1:1000,b=1:1000,c=1:1000,d=1:1000,e=1:1000,f=1:1000)

Unit: microseconds
         expr    min      lq       mean  median       uq      max neval
      df$c[3]  8.314  9.7610  12.870667 10.6260  12.0950 1250.339  1000
 df[["c"]][3]  6.932  8.0670   9.652672  8.7075   9.9445   26.512  1000
  (df[3, ])$c 72.395 77.2390  90.893724 79.8320  95.8540  256.082  1000
     df[3, 3] 14.871 16.2625  19.377482 17.1180  20.1720   47.720  1000
   df[3, ][3] 82.446 86.7680 102.462603 89.9660 107.7965  232.685  1000
 df[3, ][[3]] 70.559 75.2140  93.581394 78.3385  93.4235 1507.933  1000
   df[, 3][3]  9.933 11.4770  13.430309 12.1090  14.0900   38.213  1000
   df[[3, 3]]  6.465  7.8355   9.236773  8.4500   9.6355   29.833  1000

So it looks like df[[i,j]] is fastest, followed extremely closely by df[["colname"]][j]. Which of these to use would probably depend on whether you need to use column names or numbers.

The question is still open if we can assume that this is always the case on all platforms and for all data types.

回答1:

As stated in my comments, df$c[3] is actually parsed to '[['(df, 'c')[3], so it's not surprising that skipping the parsing process results in faster execution. The data.table comparisons are mostly non-equivalent except when using $ which is not really a data.table function..

Unit: microseconds
         expr     min       lq      mean   median       uq      max neval   cld
      df$c[3]  16.035  16.8245  17.63600  17.3090  17.9400   31.158  1000 ab   
 df[["c"]][3]  13.008  13.9090  14.60883  14.2775  14.8355  121.634  1000 a    
  (df[3, ])$c 137.376 140.4895 143.57778 141.6055 143.8310  175.180  1000    d 
     df[3, 3]  29.316  30.5715  31.25617  30.9040  31.3165   49.764  1000   c  
   df[3, ][3] 156.524 159.4180 167.99243 160.3910 162.3120 2636.693  1000     e
 df[3, ][[3]] 134.975 137.3945 142.92265 138.3810 140.2370 2675.090  1000    d 
   df[, 3][3]  20.108  21.2860  21.94357  21.5810  21.8640   59.057  1000  b   

I admit to being surprised at the fact that the code I wrote: '[['(df, 'c')[3], was unparsed as df[["c"]][3] and rather puzzled by some of the results, but the general rule is selecting columns first followed by positions in the resulting vector is generally much faster.

Also: this needs to be tested with larger objects. Ones with rows >> cols