loading...

One goal of the {svTidy} package is to provide an interface that is similar to {dplyr} and {tidyr} to performant code (both for speed and memory use), possibly using the {data.table} or {collapse} packages under the hood.

In this document, we compare speed and memory use of {svTidy} with {dplyr} and {tidyr}. Keep in mind that most benchmarks (including the present ones) are artificial and do not necessarily reflect real use cases. Test with your own data. Also, do you really need faster or more memory efficient code? It depends on your particular context! Also take your hardware into account (number of CPU and amount of RAM).

We test {svTidy} function both in standard evaluation (SE) and non standard evaluation (NSE) modes: the later one usually requires more computing time, but it is more convenient to write and read and closer to {dplyr}/{tidyr} syntax.

Functions that are considered optimized

Preparation: using 3/4 of available cores for parallel code in {data.table} and {collapse}.

data.table::setDTthreads(percent = 75)
(.nthreads <- data.table::getDTthreads())
#> [1] 3
options(collapse_nthreads = .nthreads)
options(collapse_na.rm = FALSE)
options(collapse_mask = "all")

Small and medium data sets.

# Small one
data(mtcars)
mtcars <- as_tibble(mtcars, rownames = "model")
mtcars_dt <- as.data.table(mtcars)

# Medium one
data(babynames, package = 'babynames')
babynames <- as_tibble(babynames)
babynames_dt <- as.data.table(babynames)

Here is a couple of examples of fast and memory efficient {svTidy} functions.

filter_()

Small data set.

# Note: collapse::qDF() = quickly convert to a data.frame, for identical results
bench::mark(
  dplyr      = filter(mtcars, mpg > 20) |> qDF(),
  data.table = mtcars_dt[mpg > 20] |> qDF(), 
  svTidyNSE  = filter_(mtcars, ~mpg > 20) |> qDF(),
  svTidySE   = filter_(mtcars, mtcars$mpg > 20) |> qDF())
#> # A tibble: 4 × 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 dplyr       551.4µs    603µs     1635.    1.36MB     8.59
#> 2 data.table   85.9µs   94.4µs    10277.    1.81MB     8.52
#> 3 svTidyNSE    40.4µs   44.9µs    21697.  154.44KB    10.9 
#> 4 svTidySE     35.6µs   39.6µs    24670.   43.45KB     9.87

Medium data set.

bench::mark(
  dplyr      = filter(babynames, n > 1000) |> qDF(),
  data.table = babynames_dt[n > 1000] |> qDF(),
  svTidyNSE  = filter_(babynames, ~n > 1000) |> qDF(),
  svTidySE   = filter_(babynames, babynames$n > 1000) |> qDF())
#> # A tibble: 4 × 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 dplyr        12.5ms  12.62ms      78.1    31.4MB     48.1
#> 2 data.table   6.44ms   8.23ms     119.     16.7MB     29.0
#> 3 svTidyNSE    5.41ms   5.59ms     173.     16.7MB     43.1
#> 4 svTidySE     5.55ms   5.67ms     165.     16.7MB     27.1

arrange_()

bench::mark(
  dplyr      = arrange(mtcars, cyl, desc(vs)) |> qDF(),
  data.table = mtcars_dt[order(cyl, -vs)] |> qDF(),
  svTidyNSE  = arrange_(mtcars, ~cyl, ~ -vs) |> qDF(),
  svTidySE   = arrange_(mtcars, 'cyl', '-vs') |> qDF())
#> # A tibble: 4 × 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 dplyr        1.94ms   2.05ms      483.   539.3KB     8.66
#> 2 data.table 126.93µs 134.32µs     7247.    38.9KB     8.57
#> 3 svTidyNSE      79µs  85.25µs    11379.    92.9KB    11.0 
#> 4 svTidySE    47.45µs  51.29µs    18917.    85.1KB    10.9
bench::mark(
  dplyr      = arrange(babynames, sex, desc(n)) |> qDF(),
  data.table = babynames_dt[order(sex, -n)] |> qDF(),
  svTidyNSE  = arrange_(babynames, ~sex, ~ -n) |> qDF(),
  svTidySE   = arrange_(babynames, 'sex', '-n') |> qDF())
#> # A tibble: 4 × 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 dplyr        69.9ms   69.9ms      14.3   112.7MB     85.8
#> 2 data.table   69.4ms   70.1ms      14.1    73.4MB     18.8
#> 3 svTidyNSE    27.1ms   27.8ms      34.0    73.4MB     22.7
#> 4 svTidySE     26.5ms     27ms      36.4    73.4MB     16.6

Functions that could still be optimized

Not all {svTidy} functions are currently faster or more memory efficient than their {dplyr} or {tidyr} counterparts. Those still need refactoring to be optimized. Here are some examples.

bind_rows_()

df1 <- tibble(x = 1:2, y = letters[1:2])
df1_dt <- as.data.table(df1)

bench::mark(
  dplyr      = bind_rows(df1, df1) |> qDF(),
  base       = rbind(df1, df1) |> qDF(),
  data.table = rbindlist(list(df1_dt, df1_dt)) |> qDF(),
  svTidy     = bind_rows_(df1, df1) |> qDF(),
  svTidy2    = bind_rows_(list(df1, df1)) |> qDF())
#> # A tibble: 5 × 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 dplyr        69.9µs   75.4µs    12944.    96.4KB    11.2 
#> 2 base           61µs   65.8µs    14833.    47.5KB    10.7 
#> 3 data.table   96.2µs  101.4µs     9607.   121.5KB     6.28
#> 4 svTidy      602.5µs  640.9µs     1541.   207.8KB     6.37
#> 5 svTidy2     599.9µs    641µs     1536.    55.8KB     8.60
bench::mark(
  dplyr      = bind_rows(babynames, babynames) |> qDF(),
  base       = rbind(babynames, babynames) |> qDF(),
  data.table = rbindlist(list(babynames_dt, babynames_dt)) |> qDF(),
  svTidy     = bind_rows_(babynames, babynames) |> qDF(),
  svTidy2    = bind_rows_(list(babynames, babynames)) |> qDF())
#> Warning: Some expressions had a GC in every iteration; so filtering is
#> disabled.
#> # A tibble: 5 × 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 dplyr       82.45ms  87.94ms     8.83      132MB    3.53 
#> 2 base       173.98ms 291.87ms     3.43      256MB    1.71 
#> 3 data.table  42.81ms  50.38ms    17.4       132MB    5.79 
#> 4 svTidy        1.57s    1.57s     0.638     368MB    0.638
#> 5 svTidy2       1.51s    1.51s     0.660     338MB    0.660

bind_cols_()

df1 <- tibble(x = 1:2, y = letters[1:2])
df2 <- tibble(z = 10:11, w = factor(5:6))
df1_dt <- as.data.table(df1)
df2_dt <- as.data.table(df2)

bench::mark(check = FALSE,
  dplyr      = bind_cols(df1, df2) |> qDF(),
  base       = cbind(df1, df2) |> qDF(),
  data.table = cbind(df1_dt, df2_dt) |> qDF(),
  svTidy     = bind_cols_(df1, df2) |> qDF())
#> # A tibble: 4 × 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 dplyr         110µs    121µs     8095.   32.87KB     2.09
#> 2 base          149µs    161µs     6092.    2.19KB     4.36
#> 3 data.table    136µs    147µs     6703.  187.32KB     2.09
#> 4 svTidy        116µs    127µs     7739.   13.88KB     4.37