Tutorials › Biostatistics › R for Clinical Trial Statisticians: Getting Started

Statistical Programming

R for Clinical Trial Statisticians: Getting Started

A practical introduction to R for statisticians and statistical programmers working in clinical trials, covering the R language, data manipulation, clinical-trial analysis datasets, statistical methods, visualization, functions, reproducible workflows, validation, and the transition from traditional statistical programming approaches to modern R workflows.

Intermediate 25 min read

What You'll Learn

  • How R differs conceptually from traditional clinical-trial programming
  • How to structure an R clinical-trial project
  • How vectors, data frames, tibbles, factors, and dates work
  • How to manipulate clinical-trial data with dplyr and tidyr
  • How to build ADaM-style analysis workflows in R
  • How to perform statistical analyses, create figures, and validate results

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.

Core idea: R is not a replacement for statistical knowledge. It is a programming environment in which statistical methods, data transformations, visualizations, and reporting workflows can be implemented reproducibly.

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:

Project principle: Code should not depend on the programmer's current working directory, desktop layout, or manually selected files.

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.

Clinical programming warning: 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:

  1. Placebo
  2. Drug A
  3. Drug B

Explicitly specifying that order avoids accidental alphabetical ordering.

adsl$TRT01P <-
  factor(
    adsl$TRT01P,
    levels = c(
      "Placebo",
      "Drug A",
      "Drug B"
    )
  )
Production-programming principle: Never assume that the default ordering of categorical values is appropriate for a clinical-trial output.

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:

$$ ADY = DATE - FIRSTDOSEDT + 1 $$

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)
Do not repeatedly install packages inside analysis programs. Installation and analysis execution should generally be treated as separate activities.

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.

Clinical-table warning: Never assume that a percentage denominator is obvious. A table specification should explicitly define whether the denominator is all treated patients, patients in a treatment arm, patients with nonmissing assessments, responders, or another analysis population.

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.

Key validation question: Before every join, understand the expected cardinality of both datasets: one-to-one, one-to-many, many-to-one, or many-to-many.

ADSL as the Analysis Backbone

In CDISC-oriented clinical-trial programming, ADSL often serves as the patient-level foundation for many downstream analyses.

Conceptually:

1
SDTM provides standardized clinical observations.
2
ADaM datasets transform those observations into analysis-ready structures.
3
ADSL provides treatment, population, demographic, and key study-level variables.
4
Other ADaM datasets provide endpoint-specific longitudinal information.
5
R programs derive summaries, statistical analyses, figures, and reports.

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:

$$ CHG = AVAL - BASE $$

In R:

eff |>
  mutate(
    CHG = AVAL - BASE
  )

Percent Change From Baseline

Percentage change is:

$$ PCHG = \frac{AVAL-BASE}{BASE} \times 100 $$

In R:

eff |>
  mutate(
    PCHG =
      100 * (AVAL - BASE) / BASE
  )

For Patient 001:

$$ PCHG = \frac{70-100}{100} \times100 = -30\% $$

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.

Important: The existence of an R function does not mean that it implements the exact analysis required by a protocol or SAP. Statistical programmers must understand the method, assumptions, estimand, population, variance assumptions, missing data handling, and reporting requirements before using a function in a production analysis.

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 + AGE
can 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:

$$ Y = \beta_0+ \beta_1 Treatment+ \beta_2 Baseline+ \epsilon $$

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:

1
Create or import the analysis dataset.
2
Derive analysis variables.
3
Create a clearly defined plotting dataset.
4
Validate the plotting dataset.
5
Generate the figure.

This creates a clearer separation between:

$$ \text{Data Derivation} \rightarrow \text{Analysis} \rightarrow \text{Display} $$

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:

1
SDTM: standardized clinical observations.
2
ADaM: analysis-ready datasets and traceable derivations.
3
R: statistical analysis, visualization, reporting, and automation.

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$TRT01p
instead of:

adsl$TRT01P
can cause an error or, depending on the context, an incorrect derivation.

Programming habit: Treat variable names as exact identifiers. Do not rely on visual similarity.

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.

$$ \text{Successful Execution} \neq \text{Correct Analysis} $$

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 == y
is 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:

$$ Input + Code + Environment \rightarrow Reproducible Output $$

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:

Good abstraction removes repeated logic without hiding clinically important derivations.

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:

$$ Source \rightarrow Derivation \rightarrow Analysis Variable \rightarrow Result $$

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:

$$ \text{Execute Procedure} $$

it is often more useful to think:

$$ \text{Create Object} \rightarrow \text{Transform Object} \rightarrow \text{Pass Object to Function} \rightarrow \text{Create New Object} $$

This mental model becomes increasingly important as R programs become more advanced.

A Practical Clinical-Trial Workflow

1
Read the analysis specification.
2
Identify the source datasets and variables.
3
Import or access the required data.
4
Check structure, keys, missingness, and record counts.
5
Derive the analysis population.
6
Derive endpoint-specific analysis variables.
7
Create analysis-ready summary data.
8
Run the statistical method.
9
Generate tables, listings, and figures.
10
Validate both intermediate datasets and final outputs.

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
  )
Clinical endpoint warning: This is a programming demonstration, not a substitute for a formal oncology response derivation. Real RECIST endpoints require the complete response assessment framework.

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.

Best practice: Maintain analysis precision internally and apply presentation formatting as late as practical in the reporting pipeline.

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:

$$ Code \rightarrow Analysis \rightarrow Output \rightarrow Narrative $$

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:

1
Generate simulated trial data.
2
Apply the planned analysis.
3
Record whether the statistical criterion is met.
4
Repeat hundreds or thousands of times.
5
Estimate operating characteristics from the simulations.

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

  1. Writing everything in the console. Console experiments are useful, but production logic belongs in saved, version-controlled scripts.
  2. Using absolute file paths. This makes programs difficult to reproduce.
  3. Ignoring variable classes. Dates, factors, characters, integers, and numerics behave differently.
  4. Using na.rm = TRUE without understanding the analysis. This can conceal missing-data problems.
  5. Assuming joins are harmless. Incorrect joins can silently duplicate observations.
  6. Relying on default factor ordering. This can produce incorrectly ordered tables and figures.
  7. Putting too much logic inside one expression. Readable intermediate objects are often easier to validate.
  8. Assuming successful execution means correctness. A program can run perfectly and still implement the wrong analysis.
  9. Using statistical functions without understanding their assumptions. The function call is not the statistical analysis plan.
  10. 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.

Clinical programming principle: Optimize production R code for correctness, traceability, readability, and maintainability—not merely for the smallest number of lines.

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.

A
Source clinical data
B
Population derivation
C
Baseline derivation
D
Endpoint derivation
E
Statistical analysis dataset
F
Statistical model or summary
G
Table / figure / listing

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:

$$ \text{Which R framework is "best"?} $$

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

1
Learn vectors, objects, data frames, indexing, and functions.
2
Learn dplyr filtering, selecting, mutating, joining, grouping, and summarizing.
3
Learn dates, factors, missing values, and reshaping.
4
Learn ggplot2 and clinical statistical graphics.
5
Recreate simple descriptive clinical-trial tables.
6
Build analysis datasets from simplified SDTM/ADaM-like data.
7
Implement common statistical models.
8
Learn functions, reusable programming, and project architecture.
9
Add validation and independent QC.
10
Move toward reproducible production workflows.

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

1
Is the analysis population explicitly defined?
2
Are source variables and datasets identified?
3
Are dates represented using appropriate date classes?
4
Are categorical variables ordered intentionally?
5
Are joins checked for unexpected record multiplication?
6
Are missing values handled according to the analysis specification?
7
Are derived variables traceable to their source?
8
Are statistical procedures consistent with the SAP?
9
Are outputs independently checked?
10
Can another programmer reproduce the result?

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:

$$ \text{Statistical Specification} \rightarrow \text{Data Logic} \rightarrow \text{R Code} \rightarrow \text{Validated Result} $$

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 dplyr programming
  • 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.
Bottom line: For a clinical trial statistician, learning R should not be approached as learning another programming syntax. The real objective is to develop a reproducible programming workflow that translates statistical specifications into transparent data derivations, validated analyses, and traceable clinical outputs. Start with R fundamentals, then quickly apply them to realistic clinical-trial datasets. The fastest route to proficiency is not solving generic programming exercises—it is rebuilding familiar clinical analyses in R.

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.

This version is deliberately aimed at **clinical statisticians/statistical programmers rather than generic R beginners**, with the progression from R fundamentals → `dplyr`/`tidyr` → ADaM-style derivations → statistical models → TLFs → validation/reproducibility. Would you like the next version to go **more advanced into production R/ADaM programming** or **more beginner-friendly for statisticians completely new to R**?