*! pystacked v0.8.0
*! last edited: 14july2026
*! authors: aa/ms
*! pystacked2 = uses python code appended to the file


// do not edit this file manually; edit pystacked1.ado, then run R script

// parent program
program define pystacked2, eclass
    version 16.0

    // exception for printoption which can be called w/o variables
    tokenize `"`0'"', parse(",")
    local beforecomma `1'
    macro shift
    local restargs `*'
    local printopt_on = strpos("`restargs'","print")!=0

    if ~replay() {
        // no replay - must estimate

        /* // load core python code
        python clear
        pystacked_check_python
        qui findfile pystacked.py
        cap python script "`r(fn)'", global
        if _rc != 0 {
            noi disp "Error loading Python Script for pystacked."
            error 199
        }
        python: from pystacked import *
        */ // end load core python code

        _pystacked `0'
    }
    else if replay() & `printopt_on' {
        // just print options and leave;
        // query sklearn version of the active Python so that the
        // displayed translation matches the user's setup
        cap python: import sfi
        cap python: from sklearn import __version__ as sklearn_version
        cap python: sfi.Macro.setLocal("sklearn_ver1",format(sklearn_version).split(".")[0])
        cap python: sfi.Macro.setLocal("sklearn_ver2",format(sklearn_version).split(".")[1])
        cap python: sfi.Macro.setLocal("sklearn_ver3",format(sklearn_version).split(".")[2])
        cap confirm number `sklearn_ver3'
        if _rc local sklearn_ver3 0
        if "`sklearn_ver1'"=="" | "`sklearn_ver2'"=="" {
            // version could not be detected (e.g. no sklearn); use _pyparse defaults
            _pyparse `0'
        }
        else {
            _pyparse `0' sklearn1(`sklearn_ver1') sklearn2(`sklearn_ver2') sklearn3(`sklearn_ver3')
        }
        exit
    }
    else {
        // replay - check that pystacked estimation is in memory
        if "`e(cmd)'"~="pystacked" {
            di as err "last estimates not found"
            exit 301
        }
    }

    // save for display results
    tempname weights_mat
    mat `weights_mat'=e(weights)
    local base_est `e(base_est)'
    local nlearners    = e(mcount)

    // parse and check for graph/table options
    // code borrowed from _pstacked below - needed to accommodate syntax #2
    if ~replay() {
        tokenize "`0'", parse(",")
        local beforecomma `1'
        macro shift
        local restargs `*'
        local 0 `beforecomma'
        syntax anything(name=beforeifinweight) [if] [in] [aweight fweight]
        if "`weight'"!="" {
            local ifinweight `if' `in' [`weight' `exp']
        }
        else {
            local ifinweight `if' `in' `weight' `exp'
        }
        tokenize `beforeifinweight', parse("|")
        local mainargs `1'
        local 0 `mainargs' `ifinweight' `restargs'
    }
    syntax [anything]  [if] [in] [aweight fweight] ,    ///
                [                                       ///
                    GRAPH1                              /// vanilla option, abbreviates to "graph"
                    HISTogram                           /// report histogram instead of default ROC
                    graph(string asis)                  /// for passing options to graph combine
                    lgraph(string asis)                 /// for passing options to the graphs of the learners
                    TABLE1                              ///
                    table(string)                       /// rmspe or confusion
                    HOLDOUT1                            /// vanilla option, abbreviates to "holdout"
                    holdout(varname)                    ///
                    CValid                              ///
                    NOESTIMATE                          /// suppress call to run_stacked; no estimates, only parses
                    SHOWCoefs                           ///
                    PRINTopt                            ///
                    CVC1                                /// vanilla option, abbreviates to "cvc"
                    cvc(integer 0)                      /// to capture if cvc bootstrap reps were specified
                    *                                   ///
                ]

    if "`printopt'"!="" local noestimate noestimate

    // if non-zero integer for cvc was supplied via cvc(int) option, set cvc1 to non-empty string
    // if vanilla cvc option has been specified, cvc1 is already non-empty
    if `cvc' > 0 {
        local cvc1 cvc1
    }

    // default reg table = rmspe
    // default class table = confusion
    // table1 = "table" or ""
    // table = rmspe or confusion
    if "`table'"~="" & "`table1'"~="" {
        di as err "error - multiple table options specified"
        exit 198
    }
    if "`table'"~="" & "`table'"~="rmspe" & "`table'"~="confusion" {
        di as err "error - option table(`table') not supported"
        exit 198
    }
    if "`table'"=="confusion" & "`e(type)'"=="reg" {
        di as err "error - confusion table available for classification problems only"
        exit 198
    }
    // if table1 and table both blank, set macro table to default type
    if "`table1'"~="" & "`table'"=="" {
        if "`e(type)'"=="reg" {
            local table rmspe
        }
        else {
            local table confusion
        }
    }
    // from here, table is the macro indicating table type

    // display results
    if `"`graph'`graph1'`lgraph'`histogram'`table'`noestimate'"' == "" {

        di
        di as res "Stacking weights:"
        di as text "{hline 17}{c TT}{hline 21}"
        di as text "  Method" _c
        di as text _col(18) "{c |}      Weight"
        di as text "{hline 17}{c +}{hline 21}"

        forvalues j=1/`nlearners' {
            local b : word `j' of `base_est'
            di as text "  `b'" _c
            di as text _col(18) "{c |}" _c
            di as res %15.7f el(`weights_mat',`j',1)
        }
    }

    // display results
    if "`showcoefs'"!="" & "`noestimate'" == "" {

        forvalues i = 1(1)`e(mcount)' {

            if (`e(has_coefs`i')') {

                // get learner

                local thislearner : word `i' of `e(base_est)'
                // get coefficient matrix
                tempname coefs_mat
                mat `coefs_mat'=e(coefs`i')

                // add constant as a name if it's there
                local thexvars `e(xvars_o`i')'
                if (`e(has_intercept`i')') local thexvars `thexvars' _cons

                // save number of coefficients and number of var names
                local coef_nums = rowsof(`coefs_mat')
                local varname_count : word count `thexvars'

                // get max string length of variables
                local maxstrlen = 17
                foreach i in `thexvars' {
                    local thisstrlen = strlen("`i'")
                    if (`thisstrlen'>`maxstrlen'-2) local maxstrlen = `thisstrlen'+2
                }
                local maxstrlen = `maxstrlen'+2

                // which type of coefficients are shown
                local coeftype Coefficients
                if regexm("rf gradboost","`thislearner'") {
                    local coeftype Variable importance
                }

                // only display if # names = # coefs
                if (`coef_nums' == `varname_count') {
                    di
                    di as res "`coeftype' `thislearner'`i':"
                    di as text "{hline `=`maxstrlen'-1'}{c TT}{hline 21}"
                    di as text "  Predictor  " _c
                    di as text _col(`maxstrlen') "{c |}      Value"
                    di as text "{hline `=`maxstrlen'-1'}{c +}{hline 21}"

                    forvalues j=1/`coef_nums' {
                        local thisxvar : word `j' of `thexvars'
                        di as text "  `thisxvar'" _c
                        di as text _col(`maxstrlen') "{c |}" _c
                        di as res %15.7f el(`coefs_mat',`j',1)
                    }
                }
                if "`e(type)'"=="class" & regexm("lassocv ridgecv elasticcv logit","`thislearner'") {
                    di "" _c
                    di as text "Note: " _c
                    di as text "Coefficients correspond to decision boundary function."
                }
            }
        }
    }

    // graph/table block
    if `"`graph'`graph1'`lgraph'`histogram'`table'"' ~= "" {
        pystacked_graph_table,                          ///
            `holdout1' holdout(`holdout')               ///
            `cvalid'                                    ///
            `graph1'                                    ///
            `histogram'                                 ///
            goptions(`graph') lgoptions(`lgraph')       ///
            table(`table')
    }

    // print RMSPE table if specified
    if "`table'"=="rmspe" {
        tempname m w
        mat `m' = r(m)

        di
        di as text "RMSPE: In-Sample, CV, Holdout"
        di as text "{hline 17}{c TT}{hline 47}"
        di as text "  Method" _c
        di as text _col(18) "{c |} Weight   In-Sample        CV         Holdout"
        di as text "{hline 17}{c +}{hline 47}"

        di as text "  STACKING" _c
        di as text _col(18) "{c |}" _c
        di as text "    .  " _c
        di as res  _col(30) %7.3f el(`m',1,1) _col(43) %7.3f el(`m',1,2) _col(56) %7.3f el(`m',1,3)

        forvalues j=1/`nlearners' {
            local b : word `j' of `base_est'
            di as text "  `b'" _c
            di as text _col(18) "{c |}" _c
            di as res _col(20) %5.3f el(`weights_mat',`j',1) _c
            di as res _col(30) %7.3f el(`m',`j'+1,1) _col(43) %7.3f el(`m',`j'+1,2) _col(56) %7.3f el(`m',`j'+1,3)
        }

        // add to estimation macros
        ereturn mat rmspe = `m'
    }

    // print confusion matrix if specified
    if "`table'"=="confusion" {
        tempname m w
        mat `m' = r(m)

        di
        di as text "Confusion matrix: In-Sample, CV, Holdout"
        di as text "{hline 17}{c TT}{hline 59}"
        di as text "  Method" _c
        di as text _col(18) "{c |} Weight      In-Sample             CV             Holdout"
        di as text _col(18) "{c |}             0       1         0       1         0       1"
        di as text "{hline 17}{c +}{hline 59}"

        di as text "  STACKING" _c
        di as text _col(16) "0 {c |}" _c
        di as text "    .  " _c
        di as res  _col(27) %7.0f el(`m',1,1) _col(35) %7.0f el(`m',1,2) _col(45) %7.0f el(`m',1,3) _col(53) %7.0f el(`m',1,4) _col(63) %7.0f el(`m',1,5) _col(71) %7.0f el(`m',1,6)
        di as text "  STACKING" _c
        di as text _col(16) "1 {c |}" _c
        di as text "    .  " _c
        di as res  _col(27) %7.0f el(`m',2,1) _col(35) %7.0f el(`m',2,2) _col(45) %7.0f el(`m',2,3) _col(53) %7.0f el(`m',2,4) _col(63) %7.0f el(`m',2,5) _col(71) %7.0f el(`m',2,6)

        forvalues j=1/`nlearners' {
            local b : word `j' of `base_est'
            di as text "  `b'" _c
            di as text _col(16) "0 {c |}" _c
            di as res  _col(20) %5.3f el(`weights_mat',`j',1) _c
            local r = 2*`j' + 1
            di as res  _col(27) %7.0f el(`m',`r',1) _col(35) %7.0f el(`m',`r',2) _col(45) %7.0f el(`m',`r',3) _col(53) %7.0f el(`m',`r',4) _col(63) %7.0f el(`m',`r',5) _col(71) %7.0f el(`m',`r',6)
            di as text "  `b'" _c
            di as text _col(16) "1 {c |}" _c
            di as res  _col(20) %5.3f el(`weights_mat',`j',1) _c
            local r = 2*`j' + 2
            di as res  _col(27) %7.0f el(`m',`r',1) _col(35) %7.0f el(`m',`r',2) _col(45) %7.0f el(`m',`r',3) _col(53) %7.0f el(`m',`r',4) _col(63) %7.0f el(`m',`r',5) _col(71) %7.0f el(`m',`r',6)
        }

        // add to estimation macros
        ereturn mat confusion = `m'
    }

    if "`cvc1'"~="" {
        // valid only for type=regression
        if "`e(type)'"~="reg" {
            di as err "error - CVC test available only for regression-type models"
            exit 198
        }
        tempname pmat
        mat `pmat'=e(cvc_p)
        di
        di as res `"CVC test ({browse "https://doi.org/10.1080/01621459.2019.1672556":Lei 2020}):"'
        di as res "H0: " as text "Learner has the lowest predictive risk among all candidate learners."
        di as res "HA: " as text "There is another learner with lower predictive risk."
        di as text "{hline 17}{c TT}{hline 21}"
        di as text "  Method" _c
        di as text _col(18) "{c |}      p-value"
        di as text "{hline 17}{c +}{hline 21}"

        forvalues j=1/`nlearners' {
            local b : word `j' of `base_est'
            di as text "  `b'" _c
            di as text _col(18) "{c |}" _c
            di as res %11.3f el(`pmat',`j',1)
        }
    }

end


// main program
program define _pystacked, eclass
version 16.0

    tokenize "`0'", parse(",")
    local beforecomma `1'
    macro shift
    local restargs `*'
    local 0 `beforecomma'
    syntax anything(name=beforeifinweight) [if] [in] [aweight fweight]
            if "`weight'"!="" {
                local ifinweight `if' `in' [`weight' `exp']
            }
            else {
                local ifinweight `if' `in' `weight' `exp'
            }
    tokenize `beforeifinweight', parse("|")
    local mainargs `1'
    local 0 `mainargs' `ifinweight' `restargs'
    local doublebarsyntax = ("`2'"=="|")*("`3'"=="|")
    if `doublebarsyntax'==0 {
        // required to allow for numbered options
        syntax varlist(min=1 fv ts) [if] [in] [aweight fweight] [, Methods(string) TYpe(string) *]
        // set default
        if ("`type'"=="") {
            local type reg
            local typeopt type(reg)
        }
        if ("`methods'"=="") {
            if (substr("`type'",1,5)=="class") {
                local methods logit lassocv gradboost
            }
            else {
                local methods ols lassocv gradboost
            }
            local methodsopt methods(`methods')
        }
        forv i = 1/`:list sizeof methods' {
            local numopts `numopts' cmdopt`i'(string asis) pipe`i'(string asis) xvars`i'(varlist fv ts)
        }
    }
    syntax varlist(min=1 fv ts) [if] [in] [aweight fweight], [*]
    local globalopt `options'
    syntax varlist(min=1 fv ts) [if] [in] [aweight fweight], ///
                [ ///
                    TYpe(string) /// classification or regression
                    FINALest(string) ///
                    NJobs(int 0) ///
                    CV ///
                    Folds(int 5) ///
                    FOLDVar(varname) ///
                    PREFit ///
                    BFolds(int 5) ///
                    NORANDOM ///
                    NOSHUFFLE ///
                    ///
                    ///
                    PYSeed(integer -1) ///
                    PRINTopt ///
                    NOSAVEPred ///
                    NOSAVETransform /// legacy option
                    NOSAVEBasexb /// equivalent to old NOSAVETransform
                    ///
                    VOTing ///
                    ///
                    VOTEType(string) ///
                    VOTEWeights(numlist >0) ///
                    debug ///
                    Methods(string) ///
                    `numopts' ///
                    ///
                    SHOWPymessages ///
                    backend(string) ///
                    CVC1                                    /// vanilla option, abbreviates to cvc; ignored here
                    cvc(integer 500)                        /// specifies number of bootstrap reps for cvc
                    altpython                               /// used for branching to pystacked1 or 2; saved as e(.) macro
                    ///
                    /// options for graphing etc; ignore here
                    GRAPH1                                  /// vanilla option, abbreviates to "graph"
                    HISTogram                               /// report histogram instead of default ROC
                    graph(string asis)                      /// for passing options to graph combine
                    lgraph(string asis)                     /// for passing options to the graphs of the learners
                    TABle                                   ///
                    HOLDOUT1                                /// vanilla option, abbreviates to "holdout"
                    holdout(varname)                        ///
                    CValid                                  ///
                    SHOWCoefs                               ///
                    SParse                                  ///
                    SHOWOPTions                             ///
                    NOESTIMATE                              /// suppress call to run_stacked; no estimates, only parses
                ]

    if `"`methods'"'=="" local methods `methods0'
    if "`printopt'"!="" {
        local noestimate noestimate
    }

    if ("`exp'"!="") {
        tempvar wvar
        local wvar_t = subinstr("`exp'","=","",.)
        local wvar_t = subinstr("`wvar_t'"," ","",.)
        gen `wvar'=`wvar_t'
    }

    * set defaults
    if "`type'"=="" local type reg
    if substr("`type'",1,3)=="reg" {
        local type reg
    }
    else if substr("`type'",1,5)=="class" {
        local type class
    }
    else {
        di as err "type(`type') not recognized"
        exit 198
    }
    if ("`methods'"=="") {
        if ("`type'"=="class") {
            local methods logit lassocv gradboost
        }
        else {
            local methods ols lassocv gradboost
        }
        local methodsopt methods(`methods')
    }

    * set the Python seed using randomly drawn number
    if `pyseed'<0 & "`noestimate'"=="" {
        local pyseed = round(runiform()*10^8)
    }

    * defaults
    if "`finalest'"=="" {
        local finalest nnls1
    }
    * legacy option
    if "`nosavetransform'"~="" {
        local nosavebasexb nosavebasexb
        local nosavetransform
    }

    if "`backend'"=="" local backend threading
    if "`backend'"!="loky"&"`backend'"!="multiprocessing"&"`backend'"!="threading" {
        di as err "backend not supported"
        exit 198
    }
    //local backend threading

    if "`votetype'"!="" {
        local voting voting
    }
    if "`voteweights'"!="" {
        local voting voting
    }
    if "`voting'"!="" {
        if "`votetype'"=="" {
            local votetype hard
        }
        else if "`votetype'"!="hard"&"`votetype'"!="soft" {
            di as error "votetype(`votetype') not allowed"
            error 198
        }
    }

    // get sklearn version
    python: from sklearn import __version__ as sklearn_version
    python: sfi.Macro.setLocal("sklearn_ver1",format(sklearn_version).split(".")[0])
    python: sfi.Macro.setLocal("sklearn_ver2",format(sklearn_version).split(".")[1])
    cap python: sfi.Macro.setLocal("sklearn_ver3",format(sklearn_version).split(".")[2])
    if ("`sklearn_ver3'"=="") local sklearn_ver3 = 0

    // mark sample
    marksample touse
    markout `touse' `varlist' `wvar'

    // generate fold var
    cap confirm variable `foldvar'
    if _rc>0 {
        // variable either doesn't exist or wasn't provided, so create tempvar fid
        *** gen folds
        tempvar uni cuni fid
        if "`norandom'"~="" | "`noestimate'"!="" {
            qui gen `uni' = _n
        }
        else {
            qui gen double `uni' = runiform() if `touse'
        }
        qui cumul `uni' if `touse', gen(`cuni')
        qui gen int `fid' = ceil(`folds'*`cuni') if `touse'
    }
    else {
        tempvar fid
        qui gen int `fid'=`foldvar'
        // provided foldvar may have missing values
        markout `touse' `foldvar'
    }

    qui count if `touse'
    local N        = r(N)
    // check if no observations
    if `N'==0 {
    	di as err "no observations"
    	exit 2000
    }
    tempvar id
    gen long `id'=_n
    local shuffle=("`noshuffle'"=="")

    ******** parse options using _pyparse.ado *********************************

    if `doublebarsyntax' {
        // Syntax 2
        syntax_parse `beforeifinweight' , type(`type') touse(`touse') sklearn1(`sklearn_ver1') sklearn2(`sklearn_ver2') sklearn3(`sklearn_ver3') `printopt'
        local allmethods `r(allmethods)'
        local allpyopt `r(allpyopt)'
        local mcount : word count `allmethods'
        local allpipe (
        forvalues i = 1(1)`mcount' {
            local opt`i' `r(opt`i')'
            local method`i' `r(method`i')'
            local pyopt`i' `r(pyopt`i')'
            local pipe`i' `r(pipe`i')'
            local xvars`i' `r(xvars`i')'
            local allpipe `allpipe' '`pipe`i''',
        }
        local allpipe `allpipe')
    }
    else {
        // Syntax 1
        local allmethods `methods'
        local mcount : word count `allmethods'
        local allpipe (
        forvalues i = 1(1)`mcount' {
            local method : word `i' of `allmethods'
            if "`method'"!="" {
                local mcount = `i'
                _pyparse , `cmdopt`i'' type(`type') method(`method') sklearn1(`sklearn_ver1') sklearn2(`sklearn_ver2') sklearn3(`sklearn_ver3') `printopt'
                if `i'==1 {
                    local allpyopt [`r(optstr)'
                }
                else {
                    local allpyopt `allpyopt' , `r(optstr)'
                }
                local opt`i' `cmdopt`i''
                local method`i' `method'
                local pyopt`i' `r(optstr)'
                ** if (no)stdscaler isn't included, add it at the end for linear estimators
                if strpos("`pipe`i''","stdscaler")==0 & strpos("lassoic lassocv ridgecv elasticcv logit","`method'")!=0 {
                    local pipe`i' `pipe`i'' stdscaler
                }
                ** remove 'nostdscaler'
                local pipe`i' = subinstr("`pipe`i''","nostdscaler","",.)
                if "`pipe`i''"=="" local pipe`i' passthrough
                local allpipe `allpipe' '`pipe`i''',
            }
        }
        local allpyopt `allpyopt']
        local allpipe `allpipe')
    }

    ******** dealing with varlists ********************************************

    * Split varlists called yvar and xvars
    ** xvars is the default predictor set.
    local yvar : word 1 of `varlist'
    local xvars: list varlist - yvar
    if ("`debug'"!="") {
        forvalues i = 1(1)`mcount' {
            di "xvars`i' = `xvars`i''"
        }
    }

    ** predictors
    local xvars_all  // expanded original vars for each learner (for info only)
    local xvars_all_t // Python list with temp vars for each learner
    local allxvars ( // Python list with expanded original vars for each learner (for info only)
    local allxvars_t (  // Python list with temp vars for each learner
    forvalues i = 1(1)`mcount' {
        ** if xvars() option is empty, use default list
        if "`xvars`i''"=="" {
            local xvars`i' `xvars'
        }
        local xvars_orig`i' `xvars`i''
        ** expand each varlist, and strip out variables with "o" and "b" prefix
        fvstrip `xvars`i'' if `touse', dropomit expand
        local xvars`i' `r(varlist)'
        local allxvars `allxvars' '`xvars`i''',
        local xvars_all `xvars_all' `xvars`i''
        ** create temp vars; loop through one-by-one so no zero vector temps are created
        local tlist
        foreach v in `xvars`i'' {
            fvrevar `v' if `touse'
            local tlist `tlist' `r(varlist)'
        }
        local xvars`i' `tlist'
        ** remove collinear predictors for OLS only;
        if "`method`i''"=="ols" {
            // mean-only model allowed if constant provided as separate predictor + nocons option
            local temp_opt : subinstr local opt`i' "nocons" "nocons", count(local has_nocons)
            if `has_nocons' {
                local nocons nocons
            }
            else {
                local nocons
            }
            _rmcoll `xvars`i'' if `touse', forcedrop `nocons'
            local xvars`i'  `r(varlist)'
        }
        local xvars_all_t `xvars_all_t' `xvars`i''
        local allxvars_t `allxvars_t' '`xvars`i''',
    }
    local allxvars `allxvars')
    local allxvars_t `allxvars_t')

    ** xvars_all_t is the unique list of all predictors; this will be passed to Python
    ** we use the union of all vars
    local xvars_all_t : list uniq xvars_all_t
    local xvars_all : list uniq xvars_all

    ** dependent variable (same procedure as above)
    fvstrip `yvar' if `touse', dropomit expand
    local yvar_t `r(varlist)'
    fvrevar `yvar_t' if `touse'
    local yvar_t `r(varlist)'

    if ("`debug'"!="") {
        di "Default predictors = `xvars'"
        di "All predictors = `xvars_all'"
        di "All predictors (temp) = `xvars_all_t'"
        di "Predictors for each learner = `allxvars'"
        di "Predictors for each learner (temp) = `allxvars_t'"
        forvalues i = 1(1)`mcount' {
            di "xvars`i' = `xvars`i''"
        }
        di "Summarize yvar_t and xvars_all_t:"
        sum `yvar_t' `xvars_all_t' if `touse'
    }

    ******** dealing with varlists END *****************************************

    // create esample variable for posting (disappears from memory after posting)
    tempvar esample
    qui gen byte `esample' = `touse'
    ereturn post, depname(`yvar') esample(`esample') obs(`N')

    if "`noestimate'"=="" {
        python: run_stacked( ///
                        "`type'",    ///
                        "`finalest'", ///
                        "`allmethods'", ///
                        "`yvar_t'", ///
                        "`xvars_all_t'", ///
                        "`wvar'", ///
                        "`training_var'", ///
                        ///
                        "`allpyopt'", ///
                        "`allpipe'", ///
                        "`allxvars_t'", ///
                        ///
                        "`touse'", ///
                        `pyseed', ///
                        "`nosavepred'", ///
                        "`nosavebasexb'", ///
                        "`voting'" , ///
                        "`votetype'", ///
                        "`voteweights'", ///
                        `njobs' , ///
                        "`fid'", ///
                        "`prefit'", ///
                        "`cv'", ///
                        `bfolds', ///
                        `shuffle', ///
                        "`id'", ///
                        `cvc', /// has number of bootstrap reps for cvc
                        "`showpymessages'", ///
                        "`backend'", ///
                        "`sparse'", ///
                        "`showoptions'" ///
                        )
    }
    ereturn local altpython  `altpython'
    ereturn local cmd        pystacked
    ereturn local predict    pystacked_p
    ereturn local depvar    `yvar'
    ereturn local type      `type'
    if "`voting'"~=""        ereturn local finalest voting
    else                    ereturn local finalest `finalest'

    forvalues i = 1(1)`mcount' {
        local opt`i' = stritrim("`opt`i''")
        ereturn local opt`i' `opt`i''
        ereturn local method`i' `method`i''
        ereturn local pyopt`i' `pyopt`i''
        ereturn local pipe`i' `pipe`i''
        ereturn local xvars`i' `xvars`i''
        ereturn local xvars_o`i' `xvars_orig`i''
    }
    ereturn scalar mcount = `mcount'
    ereturn scalar cvcbootnum = `cvc'
    ereturn local globalopt `globalopt'

    // if foldvar name was provided and variable exists, save name in e(foldvar)
    // if foldvar name was provided but variable doesn't exist, create it and save name in e(foldvar)
    // if foldvar name was not provided, create it and copy/overwrite to variable _pystacked_foldvar
    if "`foldvar'"=="" {
        // create or overwrite _pystacked_foldvar
        cap drop _pystacked_foldvar
        qui gen int _pystacked_foldvar = `fid'
        local foldvar _pystacked_foldvar
    }
    else {
        qui cap confirm variable `foldvar'
        if _rc>0 {
            // foldvar name provided but doesn't exist
            qui gen int `foldvar' = `fid'
        }
    }
    ereturn local foldvar `foldvar'

    // set data signature for pystacked_p
    local allxvars_o
    forvalues i=1/`mcount' {
        local allxvars_o `allxvars_o' `xvars_orig`i''
    }
    // keep only unique items
    local allxvars_o : list uniq allxvars_o
    ereturn local allxvars_o `allxvars_o'
    // get data signature based on depvar, xvars and foldvar
    fvrevar `yvar' `allxvars_o' `foldvar', list
    local dslist `r(varlist)'
    local dslist : list uniq dslist
    // qui _datasignature `yvar' `allxvars_o' `foldvar'
    qui _datasignature `dslist'
    ereturn local datasignature `r(datasignature)'
    // set sort info for pystacked_p
    local sortvars : sortedby
    ereturn local sortvars `sortvars'

end

// parses Syntax 2
program define syntax_parse, rclass

    syntax [anything(everything)] , type(string) touse(varname) sklearn1(real) sklearn2(real) sklearn3(real) `printopt'

    // save y x and if/in
    tokenize `anything', parse("|")
    local mainargs `1'

    // save method-specific parts
    local mcount = 0
    local j = 1
    while "``j''"!="" {
        local mcount = `mcount'+1
        local j = `j'+3
        local part`mcount' ``j''
    }
    local mcount = `mcount'-1

    // parse each part
    local allmethods
    forvalues i=1(1)`mcount' {
        local 0 ", `part`i''"
        syntax , [Method(string asis) OPTions(string asis) PIPEline(string asis) XVARs(varlist fv ts) ]
        local allmethods `allmethods' `method'
        return local method`i' `method'
        return local opt`i' `options'
        if strpos("`pipeline'","stdscaler")==0 & strpos("lassoic lassocv ridgecv elasticcv","`method'")!=0 {
            * stdscaler is added by default for linear regularized estimators
            local pipeline `pipeline' stdscaler
        }
        local pipeline = subinstr("`pipeline'","nostdscaler","",.)
        if "`pipeline'"=="" local pipeline passthrough
        _pyparse , `options' type(`type') method(`method') sklearn1(`sklearn1') sklearn2(`sklearn2') `printopt'
        return local pyopt`i' `r(optstr)'
        return local pipe`i' `pipeline'
        return local xvars`i' `xvars'
        if `i'==1 {
            local allpyopt [`r(optstr)'
        }
        else {
            local allpyopt `allpyopt' , `r(optstr)'
        }
    }
    local allpyopt `allpyopt']

    return local allmethods `allmethods'
    return scalar mcount = `mcount'
    return local mainargs `mainargs'
    return local restargs `restargs'
    return local allpyopt `allpyopt'

end

// graph and/or table
program define pystacked_graph_table, rclass
    version 16.0
    syntax ,                                ///
                [                           ///
                    HOLDOUT1                /// vanilla option, abbreviates to "holdout"
                    CValid                  ///
                    holdout(varname)        ///
                    GRAPH1                  /// vanilla option, abbreviates to "graph"
                    HISTogram               /// report histogram instead of default ROC
                    goptions(string asis)   ///
                    lgoptions(string asis)  ///
                    table(string)           ///
                ]

    // any graph options implies graph
    local graphflag = `"`graph1'`histogram'`goptions'`lgoptions'"'~=""

    // sample variable, holdout check, graph title
    if "`holdout'`holdout1'"=="" {
        if "`cvalid'"== "" {
            local title In-sample
        }
        else {
            local title In-sample (CV)
           }
        tempvar touse
        qui gen `touse' = e(sample)
    }
    else {
        local title Out-of-sample
        // holdout variable provided, or default = not-in-sample?
        if "`holdout'"=="" {
            // default
            tempvar touse
            qui gen `touse' = 1-e(sample)
            // check number of OOS obs
            qui count if `touse'
            if r(N)==0 {
                di as err "error - no observations in holdout sample"
                exit 198
            }
            di as text "Number of holdout observations:" as res %5.0f r(N)
        }
        else {
            // check that holdout variable doesn't overlap with e(sample)
            qui count if e(sample) & `holdout' > 0
            if r(N) > 0 {
                di as err "error - holdout and estimation samples overlap"
                exit 198
            }
            qui count if `holdout' > 0 & `holdout' < .
            if r(N) == 0 {
                di as err "error - no observations in holdout sample"
                exit 198
            }
            di as text "Number of holdout observations:" as res %5.0f r(N)
            local touse `holdout'
        }
    }

    local nlearners    = e(mcount)
    local learners    `e(base_est)'
    local y            `e(depvar)'
    // weights
    tempname weights
    mat `weights'    = e(weights)

    if "`e(type)'"=="reg" {
        // regression problem

        // complete graph title
        local title `title' Predictions

        tempvar stacking_p stacking_r stacking_p_cv stacking_r_cv stacking_rsq stacking_rsq_cv
        predict double `stacking_p'
        label var `stacking_p' "Prediction: Stacking Regressor"
        qui gen double `stacking_r' = `y' - `stacking_p'
        qui gen double `stacking_rsq' = (`stacking_r')^2
        qui predict double `stacking_p', basexb
        qui predict double `stacking_p_cv', basexb cv
        forvalues i=1/`nlearners' {
            local lname : word `i' of `learners'
            label var `stacking_p'`i' "Prediction: `lname'"
            label var `stacking_p_cv'`i' "Prediction (CV): `lname'"
            tempvar stacking_r`i' stacking_rsq`i'  stacking_r_cv`i' stacking_rsq_cv`i'
            qui gen double `stacking_r`i'' = `y' - `stacking_p'`i'
            qui gen double `stacking_rsq`i'' = (`stacking_r`i'')^2
            qui gen double `stacking_r_cv`i'' = `y' - `stacking_p_cv'`i'
            qui gen double `stacking_rsq_cv`i''=(`stacking_r_cv`i'')^2
        }
        // assemble stacked CV prediction
        qui gen double `stacking_p_cv'=0
        forvalues i=1/`nlearners' {
            qui replace `stacking_p_cv' = `stacking_p_cv' + `stacking_p_cv'`i' * `weights'[`i',1]
        }
        label var `stacking_p_cv' "Prediction (CV): Stacking Regressor"
        qui gen double `stacking_r_cv' = `y' - `stacking_p_cv'

        // graph variables
        if "`cvalid'"=="" {
            local xvar stacking_p
        }
        else {
            local xvar stacking_p_cv
        }
        tempname g0
        if `graphflag' {
            twoway (scatter ``xvar'' `y') (line `y' `y') if `touse'         ///
                ,                                                           ///
                legend(off)                                                 ///
                title("STACKING")                                           ///
                `lgoptions'                                                 ///
                nodraw                                                      ///
                name(`g0', replace)
            local glist `g0'
            forvalues i=1/`nlearners' {
                tempname g`i'
                local lname : word `i' of `learners'
                local w : di %5.3f el(`weights',`i',1)
                twoway (scatter `stacking_p'`i' `y') (line `y' `y') if `touse'      ///
                    ,                                                               ///
                    legend(off)                                                     ///
                    title("Learner: `lname'")                                       ///
                    `lgoptions'                                                     ///
                    subtitle("weight = `w'")                                        ///
                    nodraw                                                          ///
                    name(`g`i'', replace)
                local glist `glist' `g`i''
            }

            graph combine `glist'                                           ///
                            ,                                               ///
                            title("`title'")                                ///
                            `goptions'
        }
    }
    else {
        // classification problem

        tempvar stacking_p stacking_c stacking_p_cv stacking_c_cv stacking_r stacking_r_cv stacking_rsq  stacking_rsq_cv
        qui predict double `stacking_p', pr
        label var `stacking_p' "Predicted Probability: Stacking Regressor"
        qui gen double `stacking_r' = `y' - `stacking_p'
        qui gen double `stacking_rsq' = (`stacking_r')^2
        qui predict double `stacking_c', class
        label var `stacking_c' "Predicted Classification: Stacking Regressor"
        qui predict double `stacking_p', pr basexb
        qui predict double `stacking_c', class basexb
        qui predict double `stacking_p_cv', pr basexb cv
        qui predict double `stacking_c_cv', class basexb cv

        forvalues i=1/`nlearners' {
            local lname : word `i' of `learners'
            label var `stacking_p'`i' "Predicted Probability: `lname'"
            label var `stacking_c'`i' "Predicted Classification: `lname'"
            label var `stacking_p_cv'`i' "Predicted Probability (CV): `lname'"
            label var `stacking_c_cv'`i' "Predicted Classification (CV): `lname'"
            tempvar stacking_r`i' stacking_r_cv`i'
            tempvar stacking_rsq`i' stacking_rsq_cv`i'
            qui gen double `stacking_r`i'' = `y' - `stacking_p'`i'
            qui gen double `stacking_r_cv`i'' = `y' - `stacking_p_cv'`i'
            qui gen double `stacking_rsq`i'' = (`stacking_r`i'')^2
            qui gen double `stacking_rsq_cv`i'' = (`stacking_r_cv`i'')^2
       }
        // assemble stacked CV prediction
        qui gen double `stacking_p_cv'=0
        forvalues i=1/`nlearners' {
            qui replace `stacking_p_cv' = `stacking_p_cv' + `stacking_p_cv'`i' * `weights'[`i',1]
        }
        label var `stacking_p_cv' "Predicted Probability (CV): Stacking Regressor"
        qui gen double `stacking_r_cv' = `y' - `stacking_p_cv'
        qui gen byte `stacking_c_cv' = `stacking_p_cv' >= 0.5
        qui replace `stacking_c_cv' = . if `stacking_p_cv'==.

        if `graphflag' & "`histogram'"=="" {                            /// default is ROC
            // complete graph title
            local title `title' ROC

            // graph variables
            if "`cvalid'"=="" {
                local xvar stacking_p
            }
            else {
                local xvar stacking_p_cv
            }
            tempname g0
            roctab `y' ``xvar'',                                    ///
                graph                                               ///
                title("STACKING")                                   ///
                `lgoptions'                                         ///
                nodraw                                              ///
                name(`g0', replace)
            local glist `g0'
            forvalues i=1/`nlearners' {
                tempname g`i'
                local lname : word `i' of `learners'
                roctab `y' `stacking_p'`i',                         ///
                    graph                                           ///
                    title("Learner: `lname'")                       ///
                    `lgoptions'                                     ///
                    nodraw                                          ///
                    name(`g`i'', replace)
                local glist `glist' `g`i''
            }
            graph combine `glist'                                   ///
                            ,                                       ///
                            title("`title'")                        ///
                            `goptions'
        }
        else if "`histogram'"~="" {                                 /// histogram
            // complete graph title
            local title `title' predicted probabilities

            // user may have specified something other than freq
            local 0 , `lgoptions'
            syntax , [ DENsity FRACtion FREQuency percent * ]
            if "`density'`fraction'`frequency'`percent'"== "" {
                // default is frequency
                local ystyle freq
            }

            // graph variables
            if "`cvalid'"=="" {
                local xvar stacking_p
            }
            else {
                local xvar stacking_p_cv
            }
            tempname g0
            qui histogram `stacking_p',                                 ///
                title("STACKING")                                       ///
                `ystyle'                                                ///
                start(0)                                                ///
                `lgoptions'                                             ///
                nodraw                                                  ///
                name(`g0', replace)
            local glist `g0'
            forvalues i=1/`nlearners' {
                tempname g`i'
                local lname : word `i' of `learners'
                qui histogram `stacking_p'`i',                          ///
                    title("Learner: `lname'")                           ///
                    `ystyle'                                            ///
                    start(0)                                            ///
                    `lgoptions'                                         ///
                    nodraw                                              ///
                    name(`g`i'', replace)
                local glist `glist' `g`i''
            }
            graph combine `glist'                                       ///
                            ,                                           ///
                            title("`title'")                            ///
                            `goptions'
        }
    }

    // assemble table
    if "`table'"=="rmspe" {

        // save in matrix
        tempname m m_in m_cv m_out

        // column for in-sample RMSPE
        qui sum `stacking_rsq' if e(sample)
        mat `m_in' = sqrt(r(mean))
        forvalues i=1/`nlearners' {
            qui sum `stacking_rsq`i'' if e(sample)
            mat `m_in' = `m_in' \ (sqrt(r(mean)))
        }

        // column for in-sample RMSPE
        // we don't report RMSPE for composite CV prediction
        mat `m_cv' = .
        forvalues i=1/`nlearners' {
            qui sum `stacking_rsq_cv`i'' if e(sample)
            mat `m_cv' = `m_cv' \ (sqrt(r(mean)))
        }

        // column for OOS MSPE
        if "`holdout'`holdout1'"~="" {
            // touse is the holdout indicator
            qui sum `stacking_rsq' if `touse'
            mat `m_out' = sqrt(r(mean))
            forvalues i=1/`nlearners' {
                qui sum `stacking_rsq`i'' if `touse'
                mat `m_out' = `m_out' \ (sqrt(r(mean)))
            }
        }
        else {
            mat `m_out' = J(`nlearners'+1,1,.)
        }

        mat `m' = `m_in' , `m_cv', `m_out'
        mat colnames `m' = RMSPE_in RMSPE_cv RMSPE_out
        mat rownames `m' = STACKING `learners'

        return matrix m = `m'

    }
    else if "`table'"=="confusion" {

        // save in matrix
        tempname m mrow

        // stacking rows
        forvalues r=0/1 {
            qui count if `y'==0 & `stacking_c'==`r' & e(sample)
            local in_0    = r(N)
            qui count if `y'==1 & `stacking_c'==`r' & e(sample)
            local in_1    = r(N)
            // don't report for composite CV prediction
            // qui count if `y'==0 & `stacking_c_cv'==`r' & e(sample)
            // local cv_0    = r(N)
            // qui count if `y'==1 & `stacking_c_cv'==`r' & e(sample)
            // local cv_1    = r(N)
            local cv_0 = .
            local cv_1 = .
            if "`holdout'`holdout1'"~="" {
                // touse is the holdout indicator
                qui count if `y'==0 & `stacking_c'==`r' & `touse'
                local out_0    = r(N)
                qui count if `y'==1 & `stacking_c'==`r' & `touse'
                local out_1    = r(N)
            }
            else {
                local out_0 = .
                local out_1 = .
            }
            mat `mrow' = `in_0', `in_1', `cv_0', `cv_1', `out_0', `out_1'
            mat `m' = nullmat(`m') \ `mrow'
        }

        // base learner rows
        forvalues i=1/`nlearners' {

            forvalues r=0/1 {
                qui count if `y'==0 & `stacking_c'`i'==`r' & e(sample)
                local in_0    = r(N)
                qui count if `y'==1 & `stacking_c'`i'==`r' & e(sample)
                local in_1    = r(N)
                qui count if `y'==0 & `stacking_c_cv'`i'==`r' & e(sample)
                local cv_0    = r(N)
                qui count if `y'==1 & `stacking_c_cv'`i'==`r' & e(sample)
                local cv_1    = r(N)
                if "`holdout'`holdout1'"~="" {
                    // touse is the holdout indicator
                    qui count if `y'==0 & `stacking_c'`i'==`r' & `touse'
                    local out_0    = r(N)
                    qui count if `y'==1 & `stacking_c'`i'==`r' & `touse'
                    local out_1    = r(N)
                }
                else {
                    local out_0 = .
                    local out_1 = .
                }
                mat `mrow' = `in_0', `in_1', `cv_0', `cv_1', `out_0', `out_1'
                mat `m' = `m' \ `mrow'
            }
        }

        local rnames STACKING_0 STACKING_1
        forvalues i=1/`nlearners' {
            local lname : word `i' of `learners'
            local rnames `rnames' `lname'_0 `lname'_1
        }
        mat rownames `m' = `rnames'
        mat colnames `m' = in_0 cv_1 in_0 cv_1 out_0 out_1

        return matrix m = `m'

    }

end

// Internal version of matchnames
// Sample syntax:
// matchnames "`varlist'" "`list1'" "`list2'"
// takes list in `varlist', looks up in `list1', returns entries in `list2', called r(names)
program define matchnames, rclass
    version 11.2
    args    varnames namelist1 namelist2

    local k1 : word count `namelist1'
    local k2 : word count `namelist2'

    if `k1' ~= `k2' {
        di as err "namelist error"
        exit 198
    }
    foreach vn in `varnames' {
        local i : list posof `"`vn'"' in namelist1
        if `i' > 0 {
            local newname : word `i' of `namelist2'
        }
        else {
* Keep old name if not found in list
            local newname "`vn'"
        }
        local names "`names' `newname'"
    }
    local names    : list clean names
    return local names "`names'"
end

// internal version of fvstrip 1.01 ms 24march2015
// takes varlist with possible FVs and strips out b/n/o notation
// returns results in r(varnames)
// optionally also omits omittable FVs
// expand calls fvexpand either on full varlist
// or (with onebyone option) on elements of varlist
program define fvstrip, rclass
    version 11.2
    syntax [anything] [if] , [ dropomit expand onebyone NOIsily ]
    if "`expand'"~="" {                                             //  force call to fvexpand
        if "`onebyone'"=="" {
            fvexpand `anything' `if'                                //  single call to fvexpand
            local anything `r(varlist)'
        }
        else {
            foreach vn of local anything {
                fvexpand `vn' `if'                                  //  call fvexpand on items one-by-one
                local newlist    `newlist' `r(varlist)'
            }
            local anything    : list clean newlist
        }
    }
    foreach vn of local anything {                                  //  loop through varnames
        if "`dropomit'"~="" {                                       //  check & include only if
            _ms_parse_parts `vn'                                    //  not omitted (b. or o.)
            if ~`r(omit)' {
                local unstripped    `unstripped' `vn'               //  add to list only if not omitted
            }
        }
        else {                                                      //  add varname to list even if
            local unstripped        `unstripped' `vn'               //  could be omitted (b. or o.)
        }
    }
// Now create list with b/n/o stripped out
    foreach vn of local unstripped {
        local svn ""                                                //  initialize
        _ms_parse_parts `vn'
        if "`r(type)'"=="variable" & "`r(op)'"=="" {                //  simplest case - no change
            local svn    `vn'
        }
        else if "`r(type)'"=="variable" & "`r(op)'"=="o" {          //  next simplest case - o.varname => varname
            local svn    `r(name)'
        }
        else if "`r(type)'"=="variable" {                           //  has other operators so strip o but leave .
            local op    `r(op)'
            local op    : subinstr local op "o" "", all
            local svn    `op'.`r(name)'
        }
        else if "`r(type)'"=="factor" {                             //  simple factor variable
            local op    `r(op)'
            local op    : subinstr local op "b" "", all
            local op    : subinstr local op "n" "", all
            local op    : subinstr local op "o" "", all
            local svn    `op'.`r(name)'                            //  operator + . + varname
        }
        else if"`r(type)'"=="interaction" {                        //  multiple variables
            forvalues i=1/`r(k_names)' {
                local op    `r(op`i')'
                local op    : subinstr local op "b" "", all
                local op    : subinstr local op "n" "", all
                local op    : subinstr local op "o" "", all
                local opv    `op'.`r(name`i')'                     //  operator + . + varname
                if `i'==1 {
                    local svn    `opv'
                }
                else {
                    local svn    `svn'#`opv'
                }
            }
        }
        else if "`r(type)'"=="product" {
            di as err "fvstrip error - type=product for `vn'"
            exit 198
        }
        else if "`r(type)'"=="error" {
            di as err "fvstrip error - type=error for `vn'"
            exit 198
        }
        else {
            di as err "fvstrip error - unknown type for `vn'"
            exit 198
        }
        local stripped `stripped' `svn'
    }
    local stripped    : list retokenize stripped                   //  clean any extra spaces

    if "`noisily'"~="" {                                           //  for debugging etc.
di as result "`stripped'"
    }

    return local varlist    `stripped'                             //  return results in r(varlist)
end

cap program drop pystacked_check_python
program define pystacked_check_python
// code adapted from nwxtregress :)
    qui {
        cap python query
        if _rc == 0 {
            cap python which numpy
            local HasNumpy = _rc

            cap python which scipy
            local HasScipy = _rc

            cap python which sfi
            local HasSfi = _rc

            cap python which sklearn
            local HasSkl = _rc

            if `=`HasNumpy'+`HasSfi'+`HasScipy'+`HasSkl'' > 0 {
                noi disp as smcl "{cmd:pystacked} option {it:python} requires the following Python packages:"
                if `HasNumpy' != 0 noi disp "  numpy"
                if `HasScipy' != 0 noi disp "  scipy"
                if `HasSfi' != 0 noi disp "  sfi"
                if `HasSkl' != 0 noi disp "  sklearn"
                noi disp "Please install them before using the option {it:pystacked}."
            }
        *   exit
        }
        else {
            noi disp as error "Error loading Python."
            error 199
        }
    }

end

* --- Python code appended below ---
python:
#! pystacked v0.8.0
#! last edited: 14july2026
#! authors: aa/ms

# Import required Python modules
import sfi
import numpy as np
import __main__
import warnings
from sklearn.pipeline import make_pipeline,Pipeline
from sklearn.neural_network import MLPRegressor,MLPClassifier
from sklearn.preprocessing import StandardScaler,PolynomialFeatures,OneHotEncoder,MinMaxScaler
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer,KNNImputer
from sklearn.linear_model import LassoLarsIC,LassoCV,LogisticRegression,LogisticRegressionCV,LinearRegression
from sklearn.linear_model import RidgeCV,ElasticNetCV
from sklearn.ensemble import StackingRegressor,StackingClassifier
from sklearn.ensemble import VotingRegressor,VotingClassifier
from sklearn.ensemble import RandomForestRegressor,RandomForestClassifier
from sklearn.ensemble import GradientBoostingRegressor,GradientBoostingClassifier
from sklearn.base import TransformerMixin,BaseEstimator,ClassifierMixin,RegressorMixin
from sklearn.svm import LinearSVR,LinearSVC,SVC,SVR
from sklearn.utils import check_X_y,check_array
from sklearn.utils.validation import check_is_fitted
from scipy.sparse import coo_matrix,csr_matrix,issparse
from scipy.optimize import minimize
from scipy.optimize import nnls
from sklearn import __version__ as sklearn_version
from scipy import __version__ as scipy_version
from numpy import __version__ as numpy_version
from sys import version as sys_version
try:
    from sklearn.utils import parallel_backend
except ImportError:
    # from 1.7.0 onwards
    from joblib import parallel_backend
from sklearn.model_selection import PredefinedSplit,KFold,StratifiedKFold

### Define required Python functions/classes

class SingleBest(RegressorMixin,BaseEstimator):
    """
    Select base learner with lowest MSE
    """
    def fit(self, X, y):
        X, y = check_X_y(X, y, accept_sparse=True)
        self.is_fitted_ = True
        ncols = X.shape[1]
        best = 0
        lowest_mse = np.inf
        for i in range(ncols):
            this_mse=np.mean((y-X[:, i]) ** 2)
            if this_mse < lowest_mse:
                lowest_mse = this_mse
                best = i
        self.best = best
        coef = np.zeros(ncols)
        coef[best] = 1
        self.coef_ = coef
        self.cvalid=X
        return self

    def predict(self, X):
        X = check_array(X, accept_sparse=True)
        check_is_fitted(self, 'is_fitted_')
        return X[:,self.best]

class AvgEstimator(RegressorMixin,BaseEstimator):
    """
    Avg of learners
    """
    def fit(self, X, y):
        X, y = check_X_y(X, y, accept_sparse=True)
        self.is_fitted_ = True
        ncols = X.shape[1]
        self.coef_ = np.repeat(1/ncols,ncols)
        self.cvalid=X
        return self

    def predict(self, X):
        X = check_array(X, accept_sparse=True)
        check_is_fitted(self, 'is_fitted_')
        return X.mean(axis=1)

class ConstrLS(RegressorMixin,BaseEstimator):
    """
    Constrained least squares, weights sum to 1 and optionally >= 0
    """
    def __init__(self, unit_interval=True):
        self.unit_interval = unit_interval

    def fit(self, X, y):

        X,y = check_X_y(X,y, accept_sparse=True)
        xdim = X.shape[1]

        #Use nnls to get initial guess
        coef0, rnorm = nnls(X,y)

        #Define minimisation function
        def fn(coef, X, y):
            return np.linalg.norm(X.dot(coef) - y)

        #Constraints and bounds
        cons = {'type': 'eq', 'fun': lambda coef: np.sum(coef)-1}
        if self.unit_interval==True:
            bounds = [[0.0,1.0] for i in range(xdim)]
        else:
            bounds = None

        #Do minimisation
        fit = minimize(fn,coef0,args=(X, y),method='SLSQP',bounds=bounds,constraints=cons)
        self.coef_ = fit.x
        self.is_fitted_ = True
        self.cvalid=X
        return self

    def predict(self, X):
        X = check_array(X, accept_sparse=True)
        check_is_fitted(self, 'is_fitted_')
        return np.matmul(X,self.coef_)

class ConstrLSClassifier(ClassifierMixin,ConstrLS):
    def predict_proba(self, X):
        return self.predict(X)

class AvgClassifier(ClassifierMixin,AvgEstimator):
    def predict_proba(self, X):
        return self.predict(X)

class SingleBestClassifier(ClassifierMixin,SingleBest):
    def predict_proba(self, X):
        return self.predict(X)

class LinearRegressionClassifier(ClassifierMixin,LinearRegression):
    def predict_proba(self, X):
        self.cvalid=X
        return self.predict(X)

class LinearRegression2(LinearRegression):
    def fit(self,X,y):
        self.cvalid=X
        return LinearRegression.fit(self,X,y)

class RidgeCV2(RidgeCV):
    def fit(self,X,y):
        self.cvalid=X
        return RidgeCV.fit(self,X,y)

class SparseTransformer(TransformerMixin):
    def fit(self, X, y=None, **fit_params):
        return self
    def transform(self, X, y=None, **fit_params):
        return csr_matrix(X)

class DenseTransformer(TransformerMixin):
    def fit(self, X, y=None, **fit_params):
        return self
    def transform(self, X, y=None, **fit_params):
        return X.todense()

def get_index(lst, w):
    """
    return indexes of where elements in 'w' are stored in 'lst'
    """
    lst = lst.split(" ")
    w = w.split(" ")
    sel = []
    for i in range(len(w)):
        if w[i] in lst:
            ix = lst.index(w[i])
            sel.append(ix)
    return(sel)

def cvc(Yhat1, Yhat2, fid, bootnum=500, random_state=None):
    Yhat1 = np.asarray(Yhat1)
    if Yhat1.ndim == 1:
        Yhat1 = Yhat1[:, None]
    Yhat2 = np.asarray(Yhat2)
    fid = np.asarray(fid)

    N = Yhat1.shape[0]

    # step 1
    zeta = (Yhat1**2) - (Yhat2**2)

    # step 2: compute group means manually
    fid_uni = np.unique(fid)
    mu = np.zeros((len(fid_uni), zeta.shape[1]))
    for i, f in enumerate(fid_uni):
        mu[i, :] = zeta[fid == f, :].mean(axis=0)

    # step 3: group centering
    zeta_til = zeta.copy()
    for i, f in enumerate(fid_uni):
        zeta_til[fid == f, :] = zeta_til[fid == f, :] - mu[i, :]

    # step 4
    zeta_m = zeta.mean(axis=0)
    zeta_sd = zeta_til.std(axis=0, ddof=1)

    # step 5
    Tx = np.max(np.sqrt(N) * zeta_m / zeta_sd)

    # step 6
    rng = (random_state if isinstance(random_state, np.random.Generator)
           else np.random.default_rng(random_state))
    S = zeta_til / zeta_sd

    Txb = np.empty(bootnum)
    for b in range(bootnum):
        eps = rng.normal(size=N)
        boot_vec = (S * eps[:, None]).sum(axis=0) / np.sqrt(N)
        Txb[b] = np.max(boot_vec)

    # step 7
    Pval = np.mean(Txb > Tx)
    return Pval

def build_pipeline(pipes,xvars,xvar_sel):
    #
    #builds the pipeline for each base learner
    #pipes = string with pipes
    #xvars = string with all predictors (expanded original names)
    #xvar_sel = string with to-be-selected predictors
    #
    ll = []
    if xvar_sel!="":
        sel_ix = get_index(xvars,xvar_sel)
        ct = ColumnTransformer([("selector", "passthrough", sel_ix)],remainder="drop")
        ll.append(("selector",ct))
    pipes = pipes.split()
    for p in range(len(pipes)):
        if pipes[p]=="stdscaler":
            ll.append(('stdscaler',StandardScaler()))
        elif pipes[p]=="stdscaler0":
            ll.append(('stdscaler',StandardScaler(with_mean=False)))
        elif pipes[p]=="dense":
            ll.append(('dense',DenseTransformer()))
        elif pipes[p]=="sparse":
            ll.append(('sparse',SparseTransformer()))
        elif pipes[p]=="onehot":
            ll.append(('onehot',OneHotEncoder()))
        elif pipes[p]=="minmaxscaler":
            ll.append(('minmaxscaler',MinMaxScaler()))
        elif pipes[p]=="medianimputer":
            ll.append(('medianimputer',SimpleImputer(strategy='median')))
        elif pipes[p]=="knnimputer":
            ll.append(('knnimputer',KNNImputer()))
        elif pipes[p]=="poly2":
            ll.append(('poly2',PolynomialFeatures(degree=2,include_bias=False)))
        elif pipes[p]=="poly3":
            ll.append(('poly3',PolynomialFeatures(degree=3,include_bias=False)))
        elif pipes[p]=="interact":
            ll.append(('interact',PolynomialFeatures(include_bias=False,interaction_only=True)))
    return ll

def run_stacked(type, # regression or classification
    finalest, # final estimator
    methods, # list of base learners
    yvar, # outcome
    xvars, # predictors (temp names)
    wvar, # weight var
    training, # marks holdout sample
    allopt, # options for each learner
    allpipe, # pipes for each learner
    allxvar_sel, # subset predictors for each learner (expanded var names)
    touse, # sample
    seed, # seed
    nosavepred,nosavebasexb, # store predictions
    voting,votetype,voteweights, # voting
    njobs, # number of cores
    foldvar, # foldvar
    prefit, # don't do CV
    cv, # also do CV if there is only one learner
    bfolds, #
    shuff, #
    idvar, # id var
    cvcbootnum, # number of bootstrap reps for cvc
    showpywarnings, # show warnings?
    parbackend, # backend
    sparse, # sparse predictor
    showopt #
    ):

    if int(format(sklearn_version).split(".")[1])<24 and int(format(sklearn_version).split(".")[0])<1:
        sfi.SFIToolkit.stata('di as err "pystacked requires sklearn 0.24.0 or higher. Please update sklearn."')
        sfi.SFIToolkit.stata('di as err "See instructions on https://scikit-learn.org/stable/install.html, and in the help file."')
        sfi.SFIToolkit.error(198)

    # Set random seed
    if seed>0:
        rng=np.random.RandomState(seed)
    else:
        rng=None

    if showpywarnings=="":
        warnings.filterwarnings('ignore')
    else:
        warnings.filterwarnings('default')

    if njobs==0:
        nj = None
    else:
        nj = njobs

    ##############################################################
    ### load data                                              ###
    ##############################################################

    y = np.array(sfi.Data.get(yvar,selectvar=touse))
    x = np.array(sfi.Data.get(xvars,selectvar=touse))
    id = np.array(sfi.Data.get(idvar,selectvar=touse))
    if wvar!="":
        weights = np.array(sfi.Data.get(wvar,selectvar=touse))
    else:
        weights =None

    #id = np.reshape(id,(-1,1))
    id.astype(int)
    fid = np.array(sfi.Data.get(foldvar,selectvar=touse))
    # If missings are present, need to specify they are NaNs.
    x_0 = np.array(sfi.Data.get(xvars,missingval=np.nan))
    if x.ndim == 1:
        x=np.reshape(x,(-1,1))
    if x_0.ndim == 1:
        x_0 = np.reshape(x_0,(-1,1))
    if sparse!="":
        x = coo_matrix(x).tocsc()

    shuff = shuff==1
    if shuff:
        cvrng=rng
    else:
        cvrng=None

    if prefit!="":
        ccv=prefit
    else:
        ccv=PredefinedSplit(fid)

    ##############################################################
    ### prepare fit                                            ###
    ##############################################################

    # convert strings to python objects
    methods = methods.split()
    if "xgb" in methods:
        import xgboost as xgb
    allopt = eval(allopt)
    allpipe = eval(allpipe)
    allxvar_sel = eval(allxvar_sel)

    # print options:
    if showopt!="":
        sfi.SFIToolkit.displayln("")
        for m in range(len(methods)):
            opt = allopt[m]
            sfi.SFIToolkit.displayln("Base learner: "+methods[m])
            for i in opt:
                subopt_name = opt[i]
                sfi.SFIToolkit.stata('di as text "'+str(i)+' = " _c')
                sfi.SFIToolkit.stata('di as text "'+str(opt[i])+'; " _c')
            sfi.SFIToolkit.displayln("")
            sfi.SFIToolkit.displayln("")

    est_list = []
    for m in range(len(methods)):
        if type=="reg":
            if methods[m]=="ols":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('ols',LinearRegression2(**opt)))
            if methods[m]=="lassoic":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('lassoic',LassoLarsIC(**opt)))
            if methods[m]=="lassocv":
                opt =allopt[m]
                newmethod= build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('lassocv',ElasticNetCV(**opt,cv=KFold(n_splits=bfolds,shuffle=shuff,random_state=cvrng))))
            if methods[m]=="ridgecv":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('ridgecv',ElasticNetCV(**opt,cv=KFold(n_splits=bfolds,shuffle=shuff,random_state=cvrng))))
            if methods[m]=="elasticcv":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('elasticcv',ElasticNetCV(**opt,cv=KFold(n_splits=bfolds,shuffle=shuff,random_state=cvrng))))
            if methods[m]=="rf":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('rf',RandomForestRegressor(**opt)))
            if methods[m]=="gradboost":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('gradboost',GradientBoostingRegressor(**opt)))
            if methods[m]=="svm":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('svm', SVR(**opt)))
            if methods[m]=="linsvm":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('linsvm',LinearSVR(**opt)))
            if methods[m]=="nnet":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('nnet',MLPRegressor(**opt)))
            if methods[m]=="xgb":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('xgb',xgb.XGBRegressor(**opt)))
        elif type=="class":
            if methods[m]=="logit":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('logit',LogisticRegression(**opt)))
            if methods[m]=="lassoic":
                sfi.SFIToolkit.stata("di as err lassoic not supported with type(class)")
                sfi.SFIToolkit.error()
            if methods[m]=="lassocv":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('lassocv',LogisticRegressionCV(**opt,cv=StratifiedKFold(n_splits=bfolds,shuffle=shuff,random_state=cvrng))))
            if methods[m]=="ridgecv":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('ridgecv',LogisticRegressionCV(**opt,cv=StratifiedKFold(n_splits=bfolds,shuffle=shuff,random_state=cvrng))))
            if methods[m]=="elasticcv":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('elasticcv',LogisticRegressionCV(**opt,cv=StratifiedKFold(n_splits=bfolds,shuffle=shuff,random_state=cvrng))))
            if methods[m]=="rf":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('rf',RandomForestClassifier(**opt)))
            if methods[m]=="gradboost":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('gradboost',GradientBoostingClassifier(**opt)))
            if methods[m]=="svm":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('svm',SVC(**opt)))
            if methods[m]=="linsvm":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('linsvm',LinearSVC(**opt)))
            if methods[m]=="nnet":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('nnet',MLPClassifier(**opt)))
            if methods[m]=="xgb":
                opt =allopt[m]
                newmethod = build_pipeline(allpipe[m],xvars,allxvar_sel[m])
                newmethod.append(('xgb',xgb.XGBClassifier(**opt)))
        else:
            sfi.SFIToolkit.stata('di as err "method not known"')
            #"
            sfi.SFIToolkit.error()
        if prefit=="":
            est_list.append((methods[m]+str(m),Pipeline(newmethod)))
        else:
            est_list.append((methods[m]+str(m),Pipeline(newmethod).fit(x,y)))

    if finalest == "nnls0" and type == "class":
        fin_est = LinearRegressionClassifier(fit_intercept=False,positive=True)
    elif finalest == "nnls_sk" and type == "class":
        fin_est = LinearRegressionClassifier(fit_intercept=False,positive=True)
    elif finalest == "nnls1" and type == "class":
        fin_est = ConstrLSClassifier()
    elif finalest == "ridge" and type == "class":
        fin_est = LogisticRegression()
    elif finalest == "nnls0" and type == "reg":
        fin_est = LinearRegression2(fit_intercept=False,positive=True)
    elif finalest == "nnls_sk" and type == "reg":
        fin_est = LinearRegression2(fit_intercept=False,positive=True)
    elif finalest == "nnls1" and type == "reg":
        fin_est = ConstrLS()
    elif finalest == "ridge" and type == "reg":
        fin_est = RidgeCV2()
    elif finalest == "avg" and type == "reg":
        fin_est = AvgEstimator()
    elif finalest == "avg" and type == "class":
        fin_est = AvgClassifier()
    elif finalest == "singlebest" and type == "reg":
        fin_est = SingleBest()
    elif finalest == "singlebest" and type == "class":
        fin_est = SingleBestClassifier()
    elif finalest == "ols" and type == "class":
        fin_est = LinearRegressionClassifier()
    elif finalest == "ols" and type == "reg":
        fin_est = LinearRegression2()
    elif finalest == "ls1" and type == "reg":
        fin_est = ConstrLS(unit_interval=False)
    elif finalest == "ls1" and type == "class":
        fin_est = ConstrLSClassifier(unit_interval=False)
    else:
        sfi.SFIToolkit.stata('di as err "specified final estimator not supported"')
        #"
        sfi.SFIToolkit.error(198)

    # if single base learner, use voting with weight = 1
    if len(methods)==1 and cv=="":
        voting="voting"
        voteweights=""
        votetype="soft"
        sfi.SFIToolkit.stata('di as text "Single base learner: no stacking or cross-validation done."')
        #"

    if voting=="" and type=="reg":
        model = StackingRegressor(
                       estimators=est_list,
                       final_estimator=fin_est,
                       n_jobs=nj,
                       cv=ccv
                )
    elif voting=="" and type=="class":
        model = StackingClassifier(
                       estimators=est_list,
                       final_estimator=fin_est,
                       n_jobs=nj,
                       cv=ccv
                )
    elif voting!="":
        if voteweights!="":
            vweights = voteweights.split(" ")
            if len(vweights)!=len(methods)-1:
                sfi.SFIToolkit.stata('di as err "numlist in voteweights should be number of base learners - 1"')
                #"
                sfi.SFIToolkit.error(198)
            vweights = [float(i) for i in vweights]
            vw_sum = sum(vweights)
            if vw_sum>1:
                sfi.SFIToolkit.stata('di as err "sum of voting weights larger than 1."')
                #"
                sfi.SFIToolkit.error(198)
            vweights.append(1-vw_sum)
        else:
            vweights=None
        if type=="reg":
            model = VotingRegressor(
                           estimators=est_list,
                           n_jobs=nj,
                           weights=vweights
                    )
        else:
            model = VotingClassifier(
                           estimators=est_list,
                           n_jobs=nj,
                           voting=votetype,
                           weights=vweights
                    )

    ##############################################################
    ### fitting; save predictions in __main__                  ###
    ##############################################################

    # Train model on training data
    if type=="class":
        y=y!=0
    with parallel_backend(parbackend):
        model = model.fit(x,y)

    # store weights in e()
    if voting=="":
        w = model.final_estimator_.coef_
        if len(w.shape)==1:
            #w=w/sum(w)
            sfi.Matrix.store("e(weights)",w)
        else:
            sfi.Matrix.store("e(weights)",w[0])
    elif vweights!=None:
        w = np.array(vweights)
        sfi.Matrix.store("e(weights)",w)
    elif votetype=="hard" and type=="class":
        w = np.repeat(np.nan,len(methods))
    else:
        w = np.array([1/len(methods)]*len(methods))
        sfi.Matrix.store("e(weights)",w)

    for i in range(len(methods)):
        method0=methods[i]+str(i)
        coefmatname = "e(coefs"+str(i+1)+")"
        if methods[i] in ['ols','lassocv','lassoic','logit','ridgecv','elasticcv']:
            w = model.named_estimators_[method0].named_steps[methods[i]].coef_
            has_intercept=model.named_estimators_[method0].named_steps[methods[i]].fit_intercept
            if has_intercept:
                w0 = model.named_estimators_[method0].named_steps[methods[i]].intercept_
                w=np.append(w,w0)
                sfi.Scalar.setValue("e(has_intercept"+str(i+1)+")",1,vtype='hidden')
            else:
                sfi.Scalar.setValue("e(has_intercept"+str(i+1)+")",0,vtype='hidden')
            sfi.Matrix.store(coefmatname,w)
            sfi.Scalar.setValue("e(has_coefs"+str(i+1)+")",1,vtype='hidden')
        elif methods[i] in ['rf','gradboost']:
            w = model.named_estimators_[method0].named_steps[methods[i]].feature_importances_
            sfi.Matrix.store(coefmatname,w)
            sfi.Scalar.setValue("e(has_intercept"+str(i+1)+")",0,vtype='hidden')
            sfi.Scalar.setValue("e(has_coefs"+str(i+1)+")",1,vtype='hidden')
        else:
            sfi.Scalar.setValue("e(has_coefs"+str(i+1)+")",0,vtype='hidden')

    # save candidate learners
    sfi.Macro.setGlobal("e(base_est)"," ".join(methods))

    __main__.type = type
    __main__.id = id

    if nosavepred=="" or nosavebasexb=="":
        # Track NaNs
        x0_hasnan = np.isnan(x_0).any(axis=1)
        # Set any NaNs to zeros so that model.predict(.) won't crash
        x_0 = np.nan_to_num(x_0)

    if nosavepred == "" or nosavebasexb == "":
        __main__.model_object = model
        __main__.model_xvars = xvars
        __main__.model_methods = methods

    if nosavepred == "" and type =="class":
        try:
            pred = model.predict_proba(x_0)>0.5
            pred_proba = model.predict_proba(x_0)
        except AttributeError:
            pred = model.predict(x_0)
            pred_proba = np.repeat(np.nan,len(pred))
        # Set any predictions that should be missing to missing (NaN)
        pred_proba=pred_proba.astype(np.float32)
        pred_proba[x0_hasnan] = np.nan
        pred=pred.astype(np.float32)
        pred[x0_hasnan] = np.nan
        __main__.predict = pred
        __main__.predict_proba = pred_proba

    if nosavepred == "" and type =="reg":
        pred = model.predict(x_0)
        # Set any predictions that should be missing to missing (NaN)
        pred=pred.astype(np.float32)
        pred[x0_hasnan] = np.nan
        __main__.predict = pred

    if nosavebasexb == "" and (votetype!="hard" or type=="reg"):
        transf = model.transform(x_0)
        nmethods = len(methods)
        ntransf = transf.shape[1]
        if type=="class" and (2*nmethods==ntransf):
            # only use every second column since predicted values for both 0 and 1 are reported
            ncol = transf.shape[1]
            cols=np.linspace(start=1, stop=ncol-1, num=int(ncol/2)).astype(int)
            transf=transf[:,cols]
        elif type=="class" and (nmethods!=ntransf):
            sfi.SFIToolkit.stata('di as err "Internal error. Failed to save base learner predicted probabilities."')
            #"
            sfi.SFIToolkit.error(198)
        # Set any predictions that should be missing to missing (NaN)
        transf=transf.astype(np.float32)
        transf[x0_hasnan] = np.nan
        __main__.transform = transf
        try:
            __main__.cvalid = model.final_estimator_.cvalid
        except AttributeError:
            # values for cvalid unavailable so return array of correct dimension with all NaNs
            cv0 = x.shape[0]
            cv1 = transf.shape[1]
            __main__.cvalid = np.empty((cv0,cv1))*np.nan

    # cvc
    if (type=="reg") and (len(methods)>1):
        nobs = len(y)
        residuals = y[...,None] - __main__.cvalid
        fid_list = np.unique(fid)
        cvc_p = [0]*len(methods)
        for i in range(len(methods)):
            yhat1 = residuals[:,i]
            yhat2 = np.delete(residuals,i,axis=1)
            pval = cvc(yhat1,yhat2,fid,random_state=seed)
            cvc_p[i] = pval
        sfi.Matrix.store("e(cvc_p)",cvc_p)

    # save versions of Python and packages
    sfi.Macro.setGlobal("e(sklearn_ver)",format(sklearn_version))
    sfi.Macro.setGlobal("e(numpy_ver)",format(numpy_version))
    sfi.Macro.setGlobal("e(scipy_ver)",format(scipy_version))
    sfi.Macro.setGlobal("e(python_ver)",format(sys_version))
end