Core SAS pattern

Convert SAS DATA Steps to R

Use SAS2R.ai to start translating DATA step transformations into readable R, then review row order, missing values, and derived-variable behavior against your source data.

Try the SAS to R converter

Example

Convert IF/THEN/ELSE to filter() and mutate()

SAS

data raw_vs;
  input usubjid $ visitnum aval;
  datalines;
01 0 130
01 1 140
01 2 .
01 3 130
02 1 135
02 2 145
;
run;

data analysis;
  set raw_vs;
  length high_bpfl $1;
  if visitnum > 0;
  if aval >= 140 then high_bpfl = 'Y';
  else high_bpfl = 'N';
run;

R

library(dplyr)

raw_vs <- tibble::tribble(
  ~usubjid, ~visitnum, ~aval,
  '01', 0, 130,
  '01', 1, 140,
  '01', 2, NA_real_,
  '01', 3, 130,
  '02', 1, 135,
  '02', 2, 145
)

analysis <- raw_vs %>%
  filter(visitnum > 0) %>%
  mutate(
    high_bpfl = if_else(aval >= 140, 'Y', 'N', missing = 'N')
  )

stopifnot(
  nrow(analysis) == 5L,
  identical(analysis$high_bpfl, c('Y', 'N', 'N', 'N', 'Y'))
)

The synthetic data includes a missing measurement. In SAS, ordinary numeric missing is less than 140, so the ELSE branch assigns N. R's comparison returns NA; missing = 'N' preserves this particular rule. A SAS condition such as aval < 140 needs its own explicit missing-value policy. Always validate the translation against the SAS output and your standards.

Example

Translate BY-group FIRST. and LAST. to grouped row flags

SAS

proc sort data=analysis out=ordered;
  by usubjid visitnum;
run;

data first_last first_obs last_obs;
  set ordered;
  by usubjid;
  first_fl = first.usubjid;
  last_fl = last.usubjid;
  output first_last;
  if first.usubjid then output first_obs;
  if last.usubjid then output last_obs;
run;

R

# Run the first example to create analysis.
first_last <- analysis %>%
  arrange(usubjid, visitnum) %>%
  group_by(usubjid) %>%
  mutate(
    first_fl = as.integer(row_number() == 1L),
    last_fl = as.integer(row_number() == n())
  ) %>%
  ungroup()

first_obs <- first_last %>% filter(first_fl == 1L)
last_obs <- first_last %>% filter(last_fl == 1L)

stopifnot(
  identical(first_obs$visitnum, c(1, 1)),
  identical(last_obs$visitnum, c(3, 2))
)

This example has one record per subject and visit, with no missing sort keys. If visits tie, add the same tie breaker to both sort orders. Multiple SAS BY variables create a hierarchy of FIRST./LAST. flags; group by the corresponding prefix for each boundary. R and SAS also differ in where missing numeric sort keys appear.

Example

Replace a retained running total with grouped cumsum()

SAS

proc sort data=analysis out=ordered;
  by usubjid visitnum;
run;

data cumulative;
  set ordered;
  by usubjid;
  retain running_aval;
  if first.usubjid then running_aval = 0;
  running_aval = sum(running_aval, aval);
run;

R

# Run the first example to create analysis.
cumulative <- analysis %>%
  arrange(usubjid, visitnum) %>%
  group_by(usubjid) %>%
  mutate(running_aval = cumsum(coalesce(aval, 0))) %>%
  ungroup()

stopifnot(
  identical(cumulative$running_aval, c(140, 140, 270, 135, 280))
)

The SAS accumulator starts at zero for each subject and SUM ignores missing measurements. Replacing those measurements with zero before cumsum() reproduces that rule; plain cumsum(aval) would propagate NA. Other RETAIN programs can represent state machines or carry-forward logic and need a different translation.

What to validate after conversion

  • Compare the selected records and derived flags, including observations with missing measurements.
  • Define all BY variables and tie breakers before selecting first or last records or calculating cumulative totals.
  • Review SAS special missing values before replacing them with R NA. The examples below use ordinary numeric missing values only.
  • Check that retained values reset at the same subject boundary in both programs.

Frequently asked questions

How do SAS IF statements map to R?

Simple conditions often map to filter() for row selection and if_else() or case_when() for derived variables. Review type conversion and missing-value behavior rather than relying on a literal line-by-line translation.

What should I review after converting a DATA step?

Compare record counts, key uniqueness, variable types, missingness, and a sample of subject-level records. Check any logic that depends on input order, RETAIN, or BY-group processing separately.

What is the R equivalent of SAS FIRST. and LAST.?

For a single subject boundary, sort explicitly, group by the subject key, and use row_number() == 1 or row_number() == n(). You can also select rows with slice_head() and slice_tail(). Match the SAS BY hierarchy, missing-key order, and tie breakers before comparing outputs.

How do I convert RETAIN and cumulative sums to R?

A sorted, grouped cumsum() can replace a retained running sum when its initialization, missing-value policy, and reset boundary match. It is not a general replacement for RETAIN: carry-forward values and conditional state transitions require separate logic.

Is a SAS DATA step MERGE the same as a dplyr join?

Only some cases map directly. With unique keys, an explicit join may reproduce the intended result. When both inputs have repeated BY keys, SAS match-merging does not produce the same Cartesian combinations as a many-to-many SQL or dplyr join. Check duplicates and expected rows before selecting a translation.

Official documentation