From 9080ddb71f6e5dc651aada11b909b267edc8f42b Mon Sep 17 00:00:00 2001 From: Kalousios Date: Wed, 21 Aug 2024 22:19:17 +0100 Subject: [PATCH 1/3] Initial commit of intersectional bias mitigation algorithm Signed-off-by: Kalousios --- aif360/algorithms/__init__.py | 1 + aif360/algorithms/intersectional_fairness.py | 1010 ++++++++++++ .../inprocessing/adversarial_debiasing.py | 121 ++ .../isf_helpers/inprocessing/inprocessing.py | 51 + .../isf_analysis/intersectional_bias.py | 161 ++ .../isf_helpers/isf_analysis/metrics.py | 114 ++ .../isf_metrics/disparate_impact.py | 165 ++ .../isf_helpers/isf_utils/common.py | 442 ++++++ .../algorithms/isf_helpers/isf_utils/const.py | 20 + .../isf_helpers/postprocessing/eq_odds.py | 295 ++++ .../equalized_odds_postprocessing.py | 136 ++ .../postprocessing/postprocessing.py | 52 + .../reject_option_based_classification.py | 92 ++ .../isf_helpers/preprocessing/checks.py | 60 + .../isf_helpers/preprocessing/massaging.py | 80 + .../preprocessing/preprocessing.py | 52 + .../isf_helpers/preprocessing/relabelling.py | 140 ++ docs/source/modules/algorithms.rst | 1 + examples/tutorial_isf.ipynb | 1403 +++++++++++++++++ tests/test_isf.py | 304 ++++ 20 files changed, 4700 insertions(+) create mode 100644 aif360/algorithms/intersectional_fairness.py create mode 100644 aif360/algorithms/isf_helpers/inprocessing/adversarial_debiasing.py create mode 100644 aif360/algorithms/isf_helpers/inprocessing/inprocessing.py create mode 100644 aif360/algorithms/isf_helpers/isf_analysis/intersectional_bias.py create mode 100644 aif360/algorithms/isf_helpers/isf_analysis/metrics.py create mode 100644 aif360/algorithms/isf_helpers/isf_metrics/disparate_impact.py create mode 100644 aif360/algorithms/isf_helpers/isf_utils/common.py create mode 100644 aif360/algorithms/isf_helpers/isf_utils/const.py create mode 100644 aif360/algorithms/isf_helpers/postprocessing/eq_odds.py create mode 100644 aif360/algorithms/isf_helpers/postprocessing/equalized_odds_postprocessing.py create mode 100644 aif360/algorithms/isf_helpers/postprocessing/postprocessing.py create mode 100644 aif360/algorithms/isf_helpers/postprocessing/reject_option_based_classification.py create mode 100644 aif360/algorithms/isf_helpers/preprocessing/checks.py create mode 100644 aif360/algorithms/isf_helpers/preprocessing/massaging.py create mode 100644 aif360/algorithms/isf_helpers/preprocessing/preprocessing.py create mode 100644 aif360/algorithms/isf_helpers/preprocessing/relabelling.py create mode 100644 examples/tutorial_isf.ipynb create mode 100644 tests/test_isf.py diff --git a/aif360/algorithms/__init__.py b/aif360/algorithms/__init__.py index 6b2a9d3f..20cc605f 100644 --- a/aif360/algorithms/__init__.py +++ b/aif360/algorithms/__init__.py @@ -1 +1,2 @@ from aif360.algorithms.transformer import Transformer, addmetadata +from aif360.algorithms.intersectional_fairness import IntersectionalFairness diff --git a/aif360/algorithms/intersectional_fairness.py b/aif360/algorithms/intersectional_fairness.py new file mode 100644 index 00000000..27fb219d --- /dev/null +++ b/aif360/algorithms/intersectional_fairness.py @@ -0,0 +1,1010 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pandas as pd +import math +import collections as cl +import traceback +import concurrent.futures + +from aif360.datasets import StructuredDataset +from aif360.datasets import BinaryLabelDataset +from aif360.metrics import BinaryLabelDatasetMetric +from aif360.metrics import ClassificationMetric + +from aif360.algorithms.isf_helpers.isf_utils import const +from aif360.algorithms.isf_helpers.isf_utils.common import create_multi_group_label +from aif360.algorithms.isf_helpers.preprocessing.preprocessing import PreProcessing +from aif360.algorithms.isf_helpers.inprocessing.inprocessing import InProcessing +from aif360.algorithms.isf_helpers.postprocessing.postprocessing import PostProcessing + +from aif360.algorithms.isf_helpers.preprocessing.massaging import Massaging +from aif360.algorithms.isf_helpers.inprocessing.adversarial_debiasing import AdversarialDebiasing +from aif360.algorithms.isf_helpers.postprocessing.reject_option_based_classification import RejectOptionClassification +from aif360.algorithms.isf_helpers.postprocessing.equalized_odds_postprocessing import EqualizedOddsPostProcessing + +from logging import getLogger, StreamHandler, ERROR, Formatter + +class IntersectionalFairness(): + """ + Mitigate intersectional-bias caused by combining multiple sensitive attributes. Apply bias mitigation techniques to subgroups divided by sensitive attributes, and prioritize those with high mitigation effects in fairness metrics. [1]_ + + References: + .. [1] Kobayashi, K., Nakao, Y. (2022). One-vs.-One Mitigation of Intersectional Bias: A General Method for Extending Fairness-Aware Binary Classification. In: de Paz Santana, J.F., de la Iglesia, D.H., López Rivero, A.J. (eds) New Trends in Disruptive Technologies, Tech Ethics and Artificial Intelligence. DiTTEt 2021. Advances in Intelligent Systems and Computing, vol 1410. Springer, Cham. https://doi.org/10.1007/978-3-030-87687-6_5 + + Parameters + ---------- + algorithm : str + Bias mitigation technique + {'AdversarialDebiasing', 'RejectOptionClassification', 'Massaging', 'EqualizedOddsPostProcessing'} + + metric : str + Fairness metrics + {'DemographicParity', 'EqualOpportunity', 'EqualizedOdds', 'F1Parity'}. Note: currently the algorithm 'RejectOptionClassification' is not compatible with the metric 'F1Parity'. + + accuracy_metric : str + Accuracy metric + {'Balanced Accuracy', 'F1'} + + upper_limit_disparity : float + Inequality target + + debiasing_conditions : list(dictionary) + Conditions for bias mitigation + (Enabled when instruct_debiasing=True) + {'target_attrs': priority condition for bias mitigation, + 'uld_a': lower target value for bias mitigation, + 'uld_b': upper target value of bias mitigation, + 'probability': relabeling rate}. + Example: + [{'target_attrs':{'non_white': 1.0, 'Gender': 0.0}, 'uld_a': 0.8, 'uld_b':1.2, 'probability':1.0}]. + + instruct_debiasing : boolean + By setting it 'True' we can specify a combination of sensitive attributes in 'debiasing_conditions' to mitigate bias. + + upper_limit_disparity_type : str + Fairness metric calculation method + 'difference': difference between privileged and non-privileged attributes + 'ratio': Ratio of privileged and non-privileged attributes + ['difference', 'ratio'] + + instruct_debiasing : boolean + Specify targets for bias mitigation + + max_workers : int, optional + Number of parallelisms for bias mitigation + + options : dictionary, optional + Bias reduction algorithm option. + (Refer to the API of the specified algorithm for details.) + """ + + def __init__(self, algorithm, metric, accuracy_metric='Balanced Accuracy', upper_limit_disparity=0.03, + debiasing_conditions=None, instruct_debiasing=False, + upper_limit_disparity_type='difference', max_workers=4, options={}): + self.algorithm = algorithm + self.options = options + self.options['metric'] = metric + self.options['threshold'] = 0 + algo = globals()[self.algorithm](options) + if isinstance(algo, PreProcessing): + self.approach_type = 'PreProcessing' + elif isinstance(algo, InProcessing): + self.approach_type = 'InProcessing' + elif isinstance(algo, PostProcessing): + self.approach_type = 'PostProcessing' + self.metric = metric + self.instruct_debiasing = instruct_debiasing + + if self.instruct_debiasing is True: + # Convert bias mitigation priority definition to dataframe + self.debiasing_conditions_df = self._convert_to_dataframe_from_debiasing_conditions(debiasing_conditions) + else: + self.upper_limit_disparity = upper_limit_disparity + + self.upper_limit_disparity_type = upper_limit_disparity_type + + self.models = {} + self.graph_sort_label = [] # Graph item name unification list + self.pair_metric_list = [] + self.group_protected_attrs = None + + self.skip_mode = False + self.ds_dir = 'tmp/' # storage directory for intermediate results + + self.dataset_actual = None + self.dataset_valid = None + self.dataset_predicted = None + self.enable_fit = None + self.MAX_WORKERS = max_workers + self.dfst_all = None + self.scores = None # Score of all instances for debugging + + self.accuracy_metric = accuracy_metric + + allowed_accuracy_metrics = ['Balanced Accuracy', 'F1'] + if accuracy_metric not in allowed_accuracy_metrics: + raise ValueError('accuracy metric name not in the list of allowed metrics') + + self.logger = getLogger(__name__) + handler = StreamHandler() + handler.setFormatter(Formatter("%(asctime)s [%(levelname)s] %(message)s")) + self.logger.addHandler(handler) + self.logger.setLevel(ERROR) + + def fit(self, dataset_actual, dataset_predicted=None, dataset_valid=None, options={}): + """ + Learns the fair classifier. + + Parameters + ---------- + dataset_actual : StructuredDataset + Dataset for input to the model. + Enabled when PreProcessing, InProcessing algorithm is selected + dataset_predicted : StructuredDataset + Dataset of model prediction. + Enabled when PostProcessing algorithm is selected + dataset_valid : StructuredDataset + Dataset for validation. + options : dictionary, optional + Bias reduction algorithm option. + Refer to the API of the specified algorithm for details. + """ + + self.logger.debug('fitting...') + + # TODO need to fix sorting sensitive attributes + # thres_sort = sorted(thres.items(), key=lambda x:x[0]) # Fixed order of sensitive attributes + if dataset_valid is None: + if self.approach_type == 'PostProcessing': + dataset_valid = dataset_predicted.copy(deepcopy=True) + else: + dataset_valid = dataset_actual.copy(deepcopy=True) + fav_voting_rate_values = self._mitigate_each_pair(dataset_actual, dataset_valid=dataset_valid, dataset_predicted=dataset_predicted, enable_fit=True, options=options) + + p_attrs = dataset_valid.protected_attribute_names + stat_table = [] + + # Calculate the accuracy and disparity for each subgroup in about 100 ways while changing the score threshold + for uf_t in np.linspace(0.01, 0.99, 99): + ds_tmp = dataset_valid.copy(deepcopy=True) + for g in self.group_protected_attrs: + ds_tmp = self._change_labels_above_threshold(ds_tmp, fav_voting_rate_values, uf_t, protected_attributes=g) + # Calculate accuracy and fairness for each group + stat_table.extend(self._create_stat_table(dataset_actual, dataset_valid, ds_tmp, uf_t, self.metric, p_attrs)) + del ds_tmp + protected_attribute_names = list(g[0].keys()) + dfst = pd.DataFrame(stat_table, columns=protected_attribute_names + ['uf_t', 'P', 'N', 'P^', 'N^', 'TP', 'TN', 'FP', 'FN', 'tpr', 'tnr', 'bl_acc', 'precision', 'f1', 'sel_rate', 'difference', 'ratio']) + self.dfst_all = dfst + + # For each group, select the uf_t with the highest accuracy rate within the range of the disparity upper limit + df_result = pd.DataFrame() + for g in self.group_protected_attrs: + dfst_each_group = pd.DataFrame() + for key, value in g[0].items(): + if len(dfst_each_group) == 0: + dfst_each_group = dfst[(dfst[key] == value)] + else: + dfst_each_group = dfst_each_group[(dfst_each_group[key] == value)] + + # Specify targets for bias mitigation + if self.instruct_debiasing is True: + # Get fairness index value range (upper_limit_disparity_a, upper_limit_disparity_b) + upper_limit_disparity_a, upper_limit_disparity_b = self._get_upper_limit_disparity(g) + self.logger.debug("group_protected_attrs={0} upper_limit_disparity_a={1} upper_limit_disparity_b={2}".format(g[0], upper_limit_disparity_a, upper_limit_disparity_b)) + if upper_limit_disparity_a is None and upper_limit_disparity_b is None: + continue + + # Find a threshold set that satisfies the disparity upper limit (upper_limit_disparity) + # If upper_limit_disparity is not satisfied, gradually increase the upper limit + for i in np.linspace(0, 1, 51): + uld_tmp_a = upper_limit_disparity_a - i + uld_tmp_b = upper_limit_disparity_b + i + + # break if there is data that satisfies the index + dfst_each_group_fair = dfst_each_group[(dfst_each_group[self.upper_limit_disparity_type] >= uld_tmp_a) & (dfst_each_group[self.upper_limit_disparity_type] <= uld_tmp_b)] + if len(dfst_each_group_fair) > 0: + self.logger.debug("Satisfied fairness constraint in the range of uld_tmp_a = {0:.2f}, uld_tmp_b = {1:.2f}".format(uld_tmp_a, uld_tmp_b)) + break + + # skip when dfst_each_group_fair is empty + if len(dfst_each_group_fair) == 0: + self.logger.debug('Not satisfy fairness constraint: ' + str(g)) + continue + + # The bias mitigation evenly. + else: + # Find a threshold set that satisfies the disparity upper limit (upper_limit_disparity) + # If upper_limit_disparity is not satisfied, gradually increase the upper limit + for i in np.linspace(0, 1, 51): + uld_tmp = self.upper_limit_disparity + i + # Set the threshold closest to base_rate within the disparity upper limit + dfst_each_group_fair = dfst_each_group[dfst_each_group[self.upper_limit_disparity_type].abs() <= uld_tmp] + if len(dfst_each_group_fair) > 0: # If it is not within the disparity upper limit, extract the threshold that satisfies the most fairness => widen the disparity + self.logger.debug('Satisfied fairness constraint in the range of uld(upper limit disparity) = ' + str(uld_tmp)) + break + if uld_tmp > 1: + break + + # Find the threshold set that satisfies the disparity upper bound and has the highest accuracy + dfst_each_group = dfst_each_group_fair + if self.accuracy_metric == 'Balanced Accuracy': + dfst_each_group = dfst_each_group[dfst_each_group['bl_acc'] == dfst_each_group['bl_acc'].max()] + elif self.accuracy_metric == 'F1': + dfst_each_group = dfst_each_group[dfst_each_group['f1'] == dfst_each_group['f1'].max()] + dfst_each_group = dfst_each_group[dfst_each_group[self.upper_limit_disparity_type].abs() == dfst_each_group[self.upper_limit_disparity_type].abs().min()] + dfst_each_group = dfst_each_group[dfst_each_group['uf_t'] == dfst_each_group['uf_t'].max()] + if len(dfst_each_group) == 0: + self.logger.info('Not satisfy fairness constraint: ' + str(g)) + exit(1) + if len(df_result) == 0: + df_result = dfst_each_group + else: + df_result = pd.concat([df_result, dfst_each_group]) + + # df_result.to_csv(self.TO.out_dir + '/valid_result_stat.csv') + self.logger.debug('done.') + self.df_result = df_result + + def _worker(self, ids): + self.logger.debug('running isf worker for each subgroup pair:' + str(ids)) + + group1_idx = ids[0] + group2_idx = ids[1] + # Determine privileged/non-privileged group (necessary for some algorithms) + # (used demographic parity) + # print('start: ' + str(group1_idx) + str(group2_idx)) + cl_metric = BinaryLabelDatasetMetric(self.dataset_actual, + unprivileged_groups=self.group_protected_attrs[group2_idx], + privileged_groups=self.group_protected_attrs[group1_idx]) + g1 = cl_metric.base_rate(privileged=True) + g2 = cl_metric.base_rate(privileged=False) + privileged_protected_attributes = None + unprivileged_protected_attributes = None + if g1 > g2: + privileged_protected_attributes = self.group_protected_attrs[group1_idx] + unprivileged_protected_attributes = self.group_protected_attrs[group2_idx] + else: + privileged_protected_attributes = self.group_protected_attrs[group2_idx] + unprivileged_protected_attributes = self.group_protected_attrs[group1_idx] + + pname = self._get_group_name(self.dataset_actual, privileged_protected_attributes) + uname = self._get_group_name(self.dataset_actual, unprivileged_protected_attributes) + pair_name = pname + '_' + uname + + # Get dataset with extracted privileged and non-privileged groups + ds_act_pair, _, _ = self._select_protected_attributes(self.dataset_actual, + unprivileged_protected_attributes, + privileged_protected_attributes) + ds_valid_pair, _, _ = self._select_protected_attributes(self.dataset_valid, + unprivileged_protected_attributes, + privileged_protected_attributes) + + pair_key = (group1_idx, group2_idx) + + if self.enable_fit is True: + self.options['metric'] = self.metric + self.models[pair_key] = globals()[self.algorithm](options=self.options) + if isinstance(self.models[pair_key], PostProcessing): + ds_predicted_pair, _, _ = self._select_protected_attributes(self.dataset_predicted, + unprivileged_protected_attributes, + privileged_protected_attributes) + self.models[pair_key].fit(ds_act_pair, ds_predicted_pair) + else: + self.models[pair_key].fit(ds_act_pair) + if isinstance(self.models[pair_key], PreProcessing): + ds_mitig_valid_pair = self.models[pair_key].transform(ds_valid_pair) + else: + ds_mitig_valid_pair = self.models[pair_key].predict(ds_valid_pair) + + self._print_pair_metric(ds_act_pair, ds_valid_pair, ds_mitig_valid_pair, self.metric, pair_name, pname, uname) + + # Returns a single key of protected attributes + ds_train_protected_attributes = self._split_integration_key(ds_mitig_valid_pair, + self.dataset_valid, + unprivileged_protected_attributes, + privileged_protected_attributes) + + return ds_train_protected_attributes, self.models[pair_key], pair_key + + def _mitigate_each_pair(self, dataset_actual, enable_fit=False, dataset_predicted=None, dataset_valid=None, options={}): + + self.logger.debug('_mitigate_each_pair()') + + self.dataset_actual = dataset_actual.copy(deepcopy=True) + self.enable_fit = enable_fit + self.options = options + + if dataset_valid is None: + dataset_valid = dataset_actual.copy(deepcopy=True) + self.dataset_valid = dataset_actual.copy(deepcopy=True) + else: + self.dataset_valid = dataset_valid.copy(deepcopy=True) + + if dataset_predicted is not None: + self.dataset_predicted = dataset_predicted.copy(deepcopy=True) + if self.group_protected_attrs is None: + self.group_protected_attrs, _ = create_multi_group_label(dataset_valid) + # mitigate bias in all patterns that combine protective attributes + # (Generate pairs from multiple groups) + ds_pair_transf_list = [] + + id_touples = [] + for group1_idx in range(len(self.group_protected_attrs)): + for group2_idx in range(group1_idx + 1, len(self.group_protected_attrs)): + id_touples.append((group1_idx, group2_idx, enable_fit)) + + with concurrent.futures.ProcessPoolExecutor(max_workers=self.MAX_WORKERS) as excuter: + mitigation_results = list(excuter.map(self._worker, id_touples)) + for r in mitigation_results: + ds_pair_transf_list.append(r[0]) + self.models[r[2]] = r[1] + + fav_label = dataset_valid.favorable_label + + # Tally votes + instance_vote_dict = {} # key:instance_name, value:[pair_labels] e.g. [0 1 1] + instance_conf_dict = {} # key:instance_name, value:[pair_confs] e.g. [0.2 0.8 0.7] + scores_enable = None + for ds_pair in ds_pair_transf_list: + # Check if the score is valid. True if not all 0/1 + # Check only one pair, label is the same + if scores_enable is None: + if sum(ds_pair.scores) == sum(ds_pair.labels) or sum(ds_pair.scores) == 0 or sum(ds_pair.scores) == len(ds_pair.scores): + scores_enable = False + else: + scores_enable = True + for i, n in enumerate(ds_pair.instance_names): + if n in instance_vote_dict: + instance_vote_dict[n].append(ds_pair.labels[i].tolist()[0]) + instance_conf_dict[n].append(ds_pair.scores[i].tolist()[0]) + else: + instance_vote_dict[n] = [ds_pair.labels[i].tolist()[0]] + instance_conf_dict[n] = [ds_pair.scores[i].tolist()[0]] + + # Stores fav confidence (voting rate) for each instance + fav_voting_rate_dict = {} # key:instance_name, value:fav_confidence + score_type = 3 # 1:only score, 2:avg(vote*score), 3:avg(vote)*w + avg(score)*(1-w) + score_list = [] + for i, n in enumerate(dataset_valid.instance_names): + c_vote = cl.Counter(instance_vote_dict[n]) + voting_rate = c_vote[fav_label] / len(instance_vote_dict[n]) # Not necessarily label={0,1} + confidence = 1 + if scores_enable is True: + if score_type == 1: + confidence = sum(instance_conf_dict[n]) / len(instance_conf_dict[n]) + elif score_type == 2: + confidence = np.array(instance_vote_dict[n]) * np.array(instance_conf_dict[n]) + confidence = sum(confidence) / len(instance_vote_dict[n]) + elif score_type == 3: + voting_w = 0.75 + length = len(instance_vote_dict[n]) + voting_rate = sum(instance_vote_dict[n]) / length + voting_score = voting_rate * voting_w + prediction_score = (sum(instance_conf_dict[n]) / length) * (1 - voting_w) + confidence = voting_score + prediction_score + + score_list.append([voting_score, prediction_score, confidence]) + + fav_voting_rate_dict[n] = confidence + + fav_voting_rate_values = np.array(list(fav_voting_rate_dict.values())).reshape(-1, 1) + self.scores = pd.DataFrame(score_list, columns=['voting_rate', 'confidence', 'score']) + + return fav_voting_rate_values + + def transform(self, dataset): + """ + Return a new dataset generated by running this transformer on a input dataset. + + Parameters + ---------- + dataset : StructuredDataset + Input dataset + + Returns + ---------- + dataset_pred : StructuredDataset + Predicted dataset + """ + + self.logger.debug('transforming...') + + # Rewrite the corrected dataset using the threshold + dataset_cp = dataset.copy(deepcopy=True) + + fav_voting_rate_values = self._mitigate_each_pair(dataset) + + for g in self.group_protected_attrs: + param = self.df_result + for key, value in g[0].items(): + param = param[(param[key] == value)] + + # If there is no fit() result threshold, go to the next g + if len(param['uf_t']) == 0: + continue + + uf_t = param['uf_t'].values[0] + self.logger.debug('apply score threshold: ' + str(uf_t) + 'subgroup ' + str(g)) + dataset_cp = self._change_labels_above_threshold(dataset_cp, fav_voting_rate_values, uf_t, protected_attributes=g) + + self.logger.debug('done.') + return dataset_cp + + def predict(self, dataset): + """ + Obtain the prediction for the provided dataset using the learned classifier model. + + Parameters + ---------- + dataset : StructuredDataset + Dataset + + Returns + ---------- + dataset_pred : StructuredDataset + Predicted dataset + """ + dataset_cp = self.transform(dataset) + return dataset_cp + + def _change_labels_above_threshold(self, ds_target, fav_voting_rate_values, uf_t, protected_attributes=None): + + protected_attribute_values = list(protected_attributes[0].values()) + + try: + pa00 = np.all(ds_target.protected_attributes == protected_attribute_values, axis=1) + more_uf_t = fav_voting_rate_values > uf_t + lower_uf_t = fav_voting_rate_values <= uf_t + fav_instances_idx = np.logical_and(pa00, more_uf_t.ravel()) + ufav_instances_idx = np.logical_and(pa00, lower_uf_t.ravel()) + ds_target.labels[fav_instances_idx] = ds_target.favorable_label + ds_target.labels[ufav_instances_idx] = ds_target.unfavorable_label + except ValueError: + exit() + + return ds_target + + def _create_stat_table(self, dataset_act, dataset_target, dataset_target_tmp, uf_t, metric, p_attrs): + # dataset_target_tmp Dataset for tentative threshold determination + stat_table = [] + + # Calculate accuracy and fairness for each group + for g in self.group_protected_attrs: + m_oa = ClassificationMetric(dataset_act, dataset_target, privileged_groups=g) # TPR and FPR cannot be calculated before threshold determination because there is no overall post-mitigation data set. + m_sg_mitig = ClassificationMetric(dataset_act, dataset_target_tmp, privileged_groups=g) + difference = None + ratio = None + if metric in const.DEMOGRAPHIC_PARITY: + difference = m_sg_mitig.selection_rate(privileged=True) - m_oa.selection_rate() + + if self.instruct_debiasing is True: + # When setting the priority of bias mitigation, return the fairness index value instead of "disparity" + ratio = m_sg_mitig.selection_rate(privileged=True) / m_oa.selection_rate() + else: + if m_oa.selection_rate() == 0 or m_sg_mitig.selection_rate(privileged=True) == 0: + ratio = 1 + else: + ratio = 1 - min(m_oa.selection_rate() / m_sg_mitig.selection_rate(privileged=True), + m_sg_mitig.selection_rate(privileged=True) / m_oa.selection_rate()) + + elif metric in const.EQUAL_OPPORTUNITY: + difference = m_sg_mitig.true_positive_rate(privileged=True) - m_oa.true_positive_rate() + if m_oa.true_positive_rate() == 0 or m_sg_mitig.true_positive_rate(privileged=True) == 0: + ratio = 1 + else: + ratio = 1 - min(m_oa.true_positive_rate() / m_sg_mitig.true_positive_rate(privileged=True), + m_sg_mitig.true_positive_rate(privileged=True) / m_oa.true_positive_rate()) + elif metric in const.EQUALIZED_ODDS: + m_cl_TPR = m_oa.true_positive_rate() + m_cl_FPR = m_oa.false_positive_rate() + m_TPR = m_sg_mitig.true_positive_rate(privileged=True) + m_FPR = m_sg_mitig.false_positive_rate(privileged=True) + difference = 0.5 * ((m_cl_TPR + m_cl_FPR) - (m_TPR + m_FPR)) + if (m_cl_TPR + m_cl_FPR) == 0 or (m_TPR + m_FPR) == 0: + ratio = 1 + else: + ratio = 1 - min((m_cl_TPR + m_cl_FPR) / (m_TPR + m_FPR), (m_TPR + m_FPR) / (m_cl_TPR + m_cl_FPR)) + elif metric in const.F1_PARITY: + m_cl_precision = m_oa.precision() + m_cl_recall = m_oa.recall() + m_cl_f1 = 2 * m_cl_precision * m_cl_recall / (m_cl_precision + m_cl_recall) + m_precision = m_sg_mitig.precision(privileged=True) + m_recall = m_sg_mitig.recall(privileged=True) + m_f1 = 2 * m_precision * m_recall / (m_precision + m_recall) + difference = m_f1 - m_cl_f1 + ratio = 1 - min(m_cl_f1 / m_f1, m_f1 / m_cl_f1) + protected_attribute_values = list(g[0].values()) + + TPR = -1 if math.isnan(m_sg_mitig.true_positive_rate(privileged=True)) else m_sg_mitig.true_positive_rate(privileged=True) + TNR = -1 if math.isnan(m_sg_mitig.true_negative_rate(privileged=True)) else m_sg_mitig.true_negative_rate(privileged=True) + bal_acc = -1 if TPR == -1 or TNR == -1 else (TPR + TNR) * 0.5 + precision = -1 if math.isnan(m_sg_mitig.precision(privileged=True)) else m_sg_mitig.precision(privileged=True) + # TODO Warning if recall precision=0 + f1 = -1 if precision == -1 or TPR == -1 else 2 * TPR * precision / (TPR + precision) + + metrics = [uf_t, + m_sg_mitig.num_positives(privileged=True), + m_sg_mitig.num_negatives(privileged=True), + m_sg_mitig.num_pred_positives(privileged=True), + m_sg_mitig.num_pred_negatives(privileged=True), + m_sg_mitig.num_true_positives(privileged=True), + m_sg_mitig.num_true_negatives(privileged=True), + m_sg_mitig.num_false_positives(privileged=True), + m_sg_mitig.num_false_negatives(privileged=True), + TPR, + TNR, + bal_acc, + precision, + f1, + m_sg_mitig.selection_rate(privileged=True), + difference, + ratio] # TODO Combine with selection_rate after classification + stat_table.append(protected_attribute_values + metrics) + return stat_table + + def _print_pair_metric(self, ds_act_pair, ds_target_pair, ds_mitig_target_pair, metric, pair_name, pname, uname): + r = [] + for i, n in enumerate(ds_target_pair.instance_names): + if n == ds_mitig_target_pair.instance_names[i]: + pa1 = ds_mitig_target_pair.protected_attributes[i][0] + r.append([n, pa1, ds_target_pair.scores[i], ds_target_pair.labels[i], ds_mitig_target_pair.labels[i]]) + else: + self.logger.error('Not match name.') + exit(1) + + target_metric = ClassificationMetric(ds_act_pair, ds_target_pair, + unprivileged_groups=[{'ikey': 0}], + privileged_groups=[{'ikey': 1}]) + try: + ds_act_pair_cp = ds_act_pair.copy(deepcopy=True) + ds_act_pair_cp.labels = ds_mitig_target_pair.labels + mitig_metric = ClassificationMetric(ds_act_pair, ds_act_pair_cp, + unprivileged_groups=[{'ikey': 0}], + privileged_groups=[{'ikey': 1}]) + mlist = [pname, uname, + target_metric.selection_rate(privileged=True), + target_metric.selection_rate(privileged=False), + mitig_metric.selection_rate(privileged=True), + mitig_metric.selection_rate(privileged=False), + target_metric.true_positive_rate(privileged=True), + target_metric.true_positive_rate(privileged=False), + mitig_metric.true_positive_rate(privileged=True), + mitig_metric.true_positive_rate(privileged=False), + 0.5 * (target_metric.true_positive_rate(privileged=True) + target_metric.false_positive_rate(privileged=True)), + 0.5 * (target_metric.true_positive_rate(privileged=False) + target_metric.false_positive_rate(privileged=False)), + 0.5 * (mitig_metric.true_positive_rate(privileged=True) + mitig_metric.false_positive_rate(privileged=True)), + 0.5 * (mitig_metric.true_positive_rate(privileged=False) + mitig_metric.false_positive_rate(privileged=False)), + 0.5 * (target_metric.true_positive_rate(privileged=True) + target_metric.true_negative_rate(privileged=True)), + 0.5 * (target_metric.true_positive_rate(privileged=False) + target_metric.true_negative_rate(privileged=True)), + 0.5 * (mitig_metric.true_positive_rate(privileged=True) + mitig_metric.true_negative_rate(privileged=False)), + 0.5 * (mitig_metric.true_positive_rate(privileged=False) + mitig_metric.true_negative_rate(privileged=False))] + self.pair_metric_list.append(mlist) + except Exception: + v_act = vars(ds_act_pair) + with open(self.TO.out_dir + '/ds_act_pair.txt', 'w') as f: + self.logger.error(v_act, file=f) + v_mitig_target = vars(ds_mitig_target_pair) + with open(self.TO.out_dir + '/ds_mitig_target_pair.txt', 'w') as f: + self.logger.error(v_mitig_target, file=f) + self.logger.error(traceback.format_exc()) + + def _get_attribute_vals(self, dataset, attributes=[]): + """ + Return the sensitive attribute label + + Parameters + ---------- + dataset : StructuredDataset + Dataset + attributes : list, optional + Sensitive attribute + Returns + ---------- + attributes_vals : tuple + Label of the sensitive attribute + """ + if dataset is None: + raise ValueError("Input DataSet in NoneType.") + + if not isinstance(dataset, StructuredDataset): + raise ValueError("Input DataSet not StructuredDataset.") + + attributes_vals = [] + for index, key1 in enumerate(dataset.protected_attribute_names): + for item in attributes: + for key2 in item.keys(): + if key1 == key2: + attributes_vals.append(float(item[key2])) + break + return tuple(attributes_vals) + + def _get_attribute_keys(self, dataset, attributes=[]): + """ + Return the key of sensitive attribute + + Parameters + ---------- + dataset : StructuredDataset + Dataset containing sensitive attribute + attributes : list, optional + sensitive attribute + Returns + ---------- + attributes_keys: tuple + key of the sensitive attribute + """ + + if dataset is None: + raise ValueError("Input DataSet in NoneType.") + + if not isinstance(dataset, StructuredDataset): + raise ValueError("Input DataSet not StructuredDataset.") + + attributes_keys = [] + for index, key1 in enumerate(dataset.protected_attribute_names): + for item in attributes: + for key2 in item.keys(): + if key1 == key2: + attributes_keys.append(np.array([float(item[key2])])) + break + return attributes_keys + + def _split_group(self, dataset, unprivileged_protected_attributes=[], privileged_protected_attributes=[]): + """ + Extract only privileged/non-privileged groups and convert to dataset + + Parameters + ---------- + privileged_protected_attributes : list + Privileged group + unprivileged_protected_attributes : list + Non-privileged group + + Returns + ---------- + enable_ds : StructuredDataset + Dataset extracting only privileged/non-privileged groups + disable_ds : StructuredDataset + Dataset other than privileged/non-privileged groups + """ + + if dataset is None: + raise ValueError("Input DataSet in NoneType.") + + if not isinstance(dataset, StructuredDataset): + raise ValueError("Input DataSet not StructuredDataset.") + + enable_ds = None + + # Existence check for attributes + for index, item in enumerate(unprivileged_protected_attributes): + for key in item.keys(): + if key not in dataset.protected_attribute_names: + raise ValueError( + "unprivileged_protected_attributes not in protected_attribute_names.") + for index, item in enumerate(privileged_protected_attributes): + for key in item.keys(): + if key not in dataset.protected_attribute_names: + raise ValueError( + "privileged_protected_attributes not in protected_attribute_names.") + + # Convert from dataset to dataframe (Pandas) + df, attributes = dataset.convert_to_dataframe() + + unprivileged_protected_attributes_vals = self._get_attribute_vals(dataset, unprivileged_protected_attributes) + privileged_protected_attributes_vals = self._get_attribute_vals(dataset, privileged_protected_attributes) + + # Combine privileged and non-privileged groups + enable_df = None + disable_df = None + for c1, sdf in df.groupby(dataset.protected_attribute_names): + if (unprivileged_protected_attributes_vals == c1 or privileged_protected_attributes_vals == c1): + if enable_df is None: + enable_df = sdf + else: + enable_df = pd.concat([enable_df, sdf]) + else: + if disable_df is None: + disable_df = sdf + else: + disable_df = pd.concat([disable_df, sdf]) + + unprivileged_protected_attributes_keys = self._get_attribute_keys(dataset, unprivileged_protected_attributes) + privileged_protected_attributes_keys = self._get_attribute_keys(dataset, privileged_protected_attributes) + + # Convert privileged and non-privileged group dataframes (Pandas) to datasets + enable_ds = BinaryLabelDataset( + df=enable_df, + label_names=dataset.label_names, + protected_attribute_names=dataset.protected_attribute_names, +# privileged_protected_attributes=privileged_protected_attributes_keys, +# unprivileged_protected_attributes=unprivileged_protected_attributes_keys, + favorable_label=dataset.favorable_label, + unfavorable_label=dataset.unfavorable_label) + + # Search indexing for performance improvement + sortlist = {} + for i1 in range(len(enable_ds.instance_names)): + sortlist[enable_ds.instance_names[i1]] = i1 + + for i1 in range(len(dataset.instance_names)): + idx = sortlist.get(dataset.instance_names[i1]) + if idx is not None: + enable_ds.labels[idx] = dataset.labels[i1] + enable_ds.scores[idx] = dataset.scores[i1] + enable_ds.instance_weights[idx] = dataset.instance_weights[i1] + + # Store fairness results as dataframe + disable_df['labels'] = 0. + disable_df['scores'] = 0. + disable_df['instance_weights'] = 0. + + # Search indexing for performance improvement + sortlist = {} + for i1 in range(len(disable_df.index)): + sortlist[disable_df.index[i1]] = i1 + + # Restore fairness results to dataset + for i1 in range(len(dataset.instance_names)): + idx = sortlist.get(dataset.instance_names[i1]) + if idx is not None: + #disable_df['labels'][idx] = dataset.labels[i1] + #disable_df['scores'][idx] = dataset.scores[i1] + #disable_df['instance_weights'][idx] = dataset.instance_weights[i1] + disable_df.loc[idx,'labels'] = dataset.labels[i1] + disable_df.loc[idx,'scores'] = dataset.scores[i1] + disable_df.loc[idx,'instance_weights'] = dataset.instance_weights[i1] + + return enable_ds, disable_df + + def _create_integration_key(self, dataset, unprivileged_protected_attributes=[], privileged_protected_attributes=[]): + """ + Returns a dataset that converts privileged/non-privileged groups into a single attribute + + Parameters + ---------- + dataset : StructuredDataset + Dataset + privileged_protected_attributes : list + Privileged group + unprivileged_protected_attributes : list + Non-privileged group + + Returns + ---------- + new_ds : StructuredDataset + Dataset merging privileged/non-privileged groups as a single attribute + """ + + if dataset is None: + raise ValueError("Input DataSet in NoneType.") + + if not isinstance(dataset, StructuredDataset): + raise ValueError("Input DataSet not StructuredDataset.") + + new_ds = None + + # Convert from dataset to dataframe (Pandas) + df, attributes = dataset.convert_to_dataframe() + + # Combine privileged and non-privileged groups + # Create integration key + new_df = None + protected_attribute_maps_dic = {} + ikey2 = 0. + for c1, sdf in df.groupby(dataset.protected_attribute_names): + ikey = 0. + attributes = [] + dicw = {} + for i in range(len(dataset.protected_attribute_names)): + dicw[dataset.protected_attribute_names[i]] = c1[i] + + attributes.append(dicw) + if unprivileged_protected_attributes == attributes: + ikey = 0. + elif privileged_protected_attributes == attributes: + ikey = 1. + sdf_new = sdf.assign(ikey=ikey) + if new_df is None: + new_df = sdf_new + else: + new_df = pd.concat([new_df, sdf_new]) + # protected_attribute_maps_dic[ikey2] = itemname + ikey2 += 1. + + # Create a new sensitive attribute + protected_attribute_names_new = ['ikey'] + + # Remove old sensitive attribute + for name in dataset.protected_attribute_names: + new_df = new_df.drop(columns=name) + + # Create new privileged and non-privileged groups + unprivileged_protected_attributes_new = [np.array([0.])] + privileged_protected_attributes_new = [np.array([1.])] + + # Convert data frame (Pandas) converted to composite key to dataset + new_ds = BinaryLabelDataset( + df=new_df, + label_names=dataset.label_names, + protected_attribute_names=protected_attribute_names_new, + privileged_protected_attributes=privileged_protected_attributes_new, + unprivileged_protected_attributes=unprivileged_protected_attributes_new, + favorable_label=dataset.favorable_label, + unfavorable_label=dataset.unfavorable_label) + + # Search indexing for performance improvement + sortlist = {} + for i1 in range(len(new_ds.instance_names)): + sortlist[new_ds.instance_names[i1]] = i1 + + # restore fairness results to dataset + for i1 in range(len(dataset.instance_names)): + idx = sortlist.get(dataset.instance_names[i1]) + if idx is not None: + new_ds.labels[idx] = dataset.labels[i1] + new_ds.scores[idx] = dataset.scores[i1] + new_ds.instance_weights[idx] = dataset.instance_weights[i1] + + return new_ds, protected_attribute_maps_dic + + def _split_integration_key(self, ds_mitig_target_pair, ds_target, unprivileged_protected_attributes, privileged_protected_attributes): + """ + Restore the conversion dataset that summarizes the attributes to the original configuration + + Parameters + ---------- + dataset : StructuredDataset + Conversion dataset + dataset_base : StructuredDataset + Pre-conversion dataset + privileged_protected_attributes : list + Privileged group + unprivileged_protected_attributes : list + Non-privileged group + + Returns + ---------- + new_ds : StructuredDataset + Dataset converted back to multi-hierarchical key + """ + + if ds_mitig_target_pair is None: + raise ValueError("Input DataSet in NoneType.") + + if not isinstance(ds_mitig_target_pair, StructuredDataset): + raise ValueError("Input DataSet not StructuredDataset.") + + if 'ikey' not in ds_mitig_target_pair.feature_names: + raise ValueError("feature_names not in integration key.") + if 'ikey' not in ds_mitig_target_pair.protected_attribute_names: + raise ValueError("protected_attribute_names not integration key.") + + new_ds = None + + # Convert from dataset to dataframe (Pandas) + new_df, attributes = ds_mitig_target_pair.convert_to_dataframe() + + # Restore old protection attributes + for name in ds_target.protected_attribute_names: + new_df.loc[new_df['ikey'] == 0, name] = unprivileged_protected_attributes[0][name] + new_df.loc[new_df['ikey'] == 1, name] = privileged_protected_attributes[0][name] + + # Delete integration key + new_df = new_df.drop(columns='ikey') + + # Convert dataframe (Pandas) to dataset + new_ds = BinaryLabelDataset( + df=new_df, + label_names=ds_mitig_target_pair.label_names, + protected_attribute_names=ds_target.protected_attribute_names, + privileged_protected_attributes=ds_target.privileged_protected_attributes, + unprivileged_protected_attributes=ds_target.unprivileged_protected_attributes, + favorable_label=ds_mitig_target_pair.favorable_label, + unfavorable_label=ds_mitig_target_pair.unfavorable_label, + metadata=ds_target.metadata) + + # Search indexing for performance improvement + sortlist = {} + for i1 in range(len(new_ds.instance_names)): + sortlist[new_ds.instance_names[i1]] = i1 + + # Restore fairness results to dataset + for i1 in range(len(ds_mitig_target_pair.instance_names)): + idx = sortlist.get(ds_mitig_target_pair.instance_names[i1]) + if idx is not None: + new_ds.labels[idx] = ds_mitig_target_pair.labels[i1] + new_ds.scores[idx] = ds_mitig_target_pair.scores[i1] + new_ds.instance_weights[idx] = ds_mitig_target_pair.instance_weights[i1] + + return new_ds + + def _select_protected_attributes(self, dataset, unpriv_protected_attrs, priv_protected_attrs): + """ + Select 2 groups of privileged/non-privileged and return only the target label data combined + """ + + if not isinstance(dataset, StructuredDataset): + raise ValueError("Input DataSet not StructuredDataset.") + + # Extract datasets for privileged and non-privileged groups + ds1, dfw = self._split_group(dataset, unpriv_protected_attrs, priv_protected_attrs) + + # Convert composite keys for privileged and non-privileged groups to single keys + ds2, protected_attribute_maps_dic = self._create_integration_key(ds1, unpriv_protected_attrs, priv_protected_attrs) + + return ds2, dfw, protected_attribute_maps_dic + + def _get_group_name(self, dataset, groups): + """ + Get the hierarchy combination label + + Parameters + ---------- + dataset : StructuredDataset + Dataset + groups : dictionary + Group + + Returns + ---------- + name : str + Group name + e.g. (sex:1.0, age:1.0, month:6.0) + """ + name = '(' + for key, value in groups[0].items(): + name += key + ':' + str(value) + ', ' + name = name[:-2] + ')' + + return name + + def _convert_to_dataframe_from_debiasing_conditions(self, upper_limit_disparity_list): + ''' + Convert specified conditions for bias mitigation to dataframe + ''' + result_df = None + for uld_dict in upper_limit_disparity_list: + # Convert uld_dict to 1D dict + tmp_dic = {} + for k, v in uld_dict.items(): + if k == 'target_attrs': + tmp_dic.update(v) + else: + tmp_dic[k] = v + + # Dataframe conversion + if result_df is None: + result_df = pd.DataFrame(columns=list(tmp_dic.keys())) + uld_df = pd.DataFrame(tmp_dic, index=[0]) + result_df = pd.concat([result_df, uld_df], ignore_index=True) + return result_df + + def _get_upper_limit_disparity(self, g): + g_dict = g[0] + query_element_list = [] + for g_key, g_value in g_dict.items(): + query_element_list.append("(`{0}` == {1})".format(g_key, g_value)) + query_str = ' & '.join(query_element_list) + df = self.debiasing_conditions_df.query(query_str) + if len(df) == 0: + return None, None + else: + uld_a = df.iloc[0]['uld_a'] * df.iloc[0]['probability'] + uld_b = df.iloc[0]['uld_b'] / df.iloc[0]['probability'] + return uld_a, uld_b diff --git a/aif360/algorithms/isf_helpers/inprocessing/adversarial_debiasing.py b/aif360/algorithms/isf_helpers/inprocessing/adversarial_debiasing.py new file mode 100644 index 00000000..d24d9299 --- /dev/null +++ b/aif360/algorithms/isf_helpers/inprocessing/adversarial_debiasing.py @@ -0,0 +1,121 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from aif360.algorithms.inprocessing.adversarial_debiasing import AdversarialDebiasing as AD +import tensorflow as tf + +from aif360.algorithms.isf_helpers.inprocessing.inprocessing import InProcessing + + +tf.compat.v1.disable_eager_execution() + + +class AdversarialDebiasing(InProcessing): + + """ + Debiasing intersectional bias with adversarial learning(AD) called by ISF. + + Parameters + ---------- + options : dictionary + parameter of AdversarialDebiasing + num_epochs: trials of model training + batch_size:Batch size for model training + + Notes + ----- + https://aif360.readthedocs.io/en/v0.2.3/_modules/aif360/algorithms/inprocessing/adversarial_debiasing.html + + """ + + def __init__(self, options): + super().__init__() + self.ds_train = None + self.options = options + + def fit(self, ds_train): + """ + Save training dataset + + Attributes + ---------- + ds_train : Dataset + Dataset for training + """ + self.ds_train = ds_train.copy(deepcopy=True) + + def predict(self, ds_test): + """ + Model learning with debias using the training dataset imported by fit(), and predict using that model + + Parameters + ---------- + ds_test : Dataset + Dataset for prediction + + Returns + ------- + ds_predict : numpy.ndarray + Predicted label + """ + ikey = ds_test.protected_attribute_names[0] + priv_g = [{ikey: ds_test.privileged_protected_attributes[0]}] + upriv_g = [{ikey: ds_test.unprivileged_protected_attributes[0]}] + sess = tf.compat.v1.Session() + model = AD( + privileged_groups=priv_g, + unprivileged_groups=upriv_g, + scope_name='debiased_classifier', + debias=True, + sess=sess) + model.fit(self.ds_train) + ds_predict = model.predict(ds_test) + sess.close() + tf.compat.v1.reset_default_graph() + return ds_predict + + def bias_predict(self, ds_train): + """ + Model learning and prediction using AdversarialDebiasing of AIF360 without debias. + + Parameters + ---------- + ds_train : Dataset + Dataset for training and prediction + + Returns + ------- + ds_predict : numpy.ndarray + Predicted label + """ + ikey = ds_train.protected_attribute_names[0] + priv_g = [{ikey: ds_train.privileged_protected_attributes[0]}] + upriv_g = [{ikey: ds_train.unprivileged_protected_attributes[0]}] + sess = tf.compat.v1.Session() + model = AD( + privileged_groups=priv_g, + unprivileged_groups=upriv_g, + scope_name='plain_classifier', + debias=False, + sess=sess, + num_epochs=self.options['num_epochs'], + batch_size=self.options['batch_size']) + model.fit(ds_train) + ds_predict = model.predict(ds_train) + sess.close() + tf.compat.v1.reset_default_graph() + return ds_predict diff --git a/aif360/algorithms/isf_helpers/inprocessing/inprocessing.py b/aif360/algorithms/isf_helpers/inprocessing/inprocessing.py new file mode 100644 index 00000000..5dc2ba3e --- /dev/null +++ b/aif360/algorithms/isf_helpers/inprocessing/inprocessing.py @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABCMeta +from abc import abstractmethod + + +class InProcessing(metaclass=ABCMeta): + """ + Abstract Base Class for all inprocessing techniques. + """ + def __init__(self): + super().__init__() + self.model = None + + @abstractmethod + def fit(self, ds_train): + """ + Train a model on the input. + + Parameters + ---------- + ds_train : Dataset + Training Dataset. + """ + pass + + @abstractmethod + def predict(self, ds): + """ + Predict on the input. + + Parameters + ---------- + ds : Dataset + Dataset to predict. + """ + pass diff --git a/aif360/algorithms/isf_helpers/isf_analysis/intersectional_bias.py b/aif360/algorithms/isf_helpers/isf_analysis/intersectional_bias.py new file mode 100644 index 00000000..b7da3477 --- /dev/null +++ b/aif360/algorithms/isf_helpers/isf_analysis/intersectional_bias.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import matplotlib.pyplot as plt +from matplotlib.gridspec import GridSpec +import seaborn as sns + +from aif360.algorithms.isf_helpers.isf_metrics.disparate_impact import DisparateImpact +from aif360.algorithms.isf_helpers.isf_utils.common import create_multi_group_label + + +def calc_intersectionalbias(dataset, metric="DisparateImpact"): + """ + Calculate intersectional bias(DisparateImpact) by more than one sensitive attributes + + Parameters + ---------- + dataset : StructuredDataset + A dataset containing more than one sensitive attributes + + metric : str + Fairness metric name + ["DisparateImpact"] + + Returns + ------- + df_result : DataFrame + Intersectional bias(DisparateImpact) + """ + + df = dataset.convert_to_dataframe()[0] + label_info = {dataset.label_names[0]: dataset.favorable_label} + + if metric == "DisparateImpact": + fs = DisparateImpact() + else: + raise ValueError("metric name not in the list of allowed metrics") + + df_result = pd.DataFrame(columns=[metric]) + for multi_group_label in create_multi_group_label(dataset)[0]: + protected_attr_info = multi_group_label[0] + di = fs.bias_predict(df, + protected_attr_info=protected_attr_info, + label_info=label_info) + name = '' + for k, v in protected_attr_info.items(): + name += k + " = " + str(v) + "," + df_result.loc[name[:-1]] = di + + return df_result + + +def plot_intersectionalbias_compare(ds_bef, ds_aft, vmax=1, vmin=0, center=0, + metric="DisparateImpact", + title={"right": "before", "left": "after"}, + filename=None): + """ + Compare drawing of intersectional bias in heat map + + Parameters + ---------- + ds_bef : StructuredDataset + Dataset containing two sensitive attributes (left figure) + ds_aft : StructuredDataset + Dataset containing two sensitive attributes (right figure) + filename : str, optional + File name(png) + e.g. "./result/pict.png" + metric : str + Fairness metric name + ["DisparateImpact"] + title : dictonary, optional + Graph title (right figure, left figure) + """ + + df_bef = calc_intersectionalbias_matrix(ds_bef, metric) + df_aft = calc_intersectionalbias_matrix(ds_aft, metric) + + gs = GridSpec(1, 2) + ss1 = gs.new_subplotspec((0, 0)) + ss2 = gs.new_subplotspec((0, 1)) + + ax1 = plt.subplot(ss1) + ax2 = plt.subplot(ss2) + + ax1.set_title(title['right']) + sns.heatmap(df_bef, ax=ax1, vmax=vmax, vmin=vmin, center=center, annot=True, cmap='hot') + + ax2.set_title(title['left']) + sns.heatmap(df_aft, ax=ax2, vmax=vmax, vmin=vmin, center=center, annot=True, cmap='hot') + + if filename is not None: + plt.savefig(filename, format="png", dpi=300) + plt.show() + + +def calc_intersectionalbias_matrix(dataset, metric="DisparateImpact"): + """ + Comparison drawing of intersectional bias in heat map + + Parameters + ---------- + dataset : StructuredDataset + Dataset containing two sensitive attributes + metric : str + Fairness metric name + ["DisparateImpact"] + + Returns + ------- + df_result : DataFrame + Intersectional bias(DisparateImpact) + """ + + protect_attr = dataset.protected_attribute_names + + if len(protect_attr) != 2: + raise ValueError("specify 2 sensitive attributes.") + + if metric == "DisparateImpact": + fs = DisparateImpact() + else: + raise ValueError("metric name not in the list of allowed metrics") + + df = dataset.convert_to_dataframe()[0] + label_info = {dataset.label_names[0]: dataset.favorable_label} + + protect_attr0_values = list(set(df[protect_attr[0]])) + protect_attr1_values = list(set(df[protect_attr[1]])) + + df_result = pd.DataFrame(columns=protect_attr1_values) + + for val0 in protect_attr0_values: + tmp_li = [] + col_list = [] + for val1 in protect_attr1_values: + di = fs.bias_predict(df, + protected_attr_info={protect_attr[0]: val0, protect_attr[1]: val1}, + label_info=label_info) + tmp_li += [di] + col_list += [protect_attr[1]+"="+str(val1)] + + df_result.loc[protect_attr[0]+"="+str(val0)] = tmp_li + df_result = df_result.set_axis(col_list, axis=1) + + return df_result diff --git a/aif360/algorithms/isf_helpers/isf_analysis/metrics.py b/aif360/algorithms/isf_helpers/isf_analysis/metrics.py new file mode 100644 index 00000000..8ff3d056 --- /dev/null +++ b/aif360/algorithms/isf_helpers/isf_analysis/metrics.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import numpy as np +from aif360.algorithms.isf_helpers.isf_utils.common import create_multi_group_label, output_subgroup_metrics + + +def summary(dataset): + """ + Dataset statistics by attribute + + Parameters + ---------- + dataset : StructuredDataset + dataset + + Returns + ------- + columns_summary : DataFrame + Dataset statistics by attribute + """ + + df = dataset.convert_to_dataframe()[0] + + col_list = df.columns.values + row = [] + for col in col_list: + if df[col].dtypes == 'int64' or df[col].dtypes == 'float64': + ave = df[col].mean() + var = df[col].var() + std = df[col].std() + type = df[col].dtypes + else: + ave = np.nan + var = np.nan + std = np.nan + type = df[col].dtypes + + tmp = (col, + type, # datatype + df[col].isnull().sum(), # null count + ave, # mean + var, # variance + std, # standard deviation + df[col].count(), + df[col].nunique(), # amount of unique values + df[col].unique()) # example value + + row.append(tmp) + df_columns_summary = pd.DataFrame(row) + df_columns_summary.columns = ['feature', 'dtypes', 'NaN', 'mean', 'var', 'std', 'count', 'num_unique', 'unique'] + df_columns_summary = df_columns_summary.sort_values('feature').reset_index(drop=True) + + return df_columns_summary + + +def check_metrics_combination_attribute(dataset, ds_predicted): + """ + Calculating Classification Performance with sensitive attribute combinations + + Parameters + ---------- + dataset_test_pred : StructuredDataset + Dataset containing prediction + dataset_test : StructuredDataset + Dataset containing ground-truth labels. + + Returns + ---------- + sg_metrics : DataFrame + Classification Performance + """ + group_protected_attrs, _ = create_multi_group_label(dataset) + sg_metrics = output_subgroup_metrics(dataset, ds_predicted, group_protected_attrs, out_group=False) + sg_metrics = sg_metrics.set_index('group') + + return sg_metrics.drop('total', axis=0) + + +def check_metrics_single_attribute(dataset, ds_predicted): + """ + Calculating classification performance with a single sensitive attribute + + Parameters + ---------- + dataset_test_pred : StructuredDataset + Dataset containing prediction + dataset_test : StructuredDataset + Dataset containing ground-truth labels + + Returns + ---------- + g_metrics : DataFrame + Classification Performance + """ + group_protected_attrs, _ = create_multi_group_label(dataset) + g_metrics, _ = output_subgroup_metrics(dataset, ds_predicted, group_protected_attrs) + g_metrics = g_metrics.set_index('group') + return g_metrics.drop('total', axis=0) diff --git a/aif360/algorithms/isf_helpers/isf_metrics/disparate_impact.py b/aif360/algorithms/isf_helpers/isf_metrics/disparate_impact.py new file mode 100644 index 00000000..f3ddc55c --- /dev/null +++ b/aif360/algorithms/isf_helpers/isf_metrics/disparate_impact.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.cm as cm + + +class DisparateImpact(): + """ + Calculate Disparate Impact score + """ + + def bias_predict(self, df, protected_attr_info, label_info): + return self.calc_di(df, protected_attr_info, label_info) + + def calc_di(self, df, protected_attr_info, label_info): + """ + Calculate Disparate Impact score + + Parameters + ---------- + df : DataFrame + DataFrame containing sensitive attributes and label + sensitive : dictionary + Privileged group (sensitive attribute name : attribute value) + e.g. {'Gender':1.0,'Race':'black'} + label_info : dictionary + Label definition (label attribute name : attribute values) + e.g. {'denied':1.0} + + Returns + ------- + return value : float + Disparete Impact score + """ + df_bunshi, df_bunbo = self.calc_privilege_group(df, protected_attr_info) + + if (len(df_bunshi) == 0): + return np.nan + + if (len(df_bunbo) == 0): + return np.nan + + label = list(label_info.keys())[0] + privileged_value = list(label_info.values())[0] + + a = len(df_bunshi[df_bunshi[label] == privileged_value]) + b = len(df_bunbo[df_bunbo[label] == privileged_value]) + + bunshi_rate = a / len(df_bunshi) + bunbo_rate = b / len(df_bunbo) + + if bunbo_rate == 0: + return np.nan + + return (bunshi_rate/bunbo_rate) + + def calc_di_attribute(self, df, protected_attr, label_info): + """ + Specify sensitive attribute name and calculate disparete impact score for each attribute value + + Parameters + ---------- + df : DataFrame + DataFrame containing sensitive attribute and label + protected_attr : str + Sensitive attribute name + e.g. 'female' + label_info : dictionary + Label definition (label attribute name : attribute values) + e.g. {'denied':1.0} + + Returns + ------- + dic_di : dictionary + {attribute value: Disparete Impact score, ...} + """ + dic_di = {} + for privileged_value in list(set(df[protected_attr])): + di = self.calc_di(df, + protected_attr_info={protected_attr: privileged_value}, + label_info=label_info) + dic_di[privileged_value] = di + return dic_di + + def plot_di_attribute(self, dic_di, target_attr, filename=None): + """ + Draw the disparete impact score in a bar chart + + Parameters + ---------- + dic_di : dictionary + Disparete impact score + {attribute value: disparete impact score, ...} + target_attr : str + Sensitive attribute name + e.g. 'female' + filename : str, optional + File name(png) + e.g. './result/pict.png' + """ + + num = len(dic_di) + color_list = [cm.winter_r(i/num) for i in range(num)] + ymax = 1.0 + + plt.title("DI Score:"+target_attr) + plt.ylabel('DI') + plt.ylim(0, ymax) + plt.xlabel('Attribute value') + + labels = [str(k) for k, v in dic_di.items()] + vals = [val if val <= ymax else ymax for val in list(dic_di.values())] + plt.bar(labels, vals, color=color_list) + + for x, y, val in zip(labels, dic_di.values(), vals): + plt.text(x, val, round(y, 3), ha='center', va='bottom') + + if filename is not None: + plt.savefig(filename, format="png", dpi=300) + + plt.show() + + def calc_privilege_group(self, df, protected_attr_info): + """ + Split into privileged and non-privileged groups + + Parameters + ---------- + df : DataFrame + DataFrame containing sensitive attribute and label + protected_attr_info : dictionary + Privileged group definition (sensitive attribute name : attribute values) + e.g. {'female':1.0} + + Returns + ------- + privilege_group : DataFrame + Privileged group + non_privilege_group : DataFrame + Non-privileged group + """ + + privilege_group = df.copy() + for key, val in protected_attr_info.items(): + privilege_group = privilege_group.loc[(privilege_group[key] == val)] + + non_privilege_group = df.drop(privilege_group.index) + + return privilege_group, non_privilege_group diff --git a/aif360/algorithms/isf_helpers/isf_utils/common.py b/aif360/algorithms/isf_helpers/isf_utils/common.py new file mode 100644 index 00000000..4f6bc058 --- /dev/null +++ b/aif360/algorithms/isf_helpers/isf_utils/common.py @@ -0,0 +1,442 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Metrics function +import numpy as np +import pandas as pd +import itertools + +from aif360.metrics import ClassificationMetric + +from sklearn.linear_model import LogisticRegression +from sklearn.ensemble import RandomForestClassifier +from sklearn.preprocessing import StandardScaler + +from aif360.datasets import BinaryLabelDataset + + +def classify(dataset_train, dataset_test, algorithm='LR', threshold=None): + """ + Predict with LogisticRegression or RandomForest model + + Parameters + ---------- + dataset_train : StructuredDataset + Dataset for training + dataset_test : StructuredDataset + Dataset for evaluation + algorithm : str, optional + Classification algorithm + ['LR','RF'] + threshold : float, optional + Threshold for determining predicted labels + + Returns + ---------- + dataset_test_pred : StructuredDataset + Prediction dataset + best_class_thresh : float + Threshold for determining predicted labels + best_accuracy : float + Accuracy when searching for threshold + """ + + dataset_test_pred = dataset_test.copy(deepcopy=True) + scale_orig = StandardScaler() + X_train = scale_orig.fit_transform(dataset_train.features) + y_train = dataset_train.labels.ravel() + X_test = scale_orig.transform(dataset_test_pred.features) + + if algorithm == 'LR': + mod = LogisticRegression() + mod.fit(X_train, y_train) + pos_ind = np.where(mod.classes_ == dataset_train.favorable_label)[0][0] + dataset_test_pred.scores = mod.predict_proba(X_test)[:, pos_ind].reshape(-1, 1) + elif algorithm == 'RF': + mod = RandomForestClassifier(random_state=1) + mod.fit(X_train, y_train) + pos_ind = np.where(mod.classes_ == dataset_train.favorable_label)[0][0] + dataset_test_pred.scores = mod.predict_proba(X_test)[:, pos_ind].reshape(-1, 1) + elif algorithm == 'AD': + exit(0) + + dataset_test_pred, best_class_thresh, best_accuracy = decision_label(dataset_test_pred, dataset_test, metric='Balanced accuracy', threshold=threshold) + + return dataset_test_pred, best_class_thresh, best_accuracy + + +def decision_label(dataset_test_pred, dataset_test, metric='Balanced accuracy', threshold=None): + """ + Determine prediction labels from prediction scores + + Parameters + ---------- + dataset_test_pred : StructuredDataset + Dataset containing prediction + dataset_test : StructuredDataset + Dataset containing ground-truth labels. + metric : str + Accuracy metric for determining threshold + ['Balanced accuracy', F1] + threshold : float + Threshold for determining predicted labels + + Returns + ---------- + dataset_test_pred : StructuredDataset + Dataset containing prediction + threshold : float + Threshold for determining predicted labels + best_accuracy : float + Accuracy when searching for threshold + + Note + ---------- + If threshold is None, perform parameter search for threshold + + """ + best_accuracy = None + if threshold is None: + num_thresh = 100 + ba_arr = np.zeros(num_thresh) + class_thresh_arr = np.linspace(0.01, 0.99, num_thresh) + for idxw, class_thresh in enumerate(class_thresh_arr): + + fav_inds = dataset_test_pred.scores > class_thresh + dataset_test_pred.labels[fav_inds] = dataset_test_pred.favorable_label + dataset_test_pred.labels[~fav_inds] = dataset_test_pred.unfavorable_label + + classified_metric_orig_valid = ClassificationMetric(dataset_test, + dataset_test_pred) + + if metric == 'Balanced accuracy': + ba_arr[idxw] = 0.5 * (classified_metric_orig_valid.true_positive_rate() + + classified_metric_orig_valid.true_negative_rate()) + elif metric == 'F1': + recall = classified_metric_orig_valid.recall() + precision = classified_metric_orig_valid.precision() + f1 = (2 * recall * precision) / (recall + precision) + ba_arr[idxw] = f1 + else: + print('select supported metric.') + exit(1) + + best_accuracy = np.max(ba_arr) + best_ind = np.where(ba_arr == best_accuracy)[0][0] + threshold = class_thresh_arr[best_ind] + + fav_inds = dataset_test_pred.scores > threshold + dataset_test_pred.labels[fav_inds] = dataset_test_pred.favorable_label + dataset_test_pred.labels[~fav_inds] = dataset_test_pred.unfavorable_label + + return dataset_test_pred, threshold, best_accuracy + + +def get_baseline(scores, y): + """ + Calculate Accuracy When Searching for Threshold + + Parameters + ---------- + scores : list + Prediction score + y : list + True label + + Returns + ---------- + best_accuracy : float + Accuracy + """ + df = pd.DataFrame((np.stack([scores, y, np.zeros(len(y))], 1)), columns=['scores', 'y', 'pan']) + ds_act = BinaryLabelDataset(df=df, label_names=['y'], protected_attribute_names=['pan']) + ds_pred = ds_act.copy(deepcopy=True) + ds_pred.scores = scores.reshape(-1, 1) + ds_pred, _, best_accuracy = decision_label(ds_pred, ds_act) + + return best_accuracy + + +def output_subgroup_metrics(dataset_act, dataset_pred, protected_attributes, out_file_path=None, out_group=True): + """ + Calculate classification metrics by combining sensitive attributes + + Parameters + ---------- + dataset_act : StructuredDataset + Dataset containing ground-truth labels. + dataset_pred : StructuredDataset + Dataset containing prediction + protected_attributes : list + Combining Sensitive Attributes and Attribute Values(group) + out_file_path : str, optional + Save path for classification performance + out_group : boolean + Also compute df_group_metrics + + Returns + ---------- + df_group_metrics : DataFrame + Classification performance(for each sensitive attribute) + df_subgroup_metrics : DataFrame + Classification performance(for combining sensitive attributes) + """ + subgroups = protected_attributes + + metric, header = _compute_classification_metric(subgroups, dataset_act, dataset_pred) + df_subgroup_metrics = pd.DataFrame(metric, columns=header) + if out_file_path is not None: + df_subgroup_metrics.to_csv(out_file_path) + + if out_group: + value_set_dict = {} + for pa in dataset_act.protected_attribute_names: + value_set = set() + for sg in subgroups: + value_set.add(sg[0][pa]) + value_set_dict[pa] = value_set + + groups = [] + for pa, value_set in value_set_dict.items(): + for v in value_set: + groups.append([{pa: v}]) + + metric, header = _compute_classification_metric(groups, dataset_act, dataset_pred) + df_group_metrics = pd.DataFrame(metric, columns=header) + # print(df_metric[['group', 'base_rate', 'selection_rate', 'Bl_Accuracy']]) + if out_file_path is not None: + df_group_metrics.to_csv(out_file_path) + + return df_group_metrics, df_subgroup_metrics + + else: + return df_subgroup_metrics + + +def _compute_classification_metric(groups, dataset_act, dataset_pred, class_th=-1): + """ + Compute classification metric by group + + Classification metric + (CM->aif360.metrics.ClassificationMetric) + --------------------------------------- + P:Alias of CM.num_positives() + N:Alias of CM.num_negatives() + base_rate: Alias of CM.base_rate() + P^: Alias of CM.num_pred_positives() + N^: Alias of CM.num_pred_negatives() + selection_rate: Alias of CM.selection_rate() + TP: Alias of CM.num_true_positives() + FP: Alias of CM.num_false_positives() + TN: Alias of CM.num_true_negatives() + FN: Alias of CM.num_false_negatives() + TPR: Alias of CM.true_positive_rate() + FPR: Alias of CM.false_positive_rate() + TNR: Alias of CM.true_negative_rate() + FNR: Alias of CM.false_negative_rate() + Average_Odds_Difference:Average Odds Difference + Accuracy: Alias of CM.accuracy() + Balanced_Accuracy: Balanced Accuracy + Precision: Precision + F1: F1-measure + Statistical_Parity_Difference_base: Statistical Parity Difference(true label) + Disparate_Impact_base: Disparate Impact(true value) + Statistical_Parity_Difference_sel: Statistical Parity Difference(true label) + Disparate_Impact_sel(prediction label) + Equal_Opportunity_Difference:Equal Opportunity Difference(prediction label) + + Parameters + ---------- + groups : list + Combining Sensitive Attributes and Attribute Values + dataset_act : StructuredDataset + Dataset containing ground-truth labels. + dataset_pred : StructuredDataset + Dataset containing prediction. + + Returns + ---------- + metric : list + Classification metrics + header : list[str] + Metric item names + """ + + cm = None + privileged = None + metric = [] + group_name = 'total' + for i in range(len(groups) + 1): + if i == 0: + cm = ClassificationMetric(dataset_act, dataset_pred) + other_base_rate = cm.base_rate() + other_selection_rate = cm.selection_rate() + other_true_positive_rate = cm.true_positive_rate() + else: + cm = ClassificationMetric(dataset_act, dataset_pred, privileged_groups=groups[i - 1]) + privileged = True + group_name = '' + for k in groups[i - 1][0].keys(): + group_name += k + ':' + str(groups[i - 1][0][k]) + '_' + group_name = group_name.rstrip('_') + other_num_instances = cm.num_instances() - cm.num_instances(privileged=privileged) + other_num_positives = cm.num_positives() - cm.num_positives(privileged=privileged) + other_num_pred_positives = cm.num_pred_positives() - cm.num_pred_positives(privileged=privileged) + other_base_rate = other_num_positives / other_num_instances + other_selection_rate = other_num_pred_positives / other_num_instances + other_num_true_positives = cm.num_true_positives() - cm.num_true_positives(privileged=privileged) + other_true_positive_rate = other_num_true_positives / other_num_positives + + precision = cm.precision(privileged=privileged) + recall = cm.true_positive_rate(privileged=privileged) + f1 = 2 * precision * recall / (precision + recall) + metric.append([ + # class_th, + group_name, + cm.num_positives(privileged=privileged), + cm.num_negatives(privileged=privileged), + cm.base_rate(privileged=privileged), + cm.num_pred_positives(privileged=privileged), + cm.num_pred_negatives(privileged=privileged), + cm.selection_rate(privileged=privileged), + cm.num_true_positives(privileged=privileged), + cm.num_false_positives(privileged=privileged), + cm.num_true_negatives(privileged=privileged), + cm.num_false_negatives(privileged=privileged), + cm.true_positive_rate(privileged=privileged), + cm.false_positive_rate(privileged=privileged), + cm.true_negative_rate(privileged=privileged), + cm.false_negative_rate(privileged=privileged), + 0.5 * (cm.true_positive_rate(privileged=privileged) + cm.false_positive_rate(privileged=privileged)), + cm.accuracy(privileged=privileged), + 0.5 * (cm.true_positive_rate(privileged=privileged) + cm.true_negative_rate(privileged=privileged)), + precision, + f1, + cm.base_rate(privileged=privileged) - other_base_rate, + min((cm.base_rate(privileged=privileged) / other_base_rate), other_base_rate / cm.base_rate(privileged=privileged)), + cm.selection_rate(privileged=privileged) - other_selection_rate, + min((cm.selection_rate(privileged=privileged) / other_selection_rate), other_selection_rate / cm.selection_rate(privileged=privileged)), + cm.true_positive_rate(privileged=privileged) - other_true_positive_rate + ]) + header = [ + # 'class_th', + 'group', + 'P', + 'N', + 'base_rate', + 'P^', + 'N^', + 'selection_rate', + 'TP', + 'FP', + 'TN', + 'FN', + 'TPR', + 'FPR', + 'TNR', + 'FNR', + 'Average_Odds_Difference', + 'Accuracy', + 'Balanced_Accuracy', + 'Precision', + 'F1', + 'Statistical_Parity_Difference_base', + 'Disparate_Impact_base', + 'Statistical_Parity_Difference_sel', # selection_rate - other_selection_rate + 'Disparate_Impact_sel', # min{(selection_rate / other_selection_rate), (other_selection_rate / selection_rate)} + 'Equal_Opportunity_Difference' + ] + return metric, header + + +def convert_labels(ds, conversion_pattern=None): + """ + Convert label value to 1/-1, 1.0/0.0 + + Parameters + ---------- + ds : StandardDataset + Dataset containing labels + conversion_pattern : str + Select conversion pattern + 'MA': 1/-1, None: 1.0/0.0 + ['MA', None] + """ + + fav_label = ds.favorable_label + ufav_label = ds.unfavorable_label + if conversion_pattern == 'MA': + fav_label = 1 + ufav_label = -1 + else: + fav_label = 1.0 + ufav_label = 0.0 + ds.labels = np.array([[fav_label] if y == ds.favorable_label else [ufav_label] for y in ds.labels]) + ds.favorable_label = fav_label + ds.unfavorable_label = ufav_label + + +def create_multi_group_label(dataset): + """ + Combine sensitive attributes and attribute values to create group label + + Parameters + ---------- + dataset : StandardDataset + Dataset containing sensitive attribute + + Returns + ---------- + combinataion_label : list + Group label + combinataion_label_shape : list + Group label shape + """ + combinataion_label_shape = [] + + df, _ = dataset.convert_to_dataframe() + + # TODO generalize + labelss = [] + label_list = None + for i in range(len(dataset.protected_attribute_names)): + labels = df[dataset.protected_attribute_names[0]].unique() + labelss.append(labels) + combinataion_label_shape.append(len(labels)) + if len(dataset.protected_attribute_names) == 1: + label_list = list(itertools.product(labelss[0])) + elif len(dataset.protected_attribute_names) == 2: + label_list = list(itertools.product(labelss[0], labelss[1])) + elif len(dataset.protected_attribute_names) == 3: + label_list = list(itertools.product(labelss[0], labelss[1], labelss[2])) + elif len(dataset.protected_attribute_names) == 4: + label_list = list(itertools.product(labelss[0], labelss[1], labelss[2], labelss[3])) + elif len(dataset.protected_attribute_names) == 5: + label_list = list(itertools.product(labelss[0], labelss[1], labelss[2], labelss[3], labelss[4])) + else: + raise ValueError( + "Up to 5 protected_attribute_names can be set.") + + combinataion_label = [] + for i1 in range(len(label_list)): + group_label = {} + for i2 in range(len(dataset.protected_attribute_names)): + group_label[dataset.protected_attribute_names[i2]] = label_list[i1][i2] + listw = [] + listw.append(group_label) + combinataion_label.append(listw) + + return combinataion_label, combinataion_label_shape diff --git a/aif360/algorithms/isf_helpers/isf_utils/const.py b/aif360/algorithms/isf_helpers/isf_utils/const.py new file mode 100644 index 00000000..e5722fe4 --- /dev/null +++ b/aif360/algorithms/isf_helpers/isf_utils/const.py @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +DEMOGRAPHIC_PARITY = ['DemographicParity', 'StatisticalParityDifference'] +EQUAL_OPPORTUNITY = ['EqualOpportunity', 'EqualOpportunityDifference'] +EQUALIZED_ODDS = ['EqualizedOdds', 'AverageOddsDifference'] +F1_PARITY = ['F1Parity'] diff --git a/aif360/algorithms/isf_helpers/postprocessing/eq_odds.py b/aif360/algorithms/isf_helpers/postprocessing/eq_odds.py new file mode 100644 index 00000000..1b403357 --- /dev/null +++ b/aif360/algorithms/isf_helpers/postprocessing/eq_odds.py @@ -0,0 +1,295 @@ +# Copyright (c) 2017 Geoff Pleiss +# This software includes modifications made by Fujitsu Limited to the original +# software licensed under the MIT License. Modified portions of this software +# are for the addition of a new parameter threshold and support of +# Equal Opportunity in class Model, especially in its functions eq_odds and +# eq_odds_optimal_mix_rates. +# +# https://github.com/gpleiss/equalized_odds_and_calibration/blob/master/LICENSE +# +# +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cvxpy as cvx +import numpy as np +from collections import namedtuple + + +class Model(namedtuple('Model', 'pred label threshold')): + """ + Based on the model prediction (accuracy), label true value, and group information divided by sensitive attribute, optimization problem is performed and mix_rate (4 variables that become prediction value conversion rules) is calculated. + + Parameters + ---------- + pred : series + Model prediction (probability) + label : series + True label + threshold : float + Threshold for how many positive values are considered positive for model prediction + + Notes + ----- + https://github.com/gpleiss/equalized_odds_and_calibration + """ + def logits(self): + raw_logits = np.clip(np.log(self.pred / (1 - self.pred)), -100, 100) + return raw_logits + + def num_samples(self): + return len(self.pred) + + def base_rate(self): + """ + Percentage of samples belonging to the positive class + """ + return np.mean(self.label) + + def accuracy(self): + return self.accuracies().mean() + + def precision(self): + # return (self.label[self.pred.round() == 1]).mean() + return (self.label[self.pred > self.threshold]).mean() + + def recall(self): + return (self.label[self.label == 1].round()).mean() + + def tpr(self): + """ + True positive rate + """ + # return np.mean(np.logical_and(self.pred.round() == 1, self.label == 1)) + return np.mean(np.logical_and(self.pred > self.threshold, self.label == 1)) + + def fpr(self): + """ + False positive rate + """ + # return np.mean(np.logical_and(self.pred.round() == 1, self.label == 0)) + return np.mean(np.logical_and(self.pred > self.threshold, self.label == 0)) + + def tnr(self): + """ + True negative rate + """ + # return np.mean(np.logical_and(self.pred.round() == 0, self.label == 0)) + return np.mean(np.logical_and(self.pred <= self.threshold, self.label == 0)) + + def fnr(self): + """ + False negative rate + """ + # return np.mean(np.logical_and(self.pred.round() == 0, self.label == 1)) + return np.mean(np.logical_and(self.pred <= self.threshold, self.label == 1)) + + def fn_cost(self): + """ + Generalized false negative cost + """ + return 1 - self.pred[self.label == 1].mean() + + def fp_cost(self): + """ + Generalized false positive cost + """ + return self.pred[self.label == 0].mean() + + def accuracies(self): + return self.pred.round() == self.label + + def eq_odds(self, othr, mix_rates=None, threshold=None, metric='EqualOpportunity'): + """ + Based on the model prediction (accuracy), label true value, and group information divided by protection attribute, optimization problem is performed and mix_rate (4 variables that become prediction value conversion rules) is calculated. + + Parameters + ---------- + othr : Model + input model + mix_rates : tuple, optional + model parameter + If None, calculate internally + threshold : float, optional + Threshold for how many positive values are considered positive for model prediction + metric : String + Used for constraint terms in optimization problems.['EqualOpportunity',] + + Returns + ------- + fair_self : Model + self-model after debiasing + fair_othr : Model + othr-model after debiasing + mix_rates : tuple + model parameter + """ + has_mix_rates = not (mix_rates is None) + if not has_mix_rates: + mix_rates = self.eq_odds_optimal_mix_rates(othr, metric) + sp2p, sn2p, op2p, on2p = tuple(mix_rates) + + self_fair_pred = self.pred.copy() + # self_pp_indices, = np.nonzero(self.pred.round()) + # self_pn_indices, = np.nonzero(1 - self.pred.round()) + self_pp_indices, = np.nonzero(self.pred > self.threshold) + self_pn_indices, = np.nonzero(self.pred <= self.threshold) + np.random.shuffle(self_pp_indices) + np.random.shuffle(self_pn_indices) + + n2p_indices = self_pn_indices[:int(len(self_pn_indices) * sn2p)] + self_fair_pred[n2p_indices] = 1 - self_fair_pred[n2p_indices] + p2n_indices = self_pp_indices[:int(len(self_pp_indices) * (1 - sp2p))] + self_fair_pred[p2n_indices] = 1 - self_fair_pred[p2n_indices] + + othr_fair_pred = othr.pred.copy() + # othr_pp_indices, = np.nonzero(othr.pred.round()) + # othr_pn_indices, = np.nonzero(1 - othr.pred.round()) + othr_pp_indices, = np.nonzero(othr.pred > othr.threshold) + othr_pn_indices, = np.nonzero(1 - (othr.pred <= othr.threshold)) + np.random.shuffle(othr_pp_indices) + np.random.shuffle(othr_pn_indices) + + n2p_indices = othr_pn_indices[:int(len(othr_pn_indices) * on2p)] + othr_fair_pred[n2p_indices] = 1 - othr_fair_pred[n2p_indices] + p2n_indices = othr_pp_indices[:int(len(othr_pp_indices) * (1 - op2p))] + othr_fair_pred[p2n_indices] = 1 - othr_fair_pred[p2n_indices] + + fair_self = Model(self_fair_pred, self.label, threshold) + fair_othr = Model(othr_fair_pred, othr.label, threshold) + + if not has_mix_rates: + return fair_self, fair_othr, mix_rates + else: + return fair_self, fair_othr + + def eq_odds_optimal_mix_rates(self, othr, metric): + """ + Calculate the mix_rate (4 variables that are the conversion rules for predicted values) in the optimization problem + + Parameters + ---------- + othr : Model + input model + + metric : String + Used for constraint terms in optimization problems.['EqualOpportunity',] + + Returns + ------- + res : array + mix_rate + [sp2p, sn2p, op2p, on2p] + """ + sbr = float(self.base_rate()) + obr = float(othr.base_rate()) + + sp2p = cvx.Variable(1) + sp2n = cvx.Variable(1) + sn2p = cvx.Variable(1) + sn2n = cvx.Variable(1) + + op2p = cvx.Variable(1) + op2n = cvx.Variable(1) + on2p = cvx.Variable(1) + on2n = cvx.Variable(1) + + sfpr = self.fpr() * sp2p + self.tnr() * sn2p + sfnr = self.fnr() * sn2n + self.tpr() * sp2n + ofpr = othr.fpr() * op2p + othr.tnr() * on2p + ofnr = othr.fnr() * on2n + othr.tpr() * op2n + error = sfpr + sfnr + ofpr + ofnr + + sflip = 1 - self.pred + sconst = self.pred + oflip = 1 - othr.pred + oconst = othr.pred + + # sm_tn = np.logical_and(self.pred.round() == 0, self.label == 0) + # sm_fn = np.logical_and(self.pred.round() == 0, self.label == 1) + # sm_tp = np.logical_and(self.pred.round() == 1, self.label == 1) + # sm_fp = np.logical_and(self.pred.round() == 1, self.label == 0) + sm_tn = np.logical_and(self.pred <= self.threshold, self.label == 0) + sm_fn = np.logical_and(self.pred <= self.threshold, self.label == 1) + sm_tp = np.logical_and(self.pred > self.threshold, self.label == 1) + sm_fp = np.logical_and(self.pred > self.threshold, self.label == 0) + + om_tn = np.logical_and(othr.pred <= othr.threshold, othr.label == 0) + om_fn = np.logical_and(othr.pred <= othr.threshold, othr.label == 1) + om_tp = np.logical_and(othr.pred > othr.threshold, othr.label == 1) + om_fp = np.logical_and(othr.pred > othr.threshold, othr.label == 0) + + # average of N-probability for FN cases average of P-probability of FN cases + spn_given_p = (sn2p * (sflip * sm_fn).mean() + sn2n * (sconst * sm_fn).mean()) / sbr + \ + (sp2p * (sconst * sm_tp).mean() + sp2n * (sflip * sm_tp).mean()) / sbr + # average of P-probability for TP cases average of N-probability of TP cases + + spp_given_n = (sp2n * (sflip * sm_fp).mean() + sp2p * (sconst * sm_fp).mean()) / (1 - sbr) + \ + (sn2p * (sflip * sm_tn).mean() + sn2n * (sconst * sm_tn).mean()) / (1 - sbr) + + opn_given_p = (on2p * (oflip * om_fn).mean() + on2n * (oconst * om_fn).mean()) / obr + \ + (op2p * (oconst * om_tp).mean() + op2n * (oflip * om_tp).mean()) / obr + + opp_given_n = (op2n * (oflip * om_fp).mean() + op2p * (oconst * om_fp).mean()) / (1 - obr) + \ + (on2p * (oflip * om_tn).mean() + on2n * (oconst * om_tn).mean()) / (1 - obr) + + constraints = [ + sp2p == 1 - sp2n, + sn2p == 1 - sn2n, + op2p == 1 - op2n, + on2p == 1 - on2n, + sp2p <= 1, + sp2p >= 0, + sn2p <= 1, + sn2p >= 0, + op2p <= 1, + op2p >= 0, + on2p <= 1, + on2p >= 0, + spp_given_n == opp_given_n, + spn_given_p == opn_given_p, + ] + if metric == 'EqualOpportunity': + constraints = [ + sp2p == 1 - sp2n, + sn2p == 1 - sn2n, + op2p == 1 - op2n, + on2p == 1 - on2n, + sp2p <= 1, + sp2p >= 0, + sn2p <= 1, + sn2p >= 0, + op2p <= 1, + op2p >= 0, + on2p <= 1, + on2p >= 0, + spn_given_p == opn_given_p, + ] + + prob = cvx.Problem(cvx.Minimize(error), constraints) + prob.solve() + + res = np.array([sp2p.value, sn2p.value, op2p.value, on2p.value]) + return res + + def __repr__(self): + return '\n'.join([ + 'Accuracy:\t%.3f' % self.accuracy(), + 'F.P. cost:\t%.3f' % self.fp_cost(), + 'F.N. cost:\t%.3f' % self.fn_cost(), + 'Base rate:\t%.3f' % self.base_rate(), + 'Avg. score:\t%.3f' % self.pred.mean(), + ]) diff --git a/aif360/algorithms/isf_helpers/postprocessing/equalized_odds_postprocessing.py b/aif360/algorithms/isf_helpers/postprocessing/equalized_odds_postprocessing.py new file mode 100644 index 00000000..d9a5aeb0 --- /dev/null +++ b/aif360/algorithms/isf_helpers/postprocessing/equalized_odds_postprocessing.py @@ -0,0 +1,136 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pandas as pd + +from aif360.algorithms.isf_helpers.postprocessing.postprocessing import PostProcessing +from aif360.algorithms.isf_helpers.postprocessing.eq_odds import Model + + +class EqualizedOddsPostProcessing(PostProcessing): + """ + Debiasing intersectional bias with Equalized-Odds extended by ISF. + + Parameters + ---------- + options : dictionary + parameter of Equalized-Odds + metric: Constraint terms for optimization problems within Equalized-Odds + threshold: Threshold for how many positive values are considered positive for model prediction + [metric,threshold] + """ + + def __init__(self, options): + super().__init__() + self.metric = options['metric'] + self.threshold = options['threshold'] + + def fit(self, ds_act, ds_predict): + """ + Generate training data and divide it into two groups according to the attribute value of the sensitive attribute. +Then, create an EqualizedOdds model for each group data. + + Parameters + ---------- + ds_act : Dataset + Dataset containing ground-truth labels + + ds_predict : Dataset + Dataset for evaluation + The dataset containing prediction + """ + ikey = ds_act.protected_attribute_names[0] + pa_i = ds_act.protected_attribute_names.index(ikey) + + np_train = np.concatenate([np.array(ds_predict.instance_names).reshape(-1, 1), + ds_act.labels.reshape(-1, 1), + ds_predict.protected_attributes[:, pa_i].reshape(-1, 1), + ds_predict.scores], 1) + df_train = pd.DataFrame(np_train, columns=['name', 'label', 'group', 'prediction']) + + df_train['name'] = df_train['name'].astype(float) + df_train['label'] = df_train['label'].astype(float) + df_train['group'] = df_train['group'].astype(float) + df_train['prediction'] = df_train['prediction'].astype(float) + + # Create model objects - one for each group, validation and test + group_0_train_data = df_train[df_train['group'] == 0] + group_1_train_data = df_train[df_train['group'] == 1] + + group_0_train_model = Model(group_0_train_data['prediction'].values, group_0_train_data['label'].values, self.threshold) + group_1_train_model = Model(group_1_train_data['prediction'].values, group_1_train_data['label'].values, self.threshold) + + # Find mixing rates for equalized odds models + _, _, mix_rate = Model.eq_odds(group_0_train_model, group_1_train_model, threshold=self.threshold, metric=self.metric) + self.model = (pa_i, mix_rate, self.metric) + + def predict(self, ds_predict): + """ + Bias mitigate with Equalized-Odds Model + + Parameters + ---------- + ds_predict : Dataset + Dataset containing predictions + + Returns + ---------- + ds_mitig_predict : Dataset + Bias-mitigated dataset + """ + pa_i = self.model[0] + mix_rate = self.model[1] + + np_test = np.concatenate([np.array(ds_predict.instance_names).reshape(-1, 1), + ds_predict.labels.reshape(-1, 1), # not used + ds_predict.protected_attributes[:, pa_i].reshape(-1, 1), + ds_predict.scores], 1) + df_test = pd.DataFrame(np_test, columns=['name', 'label', 'group', 'prediction']) + df_test['name'] = df_test['name'].astype(float) + df_test['label'] = df_test['label'].astype(float) + df_test['group'] = df_test['group'].astype(float) + df_test['prediction'] = df_test['prediction'].astype(float) + + group_0_test_data = df_test[df_test['group'] == 0] + group_1_test_data = df_test[df_test['group'] == 1] + group_0_test_model = Model(group_0_test_data['prediction'].values, group_0_test_data['label'].values, self.threshold) + group_1_test_model = Model(group_1_test_data['prediction'].values, group_1_test_data['label'].values, self.threshold) + + # Apply the mixing rates to the test models + eq_odds_group_0_test_model, eq_odds_group_1_test_model = Model.eq_odds(group_0_test_model, + group_1_test_model, + mix_rate, threshold=self.threshold, + metric=self.metric) + predictions = [] + i0 = i1 = 0 + for i, name in enumerate(ds_predict.instance_names): + pa = ds_predict.protected_attributes[i][pa_i] + if pa == 0: + predictions.append(eq_odds_group_0_test_model.pred[i0]) + i0 += 1 + elif pa == 1: + predictions.append(eq_odds_group_1_test_model.pred[i1]) + i1 += 1 + predictions = np.array(predictions) + ds_mitig_predict = ds_predict.copy(deepcopy=True) + ds_mitig_predict.scores = predictions.reshape(-1, 1) + + fav_inds = ds_mitig_predict.scores > self.threshold + ds_mitig_predict.labels[fav_inds] = ds_mitig_predict.favorable_label + ds_mitig_predict.labels[~fav_inds] = ds_mitig_predict.unfavorable_label + + return ds_mitig_predict diff --git a/aif360/algorithms/isf_helpers/postprocessing/postprocessing.py b/aif360/algorithms/isf_helpers/postprocessing/postprocessing.py new file mode 100644 index 00000000..49488d38 --- /dev/null +++ b/aif360/algorithms/isf_helpers/postprocessing/postprocessing.py @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABCMeta +from abc import abstractmethod + + +class PostProcessing(metaclass=ABCMeta): + """ + Abstract Base Class for all postprocessing techniques. + """ + + def __init__(self): + super().__init__() + self.model = None + + @abstractmethod + def fit(self, ds_train): + """ + Train a model on the input. + + Parameters + ---------- + ds_train : Dataset + Training Dataset + """ + pass + + @abstractmethod + def predict(self, ds): + """ + Predict on the input. + + Parameters + ---------- + ds : Dataset + Dataset to predict. + """ + pass diff --git a/aif360/algorithms/isf_helpers/postprocessing/reject_option_based_classification.py b/aif360/algorithms/isf_helpers/postprocessing/reject_option_based_classification.py new file mode 100644 index 00000000..ecc9b46a --- /dev/null +++ b/aif360/algorithms/isf_helpers/postprocessing/reject_option_based_classification.py @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import traceback + +from aif360.algorithms.postprocessing.reject_option_classification import RejectOptionClassification as ROC + +from aif360.algorithms.isf_helpers.isf_utils import const +from aif360.algorithms.isf_helpers.postprocessing.postprocessing import PostProcessing + + +class RejectOptionClassification(PostProcessing): + """ + Debiasing intersectional bias with RejectOptionClassification(ROC) extended by ISF. + + Parameters + ---------- + options : dictionary + parameter of Reject-Option-Classification + [metric, low_class_thresh, high_class_thresh, num_class_thresh, num_ROC_margin, metric_ub, low_class_thresh, , ] + + Notes + ---------- + https://aif360.readthedocs.io/en/latest/modules/generated/aif360.algorithms.postprocessing.RejectOptionClassification.html + """ + def __init__(self, options): + super().__init__() + metric = options['metric'] + if metric in const.DEMOGRAPHIC_PARITY: + self.metric = 'Statistical parity difference' + elif metric in const.EQUAL_OPPORTUNITY: + self.metric = 'Equal opportunity difference' + elif metric in const.EQUALIZED_ODDS: + self.metric = 'Average odds difference' + elif metric in const.F1_PARITY: + self.metric = 'F1 difference' + self.metric_ub = options['metric_ub'] if 'metric_ub' in options else 0.05 + self.metric_lb = options['metric_lb'] if 'metric_lb' in options else -0.05 + + def fit(self, ds_act, ds_predict): + """ + Make ROC model and fitting. + + Parameters + ---------- + ds_act : Dataset + Dataset containing ground-truth labels + ds_predict : Dataset + Dataset containing prediction + """ + ikey = ds_act.protected_attribute_names[0] + priv_g = [{ikey: ds_act.privileged_protected_attributes[0]}] + upriv_g = [{ikey: ds_act.unprivileged_protected_attributes[0]}] + model = ROC( + privileged_groups=priv_g, + unprivileged_groups=upriv_g, + low_class_thresh=0.01, high_class_thresh=0.99, + num_class_thresh=100, num_ROC_margin=50, + metric_name=self.metric, + metric_ub=self.metric_ub, metric_lb=self.metric_lb, + ) + self.model = model.fit(ds_act, ds_predict) + + def predict(self, ds_predict): + """ + Bias-mitigate with ROC model + + Parameters + ---------- + ds_predict : Dataset + Dataset containing prediction + + Returns + ---------- + ds_mitig_predict : Dataset + Bias-mitigated dataset + """ + ds_mitig_predict = self.model.predict(ds_predict) + return ds_mitig_predict diff --git a/aif360/algorithms/isf_helpers/preprocessing/checks.py b/aif360/algorithms/isf_helpers/preprocessing/checks.py new file mode 100644 index 00000000..ba5a3a24 --- /dev/null +++ b/aif360/algorithms/isf_helpers/preprocessing/checks.py @@ -0,0 +1,60 @@ +# Copyright (c) 2017 Niels Bantilan +# This software is the same as the original software licensed +# under the MIT License. +# +# https://github.com/cosmicBboy/themis-ml/blob/master/LICENSE +# +# +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions for doing checks.""" + + +def check_binary(x): + """ + Binary check. + + Parameters + ---------- + x : numpy.ndarray + Target + + Returns + ------- + x : numpy.ndarray + ValueError if not binary. + """ + if not is_binary(x): + raise ValueError("%s must be a binary variable" % x) + return x + + +def is_binary(x): + """ + Check if numpy multidimensional array consists of {0,1} + + Parameters + ---------- + x : numpy.ndarray + Target + + Returns + ------- + result : boolean + Check result + """ + return set(x.ravel()).issubset({0, 1}) diff --git a/aif360/algorithms/isf_helpers/preprocessing/massaging.py b/aif360/algorithms/isf_helpers/preprocessing/massaging.py new file mode 100644 index 00000000..7dc7d85f --- /dev/null +++ b/aif360/algorithms/isf_helpers/preprocessing/massaging.py @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from sklearn.linear_model import LogisticRegression +from sklearn.preprocessing import StandardScaler + +from aif360.algorithms.isf_helpers.preprocessing.preprocessing import PreProcessing +from aif360.algorithms.isf_helpers.preprocessing.relabelling import Relabeller + + +class Massaging(PreProcessing): + """ + Mitigate intersectional bias with Massaging extended by ISF. + + Parameters + ---------- + options : dictionary + parameter of Relabeller + sensitive attribute name + If not specified, 'ikey' + [protected_attribute_name] + """ + def __init__(self, options): + super().__init__() + self.protected_attribute_name = 'ikey' + if 'protected_attribute_name' in options: # isfを使用しない場合 + self.protected_attribute_name = options['protected_attribute_name'] + + def fit(self, ds_train): + """ + Make relabelling model + + Parameters + ---------- + ds_train : Dataset + Training dataset + """ + scale_orig = StandardScaler() + X = scale_orig.fit_transform(ds_train.features) + y = ds_train.labels.ravel() + i = ds_train.protected_attribute_names.index(self.protected_attribute_name) + s = ds_train.protected_attributes[:, i] + self.model = Relabeller(ranker=LogisticRegression()) + self.model.fit(X, y, s) + + def transform(self, ds): + """ + Debiasing with relabelling model + + Parameters + ---------- + ds : Dataset + Dataset containing labels that needs to be transformed + + Returns + ---------- + ds_mitig : Dataset + Bias-mitigated dataset + """ + ds_mitig = ds.copy(deepcopy=True) + scale_orig = StandardScaler() + X = scale_orig.fit_transform(ds.features) + Y = self.model.transform(X) + ds_mitig.scores = self.model.ranks_.reshape(-1, 1) + ds_mitig.labels = np.array(Y).reshape(-1, 1) + return ds_mitig diff --git a/aif360/algorithms/isf_helpers/preprocessing/preprocessing.py b/aif360/algorithms/isf_helpers/preprocessing/preprocessing.py new file mode 100644 index 00000000..1b0ec7cf --- /dev/null +++ b/aif360/algorithms/isf_helpers/preprocessing/preprocessing.py @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABCMeta +from abc import abstractmethod + + +class PreProcessing(metaclass=ABCMeta): + """ + Abstract Base Class for all preprocessing techniques. + """ + + def __init__(self): + super().__init__() + self.model = None + + @abstractmethod + def fit(self, ds_train): + """ + Train a model on the input. + + Parameters + ---------- + ds_train : Dataset + Training Dataset + """ + pass + + @abstractmethod + def transform(self, ds): + """ + Predict on the input. + + Parameters + ---------- + ds : Dataset + Dataset to predict. + """ + pass diff --git a/aif360/algorithms/isf_helpers/preprocessing/relabelling.py b/aif360/algorithms/isf_helpers/preprocessing/relabelling.py new file mode 100644 index 00000000..a63c3bd6 --- /dev/null +++ b/aif360/algorithms/isf_helpers/preprocessing/relabelling.py @@ -0,0 +1,140 @@ +# Copyright (c) 2017 Niels Bantilan +# This software includes modifications made by Fujitsu Limited to the original +# software licensed under the MIT License. Modified portions of this software +# are the modification of the condition to correct target labels especially in +# functions _n_relabels, _relabel and _relabel_targets. +# +# https://github.com/cosmicBboy/themis-ml/blob/master/LICENSE +# +# +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Relabel examples in a dataset for fairness-aware model training.""" + +import numpy as np +import math + +from sklearn.base import BaseEstimator, TransformerMixin, MetaEstimatorMixin +from sklearn.utils.validation import check_array, check_X_y, check_is_fitted +from sklearn.linear_model import LogisticRegression + +from aif360.algorithms.isf_helpers.preprocessing.checks import check_binary +from aif360.algorithms.isf_helpers.isf_utils.common import get_baseline + + +def _n_relabels(y, s): + """ + Compute the number of promotions/demotions that need to occur. + + Parameters + ---------- + y : np.array + Target labels + s : np.array + Sensitive class labels + + Returns + ------- + return value : int + Number of promotions/demotions to occur. + """ + total = float(len(s)) + s1 = s.sum() + s0 = total - s1 + s1_positive = ((s == 1) & (y == 1)).sum() + s0_positive = ((s == 0) & (y == 1)).sum() + # return int(math.ceil(((s1 * s0_positive) - (s0 * s1_positive)) / total)) + return int(math.ceil(((s0 * s1_positive) - (s1 * s0_positive)) / total)) + + +def _relabel(y, s, r, promote_ranks, demote_ranks, n_relabels): + if n_relabels > 0: + if ((not s and not y and r in promote_ranks) or + (s and y and r in demote_ranks)): + return int(not y) + else: + return y + else: + if ((s and not y and r in promote_ranks) or + (not s and y and r in demote_ranks)): + return int(not y) + else: + return y + + +def _relabel_targets(y, s, ranks, n_relabels): + """Compute relabelled targets based on predicted ranks.""" + if n_relabels > 0: + demote_ranks = set(sorted(ranks[(s == 1) & (y == 1)])[:n_relabels]) + promote_ranks = set(sorted(ranks[(s == 0) & (y == 0)])[-n_relabels:]) + else: + demote_ranks = set(sorted(ranks[(s == 0) & (y == 1)])[:-n_relabels]) + promote_ranks = set(sorted(ranks[(s == 1) & (y == 0)])[n_relabels:]) + return np.array([ + _relabel(_y, _s, _r, promote_ranks, demote_ranks, n_relabels) + for _y, _s, _r in zip(y, s, ranks)]) + + +class Relabeller(BaseEstimator, TransformerMixin, MetaEstimatorMixin): + + def __init__(self, ranker=LogisticRegression()): + """Create a Relabeller. + + This technique relabels target variables using a function that can + compute a decision boundary in input data space using the following + heuristic + + - The top `n` -ve labelled observations in the disadvantaged group `s1` + that are closest to the decision boundary are "promoted" to the +ve + label. + - the top `n` +ve labelled observations in the advantaged group s0 + closest to the decision boundary are "demoted' to the -ve label. + + `n` is the number of promotions/demotions needed to make + p(+|s0) = p(+|s1) + + :param BaseEstimator ranker: estimator to use as the ranker for + relabelling observations close to the decision boundary. Default: + LogisticRegression + """ + self.ranker = ranker + + def fit(self, X, y=None, s=None): + """Fit relabeller.""" + X, y = check_X_y(X, y) + y = check_binary(y) + s = check_binary(np.array(s).astype(int)) + if s.shape[0] != y.shape[0]: + raise ValueError("`s` must be the same shape as `y`") + self.n_relabels_ = _n_relabels(y, s) + self.ranks_ = self.ranker.fit(X, y).predict_proba(X)[:, 1] + best_accuracy = get_baseline(self.ranks_, y) + self.X_ = X + self.y_ = y + self.s_ = s + return self, best_accuracy + + def transform(self, X): + """Transform relabeller.""" + check_is_fitted(self, ["n_relabels_", "ranks_"]) + X = check_array(X) + # Input X should be equal to the input to `fit` + if not np.isclose(X, self.X_).all(): + raise ValueError( + "`transform` input X must be equal to input X to `fit`") + return _relabel_targets( + self.y_, self.s_, self.ranks_, self.n_relabels_) diff --git a/docs/source/modules/algorithms.rst b/docs/source/modules/algorithms.rst index 96a5ab0d..eaa102d3 100644 --- a/docs/source/modules/algorithms.rst +++ b/docs/source/modules/algorithms.rst @@ -73,3 +73,4 @@ Algorithms :template: class.rst algorithms.Transformer + algorithms.IntersectionalFairness diff --git a/examples/tutorial_isf.ipynb b/examples/tutorial_isf.ipynb new file mode 100644 index 00000000..fe33fd7b --- /dev/null +++ b/examples/tutorial_isf.ipynb @@ -0,0 +1,1403 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "cb83dd63", + "metadata": {}, + "source": [ + "# Tutorial\n", + "## See the effect of Intersectional Fairness (ISF) technology using RejectOptionClassification (ROC) and the AdultDataset\n", + "In this tutorial, we will detect the intersectional bias of the AdultDataset, improve the intersectional fairness with ISF, and demonstrate its effectiveness. \n", + "While ISF supports several mitigation methods, we now select RejectOptionClassification and extend it for intersectional fairness. \n", + "We will also compare ISF with ROC to explain ISF is suitable for retaining intersectional fairness." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6f7b1448", + "metadata": {}, + "source": [ + "#### RejectOptionClassification\n", + "Reject option classification is a postprocessing technique that gives favourable outcomes to unpriviliged groups and unfavourable outcomes to priviliged groups in a confidence band around the decision boundary with the highest uncertainty.\n", + "\n", + "References. \n", + "F. Kamiran, A. Karim, and X. Zhang, “Decision Theory for Discrimination-Aware Classification,” IEEE International Conference on Data Mining, 2012." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "61f434b3", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ece30760", + "metadata": {}, + "outputs": [], + "source": [ + "from pylab import rcParams" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b388ec32-95d0-4fae-8662-8501943e334b", + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-08-21 11:46:26.407880: I tensorflow/core/util/port.cc:153] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.\n", + "2024-08-21 11:46:26.418550: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:485] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered\n", + "2024-08-21 11:46:26.430318: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:8454] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered\n", + "2024-08-21 11:46:26.434087: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1452] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered\n", + "2024-08-21 11:46:26.445493: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.\n", + "To enable the following instructions: AVX2 AVX512F AVX512_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.\n", + "2024-08-21 11:46:27.146813: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT\n", + "/home/vboxuser/.local/lib/python3.10/site-packages/inFairness/utils/ndcg.py:37: FutureWarning: We've integrated functorch into PyTorch. As the final step of the integration, `functorch.vmap` is deprecated as of PyTorch 2.0 and will be deleted in a future version of PyTorch >= 2.3. Please use `torch.vmap` instead; see the PyTorch 2.0 release notes and/or the `torch.func` migration guide for more details https://pytorch.org/docs/main/func.migrating.html\n", + " vect_normalized_discounted_cumulative_gain = vmap(\n", + "/home/vboxuser/.local/lib/python3.10/site-packages/inFairness/utils/ndcg.py:48: FutureWarning: We've integrated functorch into PyTorch. As the final step of the integration, `functorch.vmap` is deprecated as of PyTorch 2.0 and will be deleted in a future version of PyTorch >= 2.3. Please use `torch.vmap` instead; see the PyTorch 2.0 release notes and/or the `torch.func` migration guide for more details https://pytorch.org/docs/main/func.migrating.html\n", + " monte_carlo_vect_ndcg = vmap(vect_normalized_discounted_cumulative_gain, in_dims=(0,))\n" + ] + } + ], + "source": [ + "from aif360.algorithms.intersectional_fairness import IntersectionalFairness\n", + "from aif360.algorithms.isf_helpers.isf_utils.common import output_subgroup_metrics, convert_labels, create_multi_group_label\n", + "from aif360.algorithms.isf_helpers.isf_analysis.intersectional_bias import calc_intersectionalbias, plot_intersectionalbias_compare\n", + "from aif360.algorithms.isf_helpers.isf_analysis.metrics import check_metrics_combination_attribute, check_metrics_single_attribute" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4fd1a618", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d24d6739", + "metadata": {}, + "source": [ + "## Set up AdultDataset\n", + "Note: To download and install the AdultDataset manually, follow [README.md in AIF360](https://github.com/Trusted-AI/AIF360/tree/master/aif360/data). " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c6258781", + "metadata": {}, + "outputs": [], + "source": [ + "from aif360.datasets import AdultDataset" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "55ccad01", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:root:Missing Data: 3620 rows removed from AdultDataset.\n" + ] + } + ], + "source": [ + "dataset = AdultDataset()\n", + "convert_labels(dataset)\n", + "ds_train, ds_test = dataset.split([0.7])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "bc8d7f02", + "metadata": {}, + "source": [ + "### Ensure what attributes are protected \n", + "To verify intersectional bias, you need to specify two attributes in the Dataset as protected ones. \n", + "AdultDataset has already specified the following two attributes as protected: " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c5c26fd4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['race', 'sex']" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset.protected_attribute_names" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1aaea4b3", + "metadata": {}, + "source": [ + "## Classification \n", + "We first build a classification model using ROC which is a postprocessing algorithm. \n", + "We train a Logistic Regression model with data ds_train and then we proceed with classification for data ds_test. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "5a9a4b91", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.preprocessing import StandardScaler\n", + "\n", + "scale_orig = StandardScaler()\n", + "X_train = scale_orig.fit_transform(ds_train.features)\n", + "y_train = ds_train.labels.ravel()\n", + "X_test = scale_orig.transform(ds_test.features)\n", + "Y_test = ds_test.labels.ravel()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "81f3684e", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
LogisticRegression()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + ], + "text/plain": [ + "LogisticRegression()" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from sklearn.linear_model import LogisticRegression\n", + "\n", + "lr = LogisticRegression()\n", + "lr.fit(X_train, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "eb262b92", + "metadata": {}, + "outputs": [], + "source": [ + "ds_test_classified = ds_test.copy()\n", + "pos_ind = np.where(lr.classes_ == ds_train.favorable_label)[0][0]\n", + "\n", + "ds_test_classified.scores = lr.predict_proba(X_test)[:, pos_ind].reshape(-1, 1)\n", + "ds_test_classified.labels = lr.predict(X_test).reshape(-1, 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b1dc4d34", + "metadata": {}, + "outputs": [], + "source": [ + "# Calculate ds_train_classified for ROC\n", + "ds_train_classified = ds_train.copy()\n", + "pos_ind = np.where(lr.classes_ == ds_train.favorable_label)[0][0]\n", + "\n", + "ds_train_classified.scores = lr.predict_proba(X_train)[:, pos_ind].reshape(-1, 1)\n", + "ds_train_classified.labels = lr.predict(X_train).reshape(-1, 1)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "dc7811c1", + "metadata": {}, + "source": [ + "### Confirm the model performance \n", + "#### (1) Measure the performance for classification\n", + "Check the performance for classification using the above classification results. " + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "71f1958d", + "metadata": {}, + "outputs": [], + "source": [ + "df_acc = pd.DataFrame(columns=['Accuracy','Precision','Recall','F1 score'])" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "39b7d958", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score\n", + "df_acc.loc['LR Model']=(\n", + " accuracy_score(y_true=Y_test, y_pred=ds_test_classified.labels),\n", + " precision_score(y_true=Y_test, y_pred=ds_test_classified.labels),\n", + " recall_score(y_true=Y_test, y_pred=ds_test_classified.labels),\n", + " f1_score(y_true=Y_test, y_pred=ds_test_classified.labels)\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "85fcd4c0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
AccuracyPrecisionRecallF1 score
LR Model0.8488240.7441690.6078550.66914
\n", + "
" + ], + "text/plain": [ + " Accuracy Precision Recall F1 score\n", + "LR Model 0.848824 0.744169 0.607855 0.66914" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_acc" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6761827e", + "metadata": {}, + "source": [ + "* The model achieves enough accuracy since `Accuracy` is 84%." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "519268c9", + "metadata": {}, + "source": [ + "#### (2) Measure disparate impact to see intersectional bias caused by the combination of two attributes, `race` and `sex`\n", + "Check intersectional bias with disparate impact (DI). " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "0007d28c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
LR Model
race = 1.0,sex = 1.02.810223
race = 1.0,sex = 0.00.352248
race = 0.0,sex = 1.00.892224
race = 0.0,sex = 0.00.190748
\n", + "
" + ], + "text/plain": [ + " LR Model\n", + "race = 1.0,sex = 1.0 2.810223\n", + "race = 1.0,sex = 0.0 0.352248\n", + "race = 0.0,sex = 1.0 0.892224\n", + "race = 0.0,sex = 0.0 0.190748" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_lr_di = calc_intersectionalbias(ds_test_classified, \"DisparateImpact\")\n", + "df_lr_di = df_lr_di.rename(columns={\"DisparateImpact\": \"LR Model\"})\n", + "df_lr_di" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "3ff75d14", + "metadata": {}, + "source": [ + "* The model requires bias mitigation because DIs for groups other than race=0.0 and sex=1.0 are out of the range for fairness. \n", + "\n", + " Note: \n", + " In the recruitment field in the US, there is a law saying it is fair if the DI is 0.8 or more (and equal to or less than 1.25, the reciprocal of 0.8), so we consider 0.8 as a standard threshold of fairness. " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d220fe7d", + "metadata": {}, + "source": [ + "## Run ISF\n", + "Mitigate intersectional bias in this LR model's judgment. \n", + "You can use the ROC algorithm, a post-processing method, for it. Run the mitigation algorithm of ISF specifying \"RejectOptionClassification\" as a parameter. " + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "7ed1a91c-1779-47fa-bf9c-58cb36707f64", + "metadata": {}, + "outputs": [], + "source": [ + "ID = IntersectionalFairness('RejectOptionClassification', 'DemographicParity', \n", + " accuracy_metric='F1', options={'metric_ub':0.2, 'metric_lb':-0.2})" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "2430417e", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:root:[0.0] listed but not observed for feature sex\n", + "WARNING:root:[0.0] listed but not observed for feature race\n", + "WARNING:root:[1.0] listed but not observed for feature sex\n", + "WARNING:root:[1.0] listed but not observed for feature race\n" + ] + } + ], + "source": [ + "# training can take several minutes depending on hardware\n", + "ID.fit(ds_train, dataset_predicted=ds_train_classified)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "f9c4cb64", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:root:[0.0] listed but not observed for feature sex\n", + "WARNING:root:[0.0] listed but not observed for feature race\n", + "WARNING:root:[1.0] listed but not observed for feature sex\n", + "WARNING:root:[1.0] listed but not observed for feature race\n" + ] + } + ], + "source": [ + "# predict\n", + "ds_predicted = ID.predict(ds_test_classified)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "8483bcc3", + "metadata": {}, + "source": [ + "## Evaluation" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "16dee42f", + "metadata": {}, + "source": [ + "### (1) Compare the raw LR model and the model mitigated by ISF \n", + "Measure and visualize DIs for intersectional bias to check the effect of ISF. " + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "fe18fbfa", + "metadata": {}, + "outputs": [], + "source": [ + "combattr_metrics_isf = check_metrics_combination_attribute(ds_test, ds_predicted)[['base_rate', 'selection_rate', 'Balanced_Accuracy']]" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "6a5b15b5", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import seaborn as sns\n", + "rcParams['figure.figsize'] = 6,2\n", + "sns.set(font_scale = 0.7)\n", + "plot_intersectionalbias_compare(ds_test_classified,\n", + " ds_predicted,\n", + " vmax=2, vmin=0, center=1,\n", + " title={\"right\": \"LR Model\", \"left\": \"ISF(ROC is used)\"})" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7162a5fb", + "metadata": {}, + "source": [ + "* Compared to the left-hand quadrant for the LR model with no bias mitigation, DI in each subgroup gets closer to 1.0 in the right-hand quadrant for the model mitigated by ISF. \n", + "* This indicates that the judgment result of the model with ISF is closer to fair than that without ISF. " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a827905f", + "metadata": {}, + "source": [ + "Next, ensure ISF does not degrade the accuracy of the model. " + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "f3ed6a81", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score\n", + "df_acc.loc['ISF (ROC is used)']=(accuracy_score(y_true=Y_test, y_pred=ds_predicted.labels),\n", + " precision_score(y_true=Y_test, y_pred=ds_predicted.labels),\n", + " recall_score(y_true=Y_test, y_pred=ds_predicted.labels),\n", + " f1_score(y_true=Y_test, y_pred=ds_predicted.labels) )" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "b1a5aa4d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
AccuracyPrecisionRecallF1 score
LR Model0.8488240.7441690.6078550.669140
ISF (ROC is used)0.8306920.6939130.5847010.634643
\n", + "
" + ], + "text/plain": [ + " Accuracy Precision Recall F1 score\n", + "LR Model 0.848824 0.744169 0.607855 0.669140\n", + "ISF (ROC is used) 0.830692 0.693913 0.584701 0.634643" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_acc" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "53b11079", + "metadata": {}, + "source": [ + "* `Accuracy` before and after bias mitigation are almost the same. \n", + "* This indicates ISF can mitigate intersectional fairness with only minor accuracy degradation. " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "85d3578c", + "metadata": {}, + "source": [ + "### (2) Comparison of ROC and ISF (ROC is used)\n", + "Now compare the effects between ROC and ISF-leveraged ROC. \n", + "Run ROC under the same condition as the ISF-leveraged ROC. " + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "b91aa562", + "metadata": {}, + "outputs": [], + "source": [ + "from aif360.algorithms.postprocessing.reject_option_classification import RejectOptionClassification\n", + " \n", + "ROC = RejectOptionClassification(\n", + " privileged_groups=[{'race':1,'sex':1}],\n", + " unprivileged_groups=[{'race':0,'sex':0},{'race':0,'sex':1},{'race':1,'sex':0}],\n", + " low_class_thresh=0.01, \n", + " high_class_thresh=0.99,\n", + " num_class_thresh=100, \n", + " num_ROC_margin=50,\n", + " metric_name='Statistical parity difference',\n", + " metric_ub=0.2,\n", + " metric_lb=-0.2)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "1ab6402a", + "metadata": {}, + "outputs": [], + "source": [ + "# training\n", + "ROC.fit(ds_train, ds_train_classified)\n", + "# predict\n", + "ds_predicted_roc = ROC.predict(ds_test_classified)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d61f78e7", + "metadata": {}, + "source": [ + "#### Check intersectional bias for ROC " + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "e12f9ac8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
LR ModelROC
race = 1.0,sex = 1.02.8102231.753728
race = 1.0,sex = 0.00.3522480.538010
race = 0.0,sex = 1.00.8922241.264260
race = 0.0,sex = 0.00.1907480.324319
\n", + "
" + ], + "text/plain": [ + " LR Model ROC\n", + "race = 1.0,sex = 1.0 2.810223 1.753728\n", + "race = 1.0,sex = 0.0 0.352248 0.538010\n", + "race = 0.0,sex = 1.0 0.892224 1.264260\n", + "race = 0.0,sex = 0.0 0.190748 0.324319" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_roc_di =calc_intersectionalbias(ds_predicted_roc, \"DisparateImpact\")\n", + "df_roc_di = df_roc_di.rename(columns={\"DisparateImpact\": \"ROC\"})\n", + "df_lr_di[\"ROC\"]=df_roc_di[\"ROC\"]\n", + "df_lr_di" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a55eca5b", + "metadata": {}, + "source": [ + "* Since ROC does not support intersectional bias, DI values for groups are out of the fairness range though they tend to improve." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1ee272aa", + "metadata": {}, + "source": [ + "#### Compare DI values for `ROC` and `ISF (ROC is used)`\n", + "Finally, compare DI values ordinal ROC and `ISF (ROC is used)` achieve. " + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "611ccd2b", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_intersectionalbias_compare(ds_predicted_roc,\n", + " ds_predicted,\n", + " vmax=2, vmin=0, center=1,\n", + " title={\"right\": \"RoC\", \"left\": \"ISF(used RoC)\"})" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "eb02bdcc", + "metadata": {}, + "source": [ + "* Compared to `ROC` (the left-hand quadrant), `ISF (ROC is used)` (ROC-leveraged ISF; the right-hand quadrant) achieves a better bias mitigation. \n", + "* We have confirmed ISF helps ROC mitigate intersectional bias. " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "90fe261e", + "metadata": {}, + "source": [ + "#### Compare accuracies for `ROC` and `ISF (ROC is used)`" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "9bcf7d12", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score\n", + "df_acc.loc['ROC']=(\n", + " accuracy_score(y_true=Y_test, y_pred=ds_predicted_roc.labels),\n", + " precision_score(y_true=Y_test, y_pred=ds_predicted_roc.labels),\n", + " recall_score(y_true=Y_test, y_pred=ds_predicted_roc.labels),\n", + " f1_score(y_true=Y_test, y_pred=ds_predicted_roc.labels)\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "b4dc8958", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
AccuracyPrecisionRecallF1 score
LR Model0.8488240.7441690.6078550.669140
ISF (ROC is used)0.8306920.6939130.5847010.634643
ROC0.8058520.5783960.8411490.685455
\n", + "
" + ], + "text/plain": [ + " Accuracy Precision Recall F1 score\n", + "LR Model 0.848824 0.744169 0.607855 0.669140\n", + "ISF (ROC is used) 0.830692 0.693913 0.584701 0.634643\n", + "ROC 0.805852 0.578396 0.841149 0.685455" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_acc" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "ec347540", + "metadata": {}, + "source": [ + "* Accuracies for `ISF (ROC is used)` and `ROC` are almost the same. \n", + "* This indicates extending RoC with ISF does not cause significant accuracy degradation. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/test_isf.py b/tests/test_isf.py new file mode 100644 index 00000000..28a15b07 --- /dev/null +++ b/tests/test_isf.py @@ -0,0 +1,304 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import pandas as pd +from pandas.testing import assert_frame_equal + +from logging import CRITICAL, getLogger +from os import environ +# Suppress warnings that tensorflow emits +environ['TF_CPP_MIN_LOG_LEVEL'] = '3' + +import sys +import traceback +from collections import deque +import threading + +from aif360.datasets import CompasDataset + +from aif360.algorithms.intersectional_fairness import IntersectionalFairness +from aif360.algorithms.isf_helpers.isf_utils.common import classify, output_subgroup_metrics, convert_labels, create_multi_group_label +#from stream import MuteStdout + +#MODEL_ANSWER_PATH = './results/' + +class MuteStdout: + """Suppress message emission to stdout.""" + def __init__(self, debug=False): + self.org_stdout = sys.stdout + self.out_buffer = DevNull(debug=debug) + self.debug = debug + def __enter__(self): + sys.stdout = self.out_buffer + return self + def __exit__(self, ex_type, ex_value, tracebac): + sys.stdout = self.org_stdout + if ex_value is not None: + if self.debug: + print(f'[OUTPUT start] thread{threading.current_thread().name}({threading.current_thread().ident}) queue={id(self.out_buffer.queue)} len={len(self.out_buffer.queue)}') + for message in self.out_buffer.queue: + sys.stdout.write(message) + if self.debug: + print('[OUTPUT end]', flush=True) + if tracebac is not None: + traceback.print_exception(ex_type, ex_value, tracebac) + if ex_value is not None: + raise ex_value + + +class DevNull: + """Output stream to /dev/null.""" + def __init__(self, debug=False): + self.debug = debug + if self.debug: + print('***DEVNULL INITIALIZED **************', flush=True, file=sys.stderr) + self.queue = deque() + def write(self, message): + self.queue.append(message) + if self.debug: + print(f'thread{threading.current_thread().name}({threading.current_thread().ident}) queue={id(self.queue)} len={len(self.queue)}', end=' \r', file=sys.stderr) + # print(f'thread{threading.current_thread().name}({threading.current_thread().ident}) queue={id(self.queue)} len={len(self.queue)} {message}', end='', file=sys.stderr) + def flush(self): + pass + + +class TestStringMethods(unittest.TestCase): + + def __init__(self, methodName='runTest'): + super().__init__(methodName=methodName) + + def _read_modelanswer(self, s_result_singleattr, s_result_combattr): + # load of model answer + ma_singleattr_bias = pd.read_csv(MODEL_ANSWER_PATH + s_result_singleattr, index_col=0) + ma_combattr_bias = pd.read_csv(MODEL_ANSWER_PATH + s_result_combattr, index_col=0) + return ma_singleattr_bias, ma_combattr_bias + + def _comp_dataframe(self, df1, df2): + try: + assert_frame_equal(df1, df2) + except AssertionError: + return False + return True + + def _pickup_result(self, df_singleattr, df_combattr): + # load of model answer + result_singleattr_bias = df_singleattr[['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']] + result_combattr_bias = df_combattr[['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']] + return result_singleattr_bias, result_combattr_bias + + def setUp(self): + getLogger().setLevel(CRITICAL) + + # load test dataset + self.dataset = CompasDataset() + #consider a small part of the dataset for testing + self.dataset, _ = self.dataset.split([0.1], shuffle=False, seed=1) + convert_labels(self.dataset) + self.ds_train, self.ds_test = self.dataset.split([0.7], shuffle=False, seed=1) + + def test01_AdversarialDebiasing(self): + s_algorithm = 'AdversarialDebiasing' + s_metrics = 'DemographicParity' + + # test + with MuteStdout(): + ID = IntersectionalFairness(s_algorithm, s_metrics) + ID.fit(self.ds_train) + ds_predicted = ID.predict(self.ds_test) + + group_protected_attrs, label_unique_nums = create_multi_group_label(self.dataset) + g_metrics, sg_metrics = output_subgroup_metrics(self.ds_test, ds_predicted, group_protected_attrs) + + # pickup + result_singleattr_bias, result_combattr_bias = self._pickup_result(g_metrics, sg_metrics) + + # expected values + ma_singleattr_bias = pd.DataFrame( + [['total', 0.5783783783783784, 0.5621621621621622, 0.6756530074287084], + ['sex:0.0', 0.5512820512820513, 0.5833333333333334, 0.6792358803986711], + ['sex:1.0', 0.7241379310344828, 0.4482758620689655, 0.7232142857142857], + ['race:0.0', 0.5785123966942148, 0.5537190082644629, 0.707422969187675], + ['race:1.0', 0.578125, 0.578125, 0.6156156156156156]], + columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) + + ma_combattr_bias = pd.DataFrame( + [['total', 0.5783783783783784, 0.5621621621621622, 0.6756530074287084], + ['sex:0.0_race:0.0', 0.5462962962962963, 0.5370370370370371, 0.6926668972673815], + ['sex:0.0_race:1.0', 0.5625, 0.6875, 0.6455026455026455], + ['sex:1.0_race:0.0', 0.8461538461538461, 0.6923076923076923, 0.9090909090909092], + ['sex:1.0_race:1.0', 0.625, 0.25, 0.5666666666666667]], + columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) + + #assert + assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.1) + assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.1) + + def test02_EqualizedOdds(self): + s_algorithm = 'EqualizedOddsPostProcessing' + s_metrics = 'EqualizedOdds' + + # test + ds_train_classified, threshold, _ = classify(self.ds_train, self.ds_train) + ds_test_classified, _, _ = classify(self.ds_train, self.ds_test, threshold=threshold) + + ID = IntersectionalFairness(s_algorithm, s_metrics) + ID.fit(self.ds_train, dataset_predicted=ds_train_classified, options={'threshold': threshold}) + ds_predicted = ID.predict(ds_test_classified) + + group_protected_attrs, label_unique_nums = create_multi_group_label(self.dataset) + g_metrics, sg_metrics = output_subgroup_metrics(self.ds_test, ds_predicted, group_protected_attrs) + + # pickup + result_singleattr_bias, result_combattr_bias = self._pickup_result(g_metrics, sg_metrics) + + # expected values + ma_singleattr_bias = pd.DataFrame( + [['total', 0.5783783783783784, 0.572972972972973, 0.5852504193625689], + ['sex:0.0', 0.5512820512820513, 0.5512820512820513, 0.5853820598006645], + ['sex:1.0', 0.7241379310344828, 0.6896551724137931, 0.5446428571428572], + ['race:0.0', 0.5785123966942148, 0.5619834710743802, 0.5959383753501402], + ['race:1.0', 0.578125, 0.59375, 0.565065065065065]], + columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) + + ma_combattr_bias = pd.DataFrame( + [['total', 0.5783783783783784, 0.572972972972973, 0.5852504193625689], + ['sex:0.0_race:0.0', 0.5462962962962963, 0.5370370370370371, 0.599273607748184], + ['sex:0.0_race:1.0', 0.5625, 0.5833333333333334, 0.5529100529100529], + ['sex:1.0_race:0.0', 0.8461538461538461, 0.7692307692307693, 0.3636363636363636], + ['sex:1.0_race:1.0', 0.625, 0.625, 0.6]], + columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) + + #assert + assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.1) + assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.1) + + def test03_Massaging(self): + s_algorithm = 'Massaging' + s_metrics = 'DemographicParity' + + ID = IntersectionalFairness(s_algorithm, s_metrics) + ID.fit(self.ds_train) + ds_predicted = ID.transform(self.ds_train) + + group_protected_attrs, label_unique_nums = create_multi_group_label(self.dataset) + g_metrics, sg_metrics = output_subgroup_metrics(self.ds_train, ds_predicted, group_protected_attrs) + + # pickup + result_singleattr_bias, result_combattr_bias = self._pickup_result(g_metrics, sg_metrics) + + # expected values + ma_singleattr_bias = pd.DataFrame( + [['total', 0.5406032482598608, 0.5359628770301624, 0.9443252265140676], + ['sex:0.0', 0.5, 0.5316091954022989, 0.9683908045977012], + ['sex:1.0', 0.7108433734939759, 0.5542168674698795, 0.8898305084745763], + ['race:0.0', 0.5272727272727272, 0.5272727272727272, 0.9416445623342176], + ['race:1.0', 0.5641025641025641, 0.5512820512820513, 0.9495320855614974]], + columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) + + ma_combattr_bias = pd.DataFrame( + [['total', 0.5406032482598608, 0.5359628770301624, 0.9443252265140676], + ['sex:0.0_race:0.0', 0.4845814977973568, 0.5198237885462555, 0.9658119658119658], + ['sex:0.0_race:1.0', 0.5289256198347108, 0.5537190082644629, 0.9736842105263156], + ['sex:1.0_race:0.0', 0.7291666666666666, 0.5625, 0.8857142857142857], + ['sex:1.0_race:1.0', 0.6857142857142857, 0.5428571428571428, 0.8958333333333333]], + columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) + + #assert + assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.1) + assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.1) + + def test04_RejectOptionClassification(self): + s_algorithm = 'RejectOptionClassification' + s_metrics = 'DemographicParity' + + ds_train_classified, threshold, _ = classify(self.ds_train, self.ds_train) + ds_test_classified, _, _ = classify(self.ds_train, self.ds_test, threshold=threshold) + + group_protected_attrs, label_unique_nums = create_multi_group_label(self.dataset) + + ID = IntersectionalFairness(s_algorithm, s_metrics, max_workers=2, + accuracy_metric='F1', options={'metric_ub': 0.2, 'metric_lb': -0.2}) + ID.fit(self.ds_train, dataset_predicted=ds_train_classified) + ds_predicted = ID.predict(ds_test_classified) + + g_metrics, sg_metrics = output_subgroup_metrics(self.ds_test, ds_predicted, group_protected_attrs) + + # pickup + result_singleattr_bias, result_combattr_bias = self._pickup_result(g_metrics, sg_metrics) + + # expected values + ma_singleattr_bias = pd.DataFrame( + [['total', 0.5783783783783784, 0.5567567567567567, 0.5712317277737838], + ['sex:0.0', 0.5512820512820513, 0.5705128205128205, 0.5898671096345516], + ['sex:1.0', 0.7241379310344828, 0.4827586206896552, 0.4880952380952381], + ['race:0.0', 0.5785123966942148, 0.5785123966942148, 0.5932773109243697], + ['race:1.0', 0.578125, 0.515625, 0.5295295295295295]], + columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) + + ma_combattr_bias = pd.DataFrame( + [['total', 0.5783783783783784, 0.5567567567567567, 0.5712317277737838], + ['sex:0.0_race:0.0', 0.5462962962962963, 0.5648148148148148, 0.6060186786579038], + ['sex:0.0_race:1.0', 0.5625, 0.5833333333333334, 0.5529100529100529], + ['sex:1.0_race:0.0', 0.8461538461538461, 0.6923076923076923, 0.3181818181818182], + ['sex:1.0_race:1.0', 0.625, 0.3125, 0.4833333333333333]], + columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) + + #assert + assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.1) + assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.1) + + def test05_Massaging_AA(self): + s_algorithm = 'Massaging' + s_metrics = 'DemographicParity' + + debiasing_conditions = [{'target_attrs': {'sex': 1.0, 'race': 0.0}, 'uld_a': 0.8, 'uld_b': 1.2, 'probability': 1.0}] + + ID = IntersectionalFairness(s_algorithm, s_metrics, + debiasing_conditions=debiasing_conditions, instruct_debiasing=True) + ID.fit(self.ds_train) + ds_predicted = ID.transform(self.ds_train) + + group_protected_attrs, _ = create_multi_group_label(self.dataset) + g_metrics, sg_metrics = output_subgroup_metrics(self.ds_train, ds_predicted, group_protected_attrs) + + # pickup + result_singleattr_bias, result_combattr_bias = self._pickup_result(g_metrics, sg_metrics) + + # expected values + ma_singleattr_bias = pd.DataFrame( + [['total', 0.5406032482598608, 0.5614849187935035, 0.9772727272727272], + ['sex:0.0', 0.5, 0.5, 1.0], + ['sex:1.0', 0.7108433734939759, 0.8192771084337349, 0.8125], + ['race:0.0', 0.5272727272727272, 0.56, 0.9653846153846154], + ['race:1.0', 0.5641025641025641, 0.5641025641025641, 1.0]], + columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) + + ma_combattr_bias = pd.DataFrame( + [['total', 0.5406032482598608, 0.5614849187935035, 0.9772727272727272], + ['sex:0.0_race:0.0', 0.4845814977973568, 0.4845814977973568, 1.0], + ['sex:0.0_race:1.0', 0.5289256198347108, 0.5289256198347108, 1.0], + ['sex:1.0_race:0.0', 0.7291666666666666, 0.9166666666666666, 0.6538461538461539], + ['sex:1.0_race:1.0', 0.6857142857142857, 0.6857142857142857, 1.0]], + columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) + + #assert + assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.1) + assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.1) + + +if __name__ == "__main__": + unittest.main() From 86c141da74b119eb964a709fdb9a156ab941281f Mon Sep 17 00:00:00 2001 From: Kalousios Date: Mon, 26 Aug 2024 12:06:33 +0100 Subject: [PATCH 2/3] remove unnecessary comments, clean code, bug fixes Signed-off-by: ckalousi --- aif360/algorithms/intersectional_fairness.py | 13 ++------- .../isf_helpers/postprocessing/eq_odds.py | 2 -- examples/tutorial_isf.ipynb | 11 +++++++- tests/test_isf.py | 27 +++++++++---------- 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/aif360/algorithms/intersectional_fairness.py b/aif360/algorithms/intersectional_fairness.py index 27fb219d..0a936cfc 100644 --- a/aif360/algorithms/intersectional_fairness.py +++ b/aif360/algorithms/intersectional_fairness.py @@ -167,8 +167,6 @@ def fit(self, dataset_actual, dataset_predicted=None, dataset_valid=None, option self.logger.debug('fitting...') - # TODO need to fix sorting sensitive attributes - # thres_sort = sorted(thres.items(), key=lambda x:x[0]) # Fixed order of sensitive attributes if dataset_valid is None: if self.approach_type == 'PostProcessing': dataset_valid = dataset_predicted.copy(deepcopy=True) @@ -267,7 +265,6 @@ def _worker(self, ids): group2_idx = ids[1] # Determine privileged/non-privileged group (necessary for some algorithms) # (used demographic parity) - # print('start: ' + str(group1_idx) + str(group2_idx)) cl_metric = BinaryLabelDatasetMetric(self.dataset_actual, unprivileged_groups=self.group_protected_attrs[group2_idx], privileged_groups=self.group_protected_attrs[group1_idx]) @@ -534,8 +531,7 @@ def _create_stat_table(self, dataset_act, dataset_target, dataset_target_tmp, uf TNR = -1 if math.isnan(m_sg_mitig.true_negative_rate(privileged=True)) else m_sg_mitig.true_negative_rate(privileged=True) bal_acc = -1 if TPR == -1 or TNR == -1 else (TPR + TNR) * 0.5 precision = -1 if math.isnan(m_sg_mitig.precision(privileged=True)) else m_sg_mitig.precision(privileged=True) - # TODO Warning if recall precision=0 - f1 = -1 if precision == -1 or TPR == -1 else 2 * TPR * precision / (TPR + precision) + f1 = -1 if precision == -1 or TPR == -1 or (TPR + precision) == 0 else 2 * TPR * precision / (TPR + precision) metrics = [uf_t, m_sg_mitig.num_positives(privileged=True), @@ -553,7 +549,7 @@ def _create_stat_table(self, dataset_act, dataset_target, dataset_target_tmp, uf f1, m_sg_mitig.selection_rate(privileged=True), difference, - ratio] # TODO Combine with selection_rate after classification + ratio] stat_table.append(protected_attribute_values + metrics) return stat_table @@ -732,8 +728,6 @@ def _split_group(self, dataset, unprivileged_protected_attributes=[], privileged df=enable_df, label_names=dataset.label_names, protected_attribute_names=dataset.protected_attribute_names, -# privileged_protected_attributes=privileged_protected_attributes_keys, -# unprivileged_protected_attributes=unprivileged_protected_attributes_keys, favorable_label=dataset.favorable_label, unfavorable_label=dataset.unfavorable_label) @@ -763,9 +757,6 @@ def _split_group(self, dataset, unprivileged_protected_attributes=[], privileged for i1 in range(len(dataset.instance_names)): idx = sortlist.get(dataset.instance_names[i1]) if idx is not None: - #disable_df['labels'][idx] = dataset.labels[i1] - #disable_df['scores'][idx] = dataset.scores[i1] - #disable_df['instance_weights'][idx] = dataset.instance_weights[i1] disable_df.loc[idx,'labels'] = dataset.labels[i1] disable_df.loc[idx,'scores'] = dataset.scores[i1] disable_df.loc[idx,'instance_weights'] = dataset.instance_weights[i1] diff --git a/aif360/algorithms/isf_helpers/postprocessing/eq_odds.py b/aif360/algorithms/isf_helpers/postprocessing/eq_odds.py index 1b403357..e19dde6f 100644 --- a/aif360/algorithms/isf_helpers/postprocessing/eq_odds.py +++ b/aif360/algorithms/isf_helpers/postprocessing/eq_odds.py @@ -156,8 +156,6 @@ def eq_odds(self, othr, mix_rates=None, threshold=None, metric='EqualOpportunity self_fair_pred[p2n_indices] = 1 - self_fair_pred[p2n_indices] othr_fair_pred = othr.pred.copy() - # othr_pp_indices, = np.nonzero(othr.pred.round()) - # othr_pn_indices, = np.nonzero(1 - othr.pred.round()) othr_pp_indices, = np.nonzero(othr.pred > othr.threshold) othr_pn_indices, = np.nonzero(1 - (othr.pred <= othr.threshold)) np.random.shuffle(othr_pp_indices) diff --git a/examples/tutorial_isf.ipynb b/examples/tutorial_isf.ipynb index fe33fd7b..f3c03cc8 100644 --- a/examples/tutorial_isf.ipynb +++ b/examples/tutorial_isf.ipynb @@ -878,6 +878,10 @@ "execution_count": 17, "id": "2430417e", "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + }, "scrolled": true }, "outputs": [ @@ -901,7 +905,12 @@ "cell_type": "code", "execution_count": 18, "id": "f9c4cb64", - "metadata": {}, + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } + }, "outputs": [ { "name": "stderr", diff --git a/tests/test_isf.py b/tests/test_isf.py index 28a15b07..1b96c22f 100644 --- a/tests/test_isf.py +++ b/tests/test_isf.py @@ -20,7 +20,7 @@ from logging import CRITICAL, getLogger from os import environ -# Suppress warnings that tensorflow emits +# Suppress warnings that tensorflow generates environ['TF_CPP_MIN_LOG_LEVEL'] = '3' import sys @@ -32,9 +32,6 @@ from aif360.algorithms.intersectional_fairness import IntersectionalFairness from aif360.algorithms.isf_helpers.isf_utils.common import classify, output_subgroup_metrics, convert_labels, create_multi_group_label -#from stream import MuteStdout - -#MODEL_ANSWER_PATH = './results/' class MuteStdout: """Suppress message emission to stdout.""" @@ -144,8 +141,8 @@ def test01_AdversarialDebiasing(self): columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) #assert - assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.1) - assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.1) + assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.2) + assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.2) def test02_EqualizedOdds(self): s_algorithm = 'EqualizedOddsPostProcessing' @@ -181,10 +178,10 @@ def test02_EqualizedOdds(self): ['sex:1.0_race:0.0', 0.8461538461538461, 0.7692307692307693, 0.3636363636363636], ['sex:1.0_race:1.0', 0.625, 0.625, 0.6]], columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) - + #assert - assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.1) - assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.1) + assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.2) + assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.2) def test03_Massaging(self): s_algorithm = 'Massaging' @@ -218,8 +215,8 @@ def test03_Massaging(self): columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) #assert - assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.1) - assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.1) + assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.2) + assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.2) def test04_RejectOptionClassification(self): s_algorithm = 'RejectOptionClassification' @@ -258,8 +255,8 @@ def test04_RejectOptionClassification(self): columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) #assert - assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.1) - assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.1) + assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.2) + assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.2) def test05_Massaging_AA(self): s_algorithm = 'Massaging' @@ -296,8 +293,8 @@ def test05_Massaging_AA(self): columns=['group', 'base_rate', 'selection_rate', 'Balanced_Accuracy']) #assert - assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.1) - assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.1) + assert_frame_equal(result_singleattr_bias, ma_singleattr_bias, atol=0.2) + assert_frame_equal(result_combattr_bias, ma_combattr_bias, atol=0.2) if __name__ == "__main__": From b26b85021174b5f4b9f6ce9443f1b05b36ff18fe Mon Sep 17 00:00:00 2001 From: Kalousios Date: Sat, 2 Nov 2024 22:15:05 +0000 Subject: [PATCH 3/3] an issue of the init method regarding self.model clarified, progress bar support of the main algorithm added Signed-off-by: Kalousios --- aif360/algorithms/intersectional_fairness.py | 8 +++++--- .../algorithms/isf_helpers/inprocessing/inprocessing.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/aif360/algorithms/intersectional_fairness.py b/aif360/algorithms/intersectional_fairness.py index 0a936cfc..628b2719 100644 --- a/aif360/algorithms/intersectional_fairness.py +++ b/aif360/algorithms/intersectional_fairness.py @@ -38,6 +38,7 @@ from aif360.algorithms.isf_helpers.postprocessing.equalized_odds_postprocessing import EqualizedOddsPostProcessing from logging import getLogger, StreamHandler, ERROR, Formatter +from tqdm import tqdm class IntersectionalFairness(): """ @@ -345,9 +346,10 @@ def _mitigate_each_pair(self, dataset_actual, enable_fit=False, dataset_predicte for group2_idx in range(group1_idx + 1, len(self.group_protected_attrs)): id_touples.append((group1_idx, group2_idx, enable_fit)) - with concurrent.futures.ProcessPoolExecutor(max_workers=self.MAX_WORKERS) as excuter: - mitigation_results = list(excuter.map(self._worker, id_touples)) - for r in mitigation_results: + with concurrent.futures.ProcessPoolExecutor(max_workers=self.MAX_WORKERS) as executor: + futures = [executor.submit(self._worker, id_tuple) for id_tuple in id_touples] + for future in tqdm(concurrent.futures.as_completed(futures), total=len(id_touples)): + r = future.result() ds_pair_transf_list.append(r[0]) self.models[r[2]] = r[1] diff --git a/aif360/algorithms/isf_helpers/inprocessing/inprocessing.py b/aif360/algorithms/isf_helpers/inprocessing/inprocessing.py index 5dc2ba3e..2155a35d 100644 --- a/aif360/algorithms/isf_helpers/inprocessing/inprocessing.py +++ b/aif360/algorithms/isf_helpers/inprocessing/inprocessing.py @@ -24,7 +24,8 @@ class InProcessing(metaclass=ABCMeta): """ def __init__(self): super().__init__() - self.model = None + #the following line is need if we decide to expand support for more inprocessing algorithms besides adversarial debiasing + #self.model = None @abstractmethod def fit(self, ds_train):