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 [53]:
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 [54]:
%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 [55]:
target = pd.read_csv('target_ML1M.csv')
target = 1-target
data = pd.read_csv('data_ML1M.csv')
feature_names = pd.read_csv('feature_names_ML1M.csv')

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 [37]:
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 [38]:
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 [16]:
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[16]:
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 [56]:
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 [57]:
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 [19]:
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 [58]:
indices_probs_pos #all instances that are predicted as 'FEMALE'
Out[58]:
(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 [77]:
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 [78]:
explanation[0]
Out[78]:
[['Secrets & Lies (1996)',
  'Strictly Ballroom (1992)',
  'Shakespeare in Love (1998)',
  'Ideal Husband',
  'Thelma & Louise (1991)',
  'Elizabeth (1998)']]
In [63]:
print("IF the user did not watch the movie(s) " + str(explanation[0][0]) + ", 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 [88]:
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 [89]:
explanation[0]
print("IF the user did not watch the movie(s) " + str(explanation[0][0]) + ", 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[0] shows the explanation set(s), explanation[1] shows the number of active features of the instance to explain, explanation[2] shows the number of explanations found, explanation[3] shows the number of features in the smallest-sized explanation, explanation[4] shows the time elapsed in seconds to find the explanation, explanation[5] shows the predicted score change when removing the feature(s) in the smallest-sized explanation, explanation[6] shows the number of iterations that the algorithm needed.

In [90]:
explanation
Out[90]:
([['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 [72]:
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 [85]:
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 [87]:
explanation
Out[87]:
([['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)