Using winratiosim

Overview

winratiosim simulates operating characteristics for two-arm clinical trials with a hierarchical win ratio endpoint. A simulated trial includes three prioritized outcome layers:

  1. time to death,
  2. annualized recurrent event count, and
  3. a continuous quality-of-life change score.

The package was created for the simulation workflow used in Lee (2025), which compares Finkelstein-Schoenfeld permutation variance calculations with the large-sample variance formula discussed by Yu and Ganju.

Quick start

The main function is winratiosim(). The example below uses only two simulated trials and a small sample size so that the vignette builds quickly. Increase nsim and N for a real operating-characteristic study.

library(winratiosim)

quick_res <- winratiosim(
  nsim = 2,
  N = 20,
  Randomization.ratio = c(1, 1),
  alpha.JFM = 0,
  theta.JFM = 1,
  lambda_trt = 0.13,
  lambda_ctl = 0.15,
  ann.icr_trt = 0.32,
  ann.icr_ctl = 0.55,
  xbase_trt = 45,
  xfinal_trt = 52.5,
  xbase_ctl = 45,
  xfinal_ctl = 45,
  sd.delta.x_trt = 20,
  sd.delta.x_ctl = 20,
  censorrate_trt = 0.2,
  censorrate_ctl = 0.2,
  nc = 1,
  seed = 20250518
)

quick_res$df_WR.analysis.summary
#>        R_w    logR_w variance_log_R_w_permutation LB_R_w_95p_permutation
#> 1 1.281250 0.2478362                    0.3694346              0.3892732
#> 2 2.052632 0.7191227                    0.5670003              0.4691907
#>   UB_R_w_95p_permutation Var_logR_w   UB_R_w    LB_R_w p_value_R_w
#> 1               4.217094  0.4612333 4.849879 0.3384830   0.3575835
#> 2               8.979924  0.6020799 9.392968 0.4485586   0.1770208
quick_res$df_sample.size.summary
#>    N N_trt N_ctl N_comparison_win_ratio
#> 1 20    11     9                     99
#> 2 20     6    14                     84

The returned object is a named list:

names(quick_res)
#> [1] "df_FS.analysis.summary" "df_WR.analysis.summary" "df_sample.size.summary"
#> [4] "df_Total_probability"   "df_Total_count"

The most commonly used elements are:

  • df_FS.analysis.summary: Finkelstein-Schoenfeld statistic, variance, z-score, and p-value for each simulated trial.
  • df_WR.analysis.summary: win ratio estimates, confidence limits, variance estimates, and p-values for each simulated trial.
  • df_Total_probability: treatment win, tie, and control win probabilities.
  • df_sample.size.summary: treatment and control sample sizes generated under the requested randomization ratio.

Estimating power

For a one-sided superiority analysis at level 0.025, one common summary is the proportion of simulated trials with a significant result. Exact binomial confidence intervals can be calculated with binom.conf.exact().

fs_success <- quick_res$df_FS.analysis.summary$p_value_FS < 0.025
wr_success <- quick_res$df_WR.analysis.summary$LB_R_w > 1

data.frame(
  Method = c("FS test", "YG win ratio test"),
  Estimated_power = c(
    mean(fs_success, na.rm = TRUE),
    mean(wr_success, na.rm = TRUE)
  )
)
#>              Method Estimated_power
#> 1           FS test               0
#> 2 YG win ratio test               0

binom.conf.exact(
  x = sum(wr_success, na.rm = TRUE),
  n = sum(!is.na(wr_success))
)
#>  PointEst     Lower     Upper 
#> 0.0000000 0.0000000 0.8418861

This small example is intended only to show the workflow. Power estimates from two simulations are not scientifically meaningful.

Paper-style simulation workflow

The following code mirrors the larger simulation workflow used for the paper. It is not evaluated when this vignette is built because nsim = 10000 can take substantial time.

library(winratiosim)

power.design_parameters <- list(
  nsim = 10000,
  N = 400,
  Randomization.ratio = c(1, 1),
  alpha.JFM = 0,
  theta.JFM = 1,
  lambda_trt = 0.13,
  lambda_ctl = 0.15,
  ann.icr_trt = 0.32,
  ann.icr_ctl = 0.55,
  xbase_trt = 45,
  xfinal_trt = 45 + 7.5,
  sd.delta.x_trt = 20,
  xbase_ctl = 45,
  xfinal_ctl = 45,
  sd.delta.x_ctl = 20,
  censorrate_trt = 0.2,
  censorrate_ctl = 0.2,
  nc = 10,
  seed = 20250518
)

power.sim_res <- do.call(winratiosim, power.design_parameters)

Power_binom_CI_one_sided_FS_Permutation <- binom.conf.exact(
  x = sum(power.sim_res$df_FS.analysis.summary$p_value_FS < 0.025,
          na.rm = TRUE),
  n = sum(!is.na(power.sim_res$df_FS.analysis.summary$p_value_FS))
)

Power_binom_CI_one_sided_WR_Ron_Yu <- binom.conf.exact(
  x = sum(power.sim_res$df_WR.analysis.summary$LB_R_w > 1,
          na.rm = TRUE),
  n = sum(!is.na(power.sim_res$df_WR.analysis.summary$LB_R_w))
)

t1e.design_parameters <- list(
  nsim = power.design_parameters$nsim,
  N = power.design_parameters$N,
  Randomization.ratio = power.design_parameters$Randomization.ratio,
  alpha.JFM = power.design_parameters$alpha.JFM,
  theta.JFM = power.design_parameters$theta.JFM,
  lambda_trt = power.design_parameters$lambda_ctl,
  lambda_ctl = power.design_parameters$lambda_ctl,
  ann.icr_trt = power.design_parameters$ann.icr_ctl,
  ann.icr_ctl = power.design_parameters$ann.icr_ctl,
  xbase_trt = power.design_parameters$xbase_ctl,
  xfinal_trt = power.design_parameters$xfinal_ctl,
  sd.delta.x_trt = power.design_parameters$sd.delta.x_trt,
  xbase_ctl = power.design_parameters$xbase_ctl,
  xfinal_ctl = power.design_parameters$xfinal_ctl,
  sd.delta.x_ctl = power.design_parameters$sd.delta.x_ctl,
  censorrate_trt = power.design_parameters$censorrate_trt,
  censorrate_ctl = power.design_parameters$censorrate_ctl,
  nc = power.design_parameters$nc,
  seed = 20250518
)

t1e.sim_res <- do.call(winratiosim, t1e.design_parameters)

t1e_binom_CI_one_sided_FS_Permutation <- binom.conf.exact(
  x = sum(t1e.sim_res$df_FS.analysis.summary$p_value_FS < 0.025,
          na.rm = TRUE),
  n = sum(!is.na(t1e.sim_res$df_FS.analysis.summary$p_value_FS))
)

t1e_binom_CI_one_sided_WR_Ron_Yu <- binom.conf.exact(
  x = sum(t1e.sim_res$df_WR.analysis.summary$LB_R_w > 1,
          na.rm = TRUE),
  n = sum(!is.na(t1e.sim_res$df_WR.analysis.summary$LB_R_w))
)

df.power.type1 <- data.frame(
  Method = c("FS test", "YG test"),
  Power = paste(
    round(c(Power_binom_CI_one_sided_FS_Permutation[1],
            Power_binom_CI_one_sided_WR_Ron_Yu[1]), 3),
    "(",
    round(c(Power_binom_CI_one_sided_FS_Permutation[2],
            Power_binom_CI_one_sided_WR_Ron_Yu[2]), 3),
    ", ",
    round(c(Power_binom_CI_one_sided_FS_Permutation[3],
            Power_binom_CI_one_sided_WR_Ron_Yu[3]), 3),
    ")",
    sep = ""
  ),
  Type_I_Error = paste(
    round(c(t1e_binom_CI_one_sided_FS_Permutation[1],
            t1e_binom_CI_one_sided_WR_Ron_Yu[1]), 3),
    "(",
    round(c(t1e_binom_CI_one_sided_FS_Permutation[2],
            t1e_binom_CI_one_sided_WR_Ron_Yu[2]), 3),
    ", ",
    round(c(t1e_binom_CI_one_sided_FS_Permutation[3],
            t1e_binom_CI_one_sided_WR_Ron_Yu[3]), 3),
    ")",
    sep = ""
  )
)

df.variance <- data.frame(
  Median_Variance_under_Power = c(
    median(power.sim_res$df_WR.analysis.summary$variance_log_R_w_permutation,
           na.rm = TRUE),
    median(power.sim_res$df_WR.analysis.summary$Var_logR_w,
           na.rm = TRUE)
  ),
  Median_Variance_under_Type_I_Error = c(
    median(t1e.sim_res$df_WR.analysis.summary$variance_log_R_w_permutation,
           na.rm = TRUE),
    median(t1e.sim_res$df_WR.analysis.summary$Var_logR_w,
           na.rm = TRUE)
  )
)

df.combined <- cbind(df.power.type1, round(df.variance, 4))
df.combined

median(power.sim_res$df_WR.analysis.summary$R_w, na.rm = TRUE)
median(power.sim_res$df_Total_probability[, "Prob_of_tie"], na.rm = TRUE)

Output from the 10,000-simulation run

When the paper-style workflow above is run with nsim = 10000, N = 400, nc = 10, and seed = 20250518, the final summary commands produce the following output. The full simulation is not rerun during vignette building.

df.power.type1
#>    Method               Power        Type_I_Error
#> 1 FS test  0.86(0.853, 0.866) 0.024(0.021, 0.028)
#> 2 YG test 0.811(0.803, 0.819) 0.018(0.015, 0.021)

df.variance
#>   Median_Variance_under_Power Median_Variance_under_Type_I_Error
#> 1                  0.01677787                         0.01607165
#> 2                  0.01969007                         0.01882680

median(power.sim_res$df_WR.analysis.summary$R_w, na.rm = TRUE)
#> [1] 1.474161

median(power.sim_res$df_Total_probability[, "Prob_of_tie"], na.rm = TRUE)
#> [1] 0.191

Parameter notes

  • lambda_trt and lambda_ctl are annual mortality probabilities.
  • ann.icr_trt and ann.icr_ctl are annual recurrent event incidence rates.
  • xbase_* and xfinal_* define the mean continuous outcome change in each arm.
  • censorrate_* gives the annual censoring probability.
  • nc controls the number of worker processes. Use nc = 1 when debugging.
  • seed makes the simulation reproducible.

References

Lee, S. Y. (2025). A note on the sample size formula for a win ratio endpoint. Statistics in Medicine, 44, e70165. https://doi.org/10.1002/sim.70165

Finkelstein, D. M., and Schoenfeld, D. A. (1999). Combining mortality and longitudinal measures in clinical trials. Statistics in Medicine, 18(11), 1341-1354.

Pocock, S. J., Ariti, C. A., Collier, T. J., and Wang, D. (2012). The win ratio: a new approach to the analysis of composite endpoints in clinical trials based on clinical priorities. European Heart Journal, 33(2), 176-182.

Yu, R. X., and Ganju, J. (2022). Sample size formula for a win ratio endpoint. Statistics in Medicine, 41(6), 950-963.