ICD10_Code_Prediction / src /utils /retriever.py
Vaibhavi53's picture
Uploading the required files
25f8367 verified
Raw
History Blame Contribute Delete
5.5 kB
"""
Hierarchical TF-IDF Retriever for ICD-10 candidate generation.
Three-stage retrieval:
1. Chapter-level scoring (22 chapters β†’ top-5)
2. Category-level scoring (within selected chapters β†’ top-20)
3. Full-code scoring (within selected categories β†’ top-K)
"""
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from utils.config import ICD10_CHAPTERS
from utils.preprocessing import get_icd_chapter, get_icd_category
class HierarchicalTFIDFRetriever:
"""Three-level hierarchical ICD-10 code retriever using TF-IDF."""
def __init__(self):
self.icd_metadata_df = None
self.chapter_tfidf = TfidfVectorizer(max_features=10000, sublinear_tf=True, ngram_range=(1, 2))
self.category_tfidf = TfidfVectorizer(max_features=20000, sublinear_tf=True, ngram_range=(1, 2))
self.code_tfidf = TfidfVectorizer(max_features=30000, sublinear_tf=True, ngram_range=(1, 2))
self.chapter_vectors = None
self.category_vectors = None
self.code_vectors = None
self.chapter_df = None
self.category_df = None
self._fitted = False
def fit(self, bilingual_lookup: dict):
"""Build the retriever from the bilingual lookup dictionary."""
rows = []
for code, info in bilingual_lookup.items():
eng = info.get("english", "")
chi = info.get("chinese", "")
chapter_letter = get_icd_chapter(code)
chapter = ICD10_CHAPTERS.get(chapter_letter, "Unknown")
category = get_icd_category(code)
combined_desc = f"{eng} {chi}".strip()
rows.append({
"code": code,
"chapter": chapter,
"category": category,
"english_description": eng,
"chinese_description": chi,
"combined_description": combined_desc,
})
self.icd_metadata_df = pd.DataFrame(rows)
# ── Stage 1: Chapter-level TF-IDF ──
self.chapter_df = (
self.icd_metadata_df
.groupby("chapter")["combined_description"]
.apply(lambda x: " ".join(x))
.reset_index()
)
self.chapter_vectors = self.chapter_tfidf.fit_transform(self.chapter_df["combined_description"])
# ── Stage 2: Category-level TF-IDF ──
self.category_df = (
self.icd_metadata_df
.groupby("category")["combined_description"]
.apply(lambda x: " ".join(x))
.reset_index()
)
self.category_vectors = self.category_tfidf.fit_transform(self.category_df["combined_description"])
# ── Stage 3: Code-level TF-IDF ──
self.code_vectors = self.code_tfidf.fit_transform(self.icd_metadata_df["combined_description"])
self._fitted = True
return self
def retrieve(self, note_text: str, top_k: int = 100,
top_chapters: int = 5, top_categories: int = 20) -> list:
"""
Retrieve top-K ICD-10 candidate codes for a clinical note.
Returns: list of (code, score) tuples sorted by relevance.
"""
if not self._fitted:
raise RuntimeError("Retriever not fitted. Call .fit() first.")
# ── Stage 1: Score chapters ──
note_chapter_vec = self.chapter_tfidf.transform([note_text])
chapter_scores = cosine_similarity(note_chapter_vec, self.chapter_vectors).flatten()
top_chapter_idx = np.argsort(chapter_scores)[::-1][:top_chapters]
selected_chapters = set(self.chapter_df.iloc[top_chapter_idx]["chapter"].tolist())
# ── Stage 2: Score categories within selected chapters ──
chapter_to_cats = (
self.icd_metadata_df[self.icd_metadata_df["chapter"].isin(selected_chapters)]
["category"].unique()
)
cat_mask = self.category_df["category"].isin(chapter_to_cats)
cat_indices = self.category_df[cat_mask].index.tolist()
if not cat_indices:
cat_indices = list(range(len(self.category_df)))
note_cat_vec = self.category_tfidf.transform([note_text])
cat_scores = cosine_similarity(note_cat_vec, self.category_vectors[cat_indices]).flatten()
top_cat_local = np.argsort(cat_scores)[::-1][:top_categories]
top_cat_idx = [cat_indices[i] for i in top_cat_local]
selected_categories = set(self.category_df.iloc[top_cat_idx]["category"].tolist())
# ── Stage 3: Score codes within selected categories ──
code_mask = self.icd_metadata_df["category"].isin(selected_categories)
code_indices = self.icd_metadata_df[code_mask].index.tolist()
if not code_indices:
code_indices = list(range(len(self.icd_metadata_df)))
note_code_vec = self.code_tfidf.transform([note_text])
code_scores = cosine_similarity(note_code_vec, self.code_vectors[code_indices]).flatten()
top_code_local = np.argsort(code_scores)[::-1][:top_k]
results = []
for i in top_code_local:
global_idx = code_indices[i]
row = self.icd_metadata_df.iloc[global_idx]
results.append((row["code"], float(code_scores[i])))
return results
@property
def num_codes(self):
return len(self.icd_metadata_df) if self.icd_metadata_df is not None else 0