feat(clinical): radiology report section parser with stated RADS capture#1849
Merged
maziyarpanahi merged 3 commits intoJul 21, 2026
Merged
Conversation
…capture Add openmed/clinical/radiology_report.py: a deterministic parser that segments a radiology report into a findings/impression/recommendation template and captures a stated BI-RADS (0-6) or Lung-RADS (0,1,2,3,4A,4B,4X) assessment category only when it is explicitly written. Segmentation uses a documented heading lexicon matched at line starts, with an inline-cue fallback (e.g. "IMPRESSION:") when a report has no standalone headings, and preserves per-section character spans for provenance. RADIOLOGY_REPORT_ADVISORY records that any captured category is read verbatim from the report, never computed or inferred. - Export parse_radiology_report, RadiologyReportTemplate, and the advisory from openmed.clinical. - Add parser-agnostic section_boundary_accuracy and stated_category_accuracy metrics to openmed/eval/metrics.py. - Commit synthetic gold fixtures (tests/fixtures/clinical/radiology_report.jsonl and openmed/eval/golden/fixtures/radiology_report.jsonl) and an offline eval gate asserting section-boundary accuracy >= 0.90 and stated-category accuracy >= 0.95 (both 1.00 on the current gold set). - Exclude the radiology eval fixture from the PII de-identification golden-fixture loader glob so load_golden_fixtures() does not treat it as de-id gold. All fixtures are synthetic and algorithmically constructed. Closes maziyarpanahi#1838
maziyarpanahi
self-requested a review
July 20, 2026 21:55
Owner
|
Thank you @PouyanJay — this was a thoughtful, well-scoped implementation. I reviewed it against #1838 and current What changed:
Validation:
This now satisfies #1838 and is ready to merge. Thank you for the solid contribution! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #1838 (OM-837): a deterministic radiology report parser that segments a report into a findings/impression/recommendation template and captures a stated BI-RADS or Lung-RADS category. It makes no assessment decision of its own — a captured category is read verbatim from the report text, never computed or inferred.
openmed/clinical/radiology_report.py:parse_radiology_report(text) -> RadiologyReportTemplate(TypedDict:findings_text,impression_text,recommendation_text,assessment_system,assessment_category,section_spans).IMPRESSION:) when a report has no standalone headings. Per-section character spans are preserved for provenance.0–6and Lung-RADS0,1,2,3,4A,4B,4X, captured only when explicitly written (qualifier-introduced or terminal), never inferred.RADIOLOGY_REPORT_ADVISORYrecords the read-not-computed guarantee; exported fromopenmed.clinical.openmed/eval/metrics.py: parser-agnosticsection_boundary_accuracyandstated_category_accuracy.Acceptance criteria
parse_radiology_report()splits findings/impression/recommendation on all synthetic gold rows.RADIOLOGY_REPORT_ADVISORYis emitted and asserted; offline eval gate passes.Deviations from the issue's letter (with evidence)
openmed/eval/golden/loader.py. The golden-fixture loader globs**/*.jsonland raises on any unknowncategory, so a naively-addedopenmed/eval/golden/fixtures/radiology_report.jsonlwould be picked up as a PII de-identification fixture and breaktest_golden_fixtures/test_fixture_coverage. The fixture name is added to_NON_DEID_FIXTURE_NAMESso the de-id loader skips it. Verified:load_golden_fixtures()returns 90 fixtures unchanged and the golden-fixture gates still pass.tests/unit/clinical/test_radiology_report.py(unit) andtests/unit/eval/test_radiology_report_eval.py(offline eval gate) — the issue lists metrics/fixtures but not the test files that exercise them.master(clinical/sections/detect.py); OM-836 (Add a RadLex-coded radiology-finding extractor with finding, laterality, size and location #1837) is a separate module (radiology_finding.py) that this section parser does not use. This lands ahead of Add a RadLex-coded radiology-finding extractor with finding, laterality, size and location #1837 but has no code dependency on it.Data provenance
All fixtures are synthetic and algorithmically generated (fictional reports, no real patient data). Categories in fixtures are written verbatim, never computed.
Verification
tests/unit/mcp/test_mcp_entrypoint.py::test_console_entry_point_resolves, is pre-existing onmaster(a stale editable-install artifact for theopenmed-mcpconsole entry point; verified by reproducing it with these changes stashed) and is not caused by this PR.BI-RADS 4 lesions,BI-RADS 0-6 assessment scale) are correctly not captured, and a count in one section never shadows a stated category in another — covered by regression tests and gold rows.pre-commit(ruff, ruff-format, bandit, gitleaks) clean on changed files.master;git rev-list --count HEAD..upstream/masteris 0.Closes #1838