Chapter 6 data.table

In this book, we use tidyverse packages to illustrate because beginners find the code readable and this permits us to focus on data analysis and statistical concepts. However, there are other approaches to wrangling and analyzing data in R that are faster and better at handling large objects. The data.table package, for example, is one of the most popular and we therefore provide a very quick introduction in this chapter . There are several online resources, including this introduction15 for those wanting to learn more.

6.1 Manipulating data tables

data.table is a separate package that needs to be installed. Once installed, we then need to load it:


We will provide example code showing the data.table approaches to dplyr’s mutate, filter, select, group_by, and summarize shown in Chapter 4. As in that chapter, we will use the murders dataset:

The first step when using data.table is to convert the data frame into a data.table object using the setDT function:

murders <-setDT(murders)

Without this initial step, most of the approaches shown below will not work.

6.1.1 Selecting

Selecting with data.table is done in a similar way to subsetting matrices. While with dplyr we write:

select(murders, state, region)

In data.table, we use notation similar to what is used with matrices:

murders[, c("state", "region")] |> head()
#>         state region
#> 1:    Alabama  South
#> 2:     Alaska   West
#> 3:    Arizona   West
#> 4:   Arkansas  South
#> 5: California   West
#> 6:   Colorado   West

We can also use the .() data.table notation to alert R that variables inside the parenthesis are column names, not objects in the R environment. So the above can also be written like this:

murders[, .(state, region)] |> head()
#>         state region
#> 1:    Alabama  South
#> 2:     Alaska   West
#> 3:    Arizona   West
#> 4:   Arkansas  South
#> 5: California   West
#> 6:   Colorado   West

6.1.2 Adding a column or changing columns

We learned to use the dplyr mutate function with this example:

murders <- mutate(murders, rate = total / population * 100000)

data.table uses an approach that avoids a new assignment (update by reference). This can help with large datasets that take up most of your computer’s memory. The data.table :=` function permits us to do this:

murders[, rate := total / population * 100000]

This adds a new column, rate, to the table. Notice that, as in dplyr, we used total and population without quotes.

We can see that the new column is added:

#>         state abb region population total rate
#> 1:    Alabama  AL  South    4779736   135 2.82
#> 2:     Alaska  AK   West     710231    19 2.68
#> 3:    Arizona  AZ   West    6392017   232 3.63
#> 4:   Arkansas  AR  South    2915918    93 3.19
#> 5: California  CA   West   37253956  1257 3.37
#> 6:   Colorado  CO   West    5029196    65 1.29

To define new multiple columns, we can use the := function with multiple arguments:

murders[, ":="(rate = total / population * 100000, rank = rank(population))]

6.1.3 Technical detail: reference versus copy

The data.table package is designed to avoid wasting memory. So if you make a copy of a table, like this:

x <- data.table(a = 1)
y <- x

y is actually referencing x, it is not an new opject: it’s just another name for x. Until you change y, a new object will not be made. However, the := function changes by reference so if you change x, a new object is not made and y continues to be just another name for x:

x[,a := 2]
#>    a
#> 1: 2

You can also change x like this:

y[,a := 1]
#>    a
#> 1: 1

To avoid this, you can use the copy function which forces the creation of an actual copy:

x <- data.table(a = 1)
y <- copy(x)
x[,a := 2]
#>    a
#> 1: 1

6.1.4 Subsetting

With dplyr, we filtered like this:

filter(murders, rate <= 0.7)

With data.table, we again use an approach similar to subsetting matrices, except data.table knows that rate refers to a column name and not an object in the R environment:

murders[rate <= 0.7]
#>            state abb        region population total  rate rank
#> 1:        Hawaii  HI          West    1360301     7 0.515   12
#> 2:          Iowa  IA North Central    3046355    21 0.689   22
#> 3: New Hampshire  NH     Northeast    1316470     5 0.380   10
#> 4:  North Dakota  ND North Central     672591     4 0.595    4
#> 5:       Vermont  VT     Northeast     625741     2 0.320    3

Notice that we can combine the filter and select into one succint command. Here are the state names and rates for those with rates below 0.7.

murders[rate <= 0.7, .(state, rate)]
#>            state  rate
#> 1:        Hawaii 0.515
#> 2:          Iowa 0.689
#> 3: New Hampshire 0.380
#> 4:  North Dakota 0.595
#> 5:       Vermont 0.320

Compare to the dplyr approach:

murders |> filter(rate <= 0.7) |> select(state, rate)

6.2 Exercises

1. Load the data.table package and the murders dataset and convert it to data.table object:

murders <- setDT(murders)

Remember you can add columns like this:

murders[, population_in_millions := population / 10^6]

Add a murders column named rate with the per 100,000 murder rate as in the example code above.

2. Add a column rank containing the rank, from highest to lowest murder rate.

3. If we want to only show the states and population sizes, we can use:

murders[, .(state, population)] 

Show the state names and abbreviations in murders.

4. You can show just the New York row like this:

murders[state == "New York"]

You can use other logical vectors to filter rows.

Show the top 5 states with the highest murder rates. After we add murder rate and rank, do not change the murders dataset, just show the result. Remember that you can filter based on the rank column.

5. We can remove rows using the != operator. For example, to remove Florida, we would do this:

no_florida <- murders[state != "Florida"]

Create a new data frame called no_south that removes states from the South region. How many states are in this category? You can use the function nrow for this.

6. We can also use %in% to filter. You can therefore see the data from New York and Texas as follows:

murders[state %in% c("New York", "Texas")]

Create a new data frame called murders_nw with only the states from the Northeast and the West. How many states are in this category?

7. Suppose you want to live in the Northeast or West and want the murder rate to be less than 1. We want to see the data for the states satisfying these options. Note that you can use logical operators with filter. Here is an example in which we filter to keep only small states in the Northeast region.

murders[population < 5000000 & region == "Northeast"]

Make sure murders has been defined with rate and rank and still has all states. Create a table called my_states that contains rows for states satisfying both the conditions: they are in the Northeast or West and the murder rate is less than 1. Show only the state name, the rate, and the rank.

6.3 Summarizing data

As an example, we will use the heights dataset:

heights <- setDT(heights)

In data.table, we can call functions inside .() and they will be applied to rows. So the equivalent of:

s <- heights |> 
  summarize(average = mean(height), standard_deviation = sd(height))

in dplyr is the following:

s <- heights[, .(average = mean(height), standard_deviation = sd(height))]

Note that this permits a compact way of subsetting and then summarizing. Instead of:

s <- heights |> 
  filter(sex == "Female") |>
  summarize(average = mean(height), standard_deviation = sd(height))

we can write:

s <- heights[sex == "Female", .(average = mean(height), standard_deviation = sd(height))]

6.3.1 Multiple summaries

In chapter 4, we defined the function:

median_min_max <- function(x){
  qs <- quantile(x, c(0.5, 0, 1))
  data.frame(median = qs[1], minimum = qs[2], maximum = qs[3])

Similar to dplyr, we can call this function within .() to obtain the three number summary:

heights[, .(median_min_max(height))]
#>    median minimum maximum
#> 1:   68.5      50    82.7

6.3.2 Group then summarize

The group_by followed by summarize in dplyr is performed in one line in data.table. We simply add the by argument to split the data into groups based on the values in categorical variable:

heights[, .(average = mean(height), standard_deviation = sd(height)), by = sex]
#>       sex average standard_deviation
#> 1:   Male    69.3               3.61
#> 2: Female    64.9               3.76

6.4 Sorting data frames

We can order rows using the same approach we use for filter. Here are the states ordered by murder rate:

murders[order(population)] |> head()
#>                   state abb        region population total   rate rank
#> 1:              Wyoming  WY          West     563626     5  0.887    1
#> 2: District of Columbia  DC         South     601723    99 16.453    2
#> 3:              Vermont  VT     Northeast     625741     2  0.320    3
#> 4:         North Dakota  ND North Central     672591     4  0.595    4
#> 5:               Alaska  AK          West     710231    19  2.675    5
#> 6:         South Dakota  SD North Central     814180     8  0.983    6

N To sort the table in descending order, we can order by the negative of population or use the decreasing argument:

murders[order(population, decreasing = TRUE)] 

6.4.1 Nested sorting

Similarly, we can perform nested ordering by including more than one variable in order

murders[order(region, rate)] 


6.5 Exercises

For these exercises, we will be using the NHANES data.


1. We will provide some basic facts about blood pressure. First let’s select a group to set the standard. We will use 20-to-29-year-old females. AgeDecade is a categorical variable with these ages. Note that the category is coded like ” 20-29”, with a space in front! Use the data.table package to compute the average and standard deviation of systolic blood pressure as saved in the BPSysAve variable. Save it to a variable called ref.

2. Report the min and max values for the same group.

3. Compute the average and standard deviation for females, but for each age group separately rather than a selected decade as in question 1. Note that the age groups are defined by AgeDecade.

4. Repeat exercise 3 for males.

5. For males between the ages of 40-49, compare systolic blood pressure across race as reported in the Race1 variable. Order the resulting table from lowest to highest average systolic blood pressure.