library(SimDesign)library(SimNPH)library(parallel)cl<-makeCluster(8)clusterEvalQ(cl, {library(SimDesign)library(SimNPH)library(parallel)})This is a simple example on how to run a simulation of analyses ofdatasets with a delayed treatment effect using the max-combo and thelog-rank test. The
Setting up the simulations to be run.createDesigncreates atibble with every combination of the parameters.Each line corresponds to one simulation to be run.
The functiongenerate_delayed_effect needs the columns:n_trt: number of patients in the treatment arm,n_ctrl: number of patients in the control arm,delay: delay until onset of treatment effect,hazard_ctrl: hazard under control and before onset oftreatment effect,hazart_trt: hazard under treatment,t_max: maximum time of observation.
An example designtibble with all parameters filled outcan be created withdesing_skeleton_delayed_effect. Use thefunction to output an example function call that you can copy and modifyas needed, or assign the result to a variable to obtain a designtibble with some default parameters.
By default this will create a simulation design skeleton forsimulations of 50 patients in each arm, a constant hazard of 0.2 undercontrol and a hazard of 0.02 under treatment after the effect onsetvarying from 0 to 10.
N_sim<-100Assumptions<-assumptions_delayed_effect()#> expand.grid(#> delay=m2d(seq(0, 10, by=2)), # delay of 0, 1, ..., 10 months#> hazard_ctrl=m2r(24), # median survival control of 24 months#> hazard_trt=m2r(36), # median survival treatment of 36 months#> random_withdrawal=m2r(120) # median time to random withdrawal 10 years#> )Options<-design_fixed_followup()#> expand.grid(#> n_trt=150, # 150 patients in the treatment arm#> n_ctrl=150, # 150 patients in the control arm#> followup=m2d(24), # followup 2 years#> recruitment=m2d(6) # recruitment time 6 months#>#> )Design<-merge(Assumptions, Options,by=NULL)knitr::kable(Design)| delay | hazard_ctrl | hazard_trt | random_withdrawal | n_trt | n_ctrl | followup | recruitment |
|---|---|---|---|---|---|---|---|
| 0.000 | 0.0009489 | 0.0006326 | 0.0001898 | 150 | 150 | 730.5 | 182.625 |
| 60.875 | 0.0009489 | 0.0006326 | 0.0001898 | 150 | 150 | 730.5 | 182.625 |
| 121.750 | 0.0009489 | 0.0006326 | 0.0001898 | 150 | 150 | 730.5 | 182.625 |
| 182.625 | 0.0009489 | 0.0006326 | 0.0001898 | 150 | 150 | 730.5 | 182.625 |
| 243.500 | 0.0009489 | 0.0006326 | 0.0001898 | 150 | 150 | 730.5 | 182.625 |
| 304.375 | 0.0009489 | 0.0006326 | 0.0001898 | 150 | 150 | 730.5 | 182.625 |
Define the data generating ‘generate’ function, that besidesimulating the time to event also applies the different censoringprocesses.
Next, we need to specify a summary function that computes the desiredoperating characteristics for each simulation scenario, and eachanalysis method. In the example below, we use a summary function thatcomputes the power (as we consider scenarios under the alternative inthe example) of the log-rank test and the max-combo test acrosssimulated scenarios. For each scenario the function just averages thenumber of times the computed p-value is below the significance levelobtain the power.
The results object contains the results of all replications of thecorresponding method for each row of theDesign object. Inthis exampleresults$p contains allN_simp-values returned by theanalyse_maxcombo oranalyse_logrank functions respectively. The Summary willcontain columns with the rejection rate and some other summarystatistics for both methods.
Now we put it all together: in Design we give the scenarios definedbefore. We want to run 100 replications for each scenario. We want togenerate data using thegenerate_delayed_effect functionusing the parameters fromDesign and analyse eachreplication of each scenario with the two functionsanalyse_logrank andanalyse_maxcombo. Theoutput should be summarised with theSummarise functiondefined before and the simulations should be run in parallel.
res<-runSimulation( Design,replications = N_sim,generate = my_generator,analyse =list(logrank =analyse_logrank(),maxcombo =analyse_maxcombo() ),summarise = Summarise,cl = cl,save=FALSE)#>#> Number of parallel clusters in use: 8#>#> Simulation complete. Total execution time: 10.61s#> Warning in system2("quarto", "-V", stdout = TRUE, env = paste0("TMPDIR=", : Ausführung von Kommando '"quarto" TMPDIR=C:/Users/Tobias11/AppData/Local/Temp/RtmpGKzPDq/file530456a56ad2#> -V' ergab Status 1Finally we select the interesting columns from the output. Since allother parameters are the same for each scenario we just select delay.And we are interested in the rejection rate of the tests.
| delay | maxcombo.rejection_0.05 | logrank.rejection_0.05 |
|---|---|---|
| 0.000 | 0.54 | 0.52 |
| 60.875 | 0.42 | 0.41 |
| 121.750 | 0.45 | 0.34 |
| 182.625 | 0.24 | 0.15 |
| 243.500 | 0.18 | 0.18 |
| 304.375 | 0.21 | 0.18 |
In this scenario we extend the scenario from above to include a fixedfollowup as well as an interim analysis after a fixed number of events.For this we will define additional analyse functions.
First we extend the Design to include a column with the number ofevents after which an interim analysis should be done.
Options<-design_group_sequential()#> expand.grid(#> n_trt=200, # 200 patients in the treatment arm#> n_ctrl=200, # 200 patients in the control arm#> followup=m2d(48), # maximum followup time 4 years#> recruitment=m2d(6), # recruitment time 6 months#> interim_events=150, # interim analysis after 150 events#> final_events=300 # final analysis after 300 events#> )Design<-merge(Assumptions, Options,by=NULL)knitr::kable(Design)| delay | hazard_ctrl | hazard_trt | random_withdrawal | n_trt | n_ctrl | followup | recruitment | interim_events | final_events |
|---|---|---|---|---|---|---|---|---|---|
| 0.000 | 0.0009489 | 0.0006326 | 0.0001898 | 200 | 200 | 1461 | 182.625 | 150 | 300 |
| 60.875 | 0.0009489 | 0.0006326 | 0.0001898 | 200 | 200 | 1461 | 182.625 | 150 | 300 |
| 121.750 | 0.0009489 | 0.0006326 | 0.0001898 | 200 | 200 | 1461 | 182.625 | 150 | 300 |
| 182.625 | 0.0009489 | 0.0006326 | 0.0001898 | 200 | 200 | 1461 | 182.625 | 150 | 300 |
| 243.500 | 0.0009489 | 0.0006326 | 0.0001898 | 200 | 200 | 1461 | 182.625 | 150 | 300 |
| 304.375 | 0.0009489 | 0.0006326 | 0.0001898 | 200 | 200 | 1461 | 182.625 | 150 | 300 |
Theanalyse_group_sequential function allows to combinetwo or more analyse functions to create an analysis functioncorresponding to a group sequential design. The arguments are the timesor events after which the analyses are done, the nominal alpha at eachstage and the analyse functions to be used at each stage.
## O'Brien-Fleming Bounds for GSD with interim analysis at information time 1/2nominal_alpha<- ldbounds::ldBounds(c(0.5,1))$nom.alphaclusterExport(cl,"nominal_alpha")Analyse<-list(logrank_seq =analyse_group_sequential(followup =c(condition$interim_events, condition$final_events),followup_type =c("event","event"),alpha = nominal_alpha,analyse_functions =analyse_logrank() ),maxcombo_seq =analyse_group_sequential(followup =c(condition$interim_events, condition$final_events),followup_type =c("event","event"),alpha = nominal_alpha,analyse_functions =analyse_maxcombo() ))The output of the function created withanalyse_group_sequential contains additional columns.rejected_at_stage includes the stage at which the null wasfirst rejected orInf if the null was not rejected,N_pat andN_evt contain the number of patientsrecruited and the number of events observed before the null was rejectedandfollowup contains the time after study start at whichthe last analysis was done.
The results object also includes the results returned by each stageinresults_stages, but here we only use the overalltest-decision.
The call torunSimulation looks almost the same as abovebut now the additional columns we defined in ourSummarisefunctions are included in the result.
res<-runSimulation( Design,replications = N_sim,generate = my_generator,analyse = Analyse,summarise = Summarise,cl = cl,save=FALSE)#>#> Number of parallel clusters in use: 8#>#> Simulation complete. Total execution time: 21.83s#> Warning in system2("quarto", "-V", stdout = TRUE, env = paste0("TMPDIR=", : Ausführung von Kommando '"quarto" TMPDIR=C:/Users/Tobias11/AppData/Local/Temp/RtmpGKzPDq/file5304116837a0#> -V' ergab Status 1In the case of a group sequential design we are also interested inthe average running time of the study in terms of patients recruited,number of events and running time of the study.
res|>subset(select=c("delay","maxcombo_seq.rejection","logrank_seq.rejection","maxcombo_seq.n_pat","logrank_seq.n_pat","maxcombo_seq.n_evt","logrank_seq.n_evt","maxcombo_seq.followup","logrank_seq.followup" ))|> knitr::kable()| delay | maxcombo_seq.rejection | logrank_seq.rejection | maxcombo_seq.n_pat | logrank_seq.n_pat | maxcombo_seq.n_evt | logrank_seq.n_evt | maxcombo_seq.followup | logrank_seq.followup |
|---|---|---|---|---|---|---|---|---|
| 0.000 | 0.85 | 0.82 | 400 | 400 | 213.53 | 213.07 | 1278.40 | 1272.17 |
| 60.875 | 0.75 | 0.75 | 400 | 400 | 219.45 | 219.06 | 1300.93 | 1298.89 |
| 121.750 | 0.72 | 0.72 | 400 | 400 | 225.80 | 226.10 | 1342.47 | 1343.26 |
| 182.625 | 0.60 | 0.59 | 400 | 400 | 232.30 | 234.03 | 1380.38 | 1396.26 |
| 243.500 | 0.63 | 0.59 | 400 | 400 | 238.15 | 240.42 | 1410.94 | 1428.86 |
| 304.375 | 0.61 | 0.53 | 400 | 400 | 240.66 | 240.66 | 1443.26 | 1443.26 |
To evaluate the performance of an estimator, we first compute thevalues of some true summary statistics to which the estimates will becompared. The most relevant true summary statistics can be computed by aconvenience function for each scenario. Just pipe the Design data.frameto the function and the values of the statistics are added ascolumns.
Options<-design_fixed_followup()#> expand.grid(#> n_trt=150, # 150 patients in the treatment arm#> n_ctrl=150, # 150 patients in the control arm#> followup=m2d(24), # followup 2 years#> recruitment=m2d(6) # recruitment time 6 months#>#> )Design<-merge(Assumptions, Options,by=NULL)Design<- Design|>true_summary_statistics_delayed_effect(cutoff_stats =20)knitr::kable(Design)| delay | hazard_ctrl | hazard_trt | random_withdrawal | n_trt | n_ctrl | followup | recruitment | median_survival_trt | median_survival_ctrl | rmst_trt_20 | rmst_ctrl_20 | gAHR_20 | AHR_20 | AHRoc_20 | AHRoc_robust_20 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0.000 | 0.0009489 | 0.0006326 | 0.0001898 | 150 | 150 | 730.5 | 182.625 | 1095.7500 | 730.5 | 19.87402 | 19.81142 | 0.6666667 | 0.6666667 | 0.6666667 | 0.6666667 |
| 60.875 | 0.0009489 | 0.0006326 | 0.0001898 | 150 | 150 | 730.5 | 182.625 | 1065.3125 | 730.5 | 19.81142 | 19.81142 | 1.0000000 | 1.0000000 | 1.0000000 | 1.0000000 |
| 121.750 | 0.0009489 | 0.0006326 | 0.0001898 | 150 | 150 | 730.5 | 182.625 | 1034.8750 | 730.5 | 19.81142 | 19.81142 | 1.0000000 | 1.0000000 | 1.0000000 | 1.0000000 |
| 182.625 | 0.0009489 | 0.0006326 | 0.0001898 | 150 | 150 | 730.5 | 182.625 | 1004.4375 | 730.5 | 19.81142 | 19.81142 | 1.0000000 | 1.0000000 | 1.0000000 | 1.0000000 |
| 243.500 | 0.0009489 | 0.0006326 | 0.0001898 | 150 | 150 | 730.5 | 182.625 | 974.0000 | 730.5 | 19.81142 | 19.81142 | 1.0000000 | 1.0000000 | 1.0000000 | 1.0000000 |
| 304.375 | 0.0009489 | 0.0006326 | 0.0001898 | 150 | 150 | 730.5 | 182.625 | 943.5625 | 730.5 | 19.81142 | 19.81142 | 1.0000000 | 1.0000000 | 1.0000000 | 1.0000000 |
Summarise functionIn the Summarise function the true value against which the estimatorshould be compared has to be specified. If coverage and average width ofthe confidence intervals should be estimated, the CI bounds should alsobe specified.
The arguments to the functions are left un-evaluated and are laterevaluated in theresults andconditiondatasets respectively. So any expressions using variables from resultscan be used for the estimated value and the CI bounds and expressionsusing variables from condition can be used in the argument for the realvalue.
In this case we want to compare the hazard ratio estimated by the Coxmodel to the geometric average hazard ratio as well as to the hazardratio after onset of treatment, calculated from the two respectivecolumns of the Design data.frame.
Note that one name can be used twice to summarise the output of oneanalysis method two times, like in this case, comparing it to twodifferent summary statistics.
Analyse<-list(coxph =analyse_coxph())res<-runSimulation( Design,replications = N_sim,generate = my_generator,analyse = Analyse,summarise = Summarise,cl = cl,save=FALSE)#>#> Number of parallel clusters in use: 8#>#> Simulation complete. Total execution time: 0.77s#> Warning in system2("quarto", "-V", stdout = TRUE, env = paste0("TMPDIR=", : Ausführung von Kommando '"quarto" TMPDIR=C:/Users/Tobias11/AppData/Local/Temp/RtmpGKzPDq/file530433727d3d#> -V' ergab Status 1res|>subset(select=c("delay","coxph.HR.bias","coxph.gAHR.bias","coxph.HR.mse","coxph.gAHR.mse","coxph.HR.coverage","coxph.gAHR.coverage" ))|> knitr::kable()| delay | coxph.HR.bias | coxph.gAHR.bias | coxph.HR.mse | coxph.gAHR.mse | coxph.HR.coverage | coxph.gAHR.coverage |
|---|---|---|---|---|---|---|
| 0.000 | 0.0103694 | 0.0103694 | 0.0201519 | 0.0201519 | 0.95 | 0.95 |
| 60.875 | 0.0400061 | -0.2933273 | 0.0192242 | 0.1036646 | 0.95 | 0.51 |
| 121.750 | 0.0805660 | -0.2527674 | 0.0245258 | 0.0819263 | 0.96 | 0.67 |
| 182.625 | 0.1388977 | -0.1944356 | 0.0420955 | 0.0606081 | 0.80 | 0.73 |
| 243.500 | 0.1835182 | -0.1498151 | 0.0578980 | 0.0466636 | 0.78 | 0.82 |
| 304.375 | 0.1764311 | -0.1569022 | 0.0609472 | 0.0544376 | 0.77 | 0.79 |