codescan
Data PreparationCode lists to patient-level conditions
Version 1.0.0 | 2026-10-26
Quick Start · Installation · Worked Examples · Key Options · Stata-Tools · Report a bug
codescan scans wide-format diagnosis, procedure, medication, registry, and claims code slots with anchored regex or prefix rules and produces row-level indicators, counts, patient-level summaries, and exports. codescan_describe inventories the codes first so you can draft rules from the data you actually have.
Quick Start
After installing, flag six conditions across four ICD-10 diagnosis slots in a 500-patient synthetic extract (three encounters each) and chart patient-level prevalence. The setup builds the same extract as demo/demo_codescan.do:
Build the synthetic extract
clear
set seed 20260226
set obs 1500
generate long pid = ceil(_n / 3)
bysort pid: generate byte enc = _n
generate double index_dt = mdy(1,1,2020) + int(runiform() * 730) if enc == 1
bysort pid (enc): replace index_dt = index_dt[1]
generate double visit_dt = index_dt - 365 + int(runiform() * 730) if enc == 1
replace visit_dt = index_dt - 180 + int(runiform() * 540) if enc == 2
replace visit_dt = index_dt + int(runiform() * 365) if enc == 3
format index_dt visit_dt %td
generate double age = 45 + int(runiform() * 40) if enc == 1
bysort pid (enc): replace age = age[1]
generate byte female = rbinomial(1, 0.52) if enc == 1
bysort pid (enc): replace female = female[1]
* Four diagnosis slots filled at random from a pool of ICD-10 codes
local all_dx E110 E119 E102 E103 E114 E100 E109 E030 I10 I110 I120 I131 I50 I21 I252 I70 I71 I48 C50 C61 C34 C18 C78 C79 C80 C81 C85 J44 J45 J47 G30 G311 G81 G820 M05 M06 M32 B18 B20 B21 N18 N19 N250 K700 K703 K721 K765 K25 F10 F32 F33 F20 D65 D66 D500 D509 R634 R001 Z00 Z96 Z87
local n_codes : word count `all_dx'
forvalues v = 1/4 {
generate str6 dx`v' = ""
forvalues i = 1/`=_N' {
if runiform() < 0.55 + 0.1 * (`v' == 1) {
local pick = 1 + int(runiform() * `n_codes')
local code : word `pick' of `all_dx'
quietly replace dx`v' = "`code'" in `i'
}
}
}
codescan dx1 dx2 dx3 dx4, define(dm "E1[01]" | htn "I1[0-35]" | chf "I50" | copd "J4[0-7]" | cancer "C[0-7]" ~ "C77|C78|C79|C80" | metastatic "C7[789]|C80") label(dm "Diabetes" \ htn "Hypertension" \ chf "Heart failure" \ copd "COPD" \ cancer "Cancer (non-met)" \ metastatic "Metastatic cancer") id(pid) collapse graph
graph display, margin(r+8)

Drawn by demo/demo_codescan.do with the white_tableau scheme. graph display, margin(r+8) widens the right margin so the longest bar label is not clipped; codescan, graph takes no graph options of its own.
Features
- No reshape, no copies. The scanned slots are an ordinary varlist (
dx1-dx30,dx*, or several ranges), and the stored codes are never changed:nodotsandtostringwork on temporary values. - Three-state indicators. A row-level indicator is 1 (or a count) for an analyzed match, 0 for an analyzed non-match, and
.for a row outside the analysis sample, sosummarizereproduces the reported prevalence. - Rules that cannot silently match everything. Every pattern is anchored at the start of the code,
~adds exclusions, and patterns that can match without consuming a character are rejected. - Rows, patients, or both. Keep encounter rows,
collapseto one row perid(),mergepatient-level flags back onto the rows, or write the result to aframe()and leave the data in memory untouched. - Windows with their denominators.
lookback()andlookforward()restrict matches around a reference date; several lookback windows return prevalence by window inr(sensitivity)and the denominator behind each inr(sensitivity_n). - Rules you can keep.
codescan_describeinventories the codes and drafts a codefile;save()writes inline rules to a CSV with amatchcolumn, andcodefile()refuses a later call whose matching options differ.
Requirements
- Stata 16 or later
- No external package dependencies
Installation
capture ado uninstall codescan
net install codescan, from("https://raw.githubusercontent.com/tpcopeland/Stata-Tools/main/codescan") replace
Commands
| Command | Description |
|---|---|
codescan |
Scan wide-format code variables and generate indicators, counts, or patient-level summaries |
codescan_describe |
Inspect the raw code inventory before writing scan rules |
How It Works
The usual workflow is to inspect the inventory, draft a small rule set, verify row-level matches, choose the output shape, and then add windows, date summaries, diagnostics, or file outputs.
Variable lists
The words between the command and the comma are a normal Stata varlist. Explicit names work for a few slots, ranges such as dx1-dx30 work for adjacent variables, and wildcards such as dx* select every matching variable.
codescan dx1 dx2 dx3, define(dm2 "E11")
codescan dx1-dx30, define(dm2 "E11")
codescan dx*, define(dm2 "E11")
codescan dx1-dx30 proc1-proc20, define(dm2 "E11" | proc "XF001")
Run separate scans with generate() when diagnosis, procedure, or medication fields need different rule sets and output names.
Matching rules
mode(regex) is the default. Every inclusion and exclusion pattern is anchored at the start of the code, so "E11" matches E110 and E119 but not AE11; character classes and alternation are supported. Codes are matched exactly as stored: leading or trailing spaces are not trimmed, so a padded " E11" does not match "E11" (codescan_describe lists such values as separate codes; clean them with strtrim() first). mode(prefix) treats pipe-separated tokens as simple starts-with prefixes and is useful when regex features are unnecessary.
The unquoted | in define() separates conditions, while a | inside a quoted pattern is part of that regex or prefix list. Use ~ after an inclusion pattern for exclusions, for example define(dm2 "E11" ~ "E116"); spaces around the unquoted | and ~ are optional. In mode(regex) whitespace is significant and never trimmed (" *I21" is a valid pattern); a note flags a pattern or exclusion that begins with an unquantified space or ends with a space. mode(prefix) trims each prefix, so there " I10" means "I10". The regex anchor is the non-capturing ^(?:...), so backreferences such as "(A)\1" count the pattern's own groups.
Regex patterns that can match without consuming a character are rejected because an anchored empty match would identify every code; empty alternatives in prefix lists are rejected for the same reason. In mode(regex), use . to match any nonempty code rather than .*; in mode(prefix), a period is literal. nocase enables unicode-aware case-insensitive matching, and nodots removes periods from the data before matching without changing the stored data. Under nodots, a pattern that requires a literal period is rejected because it can never match: a dotted prefix in mode(prefix), or an escaped \. or [.] in mode(regex).
Choosing a Workflow
| Goal | Call pattern | Result |
|---|---|---|
| Inspect the raw code distribution | codescan_describe dx1-dx30 |
Top codes and first-character chapter counts |
| Audit rules on encounters | codescan ... without collapse or merge |
Original rows plus condition variables |
| Build one row per patient or entity | codescan ..., id(pid) collapse |
One row per id() |
| Keep encounters and attach patient flags | codescan ..., id(pid) merge |
Original rows plus patient-level results |
| Keep the active data unchanged | Add frame(results) or preserve |
A named result frame or restored data |
| Save the prevalence table | Add export(results.xlsx, replace) |
CSV or Excel summary |
| Save the transformed dataset | Add saving(results.dta, replace) |
The final collapsed or merged result dataset |
frame(name) implies preserve; add replace when the named frame already exists. The named frame must differ from the current frame because codescan cannot write its result into the frame it is reading. save() writes reusable rule definitions, whereas saving() writes the transformed result dataset.
Worked Examples
1. Inventory, rows, and patients in one block
This self-contained example shows inventory, row-level matching, and patient-level output.
clear
input long pid str6 dx1 str6 dx2
1 "E110" "I10"
1 "Z00" "E119"
2 "I50" ""
2 "E102" ""
3 "Z00" ""
end
codescan_describe dx1 dx2
codescan dx1 dx2, define(dm2 "E11" | htn "I1[0-35]")
codescan dx1 dx2, define(dm2 "E11" | htn "I1[0-35]") id(pid) collapse replace
matrix list r(summary)
The first codescan call leaves one indicator per condition on each row; the second reduces the data to one row per pid. A row-level indicator is 1 (or a count) for an analyzed match, 0 for an analyzed non-match, and . for a row outside the analysis sample (if/in, missing date, or window), so summarize on it reproduces the reported prevalence. r(summary) reports the counts and prevalence for the final analysis unit.
The remaining examples use this small wide-format dataset. Run the setup before each example that changes the data in memory.
clear
input long pid str6 dx1 str6 dx2 str6 proc1 double visit_dt double index_dt
1 "E110" "I10" "XF001" 21914 21915
1 "Z00" "E119" "" 21880 21915
2 "I50" "" "JFB10" 21900 21915
2 "E102" "" "" 22020 21915
3 "Z00" "" "" 21910 21915
end
format visit_dt index_dt %td
2. Inspect the inventory
Start with codescan_describe when you do not yet know which code values or first-character chapters occur in the selected slots.
codescan_describe dx1 dx2, top(10)
matrix list r(top_codes)
The command returns the ranked code table in r(top_codes) and the first-character summary in r(chapters).
3. Create row-level indicators
The default regex mode anchors each rule at the start of every selected code value and creates one 0/1 variable per condition.
codescan dx1 dx2, define(dm2 "E11" | htn "I1[0-35]" | chf "I50") replace
4. Collapse within a time window
Use id() and daily Stata dates when the analysis unit is a patient or other entity. alldates requests the first, last, and unique qualifying dates for each condition.
codescan dx1 dx2, id(pid) date(visit_dt) refdate(index_dt) ///
define(dm2 "E11" | htn "I1[0-35]" | chf "I50") ///
lookback(365) inclusive collapse alldates replace
5. Match procedure prefixes and avoid name collisions
In prefix mode, pipe-separated tokens are alternative prefixes. generate() keeps a procedure scan separate from other condition variables.
codescan proc1, define(mammo "XF001|XF002" | colectomy "JFB|JFH") ///
mode(prefix) generate(proc_) replace
6. Exclude specific codes and save reusable rules
Use ~ for exceptions and save() to write the parsed inline definitions to a CSV. The second call reads that CSV as a reusable codefile.
codescan dx1 dx2, define(dm2 "E11" ~ "E116" | htn "I1[0-35]") ///
save(dm_rules.csv, replace)
codescan dx1 dx2, codefile(dm_rules.csv) replace
The CSV contains name, pattern, exclusion, label, and match columns. match records the call's mode(), nocase, and nodots; codefile() refuses a later call whose options differ, because the same pattern means something else under other options. The first call creates the condition variables, so replace permits the codefile call to recreate them.
7. Keep the original data in a frame
Use frame() when the encounter-level data must remain available alongside a collapsed patient-level result.
codescan dx1 dx2, define(dm2 "E11" | htn "I1[0-35]") id(pid) ///
collapse frame(results) replace
frame results: list
8. Compare hits with positive units
countmode stores the number of matching code slots rather than binary indicators. r(summary) separates those slot hits from the number of observations or IDs with at least one hit.
codescan dx1 dx2, define(dm2 "E11") id(pid) collapse countmode replace
matrix list r(summary)
Add detail allslots in a row-level scan when every matching slot should be credited independently; without allslots, binary detail is attributed to the first matching variable in varlist order.
9. Run sensitivity windows and write deliverables
Several lookback() values require collapse or merge and return prevalence by window plus the denominator behind each window. The Excel export gets a second sheet when cooccurrence is also requested.
codescan dx1 dx2, id(pid) date(visit_dt) refdate(index_dt) ///
define(dm2 "E11" | htn "I1[0-35]") ///
lookback(90 365) inclusive collapse cooccurrence replace ///
export(codescan_results.xlsx, replace) ///
saving(codescan_results.dta, replace) format(%9.2f)
matrix list r(sensitivity)
matrix list r(sensitivity_n)
Demo
The checkout demo demo/demo_codescan.do creates the synthetic administrative data used in the Quick Start, draws the prevalence chart shown there, and writes the Excel workbook demo/codescan_results.xlsx, which holds a summary sheet and a co-occurrence sheet. Run it from the Stata-Tools repository root, the codescan directory, or codescan/demo/. It installs the local codescan and tc_schemes sources into your PLUS directory and stops if the white_tableau scheme cannot be found, so restore your usual installation afterward if needed. The image and workbook are repository checkout assets, not files installed by net install.
Command Reference
codescan
codescan varlist [if] [in], define(string asis) | codefile(string) [options]
Exactly one of define() or codefile() is required. Inline definitions use name "pattern" [~ "exclusion" ...] | name2 "pattern2"; a codefile is a CSV or Stata dataset with string name and pattern columns and optional string exclusion, label, and match columns (column names are case-insensitive). When match is present it must hold the same value on every row (mode(regex) or mode(prefix), optionally nocase and nodots), and the call must use those options. Values in the name, pattern, exclusion, and match fields must not contain a backquote or a global-macro reference such as $name or ${name}; a lone $, including the regex end-of-string anchor, is allowed. The label field is carried verbatim, backquotes and $ included. No field may contain a double quote immediately followed by an apostrophe. Condition names must be valid, unique Stata names no longer than 26 characters so generated date/count suffixes remain within Stata's name limit.
codescan_describe
codescan_describe varlist [if] [in] [, top(#) nodots tostring save(filename [, replace])]
The command pools nonempty values across all selected variables, excluding the bare . placeholder, reports the most frequent codes, and groups all codes by their first character. Use save() to write a draft CSV with one row per first-character chapter (each pattern regex-escaped, so a . chapter matches only codes starting with a period), then edit the names, patterns, exclusions, and labels before using it with codescan, codefile(). A space chapter is drafted as [ ]. When any drafted pattern is a regex (an escaped metacharacter or [ ]), the draft's match column is mode(regex), so codefile() refuses it under other options instead of matching nothing; a draft of plain letters and digits leaves match empty. When the inventory is empty, save() writes the header with no data rows.
Key Options
Definition source
| Option | Use |
|---|---|
define() |
Supply inline condition definitions separated by an unquoted pipe |
codefile() |
Read string name and pattern definitions from CSV or .dta, with optional exclusion, label, and match columns |
label() |
Add presentation labels using \ between entries; labels do not change condition identifiers |
save() |
With codescan, write define() rules (not codefile()) to .csv; with codescan_describe, write a chapter draft; use the replace suboption to overwrite a file |
Identifiers and windows
| Option | Use |
|---|---|
id() |
Identify patients or entities for collapse and merge |
date() |
Supply a numeric Stata daily event-date variable; required for windows and date summaries |
refdate() |
Supply the numeric daily reference date for lookback() or lookforward() |
lookback() |
Restrict matches to one or more nonnegative backward windows; requires date() and refdate(); multiple windows require collapse or merge |
lookforward() |
Restrict matches to a nonnegative forward window; requires date() and refdate() |
inclusive |
Include refdate() in a single-direction window; with both directions it is already included |
Result dataset
| Option | Use |
|---|---|
collapse |
Reduce the active data to one row per id() |
merge |
Attach patient-level results to the original row structure; fully excluded IDs receive missing values |
earliestdate |
Create <condition>_first variables from qualifying daily dates |
latestdate |
Create <condition>_last variables from qualifying daily dates |
countdate |
Create <condition>_count variables counting unique qualifying dates |
countrows |
Create <condition>_nrows variables counting qualifying rows or slot hits under countmode |
alldates |
Shorthand for earliestdate latestdate countdate |
preserve |
Restore the original data after patient-level processing |
frame() |
Store the final result in a named frame and imply preserve |
saving() |
Save the final collapsed or merged dataset to .dta |
Diagnostics and reporting
| Option | Use |
|---|---|
detail |
Display and return per-variable match contributions in r(varcounts); default attribution is the first matching slot per row |
allslots |
With detail, count every matching slot instead of the first matching variable; requires detail; countmode uses this rule automatically |
cooccurrence |
Return pairwise condition counts in r(cooccurrence) and add a sheet to an Excel export; counts rows or unique IDs according to output shape |
unmatched() |
Create a row-level flag: 1 analyzed and unmatched, 0 analyzed and matched, . not analyzed |
matched_code() |
Create a row-level string containing the first code that survived matching |
graph |
Draw a horizontal prevalence bar chart |
export() |
Write the summary to .csv or .xlsx with an optional replace suboption |
format() |
Set the prevalence display and export format; default is %9.1f |
Matching behavior and naming
| Option | Use |
|---|---|
mode() |
Choose regex (default, anchored and unicode-aware) or prefix |
level() |
Truncate inclusion prefixes to 1–10 characters in mode(prefix); exclusions remain full precision |
nocase |
Use unicode-aware case-insensitive matching |
nodots |
Remove periods from values before matching or tabulating |
tostring |
Scan numeric code variables through temporary strings while leaving the originals unchanged |
countmode |
Store integer code-slot counts instead of binary indicators |
generate() |
Prefix all generated condition and summary variable names |
replace |
Allow overwriting planned output variables or an existing frame() |
noisily |
Display per-condition progress and match totals |
codescan_describe options
| Option | Use |
|---|---|
top() |
Set the number of ranked codes to display; default is top(20) |
nodots |
Remove periods before tabulating, so dotted and undotted values are pooled |
tostring |
Convert numeric code variables temporarily before tabulating |
save() |
Write the chapter summary as a draft CSV codefile |
File options accept ordinary quoted paths with spaces or hyphens, reject unsafe shell/control characters, and never overwrite an existing file without the option-specific replace suboption. Within one call, save(), export(), and saving() must name different output files; this is checked on the filesystem, so two paths reaching one file through a symbolic link or a case-insensitive filesystem are refused too.
Stored Results
codescan
| Result | Meaning |
|---|---|
r(N) |
Analyzed observations, or unique id() values after collapse or merge |
r(n_conditions) |
Number of conditions |
r(collapsed) |
1 when collapse was used, otherwise 0 |
r(merged) |
1 when merge was used, otherwise 0 |
r(mode_count) |
1 when countmode was used, otherwise 0 |
r(detail_allslots) |
1 when r(varcounts) used all-slot attribution; returned with detail |
r(lookforward) |
Requested forward-window length, when specified |
r(n_excluded_missingdate) |
Rows excluded from a window because date() or refdate() was missing |
r(conditions) |
Condition names in output order |
r(newvars) |
Created variables left in memory on exit |
r(varlist) |
Scanned variables |
r(mode) |
regex or prefix |
r(nocase) |
nocase when case-insensitive matching was used |
r(generate) |
The output prefix supplied to generate() |
r(define) |
The inline definition string, when define() was used |
r(codefile) |
The codefile path, when codefile() was used |
r(id) |
The identifier variable, when specified |
r(date) |
The event-date variable, when specified |
r(lookback) |
The one lookback value as a scalar, or space-separated values as a macro for multiple windows |
r(refdate) |
The reference-date variable when windowing was used |
r(frame) |
The result frame name, when frame() was used |
r(summary) |
Matrix with count, prevalence, total_hits, and positive_units columns |
r(codelist) |
Legacy exact copy of r(summary) |
r(varcounts) |
Per-variable match contributions, with detail; default attribution is first-slot per row, while allslots or countmode credits every slot |
r(cooccurrence) |
Pairwise condition counts, with cooccurrence; rows are counted at row level and unique IDs after collapse or merge |
r(sensitivity) |
Prevalence by condition and lookback window, with multiple windows |
r(sensitivity_n) |
The denominator for each r(sensitivity) column |
Without countmode, total_hits is missing because binary matching does not count repeated slots; count then represents positive_units. With countmode, count and total_hits are slot hits, while positive_units is the number of observations or IDs with at least one hit. r(newvars) is empty after preserve or frame() because the active data are restored.
When detail is requested, r(detail_allslots) records the attribution used for r(varcounts): it is 1 for allslots or countmode, and 0 for default first-slot attribution.
Before publishing a successful result set, codescan clears prior r() contents. A successful call that omits detail therefore does not retain r(detail_allslots) or r(varcounts) from an earlier call; both are absent unless detail is specified.
codescan_describe
| Result | Meaning |
|---|---|
r(n_unique) |
Number of unique nonempty codes, excluding the bare . placeholder |
r(n_entries) |
Number of nonempty, non-. code entries across scanned variables |
r(n_vars) |
Number of variables scanned |
r(varlist) |
Scanned variables |
r(top_codes) |
Matrix with frequency, percent, and cumul_pct columns |
r(chapters) |
Matrix with codes and entries columns grouped by first character |
r(top_code_#) |
Exact code value for each displayed r(top_codes) row; needed when a code is too long for a Stata matrix row name |
r(chapter_#) |
Exact leading character for each r(chapters) row, in row order; needed when the character cannot be a matrix row name and an alias is used |
The displayed tables, returned matrices, and draft codefile are ordered by descending frequency with alphabetical tie-breaking, so repeated runs over the same data are deterministic. For a code longer than Stata's 32-character matrix row-name limit, r(top_codes) uses a bounded alias; use the matching r(top_code_#) macro to recover the exact code value.
Assumptions and Limits
- The reported prevalence is the prevalence of the supplied code definition, not the prevalence of the underlying disease; positive predictive value and sensitivity of the codes determine the gap between the two.
- Scan variables must be fixed-width strings;
tostringhandles numeric variables through temporary strings, whilestrLvariables are rejected and should be converted first. - Empty strings and the bare
.placeholder are skipped as code values; withnodots, values that become empty after periods are removed are skipped too. date()andrefdate()must be numeric Stata dates, and windows require daily (%td) dates. Datetime variables (%tc,%tC) are always rejected; weekly, monthly, quarterly, half-yearly, yearly, and business-calendar variables (%tw,%tm,%tq,%th,%ty,%tb) are rejected wheneverlookback()orlookforward()is used, because windows are measured in days; without a window they are accepted andearliestdate,latestdate, andcountdatesummarize them in their own unit.- Missing
id()values are excluded fromcollapseandmerge, and rows with missing dates are excluded whenever a time window is active. collapsekeeps only ids with at least one analyzed row. An id whose rows are all excluded byif/in, a missingid(), a missing date, or the window is absent from the collapsed data and from ther(summary)prevalence denominator, so that prevalence is among ids with an analyzed row, not among the cohort. For a cohort denominator, merge the collapsed file onto the cohort and replace missing indicators with 0 where no analyzed row should count as no match. Undermerge, such ids keep their rows and receive missing (.) indicators.unmatched()andmatched_code()are row-level outputs and are not retained aftercollapse; usemergewhen they must remain with encounter rows.- Rules apply equally to every variable in the varlist; when first-listed diagnosis validity matters, scan positions separately or use distinct
generate()prefixes. - Labels are presentation text only: identifiers in
r(conditions), matrix row names, and the exportconditioncolumn remain the condition names.
Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
not a string variable |
Add tostring or convert the code variable to a fixed-width string |
collapse requires id() |
Supply the patient or entity identifier with id() |
| Window options require dates | Supply numeric daily date() and refdate() variables |
variable ... already exists |
Add replace only when overwriting the planned output is intended |
file already exists or r(602) |
Add the option-specific file suboption, for example export(results.xlsx, replace) |
| A condition matches zero observations | Check spelling, anchoring, dots, case, and whether mode(regex) or mode(prefix) is intended |
Multi-window lookback() fails |
Multiple windows require collapse or merge |
QA
QA suites and how to run them are documented in qa/README.md.
Version History
1.0.0 (2026-10-15)
- Initial SSC release.
Author
Timothy P Copeland, Karolinska Institutet
Questions and bug reports: GitHub issues. Please include the first line of which codescan and your Stata version.
To cite: Copeland TP. codescan: Scan wide-format code fields without reshaping. Stata-Tools, https://github.com/tpcopeland/Stata-Tools/tree/main/codescan. Please give the version you used (which codescan prints it).
License
MIT (see LICENSE)