{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "#### This notebook demonstrates the use of an optimized data pre-processing algorithm for bias mitigation\n", "\n", "- The debiasing function used is implemented in the `OptimPreproc` class.\n", "- Define parameters for optimized pre-processing specific to the dataset.\n", "- Divide the dataset into training, validation, and testing partitions.\n", "- Learn the optimized pre-processing transformation from the training data.\n", "- Train classifier on original training data.\n", "- Estimate the optimal classification threshold, that maximizes balanced accuracy without fairness constraints (from the original validation set).\n", "- Determine the prediction scores for original testing data. Using the estimated optimal classification threshold, compute accuracy and fairness metrics.\n", "- Transform the testing set using the learned probabilistic transformation.\n", "- Determine the prediction scores for transformed testing data. Using the estimated optimal classification threshold, compute accuracy and fairness metrics.\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "%matplotlib inline\n", "# Load all necessary packages\n", "import sys\n", "sys.path.append(\"../\")\n", "import numpy as np\n", "from tqdm import tqdm\n", "\n", "from aif360.datasets import BinaryLabelDataset\n", "from aif360.datasets import AdultDataset, GermanDataset, CompasDataset\n", "from aif360.metrics import BinaryLabelDatasetMetric\n", "from aif360.metrics import ClassificationMetric\n", "from aif360.metrics.utils import compute_boolean_conditioning_vector\n", "from aif360.algorithms.preprocessing.optim_preproc import OptimPreproc\n", "from aif360.algorithms.preprocessing.optim_preproc_helpers.data_preproc_functions\\\n", " import load_preproc_data_adult, load_preproc_data_german, load_preproc_data_compas\n", "from aif360.algorithms.preprocessing.optim_preproc_helpers.distortion_functions\\\n", " import get_distortion_adult, get_distortion_german, get_distortion_compas\n", "from aif360.algorithms.preprocessing.optim_preproc_helpers.opt_tools import OptTools\n", "from common_utils import compute_metrics\n", "\n", "from sklearn.linear_model import LogisticRegression\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.metrics import accuracy_score\n", "\n", "from IPython.display import Markdown, display\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Load dataset and specify options" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# import dataset\n", "dataset_used = \"adult\" # \"adult\", \"german\", \"compas\"\n", "protected_attribute_used = 1 # 1, 2\n", "\n", "if dataset_used == \"adult\":\n", " if protected_attribute_used == 1:\n", " privileged_groups = [{'sex': 1}]\n", " unprivileged_groups = [{'sex': 0}]\n", " dataset_orig = load_preproc_data_adult(['sex'])\n", " else:\n", " privileged_groups = [{'race': 1}]\n", " unprivileged_groups = [{'race': 0}]\n", " dataset_orig = load_preproc_data_adult(['race'])\n", " \n", " optim_options = {\n", " \"distortion_fun\": get_distortion_adult,\n", " \"epsilon\": 0.05,\n", " \"clist\": [0.99, 1.99, 2.99],\n", " \"dlist\": [.1, 0.05, 0]\n", " }\n", " \n", "elif dataset_used == \"german\":\n", " if protected_attribute_used == 1:\n", " privileged_groups = [{'sex': 1}]\n", " unprivileged_groups = [{'sex': 0}]\n", " dataset_orig = load_preproc_data_german(['sex'])\n", " optim_options = {\n", " \"distortion_fun\": get_distortion_german,\n", " \"epsilon\": 0.05,\n", " \"clist\": [0.99, 1.99, 2.99],\n", " \"dlist\": [.1, 0.05, 0]\n", " }\n", " \n", " else:\n", " privileged_groups = [{'age': 1}]\n", " unprivileged_groups = [{'age': 0}]\n", " dataset_orig = load_preproc_data_german(['age'])\n", " optim_options = {\n", " \"distortion_fun\": get_distortion_german,\n", " \"epsilon\": 0.1,\n", " \"clist\": [0.99, 1.99, 2.99],\n", " \"dlist\": [.1, 0.05, 0]\n", " } \n", "\n", "elif dataset_used == \"compas\":\n", " if protected_attribute_used == 1:\n", " privileged_groups = [{'sex': 1}]\n", " unprivileged_groups = [{'sex': 0}]\n", " dataset_orig = load_preproc_data_compas(['sex'])\n", " else:\n", " privileged_groups = [{'race': 1}]\n", " unprivileged_groups = [{'race': 0}]\n", " dataset_orig = load_preproc_data_compas(['race'])\n", " \n", " optim_options = {\n", " \"distortion_fun\": get_distortion_compas,\n", " \"epsilon\": 0.05,\n", " \"clist\": [0.99, 1.99, 2.99],\n", " \"dlist\": [.1, 0.05, 0]\n", " }\n", "\n", "#random seed\n", "np.random.seed(1)\n", "\n", "# Split into train, validation, and test\n", "dataset_orig_train, dataset_orig_vt = dataset_orig.split([0.7], shuffle=True)\n", "dataset_orig_valid, dataset_orig_test = dataset_orig_vt.split([0.5], shuffle=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Display dataset attributes" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "#### Training Dataset shape" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "(34189, 18)\n" ] }, { "data": { "text/markdown": [ "#### Favorable and unfavorable labels" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "(1.0, 0.0)\n" ] }, { "data": { "text/markdown": [ "#### Protected attribute names" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "['sex']\n" ] }, { "data": { "text/markdown": [ "#### Privileged and unprivileged protected attribute values" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "([array([1.])], [array([0.])])\n" ] }, { "data": { "text/markdown": [ "#### Dataset feature names" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "['race', 'sex', 'Age (decade)=10', 'Age (decade)=20', 'Age (decade)=30', 'Age (decade)=40', 'Age (decade)=50', 'Age (decade)=60', 'Age (decade)=>=70', 'Education Years=6', 'Education Years=7', 'Education Years=8', 'Education Years=9', 'Education Years=10', 'Education Years=11', 'Education Years=12', 'Education Years=<6', 'Education Years=>12']\n" ] } ], "source": [ "# print out some labels, names, etc.\n", "display(Markdown(\"#### Training Dataset shape\"))\n", "print(dataset_orig_train.features.shape)\n", "display(Markdown(\"#### Favorable and unfavorable labels\"))\n", "print(dataset_orig_train.favorable_label, dataset_orig_train.unfavorable_label)\n", "display(Markdown(\"#### Protected attribute names\"))\n", "print(dataset_orig_train.protected_attribute_names)\n", "display(Markdown(\"#### Privileged and unprivileged protected attribute values\"))\n", "print(dataset_orig_train.privileged_protected_attributes, \n", " dataset_orig_train.unprivileged_protected_attributes)\n", "display(Markdown(\"#### Dataset feature names\"))\n", "print(dataset_orig_train.feature_names)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Metric for original training data" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Metric for the original dataset\n", "metric_orig_train = BinaryLabelDatasetMetric(dataset_orig_train, \n", " unprivileged_groups=unprivileged_groups,\n", " privileged_groups=privileged_groups)\n", "display(Markdown(\"#### Original training dataset\"))\n", "print(\"Difference in mean outcomes between unprivileged and privileged groups = %f\" % metric_orig_train.mean_difference())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Train with and transform the original training data" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "OP = OptimPreproc(OptTools, optim_options,\n", " unprivileged_groups = unprivileged_groups,\n", " privileged_groups = privileged_groups)\n", "\n", "OP = OP.fit(dataset_orig_train)\n", "\n", "# Transform training data and align features\n", "dataset_transf_train = OP.transform(dataset_orig_train, transform_Y=True)\n", "dataset_transf_train = dataset_orig_train.align_datasets(dataset_transf_train)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Metric with the transformed training data" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "metric_transf_train = BinaryLabelDatasetMetric(dataset_transf_train, \n", " unprivileged_groups=unprivileged_groups,\n", " privileged_groups=privileged_groups)\n", "display(Markdown(\"#### Transformed training dataset\"))\n", "print(\"Difference in mean outcomes between unprivileged and privileged groups = %f\" % metric_transf_train.mean_difference())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Optimized preprocessing has reduced the disparity in favorable outcomes between the privileged and unprivileged\n", "groups (training data)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "### Testing \n", "assert np.abs(metric_transf_train.mean_difference()) < np.abs(metric_orig_train.mean_difference())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Load, clean up original test data and compute metric" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dataset_orig_test = dataset_transf_train.align_datasets(dataset_orig_test)\n", "display(Markdown(\"#### Testing Dataset shape\"))\n", "print(dataset_orig_test.features.shape)\n", "\n", "metric_orig_test = BinaryLabelDatasetMetric(dataset_orig_test, \n", " unprivileged_groups=unprivileged_groups,\n", " privileged_groups=privileged_groups)\n", "display(Markdown(\"#### Original test dataset\"))\n", "print(\"Difference in mean outcomes between unprivileged and privileged groups = %f\" % metric_orig_test.mean_difference())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Transform test data and compute metric" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dataset_transf_test = OP.transform(dataset_orig_test, transform_Y = True)\n", "dataset_transf_test = dataset_orig_test.align_datasets(dataset_transf_test)\n", "\n", "metric_transf_test = BinaryLabelDatasetMetric(dataset_transf_test, \n", " unprivileged_groups=unprivileged_groups,\n", " privileged_groups=privileged_groups)\n", "display(Markdown(\"#### Transformed test dataset\"))\n", "print(\"Difference in mean outcomes between unprivileged and privileged groups = %f\" % metric_transf_test.mean_difference())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Optimized preprocessing has reduced the disparity in favorable outcomes between the privileged and unprivileged\n", "groups (test data)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "### Testing \n", "assert np.abs(metric_transf_test.mean_difference()) < np.abs(metric_orig_test.mean_difference())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Train classifier on original data" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Logistic regression classifier and predictions\n", "scale_orig = StandardScaler()\n", "X_train = scale_orig.fit_transform(dataset_orig_train.features)\n", "y_train = dataset_orig_train.labels.ravel()\n", "\n", "lmod = LogisticRegression()\n", "lmod.fit(X_train, y_train)\n", "y_train_pred = lmod.predict(X_train)\n", "\n", "# positive class index\n", "pos_ind = np.where(lmod.classes_ == dataset_orig_train.favorable_label)[0][0]\n", "\n", "dataset_orig_train_pred = dataset_orig_train.copy()\n", "dataset_orig_train_pred.labels = y_train_pred" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Obtain scores original test set" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dataset_orig_valid_pred = dataset_orig_valid.copy(deepcopy=True)\n", "X_valid = scale_orig.transform(dataset_orig_valid_pred.features)\n", "y_valid = dataset_orig_valid_pred.labels\n", "dataset_orig_valid_pred.scores = lmod.predict_proba(X_valid)[:,pos_ind].reshape(-1,1)\n", "\n", "dataset_orig_test_pred = dataset_orig_test.copy(deepcopy=True)\n", "X_test = scale_orig.transform(dataset_orig_test_pred.features)\n", "y_test = dataset_orig_test_pred.labels\n", "dataset_orig_test_pred.scores = lmod.predict_proba(X_test)[:,pos_ind].reshape(-1,1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Find the optimal classification threshold from the validation set" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "num_thresh = 100\n", "ba_arr = np.zeros(num_thresh)\n", "class_thresh_arr = np.linspace(0.01, 0.99, num_thresh)\n", "for idx, class_thresh in enumerate(class_thresh_arr):\n", " \n", " fav_inds = dataset_orig_valid_pred.scores > class_thresh\n", " dataset_orig_valid_pred.labels[fav_inds] = dataset_orig_valid_pred.favorable_label\n", " dataset_orig_valid_pred.labels[~fav_inds] = dataset_orig_valid_pred.unfavorable_label\n", " \n", " classified_metric_orig_valid = ClassificationMetric(dataset_orig_valid,\n", " dataset_orig_valid_pred, \n", " unprivileged_groups=unprivileged_groups,\n", " privileged_groups=privileged_groups)\n", " \n", " ba_arr[idx] = 0.5*(classified_metric_orig_valid.true_positive_rate()\\\n", " +classified_metric_orig_valid.true_negative_rate())\n", "\n", "best_ind = np.where(ba_arr == np.max(ba_arr))[0][0]\n", "best_class_thresh = class_thresh_arr[best_ind]\n", "\n", "print(\"Best balanced accuracy (no fairness constraints) = %.4f\" % np.max(ba_arr))\n", "print(\"Optimal classification threshold (no fairness constraints) = %.4f\" % best_class_thresh)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Predictions and fairness metrics from original test set" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "display(Markdown(\"#### Predictions from original testing data\"))\n", "\n", "bal_acc_arr_orig = []\n", "disp_imp_arr_orig = []\n", "avg_odds_diff_arr_orig = []\n", "\n", "display(Markdown(\"#### Testing set\"))\n", "display(Markdown(\"##### Raw predictions - No fairness constraints\"))\n", "\n", "for thresh in tqdm(class_thresh_arr):\n", " \n", " fav_inds = dataset_orig_test_pred.scores > thresh\n", " dataset_orig_test_pred.labels[fav_inds] = dataset_orig_test_pred.favorable_label\n", " dataset_orig_test_pred.labels[~fav_inds] = dataset_orig_test_pred.unfavorable_label\n", "\n", " if (thresh == best_class_thresh):\n", " disp = True\n", " else:\n", " disp = False\n", " \n", " metric_test_bef = compute_metrics(dataset_orig_test, dataset_orig_test_pred, \n", " unprivileged_groups, privileged_groups, disp=disp)\n", " \n", " bal_acc_arr_orig.append(metric_test_bef[\"Balanced accuracy\"])\n", " avg_odds_diff_arr_orig.append(metric_test_bef[\"Average odds difference\"])\n", " disp_imp_arr_orig.append(metric_test_bef[\"Disparate impact\"])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax1 = plt.subplots(figsize=(10,7))\n", "ax1.plot(class_thresh_arr, bal_acc_arr_orig)\n", "ax1.set_xlabel('Classification Thresholds', fontsize=16, fontweight='bold')\n", "ax1.set_ylabel('Balanced Accuracy', color='b', fontsize=16, fontweight='bold')\n", "ax1.xaxis.set_tick_params(labelsize=14)\n", "ax1.yaxis.set_tick_params(labelsize=14)\n", "\n", "\n", "ax2 = ax1.twinx()\n", "ax2.plot(class_thresh_arr, np.abs(1.0-np.array(disp_imp_arr_orig)), color='r')\n", "ax2.set_ylabel('abs(1-disparate impact)', color='r', fontsize=16, fontweight='bold')\n", "ax2.axvline(np.array(class_thresh_arr)[best_ind], \n", " color='k', linestyle=':')\n", "ax2.yaxis.set_tick_params(labelsize=14)\n", "ax2.grid(True)\n", "\n", "disp_imp_at_best_bal_acc_orig = np.abs(1.0-np.array(disp_imp_arr_orig))[best_ind]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```abs(1-disparate impact)``` must be close to zero for classifier predictions to be fair." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Train classifier on transformed data and obtain predictions with its fairness metrics" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "scale_transf = StandardScaler()\n", "X_train = scale_transf.fit_transform(dataset_transf_train.features)\n", "y_train = dataset_transf_train.labels.ravel()\n", "\n", "lmod = LogisticRegression()\n", "lmod.fit(X_train, y_train)\n", "y_train_pred = lmod.predict(X_train)\n", "\n", "dataset_transf_train_pred = dataset_transf_train.copy()\n", "dataset_transf_train_pred.labels = y_train_pred" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Predictions and fairness metrics from transformed test set" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dataset_transf_test_pred = dataset_transf_test.copy(deepcopy=True)\n", "X_test = scale_transf.transform(dataset_transf_test_pred.features)\n", "y_test = dataset_transf_test_pred.labels\n", "dataset_transf_test_pred.scores = lmod.predict_proba(X_test)[:,pos_ind].reshape(-1,1)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "display(Markdown(\"#### Predictions from transformed testing data\"))\n", "\n", "bal_acc_arr_transf = []\n", "disp_imp_arr_transf = []\n", "avg_odds_diff_arr_transf = []\n", "\n", "display(Markdown(\"#### Testing set\"))\n", "display(Markdown(\"##### Transformed predictions - No fairness constraints\"))\n", "\n", "for thresh in tqdm(class_thresh_arr):\n", " \n", " fav_inds = dataset_transf_test_pred.scores > thresh\n", " dataset_transf_test_pred.labels[fav_inds] = dataset_transf_test_pred.favorable_label\n", " dataset_transf_test_pred.labels[~fav_inds] = dataset_transf_test_pred.unfavorable_label\n", "\n", " if (thresh == best_class_thresh):\n", " disp = True\n", " else:\n", " disp = False\n", " \n", " metric_test_bef = compute_metrics(dataset_transf_test, dataset_transf_test_pred, \n", " unprivileged_groups, privileged_groups, disp=disp)\n", " \n", " bal_acc_arr_transf.append(metric_test_bef[\"Balanced accuracy\"])\n", " avg_odds_diff_arr_transf.append(metric_test_bef[\"Average odds difference\"])\n", " disp_imp_arr_transf.append(metric_test_bef[\"Disparate impact\"])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax1 = plt.subplots(figsize=(10,7))\n", "ax1.plot(class_thresh_arr, bal_acc_arr_transf)\n", "ax1.set_xlabel('Classification Thresholds', fontsize=16, fontweight='bold')\n", "ax1.set_ylabel('Balanced Accuracy', color='b', fontsize=16, fontweight='bold')\n", "ax1.xaxis.set_tick_params(labelsize=14)\n", "ax1.yaxis.set_tick_params(labelsize=14)\n", "\n", "\n", "ax2 = ax1.twinx()\n", "ax2.plot(class_thresh_arr, np.abs(1.0-np.array(disp_imp_arr_transf)), color='r')\n", "ax2.set_ylabel('abs(1-disparate impact)', color='r', fontsize=16, fontweight='bold')\n", "ax2.axvline(np.array(class_thresh_arr)[best_ind], \n", " color='k', linestyle=':')\n", "ax2.yaxis.set_tick_params(labelsize=14)\n", "ax2.grid(True)\n", "\n", "disp_imp_at_best_bal_acc_transf = np.abs(1.0-np.array(disp_imp_arr_transf))[best_ind]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```abs(1-disparate impact)``` must be close to zero for classifier predictions to be fair. This measure has improved using classifier trained using the transformed data compared to the original data.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "### testing\n", "assert disp_imp_at_best_bal_acc_transf < disp_imp_at_best_bal_acc_orig" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Summary of Results\n", "We show the optimal classification thresholds, and the fairness and accuracy metrics." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Classification Thresholds\n", "\n", "| Dataset |Classification threshold|\n", "|-|-|\n", "|Adult|0.2674|\n", "|German|0.6732|\n", "|Compas|0.5148|" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Fairness Metric: Disparate impact, Accuracy Metric: Balanced accuracy\n", "\n", "#### Performance\n", "\n", "| Dataset |Sex (Acc-Bef)|Sex (Acc-Aft)|Sex (Fair-Bef)|Sex (Fair-Aft)|Race/Age (Acc-Bef)|Race/Age (Acc-Aft)|Race/Age (Fair-Bef)|Race/Age (Fair-Aft)|\n", "|-|-|-|-|-|-|-|-|-|\n", "|Adult (Test)|0.7417|0.7021|0.2774|0.7729|0.7417|0.7408|0.4423|0.7645|\n", "|German (Test)|0.6524|0.5698|0.9948|1.0664|0.6524|0.6067|0.3824|0.8228|\n", "|Compas (Test)|0.6774|0.6606|0.6631|0.8085|0.6774|0.6790|0.6600|0.8430|\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 2", "language": "python", "name": "python2" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.10" } }, "nbformat": 4, "nbformat_minor": 2 }