Infer-5-Causal-Inference : Inférence Causale et do-calculus

Serie : Programmation Probabiliste avec Infer.NET (5/19) Duree estimee : 65 minutes Prerequis : Infer-4-Bayesian-Networks (reseaux bayesiens, CPT, D-separation)


Objectifs

  • Distinguer observation P(Y|X) et intervention P(Y|do(X)) (Pearl, 2000)
  • Implementer le do-opérateur par mutilation de graphe en Infer.NET
  • Calculer l’effet causal via backdoor et front-door adjustment
  • Reconnaitre le paradoxe de Simpson et le resoudre causalement
  • Aborder le contrefactuel (Niveau 3 de l’echelle de Pearl)
  • Decomposer un effet en part directe et indirect (mediation, section 8)

1. Pourquoi la causalite ? — L’echelle de Pearl

La probabilite seule repond aux questions d’association. Mais de nombreuses questions pratiques sont causales :

Question Type Reponse probabiliste
Si je vois le barometre baisser, quelle est la proba d’une tempete ? Observation P(Storm\|Baro) Oui
Si je provoque la chute du barometre, la tempete vient-elle ? Intervention P(Storm\|do(Baro)) Non (sans causalite)
Si j’avais pris le medicament (alors que je ne l’ai pas pris), aurais-je gueri ? Contrefactuel Non

Jude Pearl (2000, Causality, Cambridge University Press) structure le raisonnement causal en trois niveaux (l’echelle de Pearl) :

  1. Niveau 1 — Association (voir) : P(Y|X). “Que disent les données ?”
  2. Niveau 2 — Intervention (faire) : P(Y|do(X)). “Que se passe-t-il si j’agis ?”
  3. Niveau 3 — Contrefactuel (imaginer) : P(Y_x | X', Y'). “Que se serait-il passe si ?”

Inegalite fondamentale : P(Y|X) != P(Y|do(X)) des qu’un confondeur (cause commune de X et Y) biaise l’observation. Ce notebook montre comment un reseau bayesien observationnel ne sait pas calculer do(X) seul — il faut mutiler le graphe (couper les arcs entrants de X), une opération que nous implementons explicitement en Infer.NET.

Références : Pearl, J. (1995), Causal diagrams for empirical research, JRSS B 57(3) ; Pearl, J. (2000), Causality : Models, Reasoning, and Inference, Cambridge University Press ; Pearl, J., Glymour, M. & Jewell, N. P. (2016), Causal Inference in Statistics : A Primer, Wiley.

2. Configuration d’Infer.NET

Chargement du moteur d’inference probabiliste et du helper de visualisation des graphes de facteurs (même #load que toute la serie, cf Infer-3, Infer-4).

// Installation Infer.NET
#r "nuget: Microsoft.ML.Probabilistic"
#r "nuget: Microsoft.ML.Probabilistic.Compiler"

using Microsoft.ML.Probabilistic;
using Microsoft.ML.Probabilistic.Distributions;
using Microsoft.ML.Probabilistic.Models;
using Microsoft.ML.Probabilistic.Algorithms;
using Microsoft.ML.Probabilistic.Compiler;

Console.WriteLine("Infer.NET charge.");
Installing Packages
  • Microsoft.ML.Probabilistic
  • Microsoft.ML.Probabilistic.Compiler
Infer.NET charge.

Chargement du helper pour visualiser les graphes de facteurs (Graphviz).

// Chargement du helper de visualisation des graphes de facteurs
#load "FactorGraphHelper.cs"

Console.WriteLine("FactorGraphHelper charge.");
Console.WriteLine($"Graphviz disponible : {FactorGraphHelper.IsGraphvizAvailable()}");
FactorGraphHelper charge.
Graphviz disponible : True

3. Exemple 1 — Le barometre (confondeur a 3 noeuds)

Pearl illustre la confusion causale par le barometre : un barometre qui baisse annonce une tempete, mais provoquer la chute du barometre ne declenche pas de tempete. Pourquoi ? Parce qu’une cause commune — la basse pression atmospherique — provoque a la fois la chute du barometre ET la tempete.

            BassePression
               /    \
              v      v
        Barometre   Tempete
  • BassePression -> Barometre : quand la pression baisse, le barometre baisse.
  • BassePression -> Tempete : quand la pression baisse, la tempete vient.
  • Pas d’arc Barometre -> Tempete : le barometre n’est qu’un indicateur, pas une cause.

Le barometre et la tempete sont donc correles (observation) mais il n’y a aucun effet causal de l’un sur l’autre (intervention). Construisons ce SCM (Structural Causal Model) et affichons son graphe de facteurs.

// SCM du barometre (Pearl) : BassePression -> {Barometre, Storm}
Variable<bool> bPressFG = Variable.Bernoulli(0.30).Named("basse_pression");  // P(basse pression)=0.30
Variable<bool> baroFG    = Variable.New<bool>().Named("barometre_baisse");
using (Variable.If(bPressFG))   { baroFG.SetTo(Variable.Bernoulli(0.90)); }  // basse pression -> baro baisse
using (Variable.IfNot(bPressFG)){ baroFG.SetTo(Variable.Bernoulli(0.10)); }  // haute pression -> baro stable
Variable<bool> stormFG   = Variable.New<bool>().Named("tempete");
using (Variable.If(bPressFG))   { stormFG.SetTo(Variable.Bernoulli(0.80)); } // basse pression -> tempete
using (Variable.IfNot(bPressFG)){ stormFG.SetTo(Variable.Bernoulli(0.10)); } // haute pression -> calme

// Rendu SVG reel du graphe de facteurs via Graphviz (meme demarche qu'Infer-3 / Infer-4)
var engineFG = new InferenceEngine();
engineFG.Compiler.CompilerChoice = CompilerChoice.Roslyn;
engineFG.ShowFactorGraph = true;
var _ = engineFG.Infer<Bernoulli>(stormFG);

Console.WriteLine("Structure du SCM Barometre : BassePression -> {Barometre, Tempete}.");
Console.WriteLine("(Graphe de facteurs ci-dessous : aucun arc direct Barometre -> Tempete.)");
display(HTML(FactorGraphHelper.GetLatestFactorGraphHtml()));
Compiling model...done.
Structure du SCM Barometre : BassePression -> {Barometre, Tempete}.
(Graphe de facteurs ci-dessous : aucun arc direct Barometre -> Tempete.)
Model_09_18_26_05_25_38_96.svg
Model node0 Bernoulli(0,8) node1 Random node0->node1 dist node2 tempete node1->node2 node3 basse_pression node3->node2 condition node10 barometre_baisse node3->node10 condition node4 Bernoulli(0,1) node5 Random node4->node5 dist node5->node2 node6 Bernoulli(0,3) node7 Random node6->node7 dist node7->node3 node8 Bernoulli(0,9) node9 Random node8->node9 dist node9->node10 node11 Bernoulli(0,1) node12 Random node11->node12 dist node12->node10

3.1 Niveau 1 — Observation P(Storm | Barometre baisse)

Si on observe le barometre baisser, la probabilite d’une tempete grimpe : la baisse est un signal que la pression est probablement basse, et donc qu’une tempete approche. Mais c’est une simple association statistique.

// NIVEAU 1 — Observation : P(Storm | Barometre baisse = True)
Variable<bool> prObs = Variable.Bernoulli(0.30).Named("prObs");
Variable<bool> baroObs = Variable.New<bool>().Named("baroObs");
using (Variable.If(prObs))    { baroObs.SetTo(Variable.Bernoulli(0.90)); }
using (Variable.IfNot(prObs)) { baroObs.SetTo(Variable.Bernoulli(0.10)); }
Variable<bool> stormObs = Variable.New<bool>().Named("stormObs");
using (Variable.If(prObs))    { stormObs.SetTo(Variable.Bernoulli(0.80)); }
using (Variable.IfNot(prObs)) { stormObs.SetTo(Variable.Bernoulli(0.10)); }

baroObs.ObservedValue = true;   // on OBSERVE le barometre baisser

var eObs = new InferenceEngine();
eObs.Compiler.CompilerChoice = CompilerChoice.Roslyn;
double pStormObs = eObs.Infer<Bernoulli>(stormObs).GetProbTrue();
Console.WriteLine($"P(Storm | Barometre baisse) = {pStormObs:F3}   (observationnel)");
Console.WriteLine("Conclusion naive : 'le barometre predit la tempete'.");
Compiling model...done.
P(Storm | Barometre baisse) = 0,656   (observationnel)
Conclusion naive : 'le barometre predit la tempete'.

3.2 Niveau 2 — Intervention P(Storm | do(Barometre baisse))

Maintenant provoquons la chute du barometre (par exemple en tapant dessus). Pour modeliser cette intervention, on mutile le graphe : on coupe l’arc BassePression -> Barometre et on force Barometre = baisse independamment de la pression. En Infer.NET cela se traduit par Variable.Bernoulli(1.0) (valeur forcee, sans parent) au lieu d’un SetTo conditionne a la pression — c’est l’idiome d’introduction du do() (déjà utilise en Infer-4 cell39).

// NIVEAU 2 — Intervention : P(Storm | do(Barometre baisse))
// Mutilation : on COUPE l'arc BassePression -> Barometre en forcant Barometre sans parent.
Variable<bool> prInt = Variable.Bernoulli(0.30).Named("prInt");
Variable<bool> baroInt = Variable.Bernoulli(1.0).Named("baroInt");   // force a True, SANS dependre de prInt = do()
Variable<bool> stormInt = Variable.New<bool>().Named("stormInt");
using (Variable.If(prInt))    { stormInt.SetTo(Variable.Bernoulli(0.80)); }
using (Variable.IfNot(prInt)) { stormInt.SetTo(Variable.Bernoulli(0.10)); }

var eInt = new InferenceEngine();
eInt.Compiler.CompilerChoice = CompilerChoice.Roslyn;
double pStormDo = eInt.Infer<Bernoulli>(stormInt).GetProbTrue();
Console.WriteLine($"P(Storm | do(Barometre baisse)) = {pStormDo:F3}   (interventionnel)");
Console.WriteLine($"P(Storm) marginale             = 0.310");
Console.WriteLine();
Console.WriteLine($"=> Observer le barometre informe sur la tempete : {pStormObs:F3}");
Console.WriteLine($"   PROVOQUER sa chute ne change rien a la tempete : {pStormDo:F3}");
Console.WriteLine("   Voir != Faire (Pearl, 2000).");
Compiling model...done.
P(Storm | do(Barometre baisse)) = 0,310   (interventionnel)
P(Storm) marginale             = 0.310

=> Observer le barometre informe sur la tempete : 0,656
   PROVOQUER sa chute ne change rien a la tempete : 0,310
   Voir != Faire (Pearl, 2000).

Résultat : P(Storm|Barometre baisse) (observation) != P(Storm|do(Barometre baisse)) (intervention). L’effet causal de l’intervention est nul — la chute du barometre ne provoque pas de tempete. L’observation était biaisee par le confondeur BassePression.

Prong-B (non-trivialite) : ce résultat n’est pas une lecture triviale de CPT. Un reseau bayesien observationnel calcule P(Storm|Barometre baisse) mais ne sait pas calculer do(Barometre baisse) seul — il a fallu mutiler le graphe (couper l’arc entrant). Les deux quantites différent visiblement, ce qui prouve que le do() fait un travail reel.

4. Reseau Sprinkler — cross-check avec PyMC-4

Verifions sur le reseau canonique de Pearl (Cloudy -> {Sprinkler, Rain} -> WetGrass, déjà construit en Infer-4) que nos valeurs Infer.NET recoupent le jumeau Python/PyMC. La requête P(Cloudy | Rain=True) vs P(Cloudy | do(Rain=True)) vaut 0.800 vs 0.500 dans PyMC-4 (section 8).

// Reseau Sprinkler (Pearl 4 noeuds) — meme structure qu'Infer-4
Variable<bool> cloudySpr = Variable.Bernoulli(0.5).Named("cloudySpr");
Variable<bool> sprinklerSpr = Variable.New<bool>().Named("sprinklerSpr");
using (Variable.If(cloudySpr))    { sprinklerSpr.SetTo(Variable.Bernoulli(0.1)); }
using (Variable.IfNot(cloudySpr)) { sprinklerSpr.SetTo(Variable.Bernoulli(0.5)); }
Variable<bool> rainSpr = Variable.New<bool>().Named("rainSpr");
using (Variable.If(cloudySpr))    { rainSpr.SetTo(Variable.Bernoulli(0.8)); }
using (Variable.IfNot(cloudySpr)) { rainSpr.SetTo(Variable.Bernoulli(0.2)); }

// OBSERVATIONNEL : P(Cloudy | Rain=True)
rainSpr.ObservedValue = true;
var eSprO = new InferenceEngine();
eSprO.Compiler.CompilerChoice = CompilerChoice.Roslyn;
double pCloudyObsSpr = eSprO.Infer<Bernoulli>(cloudySpr).GetProbTrue();
Console.WriteLine($"P(Cloudy | Rain=True)      = {pCloudyObsSpr:F3}   (observationnel, theorie 0.800)");

// INTERVENTIONNEL : P(Cloudy | do(Rain=True)) — couper Cloudy -> Rain
Variable<bool> cloudyDo = Variable.Bernoulli(0.5).Named("cloudyDo");
Variable<bool> rainDo   = Variable.Bernoulli(1.0).Named("rainDo");  // force, independant de cloudy
var eSprD = new InferenceEngine();
eSprD.Compiler.CompilerChoice = CompilerChoice.Roslyn;
double pCloudyDoSpr = eSprD.Infer<Bernoulli>(cloudyDo).GetProbTrue();
Console.WriteLine($"P(Cloudy | do(Rain=True))  = {pCloudyDoSpr:F3}   (interventionnel, theorie 0.500)");
Console.WriteLine();
Console.WriteLine("Cross-check PyMC-4 (meme structure) : 0.800 obs vs 0.500 do  => Infer.NET coherent.");
Compiling model...done.
P(Cloudy | Rain=True)      = 0,800   (observationnel, theorie 0.800)
Compiling model...done.
P(Cloudy | do(Rain=True))  = 0,500   (interventionnel, theorie 0.500)

Cross-check PyMC-4 (meme structure) : 0.800 obs vs 0.500 do  => Infer.NET coherent.

5. Backdoor adjustment

Quand le confondeur U est observe, l’effet causal se calcule sans mutiler le graphe, par la formule d’adjustment backdoor (Pearl, 1995) :

P(Y | do(X)) = Somme_u P(Y | X, u) P(u)

On somme l’effet de X sur Y sur toutes les valeurs du confondeur, ponds par la distribution marginale de U. Si U bloque tous les chemins “backdoor” (arcs entrants de X passant par U), cette formule restitue l’effet causal exact. Verifions sur le barometre, ou U = BassePression bloque le chemin Barometre <- BassePression -> Tempete : l’ajustement doit coincider avec le do() direct (0.310).

// BACKDOOR ADJUSTMENT : P(Storm | do(Baro)) = Somme_pression P(Storm | Baro, pression) P(pression)
// Ici Storm independant de Baro | Pression  =>  P(Storm | Baro, pression) = P(Storm | pression)
// (le moteur d'inference calcule chaque conditionnelle)
double pBassePression = 0.30;

// P(Storm | pression=basse) et P(Storm | pression=haute), inferees par le moteur
double InferStormGiven(bool pressionBasse) {
    var p = Variable.New<bool>(); p.ObservedValue = pressionBasse;
    var s = Variable.New<bool>();
    using (Variable.If(p))    { s.SetTo(Variable.Bernoulli(0.80)); }
    using (Variable.IfNot(p)) { s.SetTo(Variable.Bernoulli(0.10)); }
    var e = new InferenceEngine(); e.Compiler.CompilerChoice = CompilerChoice.Roslyn;
    return e.Infer<Bernoulli>(s).GetProbTrue();
}

double pStormBasse = InferStormGiven(true);   // = 0.80
double pStormHaute = InferStormGiven(false);  // = 0.10
double backdoor = pStormBasse * pBassePression + pStormHaute * (1.0 - pBassePression);
Console.WriteLine($"Backdoor = P(Storm|basse)*{pBassePression} + P(Storm|haute)*{1.0-pBassePression}");
Console.WriteLine($"        = {pStormBasse:F2}*{pBassePression} + {pStormHaute:F2}*{1.0-pBassePression} = {backdoor:F3}");
Console.WriteLine($"do(Barometre) direct      = 0.310");
Console.WriteLine($"=> Les deux methodes coincident ({backdoor:F3} ~ 0.310) : verification de coherence.");
Compiling model...done.
Compiling model...done.
Backdoor = P(Storm|basse)*0,3 + P(Storm|haute)*0,7
        = 0,80*0,3 + 0,10*0,7 = 0,310
do(Barometre) direct      = 0.310
=> Les deux methodes coincident (0,310 ~ 0.310) : verification de coherence.

6. Front-door adjustment

Quand le confondeur U est non observe mais qu’un medicateur M entre X et Y est observe et capture tout l’effet de X sur Y, Pearl (1995) montre que l’effet causal est identifiable par la formule front-door :

P(Y | do(X)) = Somme_m P(M=m | X) * Somme_x' P(Y | M=m, x') P(x')

Exemple canonique (Pearl) : X = fumer, M = goudron dans les poumons, Y = cancer, U = genotype (non observe, cause a la fois l’envie de fumer et le risque de cancer). On n’a pas acces au genotype, mais le goudron transmet tout l’effet causal de la fumee sur le cancer.

// FRONT-DOOR : X=smoke, M=tar, Y=cancer, U=genotype (non observe)
// P(Y|do(X)) = Somme_m P(M=m|X) * Somme_x' P(Y|M=m,x') P(x')

// P(X=1) marginal (genotype marginalise)
double PmarginalX1() {
    var u = Variable.Bernoulli(0.20);
    var x = Variable.New<bool>();
    using (Variable.If(u))    { x.SetTo(Variable.Bernoulli(0.80)); }
    using (Variable.IfNot(u)) { x.SetTo(Variable.Bernoulli(0.30)); }
    var e = new InferenceEngine(); e.Compiler.CompilerChoice = CompilerChoice.Roslyn;
    return e.Infer<Bernoulli>(x).GetProbTrue();
}
// P(M=1 | X=1) — on INTERROGE m, on NE l'observe PAS
double PMgivenX1() {
    var x = Variable.New<bool>(); x.ObservedValue = true;
    var m = Variable.New<bool>();
    using (Variable.If(x))    { m.SetTo(Variable.Bernoulli(0.90)); }
    using (Variable.IfNot(x)) { m.SetTo(Variable.Bernoulli(0.10)); }
    var e = new InferenceEngine(); e.Compiler.CompilerChoice = CompilerChoice.Roslyn;
    return e.Infer<Bernoulli>(m).GetProbTrue();
}
// P(Y=1 | M=m, X=x') — on observe M ET X, on interroge Y
double PYgivenMX(bool smokeVal, bool mVal) {
    var u = Variable.Bernoulli(0.20);
    var x = Variable.New<bool>(); x.ObservedValue = smokeVal;
    var m = Variable.New<bool>();
    using (Variable.If(x))    { m.SetTo(Variable.Bernoulli(0.90)); }
    using (Variable.IfNot(x)) { m.SetTo(Variable.Bernoulli(0.10)); }
    m.ObservedValue = mVal;
    var y = Variable.New<bool>();
    using (Variable.If(m)) {
        using (Variable.If(u))    { y.SetTo(Variable.Bernoulli(0.95)); }
        using (Variable.IfNot(u)) { y.SetTo(Variable.Bernoulli(0.70)); }
    }
    using (Variable.IfNot(m)) {
        using (Variable.If(u))    { y.SetTo(Variable.Bernoulli(0.50)); }
        using (Variable.IfNot(u)) { y.SetTo(Variable.Bernoulli(0.05)); }
    }
    var e = new InferenceEngine(); e.Compiler.CompilerChoice = CompilerChoice.Roslyn;
    return e.Infer<Bernoulli>(y).GetProbTrue();
}
// Verification : effet causal DIRECT par mutilation (coupe U -> X)
double PdoSmoke() {
    var u = Variable.Bernoulli(0.20);
    var x = Variable.Bernoulli(1.0);   // do(X=1) : force, independant de u
    var m = Variable.New<bool>();
    using (Variable.If(x))    { m.SetTo(Variable.Bernoulli(0.90)); }
    using (Variable.IfNot(x)) { m.SetTo(Variable.Bernoulli(0.10)); }
    var y = Variable.New<bool>();
    using (Variable.If(m)) {
        using (Variable.If(u))    { y.SetTo(Variable.Bernoulli(0.95)); }
        using (Variable.IfNot(u)) { y.SetTo(Variable.Bernoulli(0.70)); }
    }
    using (Variable.IfNot(m)) {
        using (Variable.If(u))    { y.SetTo(Variable.Bernoulli(0.50)); }
        using (Variable.IfNot(u)) { y.SetTo(Variable.Bernoulli(0.05)); }
    }
    var e = new InferenceEngine(); e.Compiler.CompilerChoice = CompilerChoice.Roslyn;
    return e.Infer<Bernoulli>(y).GetProbTrue();
}

double pX1 = PmarginalX1();                     // P(X=1)
double pm1 = PMgivenX1();                       // P(M=1|X=1)
double pm0 = 1.0 - pm1;                         // P(M=0|X=1)
// Somme_x' P(Y|M=m,x') P(x')
double innerM1 = PYgivenMX(true,true)*pX1  + PYgivenMX(false,true)*(1-pX1);
double innerM0 = PYgivenMX(true,false)*pX1 + PYgivenMX(false,false)*(1-pX1);
double frontDoor = pm1*innerM1 + pm0*innerM0;
double doDirect  = PdoSmoke();
Console.WriteLine($"P(X=smoke) marginal = {pX1:F3}");
Console.WriteLine($"P(M=1|X=1) = {pm1:F3} ; P(M=0|X=1) = {pm0:F3}");
Console.WriteLine($"Front-door   P(Cancer | do(Smoke)) = {frontDoor:F3}  (ajustement sans acceder a U)");
Console.WriteLine($"do(X=1) direct (mutilation)       = {doDirect:F3}  (verification)");
Console.WriteLine($"=> Les deux coincident : la formule front-door restitue l'effet causal.");
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
P(X=smoke) marginal = 0,400
P(M=1|X=1) = 0,900 ; P(M=0|X=1) = 0,100
Front-door   P(Cancer | do(Smoke)) = 0,689  (ajustement sans acceder a U)
do(X=1) direct (mutilation)       = 0,689  (verification)
=> Les deux coincident : la formule front-door restitue l'effet causal.

7. Paradoxe de Simpson

Un renversement de Simpson survient quand une conclusion s’inverse entre l’analyse agregee et l’analyse conditionnelle. Cas clinique : un medicament semble nuire a la guerison dans la population totale, alors qu’il aide dans chaque sous-groupe (patients graves / patients legers). La cause : la severite est un confondeur — les patients graves, qui guerrissent moins bien, recoivent aussi plus souvent le medicament.

// SIMPSON REVERSAL : drug + severity (confondeur)
double RecQuery(bool drugObs, bool? sevObs) {
    var sev = Variable.Bernoulli(0.5);                       // severe = true
    var drug = Variable.New<bool>();
    using (Variable.If(sev))    { drug.SetTo(Variable.Bernoulli(0.8)); }  // graves -> plus de drug
    using (Variable.IfNot(sev)) { drug.SetTo(Variable.Bernoulli(0.2)); }
    var rec = Variable.New<bool>();
    using (Variable.If(sev)) {
        using (Variable.If(drug))    { rec.SetTo(Variable.Bernoulli(0.5)); }  // grave+drug  -> 0.5
        using (Variable.IfNot(drug)) { rec.SetTo(Variable.Bernoulli(0.3)); }  // grave+nodrug -> 0.3
    }
    using (Variable.IfNot(sev)) {
        using (Variable.If(drug))    { rec.SetTo(Variable.Bernoulli(0.9)); }  // leger+drug  -> 0.9
        using (Variable.IfNot(drug)) { rec.SetTo(Variable.Bernoulli(0.7)); }  // leger+nodrug -> 0.7
    }
    drug.ObservedValue = drugObs;
    if (sevObs.HasValue) sev.ObservedValue = sevObs.Value;
    var e = new InferenceEngine(); e.Compiler.CompilerChoice = CompilerChoice.Roslyn;
    return e.Infer<Bernoulli>(rec).GetProbTrue();
}

double recDrugAgg    = RecQuery(true,  null);   // P(Rec|Drug)      agrege
double recNoDrugAgg  = RecQuery(false, null);   // P(Rec|NoDrug)    agrege
double recDrugSev    = RecQuery(true,  true);   // P(Rec|Drug, severe)
double recNoDrugSev  = RecQuery(false, true);   // P(Rec|NoDrug, severe)
double recDrugMild   = RecQuery(true,  false);  // P(Rec|Drug, leger)
double recNoDrugMild = RecQuery(false, false);  // P(Rec|NoDrug, leger)
// Backdoor do(drug) = Somme_sev P(Rec|drug,sev) P(sev)
double doDrug   = 0.5*recDrugSev   + 0.5*recDrugMild;
double doNoDrug = 0.5*recNoDrugSev + 0.5*recNoDrugMild;

Console.WriteLine("=== ANALYSE AGREGEE (biaisee par la severite) ===");
Console.WriteLine($"P(Rec | Drug)    = {recDrugAgg:F3}");
Console.WriteLine($"P(Rec | NoDrug)  = {recNoDrugAgg:F3}   => le medicament semble NUIRE");
Console.WriteLine();
Console.WriteLine("=== ANALYSE CONDITIONNELLE (verite causale) ===");
Console.WriteLine($"Graves  : Drug {recDrugSev:F2}   >   NoDrug {recNoDrugSev:F2}");
Console.WriteLine($"Legers  : Drug {recDrugMild:F2}   >   NoDrug {recNoDrugMild:F2}");
Console.WriteLine("=> Le medicament AIDE dans chaque sous-groupe. REVERSAL de l'agrege.");
Console.WriteLine();
Console.WriteLine("=== EFFET CAUSAL (backdoor sur la severite) ===");
Console.WriteLine($"P(Rec | do(Drug))   = {doDrug:F3}");
Console.WriteLine($"P(Rec | do(NoDrug)) = {doNoDrug:F3}   => le medicament AIDE causalement.");
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
=== ANALYSE AGREGEE (biaisee par la severite) ===
P(Rec | Drug)    = 0,580
P(Rec | NoDrug)  = 0,620   => le medicament semble NUIRE

=== ANALYSE CONDITIONNELLE (verite causale) ===
Graves  : Drug 0,50   >   NoDrug 0,30
Legers  : Drug 0,90   >   NoDrug 0,70
=> Le medicament AIDE dans chaque sous-groupe. REVERSAL de l'agrege.

=== EFFET CAUSAL (backdoor sur la severite) ===
P(Rec | do(Drug))   = 0,700
P(Rec | do(NoDrug)) = 0,500   => le medicament AIDE causalement.

8. Mediation – effet direct et effet indirect

La front-door (section 6) identifie l’effet total de X sur Y en passant par le mediateur M. Mais elle ne dit pas combien de cet effet transite par M (chemin indirect X -> M -> Y) versus le chemin direct (X -> Y). C’est la question de la mediation : decomposer l’effet total en une part mediee et une part directe.

On définit le Controlled Direct Effect (CDE) : l’effet de X sur Y quand on fige le mediateur a une valeur m par une seconde intervention do(M=m) :

\[ \mathrm{CDE}(m) = P(Y{=}1 \mid do(X{=}1), do(M{=}m)) - P(Y{=}1 \mid do(X{=}0), do(M{=}m)) \]

Si l’effet total TE = P(Y|do(X=1)) - P(Y|do(X=0)) est superieur au CDE, la différence capte la part qui passe par le mediateur (effet indirect). C’est une double mutilation de graphe – le même opérateur do() qu’en section 3.2, applique deux fois.

Exemple. X = suivre un programme de formation, M = competences acquises, Y = obtenir une promotion. La formation peut mener a la promotion de deux facons : directement (effet de signal / diplome) ou indirectement (les competences declenchent la promotion).

// MEDIATION : X (formation) -> M (competences) -> Y (promotion), plus chemin direct X -> Y.
// CPTs connus. Effet total (TE) + Controlled Direct Effect (CDE) en figeant M par do(M).

// P(Y=1 | do(X=x)) -- effet TOTAL (M libre : capte chemin direct + indirect via M)
double PY_doX(bool xVal) {
    var x = Variable.New<bool>().Named("x"); x.ObservedValue = xVal;
    var m = Variable.New<bool>().Named("m");
    using (Variable.If(x))    { m.SetTo(Variable.Bernoulli(0.80)); }  // formation -> competences
    using (Variable.IfNot(x)) { m.SetTo(Variable.Bernoulli(0.20)); }
    var y = Variable.New<bool>().Named("y");
    using (Variable.If(x)) {
        using (Variable.If(m))    { y.SetTo(Variable.Bernoulli(0.90)); }  // formation + competences
        using (Variable.IfNot(m)) { y.SetTo(Variable.Bernoulli(0.60)); }  // formation seule (signal)
    }
    using (Variable.IfNot(x)) {
        using (Variable.If(m))    { y.SetTo(Variable.Bernoulli(0.70)); }  // competences sans formation
        using (Variable.IfNot(m)) { y.SetTo(Variable.Bernoulli(0.30)); }  // baseline
    }
    var e = new InferenceEngine(); e.Compiler.CompilerChoice = CompilerChoice.Roslyn;
    return e.Infer<Bernoulli>(y).GetProbTrue();
}

// P(Y=1 | do(X=x), do(M=m)) -- CDE : on FIGE le mediateur (double mutilation)
double PY_doX_doM(bool xVal, bool mVal) {
    var x = Variable.New<bool>().Named("x"); x.ObservedValue = xVal;
    var m = Variable.New<bool>().Named("m"); m.ObservedValue = mVal;  // do(M) fige le mediateur
    var y = Variable.New<bool>().Named("y");
    using (Variable.If(x)) {
        using (Variable.If(m))    { y.SetTo(Variable.Bernoulli(0.90)); }
        using (Variable.IfNot(m)) { y.SetTo(Variable.Bernoulli(0.60)); }
    }
    using (Variable.IfNot(x)) {
        using (Variable.If(m))    { y.SetTo(Variable.Bernoulli(0.70)); }
        using (Variable.IfNot(m)) { y.SetTo(Variable.Bernoulli(0.30)); }
    }
    var e = new InferenceEngine(); e.Compiler.CompilerChoice = CompilerChoice.Roslyn;
    return e.Infer<Bernoulli>(y).GetProbTrue();
}

double TE   = PY_doX(true) - PY_doX(false);
double CDE0 = PY_doX_doM(true, false) - PY_doX_doM(false, false);
double CDE1 = PY_doX_doM(true, true)  - PY_doX_doM(false, true);

Console.WriteLine("Mediation : X=formation, M=competences, Y=promotion");
Console.WriteLine($"  Effet total        TE     = P(Y|do(X=1))           - P(Y|do(X=0))           = {TE:F3}");
Console.WriteLine($"  Effet direct CDE   a M=0  = P(Y|do(X=1),do(M=0))   - P(Y|do(X=0),do(M=0))   = {CDE0:F3}");
Console.WriteLine($"  Effet direct CDE   a M=1  = P(Y|do(X=1),do(M=1))   - P(Y|do(X=0),do(M=1))   = {CDE1:F3}");
Console.WriteLine($"  -> part mediee (indirect) ~= TE - CDE(M=0) = {TE - CDE0:F3}  (chemin via les competences)");
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
Mediation : X=formation, M=competences, Y=promotion
  Effet total        TE     = P(Y|do(X=1))           - P(Y|do(X=0))           = 0,460
  Effet direct CDE   a M=0  = P(Y|do(X=1),do(M=0))   - P(Y|do(X=0),do(M=0))   = 0,300
  Effet direct CDE   a M=1  = P(Y|do(X=1),do(M=1))   - P(Y|do(X=0),do(M=1))   = 0,200
  -> part mediee (indirect) ~= TE - CDE(M=0) = 0,160  (chemin via les competences)
// EFFETS NATURELS (Pearl 2001) : NDE et NIE calcules sur le SCM de mediation ci-dessus.
// Pas de confondeur M <- U -> Y : la sequential ignorability tient, les quantites
// "cross-world" se calculent par enumeration sur le meme SCM (monde imbrique).
// P(M=m | do(X=x)) : le mediateur sous intervention (CPT direct du SCM)
double PM_doX(bool xVal, bool mVal) {
    double pM1 = xVal ? 0.80 : 0.20;
    return mVal ? pM1 : 1.0 - pM1;
}

// NDE_{0,1} = Somme_m P(m|do(0)) * [P(Y|do(1),do(m)) - P(Y|do(0),do(m))]
//   (mediateur a sa valeur NATURELLE sous X=0, pas figee -- c'est le contrefactuel imbrique)
double NDE = PM_doX(false, true)  * (PY_doX_doM(true, true)  - PY_doX_doM(false, true))
           + PM_doX(false, false) * (PY_doX_doM(true, false) - PY_doX_doM(false, false));

// NIE_{1,0} = Somme_m [P(m|do(1)) - P(m|do(0))] * P(Y=1|do(1),do(m))
//   (Y sous X=1, seul le CHANGEMENT du mediateur porte l'effet)
double NIE = (PM_doX(true, true) - PM_doX(false, true))     * PY_doX_doM(true, true)
           + (PM_doX(true, false) - PM_doX(false, false))   * PY_doX_doM(true, false);

Console.WriteLine("Effets naturels (Pearl 2001) sur le SCM formation -> competences -> promotion :");
Console.WriteLine($"  NDE = {NDE:F3}   (effet direct, mediateur naturel sous X=0)");
Console.WriteLine($"  NIE = {NIE:F3}   (effet indirect, Y evalue sous X=1)");
Console.WriteLine($"  NDE + NIE = {NDE + NIE:F3}   vs TE = {TE:F3}   -> ecart |TE - NDE - NIE| = {Math.Abs(TE - NDE - NIE):F3}");
Console.WriteLine($"  Raccourci CDE : NDE =? CDE(0) -> {NDE:F3} vs {CDE0:F3} ; part mediee =? TE-CDE(0) -> {NIE:F3} vs {TE - CDE0:F3}");
Console.WriteLine("  (NDE + NIE = TE : identite algebrique ; ce qui derive, ce sont les raccourcis via CDE)");
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
Compiling model...done.
Effets naturels (Pearl 2001) sur le SCM formation -> competences -> promotion :
  NDE = 0,280   (effet direct, mediateur naturel sous X=0)
  NIE = 0,180   (effet indirect, Y evalue sous X=1)
  NDE + NIE = 0,460   vs TE = 0,460   -> ecart |TE - NDE - NIE| = 0,000
  Raccourci CDE : NDE =? CDE(0) -> 0,280 vs 0,300 ; part mediee =? TE-CDE(0) -> 0,180 vs 0,160
  (NDE + NIE = TE : identite algebrique ; ce qui derive, ce sont les raccourcis via CDE)

Lecture. L’effet total de la formation sur la promotion vaut 0,460 (46 points de probabilite). Mais quand on fige les competences acquises (do(M=0), “formation sans competences”), l’effet direct retombe a 0,300. La différence 0,160 est la part mediee : ce que la formation apporte a la promotion via les competences. Le CDE a M=1 (0,200) est encore plus bas, car do(M=1) place tout le monde en position “competent”, ou la formation ajoute moins.

Ce que montre la cellule. La front-door (section 6) donnait l’effet total via le mediateur ; la mediation decompose cet effet. C’est le même opérateur do(), applique deux fois (double mutilation) – l’inference par message passing Infer.NET (exacte sur ce SCM discret) rend TE et CDE calculables separement et comparables. Non-trivialite : l’effet direct (0,300) differe de l’effet total (0,460) de facon visible, preuve que le mediateur porte une part reelle de l’effet – dans un SCM degenere sans chemin indirect, TE egalerais exactement CDE.

Au-dela du CDE : effets naturels (NDE, NIE) et caveats

Le Controlled Direct Effect (CDE) qu’on vient de calculer suppose une seconde intervention do(M=m) qui fige le mediateur a une valeur arbitraire. Pearl (2001, Direct and indirect effects in mediation analysis) distingue cela des effets naturels, qui demandent ce que serait Y si on observait la valeur “naturelle” de M sous le contrefactuel do(X=x') au lieu de la fixer. Formellement :

  • NDE (Natural Direct Effect) = Y_{1,M_{X=0}} - Y_{0,M_{X=0}} : effet direct avec le mediateur a sa valeur naturelle sous X=0.
  • NIE (Natural Indirect Effect) = Y_{1,M_{X=1}} - Y_{1,M_{X=0}} : effet indirect transmis par le changement “naturel” de M quand X bascule de 0 a 1.
  • TE = NDE + NIE (decomposition additive, Pearl 2001, Robins & Greenland 1992).

Dans Infer.NET, le NDE/NIE necessite une hypothese supplemental : la sequential ignorability (ou cross-world independence) sur les contrefactuels M_{X=x'} – c’est-a-dire que la distribution de M sous do(X=x') reste compatible avec le meme SCM. Sur notre SCM de mediation (X -> M -> Y, pas de confondeur M <- U -> Y), cette hypothese tient : les effets naturels sont calculables par enumeration sur le meme SCM (cellule suivante). Les valeurs mesurees : NDE = 0,280, NIE = 0,180, et NDE + NIE = 0,460 = TE – une identite algebrique (le telescopage des deux sommes rend la decomposition exacte pour tout SCM discret), pas une coincidence.

Ce que l’interaction X*M casse en revanche, ce sont les raccourcis via le CDE : notre monde a CDE(1) = 0,200 != CDE(0) = 0,300, donc NDE != CDE(0) (0,280 vs 0,300) et la “part mediee” TE - CDE(M=0) = 0,160 sous-estime le vrai NIE = 0,180 :

NDE + NIE = TE                     (exacte, toujours -- identite de telescopage)
NDE = CDE(m)                       (faux des que CDE varie en m : interaction)
part mediee = TE - CDE(M=0)        (approximation : 0,160 au lieu de 0,180 ici)

Quand CDE != NDE : si un confondeur U influence a la fois M et Y (par exemple, le “motivation” qui determine a la fois les competences acquises ET la reconnaissance patronale), CDE a do(M=0) ne capture pas l’effet direct reel – il faut le NDE, qui requiert des methodes d’inference contrefactuelle specifiques (voir section 10 et la discussion dans Pearl 2001, sec. 4). Infer.NET, comme moteur de message passing determinant sur graphes discrets, rend ce cas plus difficile (les contrefactuels “cross-world” sortent du formalisme d’un seul DAG).

Pourquoi cette clarif importe pedagogiquement. Un·e etudiant·e qui voit “TE = 0,460, CDE = 0,300, donc part mediee = 0,160” pourrait conclure hativement que la decomposition est toujours TE - CDE. Ce n’est vrai qu’en l’absence de confondeur de mediation. Le scaffold NDE/NIE prepare la transition vers les methodes contrefactuelles (section 10) et les gemives du monde reel ou le confondeur U est la regle, pas l’exception (Pearl 2014, Interpretation and Identification of Causal Mediation).

9. Le do-calculus (règles de Pearl)

Pearl (1995) a demontre que toute quantite causale identifiable peut être calculee a partir de trois règles de reecriture, qui transforment des expressions avec do() en expressions observationnelles (sans do()). Soit G_X le graphe mutile (arcs entrants de X supprimes) et G_{underline{X}} le graphe ou les arcs sortants de X sont supprimes :

Règle Enonce Effet
Règle 1 (insertion/deletion) P(y \| do(x), z, w) = P(y \| do(x), w) si Y _\|\|_ Z \| X, W dans G_X Observations irrelevantes peuvent être retirees
Règle 2 (action/observation) P(y \| do(x), do(z), w) = P(y \| do(x), z, w) si Y _\|\|_ Z \| X, W dans G_{underline{X}} Une action peut être remplacée par une observation
Règle 3 (insertion/deletion d’actions) P(y \| do(x), do(z), w) = P(y \| do(x), w) si Y _\|\|_ Z \| X, W dans G_{X, tilde{Z(W)}} Une action irrelevant peut être retiree

Critere backdoor (Shpitser, Pearl & Verma) : un ensemble Z satisfait le critere backdoor relatif a (X,Y) si aucun noeud de Z n’est un descendant de X, et Z bloque tous les chemins backdoor (arcs entrants de X). Alors P(Y|do(X)) = Somme_z P(Y|X,Z)P(Z) — c’est exactement la formule appliquee en section 5.

Théorème de completude (Shpitser & Pearl, 2006) : les trois règles sont complètes — si un effet causal n’est pas calculable par ces règles, il n’est pas identifiable a partir du graphe seul. C’est un plafond fondamental, pas une limite d’implementation.

10. Niveau 3 — Contrefactuel (capstone)

Le niveau 3 de l’echelle de Pearl repond aux questions contrefactuelles : “Sachant qu’un patient a pris le medicament ET a gueri, aurait-il gueri SANS medicament ?”

On resout cela en deux étapes (twin-network / abduction-prediction) :

  1. Abduction : inferer le posterieur sur les variables latentes (ici la severite) sachant ce qu’on observe (pris le medicament, a gueri).
  2. Prediction/action : reutiliser ce posterieur comme prior dans le graphe mutile (do(NoDrug)) pour predire le résultat contrefactuel.

Cette étape est plus couteuse qu’une simple intervention (elle requiert une inference posterieure puis une seconde inference). C’est honnetement le plafond pratique de l’inference causale déterministe (message passing) sur ce type de SCM.

// CONTREFACTUEL (Niveau 3) : abduction + prediction
// "Sachant drug=true ET rec=true, aurait-il gueri si do(drug=false) ?"

// ETAPE 1 — Abduction : posterieur sur 'severe' sachant (drug=true, rec=true)
var sevAbd = Variable.Bernoulli(0.5).Named("sevAbd");
var drugAbd = Variable.New<bool>().Named("drugAbd");
using (Variable.If(sevAbd))    { drugAbd.SetTo(Variable.Bernoulli(0.8)); }
using (Variable.IfNot(sevAbd)) { drugAbd.SetTo(Variable.Bernoulli(0.2)); }
var recAbd = Variable.New<bool>().Named("recAbd");
using (Variable.If(sevAbd)) {
    using (Variable.If(drugAbd))    { recAbd.SetTo(Variable.Bernoulli(0.5)); }
    using (Variable.IfNot(drugAbd)) { recAbd.SetTo(Variable.Bernoulli(0.3)); }
}
using (Variable.IfNot(sevAbd)) {
    using (Variable.If(drugAbd))    { recAbd.SetTo(Variable.Bernoulli(0.9)); }
    using (Variable.IfNot(drugAbd)) { recAbd.SetTo(Variable.Bernoulli(0.7)); }
}
drugAbd.ObservedValue = true;   // il a pris le medicament
recAbd.ObservedValue  = true;   // il a gueri
var eAbd = new InferenceEngine(); eAbd.Compiler.CompilerChoice = CompilerChoice.Roslyn;
double pSevereGiven = eAbd.Infer<Bernoulli>(sevAbd).GetProbTrue();   // P(severe | drug, rec)
Console.WriteLine($"Abduction : P(severe | drug=true, rec=true) = {pSevereGiven:F3}");

// ETAPE 2 — Prediction : do(drug=false) avec severe tire du posterieur
var sevPred = Variable.Bernoulli(pSevereGiven).Named("sevPred");
var drugPred = Variable.Bernoulli(0.0).Named("drugPred");   // do(NoDrug) : force a false
var recPred = Variable.New<bool>().Named("recPred");
using (Variable.If(sevPred)) {
    using (Variable.If(drugPred))    { recPred.SetTo(Variable.Bernoulli(0.5)); }
    using (Variable.IfNot(drugPred)) { recPred.SetTo(Variable.Bernoulli(0.3)); }
}
using (Variable.IfNot(sevPred)) {
    using (Variable.If(drugPred))    { recPred.SetTo(Variable.Bernoulli(0.9)); }
    using (Variable.IfNot(drugPred)) { recPred.SetTo(Variable.Bernoulli(0.7)); }
}
var ePred = new InferenceEngine(); ePred.Compiler.CompilerChoice = CompilerChoice.Roslyn;
double pRecCounterfactual = ePred.Infer<Bernoulli>(recPred).GetProbTrue();
Console.WriteLine($"Contrefactuel : P(rec | do(NoDrug), 'pris drug & gueri') = {pRecCounterfactual:F3}");
Console.WriteLine();
Console.WriteLine($"=> Meme si le patient a gueri AVEC le medicament, sans medicament il n'aurait eu");
Console.WriteLine($"   qu'environ {pRecCounterfactual*100:F0}% de chances de guerison (contrefactuel).");
Compiling model...done.
Abduction : P(severe | drug=true, rec=true) = 0,690
Compiling model...done.
Contrefactuel : P(rec | do(NoDrug), 'pris drug & gueri') = 0,424

=> Meme si le patient a gueri AVEC le medicament, sans medicament il n'aurait eu
   qu'environ 42% de chances de guerison (contrefactuel).

11. Exercices

Quatre exercices pour ancrer le do-calculus. Chaque stub s’execute sans erreur (Console.WriteLine) ; a vous de compléter le code entre les // TODO.

# Exercice Concept
1 Construire un SCM confondu (education -> revenu) Niveau 1 observation vs margeur
2 Backdoor adjustment sur un reseau personnalise Identification de l’ensemble Z
3 Front-door : smoke -> tar -> cancer (Pearl) Adjustement par medicateur
4 Paradoxe de Simpson personnalise Renversement agrege vs conditionnel

Exercice 1 - SCM confondu : education -> revenu

Concept (cf. section 3, barometre) : un même parent latent U (niveau d’études des parents) cause a la fois X (education de l’enfant) et Y (revenu). L’observation P(Revenu | Education=haute) et l’intervention P(Revenu | do(Education=haute)) différent : seule la seconde isole l’effet causal de l’education, en coupant l’influence de U sur X.

Objectif : coder le SCM (CPT via Variable.If comme en section 3), calculer les deux quantites, puis mutiler l’arc U -> X (Bernoulli(1.0) force) pour le do().

Les étapes (// Étape N) et l’indice sont dans le code ci-dessous.

// Exercice 1 : Construire un SCM confondu "education -> revenu"
// Modele suggere : U=niveau_etudes_parents cause a la fois Education (X) et Revenu (Y),
//                  X -> Y (l'education augmente le revenu). Calculez P(Revenu|Education) vs P(Revenu|do(Education)).
// Indice : codez les CPT avec Variable.If comme dans la section 3, puis mutilez l'arc U->X pour le do().
// Etape 1 : definir les variables et CPT
// Etape 2 : P(Revenu | Education=haute) observationnel
// Etape 3 : P(Revenu | do(Education=haute)) interventionnel
Console.WriteLine("Exercice 1 a completer : SCM education -> revenu (observation vs intervention).");
Exercice 1 a completer : SCM education -> revenu (observation vs intervention).

Exercice 2 - Backdoor adjustment : identifier l’ensemble Z

Concept (cf. section 5) : quand un confondeur Z est observe et bloque tous les chemins backdoor vers X, l’effet causal s’obtient sans mutiler le graphe, par la formule d’ajustement backdoor (Pearl, 1995) :

\[P(Y \mid do(X)) = \sum_z P(Y \mid X, z)\, P(z)\]

Objectif : choisir un reseau a 3-4 noeuds avec un confondeur observe, identifier l’ensemble d’ajustement Z, appliquer la formule, et comparer au do() direct (les deux doivent coïncider si Z est valide).

// Exercice 2 : Backdoor adjustment — identifier l'ensemble Z et calculer P(Y|do(X))
// Reprenez un reseau a 3-4 noeuds avec un confondeur observe Z.
// Indice : verifiez que Z bloque tous les chemins backdoor vers X, puis appliquez Somme_z P(Y|X,z)P(z).
// Etape 1 : choisir Z (ensemble d'ajustement)
// Etape 2 : calculer chaque P(Y|X,z) via le moteur
// Etape 3 : sommer poids par P(z) et comparer au do() direct
Console.WriteLine("Exercice 2 a completer : backdoor adjustment, identifier Z et calculer P(Y|do(X)).");
Exercice 2 a completer : backdoor adjustment, identifier Z et calculer P(Y|do(X)).

Exercice 3 - Front-door : smoke -> tar -> cancer (Pearl)

Concept (cf. section 6) : quand le confondeur U (predisposition génétique) est non observe mais qu’un mediateur M (goudron dans les poumons) est observe et capture tout l’effet de X (fumer) sur Y (cancer), Pearl (1995) montre l’identifiabilite par la formule front-door :

\[P(Y \mid do(X)) = \sum_m P(M=m \mid X) \sum_{x'} P(Y \mid M=m, x')\, P(x')\]

Objectif : adapter la section 6 avec de nouvelles CPT (fumer passif, exposition professionnelle) en preservant la structure X -> M -> Y + U -> X, U -> Y (U reste non observe).

// Exercice 3 : Front-door sur smoke -> tar -> cancer (classique Pearl)
// Adaptez la section 6 en changeant les CPT (par ex. fumer passif, exposition professionnelle).
// Indice : la structure X -> M -> Y + U -> X, U -> Y doit etre preservee (U non observe).
// Etape 1 : fixer de nouvelles CPT coherentes
// Etape 2 : recalculer P(M|X), P(x'), P(Y|M,x')
// Etape 3 : appliquer la formule front-door et interpreter
Console.WriteLine("Exercice 3 a completer : front-door smoke -> tar -> cancer (Pearl).");
Exercice 3 a completer : front-door smoke -> tar -> cancer (Pearl).

Exercice 4 - Paradoxe de Simpson personnalise

Concept (cf. section 7) : un renversement de Simpson survient quand une conclusion s’inverse entre l’analyse agregee et l’analyse conditionnelle - typiquement parce qu’un confondeur Z favorise X tout en defavorisant Y (les patients graves, moins soignes, faussent la moyenne).

Objectif : concevoir des CPT produisant un renversement (ex. traitement + age, ou genre + admission Berkeley 1973), puis montrer que P(Y|X) agrege differere de P(Y|do(X)) et expliquer le mécanisme. C’est le cas ou la conclusion naive (agregee) est causalement fausse - le coeur de pourquoi l’ajustement causal importe.

// Exercice 4 : Paradoxe de Simpson personnalise
// Concevez des CPT ou la conclusion s'INVERSE entre l'agrege et le conditionnel.
// Indice : il faut un confondeur Z tel que Z favorise a la fois X et defavorise Y (ou inverse).
// Etape 1 : choisir un scenario (ex. traitement + age, ou genre + admission Berkeley 1973)
// Etape 2 : coder des CPT produisant un renversement
// Etape 3 : montrer P(Y|X) agrege != P(Y|do(X)) et expliquer le mecanisme
Console.WriteLine("Exercice 4 a completer : concevoir un paradoxe de Simpson (renversement agrege vs conditionnel).");
Exercice 4 a completer : concevoir un paradoxe de Simpson (renversement agrege vs conditionnel).

Constellation causale – le do-calculus en quatre paradigmes

Le do-calculus de Pearl (1995, 2000) est un calcul formel indépendant du langage : les trois règles d’identification d’un effet causal ne dependent pas de l’implementation. Ce depot le met en oeuvre dans quatre paradigmes distincts, qui choisissent chacun ce qu’ils calculent versus ce sur quoi ils raisonnent.

Paradigme Moteur / langage Idiome du do(X) Notebook Ce qu’il apporte
Probabiliste distributionnel Infer.NET (C#) Mutilation de graphe : Variable.Bernoulli(1.0) force la valeur et coupe l’arc parent ce notebook Effet causal calcule par message passing déterministe (EP/VMP) sur le graphe mutile
Probabiliste (transformation) PyMC (Python) Opérateur natif pm.do : l’intervention est une transformation de modèle de première classe PyMC-5 do rebranche comme un opérateur du langage probabiliste
Symbolique propositionnel Tweety (Java) Raisonneur contrefactuel : logique classique, preuves argumentatives (pas de probabilites) Tweety-11 Raisonnement qualitatif : “X cause-t-il Y ?” sans chiffres
Emergence causale (echelle) PyPhi (Python) Intervention sur la partition : do_coarse_grain / do_blackbox force une echelle macro et mesure l’information effective (EI) ICT-05-CausalEmergence-Python Le do s’applique a l’echelle : quelle description (micro vs macro) est causalement la plus forte (Hoel 2013, Jansma & Hoel 2025)

Valeurs croisees (verification inter-paradigme). Sur le même exemple du barometre (confondeur BassePression -> {Barometre, Storm}), les deux versants probabilistes recoupent exactement :

Quantite Infer.NET (ce notebook, sec 3) PyMC-5
P(Storm \| Barometre baisse) (observation) 0,656 0,656
P(Storm \| do(Barometre baisse)) (intervention) 0,310 0,310

L’effondrement de la correlation (0,656 -> 0,310) lorsqu’on passe de l’observation a l’intervention est le signal que le moteur fait un vrai travail causal (mutilation, pas une simple lecture de CPT) – et les deux moteurs, malgre leurs idiomes différents (mutilation Infer.NET vs pm.do PyMC), convergent vers les mêmes valeurs. Sur le reseau Sprinkler, P(Cloudy|Rain)=0,800 vs P(Cloudy|do(Rain))=0,500 (sec 4 ici, et PyMC-4 sec 8).

L’angle ICT (emergence causale). Le quatrieme paradigme, ICT-05-CausalEmergence-Python, deplace la question : au lieu d’intervenir sur une variable (do(X=x)), il intervient sur l’echelle de description (do_coarse_grain) et mesure quelle partition (micro vs macro) maximise l’information effective. C’est le même opérateur d’intervention de Pearl, applique a la granularite du modèle plutot qu’a la valeur d’un noeud – d’ou l’expression emergence causale (la macro peut être causalement plus forte que la micro).

Ce qu’il faut retenir. Quatre implementations, un seul do-calculus. Le paradigme Infer.NET met l’accent sur la structure (mutiler le graphe = une opération que le moteur d’inference execute) ; PyMC met l’accent sur la composabilite (do comme transformation de modèle) ; Tweety met l’accent sur le raisonnement (causalite sans nombres) ; ICT (PyPhi) met l’accent sur l’echelle (intervenir sur la partition, pas sur la variable). Choisir l’un ou l’autre, c’est choisir ce que l’on veut : des probabilites déterministes (message passing), du sampling flexible, des preuves qualitatives, ou identifier la bonne echelle de description.

12. Synthese

Niveau Question Opérateur Méthode Infer.NET
1 Association Que voit-on ? P(Y\|X) ObservedValue
2 Intervention Que se passe-t-il si on agit ? P(Y\|do(X)) Mutilation : Bernoulli(1.0) force, coupe l’arc parent
3 Contrefactuel Que se serait-il passe ? P(Y_x\|X',Y') Abduction (posterieur) + prediction dans graphe mutile
Adjustment Quand Formule
Backdoor Confondeur U observe P(Y\|do(X)) = Somme_u P(Y\|X,u)P(u)
Front-door Confondeur non observe, medicateur M observe P(Y\|do(X)) = Somme_m P(M=m\|X) Somme_x' P(Y\|M=m,x')P(x')

Ce qu’il faut retenir

  • P(Y|X) != P(Y|do(X)) : l’observation est biaisee par les confondeurs. Un reseau bayesien observationnel ne sait pas calculer do(X) seul — il faut mutiler le graphe.
  • La mutilation n’est pas une lecture de CPT : couper les arcs entrants de X est une opération structurelle que le moteur d’inference Infer.NET execute après qu’on a redefini X sans parent. La différence visible entre observation et intervention prouve que le do() fait un travail reel (Prong-B).
  • Backdoor et front-door rendent l’effet causal identifiable a partir de données observationnelles, sous conditions graphiques — formalisees par le do-calculus (Pearl 1995), complet (Shpitser & Pearl 2006).

Pour aller plus loin

  • Notebook-pont — du graphe causal au do-calculus : l’armature formelle unifiée de Pearl (échelle + 3 règles) exécutée sur l’outil de référence dowhy (identification backdoor/front-door + estimation + réfutation), qui relie cette série à Tweety-11, PyMC-5 et ICT-5.

  • Pont symbolique : Tweety-11-Causal traite le do() et les contrefactuels en Java/Tweety (raisonnement propositionnel) — même echelle de Pearl, paradigme différent.

  • Pont Python : PyMC-04-Bayesian-Networks demontre la même distinction P(Cloudy|Rain) vs P(Cloudy|do(Rain)) en MCMC.

  • Références : Pearl (2000) Causality ; Pearl, Glymour & Jewell (2016) Causal Inference in Statistics : A Primer ; Shpitser & Pearl (2006) Identification of Conditional Interventional Effects.


← Infer-10-Thompson-Sampling | Retour a la serie

Retour au sommet