Skip to Content
Course content

47: Working with data.table for Performance

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Look, if you're working with a few thousand rows of data, dplyr is fantastic. It's readable, and the syntax is intuitive. But once you hit a few million rows—or you're working on a machine with limited RAM—you'll start feeling the lag. That's where data.table comes in. It's not just a different way to write code; it's a different way of handling memory. I've spent way too many hours watching a progress bar crawl across my screen before I fully embraced the data.table way of doing things.

Why is data.table actually faster than the stuff I'm already using?

The secret sauce is how data.table handles memory. Most R data frames use "copy-on-modify" semantics. If you change one column in a huge data frame, R often creates a whole new copy of that data frame in your RAM just to make that one change. When your dataset is 4GB and you only have 8GB of RAM, you're going to hit a wall very quickly.

data.table uses "modify-in-place." It updates the data exactly where it sits in memory. To see this in action, imagine we're dealing with a massive set of IoT sensor readings:

library(data.table)

# Creating a dummy dataset with 10 million rows
set.seed(123)
sensors <- data.table(
  sensor_id = sample(1:100, 1e7, replace = TRUE),
  reading = rnorm(1e7),
  timestamp = seq(as.POSIXct("2023-01-01"), by = "sec", length.out = 1e7)
)

Because it's built on top of data.frame but optimized in C, it bypasses a lot of the overhead that makes base R slow on large scales.

How do I wrap my head around the [i, j, by] syntax?

The first time I saw a data.table call, it looked like a weird piece of shorthand. But once it clicks, you'll realize it's actually incredibly concise. The formula is always DT[i, j, by].

  • i: Which rows do I want? (Filtering)
  • j: What do I want to do with them? (Selecting or calculating)
  • by: How should I group them? (Aggregation)

If we want to find the average reading for every sensor, but only for readings above 0, it looks like this:

# i = reading > 0
# j = mean(reading)
# by = sensor_id
result <- sensors[reading > 0, .(avg_val = mean(reading)), by = sensor_id]

Note the .() inside the j slot. That's just shorthand for list(). I use it constantly because it lets me name my new columns on the fly without having to rename them later.

What is this ":=" operator and why should I care?

The := operator is the "assignment-by-reference" operator. This is the most powerful tool in the data.table arsenal. It allows you to add or update columns without copying the entire table.

If I want to create a "status" column based on the reading value, I don't do sensors$status <- .... Instead, I do this:

# Add a new column 'status' based on a condition
sensors[reading > 1, status := "High"]
sensors[reading <= 1, status := "Normal"]

I cannot stress this enough: when you use :=, you are modifying the object in place. You don't need to assign it back to a variable (e.g., sensors <- sensors[...]). Doing that actually defeats the purpose and slows everything down.

Is it really that much faster for joins?

Yes, significantly—especially if you use "keys." A key tells data.table to sort the data in memory, which allows it to use binary search instead of scanning the whole table for a match.

Let's say we have a secondary table with sensor metadata:

meta <- data.table(
  sensor_id = 1:100,
  location = sample(c("North", "South", "East", "West"), 100, replace = TRUE)
)

# Set keys for lightning-fast joins
setkey(sensors, sensor_id)
setkey(meta, sensor_id)

# Perform a right join (joining sensors to meta)
final_data <- meta[sensors]

By setting the keys, the join happens almost instantaneously, even with millions of rows. It's a habit I've developed: if I'm joining two large tables more than once, I setkey them immediately.




📋 Practical Task

Optimizing a High-Frequency Trading Log

You have been handed a large data frame of trading logs. The current script is using a slow for loop and standard data frame assignment, causing the server to crash due to memory exhaustion. Your task is to refactor this into a high-performance data.table pipeline.

The Data: trades (contains symbol, price, and volume).

The Goal:

  1. Convert the data frame to a data.table.
  2. Add a new column called total_value (price * volume) using assignment-by-reference.
  3. Calculate the total volume per symbol, but only for trades where total_value is greater than 10,000.
  4. Return a table with symbol and the sum of volume.

Starter Code:

library(data.table)
trades <- data.frame(
  symbol = sample(c("AAPL", "TSLA", "MSFT", "GOOG"), 1e6, replace = TRUE),
  price = runif(1e6, 100, 1500),
  volume = sample(1:1000, 1e6, replace = TRUE)
)

# Your refactored code here:
Rating
0 0

There are no comments for now.

to be the first to leave a comment.