333 lines
8.3 KiB
Python
333 lines
8.3 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import csv
|
||
|
|
import re
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
LABEL_OFFSET = 3
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True)
|
||
|
|
class ChapterRule:
|
||
|
|
chapter: str
|
||
|
|
start: str
|
||
|
|
end: str
|
||
|
|
title: str
|
||
|
|
organ_system: str
|
||
|
|
organ_system_label: str
|
||
|
|
|
||
|
|
|
||
|
|
CHAPTER_RULES = [
|
||
|
|
ChapterRule(
|
||
|
|
"I",
|
||
|
|
"A00",
|
||
|
|
"B99",
|
||
|
|
"Certain infectious and parasitic diseases",
|
||
|
|
"infectious_systemic",
|
||
|
|
"Infectious / systemic",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"II",
|
||
|
|
"C00",
|
||
|
|
"D48",
|
||
|
|
"Neoplasms",
|
||
|
|
"neoplasm",
|
||
|
|
"Neoplasm / oncology",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"III",
|
||
|
|
"D50",
|
||
|
|
"D89",
|
||
|
|
"Diseases of the blood and blood-forming organs and certain immune disorders",
|
||
|
|
"hematologic_immune",
|
||
|
|
"Blood / immune",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"IV",
|
||
|
|
"E00",
|
||
|
|
"E90",
|
||
|
|
"Endocrine, nutritional and metabolic diseases",
|
||
|
|
"endocrine_metabolic",
|
||
|
|
"Endocrine / metabolic",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"V",
|
||
|
|
"F00",
|
||
|
|
"F99",
|
||
|
|
"Mental and behavioural disorders",
|
||
|
|
"mental_behavioral",
|
||
|
|
"Mental / behavioral",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"VI",
|
||
|
|
"G00",
|
||
|
|
"G99",
|
||
|
|
"Diseases of the nervous system",
|
||
|
|
"nervous_system",
|
||
|
|
"Nervous system",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"VII",
|
||
|
|
"H00",
|
||
|
|
"H59",
|
||
|
|
"Diseases of the eye and adnexa",
|
||
|
|
"eye",
|
||
|
|
"Eye",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"VIII",
|
||
|
|
"H60",
|
||
|
|
"H95",
|
||
|
|
"Diseases of the ear and mastoid process",
|
||
|
|
"ear",
|
||
|
|
"Ear",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"IX",
|
||
|
|
"I00",
|
||
|
|
"I99",
|
||
|
|
"Diseases of the circulatory system",
|
||
|
|
"circulatory",
|
||
|
|
"Circulatory system",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"X",
|
||
|
|
"J00",
|
||
|
|
"J99",
|
||
|
|
"Diseases of the respiratory system",
|
||
|
|
"respiratory",
|
||
|
|
"Respiratory system",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"XI",
|
||
|
|
"K00",
|
||
|
|
"K93",
|
||
|
|
"Diseases of the digestive system",
|
||
|
|
"digestive",
|
||
|
|
"Digestive system",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"XII",
|
||
|
|
"L00",
|
||
|
|
"L99",
|
||
|
|
"Diseases of the skin and subcutaneous tissue",
|
||
|
|
"skin",
|
||
|
|
"Skin / subcutaneous tissue",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"XIII",
|
||
|
|
"M00",
|
||
|
|
"M99",
|
||
|
|
"Diseases of the musculoskeletal system and connective tissue",
|
||
|
|
"musculoskeletal",
|
||
|
|
"Musculoskeletal / connective tissue",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"XIV",
|
||
|
|
"N00",
|
||
|
|
"N99",
|
||
|
|
"Diseases of the genitourinary system",
|
||
|
|
"genitourinary",
|
||
|
|
"Genitourinary system",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"XV",
|
||
|
|
"O00",
|
||
|
|
"O99",
|
||
|
|
"Pregnancy, childbirth and the puerperium",
|
||
|
|
"pregnancy_childbirth",
|
||
|
|
"Pregnancy / childbirth",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"XVI",
|
||
|
|
"P00",
|
||
|
|
"P96",
|
||
|
|
"Certain conditions originating in the perinatal period",
|
||
|
|
"perinatal",
|
||
|
|
"Perinatal conditions",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"XVII",
|
||
|
|
"Q00",
|
||
|
|
"Q99",
|
||
|
|
"Congenital malformations, deformations and chromosomal abnormalities",
|
||
|
|
"congenital_chromosomal",
|
||
|
|
"Congenital / chromosomal",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"XVIII",
|
||
|
|
"R00",
|
||
|
|
"R99",
|
||
|
|
"Symptoms, signs and abnormal clinical and laboratory findings",
|
||
|
|
"symptoms_findings",
|
||
|
|
"Symptoms / findings",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"XIX",
|
||
|
|
"S00",
|
||
|
|
"T98",
|
||
|
|
"Injury, poisoning and certain other consequences of external causes",
|
||
|
|
"injury_poisoning",
|
||
|
|
"Injury / poisoning",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"XX",
|
||
|
|
"V01",
|
||
|
|
"Y98",
|
||
|
|
"External causes of morbidity and mortality",
|
||
|
|
"external_causes",
|
||
|
|
"External causes",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"XXI",
|
||
|
|
"Z00",
|
||
|
|
"Z99",
|
||
|
|
"Factors influencing health status and contact with health services",
|
||
|
|
"health_services_factors",
|
||
|
|
"Health status / services factors",
|
||
|
|
),
|
||
|
|
ChapterRule(
|
||
|
|
"XXII",
|
||
|
|
"U00",
|
||
|
|
"U99",
|
||
|
|
"Codes for special purposes",
|
||
|
|
"special_purposes",
|
||
|
|
"Special purposes",
|
||
|
|
),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
CODE_RE = re.compile(r"^([A-Z][0-9]{2})(?:\.[0-9A-Z]+)?\b")
|
||
|
|
UNKNOWN_CANCER_RE = re.compile(r"^(CXX)\b\s*(.*)$", re.IGNORECASE)
|
||
|
|
|
||
|
|
|
||
|
|
def code_key(code: str) -> tuple[str, int]:
|
||
|
|
code = code.upper().strip()
|
||
|
|
return code[0], int(code[1:3])
|
||
|
|
|
||
|
|
|
||
|
|
def in_range(code: str, start: str, end: str) -> bool:
|
||
|
|
letter, number = code_key(code)
|
||
|
|
start_letter, start_number = code_key(start)
|
||
|
|
end_letter, end_number = code_key(end)
|
||
|
|
return (start_letter, start_number) <= (letter, number) <= (end_letter, end_number)
|
||
|
|
|
||
|
|
|
||
|
|
def parse_label(line: str) -> tuple[str, str]:
|
||
|
|
text = line.strip()
|
||
|
|
unknown_cancer = UNKNOWN_CANCER_RE.match(text)
|
||
|
|
if unknown_cancer:
|
||
|
|
return unknown_cancer.group(1).upper(), unknown_cancer.group(2).strip()
|
||
|
|
|
||
|
|
match = CODE_RE.match(text)
|
||
|
|
if not match:
|
||
|
|
return text, ""
|
||
|
|
code = match.group(1).upper()
|
||
|
|
name = text[len(code):].strip()
|
||
|
|
if name.startswith("(") and name.endswith(")"):
|
||
|
|
name = name[1:-1]
|
||
|
|
return code, name
|
||
|
|
|
||
|
|
|
||
|
|
def assign_chapter(code: str) -> ChapterRule | None:
|
||
|
|
if code.upper().strip() == "CXX":
|
||
|
|
return CHAPTER_RULES[1]
|
||
|
|
if re.fullmatch(r"[A-Z][0-9]{2}", code.upper().strip()) is None:
|
||
|
|
return None
|
||
|
|
for rule in CHAPTER_RULES:
|
||
|
|
if in_range(code, rule.start, rule.end):
|
||
|
|
return rule
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def build_mapping(labels_path: Path, output_path: Path) -> None:
|
||
|
|
rows = []
|
||
|
|
with labels_path.open("r", encoding="utf-8") as f:
|
||
|
|
for label_index, raw in enumerate(f):
|
||
|
|
text = raw.strip()
|
||
|
|
if not text:
|
||
|
|
continue
|
||
|
|
|
||
|
|
token_id = LABEL_OFFSET + label_index
|
||
|
|
code, name = parse_label(text)
|
||
|
|
|
||
|
|
if code.lower() == "death":
|
||
|
|
rows.append(
|
||
|
|
{
|
||
|
|
"label_index": label_index,
|
||
|
|
"token_id": token_id,
|
||
|
|
"code": "Death",
|
||
|
|
"name": "Death",
|
||
|
|
"icd10_chapter": "Death",
|
||
|
|
"icd10_range": "",
|
||
|
|
"icd10_chapter_title": "Death endpoint",
|
||
|
|
"organ_system": "death",
|
||
|
|
"organ_system_label": "Death",
|
||
|
|
"is_death": 1,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
continue
|
||
|
|
|
||
|
|
rule = assign_chapter(code)
|
||
|
|
if rule is None:
|
||
|
|
rows.append(
|
||
|
|
{
|
||
|
|
"label_index": label_index,
|
||
|
|
"token_id": token_id,
|
||
|
|
"code": code,
|
||
|
|
"name": name,
|
||
|
|
"icd10_chapter": "Unmapped",
|
||
|
|
"icd10_range": "",
|
||
|
|
"icd10_chapter_title": "Unmapped",
|
||
|
|
"organ_system": "unmapped",
|
||
|
|
"organ_system_label": "Unmapped",
|
||
|
|
"is_death": 0,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
continue
|
||
|
|
|
||
|
|
rows.append(
|
||
|
|
{
|
||
|
|
"label_index": label_index,
|
||
|
|
"token_id": token_id,
|
||
|
|
"code": code,
|
||
|
|
"name": name,
|
||
|
|
"icd10_chapter": rule.chapter,
|
||
|
|
"icd10_range": f"{rule.start}-{rule.end}",
|
||
|
|
"icd10_chapter_title": rule.title,
|
||
|
|
"organ_system": rule.organ_system,
|
||
|
|
"organ_system_label": rule.organ_system_label,
|
||
|
|
"is_death": 0,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
fieldnames = [
|
||
|
|
"label_index",
|
||
|
|
"token_id",
|
||
|
|
"code",
|
||
|
|
"name",
|
||
|
|
"icd10_chapter",
|
||
|
|
"icd10_range",
|
||
|
|
"icd10_chapter_title",
|
||
|
|
"organ_system",
|
||
|
|
"organ_system_label",
|
||
|
|
"is_death",
|
||
|
|
]
|
||
|
|
with output_path.open("w", encoding="utf-8", newline="") as f:
|
||
|
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||
|
|
writer.writeheader()
|
||
|
|
writer.writerows(rows)
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
build_mapping(
|
||
|
|
labels_path=Path("labels.csv"),
|
||
|
|
output_path=Path("icd10_chapter_organ_mapping.csv"),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|