Skip to content

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).

  1. Base : préparations dont l'indication correspond au premier choix (Asthme ou BPCO), plus celles à double indication (both), et available=True (disponibles en Suisse). Une préparation undefined ou indisponible ne peut jamais apparaître.
  2. Pour chaque choix suivant (dans l'ordre du chemin) : si le choix possède des sélecteurs pour la piste demandée (is_bpco et is_preferred correspondants), 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 :

  1. 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
  2. Aucun sélecteur alternatif sur tout le chemin → liste alternative vide (models.py:552-561).

  3. 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à.
  4. 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
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)