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.
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 673.1µs 716.6µs 1347. 1.36MB 8.52
#> 2 data.table 108.6µs 115µs 8330. 1.82MB 6.28
#> 3 svTidyNSE 52.7µs 56µs 17225. 154.43KB 10.8
#> 4 svTidySE 45.9µs 48.7µs 19842. 43.45KB 10.8Medium 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 10.74ms 10.97ms 76.4 31.4MB 43.0
#> 2 data.table 4.71ms 4.84ms 192. 16.7MB 47.2
#> 3 svTidyNSE 4.29ms 4.46ms 189. 16.7MB 37.8
#> 4 svTidySE 4.33ms 4.45ms 184. 16.7MB 29.8arrange_()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 2.25ms 2.31ms 429. 591.4KB 8.71
#> 2 data.table 155.89µs 163.07µs 5928. 38.9KB 8.57
#> 3 svTidyNSE 100.44µs 105.1µs 9144. 92.9KB 10.9
#> 4 svTidySE 60.58µs 64.1µs 14978. 85.1KB 10.8bench::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 84.4ms 84.4ms 11.8 112.7MB 59.2
#> 2 data.table 59.5ms 66.1ms 15.1 73.4MB 37.8
#> 3 svTidyNSE 25.9ms 27.1ms 25.5 73.4MB 11.4
#> 4 svTidySE 25.7ms 26.1ms 35.9 73.4MB 16.3Not 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 90.1µs 94.4µs 10227. 96.4KB 8.68
#> 2 base 78µs 82.2µs 11724. 47.5KB 11.1
#> 3 data.table 110.3µs 114.1µs 8461. 121.5KB 6.30
#> 4 svTidy 616.1µs 652.7µs 1525. 203.9KB 10.8
#> 5 svTidy2 618.1µs 657µs 1514. 52.5KB 8.60bench::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 74.22ms 75.57ms 10.7 132MB 3.57
#> 2 base 157.51ms 180.73ms 5.39 256MB 3.59
#> 3 data.table 44.57ms 46.07ms 17.1 132MB 5.71
#> 4 svTidy 1.56s 1.56s 0.640 368MB 1.28
#> 5 svTidy2 1.52s 1.52s 0.657 338MB 0.657bind_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 144µs 152µs 6348. 32.87KB 2.10
#> 2 base 194µs 205µs 4733. 2.19KB 2.08
#> 3 data.table 162µs 169µs 5730. 187.33KB 2.09
#> 4 svTidy 152µs 158µs 6109. 13.88KB 4.26