*! midas_metareg.ado — Bivariate meta-regression for DTA meta-analysis *! Version 2.0.0 31mar2026 *! Author: Ben Adarkwa Dwamena, MD *! *! Adds study-level covariates to the bivariate model: *! logit(Se_i) = mu1 + beta1*X_i + u1i *! logit(Sp_i) = mu2 + beta2*X_i + u2i *! *! v2.0: For binary covariates, adds comparative SROC plot with *! subgroup summary points and confidence regions. capture program drop midas_metareg program define midas_metareg, eclass version 16 #delimit ; syntax varlist(min=4 max=4 numeric) [if] [in], ID(varname) COVariates(varlist numeric) [SENonly SPEonly LEVEL(cilevel) NOGraph SAVEtable(string) NOIsily SUBESTimator(string) RPATH(string) STANdir(string) MODELfile(string) OUTPUTfile(string) CHains(integer 4) WARMup(integer 1000) ITER(integer 10000) THIN(integer 10) SEED(integer 12345) SUBCOVariance(string) *] ; #delimit cr if "`senonly'" != "" & "`speonly'" != "" { di as error "senonly and speonly are mutually exclusive" exit 198 } * Subgroup estimator for binary SROC (default: mle) if "`subestimator'" == "" local subestimator "mle" if !inlist("`subestimator'", "mle", "inla", "hmc") { di as error "subestimator() must be one of: mle, inla, hmc" exit 198 } * Build subgroup estimator option strings local inla_opts "" if "`rpath'" != "" local inla_opts `"rpath("`rpath'")"' local hmc_opts "" if "`standir'" != "" local hmc_opts `"`hmc_opts' standir("`standir'")"' if "`modelfile'" != "" local hmc_opts `"`hmc_opts' modelfile("`modelfile'")"' if "`outputfile'" != "" local hmc_opts `"`hmc_opts' outputfile("`outputfile'")"' if "`subcovariance'" != "" local hmc_opts `"`hmc_opts' covariance(`subcovariance')"' local hmc_opts `"`hmc_opts' chains(`chains') warmup(`warmup') iter(`iter') thin(`thin') seed(`seed')"' tokenize `varlist' local tp `1' local fp `2' local fn `3' local tn `4' marksample touse * Count covariates local ncov: word count `covariates' di as text _n "{hline 70}" di as result "MIDAS Bivariate Meta-Regression" di as text "{hline 70}" di as text "Covariates: " as result "`covariates' (`ncov')" if "`senonly'" != "" di as text "Applied to: " as result "Sensitivity only" else if "`speonly'" != "" di as text "Applied to: " as result "Specificity only" else di as text "Applied to: " as result "Both Se and Sp" di as text "{hline 70}" * ============================================================ * Check for binary covariate (for comparative SROC) * ============================================================ local is_binary 0 local bincov "" if `ncov' == 1 { local bincov: word 1 of `covariates' qui tab `bincov' if `touse' if r(r) == 2 { local is_binary 1 qui levelsof `bincov' if `touse', local(binlevels) } } * ============================================================ * Data preparation (mirrors midas_mle internals) * ============================================================ preserve qui keep if `touse' qui count local nstud = r(N) * Study labels capture confirm string variable `id' if _rc { qui tostring `id', gen(_midas_studylabel) } else { qui gen _midas_studylabel = `id' } sort _midas_studylabel qui gen _midas_studyid = _n * Note: binary SROC uses original data directly (restore/re-preserve) * Create the long-format data for meglm qui gen _midas_dis1 = `tp' + `fn' qui gen _midas_dis0 = `tn' + `fp' qui gen _midas_dep1 = `tp' qui gen _midas_dep2 = `tn' qui gen _midas_denom1 = _midas_dis1 qui gen _midas_denom2 = _midas_dis0 qui reshape long _midas_dep _midas_denom, i(_midas_studyid) j(_midas_dis_status) * Interaction terms: covariate * disease status indicator qui gen _dis1 = (_midas_dis_status == 1) qui gen _dis2 = (_midas_dis_status == 2) * Build covariate interaction terms local cov_se_terms "" local cov_sp_terms "" foreach cv of local covariates { if "`speonly'" == "" { qui gen _cx_se_`cv' = _dis1 * `cv' local cov_se_terms "`cov_se_terms' _cx_se_`cv'" } if "`senonly'" == "" { qui gen _cx_sp_`cv' = _dis2 * `cv' local cov_sp_terms "`cov_sp_terms' _cx_sp_`cv'" } } local allcov "`cov_se_terms' `cov_sp_terms'" * ============================================================ * Fit bivariate meta-regression via meglm * ============================================================ di as text "" di as text "Fitting bivariate meta-regression model..." di as text "" * Base model: intercepts + covariates, random effects for Se and Sp #delimit ; capture noisily meglm _midas_dep _dis1 _dis2 `allcov', nocons family(binomial _midas_denom) link(logit) || _midas_studyid: _dis1 _dis2, nocons cov(unstructured) intpoints(7) `noisily' ; #delimit cr if _rc { di as error "Meta-regression model failed to converge" restore exit _rc } * ============================================================ * Extract and display results * ============================================================ * Intercepts local mu1 = _b[_dis1] local mu2 = _b[_dis2] local se_mu1 = _se[_dis1] local se_mu2 = _se[_dis2] di as text _n "{hline 70}" di as result "Meta-Regression Results" di as text "{hline 70}" di as text "" di as text " Intercepts (baseline):" di as text "{hline 60}" di as text " Parameter" _col(20) "Coef" _col(32) "SE" _col(42) "z" _col(50) "p" _col(58) "[95% CI]" di as text "{hline 60}" local z1 = `mu1'/`se_mu1' local p1 = 2*(1-normal(abs(`z1'))) local lo1 = `mu1' - 1.96*`se_mu1' local hi1 = `mu1' + 1.96*`se_mu1' di as text " logit Se" _col(16) as result %10.4f `mu1' _col(28) %8.4f `se_mu1' /// _col(40) %6.2f `z1' _col(48) %6.4f `p1' " " %6.3f `lo1' " " %6.3f `hi1' local z2 = `mu2'/`se_mu2' local p2 = 2*(1-normal(abs(`z2'))) local lo2 = `mu2' - 1.96*`se_mu2' local hi2 = `mu2' + 1.96*`se_mu2' di as text " logit Sp" _col(16) as result %10.4f `mu2' _col(28) %8.4f `se_mu2' /// _col(40) %6.2f `z2' _col(48) %6.4f `p2' " " %6.3f `lo2' " " %6.3f `hi2' * Covariate effects di as text "" di as text " Covariate effects:" di as text "{hline 60}" foreach cv of local covariates { if "`speonly'" == "" { local b_se_`cv' = _b[_cx_se_`cv'] local se_bse_`cv' = _se[_cx_se_`cv'] local z_se = `b_se_`cv''/`se_bse_`cv'' local p_se = 2*(1-normal(abs(`z_se'))) local lo_se = `b_se_`cv'' - 1.96*`se_bse_`cv'' local hi_se = `b_se_`cv'' + 1.96*`se_bse_`cv'' di as text " `cv' -> Se" _col(16) as result %10.4f `b_se_`cv'' _col(28) %8.4f `se_bse_`cv'' /// _col(40) %6.2f `z_se' _col(48) %6.4f `p_se' " " %6.3f `lo_se' " " %6.3f `hi_se' if `p_se' < 0.05 { di as text " * Significant at 5% level" } } if "`senonly'" == "" { local b_sp_`cv' = _b[_cx_sp_`cv'] local se_bsp_`cv' = _se[_cx_sp_`cv'] local z_sp = `b_sp_`cv''/`se_bsp_`cv'' local p_sp = 2*(1-normal(abs(`z_sp'))) local lo_sp = `b_sp_`cv'' - 1.96*`se_bsp_`cv'' local hi_sp = `b_sp_`cv'' + 1.96*`se_bsp_`cv'' di as text " `cv' -> Sp" _col(16) as result %10.4f `b_sp_`cv'' _col(28) %8.4f `se_bsp_`cv'' /// _col(40) %6.2f `z_sp' _col(48) %6.4f `p_sp' " " %6.3f `lo_sp' " " %6.3f `hi_sp' if `p_sp' < 0.05 { di as text " * Significant at 5% level" } } } di as text "{hline 60}" * Random effects — extract variance components from meglm * Stata 19.5 meglm stores RE params in e(b) under equation "/" with * column names like var(_dis1[...]), var(_dis2[...]), cov(...) * Values are already on the variance/covariance scale. local tau2_1 = . local tau2_2 = . local cov_re_12 = . * Method 1: Try /lns style (older Stata versions store log-SD) capture local tau2_1 = exp(_b[/lns1_1_1])^2 capture local tau2_2 = exp(_b[/lns1_1_2])^2 * Method 2: Scan e(b) for eq="/" columns with var()/cov() in the name * Stata 19.5 meglm stores these on variance/covariance scale directly if `tau2_1' >= . | `tau2_2' >= . { tempname eb_full mat `eb_full' = e(b) local eqnames: coleq `eb_full' local cnames: colnames `eb_full' local ncols = colsof(`eb_full') local var_count = 0 forval j = 1/`ncols' { local eq: word `j' of `eqnames' local cn: word `j' of `cnames' if "`eq'" == "/" { if strpos("`cn'", "var(") == 1 { local ++var_count if `var_count' == 1 { local tau2_1 = `eb_full'[1,`j'] } else if `var_count' == 2 { local tau2_2 = `eb_full'[1,`j'] } } else if strpos("`cn'", "cov(") == 1 { local cov_re_12 = `eb_full'[1,`j'] } } } } di as text "" di as text " Random effects (residual heterogeneity after regression):" di as text "{hline 60}" di as text _col(3) "tau2(logit Se)" _col(25) as result %8.4f `tau2_1' di as text _col(3) "tau2(logit Sp)" _col(25) as result %8.4f `tau2_2' di as text "{hline 60}" * Model fit local ll = e(ll) local aic = -2*`ll' + 2*(2 + `ncov'*2 + 3) local bic = -2*`ll' + ln(`nstud')*(2 + `ncov'*2 + 3) di as text "" di as text " Model fit:" di as text _col(3) "Log-likelihood" _col(25) as result %10.3f `ll' di as text _col(3) "AIC" _col(25) as result %10.3f `aic' di as text _col(3) "BIC" _col(25) as result %10.3f `bic' di as text _col(3) "N studies" _col(25) as result `nstud' di as text "{hline 70}" * ============================================================ * Store results * ============================================================ ereturn scalar ll = `ll' ereturn scalar AIC = `aic' ereturn scalar BIC = `bic' ereturn scalar N = `nstud' ereturn scalar ncov = `ncov' ereturn local covariates "`covariates'" ereturn local estimator "metareg" ereturn local package "midas" ereturn local cmd "midas_metareg" * ============================================================ * Graphical output * ============================================================ if "`nograph'" == "" & `ncov' == 1 { local cv: word 1 of `covariates' if `is_binary' { * ========================================================== * COMPARATIVE SROC PLOT for binary covariate * ========================================================== * We are inside the outer preserve. Restore first to get * back to original data, then re-preserve for the plot. restore preserve qui keep if `touse' * --- Fit separate models per subgroup --- local gi = 0 foreach lev of local binlevels { local ++gi * Subgroup data qui count if `bincov' == `lev' local k_`gi' = r(N) * Get value label if available local vlname: value label `bincov' if "`vlname'" != "" { local lab_`gi': label `vlname' `lev' } else { local lab_`gi' "`bincov'=`lev'" } * ------------------------------------------- * Fit bivariate model for this subgroup * ------------------------------------------- * Save current state, subset, fit, extract, restore tempfile _sg_save qui save `_sg_save', replace qui keep if `bincov' == `lev' qui count local nk = r(N) if "`subestimator'" == "mle" { * Direct meglm fit qui gen _sid = _n qui gen _dep1 = `tp' qui gen _dep2 = `tn' qui gen _den1 = `tp' + `fn' qui gen _den2 = `tn' + `fp' qui reshape long _dep _den, i(_sid) j(_ds) qui gen _d1 = (_ds == 1) qui gen _d2 = (_ds == 2) capture noisily qui meglm _dep _d1 _d2, nocons /// family(binomial _den) link(logit) /// || _sid: _d1 _d2, nocons cov(unstructured) /// intpoints(7) if _rc { di as text " Note: MLE failed for subgroup `lab_`gi'' — skipping" local mu1_`gi' = . local mu2_`gi' = . local se_mu1_`gi' = . local se_mu2_`gi' = . local cov12_`gi' = 0 local summ_se_`gi' = . local summ_sp_`gi' = . local summ_fpr_`gi' = . local k_`gi' = `nk' qui use `_sg_save', clear continue } * Extract summary operating point from meglm local mu1_`gi' = _b[_d1] local mu2_`gi' = _b[_d2] local se_mu1_`gi' = _se[_d1] local se_mu2_`gi' = _se[_d2] tempname vcov mat `vcov' = e(V) local cov12_`gi' = `vcov'[1,2] } else if "`subestimator'" == "inla" { * Use midas inla for this subgroup capture noisily { midas inla `tp' `fp' `fn' `tn', id(`id') `inla_opts' } if _rc { di as text " Note: INLA failed for subgroup `lab_`gi'' — skipping" local mu1_`gi' = . local mu2_`gi' = . local se_mu1_`gi' = . local se_mu2_`gi' = . local cov12_`gi' = 0 local summ_se_`gi' = . local summ_sp_`gi' = . local summ_fpr_`gi' = . local k_`gi' = `nk' qui use `_sg_save', clear continue } * Extract from midas inla e() results local mu1_`gi' = e(b)[1,1] local mu2_`gi' = e(b)[1,2] local se_mu1_`gi' = sqrt(e(V)[1,1]) local se_mu2_`gi' = sqrt(e(V)[2,2]) local cov12_`gi' = e(V)[1,2] } else if "`subestimator'" == "hmc" { * Use midas hmc for this subgroup capture noisily { midas hmc `tp' `fp' `fn' `tn', id(`id') `hmc_opts' } if _rc { di as text " Note: HMC failed for subgroup `lab_`gi'' — skipping" local mu1_`gi' = . local mu2_`gi' = . local se_mu1_`gi' = . local se_mu2_`gi' = . local cov12_`gi' = 0 local summ_se_`gi' = . local summ_sp_`gi' = . local summ_fpr_`gi' = . local k_`gi' = `nk' qui use `_sg_save', clear continue } * Extract from midas hmc e() results local mu1_`gi' = e(b)[1,1] local mu2_`gi' = e(b)[1,2] local se_mu1_`gi' = sqrt(e(V)[1,1]) local se_mu2_`gi' = sqrt(e(V)[2,2]) local cov12_`gi' = e(V)[1,2] } * Summary Se and Sp on probability scale local summ_se_`gi' = invlogit(`mu1_`gi'') local summ_sp_`gi' = invlogit(`mu2_`gi'') local summ_fpr_`gi' = 1 - `summ_sp_`gi'' * Reload full subgroup data qui use `_sg_save', clear } * --- Build confidence and prediction ellipses via Mata --- local npts 100 local ngrp = `gi' * RE covariance was extracted before restore (tau2_1, tau2_2 already set) * Extract cov from the original meglm e(b) — saved as cov_re_12 * (tau2_1, tau2_2, cov_re_12 were set before the restore+preserve) * Check if at least one subgroup converged local any_converged = 0 forval g = 1/`ngrp' { if `summ_se_`g'' < . { local any_converged = 1 } } if `any_converged' == 0 { di as text " Note: No subgroups converged — skipping SROC plot" } else { mata: _midas_metareg_ellipses() * --- Construct the SROC plot --- * Determine colors for groups local col1 "blue" local col2 "red" local mcol1 "blue" local mcol2 "red" * Build summary table text for graph note * Format: Group | k | Se (95% CI) | Sp (95% CI) local tabline1 "" local tabline2 "" forval g = 1/`ngrp' { if `summ_se_`g'' == . continue * Delta method CIs for Se and Sp local se_lo = invlogit(`mu1_`g'' - 1.96*`se_mu1_`g'') local se_hi = invlogit(`mu1_`g'' + 1.96*`se_mu1_`g'') local sp_lo = invlogit(`mu2_`g'' - 1.96*`se_mu2_`g'') local sp_hi = invlogit(`mu2_`g'' + 1.96*`se_mu2_`g'') local fse: di %4.2f `summ_se_`g'' local fsp: di %4.2f `summ_sp_`g'' local fse_lo: di %4.2f `se_lo' local fse_hi: di %4.2f `se_hi' local fsp_lo: di %4.2f `sp_lo' local fsp_hi: di %4.2f `sp_hi' local fk_g: di %3.0f `k_`g'' local tabline`g' "`lab_`g'' (k=`fk_g'): Se=`fse' (`fse_lo'-`fse_hi'), Sp=`fsp' (`fsp_lo'-`fsp_hi')" } * --- Assemble the graph --- local plotcmds "" local legendord "" local li = 0 forval g = 1/`ngrp' { if `summ_se_`g'' == . continue * 1) Prediction ellipse (wider, dashed line) local ++li local plotcmds "`plotcmds' (line _pell_se_`g' _pell_fpr_`g', lcolor(`col`g''%60) lwidth(medium) lpattern(dash) cmissing(n))" local legendord "`legendord' `li' `"`lab_`g'' 95% PR"'" * 2) Confidence ellipse (tighter, solid line) local ++li local plotcmds "`plotcmds' (line _cell_se_`g' _cell_fpr_`g', lcolor(`col`g'') lwidth(medthick) lpattern(solid) cmissing(n))" local legendord "`legendord' `li' `"`lab_`g'' 95% CR"'" * 3) Summary operating point local ++li local plotcmds "`plotcmds' (scatteri `summ_se_`g'' `summ_fpr_`g'', ms(D) msize(vlarge) mcolor(`mcol`g'') mlcolor(black) mlwidth(thin))" local legendord "`legendord' `li' `"`lab_`g'' summary"'" } * Build full note text local notetxt "" forval g = 1/`ngrp' { if "`tabline`g''" != "" { if "`notetxt'" == "" { local notetxt "`tabline`g''" } else { local notetxt "`notetxt'" "`tabline`g''" } } } #delimit ; twoway `plotcmds', xti("1 - Specificity (FPR)") yti("Sensitivity") yla(0(0.2)1, angle(horizontal) format(%3.1f)) xla(0(0.2)1, format(%3.1f)) xscale(range(0 1)) yscale(range(0 1)) aspectratio(1) legend(order(`legendord') pos(5) ring(0) col(1) size(*.55) symxsize(*.7) region(fcolor(white%90) lcolor(gs12))) title("Comparative SROC: Effect of `cv'", size(*.85)) subtitle("Meta-regression, `nstud' studies", size(*.7)) note(`"`tabline1'"' `"`tabline2'"', size(*.5) span) name(metareg_sroc, replace) ; #delimit cr } // end if any_converged } else { * ========================================================== * STANDARD BUBBLE PLOT for continuous covariates * ========================================================== * Compute study-level Se and Sp tempvar studysen studyspe studyn qui gen `studysen' = `tp'/(`tp'+`fn') qui gen `studyspe' = `tn'/(`tn'+`fp') qui gen `studyn' = `tp'+`fp'+`fn'+`tn' * Predicted values from regression tempvar predse predspe if "`speonly'" == "" { local b_se = _b[_cx_se_`cv'] qui gen `predse' = invlogit(`mu1' + `b_se'*`cv') } if "`senonly'" == "" { local b_sp = _b[_cx_sp_`cv'] qui gen `predspe' = invlogit(`mu2' + `b_sp'*`cv') } * Two-panel plot if "`speonly'" == "" & "`senonly'" == "" { #delimit ; twoway (scatter `studysen' `cv' [aw=`studyn'], ms(Oh) mc(blue%60)) (line `predse' `cv', sort lc(blue) lw(medthick)) (scatter `studyspe' `cv' [aw=`studyn'], ms(Dh) mc(red%60)) (line `predspe' `cv', sort lc(red) lw(medthick)), legend(order(1 "Study Se" 2 "Predicted Se" 3 "Study Sp" 4 "Predicted Sp") pos(6) row(2) size(*.65)) yti("Sensitivity / Specificity") xti("`cv'") yla(0(0.2)1, angle(horizontal) format(%3.1f)) title("Meta-Regression: Effect of `cv'", size(*.85)) subtitle("Bivariate model, `nstud' studies", size(*.7)) name(metareg_bubble, replace) ; #delimit cr } else if "`speonly'" == "" { #delimit ; twoway (scatter `studysen' `cv' [aw=`studyn'], ms(Oh) mc(blue%60)) (line `predse' `cv', sort lc(blue) lw(medthick)), legend(order(1 "Study Se" 2 "Predicted Se") pos(6) row(1) size(*.65)) yti("Sensitivity") xti("`cv'") yla(0(0.2)1, angle(horizontal) format(%3.1f)) title("Meta-Regression: Effect of `cv' on Sensitivity", size(*.85)) name(metareg_bubble, replace) ; #delimit cr } else { #delimit ; twoway (scatter `studyspe' `cv' [aw=`studyn'], ms(Dh) mc(red%60)) (line `predspe' `cv', sort lc(red) lw(medthick)), legend(order(1 "Study Sp" 2 "Predicted Sp") pos(6) row(1) size(*.65)) yti("Specificity") xti("`cv'") yla(0(0.2)1, angle(horizontal) format(%3.1f)) title("Meta-Regression: Effect of `cv' on Specificity", size(*.85)) name(metareg_bubble, replace) ; #delimit cr } } } * ============================================================ * Save LaTeX table if requested * ============================================================ if "`savetable'" != "" { capture file close _mrt file open _mrt using "`savetable'", write replace file write _mrt "% Auto-generated by midas metareg" _n file write _mrt "\begin{table}[htbp]" _n file write _mrt "\centering" _n file write _mrt "\caption{Bivariate Meta-Regression Results}" _n file write _mrt "\label{tab:metareg}" _n file write _mrt "\begin{tabular}{lrrrrr}" _n file write _mrt "\toprule" _n file write _mrt "Parameter & Coef. & SE & $z$ & $p$ & 95\% CI \\" _n file write _mrt "\midrule" _n * Intercepts file write _mrt %~20s "logit Se" " & " %8.4f (`mu1') " & " %8.4f (`se_mu1') /// " & " %6.2f (`z1') " & " %6.4f (`p1') " & " /// %6.3f (`lo1') " to " %6.3f (`hi1') " \\" _n file write _mrt %~20s "logit Sp" " & " %8.4f (`mu2') " & " %8.4f (`se_mu2') /// " & " %6.2f (`z2') " & " %6.4f (`p2') " & " /// %6.3f (`lo2') " to " %6.3f (`hi2') " \\" _n file write _mrt "\midrule" _n foreach cv of local covariates { if "`speonly'" == "" { local b = `b_se_`cv'' local s = `se_bse_`cv'' local z = `b'/`s' local p = 2*(1-normal(abs(`z'))) local lo = `b' - 1.96*`s' local hi = `b' + 1.96*`s' local star "" if `p' < 0.05 local star "*" file write _mrt %~20s "`cv' $\to$ Se" " & " %8.4f (`b') " & " %8.4f (`s') /// " & " %6.2f (`z') " & " %6.4f (`p') "`star'" " & " /// %6.3f (`lo') " to " %6.3f (`hi') " \\" _n } if "`senonly'" == "" { local b = `b_sp_`cv'' local s = `se_bsp_`cv'' local z = `b'/`s' local p = 2*(1-normal(abs(`z'))) local lo = `b' - 1.96*`s' local hi = `b' + 1.96*`s' local star "" if `p' < 0.05 local star "*" file write _mrt %~20s "`cv' $\to$ Sp" " & " %8.4f (`b') " & " %8.4f (`s') /// " & " %6.2f (`z') " & " %6.4f (`p') "`star'" " & " /// %6.3f (`lo') " to " %6.3f (`hi') " \\" _n } } file write _mrt "\midrule" _n file write _mrt %~20s "$\tau^2$(logit Se)" " & \multicolumn{5}{c}{" %8.4f (`tau2_1') "} \\" _n file write _mrt %~20s "$\tau^2$(logit Sp)" " & \multicolumn{5}{c}{" %8.4f (`tau2_2') "} \\" _n file write _mrt "\midrule" _n file write _mrt %~20s "Log-likelihood" " & \multicolumn{5}{c}{" %10.3f (`ll') "} \\" _n file write _mrt %~20s "AIC" " & \multicolumn{5}{c}{" %10.3f (`aic') "} \\" _n file write _mrt %~20s "BIC" " & \multicolumn{5}{c}{" %10.3f (`bic') "} \\" _n file write _mrt "\bottomrule" _n file write _mrt "\end{tabular}" _n file write _mrt "\end{table}" _n file close _mrt di as text " [TEX] `savetable'" } restore end * ============================================================================== * Mata helper: compute confidence ellipses for subgroups * ============================================================================== version 16 mata: void _midas_metareg_ellipses() { real scalar ngrp, npts, g, i, N real scalar mu1, mu2, se_mu1, se_mu2, cov12, chi2_crit real scalar tau2_1, tau2_2, cov_re_12 real matrix Sigma_c, Sigma_p, Chol_c, Chol_p, ell_c, ell_p real colvector theta real rowvector uv string scalar vname npts = 100 chi2_crit = invchi2(2, 0.95) // 5.991 for 95% CI ngrp = strtoreal(st_local("ngrp")) tau2_1 = strtoreal(st_local("tau2_1")) tau2_2 = strtoreal(st_local("tau2_2")) cov_re_12 = strtoreal(st_local("cov_re_12")) // Angular grid — closed polygon (101 points) theta = rangen(0, 2*pi(), npts+1) // Ensure enough observations N = st_nobs() if (N < npts + 1) { st_addobs(npts + 1 - N) N = npts + 1 } for (g=1; g<=ngrp; g++) { mu1 = strtoreal(st_local("mu1_" + strofreal(g))) mu2 = strtoreal(st_local("mu2_" + strofreal(g))) if (mu1 >= . | mu2 >= .) continue se_mu1 = strtoreal(st_local("se_mu1_" + strofreal(g))) se_mu2 = strtoreal(st_local("se_mu2_" + strofreal(g))) cov12 = strtoreal(st_local("cov12_" + strofreal(g))) // ------------------------------------------------------- // Confidence ellipse: VCE of (logit_se, logit_sp) means // ------------------------------------------------------- Sigma_c = (se_mu1^2, cov12 \ cov12, se_mu2^2) if (det(Sigma_c) <= 0 | se_mu1 <= 0 | se_mu2 <= 0) continue Chol_c = cholesky(Sigma_c) ell_c = J(npts+1, 2, .) for (i=1; i<=npts+1; i++) { uv = (cos(theta[i]), sin(theta[i])) ell_c[i,1] = mu1 + sqrt(chi2_crit) * (Chol_c[1,1]*uv[1] + Chol_c[1,2]*uv[2]) ell_c[i,2] = mu2 + sqrt(chi2_crit) * (Chol_c[2,1]*uv[1] + Chol_c[2,2]*uv[2]) } ell_c[,1] = invlogit(ell_c[,1]) ell_c[,2] = 1 :- invlogit(ell_c[,2]) // Store confidence ellipse vname = "_cell_se_" + strofreal(g) (void) st_addvar("double", vname) st_store((1::npts+1), vname, ell_c[,1]) vname = "_cell_fpr_" + strofreal(g) (void) st_addvar("double", vname) st_store((1::npts+1), vname, ell_c[,2]) // ------------------------------------------------------- // Prediction ellipse: VCE of means + residual heterogeneity // ------------------------------------------------------- if (tau2_1 >= . | tau2_2 >= . | tau2_1 <= 0 | tau2_2 <= 0) { // Create empty variables so twoway doesn't error vname = "_pell_se_" + strofreal(g) (void) st_addvar("double", vname) vname = "_pell_fpr_" + strofreal(g) (void) st_addvar("double", vname) continue } // Use RE covariance if available, else 0 if (cov_re_12 >= .) cov_re_12 = 0 Sigma_p = (se_mu1^2 + tau2_1, cov12 + cov_re_12 \ cov12 + cov_re_12, se_mu2^2 + tau2_2) if (det(Sigma_p) <= 0) { vname = "_pell_se_" + strofreal(g) (void) st_addvar("double", vname) vname = "_pell_fpr_" + strofreal(g) (void) st_addvar("double", vname) continue } Chol_p = cholesky(Sigma_p) ell_p = J(npts+1, 2, .) for (i=1; i<=npts+1; i++) { uv = (cos(theta[i]), sin(theta[i])) ell_p[i,1] = mu1 + sqrt(chi2_crit) * (Chol_p[1,1]*uv[1] + Chol_p[1,2]*uv[2]) ell_p[i,2] = mu2 + sqrt(chi2_crit) * (Chol_p[2,1]*uv[1] + Chol_p[2,2]*uv[2]) } ell_p[,1] = invlogit(ell_p[,1]) ell_p[,2] = 1 :- invlogit(ell_p[,2]) // Store prediction ellipse vname = "_pell_se_" + strofreal(g) (void) st_addvar("double", vname) st_store((1::npts+1), vname, ell_p[,1]) vname = "_pell_fpr_" + strofreal(g) (void) st_addvar("double", vname) st_store((1::npts+1), vname, ell_p[,2]) } } end