Explaining a model

Creative Commons License

aGrUM

interactive online version

In [1]:
import time

import pandas as pd

import pyagrum as gum
import pyagrum.explain as expl
import pyagrum.explain.notebook as explnb

1- Building the model

We build a simple graph for the example

In [2]:
template = gum.fastBN("X1->X2->Y;X3->Z->Y;X0->Z;X1->Z;X2->R[5];Z->R;X1->Y")
data_path = "res/shap/Data_6var_direct_indirect.csv"

# gum.generateSample(template,1000,data_path)

learner = gum.BNLearner(data_path, template)
bn = learner.learnParameters(template.dag())

validation, ll = gum.generateSample(bn, n=500)

print("\n\n** Bayesian Network :")
gnb.show(bn)
print("\n\n** Valdidation dataframe :\n")
print(validation)
print(f"\n\n** Log Likehood(validation): {ll}")


** Bayesian Network :
../_images/notebooks_95-Tools_Explain_5_1.svg


** Valdidation dataframe :

    X0 X2 X1  Y  R X3  Z
0    1  0  1  1  4  0  0
1    1  0  0  1  4  0  1
2    0  1  0  1  1  0  1
3    0  0  1  0  2  0  1
4    0  0  0  1  1  0  0
..  .. .. .. .. .. .. ..
495  1  1  0  1  3  0  1
496  0  1  0  1  0  1  1
497  1  1  0  1  1  1  0
498  0  0  1  0  3  1  1
499  1  1  1  0  1  1  0

[500 rows x 7 columns]


** Log Likehood(validation): -3815.7597115659423

2-independence list (w.r.t. the class Y)

Given a model, it may be interesting to investigate the conditional independences encoded in the BN.

This function explores all the CI between 2 variables and computes the p-values w.r.t to a dataframe or a csv file.

In [3]:
# using the learning base
expl.independenceListForPairs(bn, data_path)
Out[3]:
{('R', 'X0', ('X1', 'Z')): 0.7083382647903902,
 ('R', 'X1', ('X2', 'Z')): 0.4693848625409949,
 ('R', 'X3', ('X1', 'Z')): 0.4128522974536623,
 ('R', 'Y', ('X2', 'Z')): 0.8684231094674687,
 ('X0', 'X1', ()): 0.723302358657366,
 ('X0', 'X2', ()): 0.9801394906304377,
 ('X0', 'X3', ()): 0.7676868597218647,
 ('X0', 'Y', ('X1', 'Z')): 0.5816487109659612,
 ('X1', 'X3', ()): 0.5216508257424717,
 ('X2', 'X3', ()): 0.9837021981131505,
 ('X2', 'Z', ('X1',)): 0.6638491605436834,
 ('X3', 'Y', ('X1', 'Z')): 0.8774081450472305}
../_images/notebooks_95-Tools_Explain_7_1.svg
In [4]:
# or the validation dataframe
expl.independenceListForPairs(bn, validation)
Out[4]:
{('R', 'X0', ('X1', 'Z')): 0.7350332857490167,
 ('R', 'X1', ('X2', 'Z')): 0.6704062288770123,
 ('R', 'X3', ('X1', 'Z')): 0.2729110586588866,
 ('R', 'Y', ('X2', 'Z')): 0.17226987344504074,
 ('X0', 'X1', ()): 0.22846499210964832,
 ('X0', 'X2', ()): 0.09599012262648077,
 ('X0', 'X3', ()): 0.9050225703943482,
 ('X0', 'Y', ('X1', 'Z')): 0.9660229290642519,
 ('X1', 'X3', ()): 0.8067218734360745,
 ('X2', 'X3', ()): 0.4122118109272994,
 ('X2', 'Z', ('X1',)): 0.5503525761133798,
 ('X3', 'Y', ('X1', 'Z')): 0.022654920948253814}
../_images/notebooks_95-Tools_Explain_8_1.svg

… with respect to a specific target.

In [5]:
expl.independenceListForPairs(bn, data_path, target="Y")
Out[5]:
{('Y', 'R', ('X2', 'Z')): 0.8684231094674687,
 ('Y', 'X0', ('X1', 'Z')): 0.5816487109659612,
 ('Y', 'X3', ('X1', 'Z')): 0.8774081450472305}
../_images/notebooks_95-Tools_Explain_10_1.svg

3-SHAP values : explaining a Bayesian network as a classifier

In [6]:
print(expl.ConditionalShapValues.__doc__)

The ConditionalShapValues class computes the conditional Shapley values for a given target node in a Bayesian Network.

The ShapleyValues classes compute Shapley values in Bayesian networks. You must specify a target node. Each class (ConditionalShapValues, CausalShapValues, MarginalShapValues) takes the BN and target at construction; call .compute((dataframe, with_labels)) to obtain an Explanation object.

Compute Conditionnal in Bayesian Network

In [7]:
gumshap = expl.ConditionalShapValues(bn, "Y")

A dataset (as a pandas.DataFrame) must be provided. By default with_labels=True, meaning the data uses label strings instead of integer indices. To override, pass a tuple (df, with_labels) explicitly. The compute() method returns an Explanation object whose values and importances can be visualised with explnb.beeswarm() (distribution of shap values per variable) and explnb.bar() (variable importance ranking).

In [8]:
train = pd.read_csv(data_path).sample(frac=1.0)
In [9]:
t_start = time.time()
resultat = gumshap.compute(train)
explnb.beeswarm(resultat)
explnb.bar(resultat)
print(f"Run Time : {time.time() - t_start} sec")
Run Time : 1.184525966644287 sec
../_images/notebooks_95-Tools_Explain_18_1.svg
../_images/notebooks_95-Tools_Explain_18_2.svg
In [10]:
resultat = gumshap.compute(train)
explnb.bar(resultat)
explnb.bar(resultat, y=1)
print(f"Run Time : {time.time() - t_start} sec")
Run Time : 2.619951009750366 sec
../_images/notebooks_95-Tools_Explain_19_1.svg
../_images/notebooks_95-Tools_Explain_19_2.svg
In [11]:
result = gumshap.compute(train)
explnb.beeswarm(result)
# result is an Explanation object with Shapley values for all nodes.
../_images/notebooks_95-Tools_Explain_20_0.svg

The result is an Explanation object (a MutableMapping). Its .importances attribute maps feature names to their average absolute Shapley value.

In [12]:
resultat = gumshap.compute(train)
print(f"Run Time : {time.time() - t_start} sec")
Run Time : 5.096851110458374 sec

Causal Shap Values

This method is similar to the previous one, except the formula of computation. It computes the causal shap value as described in the paper of Heskes Causal Shapley Values: Exploiting Causal Knowledge to Explain Individual Predictions of Complex Models .

In [13]:
t_start = time.time()
gumshap_causal = expl.CausalShapValues(bn, "Y", background=train)
causal = gumshap_causal.compute(train)
explnb.beeswarm(causal)
explnb.bar(causal)
print(f"Run Time : {time.time() - t_start} sec")
Run Time : 12.337070941925049 sec
../_images/notebooks_95-Tools_Explain_25_1.svg
../_images/notebooks_95-Tools_Explain_25_2.svg

As you can see, since \(R\) is not among the ‘causes’ of Y, its causal importance is null.

Marginal Shap Values

Similarly, one can also compute marginal Shap Value.

In [14]:
t_start = time.time()
gumshap_marginal = expl.MarginalShapValues(bn, "Y", background=train, sample_size=10)
marginal = gumshap_marginal.compute(train)
explnb.beeswarm(marginal)
explnb.bar(marginal)
print(f"Run Time : {time.time() - t_start} sec")
Run Time : 6.916605234146118 sec
../_images/notebooks_95-Tools_Explain_27_1.svg
../_images/notebooks_95-Tools_Explain_27_2.svg

As you can see, since \(R\), \(X0\) and \(X3\) are not in the Markov Blanket of \(Y\), their marginal importances are null.

Saving the graph

Pass a filename argument to any plot function to save the figure to a file instead of displaying it:

In [15]:
import os

os.makedirs("out", exist_ok=True)

t_start = time.time()
causal2 = gumshap_causal.compute(train)
explnb.beeswarm(causal2, filename="out/beeswarm_causal.png")
explnb.bar(causal2, filename="out/bar_causal.pdf")
print(f"Run Time : {time.time() - t_start} sec")
Run Time : 11.827458143234253 sec

Visualizing SHAP values directly on a BN

This function returns a coloured graph that makes it easier to understand which variable is important and where it is located in the graph.

In [16]:
explnb.showShapValues(bn, causal, show_legend=True)
../_images/notebooks_95-Tools_Explain_33_0.svg
../_images/notebooks_95-Tools_Explain_33_1.svg

3-ShALL values : explaining the likelihood of a BN

While ShAP values explain why a BN classifies an observation as it does (contribution of each feature to \(P(Y=y \mid x)\)), ShALL values explain why the model assigns the likelihood it does to a complete observation (contribution of each feature to \(\log P(x \mid \theta)\)).

A ShALL explanation is always local: it decomposes the log-likelihood of one specific instance into per-feature contributions relative to a baseline. The natural visualisation is a waterfall chart.

We pick one specific observation from the validation set and explain its likelihood under the learned BN using ConditionalShallValues.

In [17]:
# Single instance from validation
instance = validation.iloc[[0]]
print(instance)
  X0 X2 X1  Y  R X3  Z
0  1  0  1  1  4  0  0
In [18]:
gumshall = expl.ConditionalShallValues(bn, background=train)
shall_local = gumshall.compute(instance)
explnb.waterfall(shall_local)
print(shall_local)
Explanation(values={'X1': 0.10826734150097458, 'X2': 0.22405776089325705, 'Y': 0.17130472116824477, 'X3': 0.2590002904588106, 'Z': 0.1048797375798167, 'X0': -0.35927320197211715, 'R': 0.08135240106318156})
../_images/notebooks_95-Tools_Explain_37_1.svg

4- Visualizing information

Another view annotates the graph directly with information-theoretic quantities: each node is colored by its entropy (darker = lower, brighter = higher), and each arc thickness encodes the mutual information between its two endpoints (thicker = stronger statistical dependency). Pass show_legend=False to not display the scales.

In [19]:
expl.showInformation(bn)
G R R Z Z Z->R Y Y Z->Y X3 X3 X3->Z X2 X2 X2->R X2->Y X1 X1 X1->Z X1->X2 X1->Y X0 X0 X0->Z
In [20]:
expl.showInformation(bn, show_legend=False)
G R R Z Z Z->R Y Y Z->Y X3 X3 X3->Z X2 X2 X2->R X2->Y X1 X1 X1->Z X1->X2 X1->Y X0 X0 X0->Z