2 post(s) tagged with "fhir"

View All Tags

Don’t Get BHurned - Keeping an Eye on FHIR Safety.

As a clinician at Commure, one of my roles is stressing the importance of clinically “safe practice” when working with FHIR data. This post will go over some of the expectations clinicians have about their data and demonstrate how ignoring some basic safety principles can lead to delayed or incorrect clinical decisions. For starters, FHIR has a safety checklist which can be found here: FHIR safety, and an accompanying blog post here: FHIR Implementer’s Safety Checklist. I would encourage all healthcare developers to check out Grahame Grieve’s post which includes examples regarding synchronization: Hard #FHIR Safety Problem: Synchronization. Why does this matter? Borrowing a quote from Grieve’s post,

alt text

“These safety checks … are mostly chores, and are easily ignored, but a warning: you ignore them at your peril. Actually, it’s worse than that – you ignore them at other people’s peril.”

The Picky Physician

Physicians consume thousands of discrete data points in their daily practice. To navigate this deluge of data, they have all developed search patterns for navigating this data, whether it be in the EHR, emails, clinical decision support tools, or other sources of data. Search patterns allow physicians to know that by the time they have dissected the chart, they will have consumed all relevant data about a patient to make fully informed clinical decisions. Therefore, even subtle UI changes can be disruptive to these search patterns.

Imagine you are a developer building an app for an inpatient doctor. One common feature request would be a basic metabolic panel that is composed of seven laboratory tests that are commonly grouped together: Sodium, Potassium, Chloride, Bicarbonate, BUN, Creatinine, and Glucose.

Being a helpful developer, you decide to organize these labs alphabetically:

1/2/201/3/201/14/20
Bicarbonate272528
BUN7108
Chloride10098102
Creatinine0.70.60.8
Glucose300200400
Potassium44.34.5
Sodium130128131

Unfortunately, this alphabetizing has a negative effect. Physicians expect this particular set of labs to be in a certain order as a part of their search pattern. Instead, the ordering should look like this:

1/2/201/3/201/14/20
Sodium130128131
Chloride10098102
Bicarbonate272528
Potassium44.34.5
BUN7108
Creatinine0.70.60.8
Glucose300200400

If you had presented the physician with the alphabetical ordering above instead of the expected ordering, they might have looked at the Sodium row expecting it to be the Glucose row and consumed incorrect “normal” valued glucoses (remember they are often skimming through this in milliseconds), whereas the glucose values are rather high here.

I hope the above example illustrates that small changes in UI can have major clinical impacts and the second is that you should constantly iterate with your clinical users to make sure that things are displayed in a way that fits into their search patterns (within reason).

With this in mind, we built the Commure component library to make it easy for developers to create fantastic components for physicians. Take a look at our Laboratory components and Vital Signs components, which takes FHIR Observations and sorts them for you.

When The Absence of Evidence isn’t the Evidence of Absence

Let’s dig a bit further into other assumptions clinicians might be making when looking at your lab table and let’s assume that this data is from Commure Community Clinic. Lets look at the well-formatted table again:

1/2/201/3/201/14/20
CCC- Sodium130128131
CCC- Chloride10098102
CCC- Bicarbonate272528
CCC- Potassium44.34.5
CCC- BUN7108
CCC- Creatinine0.70.60.8
CCC- Glucose300200400

Notice here that instead of just a plain Sodium, Chloride, etc labels, I’m now showing CCC- Sodium. This means that the Sodium is coded as a Commure Community Clinic- specific Sodium code, something that’s very common in most healthcare settings. Let’s say that the clinician is looking at this table on Feb 2, 2020. When they see a table like this, they are assuming that no additional additional basic metabolic panel data is available between Jan 14, 2020 and Feb 2, 2020. When might this be problematic?

1/2/201/3/201/14/20
CCC- Sodium130128131
CCC- Chloride10098102
CCC- Bicarbonate272528
CCC- Potassium44.34.5
CCC- BUN7108
CCC- Creatinine0.70.60.8
1/5/201/10/202/3/20
CCC-Other Labs151123
CCC-Other Labs2411
CCC-Other Labs33232
CCC-Other Labs4212312
CCC-Other Labs51123
CCC-Other Labs623311
CCC-Other Labs7122

Physician stops looking here because everything looks OK on first glance except for the glucose.

2/3/201/10/202/3/20
Mike's Sodium1101123
Mike's Chloride8011
Mike's Bicarbonate15232
Mike's Potassium3.212312
Mike's BUN20123
Mike's Creatinine5311
Mike's Glucose9022

Whoa! That’s some pretty abnormal labs! If you organized your laboratories like this, a physician could completely miss the basic metabolic panel from Feb 1, 2020 because it is not displayed with the other basic metabolic panels and its more than one full screens’ length away from that first set of BMPs. How could this happen?! Well, in this fairly common scenario, the patient had labs from two different sources: Commure Community Clinic and Mike’s Clinic. Because these two sources have their own code sets, the developer here just grouped all the labs by their codes without combining labs with two codes that are actually the same lab type. There are several lessons to take away from this example. First, it highlights the importance of this item on the FHIR safety checklist:

All resources in use are valid against the base specification and the profiles that apply to my system (see note about the correct run-time use of validation) Developers must have clear understanding of the relationship between the data they are displaying (in this case which code systems are being used in the data) and how it interacts with the queries and searches they use to retrieve and display the data.

Second, I will save the details on how to actually fix this problem later, but this example highlights the importance of having good terminology services where maps can be created linking labs like Mike’s Sodium to CCC- Sodium so that they can be grouped together.

Finally, this example also again highlights the importance of having good communication with your physician users. Often, it will be technically slow or impossible to deploy custom mappings to merge data with different codes. In these cases, it is very important to make sure to let your users know that they need to expand their visual search for labs from other sources.

That’s enough of labs for now. Lets take a more detailed look at something Grahme has already touched on in his synchronization example: medications. Again focusing on an inpatient setting, lets imagine that you’re building an app for a physician and he wants you to display all active medications for a patient.

“Easy!”, you say and pull all of the MedicationRequest resources for a patient:

  • Metoprolol Tartrate 25mg PO BID
  • Metoprolol Succinate 50mg PO qday
  • Metoprolol Tartrate 5mg IV q5min
  • Aspirin 81mg PO qday
  • Aspirin 81mg PO qday
  • Aspirin 81mg PO qday
  • Atorvastatin 40mg PO QHS
  • Atorvastatin 40mg PO QHS
  • Aspirin 81mg PO qday
  • Lovenox 40mg SubQ qday
  • Heparin IV gtt
  • Lasix 40mg IV BID

You excitedly show this to the physician, who frowns. “There’s too many medications on here.” In fact, you have forgotten to address this item on the checklist:

For each resource that my system handles, my system handles the full Life cycle (status codes, currency issues, and erroneous entry status)

In the case of MedicationRequests, you need to filter out for only medications with MedicationRequest.status = ‘Active’. Let’s inspect this field.

If you filter out for only the active medications, the list now looks like this:

MedicationStatus
Metoprolol Tartrate 25mg PO BIDActive
Metoprolol Succinate 50mg PO qdayActive
Aspirin 81mg PO qdayActive
Aspirin 81mg PO qdayActive
Atorvastatin 40mg PO QHSActive
Lovenox 40mg SubQ qdayActive
Lasix 40mg IV BIDActive

Oddly, there are still duplicates here. It turns out that you also need to separate inpatient and outpatient medications. For an inpatient physician, you really should only display the inpatient medications. Exploring the Medication. Category codes gives you the following information

MedicationStatusSetting
Metoprolol Tartrate 25mg PO BIDActiveInpatient
Metoprolol Succinate 50mg PO qdayActiveOutpatient
Aspirin 81mg PO qdayActiveOutpatient
Aspirin 81mg PO qdayActiveInpatient
Atorvastatin 40mg PO QHSActiveOutpatient
Lovenox 40mg SubQ qdayActiveInpatient
Lasix 40mg IV BIDActiveInpatient

Great! Now we can just show the inpatient medications:

MedicationStatusSetting
Metoprolol Tartrate 25mg PO BIDActiveInpatient
Aspirin 81mg PO qdayActiveInpatient
Lovenox 40mg SubQ qdayActiveInpatient
Lasix 40mg IV BIDActiveInpatient

Why was it important to only display the inpatient medications? Notice that it’s only on this last cut that the physician obviously notices that Atorvastatin has NOT been ordered in as an inpatient medication. This is a crucial medication for some patients! It would have been very easy to just stop with the “Active” medication filter and display this list to the physician:

  • Metoprolol Tartrate 25mg PO BID
  • Metoprolol Succinate 50mg PO qday
  • Aspirin 81mg PO qday
  • Aspirin 81mg PO qday
  • Atorvastatin 40mg PO QHS (missing on inpatient med list)
  • Lovenox 40mg SubQ qday
  • Lasix 40mg IV BID

However, the physician would have INCORRECTLY assumed that Atorvastatin was already ordered in the inpatient setting.

This example clearly highlights the importance of understanding the lifecycle of a FHIR resource and how it impacts what clinicians expect to see. It also shows how physicians very much a set of contextual filters they expect on their data that they often will not explicitly tell developers. It will be up to the developer to realize that the inpatient context isf important and actually suggests filters on many of the resources that should be displayed. One helpful strategy in this example is to display as much information as possible if you are not sure what is relevant.

As long as you make sure to followup with your physician users about your uncertainty, the two of you together will be able to sort outr the appropriate display filters.

Work fast without breaking things

The purpose of this post is not to scare you away from developing software for physicians. Rather, it is simply meant to highlight of going through the FHIR safety checklist and asking questions when you don’t fully understand how they apply to your software. Commure’s tools will help you iterate quickly so that you can quickly resolve these complex issues with your physician partners. With that all being said, happy coding and don’t get BHurnt.

...
Read more
/ 1 min read

Profiles in FHIR

A follow up from a recent internal discussion about merits of Rust static typing for FHIR data.

In this post I'm going to take a look at the profiling in FHIR(r). I'll open with a statement which sets up the major constraint for FHIR applications:

FHIR is not a standard per se, but rather a "platform specification"!

What that means is that FHIR specifies only features which are common across jurisdictions and healthcare ecosystem in general. FHIR is by design incomplete.

The true interoperability of FHIR is defined via feature called "profiling". It's the way FHIR specification could be extended for specific scenarios, use cases, jurisdictions, and so on.

So far, many FHIR API implementations and users are successfully ignoring profiles (most of the time), however, my expectation is that everyone will have to deal with profiles 100% of the time, even though they could only define 20% remaining percent of the behavior!

Given that, in my opinion, it is impossible to talk about statically typing the data unless there is at least a basic understanding of profiles.

Profiles

What is a profile? Basically, it's another "StructureDefinition" which "constrains" the base definition it attaches to. Sounds deceptively simple, but they in fact are way more powerful than they look!

The easiest thing a profile can do is straightforward restriction on a base element. For example, "Patient.name" element is optional ("min" cardinality is 0). Profile can make field required by setting "min" to 1. For example, let's look at US Core (it's also called an "Argonaut Project") profile for "Immunization". For the "Immunization.date" field the "min" cardinality is set to 1 -- making it a required field.

An application developed with such profile in mind might expect that "date" field on "Immunization" matching US Core profile is a non-optional date value2.

Bindings

Another common use-case for profiles is to constrain bindings on elements. There are many possible scenarios here, but the general idea is that profile can restrict which codes could be used, but not allow for new codes (codes which are invalid in the base structure or profile cannot be made valid).

Again, this is somewhat deceiving. If base binding is "example", profile can, for example, replace it with "required" binding. Technically, this restricts possible codes, but from the application point of view it can be viewed as "replacing" binding with a different one.

An example of such profile is US Core Patient, which changes binding for the "Patient.communication.language" element to "http://hl7.org/fhir/us/core/ValueSet/simple-language".

An application developed with such profile in mind might expect the binding defined in profile, not the binding defined in the core specification. Although, the binding stays extensible -- so any value from the base definition could still be used!

Extensions

Before looking at more complex cases, let me introduce another piece of the puzzle, an "Extension".

"Extension" is a complex type which is crucial to the whole FHIR extensibility story. By itself it is just a pair of URL and some value (which could be of any type). However, the idea is that you can put any kind of data in it -- annotated with URL which would define its semantics.

Every data type extends complex type "Element", which has an "extension" field with value of list of "Extension"s. Therefore, it allows to add custom data to every possible place of FHIR resource -- even inside primitives (although representation of these in JSON is a bit crazy)!

Profiling Extensions

So, another possible use of profiles is defining profile for "Extension" element. This allows to restrict an "Extension" in a way that it becomes your custom data type (remember, FHIR does not allow you to define your own types and resources outside of this extension/profiling mechanism!).

For example, let's take a look at the "birthTime" extension (the easiest is to look at its definition inside "differential" field -- this would be the difference with base definition).

It specifies that "url" field can only have one possible value, "http://hl7.org/fhir/StructureDefinition/patient-birthTime". Also, it specifies that this extension cannot have its own extensions ("max" cardinality of "Extension.extension" is "0"). Finally, it specifies that value type can only be "dateTime"1.

Now we have a way to allow custom data (typed) on an existing field (the "birthTime" extension specifies context "Patient.birthDate", which means it can only be used on "birthDate" field of "Patient").

The only guarantee provided is that if application would see an extension with this URL, it should comply to this profile. Nothing is known upfront about if this extension exists at all (Patient definition doesn't say anything about it)!

Slicing

What if we want to attach our extension to the Patient resource? Let's say, we want to define an "ethnicity" field on our resource for our US Core Patient profile on Patient.

Please, meet the slicing! It's a technique to specify some constraint on a list of elements. There are two common use cases.

One use case is to specify additional requirements on a subsets of values of the repeatable element. For example, in CareConnect Patient, a "Patient.name" element is sliced by the "use" field. Then, for an element with "use" set to "official", the cardinality is set to "1" (both "min" and "max") -- which enforces that Patient has exactly one official name!

This could be seen as creating essentially a new field on a Patient, "officialName" (or, rather, a selector which will return one of the elements from the "Patient.name" array). If application is developed with this profile in mind, it might expect this field to be defined on the Patient resource!

Another common use-case for slicing (I think, it's actually the most common one) is to slice off an extensions list. The typical definition would be something like "slice me "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" extension off the extensions on Patient resource and set its cardinality to 0..1". This would essentially add a new field to the Patient, with data type defined by the extension profile!

Again, an application assuming Argonaut profile (US Core Profiles), will expect Patient to have a field called "ethnicity" on the "Patient" resource itself!

Slices: Complex Types

As a side note, profiling extensions can also create "custom" complex types! The convention is that the extension itself will be a "complex type", with no value of itself ("max" cardinality restricted to "0").

Then, the "extension" field on the extension itself could be profiled in a way that each "slice" is given a fixed URI and restriction on value data type. The fixed URI of these "fields" is usually a simple identifier, like "individual" for the "acceptance" extension.

This essentially defines a new data type, with fields of given types!

Two Uses of Profiles

There are two common uses of profiles in the context of the system: resource profiles and system profiles. Resource profiles are ones which are guaranteed for every resource in the system. For example, US Core Patient profile used in such manner would guarantee that every Patient resource returned by an endpoint is a valid US Core Patient resource.

The system profiles, though, only guarantees that these profiles are used in the system, but they are used on a per-instance level.

An example of such profile would be set of profiles on top of "Observation" resource, for example, "Blood Pressure" profile3. Not every observation is a blood pressure observation, but those which are, should comply to the blood pressure definition (for example, should have LOINC code of "85354-9").

This essentially defines a new type which application dealing with blood pressure might expect.

It is crucial not just for interoperability but also for safety. Note that the difference between guarantees given by a base "Observation" resource and "Blood Profile" is huge: base definition is essentially a collection of "anything we observed for whoever", whereas blood pressure observation is very specific.

What Next?

The key take away should be that profiles are important, they could do a lot and they define the real guarantees. Also, they are highly dynamic across multiple dimensions (could be defined "at runtime", could apply on a per-instance basis, could modify resource significantly, and so on).

I was planning to present an idea how we can map these profiles to Rust traits, but this post is getting a bit long, so see y'all next time!

References


  1. The tricky part is that an extension is considered a valid value for primitive, which means application might still receive an empty date field! Oh, well...
  2. There is some tricky part here -- this restriction is set on "expanded" variant of the field, "valueDateTime" rather than its base definition "value[x]"
  3. It's in fact defined on top of "Vital Signs" profile, which is defined on top of "Observation" resource.
...
Read more
/ 1 min read