---
title: "Building route-specific RxCUI and NDC lists"
author: "Steven Smith"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Building route-specific RxCUI and NDC lists}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
# Vignettes use precomputed example data by default.
# To rebuild examples with live RxNorm/RxClass API calls, set:
# Sys.setenv(RXREF_BUILD_VIGNETTES_ONLINE = "true")

online_env <- identical(
  tolower(Sys.getenv("RXREF_BUILD_VIGNETTES_ONLINE")),
  "true"
)

has_net <- tryCatch({
  requireNamespace("curl", quietly = TRUE) && curl::has_internet()
}, error = function(e) FALSE)

run_live <- online_env && has_net

knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)

library(rxref)
library(dplyr)

read_rxref_example <- function(file) {
  path <- system.file("extdata", file, package = "rxref")
  if (!nzchar(path)) {
    stop(
      "The example data file '", file, "' was not found. ",
      "Reinstall rxref or rebuild the vignette with ",
      "RXREF_BUILD_VIGNETTES_ONLINE=true."
    )
  }
  readRDS(path)
}
```

## Overview

A common use case for `rxref` is to develop a list of RxNorm product
concepts and National Drug Codes (NDCs) for a drug class.

For example, suppose we want to identify product RxCUIs and active NDCs
for oral beta-blockers. This is not as simple as searching for
beta-blocker ingredients and mapping every product to NDCs, because some
beta-blockers are available through multiple routes.

For example:

- `metoprolol`, `atenolol`, `labetalol`, `propranolol`, and `sotalol`
  may have oral and injectable products;
- `esmolol` is injectable only;
- `timolol`, `betaxolol`, and related drugs may have ophthalmic products.

For many pharmacoepidemiologic studies, we may want oral outpatient-use
products only.

This vignette walks through a tidy workflow for:

1. resolving drug names to ingredient RxCUIs;
2. expanding ingredient RxCUIs to product RxCUIs;
3. filtering product RxCUIs by route; and
4. mapping the filtered product RxCUIs to active NDCs.

The examples use precomputed data by default so the vignette can be built
without querying the live RxNorm API. To rebuild the examples with live
API calls, set:

```{r eval = FALSE}
Sys.setenv(RXREF_BUILD_VIGNETTES_ONLINE = "true")
```

## Define a beta-blocker ingredient list

In this example, we start with a curated list of beta-blocker ingredient
names.

Users can also use RxClass-related helpers, such as `find_classes()` or
`get_class_members()`, when a suitable source vocabulary is available.
However, curated ingredient lists remain useful because class membership
may not perfectly match a study-specific exposure definition.

```{r define}
beta_blocker_names <- c(
  "acebutolol",
  "atenolol",
  "betaxolol",
  "bisoprolol",
  "carvedilol",
  "labetalol",
  "metoprolol",
  "nadolol",
  "nebivolol",
  "penbutolol",
  "pindolol",
  "propranolol",
  "sotalol",
  "timolol"
)
```

## Resolve drug names to ingredient RxCUIs

First, use `find_ingredients()` to resolve each drug name to an RxNorm
ingredient concept.

```{r resolve}
if (run_live) {
  bb_ingredients <- find_ingredients(beta_blocker_names) |>
    filter(tty == "IN") |>
    distinct(
      input,
      ingredient_rxcui = rxcui,
      ingredient_name = name,
      ingredient_tty = tty
    )
} else {
  bb_ingredients <- read_rxref_example("bb_ingredients.rds")
}

bb_ingredients
```

The resulting table contains one row per resolved ingredient. These
ingredient RxCUIs are the starting point for product expansion.

## Expand ingredients to product RxCUIs

Next, use `products_for_ingredients()` to identify product-level RxNorm
concepts containing each ingredient.

In this example, we use active RxNorm concepts and include combination
products.

```{r expand}
if (run_live) {
  bb_products <- products_for_ingredients(
    bb_ingredients$ingredient_rxcui,
    ttys = product_ttys("default"),
    include_combos = TRUE,
    concept_status = "active"
  ) |>
    left_join(bb_ingredients, by = "ingredient_rxcui")
} else {
  bb_products <- read_rxref_example("bb_products.rds")
}

bb_products
```

Because `include_combos = TRUE`, combination products are retained. For
beta-blockers, this means products such as beta-blocker/thiazide
combinations may be included.

To exclude combination products, set `include_combos = FALSE`.

```{r exclude-option}
if (run_live) {
  bb_single_ingredient_products <- products_for_ingredients(
    bb_ingredients$ingredient_rxcui,
    ttys = product_ttys("default"),
    include_combos = FALSE,
    concept_status = "active"
  ) |>
    left_join(bb_ingredients, by = "ingredient_rxcui")
} else {
  bb_single_ingredient_products <- read_rxref_example(
    "bb_single_ingredient_products.rds"
  )
}

bb_single_ingredient_products
```

## Active versus historical RxNorm concepts

For many current medication list workflows, `concept_status = "active"`
is a good default. This limits product expansion to active RxNorm
concepts.

For studies covering older calendar periods, users may want to include
historical RxNorm concepts as well.

```{r historical-products, eval = FALSE}
bb_products_historical <- products_for_ingredients(
  bb_ingredients$ingredient_rxcui,
  ttys = product_ttys("default"),
  include_combos = TRUE,
  concept_status = "active_and_historical"
)
```

Historical concepts can be useful when reconstructing medication exposure
during older study periods. However, historical product concepts may have
less complete clinical attribute information than active concepts. Route
filtering should therefore be reviewed carefully when
`concept_status = "active_and_historical"` is used.

## Why route filtering matters

At this point, the product list may include products from multiple
routes. For example, some beta-blockers have oral tablets, injectable
products, or ophthalmic solutions.

We can use `get_clinical_attributes()` to inspect route and dose-form
information for the product RxCUIs.

```{r filtering-matters}
if (run_live) {
  bb_attrs <- get_clinical_attributes(
    unique(bb_products$product_rxcui)
  ) |>
    rename(product_rxcui = rxcui)
} else {
  bb_attrs <- read_rxref_example("bb_attrs.rds")
}

bb_attrs |>
  count(route, dose_form_group, sort = TRUE)
```

This helps verify whether the product list includes routes that are
outside the intended use case.

## Filter to oral products

To keep only orally available products, use `filter_products_by_route()`.

```{r filter}
if (run_live) {
  bb_oral_products <- bb_products |>
    filter_products_by_route(route = "ORAL")
} else {
  bb_oral_products <- read_rxref_example("bb_oral_products.rds")
}

bb_oral_products
```

The returned table keeps product-level metadata and appends summarized
route and dose-form information.

```{r filter-summary}
bb_oral_products |>
  count(route, dose_form_group, sort = TRUE)
```

This should retain products such as oral tablets, capsules, and oral
solutions, while excluding injectable or ophthalmic products.

Route filtering relies on clinical attribute information available
through RxNorm. If a product concept lacks route or dose-form
information, it may not be retained by route-specific filters. This is
especially important when historical concepts are included.

## Map oral product RxCUIs to active NDCs

Once the product list is restricted to oral products, use
`map_rxcui_to_ndc()` to map the product RxCUIs to NDCs.

```{r map}
if (run_live) {
  bb_oral_ndc_map <- map_rxcui_to_ndc(
    unique(bb_oral_products$product_rxcui),
    status = "ACTIVE"
  )

  bb_oral_ndcs <- bb_oral_ndc_map |>
    left_join(
      bb_oral_products,
      by = c("rxcui" = "product_rxcui")
    ) |>
    rename(
      product_rxcui = rxcui,
    ) |>
    distinct()
} else {
  bb_oral_ndcs <- read_rxref_example("bb_oral_ndcs.rds")
}

bb_oral_ndcs
```

The output is a flat tibble with one row per product RxCUI/NDC pair.
Product metadata is retained alongside the NDC.

```{r map-select}
bb_oral_ndcs |>
  select(
    ingredient_name,
    product_rxcui,
    name,
    tty,
    route,
    dose_form,
    ndc11,
    ndc_status
  )
```

The `status = "ACTIVE"` argument controls the NDC status returned by
`map_rxcui_to_ndc()`. This is distinct from `concept_status`, which
controls whether active or historical RxNorm concepts are included during
product expansion.

## Summarize the resulting list

For quality control, it is useful to summarize the number of products and
NDCs by ingredient.

```{r summarize-products}
bb_oral_products |>
  count(ingredient_name, sort = TRUE, name = "n_product_rxcuis")
```

```{r summarize-ndcs}
bb_oral_ndcs |>
  count(ingredient_name, sort = TRUE, name = "n_active_ndcs")
```

You may also want to inspect combination products separately.

```{r summarize-combinations}
bb_oral_products |>
  filter(n_ingredients > 1) |>
  select(
    ingredient_name,
    product_rxcui,
    name,
    tty,
    n_ingredients,
    route,
    dose_form
  )
```

Depending on the scientific question, combination products may be
appropriate to include or exclude. For example, beta-blocker/thiazide
combination products may be relevant for antihypertensive exposure
definitions but not for studies focused on beta-blocker monotherapy.

## A shortcut using `search_drug()`

The same core workflow can be run more compactly with `search_drug()`.
This function combines ingredient searching, product expansion, optional
route filtering, and optional NDC mapping.

To return oral product RxCUIs only:

```{r alt-search-rxcui}
if (run_live) {
  bb_oral_rxcuis <- search_drug(
    beta_blocker_names,
    return = "rxcui",
    route = "ORAL",
    include_combos = TRUE,
    concept_status = "active"
  )
} else {
  bb_oral_rxcuis <- read_rxref_example("bb_oral_rxcuis_search.rds")
}

bb_oral_rxcuis
```

To return a flat table of oral product RxCUIs and active NDCs:

```{r alt-search-ndc}
if (run_live) {
  bb_oral_ndcs_search_raw <- search_drug(
    beta_blocker_names,
    return = "ndc",
    route = "ORAL",
    ndc_status = "ACTIVE",
    include_combos = TRUE,
    concept_status = "active"
  )

  bb_oral_ndcs_search <- bb_oral_ndcs_search_raw

  if (!"ingredient_name" %in% names(bb_oral_ndcs_search)) {
    bb_oral_ndcs_search <- bb_oral_ndcs_search |>
      left_join(
        bb_ingredients |>
          select(ingredient_rxcui, ingredient_name),
        by = "ingredient_rxcui"
      )
  }

  product_cols <- c(
    "name",
    "tty",
    "route",
    "dose_form",
    "dose_form_group"
  )

  missing_product_cols <- setdiff(product_cols, names(bb_oral_ndcs_search))

  if (length(missing_product_cols) > 0) {
    bb_oral_ndcs_search <- bb_oral_ndcs_search |>
      left_join(
        bb_oral_products |>
          select(
            ingredient_rxcui,
            product_rxcui,
            all_of(missing_product_cols)
          ),
        by = c("ingredient_rxcui", "product_rxcui")
      )
  }

  bb_oral_ndcs_search <- bb_oral_ndcs_search |>
    distinct()
} else {
  bb_oral_ndcs_search <- read_rxref_example("bb_oral_ndcs_search.rds")
}

bb_oral_ndcs_search
```

The raw `search_drug(return = "ndc")` output is intentionally compact. In
this vignette, we join it back to the ingredient and product tables so
that the displayed example includes readable ingredient names, product
names, routes, and dose forms.

## Returning both products and NDCs

If you want both the unique product RxCUI table and the expanded NDC
table, use `return = "both"`.

```{r products-and-ndcs}
if (run_live) {
  bb_oral_both <- search_drug(
    beta_blocker_names,
    return = "both",
    route = "ORAL",
    ndc_status = "ACTIVE",
    include_combos = TRUE,
    concept_status = "active"
  )
} else {
  bb_oral_both <- read_rxref_example("bb_oral_both_search.rds")
}

names(bb_oral_both)
```

This returns a list because the two tables have different grains:

- `products`: one row per product RxCUI;
- `ndcs`: one row per product RxCUI/NDC pair.

```{r products-and-ndcs-products}
bb_oral_both$products
```

```{r products-and-ndcs-ndcs}
bb_oral_both$ndcs
```

## Suggested QC checks

When developing a study-specific medication list, it is good practice to
review the resulting concepts before finalizing the cohort definition.
Even if the package is working correctly, RxNorm concepts and source data
may occasionally contain surprises for a specific study use case.

The following are some examples.

### Check whether any non-oral product names remain

```{r qc-nonoral-names}
bb_oral_products |>
  filter(grepl(
    "Injection|Injectable|Ophthalmic|Topical|Transdermal|Nasal|Inhalation",
    name,
    ignore.case = TRUE
  ))
```

### Check the observed routes and dose-form groups

```{r qc-routes}
bb_oral_products |>
  count(route, dose_form_group, sort = TRUE)
```

### Check combination products

```{r qc-combinations}
bb_oral_products |>
  filter(n_ingredients > 1) |>
  count(ingredient_name, sort = TRUE)
```

### Check products without active NDCs

Some product RxCUIs may not map to active NDCs.

```{r qc-without-ndcs}
bb_oral_products |>
  anti_join(
    bb_oral_ndcs |>
      distinct(product_rxcui),
    by = "product_rxcui"
  ) |>
  select(
    ingredient_name,
    product_rxcui,
    name,
    tty,
    route,
    dose_form
  )
```

## Summary

This vignette demonstrated how to build a route-specific medication list,
with oral beta-blockers as an example, using `rxref`.

The key steps are:

1. `find_ingredients()` to resolve drug names to ingredient RxCUIs;
2. `products_for_ingredients()` to expand ingredients to product RxCUIs;
3. `filter_products_by_route()` to retain products for the route of
   interest;
4. `map_rxcui_to_ndc()` to obtain NDCs; or
5. `search_drug()` for a compact end-to-end workflow.

For drug classes with products available through multiple routes, route
filtering is an important quality-control step before mapping to NDCs or
using the resulting list in pharmacoepidemiologic analyses.

Users should also distinguish between two separate choices:

- `concept_status` controls whether active or historical RxNorm concepts
  are included.
- `status` or `ndc_status` controls which NDC status categories are
  returned.

For reproducible research, save the final product list, NDC list, package
version, and API-derived outputs used in the analytic workflow.

