Files
t2a_v2/tests/test_nutrition_has2021.py
dom 4e2b4bd946 refactor: réorganisation référentiels, nouveaux modules extraction, nettoyage code obsolète
- Réorganisation data/referentiels/ : pdfs/, dicts/, user/ (structure unifiée)
- Fix badges "Source absente" sur page admin référentiels
- Ré-indexation COCOA 2025 (555 → 1451 chunks, couverture 94%)
- Fix VRAM OOM : embeddings forcés CPU via T2A_EMBED_CPU
- Nouveaux modules : document_router, docx_extractor, image_extractor, ocr_engine
- Module complétude (quality/completude.py + config YAML)
- Template DIM (synthèse dimensionnelle)
- Gunicorn config + systemd service t2a-viewer
- Suppression t2a_install_rag_cleanup/ (copie obsolète)
- Suppression scripts/ et scripts_t2a_v2/ (anciens benchmarks)
- Suppression 81 fichiers _doc.txt de test
- Cache Ollama : TTL configurable, corrections loader YAML
- Dashboard : améliorations templates (base, index, detail, cpam, validation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:48:10 +01:00

199 lines
8.2 KiB
Python

"""Tests unitaires pour la détection dénutrition HAS/FFN 2021."""
import re
import pytest
from src.config import BiologieCle, Diagnostic, DossierMedical, Sejour
from src.medical.diagnostic_extraction import _detect_nutrition_has2021, _DAS_PATTERNS
from src.medical.cim10_dict import normalize_text
# ── Helpers ──────────────────────────────────────────────────────────
def _make_dossier(age=None, imc=None, albumine=None, existing_codes=None):
"""Construit un DossierMedical minimal pour les tests."""
dossier = DossierMedical()
dossier.sejour = Sejour(age=age, imc=imc)
if albumine is not None:
dossier.biologie_cle.append(
BiologieCle(
test="Albumine",
valeur=str(albumine),
valeur_num=float(albumine),
anomalie=True,
quality="ok",
)
)
for code in (existing_codes or []):
dossier.diagnostics_associes.append(
Diagnostic(texte="existant", cim10_suggestion=code, source="test")
)
return dossier
# ── Tests _detect_nutrition_has2021 ──────────────────────────────────
class TestDetectNutritionHAS2021:
"""Tests de la détection déterministe basée sur IMC/âge/albumine."""
def test_adulte_imc17_albumine28_gives_E43(self):
"""Adulte IMC 17.0 + albumine 28 → E43 (sévère via IMC ≤17 ET albumine <30)."""
dossier = _make_dossier(age=50, imc=17.0, albumine=28)
_detect_nutrition_has2021(dossier)
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
assert "E43" in codes
def test_adulte_imc18_sans_albumine_gives_E44(self):
"""Adulte IMC 18.0 sans albumine → E44.0 (modéré, 17 < IMC < 18.5)."""
dossier = _make_dossier(age=45, imc=18.0)
_detect_nutrition_has2021(dossier)
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
assert "E44.0" in codes
def test_personne_agee_75_imc21_gives_E44(self):
"""≥70 ans IMC 21.0 → E44.0 (seuil gériatrique < 22)."""
dossier = _make_dossier(age=75, imc=21.0)
_detect_nutrition_has2021(dossier)
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
assert "E44.0" in codes
def test_personne_agee_75_imc19_gives_E43(self):
"""≥70 ans IMC 19.0 → E43 (sévère, IMC < 20)."""
dossier = _make_dossier(age=75, imc=19.0)
_detect_nutrition_has2021(dossier)
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
assert "E43" in codes
def test_adulte_imc25_no_das(self):
"""Adulte IMC 25.0 → aucun DAS (au-dessus du seuil)."""
dossier = _make_dossier(age=40, imc=25.0)
_detect_nutrition_has2021(dossier)
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
assert not any(c in codes for c in ("E43", "E44.0", "E46"))
def test_e46_deja_code_no_ajout(self):
"""E46 déjà codé → aucun ajout."""
dossier = _make_dossier(age=50, imc=16.0, existing_codes=["E46"])
_detect_nutrition_has2021(dossier)
e_codes = [d.cim10_suggestion for d in dossier.diagnostics_associes
if d.cim10_suggestion in ("E43", "E44.0", "E46")]
# Seul le E46 existant doit être présent
assert e_codes == ["E46"]
def test_pas_imc_no_ajout(self):
"""Pas d'IMC → aucun ajout (dégradation gracieuse)."""
dossier = _make_dossier(age=50, imc=None)
_detect_nutrition_has2021(dossier)
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
assert not any(c in codes for c in ("E43", "E44.0", "E46"))
def test_albumine_upgrade_severity(self):
"""IMC modéré + albumine < 30 → upgrade vers E43."""
dossier = _make_dossier(age=50, imc=18.0, albumine=25)
_detect_nutrition_has2021(dossier)
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
assert "E43" in codes # Albumine < 30 → sévère
def test_alerte_codage_added(self):
"""Vérifie qu'une alerte codage est ajoutée."""
dossier = _make_dossier(age=50, imc=17.0)
_detect_nutrition_has2021(dossier)
assert any("HAS 2021" in a for a in dossier.alertes_codage)
def test_source_is_has2021(self):
"""Vérifie que la source est 'has2021'."""
dossier = _make_dossier(age=50, imc=17.0)
_detect_nutrition_has2021(dossier)
has_diags = [d for d in dossier.diagnostics_associes if d.source == "has2021"]
assert len(has_diags) == 1
def test_age_inconnu_seuils_adulte(self):
"""Âge inconnu → seuils adulte par défaut."""
dossier = _make_dossier(age=None, imc=17.0)
_detect_nutrition_has2021(dossier)
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
assert "E43" in codes # IMC ≤ 17 → sévère (seuils adulte)
def test_personne_agee_70_exact_seuil(self):
"""70 ans exactement → utilise les seuils gériatriques."""
dossier = _make_dossier(age=70, imc=21.5)
_detect_nutrition_has2021(dossier)
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
assert "E44.0" in codes # < 22 → modéré avec seuils ≥70
# ── Tests regex albumine (bio_extraction) ────────────────────────────
class TestAlbumineRegex:
"""Vérifie l'extraction regex de l'albumine."""
ALBUMINE_PATTERN = r"(?:[Aa]lbumin[ée]?(?:mie)?|[Aa]lb(?:u)?[ée]?(?:mie)?)\s*[=:àa]?\s*(\d+(?:[.,]\d+)?)\s*(?:g/[Ll])?"
PREALBUMINE_PATTERN = r"(?:[Pp]r[ée]albumine|[Tt]ransthyr[ée]tine)\s*[=:àa]?\s*(\d+(?:[.,]\d+)?)\s*(?:mg/[Ll]|g/[Ll])?"
def test_albumine_standard(self):
m = re.search(self.ALBUMINE_PATTERN, "Albumine = 28 g/L")
assert m and m.group(1) == "28"
def test_albumine_colon(self):
m = re.search(self.ALBUMINE_PATTERN, "albumine: 32.5 g/L")
assert m and m.group(1) == "32.5"
def test_albumine_sans_unite(self):
m = re.search(self.ALBUMINE_PATTERN, "Albumine 28")
assert m and m.group(1) == "28"
def test_albuminemie(self):
m = re.search(self.ALBUMINE_PATTERN, "Albuminémie à 25 g/L")
assert m and m.group(1) == "25"
def test_prealbumine(self):
m = re.search(self.PREALBUMINE_PATTERN, "Préalbumine = 0.15 g/L")
assert m and m.group(1) == "0.15"
def test_transthyretine(self):
m = re.search(self.PREALBUMINE_PATTERN, "Transthyrétine: 180 mg/L")
assert m and m.group(1) == "180"
# ── Tests regex texte dénutrition (DAS patterns) ────────────────────
class TestDenutritionRegexSeverity:
"""Vérifie que les patterns textuels de dénutrition détectent la sévérité."""
def _match_pattern(self, text):
"""Retourne le (label, code) du premier pattern DAS matché."""
text_norm = normalize_text(text.lower())
for pat, label, code in _DAS_PATTERNS:
if re.search(pat, text_norm):
return label, code
return None, None
def test_denutrition_severe_gives_E43(self):
_, code = self._match_pattern("denutrition severe")
assert code == "E43"
def test_denutrition_moderee_gives_E44(self):
_, code = self._match_pattern("denutrition moderee")
assert code == "E44.0"
def test_denutrition_generic_gives_E46(self):
_, code = self._match_pattern("denutrition")
assert code == "E46"
def test_malnutrition_severe_gives_E43(self):
_, code = self._match_pattern("malnutrition severe")
assert code == "E43"
def test_denutrition_grade_iii_gives_E43(self):
_, code = self._match_pattern("denutrition grade III")
assert code == "E43"
def test_hypoalbuminemie_severe_gives_E46(self):
"""hypoalbuminemie severe → E46 (pattern générique)."""
_, code = self._match_pattern("hypoalbuminemie severe")
assert code == "E46"