Link Search Menu Expand Document
PyXAI
Papers Video GitHub In-the-Loop EXPEKCTATION Release Notes About
download notebook

Contrastive Reasons

Unlike abductive explanations that explain why an instance $x$ is classified as belonging to a given class, contrastive explanations explain why $x$ has not been classified by the ML model as expected.

Let $f$ be a Boolean function represented by a decision tree $T$, $x$ be an instance and $p$ the prediction of $T$ on $x$ ($f(x) = p$), a contrastive reason for $x$ is a term $t$ such that:

  • $t \subseteq t_{x}$, $t_{x} \setminus t$ is not an implicant of $f$ ;
  • for every $\ell \in t$, $t \setminus {\ell}$ does not satisfy this previous condition (that is, $t$ is minimal w.r.t. set inclusion).

Formally, a contrastive reason for $x$ is a subset $t$ of the characteristics of $x$ that is minimal w.r.t. set inclusion among those such that at least one instance $x’$ that coincides with $x$ except on the characteristics from $t$ is not classified by the decision tree as $x$ is. In a simple way, a contrastive reason represents the adjustments in the features that we have to do to change the prediction for an instance.

A contrastive reason is minimal w.r.t. set inclusion, i.e., there is no subset of this reason which is also a contrastive reason. A minimal contrastive reason for $x$ is a contrastive reason for $x$ that contains a minimal number of literals. In other words, a minimal contrastive reason has a minimal size.

PyXAI provides two methods for contrastive reasons for Decision Trees:

More information about contrastive reasons can be found in the paper On the Explanatory Power of Decision Trees.

As the contrastive_reason returns the contrastive reasons in a ascending order according to their sizes, the minimal contrastive reasons are the first ones in the returned tuple.

The basic methods (initialize, set_instance, to_features, is_reason, …) of the Explainer module used in the next examples are described in the Explainer Principles page.

Example from a Hand-Crafted Tree

For this example, we take the Decision Tree of the Building Models page consisting of $4$ binary features ($x_1$, $x_2$, $x_3$ and $x_4$).

The following figure shows the new instances (respectively, $(1,1,1,0)$, $(0,0,1,1)$ and $(0,1,0,1)$) created from the contrastive reasons $(x_4)$ in red, $(x_1, x_2)$ in blue and $(x_1, x_3)$ in green of the instance $(1,1,1,1)$. Thus, the instance $(1,1,1,0)$ (resp. $(0,0,1,1)$ and $(0,1,0,1)$) that differs with $x$ only on $x_4$ (resp. $(x_1, x_2)$ and $(x_1, x_3)$) is not classified by $T$ as $x$ is ($(1,1,1,0)$, $(0,0,1,1)$ and $(0,1,0,1)$ are classified as negative instances while $(1,1,1,1)$ is classified as a positive instance).

DTcontrastive

Now, we show how to get them with PyXAI. We start by building the decision tree:

from pyxai import Builder, Explaining

node_x4_1 = Builder.DecisionNode(4, left=0, right=1)
node_x4_2 = Builder.DecisionNode(4, left=0, right=1)
node_x4_3 = Builder.DecisionNode(4, left=0, right=1)
node_x4_4 = Builder.DecisionNode(4, left=0, right=1)
node_x4_5 = Builder.DecisionNode(4, left=0, right=1)

node_x3_1 = Builder.DecisionNode(3, left=0, right=node_x4_1)
node_x3_2 = Builder.DecisionNode(3, left=node_x4_2, right=node_x4_3)
node_x3_3 = Builder.DecisionNode(3, left=node_x4_4, right=node_x4_5)

node_x2_1 = Builder.DecisionNode(2, left=0, right=node_x3_1)
node_x2_2 = Builder.DecisionNode(2, left=node_x3_2, right=node_x3_3)

node_x1_1 = Builder.DecisionNode(1, left=node_x2_1, right=node_x2_2)

tree = Builder.DecisionTree(4, node_x1_1, force_features_equal_to_binaries=True)

We compute the contrastive reasons for these two instances:

explainer = Explaining.initialize(tree)
explainer.set_instance((1,1,1,1))

contrastives = explainer.contrastive_reason(n=Explaining.ALL)
print("Contrastives:", contrastives)
for contrastive in contrastives:
  assert explainer.is_contrastive_reason(contrastive), "This is have to be a contrastive reason !"

print("-------------------------------")

explainer.set_instance((0,0,0,0))

contrastives = explainer.contrastive_reason(n=Explaining.ALL)
print("Contrastives:", contrastives)
for contrastive in contrastives:
  assert explainer.is_contrastive_reason(contrastive), "This is have to be a contrastive reason !"
Contrastives: ((4,), (1, 2), (1, 3))
-------------------------------
Contrastives: ((-1, -4), (-2, -3, -4))

Example from a Real Dataset

For this example, we take the compas dataset. We create a model using the hold-out approach (by default, the test size is set to 30%) and select a well-classified instance.

from pyxai import Learning, Explaining

learner = Learning.Scikitlearn("../../../dataset/compas.csv", problem_type='classification')
model = learner.evaluate(splitting_method=Learning.HOLD_OUT, model_type=Learning.DT)
instance, prediction = learner.get_instances(model, n=1, is_correct=True)
--------------   Information   ---------------
Problem type: classification
Instances type: tabular
Labels type: classes

Dataset path: ../../../dataset/compas.csv
nFeatures (nAttributes, with the labels): 11
nInstances (nObservations): 6172
nLabels: 2
---------------   Model creation, fitting and evaluation  ---------------
Splitting method: hold-out
Problem type: classification
Models type: decision-tree
model_parameters: {}
---------   Evaluation Information   ---------
For the evaluation number 0:
Metrics:
   sklearn_confusion_matrix: [[649, 202], [304, 388]]
   precision: 65.76271186440678
   recall: 56.06936416184971
   f1_score: 60.53042121684868
   specificity: 76.26321974148061
   true_positive: 388
   true_negative: 649
   false_positive: 202
   false_negative: 304
   accuracy: 67.20674011665587
Number of Training instances: 4629
Number of Testing instances: 1543

---------------   Explainer   ----------------
For the split number 0:
**Decision Tree Model**
nFeatures: 11
nNodes: 584
nVariables: 53

---------------   Instances   ----------------
Number of instances selected: 1
----------------------------------------------

We compute all the contrastives reasons for this instance:

explainer = Explaining.initialize(model, instance)
print("instance:", instance)
print("prediction:", prediction)
print()

constractive_reasons = explainer.contrastive_reason(n=Explaining.ALL)
print("number of constractive reasons:", len(constractive_reasons))

all_are_contrastive = True
for contrastive in constractive_reasons:
  if not explainer.is_contrastive_reason(contrastive):
    print(f"{contrastive} is not a contrastive reason.")
    all_are_contrastive = False

if all_are_contrastive: print("all reasons are indeed contrastives.")
instance: Misdemeanor             0
Number_of_Priors        0
score_factor            0
Age_Above_FourtyFive    1
Age_Below_TwentyFive    0
African_American        0
Asian                   0
Hispanic                0
Native_American         0
Other                   1
Female                  0
Name: 0, dtype: int64
prediction: 0

number of constractive reasons: 14
all reasons are indeed contrastives.

Other types of explanations are presented in the Explanations Computation page.