dplyr bind_cols() in R: Combine Tables Side by Side
The bind_cols() function in dplyr combines data frames or vectors SIDE BY SIDE (horizontally), matching by row position. It is the safe, type-checking dplyr alternative to base R cbind().
bind_cols(df1, df2) # combine by position bind_cols(df1, df2, df3) # multiple inputs bind_cols(df1, df2, .name_repair = "unique") # auto-rename clashes bind_cols(df, new_col = c(1,2,3)) # add a named column cbind(df1, df2) # base R alternative (less safe) left_join(df1, df2, by = "id") # different: JOIN by KEY
Need explanation? Read on for examples and pitfalls.
What bind_cols() does in one sentence
bind_cols(...) glues data frames or vectors side by side by ROW POSITION; all inputs MUST have the same number of rows. Unlike joins, there is no key matching; row 1 of input A is paired with row 1 of input B regardless of any id column.
This is the "concatenate columns" function. Useful when you have parallel computations or sourced columns that share row order.
Syntax
bind_cols(..., .name_repair = "unique"). All inputs must have the same number of rows (or be length 1, recycled).
bind_cols only when you are SURE the row order matches. It does NOT validate that rows correspond to the same entity. For ID-based merging, use a join.Five common patterns
1. Combine two data frames
2. Add a vector as a new column
For one new column, mutate(df, new_col = ...) is more idiomatic.
3. Combine many data frames
4. Resolve name conflicts
unique appends ...n suffixes; universal is similar; minimal keeps duplicates as-is.
5. Bind a list of data frames
For purrr users, purrr::list_cbind(list_of_dfs) is more direct.
bind_cols is POSITIONAL; left_join is KEY-BASED. bind_cols pairs row 1 with row 1, row 2 with row 2, etc., regardless of any id column. left_join matches by id. Use bind_cols when row order is meaningful and known; use a join when matching by id.bind_cols() vs cbind() vs left_join() vs mutate()
Four ways to add columns to a data frame in R.
| Function | Match by | Type-strict | Best for |
|---|---|---|---|
bind_cols() |
Position | Yes | dplyr; column glue |
base::cbind() |
Position | No | Base R; flexible but less safe |
left_join(by = "id") |
Key | Yes | Match by id |
mutate() |
(compute) | Yes | Add ONE computed column |
When to use which:
bind_colsfor parallel/sourced columns with matching row order.cbindfor quick base R; can mix matrix and df.left_joinwhen matching by id is needed.mutatefor computed columns.
A practical workflow
The "results column" pattern is the bind_cols sweet spot.
Append model predictions as a new column. Position-matching works because predict returns one value per input row in the same order.
Common pitfalls
Pitfall 1: row count mismatch errors. bind_cols(df_a, df_b) errors if df_a has 3 rows and df_b has 5 rows. Use a join when rows don't align positionally.
Pitfall 2: silent ordering bugs. bind_cols won't catch if df_a's row 3 is for "Alice" but df_b's row 3 is for "Bob". It will pair them anyway. Always verify row alignment first.
bind_cols does NOT match by id; it matches by POSITION. This is dangerous if you don't trust the row order. For id-based combination, ALWAYS use left_join or inner_join.Try it yourself
Try it: Add a column total (mpg + hp/10) to mtcars using bind_cols. Save to ex_total.
Click to reveal solution
Explanation: bind_cols appends the totals vector as a new column. For derived columns, mutate is more direct.
Related dplyr functions
After mastering bind_cols, look at:
bind_rows(): stack verticallyleft_join()/inner_join(): match by idmutate(): add computed columnspurrr::list_cbind(): bind a list of data framesbase::cbind(): base R; less type-stricttibble::add_column(): add at a specific position
For "add ONE column", mutate() or tibble::add_column() is cleaner than bind_cols.
FAQ
What does bind_cols do in dplyr?
bind_cols(df1, df2) combines data frames side by side by row position. All inputs must have the same number of rows.
What is the difference between bind_cols and cbind in R?
bind_cols is the dplyr version: type-strict, validates row counts, returns a tibble. cbind is base R: more lenient, can return matrix or df depending on inputs.
Should I use bind_cols or left_join?
Use bind_cols when the row order is meaningful and matching (e.g., model predictions for the same input). Use left_join when matching by id.
How do I handle name conflicts in bind_cols?
Pass .name_repair = "unique" to auto-rename duplicates. Or rename one input first: bind_cols(df_a, df_b |> rename(b_x = x)).
Can bind_cols accept vectors?
Yes. bind_cols(df, new_col = c(1,2,3)) adds a named vector as a new column. Length must match the data frame's row count.