VANDF → RxNorm clinical drug bi-encoder
Maps a VA National Drug File (VANDF) drug string to the RxNorm clinical drug
it means (SCD or SBD: ingredient, strength, and dose form), with a calibrated
confidence so you can auto-accept the sure cases and route the rest to review.
| Input |
Output |
Confidence |
METOPROLOL TARTRATE 12.5MG TAB |
metoprolol tartrate 12.5 MG Oral Tablet (RXCUI 866508) |
0.97 → accept |
ACETIC ACID 0.25% IRRG SOLN |
acetic acid 2.5 MG/ML Irrigation Solution |
0.96 → accept |
HYOSCYAMINE SO4 0.125MG/5ML ELIXIR |
hyoscyamine sulfate 0.125 MG/ML Oral Solution (wrong: truth is 0.025 MG/ML, listed 2nd) |
0.77 → review |
CATHETER,FOLEY SILICONE 22FR 5CC |
(nearest candidate, meaningless) |
0.02 → review |
Published tools such as RxMap normalize to the ingredient level (IN/MIN).
This model targets the full clinical drug, where a miss on strength or dose
form is a wrong answer. Method, hypotheses, the full sweep, and the error
analysis are in the write-up: https://withinnoise.dev/blog/posts/vandf-rxnorm-biencoder/
Usage
pip install "rxnorm-vandf @ git+https://github.com/kvenanzi/rxnorm"
from rxnorm_vandf.infer import Mapper
mapper = Mapper.from_pretrained("kvenanzi/vandf-rxnorm-biencoder") # ~450 MB download
for p in mapper.map(["METOPROLOL TARTRATE 12.5MG TAB", "CATHETER,FOLEY SILICONE 22FR 5CC"]):
print(p.rxcui, p.name, p.tty, f"{p.confidence:.2f}", "accept" if p.accept else "review")
# p.alternatives: the next four candidates as (rxcui, name, cosine)
Mapper loads the encoder, train_config.json (input preprocessing),
calibration.json (score → probability), and candidates.parquet (the 27,287
active RxNorm SCD/SBD names it searches) from this repo. The default acceptance
threshold (0.92) was chosen on validation for 99% precision on a population
that includes real drugs with no SCD/SBD; pass threshold= to change it.
How it works
- Preprocessing. The VA string is lowercased, punctuation is stripped, and a
deterministic rule appends the RxNorm-style concentration
(
0.125MG/5ML → 0.025 mg/ml; 0.25% → 2.5 mg/ml; mg/mg for gels and
ointments). An embedding model can't do this arithmetic; a regex can.
- Retrieval. A SapBERT
bi-encoder, fine-tuned with
MultipleNegativesRankingLoss on 9,287
(VA string, RxNorm name) pairs plus hard negatives (same ingredients,
different strength or dose form), embeds the query and all candidates;
top-k by cosine.
- Calibration. A logistic (Platt) layer over [cosine, top-1−top-2 margin,
temperature-scaled softmax] gives P(correct). Fit on validation only.
Results
Test split: 1,848 VA strings whose ingredients never appear in training.
The candidate pool is all 27,287 active SCD/SBD in RxNorm 2026-09-08.
| Method |
acc@1 |
recall@5 |
ingredient |
strength |
dose form |
| Exact string match |
0.000 |
0.000 |
– |
– |
– |
| TF-IDF char 3–5-grams |
0.509 |
0.820 |
0.978 |
0.617 |
0.698 |
| MiniLM-L6 fine-tuned |
0.836 |
0.967 |
0.983 |
0.884 |
0.935 |
| + strength normalizer |
0.886 |
0.975 |
0.982 |
0.941 |
0.943 |
| SapBERT + normalizer (this model) |
0.931 |
0.984 |
0.983 |
0.961 |
0.972 |
Validation: acc@1 0.883, recall@5 0.961.
The 0.931 is one draw of the ingredient split. Re-drawing the split five more
times and retraining the same recipe gives test acc@1 0.907 ± 0.019 (range
0.874–0.931; the published split is the most favorable of the six), and
seven-fold cross-validation by ingredient gives a pooled out-of-fold acc@1 of
0.894 (Wilson 95% 0.889–0.899, n = 12,227). The training seed contributes a
standard deviation of about 0.0045 (two seeds on each of the six draws). Read
the headline as a band of about two points. Recall@5 (0.974–0.988) and the
component accuracies vary little across draws.
Abstention. Thresholds chosen on validation, measured on test:
| Population |
Signal |
Auto-accepted |
Precision of accepted |
| VA strings that have an SCD/SBD |
softmax |
79.0% |
0.988 |
| + real drugs with no SCD/SBD (packs, ingredient-only) |
platt (default) |
46.0% |
0.982 |
The validation target was 99%; test lands at 98.2–98.8%. Treat the achieved
number as the estimate, not the target. In cross-validation of the same recipe,
a 99% threshold fit on the pooled out-of-fold predictions (about 11,000 strings)
achieved 98.6–99.8% on each held-out fold, which indicates that the shortfall
here comes from choosing the threshold on one validation split of about 2,300
strings.
An 18-run sweep (3 encoders × 3 negative strategies × normalizer on/off) found
the three effects roughly additive: domain pre-training (SapBERT vs general
encoders) +8.6 points val acc@1, the strength normalizer +4.8, ingredient-matched
hard negatives +3. The hard-negatives effect was then re-run on all six ingredient
splits (18 runs): ingredient-matched negatives beat in-batch-only on every split, by
1.6 points of test acc@1 on average (95% CI 0.8–2.4). Live charts: https://wandb.ai/within-noise/rxnorm-vandf.
A follow-up tested a second source vocabulary (the FDA label names), an
auxiliary strength head, and cross-validation by ingredient; neither training
change altered accuracy on VA strings. Write-up:
https://withinnoise.dev/blog/posts/vandf-rxnorm-interventions/
For mapping new strings, the same recipe trained on every ingredient family is
published as https://huggingface.co/kvenanzi/vandf-rxnorm-biencoder-all (no test set
of its own; cross-validated acc@1 0.894 on unseen ingredients).
Limitations
- Trained on VA strings only. Other systems' drug names are a different
distribution. On FDA Structured Product Label names (MTHSPL) the same recipe
scores acc@1 0.709 ± 0.057 across twelve draw-and-seed runs; other
vocabularies are unmeasured.
- One split for calibration. This model's abstention thresholds were chosen
on one validation draw. Re-choose thresholds on your own held-out data.
- Candidates are RxNorm 2026-09-08. RxNorm changes monthly; rebuild
candidates.parquet for a newer release (scripts/03_build_dataset.py in the repo).
- Not for unsupervised clinical use. A 7% top-1 error rate on medication
codes is a safety problem; use the confidence to route uncertain strings to a
pharmacist, or use the top-5 as suggestions.
- Of the 128 test errors, 46 involve strength (often an underdetermined string:
MANNITOL 250MG/ML INJ vs RxNorm's 50 ML mannitol 250 MG/ML Injection),
43 dose form, 31 ingredient, and 25 are SCD-vs-SBD twins with identical components.
Training details
- Base:
cambridgeltl/SapBERT-from-PubMedBERT-fulltext (110M params, Apache-2.0)
- Loss:
MultipleNegativesRankingLoss, triplets (anchor, positive, hard negative),
NO_DUPLICATES batch sampler; negatives drawn from train-split candidates only
- 4 epochs, batch 64, lr 2e-5, 10% warmup, fp16, max_seq_length 96, seed 42
- Best epoch by validation acc@1; test scored once
- ~20 min on a GTX 1070, ~2 min on a Colab A100
Data and license
Training pairs come from RxNorm itself: a VANDF atom and an RxNorm SCD/SBD atom
that share an RXCUI are the same drug by NLM's curation. Only the VANDF and
RXNORM source vocabularies were used (UMLS source restriction category 0:
"general terms of the License apply with no additional restrictions"). No PHI.
Dataset: kvenanzi/vandf-rxnorm-pairs.
Model weights: Apache-2.0, as the base model.
References
- Liu F, Shareghi E, Meng Z, Basaldella M, Collier N (2021). "Self-Alignment
Pretraining for Biomedical Entity Representations". NAACL-HLT 2021:4228–4238.
https://aclanthology.org/2021.naacl-main.334/ (SapBERT)
- Reimers N, Gurevych I (2019). "Sentence-BERT: Sentence Embeddings using Siamese
BERT-Networks". EMNLP-IJCNLP 2019:3982–3992. https://aclanthology.org/D19-1410/
- Henderson M et al (2017). "Efficient Natural Language Response Suggestion for
Smart Reply". arXiv:1705.00652. (the in-batch negatives loss)
- Guo C, Pleiss G, Sun Y, Weinberger KQ (2017). "On Calibration of Modern Neural
Networks". ICML 2017. arXiv:1706.04599. (temperature scaling)
- Platt JC (1999). "Probabilistic Outputs for Support Vector Machines and
Comparisons to Regularized Likelihood Methods". In Advances in Large Margin
Classifiers, MIT Press, pp. 61–74.
- Nelson SJ, Zeng K, Kilbourne J, Powell T, Moore R (2011). "Normalized names for
clinical drugs: RxNorm at 6 years". JAMIA 18(4):441–448.
https://doi.org/10.1136/amiajnl-2011-000116
- Korpela E, Rubin LH, Dastgheyb RM, Xu Y (2026). "RxMap: an LLM-assisted tool for
medication normalization". JAMIA Open 9(3):ooag085.
https://doi.org/10.1093/jamiaopen/ooag085
- RxNorm is produced by the U.S. National Library of Medicine; VANDF by the U.S.
Department of Veterans Affairs.