Age-Sensitive Ranges

We have a big technical design need.

For OpenMRS to be useful in general OPDs, we must have age-based-ranges for things like Vital Signs and Labs.

Why: Because a patient of any age can come through the door - from 1 hour old to 100+ years old. And many Normal vs Abnormal ranges, especially Vital Signs, are different depending on Age (but not just age - also Sex and other variables, but let’s focus on Age for now).

Here are some examples of just how different these ranges can be. Note how extremely different the normal range is for a newborn vs an adult: Something normal for an adult is in fatal-range for a newborn, and vice-versa!


Lab Ranges Example

The same is true for many lab tests:


Sex-Based and other Normal Range Differences

While ages are the most urgent use case for O3 for OPD right now, it’s not just ages that matter. Other variables about a person can change what’s a normal range for them, like kidney function, etc. But the most common example after ages is Sex. See these examples where what’s normal for a man is abnormal for a woman, and vice-versa, as this Thyroid Treatment example shows:


Currently our normal/abnormal result ranges are metadata directly on concepts, meaning, you can only have 1 range per variable.

@dkayiwa @burke @ibacher Can you start thinking about this? We need a plan, and as a community we need to implement this within the next 2-3 O3 releases or the next platform release. Because this is needed NOW, in the field.

1 Like

p.s. - @ball how have you been handling this at PIH, given many of your sites see patients of varying ages?

@caseynth2 how does OpenELIS handle this?

@pmanko or @mherman22 how have you seen FHIR handle this?

FHIR is relatively easy because the reference range is reported as part of the Observation itself, which allows it to be flexible to whatever rules the producing system knows about. The issue with OpenMRS is that we tried to encode reference ranges as a non-computable property of concepts. Aligning on the FHIR approach is probably the right approach in terms of making this data available (and I’ve already suggested that a few times here).

Of course, we then have the issue that reference ranges need to be provided at the time an observation is created. OpenElis appears to do this by having a table of per-test reference limits keyed to an age range and gender. Off-hand, I can’t think of a better system for doing this.

This implies adding a new metadata domain and maybe a custom SaveHandler for observations. Ultimately, our version of this system would need to fall back to concept-defined ranges so that we don’t lose any existing functionality and because not every observation will have age and gender specific variations. All of this is doable in a module.

Something a bit closer to a formal specification:

I suppose, theoretically, the above is useful without modifications to Obs to support a reference range, but we probably also want to support the case where an ancillary system (like the lab system) can submit the observation with the reference range and we are able to process it.


Would it be a bad idea, in addition to the above proposal, to also extend the non computable properties of concepts for numeric ranges by creating a new table concept_numeric_validation which has the same fields as the already existing concept_numeric table but with three extra fields: gender, min_age, and max_age? This table would have as many rows as wanted for each concept numeric. Then a concept screen like this OpenMRS - Login would display these rows as it does for concept mappings.


Yeah, I think that would also be a reasonable way to accomplish the above…

Hi all,

Thanks @grace for bringing this up! Indeed, we do store the reference range in the database for all age and sex group combinations, and require them to be specified between 0 and infinite ages, this is something that we have an okay interface for now, but I’d love any UX expert view into how to make this UI element less overwhelming.

To make life even more complicated, we also have the concept of critical ranges (where the result is considered an emergency that needs to follow different procedures, you can think of this as a super high CD4 count, or as a positive ebola test). Within the context of the EMR, this may cause an alert flag or a notification.

I’ve dropped in an example as we have it now, and you can see what a nightmare this can wind up being.

On the FHIR side, we don’t currently send back reference ranges, but we should update our FHIR objects to include this. Can we agree how to model that for numeric, select list and multi select results? It looks like we can use high and low in the observation resource for the OpenELIS set high and low values for the age range of the patient sent from the EMR, and use the normal for the normal range of any single or multiselect lists (the normal range for these is usually “negative”).

@mozzy Would know best how to add these to the FHIR2 module, and the FHIR IG. Would this be possible with GSOC students? I think maybe on the OpenELIS side, we can get that done this way.


I know @burke has raised this before. It is one thing to capture the normal range as attached to a given instance of data (like the FHIR example), and another to capture a list of possible ranges with different demographic or other characteristics (one thyroid medication, etc.). Right now we clearly cannot store all of these as they are in the concept dictionary. Would like to know how reference information like this could be attached to a concept. @dkayiwa’s suggestion of a validation subtable is interesting, but is validation the only reason to have additional attributes on a concept?

I think validation is probably not the right thing for what this is doing. If we have any validation, it’s just around ensuring that the obs value is within the bounds of the absolute limits. The normal and critical ranges are used to affect how we display results.

I was suggesting a sort of two-pronged approach where we expand the obs model to support reference ranges directly and expand the concept model to give us default reference ranges if none come with the observation.

Ranges for vitals and lab results can vary on a variety of factors:

  • Age
  • Gender
  • Ethnicity
  • Renal function
  • Pregnancy status
  • Weight or BMI
  • Time of Day

The simplest data model change (may not necessarily equate to the easiest to implement) would be to introduce a discriminator column to the concept_numeric table that would determine under which context(s) the range applies. The tricks would be:

  • Having a backwards-compatible-friendly default (presumably null).
  • How to handle the combinatorial (e.g., “1-3 year-old male”) without creating dozens or hundreds of discriminator values or adding column(s) for each context.
  • Deal with non-standardized buckets for things like age (e.g., a heart rate range may apply to 1-3 year-olds while a platelet count range applies to 1-5 year-olds).
  • Define explicit rules for how to handle conflicts when more than one reference range matches the current context (e.g., the more specific context is used).

Perhaps the only practical approach would be to focus on handling age & gender in our concept dictionary and rely on systems (or modules) responsible for labs to handle the more complicated scenarios. In this case, a simplified approach would be to add min_age, max_age, and gender to the concept_numeric table (and deciding on whether min & max ages should be inclusive or exclusive).

1 Like

The concept_numeric table takes a maximum of one record for each concept. Therefore, the least disruptive approach would be to leave that table as it is, for the cases that do not depend on (age, gender, etc), and then introduce a new table which can have more than one row for the same concept.

1 Like

@akanter when it comes to the CIEL dictionary distribution, do you foresee any downsides to having the new validation subtable?

Hi all,

First off, I’m new to the team so for those I haven’t met yet, I’m excited to work with all of you!

I’ll list out my approach from the platform meeting this morning to hopefully make my thoughts more clear.

I agree with Daniel that the least disruptive approach is to leave the concept_numeric table alone. We could treat it as a default fallback option for the case where a more specific numeric isn’t found for a given set of factors. While we could add additional columns to the concept_numeric table to support age and gender, it’s an incredibly rigid solution and the moment we need to add another factor (like ethnicity, weight, or any of the other factors Burke mentioned) we’ll have the same problem all over again. I get that it’s potentially the less costly option but I think it hilights the fact that we need a more generic solution here.

As Ian mentioned, pairing the ranges with observation data would put us on par with the FHIR approach but it seems that data is better configured outside of an individual observation context since its meant more as the expected range of a population at large.

Given all that, I’ve thought through a potential solution with the following table structure:

And a very rough UI for what editing these numeric groups could look like:

Essentially, instead of a concept being mapped to a single numeric, it’s mapped to a group of them, each defined by a group of factors. You can see I’ve also split out the range types (normal, critical and absolute) to their own table or enum. I feel this is more flexible than having them defined as columns since it lets us add new ones as needed without altering tables.

The big plus of this approach, aside from it more easily being able to be implemented as a module, is that it can grow dynamically as our needs change. If we want to support a new factor (such as pregnancy status) we simply need to add a row to the numeric_factor table.

Still to be sorted out is how we’d handle fallbacks in the case of where a patient only matches some of the factors, but that’s a problem to be solved no matter what approach we take.

Also of note, it seems that a concept can have multiple numeric values. For example, blood pressure has both an upper (systolic) and lower (diastolic) number. If we ever want to be able to perform automatic validation on a patient’s data to visually signify that either of these values are outside of the normal range, we’d likely want to store these two values seperately, right? If so, we could achieve this by creating a sub_concept type table to represent each individual numeric of a concept. But maybe that’s a problem for another time.


Trying to outline some user stories / use cases more clearly in this spreadsheet. @akanter your input very very invited :smiley:

1 Like

Not sure how OCL will manage this, though. It is one thing to add an attribute to a concept and a whole other thing to support relational tables… I am presuming that @burke is best positioned to comment. Although a relational table which identifies a set of reference ranges with tags, which are characteristics when those ranges are relevant… for example, this range is for all women (regardless of age), this range is for women of childbearing-age), this is the range for when a patient is on blood thinners, etc. I really wonder whether this should be captured in a knowledge graph or leave it up to CDSS.

Thanks for the model proposal, @ewaterman. I agree with you & @dkayiwa that leaving the existing concept_numeric table unchanged will be the simplest approach (I believe the plan is to start by introducing this in a module).

@caseynth2, could you share the data model behind the OpenELIS support?

We should also consider existing standards that have modeled this information so we don’t re-invent the wheel or create an OpenMRS wheel that is orthogonal to a model that is already widely adopted. For example, looking at FHIR’s Observation.referenceRange:

If you haven’t looked at it, the Wikipedia page for reference ranges for blood tests is one of the more impressive pages I’ve seen. :slight_smile:

I’ve been googling to look for existing standards to define reference ranges. Most of what I’ve found are standards for storing reference ranges provided from a lab system (like FHIR’s approach or OMOP’s CDM Measurement) and a general agreement to use UCUM for units. It would be informative to look at any WHO or other international efforts that are trying to distribute lab test definitions. @caseynth2 is there a popular resource used to populate lab tests & their reference ranges (e.g., distributed by the WHO or some other international body)?

In general, it appears the approach is to identify a “target population” for whom the range applies and age & gender tend to get special treatment.


These are stored as separate concepts: Login

How about creating a new table factor_group with fields {factor_group_id, name, operation} where operation can be AND or OR?

One question that i have for you is, how will the generic validating code know how to associate the various factors with the appropriate fields? For example, how will the code know that factor_id = 1 is for sex, factor_id = 2 is for min_age, factor_id = 3 is for ethnicity, factor_id = 4 is for pregnancy status, etc?

Otherwise, thank you so much @ewaterman for the proposal. It has lots of creativity! :slight_smile:

First off, I agree that we should try to follow existing standards. Looking at what Burke posted about the FHIR definition, the “type” and “appliesTo” fields seem quite interesting:

These look to be foreign keys and so would seem to imply a similar structure to what I’ve proposed with data driven ranges and multiple factors mapping to a group. The main difference here (as Ian mentioned) is that FHIR defines this all as a part of the observation instead of as a part of the concept.

I still feel like this makes sense to be associated with the concept but all of you know the system better than I do so I trust your opinions.

Good point. If we’re using the factors for observational data validation we’d definitely need a way to map the factors to their equivalent values in the patient’s identifier data. I’m not as familiar with how patient identifiers are stored. I’m guessing they’re defined in the patient_identifier_type table? If so I’d hope it wouldn’t be too hard to simply add a column to the new numeric_factor table that will map the factor to the equivalent indentifier.

Thinking more about it, for factors that have an enumeratable value (sex, ethnicity), we could add a factor_value table to enumerate all the allowed values for the factor, one row per possible value.

This would also allow individual implementations to define their own set of allowed values if need be (maybe a country has standardised a list of recognized enthnicities). It’d also ensure we’re using the same values everywhere. Not essential to the solution I’d say, but a definite nice to have.

Yes adding in an operation column could give some more flexibility for factor groups.

As to choosing which factor group to apply for a patient, I think the simplest solution would be to require all factors to match and we choose the group that has the most factors. Here’s an example situation:

Say we have the following factor groups defined:

  1. Female + age 30-50
  2. Male + age 10-30
  3. Male
  4. The default in the current numeric table
  • If we have a Male patient aged 40: They don’t match all the factors in group 1 or 2 so we use the numeric for group 3.
  • If we have a Female patient aged 20: They don’t match all factors of any group so we use the default numeric.
  • If we have a Male patient aged 20: They match both group 2 and 3 but group 3 has more factors so we use that one.

We’d need some rule for breaking ties if a patient matches multiple groups that all have the same number of factors. I’d be inclined to add another column to the factor table for factor priority. Then we’d pick the group with a factor of the highest priority. Alternatively we could simply return all the groups that the patient matches with and let the client decide which they want to display.

Sex and age are fixed fields (not observation data) for a patient.

Age is doubly weird since we actually store birthdate, but the relevant factor is age at the time the observation is generated. Ethnicity, too, is probably more similar to an attribute of a patient than observational data, though I don’t know that we have a standardised way of capturing it.

No we do not have it.