pymetamaplite 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pymetamaplite-0.1.0/.gitignore +46 -0
- pymetamaplite-0.1.0/CHANGELOG.md +189 -0
- pymetamaplite-0.1.0/LICENSE +75 -0
- pymetamaplite-0.1.0/NOTICE-MetaMapLite.md +56 -0
- pymetamaplite-0.1.0/PKG-INFO +267 -0
- pymetamaplite-0.1.0/README.md +223 -0
- pymetamaplite-0.1.0/docs/GETTING_STARTED.md +210 -0
- pymetamaplite-0.1.0/docs/UMLS identifiers.md +40 -0
- pymetamaplite-0.1.0/examples/README.md +468 -0
- pymetamaplite-0.1.0/examples/_annotations.py +280 -0
- pymetamaplite-0.1.0/examples/_runs.py +210 -0
- pymetamaplite-0.1.0/examples/add_icd10cm_codes.py +222 -0
- pymetamaplite-0.1.0/examples/add_icd10cm_hierarchy.py +328 -0
- pymetamaplite-0.1.0/examples/add_loinc_codes.py +228 -0
- pymetamaplite-0.1.0/examples/add_rxnorm_codes.py +196 -0
- pymetamaplite-0.1.0/examples/add_rxnorm_ingredients.py +306 -0
- pymetamaplite-0.1.0/examples/add_snomed_codes.py +203 -0
- pymetamaplite-0.1.0/examples/add_snomedct_hierarchy.py +335 -0
- pymetamaplite-0.1.0/examples/analyze_file.py +174 -0
- pymetamaplite-0.1.0/examples/analyze_folder.py +360 -0
- pymetamaplite-0.1.0/examples/compare_exports.py +313 -0
- pymetamaplite-0.1.0/examples/create_combined_codes_view.py +134 -0
- pymetamaplite-0.1.0/examples/filter_mrconso.py +111 -0
- pymetamaplite-0.1.0/examples/filter_mrrel.py +138 -0
- pymetamaplite-0.1.0/examples/json_to_sqlite.py +256 -0
- pymetamaplite-0.1.0/examples/load_to_sqlite.py +345 -0
- pymetamaplite-0.1.0/examples/note01.txt +177 -0
- pymetamaplite-0.1.0/examples/note02.txt +160 -0
- pymetamaplite-0.1.0/examples/notes_of_interest.py +285 -0
- pymetamaplite-0.1.0/examples/parse_to_csv.py +218 -0
- pymetamaplite-0.1.0/examples/parse_to_jsonl.py +210 -0
- pymetamaplite-0.1.0/examples/parse_to_jsonl_batch.py +267 -0
- pymetamaplite-0.1.0/examples/parse_to_parquet.py +282 -0
- pymetamaplite-0.1.0/examples/parse_to_sqlite.py +221 -0
- pymetamaplite-0.1.0/pyproject.toml +130 -0
- pymetamaplite-0.1.0/requirements-scispacy-py312.txt +22 -0
- pymetamaplite-0.1.0/scripts/README.md +13 -0
- pymetamaplite-0.1.0/scripts/compare_nlp_with_java.py +161 -0
- pymetamaplite-0.1.0/scripts/compare_with_java.py +197 -0
- pymetamaplite-0.1.0/scripts/gen_context_triggers.py +67 -0
- pymetamaplite-0.1.0/scripts/gen_greek_table.py +37 -0
- pymetamaplite-0.1.0/scripts/gen_negex_triggers.py +34 -0
- pymetamaplite-0.1.0/src/pymetamaplite/__init__.py +18 -0
- pymetamaplite-0.1.0/src/pymetamaplite/api.py +232 -0
- pymetamaplite-0.1.0/src/pymetamaplite/cli.py +401 -0
- pymetamaplite-0.1.0/src/pymetamaplite/config.py +125 -0
- pymetamaplite-0.1.0/src/pymetamaplite/documents/__init__.py +115 -0
- pymetamaplite-0.1.0/src/pymetamaplite/documents/bioc.py +101 -0
- pymetamaplite-0.1.0/src/pymetamaplite/documents/corpora.py +204 -0
- pymetamaplite-0.1.0/src/pymetamaplite/documents/freetext.py +81 -0
- pymetamaplite-0.1.0/src/pymetamaplite/documents/model.py +100 -0
- pymetamaplite-0.1.0/src/pymetamaplite/documents/pubmed.py +187 -0
- pymetamaplite-0.1.0/src/pymetamaplite/documents/singleline.py +165 -0
- pymetamaplite-0.1.0/src/pymetamaplite/greek.py +147 -0
- pymetamaplite-0.1.0/src/pymetamaplite/index/__init__.py +4 -0
- pymetamaplite-0.1.0/src/pymetamaplite/index/build.py +227 -0
- pymetamaplite-0.1.0/src/pymetamaplite/index/lookup.py +278 -0
- pymetamaplite-0.1.0/src/pymetamaplite/index/schema.py +49 -0
- pymetamaplite-0.1.0/src/pymetamaplite/javautil.py +78 -0
- pymetamaplite-0.1.0/src/pymetamaplite/lru.py +48 -0
- pymetamaplite-0.1.0/src/pymetamaplite/normalize.py +93 -0
- pymetamaplite-0.1.0/src/pymetamaplite/output/__init__.py +78 -0
- pymetamaplite-0.1.0/src/pymetamaplite/output/formats.py +207 -0
- pymetamaplite-0.1.0/src/pymetamaplite/output/mmi.py +212 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/__init__.py +225 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/abbreviations.py +292 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/assertion.py +82 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/context.py +581 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/context_triggers.py +399 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/entity_lookup.py +446 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/negation.py +221 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/negex_triggers.py +268 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/postag.py +91 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/precision.py +111 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/segment.py +176 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/spacy_nlp.py +174 -0
- pymetamaplite-0.1.0/src/pymetamaplite/pipeline/tokenize.py +101 -0
- pymetamaplite-0.1.0/src/pymetamaplite/py.typed +0 -0
- pymetamaplite-0.1.0/src/pymetamaplite/semtypes.py +215 -0
- pymetamaplite-0.1.0/src/pymetamaplite/server.py +707 -0
- pymetamaplite-0.1.0/src/pymetamaplite/types.py +134 -0
- pymetamaplite-0.1.0/tests/__init__.py +0 -0
- pymetamaplite-0.1.0/tests/conftest.py +73 -0
- pymetamaplite-0.1.0/tests/examples/__init__.py +0 -0
- pymetamaplite-0.1.0/tests/examples/conftest.py +171 -0
- pymetamaplite-0.1.0/tests/examples/test_add_icd10cm_codes.py +162 -0
- pymetamaplite-0.1.0/tests/examples/test_add_icd10cm_hierarchy.py +172 -0
- pymetamaplite-0.1.0/tests/examples/test_add_loinc_codes.py +139 -0
- pymetamaplite-0.1.0/tests/examples/test_add_rxnorm_codes.py +127 -0
- pymetamaplite-0.1.0/tests/examples/test_add_rxnorm_ingredients.py +244 -0
- pymetamaplite-0.1.0/tests/examples/test_add_snomed_codes.py +141 -0
- pymetamaplite-0.1.0/tests/examples/test_add_snomedct_hierarchy.py +185 -0
- pymetamaplite-0.1.0/tests/examples/test_analyze_file.py +225 -0
- pymetamaplite-0.1.0/tests/examples/test_analyze_folder.py +206 -0
- pymetamaplite-0.1.0/tests/examples/test_annotations_helper.py +208 -0
- pymetamaplite-0.1.0/tests/examples/test_compare_exports.py +167 -0
- pymetamaplite-0.1.0/tests/examples/test_create_combined_codes_view.py +110 -0
- pymetamaplite-0.1.0/tests/examples/test_filter_mrconso.py +66 -0
- pymetamaplite-0.1.0/tests/examples/test_filter_mrrel.py +76 -0
- pymetamaplite-0.1.0/tests/examples/test_json_to_sqlite.py +180 -0
- pymetamaplite-0.1.0/tests/examples/test_load_to_sqlite.py +178 -0
- pymetamaplite-0.1.0/tests/examples/test_notes_of_interest.py +167 -0
- pymetamaplite-0.1.0/tests/examples/test_parse_to_csv.py +161 -0
- pymetamaplite-0.1.0/tests/examples/test_parse_to_jsonl.py +112 -0
- pymetamaplite-0.1.0/tests/examples/test_parse_to_jsonl_batch.py +167 -0
- pymetamaplite-0.1.0/tests/examples/test_parse_to_parquet.py +130 -0
- pymetamaplite-0.1.0/tests/examples/test_parse_to_sqlite.py +159 -0
- pymetamaplite-0.1.0/tests/examples/test_runs_helper.py +170 -0
- pymetamaplite-0.1.0/tests/parity/README.md +66 -0
- pymetamaplite-0.1.0/tests/parity/corpus/abbreviations.txt +1 -0
- pymetamaplite-0.1.0/tests/parity/corpus/clinical_note_1.txt +7 -0
- pymetamaplite-0.1.0/tests/parity/corpus/clinical_note_2.txt +1 -0
- pymetamaplite-0.1.0/tests/parity/corpus/crlf_note.txt +5 -0
- pymetamaplite-0.1.0/tests/parity/corpus/empty_ish.txt +3 -0
- pymetamaplite-0.1.0/tests/parity/corpus/long_sentence.txt +1 -0
- pymetamaplite-0.1.0/tests/parity/corpus/negation_cases.txt +1 -0
- pymetamaplite-0.1.0/tests/parity/corpus/pubmed_abstract_1.txt +1 -0
- pymetamaplite-0.1.0/tests/parity/corpus/pubmed_abstract_2.txt +1 -0
- pymetamaplite-0.1.0/tests/parity/corpus/short_lines.txt +9 -0
- pymetamaplite-0.1.0/tests/parity/corpus/tricky_tokens.txt +1 -0
- pymetamaplite-0.1.0/tests/parity/corpus/unicode_terms.txt +5 -0
- pymetamaplite-0.1.0/tests/parity/test_parity.py +334 -0
- pymetamaplite-0.1.0/tests/unit/__init__.py +0 -0
- pymetamaplite-0.1.0/tests/unit/test_abbreviations.py +173 -0
- pymetamaplite-0.1.0/tests/unit/test_assertion.py +146 -0
- pymetamaplite-0.1.0/tests/unit/test_cli.py +263 -0
- pymetamaplite-0.1.0/tests/unit/test_config.py +99 -0
- pymetamaplite-0.1.0/tests/unit/test_context.py +476 -0
- pymetamaplite-0.1.0/tests/unit/test_documents.py +747 -0
- pymetamaplite-0.1.0/tests/unit/test_entity_lookup.py +363 -0
- pymetamaplite-0.1.0/tests/unit/test_index.py +227 -0
- pymetamaplite-0.1.0/tests/unit/test_javautil.py +64 -0
- pymetamaplite-0.1.0/tests/unit/test_lru.py +67 -0
- pymetamaplite-0.1.0/tests/unit/test_negation.py +98 -0
- pymetamaplite-0.1.0/tests/unit/test_normalize.py +93 -0
- pymetamaplite-0.1.0/tests/unit/test_output.py +257 -0
- pymetamaplite-0.1.0/tests/unit/test_parity_comparator.py +120 -0
- pymetamaplite-0.1.0/tests/unit/test_pipeline.py +230 -0
- pymetamaplite-0.1.0/tests/unit/test_precision.py +103 -0
- pymetamaplite-0.1.0/tests/unit/test_segment.py +106 -0
- pymetamaplite-0.1.0/tests/unit/test_semtypes.py +19 -0
- pymetamaplite-0.1.0/tests/unit/test_server.py +428 -0
- pymetamaplite-0.1.0/tests/unit/test_tokenize.py +107 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
ivf/
|
|
8
|
+
# Java MetaMapLite output: contains UMLS-derived strings, do not commit
|
|
9
|
+
tests/parity/fixtures/
|
|
10
|
+
*.RRF
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
# examples/: the scripts, their README and their tests are source and ship. Everything they
|
|
14
|
+
# *produce* is UMLS-derived and does not -- annotation exports, the loaded databases and the
|
|
15
|
+
# filtered MRCONSO/MRREL extracts. Listed by extension rather than by name so a new export
|
|
16
|
+
# format is excluded by default: only .py, .md and the synthetic .txt notes are tracked.
|
|
17
|
+
examples/*.csv
|
|
18
|
+
examples/*.jsonl
|
|
19
|
+
examples/*.parquet
|
|
20
|
+
examples/*.db
|
|
21
|
+
examples/*.sqlite
|
|
22
|
+
examples/*.json
|
|
23
|
+
|
|
24
|
+
# annotation run output (examples/parse_to_*.py): verbatim note text and UMLS-derived
|
|
25
|
+
# strings, one directory per run. Never commit it.
|
|
26
|
+
out/
|
|
27
|
+
# corpora of notes to annotate: real ones are PHI, and even synthetic ones are not source
|
|
28
|
+
free_texts/
|
|
29
|
+
|
|
30
|
+
# local agent/tool state (a shared .claude/settings.json can still be committed)
|
|
31
|
+
# and per-user editor/notes vaults
|
|
32
|
+
.obsidian/
|
|
33
|
+
.claude/*.lock
|
|
34
|
+
|
|
35
|
+
# local credentials
|
|
36
|
+
.env
|
|
37
|
+
.env.*
|
|
38
|
+
|
|
39
|
+
# internal working document, not distributed
|
|
40
|
+
docs/DEVELOPMENT_PLAN.md
|
|
41
|
+
|
|
42
|
+
# internal working documents, not distributed
|
|
43
|
+
docs/OPERATIONS_MANUAL.md
|
|
44
|
+
docs/USER_GUIDE.md
|
|
45
|
+
docs/Negation detection.md
|
|
46
|
+
docs/Deploying to another machine.md
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to pymetamaplite are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
Rationale for the parity-driven decisions behind these changes is recorded in the project's
|
|
8
|
+
development plan, an internal working document that is not distributed.
|
|
9
|
+
|
|
10
|
+
## [0.1.0] — 2026-09-19
|
|
11
|
+
|
|
12
|
+
First release. A Python port of MetaMapLite 3.6.2rc8 — the whole `EntityLookup4` path — as a
|
|
13
|
+
library, a CLI and a REST API, with no JVM.
|
|
14
|
+
|
|
15
|
+
Measured against Java MetaMapLite 3.6.2rc8 on UMLS 2026AA: the index tables are identical row
|
|
16
|
+
for row, tokenization is exact, and concepts found `(CUI, positions)` agree at P 0.975 / R 0.963
|
|
17
|
+
/ F1 0.969. Most of the residual gap is the two NLP models MetaMapLite uses (OpenNLP's sentence
|
|
18
|
+
detector and POS tagger), which have no Python equivalent. See
|
|
19
|
+
[tests/parity/README.md](tests/parity/README.md) for the method.
|
|
20
|
+
|
|
21
|
+
### Annotation
|
|
22
|
+
|
|
23
|
+
- **Index building** from UMLS RRF (`MRCONSO`, `MRSTY`, optionally `MRSAT` for MeSH tree codes),
|
|
24
|
+
producing the `cuisourceinfo`, `cuiconcept`, `cuist` and `meshtcrelaxed` tables in SQLite.
|
|
25
|
+
`--sources` restricts the build; `--include-suppressed` keeps `SUPPRESS=O/E/Y` rows.
|
|
26
|
+
- **The lookup pipeline**: normalization, sentence segmentation, tokenization, POS gating,
|
|
27
|
+
longest-match candidate generation, semantic-type and source restriction (abbreviations or
|
|
28
|
+
TUIs, any case), subsumption removal, Schwartz–Hearst abbreviation propagation, user-defined
|
|
29
|
+
acronyms, custom concept lists and excluded terms.
|
|
30
|
+
- **Negation**, two detectors: NegEx (the default, as in Java) and ConText (`--usecontext`),
|
|
31
|
+
which also reports temporality and experiencer.
|
|
32
|
+
- **Greek-letter spans.** A span may start or end with a Greek letter, as Java's `CharUtils`
|
|
33
|
+
allows, so `β-blocker` matches.
|
|
34
|
+
- **Non-ASCII dictionary keys** resolve reliably. Java's binary search compares UTF-8 as signed
|
|
35
|
+
bytes against a file sorted in `String` order and finds only about 44% of them, missing terms
|
|
36
|
+
such as `Sjögren` and `Ménière disease`; this is a deliberate divergence, pinned by name in the
|
|
37
|
+
test suite rather than averaged into the parity figures.
|
|
38
|
+
- **Precision filter** (`Settings.precision_filter`, default off, not in Java): drops a concept
|
|
39
|
+
whose only dictionary strings for the matched text are acronyms when the text is not in
|
|
40
|
+
capitals ("Plan" loses OMIM's `PLAN`), and one-word function-word mentions ("for", "with")
|
|
41
|
+
unless capitalised. Over the parity corpus it removed 267 of 2,907 matches, 261 of them (97.8%)
|
|
42
|
+
judged wrong or useless by hand.
|
|
43
|
+
|
|
44
|
+
### Input and output
|
|
45
|
+
|
|
46
|
+
- **Every input format MetaMapLite registers**: `freetext`, `sli`, `sldi`, `sldiwi`, `chemdner`,
|
|
47
|
+
`chemdnersldi`, `ncbicorpus`, `pubtator`, `pubmed`, `medline`, `bioc`. `pubtator` is a
|
|
48
|
+
correction rather than a port — Java's loader closes its reader inside the read loop and throws
|
|
49
|
+
on every input. A document model and loader registry (`pymetamaplite.documents`) takes a new
|
|
50
|
+
format without changes elsewhere.
|
|
51
|
+
- **Output formats**: `mmi` (the default), `json`, `brat`, `cuilist`, `full`, and `bc` with
|
|
52
|
+
Java's aliases `bc-evaluate`, `cdi` and `bioc`.
|
|
53
|
+
- **Segmentation**: `SENTENCES`, `BLANKLINES`, `LINES`.
|
|
54
|
+
|
|
55
|
+
### Interfaces
|
|
56
|
+
|
|
57
|
+
- **CLI** (`pymetamaplite`): `annotate`, `lookup`, `normalize`, `tokenize`, `build-index`,
|
|
58
|
+
`index-stats`, `serve`. `annotate` mirrors `metamaplite.sh`.
|
|
59
|
+
- **Python API**: `MetaMapLite(index_directory=...)`, `process_text`, `process_file`, `format`.
|
|
60
|
+
Configuration comes from a `Settings` object, MetaMapLite's own `metamaplite.properties` keys,
|
|
61
|
+
or `PYMETAMAPLITE_<NAME>` environment variables.
|
|
62
|
+
- **REST API** (`serve`): `POST /annotate`, `/annotate/batch`, `/annotate/text`,
|
|
63
|
+
`/annotate/formatted`, `GET /lookup`, `/formats`, `/semantic-types`, `/health`, with OpenAPI
|
|
64
|
+
docs at `/docs`. Requests are serialised behind a lock — annotation is CPU-bound and the
|
|
65
|
+
underlying objects are not thread-safe — so scale out with processes.
|
|
66
|
+
- **Opt-in REST authentication and CORS.** `PYMETAMAPLITE_API_TOKEN` makes every endpoint except
|
|
67
|
+
`/health` require `Authorization: Bearer <token>` (constant-time comparison);
|
|
68
|
+
`PYMETAMAPLITE_CORS_ORIGINS` adds `CORSMiddleware`. Both off unless set.
|
|
69
|
+
- **Request limits**: `PYMETAMAPLITE_MAX_TEXT_CHARS` (50,000, per document) and
|
|
70
|
+
`PYMETAMAPLITE_MAX_FORMATTED_CHARS` (1,000,000, the raw `/annotate/formatted` body). Both are
|
|
71
|
+
checked right after parsing, before anything is annotated.
|
|
72
|
+
|
|
73
|
+
### Configuration and extension
|
|
74
|
+
|
|
75
|
+
- **`strict_parity`** (default `true`). `true` reproduces MetaMapLite 3.6.2rc8 exactly, bugs
|
|
76
|
+
included. `false` fixes the upstream bugs that give wrong answers, all in ConText:
|
|
77
|
+
- Only the mentions inside the sentence being read are analysed, each within its own bullet
|
|
78
|
+
item. Java applies every sentence to every mention in the document, so a later "Family
|
|
79
|
+
history of hypertension in mother" turned the patient's own earlier "hypertension" into
|
|
80
|
+
`Historical` / `Other`. This also makes ConText close to linear in document length rather
|
|
81
|
+
than quadratic.
|
|
82
|
+
- The longest trigger at a position wins, instead of whichever bucket is substituted first.
|
|
83
|
+
"Pneumonia was ruled out." is negated by the *post* trigger "was ruled out"; Java tagged it
|
|
84
|
+
with the shorter *pre* trigger "ruled out", which lands at the end of the sentence with
|
|
85
|
+
nothing after it to negate. "may be ruled out for X" is `Possible`, not a negation.
|
|
86
|
+
- Triggers stored with a trailing space (`denies `, `no `) fire anywhere, not only right before
|
|
87
|
+
the concept, so a whole "Denies A, B, C" list is negated rather than its first item — and
|
|
88
|
+
"no evidence of X" / "without evidence of X" negate at all.
|
|
89
|
+
- Time patterns match ("3 months of", "since last march"); they were character classes where
|
|
90
|
+
groups were meant. "previous" marks the next word historical. The first pseudo-trigger keeps
|
|
91
|
+
its first letter. "Rule out X" / "r/o X" gives `Possible`.
|
|
92
|
+
- Experiencer and temporality triggers inside a concept's own name count; negation triggers
|
|
93
|
+
there deliberately do not ("No known allergies" is itself the finding).
|
|
94
|
+
- `mmi`, `json` and `brat` list semantic types and sources alphabetically and documents in
|
|
95
|
+
input order, instead of Java `HashSet` order.
|
|
96
|
+
|
|
97
|
+
NegEx output is identical in both modes. `GET /health` reports `strict_parity`, and a server
|
|
98
|
+
running strict-parity ConText logs a warning once.
|
|
99
|
+
- **Extra ConText triggers** (`context_triggers.EXTRA_TRIGGERS`, fixed mode only). `cannot` is a
|
|
100
|
+
negation trigger on its own, so "cannot exclude pneumonia" came out *negated* — the opposite of
|
|
101
|
+
what it says. Adds the exclusion family (`cannot exclude`, `cannot be excluded`,
|
|
102
|
+
`cannot rule out`, `unable to exclude`, …), the hedges `concerning for`, `suspicious for`,
|
|
103
|
+
`suspected`, `differential includes`, `low suspicion for`, `possible`, `probable`, and
|
|
104
|
+
`absent` / `is absent` / `are absent`. `ConText(extra_triggers=...)` takes your own list.
|
|
105
|
+
- **`Entity.assertion`** carries ConText's verdict — `Affirmed`, `Negated` or `Possible`. Java
|
|
106
|
+
keeps only a boolean and discards the verdict, so a hedged mention read exactly like an
|
|
107
|
+
affirmed one. Shown as `[possible]` in `full` output and returned by the REST API; `negated` is
|
|
108
|
+
unchanged, so `mmi`, `json` and `brat` are untouched. `None` under NegEx.
|
|
109
|
+
- **Pluggable assertion.** `AssertionClassifier` adapts any per-sentence assertion model
|
|
110
|
+
(medspaCy's ConText, a transformer classifier, site rules) to the pipeline.
|
|
111
|
+
`register_negation_detector(name, factory, aliases)` makes it selectable wherever `negex` /
|
|
112
|
+
`context` are, and packages can declare one under the `pymetamaplite.negation_detectors`
|
|
113
|
+
entry-point group. `--negation-detector NAME` on `annotate` and `serve`.
|
|
114
|
+
- **Selectable POS tagger.** `Settings.postag_model` names the spaCy pipeline whose tagger gates
|
|
115
|
+
candidate spans; the default is `en_core_web_sm`. scispaCy's `en_core_sci_sm` agrees with Java
|
|
116
|
+
better (F1 0.977 vs 0.969) and is opt-in because its models need spaCy 3.7 / Python ≤ 3.12 —
|
|
117
|
+
`requirements-scispacy-py312.txt` pins a tested environment for it.
|
|
118
|
+
- **POS verb rescue** (`Settings.postag_verb_rescue`, default `0.03`) re-tags a token the model
|
|
119
|
+
calls a finite verb with its most likely noun or adjective reading. `en_core_web_sm` calls rare
|
|
120
|
+
clinical words verbs far more readily than OpenNLP, and the POS gate then never looks them up.
|
|
121
|
+
Raises parity F1 from 0.969 to 0.974 while *reducing* extras.
|
|
122
|
+
|
|
123
|
+
### Performance
|
|
124
|
+
|
|
125
|
+
- Annotation runs at ~500 sentences/s on one core; resident memory ~120 MB. An index build over
|
|
126
|
+
UMLS 2026AA (all English sources) takes ~2.5 minutes and produces a 2.2 GB SQLite database:
|
|
127
|
+
9.1 M terms, 7.3 M distinct keys, 3.5 M CUIs.
|
|
128
|
+
- Abbreviation propagation tokenizes each passage once rather than once per abbreviation-bearing
|
|
129
|
+
entity (48 KB: 7.0 s → 2.1 s, byte-identical output), and NegEx receives only the entities
|
|
130
|
+
starting inside each sentence.
|
|
131
|
+
- The four per-concept caches are LRU-capped at 50,000 entries each (~125 MB together) instead of
|
|
132
|
+
growing for the life of the process.
|
|
133
|
+
- `--preload` / `PYMETAMAPLITE_PRELOAD_INDEX` is off by default and documented as rarely worth
|
|
134
|
+
turning on: measured A/B with the index on local disk it is no faster, and costs ~4 s of
|
|
135
|
+
start-up and ~1.65 GB RSS per process.
|
|
136
|
+
|
|
137
|
+
### Not implemented, deliberately
|
|
138
|
+
|
|
139
|
+
- **MetaMap-style scoring** (`EntityLookup5`, Java's `--enable_scoring`), off by default in Java.
|
|
140
|
+
Measured rather than assumed: with NLM's own index the score takes one of two values, carrying
|
|
141
|
+
~0.04 bits per evidence. See the development plan, §10.1.
|
|
142
|
+
- **Word sense disambiguation, derivational variants, disjoint entities** — absent from
|
|
143
|
+
MetaMapLite itself.
|
|
144
|
+
|
|
145
|
+
### Examples
|
|
146
|
+
|
|
147
|
+
Runnable scripts in [examples/](examples/) — a starting point to adapt, not a library. See
|
|
148
|
+
[examples/README.md](examples/README.md).
|
|
149
|
+
|
|
150
|
+
- **Annotation** — `analyze_file.py`, `analyze_folder.py` (one loaded index reused across a
|
|
151
|
+
folder, `--workers` for a process pool) and `json_to_sqlite.py`, which loads `--outputformat
|
|
152
|
+
json` files into a normalized `entities`/`concepts`/`evidence` database.
|
|
153
|
+
- **Corpus exports** — `parse_to_csv.py`, `parse_to_jsonl.py`, `parse_to_jsonl_batch.py`,
|
|
154
|
+
`parse_to_parquet.py` and `parse_to_sqlite.py` write one row per (span, concept) in five
|
|
155
|
+
formats from a single row shape (`_annotations.py`), so exports of one corpus agree and join
|
|
156
|
+
on the same key. `compare_exports.py` checks that rather than asserting it, and exits non-zero
|
|
157
|
+
on disagreement. `load_to_sqlite.py` completes the two-step route, sharing its schema with
|
|
158
|
+
`parse_to_sqlite.py` so both end at the same database.
|
|
159
|
+
- An attribute the run never assessed gets **no column**, and where one is kept it is
|
|
160
|
+
empty/NULL rather than 0: NegEx assesses `negated` only, `--usecontext` adds `assertion`,
|
|
161
|
+
`temporality` and `experiencer`, `--no-negation` assesses none. NULL means "not assessed"
|
|
162
|
+
and 0 means "assessed, and absent".
|
|
163
|
+
- Each run saves itself into a fresh `out/runs/<run-id>/` directory beside a `manifest.json`
|
|
164
|
+
recording the settings that produced it (`_runs.py`), so no run overwrites another.
|
|
165
|
+
- **Phenotyping** — `notes_of_interest.py` finds the notes that *affirm* a concept, counting a
|
|
166
|
+
document only when the mention is not negated, and matching by CUI so synonym and
|
|
167
|
+
morphological variants collapse.
|
|
168
|
+
- **Terminology crosswalks** — `filter_mrconso.py` / `filter_mrrel.py` and the `add_*_codes.py`
|
|
169
|
+
and `add_*_hierarchy.py` scripts map matched concepts to SNOMED CT, ICD-10-CM, RxNorm and
|
|
170
|
+
LOINC, with `create_combined_codes_view.py` over the result.
|
|
171
|
+
|
|
172
|
+
### Documentation
|
|
173
|
+
|
|
174
|
+
- [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md) — install to first results.
|
|
175
|
+
- [docs/UMLS identifiers.md](<docs/UMLS identifiers.md>) — CUI, LUI, SUI, AUI and TUI explained.
|
|
176
|
+
|
|
177
|
+
### Licensing
|
|
178
|
+
|
|
179
|
+
Released under the same licence as MetaMapLite — NLM's open-source BSD licence — with the
|
|
180
|
+
upstream Informational Notice retained in [NOTICE-MetaMapLite.md](NOTICE-MetaMapLite.md).
|
|
181
|
+
pymetamaplite is an independent port and is **not** developed, funded or endorsed by NLM, NIH or
|
|
182
|
+
the U.S. Government. The UMLS data it indexes is licensed separately.
|
|
183
|
+
|
|
184
|
+
### Quality
|
|
185
|
+
|
|
186
|
+
706 tests, `ruff` clean, `mypy --strict` clean over `src/pymetamaplite`, CI on Python 3.11–3.13.
|
|
187
|
+
Every `examples/` script has its own test file under `tests/examples/`, run against small
|
|
188
|
+
synthetic MRCONSO/MRSTY fixtures and an index built from them, so the suite needs no UMLS
|
|
189
|
+
download. The parity tests skip wherever licensed fixtures are absent.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Terms and Conditions for Use of pymetamaplite
|
|
2
|
+
|
|
3
|
+
pymetamaplite is a Python port of MetaMapLite. It is released under the same terms as
|
|
4
|
+
MetaMapLite itself: the open-source BSD license published by the U.S. National Library of
|
|
5
|
+
Medicine, reproduced in full below.
|
|
6
|
+
|
|
7
|
+
Scope, so the provenance is unambiguous:
|
|
8
|
+
|
|
9
|
+
* MetaMapLite was developed and funded by the National Library of Medicine. The Informational
|
|
10
|
+
Notice and LICENSE below are NLM's, and are reproduced verbatim from the MetaMapLite
|
|
11
|
+
distribution (see NOTICE-MetaMapLite.md). They apply to MetaMapLite and to the portions of
|
|
12
|
+
this software derived from it — algorithms, data tables, field layouts and output formats.
|
|
13
|
+
* pymetamaplite itself was not developed, funded, endorsed or reviewed by the National Library
|
|
14
|
+
of Medicine, the National Institutes of Health, or the U.S. Government. Copyright in the
|
|
15
|
+
Python code written for this port is held by its contributors, who release it under these
|
|
16
|
+
same terms.
|
|
17
|
+
* The UMLS Metathesaurus data this software indexes is licensed separately by NLM and is not
|
|
18
|
+
covered by this license. See the note in the Informational Notice below.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Informational Notice:
|
|
23
|
+
|
|
24
|
+
This software, “MetaMapLite” was developed and funded by the National
|
|
25
|
+
Library of Medicine, part of the National Institutes of Health, and
|
|
26
|
+
agency of the United States Department of Health and Human Services,
|
|
27
|
+
which is making the software available to the public for any
|
|
28
|
+
commercial or non-commercial purpose under the following open-source
|
|
29
|
+
BSD license.
|
|
30
|
+
|
|
31
|
+
NOTE: Users of the data distributed with MetaMapLite are
|
|
32
|
+
responsible for compliance with the UMLS Metathesaurus License
|
|
33
|
+
Agreement which requires you to respect the copyrights of the
|
|
34
|
+
constituent vocabularies and to file a brief annual report on your use
|
|
35
|
+
of the UMLS. You also must have activated a UMLS Terminology Services
|
|
36
|
+
(UTS) account.
|
|
37
|
+
|
|
38
|
+
# LICENSE:
|
|
39
|
+
|
|
40
|
+
Government Usage Rights Notice: The U.S. Government retains unlimited,
|
|
41
|
+
royalty-free usage rights to this software, but not ownership, as
|
|
42
|
+
provided by Federal law.
|
|
43
|
+
|
|
44
|
+
Redistribution and use in source and binary forms, with or without
|
|
45
|
+
modification, are permitted provided that the following conditions are
|
|
46
|
+
met:
|
|
47
|
+
|
|
48
|
+
* Redistributions of source code must retain this Informational Notice.
|
|
49
|
+
* Redistributions in binary form must reproduce this Informational
|
|
50
|
+
Notice, this list of conditions and the following disclaimer in the
|
|
51
|
+
documentation and/or other materials provided with the distribution.
|
|
52
|
+
* Neither the names of the National Library of Medicine, the National
|
|
53
|
+
Institutes of Health, nor the names of any of the software
|
|
54
|
+
developers may be used to endorse or promote products derived from
|
|
55
|
+
this software without specific prior written permission. The
|
|
56
|
+
U.S. Government retains an unlimited, royalty-free right to use,
|
|
57
|
+
distribute or modify the software.
|
|
58
|
+
* Please acknowledge NLM as the source of the MetaMapLite software by
|
|
59
|
+
including the phrase “Courtesy of the U.S. National Library of
|
|
60
|
+
Medicine” or “Source: U.S. National Library of Medicine.”
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
THIS SOFTWARE IS PROVIDED BY THE U.S. GOVERNMENT AND CONTRIBUTORS "AS
|
|
64
|
+
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
65
|
+
LIMITEDTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
66
|
+
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
67
|
+
U.S. GOVERNMENT
|
|
68
|
+
|
|
69
|
+
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
70
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
71
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
72
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
73
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
74
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
75
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Terms and Conditions for Use of MetaMapLite
|
|
2
|
+
|
|
3
|
+
# Informational Notice:
|
|
4
|
+
|
|
5
|
+
This software, “MetaMapLite” was developed and funded by the National
|
|
6
|
+
Library of Medicine, part of the National Institutes of Health, and
|
|
7
|
+
agency of the United States Department of Health and Human Services,
|
|
8
|
+
which is making the software available to the public for any
|
|
9
|
+
commercial or non-commercial purpose under the following open-source
|
|
10
|
+
BSD license.
|
|
11
|
+
|
|
12
|
+
NOTE: Users of the data distributed with MetaMapLite are
|
|
13
|
+
responsible for compliance with the UMLS Metathesaurus License
|
|
14
|
+
Agreement which requires you to respect the copyrights of the
|
|
15
|
+
constituent vocabularies and to file a brief annual report on your use
|
|
16
|
+
of the UMLS. You also must have activated a UMLS Terminology Services
|
|
17
|
+
(UTS) account.
|
|
18
|
+
|
|
19
|
+
# LICENSE:
|
|
20
|
+
|
|
21
|
+
Government Usage Rights Notice: The U.S. Government retains unlimited,
|
|
22
|
+
royalty-free usage rights to this software, but not ownership, as
|
|
23
|
+
provided by Federal law.
|
|
24
|
+
|
|
25
|
+
Redistribution and use in source and binary forms, with or without
|
|
26
|
+
modification, are permitted provided that the following conditions are
|
|
27
|
+
met:
|
|
28
|
+
|
|
29
|
+
* Redistributions of source code must retain this Informational Notice.
|
|
30
|
+
* Redistributions in binary form must reproduce this Informational
|
|
31
|
+
Notice, this list of conditions and the following disclaimer in the
|
|
32
|
+
documentation and/or other materials provided with the distribution.
|
|
33
|
+
* Neither the names of the National Library of Medicine, the National
|
|
34
|
+
Institutes of Health, nor the names of any of the software
|
|
35
|
+
developers may be used to endorse or promote products derived from
|
|
36
|
+
this software without specific prior written permission. The
|
|
37
|
+
U.S. Government retains an unlimited, royalty-free right to use,
|
|
38
|
+
distribute or modify the software.
|
|
39
|
+
* Please acknowledge NLM as the source of the MetaMapLite software by
|
|
40
|
+
including the phrase “Courtesy of the U.S. National Library of
|
|
41
|
+
Medicine” or “Source: U.S. National Library of Medicine.”
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
THIS SOFTWARE IS PROVIDED BY THE U.S. GOVERNMENT AND CONTRIBUTORS "AS
|
|
45
|
+
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
46
|
+
LIMITEDTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
47
|
+
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
48
|
+
U.S. GOVERNMENT
|
|
49
|
+
|
|
50
|
+
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
51
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
52
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
53
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
54
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
55
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
56
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pymetamaplite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python port of NLM MetaMapLite: near-real-time UMLS concept recognition
|
|
5
|
+
Project-URL: MetaMapLite (upstream), https://lhncbc.nlm.nih.gov/LHC-research/LHC-projects/NLP/MetaMapLite.html
|
|
6
|
+
Project-URL: MetaMapLite source (upstream), https://github.com/lhncbc/metamaplite
|
|
7
|
+
Project-URL: UMLS licensing, https://uts.nlm.nih.gov/uts/
|
|
8
|
+
License-Expression: LicenseRef-MetaMapLite-BSD
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: NOTICE-MetaMapLite.md
|
|
11
|
+
Keywords: biomedical,concept-recognition,metamap,metamaplite,ner,nlp,umls
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
23
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
24
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: typer>=0.12
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: fastapi>=0.110; extra == 'all'
|
|
30
|
+
Requires-Dist: spacy>=3.7; extra == 'all'
|
|
31
|
+
Requires-Dist: uvicorn>=0.29; extra == 'all'
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
34
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
35
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
37
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
38
|
+
Provides-Extra: nlp
|
|
39
|
+
Requires-Dist: spacy>=3.7; extra == 'nlp'
|
|
40
|
+
Provides-Extra: server
|
|
41
|
+
Requires-Dist: fastapi>=0.110; extra == 'server'
|
|
42
|
+
Requires-Dist: uvicorn>=0.29; extra == 'server'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# pymetamaplite
|
|
46
|
+
|
|
47
|
+
A Python port of [MetaMapLite](https://lhncbc.nlm.nih.gov/LHC-research/LHC-projects/NLP/MetaMapLite.html),
|
|
48
|
+
the U.S. National Library of Medicine's near-real-time UMLS concept recognizer — as a library,
|
|
49
|
+
a CLI, and a REST API, with no JVM.
|
|
50
|
+
|
|
51
|
+
*Courtesy of the U.S. National Library of Medicine.*
|
|
52
|
+
|
|
53
|
+
This README is a quick overview. **New here? Start with
|
|
54
|
+
[docs/GETTING_STARTED.md](docs/GETTING_STARTED.md)** — install, build the index and get your
|
|
55
|
+
first results in under an hour. New to UMLS identifiers (CUI, LUI, SUI, AUI, TUI)? See
|
|
56
|
+
[docs/UMLS identifiers.md](<docs/UMLS identifiers.md>). Release notes are in
|
|
57
|
+
[CHANGELOG.md](CHANGELOG.md). Every CLI flag has `--help`, and the REST API documents itself at
|
|
58
|
+
`/docs`.
|
|
59
|
+
|
|
60
|
+
## Verified against the original
|
|
61
|
+
|
|
62
|
+
Measured against **Java MetaMapLite 3.6.2rc8** on UMLS 2026AA (see
|
|
63
|
+
[tests/parity/README.md](tests/parity/README.md) for the method and how to reproduce):
|
|
64
|
+
|
|
65
|
+
| | |
|
|
66
|
+
|---|---|
|
|
67
|
+
| Index tables (`cuisourceinfo`, `cuiconcept`, `meshtcrelaxed`) | **identical, row for row** |
|
|
68
|
+
| Tokenization | **exact**, token for token |
|
|
69
|
+
| Concepts found, `(CUI, positions)` | **P 0.975 / R 0.963 / F1 0.969** |
|
|
70
|
+
| MMI records byte-identical | 430/565 |
|
|
71
|
+
| `json` output, spans found by both | 285/296 agree on every field Java emits |
|
|
72
|
+
|
|
73
|
+
Index contents, normalization, candidate generation, restriction, subsumption, negation,
|
|
74
|
+
abbreviations, MMI scoring and field layout are reproduced deterministically. Most of the
|
|
75
|
+
residual gap is the two NLP models MetaMapLite uses — OpenNLP's sentence detector (F1 0.923 vs
|
|
76
|
+
ours) and its POS tagger (0.850 tag agreement, 0.960 on the decision that affects results) —
|
|
77
|
+
which have no Python equivalent. One known difference is deliberate: Java's index lookup is
|
|
78
|
+
**unreliable for dictionary keys containing non-ASCII characters** — its binary search compares
|
|
79
|
+
UTF-8 as signed bytes against a file sorted in `String` order, so it finds some and misses others
|
|
80
|
+
depending on where the key falls. Measured over a 600-key sample it resolves about 44% of them,
|
|
81
|
+
missing terms such as `Sjögren` and `Ménière disease` that this port finds. That accounts for five
|
|
82
|
+
of the 35 concept-level disagreements, each pinned by name in the test suite rather than averaged
|
|
83
|
+
into the figures above. See the development plan, §16 and §21.
|
|
84
|
+
|
|
85
|
+
## Install
|
|
86
|
+
|
|
87
|
+
```powershell
|
|
88
|
+
uv venv
|
|
89
|
+
uv pip install --python .venv -e ".[dev,nlp,server]"
|
|
90
|
+
uv pip install --python .venv en_core_web_sm@https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Extras: `nlp` (spaCy — POS tagging and spaCy sentence detection), `server` (FastAPI + uvicorn),
|
|
94
|
+
`all` for both. The core package needs only `typer`; without `nlp`, pass `--no-postag`.
|
|
95
|
+
|
|
96
|
+
The POS tagger is a spaCy model chosen by name (`--postag-model`, `PYMETAMAPLITE_POSTAG_MODEL`).
|
|
97
|
+
On Python ≤ 3.12, scispaCy's `en_core_sci_sm` measures **closer to Java than the default**
|
|
98
|
+
(F1 0.977 vs 0.969) and finds 27/30 drugs in a medication list where `en_core_web_sm` finds 20 —
|
|
99
|
+
see `--postag-model` in `pymetamaplite annotate --help`.
|
|
100
|
+
|
|
101
|
+
## Build an index
|
|
102
|
+
|
|
103
|
+
You supply the UMLS — a licensed download, never redistributed with this package. `MRCONSO.RRF`
|
|
104
|
+
and `MRSTY.RRF` are required, `MRSAT.RRF` adds MeSH tree codes (used by MMI scoring):
|
|
105
|
+
|
|
106
|
+
```powershell
|
|
107
|
+
pymetamaplite -v build-index --mrconso ...\MRCONSO.RRF --mrsty ...\MRSTY.RRF --mrsat ...\MRSAT.RRF --out ivf
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
UMLS 2026AA (all English sources) takes ~2.5 min and produces a 2.2 GB SQLite database:
|
|
111
|
+
9.1 M terms / 7.3 M distinct keys / 3.5 M CUIs. `--sources MSH,SNOMEDCT_US` restricts it;
|
|
112
|
+
`--include-suppressed` keeps `SUPPRESS=O/E/Y` rows.
|
|
113
|
+
|
|
114
|
+
## Command line
|
|
115
|
+
|
|
116
|
+
```powershell
|
|
117
|
+
pymetamaplite annotate "No history of myocardial infarction." --restrict-to-sts dsyn,sosy
|
|
118
|
+
pymetamaplite annotate --input note.txt --outputformat json # mmi (default) | json | brat | cuilist | full | bc
|
|
119
|
+
pymetamaplite annotate --pipe --no-negation < note.txt
|
|
120
|
+
pymetamaplite lookup "type 2 diabetes mellitus" # dictionary spot-check
|
|
121
|
+
pymetamaplite normalize "Alzheimer's Disease" # show the lookup keys for a span
|
|
122
|
+
pymetamaplite tokenize "Dr. Smith has type 2 diabetes." # sentences + tokens + POS
|
|
123
|
+
pymetamaplite index-stats
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`annotate` mirrors `metamaplite.sh`: `--restrict-to-sts`, `--restrict-to-sources`,
|
|
127
|
+
`--segmentation SENTENCES|BLANKLINES|LINES`, `--inputformat` (`freetext`, `sli`, `sldi`,
|
|
128
|
+
`sldiwi`, `chemdner`, `chemdnersldi`, `ncbicorpus`, `pubtator`, `pubmed`, `medline`, `bioc`),
|
|
129
|
+
`--excluded-terms`, `--uda`,
|
|
130
|
+
`--cui-term-list`, `--usecontext`, `--keep-subsumed`, `--preload`. Restrictions take semantic type abbreviations or TUIs and UMLS
|
|
131
|
+
SABs, in any case (`dsyn`, `DSYN`, `T047` and `t047` all select the same concepts).
|
|
132
|
+
|
|
133
|
+
## Python API
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from pymetamaplite import MetaMapLite
|
|
137
|
+
|
|
138
|
+
mml = MetaMapLite(index_directory="ivf")
|
|
139
|
+
entities = mml.process_text(
|
|
140
|
+
"No history of type 2 diabetes mellitus (T2DM). T2DM is treated with metformin.",
|
|
141
|
+
restrict_to_sts={"dsyn", "phsu"},
|
|
142
|
+
)
|
|
143
|
+
for e in entities:
|
|
144
|
+
print(e.start, e.length, e.text, e.negated, [(ev.cui, ev.concept.preferred_name) for ev in e.evs])
|
|
145
|
+
print(mml.format(entities, "mmi"))
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
14 24 type 2 diabetes mellitus True [('C0011860', 'Diabetes Mellitus, Non-Insulin-Dependent')]
|
|
150
|
+
40 4 T2DM False [('C0011860', 'Diabetes Mellitus, Non-Insulin-Dependent')]
|
|
151
|
+
47 4 T2DM False [('C0011860', 'Diabetes Mellitus, Non-Insulin-Dependent')]
|
|
152
|
+
68 9 metformin False [('C0025598', 'metformin')]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`mml.process_file("note.txt")` annotates a file directly — offsets index the file verbatim
|
|
156
|
+
(line endings are never translated), so `brat` output lines up with a CRLF `.txt`.
|
|
157
|
+
|
|
158
|
+
`MetaMapLite(...)` takes a `Settings` object or `properties_file=` accepting MetaMapLite's own
|
|
159
|
+
`metamaplite.properties` keys (`metamaplite.index.directory`, `metamaplite.semanticgroup`,
|
|
160
|
+
`metamaplite.enable.postagging`, `metamaplite.detect.negations`, `metamaplite.postaglist`,
|
|
161
|
+
`metamaplite.uda.filename`, …); every key also reads from `PYMETAMAPLITE_<NAME>` env vars.
|
|
162
|
+
|
|
163
|
+
## REST API
|
|
164
|
+
|
|
165
|
+
```powershell
|
|
166
|
+
pymetamaplite serve --indexdir ivf --port 8000 # OpenAPI docs at /docs
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
| Endpoint | Purpose |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `POST /annotate` | one document → structured JSON entities |
|
|
172
|
+
| `POST /annotate/batch` | several documents in one request |
|
|
173
|
+
| `POST /annotate/text?format=mmi` | MetaMapLite's own output formats as `text/plain` |
|
|
174
|
+
| `POST /annotate/formatted` | a blob in any `--inputformat` (ChemDNER, PubMed XML, BioC, …) → one result per document |
|
|
175
|
+
| `GET /lookup?term=…` | dictionary lookup for a single term |
|
|
176
|
+
| `GET /formats` | every accepted input/output format, segmentation method and negation detector |
|
|
177
|
+
| `GET /semantic-types` | the UMLS semantic types, for building `restrict_to_sts` |
|
|
178
|
+
| `GET /health` | status, index metadata, uptime |
|
|
179
|
+
|
|
180
|
+
Any annotate request may pass `"detector": "context"` to use ConText for that request instead of
|
|
181
|
+
NegEx, which adds `temporality` and `experiencer` to each entity.
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
curl -X POST http://127.0.0.1:8000/annotate -H "Content-Type: application/json" \
|
|
185
|
+
-d '{"text":"No chest pain.","restrict_to_sts":["sosy"]}'
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The index and spaCy model load once at start-up (~10 s; `--preload` adds a 4 s term-map load and
|
|
189
|
+
~1.6 GB of RAM, and measured no faster, so leave it off). Requests take ~10–20 ms for a short clinical note. Annotation is
|
|
190
|
+
CPU-bound and the underlying objects are not thread-safe, so requests are serialised behind a
|
|
191
|
+
lock — **scale out with processes** (`uvicorn pymetamaplite.server:app --workers N`, each holding
|
|
192
|
+
its own copy of the index), not threads.
|
|
193
|
+
|
|
194
|
+
## Performance
|
|
195
|
+
|
|
196
|
+
UMLS 2026AA, all English sources, on a laptop: index build 2.5 min; annotation ~500 sentences/s
|
|
197
|
+
(the absolute figure varies a lot with machine state);
|
|
198
|
+
resident memory ~120 MB, or ~1.8 GB with `preload_index=True` (a 419 MB pickled term map, 4 s to
|
|
199
|
+
load). Preloading measured no faster than the default SQLite lookups with the index on local disk,
|
|
200
|
+
so it is off by default and rarely worth turning on.
|
|
201
|
+
|
|
202
|
+
## What is and isn't implemented
|
|
203
|
+
|
|
204
|
+
Implemented: the whole `EntityLookup4` path — index building, normalization, segmentation,
|
|
205
|
+
tokenization, POS gating, longest-match candidate generation, semantic-type/source restriction,
|
|
206
|
+
subsumption removal, NegEx negation, Schwartz–Hearst abbreviations, user-defined acronyms,
|
|
207
|
+
custom concept lists, excluded terms, both negation detectors (NegEx by default, ConText via
|
|
208
|
+
`--usecontext`, which adds temporality and experiencer), every `--inputformat` MetaMapLite registers (`freetext`,
|
|
209
|
+
`sli`/`sldi`, `sldiwi`, `chemdner`, `chemdnersldi`, `ncbicorpus`, `pubtator`, `pubmed`, `medline`,
|
|
210
|
+
`bioc` — `pubtator` as a correction, since Java's own loader throws on every input), and the
|
|
211
|
+
MMI / JSON / BRAT / CuiList output formats.
|
|
212
|
+
|
|
213
|
+
Not implemented, deliberately:
|
|
214
|
+
|
|
215
|
+
- **MetaMap-style scoring** (`EntityLookup5`, Java's `--enable_scoring`) — off by default in Java,
|
|
216
|
+
and measured rather than assumed: with NLM's own index the `vars` table is a single placeholder
|
|
217
|
+
row, so the variation term is identically zero and the coverage and cohesiveness terms reduce to
|
|
218
|
+
1.0 by construction. The score takes one of two values — 666.67, or 833.33 in the rare case that
|
|
219
|
+
a defective head test fires (749 / 4 over the parity corpus). `EntityLookup5`'s substantive
|
|
220
|
+
difference from `EntityLookup4` is its candidate generation, not its scores. See
|
|
221
|
+
the development plan, §10.1, for the closed form and how to reproduce it.
|
|
222
|
+
- **Word sense disambiguation, derivational variants, disjoint entities** — absent from
|
|
223
|
+
MetaMapLite itself.
|
|
224
|
+
|
|
225
|
+
## Development
|
|
226
|
+
|
|
227
|
+
```powershell
|
|
228
|
+
.venv\Scripts\python -m pytest -q # 706 tests; the parity tests skip without Java fixtures
|
|
229
|
+
.venv\Scripts\ruff check src tests scripts examples
|
|
230
|
+
.venv\Scripts\ruff format --check src tests scripts examples
|
|
231
|
+
.venv\Scripts\mypy # strict, over src/pymetamaplite (see pyproject.toml)
|
|
232
|
+
python scripts/compare_with_java.py --java-dir <public_mm_lite> # parity vs the real thing
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`ruff format` is the formatter; lint rules and the 100-character line length are configured in
|
|
236
|
+
`pyproject.toml`. [CI](.github/workflows/ci.yml) runs the same lint, `mypy --strict` and the tests
|
|
237
|
+
on Python 3.11–3.13 on every push; the parity tests skip there, since their fixtures are
|
|
238
|
+
licensed. Anything that changes annotation output must keep the parity harness green — see
|
|
239
|
+
[tests/parity/README.md](tests/parity/README.md).
|
|
240
|
+
|
|
241
|
+
## Releasing
|
|
242
|
+
|
|
243
|
+
[`release.yml`](.github/workflows/release.yml) publishes via PyPI Trusted Publishing, so no API
|
|
244
|
+
token is stored anywhere — PyPI verifies that this workflow, in this repository, produced the
|
|
245
|
+
upload. Running the workflow by hand publishes to **TestPyPI**; pushing a `v*` tag publishes to
|
|
246
|
+
**PyPI**. Both build, run `twine check`, and install the wheel into a clean environment before
|
|
247
|
+
uploading, and a tagged run additionally refuses to publish if the tag disagrees with
|
|
248
|
+
`__version__`. A version number, once on PyPI, can never be reused — so bump
|
|
249
|
+
`src/pymetamaplite/__init__.py` and tag it, rather than re-cutting one.
|
|
250
|
+
|
|
251
|
+
The project's development plan, an internal working document, records what each Java class does
|
|
252
|
+
and why each behaviour was reproduced — including deliberate quirks (a span must start and end with a
|
|
253
|
+
character Java's `CharUtils` calls alphanumeric, which is ASCII *and Greek*, so `β-blocker`
|
|
254
|
+
matches but `-blocker` cannot; sources and semantic types print in Java `HashSet` order).
|
|
255
|
+
|
|
256
|
+
## Licensing
|
|
257
|
+
|
|
258
|
+
This package is released under the **same licence as MetaMapLite** — NLM's open-source BSD
|
|
259
|
+
licence — reproduced in [LICENSE](LICENSE), with the upstream notice retained verbatim in
|
|
260
|
+
[NOTICE-MetaMapLite.md](NOTICE-MetaMapLite.md). That licence requires you to keep the
|
|
261
|
+
Informational Notice in redistributions and to acknowledge NLM as the source; hence the
|
|
262
|
+
"Courtesy of the U.S. National Library of Medicine" line at the top of this README.
|
|
263
|
+
|
|
264
|
+
pymetamaplite is an independent port and is **not** developed, funded or endorsed by NLM, NIH or
|
|
265
|
+
the U.S. Government. The UMLS data it indexes is licensed separately: you need a UMLS
|
|
266
|
+
Metathesaurus License and a UTS account, and the index you build from it is yours alone to keep —
|
|
267
|
+
don't redistribute it.
|