Team Ai
Apppublic

Hanan-Alnakhal/Lab-test-decoder

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
pdf_extractor.py189 linesDownload Raw Back to root
1"""2PDF Extraction Module for Lab Reports3Extracts lab test names, values, and ranges from uploaded PDF files4"""5 6import pdfplumber7import re8from typing import Dict, List, Optional9from dataclasses import dataclass10 11@dataclass12class LabResult:13    """Represents a single lab test result"""14    test_name: str15    value: str16    unit: str17    reference_range: str18    status: str  # 'normal', 'high', 'low', 'unknown'19 20class LabReportExtractor:21    """Extract structured data from lab report PDFs"""22    23    def __init__(self):24        # Common lab test patterns25        self.test_patterns = [26            r'(Hemoglobin|Hgb|Hb)\s*:?\s*([\d.]+)\s*([a-zA-Z/]+)?\s*(?:Ref\.?\s*Range:?\s*)?([\d.\-\s]+)',27            r'(WBC|White Blood Cell|Leukocyte)\s*:?\s*([\d.]+)\s*([a-zA-Z/]+)?\s*(?:Ref\.?\s*Range:?\s*)?([\d.\-\s]+)',28            r'(Glucose|Blood Sugar)\s*:?\s*([\d.]+)\s*([a-zA-Z/]+)?\s*(?:Ref\.?\s*Range:?\s*)?([\d.\-\s]+)',29            r'(Iron|Ferritin)\s*:?\s*([\d.]+)\s*([a-zA-Z/]+)?\s*(?:Ref\.?\s*Range:?\s*)?([\d.\-\s]+)',30            r'(Cholesterol|LDL|HDL)\s*:?\s*([\d.]+)\s*([a-zA-Z/]+)?\s*(?:Ref\.?\s*Range:?\s*)?([\d.\-\s]+)',31        ]32    33    def extract_from_pdf(self, pdf_path: str) -> List[LabResult]:34        """Extract lab results from PDF file"""35        results = []36        37        with pdfplumber.open(pdf_path) as pdf:38            for page in pdf.pages:39                text = page.extract_text()40                41                # Try to extract tables first (more structured)42                tables = page.extract_tables()43                if tables:44                    results.extend(self._parse_tables(tables))45                46                # Fall back to pattern matching47                results.extend(self._parse_text(text))48        49        # Remove duplicates50        unique_results = self._deduplicate_results(results)51        52        return unique_results53    54    def _parse_tables(self, tables: List) -> List[LabResult]:55        """Parse lab results from extracted tables"""56        results = []57        58        for table in tables:59            if not table or len(table) < 2:60                continue61            62            # Assume first row is header63            headers = [h.lower() if h else '' for h in table[0]]64            65            # Find relevant columns66            test_col = self._find_column(headers, ['test', 'name', 'component'])67            value_col = self._find_column(headers, ['value', 'result'])68            unit_col = self._find_column(headers, ['unit', 'units'])69            range_col = self._find_column(headers, ['range', 'reference', 'normal'])70            71            # Parse data rows72            for row in table[1:]:73                if not row or len(row) <= max(test_col or 0, value_col or 0):74                    continue75                76                test_name = row[test_col] if test_col is not None else ''77                value = row[value_col] if value_col is not None else ''78                unit = row[unit_col] if unit_col is not None else ''79                ref_range = row[range_col] if range_col is not None else ''80                81                if test_name and value:82                    status = self._determine_status(value, ref_range)83                    results.append(LabResult(84                        test_name=test_name.strip(),85                        value=str(value).strip(),86                        unit=str(unit).strip() if unit else '',87                        reference_range=str(ref_range).strip() if ref_range else '',88                        status=status89                    ))90        91        return results92    93    def _parse_text(self, text: str) -> List[LabResult]:94        """Parse lab results using regex patterns"""95        results = []96        97        for pattern in self.test_patterns:98            matches = re.finditer(pattern, text, re.IGNORECASE)99            for match in matches:100                groups = match.groups()101                if len(groups) >= 2:102                    test_name = groups[0]103                    value = groups[1]104                    unit = groups[2] if len(groups) > 2 else ''105                    ref_range = groups[3] if len(groups) > 3 else ''106                    107                    status = self._determine_status(value, ref_range)108                    results.append(LabResult(109                        test_name=test_name,110                        value=value,111                        unit=unit or '',112                        reference_range=ref_range or '',113                        status=status114                    ))115        116        return results117    118    def _find_column(self, headers: List[str], keywords: List[str]) -> Optional[int]:119        """Find column index by keywords"""120        for i, header in enumerate(headers):121            for keyword in keywords:122                if keyword in header:123                    return i124        return None125    126    def _determine_status(self, value: str, ref_range: str) -> str:127        """Determine if value is normal, high, or low"""128        try:129            # Clean value - remove any non-numeric characters except . and -130            clean_value = re.sub(r'[^\d.-]', '', value)131            if not clean_value or clean_value == '-':132                return 'unknown'133            134            val = float(clean_value)135            136            # Parse reference range - try multiple patterns137            # Pattern 1: "4.0 - 11.0" or "4.0-11.0"138            range_match = re.search(r'([\d.]+)\s*-\s*([\d.]+)', ref_range)139            140            if range_match:141                low = float(range_match.group(1))142                high = float(range_match.group(2))143                144                # Add small tolerance for floating point comparison145                if val < (low - 0.01):146                    return 'low'147                elif val > (high + 0.01):148                    return 'high'149                else:150                    return 'normal'151            152            # Pattern 2: "< 100" (upper limit only)153            upper_match = re.search(r'[<≤]\s*([\d.]+)', ref_range)154            if upper_match:155                upper = float(upper_match.group(1))156                return 'high' if val > upper else 'normal'157            158            # Pattern 3: "> 50" (lower limit only)159            lower_match = re.search(r'[>≥]\s*([\d.]+)', ref_range)160            if lower_match:161                lower = float(lower_match.group(1))162                return 'low' if val < lower else 'normal'163                164        except (ValueError, AttributeError) as e:165            print(f"Status detection error for value '{value}', range '{ref_range}': {e}")166        167        return 'unknown'168    169    def _deduplicate_results(self, results: List[LabResult]) -> List[LabResult]:170        """Remove duplicate test results"""171        seen = set()172        unique = []173        174        for result in results:175            key = (result.test_name.lower(), result.value)176            if key not in seen:177                seen.add(key)178                unique.append(result)179        180        return unique181 182# Example usage183if __name__ == "__main__":184    extractor = LabReportExtractor()185    results = extractor.extract_from_pdf("sample_lab_report.pdf")186    187    for result in results:188        print(f"{result.test_name}: {result.value} {result.unit} [{result.status}]")189        print(f"  Reference: {result.reference_range}")