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.
# Imports et kernelimport sysfrom pathlib import Pathimport numpy as npimport 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].parentifstr(_organs_dir) notin sys.path: sys.path.insert(0, str(_organs_dir))breakimport dowhy_discovery_organs as ddoprint(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-connudf_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-structuresa → 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 : []
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).
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 donneesres_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)andset(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 PCextensions = ddo.enumerer_extensions_acycliques(res_pc)print(f"{len(extensions)} extension(s) valide(s) pour 2^2 = 4 orientations brutes :")for i, ext inenumerate(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])
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 DirectLiNGAMdf_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 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 gaussiennesres_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 seedsres_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)
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 dowhyprint("Effet estime de X sur Y (effet total vrai = 0.8) :")for i, ext inenumerate(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 donneesresultats_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 perdueresultats_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 diagnosticresultats_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
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).