VANDF → RxNorm clinical drug bi-encoder, trained on every ingredient family
The recipe of kvenanzi/vandf-rxnorm-biencoder,
trained on all 14,369 VA National Drug File (VANDF) strings that have a clinical
drug in RxNorm, so that every ingredient family in the file is in training. It
maps a VANDF drug string to the RxNorm clinical drug it names (SCD or SBD:
ingredient, strength, and dose form), with a calibrated confidence for routing
uncertain strings to review.
This model has no test set of its own: every VA string was used for training.
Its expected accuracy comes from seven-fold cross-validation of the recipe, and
a comparison with the first model on strings neither model trained on (below).
Write-up: https://withinnoise.dev/blog/posts/vandf-rxnorm-interventions/ (§5.4–5.5).
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-all") # ~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")
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.961) was chosen on pooled out-of-fold predictions for 99% precision
on a population that includes real drugs with no SCD/SBD; pass threshold= to
change it.
Expected accuracy
Seven folds of the ingredient hash used for the first model: each fold model
trains the same recipe on five folds, selects its epoch on a sixth, and is
tested once on the seventh. Pooled over the seven test folds:
|
acc@1 |
recall@5 |
n |
| Pooled out-of-fold |
0.894 (Wilson 95% 0.889–0.899) |
0.975 |
12,227 VA strings |
| Range over the seven folds |
0.864–0.932 |
0.960–0.988 |
1,533–1,890 per fold |
This is the expected accuracy on strings whose ingredients are new to the model.
The fold models' best epochs were 1, 1, 3, 3, 1, 3, and 1; this model trained for
the median, one epoch.
Comparison with the first model
Neither model trained on the FDA Structured Product Label names (MTHSPL), so
those strings are held out from both:
| Strings |
n |
First model |
This model |
| FDA label names whose ingredients both models trained on |
28,464 |
0.729 |
0.753 |
| FDA label names whose ingredients only this model trained on |
13,492 |
0.716 |
0.746 |
| VA strings both models trained on |
9,287 |
0.939 |
0.909 |
On the first row, 1,409 strings are answered correctly by this model alone and
701 by the first model alone (exact sign test, p ≈ 2 × 10⁻⁵⁴). This model fits
its own training strings less closely, as expected from one epoch of training,
and is more accurate on strings that neither model saw.
Calibration
A temperature (0.0386) and a Platt layer over [cosine, top-1 − top-2 margin, log
softmax], fit on the pooled out-of-fold predictions of 12,227 matched VA strings
and 819 real drugs with no SCD/SBD. Thresholds chosen on six folds and applied to
the seventh:
| Target |
Threshold (pooled) |
Precision on the held-out fold |
Coverage |
| 95% |
0.735 |
0.922–0.967 |
about 0.81 |
| 99% (default) |
0.961 |
0.986–0.998 |
about 0.50 |
The 99% threshold transfers across held-out ingredients more reliably than the
95% one. Re-choose thresholds on your own held-out data.
Training
- Base model: SapBERT
MultipleNegativesRankingLoss on (VA string, RxNorm name, hard negative)
triplets; the hard negative has the same ingredients and a different strength
or dose form. The deterministic strength normalizer of the first model.
- 1 epoch, batch 64, lr 2e-5, 10% warmup, fp16, max_seq_length 96, seed 42, one
Colab A100
- Data: every VANDF string with an SCD/SBD in RxNorm 2026-09-08 (14,369 strings,
14,372 pairs); no ingredient is held out.
Limitations
- No test set of its own. The accuracy above is estimated by cross-validation
of the recipe (one partition, one seed), not measured on this model.
- Trained on VA strings only. On FDA label names acc@1 is about 0.75; other
vocabularies are unmeasured.
- 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. Use the confidence to route uncertain
strings to a pharmacist, or use the top-5 as suggestions.
Code: https://github.com/kvenanzi/rxnorm · Runs: https://wandb.ai/within-noise/rxnorm-vandf/table