How to: use data.table for more efficient data management
TL;DR: data.table is a high-performance R package for fast data manipulation. Offers concise syntax, minimal memory footprint, and 10-100x speedups over base R and dplyr for large datasets. Essential for genomics and single-cell analysis workflows.
What is data.table?
data.table is an R package that extends data.frame with:
- Speed: In-place operations avoid unnecessary copies
- Memory efficiency: Minimal allocation overhead
- Concise syntax:
DT[i, j, by]does filtering, computing, and grouping in one line - Large data support: Handles datasets larger than RAM with on-disk processing
When to use data.table
| Use Case | data.table | Alternatives |
|---|---|---|
| Large files (>1GB) | ✅ Fast fread() |
base R: slow |
| Grouped operations | ✅ Optimized by |
dplyr: similar |
| Memory-constrained | ✅ In-place updates | tidyverse: copies |
| Interactive analysis | ⚠️ Less readable | tidyverse: pipe-friendly |

Installation
Standard Installation (Linux/Windows)
install.packages("data.table")
macOS Installation (with OpenMP Support)
macOS requires manual OpenMP configuration for multi-threading:
Step 1: Install OpenMP Runtime
brew update && brew install libomp
Step 2: Configure R Makevars
mkdir -p ~/.R
nano ~/.R/Makevars
Add to the end of the file:
LDFLAGS += -L/opt/homebrew/opt/libomp/lib -lomp
CPPFLAGS += -I/opt/homebrew/opt/libomp/include -Xclang -fopenmp
Step 3: Install data.table
# Remove old version if present
remove.packages("data.table")
# Install stable version
install.packages("data.table")
# Or development version:
# install.packages("data.table", type = "source",
# repos = "https://Rdatatable.gitlab.io/data.table")
Verify installation:
library(data.table)
getDTthreads() # Should show >1 if OpenMP enabled
Core Operations
Reading and Writing Files
data.table uses fread() and fwrite() for fast I/O:
library(data.table)
# Fast read (auto-detects separator, headers, types)
dt <- fread("counts.csv")
# Fast write
fwrite(dt, "output.csv")
# Read compressed files
dt <- fread("data.csv.gz")
# Control column types
dt <- fread("data.csv", colClasses = c(gene_id = "character", count = "integer"))
Speed comparison:
fread()is 5-10x faster thanread.csv()fwrite()handles millions of rows in seconds
Basic Syntax: DT[i, j, by]
| Part | Purpose | Example |
|---|---|---|
| i | Filter rows | DT[count > 10] |
| j | Select/compute columns | DT[, .(gene, logFC)] |
| by | Group operations | DT[, mean(count), by = sample] |
Filtering Rows
# Single condition
dt[count > 100]
# Multiple conditions
dt[count > 100 & padj < 0.05]
# By row number
dt[1:10]
# Remove rows
dt[gene_id != "ERCC-00002"]
Selecting and Computing Columns
# Select columns
dt[, .(gene_id, count)]
# Compute new column
dt[, log_count := log2(count + 1)] # := modifies in-place
# Multiple computations
dt[, `:=`(
log_count = log2(count + 1),
norm_count = count / sum(count)
)]
# Summary statistics
dt[, .(mean_count = mean(count), n_genes = .N)]
Key difference: := modifies the table in-place (no copy), .() returns a new table.
Grouping Operations
# Group by single column
dt[, .(mean_expr = mean(count)), by = cell_type]
# Multiple grouping variables
dt[, .(total = sum(count)), by = .(sample, cell_type)]
# Group and filter
dt[, .SD[count == max(count)], by = gene_id] # Top expressed cell per gene
Joining Tables
# Load annotation
anno <- fread("gene_annotations.csv")
# Left join (keeps all dt rows)
dt_anno <- anno[dt, on = "gene_id"]
# Inner join (only matching rows)
dt_anno <- dt[anno, on = "gene_id", nomatch = NULL]
# Join on multiple keys
merge(dt, anno, by = c("gene_id", "transcript_id"))
Reshaping Data
# Wide to long
dt_long <- melt(dt, id.vars = "gene_id",
measure.vars = c("sample1", "sample2", "sample3"),
variable.name = "sample", value.name = "count")
# Long to wide
dt_wide <- dcast(dt_long, gene_id ~ sample, value.var = "count")
Common Bioinformatics Tasks
Normalize RNA-seq Counts
# TPM normalization
dt[, tpm := (count / gene_length) / sum(count / gene_length) * 1e6,
by = sample]
# DESeq2-style size factors
dt[, size_factor := sum(count) / median(sum(count)), by = sample]
dt[, norm_count := count / size_factor]
Filter Low-Expression Genes
# Keep genes with >10 counts in at least 3 samples
dt[, n_detected := sum(count > 10), by = gene_id]
dt_filt <- dt[n_detected >= 3]
Calculate Fold Changes
# Compare treatment vs control
dt_wide <- dcast(dt, gene_id ~ condition, value.var = "count")
dt_wide[, log2FC := log2((treatment + 1) / (control + 1))]
Performance Tips
Use
:=for in-place updates (avoid copies)dt[, new_col := value] # Fast dt <- dt[, .(old_col, new_col = value)] # Slower (creates copy)Set keys for fast subsetting
setkey(dt, gene_id) dt["GAPDH"] # Binary search, O(log n)Enable multi-threading
setDTthreads(8) # Use 8 coresUse
.SDfor subsets within groupsdt[, .SD[1], by = gene_id] # First row per gene
Troubleshooting
OpenMP not working on macOS
Symptom: getDTthreads() returns 1
Fix: Reinstall following the macOS installation steps above
"Column X not found"
Cause: Typo or trying to use a column before creating it
Fix: Check column names with names(dt)
Memory errors with large files
Solutions:
# Read subset of rows
dt <- fread("big.csv", nrows = 1000)
# Read subset of columns
dt <- fread("big.csv", select = c("gene_id", "count"))
# Increase memory limit
options(datatable.fread.memory = 16) # GB
data.table vs dplyr
| Feature | data.table | dplyr |
|---|---|---|
| Speed | Faster (especially large data) | Fast |
| Memory | Minimal (in-place updates) | Higher (copies) |
| Syntax | Compact DT[i, j, by] |
Pipe-friendly %>% |
| Learning curve | Steeper | Gentler |
| Best for | Performance-critical workflows | Readable scripts |
Use both: setDT() converts tibbles to data.tables without copying.
Resources
- Official docs: rdatatable.gitlab.io/data.table
- Installation guide: github.com/Rdatatable/data.table/wiki/Installation
- Cheat sheet: raw.githubusercontent.com/rstudio/cheatsheets/main/datatable.pdf
- macOS OpenMP: mac.r-project.org/openmp
See also:
Originally posted on WeChat Official Account: bioinfo_studyroom