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://kettlelabs.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

  1. 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.
  2. 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.
  3. 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/kettle-labs/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://kettlelabs.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.
Downloads last month
39
Safetensors
Model size
0.1B params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for kvenanzi/vandf-rxnorm-biencoder

Finetuned
(26)
this model

Dataset used to train kvenanzi/vandf-rxnorm-biencoder

Papers for kvenanzi/vandf-rxnorm-biencoder

Evaluation results

  • acc@1 on VANDF-RxNorm pairs, test split (held-out ingredients)
    self-reported
    0.931
  • recall@5 on VANDF-RxNorm pairs, test split (held-out ingredients)
    self-reported
    0.984