In this vignette we want to
compare the combination design implemented in crmPack and
explained in this vignette with the
implementation in the decider
package (Schroeter 2023). Please note
that the decider package is not available on CRAN,
therefore the vignette is only executing the code chunks if the package
is available on this system.
We are going to use the example as described in the
decider vignette here:
deciderlibrary(decider)
#> This is development version `0.0.0.9012` of the `decider` package:
#> DECIsion making in oncology Dose Escalation trials with logistic Regression.
#>
#> Attaching package: 'decider'
#> The following object is masked from 'package:crmPack':
#>
#> logitThis is the data from the historical Arm C:
historical_data <- list(
dose1 = c(0, 0, 0, 0, 0),
dose2 = c(2, 4, 8, 12, 16),
n.pat = c(3, 3, 3, 9, 12),
n.dlt = c(0, 0, 0, 1, 2),
trial = c("H1", "H1", "H1", "H1", "H1")
)The monotherapy dose grid for Arm A is:
The dose grid for compound 2 in Arm B is more sparse:
The overall dose grid for combination Arm B is therefore:
doses_of_interest <- rbind(
c(d1, rep(d1, times = length(d2))),
c(rep(0, length(d1)), rep(d2, each = length(d1)))
)The reference doses to be used in the models are:
We further need to specify the arms and types of the arms as follows:
The prior for the hypermeans is specified like this:
# Parameter Mean SD
prior_mu <- list(mu_a1 = c(logit(0.33), 2),
mu_b1 = c(0, 1), # standard normal
mu_a2 = c(logit(0.33), 2),
mu_b2 = c(0, 1), # standard normal
mu_eta = c(0, 1.121))The prior mean for \(\mu_{\alpha_{1}}\) is set to \(\text{logit}(0.33)\), which implies that we assume the reference dose has a prior median DLT rate of 33%.
Note that we use a normal prior here on the interaction parameter \(\eta\), thus allowing both positive and negative interactions. The standard deviation is set such that \(\exp(1.96 \cdot 1.121) \approx 9\), thus allowing for a 95% prior interval of \([1 / 9, 9]\) for the odds changes for a DLT at the reference dose. So \(1.121 = \log(9) / z_{0.975}\).
The prior for the between-trial heterogeneity parameters is specified like this:
# Parameter Mean SD
prior_tau <- list(tau_a1 = c(log(0.25), log(2) / 1.96),
tau_b1 = c(log(0.125), log(2) / 1.96),
tau_a2 = c(log(0.25), log(2) / 1.96),
tau_b2 = c(log(0.125), log(2) / 1.96),
tau_eta = c(log(0.125), log(2) / 1.96))These are all the log normal prior parameters for the corresponding \(\tau\) parameters. These are all “moderate” degrees of heterogeneity, according to Neuenschwander et al. (2014).
Then we look at the following scenario, where two cohorts of patients are available from Arm A:
scenario1 <- list(
dose1 = c(0.1, 0.2),
dose2 = c(0, 0),
n.pat = c(3, 3),
n.dlt = c(0, 1),
trial = c("A", "A")
)We note that the trial specification here needs to match
the name used in trials_of_interest above.
Now we can call the scenario function:
result1 <- scenario_jointBLRM(
data = scenario1,
historical.data = historical_data,
doses.of.interest = doses_of_interest,
dose.ref1 = dose_ref1,
dose.ref2 = dose_ref2,
trials.of.interest = trials_of_interest,
types.of.interest = types_of_interest,
prior.mu = prior_mu,
prior.tau = prior_tau
)
#> Warning: There were 3 divergent transitions after warmup. See
#> https://mc-stan.org/misc/warnings.html#divergent-transitions-after-warmup
#> to find out why this is a problem and how to eliminate them.
#> Warning: Examine the pairs() plot to diagnose sampling problemsWe can look at the results:
result1
#> $`trial-A`
#> mean sd q.2.5% q.50% q.97.5% P([0,0.16)) P([0.16,0.33))
#> 0.1+0 0.11427 0.10823 0.00310 0.08081 0.40574 0.74369 0.20349
#> 0.2+0 0.15309 0.12801 0.00677 0.11875 0.48056 0.61987 0.27548
#> 0.4+0 0.20643 0.15465 0.01309 0.17081 0.58031 0.47282 0.32438
#> 0.8+0 0.27510 0.18757 0.02230 0.23924 0.70465 0.33515 0.32277
#> 1.6+0 0.35517 0.22140 0.03411 0.32276 0.82838 0.22935 0.28176
#> 2.4+0 0.40419 0.23880 0.04178 0.37698 0.88412 0.18241 0.25226
#> 3.6+0 0.45268 0.25315 0.05007 0.43356 0.92653 0.14702 0.22024
#> 5+0 0.49060 0.26224 0.05722 0.48117 0.95063 0.12373 0.19725
#> 6+0 0.51088 0.26628 0.06119 0.50711 0.96082 0.11316 0.18547
#> P([0.33,1])
#> 0.1+0 0.05282
#> 0.2+0 0.10465
#> 0.4+0 0.20280
#> 0.8+0 0.34208
#> 1.6+0 0.48889
#> 2.4+0 0.56533
#> 3.6+0 0.63274
#> 5+0 0.67902
#> 6+0 0.70137
#>
#> $`trial-B`
#> mean sd q.2.5% q.50% q.97.5% P([0,0.16)) P([0.16,0.33))
#> 0.1+8 0.18452 0.12541 0.02768 0.15447 0.50703 0.51955 0.35479
#> 0.2+8 0.21921 0.14197 0.03530 0.18652 0.57618 0.41590 0.39086
#> 0.4+8 0.26628 0.16313 0.04615 0.23196 0.66192 0.30537 0.39652
#> 0.8+8 0.32727 0.18876 0.05942 0.29241 0.76093 0.20842 0.36317
#> 1.6+8 0.40012 0.21675 0.07335 0.36935 0.85836 0.13843 0.29560
#> 2.4+8 0.44565 0.23329 0.07935 0.42147 0.90440 0.11393 0.25257
#> 3.6+8 0.49083 0.25003 0.08185 0.47799 0.94152 0.09955 0.21457
#> 5+8 0.52549 0.26446 0.07718 0.52529 0.96409 0.09674 0.18619
#> 6+8 0.54337 0.27322 0.07161 0.55186 0.97344 0.09871 0.17194
#> 0.1+12 0.22242 0.12620 0.05281 0.19612 0.53824 0.36771 0.45578
#> 0.2+12 0.25552 0.14088 0.06192 0.22749 0.60190 0.28358 0.46148
#> 0.4+12 0.30047 0.16014 0.07384 0.27063 0.68115 0.20066 0.43287
#> 0.8+12 0.35880 0.18439 0.08725 0.32810 0.77553 0.13373 0.37007
#> 1.6+12 0.42849 0.21295 0.09746 0.40211 0.86987 0.09377 0.28417
#> 2.4+12 0.47179 0.23188 0.09705 0.45366 0.91562 0.08497 0.23766
#> 3.6+12 0.51397 0.25363 0.08603 0.50972 0.95226 0.08788 0.19702
#> 5+12 0.54495 0.27445 0.06925 0.55550 0.97342 0.10077 0.16941
#> 6+12 0.56002 0.28757 0.05665 0.58213 0.98216 0.11262 0.15446
#> P([0.33,1])
#> 0.1+8 0.12566
#> 0.2+8 0.19324
#> 0.4+8 0.29811
#> 0.8+8 0.42841
#> 1.6+8 0.56597
#> 2.4+8 0.63350
#> 3.6+8 0.68588
#> 5+8 0.71707
#> 6+8 0.72935
#> 0.1+12 0.17651
#> 0.2+12 0.25494
#> 0.4+12 0.36647
#> 0.8+12 0.49620
#> 1.6+12 0.62206
#> 2.4+12 0.67737
#> 3.6+12 0.71510
#> 5+12 0.72982
#> 6+12 0.73292For each trial of interest, the posterior toxicities previously designated to be of interest are shown.
Under the hood, the implementation works as follows:
post_tox_jointBLRM()
is called to sample from the posterior, which in turn usessampling_jointBLRM()
which then calls rstan::sampling() onstanmodels$jointBLRM
which is the constant Stan model sourced fromjointBLRM.stanSo we can compare this with the implementation in
crmPack which is based on JAGS.
crmPackNow we are going to define the same design and scenario in
crmPack.
We start with the monotherapy model for compound 1:
library(crmPack)
mono_model1 <- LogisticLogNormal(
mean = c(logit(0.33), 0),
cov = diag(c(2, 1)^2),
ref_dose = dose_ref1
)And for compound 2 the same:
mono_model2 <- LogisticLogNormal(
mean = c(logit(0.33), 0),
cov = diag(c(2, 1)^2),
ref_dose = dose_ref2
)Then we define the combination model:
combo_model <- TwoDrugsCombo(
list(
compound1 = mono_model1,
compound2 = mono_model2
),
gamma = 0, # prior mean for the interaction parameter
tau = 1 / (1.121^2) # prior precision for the interaction parameter
)We define the historical data which is already available:
hist_data_comp2 <- Data(
x = rep(historical_data$dose2, historical_data$n.pat),
y = c(
rep(0, sum(historical_data$n.pat) - sum(historical_data$n.dlt)),
rep(1, sum(historical_data$n.dlt))
),
doseGrid = historical_data$dose2
)
#> Used default patient IDs!
#> Used best guess cohort indices!We are going to use simple rules here (they are not relevant for the current scenario comparison):
my_stopping <- StoppingMinPatients(nPatients = 50)
my_increments <- IncrementsRelative(0, 2)
myNextBest <- NextBestNCRM(
target = c(0.16, 0.33),
overdose = c(0.33, 1),
max_overdose_prob = 0.25
)
my_cohort_size <- CohortSizeConst(size = 3)
my_increments_combo <- IncrementsComboOneDrugOnly()Then we define the design arms accordingly:
designArmA <- DesignArm(
"A",
design = Design(
data = Data(doseGrid = d1),
startingDose = d1[1],
model = mono_model1,
stopping = my_stopping,
increments = my_increments,
nextBest = myNextBest,
cohort_size = my_cohort_size
)
)
designArmB <- DesignArm(
"B",
design = DesignCombo(
data = DataCombo(doseGrid = list(compound1 = d1, compound2 = c(0, d2))),
startingDose = c(compound1 = d1[1], compound2 = 0),
model = combo_model,
stopping = my_stopping,
increments = my_increments_combo,
nextBest = myNextBest,
cohort_size = my_cohort_size
),
open_when = ArmMinDoseCondition("A", min_dose = d1[2])
)
designArmC <- HistoricalArm(
"C",
data = hist_data_comp2,
model = mono_model2
)Now we can define the hierarchical design:
design_hierarchical <- HierarchicalDesign(
designArmA,
designArmB,
designArmC,
exchangeable_parameters = list(
comp1_intercept = list(
A = "alpha0",
B = "alpha0[1]"
),
comp1_slope = list(
A = "alpha1",
B = "alpha1[1]"
),
comp2_intercept = list(
B = "alpha0[2]",
C = "alpha0"
),
comp2_slope = list(
B = "alpha1[2]",
C = "alpha1"
)
),
pool_correlations = list(
comp1 = c("comp1_intercept", "comp1_slope"),
comp2 = c("comp2_intercept", "comp2_slope")
),
pool_priors = list(
comp1_intercept = list(
mu = prior_mu$mu_a1,
tau = prior_tau$tau_a1
),
comp1_slope = list(
mu = prior_mu$mu_b1,
tau = prior_tau$tau_b1
),
comp2_intercept = list(
mu = prior_mu$mu_a2,
tau = prior_tau$tau_a2
),
comp2_slope = list(
mu = prior_mu$mu_b2,
tau = prior_tau$tau_b2
)
)
)Note that each entry in pool_correlations can correlate
exactly two scalar exchangeable parameter pools. In this example,
comp1 correlates the compound 1 intercept pool with the
compound 1 slope pool, and comp2 does the same for compound
2. Correlated blocks with three or more parameters are not currently
supported.
Then we define the scenario:
scenario_hierarchical <- HierarchicalData(
A = Data(
x = c(0.1, 0.1, 0.1, 0.2, 0.2, 0.2),
y = c(0, 0, 0, 0, 0, 1),
doseGrid = designArmA@design@data@doseGrid
),
B = designArmB@design@data,
C = designArmC@design@data
)
#> Used default patient IDs!
#> Used best guess cohort indices!And then we can use the scenario() function:
result1CrmPack <- scenario(
design_hierarchical,
data = scenario_hierarchical,
mcmcOptions = McmcOptions()
)We can look at the fit results:
result1CrmPack$fit
#> $A
#> dose middle lower upper
#> 1 0.1 0.1248389 0.006411293 0.3827125
#> 2 0.2 0.1687405 0.012779116 0.4461998
#> 3 0.4 0.2284195 0.024781664 0.5581310
#> 4 0.8 0.3039033 0.046006227 0.6925274
#> 5 1.6 0.3903258 0.064611135 0.8136128
#> 6 2.4 0.4425087 0.077012491 0.8748520
#> 7 3.6 0.4935787 0.088867972 0.9196750
#> 8 5.0 0.5330934 0.097799967 0.9466905
#> 9 6.0 0.5540643 0.103811782 0.9571126
#>
#> $B
#> compound1 compound2 middle lower upper
#> 1 0.1 0 0.1275419 0.003379436 0.4370787
#> 2 0.2 0 0.1693379 0.008294018 0.5175187
#> 3 0.4 0 0.2257700 0.018500347 0.6087808
#> 4 0.8 0 0.2981168 0.034754227 0.7207365
#> 5 1.6 0 0.3832560 0.054242906 0.8291803
#> 6 2.4 0 0.4358117 0.066349743 0.8791129
#> 7 3.6 0 0.4878269 0.078218285 0.9221310
#> 8 5.0 0 0.5283386 0.088967471 0.9480713
#> 9 6.0 0 0.5498987 0.095281937 0.9589746
#> 10 0.1 8 0.1686415 0.017436881 0.4762595
#> 11 0.2 8 0.2083532 0.025464761 0.5474206
#> 12 0.4 8 0.2620122 0.037516127 0.6329509
#> 13 0.8 8 0.3309245 0.053348887 0.7385946
#> 14 1.6 8 0.4121667 0.071704207 0.8414754
#> 15 2.4 8 0.4621874 0.081251203 0.8939637
#> 16 3.6 8 0.5110693 0.085619949 0.9353820
#> 17 5.0 8 0.5478999 0.082204383 0.9590182
#> 18 6.0 8 0.5665889 0.077397008 0.9699736
#> 19 0.1 12 0.2167558 0.047652989 0.5181625
#> 20 0.2 12 0.2541890 0.059891466 0.5828141
#> 21 0.4 12 0.3047930 0.075372393 0.6614039
#> 22 0.8 12 0.3698527 0.092781297 0.7612890
#> 23 1.6 12 0.4465125 0.106850538 0.8577739
#> 24 2.4 12 0.4933121 0.108079621 0.9061254
#> 25 3.6 12 0.5379531 0.097501738 0.9471440
#> 26 5.0 12 0.5698492 0.078639696 0.9709385
#> 27 6.0 12 0.5849331 0.065095829 0.9802927
#>
#> $C
#> dose middle lower upper
#> 1 2 0.008873019 2.124116e-10 0.07009762
#> 2 4 0.017169062 2.676432e-07 0.09862057
#> 3 8 0.042953346 2.682744e-04 0.15359087
#> 4 12 0.094606066 1.243462e-02 0.23112801
#> 5 16 0.201511538 5.504018e-02 0.42723343We can also check the probabilities to be in target and overdosing intervals:
result1CrmPack$next_best$A$probs
#> dose target overdose
#> [1,] 0.1 0.2583 0.0477
#> [2,] 0.2 0.3542 0.1043
#> [3,] 0.4 0.3812 0.2442
#> [4,] 0.8 0.3437 0.4066
#> [5,] 1.6 0.2829 0.5665
#> [6,] 2.4 0.2338 0.6523
#> [7,] 3.6 0.1921 0.7206
#> [8,] 5.0 0.1654 0.7606
#> [9,] 6.0 0.1487 0.7827
result1CrmPack$next_best$B$probs
#> compound1 compound2 target_prob overdose_prob not_eligible
#> 1 0.1 0 0.2279 0.0719 FALSE
#> 2 0.2 0 0.3020 0.1316 FALSE
#> 3 0.4 0 0.3381 0.2407 FALSE
#> 4 0.8 0 0.3274 0.3889 TRUE
#> 5 1.6 0 0.2763 0.5483 TRUE
#> 6 2.4 0 0.2379 0.6312 TRUE
#> 7 3.6 0 0.1966 0.7036 TRUE
#> 8 5.0 0 0.1682 0.7509 TRUE
#> 9 6.0 0 0.1550 0.7721 TRUE
#> 10 0.1 8 0.3249 0.1118 FALSE
#> 11 0.2 8 0.3742 0.1821 FALSE
#> 12 0.4 8 0.3746 0.3061 TRUE
#> 13 0.8 8 0.3322 0.4584 TRUE
#> 14 1.6 8 0.2616 0.6031 TRUE
#> 15 2.4 8 0.2191 0.6766 TRUE
#> 16 3.6 8 0.1828 0.7297 TRUE
#> 17 5.0 8 0.1624 0.7550 TRUE
#> 18 6.0 8 0.1502 0.7652 TRUE
#> 19 0.1 12 0.4462 0.1676 FALSE
#> 20 0.2 12 0.4548 0.2569 TRUE
#> 21 0.4 12 0.4150 0.3917 TRUE
#> 22 0.8 12 0.3431 0.5350 TRUE
#> 23 1.6 12 0.2541 0.6674 TRUE
#> 24 2.4 12 0.2090 0.7216 TRUE
#> 25 3.6 12 0.1719 0.7554 TRUE
#> 26 5.0 12 0.1482 0.7684 TRUE
#> 27 6.0 12 0.1355 0.7700 TRUEBased on this we can first compare the fit results.
Let’s look at the results for Arm A:
fitTrialADecider <- result1$`trial-A` |> as.data.frame()
fitTrialACrmPack <- result1CrmPack$fit$A
probsTrialACrmPack <- result1CrmPack$next_best$A$probs |> as.data.frame()
diffTrialA <- data.frame(
dose = fitTrialACrmPack$dose,
center = fitTrialADecider$mean - fitTrialACrmPack$middle,
lower = fitTrialADecider$`q.2.5%` - fitTrialACrmPack$lower,
upper = fitTrialADecider$`q.97.5%` - fitTrialACrmPack$upper,
target = fitTrialADecider$`P([0.16,0.33))` - probsTrialACrmPack$target,
overdose = fitTrialADecider$`P([0.33,1])` - probsTrialACrmPack$overdose
)
diffTrialA
#> dose center lower upper target overdose
#> 1 0.1 -0.01056890 -0.003311293 0.023027509 -0.05481 0.00512
#> 2 0.2 -0.01565053 -0.006009116 0.034360230 -0.07872 0.00035
#> 3 0.4 -0.02198952 -0.011691664 0.022178963 -0.05682 -0.04140
#> 4 0.8 -0.02880332 -0.023706227 0.012122634 -0.02093 -0.06452
#> 5 1.6 -0.03515576 -0.030501135 0.014767208 -0.00114 -0.07761
#> 6 2.4 -0.03831873 -0.035232491 0.009268001 0.01846 -0.08697
#> 7 3.6 -0.04089868 -0.038797972 0.006855026 0.02814 -0.08786
#> 8 5.0 -0.04249343 -0.040579967 0.003939469 0.03185 -0.08158
#> 9 6.0 -0.04318429 -0.042621782 0.003707378 0.03677 -0.08133And then the results for Arm B:
fitTrialBDecider <- result1$`trial-B` |> as.data.frame()
fitTrialBCrmPack <- result1CrmPack$fit$B |> dplyr::filter(compound2 > 0)
probsTrialBCrmPack <- result1CrmPack$next_best$B$probs |>
as.data.frame() |>
dplyr::filter(compound2 > 0)
diffTrialB <- data.frame(
dose1 = fitTrialBCrmPack$compound1,
dose2 = fitTrialBCrmPack$compound2,
center = fitTrialBDecider$mean - fitTrialBCrmPack$middle,
lower = fitTrialBDecider$`q.2.5%` - fitTrialBCrmPack$lower,
upper = fitTrialBDecider$`q.97.5%` - fitTrialBCrmPack$upper,
target = fitTrialBDecider$`P([0.16,0.33))` - probsTrialBCrmPack$target,
overdose = fitTrialBDecider$`P([0.33,1])` - probsTrialBCrmPack$overdose
)
diffTrialB
#> dose1 dose2 center lower upper target overdose
#> 1 0.1 8 0.015878535 0.010243119 0.030770454 0.02989 0.01386
#> 2 0.2 8 0.010856817 0.009835239 0.028759380 0.01666 0.01114
#> 3 0.4 8 0.004267761 0.008633873 0.028969071 0.02192 -0.00799
#> 4 0.8 8 -0.003654505 0.006071113 0.022335376 0.03097 -0.02999
#> 5 1.6 8 -0.012046738 0.001645793 0.016884583 0.03400 -0.03713
#> 6 2.4 8 -0.016537405 -0.001901203 0.010436296 0.03347 -0.04310
#> 7 3.6 8 -0.020239332 -0.003769949 0.006138037 0.03177 -0.04382
#> 8 5.0 8 -0.022409927 -0.005024383 0.005071781 0.02379 -0.03793
#> 9 6.0 8 -0.023218904 -0.005787008 0.003466382 0.02174 -0.03585
#> 10 0.1 12 0.005664168 0.005157011 0.020077494 0.00958 0.00891
#> 11 0.2 12 0.001331022 0.002028534 0.019085875 0.00668 -0.00196
#> 12 0.4 12 -0.004322972 -0.001532393 0.019746144 0.01787 -0.02523
#> 13 0.8 12 -0.011052692 -0.005531297 0.014241027 0.02697 -0.03880
#> 14 1.6 12 -0.018022472 -0.009390538 0.012096062 0.03007 -0.04534
#> 15 2.4 12 -0.021522131 -0.011029621 0.009494614 0.02866 -0.04423
#> 16 3.6 12 -0.023983112 -0.011471738 0.005116009 0.02512 -0.04030
#> 17 5.0 12 -0.024899194 -0.009389696 0.002481493 0.02121 -0.03858
#> 18 6.0 12 -0.024913078 -0.008445829 0.001867322 0.01896 -0.03708So these differences look relatively small, and there does not seem to be any systematic bias in the differences.
Let’s compare the model code used in decider and
crmPack, in order to make sure that they really match and
implement the same priors and models:
deciderHere we have the following Stan model:
/*Stan model for joint BLRM
--------------------------------------------------------------------------------
Implements the joint BLRM as described in Neuenschwander et al., 2016,
"On the use of co-data in clinical trials".
A non-centered parametrization is implemented by obtaining
multivariate normals via multiplication with cholesky factors.
The cholesky decomposition is implemented by hand, as it is
available analytically in the required 2x2-case.
*/
functions{
/*counts mono observations based on input dose levels
Note: first input vector signals the component to be counted*/
int count_n_mono(vector dose_1, vector dose_2, int n_obs){
int res = 0;
for(i in 1:n_obs){
if(dose_1[i]>0 && dose_2[i]==0){
res+=1;
}
}
return res;
}
/*Computes permutation of input data, so that the first n_obs1 observations
are mono1, the subsequent n_obs2 observations are mono2, and the remaining
ones are combination therapy.
Returns matrix with two rows, first row is the permutation for sorting, and
second row contains the inverse permutation (to reverse sorted input to
normal order).*/
int[,] sort_idx(vector dose_1, vector dose_2,
int n_obs, int n_obs1, int n_obs2)
{
int res[2, n_obs] = rep_array(0, 2, n_obs);
//n_obs1/n_obs2 allow to compute offsets for sorting by counting
int cnt1 = 0;
int cnt2 = 0;
int cnt = 0;
//loop over input and save correct placement
for(i in 1:n_obs){
if(dose_1[i]>0 && dose_2[i]==0){
res[1, cnt1+1] = i;
res[2, i] = cnt1+1;
cnt1 += 1;
}else if(dose_1[i]==0 && dose_2[i]>0){
res[1, n_obs1 + 1 + cnt2] = i;
res[2, i] = n_obs1 + 1 + cnt2;
cnt2 += 1;
}else if(dose_1[i]>0 && dose_2[i]>0){
res[1, n_obs1 + n_obs2 + 1 + cnt] = i;
res[2, i] = n_obs1 + n_obs2 + 1 + cnt;
cnt += 1;
}
}
return res;
}
}
data{
//number of observations/cohorts
int<lower=0> n_obs;
//number of studies
int<lower=0> n_studies;
//number of patients for each cohort
int<lower=0> n[n_obs];
//number of DLTs for each cohort
int<lower=0> r[n_obs];
//study number for cohorts
int<lower=1> s[n_obs];
//indicates whether a MAP prior is computed
int<lower=0, upper=1> doMAP;
//indicates whether linear or saturating
//interaction term is used
int<lower=0, upper=1> saturating;
//reference doses
vector<lower=0>[2] dose_c;
//dose levels component 1 and 2 for each cohort
vector<lower=0>[n_obs] dose_1;
vector<lower=0>[n_obs] dose_2;
/*hyper priors
Notation and order of entries:
mu = (mu_alpha1, mu_beta1, mu_alpha2, mu_beta2, mu_eta)
tau = (tau_alpha1, tau_beta1, tau_alpha2, tau_beta2, tau_eta)
*/
//mean of hyper SD tau
vector[5] mean_tau;
//sd's of hyper SD tau
vector<lower=0>[5] sd_tau;
//mean of hyper mean mu
vector[5] mean_mu;
//mean of hyper sd mu
vector<lower=0>[5] sd_mu;
}
transformed data{
//internally generates a study without observations for MAP prior
int<lower=1> num_s = doMAP? n_studies+1 : n_studies;
//count number of mono observations
int<lower=0, upper=n_obs> n_obs1 = count_n_mono(dose_1, dose_2, n_obs);
int<lower=0, upper=n_obs> n_obs2 = count_n_mono(dose_2, dose_1, n_obs);
//compute sort indices (only done once per call to stan for efficiency)
int srt_idx[2, n_obs] = sort_idx(dose_1, dose_2, n_obs, n_obs1, n_obs2);
//sort by applying computed sorting permutation
int n_srt[n_obs] = n[srt_idx[1, 1:n_obs]];
int r_srt[n_obs] = r[srt_idx[1, 1:n_obs]];
int s_srt[n_obs] = s[srt_idx[1, 1:n_obs]];
//doses are also rescaled by reference dose after sorting
vector[n_obs] dose_1_srt = dose_1[srt_idx[1, 1:n_obs]]/dose_c[1];
vector[n_obs] dose_2_srt = dose_2[srt_idx[1, 1:n_obs]]/dose_c[2];
vector[n_obs] ldose_1_srt = log(dose_1_srt);
vector[n_obs] ldose_2_srt = log(dose_2_srt);
}
parameters{
//hyper SDs
real<lower=0> tau_1a;
real<lower=0> tau_1b;
real<lower=0> tau_2a;
real<lower=0> tau_2b;
real<lower=0> tau_eta;
//correlation coefficients
real<lower=-1, upper=1> rho12;
real<lower=-1, upper=1> rho34;
/*For non-centered parametrization:
Sample only raw standard normal variables. These are later transformed to
bivariate normals by multiplying with cholesky factor*/
//matrix for log(alpha_ij), log(beta_ij) and eta_j (for comp i, study j)
matrix[num_s, 5] log_ab_raw;
//for hyper means
real mu_raw[5];
}
transformed parameters{
real mu_1a;
real mu_1b;
real mu_2a;
real mu_2b;
real mu_eta;
matrix[num_s,5] log_ab;
vector<lower=0, upper=1>[n_obs] p_srt;
vector<lower=0, upper=1>[n_obs-n_obs1-n_obs2] p_2;
vector<lower=0, upper=1>[n_obs-n_obs1-n_obs2] p_1;
vector<lower=0, upper=1>[n_obs-n_obs1-n_obs2] p_0;
//transform raw hyper means to correct distribution
mu_1a = mean_mu[1] + sd_mu[1]*mu_raw[1];
mu_1b = mean_mu[2] + sd_mu[2]*mu_raw[2];
mu_2a = mean_mu[3] + sd_mu[3]*mu_raw[3];
mu_2b = mean_mu[4] + sd_mu[4]*mu_raw[4];
mu_eta = mean_mu[5] + sd_mu[5]*mu_raw[5];
/*Hard-coded matrix multiplication with lower cholesky factor
of covariance matrix. This can be done without saving the
cholesky factor itself, as it is available analytically.
The following means:
log_ab = mu + L*log_ab_raw,
where L is a lower triangular matrix with L*L^T=Sigma,
for a covariance matrix Sigma.
Note: For general
Sigma = tau_1^2 rho*tau_1*tau_2
rho*tau_1*tau_2 tau_2^2
the lower cholesky factor is
L = tau_1 0
tau_2*rho tau_2*squareroot(1-rho^2)
*/
log_ab[1:num_s,1] = mu_1a + tau_1a*log_ab_raw[1:num_s, 1];
log_ab[1:num_s,2] = mu_1b + tau_1b*rho12*log_ab_raw[1:num_s, 1] +
tau_1b*sqrt(1-square(rho12))*log_ab_raw[1:num_s, 2];
log_ab[1:num_s,3] = mu_2a + tau_2a*log_ab_raw[1:num_s, 3];
log_ab[1:num_s,4] = mu_2b + tau_2b*rho34*log_ab_raw[1:num_s, 3] +
tau_2b*sqrt(1-square(rho34))*log_ab_raw[1:num_s, 4];
log_ab[1:num_s,5] = mu_eta + tau_eta*log_ab_raw[1:num_s, 5];
//toxicity models for mono and combination treatment are vectorized
if(n_obs1>0){
//treatments mono 1
p_srt[1:n_obs1] = inv_logit(log_ab[s_srt[1:n_obs1],1] +
(exp(log_ab[s_srt[1:n_obs1],2]).*
ldose_1_srt[1:n_obs1]));
}
if(n_obs2>0){
//treatments mono 2
p_srt[(n_obs1+1):(n_obs1+n_obs2)] =
inv_logit(log_ab[s_srt[(n_obs1+1):(n_obs1 + n_obs2)],3] +
(exp(log_ab[s_srt[(n_obs1+1): (n_obs1 + n_obs2)],4]).*
ldose_2_srt[(n_obs1+1): (n_obs1 + n_obs2)]));
}
if(n_obs-n_obs1-n_obs2>0){
//treatments combination
p_2[1 : (n_obs-n_obs1-n_obs2)] =
inv_logit(log_ab[s_srt[(n_obs1 + n_obs2 + 1) : n_obs],3] +
(exp(log_ab[s_srt[(n_obs1 + n_obs2 + 1) : n_obs],4]).*
ldose_2_srt[(n_obs1 + n_obs2 + 1) : n_obs]));
p_1[1 : (n_obs-n_obs1-n_obs2)] =
inv_logit(log_ab[s_srt[(n_obs1 + n_obs2 + 1) : n_obs],1] +
(exp(log_ab[s_srt[(n_obs1 + n_obs2 + 1) : n_obs],2]).*
ldose_1_srt[(n_obs1 + n_obs2 + 1) : n_obs]));
p_0[1 :(n_obs-n_obs1-n_obs2)] = p_1[1 : (n_obs-n_obs1-n_obs2)] +
p_2[1 : (n_obs-n_obs1-n_obs2)] -
(p_1[1 : (n_obs-n_obs1-n_obs2)] .*
p_2[1 : (n_obs-n_obs1-n_obs2)]);
if(saturating){
p_srt[(n_obs1 + n_obs2 + 1) : n_obs] =
inv_logit(logit(p_0[1 : (n_obs-n_obs1-n_obs2)]) +
(2*log_ab[s_srt[(n_obs1 + n_obs2 + 1) : n_obs],5].*
(dose_1_srt[(n_obs1 + n_obs2 + 1) : n_obs].*
dose_2_srt[(n_obs1 + n_obs2 + 1) : n_obs] )./
(1 + dose_1_srt[(n_obs1 + n_obs2 + 1) : n_obs].*
dose_2_srt[(n_obs1 + n_obs2 + 1) : n_obs])
));
}else{
p_srt[(n_obs1 + n_obs2 + 1) : n_obs] =
inv_logit(logit(p_0[1 : (n_obs-n_obs1-n_obs2)]) +
log_ab[s_srt[(n_obs1 + n_obs2 + 1) : n_obs],5].*
dose_1_srt[(n_obs1 + n_obs2 + 1) : n_obs].*
dose_2_srt[(n_obs1 + n_obs2 + 1) : n_obs] );
}
}
}
model{
//priors for hyper means (non-centered)
mu_raw ~ std_normal();
//priors for hyper SD
tau_1a ~ lognormal(mean_tau[1], sd_tau[1]);
tau_1b ~ lognormal(mean_tau[2], sd_tau[2]);
tau_2a ~ lognormal(mean_tau[3], sd_tau[3]);
tau_2b ~ lognormal(mean_tau[4], sd_tau[4]);
tau_eta ~ lognormal(mean_tau[5], sd_tau[5]);
//priors for correlation coefficients
rho12 ~ uniform(-1,1);
rho34 ~ uniform(-1,1);
//priors for regression parameters (non-centered)
for(k in 1:num_s){
log_ab_raw[k, 1:5] ~ std_normal();
}
//binomial likelihood
r_srt ~ binomial(n_srt, p_srt);
}
generated quantities{
//just to provide the sorted toxicity parameters as output
vector<lower=0, upper=1>[n_obs] p = p_srt[srt_idx[2,1:n_obs]];
}
crmPackHere we have the following JAGS model:
{
for (i in 1:nObs_A) {
logit(p_A[i]) <- alpha0_A + alpha1_A * log(x_A[i]/ref_dose_A)
y_A[i] ~ dbern(p_A[i])
}
for (i in 1:nObs_B) {
x_drug1_B[i] <- x_B[i, 1L]
}
for (i in 1:nObs_B) {
logit(p_drug1_B[i]) <- alpha0_drug1_B + alpha1_drug1_B *
log(x_drug1_B[i]/ref_dose_drug1_B)
p_single_B[i, 1L] <- p_drug1_B[i]
}
for (i in 1:nObs_B) {
x_drug2_B[i] <- x_B[i, 2L]
}
for (i in 1:nObs_B) {
logit(p_drug2_B[i]) <- alpha0_drug2_B + alpha1_drug2_B *
log(x_drug2_B[i]/ref_dose_drug2_B)
p_single_B[i, 2L] <- p_drug2_B[i]
}
for (i in 1:nObs_B) {
combo_interaction_B[i] <- x_drug1_B[i]/ref_dose_drug1_B *
(x_drug2_B[i]/ref_dose_drug2_B)
}
for (i in 1:nObs_B) {
p0_B[i] <- p_single_B[i, 1] + p_single_B[i, 2] - p_single_B[i,
1] * p_single_B[i, 2]
logit(p_B[i]) <- log(p0_B[i]/(1 - p0_B[i])) + eta_B *
combo_interaction_B[i]
y_B[i] ~ dbern(p_B[i])
}
for (i in 1:nObs_C) {
logit(p_C[i]) <- alpha0_C + alpha1_C * log(x_C[i]/ref_dose_C)
y_C[i] ~ dbern(p_C[i])
}
}
{
alpha0_A <- theta_A[1]
alpha1_A <- exp(theta_A[2])
alpha0_drug1_B <- theta_drug1_B[1]
alpha1_drug1_B <- exp(theta_drug1_B[2])
alpha0_drug2_B <- theta_drug2_B[1]
alpha1_drug2_B <- exp(theta_drug2_B[2])
alpha0_B[1L] <- alpha0_drug1_B
alpha0_B[2L] <- alpha0_drug2_B
alpha1_B[1L] <- alpha1_drug1_B
alpha1_B[2L] <- alpha1_drug2_B
eta_B ~ dnorm(eta_gamma_B, eta_tau_B)
alpha0_C <- theta_C[1]
alpha1_C <- exp(theta_C[2])
theta_A[1:2] ~ dmnorm(mu_comp1_corr[], prec_comp1_corr[,
])
theta_drug1_B[1:2] ~ dmnorm(mu_comp1_corr[], prec_comp1_corr[,
])
mu_comp1_corr[1] <- mu_comp1_intercept
mu_comp1_corr[2] <- mu_comp1_slope
rho_comp1 ~ dunif(rho_comp1_lower, rho_comp1_upper)
prec_comp1_corr[1, 1] <- 1/(pow(tau_comp1_intercept, 2) *
(1 - pow(rho_comp1, 2)))
prec_comp1_corr[2, 2] <- 1/(pow(tau_comp1_slope, 2) * (1 -
pow(rho_comp1, 2)))
prec_comp1_corr[1, 2] <- -rho_comp1/(tau_comp1_intercept *
tau_comp1_slope * (1 - pow(rho_comp1, 2)))
prec_comp1_corr[2, 1] <- prec_comp1_corr[1, 2]
mu_comp1_intercept ~ dnorm(mu_comp1_intercept_mean, pow(mu_comp1_intercept_sd,
-2))
tau_comp1_intercept ~ dlnorm(tau_comp1_intercept_meanlog,
pow(tau_comp1_intercept_sdlog, -2))
mu_comp1_slope ~ dnorm(mu_comp1_slope_mean, pow(mu_comp1_slope_sd,
-2))
tau_comp1_slope ~ dlnorm(tau_comp1_slope_meanlog, pow(tau_comp1_slope_sdlog,
-2))
theta_drug2_B[1:2] ~ dmnorm(mu_comp2_corr[], prec_comp2_corr[,
])
theta_C[1:2] ~ dmnorm(mu_comp2_corr[], prec_comp2_corr[,
])
mu_comp2_corr[1] <- mu_comp2_intercept
mu_comp2_corr[2] <- mu_comp2_slope
rho_comp2 ~ dunif(rho_comp2_lower, rho_comp2_upper)
prec_comp2_corr[1, 1] <- 1/(pow(tau_comp2_intercept, 2) *
(1 - pow(rho_comp2, 2)))
prec_comp2_corr[2, 2] <- 1/(pow(tau_comp2_slope, 2) * (1 -
pow(rho_comp2, 2)))
prec_comp2_corr[1, 2] <- -rho_comp2/(tau_comp2_intercept *
tau_comp2_slope * (1 - pow(rho_comp2, 2)))
prec_comp2_corr[2, 1] <- prec_comp2_corr[1, 2]
mu_comp2_intercept ~ dnorm(mu_comp2_intercept_mean, pow(mu_comp2_intercept_sd,
-2))
tau_comp2_intercept ~ dlnorm(tau_comp2_intercept_meanlog,
pow(tau_comp2_intercept_sdlog, -2))
mu_comp2_slope ~ dnorm(mu_comp2_slope_mean, pow(mu_comp2_slope_sd,
-2))
tau_comp2_slope ~ dlnorm(tau_comp2_slope_meanlog, pow(tau_comp2_slope_sdlog,
-2))
}
There are still some minor differences:
decider, the \(\eta\) parameter is part of the 5-parameter
hierarchical vector and has its own hypermean and heterogeneity. In
crmPack, the \(\eta\)
parameter is not part of the hierarchical vector. When there is only
combo arm then this does not matter.Apart from these, the probabilistic models are equivalent. We could also see that in the fit results which are very close to each other.