rguides

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()

ParameterTypeDefaultDescription
...data frames,Data frames to combine. Can also be a list of data frames.
.idstringNULLOptional column name to identify the source of each row

bind_cols()

ParameterTypeDefaultDescription
...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.

See also