* "over" functionality for ipdmetan * In separate program on advice of Patrick Royston * created by David Fisher, February 2013 * Release notes for versions prior to v4.0: see end of file * version 4.0 David Fisher 25nov2020 // no changes to code, other than adding check that -metan- v4.0+ is installed; upversioned to match metan/ipdmetan // some changes to text in the help file * version 4.01 David Fisher 12feb2021 // no changes to code; upversioned to match with -ipdmetan- * version 4.02 David Fisher 20apr2021 // no changes to code; upversioned to match with -ipdmetan- *! version 4.03 David Fisher 12oct2022 // Added "clear" (and undocumented "clearstack") options // Added prefix() option so that saved variables are `prefix'_ES etc. // Fixed bug which made "No. pts" column appear in forestplot with r-class commands even if not explicitly specified (displaying all missing values) // (note however that a column of missing values DOES still appear on-screen, as a prompt to the user.) program define ipdover, rclass version 11 local version : di "version " string(_caller()) ":" * NOTE: mata requires v9.x * factor variable syntax requires 11.0 * but neither is vital to ipdover // Check that -metan- v4.0+ is installed cap metan if "`r(metan_version)'"=="" { nois disp as err "This version of {bf:ipdmetan} requires version 4.00 or higher of {bf:metan}" exit 499 } else { local current_version = 4.05 if `r(metan_version)' < `current_version' { nois disp as err "{bf:metan} version " as res `r(metan_version)' as err " may not the most recent version available" nois disp as err "Please check, and consider updating {bf:metan}" } } // ipdmetan has two possible syntaxes: // "generic" effect measure / Syntax 1 ==> ipdmetan [exp_list] .... : [command] [if] [in] ... // (calculations based on an estimation model fitted within each study) // "specific" effect measure / Syntax 2 ==> ipdmetan varlist [if] [in] ... **no colon** // (raw event counts or means (SDs) within each study using some variation on -collapse-) // We can let ipdmetan.ado sort out the details; we just want to parse certain options first // (i.e. those that ipdover.ado will need to use after taking back control from ipdmetan.ado) // 30th Jan 2018 // But we do need to parse on colon just in case cap _on_colon_parse `0' if !_rc { local 0 `"`s(before)'"' local after `" :`s(after)'"' } syntax [anything(everything)], OVER(string) [STUDY(string) BY(string) PREfix(name local) * ] if `"`study'"' != `""' { disp as err `"cannot specify {bf:study()} with {bf:ipdover}; please use {bf:over()} or the {bf:ipdmetan} command"' exit 198 } if `"`by'"' != `""' { disp as err `"cannot specify {bf:by()} with {bf:ipdover}; please use {bf:over()} or the {bf:ipdmetan} command"' exit 198 } local options2 `"`options'"' local 0 `over' syntax varlist [, Missing] local over1 `varlist' local overlen : word count `over1' local missing1 `missing' * Parse "over" options and map to study() and by() as appropriate * (N.B. "over" as a named option is NOT passed to ipdmetan!) local 0 `", `options2'"' syntax [, OVER(string) * ] local options2 `"`options'"' local 0 `over' syntax [varlist(default=none)] [, Missing] local over2 `varlist' local missing2 `missing' local 0 `", `options2'"' syntax [, OVER(string) * ] if `"`over'"'!=`""' { di as error `"may not specify more than two {bf:over()} options"' exit 198 } local nv2 : word count `over2' // no. of vars in `over2' if `nv2' > 1 { disp as err `"cannot specify multiple vars to second {bf:over()} option"' exit 198 } local study `over1' if `"`missing1'"'!=`""' local study `"`over1', `missing1'"' local by `over2' if `"`missing2'"'!=`""' local by `"`over2', `missing2'"' ** Now run ipdmetan // with "ipdover" option; this is a marker that data should not be pooled (i.e. is not a MA) // ipdmetan uses -preserve- before modifying data // declare up to 6 tempvars to be used if "`cmdstruc'"=="specific" // ...and up to 4 more if logrank (note that we don't know if either of these is true yet!!) tempvar tv1 tv2 tv3 tv4 tv5 tv6 tv7 tv8 tv9 tv10 local outvlist `tv1' `tv2' `tv3' `tv4' `tv5' `tv6' local lrvlist `tv7' `tv8' `tv9' `tv10' tempfile ipdfile labfile cap nois ipdmetan `anything', study(`study') by(`by') prefix(`prefix') /// ipdover(ipdfile(`ipdfile') labfile(`labfile') outvlist(`outvlist') lrvlist(`lrvlist')) `options' `after' // 30th Jan 2018 if _rc { if `"`err'"'==`""' { if _rc==1 nois disp as err `"User break in {bf:ipdmetan}"' nois disp as err `"Error in {bf:ipdmetan}"' } exit _rc } // re-load dataset created within ipdmetan preserve qui use `ipdfile', clear // 22nd May 2018: need to collect `counts' here; modified by ipdmetan // collect "universal" returned statistics local cmdstruc `r(cmdstruc)' local effect = cond(`"`r(effect)'"'!="", `"`r(effect)'"', "Effect") local eform `r(eform)' local citype `r(citype)' local lcols `"`r(lcols)'"' local rcols `"`r(rcols)'"' local wt `r(wt)' // returned separately from ipdmetan.ado rather than sending straight to forestplot local nonbeta `r(nonbeta)' // Added Mar 2022 local opts_fplot `"`r(opts_fplot)'"' return local citype `citype' return scalar n = r(n) local totnpts = r(n) // collect returned statistics specific to particular `cmdstruc' if "`cmdstruc'"=="generic" { local estexp `r(estexp)' local invlist `prefix'_ES `prefix'_seES return local estexp `"`r(estexp)'"' return local command `"`r(finalcmd)'"' return local cmdname `"`r(cmdname)'"' } else { local lrvlist `r(lrvlist)' local invlist `r(invlist)' local summstat `r(summstat)' local log `r(log)' local usummstat = upper("`summstat'") return local measure `"`log'`usummstat'"' } // Collect variable labels forvalues h=1/`overlen' { local varlab`h' `"`r(varlab`h')'"' } // Now parse options (either originally specified to -ipdover-, or returned by -ipdmetan- local 0 `", `r(options)'"' syntax [, noOVerall noSUbgroup SUMMARYONLY noTABle KEEPAll KEEPOrder /// EFFIcacy DF(varname numeric) LEVEL(passthru) SAVING(string) noGRaph CLEAR CLEARSTACK * ] local options_ipdm `"`macval(options)'"' // July 2021: // Note: -clearstack- is undocumented; it is shorthand for "clear" + "saving(, stacklabel)", but without the actual saving if `"`clearstack'"'!=`""' local clear clear // vaccine efficacy: OR and RR only if `"`efficacy'"'!=`""' { cap assert inlist("`summstat'", "or", "rr") if _rc { nois disp as err "Vaccine efficacy statistics only possible with odds ratios and risk ratios" exit _rc } } // use locals to avoid referring to `prefix' going forward // and thereby simplify code [July 2022] local outvlist `prefix'_ES `prefix'_seES `prefix'_LCI `prefix'_UCI `prefix'_NN // permanent vars, not tempvars tokenize `outvlist' args _ES _seES _LCI _UCI _NN local _USE `prefix'_USE // markers of existence of _BY and _OVER // N.B. only _LEVEL is guaranteed to exist. // _OVER will only exist if `overlen'>1 (i.e. if there is a need to distinguish) local _BY cap confirm var `prefix'_BY if !_rc local _BY `prefix'_BY local _OVER cap confirm var `prefix'_OVER if !_rc local _OVER `prefix'_OVER local _LEVEL `prefix'_LEVEL local _LABELS `prefix'_LABELS cap confirm numeric var `prefix'_WT if !_rc local _WT `prefix'_WT ** If raw data, more processing is required to obtain _ES, _seES, _LCI and _UCI // (processes otherwise done by admetan.ado) if "`cmdstruc'"=="specific" { // Amended 22nd May 2018 // cc-checking code (and anything else here) moved to GenEffectVars // (N.B. contents of `counts' option should have been modified by ipdmetan: // `group1' etc. removed and made into a "string asis" option, albeit possibly empty) // Parse options for "specific" effect measure syntax (Syntax 2), required later on local 0 `", `options_ipdm'"' syntax [, CC(passthru) noCC2 COUNTS(string asis) LOGRank OEV * ] local options_ipdm `"`macval(options)'"' cap nois GenEffectVars `_USE' `invlist', outvlist(`outvlist') /// summstat(`summstat') `logrank' `cc' `cc2' `level' `options_ipdm' if _rc { if `"`err'"'==`""' { if _rc==1 nois disp as err `"User break in {bf:ipdover.GenEffectVars}"' else nois disp as err `"Error in {bf:ipdover.GenEffectVars}"' } exit _rc } // We now have _ES and _seES defined throughout. // Identify excluded studies qui replace `_USE'=2 if `_USE'==1 & missing(`_ES', `_seES') } // end if "`cmdstruc'"=="specific" // Jan 2018 // Now there should not be any options left un-parsed // But to get syntax to give usual error messages, there needs to be at least one option specified! // ==> use cmdstruc(), since we know that is always defined local 0 `", cmdstruc(`cmdstruc') `options_ipdm'"' syntax , CMDSTRUC(string) // Create confidence limit variables if necessary cap confirm numeric variable `_LCI' if _rc==7 { disp as err `"variable {bf:`prefix'_LCI} exists and is string"' exit _rc } else if _rc qui gen double `_LCI' = . cap confirm numeric variable `_UCI' if _rc==7 { disp as err `"variable {bf:`prefix'_UCI} exists and is string"' exit _rc } else if _rc qui gen double `_UCI' = . // Generate confidence limit values if necessary // v2.1: ifstmt completely recoded to avoid need for "assert" within GenConfInts // (N.B. only one of _LCI, _UCI needs to be missing for GenConfInts to be run) cap nois GenConfInts `invlist' if inlist(`_USE', 1, 3, 5) /// & !missing(`_ES', `_seES') & missing(`_LCI', `_UCI'), /// citype(`citype') df(`df') `level' outvlist(`outvlist') if _rc { if `"`err'"'==`""' { if _rc==1 nois disp as err `"User break in {bf:ipdover.GenConfInts}"' else nois disp as err `"Error in {bf:ipdover.GenConfInts}"' } exit _rc } local level = r(level) // MOVED FROM WITHIN PREVIOUS IFSTMT 27th June 2017 // Next, identify excluded studies, and remove them if appropriate, and check that at least one valid estimate remains // (N.B. otherwise, identify them by "_USE==2") if `"`keeporder'"'!=`""' local keepall keepall // `keeporder' implies `keepall' if `"`keepall'"'==`""' { qui drop if `_USE'==2 summ `_ES', meanonly if !`r(N)' error 2000 } // otherwise, maintain original order if requested if `"`keeporder'"'!=`""' { tempvar tempuse qui gen byte `tempuse' = `_USE' qui replace `tempuse' = 1 if `_USE'==2 // keep "insufficient data" studies in original study order (default is to move to end) } else local tempuse `_USE' ** Finish off: return stats & matrices; print to screen; saving/forestplot // need to sort before forming matrix // missing values in _BY may cause problems, so need to be careful! summ `_USE', meanonly if r(max)==5 { tempvar use5 qui gen byte `use5' = (`_USE'==5) // marker of _USE==5 to sort on *before* _BY (to get around the issue of missing _BY values) } local notuse5 = cond("`use5'"=="", "", `"*(!`use5')"') // return matrix of coefficients tempname coeffs sort `use5' `_BY' `_OVER' `tempuse' `_LEVEL' mkmat `_OVER' `_BY' `_LEVEL' `_ES' `_seES' `_WT' `_NN' if inlist(`_USE', 1, 2), matrix(`coeffs') return matrix coeffs = `coeffs' ******************************************** * Print summary info and results to screen * ******************************************** * Print number of studies/patients to screen // (NB nos. actually analysed as opposed to the number supplied in original data) disp _n _c if !missing(`totnpts') local dispnpts = string(`totnpts') else { local dispnpts "Unknown" if "`overall'"!="" local textf " (overall estimation not run)" } local dispnpts = cond(missing(`totnpts'), "Unknown", string(`totnpts')) disp as text "Participants included: " as res "`dispnpts'" as text "`textf'" * Full descriptions of `summstat', `method' and `re_model' options, for printing to screen // Build up description of effect estimate type if "`cmdstruc'"=="generic" { if `"`exp_list'"'!=`""' local disptxt "Trial subgroup analysis of user-specified effect estimate" else local disptxt "Trial subgroup analysis of main (treatment) effect estimate" di _n as text "`disptxt'" as res " `estexp'" } else if `"`summstat'"'!=`""' { local logtext = cond(`"`log'"'!=`""', `"`log' "', `""') // add a space if `log' if "`summstat'"=="rr" local efftext "`logtext'Risk Ratios" else if "`summstat'"=="irr" local efftext "`logtext'Incidence Rate Ratios" else if "`summstat'"=="rrr" local efftext "`logtext'Relative Risk Ratios" else if "`summstat'"=="or" local efftext "`logtext'Odds Ratios" else if "`summstat'"=="rd" local efftext " Risk Differences" else if "`summstat'"=="hr" local efftext "`logtext'Hazard Ratios" else if "`summstat'"=="shr" local efftext "`logtext'Sub-hazard Ratios" else if "`summstat'"=="tr" local efftext "`logtext'Time Ratios" else if "`summstat'"=="wmd" local efftext " Weighted Mean Differences" else if "`summstat'"=="smd" { local efftext " Standardised Mean Differences" if "`method'"=="cohen" local efftextf `" as text " by the method of " as res "Cohen""' else if "`method'"=="glass" local efftextf `" as text " by the method of " as res "Glass""' else if "`method'"=="hedges" local efftextf `" as text " by the method of " as res "Hedges""' } // Study-level effect derivation method if "`logrank'"!="" local efftext "Peto (logrank) `efftext'" else if "`method'"=="peto" local efftext "Peto `efftext'" di _n as text "Trial subgroup analysis of" as res " `efftext'" `efftextf' } ** Table of results if `"`table'"'==`""' { // find maximum length of labels in LHS column tempvar vlablen qui gen long `vlablen' = length(`_LABELS') if `"`_BY'"'!=`""' { tempvar bylabels cap decode `_BY' if inlist(`_USE', 1, 2), gen(`bylabels') // if value label if _rc qui gen `bylabels' = string(`_BY') if inlist(`_USE', 1, 2) // if no value label qui replace `vlablen' = max(`vlablen', length(`bylabels')) drop `bylabels' } summ `vlablen', meanonly local lablen=r(max) drop `vlablen' forvalues h=1/`overlen' { local varlabopt `"`varlabopt' varlab`h'(`"`varlab`h''"')"' local len = length(`"`varlab`h''"') if `len'>`lablen' local lablen=`len' } local stitle = cond(`overlen'>1, "Subgroup", `"`varlab1'"') if `"`_BY'"'!=`""' { local byvarlab : variable label `_BY' local byvarlab = cond(`"`byvarlab'"'!=`""', `"`byvarlab'"', `"`over2'"') local stitle `"`byvarlab' and `stitle'"' } } cap nois DrawTableIPD, overlen(`overlen') lablen(`lablen') stitle(`stitle') etitle(`effect') prefix(`prefix') /// `eform' `varlabopt' `table' `overall' `subgroup' if _rc { if `"`err'"'==`""' { if _rc==1 nois disp as err `"User break in {bf:ipdover.DrawTableIPD}"' else nois disp as err `"Error in {bf:ipdover.DrawTableIPD}"' } exit _rc } ****************************************** * Prepare dataset for graphics or saving * ****************************************** if `"`saving'"'!=`""' | `"`clear'"'!=`""' | `"`graph'"'==`""' { quietly { if `"`saving'"'!=`""' { // Parse `saving' option first // use modified version of _prefix_saving.ado to handle `stacklabel' option my_prefix_savingIPD `saving' local saving `"`s(filename)'"' local 0 `", `s(options)'"' syntax [, STACKlabel * ] local saveopts `"`options'"' } if `"`clearstack'"'!=`""' local stacklabel stacklabel // July 2021; see explanation above // summaryonly (added Sep 2017 for v2.1) if `"`summaryonly'"'!=`""' qui drop if inlist(`_USE', 1, 2) // variable name (titles) for "_LABELS" and "_NN" local labtitle = cond(`"`summaryonly'"'!=`""' & `"`_BY'"'!=`""', `"`byvarlab'"', `"`stitle'"') if `"`stacklabel'"'==`""' label variable `_LABELS' `"`labtitle'"' else label variable `_LABELS' // no title if `stacklabel' if `"`: variable label `_NN''"'==`""' label variable `_NN' "No. pts" tempvar strlen gen `strlen' = length(string(`_NN')) summ `strlen', meanonly local fmtlen = max(`r(max)', 3) // min of 3, otherwise title ("No. pts") won't fit format `_NN' %`fmtlen'.0f // right-justified; fixed format (for integers) drop `strlen' ** Counts and OE/V if `"`counts'"'!=`""' { // Titles local title1 = cond(`"`group2'"'!=`""', `"`group2'"', `"Treatment"') local title0 = cond(`"`group1'"'!=`""', `"`group1'"', `"Control"') tokenize `invlist' local params : word count `invlist' // Binary data & logrank HR if inlist(`params', 2, 4) { if `params'==4 { args e1 f1 e0 f0 tempvar n1 n0 qui gen long `n1' = `e1' + `f1' qui gen long `n0' = `e0' + `f0' } else { tokenize `lrvlist' args n1 n0 e1 e0 } qui gen str `prefix'_counts1 = string(`e1') + "/" + string(`n1') if inlist(`_USE', 1, 2, 3, 5) qui gen str `prefix'_counts0 = string(`e0') + "/" + string(`n0') if inlist(`_USE', 1, 2, 3, 5) label variable `prefix'_counts1 `"`title1' n/N"' label variable `prefix'_counts0 `"`title0' n/N"' local countsvl `prefix'_counts1 `prefix'_counts0 } // N mean SD for continuous data // counts = "N, mean (SD) in research arm; N, mean (SD) events/total in control arm" else { args n1 mean1 sd1 n0 mean0 sd0 qui gen long `prefix'_counts1 = `n1' if inlist(`_USE', 1, 2, 3, 5) qui gen str `prefix'_counts1msd = string(`mean1', "%7.2f") + " (" + string(`sd1', "%7.2f") + ")" if inlist(`_USE', 1, 2, 3, 5) label variable `prefix'_counts1 "N" label variable `prefix'_counts1msd `"`title1' Mean (SD)"' qui gen long `prefix'_counts0 = `n0' if inlist(`_USE', 1, 2, 3, 5) qui gen str `prefix'_counts0msd = string(`mean0', "%7.2f") + " (" + string(`sd0', "%7.2f") + ")" if inlist(`_USE', 1, 2, 3, 5) label variable `prefix'_counts0 "N" label variable `prefix'_counts0msd `"`title0' Mean (SD)"' local countsvl `prefix'_counts1 `prefix'_counts1msd `prefix'_counts0 `prefix'_counts0msd // Find max number of digits in `_counts1', `_counts0' summ `prefix'_counts1, meanonly if r(N) { local fmtlen = floor(log10(`r(max)')) format `prefix'_counts1 %`fmtlen'.0f } summ `prefix'_counts0, meanonly if r(N) { local fmtlen = floor(log10(`r(max)')) format `prefix'_counts0 %`fmtlen'.0f } } compress `countsvl' local wt nowt // turn off display of weights (i.e npts) if counts } // end if `"`counts'"'!=`""' if `"`oev'"'!=`""' { if "`logrank'"=="" { disp as err `"Note: {bf:oev} is not applicable without log-rank data and will be ignored"' local oev } else { tokenize `invlist' args _OE _V label variable `_OE' `"O-E(o)"' label variable `_V' `"V(o)"' format `_OE' %6.2f format `_V' %6.2f if `"`saving'"'!=`""' | `"`clear'"'!=`""' { qui rename `_OE' `prefix'_OE qui rename `_V' `prefix'_V local _OE `prefix'_OE local _V `prefix'_V } } } // end if "`oev'"!="" // Oct 2018: // Now temporarily multiply _USE by 10 // to enable intermediate numberings for sorting the extra rows qui replace `_USE' = `_USE' * 10 ** Insert extra rows for headings, labels, spacings etc. // Note: in the following routines, "half" values of _USE are used temporarily to get correct order // and are then replaced with whole numbers at the end if trim(`"`_BY'`_OVER'"') != `""' { tempvar expand * Subgroup headings (_USE==0) and spacings (_USE==4) for "over" (i.e. `overlen'>1) if `"`summaryonly'"'==`""' { bysort `_BY' `_OVER' : gen byte `expand' = 1 + 2*(_n==1)`notuse5' expand `expand' replace `expand' = !(`expand' - 1) // `expand' is now 0 if expanded and 1 otherwise sort `_BY' `_OVER' `expand' `_USE' `_LEVEL' by `_BY' `_OVER' : replace `_USE' = 0 if !`expand' & _n==2 // row for headings by `_BY' `_OVER' : replace `_USE' = 45 if !`expand' & _n==3 // row for blank line if `"`_OVER'"'!=`""' { drop if `_USE'==0 & missing(`_OVER') // ...but not needed for missing _over } drop `expand' // Extra subgroup headings if both "by" *and* "over" if `"`_BY'"'!=`""' & `"`_OVER'"'!=`""' { bysort `_BY' : gen byte `expand' = 1 + 3*(_n==1)`notuse5' expand `expand' replace `expand' = !(`expand' - 1) // `expand' is now 0 if expanded and 1 otherwise sort `_BY' `expand' `_USE' `_LEVEL' by `_BY' : replace `_USE' = -10 if !`expand' & _n==2 // row for "by" label (title) by `_BY' : replace `_USE' = -5 if !`expand' & _n==3 // row for blank line below title by `_BY' : replace `_USE' = 41 if !`expand' & _n==4 // row for blank line to separate "by" groups drop if `_USE'==41 & missing(`_OVER') // ...but not needed for missing _OVER replace `_OVER'=. if `_USE'==41 drop `expand' } } else { summ `_BY', meanonly bysort `_BY' `_OVER' : gen byte `expand' = 1 + (`_BY'==`r(max)')*(_n==_N)`notuse5' expand `expand' replace `expand' = !(`expand' - 1) // `expand' is now 0 if expanded and 1 otherwise sort `_BY' `_OVER' `expand' `_USE' `_LEVEL' by `_BY' `_OVER' : replace `_USE' = 45 if !`expand' & _n==2 // row for blank line drop `expand' } } * Blank out effect sizes etc. in `expand'-ed rows foreach x of varlist `_LABELS' `_ES' `_seES' `_LCI' `_UCI' `_WT' `_NN' `lcols' `rcols' `countsvl' `_OE' `_V' { cap confirm numeric var `x' if !_rc replace `x' = . if !inlist(`_USE', 10, 20, 30, 50) else replace `x' = "" if !inlist(`_USE', 10, 20, 30, 50) } replace `_LEVEL' = . if !inlist(`_USE', 10, 20) ** Now insert label info into new rows // over() labels if "`_OVER'"!="" { forvalues h=1/`overlen' { replace `_LABELS' = `"`varlab`h''"' if `_USE'==0 & `_OVER'==`h' label define `_OVER' `h' `"`varlab`h''"', add } label values `_OVER' `_OVER' } // extra row to contain what would otherwise be the leftmost column heading if `stacklabel' specified // (i.e. so that heading can be used for forestplot stacking) else if `"`stacklabel'"' != `""' { local newN = _N + 1 set obs `newN' replace `_USE' = -10 in `newN' if "`use5'"=="" { tempvar use5 // we need `use5' here, regardless of whether it's needed elsewhere gen byte `use5' = 0 } replace `use5' = -1 in `newN' replace `_LABELS' = `"`labtitle'"' in `newN' } // "overall" labels if `"`overall'"'==`""' { replace `_LABELS' = "Overall" if `_USE'==50 } // subgroup ("by") headings & labels if `"`_BY'"'!=`""' { qui levelsof `_BY' if `_USE'!=50, missing local(bylist) // _USE!=5 since that will always be missing local bylab : value label `_BY' foreach byi of local bylist { // headings local bylabi : label `bylab' `byi' if `"`summaryonly'"'==`""' { if `"`_OVER'"'!=`""' replace `_LABELS' = "`bylabi'" if `_USE' == -10 & `_BY'==`byi' else replace `_LABELS' = "`bylabi'" if `_USE' == 0 & `_BY'==`byi' } // labels if `"`subgroup'"'==`""' { local sglabel = cond(`"`summaryonly'"'!=`""', `"`bylabi'"', `"Subgroup"') replace `_LABELS' = "`sglabel'" if `_USE'==30 & `_BY'==`byi' } } } ** Sort, and tidy up if `"`keeporder'"'!=`""' { qui replace `tempuse' = `_USE' qui replace `tempuse' = 10 if `_USE'==20 // keep "insufficient data" studies in original study order (default is to move to end) } sort `use5' `_BY' `_OVER' `tempuse' `_LEVEL' cap drop `use5' if `"`keeporder'"'!=`""' qui drop `tempuse' replace `_USE' = 0 if `_USE' == -10 replace `_USE' = 60 if `_USE' == 41 replace `_USE' = 30 if `_USE' == 35 replace `_USE' = 50 if `_USE' == 55 replace `_USE' = 40 if inlist(`_USE', -5, 25, 45, 55) replace `_USE' = `_USE' / 10 // Insert vaccine efficacy if `"`efficacy'"'!=`""' { qui gen `prefix'_VE = string(100*(1 - exp(`_ES')), "%4.0f") + " (" /// + string(100*(1 - exp(`_LCI')), "%4.0f") + ", " /// + string(100*(1 - exp(`_UCI')), "%4.0f") + ")" if inlist(`_USE', 1, 3, 5) label variable `prefix'_VE "Vaccine efficacy (%)" qui gen `strlen' = length(`prefix'_VE) summ `strlen', meanonly format %`r(max)'s `prefix'_VE qui compress `prefix'_VE drop `strlen' local _VE `prefix'_VE } // having added "overall", het. info etc., re-format _LABELS using study names only gen `strlen' = length(`_LABELS') if `"`summaryonly'"'==`""' { summ `strlen' if inlist(`_USE', 1, 2), meanonly } else summ `strlen', meanonly // alternative if no study estimates (`summaryonly' should be the only reason for this) format `_LABELS' %-`r(max)'s // left-justified; length equal to longest study name drop `strlen' // Oct 2018: Create _EFFECT (was `estText') here rather than in -forestplot- // so that e.g. user can insert p-values into results set if `"`saving'"'!=`""' | `"`clear'"'!=`""' { // need to peek into `opts_fplot' to extract `dp' local 0 `", `opts_fplot'"' syntax [, DP(integer 2) * ] if `"`eform'"'!=`""' local xexp exp summ `_UCI', meanonly local fmtx = max(1, ceil(log10(abs(`xexp'(r(max)))))) + 1 + `dp' local _EFFECT `prefix'_EFFECT gen str `_EFFECT' = string(`xexp'(`_ES'), `"%`fmtx'.`dp'f"') if !missing(`_ES') replace `_EFFECT' = `_EFFECT' + " " if !missing(`_EFFECT') replace `_EFFECT' = `_EFFECT' + "(" + string(`xexp'(`_LCI'), `"%`fmtx'.`dp'f"') + ", " + string(`xexp'(`_UCI'), `"%`fmtx'.`dp'f"') + ")" replace `_EFFECT' = `""' if !inlist(`_USE', 1, 3, 5) replace `_EFFECT' = "(Insufficient data)" if `_USE' == 2 local f: format `_EFFECT' tokenize `"`f'"', parse("%s") confirm number `2' format `_EFFECT' %-`2's // left-justify label variable `_EFFECT' `"`effect' (`level'% CI)"' } } // end quietly ** Save _dta characteristic containing all the options passed to -forestplot- // so that they may be called automatically using "forestplot, useopts" // (N.B. _USE, _LABELS and _NN should always exist) // Modified Mar 2022 if `"`nonbeta'"'==`""' local wgtvar `_NN' if `"`_WT'"'!=`""' local wgtvar `_WT' // user-defined weights, if supplied if `"`wgtvar'"'!=`""' { // -ifstmt- is in case of `nonbeta' (see -ipdmetan- ) local wgtopt wgt(`wgtvar') if `"`wgtvar'"'==`"`_NN'"' local _NNopt `_NN' // include _NN in lcols if wgtvar, but *don't* include user-spec _WT in rcols } // end of modified section local useopts `"use(`_USE') labels(`_LABELS') `wgtopt' nowt `eform' effect(`effect') `keepall' `opts_fplot'"' if `"`_BY'"'!=`""' local useopts `"`macval(useopts)' by(`_BY')"' if trim(`"`lcols' `_NNopt' `countsvl' `_OE' `_V'"')!=`""' { local useopts `"`macval(useopts)' lcols(`lcols' `_NNopt' `countsvl' `_OE' `_V')"' } if trim(`"`_VE' `_WT' `rcols'"')!=`""' { local useopts `"`macval(useopts)' rcols(`_VE' `_WT' `rcols')"' } if `"`_WT'"'!=`""' local fpnote `"NOTE: Weights are user-defined"' else if `"`_NNopt'"'!=`""' local fpnote `"NOTE: Weighting is by sample size"' if `"`fpnote'"'!=`""' { local useopts = trim(itrim(`"`macval(useopts)' note(`fpnote')"')) } // Store data characteristics // June 2016: in future, could maybe add other warnings here, e.g. continuity correction?? // NOTE: Only relevant if `saving' (but setup anyway; no harm done) char define _dta[FPUseOpts] `"`useopts'"' char define _dta[FPUseVarlist] `_ES' `_LCI' `_UCI' ** Pass to forestplot if `"`graph'"'==`""' { tempvar touse qui gen byte `touse' = 1 // Modified Jan 30th 2018; `wgtopt' is defined earlier // N.B. if either `counts' or user-defined weights or no _NN, "nowt" is enforced. // N.B.2. wgt() specifies the weighting variable; nowt suppresses its *display* cap nois forestplot `_ES' `_LCI' `_UCI', `useopts' if _rc { if `"`err'"'==`""' { if _rc==1 nois disp as err `"User break in {bf:forestplot}"' else nois disp as err `"Error in {bf:forestplot}"' } exit _rc } return add } ** Finally, save dataset if `"`saving'"'!=`""' | `"`clear'"'!=`""' { keep `_USE' `_BY' `_OVER' `_LEVEL' `_LABELS' `_ES' `_seES' `_LCI' `_UCI' `_WT' `_NN' `_EFFECT' `lcols' `rcols' `countsvl' `_OE' `_V' `_VE' order `_USE' `_BY' `_OVER' `_LEVEL' `_LABELS' `_ES' `_seES' `_LCI' `_UCI' `_WT' `_NN' `_EFFECT' `lcols' `rcols' `countsvl' `_OE' `_V' `_VE' // Oct 2018: Variable labels if inlist("`summstat'", "or", "rr", "hr") { label variable `_ES' "Effect size (interval scale)" } else label variable `_ES' "Effect size" label variable `_seES' "Standard error of effect size" label variable `_LCI' "`level'% lower confidence limit" label variable `_UCI' "`level'% upper confidence limit" char define `_LCI'[Level] `level' char define `_UCI'[Level] `level' label variable `_NN' "No. pts" label data `"Results set created by ipdover"' qui compress if `"`saving'"'!=`""' { qui save `"`saving'"', `saveopts' } if `"`clear'"'!=`""' { restore, not } } } // end if `"`saving'"'!=`""' | `"`graph'"'==`""' end ******************************************** * Program to generate effect size variables // Based on ProcessInputVarlist and ProcessPoolingVarlist in admetan.ado // but this version is only for use with raw data in ipdover.ado. program define GenEffectVars, rclass syntax varlist(min=3 max=7 default=none), OUTVLIST(namelist min=5 max=8) /// [SUMMSTAT(string) noINTEGER CC(string) CORnfield LOGRank LEVEL(real 95) ZTOL(real 1e-6)] // unpack varlists tokenize `outvlist' args _ES _seES _LCI _UCI _NN gettoken _USE invlist : varlist tokenize `invlist' local params : word count `invlist' // generate effect size vars // Note [Oct 2015]: gen as tempvars for now (to accommodate inverse-variance) // but will be renamed to permanent variables later if appropriate // 2 or 3 vars inverse-variance, or logrank HR // (N.B. summstat may be missing for the former only) if `params' <= 3 { foreach opt in cc cornfield { cap assert `"``opt''"' == `""' if _rc { nois disp as err `"Note: Option {bf:`opt'} is not appropriate without 2x2 count data and will be ignored"' local switchoff `"`switchoff' `opt'"' } } if "`logrank'"!="" { args oe va qui replace `_USE' = 2 if `_USE'==1 & sqrt(`va') < `ztol' // insufficient data (`_USE'==2) qui gen double `_ES' = `oe'/`va' if inlist(`_USE', 1, 3, 5) // logHR qui gen double `_seES' = 1/sqrt(`va') if inlist(`_USE', 1, 3, 5) // selogHR } // Identify studies with insufficient data (`_USE'==2) else if "`3'"=="" { // input is ES + SE args _ES _seES qui replace `_USE' = 2 if `_USE'==1 & missing(`_ES', `_seES') qui replace `_USE' = 2 if `_USE'==1 & 1/`_seES' < `ztol' } else { // input is ES + CI args _ES _LCI _UCI qui replace `_USE' = 2 if `_USE'==1 & missing(`_LCI', `_UCI') qui replace `_USE' = 2 if `_USE'==1 & float(`_LCI')==float(`_UCI') cap assert `_UCI'>=`_ES' & `_ES'>=`_LCI' if `_USE'==1 if _rc { nois disp as err "Effect size and/or confidence interval limits invalid;" nois disp as err `"order should be {it:effect_size} {it:lower_limit} {it:upper_limit}"' exit _rc } // Need to generate _seES qui gen double `_seES' = (`_LCI' - `_UCI') / (2*invnormal(.5 + `level'/200)) if inlist(`_USE', 1, 3, 5) qui replace `_USE' = 2 if `_USE'==1 & float(`_seES')==0 } qui count if inlist(`_USE', 1, 3, 5) & !missing(`_ES', `_seES') if !r(N) exit 2000 } // setup for Peto OR method else if "`method'"=="peto" { assert `params' == 4 tempvar oe local a `e1' qui gen double `oe' = `a' - `ea' // N.B. `ea' was created earlier (as was `va') qui gen double `_ES' = `oe'/`va' if inlist(`_USE', 1, 3, 5) // logOR or logHR qui gen double `_seES' = 1/sqrt(`va') if inlist(`_USE', 1, 3, 5) // selogOR or selogHR if `"`cc'"'!=`""' { nois disp as err "Note: continuity correction is incompatible with Peto method and will be ignored" local switchoff `switchoff' cc } } // Binary outcome (OR, RR, RD) else if `params' == 4 { assert inlist("`summstat'", "or", "rr", "irr", "rrr", "rd") args e1 f1 e0 f0 // events & non-events in trt; events & non-events in control (a.k.a. a b c d) if "`integer'"=="" { cap { assert int(`e1')==`e1' assert int(`f1')==`f1' assert int(`e0')==`e0' assert int(`f0')==`f0' } if _rc { di as err "Non integer cell counts found" exit _rc } } cap assert `e1'>=0 & `f1'>=0 & `e0'>=0 & `f0'>=0 if _rc { di as err "Non-positive cell counts found" exit _rc } // Find studies with insufficient data (`_USE'==2) qui replace `_USE' = 2 if `_USE'==1 & missing(`e1', `f1', `e0', `f0') if "`summstat'"=="or" qui replace `_USE' = 2 if `_USE'==1 & (`e1' + `e0')*(`f1' + `f0')==0 if inlist("`summstat'", "rr", "irr", "rrr") | "`method'"=="peto" { qui replace `_USE' = 2 if `_USE'==1 & ((`e1'==0 & `e0'==0 ) | (`f1'==0 & `f0'==0)) } qui replace `_USE' = 2 if `_USE'==1 & (`e1' + `f1')*(`e0' + `f0')==0 // applies to all cases qui count if inlist(`_USE', 1, 3, 5) if !r(N) exit 2000 if "`cornfield'"!="" { if !inlist("`summstat'", "or", "") { nois disp as err "Note: {bf:cornfield} is only compatible with odds ratios; option will be ignored"' local switchoff `"`switchoff' cornfield"' } else if "`summstat'"=="" { nois disp as err `"Note: Cornfield-type confidence intervals specified; odds ratios assumed"' local summstat "or" } } if inlist("`method'", "cohen", "glass", "hedges", "nostandard") { nois disp as err `"Specified method {bf:`method'} is incompatible with the data"' exit 184 } if inlist("`summstat'", "hr", "shr", "tr") { nois disp as err "Time-to-event outcome types are incompatible with count data" exit 184 } if inlist("`summstat'", "wmd", "smd") { nois disp as err "Continuous outcome types are incompatible with count data" exit 184 } /* if "`summstat'"=="" { local summstat rr local effect `"Risk Ratio"' } */ assert "`summstat'"!="" // temp error trap 23rd March 2017 local method = cond("`method'"=="", "mh", "`method'") // default pooling method is Mantel-Haenszel // 27th March 2017 // tokenize `binvlist' // args r1 r0 tempvar r1 r0 local type = cond("`integer'"=="", "long", "double") qui gen `type' `r1' = `e1' + `f1' // total in trt arm qui gen `type' `r0' = `e0' + `f0' // total in control arm // qui gen `type' `_NN' = `r1' + `r0' // overall total // zero-cell adjustments: amended May 2018 local ccval = cond(`"`cc2'"'!=`""', 0, 0.5) // default if trim(`"`cc'`cc2'"') != `""' { if `"`cc'"'!=`""' { local 0 `"`cc'"' syntax [anything(id="value supplied to {bf:cc()}")] [, OPPosite EMPirical] if `"`anything'"'!=`""' { confirm number `anything' local ccval = `anything' } if `"`cc2'"'!=`""' & `ccval' != 0 { disp as err `"Cannot specify both {bf:cc()} and {bf:nocc}; please choose one or the other"' exit 198 } cap assert `ccval'>=0 & `ccval'<1 if _rc { nois disp as err "Invalid continuity correction: must be in range [0,1)" exit _rc } } } local cc = `ccval' tempvar zeros qui gen byte `zeros' = `e1'*`f1'*`e0'*`f0'==0 summ `zeros' if `_USE'==1, meanonly local nz = r(N) if `nz' & `cc' { // Sweeting's "opposite treatment arm" correction if `"`opposite'"'!=`""' { tempvar cc1 cc0 qui gen `cc1' = 2*`cc'*`r1'/(`r1' + `r0') qui gen `cc0' = 2*`cc'*`r0'/(`r1' + `r0') } // Empirical correction: not valid with ipdover else if `"`empirical'"'!=`""' { nois disp as err `"Empirical continuity correction not valid with {bf:ipdover}"' exit 198 } else { local cc1 = `cc' local cc0 = `cc' } tempvar e1_cont f1_cont e0_cont f0_cont t_cont qui gen double `e1_cont' = cond(`zeros', `e1' + `cc1', `e1') qui gen double `f1_cont' = cond(`zeros', `f1' + `cc1', `f1') qui gen double `e0_cont' = cond(`zeros', `e0' + `cc0', `e0') qui gen double `f0_cont' = cond(`zeros', `f0' + `cc0', `f0') tempvar r1_cont r0_cont t_cont qui gen double `r1_cont' = `e1_cont' + `f1_cont' qui gen double `r0_cont' = `e0_cont' + `f0_cont' qui gen double `t_cont' = `r1_cont' + `r0_cont' if `"`opposite'"' != `""' { drop `cc1' `cc0' // tidy up } } else { local e1_cont `e1' local f1_cont `f1' local e0_cont `e0' local f0_cont `f0' local r1_cont `r1' local r0_cont `r0' local t_cont `_NN' } // now branch by outcome measure if "`summstat'" == "or" { if "`method'"=="peto" { local a `e1' tempvar c1 c0 ae oe va qui gen `type' `c1' = `a' + `c' // total events qui gen `type' `c0' = `b' + `d' // total non-events qui gen double `ea' = (`r1'*`c1')/ `_NN' // expected events in trt arm qui gen double `va' = `r1'*`r0'*`c1'*`c0'/( `_NN'*`_NN'*(`_NN' - 1)) qui gen double `oe' = `a' - `ea' qui gen double `_ES' = `oe'/`va' if inlist(`_USE', 1, 3, 5) // logOR or logHR qui gen double `_seES' = 1/sqrt(`va') if inlist(`_USE', 1, 3, 5) // selogOR or selogHR } else { // calculate individual ORs and variances using cc-adjusted counts // (on the linear scale, i.e. logOR) qui gen double `_ES' = ln(`e1_cont'*`f0_cont') - ln(`f1_cont'*`e0_cont') if inlist(`_USE', 1, 3, 5) qui gen double `_seES' = sqrt(1/`e1_cont' + 1/`f1_cont' + 1/`e0_cont' + 1/`f0_cont') if inlist(`_USE', 1, 3, 5) } } /* end OR */ // setup for RR else if "`summstat'" == "rr" { tempvar r s v qui gen double `r' = `e1_cont'*`r0_cont' / `t_cont' qui gen double `s' = `e0_cont'*`r1_cont' / `t_cont' qui gen double `v' = 1/`e1_cont' + 1/`e0_cont' - 1/`r1_cont' - 1/`r0_cont' qui gen double `_ES' = ln(`r'/`s') if inlist(`_USE', 1, 3, 5) // logRR qui gen double `_seES' = sqrt(`v') if inlist(`_USE', 1, 3, 5) // selogRR } // setup for RD else if "`summstat'" == "rd" { tempvar v qui gen double `v' = `e1_cont'*`f1_cont'/(`r1_cont'^3) + `e0_cont'*`f0_cont'/(`r0_cont'^3) qui gen double `_ES' = `e1'/`r1' - `e0'/`r0' if inlist(`_USE', 1, 3, 5) qui gen double `_seES' = sqrt(`v') if inlist(`_USE', 1, 3, 5) } } /* end if `params' == 4 */ // N mean SD for continuous data else { assert `params' == 6 assert inlist("`summstat'", "wmd", "smd") args n1 mean1 sd1 n0 mean0 sd0 // input is form N mean SD for continuous data if "`integer'"=="" { cap assert int(`n1')==`n1' & int(`n0')==`n0' if _rc { nois disp as err "Non integer sample sizes found" exit _rc } } cap assert `n1'>0 & `n0'>0 if _rc { nois disp as err "Non positive sample sizes found" exit _rc } foreach opt in cc cornfield { cap assert `"``opt''"' == `""' if _rc { nois disp as err `"Option {bf:`opt'} is not appropriate without 2x2 count data and will be ignored"' local switchoff `"`switchoff' `opt'"' } } // Find studies with insufficient data (`_USE'==2) qui replace `_USE' = 2 if `_USE'==1 & missing(`n1', `mean1', `sd1', `n0', `mean0', `sd0') qui replace `_USE' = 2 if `_USE'==1 & `n1' < 2 | `n0' < 2 qui replace `_USE' = 2 if `_USE'==1 & `sd1'<=0 | `sd0'<=0 qui count if inlist(`_USE', 1, 3, 5) if !r(N) exit 2000 if "`method'"=="nostandard" & "`summstat'"=="smd" { nois disp as err `"Cannot specify both SMD and the {bf:nostandard} option"' exit 184 } if inlist("`method'", "cohen", "glass", "hedges") & "`summstat'"=="wmd" { nois disp as err `"Cannot specify both WMD and the {bf:`mdmethod'} option"' exit 184 } if inlist("`method'", "mh", "peto") | "`logrank'"!="" { nois disp as err `"Specified method {bf:`method'} is incompatible with the data"' exit 184 } cap assert inlist("`summstat'", "", "wmd", "smd") if _rc { nois disp as err "Invalid specifications for combining trials" exit 184 } /* if "`summstat'"=="" { if "`method'"=="nostandard" { // "nostandard" is a synonym for "wmd" local summstat "wmd" local effect `"WMD"' } else { local summstat "smd" // default is standardized mean differences... local effect `"SMD"' } } */ assert "`summstat'"!="" // temp error trap 23rd March 2017 local method = cond(inlist("`method'", "", "iv"), "cohen", "`method'") // ...by the method of Cohen // qui gen long `_NN' = `n1' + `n0' if inlist(`_USE', 1, 2, 3, 5) if "`summstat'" == "wmd" { qui gen double `_ES' = `mean1' - `mean0' if inlist(`_USE', 1, 3, 5) qui gen double `_seES' = sqrt((`sd1'^2)/`n1' + (`sd0'^2)/`n0') if inlist(`_USE', 1, 3, 5) } else { // summstat = SMD tempvar s qui gen double `s' = sqrt( ((`n1'-1)*(`sd1'^2) + (`n0'-1)*(`sd0'^2) )/( `_NN' - 2) ) if "`mdmethod'" == "cohen" { qui gen double `_ES' = (`mean1' - `mean0')/`s' if inlist(`_USE', 1, 3, 5) qui gen double `_seES' = sqrt((`_NN' /(`n1'*`n0')) + (`_ES'*`_ES'/ (2*(`_NN' - 2)) )) if inlist(`_USE', 1, 3, 5) } else if "`mdmethod'" == "glass" { qui gen double `_ES' = (`mean1' - `mean0')/`sd0' if inlist(`_USE', 1, 3, 5) qui gen double `_seES' = sqrt(( `_NN' /(`n1'*`n0')) + (`_ES'*`_ES'/ (2*(`n0' - 1)) )) if inlist(`_USE', 1, 3, 5) } else if "`mdmethod'" == "hedges" { qui gen double `_ES' = ((`mean1' - `mean0')*(1 - 3/(4*`_NN' - 9))/`s' if inlist(`_USE', 1, 3, 5) qui gen double `_seES' = sqrt(( `_NN' /(`n1'*`n0')) + (`_ES'*`_ES'/ (2*(`_NN' - 3.94)) )) if inlist(`_USE', 1, 3, 5) } drop `s' } } /* end else (i.e. if `params' == 6) */ end ********************************************************************* * Program to generate study-level confidence intervals // identical subroutine also used in admetan.ado program define GenConfInts, rclass syntax varlist(numeric min=2 max=6 default=none) [if] [in], CItype(string) /// OUTVLIST(varlist numeric) [ DF(varname numeric) LEVEL(real 95) ] marksample touse, novarlist // if no data to process, exit without error return scalar level = `level' qui count if `touse' if !r(N) exit // unpack varlists tokenize `outvlist' args _ES _seES _LCI _UCI _WT _NN local params : word count `varlist' // `varlist' == `invlist' // Confidence limits need calculating if: // - not supplied by user (i.e. `params'!=3); or // - desired coverage is not 95% if `params'==3 & `level'==95 exit * Calculate confidence limits for original study estimates using specified `citype' // (unless limits supplied by user) if "`citype'"=="normal" { // normal distribution - default tempname critval scalar `critval' = invnormal(.5 + `level'/200) qui replace `_LCI' = `_ES' - `critval'*`_seES' if `touse' qui replace `_UCI' = `_ES' + `critval'*`_seES' if `touse' } else if inlist("`citype'", "t", "logit") { // t or logit distribution cap confirm numeric variable `df' if !_rc { summ `df' if `touse', meanonly // use supplied df if available cap assert r(max) < . if _rc { nois disp as err `"Degrees-of-freedom variable {bf:`df'} contains missing values;"' nois disp as err `" cannot use {bf:`citype'}-based confidence intervals for study estimates"' exit 198 } } else { cap confirm numeric variable `_NN' if !_rc { summ `_NN' if `touse', meanonly // otherwise try using npts cap assert r(max) < . if _rc { nois disp as err `"Participant numbers not available for all studies;"' nois disp as err `" cannot use {bf:`citype'}-based confidence intervals for study estimates"' exit 198 } tempvar df qui gen `: type `_NN'' `df' = `_NN' - 2 // use npts-2 as df for t distribution of df not explicitly given local disperr `"nois disp as err `"Note: Degrees of freedom for {bf:`citype'}-based confidence intervals not supplied; using {it:n-2} as default"'"' // delay error message until after checking _ES is between 0 and 1 for logit } else { nois disp as err `"Neither degrees-of-freedom nor participant numbers available;"' nois disp as err `" cannot use {bf:`citype'}-based confidence intervals for study estimates"' exit 198 } } tempvar critval qui gen double `critval' = invttail(`df', .5 - `level'/200) if "`citype'"=="t" { qui replace `_LCI' = `_ES' - `critval'*`_seES' if `touse' qui replace `_UCI' = `_ES' + `critval'*`_seES' if `touse' } else { // logit, proportions only (for formula, see Stata manual for -proportion-) summ `_ES' if `touse', meanonly if r(min)<0 | r(max)>1 { nois disp as err "{bf:citype(logit)} may only be used with proportions" exit 198 } qui replace `_LCI' = invlogit(logit(`_ES') - `critval'*`_seES'/(`_ES'*(1 - `_ES'))) if `touse' qui replace `_UCI' = invlogit(logit(`_ES') + `critval'*`_seES'/(`_ES'*(1 - `_ES'))) if `touse' } } else if inlist("`citype'", "cornfield", "exact", "woolf") { // options to pass to -cci-; summstat==OR only tokenize `varlist' args a b c d // events & non-events in trt; events & non-events in control (c.f. -metan- help file) // sort appropriately, then find observation number of first relevant obs tempvar obs qui bysort `touse' : gen long `obs' = _n if `touse' // N.B. MetaAnalysisLoop uses -sortpreserve- sort `obs' // so this sorting should not affect the original data summ `obs' if `touse', meanonly forvalues j = 1/`r(max)' { qui cci `=`a'[`j']' `=`b'[`j']' `=`c'[`j']' `=`d'[`j']', `citype' level(`level') qui replace `_LCI' = log(`r(lb_or)') in `j' qui replace `_UCI' = log(`r(ub_or)') in `j' } } // Now display delayed error message if appropriate `disperr' end ********************************************************************* * Routine to draw output table (ipdover.ado version) * Could be done using "tabdisp", but doing it myself means it can be tailored to the situation * therefore looks better (I hope!) // DF Aug 2016: // N.B. Code taken directly from latest version of DrawTableAD (as used in admetan.ado), but then modified for use with -ipdover- // (e.g. "tests and heterogeneity" subroutine is removed, and other small additions/removals) program define DrawTableIPD syntax, OVERLEN(integer) /// [PREfix(name local) LABLEN(integer 0) STITLE(string asis) ETITLE(string asis) /// EFORM noTABle noOVerall noSUbgroup * ] tempvar obs qui gen long `obs'=_n sort `obs' // EXTRA LINES FOR USE WITH IPDOVER cap confirm var `prefix'_BY if !_rc local _BY `prefix'_BY cap confirm var `prefix'_NN if !_rc local _NN `prefix'_NN cap confirm var `prefix'_OVER if !_rc local _OVER `prefix'_OVER foreach x in _USE _LABELS _ES _LCI _UCI { local `x' `prefix'`x' } local swidth = 25 // define `swidth' in case noTAB if `"`table'"'==`""' { // EXTRA LINES FOR USE WITH IPDOVER if `overlen'>1 { // if "over", parse "variable label" options forvalues h=1/`overlen' { local 0 `", `options'"' syntax, VARLAB`h'(string) * } } * Find maximum length of study title and effect title * Allow them to spread over several lines, but only up to a maximum number of chars * If a single line must be more than 32 chars, truncate and stop local uselen = 25 // default (minimum); max is 32 if `lablen'>21 local uselen=min(`lablen', 32) cap nois SpreadTitle `"`stitle'"', target(`uselen') maxwidth(32) // study (+ subgroup) title if _rc { if _rc==1 nois disp as err `"User break in {bf:ipdover.SpreadTitle}"' nois disp as err `"Error in {bf:ipdover.SpreadTitle}"' c_local err "noerr" // tell ipdover not to also report an "error in {bf:ipdover.DrawTableIPD}" exit _rc } local swidth = max(`uselen', `r(maxwidth)') local slines = r(nlines) forvalues i=1/`slines' { local stitle`i' `"`r(title`i')'"' } cap nois SpreadTitle `"`etitle'"', target(10) maxwidth(15) // effect title (i.e. "Odds ratio" etc.) if _rc { if _rc==1 nois disp as err `"User break in {bf:ipdover.SpreadTitle}"' nois disp as err `"Error in {bf:ipdover.SpreadTitle}"' c_local err "noerr" // tell ipdover not to also report an "error in {bf:ipdover.DrawTableIPD}" exit _rc } local ewidth = max(10, `r(maxwidth)') local elines = r(nlines) local diff = `elines' - `slines' if `diff'<=0 { forvalues i=1/`slines' { local etitle`i' `"`r(title`=`i'+`diff'')'"' // if stitle uses more (or equal) lines ==> line up etitle with stitle } } else { forvalues i=`elines'(-1)1 { // run backwards, otherwise macros are deleted by the time they're needed local etitle`i' `"`r(title`i')'"' local stitle`i' = cond(`i'>=`diff', `"`stitle`=`i'-`diff'''"', `""') // if etitle uses more lines ==> line up stitle with etitle } } * Now display the title lines, starting with the "extra" lines and ending with the row including CI & weight di as text _n `"{hline `swidth'}{c TT}{hline `=`ewidth'+35'}"' local nl = max(`elines', `slines') if `nl' > 1 { forvalues i=1/`=`nl'-1' { di as text `"`stitle`i''{col `=`swidth'+1'}{c |} "' %~`ewidth's `"`etitle`i''"' } } di as text `"`stitle`nl''{col `=`swidth'+1'}{c |} "' %~10s `"`etitle`nl''"' `"{col `=`swidth'+`ewidth'+4'}[`c(level)'% Conf. Interval]{col `=`swidth'+`ewidth'+27'}No. pts"' *** Loop over studies, and subgroups if appropriate if `"`_BY'"'!=`""' { qui levelsof `_BY' if `_USE'!=5, missing local(bylist) // _USE!=5 since that will always be missing! local bylab : value label `_BY' } local nby = max(1, `: word count `bylist'') tempvar touse touse2 qui gen byte `touse' = 1 tempname _ES_ _LCI_ _UCI_ local xexp = cond("`eform'"!=`""', `"exp"', `""') forvalues i=1/`nby' { // this will be 1/1 if no subgroups di as text `"{hline `swidth'}{c +}{hline `=`ewidth'+35'}"' if `"`_BY'"'!=`""' { local byi : word `i' of `bylist' qui replace `touse' = (`_BY'==`byi') summ `_ES' if `touse' & `_USE'==1, meanonly if !r(N) local nodata `"{col `=`swidth'+4'} (No subgroup data)"' else local K`i' = r(N) if `"`bylab'"'!=`""' { local bylabi : label `bylab' `byi' } else local bylabi `"`byi'"' di as text substr(`"`bylabi'"', 1, `swidth'-1) + `"{col `=`swidth'+1'}{c |}`nodata'"' local nodata // clear macro } // EXTRA LINES FOR USE WITH IPDOVER forvalues h=1/`overlen' { gen byte `touse2' = `touse' if `"`_OVER'"'!=`""' { qui replace `touse2' = `touse' * (`_OVER'==`h') } summ `obs' if `touse2' & inlist(`_USE', 1, 2), meanonly // EXTRA LINES FOR USE WITH IPDOVER if `overlen'>1 { di as text `"{col `=`swidth'+1'}{c |}"' if !r(N) local nodata `"{col `=`swidth'+4'} (Insufficient data)"' di as text substr(`"`varlab`h''"', 1, `=`swidth'-1') `"{col `=`swidth'+1'}{c |}`nodata'"' local nodata // clear macro } if r(N) { forvalues k = `r(min)' / `r(max)' { local _labels_ = `_LABELS'[`k'] summ `obs' if `touse2' & `_USE'==1 & _n==`k', meanonly // 30th March 2017: can this be improved/made more efficient? if !r(N) { di as text substr(`"`_labels_'"', 1, 32) `"{col `=`swidth'+1'}{c |}{col `=`swidth'+4'} (Insufficient data)"' } else { scalar `_ES_' = `_ES'[`k'] scalar `_LCI_' = `_LCI'[`k'] scalar `_UCI_' = `_UCI'[`k'] local _labels_ = `_LABELS'[`k'] di as text substr(`"`_labels_'"', 1, 32) `"{col `=`swidth'+1'}{c |}{col `=`swidth'+`ewidth'-6'}"' /// as res %7.3f `xexp'(`_ES_') `"{col `=`swidth'+`ewidth'+5'}"' /// as res %7.3f `xexp'(`_LCI_') `"{col `=`swidth'+`ewidth'+15'}"' /// as res %7.3f `xexp'(`_UCI_') `"{col `=`swidth'+`ewidth'+26'}"' %7.0f `_NN'[`k'] } } } drop `touse2' } // end forvalues j=1/`overlen' * Subgroup effects if `"`_BY'"'!=`""' & `"`subgroup'"'==`""' { di as text `"{col `=`swidth'+1'}{c |}"' local byi: word `i' of `bylist' summ `obs' if `touse' & `_USE'==3, meanonly if !r(N) { di as text `"Effect in subset{col `=`swidth'+1'}{c |}{col `=`swidth'+4'} (Insufficient data)"' } else { scalar `_ES_' = `_ES'[`r(min)'] scalar `_LCI_' = `_LCI'[`r(min)'] scalar `_UCI_' = `_UCI'[`r(min)'] di as text `"Effect in subset{col `=`swidth'+1'}{c |}{col `=`swidth'+`ewidth'-6'}"' /// as res %7.3f `xexp'(`_ES_') `"{col `=`swidth'+`ewidth'+5'}"' /// as res %7.3f `xexp'(`_LCI_') `"{col `=`swidth'+`ewidth'+15'}"' /// as res %7.3f `xexp'(`_UCI_') `"{col `=`swidth'+`ewidth'+26'}"' %7.0f `_NN'[`r(min)'] } } } // end forvalues i=1/`nby' drop `touse' // tidy up *** Overall effect if `"`overall'"'==`""' { di as text `"{hline `swidth'}{c +}{hline `=`ewidth'+35'}"' summ `obs' if `_USE'==5, meanonly if !r(N) { di as text `"Overall effect{col `=`swidth'+1'}{c |}{col `=`swidth'+4'} (Insufficient data)"' } else { scalar `_ES_' = `_ES'[`r(min)'] scalar `_LCI_' = `_LCI'[`r(min)'] scalar `_UCI_' = `_UCI'[`r(min)'] di as text %-20s `"Overall effect{col `=`swidth'+1'}{c |}{col `=`swidth'+`ewidth'-6'}"' /// as res %7.3f `xexp'(`_ES_') `"{col `=`swidth'+`ewidth'+5'}"' /// as res %7.3f `xexp'(`_LCI_') `"{col `=`swidth'+`ewidth'+15'}"' /// as res %7.3f `xexp'(`_UCI_') `"{col `=`swidth'+`ewidth'+26'}"' %7.0f `_NN'[`r(min)'] } } di as text `"{hline `swidth'}{c BT}{hline `=`ewidth'+35'}"' } // end if `"`table'"'==`""' end * Subroutine to "spread" titles out over multiple lines if appropriate // Updated July 2014 // August 2016: identical program now used here, in forestplot.ado, and in admetan.ado // May 2017: updated to accept substrings delineated by quotes (c.f. multi-line axis titles) // August 2017: updated for better handling of maxlines() // March 2018: updated to receive text in quotes, hence both avoiding parsing problems with commas, and maintaining spacing // May 2018 and Nov 2018: updated truncation procedure // subroutine of DrawTableIPD program define SpreadTitle, rclass syntax [anything(name=title id="title string")] [, TArget(integer 0) MAXWidth(integer 0) MAXLines(integer 0) noTRUNCate noUPDATE ] * Target = aim for this width, but allow expansion if alternative is wrapping "too early" (i.e before line is adequately filled) // (may be replaced by `titlelen'/`maxlines' if `maxlines' and `notruncate' are also specified) * Maxwidth = absolute maximum width ... but will be increased if a "long" string is encountered before the last line * Maxlines = maximum no. lines (default 3) * noTruncate = don't truncate final line if "too long" (even if greater than `maxwidth') * noUpdate = don't update `target' if `maxwidth' is increased (see above) tokenize `title' if `"`1'"'==`""' { return scalar nlines = 0 return scalar maxwidth = 0 exit } if `maxwidth' & !`maxlines' { cap assert `maxwidth'>=`target' if _rc { nois disp as err `"{bf:maxwidth()} must be greater than or equal to {bf:target()}"' exit 198 } } ** Find length of title string, or maximum length of multi-line title string // First run: strip off outer quotes if necessary, but watch out for initial/final spaces! gettoken tok : title, qed(qed) cap assert `"`tok'"'==`"`1'"' if _rc { gettoken tok rest : tok, qed(qed) assert `"`tok'"'==`"`1'"' local title1 title1 // specifies that title is not multi-line } local currentlen = length(`"`1'"') local titlelen = length(`"`1'"') // Subsequent runs: successive calls to -gettoken-, monitoring quotes with the qed() option macro shift while `"`1'"'!=`""' { local oldqed = `qed' gettoken tok rest : rest, qed(qed) assert `"`tok'"'==`"`1'"' if !`oldqed' & !`qed' local currentlen = `currentlen' + 1 + length(`"`1'"') else { local titlelen = max(`titlelen', `currentlen') local currentlen = length(`"`1'"') } macro shift } local titlelen = max(`titlelen', `currentlen') // Save user-specified parameter values separately local target_orig = `target' local maxwidth_orig = `maxwidth' local maxlines_orig = `maxlines' // Now finalise `target' and calculate `spread' local maxlines = cond(`maxlines_orig', `maxlines_orig', 3) // use a default value for `maxlines' of 3 in these calculations local target = cond(`target_orig', `target_orig', /// cond(`maxwidth_orig', min(`maxwidth_orig', `titlelen'/`maxlines'), `titlelen'/`maxlines')) local spread = min(int(`titlelen'/`target') + 1, `maxlines') local crit = cond(`maxwidth_orig', min(`maxwidth_orig', `titlelen'/`spread'), `titlelen'/`spread') ** If substrings are present, delineated by quotes, treat this as a line-break // Hence, need to first process each substring separately and obtain parameters, // then select the most appropriate overall parameters given the user-specified options, // and finally create the final line-by-line output strings. tokenize `title' local line = 1 local title`line' : copy local 1 // i.e. `"`title`line''"'==`"`1'"' local newwidth = length(`"`title`line''"') // if first "word" is by itself longer than `maxwidth' ... if `maxwidth' & !(`maxlines' & (`line'==`maxlines')) { // ... reset parameters and start over while length(`"`1'"') > `maxwidth' { local maxwidth = length(`"`1'"') local target = cond(`target_orig', cond(`"`update'"'!=`""', `target_orig', `target_orig' + `maxwidth' - `maxwidth_orig'), /// cond(`maxwidth', min(`maxwidth', `titlelen'/`maxlines'), `titlelen'/`maxlines')) local spread = min(int(`titlelen'/`target') + 1, `maxlines') local crit = cond(`maxwidth', min(`maxwidth', `titlelen'/`spread'), `titlelen'/`spread') } } macro shift local next : copy local 1 // i.e. `"`next'"'==`"`1'"' (was `"`2'"' before macro shift!) while `"`1'"' != `""' { // local check = `"`title`line''"' + `" "' + `"`next'"' // (potential) next iteration of `title`line'' local check `"`title`line'' `next'"' // (amended Apr 2018 due to local x = "" issue with version <13) if length(`"`check'"') > `crit' { // if longer than ideal... // ...and further from target than before, or greater than maxwidth if abs(length(`"`check'"') - `crit') > abs(length(`"`title`line''"') - `crit') /// | (`maxwidth' & (length(`"`check'"') > `maxwidth')) { if `maxlines' & (`line'==`maxlines') { // if reached max no. of lines local title`line' : copy local check // - use next iteration anyway (to be truncated) macro shift local next : copy local 1 local newwidth = max(`newwidth', length(`"`title`line''"')) // update `newwidth' continue, break } else { // otherwise: local ++line // - new line // if first "word" of new line (i.e. `next') is by itself longer than `maxwidth' ... if `maxwidth' & (length(`"`next'"') > `maxwidth') { // ... if we're on the last line or last token, continue as normal ... if !((`maxlines' & (`line'==`maxlines')) | `"`2'"'==`""') { // ... but otherwise, reset parameters and start over local maxwidth = length(`"`next'"') local target = cond(`target_orig', cond(`"`update'"'!=`""', `target_orig', `target_orig' + `maxwidth' - `maxwidth_orig'), /// cond(`maxwidth', min(`maxwidth', `titlelen'/`maxlines'), `titlelen'/`maxlines')) local spread = min(int(`titlelen'/`target') + 1, `maxlines') local crit = cond(`maxwidth', min(`maxwidth', `titlelen'/`spread'), `titlelen'/`spread') // restart loop tokenize `title' local tok = 1 local line = 1 local title`line' : copy local 1 // i.e. `"`title`line''"'==`"`1'"' local newwidth = length(`"`title`line''"') macro shift local next : copy local 1 // i.e. `"`next'"'==`"`1'"' (was `"`2'"' before macro shift!) continue } } local title`line' : copy local next // - begin new line with next word } } else local title`line' : copy local check // else use next iteration } else local title`line' : copy local check // else use next iteration macro shift local next : copy local 1 local newwidth = max(`newwidth', length(`"`title`line''"')) // update `newwidth' } // (N.B. won't be done if reached max no. of lines, as loop broken) * Return strings forvalues i=1/`line' { // truncate if appropriate (last line only) if `i'==`line' & "`truncate'"=="" & `maxwidth' { local title`i' = substr(`"`title`i''"', 1, `maxwidth') } return local title`i' `"`title`i''"' } * Return values return scalar nlines = `line' return scalar maxwidth = min(`newwidth', `maxwidth') return scalar target = `target' end * Modified version of _prefix_saving.ado // [IPD version] modified so as to include `stacklabel' option // April 2018, for ipdover v2.2 program define my_prefix_savingIPD, sclass cap nois syntax anything(id="file name" name=fname) [, REPLACE * ] if !_rc { if "`replace'" == "" { local ss : subinstr local fname ".dta" "" confirm new file `"`ss'.dta"' } } else { di as err "invalid saving() option" exit _rc } sreturn clear sreturn local filename `"`fname'"' sreturn local options `"`replace' `options'"' end ************************************************************** * Release notes prior to v4.00 * originally created by David Fisher, February 2013 * version 1.0 David Fisher 31jan2014 * version 1.01 David Fisher 11aug2016 * August 2016: began adapting code for Syntax 2 of -ipdmetan- (see help file) * March 2017 // Needs further thought re behaviour of keepall, "study, m" and "by, m" // when some combinations of study and by do not exist in the data. // Should they be displayed nevertheless, with the message "(No subgroup data)" or similar? // (note this is an ipdover issue rather than an ipdmetan/admetan issue) // (this is for the next version of the package) * version 2.0 David Fisher 11may2017 * Major update of all parts of the ipdmetan package, including ipdover.ado * version 2.1 David Fisher 14sep2017 // various bug fixes // improvements to behaviour if some estimates are missing * version 3.0 David Fisher 08nov2018 // fixed bug preventing options to [command] // upversioned to v3.0 to match with ipdmetan and admetan // added `useopts' functionality * version 3.1 David Fisher 03dec2018 // fixed bug in RD option * version 3.2 David Fisher 28jan2019 // no changes to code; upversioned to match admetan/ipdmetan // but a minor change to text in the help file