CAFF / scripts /convert_orphanet_xml_to_tsv.py
MrDhifallah's picture
Upload folder using huggingface_hub
634ebe8 verified
Raw
History Blame Contribute Delete
7.56 kB
"""
scripts/convert_orphanet_xml_to_tsv.py — Convert Orphanet XML dumps
to the TSV format expected by scripts/build_kg.py::load_orphanet.
Reads:
data/raw/orphanet/en_product6.xml (disease-gene)
data/raw/orphanet/en_product4.xml (disease-phenotype)
data/raw/orphanet/en_product1.xml (disease nomenclature)
Writes:
data/raw/orphanet/genes_to_diseases_en_2025.tsv
data/raw/orphanet/phenotypes_en_2025.tsv
data/raw/orphanet/ORDO_names_en_2025.tsv (ORPHAcode preferred_term)
The build_kg.py script reads ORDO from XLSX; we ship a TSV
companion that build_kg.py can be patched to read instead, OR
we install openpyxl to read XLSX directly. This converter emits
TSV (simpler and Git-friendly).
Usage:
python scripts/convert_orphanet_xml_to_tsv.py
"""
from __future__ import annotations
import logging
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger("orphanet_xml_to_tsv")
ROOT = Path(__file__).parent.parent
ORPH = ROOT / "data" / "raw" / "orphanet"
PRODUCT1 = ORPH / "en_product1.xml"
PRODUCT4 = ORPH / "en_product4.xml"
PRODUCT6 = ORPH / "en_product6.xml"
OUT_NAMES = ORPH / "ORDO_names_en_2025.tsv"
OUT_GENES = ORPH / "genes_to_diseases_en_2025.tsv"
OUT_PHENOS = ORPH / "phenotypes_en_2025.tsv"
def _text(element: ET.Element | None, default: str = "") -> str:
"""Safe extraction of element text."""
if element is None or element.text is None:
return default
return element.text.strip()
def parse_nomenclature(path: Path) -> dict[str, str]:
"""Parse en_product1.xml -> {OrphaCode: preferred_name}.
Returns the mapping AND writes it to OUT_NAMES as a TSV.
"""
if not path.exists():
logger.error(f"Missing file: {path}")
return {}
logger.info(f"Parsing {path.name} ...")
tree = ET.parse(path)
root = tree.getroot()
names: dict[str, str] = {}
n_disorders = 0
# JDBOR > DisorderList > Disorder
for disorder in root.iter("Disorder"):
orpha_code = _text(disorder.find("OrphaCode"))
name = _text(disorder.find("Name"))
if orpha_code and name:
names[orpha_code] = name
n_disorders += 1
logger.info(f" Parsed {n_disorders:,} disorders from nomenclature")
# Write TSV
with OUT_NAMES.open("w", encoding="utf-8") as f:
f.write("ORPHAcode\tPreferred term\n")
for code, name in sorted(names.items(), key=lambda kv: int(kv[0]) if kv[0].isdigit() else 1_000_000):
# Escape any tabs/newlines defensively
safe_name = name.replace("\t", " ").replace("\n", " ").replace("\r", " ")
f.write(f"{code}\t{safe_name}\n")
logger.info(f" Wrote {OUT_NAMES.name} ({len(names):,} rows)")
return names
def parse_genes(path: Path, disease_names: dict[str, str]) -> int:
"""Parse en_product6.xml -> genes_to_diseases TSV."""
if not path.exists():
logger.error(f"Missing file: {path}")
return 0
logger.info(f"Parsing {path.name} ...")
tree = ET.parse(path)
root = tree.getroot()
rows: list[tuple[str, str, str]] = [] # (orpha_code, gene_symbol, association_type)
n_disorders = 0
for disorder in root.iter("Disorder"):
orpha_code = _text(disorder.find("OrphaCode"))
if not orpha_code:
continue
n_disorders += 1
# DisorderGeneAssociationList > DisorderGeneAssociation
assoc_list = disorder.find("DisorderGeneAssociationList")
if assoc_list is None:
continue
for assoc in assoc_list.findall("DisorderGeneAssociation"):
gene = assoc.find("Gene")
if gene is None:
continue
symbol = _text(gene.find("Symbol"))
if not symbol:
continue
# Association type is optional in some entries.
# Path: DisorderGeneAssociation > DisorderGeneAssociationType > Name
assoc_type_elem = assoc.find("DisorderGeneAssociationType/Name")
if assoc_type_elem is not None:
assoc_type = _text(assoc_type_elem) or "associated_with"
else:
assoc_type = "associated_with"
rows.append((orpha_code, symbol, assoc_type))
logger.info(
f" Parsed {len(rows):,} disease-gene associations "
f"from {n_disorders:,} disorders"
)
with OUT_GENES.open("w", encoding="utf-8") as f:
f.write("orpha_code\tgene_symbol\tassociation_type\n")
for orpha_code, symbol, assoc_type in rows:
safe_type = assoc_type.replace("\t", " ").replace("\n", " ").replace("\r", " ")
f.write(f"{orpha_code}\t{symbol}\t{safe_type}\n")
logger.info(f" Wrote {OUT_GENES.name} ({len(rows):,} rows)")
return len(rows)
def parse_phenotypes(path: Path, disease_names: dict[str, str]) -> int:
"""Parse en_product4.xml -> phenotypes TSV."""
if not path.exists():
logger.error(f"Missing file: {path}")
return 0
logger.info(f"Parsing {path.name} ...")
tree = ET.parse(path)
root = tree.getroot()
rows: list[tuple[str, str, str]] = [] # (orpha_code, hpo_id, hpo_term)
n_disorders = 0
# JDBOR > HPODisorderSetStatusList > HPODisorderSetStatus > Disorder
for status in root.iter("HPODisorderSetStatus"):
disorder = status.find("Disorder")
if disorder is None:
continue
orpha_code = _text(disorder.find("OrphaCode"))
if not orpha_code:
continue
n_disorders += 1
assoc_list = disorder.find("HPODisorderAssociationList")
if assoc_list is None:
continue
for assoc in assoc_list.findall("HPODisorderAssociation"):
hpo = assoc.find("HPO")
if hpo is None:
continue
hpo_id = _text(hpo.find("HPOId"))
hpo_term = _text(hpo.find("HPOTerm"))
if hpo_id and hpo_term:
rows.append((orpha_code, hpo_id, hpo_term))
logger.info(
f" Parsed {len(rows):,} disease-phenotype associations "
f"from {n_disorders:,} disorders"
)
with OUT_PHENOS.open("w", encoding="utf-8") as f:
f.write("orpha_code\thpo_id\thpo_term\n")
for orpha_code, hpo_id, hpo_term in rows:
safe_term = hpo_term.replace("\t", " ").replace("\n", " ").replace("\r", " ")
f.write(f"{orpha_code}\t{hpo_id}\t{safe_term}\n")
logger.info(f" Wrote {OUT_PHENOS.name} ({len(rows):,} rows)")
return len(rows)
def main() -> int:
if not ORPH.exists():
logger.error(f"Orphanet directory missing: {ORPH}")
logger.error("Run downloads first (see Phase 3 plan).")
return 1
# 1. Names (ordo)
names = parse_nomenclature(PRODUCT1)
if not names:
logger.error("No disease names parsed; cannot continue.")
return 1
# 2. Genes
n_genes = parse_genes(PRODUCT6, names)
# 3. Phenotypes
n_phenos = parse_phenotypes(PRODUCT4, names)
logger.info("=" * 60)
logger.info(" Conversion complete")
logger.info("=" * 60)
logger.info(f" Disorders (names): {len(names):,}")
logger.info(f" Disease-gene rows: {n_genes:,}")
logger.info(f" Disease-phenotype rows: {n_phenos:,}")
logger.info(f" Output: {ORPH}")
return 0
if __name__ == "__main__":
sys.exit(main())