483 lines
18 KiB
Python
483 lines
18 KiB
Python
"""
|
|
Tests unitaires pour le ClinicalFactsExtractor.
|
|
|
|
Ces tests vérifient l'extraction de faits cliniques, la détection de qualificateurs
|
|
(négation, suspicion, temporalité) et l'association de preuves textuelles.
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime
|
|
|
|
from src.pipeline_mco_pmsi.extractors.clinical_facts_extractor import ClinicalFactsExtractor
|
|
from src.pipeline_mco_pmsi.models.clinical import (
|
|
ClinicalDocument,
|
|
Section,
|
|
Span,
|
|
StructuredStay,
|
|
Qualifier,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def extractor():
|
|
"""Fixture pour créer un extracteur de faits cliniques."""
|
|
return ClinicalFactsExtractor()
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_document():
|
|
"""Fixture pour créer un document clinique de test."""
|
|
return ClinicalDocument(
|
|
document_id="doc_001",
|
|
document_type="cr_medical",
|
|
content="Diagnostic: Gastrite aiguë. Traitement: Oméprazole 20mg.",
|
|
creation_date=datetime(2024, 1, 15, 10, 30),
|
|
author="Dr. Martin",
|
|
priority=2,
|
|
)
|
|
|
|
|
|
class TestQualifierDetection:
|
|
"""Tests pour la détection de qualificateurs."""
|
|
|
|
def test_detect_negation_pas_de(self, extractor):
|
|
"""Test détection de négation avec 'pas de'."""
|
|
text = "Le patient ne présente pas de gastrite."
|
|
qualifier = extractor.detect_qualifiers(text, "gastrite")
|
|
|
|
assert qualifier.certainty == "nié"
|
|
assert len(qualifier.markers) > 0
|
|
assert qualifier.confidence < 0.5
|
|
|
|
def test_detect_negation_absence_de(self, extractor):
|
|
"""Test détection de négation avec 'absence de'."""
|
|
text = "Absence de signes d'infection."
|
|
qualifier = extractor.detect_qualifiers(text, "infection")
|
|
|
|
assert qualifier.certainty == "nié"
|
|
assert "absence de" in " ".join(qualifier.markers).lower()
|
|
assert qualifier.confidence < 0.5
|
|
|
|
def test_detect_negation_sans(self, extractor):
|
|
"""Test détection de négation avec 'sans'."""
|
|
text = "Examen sans particularité, sans fièvre."
|
|
qualifier = extractor.detect_qualifiers(text, "fièvre")
|
|
|
|
assert qualifier.certainty == "nié"
|
|
assert qualifier.confidence < 0.5
|
|
|
|
def test_detect_suspicion_possible(self, extractor):
|
|
"""Test détection de suspicion avec 'possible'."""
|
|
text = "Pneumonie possible à confirmer."
|
|
qualifier = extractor.detect_qualifiers(text, "Pneumonie")
|
|
|
|
assert qualifier.certainty == "suspecté"
|
|
assert len(qualifier.markers) > 0
|
|
assert 0.5 <= qualifier.confidence < 0.8
|
|
|
|
def test_detect_suspicion_suspecte(self, extractor):
|
|
"""Test détection de suspicion avec 'suspecté'."""
|
|
text = "Appendicite suspectée."
|
|
qualifier = extractor.detect_qualifiers(text, "Appendicite")
|
|
|
|
assert qualifier.certainty == "suspecté"
|
|
assert qualifier.confidence < 0.8
|
|
|
|
def test_detect_suspicion_probable(self, extractor):
|
|
"""Test détection de suspicion avec 'probable'."""
|
|
text = "Diagnostic probable de gastro-entérite."
|
|
qualifier = extractor.detect_qualifiers(text, "gastro-entérite")
|
|
|
|
assert qualifier.certainty == "suspecté"
|
|
assert qualifier.confidence < 0.8
|
|
|
|
def test_detect_affirmation(self, extractor):
|
|
"""Test détection d'affirmation (pas de marqueurs)."""
|
|
text = "Le patient présente une gastrite aiguë."
|
|
qualifier = extractor.detect_qualifiers(text, "gastrite aiguë")
|
|
|
|
assert qualifier.certainty == "affirmé"
|
|
assert len(qualifier.markers) == 0
|
|
assert qualifier.confidence == 1.0
|
|
|
|
def test_negation_priority_over_suspicion(self, extractor):
|
|
"""Test que la négation a priorité sur la suspicion."""
|
|
text = "Pas de pneumonie possible."
|
|
qualifier = extractor.detect_qualifiers(text, "pneumonie")
|
|
|
|
# La négation doit avoir priorité
|
|
assert qualifier.certainty == "nié"
|
|
|
|
|
|
class TestTemporalityDetection:
|
|
"""Tests pour la détection de temporalité."""
|
|
|
|
def test_detect_antecedent_with_antecedent_keyword(self, extractor):
|
|
"""Test détection d'antécédent avec mot-clé 'antécédent'."""
|
|
text = "Antécédent de diabète de type 2."
|
|
temporality = extractor._detect_temporality(text)
|
|
|
|
assert temporality == "antecedent"
|
|
|
|
def test_detect_antecedent_with_ancien(self, extractor):
|
|
"""Test détection d'antécédent avec 'ancien'."""
|
|
text = "Ancien fumeur, ancienne fracture du poignet."
|
|
temporality = extractor._detect_temporality(text)
|
|
|
|
assert temporality == "antecedent"
|
|
|
|
def test_detect_antecedent_with_histoire_de(self, extractor):
|
|
"""Test détection d'antécédent avec 'histoire de'."""
|
|
text = "Histoire de cancer du sein traité."
|
|
temporality = extractor._detect_temporality(text)
|
|
|
|
assert temporality == "antecedent"
|
|
|
|
def test_detect_chronique(self, extractor):
|
|
"""Test détection de condition chronique."""
|
|
text = "Insuffisance rénale chronique."
|
|
temporality = extractor._detect_temporality(text)
|
|
|
|
assert temporality == "chronique"
|
|
|
|
def test_detect_chronique_persistant(self, extractor):
|
|
"""Test détection de condition chronique avec 'persistant'."""
|
|
text = "Douleurs persistantes depuis 6 mois."
|
|
temporality = extractor._detect_temporality(text)
|
|
|
|
assert temporality == "chronique"
|
|
|
|
def test_detect_actuel_by_default(self, extractor):
|
|
"""Test que 'actuel' est la temporalité par défaut."""
|
|
text = "Le patient présente une gastrite aiguë."
|
|
temporality = extractor._detect_temporality(text)
|
|
|
|
assert temporality == "actuel"
|
|
|
|
|
|
class TestFactExtraction:
|
|
"""Tests pour l'extraction de faits cliniques."""
|
|
|
|
def test_extract_diagnostic_from_section(self, extractor):
|
|
"""Test extraction d'un diagnostic depuis une section."""
|
|
section = Section(
|
|
section_id="doc_001_section_0",
|
|
section_type="diagnostic",
|
|
content="Diagnostic: Gastrite aiguë hémorragique.",
|
|
span=Span(start=0, end=42),
|
|
)
|
|
|
|
facts = extractor._extract_facts_from_section(section, "doc_001")
|
|
|
|
assert len(facts) > 0
|
|
diagnostic_facts = [f for f in facts if f.type == "diagnostic"]
|
|
assert len(diagnostic_facts) > 0
|
|
|
|
fact = diagnostic_facts[0]
|
|
assert "Gastrite" in fact.text or "gastrite" in fact.text.lower()
|
|
assert fact.evidence.document_id == "doc_001"
|
|
assert fact.evidence.span.start >= 0
|
|
assert fact.evidence.span.end > fact.evidence.span.start
|
|
|
|
def test_extract_traitement_from_section(self, extractor):
|
|
"""Test extraction d'un traitement depuis une section."""
|
|
section = Section(
|
|
section_id="doc_001_section_1",
|
|
section_type="traitement",
|
|
content="Traitement: Oméprazole 20mg 2 fois par jour.",
|
|
span=Span(start=50, end=95),
|
|
)
|
|
|
|
facts = extractor._extract_facts_from_section(section, "doc_001")
|
|
|
|
assert len(facts) > 0
|
|
traitement_facts = [f for f in facts if f.type == "traitement"]
|
|
assert len(traitement_facts) > 0
|
|
|
|
fact = traitement_facts[0]
|
|
assert "Oméprazole" in fact.text or "oméprazole" in fact.text.lower()
|
|
|
|
def test_extract_acte_from_section(self, extractor):
|
|
"""Test extraction d'un acte depuis une section."""
|
|
section = Section(
|
|
section_id="doc_001_section_2",
|
|
section_type="autre",
|
|
content="Intervention: Appendicectomie par laparoscopie.",
|
|
span=Span(start=100, end=148),
|
|
)
|
|
|
|
facts = extractor._extract_facts_from_section(section, "doc_001")
|
|
|
|
assert len(facts) > 0
|
|
acte_facts = [f for f in facts if f.type == "acte"]
|
|
assert len(acte_facts) > 0
|
|
|
|
fact = acte_facts[0]
|
|
assert "Appendicectomie" in fact.text or "appendicectomie" in fact.text.lower()
|
|
|
|
def test_extract_examen_from_section(self, extractor):
|
|
"""Test extraction d'un examen depuis une section."""
|
|
section = Section(
|
|
section_id="doc_001_section_3",
|
|
section_type="examen",
|
|
content="Scanner: Lésion hépatique de 3cm.",
|
|
span=Span(start=150, end=185),
|
|
)
|
|
|
|
facts = extractor._extract_facts_from_section(section, "doc_001")
|
|
|
|
assert len(facts) > 0
|
|
examen_facts = [f for f in facts if f.type == "examen"]
|
|
assert len(examen_facts) > 0
|
|
|
|
def test_extract_facts_with_negation(self, extractor):
|
|
"""Test extraction de faits avec négation."""
|
|
section = Section(
|
|
section_id="doc_001_section_4",
|
|
section_type="diagnostic",
|
|
content="Diagnostic: Pas de signe d'infection.",
|
|
span=Span(start=200, end=238),
|
|
)
|
|
|
|
facts = extractor._extract_facts_from_section(section, "doc_001")
|
|
|
|
# Vérifier qu'au moins un fait est extrait
|
|
assert len(facts) > 0
|
|
|
|
# Vérifier que le qualificateur est "nié"
|
|
for fact in facts:
|
|
if "infection" in fact.text.lower():
|
|
assert fact.qualifier.certainty == "nié"
|
|
assert fact.confidence < 0.5
|
|
|
|
def test_extract_facts_with_suspicion(self, extractor):
|
|
"""Test extraction de faits avec suspicion."""
|
|
section = Section(
|
|
section_id="doc_001_section_5",
|
|
section_type="diagnostic",
|
|
content="Diagnostic: Pneumonie possible à confirmer.",
|
|
span=Span(start=250, end=293),
|
|
)
|
|
|
|
facts = extractor._extract_facts_from_section(section, "doc_001")
|
|
|
|
assert len(facts) > 0
|
|
|
|
for fact in facts:
|
|
if "pneumonie" in fact.text.lower():
|
|
assert fact.qualifier.certainty == "suspecté"
|
|
assert fact.confidence < 0.8
|
|
|
|
def test_extract_facts_with_antecedent(self, extractor):
|
|
"""Test extraction de faits avec antécédent."""
|
|
section = Section(
|
|
section_id="doc_001_section_6",
|
|
section_type="anamnese",
|
|
content="Antécédent de diabète de type 2.",
|
|
span=Span(start=300, end=333),
|
|
)
|
|
|
|
facts = extractor._extract_facts_from_section(section, "doc_001")
|
|
|
|
# Vérifier qu'au moins un fait est extrait
|
|
assert len(facts) > 0
|
|
|
|
# Vérifier la temporalité
|
|
for fact in facts:
|
|
if "diabète" in fact.text.lower():
|
|
assert fact.temporality == "antecedent"
|
|
|
|
|
|
class TestFactExtractionFromStay:
|
|
"""Tests pour l'extraction de faits depuis un séjour complet."""
|
|
|
|
def test_extract_facts_from_structured_stay(self, extractor):
|
|
"""Test extraction de faits depuis un séjour structuré."""
|
|
# Créer un document avec plusieurs sections
|
|
document = ClinicalDocument(
|
|
document_id="doc_001",
|
|
document_type="cr_medical",
|
|
content="Diagnostic: Gastrite aiguë. Traitement: Oméprazole 20mg.",
|
|
creation_date=datetime(2024, 1, 15, 10, 30),
|
|
author="Dr. Martin",
|
|
priority=2,
|
|
)
|
|
|
|
sections = [
|
|
Section(
|
|
section_id="doc_001_section_0",
|
|
section_type="diagnostic",
|
|
content="Diagnostic: Gastrite aiguë.",
|
|
span=Span(start=0, end=27),
|
|
),
|
|
Section(
|
|
section_id="doc_001_section_1",
|
|
section_type="traitement",
|
|
content="Traitement: Oméprazole 20mg.",
|
|
span=Span(start=28, end=56),
|
|
),
|
|
]
|
|
|
|
stay = StructuredStay(
|
|
stay_id="stay_001",
|
|
documents=[document],
|
|
sections=sections,
|
|
facts=[],
|
|
)
|
|
|
|
facts = extractor.extract_facts(stay)
|
|
|
|
# Vérifier qu'au moins 2 faits sont extraits (diagnostic + traitement)
|
|
assert len(facts) >= 2
|
|
|
|
# Vérifier les types de faits
|
|
fact_types = {f.type for f in facts}
|
|
assert "diagnostic" in fact_types or "traitement" in fact_types
|
|
|
|
# Vérifier que chaque fait a une preuve
|
|
for fact in facts:
|
|
assert fact.evidence is not None
|
|
assert fact.evidence.document_id == "doc_001"
|
|
assert fact.evidence.span.start >= 0
|
|
assert fact.evidence.span.end > fact.evidence.span.start
|
|
assert len(fact.evidence.text) > 0
|
|
|
|
def test_extract_facts_preserves_document_id(self, extractor):
|
|
"""Test que l'extraction préserve le document_id dans les preuves."""
|
|
document = ClinicalDocument(
|
|
document_id="doc_123",
|
|
document_type="cr_medical",
|
|
content="Diagnostic: Hypertension artérielle.",
|
|
creation_date=datetime(2024, 1, 15, 10, 30),
|
|
author="Dr. Dupont",
|
|
priority=2,
|
|
)
|
|
|
|
section = Section(
|
|
section_id="doc_123_section_0",
|
|
section_type="diagnostic",
|
|
content="Diagnostic: Hypertension artérielle.",
|
|
span=Span(start=0, end=37),
|
|
)
|
|
|
|
stay = StructuredStay(
|
|
stay_id="stay_002",
|
|
documents=[document],
|
|
sections=[section],
|
|
facts=[],
|
|
)
|
|
|
|
facts = extractor.extract_facts(stay)
|
|
|
|
# Vérifier que tous les faits ont le bon document_id
|
|
for fact in facts:
|
|
assert fact.evidence.document_id == "doc_123"
|
|
|
|
|
|
class TestConfidenceCalculation:
|
|
"""Tests pour le calcul de confiance."""
|
|
|
|
def test_confidence_high_for_affirmed_current(self, extractor):
|
|
"""Test confiance élevée pour faits affirmés actuels."""
|
|
qualifier = Qualifier(certainty="affirmé", markers=[], confidence=1.0)
|
|
confidence = extractor._calculate_confidence(qualifier, "actuel")
|
|
|
|
assert confidence >= 0.9
|
|
|
|
def test_confidence_reduced_for_suspected(self, extractor):
|
|
"""Test confiance réduite pour faits suspectés."""
|
|
qualifier = Qualifier(certainty="suspecté", markers=["possible"], confidence=0.6)
|
|
confidence = extractor._calculate_confidence(qualifier, "actuel")
|
|
|
|
assert confidence < 0.8
|
|
|
|
def test_confidence_very_low_for_negated(self, extractor):
|
|
"""Test confiance très basse pour faits niés."""
|
|
qualifier = Qualifier(certainty="nié", markers=["pas de"], confidence=0.3)
|
|
confidence = extractor._calculate_confidence(qualifier, "actuel")
|
|
|
|
assert confidence < 0.5
|
|
|
|
def test_confidence_slightly_reduced_for_antecedent(self, extractor):
|
|
"""Test confiance légèrement réduite pour antécédents."""
|
|
qualifier = Qualifier(certainty="affirmé", markers=[], confidence=1.0)
|
|
confidence = extractor._calculate_confidence(qualifier, "antecedent")
|
|
|
|
assert 0.8 <= confidence < 1.0
|
|
|
|
def test_confidence_bounds(self, extractor):
|
|
"""Test que la confiance reste dans les bornes [0.0, 1.0]."""
|
|
qualifier = Qualifier(certainty="affirmé", markers=[], confidence=1.0)
|
|
|
|
# Test avec différentes temporalités
|
|
for temporality in ["actuel", "antecedent", "chronique"]:
|
|
confidence = extractor._calculate_confidence(qualifier, temporality)
|
|
assert 0.0 <= confidence <= 1.0
|
|
|
|
|
|
class TestContextExtraction:
|
|
"""Tests pour l'extraction de contexte."""
|
|
|
|
def test_extract_context_with_window(self, extractor):
|
|
"""Test extraction de contexte avec fenêtre."""
|
|
text = "Le patient présente une gastrite aiguë hémorragique depuis 3 jours."
|
|
context = extractor._extract_context(text, 24, 38, context_size=10)
|
|
|
|
assert "gastrite" in context.lower()
|
|
assert len(context) > len("gastrite aiguë")
|
|
|
|
def test_extract_context_at_start(self, extractor):
|
|
"""Test extraction de contexte au début du texte."""
|
|
text = "Gastrite aiguë confirmée."
|
|
context = extractor._extract_context(text, 0, 14, context_size=10)
|
|
|
|
assert "gastrite" in context.lower()
|
|
# Pas d'ellipse au début
|
|
assert not context.startswith("...")
|
|
|
|
def test_extract_context_at_end(self, extractor):
|
|
"""Test extraction de contexte à la fin du texte."""
|
|
text = "Le patient présente une gastrite."
|
|
context = extractor._extract_context(text, 24, 33, context_size=10)
|
|
|
|
assert "gastrite" in context.lower()
|
|
# Pas d'ellipse à la fin
|
|
assert not context.endswith("...")
|
|
|
|
def test_get_context_window(self, extractor):
|
|
"""Test extraction de fenêtre de contexte."""
|
|
text = "Le patient ne présente pas de gastrite mais une infection."
|
|
window = extractor._get_context_window(text, 30, 38, window_size=20)
|
|
|
|
assert "pas de" in window.lower()
|
|
assert "gastrite" in window.lower()
|
|
|
|
|
|
class TestMarkerRelevance:
|
|
"""Tests pour la pertinence des marqueurs."""
|
|
|
|
def test_marker_relevant_when_close(self, extractor):
|
|
"""Test qu'un marqueur proche est considéré pertinent."""
|
|
text = "Pas de gastrite."
|
|
is_relevant = extractor._is_marker_relevant(text, 0, "gastrite")
|
|
|
|
assert is_relevant
|
|
|
|
def test_marker_not_relevant_when_far(self, extractor):
|
|
"""Test qu'un marqueur loin n'est pas considéré pertinent."""
|
|
text = "Pas de fièvre. " + "x" * 100 + " Gastrite aiguë."
|
|
is_relevant = extractor._is_marker_relevant(text, 0, "Gastrite")
|
|
|
|
assert not is_relevant
|
|
|
|
def test_marker_not_relevant_when_after_fact(self, extractor):
|
|
"""Test qu'un marqueur loin après le fait n'est pas pertinent."""
|
|
text = "Gastrite aiguë. " + "x" * 100 + " Pas de fièvre."
|
|
is_relevant = extractor._is_marker_relevant(text, len(text) - 15, "Gastrite")
|
|
|
|
assert not is_relevant
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|