Files
aivanov_CIM/tests/test_clinical_facts_extractor.py
2026-03-05 01:20:14 +01:00

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"])