CausalBridges-04 — La découverte de structure : le graphe qu’on n’a pas

Renumerotation #17421 : ce carnet etait publie sous le nom DoWhy-3 ; les renvois internes de la serie citent desormais le numero courant.

CausalBridges-02 (section 4, « le graphe assumé ») identifie, estime et réfute un effet sur un graphe donné. Mais d’où vient ce graphe ? D’une expertise métier — ou des données elles-mêmes. Ce notebook traite la question que CausalBridges-02 laissait ouverte : que peut-on retrouver du graphe causal depuis les données seules, et où s’arrête exactement ce que les données peuvent dire ?

Moteurs : causal-learn pour la découverte (PC, GES, DirectLiNGAM — réellement exécutés, règle F / SOTA-OK), dowhy pour le retour à l’estimand. Kernel coursia-ml-training.

Plan : 1. un monde DGP-connu — 2. PC (contraintes) — 3. GES (score) — 4. la classe d’équivalence — 5. LiNGAM (hypothèse fonctionnelle) — 6. l’échec honnête — 7. l’ambiguïté se propage à l’estimand — 8. exercices — 9. synthèse.

Durée : ~45 min. Prérequis : CausalBridges-02 (estimand, backdoor), DAG, indépendance conditionnelle.

# Imports et kernel
import sys
from pathlib import Path

import numpy as np
import pandas as pd

# L'organe canonique de la serie dowhy pour la decouverte -- DoWhy-3 (issue #14049).
# Le notebook consomme l'organe, il ne redefinit pas les fonctions (lecon #13921).
# Recherche dans cwd et dans tous les dossiers proches (le kernel peut demarrer
# depuis la racine du repo, pas depuis le dossier du notebook).
_CANDIDATES = [Path.cwd().resolve()]
for _p in Path.cwd().resolve().parents:
    _CANDIDATES.append(_p)
for _p in _CANDIDATES:
    _hits = list(_p.rglob("dowhy_discovery_organs.py"))
    if _hits:
        _organs_dir = _hits[0].parent
        if str(_organs_dir) not in sys.path:
            sys.path.insert(0, str(_organs_dir))
        break

import dowhy_discovery_organs as ddo
print(f"dowhy_discovery_organs charge depuis : {_organs_dir}")
print(f"  DAG vrai : {ddo.ARETES_DAG_VRAI}")
print(f"  alpha PC par defaut : {ddo.ALPHA_PC_DEFAUT} | seuil coef LiNGAM : {ddo.SEUIL_COEF_LINGAM}")
dowhy_discovery_organs charge depuis : D:\Dev\CoursIA-14049\MyIA.AI.Notebooks\Probas\DecisionTheory\Causal-Bridges
  DAG vrai : [('C', 'X'), ('X', 'M'), ('M', 'Y'), ('C', 'Y'), ('Y', 'Z')]
  alpha PC par defaut : 0.05 | seuil coef LiNGAM : 0.1

1. Le monde — cinq variables, un DAG connu

Le simulateur de l’organe produit un monde linéaire à bruits indépendants :

Variable Rôle Équation
C confondeur observé C = Bruit(1.0)
X traitement X = 1.0·C + Bruit(0.5)
M médiateur M = 1.0·X + Bruit(0.5)
Y résultat Y = 0.8·M + 0.3·C + Bruit(0.5)
Z descendant de Y (capteur) Z = 1.0·Y + Bruit(0.5)

Le DAG vrai C→X→M→Y, C→Y, Y→Z ne porte qu’une seule v-structure (collisionneur non briqué) : C → Y ← M.

La question du notebook : depuis un échantillon de ce monde, sans connaître ces équations, que retrouve chaque famille de découverte — et que refuse-t-elle à juste titre de trancher ?

# Le monde gaussien, DGP-connu
df_monde = ddo.generer_donnees_decouverte(n=2000, bruit="gaussien", seed=42)
print(df_monde.head(3).round(3).to_string())
print()
print("Matrice de correlation :")
print(df_monde.corr().round(2).to_string())
       C      X      M      Y      Z
0  0.497  0.159 -0.273 -0.626 -0.643
1 -0.138 -0.211 -0.226 -0.538 -0.790
2  0.648  0.251  0.260 -0.068 -0.155

Matrice de correlation :
      C     X     M     Y     Z
C  1.00  0.89  0.80  0.81  0.77
X  0.89  1.00  0.91  0.86  0.81
M  0.80  0.91  1.00  0.91  0.86
Y  0.81  0.86  0.91  1.00  0.94
Z  0.77  0.81  0.86  0.94  1.00

Lecture fine de la matrice — l’ordre des corrélations ment. Z–Y culmine à 0.94, devant M–Y (0.91) et X–Y (0.86) ; or Z est aval de Y dans le DAG vrai, deux flèches plus loin que M. La corrélation la plus forte du dataset relie donc deux variables dont l’une est une conséquence de l’autre — elle mesure la force des mécanismes, pas la proximité causale. C’est la raison pour laquelle aucun algorithme de découverte ne part des corrélations ordonnées : la matrice est symétrique (C→X = X→C = 0.89) et ne contient, par construction, aucune information de direction. Toute la marchandise de la section suivante (PC, GES, LiNGAM) consiste à fabriquer de l’asymétrie — par v-structures pour les deux premiers, par non-gaussianité pour le troisième — à partir d’un objet qui n’en a pas.

2. PC — l’algorithme à contraintes

PC (Spirtes & Glymour, 1991) procède en trois temps : (1) le squelette — on part du graphe complet et on retire une arête u–v dès qu’un ensemble S rend u ⊥ v | S (test de Fisher-z, seuil alpha) ; (2) l’orientation des v-structures a → b ← c quand a, c sont non adjacents et que b n’appartient pas à leur sepset ; (3) propagation par les règles de Meek.

Choix d’alpha, mesuré sur ce monde : à 0.05, la v-structure est perdue environ 1 seed sur 4 (un test de vraie indépendance est rejeté au niveau 5 %, et le sepset qui en résulte désarme le collisionneur) ; à 0.01, le CPDAG canonique est rendu 20 fois sur 20. Ce compromis n’est pas gratuit — l’exercice 2 le mesure dans l’autre sens (arêtes parasites à alpha élevé, puissance perdue à alpha bas).

# PC sur le monde gaussien -- alpha=0.01 (v-structure robuste, cf. ci-dessus)
res_pc = ddo.executer_pc(df_monde, alpha=0.01)
print("PC -- aretes orientees     :", res_pc.aretes_orientees)
print("PC -- aretes NON orientees :", res_pc.aretes_non_orientees)
print()
verdict_pc = ddo.verdict_cpdag(res_pc)
print(f"Verdict : {verdict_pc['verdict']} ({verdict_pc['n_aretes_non_orientees']} arete(s) ambigue(s))")
print(verdict_pc["message"])
print()
comp_pc = ddo.comparer_au_dag_vrai(res_pc)
print("Confrontation au DAG vrai :")
for cle, val in comp_pc.items():
    print(f"  {cle:28s}: {val}")
PC -- aretes orientees     : [('C', 'Y'), ('M', 'Y'), ('Y', 'Z')]
PC -- aretes NON orientees : [('C', 'X'), ('M', 'X')]

Verdict : CPDAG_AMBIGU (2 arete(s) ambigue(s))
2 arete(s) non orientee(s) : classe d'equivalence de Markov. L'ambiguite est un RESULTAT -- aucun n plus grand ne la tranche (donnees gaussiennes). Chaque extension valide donne un estimand potentiellement different (cf. effet_backdoor_depuis_aretes).

Confrontation au DAG vrai :
  squelette_trouvees          : 5/5
  oriente_comme_vrai          : [('C', 'Y'), ('M', 'Y'), ('Y', 'Z')]
  oriente_inverse             : []
  non_orientees_parmi_vraies  : [('C', 'X'), ('M', 'X')]
  parasites                   : []
  manquantes                  : []

Lecture du CPDAG — trois résultats dans un

  1. Squelette exact 5/5 : aucune arête parasite, aucune manquante.
  2. La v-structure est trouvée : C → Y ← M est orientée — le collisionneur est détectable, parce que C ⊥ M | X mais C ⊭ M | Y. La règle de Meek oriente ensuite Y → Z (sinon M → Y ← Z créerait une v-structure que la donnée exclut).
  3. C–X et X–M restent ambiguës — et c’est le résultat central : pour des données linéaires gaussiennes, C → X et X → C impliquent exactement les mêmes distributions conditionnelles (même classe d’équivalence de Markov). Aucun test d’indépendance, quelle que soit la taille n, ne peut les distinguer.

Le verdict CPDAG_AMBIGU n’est pas un échec d’algorithme : c’est la borne exacte de ce que ces données, avec ces hypothèses, peuvent trancher.

3. GES — l’algorithme à score

GES (Chickering, 2002) ne fait aucun test d’indépendance : il cherche gloutonnement (phase forward d’ajout, phase backward de retrait) les classes d’équivalence qui maximisent un score BIC. Origine méthodologique complètement différente de PC — même théorie sous-jacente : GES navigue nativement dans l’espace des CPDAGs.

# GES (BIC) sur les memes donnees
res_ges = ddo.executer_ges(df_monde)
print("GES -- aretes orientees     :", res_ges.aretes_orientees)
print("GES -- aretes NON orientees :", res_ges.aretes_non_orientees)
print()
meme_cpdag = (set(res_ges.aretes_orientees) == set(res_pc.aretes_orientees)
              and set(res_ges.aretes_non_orientees) == set(res_pc.aretes_non_orientees))
print("PC et GES rendent-ils le meme CPDAG sur ce monde ?", meme_cpdag)
GES -- aretes orientees     : [('C', 'Y'), ('M', 'Y'), ('Y', 'Z')]
GES -- aretes NON orientees : [('C', 'X'), ('M', 'X')]

PC et GES rendent-ils le meme CPDAG sur ce monde ? True

Deux familles, un verdict

Contraintes (PC) et score (GES) partent d’axes opposés et rendent le même CPDAG. Ce n’est pas une convergence d’implémentation : aucun des deux ne peut dépasser la classe d’équivalence de Markov des données gaussiennes. L’ambiguïté de C–X n’est le défaut d’aucun des deux — c’est une propriété du monde, pas des algorithmes.

4. La classe d’équivalence — combien de mondes derrière ce CPDAG ?

Deux arêtes ambiguës (C–X, X–M) → \(2^2 = 4\) orientations brutes. Mais une extension n’est valide que si le DAG obtenu est (a) acyclique et (b) porte exactement les mêmes v-structures que le CPDAG (Verma & Pearl, 1990 : squelette + v-structures = identité de classe). L’organe énumère ces extensions — comptez-les avant de lire la suite.

# Extensions valides du CPDAG decouvert par PC
extensions = ddo.enumerer_extensions_acycliques(res_pc)
print(f"{len(extensions)} extension(s) valide(s) pour 2^2 = 4 orientations brutes :")
for i, ext in enumerate(extensions, 1):
    est_vrai = sorted(ext) == sorted(ddo.ARETES_DAG_VRAI)
    marque = "   <-- DAG VRAI du simulateur" if est_vrai else ""
    print(f"  {i}. {ext}{marque}")
print()
print("V-structures de chaque extension :", [ddo.v_structures(e) for e in extensions])
3 extension(s) valide(s) pour 2^2 = 4 orientations brutes :
  1. [('C', 'X'), ('C', 'Y'), ('M', 'Y'), ('X', 'M'), ('Y', 'Z')]   <-- DAG VRAI du simulateur
  2. [('C', 'Y'), ('M', 'X'), ('M', 'Y'), ('X', 'C'), ('Y', 'Z')]
  3. [('C', 'Y'), ('M', 'Y'), ('X', 'C'), ('X', 'M'), ('Y', 'Z')]

V-structures de chaque extension : [[('C', 'Y', 'M')], [('C', 'Y', 'M')], [('C', 'Y', 'M')]]

Les trois extensions racontent l’équivalence de Markov en acte. La sortie montre que les trois DAG valides partagent exactement la même v-structure ('C', 'Y', 'M') — c’est pourquoi ils sont indiscernables : deux DAGs sont équivalents au sens de Markov s’ils ont même squelette et mêmes v-structures, et ces trois-là cochent les deux cases. L’information qui les sépare (l’orientation de C–X et de M–X en particulier) n’existe dans aucune distribution générée par ce monde gaussien — ce n’est pas un déficit d’échantillon, c’est un déficit d’information. Le prix se paie plus loin, à la section 7 : selon l’extension choisie, l’effet de X sur Y vaut 0.81, 0.22 ou 1.05 — trois réponses incompatibles, toutes cohérentes avec les données.

Pourquoi 3 et pas 4

L’orientation manquante est C → X combiné à M → X : elle créerait la v-structure C → X ← M. Or les données disent C ⊥ M | X (c’est même le sepset qui a retiré l’arête C–M du squelette) — cette v-structure est exclue par la donnée, pas par notre goût. Le CPDAG n’est donc pas « un DAG partiellement deviné » : c’est la représentation exacte d’un ensemble de 3 DAGs, ni plus ni moins. Toute prétention à trancher C–X sans hypothèse supplémentaire est un mensonge statistique.

5. LiNGAM — l’hypothèse fonctionnelle qui tranche

Théorème DARM (Shimizu et al., 2006) : si le monde est linéaire à bruit non gaussien, alors l’ordre causal devient identifiable — les queues de distribution « cassent » la symétrie entre u → v et v → u que le monde gaussien respecte exactement. L’organe sait générer le même monde en bruit non gaussien (exponentiel centré, même variance — seul le kurtosis change) : tout ce qui suit porte sur les mêmes coefficients, mêmes tailles, même DAG.

# Le meme monde en bruit NON GAUSSIEN, puis DirectLiNGAM
df_ng = ddo.generer_donnees_decouverte(n=2000, bruit="non_gaussien", seed=42)
res_lingam = ddo.executer_lingam(df_ng)
print("LiNGAM -- aretes orientees  :", res_lingam.aretes_orientees)
print("LiNGAM -- ordre causal estime :", res_lingam.ordre_causal)
print()
comp_lingam = ddo.comparer_au_dag_vrai(res_lingam)
print("Confrontation au DAG vrai :")
for cle, val in comp_lingam.items():
    print(f"  {cle:28s}: {val}")
print()
print("Verdict :", ddo.verdict_cpdag(res_lingam)["verdict"])
LiNGAM -- aretes orientees  : [('C', 'X'), ('C', 'Y'), ('M', 'Y'), ('X', 'M'), ('Y', 'Z')]
LiNGAM -- ordre causal estime : ['C', 'X', 'M', 'Y', 'Z']

Confrontation au DAG vrai :
  squelette_trouvees          : 5/5
  oriente_comme_vrai          : [('C', 'X'), ('C', 'Y'), ('M', 'Y'), ('X', 'M'), ('Y', 'Z')]
  oriente_inverse             : []
  non_orientees_parmi_vraies  : []
  parasites                   : []
  manquantes                  : []

Verdict : DAG_ORIENTE

DARM en lecture

LiNGAM recouvre le DAG exact — y compris C → X et X → M que PC ne pouvait pas trancher. L’information qui tranche n’est pas dans les indépendances conditionnelles : elle est dans la forme du bruit (l’asymétrie des queues). C’est une vraie puissance d’identification — achetée au prix d’une hypothèse forte : « bruit non gaussien ». Et PC, sur ces mêmes données non gaussiennes ?

# PC sur les memes donnees non gaussiennes
res_pc_ng = ddo.executer_pc(df_ng, alpha=0.01)
print("PC -- aretes NON orientees (donnees non gaussiennes) :", res_pc_ng.aretes_non_orientees)
print()
print("PC ne consomme pas la forme du bruit : C--X et X--M restent ambigues,")
print("alors que LiNGAM vient de les trancher sur les MEMES donnees.")
PC -- aretes NON orientees (donnees non gaussiennes) : [('C', 'X'), ('M', 'X')]

PC ne consomme pas la forme du bruit : C--X et X--M restent ambigues,
alors que LiNGAM vient de les trancher sur les MEMES donnees.

6. L’échec honnête — LiNGAM sur bruit gaussien

Que rend LiNGAM quand son hypothèse n’est pas tenue ? Réponse mesurée : un DAG complet, faux, sans erreur ni avertissement.

# LiNGAM sur bruit GAUSSIEN (hypothese DARM violee) -- deux seeds
res_lg_a = ddo.executer_lingam(df_monde)
res_lg_b = ddo.executer_lingam(ddo.generer_donnees_decouverte(seed=7))
print("LiNGAM gaussien seed=42 :", res_lg_a.aretes_orientees)
print("LiNGAM gaussien seed=7  :", res_lg_b.aretes_orientees)
print()
print("DAG vrai                :", sorted(ddo.ARETES_DAG_VRAI))
print()
comp_a = ddo.comparer_au_dag_vrai(res_lg_a)
print(f"seed=42 : squelette {comp_a['squelette_trouvees']}, parasites {comp_a['parasites']}, inverses {comp_a['oriente_inverse']}")
print("Les deux DAG faux sont-ils au moins identiques entre eux ?",
      res_lg_a.aretes_orientees == res_lg_b.aretes_orientees)
LiNGAM gaussien seed=42 : [('C', 'M'), ('C', 'X'), ('X', 'M'), ('Y', 'C'), ('Y', 'M'), ('Y', 'X'), ('Y', 'Z')]
LiNGAM gaussien seed=7  : [('C', 'Y'), ('M', 'Y'), ('X', 'C'), ('X', 'M'), ('Z', 'C'), ('Z', 'M'), ('Z', 'X'), ('Z', 'Y')]

DAG vrai                : [('C', 'X'), ('C', 'Y'), ('M', 'Y'), ('X', 'M'), ('Y', 'Z')]

seed=42 : squelette 5/5, parasites [('C', 'M'), ('X', 'Y')], inverses [('Y', 'C'), ('Y', 'M')]
Les deux DAG faux sont-ils au moins identiques entre eux ? False

Le silence n’est pas une preuve

Trois faits mesurés sur ces deux seeds : le DAG rendu est faux (arêtes parasites, orientations inversées — Y → C au lieu de C → Y), il est complet (aucune arête ambiguë qui signalerait un doute), et il est instable (deux seeds gaussiens rendent des graphes différents). DirectLiNGAM ne détecte pas la violation de sa propre hypothèse — il produit toujours un DAG.

Diagnostics praticiens : (1) le kurtosis des résidus (une valeur ≈ 0 dit que l’hypothèse non gaussienne n’est pas tenable) ; (2) l’instabilité inter-seeds. Le verdict « cette orientation est soutenable » vient du praticien, jamais de la librairie — c’est la leçon structurelle de la série (NON_IDENTIFIABLE dans CausalBridges-06, CPDAG ambigu ici : chaque fois, l’honnêteté est un résultat, pas un échec).

7. L’ambiguïté se propage à l’estimand — le pont dowhy

CausalBridges-02 identifie un estimand sur un graphe. Reprenons le CPDAG découvert en section 2 : ses 3 extensions valides sont 3 graphes causaux incompatibles. Sur les mêmes données, chacune mène à un ensemble d’ajustement différent — donc à un estimand différent. L’effet total vrai de X sur Y vaut COEF_X_M · COEF_M_Y = 0.8.

# Trois extensions valides du meme CPDAG, trois estimands dowhy
print("Effet estime de X sur Y (effet total vrai = 0.8) :")
for i, ext in enumerate(extensions, 1):
    r = ddo.effet_backdoor_depuis_aretes(df_monde, ext)
    est_vrai = sorted(ext) == sorted(ddo.ARETES_DAG_VRAI)
    marque = "   <-- DAG VRAI" if est_vrai else ""
    print(f"  ext{i} ajustement={r.ensemble_ajustement!s:6s} estimand = {r.estimate_value:.3f}{marque}")
Effet estime de X sur Y (effet total vrai = 0.8) :
  ext1 ajustement=['C']  estimand = 0.812   <-- DAG VRAI
  ext2 ajustement=['M']  estimand = 0.219
  ext3 ajustement=[]     estimand = 1.046

Décider, c’est assumer

0.81 en ajustant {C} (le DAG vrai), 0.22 en ajustant {M} (l’extension qui prend M pour parent de X — ajuster le médiateur écrase la part médiatisée), 1.05 sans rien ajuster (l’extension où X est racine — le confondeur n’est plus confondeur). Aucune donnée supplémentaire, aucun n plus grand ne tranche entre ces trois mondes gaussiens : le choix d’une extension est une hypothèse causale assumée, pas un réglage statistique.

C’est la chaîne complète de la série : découverte (ce notebook) → hypothèse de graphe (CausalBridges-02 §4) → identification → estimation → réfutation. Chaque flèche de cette chaîne ne supprime jamais une ambiguïté : elle la convertit en hypothèse explicite.

8. Exercices

Trois exercices, stubs à compléter sans erreur volontaire (convention C.1 : le notebook s’exécute de bout en bout même non complété).

Exercice 1 — L’ambiguïté ne se résout pas avec des données

Pour n ∈ {500, 2000, 10000} : générer le monde gaussien (seed=42), exécuter PC (alpha=0.01) et afficher les arêtes non orientées et le verdict à chaque taille. Constat attendu : C–X et X–M restent ambiguës à chaque n — l’ambiguïté est structurelle, pas un problème d’échantillonnage. Indice : boucle sur les tailles, puis ddo.verdict_cpdag(res)["verdict"].

# Exercice 1 : l'ambiguite ne se resout pas avec des donnees
resultats_ex1 = None  # TODO etudiant
# attendu :
#   - pour n in (500, 2000, 10000) : df = ddo.generer_donnees_decouverte(n=n, seed=42)
#   - res = ddo.executer_pc(df, alpha=0.01)
#   - afficher n, res.aretes_non_orientees, ddo.verdict_cpdag(res)["verdict"]
#   - constater : C--X et X--M restent ambigues a CHAQUE taille

Exercice 2 — alpha de PC : le compromis mesuré

Pour alpha ∈ {0.2, 0.05, 0.01} et 5 seeds (0..4) : compter les arêtes parasites (ddo.comparer_au_dag_vrai(res)["parasites"]) et détecter la perte de v-structure (l’arête C–Y non orientée). Constat attendu : à 0.2 des arêtes parasites apparaissent, à 0.05 la v-structure saute sur certains seeds, à 0.01 tout est propre sur ce monde aux signaux forts — au prix de la puissance sur signaux faibles. Il n’y a pas d’alpha gratuit.

# Exercice 2 : alpha de PC -- parasites vs v-structure perdue
resultats_ex2 = None  # TODO etudiant
# attendu :
#   - pour alpha in (0.2, 0.05, 0.01) et seed in (0, 1, 2, 3, 4) :
#     res = ddo.executer_pc(ddo.generer_donnees_decouverte(seed=seed), alpha=alpha)
#   - compter len(ddo.comparer_au_dag_vrai(res)["parasites"])
#   - v-structure perdue si ("C", "Y") absente de res.aretes_orientees
#   - resumer par alpha : parasites moyens + taux de v-structure retrouvee

Exercice 3 — Diagnostiquer l’échec silencieux de LiNGAM

Sur 5 seeds gaussiens (0..4) : exécuter LiNGAM, mesurer le taux d’arêtes correctes (len(oriente_comme_vrai) / 5 via ddo.comparer_au_dag_vrai) et vérifier si les DAGs rendus coïncident entre seeds. Constat attendu : taux < 1 et graphes différents d’un seed à l’autre — l’instabilité inter-seeds est le signal de l’hypothèse non tenue, là où la librairie reste muette.

# Exercice 3 : LiNGAM sur bruit gaussien -- l'instabilite comme diagnostic
resultats_ex3 = None  # TODO etudiant
# attendu :
#   - pour seed in (0, 1, 2, 3, 4) : res = ddo.executer_lingam(
#       ddo.generer_donnees_decouverte(seed=seed))
#   - taux d'aretes correctes : len(ddo.comparer_au_dag_vrai(res)["oriente_comme_vrai"]) / 5
#   - comparer les ensembles d'aretes entre seeds (stabilite)
#   - conclure : DAG complet + faux + instable = hypothese non tenue

Attendus et anti-pièges (exercices 1 à 3)

Exercice 1 (n croissant). Attendu : le CPDAG ne change pas — squelette et v-structures convergent vers leurs valeurs de population, mais l’ambiguïté résiduelle (les arêtes non orientées) est structurelle. Doubler n affine les tests d’indépendance, il ne crée pas d’arêtes orientées là où le monde gaussien n’en rend aucune. Anti-piège : conclure « l’algorithme est mauvais » — c’est le monde qui est non identifiable par cette classe d’algorithmes ; le verdict correct renvoie à la section 5 (LiNGAM change de classe d’hypothèses, pas de puissance de calcul).

Exercice 2 (alpha de PC). Attendu : le compromis est asymétrique — à alpha=0.2 les tests rejettent trop : des indépendances vraies sont cassées et des arêtes parasites apparaissent ; à alpha trop petit (1e-4), c’est la v-structure qui se perd (un test d’indépendance conditionnelle manque, la fourche ne s’oriente pas) et le CPDAG devient plus ambigu, pas moins. La « bonne » valeur est celle qui préserve la v-structure connue du simulateur — critère externe disponible ici parce que le DGP est connu, jamais en pratique réelle. Anti-piège : optimiser alpha sur le nombre d’arêtes (plus d’arêtes = mieux) — l’objectif est l’orientation correcte, pas la densité.

Exercice 3 (LiNGAM gaussien, 5 seeds). Attendu : d’une seed à l’autre, l’ordre causal rendu change — l’instabilité EST le diagnostic. Sur bruit gaussien, l’hypothèse DARM est violée : l’algorithme rend un DAG qui n’a pas plus de valeur qu’un tirage au sort parmi les extensions ; la variance inter-seeds explose au lieu de converger. Comparer au comportement sur le monde non gaussien (section 5) où chaque seed rend le même DAG. Anti-piège : moyenner les estimations inter-seeds — la moyenne de réponses incohérentes n’est pas une réponse plus robuste, c’est une réponse qui ne correspond à aucun DAG.

9. Synthèse — l’échelle de l’identifiabilité

Marche Ce qu’on obtient À quelle condition
Données gaussiennes CPDAG (PC, GES) — squelette + v-structures aucune hypothèse au-delà de Markov + fidélité
+ hypothèse fonctionnelle DAG complet (LiNGAM) bruit non gaussien — et il rend un DAG faux si elle ne tient pas
+ choix d’une extension estimand (dowhy) hypothèse causale assumée : 3 extensions = 3 estimands (0.81 / 0.22 / 1.05)

Les trois verdicts honnêtes du notebook : CPDAG ambigu = un résultat (l’ambiguïté est la borne exacte des données gaussiennes) ; LiNGAM silencieux = un danger (complétude n’est pas preuve — kurtosis et stabilité inter-seeds sont les gardes-fous) ; ambiguïté du graphe = ambiguïté du chiffre (le choix d’extension est causal, pas statistique).

Dans la constellation : CausalBridges-02 (le graphe assumé et sa sensibilité), CausalBridges-06 (NON_IDENTIFIABLE est un résultat), CausalBridges-07-Quasi-Experimental.ipynb (la pratique observationnelle). L’organe dowhy_discovery_organs.py reste importable pour tout consommateur tiers — c’est la contrainte d’architecture de la série (#13921 : module canonique, jamais de duplication cell-scoped).

Retour au sommet