Tweety-5 répond à « que déduire de ce que je vois ? » ; Tweety-11 répond à « que se passerait-il si j’agissais différemment ? ».
1. Pourquoi la causalité ? — L’échelle de Pearl
La logique classique (notebooks 1-4) et probabiliste (Tweety-10 MLN) répondent à la question « que peut-on déduire d’un ensemble de croyances ? ». Mais elles sont aveugles à la direction causale : un théorème de Bayes ne distingue pas « le baromètre baisse → orage » de « l’orage → le baromère baisse ».
Judea Pearl (Prix Turing 2011) a formalisé cette distinction via l’échelle causale à trois niveaux :
Niveau
Question
Symbole
Exemple
1. Association
Qu’observe-t-on ensemble ?
\(P(y \mid x)\)
Les patients sous traitement guérissent plus souvent.
2. Intervention
Que se passe-t-il si j’agis sur X ?
\(P(y \mid do(x))\)
Si je prescris le traitement, la guérison augmente-t-elle ?
3. Contrefactuel
Que serait-il arrivé si X avait été différent ?
\(P(y_x \mid x', y')\)
Ce patient aurait-il guéri sans traitement ?
Le saut du niveau 1 au niveau 2 est révolutionnaire : \(P(y \mid x) \neq P(y \mid do(x))\) en présence d’un confondeur (cause commune). C’est la frontière entre corrélation et causalité, inaccessible à la statistique seule.
L’idée centrale du do-calculus : do(X = x) ne signifie pas « observer X = x » mais « forcer X à valoir x en remplaçant son équation causale par une constante ». Cette opération rompt tous les liens entrants vers X, isolant l’effet causal propre de X sur le reste du système.
1.1 Structural Causal Model (SCM)
Un SCM encode un graphe causal comme un système d’équations structurelles :
\[X_i = f_i(\text{Parents}(X_i), U_i)\]
où \(U_i\) est un bruit exogène (les atomes de fond chez Tweety). Dans ce notebook, nous travaillons en logique propositionnelle déterministe : chaque atome explicable est défini comme une formule PL de ses parents, et chaque atome de fond est une cause racine (non expliquée dans le modèle).
La sémantique est symbolique (raisonnement argumentatif sur les mondes possibles), pas numérique — Tweety fait de la causalité qualitative, complémentaire de l’approche bayésienne numérique.
2. Configuration du backend causal Tweety (règle F)
Ce notebook exige le JAR causal-1.30.jar (module org.tweetyproject.causal), invoqué via JPype (pont Python↔︎JVM). Aucune simulation, aucun stub : toutes les requêtes ci-dessous sont exécutées par le vrai moteur causal de Tweety.
Règle F (fail-loud) : si l’API causale n’est pas invocable, le notebook échoue bruyamment plutôt que de produire une sortie de substitution.
import os, sysTWEETY_DIR = os.path.abspath(".") # ce notebook vit dans le dossier Tweety/assert os.path.isfile(os.path.join(TWEETY_DIR, "tweety_init.py")), \"Lancez ce notebook depuis le dossier Tweety/ (tweety_init.py introuvable)"os.chdir(TWEETY_DIR)sys.path.insert(0, os.getcwd())from tweety_init import init_tweetyok = init_tweety(verbose=False)print("Backend Tweety initialise :", ok)
Backend Tweety initialise : True
# Verification fail-loud : le module causal est-il reellement present ?import globcausal_jars = [j for j in glob.glob(os.path.join(TWEETY_DIR, "libs", "*.jar"))if"causal"in os.path.basename(j).lower()]assert causal_jars, "causal-1.30.jar MANQUANT dans libs/ -- regle F : installer, ne pas contourner"print("JAR causal detecte :", [os.path.basename(j) for j in causal_jars])# Imports de l'API causale (verifies au prealable via reflection)from org.tweetyproject.logics.pl.syntax import Proposition, Negationfrom org.tweetyproject.logics.pl.parser import PlParserfrom org.tweetyproject.causal.syntax import StructuralCausalModel, CausalKnowledgeBasefrom org.tweetyproject.causal.semantics import InterventionalStatement, CounterfactualStatementfrom org.tweetyproject.causal.reasoner import ArgumentationBasedCausalReasoner, ArgumentationBasedCounterfactualReasonerprint("Imports causaux OK : StructuralCausalModel, CausalKnowledgeBase, ""InterventionalStatement, CounterfactualStatement, 2 reasoners")
JAR causal detecte : ['causal-1.30.jar']
Imports causaux OK : StructuralCausalModel, CausalKnowledgeBase, InterventionalStatement, CounterfactualStatement, 2 reasoners
Carte de l’API causale (vérifiée par réflexion sur causal-1.30.jar) :
Classe
Rôle
StructuralCausalModel
Le graphe causal : atome de fond (cause racine) + atome explicable (équation)
CausalKnowledgeBase
SCM + hypothèses sur les atomes de fond (nécessaire pour les contrefactuels)
ArgumentationBasedCausalReasoner
Requêtes observation + intervention (renvoie un boolean qualitatif)
InterventionalStatement
Encode une requête do(X)
CounterfactualStatement
Encode un contrefactuel « si X avait été différent »
ArgumentationBasedCounterfactualReasoner
Requêtes contrefactuelles (via twin model)
Note API : un atome explicable a exactement une équation causale. Pour exprimer « \(X\) arrive si \(A\)ou\(B\) », on passe une formule composée A || B, pas deux appels séparés (le second lèverait Atom already exists in the model).
3. Exemple 1 — Le baromètre de Pearl (confondeur à 3 nœuds)
Scénario canonique de la littérature causale. Trois propositions :
storm (orage) — cause racine, atome de fond.
drops (le baromètre baisse) — expliqué par storm.
rain (il pleut) — expliqué par storm.
Le graphe causal est donc storm → drops et storm → rain : storm est un confondeur de drops et rain. Intuitivement, observer que le baromètre baisse prédit la pluie (corrélation), mais forcer le baromètre à baisser ne fait pas pleuvoir (pas de causalité).
plp = PlParser()def F(s):"""Raccourci : parse une formule PL depuis une chaine."""return plp.parseFormula(s)storm = Proposition("storm")drops = Proposition("drops")rain = Proposition("rain")barometer = StructuralCausalModel()barometer.addBackgroundAtom(storm) # storm = cause racine exogenebarometer.addExplainableAtom(drops, storm) # drops <=> stormbarometer.addExplainableAtom(rain, storm) # rain <=> stormprint("SCM barometre :")print(barometer.prettyPrint())
Le graphe causal : storm → {drops, rain}. La structure drops <=> storm et rain <=> storm encode les deux flèches sortant de storm.
3.1 Niveau 1 — Observation (corrélation)
Sachant que le baromètre baisse, pleut-il ?
# Base de connaissance causale : on couvre les 2 mondes possibles pour l'atome de fondckb_bar = CausalKnowledgeBase(barometer)ckb_bar.addAssumption(storm)ckb_bar.addAssumption(Negation(storm))obs_reasoner = ArgumentationBasedCausalReasoner()# NIVEAU 1 : observation (correlation)p_rain_if_drops = obs_reasoner.query(ckb_bar, [drops], rain)print("[OBSERVATION] observe(drops=True) -> pleut-il ?", p_rain_if_drops)print(" -> Le barometre predit la pluie : P(rain | drops) = True (correlation via storm)")
[OBSERVATION] observe(drops=True) -> pleut-il ? True
-> Le barometre predit la pluie : P(rain | drops) = True (correlation via storm)
Lecture : observe(drops) → rain = True. Statistiquement correct — le baromètre baisse et la pluie sont corrélés via l’orage. Mais cela n’établit aucune causalité du baromètre vers la pluie.
3.2 Niveau 2 — Intervention do(drops) (causalité)
Si je force le baromètre à baisser (je le débranche et je le règle à la main), pleut-il ?
# NIVEAU 2 : intervention do(drops=True)do_stmt = InterventionalStatement()do_stmt.addIntervention(drops, True) # do(drops) = Truedo_stmt.setConclusion(rain)p_rain_if_do_drops = obs_reasoner.query(ckb_bar, do_stmt)print("[INTERVENTION] do(drops=True) -> pleut-il ?", p_rain_if_do_drops)print(" -> Forcer le barometre NE FAIT PAS pleuvoir : P(rain | do(drops)) = False")
[INTERVENTION] do(drops=True) -> pleut-il ? False
-> Forcer le barometre NE FAIT PAS pleuvoir : P(rain | do(drops)) = False
Lecture : do(drops) → rain = False. C’est la signature du do-calculus :
L’intervention do(drops) a remplacé l’équationdrops <=> storm par la constante True, rompant le lien storm → drops. Il ne reste alors aucun chemin de drops vers rain. C’est exactement ce que la statistique seule ne sait pas exprimer.
3.3 Visualiser l’opérateur do
La méthode intervene(prop, bool) renvoie un nouveau SCM où l’équation de prop est remplacée par une constante. Comparons avant/après :
print("SCM original :")print(" ", barometer.prettyPrint().replace("\n", "\n "))print("\nSCM apres do(drops=True) :")barometer_do = barometer.intervene(drops, True)print(" ", barometer_do.prettyPrint().replace("\n", "\n "))print("\n=> L'equation 'drops <=> storm' est devenue 'drops <=> +' (constante).")print(" Le lien storm -> drops est ROMPU : drops ne depend plus de storm.")
SCM original :
Background atoms: [storm]
Structural Equations:
drops <=> storm
rain <=> storm
SCM apres do(drops=True) :
Background atoms: [storm]
Structural Equations:
drops <=> +
rain <=> storm
=> L'equation 'drops <=> storm' est devenue 'drops <=> +' (constante).
Le lien storm -> drops est ROMPU : drops ne depend plus de storm.
4. Exemple 2 — Le réseau Sprinkler (Pearl, 4 nœuds, chemins multiples)
Le scénario le plus enseigné en causalité (Pearl, Causality 2009). Quatre propositions :
cloudy (nuageux) — cause racine.
sprinkler (arrosage activé) — expliqué par cloudy.
rain (pluie) — expliqué par cloudy.
wet (herbe mouillée) — expliqué par sprinkler OR rain (conjonction de causes).
Ici, sprinkler et rain sont corrélés (via cloudy) sans que l’un cause l’autre. C’est le piège classique : observer qu’il pleut « prédit » que l’arrosage est actif, mais forcer la pluie n’active pas l’arrosage.
C’est exactement la distinction qui permet de répondre à des questions d’action : « faut-il activer l’arrosage pour compenser le manque de pluie ? » — la statistique ne sait pas, le SCM oui.
4.2 Niveau 3 — Requête contrefactuelle
Le 3ᵉ niveau de Pearl : « sachant que l’herbe est mouillée, si l’arrosage avait été désactivé, l’herbe aurait-elle été mouillée quand même ? »
Cela requiert un twin model — un monde contrefactuel où l’intervention contrefactuelle est appliquée tout en conservant les observations du monde réel. Tweety l’encode via CounterfactualStatement + ArgumentationBasedCounterfactualReasoner.
cf_reasoner = ArgumentationBasedCounterfactualReasoner()# CONTREFACTUEL : observe(wet=True) ; si sprinkler avait ete False, wet aurait-il eu lieu ?cf = CounterfactualStatement()cf.addObservation(wet) # monde reel : on a observe wet=Truecf.addCounterfactualIntervention(sprinkler, False) # monde contrefactuel : sprinkler=Falsecf.setConclusion(wet) # question : wet dans le monde contrefactuel ?result_cf = cf_reasoner.query(ckb_sp, cf)print(f"[CONTREFACTUEL] observe(wet=True) ; 'si sprinkler avait ete OFF, herbe mouillee ?' {result_cf}")print(" -> Oui : sous cloudy=True, rain aurait mouille l'herbe meme sans arrosage.")
[CONTREFACTUEL] observe(wet=True) ; 'si sprinkler avait ete OFF, herbe mouillee ?' True
-> Oui : sous cloudy=True, rain aurait mouille l'herbe meme sans arrosage.
Lecture : le contrefactuel répond True — sachant que l’herbe est mouillée (et donc probablement cloudy=True), si l’arrosage avait été coupé, il aurait quand même plu, et l’herbe aurait été mouillée. C’est le raisonnement contrefactuel qui sous-tend la responsabilité causale et l’attribution de blâme en droit ou en médecine.
Contrainte API : CounterfactualStatement exige au moins une addAssumption par atome de fond (ici cloudy), faute de quoi le reasoner lève IllegalArgumentException: There must be at least one assumption for each background atom. Nous avons couvert les deux mondes (cloudy et ¬cloudy) pour énumérer l’incertitude exogène.
5. Exercices
Les trois exercices suivent la convention C.1 (stubs sans erreur volontaire). Complétez-les en vous inspirant des exemples ci-dessus ; le notebook doit rester exécutable de bout en bout.
Exercice 1 — Construire un SCM (éducation et revenu)
Construisez un SCM modélisant : ability (aptitude, atome de fond) cause à la fois education (niveau d’études) et income (revenu). Autrement dit, ability est un confondeur.
Définissez les propositions et le SCM (3 atomes).
Ajoutez les équations structurelles.
Vérifiez que observe(education=True) prédit income=True, mais que do(education=True) ne le fait plus (ou plus faiblement).
# Exercice 1 : a completerability = Proposition("ability")education = Proposition("education")income = Proposition("income")# TODO etudiant : construire le SCM edu_model# - ability = atome de fond# - education = f(ability)# - income = f(ability) [ability est le confondeur !]edu_model =None# TODO etudiant : StructuralCausalModel() ...# TODO etudiant : comparer observe(education) -> income vs do(education) -> incomeresult_observe =None# TODO etudiantresult_do =None# TODO etudiantprint("Exercice 1 a completer")print(" (attendu : observe(education) -> income True ; do(education) -> income False/plus faible)")
Exercice 1 a completer
(attendu : observe(education) -> income True ; do(education) -> income False/plus faible)
Exercice 2 — Prédire l’effet d’une intervention
Dans le réseau Sprinkler (sprinkler_net déjà construit), prédisez puis vérifiez :
do(cloudy=True) → wet ? (intervention sur la cause racine)
do(wet=True) → rain ? (intervention sur un effet — que se passe-t-il ?)
Réfléchissez au résultat attendu avant d’exécuter, puis confirmez avec le reasoner.
# Exercice 2 : a completer# TODO etudiant : construire 2 InterventionalStatement et interroger ckb_sp# Indice : do(wet=True) sur un EFFET ne remonte pas la causalite (le graphe est oriente).ex2_a =None# TODO etudiant : do(cloudy=True) -> wetex2_b =None# TODO etudiant : do(wet=True) -> rainprint("Exercice 2 a completer")
Exercice 2 a completer
Exercice 3 — Requête contrefactuelle
Formulez un contrefactuel sur le réseau Sprinkler : « sachant qu’il pleut (rain=True), si cloudy avait été faux, aurait-il plu quand même ? »
Construisez un CounterfactualStatement, ajoutez l’observation, l’intervention contrefactuelle, et la conclusion. N’oubliez pas les assumptions sur cloudy.
# Exercice 3 : a completer# TODO etudiant : CounterfactualStatement sur ckb_sp# observe(rain=True) ; cf-intervention cloudy=False ; conclusion = raincf_ex3 =None# TODO etudiantprint("Exercice 3 a completer")print(" (reflechir : cloudy est la cause de rain ; si cloudy avait ete faux, rain n'aurait pas eu lieu)")
Exercice 3 a completer
(reflechir : cloudy est la cause de rain ; si cloudy avait ete faux, rain n'aurait pas eu lieu)
observe(X) ≠ do(X) dès qu’il existe un confondeur. La statistique (Tweety-10 MLN, Tweety-5 observation) reste au niveau 1 ; le do-calculus (ce notebook) atteint le niveau 2.
L’opérateur do casse les liens amont : intervene(prop, bool) remplace l’équation prop <=> parents par une constante, visualisant mécaniquement la coupure causale.
Les contrefactuels nécessitent un twin model et des assumptions sur les atomes de fond ; ils sous-tendent la responsabilité causale (droit, médecine, éthique).
La causalité qualitative de Tweety complète l’approche bayésienne numérique : ici, le moteur argumente sur les mondes possibles pour décider Oui/Non, sans paramètres probabilistes.
Limites honestes (RECOVERABLE)
Le moteur causal de Tweety est qualitatif (booléen) : il ne calcule pas de probabilités numériques \(P(y \mid do(x))\), mais décide si une conclusion peut / doit suivre causalement. Pour des estimations quantitatives (effets moyens, intervalles de confiance), il faudrait coupler le SCM à un estimateur bayésien ou à des données observationnelles (front-door / back-door adjustment). C’est une frontière de paradigme, pas un bug — le verdict SOTA est SOTA-OK pour le raisonnement qualitatif, avec cette borne explicite.
Pour aller plus loin
Notebook-pont — du graphe causal au do-calculus : l’armature formelle unifiée de Pearl (échelle + 3 règles du do-calculus) exécutée sur l’outil de référence dowhy, qui relie cette série à Infer-5, PyMC-5 et ICT-5.
Judea Pearl, Causality: Models, Reasoning, and Inference (Cambridge, 2009) — la référence formelle du do-calculus.
Pearl & Mackenzie, The Book of Why (2018) — version accessible, l’échelle causale.
Tweety-10-MLN — l’autre pont symbolique/statistique (FOL pondérée), au niveau 1 de l’échelle.
Notebook 11/11 de la série TweetyProject — capstone de synthèse sur le raisonnement causal.
7. Aller plus loin — le même do-calculus, d’autres paradigmes
Tweety traite la causalité en logique propositionnelle : do(X) y est qualitatif — un atome forcé, un SCM réécrit, une réponse vrai/faux. C’est idéal pour voir la différence entre observe et do sans se noyer dans les nombres, mais Tweety ne chiffre pas un effet causal (« de combien la probabilité de pluie change-t-elle ? ») ni ne lève quantitativement un paradoxe de Simpson. Les séries probabilistes du dépôt reprennent exactement ce notebook avec des distributions :
Infer-5 — Inférence Causale : le jumeau distributionnel par message passing. P(Y|do(X)) calculé par mutilation de graphe en Infer.NET (inférence par propagation de messages — EP/VMP par défaut, Gibbs aussi disponible), ajustements backdoor et front-door, paradoxe de Simpson résolu numériquement.
PyMC-5 — Inférence Causale : la version MCMC, avec l’opérateur natif pm.do(model, {X:x}) et le contrefactuel par abduction (postérieur sur les variables exogènes).
ICT-5 — Émergence Causale : le même do monte d’un cran — la distribution d’intervention uniforme p(C)estdo(X_t = x) appliqué à tout le micro-état, et mesure à quelle échelle un système réalise le plus de travail causal (émergence).
Vue d’ensemble des quatre paradigmes : la section « Ponts causaux : le do-calculus de Pearl à travers les paradigmes » du README IIT cartographie comment le symbolique (ce notebook), le bayésien par message passing, le MCMC et la théorie de l’information instancient le même opérateur do(·).