* metan.ado * Study-level (aka "aggregate-data" or "published data") meta-analysis *! version 4.07 15sep2023 *! Current version by David Fisher *! Previous versions by Ross Harris and Michael Bradburn ***************************** * metan.ado version history * ***************************** // by Ross Harris and others; further details at end * Based on previous version: 1.86 1Apr2004 * Based on 2.34 11May2007 * Based on 3.01 07Jul2008 * 3.03 19May2009 * fixes prediction interval calculation for non-ratio measures and change of variable type in lcols() rcols() * think latter is ok actually- check download, perhaps was using old version! * 3.04 21Sep2010 * fixed small bug in counts option (`rawcounts' var truncated as str20) ******************************* * admetan.ado version history * ******************************* // by David Fisher; further details at end * originally written by David Fisher, June 2013 // to perform the second stage of two-stage IPDMA, using ipdmetan.ado * version 1.0 David Fisher 31jan2014 * version 2.0 David Fisher 11may2017 // Major update to extend functionality beyond "estimation commands" (cf ipdmetan); // now has most of the functionality of -metan- * version 2.1 David Fisher 14sep2017 // various bug fixes * version 3.0 David Fisher 08nov2018 // IPD+AD code now moved to ipdmetan.ado // so that -admetan- is completely self-contained, with minimal reference to -ipdmetan- * version 3.1 David Fisher 04dec2018 // various bug fixes * version 3.2 David Fisher 28jan2019 // added some new estimators // some bug fixes * version 3.3 (beta; never released) David Fisher 14nov2019 // restored previous -metan- second() option, and extended to multiple models using model(.. \ .. \ ..) // improvements to handling of zero cells; added Tarone and CMH statistics for M-H // minor bug fixes * version 3.4 (beta; never released) David Fisher 12dec2019 // added meta-analysis of proportions // restored previous -metan- behaviour of sorting string by() by order of appearance, rather than alphabetically // minor bug fixes * version 3.5 (beta; never released) David Fisher 21jan2020 // various bug fixes and code improvements * version 3.6 (beta; never released) David Fisher 22may2020 // restored -double- option to -forestplot- // corrected code for between-study heterogeneity under random-effects * version 3.7 (beta; never released) David Fisher 19jun2020 // allow all previously-valid -metan- options to continue working as documented... // ...including options now expected in forestplot() // ... (but with a warning message if appropriate, explaining syntax changes) // responded to other minor comments from JS and JPTH // moved Mata functions to a separate library file; improved some routines using optimize() * version 3.8 (beta; never released) David Fisher 07sep2020 // Following discussion with JPTH 1st Sep 2020: // - make "qncchi2" the default method for heterogeneity CIs, rather than Higgins & Thompson test-based // - by default, I-squared and H should always be Q-based ... introduce an "advanced" option for presenting tausq-based het. stats // - plus, change explanatory text underneath table to match. // Other fixes: study is blank if undefined (should be 1, 2, 3 etc.) // [Note: -twoway- doesn't seem to like text starting with double-quotes... this doesn't seem to be a -metan- problem specifically] * version 4.00 // 7th October 2020, following email from JPTH // "higgins" --> "testbased" // "tsqbased" --> "isqparam" // - added possibility of "labelling" models so that e.g. model(iv \ dl) can be labelled "Fixed" and "Random" // - also allow user-supplied effect estimates within model(...), rather than only with -metan9- first() and second() options * version 4.01 // - added Barendregt-Doi proposed denominator for double-arcsine back-transform for proportions // Bug fixes: // - corrected "syntax" statement in BuildResultsSet so that it works with Stata versions pre-15 // (option list was >70, which was too long for Stata pre-15, causing "invalid syntax" error) // - cumulative/influence with saving() now stores all relevant variables, including Q, sigmasq etc. // - improved user control over labelling of RE models // - fixed minor bug in display of SMD options (always said Cohen even if Glass/Hedges; pooled results were correct) * version 4.02 // Bug fixes: for future reference, MUST ensure that metan_pooling.ado and metan.PerformPoolingIV are kept in sync! * version 4.03 // - added new option -nobetween- : suppresses reporting of between-subgroup heterogeneity (in table and forestplot) // Bug fixes: // - improved behaviour with single-study subgroups // - corrected placement of between-group heterogeneity in forest plot // - corrected behaviour of `proportion' with `cumulative'/`influence'; also denominator(#) in forest plot // - with Mantel-Haenszel Risk Ratios with zero cells, pooled result sometimes displayed erroneously as 1.000 * version 4.04 // - improvements to Freeman-Tukey double-arcsine transformation for proportions // - fixed bug which prevented saving when calling from -ipdmetan- under certain circumstances // - improvements to "clear" option to enable it to be used within ipdmetan/ipdover * version 4.05 // - option "summaryonly" now saves the same (extended) set of variables as cumulative/influence // - fixed bug that meant "summaryonly" with "saving()/clear" and "nograph" saved the *full* results-set (i.e. as if ignoring "summaryonly") // - addition of prefix() option so that saved variables are `prefix'_ES etc. // - tweaked _rsample so that it takes on 0, 1, 2; where 2 corresponds to "insufficient data" // ... note that r(n), r(ovstats) and r(bystats) always give the counts for _USE/_rsample==1 *only*. * version 4.06 // Bug fixes: // - `proportion' with `cumulative'/`influence', when combined with `saving' or `clear', resulted in an error; this has been fixed // - `proportion' with `counts' gives incorrect denominators for subgroups (they are doubled); this has been fixed // - with hetinfo(tausq), the code only picked up the tsq value from the first subgroup, and repeated it across all other subgroups; this has been fixed // Improvements: // - improved code to avoid rounding errors/negative weights when `qwt' is zero // - new option "labtitle" to over-ride the default "Subgroup and Study" title with by() // - minor improvements to behaviour with zero cells in special cases (by, cumulative, influence) // - for proportion data, where a "pooled" result is actually just a single study, // CIs on the proportion scale are handled by -cii- rather than by back-transformation // Plus: code subroutines re-organized for improved readability and debugging going forward * version 4.07 // Bug fixes: // - `proportion' with `rfdist' and by() resulted in predictive intervals being displayed for subgroups under inappropriate models (e.g. IV); this has been fixed // - in some circumstances with proportion data, diamonds and predictive intervals displayed incorrectly in forestplot; this has been fixed // - when model(user) was used, failure to specify option extralabel() resulted in error "ovstats not found"; this has been fixed // - metan..., forestplot(nolcolscheck) resulted in error. This is not official syntax, but has nevertheless been fixed // Improvements: // - improved behaviour of hetinfo() when combined with `isqparam' and/or extraline() // - introduced new value _USE==7 specifically for predictive intervals (previously they were _USE==3/5 plus additional checks/options) // (this helps resolve the issue above r.e. proportion data) // - sensitivity analysis *can* now be used with by() // - by() now supports use of "pooled (across subgroups)" heterogeneity parameters, either additive (DerSimonian-Laird) or multiplicative // - weights from multiple models may now be displayed in forestplot and/or saved to new dataset, with new `allweights' option // - added Viechtbauer (2021) median-unbiased version of Mandel-Paule tausq estimator program define metan, rclass properties(hr nohr shr noshr irr or rrr) version 11.0 local version : di "version " string(_caller()) ":" if _caller() >= 12 { local hidden hidden local historical historical } return `hidden' local metan_version "4.07" // Clear historical global macros (see metan9.ado) forvalues i = 1/15 { global S_`i' } global S_51 syntax varlist(numeric min=2 max=6) [if] [in] [, SORTBY(varlist) /// FORESTplot(passthru) /// forestplot (ultimately -twoway-) options USE(varname numeric) /// undocumented option, for use with e.g. -ipdmetan- * ] // all other options, to be parsed later local ifopt : copy local if // for warning message regarding missing/insufficient data local inopt : copy local in // for warning message regarding missing/insufficient data marksample touse, novarlist // `novarlist' option so that entirely missing/nonexistent studies/subgroups may be included local invlist `varlist' // list of "original" vars passed by the user to the program if `"`use'"'!=`""' { // e.g. if passed from -ipdmetan- local _USE : copy local use cap assert inlist(`_USE', 1, 2, 3, 5) if `touse' if _rc { nois disp as err `"error in {bf:use(}{it:varname}{bf:)}"' exit 198 } local ifopt local inopt // re-define `touse' as if running -metan- directly; // values of _USE other than 1 and 2 will not be needed until BuildResultsSet. tempvar touse2 qui gen byte `touse2' = `touse' qui replace `touse' = `touse' * inlist(`_USE', 1, 2) local touse2opt touse2(`touse2') } else { tempvar _USE // Note that `_USE' is defined if-and-only-if `touse' qui gen byte `_USE' = 1 if `touse' // i.e. !missing(`_USE') <==> `touse' local touse2 `touse' } if `"`ifopt'`inopt'"'!=`""' local ifinopt ifinopt ******************* * Initial parsing * ******************* ** Parse -forestplot- options to extract those relevant to -metan- // N.B. Certain options may be supplied EITHER to metan directly, OR as sub-options to forestplot() // with "forestplot options" prioritised over "metan options" in the event of a clash. // These options are: // effect options parsed by CheckOpts (e.g. `rr', `rd', `md', `smd', `wmd', `log') // nograph, nohet, nobetween, nooverall, nosubgroup, nowarning, nowt, nostats // effect, hetinfo, lcols, rcols, plotid, ovwt, sgwt, sgweight // cumulative, efficacy, influence, interaction // counts, group1, group2 (for compatibility with previous version of metan.ado) // rfdist, rflevel (for compatibility with previous version of metan.ado) // N.B. if -metan- was called by -ipdmetan- , some of this may already have been done cap nois ParseFPlotOpts, cmdname(`cmdname') touse(`touse') options(`options') `forestplot' if _rc { if `"`err'"'==`""' { if _rc==1 nois disp as err `"User break in {bf:metan.ParseFPlotOpts}"' else nois disp as err `"Error in {bf:metan.ParseFPlotOpts}"' } c_local err noerr // if called by -ipdmetan- , no need to also report an "error in {bf:metan}" exit _rc } local eform `s(eform)' local log `s(log)' local summstat `s(summstat)' local effect `"`s(effect)'"' local opts_adm = trim(`"`s(opts_parsed)' `s(options)'"') // options as listed above, plus other options supplied directly to -metan- local opts_fplot = trim(`"`s(opts_fplot)'"') local forestplot forestplot(`opts_fplot') // other options supplied as sub-options to forestplot(), or main options with relevance *only* to forestplot.ado **************************************** * Establish basic data characteristics * **************************************** // Generate stable ordering to pass to future subroutines tempvar obs qui gen long `obs' = _n ** Parse `study' and `by' (including first element of `lcols' if appropriate) // Checks for problems with `study' and `by' // and, amongst other things, converts them from string to numeric if necessary tempname newstudylab newbylab tempvar newstudy newby cap nois ProcessLabels `invlist' if `touse', sortby(`sortby' `obs') `opts_adm' /// newstudy(`newstudy') newby(`newby') /// newstudylab(`newstudylab') newbylab(`newbylab') if _rc { if _rc==1 nois disp as err `"User break in {bf:metan.ProcessLabels}"' else nois disp as err `"Error in {bf:metan.ProcessLabels}"' c_local err noerr exit _rc } // Parse and re-define lcols(), which may have been altered by ProcessLabels // and remove original options by(), study() and label() from options list // (replacements for these are returned by ProcessLabels) // plus labtitle() will be stored in `output_opts', not `opts_adm' local 0 `", `opts_adm'"' syntax , [ STUDY(string) BY(string) LABEL(string) LCols(namelist) LABTITLE(string asis) * ] local opts_adm `"`macval(options)'"' // study/title options, to pass onto metan_output.ado local output_opts `"study(`s(study)') sfmtlen(`s(sfmtlen)') labtitle(`s(labtitle)') lcols(`s(lcols)')"' // `_STUDY' and `_BY' are the "working" variables from now on; guaranteed numeric. // We don't need `study' and `by' anymore (except for -markout- immediately below); // instead `_STUDY' and `_BY' indicate the existence of these variables (e.g. in validity checks). local _STUDY `s(study)' local _BY `s(by)' if `"`s(smissing)'"'==`""' markout `touse' `study', strok if `"`_BY'"'!=`""' { if `"`s(bymissing)'"'==`""' markout `touse' `by', strok // Form `bylist' qui levelsof `_BY' if `touse', missing local(bylist) // "missing" since `touse' should already be appropriate for missing yes/no } ** Setup data and model(s) // - Parse inputted varlist structure and check for validity // - Parse models and modelling options cap nois ProcessData `_USE' `invlist' if `touse', summstat(`summstat') /// by(`_BY') /*Sep 2023: by() ONLY needed in case "pooled heterogeneity" requested*/ /// `eform' `log' `opts_adm' `touse2opt' // <-- this latter is a marker of being passed from -ipdmetan- if _rc { if `"`err'"'==`""' { if _rc==2000 nois disp as err "No studies found with sufficient data to be analysed" else if _rc==1 nois disp as err `"User break in {bf:metan.ProcessData}"' else nois disp as err `"Error in {bf:metan.ProcessData}"' } c_local err noerr exit _rc } local opts_adm `"`s(options)' `s(eform)' `s(log)'"' // all other options (rationalised) local params = `s(params)' local eform `s(eform)' // needed later (just once) in this main routine, for return historical local rownames `s(rownames)' local qstat `s(qstat)' local citype `s(citype)' assert `"`citype'"'!=`""' return local citype `citype' // citype is now established // internal options, to send to metan_analysis.ado local userwgt `s(userwgt)' local olduser `s(olduser)' // Finalize `effect' if `"`effect'"'==`""' local effect `"`s(effect)'"' // don't override user-specified value if `"`effect'"'==`""' local effect "Effect" if `"`s(log)'"'!=`""' local effect `"log `effect'"' if `"`s(interaction)'"'!=`""' local effect `"Interact. `effect'"' // options to send to metan_output.ado local output_opts `"`macval(output_opts)' effect(`"`effect'"') `s(labelopts)' `s(interaction)'"' // N.B. labelopts here are MODEL labels, not to be confused with LABTITLE() local m = `s(m)' local modellist `s(modellist)' local teststatlist `s(teststatlist)' return scalar m = `m' return local model `modellist' // model(s) are now established forvalues j = 1 / `m' { local modelopts `"`macval(modelopts)' model`j'opts(`s(model`j'opts)')"' // to pass to metan_analysis.ado if `m'==1 return local modelopts `"`s(model`j'opts)'"' // return model options else return local model`j'opts `"`s(model`j'opts)'"' } local summstat `s(summstat)' if "`summstat'"=="cohend" local measure "Cohen's d SMD" else if "`summstat'"=="glassd" local measure "Glass's delta SMD" else if "`summstat'"=="hedgesg" local measure "Hedges's g SMD" else if "`summstat'"=="ftukey" local measure "Freeman-Tukey transformed proportion" else if "`summstat'"=="arcsine" local measure "Arcsine-transformed proportion" else if "`summstat'"=="logit" local measure "Logit-transformed proportion" else if "`summstat'"=="pr" local measure "Untransformed proportion" else if "`summstat'"!="" local measure = upper("`summstat'") return local measure `"`measure'"' // If 2x2 data, return tger and cger if `params'==4 { return scalar tger = `s(tger)' return scalar cger = `s(cger)' // Historical global S_13 = `s(tger)' global S_14 = `s(cger)' } // Return historical, from first() or second() local ES `s(ES)' local ci_low `s(ci_low)' local ci_upp `s(ci_upp)' local ES_2 `s(ES_2)' local ci_low_2 `s(ci_low_2)' local ci_upp_2 `s(ci_upp_2)' *************************************** * Prepare for meta-analysis modelling * *************************************** // Basic option compatibility checks ParseOptions, modellist(`modellist') by(`_BY') `opts_adm' local opts_adm `"`s(newopts)' `s(options)'"' local keepvars `s(keepvars)' ** Setup tempvars // The "core" elements of `outvlist' are _ES, _seES, _LCI, _UCI, _WT (plus _NN and _CC if appropriate) // By default, these will be left behind in the dataset upon completion of -metan- // _ES, _seES, _LCI and _UCI are *always* on the linear/interval scale, for pooling // and so may need to be back-transformed to the original scale for display on-screen or in the forestplot. // Typically this just involves exponentiating; but for proportions using e.g. Freeman-Tukey, // the back-transform is too complex, so we create separate variables _Prop_ES, _Prop_LCI, _Prop_UCI (`prvlist') // containing the proportion and CI on the original scale. // `tvlist' = list of elements of `outvlist' that need to be generated as *tempvars* (i.e. do not already exist) // (whilst ensuring that any overlapping elements in `invlist' and `outvlist' point to the same actual variables) cap nois SetupOutVList `invlist' if `touse', rownames(`rownames') `opts_adm' if _rc { if `"`err'"'==`""' { if _rc==1 nois disp as err `"User break in {bf:metan.SetupOutVList}"' else nois disp as err `"Error in {bf:metan.SetupOutVList}"' } c_local err noerr exit _rc } local opts_adm `"`s(options)'"' local xrownames `"`s(xrownames)'"' // Create tempvars based on `tvlist' // and finally create `outvlist' = list of "standard" vars = _ES _seES _LCI _UCI _WT [_NN _CC] // where _NN is optional if `params'==2 or 3 (excluding `proportion'; see above) // and where _CC is optional if `params'==4 (or `proportion'; note that in either case, _NN must exist, so there is no ambiguity of argument names) local tvlist `s(tvlist)' foreach tv of local tvlist { tempvar `tv' qui gen double ``tv'' = . } tokenize `invlist' `s(nptsvar)' // N.B. `s(nptsvar)' is only applicable with 2- or 3-variable syntax (ES seES or ES LCI UCI), in which case _CC is *not* applicable args `s(arglist)' local outvlist `_ES' `_seES' `_LCI' `_UCI' `_WT' `_NN' `_CC' if `"`s(intopt)'"'==`""' { cap recast int `_NN' // `s(intopt)' records whether the -nointeger- option was supplied } cap recast byte `_CC' // If transformed proportions ("`summstat'"!="pr"), default is to display back-transformed effect sizes ("`nopr'"=="") // March 2021: Not applicable to `cumulative' or `influence'; this functionality is handled differently, see below local 0 `", `opts_adm'"' syntax [, PRoportion NOPR CUmulative INFluence * ] if `"`xrownames'"'!=`""' { local nt = `: word count `xrownames'' forvalues i = 1 / `nt' { tempvar tv`i' qui gen double `tv`i'' = . local xoutvlist `xoutvlist' `tv`i'' } local xv_opt xoutvlist(`xoutvlist') // March 2021: `prvlist' is a subset of `xoutvlist' if "`proportion'"!="" & "`summstat'"!="pr" & "`nopr'"=="" { local prvlist tokenize `xoutvlist' foreach el in prop_eff prop_lci prop_uci { local i : list posof `"`el'"' in xrownames local prvlist `prvlist' ``i'' } local prv_opt prvlist(`prvlist') } } else if "`proportion'"!="" & "`summstat'"!="pr" & "`nopr'"=="" { local prvlist forvalues i = 1 / 3 { // _Prop_ES, _Prop_LCI, _Prop_UCI tempvar prv`i' qui gen double `prv`i'' = . local prvlist `prvlist' `prv`i'' } local prv_opt prvlist(`prvlist') } // N.B. `xoutvlist' now contains the tempvars which will hold the relevant returned stats... // - with the same contents as the elements of `rownames' // - but *without* npts (as _NN is handled separately) // - and with the addition of Q, Qdf, Q_lci, Q_uci, [sigmasq] // - and with the addition of a separate weight variable (`_WT2') if cumulative/influence // Subsequently, `rownames' will be passed between subroutines // and `xrownames' will be re-derived whenever needed // by taking `rownames' and applying the "algorithm" above. // August 2023 local 0 `", `opts_adm'"' syntax [, ALLWEIGHTS * ] if `"`allweights'"'!=`""' { local m1 = `m' - 1 forvalues j = 1 / `m1' { tempvar _WT_`j' local allwtnames `allwtnames' `_WT_`j'' } local allwtnamesopt allwtnames(`allwtnames') local opts_adm `"`macval(options)'"' // remove allweights form `opts_adm'; it is otherwise unchanged } ****************************************** * Run the actual meta-analysis modelling * ****************************************** // ... and collect pooled statistics metan_analysis `_USE' `invlist' if `touse', `ifinopt' `touse2opt' `opts_adm' `prv_opt' `xv_opt' `forestplot' /// sortby(`sortby' `obs') summstat(`summstat') qstat(`qstat') citype(`citype') rownames(`rownames') /// teststatlist(`teststatlist') modellist(`modellist') outvlist(`outvlist') by(`_BY') /// `modelopts' /// list of options for each model, in the form "model`j'opts(`s(model`j'opts)')" `userwgt' /// internal option from ProcessData `allwtnamesopt' // Aug 2023: internal option relating to option "allweights" // Form final list of matrices to send to metan_output.ado forvalues j = 1 / `m' { if `m' > 1 local jj = `j' cap { confirm matrix r(bystats`j') assert rowsof(r(bystats`j')) > 1 } if !_rc { tempname bystats`j' matrix `bystats`j'' = r(bystats`j') local bystatslist `"`bystatslist' `bystats`j''"' return matrix bystats`jj' = `bystats`j'', copy } cap { confirm matrix r(byhet`j') assert rowsof(r(byhet`j')) > 1 } if !_rc { tempname byhet`j' matrix `byhet`j'' = r(byhet`j') local byhetlist `"`byhetlist' `byhet`j''"' return matrix byhet`jj' = `byhet`j'', copy } } if `"`bystatslist'"'!=`""' local mmatlist `"`mmatlist' bystatslist(`bystatslist')"' if `"`byhetlist'"'!=`""' local mmatlist `"`mmatlist' byhetlist(`byhetlist')"' foreach x in ovstats hetstats byQ { cap { confirm matrix r(`x') assert rowsof(r(`x')) > 1 } if !_rc { local lowerx = lower("`x'") tempname `x' matrix ``x'' = r(`x') local mmatlist `"`mmatlist' `lowerx'(``x'')"' return matrix `x' = ``x'', copy } } cap { confirm matrix r(mwt) assert rowsof(r(mwt)) == `m' assert colsof(r(mwt)) >= `r(nby)' // r(nby) may be less than colsof(mwt) if missing subgroups assert colsof(r(mwt)) <= `: word count `bylist'' // `bylist' is the *maximum* number of subgroups to be analysed } if !_rc { tempname mwt matrix `mwt' = r(mwt) local mmatlist `"`mmatlist' mwt(`mwt')"' // N.B. don't return matrix `mwt'; this is for internal use only } // Simple marker of whether above matrices exist; if not, no pooling has taken place if `"`mmatlist'"'==`""' local mmatlist nopool // Return other scalars (relevant to "primary" model) // some of which are also saved in r(ovstats) return scalar k = r(k) return scalar n = r(n) if `"`ovstats'"'!=`""' { return scalar eff = r(eff) return scalar se_eff = r(se_eff) return scalar Q_uci = r(Q_uci) return scalar Q_lci = r(Q_lci) return scalar Q = r(Q) return scalar Isq = r(Isq) return scalar H = r(H) return scalar HsqM = r(HsqM) if `"`proportion'"'!=`""' & `"`nopr'"'==`""' { return scalar prop_eff = r(prop_eff) return scalar prop_lci = r(prop_lci) return scalar prop_uci = r(prop_uci) } if `params'==4 { if !missing(r(RR)) return scalar RR = r(RR) if !missing(r(OR)) return scalar OR = r(OR) } local tausq_or_phi = 0 if !missing(`ovstats'[rownumb(`ovstats', "tausq"), 1]) { return scalar tausq = r(tausq) return `historical' scalar sigmasq = r(sigmasq) local tausq_or_phi = 1 } if !missing(`ovstats'[rownumb(`ovstats', "phi"), 1]) { return scalar phi = r(phi) // Sep 2023: multiplicative heterogeneity only (=H^2) local tausq_or_phi = 1 } if `tausq_or_phi' { // May 2020: Qr deprecated in favour of Hstar: latter is more interpretable, and has appeared in print // (van Aert & Jackson 2019) return `historical' scalar Qr = r(Qr) return scalar Hstar = r(Hstar) } gettoken model1 : modellist local RefREModList mp pmm ml pl reml bt dlb // random-effects models where a conf. interval for tausq is estimated if `: list model1 in RefREModList' { return scalar tsq_var = r(tsq_var) return scalar rc_tsq_lci = r(rc_tsq_lci) return scalar rc_tsq_uci = r(rc_tsq_uci) if "`model'"=="pl" { return scalar rc_eff_lci = r(rc_eff_lci) return scalar rc_eff_uci = r(rc_eff_uci) } if inlist("`model1'", "qprofile", "bt") { return scalar rc_tausq = r(rc_tausq) } else { return scalar converged = r(converged) } } } if `"`bystatslist'"'!=`""' { return `hidden' scalar tsq_common = r(tsq_common) return scalar Qsum = r(Qsum) return scalar Qbet = r(Qbet) return scalar F = r(Fstat) return scalar nby = r(nby) } // User-defined estimates via "legacy" -metan- options first() and/or second() // These values were returned by subroutine ProcessModelOpts if "`olduser'"!="" & "`model1"=="user" { // first(es lci uci) return `historical' scalar ES = `ES' return `historical' scalar ci_low = `ci_low' return `historical' scalar ci_upp = `ci_upp' global S_1 = `ES' global S_3 = `ci_low' global S_4 = `ci_upp' if "`: word 2 of `modellist''"=="user" { // second(es lci uci) return `historical' scalar ES_2 = `ES_2' return `historical' scalar ci_low_2 = `ci_low_2' return `historical' scalar ci_upp_2 = `ci_upp_2' } } else { if "`eform'"!="" { return `historical' scalar ES = exp(r(eff)) return `historical' scalar selogES = r(se_eff) return `historical' scalar ci_low = exp(r(eff_lci)) return `historical' scalar ci_upp = exp(r(eff_uci)) global S_1 = exp(r(eff)) global S_2 = r(se_eff) global S_3 = exp(r(eff_lci)) global S_4 = exp(r(eff_uci)) if "`olduser'"!="" { if "`: word 2 of `modellist''"=="user" { // second(es lci uci) return `historical' scalar ES_2 = `ES_2' return `historical' scalar ci_low_2 = `ci_low_2' return `historical' scalar ci_upp_2 = `ci_upp_2' } else if `m'==2 { // if `second' is a model name e.g. "second(random)" return `historical' scalar ES_2 = exp(`ovstats'[rownumb(`ovstats', "eff"), 2]) return `historical' scalar selogES_2 = `ovstats'[rownumb(`ovstats', "se_eff"), 2] return `historical' scalar ci_low_2 = exp(`ovstats'[rownumb(`ovstats', "eff_lci"), 2]) return `historical' scalar ci_upp_2 = exp(`ovstats'[rownumb(`ovstats', "eff_uci"), 2]) } } } else { return `historical' scalar ES = r(eff) return `historical' scalar seES = r(se_eff) return `historical' scalar ci_low = r(eff_lci) return `historical' scalar ci_upp = r(eff_uci) global S_1 = r(eff) global S_2 = r(se_eff) global S_3 = r(eff_lci) global S_4 = r(eff_uci) if "`olduser'"!="" { if "`: word 2 of `modellist''"=="user" { // second(es lci uci) return `historical' scalar ES_2 = `ES_2' return `historical' scalar ci_low_2 = `ci_low_2' return `historical' scalar ci_upp_2 = `ci_upp_2' } else if `m'==2 { // if `second' is a model name e.g. "second(random)" return `historical' scalar ES_2 = `ovstats'[rownumb(`ovstats', "eff"), 2] return `historical' scalar seES_2 = `ovstats'[rownumb(`ovstats', "se_eff"), 2] return `historical' scalar ci_low_2 = `ovstats'[rownumb(`ovstats', "eff_lci"), 2] return `historical' scalar ci_upp_2 = `ovstats'[rownumb(`ovstats', "eff_uci"), 2] } } } global S_7 = r(Q) global S_8 = r(Qdf) global S_9 = chi2tail(r(Qdf), r(Q)) global S_51 = r(Isq) } // if "`second'"!="" { if "`olduser'"!="" & `m'==2 { // NOV 2020 tokenize `modellist' if `"`3'"'!=`""' { nois disp as err "Invalid use of option {bf:second()}" exit 198 } forvalues i = 1/2 { if "``i''"=="mh" local method_`i' "M-H" else if "``i''"=="peto" local method_`i' "Peto" else if "``i''"=="iv" local method_`i' "I-V" else if "``i''"=="dl" local method_`i' "D+L" else if "``i''"=="user" local method_`i' "USER" else { nois disp as err "Invalid use of option {bf:second()}" exit 198 } if `i'==1 & "`userwgt'"!="" local method_1 "*" return `historical' local method_`i' `method_`i'' } } if !missing(r(z)) { return `historical' scalar z = r(z) return `historical' scalar p_z = r(pvalue) global S_5 = abs(r(z)) global S_6 = r(pvalue) } return `historical' scalar i_sq = r(Isq) return `historical' scalar het = r(Q) return `historical' scalar df = r(Qdf) return `historical' scalar p_het = chi2tail(r(Qdf), r(Q)) if !missing(r(chi2)) { return `historical' scalar chi2 = r(chi2) return `historical' scalar p_chi2 = chi2tail(1, r(chi2)) global S_10 = r(chi2) global S_11 = chi2tail(1, r(chi2)) } if !missing(r(tausq)) { return `historical' scalar tau2 = r(tausq) global S_12 = r(tausq) } // END OF RETURN HISTORICAL if `"`keepvars'"'!=`""' { // tidy up [moved Jun 2022] cap drop `r(todrop)' } *********************************************** * Outputs to screen, saved datasets and plots * *********************************************** local qlist `r(Q)' `r(Qdf)' `r(Q_lci)' `r(Q_uci)' `r(Qsum)' `r(Qbet)' `r(nby)' local extra extra(`r(nsg)' `r(nzt)') `r(mhallzero)' `r(clearnpts)' local outvlist `r(outvlist)' if `"`r(xoutvlist)'"'!=`""' { local xv_opt xoutvlist(`r(xoutvlist)') local oldvlist `r(oldvlist)' } if `"`sortby'"'==`""' local sorted nosorted // for PrintDesc // Remove options not needed for metan_ouput but needed in main routine local 0 `", `opts_adm' `keepvars'"' syntax [, noPRESERVE noKEEPvars noRSample ALTSAVedvars /* Added May 2023 */ * ] local opts_adm `"`macval(options)'"' if `"`rsample'"'!=`""' local keepvars nokeepvars // noRSample implies noKEEPVars // Now parse additional options needed in this part of the code (AND in metan_output) local 0 `", `opts_adm'"' syntax [, SAVING(passthru) CLEAR CLEARSTACK noGRaph KEEPAll KEEPOrder /// PREfix(name local) ILevel(cilevel) OLevel(cilevel) * ] // [Aug 2023] consistency check r.e. cumul/influence cap assert (`"`oldvlist'"'!=`""') == (`"`cumulative'`influence'"'!=`""') if _rc { nois disp as err `"Something has gone wrong regarding {bf:`cumulative'`influence'}"' exit _rc } if `"`altsavedvars'"'!=`""' & `"`cumulative'`influence'"'==`""' { disp `"{error}{bf:altsavedvars} may only be specified with {bf:cumulative} or {bf:influence}; option will be ignored"' local altsavedvars } // [July 2021] prefix() option if length("`prefix'") > 5 { disp as err `"{bf:prefix()} invalid; stub name too long ( >5 )"' exit 198 } // Handle studies with insufficient data (_USE==2) // N.B. additional changes of `_USE' from 1 to 2 may have been done within metan_analysis (e.g. if no zero-cell corrections) if `"`keepall'`keeporder'"'==`""' { qui replace `touse' = 0 if `_USE'==2 qui replace `touse2' = 0 if `_USE'==2 } ** Create and return matrix of coefficients // if cumul/influence, `outvlist' contains cumul/influence effect sizes // but r(coeffs) and saved variables should contain original study-specific effect sizes if `"`oldvlist'"'==`""' tokenize `outvlist' // default else { // if cumul/influence tokenize `oldvlist' local extravarlab `"`cumulative'`influence' "' // for later // May-Aug 2023: `altsavedvars' option // This over-rides the default behaviour regarding cumulative/influence data saved *in the current dataset* (or in matrix r(coeffs)) // By default, such data mirrors the raw data, i.e. it is *not* cumulative/influence. if `"`altsavedvars'"'!=`""' { tempvar _NN2 qui gen byte `_NN2' = . tokenize `outvlist' args _ES _seES _LCI _UCI _WT _NN _CC tokenize `_ES' `_seES' `_LCI' `_UCI' `_WT' `_NN2' `_CC' // ^^ don't re-define `outvlist' as sent to metan_output // Instead, retokenize for use within metan.ado only // (i.e. GetMatCoeffs, and then finishing off after metan_ouput) FillDown `outvlist', newn(`_NN2') sortby(`sortby' `obs') `opts_adm' // Because in some situations the fill-down routine within metan_output will not be executed, // just do it here instead (within a subroutine, in order to use `sortpreserve') } } args _ES _seES _LCI _UCI _WT _NN _CC if `"`keepvars'"'!=`""' { // 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' qui count if `touse' if r(N) > c(matsize) { disp `"{error}matsize too small to store matrix of study coefficients; this step will be skipped"' disp `"{error} (see {bf:help matsize})"' } else { // do this in a subroutine, so that we can use -sortpreserve- GetMatCoeffs `_BY' `_STUDY' `_ES' `_seES' `_NN' `_WT' if `touse', sortby(`_BY' `tempuse' `sortby' `obs') tempname coeffs matrix `coeffs' = r(coeffs) if `"`_BY'"'!=`""' local _BYexist _BY if `"`_NN'"'!=`""' local _NNexist _NN if `"`_WT'"'!=`""' local _WTexist _WT matrix colnames `coeffs' = `_BYexist' _STUDY _ES _seES `_NNexist' `_WTexist' return matrix coeffs = `coeffs' } } ** Set up -preserve- prior to running metan_output // July 2021: // Only need to -preserve- if: // - saving a results set // - creating a forest plot // - clearing the original data, to leave the results set in memory if `"`saving'"'!=`""' | `"`clear'`clearstack'"'!=`""' | `"`graph'"'==`""' { // Note: if option -nopreserve- , assumption is that (original) data is *already* preserved (e.g. by -ipdmetan-) if `"`preserve'"'==`""' { local preserve_opt preserve `preserve_opt' } } metan_output `_USE' `invlist' if `touse', `touse2opt' `opts_adm' `prv_opt' `xv_opt' `forestplot' /// sortby(`sortby' `obs') `sorted' summstat(`summstat') qstat(`qstat') /// teststatlist(`teststatlist') modellist(`modellist') outvlist(`outvlist') /// `modelopts' /// list of options for each model, in the form "model`j'opts(`s(model`j'opts)')" by(`_BY') `output_opts' /// by() bylist() study() plus suboptions relating to titling of tables etc. `mmatlist' qlist(`qlist') /// results matrices (including subgroups & heterogeneity) from "metan_analysis" `extra' /// /* internal options/macros from metan_analysis */ `allwtnamesopt' // Aug 2023: internal option relating to option "allweights" ************* * Finish up * ************* // exit early... : // (1) if option -nopreserve- (e.g. if called from -ipdmetan- ) // (2) if option "clear" (so no need for stored variables) if `"`clear'`clearstack'"'!=`""' | `"`preserve'"'!=`""'{ if `"`preserve_opt'"'!=`""' { restore, not } exit } // Otherwise, restore "original" data (that is, original *observations* but with added tempvars); // but preserve it again temporarily while "stored" variables are processed // if all goes well, this -preserve- will be cancelled later with -restore, not- ... if `"`preserve_opt'"'!=`""' local restore_opt restore, `restore_opt' preserve ** Stored (left behind) variables // Unless -noKEEPVars- (i.e. "`keepvars'"!=""), leave behind _ES, _seES etc. in the original dataset // List of these "permanent" names = _ES _seES _LCI _UCI _WT _NN ... plus _CC if applicable // (as opposed to `outvlist', which contains the *temporary* names `_ES', `_seES', etc.) // (N.B. this code applies whether or not cumulative/influence options are present) if `"`keepvars'"'==`""' { // July 2021: _CC is defined within DrawTableAD using c_local local tostore _ES _seES _LCI _UCI _WT _NN _CC foreach v of local tostore { if `"``v''"'!=`""' { if `"``v''"'!=`"`prefix'`v'"' { // If pre-existing var has the same name (i.e. was named _ES etc.), nothing needs to be done. cap drop `prefix'`v' // Else, first drop any existing var named _ES (e.g. left over from previous analysis) // If in `tvlist', we can directly rename if `: list v in tvlist' { qui rename ``v'' `prefix'`v' } // Otherwise, ``v'' is a pre-existing var which needs to be retained at program termination // so, use -clonevar- else qui clonevar `prefix'`v' = ``v'' if `touse' } local `v' `prefix'`v' // for use with subsequent code (local _ES now contains "_ES", etc.) } else cap drop `prefix'`v' // in any case, drop existing vars named _ES etc. } // qui compress `tvlist' order `_ES' `_seES' `_LCI' `_UCI' `_WT' `_NN' `_CC' `_rsample', last char define `_LCI'[Level] `ilevel' char define `_UCI'[Level] `ilevel' char define `_LCI'[LevelPooled] `olevel' char define `_UCI'[LevelPooled] `olevel' // variable labels StrProper `extravarlab'effect label variable `_ES' `"`s(result)' size"' label variable `_seES' `"Standard error of `extravarlab'effect size"' label variable `_LCI' "`ilevel'% lower confidence limit" label variable `_UCI' "`ilevel'% upper confidence limit" label variable `_WT' `"`=proper(`"`extravarlab'"')'% Weight"' format `_WT' %6.2f if `"`_NN'"'!=`""' { StrProper `extravarlab'no. label variable `_NN' `"`s(result)' pts"' } if `"`_CC'"'!=`""' { label variable `_CC' "Continuity correction applied?" } if `"`_rsample'"'==`""' { cap drop _rsample qui gen byte _rsample = 0 qui replace _rsample = `_USE' if inlist(`_USE', 1, 2) // this shows which observations were used label variable _rsample "Sample included in most recent model" } } // else (if -noKEEPVars- specified), check for existence of pre-existing vars named _ES, _seES etc. and give warning if found else { cap confirm numeric var `_CC' if !_rc { local _CCexist _CC local ortext `", {bf:_NN} or {bf:_CC})"' } else local ortext `" or {bf:_NN}"' // If -noKEEPVars- but not -noRSample-, need to create _rsample as above if `"`rsample'"'==`""' { // create _rsample cap drop _rsample qui gen byte _rsample = 0 qui replace _rsample = `_USE' if inlist(`_USE', 1, 2) // this shows which observations were used label variable _rsample "Sample included in most recent model" local warnlist local rc = 111 foreach v in _ES _seES _LCI _UCI _WT _NN `_CCexist' { cap confirm var `prefix'`v' if !_rc local warnlist `"`warnlist' {bf:`prefix'`v'}"' local rc = min(`rc', _rc) } if !`rc' { disp _n `"{error}Warning: option {bf:nokeepvars} specified, but the following "stored" variables already exist:"' disp `"{error}`warnlist'"' disp `"{error}Note that these variables are therefore no longer associated with the most recent analysis"' disp `"{error}(although {bf:`prefix'_rsample} {ul:is})."' } } // -noKEEPVars- *and* -noRSample- else { // give warning if variable named _rsample already existed cap confirm var `prefix'_rsample if !_rc { disp _n `"{error}Warning: option {bf:norsample} specified, but "stored" variable {bf:`prefix'_rsample} already exists"' } local rsrc = _rc local warnlist local rc = 111 foreach v in _ES _seES _LCI _UCI _WT _NN _CC { cap confirm var `prefix'`v' if !_rc { local warnlist `"`warnlist' {bf:`prefix'`v'}"' local rc = 0 } } if !`rc' { if !`rsrc' disp `"{error}as do the following "stored" variables:"' else disp _n `"{error}Warning: option {bf:norsample} specified, but the following "stored" variables may already exist:"' disp `"{error}`warnlist'"' } local plural = cond(!`rc', "these variables are", "this variable is") if !`rsrc' | !`rc' disp `"{error}Note that `plural' therefore NOT associated with the most recent analysis."' } } // Clear/restore characteristics char _dta[FPUseOpts] `char_fpuseopts' char _dta[FPUseVarlist] `char_fpusevlist' // Finally, cancel -preserve- restore, not end program define FillDown, sortpreserve syntax varlist [if] [in] [, SORTBY(varlist) BY(varname numeric) NEWN(varname numeric) CUmulative INFluence SGWt * ] marksample touse, novarlist tokenize `varlist' args _ES _seES _LCI _UCI _WT _NN _CC if `"`sgwt'"'!=`""' local by2 : copy local by sort `touse' `by2' `sortby' qui by `touse' `by2' : replace `newn' = sum(`_NN') if `touse' if `"`influence'"'!=`""' { qui by `touse' `by2' : replace `newn' = `newn'[_N] if `touse' qui replace `newn' = `newn' - `_NN' if `touse' } end program define GetMatCoeffs, rclass sortpreserve syntax varlist [if] [in], [SORTBY(varlist)] marksample touse, novarlist sort `touse' `sortby' tempname coeffs mkmat `varlist' if `touse', matrix(`coeffs') return matrix coeffs = `coeffs' end program define StrProper, sclass gettoken word1 rest : 0 local result = proper(`"`word1'"') + lower(`"`rest'"') sreturn local result `"`result'"' end ******************************************************************************************************** ** Routine to parse main options and forestplot options together, and: // a. Parse some general options, such as -eform- options and counts() // b. Check for conflicts between main options and forestplot() suboptions. // (called directly by metan.ado) * Notes: // N.B. This program was originally designed to be used as a subroutine for both -metan- and -ipdmetan- // Certain options may be supplied EITHER to -(ipd)metan- directly, OR as sub-options to forestplot() // with "forestplot options" prioritised over "main options" in the event of a clash. // These options are: // - effect/eform options parsed by CheckOpts (e.g. `rr', `rd', `md', `smd', `wmd', `log') // - nograph, nohet, nobetween, nooverall, nosubgroup, nowarning, nowt, nostats, nopr // - effect, hetinfo, lcols, rcols, plotid, ovwt, sgwt, sgweight // - cumulative, efficacy, influence, interaction // Plus, specifically for compatibility with metan9.ado: // - counts, group1, group2, rfdist, rflevel, nobox, boxsca(#), force program define ParseFPlotOpts, sclass ** Parse top-level summary info and option lists // Note June 2022: touse() is only here for use with `plotid', see below syntax [, CMDNAME(string) TOUSE(varname numeric) OPTIONS(string asis) FORESTplot(string asis)] ** Plot-related options valid with -metan- version 4 only local badopts0 ADJust LCOLSCHeck NAmes NULL TRUNCate local badopts1 COLSONLY KEEPXLabs LEFTJustify USESTRICT local badopts2 ADDHeight CIRAnge DATAID FP MAXWidth MAXLines NLINEOPts OCILINEOPts local badopts2 `badopts2' RAnge RFOPts RFCILINEOPts SAVEDIms SPacing TArget USEDIms local nobadopts0 foreach op of local badopts0 { // optionally off (badopts1 = optionally on) local nobadopts0 `nobadopts0' no`op' } local badopts2p foreach op of local badopts2 { // content in brackets local badopts2p `badopts2p' `op'(passthru) } local 0 `", `options'"' syntax [, `nobadopts0' `badopts1' `badopts2p' * ] foreach op of local badopts0 { local lop = lower(`"`op'"') if `"``lop''"'!=`""' { nois disp as err _n `"Option {bf:no``lop''} may only be supplied as a sub-option to the {bf:forestplot()} option; see {help metan:help metan}"' exit 198 } } foreach op of local badopts1 { local lop = lower(`"`op'"') if `"``lop''"'!=`""' { nois disp as err _n `"Option {bf:``lop''} may only be supplied as a sub-option to the {bf:forestplot()} option; see {help metan:help metan}"' exit 198 } } foreach op of local badopts2 { local lop = lower(`"`op'"') if `"``lop''"'!=`""' { nois disp as err _n `"Option {bf:``lop''()} may only be supplied as a sub-option to the {bf:forestplot()} option; see {help metan:help metan}"' exit 198 } } ** Quick initial parse, to extract specific options and correct for synonyms etc. local 0 `", `options'"' syntax [, EFFect(string asis) /// HETINFO(string) HETStat(string) OVStat(string) /// /* N.B. `hetstat' and `ovstat' are legacy synonyms for `hetinfo' */ OVWt SGWt OVWEIGHT SGWEIGHT /// /* synonyms */ COUNTS2 COUNTS(string asis) GROUP1(passthru) GROUP2(passthru) * ] /* ^^ modern syntax has "group1" and "group2" as sub-options to "count"; older syntax had them as separate options */ local opts_main `"`macval(options)'"' local sgwt = cond("`sgweight'"!="", "sgwt", "`sgwt'") // sgweight is a synonym (for compatibility with -metan9- ) local sgweight local ovwt = cond("`ovweight'"!="", "ovwt", "`ovwt'") // ovweight is a synonym (for consistency with sgweight) local ovweight if (`"`hetinfo'"'!=`""') + (`"`hetstat'"'!=`""') + (`"`ovstat'"'!=`""') > 1 { nois disp as err `"only one of {bf:hetinfo()}, {bf:hetstat()}, or {bf:ovstat()} is allowed"' exit 184 // hetstat and ovstat are synonyms (carried over from -admetan- ) } if `"`hetinfo'"'==`""' local hetinfo : copy local hetstat if `"`hetinfo'"'==`""' local hetinfo : copy local ovstat if `"`hetinfo'"'!=`""' local hetinfo `"hetinfo(`hetinfo')"' local hetstat local ovstat // Process -counts- options if `"`counts'"' != `""' { local group1_main : copy local group1 local group2_main : copy local group2 local 0 `", `counts'"' syntax [, COUNTS GROUP1(passthru) GROUP2(passthru) ] foreach opt in group1 group2 { if `"``opt''"'!=`""' & `"``opt'_main'"'!=`""' & `"``opt''"'!=`"``opt'_main'"' { disp `"{error}Note: Conflicting option {bf:`opt'()}; {bf:counts()} suboption will take priority"' } if `"``opt''"'==`""' & `"``opt'_main'"'!=`""' local `opt' : copy local `opt'_main local `opt'_main } } else local counts : copy local counts2 if `"`counts'"'!=`""' local counts `"counts(counts `group1' `group2')"' // counts(counts...) so that contents are never null local group1 local group2 // Process -eform- options cap nois CheckOpts, soptions opts(`opts_main') if _rc { if _rc==1 nois disp as err `"User break in {bf:metan.CheckOpts}"' else nois disp as err `"Error in {bf:metan.CheckOpts}"' c_local err noerr // tell main program not to also report an error in ParseFPlotOpts exit _rc } local eform `"`s(eform)'"' local log `"`s(log)'"' local summstat `"`s(summstat)'"' if `"`effect'"'==`""' local effect `"`s(effect)'"' // N.B. `s(effect)' contains automatic effect text from -eform-; `effect' contains user-specified text ** Main parse of options supplied directly to -(ipd)metan- local optlist0 GRaph HET BETWeen OVerall SUbgroup WARNing WT STATs BOX // "stand-alone" options: optionally-off local optlist1 CUmulative INFluence INTERaction EFFIcacy RFDist OVWt SGWt FORCE NOPR // "stand-alone" options: optionally-on (note "NOPR") local optlist2 PLOTID HETINFO RFLevel COUNTS BOXSCale EXTRALine // options requiring content within brackets (passthru) local optlist3 LCOLS RCOLS // options which cannot conflict local nooptlist0 foreach op of local optlist0 { local nooptlist0 `nooptlist0' no`op' } local optlist2p foreach op of local optlist2 { local optlist2p `optlist2p' `op'(passthru) } local optlist3p foreach op of local optlist3 { local optlist3p `optlist3p' `op'(passthru) } local 0 `", `s(options)' `counts' `ovwt' `sgwt' `hetinfo'"' syntax [, `nooptlist0' `optlist1' `optlist2p' `optlist3p' * ] local opts_main `"`macval(options)'"' if `"`forestplot'"'!=`""' { // Need to temp rename options which may be supplied as either "main options" or "forestplot options" // (N.B. `effect' should be part of `optlist2', but needs to be treated slightly differently) local optlist `optlist0' `optlist1' `optlist2' `optlist3' effect foreach opt of local optlist { local lopt = lower(`"`opt'"') local `lopt'_main : copy local `lopt' } ** Quick initial parse, to extract specific options and correct for synonyms etc. local 0 `", `forestplot'"' syntax [, EFFect(string asis) /// HETINFO(string) HETStat(string) OVStat(string) /// /* N.B. `hetstat' and `ovstat' are legacy synonyms for `hetinfo' */ OVWt SGWt OVWEIGHT SGWEIGHT /// /* synonyms */ COUNTS2 COUNTS(string asis) GROUP1(passthru) GROUP2(passthru) * ] /* ^^ modern syntax has "group1" and "group2" as sub-options to "count"; older syntax had them as separate options */ local opts_fplot `"`macval(options)'"' local sgwt = cond("`sgweight'"!="", "sgwt", "`sgwt'") // sgweight is a synonym (for compatibility with -metan9- ) local sgweight local ovwt = cond("`ovweight'"!="", "ovwt", "`ovwt'") // ovweight is a synonym (for consistency with sgweight) local ovweight if (`"`hetinfo'"'!=`""') + (`"`hetstat'"'!=`""') + (`"`ovstat'"'!=`""') > 1 { nois disp as err `"only one of {bf:hetinfo()}, {bf:hetstat()}, or {bf:ovstat()} is allowed"' exit 184 // hetstat and ovstat are synonyms (carried over from -admetan- ) } if `"`hetinfo'"'==`""' local hetinfo : copy local hetstat if `"`hetinfo'"'==`""' local hetinfo : copy local ovstat if `"`hetinfo'"'!=`""' local hetinfo `"hetinfo(`hetinfo')"' local hetstat local ovstat // Process -counts- options if `"`counts'"' != `""' { local group1_fplot : copy local group1 local group2_fplot : copy local group2 local 0 `", `counts'"' syntax [, COUNTS GROUP1(passthru) GROUP2(passthru) ] foreach opt in group1 group2 { if `"``opt''"'!=`""' & `"``opt'_fplot'"'!=`""' & `"``opt''"'!=`"``opt'_fplot'"' { disp `"{error}Note (referring to {bf:forestplot()} option): Conflicting option {bf:`opt'()}; {bf:counts()} suboption will take priority"' } if `"``opt''"'==`""' & `"``opt'_fplot'"'!=`""' local `opt' : copy local `opt'_fplot local `opt'_fplot } } else local counts : copy local counts2 if `"`counts'"'!=`""' local counts `"counts(counts `group1' `group2')"' // counts(counts...) so that contents are never null local group1 local group2 // Process -eform- for forestplot, and check for clashes/prioritisation cap nois CheckOpts `cmdname', soptions opts(`opts_fplot') if _rc { if _rc==1 nois disp as err `"User break in {bf:metan.CheckOpts}"' else nois disp as err `"Error in {bf:metan.CheckOpts}"' c_local err noerr // tell main program not to also report an error in ParseFPlotOpts exit _rc } ** Main parse of options supplied within -forestplot- option local 0 `", `s(options)' `counts' `ovwt' `sgwt' `hetinfo'"' syntax [, `nooptlist0' `optlist1' `optlist2p' `optlist3p' * ] local opts_fplot `"`macval(options)'"' if `"`summstat'"'!=`""' & `"`s(summstat)'"'!=`""' & `"`summstat'"'!=`"`s(summstat)'"' { nois disp as err `"Conflicting summary statistics supplied to {bf:metan} and to {bf:forestplot()}"' exit 184 } // Finalise locals & scalars as appropriate; forestplot options take priority local eform = cond(`"`s(eform)'"'!=`""', `"`s(eform)'"', cond(trim(`"`log'`s(log)'"')!=`""', `""', `"`eform'"')) local log = cond(`"`s(log)'"'!=`""', `"`s(log)'"', `"`log'"') local summstat = cond(`"`s(summstat)'"'!=`""', `"`s(summstat)'"', `"`summstat'"') if `"`effect'"'==`""' local effect `"`s(effect)'"' // N.B. `s(effect)' contains automatic effect text from -eform-; `effect' contains user-specified text } // `optlist0' and `optlist1': allowed to conflict, but forestplot will take priority foreach opt in `optlist0' `optlist1' { local lopt = lower(`"`opt'"') if `"``lopt''"'==`""' & `"``lopt'_main'"'!=`""' local `lopt' : copy local `lopt'_main if `"``lopt''"'!=`""' { if inlist("`opt'", "BOX", "FORCE") { local opts_fplot `"`macval(opts_fplot)' ``lopt''"' } else local opts_parsed `"`macval(opts_parsed)' ``lopt''"' } } // `optlist2': Same, but this time display warning for options requiring content within brackets (`optlist2') foreach opt in `optlist2' effect { local lopt = lower(`"`opt'"') if `"``lopt'_main'"'!=`""' { if `"``lopt''"'!=`""' { if `"``lopt''"'!=`"``lopt'_main'"' { disp `"{error}Note: Conflicting option {bf:`lopt'()}; {bf:forestplot()} suboption will take priority"' } } else local `lopt' : copy local `lopt'_main } // Don't add `effect' to opts_parsed; needed separately in main routine if `"``lopt''"'!=`""' & "`lopt'"!="effect" { if "`opt'"=="BOXSCale" { local opts_fplot `"`macval(opts_fplot)' ``lopt''"' } else local opts_parsed `"`macval(opts_parsed)' ``lopt''"' } } // `optlist3': these *cannot* conflict (also see `summstat' above) foreach opt in `optlist3' { local lopt = lower(`"`opt'"') if `"``lopt'_main'"'!=`""' { if `"``lopt''"'!=`""' { cap assert `"``lopt''"'==`"``lopt'_main'"' if _rc { nois disp as err `"Conflicting option {bf:`lopt'()} supplied to {bf:metan} and to {bf:forestplot()}"' exit 184 } local `lopt' } } if `"``lopt'_main'``lopt''"'!=`""' { local opts_parsed `"`macval(opts_parsed)' ``lopt'_main'``lopt''"' } } // May 2020: parse for options of the form "[plot]#opts" outside of forestplot() option // If found, exit with error // Note: do this *after* finalizing possible plotid() options in opts_main and in opts_fplot if `"`plotid'"'!=`""' { local 0 `", `plotid'"' syntax [, PLOTID(string) ] local 0 `plotid' syntax varname [, *] qui levelsof `varlist' if `touse' CheckPlotIDOpts, plotid(`r(levels)') `opts_main' } // Return locals sreturn clear sreturn local effect `"`effect'"' sreturn local eform `eform' sreturn local log `log' sreturn local summstat `summstat' sreturn local options `"`macval(opts_main)'"' sreturn local opts_fplot `"`macval(opts_fplot)'"' sreturn local opts_parsed `"`macval(opts_parsed)'"' end * CheckPlotIDOpts // May 2020: parse for options of the form "[plot]#opts" // If found, exit with error program define CheckPlotIDOpts syntax, [ PLOTID(numlist integer) * ] local badopts box diam point ci oline nline ociline rf rficline local 0 `", `options'"' foreach p of local plotid { local optlist foreach op of local badopts { local uop = upper("`op'") local optlist `optlist' `uop'`p'opts(passthru) } syntax [, `optlist' * ] foreach op of local badopts { if `"``op'`p'opts'"'!=`""' { nois disp as err _n `"Option {bf:`op'`p'opts()} may only be supplied as a sub-option to the {bf:forestplot()} option; see {help metan:help metan}"' exit 198 } } } end * CheckOpts // Based on the built-in _check_eformopt.ado, // but expanded from -eform- to general effect specifications. // This program is used by -metan- and -forestplot- , and also by -ipdmetan- // Not all aspects are relevant to all programs, // but easier to maintain just a single subroutine! // subroutine of ParseFPlotOpts program define CheckOpts, sclass syntax [name(name=cmdname)] [, soptions OPts(string asis) ESTEXP(string) ] // estexp(string), as could include equation term if "`cmdname'"!="" { _check_eformopt `cmdname', `soptions' eformopts(`opts') } else _get_eformopts, `soptions' eformopts(`opts') allowed(__all__) local summstat = cond(`"`s(opt)'"'==`"eform"', `""', `"`s(opt)'"') if "`summstat'"=="rrr" { local effect `"Risk Ratio"' // Stata by default refers to this as a "Relative Risk Ratio" or "RRR" local summstat rr // ... but in MA context most users will expect "Risk Ratio" } else if "`summstat'"=="nohr" { // nohr and noshr are accepted by _get_eformopts local effect `"Haz. Ratio"' // but are not assigned names; do this manually local summstat hr local logopt nohr } else if "`summstat'"=="noshr" { local effect `"SHR"' local summstat shr local logopt noshr } else local effect `"`s(str)'"' if "`estexp'"=="_cons" { // if constant model, make use of eform_cons_ti if available local effect = cond(`"`s(eform_cons_ti)'"'!=`""', `"`s(eform_cons_ti)'"', `"`effect'"') } local 0 `", `s(eform)'"' syntax [, EFORM(string asis) * ] local eform = cond(`"`eform'"'!=`""', "eform", "") // Next, parse `s(options)' to extract anything that wouldn't usually be interpreted by _check_eformopt // that is: mean differences (`smd', `wmd' with synonym `md'); `rd' (unless -binreg-); // `coef'/`log' and `nohr'/`noshr' (which all imply `log') // (N.B. do this even if a valid option was found by _check_eformopt, since we still need to check for multiple options) local 0 `", `s(options)'"' syntax [, COEF LOG NOHR NOSHR RD SMD WMD MD * ] // identify multiple options; exit with error if found opts_exclusive "`coef' `log' `nohr' `noshr'" `""' 184 if `"`summstat'"'!=`""' { opts_exclusive `"`summstat' `md' `smd' `wmd' `rr' `rd' `nohr' `noshr'"' `""' 184 } // if "nonstandard" effect option used else { if `"`md'`wmd'"'!=`""' { // MD and WMD are synonyms local effect WMD if `"`eform'"'!=`""' local effect `"exp(WMD)"' local summstat wmd } else if `"`smd'`rd'"'!=`""' { if "`smd'"!="" local effect SMD else if "`rd'"!="" local effect `"`"Risk Diff."'"' // May 2020: double quotes to ensure it ends up on a separate line from "95% CI" if `"`eform'"'!=`""' local effect `"exp(`effect')"' local summstat `smd'`rd' } else if "`nohr'"!="" { local effect `"Haz. Ratio"' local summstat hr local logopt nohr } else if "`noshr'"!="" { local effect `"SHR"' local summstat shr local logopt noshr } // now check against program properties and issue warning if "`cmdname'"!="" { local props : properties `cmdname' if "`cmdname'"=="binreg" local props `props' rd if !`:list summstat in props' { cap _get_eformopts, eformopts(`summstat') if _rc { disp `"{error}Note: option {bf:`summstat'} does not appear in properties of command {bf:`cmdname'}"' } } } } // log always takes priority over eform // ==> cancel eform if appropriate local log = cond(`"`coef'`logopt'"'!=`""', "log", "`log'") // `coef' is a synonym for `log'; `logopt' was defined earlier if `"`log'"'!=`""' { if inlist("`summstat'", "rd", "smd", "wmd") { nois disp as err "Log option only appropriate with ratio statistics" exit 184 } local eform } sreturn clear sreturn local logopt `coef'`logopt' // "original" log option sreturn local log `log' // either "log" or nothing sreturn local eform `eform' // either "eform" or nothing sreturn local summstat `summstat' // if `eform', original eform option sreturn local effect `"`effect'"' sreturn local options `"`macval(options)'"' end ********************************************************************* ** Setup tempvars // The "core" elements of `outvlist' are _ES, _seES, _LCI, _UCI, _WT and _NN // By default, these will be left behind in the dataset upon completion of -metan- // _ES, _seES, _LCI and _UCI are *always* on the linear/interval scale, for pooling // and so may need to be back-transformed to the original scale for display on-screen or in the forestplot. // Typically this just involves exponentiating; but for proportions using e.g. Freeman-Tukey, // the back-transform is too complex, so we create separate variables _Prop_ES, _Prop_LCI, _Prop_UCI // containing the proportion and CI on the original scale. // `tvlist' = list of elements of `outvlist' that need to be generated as *tempvars* (i.e. do not already exist) // (whilst ensuring that any overlapping elements in `invlist' and `outvlist' point to the same actual variables) program define SetupOutVList, sclass // Locals needed for reference, but not to be altered: syntax varlist [if] [in], [ TOUSE2(passthru) LOGRank PRoportion ILevel(cilevel) noINTeger * ] syntax varlist [if] [in], [ NPTS(string) ROWNAMES(namelist) * ] // do not return these options within s(options) marksample touse, novarlist tokenize `varlist' local params : word count `varlist' local opts_adm `"`macval(options)'"' // Optional "npts(varname)": only permitted with 2- or 3-element varlist AD; // that is, "ES, seES", "ES, LCI, UCI", or "OE, V" if `"`npts'"'!=`""' { if `params' > 3 | "`proportion'"!="" { if `"`touse2'"'!=`""' local orwith `" or with logrank (O-E & V) HR"' // if passed from -ipdmetan- nois disp as err `"option {bf:npts(}{it:varname}{bf:)} syntax only valid with generic inverse-variance model`orwith'"' nois disp as err `"maybe the option {bf:npts} was intended?"' exit 198 } local old_integer `integer' local 0 `"`npts'"' syntax varname(numeric) [, noPlot noINTeger] local nptsvar `varlist' if `"`integer'"'==`""' local integer `old_integer' if `"`integer'"'==`""' { cap assert int(`nptsvar')==`nptsvar' if `touse' if _rc { nois disp as err `"Non-integer counts found in {bf:npts()} option"' exit _rc } } if `"`plot'"'==`""' local nptsflag npts // send simple on/off option to BuildResultsSet (e.g. for forestplot) local _NN _NN } // Now assemble `tvlist' if `"`logrank'"'!=`""' { local arglist OE V `_NN' // OE and V supplied (optional `_NN') local tvlist _ES _seES _LCI _UCI // ...need to create everything (except _NN) } else if `"`proportion'"'!=`""' { local arglist succ _NN // n and N supplied (latter is _NN) local tvlist _ES _seES _LCI _UCI _CC // ...need to create everything (except _NN) including _CC } else if `params'==2 { local arglist _ES _seES `_NN' // `_ES' and `_seES' supplied (optional `_NN') local tvlist _LCI _UCI // `_LCI', `_UCI' need to be created (at `ilevel'%) } else if `params'==3 { local arglist _ES _LCI _UCI `_NN' // `_ES', `_LCI' and `_UCI' supplied (assumed 95% CI); optional `_NN' local tvlist _seES // `_seES' needs to be created if `ilevel'!=c(level) { // but if ilevel() option supplied, requesting coverage other than 95% ... local tvlist `tvlist' _LCI _UCI // ... then tempvars for _LCI, _UCI are needed too } } else { local tvlist _ES _seES _LCI _UCI _NN // `params'==4 or 6: need to create everything, including _NN... if `params'==4 local tvlist `tvlist' _CC // ...including _CC if `params'==4 } // Finally, _WT always needs to be generated as tempvar local tvlist `tvlist' _WT // If cumulative or influence, need to generate additional tempvars. // `xoutvlist' ("extra" outvlist) contains results of each individual analysis // to be printed to screen, displayed in forestplot and stored in saved dataset. // (plus Q, tausq, sigmasq, df from each analysis.) // Meanwhile `outvlist' contains effect sizes etc. for each individual *study*, as usual, // which will be left behind in the current dataset. // Nov 2021: a simplified version of `xoutvlist' is also needed if `summaryonly' and saving()/clear. // Return locals sreturn clear sreturn local tvlist `tvlist' sreturn local arglist `arglist' if `"`_NN'"'!=`""' { sreturn local nptsvar `nptsvar' sreturn local intopt `integer' } sreturn local options `"`macval(opts_adm)' `nptsflag'"' GetXRowNames `rownames', `opts_adm' // returns s(xrownames) end program define GetXRowNames, sclass syntax namelist(name=rownames) [ , CUmulative INFluence SUMMARYONLY SAVING(passthru) CLEAR CLEARSTACK * ] if `"`clearstack'"'!=`""' local clear clearstack // Note: a simplified version of `xoutvlist' is also needed if `summaryonly' and saving()/clear if `"`cumulative'`influence'"'!=`""' /// | (`"`summaryonly'"'!=`""' & !(`"`saving'"'==`""' & `"`clear'"'==`""')) { local toremove npts if `"`cumulative'`influence'"'==`""' { local toremove `toremove' eff se_eff eff_lci eff_uci } local xrownames : copy local rownames local xrownames : list xrownames - toremove local xrownames `xrownames' Q Qdf Q_lci Q_uci if `: list posof "tausq" in rownames' local xrownames `xrownames' sigmasq if `"`cumulative'`influence'"'!=`""' { local xrownames `xrownames' _WT2 } sreturn local xrownames `xrownames' } end * Program to process `study' and `by' labels // (called directly by metan.ado) program define ProcessLabels, sclass sortpreserve syntax varlist(numeric min=2 max=6) [if] [in], SORTBY(varlist) /// NEWSTUDY(name) NEWSTUDYLAB(name) NEWBY(name) NEWBYLAB(name) /// [ STUDY(string) LABTITLE(string asis) LABEL(string) BY(string) LCols(namelist) SUMMARYONLY INFluence * ] marksample touse, novarlist // `novarlist' option so that entirely missing/nonexistent studies/subgroups may be included local invlist `varlist' // list of "original" vars passed by the user to the program local opts_adm : copy local options ** Parse `by' // N.B. do this before `study' in case `by' is string and contains missings. // Stata sorts string missings to be *first* rather than last. if `"`by'"'!=`""' { local 0 `"`by'"' syntax name [, Missing ] // only a single (var)name is allowed cap confirm var `namelist' if _rc { nois disp as err `"error in option {bf:by()}: "' _c confirm var `namelist' } local _BY `namelist' // `"`_BY'"'!=`""' is a marker of `by' being present in the current data local bymissing `missing' } ** Now, parse ways of supplying study names // label([namevar=namevar], [yearvar=yearvar]), carried forward from -metan- v9 // Note, there is some inconsistency in the way this option is documented // option list says "yearvar", but Examples and code itself uses "yearid" // Here, either is permitted; but only "yearvar" is documented, to match with "namevar" if `"`label'"'!=`""' { if `"`study'"'!=`""' { nois disp as err `"Cannot specify both {bf:label()} and {bf:study()}; please choose just one"' exit 184 } // while loop taken directly from metan9.ado by Ross Harris: tokenize "`label'", parse("=, ") while "`1'"!="" { cap assert inlist(`"`1'"', "namevar", "yearvar", "yearid") if _rc local rc = _rc else { cap confirm var `3' if _rc & `: word count `3''==1 { nois disp as err `"error in option {bf:label()}: "' _c confirm var `3' exit _rc } local rc = _rc } if `rc' { nois disp as err `"Syntax of option {bf:label()} is {bf:label(}[{bf:namevar}={it:namevar}]{bf:,} [{bf:yearvar}={it:yearvar}]{bf:)}"' exit _rc } local `1' "`3'" mac shift 4 } if `"`yearid'"'!=`""' { if `"`yearvar'"'!=`""' { nois disp as err `"option {bf:label()} invalid;"' nois disp as err `"only one of {bf:yearvar} or {bf:yearid} is allowed"' exit 184 } local yearvar : copy local yearid } // put name/year variables into appropriate macros if `: word count `namevar' `yearvar''==1 local _STUDY `namevar' `yearvar' else { tempvar _STUDY cap confirm string var `namevar' if !_rc local namestr `namevar' else { tempvar namestr cap decode `namevar', gen(`namestr') if _rc==182 qui gen `namestr' = "" // if no value label else if _rc { decode `namevar', gen(`namestr') // otherwise force exit, with appropriate error message } qui replace `namestr' = strofreal(`namevar', `"`: format `namevar''"') if missing(`"`namestr'"') // ^^ added Aug 2020; if *some* values are labelled but *not* all, take the values themselves // -decode- replaces with missing if no label defined for a particular value // hence, use these lines regardless of whether a value label exists // if _rc==182 qui gen `namestr' = string(`namevar') // no value label } cap confirm string var `yearvar' if !_rc local yearstr `yearvar' else { tempvar yearstr cap decode `yearvar', gen(`yearstr') if _rc==182 qui gen `yearstr' = "" // if no value label else if _rc { decode `yearvar', gen(`yearstr') // otherwise force exit, with appropriate error message } qui replace `yearstr' = strofreal(`yearvar', `"`: format `yearvar''"') if missing(`yearstr') // ^^ added Aug 2020; if *some* values are labelled but *not* all, take the values themselves // -decode- replaces with missing if no label defined for a particular value // hence, use these lines regardless of whether a value label exists // if _rc==182 qui gen `yearstr' = string(`yearvar') // no value label } qui gen `_STUDY' = `namestr' + " (" + `yearstr' + ")" local namevarlab : variable label `namevar' if `"`namevarlab'"'==`""' local namevarlab `namevar' local yearvarlab : variable label `yearvar' if `"`yearvarlab'"'==`""' local yearvarlab `yearvar' label variable `_STUDY' `"`namevarlab' (`yearvarlab')"' } } // end if `"`label'"'!=`""' // If `study' not supplied: // First, look at `lcols' as per -metan- syntax proposed in Harris et al, SJ 2008 else if `"`study'"'==`""' { gettoken _STUDY lcols : lcols // remove _STUDY from lcols if `"`_STUDY'"'!=`""' { cap confirm var `_STUDY' if _rc { nois disp as err `"option {bf:label()} not supplied; variable {bf:`_STUDY'} in option {bf:lcols()} not found or ambiguous abbreviation"' exit _rc } markout `touse' `_STUDY', strok local slcol slcol // mark as being actually lcols() rather than label() or study(); used later for error message } // Else, start by assuming entire dataset is to be used // and remove any observations with no (i.e. missing) data in `invlist'. // (code fragment taken from _grownonmiss.ado) else { tokenize `invlist' qui gen byte `newstudy' = (`1'<.) if `touse' mac shift while "`1'" != "" { qui replace `newstudy' = `newstudy' + (`1'<.) if `touse' mac shift } qui replace `newstudy' = . if `newstudy' == 0 // set to missing for benefit of markout markout `touse' `newstudy' // now generate dummy numbering qui bysort `touse' (`sortby') : replace `newstudy' = _n if `touse' label variable `newstudy' "Study" local _STUDY `newstudy' } } // If study is supplied directly else { local 0 `"`study'"' syntax varname [, Missing ] local _STUDY `varlist' if `"`missing'"'==`""' markout `touse' `_STUDY', strok } // Dec 2020: If labelled-integer, format length may also have an effect on the displayed string // Hence, *always* save format length to apply to _LABELS later confirm variable `_STUDY' local sfmtlen = fmtwidth("`: format `_STUDY''") ** Check that `touse' is populated (after markout) qui count if `touse' if !r(N) { if `"`slcol'"'!=`""' local errtext `"in first {bf:lcols()} variable"' else local errtext `"in {bf:study()} variable"' if `"`_BY'"'!=`""' local errtext `"`errtext' or in {bf:by()} variable"' nois disp as err `"no valid observations `errtext'"' exit 2000 } local ns = r(N) ** Sort out value labels of `_STUDY' and/or `_BY' if either are string local rc_by = 1 if `"`_BY'"'!=`""' { cap confirm string variable `_BY' local rc_by = _rc if !_rc { qui gen long `newby' = . } // May 2020: also be careful with double, if there are ambiguities when recast to float if "`: type `_BY''"=="double" { tempvar bycheck qui clonevar `bycheck' = `_BY' qui recast float `bycheck', force qui tab `_BY' if `touse', m local r = r(r) qui tab `bycheck' if `touse', m cap assert `r' == r(r) if _rc { nois disp as err "Variable {bf:`_BY'} in option {bf:by()} has {help data_type} {bf:double}" nois disp as err " and has values which are ambiguous when {bf:{help recast}} to {bf:float}." nois disp as err "Please check, and re-evaluate or re-label if necessary" exit 184 } } } cap confirm string var `_STUDY' local rc_study = _rc if !_rc { qui gen long `newstudy' = . qui bysort `touse' (`sortby') : replace `newstudy' = _n if `touse' } // May 2020: also be careful with double, if there are ambiguities when recast to float else if "`: type `_STUDY''"=="double" { tempvar scheck qui clonevar `scheck' = `_STUDY' qui recast float `scheck', force qui tab `_STUDY' if `touse', m local r = r(r) qui tab `scheck' if `touse', m cap assert `r' == r(r) if _rc { nois disp as err "Variable {bf:`_STUDY'} has {help data_type} {bf:double}" nois disp as err " and has values which are ambiguous when {bf:{help recast}} to {bf:float}." nois disp as err "Please check, and re-evaluate or re-label if necessary" exit 184 } } // Now generate new label(s) if !`rc_study' | !`rc_by' { qui replace `touse' = !`touse' // place `touse' first sort `touse' `sortby' // studies of interest should now be the first `ns' observations local j = 0 // for `by' labels forvalues i = 1 / `ns' { // If `study' is string if !`rc_study' { local si = `_STUDY'[`i'] label define `newstudylab' `i' `"`si'"', add } // If `by' is string: // Check whether current `by' group has already been defined by a previous study (or studies) // If not, create it now. if !`rc_by' { local byi = `_BY'[`i'] summ `newby' if !`touse' & `_BY'==`"`byi'"', meanonly if !r(N) { local ++j qui replace `newby' = `j' if !`touse' & `_BY'==`"`byi'"' label define `newbylab' `j' `"`byi'"', add } } } // Apply variable and value label if !`rc_study' { local svarlab : variable label `_STUDY' // extract original variable label... if `"`svarlab'"'==`""' local svarlab `_STUDY' // ...or varname label variable `newstudy' `"`svarlab'"' label values `newstudy' `newstudylab' local _STUDY `newstudy' } if !`rc_by' { local byvarlab : variable label `_BY' // extract original variable label... if `"`byvarlab'"'==`""' local byvarlab `_BY' // ...or varname label variable `newby' `"`byvarlab'"' label values `newby' `newbylab' local _BY `newby' } } // Check that `_STUDY' and `_BY' are not identical confirm numeric variable `_STUDY' cap assert `"`_STUDY'"'!=`"`_BY'"' if _rc { nois disp as err `"the same variable cannot be used in both {bf:study()} and {bf:by()}"' exit 184 } ** Titles if `"`labtitle'"'==`""' { if `"`influence'"'!=`""' local omitted " omitted" local svarlab : variable label `_STUDY' // Note: `_STUDY' is guaranteed to exist (but might have no varlabel) if `"`svarlab'"'==`""' & `"`summaryonly'"'==`""' local svarlab Study local labtitle `"`svarlab'`omitted'"' if `"`_BY'"'!=`""' { confirm numeric variable `_BY' local byvarlab : variable label `_BY' if `"`byvarlab'"'==`""' local byvarlab Subgroup if `"`summaryonly'"'!=`""' local labtitle `"`byvarlab'"' // NEW Mar 2022 else local labtitle `"`byvarlab' and `svarlab'`omitted'"' } } // Return sreturn clear sreturn local by `_BY' sreturn local bymissing `bymissing' sreturn local study `_STUDY' sreturn local smissing `missing' sreturn local sfmtlen `sfmtlen' sreturn local lcols `lcols' sreturn local labtitle `"`labtitle'"' end * Program to parse inputted varlist structure and // - identify studies with insufficient data (`_USE'==2) // - check for validity // (called directly by metan.ado) // ...and then to parse models and modelling options // via subroutine "ProcessModelOpts" /* Syntax: a) binary data (4 vars): metan #events_research #nonevents_research #events_control #nonevents_control , ... b) cts data (6 vars): metan #N_research mean_research sd_research #N_control mean_control sd_control , ... c) logrank survival (OE & V) (2 vars): metan theta oe v, [NPTS(varname numeric] ... d) generic inverse-variance (2 vars): metan theta se_theta , [NPTS(varname numeric] ... e) generic inverse-variance with CI instead of SE (3 vars): metan theta lowerlimit upperlimit , [NPTS(varname numeric] ... */ program define ProcessData, sclass syntax varlist(numeric min=3 max=7 default=none) [if] [in] [, SUMMSTAT(name) TOUSE2(passthru) /// COHend GLAssd HEDgesg noSTANdard TRansform(string) FTT /// model options (`ftt' included for compatibility with -metaprop-) CORnfield EXact WOolf CItype(name) CIMEThod(name) /// individual study CI options/// /*options which can be checked against `summstat' and/or `params' for "quick wins", since not model-dependent*/ /// INTERaction EFORM LOG ZTOL(real 1.0x-1a) * ] local opts_adm `"`macval(options)'"' // if missing values in `invlist', set _USE==2 immediately gettoken _USE invlist : varlist marksample touse, novarlist foreach v of local invlist { qui replace `_USE' = 2 if `touse' & missing(`v') } // Now parse options needed for reference only; pass these back to main routine as-is local 0 `", `opts_adm'"' syntax [, BREslow TArone CMH CMHNocc CHI2 CC(passthru) noCC2 NPTS /// LOGRank PRoportion NOPR DENOMinator(string) noINTeger MH PETO * ] opts_exclusive `"`logrank' `proportion'"' `""' 184 // Parse explicitly-specified SMD/WMD options, and store in `summstat' opts_exclusive `"`cohend' `glassd' `hedgesg' `standard'"' `""' 184 if `"`cohend'`glassd'`hedgesg'"'!=`""' { if inlist("`summstat'", "", "smd") local summstat `cohend'`glassd'`hedgesg' else { nois disp as err `"Option {bf:`cohend'`glassd'`hedgesg'} incompatible with `=upper(`summstat')'s"' exit 184 } } else if `"`standard'"'!=`""' { if inlist("`summstat'", "", "wmd") local summstat wmd else { nois disp as err `"Option {bf:`standard'} incompatible with `=upper(`summstat')'s"' exit 184 } } else if "`summstat'"=="smd" local summstat cohend // default SMD is Cohen's d // Parse explicitly-specified `citype' options // [N.B. cornfield, exact, woolf were main options to -metan- so are also allowed here // however the preferred -metan- syntax is "citype()" ] if `"`cimethod'"'!=`""' { // added Mar 2020 to allow compatibility with -metaprop- syntax if `"`citype'"'!=`""' { nois disp as err `"only one of {bf:citype()} or {bf:cimethod()} is allowed"' exit 184 } local citype : copy local cimethod } opts_exclusive `"`cornfield' `exact' `woolf'"' `""' 184 local cimainopt `cornfield'`exact'`woolf' // marker as whether supplied as a "main" option (cf -metan-) local 0 `", `citype'"' // now parse preferred "citype()" syntax syntax [, CORnfield EXact WOolf * ] cap assert `: word count `cimainopt' `cornfield' `exact' `woolf' `options'' <= 1 if _rc { nois disp as err `"Conflict between options {bf:citype(`citype')} and {bf:`cimainopt'}"' exit _rc } local citype `citype'`cimainopt' ** Now begin parsing `invlist' tokenize `invlist' cap assert "`7'" == "" if _rc { nois disp as err "Too many variables specified" exit _rc } if "`6'"=="" { // input is generic inverse-variance (2 or 3 vars) // or 2 vars with options -logrank- or -proportion- if "`4'"=="" { // incompatible options foreach opt in mh peto breslow tarone cmh cmhnocc { cap assert `"``opt''"' == `""' if _rc { nois disp as err `"Option {bf:`opt'} is not appropriate without 2x2 count data"' exit 184 } } if inlist("`summstat'", "wmd", "cohend", "glassd", "hedgesg") { nois disp as err "Specified method of constructing effect size {bf:`summstat'} is incompatible with the data" exit 184 } // input is HR logrank (2 vars: OE & V) if "`logrank'" != "" { cap assert "`3'"=="" & "`2'"!="" if _rc { nois disp as err "Option {bf:logrank} is only appropriate with two-variable syntax" exit 184 } if "`npts'"!="" { // [MOVED HERE AUG 2021] nois disp as err _n `"Option {bf:npts} is not appropriate with logrank OE & V data"' nois disp as err `"maybe the option {bf:npts(}{it:varname}{bf:)} was intended?"' exit 198 } local params = 2 local summstat hr local effect `"Haz. Ratio"' args _OE _V cap assert `_V'>=0 if `touse' if _rc { nois disp as err "Variances cannot be negative" exit _rc } foreach opt in cc cc2 { cap assert `"``opt''"' == `""' if _rc { if `"`opt'"'==`"cc2"' local opt nocc nois disp as err `"Option {bf:`opt'} is not appropriate without proportion or 2x2 count data"' exit 184 } } // Identify studies with insufficient data (`_USE'==2) qui replace `_USE' = 2 if `touse' & `_V'==0 qui replace `_USE' = 2 if `touse' & sqrt(`_V') < `ztol' } // input is proportions, n & N else if "`proportion'"!="" { cap assert "`3'"=="" & "`2'"!="" if _rc { nois disp as err "Option {bf:proportion} is only appropriate with two-variable syntax" exit 184 } local params = 2 if "`denominator'"=="" local effect "Proportion" else { cap confirm number `denominator' if _rc { nois disp as err `"`denominator' found where number expected in option {bf:denominator(#)}"' exit 198 } cap assert `denominator' > 0 if _rc { nois disp as err `"Error in option {bf:denominator(#)}: value must be greater than zero"' exit 198 } if `denominator'==1 local effect "Proportion" else if `denominator'==100 local effect "Percentage" else local effect `"Events per `denominator' obs."' } if "`nopr'"!="" local effect "Transformed proportion" args succ _NN cap assert `succ'<=`_NN' if `touse' & !missing(`succ') if _rc { nois disp as err "Number of events must be at most equal to the sample size" exit _rc } if "`integer'"=="" { cap { assert int(`succ')==`succ' if `touse' assert int(`_NN')==`_NN' if `touse' } if _rc { nois disp as err "Non-integer cell counts found; use the {bf:nointeger} option if appropriate" exit _rc } } /* else if !inlist(`"`citype'"', `""', `"wald"') { nois disp as err `"Cannot specify {bf:citype(`citype')} with {bf:nointeger}"' exit 198 } */ // DF July 2021: Removed; since CIs for proportions may have to be generated manually anyway (see GenConfIntsPr) // there is no real need for this restriction cap assert `succ'>=0 & `_NN'>=0 if `touse' if _rc { nois disp as err "Non-positive cell counts found" exit _rc } if `"`ftt'"'!=`""' { if `"`transform'"'!=`""' { nois disp as err `"Cannot specify both {bf:ftt} and {bf:transform()}"' exit 184 } local transform ftt } local 0 `transform' syntax [name(name=transform id="transform" ) ] [, N(string) POVERV(real 2) Arithmetic Geometric Harmonic IVariance INVVariance ] // N.B. `n' is an undocumented generalisation of `arithmetic' | `geometric' | `harmonic' // Jan 2021: added Barendregt/Doi's suggested "inverse-variance"-based back-transform local 0 `", `transform'"' syntax [, FTT FTukey ARcsine ASine DOUblearcsine LOGIT ] if "`ftt'"!="" local ftukey ftukey if "`doublearcsine'"!="" local ftukey ftukey if "`asine'"!="" local arcsine arcsine opts_exclusive `"`ftukey' `arcsine' `logit'"' transform 184 local summstat `ftukey'`arcsine'`logit' if "`summstat'"=="ftukey" { if `"`n'"'!=`""' { cap { confirm number `n' assert `n' > 0 & !missing(`n') } if _rc { nois disp as err `"In option {bf:transform(ftukey, n(}{it:#}{bf:))}, {it:#} must be > 0 and non-missing"' exit 198 } if `"`arithmetic'`geometric'`harmonic'`ivariance'`invvariance'"'!=`""' { local erropt : word 1 of `arithmetic' `geometric' `harmonic' `ivariance' `invvariance' nois disp as err `"option {bf:transform()} invalid;"' nois disp as err `"only one of {bf:n(}{it:#}{bf:)} or {bf:`erropt'} is allowed"' exit 184 } } else { opts_exclusive `"`arithmetic' `geometric' `harmonic' `ivariance' `invvariance'"' transform 184 local n `arithmetic'`geometric'`harmonic'`ivariance'`invvariance' if `"`n'"'==`"invvariance"' local n ivariance // these are synonyms } if `"`n'"'==`"ivariance"' { cap { confirm number `poverv' assert `poverv' >= 0 & !missing(`poverv') } if _rc { nois disp as err `"In option {bf:transform(ftukey, ivariance poverv(}{it:#}{bf:))}, {it:#} must be >= 0 and non-missing"' exit 198 } } local tnopt `"tn(`n') poverv(`poverv')"' } else { if `"`n'"'!=`""' { nois disp as err `"option {bf:transform()} invalid;"' nois disp as err `"option {bf:n(}#{bf:)} only allowed with {bf:transform(ftukey)}"' exit 198 } if `"`arithmetic'`geometric'`harmonic'`ivariance'`invvariance'"'!=`""' { local erropt : word 1 of `arithmetic' `geometric' `harmonic' `ivariance' `invvariance' nois disp as err `"option {bf:transform()} invalid;"' nois disp as err `"option {bf:`erropt'} only allowed with {bf:transform(ftukey)}"' exit 198 } if "`summstat'"=="" local summstat pr } // Identify studies with insufficient data (`_USE'==2) qui replace `_USE' = 2 if `touse' & `_NN' < 2 // Note: with proportions, `citype' *must* be sent to -cii- , so it will pick up any invalid citypes // (set default to -wilson- , as with -metaprop- ) if `"`citype'"'==`""' local citype wilson } // input is generic inverse-variance (2 or 3 vars) else { if "`transform'"!="" { nois disp as err "Option {bf:transform()} only valid with {bf:proportion}" exit 184 } foreach opt in cc cc2 { cap assert `"``opt''"' == `""' if _rc { if `"`opt'"'==`"cc2"' local opt nocc nois disp as err `"Option {bf:`opt'} is not appropriate without proportion or 2x2 count data"' exit 184 } } if "`npts'"!="" { // [MOVED HERE AUG 2021] nois disp as err _n `"Option {bf:npts} is not appropriate without proportion or 2x2 count data"' nois disp as err `"maybe the option {bf:npts(}{it:varname}{bf:)} was intended?"' exit 198 } // citype cap assert !inlist("`citype'", "cornfield", "exact", "woolf") if _rc { if `"`cimainopt'"'!=`""' { nois disp as err `"Option {bf:`citype'} is not appropriate without 2x2 count data"' } else nois disp as err `"{bf:citype(`citype')} is not appropriate without 2x2 count data"' exit 184 } // Identify studies with insufficient data (`_USE'==2) if "`3'"=="" { // input is ES + SE local params = 2 args _ES _seES cap assert `_seES'>=0 if `touse' if _rc { nois disp as err "Standard errors cannot be negative" exit _rc } qui replace `_USE' = 2 if `touse' & `_seES'==0 qui replace `_USE' = 2 if `touse' & 1/`_seES' < `ztol' } else { // input is ES + 95% CI local params = 3 args _ES _LCI _UCI qui replace `_USE' = 2 if `touse' & float(`_LCI')==float(`_UCI') cap assert `_UCI'>=`_ES' & `_ES'>=`_LCI' if `touse' & `_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 } cap assert `_UCI'>=`_LCI' if `touse' & `_USE'==2 & missing(`_ES') & !missing(`_LCI') & !missing(`_UCI') /// & !(float(`_LCI')==float(`_UCI')) 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 } qui replace `_USE' = 2 if `touse' & 2*invnormal(.975)/(`_UCI' - `_LCI') < `ztol' } } } // end of inverse-variance setup // input is 2x2 tables else { cap assert "`5'"=="" if _rc { nois disp as err "Invalid number of variables specified" exit _rc } local params = 4 args e1 f1 e0 f0 // events, non-events in trt group; events, non-events in ctrl group (a.k.a. a b c d) if "`integer'"=="" { cap { assert int(`e1')==`e1' if `touse' assert int(`f1')==`f1' if `touse' assert int(`e0')==`e0' if `touse' assert int(`f0')==`f0' if `touse' } if _rc { nois disp as err "Non-integer cell counts found; use the {bf:nointeger} option if appropriate" exit _rc } } else if !inlist(`"`citype'"', `""', `"normal"') { nois disp as err `"Cannot specify {bf:citype(`citype')} with {bf:nointeger}"' exit 198 } cap assert `e1'>=0 & `f1'>=0 & `e0'>=0 & `f0'>=0 if `touse' if _rc { nois disp as err "Non-positive cell counts found" exit _rc } // citype cap assert !inlist("`citype'", "cornfield", "exact", "woolf") if _rc { if !inlist("`summstat'", "or", "") { if `"`cimainopt'"'!=`""' { nois disp as err `"Option {bf:`citype'} is only compatible with odds ratios"' } else nois disp as err `"Option {bf:citype(`citype')} is only compatible with odds ratios"' exit 184 } else if "`summstat'"=="" { if "`citype'"=="cornfield" { disp `"{error}Note: Cornfield-type confidence intervals specified; odds ratios assumed"' } else if "`citype'"=="exact" { disp `"{error}Note: Exact confidence intervals specified; odds ratios assumed"' } else disp `"{error}Note: Woolf-type confidence intervals specified; odds ratios assumed"' local summstat or local effect `"Odds Ratio"' } } if "`chi2'"!="" { if !inlist("`summstat'", "or", "") { nois disp as err `"Option {bf:chi2} is only compatible with odds ratios"' exit 184 } else if "`summstat'"=="" { disp `"{error}Note: Chi-squared option specified; odds ratios assumed"' local summstat or local effect `"Odds Ratio"' } } foreach opt in breslow tarone cmh cmhnocc { if "``opt''"!="" { if !inlist("`summstat'", "or", "") { nois disp as err `"Option {bf:`opt'} is only compatible with odds ratios"' exit 184 } else if "`summstat'"=="" { local opttxt = cond("`opt'"=="breslow", "Breslow-Day homogeneity test", /// cond("`opt'"=="tarone", `"Breslow-Day-Tarone homogeneity test"', /// cond(inlist("`opt'", "cmh", "cmhnocc"), "Cochran-Mantel-Haenszel test", ""))) disp `"{error}`opttxt' specified; odds ratios assumed"' local summstat or local effect `"Odds Ratio"' } } } if "`peto'"!="" { if !inlist("`summstat'", "or", "") { nois disp as err "Peto method option can only be used with odds ratios" exit 184 } else if "`summstat'"=="" { disp `"{error}Note: Peto method specified; odds ratios assumed"' local summstat or local effect `"Odds Ratio"' } local chi2 chi2 } if `"`logrank'`proportion'"'!=`""' { nois disp as err "Option {bf:`logrank'`proportion'} is only appropriate with two-variable syntax" exit 184 } if "`transform'"!="" { nois disp as err "Option {bf:transform()} only valid with {bf:proportion}" exit 184 } if inlist("`summstat'", "hr", "shr", "tr") { nois disp as err "Time-to-event outcome types are incompatible with count data" exit 184 } else if inlist("`summstat'", "wmd", "cohend", "glassd", "hedgesg") { nois disp as err "Continuous outcome types are incompatible with count data" exit 184 } ** Average event rate (binary outcomes only) // (do this before any 0.5 adjustments or excluding 0-0 studies) tempname e_sum summ `e1' if `touse', meanonly scalar `e_sum' = cond(r(N), r(sum), .) summ `f1' if `touse', meanonly local tger = cond(r(N), `e_sum'/(`e_sum' + `r(sum)'), .) summ `e0' if `touse', meanonly scalar `e_sum' = cond(r(N), r(sum), .) summ `f0' if `touse', meanonly local cger = cond(r(N), `e_sum'/(`e_sum' + `r(sum)'), .) // Find studies with insufficient data (`_USE'==2) qui replace `_USE' = 2 if `touse' & (`e1' + `f1')*(`e0' + `f0')==0 // No data AT ALL in at least one arm (i.e. `r1'==0 or `r0'==0) /* if "`summstat'"!="rd" { // i.e. `r1'==0 or `r0'==0 // M-H RR: double-zero *non*-event (i.e. ALL events in both arms; unusual but not impossible) is OK if "`summstat'"=="rr" qui replace `_USE' = 2 if `touse' & `e1' + `e0'==0 // Else: any double-zero else qui replace `_USE' = 2 if `touse' & (`e1' + `e0'==0 | `f1' + `f0'==0) } */ } // end of binary variable setup // log only allowed if OR, RR, RRR, HR, SHR, TR if "`log'"!="" { cap assert (`params'==4 & "`summstat'"!="rd") | "`logrank'"!="" | "`touse2'"!="" if _rc { disp `"{error}{bf:log} may only be specified with 2x2 count data or log-rank HR; option will be ignored"' local log } } } // end of all non-6 variable setup if "`6'"!="" { // log not allowed if "`log'"!="" { disp `"{error}{bf:log} may only be specified with 2x2 count data or log-rank HR; option will be ignored"' local log } local params = 6 args n1 mean1 sd1 n0 mean0 sd0 // input is form N mean SD for continuous outcome data if "`integer'"=="" { cap assert int(`n1')==`n1' & int(`n0')==`n0' if `touse' if _rc { nois disp as err "Non integer sample sizes found" exit _rc } } cap assert `n1'>=0 & `n0'>=0 if `touse' if _rc { nois disp as err "Non-positive sample sizes found" exit _rc } cap assert `sd1'>=0 & `sd0'>=0 if `touse' if _rc { nois disp as err "Standard errors cannot be negative" exit _rc } foreach opt in mh peto breslow cc cc2 chi2 tarone cmh cmhnocc { cap assert `"``opt''"' == `""' if _rc { nois disp as err `"Option {bf:`opt'} is not appropriate without 2x2 count data"' exit 184 } } if `"`logrank'`proportion'"'!=`""' { nois disp as err "Option {bf:`logrank'`proportion'} is only appropriate with two-variable syntax" exit 184 } if "`transform'"!="" { nois disp as err "Option {bf:transform()} only valid with {bf:proportion}" exit 184 } // citype cap assert !inlist("`citype'", "cornfield", "exact", "woolf") if _rc { if `"`cimainopt'"'!=`""' { nois disp as err `"Option {bf:`citype'} is not appropriate without 2x2 count data"' } else nois disp as err `"Note: {bf:citype(`citype')} is not appropriate without 2x2 count data"' exit 184 } // summstat cap assert inlist("`summstat'", "", "wmd", "cohend", "glassd", "hedgesg") if _rc { nois disp as err "Invalid specifications for combining trials" exit 184 } if "`summstat'"=="" { local summstat cohend // default is Cohen's d standardised mean difference local effect SMD } // Find studies with insufficient data (`_USE'==2) qui replace `_USE' = 2 if `touse' & `n1' < 2 | `n0' < 2 qui replace `_USE' = 2 if `touse' & `sd1'==0 | `sd0'==0 } // end of 6-var set-up qui count if `touse' & `_USE'==1 if !r(N) exit 2000 ** Identify models // Process meta-analysis modelling options // (random-effects, test & het stats, etc.) // Could be given in a variety of ways, e.g. stand-alone options, random(), second(), model() etc. cap nois ProcessModelOpts, `opts_adm' `tnopt' summstat(`summstat') /*`summorig'*/ params(`params') if _rc { if `"`err'"'==`""' { if _rc==1 nois disp as err `"User break in {bf:metan.ProcessModelOpts}"' else nois disp as err `"Error in {bf:metan.ProcessModelOpts}"' } c_local err noerr // tell main program not to also report an error in ProcessData exit _rc } // Note: sclass is cleared by ProcessModelOpts // ...and since sclass is static, no need to repeat "sreturn local" here /* sreturn local rownames `s(rownames)' sreturn local modellist `s(modellist)' sreturn local teststatlist `s(teststatlist)' sreturn local qstat `s(qstat)' sreturn local wgtoptlist `s(wgtoptlist)' // to send to DrawTableAD sreturn local labelopts `"`s(labelopts)'"' // [Nov 2020] list of labelling options local m = `s(m)' forvalues j = 1 / `m' { sreturn local model`j'opts `"`s(model`j'opts)'"' } */ if `params'==4 { if `"`s(summnew)'"'==`"or"' { // corrective macro: only applies to 2x2 count data; is either "or" or nothing local summstat or local effect `"Odds Ratio"' } else if `"`summstat'"'==`""' { local summstat rr local effect `"Risk Ratio"' } // Finalize studies with insufficient data (`_USE'==2) // (following possible corrective macro `summnew') if "`summstat'"!="rd" { // RR: double-zero *non*-event (i.e. ALL events in both arms; unusual but not impossible) is OK if "`summstat'"=="rr" qui replace `_USE' = 2 if `touse' & `e1' + `e0'==0 // Else: any double-zero else qui replace `_USE' = 2 if `touse' & (`e1' + `e0'==0 | `f1' + `f0'==0) } // If `params'==4, default to eform unless Risk Diff... if `"`summstat'"'!=`"rd"' & `"`log'"'==`""' local eform eform } // ...similarly, if `logrank' then default to log else if `"`logrank'"'!=`""' & `"`eform'"'==`""' local log log // summstat should be NON-MISSING *UNLESS* "generic" es/se if `params' > 3 | `"`logrank'"'!=`""' { assert `"`summstat'"'!=`""' } // Finalize citype if inlist(`"`citype'"', `""', `"z"') local citype normal sreturn local effect `"`effect'"' sreturn local summstat `summstat' sreturn local params `params' sreturn local citype `citype' sreturn local eform `eform' sreturn local log `log' sreturn local interaction `interaction' if `params'==4 { sreturn local tger `tger' sreturn local cger `cger' } end ***************************************************************** * Parse meta-analysis modelling options (incl. random-effects) // compatibility, error checking // (called directly by metan.ado) // Note: First part updated Nov 2020 for restructuring of first() / second() options program define ProcessModelOpts, sclass syntax, PARAMS(passthru) [ SUMMSTAT(passthru) TN(passthru) POVERV(passthru) /// /// /* Test (`teststat') and heterogeneity (`hetstat') statistics */ T Z CHI2 CMH CMHNocc BREslow TArone COCHranq /// /// /* Other "global" options, for passing to ParseModel */ CC(string) noCC2 WGT(passthru) * ] ** First, sort out: // - legacy -metan- options first() and second() // - options permitted outside of model() option (either legacy -metan- or otherwise) cap nois ProcessFirstSecond, `params' `options' if _rc { if _rc==1 nois disp as err `"User break in {bf:metan.ProcessFirstSecond}"' else nois disp as err `"Error in {bf:metan.ProcessFirstSecond}"' c_local err noerr // tell -metan- not to also report an "error in metan.ProcessModelOpts" exit _rc } local opts_adm `"`s(options)'"' local model `"`s(model)'"' local olduser `s(olduser)' // Internal macro lists local teststat `t' `z' `chi2' `cmh' `cmhnocc' local qstat `breslow' `tarone' `cochranq' opts_exclusive `"`teststat'"' `""' 184 opts_exclusive `"`qstat'"' `""' 184 if `"`cc'"'!=`""' local ccopt `"cc(`cc')"' else local ccopt `cc2' // Now parse options needed for reference only; pass these back to main routine as-is local 0 `", `opts_adm'"' syntax [, RFDist CUmulative INFluence LOGRank PRoportion * ] ***************** * Parse model() * ***************** // Multiple models, separated with a backslash // [Nov 2020] use gettoken ... parse(",") instead of strpos(",") local m = 1 if `"`model'"'!=`""' { gettoken model`m' rest : model, parse("\") bind if trim(`"`model`m''"')=="null" local model`m' // remove temporary "null" marker gettoken foo comma : model`m', parse(",") bind if `"`comma'"'==`""' local model`m' `model`m'', // add comma if no options while `"`rest'"'!=`""' { gettoken bs rest2 : rest, parse("\") bind cap assert "`bs'"=="\" if _rc { nois disp as err `"Multiple models or heterogeneity estimators must be specified in the following form (see {help metan_model}):"' nois disp as err `" {bf:model(} {it:model} [ {bf:\} {it:model} [ {bf:\} {it:model} [...]]] {bf:)}"' exit 198 } if `"`rest2'"'==`""' continue, break else { local ++m gettoken model`m' rest : rest2, parse("\") bind gettoken foo comma : model`m', parse(",") bind if `"`comma'"'==`""' local model`m' `model`m'', // add comma if no options } } } else local model1 , // add comma (so that ParseModel doesn't complain) // Now process each model in turn using ParseModel to return "canonical" form forvalues j = 1 / `m' { // [Nov 2020:] local first = cond(`j'==1, "first", "") // marker of this model being the first/main/primary model cap nois ParseModel `model`j'' `summstat' `params' `logrank' `proportion' `tn' `poverv' `first' /// globalopts(`teststat' `wgt' `ccopt' `opts_adm') if _rc { if _rc==1 nois disp as err `"User break in {bf:metan.ParseModel}"' else nois disp as err `"Error in {bf:metan.ParseModel}"' c_local err noerr // tell -metan- not to also report an "error in metan.ProcessModelOpts" exit _rc } cap assert `"`s(model)'"'!=`""' if _rc { nois disp as err "No model found" exit 198 } // corrective macro: only applies to 2x2 count data; is either "or" or nothing if `"`s(summnew)'"'!=`""' { assert `"`summstat'"'==`""' local summnew `s(summnew)' local summstat summstat(`summnew') } // standard macros local model`j'opts `"`s(modelopts)'"' if `: list posof "kroger" in model`j'opts' local kroger kroger // for later error messages e.g. relating to predictive interval // labelling macro local labelopts `"`labelopts' label`j'opt(`s(labelopt)')"' // [Nov 2020:] user-supplied effect sizes if `"`s(extralabel)'"'!=`""' { local labelopts `"`labelopts' extra`j'opt(`s(extralabel)')"' } if `"`s(userstats)'"'!=`""' { local labelopts `"`labelopts' user`j'stats(`s(userstats)')"' local user`j'stats `s(userstats)' // for -return historical- } local modellist `modellist' `s(model)' local teststatlist `teststatlist' `s(teststat)' // Error prompts local gl_error `gl_error' `s(gl_error)' } // collect remaining options local opts_adm `"`s(opts_adm)'"' // Compare `qstat' to `model1' gettoken model1 : modellist if "`model1'"=="mh" { if "`qstat'"=="" local qstat mhq // default if M-H method } else if inlist("`qstat'", "breslow", "tarone") { nois disp as err "cannot specify {bf:`qstat'} heterogeneity option without Mantel-Haenszel odds ratios" exit 184 } if "`model1'"=="peto" { if "`qstat'"=="" local qstat petoq // default if Peto method } if "`qstat'"=="" local qstat cochranq // default otherwise // Display other error prompts local gl_error : list uniq gl_error foreach opt of local gl_error { if `m'==1 { nois disp as err "Option {bf:`opt'} is not compatible with model {bf:`modellist'}" exit 184 } disp `"{error}Note: global option {bf:`opt'} is not applicable to all models; local defaults will apply"' } if "`rfdist'"!="" { if `"`cumulative'"'!=`""' { nois disp as err `"Options {bf:cumulative} and {bf:rfdist} are not compatible"' exit 184 } if `"`influence'"'!=`""' { nois disp as err `"Options {bf:influence} and {bf:rfdist} are not compatible"' exit 184 } // Note: cumulative and influence have not yet been tested for exclusivity, hence the repetition here local nopredint mh iv peto bt hc ivhet qe mu pl user if `"`: list modellist - nopredint'"'==`""' | (`m'==1 & "`kroger'"!="") { disp `"{error}Note: predictive interval cannot be estimated under "' _c if `m'==1 disp `"{error}the specified model; {bf:rfdist} will be ignored"' else disp as err `"{error}any of the specified models; {bf:rfdist} will be ignored"' local rfdist } else if `"`: list modellist & nopredint'"'!=`""' | "`kroger'"!="" { disp as err `"{error}Note: predictive interval cannot be estimated for all models"' } } *********************** * Initialise rownames * *********************** // of matrices to hold overall/subgroup pooling results // ... and "parametrically-defined Isq" -based heterogeneity if specified and appropriate [Sep 2020] // (c.f. r(table) after regression) local rownames eff se_eff eff_lci eff_uci if "`proportion'"!="" { local 0 `", `summstat'"' syntax, SUMMSTAT(name) if "`summstat'"!="pr" local rownames `rownames' prop_eff prop_lci prop_uci } // effect size; std. err.; conf. limits; no. pts.; critical value // plus, add z by default as a fall-back e.g. if t-dist requested but only 1 study; can always be removed later local rownames `rownames' npts crit z // test statistics: remove duplicates, then use set ordering: z, t, chi2, u local rowtest : copy local teststatlist local rowtest : list uniq rowtest foreach el in t chi2 u { if `: list el in rowtest' local rownames `rownames' `el' // test statistics, in pre-defined order } if `: list posof "t" in rowtest' local rownames `rownames' df // df for t (including Kenward-Roger) local rownames `rownames' pvalue // p-value if "`logrank'"!="" | `: list posof "peto" in modellist' local rownames `rownames' OE V // logrank and Peto OR only if `: list posof "mu" in modellist' local rownames `rownames' phi // multiplicative heterogeneity model only [Sep 2023] local UniqModels : list uniq modellist local NoTau2Models mh peto mu iv local UniqModels : list UniqModels - NoTau2Models if `"`UniqModels'"'!=`""' local rownames `rownames' tausq // ^^ *unless* all models are common-effect (M-H, Peto, IV or MU), include tausq and sigmasq // [Sep 2020:] // Note: Heterogeneity stats derived from the *data*, independently of model (i.e. Q Qdf H Isq) // are returned in r() rather than stored in a matrix // "parametrically-defined Isq" -based heterogeneity values, if requested [i.e. derived from Isq = tsq/(tsq+sigmasq) ] // are stored in matrix r(hetstats) [and byhet1...byhet`nby' for subgroups] // (plus tsq + CI itself) // model-based tausq confidence intervals local RefModList mp pmm ml pl reml bt dlb if `"`: list UniqModels & RefModList'"'!=`""' { local rownames `rownames' tsq_lci tsq_uci } if "`rfdist'"!="" { local rownames `rownames' rflci rfuci // if predictive distribution if "`proportion'"!="" & "`summstat'"!="pr" local rownames `rownames' prop_rflci prop_rfuci } // Return options sreturn clear sreturn local options `"`macval(opts_adm)'"' // non model-specific options sreturn local rownames `rownames' sreturn local modellist `modellist' sreturn local teststatlist `teststatlist' sreturn local qstat `qstat' sreturn local summnew `summnew' // corrective macro: only applies to 2x2 count data; is either "or" or nothing sreturn local labelopts `"`labelopts'"' // Nov 2020 forvalues j = 1 / `m' { sreturn local model`j'opts `"`model`j'opts'"' } sreturn local m `m' // Internal markers if "`wgt'"!="" { sreturn local userwgt userwgt // marker of user-defined weights, c.f. previous versions of -metan- } sreturn local olduser `olduser' // marker that "legacy" -metan- options first() and/or second() were supplied // June 2020: Return historical if `"`user2stats'"'!=`""' { tokenize `user2stats' sreturn local ES_2 `1' sreturn local ci_low_2 `2' sreturn local ci_upp_2 `3' } if `"`user1stats'"'!=`""' { tokenize `user1stats' sreturn local ES `1' sreturn local ci_low `2' sreturn local ci_upp `3' } end // Process legacy -metan- options first() and second() // Display "legacy" error messages etc. if necessary, and return as "canonical" form // This subroutine is called by ProcessModelOpts program define ProcessFirstSecond, sclass *** (1) Legacy -metan- options first() and second() *** syntax, [ FIRST(string asis) FIRSTSTATS(string asis) SECOND(string asis) SECONDSTATS(string asis) * ] local opts_adm `"`macval(options)'"' if `"`second'"'!=`""' { local olduser olduser // marker that "legacy" -metan- options first() and/or second() were supplied gettoken _ES2_ rest : second cap confirm number `_ES2_' if !_rc { // if user-defined pooled effect gettoken _LCI2_ rest : rest gettoken _UCI2_ desc : rest cap { confirm number `_LCI2_' confirm number `_UCI2_' assert `_LCI2_' <= `_ES2_' assert `_UCI2_' >= `_ES2_' } if _rc { nois disp as err `"Error in option {bf:second()}: must supply user-defined main analysis in the format:"' nois disp as err " {it:ES lci uci desc}" exit 198 } // [Nov 2020:] // "gettoken _UCI2_ desc" leaves a space as the first character of "`desc'" local desc = ltrim(`"`desc'"') local second `"`_ES2_' `_LCI2_' `_UCI2_', label(`desc') extralabel(`secondstats')"' } // Else, assume that "`second'" is a model name... else { if "`rest'"!="" { nois disp as err `"Error in option {bf:second()}: must supply user-defined main analysis in the format:"' nois disp as err " {it:ES lci uci desc}" exit 198 } // ...in which case it must be fixed (common), random, mh, or peto local 0 `", `second'"' cap syntax [, FIXEDI FIXED IVariance INVVariance IVCommon COMMON RANDOMi MHaenszel PETO ] if _rc { nois disp as err "Option {bf:second()} can be one of {bf:fixed}|{bf:{ul:iv}common}, {bf:random}, {bf:{ul:mh}aenzel} or {bf:peto}" nois disp as err " or a user-defined estimate with confidence intervals in the format: {it:ES lci uci desc}" exit 198 } if `"`secondstats'"'!=`""' { disp `"{error}Note: {bf:secondstats()} is only valid with {bf:second(}{it:ES lci uci desc}{bf:)}, so will be ignored"' local secondstats } if "`second'"=="randomi" local second random // Jan 2021: "randomi" is not understood by model() // Use existing ("legacy") error message from -metan- if `"`first'"'!=`""' { nois disp as err "Cannot have user-defined analysis as main analysis," nois disp as err " and a standard analysis as second analysis." nois disp as err "You can do it the other way round, or have two user defined analyses," nois disp as err " but you can't do this particular thing. Sorry, that's just the way it is." exit 198 } } } else if `"`secondstats'"'!=`""' { disp `"{error}Note: {bf:secondstats()} is only valid with {bf:second(}{it:ES lci uci desc}{bf:)}, so will be ignored"' } if `"`first'"'!=`""' { local olduser olduser // marker that "legacy" -metan- options first() and/or second() were supplied gettoken _ES_ rest : first gettoken _LCI_ rest : rest gettoken _UCI_ desc : rest cap { confirm number `_ES_' confirm number `_LCI_' confirm number `_UCI_' assert `_LCI_' <= `_ES_' assert `_UCI_' >= `_ES_' } if _rc { nois disp as err `"Error in option {bf:first()}: must supply user-defined main analysis in the format:"' nois disp as err " {it:ES lci uci desc}" exit _rc } local first `"`_ES_' `_LCI_' `_UCI_', label(`desc') extralabel(`firststats')"' } else if `"`firststats'"'!=`""' { disp `"{error}Note: {bf:firststats()} is only valid with {bf:first(}{it:es lci uci desc}{bf:)}, so will be ignored"' } *** (2) Other options *** local 0 `", `opts_adm'"' syntax, PARAMS(integer) [ MODEL(string asis) /// RANDOM1 RANDOM(string) RE1 RE(string) RANDOMI /// // synonyms for D+L random-effects inverse-variance FIXEDI FIXED COMMON FE INVVariance IVariance IVCommon /// // synonyms for common-effect inverse-variance MHaenszel PETO LOGRank /// // MH/Peto (N.B. `fixed' imples MH if count data; `logrank' mostly for use with -ipdmetan-) IVHet QE(varname numeric) * ] // for backwards-compatibility with -admetan- local opts_adm `"`macval(options)'"' // re/re() and random/random() as main options if `"`random'"'!=`""' local rabr `"()"' if `"`re'"'!=`""' local rebr `"()"' if `"`random1'"'!=`""' & `"`random'"'==`""' local random random if `"`re1'"'!=`""' & `"`re'"'==`""' local re re if `"`re'"'!=`""' { if `"`random'"'!=`""' { nois disp as err `"Cannot specify both {bf:re`rebr'} and {bf:random`rabr'}; please choose just one"' exit 184 } else if `"`randomi'"'!=`""' { nois disp as err `"Cannot specify both {bf:re`rebr'} and {bf:randomi}; please choose just one"' exit 184 } local re_orig re`rebr' // store actual supplied option for error displays local newModel : copy local re // `re' is a synonym for `model'; henceforth use the latter } else if `"`randomi'"'!=`""' { if `"`random'"'!=`""' { nois disp as err `"Cannot specify both {bf:randomi} and {bf:random`rabr'}; please choose just one"' exit 184 } local re_orig randomi // store actual supplied option for error displays local newModel re // "randomi" is a synonym for "re"; henceforth use the latter } else if `"`random'"'!=`""' { local re_orig random`rabr' // store actual supplied option for error displays local newModel : copy local random // `random' is a synonym for `model'; henceforth use the latter } // NOTE: Only minimal validity checking here; just want to identify single unique model if "`logrank'"!="" { opts_exclusive `"`logrank' `fixedi' `fixed' `common' `mhaenszel' `ivcommon' `invvariance' `ivariance' `fe' `peto' `re_orig'"' `""' 184 local peto peto } opts_exclusive `"`fixedi' `fixed' `common' `mhaenszel' `ivcommon' `invvariance' `ivariance' `fe' `peto' `re_orig'"' `""' 184 // Similarly, `fe' etc. are synonyms for model(ivcommon)... if `"`fixedi'`fe'`ivcommon'`common'`invvariance'`ivariance'"'!=`""' { local model_orig `fixedi'`fe'`ivcommon'`common'`invvariance'`ivariance' // store actual supplied option for error displays local newModel ivcommon } // ...`mhaenszel' is a synonym for model(mhaenszel)... if `"`mhaenszel'"'!=`""' { local model_orig mhaenszel // store actual supplied option for error displays local newModel mhaenszel } // and ...`peto' is a synonym for model(peto) if `"`peto'"'!=`""' { local model_orig peto // store actual supplied option for error displays local newModel peto } // `fixed' is a synonym for model(mhaenszel) if 2x2 data; otherwise it is a synonym for model(ivcommon) if `"`fixed'"'!=`""' { local model_orig fixed // store actual supplied option for error displays local newModel = cond(`params'==4, "mhaenszel", "ivcommon") } // qe() and ivhet if `"`qe'"'!=`""' { local model_orig qe() // store actual supplied option for error displays local newModel qe, qwt(`qe') // format to pass to ParseModel } if `"`ivhet'"'!=`""' { local model_orig ivhet // store actual supplied option for error displays local newModel ivhet } if `"`model'"'!=`""' & `"`first'"'!=`""' { nois disp as err `"Cannot specify both {bf:model()} and {bf:first()}; please choose just one"' exit 184 } if `"`model_orig'`re_orig'"'!=`""' { if `"`model_orig'"'!=`""' & `"`re_orig'"'!=`""' { nois disp as err `"Cannot specify both {bf:`re_orig'} and {bf:`model_orig'}; please choose just one"' exit 184 } if `"`model'`first'"'!=`""' { nois disp as err `"Cannot specify both {bf:`model_orig'`re_orig'} and {bf:`model'`first'()}; please choose just one"' exit 184 } local model : copy local newModel } // Return options sreturn clear sreturn local options `"`opts_adm' `logrank'"' if `"`first'"'!=`""' { local model : copy local first } if `"`second'"'!=`""' { if `"`model'"'==`""' local model null // temporary marker; will be re-parsed later local model `"`model' \ `second'"' } sreturn local model `"`model'"' sreturn local olduser `olduser' // internal marker that "legacy" -metan- options first() and/or second() were supplied end // Simply parse multiple models one-at-a-time, and return the results // Validity checking is done elsewhere. // This subroutine is called by ProcessModelOpts program define ParseModel, sclass syntax [anything(name=model id="meta-analysis model")] /// , PARAMS(integer) [ SUMMSTAT(name) /*SUMMNEW*/ GLOBALOPTS(string asis) /// // default/global options: `teststat' `hetstat' [WGT() CC() etc.] Z T CHI2 CMH CMHNocc /// // test statistic options HKSj HKnapp KHartung KRoger BArtlett PETO RObust SKovgaard EIM OIM QWT(varname numeric) /*contains quality weights*/ /// INIT(name) CC(passthru) noCC2 LOGRank PRoportion TN(passthru) POVERV(passthru) /// POOLed SGPooled SGCommon /*Added Sep 2023: requests that het. param. is "pooled" (Borenstein) or "common" (Schwarzer, Carpenter, Ruecker) across subroups */ /// LAbel(string asis) EXtralabel(string asis) FIRST /*SECOND*/ /// // [Oct 2020:] user-defined model labelling WGT(passthru) TRUNCate(passthru) ISQ(string) TAUSQ(string) PHI(string) /// ITOL(passthru) MAXTausq(passthru) REPS(passthru) MAXITer(passthru) QUADPTS(passthru) DIFficult TECHnique(passthru) ] // last line ^^ "global" opts to compare with "specific model" opts // tausq() option added 24th July 2017 // bartlett and z (i.e. "not LR" options) added 5th March 2018; "z" returns signed LR statistic (as opposed to Wald-type) as of Jan 2019 // robust option added 13th Dec 2018 // skovgaard option added 5th Jan 2019 // Jan 2021: `label' local islabel = (`"`label'"'!=`""') // user has invoked the option in some form gettoken label : label // remove outer quotes // Oct 2020: `model' can either be a set of three numeric values, or a (valid) model name tokenize `model' if `"`2'"'!=`""' { cap numlist "`model'", min(3) max(3) if _rc { nois disp as err `"Invalid model name: `model'"' exit 198 } args _ES_ _LCI_ _UCI_ cap { assert `_LCI_' <= `_ES_' assert `_UCI_' >= `_ES_' } if _rc { nois disp as err `"Must supply user-defined analysis in the format {it:ES lci uci}"' nois disp as err `"(supplied was `model')"' exit _rc } local userstats : copy local model local model user local teststat user } // Else, assume that `model' is a (valid) model name; continue parsing else { // global options if "`hksj'`hknapp'`khartung'"!="" local hksj_opt hksj if "`kroger'"!="" local kr_opt kroger // Test statistic and heterogeneity options: should be unique local teststat `t' `z' `chi2' `cmh' `cmhnocc' opts_exclusive `"`teststat'"' `""' 184 // Tausq estimators, with synonyms local 0 `", `model'"' syntax [, COMMON IVCommon INVVariance IVariance FIXed MHaenszel Random DLaird HMakambi MLe MPaule PMandel PMMedian EBayes CAnova HEdges SEnsitivity * ] if inlist("`model'", "fe", "iv") /// | "`fixed'`common'`ivcommon'`invvariance'`ivariance'"!="" local model iv // Fixed (common)-effects inverse-variance else if "`model'"=="re" | "`random'`dlaird'"!="" local model dl // DerSimonian-Laird random-effects else if "`mhaenszel'"!="" local model mh // Mantel-Haenszel methods else if "`hmakambi'"!="" local model hm // Hartung-Makambi non-zero estimator else if "`mle'"!="" local model ml // Maximum Likelihood estimator else if inlist("`model'", "bdl", "dlb") local model dlb // Bootstrap DerSimonian-Laird (Kontopantelis) else if inlist("`model'", "gq", "genq", "vb") /// | "`mpaule'`pmandel'`ebayes'"!="" local model mp // Mandel-Paule aka Generalised Q aka Empirical Bayes else if "`model'"=="pmmedian" local model pmm // Viechtbauer (2021) median-unbiased version of Mandel-Paule else if "`model'"=="vc" | "`canova'`hedges'"!="" local model he // Hedges aka "variance-component" aka Cochran's ANOVA-type estimator else if inlist("`model'", "sj2", "sj2s") local model sj2s // Sidik-Jonkman two-step (default init=vc) else if inlist("`model'", "dk2", "dk2s") local model dk2s // DerSimonian-Kacker two-step (default init=vc) else if "`model'"=="sa" | "`sensitivity'"!="" local model sa // Sensitivity analysis (at fixed Isq) as suggested by Kontopantelis // Other model types (with synonyms) else { local 0 `", `options'"' syntax [, Gamma BTweedie HCopas MUltiplicative FVariance FULLVariance IVHet HKSj HKnapp KHartung KRoger QEffects QUality * ] if "`model'"=="bs" | "`gamma'`btweedie'"!="" local model bt // Biggerstaff-Tweedie approx. Gamma-based model else if "`hcopas'"!="" local model hc // Henmi-Copas model else if "`multiplicative'`fvariance'`fullvariance'"!="" local model mu // Multiplicative heterogeneity (aka Sandercock's "full variance") else if "`ivhet'"!="" local model ivhet // Doi's IVhet ("inverse-variance heterogeneity") model else if "`hksj'`hknapp'`khartung'"!="" local model hksj // Hartung-Knapp-Sidik-Jonkman variance correction else if "`kroger'"!="" local model kr // Kenward-Roger variance correction else if "`qeffects'`quality'"!="" local model qe // Doi's Quality Effects model } // Hartung-Knapp-Sidik-Jonkman variance correction if "`hksj_opt'"!="" { if "`model'"=="hksj" local model dl else if inlist("`model'", "mu", "bt", "kr", "hc", "ivhet", "qe", "pl") { nois disp as err `"Specified random-effects model is incompatible with Hartung-Knapp-Sidik-Jonkman variance estimator"' exit 184 } } else if "`model'"=="hksj" { local model dl // DL is default tausq estimator local hksj_opt hksj } // Kenward-Roger variance correction: this is basically REML, so allow "reml, kr" as an alternative if "`kr_opt'"!="" { if !inlist("`model'", "", "kr", "reml") { nois disp as err "Kenward-Roger variance estimator may only be combined" nois disp as err " with the REML estimator of tau{c 178}" exit 184 } local model reml } else if "`model'"=="kr" { local model reml local kr_opt kroger } // observed/expected information matrix for Kenward-Roger if `"`eim'`oim'"'!=`""' { cap assert "`kr_opt'"!="" if _rc { nois disp as err `"Options {bf:eim} and {bf:oim} are only appropriate with the Kenward-Roger variance estimator"' exit 184 } else { cap assert `: word count `eim' `oim'' == 1 if _rc { nois disp as err `"May only specify one of {bf:eim} or {bf:oim}, not both"' exit _rc } } } else if "`kr_opt'"!="" local eim eim // default // Sidik-Jonkman robust ("sandwich-like") variance estimator if "`robust'"!="" { if inlist("`model'", "mu", "bt", "hc", "ivhet", "qe", "pl") | "`kr_opt'"!="" { nois disp as err `"Specified random-effects model is incompatible with Sidik-Jonkman robust variance estimator"' exit 184 } } // Two-step models if !inlist("`model'", "sj2s", "dk2s") { if "`init'"!=`""' { nois disp as err `"Option {bf:init()} is only valid with two-step estimators of tausq"' exit 184 } } else { // default initial estimate if "`init'"==`""' { local init = cond("`model'"=="sj2s", "ev", "he") } // user-specified initial estimate // Note: "sensitivity analysis" (i.e. user-specified value) is not currently supported (Sep 2023: other than zero for Sidik-Jonkman) else { local 0 `", `init'"' syntax [, CAnova HEdges DLaird HMakambi MPaule PMandel EBayes * ] if "`canova'`hedges'"!="" | "`init'"=="vc" local init he // Hedges/Cochran/Variance-component else if inlist("`init'", "gq", "genq", "vb") | "`mpaule'`pmandel'`ebayes'"!="" local init mp // Mandel-Paule aka Generalised Q aka Empirical Bayes else if "`hmakambi'"!="" local init hm else if "`dlaird'"!="" local init dl else if "`init'"=="bdl" local init dlb } if "`model'"=="dk2s" { // DerSimonian-Kacker two-step is valid for MM estimators only if !inlist("`init'", "he", "dl") { nois disp as err `"Option {bf:init()} must be {bf:hedges} or {bf:dlaird} with DerSimonian-Kacker two-step estimator"' exit 184 } } else if !inlist("`init'", "he", "dl", "dlb", "ev", "hm") /// & !inlist("`init'", "b0", "bp", "mp", "pmm", "ml", "reml") { nois disp as err `"Invalid {bf:init()} option with Sidik-Jonkman two-step estimator"' exit 184 } local init_opt `"init(`init')"' } // Quality effects if "`model'"=="qe" { if `"`qwt'"'==`""' { nois disp as err "Quality-effects model specified but no quality weights found" exit 198 } local qe_opt `"qwt(`qwt')"' } else if `"`qwt'"'!=`""' { nois disp as err "Quality weights cannot be specified without quality-effects model" exit 198 } // conflicting options: variance modifications opts_exclusive `"`hksj_opt' `kr_opt' `bartlett' `skovgaard' `robust'"' model 184 // Bartlett and Skovgaard likelihood corrections: profile likelihood only if `"`bartlett'`skovgaard'"'!=`""' { cap assert "`model'"=="pl" if _rc { local errtext = cond(`"`bartlett'"'!=`""', `"Bartlett's"', `"Skovgaard's"') nois disp as err `"`errtext' correction is only valid with Profile Likelihood"' exit 184 } } // truncate() option: multiplicative or HKSJ only if `"`truncate'"'!=`""' { cap assert "`model'"=="mu" | "`hksj_opt'"!="" if _rc { nois disp as err `"{bf:truncate()} option is only valid with HKSJ model or with Multiplicative Heterogeneity"' exit 184 } } // Added Sep 2023 // "Pooled" (across subgroups) heterogeneity parameters // see e.g. Borenstein et al 2009 for the DerSimonian-Laird case // Since this approach is equivalent to meta-regression on subgroup indicators, it can also be done using REML or EB (see e.g. -metareg-) // Similarly, a weighted OLS regression on subgroup indicators yields a "pooled" *multiplicative* parameter (see e.g. Thompson & Sharp 1999) if `"`pooled'`sgpooled'`sgcommon'"'!=`""' { local errword : word 1 of `pooled' `sgpooled' `sgcommon' cap assert inlist("`model'", "dl", "reml", "mp", "mu") if _rc { nois disp as err `"Option {bf:`errword'} specified, requesting that heterogeneity parameter be stratified by subgroup"' nois disp as err `"Only models {bf:dlaird}, {bf:reml}, {bf:mpaule} and {bf:multiplicative} are currently allowed in this context"' exit 184 } capture which metareg if _rc { nois disp as err `"Heterogeneity parameter stratified by subgroup requires the user-written {bf:metareg} package"' nois disp as err `"Type {stata findit metareg:findit metareg} to find and install it"' exit 499 } local pooled pooled } // dependencies if inlist("`model'", "mp", "pmm", "ml", "pl", "reml", "bt", "hc") { capture mata mata which mm_root() if _rc { nois disp as err `"Iterative tau-squared calculations require the Mata function {bf:mm_root()} from {bf:moremata}"' nois disp as err `"Type {stata ssc describe moremata:ssc install moremata} to install it"' exit 499 } if inlist("`model'", "bt", "hc") { capture mata mata which integrate() if _rc { if "`model'"=="bt" nois disp as err `"Biggerstaff-Tweedie model requires the Mata function {bf:integrate()}"' else nois disp as err `"Henmi-Copas model requires the Mata function {bf:integrate()}"' nois disp as err `"Type {stata ssc describe integrate:ssc install integrate} to install it"' exit 499 } } } if "`model'"=="dlb" { capture mata mata which mm_bs() local rc1 = _rc capture mata mata which mm_jk() if _rc | `rc1' { nois disp as err `"Bootstrap DerSimonian-Laird method requires the Mata functions {bf:mm_bs()} and {bf:mm_jk()} from {bf:moremata}"' nois disp as err `"Type {stata ssc describe moremata:ssc install moremata} to install them"' exit 499 } } // final check for valid random-effects models: if !inlist("`model'", "", "user", "iv", "mh", "peto", "mu") /// I-V common-effect, Mantel-Haenszel, Peto, mult. het. [PLUS USER-DEFINED] & !inlist("`model'", "dl", "dlb", "ev", "he", "hm", "b0", "bp") /// simple tsq estimators (non-iterative) & !inlist("`model'", "mp", "pmm", "ml", "reml") /// simple tsq estimators (iterative) & !inlist("`model'", "sj2s", "dk2s", "dlc", "sa") /// two-step estimators; sensitivity analysis at fixed tsq/Isq & !inlist("`model'", "pl", "bt", "hc", "qe", "ivhet") { // complex models nois disp as err `"Invalid random-effects model"' nois disp as err `"Please see {help metan_model:help metan} for a list of valid model names"' exit 198 } if inlist("`model'", "mh", "peto") { cap assert `params'==4 | "`logrank'"!="" if _rc { nois disp as err "Mantel-Haenszel and Peto options only valid with 2x2 count data" exit 184 } } } ** PARSE "GLOBAL" OPTIONS (if applicable) // N.B. global opts will already have been checked against the data structure by ProcessInputVarlist // it only remains to check them against the *model* (and teststat/hetstat) opts_exclusive `"`cc' `cc2'"' `""' 184 local old_ccopt `"`cc'`cc2'"' foreach opt in wgt truncate isq tausq phi tn poverv itol maxtausq reps maxiter quadpts difficult technique { local old_`opt' : copy local `opt' } local 0 `", `globalopts'"' syntax [, Z T CHI2 CMH CMHNocc /// BY(passthru) CC(passthru) noCC2 WGT(passthru) TRUNCate(passthru) ISQ(string) TAUSQ(string) PHI(string) TN(passthru) POVERV(passthru) /// ITOL(passthru) MAXTausq(passthru) REPS(passthru) MAXITer(passthru) QUADPTS(passthru) DIFficult TECHnique(passthru) * ] // last line ^^ "global" opts to compare with "specific model" opts // add "*" as this macro also contains `opts_adm' (not relevant to this subroutine) local opts_adm `"`macval(options)'"' local gTestStat `t' `z' `chi2' `cmh' `cmhnocc' opts_exclusive `"`gTestStat'"' `""' 184 opts_exclusive `"`cc' `cc2'"' `""' 184 local ccopt `"`cc'`cc2'"' foreach opt in ccopt wgt truncate isq tausq phi tn poverv itol maxtausq reps maxiter quadpts difficult technique { if `"`old_`opt''"'!=`""' { local `opt' : copy local old_`opt' } } // default model if none defined if `"`model'"'==`""' { local model = cond(`params'==4, "mh", "iv") // If user-defined weights, model not specified and `params'==4, assume I-V Common rather than exit with error // This matches with previous -metan9- behaviour. // (do this now, so that hetstat is processed correctly. user-defined weights are otherwise processed later) if `"`wgt'"'!=`""' & `params'==4 { local model iv disp `"{error}Note: user-defined weights supplied; inverse-variance model assumed"' } } ** TEST STATISTICS if inlist("`teststat'", "cmh", "cmhnocc") { if "`model'"!="mh" | !inlist("`summstat'", "or", "") { nois disp as err "Cannot specify {bf:`teststat'} test option without Mantel-Haenszel odds ratios" exit 184 } if "`summstat'"=="" & `params'==4 { local summnew or disp `"{error}Note: {bf:`teststat'} test option specified; odds ratios assumed"' } } else if "`teststat'"=="" & inlist("`gTestStat'", "cmh", "cmhnocc") { if "`model'"!="mh" | !inlist("`summstat'", "or", "") { // disp `"{error}Note: global option {bf:`gTestStat'} is not applicable to all models; local defaults will apply"' local gl_error `gl_error' `gTestStat' } if "`summstat'"=="" & `params'==4 { local summnew or disp `"{error}Note: {bf:`teststat'} test option specified; odds ratios assumed"' } } // Profile likelihood: cannot use t-based confidence interval // (or z with Bartlett's correction) if "`model'"=="pl" { if "`bartlett'"!="" { if "`teststat'"=="z" { nois disp as err `"Cannot specify option {bf:z} with Bartlett's correction"' exit 184 } else if "`teststat'"=="" & "`gTestStat'"=="z" { // disp `"{error}Note: global option {bf:z} is not applicable to all models; local defaults will apply"' local gl_error `gl_error' z } } else if "`teststat'"=="t" { nois disp as err `"Cannot specify option {bf:t} with Profile Likelihood"' exit 184 } else if "`teststat'"=="" & "`gTestStat'"=="t" { // disp `"{error}Note: global option {bf:t} is not applicable to all models; local defaults will apply"' local gl_error `gl_error' t } } // MH, Peto and PL uses chi2 as default; HKSJ and Robust methods use t as default. // All three can be overridden with "z" option. // (Note that PL with "z" uses signed likelihood statistic.) else if "`model'"=="hc" { if "`teststat'"!="" { nois disp as err "Cannot specify option {bf:`teststat'} with Henmi-Copas model" exit 184 } else if "`gTestStat'"!="" { // disp `"{error}Note: global option {bf:`gTestStat'} is not applicable to all models; local defaults will apply"' local gl_error `gl_error' `gTestStat' } local teststat u } // Default teststat if "`teststat'"=="" { if "`gTestStat'"!="" local teststat `gTestStat' else { if "`model'"=="peto" | "`logrank'"!="" local teststat chi2 else if "`bartlett'"!="" local teststat chi2_lr // [Jan 2020] if "`hksj_opt'`kr_opt'`robust'"!="" local teststat t if "`teststat'"=="" local teststat z } } if "`model'"=="mh" { local cmhnocc if inlist("`teststat'", "cmh", "cmhnocc", "chi2") { if "`teststat'"!="cmh" local cmhnocc cmhnocc // MH: cmhnocc is the same as chi2 local teststat chi2 } // N.B. Example reference for the difference between "cmh" and "cmhnocc" is: // McDonald, J.H. 2014. Handbook of Biological Statistics, 3rd ed. Sparky House Publishing, Baltimore, Maryland // cmhnocc is undocumented, since McDonald suggests use of correction factor is best // (correction factor is also implemented by default in "metafor" by Wolfgang Viechtbauer) } ** SENSITIVITY ANALYSIS if "`model'"=="sa" { if `"`tausq'"'!=`""' { cap confirm number `tausq' if _rc { nois disp as err `"Error in {bf:tausq()} suboption to {bf:sa()}; a single number was expected"' exit _rc } if `tausq'<0 { nois disp as err `"tau{c 178} value for sensitivity analysis cannot be negative"' exit 198 } local tsqsa_opt `"tsqsa(`tausq')"' // if !`islabel' local label = `"SA(tau{c 178}="' + strofreal(`tausq', "%05.3f") + `")"' } else if `"`isq'"'!=`""' { cap confirm number `isq' if _rc { nois disp as err `"Error in {bf:isq()} suboption to {bf:sa()}; a single number was expected"' exit _rc } if `isq'<0 | `isq'>=100 { nois disp as err `"I{c 178} value for sensitivity analysis must be at least 0% and less than 100%"' exit 198 } local isqsa_opt `"isqsa(`isq')"' // if !`islabel' local label `"SA(I{c 178}=`isq'%)"' } else if `"`phi'"'!=`""' { cap confirm number `phi' if _rc { nois disp as err `"Error in {bf:phi()} suboption to {bf:sa()}; a single number was expected"' exit _rc } if `phi'<0 { nois disp as err `"{bf:phi()} is a multiplicative factor for variance and cannot be negative"' exit 198 } local phisa_opt `"phisa(`phi')"' } else { nois disp as err `"Error in options for sensitivity analysis: expected one of {bf:isq()}, {bf:tausq()} or {bf:phi()}"' exit 198 } if `: word count `tsqsa' `isqsa' `phi'' >=3 { nois disp as err `"Only one of {bf:isq()}, {bf:tausq()} or {bf:phi()} may be supplied as suboptions to {bf:sa()}"' exit 184 } } // if NOT sensitivity analysis else { foreach x in isq tausq phi { if `"``x''"'!=`""' { nois disp as err `"Option {bf:`x'()} may only be specified when requesting a sensitivity analysis model"' exit 184 } } } ** OTHER OPTIONS // "Pooled heterogeneity" (across subgroups) only valid with by() // (this is the only reason to consider by() alongside model-related options; other option checks are performed by ParseOptions) if `"`pooled'"'!=`""' { if `"`by'"'==`""' { nois disp as err `"Option {bf:`errword'} is not valid without {bf:by()}"' exit 184 } } // Continuity correction: some checks needed for specific option, others for global... do them all together here if `params'!=4 & "`proportion'"=="" { if `"`ccopt'"'!=`""' { nois disp as err `"Continuity correction only valid with proportions or 2x2 count data"' exit 184 } } else { local 0 `", `ccopt'"' syntax [, CC(string) noCC2] if `"`cc'"'!=`""' { local 0 `"`cc'"' syntax [anything(name=ccval id="value supplied to {bf:cc()}")] [, ALLifzero OPPosite EMPirical] if `"`ccval'"'!=`""' { confirm number `ccval' } else local ccval = 0.5 if `"`cc2'"'!=`""' & `ccval' != 0 { nois disp as err `"Cannot specify both {bf:cc()} and {bf:nocc}; please choose one or the other"' exit 184 } if "`proportion'"!="" { if "`opposite'"!="" { nois disp as err "Opposite-arm continuity correction only valid with 2x2 count data" exit 184 } if "`empirical'"!="" { nois disp as err "Empirical continuity correction only valid with 2x2 count data" exit 184 } } opts_exclusive `"`opposite' `empirical'"' cc 184 // Empirical CC valid with odds ratio only if `"`empirical'"'!=`""' { if "`summstat'"=="" { local summnew or disp `"{error}Note: Empirical continuity correction specified; odds ratios assumed"' } else if "`summstat'"!="or" { nois disp as err "Empirical continuity correction only valid with odds ratios" exit 184 } } // ensure continuity correction is valid if "`model'"=="peto" { nois disp as err "Continuity correction is incompatible with Peto method" exit 184 } else if "`proportion'"!="" & inlist("`summstat'", "arcsine", "ftukey") { disp `"{error}Note: Continuity correction is not necessary with {bf:`summstat'} transformed proportions; option will be ignored"' } else { cap assert `ccval'>=0 & `ccval'<1 if _rc { nois disp as err "Invalid continuity correction: must be in range [0,1)" exit _rc } } local ccval_orig : copy local ccval } else { // Amended May 2020 if `"`cc2'"'!=`""' | inlist("`model'", "mh", "peto", "user") /// | ("`proportion'"!="" & inlist("`summstat'", "arcsine", "ftukey")) { local ccval = 0 } else local ccval = 0.5 // default local ccval_orig = 0.5 } if `ccval'==0 & !(`"`cc2'"'!=`""' | `ccval_orig'==0) & "`summstat'"!="rd" { // Macro for uncorrected M-H, so that study estimates & weights *are* corrected if necessary // [ but *not* if correction was explicitly set to zero, via either nocc or cc(0) ] // [ and *not* for Risk Differences ] if "`model'"=="mh" { local ccval = 0.5 local mhuncorr mhuncorr } } local ccopt_final `"cc(`ccval', `allifzero' `opposite' `empirical' `mhuncorr')"' } // chi2 is only valid with: // - 2x2 Odds Ratios (including Peto) // - logrank HR // - Profile Likelihood ... but this is stored separately, as `chi2_lr' [Jan 2020] if "`teststat'"=="chi2" { if "`summstat'"=="" & `params'==4 { local summnew or disp `"{error}Note: Chi-squared option or Peto model specified; odds ratios assumed"' } cap assert (inlist("`summstat'", "or", "") & `params'==4) | "`logrank'"!="" if _rc { nois disp as err `"Option {bf:chi2} is incompatible with other options"' exit 184 } } else if "`teststat'"=="chi2_lr" local teststat chi2 // now combine the two [Jan 2020] // User-defined weights if `"`wgt'"'!=`""' { // Only "vanilla" random-effects models are compatible // (i.e. those which simply estimate tau-squared and use it in the standard way) if "`model'"=="peto" local model_err peto else if "`model'"=="mh" local model_err mhaenszel else if "`model'"=="bt" local model_err btweedie else if "`model'"=="hc" local model_err hcopas else if "`model'"=="ivhet" local model_err ivhet else if "`model'"=="qe" local model_err qe else if "`model'"=="mu" local model_err mult else if "`model'"=="pl" local model_err pl else if "`kr_opt'"!="" local model_err kroger if "`model_err'"!="" { nois disp as err "User-defined weights can only be used with models based on the inverse-variance method" nois disp as err " which does not include {bf:`model_err'}" exit 184 } } else if "`model'"=="user" & "`first'"!="" { nois disp as err `"Must supply weight variable with option {bf:wgt(}{it:varname}{bf:)}"' nois disp as err `" with user-supplied effect sizes"' exit 198 } // [Modified Oct 2020] // Model description to display on-screen (shorthand, in case of multiple models) if !`islabel' { if "`model'"=="peto" local label Peto else if "`model'"=="ivhet" local label IVhet else if "`model'"=="dlb" local label DLb else if "`model'"=="ev" local label "Emp. Var." else if inlist("`model'", "bp", "b0") local label = "Rukhin " + upper("`model'") else if "`model'"=="user" local label User else /*if "`model'"!="sa"*/ local label = upper("`model'") local labt = 0 // Added Sep 2023 if "`hksj_opt'"!="" local label `"`label'+HKSJ"' else if "`kr_opt'"!="" local label `"`label'+KR"' else if "`robust'"!="" local label `"`label'+Rob."' else if "`bartlett'"!="" local label `"`label'+Bart."' else if "`skovgaard'"!="" local label `"`label'+Skov."' else if "`teststat'"=="t" { if "`pooled'"!="" local label `"`label' (t CI, Pooled)"' else local label `"`label' (t CI)"' local labt = 1 } if "`pooled'"!="" & !`labt' local label `"`label' (Pooled)"' } ** COLLECT OPTIONS AND RETURN sreturn clear // Model options local modelopts `"`cmhnocc' `robust' `hksj_opt' `kr_opt' `bartlett' `skovgaard' `eim' `oim' `ccopt_final' `tn' `poverv'"' local modelopts `"`modelopts' `wgt' `pooled' `truncate' `isqsa_opt' `tsqsa_opt' `phisa_opt' `qe_opt' `init_opt'"' // N.B. `tsqlevel' removed June 2022 local modelopts `"`modelopts' `itol' `maxtausq' `reps' `maxiter' `quadpts' `difficult' `technique'"' local modelopts = trim(itrim(`"`modelopts'"')) // Return sreturn local model `model' sreturn local modelorig `modelorig' sreturn local teststat `teststat' sreturn local modelopts `"`modelopts'"' // Additional model options (for PerformPooling) sreturn local labelopt `"`label'"' // Model label (for DrawTableAD) sreturn local extralabel `"`extralabel'"' // "Extra" model label (e.g. heterogeneity; for DrawTableAD) sreturn local userstats `"`userstats'"' // User-supplied effect sizes // Remaining options sreturn local opts_adm `"`macval(opts_adm)'"' // Corrective macro: only applies to 2x2 count data; is either "or" or nothing sreturn local summnew `summnew' // Error prompts sreturn local gl_error `gl_error' end ** Basic option compatibility checks // Locals to be altered in main routine: // ilevel olevel (+ level to be discarded) // ccvar() added (if params==4 or proportion) // hlevel (+ tsqlevel discarded) // (no)keepvars (no)rsample clear (could be "", "clear", "clearstack") // (no)het (no)wt (no)subgroup (no)overall // sgwt ovwt altwt // isqparam program define ParseOptions, sclass // Locals needed for reference, but not to be altered: syntax [, BY(varname numeric) * ] // do not return `byopts' in `opts_adm' local 0 `", `macval(options)'"' syntax [, SAVING(passthru) CREATEDBY(passthru) DENOMinator(passthru) /// PRoportion CUmulative INFluence SUMMARYONLY NOPR TESTBased * ] // Locals potentially to be altered here: syntax [, LEVEL(passthru) ILevel(passthru) OLevel(passthru) TSQLEVEL(passthru) HLevel(passthru) /// noKEEPvars noRSample CLEARSTACK CLEAR noHET noWT noOVerall noSUbgroup OVWt SGWt ALTWt ISQParam /// MODELLIST(namelist) * ] // <-- *not* to be altered; but also not to be returned within `opts_adm' local opts_adm `"`macval(options)'"' local m : word count `modellist' gettoken model1 : modellist // Significance levels if `"`hlevel'"'==`""' local hlevel : copy local tsqlevel // TSQLEVEL is a legacy -admetan- synonym for HLEVEL if `"`level'"'!=`""' { if `"`ilevel'"'!=`""' { nois disp as err "Cannot specify both {bf:level()} and {bf:ilevel()}" exit 184 } if `"`olevel'"'!=`""' { nois disp as err "Cannot specify both {bf:level()} and {bf:olevel()}" exit 184 } local ilevel `"i`level'"' local olevel `"o`level'"' local level } if `"`clearstack'"'!=`""' local clear clearstack // Added July 2021 if `"`clear'"'!=`""' & `"`createdby'"'==`""' { if `"`keepvars'"'!=`""' { nois disp as err `"Option {bf:`clear'} specified; option {bf:nokeepvars} will be ignored"' local keepvars } if `"`rsample'"'!=`""' { nois disp as err `"Option {bf:`clear'} specified; option {bf:norsample} will be ignored"' local rsample } } if `"`rsample'"'!=`""' local keepvars nokeepvars // noRSample implies noKEEPVars // May 2020: // Restore -metan9- behaviour that noOVerall ==> noHET and noWT if `"`overall'"'!=`""' & !(`"`by'"'!=`""' & `"`subgroup'"'==`""') { local het nohet local wt nowt } // cumulative and influence // if `by', cumulative *must* be done by subgroup and not overall ==> nooverall is "compulsory" opts_exclusive `"`cumulative' `influence'"' `""' 184 if `"`cumulative'"'!=`""' { if `"`subgroup'"'!=`""' { disp `"{error}Note: {bf:nosubgroup} is not compatible with {bf:cumulative} and will be ignored"' local subgroup } if `"`by'"'==`""' { if `"`overall'"'!=`""' { disp `"{error}Note: {bf:nooverall} is not compatible with {bf:cumulative} (unless with {bf:by()}) and will be ignored"' local overall } } else { if `"`overall'"'!=`""' { disp `"{error}Note: {bf:nooverall} is compulsory with {bf:cumulative} and {bf:by()}"' } } } if `"`cumulative'`influence'"'!=`""' { if `"`summaryonly'"'!=`""' { disp `"{error}Note: {bf:`cumulative'`influence'} and {bf:summaryonly} are not compatible"' exit 184 } // Multiple models cannot be specified with cumulative or influence (for simplicity) if `m' > 1 { nois disp as err `"Option {bf:`cumulative'`influence'} cannot be specified with multiple pooling methods"' exit 184 } if "`model1'"=="user" { nois disp as err "Cannot use option {bf:`cumulative'`influence'} with user-defined main analysis" exit 184 } } // Compatibility tests for ovwt, sgwt, altwt // [modified Jan 2020] opts_exclusive `"`ovwt' `sgwt'"' `""' 184 if `"`by'"'==`""' { if `"`subgroup'"'!=`""' { disp `"{error}Note: {bf:nosubgroup} cannot be specified without {bf:by()} and will be ignored"' local subgroup } if `"`sgwt'"'!=`""' { disp `"{error}Note: {bf:sgwt} is not applicable without {bf:by()} and will be ignored"' local sgwt } local ovwt ovwt } else { if `"`cumulative'`influence'"'==`""' { // default handling of sgwt and ovwt if `"`ovwt'`sgwt'"'==`""' { if `"`overall'"'!=`""' & `"`subgroup'"'==`""' local sgwt sgwt else local ovwt ovwt } } else if `"`cumulative'"'!=`""' { if `"`ovwt'"'!=`""' disp `"{error}Note: {bf:ovwt} is not compatible with {bf:cumulative} and {bf:by()}, and will be ignored"' local ovwt local sgwt sgwt } else { // `"`influence'"'!=`""' ==> `nooverall' and `sgwt' are synonyms // ...and ==> `nosubgroup' and `ovwt' are synonyms if `"`overall'"'!=`""' & `"`subgroup'"'!=`""' { nois disp as err `"With {bf:influence}, only one of {bf:nooverall} or {bf:nosubgroup} is allowed"' exit 184 } if `"`overall'"'!=`""' | `"`sgwt'"'!=`""' { local overall nooverall local sgwt sgwt } else { local subgroup nosubgroup local ovwt ovwt } } if "`model1'"=="user" { nois disp as err "Cannot use option {bf:by()} with user-defined main analysis" exit 184 } } assert `: word count `sgwt' `ovwt''==1 if `"`altwt'"'!=`""' & `"`cumulative'`influence'"'==`""' { disp `"{error}Note: {bf:altwt} is not applicable without {bf:cumulative} or {bf:influence}, and will be ignored"' local altwt } // April 2021 // if `"`subgroup'`het'"'!=`""' local between nobetween // June 2022 -- moved to metan_output.ado // proportions if `"`denominator'"'!=`""' { if `"`proportion'"'==`""' { nois disp as err `"Cannot specify {bf:denominator(}#{bf:)} without {bf:proportion}"' exit 184 } if `"`nopr'"'!=`""' { nois disp as err `"Cannot specify both {bf:nopr} and {bf:denominator(}#{bf:)}"' exit 184 } } if `"`nopr'"'!=`""' & `"`proportion'"'==`""' { nois disp as err `"Cannot specify {bf:nopr} without {bf:proportion}"' exit 184 } // [SEP 2020:] error message relating to `isqparam' if "`isqparam'"!="" { local notsqb peto mh iv mu user if `"`: list modellist - notsqb'"'==`""' { disp "{error}Note: heterogeneity measures based on tau{c 178} and sigma{c 178} cannot be estimated under " _c if `m'==1 disp `"{error}the specified model; {bf:isqparam} will be ignored"' else disp `"{error}any of the specified models; {bf:isqparam} will be ignored"' local isqparam } } // June 2023: same, but related to DL if "`isqparam'"!="" { local notsqb dl ivhet qe hc if `"`: list modellist - notsqb'"'==`""' { disp `"{error}Note: for models based on the DerSimonian-Laird moments-based estimate of tau{c 178}, "' disp `"{error}{bf:isqparam} will have no effect; therefore it will be ignored for such models"' local isqparam } } // Re-assemble options list // (see "Prepare for meta-analysis modelling" section above) local newopts `ilevel' `olevel' `hlevel' `clear' local newopts `newopts' `het' `wt' `subgroup' `overall' `sgwt' `ovwt' `altwt' `isqparam' sreturn local newopts `"`newopts'"' sreturn local keepvars `keepvars' `rsample' sreturn local options `"`macval(opts_adm)'"' end ******************************************************************************** *************************************** * metan.ado detailed revision notes * *************************************** // METAN UPDATE // ROSS HARRIS, DEC 2006 // MAIN UPDATE IS GRAPHICS IN THE _dispgby PROGRAM // ADDITIONAL OPTIONS ARE lcols AND rcols // THESE AFFECT DISPLAY ONLY AND ALLOW USER TO SPECIFY // VARIABLES AS A FORM OF TABLE. THIS EXTENDS THE label(namevar yearvar) // SYNTAX, ALLOWING AS MANY LEFT COLUMNS AS REQUIRED (WELL, LIMIT IS 10) // IF rcols IS OMMITTED DEFAULT IS THE STUDY EFFECT (95% CI) AND WEIGHT // AS BEFORE- THESE ARE ALWAYS IN UNLESS OMITTED USING OPTIONS // ANYTHING ADDED TO rcols COMES AFTER THIS. // v1.4 WAS GETTING THERE // v1.5 SORT OUT EXTRA LINE AT THE TOP AND ALLOW "DOUBLE LINES" // FOR FIXED AND RANDOM EFFECTS // SOMETHING TO ADD- RETURN ES_2? // v1.7 - TRY TO SORT OUT LABELLING // CHANGED LABELS TO 7.3g -WORKS NICELY // "FAVOURS" NOW WORKS- USES xmlabel // v1.8 ADDED IN COUNTS OPTION, SORTED OUT TEXTSIZE, PROPER DEFINITION AND SPLIT OF VAR LABELS // v1.9 DELETE UNECESSARY OPTIONS // OH, AND ADD effect OPTION // v1.10 FINAL TIDYING, USED Jeff Pitblado's SUGGESTION FOR getWidth // v1.11 USE label() OPTIONS IF NO lcols rcols, WORK ON AUTO FIT TEXT // v1.12 FURTHER WORK... // v1.14 DONE ON 12TH OCTOBER, FINALLY DISCOVERED WHAT IS CAUSING PROBLEM // WITH "NON-MATCHING CLOSE BRACE" AT END OF FILE- NO v7 STYLE IF STATEMENTS! // EVERYTHING GOES ON A SEPARATE LINE NOW. PHEW. // v1.15 NOW ADDING IN OPTIONS TO CONTROL BOXES, CI LINES, OVERALL // TITLES WEREN'T SPREADING ACROSS SINCE OPTION TO CONTROL OVERALL TEXT- FIXED AGAIN // v1.16 LAST ATTEMPT TO GET TEXT SIZE RIGHT! WORK OUT WHAT ASPECT SHOULD BE AND USE // IF ASPECT DEFINED THEN DECREASE TEXT SIZE BY RATIO TO IDEAL ASPECT // TEXT SCALING WORKING BETTER NOW // LAST THING TO TRY TO SORT IS LOOK FOR LEFT OF DIAMOND AND MOVE HET STATS // COULD PUT UNDERNEATH IF NOT MUCH SPACE? THIS WOULD BE GOOD v1.17 // STILL DEBATING WHETHER TO PUT favours BIT BELOW xticks... // V19 LOTS OF COMMENTS FROM JONATHAN AND BITS TO DO. SUMMARY: // aspect Y // note if random weights Y // update to v8 Y // graph in mono Y // extend overall text into plot Y // labels Y // help file not v8 yet // v1.21 EVERY PROGRAM NOW CONVERTED TO v9.0, NO "CODE FOLLOWS BRACES!" // WHAT ELSE DID PATRICK DO TO UPDATE TO v8? // NO "#delimit ;" - I QUITE LIKE THIS THOUGH! // GLOBALS ARE DECLARED WITHOUT = SIGN - EXCEPT WHEN USING STRING FUNCTION ETC. IT DOESN'T LIKE THIS! // - WILL THIS EVER CAUSE PROBLEMS? // - CAN'T BE BOTHERED TO CHANGE ALL THE NUMERIC ONES // USE TOKENIZE INSTEAD OF PARSE - DONE // USE di as err NOT in red, EXIT - DONE, PROPER RETURN CODES STILL NEEDED, MAYBE SOMEDAY! // DECENT HELP FILE - USED, JUST ADD IN NEW BITS // v1.22 ENCODE STUFF FOR metanby NOW LABORIOUSLY RECODED SO THAT GROUPS ARE IN ORIGINAL SORT ORDER // THIS IS USEFUL IF YOU DON'T WANT STUFF IN ALPHA-NUMERIC ORDER, OR TO PUT "1..." "2..." ETC. // counts OPTION DOES NOT WORK WITH MEAN DIFFERENCES- AND LOOKS LIKE IT NEVER DID- PUT IN // DO OWN LINES FOR OVERALL ETC. EXTENDS TOO FAR // LABELS NEVER RUN TO FOUR LINES- SORT OUT- QUICK SOLU- DO FIVE TIMES AND DROP ONE! // v1.23 USES pcspike FOR OVERALL AND NULL LINES TO PREVENT OVER-EXTENDING // NOW HAS OPTION FOR USER DEFINED "SECOND" ANALYSIS // v1.24 ALLOW USER TO COMPLETELY DEFINE ANALYSIS WITH WEIGHTS // v2.34 problem with nosubgroup nooverall sorted out (this combination failed) ******************** ** May 2007 fixes ** ******************** // "nostandard" had disappeared from help file- back in // I sq. in return list // sorted out the extra top line that appears in column labels // fixed when using aspect ratio using xsize and ysize so inner bit matches graph area- i.e., get rid of spaces for long/wide graphs // variable display format preserved for lcols and rcols // abbreviated varlist now allowed // between groups het. only available with fixed // warnings if any heterogeneity with fixed (for between group het if any sub group has het, overall est if any het) // nulloff option to get rid of line // May 2009 // prediction interval sorted out. Note error in Higgins presentation (too wide- there is no heterogeneity in data!) // so don't check with this! George Kelley has sent example data *************************************** * admetan.ado detailed revision notes * *************************************** * originally written by David Fisher, June 2013 * version 1.0 David Fisher 31jan2014 * version 2.0 David Fisher 11may2017 * Major update to extend functionality beyond estimation commands; now has most of the functionality of -metan- // - Reworked so that ipdmetan.ado does command processing and looping, and admetan.ado does RE estimation (including -metan- functionality) // - Hence, ipdover calls ipdmetan (but never admetan); ipdmetan calls admetan (if not called by ipdover); admetan can be run alone. // Any of the three may call forestplot, which of course can also be run alone. * version 2.1 David Fisher 14sep2017 // various bug fixes // Note: for harmonisation with metan/metaan, Isq input and output is now in the range 0 to 100, rather than 0 to 1. // Corrected error whereby tausq could not be found by iterative methods if >> 0 // due to assumptions based on me mostly using ratio statistics, where tausq < 1, and not mean differences where tausq can be any magnitude. * version 3.0 David Fisher 08nov2018 // IPD+AD code now moved to ipdmetan.ado // so that admetan is completely self-contained, with minimal reference to -ipdmetan- // various bug fixes and minor improvements // implemented -useopts- facility and _EFFECT variable * version 3.1 David Fisher 04dec2018 // Allow `oev' with Peto ORs // Specify default format & title for numeric vars in results sets, so that they display nicely in forestplot // Fixed bug which meant "HKSJ method" was not displayed on screen (although the method itself was used) // `hksj' and `bartlett' are returned (if applicable) in r(vce_model) * version 3.2 David Fisher 28jan2019 // Do not allow `study' and `by' to have the same name // Added SJ Robust ("sandwich-like") variance estimator (Sidik & Jonkman CompStatDataAnalysis 2006) // Added Skovgaard's correction to the signed likelihood statistic (Guolo Stat Med 2012) // Corrected bug when continuity correction options were specified; also added new on-screen text r.e. cc [ADDED FEB 2019] // Corrected returned statistics for `chi2opts' and Henmi-Copas model // Corrected bug when specifying npts(varname) with "generic" effect measures // Generalised the two-step estimators (Sidik-Jonkman and DerSimonian-Kacker) // `hksj', `bartlett', `skovgaard' and `robust' are returned (if applicable) as part of r(model) // Some text in help file has been changed/updated * version 3.3 (beta; never released) David Fisher 30aug2019 // Zero cells and the Mantel-Haenszel method: default is to add cc=0.5 for display purposes only... // ...including "double-zero" studies, as they still contribute to the MH pooled estimate // (if M-H) If -nocc- is explicitly requested, *KEEP ALL STUDIES IN* and print appropriate warning message // Zero-cell studies must be *explicitly excluded* using if/in in this scenario. // Q, tausq etc. based on MH vs Inverse-Variance pooled estimate: No tausq/Isq if M-H. // Also if M-H: CMH with/without correction; "Old" Breslow/Day; "New" Breslow/Day/Tarone. * Current version 3.4 (beta; will be 4.0 upon release) David Fisher 23oct2019 // Fixed bug where main options (e.g. nograph) would be ignored under certain circumstances // Fixed bug where id would be repeated if given as lcols(id) // Major addition: multiple models, either as backslash-separated list e.g. model(fe\dl\reml\pl) // or, as in -metan- first() second() // Also now incorporating code from "heterogi.ado" for calculating CIs for Isq // (c.f. Higgins & Thompson Stat Med 2002, "Quantifying heterogeneity") // - test-based (for lnH or lnQ) // - noncentral Q // - Q profiling // Major addition: added meta-analysis of proportions // Restored previous -metan- behaviour of sorting string by() by order of appearance, rather than alphabetically // Improved behaviour of -influence- : corrected bug in weight normalisation with subgroups; // -forestplot- now by default displays -influence- pooled results as vertical lines rather than diamonds