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()