*! Confirmatory factor analysis with a single factor: v.2.2
*! Stas Kolenikov, skolenik-gmail-com

program define cfa1, eclass
  version 9.1

  if replay() {
     if ("`e(cmd)'" != "cfa1") error 301
     Replay `0'
  }

  else Estimate `0'
end

program Estimate `0', eclass

  syntax varlist(numeric min=3) [if] [in] [aw pw / ] ///
     , [unitvar FREE POSvar FROM(str) CONSTRaint(numlist) LEVel(int $S_level) ///
       ROBust VCE(string) CLUster(passthru) SVY SEArch(passthru) * ]
  * syntax: cfa1 <list of the effect indicators>
  * untivar is for the identification condition of the unit variance of the latent variable

  unab varlist : `varlist'
  tokenize `varlist'
  local q: word count `varlist'
  marksample touse
  preserve
  qui keep if `touse'
  * weights!
  global CFA1N = _N

/*
  * we'll estimate the means instead
  qui foreach x of varlist `varlist' {
     sum `x', meanonly
     replace `x' = `x'-r(mean)
     * deviations from the mean
  }
*/

  if "`weight'" != "" {
     local mywgt [`weight'=`exp']
  }

  if "`robust'`cluster'`svy'`weight'"~="" {
     local needed 1
  }
  else {
     local needed 0
  }

  Parse `varlist'  , `unitvar'
  local toml `r(toml)'
  if "`from'" == "" {
    local from `r(tostart)', copy
  }

  * identification
  constraint free
  global CFA1constr `r(free)'
  if "`unitvar'" ~= "" {
     * identification by unit variance
     constraint $CFA1constr [phi]_cons = 1
  }
  else if "`free'"=="" {
     * identification by the first variable
     constraint $CFA1constr [`1']_cons = 1
  }
  else {
     * identification imposed by user
     global CFA1constr
  }
  local nconstr : word count `constraint'

  global CFA1PV = ("`posvar'" != "")

  if "`posvar'" ~= "" {
     di as text _n "Fitting the model without restrictions on error variances..."
  }

  * variance estimation
  local vce = trim("`vce'")
  if "`vce'" == "boot" local vce bootstrap
  if "`vce'" == "sbentler" {
     global CFA1SBV = 1
     local vce
  }
  else {
    if index("robustoimopg","`vce'") {
       local vce vce(`vce')
    }
    else {
       di as err "`vce' estimation is not supported"
       if "`vce'" == "bootstrap" | "`vce'" == "boot" {
          di as err "try {help bootstrap} command directly"
       }
       exit 198
    }
  }

  tempname ilog1 ilog2
  Tryit ml model lf cfa1_lf `toml' `mywgt', constraint($CFA1constr `constraint') ///
      init (`from') maximize nooutput `options' `search' ///
      `svy' `cluster' `robust' `vce'
/*
  ml model lf cfa1_lf `toml' `mywgt', ///
      constraint($CFA1constr `constraint') `svy' `robust' `cluster' ///
      maximize
*  ml check
*  ml search, rep(5)
  ml init `from', copy
  cap noi ml maximize , `options'
*/
  mat `ilog1' = e(ilog)

  local nz = 0
  if $CFA1PV {
     * determine if refitting the model is needed
     tempname ll_unr
     scalar `ll_unr' = e(ll)
     forvalues i=1/`q' {
        if [``i''_v]_cons <0 {
           constraint free
           local ccc = `r(free)'
           const define `ccc' [``i''_v]_cons = 0
           local zerolist `zerolist' `ccc'
           local ++nz
        }
     }
     local zerolist = trim("`zerolist'")
     global CFA1constr $CFA1constr `zerolist'
     if "`zerolist'" ~= "" {
        di as text _n "Fitting the model with some error variances set to zero..."
        Tryit ml model lf cfa1_lf `toml' `mywgt' , constraint($CFA1constr `constraint') ///
            init (`from') maximize nooutput `options' `search' ///
            `svy' `robust' `cluster' `vce'
        mat `ilog2' = e(ilog)

     }

     * adjust degrees of freedom!
  }


  * we better have this before Satorra-Bentler
  if "`unitvar'" ~= "" {
    ereturn local normalized Latent Variance
  }
  else if "`free'"=="" {
    ereturn local normalized `1'
  }


  * work out Satorra-Bentler estimates
  if "$CFA1SBV"!="" {
     * repost Satorra-Bentler covariance matrix
     tempname SBVar SBV Delta Gamma
     cap SatorraBentler
     if _rc {
        di as err "Satorra-Bentler standard errors are not supported for this circumstance; revert to vce(oim)"
        global CFA1SBV
     }
     else {
       mat `SBVar' = r(SBVar)
       mat `Delta' = r(Delta)
       mat `Gamma' = r(Gamma)
       mat `SBV'   = r(SBV)
       ereturn repost V = `SBVar'
       ereturn matrix SBGamma = `Gamma', copy
       ereturn matrix SBDelta = `Delta', copy
       ereturn matrix SBV = `SBV', copy
       ereturn local vce SatorraBentler
     }
  }



  * get the covariance matrix and the number of observations!
  ***********************************************************
  tempname lambda vars phi S Sindep Sigma trind eb

  qui mat accum `S' = `varlist', dev nocons
  mat `S' = `S' / $CFA1N

  * implied matrix
  mat `eb' = e(b)
  mat `lambda' = `eb'[1,1..`q']
  mat `vars' = `eb'[1,`q'+1..2*`q']
  scalar `phi' = `eb'[1,3*`q'+1]
  mat `Sigma' = `lambda''*`phi'*`lambda' + diag(`vars')
  mat `Sindep' = diag(vecdiag(`S'))

  * test against independence
  mat `trind' = trace( syminv(`Sindep') * `S' )
  local trind = `trind'[1,1]
  ereturn scalar ll_indep = -0.5 * `q' * $CFA1N * ln(2*_pi) - 0.5 * $CFA1N * ln(det(`Sindep')) - 0.5 * $CFA1N * `trind'
  ereturn scalar lr_indep = 2*(e(ll)-e(ll_indep))
  ereturn scalar df_indep = `q'-`nz'-`nconstr'
  ereturn scalar p_indep  = chi2tail(e(df_indep),e(lr_indep))

  * goodness of fit test
  ereturn scalar ll_u = -0.5 * `q' * $CFA1N * ln(2*_pi) - 0.5 * $CFA1N * ln(det(`S')) - 0.5 * `q' * $CFA1N
  ereturn scalar lr_u = -2*(e(ll)-e(ll_u))
  ereturn scalar df_u = `q'*(`q'+1)*.5 - (2*`q' - `nz' - `nconstr')
  * wrong if there are any extra constraints in -constraint- command!!!
  ereturn scalar p_u  = chi2tail(e(df_u),e(lr_u))
  ereturn matrix ilog1 `ilog1'
  cap ereturn matrix ilog2 `ilog2'

  * Satorra-Bentler corrections
  if "$CFA1SBV"!="" {
     * compute the corrected tests, too
     * Satorra-Bentler 1994
     tempname U trUG2 Tdf
     mat `U' = `SBV' - `SBV'*`Delta'*syminv(`Delta''*`SBV'*`Delta')*`Delta''*`SBV'
     ereturn matrix SBU = `U'
     mat `U' = trace( e(SBU)*`Gamma' )
     ereturn scalar SBc = `U'[1,1]/e(df_u)
     ereturn scalar Tscaled = e(lr_u)/e(SBc)
     ereturn scalar p_Tscaled = chi2tail( e(df_u), e(Tscaled) )

     mat `trUG2' = trace( e(SBU)*`Gamma'*e(SBU)*`Gamma')
     ereturn scalar SBd = `U'[1,1]*`U'[1,1]/`trUG2'[1,1]
     ereturn scalar Tadj = ( e(SBd)/`U'[1,1]) * e(lr_u)
     ereturn scalar p_Tadj = chi2tail( e(SBd), e(Tadj) )

     * Yuan-Bentler 1997
     * weights!
     ereturn scalar T2 = e(lr_u)/(1+e(lr_u)/e(N) )
     ereturn scalar p_T2 = chi2tail( e(df_u), e(T2) )
  }

  if "`posvar'" ~= "" {
     ereturn scalar lr_zerov = 2*(`ll_unr' - e(ll))
     ereturn scalar df_zerov = `nz'
     local replay_opt posvar llu(`ll_unr')
  }
  ereturn local cmd cfa1

  Replay , `replay_opt' level(`level')
  Finish
  restore
  ereturn repost, esample(`touse')

end

program define Tryit

  cap noi `0'
  local rc=_rc
  if `rc' {
     Finish
     exit `rc'
  }

end

program define Finish

  * finishing off
  constraint drop $CFA1constr
  global CFA1S
  global CFA1N
  global CFA1PV
  global CFA1theta
  global CFA1arg
  global CFA1data
  global CFA1constr
  global CFA1vars
  global CFA1SBV

end

program define Replay

  syntax, [posvar llu(str) level(passthru)]

  di _n as text "Log likelihood = " as res e(ll) _col(59) as text "Number of obs = " as res e(N)
  di as text "{hline 13}{c TT}{hline 64}"
  di as text "             {c |}      Coef.   Std. Err.      z    P>|z|     [$S_level% Conf. Interval]"
  di as text "{hline 13}{c +}{hline 64}"

  tempname vce
  mat `vce' = e(V)
  local q = colsof(`vce')
  local q = (`q'-1)/3
  local a : colfullnames(`vce')
  tokenize `a'

  di as text "Lambda{col 14}{c |}"
  forvalues i = 1/`q' {
    gettoken v`i' : `i' , parse(":")
    _diparm `v`i'' , label("`v`i''") prob `level'
  }
  di as text "Var[error]{col 14}{c |}"
  forvalues i = 1/`q' {
    _diparm `v`i''_v , label("`v`i''") prob `level'
  }
  di as text "Means{col 14}{c |}"
  forvalues i = 1/`q' {
    _diparm `v`i''_m , label("`v`i''") prob `level'
  }
  di as text "Var[latent]{col 14}{c |}"
  _diparm phi , label("phi1") prob

  di as text "{hline 13}{c +}{hline 64}"
  di as text "R2{col 14}{c |}"
  forvalues i = 1/`q' {
    di as text %12s "`v`i''" "{col 14}{c |}{col 20}" ///
       as res %6.4f (_b[`v`i'':_cons]^2*_b[phi:_cons]) / ///
              (_b[`v`i'':_cons]^2*_b[phi:_cons] + _b[`v`i''_v:_cons])
  }


  di as text "{hline 13}{c BT}{hline 64}"

  if e(df_u)>0 {
     di as text _n "Goodness of fit test: LR = " as res %6.3f e(lr_u) ///
        as text _col(40) "; Prob[chi2(" as res %2.0f e(df_u) as text ") > LR] = " as res %6.4f e(p_u)
  }
  else {
     di as text "No degrees of freedom to perform the goodness of fit test"
  }
  di as text "Test vs independence: LR = " as res %6.3f e(lr_indep) ///
     as text _col(40) "; Prob[chi2(" as res %2.0f e(df_indep) as text ") > LR] = " as res %6.4f e(p_indep)

  if "`e(vce)'" == "SatorraBentler" & e(df_u)>0 {
     * need to report all those corrected statistics

     di as text _n "Satorra-Bentler Tbar" _col(26) "= " as res %6.3f e(Tscaled) ///
        as text _col(40) "; Prob[chi2(" as res %2.0f e(df_u) as text ") > Tbar] = " as res %6.4f e(p_Tscaled)

     di as text "Satorra-Bentler Tbarbar" _col(26) "= " as res %6.3f e(Tadj) ///
        as text _col(40) "; Prob[chi2(" as res %4.1f e(SBd) as text ") > Tbarbar] = " as res %6.4f e(p_Tadj)

     di as text "Yuan-Bentler T2" _col(26) "= " as res %6.3f e(T2) ///
        as text _col(40) "; Prob[chi2(" as res %2.0f e(df_u) as text ") > T2] = " as res %6.4f e(p_T2)
  }

  if "`posvar'" ~= "" {
     * just estimated?
     if "`llu'" == "" {
        di as err "cannot specify -posvar- option, need to refit the whole model"
     }
     else {
      if e(df_zerov)>0 {
        di as text "Likelihood ratio against negative variances: LR = " as res %6.3f e(lr_zerov)
        di as text "Conservative Prob[chi2(" as res %2.0f e(df_zerov) as text ") > LR] = " ///
           as res %6.4f chi2tail(e(df_zerov),e(lr_zerov))
      }
      else {
        di as text "All variances are non-negative, no need to test against zero variances"
      }
     }
  }

end

program define Parse , rclass
  * takes the list of variables and returns the appropriate ml model statement
  syntax varlist , [unitvar]

  global CFA1arg
  global CFA1theta
  global CFA1vars
  local q : word count `varlist'

  * lambdas
  forvalues i = 1/`q' {
     local toml `toml' (``i'': ``i'' = )
     local tostart `tostart' 1
     global CFA1arg $CFA1arg g_``i''
     global CFA1theta $CFA1theta l_`i'
     global CFA1vars $CFA1vars ``i''
  }

  * variances
  forvalues i = 1/`q' {
     local toml `toml' (``i''_v: )
     local tostart `tostart'  1
     global CFA1arg $CFA1arg g_``i''_v
     global CFA1theta $CFA1theta v_`i'
  }

  * means
  forvalues i = 1/`q' {
     local toml `toml' (``i''_m: )
     qui sum ``i'', mean
     local mean = r(mean)
     local tostart `tostart' `mean'
     global CFA1arg $CFA1arg g_``i''_m
     global CFA1theta $CFA1theta m_`i'
  }

  * variance of the factor
  local toml `toml' (phi: )
  local tostart `tostart' 1
  global CFA1arg $CFA1arg g_Phi
  global CFA1theta $CFA1theta phi

  * done!
  return local toml `toml'
  return local tostart `tostart'

end


**************************** Satorra-Bentler covariance matrix code

program SatorraBentler, rclass
   version 9.1
   syntax [, noisily]
   * assume the maximization completed, the results are in memory as -ereturn data-
   * we shall just return the resulting matrix

   if "`e(normalized)'" == "" {
      di as err "cannot compute Satorra-Bentler variance estimator with arbitrary identification... yet"
      exit 198
   }

   * assume sample is restricted to e(sample)
   * preserve
   * keep if e(sample)

   * get the variable names
   tempname VV bb
   mat `VV' = e(V)
   local q = rowsof(`VV')
   local p = (`q'-1)/3
   local eqlist : coleq `VV'
   tokenize `eqlist'
   forvalues k=1/`p' {
     local varlist `varlist' ``k''
   }

   * compute the implied covariance matrix
   tempname Lambda Theta phi Sigma
   mat `bb' = e(b)
   mat `Lambda' = `bb'[1,1..`p']
   mat `Theta' = `bb'[1,`p'+1..2*`p']
   scalar `phi' = `bb'[1,`q']
   mat `Sigma' = `Lambda''*`phi'*`Lambda' + diag(`Theta')

   * compute the empirical cov matrix
   tempname SampleCov
   qui mat accum `SampleCov' = `varlist' , nocons dev
   * weights!!!
   mat `SampleCov' = `SampleCov' / (r(N)-1)

   * compute the matrix Gamma
   `noisily' di as text "Computing the Gamma matrix of fourth moments..."
   tempname Gamma
   SBGamma `varlist'
   mat `Gamma' = r(Gamma)
   return add

   * compute the duplication matrix
   * Dupl `p'
   * let's call it from within SBV!

   * compute the V matrix
   `noisily' di as text "Computing the V matrix..."
   SBV `SampleCov' `noisily'
   tempname V
   mat `V' = r(SBV)
   return add

   * compute the Delta matrix
   `noisily' di as text "Computing the Delta matrix..."
   tempname Delta
   mata : SBDelta("`bb'","`Delta'")

   *** put the pieces together now

   tempname DeltaId

   * enact the constraints!
   SBconstr `bb'
   mat `DeltaId' = `Delta' * diag( r(Fixed) )

   * those should be in there, but it never hurts to fix!
   if "`e(normalized)'" == "Latent Variance" {
      * make the last column null
      mat `DeltaId' = ( `DeltaId'[1...,1...3*`p'] , J(rowsof(`Delta'), 1, 0)  )
   }
   else if "`e(normalized)'" ~= "" {
      * normalization by first variable
      local idvar `e(normalized)'
      if "`idvar'" ~= "`1'" {
         di as err "cannot figure out the identification variable"
         exit 198
      }
      mat `DeltaId' = ( J(rowsof(`Delta'), 1, 0) , `DeltaId'[1...,2...] )
   }
   local dcnames : colfullnames `bb'
   local drnames : rownames `Gamma'
   mat colnames `DeltaId' = `dcnames'
   mat rownames `DeltaId' = `drnames'
   return matrix Delta = `DeltaId', copy

   tempname VVV
   mat `VVV' = ( `DeltaId'' * `V' * `DeltaId' )
   mat `VVV' = syminv(`VVV')
   mat `VVV' = `VVV' * ( `DeltaId'' * `V' * `Gamma' * `V' * `DeltaId' ) * `VVV'

   * add the covariance matrix for the means, which is just Sigma/_N
   * weights!
   tempname CovM
   mat `CovM' = ( J(2*`p',colsof(`bb'),0) \ J(`p',2*`p',0) , `Sigma', J(`p',1,0) \ J(1, colsof(`bb'), 0) )

   mat `VVV' = (`VVV' + `CovM')/_N
   return matrix SBVar = `VVV'

end
* of satorrabentler

program define SBGamma, rclass
   syntax varlist
   unab varlist : `varlist'
   tokenize `varlist'

   local p: word count `varlist'

   forvalues k=1/`p' {
     * make up the deviations
     * weights!!!
     qui sum ``k'', meanonly
     tempvar d`k'
     qui g double `d`k'' = ``k'' - r(mean)
     local dlist `dlist' `d`k''
   }

   local pstar = `p'*(`p'+1)/2
   forvalues k=1/`pstar' {
      tempvar b`k'
      qui g double `b`k'' = .
      local blist `blist' `b`k''
   }


   * convert into vech (z_i-bar z)(z_i-bar z)'
   mata : SBvechZZtoB("`dlist'","`blist'")

   * blist now should contain the moments around the sample means
   * we need to get their covariance matrix

   tempname Gamma
   qui mat accum `Gamma' = `blist', dev nocons
   * weights!
   mat `Gamma' = `Gamma'/(_N-1)
   mata : Gamma = st_matrix( "`Gamma'" )

   * make nice row and column names
   forvalues i=1/`p' {
     forvalues j=`i'/`p' {
        local namelist `namelist' ``i''_X_``j''
     }
   }
   mat colnames `Gamma' = `namelist'
   mat rownames `Gamma' = `namelist'

   return matrix Gamma = `Gamma'

end
* of computing Gamma

program define SBV, rclass
   args A noisily
   tempname D Ainv V
   local p = rowsof(`A')
   `noisily' di as text "Computing the duplication matrix..."
   mata : Dupl(`p',"`D'")
   mat `Ainv' = syminv(`A')
   mat `V' = .5*`D''* (`Ainv' # `Ainv') * `D'
   return matrix SBV = `V'
end
* of computing V

   * need to figure out whether a constraint has the form parameter = value,
   * and to nullify the corresponding column
program define SBconstr, rclass
   args bb
   tempname Iq
   mat `Iq' = J(1,colsof(`bb'),1)
   tokenize $CFA1constr
   while "`1'" ~= "" {
     constraint get `1'
     local constr `r(contents)'
     gettoken param value  : constr, parse("=")
     * is the RHS indeed a number?
     local value = substr("`value'",2,.)
     confirm number `value'
     * parse the square brackets and turn them into colon
     * replace the opening brackets with nothing, and closing brackets, with :
     local param = subinstr("`param'","["," ",1)
     local param = subinstr("`param'","]",":",1)
     local param = trim("`param'")
     local coln = colnumb(`bb',"`param'" )
     mat `Iq'[1,`coln']=0

     mac shift
   }
   return matrix Fixed = `Iq'
end


cap mata : mata drop SBvechZZtoB()
cap mata : mata drop Dupl()
cap mata : mata drop SBDelta()

mata:
void SBvechZZtoB(string dlist, string blist) {
   // view the deviation variables
   st_view(data=.,.,tokens(dlist))
   // view the moment variables
   // blist=st_local("blist")
   st_view(moments=.,.,tokens(blist))
   // vectorize!
   for(i=1; i<=rows(data); i++) {
     B = data[i,.]'*data[i,.]
     moments[i,.] = vech(B)'
   }
}

void Dupl(scalar p, string Dname) {
   pstar = p*(p+1)/2
   Ipstar = I(pstar)
   D = J(p*p,0,.)
   for(k=1;k<=pstar;k++) {
      D = (D, vec(invvech(Ipstar[.,k])))
   }
   st_matrix(Dname,D)
}

void SBDelta(string bbname, string DeltaName) {
   bb = st_matrix(bbname)
   p = (cols(bb)-1)/3
   Lambda = bb[1,1..p]
   Theta = bb[1,p+1..2*p]
   phi = bb[1,cols(bb)]
   Delta = J(0,cols(bb),.)
   for(i=1;i<=p;i++) {
     for(j=i;j<=p;j++) {
       DeltaRow = J(1,cols(Delta),0)
       for(k=1;k<=p;k++) {
         // derivative wrt lambda_k
         DeltaRow[k] = (k==i)*Lambda[j]*phi + (j==k)*Lambda[i]*phi
         // derivative wrt sigma^2_k
         DeltaRow[p+k] = (i==k)*(j==k)
       }
       DeltaRow[cols(Delta)] = Lambda[i]*Lambda[j]
       Delta = Delta \ DeltaRow
     }
   }
   st_matrix(DeltaName,Delta)
}

end
* of mata piece

***************************** end of Satorra-Bentler covariance matrix code

exit

History:
   v.1.0 -- May 19, 2004: basic operation, method d0
   v.1.1 -- May 19, 2004: identification by -constraint-
                          common -cfa1_ll-
                          from()
   v.1.2 -- May 21, 2004: method -lf-, robust
                          constraint free
   v.1.3 -- unknown
   v.1.4 -- Feb 15, 2005: pweights, arbitrary constraints
   v.2.0 -- Feb 28, 2006: update to version 9 using Mata
   v.2.1 -- Apr 11, 2006: whatever
   v.2.2 -- Apr 13, 2006: Satorra-Bentler standard errors and test corrections
                          -vce- option
            Apr 14, 2006: degrees of freedom corrected for # constraints
            July 5, 2006: minor issue with -from(, copy)-
