dplyr::bind_rows() / dplyr::bind_cols()
bind_rows(..., .id = NULL) bind_rows() and bind_cols() are dplyr functions for combining data frames. bind_rows() stacks data frames vertically (adding rows), while bind_cols() joins them horizontally (adding columns). These functions are faster than base R alternatives like rbind() and cbind().
Syntax
bind_rows(..., .id = NULL)
bind_cols(...)
Parameters
bind_rows()
| Parameter | Type | Default | Description |
|---|---|---|---|
... | data frames | , | Data frames to combine. Can also be a list of data frames. |
.id | string | NULL | Optional column name to identify the source of each row |
bind_cols()
| Parameter | Type | Default | Description |
|---|---|---|---|
... | data frames | , | Data frames to combine side by side. Must have same number of rows. |
Examples
Basic bind_rows
Stack two data frames:
library(dplyr)
df1 <- data.frame(name = c("Alice", "Bob"), score = c(85, 92))
df2 <- data.frame(name = c("Charlie", "Diana"), score = c(78, 88))
bind_rows(df1, df2)
# name score
# 1 Alice 85
# 2 Bob 92
# 3 Charlie 78
# 4 Diana 88
Using .id to track source
When you combine data frames from different sources, knowing which rows came from which input is often critical for debugging and auditing. The .id argument adds a new column populated with the names of the input data frames. Providing named arguments — like bind_rows(east = df_east, west = df_west, .id = "region") — gives each row a meaningful source label.
df1 <- data.frame(name = "Alice", score = 85)
df2 <- data.frame(name = "Bob", score = 92)
bind_rows(a = df1, b = df2, .id = "source")
# source name score
# 1 a Alice 85
# 2 b Bob 92
Combining data with different columns
When data frames have different column sets, bind_rows() fills missing columns with NA rather than raising an error. Columns are matched by name, not position, so even if the column order differs between data frames, values land in the correct column. This permissive behavior makes bind_rows() a better default than base R’s rbind(), which rejects mismatched column sets outright.
df1 <- data.frame(name = "Alice", score = 85)
df2 <- data.frame(name = "Bob", score = 92, grade = "A")
bind_rows(df1, df2)
# name score grade
# 1 Alice 85 NA
# 2 Bob 92 A
Basic bind_cols
While bind_rows() stacks data vertically, bind_cols() attaches columns side by side by position. There is no key-based alignment — rows are paired solely by their index. This is safe when you have split a data frame into separate column groups and need to reassemble them, or when you construct columns from independent computations that share a consistent row ordering.
df1 <- data.frame(name = c("Alice", "Bob"))
df2 <- data.frame(score = c(85, 92), grade = c("B", "A"))
bind_cols(df1, df2)
# name score grade
# 1 Alice 85 B
# 2 Bob 92 A
Common patterns
The following idioms combine bind_rows() and bind_cols() with other dplyr verbs for everyday data-wrangling tasks. These patterns replace verbose base R equivalents and handle edge cases like mismatched columns without extra error-checking code. Each example below addresses a specific scenario that arises frequently when working with multiple data sources.
Efficiently combining many data frames from a list
# Combine all data frames in a list
df_list <- list(df1, df2, df3, df4)
combined <- bind_rows(df_list)
Appending new data
Instead of manually adding rows one at a time with rbind() or editing the source data, bind_rows() lets you concatenate an existing data set with a batch of new observations. The column names must match for proper alignment. If the new batch has extra columns, they appear with NA in the existing rows; if it lacks columns, those appear as NA in the new rows.
# Add new observations to existing dataset
existing_data <- data.frame(name = c("Alice", "Bob"), score = c(85, 92))
new_data <- data.frame(name = "Charlie", score = 78)
updated <- bind_rows(existing_data, new_data)
Combining summary statistics
When computing separate group summaries and then wanting them in a single table, bind_rows() with .id converts each summary into a row and labels it with the group name. This pattern avoids manually constructing a data frame of results and is especially useful when the number of groups is large or unknown ahead of time.
df <- data.frame(
group = c("A", "A", "B", "B"),
value = c(10, 15, 20, 25)
)
summary_a <- df |> filter(group == "A") |> summarise(mean = mean(value))
summary_b <- df |> filter(group == "B") |> summarise(mean = mean(value))
bind_rows(a = summary_a, b = summary_b, .id = "group")
# group mean
# 1 a 12.5
# 2 b 22.5
dplyr::bind_rows() and bind_cols() in practice
bind_rows() combines data frames vertically, stacking rows. It is more permissive than rbind(): columns present in one data frame but not another are filled with NA. Column matching is by name, not position, so bind_rows(df1, df2) correctly aligns columns even if they appear in different order. The .id argument adds a column with the name of each source: bind_rows(list(a=df1, b=df2), .id="source") creates a source column with values "a" and "b".
bind_cols() combines data frames horizontally, adding columns side by side. Rows must be in the same order — there is no key-based alignment. For key-based alignment, use join functions. bind_cols() is typically used when you have split a data frame into parts and need to reassemble, guaranteeing positional alignment.
bind_rows(list_of_dfs) combines a list of data frames, which is the most common use case: reading multiple files with lapply() and then binding. The tidyverse idiom is map(files, read_csv) |> bind_rows() or map_dfr(files, read_csv) (which calls bind_rows() internally).
Type compatibility: bind_rows() coerces compatible types (integer + double → double) but errors on incompatible ones (numeric + character). If columns have inconsistent types across data frames, use type.convert() or explicit coercion before binding.
bind_rows() stacks data frames vertically, matching by column name and filling mismatches with NA. It is equivalent to rbind() but handles mismatched columns gracefully and works with lists of data frames: bind_rows(list_of_dfs). bind_cols() joins data frames side by side by position — it does not use keys and requires equal row counts. Use left_join() for key-based horizontal merging.