Among the many reasons to use data.table in your code (which includes the more common answers of speed, memory efficiency, etc.) is the syntax. The syntax is
concise,
predictable, and
R-centric.
In this post, I’d like to show how these features are beneficial and useful in working with data regardless of the size of the data. To do this, I’ll use two packages:
library(data.table)
Attaching package: 'data.table'
The following object is masked from 'package:base':
%notin%
library(palmerpenguins)
Attaching package: 'palmerpenguins'
The following objects are masked from 'package:datasets':
penguins, penguins_raw
and we’ll create a data.table of the penguins data set (and a data.frame version for other examples):
This post assumes some familiarity with data.table syntax but even if you are new to it, there is likely a lot of information that is quite useful for you.
Concise
The syntax ultimately is built around the concise dt[i, j, by] framework (built on the core functionality of data frames, see the R-centric section below). This syntax allows you to:
Subset (“filter”) your data using the i argument.
# Subset to only Adelie speciesdt[species =="Adelie"]
Do all sorts of data work on groups using the by argument.
# create a new variable that is the average of the body mass by speciesdt[, avg_mass_lbs :=mean(body_mass_lbs, na.rm=TRUE), by = sex]
species sex avg_mass_lbs
<fctr> <fctr> <num>
1: Adelie male 10.021507
2: Adelie female 8.514844
3: Adelie female 8.514844
4: Adelie <NA> 8.830728
5: Adelie female 8.514844
---
340: Chinstrap male 10.021507
341: Chinstrap female 8.514844
342: Chinstrap male 10.021507
343: Chinstrap male 10.021507
344: Chinstrap female 8.514844
This is more difficult, but possible, in base R to get a summary and add it to the existing data.frame:
tapply(df$body_mass_lbs, df$sex, mean, na.rm=TRUE) # doesn't keep all rows# does keep all rows but complicated codedf <-by(df, INDICES = df$sex, FUN =function(x){ x$avg_mass_lbs <-mean(x$body_mass_lbs)return(x) })df <-do.call("rbind", df)
In each example, you can see a lot of work can be done in a single line of code with minimal redundancy. Although in each situation base R and tidyverse equivalents exist (often with a lot of powerful flexibility in the tidyverse approaches), the concise nature of data.table syntax can make writing and reading the code quicker.
Predictable
The syntax is naturally predictable without being verbose. For instance, whenever you use :=, it’s going to keep the same shape as the current data (“mutate”) while the use of .(var = fun(x)) will summarize to the fewest number of rows appropriate (1 row for non-grouped expressions and x rows for x number of unique groups).
To get an idea of how this predictability manifests in the code, we’ll use an example. Here, we can grab the average bill length by sex. We could do this two ways. The first is mutating in place where the data do not change size or shape. Note, the .() function is shorthand for list().
dt[, avg_bill_length :=mean(bill_length_mm, na.rm=TRUE), by = sex]
This gives us a new variable in the original data.
species sex avg_bill_length
<fctr> <fctr> <num>
1: Adelie male 45.85476
2: Adelie female 42.09697
3: Adelie female 42.09697
4: Adelie <NA> 41.30000
5: Adelie female 42.09697
---
340: Chinstrap male 45.85476
341: Chinstrap female 42.09697
342: Chinstrap male 45.85476
343: Chinstrap male 45.85476
344: Chinstrap female 42.09697
However, sometimes we just want the data summarized. We can use the syntax below for that (notice no :=).
dt[, .(avg_bill_length =mean(bill_length_mm, na.rm=TRUE)), by = sex]
sex avg_bill_length
<fctr> <num>
1: male 45.85476
2: female 42.09697
3: <NA> 41.30000
We can always assign this so we can access it later.
avg_bill <- dt[, .(avg_bill_length =mean(bill_length_mm, na.rm=TRUE)), by = sex]
One way data.table makes the code predictable is that the data operations happen all within the square brackets without lingering attributes that may produce surprising results. That is, whatever I put in the brackets will be run together and then done. For example, I may have several grouping variables that I use to modify some variables, and only do it for a subset of the data.
dt[species =="Adelie", max_bill :=max(bill_length_mm, na.rm=TRUE), by = .(species, sex)]
The new variable max_bill is made for the data but is only applicable to the Adelie species and is done by both species as sex. Once this operation is done, the grouping variables are just normal variables again and we still have access to the full data.
species sex max_bill
<fctr> <fctr> <num>
1: Adelie male 46.0
2: Adelie female 42.2
3: Adelie female 42.2
4: Adelie <NA> 42.0
5: Adelie female 42.2
---
340: Chinstrap male NA
341: Chinstrap female NA
342: Chinstrap male NA
343: Chinstrap male NA
344: Chinstrap female NA
R-centric
All of the main functionality in data.table is structured around vectors, lists, and (a modified form) of data frames. These core structures in R can be seeing throughout the syntax and design of the package. Even the dt[i, j, by] syntax is designed to mirror (and simplify) data frames. For new users, this can be particularly useful: no additional data structures are needed to work with the data and do both simple and complicated data operations.
Conclusions
In my experience, as one gets more familiar with the syntax of data.table, the more it becomes clear that the syntax (although less verbose than other approaches like the tidyverse), is concise, predictable, and familiar to the basics of the R programming language. Among many reasons to leverage data.table in your workflow, the syntax is one to not overlook.