{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "![MLU Logo](../../data/MLU_Logo.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Responsible AI - Final Project Solution\n", "\n", "Build a fair [LogisticRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html) that predicts the __credit_risk__ field (whether some is a credit risk or not) of the [German Credit Dataset](https://archive.ics.uci.edu/ml/datasets/South+German+Credit+%28UPDATE%29).\n", "\n", "### Final Project Problem: Loan Approval\n", "\n", "__Problem Definition:__\n", "Given a set of features about an individual (e.g. age, past credit history, immigration status, ...) predict whether a loan is repaid or not (is customer a credit risk). We impose the additional constraint that the model should be fair with respect to different age groups ($\\geq$ 25 yrs and $<$ 25 yrs).\n", "\n", "In the banking industry, there are certain regulations regarding the use of sensitive features (e.g., age, ethnicity, marital status, ...). According to those regulations, it would not be okay if age played a significant role in the model (loans should be approved/denied regardless of an individuals' age).\n", "\n", "\n", "``` \n", "F. Kamiran and T. Calders, \"Data Preprocessing Techniques for Classification without Discrimination,\" Knowledge and Information Systems, 2012\n", "```\n", "\n", "1. Read the datasets (Given) \n", "2. Data Processing (Implement)\n", " * Exploratory Data Analysis\n", " * Select features to build the model (Suggested)\n", " * Train - Validation - Test Datasets\n", " * Feature transformation\n", "3. Train a Classifier on the Training Dataset (Implement)\n", "4. Make Predictions on the Test Dataset (Implement)\n", "5. Evaluate Results (Given)\n", "\n", "\n", "__Datasets and Files:__\n", "\n", "\n", "- ```german_credit_training.csv```: Training data with loan applicants features, credit history, dependents, savings, account status, age group (and more). The label is __credit_risk__.\n", "\n", "- ```german_credit_test.csv```: Test data with same features as above apart from label. This will be the data to make predictions for to emulate a production environment." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This notebook assumes an installation of the SageMaker kernel `conda_pytorch_p39`. In addition, libraries from a requirements.txt need to be installed:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "tags": [] }, "outputs": [], "source": [ "!pip install --no-deps -U -q -r ../../requirements.txt" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "%%capture\n", "\n", "# Reshaping/basic libraries\n", "import pandas as pd\n", "import numpy as np\n", "\n", "# Plotting libraries\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "\n", "sns.set_style(\"darkgrid\", {\"axes.facecolor\": \".9\"})\n", "\n", "# ML libraries\n", "from sklearn.model_selection import train_test_split\n", "from sklearn.metrics import confusion_matrix, accuracy_score, f1_score\n", "from sklearn.impute import SimpleImputer\n", "from sklearn.preprocessing import OneHotEncoder, MinMaxScaler\n", "from sklearn.pipeline import Pipeline\n", "from sklearn.compose import ColumnTransformer\n", "from sklearn.linear_model import LogisticRegression\n", "\n", "# Fairness libraries\n", "from folktables.acs import *\n", "from folktables.folktables import *\n", "from folktables.load_acs import *\n", "from aif360.datasets import BinaryLabelDataset, Dataset\n", "from aif360.metrics import BinaryLabelDatasetMetric\n", "from aif360.algorithms.preprocessing import DisparateImpactRemover\n", "\n", "# Operational libraries\n", "import sys\n", "\n", "sys.path.append(\"..\")\n", "\n", "# Jupyter(lab) libraries\n", "import warnings\n", "\n", "warnings.filterwarnings(\"ignore\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Read the datasets (Given)\n", "(Go to top)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Then, we read the __training__ and __test__ datasets into dataframes, using [Pandas](https://pandas.pydata.org/pandas-docs/stable/user_guide/10min.html). This library allows us to read and manipulate our data." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The shape of the training dataset is: (800, 24)\n", "The shape of the test dataset is: (200, 23)\n" ] } ], "source": [ "training_data = pd.read_csv(\"../../data/final_project/german_credit_training.csv\")\n", "test_data = pd.read_csv(\"../../data/final_project/german_credit_test.csv\")\n", "\n", "print(\"The shape of the training dataset is:\", training_data.shape)\n", "print(\"The shape of the test dataset is:\", test_data.shape)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Data Processing (Implement)\n", "(Go to top) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2.1 Exploratory Data Analysis\n", "(Go to Data Processing)\n", "\n", "We look at number of rows, columns, and some simple statistics of the datasets." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "tags": [] }, "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", " \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", " \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", " \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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
IDsexpersonal_statuschecking_account_statuscredit_duration_monthscredit_historycredit_purposecredit_amountsavingsemployed_since_years...age_yearsother_installment_planshousingnum_existing_creditsjob_statusnum_people_liable_fortelephoneforeign_workercredit_riskage_groups
0564malemarried/widowed... < 0 DM24existing credits paid back duly till nowretraining4712.. >= 1000 DM1 <= ... < 4 years...37bankrent2-3management/self-employed/highly qualified empl...0 to 2TrueFalse01
1484malemarried/widowed... >= 200 DM12all credits at this bank paid back dulyothers1163100 <= ... < 500 DM1 <= ... < 4 years...44nonerent1skilled employee/official0 to 2TrueFalse01
226femalesingle... >= 200 DM6delay in paying off in the pastfurniture/equipment426unknown/no savings account.. >= 7 years...39nonerent1unskilled - resident0 to 2FalseFalse01
3443malemarried/widowed... >= 200 DM12no credits taken/all credits paid back dulyrepairs719unknown/no savings account.. >= 7 years...41bankrent1unskilled - resident3 or moreFalseFalse11
4211female/malefemale (non-single) or male (single)... >= 200 DM36no credits taken/all credits paid back dulyfurniture/equipment3835.. >= 1000 DM.. >= 7 years...45nonerent1unskilled - resident0 to 2TrueFalse01
\n", "

5 rows × 24 columns

\n", "
" ], "text/plain": [ " ID sex personal_status \\\n", "0 564 male married/widowed \n", "1 484 male married/widowed \n", "2 26 female single \n", "3 443 male married/widowed \n", "4 211 female/male female (non-single) or male (single) \n", "\n", " checking_account_status credit_duration_months \\\n", "0 ... < 0 DM 24 \n", "1 ... >= 200 DM 12 \n", "2 ... >= 200 DM 6 \n", "3 ... >= 200 DM 12 \n", "4 ... >= 200 DM 36 \n", "\n", " credit_history credit_purpose \\\n", "0 existing credits paid back duly till now retraining \n", "1 all credits at this bank paid back duly others \n", "2 delay in paying off in the past furniture/equipment \n", "3 no credits taken/all credits paid back duly repairs \n", "4 no credits taken/all credits paid back duly furniture/equipment \n", "\n", " credit_amount savings employed_since_years ... \\\n", "0 4712 .. >= 1000 DM 1 <= ... < 4 years ... \n", "1 1163 100 <= ... < 500 DM 1 <= ... < 4 years ... \n", "2 426 unknown/no savings account .. >= 7 years ... \n", "3 719 unknown/no savings account .. >= 7 years ... \n", "4 3835 .. >= 1000 DM .. >= 7 years ... \n", "\n", " age_years other_installment_plans housing num_existing_credits \\\n", "0 37 bank rent 2-3 \n", "1 44 none rent 1 \n", "2 39 none rent 1 \n", "3 41 bank rent 1 \n", "4 45 none rent 1 \n", "\n", " job_status num_people_liable_for \\\n", "0 management/self-employed/highly qualified empl... 0 to 2 \n", "1 skilled employee/official 0 to 2 \n", "2 unskilled - resident 0 to 2 \n", "3 unskilled - resident 3 or more \n", "4 unskilled - resident 0 to 2 \n", "\n", " telephone foreign_worker credit_risk age_groups \n", "0 True False 0 1 \n", "1 True False 0 1 \n", "2 False False 0 1 \n", "3 False False 1 1 \n", "4 True False 0 1 \n", "\n", "[5 rows x 24 columns]" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "training_data.head()" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "tags": [] }, "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", " \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", " \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", " \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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
IDsexpersonal_statuschecking_account_statuscredit_duration_monthscredit_historycredit_purposecredit_amountsavingsemployed_since_years...propertyage_yearsother_installment_planshousingnum_existing_creditsjob_statusnum_people_liable_fortelephoneforeign_workerage_groups
0963malemarried/widowed... >= 200 DM24no credits taken/all credits paid back dulyfurniture/equipment2397100 <= ... < 500 DM.. >= 7 years...building society savings agreement/life insurance35bankrent2-3skilled employee/official0 to 2TrueFalse1
1611female/malefemale (non-single) or male (single)0 <= ... < 200 DM10no credits taken/all credits paid back dulyothers1240... < 100 DM.. >= 7 years...real estate48noneown1unskilled - resident3 or moreFalseFalse1
2106malemarried/widowed... >= 200 DM18critical account/other credits existing (not a...others6458unknown/no savings account.. >= 7 years...real estate39bankrent2-3management/self-employed/highly qualified empl...3 or moreTrueFalse1
3891malemarried/widowed... >= 200 DM15all credits at this bank paid back dulyfurniture/equipment1829unknown/no savings account.. >= 7 years...building society savings agreement/life insurance46nonerent2-3skilled employee/official0 to 2TrueFalse1
4342femalesingle... < 0 DM18no credits taken/all credits paid back dulyfurniture/equipment3213100 <= ... < 500 DM... < 1 year...unknown/no property25nonefor free1skilled employee/official0 to 2FalseFalse1
\n", "

5 rows × 23 columns

\n", "
" ], "text/plain": [ " ID sex personal_status \\\n", "0 963 male married/widowed \n", "1 611 female/male female (non-single) or male (single) \n", "2 106 male married/widowed \n", "3 891 male married/widowed \n", "4 342 female single \n", "\n", " checking_account_status credit_duration_months \\\n", "0 ... >= 200 DM 24 \n", "1 0 <= ... < 200 DM 10 \n", "2 ... >= 200 DM 18 \n", "3 ... >= 200 DM 15 \n", "4 ... < 0 DM 18 \n", "\n", " credit_history credit_purpose \\\n", "0 no credits taken/all credits paid back duly furniture/equipment \n", "1 no credits taken/all credits paid back duly others \n", "2 critical account/other credits existing (not a... others \n", "3 all credits at this bank paid back duly furniture/equipment \n", "4 no credits taken/all credits paid back duly furniture/equipment \n", "\n", " credit_amount savings employed_since_years ... \\\n", "0 2397 100 <= ... < 500 DM .. >= 7 years ... \n", "1 1240 ... < 100 DM .. >= 7 years ... \n", "2 6458 unknown/no savings account .. >= 7 years ... \n", "3 1829 unknown/no savings account .. >= 7 years ... \n", "4 3213 100 <= ... < 500 DM ... < 1 year ... \n", "\n", " property age_years \\\n", "0 building society savings agreement/life insurance 35 \n", "1 real estate 48 \n", "2 real estate 39 \n", "3 building society savings agreement/life insurance 46 \n", "4 unknown/no property 25 \n", "\n", " other_installment_plans housing num_existing_credits \\\n", "0 bank rent 2-3 \n", "1 none own 1 \n", "2 bank rent 2-3 \n", "3 none rent 2-3 \n", "4 none for free 1 \n", "\n", " job_status num_people_liable_for \\\n", "0 skilled employee/official 0 to 2 \n", "1 unskilled - resident 3 or more \n", "2 management/self-employed/highly qualified empl... 3 or more \n", "3 skilled employee/official 0 to 2 \n", "4 skilled employee/official 0 to 2 \n", "\n", " telephone foreign_worker age_groups \n", "0 True False 1 \n", "1 False False 1 \n", "2 True False 1 \n", "3 True False 1 \n", "4 False False 1 \n", "\n", "[5 rows x 23 columns]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "test_data.head()" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Implement more EDA here" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "RangeIndex: 800 entries, 0 to 799\n", "Data columns (total 24 columns):\n", " # Column Non-Null Count Dtype \n", "--- ------ -------------- ----- \n", " 0 ID 800 non-null int64 \n", " 1 sex 800 non-null object\n", " 2 personal_status 800 non-null object\n", " 3 checking_account_status 800 non-null object\n", " 4 credit_duration_months 800 non-null int64 \n", " 5 credit_history 800 non-null object\n", " 6 credit_purpose 800 non-null object\n", " 7 credit_amount 800 non-null int64 \n", " 8 savings 800 non-null object\n", " 9 employed_since_years 800 non-null object\n", " 10 installment_rate 800 non-null object\n", " 11 other_debtors_guarantors 800 non-null object\n", " 12 present_residence_since 800 non-null object\n", " 13 property 800 non-null object\n", " 14 age_years 800 non-null int64 \n", " 15 other_installment_plans 800 non-null object\n", " 16 housing 800 non-null object\n", " 17 num_existing_credits 800 non-null object\n", " 18 job_status 800 non-null object\n", " 19 num_people_liable_for 800 non-null object\n", " 20 telephone 800 non-null bool \n", " 21 foreign_worker 800 non-null bool \n", " 22 credit_risk 800 non-null int64 \n", " 23 age_groups 800 non-null int64 \n", "dtypes: bool(2), int64(6), object(16)\n", "memory usage: 139.2+ KB\n" ] } ], "source": [ "training_data.info()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2.2 Select features to build the model \n", "(Go to Data Processing)\n", "\n", "Let's use all the features. Below you see a snippet of code that separates categorical and numerical columns based on their data type. This should only be used if we are sure that the data types are correctly assigned (check during EDA). Mindful with some of the feature names - they suggest numerical values but upon inspection it should become clear that they are actually categoricals (e.g. `employed_since_years` has been binned into groups).\n" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Categorical columns: ['sex', 'personal_status', 'checking_account_status', 'credit_history', 'credit_purpose', 'savings', 'employed_since_years', 'installment_rate', 'other_debtors_guarantors', 'present_residence_since', 'property', 'other_installment_plans', 'housing', 'num_existing_credits', 'job_status', 'num_people_liable_for']\n", "\n", "Numerical columns: ['ID', 'credit_duration_months', 'credit_amount', 'age_years', 'age_groups']\n" ] } ], "source": [ "# Grab model features/inputs and target/output\n", "categorical_features = (\n", " training_data.drop(\"credit_risk\", axis=1)\n", " .select_dtypes(include=\"object\")\n", " .columns.tolist()\n", ")\n", "print(\"Categorical columns:\", categorical_features)\n", "\n", "print(\"\")\n", "\n", "numerical_features = (\n", " training_data.drop(\"credit_risk\", axis=1)\n", " .select_dtypes(include=np.number)\n", " .columns.tolist()\n", ")\n", "print(\"Numerical columns:\", numerical_features)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We notice that `ID` is identified as numerical column. ID's should never be used as features for training as they are unique by row. Let's drop the ID from the model features after we have separated target and features:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Model features: ['sex', 'personal_status', 'checking_account_status', 'credit_history', 'credit_purpose', 'savings', 'employed_since_years', 'installment_rate', 'other_debtors_guarantors', 'present_residence_since', 'property', 'other_installment_plans', 'housing', 'num_existing_credits', 'job_status', 'num_people_liable_for', 'ID', 'credit_duration_months', 'credit_amount', 'age_years', 'age_groups']\n", "\n", "\n", "Model target: credit_risk\n" ] } ], "source": [ "model_target = \"credit_risk\"\n", "model_features = categorical_features + numerical_features\n", "\n", "print(\"Model features: \", model_features)\n", "print(\"\\n\")\n", "print(\"Model target: \", model_target)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "tags": [] }, "outputs": [], "source": [ "to_remove = \"ID\"\n", "\n", "# Drop 'ID' feature from the respective list(s)\n", "if to_remove in model_features:\n", " model_features.remove(to_remove)\n", "if to_remove in categorical_features:\n", " categorical_features.remove(to_remove)\n", "if to_remove in numerical_features:\n", " numerical_features.remove(to_remove)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's also remove `age_years` as this is an obvious proxy for the age groups." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "to_remove = \"age_years\"\n", "\n", "# Drop 'ID' feature from the respective list(s)\n", "if to_remove in model_features:\n", " model_features.remove(to_remove)\n", "if to_remove in categorical_features:\n", " categorical_features.remove(to_remove)\n", "if to_remove in numerical_features:\n", " numerical_features.remove(to_remove)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2.3 Train - Validation Datasets\n", "(Go to Data Processing)\n", "\n", "We already have training and test datasets, but no validation dataset (which you need to create). Furthermore, the test dataset is missing the labels - the goal of the project is to predict these labels. \n", "\n", "To produce a validation set to evaluate model performance, split the training dataset into train and validation subsets using sklearn's [train_test_split()](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) function. " ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Implement here" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Train - Test - Validation datasets shapes: (720, 24) (200, 23) (80, 24)\n" ] } ], "source": [ "# We only need to split between train and val (test is already separate)\n", "train_data, val_data = train_test_split(\n", " training_data, test_size=0.1, shuffle=True, random_state=23\n", ")\n", "\n", "# Print the shapes of the Train - Test Datasets\n", "print(\n", " \"Train - Test - Validation datasets shapes: \",\n", " train_data.shape,\n", " test_data.shape,\n", " val_data.shape,\n", ")\n", "\n", "train_data.reset_index(inplace=True, drop=True)\n", "val_data.reset_index(inplace=True, drop=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2.4 Feature transformation\n", "(Go to Data Processing)\n", "\n", "Here, you have different options. You can use Reweighing, prepare for Disparate Impact Remover or use Suppression. Regardless of which method to use, it makes sense to prepare the data first by dealing with missing values, one-hot encoding and scaling." ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "tags": [] }, "outputs": [], "source": [ "### STEP 1 ###\n", "##############\n", "\n", "# Preprocess the numerical features\n", "numerical_processor = Pipeline(\n", " [(\"num_imputer\", SimpleImputer(strategy=\"mean\")), (\"num_scaler\", MinMaxScaler())]\n", ")\n", "# Preprocess the categorical features\n", "categorical_processor = Pipeline(\n", " [\n", " (\"cat_imputer\", SimpleImputer(strategy=\"constant\", fill_value=\"missing\")),\n", " (\"cat_encoder\", OneHotEncoder(handle_unknown=\"ignore\")),\n", " ]\n", ")\n", "\n", "### STEP 2 ###\n", "##############\n", "\n", "# Combine all data preprocessors from above\n", "data_processor = ColumnTransformer(\n", " [\n", " (\"numerical_processing\", numerical_processor, numerical_features),\n", " (\"categorical_processing\", categorical_processor, categorical_features),\n", " ]\n", ")\n", "\n", "### STEP 3 ###\n", "##############\n", "\n", "# Fit the dataprocessor to our training data and apply transform to test data\n", "processed_train = data_processor.fit_transform(train_data[model_features])\n", "processed_test = data_processor.transform(test_data)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 2.4.1 DI-Transformation\n", "\n", "The dataframe you just created will not have any column names for the one-hot encoded categorical features and will also be saved as sparse matrix. If you want to proceed with DI transformation, you need to convert the sparse matrix back to a data frame and also re-create the column names." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "# Implement here" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Extract feature names from data processor (we need this as one-hot encoding creates new columns)\n", "cat_ft_names = list(\n", " data_processor.transformers_[1][1]\n", " .named_steps[\"cat_encoder\"]\n", " .get_feature_names_out(categorical_features)\n", ")\n", "\n", "# Convert the sparse matrix to a pandas dataframe and name columns\n", "processed_train_df = pd.DataFrame.sparse.from_spmatrix(\n", " processed_train, columns=numerical_features + cat_ft_names\n", ")\n", "\n", "# Join label with the transformed dataset so we can pass it to AIF360\n", "train_df = pd.concat([processed_train_df, train_data[model_target]], axis=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As final step for DI transformation you have to create a `BinaryLabelDataset` and also check the DI value of the dataset." ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.8371212121212122\n" ] } ], "source": [ "# Create a Dataset construct for AIF360\n", "binaryLabelDataset_train = BinaryLabelDataset(\n", " df=train_df,\n", " label_names=[\"credit_risk\"],\n", " protected_attribute_names=[\"age_groups\"],\n", " favorable_label=0.0,\n", " unfavorable_label=1.0,\n", ")\n", "\n", "# Check pre-training bias\n", "print(\n", " BinaryLabelDatasetMetric(\n", " binaryLabelDataset_train,\n", " unprivileged_groups=[{\"age_groups\": 0}],\n", " privileged_groups=[{\"age_groups\": 1}],\n", " ).disparate_impact()\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, use the AIF360 `DisparateImpactRemover`." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "# Initialize DisparateImpactRemover\n", "di_remover = DisparateImpactRemover(repair_level=0.5)\n", "\n", "# Create transformed version (DI removed)\n", "binaryLabelDataset_transform_train = di_remover.fit_transform(binaryLabelDataset_train)\n", "\n", "# Convert back to dataframe\n", "df_transform_train = binaryLabelDataset_transform_train.convert_to_dataframe()[0]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can check that the transformation changed only the features, but not the labels by calculating the DI metric for the transformed dataset. The value will match what you had previously." ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.8371212121212122\n" ] } ], "source": [ "# Check pre-training bias for transformed dataset\n", "print(\n", " BinaryLabelDatasetMetric(\n", " binaryLabelDataset_transform_train,\n", " unprivileged_groups=[{\"age_groups\": 0}],\n", " privileged_groups=[{\"age_groups\": 1}],\n", " ).disparate_impact()\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To visualize the transformation let's have a look at how the distribution of one of the numerical features changed." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "image/png": "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27evmjVrprp16+qhhx7S22+/nWn8I0eO6IcffrDvQ58+fSRl//lh1mczAABm4cxIAMBN41//+peqVKmihg0b5mr9iRMnauvWrWrXrp0GDBigAwcOaNmyZTp27JgWL17ssO7Jkyf13HPP6fHHH1fHjh31xhtvaOjQoZo2bZpef/11+30Jly9frqefflqff/65rNb/feeXnp6uQYMGqX79+nr++ee1c+dOLVy4UOnp6Ro9erR9vTVr1qht27Z6+OGHlZqaqk8++USjR4/WsmXL1Lp1a4eY9uzZo88++0y9evVSuXLlVLly5Sz3c8qUKdq2bZt69+6tGjVq6MKFC/rpp5907Ngx1alTR9L/7h9Xr149Pfvss4qNjdWaNWv073//W++//77DGVzp6ekaOHCgQkND9cILL2j37t164403VKVKFfXs2TPHY55xf8fmzZurR48eOn78uDZs2KBffvlFGzZskIeHhyZMmKD3339fX375pf3S64xiX06uL9S5ubnJ19dXkrRkyRLNnz9f7du316OPPqq4uDitW7dOvXr1cti/zz//XCkpKerRo4fKli2rAwcOaN26dfrrr7+0YMECSVL37t11/vx57dq1K9NlrnmVlpamgQMHqlGjRho7dqz97KzJkydr69at6tq1q/r06aMzZ85o/fr1+u233+zHKSe5fb3lZpw5c+Zo8+bNOnDggGbMmCFJ9vdYXt5Dx48f13PPPafu3burW7duuuOOO5ScnKzevXsrKipKjz/+uG677Tbt27dPr732mqKjo/Xiiy/muJ9bt26Vt7e3BgwYIG9vb+3Zs0cLFixQYmKixo4d67DuxYsXNWjQIN1///1q3769tm3bprlz5yo4OFitWrWSdPXy6X79+unPP/9Unz59VKFCBX3wwQeZCmzZ+eWXX7Rv3z5FRkaqYsWKOnv2rDZs2KC+ffvqk08+kZeXl+6//36VLl1as2bNUocOHXTvvfeqVKlS8vLyUkJCgv766y/7vRwzLpG22WwaNmyYfvrpJ3Xr1k01atTQ77//rrfeeksnTpzQP//5T4c4cvvZkFtbtmzR5cuX1a1bN5UoUUK+vr5KTEzUO++8ow4dOuixxx5TUlKS3n33XQ0aNEjvvPOOateu7dDHxx9/rKSkJHXv3l0Wi0UrV67UyJEjtX37dvvreeTIkTp69Kh69+6typUrKy4u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1Vdf5XvM9PT0TIUQPz8/VahQwX4ZeN26deXn56eNGzc6XBr+zTff6NixY5me4p1fzZs3l4eHh9auXetwNtS7776rhISEQjnm0tX73bm5uWnRokWZzk4zDEN///23pP+d+XTtOoZhaM2aNZn69PLykpT5cvzKlSvLzc0t070ON2zYkOt427dvr/T09ExPSJauPn37+jGzc6PXW0HHccZ7qH379tq3b5927tyZaVl8fLzD/fiul9V8XblyRW+//fYNx83Ovffeq/Pnz+vzzz+3tyUnJ2vz5s252j6rM9PWrl3r8J7OScYTta/Xvn17RUVFZRlHSkqKLl26lKv+nSljX689/vv379fPP/+cr/6Sk5N1+fJlh7aqVauqVKlSDp9LWSmKz/PrZXU8EhISsvwyycvLK1fvY7M+mwEAMBtnRgIAbhpVq1bV3Llz9cwzz+ihhx5Sp06dFBwcrCtXrmjfvn36/PPP1bVrV0lSrVq11KVLF23atEnx8fG6++679csvv2jr1q2KiIiwPxjCWTw9PbVz506NHTtWoaGh2rlzp77++msNHTrUfjlsq1attHr1ag0aNEgdOnRQbGys3n77bVWtWtXhvnZ5kZSUpFatWqldu3aqVauWvL299d133+mXX36xn73l4eGhMWPGaPz48erdu7ciIyMVGxurNWvWqHLlyurfv79TjkH58uX15JNPatGiRRo0aJDatm2r48eP6+2331a9evXUsWNHp4xzvapVq+rpp5/Wq6++qrNnzyoiIkKlSpXSmTNntH37dnXr1k0DBw5U9erVVbVqVc2ePVtRUVHy8fHRtm3bsiwa1KlTR5I0Y8YMhYeHy83NTZGRkSpdurQefPBBrVu3ThaLRVWqVNHXX3+dq/s8ZmjSpIm6d++uZcuW6dChQ2rRooU8PDx04sQJff7553rxxRcdHrCSldy83go6jjPeQwMHDtSOHTs0dOhQdenSRXXq1FFycrJ+//13bdu2TV999VW2l4uHhYXJ19dX48aNU58+fWSxWPTBBx8U6HLobt26af369Ro7dqx+/fVXBQQE6IMPPsj1maOtW7fWBx98IB8fH9WsWVM///yzvvvuO5UtWzZX29epU0effvqpZs2apXr16snb21tt27ZVp06d9Nlnn2nKlCn6/vvv1bBhQ6Wnp+uPP/7Q559/rpUrV6pevXr53u/8aN26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J0+e1Lp16+Tu7i6LxaL4+HiNGDFC+/fv15YtW1S5cmWNGDHCvu2GDRt05513qm3btnJ3d9e//vUvTZs2TYZhqFevXnne/3379qlOnTqZ2keOHKmjR4+qd+/eqly5suLi4rRr1y79+eefCgoKknQ1CZkwYYLuvPNOPfnkkypdurQOHTqknTt36uGHH5Z09fLllJQU9ejRQ2XLltWBAwe0bt06/fXXX1qwYEGOsS1ZskTz589X+/bt9eijjyouLk7r1q1Tr1699P7776tMmTKSpHfeeUeTJ0+2F8FOnz6tYcOGydfXV7fddlumfuvUqaOvvvpKiYmJ8vHxyXb8w4cPa+DAgSpfvrxGjhyptLQ0LVy4UH5+fjc8rosWLdKyZcv02GOPKTQ0VImJiTp48KB+/fVXtWjRQt27d9f58+ezvKwqw5YtW3T58mV169ZNJUqUkK+vb7YJb3p6ugYNGqT69evr+eef186dO7Vw4UKlp6fn+QzQ3MR2rSNHjqhXr14qVaqUBg0aJHd3d23atEl9+vTRunXrVL9+fYf1Z8yYoTJlymjEiBE6e/as3nrrLf3jH//QvHnz8hQnAADI3oULFzR48GBFRkaqY8eO9vxl69at8vb21oABA+Tt7a09e/ZowYIFSkxM1NixYx36uHjxogYNGqT7779f7du317Zt2zR37lwFBwerVatWkqTNmzdrxowZateunfr27avLly/r8OHD2r9/vx5++GF1795dgYGBWrp0qfr06aN69erJ399fkvTdd99p8ODBCgoK0ogRI5SSkqJ169apR48e2rJliz3nzDB69GhVq1ZNzzzzjMMXridPntRzzz2nxx9/XB07dtQbb7yhoUOHatq0aXr99dfVo0cPSVdz8Kefflqff/65rNar5w8dOXJEPXr0UGBgoAYPHixvb2999tlnGj58uBYuXKj7779f0tUv6vv27av09HQNGTJEXl5e2rx5c67P7Pv22281cuRI1axZU88995z+/vtvjR8/XhUrVrzhtlOmTNG2bdvUu3dv1ahRQxcuXNBPP/2kY8eOqU6dOho6dKgSEhL0119/afz48ZKkUqVKOfTxz3/+Ux4eHho4cKCuXLmS46XZFy9e1JAhQ9S+fXtFRkbqs88+09SpU+Xh4aFHH300V/ubITexXSuvr4mnn35aQUFBevbZZ/Xbb7/pnXfeUfny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69atU+nSpTVixIh8x5QXU6ZMUc+ePfXwww+rW7duqlKlimJiYvTzzz/rr7/+0ocffihJGjRokD744AMNGjRIffv2lZeXlzZv3qxKlSrdMC+UpGeffVZPPvmkevbsqUceeUQXLlzQunXrdOedd+rSpUvZbpeUlKRWrVqpXbt2qlWrlry9vfXdd9/pl19+0bhx4+zr1alTR59++qlmzZqlevXqydvbW23bts3XMalQoYJWrFihs2fP6vbbb9enn36qQ4cOafr06faHcN55551q0KCBXnvtNV28eFG+vr769NNPHR68mZ/YXOE1ARRnFCOBYqpatWrq2LGjtm7dmmnZjBkzFBQUpK1bt2r79u3y9/fXk08+ado/jNWrV9eCBQs0b948zZ49W/7+/urRo4fKly+vCRMm5KvPNm3aqFy5cvrss8/01FNPSbp6s+/IyEjt3r1bH374odzc3FS9enXNmzdP7dq1s2/72GOPyc/PT8uXL9c///lPubu7q3r16urfv7+kq5e8LF26VDNmzNCyZcvk6emp+++/X7169XJ4MmN2hgwZottvv11vvvmmFi9ebI+tRYsWDglM9+7dlZ6erlWrVmnOnDkKDg7WkiVLsrwR+e7du3XhwgV16dLlhuPXqlVLq1at0qxZs7RgwQJVrFhRI0eOVHR09A2Tzj59+mjHjh3atWuXrly5okqVKunpp5/WwIED7es88MAD6tOnjz755BN9+OGHMgwj38VINzc3rVy5UlOnTtUrr7yiUqVKacSIERo+fLjDehMnTlRaWpo2btyoEiVK6MEHH9QLL7ygDh06OKyXl9juvPNOrV+/Xq+++qqWLVsmwzAUGhqqV155JcsbvQMAgKJRrlw5LV26VLNnz9a8efNUpkwZdezYUc2aNXPIUfKie/fu+uijj7R69WpdunRJFStWVJ8+fex5ZU6aN2+ulStXasGCBVqwYIHc3d1199136/nnn8/Tw1kKombNmnrvvfe0aNEibd26VRcuXFD58uV11113OeRRFSpU0Jo1azRjxgwtX75cZcuW1eOPP64KFSroxRdfvOE49957r+bPn6958+bp1VdfVdWqVTVr1ix99dVX+uGHH7LdrmTJkurRo4d27dqlL774QoZhqGrVqvYiaoaePXvq0KFD2rJli958801Vrlw538VIX19fvfzyy5oxY4Y2b94sf39/TZ48Wd26dXNYb+7cuZo8ebKWL1+uMmXK6NFHH1XTpk01YMAAh/XyEpsrvCaA4sxicMdUAMXE4sWLtWXLFn3xxRe5ulF5cfbUU0/JYrHYi5sAAAAAANwMuGckgGKjf//+unTpkj755JOiDqVQHTt2TF9//bVGjx5d1KEAAAAAAOBUnBkJAAAAAAAAwBScGQkAAAAAAADAFBQjAQAAAAAAAJiCYiQAAAAAAAAAU1CMBAAAAAAAAGAKipEAAAAAAAAATEExEgAAAAAAAIAp3Is6AFcSFRUlwzAKpW+LxaLAwMBCHQP5w9y4JubFdTE3rol5cV1mzE3GGCj+Cvs9zGeFa2N+XBvz4/qYI9fG/Lg2Z8xPbnNSipHXMAyj0N8QZoyB/GFuXBPz4rqYG9fEvLgu5ga5YdbrhNeja2N+XBvz4/qYI9fG/Lg2M+aHy7QBAAAAAAAAmIJiJAAAAAAAAABTUIwEAAAAAAAAYAruGQkAAArMZrPJZrNx/58sWCwWpaSkKDU1tUA3A7darbJa+R4ZAAAgK4Zh2PNRctK8u1HOarFY7DmpxWIp0FgUIwEAQIFcuXJFFy9eLOowXFp8fLzS09ML3I+vr69KlCjhhIgAAABuHunp6UpISFBqampRh1Ks5SZn9fDwUOnSpeXm5pbvcShGAgCAfLPZbLp48aJKlCghb2/vog7HZXl4eBQ4Ob506ZIuXrwoPz8/zpAEAAD4L8Mw9Pfff8tisRS4SHaru1HOmp6erqSkJP3999/y8/PL9xmSFCMBAEC+2Ww2SZK3t7c8PDyKOBrX5Yxj4+3trStXrshms1GMBAAA+K/09HQZhiFfX1/y0QK60fHz8PCQm5ubLly4oPT0dLm756+sSCYLAADyjfvxmI9jDgAA8D/kRkWjIMedYiQAAAAAAAAAU1CMBAAAAAAAAGAKipEAAAAAAAAATEExEgAAAAAAAIApKEYCAABcIzU1tahDAAAAwC3sZs9H8/cMbgAAgFz4/vvvtXbtWh0/flxubm666667NHLkSFWuXFmSdPDgQb3++us6deqU7rjjDvXp00eTJk3SihUrdOedd0qS/vjjDy1dulQHDhyQl5eXGjdurOHDh6ts2bI3HP/SpUt67bXX9O2338rb21s9evTQt99+q5o1a2rkyJGSpO7duysyMlJnzpzRt99+q5YtW2r8+PH65ptvtHr1ap09e1bly5dX165d1b17d3vfrVu31vTp09WyZUt7W2RkpEaMGKH27dvrzz//VI8ePTRp0iRt3bpVhw8fVuXKlfX000+rQYMGkqSEhATNnz9fe/fuVXJysgICAtS7d2+1b9/eSTMAAABwayMfvZqPbtmyRb///rtL5KMUIwEAQKFJSUlRt27dVL16dSUnJ2v16tWaNGmSVq5cqeTkZE2YMEFNmzbVpEmTFBUVpUWLFjlsn5CQoGeffVaRkZEaPny4rly5omXLlmnatGl6/fXXbzj+4sWL9csvv2jmzJkqV66cVq9erSNHjqhmzZoO623atEl9+/ZVv379JEmHDx/WtGnT1L9/f7Vp00YHDx7UvHnzVKZMmTwnZkuXLtXo0aNVpUoVbd68WRMmTNCGDRvk6+urVatW6cSJE5o9e7Z8fX119uxZXblyJU/9AwAAIHvko1fz0REjRuj222/PMR/19/fXiRMnCj0fpRgJAAAKTatWrRx+fuGFF9S5c2edOHFCv/zyiyRpzJgx8vT01O23367o6GjNnTvXvv7WrVt15513avDgwQ59dOvWTadPn1aVKlWyHfvSpUvatm2bJk6cqEaNGkmSxo4dq0cffTTTumFhYQ7fMs+YMUMNGzZU3759JUlVqlTRyZMntWnTpjwnf126dFGbNm2UmpqqZ555Rj/88IM+/fRT9ejRQ+fPn9edd96pWrVqSZJuu+22PPUNAACAnJGPXs1HM45DTvmoh4eH/P3989R3flCMBAAAhebMmTN64403dOjQIV28eFE2m02SdP78eZ0+fVo1atSQp6enff3atWs7bH/s2DHt27dPDz74YKa+z507l2Pyd+7cOaWlpTn06ePjk+U2ISEhDj+fPHlSLVq0cGirW7eu3n33XaWnp8vNzS2HvXZUp04d+/+7u7srJCREJ0+elCR16tRJkydP1u+//667775b4eHhqlu3bq77BgAAQM7IR3OfjzZt2lTNmzcv9HyUYiQAACg048ePV8WKFTVmzBj5+/vLZrNpwIABub4pd3Jyspo3b64hQ4ZkWubn5+e0OEuWLJnnbSwWS6a29PT0PPXRtGlTbdq0SXv27NGPP/6oZ599Vp07d9ZTTz2V53gAAACQGflozq7NR//973+bko8W6dO09+7dq6FDhyo8PFwhISHavn27fVlqaqpeeeUVPfzww2rQoIHCw8P1wgsvKCoqyqGPCxcu6LnnnlPDhg3VuHFjTZgwQUlJSWbviumsVqvc3d2d8tdq5aHqAADnu3jxok6fPq0+ffqoUaNGqlatmhISEuzLq1Spoj/++MPhnjT/+c9/HPoIDg7W8ePHVbFiRQUFBTn89fLyynH8SpUqyd3d3aHPxMREnT59+oaxV6tWTQcPHnRoO3jwoIKCguzfQpctW1axsbH25WfOnFFKSkqmvn777Tf7/6elpen3339XtWrV7G1ly5bVgw8+qIkTJ2rEiBH6+OOPbxgf4CrISQEArox89Krc5qNTpkwxJR8t0jMjL126pJCQED3yyCMaMWKEw7KUlBT99ttvGjZsmGrVqqX4+HjNnDlTw4YN05YtW+zrjRkzRtHR0Vq9erVSU1M1YcIETZ48Wa+++qrZu2Maq9WqCgH+slhzf0puTgxbus5Hx9hPVQYAwBlKly6tMmXK6KOPPlL58uV1/vx5LV++3L48IiJCq1at0quvvqqePXsqKipKmzZtkvS/b3k7d+6sjz/+WNOnT1ePHj1UunRpnT17Vjt27NDzzz+f4+Up3t7eateunZYuXaoyZcqobNmyWr16taxWa5bfIl+rW7duGjp0qNasWaM2bdro119/1datW/X000/b1wkLC9PWrVtVp04dpaena/ny5XJ3z5xavf/++6pWrZoqV66sd999VwkJCfb7/LzxxhsKDg7W7bffrtTUVO3evdshMQRcGTkpAMDVkY9e9f777ysoKEhVq1bNMR81DMOUfLRIi5GtWrXKdCPRDKVLl9bq1asd2iZNmqTHHntM586dU6VKlXTs2DHt3LlT7777rurVqydJmjhxooYMGaIXXnhBgYGBhb4PRcFqtcpidVPyvzfJlhhdsL58AuTVsLusViuJHwDAqaxWqyZPnqyFCxdqwIABqlq1qkaOHGlPoEqVKqWXXnpJr7/+ugYNGqQ77rhD/fr10/Tp01WiRAlJkr+/vxYtWqRly5ZpzJgxSk1NVWBgoJo0aZKrs6iGDx+u1157TePHj5e3t7d69Oih6Ohoe//ZCQ4O1pQpU7R69WqtWbNGfn5+GjBggMPNwp966inNnj1bI0eOlL+/v0aOHKnDhw9n6mvIkCFat26djhw5osqVK+ull15S2bJlJV29Z8+KFSv0119/ydPTU/Xq1dPkyZNzeYSBokVOCgBwdeSjVw0ZMkRvv/22jh496hL5qMUwDKNQR8ilkJAQLV68WBEREdmu89133+mJJ57Qjz/+KB8fH7377ruaPXu29u7da18nLS1NoaGhmj9/vu6///48xRAVFaXCOhwWi0WBgYFOGcPd3V3+/v669H+LZbt4rkB9WX0ryfve4YqJiVFaWlqB+iqunDk3cB7mxXUxN66pqOYlNTVVf//9t8qWLSsPD48C9/fll19q9uzZ+uSTTxxuJO4sycnJevTRR/XUU08pMjLS6f1f688//1SPHj20YsUK3XXXXbm+L1F2UlNTdeHCBZUrVy7Tsc6YfxR/f/31V6G+hy0WiypWrOiUcdzd3RUQEKCk/1vklJy01L0jFB0dfcvmpJJz5wfOx/y4PubItRXW/GTkSOSjmV2bj9555503XN/Dw+OGOWtOxztjjm+k2DzA5vLly5o7d64iIyPl4+MjSYqJiVH58uUd1nN3d5evr6+io/P+7awZSbwzx/D28pLSfQrWyX/vb2DGo9tdHb/EuSbmxXUxN67J7HlJSUlRfHy8PDw88pX8ffbZZ6pUqZICAgJ09OhRLV++XG3btrX/W19Qv//+u06ePKnatWsrKSlJq1evlsViUevWrZ2SrOYko/+MS2WcMZ6bm5sCAgLydYNzAAAAZLZt2zbddttt9nx02bJlat26tdMKkUeOHNGpU6dUq1YtJSUl6a233pKkTE/KvpUUi2JkamqqRo8eLcMwNG3atEIbp9idGZmcLFtiYoH6srqVkbfEmZGc5eVymBfXxdy4pqI8MzI9PT3fZ/1FR0drxYoViouLk5+fn1q1aqVBgwblqr+oqCj169cv2+VvvfWW0tLS9Pbbb+vUqVPy8PBQcHCwFixYoFKlShX4TMUbyeg/499XZ5wZmZ6erujoaM6MBAAAcJK4uDi98cYb9ny0devWGjRoUK62zU0+KkmbNm1yyEcXLlxov0z6VuTyxcjU1FQ9/fTTOnfunN566y2HMyX8/f0VFxfnsH5aWpouXryogICAPI9lGEah/wLnjDEytjf++6dAff13ezP23dVxDFwT8+K6mBvXZPa8FHSsHj16qEePHvna1s/PTytXrsxxeWBgoMNNys1022236euvv3Z6v7z3AAAAnId81HwuXYzMKESePHlSa9asUbly5RyWh4WFKT4+XgcPHlTdunUlSXv27JHNZlNoaGhRhAwAAEzi7u6uoKCgog4DAAAAtyjy0fwp0mJkUlKSTp06Zf/5zJkzOnTokHx9fRUQEKBRo0bpt99+07Jly+yXJUmSr6+vSpQooRo1aqhly5aaNGmSpk2bptTUVE2fPl2RkZFcqgQAAAAAAAC4mCItRh48eFB9+/a1/zxr1ixJUpcuXTRixAjt2LFDktSpUyeH7dasWaOmTZtKkubOnavp06erX79+slqteuCBBzRx4kST9gAAAAAAAABAbhVpMbJp06Y6fPhwtstzWpahbNmyevXVV50ZFgAAAAAAAIBCYC3qAAAAAAAAAADcGlz6ATYAAODmZrVaZbWa992ozWaTzWYzbTwAAAC4NvJR81GMBAAARcJqtco/IEBuJiZ/6TabYqKjb/kEEAAAAOSjRYViJAAAKBJWq1VuVqu27j2umITkQh/Pv7SXutx9h6xWa56Tv61bt2rjxo2Ki4tTzZo1NWrUKNWuXTvb9b/++mutWrVKf/31l4KCgvTUU0/p7rvvLuguAAAAwIlupXz0ySef1D333FPQXXAKipEAAKBIxSQk66+LhZ/85deOHTv0z3/+U88++6xq166td999V88//7zWrl2rcuXKZVr/4MGD+sc//qEhQ4aoWbNm2r59u8aPH6/ly5erevXqRbAHAAAAyMmtkI9OnDjRZfJRHmADAACQg3feeUeRkZFq3769br/9dj377LMqWbKkPv300yzXf++999SkSRM9/vjjqlatmgYOHKjg4GBt3brV5MgBAABwM3BGPnrnnXe6TD5KMRIAACAbqampOnz4sBo1amRvs1qtatSokX777bcst/n1118d1pekpk2bZrs+AAAAkB1n5aNNmjRxmXyUYiQAAEA2Ll68KJvNpvLlyzu0lytXTnFxcVluExcXl2n98uXLZ7s+AAAAkB1n5aM5rW82ipEAAAAAAAAATEExEgAAIBu+vr6yWq2ZvkX++++/M33bnCGrsyCz+nYaAAAAuBFn5aM5rW82ipEAAADZ8PDwUEhIiP7973/b22w2m3766SfdddddWW5Tp04dh/Ulae/evdmuDwAAAGTHWfnojz/+6DL5qHtRBwAAAG5t/qW9XHqcxx57TLNmzVJISIhq166td999VykpKWrfvr0k6aWXXpK/v7+GDBkiSXrkkUc0evRobdq0Sffcc4927Nih//znP3r22Wedti8AAABwnlshHz18+LCee+45p+1LQVCMBAAARcJmsyndZlOXu+8wbcx0m002my1P27Rt21YXLlzQ6tWrFRcXp5o1a2rOnDn2y1yioqJksVjs69etW1eTJk3SqlWrtHLlSlWuXFmzZs1S9erVnbovAAAAKJhbKR+dMWOGy+SjFCMBAECRsNlsiomOltVq3l1jbPlI/iSpa9eu6tq1a5bL5s+fn6mtdevWat26tf1nDw8Ppaam5nlcAAAAFJ5bKR91JRQjAQBAkclvMgYAAAA4A/mo+XiADQAAAAAAAABTUIwEAADALWvv3r0aOnSowsPDFRISou3bt9uXpaam6pVXXtHDDz+sBg0aKDw8XC+88IKioqIc+rhw4YKee+45NWzYUI0bN9aECROUlJRk9q4AAAAUCxQjAQAAcMu6dOmSQkJCNGXKlEzLUlJS9Ntvv2nYsGHasmWLFi1apOPHj2vYsGEO640ZM0ZHjx7V6tWrtXTpUv3444+aPHmyWbsAAABQrHDPSAAAANyyWrVqpVatWmW5rHTp0lq9erVD26RJk/TYY4/p3LlzqlSpko4dO6adO3fq3XffVb169SRJEydO1JAhQ/TCCy8oMDCw0PcBAACgOKEYCQAAAORSYmKiLBaLypQpI0nat2+fypQpYy9ESlLz5s1ltVp14MAB3X///Xnq32KxODXe7Pp3xjj2vv77p0B96X9xFfYxcGXOnB84H/Pj+pgj11ZY88N8F42s/s3O7VxQjAQAAABy4fLly5o7d64iIyPl4+MjSYqJiVH58uUd1nN3d5evr6+io6PzPIZZZ1I6cxxvLy8p3adgnXh5SZL8/f2dEFHxxxm1ro35cX3MkWtz9vykpKQoPj5eHh4e8vDwcGrft6LcHEM3NzcFBASoZMmS+RqDYiQAAABwA6mpqRo9erQMw9C0adMKbZyoqCgZhlFo/VssFgUGBjplHHd3d/n7++tScrJsiYkF6svqVkbeulrcTUtLK1BfxZkz5wfOx/y4PubItRXW/KSmpio9PV2pqalO6/NW5eHhccPjmHG8o6OjMxUuM+b4RihGAgCAImO1WmW1mvc8PZvNJpvNZtp4uDmkpqbq6aef1rlz5/TWW2/Zz4qUrp7JFxcX57B+WlqaLl68qICAgDyPZRiGKb9AO2OcjO2N//4pUF//3d6s/Xd1HAfXxvy4PubItTl7fgraF/lo/hRkHilGAgCAImG1WlUhwF8Wq5tpYxq2dJ2PjrkpEkCYI6MQefLkSa1Zs0blypVzWB4WFqb4+HgdPHhQdevWlSTt2bNHNptNoaGhRREyAADIJfLRokExEgAAFAmr1SqL1U3J/94kW2Le762X5/F8AuTVsLusVmuuk7/9+/dr48aN+v333xUbG6vp06erZcuWOW6zb98+/fOf/9SJEycUEBCgPn36qGPHjs7YBRSCpKQknTp1yv7zmTNndOjQIfn6+iogIECjRo3Sb7/9pmXLltkvSZIkX19flShRQjVq1FDLli01adIkTZs2TampqZo+fboiIyO5ZxkAAC6uOOSjkvNy0vbt2xd0F5yCYiQAAChStsRo2S6eK+owspSSkqIaNWrooYce0qRJk264/p9//qnx48erY8eOmjhxon766Se98sorCgwMVMOGDU2IGHl18OBB9e3b1/7zrFmzJEldunTRiBEjtGPHDklSp06dHLZbs2aNmjZtKkmaO3eupk+frn79+slqteqBBx7QxIkTTdoDAABQUK6cj0rOy0n9/PzUpEkTEyLOGcVIAACAbDRt2tRecMqNDz/8UBUrVtRTTz0lSapWrZp++eUXbdq0iWKki2ratKkOHz6c7fKclmUoW7asXn31VWeGBQAAYOesnPSdd95xiWKkeXfoBAAAuMn9+uuvatSokUNbkyZNdPDgwSKKCAAAALea7HLS3377rYgickQxEgAAwEni4uJUvnx5h7Zy5copKSlJly9fLqKoAAAAcCtx9ZyUYiQAAAAAAAAAU1CMBAAAcJLy5csrLi7Ooe3vv/9WqVKl5OnpWURRAQAA4Fbi6jkpxUgAAAAnqVOnjv797387tP3444+qW7duEUUEAACAW012Oeldd91VRBE54mnaAACgSFl9Alx2nEuXLuns2bP2n//66y8dOXJEZcqUUWBgoJYvX66YmBhNmDBBktSxY0dt3bpVS5cuVfv27bVv3z7961//0ty5c522HwAAAHAuV85HJeflpC+//LJT9qOgKEYCAIAiYbPZZNjS5dWwu2ljGrZ02Wy2XK9/+PBhPfPMM/afFy9eLElq166dxo8fr9jYWEVFRdmX33bbbZo1a5YWL16s9957TwEBAXr++efVtGlTpaamOm9HAAAAUGDFIR+VnJeTNmnSxDk7UUAUIwEAQJGw2Ww6Hx0j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74fsYLu9dwkEXM/DaZyn6OtYqNAhnd3FMvW19zCv9nk/zhizLNm27SrWgVhevC0ALZYo9ahETZoIsSL5GetVXtRELYzVITE/t1IlVnNfQynxuWrqMTbmlfu/cly+K9jT3wV+jNW5XRv5Xc4Nz8ve24w0bR0bfxewbfuA9WhjjeCmFom0JrWcWFY7cVBeXq4TTzxRCxYsUFFRUfj26dOn64MPPtDChQtbMTsAAAAkO+pRAACAyMXm9Hwc5eTkKC0tTVVVVU1ur6qqUl5eXitlBQAAgFRBPQoAABC5hG9Gpqen65hjjtGKFSvCt4VCIa1YsaLJmWkAAAAgFqhHAQAAIpfwMyMlaeTIkbr55pvVs2dP9e7dW0899ZR27dql4cOHt3ZqAAAASAHUowAAAJFJimbk4MGDtXXrVj300EOqqKhQYWGh5s+fz7YYAAAAxAX1KAAAQGQS/gI2AAAAAAAAABJDws+MBAAAAAAAAJAYaEYCAAAAAAAAiAuakQAAAAAAAADigmYkAAAAAAAAgLigGQkAAAAAAAAgLmhGeujZZ5/VKaecol69eum8887Tp59+etD7v/HGGzr99NPVq1cvDR06VMuWLYtTpqknmrV58cUXddFFF6lv377q27evLr/88mbXEi0T7ddMo9dff10FBQW67rrrYpxh6op2bbZv367S0lL1799fPXv21KBBg/ieFgPRrssf/vAHDRo0SL1799ZJJ52kqVOnas+ePXHKNjV88MEHuvbaa9W/f38VFBTo7bffbvYxK1eu1LBhw9SzZ0+ddtppWrRoURwyRSqhJjUbdanZqE/NRo1qPupVc5lUt9KM9MiSJUtUVlam66+/Xq+88oqOOuoojRo1SlVVVfu9/0cffaSbbrpJ5557rhYvXqxTTz1V119/vb788ss4Z578ol2blStX6swzz9TTTz+tBQsWqHPnzrriiitUXl4e58yTW7Tr0mjTpk363e9+pz59+sQp09QT7drU1tZq5MiR+vbbbzVr1iwtXbpUkydPVseOHeOceXKLdl1effVVzZw5U2PGjNGSJUt07733asmSJbr//vvjnHlyq6mpUUFBge66666I7r9x40Zdc801OuGEE/SnP/1Jl112me644w4tX748xpkiVVCTmo261GzUp2ajRjUf9arZjKpbHXji3HPPdUpLS8P/b2hocPr37+/MmTNnv/e/8cYbnauvvrrJbeedd54zadKkmOaZiqJdm33V19c7RUVFziuvvBKjDFNTS9alvr7eOf/8850XX3zRufnmm53Ro0fHI9WUE+3aPPfcc86pp57q1NbWxivFlBTtupSWljojRoxocltZWZlzwQUXxDTPVNajRw/nrbfeOuh9pk+f7px55plNbhs3bpxzxRVXxDI1pBBqUrNRl5qN+tRs1Kjmo15NHK1dt/LOSA/U1tZqzZo16tevX/g227bVr18/rVq1ar+P+fjjj1VSUtLktv79++vjjz+OZaoppyVrs69du3apvr5ebdu2jVWaKael6/LII48oNzdX5513XjzSTEktWZu//e1vOu6443TPPfeoX79+GjJkiB577DE1NDTEK+2k15J1KSoq0po1a8JbYzZu3Khly5bppJNOikvO2D9+/iOWqEnNRl1qNupTs1Gjmo96NfnEskbwuY4Aff/992poaFBubm6T23Nzc7V+/fr9PqayslJ5eXk/u39lZWXM8kxFLVmbfc2YMUMdOnRo8k0V7rRkXT788EO99NJLWrx4cRwyTF0tWZuNGzfqvffe09ChQzV37lx98803Ki0tVX19vcaMGROPtJNeS9Zl6NCh+v7773XRRRfJcRzV19frggsu0LXXXhuPlHEA+/v5n5eXp+rqau3evVsZGRmtlBmSATWp2ahLzUZ9ajZqVPNRryafWNatvDMSOIi5c+dqyZIlmj17tgKBQGunk7Kqq6s1ceJETZ48We3bt2/tdLAPx3GUm5uryZMnq2fPnho8eLCuvfZaLViwoLVTS2krV67UnDlzdNddd2nRokWaPXu2li1bpkceeaS1UwMAtAB1qVmoT81HjWo+6tXUxTsjPZCTk6O0tLSfDWWtqqr6WRe5UV5e3s/OOB/s/miZlqxNo8cff1xz587Vk08+qaOOOiqWaaacaNdl48aN+vbbbzV69OjwbaFQSJJ09NFHa+nSpTr00ENjm3SKaMnXTH5+vnw+n9LS0sK3de/eXRUVFaqtrVV6enpMc04FLVmXWbNm6ayzzgpvGysoKFBNTY3uvPNOjR49WrbN+cjWsL+f/5WVlcrKyuJdkXCNmtRs1KVmoz41GzWq+ahXk08s61ZW1gPp6ek65phjtGLFivBtoVBIK1asUFFR0X4fc9xxx+m9995rcts///lPHXfccbFMNeW0ZG0kad68eXr00Uc1f/589erVKx6pppRo16V79+569dVXtXjx4vCfU045RSeccIIWL16sTp06xTP9pNaSr5ni4mJ988034QJckjZs2KD8/HyKPI+0ZF127979swKusRh3HCd2yeKg+PmPWKImNRt1qdmoT81GjWo+6tXkE8sagWakR0aOHKkXX3xRr7zyitatW6e7775bu3bt0vDhwyVJEydO1MyZM8P3HzFihJYvX64nnnhC69at08MPP6zVq1frkksuaa1DSFrRrs3cuXM1a9YsTZ06VV27dlVFRYUqKiq0c+fO1jqEpBTNugQCAfXo0aPJn0MOOUSZmZnq0aMHxYTHov2aufDCC/XDDz/o3nvv1ddff6133nlHc+bM0cUXX9xah5CUol2Xk08+Wc8//7xef/11bdy4Ue+++65mzZqlk08+uck7BODOzp07tXbtWq1du1aStGnTJq1du1abN2+WJM2cOVMTJ04M3/+CCy7Qxo0bNX36dK1bt07PPvus3njjDV1++eWtkT6SEDWp2ahLzUZ9ajZqVPNRr5rNpLqVbdoeGTx4sLZu3aqHHnpIFRUVKiws1Pz588NvR/7uu++adPyLi4s1Y8YMPfjgg7r//vt1+OGH65FHHlGPHj1a6xCSVrRrs2DBAtXV1Wns2LFN4owZM0Y33HBDXHNPZtGuC+In2rXp3LmzHn/8cZWVlemss85Sx44dNWLECF111VWtdQhJKdp1GT16tCzL0oMPPqjy8nK1b99eJ598ssaPH99ah5CUVq9erREjRoT/X1ZWJkkaNmyYpk2bpoqKCn333Xfhj3fr1k1z5sxRWVmZnn76aXXq1ElTpkzRgAED4p47khM1qdmoS81GfWo2alTzUa+azaS61XJ47ysAAAAAAACAOOC0DgAAAAAAAIC4oBkJAAAAAAAAIC5oRgIAAAAAAACIC5qRAAAAAAAAAOKCZiQAAAAAAACAuKAZCQAAAAAAACAuaEYCAAAAAAAAiAuakQDwo02bNqmgoEBr166VJK1cuVIFBQXavn17K2cGAACAVEA9CiAV0IwEgAMoKirSP/7xD2VnZ0uSFi1apD59+rRyVvF16aWX6t57723tNAAAAFIS9Sj1KJCMaEYCSDp1dXWexElPT1d+fr4sy/IkHgAAAFID9SgAHBjNSAAJIRQKad68eTrttNPUs2dP/frXv9bvf//78FaWJUuW6JJLLlGvXr306quvSpIWLlyoM844Q7169dLpp5+uZ599tknMTz/9VGeffbZ69eql4cOHh7fDNPrptpiVK1fq1ltv1Y4dO1RQUKCCggI9/PDDzea9ePFiDR8+XEVFRfrVr36lm266SVVVVT97juXLl+vss89W7969NWLECFVVVWnZsmU644wzVFxcrJtuukm7du0KP662tlZTpkxRSUmJevXqpQsvvFCffvpp+OP7O2v+9ttvq6CgIPz/hx9+WL/5zW+0ePFinXLKKTr++OM1fvx4VVdXS5JuueUWvf/++3r66afDx7xp06ZmjxkAACAZUY9SjwLwhq+1EwCASMycOVMLFy7UrbfequOPP15btmzR119/Hf74jBkzdMstt6iwsFCBQEB//vOfNWvWLN15550qLCzU2rVrNWnSJLVp00bDhg3Tzp07dc0116hfv3667777tGnTpoNu/ygqKtJtt92mhx56SEuXLpUktWnTptm86+vrdeONN6p79+6qqqrStGnTdMstt2jevHlN7jd79mxNmjRJwWBQ48aN07hx45Senq6ZM2eqpqZG119/vZ555hldffXVkqTp06frL3/5i6ZNm6auXbtq/vz5uvLKK/Xmm2+qXbt2Eb+u33zzjf7617/qscce0/bt2zVu3DjNmzdP48eP1+23364NGzboyCOP1NixYyVJ7du3jzg2AABAMqEepR4F4A2akQCMV11draefflp33nmnhg0bJkk69NBD1adPn/CZ0csuu0wDBw4MP+bhhx/WLbfcEr6tW7du+te//qUXXnhBw4YN02uvvaZQKKSpU6cqEAjoyCOP1H/+8x/dfffd+80hPT1d2dnZsixL+fn5Eed+7rnnhv/drVs33X777Tr33HO1c+dOZWZmhj82btw4HX/88eHHzJw5U2+//ba6desmSRo0aJBWrlypq6++WjU1NVqwYIHKysp00kknSZImT56sd999Vy+99JKuvPLKiPNzHEdlZWXKysqSJJ111llasWKFxo8fr+zsbPn9fmVkZER1zAAAAMmGepR6FIB3aEYCMN769etVW1urX/7ylwe8T8+ePcP/rqmp0TfffKPbb79dkyZNCt9eX18fHv69bt06FRQUKBAIhD9eVFTkee6rV6/W7Nmz9fnnn2vbtm1yHEeS9N133+kXv/hF+H4/3a6Sm5urYDAYLvwkKS8vT5999pmkvWeP6+rqVFxcHP643+9X7969tW7duqjy69q1a7jwk6QOHTo02bYDAAAA6lGJehSAd2hGAjDeTwu0A/npFpWamhpJe8/OHnvssU3uZ9vxG5VbU1OjUaNGqX///poxY4ZycnL03XffadSoUT8bau7z/ffbsWVZTf7feFsoFIr4uW3bDheajfY3SH3f55H0s8cBAACkOupR6lEA3uECNgCMd/jhhysjI0PvvfdeRPfPy8tThw4dtHHjRh122GFN/jSe3T3iiCP0xRdfaM+ePeHHffzxxweN6/f71dDQEHHe69ev1w8//KAJEyaoT58+OuKIIzw5y3vooYfK7/fro48+Ct9WV1enzz77LHx2OycnRzt37gwXwpL0+eefR/1cfr8/qqITAAAgGVGPNkU9CsAN3hkJwHiBQEBXXXWV7rvvPvn9fhUXF2vr1q366quvVFJSst/HjB07VlOmTFF2drYGDBig2tparV69Wtu3b9fIkSM1ZMgQPfDAA7rjjjt0zTXX6Ntvv9UTTzxx0Dy6du2qmpoarVixQgUFBQoGgwoGgwe8f5cuXeT3+/XMM8/owgsv1JdffqlHH33U1Wsh7T3rfuGFF2r69Olq27atunTpovnz52v37t3hmUDHHnusgsGg7r//fo0YMUKffPKJFi1aFPVzde3aVZ988ok2bdqkNm3aqF27dnE9mw8AAGAC6tGmqEcBuMFXMICEcN1112nkyJF66KGHNHjwYI0fP15bt2494P3PO+88TZkyRYsWLdLQoUN16aWX6pVXXtH//M//SJIyMzP12GOP6csvv9TZZ5+tBx54QBMmTDhoDsXFxbrgggs0btw4lZSUaP78+Qe9f/v27TVt2jQtXbpUgwcP1rx583TzzTdHf/D7MWHCBA0aNEgTJ07UsGHD9O9//1vz589X27ZtJUnt2rXTfffdp7///e8aOnSoXn/9dd1www1RP88VV1yhtLQ0nXnmmSopKdHmzZs9yR8AACDRUI82RT0KoKUsh2EMAAAAAAAAAOKAd0YCAAAAAAAAiAtmRgJAC3344Ye66qqrDvjxVatWxTEbAAAApBrqUQCJiG3aANBCu3fvVnl5+QE/fthhh8UxGwAAAKQa6lEAiYhmJAAAAAAAAIC4YGYkAAAAAAAAgLigGQkAAAAAAAAgLmhGAgAAAAAAAIgLmpEAAAAAAAAA4oJmJAAAAAAAAIC4oBkJAAAAAAAAIC5oRgIAAAAAAACIC5qRAAAAAAAAAOLi/wMhwL+Ytf7ubwAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%matplotlib inline\n", "\n", "# Initialize figure\n", "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))\n", "\n", "# Set title of figure\n", "fig.suptitle(\"Comparison of Feature before and after Transformation\")\n", "\n", "# Set title\n", "ax1.title.set_text(\"Normal (scaled) distribution\")\n", "ax2.title.set_text(\"Transformed distribution\")\n", "\n", "# Create plots\n", "sns.histplot(train_df, x=\"credit_amount\", hue=\"age_groups\", ax=ax1)\n", "sns.histplot(df_transform_train, x=\"credit_amount\", hue=\"age_groups\", ax=ax2)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, apply the DI transformation to the test data too. If you wanted to try different DI repair values, you could add a section before this one th at uses that validation dataset." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "# Convert the sparse matrix to a pandas dataframe and name columns\n", "processed_test_df = pd.DataFrame.sparse.from_spmatrix(\n", " processed_test, columns=numerical_features + cat_ft_names\n", ")\n", "\n", "# Repeat for test data\n", "test_df = pd.concat(\n", " [\n", " processed_test_df,\n", " pd.read_csv(\"../../data/final_project/german_credit_test_labels.csv\")[\n", " \"credit_risk\"\n", " ],\n", " ],\n", " axis=1,\n", ")\n", "\n", "# Create a Dataset construct for AIF360\n", "binaryLabelDataset_test = BinaryLabelDataset(\n", " df=test_df,\n", " label_names=[\"credit_risk\"],\n", " protected_attribute_names=[\"age_groups\"],\n", " favorable_label=0.0,\n", " unfavorable_label=1.0,\n", ")\n", "\n", "# Create transformed version (DI removed)\n", "binaryLabelDataset_transform_test = di_remover.fit_transform(binaryLabelDataset_test)\n", "\n", "# Convert back to dataframe\n", "df_transform_test = binaryLabelDataset_transform_test.convert_to_dataframe()[0].drop(\n", " \"credit_risk\", axis=1\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 2.4.2 Reweighing\n", "\n", "Alternatively if you want to build the model with reweighing, you can use the custom function below and then apply the weights during the training stage." ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "tags": [] }, "outputs": [], "source": [ "def reweighing(data, label, sensitive_attr, return_list=True):\n", " label_dict = dict()\n", " try:\n", " # This will loop through the different labels (1 - awarded grant, 0 - not awarded)\n", " for outcome in data[label].unique():\n", " weight_map = dict()\n", " # Check for all possible groups (here we have A & B but there could be more in reality)\n", " for val in data[sensitive_attr].unique():\n", " # Calculate the probabilities\n", " nom = (\n", " len(data[data[sensitive_attr] == val])\n", " / len(data)\n", " * len(data[data[label] == outcome])\n", " / len(data)\n", " )\n", " denom = len(\n", " data[(data[sensitive_attr] == val) & (data[label] == outcome)]\n", " ) / len(data)\n", " # Store weights according to sensitive attribute\n", " weight_map[val] = round(nom / denom, 2)\n", " # Store\n", " label_dict[outcome] = weight_map\n", " # Create full list of all weights for every data point provided as input\n", " data[\"weights\"] = list(\n", " map(lambda x, y: label_dict[y][x], data[sensitive_attr], data[label])\n", " )\n", " if return_list == True:\n", " return data[\"weights\"].to_list()\n", " else:\n", " return label_dict\n", " except Exception as err:\n", " print(err)\n", " print(\"Dataframe might have no entries.\")\n", "\n", "\n", "reweigh_dict = reweighing(train_data, \"credit_risk\", \"age_groups\", False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Train a Classifier (Implement)\n", "(Go to top)\n", "\n", "Train the [LogisticRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html) pipeline." ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Implement here" ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Training the model with re-weighting\n", "\n", "# Get train data to train the classifier\n", "X_train = processed_train_df.drop([\"age_groups\"], axis=1)\n", "y_train = train_data[model_target]\n", "\n", "# Fit the classifier to the train data\n", "train_sample_weights = list(\n", " map(\n", " lambda x, y: reweigh_dict[y][x],\n", " train_df[\"age_groups\"],\n", " train_df[model_target],\n", " )\n", ")\n", "\n", "# Initialize the LR\n", "lr_rw = LogisticRegression(solver=\"liblinear\")\n", "\n", "# Make sure to add the sample weights\n", "rw_model = lr_rw.fit(X_train, y_train.ravel(), sample_weight=train_sample_weights)" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Training the model with disparate impact remover\n", "\n", "# Get train data to train the classifier\n", "X_train_transform = df_transform_train.drop([model_target, \"age_groups\"], axis=1)\n", "y_train = train_data[model_target]\n", "\n", "# Initialize the LR\n", "lr_di = LogisticRegression(solver=\"liblinear\")\n", "\n", "# Fit the classifier to the transformed data\n", "di_model = lr_di.fit(X_train_transform, y_train.ravel())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Make Predictions on the Test Dataset (Implement)\n", "(Go to top)\n", "\n", "Use the trained classifier to predict the labels on the test set. Below you will find a code snippet that evaluates for DI." ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Implement here\n", "\n", "# Get test data to test the classifier\n", "# ! test data should come from german_credit_test.csv !\n", "# ...\n", "\n", "# Use the trained model to make predictions on the test dataset\n", "# test_predictions = ..." ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Get test data to validate the classifier\n", "X_test = processed_test_df.drop([\"age_groups\"], axis=1).copy(deep=True)\n", "X_test_di = df_transform_test.drop([\"age_groups\"], axis=1).copy(deep=True)\n", "\n", "# Use the fitted model to make predictions on the test dataset\n", "test_predictions_rw = di_model.predict(X_test)\n", "\n", "# Use the fitted model to make predictions on the test dataset\n", "test_predictions_di = rw_model.predict(X_test_di)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Decide which prediction you want to evaluate and assign to `test_predictions`." ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [], "source": [ "test_predictions = test_predictions_di" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Evaluate Results (Given)\n", "(Go to top)" ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "tags": [] }, "outputs": [], "source": [ "result_df = pd.DataFrame(columns=[\"ID\", \"credit_risk_pred\"])\n", "result_df[\"ID\"] = test_data[\"ID\"].tolist()\n", "result_df[\"credit_risk_pred\"] = test_predictions\n", "\n", "result_df.to_csv(\"../../data/final_project/project_day2_result.csv\", index=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Final Evaluation on Test Data - Disparate Impact\n", "To evaluate the fairness of the model predictions, we will calculate the disparate impact (DI) metric. For more details about DI you can have a look [here](https://docs.aws.amazon.com/sagemaker/latest/dg/clarify-post-training-bias-metric-di.html)." ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "tags": [] }, "outputs": [], "source": [ "def calculate_di(test_data, pred_df, pred_col=\"credit_risk_pred\"):\n", " \"\"\"\n", " Function to calculate Disparate Impact metric using the results from this notebook.\n", " \"\"\"\n", " try:\n", " # Merge predictions with original test data to model per group\n", " di_df = pred_df.merge(test_data, on=\"ID\")\n", " # Count for group with members less than 25y old\n", " pos_outcomes_less25 = di_df[di_df[\"age_groups\"] == 0][pred_col].value_counts()[\n", " 0\n", " ] # value_counts()[0] takes the count of the '0 credit risk' == 'not credit risk'\n", " total_less25 = len(di_df[di_df[\"age_groups\"] == 0])\n", " # Count for group with members greater equal 25y old\n", " pos_outcomes_geq25 = di_df[di_df[\"age_groups\"] == 1][pred_col].value_counts()[\n", " 0\n", " ] # value_counts()[0] takes the count of the '0 credit risk' == 'not credit risk'\n", " total_geq25 = len(di_df[di_df[\"age_groups\"] == 1])\n", " # Check if correct number of gorups\n", " if total_geq25 == 0:\n", " print(\"There is only one group present in the data.\")\n", " elif total_less25 == 0:\n", " print(\"There is only one group present in the data.\")\n", " else:\n", " disparate_impact = (pos_outcomes_less25 / total_less25) / (\n", " pos_outcomes_geq25 / total_geq25\n", " )\n", " return disparate_impact\n", " except:\n", " print(\"Wrong inputs provided.\")" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "0.7338129496402878" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "calculate_di(test_data, result_df, \"credit_risk_pred\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Final Evaluation on Test Data - Accuracy & F1 Score\n", "In addition to fairness evaluation, we also need to check the general model performance. During the EDA stage we learned that the target distribution is skewed so we will use F1 score in addition to accuracy." ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "0.785" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "accuracy_score(\n", " pd.read_csv(\"../../data/final_project/german_credit_test_labels.csv\")[\n", " \"credit_risk\"\n", " ],\n", " result_df[\"credit_risk_pred\"],\n", ")" ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "0.5825242718446603" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f1_score(\n", " pd.read_csv(\"../../data/final_project/german_credit_test_labels.csv\")[\n", " \"credit_risk\"\n", " ],\n", " result_df[\"credit_risk_pred\"],\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is the end of the notebook." ] } ], "metadata": { "kernelspec": { "display_name": "conda_pytorch_p39", "language": "python", "name": "conda_pytorch_p39" }, "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.9.15" } }, "nbformat": 4, "nbformat_minor": 4 }