Announcement

Collapse
No announcement yet.
X
  • Filter
  • Time
  • Show
Clear All
new posts

  • syntax model GBTM


    Good morning to everybody I have 4 variables measured at 3 timepoints: 12 months, 18 months, and 24 months. Is the syntax for choosing the GBTM model correct? I have 106 adults(at least two measurements per outcome).
    Censoring limits were defined outcome by outcome in accordance with the observed empirical range, with a small margin beyond the extremes, as no unambiguous theoretical limits were available for these standardized variables.
    Based on the data structure and criteria of parsimony, stability, and interpretability, with three time points, the search was limited to polynomial forms of order 0/1, without exploring quadratic terms. Although a quadratic specification is technically possible with three surveys, it is often poorly informative and potentially unstable, especially with multiple outcomes and a small sample size. What do you think? Thanks in advanced to everybody
    Code:
    **************************************************** 
     * GBTM MULTI-OUTCOME (4 outcomes, cnorm) * FINAL OPERATIONAL VERSION * CORRECT VERSION: pass uses OCC_pp, also checks TotProb and adds diagnostic entropy * Consistent with: Klijn + Nagin multitrajectory + recent review * * LOGIC: * STEP 0 = preliminary univariate exploration of individual outcomes * STEP 1 = choice of K in the multi-outcome model with equal initial order * STEP 2 = fixed K, structured comparison of all plausible 0/1 models * STEP 2B = refit/inspection of finalist models * * CRITERIA: * - BIC = primary criterion * - APPA / OCC_pp / minP / minTotProb / mismatch = adequacy/support criteria * - relative entropy = additional diagnostic of assignment clarity; NOT included in the pass * - absolute number of groups = descriptive; Does NOT qualify * - DELTABIC <= 2 = competing models * - final decision = BIC + parsimony + interpretability + classification diagnostics * - with 3 time points and MAXORDER=1, the possible orders are 0/1 
    ****************************************************
    
    clear all
    set more off
    set seed 12345
    set sortseed 12345
    
    cd "C:\Users\xxxxxxxxxxx\Desktop\LCA_prova"
    
    global DATAFILE "databasex.dta"
    global IDVAR    "id"
    
    ****************************************************
    * OUTCOME
    ****************************************************
    global VAR1 "var1_12 var1_18 var1_24"
    global VAR2 "var2_12 var2_18 var2_24"
    global VAR3 "var3_12 var3_18 var3_24"
    global VAR4 "var4_12 var4_18 var4_24"
    
    
    **************************************************** 
     * CNORM RANGE - OPTIMIZED ON EMPIRICAL DATA * Expanded outward to ensure numerical stability and avoid artificial clipping 
    ****************************************************
    global MIN1 -8
    global MAX1  6
    global MIN2 -9
    global MAX2 13
    global MIN3 -15
    global MAX3  11
    global MIN4 -3
    global MAX4  9
    
    ****************************************************
    * RICERCA
    ****************************************************
    global MAXK       2
    global MAXORDER   1
    global STARTORDER 1
    global NREFIT     5
    
    ****************************************************
    * SOGLIE DI ADEGUATEZZA
    **************************************************** 
     global THR_MINP 0.05 // minimum assigned proportion of the group global THR_MINTOTPROB 0.05 // minimum estimated proportion from posterior probabilities global THR_MINAPP 0.70 // minimum average posterior probability global THR_MINOCC 5 // minimum OCC_pp global THR_MAXMIS 0.05 // maximum mismatch 
    
    global DELTABIC    2
    
    ****************************************************
    * 
     PROGRAM: create times 
    ****************************************************
    capture program drop make_time
    program define make_time
        capture drop t1 t2 t3
        gen t1 = 0
        gen t2 = 1
        gen t3 = 2
    end
    
    ****************************************************
    * PROGRAMMA: statistiche post-traj
    ****************************************************
    capture program drop gbtm_stats
    program define gbtm_stats, rclass
        syntax , K(integer)
    
        capture drop Mp countG counter APP p n d OCC TotProb mismatch d_pp OCC_pp SD_post __sdtmp
    
        gen double Mp = 0
        foreach pr of varlist _traj_ProbG* {
            replace Mp = `pr' if `pr' > Mp
        }
    
        sort _traj_Group
        by _traj_Group: gen countG  = _N
        by _traj_Group: gen counter = _n
        by _traj_Group: egen double APP = mean(Mp)
    
        gen double p = countG/_N
    
        gen double TotProb = .
        forvalues gg = 1/`k' {
            quietly summarize _traj_ProbG`gg', meanonly
            replace TotProb = r(mean) if _traj_Group == `gg'
        }
    
        gen double mismatch = abs(TotProb - p)
    
        gen double OCC = .
        gen double OCC_pp = .
        if `k' > 1 {
            gen double n = APP/(1-APP)
            gen double d = p/(1-p)
            replace OCC = n/d
            gen double d_pp = TotProb/(1-TotProb)
            replace OCC_pp = n/d_pp
        }
        else {
            replace OCC    = 999
            replace OCC_pp = 999
        }
    
        * PROTEZIONE: Evita crash di Stata se un sottogruppo contiene un solo record (SD non calcolabile)
        gen double SD_post = .
        forvalues gg = 1/`k' {
            capture by _traj_Group: egen double __sdtmp = sd(_traj_ProbG`gg') if _traj_Group == `gg'
            if !_rc {
                replace SD_post = __sdtmp if _traj_Group == `gg'
                drop __sdtmp
            }
        }
    
        * Relative entropy (0-1)
        tempvar __hsum __plnp
        local entropy = 1
        if `k' > 1 {
            gen double `__hsum' = 0
            forvalues gg = 1/`k' {
                gen double `__plnp' = cond(_traj_ProbG`gg' > 0, _traj_ProbG`gg' * ln(_traj_ProbG`gg'), 0)
                replace `__hsum' = `__hsum' + `__plnp'
                drop `__plnp'
            }
            quietly summarize `__hsum', meanonly
            local entropy = 1 + (r(sum) / (_N * ln(`k')))
        }
    
        preserve
            keep if counter == 1
            quietly summarize APP, meanonly
            local minAPP  = r(min)
            local meanAPP = r(mean)
            quietly summarize p, meanonly
            local minP = r(min)
            quietly summarize TotProb, meanonly
            local minTotProb = r(min)
            quietly summarize mismatch, meanonly
            local maxMismatch = r(max)
            quietly summarize OCC, meanonly
            local minOCC = r(min)
            quietly summarize OCC_pp, meanonly
            local minOCCpp = r(min)
        restore
    
        local pass = (`minP' >= $THR_MINP) & ///
                     (`minTotProb' >= $THR_MINTOTPROB) & ///
                     (`minAPP' >= $THR_MINAPP) & ///
                     (`minOCCpp' >= $THR_MINOCC) & ///
                     (`maxMismatch' <= $THR_MAXMIS)
    
        return scalar minAPP      = `minAPP'
        return scalar meanAPP     = `meanAPP'
        return scalar minP        = `minP'
        return scalar minTotProb  = `minTotProb'
        return scalar maxMismatch = `maxMismatch'
        return scalar entropy     = `entropy'
        return scalar minOCC      = `minOCC'
        return scalar minOCCpp    = `minOCCpp'
        return scalar pass        = `pass'
    end
    
    **************************************************** 
     * TEMPORARY FILES 
    ****************************************************
    tempfile phase0tmp step1tmp step2tmp finalists4 step2ranked
    
    ****************************************************
    
    * PHASE 0: PRELIMINARY UNIVARIATE EXPLORATION
    ****************************************************
    tempname h0
    capture postclose `h0'
    postfile `h0' str8 outcome int K str20 orders ///
        double ll aic bic minAPP minOCC minOCCpp minP minTotProb maxMismatch entropy pass ///
        using `phase0tmp', replace
    
    forvalues vv = 1/4 {
        forvalues k = 1/$MAXK {
            use "$DATAFILE", clear
            sort $IDVAR, stable
            make_time
            local indep t1 t2 t3
    
            local oo ""
            forvalues g = 1/`k' {
                local oo "`oo' $STARTORDER"
            }
            local oo : list retok oo
    
            quietly capture traj, ///
                var(${VAR`vv'}) indep(`indep') order(`oo') model(cnorm) min(${MIN`vv'}) max(${MAX`vv'})
            if _rc continue
    
            quietly gbtm_stats, k(`k')
            post `h0' ("VAR`vv'") (`k') ("`oo'") (e(ll)) (e(AIC)) (e(BIC_n_subjects)) ///
                (r(minAPP)) (r(minOCC)) (r(minOCCpp)) ///
                (r(minP)) (r(minTotProb)) (r(maxMismatch)) (r(entropy)) (r(pass))
        }
    }
    postclose `h0'
    
    use `phase0tmp', clear
    save phase0_univariate_scan_4var.dta, replace
    
    **************************************************** 
     * STEP 1: choice of K in multi-outcome 
    ****************************************************
    tempname h1
    capture postclose `h1'
    postfile `h1' ///
        str5 stage int K str20 o1 str20 o2 str20 o3 str20 o4 ///
        int group nG ///
        double p TotProb APP OCC OCC_pp mismatch SD_post ///
        double ll aic bic minAPP meanAPP minOCC minOCCpp minP minTotProb maxMismatch entropy pass ///
        using `step1tmp', replace
    
    forvalues k = 1/$MAXK {
        use "$DATAFILE", clear
        sort $IDVAR, stable
        make_time
        local indep t1 t2 t3
    
        local o1 ""
        local o2 ""
        local o3 ""
        local o4 ""
        forvalues g = 1/`k' {
            local o1 "`o1' $STARTORDER"
            local o2 "`o2' $STARTORDER"
            local o3 "`o3' $STARTORDER"
            local o4 "`o4' $STARTORDER"
        }
        local o1 : list retok o1
        local o2 : list retok o2
        local o3 : list retok o3
        local o4 : list retok o4
    
        quietly capture traj, multgroups(`k') ///
            var1($VAR1) indep1(`indep') order1(`o1') model1(cnorm) min1($MIN1) max1($MAX1) ///
            var2($VAR2) indep2(`indep') order2(`o2') model2(cnorm) min2($MIN2) max2($MAX2) ///
            var3($VAR3) indep3(`indep') order3(`o3') model3(cnorm) min3($MIN3) max3($MAX3) ///
            var4($VAR4) indep4(`indep') order4(`o4') model4(cnorm) min4($MIN4) max4($MAX4)
        if _rc continue
    
        quietly gbtm_stats, k(`k')
        local ll  = e(ll)
        local aic = e(AIC)
        local bic = e(BIC_n_subjects)
        local minAPP      = r(minAPP)
        local meanAPP     = r(meanAPP)
        local minOCC      = r(minOCC)
        local minOCCpp    = r(minOCCpp)
        local maxMismatch = r(maxMismatch)
        local entropy     = r(entropy)
        local minP        = r(minP)
        local minTotProb  = r(minTotProb)
        local pass        = r(pass)
    
        forvalues gg = 1/`k' {
            quietly summarize countG if _traj_Group == `gg', meanonly
            local nG = r(mean)
            quietly summarize p if _traj_Group == `gg', meanonly
            local pg = r(mean)
            quietly summarize TotProb if _traj_Group == `gg', meanonly
            local tpg = r(mean)
            quietly summarize APP if _traj_Group == `gg', meanonly
            local appg = r(mean)
            quietly summarize OCC if _traj_Group == `gg', meanonly
            local occg = r(mean)
            quietly summarize OCC_pp if _traj_Group == `gg', meanonly
            local occppg = r(mean)
            quietly summarize mismatch if _traj_Group == `gg', meanonly
            local misg = r(mean)
            
            local sdg = .
            quietly count if _traj_Group == `gg'
            if r(N) > 1 {
                quietly summarize SD_post if _traj_Group == `gg', meanonly
                local sdg = r(mean)
            }
    
            post `h1' ("STEP1") (`k') ("`o1'") ("`o2'") ("`o3'") ("`o4'") ///
                (`gg') (`nG') (`pg') (`tpg') (`appg') (`occg') (`occppg') (`misg') (`sdg') ///
                (`ll') (`aic') (`bic') (`minAPP') (`meanAPP') (`minOCC') (`minOCCpp') ///
                (`minP') (`minTotProb') (`maxMismatch') (`entropy') (`pass')
        }
    }
    postclose `h1'
    
    use `step1tmp', clear
    egen byte tagmodel = tag(K o1 o2 o3 o4)
    keep if tagmodel
    drop tagmodel
    save step1_kselection_4var.dta, replace
    
    gsort -pass -bic
    count if pass == 1
    if r(N) > 0 {
        keep if pass == 1
        gsort -bic
    }
    else {
        gsort -bic
    }
    quietly summarize K in 1, meanonly
    local BESTK = r(min)
    di as result "K selezionato = `BESTK'"
    
    ****************************************************
    
    * STEP 2: STRUCTURED SEARCH (Safe Combinatorial Logic)
    ****************************************************
    tempname h2
    capture postclose `h2'
    postfile `h2' ///
        str5 stage int K str20 o1 str20 o2 str20 o3 str20 o4 ///
        int group nG ///
        double p TotProb APP OCC OCC_pp mismatch SD_post ///
        double ll aic bic minAPP meanAPP minOCC minOCCpp minP minTotProb maxMismatch entropy pass ///
        using `step2tmp', replace
    
    local k = `BESTK'
    local base = $MAXORDER + 1
    local ncomb = `base'^`k'
    
    forvalues i1 = 1/`ncomb' {
        local o1 ""
        forvalues g = 1/`k' {
            local div   = `base'^(`k' - `g')
            local digit = mod(int((`i1' - 1)/`div'), `base')
            local o1 "`o1' `digit'"
        }
        local o1 : list retok o1
    
        forvalues i2 = 1/`ncomb' {
            local o2 ""
            forvalues g = 1/`k' {
                local div   = `base'^(`k' - `g')
                local digit = mod(int((`i2' - 1)/`div'), `base')
                local o2 "`o2' `digit'"
            }
            local o2 : list retok o2
    
            forvalues i3 = 1/`ncomb' {
                local o3 ""
                forvalues g = 1/`k' {
                    local div   = `base'^(`k' - `g')
                    local digit = mod(int((`i3' - 1)/`div'), `base')
                    local o3 "`o3' `digit'"
                }
                local o3 : list retok o3
    
                forvalues i4 = 1/`ncomb' {
                    local o4 ""
                    forvalues g = 1/`k' {
                        local div   = `base'^(`k' - `g')
                        local digit = mod(int((`i4' - 1)/`div'), `base')
                        local o4 "`o4' `digit'"
                    }
                    local o4 : list retok o4
    
                    use "$DATAFILE", clear
                    sort $IDVAR, stable
                    make_time
                    local indep t1 t2 t3
    
                    quietly capture traj, multgroups(`k') ///
                        var1($VAR1) indep1(`indep') order1(`o1') model1(cnorm) min1($MIN1) max1($MAX1) ///
                        var2($VAR2) indep2(`indep') order2(`o2') model2(cnorm) min2($MIN2) max2($MAX2) ///
                        var3($VAR3) indep3(`indep') order3(`o3') model3(cnorm) min3($MIN3) max3($MAX3) ///
                        var4($VAR4) indep4(`indep') order4(`o4') model4(cnorm) min4($MIN4) max4($MAX4)
                    if _rc continue
    
                    quietly gbtm_stats, k(`k')
                    local ll  = e(ll)
                    local aic = e(AIC)
                    local bic = e(BIC_n_subjects)
                    local minAPP      = r(minAPP)
                    local meanAPP     = r(meanAPP)
                    local minOCC      = r(minOCC)
                    local minOCCpp    = r(minOCCpp)
                    local maxMismatch = r(maxMismatch)
                    local entropy     = r(entropy)
                    local minP        = r(minP)
                    local minTotProb  = r(minTotProb)
                    local pass        = r(pass)
    
                    forvalues gg = 1/`k' {
                        quietly summarize countG if _traj_Group == `gg', meanonly
                        local nG = r(mean)
                        quietly summarize p if _traj_Group == `gg', meanonly
                        local pg = r(mean)
                        quietly summarize TotProb if _traj_Group == `gg', meanonly
                        local tpg = r(mean)
                        quietly summarize APP if _traj_Group == `gg', meanonly
                        local appg = r(mean)
                        quietly summarize OCC if _traj_Group == `gg', meanonly
                        local occg = r(mean)
                        quietly summarize OCC_pp if _traj_Group == `gg', meanonly
                        local occppg = r(mean)
                        quietly summarize mismatch if _traj_Group == `gg', meanonly
                        local misg = r(mean)
                        
                        local sdg = .
                        quietly count if _traj_Group == `gg'
                        if r(N) > 1 {
                            quietly summarize SD_post if _traj_Group == `gg', meanonly
                            local sdg = r(mean)
                        }
    
                        post `h2' ("STEP2") (`k') ("`o1'") ("`o2'") ("`o3'") ("`o4'") ///
                            (`gg') (`nG') (`pg') (`tpg') (`appg') (`occg') (`occppg') (`misg') (`sdg') ///
                            (`ll') (`aic') (`bic') (`minAPP') (`meanAPP') (`minOCC') (`minOCCpp') ///
                            (`minP') (`minTotProb') (`maxMismatch') (`entropy') (`pass')
                    }
                }
            }
        }
    }
    postclose `h2'
    
    use `step2tmp', clear
    save step2_models_4var.dta, replace
    
    egen byte tagmodel = tag(K o1 o2 o3 o4)
    keep if tagmodel
    drop tagmodel
    keep if K == `BESTK'
    
    count if pass == 1
    if r(N) > 0 {
        keep if pass == 1
    }
    
    gsort -bic
    quietly summarize bic, meanonly
    local bestbic = r(max)
    keep if bic >= (`bestbic' - $DELTABIC)
    
    gsort -bic -minAPP -minOCCpp maxMismatch
    gen rank_finalista = _n
    save `step2ranked', replace
    save finalists_step2_4var.dta, replace
    
    count
    local NFINAL = r(N)
    di as result "Numero modelli finalisti entro DeltaBIC = `NFINAL'"
    list rank_finalista K o1 o2 o3 o4 bic minAPP minOCCpp maxMismatch entropy minP minTotProb pass, noobs
    
    **************************************************** 
     * STEP 2B: refit of the finalist models 
    ****************************************************
    local NINSPECT = cond(`NFINAL' < $NREFIT, `NFINAL', $NREFIT)
    forvalues i = 1/`NINSPECT' {
        use finalists_step2_4var.dta, clear
        local CK  = K[`i']
        local CO1 = o1[`i']
        local CO2 = o2[`i']
        local CO3 = o3[`i']
        local CO4 = o4[`i']
    
        capture log close candlog
        log using "candidate4_`i'_K`CK'.smcl", replace name(candlog)
    
        use "$DATAFILE", clear
        sort $IDVAR, stable
        make_time
        local indep t1 t2 t3
    
        traj, multgroups(`CK') ///
            var1($VAR1) indep1(`indep') order1(`CO1') model1(cnorm) min1($MIN1) max1($MAX1) ///
            var2($VAR2) indep2(`indep') order2(`CO2') model2(cnorm) min2($MIN2) max2($MAX2) ///
            var3($VAR3) indep3(`indep') order3(`CO3') model3(cnorm) min3($MIN3) max3($MAX3) ///
            var4($VAR4) indep4(`indep') order4(`CO4') model4(cnorm) min4($MIN4) max4($MAX4)
    
        di as result "BIC = " e(BIC_n_subjects)
        di as result "AIC = " e(AIC)
        di as result "LL  = " e(ll)
    
        log close candlog
    }
    
    **************************************************** 
     * LEAD CANDIDATE ACCORDING TO PRE-SPECIFIED CRITERIA 
    ****************************************************
    use `step2ranked', clear
    gen byte _pick = (_n == 1)
    quietly summarize K if _pick, meanonly
    local FK = r(min)
    levelsof o1 if _pick, local(FO1) clean
    levelsof o2 if _pick, local(FO2) clean
    levelsof o3 if _pick, local(FO3) clean
    levelsof o4 if _pick, local(FO4) clean
    drop _pick
    
    di as result "CANDIDATO PRINCIPALE:"
    di as result "K      = `FK'"
    di as result "order1 = `FO1'"
    di as result "order2 = `FO2'"
    di as result "order3 = `FO3'"
    di as result "order4 = `FO4'"
    
    use "$DATAFILE", clear
    sort $IDVAR, stable
    make_time
    local indep t1 t2 t3
    
    traj, multgroups(`FK') ///
        var1($VAR1) indep1(`indep') order1(`FO1') model1(cnorm) min1($MIN1) max1($MAX1) ///
        var2($VAR2) indep2(`indep') order2(`FO2') model2(cnorm) min2($MIN2) max2($MAX2) ///
        var3($VAR3) indep3(`indep') order3(`FO3') model3(cnorm) min3($MIN3) max3($MAX3) ///
        var4($VAR4) indep4(`indep') order4(`FO4') model4(cnorm) min4($MIN4) max4($MAX4)
    
    di as result "BIC finale = " e(BIC_n_subjects)
    di as result "AIC finale = " e(AIC)
    di as result "LL finale  = " e(ll)
    
    **************************************************** 
     * FINAL STATISTICS OF THE SELECTED MODEL 
    ****************************************************
    quietly gbtm_stats, k(`FK')
    
    di as result "minAPP finale      = " r(minAPP)
    di as result "meanAPP finale     = " r(meanAPP)
    di as result "minP finale        = " r(minP)
    di as result "minTotProb finale  = " r(minTotProb)
    di as result "minOCC finale      = " r(minOCC)
    di as result "minOCCpp finale    = " r(minOCCpp)
    di as result "maxMismatch finale = " r(maxMismatch)
    di as result "entropy finale     = " r(entropy)
    di as result "pass finale        = " r(pass)
    Attached Files

  • #2
    Just to clarify: variable names and the dataset name have been changed for confidentiality. Also, in my actual do-file the threshold globals are written on separate lines.

    In the final version of the do-file, MAXK is set to 3, not 2, and the final number of groups is selected according to BIC, parsimony, interpretability, and classification diagnostics.

    Comment


    • #3
      Originally posted by Tommaso Salvitti View Post
      . . . the final number of groups is selected according to BIC, parsimony, interpretability, and classification diagnostics.
      Originally posted by Tommaso Salvitti View Post
      Is the syntax for choosing the GBTM model correct?
      I'm not very familiar with group-based trajectory modeling (GBTM),but the displayed Stata code appears to choose the final number of groups solely on the basis of the Bayesian information criterion (BIC)..

      Also, the purpose of the article whose reprint that you attached in #1 is to advocate a graphical approach to assist choosing the number of groups. I don't see any graphs created in the code that you show. Instead, it seems to select the model in a "plug-and-chug" manner that that article discourages.

      As an aside, it seems that you're using user-written command to fit these models. As that linked FAQ advises, it would be helpful to give the source of the user-written command, especially because it's not SSC.
      Code:
      net from "https://www.andrew.cmu.edu/user/bjones/traj/"
      Last, the displayed Stata code is difficult to follow (and nonsensical in at least a couple of places), but because you have only six models to examine (intercept-only & linear regression for the polynomial orders and 1, 2 & 3 for the number of groups) you could probably have accomplished your objective with much simpler code, essentially two forvalue loops, one nested within the other.

      Comment


      • #4
        THANKS A LOT IS OK...IS IT CORRECT? Thank you very much for your helpful comments.
        I have revised the code following your suggestions. I now specify the source of the user-written Stata `traj` command:

        ```stata
        net from "https://www.andrew.cmu.edu/user/bjones/traj/"
        ```

        I also simplified the initial class-enumeration step. Instead of using a long automated search, I now compare only the six initial models implied by the design:

        - K = 1, 2, 3
        - common polynomial order = 0 or 1

        For each model, the code extracts BIC, AIC, log-likelihood, APPA, OCC, mismatch between assigned and estimated group proportions, and minimum group proportions. These are inspected as descriptive/supportive diagnostics, not as automatic pass/fail rules. I also added trajectory plots and F-CAP-like graphical summaries of the fit and classification criteria.

        So the code is intended to summarize candidate models and support graphical/substantive inspection, not to choose the final number of groups automatically.

        In my example, the three-group linear model has acceptable classification diagnostics, but the smallest group is close to 5%, so I would treat that as a point requiring substantive and graphical caution rather than as an automatic decision.

        Thanks again — your comment helped me make the model-selection step much clearer and less mechanical.
        Code:
        ****************************************************
        * GBTM / MULTI-TRAJECTORY - CLEAN STATAFORUM VERSION
        * 4 outcomes, 3 time points, cnorm
        *
        * User-written command:
        *     net from "https://www.andrew.cmu.edu/user/bjones/traj/"
        *
        * This do-file follows the StataForum suggestion:
        * - compare only six initial models:
        *       K = 1, 2, 3
        *       common polynomial order = 0 or 1
        * - produce tables and graphs;
        * - do NOT select the final model automatically.
        *
        * APPA, OCC_pp, mismatch and group proportions are descriptive /
        * supportive diagnostics, not automatic pass/fail filters.
        ****************************************************
        
        version 18.0
        clear all
        set more off
        set seed 12345
        set sortseed 12345
        
        ****************************************************
        * EDIT THIS PATH
        ****************************************************
        cd "C:\Users\xxxxxxxxxxx\Desktop\LCA_prova"
        
        global DATAFILE "databasex.dta"
        global IDVAR    "id"
        
        ****************************************************
        * OUTCOMES
        ****************************************************
        global VAR1 "var1_12 var1_18 var1_24"
        global VAR2 "var2_12 var2_18 var2_24"
        global VAR3 "var3_12 var3_18 var3_24"
        global VAR4 "var4_12 var4_18 var4_24"
        
        ****************************************************
        * CNORM LIMITS
        * For the real comprehension dataset these correspond to observed
        * empirical min/max ranges with a small outward margin:
        * VAR1: -5 / 4
        * VAR2: -6 / 5
        * VAR3: -5 / 7
        * VAR4: -3 / 10
        ****************************************************
        global MIN1 -5
        global MAX1  4
        
        global MIN2 -6
        global MAX2  5
        
        global MIN3 -5
        global MAX3  7
        
        global MIN4 -3
        global MAX4 10
        
        ****************************************************
        * Reference thresholds for reading diagnostics only
        ****************************************************
        global REF_MINP        0.05
        global REF_MINTOTPROB  0.05
        global REF_MINAPP      0.70
        global REF_MINOCC      5
        global REF_MAXMIS      0.05
        
        ****************************************************
        * Time coding: 12 months = 0; 18 months = 6; 24 months = 12
        ****************************************************
        capture program drop make_time
        program define make_time
            capture drop t1 t2 t3
            gen double t1 = 0
            gen double t2 = 6
            gen double t3 = 12
        end
        
        ****************************************************
        * Post-traj descriptive diagnostics
        ****************************************************
        capture program drop gbtm_stats
        program define gbtm_stats, rclass
            syntax , K(integer)
        
            capture drop Mp countG counter APP p n d OCC TotProb mismatch d_pp OCC_pp SD_post __sdtmp
        
            gen double Mp = 0
            foreach pr of varlist _traj_ProbG* {
                replace Mp = `pr' if `pr' > Mp
            }
        
            sort _traj_Group
            by _traj_Group: gen countG  = _N
            by _traj_Group: gen counter = _n
            by _traj_Group: egen double APP = mean(Mp)
        
            gen double p = countG/_N
        
            gen double TotProb = .
            forvalues gg = 1/`k' {
                quietly summarize _traj_ProbG`gg', meanonly
                replace TotProb = r(mean) if _traj_Group == `gg'
            }
        
            gen double mismatch = abs(TotProb - p)
        
            gen double OCC    = .
            gen double OCC_pp = .
        
            if `k' == 1 {
                replace OCC    = 999
                replace OCC_pp = 999
            }
            else {
                gen double n = APP/(1-APP)
                gen double d = p/(1-p)
                replace OCC = n/d
        
                gen double d_pp = TotProb/(1-TotProb)
                replace OCC_pp = n/d_pp
            }
        
            gen double SD_post = .
            forvalues gg = 1/`k' {
                capture drop __sdtmp
                capture egen double __sdtmp = sd(_traj_ProbG`gg') if _traj_Group == `gg'
                if !_rc {
                    replace SD_post = __sdtmp if _traj_Group == `gg'
                    drop __sdtmp
                }
            }
        
            local entropy = 1
            if `k' > 1 {
                tempvar __hsum __plnp
                gen double `__hsum' = 0
                forvalues gg = 1/`k' {
                    gen double `__plnp' = cond(_traj_ProbG`gg' > 0, _traj_ProbG`gg' * ln(_traj_ProbG`gg'), 0)
                    replace `__hsum' = `__hsum' + `__plnp'
                    drop `__plnp'
                }
                quietly summarize `__hsum', meanonly
                local entropy = 1 + (r(sum) / (_N * ln(`k')))
            }
        
            preserve
                keep if counter == 1
        
                quietly summarize APP, meanonly
                local minAPP  = r(min)
                local meanAPP = r(mean)
        
                quietly summarize p, meanonly
                local minP = r(min)
        
                quietly summarize TotProb, meanonly
                local minTotProb = r(min)
        
                quietly summarize mismatch, meanonly
                local maxMismatch = r(max)
        
                quietly summarize OCC, meanonly
                local minOCC = r(min)
        
                quietly summarize OCC_pp, meanonly
                local minOCCpp = r(min)
            restore
        
            return scalar minAPP      = `minAPP'
            return scalar meanAPP     = `meanAPP'
            return scalar minP        = `minP'
            return scalar minTotProb  = `minTotProb'
            return scalar maxMismatch = `maxMismatch'
            return scalar entropy     = `entropy'
            return scalar minOCC      = `minOCC'
            return scalar minOCCpp    = `minOCCpp'
        end
        
        ****************************************************
        * Save trajectory graph if a plotting command is available
        ****************************************************
        capture program drop save_traj_graph
        program define save_traj_graph
            syntax , Name(string)
        
            local graphrc = 1
        
            capture noisily multtrajplot
            if !_rc local graphrc = 0
        
            if `graphrc' != 0 {
                capture noisily trajplot
                if !_rc local graphrc = 0
            }
        
            if `graphrc' == 0 {
                capture graph export "`name'.png", replace width(2400)
            }
        end
        
        ****************************************************
        * 1. Checks
        ****************************************************
        capture which traj
        if _rc {
            di as error "traj is not installed."
            di as error `"Install with: net from "https://www.andrew.cmu.edu/user/bjones/traj/""'
            exit 199
        }
        
        use "$DATAFILE", clear
        
        confirm variable $IDVAR
        confirm variable var1_12
        confirm variable var1_18
        confirm variable var1_24
        confirm variable var2_12
        confirm variable var2_18
        confirm variable var2_24
        confirm variable var3_12
        confirm variable var3_18
        confirm variable var3_24
        confirm variable var4_12
        confirm variable var4_18
        confirm variable var4_24
        
        di as result "Checks OK."
        
        ****************************************************
        * 2. Six candidate models:
        *    K = 1, 2, 3 and common polynomial order = 0 or 1
        ****************************************************
        tempfile results6
        
        tempname h1
        capture postclose `h1'
        postfile `h1' ///
            int K common_order str20 orders ///
            double ll aic bic minAPP meanAPP minOCC minOCCpp ///
            double minP minTotProb maxMismatch entropy ///
            using `results6', replace
        
        forvalues common = 0/1 {
            forvalues k = 1/3 {
        
                use "$DATAFILE", clear
                sort $IDVAR, stable
                make_time
                local indep t1 t2 t3
        
                local oo ""
                forvalues g = 1/`k' {
                    local oo "`oo' `common'"
                }
                local oo : list retok oo
        
                quietly capture traj, multgroups(`k') ///
                    var1($VAR1) indep1(`indep') order1(`oo') model1(cnorm) min1($MIN1) max1($MAX1) ///
                    var2($VAR2) indep2(`indep') order2(`oo') model2(cnorm) min2($MIN2) max2($MAX2) ///
                    var3($VAR3) indep3(`indep') order3(`oo') model3(cnorm) min3($MIN3) max3($MAX3) ///
                    var4($VAR4) indep4(`indep') order4(`oo') model4(cnorm) min4($MIN4) max4($MAX4)
        
                if _rc {
                    di as error "Model not estimated: K=`k', common_order=`common'"
                    continue
                }
        
                quietly gbtm_stats, k(`k')
        
                post `h1' (`k') (`common') ("`oo'") ///
                    (e(ll)) (e(AIC)) (e(BIC_n_subjects)) ///
                    (r(minAPP)) (r(meanAPP)) (r(minOCC)) (r(minOCCpp)) ///
                    (r(minP)) (r(minTotProb)) (r(maxMismatch)) (r(entropy))
        
                save_traj_graph, name("ANONYMIZED_4OUTCOME_trajectory_K`k'_order`common'")
            }
        }
        postclose `h1'
        
        use `results6', clear
        sort common_order K
        save "ANONYMIZED_4OUTCOME_six_model_results.dta", replace
        
        di as result "================ SIX MODEL RESULTS ================"
        list K common_order orders bic aic ll minAPP minOCCpp ///
             minP minTotProb maxMismatch entropy, ///
             noobs abbreviate(20)
        
        ****************************************************
        * 3. F-CAP-like graphical summaries
        ****************************************************
        twoway ///
            (connected bic K if common_order == 0, msymbol(O)) ///
            (connected bic K if common_order == 1, msymbol(T)), ///
            xlabel(1(1)3) ///
            title("BIC by K and common polynomial order") ///
            ytitle("BIC") xtitle("K") ///
            legend(order(1 "order 0" 2 "order 1")) ///
            name(g_bic, replace)
        
        twoway ///
            (connected minAPP K if common_order == 0, msymbol(O)) ///
            (connected minAPP K if common_order == 1, msymbol(T)), ///
            xlabel(1(1)3) ///
            yline($REF_MINAPP, lpattern(dash)) ///
            title("Minimum APPA") ///
            ytitle("min APPA") xtitle("K") ///
            legend(order(1 "order 0" 2 "order 1")) ///
            name(g_app, replace)
        
        gen minOCCpp_plot = minOCCpp
        replace minOCCpp_plot = . if K == 1 | minOCCpp_plot > 100
        
        twoway ///
            (connected minOCCpp_plot K if common_order == 0 & K > 1, msymbol(O)) ///
            (connected minOCCpp_plot K if common_order == 1 & K > 1, msymbol(T)), ///
            xlabel(1(1)3) ///
            yline($REF_MINOCC, lpattern(dash)) ///
            title("Minimum OCC_pp") ///
            ytitle("min OCC_pp") xtitle("K") ///
            legend(order(1 "order 0" 2 "order 1")) ///
            name(g_occ, replace)
        
        twoway ///
            (connected minP K if common_order == 0, msymbol(O)) ///
            (connected minP K if common_order == 1, msymbol(T)) ///
            (connected minTotProb K if common_order == 0, msymbol(Oh)) ///
            (connected minTotProb K if common_order == 1, msymbol(Th)), ///
            xlabel(1(1)3) ///
            yline($REF_MINP, lpattern(shortdash)) ///
            title("Minimum group proportions") ///
            ytitle("Proportion") xtitle("K") ///
            legend(order(1 "assigned order 0" 2 "assigned order 1" 3 "estimated order 0" 4 "estimated order 1")) ///
            name(g_prop, replace)
        
        twoway ///
            (connected maxMismatch K if common_order == 0, msymbol(O)) ///
            (connected maxMismatch K if common_order == 1, msymbol(T)), ///
            xlabel(1(1)3) ///
            yline($REF_MAXMIS, lpattern(dash)) ///
            title("Maximum mismatch") ///
            ytitle("Mismatch") xtitle("K") ///
            legend(order(1 "order 0" 2 "order 1")) ///
            name(g_mis, replace)
        
        graph combine g_bic g_app g_occ g_prop g_mis, ///
            cols(2) ///
            title("K-selection summary") ///
            name(fcap_like_summary, replace)
        
        graph export "ANONYMIZED_4OUTCOME_fcap_like_six_models.png", replace width(2400)
        
        di as result "Done. Inspect the table, the F-CAP-like graph, and trajectory plots."
        di as result "Do not select the final model automatically."

        Comment


        • #5
          Joseph Coveney Thank you again for the helpful comments.

          I realize that my previous code may have given the impression that the model was selected automatically, mainly by BIC. That was not my intention. In the revised version I am trying to make the procedure clearer and less “plug-and-chug”.

          I am using the user-written Stata traj command from:
          net from "https://www.andrew.cmu.edu/user/bjones/traj/"
          The revised procedure is now split into two parts.

          First, for the initial class-enumeration step, I compare only the six basic candidate models:
          K = 1, 2, 3 common polynomial order = 0 or 1
          For each model I save BIC, AIC, log-likelihood and several classification diagnostics: minimum APPA, OCC based on posterior probabilities, minimum assigned group proportion, minimum estimated group proportion, mismatch between assigned and estimated proportions, and relative entropy.

          Importantly, these quantities are not used as an automatic selection rule. The code is intended to produce a table and graphical summaries. The final decision is based on BIC as the primary fit criterion, but also on classification quality, group size, mismatch, parsimony, graphical inspection of the trajectories, and substantive interpretability.

          For example, the code does not simply say “choose the model with the best BIC”. Instead, I inspect a table such as:
          list K common_order orders bic aic ll minAPP minOCCpp /// minP minTotProb maxMismatch entropy, /// noobs abbreviate(20)
          and I also produce F-CAP-like plots and trajectory plots.

          After the initial evaluation of K, I then fix the selected/candidate K and compare polynomial-order structures separately. With three time points and maximum order 1, the possible polynomial orders are 0 and 1. This second step compares more parsimonious 0/1 structures across outcomes and groups. Again, BIC is the primary criterion, but the final choice is not automatic: I also consider parsimony, interpretability, posterior classification diagnostics and the trajectory plots.

          So the intended logic is:
          Step 1: compare K = 1, 2, 3 using common order 0/1. Step 2: after choosing a candidate K, compare polynomial-order structures with K fixed. Step 3: inspect BIC, diagnostics, group proportions, mismatch and trajectory plots before deciding.
          In the revised code, APPA, OCC, group proportions, mismatch and entropy are therefore reported as diagnostic/supportive information. They are not meant to replace substantive judgement or graphical inspection.

          Does this way of structuring the model-selection procedure seem more appropriate?

          Comment


          • #6

            Thank you for your comments. I tried to revise the workflow so that the code no longer automatically selects the final number of groups.

            The idea is now:
            1. Run a transparent class-enumeration step with only the main models:
              • K = 1, 2, 3
              • uniform polynomial order = 0 or 1
              • therefore 6 models in total.
            2. Store BIC, AIC, log-likelihood and simple classification diagnostics.
            3. Produce F-CAP-style plots for BIC, minimum APPA, minimum OCC, mismatch and minimum group proportion.
            4. Choose K manually after inspecting the table, the plots and the substantive interpretability.
            5. Only after K has been fixed manually, run the second step comparing different 0/1 polynomial-order combinations across outcomes.
            The user-written command is traj, installed from:
            net from "https://www.andrew.cmu.edu/user/bjones/traj/"
            Below is a simplified version of the revised class-enumeration step. Variable names and dataset name have been changed for confidentiality.

            Code:
            clear all set more off set seed 12345 set sortseed 12345 use "mydata.dta", clear * ID variable global IDVAR id * Six outcomes, each measured at 12, 18 and 24 months global VAR1 "y1_12 y1_18 y1_24" global VAR2 "y2_12 y2_18 y2_24" global VAR3 "y3_12 y3_18 y3_24" global VAR4 "y4_12 y4_18 y4_24" global VAR5 "y5_12 y5_18 y5_24" global VAR6 "y6_12 y6_18 y6_24" * Censored-normal limits, defined outcome by outcome global MIN1 -5 global MAX1 3 global MIN2 -4 global MAX2 3 global MIN3 -3 global MAX3 3 global MIN4 -5 global MAX4 3 global MIN5 -3 global MAX5 7 global MIN6 -4 global MAX6 4 * Time coded as elapsed months since the first measurement gen t1 = 0 gen t2 = 6 gen t3 = 12 local indep t1 t2 t3 * Model limits global MAXK 3 global MAXORDER 1 tempfile results tempname H postfile `H' int K int order /// double ll aic bic minAPP minOCC minP maxMismatch /// using `results', replace forvalues ord = 0/$MAXORDER { forvalues k = 1/$MAXK { preserve * Same polynomial order for all groups and all outcomes local o "" forvalues g = 1/`k' { local o "`o' `ord'" } local o : list retok o di as text "Fitting K=`k', uniform order=`ord'" capture noisily traj, multgroups(`k') /// var1($VAR1) indep1(`indep') order1(`o') model1(cnorm) min1($MIN1) max1($MAX1) /// var2($VAR2) indep2(`indep') order2(`o') model2(cnorm) min2($MIN2) max2($MAX2) /// var3($VAR3) indep3(`indep') order3(`o') model3(cnorm) min3($MIN3) max3($MAX3) /// var4($VAR4) indep4(`indep') order4(`o') model4(cnorm) min4($MIN4) max4($MAX4) /// var5($VAR5) indep5(`indep') order5(`o') model5(cnorm) min5($MIN5) max5($MAX5) /// var6($VAR6) indep6(`indep') order6(`o') model6(cnorm) min6($MIN6) max6($MAX6) if _rc { di as error "Model failed: K=`k', order=`ord'" restore continue } * Maximum posterior probability for each subject gen double maxpp = 0 foreach pr of varlist _traj_ProbG* { replace maxpp = `pr' if `pr' > maxpp } * Assigned proportion and APPA by group sort _traj_Group by _traj_Group: gen nG = _N by _traj_Group: gen first = (_n == 1) by _traj_Group: egen double APP = mean(maxpp) gen double p = nG / _N * Estimated group proportions from posterior probabilities gen double TotProb = . forvalues gg = 1/`k' { quietly summarize _traj_ProbG`gg', meanonly replace TotProb = r(mean) if _traj_Group == `gg' } gen double mismatch = abs(TotProb - p) * OCC based on estimated posterior probability group size gen double OCC = . if `k' > 1 { replace OCC = (APP/(1-APP)) / (TotProb/(1-TotProb)) } else { replace OCC = 999 } quietly summarize APP if first, meanonly local minAPP = r(min) quietly summarize OCC if first, meanonly local minOCC = r(min) quietly summarize p if first, meanonly local minP = r(min) quietly summarize mismatch if first, meanonly local maxMismatch = r(max) post `H' (`k') (`ord') /// (e(ll)) (e(AIC)) (e(BIC_n_subjects)) /// (`minAPP') (`minOCC') (`minP') (`maxMismatch') * Trajectory plot for visual inspection capture noisily multtrajplot if !_rc { graph export "traj_K`k'_order`ord'.png", replace } restore } } postclose `H' use `results', clear gsort -bic gen deltaBIC = bic[1] - bic list K order bic deltaBIC aic ll minAPP minOCC minP maxMismatch, noobs * F-CAP-style simple plots twoway /// (connected bic K if order == 0) /// (connected bic K if order == 1), /// title("BIC by K and polynomial order") /// xtitle("Number of groups") /// ytitle("BIC") /// legend(order(1 "Order 0" 2 "Order 1")) graph export "fcap_BIC.png", replace twoway /// (connected minAPP K if order == 0) /// (connected minAPP K if order == 1) /// (function y=.70, range(1 3)), /// title("Minimum APPA by K") /// xtitle("Number of groups") /// ytitle("Minimum APPA") /// legend(order(1 "Order 0" 2 "Order 1" 3 "0.70 threshold")) graph export "fcap_minAPP.png", replace twoway /// (connected minOCC K if order == 0) /// (connected minOCC K if order == 1) /// (function y=5, range(1 3)), /// title("Minimum OCC by K") /// xtitle("Number of groups") /// ytitle("Minimum OCC") /// legend(order(1 "Order 0" 2 "Order 1" 3 "5 threshold")) graph export "fcap_minOCC.png", replace twoway /// (connected maxMismatch K if order == 0) /// (connected maxMismatch K if order == 1), /// title("Maximum mismatch by K") /// xtitle("Number of groups") /// ytitle("Maximum mismatch") /// legend(order(1 "Order 0" 2 "Order 1")) graph export "fcap_mismatch.png", replace twoway /// (connected minP K if order == 0) /// (connected minP K if order == 1), /// title("Minimum assigned group proportion by K") /// xtitle("Number of groups") /// ytitle("Minimum assigned proportion") /// legend(order(1 "Order 0" 2 "Order 1")) graph export "fcap_minP.png", replace
            After inspecting this table and the plots, I manually set K before moving to the second step. For example:
            global FINALK 2
            Then, with K fixed, I compare the 0/1 polynomial-order combinations across the six outcomes. I do not use that second step to re-select K.

            Does this revised workflow and simplified syntax address the previous concern about an overly automatic BIC-only selection?

            Comment


            • #7
              Thank you again for your comments. I revised the workflow so that the code does not automatically select the final model. The first step now compares only six initial models: K = 1, 2, 3 crossed with a common polynomial order equal to 0 or 1. The code then produces a table and F-CAP-style plots. K is chosen manually after inspecting BIC, APPA, OCC, mismatch, group proportions and trajectory plots. Only after K has been fixed manually would I proceed to the second step, comparing polynomial-order combinations with K fixed.

              The user-written command is traj, from:

              Code:
              net from "https://www.andrew.cmu.edu/user/bjones/traj/"
              Variable names and the dataset name have been anonymized.

              Code:
              version 18.0
              clear all
              set more off
              set seed 12345
              set sortseed 12345
              
              ****************************************************
              * Change path and dataset
              ****************************************************
              cd "C:\Users\xxxxxxxxxxx\Desktop\LCA_prova"
              
              global DATAFILE "database_anonymized.dta"
              global IDVAR    "id"
              
              ****************************************************
              * Four outcomes, each measured at 12, 18 and 24 months
              ****************************************************
              global VAR1 "var1_12 var1_18 var1_24"
              global VAR2 "var2_12 var2_18 var2_24"
              global VAR3 "var3_12 var3_18 var3_24"
              global VAR4 "var4_12 var4_18 var4_24"
              
              ****************************************************
              * Censored-normal limits, defined outcome by outcome
              ****************************************************
              global MIN1 -5
              global MAX1  4
              
              global MIN2 -6
              global MAX2  5
              
              global MIN3 -5
              global MAX3  7
              
              global MIN4 -3
              global MAX4 10
              
              ****************************************************
              * Reference thresholds for reading the diagnostics only
              ****************************************************
              global REF_MINP       0.05
              global REF_MINAPP     0.70
              global REF_MINOCC     5
              global REF_MAXMIS     0.05
              
              ****************************************************
              * Time coding: 12 months = 0; 18 months = 6; 24 months = 12
              ****************************************************
              capture program drop make_time
              program define make_time
                  capture drop t1 t2 t3
                  gen double t1 = 0
                  gen double t2 = 6
                  gen double t3 = 12
              end
              
              ****************************************************
              * Post-traj descriptive diagnostics
              ****************************************************
              capture program drop gbtm_stats
              program define gbtm_stats, rclass
                  syntax , K(integer)
              
                  capture drop Mp countG counter APP p n d OCC TotProb mismatch d_pp OCC_pp
              
                  gen double Mp = 0
                  foreach pr of varlist _traj_ProbG* {
                      replace Mp = `pr' if `pr' > Mp
                  }
              
                  sort _traj_Group
                  by _traj_Group: gen countG  = _N
                  by _traj_Group: gen counter = _n
                  by _traj_Group: egen double APP = mean(Mp)
              
                  gen double p = countG / _N
              
                  gen double TotProb = .
                  forvalues gg = 1/`k' {
                      quietly summarize _traj_ProbG`gg', meanonly
                      replace TotProb = r(mean) if _traj_Group == `gg'
                  }
              
                  gen double mismatch = abs(TotProb - p)
              
                  gen double OCC = .
                  gen double OCC_pp = .
              
                  if `k' == 1 {
                      replace OCC    = 999
                      replace OCC_pp = 999
                  }
                  else {
                      gen double n = APP / (1 - APP)
                      gen double d = p / (1 - p)
                      replace OCC = n / d
              
                      gen double d_pp = TotProb / (1 - TotProb)
                      replace OCC_pp = n / d_pp
                  }
              
                  preserve
                      keep if counter == 1
              
                      quietly summarize APP, meanonly
                      local minAPP  = r(min)
                      local meanAPP = r(mean)
              
                      quietly summarize p, meanonly
                      local minP = r(min)
              
                      quietly summarize TotProb, meanonly
                      local minTotProb = r(min)
              
                      quietly summarize mismatch, meanonly
                      local maxMismatch = r(max)
              
                      quietly summarize OCC, meanonly
                      local minOCC = r(min)
              
                      quietly summarize OCC_pp, meanonly
                      local minOCCpp = r(min)
                  restore
              
                  return scalar minAPP      = `minAPP'
                  return scalar meanAPP     = `meanAPP'
                  return scalar minP        = `minP'
                  return scalar minTotProb  = `minTotProb'
                  return scalar maxMismatch = `maxMismatch'
                  return scalar minOCC      = `minOCC'
                  return scalar minOCCpp    = `minOCCpp'
              end
              
              ****************************************************
              * Check traj installation and variables
              ****************************************************
              capture which traj
              if _rc {
                  di as error "traj is not installed."
                  di as error `"Install it with: net from "https://www.andrew.cmu.edu/user/bjones/traj/""'
                  exit 199
              }
              
              use "$DATAFILE", clear
              
              confirm variable $IDVAR
              confirm variable var1_12
              confirm variable var1_18
              confirm variable var1_24
              confirm variable var2_12
              confirm variable var2_18
              confirm variable var2_24
              confirm variable var3_12
              confirm variable var3_18
              confirm variable var3_24
              confirm variable var4_12
              confirm variable var4_18
              confirm variable var4_24
              
              di as result "Variable checks OK."
              
              ****************************************************
              * Six initial candidate models:
              * K = 1, 2, 3 and common polynomial order = 0 or 1
              ****************************************************
              tempfile results6
              tempname h1
              
              capture postclose `h1'
              postfile `h1' ///
                  int K common_order str20 orders ///
                  double ll aic bic minAPP meanAPP minOCC minOCCpp ///
                  double minP minTotProb maxMismatch ///
                  using `results6', replace
              
              forvalues common = 0/1 {
                  forvalues k = 1/3 {
              
                      use "$DATAFILE", clear
                      sort $IDVAR, stable
                      make_time
                      local indep t1 t2 t3
              
                      local oo ""
                      forvalues g = 1/`k' {
                          local oo "`oo' `common'"
                      }
                      local oo : list retok oo
              
                      di as text "Fitting K=`k', common order=`common'"
              
                      capture noisily traj, multgroups(`k') ///
                          var1($VAR1) indep1(`indep') order1(`oo') model1(cnorm) min1($MIN1) max1($MAX1) ///
                          var2($VAR2) indep2(`indep') order2(`oo') model2(cnorm) min2($MIN2) max2($MAX2) ///
                          var3($VAR3) indep3(`indep') order3(`oo') model3(cnorm) min3($MIN3) max3($MAX3) ///
                          var4($VAR4) indep4(`indep') order4(`oo') model4(cnorm) min4($MIN4) max4($MAX4)
              
                      if _rc {
                          di as error "Model not estimated: K=`k', common_order=`common'"
                          continue
                      }
              
                      quietly gbtm_stats, k(`k')
              
                      post `h1' (`k') (`common') ("`oo'") ///
                          (e(ll)) (e(AIC)) (e(BIC_n_subjects)) ///
                          (r(minAPP)) (r(meanAPP)) (r(minOCC)) (r(minOCCpp)) ///
                          (r(minP)) (r(minTotProb)) (r(maxMismatch))
              
                      capture noisily multtrajplot
                      if !_rc {
                          graph export "ANONYMIZED_4OUTCOME_trajectory_K`k'_order`common'.png", replace width(2400)
                      }
                  }
              }
              
              postclose `h1'
              
              use `results6', clear
              gsort -bic
              gen double deltaBIC = bic[1] - bic
              
              save "ANONYMIZED_4OUTCOME_six_model_results.dta", replace
              
              di as result "================ SIX INITIAL MODELS ================"
              list K common_order orders bic deltaBIC aic ll minAPP minOCCpp ///
                   minP minTotProb maxMismatch, ///
                   noobs abbreviate(20)
              
              ****************************************************
              * F-CAP-style summary plots
              ****************************************************
              twoway ///
                  (connected bic K if common_order == 0, msymbol(O)) ///
                  (connected bic K if common_order == 1, msymbol(T)), ///
                  xlabel(1(1)3) ///
                  title("BIC by K and common polynomial order") ///
                  ytitle("BIC") ///
                  xtitle("K") ///
                  legend(order(1 "Order 0" 2 "Order 1")) ///
                  name(g_bic, replace)
              
              twoway ///
                  (connected minAPP K if common_order == 0, msymbol(O)) ///
                  (connected minAPP K if common_order == 1, msymbol(T)), ///
                  xlabel(1(1)3) ///
                  yline($REF_MINAPP, lpattern(dash)) ///
                  title("Minimum APPA") ///
                  ytitle("Minimum APPA") ///
                  xtitle("K") ///
                  legend(order(1 "Order 0" 2 "Order 1")) ///
                  name(g_app, replace)
              
              gen double minOCCpp_plot = minOCCpp
              replace minOCCpp_plot = . if K == 1 | minOCCpp_plot > 100
              
              twoway ///
                  (connected minOCCpp_plot K if common_order == 0 & K > 1, msymbol(O)) ///
                  (connected minOCCpp_plot K if common_order == 1 & K > 1, msymbol(T)), ///
                  xlabel(1(1)3) ///
                  yline($REF_MINOCC, lpattern(dash)) ///
                  title("Minimum OCC_pp") ///
                  ytitle("Minimum OCC_pp") ///
                  xtitle("K") ///
                  legend(order(1 "Order 0" 2 "Order 1")) ///
                  name(g_occ, replace)
              
              twoway ///
                  (connected minP K if common_order == 0, msymbol(O)) ///
                  (connected minP K if common_order == 1, msymbol(T)) ///
                  (connected minTotProb K if common_order == 0, msymbol(Oh)) ///
                  (connected minTotProb K if common_order == 1, msymbol(Th)), ///
                  xlabel(1(1)3) ///
                  yline($REF_MINP, lpattern(shortdash)) ///
                  title("Minimum group proportions") ///
                  ytitle("Proportion") ///
                  xtitle("K") ///
                  legend(order(1 "Assigned, order 0" 2 "Assigned, order 1" ///
                               3 "Estimated, order 0" 4 "Estimated, order 1")) ///
                  name(g_prop, replace)
              
              twoway ///
                  (connected maxMismatch K if common_order == 0, msymbol(O)) ///
                  (connected maxMismatch K if common_order == 1, msymbol(T)), ///
                  xlabel(1(1)3) ///
                  yline($REF_MAXMIS, lpattern(dash)) ///
                  title("Maximum mismatch") ///
                  ytitle("Mismatch") ///
                  xtitle("K") ///
                  legend(order(1 "Order 0" 2 "Order 1")) ///
                  name(g_mis, replace)
              
              graph combine g_bic g_app g_occ g_prop g_mis, ///
                  cols(2) ///
                  title("F-CAP-style summary for K selection") ///
                  name(fcap_like_summary, replace)
              
              graph export "ANONYMIZED_4OUTCOME_fcap_like_six_models.png", replace width(2400)
              
              di as result "Done. Inspect the table, F-CAP-style plot and trajectory plots."
              di as result "The code does not automatically select the final model."
              After this step, I would inspect the table, the F-CAP-style plots and the trajectory plots, and only then manually set the chosen K before comparing polynomial-order combinations with K fixed.

              Does this revised workflow and syntax address the previous concern about a BIC-only, automatic model-selection procedure?

              Comment


              • #8
                IS IT CORRECT ACCORDING TO YOU AND Joseph Coveney ?

                Comment

                Working...
                X