PROC SORT

PROC Steps
Overview

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.

⚠ Behavior Difference

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.

Syntax Comparison
SAS
SAS
/* 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;
R
R
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))
Details

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.

Example 1 — Basic Sort and Multi-Variable SortExample 1

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 Datasets

── 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
SAS Code
SAS
/* 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;
R Code
R
library(dplyr)

# Sort ADSL by USUBJID
ADSL <- ADSL %>% arrange(USUBJID)

# Sort ADLB by USUBJID, LBTESTCD, VISITNUM
ADLB <- ADLB %>% arrange(USUBJID, LBTESTCD, VISITNUM)
Output Dataset

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

Example 2 — Descending Sort and Mixed SortExample 2

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 Datasets

── 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
SAS Code
SAS
/* 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;
R Code
R
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 Dataset

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

Example 3 — NODUPKEY and NODUPExample 3

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 Datasets

── 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
SAS Code
SAS
/* NODUPKEYkeep 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;
R Code
R
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 Dataset

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

Example 4 — OUT= and Sort Within Groups (Clinical Use)Example 4

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 Datasets

── 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
SAS Code
SAS
/* 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;
R Code
R
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 Dataset

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

Watch Out⚠ Common Pitfall

PROC SORT — Common Pitfall

In SAS, any DATA step or PROC that uses a BY statementrequires the dataset to be pre-sorted first.

❌ Problem — BY statement without PROC SORT
SAS
/* 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 */
✅ Fix — always PROC SORT first
SAS
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

Conclusion

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