********************************************************************************
*! "opl_budget", v.2, GCerulli, 11/12/2025
********************************************************************************

program define opl_budget, rclass
    version 17.0
    /*
        Optimal Policy Learning with Budget Constraint
        Binary treatment assignment (0/1), selecting individuals to treat
        under two constraints:
            (i)  total treatment cost <= budget C
            (ii) number of treated individuals >= N*

        Syntax:
            opl_budget tauvar costvar, budget(#) nmin(#) em0(#)
                [ policy(name) replace custom_pol(varname) ]

        Inputs:
            tauvar   = estimated individual treatment effects (τ̂_i)
            costvar  = individual treatment costs (c_i)

        Required options:
            budget(#)  = total available budget C
            nmin(#)    = minimum number of treated individuals N*
            em0(#)     = baseline welfare E[m0(X)]

        Optional:
            policy(name)    = name of generated optimal policy variable (default: opl_policy)
            replace         = allow overwriting existing policy variable
            custom_pol(var) = name of an existing 0/1 policy variable defined by the user,
                              which will be evaluated in terms of welfare and coverage
    */

    //----------------------------------------------------------------------
    // 1. Parse arguments
    //----------------------------------------------------------------------
    syntax varlist(min=2 max=2 numeric) , ///
        BUDget(real) ///
        NMin(integer) ///
        EM0(real) ///
        [ POLicy(name) REPLACE CUSTOM_pol(varname) ]

    tokenize `varlist'
    local tauvar  `1'     // variable storing τ̂_i
    local costvar `2'     // variable storing c_i

	tempvar mysample
	gen `mysample' = !missing(`tauvar' , `costvar')
	qui count if `mysample'==1
	local N_used = r(N)

    // Default name for policy variable if not provided
    if ("`policy'" == "") local policy "opl_policy"

    //----------------------------------------------------------------------
    // 2. Basic dataset checks
    //----------------------------------------------------------------------
    quietly count
    local N = r(N)

    if `N' == 0 {
        di as err "dataset is empty"
        exit 200
    }

    if `nmin' < 1 {
        di as err "nmin() must be >= 1"
        exit 198
    }

    if `nmin' > `N' {
        di as err "nmin() exceeds the number of observations"
        exit 198
    }

    if `budget' <= 0 {
        di as err "budget() must be strictly positive"
        exit 198
    }

    // Costs must be positive and nonmissing
    capture assert `costvar' > 0 & !missing(`costvar')
    if _rc {
        di as err "cost variable must be strictly positive and non-missing"
        exit 459
    }

    //----------------------------------------------------------------------
    // 3. Handling existing policy variable (optimal policy)
    //----------------------------------------------------------------------
    capture confirm variable `policy'
    if !_rc {
        if ("`replace'" == "") {
            di as err "variable `policy' already exists. Use option replace."
            exit 110
        }
        else {
            drop `policy'
        }
    }

    //----------------------------------------------------------------------
    // 4. Store original order
    //----------------------------------------------------------------------
    tempvar origorder
    gen long `origorder' = _n     // preserve dataset order for final sorting

    //----------------------------------------------------------------------
    // 5. Compute cost-effectiveness score s_i = τ̂_i / c_i
    //----------------------------------------------------------------------
    tempvar score
    gen double `score' = `tauvar' / `costvar'

    //----------------------------------------------------------------------
    // 6. Sort by score in descending order
    //----------------------------------------------------------------------
    gsort - `score'

    //----------------------------------------------------------------------
    // 7. Ranking and cumulative sums
    //----------------------------------------------------------------------
    tempvar rank cumtau cumcost feas

    gen long   `rank'    = _n                     // ranking index after sorting
    gen double `cumtau'  = sum(`tauvar')          // cumulative τ̂
    gen double `cumcost' = sum(`costvar')         // cumulative cost

    // feasible solutions: must meet both constraints:
    // (i) rank >= nmin   -> at least N* units selected
    // (ii) cumcost <= budget
    gen byte `feas' = (`rank' >= `nmin' & `cumcost' <= `budget')

    //----------------------------------------------------------------------
    // 8. Check feasibility
    //----------------------------------------------------------------------
    quietly count if `feas'
    if r(N) == 0 {
        di as err "no feasible solution: budget too small to treat at least N* individuals"
        sort `origorder'
        drop `origorder' `rank' `cumtau' `cumcost' `feas' `score'
        exit 498
    }

    //----------------------------------------------------------------------
    // 9. Identify k* maximizing cumulative tau among feasible k
    //----------------------------------------------------------------------
    quietly summarize `cumtau' if `feas', meanonly
    scalar besttau = r(max)

    quietly summarize `rank' if `feas' & `cumtau' == besttau, meanonly
    local kstar = r(min)      // choose smallest rank achieving max welfare

    //----------------------------------------------------------------------
    // 10. Generate optimal policy: treat if rank <= k*
    //----------------------------------------------------------------------
    gen byte `policy' = (`rank' <= `kstar')

    //----------------------------------------------------------------------
    // 11. Compute total welfare and total cost of selected (optimal) policy
    //----------------------------------------------------------------------

	quietly count
    local N = r(N)

	quietly count if `policy'
    local Ntreated = r(N)

	quietly sum `tauvar' if `policy'
	local ATET = r(mean)

	local TTET = `ATET'*`Ntreated'

	local averageWelfare = `em0' + `ATET' * `Ntreated'/`N'

	local totalWelfare = `averageWelfare' * `N'

    quietly summarize `costvar' if `policy', meanonly
    local totalCost = r(sum)

	local averageCost = `totalCost'/`Ntreated'

	local pnr = `TTET' - `totalCost' // policy net return

	local pnr_per_treat = `pnr'/`Ntreated' // policy net return per treated

    //----------------------------------------------------------------------
    // 12. Restore original dataset order
    //----------------------------------------------------------------------
    sort `origorder'
    drop `origorder' `rank' `cumtau' `cumcost' `feas' `score'

    //----------------------------------------------------------------------
    // 13. Return results for optimal policy to r()
    //----------------------------------------------------------------------
    return scalar budget        = `budget'
    return scalar kstar         = `kstar'
    return scalar AW            = `averageWelfare'
    return scalar TW            = `totalWelfare'
    return scalar AC            = `averageCost'
    return scalar TC            = `totalCost'
	return scalar AI            = `ATET' // average impact
	return scalar TI            = `TTET' // total impact
	return scalar PNR           = `pnr' // policy net return
	return scalar PNRT          = `pnr_per_treat' // policy net return per treated
    return scalar Ntreated      = `Ntreated'
    return scalar Ptreated      = `Ntreated' / `N' * 100

    //----------------------------------------------------------------------
    // 14. Evaluate custom policy (if requested)
    //----------------------------------------------------------------------
    if ("`custom_pol'" != "") {

        // Check that custom_pol variable exists
        capture confirm variable `custom_pol'
        if _rc {
            di as err "custom_pol(`custom_pol') not found in the dataset"
            exit 111
        }

        // Check custom_pol is 0/1 and non-missing
        capture assert inlist(`custom_pol',0,1) & !missing(`custom_pol')
        if _rc {
            di as err "custom_pol() must be a 0/1, non-missing variable"
            exit 459
        }


        // Number treated under custom policy
        quietly count if `custom_pol' == 1
        local Ntreated_c = r(N)

		quietly sum `tauvar' if `custom_pol' == 1
		local ATET_c = r(mean)

		local TTET_c = `ATET_c'*`Ntreated_c'

		local averageWelfare_c = `em0' + `ATET_c' * `Ntreated_c'/`N'

		local totalWelfare_c = `averageWelfare_c' * `N'

		quietly summarize `costvar' if `custom_pol' == 1, meanonly
		local totalCost_c = r(sum)

		local averageCost_c = `totalCost_c'/`Ntreated_c'

		local pnr_c = `TTET_c' - `totalCost_c' // policy net return

		local pnr_per_treat_c = `pnr_c'/`Ntreated_c' // policy net return per treated

        // Return in r()

		return scalar N               = `N'
		return scalar N_used          = `N_used'
		return scalar AW_c            = `averageWelfare_c'
		return scalar TW_c            = `totalWelfare_c'
		return scalar AC_c            = `averageCost_c'
		return scalar TC_c            = `totalCost_c'
		return scalar AI_c            = `ATET_c' // average impact
		return scalar TI_c            = `TTET_c' // total impact
		return scalar PNR_c           = `pnr_c'  // policy net return
	    return scalar PNRT_c          = `pnr_per_treat_c' // policy net return per treated
		return scalar Ntreated_c      = `Ntreated_c'
		return scalar Ptreated_c      = `Ntreated_c' / `N' * 100
		return scalar NMin            = `nmin'
    }

    //----------------------------------------------------------------------
    // 15. Summary tables
    //----------------------------------------------------------------------
                di ""
		di "{hline 60}"
		di "                  - TABLE OF RESULTS -                 "
		di "              {it:OPL WITH BUDGET CONSTRAIN} "
		di "             {it:AND MINIMUM NUMBER OF TREATED} "
		di "{hline 60}"
		di "                    - General Info -   "
		di "{hline 60}"
		di "  Number of obs                     " %12.0f return(N)
		di "  Number of used obs                " %12.0f return(N_used)
		di "  Budget                            " %12.0f return(budget)
		di "  Minimum N. of requested treated   " %12.0f return(NMin)
		di "{hline 60}"
		di "                   - Optimal Policy - "
		di "{hline 60}"
		di "  Parameter                               Value"
		di "{hline 60}"
		di "  Optimal number of treated         " %12.0f return(Ntreated)
		di "  Percentage of treated             " %12.0f return(Ptreated)
		di "  Total cost (TC)                   " %12.4f return(TC)
		di "  Average cost per treated (AC)     " %12.4f return(AC)
		di "  Total welfare (TW)                " %12.4f return(TW)
		di "  Average welfare (AW)              " %12.4f return(AW)
		di "  Average impact (ATET)             " %12.4f return(AI)
		di "  Total impact (TTET)               " %12.4f return(TI)
		di "  Policy net return (PNR)           " %12.4f return(PNR)
		di "  PNR per treated (PNRT)            " %12.4f return(PNRT)
		di "  Policy variable created           " %12s   "`policy'"
		di "{hline 60}"
		di "  Note: PNR = TTET-TC; PNRT = PNR/N1"
		di "{hline 60}"


	if ("`custom_pol'" != "") {
		di " "
		di "{hline 60}"
		di "                   - Custom Policy - "
		di "{hline 60}"
		di "  Parameter                               Value"
		di "{hline 60}"
		di "  Number of treated                  " %12.0f return(Ntreated_c)
		di "  Percentage of treated              " %12.0f return(Ptreated_c)
		di "  Total cost (TC_c)                  " %12.4f return(TC_c)
		di "  Average cost per treated (AC_c)    " %12.4f return(AC_c)
		di "  Total welfare (TW_c)               " %12.4f return(TW_c)
		di "  Average welfare (AW_c)             " %12.4f return(AW_c)
		di "  Average impact (ATET_c)            " %12.4f return(AI_c)
		di "  Total impact (TTET_c)              " %12.4f return(TI_c)
		di "  Policy net return (PNR_c)          " %12.4f return(PNR_c)
		di "  PNR_c per treated (PNRT_c)         " %12.4f return(PNRT_c)
		di "  Policy variable created            " %12s   "`policy'"
		di "{hline 60}"
		di "{hline 60}"
		di "  Note: PNR_c = TTET_c-TC_c; PNRTc= PNR_c/N1_c"
		di "{hline 60}"
	}
    di " "
end
********************************************************************************