prog def rct_minim *! version 2.3.0 Philip Ryan 2021-07-20 * v 1.0 2009-04-13 * v 1.0.1 change certain global macros to locals * v 1.1.0 [2009-04-18] allow more choices of measuring imbalance. * v 1.1.0 add further error trapping * v 1.1.1 fix program name in header! posted to SSC [2009-04-21] * v 1.1.2 further error trapping for misspecified factor levels [2009-04-23] * v 1.2.0 add P&S option 3.3(c) to pchoice() option, and add t() option for the corresponding parameter [2010-08-14] * v 2.0.0 allow first # of subjects to be allocated at random (no minimisation) using option delay(#) [2010-10-04] * v 2.0.0 and add _minim system variable to denote whether allocation is at random (_minim=0) or by minimisation (_minim=1) * v 2.0.0 provide pocock_simon_v2_rand.dta incorporating the _minim variable and pocock_simon_v2_factbal.dta to SSC * v 2.0.1 copyright information re -matsort- * v 2.1.0 -matsort- no longer required, replaced with mata code (courtesy Kit Baum) [2010-12-20] * v 2.1.0 provide pocock_simon_v3_rand.dta and pocock_simon_v3_factbal.dta incorporating minor corrections to v2 [2010-12-20] * v 2.2.0 fix error in name of matrix; bug reported by Paul Silcocks [Paul.Silcocks@liverpool.ac.uk] [2011-03-31] * v 2.2.1 fix error whereby the seed was never actually set; bug reported by Benjamin Leiby [benjamin.leiby@jefferson.edu] [2017-02-01] * v 2.2.1 formally set random number generator to kiss32 [suggested by Nooreen Dabbish [ooreen.dabbish@jefferson.edu] [2017-03-02] * v 2.2.1 add subject counter to screen message showing subject's treatment assignment * v 2.2.2 check for Stata Release < or >= 14 to check for valid -set rng- statement and enforce old KISS32 RNG * v 2.3.0 add sleep() option to slow program if rapid repeated invocations of rct_minim would mean duplicated seeds; Karla Hemming [K.HEMMING@bham.ac.uk][2021-07-20] version 10 clear syntax , FILEstub(string) NT(int) NF(int) NL(string) PCHOICE(string) [MCHOICE(string) P(string) Q(string) T(string)] /// [TREATVar(string) FACTNames(string) PATtern(string) W(string) LIMIT(int 1)] /// [SHOWDetail] [WARN] [delay(int 1)] [SLEEP(int 0)] di "Stata Release `c(stata_version)' is being used" if `=`c(stata_version)'' < 14 { local seed `=`=d(`=subinstr("`c(current_date)'", " ", "",.)')' + `=subinstr("`c(current_time)'",":","",.)'' set seed `seed' } if `=`c(stata_version)'' >= 14 { set rng kiss32 local seed `=`=d(`=subinstr("`c(current_date)'", " ", "",.)')' + `=subinstr("`c(current_time)'",":","",.)'' set seed_kiss32 `seed' } if "`showdetail'" !="" { di _new(2) in ye "Seed specified is " in wh "`seed'" if `=`c(stata_version)'' < 14 { di _new(1) in ye "Seed generated by Stata from specification is " in wh "`c(seed)'" } if `=`c(stata_version)'' >= 14 { di _new(1) in ye "Seed generated by Stata from specification is " in wh "`c(seed_kiss32)'" di "note that KISS32 random number generator is used" } } if `nt' < 2 { di _new as err "Must specify at least 2 treatments" exit 198 } if `nf' < 1 { di _new as err "Must specify at least 1 prognostic factor" exit 198 } if `sleep' < 0 { di _new as err "option sleep() must be a non-negative integer" exit 198 } mat drop _all if "`treatvar'" == "" { local treatvar "_group" } if substr("`filestub'", `=length("`filestub'")-3',4) == ".dta" { local filestub = substr("`filestub'", 1, `=length("`filestub'")-4') } local tol = 0.00001 if "`mchoice'" == "" { local mchoice "range" } else { if "`mchoice'" != "range" & "`mchoice'" != "sd" & "`mchoice'" != "var" & "`mchoice'" != "thresh" { di as err "mchoice must be specified as `=char(147)'range`=char(148)'," /// " `=char(147)'var`=char(148)', `=char(147)'sd`=char(148)' or `=char(147)'thresh`=char(148)'" exit 198 } } if "`mchoice'" == "thresh" & `limit' < 0 { di _new as err "limit must be > 0" exit 198 } if "`showdetail'" != "" { di _new in ye "using method " in wh "`mchoice'" in ye " to measure amount of imbalance" if "`mchoice'" == "thresh" { di in ye "limit for tolerating imbalance: " in wh "`limit'" } } if "`pchoice'" != "a" & "`pchoice'" != "b" & "`pchoice'" != "c" { di as err "pchoice must be specified as either `=char(147)'a`=char(148)' or `=char(147)'b`=char(148)' or `=char(147)'c`=char(148)'" exit 198 } if "`p'" !="" & "`q'" != "" & "`t'" != "" { di as err " may not specify p and q and t" exit 198 } if "`p'" !="" & "`q'" != "" { di as err " may not specify both p and q" exit 198 } if "`p'" !="" & "`t'" != "" { di as err " may not specify both p and t" exit 198 } if "`q'" !="" & "`t'" != "" { di as err " may not specify both q and t" exit 198 } if "`pchoice'" == "a" & "`p'" == "" { di as err "must specify p for pchoice method a" exit 198 } if "`pchoice'" == "b" & "`q'" == "" { di as err "must specify q for pchoice method b" exit 198 } if "`pchoice'" == "c" & "`t'" == "" { di as err "must specify t for pchoice method c" exit 198 } if "`p'" != "" { if (`=1/`nt'' - `p' > `tol') | (`p' > 1) { di _new as err "must have 1 >= p >= " `=1/`nt'' exit } if abs(`p' - (1/`nt')) < `tol' { di _new in ye "This specification of p will yield equal probabilities of treatment allocation" di _new in ye "Do you wish to proceed " in wh "(y/n)" in ye " ? " _re(_go) if "`go'" != "y" { exit } } } if "`q'" != "" { if (`=1/`nt'' - `q' > `tol') | (`q' - `=(2/(`nt'-1))' > `tol') { di _new as err "must have `=(2/(`nt'-1))' >= q >= " `=1/`nt'' exit } } if "`t'" != "" { if (0 - `t' > `tol') | (`t' > 1) { di _new as err "must have 1 >= t >= 0" exit } if abs(`t' - 0) < `tol' { di _new in ye "This specification of t will yield equal probabilities of treatment allocation" di _new in ye "Do you wish to proceed " in wh "(y/n)" in ye " ? " _re(_go) if "`go'" != "y" { exit } } } if `:word count `nl'' != `nf' { di _new as err "number of arguments of option" in wh " nl " as err "must equal number of factors" exit 198 } forvalues i = 1/`nf' { if `:word `i' of `nl'' < 2 { di _new as err "each factor must have at least 2 levels" exit 198 } if ceil(`:word `i' of `nl'') != floor(`:word `i' of `nl'' ) { di _new as err "each factor must have an integer number of levels" exit 198 } } if "`w'" != "" { if `:word count `w'' != `nf' { di as err "number of arguments of option" in wh " w " as err "must equal number of factors" exit 198 } forvalues i=1/`nf' { if `:word `i' of `w'' < `tol' { di as err "each weight must exceed 0" exit 198 } } } else { forvalues i=1/`nf' { local w "`w' 1" } } if "`showdetail'" !="" { display _new in ye "weight vector is " in wh "[`w']" } if "`factnames'" != "" & `:word count `factnames'' != `nf' { di as err "number of arguments of option" in wh " factnames " as err "must equal number of factors" exit 121 } forvalues i=1/`nf' { local nl_f`i' `:word `i' of `nl'' } if "`pattern'" == "" { forvalues i = 1/`nf' { if "`factnames'" == "" { display _new in ye "For this subject, level of " in wh "factor `i'" in ye " is " in wh "..." _re(_pat) } else { display _new in ye "For this subject, level of " in wh "`:word `i' of `factnames''" in ye " is " in wh "..." _re(_pat) } if `pat' <1 | `pat' > `nl_f`i'' { di _new as err "level for this factor must lie between 1 and `nl_f`i'' " exit 125 } if ceil(`pat') != floor(`pat') { di _new as err "factor levels must be integer" exit 198 } local pattern "`pattern' `pat'" } } if `:word count `pattern'' != `nf' { di as err "number of arguments of option" in wh " pattern " as err "must equal number of factors" exit 121 } forvalues i=1/`nf' { if `:word `i' of `pattern'' > `:word `i' of `nl'' { di _new as err "subject cannot have a factor `i' level exceeding maximum # of levels for this factor" exit 125 } if `:word `i' of `pattern'' < 1 { di _new as err "subject cannot have a factor `i' level less than 1" exit 125 } if ceil(`:word `i' of `pattern'') != floor(`:word `i' of `pattern'') { di _new as err "subject must have integer factor levels" exit 125 } } local ext ".dta" capture use `"`filestub'_factbal`ext'"' ******************************************************** ****************** first allocation ******************** ******************************************************** if _rc == 601 { local totlev 0 forvalues i=1/`nf' { local nl_f`i' `:word `i' of `nl'' local totlev = `totlev'+`nl_f`i'' } local coln "" mat def FactBal = J(`nt' , `totlev' , 0) forvalues i = 1/`nf' { forvalues j = 1/`nl_f`i'' { if "`factnames'" == "" { local coln `="`coln'" + " " + "f`i'_lev`j'"' } else { local coln `="`coln'" + " " + "`:word `i' of `factnames''_`j'"' } } } mat colnames FactBal = `coln' local rx = ceil(runiform()*`nt') di _new in ye "For the " in wh "1st" in ye " subject, treatment is always assigned at random with equal probability" di _new in ye "Assignment is to treatment " in wh "`rx'" set more off local col 0 local col_x 0 forvalues i = 1/`nf' { local col = `col' + `:word `i' of `pattern'' mat FactBal[`rx', `col'] = FactBal[`rx', `col'] + 1 local col_x = `col_x' + `nl_f`i'' local col = `col_x' } if "`showdetail'" !="" { mat list FactBal } qui svmat FactBal , names(col) di _new save `"`filestub'_factbal`ext'"', replace di _new clear qui { set obs 1 di _new gen int _SeqNum = 1 gen byte `treatvar' = `rx' if "`factnames'" != "" { forvalues i = 1/`nf' { gen byte `:word `i' of `factnames'' = `:word `i' of `pattern'' } } else { forvalues i = 1/`nf' { gen byte factor`i' = `:word `i' of `pattern'' } } } order _SeqNum `treatvar' char _dta[minim_1] rct_minim `0' char _dta[TD_1] `c(current_time)' `c(current_date)' char _dta[orig_delay] `delay' gen byte _minim = 0 lab var _minim "minimization algorithm used" label define yesno 0 no 1 yes labe values _minim yesno save `"`filestub'_rand`ext'"', replace set more on mat drop _all } //end if 1st subject ********************************************************** **************** end of first allocation ***************** ********************************************************** ******* ******* ******* * ********************************************************** **************** subsequent allocations ****************** ********************************************************** * ******* ******* ******* else { // that is, existing file found must have at least one subject randomised mat drop _all use `"`filestub'_rand`ext'"' gettoken my_cmd: 0 unab varlist: _all if `=`:word count `varlist''-3' != `nf' { di _new as err "number of factors specified for this subject does not match number specified" di as err "for previous subject" exit 198 } if "`:word 2 of `varlist''" != "`treatvar'" { di _new as err "name of treatment variable specified does not match variable name in data file" clear exit 111 } local totlev 0 forvalues i=1/`nf' { local nl_f`i' `:word `i' of `nl'' local totlev = `totlev'+`nl_f`i'' } * di _new "total levels are `totlev'" if "`factnames'" == "" { local coln "" forvalues i = 1/`nf' { local coln `="`coln'" + " " + "factor`i'"' } } else { local coln "`factnames'" } local vr "_minim" forvalues i=1/2 { local j : word `i' of `varlist' local vr = "`vr'" + " " + "`j'" } local nvl : list varlist - vr if "`nvl'" != "`coln'" { di _new in red "factor names specified do not match existing variable names in " in wh `"`filestub'_rand`ext'"' clear exit 111 } if "`warn'" != "" { di _new(3) in ye "The command you just issued was: " _new in gr "rct_minim `0'" di _new in ye "For the *previous* allocation, stamped " in gr "`_dta[TD_`=_N']'" in ye ", the command issued was:" di in gr "`_dta[minim_`=_N']'" if `delay' != `_dta[orig_delay]' { di _new in ye "The -delay- parameter was originally (ie on allocation of 1st subject) set to " in gr `_dta[orig_delay]' /// in ye "; now requested set to " in gr `delay' in ye ". CAUTION!!" } di _new in ye "Check consistency of commands. Do you wish to proceed " in wh "(y/n)" in ye " ? " _re(_go) if "`go'" != "y" { exit } } use `"`filestub'_factbal`ext'"', clear unab varns: _all if `:word count `varns'' != `totlev' { di _new as err "the total number of levels across all factors for this subject does not match" di as err "the total number for the previous subject." exit 198 } if `nt' != `=_N' { di _new as err "the number of treatments specified for this subject does not match that specified" di as err "for the previous subject" exit 198 } mat def W = J(`nf', 1, 0) forvalues i=1/`nf' { mat W[`i', 1] = `:word `i' of `w'' } mat colnames W = "weight" mkmat _all, mat(FactBal) if "`showdetail'" !="" { di _new "Current Factor Balance matrix is:" mat li FactBal } local numsofar = 0 forvalues i = 1/`nl_f1' { forvalues j = 1/`nt' { local numsofar = `numsofar' + FactBal[`j', `i'] } } di _new "Now working on subject number " in wh "`=`numsofar'+1'" mat FL = J(1,`totlev', 0) local rflag = `=`numsofar'+1' <= `delay' set more off local col 0 local col_x 0 forvalues i = 1/`nf' { local col = `col' + `: word `i' of `pattern'' mat FL[1, `col'] = 1 local col_x = `col_x' + `nl_f`i'' local col = `col_x' } * mat li FL forvalues k = 1/`nt' { mat H`k' = FactBal forvalues i=1/`totlev' { mat H`k'[`k', `i'] = H`k'[`k', `i'] + FL[1,`i'] } if "`showdetail'" !="" { di _new in ye "If treatment " in wh "`k'" in ye " is assigned, Factor Balance matrix will be:" mat list H`k' } mat H`k'_tmp = H`k' } if `rflag' == 0 { // allocations after number specified by the option "delay" if "`mchoice'" == "range" { local xtra = `nf' + 1 forvalues k=1/`nt' { mat D`k' = J(`xtra',1,0) local j = 0 forvalues i = 1/`totlev' { if FL[1, `i'] == 1 { mata: st_matrix("H`k'_tmp", sort(st_matrix("H`k'_tmp"), `i')) local j = `j'+1 mat D`k'[`j',1] = H`k'_tmp[`nt', `i'] - H`k'_tmp[1, `i'] } } forvalues s=1/`nf' { mat D`k'[`xtra', 1] = D`k'[`xtra', 1] + (D`k'[`s',1]*W[`s', 1]) } * mat li D`k' } /* end forvalues k=1/`nt' */ } /* end if mchoice is range */ if "`mchoice'" == "thresh" { local xtra = `nf' + 1 forvalues k=1/`nt' { mat D`k' = J(`xtra',1,0) local j = 0 forvalues i = 1/`totlev' { if FL[1, `i'] == 1 { mata: st_matrix("H`k'_tmp", sort(st_matrix("H`k'_tmp"), `i')) local j = `j'+1 mat D`k'[`j',1] = (H`k'_tmp[`nt', `i'] - H`k'_tmp[1, `i']) > `limit' } } forvalues s=1/`nf' { mat D`k'[`xtra', 1] = D`k'[`xtra', 1] + (D`k'[`s',1]*W[`s', 1]) } * mat li D`k' } /* end forvalues k=1/`nt' */ } /* end if mchoice is thresh */ if "`mchoice'" == "sd" | "`mchoice'" == "var" { local xtra = `nf' + 1 forvalues k=1/`nt' { mat D`k' = J(`xtra',1,0) local j = 0 forvalues i = 1/`totlev' { if FL[1, `i'] == 1 { local j = `j'+1 local sum 0 local sumsq 0 forvalues x= 1/`nt' { local sum = `sum' + H`k'_tmp[`x', `i'] local sumsq = `sumsq' + (H`k'_tmp[`x', `i'])^2 } local varc = (`sumsq'-((`sum'^2)/`nt'))/(`=`nt'-1') local sd = sqrt(`varc') mat D`k'[`j',1] = cond("`mchoice'"=="var",`varc', `sd') } } forvalues s=1/`nf' { mat D`k'[`xtra', 1] = D`k'[`xtra', 1] + (D`k'[`s',1]*W[`s', 1]) } * mat li D`k' } } /* end if mchoice is sd or var */ mat G = J(`nt', 2, 0) forvalues k=1/`nt' { mat G[`k', 1] = `k' mat G[`k', 2] = D`k'[`xtra', 1] } mat colnames G = `treatvar' weighted_G if "`showdetail'" !="" { di _new "G matrix before sorting is:" mat list G } ********* qui { clear svmat G, names(col) gen y = uniform() sort w y drop y mkmat _all, mat(G) } ********* local Gsmall = G[1,2] if "`showdetail'" !="" { di _new "G matrix after sorting (and random breaking of ties if present) is:" mat li G } mat RO = G[1..`nt',1..1] if "`showdetail'" !="" { display in ye _new "G(smallest) = " in wh "`Gsmall'" in ye " representing treatment " in wh RO[1,1] } * mat list RO if "`pchoice'" == "c" { local sumG = 0 forv i=1/`nt' { local sumG = `sumG' + G[`i',2] } if "`showdetail'" !="" { di _new in ye "sum of elements of weighted G = " in wh `sumG' } } mat def P = J(`nt',1,0) if "`pchoice'" == "a" { mat P[1,1] = `p' forvalues i = 2/`nt' { mat P[`i', 1] = (1-`p')/(`nt'-1) } } else if "`pchoice'" == "b" { forvalues i=1/`nt' { mat P[`i',1] = `q' - `i'*((2*((`nt'*`q')-1)) /(`nt'*(`nt'+1) ) ) } } else { forvalues i=1/`nt' { mat P[`i',1] = (1/(`nt'-`t')) * (1-(`t'* G[`i',2]/`sumG')) } } mat colnames P = "prob" if "`showdetail'" !="" { mat list P } mat cdf = J(`nt',2,0) local v = 0 forvalues i = 1/`nt' { local v = `v'+ P[`i', 1] mat cdf[`i', 1] = `v' } *** * set trace on *** mat RO_cdf = RO, P mat RO_cdf = RO_cdf, cdf local u = runiform() forvalues i = 1/`nt' { mat RO_cdf[`i', 4] = `u' <= RO_cdf[`i',3] } matrix colnames RO_cdf = `treatvar' P cdf u_flag if "`showdetail'" != "" { di _new in ye "Random number, " in wh "u" in ye ", for allocation of treatment is " in wh "`u'" di in ye _new "Cumulative distribution function for probabilities, ordered by treatments based on G:" mat li RO_cdf } *** * set trace off *** local i 1 while `i' <= `nt' { if RO_cdf[`i',4] == 1 { local rx = RO_cdf[`i',1] local i = `nt' } local i=`i'+1 } di _new in ye "For this subject, treatment is assigned using the minimisation algorithm" } // end if dealing with allocations after value specified by option "delay" if `rflag' == 1 { // deal with allocations specified by option "delay" local rx = int((`nt'*runiform())+1) di _new in ye "For this subject, treatment is assigned at random with equal probability" } di in ye _new "Assignment for subject " in wh "`=`numsofar'+1'" in ye " is to treatment " in wh "`rx'" mat FactBal = H`rx' if "`showdetail'" != "" { di in ye _new "Updated Factor Balance matrix is:" mat list FactBal } set more off di _new clear qui svmat FactBal , names(col) save `"`filestub'_factbal`ext'"', replace qui { clear use `"`filestub'_rand`.ext.'"' set obs `=`numsofar'+1' replace _SeqNum = _N in `=_N' replace `treatvar' = `rx' in `=_N' if "`factnames'" != "" { forvalues i = 1/`nf' { replace `:word `i' of `factnames'' = `:word `i' of `pattern'' in `=_N' } } else { forvalues i = 1/`nf' { replace factor`i' = `:word `i' of `pattern'' in `=_N' } } } order _SeqNum `treatvar' char _dta[minim_`=_N'] rct_minim `0' char _dta[TD_`=_N'] "`c(current_time)' `c(current_date)'" replace _minim = !`rflag' in `=_N' save `"`filestub'_rand`ext'"', replace } /* end else*/ mat drop _all set more on if `sleep' > 0 { di in ye _new " sleeping now....YAWN....." } sleep `sleep' end