# Introducing icd9: working with ICD-9 codes and comorbidities in R

#### Jack O. Wasey

#### 2016-04-14

## Introduction

Calculate comorbidities, and perform fast and accurate validation, conversion, manipulation, filtering and comparison of ICD-9-CM (clinical modification) codes. ICD-9 codes appear numeric but leading and trailing zeroes, and both decimal and non-decimal “short” format codes exist. The package enables a work flow from raw lists of ICD-9 codes from hospital billing databases to comorbidities. ICD-9 to comorbidity mappings from Quan (Deyo and Elixhauser versions), Elixhauser and AHRQ included.

When calcuating which patients have which comorbidities, the input data is typically structured as follows:

    #>   visitId  icd9  poa
    #> 1    1000 40201    Y
    #> 2    1000  2258 <NA>
    #> 3    1000  7208    N
    #> 4    1000 25001    Y
    #> 5    1001 34400    X
    #> 6    1001  4011    Y
    #> 7    1002  4011    E

or

    #>   visit_id   DX1   DX2   DX3   DX4   DX5   DX6   DX7   DX8   DX9  DX10
    #> 1        7 27801 03842 51881 41519 99591 42842  5849  5609  6826  5853
    #> 2       10 71526 25000 42830  4280  4019  4241   311 49390  2724 73300
    #> 3       13 71535 59651 78052 27800 V8537   311  4019 53081 56400
    #> 4       16 71535 49390 53081 27800  V140  V141  V142  V160 V8538
    #> 5       37 71536  4241  2859  2720  4414 53081 V5866
    #> 6       41 71536 V1259 V1582  V160  V171

Of course, in real life, there are many problems with the data, such is `NA` entries, out-of-order `visitId`s, non-existent or invalid ICD-9 codes, etc.. Tools are provided to clean up the mess, and discover the comorbidities before admission or after discharge for these patients (or simply for any list of patients and ICD-9 codes: there is no requirement to work in the hospital admission paradigm.) The optional `poa` field indicates whether the code was determined to be present on arrival. The implicit default is to ignore it, and give ICD-9 code regardless of POA status, but filtering functions are provided and demonstrated later in this vignette.

The comorbidities can be determined as follows (showing the first few columns for brevity):

    icd9ComorbidAhrq(patientData)[, 1:8]
    #>        CHF Valvular  PHTN   PVD  HTN Paralysis NeuroOther Pulmonary
    #> 1000  TRUE    FALSE FALSE FALSE TRUE     FALSE      FALSE     FALSE
    #> 1001 FALSE    FALSE FALSE FALSE TRUE      TRUE      FALSE     FALSE
    #> 1002 FALSE    FALSE FALSE FALSE TRUE     FALSE      FALSE     FALSE

or

    icd9ComorbidQuanDeyo(patientData)[, 1:8]
    #>         MI   CHF   PVD Stroke Dementia Pulmonary Rheumatic   PUD
    #> 1000 FALSE  TRUE FALSE  FALSE    FALSE     FALSE     FALSE FALSE
    #> 1001 FALSE FALSE FALSE  FALSE    FALSE     FALSE     FALSE FALSE
    #> 1002 FALSE FALSE FALSE  FALSE    FALSE     FALSE     FALSE FALSE

and things work beautifully using magrittr `%>%` to chain functions together. This is not a dependency for this package, but is recommended because of the frequent need to chain together icd9 commands, and greater clarity.

    patientData %>% icd9FilterPoaYes %>% icd9ComorbidAhrq %>% extract(1:8)
    #> [1]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE

## Converting ICD-9 codes between types

ICD-9 codes are usually presented in decimal format (beware, for this is not a number), e.g. `003.21`, whereas most electronic records seem to use the short form without a decimal place. These are not interchangeable simply by removing the decimal place, and great care is taken to do this correctly. The functions were also designed to deal with the common problem of incorrectly formatted ICD-9 codes. The assumption is made that short codes of three or fewer characters are describing only the ‘major’ part: there is no other reasonable interpretation. For example, `020` must be taken to mean `20`, not `2.0` or even `0.20`. In most cases, when icd9 works on ICD-9 codes, it will convert any codes of fewer than three characters into zero-padded three-digit codes.

    icd9DecimalToShort(c("1", "10.20", "100", "123.45"))
    #> [1] "001"   "01020" "100"   "12345"
    icd9ShortToDecimal(c("1", "22", "2244", "1005"))
    #> [1] "001"   "022"   "224.4" "100.5"

    # similar operations with magrittr, also showing invalid codes
    codes <- c("87.65", "9999", "Aesop", -100, "", NA)
    icd9DecimalToShort(codes)
    #> [1] "08765" ""      ""      ""      ""      NA

## Validation of ICD-9 codes

    icd9IsValidDecimal("V10.2")
    #> [1] TRUE
    icd9IsValidShort(c("099.17", "-1"))
    #> [1] FALSE FALSE
    icd9IsValidDecimal(c("099.17", "-1.1"))
    #> [1]  TRUE FALSE
    icd9IsValidShort(c("1", "001", "100", "123456", "003.21"))
    #> [1]  TRUE  TRUE  TRUE FALSE FALSE

Validation forces the package user to provide character format ICD-9 codes. If great care is taken, passing some integers could be valid, but given the high chance of mistakes, and the simplicity of dealing entirely with character input, character or factor is enforced. Furthermore, integer values could never include V or E codes (and all ICD-10 codes have non-numeric characters).

    #icd9IsValidShort(100) # gives an error

## Ranges of ICD-9 codes

These functions generate syntactically valid ICD-9 codes, without including parent codes when the range limit would subset the parent. E.g. `"100.99" %i9da% "101.01"` does not include `100` or `100.0`, both of which imply larger subsets than requested by the range command (i.e. every code up to `100.99`). The shorter forms `%i9s%` and `%i9d%` return only real codes (i.e. listed in the CMS definitions as either three-digit codes or diagnoses), whereas `%i9sa%` and `%i9da%` return all possible syntactically valid ICD-9 codes:

    # get all possible codes
    "003" %i9sa% "0033" %>% head(9) # show first 9 of 111 values
    #> [1] "0030"  "00300" "00301" "00302" "00303" "00304" "00305" "00306" "00307"
    # just get the ones which correspond to diagnoses (keeping the 3-digit chapters)
    "494" %i9s% "4941"
    #> [1] "494"  "4940" "4941"

    "10099" %i9sa% "10101"
    #> [1] "10099" "10100" "10101"
    "V10" %i9da% "V10.02"
    #> [1] "V10.00" "V10.01" "V10.02"
    "E987" %i9da% "E988.1"
    #>  [1] "E987"   "E987.0" "E987.1" "E987.2" "E987.3" "E987.4" "E987.5"
    #>  [8] "E987.6" "E987.7" "E987.8" "E987.9" "E988.0" "E988.1"

    # can't range between different types:
    # "V10" %i9s% "E800" # throws an error

This is used internally to interpret ranges of ICD-9 codes specified in the literature. Sometimes it is not clear exactly what an ICD-9 range presented in a paper means, but at least we can explicitly decide what should be included in our interpretation, and the ranges can be reused even when the underlying codes may be different, as codes are added and removed from time-to-time, and although the original papers would have been based on their ICD-9 ranges resolving to a specific set of codes, they are likely to be valid for new diagnoses in the given subgroups. Ultimately, this needs detailed attention, but the strategy in is to give a good best guess, given these limitations.

Another way of specifying ranges are to use function calls. These are exactly equivalent to the `%i9s%` and `%i9d%` range operators. This example shows the result when the user specifies a range which would include parents but not all their children:

    icd9ExpandRangeShort("4820", "4823") # default, equivalent to %i9s%
    #> [1] "4820"  "4821"  "4822"  "4823"  "48230" "48231" "48232" "48239"
    icd9ExpandRangeShort("4820", "4823", onlyReal = FALSE)
    #>  [1] "4820"  "48200" "48201" "48202" "48203" "48204" "48205" "48206"
    #>  [9] "48207" "48208" "48209" "4821"  "48210" "48211" "48212" "48213"
    #> [17] "48214" "48215" "48216" "48217" "48218" "48219" "4822"  "48220"
    #> [25] "48221" "48222" "48223" "48224" "48225" "48226" "48227" "48228"
    #> [33] "48229" "4823"  "48230" "48231" "48232" "48233" "48234" "48235"
    #> [41] "48236" "48237" "48238" "48239"
    # see the first few differences (which are by definition not 'real' codes):
    setdiff(icd9ExpandRangeShort("4820", "4823", onlyReal = FALSE),
            icd9ExpandRangeShort("4820", "4823")) %>% head
    #> [1] "48200" "48201" "48202" "48203" "48204" "48205"

It is easy to find the children of a higher-level ICD-9 code:

    icd9Children("391")
    #> [1] "391"  "3910" "3911" "3912" "3918" "3919"
    # mid-level code
    icd9Children("0032")
    #> [1] "0032"  "00320" "00321" "00322" "00323" "00324" "00329"
    # leaf node has no children
    icd9Children("00321")
    #> [1] "00321"
    # pneumococcal pneumonia is a three-digit code with no descendants
    icd9Children("481")
    #> [1] "481"

By adding `onlyReal = TRUE`, all syntactically valid ICD-9 codes are returned, even if not defined by CMS as diagnoses. This is relevant because of minor coding errors, or coding in a different year to the master list. A planned feature is to allow testing of an ICD-9 code against the valid codes for the year it was entered, but at present only the 2014 master list is used. This means that some older valid codes may no longer be on the list. However, there have been very few changes to ICD-9-CM in the last five years with ICD-10-CM in the wings.

    # first ten possible ICD-9 child codes from 391
    icd9Children("391", onlyReal = FALSE)[1:10]
    #>  [1] "391"   "3910"  "39100" "39101" "39102" "39103" "39104" "39105"
    #>  [9] "39106" "39107"

## Decoding ICD-9 codes to descriptions

There are various ways of extracting the description of the condition described by an ICD-9 code. the icd9Explain group of functions return a data frame with a column for the ICD-9 code, a column for the full length Diagnosis, and a column for the short Description.

    icd9Explain("1.0") # 'decimal' format code inferred
    #> [1] "Cholera due to vibrio cholerae"
    icd9Explain("0019") # 'short' format code inferred
    #> [1] "Cholera, unspecified"

    # we can be explicit about short vs decimal
    icd9Explain("434.00", isShort = FALSE)
    #> [1] "Cerebral thrombosis without mention of cerebral infarction"
    icd9Explain(c("43410","43491"), isShort = TRUE)
    #> [1] "Cerebral embolism without mention of cerebral infarction"
    #> [2] "Cerebral artery occlusion, unspecified with cerebral infarction"
    #explain top level code with children
    "391" %>% icd9Explain # single three-digit code
    #> [1] "Rheumatic fever with heart involvement"
    "391" %>% icd9Children # let's see the child codes
    #> [1] "391"  "3910" "3911" "3912" "3918" "3919"
    "391" %>% icd9Children %>% icd9Explain # children condensed to parent code
    #> [1] "Rheumatic fever with heart involvement"
    "391" %>% icd9Children %>% icd9Explain(doCondense = FALSE) # prevent condense
    #> [1] "Rheumatic fever with heart involvement"
    #> [2] "Acute rheumatic pericarditis"
    #> [3] "Acute rheumatic endocarditis"
    #> [4] "Acute rheumatic myocarditis"
    #> [5] "Other acute rheumatic heart disease"
    #> [6] "Acute rheumatic heart disease, unspecified"

Arbitrary named list(s) of codes:

    icd9Explain(list(somecodes = c("001", "391.0"),
                     morecodes = c("001.1", "001.9")))
    #> $somecodes
    #> [1] "Cholera"                      "Acute rheumatic pericarditis"
    #>
    #> $morecodes
    #> [1] "Cholera due to vibrio cholerae el tor"
    #> [2] "Cholera, unspecified"

`001` (Cholera) isn’t itself a diagnostic code, i.e. leaf node in the hierarchy, but `390` (Rheumatic fever without heart involvement) is. Both are explained correctly:

    icd9Explain(list(cholera = "001", rheumatic_heart = "390"))
    #> $cholera
    #> [1] "Cholera"
    #>
    #> $rheumatic_heart
    #> [1] "Rheumatic fever without mention of heart involvement"

Now try to explain on a non-existent (but ‘valid’) ICD-9 code:

    s <- icd9ExplainDecimal("001.5") # gives warning

As we have just seen, icd9Explain can convert lists of ICD-9 codes to a human-readable format. Let’s apply the `icd9Explain` to a list of comorbidity ICD-9 codes in one of the commonly-used mappings. This makes comprehending a complicated list much easier. Taking the list for dementia:

    length(quanDeyoComorbid[["Dementia"]]) # 133 possible ICD-9 codes
    #> [1] 133
    # icd9Explain summarizes these to just two groups:
    quanDeyoComorbid[["Dementia"]] %>% icd9Explain(warn = FALSE)
    #> [1] "Dementias"
    #> [2] "Dementia in conditions classified elsewhere"
    #> [3] "Senile degeneration of brain"
    # contrast with:
    quanDeyoComorbid[["Dementia"]] %>% icd9Explain(doCondense = FALSE, warn = FALSE)
    #>  [1] "Dementias"
    #>  [2] "Senile dementia, uncomplicated"
    #>  [3] "Presenile dementia"
    #>  [4] "Presenile dementia, uncomplicated"
    #>  [5] "Presenile dementia with delirium"
    #>  [6] "Presenile dementia with delusional features"
    #>  [7] "Presenile dementia with depressive features"
    #>  [8] "Senile dementia with delusional or depressive features"
    #>  [9] "Senile dementia with delusional features"
    #> [10] "Senile dementia with depressive features"
    #> [11] "Senile dementia with delirium"
    #> [12] "Vascular dementia"
    #> [13] "Vascular dementia, uncomplicated"
    #> [14] "Vascular dementia, with delirium"
    #> [15] "Vascular dementia, with delusions"
    #> [16] "Vascular dementia, with depressed mood"
    #> [17] "Other specified senile psychotic conditions"
    #> [18] "Unspecified senile psychotic condition"
    #> [19] "Dementia in conditions classified elsewhere"
    #> [20] "Dementia in conditions classified elsewhere without behavioral disturbance"
    #> [21] "Dementia in conditions classified elsewhere with behavioral disturbance"
    #> [22] "Senile degeneration of brain"

Use a range with more than two hundred ICD-9 codes (most of them not real):

    length("390" %i9da% "392.1")
    #> [1] 244
    "390" %i9da% "392.1" %>% icd9Explain(warn = FALSE)
    #> [1] "Rheumatic fever without mention of heart involvement"
    #> [2] "Rheumatic fever with heart involvement"
    #> [3] "Rheumatic chorea with heart involvement"

The warnings here are irrelevant because we know that `%i9da% produces codes which do not correspond to diagnoses. However, in other usage, the user would typically expect the ICD-9 codes he or she is using to be diagnostic, hence the default to warn.

## Filtering by Present-on-Arrival

This flag is recorded with each ICD-9 code, indicating whether that diagnosis was present on admission. With some caution, codes flagged specifically not POA can be treated as new diseases during an admission.

Present-on-arrival (POA) is typically a factor, or vector of values such as “Y”, “N”, “X”, “E”, or NA. Intermediate codes, such as “exempt”, “unknown” and NA mean that “yes” is not the same as “not no.” This requires four functions to cover the possibilities stored in icd9PoaChoices:

    #> [1] "yes"    "no"     "notYes" "notNo"

Filter for present-on-arrival being “Y”

    patientData %>% icd9FilterPoaYes
    #>   visitId  icd9
    #> 1    1000 40201
    #> 4    1000 25001
    #> 6    1001  4011

Show that yes is not equal to not no (e.g. due to NA in `poa` field)

    patientData %>% icd9FilterPoaNotNo
    #>   visitId  icd9
    #> 1    1000 40201
    #> 2    1000  2258
    #> 4    1000 25001
    #> 5    1001 34400
    #> 6    1001  4011
    #> 7    1002  4011

## Comorbidities

The comorbidities from different sources are provided as lists. At present only the most recent mapping of ICD-9 codes to comorbidities is provided. See these github issues.

This package contains ICD-9-CM to co-morbidity mappings from several sources, based on either the Charlson or Elixhauser lists of co-morbidities. Updated versions of these lists from AHRQ and Quan et al are included, along with the original Elixhauser mapping . Since some data is provided in SAS source code format, this package has internalfunctions to parse this SAS source code and generate R data structures. This processing is limited to what is needed for this purpose, although may be generalizable and useful in other contexts. Other lists are transcribed directly from the published articles, but interpretation of SAS code used for the original publications is prefererable.

### AHRQ comorbidity classification

The AHRQ keeps an updated version of the Elixhauser classification of ICD-9-CM codes into comorbidities, useful for research. They provide the data in the form of SAS code.

    #ahrqComorbid <- icd9:::parseAhrqSas() # user doesn't need to do this
    names(ahrqComorbid)
    #>  [1] "CHF"          "Valvular"     "PHTN"         "PVD"
    #>  [5] "HTN"          "HTNcx"        "Paralysis"    "NeuroOther"
    #>  [9] "Pulmonary"    "DM"           "DMcx"         "Hypothyroid"
    #> [13] "Renal"        "Liver"        "PUD"          "HIV"
    #> [17] "Lymphoma"     "Mets"         "Tumor"        "Rheumatic"
    #> [21] "Coagulopathy" "Obesity"      "WeightLoss"   "FluidsLytes"
    #> [25] "BloodLoss"    "Anemia"       "Alcohol"      "Drugs"
    #> [29] "Psychoses"    "Depression"

### Elixhauser co-morbidities

Elixhauser originally devleoped this set of co-morbidities to predict long term mortality based on hospital ICD-9-CM coding records. The AHRQ comorbidities are an updated version of this, however the original Elixhauser have been used in many publications. The ICD-9-CM codes have changed slightly over the years.

    names(elixComorbid)
    #>  [1] "CHF"          "Arrhythmia"   "Valvular"     "PHTN"
    #>  [5] "PVD"          "HTN"          "HTNcx"        "Paralysis"
    #>  [9] "NeuroOther"   "Pulmonary"    "DM"           "DMcx"
    #> [13] "Hypothyroid"  "Renal"        "Liver"        "PUD"
    #> [17] "HIV"          "Lymphoma"     "Mets"         "Tumor"
    #> [21] "Rheumatic"    "Coagulopathy" "Obesity"      "WeightLoss"
    #> [25] "FluidsLytes"  "BloodLoss"    "Anemia"       "Alcohol"
    #> [29] "Drugs"        "Psychoses"    "Depression"

### Quan

Quan’s paper looked at indices using both ICD-10 and ICD-9-CM. Quan generated updated ICD-9-CM codes for all 30 of Elixhauser and all 17 of Charlson/Deyo’s co-morbidities. Thus there are two ‘Quan’ comorbidity mappings.

    names(quanDeyoComorbid)
    #>  [1] "MI"          "CHF"         "PVD"         "Stroke"      "Dementia"
    #>  [6] "Pulmonary"   "Rheumatic"   "PUD"         "LiverMild"   "DM"
    #> [11] "DMcx"        "Paralysis"   "Renal"       "Cancer"      "LiverSevere"
    #> [16] "Mets"        "HIV"
    names(quanElixComorbid)
    #>  [1] "CHF"          "Arrhythmia"   "Valvular"     "PHTN"
    #>  [5] "PVD"          "HTN"          "HTNcx"        "Paralysis"
    #>  [9] "NeuroOther"   "Pulmonary"    "DM"           "DMcx"
    #> [13] "Hypothyroid"  "Renal"        "Liver"        "PUD"
    #> [17] "HIV"          "Lymphoma"     "Mets"         "Tumor"
    #> [21] "Rheumatic"    "Coagulopathy" "Obesity"      "WeightLoss"
    #> [25] "FluidsLytes"  "BloodLoss"    "Anemia"       "Alcohol"
    #> [29] "Drugs"        "Psychoses"    "Depression"

## Examples

### Filter patients and create comorbidities

Take my patients, find the ones where there definitely or maybe was a diagnosis present on admission, then generate comorbidities based on the AHRQ mapping. N.b. `NotNo` is not the same as `Yes` because of some exempt, unclassifiable conditions, or `NA` values for `poa`.

    patientData %>%
      icd9FilterPoaNotNo %>%
      icd9ComorbidAhrq %>%
      extract(1:9)
    #> [1]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE

### Compare two comorbidity defintions

We will find the differences between some categories of the original Elixhauser and the updated version by Quan. Just taking the select few comorbidity groups for brevity:

    difference <- icd9DiffComorbid(elixComorbid, quanElixComorbid,
                     names = c("CHF", "PHTN", "HTN", "Valvular"))
    #> Comorbidity CHF:
    #> quanElixComorbid has 70 codes not in elixComorbid. First few are: 'Heart failure' 'Mal hypert hrt dis w hf' 'Mal hyp ht/kd I-IV w hf' 'Mal hyp ht/kd stg V w hf' 'Prim cardiomyopathy NEC'
    #> Comorbidity PHTN:
    #> quanElixComorbid has 45 codes not in elixComorbid. First few are: 'Acute pulmonary heart disease' 'Chronic pulmonary heart disease' 'Arterioven fistu pul ves' 'Pulmon circulat dis NEC'
    #> Comorbidity HTN:
    #> quanElixComorbid has 89 codes not in elixComorbid. First few are: 'Essential hypertension'
    #> Comorbidity Valvular:
    #> quanElixComorbid has 106 codes not in elixComorbid. First few are: 'Diseases of mitral valve' 'Diseases of other endocardial structures' 'Other diseases of endocardium' 'Syphilitic endocarditis'
    # reuslts also returned as data
    str(difference)
    #> List of 4
    #>  $ CHF     :List of 3
    #>   ..$ both  : chr [1:117] "39891" "40211" "40291" "40411" ...
    #>   ..$ only.x: chr(0)
    #>   ..$ only.y: chr [1:70] "40201" "40401" "40403" "4254" ...
    #>  $ PHTN    :List of 3
    #>   ..$ both  : chr [1:121] "4160" "41600" "41601" "41602" ...
    #>   ..$ only.x: chr(0)
    #>   ..$ only.y: chr [1:45] "4150" "41500" "41501" "41502" ...
    #>  $ HTN     :List of 3
    #>   ..$ both  : chr [1:22] "4011" "40110" "40111" "40112" ...
    #>   ..$ only.x: chr(0)
    #>   ..$ only.y: chr [1:89] "401" "4010" "40100" "40101" ...
    #>  $ Valvular:List of 3
    #>   ..$ both  : chr [1:526] "09320" "09321" "09322" "09323" ...
    #>   ..$ only.x: chr(0)
    #>   ..$ only.y: chr [1:106] "0932" "09325" "09326" "09327" ...

Which pulmonary hypertension codes are only in Quan’s version?

    difference$PHTN$only.y %>% icd9GetReal %>% icd9Explain
    #> [1] "Acute pulmonary heart disease"
    #> [2] "Chronic pulmonary heart disease"
    #> [3] "Arteriovenous fistula of pulmonary vessels"
    #> [4] "Other specified diseases of pulmonary circulation"

(Passing through `icd9GetReal` stops `icd9Explain` complaining that some of the input codes don’t exist. This is because the comorbidity mappings have every possible numerical ICD-9 code, not just the official ones. Could also use `warn = FALSE` option in `icd9Explain`)

### Find cardiac-related ICD-9 codes:

    icd9Hierarchy[
      grepl(pattern = "(heart)|(cardiac)",
            x = c(icd9Hierarchy$descLong, icd9Hierarchy$descShort),
            ignore.case = TRUE),
      "icd9"] %>% unique -> cardiac

then explain the list, just showing the first ten:

    cardiac %>% icd9Explain(warn = FALSE) %>% head(10)
    #>  [1] "Malignant neoplasm of thymus heart and mediastinum"
    #>  [2] "Rheumatic fever without mention of heart involvement"
    #>  [3] "Rheumatic fever with heart involvement"
    #>  [4] "Rheumatic chorea"
    #>  [5] "Other rheumatic heart disease"
    #>  [6] "Hypertensive heart disease"
    #>  [7] "Hypertensive heart and chronic kidney disease"
    #>  [8] "Other acute and subacute forms of ischemic heart disease"
    #>  [9] "Other forms of chronic ischemic heart disease"
    #> [10] "Acute pulmonary heart disease"

### Find comorbidities for a large number of patients.

I understand that comorbiditity assignment using SAS is a lengthy business. Let’s generate 100,000 patients with a random selection of comorbidities:

    # codes selected from AHRQ mapping
    many_patients <- icd9:::randomPatients(100000)

    # result in seconds (platform and load dependent, of course)
    system.time(
      icd9ComorbidAhrq(many_patients)
      )[["elapsed"]]
    #> [1] 0.904

Don’t want to stress CRAN, so no larger numbers demonstrated, but I do consistently get about 5 seconds for 10 million rows of comorbidities on a moderately powerful workstation.

### Arbitrary ICD-9 mapping

The user can provide any ICD-9, ICD-10 or other code mapping to comorbidities they wish. Submissions of other peer-reviewed published mappings could be included in this package, if their license permits. Create an issue in github or email me at jack@jackwasey.com) Included in this package is a small data set called `icd9Chapters`, which lists the ICD-9-CM (and indeed ICD-9) Chapters. These can easily be expanded out and used as a mapping, so instead of a comorbidity, we see which patients have codes in each chapter of the ICD-9 defintion.

    names(icd9Chapters)[c(1:5, 14)]
    #> [1] "Infectious And Parasitic Diseases"
    #> [2] "Neoplasms"
    #> [3] "Endocrine, Nutritional And Metabolic Diseases, And Immunity Disorders"
    #> [4] "Diseases Of The Blood And Blood-Forming Organs"
    #> [5] "Mental Disorders"
    #> [6] "Congenital Anomalies"
    myMap <- icd9:::icd9ChaptersToMap(icd9Chapters[c(2, 5, 14)])
    icd9Comorbid(patientData, myMap) # no +ve
    #>      Neoplasms Mental Disorders Congenital Anomalies
    #> 1000      TRUE            FALSE                FALSE
    #> 1001     FALSE            FALSE                FALSE
    #> 1002     FALSE            FALSE                FALSE

### Reduce comorbidity mapping from possible values to defined diagnostic codes.

Suppose we want to exact match only real ICD-9 codes when looking up comorbdities for some patients. E.g. if the coder accidentally omited a trailing zero, e.g. code `003.20` (Localized salmonella infection, unspecified) might have been written as `003.2` which has a heading (Localized salmonella infections) but is not itself billable. Use of ICD-9 codes for comorbidities generally assumes the codes are either right or wrong. How do we match only real codes, for a strict interpretation of comorbidities? It’s one line or R code:

    ahrqStrict <- lapply(ahrqComorbid, icd9GetReal)
    str(ahrqComorbid[1:5]) # first five of the original:
    #> List of 5
    #>  $ CHF     : chr [1:132] "39891" "428" "4280" "42800" ...
    #>  $ Valvular: chr [1:568] "0932" "09320" "09321" "09322" ...
    #>  $ PHTN    : chr [1:136] "4151" "41510" "41511" "41512" ...
    #>  $ PVD     : chr [1:616] "440" "4400" "44000" "44001" ...
    #>  $ HTN     : chr [1:36] "4011" "40110" "40111" "40112" ...
    str(ahrqStrict[1:5]) # and first five of the result:
    #> List of 5
    #>  $ CHF     : chr [1:40] "39891" "428" "4280" "4281" ...
    #>  $ Valvular: chr [1:55] "0932" "09320" "09321" "09322" ...
    #>  $ PHTN    : chr [1:15] "4151" "41511" "41512" "41513" ...
    #>  $ PVD     : chr [1:76] "440" "4400" "4401" "4402" ...
    #>  $ HTN     : chr [1:11] "4011" "4019" "6420" "64200" ...

Note the much smaller numbers of codes in each group, now we have discarded all the ones which are not defined as diagnoses.

### Which three-character ICD-9 codes have no subcodes

The ICD-9-CM scheme is structured as follows: - Chapter - Sub-chapter - Major part (3 digit codes) - sub-division (1st decimal place) - sub-sub-division (2nd decimal place)

For most combinations of 0 to 9, nothing is defined. Sometimes, nodes at one level in the hierarchy are descriptive only of their children (branch nodes), whereas some are themselves billable. For this example, let’s find those numeric-only codes which have no children, and by implication are themselves directly billable codes. Here are the first ten:

    allreal <- icd9::icd9Hierarchy[["icd9"]]
    # select the non-V and non-E codes with three characters (zeroes already prefixed)
    threedigitreal <- allreal[nchar(allreal) == 3 & icd9IsN(allreal)]
    # display
    threedigitdf <- data.frame(icd9 = threedigitreal,  description = icd9Explain(threedigitreal))
    print(threedigitdf[1:10, ], row.names = FALSE)
    #>  icd9                                  description
    #>   001                                      Cholera
    #>   002               Typhoid and paratyphoid fevers
    #>   003                  Other salmonella infections
    #>   004                                  Shigellosis
    #>   005             Other food poisoning (bacterial)
    #>   006                                    Amebiasis
    #>   007          Other protozoal intestinal diseases
    #>   008 Intestinal infections due to other organisms
    #>   009            Ill-defined intestinal infections
    #>   010                Primary tuberculous infection

### Which ICD-9 codes have changed between versions of ICD-9?

    new_since_27 <- setdiff(icd9Billable[["32"]][["icd9"]],
                             icd9Billable[["27"]][["icd9"]]) %>% head
    lost_since_27 <- setdiff(icd9Billable[["27"]][["icd9"]],
                             icd9Billable[["32"]][["icd9"]]) %>% tail

    # these are a few which were gained since v27
    data.frame(icd9 = new_since_27, desc = new_since_27 %>% icd9Explain)
    #>    icd9                                          desc
    #> 1 04141                     Escherichia coli [E.coli]
    #> 2 04142 Unspecified malignant neoplasm of skin of lip
    #> 3 04143           Basal cell carcinoma of skin of lip
    #> 4 04149                     Escherichia coli [E.coli]
    #> 5 17300 Unspecified malignant neoplasm of skin of lip
    #> 6 17301           Basal cell carcinoma of skin of lip
    # these are a few which were lost since v27
    data.frame(icd9 = lost_since_27, desc = lost_since_27 %>% icd9Explain)
    #>   icd9
    #> 1 V122
    #> 2 V138
    #> 3 V191
    #> 4 V251
    #> 5 V403
    #> 6 V854
    #>                                                                      desc
    #> 1                            Endocrine, metabolic, and immunity disorders
    #> 2                                                Other specified diseases
    #> 3                                                     Other eye disorders
    #> 4 Encounter for insertion or removal of intrauterine contraceptive device
    #> 5                                               Other behavioral problems
    #> 6                                      Body Mass Index 40 and over, adult

## Conclusion

This package allows fluid, fast and accurate manipulation of ICD-9 codes, especially when combined with magrittr. Suggestions, contributions and comments are welcome via github.