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 converterExample
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
Related SAS to R resources
Core SAS patterns
Convert SAS PROC SQL to R
Translate common PROC SQL joins, filters, and summaries into clear R data pipelines, then validate key uniqueness, join cardinality, and record counts before using the result.
Core SAS patterns
Convert SAS Macros to R Functions
Move SAS macro variables and repeatable macro logic toward explicit R functions and parameters, giving teams a clearer path to testable, reviewable migration code.