Skip to Content
Course content

135: Working with Electronic Health Record Data Formats

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're trying to organize a massive community library, but every donor sent their books in a different way. Some sent a modern digital spreadsheet (JSON), some sent hand-written ledger cards with cryptic shorthand (HL7 v2), and others sent a formal, printed catalog that looks like a legal document (CCDA). If you want to know how many medical textbooks you have in total, you can't just "count" them; you first have to translate three entirely different languages into one master list.

Working with Electronic Health Record (EHR) data is exactly like that. In the real world, you'll rarely be handed a clean CSV of patient vitals. Instead, you'll be staring at a mix of nested structures and legacy strings. Here is how that library analogy maps to the R code you'll actually be writing:

  • The Digital Spreadsheet (FHIR): This is the modern standard. It's usually JSON. It's structured, but deeply nested. You'll use jsonlite to parse it, but you'll spend most of your time "flattening" the lists.
  • The Handwritten Ledger (HL7 v2): This is the "old school" way. It's pipe-delimited (|). It looks like gibberish at first, but it follows a strict positional logic. You'll use stringr or strsplit to chop these into pieces.
  • The Master List (OMOP CDM): This is the goal. The Observational Medical Outcomes Partnership (OMOP) Common Data Model is where you move all those disparate formats so you can actually run a regression or a T-test without losing your mind.

Wrestling with FHIR's Nested JSON

FHIR (Fast Healthcare Interoperability Resources) is the gold standard now, but it's a pain for R users because R prefers rectangles (data frames), and FHIR is a tree. A single "Patient" resource might have a list of names, and each name might have a list of prefixes.

library(jsonlite)
library(dplyr)

# A simplified FHIR Patient resource
fhir_json <- '{
  "resourceType": "Patient",
  "id": "123",
  "name": [{"family": "Smith", "given": ["John", "Jacob"]}],
  "gender": "male",
  "birthDate": "1980-01-01"
}'

patient_data <- fromJSON(fhir_json)

# Notice that name is a data frame inside a list. 
# To get the family name, you can't just call patient_data$name.
family_name <- patient_data$name$family[1]
print(family_name) # "Smith"

I've spent hours debugging code because I forgot that FHIR arrays (like name) can contain multiple entries. Always check the length of your lists before assuming the first element is the only one.

Decoding the Pipe-Delimited Maze of HL7 v2

If you're working with older hospital systems, you'll encounter HL7 v2. It doesn't use keys like "gender" or "birthDate." Instead, it uses positions. For example, in a PID (Patient Identification) segment, the 5th field is usually the patient's name.

library(stringr)

# A raw HL7 PID segment
hl7_message <- "PID|1||12345^^^MRN||Smith^John||19800101|M"

# Split by the pipe character
fields <- str_split(hl7_message, "\\|")[[1]]

# The 5th field (index 5) contains the name, but it's further split by carets (^)
patient_name_raw <- fields[5] 
name_parts <- str_split(patient_name_raw, "\\^")[[1]]

family_name_hl7 <- name_parts[1]
given_name_hl7 <- name_parts[2]

cat("Patient:", family_name_hl7, given_name_hl7) # "Patient: Smith John"

It feels primitive, right? It is. But this is how a huge portion of the world's healthcare data still moves. The trick here is to build a "parser" function that takes these strings and converts them into a tidy data frame immediately so you never have to look at the pipes again.

Flattening Data for Analysis

The final hurdle is taking these different sources and making them look the same. In clinical informatics, we call this "ETL" (Extract, Transform, Load). Your goal is to move from the "Resource" mindset (one file per patient) to the "Analytical" mindset (one row per observation).

If you have a list of 1,000 FHIR JSON files, don't try to loop through them with a for loop and rbind. It'll be painfully slow. Instead, use purrr::map_df to parse the JSON and bind it into a tibble in one go. This converts the "tree" of the EHR into the "table" that R loves.




📋 Practical Task

Exercise: Extracting Patient Demographics from a FHIR Bundle

You have been given a small "Bundle" of FHIR data represented as a JSON string. This bundle contains two different patient resources. Your task is to parse this JSON and create a clean R data frame with three columns: patient_id, family_name, and gender.

library(jsonlite)
library(dplyr)
library(purrr)

# The raw data bundle
fhir_bundle <- '{
  "resourceType": "Bundle",
  "entry": [
    {
      "resource": {
        "resourceType": "Patient",
        "id": "p001",
        "name": [{"family": "Doe", "given": ["Jane"]}],
        "gender": "female"
      }
    },
    {
      "resource": {
        "resourceType": "Patient",
        "id": "p002",
        "name": [{"family": "Brown", "given": ["Charlie"]}],
        "gender": "male"
      }
    }
  ]
}'

# YOUR CODE HERE:
# 1. Use fromJSON to parse the bundle.
# 2. Use map_df or a similar approach to extract the id, 
#    the first element of the family name, and the gender 
#    from each entry in the bundle.
# 3. Return a data frame named 'patient_summary'.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.