Having age and gender specific ranges in OpenMRS

To expand on what @prabhuawasthi is saying…

On the endTB Bahmni project we need to define different normal ranges for concepts depending on age and gender. (There shouldn’t be anything endTB-specific about the feature request though.)

The OpenMRS API doesn’t support this today, so we’d like to add it. How do people suggest we implement this, such that it can be most cleanly merged into openmrs-core when it’s done?

(Or, has someone already implemented this in a module somewhere?)

@akanter, @burke, @jteich, your advice for how this is typically modeled is most welcome!

Mmm. This is an interesting topic. I have always been of mixed mind when it comes to the normal ranges on the concepts. I typically do not populate them unless they are obvious. I think we have to recognize that concepts might have different states based on their purpose or use case. The same concept could have a different normal range based on age or gender, but also perhaps as to HOW it is being used. We know that a given lab test might have different ranges based on the machine, even if it has the same LOINC code. I really don’t want to go down the road of having to create gender-specific concepts or perhaps versions of concepts for each particular use case. It is bad enough that I think we need to do that for aggregations (value sets/groupers). I don’t know if anyone has done this right yet. I wonder whether meta data like normal ranges should really be split out entirely from the concept and be mapped based on other meta data (age, gender, purpose). That would be entirely extensible.

Reference ranges can vary by context. Some common examples of things that can affect reference ranges:

  • Age
  • Gender
  • Ethnicity
  • Physiologic Timing (time of day, physiologic cycles like menstruation, etc.)
  • Therapeutic Timing (initiation, maintenance, post-treatment)

Those are just a few; I’m sure there are others. For this reason, the common approach is to associate ranges with the individual observations. You can see this in FHIR’s Observation.referenceRange and this random discussion in the LOINC forum.

Rather than trying to model all the possible reference ranges for each concept, I would suggest the following approach:

  • Continue to include reference ranges that are globally applicable at the concept level as @akanter has been doing.

  • Add obs_reference_range (low, high, meaning, age, & text linked 1-to-many to obs) to align with FHIR.

Our default HL7 processor could be adapted to recognize and store reference ranges when reported with results. Other tools (like Chart Search & reporting) could then grow to include reference ranges (if present) when showing results.

@burke, so if I’m understanding you correctly you’re suggesting that the changes to OpenMRS Core are:

  • create an obs_reference_range table
  • FK to obs, and has columns like from FHIR: low, high, meaning, age, text
  • can have many of these for one obs.
  • no changes to obs table
  • add Obs.getReferenceRanges (returns List)

Then within Bahmni we’d be responsible for:

  • defining our own tables to store guidelines for range definitions by age, sex, etc
  • either put this in a bahmni_concept_reference_ranges or maybe implement concept_attributes for this
  • within the form entry technology +/- application UI we’d need to
  • (a) recognize when a user is entering an obs that matches a known range definition, and give them visual feedback
  • (b) include this in the submission to the API call

Further, every numeric obs we save for which ranges are defined would lead to 3 rows in obs_reference_range (if we defined abnormal, critical, and absolute).

I’m not completely sold that this is the right approach…

(One minor variation would be have a reference_range_definition table and then an obs_reference_range_map table, to make this a many to many relationship. But I don’t know if that’s better.)

While FHIR does support many-to-one ranges per observation, most labs will report a single reference range (the “normal” range) ± additional text with the result. Labs don’t typically report critical or absolute ranges with the results.

This leads to something I left out: the interpretation (aka abnormal flag):

  • add interpretation column to the obs table. This would either store the codes in HL7 Table 0078 or be a FK to concept (FK may be overkill).

Since the determination of the abnormal flag and appropriate reference range is the responsibility of the party generating the result, any application generating lab results (including Bahmni) would be responsible for deciding if the lab being reported should be flagged as abnormal or include a reference range. We would continue to have canonical reference ranges (normal, critical, and absolute) in the concept dictionary.

Every numeric obs could include an abnormal flag and a normal range ± any range-specific information. In most cases, this is a single normal range (low & high) and, for abnormal results, an abnormal flag. Some labs include canned text like:

Normal <0.05 ng/mL Intermediate Risk 0.05-0.09 ng/mL High Risk >=0.10 ng/mL 95% Sensitivity and Specificity for MI: >0.6 ng/mL


Cholesterol Interpretation: Desirable: Adult <200 mg/dL Desirable: Child <170 mg/dL Borderline/Mod Cardiac Risk: 200-239 mg/dL High Cardiac Risk, Adult >240 mg/dL High Cardiac Risk, Child: >200 mg/dL

The reasons for this approach:

  • This is how everyone else is handling ranges (i.e., it’s up to the system reporting the result to report abnormal flag and reference range)
  • When you inevitably start needing to accept results from other systems, you can still properly report abnormal flags and reference ranges.
  • Lab machines often report results with abnormal flag and reference range.
  • Reference ranges can vary by machine used and vary over time, so trying to manage them in the dictionary can get even uglier than managing at the point of result generation.

While we could approach these as extensions of concepts, I think it helps to take this approach:

  • A concept reference range is a canonical reference range – i.e., absolute ranges ± normal or critical ranges independent of context or method.

  • Context-specific reference ranges belong not to the concept, but to the filler – i.e., the system or tool generating the result.

So, while storing context-specific reference ranges metadata alongside our concepts may help provide a central source for various data input methods to use, the important thing is that the data entry tools can get to these metadata (not that they’re part of the dictionary).

This extra context is very helpful. So taking a step back, there are two things going on:

(1) OpenMRS Core should support flagging obs as abnormal

We agree 100%.

Bahmni already has a workaround for this: we store lab results in an obs group with a boolean obs for normal/abnormal, and numeric obs for min and max range. (In fact these are our 3 most-used concepts.)

We would welcome native support for this in openmrs-core, via some mechanism like interpretation and range. This seems like a good GSoC project, or maybe a good independent project for a team doing hack nights.

But this is actually what we meant for the point of this thread to be.

(2) Help us define a common approach for storing multiple reference ranges in an OpenMRS system

Our endTB implementation wants to define project-wide reference ranges that differ by age and gender. We think that a (maybe simplistic) 80% solution to this problem would be valuable for other implementations too.

Can we define a common approach for storing reference ranges by age, gender, and a couple more common criteria (but without solving the fully general case)? Maybe we’d put the data model for this in a “Simple Reference Ranges” module.


  1. Agree to what Burke said. Should we introduce additional column “obs.interpretation”. I am guessing the column datatype would be VARCHAR. Should OpenMRS do anything in terms of only allowed values from HL7 0078 valueset table or should it just leave it to the implementers? Q: When (OpenMRS version) can we introduce this?

  2. Starting OpenMRS 2.0, we have concept attributes. How about introducing a “reference range” attribute type, which just defines the contextual ranges (gender, age). Alternate would be to have a different model to do this (separate tables, but linked with a concept)

(1) I think it makes sense to add an optional varchar obs.interpretation column. I don’t think we should limit it to any particular valueset in the underlying API, though we should recommend that people use the FHIR valueset that @angshuonline links to (and in Bahmni we’d standardize on this).

We’d do this in the next minor OpenMRS Platform release, i.e. Platform 2.1. (If it’s especially important to Bahmni to get this released sooner than currently scheduled, this could be a chance to develop a process to empower a motivated community member do a non-LTS release, and pushing the next scheduled-and-supported release to be Platform 2.2.)

(2) It would be fine for our Bahmni use case if we use concept attributes for this in our Bahmni use case (rather than introducing a new module/table). However this would conflict with our intended future push for people to use CIEL concepts Do we still care about (2), since this is no longer prioritized for endTB? Perhaps instead we should think about adding obs.referenceRange, which is what FHIR does, for some future use case?

I created https://issues.openmrs.org/browse/TRUNK-4976 to track (1).

Can we pick up “obs.interpretation” soon enough? Is it possible to introduce this in any upcoming OpenMRS release very soon?

@angshuonline I replied to you on Suggestions to model marking an observation as "Abnormal"

Executive summary: Bahmni will prioritize adding this; someone like AMPATH needs to ensure that MySQL’s online DDL is sufficient to add these columns while the system is online.

Bumping this up for attention. The ranges topic and requirements are coming up from many prospects. Can we get to some sort of agreement for possible modelling for ref ranges for age & gender?

One thought I had was, how about just a simple reference model (can be a csv or table - external to core openmrs, but invoke-able from openmrs core) ?

@angshuonline, my takeaway from this thread is that OpenMRS for doesn’t want to model these reference ranges.

So we should propose some approach for this in a module, and hopefully the next interested person will follow our lead.

A csv seems sensible and an easy way for implementers to maintain/enter the data. And I would lean towards a simple new DB table, rather than concept attributes.

Are you expecting openmrs-core to consume this data and use it for something?

I assume we’d want to use it from some Bahmni display controls, but that OpenMRS core doesn’t need to know.

there are different requirements that I am seeing

  1. to be able to display at patient level in UI - this is easy by fetching some reference range from CSV and using attributes for a concept, we can even figure out which CSV to fetch. Although, I would preferred that fetching a concept gives me details or a link that provides me HATEOAS way of leading me to the reference ranges. That was the question/query of whether we want to extend the openmrs to support the extensibility.
  2. To be able to raise flags against a patient while saving/capturing data - this is also, where I can having the concept model extendable will help for validating results and raising appropriate flags/alerts.

Of course, in Bahmni we can always use concept attribute and/or configuration to extend the above features.

How would concept_attribute work in this case? Would you have to have an attribute called “NORMAL_RANGE_MALE_LOW” and the value of the attribute be the lower bound? And then another for each low, high and stratification?

Thats not really the option I had in mind, as this will mean

  1. We need to come up with convention of naming the attribute, and Bahmni will have to interpret the naming convention.
  2. At a given point of time support only known attributes.

None of the above are extensible. Ranges or their references are more complex. Again, not disregarding the above approach for simpler things.

What I meant, was using concept attribute to specify where the references ranges can be found. imagine a “HEIGHT” concept having an attribute “reference ranges chart”, like here

Whether to use the references range chart, is decided by an extension (example: a display control, or other script) - which takes into consideration of the patient’s age & gender and shows (or even updates) information.

So I guess what you’re suggesting is that the OpenMRS platform should know how to interpret certain kinds of reference range documents, with certain known types of patient constraints (gender, age in years)?

The growth chart example would be one, and I can imagine another that’s equivalent except that the column headings are hiAbsolute, hiCritical, hiNormal, lowAbsolute, lowCritical, lowNormal.

@akanter, @burke, @jteich, @terry is there any standard representation of this, or precedent for it in other EHR systems?

(Burke I can see that we wouldn’t want to build specific attributes into openmrs-core, but we could build it into some other platform module so that when you fetch a concept via REST you have some nice semantic access to this reference range data if it’s provided in a concept attribute. Or alternately we could have a built-in CustomDatatype for this in openmrs-core which webservices.rest would know how to represent.)

The most common approach I’ve seen is to leave these context-specific ranges within the purview of the lab performing the test – i.e., focus on reporting abnormal & ranges with results and leave the data entry validation to the lab.

In OpenMRS, this would mean adding interpretation (abnormal flag) and a mechanism for reporting reference range to obs. If/when input validation for context-specific ranges was needed, it would either be performed in a lab module or in an integrated lab information system.

I’m not aware of a standard way that EHRs do this. In some cases LOINC sets up a different code for a different reference range (e.g., creatinine results in African-American and non-African-American patients), but I suspect that’s way too complicated and creates as many problems as it solves.

I’d go with something similar to the most recent few suggestions above:

  • Provide a field for text description of the more complex range instructions (“abnormal = this for males, = that for females”, etc.) - useful whether or not we also do the following structured part
  • For structured flagging of abnormals, modify the reference range parameter so that it can be fixed (same low, hi, very low, very hi for all groups) or complex/parametric
  • Define the parametric reference ranges in a separate, fairly simple structure, where each subgroup would be defined by parameter type (Age Range, gender, ethnicity, etc.), parameter value 1 and value 2 (e.g., low and high age values; or for a gender parameter just one value is used to specify Male or Female), and the various reference range boundary values that are already in use for the fixed case — I suppose that ‘value 1’ and ‘value 2’ also have to have typing, as they could be numbers or enums
1 Like

We would be very interested in this too for Lab Order Fulfillment Forms, I like @jteich suggestion of modifying range instructions, maybe we can also define the age ranges in the reporting_age_group table.

UREA [Normal low, Normal high]

<15days  [1.1 - 4.3]
15d<1y  [1.5 - 6.1]
>=1y  [2.5 - 6.4]

reporting_age_group table:

| id | name | report_group_name | min_years | min_days | max_years | max_days | sort_order |
|  1 | ChemLabR1  | Chemistry Lab |       0 |        0 |       0 |        14 |          0 |
|  1 | ChemLabR2  | Chemistry Lab |      0 |        15 |       1 |        -1 |          0 |
|  1 | ChemLabR3  | Chemistry Lab |       1 |        0 |       200 |        0 |          0 |

Example of a possible range instruction format for the above UREA example using variable length arrays of format TYPE [ label1, value1, label2, value2, …]:

"Normal low" =  AgeRange ["ChemLabR1",1.1,"ChemLabR2",1.5, "ChemLabR2",2.5] 


"Normal low" = Gender [ "M", 1.1, "F", 1.0]

and some recursive parsing to deal with combined case:

 "Normal low" =  AgeRange ["ChemLabR1", Gender [ "M", 1.1, "F", 1.0], "ChemLabR2", Gender [ "M", 1.5, "F", 1.4]......] 

while maintaing backward compatibility for simple ranges of course. I’m guessing just the age ranges and gender will cover most use cases?

If that’s the case, we could also just have 2 values for each label, one for Female and one for Male, to simplify things

     "Normal low" =  AgeRange ["ChemLabR1", 1.0, 1.1, "ChemLabR2", 1.4, 1.5......]