*! nstagebinopt v1.02 20Dec2022 by BCO, based on v1.0 17jul2014 by DB /* nstageoptbinopt Output admissible designs into a Stata dataset (control alpha and power) */ program def nstagebinopt, rclass version 10.0 syntax, Nstage(int) /// total number of stages ARms(int) /// total number of arms (inc. control) ALpha(real) /// type I error rate POwer(real) /// pairwise power theta0(string) theta1(string) /// risk differences under H0 and H1 Ctrlp(string) /// control event rate ARAtio(numlist >=0.1 <=2) /// allocation ratio [ppv(real 999) /// P(D=1|I=1) Save(string) /// filename in which to save dataset containing designs FWer /// control FWER at level alpha pi(real 0.1) /// min proportion of max control sample size recruited in each stage (default 0.1) p(string) /// power(s) in alpha function (>2-stages only) ***make numlist or remove? Ltfu(string) /// loss-to-follow-up rates fu(real 0) /// length of f/u on I outcome ACCRate(string) /// accrual rate in each stage acc(real 0.0005) /// max diff in power and alpha of feasible designs from specified values PLot *] // plot expected sample sizes of admissible designs local xopt `"`options'"' nois dis `xopt' local ver 1.0.2 local date 09 June 2023 local seed 12345 if `seed'>0 set seed `seed' // Rename macros local J = `nstage' // # stages local Jm1 = `J'-1 // # I-stages local K = `arms'-1 // # experimental arms if "`fwer'"!="" local fwerate = `alpha' // Check for errors local errcount 0 if `nstage' < 2 { di as err "nstage() must be 2 or more" local ++errcount } if `nstage' > 5 { di as err "no more than 5 stages are recommended" local ++errcount } if `pi'<0 | `pi'>1 { di as err "pi() must be between 0 and 1/`J'" local ++errcount } if `ppv'!=999 { if `ppv'<0 | `ppv'>1 { di as err "ppv() should be between 0 and 1" local ++errcount } } if `alpha'!=-1 { if `alpha'>0.5 | `alpha'<=0.001 { di as err "alpha() must be within a plausible range" local ++errcount } } if `power'>=1 | `power'<0.7 { di as err "power() must be within a plausible range" local ++errcount } if `acc'<0 | `acc'>0.01 { di as err "acc() must be between 0 and 0.01" local ++errcount } // Set default ltfu if "`ltfu'"=="" { if `ppv'==999 local ltfu 0 else local ltfu 0 0 } if `fu'==0 & "`accrate'"=="" { forvalues j = 1/`J' { local accrate `accrate' 100 } } // Split strings up * Strings with J items local nopts: word count `accrate' if `nopts'!=`J' { di as err "must specify `J' values for accrate()" local ++errcount } forvalues j = 1/`J' { local accrS`j' : word `j' of `accrate' cap confirm number `accrS`j'' if _rc { di as err "`accrS`j'' found where number expected" local ++errcount } } * Strings with 1 (I=D) or 2 (I!=D) items local nval = cond(`ppv'!=999, 2, 1) if `nval'==2 local s `s' local opts theta0 theta1 ctrlp ltfu local nopts: word count `opts' tokenize `opts' forvalues i = 1/`nopts' { local opt ``i'' local nopt: word count ``opt'' if `nopt'>`nval' { di as err "too many items specified for `opt'() - maximum is `nval'" local ++errcount } if `nopt'<`nval' { di as err "must specify `nval' value`s' for `opt'()" local ++errcount } forvalues j = 1/`nopt' { local `opt'O`j': word `j' of ``opt'' cap confirm number ``opt'O`j'' if c(rc) { di as err "'``opt'O`j''' found where number expected" local ++errcount } } if `nval'==1 { forvalues j = 1/`J' { local `opt'S`j' ``opt'O1' } } else { forvalues j = 1/`Jm1' { local `opt'S`j' ``opt'O1' } local `opt'S`J' ``opt'O2' } } if `errcount'>0 exit 198 // Check p if "`p'"=="" { if `J'>2 { if `ppv'==999 local p 0 0.25 0.5 else local p 0 0.25 0.5 0.75 1 } else local p 0 } else { if `J'==2 local p 0 } numlist "`p'", range(>=0 <=1) sort missingokay min(0) local p_list `r(numlist)' local pci `ctrlpS1' local th0i `theta0S1' local th1i `theta1S1' local ltfui `ltfuS1' local accri `accrS1' forvalues j = 2/`J' { local pci `pci' \ `ctrlpS`j'' local th0i `th0i' \ `theta0S`j'' local th1i `th1i' \ `theta1S`j'' local ltfui `ltfui' \ `ltfuS`j'' local accri `accri' \ `accrS`j'' } matrix pc = (`pci') matrix th0 = (`th0i') matrix th1 = (`th1i') matrix ltfu = (`ltfui') matrix accr = (`accri') // Generate feasible design dataset here - specify location and name in command preserve clear forvalues j = 1/`J' { gen a`j' = . gen w`j' = . gen N`j' = . } gen A = . gen alpha = . gen fwer = . if `ppv'==999 gen se_fwer = . gen power = . gen nCfix = . gen p = . di as text " Finding set of feasible designs..." * (run p forloop here) foreach A of numlist `aratio' { // If FWER specified, find required PWER (search between FWER/K and FWER) qui if "`fwer'"!="" & `K'>1 { local a = round(`fwerate'/`K',0.0001) local min 0 local max 0 qui while !`max' { fwer1stage, arms(`arms') alpha(`a') aratio(`A') local fwerhat = r(fwer) if !`min' & `fwerhat'-`fwerate'>-`acc' { local la = `a' local min 1 } if `fwerhat'-`fwerate'>`acc' { local ua = `a'-0.00001 local max 1 } else local a = `a'+0.00001 } local alpacc = (`ua'-`la')/2 local alpha = (`ua'+`la')/2 } else local alpacc = `acc' foreach pp in `p_list' { mata: feas(`J', `K', `alpha', `power', st_matrix("pc"), st_matrix("th0"), /// st_matrix("th1"), st_matrix("ltfu"), `A', `pi', `ppv', `pp', `alpacc', /// `fu', st_matrix("accr"), `seed') } } qui count if r(N)==0 { di as err "No designs" exit 198 } // Design parameters format a? w? %9.2f if `ppv'==999 | "`fwer'"=="" format a`J' %9.3f *else format a`J' %9.0g // 4 dp enough? else format a`J' %9.4f // Babak changed this from version 1 qui tostring a? w?, force u replace gen sw_alpha = a1 gen sw_power = w1 qui forvalues j = 2/`J' { replace sw_alpha = sw_alpha + " " + a`j' replace sw_power = sw_power + " " + w`j' } // Expected sample sizes (if K>1) qui { noi di as text " Calculating expected sample sizes..." foreach k in 0 `K' { gen ess`k' = . } local ndes = _N *gen ok = . local narms local acc forvalues j = 1/`J' { local narms `narms' `arms' } if `ppv'==999 local fup `fu' else local fup `fu' 0 forvalues d = 1/`ndes' { local a_set = sw_alpha in `d' local w_set = sw_power in `d' local A = A in `d' if "`fwer'"=="" local nofwer nofwer qui which nstagebin if `ppv'==999 cap nstagebin, nstage(`J') acc(`accrate') theta0(`theta0') /// theta1(`theta1') ctrlp(`ctrlp') arms(`narms') alpha(`a_set') /// power(`w_set') aratio(`A') ltfu(`ltfu') fu(`fup') ess seed(`seed') `nofwer' else cap nstagebin, nstage(`J') acc(`accrate') theta0(`theta0') theta1(`theta1') /// ctrlp(`ctrlp') arms(`narms') alpha(`a_set') power(`w_set') aratio(`A') /// ppvc(`ppv') ppve(`ppv') ltfu(`ltfu') fu(`fup') ess seed(`seed') `nofwer' *if _rc==0 { forvalues j = 1/`J' { local N`j' = r(totS`j') replace N`j' = `N`j'' in `d' } if "`fwer'"!="" { if `ppv'==999 { local fwerhat = r(fwer) local se_fwerhat = r(se_fwer) replace se_fwer = `se_fwerhat' in `d' } else local fwerhat = r(maxfwer) replace fwer = `fwerhat' in `d' } foreach k in 0 `K' { local ess`k' = r(ess`k') replace ess`k' = round(`ess`k'') in `d' // NB: rounding makes it less sensitive to the simulation error } *replace ok = 1 in `d' *} *else replace ok = 0 in `d' } *drop if ok==0 } /* Optimal designs - K>1 only qui if `K'>1 { noi di as text "Finding set of optimal designs..." gen opt = "" forvalues k = 0/`K' { egen miness`k' = min(ess`k') replace opt = opt+"`k' " if ess`k'==miness`k' } drop miness* } */ /* Admissible designs - if K=1 minimise qE(N|H0)+(1-q)max(N) - otherwise minimise qE(N|H0)+(1-q)E(N|HK) */ di as text " Finding set of admissible designs..." qui { // Size of shortest stage gen N0 = 0 forvalues j = 1/`J' { local jm1 = `j'-1 gen dN`j' = N`j'-N`jm1' } egen min_n = rowmin(dN*) drop dN* N0 /* Admissible designs if >1, choose one with: lowest ESS, lowest MSS, longest stage duration */ gen min_q = . gen max_q = . forvalues q = 0(0.01)1.001 { cap drop min_loss q loss if `K'==1 { *replace ess0 = round(ess0) *gen loss = (1-`q')*ess0+`q'*N`J' /*Babak changed this from V1.0*/ gen loss = (1-`q')*ess0+`q'*ess`K' } else gen loss = (1-`q')*ess0+`q'*ess`K' egen min_loss = min(loss) gen q = `q' if loss==min_loss count if q==float(`q') if r(N)>1 { egen min_ess0 = min(ess0) if q==float(`q') replace q = . if ess0!=min_ess0 if `K'==1 { *egen min_max = min(N`J') if q==float(`q') /*Babak changed this from V1.0*/ *replace q = . if N`J'!=min_max /*Babak changed this from V1.0*/ *drop min_max /*Babak changed this from V1.0*/ egen min_ess`K' = min(ess`K') if q==float(`q') replace q = . if ess`K'!=min_ess`K' drop min_ess`K' } else { egen min_ess`K' = min(ess`K') if q==float(`q') replace q = . if ess`K'!=min_ess`K' drop min_ess`K' } egen max_dN = max(min_n) if q==float(`q') replace q = . if min_n!=max_dN drop min_ess0 max_dN } replace min_q = q if min_q==. replace max_q = q if (max_q2 order q_range p sw_alpha sw_power sw_N ess* alpha power else order q_range sw_alpha sw_power sw_N ess* alpha power } label var sw_alpha "Stagewise sig. levels" label var sw_power "Stagewise powers" label var sw_N "Stagewise sample size" label var ess0 "Expected sample size under H0" label var ess`K' "Expected sample size under H`K'" label var alpha "Pairwise type I error rate" label var power "Pairwise power" label var A "Allocation ratio" label var nCfix "Fixed sample size for control" if "`fwer'"!="" { label var fwer "Maximum FWER" format fwer %9.4f } *drop nfix if "`fwer'"!="" drop alpha power else drop power // Output designs // Display results if "`fwer'"!="" & `K'>1 local ncol 9 /*Babak changed this from V1.0*/ else local ncol 8 /*Babak changed this from V1.0*/ local ww1 11 local ww2 9 local ww3 7 /*Babak added this from V1.0*/ local sr1 %`ww1's local sr2 %`ww2's local sr3 %`ww3's /*Babak added this from V1.0*/ local totww = (`ncol'-1)*`ww2'+`ww2' /*Babak changed this from V1.0*/ local title1 "n-stage (binary) trial design version `ver', `date'" local title2 "Choodari-Oskooei, Bratton, and Parmar (2023) Stata Journal 23(3)." local hline = `totww' di as text _n "`title1'" _n "{hline `hline'}" di as text "Admissible designs for a " as res "`arms'" as text "-arm " as res /// "`nstage'" as txt "-stage trial with binary outcome based on" di as text "`title2'" di as text "{hline `totww'}" di as text "Design" `sr1' "q-range" `sr2' "Stage" `sr2' "Sig." `sr2' /// "Power" `sr2' "Alloc." `sr2' "E(N|H0)" _cont * if `arms'>2 di as text `sr2' "E(N|H1)" _cont /* Babak changed this from V1.0*/ * else di as text `sr2' "max(N)" _cont /* Babak changed this from V1.0*/ di as text `sr2' "E(N|H1)" _cont /* Babak changed this from V1.0*/ if "`fwer'"!="" & `K'>1 di as text `sr3' "FWER" _cont di _n as text "number" `sr1' "" `sr2' "" `sr2' "level" `sr2' "" `sr2' "ratio" _cont if "`fwer'"!="" & `ppv'==999 & `K'>1 di as text `sr2' "" `sr2' "" `sr3' "(SE)" _cont /*This is from Babak - might have to change this*/ di _n as text "{hline `totww'}" local ndes = _N forvalues d = 1/`ndes' { if "`fwer'"!="" { local fwerate = fwer in `d' if `ppv'==999 local se_fwerate = se_fwer in `d' } local ess0 = ess0 in `d' *if `arms'>2 local ess`K' = ess`K' in `d' /* Babak changed this from V1.0*/ local ess`K' = ess`K' in `d' local A = A in `d' local q_range = q_range in `d' forvalues j = 1/`J' { local a`j' = a`j' in `d' local w`j' = w`j' in `d' local N`j' = N`j' in `d' } forvalues j = 1/`J' { if `j'==1 { di as res _skip(1) "`d'" _skip(6) "`q_range'" _skip(4) `j' _skip(6) /// "`a`j''" `sr2' "`w`j''" %`ww2'.2f `A' %`ww2'.0f `ess0' _cont *if `arms'>2 di as res %`ww2'.0f `ess`K'' _cont /* Babak changed this from V1.0*/ *else di as res %`ww2'.0f `N`J'' _cont di as res %`ww2'.0f `ess`K'' _cont if "`fwer'"!="" & `K'>1 di as res _cont %`ww2'.4f `fwerate' _cont } else { di _n as res _skip(4) `sr1' "" %`ww2'.0f `j' _skip(1) `sr2' "`a`j''" /// `sr2' "`w`j''" _cont if `j'==2 & "`fwer'"!="" & `ppv'==999 & `K'>1 di `sr2' "" `sr2' "" /// `sr2' "" as text " (" as res %5.4f `se_fwerate' as text ")" _cont } } noi di _n as text "{hline `totww'}" } if `K'==1 { di as txt " Note: each design minimises the loss function (1-q)E(N|H0)+qE(N|H1) for" /* Babak changed this from V1.0*/ di as txt " weights q specified in q-range." *di as txt " q=0 corresponds to minimax design & q=1 corresponds to null-optimal design" } else { label var ess`K' "Expected sample size under H`K'" di as txt " Note: each design minimises the loss function (1-q)E(N|H0)+qE(N|H1) for values of" di as txt " q specified in q_range. H1 is the hypothesis that all of the experimental" di as txt " arms are effective." *di as txt " q=0 corresponds to alternative-optimal design & q=1 corresponds to null-optimal design" } drop a? w? N? // Save designs if filename specified if `K'==1 drop *fwer* if "`save'"!="" qui sav "`save'.dta", replace // Draw plot of ESS vs MSS qui if "`plot'"!="" { tw /// (scatter ess0 ess`K', c(l) ms(O) mfc(white)), /// xtitle(Expected sample size | H{sub:1}) /// ytitle(Expected sample size | H{sub:0}) `xopt' } restore end *! fwer1stage v0.02 program def fwer1stage, rclass version 10 /* Calculate FWER for multi-arm 1-stage trials algebraically v0.02 - can specify 1 alpha for each arm or a common alpha */ syntax, arms(int) alpha(string) aratio(real) local K = `arms'-1 // # E arms local A = `aratio' local nopts: word count `alpha' if `nopts'!=1 & `nopts'!=`K' { di as err "1 or `K' options should be specified for alpha" exit 198 } forvalues k = 1/`K' { if `nopts'==1 local z`k' = invnormal(1-`alpha') else { local a`k': word `k' of `alpha' local z`k' = invnormal(1-`a`k'') } } // Correlation matrix local r = `A'/(`A'+1) tempname R matrix def `R' = I(`K') forvalues k = 1/`K' { forvalues l = 1/`K' { if `k'!=`l' matrix def `R'[`k',`l'] = `r' else matrix def `R'[`k',`l'] = 1 } } // Vector of z values local z1ma `z1' forvalues k = 2/`K' { local z1ma `z1ma', `z`k'' } tempname Z matrix `Z' = (`z1ma') local rep = 5000 mata: mvnpb("`Z'", "`R'", `rep') local fwer = 1-r(p) return scalar fwer = `fwer' di as text "FWER = " as res %5.4f `fwer' end mata: mata clear void mvnpb(string scalar xx, string scalar vv, real scalar reps) { /* Assumes Hammersley sequences are to be generated, without antithetics. */ real vector opt x = st_matrix(xx) // row or col vector of args at which probability is required V = st_matrix(vv) // variance-covariance matrix opt = (2, reps, 1, 0) // 2 for Hammersley p = ghk( x, V, opt, rank=.) st_numscalar("r(p)", p) } end // Start of Mata commands mata: void feas(J, K, alpha, power, pc, th0, th1, ltfu, A, pi, ppv, p, acc, fu, accr, seed) { Jm1 = J-1 /* Event rates under H0 and H1 in E arm */ p0 = pc:+th0 p1 = pc:+th1 FU = J(J,1,fu) FU[J]=0 /* Accrual rate in control arm */ /*accr0 = accr:/(1+A)*/ /* Fixed sample size */ nCfix = round((invnormal(1-alpha)+invnormal(power))^2 * (A*pc[J]*(1-pc[J])+p1[J]*(1-p1[J]))/(A*(1-ltfu[J])*(th1[J]-th0[J])^2)) /*nEfix = round(A*nCfix) nfix = nCfix+K*nEfix*/ count = 0 min_loss = 1E6 opt_ess = 1E6 opt_mss = 1E6 reps = 2000 /* Stage 1 alpha */ if (ppv==999) min_alpha = round(floor(1000*alpha)/1000,0.001) else min_alpha = alpha a1 = 0.5 while (a1 >= min_alpha ) { /* Power in intermediate stages */ wI = power while ( wI <= 0.991 ) { /* Calculate S1 sample size */ nc1 = round((invnormal(1-a1)+invnormal(wI))^2 * (A*pc[1]*(1-pc[1])+p1[1]*(1-p1[1]))/(A*(th1[1]-th0[1])^2)) /* Upper bound for final stage power */ min_w = wI^(J-1) maxwD = min((wI, ceil(100*power/min_w)/100)) maxwD = maxwD + 0.001 /* Final stage power */ wD = power while ( wD <= maxwD ) { /* Upper bound for final stage alpha */ if (ppv==999) maxaJ=2*alpha else maxaJ = alpha /* Final stage alpha */ aJ = min_alpha while ( aJ <= maxaJ ) { a = J(J,1,0) w = J(J,1,0) nc = J(J,1,0) /*NC = J(J,1,0) maxN = J(J,1,0)*/ /* Stagewise alpha and power vectors */ a[1] = a1 a[J] = aJ min_a = 1 err_a = 0 for (j=1 ; j<=J ; j++) { jm1 = j-1 if ((j>1) & (j1) { if (a[j]>=a[jm1]) ++err_a } } if (err_a>0) { aJ = aJ+0.001 continue } if (min_a > alpha) { aJ = aJ+0.001 continue } /* Sample size in each stage */ z_a = invnormal(a) z_w = invnormal(w) nc = round((z_w:-z_a):^2:*(A:*pc:*(1:-pc):+p1:*(1:-p1)):/(A:*(th1:-th0):^2)) /*NC = round(nc:/(1:-ltfu)+FU:*r0) /* need to incorporate f/u */ maxN = NC:+K*round(A:*NC)*/ /* Number of controls and total recruited in each stage */ /*dNC = J(J,1,0) dN = J(J,1,0)*/ dnc = J(J,1,0) for (j=1 ; j<=J ; j++) { jm1 = j-1 /*if (j==1) dNC[1] = NC[1] else dNC[j] = (NC[j]-NC[jm1]) if (j==1) dN[1] = maxN[1] else dN[j] = (maxN[j]-maxN[jm1])*/ if (j==1) dnc[1] = nc[1] else dnc[j] = nc[j]-nc[jm1] } if (sum((dnc:/nc[J]):2-stages */ else { rho0 = J(J,J,1) if (ppv!=999) v_th0 = (A:*pc:*(1:-pc):+p0:*(1:-p0)):/(A:*nc) for (j=1 ; j<=J ; j++) { jm1 = j-1 for(i=1 ; i<=jm1 ; i++) { if (j2-stages */ else { rho1 = rho0 if (ppv!=999) { v_th1 = (A:*pc:*(1:-pc):+p1:*(1:-p1)):/(A:*nc) for (i=1 ; i<=J-1 ; i++) { rho1[i,J] = ((ppv*p1[i]-p1[i]*p1[J])+A*(ppv*pc[i]-pc[i]*pc[J])) / (A*nc[J]*sqrt(v_th1[i])*sqrt(v_th1[J])) rho1[J,i] = rho1[i,J] } } w_hat = mvnp(z_w, rho1, reps) } /* Continue with near-feasible designs */ if (abs(w_hat-power)<0.0005) { /* /* Calculate expected sample size if K=1 */ if (K==1) { /* 2-stages */ if (J==2) ess = dN[1]+a[1]*dN[2] /* >2-stages */ else { sw_a = J(1,J,1) sw_a[2] = a[1] sw_a[3] = binormal(z_a[1], z_a[2], rho0[1,2]) for (j=4 ; j<=J ; j++) { jm1 = j-1 sw_a[j] = mvnp(z_a[(1..jm1)] , rho0[(1..jm1),(1..jm1)] , reps) } ess = sw_a*dN } } */ /* Store results in dataset: - stagewise alphas, power and max sample sizes - ESS - pairwise alpha and power */ st_addobs(1) i = st_nobs() for (j=1 ; j<=J ; j++) { st_store(i, 3*(j-1) + 1, a[j]) st_store(i, 3*(j-1) + 2, w[j]) /*st_store(i, 3*(j-1) + 3, maxN[j])*/ } /*if (K==1) st_store(i, "ess", ess)*/ st_store(i, "A", A) st_store(i, "alpha", a_hat) st_store(i, "power", w_hat) st_store(i, "p", p) st_store(i, "nCfix", nCfix) } } } aJ = aJ+0.001 } wD = wD+0.01 } wI = wI+0.01 } a1 = a1-0.01 } } real scalar mvnp(Z, R, reps) { /* Assumes Hammersley sequences are to be generated, without antithetics. */ real vector opt opt = (2, reps, 1, 0) // 2 for Hammersley p = ghk( Z, R, opt, rank=.) return(p) } end