Files
t2a/tests/test_dp_scoring.py
dom aa501789fd feat: scoring DP déterministe + parser CPAM nouveau format + sections CRH
- Nouveau module dp_scoring.py : shortlist, scoring multi-critères, select_dp,
  LLM one-shot fallback avec garde-fous (négation, comorbidité, Z/R-codes)
- Parser CPAM : auto-détection format legacy/ucr_extract, 6 nouveaux champs
  ControleCPAM (codes_etablissement, libelle, codes_retenus, ghm_ghs)
- CRH parser : 3 nouvelles sections (diag_sortie, diag_principal, synthese)
- Prompt DP_LLM_ONESHOT externalisé dans templates.py
- Propagation dp_selection dans fusion.py
- 808 tests passent (dont 21 nouveaux CPAM + 77 dp_scoring + 8 CRH)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 22:28:59 +01:00

711 lines
29 KiB
Python

"""Tests pour le module de scoring DP (Diagnostic Principal)."""
import pytest
from src.config import (
DossierMedical,
Diagnostic,
DPCandidate,
DPSelection,
DP_SCORING_WEIGHTS,
DP_REVIEW_THRESHOLD,
Sejour,
)
from src.medical.dp_scoring import (
build_dp_shortlist,
score_candidates,
select_dp,
_get_context_window,
_is_z_code_whitelisted,
_is_comorbidity_code,
_has_explicit_pec_proof,
_dedup_by_code,
_normalize_evidence_section,
)
# --- Helpers ---
def _make_parsed(sections: dict | None = None, diagnostics: list | None = None) -> dict:
return {
"type": "crh",
"patient": {"sexe": "M"},
"sejour": {},
"diagnostics": diagnostics or [],
"sections": sections or {},
}
def _make_candidate(
code: str = "K85.1",
label: str = "Pancréatite aiguë biliaire",
source_section: str = "diag_sortie",
**kwargs,
) -> DPCandidate:
return DPCandidate(code=code, label=label, source_section=source_section, **kwargs)
# === Tests build_dp_shortlist ===
class TestBuildDPShortlist:
def test_from_diag_sortie_with_cim10_code(self):
parsed = _make_parsed(sections={
"diag_sortie": "Pancréatite aiguë biliaire K85.1",
})
dossier = DossierMedical()
candidates = build_dp_shortlist(parsed, "", None, dossier)
codes = [c.code for c in candidates]
assert "K85.1" in codes
def test_from_diag_principal_section(self):
parsed = _make_parsed(sections={
"diag_principal": "Embolie pulmonaire I26.9",
})
dossier = DossierMedical()
candidates = build_dp_shortlist(parsed, "", None, dossier)
codes = [c.code for c in candidates]
assert "I26.9" in codes
def test_from_conclusion_via_cim10_map(self):
parsed = _make_parsed(sections={
"conclusion": "pancréatite aiguë biliaire, bonne évolution",
})
dossier = DossierMedical()
candidates = build_dp_shortlist(parsed, "", None, dossier)
codes = [c.code for c in candidates]
assert "K85.1" in codes
def test_from_regex_fallback(self):
parsed = _make_parsed(sections={})
text = "Au total : pancréatite aiguë biliaire.\nDevenir : retour."
dossier = DossierMedical()
candidates = build_dp_shortlist(parsed, text, None, dossier)
codes = [c.code for c in candidates]
assert "K85.1" in codes
def test_from_edsnlp(self):
from src.medical.edsnlp_pipeline import EdsnlpResult, CIM10Entity
parsed = _make_parsed(sections={})
edsnlp = EdsnlpResult(cim10_entities=[
CIM10Entity(texte="douleur abdominale", code="R10.4", negation=False),
])
dossier = DossierMedical()
candidates = build_dp_shortlist(parsed, "", edsnlp, dossier)
codes = [c.code for c in candidates]
assert "R10.4" in codes
def test_edsnlp_negated_excluded(self):
from src.medical.edsnlp_pipeline import EdsnlpResult, CIM10Entity
parsed = _make_parsed(sections={})
edsnlp = EdsnlpResult(cim10_entities=[
CIM10Entity(texte="fièvre", code="R50.9", negation=True),
])
dossier = DossierMedical()
candidates = build_dp_shortlist(parsed, "", edsnlp, dossier)
codes = [c.code for c in candidates]
assert "R50.9" not in codes
def test_dedup_keeps_strongest_section(self):
"""Si le même code vient de diag_sortie et conclusion, garder diag_sortie."""
parsed = _make_parsed(sections={
"diag_sortie": "Pancréatite K85.1",
"conclusion": "pancréatite K85.1 bonne évolution",
})
dossier = DossierMedical()
candidates = build_dp_shortlist(parsed, "", None, dossier)
k85_candidates = [c for c in candidates if c.code == "K85.1"]
assert len(k85_candidates) == 1
assert k85_candidates[0].source_section == "diag_sortie"
def test_empty_sections_returns_empty(self):
parsed = _make_parsed(sections={})
dossier = DossierMedical()
candidates = build_dp_shortlist(parsed, "Patient en bon état.", None, dossier)
assert candidates == []
# === Tests score_candidates ===
class TestScoreCandidates:
def test_section_bonus_diag_sortie(self):
c = _make_candidate(source_section="diag_sortie")
scored = score_candidates([c], DossierMedical())
assert scored[0].score_details.get("section") == DP_SCORING_WEIGHTS["section_diag_sortie"]
def test_section_bonus_conclusion(self):
c = _make_candidate(source_section="conclusion")
scored = score_candidates([c], DossierMedical())
assert scored[0].score_details.get("section") == DP_SCORING_WEIGHTS["section_conclusion"]
def test_section_bonus_edsnlp(self):
c = _make_candidate(source_section="edsnlp")
scored = score_candidates([c], DossierMedical())
assert scored[0].score_details.get("section") == DP_SCORING_WEIGHTS["section_edsnlp"]
def test_proof_excerpt_bonus(self):
c = _make_candidate(source_excerpt="Pancréatite aiguë biliaire confirmée au scanner")
scored = score_candidates([c], DossierMedical())
assert scored[0].score_details.get("proof_excerpt") == DP_SCORING_WEIGHTS["proof_excerpt"]
def test_no_proof_bonus_without_excerpt(self):
c = _make_candidate(source_excerpt=None)
scored = score_candidates([c], DossierMedical())
assert "proof_excerpt" not in scored[0].score_details
def test_negation_penalty(self):
c = _make_candidate(label="Fièvre")
text = "Pas de fièvre constatée."
scored = score_candidates([c], DossierMedical(), full_text=text)
assert scored[0].is_negated is True
assert scored[0].score_details.get("negation") == DP_SCORING_WEIGHTS["negation"]
def test_conditional_penalty(self):
c = _make_candidate(label="Embolie pulmonaire", code="I26.9")
text = "Embolie pulmonaire suspectée, à confirmer par angioscanner."
scored = score_candidates([c], DossierMedical(), full_text=text)
assert scored[0].is_conditional is True
assert scored[0].score_details.get("conditional") == DP_SCORING_WEIGHTS["conditional"]
def test_z_code_penalty(self):
c = _make_candidate(code="Z76.0", label="Bilan de santé", source_section="conclusion")
scored = score_candidates([c], DossierMedical())
assert scored[0].score_details.get("z_code_dp") == DP_SCORING_WEIGHTS["z_code_dp"]
def test_z_code_whitelist_no_penalty(self):
c = _make_candidate(code="Z51.1", label="Chimiothérapie", source_section="conclusion")
scored = score_candidates([c], DossierMedical())
assert "z_code_dp" not in scored[0].score_details
def test_r_code_penalty(self):
c = _make_candidate(code="R10.4", label="Douleur abdominale", source_section="edsnlp")
scored = score_candidates([c], DossierMedical())
assert scored[0].score_details.get("r_code_dp") == DP_SCORING_WEIGHTS["r_code_dp"]
def test_sort_by_score_descending(self):
c1 = _make_candidate(code="K85.1", source_section="diag_sortie")
c2 = _make_candidate(code="R10.4", label="Douleur", source_section="edsnlp")
scored = score_candidates([c2, c1], DossierMedical())
assert scored[0].code == "K85.1" # diag_sortie score > edsnlp
def test_combined_scoring(self):
"""Score = section bonus + proof - negation penalties."""
c = _make_candidate(
code="K85.1",
source_section="diag_sortie",
source_excerpt="Pancréatite aiguë",
)
scored = score_candidates([c], DossierMedical())
expected = DP_SCORING_WEIGHTS["section_diag_sortie"] + DP_SCORING_WEIGHTS["proof_excerpt"]
assert scored[0].score == expected
# === Tests select_dp ===
class TestSelectDP:
def test_no_candidates_returns_review(self):
sel = select_dp([], DossierMedical())
assert sel.verdict == "review"
def test_single_candidate_confirmed(self):
c = _make_candidate()
c.score = 6
sel = select_dp([c], DossierMedical())
assert sel.verdict == "confirmed"
assert sel.winner_reason == "candidat unique"
def test_clear_winner_confirmed(self):
c1 = _make_candidate(code="K85.1")
c1.score = 6
c2 = _make_candidate(code="R10.4", label="Douleur", source_section="edsnlp")
c2.score = 1
sel = select_dp([c1, c2], DossierMedical())
assert sel.verdict == "confirmed"
assert "delta" in sel.winner_reason
def test_close_scores_returns_review(self):
c1 = _make_candidate(code="K85.1")
c1.score = 3
c2 = _make_candidate(code="K80.5", label="Lithiase", source_section="conclusion")
c2.score = 2
sel = select_dp([c1, c2], DossierMedical())
assert sel.verdict == "review"
def test_review_returns_top3(self):
candidates = [
_make_candidate(code=f"K8{i}.{i}", label=f"Diag {i}")
for i in range(5)
]
for i, c in enumerate(candidates):
c.score = 5 - i
# delta between top1 and top2 = 1, < DP_REVIEW_THRESHOLD
sel = select_dp(candidates, DossierMedical())
assert sel.verdict == "review"
assert len(sel.candidates) <= 3
# === Tests utilitaires ===
class TestContextWindow:
def test_finds_label_in_text(self):
text = "Patient admis pour pancréatite aiguë biliaire confirmée."
window = _get_context_window(text, "pancréatite aiguë", radius=50)
assert "pancréatite" in window.lower()
def test_returns_empty_when_not_found(self):
text = "Patient en bon état."
window = _get_context_window(text, "embolie pulmonaire")
assert window == ""
class TestZCodeWhitelist:
def test_z51_1_whitelisted(self):
assert _is_z_code_whitelisted("Z51.1") is True
def test_z45_prefix_whitelisted(self):
assert _is_z_code_whitelisted("Z45.80") is True
def test_z76_not_whitelisted(self):
assert _is_z_code_whitelisted("Z76.0") is False
class TestDedupByCode:
def test_dedup_same_code_keeps_strongest(self):
c1 = _make_candidate(code="K85.1", source_section="conclusion")
c2 = _make_candidate(code="K85.1", source_section="diag_sortie")
priority = ["diag_sortie", "diag_principal", "motif_hospitalisation", "conclusion", "synthese"]
result = _dedup_by_code([c1, c2], priority)
assert len(result) == 1
assert result[0].source_section == "diag_sortie"
def test_dedup_different_codes_kept(self):
c1 = _make_candidate(code="K85.1")
c2 = _make_candidate(code="K80.5", label="Lithiase")
priority = ["diag_sortie"]
result = _dedup_by_code([c1, c2], priority)
assert len(result) == 2
# === Tests intégration légère ===
class TestDPScoringIntegration:
def test_crh_with_diag_sortie_section(self):
"""Un CRH avec section 'Diagnostic de sortie' produit un dp_selection."""
from src.medical.cim10_extractor import extract_medical_info
parsed = {
"type": "crh",
"patient": {"sexe": "M"},
"sejour": {},
"diagnostics": [],
"sections": {
"diag_sortie": "Pancréatite aiguë biliaire K85.1",
},
}
text = "Diagnostic de sortie :\nPancréatite aiguë biliaire K85.1\n\nTraitement de sortie :\nParacétamol"
dossier = extract_medical_info(parsed, text)
assert dossier.diagnostic_principal is not None
assert dossier.diagnostic_principal.cim10_suggestion == "K85.1"
assert dossier.dp_selection is not None
assert dossier.dp_selection.verdict == "confirmed"
def test_llm_fallback_confirmed_high_strong_section(self):
"""LLM one-shot CONFIRMED : high confidence + section forte."""
from unittest.mock import patch
from src.medical.cim10_extractor import extract_medical_info
parsed = {
"type": "crh",
"patient": {"sexe": "M"},
"sejour": {},
"diagnostics": [],
"sections": {
"conclusion": "Pancréatite aiguë biliaire avec HTA connue.",
},
}
text = "Conclusion : Pancréatite aiguë biliaire avec HTA connue."
mock_result = {
"dp_code": "K85.1",
"dp_label": "Pancréatite aiguë biliaire",
"evidence_section": "conclusion",
"evidence_excerpt": "Pancréatite aiguë biliaire",
"confidence": "high",
}
with patch("src.medical.ollama_client.call_ollama", return_value=mock_result):
dossier = extract_medical_info(parsed, text, use_rag=True)
assert dossier.dp_selection is not None
assert dossier.dp_selection.verdict == "confirmed"
assert dossier.diagnostic_principal is not None
assert dossier.diagnostic_principal.cim10_suggestion == "K85.1"
def test_llm_fallback_confirmed_conclusion_section(self):
"""LLM one-shot CONFIRMED : conclusion est section forte."""
from unittest.mock import patch
from src.medical.cim10_extractor import extract_medical_info
parsed = {
"type": "crh",
"patient": {"sexe": "M"},
"sejour": {},
"diagnostics": [],
"sections": {"conclusion": "Pneumopathie avec insuffisance rénale aiguë."},
}
text = "Conclusion : Pneumopathie avec insuffisance rénale aiguë."
mock_result = {
"dp_code": "J18.9",
"dp_label": "Pneumopathie, sans précision",
"evidence_section": "conclusion",
"evidence_excerpt": "Pneumopathie avec insuffisance rénale aiguë",
"confidence": "high",
}
with patch("src.medical.ollama_client.call_ollama", return_value=mock_result):
dossier = extract_medical_info(parsed, text, use_rag=True)
assert dossier.dp_selection is not None
assert dossier.dp_selection.verdict == "confirmed"
assert dossier.diagnostic_principal is not None
def test_llm_fallback_review_weak_section(self):
"""LLM one-shot REVIEW : evidence de histoire_maladie (section faible) → guardrail."""
from unittest.mock import patch
from src.medical.dp_scoring import llm_dp_fallback
from src.config import DossierMedical, DPCandidate
parsed = {"type": "crh", "sections": {"histoire_maladie": "Dyspnée aiguë."}}
text = "Histoire de la maladie : Dyspnée aiguë."
dossier = DossierMedical()
dp_candidates = [DPCandidate(code="R06.0", label="Dyspnée", source_section="edsnlp")]
mock_result = {
"dp_code": "R06.0",
"dp_label": "Dyspnée",
"evidence_section": "histoire_maladie",
"evidence_excerpt": "Dyspnée aiguë",
"confidence": "high",
}
with patch("src.medical.ollama_client.call_ollama", return_value=mock_result):
selection = llm_dp_fallback(parsed, text, dossier, dp_candidates=dp_candidates)
assert selection.verdict == "review"
assert len(selection.candidates) >= 1
def test_llm_fallback_review_low_confidence(self):
"""LLM one-shot REVIEW : confidence=medium → guardrail."""
from unittest.mock import patch
from src.medical.dp_scoring import llm_dp_fallback
from src.config import DossierMedical, DPCandidate
parsed = {"type": "crh", "sections": {"conclusion": "HTA connue, diabète équilibré."}}
text = "Conclusion : HTA connue, diabète équilibré."
dossier = DossierMedical()
dp_candidates = [DPCandidate(code="I10", label="HTA", source_section="edsnlp")]
mock_result = {
"dp_code": "I10",
"dp_label": "Hypertension essentielle",
"evidence_section": "conclusion",
"evidence_excerpt": "HTA connue",
"confidence": "medium",
}
with patch("src.medical.ollama_client.call_ollama", return_value=mock_result):
selection = llm_dp_fallback(parsed, text, dossier, dp_candidates=dp_candidates)
assert selection.verdict == "review"
assert "confidence medium" in selection.winner_reason
def test_llm_fallback_guardrail_no_evidence(self):
"""Garde-fou : LLM renvoie evidence vide → REVIEW."""
from unittest.mock import patch
from src.medical.dp_scoring import llm_dp_fallback
from src.config import DossierMedical, DPCandidate
parsed = {"type": "crh", "sections": {"conclusion": "Pancréatite."}}
text = "Conclusion : Pancréatite."
dossier = DossierMedical()
dp_candidates = [DPCandidate(code="K85.9", label="Pancréatite", source_section="edsnlp")]
mock_result = {
"dp_code": "K85.9",
"dp_label": "Pancréatite aiguë",
"evidence_section": "conclusion",
"evidence_excerpt": "",
"confidence": "high",
}
with patch("src.medical.ollama_client.call_ollama", return_value=mock_result):
selection = llm_dp_fallback(parsed, text, dossier, dp_candidates=dp_candidates)
assert selection.verdict == "review"
def test_llm_fallback_guardrail_comorbidity_weak_section(self):
"""Garde-fou : HTA en section non-forte → REVIEW."""
from unittest.mock import patch
from src.medical.dp_scoring import llm_dp_fallback
from src.config import DossierMedical, DPCandidate
parsed = {"type": "crh", "sections": {"histoire_maladie": "Patient hypertendu."}}
text = "Histoire de la maladie : Patient hypertendu."
dossier = DossierMedical()
dp_candidates = [DPCandidate(code="I10", label="HTA", source_section="edsnlp")]
mock_result = {
"dp_code": "I10",
"dp_label": "Hypertension essentielle",
"evidence_section": "histoire_maladie",
"evidence_excerpt": "Patient hypertendu",
"confidence": "high",
}
with patch("src.medical.ollama_client.call_ollama", return_value=mock_result):
selection = llm_dp_fallback(parsed, text, dossier, dp_candidates=dp_candidates)
assert selection.verdict == "review"
def test_llm_fallback_comorbidity_in_strong_section(self):
"""I10 en section forte + high confidence → CONFIRMED (garde-fou GF-2 ne bloque pas)."""
from unittest.mock import patch
from src.medical.dp_scoring import llm_dp_fallback
from src.config import DossierMedical, DPCandidate
parsed = {"type": "crh", "sections": {"motif_hospitalisation": "HTA maligne."}}
text = "Motif d'hospitalisation : HTA maligne."
dossier = DossierMedical()
dp_candidates = [DPCandidate(code="I10", label="HTA", source_section="edsnlp")]
mock_result = {
"dp_code": "I10",
"dp_label": "Hypertension essentielle",
"evidence_section": "motif_hospitalisation",
"evidence_excerpt": "HTA maligne",
"confidence": "high",
}
with patch("src.medical.ollama_client.call_ollama", return_value=mock_result):
selection = llm_dp_fallback(parsed, text, dossier, dp_candidates=dp_candidates)
assert selection.verdict == "confirmed"
assert selection.candidates[0].code == "I10"
def test_no_llm_fallback_without_use_rag(self):
"""Sans use_rag, le fallback LLM ne se déclenche PAS."""
from src.medical.cim10_extractor import extract_medical_info
parsed = {
"type": "crh",
"patient": {"sexe": "M"},
"sejour": {},
"diagnostics": [],
"sections": {"conclusion": "Bonne évolution."},
}
text = "Conclusion : Bonne évolution."
dossier = extract_medical_info(parsed, text, use_rag=False)
# Sans use_rag → pas de fallback LLM → verdict review
assert dossier.dp_selection is not None
assert dossier.dp_selection.verdict == "review"
def test_trackare_dp_bypasses_scoring(self):
"""Un Trackare avec DP codé ne déclenche PAS le scoring."""
from src.medical.cim10_extractor import extract_medical_info
parsed = {
"type": "trackare",
"patient": {"sexe": "F"},
"sejour": {"date_entree": "01/01/2024", "date_sortie": "05/01/2024"},
"diagnostics": [
{"type": "Principal", "code_cim10": "K80.5", "libelle": "Calcul des canaux biliaires"},
],
}
text = "Calcul des canaux biliaires."
dossier = extract_medical_info(parsed, text)
assert dossier.diagnostic_principal is not None
assert dossier.diagnostic_principal.cim10_suggestion == "K80.5"
assert dossier.dp_selection is None # Trackare DP, pas de scoring
# === Tests comorbidité-banale DP ===
class TestComorbidityGuard:
"""Règle comorbidité-banale : I10/E66.x/E78.x/E11.x/D64.9 en DP → REVIEW
sauf preuve explicite de PEC principale."""
def test_is_comorbidity_expanded(self):
"""La liste élargie couvre I10, E66.*, E78.*, E11.*, D64.9."""
assert _is_comorbidity_code("I10") is True
assert _is_comorbidity_code("E66.0") is True
assert _is_comorbidity_code("E66.9") is True
assert _is_comorbidity_code("E78.0") is True
assert _is_comorbidity_code("E11.9") is True
assert _is_comorbidity_code("E11.0") is True
assert _is_comorbidity_code("D64.9") is True
# Pas comorbidité
assert _is_comorbidity_code("D64.0") is False
assert _is_comorbidity_code("E10.9") is False
assert _is_comorbidity_code("K85.1") is False
def test_sole_comorbidity_review(self):
"""Candidat unique comorbidité → REVIEW (même section forte)."""
c = _make_candidate(code="E66.0", label="Obésité", source_section="conclusion")
c.score = 4
c.score_details = {"section": 2, "proof_excerpt": 2, "comorbidity_weak": -3}
sel = select_dp([c], DossierMedical())
assert sel.verdict == "review"
assert "comorbidité banale" in sel.winner_reason
def test_comorbidity_top1_multi_review(self):
"""Comorbidité top1 parmi plusieurs → REVIEW."""
c1 = _make_candidate(code="I10", label="Hta", source_section="motif_hospitalisation")
c1.score = 3
c1.score_details = {"section": 3, "comorbidity_weak": -3}
c2 = _make_candidate(code="K85.1", label="Pancréatite", source_section="edsnlp")
c2.score = 1
sel = select_dp([c1, c2], DossierMedical())
assert sel.verdict == "review"
assert "comorbidité banale" in sel.winner_reason
def test_comorbidity_with_pec_proof_confirmed(self):
"""Comorbidité + preuve PEC → CONFIRMED."""
c = _make_candidate(code="I10", label="Hta", source_section="motif_hospitalisation")
c.score = 3
c.score_details = {"section": 3, "comorbidity_weak": -3, "comorbidity_pec_proof": 3}
sel = select_dp([c], DossierMedical())
assert sel.verdict == "confirmed"
assert sel.winner_reason == "candidat unique"
def test_non_comorbidity_sole_confirmed(self):
"""Candidat unique non-comorbidité → CONFIRMED (pas affecté)."""
c = _make_candidate(code="K85.1", label="Pancréatite", source_section="conclusion")
c.score = 4
sel = select_dp([c], DossierMedical())
assert sel.verdict == "confirmed"
def test_score_comorbidity_penalty_strong_section(self):
"""Comorbidité pénalisée même en section forte (conclusion)."""
c = _make_candidate(code="E66.0", label="Obésité", source_section="conclusion")
scored = score_candidates([c], DossierMedical())
assert "comorbidity_weak" in scored[0].score_details
assert scored[0].score_details["comorbidity_weak"] == DP_SCORING_WEIGHTS["comorbidity_weak"]
def test_score_comorbidity_penalty_motif(self):
"""Comorbidité pénalisée en motif_hospitalisation."""
c = _make_candidate(code="I10", label="Hta", source_section="motif_hospitalisation")
scored = score_candidates([c], DossierMedical())
assert "comorbidity_weak" in scored[0].score_details
def test_pec_proof_detected(self):
"""PEC proof détectée dans le texte → bonus dans score_details."""
c = _make_candidate(code="I10", label="Hta", source_section="motif_hospitalisation")
text = "Patient hospitalisé pour hta maligne résistante au traitement."
scored = score_candidates([c], DossierMedical(), full_text=text)
assert "comorbidity_pec_proof" in scored[0].score_details
assert scored[0].score_details["comorbidity_pec_proof"] > 0
def test_pec_proof_not_found(self):
"""Pas de PEC proof → pas de bonus."""
c = _make_candidate(code="E66.0", label="Obésité", source_section="conclusion")
text = "Patient obèse, pneumopathie communautaire."
scored = score_candidates([c], DossierMedical(), full_text=text)
assert "comorbidity_pec_proof" not in scored[0].score_details
def test_has_explicit_pec_proof_hospitalized(self):
"""Détection 'hospitalisé pour' + label."""
assert _has_explicit_pec_proof("hta", "Patient hospitalisé pour HTA maligne.") is True
def test_has_explicit_pec_proof_prise_en_charge(self):
"""Détection 'prise en charge' + label."""
assert _has_explicit_pec_proof("obésité", "Prise en charge de l'obésité morbide.") is True
def test_has_explicit_pec_proof_absent(self):
"""Pas de PEC proof pour un label non mentionné."""
assert _has_explicit_pec_proof("hta", "Patient admis pour douleur thoracique.") is False
def test_has_explicit_pec_proof_admission(self):
"""Détection 'admission pour' + label."""
assert _has_explicit_pec_proof("diabète", "Admission pour diabète déséquilibré.") is True
class TestSectionNormalization:
"""Tests pour _normalize_evidence_section — normalisation robuste."""
# --- Correspondances exactes existantes ---
def test_exact_conclusion(self):
assert _normalize_evidence_section("conclusion") == "conclusion"
def test_exact_synthese(self):
assert _normalize_evidence_section("synthèse") == "synthese"
def test_exact_motif_hospitalisation(self):
assert _normalize_evidence_section("motif_hospitalisation") == "motif_hospitalisation"
# --- Nouveaux alias exacts ---
def test_synthese_du_sejour(self):
assert _normalize_evidence_section("synthèse du séjour") == "synthese"
def test_synthese_du_sejour_ascii(self):
assert _normalize_evidence_section("synthese du sejour") == "synthese"
def test_conclusions_pluriel(self):
assert _normalize_evidence_section("conclusions") == "conclusion"
def test_secretariat_to_autres(self):
assert _normalize_evidence_section("secrétariat") == "autres"
def test_medecine_interne_to_autres(self):
assert _normalize_evidence_section("médecine interne") == "autres"
def test_sections_cliniques_to_autres(self):
assert _normalize_evidence_section("sections cliniques") == "autres"
# --- Nettoyage crochets/guillemets ---
def test_brackets_conclusion(self):
assert _normalize_evidence_section("[conclusion]") == "conclusion"
def test_brackets_motif(self):
assert _normalize_evidence_section("[motif_hospitalisation]") == "motif_hospitalisation"
def test_colon_conclusion(self):
assert _normalize_evidence_section("conclusion:") == "conclusion"
def test_quotes_synthese(self):
assert _normalize_evidence_section('"synthèse"') == "synthese"
# --- Fallback par mots-clés ---
def test_keyword_conclusion_du_sejour(self):
assert _normalize_evidence_section("conclusion du séjour") == "conclusion"
def test_keyword_synthese_medicale(self):
assert _normalize_evidence_section("synthèse médicale du dossier") == "synthese"
def test_keyword_diagnostic_de_sortie_variant(self):
assert _normalize_evidence_section("diagnostic(s) de sortie") == "diag_sortie"
def test_keyword_diagnostic_retenu_variant(self):
assert _normalize_evidence_section("diagnostics retenus à la sortie") == "diagnostics_retenus"
def test_keyword_motif_admission(self):
assert _normalize_evidence_section("motif d'admission aux urgences") == "motif_hospitalisation"
# --- Cas limites ---
def test_empty_string(self):
assert _normalize_evidence_section("") == ""
def test_none_like_empty(self):
assert _normalize_evidence_section(" ") == ""
def test_unknown_section_passthrough(self):
"""Section inconnue sans mot-clé → passthrough nettoyé."""
result = _normalize_evidence_section("biologie")
assert result == "biologie"
def test_sections_fortes_du_dossier(self):
"""Alias administratif observé en benchmark."""
assert _normalize_evidence_section("sections fortes du dossier") == "autres"