Learning Objectives and Key Concepts

Workshop attendees will learn how to: * Query a FHIR Server to learn about its capabilities * How to read FHIR specifications and understand how it applies to FHIR interactions and data * Search for resources of various types and parse the results to find information of interest * Process paginated responses * Understanding FHIR Resources to find data of interest * Exploring MedicationRequest * Integrate other, non-FHIR based APIs

Icons in this Guide

📘A link to a useful external reference related to the section the icon appears in

Introduction

This notebook explores a FHIR server with a RESTful API which contains data with patients currently prescribed opioids. We’ll explore the FHIR Server to learn about its capabilities, query for FHIR resources, and generate some basic visualizations from the data.

Initial setup

First, let’s configure the environment with the libraries and settings that will be used throughout the rest of the exercise.

library(fhircrackr)
library(tidyverse)
## ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.1 ──
## ✓ ggplot2 3.3.5     ✓ purrr   0.3.4
## ✓ tibble  3.1.6     ✓ dplyr   1.0.7
## ✓ tidyr   1.1.4     ✓ stringr 1.4.0
## ✓ readr   2.1.1     ✓ forcats 0.5.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()
library(skimr)
library(summarytools)
## 
## Attaching package: 'summarytools'
## The following object is masked from 'package:tibble':
## 
##     view
# Used for direct RESTful queries against the FHIR server
library(httr)
library(jsonlite)
## 
## Attaching package: 'jsonlite'
## The following object is masked from 'package:purrr':
## 
##     flatten
library(lubridate) # Datetime manipulation
## 
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
## 
##     date, intersect, setdiff, union
# Visualizations
library(ggthemes)
theme_set(ggthemes::theme_economist_white())

Environment configuration

fhir_server <- "https://api.logicahealth.org/opioids/open"

Querying the Server

All servers are required to support the capabilities interaction which documents the server’s functionality. The capability interaction is of the form:

GET [base]/metadata

📘Read more about the capabilities interaction

fhircrackr provides an easy way to query this endpoint and load the results into R:

cs <- fhir_capability_statement(fhir_server)
## 
##  Meta
##  1.
## FHIR-Resources cracked.
## 
##  Rest
##  1.
## FHIR-Resources cracked.
## 
##  Resources
##  1..................................................................................................................................................
## FHIR-Resources cracked.

The CapabilityStatement is usually a very large resource so we’ll focus on a few key elements. Namely, the FHIR version supported, the Resource formats supported, and the endpoint types.

str_c("FHIR Version Supported: ", cs$Meta$fhirVersion, "\n") %>% cat
## FHIR Version Supported: 4.0.1
str_c("Formats Supported: ", cs$Meta$format, "\n") %>% cat
## Formats Supported: application/fhir+xml || application/fhir+json
str_c("FHIR Resources Supported: ", cs$Resources$type %>% length, "\n") %>% cat
## FHIR Resources Supported: 146

We can also see the details of the types of FHIR Resources available on this endpoint and the supported operations. Keep in mind that there might not be any data available for a particular FHIR resource even if the server supports that type.

cs$Resources %>% glimpse
## Rows: 146
## Columns: 11
## $ type                      <chr> "Account", "ActivityDefinition", "AdverseEve…
## $ profile                   <chr> "http://hl7.org/fhir/StructureDefinition/Acc…
## $ interaction.code          <chr> "read || vread || update || patch || delete …
## $ versioning                <chr> "versioned-update", "versioned-update", "ver…
## $ conditionalCreate         <chr> "true", "true", "true", "true", "true", "tru…
## $ conditionalUpdate         <chr> "true", "true", "true", "true", "true", "tru…
## $ conditionalDelete         <chr> "multiple", "multiple", "multiple", "multipl…
## $ searchInclude             <chr> "* || Account:owner || Account:patient || Ac…
## $ searchParam.name          <chr> "_language || owner || identifier || period …
## $ searchParam.type          <chr> "string || reference || token || date || ref…
## $ searchParam.documentation <chr> "The language of the resource || Entity mana…

If you look at the full data frame below, the interaction.code column tells you which FHIR interactions are supported for a given resource type.

cs$Resources

FHIR Resources

We can see that this FHIR server supports a lot of resources, but what resources might we typically expect a FHIR server to support and where can we learn more about them?

The FHIR spec defines a large number of resources. Some of these resources have been widely implemented and have stable structures, while others are merely considered draft. As a standards framework, the FHIR specification itself largely does not require that an implementation support any specific resources.

Implementation Guides, like the US Core Implementation Guide, which reflects the U.S. Core Data for Interoperability (USCDI) illustrate the resources and profiles on those resources that an implementation would be expected to support for most US health data.

📘Read more about FHIR Resources

📘Read more about FHIR Maturity Levels

📘Read more about US Core

📘Read more about USCDI

FHIR Profiles and Data Types

The structure of a FHIR resource is defined by a FHIR StructureDefinition. Some of the key things a FHIR StructureDefinition defines for a resource includes:

Profiles within Implementation Guides build off the StructureDefinitions within the base FHIR specification to further constrain requirements or add expectations around extensions to support additional information not covered by the base resource profile.

Base FHIR types

📘Read more about StructureDefinitions

📘Read more about Data Types

📘Read more about Cardinality

📘Read more about Terminology Bindings

Querying the Server

The FHIR Spec has a nice summary cheat sheet which is helpful for crafting queries and understanding the resources they return.

FHIR cheatsheet

(Source: https://www.hl7.org/fhir/http.html#summary)

Now that we know a little bit about the server, let’s query for all the patients.

request <- fhir_url(url = fhir_server, resource = "Patient")
patient_bundle <- fhir_search(request = request)
## Starting download of ALL! bundles of resource type https://api.logicahealth.org/opioids/open/Patient from FHIR base URL https://api.logicahealth.org/opioids/open/Patient.
## This may take a while...
## 
## Download completed. All available bundles were downloaded.

Note that FHIR servers will typically split responses into “pages” to limit the response size. This is important for servers that could have thousands or millions of instances of a given resource.

By default, fhircrackr will make a separate request to get the Bundle of FHIR resources contained in each “page” from the server for a given query, and will automatically stitch the results together into a single data frame. However, this can take a long time, so you may wish to set the maximum number of Bundles downloaded by fhircrackr, like fhir_search(request = request, max_bundles = 1).

You can also request a certain number of resources in each Bundle with the _count parameter.

Once we have our Bundle(s) of Patient resources, we need to define the table description for fhircrackr to convert from the FHIR resources into data frames. To understand the structure of the XML, let’s look at the second resource returned by the server (using ./entry[2]/resource as the XPath query to get the 2nd resource instead of ./entry[1]/resource to get the first resource; the first one is missing some data that the others have):

xml2::xml_find_first(x = patient_bundle[[1]], xpath = "./entry[2]/resource") %>%
  paste0 %>%
  cat
## <resource>
##   <Patient>
##     <id value="4085"/>
##     <meta>
##       <versionId value="1"/>
##       <lastUpdated value="2021-12-29T23:37:18.000+00:00"/>
##       <source value="#yTw02TRSmhZHShp5"/>
##       <profile value="http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"/>
##     </meta>
##     <text>
##       <status value="generated"/>
##       <div>Generated by 
##                   <a href="https://github.com/synthetichealth/synthea">Synthea</a>.Version identifier: master-branch-latest
##  .   Person seed: -4114173650285919840  Population seed: 1640814769112
##                </div>
##     </text>
##     <extension url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-race">
##       <extension url="ombCategory">
##         <valueCoding>
##           <system value="urn:oid:2.16.840.1.113883.6.238"/>
##           <code value="2106-3"/>
##           <display value="White"/>
##         </valueCoding>
##       </extension>
##       <extension url="text">
##         <valueString value="White"/>
##       </extension>
##     </extension>
##     <extension url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity">
##       <extension url="ombCategory">
##         <valueCoding>
##           <system value="urn:oid:2.16.840.1.113883.6.238"/>
##           <code value="2186-5"/>
##           <display value="Not Hispanic or Latino"/>
##         </valueCoding>
##       </extension>
##       <extension url="text">
##         <valueString value="Not Hispanic or Latino"/>
##       </extension>
##     </extension>
##     <extension url="http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName">
##       <valueString value="Shawnee493 Hoeger474"/>
##     </extension>
##     <extension url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex">
##       <valueCode value="F"/>
##     </extension>
##     <extension url="http://hl7.org/fhir/StructureDefinition/patient-birthPlace">
##       <valueAddress>
##         <city value="Saugus"/>
##         <state value="Massachusetts"/>
##         <country value="US"/>
##       </valueAddress>
##     </extension>
##     <extension url="http://synthetichealth.github.io/synthea/disability-adjusted-life-years">
##       <valueDecimal value="0.0"/>
##     </extension>
##     <extension url="http://synthetichealth.github.io/synthea/quality-adjusted-life-years">
##       <valueDecimal value="23.0"/>
##     </extension>
##     <identifier>
##       <system value="https://github.com/synthetichealth/synthea"/>
##       <value value="3c2f9134-21ad-ac38-1f7e-d9fa0e0c010b"/>
##     </identifier>
##     <identifier>
##       <type>
##         <coding>
##           <system value="http://terminology.hl7.org/CodeSystem/v2-0203"/>
##           <code value="MR"/>
##           <display value="Medical Record Number"/>
##         </coding>
##         <text value="Medical Record Number"/>
##       </type>
##       <system value="http://hospital.smarthealthit.org"/>
##       <value value="3c2f9134-21ad-ac38-1f7e-d9fa0e0c010b"/>
##     </identifier>
##     <identifier>
##       <type>
##         <coding>
##           <system value="http://terminology.hl7.org/CodeSystem/v2-0203"/>
##           <code value="SS"/>
##           <display value="Social Security Number"/>
##         </coding>
##         <text value="Social Security Number"/>
##       </type>
##       <system value="http://hl7.org/fhir/sid/us-ssn"/>
##       <value value="999-41-3902"/>
##     </identifier>
##     <identifier>
##       <type>
##         <coding>
##           <system value="http://terminology.hl7.org/CodeSystem/v2-0203"/>
##           <code value="DL"/>
##           <display value="Driver's License"/>
##         </coding>
##         <text value="Driver's License"/>
##       </type>
##       <system value="urn:oid:2.16.840.1.113883.4.3.25"/>
##       <value value="S99997666"/>
##     </identifier>
##     <identifier>
##       <type>
##         <coding>
##           <system value="http://terminology.hl7.org/CodeSystem/v2-0203"/>
##           <code value="PPN"/>
##           <display value="Passport Number"/>
##         </coding>
##         <text value="Passport Number"/>
##       </type>
##       <system value="http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber"/>
##       <value value="X42965923X"/>
##     </identifier>
##     <name>
##       <use value="official"/>
##       <family value="Konopelski743"/>
##       <given value="Doloris378"/>
##       <prefix value="Ms."/>
##     </name>
##     <telecom>
##       <system value="phone"/>
##       <value value="555-830-4368"/>
##       <use value="home"/>
##     </telecom>
##     <gender value="female"/>
##     <birthDate value="1997-07-02"/>
##     <address>
##       <extension url="http://hl7.org/fhir/StructureDefinition/geolocation">
##         <extension url="latitude">
##           <valueDecimal value="42.80665114148182"/>
##         </extension>
##         <extension url="longitude">
##           <valueDecimal value="-70.86563026842043"/>
##         </extension>
##       </extension>
##       <line value="867 Block Passage Unit 41"/>
##       <city value="Newburyport"/>
##       <state value="MA"/>
##       <country value="US"/>
##     </address>
##     <maritalStatus>
##       <coding>
##         <system value="http://terminology.hl7.org/CodeSystem/v3-MaritalStatus"/>
##         <code value="S"/>
##         <display value="Never Married"/>
##       </coding>
##       <text value="Never Married"/>
##     </maritalStatus>
##     <multipleBirthBoolean value="false"/>
##     <communication>
##       <language>
##         <coding>
##           <system value="urn:ietf:bcp:47"/>
##           <code value="en-US"/>
##           <display value="English"/>
##         </coding>
##         <text value="English"/>
##       </language>
##     </communication>
##   </Patient>
## </resource>

You can also just print the structure, which can help when constructing XPath queries:

xml2::xml_structure(
  xml2::xml_find_first(x = patient_bundle[[1]], xpath = "./entry[2]/resource")
)
## <resource>
##   <Patient>
##     <id [value]>
##     <meta>
##       <versionId [value]>
##       <lastUpdated [value]>
##       <source [value]>
##       <profile [value]>
##     <text>
##       <status [value]>
##       <div>
##         {text}
##         <a [href]>
##           {text}
##         {text}
##     <extension [url]>
##       <extension [url]>
##         <valueCoding>
##           <system [value]>
##           <code [value]>
##           <display [value]>
##       <extension [url]>
##         <valueString [value]>
##     <extension [url]>
##       <extension [url]>
##         <valueCoding>
##           <system [value]>
##           <code [value]>
##           <display [value]>
##       <extension [url]>
##         <valueString [value]>
##     <extension [url]>
##       <valueString [value]>
##     <extension [url]>
##       <valueCode [value]>
##     <extension [url]>
##       <valueAddress>
##         <city [value]>
##         <state [value]>
##         <country [value]>
##     <extension [url]>
##       <valueDecimal [value]>
##     <extension [url]>
##       <valueDecimal [value]>
##     <identifier>
##       <system [value]>
##       <value [value]>
##     <identifier>
##       <type>
##         <coding>
##           <system [value]>
##           <code [value]>
##           <display [value]>
##         <text [value]>
##       <system [value]>
##       <value [value]>
##     <identifier>
##       <type>
##         <coding>
##           <system [value]>
##           <code [value]>
##           <display [value]>
##         <text [value]>
##       <system [value]>
##       <value [value]>
##     <identifier>
##       <type>
##         <coding>
##           <system [value]>
##           <code [value]>
##           <display [value]>
##         <text [value]>
##       <system [value]>
##       <value [value]>
##     <identifier>
##       <type>
##         <coding>
##           <system [value]>
##           <code [value]>
##           <display [value]>
##         <text [value]>
##       <system [value]>
##       <value [value]>
##     <name>
##       <use [value]>
##       <family [value]>
##       <given [value]>
##       <prefix [value]>
##     <telecom>
##       <system [value]>
##       <value [value]>
##       <use [value]>
##     <gender [value]>
##     <birthDate [value]>
##     <address>
##       <extension [url]>
##         <extension [url]>
##           <valueDecimal [value]>
##         <extension [url]>
##           <valueDecimal [value]>
##       <line [value]>
##       <city [value]>
##       <state [value]>
##       <country [value]>
##     <maritalStatus>
##       <coding>
##         <system [value]>
##         <code [value]>
##         <display [value]>
##       <text [value]>
##     <multipleBirthBoolean [value]>
##     <communication>
##       <language>
##         <coding>
##           <system [value]>
##           <code [value]>
##           <display [value]>
##         <text [value]>

Each instance of a given resource will be roughly the same (assuming the FHIR server is working properly), so looking at the structure of just one resource typically provides most of the information you’ll need to extract the data from all the resources. By incrementing entry[1] to entry[2] (to see the next resource in the bundle) or [[1]] to [[2]] (to see the _n_th resource in the 2nd bundle), etc. in the commands above you can look at additional resource instances.

You may also want to reference the FHIR specification for the resource you’re working with (in this case, Patient) to better understand what elements may be available, as well as documentation specific to the server’s implementation (which may be found in a FHIR Implementation Guide).

Using this information, we can use these to create the fhircrackr “table description”:

table_desc_patient <- fhir_table_description(
  resource = "Patient",

  cols = c(
    PID           = "id",
    given_name    = "name/given",
    family_name   = "name/family",
    gender        = "gender",
    birthday      = "birthDate",
    maritalStatus = "maritalStatus/coding[1]/code", # Note that marital status is not in the example we printed above - but you can see it may be available
                                                    # by looking at the FHIR spec: https://www.hl7.org/fhir/R4/patient.html
    maritalStatusDisplay = "maritalStatus/coding[1]/display"
  )

)

# Convert to R data frame
df_patient <- fhir_crack(bundles = patient_bundle, design = table_desc_patient, verbose = 0)

df_patient

Patient Demographics

Now that we have all our patients, let’s try and analyze their demographics.

First lets find the frequency of gender values:

freq(df_patient, gender)
## Frequencies  
## df_patient$gender  
## Type: Character  
## 
##                Freq   % Valid   % Valid Cum.   % Total   % Total Cum.
## ------------ ------ --------- -------------- --------- --------------
##       female     43     45.26          45.26     45.26          45.26
##         male     52     54.74         100.00     54.74         100.00
##         <NA>      0                               0.00         100.00
##        Total     95    100.00         100.00    100.00         100.00

Let’s also look at the frequency of the marital status codes.

freq(df_patient, maritalStatus)
## Frequencies  
## df_patient$maritalStatus  
## Type: Character  
## 
##               Freq   % Valid   % Valid Cum.   % Total   % Total Cum.
## ----------- ------ --------- -------------- --------- --------------
##           M     45     47.37          47.37     47.37          47.37
##           S     50     52.63         100.00     52.63         100.00
##        <NA>      0                               0.00         100.00
##       Total     95    100.00         100.00    100.00         100.00

M and S codes are a bit cryptic, so let’s check them against the display text and create a cross tabulation of the factors.

ctable(df_patient$maritalStatusDisplay, df_patient$maritalStatus)
## Cross-Tabulation, Row Proportions  
## maritalStatusDisplay * maritalStatus  
## Data Frame: df_patient  
## 
## ---------------------- --------------- ------------- ------------- -------------
##                          maritalStatus             M             S         Total
##   maritalStatusDisplay                                                          
##                      M                   45 (100.0%)    0 (  0.0%)   45 (100.0%)
##          Never Married                    0 (  0.0%)   36 (100.0%)   36 (100.0%)
##                      S                    0 (  0.0%)   14 (100.0%)   14 (100.0%)
##                  Total                   45 ( 47.4%)   50 ( 52.6%)   95 (100.0%)
## ---------------------- --------------- ------------- ------------- -------------

It looks like there may be some data quality issues with this variable – why does maritalStatusDisplay have both Never Married and S if those equate to the same code of S?

Patient age isn’t directly available in the data set, but can be calculated via the patient’s birthday.

df_patient %>%
        mutate(
          # `%--%` creates an interval: https://lubridate.tidyverse.org/reference/interval.html
          # `/ years(1)` converts the interval into a number of years
          age = (lubridate::date(birthday) %--% lubridate::today()) / years(1)
        ) %>%
        ggplot(aes(age)) +
        geom_histogram()
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Querying for Resources

FHIR breaks up health information into chunks of data called Resources which are connected together via references.

In this use case we’re interested in patients who’ve been prescribed opioids. Looking at the FHIR Patient profile we see a few references to things like their primary care provider or the organization managing the patient record, but nothing about medications. Luckily, above this profile we see a list of other resources that reference Patient - including MedicationRequest. Looking within MedicationRequest we see that MedicationRequest.subject identifies the patient the medication is for.

Let’s see if we can find the Medications prescribed to the patient with id 10098. The core FHIR spec doesn’t require that any specific searches be supported so it’s important to read the documentation and look at the CapabilityStatement of the server being queried to get an idea of the options available. For now we’ll look at the core FHIR spec for an idea of search parameters defined in the base spec that servers might implement.

Looking at the MedicationRequest Resource core FHIR documentation it looks like there are two search parameters that would be helpful: patient and subject. Practically either would work just fine, but looking at the Expression we can see that patient only works for references to a Patient resource, while subject would work for references to either a Patient or a Group. Looking at the CapabilityStatement, it also appears that the server supports both!

(cs$Resources %>% filter(type == "MedicationRequest"))$searchParam.name %>%
  str_replace_all(" \\|\\| ", "\n") %>% cat
## _language
## date
## requester
## identifier
## intended-dispenser
## authoredon
## code
## subject
## medication
## encounter
## priority
## intent
## intended-performer
## patient
## intended-performertype
## _id
## category
## status

Note: Specific FHIR Implementation Guides, like US Core, may define their own SearchParameters for servers to implement

📘Read more about FHIR Resource Organization

Let’s request the MedicationRequest resources for a specific patient from the server:

request <-
  fhir_url(
    url = fhir_server,
    resource = "MedicationRequest",
    parameters = c(patient = "10098")
  )
medication_request_bundle <- fhir_search(request = request)
## Starting download of ALL! bundles of resource type MedicationRequest from FHIR base URL https://api.logicahealth.org/opioids/open.
## This may take a while...
## 
## Download completed. All available bundles were downloaded.

Let’s look at one of the resources returned by the server:

xml2::xml_find_first(x = medication_request_bundle[[1]], xpath = "./entry[1]/resource") %>%
  paste0 %>%
  cat
## <resource>
##   <MedicationRequest>
##     <id value="10389"/>
##     <meta>
##       <versionId value="1"/>
##       <lastUpdated value="2021-12-29T23:58:33.000+00:00"/>
##       <source value="#p9JfGLVEkVh9gJXV"/>
##       <profile value="http://hl7.org/fhir/us/core/StructureDefinition/us-core-medicationrequest"/>
##     </meta>
##     <status value="stopped"/>
##     <intent value="order"/>
##     <medicationCodeableConcept>
##       <coding>
##         <system value="http://www.nlm.nih.gov/research/umls/rxnorm"/>
##         <code value="835603"/>
##         <display value="tramadol hydrochloride 50 MG Oral Tablet"/>
##       </coding>
##       <text value="tramadol hydrochloride 50 MG Oral Tablet"/>
##     </medicationCodeableConcept>
##     <subject>
##       <reference value="Patient/10098"/>
##     </subject>
##     <encounter>
##       <reference value="Encounter/10371"/>
##     </encounter>
##     <authoredOn value="2016-04-12T20:46:17-04:00"/>
##     <requester>
##       <reference value="Practitioner/2129"/>
##       <display value="Dr. Jefferson174 Murphy561"/>
##     </requester>
##     <dosageInstruction>
##       <sequence value="1"/>
##       <text value="Take as needed."/>
##       <asNeededBoolean value="true"/>
##     </dosageInstruction>
##   </MedicationRequest>
## </resource>

You can also look at the MedicationRequest spec to help determine which elements are populated.

We can use this information to construct the table description that will allow us to convert this to a data frame:

table_desc_medication_request <- fhir_table_description(
  resource = "MedicationRequest",

  cols = c(
    patient = "subject/reference",
    med_code_value = "medicationCodeableConcept/coding/code",
    med_code_system = "medicationCodeableConcept/coding/system",
    med_code_display = "medicationCodeableConcept/coding/display"
  )

)

# Convert to R data frame
df_meds <-
  fhir_crack(bundles = medication_request_bundle,
             design = table_desc_medication_request,
             verbose = 0)

df_meds

Looks like this patient has been prescribed tramadol hydrocholoride.

FHIR servers can provide terminology services as well so let’s $lookup some additional details about this code. In particular, let’s check to see if the code is Inactive.

📘Read more about FHIR Terminology

📘Read more about using RxNorm with FHIR

Unfortunately, fhircrackr does not support this query so we will need to use httr, a lower-level library for making generic (not just FHIR-related) web requests in R. (You can replicate the queries we’ve done with fhircrackr using httr instead; see exercise_1_appendix.Rmd.)

response <- httr::GET(
  url = str_interp("http://tx.fhir.org/r4/CodeSystem/$lookup?system=http://www.nlm.nih.gov/research/umls/rxnorm&code=835603"),
  config = list(add_headers( Accept = 'application/fhir+json'))
)

# Convert from raw `httr` response into an R list for easier access
response_list <- fromJSON(httr::content(response, as = "text", encoding = "UTF-8"), flatten = TRUE)

response_list$parameter[4,]$part
## [[1]]
##    name valueCode valueBoolean
## 1  code  inactive           NA
## 2 value      <NA>        FALSE

Here we can see that the code is not inactive.

But what if we want to know something the FHIR Terminology server can’t provide like the brand name for our research or a SMART-on-FHIR app used by clinicians? Let’s see if the RxNorm API can help. This is where we can look to other APIs!

https://lhncbc.nlm.nih.gov/RxNav/APIs/RxNormAPIs.html

Looks like https://lhncbc.nlm.nih.gov/RxNav/APIs/api-RxNorm.getAllRelatedInfo.html will have what we need since I know I want the Brand Name term type

response <- httr::GET(
  url = str_interp("https://rxnav.nlm.nih.gov/REST/rxcui/835603/allrelated.json"),
  config = list(add_headers( Accept = 'application/json'))
)

# Convert from raw `httr` response into an R list for easier access
response_list <- fromJSON(httr::content(response, as = "text", encoding = "UTF-8"), flatten = TRUE)

response_list %>% glimpse
## List of 1
##  $ allRelatedGroup:List of 2
##   ..$ rxcui       : chr ""
##   ..$ conceptGroup:'data.frame': 16 obs. of  2 variables:
##   .. ..$ tty              : chr [1:16] "BN" "BPCK" "DF" "DFG" ...
##   .. ..$ conceptProperties:List of 16

We can pull the brand names out of this:

(response_list$allRelatedGroup$conceptGroup %>% filter(tty == "BN"))$conceptProperties[[1]]$name
## [1] "Ultram"

What if we want to know the drug schedule?

That’s in the getAllProperties API

response <- httr::GET(
  url = str_interp("https://rxnav.nlm.nih.gov/REST/rxcui/835603/allProperties.json?prop=Attributes"),
  config = list(add_headers( Accept = 'application/json'))
)

# Convert from raw `httr` response into an R list for easier access
response_list <- fromJSON(httr::content(response, as = "text", encoding = "UTF-8"), flatten = TRUE)

response_list %>% glimpse
## List of 1
##  $ propConceptGroup:List of 1
##   ..$ propConcept:'data.frame':  13 obs. of  3 variables:
##   .. ..$ propCategory: chr [1:13] "ATTRIBUTES" "ATTRIBUTES" "ATTRIBUTES" "ATTRIBUTES" ...
##   .. ..$ propName    : chr [1:13] "Active_ingredient_name" "Active_ingredient_RxCUI" "Active_moiety_name" "Active_moiety_RxCUI" ...
##   .. ..$ propValue   : chr [1:13] "tramadol hydrochloride" "82110" "tramadol" "10689" ...

We can pull the schedule out of this:

response_list$propConceptGroup$propConcept %>% filter(propName == "SCHEDULE")

Looks like it’s Schedule 4. It’s even listed as an example on the FDA Drug Schedule website

The RxNorm APIs offer a number of other helpful services which can be used to determine things like relationships to other terminology systems including Medication Reference Terminology (MED-RT) for determining what the drug may treat and the Anatomical Therapeutic Chemical Classification System (ATC) to get the class of drug.

response <- httr::GET(
  url = str_interp("https://rxnav.nlm.nih.gov/REST/rxclass/class/byRxcui.json?rxcui=835603"),
  config = list(add_headers( Accept = 'application/json'))
)

# Convert from raw `httr` response into an R list for easier access
response_list <- fromJSON(httr::content(response, as = "text", encoding = "UTF-8"), flatten = TRUE)

response_list %>% glimpse
## List of 1
##  $ rxclassDrugInfoList:List of 1
##   ..$ rxclassDrugInfo:'data.frame':  34 obs. of  9 variables:
##   .. ..$ rela                           : chr [1:34] "isa_structure" "isa_disposition" "isa_disposition" "" ...
##   .. ..$ relaSource                     : chr [1:34] "SNOMEDCT" "SNOMEDCT" "SNOMEDCT" "MESH" ...
##   .. ..$ minConcept.rxcui               : chr [1:34] "10689" "10689" "10689" "10689" ...
##   .. ..$ minConcept.name                : chr [1:34] "tramadol" "tramadol" "tramadol" "tramadol" ...
##   .. ..$ minConcept.tty                 : chr [1:34] "IN" "IN" "IN" "IN" ...
##   .. ..$ rxclassMinConceptItem.classId  : chr [1:34] "1149498006" "360204007" "406456001" "D000701" ...
##   .. ..$ rxclassMinConceptItem.className: chr [1:34] "Ether structure-containing product" "Opioid receptor agonist-containing product" "NMDA receptor antagonist-containing product" "Analgesics, Opioid" ...
##   .. ..$ rxclassMinConceptItem.classType: chr [1:34] "STRUCT" "DISPOS" "DISPOS" "MESHPA" ...
##   .. ..$ rxclassMinConceptItem.classUrl : chr [1:34] "http://snomed.info/id/1149498006" "http://snomed.info/id/360204007" "http://snomed.info/id/406456001" NA ...
response_list$rxclassDrugInfoList$rxclassDrugInfo %>% filter(rela == "may_treat") %>% select("minConcept.name", "rxclassMinConceptItem.className")

What about ATC class? This uses the same API as what the drug treats, so we can also pull that out of the same response.

response_list$rxclassDrugInfoList$rxclassDrugInfo %>% filter(relaSource == "ATC") %>%
        select("minConcept.name", "rxclassMinConceptItem.className")

If we want to add the ATC class name to every medication for our patient, we can do this by defining a custom function that calls the API and extracts the first className value, and then applying this to each row in the df_meds data frame.

fn_get_atc_class <- function(rxnorm) {
  print(str_interp("Getting ATC class for ${rxnorm}"))
  response <- httr::GET(
    url = str_interp("https://rxnav.nlm.nih.gov/REST/rxclass/class/byRxcui.json?rxcui=${rxnorm}"),
    config = list(add_headers( Accept = 'application/json'))
  )

  # Convert from raw `httr` response into an R list for easier access
  response_list <- fromJSON(httr::content(response, as = "text", encoding = "UTF-8"), flatten = TRUE)

  return((response_list$rxclassDrugInfoList$rxclassDrugInfo %>% filter(relaSource == "ATC"))[1,"rxclassMinConceptItem.className"])
}

# Test to make sure it works
fn_get_atc_class("835603")
## [1] "Getting ATC class for 835603"
## [1] "Other opioids"

Now apply this to each row in df_meds (we will actually only do the first 5 rows to save time):

fn_get_atc_class_vectorized <- Vectorize(fn_get_atc_class)
df_meds %>%
        mutate(
          atc_class = fn_get_atc_class_vectorized(med_code_value)
        ) %>%
  select(patient, med_code_value, atc_class, everything())
## [1] "Getting ATC class for 835603"
## [1] "Getting ATC class for 1860491"
## [1] "Getting ATC class for 835603"
## [1] "Getting ATC class for 1860491"
## [1] "Getting ATC class for 748962"
## [1] "Getting ATC class for 1860491"
## [1] "Getting ATC class for 1860491"
## [1] "Getting ATC class for 835603"
## [1] "Getting ATC class for 835603"
## [1] "Getting ATC class for 748962"
## [1] "Getting ATC class for 1534809"
## [1] "Getting ATC class for 749762"
## [1] "Getting ATC class for 835603"
## [1] "Getting ATC class for 835603"
## [1] "Getting ATC class for 1860491"
## [1] "Getting ATC class for 1860491"
## [1] "Getting ATC class for 835603"
## [1] "Getting ATC class for 835603"
## [1] "Getting ATC class for 1860491"
## [1] "Getting ATC class for 1860491"
## [1] "Getting ATC class for 835603"
## [1] "Getting ATC class for 1860491"
## [1] "Getting ATC class for 749762"
## [1] "Getting ATC class for 1000126"
## [1] "Getting ATC class for 757594"

In Exercise 3 we’ll work more with the RxNav APIs to detect drug interactions in a patient.

There’s other APIs we could use as well, including FDA APIs where we can retrieve things like adverse event reports or product labels for specific drugs.

https://open.fda.gov/apis/

response <- httr::GET(
  url = str_interp("https://api.fda.gov/drug/label.json?search=openfda.rxcui.exact=835603"),
  config = list(add_headers( Accept = 'application/json'))
)

# Convert from raw `httr` response into an R list for easier access
response_list <- fromJSON(httr::content(response, as = "text", encoding = "UTF-8"), flatten = TRUE)

response_list$results$mechanism_of_action %>% paste %>% cat
## 12.1 Mechanism of Action Tramadol hydrochloride tablets contain tramadol, an opioid agonist and inhibitor of norepinephrine and serotonin re- uptake. Although the mode of action is not completely understood, the analgesic effect of tramadol is believed to be due to both binding to µ-opioid receptors and weak inhibition of re- uptake of norepinephrine and serotonin. Opioid activity is due to both low affinity binding of the parent compound and higher affinity binding of the O -demethylated metabolite M1 to µ-opioid receptors. In animal models, M1 is up to 6 times more potent than tramadol in producing analgesia and 200 times more potent in µ opioid binding. Tramadol-induced analgesia is only partially antagonized by the opioid antagonist naloxone in several animal tests. The relative contribution of both tramadol and M1 to human analgesia is dependent upon the plasma concentrations of each compound [see Clinical Pharmacology (12.2)] . Analgesia in humans begins approximately within one hour after administration and reaches a peak in approximately two to three hours.

Learning Objectives and Key Concepts

As a recap, this this exercise you learned how to: