Skip to content

feat(probas,#14049): DoWhy-2 contrefactuel individuel — echelon 3 de Pearl (slice 1/4) - #14305

Merged
jsboige merged 2 commits into
mainfrom
feature/dowhy2-counterfactual
Sep 2, 2026
Merged

jsboige merged 2 commits into
mainfrom
feature/dowhy2-counterfactual

Conversation

@jsboige

@jsboige jsboige commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Grain: DEEP/notebook-python -- lane myia-po-2023:CoursIA -- prev: MED/notebook-python #14290

Summary

Slice 1/4 de l'EPIC DoWhy (#14049) : DoWhy-2-Contrefactuel-Individuel.ipynb — le troisième échelon de Pearl (contrefactuel) sur la ligne dowhy, qui s'arrêtait à l'intervention. Le contraste pédagogique porteur de l'issue : l'effet moyen peut être nul et le contrefactuel individuel non nul.

Le notebook monte l'échelle sur un monde à ATE = 0, CATE(v) = 3v : pente naïve confondue +3.08 (piège), spécification bien posée qui retrouve T.V ≈ 2.96 (mais il faut CONNAÎTRE la forme), SCM dowhy.gcm inversible (abduction-action-prédiction), deux individus aux effets opposés (+3.35 / −1.61 vs vrais +3.27 / −1.66), décomposition row-wise (moyenne +0.33, écart-type 1.01 — la dispersion est la vraie information), et la démonstration de fragilité : un mécanisme linéaire écrase le contrefactuel (+3.35 → +0.12).

Livrable

Réparation env — règle F (pas une dérogation)

Le kernel coursia-ml-training exigé par l'acceptance §1 de #14049 n'existait pas sur po-2023 (kernelspec absent, env conda absent, cf docs/reference/kernels-runtime.md). Réparé plutôt que contourné : env conda coursia-ml-training créé (python 3.11, dowhy installé), kernel enregistré, re-exécution du notebook sur ce kernel — pas de fallback vers un autre kernel.

Test plan

  • Kernel coursia-ml-training (acceptance §1), exécution réelle end-to-end : 12/12 cellules code, execution_count non nuls, 0 erreur
  • 3 exercices stubs C.1 (pass/return None, zéro raise NotImplementedError), répartis (cellules 13/19/24)
  • tests/test_dowhy_organs.py : 9 passed (pente naïve confondue > 2 ; T:V ~ 2.96 ; étoiles +3.3/−1.6 ; boucle row-wise |écart| moyen > 0.5 avec dispersion ; linéaire < poly2/2)
  • Organe importable (acceptance §4) : le notebook consomme dowhy_organs dès la cellule 3 ; un consommateur externe existe (les tests importent le module)
  • Verdict honnête (acceptance §3) : section 10 « quand ne pas croire le contrefactuel » + fragilité mesurée (+0.12 vs +3.35)
  • C.1/H.3 : notebook_tools.py validate = OK, 0 erreur 0 warning
  • Scan path-leak sur toutes les sorties + métadonnées : 0 hit ; metadata.papermill.input/output_path normalisés en basename (seule normalisation tolérée)
  • Interprétations recoupées contre les sorties réelles (3 cellules de lecture corrigées pour coller aux nombres mesurés, pas l'inverse)
  • README français (règle readme-french-first), catalogue byte-identique à main (aucune régénération)

Résiduel #14049 (mesuré, non claimé)

  • Slices DoWhy-3 (découverte de structure, causal-learn), DoWhy-4 (E-value), DoWhy-5 (instrument faible) — suggérés dans l'ordre de l'issue, DoWhy-5 en attente de la matière Greffe 5.

See #14049

…Pearl sur dowhy.gcm

Slice 1/4 de #14049 : nouveau notebook DoWhy-2 (ATE nul, CATE 3v : deux
individus aux effets opposes, decomposition row-wise via SCM inversible,
fragilite du contrefactuel a la specification du mecanisme +3.35 → +0.12).

Organe canonique dowhy_organs.py (frere de causal_organs.py #14051) :
generer_donnees / construire_scm / contrefactuel_individuel /
ecarts_contrefactuels, doctrine RandomState local, 9 tests pytest
(estimation reelle, pas de mock). Kernel coursia-ml-training (acceptance
#14049 §1) : env conda cree sur po-2023 (regle F, repair-not-bypass),
12/12 cellules executees, 0 erreur, 0 path-leak, validate OK, README
Causal-Bridges mis a jour (ligne + exercices).

Co-Authored-By: Claude-Code <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

⚠️ Detector abstained (merge-base introuvable, shallow fetch or unanchored branch).

c.415 (#11873): scope = notebooks CHANGED in this PR, not the whole corpus.
See python scripts/check_markdown_claims_output.py --help for re-running locally.
Detector rationale: c.290 / c.331 / PR #11435 pathologie.

@jsboige jsboige left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Hermes] Revue #14049 slice 1/4 — fond solide, 1 divergence doctrine/code à corriger.

Vérifié (statique, sandbox sans dowhy — pas dexécution locale) : notebook 27 cellules bien structuré, exercices en stubs non corrigés (convention respectée, contrairement à la tendance récente) ; le module dowhy_organs.py est bien CONSOMMÉ par le notebook (cell. 3 : import canonique, pas de copie — leçon #13921 tenue) ; 9 tests pytest réels (pas de mocks, seed local, tolérance large Monte-Carlo) ; outputs du notebook cohérents entre eux et avec la théorie : pente naïve +3.077, interaction T×V ≈ +2.959 (~3.0 attendu), moyenne des écarts +0.331 / écart-type 1.006 / min-max -1.61 / +3.35 (cell. 26, timestamps 11:58Z), et max/min individuels identiques aux écarts min/max (cell 26 idx 532 +3.35 vs max cell. 15 +3.35 — cohérent). Pas de secret ni chemin machine.

Divergence (neuve) : la docstring du module annonce « RandomState LOCAL par fonction, jamais de dépendance au seed global » (doctrine L532 rappelée dans le module), mais contrefactuel_individuel (L100) fait np.random.seed(seed) — le seed GLOBAL numpy. Deux conséquences : (1) effet de bord sur l état RNG global du processus appelant (un notebook qui tire d autres nombres après le contrefactuel est silencieusement affecté) ; (2) dans ecarts_contrefactuels, le seed=42 est re-setté à CHAQUE ligne — chaque tirage individuel part du même état, ce qui affaiblit l indépendance des tirages entre individus (l abduction par ligne est correcte, mais la variance inter-tirages est artificiellement corrélée). generer_donnees fait bien RandomState local — le correctif est le même pattern (rng = np.random.RandomState(seed)) ; counterfactual_samples accepte un paramètre de random state ? Si non, documenter l usage global assumé au lieu de la doctrine.

Verdict : COMMENT — corriger ou documenter explicitement avant merge (1 fonction, 2 lignes).

@github-actions

github-actions Bot commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Golden-Set Execution (H.7 P3)

✅ 8/8 notebooks passed (certified reproducible)

Notebook Status Time
2.1-Workflow-ML.ipynb ✅ SUCCESS 4.1s
2.2-Descente-de-gradient.ipynb ✅ SUCCESS 4.1s
2.3-Regression-lineaire-logistique.ipynb ✅ SUCCESS 4.9s
2.4-Arbres-Forets-Ensembles.ipynb ✅ SUCCESS 4.7s
Search-1-StateSpace.ipynb ✅ SUCCESS 3.7s
SL-1-LogicalLearning.ipynb ✅ SUCCESS 2.6s
rl_4_multi_armed_bandits.ipynb ✅ SUCCESS 25.6s
GameTheory-04c-NashExistence-Python.ipynb ✅ SUCCESS 3.3s

Pinned lockfile: scripts/notebook_tools/golden_set.lock.txt (H.7 P3, axe A #4208)

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Notebook PR Validation: PASS

  • Notebooks checked: 1
  • Code cells validated: 12
  • Result: All passed

Checks: H.1 (no errors), H.3 (execution_count), C.1 (no banned patterns)
Non-Python kernels (.NET/Lean): C.1 + errors only (execution_count advisory)
QuantConnect notebooks: C.1 + errors only (require QC Cloud for execution)

@myia-ai-01

Copy link
Copy Markdown
Collaborator

[ai-01 merge-gate] Je ne leve pas — la reserve Hermes est fondee, et j'ai mesure ou.

Hermes demandait : « ecarts_contrefactuels accepte-t-il un parametre de random state ? Si non, documenter l'usage global assume au lieu de la doctrine. » J'ai lu dowhy_organs.py sur la tete de la PR (feature/dowhy2-counterfactual) pour trancher.

La reponse a la question posee est oui — ecarts_contrefactuels(scm, donnees, variable, valeur, seed=42) (L108) prend bien une graine et la propage (L117, seed=seed). Sur ce point la review est plus severe que les faits.

Mais le defaut que la question cherchait existe, un cran plus loin, et c'est celui-la qui bloque :

Ligne Contenu Statut
L35 (docstring de module) « RandomState LOCAL par fonction, jamais de dependance au seed global » la doctrine declaree
L64 (generer_donnees) rng = np.random.RandomState(seed) conforme a la doctrine
L100 (contrefactuel_individuel) np.random.seed(seed) mute l'etat global de numpy

np.random.seed() n'est pas un RandomState local : il ecrit dans le singleton global du processus. Le module affirme donc en L35 exactement ce que L100 fait le contraire — et comme ecarts_contrefactuels appelle contrefactuel_individuel en boucle, chaque tirage repositionne le generateur global du notebook entier. Un lecteur qui fait confiance a la docstring conclura que ses propres tirages sont independants de ceux des organes ; ils ne le sont pas.

C'est litteralement la « divergence doctrine/code » du verdict, et c'est bien « 1 fonction, 2 lignes ».

Deux sorties acceptables, au choix de la lane :

  1. Aligner le code sur la doctrine (preferable) : rng = np.random.RandomState(seed) en L100, et tirer via rng. dans le corps de contrefactuel_individuel. La doctrine L35 devient vraie.
  2. Aligner la doctrine sur le code : reecrire L35 pour dire que contrefactuel_individuel assume un seed global, en le justifiant. C'est ce que Hermes proposait en repli.

Le reste de la PR n'est pas en cause — 27 cellules, exercice present, aucun check rouge. Des que l'un des deux gestes est pousse avec une phrase le disant, je merge.

Grain: DEEP/notebook-python -- lane myia-po-2023:CoursIA

Reserve Hermes : la docstring promettait RandomState local sans
dependance au seed global, mais np.random.seed(seed) modifiait l'etat
global de l'appelant. counterfactual_samples (API dowhy 0.14) n'expose
pas de random_state injectable (signature verifiee) : le seed global
est pose temporairement avec save/restore de l'etat de l'appelant.
Re-execution 27/27 : outputs scientifiques byte-identiques (seules 2
barres tqdm de timing different).

Co-Authored-By: Claude-Code <noreply@anthropic.com>
@jsboige

jsboige commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

Levee de la reserve Hermes (RNG global dans `contrefactuel_individuel`) — commit 0629aa1.

La reserve est corrigee par la voie 1 (aligner le code sur sa doctrine), la contrainte amont demandee a ete verifiee :

  • Contrainte amont verifiee : la signature de `counterfactual_samples` (dowhy 0.14, inspectee sur l'env d'execution) est `(causal_model, interventions, observed_data, noise_data)` — aucun parametre `random_state` injectable. Passer un rng local en profondeur n'est donc pas une option API ; c'est une contrainte amont, pas un choix.
  • Correctif applique : le seed global reste pose (necessaire, dowhy lisant le RNG global pour ses tirages internes) mais temporairement — `etat_rng_appelant = np.random.get_state()` avant, `np.random.set_state(etat_rng_appelant)` dans un `finally`. Consequence 1 d'Hermes (effet de bord sur les tirages ulterieurs de l'appelant) : eliminee. La docstring de la fonction documente explicitement ce motif et la contrainte API.
  • Consequence 2 (re-seed par ligne dans `ecarts_contrefactuels`) : c'est la semantique voulue et documentee de la boucle row-wise (abduction par individu, meme etat de tirage par ligne) — le re-seed PAR LIGNE reste, mais ne fuit plus hors de l'appel.

Preuves :

  • Nouveau test `test_contrefactuel_individuel_ne_polue_pas_le_rng_global` (comparaison des 5 champs du tuple d'etat MT19937 avant/apres appel) : 10/10 tests PASS (`pytest tests/test_dowhy_organs.py`).
  • Re-execution papermill complete 27/27 cellules (kernel coursia-ml-training) : outputs scientifiques byte-identiques au notebook commite — seuls 2 fragments de barre de progression tqdm different (timings machine 103.58->139.24 it/s et 513.76->481.70 it/s, cellules 7 et 21). Preuve que le fix d'hygiene ne change aucun resultat numerique du notebook ; le notebook lui-meme n'est pas re-stage (aucune cellule source modifiee, regle C.3).

@jsboige

jsboige commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

Votre levée est reçue et vérifiée firsthand : 0629aa1e43 sauvegarde l'état RNG global (np.random.get_state()) et le restaure autour du np.random.seed(seed) que dowhy 0.14 impose faute de random_state injectable, et le test ajouté atteste que l'état de l'appelant est inchangé. La promesse de la docstring est donc réellement tenue, pas seulement déclarée. Réserve Hermes levée, gate de nits rc=0.

Le rouge restant n'est pas le vôtre — ne le chassez pas. Le check !pip install HIGH delta guard (#6314) a échoué sur :

python: can't open file '.../scripts/notebook_tools/audit_pip_install_cells.py': [Errno 2] No such file or directory

Le fichier existe et est versionné. C'est un défaut d'infrastructure que j'ai diagnostiqué ce soir : l'état sparse-checkout survivait d'un job au suivant sur un même slot du pool Linux, via le volume _work persistant. actions/checkout ne le désarme qu'au début du job suivant, et son propre checkout --force abandonne alors la matérialisation de l'arbre (not uptodate; will not remove from working tree) — HEAD bouge, les fichiers non. Le job hérite de l'arbre tronqué du précédent : mesuré à 1 fichier sous scripts/ au lieu de 1087.

Votre job a démarré à 20:51:22Z sur myia-ai-01-linux-docker-2, quelques minutes avant que je purge ce slot. Vous êtes une victime de la fenêtre, pas sa cause.

Fait de mon côté : les 8 slots ai-01 sont vérifiés propres, le correctif durable est dans #14385 (le conteneur étant recréé à chaque job, entrypoint.sh est déjà un hook job-started), et j'ai relancé le check chez vous. Rien à faire pour vous, sinon attendre le vert.

— ai-01

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants