File size: 7,590 Bytes
d2a8669
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
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
from typing import Union

from aif360.datasets import BinaryLabelDataset
from aif360.metrics import ClassificationMetric

from aif360.detectors.mdss.ScoringFunctions import Bernoulli, BerkJones, ScoringFunction
from aif360.detectors.mdss.MDSS import MDSS

import pandas as pd
from sklearn.utils.deprecation import deprecated


class MDSSClassificationMetric(ClassificationMetric):
    """Bias subset scanning is proposed as a technique to identify bias in
    predictive models using subset scanning [#zhang16]_.

    This class is a wrapper for the bias scan scoring and scanning methods that
    uses the ClassificationMetric abstraction.

    References:
        .. [#zhang16] `Zhang, Z. and Neill, D. B., "Identifying significant
           predictive bias in classifiers," arXiv preprint, 2016.
           <https://arxiv.org/abs/1611.08292>`_
    """

    def __init__(
        self,
        dataset: BinaryLabelDataset,
        classified_dataset: BinaryLabelDataset,
        scoring: Union[str, ScoringFunction] = 'Bernoulli',
        unprivileged_groups: dict = None,
        privileged_groups: dict = None,
        **kwargs
    ):
        """
        Args:
            dataset (BinaryLabelDataset): Dataset containing ground-truth
                labels.
            classified_dataset (BinaryLabelDataset): Dataset containing
                predictions.
            scoring (str or ScoringFunction): One of 'Bernoulli' (parametric), or 'BerkJones' (non-parametric)
                        or subclass of :class:`aif360.metrics.mdss.ScoringFunctions.ScoringFunction`.
                        Defaults to Bernoulli.
            privileged_groups (list(dict)): Privileged groups. Format is a list
                of `dicts` where the keys are `protected_attribute_names` and
                the values are values in `protected_attributes`. Each `dict`
                element describes a single group. See examples for more details.
            unprivileged_groups (list(dict)): Unprivileged groups in the same
                format as `privileged_groups`.
        """

        super(MDSSClassificationMetric, self).__init__(
            dataset,
            classified_dataset,
            unprivileged_groups=unprivileged_groups,
            privileged_groups=privileged_groups,
        )

        self.scoring = scoring
        self.kwargs = kwargs

    def score_groups(self, privileged=True, penalty=1e-17):
        """Compute the bias score for a prespecified group of records.

        Args:
            privileged (bool): Flag for which direction to scan: privileged
                (``True``) implies negative (observed worse than predicted
                outcomes) while unprivileged (``False``) implies positive
                (observed better than predicted outcomes).

        Returns:
            float: Bias score for the given group.
                The higher the score, the evidence for bias.
        """
        groups = self.privileged_groups if privileged else self.unprivileged_groups
        subset = dict()

        for g in groups:
            for k, v in g.items():
                if k in subset.keys():
                    subset[k].append(v)
                else:
                    subset[k] = [v]

        coordinates = pd.DataFrame(
            self.dataset.features, columns=self.dataset.feature_names
        )
        expected = pd.Series(self.classified_dataset.scores.flatten())
        outcomes = pd.Series(self.dataset.labels.flatten() == self.dataset.favorable_label, dtype=int)

        # In MDSS, we look for subset whose observations systematically deviates from expectations.
        # Positive direction means observations are systematically higher than expectations
        # (or expectations are systematically higher than observations) while
        # Negative direction means observatons are systematically lower than expectations
        # (or expectations are systematically higher than observations)

        # For a privileged group, we are looking for a subset whose expectations
        # (where expectations is obtained from a model) is systematically higher than the observations.
        # This means we scan in the negative direction.

        # For an uprivileged group, we are looking for a subset whose expectations
        # (where expectations is obtained from a model) is systematically lower the observations.
        # This means we scan in the position direction.

        self.kwargs['direction'] = "negative" if privileged else "positive"

        if self.scoring == "Bernoulli":
            scoring_function = Bernoulli(**self.kwargs)
        elif self.scoring == "BerkJones":
            scoring_function = BerkJones(**self.kwargs)
        else:
            scoring_function = self.scoring(**self.kwargs)

        scanner = MDSS(scoring_function)

        return scanner.score_current_subset(
            coordinates, expected, outcomes, dict(subset), penalty
        )

    @deprecated('Change to new interface - aif360.detectors.mdss_detector.bias_scan by version 0.5.0.')
    def bias_scan(self, privileged=True, num_iters=10, penalty=1e-17):
        """
        scan to find the highest scoring subset of records

        :param privileged: flag for group to scan for - privileged group (True) or unprivileged group (False).
        This abstract the need to explicitly specify the direction of bias to scan for which depends on what the favourable label is.
        :param num_iters: number of iterations (random restarts)
        :param penalty: penalty term. Should be positive. The penalty term as with any regularization parameter may need to be
        tuned for ones use case. The higher the penalty, the less complex (number of features and feature values) the highest scoring
        subset that gets returned is.

        :returns: the highest scoring subset and the score
        """

        coordinates = pd.DataFrame(
            self.classified_dataset.features,
            columns=self.classified_dataset.feature_names,
        )

        expected = pd.Series(self.classified_dataset.scores.flatten())
        outcomes = pd.Series(self.dataset.labels.flatten() == self.dataset.favorable_label, dtype=int)

        # In MDSS, we look for subset whose observations systematically deviates from expectations.
        # Positive direction means observations are systematically higher than expectations
        # (or expectations are systematically lower than observations) while
        # Negative direction means observatons are systematically lower than expectations
        # (or expectations are systematically higher than observations)

        # For a privileged group, we are looking for a subset whose expectations
        # (where expectations is obtained from a model) is systematically higher than the observations.
        # This means we scan in the negative direction.

        # For an uprivileged group, we are looking for a subset whose expectations
        # (where expectations is obtained from a model) is systematically lower the observations.
        # This means we scan in the position direction.

        self.kwargs['direction'] = "negative" if privileged else "positive"

        if self.scoring == "Bernoulli":
            scoring_function = Bernoulli(**self.kwargs)
        elif self.scoring == "BerkJones":
            scoring_function = BerkJones(**self.kwargs)
        else:
            scoring_function = self.scoring(**self.kwargs)

        scanner = MDSS(scoring_function)
        return scanner.scan(coordinates, expected, outcomes, penalty, num_iters)