MERGE is used to combine observations from two or more datasets horizontally (adding columns/variables from different sources). The behaviour depends on the relationship between the datasets based on the BY variable(s).
SAS merge in DATA step shows max(Rows_left, Rows_right) and the log will have the note "merge by repeats" if the rows are not unique as per the BY variables. Whereas R performs a full join and gives a Cartesian product, and also throws a warning "Detected an unexpected many-to-many relationship between x and y" unless the relationship is specified as many-to-many. Also, R merges without sorting the datasets by the BY variables.
proc sort data=LEFT; by SUBJID; run; proc sort data=RIGHT; by SUBJID; run; data FULLJOIN; merge LEFT RIGHT; by SUBJID; run;
library(dplyr) FULLJOIN <- full_join(LEFT, RIGHT, by = "SUBJID")
Full Join is used when you need to retain all records from both datasets, regardless of whether matching records exist. It returns all observations from both tables, combining matched records where possible and displaying missing values when no match is found. In clinical programming, it is commonly used when merging two different datasets to ensure that all records from both sources are preserved.
We have 2 datasets: DM (Demographics) on the left and AE (Adverse Events) on the right. To calculate the duration of AEs from the start of treatment, we merge both datasets and derive 2 new columns: AESTDY and AEENDY.
── Input Dataset: DM ────────────────────────────── SUBJID RFSTDTC 1015 2013-12-01 1023 2012-05-01 1028 2013-06-01 ── Input Dataset: AE ────────────────────────────── SUBJID AETERM AESTDTC AEENDTC 1015 APPLICATION SITE ERYTHEMA 2014-01-03 . 1015 APPLICATION SITE PRURITUS 2014-01-03 . 1015 DIARRHOEA 2014-01-09 2014-01-11 1023 ERYTHEMA 2012-08-07 . 1023 ERYTHEMA 2012-08-07 2012-08-30 1028 APPLICATION SITE ERYTHEMA 2013-07-21 . 1028 APPLICATION SITE PRURITUS 2013-08-08 .
proc sort data=DM; by SUBJID; run; proc sort data=AE; by SUBJID; run; data ADAE; merge DE AE; by SUBJID; if AESTDTC not in ('' ' ') and RFSTDTC not in ('' ' ') then AESTDY = input(AESTDTC, yymmdd10.) - input(RFSTDTC, yymmdd10.) + 1; if AEENDTC not in ('' ' ') and RFSTDTC not in ('' ' ') then AEENDY = input(AEENDTC, yymmdd10.) - input(RFSTDTC, yymmdd10.) + 1; run;
library(dplyr) ADAE <- full_join(DEMO, ADAE_, by = "SUBJID") %>% mutate( AESTDY = case_when( !is.na(AESTDTC) & !is.na(RFSTDTC) ~ as.numeric(as.Date(AESTDTC) - as.Date(RFSTDTC)) + 1 ), AEENDY = case_when( !is.na(AEENDTC) & !is.na(RFSTDTC) ~ as.numeric(as.Date(AEENDTC) - as.Date(RFSTDTC)) + 1 ) )
SUBJID RFSTDTC AETERM AESTDTC AEENDTC AESTDY AEENDY 1015 2013-12-01 APPLICATION SITE ERYTHEMA 2014-01-03 . 34 . 1015 2013-12-01 APPLICATION SITE PRURITUS 2014-01-03 . 34 . 1015 2013-12-01 DIARRHOEA 2014-01-09 2014-01-11 40 42 1023 2012-05-01 ERYTHEMA 2012-08-07 . 99 . 1023 2012-05-01 ERYTHEMA 2012-08-07 2012-08-30 99 122 1028 2013-06-01 APPLICATION SITE ERYTHEMA 2013-07-21 . 51 . 1028 2013-06-01 APPLICATION SITE PRURITUS 2013-08-08 . 69 .
📝 Note: The output dataset is the same in both SAS and R. In SAS, the input() function converts character dates to numeric before calculating the day difference. In R, as.Date() converts the character date and as.numeric() converts the result. Both produce identical AESTDY and AEENDY values.
This example shows the key difference between SAS and R when merging datasets that have multiple observations for the same subject. Datasets AE and CM are merged by SUBJID.
── Input Dataset: AE ────────────────────────────── SUBJID AETERM AESTDTC AEENDTC 1015 APPLICATION SITE ERYTHEMA 2014-01-03 . 1015 APPLICATION SITE PRURITUS 2014-01-03 . 1015 DIARRHOEA 2014-01-09 2014-01-11 1023 ERYTHEMA 2012-08-07 . 1023 ERYTHEMA 2012-08-07 2012-08-30 1023 ERYTHEMA 2012-08-07 2012-08-30 1023 ATRIOVENTRICULAR BLOCK 2012-08-26 . 1028 APPLICATION SITE ERYTHEMA 2013-07-21 . 1028 APPLICATION SITE PRURITUS 2013-08-08 . ── Input Dataset: CM ────────────────────────────── SUBJID CMTRT CMSTDTC 1015 PARACETAMOL 2013-12-15 1015 IBUPROFEN 2014-01-02 1023 METFORMIN 2012-05-10 1023 ATORVASTATIN 2012-07-20 1028 OMEPRAZOLE 2013-06-01 1028 SALBUTAMOL 2013-07-15 1033 AMLODIPINE 2014-02-05 1033 LOSARTAN 2014-03-10 1057 LEVOTHYROXINE 2015-01-20 1057 VITAMIN D 2015-04-01
data AECM; merge AE CM; by SUBJID; run; /* To get Cartesian product like R, use PROC SQL */ proc sql; create table merged_cartesian as select COALESCE(AE.SUBJID, CM.SUBJID) as SUBJID, AE.AETERM, AE.AESTDTC, AE.AEENDTC, CM.CMTRT, CM.CMSTDTC from AE full join CM on AE.SUBJID = CM.SUBJID; quit;
library(dplyr) # Basic full join (throws many-to-many warning) AECM <- full_join(AE, CM, by = "SUBJID") # Suppress warning by specifying relationship AECM <- full_join(AE, CM, by = "SUBJID", relationship = "many-to-many")
── SAS Output — max(rows_left, rows_right) per subject ──── SUBJID AETERM AESTDTC AEENDTC CMTRT CMSTDTC 1015 APPLICATION SITE ERYTHEMA 2014-01-03 . PARACETAMOL 2013-12-15 1015 APPLICATION SITE PRURITUS 2014-01-03 . IBUPROFEN 2014-01-02 1015 DIARRHOEA 2014-01-09 2014-01-11 IBUPROFEN 2014-01-02 1023 ERYTHEMA 2012-08-07 . METFORMIN 2012-05-10 1023 ERYTHEMA 2012-08-07 2012-08-30 ATORVASTATIN 2012-07-20 1023 ERYTHEMA 2012-08-07 2012-08-30 ATORVASTATIN 2012-07-20 1023 ATRIOVENTRICULAR BLOCK 2012-08-26 . ATORVASTATIN 2012-07-20 1028 APPLICATION SITE ERYTHEMA 2013-07-21 . OMEPRAZOLE 2013-06-01 1028 APPLICATION SITE PRURITUS 2013-08-08 . SALBUTAMOL 2013-07-15 1033 . . . AMLODIPINE 2014-02-05 1033 . . . LOSARTAN 2014-03-10 1057 . . . LEVOTHYROXINE 2015-01-20 1057 . . . VITAMIN D 2015-04-01 ── R Output — Full Cartesian product (22 rows) ──────────── SUBJID AETERM AESTDTC AEENDTC CMTRT CMSTDTC 1015 APPLICATION SITE ERYTHEMA 2014-01-03 . PARACETAMOL 2013-12-15 1015 APPLICATION SITE ERYTHEMA 2014-01-03 . IBUPROFEN 2014-01-02 1015 APPLICATION SITE PRURITUS 2014-01-03 . PARACETAMOL 2013-12-15 1015 APPLICATION SITE PRURITUS 2014-01-03 . IBUPROFEN 2014-01-02 1015 DIARRHOEA 2014-01-09 2014-01-11 PARACETAMOL 2013-12-15 1015 DIARRHOEA 2014-01-09 2014-01-11 IBUPROFEN 2014-01-02 1023 ERYTHEMA 2012-08-07 . METFORMIN 2012-05-10 1023 ERYTHEMA 2012-08-07 . ATORVASTATIN 2012-07-20 1023 ERYTHEMA 2012-08-07 2012-08-30 METFORMIN 2012-05-10 1023 ERYTHEMA 2012-08-07 2012-08-30 ATORVASTATIN 2012-07-20 1023 ERYTHEMA 2012-08-07 2012-08-30 METFORMIN 2012-05-10 1023 ERYTHEMA 2012-08-07 2012-08-30 ATORVASTATIN 2012-07-20 1023 ATRIOVENTRICULAR BLOCK 2012-08-26 . METFORMIN 2012-05-10 1023 ATRIOVENTRICULAR BLOCK 2012-08-26 . ATORVASTATIN 2012-07-20 1028 APPLICATION SITE ERYTHEMA 2013-07-21 . OMEPRAZOLE 2013-06-01 1028 APPLICATION SITE ERYTHEMA 2013-07-21 . SALBUTAMOL 2013-07-15 1028 APPLICATION SITE PRURITUS 2013-08-08 . OMEPRAZOLE 2013-06-01 1028 APPLICATION SITE PRURITUS 2013-08-08 . SALBUTAMOL 2013-07-15 1033 . . . AMLODIPINE 2014-02-05 1033 . . . LOSARTAN 2014-03-10 1057 . . . LEVOTHYROXINE 2015-01-20 1057 . . . VITAMIN D 2015-04-01
📝 Note: SAS produces max(rows_left, rows_right) = 3 rows for SUBJID 1015 and logs: NOTE: MERGE statement has more than one data set with repeats of BY values. R produces a Cartesian product = 3 x 2 = 6 rows for SUBJID 1015 and throws a warning.
Key Takeaway: • SAS DATA step MERGE → gives max rows per subject (sequential pairing) • R full_join() → gives Cartesian product (all combinations) • To get the same Cartesian result as R in SAS → use PROC SQL full join • Always check the SAS log for the MERGE repeat note when working with non-unique BY values