*! 1.0.0	Ariel Linden 10jul2026

program define wksmirnov, rclass
	version 14

	syntax varname(numeric) [pweight fweight aweight iweight] [if] [in], ///
		BY(varname numeric) ///
		[Reps(integer 0) ///
		SEED(integer -1) ///
		NODOts ///
		GRaph]

	if `reps' < 0 {
		di as error "reps() must be nonnegative"
		exit 198
	}

	marksample touse
	markout `touse' `by'

	quietly count if `touse'
	if r(N) == 0 {
		di as error "no observations satisfy the estimation sample"
		exit 2000
	}

	// weights
	tempvar w
	if "`weight'" != "" {
		local wexp = trim(`"`exp'"')
		if substr(`"`wexp'"', 1, 1) == "=" {
			local wexp = trim(substr(`"`wexp'"', 2, .))
		}
		quietly gen double `w' = `wexp' if `touse'
		quietly replace `touse' = 0 if `touse' & (`w' <= 0 | missing(`w'))
	}
	else {
		quietly gen double `w' = 1 if `touse'
	}

	// validate two groups
	quietly levelsof `by' if `touse', local(glevels)
	local ng : word count `glevels'
	if `ng' != 2 {
		di as error "by(`by') must identify exactly two nonmissing groups in the estimation sample; found `ng'"
		exit 198
	}
	local g0 : word 1 of `glevels'
	local g1 : word 2 of `glevels'

	tempvar gg
	quietly gen byte `gg' = (`by' == `g1') if `touse'

	if `seed' >= 0 {
		set seed `seed'
	}

	// display dots
	local dots = ("`nodots'" == "")

	if `reps' > 0 & `dots' == 1 {
		di _n
		_dots 0
	}

	// call Mata
	tempname sD sPa sPp sNe1 sNe0 sN1 sN0

	mata: wksmirnov_calc("`varlist'", "`gg'", "`w'", "`touse'", ///
		`reps', `dots', "`sD'", "`sPa'", "`sPp'", "`sNe1'", "`sNe0'", "`sN1'", "`sN0'")

	local D   = scalar(`sD')
	local n1  = scalar(`sN1')
	local n0  = scalar(`sN0')
	local ne1 = scalar(`sNe1')
	local ne0 = scalar(`sNe0')
	local pa  = scalar(`sPa')
	if `reps' > 0 local pp = scalar(`sPp')

	// display
	if "`weight'" != "" {
		di as txt _n "Weighted two-sample Kolmogorov–Smirnov test for equality of distribution functions"
	}
	else {
		di as txt _n "Two-sample Kolmogorov–Smirnov test for equality of distribution functions"
	}
	di as txt "{hline 55}"
	di as txt "Variable: " as res "`varlist'" _col(30) as txt "Group variable: " as res "`by'"
	di as txt "Group `g1' =" as res %6.0f `n1' as txt _col(30) "Effective N = " as res %6.0f `ne1'
	di as txt "Group `g0' =" as res %6.0f `n0' as txt _col(30) "Effective N = " as res %6.0f `ne0'
	di as txt "{hline 55}"
	di as txt "D statistic" _col(30) " = " as res %9.4f `D'
	di as txt "Analytic p-value (asymptotic)" _col(30) " = " as res %9.4f `pa'
	if `reps' > 0 {
		di as txt "Permuted p-value ({bf:`reps'} reps)" _col(30) " = " as res %9.4f `pp'
	}
	di as txt "{hline 55}"

	// saved results
	return scalar D     = `D'
	return scalar N1    = `n1'
	return scalar N0    = `n0'
	return scalar effN1 = `ne1'
	return scalar effN0 = `ne0'
	return scalar p     = `pa'
	if `reps' > 0 {
		return scalar p_perm    = `pp'
		return scalar n_reps    = `reps'
	}
	return local group1 "`g1'"
	return local group0 "`g0'"
	return local varname "`varlist'"

	// graph
	if "`graph'" != "" {
		tempname M
		mata: st_matrix("`M'", wks_ecdf(st_data(.,"`varlist'","`touse'"), ///
			st_data(.,"`gg'","`touse'"), st_data(.,"`w'","`touse'")))

		preserve
		quietly drop _all
		quietly svmat `M', names(col)
		quietly rename c1 x
		quietly rename c2 F1
		quietly rename c3 F0

		// locate the D point
		quietly gen double gap = abs(F1 - F0)
		quietly summarize gap, meanonly
		quietly gen byte atmax = (gap == r(max)) if !missing(gap)
		quietly summarize x if atmax, meanonly
		local Dx    = r(mean)
		quietly summarize F1 if atmax, meanonly
		local DF1   = r(mean)
		quietly summarize F0 if atmax, meanonly
		local DF0   = r(mean)
		local Dmidy = (`DF1' + `DF0') / 2

		quietly summarize x, meanonly
		local Dtextx = `Dx' + 0.035 * (r(max) - r(min))

		local cdftitle "Empirical CDFs"
		local cdfytitle "Cumulative proportion"
		if "`weight'" != "" {
			local cdftitle "Weighted empirical CDFs"
			local cdfytitle "Weighted cumulative proportion"
		}

		quietly twoway (line F1 x, sort connect(stairstep) lwidth(medthick))          ///
						(line F0 x, sort connect(stairstep) lpattern(dash))           ///
						(pcspike F0 x F1 x if atmax, lcolor(black) lwidth(medium)      ///
							mcolor(black) msymbol(O) msize(small)),                   ///
						legend(order(1 "Group `g1'" 2 "Group `g0'"))                  ///
						ytitle("`cdfytitle'")                                        ///
						xtitle("`varlist'")                                          ///
						title("`cdftitle'")                                          ///
						text(`Dmidy' `Dtextx' "D = `: display %5.3f `D''", place(e) size(small)) ///
						name(wksmirnov_ecdf, replace)
		restore
	}
end

version 14
mata:

real matrix wks_ecdf(real vector x, real vector g, real vector w)
{
	real vector idx, xs, gs, ws, cw1, cw0, last, ux, Fx1, Fx0
	real scalar n, s1, s0

	n   = rows(x)
	idx = order((x,g), (1,2))
	xs  = x[idx]
	gs  = g[idx]
	ws  = w[idx]

	s1 = sum(ws :* (gs:==1))
	s0 = sum(ws :* (gs:==0))

	cw1 = runningsum(ws :* (gs:==1))
	cw0 = runningsum(ws :* (gs:==0))

	if (n > 1) {
		last = selectindex(xs[1::(n-1)] :!= xs[2::n])
		last = last \ n
	}
	else {
		last = 1
	}

	ux  = xs[last]
	Fx1 = cw1[last] :/ s1
	Fx0 = cw0[last] :/ s0

	return((ux, Fx1, Fx0))
}

real scalar wks_Dstat(real vector x, real vector g, real vector w)
{
	real matrix E
	real scalar D

	E = wks_ecdf(x, g, w)
	D = max(abs(E[.,2] :- E[.,3]))
	return(D)
}

real scalar wks_kolm_p(real scalar D, real scalar ne1, real scalar ne0)
{
	real scalar en, lambda, p, k, term

	en     = ne1*ne0/(ne1+ne0)
	lambda = (sqrt(en) + 0.12 + 0.11/sqrt(en)) * D
	p      = 0
	for (k=1; k<=100; k++) {
		term = 2*(-1)^(k-1)*exp(-2*k^2*lambda^2)
		p    = p + term
		if (abs(term) < 1e-10) break
	}
	if (p < 0) p = 0
	if (p > 1) p = 1
	return(p)
}

real vector wks_wsample(real scalar n, real scalar size, real vector prob)
{
	// weighted sampling with replacement via inverse-CDF + binary search
	real vector cume, u, idx
	real scalar i, lo, hi, mid

	cume = runningsum(prob) :/ sum(prob)
	u    = runiform(size, 1)
	idx  = J(size, 1, 0)
	for (i=1; i<=size; i++) {
		lo = 1
		hi = n
		while (lo < hi) {
			mid = floor((lo+hi)/2)
			if (cume[mid] < u[i]) {
				lo = mid+1
			}
			else {
				hi = mid
			}
		}
		idx[i] = lo
	}
	return(idx)
}

// prints Stata-style progress dots from inside a Mata loop, with a running
// count every 50 reps -- a simplified version of what _dots draws for
// bootstrap/simulate/permute (no tick-mark ruler header, just dots+counts)
void wks_dots_tick(real scalar b, real scalar B)
{
	real scalar pad
	string scalar fmt

	printf(".")
	if (mod(b,50)==0) {
		printf("%5.0f\n", b)
	}
	else if (b==B) {
		pad = 50 - mod(B,50)
		fmt = "%" + strofreal(5*pad+5) + ".0f\n"
		printf(fmt, b)
	}
	displayflush()
}

// bootstrap approach to compute p value  as used in TWANG package
real scalar wks_twang_boot_p(real vector x, real vector g, real vector w, real scalar Dobs, real scalar B, real scalar showdots)
{
	real scalar n, ess_t, ess_c, sizeb, mt, mc, b, count, Dboot
	real vector w1, idx, gb, xb

	n     = rows(g)
	ess_t = (sum(w :* (g:==1)))^2 / sum((w:*(g:==1)):^2)
	ess_c = (sum(w :* (g:==0)))^2 / sum((w:*(g:==0)):^2)

	w1 = (g:==1) :* (ess_t :* w :/ sum(w :* (g:==1))) + (g:==0) :* (ess_c :* w :/ sum(w :* (g:==0)))
	w1 = w1 :/ (ess_t + ess_c)

	sizeb = trunc(ess_t + ess_c)
	mt    = floor(ess_t)
	mc    = sizeb - mt

	count = 0
	for (b=1; b<=B; b++) {
		idx = wks_wsample(n, sizeb, w1)
		xb  = x[idx]
		gb  = J(sizeb, 1, 0)
		if (mt > 0) gb[1::mt] = J(mt, 1, 1)
		Dboot = wks_Dstat(xb, gb, J(sizeb, 1, 1))
		if (Dboot >= Dobs) count = count + 1
		if (showdots==1) wks_dots_tick(b, B)
	}
	return(count/B)
}

void wksmirnov_calc(string scalar xvar, string scalar gvar, string scalar wvar,
	string scalar touse, real scalar B, real scalar showdots,
	string scalar sD, string scalar sPa, string scalar sPp,
	string scalar sNe1, string scalar sNe0, string scalar sN1, string scalar sN0)
{
	real vector x, g, w, w1, w0
	real scalar D, ne1, ne0, n1, n0, pa, pp

	x = st_data(., xvar, touse)
	g = st_data(., gvar, touse)
	w = st_data(., wvar, touse)

	D  = wks_Dstat(x, g, w)
	n1 = sum(g:==1)
	n0 = sum(g:==0)

	w1  = select(w, g:==1)
	w0  = select(w, g:==0)
	ne1 = (sum(w1))^2 / sum(w1:^2)
	ne0 = (sum(w0))^2 / sum(w0:^2)

	st_numscalar(sD,   D)
	st_numscalar(sN1,  n1)
	st_numscalar(sN0,  n0)
	st_numscalar(sNe1, ne1)
	st_numscalar(sNe0, ne0)

	pa = wks_kolm_p(D, ne1, ne0)
	st_numscalar(sPa, pa)

	if (B > 0) {
		pp = wks_twang_boot_p(x, g, w, D, B, showdots)
		st_numscalar(sPp, pp)
	}
	else {
		st_numscalar(sPp, .)
	}
}

end