Introduction
For decades, SAS has been one of the dominant programming languages and platforms used for clinical-trial data processing, statistical analysis, and regulatory submissions.
At the same time, R has become an increasingly important language for clinical statistics, data science, visualization, reporting, simulation, and statistical programming.
This creates an important practical question for clinical programmers:
The answer is generally yes.
But learning R effectively requires understanding that R is not simply "SAS with different syntax."
The two ecosystems have different programming models, data structures, development workflows, statistical libraries, and approaches to reproducibility.
R and SAS at a Glance
| Area | SAS | R |
|---|---|---|
| Primary model | Procedure-oriented programming | Object/function-oriented programming |
| Data manipulation | DATA step, procedures, PROC SQL | Base R, dplyr, data.table |
| Statistical procedures | PROC GLM, PROC MIXED, PROC PHREG, etc. | Functions and specialized packages |
| Visualization | ODS Graphics and procedures | ggplot2 and related packages |
| Reusable code | Macros, autocall macros, include files | Functions, packages, scripts |
| Package ecosystem | Curated SAS procedures and products | Very large open-source package ecosystem |
| Cost model | Commercial licensing | Open-source R itself |
| Clinical-trial legacy | Extensive | Rapidly growing |
| Regulatory familiarity | Extensive industry history | Increasing adoption and validation frameworks |
| Interactive visualization | Available through SAS ecosystem | Very extensive |
The Most Important Conceptual Difference
A traditional SAS program often reads like a sequence of instructions:
DATA adsl;
SET dm;
WHERE SAFFL = "Y";
AGEGR1 = "65 years and older";
RUN;
PROC SORT DATA=adsl;
BY TRT01P USUBJID;
RUN;
An R workflow is more naturally expressed as a series of transformations applied to objects.
adsl <- dm |>
dplyr::filter(SAFFL == "Y") |>
dplyr::mutate(
AGEGR1 = "65 years and older"
) |>
dplyr::arrange(TRT01P, USUBJID)
The difference is more than cosmetic.
In SAS, the programmer frequently thinks in terms of:
- DATA steps
- Procedures
- Input and output datasets
- BY-group processing
- Macro variables
- Formats and informats
In R, the programmer frequently thinks in terms of:
- Objects
- Functions
- Data frames
- Vectors
- Pipelines
- Packages
- Environments
A Visual Comparison of the Two Programming Models
The illustration is conceptual rather than a statement that every SAS or R program follows exactly this pattern.
The SAS DATA Step
The SAS DATA step is one of the most important concepts for clinical programmers.
A typical DATA step reads observations, performs calculations or conditional logic, and writes a new SAS dataset.
data adsl;
set dm;
if saffl = "Y";
agegr1 = "65 years and older";
if age < 65 then
agegr1 = "<65 years";
run;
The equivalent R implementation might use dplyr.
adsl <- dm |>
dplyr::filter(SAFFL == "Y") |>
dplyr::mutate(
AGEGR1 = dplyr::if_else(
AGE < 65,
"<65 years",
"65 years and older"
)
)
The conceptual mapping is:
| SAS | R |
|---|---|
SET |
Data-frame object / input to transformation |
IF |
filter(), if_else(), case_when() |
| Assignment | mutate() |
| DROP | select(-variable) |
| KEEP | select(variable) |
| RENAME | rename() |
| WHERE | filter() |
| OUTPUT | Assignment such as result <- ... |
Conditional Logic
Conditional logic is one of the easiest areas for a SAS programmer to recognize in R.
SAS IF/THEN/ELSE
data adsl;
set dm;
if age < 18 then
agegrp = "Pediatric";
else if age < 65 then
agegrp = "Adult";
else
agegrp = "Older Adult";
run;
R case_when()
adsl <- dm |>
dplyr::mutate(
agegrp = dplyr::case_when(
AGE < 18 ~ "Pediatric",
AGE < 65 ~ "Adult",
TRUE ~ "Older Adult"
)
)
For multiple conditions, case_when() is often
the most readable equivalent to a long sequence of SAS conditional
statements.
WHERE Versus IF
SAS programmers should pay close attention to the distinction between WHERE and IF.
data adsl;
set dm;
where saffl = "Y";
run;
In R, a filtering operation is usually explicit.
adsl <- dm |> dplyr::filter(SAFFL == "Y")
This is one of the first places where a direct syntax translation is easy.
SAS PROC SORT vs. R arrange()
Sorting is a fundamental SAS operation.
proc sort data=adsl;
by TRT01P USUBJID;
run;
The corresponding dplyr code is:
adsl <- adsl |> dplyr::arrange(TRT01P, USUBJID)
arrange() returns a data frame
ordered according to the requested variables. R does not use SAS-style
sorted BY-group metadata in the same way.
BY-Group Processing
BY-group processing is central to many SAS clinical-programming workflows.
proc sort data=adae;
by usubjid aestdt;
run;
data adae2;
set adae;
by usubjid aestdt;
if first.usubjid then
n_event = 0;
n_event + 1;
run;
In R, grouped operations are usually expressed explicitly.
adae2 <- adae |>
dplyr::arrange(USUBJID, AESTDT) |>
dplyr::group_by(USUBJID) |>
dplyr::mutate(
n_event = dplyr::row_number()
) |>
dplyr::ungroup()
This is a major conceptual transition for SAS programmers.
Instead of thinking:
think:
FIRST. and LAST. Variables
The SAS automatic variables FIRST.variable and LAST.variable are particularly common in
clinical programming.
data firstae;
set adae;
by usubjid aestdt;
if first.usubjid;
run;
A common R equivalent is to use ranking or slicing operations.
firstae <- adae |> dplyr::arrange(USUBJID, AESTDT) |> dplyr::group_by(USUBJID) |> dplyr::slice_head(n = 1) |> dplyr::ungroup()
The important lesson is that there is usually not a one-to-one R replacement for every SAS automatic variable. R provides several higher-level ways to express the same analytical intent.
PROC SQL vs. R Joins
SAS programmers frequently use PROC SQL to merge clinical datasets.
proc sql;
create table adsl_adae as
select a.*,
b.aeterm,
b.aestdt
from adsl as a
left join adae as b
on a.usubjid = b.usubjid;
quit;
In R:
adsl_adae <- adsl |>
dplyr::left_join(
adae |>
dplyr::select(
USUBJID,
AETERM,
AESTDT
),
by = "USUBJID"
)
The most common join mappings are:
| SAS / SQL | R / dplyr |
|---|---|
| INNER JOIN | inner_join() |
| LEFT JOIN | left_join() |
| RIGHT JOIN | right_join() |
| FULL JOIN | full_join() |
| WHERE | filter() |
| SELECT | select() |
| GROUP BY | group_by() |
| ORDER BY | arrange() |
Many-to-Many Joins Require Special Attention
One of the most important clinical-programming risks when moving between SAS and R is accidental row multiplication.
Suppose:
- ADSL contains one record per subject.
- ADAE contains multiple adverse events per subject.
A subject-level-to-event-level join naturally creates multiple rows per subject.
That may be correct.
But if both input datasets contain multiple records per subject, an unintended many-to-many join can multiply records dramatically.
PROC TRANSPOSE vs. pivot_longer() and pivot_wider()
SAS programmers frequently use PROC TRANSPOSE
to convert between wide and long structures.
SAS
proc transpose data=lab
out=lab_long;
by usubjid;
var ALT AST BILI;
run;
In R, pivot_longer() performs a similar
wide-to-long transformation.
lab_long <- lab |>
tidyr::pivot_longer(
cols = c(ALT, AST, BILI),
names_to = "PARAM",
values_to = "AVAL"
)
The opposite operation is:
lab_wide <- lab_long |>
tidyr::pivot_wider(
names_from = PARAM,
values_from = AVAL
)
pivot_longer() and pivot_wider() much easier to understand.
PROC FREQ vs. count() and summarise()
A basic SAS frequency table might be:
proc freq data=adsl;
tables sex*trt01p;
run;
In R:
adsl |>
dplyr::count(
SEX,
TRT01P
)
For percentages:
adsl |>
dplyr::count(
TRT01P,
SEX
) |>
dplyr::group_by(TRT01P) |>
dplyr::mutate(
PCT = n / sum(n) * 100
) |>
dplyr::ungroup()
Clinical programmers should remember that producing a simple count is much easier than producing a fully formatted regulatory table. The latter requires additional decisions about denominators, missing categories, zero-count categories, ordering, labels, rounding, and presentation.
PROC MEANS vs. summarise()
SAS:
proc means data=adsl
n mean std median min max;
var age;
run;
R:
adsl |>
dplyr::summarise(
N = sum(!is.na(AGE)),
MEAN = mean(AGE, na.rm = TRUE),
SD = sd(AGE, na.rm = TRUE),
MEDIAN = median(AGE, na.rm = TRUE),
MIN = min(AGE, na.rm = TRUE),
MAX = max(AGE, na.rm = TRUE)
)
The important difference is that R's missing-value behavior often has to be specified explicitly.
Missing Values: A Critical Difference
SAS and R both support missing values, but they do not behave identically.
In SAS, a numeric missing value is represented by:
.
In R, the standard missing value is:
NA
SAS programmers should be particularly careful with logical comparisons.
SAS
if age ne . then
include = "Y";
R
include <- !is.na(AGE)
Do not write:
AGE != NA
to test for nonmissing values.
The correct R pattern is:
!is.na(AGE)
is.na() and !is.na() when explicitly testing for missing
values.
Missing Values in Statistical Functions
Another important difference is how R functions treat missing values.
mean(x)
returns NA if the vector contains missing values.
The usual clinical-programming form is:
mean(x, na.rm = TRUE)
Similarly:
median(x, na.rm = TRUE) sd(x, na.rm = TRUE) sum(x, na.rm = TRUE)
This is analogous to the way many SAS summary procedures exclude missing numeric values from calculations by default.
Character and Numeric Variables
SAS distinguishes numeric and character variables at the dataset level.
R also has different vector types, but R's data structures are more flexible.
| Concept | SAS | R |
|---|---|---|
| Numeric | Numeric variable | Numeric vector |
| Character | Character variable | Character vector |
| Date | Numeric value with DATE format | Date-class object |
| Date-time | Numeric datetime value | POSIXct / POSIXlt |
| Logical | Often represented through numeric/character conventions | TRUE / FALSE |
| Missing | . / blank |
NA |
SAS Dates vs. R Dates
SAS dates are stored internally as the number of days relative to a reference date.
R's Date class similarly supports arithmetic
on dates, but the programming syntax differs.
SAS
AESTDY = AESTDT - RFSTDTC;
R
AESTDY <- as.Date(AESTDT) -
as.Date(RFSTDTC)
In practice, clinical R programming often uses packages such as lubridate to simplify date and datetime
operations.
Character Dates in Clinical Data
CDISC-oriented datasets frequently contain ISO 8601 character dates such as:
"2026-04-15" "2026-04-15T14:30" "2026-04-15T14:30:00"
R programmers need to explicitly parse these values into date or datetime objects when performing arithmetic.
date_value <- as.Date( "2026-04-15" )
For more complex ISO 8601 values:
lubridate::ymd_hms( "2026-04-15T14:30:00" )
Formats and Informats
SAS formats and informats are an important concept that does not have a perfect one-to-one equivalent in base R.
For example:
format AEDTM datetime20.;
changes how SAS displays the underlying value.
In R, the underlying object and its printed representation are more commonly controlled through classes and formatting functions.
format( date_value, "%d%b%Y" )
For clinical reporting, it is often useful to separate:
- The underlying analysis variable
- The human-readable display value
- The table-formatting logic
User-Defined Formats
SAS formats are heavily used in clinical TLF programming.
proc format;
value trtfmt
1 = "Placebo"
2 = "Treatment A"
3 = "Treatment B";
run;
In R, equivalent labeling can be implemented using named vectors or factor levels.
trt_labels <- c(
"1" = "Placebo",
"2" = "Treatment A",
"3" = "Treatment B"
)
adsl <- adsl |>
dplyr::mutate(
TRT_LABEL = unname(
trt_labels[as.character(TRT01N)]
)
)
Factors can also encode categorical levels and ordering.
adsl$TRT_LABEL <- factor(
adsl$TRT_LABEL,
levels = c(
"Placebo",
"Treatment A",
"Treatment B"
)
)
Macros vs. Functions
SAS macros are one of the most powerful tools available to experienced SAS programmers.
%macro make_summary(
data=,
variable=,
output=
);
proc means data=&data;
var &variable;
output out=&output
mean=mean
std=sd;
run;
%mend;
R's primary equivalent is usually a function.
make_summary <- function(
data,
variable
){
data |>
dplyr::summarise(
mean = mean(
{{ variable }},
na.rm = TRUE
),
sd = sd(
{{ variable }},
na.rm = TRUE
)
)
}
The programming philosophy is different.
A SAS macro generally manipulates SAS source code or generates SAS code.
An R function generally receives objects, transforms them, and returns objects.
Macro Variables vs. R Objects
SAS
%let analysis_date = 15APR2026; %put &analysis_date;
R
analysis_date <- as.Date( "2026-04-15" ) analysis_date
In R, most values should simply be represented as objects rather than as pieces of source-code text.
Macro Loops vs. R Iteration
A SAS programmer might write:
%do i = 1 %to 5;
/* generated SAS code */
%end;
In R, explicit loops are available:
for(i in 1:5){
print(i)
}
But many R workflows prefer vectorized functions or functional programming tools.
purrr::map( 1:5, ~ print(.x) )
For clinical programming, however, readability should generally take priority over forcing every task into a functional-programming pattern.
PROC REPORT vs. R Table Packages
PROC REPORT is widely used for producing clinical tables in SAS.
proc report data=summary nowd;
column TRT01P N MEAN SD;
define TRT01P / group;
define N / "N";
define MEAN / "Mean";
define SD / "SD";
run;
R has several ecosystems for table production, including:
gtgtsummaryflextablehuxtableofficer
For clinical-trial reporting, gtsummary is
particularly useful for standard descriptive tables, while gt provides extensive table-formatting control.
Statistical Programming
One of the biggest strengths of both SAS and R is the breadth of statistical methods available.
The syntax differs substantially.
Two-Sample t Test
SAS:
proc ttest data=adtte;
class TRT01P;
var AVAL;
run;
R:
t.test( AVAL ~ TRT01P, data = adtte )
Chi-Square Test
SAS:
proc freq data=adsl;
tables TRT01P*SEX / chisq;
run;
R:
tab <- table( adsl$TRT01P, adsl$SEX ) chisq.test(tab)
Linear Models
SAS:
proc glm data=adlb;
class TRT01P;
model AVAL = TRT01P BASE;
run;
R:
fit <- lm( AVAL ~ TRT01P + BASE, data = adlb ) summary(fit)
The R formula interface is one of the most important concepts for SAS programmers learning R.
The R Formula Interface
An R formula such as:
AVAL ~ TRT01P + BASEcan be interpreted as:
For many R statistical models, the formula describes the relationship between the response and explanatory variables.
| R Formula | Concept |
|---|---|
Y ~ X |
Outcome modeled as a function of X |
Y ~ X + Z |
Main effects for X and Z |
Y ~ X * Z |
Main effects plus interaction |
Y ~ X + Z + X:Z |
Main effects plus explicit interaction |
Y ~ . |
Use available predictors according to model context |
Mixed Models: SAS PROC MIXED vs. R
Mixed models are extremely important in longitudinal clinical trials.
A SAS model might be:
proc mixed data=adlb;
class USUBJID TRT01P AVISIT;
model AVAL =
TRT01P
AVISIT
TRT01P*AVISIT
BASE;
repeated AVISIT /
subject=USUBJID
type=cs;
run;
R packages such as nlme and lme4 provide mixed-model capabilities.
library(nlme)
fit <- lme(
AVAL ~ TRT01P * AVISIT + BASE,
random = ~ 1 | USUBJID,
correlation =
corCompSymm(
form = ~ AVISIT | USUBJID
),
data = adlb
)
summary(fit)
Survival Analysis
SAS programmers often use PROC PHREG.
proc phreg data=adtte;
class TRT01P;
model AVAL* CNSR(1) =
TRT01P AGE SEX;
run;
In R:
library(survival)
fit <- coxph(
Surv(AVAL, 1 - CNSR) ~
TRT01P + AGE + SEX,
data = adtte
)
summary(fit)
Again, the conceptual mapping is more useful than attempting to memorize syntax character by character.
Kaplan-Meier Analysis
SAS:
proc lifetest data=adtte
plots=survival;
time AVAL*CNSR(1);
strata TRT01P;
run;
R:
library(survival) km_fit <- survfit( Surv(AVAL, 1 - CNSR) ~ TRT01P, data = adtte ) plot(km_fit)
For publication-quality clinical graphics, R users frequently combine survival with survminer or a custom ggplot2-based workflow.
ODS vs. R Markdown / Quarto
SAS programmers are familiar with the Output Delivery System, or ODS.
ODS provides mechanisms for directing SAS output into formats such as:
- HTML
- RTF
- Excel-compatible outputs
- Other reporting destinations
R has a different reporting ecosystem.
Common tools include:
- R Markdown
- Quarto
knitrrmarkdowngtofficer
The major conceptual advantage is that the report can contain the analysis code, output, narrative, tables, and figures in a single reproducible workflow.
Reproducible Reporting
Consider a clinical report containing:
- Demographics
- Disposition
- Adverse events
- Laboratory summaries
- Efficacy analyses
- Figures
A traditional workflow may involve generating individual outputs and then assembling them into a report.
A reproducible R workflow can instead follow:
ADSL in SAS and R
A simplified ADSL derivation might include:
- Subject identifier
- Treatment assignment
- Age
- Sex
- Race
- Safety flag
- Intent-to-treat flag
- Randomization date
- First-dose date
SAS:
data adsl;
merge dm ex;
by usubjid;
if saffl = "Y";
trt01p = TRT01P;
if age < 65 then
agegr1 = "<65";
else
agegr1 = "≥65";
run;
R:
adsl <- dm |>
dplyr::left_join(
ex |>
dplyr::select(
USUBJID,
TRT01P,
RFSTDTC
),
by = "USUBJID"
) |>
dplyr::filter(SAFFL == "Y") |>
dplyr::mutate(
AGEGR1 = dplyr::case_when(
AGE < 65 ~ "<65",
AGE >= 65 ~ "≥65",
TRUE ~ NA_character_
)
)
ADAE Derivations
Adverse-event analysis often requires subject-level and event-level derivations.
For example, suppose the analysis requires treatment-emergent adverse events defined as events beginning on or after first dose.
SAS
data adae;
set ae;
if AESTDT >= TRTSDT then
TRTEMFL = "Y";
else
TRTEMFL = "N";
run;
R
adae <- ae |>
dplyr::mutate(
TRTEMFL = dplyr::if_else(
AESTDT >= TRTSDT,
"Y",
"N"
)
)
The derivation itself is straightforward.
The difficult part in a real clinical study is usually the definition of the dates, not the programming syntax.
Programming Language vs. Analysis Specification
This distinction is critical.
Suppose the SAP states:
Neither SAS nor R determines the correct answer automatically.
The programmer must translate the specification into deterministic logic.
Therefore:
The programming language is only one part of this chain.
Partial Dates
Clinical datasets frequently contain incomplete dates.
For example:
2026-04 2026 2026-04-15
A programmer should never silently invent a full date unless the protocol, SAP, derivation specification, or data-standard convention defines how the partial date should be handled.
Both SAS and R can process partial dates, but the clinical rule must come first.
Generating TLFs
A common misconception is that R is mainly useful for visualization and exploratory analysis.
Modern R workflows can also generate:
- Tables
- Listings
- Figures
- Statistical analyses
- Clinical reports
- Supporting datasets
For example, a demographic summary can be generated with gtsummary.
library(gtsummary)
tbl_summary(
adsl,
by = TRT01P,
include = c(
AGE,
SEX,
RACE
)
)
A more customized gt workflow can provide
fine-grained control over formatting.
library(gt)
summary_data |>
gt() |>
tab_header(
title = "Demographic Characteristics"
)
Clinical Listings
Listings are often easier to create in R than highly structured tables because a listing is essentially a carefully formatted dataset.
listing <- adae |>
dplyr::select(
USUBJID,
TRT01P,
AETERM,
AESEV,
AESER,
AESTDT
) |>
dplyr::arrange(
TRT01P,
USUBJID,
AESTDT
)
The remaining work is presentation: page breaks, titles, footnotes, headers, column widths, and output format.
Visualization: SAS vs. ggplot2
R has a particularly strong advantage in custom statistical visualization.
A basic SAS graph may be generated through ODS Graphics.
An R graph can be constructed using a layered grammar.
library(ggplot2)
ggplot(
adlb,
aes(
x = AVISIT,
y = AVAL,
group = USUBJID
)
) +
geom_line(alpha = 0.4) +
geom_point() +
labs(
x = "Visit",
y = "Laboratory Value"
) +
theme_classic()
The ggplot2 philosophy is based on combining
layers such as:
- Data
- Aesthetic mappings
- Geometries
- Scales
- Statistics
- Facets
- Coordinate systems
- Themes
Why ggplot2 Is So Important for Clinical Programmers
Clinical programming increasingly requires more than traditional tables.
Examples include:
- Waterfall plots
- Spider plots
- Swimmer plots
- Kaplan-Meier plots
- Forest plots
- Boxplots
- Spaghetti plots
- Volcano plots
- Laboratory shift plots
- Exposure-response plots
The layered structure of ggplot2 makes it possible
to construct these figures from relatively small reusable components.
Packages: A Major Difference Between the Ecosystems
SAS provides a large integrated environment of procedures and supporting functionality.
R uses packages extensively.
A clinical R project might include:
library(dplyr) library(tidyr) library(ggplot2) library(gtsummary) library(gt) library(survival) library(nlme)
The package ecosystem is one of R's greatest strengths, but it also creates responsibilities.
A clinical programmer needs to know:
- Which package performs the analysis
- Which version was used
- Which dependencies are required
- Whether the implementation is appropriate
- How the method has been validated
- How the computational environment can be reproduced
Package Versioning
A production clinical R workflow should not rely on an undocumented collection of packages installed on a programmer's computer.
Tools such as renv can help capture project
dependencies and package versions.
renv::init() renv::snapshot() renv::restore()
The underlying principle is:
SAS Libraries vs. R Projects
A SAS programmer may organize data into libraries:
libname adam "/project/adam";
data adam.adsl;
set adam.source_adsl;
run;
An R project usually organizes files through directories and project-relative paths.
library(here)
adsl <- readr::read_csv(
here(
"data",
"adam",
"adsl.csv"
)
)
Project-relative paths are generally preferable to hard-coded personal paths.
read.csv("C:/Users/Aaron/Desktop/project/data/adsl.csv")
Use a project-based path strategy instead so that another programmer can run the code in a different environment.
SAS Logs vs. R Logs and Diagnostics
SAS programmers are accustomed to examining the SAS log after every major program.
Important information includes:
- Errors
- Warnings
- NOTE messages
- Observation counts
- Variable attributes
- Merge diagnostics
R provides diagnostics through:
- Console output
- Warnings
- Errors
- Messages
- Object inspection
- Assertions
- Testing frameworks
For example:
nrow(adsl) names(adsl) str(adsl) summary(adsl)
Assertions can make assumptions explicit.
stopifnot( all(!is.na(adsl$USUBJID)) )
Validation: SAS Has an Important Historical Advantage
One reason SAS remains deeply embedded in clinical development is its long history of use in regulated pharmaceutical environments.
Organizations often have established:
- SAS programming standards
- Validation procedures
- Macro libraries
- QC processes
- Statistical programming specifications
- Submission experience
- Inspection experience
Moving an organization from SAS to R therefore involves much more than teaching programmers a new syntax.
It involves evaluating the entire software lifecycle.
Does R Need Validation?
Yes.
The fact that R is open source does not mean that analyses performed using R are automatically validated.
The relevant question is not:
but rather:
A regulated clinical analysis should have appropriate controls over:
- Source code
- Analysis specifications
- Data inputs
- Software versions
- Dependencies
- Execution environment
- Testing
- Review
- Change control
- Traceability
- Archived outputs
Independent Programming and QC
One common clinical-programming model is independent QC.
For example:
A cross-language approach is also possible:
Numerical Differences
SAS and R can sometimes produce slightly different numerical results even when the analytical method is conceptually the same.
Potential causes include:
- Different algorithms
- Different optimization routines
- Different convergence criteria
- Different default contrasts
- Different degrees-of-freedom methods
- Different handling of missing values
- Different numerical tolerances
- Different covariance estimators
Therefore, validation should compare the correct quantities at appropriate precision rather than assuming every decimal digit must be identical.
Floating-Point Arithmetic
Both environments use finite-precision numerical computation.
For example, mathematically:
but finite-precision computers may not represent these decimal numbers exactly.
In R:
0.1 + 0.2 == 0.3
may not behave as a mathematically naive comparison would suggest.
Clinical validation therefore should use appropriate tolerances where numerical equality is not expected to be exact.
Assertions in Clinical R Programming
Assertions are useful for converting programming assumptions into explicit checks.
stopifnot( all(!duplicated(adsl$USUBJID)) )
This can test the expectation that ADSL contains one record per subject.
A more explicit approach might be:
duplicate_subjects <- adsl |>
dplyr::count(USUBJID) |>
dplyr::filter(n > 1)
if(nrow(duplicate_subjects) > 0){
stop(
"ADSL contains duplicate USUBJID values."
)
}
Unit Testing
R has mature testing frameworks such as testthat.
A derivation function can be tested directly.
test_that(
"percent change is calculated correctly",
{
expect_equal(
derive_pchg(
baseline = 100,
value = 75
),
-25
)
}
)
This style of testing is particularly useful for reusable clinical functions.
Reusable R Functions for Clinical Programming
A major opportunity when transitioning from SAS to R is to replace repeated macro logic with reusable functions.
For example:
derive_pchg <- function(
value,
baseline
){
ifelse(
is.na(value) |
is.na(baseline) |
baseline == 0,
NA_real_,
(value - baseline) /
baseline * 100
)
}
The function can then be reused:
adlb <- adlb |>
dplyr::mutate(
PCHG = derive_pchg(
AVAL,
BASE
)
)
This promotes consistency across tables and figures.
Vectorization
SAS programmers often write row-oriented DATA step logic.
R often performs operations over complete vectors.
For example:
PCHG <- (AVAL - BASE) / BASE * 100
This can operate on an entire vector without explicitly writing a loop.
The conceptual difference is:
versus:
This is not an absolute rule, but it is a useful mental model.
Data.table vs. dplyr
R itself has multiple approaches to data manipulation.
Two important ecosystems are:
dplyrdata.table
A clinical programmer does not necessarily need to choose one forever.
For many users, dplyr provides readable code that
is relatively approachable for SAS programmers.
For very large datasets, data.table can provide
high-performance data manipulation.
A data.table Example
library(data.table)
adsl <- as.data.table(adsl)
adsl[
SAFFL == "Y",
AGEGR1 := fifelse(
AGE < 65,
"<65",
"≥65"
)
]
The syntax is compact and powerful, but it represents another programming style.
SAS Macro Libraries vs. R Packages
A mature SAS environment may have hundreds of reusable macros.
An R organization can package reusable functions into an internal package.
For example:
clinicalTools::derive_pchg() clinicalTools::derive_age_group() clinicalTools::make_demog_table() clinicalTools::validate_adsl()
This can create an internal clinical-programming ecosystem analogous to a shared SAS macro library, while providing additional package-management features.
Git and Version Control
One of the biggest practical advantages of an R-centered development workflow is the natural integration with modern software-engineering practices such as Git.
Clinical SAS programs can also be managed with Git, of course.
The difference is cultural and tooling-related rather than a fundamental language limitation.
A Git-based workflow allows programmers to:
- Track changes
- Review differences
- Create branches
- Merge work
- Tag releases
- Collaborate asynchronously
- Maintain an audit trail of source-code changes
Why Version Control Matters in Clinical Programming
Consider a derivation that changes between interim and final analyses.
Without version control, it can become difficult to determine:
- Which program generated a particular output
- When the derivation changed
- Why it changed
- Who made the change
- Which datasets were used
A disciplined version-control strategy can make these questions much easier to answer.
Environment Management
SAS environments are often centrally administered.
R environments may vary more across individual machines unless deliberately controlled.
A production R workflow should therefore define:
- R version
- Package versions
- Operating-system dependencies
- System libraries where relevant
- Configuration settings
- Execution procedures
Containers can also be used where appropriate to standardize computational environments.
Reproducibility Comparison
| Feature | SAS | R |
|---|---|---|
| Source-code versioning | Possible | Excellent tooling ecosystem |
| Package/environment locking | Centralized software environment often used | Tools such as renv available |
| Parameterized reports | Possible | Strong support through R Markdown/Quarto |
| Unit testing | Possible through organizational frameworks | Strong package ecosystem |
| Open-source ecosystem | Limited compared with R | Extensive |
| Integrated clinical legacy | Very strong | Growing |
R vs. SAS for CDISC Work
Clinical programmers frequently need to work with:
- SDTM
- ADaM
- Define-XML
- Controlled terminology
- Analysis specifications
- Submission datasets
The data standards themselves do not require a particular programming language.
A variable such as:
USUBJIDis not inherently a SAS variable or an R variable.
It is a variable defined within a clinical-data standard.
Reading XPT Files in R
R can work with SAS transport files, which are commonly used in clinical-data exchange.
library(haven) adsl <- read_xpt( "adsl.xpt" )
The reverse workflow is also possible for suitable output requirements.
write_xpt( adsl, "adsl.xpt" )
This allows R-based workflows to participate in environments where SAS transport-format datasets are required.
Variable Labels
SAS datasets often carry variable labels.
R packages such as haven can preserve SAS-style
metadata such as labels when reading supported datasets.
library(haven) adsl <- read_xpt( "adsl.xpt" ) attr( adsl$AGE, "label" )
Metadata preservation should always be checked rather than assumed.
SAS vs. R for Statistical Tables
A useful comparison is not "Which language makes tables faster?"
Instead, consider the entire workflow:
Both SAS and R can implement this workflow.
The difference is in the available tooling and programming conventions.
Example: Adverse Event Summary
Suppose the analysis requires the number and percentage of subjects with at least one treatment-emergent adverse event.
The key denominator is typically the number of subjects in the relevant analysis population and treatment group.
This is more complicated than simply counting AE records.
A subject with five adverse events should generally contribute:
to the number of subjects with at least one event, not:
event records.
R Example
ae_subjects <- adae |>
dplyr::filter(
TRTEMFL == "Y"
) |>
dplyr::distinct(
USUBJID,
TRT01P
) |>
dplyr::count(
TRT01P,
name = "N"
)
The important clinical-programming principle is the unit of analysis.
R vs. SAS for Listings
Listings can be generated using combinations of:
gtflextableofficer- Quarto
- R Markdown
A listing workflow can therefore become highly automated.
adae |>
dplyr::select(
USUBJID,
TRT01P,
AETERM,
AESEV,
AESER
) |>
dplyr::arrange(
TRT01P,
USUBJID
) |>
gt::gt()
R vs. SAS for Figures
R is particularly attractive when the clinical-programming organization needs many customized figures.
For example, a waterfall plot can be built from:
ggplot(
response,
aes(
x = reorder(
USUBJID,
PCHG
),
y = PCHG
)
) +
geom_col() +
geom_hline(
yintercept = -30,
linetype = "dashed"
) +
coord_flip()
The same underlying analysis dataset can then be reused to generate a spider plot, swimmer plot, or other efficacy visualization.
Interactive Graphics
R also has strong support for interactive graphics through tools such as:
- plotly
- Shiny
- htmlwidgets
This can be useful for exploratory analysis.
For example, a clinical statistician might inspect a patient-level dataset interactively before creating the final static CSR figure.
Shiny for Clinical Review
Shiny allows R code to power interactive web applications.
A clinical application could allow a reviewer to:
- Select a treatment group
- Select a patient
- Inspect laboratory values
- Display adverse events
- Review tumor trajectories
- Filter analysis populations
This can be extremely useful during exploratory clinical review, although interactive applications should be subject to appropriate controls when they are used in regulated workflows.
Performance
Performance comparisons between R and SAS are highly dependent on:
- Dataset size
- Algorithm
- Package
- Hardware
- Memory
- Database architecture
- Programming implementation
It is therefore misleading to state that one language is universally faster.
A well-designed R program can be extremely fast.
A poorly designed R program can also be slow.
The same is true for SAS.
Memory Management
R traditionally emphasizes in-memory data analysis, although modern R workflows can interact with databases and other storage systems.
Clinical programmers working with large datasets should understand:
- Object sizes
- Copies of data frames
- Efficient joins
- Column selection
- Database-backed workflows
- data.table
Do not load every dataset into memory simply because it is possible.
SQL Is Still Useful in R
SAS programmers sometimes assume that moving to R means abandoning SQL.
That is not true.
R can work with SQL databases directly and can also provide SQL-like functionality through several tools.
For example, database-backed workflows can allow large clinical datasets to remain in the database until only the necessary result is collected into R.
Hybrid SAS + R Workflows
A pharmaceutical organization does not necessarily need to choose a single language.
A realistic workflow might use:
Another organization might use R for most of the workflow and retain SAS for specific legacy procedures or established submission processes.
Where SAS Often Has an Advantage
| Area | Why SAS Can Be Attractive |
|---|---|
| Established clinical organizations | Large existing infrastructure and expertise |
| Legacy submission workflows | Long history and established processes |
| Existing macro libraries | Large reusable programming assets |
| Team familiarity | Many experienced clinical programmers |
| Integrated procedures | Strong standardized statistical procedures |
| Established validation processes | Existing organizational controls |
Where R Often Has an Advantage
| Area | Why R Can Be Attractive |
|---|---|
| Visualization | Extensive and highly customizable ecosystem |
| Open-source packages | Large range of statistical and computational methods |
| Reproducible reporting | Strong integration between code and documents |
| Modern software development | Strong integration with Git and package tooling |
| Advanced statistics | Large ecosystem for specialized methods |
| Interactive applications | Strong support through Shiny and related tools |
| Simulation | Excellent ecosystem for simulation and statistical experimentation |
Where the Two Are Surprisingly Similar
Despite their differences, SAS and R can perform many of the same analytical tasks.
| Task | SAS | R |
|---|---|---|
| Filter data | DATA step / WHERE | filter() |
| Create variable | DATA step | mutate() |
| Sort | PROC SORT | arrange() |
| Merge | MERGE / PROC SQL | join functions |
| Summarize | PROC MEANS / SUMMARY | summarise() |
| Frequency | PROC FREQ | count() / table() |
| Transpose | PROC TRANSPOSE | pivot_longer() / pivot_wider() |
| Linear model | PROC GLM | lm() |
| Mixed model | PROC MIXED | lme() / lmer() |
| Cox model | PROC PHREG | coxph() |
| Kaplan-Meier | PROC LIFETEST | survfit() |
| Reporting | ODS / PROC REPORT | gt / gtsummary / Quarto |
The SAS Programmer's R Translation Dictionary
| If You Think in SAS... | Start Thinking in R... |
|---|---|
| DATA step | Data-frame transformation |
| SET | Input object |
| BY group | group_by() |
| FIRST. | slice_head(), row_number(), lag(), etc. |
| LAST. | slice_tail(), lead(), row_number(), etc. |
| PROC SORT | arrange() |
| PROC SQL | join/filter/summarise operations |
| PROC FREQ | count(), table() |
| PROC MEANS | summarise() |
| PROC TRANSPOSE | pivot_longer()/pivot_wider() |
| Macro | Function |
| Macro variable | Object |
| Format | Labels/factors/formatting functions |
| ODS | Reporting/rendering ecosystem |
| SAS dataset | Data frame / tibble / data.table |
A Complete Miniature Clinical Example
Suppose we want to create an analysis dataset containing subjects who are safety evaluable, derive an age group, and calculate treatment-group summaries.
SAS Implementation
data adsl;
set dm;
where SAFFL = "Y";
if age < 65 then
AGEGR1 = "<65";
else if age >= 65 then
AGEGR1 = "≥65";
else
AGEGR1 = "Missing";
run;
proc sort data=adsl;
by TRT01P;
run;
proc means data=adsl n mean std median;
class TRT01P;
var AGE;
run;
R Implementation
adsl <- dm |>
dplyr::filter(
SAFFL == "Y"
) |>
dplyr::mutate(
AGEGR1 = dplyr::case_when(
AGE < 65 ~ "<65",
AGE >= 65 ~ "≥65",
TRUE ~ "Missing"
)
) |>
dplyr::arrange(
TRT01P
)
summary <- adsl |>
dplyr::group_by(
TRT01P
) |>
dplyr::summarise(
N = sum(!is.na(AGE)),
MEAN = mean(
AGE,
na.rm = TRUE
),
SD = sd(
AGE,
na.rm = TRUE
),
MEDIAN = median(
AGE,
na.rm = TRUE
),
.groups = "drop"
)
The analytical intent is essentially the same.
The R code, however, explicitly creates a summary object rather than relying on a procedure to print the results.
That Difference Is Important
In SAS:
proc means data=adsl;
class TRT01P;
var AGE;
run;
the procedure produces output.
In R:
summary <- adsl |> group_by(TRT01P) |> summarise(...)
the programmer usually creates an object containing the result.
That object can then be:
- Displayed
- Validated
- Joined to another object
- Formatted
- Plotted
- Written to a file
- Inserted into a report
Programming Pipelines
The R pipe:
|>
allows transformations to be chained.
result <- data |> filter(...) |> mutate(...) |> group_by(...) |> summarise(...) |> arrange(...)
A useful interpretation is:
where each step transforms one object into another.
Why SAS Programmers Sometimes Find Pipes Strange
SAS programmers are accustomed to explicit output datasets:
data step1;
set source;
run;
data step2;
set step1;
run;
data step3;
set step2;
run;
An R pipeline compresses these transformations:
result <- source |> step1() |> step2() |> step3()
The resulting code can be more concise, but excessive chaining can also make debugging difficult.
R and SAS Are Not Identical Statistical Engines
Even when two procedures have similar names, the implementation may differ.
For a clinical analysis, programmers should verify:
- Model specification
- Parameterization
- Reference groups
- Contrasts
- Estimation method
- Covariance structure
- Degrees of freedom
- Missing-data handling
- Convergence criteria
- Confidence interval construction
- Output interpretation
A successful translation is therefore not:
It is:
Reference Categories
Categorical-variable coding can be a source of unexpected differences.
For example, an R factor has an ordered set of levels.
adsl$TRT01P <- factor(
adsl$TRT01P,
levels = c(
"Placebo",
"Treatment A"
)
)
The reference level can influence model coefficients.
Clinical programmers should therefore explicitly control reference levels rather than relying on accidental alphabetical ordering.
Explicit Modeling Specifications
A good R analysis program should make important choices visible.
adsl$TRT01P <- relevel( factor(adsl$TRT01P), ref = "Placebo" )
This is preferable to assuming that the desired reference category will always happen to be the first factor level.
Programming Standards
A clinical R team should establish standards covering at least:
- File naming
- Object naming
- Function naming
- Package usage
- Pipe style
- Date handling
- Missing-value handling
- Factor handling
- Error handling
- Logging
- Testing
- Documentation
- Version control
- Output directories
- Environment management
The exact standards can vary, but consistency is essential.
Naming Conventions
Clinical datasets often use uppercase CDISC variable names.
That convention can be retained in R.
ADSL ADAE ADLB USUBJID TRT01P SAFFL PCHG
Derived internal variables can follow the same organization's naming standards.
There is no requirement that R programmers rename every clinical variable to lowercase.
USUBJID, keep the
standard variable name when appropriate. Style guides should make the
distinction between standardized data structures and internal programming
objects explicit.
Debugging: SAS vs. R
SAS Debugging
Common approaches include:
- Read the SAS log
- Use PUT statements
- Inspect intermediate datasets
- Use PROC CONTENTS
- Use PROC PRINT
- Review observation counts
R Debugging
Common approaches include:
- Inspect objects directly
- Use
print() - Use
str() - Use
glimpse() - Use
View() - Use
browser() - Use
traceback() - Use unit tests
For example:
dplyr::glimpse(adsl) dplyr::count( adsl, TRT01P )
Intermediate Dataset Philosophy
SAS programmers often create multiple intermediate datasets:
data step1;
...
run;
data step2;
...
run;
data step3;
...
run;
R programmers sometimes compress all transformations into one pipeline.
That is not always preferable.
For complex clinical derivations, named intermediate objects can improve traceability:
adsl_population <- ... adsl_dates <- ... adsl_flags <- ... adsl_final <- ...
The best choice depends on complexity and organizational standards.
Traceability Is More Important Than Brevity
A 10-line program is not necessarily better than a 50-line program.
In clinical programming, code should be:
- Understandable
- Traceable
- Testable
- Maintainable
- Reproducible
- Reviewable
Short code that obscures the derivation can be worse than longer code that clearly documents each step.
Common Mistakes When SAS Programmers Learn R
- Trying to translate syntax literally. Learn the underlying operation rather than replacing each SAS keyword with an R keyword.
-
Ignoring R's missing-value rules.
Remember
NAandis.na(). - Forgetting that joins can multiply records. Always understand dataset keys.
- Relying on implicit factor ordering. Explicitly control reference categories and display order.
- Writing everything as one enormous pipeline. Intermediate objects can improve traceability.
- Using too many packages without governance. A clinical environment should define approved or evaluated packages and versions as appropriate.
- Assuming open source means unvalidated. R workflows require appropriate controls and validation just as other regulated computational workflows do.
- Assuming identical procedures produce identical results. Check statistical specifications and computational details.
- Ignoring the computational environment. Package and R versions can matter.
- Replacing every SAS macro with generated text. Prefer ordinary R functions when possible.
- Failing to define the unit of analysis. A subject-level count is not the same as an event-level count.
- Using exploratory code directly in production. Exploratory scripts should be cleaned, tested, documented, and controlled before being used for production analyses.
When SAS Is Still the Better Choice
There are situations where continuing to use SAS is entirely reasonable.
Examples include:
- An organization has mature SAS infrastructure.
- The entire submission workflow is SAS-based.
- The programming team has deep SAS expertise.
- Existing validated macro libraries provide substantial value.
- Switching languages would create unnecessary operational risk.
- A specific procedure or workflow is already well established.
Using SAS is not evidence of being technologically behind.
It may simply be the appropriate engineering decision for the environment.
When R Is Particularly Attractive
R can be particularly valuable when an organization needs:
- Advanced visualization
- Modern statistical methods
- Simulation
- Interactive applications
- Reproducible reports
- Integrated statistical workflows
- Open-source tooling
- Strong software-development practices
When a Hybrid Strategy Is Best
For many organizations, a hybrid approach can be highly effective.
| Task | Potential Choice |
|---|---|
| Legacy production TLFs | SAS |
| Exploratory analysis | R |
| Advanced graphics | R |
| Independent QC | R and SAS |
| Simulation | R |
| Established submission pipeline | SAS or controlled hybrid |
| Interactive clinical review | R / Shiny |
| Reproducible narrative reports | R / Quarto |
Learning R If You Already Know SAS
SAS experience provides a major advantage when learning R.
You already understand:
- Clinical data structures
- Analysis populations
- Derivations
- ADaM concepts
- TLFs
- Statistical procedures
- Missing data
- Validation
- QC
- Clinical reporting
The main learning challenge is therefore not clinical statistics.
It is learning a different computational language.
A Recommended Learning Sequence
The 80/20 R Skill Set for SAS Programmers
A SAS programmer does not need to learn every feature of R before becoming productive.
The most valuable initial skills are:
| Skill | Why It Matters |
|---|---|
| Data frames / tibbles | Core clinical-data structure |
| filter() | Population selection |
| mutate() | Derivations |
| select() | Variable selection |
| arrange() | Ordering |
| group_by() | BY-group-like analysis |
| summarise() | Summary statistics |
| left_join() | Dataset integration |
| pivot_longer() | Long-format conversion |
| ggplot2 | Clinical graphics |
| Functions | Reusable programming |
| is.na() | Missing-value handling |
| R formulas | Statistical modeling |
| Git | Version control |
What Not to Learn First
A common mistake is starting with advanced R programming concepts such as:
- Metaprogramming
- Environments
- Closures
- S3/S4 internals
- Advanced functional programming
- Package internals
These concepts can become valuable later.
But a clinical programmer can become highly productive without mastering them initially.
A Practical SAS-to-R Exercise
Take a SAS program you already understand.
For example:
data adsl;
set dm;
where saffl = "Y";
if age < 65 then
agegrp = "<65";
else
agegrp = "≥65";
run;
proc sort data=adsl;
by TRT01P USUBJID;
run;
Do not immediately rewrite it.
First identify the operations:
- Read DM.
- Filter safety population.
- Derive age group.
- Sort by treatment and subject.
Then implement those operations in R:
adsl <- dm |>
dplyr::filter(
SAFFL == "Y"
) |>
dplyr::mutate(
AGEGRP = dplyr::if_else(
AGE < 65,
"<65",
"≥65"
)
) |>
dplyr::arrange(
TRT01P,
USUBJID
)
This is a much better learning exercise than trying to memorize a SAS-to-R syntax dictionary.
R vs. SAS: A Deeper Comparison
The graphic illustrates broad ecosystem characteristics. It should not be interpreted as a quantitative performance benchmark.
The Regulatory Question
The important regulatory question is not simply:
It is:
The answer depends on the complete process.
That process includes:
- Validated or appropriately evaluated software components
- Controlled source code
- Defined specifications
- Testing
- Independent review
- Change control
- Environment management
- Documentation
- Traceability
- Archiving
R Does Not Eliminate the Need for Statistical Programmers
One misconception is that modern R packages make clinical programming automatic.
They do not.
A package can calculate a statistic.
It cannot determine whether the statistic is the correct one for the study.
A package can generate a beautiful table.
It cannot determine whether the denominator is correct.
A package can create a Kaplan-Meier curve.
It cannot determine whether the censoring rules match the SAP.
Where the Programmer's Expertise Matters Most
The highest-value skills remain:
- Understanding the analysis specification
- Understanding the data model
- Identifying the correct analysis population
- Understanding derivation rules
- Recognizing edge cases
- Validating results
- Communicating with statisticians and clinicians
- Building maintainable production code
A Clinical Programmer's Decision Framework
When deciding between SAS and R, ask:
R vs. SAS: Practical Recommendation
For a clinical programmer who already knows SAS, learning R is usually best viewed as an expansion of the programming toolkit rather than a replacement for everything already known.
SAS expertise remains valuable because it provides:
- Deep clinical-trial programming experience
- Knowledge of regulatory workflows
- Understanding of TLF production
- Experience with CDISC datasets
- Knowledge of statistical programming conventions
- Experience with independent QC
R adds:
- Modern data manipulation
- Advanced visualization
- Flexible statistical methods
- Reproducible reporting
- Interactive analysis
- Open-source package development
- Modern software engineering practices
The Best Long-Term Skill Set
For the modern clinical programmer, the strongest combination is not:
but:
This combination provides both traditional clinical-programming depth and modern analytical flexibility.
Summary Table
| Question | Best General Answer |
|---|---|
| Is SAS still useful? | Yes. It remains deeply established in clinical development. |
| Is R useful for clinical programming? | Yes. Its role continues to expand across analysis, visualization, and reporting. |
| Can R create clinical TLFs? | Yes, using several mature table and reporting ecosystems. |
| Can R work with CDISC data? | Yes. CDISC standards are independent of programming language. |
| Can R generate submission data? | It can, provided the workflow satisfies applicable organizational and regulatory requirements. |
| Is R automatically validated? | No. The computational workflow still requires appropriate controls. |
| Should SAS programmers learn R? | Generally yes, especially for modern visualization, reporting, and statistical workflows. |
| Should organizations abandon SAS immediately? | No. Migration should be driven by scientific, operational, and quality considerations. |
| Is a hybrid environment reasonable? | Yes. SAS and R can complement one another. |
The Most Important Lessons
- R is not simply SAS with different syntax. It uses a different programming model and encourages different workflows.
- Learn concepts before syntax. Think "filter," "join," "group," "summarize," and "reshape" rather than memorizing keyword substitutions.
- Clinical knowledge transfers extremely well. Your knowledge of ADaM, TLFs, analysis populations, derivations, and QC remains valuable when moving from SAS to R.
-
Missing values require special attention.
Use
NAandis.na()correctly. - Joins require key awareness. Never assume that a successful join is a correct join.
- R functions are generally a better replacement for SAS macros than code-generating text.
- Statistical equivalence must be demonstrated. Similar-looking SAS and R models can have different defaults and implementations.
- R is particularly strong for visualization and reproducible reporting.
- SAS remains highly valuable in regulated clinical development. Its established infrastructure should not be dismissed.
- A hybrid SAS/R environment can be highly effective. Use the tool that best fits the scientific and operational requirement.
Quick Reference: SAS to R
| SAS | Common R Approach |
|---|---|
| DATA step | dplyr transformations |
| SET | Input data frame / tibble |
| WHERE | filter() |
| IF/THEN | if_else() / case_when() |
| KEEP | select() |
| DROP | select(-...) |
| RENAME | rename() |
| PROC SORT | arrange() |
| PROC SQL | dplyr joins and summaries |
| PROC FREQ | count() / table() |
| PROC MEANS | summarise() |
| PROC TRANSPOSE | pivot_longer() / pivot_wider() |
| PROC GLM | lm() |
| PROC MIXED | lme() / lmer() |
| PROC PHREG | coxph() |
| PROC LIFETEST | survfit() |
| PROC REPORT | gt / gtsummary / flextable |
| Macro | Function |
| Macro variable | Object |
| ODS | Quarto / R Markdown / reporting packages |
| SAS log | Console + warnings + tests + logs |
| SAS library | Project/data directories and data objects |
Final Clinical Programming Checklist
References and Further Reading
R Core Team. R: A Language and Environment for Statistical Computing.
R Foundation for Statistical Computing.
Wickham, H., Averick, M., Bryan, J., et al. (2019). Welcome to the Tidyverse. Journal of Open Source Software, 4(43), 1686.
Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis.
Springer.
Wickham, H., François, R., Henry, L., Müller, K., & Vaughan, D. dplyr: A Grammar of Data Manipulation.
Wickham, H., & Grolemund, G. (2017). R for Data Science.
O'Reilly Media.
RStudio / Posit. Quarto Documentation.
R Core Team. Writing R Extensions.
U.S. Food and Drug Administration. Data Standards Catalog and Study Data Technical Conformance guidance.
CDISC. SDTM and ADaM standards and implementation guidance.