30-Day Course
Clinical SAS → R
Day 10 of 30·Data Manipulation

Joins & Stacking: from MERGE/SET to dplyr joins & bind_rows()

Combine datasets horizontally and vertically with explicit, key-safe joins.

Learning objectives

  • 1Replace MERGE...BY with left_join(), inner_join(), full_join().
  • 2Replace SET (stacking) with bind_rows().
  • 3Understand how join keys prevent accidental many-to-many merges.
  • 4Use anti_join() / semi_join() for filtering joins.

Key SAS® → R comparison

TaskSAS®R
Match-mergemerge a b; by usubjid;left_join(a, b, by='usubjid')
Inner mergemerge + if a and b;inner_join(a, b, by=...)
Stack datasetsset a b;bind_rows(a, b)
Find non-matchesif a and not b;anti_join(a, b, by=...)

Code examples

Add subject-level vars to a BDS dataset
SAS®
proc sort data=adlb; by usubjid; run;
proc sort data=adsl; by usubjid; run;
data adlb; merge adlb adsl(keep=usubjid trt01p age);
  by usubjid;
run;
R
adlb <- adlb |>
  left_join(
    adsl |> select(usubjid, trt01p, age),
    by = "usubjid"
  )

Note: Joins don't require pre-sorting and won't silently retain unmatched rows the way a careless MERGE can.

Explanation

dplyr joins name the type of join explicitly — left, inner, full, right — so the intent is unambiguous and there is no reliance on a prior PROC SORT. The by argument states the key, and a mismatch in keys raises a clear error rather than a silent Cartesian blow-up.

bind_rows() stacks datasets and aligns columns by name, adding NA where a column is absent, which is safer than SET's positional behavior. anti_join() and semi_join() express the common 'in A but not B' filtering patterns directly.

Practice exercise

Left-join treatment and age from ADSL onto a lab BDS dataset, stack two adverse-event extracts with bind_rows(), and use anti_join() to find subjects in ADSL with no AE records.