 In this tutorial, we show you how to compute counterfactual explanations for explaining positively-predicted instances. We use movie viewing data (Movielens1m) where the goal is to predict gender ('Female' user). The counterfactual explanation shows a set of movies such that when removing them from the user's viewing history, the predicted class changes from 'Female' to 'Male'.

Import libraries and import data set.

In :
import pandas as pd
import numpy as np
import sedc_algorithm
from function_edc import fn_1
import scipy
from sklearn.metrics import roc_auc_score, accuracy_score, precision_recall_fscore_support, f1_score, confusion_matrix

In :
%run sedc_algorithm.py #run sedc_algorithm.py module


For this demonstration, we use the Movielens 1M data set, which contains movie viewing behavior of users. The target variable is binary (taking value 1 if gender = 'FEMALE' and 0 if gender = 'MALE').

In :
target = pd.read_csv('target_ML1M.csv')
target = 1-target


Split data into a training and test set (80-20%). We use a L2-regularized Logistic Regression model. We train the LR classifier on the training data set.

In :
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(scipy.sparse.csr_matrix(data.iloc[:,1:3707].values), target.iloc[:,1], test_size=0.2, random_state=0)
x_train, x_val, y_train, y_val = train_test_split(x_train, y_train, test_size=0.2, random_state=0)

In :
from sklearn.linear_model import LogisticRegression
#Values of the regularization parameter C in L2-LR.
C = [10**(-3),10**(-2),10**(-1),10**(0),10**(1),10**(2)]
p = np.sum(y_train)/np.size(y_train)
print("The balance of target in training subset is %f." %p)
#There are 70% male users, 30% female users in the training data.

The balance of target in training subset is 0.285123.


We finetune the regularization parameter using a hold-out validation data set. We finetune the model on validation accuracy.

In :
accuracy_vals=[]
for c in C:
LR = LogisticRegression(penalty='l2', solver='sag', C = c) #L2-regularized Logistic Regression
LR.fit(x_train, y_train)

probs = LR.predict_proba(x_val)[:,1]
threshold_classifier_probs = np.percentile(probs,(100-(p*100)))
predictions_probs = (probs >= threshold_classifier_probs) #Explicit, discrete predictions for validation data instances

accuracy_val = accuracy_score(y_val, np.array(predictions_probs))
accuracy_vals.append(accuracy_val)
print("The finetuning process has ended...")

C_optimal_accuracy = C[np.argmax(accuracy_vals)]
LR_best = LogisticRegression(penalty='l2', solver='sag', C = C_optimal_accuracy)
LR_best.fit(x_train, y_train)

The finetuning process has ended...

Out:
LogisticRegression(C=0.01, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, l1_ratio=None, max_iter=100,
multi_class='warn', n_jobs=None, penalty='l2',
random_state=None, solver='sag', tol=0.0001, verbose=0,
warm_start=False)
In :
probs = LR_best.predict_proba(x_test)[:,1]
threshold_classifier_probs = np.percentile(probs,(100-(p*100)))
predictions_probs = (probs >= threshold_classifier_probs) #Explicit, discrete predictions for validation data instances

accuracy_test = accuracy_score(y_test, np.array(predictions_probs))
print("The accuracy of the model on the test data is %f" %accuracy_test)

indices_probs_pos = np.nonzero(predictions_probs)#indices of the test instances that are positively-predicted

The accuracy of the model on the test data is 0.784768

In :
classification_model = LR_best

def classifier_fn(X):
c=classification_model.predict_proba(X)
y_predicted_proba=c[:,1]
return y_predicted_proba


Create an SEDC explainer object. By default, the SEDC algorithm stops looking for explanations when a first explanation is found or when a 5-minute time limit is exceeded or when more than 50 iterations are required (see edc_agnostic.py for more details). Only the active (nonzero) features are perturbed (set to zero) to evaluate the impact on the model's predicted output. In other words, only the movies that a user has watched can become part of the counterfactual explanation of the model prediction.

In :
explainer_SEDC = SEDC_Explainer(feature_names = np.array(feature_names.iloc[:,1]),
threshold_classifier = threshold_classifier_probs,
classifier_fn = classifier_fn)


Show indices of positively-predicted test instances.

In :
indices_probs_pos #all instances that are predicted as 'FEMALE'

Out:
(array([   1,    2,   13,   15,   16,   17,   33,   35,   36,   39,   45,
46,   47,   50,   51,   53,   56,   58,   59,   68,   72,   85,
92,   96,   98,   99,  105,  108,  109,  113,  121,  126,  128,
129,  130,  132,  134,  145,  155,  158,  165,  172,  178,  182,
184,  187,  188,  193,  194,  196,  205,  207,  208,  209,  210,
212,  217,  218,  224,  225,  226,  227,  229,  231,  232,  236,
240,  246,  251,  260,  261,  266,  267,  270,  286,  288,  293,
297,  299,  300,  303,  307,  308,  311,  313,  321,  327,  334,
335,  337,  344,  345,  347,  348,  357,  359,  362,  368,  370,
373,  377,  379,  381,  382,  387,  388,  390,  392,  393,  400,
402,  404,  405,  406,  407,  412,  414,  422,  426,  428,  429,
432,  434,  435,  438,  441,  445,  446,  447,  448,  449,  450,
452,  457,  459,  461,  467,  468,  480,  481,  488,  492,  494,
495,  497,  499,  500,  505,  507,  509,  511,  512,  516,  517,
518,  520,  522,  528,  531,  535,  536,  538,  541,  543,  544,
545,  551,  553,  560,  562,  580,  582,  585,  589,  591,  594,
598,  602,  604,  605,  611,  613,  614,  618,  621,  622,  626,
638,  646,  656,  658,  660,  662,  664,  668,  673,  674,  675,
683,  684,  693,  705,  707,  718,  721,  726,  728,  729,  733,
734,  736,  742,  746,  760,  764,  770,  774,  780,  782,  785,
787,  788,  790,  792,  794,  798,  799,  802,  804,  809,  812,
813,  824,  827,  829,  835,  853,  861,  863,  864,  865,  867,
868,  872,  874,  879,  881,  883,  884,  889,  891,  898,  902,
905,  906,  908,  909,  911,  914,  915,  921,  931,  933,  935,
942,  946,  948,  951,  958,  959,  960,  961,  963,  967,  972,
979,  982,  985,  992,  994,  995,  998, 1004, 1005, 1012, 1014,
1017, 1018, 1026, 1028, 1029, 1031, 1039, 1041, 1043, 1045, 1048,
1052, 1053, 1058, 1061, 1068, 1072, 1075, 1077, 1083, 1084, 1087,
1090, 1096, 1098, 1099, 1101, 1102, 1108, 1109, 1110, 1111, 1112,
1113, 1116, 1119, 1124, 1130, 1137, 1144, 1145, 1148, 1149, 1151,
1152, 1153, 1154, 1158, 1159, 1160, 1163, 1165, 1168, 1171, 1179,
1180, 1181, 1190, 1196], dtype=int64),)

Explain why the user with index = 13 is predicted as a 'FEMALE' user by the model.

In :
index = 13
instance_idx = x_test[index]
explanation = explainer_SEDC.explanation(instance_idx)

Initialization is complete.

Elapsed time 0

Iteration 1

The difference is 0.041250
Index is 0.000000
Length of new_combinations is 1 features.
New combinations can be expanded
Threshold is 0.080442

Elapsed time 0

Size combis to expand 344

Iteration 2

The difference is 0.080442
Index is 143.000000
Length of new_combinations is 2 features.
New combinations can be expanded
Threshold is 0.111525

Elapsed time 0

Size combis to expand 514

Iteration 3

The difference is 0.111525
Index is 55.000000
Length of new_combinations is 3 features.
New combinations can be expanded
Threshold is 0.142423

Elapsed time 0

Size combis to expand 683

Iteration 4

The difference is 0.142423
Index is 0.000000
Length of new_combinations is 4 features.
New combinations can be expanded
Threshold is 0.171638

Elapsed time 1

Size combis to expand 851

Iteration 5

The difference is 0.171638
Index is 96.000000
Length of new_combinations is 5 features.
New combinations can be expanded
Threshold is 0.193319

Elapsed time 2

Size combis to expand 1018

Iteration 6

The difference is 0.193319
Index is 78.000000
Length of new_combinations is 6 features.
New combination cannot be expanded

Elapsed time 2

Size combis to expand 1018

Iterations are done.

Elapsed time 2


In :
explanation

Out:
[['Secrets & Lies (1996)',
'Strictly Ballroom (1992)',
'Shakespeare in Love (1998)',
'Ideal Husband',
'Thelma & Louise (1991)',
'Elizabeth (1998)']]
In :
print("IF the user did not watch the movie(s) " + str(explanation) + ", THEN the predicted class would change from 'FEMALE' to 'MALE'.")

IF the user did not watch the movie(s) ['Secrets & Lies (1996)', 'Strictly Ballroom (1992)', 'Shakespeare in Love (1998)', 'Ideal Husband', 'Thelma & Louise (1991)', 'Elizabeth (1998)'], THEN the predicted class would change from 'FEMALE' to 'MALE'.


Explain why the user with index = 15 is predicted as a 'FEMALE' user by the model.

In :
index = 15
instance_idx = x_test[index]
explanation = explainer_SEDC.explanation(instance_idx)

Initialization is complete.

Elapsed time 0

Iteration 1

The difference is 0.042161
Index is 1.000000
Length of new_combinations is 1 features.
New combinations can be expanded
Threshold is 0.080799

Elapsed time 0

Size combis to expand 120

Iteration 2

The difference is 0.080799
Index is 23.000000
Length of new_combinations is 2 features.
New combinations can be expanded
Threshold is 0.081043

Elapsed time 0

Size combis to expand 178

Iteration 3

The difference is 0.081043
Index is 24.000000
Length of new_combinations is 3 features.
New combination cannot be expanded

Elapsed time 0

Size combis to expand 178

Iterations are done.

Elapsed time 0


In :
explanation
print("IF the user did not watch the movie(s) " + str(explanation) + ", THEN the predicted class would change from 'FEMALE' to 'MALE'.")

IF the user did not watch the movie(s) ['Secrets & Lies (1996)', 'Sense and Sensibility (1995)', 'Thelma & Louise (1991)'], THEN the predicted class would change from 'FEMALE' to 'MALE'.


Show more information about the explanation(s): explanation shows the explanation set(s), explanation shows the number of active features of the instance to explain, explanation shows the number of explanations found, explanation shows the number of features in the smallest-sized explanation, explanation shows the time elapsed in seconds to find the explanation, explanation shows the predicted score change when removing the feature(s) in the smallest-sized explanation, explanation shows the number of iterations that the algorithm needed.

In :
explanation

Out:
([['Secrets & Lies (1996)',
'Sense and Sensibility (1995)',
'Thelma & Louise (1991)']],
61,
27,
3,
0.1884608268737793,
[array([0.10901977])],
3)

Show the 10 first explanation(s) found by the SEDC algorithm to explain the user index = 13. We change max_explained to 10.

In :
explainer_SEDC2 = SEDC_Explainer(feature_names = np.array(feature_names.iloc[:,1]),
threshold_classifier = threshold_classifier_probs,
classifier_fn = classifier_fn, max_explained = 10)

In :
index = 45
instance_idx = x_test[index]
explanation = explainer_SEDC2.explanation(instance_idx)

Initialization is complete.

Elapsed time 0

Iteration 1

The difference is 0.039333
Index is 28.000000
Length of new_combinations is 1 features.
New combinations can be expanded
Threshold is 0.069850

Elapsed time 0

Size combis to expand 242

Iteration 2

The difference is 0.069850
Index is 1.000000
Length of new_combinations is 2 features.
New combinations can be expanded
Threshold is 0.080090

Elapsed time 0

Size combis to expand 361

Iteration 3

The difference is 0.080090
Index is 78.000000
Length of new_combinations is 3 features.
New combination cannot be expanded

Elapsed time 0

Size combis to expand 361

Iterations are done.

Elapsed time 0



There are 32 explanations found after 3 iterations. The time elapsed is less than a second. The number of active features (movies watched) is 122 movies.

In :
explanation

Out:
([['Sense and Sensibility (1995)',
'Shakespeare in Love (1998)',
'Elizabeth (1998)'],
['Sense and Sensibility (1995)',
'Shakespeare in Love (1998)',
'28 Days (2000)'],
['Sense and Sensibility (1995)',
'Shakespeare in Love (1998)',
'Kiss the Girls (1997)'],
['Sense and Sensibility (1995)',
'Shakespeare in Love (1998)',
'What Lies Beneath (2000)'],
['Sense and Sensibility (1995)',
'Shakespeare in Love (1998)',
'Working Girl (1988)'],
['Sense and Sensibility (1995)',
'Shakespeare in Love (1998)',
'Pretty Woman (1990)'],
['Sense and Sensibility (1995)',
'Shakespeare in Love (1998)',
'Babe (1995)'],
['Sense and Sensibility (1995)',
'Shakespeare in Love (1998)',
'Fatal Attraction (1987)'],
['Sense and Sensibility (1995)',
'Shakespeare in Love (1998)',
"William Shakespeare's Romeo and Juliet (1996)"],
['Sense and Sensibility (1995)',
'Shakespeare in Love (1998)',
'Seven (Se7en) (1995)']],
122,
32,
3,
0.3930349349975586,
[array([0.09805177]),
array([0.0978386]),
array([0.0947016]),
array([0.09215392]),
array([0.09175856]),
array([0.09097451]),
array([0.09062934]),
array([0.08967295]),
array([0.08961191]),
array([0.08835361])],
3)