Description

The Two Arm Expected Events calculator computes an estimate of the expected events for a planned two-arm study. The program assumes uniform accrual and exponential survival. It allows the user to specify a follow-up period after the close of study accrual. The calculations are provided both under the assumption of the null hypothesis (equal hazards in the two arms) and under the alternative hypothesis (hazard rate in the experimental arm is different from the standard arm).The program default presents a table of expected event totals at timepoints spanning the duration of the study. Alternatively, user can calculate the expected proportion of events at a given time, or the time at which a given proportion of events have occurred. The program will allow the user to specify a percentage of patients that have no risk (‘Percent cured’).

Input Items

The user is prompted for values to the following items. For items that have initial default values set, the values are given in parentheses.

Output Items

For calculations of expected events at given analysis time(s):

For calculations of analysis time for a given number of expected events:

Calculations

Hazard rate: The hazard rate \(\lambda\) for a survival probability S(t) at time t is computed as follows:

\[ \lambda = \frac{-ln(S(t))}{t} \]

Total probability of event: In either study arm, the probability \(P_{tot}\) of an event for a study with hazard rate \(\lambda\), accrual time \(t_{acc}\), and follow-up time \(t_{fu}\), is as follows:

\[ P_{tot} = 1 - \frac{e^{-\lambda t_{fu}}(1-e^{-\lambda t_{acc}})}{\lambda t_{acc}} \]

Fraction of the total events at time t: At times before the completion of accrual, the probability of an event at analysis time t is

\[ P(Event|t) =\frac{ P_{d}|t}{P_{tot}} \] At times before the completion of accrual, \(P_{d}|t\) is as follows:

\[ P_{d}|t = \frac{(\frac{1}{\lambda}e^{-\lambda t})+t-\frac{1}{\lambda}}{t_{acc}} \]

At all other times, \(P_{d}|t\) is as follows:

\[ P_{d}|t = 1 - \frac{e^{-\lambda (t-t_{acc})}(1-e^{-\lambda t_{acc}})}{\lambda t_{acc}} \]

Expected events under the null hypothesis: Calculations under the null hypothesis assume the same hazard rate for both the standard and experimental study arms, and perform all calculations for both arms using the hazard rate from the standard arm.

Expected events under the alternative hypothesis: Calculations under the alternative hypothesis assume different hazard rates for the standard and experimental study arms. Calculations for each arm are performed using the hazard rate input for that arm.

Total expected events: Total expected events are calculated as the weighted sum of expected events from each study arm as follows, where \(P_{std}\) represents the proportion of patients enrolled in the standard arm:

\[ EE_{tot} = EE_{std}P_{std} + EE_{exp}(1-P_{std}) \]

Statistical Code

The program is written in R.

View Analysis Time for a Given Proportion of Total Events Code


function(input_type, S_mS, S_mE, S_tS, S_tE, tS, tE, haz_S, haz_E, accrual_time, fu_time, N, pctS, pct_events, pctCure_S, pctCure_E) {
  # George and Desu (1974) #

  if (input_type == "Survival Probability") {
    haz_S = -1 * log(S_tS) / tS
    haz_E = -1 * log(S_tE) / tE
  } else if (input_type == "Median Survival") {
    haz_S = -(log(0.5)) / S_mS
    haz_E = -(log(0.5)) / S_mE
  }

  perdeath = function(hazard, accrual_time, fu_time, time) {
    ptot = 1 - exp(-1 * hazard * fu_time) * (1 - exp(-1 * hazard * accrual_time)) / (hazard * accrual_time)
    if (time < accrual_time) {
      pd = ((1 / hazard) * exp(-1 * hazard * time) + time - 1 / hazard) / accrual_time
    }
    else {
      pd = 1 - exp(-1 * hazard * (time - accrual_time)) * (1 - exp(-1 * hazard * accrual_time)) / (hazard * accrual_time)
    }
    return(pd / ptot)
  }

  aperdeath = function(hazard, accrual, follow, p) {
    v = .5 
    dv = .5 
    x = 0
    while (dv > 1e-6) {
      x = 1 / v - 1
      dv = dv / 2
      if (perdeath(hazard, accrual, follow, x) > p) {
        v = v + dv
      } else {
        v = v - dv
      }
    }
    return(x)
  }

  eea_S = (1 - pctCure_S) * N * pctS * (1 - exp(-1 * haz_S * fu_time) * (1 - exp(-1 * haz_S * accrual_time)) / (haz_S * accrual_time))
  eea_E = (1 - pctCure_E) * N * (1 - pctS) * (1 - exp(-1 * haz_E * fu_time) * (1 - exp(-1 * haz_E * accrual_time)) / (haz_E * accrual_time))
  analysis_time_S = aperdeath(haz_S, accrual_time, fu_time, pct_events)
  analysis_time_E = aperdeath(haz_E, accrual_time, fu_time, pct_events)
  #eet_S = eea_S * perdeath(haz_S, accrual_time, fu_time, analysis_time)
  #eet_E = eea_E * perdeath(haz_E, accrual_time, fu_time, analysis_time)

  result = list(haz_S = round(haz_S, 3),
                haz_E = round(haz_E, 3),
                analysis_time_S = round(analysis_time_S, 1),
                analysis_time_E = round(analysis_time_E, 1))
  return(jsonlite::toJSON(result, pretty = TRUE))
}


View Expected Events for Given Analysis Time Code


function(input_type, S_mS, S_mE, S_tS, S_tE, tS, tE, haz_S, haz_E, accrual_time, fu_time, analysis_time, N, pctS, pctCure_S, pctCure_E) {
  # George and Desu (1974) #
  
  if (input_type == "Survival Probability") {
    haz_S = -1 * log(S_tS) / tS
    haz_E = -1 * log(S_tE) / tE
  } else if (input_type == "Median Survival") {
    haz_S = -(log(0.5)) / S_mS
    haz_E = -(log(0.5)) / S_mE
  }

  perdeath = function(hazard, accrual_time, fu_tume, time) {
    ptot = 1 - exp(-1 * hazard * fu_tume) * (1 - exp(-1 * hazard * accrual_time)) / (hazard * accrual_time)
    if (time < accrual_time) {
      pd = ((1 / hazard) * exp(-1 * hazard * time) + time - 1 / hazard) / accrual_time
    } else {
      pd = 1 - exp(-1 * hazard * (time - accrual_time)) * (1 - exp(-1 * hazard * accrual_time)) / (hazard * accrual_time)
    }
    return(pd / ptot)
  }
  
  pct_eet_S = perdeath(haz_S, accrual_time, fu_time, analysis_time)
  pct_eet_S_100 = pct_eet_S * 100
  pct_eet_E = perdeath(haz_E, accrual_time, fu_time, analysis_time)
  pct_eet_E_100 = pct_eet_E * 100
  
  eea_S = (1 - pctCure_S) * N * pctS * (1 - exp(-1 * haz_S * fu_time) * (1 - exp(-1 * haz_S * accrual_time)) / (haz_S * accrual_time))
  eet_S = pct_eet_S * eea_S
  eea_E = (1 - pctCure_E) * N * (1-pctS) * (1 - exp(-1 * haz_E * fu_time) * (1 - exp(-1 * haz_E * accrual_time)) / (haz_E * accrual_time))
  eet_E = pct_eet_E * eea_E
  
  
  result = list(haz_S = signif(haz_S, 3),
                haz_E = signif(haz_E, 3),
                eet_S = floor(eet_S),
                pct_eet_S_100 = paste0(round(pct_eet_S_100,1), "%"),
                eet_E= floor(eet_E),
                pct_eet_E_100 = paste0(round(pct_eet_E_100,1), "%"))
  return(jsonlite::toJSON(result, pretty = TRUE))
}


View Expected Event Table Code


function(input_type, S_mS, S_mE, S_tS, S_tE, tS, tE, haz_S, haz_E, accrual_time, fu_time, N, pctS, pctCure_S, pctCure_E, time_unit) {
  # George and Desu (1974) #
  
  if (input_type == "Survival Probability") {
    haz_S = -1 * log(S_tS) / tS
    haz_E = -1 * log(S_tE) / tE
  } else if (input_type == "Median Survival") {
    haz_S = -(log(0.5)) / S_mS
    haz_E = -(log(0.5)) / S_mE
  }
  
  end_time = (accrual_time + fu_time) * (ifelse(time_unit == 'Years', 12, 1))
  follow_time = (fu_time) * (ifelse(time_unit == 'Years', 12, 1))
  acc_time = (accrual_time) * (ifelse(time_unit == 'Years', 12, 1))
  haz_S_months = haz_S / ifelse(time_unit == 'Years', 12, 1)
  haz_E_months = haz_E / ifelse(time_unit == 'Years', 12, 1)
  
  bymonth = matrix(nrow = end_time, ncol = 7)
  
  perdeath = function(hazard, accrual_time, fu_time, time) {
    ptot = 1 - exp(-1 * hazard * fu_time) * (1 - exp(-1 * hazard * accrual_time)) / (hazard * accrual_time)
    if (time < accrual_time) {
      pd = ((1 / hazard) * exp(-1 * hazard * time) + time - 1 / hazard) / accrual_time
    } else {
      pd = 1 - exp(-1 * hazard * (time - accrual_time)) * (1 - exp(-1 * hazard * accrual_time)) / (hazard * accrual_time)
    }
    return(pd / ptot)
  }
  
  pct_eet_S = perdeath(haz_S_months, acc_time, follow_time, end_time)
  pct_eet_S_100 = pct_eet_S * 100
  pct_eet_E = perdeath(haz_E_months, acc_time, follow_time, end_time)
  pct_eet_E_100 = pct_eet_E * 100
  
  eea_S = (1 - pctCure_S) * N * pctS * (1 - exp(-1 * haz_S_months * follow_time) * (1 - exp(-1 * haz_S_months * acc_time)) / (haz_S_months * acc_time))
  eet_S = pct_eet_S * eea_S
  eea_E = (1 - pctCure_E) * N * (1-pctS) * (1 - exp(-1 * haz_E_months * follow_time) * (1 - exp(-1 * haz_E_months * acc_time)) / (haz_E_months * acc_time))
  eet_E = pct_eet_E * eea_E
  
  for (i in 1:end_time) {
    Ni = floor(min(N, i * (N / acc_time)))
    p_S_i = perdeath(haz_S_months, acc_time, follow_time, i)
    p_E_i = perdeath(haz_E_months, acc_time, follow_time, i)
    
    ee_S_i = (1 - pctCure_S) * eet_S * p_S_i
    ee_E_i = (1 - pctCure_E) * eet_E * p_E_i
    ee_T_H0_i = ee_S_i + (ee_S_i * (1 - pctS) / pctS)
    ee_T_HA_i = ee_S_i + ee_E_i
    pct_S_i = ee_S_i / (eet_S * (1 - pctCure_S)) * 100
    pct_E_i = ee_E_i / (eet_E * (1 - pctCure_E)) * 100
    pct_T_H0_i = pct_S_i
    pct_T_HA_i = ((ee_S_i * pct_S_i) + (ee_E_i * pct_E_i)) / (ee_S_i + ee_E_i)
    pct_accr = Ni * 100 / N
    
    bymonth[i, ] = c(i, 
                     paste(format(round(ee_S_i,1),nsmall=1), ' (', format(round(pct_S_i,1),nsmall=1),'%)',sep=''),
                     paste(format(round(ee_E_i,1),nsmall=1), ' (', format(round(pct_E_i,1),nsmall=1),'%)',sep=''),
                     paste(format(round(ee_T_H0_i,1),nsmall=1), ' (', format(round(pct_T_H0_i,1),nsmall=1),'%)',sep=''),
                     paste(format(round(ee_T_HA_i,1),nsmall=1), ' (', format(round(pct_T_HA_i,1),nsmall=1),'%)',sep=''),
                     Ni, paste(format(round(pct_accr,1),nsmall=1),'%',sep=''))
    
  } 

  bymonth = as.data.frame(bymonth)
  names(bymonth) = c("TimeinMonths", "EventsUnderH0", "EventsUnderHA", 
                     "PctTotalEventsH0", "PctTotalEventsHA", "Accrual", "PctAccrual")

  result = list(haz_S = signif(haz_S, 3),
                haz_E = signif(haz_E, 3),
                event_table = bymonth)

  return(jsonlite::toJSON(result, pretty = TRUE))
}