Skip to contents

Guide index | Guide 4 of 5

This standalone guide uses invented diagnosis records. icd_flag() accepts local data; it neither queries a database nor chooses clinical definitions.

1. Supply the lists you want

library(thipR)
library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
records <- data.frame(
  record_id = 1:5, person = c("A", "A", "B", "C", "D"),
  pdx = c(" e11.9 ", "J06", "F20.0", NA, "J06"),
  sdx = c(NA, "F12.1", NA, "", NA)
)
conditions <- list(diabetes = "E11", cannabis = c("F12.1", "F12.2"), psychosis = "F20")

Named list elements become output columns. Empty code vectors are allowed and match no recorded codes. These lists illustrate syntax, not validated phenotypes.

2. Flag each record

flags <- icd_flag(
  records, conditions,
  id = "record_id", cols = c("pdx", "sdx"), match = "prefix"
)
records |> left_join(flags, by = "record_id")
#>   record_id person     pdx   sdx diabetes cannabis psychosis
#> 1         1      A  e11.9   <NA>     TRUE    FALSE     FALSE
#> 2         2      A     J06 F12.1    FALSE     TRUE     FALSE
#> 3         3      B   F20.0  <NA>    FALSE    FALSE      TRUE
#> 4         4      C    <NA>             NA       NA        NA
#> 5         5      D     J06  <NA>    FALSE    FALSE     FALSE

The result is a separate table: one row per unique ID, with logical condition columns. Join it explicitly if you want the original fields alongside flags.

3. Change the aggregation ID

person_flags <- icd_flag(
  records, conditions,
  id = "person", cols = c("pdx", "sdx"), match = "prefix"
)
person_flags
#>   person diabetes cannabis psychosis
#> 1      A     TRUE     TRUE     FALSE
#> 2      B    FALSE    FALSE      TRUE
#> 3      C       NA       NA        NA
#> 4      D    FALSE    FALSE     FALSE

Person A’s diagnoses are combined across rows. For entry-window records, id = "entry_id" instead combines diagnoses within each entry. Define the history/follow-up window before flagging; the function does not apply dates. Do not aggregate by person if separate entry-specific histories are needed.

4. Exact versus prefix

exact <- icd_flag(records, list(diabetes = "E11"),
                  id = "record_id", cols = "pdx", match = "exact")
prefix <- icd_flag(records, list(diabetes = "E11"),
                   id = "record_id", cols = "pdx", match = "prefix")
data.frame(record_id = exact$record_id, exact = exact$diabetes, prefix = prefix$diabetes)
#>   record_id exact prefix
#> 1         1 FALSE   TRUE
#> 2         2 FALSE  FALSE
#> 3         3 FALSE  FALSE
#> 4         4    NA     NA
#> 5         5 FALSE  FALSE

exact is the default. prefix includes literal subcodes: E11 matches E11.9. Dots, case and surrounding whitespace are normalized. Prefixes are not regular expressions; a wildcard or regex is not a substitute for a code list.

Thai ICD-10-TM codes can have a fifth character

Thai records often add a fifth character for clinical detail, for example F10.30 and F10.31 beneath the four-character F10.3. Exact matching on F10.3 silently misses those records. A code list built at four-character resolution, including lists exported from tools that only offer four-character codes, needs prefix matching to include the more detailed codes:

detail <- data.frame(id = 1:3, dx = c("F10.3", "F10.31", "F10.30"))
data.frame(
  id = detail$id, dx = detail$dx,
  exact = icd_flag(detail, list(withdrawal = "F10.3"), "id", "dx", "exact")$withdrawal,
  prefix = icd_flag(detail, list(withdrawal = "F10.3"), "id", "dx", "prefix")$withdrawal
)
#>   id     dx exact prefix
#> 1  1  F10.3  TRUE   TRUE
#> 2  2 F10.31 FALSE   TRUE
#> 3  3 F10.30 FALSE   TRUE

Before freezing a definition, tabulate which recorded codes, at which lengths and diagnosis positions, actually occur in your source.

Range notation such as "Q20-Q26", SQL/regex wildcards (%, _, ?, ^, brackets) and comma- or space-separated lists stop with an error instead of quietly matching nothing. Write the reviewed codes out explicitly:

congenital_heart <- paste0("Q", 20:26)  # Q20-Q26, reviewed
congenital_heart
#> [1] "Q20" "Q21" "Q22" "Q23" "Q24" "Q25" "Q26"

*, + and / are kept as literal characters because some sources record dagger/asterisk or similar notation.

Missingness and counting

  • TRUE: at least one selected recorded code matches.
  • FALSE: there are recorded codes but none match; this is not proof of disease absence.
  • NA: all selected codes for the ID are blank or missing.

No input row means no output ID; preserve the cohort roster using a left join. Unknown flags and absent records need an explicit study policy. Flags express presence, not frequency: requiring two admissions needs a reliable admission key and an explicit distinct-admission count, not duplicated diagnosis-row counts.

Filtering versus flagging

thip_filter_icd() is a lazy DB row filter, with a single code vector and cols. icd_flag() is a local aggregation for multiple named conditions. Use standard collect() to make the transition explicit. No automatic download or backend switch occurs. Choose primary versus secondary diagnosis columns visibly in the call.

Next: worked cohort example.