/* Stata Macro -- Written by David B. Wilson
   Version 2022.01.23
   See help for maanova */

program define maanova
preserve
version 14.0

#delimit ;
syntax varlist(min=2 max=2 numeric) [if] [in],
   [var(string)] [se(string)] [w(string)] [print(string)]
   [model(string)] [tau_unique(string)];
#delimit cr

marksample touse
markout `touse'
tokenize `varlist'

tempvar tau2 tau _w _v _x _lbl _minw _maxw _mines _maxes _j _k _dfb _dfe _dft
tempvar Y X
tempvar _intercept W W1 b resid2 qe_f qe_r pqe Wdiag _wfixed
tempvar ywy iwy iwi qt qb pqb pqt I2 _t2 vtypical se_tau2 _set2

/* apply "if" statement */
qui keep if `touse'==1

/* Errors */
if "`var'"=="" &  "`se'"=="" &  "`w'"=="" {
  di in error "Specify variance [var(varname)], standard error [se(varname)], "
  di in error "or inverse variance weight [w(varname)]."
  exit
}

if ("`var'"!="" &  "`se'"!="") |   ("`var'"!="" &  "`w'"!="") |   ("`se'"!="" &  "`w'"!="") {
  di in error "Only specify one of the following:"
  di in error "variance [var(varname)], standard error [se(varname)], "
  di in error "or inverse variance weight [w(varname)]."
  exit
}

if "`var'"!="" {
       qui g `_w' = 1/`var'
       qui g `_v' = `var'
   }
if  "`se'"!="" {
       qui g `_w' = 1/`se'^2
       qui g `_v' = `se'^2
   }
if "`w'"!="" {
       qui g `_w' = `w'
       qui g `_v' = 1/`w'
   }

/* Drop missing values */
if `1'==. | `_w'==. {
   qui drop if `1'==. | `_w'==. | `2'==.
   }

/* Set default model to REML */
local model = strupper("`model'")
if "`model'"!="FE" &  "`model'"!="DL" &  "`model'"!="HE" &  "`model'"!="HS" &  "`model'"!="SJ" &  "`model'"!="SJIT" &  "`model'"!="ML" &  "`model'"!="EB" {
   local model = "REML"
}

/* Set model to FE if only 1 effect size */
if _N == 1 {
  local model = "FE"
}

/* Dummy code independent variable */
qui tab `2' `if' `in', gen(`_x')
scalar `_j' = r(r)
local j = r(r)
scalar `_k' = r(N)
scalar `_dfe' = `_k' - `_j'
scalar `_dfb' = `_j' - 1
scalar `_dft' = `_k' - 1

/* Determine common tau^2 if not a fixed effect model */
local tau_unique = strupper("`tau_unique'")
if "`model'" != "FE" & "`tau_unique'"!="YES" {
   _matau2 `1' `_w' `2', model("`model'") modtype(aov)
   scalar `tau2' = r(tau2)
   scalar `se_tau2' = r(se_tau2)
   qui replace `_w' = 1/(`_v'+`tau2') /* adjust the weight */
   scalar `tau' = sqrt(`tau2')
   }

/* Determine sub-group tau^2 */
if "`model'"!="FE" & "`tau_unique'"=="YES" {
  forvalues i = 1/`j' {
      qui sum `_x'`i'
      local _n = int(r(mean)*r(N))
      if `_n'>1 {
      _matau2 `1' `_w' if `_x'`i' == 1, model("`model'")
       scalar `_t2'`i' = r(tau2)
       scalar `_set2'`i' = r(se_tau2)
          if `_t2'`i' < 0 {
             scalar `_t2'`i' = 0
          }
       qui replace `_w' = 1/(`_v'+`_t2'`i') if `_x'`i' == 1
       }
      if `_n'==1 {
       scalar `_t2'`i' = .
       }
}
}

/* some values needed for header info */
qui sum `_w'
scalar `_minw' = r(min)
scalar `_maxw' = r(max)
qui sum `1'
scalar `_mines' = r(min)
scalar `_maxes' = r(max)

/* Analog-to-the-ANOVA results */
mkmat `1', matrix(`Y')
mkmat `_x'*, matrix(`X')
qui g `_intercept' = 1
mkmat `_intercept', matrix(`W1')

/* Q-between/Q-model (based on weights used in model */
mkmat `_w', matrix(`W')
matrix `Wdiag' = diag(`W')
matrix `ywy' = `Y'' * `Wdiag' * `Y'
matrix `iwy' = `W1'' * `Wdiag' * `Y'
matrix `iwi' = `W1'' * `Wdiag' * `W1'
matrix `qt' = `ywy' - (`iwy' * `iwy') * syminv(`iwi')
matrix `b' = syminv(`X'' * `Wdiag' * `X') * `X'' * `Wdiag' * `Y'
matrix `qe_r' = (`Y'' * `Wdiag' * `Y') - (2 * `b'' * `X'' * `Wdiag' * `Y') + (`b'' * `X'' * `Wdiag' * `X' *  `b')
matrix `qb' = `qt' - `qe_r'
scalar `qb' = `qb'[1,1]
scalar `qt' = `qt'[1,1]
scalar `pqb' = chiprob(`_dfb',`qb')
scalar `pqt' = chiprob(`_dft',`qt')

/* Q-within/Q-errors (based on fixed-effect weights */
qui g `_wfixed' = 1/`_v'
mkmat `_wfixed', matrix(`W')
matrix `Wdiag' = diag(`W')
matrix `b' = syminv(`X'' * `Wdiag' * `X') * `X'' * `Wdiag' * `Y'
matrix `qe_f' = (`Y'' * `Wdiag' * `Y') - (2 * `b'' * `X'' * `Wdiag' * `Y') + (`b'' * `X'' * `Wdiag' * `X' *  `b')
scalar `qe_f' = `qe_f'[1,1]
scalar `pqe' = chiprob(`_dfe',`qe_f')

/* compute I^2 */
if ("`model'" == "FE" | "`model'" == "DL") &  "`tau_unique'"!="YES" {
  scalar `I2' = ((`qe_f' - `_dfe')/`qe_f') * 100
  }
if "`model'" != "FE" & "`model'" != "DL" & "`tau_unique'"!="YES" {
  matrix `vtypical' = (`_k' - `_j')/trace(`Wdiag' - `Wdiag' * `X' * syminv(`X''*`Wdiag'*`X') * `X'' * `Wdiag')
  scalar `I2' = `tau2'/(`vtypical'[1,1] + `tau2') * 100
  }
if "`tau_unique'"!="YES" {
if `I2' < 0 {
  scalar `I2' = 0
}
}


/* some labels */
if "`model'"=="FE" {
    local model_type = "Fixed (Common) Effect model"
    }
if "`model'"=="DL" {
    local model_type = "Random effects: Dersimonian-Laird"
    }
if "`model'"=="HS" {
    local model_type = "Random effects: Hunter-Schmidt"
    }
if "`model'"=="HE" {
    local model_type = "Random effects: Hedges"
    }
if "`model'"=="SJ" {
    local model_type = "Random effects: Sikik-Jonkman non-iterative"
    }
if "`model'"=="SJIT" {
    local model_type = "Random effects: Sikik-Jonkman iterative"
    }
if "`model'"=="ML" {
    local model_type = "Random effects: iterative full-information maximum likelihood"
    }
if "`model'"=="REML" {
    local model_type = "Random effects: iterative restricted maximum likelihood (default)"
    }
if "`model'"=="EB" {
    local model_type = "Random effects: iterative empirical bayes"
    }

/* total row of results */
qui masum `1' `if' `in', w(`_w') model(FE) /* use weights computed above base on tau^2 */

/* header info */
di " "
di in text _col(1) "No. of obs  =" in result _col(24) %8.0f r(k)
di in text _col(1) "Minimum effect size =" in result _col(24) %8.4f `_mines'
di in text _col(1) "Maximum effect size =" in result _col(24) %8.4f `_maxes'
di in text _col(1) "Minimum weight =" in result _col(24) %8.4f `_minw'
di in text _col(1) "Maximum weight =" in result _col(24) %8.4f `_maxw'
if "`model'"!="FE" & ("`tau_unique'"!="yes" & "`tau_unique'"!="YES") {
    di in text _col(1) "Common tau^2 =" in result _col(24) %8.4f `tau2'
    di in text _col(1) "SE tau^2 =" in result _col(24) %8.4f `se_tau2'
    di in text _col(1) "Common tau   =" in result _col(24) %8.4f `tau'
    di in text _col(1) "I^2 =" in result _col(24) %4.2f `I2' "%"
}
di " "
di in text _col(1) "----------------+-------------------------------------------------------------"
di in text _col(1) " Category       |      Mean    -95%CI    +95%CI      se       z       p      k"
di in text _col(1) "----------------+-------------------------------------------------------------"
/* overall results */
di in text _col(1) "Total" in text _col(17) "|" /*
      */ in result _col(20) %8.4f r(mean)  /*
      */           _col(30) %8.4f r(lci)  /*
      */           _col(40) %8.4f r(uci) /*
      */           _col(47) %8.4f r(se) /*
      */           _col(54) %8.4f r(z) /*
      */           _col(66) %6.4f r(pz) /*
      */           _col(75)  %4.0f r(k)
di in text _col(1) "----------------+-------------------------------------------------------------"
/* subgroup results */
foreach v of varlist `_x'* {
   qui masum `1' if `v'==1, w(`_w') model(FE) print("`print'")
   local label : var label `v'
   scalar `_lbl' = strlen("`2'")
   local label = substr(`"`label'"', `_lbl'+3, 16)
   di in text _col(1) `"`label'"' in text _col(17) "|" /*
      */ in result _col(20) %8.4f r(mean)  /*
      */           _col(30) %8.4f r(lci)  /*
      */           _col(40) %8.4f r(uci) /*
      */           _col(47) %8.4f r(se) /*
      */           _col(54) %8.4f r(z) /*
      */           _col(66) %6.4f r(pz) /*
      */           _col(75)  %4.0f r(k)
  }
di in text _col(1) "----------------+--------------------------------------------------------------"
if "`print'"=="EXP" {
   di in text _col(1) /*
   */ "Results are the exponent of computed values (i.e., results are odds-ratios)"
}
if "`print'"=="IVZR" {
   di in text _col(1) /*
   */ "Results are inverse Fisher Zr transformed (i.e., results are correlations)"
}
if "`print'"=="PROP" {
   di in text _col(1) /*
   */ "Results are inverse logits (i.e., results are proportions)"
}


if "`tau_unique'"!="YES" {
di "  "
di in text _col(1) "----------------+-------------------------------"
di in text _col(1) " Cateogry       |  Q-within  p(Q-within)      df"
di in text _col(1) "----------------+-------------------------------"
foreach v of varlist `_x'* {
   qui masum `1' if `v'==1, var(`_v') model(FE) /* use FE weights */
   local label : var label `v'
   scalar `_lbl' = strlen("`2'")
   local label = substr(`"`label'"', `_lbl'+3, 16)
   di in text _col(1) `"`label'"' in text _col(17) "|" /*
      */ in result _col(20) %8.4f r(Q)  /*
      */           _col(33) %8.4f r(pQ)  /*
      */           _col(40) %8.0f r(k)-1
  }
di in text _col(1) "----------------+-------------------------------"
}

if "`tau_unique'"=="YES" {
di "  "
di in text _col(1) "----------------+--------------------------------------------------------------"
di in text _col(1) " Cateogry       |  Q-within  p(Q-within)      df      tau2      tau    se(tau2)"
di in text _col(1) "----------------+--------------------------------------------------------------"
local i = 0
foreach v of varlist `_x'* {
   local i = `i' + 1
   qui masum `1' if `v'==1, var(`_v') model(FE) /* use FE weights */
   local label : var label `v'
   scalar `_lbl' = strlen("`2'")
   local label = substr(`"`label'"', `_lbl'+3, 16)
   di in text _col(1) `"`label'"' in text _col(17) "|" /*
      */ in result _col(20) %8.4f r(Q)  /*
      */           _col(33) %8.4f r(pQ)  /*
      */           _col(40) %8.0f r(k)-1  /*
      */           _col(51) %8.5f `_t2'`i' /*
      */           _col(60) %8.5f sqrt(`_t2'`i') /*
      */           _col(72) %8.5f `_set2'`i'
  }
di in text _col(1) "----------------+--------------------------------------------------------------"
}
if "`model'"!="FE" {
  di in text _col(1) "Q-within based on fixed effect weights"
}

di " "
di "Analog-to-the-ANOVA Table"
di in text _col(1) "----------------+------------------------------------"
di in text _col(1) "Source          |          Q          p(Q)         df"
di in text _col(1) "----------------+------------------------------------"
di in text _col(1) "Between (Model)"  in text _col(17) "|" /*
      */ in result _col(21) %8.4f `qb'  /*
      */           _col(35) %8.4f `pqb'  /*
      */           _col(46) %8.0f `_dfb'
di in text _col(1) "Within (Error)"  in text _col(17) "|" /*
      */ in result _col(21) %8.4f `qe_f'  /*
      */           _col(35) %8.4f `pqe'  /*
      */           _col(46) %8.0f `_dfe'
di in text _col(1) "----------------+------------------------------------"
if "`model'"=="FE" {
di in text _col(1) "Total"  in text _col(17) "|" /*
      */ in result _col(21) %8.4f `qt'  /*
      */           _col(35) %8.4f `pqt'  /*
      */           _col(46) %8.0f `_dft'
di in text _col(1) "----------------+------------------------------------"
}
di in text _col(1) "`model_type'"
if "`model'"!="FE" {
  di in text _col(1) "Q-within based on fixed effect weights"
}
di " "
di  in gr "Version 2021.04.18 of maanova.ado written by David B. Wilson"


restore
end