Skip to contents

Who is this guide for?

Imagine you work in the statistics unit of a hospital and every month you are the person responsible for preparing the diabetes discharge report for the service directorate. You receive the hospital discharge database, and your goal is to answer concrete questions: how many discharges had diabetes as the primary diagnosis?, of what type?, how complex were those patients?

The problem is that the database arrives with ICD-10 codes in inconsistent formats and without descriptions: to interpret them you would have to manually consult the official catalog in PDF or Excel. ciecl removes that step: it bundles the official Chilean ICD-10 catalog (MINSAL/DEIS v2018) inside R and lets you normalize, describe, search and analyze the codes directly on your database.

This guide walks through that complete workflow, from basic to advanced. You only need basic R knowledge; if you also use dplyr, the examples fit directly into your pipelines.

The data: DEIS hospital discharges

The Hospital Discharge databases are published by the Department of Health Statistics and Information (DEIS) of the Ministry of Health of Chile. Each row is a hospital discharge and the DIAG1 column contains the primary diagnosis coded in ICD-10.

In practice, these files arrive with two very common format variations:

  1. Compact formats: codes without a decimal point (e.g., J189 instead of J18.9).
  2. Filler suffixes: an X letter to complete the field length in 3-character categories (e.g., I10X for essential hypertension).

Let’s generate a synthetic dataset that replicates the structure and typical anomalies of DEIS files:

set.seed(42)

# Simulation of 200 records with typical DEIS Chile formats
discharges <- data.frame(
  DISCHARGE_ID = 1:200,
  PATIENT_ID   = sample(1:50, 200, replace = TRUE),
  YEAR         = sample(2018:2022, 200, replace = TRUE),
  DIAG1        = sample(
    c(
      "J189", "O800", "Z380", "K359", "N390",
      "I10X", "J449", "E119", "O829", "J069",
      "K922", "N185", "I509", "C509", "A099",
      "N40X", "K800", "I259", "J180", "E149"
    ),
    size    = 200,
    replace = TRUE
  ),
  stringsAsFactors = FALSE
)

head(discharges)
#>   DISCHARGE_ID PATIENT_ID YEAR DIAG1
#> 1            1         49 2018  J189
#> 2            2         37 2022  E119
#> 3            3          1 2021  E149
#> 4            4         25 2018  N390
#> 5            5         10 2022  I10X
#> 6            6         36 2021  C509

Step 1: Normalize codes with cie_norm()

Before any analysis, DIAG1 must be standardized. cie_norm() applies the official MINSAL coding rules in a vectorized way: it removes the filler X, inserts the decimal point in the correct position and cleans whitespace, hyphens and special symbols (such as † or *).

# Cleaning and standardization of diagnoses in the workflow
discharges <- discharges |>
  mutate(
    DIAG1_NORM = cie_norm(codes = DIAG1)
  )

# Comparison between original and normalized formats
discharges |>
  select(DIAG1, DIAG1_NORM) |>
  distinct() |>
  head(5)
#>   DIAG1 DIAG1_NORM
#> 1  J189      J18.9
#> 2  E119      E11.9
#> 3  E149      E14.9
#> 4  N390      N39.0
#> 5  I10X        I10

With this, I10X became I10 and J189 became J18.9: the codes are now comparable with the official catalog.

Step 2: Add the official descriptions with cie_describe()

For the report you need the clinical descriptions, not just the codes. cie_describe() returns a character vector with one description per code, so you can add it as another column of your table with mutate(), with no intermediate steps:

# Direct integration of descriptions into the main dataframe
discharges_full <- discharges |>
  mutate(
    description = cie_describe(DIAG1_NORM)
  )

head(discharges_full |> select(DISCHARGE_ID, DIAG1, description))
#>   DISCHARGE_ID DIAG1
#> 1            1  J189
#> 2            2  E119
#> 3            3  E149
#> 4            4  N390
#> 5            5  I10X
#> 6            6  C509
#>                                                       description
#> 1                                       Neumonía, no especificada
#> 2                     Diabetes mellitus tipo 2 sin complicaciones
#> 3 Diabetes mellitus, no especificada, sin mención de complicación
#> 4              Infección de vías urinarias, sitio no especificado
#> 5                                Hipertensión esencial (primaria)
#> 6                 Tumor maligno de la mama, parte no especificada

If, in addition to the description, you need the full metadata (chapter, group, inclusion/exclusion notes), use cie_lookup(), which returns a structured tibble ready for a left_join():

# Extracting full metadata via lookup + join
metadata <- cie_lookup(
  code = unique(discharges$DIAG1_NORM),
  full_description = TRUE
)
#>  Códigos no encontrados: "K35.9"

discharges_metadata <- discharges |>
  left_join(metadata, by = c("DIAG1_NORM" = "codigo"))

Back to your diabetes report: you suspect there are diabetes discharges in the database, but which exact codes does the catalog cover? Instead of flipping through the PDF, you search by text. cie_search() uses Jaro-Winkler similarity, so it tolerates typos (here we search for “diabetis” on purpose):

# Tolerant search: "diabetis" instead of "diabetes"
# (by default the 50 most similar results are shown;
#  we raise the limit because the catalog has many diabetes codes)
search_results <- cie_search(text = "diabetis", threshold = 0.7, max_results = 100)

search_results
#> # A tibble: 100 × 4
#>    codigo descripcion                                            score categoria
#>    <chr>  <chr>                                                  <dbl> <chr>    
#>  1 E10    Diabetes mellitus insulinodependiente                  0.917 E10 DIAB…
#>  2 E10.0  Diabetes mellitus tipo 1 con coma                      0.917 E10 DIAB…
#>  3 E10.1  Diabetes mellitus tipo 1 con cetoacidosis              0.917 E10 DIAB…
#>  4 E10.2  Diabetes mellitus tipo 1 con complicaciones renales    0.917 E10 DIAB…
#>  5 E10.3  Diabetes mellitus tipo 1 con complicaciones oftálmicas 0.917 E10 DIAB…
#>  6 E10.4  Diabetes mellitus tipo 1 con complicaciones neurológi… 0.917 E10 DIAB…
#>  7 E10.5  Diabetes mellitus tipo 1 con complicaciones  circulat… 0.917 E10 DIAB…
#>  8 E10.6  Diabetes mellitus tipo 1 con otras complicaciones esp… 0.917 E10 DIAB…
#>  9 E10.7  Diabetes mellitus tipo 1 con complicaciones múltiples  0.917 E10 DIAB…
#> 10 E10.8  Diabetes mellitus tipo 1 con complicaciones no especi… 0.917 E10 DIAB…
#> # ℹ 90 more rows

Each result includes a similarity score to assess the reliability of the match. But the table above lists every diabetes code in the catalog, and not all of them necessarily appear in your data. To find out which ones do, cross the search results with the codes that actually show up in your dataset:

# Which diabetes codes are actually in my data?
diabetes_codes <- intersect(
  search_results$codigo,
  unique(discharges$DIAG1_NORM)
)

diabetes_codes
#> [1] "E11.9" "E14.9"

As the result shows, of all the diabetes codes in the catalog only two are present in the DIAG1 column of your data: E11.9 and E14.9. The cross identifies which codes your data actually contains, without reviewing the full table by hand. With that list you can now filter the discharges and close the report:

# Final report: diabetes discharges, summarized by type
discharges_full |>
  filter(DIAG1_NORM %in% diabetes_codes) |>
  count(description, sort = TRUE)
#>                                                       description  n
#> 1                     Diabetes mellitus tipo 2 sin complicaciones 14
#> 2 Diabetes mellitus, no especificada, sin mención de complicación 12

With this, your monthly diabetes discharge report is ready: you know how many there were and of what type, with the official catalog descriptions.

When a search returns no results

It is normal for some queries to find nothing, and it is worth knowing how the package behaves in those cases: the functions never fail with an error when there are no results; they return an empty tibble with the correct column structure and an informative message.

If you search for a code that does not exist in the catalog:

cie_lookup("XYZ123")
#>  Código no encontrado: "XYZ123"
#> # A tibble: 0 × 11
#> # ℹ 11 variables: codigo <chr>, descripcion <chr>, categoria <chr>,
#> #   seccion <chr>, capitulo_nombre <chr>, inclusion <chr>, exclusion <chr>,
#> #   capitulo <chr>, es_daga <lgl>, es_cruz <lgl>, uso_cl <chr>

If the cie_search() threshold is too strict for the term entered:

cie_search("zzzqwerty", threshold = 0.95)
#>  Sin coincidencias >= threshold 0.95
#> # A tibble: 0 × 4
#> # ℹ 4 variables: codigo <chr>, descripcion <chr>, score <dbl>, categoria <chr>

In both cases the workflow is not interrupted: you can check nrow(result) == 0 and react (lower the threshold, check the spelling or validate the code). To quickly check which codes of a vector are valid according to the catalog, use cie_validate_vector():

cie_validate_vector(c("E11.0", "XYZ123", "I10X"))
#> [1]  TRUE FALSE  TRUE

Step 4: Stratify risk with cie_comorbid()

The last level of the report is patient complexity. cie_comorbid() maps the diagnoses to the Charlson or Elixhauser indices and returns a comorbidity matrix per patient, ready for statistical models:

# Requires the 'comorbidity' package to be installed
# Calculation of the Charlson Index consolidated by patient
comorbidities <- cie_comorbid(
  data = discharges,
  id   = "PATIENT_ID",
  code = "DIAG1",
  map  = "charlson"
)

head(comorbidities, 10)
#> # A tibble: 10 × 19
#>    PATIENT_ID    mi   chf   pvd  cevd dementia   cpd rheumd   pud   mld  diab
#>         <int> <int> <int> <int> <int>    <int> <int>  <int> <int> <int> <int>
#>  1          1     0     0     0     0        0     0      0     0     0     1
#>  2          2     0     0     0     0        0     0      0     0     0     0
#>  3          3     0     0     0     0        0     0      0     0     0     1
#>  4          4     0     1     0     0        0     1      0     0     0     0
#>  5          5     0     1     0     0        0     0      0     0     0     1
#>  6          6     0     0     0     0        0     1      0     0     0     1
#>  7          7     0     0     0     0        0     0      0     0     0     0
#>  8          8     0     0     0     0        0     0      0     0     0     1
#>  9          9     0     0     0     0        0     1      0     0     0     0
#> 10         10     0     0     0     0        0     0      0     0     0     1
#> # ℹ 8 more variables: diabwc <int>, hp <int>, rend <int>, canc <int>,
#> #   msld <int>, metacanc <int>, aids <int>, score_charlson <dbl>

Workflow summary

This guide covered the complete cycle from the raw database to the analytical input:

  1. Standardization: format correction with cie_norm().
  2. Contextualization: official descriptions with cie_describe() and metadata with cie_lookup().
  3. Exploration: text-based code search with cie_search(), error-tolerant and with predictable behavior when there are no results.
  4. Aggregation: comorbidity indices with cie_comorbid().

Not sure which function to use in another scenario? Run cie_guide() to see a comparison table with the recommended function and an example per case.

Next steps


Data source: This tool uses the official ICD-10 catalog for Chile, managed by the DEIS of the Ministry of Health. More details at deis.minsal.cl.