Introduction
R has become an important programming language for modern clinical-trial statistics. It is used for exploratory analysis, statistical modeling, visualization, reporting, simulation, data engineering, and increasingly for production clinical-trial analyses.
For a clinical statistician, however, learning R is different from learning programming from scratch.
You already understand concepts such as:
- Analysis populations
- Baseline definitions
- Visit windows
- Change from baseline
- Descriptive statistics
- Confidence intervals
- Hypothesis testing
- Regression models
- Time-to-event analysis
- Missing-data conventions
- Estimands and treatment effects
The goal is therefore not to teach statistics.
The goal is to teach you how to express statistical thinking programmatically in R.
Why Clinical Statisticians Should Learn R
R provides several capabilities that are particularly valuable in clinical development.
| Capability | Clinical-Trial Use |
|---|---|
| Data manipulation | Deriving analysis datasets and analysis variables |
| Statistical modeling | Regression, survival analysis, longitudinal models, categorical analyses |
| Visualization | Patient profiles, efficacy figures, safety displays, diagnostic plots |
| Automation | Repeated tables, figures, listings, and analyses |
| Reproducibility | Scripted analyses with version-controlled source code |
| Simulation | Power, operating characteristics, sensitivity analyses, trial design |
| Reporting | Automated statistical reports and regulatory documentation |
R Is a Programming Language
A common mistake is to think of R as simply a statistical calculator.
R is a full programming language.
For example:
x <- c(10, 20, 30, 40, 50) mean(x) sd(x) median(x)
The object x contains a vector of values.
R can then apply statistical functions to that vector.
But the same language can also create functions, manipulate datasets, read files, generate graphics, fit models, and automate entire analysis pipelines.
RStudio and the R Environment
Most clinical statisticians who use R interact with R through RStudio.
RStudio provides an integrated development environment containing:
- Source-code editor
- Console
- Environment viewer
- Files pane
- Plots pane
- Package management
- Debugging tools
- Integrated documentation
The distinction between R and RStudio is important.
R
The programming language and statistical computing environment that executes your code.
RStudio
An integrated development environment that makes working with R substantially easier.
Start With an R Project
Clinical-trial analyses should generally be organized as projects rather than as collections of unrelated scripts.
A simple structure might look like:
clinical-study/ | ├── clinical-study.Rproj | ├── data/ │ ├── raw/ │ ├── sdtm/ │ └── adam/ | ├── R/ │ ├── functions.R │ ├── adsl.R │ ├── adae.R │ └── efficacy.R | ├── outputs/ │ ├── tables/ │ ├── figures/ │ └── listings/ | ├── programs/ | └── reports/
The exact structure will depend on organizational standards, but the principle is important:
Why Working Directories Matter
A fragile script might contain:
setwd("C:/Users/John/Desktop/Study123/Analysis")
This makes the program dependent on one person's computer.
A better approach is to work from an R project and use project-relative paths.
read.csv("data/adam/adsl.csv")
This makes the analysis substantially easier to reproduce.
Your First R Objects
R stores values in objects.
age <- 62 treatment <- "Drug A" response <- TRUE baseline_weight <- 82.5
The assignment operator:
<-
assigns a value to an object.
For example:
n_patients <- 125
creates an object called n_patients containing
the value 125.
Vectors
A vector is one of the most fundamental R data structures.
ages <- c(42, 55, 61, 38, 72)
The function c() combines values.
You can calculate:
mean(ages) median(ages) sd(ages) min(ages) max(ages)
This is particularly useful because many statistical functions operate naturally on vectors.
Clinical-Trial Example
Suppose five patients have changes from baseline:
chg <- c(-12, -5, 8, -20, 3)
Then:
mean(chg) sd(chg)
returns the mean and standard deviation of the change values.
The important conceptual point is that R treats the collection of values as an object that can be passed into functions.
Missing Values
Missing data are represented by NA.
x <- c(10, 20, NA, 40, 50)
A naive calculation:
mean(x)
returns NA because one value is missing.
To exclude missing values:
mean(x, na.rm = TRUE)
This produces a numeric result.
na.rm = TRUE is a programming instruction, not a
statistical missing-data strategy. Removing missing values from a calculation
does not mean that missing data have been appropriately handled for the
analysis specified in the SAP.
Data Frames
Clinical-trial datasets are generally rectangular data structures.
A simple data frame might contain:
adsl <- data.frame(
USUBJID = c("001", "002", "003", "004"),
TRT01P = c("Drug A", "Drug A", "Placebo", "Placebo"),
AGE = c(62, 55, 71, 48),
SEX = c("F", "M", "F", "M")
)
You can inspect it with:
head(adsl) str(adsl) summary(adsl)
Tibbles
Modern R workflows frequently use tibbles, provided by the tidyverse ecosystem.
library(tibble)
adsl <- tibble(
USUBJID = c("001", "002", "003", "004"),
TRT01P = c("Drug A", "Drug A", "Placebo", "Placebo"),
AGE = c(62, 55, 71, 48)
)
A tibble behaves similarly to a data frame but provides a more convenient printing and programming interface.
Columns Are Vectors
A particularly important R concept is that each column of a dataset is itself a vector.
adsl$AGE adsl$TRT01P
You can calculate:
mean(adsl$AGE)
or:
table(adsl$TRT01P)
Factors and Categorical Variables
Categorical clinical-trial variables require careful handling.
Examples include:
- Treatment group
- Sex
- Race
- Region
- Response category
- Severity
- Visit
R can represent categorical variables using factors.
adsl$TRT01P <-
factor(
adsl$TRT01P,
levels = c("Placebo", "Drug A")
)
Factor ordering becomes particularly important for tables and graphics.
Why Factor Ordering Matters
Suppose the desired treatment order is:
- Placebo
- Drug A
- Drug B
Explicitly specifying that order avoids accidental alphabetical ordering.
adsl$TRT01P <-
factor(
adsl$TRT01P,
levels = c(
"Placebo",
"Drug A",
"Drug B"
)
)
Dates in Clinical Trials
Dates are fundamental to clinical-trial programming.
Examples include:
- Date of informed consent
- Date of randomization
- First dose date
- Visit date
- Adverse-event onset date
- End-of-treatment date
R provides the Date class for calendar dates.
date <- as.Date( "2026-01-15" )
You can subtract dates:
end_date <-
as.Date("2026-04-15")
start_date <-
as.Date("2026-01-15")
end_date - start_date
This returns the elapsed number of days.
Analysis Day
A common clinical-trial derivation is study day.
For a post-baseline observation:
depending on the analysis convention.
In R:
data <- data |>
mutate(
ADY = as.integer(
ADT - TRTSDT + 1
)
)
The exact derivation should always follow the study-specific data standards and analysis specifications.
The Pipe Operator
Modern R programming frequently uses the pipe:
|>
The pipe allows a sequence of operations to be written from left to right.
Instead of:
mean(
subset(
adsl$AGE,
adsl$TRT01P == "Drug A"
)
)
you can use a data-manipulation workflow:
adsl |>
filter(TRT01P == "Drug A") |>
summarise(
mean_age = mean(AGE)
)
This becomes particularly powerful when analysis derivations contain multiple steps.
The tidyverse
The tidyverse is a collection of R packages designed around a consistent approach to data analysis.
Important packages include:
| Package | Typical Use |
|---|---|
dplyr |
Filtering, selecting, joining, grouping, summarizing |
tidyr |
Reshaping and organizing data |
ggplot2 |
Statistical graphics |
readr |
Reading rectangular text data |
stringr |
String manipulation |
forcats |
Factor manipulation |
Installing Packages
Packages are installed once per R installation.
install.packages("dplyr")
install.packages("ggplot2")
install.packages("tidyr")
Then they can be loaded:
library(dplyr) library(ggplot2) library(tidyr)
Filtering Patients
Suppose an ADSL dataset contains treatment assignment.
adsl_drug_a <- adsl |> filter(TRT01P == "Drug A")
Multiple conditions can be combined.
adsl |>
filter(
TRT01P == "Drug A",
SAFFL == "Y",
AGE >= 18
)
This is conceptually similar to applying multiple WHERE conditions in traditional statistical programming.
Selecting Variables
adsl |>
select(
USUBJID,
TRT01P,
AGE,
SEX
)
You can remove columns with:
adsl |>
select(
-RACE
)
Creating Derived Variables
The mutate() function creates or modifies columns.
adsl |>
mutate(
AGEGR1 = case_when(
AGE < 65 ~ "<65",
AGE >= 65 ~ "≥65"
)
)
More complicated clinical derivations can be constructed in the same manner.
Conditional Derivations
The case_when() function is particularly useful
for analysis programming.
data |>
mutate(
RESP = case_when(
PCHG <= -30 ~ "Response",
PCHG > 20 ~ "Progression",
TRUE ~ "Stable"
)
)
For formal clinical endpoints, however, the derivation must reflect the complete endpoint definition rather than an oversimplified rule.
Grouping and Summarizing
Clinical-trial tables frequently summarize patients by treatment.
adsl |>
group_by(TRT01P) |>
summarise(
N = n(),
mean_age = mean(AGE, na.rm = TRUE),
sd_age = sd(AGE, na.rm = TRUE)
)
This produces one row per treatment group.
Multiple Summary Statistics
adsl |>
group_by(TRT01P) |>
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)
)
This pattern is one of the foundations of clinical-trial table programming.
Counting Categories
For categorical variables:
adsl |>
count(
TRT01P,
SEX
)
This produces counts for each treatment-by-sex combination.
Percentages
Percentages can be derived from grouped counts.
adsl |>
count(TRT01P, SEX) |>
group_by(TRT01P) |>
mutate(
PCT = 100 * n / sum(n)
)
The denominator is critical.
Sorting Data
data |>
arrange(
TRT01P,
USUBJID,
AVISITN
)
Sorting is often important before:
- Retaining first or last records
- Calculating changes
- Creating patient profiles
- Constructing longitudinal displays
- Performing sequence-dependent derivations
Selecting the First Observation
After sorting:
data |>
group_by(USUBJID) |>
slice_min(
ADT,
n = 1,
with_ties = FALSE
)
This can be useful for identifying the earliest record, but clinical-trial programming requires the selection criterion to be based on the endpoint specification rather than simply "first record."
Selecting the Last Observation
data |>
group_by(USUBJID) |>
slice_max(
ADT,
n = 1,
with_ties = FALSE
)
Again, the clinical definition of the appropriate observation should drive the programming logic.
Joining Clinical-Trial Datasets
Clinical analyses frequently require combining datasets.
For example, an adverse-event dataset may need treatment information from ADSL.
adae <-
adae |>
left_join(
adsl |>
select(
USUBJID,
TRT01P,
SAFFL
),
by = "USUBJID"
)
A left join retains all observations from the primary dataset.
Why Joins Require Care
Suppose ADSL has one record per patient.
Suppose ADAE has multiple records per patient.
Joining ADSL to ADAE therefore produces multiple ADAE rows for patients with multiple adverse events.
That is usually correct.
But if both datasets contain multiple records per patient and are joined on patient alone, the result can unintentionally multiply records.
ADSL as the Analysis Backbone
In CDISC-oriented clinical-trial programming, ADSL often serves as the patient-level foundation for many downstream analyses.
Conceptually:
A Simplified ADSL Example
adsl <- tibble(
USUBJID = c(
"01-001",
"01-002",
"01-003",
"01-004",
"01-005"
),
TRT01P = c(
"Placebo",
"Drug A",
"Drug A",
"Placebo",
"Drug A"
),
AGE = c(
64, 58, 71, 49, 67
),
SEX = c(
"F", "M", "F", "M", "F"
),
SAFFL = c(
"Y", "Y", "Y", "Y", "Y"
),
FASFL = c(
"Y", "Y", "Y", "Y", "Y"
)
)
Creating an Analysis Population
A common pattern is to create an analysis-specific dataset.
efficacy_pop <-
adsl |>
filter(
FASFL == "Y"
)
The important concept is that the population flag is part of the analysis definition.
For example:
- Safety population
- Full analysis set
- Intent-to-treat population
- Per-protocol population
- Biomarker-defined subgroup
should not be reconstructed ad hoc in every downstream program.
Change From Baseline
Suppose an efficacy dataset contains:
eff <- tibble(
USUBJID = c(
"001","001","002","002","003","003"
),
AVISIT = c(
"Baseline",
"Week 8",
"Baseline",
"Week 8",
"Baseline",
"Week 8"
),
BASE = c(
100,100,80,80,120,120
),
AVAL = c(
100,70,80,60,120,132
)
)
Change from baseline is:
In R:
eff |>
mutate(
CHG = AVAL - BASE
)
Percent Change From Baseline
Percentage change is:
In R:
eff |>
mutate(
PCHG =
100 * (AVAL - BASE) / BASE
)
For Patient 001:
Baseline Is a Derivation, Not Just a Variable
One of the biggest conceptual differences between toy R examples and
production clinical-trial programming is that BASE
usually cannot simply be assumed to be present.
The analysis specification may define baseline as:
- Last nonmissing assessment before first dose
- Last assessment on or before randomization
- Baseline value within a prespecified window
- Average of repeated measurements
- Another protocol-defined measurement
Therefore, baseline derivation should be treated as a formal analysis step.
A More Realistic Baseline Workflow
baseline <-
efficacy |>
filter(
ADT < TRTSDT,
!is.na(AVAL)
) |>
group_by(USUBJID) |>
slice_max(
ADT,
n = 1,
with_ties = FALSE
) |>
select(
USUBJID,
BASE = AVAL
)
The exact logic must be adapted to the protocol, SAP, and analysis dataset structure.
Joining Baseline Back to Longitudinal Data
efficacy <-
efficacy |>
left_join(
baseline,
by = "USUBJID"
) |>
mutate(
CHG = AVAL - BASE,
PCHG = 100 * CHG / BASE
)
This pattern appears repeatedly in longitudinal clinical-trial analysis.
Working With Visits
Clinical trials frequently use nominal visits such as:
- Baseline
- Week 4
- Week 8
- Week 12
- Week 16
A numeric visit variable such as AVISITN is
often useful for ordering.
efficacy |>
arrange(
USUBJID,
AVISITN
)
Keeping both a numeric ordering variable and a display label is often more robust than trying to sort textual visit labels directly.
Wide Versus Long Data
Clinical programmers frequently encounter both wide and long structures.
Long format:
USUBJID AVISIT AVAL 001 Week 4 92 001 Week 8 80 001 Week 12 75 002 Week 4 98 002 Week 8 90
Wide format:
USUBJID Week4 Week8 Week12 001 92 80 75 002 98 90 NA
Long format is often preferable for modeling and visualization because observations are naturally represented as repeated records.
Reshaping With tidyr
Wide to long:
long_data <-
wide_data |>
pivot_longer(
cols = starts_with("Week"),
names_to = "AVISIT",
values_to = "AVAL"
)
Long to wide:
wide_data <-
long_data |>
pivot_wider(
names_from = AVISIT,
values_from = AVAL
)
Creating Clinical Summary Tables
Suppose we want a treatment-arm summary of age.
age_summary <-
adsl |>
group_by(TRT01P) |>
summarise(
N = n(),
Mean = mean(AGE, na.rm = TRUE),
SD = sd(AGE, na.rm = TRUE),
Median = median(AGE, na.rm = TRUE),
Q1 = quantile(
AGE,
0.25,
na.rm = TRUE
),
Q3 = quantile(
AGE,
0.75,
na.rm = TRUE
),
Min = min(
AGE,
na.rm = TRUE
),
Max = max(
AGE,
na.rm = TRUE
)
)
This is already approaching the structure of a clinical-trial descriptive table.
Statistical Analysis With R
R contains extensive statistical functionality.
For example, a two-sample comparison can be performed with:
t.test( AGE ~ TRT01P, data = adsl )
This produces inferential output including an estimated difference and confidence interval.
Confidence Intervals
Suppose we have treatment-response observations.
response <-
c(
12, 15, 18, 21, 16,
14, 20, 19, 17, 22
)
mean(response)
t.test(response)$conf.int
The confidence interval is calculated from the statistical model or procedure implemented by the function.
Categorical Analyses
Contingency tables can be created with:
table( adsl$TRT01P, adsl$SEX )
A chi-squared test can be performed with:
chisq.test(
table(
adsl$TRT01P,
adsl$SEX
)
)
For small samples, Fisher's exact test may be more appropriate:
fisher.test(
table(
adsl$TRT01P,
adsl$SEX
)
)
Regression Models
R uses formulas extensively.
For example:
model <-
lm(
AVAL ~ TRT01P + BASE + AGE,
data = efficacy
)
summary(model)
The formula:
AVAL ~ TRT01P + BASE + AGEcan be interpreted as modeling the outcome
AVAL
using treatment, baseline, and age as explanatory variables.
Formula Syntax Is Fundamental
Clinical statisticians will encounter formulas throughout R.
| R Formula | Concept |
|---|---|
Y ~ X |
Outcome Y modeled by X |
Y ~ A + B |
Main effects of A and B |
Y ~ A * B |
A, B, and their interaction |
Y ~ A + B + C |
Multiple covariates |
Y ~ factor(X) |
Treat X as categorical |
Analysis of Covariance Example
A simplified ANCOVA model might be:
In R:
ancova <-
lm(
CHG ~ TRT01P + BASE,
data = efficacy
)
summary(ancova)
The model itself is straightforward.
The difficult part in a clinical trial is defining the appropriate analysis population, endpoint, baseline, covariates, treatment coding, missing-data strategy, and estimand.
Survival Analysis
R is also widely used for time-to-event analysis.
A survival object can be constructed using the Surv() function.
library(survival)
surv_obj <-
Surv(
time = AVAL,
event = CNSR == 0
)
A Kaplan-Meier model can then be fitted:
km_fit <-
survfit(
surv_obj ~ TRT01P,
data = adtte
)
summary(km_fit)
A Cox proportional hazards model can be fitted using:
cox_fit <-
coxph(
surv_obj ~ TRT01P + AGE,
data = adtte
)
summary(cox_fit)
Clinical-Trial Visualization With ggplot2
ggplot2 is one of the most important packages for
clinical statistical graphics.
The basic structure is:
ggplot(
data,
aes(
x = variable_x,
y = variable_y
)
) +
geom_point()
This is based on the grammar of graphics.
Basic Treatment-Group Boxplot
ggplot(
efficacy,
aes(
x = TRT01P,
y = CHG
)
) +
geom_boxplot() +
labs(
x = "Treatment",
y = "Change from Baseline"
) +
theme_classic()
Longitudinal Mean Plot
First calculate treatment-by-visit summaries.
summary_data <-
efficacy |>
group_by(
TRT01P,
AVISITN,
AVISIT
) |>
summarise(
N = sum(!is.na(CHG)),
Mean = mean(
CHG,
na.rm = TRUE
),
SD = sd(
CHG,
na.rm = TRUE
),
.groups = "drop"
)
Then plot the means.
ggplot(
summary_data,
aes(
x = AVISITN,
y = Mean,
group = TRT01P,
color = TRT01P
)
) +
geom_line() +
geom_point() +
labs(
x = "Analysis Visit",
y = "Mean Change from Baseline",
color = "Treatment"
) +
theme_classic()
Why Separate Derivation From Visualization?
A common mistake is to put all statistical calculations directly inside a plotting command.
For example, a complicated expression inside ggplot()
may be difficult to validate.
A more robust workflow is:
This creates a clearer separation between:
Functions: The Key to Reusable Programming
As clinical programs become more complex, repeated code becomes difficult to maintain.
R allows programmers to define functions.
mean_ci <- function(x){
x <- x[!is.na(x)]
n <- length(x)
mean_x <- mean(x)
se <- sd(x) / sqrt(n)
lower <-
mean_x -
qt(0.975, df = n - 1) * se
upper <-
mean_x +
qt(0.975, df = n - 1) * se
tibble(
N = n,
Mean = mean_x,
Lower = lower,
Upper = upper
)
}
The function can then be reused:
mean_ci( efficacy$CHG )
Why Functions Matter in Clinical Programming
Reusable functions can standardize:
- Descriptive statistics
- Confidence intervals
- Denominator calculations
- Visit derivations
- Formatting
- Table construction
- Figure annotations
- Quality-control checks
Instead of writing the same logic twenty times, define it once and test it carefully.
A More Clinical Example: Descriptive Statistics Function
summarise_continuous <-
function(data, variable){
x <-
data[[variable]]
tibble(
N = sum(!is.na(x)),
Mean = mean(
x,
na.rm = TRUE
),
SD = sd(
x,
na.rm = TRUE
),
Median = median(
x,
na.rm = TRUE
),
Min = min(
x,
na.rm = TRUE
),
Max = max(
x,
na.rm = TRUE
)
)
}
This illustrates a powerful concept: functions can accept both data and parameters describing what should be done to that data.
Programming With Missing Data
Clinical-trial datasets often contain multiple kinds of missingness.
For example:
- Missing assessment
- Missing treatment
- Missing baseline
- Missing visit date
- Missing laboratory value
- Not applicable
- Not done
R often represents missing numeric or character values with NA, but the analysis meaning of the missing value
must come from the data standard and analysis specification.
Detecting Missing Values
sum(is.na(efficacy$AVAL)) mean(is.na(efficacy$AVAL))
Missingness by treatment:
efficacy |>
group_by(TRT01P) |>
summarise(
N = n(),
Missing = sum(is.na(AVAL)),
Missing_Pct =
100 * Missing / N
)
Never Hide Missingness Accidentally
This code:
mean(x, na.rm = TRUE)may be correct for a descriptive statistic.
But it can also conceal the fact that a substantial proportion of values are missing.
A production analysis should generally report both:
- Number of nonmissing observations
- Number of missing observations
when they are relevant to the analysis.
Clinical-Trial Programming Is Mostly Data Logic
New R users often assume that statistical programming consists primarily of calling statistical functions.
In clinical development, a large portion of the work is actually:
Population Logic
Who belongs in the analysis?
Endpoint Logic
Which observations define the endpoint?
Time Logic
Which visit or assessment is used?
Derivation Logic
How are analysis variables constructed?
The statistical procedure may ultimately be only one line of code.
Example: A Treatment Summary Workflow
Suppose we want the mean change from baseline at Week 12.
week12 <-
efficacy |>
filter(
AVISITN == 12,
FASFL == "Y"
)
week12_summary <-
week12 |>
group_by(TRT01P) |>
summarise(
N = sum(!is.na(CHG)),
Mean = mean(
CHG,
na.rm = TRUE
),
SD = sd(
CHG,
na.rm = TRUE
)
)
The analysis is conceptually simple.
But a real clinical trial might require additional logic for:
- Visit windows
- Baseline selection
- Post-baseline flags
- Population flags
- Unscheduled visits
- Repeated measurements
- Protocol deviations
- Intercurrent events
R and CDISC
R does not require CDISC data structures.
However, R can work very effectively with SDTM- and ADaM-oriented data.
A common conceptual architecture is:
R therefore sits alongside the data standards rather than replacing them.
Reading a CSV File
library(readr)
adsl <-
read_csv(
"data/adam/adsl.csv"
)
After importing, inspect the result.
glimpse(adsl) names(adsl) dim(adsl) summary(adsl)
Reading SAS Data
Clinical-trial organizations frequently work with SAS datasets.
R can read SAS transport files and SAS datasets using packages such as haven.
library(haven)
adsl <-
read_sas(
"data/adam/adsl.sas7bdat"
)
For XPT files:
adsl <-
read_xpt(
"data/adam/adsl.xpt"
)
Writing Data
A CSV can be written with:
write_csv( adsl, "outputs/adsl.csv" )
SAS data can be written using appropriate functions from haven.
Labels and Metadata
One challenge when moving between clinical-trial ecosystems is metadata.
A clinical dataset is more than a collection of numbers.
It can contain:
- Variable names
- Variable labels
- Formats
- Controlled terminology
- Origin information
- Derivation descriptions
- Dataset-level metadata
When using R in regulated environments, metadata management should therefore be treated as part of the overall programming architecture.
R Is Case Sensitive
These are different objects:
BASE base Base
This is particularly important with CDISC-style variable names.
A typo such as:
adsl$TRT01pinstead of:
adsl$TRT01Pcan cause an error or, depending on the context, an incorrect derivation.
Debugging R Code
Errors are a normal part of programming.
Suppose R reports:
object 'TRT01P' not found
The first question should be:
Where does R expect TRT01P to exist?
Inspect the dataset:
names(adsl) glimpse(adsl)
Then inspect the specific expression that failed.
Common R Error Categories
| Error Type | Typical Cause |
|---|---|
| Object not found | Incorrect object or variable name |
| Could not find function | Package not loaded or function name incorrect |
| Non-numeric argument | Unexpected character or factor-like data |
| Replacement has length | Incompatible vector lengths |
| Join unexpectedly expands rows | Unexpected key cardinality |
| Missing values appear | Derivation introduced NA values |
Validation Is Part of Programming
A script running without errors does not mean the analysis is correct.
This distinction is fundamental.
A clinical programmer should therefore build validation into the workflow.
Basic Dataset Checks
nrow(adsl) ncol(adsl) sum(duplicated(adsl$USUBJID)) table(adsl$TRT01P, useNA = "ifany") summary(adsl$AGE)
These simple checks can detect unexpected problems before analysis results are generated.
Checking Expected Record Counts
Suppose ADSL should contain one record per patient.
adsl |> count(USUBJID) |> filter(n > 1)
An empty result indicates that no patient has more than one record.
If records appear, investigate before proceeding.
Checking Treatment Assignment
adsl |> count(TRT01P)
The result can be compared against expected randomized or treated patient counts.
Checking Derivations
Suppose percentage change was derived.
efficacy |>
mutate(
PCHG_CHECK =
100 * (AVAL - BASE) / BASE
) |>
summarise(
max_difference =
max(
abs(
PCHG - PCHG_CHECK
),
na.rm = TRUE
)
)
The maximum difference should generally be zero or within a deliberately specified numerical tolerance.
Numerical Precision
Computers represent many decimal numbers approximately.
Therefore, this test:
x == yis not always appropriate for floating-point calculations.
A tolerance-based comparison is safer:
abs(x - y) < 1e-10
The appropriate tolerance depends on the calculation and reporting precision.
Reproducibility
One of R's major strengths is reproducibility.
A reproducible analysis should allow another programmer to recreate the result from:
- Defined input data
- Specified source code
- Controlled package versions
- Defined configuration
- Documented analysis specifications
The objective is:
Version Control
Git is commonly used to track changes to R programs.
A simple history might look like:
Initial analysis program Add baseline derivation Correct treatment ordering Add Week 24 analysis Fix missing-data handling Update table shell
Version control makes it possible to understand how an analysis evolved.
Package Versions Matter
An analysis can depend on the behavior of R packages.
Therefore, production workflows should control or document:
- R version
- Package versions
- Operating environment
- Source-code version
Tools such as renv can help manage package
environments for R projects.
Reusable Analysis Functions
Suppose many tables require treatment-group descriptive statistics.
Instead of repeating the entire calculation:
make_cont_summary <-
function(
data,
value,
treatment
){
data |>
group_by(
{{ treatment }}
) |>
summarise(
N = sum(
!is.na({{ value }})
),
Mean = mean(
{{ value }},
na.rm = TRUE
),
SD = sd(
{{ value }},
na.rm = TRUE
),
Median = median(
{{ value }},
na.rm = TRUE
),
.groups = "drop"
)
}
The function can then be reused across multiple endpoints.
This introduces an advanced but important concept: programming abstractions.
Don't Over-Abstract Too Early
Reusable functions are valuable, but excessive abstraction can make clinical programs difficult to audit.
A useful principle is:
For example, hiding a complex endpoint derivation inside five nested helper functions may make the program shorter but harder to review.
A reviewer should be able to trace:
R Versus Traditional SAS Programming
Clinical statisticians transitioning from SAS often recognize many familiar concepts in R.
| Traditional Concept | R Equivalent |
|---|---|
| DATA step | Data transformation pipeline |
| SET | bind_rows() or data import |
| MERGE | left_join(), inner_join(), etc. |
| WHERE | filter() |
| KEEP | select() |
| IF/THEN | if_else() or case_when() |
| PROC MEANS | summarise() |
| PROC FREQ | count(), table() |
| PROC SQL | joins, aggregation, filtering, and transformation functions |
| PROC PHREG | coxph() |
| PROC LIFETEST | survfit() |
| PROC GLM | lm() and related modeling functions |
But R Is Not "SAS With Different Syntax"
This is an important transition point.
R encourages a more object-oriented and functional programming style.
Instead of thinking primarily:
it is often more useful to think:
This mental model becomes increasingly important as R programs become more advanced.
A Practical Clinical-Trial Workflow
Example: A Complete Mini Analysis
Consider a simplified efficacy dataset.
library(dplyr)
library(tibble)
efficacy <- tibble(
USUBJID = c(
"001","002","003","004",
"005","006","007","008"
),
TRT01P = c(
"Placebo",
"Drug A",
"Drug A",
"Placebo",
"Drug A",
"Placebo",
"Drug A",
"Placebo"
),
BASE = c(
100, 90, 110, 95,
105, 100, 120, 115
),
AVAL = c(
96, 65, 78, 102,
72, 108, 88, 111
)
)
Calculate change and percentage change.
efficacy <-
efficacy |>
mutate(
CHG = AVAL - BASE,
PCHG =
100 * CHG / BASE
)
Summarize by treatment:
summary <-
efficacy |>
group_by(TRT01P) |>
summarise(
N = n(),
Mean_CHG =
mean(CHG),
SD_CHG =
sd(CHG),
Mean_PCHG =
mean(PCHG),
SD_PCHG =
sd(PCHG),
.groups = "drop"
)
The workflow can then be extended to confidence intervals, treatment comparisons, and graphical displays.
Creating a Simple Response Indicator
For illustration only, suppose we want to flag patients with at least 30% tumor shrinkage.
efficacy <-
efficacy |>
mutate(
SHRINK30 =
PCHG <= -30
)
Then count them:
efficacy |>
count(
TRT01P,
SHRINK30
)
Creating a Basic Table Dataset
Clinical reporting often requires transforming analysis results into a display structure.
table_data <-
efficacy |>
group_by(TRT01P) |>
summarise(
N = n(),
Mean = mean(PCHG),
SD = sd(PCHG),
.groups = "drop"
) |>
mutate(
Mean_SD =
sprintf(
"%.1f (%.1f)",
Mean,
SD
)
)
Separating the statistical result from the display formatting is often a good practice.
Formatting Is Not the Same as Analysis
For example:
Mean = 12.345678
might ultimately be displayed as:
12.3
The underlying analysis value should not necessarily be rounded merely because the display requires one decimal place.
R Markdown and Quarto
R can combine analysis code with narrative text.
This is useful for:
- Statistical reports
- Exploratory analyses
- Programming documentation
- Quality-control reports
- Analysis notebooks
- Training materials
For example, a report can contain:
The analysis included `N` patients.
The mean change from baseline was:
```{r}
mean(efficacy$CHG)
```
The result is generated directly from the analysis environment.
Why Literate Programming Matters
Traditional workflows can separate:
- Programming code
- Statistical output
- Interpretation
- Documentation
R Markdown and Quarto allow these components to be connected.
This creates a workflow in which:
can be generated reproducibly.
Simulation Is a Major R Strength
Clinical statisticians should not think of R only as a reporting language.
It is particularly powerful for simulation.
For example:
set.seed(12345)
x <-
rnorm(
1000,
mean = 10,
sd = 2
)
mean(x)
sd(x)
The set.seed() call makes the random-number
sequence reproducible.
Power Simulation Concept
Suppose we want to investigate power under a hypothesized treatment effect.
A simplified simulation might:
This is one of the areas where R can become extremely valuable for statistical design work.
Reproducible Randomization
If a simulation uses random numbers, record the seed.
set.seed(20260910)
Without a controlled seed, two executions may produce different simulated datasets.
Quality Control of Statistical Results
A strong clinical R workflow should distinguish between:
| QC Layer | Purpose |
|---|---|
| Program checks | Detect unexpected data or derivation behavior |
| Independent programming | Compare results against an independently derived implementation |
| Output review | Confirm tables and figures are clinically interpretable |
| Traceability review | Connect output back to analysis data |
| Statistical review | Confirm the method matches the analysis specification |
Independent QC Example
Suppose the production program calculates:
mean_prod <-
mean(
efficacy$CHG,
na.rm = TRUE
)
An independent QC implementation might calculate the same quantity through a different route.
mean_qc <-
efficacy |>
filter(
!is.na(CHG)
) |>
summarise(
result = sum(CHG) / n()
) |>
pull(result)
Then compare:
abs( mean_prod - mean_qc ) < 1e-10
The principle is more important than the particular implementation.
Common Beginner Mistakes
- Writing everything in the console. Console experiments are useful, but production logic belongs in saved, version-controlled scripts.
- Using absolute file paths. This makes programs difficult to reproduce.
- Ignoring variable classes. Dates, factors, characters, integers, and numerics behave differently.
-
Using
na.rm = TRUEwithout understanding the analysis. This can conceal missing-data problems. - Assuming joins are harmless. Incorrect joins can silently duplicate observations.
- Relying on default factor ordering. This can produce incorrectly ordered tables and figures.
- Putting too much logic inside one expression. Readable intermediate objects are often easier to validate.
- Assuming successful execution means correctness. A program can run perfectly and still implement the wrong analysis.
- Using statistical functions without understanding their assumptions. The function call is not the statistical analysis plan.
- Failing to document the analysis environment. Package and R-version differences can affect reproducibility.
A Better R Programming Style
For clinical programming, readability matters.
Compare:
x<-df|>filter(A=="Y")|>group_by(B)|>summarise(m=mean(C,na.rm=T))
with:
summary_data <-
df |>
filter(
FASFL == "Y"
) |>
group_by(
TRT01P
) |>
summarise(
Mean = mean(
CHG,
na.rm = TRUE
),
.groups = "drop"
)
The second version is longer.
It is also much easier to review.
Naming Conventions
Use names that communicate meaning.
Prefer:
baseline_data efficacy_analysis response_summary km_fit
over:
x tmp foo data2
Temporary objects are sometimes necessary, but meaningful names reduce programming errors.
Use Intermediate Objects Strategically
Instead of writing:
result <- source |> filter(...) |> mutate(...) |> group_by(...) |> summarise(...) |> filter(...) |> mutate(...) |> arrange(...)
consider breaking clinically meaningful stages into separate objects.
analysis_pop <-
source |>
filter(
FASFL == "Y"
)
derived_data <-
analysis_pop |>
mutate(
CHG = AVAL - BASE
)
summary_data <-
derived_data |>
group_by(TRT01P) |>
summarise(
Mean = mean(
CHG,
na.rm = TRUE
),
.groups = "drop"
)
This makes intermediate validation much easier.
Clinical Analysis as a Dependency Graph
A useful mental model for advanced R programming is to think of an analysis as a dependency graph.
If an upstream derivation changes, downstream outputs may change.
Thinking in terms of dependencies makes large R projects easier to maintain.
When to Use Base R
You do not need the tidyverse for everything.
Base R is powerful and remains important.
mean(x, na.rm = TRUE) median(x, na.rm = TRUE) summary(x) unique(x) sort(x) table(x) merge(x, y)
Understanding base R makes it easier to understand packages built on top of R.
When to Use dplyr
For complex rectangular-data workflows, dplyr is
often more readable.
analysis <-
data |>
filter(
FASFL == "Y"
) |>
select(
USUBJID,
TRT01P,
AVISITN,
CHG
) |>
group_by(
TRT01P,
AVISITN
) |>
summarise(
Mean = mean(
CHG,
na.rm = TRUE
),
.groups = "drop"
)
When to Use data.table
For very large datasets, data.table is another
important R ecosystem.
It provides highly efficient data manipulation and is widely used in large data-processing workflows.
The choice between tidyverse and data.table should depend on:
- Team standards
- Performance requirements
- Programmer expertise
- Existing infrastructure
- Maintainability
Do Not Turn Package Choice Into a Religion
Clinical statistical programming is about producing correct, traceable, maintainable analyses.
The question should not be:
The better questions are:
- Does it implement the required analysis correctly?
- Can the team maintain it?
- Can the result be validated?
- Is performance adequate?
- Is the workflow reproducible?
- Is the code understandable to reviewers?
Production R Versus Exploratory R
The same language can be used for radically different purposes.
| Exploratory R | Production Clinical R |
|---|---|
| Rapid experimentation | Controlled analysis workflow |
| Console commands | Version-controlled scripts |
| Flexible data manipulation | Prespecified derivations |
| Informal visualization | Validated clinical outputs |
| Ad hoc decisions | Documented analysis specifications |
| Fast iteration | Traceability and reproducibility |
Learning to distinguish these modes is one of the most important skills for a clinical statistician adopting R.
A Practical Learning Sequence
A Recommended First Clinical R Project
A particularly useful learning exercise is to build a small mock clinical trial from beginning to end.
For example, create:
- ADSL
- ADAE
- ADLB
- ADVS
- ADTTE
Then implement:
- Demographic summary table
- Safety population counts
- Adverse-event summary
- Laboratory change-from-baseline analysis
- Vital-sign summary
- Kaplan-Meier curve
- Longitudinal efficacy figure
- One inferential treatment comparison
This teaches substantially more than isolated R exercises because the pieces must interact.
Example Project Architecture
study_r_project/
├── study_r_project.Rproj
├── data/
│
├── R/
│ ├── 01_import.R
│ ├── 02_adsl.R
│ ├── 03_adae.R
│ ├── 04_adlb.R
│ ├── 05_adtte.R
│ ├── 06_tables.R
│ └── 07_figures.R
│
├── functions/
│ ├── summary_functions.R
│ ├── formatting_functions.R
│ └── qc_functions.R
│
├── output/
│ ├── tables/
│ ├── figures/
│ └── listings/
│
└── qc/
├── checks.R
└── independent_results/
Clinical R Programming Checklist
The Most Important Mindset Shift
Learning R syntax is relatively easy.
Becoming a strong clinical R programmer requires something deeper.
You must learn to translate:
For example, a statistician may specify:
"Estimate the treatment difference in change from baseline at Week 12 using the full analysis set."
The programmer must translate that into a sequence of concrete decisions:
- What constitutes the full analysis set?
- Which assessment is Week 12?
- How is baseline defined?
- What happens if Week 12 is missing?
- Are unscheduled visits eligible?
- Which statistical model is specified?
- Which treatment is the reference?
- What confidence interval is required?
- How are results rounded?
- How is the output validated?
That translation process—not memorizing R functions—is the core skill.
What to Learn Next
Once the fundamentals in this tutorial are comfortable, the next level should move toward clinical-trial-specific programming rather than generic R syntax.
High-value topics include:
- Advanced
dplyrprogramming - Advanced joins and data validation
- ADSL construction
- ADAE programming
- ADLB and laboratory shift tables
- ADVS programming
- ADTTE and survival analysis
- Time-to-event derivations
- MMRM and longitudinal models
- Mixed-effects models
- Generalized linear models
- Multiple imputation
- Estimand-aligned analyses
- RECIST and oncology ADaM programming
- Automated TLF generation
- R Markdown and Quarto
- Package development
- Unit testing
- Independent QC
- Reproducible regulatory submissions
Summary
R provides clinical statisticians with a flexible environment for data manipulation, statistical analysis, visualization, simulation, and automated reporting.
The basic language is relatively small:
- Objects
- Vectors
- Data frames
- Functions
- Expressions
- Conditionals
- Loops
- Packages
But those building blocks can support sophisticated clinical-trial workflows.
The most important early skills are:
| Skill | Why It Matters |
|---|---|
| Data manipulation | Clinical analyses depend heavily on precise data transformations. |
| Functions | Reusable logic improves consistency and maintainability. |
| Dates and factors | Clinical datasets depend heavily on time and categorical variables. |
| Missing-data handling | Missingness must be handled deliberately rather than accidentally. |
| Visualization | Figures reveal patterns that summary statistics may hide. |
| Statistical modeling | R provides extensive methods for clinical analysis. |
| Validation | Executable code is not automatically correct code. |
| Reproducibility | Clinical results must be traceable and reproducible. |
References
R Core Team. R: A Language and Environment for Statistical Computing.
R Foundation for Statistical Computing, Vienna, Austria.
Wickham, H., Averick, M., Bryan, J., et al. (2019). Welcome to the Tidyverse. Journal of Open Source Software, 4(43), 1686.
Wickham, H., Çetinkaya-Rundel, M., & Grolemund, G. R for Data Science.
O'Reilly Media.
Wickham, H. 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.
RStudio / Posit. RStudio IDE Documentation.
The R Foundation. Writing R Extensions.
Carpenter, J. R., & Kenward, M. G. Multiple Imputation and its Application.
Wiley.
Therneau, T. M. A Package for Survival Analysis in R. survival package documentation.