Files
udlbook/Notebooks/Chap21/21_1_Bias_Mitigation.ipynb
2023-11-24 11:18:20 +01:00

441 lines
18 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": [],
"authorship_tag": "ABX9TyNQPfTDV6PFG7Ctcl+XVNlz",
"include_colab_link": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/github/udlbook/udlbook/blob/main/Notebooks/Chap21/21_1_Bias_Mitigation.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "markdown",
"source": [
"# **Notebook 21.1: Bias mitigation**\n",
"\n",
"This notebook investigates a post-processing method for bias mitigation (see figure 21.2 in the book). It based on this [blog](https://www.borealisai.com/research-blogs/tutorial1-bias-and-fairness-ai/) that I wrote for Borealis AI in 2019, which itself was derived from [this blog](https://research.google.com/bigpicture/attacking-discrimination-in-ml/) by Wattenberg, Viégas, and Hardt.\n",
"\n",
"Work through the cells below, running each cell in turn. In various places you will see the words \"TO DO\". Follow the instructions at these places and make predictions about what is going to happen or write code to complete the functions.\n",
"\n",
"Contact me at udlbookmail@gmail.com if you find any mistakes or have any suggestions.\n"
],
"metadata": {
"id": "t9vk9Elugvmi"
}
},
{
"cell_type": "code",
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt"
],
"metadata": {
"id": "yC_LpiJqZXEL"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"# Worked example: loans\n",
"\n",
"Consider the example of an algorithm $c=\\mbox{f}[\\mathbf{x},\\boldsymbol\\phi]$ that predicts credit rating scores $c$ for loan decisions. There are two pools of loan applicants identified by the variable $p\\in\\{0,1\\}$ that well describe as the blue and yellow populations. We assume that we are given historical data, so we know both the credit rating and whether the applicant actually defaulted on the loan ($y=0$) or\n",
" repaid it ($y=1$).\n",
"\n",
"We can now think of four groups of data corresponding to (i) the blue and yellow populations and (ii) whether they did or did not repay the loan. For each of these four groups we have a distribution of credit ratings (figure 1). In an ideal world, the two distributions for the yellow population would be exactly the same as those for the blue population. However, as figure 1 shows, this is clearly not the case here."
],
"metadata": {
"id": "2FYo1dWGZXgg"
}
},
{
"cell_type": "code",
"source": [
"# Class that can describe interesting curve shapes based on the input parameters\n",
"# Details dont' matter\n",
"class FreqCurve:\n",
" def __init__(self, weight, mean1, mean2, sigma1, sigma2, prop):\n",
" self.mean1 = mean1\n",
" self.mean2 = mean2\n",
" self.sigma1 = sigma1\n",
" self.sigma2 = sigma2\n",
" self.prop = prop\n",
" self.weight = weight\n",
"\n",
" def freq(self, x):\n",
" return self.weight * self.prop * np.exp(-0.5 * (x-self.mean1) * (x-self.mean1) / (self.sigma1 * self.sigma1)) \\\n",
" * 1.0 / np.sqrt(2*np.pi*self.sigma1*self.sigma1) \\\n",
" + self.weight * (1-self.prop) * np.exp(-0.5 * (x-self.mean2) * (x-self.mean2) / (self.sigma2 * self.sigma2)) \\\n",
" * 1.0 / np.sqrt(2*np.pi*self.sigma2*self.sigma2)\n"
],
"metadata": {
"id": "O_0gGH9hZcjo"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"credit_scores = np.arange(-4,4,0.01)\n",
"freq_y0_p0 = FreqCurve(800, -1.5, -2.5, 0.8, 0.6, 0.6).freq(credit_scores)\n",
"freq_y1_p0 = FreqCurve(500, 0.1, 0.7, 1.5, 0.8, 0.4 ).freq(credit_scores)\n",
"freq_y0_p1 = FreqCurve(400, 0.2, -0.1, 0.8, 0.6, 0.3).freq(credit_scores)\n",
"freq_y1_p1 = FreqCurve(650, 0.6, 1.6, 1.2, 0.7, 0.6 ).freq(credit_scores)\n"
],
"metadata": {
"id": "Bkp7vffBbrNW"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"\n",
"fig = plt.figure\n",
"ax = plt.subplot(2,2,1)\n",
"plt.tight_layout()\n",
"ax.plot(credit_scores, freq_y0_p0, 'b--', label='y=0 (defaulted)')\n",
"ax.plot(credit_scores, freq_y1_p0, 'b-', label='y=1 (repaid)')\n",
"ax.set_xlim(-4,4)\n",
"ax.set_ylim(0,500)\n",
"ax.set_xlabel('Credit score, $c$')\n",
"ax.set_ylabel('Frequency')\n",
"ax.set_title('Population p=0')\n",
"ax.legend()\n",
"\n",
"ax = plt.subplot(2,2,2)\n",
"plt.tight_layout()\n",
"ax.plot(credit_scores, freq_y0_p1, 'y--', label='y=0 (defaulted)')\n",
"ax.plot(credit_scores, freq_y1_p1, 'y-', label='y=1 (repaid)')\n",
"ax.set_xlim(-4,4)\n",
"ax.set_ylim(0,500)\n",
"ax.set_xlabel('Credit score, $c$')\n",
"ax.set_ylabel('Frequency')\n",
"ax.set_title('Population p=1')\n",
"ax.legend()\n",
"\n",
"plt.show()"
],
"metadata": {
"id": "Jf7uqyRyhVdS"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Why might the distributions for blue and yellow populations be different? It could be that the behaviour of the populations is identical, but the credit rating algorithm is biased; it may favor one population over another or simply be more noisy for one group. Alternatively, it could be that that the populations genuinely behave differently. In practice, the differences in blue and yellow distributions are probably attributable to a combination of these factors.\n",
"\n",
"Lets assume that we cant retrain the credit score prediction algorithm; our job is to adjudicate whether each individual is refused the loan ($\\hat{y}=0$)\n",
" or granted it ($\\hat{y}=1$). Since we only have the credit score\n",
" to go on, the best we can do is to assign different thresholds $\\tau_{1}$\n",
" and $\\tau_{2}$\n",
" for the blue and yellow populations so that the loan is granted if the credit score $c$ generated by the model exceeds $\\tau_0$ for the blue population and $\\tau_1$ for the yellow population."
],
"metadata": {
"id": "CfZ-srQtmff2"
}
},
{
"cell_type": "markdown",
"source": [
"Now let's investiate how to set these thresholds to fulfil different criteria."
],
"metadata": {
"id": "569oU1OtoFz8"
}
},
{
"cell_type": "markdown",
"source": [
"# Blindness to protected attribute\n",
"\n",
"We'll first do the simplest possible thing. We'll choose the same threshold for both blue and yellow populations so that $\\tau_0$ = $\\tau_1$. Basically, we'll ignore what we know about the group membership. Let's see what the ramifications of that."
],
"metadata": {
"id": "bE7yPyuWoSUy"
}
},
{
"cell_type": "code",
"source": [
"# Set the thresholds\n",
"tau0 = tau1 = 0.0"
],
"metadata": {
"id": "WIG8I-LvoFBY"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"def compute_probability_get_loan(credit_scores, frequencies, threshold):\n",
" # TODO - Write this function\n",
" # Return the probability that someone from this group loan based on the frequencies of each\n",
" # credit score for this group\n",
" # Replace this line:\n",
" prob = 0.5\n",
"\n",
"\n",
" return prob"
],
"metadata": {
"id": "2EvkCvVBiCBn"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"First let's see what the overall probability of getting the loan is for the yellow and blue populations."
],
"metadata": {
"id": "AGT40q6_qfpv"
}
},
{
"cell_type": "code",
"source": [
"pr_get_loan_p0 = compute_probability_get_loan(credit_scores, freq_y0_p0+freq_y1_p0, tau0)\n",
"pr_get_loan_p1 = compute_probability_get_loan(credit_scores, freq_y0_p1+freq_y1_p1, tau1)\n",
"print(\"Probability blue group gets loan = %3.3f\"%(pr_get_loan_p0))\n",
"print(\"Probability yellow group gets loan = %3.3f\"%(pr_get_loan_p1))"
],
"metadata": {
"id": "4nI-PR_wqWj6"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Now let's plot a receiver operating characteristic (ROC) curve. This shows the rate of true positives $Pr(\\hat{y}=1|y=1)$ (people who got loan and paid it back) and false alarms $Pr(\\hat{y}=1|y=0)$ (people who got the loan but didn't pay it back) for all possible thresholds."
],
"metadata": {
"id": "G2pEa6h6sIyu"
}
},
{
"cell_type": "code",
"source": [
"def plot_roc(credit_scores, freq_y0_p0, freq_y1_p0, freq_y0_p1, freq_y1_p1, tau0, tau1):\n",
" true_positives_p0 = np.zeros_like(credit_scores)\n",
" false_alarms_p0 = np.zeros_like(credit_scores)\n",
" true_positives_p1 = np.zeros_like(credit_scores)\n",
" false_alarms_p1 = np.zeros_like(credit_scores)\n",
" for i in range(len(credit_scores)):\n",
" true_positives_p0[i] = compute_probability_get_loan(credit_scores, freq_y1_p0, credit_scores[i])\n",
" true_positives_p1[i] = compute_probability_get_loan(credit_scores, freq_y1_p1, credit_scores[i])\n",
" false_alarms_p0[i] = compute_probability_get_loan(credit_scores, freq_y0_p0, credit_scores[i])\n",
" false_alarms_p1[i] = compute_probability_get_loan(credit_scores, freq_y0_p1, credit_scores[i])\n",
"\n",
" true_positives_p0_tau0 = compute_probability_get_loan(credit_scores, freq_y1_p0, tau0)\n",
" true_positives_p1_tau1 = compute_probability_get_loan(credit_scores, freq_y1_p1, tau1)\n",
" false_alarms_p0_tau0 = compute_probability_get_loan(credit_scores, freq_y0_p0, tau0)\n",
" false_alarms_p1_tau1 = compute_probability_get_loan(credit_scores, freq_y0_p1, tau1)\n",
"\n",
" fig, ax = plt.subplots()\n",
" ax.plot(false_alarms_p0, true_positives_p0, 'b-')\n",
" ax.plot(false_alarms_p1, true_positives_p1, 'y-')\n",
" ax.plot(false_alarms_p0_tau0, true_positives_p0_tau0,'bo')\n",
" ax.plot(false_alarms_p1_tau1, true_positives_p1_tau1,'yo')\n",
" ax.set_xlim(0,1)\n",
" ax.set_ylim(0,1)\n",
" ax.set_xlabel('False alarms $Pr(\\hat{y}=1|y=0)$')\n",
" ax.set_ylabel('True positives $Pr(\\hat{y}=1|y=1)$')\n",
" ax.set_aspect('equal')\n",
"\n",
" plt.show()"
],
"metadata": {
"id": "2C7kNt3hqwiu"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"plot_roc(credit_scores, freq_y0_p0, freq_y1_p0, freq_y0_p1, freq_y1_p1, tau0, tau1)"
],
"metadata": {
"id": "h3OOQeTsv8uS"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"On this plot, the true positive and false alarm rate for the particular thresholds ($\\tau_0=\\tau_{1}=0$) that we chose are indicated by the circles.\n",
"\n",
"This criterion is clearly not great. The blue and yellow groups get given loans at different rates overall, and (for this threshold), the false alarms and true positives are also different, so it's not even fair when we consider whether the loans really were paid back. \n",
"\n",
"TODO -- investigate setting a different threshold $\\tau_{0}=\\tau_{1}$. Is it possible to make the overall rates that loans are given the same? Is it possible to make the false alarm rates the same? Is it possible to make the true positive rates the same?"
],
"metadata": {
"id": "UCObTsa57uuC"
}
},
{
"cell_type": "markdown",
"source": [
"# Equality of odds\n",
"\n",
"This definition of fairness proposes that the false positive and true positive rates should be the same for both populations. This also sounds reasonable, but the ROC curve shows that it is not possible for this example. There is no combination of thresholds that can achieve this because the ROC curves do not intersect. Even if they did, we would be stuck giving loans based on the particular false positive and true positive rates at the intersection which might not be desirable."
],
"metadata": {
"id": "Yhrxv5AQ-PWA"
}
},
{
"cell_type": "markdown",
"source": [
"Demographic parity\n",
"\n",
"The thresholds can be chosen so that the same proportion of each group are classified as $\\hat{y}=1$ and given loans. We make an equal number of loans to each group despite the different tendencies of each to repay. This has the disadvantage that the true positive and false positive rates might be completely different in different populations. From the perspective of the lender, it is desirable to give loans in proportion to peoples ability to pay them back. From the perspective of an individual in a more reliable group, it may seem unfair that the other group gets offered the same number of loans despite the fact they are less reliable."
],
"metadata": {
"id": "l6yb8vjX-gdi"
}
},
{
"cell_type": "code",
"source": [
"# TO DO -- try to change the two thresholds so the overall probability of getting the loan is 0.6 for each group\n",
"# Change the values in these lines\n",
"tau0 = 0.3\n",
"tau1 = -0.1\n",
"\n",
"\n",
"\n",
"# Compute overall probability of getting loan\n",
"pr_get_loan_p0 = compute_probability_get_loan(credit_scores, freq_y0_p0+freq_y1_p0, tau0)\n",
"pr_get_loan_p1 = compute_probability_get_loan(credit_scores, freq_y0_p1+freq_y1_p1, tau1)\n",
"print(\"Probability blue group gets loan = %3.3f\"%(pr_get_loan_p0))\n",
"print(\"Probability yellow group gets loan = %3.3f\"%(pr_get_loan_p1))"
],
"metadata": {
"id": "syjZ2fn5wC9-"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"This is good, because now both groups get roughly the same amount of loans. But hold on... let's look at the ROC curve:"
],
"metadata": {
"id": "5QrtvZZlHCJy"
}
},
{
"cell_type": "code",
"source": [
"plot_roc(credit_scores, freq_y0_p0, freq_y1_p0, freq_y0_p1, freq_y1_p1, tau0, tau1)"
],
"metadata": {
"id": "VApyl_58GUQb"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"The blue dot is waaay above the yellow dot. The proportion of people who are given a load and do pay it back from the blue population is much higher than that from the yellow population. From another perspective, that's unfair... it seems like the yellow population are 'allowed' to default more often than the blue. This leads to another possibility."
],
"metadata": {
"id": "_GgX_b6yIE4W"
}
},
{
"cell_type": "markdown",
"source": [
"# Equal opportunity:\n",
"\n",
"The thresholds are chosen so that so that the true positive rate is is the same for both population. Of the people who pay back the loan, the same proportion are offered credit in each group. In terms of the two ROC curves, it means choosing thresholds so that the vertical position on each curve is the same without regard for the horizontal position."
],
"metadata": {
"id": "WDnaqetXHhlv"
}
},
{
"cell_type": "code",
"source": [
"# TO DO -- try to change the two thresholds so the true positive are 0.8 for each group\n",
"# Change the values in these lines so that both points on the curves have a height of 0.8\n",
"tau0 = -0.1\n",
"tau1 = -0.7\n",
"\n",
"\n",
"plot_roc(credit_scores, freq_y0_p0, freq_y1_p0, freq_y0_p1, freq_y1_p1, tau0, tau1)"
],
"metadata": {
"id": "zEN6HGJ7HJAZ"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"This seems fair -- people who are given loans default at the same rate (20%) for both groups. But hold on... let's look at the overall loan rate between the two populations:"
],
"metadata": {
"id": "JsyW0pBGJ24b"
}
},
{
"cell_type": "code",
"source": [
"# Compute overall probability of getting loan\n",
"pr_get_loan_p0 = compute_probability_get_loan(credit_scores, freq_y0_p0+freq_y1_p0, tau0)\n",
"pr_get_loan_p1 = compute_probability_get_loan(credit_scores, freq_y0_p1+freq_y1_p1, tau1)\n",
"print(\"Probability blue group gets loan = %3.3f\"%(pr_get_loan_p0))\n",
"print(\"Probability yellow group gets loan = %3.3f\"%(pr_get_loan_p1))"
],
"metadata": {
"id": "2a5PXHeNJDjg"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"The conclusion from all this is that (i) definitions of fairness are quite subtle and (ii) it's not possible to satisfy them all simultaneously."
],
"metadata": {
"id": "tZTM7N6jKC7q"
}
}
]
}