*! 1.0.0 Ariel Linden 15Aug2026 program define varatio, rclass version 11.0 syntax varname [pweight aweight iweight] [if] [in], /// BY(varname) tempvar touse marksample touse, novarlist markout `touse' `varlist' // handle the by() variable separately to avoid markout issues with strings qui replace `touse' = 0 if missing(`by') local depvar `varlist' local origby "`by'" // weights tempvar w local hasweight = ("`weight'" != "") if `hasweight' { local wexp = trim(`"`exp'"') if substr(`"`wexp'"', 1, 1) == "=" { local wexp = trim(substr(`"`wexp'"', 2, .)) } quietly gen double `w' = `wexp' if `touse' quietly replace `touse' = 0 if `touse' & (`w' <= 0 | missing(`w')) } else { quietly gen double `w' = 1 if `touse' } // check if there are any observations in the sample qui count if `touse' if r(N) == 0 { di as error "no observations in the sample" exit 2000 } // get distinct values of by() variable qui levelsof `by' if `touse', local(by_vals) local num_groups : word count `by_vals' if `num_groups' < 2 { di as error "{bf:by()} variable must have at least 2 distinct values" exit 420 } // build numeric group codes (recoding string by() to 1..k), and labels capture confirm numeric variable `by' local needrecode = _rc != 0 tempvar numby if `needrecode' { qui gen `numby' = . } local i = 0 foreach lv of local by_vals { local ++i if `needrecode' { qui replace `numby' = `i' if `origby' == "`lv'" & `touse' local grouplabel`i' "`lv'" local grpval`i' "`i'" } else { capture local lbl : label (`by') `lv' if "`lbl'" == "" local lbl "`lv'" local grouplabel`i' "`lbl'" local grpval`i' "`lv'" } } if `needrecode' local by "`numby'" // per-group N, weighted mean, direct weighted variance tempvar w2 sqdev quietly gen double `w2' = `w'^2 quietly gen double `sqdev' = . if `hasweight' { local wtclause "[`weight'=`w']" } else { local wtclause "" } local N = 0 forvalues j = 1/`num_groups' { qui count if `by' == `grpval`j'' & `touse' local n`j' = r(N) local N = `N' + `n`j'' quietly mean `depvar' `wtclause' if `by' == `grpval`j'' & `touse' local mean`j' = _b[`depvar'] qui summarize `w' if `by' == `grpval`j'' & `touse', meanonly local sumw`j' = r(sum) qui summarize `w2' if `by' == `grpval`j'' & `touse', meanonly local sumw2`j' = r(sum) local ess`j' = (`sumw`j''^2) / `sumw2`j'' quietly replace `sqdev' = `w' * (`depvar' - `mean`j'')^2 /// if `by' == `grpval`j'' & `touse' qui summarize `sqdev' if `by' == `grpval`j'' & `touse', meanonly local var`j' = r(sum) / `sumw`j'' local logvar`j' = ln(`var`j'') } // omnibus group weights w_j = n_j/N, weighted mean log-variance local lbar = 0 forvalues j = 1/`num_groups' { local wj`j' = `n`j''/`N' local lbar = `lbar' + `wj`j'' * `logvar`j'' } forvalues j = 1/`num_groups' { local dev`j' = `logvar`j'' - `lbar' } // F_VR: size-weighted quadratic (RMS) combination local FVR2 = 0 forvalues j = 1/`num_groups' { local FVR2 = `FVR2' + `wj`j'' * (`logvar`j'' - `lbar')^2 } local FVR = sqrt(`FVR2') // pairwise log-differences, VR*, GMVR, max(VR*) local npairs = `num_groups' * (`num_groups' - 1) / 2 local pr = 0 local sumlogvr = 0 local maxvr = . forvalues a = 1/`=`num_groups'-1' { forvalues b = `=`a'+1'/`num_groups' { local ++pr local absdiff`pr' = abs(`logvar`a'' - `logvar`b'') local vrstar`pr' = exp(`absdiff`pr'') local rn`pr' "`grouplabel`a'' vs `grouplabel`b''" local sumlogvr = `sumlogvr' + `absdiff`pr'' if `vrstar`pr'' > `maxvr' | `maxvr' == . { local maxvr = `vrstar`pr'' } } } local GMVR = exp(`sumlogvr' / `npairs') // display if `hasweight' == 0 { di _newline as text "K-group variance-ratio balance diagnostics" di "{hline 65}" } else { di _newline as text "{bf:Weighted} k-group variance-ratio balance diagnostics" di "{hline 65}" } di as txt "Groups: `origby'" forvalues j = 1/`num_groups' { di as txt " `j'. `grouplabel`j''" _col(35) "N = " as res %6.0fc `n`j'' } di "{hline 65}" di as txt %-34s "F_VR (omnibus, quadratic):" as res %6.4f `FVR' di as txt %-34s "GMVR (geometric mean):" as res %6.4f `GMVR' di as txt %-34s "Max VR* (Lopez-Gutman-style):" as res %6.4f `maxvr' di "{hline 65}" di as txt _n "Per-group variance" di "{hline 65}" di as txt %-20s "Group" " Variance log(Variance) log(Var) - lbar" di "{hline 65}" forvalues j = 1/`num_groups' { di as txt %-20s "`grouplabel`j''" as res /// %10.4f `var`j'' " " %10.4f `logvar`j'' " " %8.4f `dev`j'' } di "{hline 65}" di as txt _n "Pairwise comparisons (" as res `npairs' as txt " pairs)" di "{hline 55}" di as txt %-25s "Groups" " VR* log(VR*)" di "{hline 55}" forvalues pr = 1/`npairs' { di as txt %-25s "`rn`pr''" as res %9.4f `vrstar`pr'' " " %9.4f `absdiff`pr'' } di "{hline 55}" di as txt "For GMVR and max(VR*): > 0.50 and < 2.0 - groups are well balanced" di as txt "For F_VR: < 0.10 - groups are well balanced" di as txt " > 0.30 - groups are imbalanced" di as txt " >= 0.10 and <= 0.30 - balance is uncertain (inspect the per-group and pairwise results)" // return values return scalar F_VR = `FVR' return scalar GMVR = `GMVR' return scalar maxVR = `maxvr' return scalar N = `N' return scalar k = `num_groups' return scalar npairs = `npairs' return local by = "`origby'" // per-group variance, log-variance, and deviation from the weighted mean log-variance tempname pergroup matrix `pergroup' = J(`num_groups', 3, .) local grouprows "" forvalues j = 1/`num_groups' { matrix `pergroup'[`j',1] = `var`j'' matrix `pergroup'[`j',2] = `logvar`j'' matrix `pergroup'[`j',3] = `dev`j'' local grouprows `"`grouprows' "`grouplabel`j''""' } matrix colnames `pergroup' = Variance LogVariance Deviation matrix rownames `pergroup' = `grouprows' return matrix pergroup = `pergroup' // pairwise VR* and log(VR*) tempname pairwise matrix `pairwise' = J(`npairs', 2, .) local pairrows "" forvalues pr = 1/`npairs' { matrix `pairwise'[`pr',1] = `vrstar`pr'' matrix `pairwise'[`pr',2] = `absdiff`pr'' local pairrows `"`pairrows' "`rn`pr''""' } matrix colnames `pairwise' = VRstar LogVRstar matrix rownames `pairwise' = `pairrows' return matrix pairwise = `pairwise' // clean up temporary variable if created if `needrecode' { capture drop `numby' } end