How To Combine Datasets In R

5 min read

Combining datasets is a fundamental skill in data analysis, serving as the bridge between raw, fragmented information and a cohesive analytical framework. In R, this process—often referred to as joining or merging—allows analysts to enrich a primary dataset with variables from a secondary source, append new observations, or reshape data for specific modeling requirements. Mastering the various methods available in base R and the tidyverse ecosystem, specifically the dplyr package, ensures that data preparation remains efficient, reproducible, and error-free.

Understanding the Core Concepts: Keys and Join Types

Before writing code, it is critical to understand the structural logic behind combining data. Still, every join operation relies on a key—a column (or set of columns) shared between two datasets that uniquely identifies observations. These keys act as the "glue" binding the tables together.

People argue about this. Here's where I land on it.

There are four primary categories of joins, defined by how they handle matches and non-matches between the left (primary) and right (secondary) datasets:

  • Inner Join: Returns only rows where the key exists in both datasets. Non-matching rows are dropped entirely. This is the most restrictive join.
  • Left Join: Returns all rows from the left dataset. Where keys match, columns from the right dataset are added. Where no match exists, the new columns are filled with NA. This is the most common join for data enrichment because it preserves the primary dataset's integrity.
  • Right Join: The mirror of a left join; preserves all rows from the right dataset.
  • Full Join: Returns all rows from both datasets. Where keys match, data is combined; where they don't, missing values are filled with NA. This creates the most comprehensive union of data.

Additionally, filtering joins (semi_join, anti_join) do not add columns. Still, instead, they filter the left dataset based on whether a match exists in the right dataset. A semi_join keeps matches; an anti_join keeps non-matches—useful for diagnosing data quality issues.

The dplyr Approach: Modern, Readable, and Fast

The dplyr package, part of the tidyverse, has become the industry standard for data manipulation in R. Its join functions (inner_join, left_join, right_join, full_join) share a consistent syntax, making code highly readable Small thing, real impact..

Basic Syntax and Single-Key Joins

The standard syntax is join_function(x, y, by = "key_column"), where x is the left table and y is the right table.

Consider two data frames: customers (ID, Name, City) and orders (OrderID, CustomerID, Amount). To analyze customer spending, we need to bring Amount into the customers table.

library(dplyr)

# Sample data
customers <- data.frame(
  CustomerID = c(1, 2, 3, 4),
  Name = c("Alice", "Bob", "Charlie", "David"),
  City = c("New York", "London", "Paris", "Tokyo")
)

orders <- data.frame(
  OrderID = c(101, 102, 103, 104),
  CustomerID = c(1, 2, 2, 5), # Note: Customer 5 doesn't exist in customers
  Amount = c(250, 100, 50, 300)
)

# Left Join: Keep all customers, add order info
customer_orders <- left_join(customers, orders, by = "CustomerID")

In this left_join, Alice, Bob, and Charlie receive their order amounts. David (ID 4) has no orders, so his Amount becomes NA. Crucially, the order for Customer ID 5 is dropped because it has no match in the left table Easy to understand, harder to ignore..

Handling Different Column Names

Often, the key column has different names across datasets (e.g., CustomerID vs Cust_ID) The details matter here..

# If orders column was named 'Cust_ID'
orders_v2 <- rename(orders, Cust_ID = CustomerID)

left_join(customers, orders_v2, by = c("CustomerID" = "Cust_ID"))

Joining on Multiple Keys

In complex datasets, a single column may not uniquely identify a row. Take this case: sales data might require matching on both StoreID and Date. Pass a character vector to by:

# by = c("StoreID", "Date")

This ensures a precise match, preventing the Cartesian product (cross join) explosion that occurs when duplicate keys exist in both tables without a secondary differentiator It's one of those things that adds up..

Managing Suffixes for Overlapping Columns

If both datasets contain non-key columns with identical names (e.xand., both have a Status column), dplyr appends suffixes .g.y by default.

left_join(df1, df2, by = "ID", suffix = c("_left", "_right"))

This explicit naming prevents ambiguity during downstream analysis.

Base R: The merge() Function

While dplyr is preferred for interactive analysis and piping (%>% or |>), base R’s merge() function remains relevant for environments where dependency minimization is required or for users comfortable with SQL-like syntax.

The syntax is merge(x, by, all.x, all.y).

  • all.x = TRUE performs a Left Join.
  • all.y = TRUE performs a Right Join.
  • all.x = TRUE, all.y = TRUE performs a Full Join.
  • Defaults (all.x = FALSE, all.y = FALSE) perform an Inner Join.
# Base R Left Join equivalent
customer_orders_base <- merge(customers, orders, 
                              by = "CustomerID", 
                              all.x = TRUE)

Key differences to note:

  1. Row Order: merge() sorts the result by the key column by default. dplyr preserves the row order of the left dataset (x), which is often desirable for time-series or indexed data. Use sort = FALSE in merge() to replicate dplyr behavior.
  2. Column Names: merge() handles differing key names via by.x and by.y arguments (e.g., by.x = "CustomerID", by.y = "Cust_ID").
  3. Performance: For very large datasets (millions of rows), data.table (discussed below) significantly outperforms both base merge and dplyr joins.

High-Performance Joining with data.table

When datasets exceed available RAM or contain tens of millions of rows, the data.table package offers unparalleled speed and memory efficiency. Which means its syntax is distinct: dt1[dt2, on = . (key), nomatch = 0L] And it works..

  • dt1[dt2, ...]: Looks up dt2 in dt1 (Right Join logic by default).
  • on = .(key): Specifies the join columns.
  • nomatch = 0L: Converts the default right join to an Inner Join (drops non-matches). For a Left Join (keeping all dt1), the syntax flips: dt2[dt1, on = .(key)].
library(data.table)

setDT(customers)
setDT(orders)

# Left Join: Keep all customers
# Syntax: Right_Table[Left_Table, on = .(Key)]
result_dt <- orders[customers, on = .(
Just Came Out

Dropped Recently

Branching Out from Here

Picked Just for You

Thank you for reading about How To Combine Datasets In R. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home