This skill produces an honest entity-level scorecard for an OpenMed NER model:
precision / recall / F1 plus a per-label error breakdown. It scores spans,
not tokens, because clinical entities are multi-token ("type 2 diabetes
mellitus") and token-level accuracy hides boundary errors. Reported numbers are
entity-level in the seqeval tradition (CoNLL-2000 / SemEval-2013 families).
mislabeled.
For PHI de-id specifically, gate on leakage with evaluating-with-leakage-gates
instead of (or in addition to) F1.
| Mode | Counts a hit when… | Use for |
|---|---|---|
| Strict / exact | predicted span boundaries and label match gold exactly | release scoring, boundary-sensitive tasks |
| Partial / relaxed | predicted span overlaps gold with the right label | recall-oriented triage, tokenizer-mismatch tolerance |
OpenMed exposes both: compute_exact_span_f1 (strict) andcompute_relaxed_span_f1 (partial), with the full bundle incompute_metrics_bundle.
Run a model over a user-supplied gold fixtures file and print a scorecard:
from openmed.eval import run_suite, error_report
# Fixtures: JSON list of {"id", "text", "gold_spans": [{start, end, label}, ...]}
report = run_suite(
"eval/gold/clinical_ner.json", # YOUR gold corpus, not bundled
suite="golden",
model_name="OpenMed/Disease-Detection",
device="cpu",
)
m = report.metrics
print("exact F1 :", m["exact_span_f1"]["f1"]) # strict
print("relaxed F1:", m["relaxed_span_f1"]["f1"]) # partial
print("recall by label:", m["recall_slices"]["by_label"])
# Per-label confusion matrix + capped, no-PHI error examples.
errors = error_report(
"OpenMed/Disease-Detection",
"eval/gold/clinical_ner.json",
suite_name="clinical_ner",
example_cap=5,
)
print(errors.to_markdown()) # confusion matrix + FN/FP tables
errors.write_json("eval/out/error_analysis.json")
Need just the metrics on spans you already have? Call the metric functions
directly:
from openmed.eval import compute_exact_span_f1, compute_relaxed_span_f1
strict = compute_exact_span_f1(gold_spans, predicted_spans)
partial = compute_relaxed_span_f1(gold_spans, predicted_spans)
standoff into the fixture shape: text + gold_spans of{start, end, label} character offsets. (CoNLL → offsets; BRAT .ann is
already character offsets.)
don't count as label confusion. Mislabeled-but-overlapping spans show up in
the confusion matrix, not as misses.
run_suite / run_benchmark to get a BenchmarkReport.detection failures — often tokenizer or whitespace issues.
error_report for the per-label confusion matrix and cappedexamples. MISSED = false negatives (recall problem); SPURIOUS = false
positives (precision problem); off-diagonal = label confusion.
labels usually dominate the error budget.
extracting-clinical-entities (openmed.analyze_text): the model andpredictions you score here come from the NER pipeline.
evaluating-with-leakage-gates: for de-id models, F1 is necessary butnot sufficient — pass the same fixtures through the release gates.
authoring-model-cards: drop error_report confusion matrices andper-label F1 straight into the model card's quantitative-analysis section.
building-gold-corpus (supplies the fixtures) andauditing-subgroup-fairness (slices the same run by demographic group).
(compute_exact_span_f1 / compute_relaxed_span_f1), not token accuracy.
matcher picks the best single overlapping prediction per gold span; nested
schemes (e.g. DISEASE inside ANATOMY) should be flattened or scored per layer.
critical entity (e.g. ALLERGY) that micro-F1 buries.
ErrorSpanExample stores offsets,context windows, and sha256: text hashes — never plaintext. Keep it that way.
"low-F1" model may be right and the gold wrong. Spot-check disagreements before
blaming the model.
from the user's licensed copy at eval time; never commit them.
https://github.com/chakki-works/seqeval
https://www.davidsbatista.net/blog/2018/05/09/NamedEntityEvaluation/
https://aclanthology.org/W03-0419/
https://brat.nlplab.org/standoff.html
openmed/eval/metrics.py,openmed/eval/error_analysis.py, openmed/eval/harness.py.