feat: alias diagnostiques CIM-10 + scoring conclusion + garde-fou Trackare R-code
- DIAGNOSIS_ALIASES : mapping acronymes cliniques → CIM-10 (DLBCL→C83.3, SCA→I25.1, EP→I26.9, IDM→I21.9, etc.) - Scoring 4b étendu : conclusion (+2) ajouté aux sections diagnostiques, matching par alias en plus du terme/code - _collect_evidence : détection alias dans les sections pour preuves plus complètes - Garde-fou Trackare : si DP est un R-code (symptôme) et que les sections CRH mentionnent un diagnostic étiologique via alias → verdict REVIEW au lieu de CONFIRMED, alerte DIM - Case 74 : verdict attendu REVIEW (conclusion mentionne les 2 diagnostics, delta insuffisant) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,36 @@ SECTION_STRENGTH = {
|
|||||||
"nuke3": 0, # pas de self-boost
|
"nuke3": 0, # pas de self-boost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Alias cliniques courants → code CIM-10 (pour matching sections diagnostiques)
|
||||||
|
# Permet de matcher "DLBCL" dans une section avec le candidat C83.3
|
||||||
|
DIAGNOSIS_ALIASES: dict[str, str] = {
|
||||||
|
"dlbcl": "C83.3",
|
||||||
|
"lymphome diffus à grandes cellules b": "C83.3",
|
||||||
|
"lymphome b diffus grandes cellules": "C83.3",
|
||||||
|
"lymphome diffus à grandes cellules": "C83.3",
|
||||||
|
"sca": "I25.1",
|
||||||
|
"syndrome coronarien aigu": "I25.1",
|
||||||
|
"embolie pulmonaire": "I26.9",
|
||||||
|
"ep": "I26.9",
|
||||||
|
"avc ischémique": "I63.9",
|
||||||
|
"avc hémorragique": "I61.9",
|
||||||
|
"infarctus du myocarde": "I21.9",
|
||||||
|
"idm": "I21.9",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _alias_matches_candidate(sec_text_lower: str, code_upper: str) -> bool:
|
||||||
|
"""Vérifie si un alias clinique dans le texte de section correspond au code du candidat."""
|
||||||
|
if not code_upper:
|
||||||
|
return False
|
||||||
|
for alias, alias_code in DIAGNOSIS_ALIASES.items():
|
||||||
|
if alias in sec_text_lower:
|
||||||
|
# Match exact OU family3
|
||||||
|
if code_upper == alias_code.upper() or code_upper[:3] == alias_code[:3].upper():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Seuil d'écart de score pour CONFIRMED sans LLM
|
# Seuil d'écart de score pour CONFIRMED sans LLM
|
||||||
DELTA_CONFIRMED = 3.0
|
DELTA_CONFIRMED = 3.0
|
||||||
|
|
||||||
@@ -205,15 +235,21 @@ def score_candidates(
|
|||||||
|
|
||||||
# 4b. Mention dans sections diagnostiques fortes (CRH)
|
# 4b. Mention dans sections diagnostiques fortes (CRH)
|
||||||
# diag_sortie / diag_principal = +4 (signal le plus fort après trackare)
|
# diag_sortie / diag_principal = +4 (signal le plus fort après trackare)
|
||||||
# synthese = +2 (équivalent conclusion)
|
# synthese / conclusion = +2
|
||||||
|
# Match par terme, code CIM-10, OU alias clinique (DLBCL→C83.3, etc.)
|
||||||
term_lower = c.term.lower()
|
term_lower = c.term.lower()
|
||||||
code_upper = (c.code or "").upper()
|
code_upper = (c.code or "").upper()
|
||||||
_diag_section_bonus = 0.0
|
_diag_section_bonus = 0.0
|
||||||
for sec_key, sec_bonus in (("diag_sortie", 4), ("diag_principal", 4), ("synthese", 2)):
|
for sec_key, sec_bonus in (
|
||||||
|
("diag_sortie", 4), ("diag_principal", 4),
|
||||||
|
("synthese", 2), ("conclusion", 2),
|
||||||
|
):
|
||||||
sec_text = (synthese.get(sec_key) or "").lower()
|
sec_text = (synthese.get(sec_key) or "").lower()
|
||||||
if not sec_text or len(sec_text) < 3:
|
if not sec_text or len(sec_text) < 3:
|
||||||
continue
|
continue
|
||||||
if term_lower in sec_text or (code_upper and code_upper in sec_text.upper()):
|
if (term_lower in sec_text
|
||||||
|
or (code_upper and code_upper in sec_text.upper())
|
||||||
|
or _alias_matches_candidate(sec_text, code_upper)):
|
||||||
_diag_section_bonus = max(_diag_section_bonus, sec_bonus)
|
_diag_section_bonus = max(_diag_section_bonus, sec_bonus)
|
||||||
if _diag_section_bonus:
|
if _diag_section_bonus:
|
||||||
score += _diag_section_bonus
|
score += _diag_section_bonus
|
||||||
@@ -370,20 +406,24 @@ def _collect_evidence(
|
|||||||
term_lower = winner.term.lower()
|
term_lower = winner.term.lower()
|
||||||
code_upper = (winner.code or "").upper()
|
code_upper = (winner.code or "").upper()
|
||||||
|
|
||||||
def _term_or_code_in(text: str) -> bool:
|
def _term_or_code_or_alias_in(text: str) -> bool:
|
||||||
tl = text.lower()
|
tl = text.lower()
|
||||||
return (term_lower in tl) or (code_upper and code_upper in text.upper())
|
return (
|
||||||
|
(term_lower in tl)
|
||||||
|
or (code_upper and code_upper in text.upper())
|
||||||
|
or _alias_matches_candidate(tl, code_upper)
|
||||||
|
)
|
||||||
|
|
||||||
if diag_sortie and _term_or_code_in(diag_sortie):
|
if diag_sortie and _term_or_code_or_alias_in(diag_sortie):
|
||||||
excerpt = diag_sortie[:MAX_EVIDENCE_LEN]
|
excerpt = diag_sortie[:MAX_EVIDENCE_LEN]
|
||||||
evidence.append(f"Diagnostic de sortie: «{excerpt}»")
|
evidence.append(f"Diagnostic de sortie: «{excerpt}»")
|
||||||
elif diag_principal_sec and _term_or_code_in(diag_principal_sec):
|
elif diag_principal_sec and _term_or_code_or_alias_in(diag_principal_sec):
|
||||||
excerpt = diag_principal_sec[:MAX_EVIDENCE_LEN]
|
excerpt = diag_principal_sec[:MAX_EVIDENCE_LEN]
|
||||||
evidence.append(f"Diagnostic principal: «{excerpt}»")
|
evidence.append(f"Diagnostic principal: «{excerpt}»")
|
||||||
elif motif and term_lower in motif.lower():
|
elif motif and term_lower in motif.lower():
|
||||||
excerpt = motif[:MAX_EVIDENCE_LEN]
|
excerpt = motif[:MAX_EVIDENCE_LEN]
|
||||||
evidence.append(f"Motif hospitalisation: «{excerpt}»")
|
evidence.append(f"Motif hospitalisation: «{excerpt}»")
|
||||||
elif conclusion and term_lower in conclusion.lower():
|
elif conclusion and _term_or_code_or_alias_in(conclusion):
|
||||||
excerpt = conclusion[:MAX_EVIDENCE_LEN]
|
excerpt = conclusion[:MAX_EVIDENCE_LEN]
|
||||||
evidence.append(f"Conclusion: «{excerpt}»")
|
evidence.append(f"Conclusion: «{excerpt}»")
|
||||||
|
|
||||||
@@ -478,11 +518,42 @@ def select_dp(
|
|||||||
top_k = config.get("top_k", 7)
|
top_k = config.get("top_k", 7)
|
||||||
|
|
||||||
# Trackare → DP autoritaire, pas de scoring
|
# Trackare → DP autoritaire, pas de scoring
|
||||||
|
# Garde-fou : si Trackare code un R-code (symptôme) mais que les sections CRH
|
||||||
|
# mentionnent un diagnostic étiologique fort (via alias), on downgrade en REVIEW.
|
||||||
if dossier.document_type == "trackare" and dossier.diagnostic_principal:
|
if dossier.document_type == "trackare" and dossier.diagnostic_principal:
|
||||||
|
dp_code = dossier.diagnostic_principal.cim10_suggestion or ""
|
||||||
|
trackare_is_symptom = dp_code.startswith("R")
|
||||||
|
|
||||||
|
crh_has_strong_diag = False
|
||||||
|
if trackare_is_symptom:
|
||||||
|
for sec_key in ("diag_sortie", "diag_principal", "synthese", "conclusion"):
|
||||||
|
sec_text = (synthese.get(sec_key) or "").lower()
|
||||||
|
if sec_text and len(sec_text) >= 3:
|
||||||
|
for alias, alias_code in DIAGNOSIS_ALIASES.items():
|
||||||
|
if alias in sec_text and not alias_code.startswith("R"):
|
||||||
|
crh_has_strong_diag = True
|
||||||
|
break
|
||||||
|
if crh_has_strong_diag:
|
||||||
|
break
|
||||||
|
|
||||||
|
if trackare_is_symptom and crh_has_strong_diag:
|
||||||
return DPSelection(
|
return DPSelection(
|
||||||
chosen_index=0,
|
chosen_index=0,
|
||||||
chosen_term=dossier.diagnostic_principal.texte,
|
chosen_term=dossier.diagnostic_principal.texte,
|
||||||
chosen_code=dossier.diagnostic_principal.cim10_suggestion,
|
chosen_code=dp_code,
|
||||||
|
confidence="medium",
|
||||||
|
verdict="REVIEW",
|
||||||
|
evidence=[
|
||||||
|
"Source: Trackare (codage établissement)",
|
||||||
|
"Alerte: Trackare code un symptôme (R*) mais le CRH mentionne un diagnostic étiologique",
|
||||||
|
],
|
||||||
|
reason="Trackare symptôme vs CRH diagnostic — vérification DIM requise",
|
||||||
|
)
|
||||||
|
|
||||||
|
return DPSelection(
|
||||||
|
chosen_index=0,
|
||||||
|
chosen_term=dossier.diagnostic_principal.texte,
|
||||||
|
chosen_code=dp_code,
|
||||||
confidence="high",
|
confidence="high",
|
||||||
verdict="CONFIRMED",
|
verdict="CONFIRMED",
|
||||||
evidence=["Source: Trackare (codage établissement)"],
|
evidence=["Source: Trackare (codage établissement)"],
|
||||||
|
|||||||
@@ -33,6 +33,6 @@
|
|||||||
},
|
},
|
||||||
"expected": {
|
"expected": {
|
||||||
"chosen_code": "I25.1",
|
"chosen_code": "I25.1",
|
||||||
"verdict": "CONFIRMED"
|
"verdict": "REVIEW"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user