# NoteGuard — NHS Clinical-Note PII Sanitisation Sanitise-at-source: detect + de-identify PII in free-text NHS clinical notes so only de-identified data leaves a Trust. Encode Vibe Coding Hackathon — FLock Sovereign AI Challenge (Encode Hub); fork of `NoteGuard/`. ## Commands ```bash # Setup (Windows PowerShell) python -m venv .venv; .\.venv\Scripts\Activate.ps1 pip install -e ".[app,dev]"; python -m spacy download en_core_web_lg python tests/run_eval.py --compare --limit 300 # VERIFIABLE SIGNAL: rules vs presidio+rules -> outputs/results.json python -m src.trust_demo # two NHS Trusts share only de-identified data -> outputs/ streamlit run streamlit_app.py # demo (Try-it / Metrics / Governance / Two-Trust) python -m pytest tests/ -v # Offline data: set NOTEGUARD_DATA_DIR to a folder holding the 3 CSVs (else auto-downloaded from HF). ``` ## Architecture - `src/` — `data` (load + ground-truth join, EVAL-ONLY oracle) · `recognisers` (pure-Python rules) · `detect` (Rule / Presidio, graceful fallback) · `transform` (redact | patient-consistent pseudonymise + date-shift, Faker) · `evaluate` (P/R/F1 + residual leakage) · `pipeline` · `trust_demo`. - `tests/run_eval.py` CLI · `streamlit_app.py` demo · `tests/` mirror `src/`. Packaged via `pyproject.toml`. ## Code style - Python 3.10+, type hints on function signatures. The pure-Python rule layer must stay importable WITHOUT spaCy/Presidio (the fallback path). snake_case / PascalCase. ## Data rules (treat the synthetic notes as if real NHS PHI) - `data/raw/`, `outputs/`, and any vault export are gitignored — never commit. Never paste note text into prompts; point at file paths. - The note→patient join (`src/data.py` ground truth) is the EVAL-ONLY oracle. It must NEVER feed detection/transform — that is data leakage and invalidates the metric. - Never silently fall back to an older/cached dataset — fail loudly. ## Decisions locked in (version 1 branch) - **Default model: `en_core_web_lg`** — 100% name recall vs 91% for sm; clinical transformer (`obi/deid_roberta_i2b2`) was tested and performed worse on UK names (US i2b2 training data). - **ORGANIZATION excluded from PresidioDetector.KEEP** — spaCy lg over-tags labels/abbreviations ("NHS", "DOB …", "GMC") as ORG, causing false positives and swallowing precise rule spans. NHS site names are caught by the `_SITE_RE` LOCATION rule (incl. "… Trust") instead. - **`_merge` is overlap-safe + priority-ranked** — output spans are disjoint (no transform corruption); on overlap, precise rule entities (date/NHS/GMC/…) beat broad NER spans. - **Human-in-the-loop review queue** — spans with score in `[review_threshold, score_threshold)` are redacted but flagged `needs_review=True` for IG analyst review before SDE pool admission. - **Places recall** — low recall (0–0.7) was mostly generic "ward"/"bay" in GT (now filtered by `_GENERIC`); NHS site names are caught by the `_SITE_RE` LOCATION rule in recognisers. ## Gotchas - Note text has mojibake (`·`) — `_fix_mojibake` runs before detection. - Synthetic NHS numbers are 9 digits (no valid mod-11) — caught via the "NHS …" context anchor. - Default spaCy model is now `en_core_web_lg`; the `PII_SPACY_MODEL` env var still overrides. ## Working with Claude - After editing `src/recognisers.py` / `detect.py` / `transform.py`, run `python tests/run_eval.py --compare` and check residual leakage didn't regress.