*! 1.0.0 Ariel Linden 16Sep2024

program define csum, rclass

	version 11.0

	/* obtain settings */
	syntax varlist(min=2 max=2 numeric) [if] [in] , ///
	BLProb(numlist >0 <1 max=1)						///
	[ ODDS(real 2)									///
	LIMit(numlist max=1)							///
	Reps(integer 50)				                ///
	SEED(string) 									///
	CENtile(real 95)								///
	Wt(varlist min=1 max=1)							///
	REPLace											///
	NOgraph	*										///
	]

		marksample touse
		qui count if `touse'
		if r(N) == 0 error 2000
		local N = r(N)
		qui replace `touse' = -`touse'

        // parse yvar and xvar
		tokenize `varlist'
		local yvar `1'
		local xvar `2'

        // verification that yvar is binary and coded as 0 or 1
        capture assert inlist(`yvar', 0, 1) if `touse'
        if _rc {
            di as err "`yvar' must be coded as either 0 or 1"
            exit 450
        }

		// error if odds == 1
		if `odds' == 1 {
			di as err "the odds cannot be set to 1 because it won't detect a process change"
            exit 198
        }

		// odds cannot be negative
		if `odds' <= 0 {
			di as err "the odds multiplier must be greater than 0"
            exit 198
		}

		// replace previously generated variables by csum
		if "`replace'" != "" {
			local csumvars : char _dta[_csumvars]
			if "`csumvars'" != "" {
				foreach v of local csumvars {
					capture drop `v'
				}
			}
		}

		// recode baseline probability (blprob) if > 50%
		if `blprob' >  0.50 {
			local blprob = 1 - `blprob'
		}

		// compute limit if not specified
		if "`limit'" == "" {
			if `odds' > 1 {
				preserve
				simulate ct_max = r(max) , reps(`reps') seed(`seed') nolegend: csumdgp , obs(`N') mult(`odds') p0(`blprob')
				qui centile ct_max, centile(`centile')
				local limit = r(c_1)
				restore
			}
			else if `odds' < 1 {
				preserve
				local centile = 100 - `centile'
				simulate ct_min = r(min) , reps(`reps') seed(`seed') nolegend: csumdgp , obs(`N') mult(`odds') p0(`blprob')
				qui centile ct_min, centile(`centile')
				local limit = r(c_1)
				restore
			}
		} // end no limit
		return scalar limit = `limit'

		// weighting
		local o0 = `blprob' / (1 - `blprob')
		local oA = `odds' * `o0'
		local pA = `oA' / (1 + `oA')

		if 	"`wt'" == "" {
			local wf = log(`pA' /  `blprob')
			local ws = log((1 - `pA') / (1 - `blprob'))
			qui gen _wt = cond(y==1, `wf', `ws') if `touse'
		}
		else {
			qui gen _wt = `wt' if `touse'
		}

		// generate cusum
		qui gen _ct = 0 if `touse'

		sort `touse' `xvar'

		// odds multiplier > 1 indicates process deterioration
		if `odds' > 1 {
			qui replace _ct in 1 = max(0, _ct[1] + _wt[1]) if `touse'
			forvalues ii = 2/`N' {
				qui replace _ct in `ii' = max(0, _ct[`ii'-1] + _wt[`ii']) if `touse'
			}
			qui gen _signal = cond(_ct > `limit', 1, 0) if `touse'
		}
		// odds multiplier < 1 indicates process improvement
		else if `odds' < 1 {
			qui replace _ct in 1 = min(0, _ct[1] - _wt[1]) if `touse'
			forvalues ii = 2/`N' {
				qui replace _ct in `ii' = min(0, _ct[`ii'-1] - _wt[`ii']) if `touse'
			}
			qui gen _signal = cond(_ct < `limit', 1, 0) if `touse'
		}

		// keep track of variables created by csum
		local csumvars _wt _ct _signal
        char def _dta[_csumvars] "`csumvars'"


		// graph it
        if "`nograph'" == ""  {
			local roundlimit = round(`limit',.001)
			twoway(connected _ct `xvar' if `touse', mcol(black) msize(vsmall) lcolor(black)) ///
				(connected _ct `xvar' if _signal==1 & `touse', mcol(orange) msize(vsmall) lcolor(orange)), yline(`limit') ///
				ytitle("Cusum (`1')") note("control limit: `roundlimit'") legend(off) `options'
		}

end