Learning Objectives and Key Concepts

In this exercise, you will:

Drug on Drug Interactions

For this exercise we will explore potential drug on drug interactions in a sizable patient cohort stored in FHIR combined with drug interaction data from the NIH’s Drug RxNAV database.

Motivation/Purpose

From a research persective we can envision leveraging these sorts of analyses to do post-market surveillance of drugs to determine both the rate of known adverse events among patients, as well as to potentially flag additional risks not yet identified.

From a clinical perspective, this exercise demonstrates the ability for third-party data (in this case Drug on Drug interaction data), can be pulled in, paired with FHIR formatted clinical data, and then leveraged to better inform patient care in the form of Clinical Decision Support tools.

### Icons in this Guide πŸ“˜ A link to a useful external reference related to the section the icon appears in

πŸ– A hands-on section where you will code something or interact with the server

Initial setup

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/researchonfhir/open"

Step 1: Query all active prescriptions in our patient cohort

For this exercise we will call on the MedicationRequest which represents a medication prescription in FHIR.

πŸ“˜Read more about the MedicationRequest Resource

Each MedicationRequest represents a single prescription, such that you may have a many-to-one relationship between MedicationRequests and patients, as it is often the case that patients will have multiple prescriptions.

(This fact will be critical for our exercise, as determining a potential drug on drug interaction will require effectively grouping MedicationRequest resources by patient, to determine if the patient is on multiple concurrent prescriptions. We will therefore want to make sure we can include the relevant patient information to ensure we can map multiple prescriptions to individual patients.)

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

View the structure of the first entry in the bundle to help with designing the table description for fhircrackr:

xml2::xml_structure(
  xml2::xml_find_first(x = medication_request_bundle[[1]], xpath = "./entry[1]/resource")
)
## <resource>
##   <MedicationRequest>
##     <id [value]>
##     <meta>
##       <versionId [value]>
##       <lastUpdated [value]>
##       <source [value]>
##     <text>
##       <status [value]>
##       <div>
##         {text}
##     <status [value]>
##     <intent [value]>
##     <medicationCodeableConcept>
##       <coding>
##         <system [value]>
##         <code [value]>
##         <display [value]>
##       <text [value]>
##     <subject>
##       <reference [value]>
##     <dosageInstruction>
##       <text [value]>
##       <timing>
##         <repeat>
##           <boundsPeriod>
##             <start [value]>
##           <frequency [value]>
##           <period [value]>
##           <periodUnit [value]>
##     <dispenseRequest>
##       <numberOfRepeatsAllowed [value]>
##       <quantity>
##         <value [value]>
##         <unit [value]>
##         <system [value]>
##         <code [value]>
##       <expectedSupplyDuration>
##         <value [value]>
##         <unit [value]>
##         <system [value]>
##         <code [value]>
# Identify which elements of the FHIR resource we want to capture in our data frame
table_desc_medication_request <- fhir_table_description(
  resource = "MedicationRequest",
  
  cols = c(
    medication_request_id = "id",
    subject.reference = "subject/reference",
    active = "status",
    rxcode = "medicationCodeableConcept/coding/code"
  )
  
)

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

df_medication_requests

So we now have a basic datafame with drug and patient information. Before we can begin trying to construct a parser, we need to examine our Drug Interaction API to see how data is submitted and returned.

Step 2: Understanding the Drug API and using that API with FHIR data

πŸ“˜Review the NIH’s RXNav API documentation

We see one clear option we have to use the six-digit RxNorm identifier code to query for drug interactions

πŸ“˜Review RXNav API findInteractionsFromList API documentation

This correlates with our Patient data column: resource.medicationCodeableConcept.coding.codes (quite a mouthful! But we’ll deal with that shortly).

Let’s pull two sample interactions using the following general notation:

https://rxnav.nlm.nih.gov/REST/interaction/list.json?rxcuis=[code 1]+[code 2]

Two combinations we can try are: - 207106 and 656659 - 762675 and 859258

πŸ– For each drug combination call the API and display the JSON response

response <- httr::GET(
        url = str_interp("https://rxnav.nlm.nih.gov/REST/interaction/list.json?rxcuis=207106+656659"),
        config = list(add_headers( Accept = 'application/json'))
)

# Look at raw JSON
httr::content(response, as = "text", encoding = "UTF-8") %>% prettify()
## {
##     "nlmDisclaimer": "It is not the intention of NLM to provide specific medical advice, but rather to provide users with information to better understand their health and their medications. NLM urges you to consult with a qualified physician for advice about medications.",
##     "fullInteractionTypeGroup": [
##         {
##             "sourceDisclaimer": "DrugBank is intended for educational and scientific research purposes only and you expressly acknowledge and agree that use of DrugBank is at your sole risk. The accuracy of DrugBank information is not guaranteed and reliance on DrugBank shall be at your sole risk. DrugBank is not intended as a substitute for professional medical advice, diagnosis or treatment..[www.drugbank.ca]",
##             "sourceName": "DrugBank",
##             "fullInteractionType": [
##                 {
##                     "comment": "Drug1 (rxcui = 207106, name = fluconazole 50 MG Oral Tablet [Diflucan], tty = SBD). Drug2 (rxcui = 656659, name = bosentan 125 MG Oral Tablet, tty = SCD). Drug1 is resolved to fluconazole, Drug2 is resolved to bosentan and interaction asserted in DrugBank between Fluconazole and Bosentan. Drug1 is resolved to fluconazole, Drug2 is resolved to bosentan anhydrous and interaction asserted in DrugBank between Fluconazole and Bosentan.",
##                     "minConcept": [
##                         {
##                             "rxcui": "207106",
##                             "name": "fluconazole 50 MG Oral Tablet [Diflucan]",
##                             "tty": "SBD"
##                         },
##                         {
##                             "rxcui": "656659",
##                             "name": "bosentan 125 MG Oral Tablet",
##                             "tty": "SCD"
##                         }
##                     ],
##                     "interactionPair": [
##                         {
##                             "interactionConcept": [
##                                 {
##                                     "minConceptItem": {
##                                         "rxcui": "4450",
##                                         "name": "fluconazole",
##                                         "tty": "IN"
##                                     },
##                                     "sourceConceptItem": {
##                                         "id": "DB00196",
##                                         "name": "Fluconazole",
##                                         "url": "https://go.drugbank.com/drugs/DB00196#interactions"
##                                     }
##                                 },
##                                 {
##                                     "minConceptItem": {
##                                         "rxcui": "1468845",
##                                         "name": "bosentan anhydrous",
##                                         "tty": "PIN"
##                                     },
##                                     "sourceConceptItem": {
##                                         "id": "DB00559",
##                                         "name": "Bosentan",
##                                         "url": "https://go.drugbank.com/drugs/DB00559#interactions"
##                                     }
##                                 }
##                             ],
##                             "severity": "N/A",
##                             "description": "The metabolism of Bosentan can be decreased when combined with Fluconazole."
##                         },
##                         {
##                             "interactionConcept": [
##                                 {
##                                     "minConceptItem": {
##                                         "rxcui": "4450",
##                                         "name": "fluconazole",
##                                         "tty": "IN"
##                                     },
##                                     "sourceConceptItem": {
##                                         "id": "DB00196",
##                                         "name": "Fluconazole",
##                                         "url": "https://go.drugbank.com/drugs/DB00196#interactions"
##                                     }
##                                 },
##                                 {
##                                     "minConceptItem": {
##                                         "rxcui": "75207",
##                                         "name": "bosentan",
##                                         "tty": "IN"
##                                     },
##                                     "sourceConceptItem": {
##                                         "id": "DB00559",
##                                         "name": "Bosentan",
##                                         "url": "https://go.drugbank.com/drugs/DB00559#interactions"
##                                     }
##                                 }
##                             ],
##                             "severity": "N/A",
##                             "description": "The metabolism of Bosentan can be decreased when combined with Fluconazole."
##                         }
##                     ]
##                 }
##             ]
##         }
##     ]
## }
## 

Now for the secondpair of RxNorm codes:

response <- httr::GET(
        url = str_interp("https://rxnav.nlm.nih.gov/REST/interaction/list.json?rxcuis=762675+859258"),
        config = list(add_headers( Accept = 'application/json'))
)

# Look at raw JSON
httr::content(response, as = "text", encoding = "UTF-8") %>% prettify()
## {
##     "nlmDisclaimer": "It is not the intention of NLM to provide specific medical advice, but rather to provide users with information to better understand their health and their medications. NLM urges you to consult with a qualified physician for advice about medications."
## }
## 

We see that if there is an interaction, the fullInteractionTypeGroup element is set at the top level of the JSON response. This can be detected like so:

response <- httr::GET(
        url = str_interp("https://rxnav.nlm.nih.gov/REST/interaction/list.json?rxcuis=207106+656659"),
        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[["fullInteractionTypeGroup"]] %>% length > 0
## [1] TRUE
response <- httr::GET(
        url = str_interp("https://rxnav.nlm.nih.gov/REST/interaction/list.json?rxcuis=762675+859258"),
        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[["fullInteractionTypeGroup"]] %>% length > 0
## [1] FALSE

Feel free to experiment with additional drug combinations, including 3 or more drugs to see how the information varies.

Reviewing the returned output we can begin to analyze the information provided, and assess our approach. Why are the RxNorm codes in the interactionPair different than what we sent? Do we need to care about severity? What other elements could be present and where are the descriptions indicating what each element represents (hint should we be looking back at the API docs to interpret?)

Taking stock, we have successfully accessed the Drug API, and hopefully now have an understanding of what the API returns when there is a drug interaction versus when there isn’t.

We now have important information informing our next steps.

First, we have a structured target to work toward for submitting our patient data to the Drug API. For each patient, we will need to compile a list of RxNorm codes of the prescriptions they are on, and then append them to our API query with a + or %20 between each code. For our next step we’ll go about constructing that!

Second, we have an understanding of how the Drug API returns a known interaction, versus how it returns when there isn’t one. We can begin to consider how the format of this data can be used to indicate - in bulk - the presence or absence of a reaction.

Step 3: Construct a composite list of all drugs per-patient (so we can determine a potential Drug on Drug interaction

The API documentation for the drug/drug interaction endpoint we are using indicates that a maximum of 50 drugs can be assessed at once for interactions.

If we want to submit all the drugs each patient in our dataset is actively taking, we first need to make sure that none have more than 50 active drugs:

df_medication_requests %>%
  filter(active == "active") %>% # Active drugs only
  distinct(subject.reference, rxcode) %>% # Remove any duplicates
  count(subject.reference) %>% # Count the number of drugs for each patient
  arrange(-n) # Sort in descending order

The most active drugs per patient we have is 21, so we can safely make one API call for each patient.

Let’s define a custom function to identify if there are drug/drug interactions using the API:

fn_get_drug_drug_interaction <- function(rxcodes) {
  str_interp("Getting interactions for ${rxcodes}\n") %>% cat
  response <- httr::GET(
    url = str_interp("https://rxnav.nlm.nih.gov/REST/interaction/list.json?rxcuis=${rxcodes}"),
    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[["fullInteractionTypeGroup"]] %>% length > 0)
}
fn_get_drug_drug_interaction <- Vectorize(fn_get_drug_drug_interaction) # Makes this work with `mutate` in dplyr

πŸ– Test our original two drug combinations with the custom function to ensure that it is outputting the expected responses.

# Test to make sure it works
fn_get_drug_drug_interaction("207106+656659") %>% cat
## Getting interactions for 207106+656659
## TRUE
paste("\n\n") %>% cat
fn_get_drug_drug_interaction("762675+859258") %>% cat
## Getting interactions for 762675+859258
## FALSE

Now if we reshape the df_medication_requests data frame from long to wide, and concatenate all the RXNorm codes (separated with a +), we can then run this function on each row to identify the patients with drug/drug interactions.

df <- df_medication_requests %>% 
  filter(active == "active") %>% 
  distinct(subject.reference, rxcode) %>% # Remove any duplicates
  select(subject.reference, rxcode) %>% 
  group_by(subject.reference) %>% 
  summarize(
    rxcodes = str_c(rxcode, collapse = "+"),
    total_rx = sum(!is.na(rxcode))
  )
df %>% glimpse
## Rows: 44
## Columns: 3
## $ subject.reference <chr> "Patient/15170", "Patient/smart-1032702", "Patient/s…
## $ rxcodes           <chr> NA, "582620", "308189+617993+211307+261091+404630+23…
## $ total_rx          <int> 0, 1, 7, 5, 7, 7, 4, 7, 5, 5, 13, 10, 7, 11, 10, 8, …

Try with just two rows to make sure it works:

df %>% 
  filter(total_rx >= 2) %>% 
  head(2) %>% 
  mutate(
    possible_interaction = fn_get_drug_drug_interaction(rxcodes)
  )
## Getting interactions for 308189+617993+211307+261091+404630+239191+208406
## Getting interactions for 150840+309462+310333+351396+310893

Now run for the whole DataFrame – this make take a minute.

df <- df %>% 
  filter(total_rx >= 2) %>% 
  mutate(
    possible_interaction = fn_get_drug_drug_interaction(rxcodes)
  )
## Getting interactions for 308189+617993+211307+261091+404630+239191+208406
## Getting interactions for 150840+309462+310333+351396+310893
## Getting interactions for 239191+352027+104884+755272+308189+106346+617423
## Getting interactions for 308607+312961+381056+866511+866514+197589+762675
## Getting interactions for 151124+877300+311992+285128
## Getting interactions for 745679+404473+284429+404630+352318+352319+745752
## Getting interactions for 314077+404673+153357+197770+199381
## Getting interactions for 106256+197803+313960+198305+309367
## Getting interactions for 762675+859258+795737+261339+312615+310942+198145+309098+259966+309309+200329+580261+199026
## Getting interactions for 104112+153666+198211+206533+729929+562508+198240+861771+745752+313586
## Getting interactions for 313850+309367+313797+309054+197803+308189+312320
## Getting interactions for 206485+823934+314200+198191+617264+312961+855918+197886+746201+284400+314077
## Getting interactions for 197885+311946+308194+311945+213186+859749+762675+828348+859753+854873
## Getting interactions for 200033+309889+828348+855334+213469+261962+197606+310429
## Getting interactions for 745752+213271
## Getting interactions for 153666+153843+314076+153591+197449+199246+197886+314077+309114
## Getting interactions for 310149+239191+630208
## Getting interactions for 198014+198039+866924+213169+262095+311354+686924+311681+198342+204135
## Getting interactions for 197633+208149+748857+284497+199026+284544+750244+314106+308047+753482+312938+309309
## Getting interactions for 309114+309438+562508+312289+202301
## Getting interactions for 197379+310812+580261+745813+790840+859088+197633+312504+856903
## Getting interactions for 569998+1189780+635969+567575+860976
## Getting interactions for 847241+847265+351250+197418+979480+200033+197987+884173+197320+999946+1367410+243670+197527
## Getting interactions for 259255+352304+1098141+1043582+1544918+897646+751616+401968+746201+855174+799040+859088+1092380+411110+197807+828348+854873
## Getting interactions for 200033+200096+243670+402014+966247+707566+745752+896031+352304+577208+880853+240029
## Getting interactions for 748857+352272
## Getting interactions for 199903+617318+198211
## Getting interactions for 197379+197745+197382+197746+845660+403917+316133
## Getting interactions for 573621+896235+373585+1183694
## Getting interactions for 210596+211816+312664+856377+153892+352063+310812+314077+615186+828576+311470+311992+752370
## Getting interactions for 206486+311304+206475+309114+856903+308194+317797+199903+206742+834102+854873+834127+314062+312961
## Getting interactions for 197528+309462+856903+197582+197901+198365+312055+198080+198382+762675+197574+856377
## Getting interactions for 309114+866514+283342+861007+309428+310489+543354+352304+859749+828576
## Getting interactions for 617993+308189
## Getting interactions for 1169923+402097+801663+597983+572018+309094
## Getting interactions for 746735+353534+565420+997489+352272+565167+1167622
## Getting interactions for 323925+966171+215098+801663+812178
## Getting interactions for 1169778+313219+213469+197319+860975
## Getting interactions for 206206+213169+311353+351761+617318+866427+562508+866429
## Getting interactions for 153892+199246+213469+260333+311946+859749+199026+198211+261962+859046+197381+200345+213169+795737+795735+259543+314200+311945+860981+199247+198039
## Getting interactions for 745679+828348+856903+285004+745752+858869+311753+309114
## Getting interactions for 309054+312320

Total number of drug/drug interactions in the cohort for patients with at least 2 active drugs:

df %>% freq(possible_interaction)
## Frequencies  
## df$possible_interaction  
## Type: Logical  
## 
##               Freq   % Valid   % Valid Cum.   % Total   % Total Cum.
## ----------- ------ --------- -------------- --------- --------------
##       FALSE      7     16.67          16.67     16.67          16.67
##        TRUE     35     83.33         100.00     83.33         100.00
##        <NA>      0                               0.00         100.00
##       Total     42    100.00         100.00    100.00         100.00

As a bonus consider some additional information you can output, such as keeping a running count of total interactions, or specific details about the interaction types.

Summary

This exercise demonstrates how FHIR data can interact with the broader ecosystem of healthcare data and resources to determine additional health care insights. Here we pulled data from multiple resources into a unified dataframe, and then modified how the data was stored in order to pass it through to a third-party API and determine health outcomes.