The goal of tidyfast is to provide fast and efficient alternatives to some tidyr (and a few dplyr) functions using data.table under the hood. Each have the prefix of dt_ to allow for autocomplete in IDEs such as RStudio. These should compliment some of the current functionality in dtplyr (but notably does not use the lazy_dt() framework of dtplyr). This package imports data.table and Rcpp (no other dependencies).

These are, in essence, translations from a more tidyverse grammar to data.table. Most functions herein are in places where, in my opinion, the data.table syntax is not obvious or clear. As such, these functions can translate a simple function call into the fast, efficient, and concise syntax of data.table.

The current functions include:

Nesting and unnesting (similar to dplyr::group_nest() and tidyr::unnest()):

Pivoting (similar to tidyr::pivot_longer() and tidyr::pivot_wider())

If Else (similar to dplyr::case_when()):

Fill (similar to tidyr::fill())

  • dt_fill() for filling NA values with values before it, after it, or both. This can be done by a grouping variable (e.g. fill in NA values with values within an individual).

Count and Uncount (similar to tidyr::uncount() and dplyr::count())

Separate (similar to tidyr::separate())

  • dt_separate() for splitting a single column into multiple based on a match within the column (e.g., column with values like “A.B” could be split into two columns by using the period as the separator where column 1 would have “A” and 2 would have “B”). It is built on data.table::tstrsplit(). This is not well tested yet and lacks some functionality of tidyr::separate().

Adjust data.table print options

General API

tidyfast attempts to convert syntax from tidyr with its accompanying grammar to data.table function calls. As such, we have tried to maintain the tidyr syntax as closely as possible without hurting speed and efficiency. Some more advanced use cases in tidyr may not translate yet. We try to be transparent about the shortcomings in syntax and behavior where known.

Each function that takes data (labeled as dt_ in the package docs) as its first argument automatically coerces it to a data table with as.data.table() if it isn’t already a data table. Each of these functions will return a data table.

Installation

You can install the development version from GitHub with:

Examples

The initial versions of the nesting and unnesting functions were shown in a preprint. Herein is shown some simple applications and the functions’ speed/efficiency.

Nesting and Unnesting

The following data table will be used for the nesting/unnesting examples.

set.seed(84322)

library(tidyfast)
library(data.table)
library(dplyr)       # to compare with case_when()
library(tidyr)       # to compare with fill() and separate()
library(ggplot2)     # figures
library(ggbeeswarm)  # figures

dt <- data.table(
   x = rnorm(1e5),
   y = runif(1e5),
   grp = sample(1L:5L, 1e5, replace = TRUE),
   nested1 = lapply(1:10, sample, 10, replace = TRUE),
   nested2 = lapply(c("thing1", "thing2"), sample, 10, replace = TRUE),
   id = 1:1e5)

To make all the comparisons herein more equal, we will set the number of threads that data.table will use to 1.

We can nest this data using dt_nest():

We can also unnest this with dt_unnest():

When our list columns don’t have data tables (as output from dt_nest()) we can use the dt_hoist() function, that will unnest vectors. It keeps all the other variables that are not list-columns as well.

Speed comparisons (similar to those shown in the preprint) are highlighted below. Notably, the timings are without the nested1 and nested2 columns of the original dt object from above. Also, all dplyr and tidyr functions use a tbl version of the dt table.

Pivoting

Finally, thanks to [@mtfairbanks](https://github.com/mtfairbanks), we now have pivoting translations to data.table::melt() and data.table::dcast(). Consider the following example (similar to the example in tidyr::pivot_longer() and tidyr::pivot_wider()):

billboard <- tidyr::billboard

# note the warning - melt is telling us what 
#   it did with the various data types---logical (where there were just NAs
#   and numeric
longer <- billboard %>%
  dt_pivot_longer(
     cols = c(-artist, -track, -date.entered),
     names_to = "week",
     values_to = "rank"
  )
#> Warning in melt.data.table(data = dt_, id.vars = id_vars, measure.vars =
#> cols, : 'measure.vars' [wk1, wk2, wk3, wk4, ...] are not all of the same
#> type. By order of hierarchy, the molten data value column will be of type
#> 'double'. All measure variables not of type 'double' will be coerced too.
#> Check DETAILS in ?melt.data.table for more on coercion.
longer
#>                  artist                   track date.entered week rank
#>     1:            2 Pac Baby Don't Cry (Keep...   2000-02-26  wk1   87
#>     2:          2Ge+her The Hardest Part Of ...   2000-09-02  wk1   91
#>     3:     3 Doors Down              Kryptonite   2000-04-08  wk1   81
#>     4:     3 Doors Down                   Loser   2000-10-21  wk1   76
#>     5:         504 Boyz           Wobble Wobble   2000-04-15  wk1   57
#>    ---                                                                
#> 24088:      Yankee Grey    Another Nine Minutes   2000-04-29 wk76   NA
#> 24089: Yearwood, Trisha         Real Live Woman   2000-04-01 wk76   NA
#> 24090:  Ying Yang Twins Whistle While You Tw...   2000-03-18 wk76   NA
#> 24091:    Zombie Nation           Kernkraft 400   2000-09-02 wk76   NA
#> 24092:  matchbox twenty                    Bent   2000-04-29 wk76   NA

wider <- longer %>% 
  dt_pivot_wider(
    names_from = week,
    values_from = rank
  )
wider[, .(artist, track, wk1, wk2)]
#>                artist                   track wk1 wk2
#>   1:            2 Pac Baby Don't Cry (Keep...  87  82
#>   2:          2Ge+her The Hardest Part Of ...  91  87
#>   3:     3 Doors Down              Kryptonite  81  70
#>   4:     3 Doors Down                   Loser  76  76
#>   5:         504 Boyz           Wobble Wobble  57  34
#>  ---                                                 
#> 313:      Yankee Grey    Another Nine Minutes  86  83
#> 314: Yearwood, Trisha         Real Live Woman  85  83
#> 315:  Ying Yang Twins Whistle While You Tw...  95  94
#> 316:    Zombie Nation           Kernkraft 400  99  99
#> 317:  matchbox twenty                    Bent  60  37

Notably, there are some current limitations to these: 1) tidyselect techniques do not work across the board (e.g. cannot use start_with() and friends) and 2) the functions are new and likely prone to edge-case bugs.

But let’s compare some basic speed and efficiency. Note that the figures are in log-base-10 scale. Because of the data.table functions, these are extremely fast and efficient.

If Else

Also, the new dt_case_when() function is built on the very fast data.table::fiflese() but has syntax like unto dplyr::case_when(). That is, it looks like:

To show that each method, dt_case_when(), dplyr::case_when(), and data.table::fifelse() produce the same result, consider the following example.

x <- rnorm(1e6)

medianx <- median(x)
x_cat <-
  dt_case_when(x < medianx ~ "low",
               x >= medianx ~ "high",
               is.na(x) ~ "unknown")
x_cat_dplyr <-
  case_when(x < medianx ~ "low",
            x >= medianx ~ "high",
            is.na(x) ~ "unknown")
x_cat_fif <-
  fifelse(x < medianx, "low",
  fifelse(x >= medianx, "high",
  fifelse(is.na(x), "unknown", NA_character_)))

identical(x_cat, x_cat_dplyr)
#> [1] TRUE
identical(x_cat, x_cat_fif)
#> [1] TRUE

Notably, dt_case_when() is very fast and memory efficient, given it is built on data.table::fifelse().

Fill

A new function is dt_fill(), which fulfills the role of tidyr::fill() to fill in NA values with values around it (either the value above, below, or trying both). This currently relies on the efficient C++ code from tidyr (fillUp() and fillDown()).

In its current form, dt_fill() is faster than tidyr::fill() and uses slightly less memory. Below are the results of filling in the NAs within each id on a 19 MB data set.

Separate

The dt_separate() function is still under heavy development. Its behavior is similar to tidyr::separate() but is lacking some functionality currently. For example, into needs to be supplied the maximum number of possible columns to separate.

For current functionality, consider the following example.

dt_to_split <- data.table(
  x = paste(letters, LETTERS, sep = ".")
)

dt_separate(dt_to_split, x, into = c("lower", "upper"))

Testing with a 4 MB data set with one variable that has columns of “A.B” repeatedly, shows that dt_separate() is fast but less memory efficient than tidyr::separate().

Count and Uncount

The dt_count() function does essentially what dplyr::count() does. Notably, this, unlike the majority of other dt_ functions, wraps a very simple statement in data.table. That is, data.table makes getting counts very simple and concise. Nonetheless, dt_count() fits the general API of tidyfast. To some degree, dt_uncount() is also a fairly simple wrapper, although the approach may not be as straightforward as that for dt_count().

The following examples show how count and uncount can work. We’ll use the dt data table from the nesting examples.

These are also quick (not that the tidyverse functions were at all slow here).

Notes

Please note that the tidyfast project is released with a Contributor Code of Conduct. By contributing to this project, you agree to abide by its terms.

We want to thank our wonderful contributors:

  • mtfairbanks for PR #6 providing initial the pivoting functions. Note the gdt package that compliments some of tidyfasts functionality.

Complementary Packages: