Pediatric Complex Chronic Conditions

# IMPORTANT SYNTAX NOTE:
#
# DO NOT USE the pipeOp `|>`
#
# While convenient, that is a R 4.1.0 feature at a minimum. Notable improvements
# to the pipeOp come in 4.2.0 and 4.2.1.  To keep this package dependent on R >=
# 3.5.0 do not use the pipeOp.

library(kableExtra)
options(qwraps2_markup = "markdown")
options(knitr.kable.NA = '')
knitr::opts_chunk$set(collapse = TRUE, fig.align = "center")
library(medicalcoder)

Introduction

The Pediatric Complex Chronic Condition (PCCC) coding schema version 2 was published in 2014 [@feudtner2014pediatric] and updated to version 3 in 2024 [@feinstein2024pediatric]. Both versions identify 11 conditions, each with multiple subconditions.

CNDS <- subset(get_pccc_conditions(), select = c("condition", "condition_label"))
data.table::setDT(CNDS)
data.table::setkey(CNDS, condition)
CNDS <- unique(CNDS)

tab <-
  kbl(
    CNDS,
    caption = "Syntactically valid names for complex chronic conditions",
    row.names = TRUE
  )
tab <- kable_styling(tab, bootstrap_options = c("striped"), full_width = FALSE, font_size = 10)
tab

The PCCC system provides a standardized approach to identifying children with complex chronic conditions using administrative data. This has several important implications:

Without a common framework such as the PCCC, studies of chronic pediatric conditions would be fragmented, limiting their impact on both research and practice.

PCCC Version 2.0 vs PCCC Version 3.0

Versions 2 and 3 differ mainly in how technology dependence is treated. Many ICD codes map to both a primary condition and either technology dependence or transplant.

In both versions, transplant-related codes indicate organ system failure. A patient with such a code is flagged as having both a transplant and the related condition.

Technology dependence, however, diverges between versions. In version 2, the presence of a technology dependence code classifies the patient as having both the associated condition and technology dependence. For example, ICD-10 Z46.81 is both a metabolic and technology dependence code. A patient with this code is classified as having a metabolic condition and technology dependence.

Version 3 refines this rule: technology dependence codes are assessed conditionally, recognizing that many do not reflect chronic conditions.

Example: ICD-10 Z46.81 (r subset(get_icd_codes(with.descriptions = TRUE), code == "Z4681")$desc) is a metabolic and technology dependence code. If a patient had this code in their medical records then they would be classified as having a metabolic condition and a tech dependence. Under version 2, this patient would be flagged as having a metabolic condition and technology dependence. Under version 3, the patient would only be flagged with a metabolic condition and technology dependence if at least one non-technology condition is flagged.

ICD Codes for PCCC

Let's look at the codes that are in the PCCC schema. Calling get_pccc_codes() returns a data.frame.

pccc_codes <- get_pccc_codes()
str(pccc_codes)

The columns are:

Examples

Example: Consider a patient with the following four diagnostic and two procedure ICD-9 codes:

pat1 <-
  data.frame(
    dx = c(1, 1, 1, 1, 0, 0),
    icdv = 9L,
    code = c("34590", "78065", "3432", "78065", "9929", "8606")
  )

An inner join between the pccc_codes and pat1 will yield the conditions this patient has.

merge(x = pccc_codes, y = pat1, all = FALSE, by = c("icdv", "dx", "code"))

For all PCCC variants, there is one matching dx code, 343.2, for infantile cerebral palsy, which matches a neuromuscular condition. The procedure code 86.06 matches a technology-dependent metabolic condition.

Under version 2.0 of PCCC (variants pccc_v2.0 and pccc_v2.1), this patient has two conditions: neuromuscular and metabolic. This patient also has a flag for device and technology use.

pat1_pccc_v2.0 <-
  comorbidities(
    data = pat1,
    icd.codes = "code",
    dx.var = "dx",
    icdv = 9,
    method = "pccc_v2.0",
    flag.method = "current", # default
    poa = 1                  # default for flag.method = 'current'
  )

pat1_pccc_v2.1 <-
  comorbidities(
    data = pat1,
    icd.codes = "code",
    dx.var = "dx",
    icdv = 9,
    method = "pccc_v2.1",
    flag.method = "current",
    poa = 1
  )

all.equal(pat1_pccc_v2.0, pat1_pccc_v2.1, check.attributes = FALSE)
pat1_pccc_v2.1

Under version 3 of the PCCC, this patient has two conditions: neuromuscular and metabolic. The technology dependence flags are also 1 for this patient, but are not counted in the total number of conditions.

pat1_pccc_v3.0 <-
  comorbidities(
    data = pat1,
    icd.codes = "code",
    dx.var = "dx",
    icdv = 9,
    method = "pccc_v3.0",
    flag.method = 'current',
    poa = 1
  )

pat1_pccc_v3.1 <-
  comorbidities(
    data = pat1,
    icd.codes = "code",
    dx.var = "dx",
    icdv = 9,
    method = "pccc_v3.1",
    flag.method = 'current',
    poa = 1
  )

all.equal(pat1_pccc_v3.0, pat1_pccc_v3.1, check.attributes = FALSE)

# retain the needed columns, there are four columns for each condition in v3
pat1_pccc_v3.0[, grep("^(cmrb_flag|num_cmrb|neuromus|metabolic|tech_dep_flag)", names(pat1_pccc_v3.0))]

In the output from version 3, we have four 0/1 indicator columns for each of the conditions.

The details in the list above might be easier to understand in a tabular form of possible sets. In the case of no conditions, only the <condition>_dxpr_or_tech columns are flagged as 0/1 with the <condition>_dxpr_only, <condition>_tech_only, and <condition>_dxpr_and_tech columns set to NA. When at least one condition is flagged, all the columns will be populated as 0/1.

tab <- data.table::fread(colClass = character(), text = "
cmrb_flag | num_cmrb | <condition>_dxpr_or_tech | <condition>_dxpr_only | <condition>_tech_only | <condition>_dxpr_and_tech | <other condition(s)>_dxpr_or_tech
0         | 0        | 0                        | 0                     | 0                     | 0                         | 0
1         | 1        | 0                        | 0                     | 0                     | 0                         | 1
1         | 1        | 1                        | 1                     | 0                     | 0                         | 0
1         | 1        | 1                        | 1                     | 1                     | 1                         | 0
1         | >1       | 0                        | 0                     | 0                     | 0                         | 1
1         | >1       | 1                        | 0                     | 1                     | 0                         | 1
1         | >1       | 1                        | 1                     | 0                     | 0                         | 1
1         | >1       | 1                        | 1                     | 1                     | 1                         | 1                                  ")

tab <-
  kbl(
    x = tab,
    align = rep("c", ncol(tab)),
    col.names = c("cmrb flag", "num cmrb", "_dxpr_or_tech", "_dxpr_only", "_tech_only", "_dxpr_and_tech", "_dxpr_or_tech")
  )
tab <- kable_styling(tab, bootstrap_options = c("striped"), font_size = 10)
tab <- add_header_above(tab, c("", "", "<condition>" = 4, "<other condition(s)>" = 1))
tab

Now, consider another patient, pat2, with the same codes as pat1 except for 3432, the code mapping to a neuromuscular condition.

pat2 <- subset(pat1, code != "3432")

Under version 2 of the PCCC this patient will still have metabolic and technology dependence conditions because of the code 86.06 is in the record, but will not have the neuromuscular condition.

pat2_pccc_v2.1 <-
  comorbidities(
    data = pat2,
    icd.codes = "code",
    dx.var = "dx",
    icdv = 9,
    method = "pccc_v2.1",
    flag.method = 'current',
    poa = 1
  )
Filter(f = function(x) x > 0, pat2_pccc_v2.1)

Under version 3 of the PCCC, this patient will have no conditions. This is because no condition was identified based on non-technology dependent codes and thus the one technology dependent code is ignored.

pat2_pccc_v3.1 <-
  comorbidities(
    data = pat2,
    icd.codes = "code",
    dx.var = "dx",
    icdv = 9,
    method = "pccc_v3.1",
    flag.method = 'current',
    poa = 1
  )
Filter(f = function(x) x > 0, pat2_pccc_v3.1)

Expected Data Structures

The expected input data format for comorbidities() is a "long" format. The only mandatory column is one column of ICD codes. These codes can be full codes (include the decimal point) or compact codes (omitting the decimal point). Additionally, column(s) for identifying patients, encounters, and any other important groups are encouraged. A column to indicate the ICD version (9 or 10), and another column for identifying the code as a diagnostic or procedure code are also encouraged. The example mdcr data set has three columns, a patient id (patid), the ICD compact codes (code), and a column to indicate if the ICD code is a diagnostic or procedure code, (dx: 1 for diagnostic, 0 for procedure).

The mdcr data is provided with columns for

head(mdcr)
str(mdcr)

Assessing PCCC

Ignoring ICD Version and diagnostic/procedure codes

Applying pccc_v2.1 and pccc_v3.1 methods to mdcr without accounting for ICD version or the diagnostic/procedure status of the code can be done as follows:

mdcr_results_v2.1_01 <-
  comorbidities(
    data = mdcr,
    icd.codes = "code",
    id.vars = "patid",
    poa = 1,
    flag.method = 'current',
    method = "pccc_v2.1"
  )

mdcr_results_v3.1_01 <-
  comorbidities(
    data = mdcr,
    icd.codes = "code",
    id.vars = "patid",
    poa = 1,
    flag.method = 'current',
    method = "pccc_v3.1"
  )

A useful summary of the object returned from comorbidities() is gained by calling summary(). The return is a data.frame with columns for the count and percentages. For pccc_v2.0 and pccc_v2.1 the condition, label, count, and percentage, are reported. For pccc_v3.0 and pccc_v3.1 the columns are extended to provide the counts and percentages for _dxpr_or_tech, _dxpr_only, _tech_only, and _dxpr_and_tech.

str(summary(mdcr_results_v2.1_01))
str(summary(mdcr_results_v3.1_01))

Since the summary tables are data.frames the end user may manipulate the results for reporting as they want, see the following table.

x <-
  merge(
    summary(mdcr_results_v2.1_01),
    summary(mdcr_results_v3.1_01),
    all = TRUE,
    by = c("condition", "label"),
    sort = FALSE
  )
x[["condition"]] <- NULL
tab <-
  kableExtra::kbl(
    x,
    digits = 1,
    col.names = c("", rep(c("count", "%"), times = 5)),
    caption = "Summary Table for `mdcr_results_v2.1_01` and `mdcr_results_v3.1_01`."
  )
tab <-
  kableExtra::kable_styling(tab, bootstrap_options = c("striped"), font_size = 10)
tab <-
  kableExtra::pack_rows(tab, group_label = c("Conditions"), start_row = 1, end_row = 11)
tab <-
  kableExtra::pack_rows(tab, group_label = c("Flags"), start_row = 12, end_row = 13)
tab <-
  kableExtra::pack_rows(tab, group_label = c("Total Conditions"), start_row = 14, end_row = 24)
tab <-
  kableExtra::add_header_above(tab, c("", "v2.1" = 2, "dxpr or tech" = 2, "dxpr only" = 2, "tech only" = 2, "dxpr and tech" = 2))
tab <-
  kableExtra::add_header_above(tab, c("", "", "", "v3.1" = 8))
tab

Accounting for Diagnostic/Procedure

There are additional details we should consider with respect to the ICD codes. The ICD version, 9 or 10, and if the code is a diagnostic or a procedure code. For example, code ICD-9 diagnostic code 332.1 has the same compact code as ICD-9 procedure code 33.21, 3321. In the case of the mdcr data where we have only compact codes, the need to distinguish between ICD-9 diagnostic and ICD-9 procedure is critically important. In the mdcr data the code 3321 does appear as both diagnostic and procedure.

pccc_codes[pccc_codes$code == "3321", ]
table(mdcr[mdcr$code == "3321", "dx"])

To account for the diagnostic or procedure status of the codes specify a value for the dx.var argument.

mdcr_results_v2.1_02 <-
  comorbidities(
    data = mdcr,
    id.vars = "patid",
    icd.codes = "code",
    dx.var = "dx",
    flag.method = 'current',
    poa = 1,
    method = "pccc_v2.1"
  )

mdcr_results_v3.1_02 <-
  comorbidities(
    data = mdcr,
    id.vars = "patid",
    icd.codes = "code",
    dx.var = "dx",
    flag.method = 'current',
    poa = 1,
    method = "pccc_v3.1"
  )

Specificity is increased by using the diagnostic/procedure flag. Using pccc_v2.1 there are r sum(mdcr_results_v2.1_02$cmrb_flag != mdcr_results_v2.1_01$cmrb_flag) false positive flags when the diagnostic/procedure flag is omitted from the comorbidities() call. Using pccc_v3.1 there are r sum(mdcr_results_v3.1_02$cmrb_flag != mdcr_results_v3.1_01$cmrb_flag) false positive flags when the diagnostic/procedure flag is omitted from the comorbidities() call.

# verify that the cmrb_flag and number of conditions is the same or less after
# accounting for the diagnostic/procedure flag in the comorbidities call
stopifnot(all(mdcr_results_v2.1_02$cmrb_flag <= mdcr_results_v2.1_01$cmrb_flag))
stopifnot(all(mdcr_results_v2.1_02$num_cmrb  <= mdcr_results_v2.1_01$num_cmrb))

sum(mdcr_results_v2.1_02$cmrb_flag != mdcr_results_v2.1_01$cmrb_flag)
sum(mdcr_results_v2.1_02$num_cmrb  != mdcr_results_v2.1_01$num_cmrb)

stopifnot(all(mdcr_results_v3.1_02$cmrb_flag <= mdcr_results_v3.1_01$cmrb_flag))
stopifnot(all(mdcr_results_v3.1_02$num_cmrb  <= mdcr_results_v3.1_01$num_cmrb))

sum(mdcr_results_v3.1_02$cmrb_flag != mdcr_results_v3.1_01$cmrb_flag)
sum(mdcr_results_v3.1_02$num_cmrb  != mdcr_results_v3.1_01$num_cmrb)

Let's explore the record for patient 87420.

# this chunk is not included, just test that the results for patient 87420 has
# not changed.
#
# NOTE: vignettes are built in one R session.  As a result, in a prior version
# of this package, mdcr was modified in the icd.Rmd script. Those changes
# persisted into this vignette and resulted in an error.  The fix was to use an
# object in the icd.Rmd vignette called mdcr_copy.
subset(mdcr, patid %in% mdcr[mdcr$code == "5641" & mdcr$dx == 1, "patid"])
pat87420 <- subset(mdcr, patid == 87420)
stopifnot(
  isTRUE(
    all.equal(
      pat87420,
      structure(list(patid = c(87420L, 87420L), icdv = c(9L, 9L), code = c("78321", "5641"), dx = c(1L, 1L)), row.names = 4073:4074, class = "data.frame"),
      check.attributes = FALSE
    )
  )
)
subset(mdcr, patid == "87420")
subset(get_pccc_codes(), code %in% c("78321", "5641"))
subset(mdcr_results_v2.1_01, patid == "87420", select = c("cmrb_flag", "renal"))
subset(mdcr_results_v2.1_02, patid == "87420", select = c("cmrb_flag", "renal"))

subset(mdcr_results_v3.1_01, patid == "87420", select = c("cmrb_flag", "renal_dxpr_or_tech"))
subset(mdcr_results_v3.1_02, patid == "87420", select = c("cmrb_flag", "renal_dxpr_or_tech"))

subset(get_icd_codes(with.descriptions = TRUE), full_code %in% c("56.41", "564.1"))

In the above, the compact code "5641" matches procedure code 56.41 for a renal condition. In mdcr_results_v2.1_01 and mdcr_results_v3.1_01 where no distinction was made between diagnostic and procedure codes this patient was flagged as having a renal condition. However, when reviewing the patient record, the compact code "5641" is listed as a diagnostic criteria and the full code 564.1 is for r subset(get_icd_codes(with.descriptions = TRUE), full_code == "564.1" & desc_end == 2015)$desc. This is an example of where discriminating between diagnostic and procedure codes is critically important when looking for complex chronic conditions.

If we explicitly look at an inner join between this patient's data and the pccc lookup table we see that the code 5641 matches the procedure code in the pccc lookup table. By not accounting for diagnostic and procedure codes, the overlaps between the two coding structures can lead to false positives.

merge(
  x = subset(mdcr, patid == "87420"),
  y = pccc_codes,
  by.x = c("code"),
  by.y = c("code"),
  suffixes = c(".mdcr", ".pccc_codes")
)

Full and Compact ICD codes

Using full codes can prevent false positives too. Here are several different ways that comorbidities() could be called resulting in different outcomes.

Note: this is a good example of how medicalcoder can handle full and compact codes within a single record.

DF <-
  data.frame(
    id = c("full dx", "full pr", "compact dx", "compact pr"),
    code = c("564.1", "56.41", "5641", "5641"),
    dx = c(1, 0, 1, 0)
  )

# ideal: using the dx/pr status and matching on full and compact codes.
comorbidities(
  data = DF,
  id.vars = "id",
  dx.var = "dx",
  icd.codes = "code",
  poa = 1,
  method = "pccc_v3.1"
)[, c("id", "cmrb_flag", "renal_dxpr_or_tech")]

# false positive for the compact dx
comorbidities(
  data = DF,
  id.vars = "id",
  icd.codes = "code",
  poa = 1,
  method = "pccc_v3.1"
)[, c("id", "cmrb_flag", "renal_dxpr_or_tech")]

# false negative for compact pr
comorbidities(
  data = DF,
  id.vars = "id",
  icd.codes = "code",
  poa = 1,
  full.code = TRUE,
  compact.codes = FALSE,
  method = "pccc_v3.1"
)[, c("id", "cmrb_flag", "renal_dxpr_or_tech")]

# false positive for compact dx
comorbidities(
  data = DF,
  id.vars = "id",
  icd.codes = "code",
  poa = 1,
  full.code = FALSE,
  compact.codes = TRUE,
  method = "pccc_v3.1"
)[, c("id", "cmrb_flag", "renal_dxpr_or_tech")]

# false negatives for compact and full pr
comorbidities(
  data = DF,
  id.vars = "id",
  icd.codes = "code",
  dx.var = "dx",
  poa = 1,
  full.code = FALSE,
  compact.codes = TRUE,
  method = "pccc_v3.1"
)[, c("id", "cmrb_flag", "renal_dxpr_or_tech")]

Accounting for ICD Version

Another consideration is the version of ICD, 9 or 10.

The record for patid 95471 is a great example of the problem that a compact code can cause. "E030" matches ICD-9 dx compact and full code E030 (no decimal point), and matches the ICD-10 dx compact code for full code E03.0 with only the ICD-10 version being related to a chronic complex condition.

Inputs to the comorbidities() call for the ICD version will impact the output. When calling comorbidities() with a variable to indicate the ICD version NA values will not be joined against and the codes are ignored resulting in no condition being flagged. If we know that we only want to compare against ICD-9 or ICD-10 values then using the icdv argument can simplify the call and in this case, no condition for ICD-9 and a condition is flagged for ICD-10.

subset(mdcr, patid == "95471")

# no flag becuse icdv = 9 which treats all input codes as ICD-9
comorbidities(
  data = subset(mdcr, patid == "95471"),
  icd.codes = "code",
  id.vars = 'patid',
  dx.var = "dx",
  icdv = 9L,
  poa = 1,
  method = "pccc_v3.1"
)[, c('patid', 'cmrb_flag')]

# flag because icdv = 10 - same as using `icdv.var = "icdv"`
comorbidities(
  data = subset(mdcr, patid == "95471"),
  icd.codes = "code",
  id.vars = 'patid',
  dx.var = "dx",
  icdv = 10L,
  poa = 1,
  method = "pccc_v3.1"
)[, c('patid', 'cmrb_flag')]

comorbidities(
  data = subset(mdcr, patid == "95471"),
  icd.codes = "code",
  id.vars = 'patid',
  dx.var = "dx",
  icdv.var = "icdv",
  poa = 1,
  method = "pccc_v3.0"
)[, c('patid', 'cmrb_flag')]

Lastly, it should be noted that a lot of the ambiguity resulting from compact codes can be avoided when full codes are available. medicalcoder can handle both forms. In the example below we again use the "E030" and assess it against all full and compact codes (default), against only full codes, and lastly against only compact codes. Note in this example that we are not specifying the ICD version nor the diagnostic/procedure status of the code.

lookup_icd_codes("E030")
data <- data.frame(id = c("Ambiguous compact code", "Full ICD-9 code", "Full ICD-10 code"),
                   code  = c("E030", "E030", "E03.0"))
data

args <-
  list(
    data = data,
    id.vars = "id",
    icd.codes = "code",
    poa = 1,
    method = "pccc_v3.1"
  )

default <-
  do.call(comorbidities, c(args, list(full.codes = TRUE,  compact.codes = TRUE )))
full_only <-
  do.call(comorbidities, c(args, list(full.codes = TRUE,  compact.codes = FALSE)))
compact_only <-
  do.call(comorbidities, c(args, list(full.codes = FALSE, compact.codes = TRUE )))

default[,       c("id", "cmrb_flag")]
full_only[,     c("id", "cmrb_flag")]
compact_only[,  c("id", "cmrb_flag")]

With no information about the "E030" being ICD-9 or ICD-10, full or compact, (can only be a diagnostic code in either ICD-9 or ICD-10) we get different flags. The default, the most liberal approach flags this example patient as having a condition in all cases. When only considering the code to be a full code, then only the ICD-10 version matches. When only considering the compact codes the flag is true for the ambiguous version and the ICD-9 full version since ICD-9 E030 is a full code with the same compact form.

Longitudinal Conditions

The medicalcoder package includes the example data set, mdcr_longitudinal, with ICD-9 and ICD-10 codes for r length(unique(mdcr_longitudinal$patid)) synthetic patients with multiple encounters. Each row has a date (encounter) for when the ICD code was reported.

head(mdcr_longitudinal)

PCCC V2

Let's look at the pccc_v2.1 flags for each patient, using all the information from all the encounters. This can easily be done by specifying id.vars = c("patid") such that the comorbidities() method considers all codes as occurring on one encounter.

longitudinal_v2_patid <-
  comorbidities(
    data = mdcr_longitudinal,
    icd.codes = "code",
    id.vars = c("patid"),
    icdv.var = "icdv",
    method = "pccc_v2.1",
    flag.method = "current",
    poa = 1
  )
tab <- kableExtra::kbl(longitudinal_v2_patid)
tab <- kableExtra::kable_styling(tab, bootstrap_options = c("striped"), font_size = 10)
tab

We can look at the conditions flagged at each encounter by specifying the id.vars = c("patid", "date").

longitudinal_v2_patid_date <-
  comorbidities(data = mdcr_longitudinal,
    icd.codes = "code",
    id.vars = c("patid", "date"),
    icdv.var = "icdv",
    method = "pccc_v2.1",
    flag.method = "current",
    poa = 1
  )
tab <-
  kableExtra::kbl(
    subset(longitudinal_v2_patid_date, patid == "9663901"),
    row.names = FALSE
  )
tab <- kableExtra::kable_styling(tab, bootstrap_options = c("striped"), font_size = 10)
tab

Looking at patid 9663901 at an encounter level we see that the conditions occur at different moments in time and the condition the patient has change overtime. Because these are chronic conditions, once the condition is observed, it should be considered to exist in perpetuity.

For pccc_v2.1 a simple carry-forward method can be applied to the data set to mark the presence of a condition at the time of reporting and thereafter.

longitudinal_v2_patid_date_cumulative_poa0 <-
  comorbidities(
    data = mdcr_longitudinal,
    icd.codes = "code",
    id.vars = c("patid", "date"),
    icdv.var = "icdv",
    method = "pccc_v2.1",
    flag.method = "cumulative",
    poa = 0
  )
tab <-
  kableExtra::kbl(
    subset(longitudinal_v2_patid_date_cumulative_poa0, patid == "9663901"),
    row.names = FALSE
  )
tab <- kableExtra::kable_styling(tab, bootstrap_options = c("striped"), font_size = 10)
tab
longitudinal_v2_patid_date_cumulative_poa1 <-
  comorbidities(
    data = mdcr_longitudinal,
    icd.codes = "code",
    id.vars = c("patid", "date"),
    icdv.var = "icdv",
    method = "pccc_v2.1",
    flag.method = "cumulative",
    poa = 1
  )
tab <-
  kableExtra::kbl(
    subset(longitudinal_v2_patid_date_cumulative_poa1, patid == "9663901"),
    row.names = FALSE
  )
tab <- kableExtra::kable_styling(tab, bootstrap_options = c("striped"), font_size = 10)
tab

PCCC V3

For pccc_v3.0 and pccc_v3.1 a simple carry-forward method would not be easy to use as information about technology dependent codes is omitted when non-technology dependent codes do not exist.

Let's use three ICD-10 diagnostic codes for this example and we will explore all six possible permutations of the codes. We'll generate a data set with seven encounters and one code appearing on each of encounters 2, 4, and 6.

The codes we'll use are:

codes <- c("H49.811", "J84.111", "Z96.41")
subset(get_pccc_codes(), full_code %in% codes)

The constructed data and permutations are:

permutations <-
  data.table::data.table(
    permutation = rep(1:6, each = 7),
    encounter_id = rep(1:7, times = 6),
    code =
      codes[c(NA, 1, NA, 2, NA, 3, NA,
              NA, 1, NA, 3, NA, 2, NA,
              NA, 2, NA, 1, NA, 3, NA,
              NA, 2, NA, 3, NA, 1, NA,
              NA, 3, NA, 1, NA, 2, NA,
              NA, 3, NA, 2, NA, 1, NA)]
  )

permutations[, plabel := paste(na.omit(code), collapse = ", "), by = .(permutation)]
permutations[, plabel := paste0("Permutation ", permutation, ": ", plabel)]
str(permutations, vec.len = 1)
cat(paste("*", permutations[, unique(plabel)]), sep = "\n")

We'll apply the pccc_v3.1 to this code set with flag.method = "cumulative" and all codes considered to be present-on-admission.

rtn <-
  comorbidities(
    data = permutations,
    icd.codes = "code",
    id.vars = c("permutation", "plabel", "encounter_id"),
    icdv = 10L,
    compact.codes = FALSE,
    method = "pccc_v3.1",
    flag.method = "cumulative",
    poa = 1
  )
rtn_wide <-
  data.table::dcast(
    encounter_id ~ plabel,
    data = rtn,
    value.var = c("metabolic_dxpr_or_tech", "metabolic_dxpr_only",
                  "metabolic_tech_only", "metabolic_dxpr_and_tech",
                  "respiratory_dxpr_or_tech", "respiratory_dxpr_only",
                  "respiratory_tech_only", "respiratory_dxpr_and_tech",
                  "cmrb_flag", "num_cmrb")
  )
pkbl <- function(permutation = 1) {
  stopifnot(length(permutation) == 1L)

  x <- rtn_wide[
        ,
        .SD,
        .SDcols = c("encounter_id",
                    grep(paste0("Permutation ", permutation), names(rtn_wide), value = TRUE))
       ]

  pl <- rtn[["plabel"]][rtn$permutation == permutation][1]

  tab <-
    kableExtra::kbl(
      x,
      col.names = c("encounter_id", rep(c("dxpr or tech", "dxpr only", "tech only", "dxpr and tech"), times = 2), "ccc flag", "num ccc")
    )
  tab <- kableExtra::kable_styling(kable_input = tab, bootstrap_options = c("striped"), font_size = 10)
  tab <- kableExtra::add_header_above(kable_input = tab, header = c("", c("Metabolic" = 4, "Respiratory" = 4), "", ""))
  tab <- kableExtra::add_header_above(kable_input = tab, header = c("", setNames(10, pl)))
  tab
}

Let's walk through the results for each permutation.

Permutation 1

pkbl(1)

The first code to appear in this permutation is H49.811, metabolic (other). This is a diagnostic code and will flag the metabolic condition for encounters 2 through 7 as _dxpr_or_tech. The Z96.41 code, metabolic (tech), appears on encounter 6. Thus, for encounters 2 through 5 metabolic should be flagged as _dxpr_or_tech = 1, dxpr_only = 1, tech_only = 0, and dxpr_and_tech = 0. Encounters 6 and 7 then have dxpr_only = 0 and tech_only = 0 with dxpr_and_tech = 1. The J84.111 for respiratory is a non-tech code appearing on encounter 4 and should flag as dxpr_or_tech = 1, dxpr_only = 1, tech_only = 0, and dxpr_and_tech = 0 for encounters 4 through 7.

Permutation 2

pkbl(2)

As with permutation 1, having the non-tech dependent metabolic code H49.811 appearing on encounter 2 means that metabolic is flagged for encounters 2 through 7. What should differ is that dxpr_only is 1 for encounters 2 and 3, with dxpr_and_tech flagging for encounters 4 through 7. Lastly, the non-tech code J84.111 for respiratory condition flagging as dxpr_or_tech = dxpr_only = 1 for encounters 6 and 7.

Permutation 3

pkbl(3)

Permutation three has respiratory flagged for encounters 2 through 7. The non-tech metabolic code on encounter 4 results in the flagging of metabolic for encounters 4 through 7.

Permutation 4

pkbl(4)

Permutation 4 is notable as presence of the respiratory condition on encounters 2 through 7 means that when the technology dependent metabolic code appears on encounter 4, a metabolic is flagged for encounters 4 through 7. Compare this with permutations 5 and 6.

Permutation 5

pkbl(5)

For permutation 5 the first code is a tech dependent metabolic code on encounter 2. Because the only code for flagging a condition is a technology dependent code the PCCC version 3 algorithm results in no condition being flagged for encounters 2 and 3. On encounter 4, when the non-tech metabolic code appears then the metabolic condition is flagged and the past history of the technology dependent code persists.

Permutation 6

pkbl(6)

As with permutation 5, since the only code in the record for encounter 2 and 3 is the technology dependent metabolic code, there is no flagged condition. On encounter 4, when the dxpr code for a respiratory condition is reported then the respiratory condition and the metabolic condition is flagged as technology dependent. Note that technology only conditions are flagged if at least one other condition is flagged.

Subconditions

The documentation for PCCC version 2 [@feudtner2014pediatric] and version 3 [@feinstein2024pediatric] include subconditions under each of the major conditions. However, to our knowledge, no software prior to medicalcoder implemented flagging for the subconditions.

The subconditions for each condition are shown in the next table.

SCNDS <- get_pccc_conditions()
data.table::setDT(SCNDS)
data.table::setkey(SCNDS, condition, subcondition)
SCNDS[, condition := paste(condition, condition_label, sep = ": ")]

tab <-
  kableExtra::kbl(SCNDS[, .(subcondition, subcondition_label)],
      capttion = "Syntactically valid names for subconditions",
      row.names = FALSE
  )
tab <-
  kableExtra::pack_rows(tab, index = table(SCNDS$condition))
tab <-
  kableExtra::kable_styling(tab, bootstrap_options = c('striped'), fixed_thead = TRUE, font_size = 10)
tab <-
  kableExtra::scroll_box(tab, height = "300px")
tab

To get the subconditions all you need to do is use the subconditions = TRUE argument in the comorbidities() call. For this example we will apply pccc_v3.1 with and without comorbidities.

without_subconditions <-
  comorbidities(
    data = mdcr,
    id.vars = "patid",
    icd.codes = "code",
    icdv.var = "icdv",
    dx.var = "dx",
    poa = 1,
    method = "pccc_v3.1",
    subconditions = FALSE
  )

with_subconditions <-
  comorbidities(
    data = mdcr,
    id.vars = "patid",
    icd.codes = "code",
    icdv.var = "icdv",
    dx.var = "dx",
    poa = 1,
    method = "pccc_v3.1",
    subconditions = TRUE
  )

The structure of the return object with_subconditions is a list with two elements. The first element, conditions, is identical to the results of calling comorbidities() with subconditions = FALSE.

with_subconditions

all.equal(
  with_subconditions$conditions,
  without_subconditions,
  check.attributes = FALSE
)

The second element of with_subconditions is list of data.frames, one for each condition, with indicators for only those with the condition.

A quick and easy way to get a summary of the subconditions is to call summary().

str(summary(with_subconditions))

The subconditions are available for all pccc variants. A summary is presented in the following tables.

args <-
  list(
    data = mdcr,
    id.vars = "patid",
    icd.codes = "code",
    icdv.var = "icdv",
    dx.var = "dx",
    poa = 1,
    subconditions = TRUE
  )
with_subconditions_v2.0 <- do.call(comorbidities, c(args, list(method = "pccc_v2.0")))
with_subconditions_v2.1 <- do.call(comorbidities, c(args, list(method = "pccc_v2.1")))
with_subconditions_v3.0 <- do.call(comorbidities, c(args, list(method = "pccc_v3.0")))
with_subconditions_v3.1 <- do.call(comorbidities, c(args, list(method = "pccc_v3.1")))
rslts <-
  merge(
    merge(
      summary(with_subconditions_v2.0),
      summary(with_subconditions_v2.1),
      by = c("condition", "subcondition"),
      suffixes = c("_v2.0", "_v2.1"),
      sort = FALSE
    ),
    merge(
      summary(with_subconditions_v3.0),
      summary(with_subconditions_v3.1),
      by = c("condition", "subcondition"),
      suffixes = c("_v3.0", "_v3.1"),
      sort = FALSE
    ),
    by = c("condition", "subcondition"),
    sort = FALSE
  )

rslts$idx <- 1:nrow(rslts)

rslts <-
  merge(rslts,
        unique(get_pccc_conditions()[c("condition", "condition_label")]),
        all = TRUE,
        by = "condition",
        sort = FALSE)
rslts <-
  merge(rslts,
        unique(get_pccc_conditions()[c("subcondition", "subcondition_label")]),
        all = TRUE,
        by = "subcondition",
        sort = FALSE)
rslts$lab <- rslts$subcondition_label
rslts$lab[is.na(rslts$subcondition)] <- rslts$condition_label[is.na(rslts$subcondition)]
rslts <- rslts[order(rslts$idx), ]

rslts <- split(rslts, f = rslts[["condition"]])

cols_to_keep <-
  c(
    "lab",
    "count_v2.0",
    "percent_of_cohort_v2.0",
    "percent_of_those_with_condition_v2.0",
    "count_v2.1",
    "percent_of_cohort_v2.1",
    "percent_of_those_with_condition_v2.1",
    "count_v3.0",
    "percent_of_cohort_v3.0",
    "percent_of_those_with_condition_v3.0",
    "count_v3.1",
    "percent_of_cohort_v3.1",
    "percent_of_those_with_condition_v3.1"
  )

tabs <- lapply(rslts, function(x) {x[, cols_to_keep]})

tabs <-
  lapply(tabs,
    kableExtra::kbl,
    col.names = c("", rep(c("count", "% of cohort", "% of those with condition"), 4)),
    row.names = FALSE,
    digits = 1
  )
tabs <-
  Map(
    f = function(t,r) {
          x <- kableExtra::column_spec(t, column = 1, bold = is.na(r$subcondition))
          kableExtra::add_indent(x, which(!is.na(r$subcondition)))
        },
    t = tabs,
    r = rslts
  )
tabs <-
  lapply(tabs, kableExtra::kable_styling, bootstrap_options = "striped", font_size = 10)
tabs <-
  lapply(tabs, kableExtra::add_header_above, header = c("", "v2.0" = 3, "v2.1" = 3, "v3.0" = 3, "v3.1" = 3))
tabs

Congenital or Genetic

tabs[["congeni_genetic"]]

Cardiovascular

tabs[["cvd"]]

Gastrointestinal

tabs[["gi"]]

Hematologic

tabs[["hemato_immu"]]

Malignancy

tabs[["malignancy"]]

Metabolic

tabs[["metabolic"]]

Miscellaneous

tabs[["misc"]]

Neonatal

tabs[["neonatal"]]

Neuromuscular

tabs[["neuromusc"]]

Renal

tabs[["renal"]]

Respiratory

tabs[["respiratory"]]

Longitudinal

The longitudinal assessment for subconditions work as well. Using the same permutations data set from above we will look at the metabolic and respiratory conditions and subconditions.

rslts <-
  comorbidities(
    data = permutations,
    icd.codes = "code",
    id.vars = c("permutation", "plabel", "encounter_id"),
    icdv = 10L,
    compact.codes = FALSE,
    method = "pccc_v3.1",
    flag.method = "cumulative",
    poa = 1,
    subconditions = TRUE
  )

Let's start by looking at the respiratory results. The only subcondition that should be, and is, flagged is chronic respiratory diseases. A reminder: the data.frame for a subcondition only report rows for when the primary condition was flagged. We see in the following encounters where the chronic respiratory disease is flagged is consistent with when the primary respiratory condition is flagged.

all(rslts$subconditions$respiratory$chronic_respiratory_diseases == 1)
sapply(rslts$subconditions$respiratory[, -(1:3)], max)

# which encounters flag for primary condition respiratory?
cnd <-
  rslts$conditions[
    respiratory_dxpr_or_tech == 1,
    .(cencid = paste(encounter_id, collapse = ", ")),
    by = .(plabel)
  ]


# which encounters flag for the subcondition chronic_respiratory_diseases?
scnd <-
  rslts$subconditions$respiratory[
    ,
    .(sencid = paste(encounter_id, collapse = ", ")),
    by = .(plabel)
  ]
tab <-
  kableExtra::kbl(
    merge(cnd, scnd, all = TRUE, by = "plabel"),
    caption = "Encounters flagging for respiratory condition and the chronic respiratory disease subcondition.",
    col.names = c("", "Condition", "Subcondition")
  )
tab <- kableExtra::kable_styling(tab, bootstrap_options = c("striped"), font_size = 10)
tab <- kableExtra::add_header_above(tab, c("", "Encounters" = 2))
tab

For the metabolic condition we have two subconditions to look at, 1) device and technology use, and 2) other metabolic disorders.

# which encounters flag for primary condition metabolic?
cnd <-
  rslts$conditions[
    metabolic_dxpr_or_tech == 1,
    .(cencid = paste(encounter_id, collapse = ", ")),
    by = .(plabel)
  ]

# which encounters flag for the subconditions?
scnd <-
  data.table::melt(
    rslts$subconditions$metabolic,
    id.vars = c("plabel", "encounter_id"),
    measure.vars = c("device_and_technology_use", "other_metabolic_disorders"),
    variable.factor = FALSE,
    variable.name = "subcondition"
  )
scnd <- scnd[value == 1]
scnd <-
  scnd[
    ,
    .(sencid = paste(encounter_id, collapse = ", ")),
    by = .(plabel, subcondition)
  ]

scnd <-
  data.table::dcast(
    scnd,
    plabel ~ subcondition,
    value.var = "sencid"
  )
tab <-
  kableExtra::kbl(
    x = merge(cnd, scnd, all = TRUE, by = "plabel"),
    caption = "Encounters flagging for a metabolic condition and the encounters flagging for subconidtions device and technology use and/or other metabolic disorders.",
    col.names = c("", "Condition", "Device and Technology Use", "Other Metabolic Disorders")
  )
tab <- kableExtra::kable_styling(tab, bootstrap_options = c("striped"), font_size = 10)
tab <-
  kableExtra::add_header_above(tab, c("", "Encounters" = 3))
tab

References



Try the medicalcoder package in your browser

Any scripts or data that you put into this service are public.

medicalcoder documentation built on Feb. 22, 2026, 5:08 p.m.