/******************************************************************************* APCDESCRIBE: A tool for producing descriptive diagnostic APC graphs. ******************************************************************************** Version: 2.0 (23.07.2026) Author: Gordey Yastrebov, University of Cologne License: GPL-3.0 *******************************************************************************/ version 14 pr de apcdescribe, rclass syntax varlist(min=1 max=1 numeric) [if] [in] [aw fw pw iw], /// [A(string asis) /// age variable + specification P(string asis) /// period variable + specification C(string asis) /// cohort variable + specification Binpos(string) /// bin positioning for grouped data CI(string) /// confidence level for pointwise intervals RECASTci(string) /// CI rendering LIneops(string asis) /// common estimate-line and marker options ALIneops(string asis) /// age-specific estimate-line and marker options PLIneops(string asis) /// period-specific estimate-line and marker options CLIneops(string asis) /// cohort-specific estimate-line and marker options CIPLotops(string asis) /// common CI-layer options ACIPLotops(string asis) /// age-specific CI-layer options PCIPLotops(string asis) /// period-specific CI-layer options CCIPLotops(string asis) /// cohort-specific CI-layer options PLotops(string asis) /// common graph options APLotops(string asis) /// age-specific graph options PPLotops(string asis) /// period-specific graph options CPLotops(string asis) /// cohort-specific graph options COMBined /// combine requested plots COMBPLotops(string asis)] // combined graph options loc depvar `varlist' marksample touse *** Parse common options loc binpos = lower(strtrim(`"`binpos'"')) if `"`binpos'"' == "" loc binpos center if !inlist(`"`binpos'"', "center", "mean") { di as err "Option {bf:binpos()} must be either {bf:center} or {bf:mean}." exit 198 } loc ci = strtrim(`"`ci'"') loc no_ci = (`"`ci'"' == "") loc ci_lvl . if !`no_ci' { cap conf n `ci' if _rc { di as err "Option {bf:ci()} must contain one numeric confidence level." exit 198 } loc ci_lvl = `ci' if `ci_lvl' <= 0 | `ci_lvl' >= 100 { di as err "The confidence level in {bf:ci()} must be greater than 0 and less than 100." exit 198 } } loc recastci = lower(strtrim(`"`recastci'"')) if `no_ci' & `"`recastci'"' != "" { di as err "Option {bf:recastci()} requires option {bf:ci()}." exit 198 } if !`no_ci' { if `"`recastci'"' == "" loc recastci rarea if !inlist(`"`recastci'"', "rcap", "rspike", "rline", "rarea") { di as err "Option {bf:recastci()} must be {bf:rcap}, {bf:rspike}, {bf:rline}, or {bf:rarea}." exit 198 } if inlist(`"`weight'"', "pweight", "iweight") { di as err "Option {bf:ci()} is not supported with probability or importance weights." exit 198 } } else if `"`ciplotops'`aciplotops'`pciplotops'`cciplotops'"' != "" { di as err "CI styling options require option {bf:ci()}." exit 198 } loc combined = (`"`combined'"' != "") *** Parse requested APC dimensions loc selection foreach apcvar in a p c { if strtrim(`"``apcvar''"') != "" loc selection `selection' `apcvar' } if `"`selection'"' == "" { di as err "At least one of {bf:a()}, {bf:p()}, or {bf:c()} must be specified." exit 198 } loc has_lpoly 0 foreach apcvar of loc selection { loc spec = strtrim(`"``apcvar''"') loc colon = strpos(`"`spec'"', ":") loc rawvar loc rawcuts loc method means loc lpolyopts loc lpoly_ciopts 0 if `colon' { loc rawvar = strtrim(substr(`"`spec'"', 1, `colon' - 1)) loc rhs = strtrim(substr(`"`spec'"', `colon' + 1, .)) if `"`rawvar'"' == "" | `"`rhs'"' == "" { di as err "Option {bf:`apcvar'()} is incorrectly specified." exit 198 } loc comma = strpos(`"`rhs'"', ",") if `comma' { loc rhs_head = strtrim(substr(`"`rhs'"', 1, `comma' - 1)) loc rhs_opts = strtrim(substr(`"`rhs'"', `comma' + 1, .)) if `"`rhs_opts'"' == "" { di as err "No options were specified after the comma in {bf:`apcvar'(`spec')}." exit 198 } } else { loc rhs_head `rhs' loc rhs_opts } if lower(`"`rhs_head'"') == "lpoly" { loc method lpoly loc has_lpoly 1 if `"`rhs_opts'"' != "" { cap noi _apcdescribe_parse_lpoly, `rhs_opts' if _rc exit _rc loc lpolyopts `"`r(options)'"' loc lpoly_ciopts = r(ci_options) } if `no_ci' & `lpoly_ciopts' { di as err /// "Options {bf:pwidth()} and {bf:var()} within an {bf:lpoly} specification require option {bf:ci()}." exit 198 } } else { if `comma' { di as err /// "A comma within {bf:`apcvar'()} is allowed only after the {bf:lpoly} method." exit 198 } loc method grouped loc rawcuts `rhs' } } else loc rawvar `spec' cap unab xvar : `rawvar' if _rc { di as err "Variable {bf:`rawvar'} specified in {bf:`apcvar'()} not found." exit 111 } if `: word count `xvar'' != 1 { di as err "Specification {bf:`rawvar'} in {bf:`apcvar'()} must identify one variable." exit 198 } cap conf numeric var `xvar' if _rc { di as err "Variable {bf:`xvar'} specified in {bf:`apcvar'()} must be numeric." exit 109 } loc `apcvar'var `xvar' loc `apcvar'method `method' loc `apcvar'cuts loc `apcvar'lpolyopts `"`lpolyopts'"' if `"`method'"' == "grouped" { cap numlist `"`rawcuts'"' if _rc { di as err /// "The specification after the colon in {bf:`apcvar'()} must be either {bf:lpoly} or a valid numlist." exit 198 } loc cuts `r(numlist)' loc ncuts : word count `cuts' if `ncuts' < 2 { di as err "The numlist in {bf:`apcvar'()} must contain at least two cut points." exit 198 } forv j = 2/`ncuts' { loc k = `j' - 1 loc prev : word `k' of `cuts' loc curr : word `j' of `cuts' if `curr' <= `prev' { di as err "Cut points in {bf:`apcvar'()} must be strictly increasing." exit 198 } } loc `apcvar'cuts `cuts' } } if `has_lpoly' & inlist(`"`weight'"', "pweight", "iweight") { di as err /// "Local-polynomial descriptive curves are not supported with probability or importance weights." exit 198 } *** Generate inputs and plots loc wgt if `"`weight'"' != "" loc wgt [`weight'`exp'] loc a_title Age loc p_title Period loc c_title Cohort tempname a_input p_input c_input loc nplots : word count `selection' loc suppress = (`combined' & `nplots' > 1) loc plots foreach apcvar of loc selection { loc method "``apcvar'method'" loc dim_lpolyopts `"``apcvar'lpolyopts'"' if `"`method'"' == "lpoly" { loc lp_arg if `"`dim_lpolyopts'"' != "" loc lp_arg lpolyopts(`dim_lpolyopts') loc ci_arg if !`no_ci' loc ci_arg cilevel(`ci_lvl') _apcdescribe_build_lpoly_input `depvar' if `touse' `wgt', /// xvar(``apcvar'var') dimension(``apcvar'_title') /// no_ci(`no_ci') `lp_arg' `ci_arg' } else { loc opts xvar(``apcvar'var') binpos(`binpos') /// dimension(``apcvar'_title') no_ci(`no_ci') if `"`method'"' == "grouped" loc opts `opts' cuts(``apcvar'cuts') if !`no_ci' loc opts `opts' cilevel(`ci_lvl') _apcdescribe_build_input `depvar' if `touse' `wgt', `opts' } mat ``apcvar'_input' = r(input) preserve qui clear cap qui svmat double ``apcvar'_input', names(col) loc rc = _rc if !`rc' { loc ci_layer if !`no_ci' loc ci_layer /// (`recastci' y_lower y_upper x_value, sort /// `ciplotops' ``apcvar'ciplotops') loc estimate_defaults if `"`method'"' == "lpoly" loc estimate_defaults msymbol(none) loc graphics `c(graphics)' if `suppress' qui set gr off cap noi twoway `ci_layer' /// (scatter y_mean x_value, sort connect(l) /// `estimate_defaults' /// `lineops' ``apcvar'lineops'), /// yti("") xti(`"``apcvar'_title'"', height(7)) leg(off) /// `plotops' ``apcvar'plotops' /// name(``apcvar'_title', replace) loc rc = _rc if `suppress' qui set gr `graphics' } restore if `rc' exit `rc' loc plots `plots' ``apcvar'_title' } *** Combine and return if `combined' & `nplots' > 1 gr combine `plots', /// r(1) ycom name(Combined, replace) `combplotops' ret clear ret loc dimensions `selection' ret loc binpos `binpos' ret scalar ci = `ci_lvl' if !`no_ci' ret loc recastci `recastci' if `"`a'"' != "" { ret mat age = `a_input' ret loc age_method `amethod' } if `"`p'"' != "" { ret mat period = `p_input' ret loc period_method `pmethod' } if `"`c'"' != "" { ret mat cohort = `c_input' ret loc cohort_method `cmethod' } end *** Routines: ****************************************************************** pr de _apcdescribe_parse_lpoly, rclass /* One-letter abbreviations are accepted: k(), b(), d(), n(), p(), v(). Optional numeric descriptors in syntax require defaults. Capture the arguments as strings instead so that omission can be distinguished from any valid numeric value, then validate explicitly. */ syntax [, Kernel(string) Bwidth(string) Degree(string) N(string) /// Pwidth(string) Var(string)] loc kernel = lower(strtrim(`"`kernel'"')) if `"`kernel'"' != "" & /// !inlist(`"`kernel'"', "epanechnikov", "epan2", "biweight", /// "cosine", "gaussian", "parzen", "rectangle", "triangle") { di as err /// "Option {bf:kernel()} must specify a kernel supported by Stata's {bf:lpoly} command." exit 198 } /* bwidth() and var() accept either a number or a numeric variable. */ foreach opt in bwidth var { loc value = strtrim(`"``opt''"') if `"`value'"' != "" { cap conf number `value' if !_rc { if `"`opt'"' == "bwidth" & `value' <= 0 { di as err "Option {bf:bwidth()} must be greater than zero." exit 198 } if `"`opt'"' == "var" & `value' < 0 { di as err "Option {bf:var()} must be nonnegative." exit 198 } } else { cap conf numeric var `value' if _rc { di as err /// "Option {bf:`opt'()} must contain one number or identify one numeric variable." exit 198 } } loc `opt' `value' } } loc pwidth = strtrim(`"`pwidth'"') if `"`pwidth'"' != "" { cap conf number `pwidth' if _rc { di as err "Option {bf:pwidth()} must contain one numeric value." exit 198 } if `pwidth' <= 0 { di as err "Option {bf:pwidth()} must be greater than zero." exit 198 } } foreach opt in degree n { loc value = strtrim(`"``opt''"') if `"`value'"' != "" { cap conf number `value' if _rc | `value' != floor(`value') { di as err "Option {bf:`opt'()} must contain one integer." exit 198 } if `"`opt'"' == "degree" & `value' < 0 { di as err "Option {bf:degree()} must be a nonnegative integer." exit 198 } if `"`opt'"' == "n" & `value' <= 0 { di as err "Option {bf:n()} must be a positive integer." exit 198 } loc `opt' `value' } } loc options if `"`kernel'"' != "" loc options `options' kernel(`kernel') if `"`bwidth'"' != "" loc options `options' bwidth(`bwidth') if `"`degree'"' != "" loc options `options' degree(`degree') if `"`n'"' != "" loc options `options' n(`n') if `"`pwidth'"' != "" loc options `options' pwidth(`pwidth') if `"`var'"' != "" loc options `options' var(`var') loc ci_options = (`"`pwidth'"' != "" | `"`var'"' != "") ret loc options `"`options'"' ret scalar ci_options = `ci_options' end pr de _apcdescribe_build_lpoly_input, rclass syntax varlist(min=1 max=1 numeric) [if] [in] [aw fw], /// XVAR(varname numeric) DIMENSION(string) no_ci(integer) /// [CILEVEL(real 95) LPOLYOPTS(string asis)] loc depvar `varlist' marksample sample loc wgt if `"`weight'"' != "" loc wgt [`weight'`exp'] preserve qui keep if `sample' qui drop if mi(`depvar', `xvar') qui count if !r(N) { restore di as err "No observations remain for the `dimension' local-polynomial plot." exit 2000 } tempvar x_value x_label y_mean y_se y_lower y_upper loc seopt if !`no_ci' loc seopt se(`y_se') cap qui lpoly `depvar' `xvar' `wgt', /// generate(`x_value' `y_mean') `seopt' nograph `lpolyopts' loc rc = _rc if `rc' { restore exit `rc' } qui keep if !mi(`x_value', `y_mean') qui count if !r(N) { restore di as err /// "The local-polynomial estimator produced no usable values for the `dimension' plot." exit 2000 } qui gen double `x_label' = `x_value' qui gen double `y_lower' = . qui gen double `y_upper' = . if !`no_ci' { loc alpha2 = (100 - `cilevel') / 200 loc zcrit = invnormal(1 - `alpha2') qui replace `y_lower' = `y_mean' - `zcrit' * `y_se' /// if !mi(`y_se') qui replace `y_upper' = `y_mean' + `zcrit' * `y_se' /// if !mi(`y_se') } qui sort `x_value' tempname input cap qui mkmat `x_value' `x_label' `y_mean' `y_lower' `y_upper', /// mat(`input') loc rc = _rc if `rc' { restore exit `rc' } mat colnames `input' = x_value x_label y_mean y_lower y_upper restore ret mat input = `input' end pr de _apcdescribe_build_input, rclass syntax varlist(min=1 max=1 numeric) [if] [in] [aw fw pw iw], /// XVAR(varname numeric) BINPOS(string) DIMENSION(string) no_ci(integer) /// [CUTS(string asis) CILEVEL(real 95)] loc depvar `varlist' marksample sample loc grouped = (`"`cuts'"' != "") loc wgt if `"`weight'"' != "" loc wgt [`weight'`exp'] preserve qui keep if `sample' qui drop if mi(`xvar') tempvar group x_value x_label y_mean y_se n y_lower y_upper if `grouped' { cap qui egen long `group' = cut(`xvar'), at(`cuts') icodes loc rc = _rc if `rc' { restore exit `rc' } qui drop if mi(`group') } else qui clonevar `group' = `xvar' qui count if !r(N) { restore di as err "No observations remain for the `dimension' descriptive plot." exit 2000 } loc xpos if `grouped' & `"`binpos'"' == "mean" loc xpos `x_value' = `xvar' if `no_ci' { cap qui collapse (mean) `y_mean' = `depvar' `xpos' `wgt', /// by(`group') } else { cap qui collapse /// (mean) `y_mean' = `depvar' `xpos' /// (semean) `y_se' = `depvar' /// (count) `n' = `depvar' `wgt', by(`group') } loc rc = _rc if `rc' { restore exit `rc' } if `grouped' { qui gen double `x_label' = . loc nbins = `: word count `cuts'' - 1 forv j = 1/`nbins' { loc next = `j' + 1 loc lower : word `j' of `cuts' loc upper : word `next' of `cuts' loc code = `j' - 1 qui replace `x_label' = (`lower' + `upper') / 2 /// if `group' == `code' } if `"`binpos'"' == "center" /// qui gen double `x_value' = `x_label' } else { qui gen double `x_value' = `group' qui gen double `x_label' = `group' } qui gen double `y_lower' = . qui gen double `y_upper' = . if !`no_ci' { loc alpha2 = (100 - `cilevel') / 200 qui replace `y_lower' = /// `y_mean' - invttail(`n' - 1, `alpha2') * `y_se' /// if `n' >= 2 & !mi(`y_se') qui replace `y_upper' = /// `y_mean' + invttail(`n' - 1, `alpha2') * `y_se' /// if `n' >= 2 & !mi(`y_se') } qui sort `x_value' tempname input cap qui mkmat `x_value' `x_label' `y_mean' `y_lower' `y_upper', /// mat(`input') loc rc = _rc if `rc' { restore exit `rc' } mat colnames `input' = x_value x_label y_mean y_lower y_upper restore ret mat input = `input' end