*! 1.1.0 Ariel Linden 05Feb2026 // added truncation if LCL < 0 and UCL > 1 *! 1.0.0 Ariel Linden 30Jan2026 program define tau2ci, rclass version 11 syntax [, Level(cilevel)] // check if meta regress was last estimation model if !inlist("`e(cmd)'", "meta regress") { di as error "{bf:tau2ci} will only work after {bf:meta regress} with the {bf:random} option" exit 301 } // check if random-effects model was estimated if "`e(model)'" != "random" { di as error "{bf:tau2ci} will only work with a random-effects model" exit 198 } local k = e(N) local tau2 = e(tau2) local stmethod = e(method) // map to method names for display if "`stmethod'" == "reml" local rmethod "restricted maximum likelihood" else if "`stmethod'" == "mle" local rmethod "maximum likelihood" else if "`stmethod'" == "dlaird" local rmethod "DerSimonian–Laird" else if "`stmethod'" == "ebayes" local rmethod "empirical Bayes" else if "`stmethod'" == "sjonkman" local rmethod "Sidik–Jonkman" else if "`stmethod'" == "hedges" local rmethod "Hedges" else if "`stmethod'" == "hschmidt" local rmethod "Hunter–Schmidt" else local rmethod "`stmethod'" // get moderator names matrix b = e(b) local p = colsof(b) foreach name in `:colnames b' { if "`name'" != "_cons" local xvars "`xvars' `name'" } // generate sample indicator tempvar esample gen byte `esample' = e(sample) // call Mata function to compute std err and CIs mata: compute_tau2_se("`esample'", "`xvars'", `tau2', `k', `p', "`rmethod'", "`stmethod'", `level') // get results local se_tau2 = r(se_tau2) local ci_lb = r(ci_lb) local ci_ub = r(ci_ub) // track if lower CI was truncated local clipped_lower = 0 if `ci_lb' < 0 { local ci_lb = 0 local clipped_lower = 1 } // track if upper CI was truncated at 1 local clipped_upper = 0 if `ci_ub' > 1 { local ci_ub = 1 local clipped_upper = 1 } // return values return scalar se = `se_tau2' return scalar tau2 = `tau2' return scalar ll = `ci_lb' return scalar ul = `ci_ub' return scalar clipped_lower = `clipped_lower' return scalar clipped_upper = `clipped_upper' return local method "`stmethod'" // display output table tempname mytab .`mytab' = ._tab.new, col(5) lmargin(0) .`mytab'.width 10 |11 12 12 12 .`mytab'.titlefmt . . . %24s . .`mytab'.pad . 1 1 3 3 .`mytab'.numfmt . %9.0g %9.0g %9.0g %9.0g .`mytab'.strcolor result . . . . .`mytab'.strfmt %19s . . . . .`mytab'.strcolor text . . . . .`mytab'.sep, top .`mytab'.titles "" /// 1 "Tau-sq" /// 2 "Std. Err." /// 3 "[`level'% Conf. Interval]" "" // 4 5 .`mytab'.sep, middle // format values with asterisks if truncated local lb_display = `ci_lb' local ub_display = `ci_ub' local mark = "" if `clipped_lower' == 1 & `clipped_upper' == 1 { local mark "* **" } else if `clipped_lower' == 1 { local mark "*" } else if `clipped_upper' == 1 { local mark "**" } // display the row - we need to manually display the asterisks // first, display the first 4 columns normally di as txt %10s "" "{c |}" _c di as res _col(13) %9.0g `tau2' _c di as res _col(24) %9.0g `se_tau2' _c di as res _col(38) %9.0g `lb_display' _c // display the last column with the asterisk mark di as res _col(50) %9.0g `ub_display' as txt "`mark'" .`mytab'.sep, bottom // display method information below the table di as text "Random-effects method: " as result "`rmethod'" // display warning if CI was truncated if `clipped_lower' == 1 | `clipped_upper' == 1 { di if `clipped_lower' == 1 { di as text "(*) The `rmethod' interval was clipped at the lower endpoint" } if `clipped_upper' == 1 { di as text "(**) The `rmethod' interval was clipped at the upper endpoint" } } end // Mata function to compute std err and CIs mata: mata clear void compute_tau2_se(string scalar esample, string scalar xvars, real scalar tau2, real scalar k, real scalar p, string scalar method, string scalar stmethod, real scalar level) { // get effect sizes and standard errors y = st_data(., "_meta_es", esample) se = st_data(., "_meta_se", esample) vi = se:^2 // build X matrix (design matrix with intercept) X = J(k, 1, 1) if (xvars != "") { vars = tokens(xvars) for (i = 1; i <= cols(vars); i++) { X = X, st_data(., vars[i], esample) } } // ensure non-negative tau2 if (tau2 < 0) tau2 = 0 // calculate weights with estimated tau2 (for most methods) wi_tau = 1 :/ (vi :+ tau2) // DerSimonian-Laird method if (stmethod == "dlaird") { wi = 1 :/ vi // DL uses tau2=0 for weights W = diag(wi) stXWX = invsym(X' * W * X) P = W - W * X * stXWX * X' * W trP = trace(P) sum_P_sq = sum(P:*P) se = sqrt(1/trP^2 * (2*(k-p) + 4*max((tau2,0))*trP + 2*max((tau2,0))^2*sum_P_sq)) } // Sidik-Jonkman method else if (stmethod == "sj") { ymc = y :- mean(y) tau2_0 = variance(ymc) * (k-1)/k wi_sj = 1 :/ (vi :+ tau2_0) W_sj = diag(wi_sj) stXWX_sj = invsym(X' * W_sj * X) P_sj = W_sj - W_sj * X * stXWX_sj * X' * W_sj V = diag(vi) PV_sj = P_sj * V sum_P_sq_sj = sum(P_sj:*P_sj) sum_PV_PV_sj = sum(PV_sj:*PV_sj) sum_PV_P_sj = sum(PV_sj:*P_sj) se = sqrt(tau2_0^2/(k-p)^2 * (2*sum_PV_PV_sj + 4*max((tau2,0))*sum_PV_P_sj + 2*max((tau2,0))^2*sum_P_sq_sj)) } // Hedges method else if (stmethod == "he") { stXX = invsym(X' * X) P_he = I(k) - X * stXX * X' V = diag(vi) PV_he = P_he * V trPV_he = trace(PV_he) sum_PV_PV_he = sum(PV_he:*PV_he) se = sqrt(1/(k-p)^2 * (2*sum_PV_PV_he + 4*max((tau2,0))*trPV_he + 2*max((tau2,0))^2*(k-p))) } // Hunter-Schmidt method else if (stmethod == "hs") { wi_hs = 1 :/ vi W_hs = diag(wi_hs) stXWX_hs = invsym(X' * W_hs * X) P_hs = W_hs - W_hs * X * stXWX_hs * X' * W_hs V = diag(vi) PV_hs = P_hs * V sum_P_sq_hs = sum(P_hs:*P_hs) sum_PV_PV_hs = sum(PV_hs:*PV_hs) sum_PV_P_hs = sum(PV_hs:*P_hs) sum_wi_hs = sum(wi_hs) se = sqrt(1/sum_wi_hs^2 * (2*sum_PV_PV_hs + 4*max((tau2,0))*sum_PV_P_hs + 2*max((tau2,0))^2*sum_P_sq_hs)) } // Restricted Maximum Likelihood else if (stmethod == "reml") { W = diag(wi_tau) stXWX = invsym(X' * W * X) P = W - W * X * stXWX * X' * W sum_P_sq = sum(P:*P) se = sqrt(2/sum_P_sq) } // Maximum Likelihood else if (stmethod == "mle") { se = sqrt(2/sum(wi_tau:^2)) } // Empirical Bayes else if (stmethod == "eb") { se = sqrt(2*k^2/(k-p) / sum(wi_tau)^2) } // Default for unknown methods (added in the future?) else { se = sqrt(2/sum(wi_tau:^2)) } // confidence interval using normal approximation (two-sided) alpha = (100 - level)/100 z = invnormal(1 - alpha/2) ci_lb = tau2 - z * se ci_ub = tau2 + z * se // store results st_numscalar("r(se_tau2)", se) st_numscalar("r(tau2)", tau2) st_numscalar("r(ci_lb)", ci_lb) st_numscalar("r(ci_ub)", ci_ub) } end