Implementing the EffTox Dose-Finding Design in the Matchpoint Trials

This tutorial complements the manuscript Implementing the EffTox Dose-Finding Design in the Matchpoint Trial (Brock et al.,in submission). Please consult the paper for the clinical background, the methodology details, and full explanation of the terminology.

Dose Ambivalence

In this notebook, we illustrate the phenomenon of dose ambivalence using the EffTox design in the seamless phase I/II dose-finding clinical trial, Matchpoint.


In [49]:
import numpy as np
from scipy.stats import norm

from clintrials.dosefinding.efftox import EffTox, LpNormCurve

In [2]:
real_doses = [7.5, 15, 30, 45]
trial_size = 30
cohort_size = 3
first_dose = 3
prior_tox_probs = (0.025, 0.05, 0.1, 0.25)
prior_eff_probs = (0.2, 0.3, 0.5, 0.6)
tox_cutoff = 0.40
eff_cutoff = 0.45
tox_certainty = 0.05
eff_certainty = 0.03

In [3]:
mu_t_mean, mu_t_sd = -5.4317, 2.7643
beta_t_mean, beta_t_sd = 3.1761, 2.7703
mu_e_mean, mu_e_sd = -0.8442, 1.9786
beta_e_1_mean, beta_e_1_sd = 1.9857, 1.9820
beta_e_2_mean, beta_e_2_sd = 0, 0.2
psi_mean, psi_sd = 0, 1
efftox_priors = [
    norm(loc=mu_t_mean, scale=mu_t_sd),
    norm(loc=beta_t_mean, scale=beta_t_sd),
    norm(loc=mu_e_mean, scale=mu_e_sd),
    norm(loc=beta_e_1_mean, scale=beta_e_1_sd),
    norm(loc=beta_e_2_mean, scale=beta_e_2_sd),
    norm(loc=psi_mean, scale=psi_sd),
    ]

The above parameters are explained in the manuscript.


In [4]:
hinge_points = [(0.4, 0), (1, 0.7), (0.5, 0.4)]
metric = LpNormCurve(hinge_points[0][0], hinge_points[1][1], hinge_points[2][0], hinge_points[2][1])

In [5]:
et = EffTox(real_doses, efftox_priors, tox_cutoff, eff_cutoff, tox_certainty, eff_certainty, metric, trial_size,
            first_dose)

The EffTox class is an object-oriented implementation of the trial design by Thall & Cook (Thall, P. F., & Cook, J. D. (2004). Dose-Finding Based on Efficacy-Toxicity Trade-Offs. Biometrics, 60(3), 684–693.)

Dose ambivalence after 3NTE

Outcomes for a patient are represented by a three item tuple, where:

  • first item is 1-based dose-index give (i.e. 3 is dose-level 3);
  • second item is 1 if toxicity happened, else 0;
  • third item is 1 if efficacy happened, else 0.

Outcomes for several patients are represented as lists:


In [42]:
outcomes = [(3, 0, 0), (3, 1, 0), (3, 0, 1)]

In [43]:
et.reset()
np.random.seed(123)
et.update(outcomes)


Out[43]:
3

So, using seed 123, dose-level 3 is recommended to be given to the next patient after oberving 3NTE in the first cohort of patients. Fair enough.


In [44]:
et.reset()
np.random.seed(321)
et.update(outcomes)


Out[44]:
4

Wait...using seed 321, that advice is now dose-level 4. I need a single answer. What should I do?

Let's define a simple function to calculate next dose based on some outcomes:


In [45]:
def get_next_dose(trial, outcomes, **kwargs):
    trial.reset()
    next_dose = trial.update(outcomes, **kwargs)
    return next_dose

And then run that a number of times. For indication, 100 iterations will suffice (it takes a wee while...). In practice, you might use more iterations.


In [46]:
np.random.seed(123)
replicates = [get_next_dose(et, outcomes, n=10**5) for i in range(100)]

In [47]:
doses, freq = np.unique(replicates, return_counts=True)
list(zip(doses, 1.0 * freq / len(replicates)))


Out[47]:
[(3, 0.56000000000000005), (4, 0.44)]

So, dose 3 gets recommended in 56% of iterations; slightly more frequently dose 4. This is useful information. The lack of a strong consensus here would suggest that clinical opinion should be used to select the next dose from doses 3 and 4. Had the split been 90:10, we might have been more inclined to go with the majority decision.


In [ ]: