* version 2.1 of 5 January 2009 * Modified regression test for funnel plot asymmetry in 2x2 tables * based on statistics of score test instead of Wald test. * Author: roger.harbord@bristol.ac.uk * Peters test (effect size on 1/N) added * updates added by Ross Harris *! version 3.0 David Fisher 09jul2020 *! Previous versions by Ross Harris and Roger Harbord // Release notes DF 2020: // - Minimal changes to interface with updated -metan- v4.00 // - Also taken the opportunity to streamline / modernise the code structure in places. program define metabias, rclass byable(onecall) version 9.2 syntax varlist(min=2 max=4 numeric) [if] [in] /// [, or rr EGGer BEGg PETers HARbord CC(passthru) noFit Graph * ] marksample touse // error traps for method and data combinations opts_exclusive `"`or' `rr'"' `""' 184 local measure `or'`rr' opts_exclusive `"`egger' `begg' `harbord' `peters'"' `""' 184 local test `egger'`begg'`harbord'`peters' // check variables given and assign data_type tokenize `varlist' if "`4'" == "" { if "`2'" == "" | "`3'" != "" { di as err `"Must specify variables as either {it:theta se_theta} or as binary data"' exit 198 } args theta se_theta di "" di as text "Note: data input format " as res `"{it:theta se_theta}"' as text " assumed" if inlist("`test'", "peters", "harbord") { di as err `"Peters and Harbord tests cannot be used with data format {it:theta se_theta}"' di as err "Egger test is recommended" exit 198 } else if "`test'"=="" { di as err `"Must specify test: {bf:egger} or {bf:begg}"' di as err `"(Peters and Harbord tests cannot be used with data format {it:theta se_theta})"' di as err "Egger test is recommended" exit 198 } } // binary data else { args a b c d /* Args in same order as -metan-, i.e.: */ /* a = Treatment group, event */ /* b = Treatment group, no event */ /* c = Control group, event */ /* d = Control group, no event */ di "" di as text "Note: data input format " as res `"{it:tcases tnoncases ccases cnoncases}"' as text " assumed" if "`test'"=="" { di as err `"Must specify test: {bf:egger}, {bf:begg}, {bf:peters} or {bf:harbord}"' exit 198 } // default effect measure if "`measure'"=="" { di as text "Note: " as res "Odds ratios" as text " assumed as effect estimate of interest" local measure or } else { if "`measure'"=="or" local meastext "Odds ratios" else local meastext "Relative risks" di as text "Note: " as res "`meastext'" as text " specified as effect estimate of interest" } if inlist("`test'", "egger", "begg") { di as text "Note: Peters or Harbord tests generally recommended for binary data" // change to theta se_theta, accounting for zero cells if appropriate tempvar theta se_theta qui cap metan `a' `b' `c' `d' if `touse', `measure' nograph nooverall notable if _rc { di as error "User-written package -metan- required but not found -" di as error "Please install, e.g. by typing -ssc install metan-" exit 111 // something not found - a command in this case } // if -metan- v3 or below if "`r(metan_version)'"=="" { if "`cc'"!="" { di as err `"option {bf:`cc'} cannot be used with {bf:metan} version 3 or below"' exit 198 } qui gen `theta' = ln(_ES) if `touse' qui gen `se_theta' = _selogES if `touse' qui drop _SS _ES _selogES _LCI _UCI _WT } // if -metan- v4 or above else { if "`cc'"!="" { // if user-specified cc option qui metan `a' `b' `c' `d' if `touse', `measure' `cc' nograph nooverall notable } rename _ES `theta' rename _seES `se_theta' qui drop _LCI _UCI _WT _NN } local varlist `theta' `se_theta' } /// RJH // (this check is done by -metan- if Egger or Begg; see above) else { capture assert `a'>=0 & `b'>=0 & `c'>=0 & `d'>=0 if `touse' if _rc { di as error "All cell counts must be >=0" exit 459 // something that should be true of your data is not } } /* Added by RMH in v2.1 */ qui count if ( `a'+`c'==0 | `b'+`d'==0 | `a'+`b'==0 | `c'+`d'==0 ) & `touse' if r(N) { local ies ies if r(N) == 1 local ies y di as txt "Note: excluding " as res r(N) as text " non-informative stud`ies' with a zero marginal total" } } /* don't allow RR with Peters as unsure of weights */ if "`rr'" != "" & "`peters'" != ""{ di as error "Cannot use Peters test for risk ratios - appropriate weights not known" exit 198 } if _by() { if "`graph'" != "" { di as err `"option {bf:graph} may not be combined with the {bf:by:} prefix"' exit 198 } local by "by `_byvars' `_byrc0':" } if inlist("`test'", "egger", "harbord") { `by' egger_harbord `varlist' if `touse', test(`test') measure(`measure') `graph' `fit' `options' } else { local proper = strproper("`test'") if "`graph'"!="" { di as err `"option {bf:graph} cannot be used with `proper' test"' exit 198 } if "`fit'"!="" { di as err `"option {bf:nofit} cannot be used with `proper' test"' exit 198 } `by' `test' `varlist' if `touse', measure(`measure') } return add end *********** TESTS *********** ****************************************************** // original Egger test, and modified Harbord test ****************************************************** // Note: David Fisher, Nov 2019 // These tests are very similar to each other program define egger_harbord, rclass byable(recall) syntax varlist(min=2 max=4) [if] [in] [, TEST(name) MEASURE(name) LEVEL(real 95) z(name) v(name) noFit Graph * ] marksample touse tokenize `varlist' if "`3'"!="" { cap { assert "`4'"!="" assert "`test'"=="harbord" } if _rc exit 198 args a b c d di as text _n "Harbord's modified test for small-study effects: " di as text `"Regress {it:Z}{bf:/sqrt(}{it:V}{bf:)} on {bf:sqrt(}{it:V}{bf:)}, "' _c di as text `"where {it:Z} is the efficient score and {it:V} is the score variance"' _n qui { tempvar n V Z gen `n' = `a' + `b' + `c' + `d' if `touse' // RJH added rr, use or as default (checked previously that not both) if "`measure'"=="rr" { gen `V' = ((`c'+`d')*(`a'+`b')*(`a'+`c')) / (`n'*(`b'+`d')) if `touse' gen `Z' = (`a'*`n' - (`a'+`c')*(`a'+`b')) / (`b'+`d') if `touse' } // Odds ratio else { gen `V' = (`a'+`b')*(`c'+`d')*(`a'+`c')*(`b'+`d') / ( (`n')^2 * (`n'-1) ) if `touse' gen `Z' = (`a'*`d' - `b'*`c') / `n' if `touse' } tempvar y x // `ZoverrootV' `rootV' gen `x' = sqrt(`V') if `touse' gen `y' = `Z'/`x' if `touse' local yvarlab "Z / sqrt(V)" local xvarlab "sqrt(V)" } local beta "sqrt(V)" local cons bias local mnames `beta' `cons' local depname "Z/sqrt(V)" } else { cap { assert "`2'"!="" assert "`test'"=="egger" } if _rc exit 198 args theta se_theta di as text _n "Egger's test for small-study effects:" di as text "Regress standard normal deviate of intervention" di as text " effect estimate against its standard error" _n qui { tempvar y x // `snd' `prec' qui gen `y' = `theta' / `se_theta' qui gen `x' = 1 / `se_theta' local yvarlab "SND of effect estimate" local xvarlab "Precision" } local beta slope local cons bias local mnames `beta' `cons' local depname Std_Eff } tempname pbias rmse qui regress `y' `x' if `touse', level(`level') scalar `pbias' = 2*ttail( e(df_r), abs(_b[_cons]/_se[_cons]) ) scalar `rmse' = e(rmse) return scalar rmse = e(rmse) return scalar p_bias = `pbias' return scalar se_bias = _se[_cons] return scalar bias = _b[_cons] return scalar df_r = e(df_r) return scalar N = e(N) // Now rename row/colnames of e(b) and e(V) and use -ereturn post- tempname vcov b matrix define `vcov' = e(V) matrix define `b' = e(b) matrix colnames `vcov' = `mnames' matrix rownames `vcov' = `mnames' matrix colnames `b' = `mnames' ereturn post `b' `vcov', depname("`depname'") dof(`e(df_r)') obs(`e(N)') /* maybe prefer z-test? In which case don't post dof. */ nois disp e(rmse) di as txt "Number of studies = ", as res e(N) _c di as txt _col(55) " Root MSE = ", as res %6.0g `rmse' ereturn display, level(`level') di as text _n "Test of H0: no small-study effects" _col(45) "P = " as res %5.3f `pbias' if "`graph'" == "" { if "`options'" != "" { di as err `"option {bf:`options'} not allowed"' exit 198 } } else { tempvar fitted lci uci tempname cihw scalar `cihw' = _se[`cons'] * invttail( e(df_r), (1-`level'/100)/2 ) if "`fit'" != "nofit" { nobreak { quietly { preserve set obs `= _N + 1' replace `x' = 0 in l gen `lci' = _b[`cons'] - `cihw' in l gen `uci' = _b[`cons'] + `cihw' in l gen `fitted' = _b[`cons'] + _b[`beta']*`x' if `touse' twoway ( scatter `y' `x', /// ytitle(`"`yvarlab'"') xtitle(`"`xvarlab'"') /// yline(0, lc(fg)) yla(-2 0 2) `options' ) /// ( line `fitted' `x', sort clsty(p2) ) /// ( rcap `lci' `uci' `x', msize(*2) blsty(p2) ) /// , legend(label(1 "Study") label(2 "regression line") /// label(3 "`level'% CI for intercept") order(1 2 - " " 3)) restore } } } else { // nofit twoway scatter `y' `x', yline(0, lc(fg)) /// ytitle(`"`yvarlab'"') xtitle(`"`xvarlab'"') `options' } } /* output Z & V if requested */ if "`test'"=="harbord" { if "`z'"!="" & _bylastcall() { rename `Z' `z' label variable `z' "Efficient score" } if "`v'"!="" & _bylastcall() { rename `V' `v' label variable `v' "Fisher's information" } } end ***************************************** // RJH added- Peter's test ***************************************** program define peters, rclass byable(recall) syntax varlist(min=4 max=4) [if] [in] [, MEASURE(string) LEVEL(real 95) noFit Graph ] marksample touse tokenize `varlist' args a b c d tempvar theta ovN wgt prec_lnodds if "`measure'"=="rr" { di as text "Note: test is formulated in terms of odds ratios" _n /// " but should give valid results for risk ratios" qui gen `theta' = ln( (`a'/(`a'+`b')) / (`c'/(`c'+`d')) ) if `touse' } else { qui gen `theta' = ln( (`a'/`b') / (`c'/`d') ) if `touse' } gen `ovN' = 1/(`a'+`b'+`c'+`d') if `touse' gen `wgt' = (`a'+`c')*(`b'+`d')/(`a'+`b'+`c'+`d') // from http://www.rss.org.uk/pdf/Jaime%20Peters%20Presentation.pdf // weight is the same- should weight be different for RR? * gen `prec_lnodds'= 1/(1/(`se'+`sc') + 1/(`fe'+`fc')) di as text _n "Peter's test for small-study effects:" di as text `"Regress intervention effect estimate on {bf:1/}{it:Ntot}, with weights {it:S}{bf:×}{it:F}{bf:/}{it:Ntot}"' _n tempname pbias rmse qui regress `theta' `ovN' [aweight=`wgt'], noheader scalar `pbias' = 2*ttail( e(df_r), abs(_b[`ovN']/_se[`ovN']) ) scalar `rmse' = e(rmse) return scalar rmse = e(rmse) return scalar p_bias = `pbias' return scalar se_bias = _se[`ovN'] return scalar bias = _b[`ovN'] return scalar df_r = e(df_r) return scalar N = e(N) tempname vcov b matrix define `vcov' = e(V) matrix define `b' = e(b) matrix colnames `b' = bias constant matrix rownames `vcov' = bias constant matrix colnames `vcov' = bias constant ereturn post `b' `vcov', depname(Std_Eff) dof(`e(df_r)') obs(`e(N)') di as txt "Number of studies = " as res e(N) _c di as txt _col(55) " Root MSE = " as res %6.0g `rmse' ereturn display, level(`level') di as text _n "Test of H0: no small-study effects" _col(45) "P = " as res %5.3f `pbias' end ***************************************** // RJH added- Begg test ***************************************** program define begg, rclass byable(recall) syntax varlist(min=2 max=2) [if] [in] [, LEVEL(real 95) noFit Graph ] marksample touse tokenize `varlist' args theta se_theta if "`graph'"!="" { di as err "option {bf:graph} cannot be used with Begg test" exit 198 } if "`fit'"!="" { di as err "option {bf:nofit} cannot be used with Begg test" exit 198 } quietly { tempvar var w gen `var' = `se_theta'^2 if `touse' gen `w' = 1 / `var' if `touse' tempvar vt wtheta tempname sw summ `w' if `touse', meanonly scalar `sw' = r(sum) gen `vt' = `var' - 1/`sw' if `touse' gen `wtheta' = `w' * `theta' if `touse' tempvar Ts wl tempname oe summ `wtheta' if `touse', meanonly gen `Ts' = (`theta' - `r(sum)'/`sw') / sqrt(`vt') if `touse' gen `wl' = `w' * `theta' if `touse' summ `wl' if `touse', meanonly scalar `oe' = r(sum) / `sw' } // end quietly capture ktau2 `var' `Ts' if `touse' if _rc & _rc != 2001 { di as err "error " _rc `" in call to {bf:ktau2}"' exit _rc } di _n as text "Begg's test for small-study effects:" di as text "Rank correlation between standardized intervention effect and its standard error" di as text " " di as text " adj. Kendall's Score (P-Q) = " as res %7.0f r(ks) di _c as text " Std. Dev. of Score = " as res %7.2f r(se_ks) if r(c) == 1 { di as text " (corrected for ties)" } else { di " " } di as text " Number of Studies = " as res %7.0f r(N) di as text " z = " as res %7.2f r(z) di as text " Pr > |z| = " as res %7.3f r(p) di _c as text " z = " as res %7.2f r(zcc) di as text " (continuity corrected)" di _c as text " Pr > |z| = " as res %7.3f r(pcc) di as text " (continuity corrected)" return scalar p_bias_ncc = r(p) return scalar p_bias = r(pcc) return scalar score_sd = r(se_ks) return scalar score = r(ks) return scalar N = r(N) end **************************************************** * ktau program from original metabias -RJH * **************************************************** * version 4.1.0 26sep97 TJS * modification of ktau to allow N==2, un-continuity-corrected * z and p values, and to pass more parameters program define ktau2, rclass sortpreserve version 9.0 syntax varlist(min=2 max=2) [if] [in] marksample touse tokenize `varlist' args x y quietly count if `touse' local N = r(N) if `N' < 2 { exit 2001 } quietly { replace `touse' = 1 - `touse' sort `touse' /* put obs for computation first */ replace `touse' = 1 - `touse' tempvar work gen double `work' = 0 /* using type double is fastest */ local k = 2 while (`k' <= `N') { local kk = `k' - 1 replace `work' = `work' + sign((`x' - `x'[`k'])*(`y' - `y'[`k'])) in 1 / `kk' /* using "in" is fastest */ local ++k } replace `work' = sum(`work') if `touse' tempname ks scalar `ks' = `work'[`N'] /* Calculate ties on `x' */ tempvar nobs egen long `nobs' = count(`x') if `touse', by(`x') summ `nobs', meanonly local nobsxm = r(max) /* Calculate correction term for ties on `x' */ tempname xt replace `work' = sum((`nobs' - 1)*(2*`nobs' + 5)) if `touse' scalar `xt' = `work'[`N'] /* Calculate correction term for pairs of ties on `x' */ tempname xt2 replace `work' = sum(`nobs' - 1) if `touse' scalar `xt2' = `work'[`N'] /* Calculate correction term for triplets of ties on `x' */ tempname xt3 replace `work' = sum((`nobs' - 1)*(`nobs' - 2)) if `touse' scalar `xt3' = `work'[`N'] /* Calculate ties on `y' */ drop `nobs' egen long `nobs' = count(`y') if `touse', by(`y') summ `nobs', meanonly local nobsym = r(max) /* Calculate correction term for ties on `y' */ tempname yt replace `work' = sum((`nobs' - 1)*(2*`nobs' + 5)) if `touse' scalar `yt' = `work'[`N'] /* Calculate correction term for pairs of ties on `y' */ tempname yt2 replace `work' = sum(`nobs' - 1) if `touse' scalar `yt2' = `work'[`N'] /* Calculate correction term for triplets of ties on `y' */ tempname yt3 replace `work' = sum((`nobs' - 1)*(`nobs' - 2)) if `touse' scalar `yt3' = `work'[`N'] } // end quietly /* Compute Kendall's tau-a, tau-b, s.e. of score, and pval */ tempname NN tau_a tau_b se_ks scalar `NN' = `N'*(`N' - 1) scalar `tau_a' = 2*`ks'/`NN' scalar `tau_b' = 2*`ks'/sqrt((`NN' - `xt2')*(`NN' - `yt2')) scalar `se_ks' = `NN'*(2*`N' + 5) if max(`nobsxm', `nobsym') > 1 { scalar `se_ks' = `se_ks' - (`xt' - `yt') + `xt3'*`yt3'/(9*`NN'*(`N' - 2)) + `xt2'*`yt2'/(2*`NN') } scalar `se_ks' = sqrt(`se_ks' / 18) tempname z zcc scalar `z' = `ks '/ `se_ks' scalar `zcc' = (abs(`ks') - 1) / `se_ks' tempname p pcc if `ks' == 0 { scalar `p' = 1 scalar `pcc' = 1 } else { scalar `p' = 2*(1 - normal( abs(`ks') / `se_ks')) scalar `pcc' = 2*(1 - normal((abs(`ks') - 1)/ `se_ks')) } /* Print results */ di _n as text " Number of obs = " as res %7.0f `N' di as text "Kendall's tau-a = " as res %12.4f `tau_a' di as text "Kendall's tau-b = " as res %12.4f `tau_b' di as text "Kendall's score = " as res %7.0f `ks' di as text " SE of score = " as res %11.3f `se_ks' _c if `xt2' > 0 | `yt2' > 0 { di as text " (corrected for ties)" _c } di _n(2) as text "Test of H0: `x' and `y' independent" di as text " z = " as res %12.2f `z' di as text " Pr > |z| = " as res %12.4f = `p' di "" di as text " z = " as res %12.2f sign(`ks')*`zcc' di as text " Pr > |z| = " as res %12.4f = `pcc' as text " (continuity corrected)" local c = 0 if `xt2' > 0 | `yt2' > 0 local c = 1 return scalar N = `N' return scalar tau_a = `tau_a' return scalar tau_b = `tau_b' return scalar ks = `ks' return scalar se_ks = `se_ks' return scalar p = `p' return scalar z = `z' return scalar pcc = `pcc' return scalar zcc = `zcc' return scalar c = `c' end