PROC SORT arranges the observations in a SAS dataset by one or more variables. It must be run before any BY-group processing in a DATA step or other PROC. When no OUT= option is specified, PROC SORT modifies the dataset in-place. In R, the equivalent is dplyr::arrange() — which always returns a new object and never modifies the original dataset.
In SAS, PROC SORT physically reorders the observations in the dataset. Sorting is required before any BY statement in a DATA step or PROC — otherwise SAS will produce an error or unexpected results. By default, SAS sorts ascending. Use the DESCENDING keyword before a variable name to reverse order. NODUPKEY removes rows with duplicate BY-variable values, keeping the first occurrence. NODUP removes rows where every column is identical. In R, arrange() from dplyr sorts without modifying the original — the result must always be assigned. desc() inside arrange() reverses the sort order for a specific variable. distinct() replicates NODUPKEY and NODUP behaviour depending on arguments passed.
/* Basic ascending sort */ proc sort data=ADSL; by USUBJID; run; /* Multi-variable sort */ proc sort data=ADLB; by USUBJID LBTESTCD VISITNUM; run; /* Descending sort */ proc sort data=ADSL; by descending AGE; run; /* Mixed — ascending TRT, descending AVAL */ proc sort data=ADLB; by TRT01P descending AVAL; run;
library(dplyr) # Basic ascending sort ADSL <- ADSL %>% arrange(USUBJID) # Multi-variable sort ADLB <- ADLB %>% arrange(USUBJID, LBTESTCD, VISITNUM) # Descending sort ADSL <- ADSL %>% arrange(desc(AGE)) # Mixed — ascending TRT, descending AVAL ADLB <- ADLB %>% arrange(TRT01P, desc(AVAL))
PROC SORT is one of the most frequently used procedures in SAS clinical programming. It is required before any BY-group processing — including DATA step merges, FIRST./LAST. logic, PROC MEANS with CLASS, and PROC FREQ with BY. In R, arrange() is lazy — it returns a sorted copy without touching the original. Always reassign the result back to the dataset name, or to a new name if you need both versions. The pipe operator |> (or %>%) allows chaining arrange() with other dplyr verbs in one step.
Sort ADSL by USUBJID (ascending) — the most common sort used before a DATA step merge. Then sort ADLB by USUBJID, LBTESTCD and VISITNUM to prepare for BY-group processing that will derive BASE and CHG per subject and lab test.
── Input Dataset: ADSL (unsorted) ───────────────── USUBJID AGE SEX TRT01P CDISC003 38 F Drug A CDISC001 45 M Placebo CDISC004 61 F Drug A CDISC002 52 M Placebo ── Input Dataset: ADLB (unsorted) ───────────────── USUBJID LBTESTCD VISITNUM AVAL CDISC002 ALT 2 35 CDISC001 CREAT 1 0.9 CDISC001 ALT 2 30 CDISC002 CREAT 1 1.0 CDISC001 ALT 1 25 CDISC002 ALT 1 28
/* Sort ADSL by USUBJID */ proc sort data=ADSL; by USUBJID; run; /* Sort ADLB by USUBJID, LBTESTCD, VISITNUM */ proc sort data=ADLB; by USUBJID LBTESTCD VISITNUM; run;
library(dplyr) # Sort ADSL by USUBJID ADSL <- ADSL %>% arrange(USUBJID) # Sort ADLB by USUBJID, LBTESTCD, VISITNUM ADLB <- ADLB %>% arrange(USUBJID, LBTESTCD, VISITNUM)
── Output: ADSL sorted by USUBJID ───────────────── USUBJID AGE SEX TRT01P CDISC001 45 M Placebo CDISC002 52 M Placebo CDISC003 38 F Drug A CDISC004 61 F Drug A ── Output: ADLB sorted by USUBJID, LBTESTCD, VISITNUM USUBJID LBTESTCD VISITNUM AVAL CDISC001 ALT 1 25 CDISC001 ALT 2 30 CDISC001 CREAT 1 0.9 CDISC002 ALT 1 28 CDISC002 ALT 2 35 CDISC002 CREAT 1 1.0
📝 Note: ADSL is now in USUBJID order — ready for a DATA step merge with any other dataset sorted by USUBJID. ADLB is sorted by USUBJID first, then LBTESTCD within each subject, then VISITNUM within each test. This order is essential before any DATA step that uses FIRST.LBTESTCD or LAST.LBTESTCD to detect group boundaries for deriving BASE and CHG. The result is the same in both SAS and R.
Sort ADSL by AGE descending to identify the oldest subjects first. Then sort ADLB by TRT01P ascending and AVAL descending — a mixed sort commonly used to rank lab values within each treatment group for a top-N listing or shift table.
── Input Dataset: ADSL ──────────────────────────── USUBJID AGE TRT01P CDISC001 45 Placebo CDISC002 52 Placebo CDISC003 38 Drug A CDISC004 61 Drug A CDISC005 29 Drug A ── Input Dataset: ADLB ──────────────────────────── USUBJID TRT01P LBTESTCD AVAL CDISC001 Placebo ALT 25 CDISC002 Placebo ALT 55 CDISC003 Drug A ALT 18 CDISC004 Drug A ALT 72 CDISC005 Drug A ALT 40
/* Descending sort — oldest subjects first */ proc sort data=ADSL; by descending AGE; run; /* Mixed sort — TRT01P ascending, AVAL descending */ proc sort data=ADLB; by TRT01P descending AVAL; run;
library(dplyr) # Descending sort — oldest subjects first ADSL <- ADSL %>% arrange(desc(AGE)) # Mixed sort — TRT01P ascending, AVAL descending ADLB <- ADLB %>% arrange(TRT01P, desc(AVAL))
── Output: ADSL sorted by AGE descending ────────── USUBJID AGE TRT01P CDISC004 61 Drug A CDISC002 52 Placebo CDISC001 45 Placebo CDISC003 38 Drug A CDISC005 29 Drug A ── Output: ADLB sorted by TRT01P asc, AVAL desc ─── USUBJID TRT01P LBTESTCD AVAL CDISC004 Drug A ALT 72 CDISC005 Drug A ALT 40 CDISC003 Drug A ALT 18 CDISC002 Placebo ALT 55 CDISC001 Placebo ALT 25
📝 Note: ADSL is now sorted from oldest to youngest — CDISC004 (age 61) appears first. ADLB is sorted by TRT01P alphabetically (Drug A before Placebo), then within each treatment group the highest AVAL appears first. This ordering is useful for generating ranked lab listings or identifying subjects with the most extreme values within each treatment arm. In SAS, DESCENDING is placed before each variable that needs descending order. In R, each variable is wrapped individually in desc(). The result is the same in both SAS and R.
NODUPKEY removes rows with duplicate BY-variable values — keeping only the first occurrence. This is commonly used to deduplicate ADSL to one record per subject, or to get one record per subject per visit. NODUP removes rows where every single column is identical — a stricter check. Both are demonstrated below with clinical dataset examples.
── Input Dataset: ADSL with duplicate USUBJIDs ──── USUBJID AGE TRT01P SITEID CDISC001 45 Drug A 001 CDISC001 45 Drug A 001 ← exact duplicate CDISC002 52 Placebo 002 CDISC003 38 Drug A 001 CDISC003 38 Drug A 003 ← same USUBJID, different SITEID ── Input Dataset: AE with exact duplicate rows ──── USUBJID AESEQ AETERM AESTDTC CDISC001 1 HEADACHE 2023-01-05 CDISC001 1 HEADACHE 2023-01-05 ← exact duplicate CDISC002 1 NAUSEA 2023-02-03 CDISC002 2 DIZZINESS 2023-03-10 CDISC002 2 DIZZINESS 2023-03-10 ← exact duplicate
/* NODUPKEY — keep first record per USUBJID */ proc sort data=ADSL nodupkey; by USUBJID; run; /* NODUP — remove only fully identical rows */ proc sort data=AE nodup; by USUBJID AESEQ; run;
library(dplyr) # NODUPKEY equivalent # arrange first to control which row is kept ADSL <- ADSL %>% arrange(USUBJID) %>% distinct(USUBJID, .keep_all = TRUE) # NODUP equivalent — remove fully identical rows AE <- AE %>% distinct()
── Output: ADSL after NODUPKEY ──────────────────── USUBJID AGE TRT01P SITEID CDISC001 45 Drug A 001 ← first kept, duplicate dropped CDISC002 52 Placebo 002 CDISC003 38 Drug A 001 ← first kept (SITEID 003 row dropped) ── Output: AE after NODUP ───────────────────────── USUBJID AESEQ AETERM AESTDTC CDISC001 1 HEADACHE 2023-01-05 ← exact dup removed CDISC002 1 NAUSEA 2023-02-03 CDISC002 2 DIZZINESS 2023-03-10 ← exact dup removed
📝 Note: NODUPKEY for ADSL: CDISC001 had two identical rows — the second is dropped. CDISC003 had two rows with the same USUBJID but different SITEID — NODUPKEY keeps only the first occurrence (SITEID 001) and drops SITEID 003 because the BY variable USUBJID alone determines the duplicate, not other columns. NODUP for AE: only rows where every single column matches are removed. CDISC001 AESEQ=1 had an exact duplicate row — removed. CDISC002 AESEQ=2 had an exact duplicate row — removed. In R: distinct(USUBJID, .keep_all=TRUE) matches NODUPKEY. distinct() with no arguments matches NODUP. Always arrange() before distinct() in R to control which row is kept.
OUT= saves the sorted result to a new dataset without modifying the original — equivalent to assigning to a new name in R. Sort within groups is used to rank subjects within each treatment group by lab value — a common step before FIRST./LAST. processing in a DATA step. This example also shows the complete clinical pattern: sort → DATA step FIRST./LAST. → derive per-subject flag.
── Input Dataset: ADLB ──────────────────────────── USUBJID TRT01P LBTESTCD VISITNUM AVAL ABLFL CDISC003 Drug A ALT 2 45 CDISC001 Placebo ALT 1 25 Y CDISC003 Drug A ALT 1 30 Y CDISC002 Placebo ALT 2 55 CDISC004 Drug A ALT 2 72 CDISC001 Placebo ALT 2 38 CDISC004 Drug A ALT 1 60 Y CDISC002 Placebo ALT 1 40 Y
/* OUT= — save sorted copy, keep original intact */ proc sort data=ADLB out=ADLB_SORTED; by USUBJID LBTESTCD VISITNUM; run; /* Sort within groups — TRT01P asc, AVAL desc */ /* Used to rank subjects by lab value per treatment */ proc sort data=ADLB out=ADLB_RANKED; by TRT01P descending AVAL; run; /* Full clinical pattern — sort then FIRST./LAST. */ /* Derive BASEFL: flag baseline record per subject */ proc sort data=ADLB out=ADLB_PREP; by USUBJID LBTESTCD VISITNUM; run; data ADLB_FINAL; set ADLB_PREP; by USUBJID LBTESTCD; if first.LBTESTCD then RANK = 0; RANK + 1; run;
library(dplyr) # OUT= equivalent — assign to new name ADLB_SORTED <- ADLB %>% arrange(USUBJID, LBTESTCD, VISITNUM) # Sort within groups — TRT01P asc, AVAL desc ADLB_RANKED <- ADLB %>% arrange(TRT01P, desc(AVAL)) # Full clinical pattern — sort + rank within group # Equivalent of sort + FIRST./LAST. DATA step ADLB_FINAL <- ADLB %>% arrange(USUBJID, LBTESTCD, VISITNUM) %>% group_by(USUBJID, LBTESTCD) %>% mutate(RANK = row_number()) %>% ungroup()
── Output: ADLB_SORTED (by USUBJID, LBTESTCD, VISITNUM) USUBJID TRT01P LBTESTCD VISITNUM AVAL ABLFL CDISC001 Placebo ALT 1 25 Y CDISC001 Placebo ALT 2 38 CDISC002 Placebo ALT 1 40 Y CDISC002 Placebo ALT 2 55 CDISC003 Drug A ALT 1 30 Y CDISC003 Drug A ALT 2 45 CDISC004 Drug A ALT 1 60 Y CDISC004 Drug A ALT 2 72 ── Output: ADLB_RANKED (TRT01P asc, AVAL desc) ──── USUBJID TRT01P LBTESTCD VISITNUM AVAL CDISC004 Drug A ALT 2 72 CDISC004 Drug A ALT 1 60 CDISC003 Drug A ALT 2 45 CDISC003 Drug A ALT 1 30 CDISC002 Placebo ALT 2 55 CDISC002 Placebo ALT 1 40 CDISC001 Placebo ALT 2 38 CDISC001 Placebo ALT 1 25 ── Output: ADLB_FINAL (with RANK within USUBJID+LBTESTCD) USUBJID TRT01P LBTESTCD VISITNUM AVAL ABLFL RANK CDISC001 Placebo ALT 1 25 Y 1 CDISC001 Placebo ALT 2 38 2 CDISC002 Placebo ALT 1 40 Y 1 CDISC002 Placebo ALT 2 55 2 CDISC003 Drug A ALT 1 30 Y 1 CDISC003 Drug A ALT 2 45 2 CDISC004 Drug A ALT 1 60 Y 1 CDISC004 Drug A ALT 2 72 2
📝 Note: ADLB_SORTED: the original ADLB is unchanged — OUT= saves the sorted copy separately. In R, assigning to a new name (ADLB_SORTED <-) is the direct equivalent of OUT=. ADLB_RANKED: within Drug A, CDISC004 (AVAL=72) ranks first, CDISC003 (AVAL=30) last. Within Placebo, CDISC002 (AVAL=55) ranks first, CDISC001 (AVAL=25) last. ADLB_FINAL: RANK=1 marks the first visit per subject per lab test — which is the baseline record (ABLFL=Y) when the data is sorted by VISITNUM. In SAS, FIRST.LBTESTCD resets the counter at each new subject-test group. In R, group_by(USUBJID, LBTESTCD) combined with row_number() achieves the same result. Always call ungroup() after mutate() to avoid unexpected grouped behaviour downstream.
In SAS, any DATA step or PROC that uses a BY statementrequires the dataset to be pre-sorted first.
/* Dataset is NOT sorted by USUBJID LBTESTCD */ data ADLB_FINAL; set ADLB; by USUBJID LBTESTCD; if first.LBTESTCD then RANK = 0; RANK + 1; run; /* SAS Log ERROR: ERROR: BY variables are not properly sorted */
proc sort data=ADLB; by USUBJID LBTESTCD VISITNUM; run; data ADLB_FINAL; set ADLB; by USUBJID LBTESTCD; if first.LBTESTCD then RANK = 0; RANK + 1; run;
Key Takeaway: • In SAS — always PROC SORT before any BY statement • PROC TRANSPOSE with BY also requires sorted data • In R — pivot_wider() and pivot_longer() do not require sorting
Key Takeaway: • PROC SORT must run before any BY-group processing in SAS — DATA step, PROC MEANS, PROC FREQ • In R, arrange() never modifies the original — always reassign the result • SAS uses DESCENDING keyword before the variable — R wraps it in desc() • NODUPKEY → distinct(key_col, .keep_all = TRUE) — duplicates by key variable only • NODUP → distinct() — duplicates only when every column is identical • OUT= in SAS → assign to a new name in R to preserve the original • FIRST./LAST. in DATA step requires a prior PROC SORT — in R use group_by() + row_number() • Always arrange() before distinct() in R to control which row is kept