Ce notebook est le jumeau C# / .NET de Tweety-11-Causal.ipynb — marathon de parité #4956.
La version Python s’appuie sur Tweety causal-1.30.jar (via JPype) : un reasoner causal argumentatif construit sur des Structural Causal Models booléens. Ce twin C# réimplémente le moteur causal from-scratch (BCL .NET 9, 0 NuGet) pour rendre visibles les internes : la chirurgie de graphe du do-operator, l’énumération des mondes possibles, la sémantique contrefactuelle. La lib Java Tweety = RECOVERABLE-MACHINE (Python-only) ; le moteur from-scratch EST la substance.
Pourquoi from-scratch ? L’enjeu pédagogique de l’inférence causale (Pearl) n’est pas d’appeler une boîte noire, mais de voir pourquoi P(rain | observe drops) != P(rain | do drops). Cela exige d’exposer le graphe causal orienté, la surgery do(X) qui rompt les liens entrants, et l’évaluation des mondes possibles.
1. Moteur causal — modèles structurels booléens
Un Structural Causal Model (SCM) booléen est un DAG d’atomes propositionnels : - les atomes de fond (exogènes) sont les causes racines, libres ; - les atomes expliqués (endogènes) sont définis par une équation booléenne sur leurs parents.
Étant donné une assignation des atomes de fond, tous les atomes expliqués sont déterminés par propagation (le graphe est acyclique). Un monde possible = une assignation complète cohérente avec les équations.
#nullable enable// --- Formule booleenne sur des atomes : || && ! ( ) + (vrai) - (faux) ---publicsealedclass Formula{public Func<IReadOnlyDictionary<string,bool>,bool> Eval;public HashSet<string> Vars;publicFormula(Func<IReadOnlyDictionary<string,bool>,bool> e, HashSet<string> v){ Eval = e; Vars = v;}publicstatic Formula Atom(string n)=>new(a => a[n],new(){ n });publicstatic Formula Const(bool b)=>new(_ => b,new());publicstatic Formula Not(Formula f)=>new(a =>!f.Eval(a), f.Vars);publicstatic Formula And(Formula x, Formula y)=>new(a => x.Eval(a)&& y.Eval(a),Union(x.Vars, y.Vars));publicstatic Formula Or(Formula x, Formula y)=>new(a => x.Eval(a)|| y.Eval(a),Union(x.Vars, y.Vars));static HashSet<string>Union(HashSet<string> a, HashSet<string> b){var s =new HashSet<string>(a); s.UnionWith(b);return s;}}// Petite analyse recursive descendante (tokeniseur + grammaire PL minimale).publicstaticclass FormulaParser{publicstatic Formula Parse(string src){var toks =Tokenize(src);int i =0; Formula f =ParseOr(toks,ref i);return f;}static List<string>Tokenize(string s){var t =new List<string>();int i =0;while(i < s.Length){char c = s[i];if(char.IsWhiteSpace(c)){ i++;continue;}if(c =='('|| c ==')'|| c =='+'|| c =='-'|| c =='!'){ t.Add(c.ToString()); i++;continue;}if(c =='&'&& i +1< s.Length&& s[i +1]=='&'){ t.Add("&&"); i +=2;continue;}if(c =='|'&& i +1< s.Length&& s[i +1]=='|'){ t.Add("||"); i +=2;continue;}if(char.IsLetter(c)|| c =='_'){int j = i;while(j < s.Length&&(char.IsLetterOrDigit(s[j])|| s[j]=='_')) j++; t.Add(s.Substring(i, j - i)); i = j;continue;}thrownewArgumentException($"Caractere inattendu '{c}' dans la formule : {s}");}return t;}static Formula ParseOr(List<string> t,refint i){ Formula f =ParseAnd(t,ref i);while(i < t.Count&& t[i]=="||"){ i++; f = Formula.Or(f,ParseAnd(t,ref i));}return f;}static Formula ParseAnd(List<string> t,refint i){ Formula f =ParseNot(t,ref i);while(i < t.Count&& t[i]=="&&"){ i++; f = Formula.And(f,ParseNot(t,ref i));}return f;}static Formula ParseNot(List<string> t,refint i){if(i < t.Count&& t[i]=="!"){ i++;return Formula.Not(ParseNot(t,ref i));}returnParsePrimary(t,ref i);}static Formula ParsePrimary(List<string> t,refint i){string tok = t[i];if(tok =="+"){ i++;return Formula.Const(true);}if(tok =="-"){ i++;return Formula.Const(false);}if(tok =="("){ i++; Formula f =ParseOr(t,ref i); i++;return f;}// saute ')' i++;return Formula.Atom(tok);// identificateur}}// --- Modele causal structurel booleen ---publicsealedclass StructuralCausalModel{publicenum Kind { Background, Explainable }public Dictionary<string, Kind> Kinds =new();public Dictionary<string, Formula> Eq =new();// eq d'evaluationpublic Dictionary<string,string> EqSrc =new();// eq source pour affichagepublic List<string> Background => Kinds.Where(kv => kv.Value== Kind.Background).Select(kv => kv.Key).ToList();public List<string> Explainable => Kinds.Where(kv => kv.Value== Kind.Explainable).Select(kv => kv.Key).ToList();publicvoidAddBackgroundAtom(string a){ Kinds[a]= Kind.Background;}publicvoidAddExplainableAtom(string a,string src){ Kinds[a]= Kind.Explainable; Eq[a]= FormulaParser.Parse(src); EqSrc[a]= src;}// Ordre topologique sur les atomes expliques (Kahn ; les atomes de fond sont des sources). List<string>TopoOrder(){var remaining = Explainable.ToHashSet();var order =new List<string>();bool progress =true;while(remaining.Count>0){ progress =false;foreach(var a in remaining.ToList()){bool depsOk = Eq[a].Vars.All(d =>!remaining.Contains(d));if(depsOk){ order.Add(a); remaining.Remove(a); progress =true;}}if(!progress)thrownewInvalidOperationException("SCM cyclique (equations structurelles doivent etre acycliques).");}return order;}// Propage : etant donne les atomes de fond, calcule le monde complet.public Dictionary<string,bool>Propagate(Dictionary<string,bool> bg){var w =new Dictionary<string,bool>(bg);foreach(var a inTopoOrder()) w[a]= Eq[a].Eval(w);return w;}// Tous les mondes possibles = produit de toutes les assignations des atomes de fond.public List<Dictionary<string,bool>>AllWorlds(){var bg = Background;var worlds =new List<Dictionary<string,bool>>();int n = bg.Count;for(int mask =0; mask <(1<< n); mask++){var assign =new Dictionary<string,bool>();for(int i =0; i < n; i++) assign[bg[i]]=((mask >> i)&1)==1; worlds.Add(Propagate(assign));}return worlds;}public Dictionary<string,bool>BackgroundOf(Dictionary<string,bool> world){var b =new Dictionary<string,bool>();foreach(var a in Background) b[a]= world[a];return b;}// Chirurgie du do-operator : do(X = val) rompt les liens entrants vers X (X devient constante).public StructuralCausalModel Intervene(string atom,bool val){var c =Clone(); c.Kinds[atom]= Kind.Explainable;// n'est plus libre : fixe par l'intervention c.Eq[atom]= Formula.Const(val); c.EqSrc[atom]= val ?"+":"-";return c;}public StructuralCausalModel Clone(){var c =newStructuralCausalModel();foreach(var kv in Kinds) c.Kinds[kv.Key]= kv.Value;foreach(var kv in Eq) c.Eq[kv.Key]= kv.Value;foreach(var kv in EqSrc) c.EqSrc[kv.Key]= kv.Value;return c;}publicstringPrettyPrint(){var sb =newStringBuilder();foreach(var a in Background) sb.AppendLine($" [fond] {a}");foreach(var a in Explainable) sb.AppendLine($" [explique] {a} <=> {EqSrc[a]}");return sb.ToString().TrimEnd();}}Show("StructuralCausalModel + parser de formules + propagation + Intervene (do-operator) prets.");
Ce que « from scratch » veut dire ici. Le moteur définit ci-dessus les trois briques minimales d’un modèle causal structural (Pearl) en C# pur : les variables de fond (storm — des faits exogènes sans équation), les équations structurelles (drops <=> storm — chaque variable expliquée est une fonction de ses causes), et l’interventiondo (remplacer une équation par une constante — la « chirurgie »). Pas de probabilités dans cette version : les valeurs sont booléennes et la propagation déterministe, ce qui isole la structure du raisonnement causal — le squelette que la version probabiliste habille ensuite de chiffres.
ObservationP(Y | observe X) : correlation. Parmi les mondes ou X est vrai, Y est-il etabli ?
InterventionP(Y | do X) : on applique la chirurgie do(X) puis on mesure Y. Casse les confondeurs.
Contrefactuelobserve W ; si X avait ete autre, Y aurait-il eu lieu ? : monde reel + monde contrefactuel.
Convention : une requête renvoie True si la conclusion est etablie dans tous les mondes consistants (si elle est vraie dans certains et fausse dans d’autres, la reponse est False = non determine).
#nullable enable// Base de connaissance causale : enveloppe le SCM (tous les mondes du modele sont candidats).publicsealedclass CausalKnowledgeBase{public StructuralCausalModel Model;publicCausalKnowledgeBase(StructuralCausalModel m){ Model = m;}public List<Dictionary<string,bool>>Worlds()=> Model.AllWorlds();}// Declaration d'intervention : do(atom=val) pour plusieurs atomes + conclusion.publicsealedclass InterventionalStatement{public List<(string Atom,bool Val)> Interventions =new();publicstring Conclusion ="";publicvoidAddIntervention(string atom,bool val)=> Interventions.Add((atom, val));publicvoidSetConclusion(string c)=> Conclusion = c;}// Declaration contrefactuelle : observations (monde reel) + interventions contrefactuelles + conclusion.publicsealedclass CounterfactualStatement{public List<(string Atom,bool Val)> Observations =new();public List<(string Atom,bool Val)> CfInterventions =new();publicstring Conclusion ="";publicvoidAddObservation(string atom,bool val)=> Observations.Add((atom, val));publicvoidAddCounterfactualIntervention(string atom,bool val)=> CfInterventions.Add((atom, val));publicvoidSetConclusion(string c)=> Conclusion = c;}publicsealedclass CausalReasoner{// Observation : conclusion etablie dans tous les mondes ou les atomes observes ont la valeur donnee.publicboolQuery(CausalKnowledgeBase ckb, List<(string Atom,bool Val)> observed,string conclusion){var consistent = ckb.Worlds().Where(w => observed.All(o => w[o.Atom]== o.Val)).ToList();if(consistent.Count==0)returnfalse;return consistent.All(w => w[conclusion]);}// Raccourci : observer un atome = True.publicboolQuery(CausalKnowledgeBase ckb, List<string> observedTrue,string conclusion)=>Query(ckb, observedTrue.Select(a =>(a,true)).ToList(), conclusion);// Intervention : on applique do(*) puis on demande si la conclusion est etablie dans tous les mondes.publicboolQuery(CausalKnowledgeBase ckb, InterventionalStatement stmt){var m = ckb.Model;foreach(var iv in stmt.Interventions) m = m.Intervene(iv.Atom, iv.Val);var worlds = m.AllWorlds();if(worlds.Count==0)returnfalse;return worlds.All(w => w[stmt.Conclusion]);}// Contrefactuel : mondes reels consistants avec l'observation ; sur CHACUN (fond fixe) on applique// l'intervention contrefactuelle et on re-evalue. La conclusion doit tenir dans tous les mondes contrefactuels.publicboolQuery(CausalKnowledgeBase ckb, CounterfactualStatement stmt){var actual = ckb.Worlds().Where(w => stmt.Observations.All(o => w[o.Atom]== o.Val)).ToList();if(actual.Count==0)returnfalse;foreach(var w in actual){var m = ckb.Model;foreach(var ci in stmt.CfInterventions) m = m.Intervene(ci.Atom, ci.Val);var cfWorld = m.Propagate(m.BackgroundOf(w));// fond du monde reel, puis surgeryif(!cfWorld[stmt.Conclusion])returnfalse;}returntrue;}}Show("CausalKnowledgeBase + InterventionalStatement + CounterfactualStatement + CausalReasoner prets.");
Les trois modes d’interrogation. Le CausalReasoner exposé ci-dessus distingue exactement les trois régimes de requêtes qui structurent la suite du notebook :
observe(X) — conditionnement : « sachant ce que je vois, que déduire ? » (statistique) ;
do(X) — intervention : « si je force X, que devient le système ? » (la flèche entrante de X est coupée, son équation remplacée par la valeur forcée) ;
contrefactuel — rétro-mondes : « dans le monde qui explique mon observation, si X avait différé… »
L’API du reasoner est volontairement verbeuse sur ce point : confondre observe et do est l’erreur n°1 du raisonnement causal, et le type (ObservationalStatement vs InterventionalStatement vs CounterfactualStatement) rend la confusion impossible à écrire par accident.
3. Le baromètre — corrélation n’est pas causalité
Exemple canonique : un baromètre qui tombe (drops) et la pluie (rain) sont toutes deux causées par l’orage (storm). Observer le baromètre prédit la pluie (corrélation). Forcer le baromètre ne fait PAS pleuvoir (pas de lien causal drops -> rain).
C’est la distinction fondamentale P(rain | drops) != P(rain | do drops).
-> Le barometre predit la pluie : P(rain | drops) = True (correlation via storm)
[INTERVENTION] do(drops=True) -> pleut-il ? False
-> Forcer le barometre NE FAIT PAS pleuvoir : P(rain | do(drops)) = False
Visualisation de la chirurgie : do(drops=True) remplace l’équation drops <=> storm par drops <=> + (constante). Le lien storm -> drops est rompu — drops ne dépend plus de storm.
Show("SCM original :");Show(" "+ barometer.PrettyPrint().Replace("\n","\n "));Show("\nSCM apres do(drops=True) :");var barometer_do = barometer.Intervene(drops,true);Show(" "+ barometer_do.PrettyPrint().Replace("\n","\n "));Show("\n=> L'equation 'drops <=> storm' est devenue 'drops <=> +' (constante).");Show(" Le lien storm -> drops est ROMPU : drops ne depend plus de storm.");
Un SCM plus riche : cloudy cause sprinkler et rain ; tous deux causent wet. C’est lejouet classique pour montrer que do(rain) casse le confondeur cloudy tandis que do(sprinkler) préserve la causalité directe vers wet.
Lecture du réseau. Trois différences structurelles avec le baromètre méritent d’être dites avant de lancer les requêtes :
le nœud cloudy est la cause commune de deux effets indépendants (sprinkler, rain) — c’est un confondu : il crée de la corrélation entre ses deux enfants sans que l’un cause l’autre ;
wet est un collier (collider) : il a deux parents. La corrélation entre sprinkler et rain est nulle par défaut, mais elle apparaît dès qu’on conditionne sur wet (sélection sur l’effet) ;
les équations sont déterministes (<=>), la version probabiliste de Pearl remplacerait chaque <=> par une table conditionnelle — le raisonnement structurel, lui, est identique. C’est le biais de sélection dans sa forme la plus pure : filtrer sur un effet fabrique de la dépendance entre ses causes.
Show("=== Correlation vs Causalite dans le reseau Sprinkler ===\n");// (a) OBSERVATION : observe(rain=True) -> sprinkler ?bool p_sp_if_rain = reasoner.Query(ckb_sp,new List<string>{ rain2 }, sprinkler);Show($"[OBS] observe(rain=True) -> sprinkler actif ? {p_sp_if_rain}");Show(" (correlation : rain et sprinkler coincident via cloudy)\n");// (b) INTERVENTION : do(rain=True) -> sprinkler ?var do_rain =newInterventionalStatement();do_rain.AddIntervention(rain2,true);do_rain.SetConclusion(sprinkler);bool p_sp_if_do_rain = reasoner.Query(ckb_sp, do_rain);Show($"[INT] do(rain=True) -> sprinkler actif ? {p_sp_if_do_rain}");Show(" (intervention : do(rain) CASSE cloudy->rain, donc plus de correlation avec sprinkler)\n");// (c) INTERVENTION causale directe : do(sprinkler=True) -> wet ?var do_sp =newInterventionalStatement();do_sp.AddIntervention(sprinkler,true);do_sp.SetConclusion(wet);bool p_wet_if_do_sp = reasoner.Query(ckb_sp, do_sp);Show($"[INT] do(sprinkler=True) -> herbe mouillee ? {p_wet_if_do_sp}");Show(" (causalite directe preservee : sprinkler CAUSE wet)\n");Show("=> P(sprinkler | observe rain) != P(sprinkler | do rain)");Show(" Signature du do-calculus : l'intervention isole l'effet causal propre.");
=== Correlation vs Causalite dans le reseau Sprinkler ===
(correlation : rain et sprinkler coincident via cloudy)
[INT] do(rain=True) -> sprinkler actif ? False
(intervention : do(rain) CASSE cloudy->rain, donc plus de correlation avec sprinkler)
[INT] do(sprinkler=True) -> herbe mouillee ? True
(causalite directe preservee : sprinkler CAUSE wet)
=> P(sprinkler | observe rain) != P(sprinkler | do rain)
Signature du do-calculus : l'intervention isole l'effet causal propre.
Lecture du résultat — la signature du do-calculus. Le réseau Sprinkler montre pourquoi observer n’est pas intervenir. La variable cloudy est un facteur de confusion (confounder) : elle cause à la fois rain (nuage → pluie) et sprinkler (nuage → on n’arrose pas), ce qui crée une corrélation fallacieuse entre rain et sprinkler. C’est pourquoi observe(rain=True) re monte vers sprinkler (P = True) : l’observation circule par le chemin du dossprinkler ← cloudy → rain.
L’intervention do(rain=True)chirurgie ce chemin : elle rompt cloudy → rain et fixe rain depuis l’extérieur du système. La remontée vers sprinkler disparaît (P = False) — une intervention ne remonte pas le courant causal. En revanche, do(sprinkler=True) → wet reste vraie : aucun confondeur sur ce chemin, la causalité directe est préservée.
La dernière ligne formalise la leçon centrale de Pearl : P(sprinkler | observe rain) ≠ P(sprinkler | do rain). Cette inégalité — invisible à la seule statistique observationnelle — est exactement ce que le do-calculus permet de calculer, en identifiant puis en « chirurgiant » les chemins du dos (backdoor paths) qui mêlent corrélation et causalité.
5. Raisonnement contrefactuel — mondes possibles
Un contrefactuel : « on a observé wet=True ; si sprinkler avait été OFF, l’herbe aurait-elle tout de même été mouillée ? »
Sémantique des mondes possibles : on fixe le monde réel (observé), on applique l’intervention contrefactuelle en gardant les variables exogènes fixes, puis on ré-évalue la conclusion dans le monde contrefactuel.
var cf =newCounterfactualStatement();cf.AddObservation(wet,true);// monde reel : on a observe wet=Truecf.AddCounterfactualIntervention(sprinkler,false);// monde contrefactuel : sprinkler=Falsecf.SetConclusion(wet);// question : wet dans le monde contrefactuel ?bool result_cf = reasoner.Query(ckb_sp, cf);Show($"[CONTREFACTUEL] observe(wet=True) ; 'si sprinkler avait ete OFF, herbe mouillee ?' {result_cf}");Show(" -> Oui : sous cloudy=True, rain aurait mouille l'herbe meme sans arrosage.");
-> Oui : sous cloudy=True, rain aurait mouille l'herbe meme sans arrosage.
Les trois temps du contrefactuel. Le verdict True ci-dessus se déroule en trois étapes, toutes présentes dans le code de la cellule :
Abduction — conditionner sur l’observation wet=True : le monde possible est reconstitué (cloudy=True, donc rain=True a eu lieu — c’est l’inférence qui explique le fait observé) ;
Action — appliquer l’antécédent du contrefactuel dans ce monde : écraser l’équation sprinkler avec OFF (la même chirurgie do que section 4, mais dans le monde contrefactuel) ;
Prédiction — évaluer la conséquence dans le monde modifié : rain reste actif, l’herbe reste mouillée, réponse True.
C’est exactement la structure du raisonnement « si Napoléon n’avait pas… » : on n’imagine pas un monde au hasard, on imagine le nôtre avec une seule équation changée. Le contraste avec l’exercice 3 sera instructif : quand l’antécédent porte sur la cause, la même machinerie répond l’inverse.
6. Parité jpype ⇔ IKVM — le moteur causal TweetyProject via le pont .NET
Le jumeau Python (Tweety-11-Causal.ipynb) charge la même librairie Java TweetyProject org.tweetyproject.causal (v1.30) via jpype ; ce twin C# charge la même librairie via IKVM (pont Java⇄.NET). C’est la parité visée par la série (#10381) : deux écosystèmes, un moteur de production.
L’implémentation from-scratch des sections 1–5 reste le cœur pédagogique (elle expose la chirurgie du graphe). Cette section l’accompagne et démontre que le vrai moteur Java produit les mêmes équations structurelles, chirurgies do(.) et mondes contrefactuels — la sémantique from-scratch n’est pas une réinvention arbitraire, c’est la sémantique de TweetyProject.
La DLL org.tweetyproject.tweety-causal.dll (shade IKVM 8.15, v1.30) expose 13/13 classes causales actives — la 14ᵉ (SimpleCausalReasoner) est du code mort commenté en amont (PR #10566, leçon tweety8-gate).
// Chargement du pont IKVM (TweetyProject v1.30 -> .NET) — recette éprouvée (cf. Tweety-5 §4.1.2).#r "nuget: IKVM, 8.15.0"#r "nuget: IKVM.Image, 8.15.0"#r "org.tweetyproject.tweety-causal.dll"using System.IO;using System.Reflection;// IKVM runtime : home explicite (économie de cold-restart, recette c.758)string ikvmVer ="8.15.0";// RID de la machine courante (win-x64, linux-x64, osx-arm64...) : IKVM.Image tire l'image native de chaque plateforme.string ikvmRid =(OperatingSystem.IsWindows()?"win": OperatingSystem.IsMacOS()?"osx":"linux")+"-"+ System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();string nugetRoot = Environment.GetEnvironmentVariable("NUGET_PACKAGES")?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),".nuget","packages");string ikvmBaseAny = Path.Combine(nugetRoot,"ikvm.image", ikvmVer,"ikvm","any","any");string ikvmArchDir = Path.Combine(nugetRoot,"ikvm.image.runtime."+ ikvmRid, ikvmVer,"ikvm","any", ikvmRid);string ikvmHome = Path.Combine(Path.GetTempPath(),"ikvm-home-"+ ikvmVer +"-"+ ikvmRid);if(Directory.Exists(ikvmBaseAny)&& Directory.Exists(ikvmArchDir)){ Directory.CreateDirectory(ikvmHome);foreach(var f in Directory.GetFiles(ikvmBaseAny,"*", SearchOption.AllDirectories)){var t = f.Replace(ikvmBaseAny, ikvmHome); Directory.CreateDirectory(Path.GetDirectoryName(t)); File.Copy(f, t,true);}foreach(var f in Directory.GetFiles(ikvmArchDir,"*", SearchOption.AllDirectories)){var t = f.Replace(ikvmArchDir, ikvmHome); Directory.CreateDirectory(Path.GetDirectoryName(t)); File.Copy(f, t,true);}}AppContext.SetData("IKVM.Home", ikvmHome);var causalDll ="org.tweetyproject.tweety-causal.dll";Show($"DLL causale chargée : {AssemblyName.GetAssemblyName(causalDll)} ({new FileInfo(causalDll).Length / 1024.0:F0} Ko)");
=> 'sprinkler <=> cloudy' est devenu 'sprinkler <=> +' (constante) : le moteur Java
coupe bien le lien cloudy -> sprinkler, exactement comme la chirurgie from-scratch du §3.
Parité confirmée. Le moteur causal TweetyProject (Java, via IKVM) produit exactement la même chirurgie que l’implémentation from-scratch du §3–§4 : do(sprinkler=True) remplace l’équation sprinkler <=> cloudy par la constante + (vrai), rompant le lien cloudy -> sprinkler.
Les deux ponts — jpype (côté Python) et IKVM (côté C#) — accèdent au même moteur de production. La réimplémentation from-scratch n’était pas une réinvention arbitraire : c’est la sémantique exposée de ce moteur. C’est la valeur pédagogique de la parité : voir les internes (from-scratch) puis confirmer que le moteur de référence les reproduit (IKVM).
Au-delà de la chirurgie — getTwinModel() construit le twin network de Pearl (les variables étoilées sprinkler*, wet* sont les copies contrefactuelles du §5), et le reasoner argumentatif ArgumentationBasedCausalReasoner résout les requêtes via SAT (sat4j). Ces capacités supplémentaires du moteur Java sont accessibles par le même pont IKVM.
Plafond upstream du pont causal — diagnostic réfuté (PR #10471). La v1 du shade IKVM ne matérialisait que 5 des 14 classes du contrat org.tweetyproject.causal.* — un verdict INTRINSIC avait été posé au registre SOTA (#3801). Les 9 classes alors réputées inaccessibles : CausalParser, AbstractArgumentationBasedCausalReasoner, AbstractCausalReasoner, ArgumentationBasedCausalReasoner, ArgumentationBasedCounterfactualReasoner, CausalInterpretation, CausalArgument, CausalKnowledgeBase, StructuralCausalModel.
La cause n’était pas une limite IKVM mais un shade JAR incomplet : recompilées javac --release 8 (recette #10566) puis re-shadées, la DLL v1.30 committée (#10570) matérialise l’intégralité du contrat — les 13 classes d’API et la classe imbriquée StructuralCausalModel+CyclicDependencyException (séparateur .NET +), vérifié par GetType sur la DLL committée — cohérent avec le « 13/13 classes actives » annoncé à la cellule 17 et la parité vérifiée ci-dessus. L’issue #10471 est close comme superseded : il n’y a plus de plafond upstream.
Construisez un SCM où ability cause à la fois education et income (ability est le conclondant). Comparez observe(education=True) -> income (biaisé par le conclondant) et do(education=True) -> income (effet causal propre).
Indice : avec ability comme cause commune, l’observation educationporte l’information sur ability (donc sur income), mais l’intervention do(education) coupe ce lien — l’effet direct devrait tomber à False.
#nullable enable// Exercice 1 : a completervar ability ="ability";var education ="education";var income ="income";// TODO etudiant : construire le SCM edu_model// - ability = atome de fond// - education = f(ability)// - income = f(ability) [ability est le confondeur !]StructuralCausalModel? edu_model =null;// TODO etudiant : new StructuralCausalModel() ...// TODO etudiant : comparer observe(education) -> income vs do(education) -> incomebool? result_observe =null;// TODO etudiantbool? result_do =null;// TODO etudiantShow("Exercice 1 a completer");Show(" (attendu : observe(education) -> income True ; do(education) -> income False)");
Exercice 1 a completer
(attendu : observe(education) -> income True ; do(education) -> income False)
Exercice 2 — L’intervention ne remonte pas la causalité
Sur ckb_sp, comparez do(cloudy=True) -> wet (cause ancêtre, effet transmis) et do(wet=True) -> rain. Le graphe est orienté : forcer un effet ne propage rien vers ses causes.
#nullable enable// 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).InterventionalStatement? ex2_a =null;// TODO etudiant : do(cloudy=True) -> wetInterventionalStatement? ex2_b =null;// TODO etudiant : do(wet=True) -> rainShow("Exercice 2 a completer");
Exercice 2 a completer
Exercice 3 — Contrefactuel sur la cause
Construisez un CounterfactualStatement sur ckb_sp : observe(rain=True) ; intervention contrefactuelle cloudy=False ; conclusion rain. Réfléchissez : cloudy est la cause de rain — si cloudy avait été faux, rain aurait-il eu lieu ?
#nullable enable// Exercice 3 : a completer// TODO etudiant : CounterfactualStatement sur ckb_sp// observe(rain=True) ; cf-intervention cloudy=False ; conclusion = rainCounterfactualStatement? cf_ex3 =null;// TODO etudiantShow("Exercice 3 a completer");Show(" (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)
Synthèse
Ce twin C# a réimplémenté from-scratch les trois piliers de l’inférence causale de Pearl :
Requête
Sémantique implémentée
observe(X)
filtrer les mondes possibles consistants
do(X)
chirurgie de graphe : remplacer l’équation de X par une constante
contrefactuel
monde réel observé + surgery sur fond fixe
La lib Tweety causal-1.30.jar (reasoner argumentatif) fournit la même sémantique en boîte noire ; l’implémentation from-scratch rend visible pourquoi l’intervention isole l’effet causal propre — la valeur pédagogique.
Notebook-pont — du graphe causal au do-calculus : la chirurgie de graphe implémentée from-scratch ici (remplacer l’équation de X par une constante) est la sémantique du do-operator ; le pont en montre le calcul — l’échelle de causalité et les 3 règles de Pearl exécutées sur l’outil de référence dowhy (identification backdoor/front-door + estimation + réfutation), en quantitatif là où ce twin décide en booléen. Il relie cette série à Infer-5, PyMC-5 et ICT-5.