This package serves to simulate jointly distributed patient-level data from historical data based on the copula invariance property.
To consistently optimize clinical trial designs and data analysis methods through trial simulation, we need to simulate multivariate mixed-type virtual patient data independent of designs and analysis methods under evaluation. To make the outcome of optimization more realistic, we should utilize relevant empirical patient-level data when it is available.
When simulating small empirical data, the underlying marginal distributions and their dependence structure cannot be understood or verified thoroughly due to the limited sample size.
To resolve this issue, we use the copula invariance property to generate the joint distribution without making a strong parametric assumption. The theoretical background is addressed below.
The idea of copula was first introduced by Dr. Abe Sklar in 1959 in the field of statistics. He proposed a theorem, which is later called Sklar’s theorem. This theorem essentially consists of two parts. First, the copula function can be used to describe the relationship between the joint and marginal distributions. This function assigns the value of joint distribution to each ordered pair of values of marginal distributions. That is, the coupla function maps the range of joint distribution from a d-dimensional ball to a unit line. The second part is that the copula function can be uniquely determined for every joint distribution.
Each joint density can be viewed as the product of marginal densities multiplied by copula density. The copula density, which is defined as the partial derivative of the copula function, contains all the information about the dependence structure of the joint distribution. As a result, the joint distribution can be flexibly constructed by copula dependency and marginal distributions.
To share this finding with the community, we have implemented the copula algorithm into a new R package entitled copulaSim. The copulaSim package is designed to perform virtual patient simulation. The idea of the copula simulation algorithm is given in the following. Based on the copula invariance property, the dependence structure of the joint distribution can be well preserved when performing quantile transformation. Because of this feature, the copula simulation algorithm allows for the simulated data to resemble the empirical data.

library(mvtnorm)
arm1 <- rmvnorm(n = 30, mean = rep(10, 5), sigma = diag(5) + 0.5)
test_data <- as.data.frame(cbind(1:30, rep(1, 30), arm1))
colnames(test_data) <- c("id","arm",paste0("time_", 1:5))
knitr::kable((test_data), "simple")| id | arm | time_1 | time_2 | time_3 | time_4 | time_5 |
|---|---|---|---|---|---|---|
| 1 | 1 | 9.627204 | 8.637565 | 9.124648 | 9.358849 | 7.042886 |
| 2 | 1 | 9.400817 | 9.539136 | 8.626390 | 10.721891 | 9.849703 |
| 3 | 1 | 10.525953 | 10.962487 | 10.655943 | 9.567267 | 11.557820 |
| 4 | 1 | 9.500646 | 9.654667 | 11.917564 | 9.839404 | 9.955671 |
| 5 | 1 | 10.832911 | 8.543376 | 9.853115 | 9.354814 | 9.880304 |
| 6 | 1 | 9.786971 | 9.475524 | 8.512444 | 7.455937 | 7.716020 |
| 7 | 1 | 6.877272 | 6.619353 | 10.226618 | 9.729260 | 9.976880 |
| 8 | 1 | 10.293420 | 9.151424 | 10.146273 | 9.824384 | 9.674186 |
| 9 | 1 | 10.905704 | 10.459512 | 9.515360 | 10.644187 | 9.279326 |
| 10 | 1 | 8.889984 | 8.575109 | 7.987337 | 6.560646 | 7.898952 |
| 11 | 1 | 8.322391 | 9.357010 | 8.646798 | 8.458873 | 8.563257 |
| 12 | 1 | 9.753353 | 10.646491 | 10.573054 | 10.387478 | 9.112172 |
| 13 | 1 | 11.611104 | 9.518441 | 10.805418 | 9.676354 | 10.589335 |
| 14 | 1 | 9.185999 | 7.379375 | 7.592594 | 9.454711 | 9.452269 |
| 15 | 1 | 10.117739 | 9.465760 | 9.955721 | 9.977785 | 9.442585 |
| 16 | 1 | 11.447906 | 10.400886 | 9.798964 | 9.663380 | 11.471220 |
| 17 | 1 | 11.280993 | 9.311273 | 9.163257 | 10.698507 | 9.125908 |
| 18 | 1 | 11.113115 | 10.398463 | 11.250797 | 10.049011 | 10.419165 |
| 19 | 1 | 11.280591 | 10.723999 | 11.813133 | 10.089103 | 10.768929 |
| 20 | 1 | 11.884771 | 8.270124 | 8.992611 | 9.384906 | 11.648840 |
| 21 | 1 | 8.695082 | 9.594781 | 9.010456 | 8.937616 | 8.175465 |
| 22 | 1 | 11.926154 | 11.138196 | 9.636946 | 9.522035 | 8.282632 |
| 23 | 1 | 10.386927 | 9.642483 | 8.128166 | 8.132292 | 9.732986 |
| 24 | 1 | 11.952990 | 11.287934 | 8.592102 | 11.808327 | 11.622872 |
| 25 | 1 | 10.161042 | 9.540962 | 10.618746 | 9.120664 | 8.873743 |
| 26 | 1 | 12.233172 | 12.009369 | 10.349465 | 11.754983 | 10.512409 |
| 27 | 1 | 9.048274 | 8.515529 | 7.171500 | 9.123058 | 9.653560 |
| 28 | 1 | 9.867943 | 8.786860 | 9.542485 | 11.213716 | 9.813047 |
| 29 | 1 | 10.869935 | 10.848680 | 9.673252 | 10.773960 | 10.039237 |
| 30 | 1 | 9.292278 | 11.528461 | 9.953363 | 10.940886 | 11.782387 |
| Argument | Definition | Assigned Value |
|---|---|---|
| data.input | The empirical data | test_data[,-c(1,2)] |
| id.vec | ID fo individual patient in the input data | test_data$id |
| arm.vec | The column to identify the arm in clinical trial | test_data$arm |
| n.patient | The targeted number of patients in each simulated dataset | 50 |
| n.simulation | The number of simulated datasets | 1 |
| seed | The random seed to reproduce the simulation study | 2022 |
| validation.type | Specify hypothesis test to detect the difference between empirical data and simulated data | “energy” |
| verbose | Whether to print message for simulation process or not | TRUE |
As a means of avoiding extreme sampling results when performing one simulated dataset, it is advised to use “energy” or “ball” in the argument validation.type. The purpose of doing this is to perform data validation, which ensures the similarity between empirical data and the simulated data based on 2-sample test.
Below shows the 2-sample test result, which indicates that the joint distribution of the simulated data is not significantly different from the empirical data.
## Generate 1 simulated dataset
simu_S1 <- copula.sim(data.input = test_data[,-c(1,2)],
id.vec = test_data$id,
arm.vec = test_data$arm,
n.patient = 50 ,
n.simulation = 1,
seed = 2022,
validation.type = "energy",
verbose = TRUE)## Simulate 1th Dataset
## p.value for energy test: 0.7410
## Compelete simulating 1th Dataset in 0.112 seconds
## # A tibble: 250 × 6
## id arm col.num col.name data.sim sim.id
## <dbl> <dbl> <dbl> <chr> <dbl> <int>
## 1 1 1 1 time_1 11.1 1
## 2 2 1 1 time_1 9.20 1
## 3 3 1 1 time_1 11.9 1
## 4 4 1 1 time_1 11.8 1
## 5 5 1 1 time_1 9.25 1
## 6 6 1 1 time_1 9.76 1
## 7 7 1 1 time_1 11.9 1
## 8 8 1 1 time_1 10.3 1
## 9 9 1 1 time_1 8.95 1
## 10 10 1 1 time_1 10.0 1
## # ℹ 240 more rows
library(dplyr)
## Obtain the empirical long-form dataset
empir <- simu_S1$data.input.long %>% mutate(cate = "empirical_n30") %>% rename(data = data.input)
## Produce the marginal density plot
simul <- simu_S1$data.simul %>% mutate(cate = "copulaSim_n50") %>%
rename(data = data.sim) %>% select(-sim.id)
library(ggplot2)
rbind(empir, simul) %>% filter(grepl('time', col.name)) %>%
ggplot(aes(x = data, color = cate, fill = cate)) +
facet_wrap(.~col.name, ncol = 5) +
geom_density(alpha = 0.001, size = 1)## Converting the long-form simulated dataset to wide-form
simu.wide <- extract.data.sim(simu_S1)
simu.wide## $`sim.id=1`
## $`sim.id=1`$`arm=1`
## time_1 time_2 time_3 time_4 time_5
## [1,] 11.127030 11.325799 11.786916 11.769028 11.623775
## [2,] 9.203379 11.054563 10.525130 8.630098 7.353626
## [3,] 11.924163 11.812299 11.844487 11.806753 11.479405
## [4,] 11.782184 10.671019 10.598892 10.565174 11.773225
## [5,] 9.249597 7.703238 10.030171 9.672915 9.954657
## [6,] 9.759826 8.532776 8.634592 9.728963 10.550438
## [7,] 11.911844 10.918633 9.594576 10.703994 11.603200
## [8,] 10.339377 8.527943 7.389786 9.167303 9.444960
## [9,] 8.946892 9.539930 9.783363 10.031107 8.893265
## [10,] 10.045914 9.962378 8.532143 9.540387 10.551221
## [11,] 9.447876 7.729853 10.610530 8.381324 11.550052
## [12,] 10.865246 9.468578 9.129310 7.169852 7.720598
## [13,] 9.782136 9.642103 10.582774 8.811649 9.475999
## [14,] 11.996090 10.984104 11.788005 11.203946 11.218175
## [15,] 10.856761 9.613014 8.545528 8.221973 9.967647
## [16,] 9.631585 8.174877 7.988731 8.384340 9.780465
## [17,] 8.644228 7.449185 7.805120 7.101433 7.161039
## [18,] 8.530870 6.813254 8.090133 6.821707 7.152442
## [19,] 10.847863 8.523417 10.776287 8.367029 9.145951
## [20,] 8.588792 8.877201 8.746274 7.471506 7.839983
## [21,] 10.892390 9.528601 9.626109 10.812180 11.521295
## [22,] 7.780555 8.272926 9.424159 10.009823 9.137528
## [23,] 9.620048 9.649411 10.659039 10.540234 9.789219
## [24,] 11.280750 11.113207 9.125195 9.698404 9.349368
## [25,] 9.280459 9.578634 10.774475 9.675663 9.957034
## [26,] 10.228388 9.165494 7.380182 9.037418 9.663526
## [27,] 10.447642 9.574762 7.648439 9.224875 9.117408
## [28,] 10.851224 9.479127 9.923657 10.944821 11.484996
## [29,] 11.201100 9.474327 10.215350 10.769165 11.431480
## [30,] 8.636720 9.412743 10.196494 10.731829 9.289992
## [31,] 11.477822 10.999094 10.766194 9.674396 8.355948
## [32,] 10.889466 10.946969 8.568303 9.553431 9.448822
## [33,] 11.014796 8.605659 8.644384 9.451497 9.915779
## [34,] 10.131326 10.399864 9.998449 10.658874 11.623009
## [35,] 9.475226 9.483848 11.888333 10.664428 10.451769
## [36,] 11.615231 8.664343 9.563163 9.172283 9.924852
## [37,] 10.244199 8.414233 9.694495 8.964739 8.594206
## [38,] 9.064099 8.563456 9.671788 9.601551 11.528291
## [39,] 10.853139 9.539907 9.949911 9.098377 9.970234
## [40,] 9.398315 8.594853 10.614556 8.729805 9.867850
## [41,] 10.307585 10.550908 10.244333 10.015723 9.823887
## [42,] 8.875791 8.889099 9.953758 9.121014 10.525687
## [43,] 10.249062 11.108764 8.608275 9.028071 7.824461
## [44,] 9.580994 7.864856 10.176663 9.357681 9.236152
## [45,] 8.990349 6.761218 9.516359 8.519611 9.488778
## [46,] 8.020852 7.913411 9.681183 9.523703 9.634955
## [47,] 11.895511 10.747081 9.939754 9.188391 8.705108
## [48,] 10.120452 10.693661 8.089758 9.423969 9.116244
## [49,] 9.938313 9.540008 8.057750 8.765834 8.798658
## [50,] 11.132536 9.567156 10.739242 9.667163 9.475772
## Generate 100 simulated datasets
simu_S100 <- copula.sim(data.input = test_data[,-c(1,2)],
id.vec = test_data$id,
arm.vec = test_data$arm,
n.patient = 50 ,
n.simulation = 100,
seed = 2022,
validation.type = "none",
verbose = FALSE)
## Compare the marginal mean via the function compare.copula.sim
compare <- compare.copula.sim(simu_S100)
knitr::kable(compare$mean.comparison, "simple")| marginal.name | arm | empir.sample | simu.sample | n.simu | empir.mean | simu.mean | simu.mean.low.lim | simu.mean.upp.lim | simu.mean.RB | simu.mean.SB | simu.mean.RMSE | empir.sd | simu.sd |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| time_1 | 1 | 30 | 50 | 100 | 10.2358 | 10.2774 | 10.0333 | 10.5614 | 0.0041 | 0.1063 | 0.1425 | 1.2439 | 1.1212 |
| time_2 | 1 | 30 | 50 | 100 | 9.6661 | 9.6836 | 9.3919 | 9.9572 | 0.0018 | 0.1157 | 0.1506 | 1.2221 | 1.0965 |
| time_3 | 1 | 30 | 50 | 100 | 9.5945 | 9.5729 | 9.3073 | 9.8418 | -0.0023 | 0.1032 | 0.1406 | 1.1605 | 1.0652 |
| time_4 | 1 | 30 | 50 | 100 | 9.7408 | 9.7648 | 9.4912 | 10.0138 | 0.0025 | 0.1173 | 0.1413 | 1.1444 | 1.0208 |
| time_5 | 1 | 30 | 50 | 100 | 9.7305 | 9.7423 | 9.4705 | 9.9968 | 0.0012 | 0.0983 | 0.1526 | 1.2228 | 1.1406 |
## Generate Empirical Data
## Assume that the single-arm, 3-dimensional empirical data follows multivariate normal data
arm1 <- rmvnorm(n = 80, mean = c(10,10.5,11), sigma = diag(3) + 0.5)
test_data2 <- as.data.frame(cbind(1:80, rep(1,80), arm1))
colnames(test_data2) <- c("id", "arm", paste0("time_", 1:3))
## Generate 1 simulated datasets with one empirical arm and two new-arms
## The mean difference between empirical arm and
## (i) the 1st new arm is assumed to be 2.5, 2.55, and 2.6 at each time point
## (ii) the 2nd new arm is assumed to be 4.5, 4.55, and 4.6 at each time point
newARM <- new.arm.copula.sim(data.input = test_data2[,-c(1,2)],
id.vec = test_data2$id,
arm.vec = test_data2$arm,
n.patient = 100 ,
n.simulation = 1,
seed = 2022,
shift.vec.list = list(c(2.5,2.55,2.6), c(4.5,4.55,4.6)),
verbose = FALSE)
## Obtain the simulated long-form dataset
newARM$data.simul## # A tibble: 900 × 6
## id arm col.num col.name data.sim sim.id
## <dbl> <dbl> <dbl> <chr> <dbl> <int>
## 1 1 1 1 time_1 9.63 1
## 2 2 1 1 time_1 9.65 1
## 3 3 1 1 time_1 8.24 1
## 4 4 1 1 time_1 12.1 1
## 5 5 1 1 time_1 9.12 1
## 6 6 1 1 time_1 10.0 1
## 7 7 1 1 time_1 8.56 1
## 8 8 1 1 time_1 10.6 1
## 9 9 1 1 time_1 10.5 1
## 10 10 1 1 time_1 8.48 1
## # ℹ 890 more rows
## Verify the mean difference
newARM$data.simul %>%
group_by(.data$arm, .data$col.num) %>%
summarise(N = n(), Mean = mean(.data$data.sim), SD = sd(.data$data.sim))## # A tibble: 9 × 5
## # Groups: arm [3]
## arm col.num N Mean SD
## <dbl> <dbl> <int> <dbl> <dbl>
## 1 1 1 100 9.71 1.01
## 2 1 2 100 10.2 1.26
## 3 1 3 100 10.8 1.23
## 4 2 1 100 12.0 0.975
## 5 2 2 100 12.7 1.30
## 6 2 3 100 13.3 1.14
## 7 3 1 100 14.0 0.975
## 8 3 2 100 14.7 1.30
## 9 3 3 100 15.3 1.14
| - Authored by Pei-Shan Yen |
| - CRAN page: https://CRAN.R-project.org/package=copulaSim |
| - github page: https://github.com/psyen0824/copulaSim |
This research project and the development of the R package are supported by AbbVie Experiential Internship Program. I am also grateful to Dr. Xuemin Gu, Dr. Jenny Jiao, and Dr. Jane Zhang at the Eyecare Clinical Statistics Team for valuable comments on this work.