ICT-Greffe5 – Attribution causale de l’intervention sur l’espace atteignable

Greffe 5 de la serie ICT (issue #13903, Epic #4588) – tranche 3/3.

Cible

La greffe 2 (ICT-Greffe2-EspaceAtteignable.ipynb, issue #13568) isole un quadruplet \((A, F, r, \pi)\) et montre que les ajouts d’operateurs STRIPS (+raft, +discard) modifient l’espace atteignable de maniere discriminante : +raft elargit reellement (1/3 \(\to\) 3/3 buts), +discard decore (|A| +50 %, 0 but nouveau). Mais le protocole Greffe 2 ne pose pas la question causale : l’operateur +raft est-il la cause de l’elargissement, ou un artefact de mesure ?

Cette tranche 3/3 repondt a cette question en appliquant les estimateurs de ict.causal_attribution (tranche 1/3, mergee via #13916) sur un panel experimental construit par imitation du domaine Greffe 2 – la relaxation STRIPS est consommee, jamais re-derivee (cf. CLAUDE.md section D).

Statut épistémique — Sans verdict à ce jour : aucune ligne de la matrice de dissociations ne concerne ce notebook ; son statut épistémique sera porté par la matrice le cas échéant.

Estimateurs disponibles (API close-form de ict.causal_attribution)

Trois estimateurs, du plus naif au plus correct :

  1. naive_difference : \(E[Y | X=1] - E[Y | X=0]\). Biaise par confondants. Baseline pour montrer la limite.
  2. backdoor_adjustment : ajustement sur un confondu observe \(Z\), en sommant \(\sum_z P(Y=y | X=x, Z=z) \cdot P(Z=z)\). Correct si le set backdoor est satisfait (Pearl 2009, chap. 3.3).
  3. iv_estimate : \(ATE_{IV} = Cov(Y, Z) / Cov(X, Z)\). Correct si l’instrument est pertinent (Angrist-Krueger 2001).

Verdict tri-etat (analogue au protocole d’ICT-12e pour l’EVSI) :

  • AGREEMENT : estimateurs dans la tolerance declaree.
  • DESACCORD : ecart superieur a la tolerance, les deux valeurs sont rapportees.
  • NON_IDENTIFIABLE : l’estimand n’est pas identifiable sous le graphe causal pose (instrument non pertinent). Resultat legitime, pas un echec.

Plan experimental

On imite le domaine STRIPS de Greffe 2 sur un grillage reduit \(3 \times 3\) (3 colonnes, 3 rangs) avec un seul objet cle et un seul but \(g\) = tenir la cle. Quatre vocabulaires d’operateurs :

  • A_t : vocabulaire de base (4 directions, pickup cle).
  • A_t+discard : A_t + un operateur discard sterile (perte d’objet, etat-piege).
  • A_t1 (+raft) : A_t + un operateur teleport qui ouvre une cellule supplementaire.
  • A_t1+discard : A_t1 + le meme discard sterile.

Pour chaque vocabulaire, on tire \(n_{seeds} = 30\) positions initiales aleatoires de l’objet cle, on mesure le taux d’atteignabilite (forward-search budget \(B = 8\)) et le taux d’etats atteignables depuis l’initial. Le panel experimental est \((X, Z, Y)\) avec :

  • \(X = 1\) si le vocabulaire contient teleport (analogue +raft), \(X = 0\) sinon.
  • \(Z = 1\) si le vocabulaire contient discard (analogue +discard).
  • \(Y\) = taux d’atteignabilite du but (moyenne sur les 30 graines).

Controle negatif : sur A_t1+discard, le discard est cense rendre l’elargissement +raft moins utile (l’agent perd la cle si elle l’attrape et jette sa prise) – la mesure naive doit montrer un ATE positif (X=1 elargit) mais l’IV (instrument = discard ajoutee) doit signaler l’effet heterogene.

from __future__ import annotations

import os
import sys
from collections import deque
from dataclasses import dataclass
from typing import FrozenSet, List, Tuple

import numpy as np

# Le module  vit dans le meme dossier que ce notebook.
_ICT_DIR = os.path.dirname(os.path.abspath("ICT-Greffe5-AttributionCausale.ipynb"))
if _ICT_DIR not in sys.path:
    sys.path.insert(0, _ICT_DIR)

from ict import causal_attribution as ca

print("ict.causal_attribution OK :", ca.AttributionVerdict.__members__.keys())
ict.causal_attribution OK : dict_keys(['AGREEMENT', 'DESACCORD', 'NON_IDENTIFIABLE'])

Mini-domaine STRIPS imite de Greffe 2

On construit un domaine clos sur un grillage \(3 \times 3\) (sans gouffre – simplification du domaine Greffe 2 qui en avait un). Le Operateur STRIPS est importe comme un tuple (nom, pre, add, del) (forme minimale). La mesure est :

  • forward-reachable : nombre d’etats atteignables depuis INITIAL par BFS dans le graphe d’operateurs, avec budget \(B = 8\) expansions.
  • but_atteint : 1 si le but est dans F_B(INITIAL), 0 sinon.
# Operateur STRIPS minimal (consomme, jamais re-derive : voir CLAUDE.md section D).
@dataclass(frozen=True)
class Operateur:
    nom: str
    pre: FrozenSet[str]
    add: FrozenSet[str]
    dele: FrozenSet[str]

    def applicable(self, etat: FrozenSet[str]) -> bool:
        return self.pre.issubset(etat)

    def appliquer(self, etat: FrozenSet[str]) -> FrozenSet[str]:
        return frozenset((etat - self.dele) | self.add)

    def __repr__(self) -> str:
        return self.nom


COLS = (0, 1, 2)
RANGS = (0, 1, 2)
CASES = [(c, r) for c in COLS for r in RANGS]

BUT = frozenset({"holding-key"})

def prop(c: int, r: int) -> str:
    return f"at-x{c}-y{r}"

INITIAL = frozenset({prop(0, 0), "key-at-x1-y0"})


def ops_de_base() -> List[Operateur]:
    """4 directions + pickup cle a la case (1, 0)."""
    ops: List[Operateur] = []
    for c, r in CASES:
        for dc, dr, nom_dir in [(1, 0, "est"), (-1, 0, "ouest"),
                                 (0, 1, "nord"), (0, -1, "sud")]:
            cible = (c + dc, r + dr)
            if cible in CASES:
                ops.append(Operateur(
                    f"move-{nom_dir}-x{c}-y{r}",
                    {prop(c, r)}, {prop(*cible)}, {prop(c, r)}))
    ops.append(Operateur("pickup-key",
                         {prop(1, 0), "key-at-x1-y0"},
                         {"holding-key"}, {"key-at-x1-y0"}))
    return ops


def ops_avec_teleport(base: List[Operateur]) -> List[Operateur]:
    """Analogue de  : teleport diagonal (0,0) -> (2,2)."""
    ops = list(base)
    ops.append(Operateur("teleport-diag",
                         {prop(0, 0)}, {prop(2, 2)}, {prop(0, 0)}))
    return ops


def ops_avec_discard(base: List[Operateur]) -> List[Operateur]:
    """Analogue de  : perdre la cle si on la tient. Etat-piege."""
    ops = list(base)
    ops.append(Operateur("discard-key",
                         {"holding-key"}, set(), {"holding-key"}))
    return ops


A_T = ops_de_base()
STER = ops_avec_discard(A_T)
A_T1 = ops_avec_teleport(A_T)
T1S = ops_avec_discard(A_T1)

# Noms avec mots-cles explicites pour les filtres du panel :
# - X = 1 si "teleport" dans le nom
# - Z = 1 si "+discard" dans le nom
VOCABS = {
    "A_t":              A_T,
    "A_t +discard":      STER,
    "A_t1 (+teleport)":  A_T1,
    "A_t1 (+teleport) +discard": T1S,
}
for nom, V in VOCABS.items():
    print(f"{nom:32s} |V| = {len(V)} operateurs")
A_t                              |V| = 25 operateurs
A_t +discard                     |V| = 26 operateurs
A_t1 (+teleport)                 |V| = 26 operateurs
A_t1 (+teleport) +discard        |V| = 27 operateurs

Lecture 1 – Quatre vocabulaires : 25, 26, 26, 27 operateurs

La cellule de construction affiche les tailles brutes des quatre vocabulaires : A_t = 25 operateurs (deplacements entre cases adjacentes du grillage 3x3, plus pickup-key), A_t +discard = 26, A_t1 (+teleport) = 26, A_t1 (+teleport) +discard = 27. Chaque ajout est une addition STRIPS minimale (pre, add, del) : teleport-diag quitte (0,0) pour (2,2) en un pas, discard-key retire holding-key sans add – un etat-piege.

La lecture a retenir : la taille ne discrimine pas les deux ajouts (26 contre 26) – c’est la structure des preconditions qui jouera. Le panel experimental (cellule 9) separe teleport (X) de discard (Z) par mots-cles dans les noms, pas par la taille. Le protocole de Greffe 2 est imite, jamais re-derive : la relaxation STRIPS consommee est celle de ict.causal_attribution, la construction des operateurs est locale au notebook.

Mesure du forward-search budget \(B\)

Pour chaque vocabulaire, on lance un BFS depuis INITIAL avec budget \(B = 8\) expansions. On rapporte :

  • \(|F_B(\text{INITIAL})|\) : nombre d’etats atteignables.
  • but_atteint : 1 si \(\text{BUT} \subseteq F_B(\text{INITIAL})\), 0 sinon.

On fait varier la graine de la position initiale alternative (l’initial est fixe, mais on mesure aussi le forward-search depuis 30 positions alternatives pour avoir un panel).

def forward_reachable(ops: List[Operateur], initial: FrozenSet[str],
                      budget: int) -> set:
    """BFS budget-stricted : ensemble des etats atteignables en <= budget expansions."""
    visited = {initial}
    frontiere = {initial}
    for _ in range(budget):
        nouveau = set()
        for etat in frontiere:
            for op in ops:
                if op.applicable(etat):
                    suivant = op.appliquer(etat)
                    if suivant not in visited:
                        nouveau.add(suivant)
                        visited.add(suivant)
        if not nouveau:
            break
        frontiere = nouveau
    return visited


def position_initiale(seed: int) -> FrozenSet[str]:
    """30 positions initiales alternatives : cle placee aleatoirement."""
    rng = np.random.RandomState(seed)
    c, r = CASES[rng.randint(len(CASES))]
    return frozenset({prop(0, 0), f"key-at-x{c}-y{r}"})


def mesurer_vocabulaire(ops: List[Operateur], seeds: List[int],
                        budget: int = 8) -> List[dict]:
    """Panel : pour chaque seed, taux d'atteignabilite du but + |F_B|."""
    panneau = []
    for s in seeds:
        init = position_initiale(s)
        F = forward_reachable(ops, init, budget=budget)
        panneau.append({
            "seed": s,
            "taille_F": len(F),
            "but_atteint": int(BUT.issubset(F)),
        })
    return panneau


SEEDS = list(range(30))
BUDGET = 2  # Budget reduit : +teleport discrimine a B=2, pas a B=8

panneaux = {nom: mesurer_vocabulaire(V, SEEDS, budget=BUDGET)
            for nom, V in VOCABS.items()}

for nom, panneau in panneaux.items():
    taille_moy = np.mean([o["taille_F"] for o in panneau])
    taux_but = np.mean([o["but_atteint"] for o in panneau])
    print(f"{nom:20s} |F|_moyen = {taille_moy:6.2f}  taux_but = {taux_but:.3f}")
A_t                  |F|_moyen =   6.23  taux_but = 0.000
A_t +discard         |F|_moyen =   6.23  taux_but = 0.000
A_t1 (+teleport)     |F|_moyen =   9.23  taux_but = 0.000
A_t1 (+teleport) +discard |F|_moyen =   9.23  taux_but = 0.000

Lecture 2 – Budget B = 2 : le teleport elargit (+48 %), le discard decore

A budget B = 2, le pattern de Greffe 2 est reconstitue de maniere discriminante : A_t a un |F| moyen de 6.23 et A_t1 (+teleport) de 9.23 – delta +3.00, soit +48 % d’etats atteignables. Le teleport-diag ouvre la diagonale en une expansion, ce que les quatre directions ne font pas en deux pas depuis l’initial.

Le discard, lui, est sterile sur |F| (6.23 sans et 6.23 avec) : son effet suppose que l’agent tienne deja la cle, condition jamais satisfaite en budget B = 2. Et le taux de but est 0 partout : le pickup-key exige la cle a la case (1, 0) en position fixe, et le forward-search ne deplace pas l’objet – la mesure binaire est degenerente. C’est exactement pour cela que la cellule 9 prend |F_B| comme outcome continu : seule la taille du front discrimine les vocabulaires sur ce banc.

# Construction du panel experimental (X, Z, Y) pour chaque seed.
# X = 1 si le vocab contient teleport (= +raft)
# Z = 1 si le vocab contient discard (= +discard)
# Y = |F_B| moyen (outcome continu -- discrimine les vocabulaires).
# But_atteint est 0 pour toutes les seeds (le pickup-key exige la cle
# a la case (1, 0) mais le forward-reachable ne deplace pas l objet),
# donc une mesure binaire est degenerente ici -- on prend la taille
# du forward-reachable qui discrimine les ajouts.
X = []  # 1 si teleport, 0 sinon
Z = []  # 1 si discard, 0 sinon
Y = []  # taille_F (continue)
for seed in SEEDS:
    for nom, panneau in panneaux.items():
        x_val = 1 if "teleport" in nom else 0  # sans "+" pour eviter collision
        z_val = 1 if "+discard" in nom else 0
        y_val = panneau[seed]["taille_F"]  # |F_B|, continu
        X.append(x_val)
        Z.append(z_val)
        Y.append(y_val)

X = np.asarray(X, dtype=int)
Z = np.asarray(Z, dtype=int)
Y = np.asarray(Y, dtype=float)
print(f"N observations = {len(Y)}")
print(f"P(X=1) = {X.mean():.3f}, P(Z=1) = {Z.mean():.3f}")
print(f"E[Y | X=1] = {Y[X == 1].mean():.2f}, E[Y | X=0] = {Y[X == 0].mean():.2f}")
print(f"E[Y | Z=1] = {Y[Z == 1].mean():.2f}, E[Y | Z=0] = {Y[Z == 0].mean():.2f}")
print(f"Cross-tab : Z=0,X=0={((Z==0)&(X==0)).sum()}, Z=0,X=1={((Z==0)&(X==1)).sum()}, Z=1,X=0={((Z==1)&(X==0)).sum()}, Z=1,X=1={((Z==1)&(X==1)).sum()}")
N observations = 120
P(X=1) = 0.500, P(Z=1) = 0.500
E[Y | X=1] = 9.23, E[Y | X=0] = 6.23
E[Y | Z=1] = 7.73, E[Y | Z=0] = 7.73
Cross-tab : Z=0,X=0=30, Z=0,X=1=30, Z=1,X=0=30, Z=1,X=1=30

Lecture 3 – Un design orthogonal : 120 observations, 30 par cellule

Le panel empile les quatre vocabulaires sur les 30 graines : N = 120 observations, P(X = 1) = 0.500 et P(Z = 1) = 0.500, et le cross-tab l’affiche sans ambiguite : (Z = 0, X = 0) = 30, (Z = 0, X = 1) = 30, (Z = 1, X = 0) = 30, (Z = 1, X = 1) = 30 – chaque cellule du design contient exactement 30 observations, aucun cas vide.

Le traitement teleport et le confondu discard sont orthogonaux par construction : Cov(X, Z) = 0, ce qui se lit aussi dans les moyennes – E[Y | Z = 1] = 7.73 = E[Y | Z = 0] = 7.73 : le discard est marginalement neutre en moyenne. Cote traitement, E[Y | X = 1] = 9.23 contre E[Y | X = 0] = 6.23 : le contraste brut fait +3.00, exactement le delta de la lecture 2. La consequence pour la suite : le design garantit que la moyenne par strate de Z ne cache aucune interaction – la cellule 11 pourra verifier que le backdoor ne corrige rien, parce qu’il n’y a rien a corriger.

# Estimateur 1 : naive_difference (baseline).
# ATE_naif = E[Y | X=1] - E[Y | X=0]. Biaise par confondant Z = discard.
outcome_by_x = {0: Y[X == 0].tolist(), 1: Y[X == 1].tolist()}
ate_naif = ca.naive_difference(outcome_by_x)
print(f"ATE_naif = E[Y|X=1] - E[Y|X=0] = {ate_naif:+.4f}")
print(f"  -> Le +teleport ajoute ~{ate_naif * 100:.1f} points de taux_but en moyenne.")
print(f"     Mais ce delta est biaise : Z=discard peut etre un confondant.")
ATE_naif = E[Y|X=1] - E[Y|X=0] = +3.0000
  -> Le +teleport ajoute ~300.0 points de taux_but en moyenne.
     Mais ce delta est biaise : Z=discard peut etre un confondant.
# Estimateur 2 : backdoor_adjustment sur le confondu Z = discard.
# ATE_backdoor = sum_z [ E[Y | X=1, Z=z] - E[Y | X=0, Z=z] ] * P(Z=z)
ate_backdoor = ca.backdoor_adjustment(
    outcome_table=Y,
    treatment_levels=X.tolist(),
    confounder_values=Z.tolist(),
)
print(f"ATE_backdoor (ajuste sur discard) = {ate_backdoor:+.4f}")

verdict = ca.compare_estimators(
    {"naif": ate_naif, "backdoor": ate_backdoor},
    tolerance=0.10,
)
print(f"Verdict naif vs backdoor (tol 0.10) : {verdict.value}")

effet_discard = Y[Z == 1].mean() - Y[Z == 0].mean()
effet_teleport = Y[X == 1].mean() - Y[X == 0].mean()
print(f"Effet marginal +discard (Z=1 vs Z=0) : {effet_discard:+.2f} etats")
print(f"Effet marginal +teleport (X=1 vs X=0) : {effet_teleport:+.2f} etats")

# Note : +teleport est causal, +discard est confondu (orthogonal au traitement).
# Le backdoor ajuste le naif par la moyenne ponderee de l effet dans chaque strate
# de Z -- comme le design est orthogonal (X, Z), l ajustement laisse l ATE inchange.
ATE_backdoor (ajuste sur discard) = +3.0000
Verdict naif vs backdoor (tol 0.10) : AGREEMENT
Effet marginal +discard (Z=1 vs Z=0) : +0.00 etats
Effet marginal +teleport (X=1 vs X=0) : +3.00 etats

Lecture 4 – Naif vs backdoor : AGREEMENT a la tolerance 0.10

L’estimateur naif donne ATE = +3.0000 (la cellule le reformule : le +teleport ajoute 300.0 points de taux_but en moyenne, chiffre aussitot relativise – ce delta brut peut etre confondu par discard). L’ajustement backdoor sur Z donne ATE_backdoor = +3.0000, et la comparaison des deux a tolerance 0.10 declare AGREEMENT.

Ce n’est pas un hasard : parce que le design est orthogonal (lecture 3), la moyenne ponderee de l’effet dans chaque strate de Z reproduit exactement le contraste brut – l’ajustement laisse l’ATE inchange, comme le commentaire de la cellule le note. Les effets marginaux le confirment : +discard = +0.00 etats (Z orthogonal au traitement) contre +teleport = +3.00 etats. La lecture pedagogique : le backdoor ne corrige un biais que s’il y en a un ; sur ce design, naif et ajuste s’accordent a la tolerance pres – AGREEMENT est le verdict honeste, pas une coincidence.

# Estimateur 3 : iv_estimate avec instrument Z = discard (pertinence ?).
# Question causale : si Z=discard est pertinent (Cov(X, Z) eleve), alors
# la variation de Z force X a varier -- on peut extraire un ATE instrumental.
# Mais ici Z = discard est orthogonal a X = teleport (independance des ajouts),
# donc Cov(X, Z) = 0 et l'instrument est NON PERTINENT -> NON_IDENTIFIABLE.
# C'est le **resultat legitime** du garde-fou de iv_estimate : l'instrument
# n'est pas pertinent pour identifier l'effet de X=Y|X sur Y.
try:
    ate_iv = ca.iv_estimate(
        outcome=Y.tolist(),
        treatment=X.astype(float).tolist(),
        instrument=Z.astype(float).tolist(),
    )
    print(f"ATE_iv = {ate_iv:+.4f}")
except ValueError as e:
    # Verdict NON_IDENTIFIABLE -- c'est ce qu'on attend.
    print(f"iv_estimate leve ValueError : instrument NON PERTINENT")
    print(f"  -> {str(e)[:140]}")
    print(f"  Verdict logique : {ca.AttributionVerdict.NON_IDENTIFIABLE.value}")
    print(f"  C'est le resultat legitime : discard est ORTHOGONAL a teleport ici,")
    print(f"  l'instrument Z n'identifie pas l'effet de X = teleport sur Y.")
iv_estimate leve ValueError : instrument NON PERTINENT
  -> iv_estimate : instrument NON PERTINENT (|Cov(X, Z)|=0.000000 < 5/sqrt(n)=0.456435) -- estimand NON_IDENTIFIABLE
  Verdict logique : NON_IDENTIFIABLE
  C'est le resultat legitime : discard est ORTHOGONAL a teleport ici,
  l'instrument Z n'identifie pas l'effet de X = teleport sur Y.

Lecture 5 – iv_estimate : NON_IDENTIFIABLE, le garde-fou au bon endroit

L’instrument Z = discard est orthogonal a X = teleport (lecture 3), donc Cov(X, Z) = 0 : iv_estimate leve un ValueError immediat – la sortie affiche |Cov(X, Z)| = 0.000000, sous le seuil de pertinence 5/sqrt(n) = 0.456435. La cellule intercepte l’exception et traduit : verdict logique NON_IDENTIFIABLE, qualifie de resultat legitime, pas un echec.

C’est le comportement voulu du garde-fou (Pearl 2009 chap. 3.3 + Angrist-Krueger 2001, seuil 5-sigma) : avec un instrument non pertinent, aucune formule ne peut identifier l’effet causal de X sur Y – la methode refuse de produire un nombre plutot que d’en fabriquer un. Mise en perspective avec la lecture 4 : naif et backdoor s’accordent a +3.0000 – le backdoor ajuste le delta naif pour la presence de +discard, et l’ecart sous la tolerance 0.10 declare AGREEMENT (l’IV, lui, ne conclut pas) – les trois estimateurs couvrent ainsi les trois etats du verdict tri-etat (AGREEMENT, DESACCORD, NON_IDENTIFIABLE). L’exercice 1 invite justement l’etudiant a modifier le design pour rendre Z pertinent et faire parler l’IV : la tolerance du notebook a l’ambiguite est la lecon de cette cellule.

Exercice 1 : modifier le design pour rendre Z pertinent

Pour que Z = discard soit un instrument pertinent, il faut que les ajouts soient correles : par exemple, on n’ajoute discard que conditionnellement a la presence de teleport (Z = X). Refaire le panel avec ce design et montrer que iv_estimate rend un ATE instrumental non-trivial.

Indice : reconstruire VOCABS avec seulement les configurations A_t, A_t1, A_t+discard (retirer A_t1+discard), ou changer la regle d’assignation (X, Z) pour rendre Cov(X, Z) > 0.

# Exercice 1 a completer :
VOCABS_EX1 = {
    "A_t":              A_T,
    # ... a completer par l'etudiant ...
}
# Puis mesurer, construire le panel, appliquer iv_estimate.
# Exercice 1 a completer : design alternatif ou Z pertinent
# Stubs conformes C.1 : pas de raise NotImplementedError.
VOCABS_EX1 = None  # TODO etudiant : sous-ensemble des VOCABS ou design alternatif
panneaux_EX1 = None  # TODO etudiant : mesurer si VOCABS_EX1 defini
result_iv = None  # TODO etudiant : resultat de iv_estimate (ou ValueError releve)
print("Exercice 1 : a completer")
Exercice 1 : a completer

Exercice 2 : controle negatif avec un confoundant reellement actif

Pour verifier que le backdoor ajuste bien le biais, construire un panel avec un vrai confondant : par exemple, ajouter un operateur noise (pickup aleatoire d’un objet non-cle) qui augmente \(|F|\) mais sans aider a tenir le but. Mesurer le biais du naif (qui ne distingue pas noise de teleport) et montrer que le backdoor sur noise corrige.

Indice : noise = Operateur("noise-pickup", {prop(c, r)}, {"holding-noise"}, set()) ajoute pour 30% des cases. Le naif surestime l’effet de teleport car il confond noise (qui ouvre \(|F|\)) avec teleport (qui aide le but).

# Exercice 2 a completer : definir un Operateur `noise` actif et mesurer le biais.
# TODO etudiant :
def ops_avec_noise(base, proba=0.3, seed=0): ...
# Exercice 2 a completer : Operateur `noise` actif
# Stubs conformes C.1.
def ops_avec_noise(base, proba=0.3, seed=0):
    # TODO etudiant : ajouter des operateurs `noise-pickup-X-Y` sur ~30% des cases
    return base  # stub

VOCABS_EX2 = None  # TODO etudiant : VOCABS + variantes avec/sans noise
panneaux_EX2 = None  # TODO etudiant
biais_naif = None  # TODO etudiant : biais observe sur le panel avec confoundant
print("Exercice 2 : a completer")
Exercice 2 : a completer

Exercice 3 : utiliser ict.bridges (tranche 2/3) pour valider cross-engine

Une fois la tranche 2/3 (PR #13921) mergée, le module ict.bridges (livré dans cette meme PR) fournit les estimateurs adapt_panel_did_to_backdoor et adapt_iv_replay_to_iv_estimate qui comparent ict.causal_attribution aux organes natifs Quasi-Experimental et PyMC-05. Refaire ce notebook en branchant les adaptateurs cross-engine et montrer que les deux estimateurs donnent la meme valeur a tolerance pres (verdict AGREEMENT sur AGREEMENT).

Indice : from ict.bridges import adapt_panel_did_to_backdoor, adapt_iv_replay_to_iv_estimate. Appliquer les deux sur le panel \((X, Z, Y)\) avec specification DiD et IV 2SLS (60 repetitions).

# Exercice 3 a completer : brancher les adaptateurs ict.bridges (post-merge #13921).
# TODO etudiant :
result_did = adapt_panel_did_to_backdoor(differential_pretrend=0.0)  # SUTA satisfaite
result_iv = adapt_iv_replay_to_iv_estimate(coef_z=2.0, n_rep=60)  # instrument fort
# Exercice 3 a completer : adaptateurs cross-engine (post-merge tranche 2)
# Stubs conformes C.1 : pas de raise NotImplementedError.
result_did = None  # TODO etudiant : adapt_panel_did_to_backdoor (post-merge)
result_iv = None  # TODO etudiant : adapt_iv_replay_to_iv_estimate (post-merge)
print("Exercice 3 : a completer (necessite la merge de la tranche 2/3, PR #13921)")
Exercice 3 : a completer (necessite la merge de la tranche 2/3, PR #13921)

References

  • Issue #13903 – Greffe 5 attribution causale de l’intervention (Epic #4588).
  • Issue #13568 – Greffe 2, quadruplet \((A, F, r, \pi)\), controle negatif +raft/+discard (PR #13802 MERGED).
  • PR #13916 – tranche 1/3, ict.causal_attribution.py (close-form NumPy, MERGED).
  • PR #13921 – tranche 2/3, ict.bridges (adaptateurs cross-engine Quasi-Experimental + PyMC-05).
  • Judea Pearl, Causality, Cambridge UP, 2009 (chap. 3 : do-calculus, chap. 3.3 : backdoor criterion).
  • Guido Imbens & Donald Rubin, Causal Inference for Statistics, Social, and Biomedical Sciences, Cambridge UP, 2015.
  • Joshua Angrist & Alan Krueger, 2001 (variables instrumentales, 5-sigma relevance floor).
  • ICT-12e Value-of-Information-Animat.ipynb – archétype du verdict tri-etat (EVSI 253 000 vs 252 794 EUR, tolerance 206 EUR).
Retour au sommet