*! garbage_mixl 1.0.0 Jan 2024
*! author MF Jonker
program garbage_mixl
  version 9.2
  if replay() {
    if (`"`e(cmd)'"' != "garbage_mixl") error 301
    Replay `0'
  }
  else {
    Estimate `0'
  }
end

program Estimate, eclass
  syntax varlist(numeric max=1) [if] [in],  ///
         RAND(varlist)        ///
         ID(varname)          ///
         GROUP(varname)[      ///
         BURN(int 50000)      ///
         MCMC(int 50000)      ///
         BERNoulli(real 0.5)  ///
         DIAG                 ///
         INVWISHART           ///
         NOGARBAGE            ///
         NOLAPLACE            ///
         NOMALA               ///
  ]

  ** Check that optional inputs are within range **
  if (`burn'<50000) {
    di as error "Error: The # of burn-in draws must be at least 50,000"
    exit
  }
  if (`mcmc'<50000) {
    di as error "Error: The # of monitored draws must be at least 50,000"
    exit
  }
  if (`bernoulli'<=0 | `bernoulli'>=1) {
    di as error "Error: The MIXL class-memberhip probability must be between 0 and 1"
    exit
  }


  ** Check that id and group variables are numeric **
  capture confirm numeric var `group'
  if _rc != 0 {
    di in r "The group variable must be numeric"
    exit 498
  }
  capture confirm numeric var `id'
  if _rc != 0 {
    di in r "The id variable must be numeric"
    exit 498
  }


  ** Select the estimation sample **
  marksample touse
  markout `touse' `rand' `id' `group'
  preserve
    qui keep if `touse'

    ** place y in local variable
    gettoken lhs: varlist

    ** Check that the dependent variable only takes values 0-1 **
    capture assert `lhs' == 0 | `lhs' == 1
    if (_rc != 0) {
      di in red "The dependent variable must be a 0-1 variable indicating which alternatives are chosen"
      exit 450
    }

    ** Check that each group has one chosen alternative **
    tempvar n_choices
    sort `group'
    qui by `group': egen `n_choices' = sum(`lhs')
    capture assert `n_choices' == 1
    if (_rc != 0) {
      di in red "At least one choice task does not have one (and only one) chosen alternative"
      exit 498
    }

    ** Check that each group has at least two alternatives **
    tempvar n_alts
    qui by `group': egen `n_alts' = count(`lhs')
    capture assert `n_alts' >= 2
     if (_rc != 0) {
      di in red "Each choice task should have at least two alternatives (without missing values)"
      exit 498
    }

    ** Check that each group has an identical number of alternatives **
    capture assert `n_alts' == `n_alts'[1]
     if (_rc != 0) {
      di in red "Not all choice tasks have the same number of alternatives"
      exit 498
    }

    ** Check that each id has at least as many choices as independent variables **
    local n_betas : word count `rand'
    tempvar n_tasks
    sort `id'
    qui bys `id': egen `n_tasks' = sum(`lhs')
    capture assert `n_tasks' >= `n_betas'
     if (_rc != 0) {
      di as txt "Warning: At least one individual has fewer choice tasks than random variables
      if ("`nogarbage'" == "") {
        di as txt "This is only permitted with the 'nogarbage' model estimation option"
        exit 498
      }
    }

    ** Check for multicollinearity in the random variables **
    qui _rmcoll `rand'
    if ("`r(varlist)'" != "`rand'") {
      di in red "Some random variables are collinear - check your model specification"
      exit 498
    }

    ** Check that the random variables vary within groups **
    sort `group'
    foreach var of varlist `rand' {
      capture by `group': assert `var'==`var'[1]
      if (_rc == 0) {
        di in red "Variable `var' has no within-group variance"
        exit 459
      }
    }

    ** Check for missing values in the random variables **
    foreach var of varlist `rand' {
      capture assert `var', nomissing
      if (_rc == 0) {
        di in red "Variable `var' has missing values"
        exit 459
      }
    }


    ** Process data for C++
    display as text "The user interface will freeze during C++ model estimation; please do not close Stata!"

    sort `group' `lhs'
    foreach var of varlist `rand' {
      qui by `group': replace `var' = `var' - `var'[_N]
    }
    local obs_before = _N
    qui drop if `lhs'==1
    scalar nr_obs = `obs_before' - _N
    sort `id' `group'


    ** Transfer data to C++ plugin
    scalar nr_rowstask = `n_alts'[1] - 1
    scalar nr_betas = `n_betas'
    scalar nr_burn = `burn'
    scalar nr_mcmc = `mcmc'
    scalar bernoulli_prior = `bernoulli'
    scalar diagonal = 0
    if ("`diag'" != "") {
      scalar diagonal = 1
    }
    scalar inv_wishart = 0
    if ("`diag'" == "" & "`invwishart'" != "") {
      scalar inv_wishart = 1
    }
    scalar no_garbage = 0
    if ("`nogarbage'" != "") {
      scalar no_garbage = 1
    }

    scalar no_laplace = 0
    if ("`nolaplace'" != "") {
      scalar no_laplace = 1
    }

    scalar no_mala = 0
    if ("`nomala'" != "") {
      scalar no_mala = 1
    }

    tempvar id2
    egen `id2' = group(`id')
    quietly summarize `id2'
    scalar nr_resp = r(max)

    quietly by `id': gen nr_rows = _N if _n==_N
    mkmat nr_rows, matrix(nr_rowsXresp) nomissing

    ** Specify return matrices for C++ plugin
    tempname MU SD COR GAR PR_GAR
    matrix `MU' = J(`n_betas', 6, .)
    matrix `SD' = J(`n_betas', 6, .)
    matrix `COR' = J(`n_betas'*(`n_betas'-1)/2 , 6, .)
    matrix `GAR' = J(1,6,.)
    quietly levelsof `id', matrow(resp_column)
    matrix `PR_GAR' = J(nr_resp,1,.), resp_column
    matrix colnames `PR_GAR' = "pr_gar" `id'

    ** Run Plugin and obtain results
    plugin call garbage_mixlogit `rand' , `MU' `SD' `COR' `GAR' `PR_GAR'

    ** Ereturn estimation results
    ereturn post
    ereturn local cmd "garbage_mixl"
    ereturn local rand `rand'

    ereturn scalar nr_resp = nr_resp
    ereturn scalar nr_obs = nr_obs
    ereturn scalar nr_burn = nr_burn
    ereturn scalar nr_mcmc = nr_mcmc
    ereturn scalar nr_betas = nr_betas
    ereturn scalar no_garbage = no_garbage
    ereturn scalar no_laplace = no_laplace
    ereturn scalar no_mala = no_mala
    ereturn scalar bernoulli = bernoulli_prior
    ereturn scalar diagonal = diagonal
    ereturn scalar invwishart = inv_wishart

    ereturn matrix mu = `MU'
    ereturn matrix sd = `SD'
    if ("`diag'" == "") {
      ereturn matrix cor = `COR'
    }
    if ("`nogarbage'" == "") {
      ereturn matrix gar = `GAR'
      ereturn matrix pr_gar = `PR_GAR'
    }

    ** Print estimation results
    Replay

  ** Restore data **
  restore

end

program table_line
  args vname mean stdev mcse median 95LB 95UB
  display in smcl _col(3) as text %13s abbrev("`vname'",13) _col(17) "{c |}" _col(19) as result %8.0g `mean' _col(29) %8.0g `stdev' _col(39) %8.0g `mcse' _col(50) %8.0g `median' _col(61) %8.0g `95LB' _col(72) %8.0g `95UB'
end


program Replay
  syntax [, *]
  ** Display estimation results in Stata table format
  di in smcl "{hline 80}"
  if e(no_garbage) == 0 {
    if e(diagonal) == 0 {
	    di in smcl " Bayesian garbage class mixed logit" _col(54) as text "number of resp" _col(68) " = " _col(71) "{ralign 9:  `: display e(nr_resp)'}"
    }
    else {
	    di in smcl " Bayesian (diagonal) garbage class mixed logit" _col(54) as text "number of resp" _col(68) " = " _col(71) "{ralign 9:  `: display e(nr_resp)'}"
    }
	}
  else {
    if e(diagonal) == 0 {
      di in smcl " Bayesian mixed logit" _col(54) as text "number of resp" _col(68) " = " _col(71) "{ralign 9:  `: display e(nr_resp)'}"
    }
    else {
      di in smcl " Bayesian (diagonal) mixed logit" _col(54) as text "number of resp" _col(68) " = " _col(71) "{ralign 9:  `: display e(nr_resp)'}"
    }
  }
  di in smcl _col(54) as text "number of obs" _col(68) " = " _col(71) "{ralign 9: `: display e(nr_obs)'}"
	di in smcl _col(54) as text "MCMC burn-in" _col(68) " = " _col(71) "{ralign 9:  `: display e(nr_burn)'}"
	di in smcl _col(54) as text "MCMC sample" _col(68) " = " _col(71) "{ralign 9:  `: display e(nr_mcmc)'}"

	di in smcl "{hline 16}{c TT}{hline 63}"
	** di in smcl _col(17) "{c |}" _col(65) "Equal-tailed"
	di in smcl _col(17) "{c |}" _col(18) "{ralign 9: Mean}"  _col(29) "{ralign 9: Std.Dev.}" _col(38) "{ralign 9: MCSE}"  _col(49) "{ralign 9: Median}"  _col(60) "{ralign 20: [95% Cred. Interval] }"

	di in smcl "{hline 16}{c +}{hline 63}"
	di in smcl "{bf: Mean}" _col(17) "{c |}"
    forval i = 1/`e(nr_betas)' {
      local varname = word("`e(rand)'", `i')
      table_line `varname' e(mu)[`i',1] e(mu)[`i',2] e(mu)[`i',3] e(mu)[`i',4] e(mu)[`i',5] e(mu)[`i',6]
    }

	di in smcl "{hline 16}{c +}{hline 63}"
	di in smcl "{bf: SD}" _col(17) "{c |}"
	  forval i = 1/`e(nr_betas)' {
      local varname = word("`e(rand)'", `i')
	    table_line `varname' e(sd)[`i',1] e(sd)[`i',2] e(sd)[`i',3] e(sd)[`i',4] e(sd)[`i',5] e(sd)[`i',6]
	  }

  if e(diagonal) == 0 {
	  di in smcl "{hline 16}{c +}{hline 63}"
	  di in smcl "{bf: Corr}" _col(17) "{c |}"
      local row = 0
      local end_i = `e(nr_betas)' -1
	    forval i = 1/`end_i'      {
        local begin_j = `i' + 1
        forval j = `begin_j'/`e(nr_betas)' {
          local row = `row' + 1
          local varname = "β`i'-β`j'"
	        table_line `varname' e(cor)[`row',1] e(cor)[`row',2] e(cor)[`row',3] e(cor)[`row',4] e(cor)[`row',5] e(cor)[`row',6]
        }
	    }
  }

  if e(no_garbage) == 0 {
	  di in smcl "{hline 16}{c +}{hline 63}"
	  forval i = 1/1 {
	    table_line "garbage class" e(gar)[`i',1] e(gar)[`i',2] e(gar)[`i',3] e(gar)[`i',4] e(gar)[`i',5] e(gar)[`i',6]
    }
  }

	di in smcl "{hline 16}{c BT}{hline 63}"
  if (e(bernoulli) != 0.5 & e(no_garbage) == 0) {
    di as text "Note: Bernoulli(0" as text e(bernoulli) ") priors are used for MIXL class selection parameters,"
    if e(invwishart) == 1 {
      di as text "      Inverse Wishart prior is used for covariance matrix parameters,"
    }
    di as text "      default priors are used for other model parameters"
  }
  else if e(invwishart) == 1 {
	  di as text "Note: Inverse Wishart prior is used for covariance matrix parameters,"
    di as text "      default priors are used for other model parameters"
  }
  else {
    di as text "Note: Default priors are used for model parameters"
  }

end

cap program drop garbage_mixlogit
program garbage_mixlogit, plugin using(garbage_mixlogit.plugin)