Le moteur de recommandations
Ce document décrit comment l'application transforme les réponses d'un questionnaire en listes de préparations recommandées. Chaque affirmation renvoie au code source (sad_pulm/recommendations/).
Vue d'ensemble
Une session enregistre les réponses d'un utilisateur au questionnaire. Chaque réponse est un choix (QuestionChoice) rattaché à une question. La suite ordonnée des choix forme un chemin dans l'arbre de décision.
Le premier choix est toujours l'indication : Asthme ou BPCO. Il fixe la population de départ. Chaque choix suivant peut porter des sélecteurs (QuestionChoicePreparationSelector) qui restreignent la liste des préparations.
flowchart TD
A[Réponse 1 : Asthme ou BPCO] --> B["Base : préparations de l'indication<br/>(+ double indication)<br/>ET disponibles en Suisse"]
B --> C[Choix 2 : sélecteurs appliqués]
C --> D[Choix 3 : sélecteurs appliqués]
D --> E[...]
E --> F[Liste préférée]
E --> G[Liste alternative]
E --> H[Liste réserve]
style F fill:#1a7f37,color:#fff
style G fill:#9a6700,color:#fff
style H fill:#0969da,color:#fff
Trois listes sont produites, calculées indépendamment :
| Liste | Sélecteurs utilisés | Portée |
|---|---|---|
| Préférée | is_preferred=True |
tous les choix du chemin |
| Alternative | is_preferred=False |
tous les choix du chemin |
| Réserve | is_reserve=True |
uniquement le dernier choix |
La navigation dans l'arbre
- Chaque choix pointe (ou non) vers une question suivante : champ
next_question(models.py,QuestionChoice). Pas de question suivante = fin du chemin. - Quand une question est passée (skip), le moteur choisit dynamiquement la meilleure question suivante parmi les questions enfants et les
additional_skip_questions: celle qui discrimine le mieux les préparations restantes (score = pouvoir d'élimination × couverture,question_selector.py::select_best_question). S'il reste ≤ 1 préparation, plus aucune question n'est posée. - Un choix peut marquer
chamber_required: la session recommande alors une chambre d'inhalation (Session.chamber_recommended).
Le pipeline de filtrage
Point d'entrée : Session.get_matched_preparations(is_preferred=...) (models.py:522).
- Base : préparations dont l'indication correspond au premier choix (Asthme ou BPCO), plus celles à double indication (
both), etavailable=True(disponibles en Suisse). Une préparationundefinedou indisponible ne peut jamais apparaître. - Pour chaque choix suivant (dans l'ordre du chemin) : si le choix possède des sélecteurs pour la piste demandée (
is_bpcoetis_preferredcorrespondants), la liste courante est intersectée avec l'union des résultats de ses sélecteurs. Un choix sans sélecteur ne restreint pas la liste.
Deux règles de combinaison :
- OR entre les sélecteurs d'un même choix - une préparation matche le choix si elle matche au moins un de ses sélecteurs (
QuestionChoice.get_selected_preparations,models.py:91). - AND entre les choix successifs - une préparation doit survivre à chaque choix du chemin (
models.py:576-589).
La liste ne peut donc que rétrécir le long du chemin, jamais s'élargir.
Les critères d'un sélecteur
QuestionChoicePreparationSelector.get_matching_preparations (models.py:342) applique les critères dans cet ordre, chacun étant optionnel :
| # | Critère | Effet |
|---|---|---|
| 1 | dosage_form |
Ne garde que cette forme galénique |
| 2 | atc_codes_filter |
Ne garde que ces codes ATC (liste blanche) |
| 3 | atc_codes_exclude |
Écarte ces codes ATC (liste noire) |
| 4 | preparation_classes |
Filtre strict (voir ci-dessous) |
| 5 | is_monotherapy |
Avec le critère 4 : la préparation doit contenir toutes les classes du sélecteur (AND) |
| 6 | reserve_treatment_filter |
True = traitements de réserve seulement, False = les exclut, vide = indifférent |
| 7 | background_treatment_filter |
Idem pour les traitements de fond |
Le filtre de classes est strict (models.py:383-392) : dès que le sélecteur définit des classes,
- une préparation ayant une classe hors de la liste est écartée (même si elle a aussi les bonnes) ;
- une préparation sans aucune classe est écartée ;
- sans monothérapie, avoir une des classes suffit ; avec monothérapie, il les faut toutes.
Pistes préférée et alternative : comportement voulu
« Alternative » = une piste cohérente de bout en bout
La liste alternative n'est pas « tout ce qui est acceptable sans être préféré ». C'est un second chemin complet, rejoué depuis la base avec les seuls sélecteurs is_preferred=False de chaque choix. Décision produit confirmée le 19.08.2026.
Conséquences, toutes voulues :
-
Une préparation doit tenir une piste de bout en bout. Une préparation qui matche les critères préférés d'un choix mais seulement les critères alternatifs d'un autre ne sort dans aucune liste :
Choix 1 Choix 2 Résultat Critères préférés matche ✅ ne matche pas ❌ éliminée de la liste préférée Critères alternatifs ne matche pas ❌ matche ✅ éliminée de la liste alternative -
Aucun sélecteur alternatif sur tout le chemin → liste alternative vide (
models.py:552-561). - Un choix sans sélecteur alternatif ne restreint pas la piste alternative - y compris si ses sélecteurs préférés imposaient une contrainte (ex. une forme galénique). La piste alternative ignore cette contrainte-là.
- Dédoublonnage : les préparations déjà présentes en préféré sont retirées de l'affichage alternatif (
get_alternative_preparations,models.py:613).
Liste de réserve
Seuls les sélecteurs is_reserve=True du dernier choix du chemin sont considérés (get_reserve_preparations, models.py:617), appliqués sur la base indication + disponibilité (pas sur la liste déjà filtrée). Les réponses intermédiaires n'influencent pas la réserve.
Caches
Deux vues sont mises en cache 24 h, par langue : le graphe de décision (decision_graph_mermaid_{bpco|asthma}_{lang}, graph_generator.py:447) et la vue « tous les chemins » (all_paths_{asthma|bpco}_{lang}, views.py:638).
Invalidation :
- automatique à chaque création/modification/suppression de sélecteur (
invalidate_path_caches,views.py:624) ; - bouton « clear cache » sur les pages graphe et chemins ;
- commande
python manage.py clear_decision_graph_cache.
Si un changement de sélecteur ne semble pas visible dans ces deux vues, penser au cache. Les résultats d'une session, eux, ne sont jamais mis en cache.
Références rapides
| Quoi | Où |
|---|---|
| Pipeline session → préparations | recommendations/models.py:522 (get_matched_preparations) |
| Critères d'un sélecteur | recommendations/models.py:342 (get_matching_preparations) |
| Union des sélecteurs d'un choix | recommendations/models.py:91 (get_selected_preparations) |
| Alternatives / réserve | recommendations/models.py:613 / :617 |
| Choix de la question après un skip | recommendations/question_selector.py |
| Énumération de tous les chemins | recommendations/path_generator.py |
| Explications lisibles des critères | recommendations/models.py:127 (get_filtering_explanations) |