When a Drug Isn’t Just a Drug: Inside RxNorm

A medication may have a brand name, a generic name, a source-specific name, and even different generic names depending on where it is used. For usability in public health research and direct patient care, treating all of these as ordinary strings creates a surprisingly difficult problem: how do you know that two different names represent the same medication, and, more importantly, what is actually inside that medication?

Drug terminologies exist to solve different parts of this problem.The Anatomical Therapeutic Chemical (ATC) Classification System, for example, organizes medicines according to their anatomical, therapeutic, pharmacological, and chemical properties. Maintained by the World Health Organization, ATC is widely used in public-health research, particularly drug-utilization research and comparisons of medication consumption.

However, classification is not the same as clinical medication normalization. 

When an EHR needs to represent a medication for prescribing, reconciliation, interoperability, or an ingredient-level allergy check and adverse drug reaction checks, it needs to understand considerably more than the therapeutic class of a drug. It needs to deal with the messy reality of medication names, strengths, dose forms, brands, ingredients, and combinations.

This is where RxNorm becomes particularly interesting.

What is RxNorm, and why does it matter to EHRs?

RxNorm is a standardized nomenclature and vocabulary for clinical drugs, created and maintained by the U.S. National Library of Medicine (NLM). Rather than treating a medication as nothing more than a name, RxNorm represents medications as concepts and connects those concepts through relationships and source-specific representations.

To understand how it works, two terms are particularly important: RxNorm concepts and RxNorm atoms.

RxNorm Drug Concept

An RxNorm Concept Unique Identifier (RxCUI) identifies a normalized drug concept at a particular level of abstraction. Multiple synonymous drug strings from different source vocabularies can map to the same RxNorm concept when they represent the same clinical entity at that level.

For example, RxCUI 200977 represents a particular 500 mg oral tablet concept for acetaminophen. Different source vocabularies may provide different names for that same concept, including branded and source-specific representations.

The important distinction is this:

The RxCUI identifies the normalized concept; it does not simply identify one particular spelling of its name.

RxNorm Drug Atom

An RxNorm Atom is an individual representation of a concept from a particular source vocabulary. Each atom has its own RxAUI (RxNorm Atom Unique Identifier) and is associated with information such as its source vocabulary and Term Type (TTY).

For example, RxAUI 12540121 can represent one specific source term associated with RxCUI 200977, while another RxAUI can represent a different source-specific representation of the same underlying concept.

This distinction is subtle, but it becomes extremely important when building an EHR.

A concept is not simply a name. An atom is not simply another concept.

The concept provides the normalized identity; the atoms provide the individual vocabulary representations attached to that identity.

And once those concepts are connected through RxNorm's relationships, things get considerably more interesting.

RxNorm inside an EHR

To understand how RxNorm can actually be used inside an Electronic Health Record, we need to look at one of its most important pieces of structure: Term Types (TTYs).

If the RxCUI identifies the concept, the TTY tells us what kind of concept we are looking at and, therefore, the level of clinical granularity it represents.

For medication data, some of the important Term Types include:

  • IN — Ingredient: the active ingredient, such as acetaminophen.

  • PIN — Precise Ingredient: a more specific form of an ingredient, such as a particular salt or physical form.

  • DF — Dose Form: the dosage form, such as an oral tablet.

  • SCD — Semantic Clinical Drug: a clinical drug defined by its ingredients, strength, and dose form.

  • SBD — Semantic Branded Drug: a branded clinical drug combining a brand name with its clinical drug characteristics.

  • SCDF — Semantic Clinical Dose Form: a clinical drug form without a specific strength.

  • SBDF — Semantic Branded Dose Form: the branded equivalent at the dose-form level.

This is where RxNorm stops looking like a dictionary and starts looking like a model of medication.

A prescription can be connected to a clinical drug. That clinical drug can be connected to its ingredients. And those relationships allow an EHR to reason about the medication beyond the string displayed on the prescription.

Consider a patient who has an allergy to amoxicillin.

Now imagine that the clinician prescribes Augmentin.

To a simple EHR, these are two strings:

"amoxicillin" "Augmentin"

To a terminology-aware EHR, the prescription can be decomposed into its underlying ingredients:

Augmentin

├── amoxicillin
└── clavulanate

That difference matters.

The EHR does not need the patient's allergy record to contain the brand name Augmentin. It can compare the ingredients of the prescribed medication against the patient's known allergic ingredients.

And this is where we can stop talking about RxNorm in the abstract and actually see it working.

Workshop Implementation

Let's open the database. First, we download the RxNorm full monthly release from the NLM registry and load the raw .RRF files into our SQL database. For this walkthrough, we will focus primarily on three tables:

  • RXNCONSO.RRF: Contains the drug concepts (RxCUI), their individual atoms (RxAUI), source vocabularies, and Term Types (TTY).

  • RXNREL.RRF: Defines relationships between concepts, enabling us to traverse from a brand or clinical drug directly down to its active ingredients.

  • RXNSAT.RRF: Contains additional concept attributes and metadata, such as strengths, active modalities, and source-specific identifiers.

Instead of writing raw SQL joins across millions of .RRF rows, we’ll use Python and Django's ORM to model these tables and query RxNorm programmatically. It is lazy and fast!

A basic model for RxNorm relationship:

class TermType(models.Model):
    """TTY - Term Types from RxNorm (IN, BN, SCD, etc.)"""
    code = models.CharField(max_length=10, primary_key=True)
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True)

class Source(models.Model):
    """SAB - Source Vocabularies (RxNorm, GS, MTHSPL, etc.)"""
    code = models.CharField(max_length=20, primary_key=True)
    name = models.CharField(max_length=100)
    version = models.CharField(max_length=20, blank=True)

class SemanticType(models.Model):
    """STY - Semantic Types from RXNSTY"""
    code = models.CharField(max_length=50, primary_key=True)
    name = models.CharField(max_length=200)
    definition = models.TextField(blank=True)

class DrugConcept(models.Model):
    """RXCUI Level - Core Drug Concept"""
    rxcui = models.CharField(max_length=20, primary_key=True)
    name = models.CharField(max_length=255, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

class ConceptSemanticType(models.Model):
    """Through model for semantic type assignments"""
    concept = models.ForeignKey(DrugConcept, on_delete=models.CASCADE)
    semantic_type = models.ForeignKey(SemanticType, on_delete=models.CASCADE)
    source = models.ForeignKey(Source, on_delete=models.SET_NULL, null=True)

class DrugAtom(models.Model):
    """RXAUI Level - Term Representations"""
    rxaui = models.CharField(max_length=20, primary_key=True)
    concept = models.ForeignKey(
        DrugConcept, on_delete=models.CASCADE, related_name="atoms")
    term_type = models.ForeignKey(TermType, on_delete=models.PROTECT)
    source = models.ForeignKey(Source, on_delete=models.PROTECT)
    term = models.TextField()
    code = models.CharField(max_length=255)

class RelationshipType(models.Model):
    """Dynamic relationship types (RO, PAR, etc.)"""
    code = models.CharField(max_length=100, primary_key=True)
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True)

class DrugAttribute(models.Model):
    """RXNSAT - Atom-level Attributes"""
    atom = models.ForeignKey(
        DrugAtom,
        on_delete=models.CASCADE,
        related_name="attributes")
    name = models.CharField(max_length=50)  # ATN
    value = models.TextField()  # ATV
    source = models.ForeignKey(Source, on_delete=models.PROTECT)

class DrugRelationship(models.Model):
    """RXNREL - Concept Relationships"""
    source_concept = models.ForeignKey(
        DrugConcept,
        on_delete=models.CASCADE,
        related_name="outgoing_concept_rels",
        null=True,
        blank=True,
    )
    target_concept = models.ForeignKey(
        DrugConcept,
        on_delete=models.CASCADE,
        related_name="incoming_concept_rels",
        null=True,
        blank=True,
    )

    # Atom-level relationships
    source_atom = models.ForeignKey(
        DrugAtom,
        on_delete=models.CASCADE,
        related_name="outgoing_atom_rels",
        null=True,
        blank=True,
    )
    target_atom = models.ForeignKey(
        DrugAtom,
        on_delete=models.CASCADE,
        related_name="incoming_atom_rels",
        null=True,
        blank=True,
    )

    rel_type = models.ForeignKey(
        RelationshipType, on_delete=models.PROTECT, null=True)
    source_vocab = models.ForeignKey(Source, on_delete=models.PROTECT)

From out previous example of "Augmentin", we can extract the active ingredients of that drug combination and query it against records of a patient's drug allergy as shown below:

# Fetch the exact drug atom from the dataset
augmentin = DrugAtom.objects.get(
    term__icontains='amoxicillin 875 MG / clavulanate 125 MG Oral Tablet',
    term_type='SBD'
)

# Extract the active modalities (RXN_AM) from attributes
attributes = augmentin.attributes.filter(name__icontains='RXN_AM').values_list('value', flat=True)
# Outputs: 

# Extract the ingredient concepts (723 and 21216)
ing_1 = DrugConcept.objects.get(rxcui='723').atoms.filter(term_type__in=['PIN', 'IN'], source='RXNORM').first().term
ing_2 = DrugConcept.objects.get(rxcui='21216').atoms.filter(term_type__in=['PIN', 'IN'], source='RXNORM').first().term

print(ing_1)  # amoxicillin
print(ing_2)  # clavulanic acid

This snippet provides a clear demonstration of how querying RXN_AM attributes allows an EHR to break down compound brand names into active ingredients for automated allergy checks and Adverse Drug Reaction (ADR) reporting.

Limitations of RxNorm in Nigeria

As we've seen, RxNorm is crucial in health informatics, yet it hasn't been fully adopted across the Nigerian health ecosystem. A primary barrier is vocabulary localization: RxNorm's primary US focus means that standard Nigerian clinical nomenclature, local brand names, and British Approved Names (BAN) are often absent or mapped differently in the default NLM dataset—such as paracetamol being represented predominantly as acetaminophen, or salbutamol as albuterol. 

Because local prescribers, pharmacies, and NAFDAC (National Agency for Food and Drug Administration and Control) registrations rely heavily on these local conventions and commercial brand listings, establishing direct initial mappings to native RxNorm concepts requires non-trivial vocabulary alignment. Cross-referencing NAFDAC Greenbook entries against standard RxCUIs often reveals missing brand-to-ingredient links for locally manufactured formulations.

At Clinical Records Hub (CRH), we are actively addressing this gap by bridging NAFDAC-registered products with RxNorm concept graphs. By curating localized alias tables that map Nigerian pharmaceutical listings directly to normalized RxNorm concepts, we are building the semantic foundation necessary for terminology-aware EHRs, automated allergy alerts, and localized clinical decision support in Nigeria.

Comments