55#
66# License: BSD 3 clause
77
8+
89import warnings
910
1011import numpy as np
@@ -17,8 +18,10 @@ def create_counterfactual(
1718 model ,
1819 X_dataset ,
1920 y_desired_proba = None ,
20- lammbda = 0.1 ,
21+ lammbda = 10 ,
2122 random_seed = None ,
23+ fixed_features = None ,
24+ method = "Nelder-Mead" ,
2225):
2326 """
2427 Implementation of the counterfactual method by Wachter et al. 2017
@@ -27,8 +30,8 @@ def create_counterfactual(
2730
2831 - Wachter, S., Mittelstadt, B., & Russell, C. (2017).
2932 Counterfactual explanations without opening the black box:
30- Automated decisions and the GDPR. Harv. JL & Tech., 31, 841.,
31- https://arxiv.org/abs/1711.00399
33+ Automated decisions and the GDPR. Harv. JL & Tech., 31, 841.,
34+ https://arxiv.org/abs/1711.00399
3235
3336 Parameters
3437 ----------
@@ -79,42 +82,53 @@ class probability for `y_desired`.
7982 )
8083 else :
8184 use_proba = False
82-
8385 if y_desired_proba is None :
8486 # class label
87+
8588 y_to_be_annealed_to = y_desired
8689 else :
8790 # class proba corresponding to class label y_desired
88- y_to_be_annealed_to = y_desired_proba
8991
90- # start with random counterfactual
92+ y_to_be_annealed_to = y_desired_proba
93+ all_indices = np .arange (x_reference .shape [0 ])
94+ if fixed_features is not None :
95+ varying_indices = np .array ([i for i in all_indices if i not in fixed_features ])
96+ else :
97+ varying_indices = all_indices
9198 rng = np .random .RandomState (random_seed )
92- x_counterfact = X_dataset [rng .randint (X_dataset .shape [0 ])]
99+ initial_x = X_dataset [rng .randint (X_dataset .shape [0 ])].copy ()
100+
101+ if fixed_features is not None :
102+ initial_x [list (fixed_features )] = x_reference [list (fixed_features )]
103+ x0 = initial_x [varying_indices ]
93104
94105 # compute median absolute deviation
106+
95107 mad = np .abs (np .median (X_dataset , axis = 0 ) - x_reference )
96108
97109 def dist (x_reference , x_counterfact ):
98110 numerator = np .abs (x_reference - x_counterfact )
99- return np .sum (numerator / mad )
111+ return np .sum (numerator / (mad + 1e-8 ))
112+
113+ def loss (varying_values , lammbda ):
114+ current_x = x_reference .copy ()
115+ current_x [varying_indices ] = varying_values
100116
101- def loss (x_counterfact , lammbda ):
102117 if use_proba :
103- y_predict = model .predict_proba (x_counterfact .reshape (1 , - 1 )).flatten ()[
118+ y_predict = model .predict_proba (current_x .reshape (1 , - 1 )).flatten ()[
104119 y_desired
105120 ]
106121 else :
107- y_predict = model .predict (x_counterfact .reshape (1 , - 1 ))
108-
122+ y_predict = model .predict (current_x .reshape (1 , - 1 ))
109123 diff = lammbda * (y_predict - y_to_be_annealed_to ) ** 2
110124
111- return diff + dist (x_reference , x_counterfact )
125+ return diff + dist (x_reference , current_x )
112126
113- res = minimize (loss , x_counterfact , args = (lammbda ), method = "Nelder-Mead" )
127+ res = minimize (loss , x0 , args = (lammbda , ), method = method )
114128
115129 if not res ["success" ]:
116130 warnings .warn (res ["message" ])
131+ final_counterfactual = x_reference .copy ()
132+ final_counterfactual [varying_indices ] = res ["x" ]
117133
118- x_counterfact = res ["x" ]
119-
120- return x_counterfact
134+ return final_counterfactual
0 commit comments