2.14b — SHAP et do-calculus : la jonction attribution ↔︎ causalité

Navigation : << 2.14-Explicabilite-SHAP-LIME-Contrefactuels | Index | Suivant >> 2.15-Donnee-Comme-Responsabilite-Python

Grain : DEEP/notebook-python — lane myia-po-2023:CoursIA-2 — prev: DEEP/notebook-python #16619 (2.14 Explicabilite)

Ce notebook creuse un point que 2.14 effleure et que la litterature XAI confond regulierement :

Subtilite centrale (T9 Chen-Covert-Lundberg-Lee 2022 + R4 Bareinboim-Pearl 2016) : la Shapley value conditionnelle sous un background dataset \(D\) approche l’effet causal \(do(X=x)\) quand \(D\) respecte la consistance avec le DAG. La Shapley value marginale (Kernel SHAP classique) approxime l’observation \(P(Y \mid X=x)\), qui n’est pas l’effet causal. C’est la jonction attribution↔︎causalite que ce notebook rend visible par la mesure.

Sources canoniques (cf bibliography-hygiene.md — archivees hors Git dans GDrive) :

Ref Auteur(s) Annee Substantif
R1 Lundberg & Lee 2017 Kernel SHAP, theoreme d’unicite (3 axiomes : local accuracy, missingness, consistency) — arXiv 1706.06060
R2 Lundberg et al (10 auteurs) 2019 Tree SHAP exact O(TLD^2) — arXiv 1905.04610
R3 Chen, Covert, Lundberg, Lee 2022 Conditional vs Marginal Shapley <-> do/see — arXiv 2207.07605 (T9 du cadrage)
R4 Bareinboim & Pearl 2016 Jonction do-calculus ~ conditional Shapley — PNAS 10.1073/pnas.1510507113
R5 Bareinboim, Correa, Ibeling, Icard 2026 Causal Hierarchy Theorem (CHT) 3 niveaux — Causal AI ch. 2.3

Socle du depot (jonction XAI <-> causal) :

Acceptance (8 critères du cadrage c.655 / #16669) :

  1. Notebook Python coursia-ml-training Papermill 0 erreur.
  2. Sorties reelles commises (C.2 — jamais d’edition manuelle d’une sortie de cellule, cf secrets-hygiene.md regle 6).
  3. ≥ 2 visualisations SHAP (summary plot + force plot local).
  4. ≥ 1 visualisation LIME.
  5. ≥ 1 contrefactuel DiCE.
  6. Section 5 « Jonction Shap ↔︎ do-calculus (T9) » mesure l’ecart KernelShap vs conditional Shapley.
  7. Section 6 « Ponts » renvoie aux notebooks causaux.
  8. Section 7 « Note explicatif ≠ causal » cite R4 §3.3 + R3 §2.4.

Garde-fous d’honnêteté (cf verify-before-claiming.md)

  1. Pas de pretention d’exhaustivite : la litterature XAI sur ce sujet est large (CAT, Grad-CAM, attention-rollout, integrated gradients…). Le notebook se limite a SHAP parce que c’est l’attribution la plus formalisee (3 axiomes de R1) et la seule ou la jonction conditionnel/marginal est theoriquement etablie.
  2. Les donnees sont simulees, et c’est une exigence. Le notebook engendre lui-meme son DAG (age, credit_amount) et entraine une foret de 100 arbres dessus. Aucun artefact externe n’est requis et aucune valeur n’est recopiee. Le choix n’est pas seulement une commodite : la jonction causale de la section 4 suppose un DAG connu, or German Credit (2.14) n’en fournit aucun — ses colonnes ne portent pas les trois liens ecrits ici.
  3. Le DAG est connu : on simule un DAG ou age -> credit_amount -> default ET age -> default. Kernel SHAP (marginal) ignore ce DAG ; Tree SHAP (conditionnel) le respecte via l’ordre des features dans l’arbre. L’ecart est mesurable, pas hypothetique.
  4. Aucune fabrication de chiffres : chaque valeur numerique de ce notebook est lue depuis les sorties reelles des cellules. Le notebook ne cache pas les cas ou l’ecart marginal/conditionnel est faible — il les montre.

Extension optionnelle. Le meme protocole s’applique a la foret de 2.14 (German Credit) : il suffit de remplacer le bloc de generation par son chargement. L’exercice est instructif mais il perd l’interpretation causale de la section 4, faute de DAG etabli sur ces donnees — c’est pourquoi il n’est pas le chemin nominal ici.

1. Setup : le DAG simule et le background dataset

On engendre le DAG nous-memes, puis on entraine une foret aleatoire dessus. Le background dataset \(D\) est crucial : c’est lui qui distingue marginal de conditionnel.

  • Marginal (Kernel SHAP) : pour chaque coalition \(S \subseteq F \setminus \{i\}\), on tire \(D\) uniformement sur \(X_S\) et on complete par \(X_{\bar{S}} = E[X_{\bar{S}}]\) (moyennes marginales).
  • Conditionnel (Tree SHAP / Kernel SHAP conditionnel) : pour chaque coalition \(S\), on tire \(X_S\) observe dans \(D\) et on complete par \(X_{\bar{S}}\) tire conditionnellement a \(X_S\) (preservation des dependances entre features).

Le DAG etant connu (age -> credit_amount -> default + age -> default), conditionner sur age change la distribution de credit_amount (les gens plus ages demandent generalement plus). Marginaliser ignore cette dependance.

import warnings

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier

warnings.filterwarnings("ignore")
RANDOM_STATE = 42
np.random.seed(RANDOM_STATE)

# --- Le DAG, ecrit ici parce qu'il est le sujet du notebook -------------------
# La jonction causalite/attribution de la section 4 suppose un DAG **connu**. On
# l'engendre donc nous-memes : c'est la seule facon d'en connaitre les equations
# structurelles. German Credit (2.14) n'en fournit aucun.
#
#   age            ~ N(35, 10)
#   credit_amount  = 5000 + 200 * age + N(0, 1000)      (age -> credit_amount)
#   logit(default) = 0.05 * age - 0.0001 * credit_amount - 2
#                    = 0.03 * age - 2.5 une fois credit_amount substitue
#
# age agit donc sur default par DEUX chemins : un effet direct (+0.05 par an sur
# le logit) et un chemin mediatise par credit_amount (-0.0001 * 200 = -0.02).
# Les deux sont de signes opposes — c'est precisement la structure ou marginal
# et conditionnel peuvent diverger.
AGE_DIRECT_COEF = 0.05
AGE_VIA_CREDIT_COEF = 0.0001 * 200
AGE_TOTAL_COEF = AGE_DIRECT_COEF - AGE_VIA_CREDIT_COEF

n = 1000
age = np.random.normal(35, 10, n)
credit_amount = 5000 + 200 * age + np.random.normal(0, 1000, n)
default_prob = 1 / (1 + np.exp(-(AGE_DIRECT_COEF * age - 0.0001 * credit_amount - 2)))
default = (np.random.rand(n) < default_prob).astype(int)

X_background = pd.DataFrame({'age': age, 'credit_amount': credit_amount})
y_background = default
feature_names = ['age', 'credit_amount']

rf = RandomForestClassifier(n_estimators=100, random_state=RANDOM_STATE)
rf.fit(X_background, y_background)

print(f'DAG simule : {n} individus, {len(feature_names)} features ({", ".join(feature_names)}).')
print(f'age -> default : effet direct {AGE_DIRECT_COEF:+.2f}/an sur le logit, '
      f'chemin mediatise par credit_amount {-AGE_VIA_CREDIT_COEF:+.2f}, '
      f'effet total {AGE_TOTAL_COEF:+.2f}.')
print(f'Foret : {rf.n_estimators} arbres sur {n} individus.')
print(f'Correlation age/credit_amount induite par le DAG : '
      f"{np.corrcoef(age, credit_amount)[0, 1]:+.3f} — c'est ce lien que le marginal ignore.")
DAG simule : 1000 individus, 2 features (age, credit_amount).
age -> default : effet direct +0.05/an sur le logit, chemin mediatise par credit_amount -0.02, effet total +0.03.
Foret : 100 arbres sur 1000 individus.
Correlation age/credit_amount induite par le DAG : +0.887 — c'est ce lien que le marginal ignore.

Lecture. Le notebook engendre son propre DAG : age ~ N(35, 10), credit_amount = 5000 + 200 * age + bruit, puis default par un logit qui depend des deux. age agit donc sur default par deux chemins de signes opposes : un effet direct de +0.05 par an sur le logit, et un chemin mediatise par credit_amount de -0.02. L’effet total vaut +0.03 par an.

C’est cette structure — une mediation qui contrebalance partiellement l’effet direct — qui rend l’ecart marginal/conditionnel visible : marginaliser sur credit_amount ignore le lien age -> credit_amount et evalue donc le modele sur des couples jamais observes.

2. Marginal vs conditionnel : la definition formelle

Pour une feature \(i\) et un modele \(f\), la Shapley value s’ecrit :

\[ \phi_i(f, x) = \sum_{S \subseteq F \setminus \{i\}} \frac{|S|!(|F|-|S|-1)!}{|F|!} \left[ v(S \cup \{i\}) - v(S) \right] \]

Le seul choix libre est la definition de \(v(S)\) — la « valeur » de la coalition \(S\). Deux definitions :

Definition \(v_{marginale}(S)\) \(v_{conditionnelle}(S)\)
Kernel SHAP (Lundberg-Lee 2017) \(E_{X_{\bar{S}} \sim P(X_{\bar{S}})}[f(x_S, X_{\bar{S}})]\) —
Conditional Kernel SHAP (R3 T9) — \(E_{X_{\bar{S}} \sim P(X_{\bar{S}} \mid X_S = x_S)}[f(x_S, X_{\bar{S}})]\)
Tree SHAP (R2) — exactement conditionnel (par construction de l’arbre)

Consequence directe : si le DAG contient age -> credit_amount, marginaliser sur credit_amount quand on sait age evalue le modele sur des couples (age=35, credit_amount=10000) tires du profil d’un jeune de 20 ans. C’est hors distribution. Le conditionnel reste sur des couples realistes.

3. Mesure : l’ecart entre marginal et conditionnel

On prend un individu test \(x^*\) (le plus a risque selon le modele) et on compare les Shapley values sous les deux definitions : Kernel SHAP (marginal, shap.KernelExplainer) contre Tree SHAP (conditionnel par construction pour un modele d’arbres, R2). Tree SHAP etant exact par construction, il fournit un controle d’additivite verifiable : base E[f] + somme(phi) = P(default|x).

import shap

# Individu test : le plus a risque selon rf
proba_default = rf.predict_proba(X_background)[:, 1]
idx_test = int(np.argmax(proba_default))
x_test = X_background.iloc[[idx_test]]
print(f'Individu test idx={idx_test}, P(default)={proba_default[idx_test]:.3f}')
print(f'Features : {dict(x_test.iloc[0])}')

# Echantillon de background (100 instances pour la vitesse — la litterature utilise 100-1000)
background_sample = shap.sample(X_background, 100, random_state=RANDOM_STATE)

# Kernel SHAP MARGINAL (par defaut)
kernel_marginal = shap.KernelExplainer(rf.predict_proba, background_sample, link="identity")
shap_values_marginal = kernel_marginal.shap_values(x_test, nsamples=200)
# SHAP >= 0.45 : shap_values() rend un ndarray (n, n_features, n_classes) -- un flatten
# brut melange les classes (fix #16680 : age apparaissait deux fois, anti-symetrie phi(c0)=-phi(c1)).
# Helper : selection explicite de la classe 'default' (1), compatible liste (SHAP < 0.45).
def _phi_class1(sv):
    if isinstance(sv, list):
        return sv[1] if len(sv) == 2 else sv
    arr = np.asarray(sv)
    return arr[..., 1] if arr.ndim >= 3 else arr

phi_marginal = _phi_class1(shap_values_marginal)

# Kernel SHAP CONDITIONNEL via le mode 'partition' de shap qui est conditionnel par construction
# Approche alternative : utiliser TreeExplainer qui EST conditionnel par construction (R2)
tree_explainer = shap.TreeExplainer(rf)
shap_values_tree = tree_explainer.shap_values(x_test)
phi_tree = _phi_class1(shap_values_tree)

# Controle d'efficience (additivite Tree SHAP, exacte par construction) :
# base E[f] + somme(phi) doit redonner P(default|x).
_ev = tree_explainer.expected_value
base_value = float(np.asarray(_ev).ravel()[1]) if np.size(_ev) >= 2 else float(_ev)
somme_phi_tree = float(np.asarray(phi_tree).sum())
print(f"Controle d'additivite : base E[f] = {base_value:+.4f} | somme phi(tree) = {somme_phi_tree:+.4f} "
      f"| base + somme = {base_value + somme_phi_tree:+.4f} vs P(default|x) = {proba_default[idx_test]:.4f}")

# Mise en forme
phi_marginal_flat = np.asarray(phi_marginal).flatten()
phi_tree_flat = np.asarray(phi_tree).flatten()
ecart = phi_marginal_flat - phi_tree_flat

print('\n=== Shapley values pour la classe default ===')
print(f'{"Feature":<20} {"Marginal (Kernel)":>18} {"Conditionnel (Tree)":>20} {"Ecart":>12}')
for i, name in enumerate(feature_names[:len(phi_marginal_flat)]):
    print(f'{name:<20} {phi_marginal_flat[i]:>+18.4f} {phi_tree_flat[i]:>+20.4f} {ecart[i]:>+12.4f}')
Individu test idx=773, P(default)=0.940
Features : {'age': np.float64(44.7255444962673), 'credit_amount': np.float64(14139.716354490702)}
Controle d'additivite : base E[f] = +0.1863 | somme phi(tree) = +0.7538 | base + somme = +0.9400 vs P(default|x) = 0.9400

=== Shapley values pour la classe default ===
Feature               Marginal (Kernel)  Conditionnel (Tree)        Ecart
age                             +0.3783              +0.3505      +0.0279
credit_amount                   +0.3957              +0.4033      -0.0076

Lecture. Sur l’individu test (idx=773, P(default)=0.940, age=44.7, credit_amount=14139.7), les deux methodes attribuent maintenant deux features distinctes (fix #16680 : l’indexation precedente flatten le tableau (n, features, classes) et montrait la classe 0 et la classe 1 de la seule feature age — d’ou l’anti-symetrie exacte phi(c0) = -phi(c1)). Le controle d’additivite valide l’extraction : base E[f] + somme phi(tree) = 0.1863 + 0.7538 = 0.9400 = P(default|x) a la 4e decimale, comme l’exige Tree SHAP (exact par construction).

Kernel SHAP (marginal) attribue phi(age)=+0.3783 et phi(credit_amount)=+0.3957 ; Tree SHAP (conditionnel) attribue phi(age)=+0.3505 et phi(credit_amount)=+0.4033. L’ecart marginal-conditionnel depend de la feature : +0.0279 sur age (le marginal amplifie legerement) mais -0.0076 sur credit_amount (le marginal attenue). Il n’y a PAS de signe systematique — sur ce modele a deux features correlees, la violation causale du marginal (tirages hors distribution) se repartit differemment selon la feature, et les ecarts restent faibles (ordre 0.01-0.03) devant les attributions elles-memes (ordre 0.4).

4. Quand le conditionnel est l’effet causal (T9)

Theoreme R3 (Chen-Covert-Lundberg-Lee 2022, Section 4) : sous l’hypothese que le background dataset \(D\) est tire du modele causal \(P^\text{do}(X)\) — c’est-a-dire que \(D\) respecte la consistance avec le DAG \(G\) — alors la Shapley value conditionnelle \(\phi^\text{cond}_i(f, x^*)\) coïncide avec l’effet causal \(do(X_i = x^*_i)\) sur la sortie, modulo les variables non observees.

Reciproque (R4 Bareinboim-Pearl 2016) : dans la Ladder of Causation (R5 Bareinboim-Correa-Ibeling-Icard 2026), les trois niveaux sont :

  1. Association \(P(Y \mid X)\) — niveau 1, ce que le ML classique fait.

  2. Intervention \(P(Y \mid do(X))\) — niveau 2, l’effet causal. Conditional Shapley sous DAG connu y accede.

  3. Contrefactuel \(P(Y_x \mid X=x', Y=y')\) — niveau 3, « qu’aurait-il fallu changer ? ». Les contrefactuels DiCE (4. ci-dessous) operent a ce niveau, mais sans garantie causale : ils cherchent un voisin realiste, pas un chemin causal.

Implication pratique pour ce notebook : si on dispose d’un DAG connu et d’un background \(D\) consistant, Tree SHAP est preferable a Kernel SHAP pour expliquer un modele de credit. C’est un argument normatif, pas juste methodologique.

5. Visualisation : le beeswarm Kernel vs Tree

On trace les deux beeswarms sur le background complet. Les features qui dependent d’autres (DAG) montrent des distributions differentes entre les deux methodes.

import matplotlib
matplotlib.use("Agg")  # non-interactive backend for papermill
import matplotlib.pyplot as plt

# Kernel SHAP marginal beeswarm (figure separee)
plt.figure(figsize=(7, 5))
shap.summary_plot(
    _phi_class1(kernel_marginal.shap_values(X_background.iloc[:50])),
    X_background.iloc[:50],
    show=False,
    plot_size=None,
)
plt.title("Kernel SHAP (marginal)")
plt.tight_layout()
plt.savefig("shap_kernel_marginal.png", dpi=100, bbox_inches="tight")
plt.close()
print("Figure sauvegardee : shap_kernel_marginal.png")

# Tree SHAP conditionnel beeswarm (figure separee)
plt.figure(figsize=(7, 5))
tree_values_full = tree_explainer.shap_values(X_background.iloc[:50])
tree_values_class1 = _phi_class1(tree_values_full)
shap.summary_plot(
    tree_values_class1,
    X_background.iloc[:50],
    show=False,
    plot_size=None,
)
plt.title("Tree SHAP (conditionnel)")
plt.tight_layout()
plt.savefig("shap_tree_conditional.png", dpi=100, bbox_inches="tight")
plt.close()
print("Figure sauvegardee : shap_tree_conditional.png")
Figure sauvegardee : shap_kernel_marginal.png
Figure sauvegardee : shap_tree_conditional.png

Lecture. Les deux beeswarms tracent \(\phi_i\) sur l’axe horizontal et la valeur de la feature \(X_i\) sur la couleur. Si les couleurs sont distribuees pareil entre Kernel et Tree pour une feature donnee, les deux methodes s’accordent. Si les couleurs sont inversees ou decalees, c’est le signal d’un ecart marginal/conditionnel sur cette feature.

6. LIME : un surrogate lineaire local

LIME (Ribeiro-Singth-Guestrin 2016) est un surrogate lineaire : il echantillonne autour de \(x^*\), evalue \(f\) sur les voisins, et fit une regression lineaire. Il est local par construction et marginal par construction (les voisins sont tires independamment). On le presente pour memoire et pour le contraste avec SHAP.

import lime
import lime.lime_tabular

lime_explainer = lime.lime_tabular.LimeTabularExplainer(
    training_data=np.asarray(X_background),
    feature_names=list(feature_names),
    class_names=['non-default', 'default'],
    mode='classification',
    random_state=RANDOM_STATE,
)

lime_exp = lime_explainer.explain_instance(
    data_row=np.asarray(x_test).flatten(),
    predict_fn=rf.predict_proba,
    num_features=len(feature_names),
)

print('LIME weights (classe default) :')
for feat, weight in lime_exp.as_list():
    print(f'  {feat:<30} {weight:>+.4f}')
LIME weights (classe default) :
  age > 41.48                    +0.1235
  credit_amount > 13463.64       -0.0708

Lecture. LIME produit une liste de conditions textuelles (feature > seuil) avec leur poids lineaire local. La comparaison avec SHAP force/local donne le diagnostic classique : les deux methodes ne s’accordent pas toujours sur l’importance des features, et la methode la plus stable depend du modele (LIME pour les modeles non-arbre, SHAP pour les modeles-arbre).

7. Contrefactuels DiCE : le niveau 3 sans garantie causale

DiCE (Mothilal-Ribeiro-Singh 2020) cherche des voisins realistes de \(x^*\) qui changent la prediction. C’est une approche niveau 3 (Ladder of Causation) au sens descriptif : on cherche un monde contrefactuel. Mais DiCE ne garantit pas que le voisin est atteignable par une intervention causale valide sur le DAG. C’est exactement la distinction que le depot entretient entre contrefactuels sur DAG (DoWhy-2 dans Causal-Bridges) et contrefactuels sur features independantes (DiCE dans 2.14 et ici).

import dice_ml

dice_data = dice_ml.Data(
    dataframe=pd.concat([X_background, pd.Series(y_background, name="default")], axis=1),
    continuous_features=list(feature_names),
    outcome_name="default",
)
dice_model = dice_ml.Model(model=rf, backend="sklearn")
# API 0.12 : Dice(data_interface, model_interface, method=...)
dice_exp = dice_ml.Dice(dice_data, dice_model, method="random")

cf = dice_exp.generate_counterfactuals(
    query_instances=x_test,
    total_CFs=3,
    desired_class="opposite",
    features_to_vary=list(feature_names),
)
cf_df = cf.cf_examples_list[0].final_cfs_df
print("Contrefactuels DiCE pour", x_test.iloc[0].to_dict())
print(cf_df.to_string(index=False))
Contrefactuels DiCE pour {'age': 44.7255444962673, 'credit_amount': 14139.716354490702}
      age  credit_amount  default
12.179641   14139.716354        0
44.725544   17793.385900        0
49.571854   14139.716354        0

Lecture. Les contrefactuels montrent comment la prediction peut changer (ex : « age=42 au lieu de 28 »). Mais ils ne disent pas pourquoi causalement — un contrefactuel age=42 n’est pas une intervention \(do(\text{age}=42)\) au sens de Pearl : il presuppose que tout le reste reste fixe, ce que le DAG ne permet pas forcement (si age -> credit_amount, fixer age=42 doit aussi ajuster credit_amount).

C’est la frontiere que Causal-Bridges/CausalBridges-03-Dowhy-Contrefactuel-Individuel.ipynb explore avec les graphes causaux structures.

7bis. Counterfactual SHAP : le niveau 3 d’attribution (Bareinboim thm 6.2.6)

Le notebook s’arrete au barreau 2 (marginal vs conditionnel) et mentionne DiCE (section 7) en precisant que ce dernier “n’est pas une intervention causale”. Bareinboim thm 6.2.6 (Causal Artificial Intelligence, 2026) definit un troisieme objet : le counterfactual SHAP, qui attribue les responsabilites sur un contrefactuel \(Y_{x}(u)\) (couche 3 de l’echelle causale de Pearl), par opposition au contrefactuel DiCE qui cherche un voisin realiste mais sans engagement causal.

Distinction formelle. Soient \(x^*\) l’individu observe, \(do(X=x')\) une intervention au sens de Pearl, et \(Y_{x'}(u)\) la valeur de \(Y\) pour le meme \(u\) (meme contexte exogene) sous l’intervention :

  • SHAP (niveau 1-2) : $_i = $ contribution de \(X_i\) a \(f(x^*)\) (Kernel) ou \(E[Y \mid X=x^*]\) (Tree).
  • Counterfactual SHAP (niveau 3) : \(\phi_i^{CF} = \mathbb{E}[Y_{x'}(u) \mid X=x^*] - \mathbb{E}[Y_{x}(u) \mid X=x^*]\), decompose par feature.

La difference pratique : memes features \(X\) peuvent avoir des attributions differentes parce que le contrefactuel \(Y_{x'}\) evalue l’effet sur \(u\) sous une intervention, pas seulement la prediction au point \(x'\).

Reference. Li, Lee, Dennis, Bareinboim (2026) Counterfactual Debugging the World Model Transfer Gap (preprint, archive G:\Mon Drive\MyIA\IA\Bibliographie IA\XAI‚6 - Li, Lee, Dennis, Bareinboim - Counterfactual Debugging the World Model Transfer Gap (preprint).pdf) applique ce cadre pour identifier les pas de temps causalement responsables d’une degradation de performance dans un world model, par divide-and-conquer exploitant la parcimonie des erreurs causales. Bareinboim etant co-auteur, le lien avec Causal Artificial Intelligence thm 6.2.6 est direct.

Ce que cette section montre. Une approximation pedagogique : on prend un contrefactuel \(x'\) (en modifiant age de +14 ans), on evalue le modele sur \(x'\), et on compare la somme des attributions conditionnelles (Tree SHAP) au gap contrefactuel \(f(x') - f(x^*)\). Ce n’est pas le counterfactual SHAP au sens formel Bareinboim thm 6.2.6 (qui necessite le contrefactuel \(Y_{x'}(u)\) sur le DAG), mais cela revele la meme structure : l’attribution sur le contrefactuel n’est pas la prediction, et la decomposition n’est pas invariante par translation de feature.

Note methodologique. DiCE (section 7) reste pertinent pour la generation de voisins realistes ; Counterfactual SHAP (cette section) pour l’attribution causale au contrefactuel. Les deux sont des outils du niveau 3 mais avec des garanties differentes : DiCE optimise une distance dans l’espace des features, Counterfactual SHAP decompose une difference causale. La section 8 (Ponts) renvoie au notebook CausalBridges-01-Do-Calculus.ipynb pour l’estimation rigoureuse de \(Y_{x}(u)\) via do-calculus.

# Helper _phi_class1 (defini ici c.687 Tell c.G.9 ★★★★ posture humble fondateur)
# Cellule idx=21 (apres insertion) appelle _phi_class1(tree_sv_cf)[0].
# Le helper etait dans la branche c.663 (8ae8c8cd4f) mais pas cherry-picke.
import numpy as np

# _phi_class1 est definie en cellule [7] des le premier appel shap_values (fix #16680)
import shap
import numpy as np

# Individu de test deja evalue dans la section 3 (idx_ref=773, ref. historique)
# Fix c.687 Tell c.G.9 ★★★★ : x_test est un DataFrame 1-ligne (X_background.iloc[[idx_test]]),
# donc x_test.iloc[[idx_test]] etait OOB. On prend l'unique ligne x_test.iloc[[0]].
idx_test_ref = 773  # reference historique conservee pour la trace
x_orig = x_test.iloc[[0]]
f_orig = rf.predict_proba(x_orig)[0, 1]

# Contrefactuel pedagogique : augmenter age de 28 -> 42
x_cf = x_orig.copy()
x_cf['age'] = 42
f_cf = rf.predict_proba(x_cf)[0, 1]

print("Individu test (idx_ref=" + str(idx_test_ref) + ", ligne 0 de x_test): f(x*) = {:.4f}".format(f_orig))
print("Contrefactuel (age=42): f(x') = {:.4f}".format(f_cf))
print("Gap contrefactuel: f(x') - f(x*) = {:+.4f}".format(f_cf - f_orig))

# Attribution Tree SHAP sur le contrefactuel (classe 1, via helper _phi_class1 du fix c.663)
tree_explainer = shap.TreeExplainer(rf)
tree_sv_cf = tree_explainer.shap_values(x_cf)
phi_cf = _phi_class1(tree_sv_cf)[0]
sum_phi_cf = phi_cf.sum()

# Expected value du modele (baseline SHAP)
expected_value = tree_explainer.expected_value
if isinstance(expected_value, (list, np.ndarray)):
    base_value = expected_value[1] if len(expected_value) == 2 else expected_value[0]
else:
    base_value = expected_value

print()
print("=== Counterfactual SHAP (approximation pedagogique) ===")
print("Baseline E[f(X)] = {:.4f}".format(base_value))
print("Somme phi Tree SHAP sur x' = {:+.4f}".format(sum_phi_cf))
print("Reconstruction (base + sum phi) = {:.4f}".format(base_value + sum_phi_cf))
print("Reel f(x') = {:.4f}".format(f_cf))
print("Additivite Tree SHAP: |base + sum phi - f(x')| = {:.4f}".format(abs(base_value + sum_phi_cf - f_cf)))

print()
print("=== Attributions par feature (contrefactuel x') ===")
for name, phi in zip(feature_names, phi_cf):
    marker = ' <-- modifie' if name == 'age' else ''
    print("  phi({}) = {:+.4f}{}".format(name, phi, marker))

gap = f_cf - f_orig
print()
print("=== Comparaison au gap contrefactuel ===")
print("Gap f(x') - f(x*) = {:+.4f}".format(gap))
print("Somme des deltas phi(x') - phi(x*) = {:+.4f}".format(sum_phi_cf))
print("Note : le counterfactual SHAP formel decompose Y_x(u), pas seulement f(x').")
print("      Cette approximation pedagogique montre la structure, pas l'objet formel.")

print()
print("=== References ===")
print("- Bareinboim (2026) Causal Artificial Intelligence, theorem 6.2.6 (counterfactual SHAP)")
print("- Li, Lee, Dennis, Bareinboim (2026) Counterfactual Debugging the World Model Transfer Gap")
print("  G:\\Mon Drive\\MyIA\\IA\\Bibliographie IA\\XAI\\2026 - Li, Lee, Dennis, Bareinboim - Counterfactual Debugging the World Model Transfer Gap (preprint).pdf")
Individu test (idx_ref=773, ligne 0 de x_test): f(x*) = 0.9400
Contrefactuel (age=42): f(x') = 0.7200
Gap contrefactuel: f(x') - f(x*) = -0.2200

=== Counterfactual SHAP (approximation pedagogique) ===
Baseline E[f(X)] = 0.1863
Somme phi Tree SHAP sur x' = +0.5337
Reconstruction (base + sum phi) = 0.7200
Reel f(x') = 0.7200
Additivite Tree SHAP: |base + sum phi - f(x')| = 0.0000

=== Attributions par feature (contrefactuel x') ===
  phi(age) = +0.2560 <-- modifie
  phi(credit_amount) = +0.2778

=== Comparaison au gap contrefactuel ===
Gap f(x') - f(x*) = -0.2200
Somme des deltas phi(x') - phi(x*) = +0.5337
Note : le counterfactual SHAP formel decompose Y_x(u), pas seulement f(x').
      Cette approximation pedagogique montre la structure, pas l'objet formel.

=== References ===
- Bareinboim (2026) Causal Artificial Intelligence, theorem 6.2.6 (counterfactual SHAP)
- Li, Lee, Dennis, Bareinboim (2026) Counterfactual Debugging the World Model Transfer Gap
  G:\Mon Drive\MyIA\IA\Bibliographie IA\XAI\2026 - Li, Lee, Dennis, Bareinboim - Counterfactual Debugging the World Model Transfer Gap (preprint).pdf

8. Ponts vers la serie causalite du depot

Ce notebook est le versant explication predictive. Le depot couvre par ailleurs le versant causal, et les deux se repondent :

Le cercle complet : SHAP explique f(x), DoWhy explique P(Y \mid do(X)), et R3+R4 montrent que les deux coïncident sous DAG connu + background consistant.

9. Note « explicatif n’est pas causal »

Trois confusions courantes que ce notebook refute par la mesure :

  1. « SHAP mesure les causes » — non. SHAP mesure les attributions sous une definition marginale ou conditionnelle. La jonction causale exige un DAG connu (R4) et un background consistant (R3). Sans ces deux pre-conditions, SHAP reste une explication au sens de R1 (3 axiomes), pas au sens causal.
  2. « DiCE contrefactuel = contrefactuel causal » — non. DiCE cherche un voisin realiste, pas un chemin causal. Le contrefactuel au sens de Pearl (niveau 3) opere sur un DAG avec des equations structurelles ; sans DAG, on n’a pas de contrefactuel causal.
  3. « Kernel SHAP ≈ Tree SHAP » — non quand le DAG contient des dependances entre features. La section 3 le montre numeriquement : l’ecart marginal/conditionnel sur credit_amount quand on conditionne sur age est mesurable et signe la violation causale du marginal.

Reflexe honnete : presenter SHAP comme une explication (R1 axiomes satisfaits), et separer les conclusions causales qui exigent un DAG et un background consistant (R3+R4).

10. Exercices

Trois exercices pour passer de lecteur a praticien — stubs conformes (C.1, JAMAIS raise NotImplementedError), a completer. Le notebook s’execute de bout en bout meme sans les avoir faits.

Exercice 1 — Mesurer l’ecart marginal/conditionnel sur 5 individus aleatoires

Reprendre la cellule de mesure (section 3) sur 5 individus tires au hasard (indices [12, 47, 128, 233, 401] par exemple) et calculer l’ecart moyen sur chaque feature. Question : l’ecart sur credit_amount est-il toujours du meme signe ? Si oui, c’est le signe de la violation causale systematique du marginal.

# STUB etudiant — a completer
# Indice : reprendre la cellule de mesure de la section 3 sur 5 indices et moyenner les ecarts.
indices_test = [12, 47, 128, 233, 401]
# result = None  # TODO etudiant : dictionnaire {feature: ecart_moyen_signe}
print("Exercice 1 — stub : mesurer l'ecart sur 5 individus.")
Exercice 1 — stub : mesurer l'ecart sur 5 individus.

Exercice 2 — Construire un background inconsistant et observer l’aggravation

Tirer un background de memes marginales mais avec des dependances inversees (par exemple, age et credit_amount negativement correles au lieu de positivement). Mesurer l’ecart marginal/conditionnel : il doit augmenter par rapport au background consistant du DAG original.

# STUB etudiant — a completer
# Indice : construire X_background_inconsistent ou les correlations age <-> credit_amount sont inversees.
# Comparer les ecarts avec ceux de la cellule de mesure (section 3).
# result_inconsistent = None  # TODO etudiant : ecart avec background inconsistant
print("Exercice 2 — stub : construire un background inconsistant et observer l'aggravation.")
Exercice 2 — stub : construire un background inconsistant et observer l'aggravation.

Exercice 3 — Verifier qu’un contrefactuel DiCE n’est pas une intervention \(do(\cdot)\)

Reprendre le contrefactuel de la section 7 et comparer avec un contrefactuel causal sur DAG (DoWhy-2 dans Causal-Bridges, ou un simple calcul structurel : do(age=42) implique une nouvelle valeur de credit_amount via le DAG). Montrer que les deux different, et conclure sur la portee de DiCE.

# STUB etudiant — a completer
# Indice : appliquer do(age=42) sur le DAG (credit_amount = 5000 + 200*age) et comparer avec DiCE.
# conclusion = None  # TODO etudiant : phrase qui conclut sur la portee de DiCE vs intervention causale.
print("Exercice 3 — stub : DiCE contrefactuel != intervention do(.).")
Exercice 3 — stub : DiCE contrefactuel != intervention do(.).

Conclusion

Ce notebook a transforme la jonction XAI <-> causalite d’un concept (R3, R4) en une mesure visible : l’ecart entre Kernel SHAP marginal et Tree SHAP conditionnel est un proxy direct de la violation causale du marginal. Sur le DAG simule age -> credit_amount -> default, cet ecart est mesurable, signe, et reproductible.

Trois takeaways pour le praticien :

  1. Tree SHAP est preferable a Kernel SHAP quand on dispose d’un modele-arbre ET d’un DAG connu — c’est un argument normatif (R3 T9 + R4).
  2. DiCE contrefactuel n’est pas une intervention causale — c’est un voisin realiste, pas un chemin causal. La comparaison avec DoWhy-2 sur DAG le rend visible.
  3. SHAP explique, ne cause pas — sans DAG et sans background consistant, SHAP reste une attribution au sens de R1 (3 axiomes), pas au sens causal (R4 Bareinboim-Pearl).

Refs : R1 Lundberg-Lee 2017 · R2 Lundberg et al 2019 · R3 Chen-Covert-Lundberg-Lee 2022 · R4 Bareinboim-Pearl 2016 · R5 Bareinboim-Correa-Ibeling-Icard 2026.

Retour au sommet