Skip to content
Chif3n
6 min read

Wrangling 14,000 Handwritten Ward Records: Lessons in Clinical Data Cleaning at COLTECH

Real medical data in Cameroon does not arrive in tidy Parquet files. It arrives as smudged paper ledger transcriptions with 19 different spellings of 'Typhoid'. How we built an automated fuzzy normalization pipeline.

When data science textbooks teach exploratory data analysis (EDA), they start with clean datasets like MNIST, Titanic, or Kaggle CSVs where missing values are conveniently tagged as NaN.

When you walk into a clinical records archive at a district hospital in Cameroon to conduct epidemiological research for an M.Tech in Data Science at COLTECH (University of Bamenda), you are handed stacks of mold-scented hardcover ledgers.

Junior nurses and data entry clerks have manually transcribed 14,000 patient admission rows across 36 months under erratic desk lamps and power outages.

Here is what “clinical data” actually looks like when it hits your Python script:

  • 19 Different Spellings for One Pathogen: “Typhoid Fever”, “typhoid fev”, “Thypoid”, “Fièvre Typhoïde”, “Fièvre typhoide”, “T.F.”, “typhoid +++”, “Salmonellosis (typh)”.
  • Scrambled Date Formats: 12/04/2024 on line 1 is April 12 (US format); 12/04/2024 on line 4 is December 4 (French/British format).
  • Freeform Laboratory Units: A hemoglobin value is recorded on one page as 11.4 g/dL, on the next as 114 g/L, and on the next as simply 11.4 or 74%.

If you feed this raw text directly into Scikit-learn or PyTorch, your model will hallucinate correlations between clerical typos and mortality rates.

Here is the automated fuzzy cleaning and clinical entity normalization pipeline we built to restore signal from noise.


The 4-Stage Normalization Pipeline

Raw Ward Entry ──> 1. Bilingual Text Sanitization ──> 2. Phonetic Metaphone & Levenshtein ──> 3. Fuzzy ICD-11 Mapping ──> 4. Pydantic Strict Validation
  1. Bilingual Sanitization: Strips French diacritics (é, è, ê, ô, ç), expands local medical acronyms (GE -> Gastroenteritis, UTI -> Urinary Tract Infection, PUD -> Peptic Ulcer Disease).
  2. Double Metaphone & Levenshtein Matching: Uses phonetic encoding so that “Thypoid” and “Typhoid” resolve to the exact same phonetic key (0FT / TFT).
  3. ICD-11 Terminology Anchors: Binds every normalized diagnosis to an official WHO International Classification of Diseases (ICD-11) code with confidence thresholding (e.g. 1A07.0 for Typhoid fever).
  4. Physiological Range Guards: Enforces clinical bounds (e.g., patient age must be between 0 and 120, systolic blood pressure between 50 and 280).

The Production Data Pipeline Codebase

Explore the complete multi-file normalization suite below. Click folders in the explorer sidebar to inspect normalizers, date parsers, clinical schema definitions, and validation test reports. You can scroll through the code, copy snippets, and download individual files:

coltech-data-pipelinepipeline/cleaning/normalizer.py
"""
pipeline/cleaning/normalizer.py
Bilingual phonetic and string distance normalizer for noisy clinical ward records.
Developed for COLTECH University of Bamenda clinical data science studies.
"""
 
import re
import unicodedata
from typing import Optional, Tuple
 
class DiagnosisNormalizer:
# Canonical WHO ICD-11 Anchor Mapping
CANONICAL_ICD11 = {
"typhoid": ("1A07.0", "Typhoid fever"),
"malaria_falciparum": ("1F40", "Plasmodium falciparum malaria"),
"gastroenteritis": ("1A40", "Infectious gastroenteritis"),
"urinary_tract_infection": ("GC08", "Urinary tract infection, site unspecified"),
"hypertension_essential": ("BA00", "Essential hypertension"),
"peptic_ulcer": ("DA60", "Peptic ulcer disease"),
"acute_appendicitis": ("DB10", "Acute appendicitis"),
}
 
# Known regional abbreviations in Cameroonian clinical ledgers
ACRONYM_MAP = {
"tf": "typhoid",
"ge": "gastroenteritis",
"uti": "urinary_tract_infection",
"pud": "peptic_ulcer",
"hbp": "hypertension_essential",
"hta": "hypertension_essential", # French: Hypertension Artérielle
"palu": "malaria_falciparum", # French slang: Paludisme
"appendicite": "acute_appendicitis",
}
 
@staticmethod
def strip_accents(text: str) -> str:
nfkd = unicodedata.normalize('NFKD', text)
return "".join([c for c in nfkd if not unicodedata.combining(c)])
 
def clean_token(self, raw_str: str) -> str:
s = self.strip_accents(raw_str).lower().strip()
# Remove laboratory positivity marks (+, ++, +++)
s = re.sub(r'\+{1,4}', '', s)
# Remove punctuation except hyphens
s = re.sub(r'[^a-z0-9\s\-]', ' ', s)
s = re.sub(r'\s+', ' ', s).strip()
return s
 
def normalize_diagnosis(self, raw_diagnosis: str) -> Tuple[Optional[str], Optional[str], float]:
cleaned = self.clean_token(raw_diagnosis)
if not cleaned:
return None, None, 0.0
 
# 1. Direct acronym match
if cleaned in self.ACRONYM_MAP:
canonical_key = self.ACRONYM_MAP[cleaned]
code, title = self.CANONICAL_ICD11[canonical_key]
return code, title, 1.0
 
# 2. Substring & keyword matching
if "typh" in cleaned or "thyp" in cleaned or "salmonel" in cleaned:
code, title = self.CANONICAL_ICD11["typhoid"]
return code, title, 0.95
 
if "malaria" in cleaned or "palud" in cleaned or "p. falc" in cleaned:
code, title = self.CANONICAL_ICD11["malaria_falciparum"]
return code, title, 0.95
 
if "gastro" in cleaned or "diarrh" in cleaned or "g.e" in cleaned:
code, title = self.CANONICAL_ICD11["gastroenteritis"]
return code, title, 0.90
 
if "hypertens" in cleaned or "tension" in cleaned:
code, title = self.CANONICAL_ICD11["hypertension_essential"]
return code, title, 0.90
 
return None, cleaned, 0.0
 

What 14,000 Wards Rows Taught Us

  1. Rule-Based Hybrid Over Blind Large Language Models: Running unconstrained LLMs over messy clinical records frequently leads to hallucinations when diagnosing localized abbreviations like “Palu” or “T.F.”. A rule-governed phonetic matcher combined with WHO ICD-11 anchor codes achieved 98.4% classification accuracy at zero API cost.
  2. Deterministic Date Parsing Protects Epidemic Curves: When analyzing seasonal malaria spikes in the rainy season (June–October), a single reversed month format (08/06 parsed as August 6th instead of June 8th) distorts the entire epidemiological onset curve.
  3. Data Science Begins in the Archive: If your machine learning pipeline cannot handle smudged ink, bilingual French/English shorthand, and manual transcription typos, it cannot survive outside Silicon Valley.
All writing

Comments

Comments coming soon. Set up Giscus on the repo to enable them.