Skip to content

Counterfactual Explainer

interpretation.explainer.agnostic.counterfactual.CounterfactualExplainer

Bases: AgnosticExplainer

Counterfactual explainer using methods proposed by Wachter et al. (2018).

Source code in interpretation/explainer/agnostic/counterfactual/explainer.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
class CounterfactualExplainer(AgnosticExplainer):
    """Counterfactual explainer using methods proposed by Wachter et al. (2018)."""
    def __init__(self, input_model, input_data):
        """Initialise the explainer by taking in the model to explain
        and the observed data to be used for to estimate the MAD and sampling."""
        super().__init__(input_model)
        self.data = input_data
        self.mad = median_abs_deviation(input_data, axis=0)
        self.safe_mad = np.where(self.mad == 0, 1.0, self.mad)
        self.xl = np.min(input_data, axis=0)
        self.xu = np.max(input_data, axis=0)
        self.r = self.xu - self.xl
        self.safe_r = np.where(self.r == 0, 1.0, self.r)


    def explain(
        self,
        X: npt.NDArray,
        desired_y: float,
        method: str = "wachter",
        **method_kwargs
    ) -> npt.NDArray:
        """Computes a counterfactual that produces a model output specified by desired_y.
        Only supports scalar labels.

        Parameters
        ----------
        X : npt.NDArray
            The instance to be explained by the counterfactual, (n_features).
        desired_y : float
            The predefined prediction for the counterfactual explanation to produce.
        method: str
            The method used to compute the counterfactual, either 'wachter' or 'dandl', by default 'wachter'.

            - 'wachter': uses the optimisation method proposed by Wachter et al. (2018).
            - 'dandl': uses the multi-objective + NSGA optimisation method proposed by Dandl et al. (2020).
        **method_kwargs
            Additional keyword arguments passed directly to the selected
            counterfactual generation method. See the documentation of
            :meth:`wachter` and :meth:`dandl` for the available options.

        Returns
        -------
        npt.NDArray
            One Counterfactual instance for wachter and multiple instances for dandl.

        Raises
        ------
        ValueError
            If ``method`` is not one of the supported methods.
        """

        X = validate_input_1d(X)

        if method == "wachter":
            cf = self.wachter(X, desired_y, **method_kwargs)
        elif method == "dandl":
            cf = self.dandl(X, desired_y, **method_kwargs)
        else:
            raise ValueError(f"Unknown method {method}. Expected to be: 'wachter' or 'dandl'")

        return cf

    def wachter(
        self,
        X: npt.NDArray,
        desired_y: float,
        lambda_initial: float = 1e-2,
        lambda_max: float = 1e4,
        lambda_multiplier: float = 10.0,
        tol: float = 1e-3
    ):
        """Computes a counterfactual using method proposed by Wachter et al.

        Parameters
        ----------
        X : npt.NDArray
            The instance to be explained by the counterfactual, (n_features).
        desired_y : float
            The predefined prediction for the counterfactual explanation to produce.
        lambda_initial : float, optional
            Initial lambda, by default 1e-2
        lambda_max : float, optional
            Maximum lambda, by default 1e4
        lambda_multiplier : float, optional
            Multiplier of lambda after each iteration, by default 10
        tol : float, optional
            Tolerance for prediction error, the function increases lambda exponentially if prediction error is higher than tol, by default 1e-3.

        Returns
        -------
        npt.NDArray
            Counterfactual instance.
        """
        prediction_error = np.inf
        lam = lambda_initial
        bounds = list(zip(
            self.xl,
            self.xu
        ))

        sample_idx = np.random.randint(0, len(self.data))
        cf = self.data[sample_idx].copy()

        while lam <= lambda_max and prediction_error > tol:
            cf = spminimize(
                fun=wachter_loss,
                x0=cf,
                method="Nelder-Mead",
                args=(X, desired_y, lam, self.model, self.safe_mad),
                bounds=bounds
            ).x
            prediction_error = np.abs(float(self.model(cf.reshape(1, -1))) - desired_y)
            lam *= lambda_multiplier

        return cf

    def dandl(
        self, 
        X: npt.NDArray, 
        desired_y: float, 
        k: int = 5,
        topn: int = 10
    ):
        """Computes a counterfactual via nondominated sorting genetic algorithm.

        Parameters
        ----------
        X : npt.NDArray
            The instance to be explained by the counterfactual, (n_features).
        desired_y : float
            The predefined prediction for the counterfactual explanation to produce.
        k : int, optional
            Number of samples taken to take average in fourth objective, k > 0, by default 5.
        topn: int, optional
            Top n counterfactual to return, by default 10.

        Returns
        -------
        npt.NDArray
            Counterfactual instances with shape (n_candidates, n_features).
        """
        problem = FourCriteriaLoss(
            X, 
            desired_y, 
            self.data, 
            self.model, 
            self.safe_r, 
            self.xl,
            self.xu,
            k
        )

        algorithm = NSGA2(
            pop_size=100, 
            eliminate_duplicates=True
        )

        result = minimize(
            problem,
            algorithm,
            termination=get_termination("n_gen", 200),
        )

        prediction_errors = np.abs(self.model(result.X) - desired_y)
        top_idx = np.argsort(prediction_errors)
        cfs = result.X[top_idx[:topn]]

        return cfs

__init__(input_model, input_data)

Initialise the explainer by taking in the model to explain and the observed data to be used for to estimate the MAD and sampling.

Source code in interpretation/explainer/agnostic/counterfactual/explainer.py
14
15
16
17
18
19
20
21
22
23
24
def __init__(self, input_model, input_data):
    """Initialise the explainer by taking in the model to explain
    and the observed data to be used for to estimate the MAD and sampling."""
    super().__init__(input_model)
    self.data = input_data
    self.mad = median_abs_deviation(input_data, axis=0)
    self.safe_mad = np.where(self.mad == 0, 1.0, self.mad)
    self.xl = np.min(input_data, axis=0)
    self.xu = np.max(input_data, axis=0)
    self.r = self.xu - self.xl
    self.safe_r = np.where(self.r == 0, 1.0, self.r)

dandl(X, desired_y, k=5, topn=10)

Computes a counterfactual via nondominated sorting genetic algorithm.

Parameters:

Name Type Description Default
X NDArray

The instance to be explained by the counterfactual, (n_features).

required
desired_y float

The predefined prediction for the counterfactual explanation to produce.

required
k int

Number of samples taken to take average in fourth objective, k > 0, by default 5.

5
topn int

Top n counterfactual to return, by default 10.

10

Returns:

Type Description
NDArray

Counterfactual instances with shape (n_candidates, n_features).

Source code in interpretation/explainer/agnostic/counterfactual/explainer.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def dandl(
    self, 
    X: npt.NDArray, 
    desired_y: float, 
    k: int = 5,
    topn: int = 10
):
    """Computes a counterfactual via nondominated sorting genetic algorithm.

    Parameters
    ----------
    X : npt.NDArray
        The instance to be explained by the counterfactual, (n_features).
    desired_y : float
        The predefined prediction for the counterfactual explanation to produce.
    k : int, optional
        Number of samples taken to take average in fourth objective, k > 0, by default 5.
    topn: int, optional
        Top n counterfactual to return, by default 10.

    Returns
    -------
    npt.NDArray
        Counterfactual instances with shape (n_candidates, n_features).
    """
    problem = FourCriteriaLoss(
        X, 
        desired_y, 
        self.data, 
        self.model, 
        self.safe_r, 
        self.xl,
        self.xu,
        k
    )

    algorithm = NSGA2(
        pop_size=100, 
        eliminate_duplicates=True
    )

    result = minimize(
        problem,
        algorithm,
        termination=get_termination("n_gen", 200),
    )

    prediction_errors = np.abs(self.model(result.X) - desired_y)
    top_idx = np.argsort(prediction_errors)
    cfs = result.X[top_idx[:topn]]

    return cfs

explain(X, desired_y, method='wachter', **method_kwargs)

Computes a counterfactual that produces a model output specified by desired_y. Only supports scalar labels.

Parameters:

Name Type Description Default
X NDArray

The instance to be explained by the counterfactual, (n_features).

required
desired_y float

The predefined prediction for the counterfactual explanation to produce.

required
method str

The method used to compute the counterfactual, either 'wachter' or 'dandl', by default 'wachter'.

  • 'wachter': uses the optimisation method proposed by Wachter et al. (2018).
  • 'dandl': uses the multi-objective + NSGA optimisation method proposed by Dandl et al. (2020).
'wachter'
**method_kwargs

Additional keyword arguments passed directly to the selected counterfactual generation method. See the documentation of :meth:wachter and :meth:dandl for the available options.

{}

Returns:

Type Description
NDArray

One Counterfactual instance for wachter and multiple instances for dandl.

Raises:

Type Description
ValueError

If method is not one of the supported methods.

Source code in interpretation/explainer/agnostic/counterfactual/explainer.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def explain(
    self,
    X: npt.NDArray,
    desired_y: float,
    method: str = "wachter",
    **method_kwargs
) -> npt.NDArray:
    """Computes a counterfactual that produces a model output specified by desired_y.
    Only supports scalar labels.

    Parameters
    ----------
    X : npt.NDArray
        The instance to be explained by the counterfactual, (n_features).
    desired_y : float
        The predefined prediction for the counterfactual explanation to produce.
    method: str
        The method used to compute the counterfactual, either 'wachter' or 'dandl', by default 'wachter'.

        - 'wachter': uses the optimisation method proposed by Wachter et al. (2018).
        - 'dandl': uses the multi-objective + NSGA optimisation method proposed by Dandl et al. (2020).
    **method_kwargs
        Additional keyword arguments passed directly to the selected
        counterfactual generation method. See the documentation of
        :meth:`wachter` and :meth:`dandl` for the available options.

    Returns
    -------
    npt.NDArray
        One Counterfactual instance for wachter and multiple instances for dandl.

    Raises
    ------
    ValueError
        If ``method`` is not one of the supported methods.
    """

    X = validate_input_1d(X)

    if method == "wachter":
        cf = self.wachter(X, desired_y, **method_kwargs)
    elif method == "dandl":
        cf = self.dandl(X, desired_y, **method_kwargs)
    else:
        raise ValueError(f"Unknown method {method}. Expected to be: 'wachter' or 'dandl'")

    return cf

wachter(X, desired_y, lambda_initial=0.01, lambda_max=10000.0, lambda_multiplier=10.0, tol=0.001)

Computes a counterfactual using method proposed by Wachter et al.

Parameters:

Name Type Description Default
X NDArray

The instance to be explained by the counterfactual, (n_features).

required
desired_y float

The predefined prediction for the counterfactual explanation to produce.

required
lambda_initial float

Initial lambda, by default 1e-2

0.01
lambda_max float

Maximum lambda, by default 1e4

10000.0
lambda_multiplier float

Multiplier of lambda after each iteration, by default 10

10.0
tol float

Tolerance for prediction error, the function increases lambda exponentially if prediction error is higher than tol, by default 1e-3.

0.001

Returns:

Type Description
NDArray

Counterfactual instance.

Source code in interpretation/explainer/agnostic/counterfactual/explainer.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def wachter(
    self,
    X: npt.NDArray,
    desired_y: float,
    lambda_initial: float = 1e-2,
    lambda_max: float = 1e4,
    lambda_multiplier: float = 10.0,
    tol: float = 1e-3
):
    """Computes a counterfactual using method proposed by Wachter et al.

    Parameters
    ----------
    X : npt.NDArray
        The instance to be explained by the counterfactual, (n_features).
    desired_y : float
        The predefined prediction for the counterfactual explanation to produce.
    lambda_initial : float, optional
        Initial lambda, by default 1e-2
    lambda_max : float, optional
        Maximum lambda, by default 1e4
    lambda_multiplier : float, optional
        Multiplier of lambda after each iteration, by default 10
    tol : float, optional
        Tolerance for prediction error, the function increases lambda exponentially if prediction error is higher than tol, by default 1e-3.

    Returns
    -------
    npt.NDArray
        Counterfactual instance.
    """
    prediction_error = np.inf
    lam = lambda_initial
    bounds = list(zip(
        self.xl,
        self.xu
    ))

    sample_idx = np.random.randint(0, len(self.data))
    cf = self.data[sample_idx].copy()

    while lam <= lambda_max and prediction_error > tol:
        cf = spminimize(
            fun=wachter_loss,
            x0=cf,
            method="Nelder-Mead",
            args=(X, desired_y, lam, self.model, self.safe_mad),
            bounds=bounds
        ).x
        prediction_error = np.abs(float(self.model(cf.reshape(1, -1))) - desired_y)
        lam *= lambda_multiplier

    return cf