Skip to contents

#Introduction This vignette demonstrates how to use the clinicalTrialSimulator package to simulate a simple clinical trial from start to finish. We will simulate patient demographics, randomize patients to treatment arms, generate time-to-event outcomes (e.g., survival), simulate dropouts (including some related to adverse events), produce longitudinal repeated measures, and finally compile the subject-level analysis dataset (ADSL).

Simulating Patient Characteristics

First, we simulate the baseline patient demographics and site information. In this example, we simulate 100 patients across 2 countries (with 5 sites per country). Each patient has an ID, is assigned to a country and site, and has attributes like age and sex.

# Simulate baseline data for 100 patients in 2 countries with 5 sites each
patients <- simulate_patients(n = 100, countries = 2, sites_per_country = 5,
                              mean_age = 50, sd_age = 10, prop_female = 0.5)
# Inspect first few patients
head(patients, 10)
##    ID  Country   Site Age    Sex
## 1   1 Country1  Site3  54 Female
## 2   2 Country1  Site3  46   Male
## 3   3 Country2 Site10  56   Male
## 4   4 Country1  Site2  48 Female
## 5   5 Country2  Site6  53   Male
## 6   6 Country1  Site5  61   Male
## 7   7 Country1  Site4  54   Male
## 8   8 Country2  Site6  47   Male
## 9   9 Country2  Site9  61   Male
## 10 10 Country2 Site10  60   Male

Each row represents a patient with their demographic information. We see columns for ID, Country, Site, Age, and Sex. These will serve as the starting point for further simulations.

##Treatment Allocation

Next, we randomize patients into treatment groups. Let’s assume a two-arm trial with equal allocation between a placebo and an active drug.

# Randomly allocate patients to two arms: Placebo vs Drug (1:1 ratio)
patients <- simulate_allocation(patients, arm_names = c("Placebo", "Drug"), ratio = c(1, 1))
table(patients$Arm)
## 
## Placebo    Drug 
##      49      51
head(patients, 5)
##   ID  Country   Site Age    Sex     Arm
## 1  1 Country1  Site3  54 Female    Drug
## 2  2 Country1  Site3  46   Male    Drug
## 3  3 Country2 Site10  56   Male Placebo
## 4  4 Country1  Site2  48 Female    Drug
## 5  5 Country2  Site6  53   Male    Drug

The output shows the distribution of patients between the “Placebo” and “Drug” arms and the updated data frame now includes an Arm column indicating each patient’s assigned group.

##Simulating Survival (Time-to-Event) Outcome

Suppose the primary endpoint is a time-to-event outcome (e.g., overall survival). We will simulate survival times with a specified median for the placebo group and a hazard ratio for the drug group. Here we assume the placebo median survival is 24 months and the drug is expected to improve survival with a hazard ratio of 0.6 (i.e., 40% reduction in hazard). We also define a maximum study follow-up of 36 months.

# Simulate survival times (e.g., overall survival) with median 24 for placebo and HR 0.6 for drug
patients <- simulate_survival(patients, median_time_control = 24, hr = 0.6, study_duration = 36)
# Check a summary of event status by arm
with(patients, table(Arm, EventStatus))
##          EventStatus
## Arm        0  1
##   Placebo 21 28
##   Drug    24 27
head(patients[c("ID","Arm","EventTime","EventStatus")], 10)
##    ID     Arm EventTime EventStatus
## 1   1    Drug 31.079753           1
## 2   2    Drug 36.000000           0
## 3   3 Placebo 10.405513           1
## 4   4    Drug 17.747074           1
## 5   5    Drug 36.000000           0
## 6   6 Placebo 21.626910           1
## 7   7    Drug  5.427236           1
## 8   8 Placebo 13.279536           1
## 9   9    Drug 10.094406           1
## 10 10 Placebo 36.000000           0

The EventTime column represents either the time of event (if the event occurred) or the censoring time (if the patient did not have an event by end of follow-up). EventStatus is 1 if the event occurred, or 0 if the time is a censoring time (e.g., the patient was event-free at study end or dropped out before event as we will simulate next). The contingency table above shows how many events (1) and censored observations (0) occurred in each arm; we expect fewer events in the Drug arm due to the hazard reduction.

##Simulating Dropouts Patients may drop out of the trial (for reasons like loss to follow-up or withdrawal due to adverse events). We simulate dropouts such that about 30% of patients withdraw by 36 months. We will also illustrate an original feature: increasing dropout risk for patients who experience a serious adverse event (AE). Let’s say 20% of patients will have a simulated serious AE, and those patients have twice the hazard of dropout.

# Simulate dropouts: 30% dropout rate by 36 months
# 20% of patients have a serious AE which doubles their dropout hazard
patients <- simulate_dropout(patients, dropout_rate = 0.3, study_duration = 36,
                             p_ae = 0.2, dropout_hr_ae = 2)
# Inspect dropout and AE columns
with(patients, table(Arm, Dropped))
##          Dropped
## Arm       FALSE TRUE
##   Placebo    36   13
##   Drug       35   16
with(patients, table(SeriousAE, Dropped))
##          Dropped
## SeriousAE FALSE TRUE
##     FALSE    59   20
##     TRUE     12    9
head(patients[c("ID","Arm","SeriousAE","DropoutTime","Dropped")], 10)
##    ID     Arm SeriousAE DropoutTime Dropped
## 1   1    Drug     FALSE   18.147944    TRUE
## 2   2    Drug      TRUE    6.225057    TRUE
## 3   3 Placebo     FALSE          NA   FALSE
## 4   4    Drug     FALSE          NA   FALSE
## 5   5    Drug     FALSE   21.314232    TRUE
## 6   6 Placebo     FALSE    5.307033    TRUE
## 7   7    Drug     FALSE          NA   FALSE
## 8   8 Placebo     FALSE          NA   FALSE
## 9   9    Drug     FALSE          NA   FALSE
## 10 10 Placebo     FALSE          NA   FALSE

We can see the SeriousAE flag indicating which patients had the adverse event and the DropoutTime and Dropped columns indicating if and when patients left the study. The first table shows the number of dropouts (Dropped=TRUE) by arm, and the second table illustrates that patients with serious AEs tend to have a higher dropout rate (rows where SeriousAE=TRUE likely show more dropouts). Note: Patients who died (had the event) before they could drop out are treated as not dropped. The simulation logic censors the survival time at dropout if dropout occurs first, or ignores the dropout if an event occurred first. Thus, each patient’s timeline is appropriately handled.

Simulating Longitudinal Outcomes

Now let’s simulate a longitudinal outcome, for example a biomarker measured at clinic visits over time. We’ll assume visits at baseline and every 6 months up to 24 months (so 0, 6, 12, 18, 24). We also incorporate a treatment effect on this biomarker: perhaps the biomarker decreases over time in the treatment group compared to placebo. We set up a scenario where the placebo group stays roughly constant on average, while the treatment group sees an improvement (decrease) of about 1 unit every 6 months. We also include a random site effect to demonstrate multi-level data (each site has a random offset on the biomarker).

# Define planned visit times (months)
visit_schedule <- c(0, 6, 12, 18, 24)
# Generate the schedule of visits for each patient (accounting for dropouts and deaths)
visits <- simulate_visits(patients, visit_times = visit_schedule)
# Simulate longitudinal biomarker outcomes:
# baseline mean = 100, control group slope ~ 0 per month, treatment slope = -0.2 per month (improvement),
# measurement error SD = 5, and random site intercept SD = 3.
visit_data <- simulate_longitudinal_outcomes(visits, patients,
                                             baseline_mean = 100, baseline_sd = 10,
                                             control_slope = 0, treatment_slope = -0.2,
                                             error_sd = 5,
                                             cluster_var = "Site", cluster_effect_sd = 3)
# Look at some longitudinal data points
head(visit_data, 10)
##    ID Time     Arm   Site  Country   Outcome
## 1   1    0    Drug  Site3 Country1  98.27484
## 2   1    6    Drug  Site3 Country1 111.83072
## 3   1   12    Drug  Site3 Country1  95.44381
## 4   1   18    Drug  Site3 Country1  98.60257
## 5   2    0    Drug  Site3 Country1  99.38935
## 6   2    6    Drug  Site3 Country1 101.99665
## 7   3    0 Placebo Site10 Country2  98.83889
## 8   3    6 Placebo Site10 Country2 104.58937
## 9   4    0    Drug  Site2 Country1  96.50052
## 10  4    6    Drug  Site2 Country1 100.27258

In the visit_data, each patient has multiple rows (one per visit) with an Outcome value for the biomarker. The Arm, Site, and Country columns provide context for each measurement. Patients who dropped out or died before certain visits will have no entries for those later times (e.g., if a patient dropped at month 18, you will not see a 24-month record for them). We can examine the treatment effect by comparing the biomarker values at baseline and 24 months for each arm:

# Baseline and final visit outcomes by treatment arm
baseline_data <- subset(visit_data, Time == 0)
final_data <- subset(visit_data, Time == 24)
cat("Mean outcome at baseline by arm:\n")
## Mean outcome at baseline by arm:
print(tapply(baseline_data$Outcome, baseline_data$Arm, mean))
##  Placebo     Drug 
## 99.19378 99.88680
cat("Mean outcome at 24 months by arm:\n")
## Mean outcome at 24 months by arm:
print(tapply(final_data$Outcome, final_data$Arm, mean))
##  Placebo     Drug 
## 97.94677 98.08435

We expect the baseline means to be roughly equal (by design), and by 24 months the Drug arm has a lower mean outcome (indicating improvement) compared to Placebo. The random site effect and error add variability, so these are average trends.

Simulating a Binary Outcome

For illustration, let’s also simulate a binary endpoint, such as an overall response (Yes/No) at end of study. We will assume, for example, a 40% response rate in the placebo group and a 60% response rate in the drug group. We’ll add this as a single outcome per patient.

# Simulate a binary outcome (e.g., treatment response)
patients <- simulate_outcomes(patients, outcome_type = "binary",
                              outcome_prob_control = 0.4, outcome_prob_treatment = 0.6)
# Response rates by arm
with(patients, table(Arm, Outcome))
##          Outcome
## Arm       No Yes
##   Placebo  9   4
##   Drug     5  11
head(patients[c("ID","Arm","Outcome")], 10)
##    ID     Arm Outcome
## 1   1    Drug    <NA>
## 2   2    Drug    <NA>
## 3   3 Placebo    <NA>
## 4   4    Drug    <NA>
## 5   5    Drug    <NA>
## 6   6 Placebo    <NA>
## 7   7    Drug    <NA>
## 8   8 Placebo    <NA>
## 9   9    Drug    <NA>
## 10 10 Placebo      No

The output shows the number of responders (“Yes”) vs non-responders (“No”) in each arm. The simulation assigns “Yes”/“No” taking into account the specified probabilities. Note that patients who dropped out or died have their Outcome set to NA (missing), as they did not complete the study to have a measured response.

Creating the ADSL Dataset

Finally, we compile all the simulated data into a single subject-level analysis dataset (ADSL). This will include one row per patient with their baseline info, treatment, and outcomes.

adsl <- export_adam_format(patients, study_id = "SIMTRIAL01")
# Examine the first few records of ADSL
head(adsl, 5)
##      STUDYID      USUBJID ID  Country   Site     Arm Age    Sex SeriousAE
## 1 SIMTRIAL01 SIMTRIAL01-1  1 Country1  Site3    Drug  54 Female     FALSE
## 2 SIMTRIAL01 SIMTRIAL01-2  2 Country1  Site3    Drug  46   Male      TRUE
## 3 SIMTRIAL01 SIMTRIAL01-3  3 Country2 Site10 Placebo  56   Male     FALSE
## 4 SIMTRIAL01 SIMTRIAL01-4  4 Country1  Site2    Drug  48 Female     FALSE
## 5 SIMTRIAL01 SIMTRIAL01-5  5 Country2  Site6    Drug  53   Male     FALSE
##   DropoutTime Dropped EventTime EventStatus Outcome
## 1   18.147944    TRUE 18.147944           0    <NA>
## 2    6.225057    TRUE  6.225057           0    <NA>
## 3          NA   FALSE 10.405513           1    <NA>
## 4          NA   FALSE 17.747074           1    <NA>
## 5   21.314232    TRUE 21.314232           0    <NA>

The adsl data frame contains columns like STUDYID (study identifier), USUBJID (unique subject ID), treatment Arm, demographic variables (Age, Sex, etc.), and the various outcomes: - EventTime and EventStatus (time-to-event outcome and censoring status), - DropoutTime and Dropped (dropout information), - Outcome (our binary response outcome in this case). Each patient’s row aggregates all their relevant information. For example, if a patient died during the trial, EventStatus=1 and EventTime is when it happened; if a patient dropped out, Dropped=TRUE and DropoutTime is given (and their Outcome may be NA due to missing data from dropout).

Conclusion

We have simulated a complete clinical trial dataset. The clinicalTrialSimulator package allowed us to generate: - Demographics and multi-level site data, - Treatment assignments, - Time-to-event outcomes with treatment effect, - Dropouts including those triggered by adverse events, - Longitudinal repeated measures with treatment and site effects, - A binary endpoint, - A compiled ADSL dataset for analysis.