*! version 1.0.0 20260718 Eric A. Booth, Sr Researcher, Texas 2036 *! applyvarlabels: label the variables in memory from a two-column *! (Variable, Label) crosswalk held in Excel or a CSV * * The problem (Statalist 1768839): you are handed a dataset of 100+ variables, * none of them labeled, alongside a spreadsheet that lists each variable name * beside the label it should carry. The tempting loop -- levelsof the labels, * tokenize the variables, march down both -- fails, because levelsof returns * the distinct labels sorted ALPHABETICALLY, so pairing them positionally with * the variables lines them up in the wrong order. * * applyvarlabels reads the crosswalk in ROW ORDER and matches every label to * its variable BY NAME, never by position. It WRITES a capture-guarded relabel * do-file (a variable named in the crosswalk but absent from your data is simply * skipped, not an error), RUNS that do-file to apply the labels, and prints an * inventory of everything that did not line up: crosswalk rows with no such * variable, and variables in memory that the crosswalk never mentions. * * The data in memory are never sorted, dropped, or reordered -- the crosswalk is * read in its own frame, so nothing changes but the variable labels themselves. * The generated do-file is a keepable, rerunnable artifact (see dofile()). program define applyvarlabels, rclass version 16.0 syntax using/ [, Variable(name) Label(name) SHEET(string) /// DOfile(string) SMCL(string) REPLACE NOCASE NORUN ] * ------------------------------------------------------------------ setup -- if "`variable'" == "" local variable Variable if "`label'" == "" local label Label if c(k) == 0 { di as err "no variables in memory to label; use or import your data first" exit 2000 } capture confirm file `"`using'"' if _rc { di as err `"crosswalk file not found: `using'"' exit 601 } * exact name matching: turn off abbreviation so a crosswalk row "inc" never * silently lands on a variable named "income". Restored on the way out. local va = c(varabbrev) set varabbrev off quietly ds local mainvars `r(varlist)' local nmain : word count `mainvars' * pick the importer from the file extension (Excel by default) -------------- local low = lower(`"`using'"') if regexm(`"`low'"', "\.(csv|tsv|txt)$") local how delim else local how excel local sheetopt "" if `"`sheet'"' != "" { if "`how'" == "excel" local sheetopt `"sheet(`"`sheet'"')"' else di as txt "note: sheet() ignored for a delimited crosswalk" } * where the relabel do-file will be written -------------------------------- if `"`dofile'"' == "" { local dofile `"`c(tmpdir)'applyvarlabels_relabel.do"' local dodefault 1 } else { if !regexm(lower(`"`dofile'"'), "\.do$") local dofile `"`dofile'.do"' if "`replace'" == "" { capture confirm new file `"`dofile'"' if _rc { set varabbrev `va' di as err `"do-file `dofile' already exists; specify replace"' exit 602 } } } * where the inventory SMCL will be written (validated up front, like dofile) - if `"`smcl'"' == "" local smclout `"`c(tmpdir)'applyvarlabels_inventory.smcl"' else { local smclout `"`smcl'"' if !regexm(lower(`"`smclout'"'), "\.smcl$") local smclout `"`smclout'.smcl"' if "`replace'" == "" { capture confirm new file `"`smclout'"' if _rc { set varabbrev `va' di as err `"smcl file `smclout' already exists; specify replace"' exit 602 } } } * ------------------------------------------------ read the crosswalk frame -- tempname cw frame create `cw' frame `cw' { if "`how'" == "excel" /// capture noisily import excel using `"`using'"', `sheetopt' firstrow allstring clear else /// capture noisily import delimited using `"`using'"', varnames(1) stringcols(_all) clear if _rc { local importrc = _rc } else { quietly ds local cols `r(varlist)' local vcol "" local lcol "" foreach c of local cols { if strlower("`c'") == strlower("`variable'") local vcol `c' if strlower("`c'") == strlower("`label'") local lcol `c' } local ncross = _N } } if "`importrc'" != "" { capture frame drop `cw' set varabbrev `va' di as err `"could not read the crosswalk (import returned r(`importrc'))"' exit `importrc' } if "`vcol'" == "" | "`lcol'" == "" { capture frame drop `cw' set varabbrev `va' if "`vcol'" == "" di as err `"variable-name column "`variable'" not found in crosswalk (columns: `cols')"' if "`lcol'" == "" di as err `"label column "`label'" not found in crosswalk (columns: `cols')"' di as err "use variable() and label() to name the crosswalk columns" exit 111 } * ---------------------------------------- write the capture-guarded do-file -- local bq = char(96) // ` -- placeholders so the generated file carries a local fq = char(39) // ' -- LITERAL `avl_va', not this program's macro tempname fh capture file open `fh' using `"`dofile'"', write text replace if _rc { capture frame drop `cw' set varabbrev `va' di as err `"cannot write the relabel do-file: `dofile'"' exit _rc } file write `fh' `"*! relabel do-file written by -applyvarlabels- on `c(current_date)' `c(current_time)'"' _n file write `fh' `"*! crosswalk: `using'"' _n file write `fh' `"*! Each line is capture-guarded: a variable absent from the data is skipped,"' _n file write `fh' `"*! so this file is safe to rerun on any dataset that shares these names."' _n file write `fh' "" _n file write `fh' "local avl_va = c(varabbrev)" _n file write `fh' "set varabbrev off" _n local matched "" // real variable names that received a label local missing "" // crosswalk rows with no such variable in memory local dups "" // variables named more than once in the crosswalk local blanks "" // crosswalk rows whose label cell was empty local normalized "" // labels altered for Stata (curly quotes, apostrophes) local nlong = 0 // labels over Stata's 80-character limit local seen "" local napplied = 0 local nmisses = 0 // crosswalk ROWS with no matching variable local dq = char(34) // " -- a straight double quote local cq = uchar(8221) // " -- its typographic replacement local bd = char(92) + char(36) // \$ -- written escape that survives -do- frame `cw' { forvalues i = 1/`ncross' { local vname = strtrim(`vcol'[`i']) local vlab = strtrim(`lcol'[`i']) if "`vname'" == "" continue // blank / spacer row * resolve the crosswalk name to a real variable in memory ---------- local realname "" local hit : list posof "`vname'" in mainvars if `hit' local realname `vname' else if "`nocase'" != "" { foreach m of local mainvars { if strlower("`m'") == strlower("`vname'") local realname `m' } } if "`realname'" == "" { local missing `missing' `vname' local ++nmisses continue } if `"`vlab'"' == "" { local blanks `blanks' `realname' continue } * Two characters cannot be carried as-is and are normalized (the * label still reads; the affected variables are reported): * - a straight double quote " -- -label variable- cannot parse one; * - a backtick ` -- no do-file escape exists for it, and it would * open a macro reference when the relabel do-file runs. if strpos(`"`macval(vlab)'"', `"`dq'"') { local vlab : subinstr local vlab `"`dq'"' `"`cq'"', all local normalized `normalized' `realname' } if strpos(`"`macval(vlab)'"', char(96)) { local vlab = subinstr(`"`macval(vlab)'"', char(96), char(39), .) local normalized `normalized' `realname' } local wasseen : list posof "`realname'" in seen if `wasseen' local dups `dups' `realname' local seen `seen' `realname' if length(`"`macval(vlab)'"') > 80 local ++nlong * A dollar sign IS preserved: escape $ -> \$ in the WRITTEN line so it * is not macro-expanded when the relabel do-file is run; the label * applied to the data keeps a real $. local vlabw = subinstr(`"`macval(vlab)'"', char(36), `"`macval(bd)'"', .) * capture label variable `"