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_reasonreturns 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).

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.