Spaces:
Sleeping
Sleeping
| """Deterministic numeric fact extraction with exact source spans. | |
| This module never validates or corrects a value. Every extracted number starts | |
| with status=pending and must pass the dedicated numeric validator later. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from dataclasses import dataclass | |
| class NumericMatch: | |
| kind: str | |
| raw_text: str | |
| value: float | str | None | |
| unit: str | None | |
| start: int | |
| end: int | |
| _PATTERNS: list[tuple[str, re.Pattern[str]]] = [ | |
| ("confidence_interval", re.compile(r"(?:95\s*%?\s*(?:CI|IC))\s*[:=\[(]?\s*(-?\d+(?:\.\d+)?)\s*(?:[-–,]|to)\s*(-?\d+(?:\.\d+)?)", re.I)), | |
| ("p_value", re.compile(r"\bp\s*([<>=]=?)\s*(0?\.\d+|\d+(?:\.\d+)?(?:e-?\d+)?)", re.I)), | |
| ("effect_estimate", re.compile(r"\b(OR|RR|HR|SMD|MD)\s*[=:]?\s*(-?\d+(?:\.\d+)?)", re.I)), | |
| ("dose", re.compile(r"\b(\d+(?:\.\d+)?)\s*(mg|g|µg|ug|mcg|IU|UI|mL|ml|L|mmol|µmol|umol|nmol)(?:\s*/\s*(?:kg|day|d|jour))?\b", re.I)), | |
| ("percentage", re.compile(r"\b(\d+(?:\.\d+)?)\s*%(?!\s*(?:CI|IC))", re.I)), | |
| ("sample_size", re.compile(r"\bn\s*=\s*(\d[\d ,]*)", re.I)), | |
| ("sample_size", re.compile(r"\b(\d[\d ,]*)\s+(?:(?:[A-Za-zÀ-ÿ-]+)\s+){0,2}(?:participants?|patients?|subjects?|students?|volunteers?|adults?|children|individuals?|women|men)\b", re.I)), | |
| ("duration", re.compile(r"\b(\d+(?:\.\d+)?)\s*(hours?|days?|weeks?|months?|years?)\b", re.I)), | |
| ("age", re.compile(r"\b(?:aged?|age)\s*(\d+(?:\.\d+)?)\s*(?:years?|yrs?)\b", re.I)), | |
| ("frequency", re.compile(r"\b(once|twice|three times|four times)\s+(?:a|per)\s+(day|week)\b", re.I)), | |
| ] | |
| def _number(value: str) -> float | str: | |
| cleaned = value.replace(",", "").replace(" ", "") | |
| try: | |
| return float(cleaned) | |
| except ValueError: | |
| return value | |
| def extract_numeric_matches(text: str) -> list[NumericMatch]: | |
| matches: list[NumericMatch] = [] | |
| occupied: list[tuple[int, int]] = [] | |
| for kind, pattern in _PATTERNS: | |
| for match in pattern.finditer(text): | |
| span = match.span() | |
| if any(span[0] < end and span[1] > start for start, end in occupied): | |
| continue | |
| groups = match.groups() | |
| raw = match.group(0) | |
| value: float | str | None = None | |
| unit: str | None = None | |
| if kind == "confidence_interval": | |
| value = f"{groups[0]} to {groups[1]}" | |
| unit = "95% CI" | |
| elif kind == "p_value": | |
| value = _number(groups[1]) | |
| unit = f"p{groups[0]}" | |
| elif kind == "effect_estimate": | |
| value = _number(groups[1]) | |
| unit = groups[0].upper() | |
| elif kind == "dose": | |
| value = _number(groups[0]) | |
| unit = groups[1] | |
| elif kind in {"percentage", "sample_size", "duration", "age"}: | |
| value = _number(groups[0]) | |
| unit = groups[1] if len(groups) > 1 and groups[1] else { | |
| "percentage": "%", | |
| "sample_size": "participants", | |
| "age": "years", | |
| }.get(kind) | |
| elif kind == "frequency": | |
| value = groups[0] | |
| unit = groups[1] | |
| matches.append(NumericMatch(kind, raw, value, unit, span[0], span[1])) | |
| occupied.append(span) | |
| return sorted(matches, key=lambda item: (item.start, item.end, item.kind)) | |
| def to_numeric_facts(text: str) -> list[dict]: | |
| return [ | |
| { | |
| "kind": item.kind, | |
| "raw_text": item.raw_text, | |
| "value": item.value, | |
| "unit": item.unit, | |
| "context": text, | |
| "status": "pending", | |
| "correction": None, | |
| } | |
| for item in extract_numeric_matches(text) | |
| ] | |