# Dependances pre-provisionnees (rdflib ortools pyro-ppl torch pandas matplotlib seaborn).
# Aucun install requis : voir CaseStudies/requirements.txt ; imports directs en cellule suivante.Navigation : Index # CC2 : OncoPlan - Squelette
Cours : IA101 - Intelligence Artificielle (EPF 2026) Étudiant(s) : [VOTRE NOM ICI]
Ce notebook est le squelette à compléter pour le devoir OncoPlan. Il couvre : 1. Symbolique : Ontologie RDF et Planification OR-Tools. 2. Probabiliste : Modélisation Bayésienne avec Pyro. 3. Bonus : Traçabilité via Hashage.
Installation des Dépendances
Les bibliothèques utilisées par ce devoir sont pré-provisionnées : aucune commande pip install n’est nécessaire dans ce notebook. L’environnement d’exécution référence CaseStudies/requirements.txt, qui déclare les versions retenues pour la cohorte.
Le trio de bibliothèques couvre les trois paradigmes de l’IA explorés dans ce devoir :
| Bibliothèque | Paradigme | Rôle dans OncoPlan |
|---|---|---|
rdflib |
Symbolique | Ontologie des médicaments : graphe RDF des classes et propriétés |
ortools |
Symbolique | Planification des cures : solveur CP-SAT sur contraintes |
pyro |
Probabiliste | Modèle bayésien : inférence variationnelle (SVI) sur le profil patient |
Les cellules suivantes font directement les imports ; aucune étape de build n’est requise avant d’exécuter.
Import des bibliothèques nécessaires.
La cellule d’import charge tout l’écosystème du devoir et le configure :
- Manipulation de données :
pandas/numpypour les tableaux patients et les calculs vectoriels. - Visualisation :
matplotlib/seabornpour tracer les distributions a posteriori et les plans de cure. - Ontologie :
rdflib(graphe RDF,Namespace,URIRef, typesXSD/FOAF) pour la Partie 1. - Planification :
ortools.sat.python.cp_model(solveur CP-SAT) pour la contrainte des jours d’administration. - Probabiliste :
torch+pyro(distributions,SVI,Trace_ELBO,Predictive,Adam) pour la Partie 2. - Traçabilité :
hashlib+jsonpour le hash SHA-256 du contrat de la Partie 3.
Notez la graine aléatoire fixée (pyro.set_rng_seed(101)) : elle rend l’inférence reproductible d’une exécution à l’autre — indispensable pour comparer deux runs du modèle bayésien.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from rdflib import Graph, Literal, RDF, URIRef, Namespace
from rdflib.namespace import FOAF, XSD
from ortools.sat.python import cp_model
import torch
import pyro
import pyro.distributions as dist
from pyro.infer import SVI, Trace_ELBO, Predictive
from pyro.optim import Adam
import hashlib
import json
# Configuration
sns.set_style("whitegrid")
pyro.set_rng_seed(101)Les trois paradigmes d’OncoPlan
Le devoir traverse trois familles de méthodes d’IA sur un même fil conducteur — le plan de traitement d’un patient oncologique :
- Symbolique (Partie 1) : connaissances explicites — l’ontologie RDF des médicaments (règles de toxicité, incompatibilités) et la planification OR-Tools (contraintes de ressources).
- Probabiliste (Partie 2) : raisonnement sous incertitude — le modèle bayésien Pyro qui infère le profil du patient (Résistant/Normal/Sensible) à partir d’observations de globules blancs.
- Bonus (Partie 3) : intégrité — le hash SHA-256 qui garantit qu’un plan signé n’a pas été altéré.
Chaque partie s’appuie sur les sorties de la précédente : l’ontologie nourrit la vérification de prescription, le modèle bayésien alimente la décision de dose, le contrat scelle le résultat. Ce découpage illustre la complémentarité des approches plutôt que leur concurrence.
Partie 1 : Le Pharmacien Symbolique
1.1. Ontologie des Médicaments (RDFLib)
Objectif : Créer un graphe RDF contenant les médicaments et leurs propriétés (toxicité, incompatibilités).
L’ontologie est la brique symbolique du devoir : elle encode les connaissances du domaine sous forme de triplets (sujet, prédicat, objet) — par exemple Cisplatine -- estNephrotoxique -> true. Trois éléments RDFLib sont à construire :
- Le Namespace (
Namespace("http://onco.example/")) : l’URI de base qui préfixe toutes les classes et propriétés, pour qu’elles soient identifiables de manière unique dans le graphe. - Le Graph : le conteneur des triplets, sur lequel on pourra requêter (
graph.query(...)) pour vérifier une prescription. - Les classes et propriétés :
Medicament,estNephrotoxique,incompatibleAvec, etc. — la structure de l’ontologie, qui doit couvrir les 4 molécules de base : Cisplatine, 5-FU, Docetaxel, Gentamicine.
L’objectif final est un graphe requêtable : la vérification de prescription (section suivante) interrogera cette ontologie pour décider si un protocole est sûr.
# TODO: Créer le Namespace ONCO
# TODO: Initialiser le Graph
# TODO: Définir les classes et propriétés
# TODO: Peupler la base avec les médicaments (Cisplatine, 5-FU, Docetaxel, Gentamicine)
# VOTRE CODE ICI
passVérification de prescription basée sur l’ontologie.
La fonction verifier_prescription(protocole_noms, patient_insuffisance_renale) consulte l’ontologie pour rejeter un protocole dangereux. Elle retourne une liste d’alertes — vide si la prescription est sûre, peuplée sinon. Deux règles de sécurité sont attendues :
- Néphrotoxicité : si le patient est insuffisant rénal ET qu’un médicament du protocole est marqué
estNephrotoxique, ajouter une alerte. - Incompatibilités : pour chaque paire de médicaments du protocole, si l’ontologie déclare
incompatibleAvec, ajouter une alerte.
Lecture des sorties ci-dessous : les trois tests affichent actuellement [] (aucune alerte) — c’est le comportement attendu du squelette avant implémentation : les TODO de l’ontologie (cellule précédente) et de la fonction sont encore vides, donc la vérification ne trouve rien à signaler. Une fois l’ontologie peuplée et les règles implémentées, le Test 2 (patient insuffisant rénal + Cisplatine) et le Test 3 (Cisplatine + Gentamicine) devront produire des alertes non vides.
def verifier_prescription(protocole_noms, patient_insuffisance_renale):
"""
Vérifie si une prescription est valide.
Args:
protocole_noms (list): Liste de noms de médicaments (str)
patient_insuffisance_renale (bool): True si le patient a une insuffisance rénale
Returns:
list: Liste de messages d'alerte (str)
"""
alertes = []
# TODO: Vérifier la néphrotoxicité si le patient est insuffisant rénal
# TODO: Vérifier les incompatibilités entre paires de médicaments
return alertes
# Test (Ne pas modifier)
print("--- Test 1 : Patient Sain, Protocole OK ---")
print(verifier_prescription(["Cisplatine", "5-FU"], False))
print("\n--- Test 2 : Patient Insuffisant Rénal ---")
print(verifier_prescription(["Cisplatine"], True))
print("\n--- Test 3 : Incompatibilité ---")
print(verifier_prescription(["Cisplatine", "Gentamicine"], False))--- Test 1 : Patient Sain, Protocole OK ---
[]
--- Test 2 : Patient Insuffisant Rénal ---
[]
--- Test 3 : Incompatibilité ---
[]
# Exercice 1 : Etendre l'ontologie medicamenteuse
# TODO etudiant : Ajouter "Paclitaxel" et "Carboplatine" au dictionnaire medicaments
# TODO etudiant : Peupler le graphe RDF avec les nouveaux triplets
# TODO etudiant : Tester verifier_prescription avec les nouvelles combinaisons
result = None # TODO etudiant : remplacer par votre ontologie etendue
print("Exercice a completer : ontologie etendue")Exercice a completer : ontologie etendue
Exercice 1 : Etendre l’ontologie avec de nouveaux medicaments et interactions
Objectif : Ajouter de nouveaux medicaments a l’ontologie RDF et implementer une verification d’interactions etendue.
Contexte : L’ontologie actuelle contient 4 medicaments. En pratique, les protocoles oncologiques combinent davantage de molecules, et les interactions croisees sont critiques.
Indices : - Indice 1 : Ajoutez “Paclitaxel” (toxicite_renale=False, incompatible avec “Docetaxel”) et “Carboplatine” (toxicite_renale=True, incompatible avec “Cisplatine”) - Indice 2 : Verifiez qu’un protocole [“Cisplatine”, “Carboplatine”] declenche bien une alerte d’incompatibilite - Étape 1 : Ajouter les nouveaux medicaments au dictionnaire medicaments - Étape 2 : Peupler le graphe RDF avec les nouveaux triplets - Étape 3 : Tester la fonction verifier_prescription avec des combinaisons incluant les nouveaux medicaments
1.2. Planification des Cures (OR-Tools)
Objectif : Planifier 4 cycles de chimiothérapie en respectant les contraintes de temps et de ressources.
Le problème est un problème de satisfaction de contraintes (CSP) résolu par le solveur CP-SAT d’OR-Tools. La variable de décision est le jour de début du traitement debut ; le planning en découle (administrations aux jours [1, 8] de chaque cycle de 21 jours). Les contraintes à modéliser :
- Pas d’administration le dimanche : si J1 est un lundi, les dimanches sont les jours multiples de 7 (7, 14, 21…). L’indice fourni suggère
AddModuloEqualitypour contraindrejour % 7 != 0sur chaque jour d’administration. - Capacité maximale de la salle :
capacite_max=3patients simultanés. La simulation fournit desoccupations_existantes({10: 3, 11: 3, 12: 3}) : les jours 10, 11 et 12 sont déjà pleins — le planning ne doit pas y ajouter de patient.
Lecture des sorties ci-dessous : le squelette affiche Jour 0 et un planning [] — là encore l’état avant implémentation : debut = 0 est une valeur factice (le TODO de récupération de la valeur optimale n’est pas implémenté) et planning n’est jamais rempli. Un modèle complété doit retourner un début après les jours saturés et une liste d’administrations respectant la contrainte dominicale.
def planifier_chimio(nb_cycles=4, duree_cycle=21, jours_admin=[1, 8], capacite_max=3, occupations_existantes={}):
model = cp_model.CpModel()
horizon = nb_cycles * duree_cycle + 10 # Marge
# TODO: Définir la variable de décision (Jour de début du traitement)
# TODO: Ajouter les contraintes
# 1. Pas de dimanche (supposons J1 = Lundi, donc Dimanche = 7, 14...)
# Indice : Utilisez AddModuloEquality pour vérifier si le jour est un multiple de 7
# 2. Respecter la capacité max (occupations_existantes)
# Résolution
solver = cp_model.CpSolver()
status = solver.Solve(model)
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
# TODO: Récupérer la valeur de début et calculer le planning complet
debut = 0 # À remplacer
planning = [] # À remplir
return debut, planning
else:
return None, []
# Simulation d'occupations (Jours 10, 11, 12 sont complets)
occupations = {10: 3, 11: 3, 12: 3}
debut, planning = planifier_chimio(occupations_existantes=occupations)
print(f"Début optimal du traitement : Jour {debut}")
print(f"Jours d'administration : {planning}")Début optimal du traitement : Jour 0
Jours d'administration : []
Transition : du symbolique au probabiliste
La Partie 1 raisonnait avec des connaissances certaines : soit un médicament est incompatible, soit il ne l’est pas. Or la pratique clinique est dominée par l’incertitude : un même protocole peut être efficace chez un patient et toxique chez un autre, selon son profil biologique (Résistant / Normal / Sensible).
La Partie 2 bascule donc de paradigme : au lieu de règles explicites, un modèle probabiliste (Pyro) apprend à partir de données — la chute des globules blancs observée après administration — à inférer le profil du patient. C’est le passage de la logique (vrai/faux) à la distribution de probabilité (degré de croyance).
Partie 2 : Le Médecin Probabiliste (Pyro)
Objectif : Modéliser la réponse du patient (Toxicité) en fonction des doses reçues et des observations (Globules Blancs).
Le modèle est un modèle génératif bayésien : on décrit comment les données ont pu être produites, puis on inverse le raisonnement pour inférer les causes latentes. Deux blocs sont à écrire dans OncoModel :
model(doses, observations)— le génératif : le profil du patient (profil→Categoricalsur 3 valeurs : 0=Résistant, 1=Normal, 2=Sensible), la dynamique temporelle (toxicite_cumuleequi évolue selon dose et sensibilité,mu_gbdes globules blancs qui en dépend), et la vraisemblance (observationsconditionnées sur ce modèle).guide(doses, observations)— le variationnel : l’approximation paramétrique de la distribution a posteriori (ici, unCategoricalsur le profil). SVI aligne guide sur modèle viaTrace_ELBO.
Les données simulées illustrent le scénario : doses [100, 0, 0] (J1, J8, J15) et globules blancs [7800, 2500] — une chute de 7800 à 2500 entre J1 et J8, caractéristique d’une neutropénie (seuil d’alerte < 2000). L’inférence SVI (commentée, à débloquer une fois le modèle implémenté) estime alors les probabilités a posteriori du profil.
class OncoModel:
def __init__(self):
pass
def model(self, doses, observations=None):
# TODO: Définir le Prior sur le profil du patient (Categorical)
# 0: Résistant, 1: Normal, 2: Sensible
# TODO: Définir la boucle temporelle
# toxicite_cumulee évolue selon dose et sensibilité
# mu_gb (Globules Blancs) dépend de toxicite_cumulee
# TODO: Définir la Likelihood (Observation)
pass
def guide(self, doses, observations=None):
# TODO: Définir le guide variationnel pour 'profil'
pass
# Instanciation
onco_model = OncoModel()
# Données simulées : Patient reçoit 100mg à T0, 0 à T1...
# Observation : Chute brutale à T1 (J8)
doses_plan = torch.tensor([100.0, 0.0, 0.0]) # J1, J8, J15
observations_reelles = torch.tensor([7800.0, 2500.0]) # J1 Normal, J8 Neutropénie sévère
# Inférence SVI (À décommenter une fois le modèle implémenté)
# pyro.clear_param_store()
# svi = SVI(onco_model.model, onco_model.guide, Adam({"lr": 0.01}), loss=Trace_ELBO())
# IMPORTANT : Pour l'inférence, assurez-vous d'aligner les doses avec les observations disponibles.
# Si vous passez des doses futures sans observations, Pyro tentera d'inférer ces observations manquantes.
# doses_inf = ... (à définir)
# print("Début de l'inférence...")
# for step in range(1000):
# loss = svi.step(doses_inf, observations_reelles)
# if step % 200 == 0:
# print(f"Step {step} : Loss = {loss}")
# Résultats
# post_probs = pyro.param("profil_probs_post").detach()
# print(f"\nProbabilités a posteriori du profil :")
# print(f"Résistant : {post_probs[0]:.2f}")
# print(f"Normal : {post_probs[1]:.2f}")
# print(f"Sensible : {post_probs[2]:.2f}")# Exercice 2 : Sensibilite au prior du profil patient
# TODO etudiant : Creer une version parametrable du modele avec profil_probs en argument
# TODO etudiant : Tester 3 configurations de prior differentes
# TODO etudiant : Comparer les posterieurs dans un tableau
result = None # TODO etudiant : remplacer par votre analyse
print("Exercice a completer : sensibilite au prior")Exercice a completer : sensibilite au prior
Exercice 2 : Sensibilite du modèle bayesien au prior sur le profil patient
Objectif : Etudier comment le choix du prior sur les profils (Resistant/Normal/Sensible) influence le diagnostic posterieur.
Contexte : Le prior actuel est [0.1, 0.6, 0.3] (10% Resistants, 60% Normaux, 30% Sensibles). Si la population est différente, le diagnostic change.
Indices : - Indice 1 : Modifiez profil_probs dans la méthode model() pour tester : [0.33, 0.34, 0.33] (uniforme), [0.05, 0.90, 0.05] (peu de sensibles) - Indice 2 : Relancez l’inference SVI et comparez les probabilites a posteriori du profil - Étape 1 : Créer une fonction qui accepte le prior en paramètre - Étape 2 : Tester 3 configurations de prior différentes - Étape 3 : Afficher les posterieurs dans un tableau comparatif
2.2. Prédiction et Prise de Décision
Objectif : Comparer le risque de neutropénie sévère (< 2000) pour deux scénarios futurs.
Une fois le modèle bayésien calibré (Partie 2.1), il devient un simulateur conditionnel : on peut tirer des échantillons de l’avenir via Predictive. L’enjeu clinique est ici la décision de dose :
- Scénario 1 — Maintien : poursuivre à 100 mg. La toxicité cumulée continue de croître → risque élevé de passer sous le seuil de neutropénie sévère (< 2000 globules blancs).
- Scénario 2 — Réduction : réduire à 50 mg. Moins de toxicité cumulée → probabilité plus faible de neutropénie, au prix d’une efficacité potentiellement réduite.
La cellule Predictive (à compléter) simule les deux futurs et compare les distributions de globules blancs. C’est le raisonnement contrefactuel que permet le bayésien : « si on avait fait l’autre choix, quelle serait la distribution des observations ? » — la base statistique de la prise de décision médicale.
# TODO: Utiliser Predictive pour simuler l'avenir
# Scénario 1 : Maintien de la dose (100mg)
# Scénario 2 : Réduction de dose (50mg)
# VOTRE CODE ICI
passTransition : de la décision à l’intégrité
Le raisonnement probabiliste a produit une décision de dose ; il reste à s’assurer qu’elle est appliquée telle quelle. C’est le rôle de la Partie 3 : garantir l’intégrité du plan de traitement contre toute altération (erreur de transcription, modification malveillante, fraude).
Le mécanisme est cryptographique : un hash SHA-256 du plan signé par le médecin et le patient. Toute modification du plan — même d’un caractère — change le hash et fait échouer la vérification. C’est l’application du concept de garde-fou : le raisonnement (Parties 1-2) reste libre, mais son résultat est scellé.
Partie 3 : Bonus Smart Contract
Objectif : Garantir l’intégrité du plan de traitement.
La classe OncoContract implémente un contrat d’intégrité à trois méthodes :
enregistrer_plan(plan_data): calcule le hash SHA-256 du plan (hashlib.sha256sur la représentation JSON) et le stocke dansplan_hash.signer(role, cle_privee_simulee): enregistre l’accord du médecin et/ou du patient (drapeauxsignatures).verifier_execution(plan_propose): compare le hash du plan proposé àplan_hashET vérifie que les deux signatures sont présentes. Une différence de hash = tentative d’altération → refus.
Lecture des sorties ci-dessous : les deux appels affichent Non implémenté — le retour par défaut du squelette, car verifier_execution est encore un stub (return "Non implémenté"). Une fois la méthode complétée, le premier test (plan identique, toutes signatures) doit passer, tandis que le second (Cisplatine 100mg au lieu de 50mg — la tentative de fraude du test) doit être rejeté car son hash diffère du plan enregistré.
Retour au sommaire : Index CaseStudies
class OncoContract:
def __init__(self, medecin_id, patient_id):
self.medecin_id = medecin_id
self.patient_id = patient_id
self.plan_hash = None
self.signatures = {"medecin": False, "patient": False}
def enregistrer_plan(self, plan_data):
# TODO: Hasher le plan (SHA-256)
pass
def signer(self, role, cle_privee_simulee):
# TODO: Enregistrer la signature
pass
def verifier_execution(self, plan_propose):
# TODO: Vérifier que le hash correspond et que tout le monde a signé
return "Non implémenté"
# Test
contract = OncoContract("DrHouse", "P004")
plan_final = {"J1": "Cisplatine 50mg", "J8": "Repos"}
contract.enregistrer_plan(plan_final)
contract.signer("medecin", "key123")
contract.signer("patient", "key456")
print(contract.verifier_execution(plan_final))
print(contract.verifier_execution({"J1": "Cisplatine 100mg"})) # Tentative de fraudeNon implémenté
Non implémenté
# Exercice 3 : Journal d'audit pour le contrat intelligent
# TODO etudiant : Etendre OncoContract avec un attribut historique
# TODO etudiant : Implementer afficher_historique() qui liste toutes les modifications
# TODO etudiant : Tester avec plusieurs modifications successives
result = None # TODO etudiant : remplacer par votre contrat avec audit
print("Exercice a completer : journal d'audit")Exercice a completer : journal d'audit
Exercice 3 : Ajouter un journal d’audit au contrat intelligent
Objectif : Etendre la classe OncoContract pour maintenir un historique des modifications et detecter les tentatives de fraude.
Contexte : Le contrat actuel enregistre un seul plan. En pratique, les plans peuvent etre modifiés plusieurs fois, et il faut tracer qui a fait quoi et quand.
Indices : - Indice 1 : Ajoutez un attribut self.historique = [] qui stocke chaque modification - Indice 2 : Chaque entree du journal doit contenir : horodatage, auteur, action, hash du plan - Étape 1 : Ajouter l’attribut historique et le mettre a jour a chaque appel de enregistrer_plan - Étape 2 : Implementer une méthode afficher_historique() qui affiche le journal - Étape 3 : Tester en modifiant le plan plusieurs fois et verifier que l’historique est complet