Spaces:
Sleeping
Sleeping
| """ | |
| Text preprocessing pipeline for MAUDE narrative reports. | |
| Cleans and normalizes raw MDR narrative text for NLP ingestion. | |
| """ | |
| import re | |
| import string | |
| import logging | |
| from typing import List | |
| import pandas as pd | |
| logger = logging.getLogger(__name__) | |
| # Common medical abbreviation expansions (extend as needed) | |
| ABBREVIATION_MAP = { | |
| r"\bpt\b": "patient", | |
| r"\bpts\b": "patients", | |
| r"\bmd\b": "physician", | |
| r"\bdr\b": "doctor", | |
| r"\bhosp\b": "hospital", | |
| r"\badm\b": "admitted", | |
| r"\bdx\b": "diagnosis", | |
| r"\btx\b": "treatment", | |
| r"\brx\b": "prescription", | |
| r"\bs/p\b": "status post", | |
| r"\bw/\b": "with", | |
| r"\bh/o\b": "history of", | |
| r"\bc/o\b": "complaint of", | |
| r"\bn/v\b": "nausea vomiting", | |
| r"\bSOB\b": "shortness of breath", | |
| r"\bUNK\b": "unknown", | |
| } | |
| def expand_abbreviations(text: str) -> str: | |
| """Replace common medical abbreviations with full forms.""" | |
| for pattern, replacement in ABBREVIATION_MAP.items(): | |
| text = re.sub(pattern, replacement, text, flags=re.IGNORECASE) | |
| return text | |
| def remove_boilerplate(text: str) -> str: | |
| """ | |
| Remove common MAUDE boilerplate phrases that carry no signal. | |
| These often appear at the start/end of narrative sections. | |
| """ | |
| boilerplate_patterns = [ | |
| r"it was reported that", | |
| r"the reporter stated", | |
| r"according to the report", | |
| r"this is a report", | |
| r"per the report", | |
| r"the following information was received", | |
| r"information has been received", | |
| r"no further information (is|was) available", | |
| r"follow.?up (is|will be) requested", | |
| ] | |
| for pattern in boilerplate_patterns: | |
| text = re.sub(pattern, " ", text, flags=re.IGNORECASE) | |
| return text | |
| def clean_text(text: str, lowercase: bool = True, preserve_digits: bool = False) -> str: | |
| """ | |
| Full cleaning pipeline for a single narrative string. | |
| Steps: | |
| 1. Strip whitespace | |
| 2. Remove boilerplate | |
| 3. Expand abbreviations | |
| 4. Remove special characters (and optionally digits) | |
| 5. Normalize whitespace | |
| 6. Lowercase (optional) | |
| Args: | |
| text: Raw narrative text string. | |
| lowercase: Whether to convert to lowercase. | |
| preserve_digits: If True, keep digit tokens (required for BERT — device | |
| model numbers and dosages are meaningful to the encoder). | |
| If False (default, TF-IDF mode), digits are stripped. | |
| Returns: | |
| Cleaned text string. | |
| """ | |
| if not isinstance(text, str) or not text.strip(): | |
| return "" | |
| text = text.strip() | |
| text = remove_boilerplate(text) | |
| text = expand_abbreviations(text) | |
| # Remove non-alphanumeric characters (keep spaces; optionally keep digits) | |
| if preserve_digits: | |
| text = re.sub(r"[^a-zA-Z0-9\s]", " ", text) | |
| else: | |
| text = re.sub(r"[^a-zA-Z\s]", " ", text) | |
| # Collapse multiple spaces | |
| text = re.sub(r"\s+", " ", text).strip() | |
| if lowercase: | |
| text = text.lower() | |
| return text | |
| def clean_dataframe( | |
| df: pd.DataFrame, | |
| text_col: str = "narrative_text", | |
| output_col: str = "clean_text", | |
| preserve_digits: bool = False, | |
| ) -> pd.DataFrame: | |
| """ | |
| Apply clean_text to an entire DataFrame column. | |
| Args: | |
| df: Input DataFrame with raw narrative text. | |
| text_col: Name of the column containing raw text. | |
| output_col: Name of the new column to store cleaned text. | |
| preserve_digits: Passed through to clean_text; set True for BERT. | |
| Returns: | |
| DataFrame with a new cleaned text column, with empty-text rows dropped. | |
| """ | |
| logger.info(f"Cleaning text in column '{text_col}'...") | |
| df = df.copy() | |
| df[output_col] = df[text_col].apply( | |
| lambda t: clean_text(t, preserve_digits=preserve_digits) | |
| ) | |
| # Drop rows where cleaning produced empty strings | |
| before = len(df) | |
| df = df[df[output_col].str.len() > 10].reset_index(drop=True) | |
| after = len(df) | |
| logger.info(f"Dropped {before - after} rows with insufficient text. {after} rows remaining.") | |
| return df | |
| def get_label_distribution(df: pd.DataFrame, label_col: str = "severity_label") -> pd.Series: | |
| """Return label counts for inspection.""" | |
| return df[label_col].value_counts() | |