# NUKE-3 — Plan de sprint court ## 3 Objectifs 1. **Réduire le taux REVIEW sur les dossiers sans DP** : 43/43 → cible < 50% REVIEW 2. **Améliorer la qualité du pool** : éliminer le bruit OCR et fusionner les doublons 3. **Activer le signal synthèse** : rendre motif_align opérant même sur les dossiers trackare ## 5 Patchs proposés (ordre d'impact) ### Patch 1 — Dedup par code dans `build_candidates()` ⭐⭐⭐ **Symptôme** : Même code CIM-10 apparaît 2× (edsnlp + regex) → le candidat se bat contre lui-même, delta artificiellement réduit. **Patch minimal** : `src/medical/dp_selector.py`, fonction `build_candidates()` ```python # Après la boucle de construction des candidats : # Dedup par code : garder le meilleur section_strength, ajouter bonus multi-source seen: dict[str, DPCandidate] = {} for c in candidates: if not c.code: continue if c.code in seen: existing = seen[c.code] existing.num_occurrences += 1 if c.section_strength > existing.section_strength: existing.section_strength = c.section_strength existing.source = c.source else: seen[c.code] = c candidates = list(seen.values()) # Réindexer for i, c in enumerate(candidates): c.index = i ``` **Pourquoi ça aide** : Les 4 dossiers avec doublons (I26.9, K81.0, D69.6, J18.9) gagnent un candidat fusionné plus fort. Le bonus `occurrences` existant (+1 ou +2) s'active déjà sur `num_occurrences`. **Test** : `test_dedup_same_code_merged()` — 2 candidats avec même code, sources différentes → 1 seul candidat avec meilleur section_strength. **Effort** : 1h --- ### Patch 2 — Filtre bruit OCR dans `build_candidates()` ⭐⭐⭐ **Symptôme** : Candidats avec texte OCR corrompu ("C : 9.4", "C omprend décollement de la (d") polluent le pool et consomment des places dans top_k=7. **Patch minimal** : `src/medical/dp_selector.py`, dans `build_candidates()` ```python MIN_TERM_WORDS = 2 def _is_ocr_noise(text: str) -> bool: """Rejette les candidats dont le texte est du bruit OCR.""" clean = text.strip() if len(clean) < 4: return True words = clean.split() if len(words) < MIN_TERM_WORDS: return True # Ratio de caractères non-alpha suspect alpha = sum(1 for ch in clean if ch.isalpha()) if alpha / max(len(clean), 1) < 0.5: return True return False ``` **Pourquoi ça aide** : Réduit le pool de 7 à ~4-5 candidats pertinents, améliore le delta. **Test** : `test_ocr_noise_excluded()` — candidat "C : 9.4" exclu du pool. **Effort** : 1h --- ### Patch 3 — Synthèse depuis top-level JSON + sections trackare ⭐⭐ **Symptôme** : 100% des REVIEW ont synthèse vide → `motif_align` ne fonctionne jamais. Le JSON a `sejour.motif` mais `build_synthese()` ne lit que `sections.motif_hospitalisation`. **Patch minimal** : `src/medical/dp_selector.py`, fonction `build_synthese()` ```python def build_synthese(dossier, parsed_data): sections = parsed_data.get("sections", {}) motif = sections.get("motif_hospitalisation", "") conclusion = sections.get("conclusion", "") # Fallback : motif depuis le séjour (trackare) if not motif and dossier.sejour.motif: motif = dossier.sejour.motif return {"motif": motif, "conclusion": conclusion, ...} ``` **Pourquoi ça aide** : Active le bonus `motif_align` (+2) sur les trackare qui stockent le motif dans `sejour.motif`, discriminant les candidats. **Test** : `test_synthese_fallback_sejour_motif()` — synthèse avec motif depuis séjour. **Effort** : 30min --- ### Patch 4 — Abaisser DELTA_CONFIRMED de 3.0 à 2.0 ⭐⭐ **Symptôme** : 13/43 REVIEW ont delta 2.0-2.9 — cas où le pré-ranker a une préférence nette mais pas assez pour le seuil actuel. Exemple : T83.5 (4.0) vs R50.9 (2.0) = delta 2.0 → REVIEW, alors que le symptôme (R50.9) est clairement un mauvais DP. **Patch minimal** : `src/medical/dp_selector.py` ```python DELTA_CONFIRMED = 2.0 # Était 3.0 ``` **Pourquoi ça aide** : +13 CONFIRMED (30% des REVIEW), mais uniquement si les gardes A1/A2/A3 valident. Le hardening empêche les faux positifs. **Test** : Adapter les fixtures existantes (dp_acute_vs_comorbidity.json utilise un delta attendu). **Effort** : 30min (+ vérification non-régression) --- ### Patch 5 — DAS order bonus (premier listé = plus saillant) ⭐ **Symptôme** : En cas d'ex-aequo total (15 cas), il n'y a aucun signal pour départager. Or l'ordre des DAS dans le document médical n'est pas aléatoire — les premiers listés sont souvent les plus pertinents. **Patch minimal** : `src/medical/dp_selector.py`, dans `score_candidates()` ```python # 9. Bonus d'ordre (premier DAS listé = +1) if c.index == 0: score += 1 details["first_listed"] = 1 elif c.index == 1: score += 0.5 details["second_listed"] = 0.5 ``` **Pourquoi ça aide** : Brise les ex-aequo dans 15 cas (36% des REVIEW) avec un signal faible mais non-arbitraire. Le premier DAS listé reflète l'ordre du document source. **Test** : `test_first_listed_bonus()` — 2 candidats identiques, le premier listé gagne. **Effort** : 30min --- ## Ordre d'exécution | # | Patch | Effort | Impact estimé | Pré-requis | |---|-------|--------|---------------|------------| | 1 | Dedup par code | 1h | 4 dossiers améliorés | Aucun | | 2 | Filtre bruit OCR | 1h | ~5 dossiers pool nettoyé | Aucun | | 3 | Synthèse fallback | 30min | Jusqu'à 43 dossiers (si motif trackare dispo) | Vérifier séjour.motif | | 4 | DELTA 3.0 → 2.0 | 30min | +13 CONFIRMED | Patches 1+2 (pool propre) | | 5 | DAS order bonus | 30min | Brise 15 ex-aequo | Après patch 4 | **Total estimé** : 3h30 ## Critères d'acceptation (KPI) | KPI | Avant | Cible | |-----|-------|-------| | REVIEW rate (sans-DP) | 100% (43/43) | < 50% | | CONFIRMED + evidence | 100% | 100% (maintenu) | | DP symptôme R* (CONFIRMED) | 0% | < 5% | | DP comorbidité (CONFIRMED) | 0% | < 3% | | Candidats bruit OCR dans pool | ~5% | 0% | | Ex-aequo (delta=0) | 36% (15/43) | < 15% | ## Prochaines étapes hors sprint - **Gold standard CRH** : sélectionner 20 CRH avec DP expert pour mesurer `confirmed_accuracy` - **Benchmark LLM ON** : relancer le pipeline avec `T2A_DP_RANKER_LLM=1` sur les 43 REVIEW - **Extraction synthèse trackare** : parser le motif d'hospitalisation depuis les PDF trackare --- *Plan rédigé le 2026-02-24 — benchmark offline sur 249 dossiers*