{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Law, Order, and Algorithms\n", "## Algorithmic fairness (2/2)\n", "\n", "Today, we will continue building and evaluating our own risk assessment tool using the COMPAS data to examine some other aspects of fairness.\n", "\n", "To recap from last week, let's start by loading the data and refitting the model:"]}, {"cell_type": "code", "execution_count": 0, "metadata": {}, "outputs": [], "source": ["# Some initial setup\n", "options(digits = 3)\n", "library(tidyverse) \n", "\n", "theme_set(theme_bw())\n", "\n", "# Because huge plots are ugly\n", "options(repr.plot.width = 6, repr.plot.height = 4)\n", "\n", "# Read the data\n", "compas_df <- read_rds(\"../data/compas.rds\")\n", "\n", "# Recap the model\n", "recid_model <- glm(is_recid ~ priors_count + age, data = compas_df, family = \"binomial\")\n", "compas_df <- compas_df %>%\n", " mutate(\n", " risk = predict(recid_model, type = \"response\"),\n", " risk_bin = round(risk * 10),\n", " binary_recid = risk >= 0.5\n", " )"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## COMPAS data revisited"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Recall that the cleaned version of the COMPAS data is loaded as `compas_df`, with the following columns\n", "\n", "* `id`: unique identifiers for each case\n", "* `sex`, `dob`, `age`, `race`: demographic information for each defendant\n", "* `recid_score`, `violence_score`: COMPAS scores assessing risk that a defendant will recidivate (`violence_score` for violent crimes) within two years of release (higher score correspond to higher risk)\n", "* `priors_count`: number of prior arrests\n", "* `is_recid`, `is_violent_recid`: Indicator variable that is `1` if the defendant was arrested for a new (violent) crime within two years of release, and `0` otherwise.\n", "\n", "and after fitting our model, we have added the following columns\n", "\n", "* `risk`: the model-predicted probability of recidivism\n", "* `predicted_risk_score`: a integer risk score between 0 and 10\n", "* `pred_recid`: a binary prediction of whether each defendant will recidivate"]}, {"cell_type": "code", "execution_count": 0, "metadata": {}, "outputs": [], "source": ["head(compas_df)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Exercise 1: Calibration by gender"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Last week we examined how our recidivism prediction model performed for different racial groups, and it turned out our model was well calibrated for white and Black defendants.\n", "\n", "For this exercise, we will continue examining calibration of our model predictions, but for different genders.\n", "\n", "We will reuse the `risk_bin` we calculated last week, which is a discretized (rounded) version of predicted risk probability.\n", "\n", "For Exercise 1, calculate recidivism rates for male and female defendants in our dataset by creating a data frame called `calibration_by_gender` containing three columns: `sex`, `risk_bin`, and `recidivism_rate`.\n", "Additionally, to ensure we have enough defendents of each gender in every score bucket, we will limit our maximum score to 8 and remove everyone with score greater than 8."]}, {"cell_type": "code", "execution_count": 0, "metadata": {}, "outputs": [], "source": ["# Calculate discretized risk score\n", "# group people with risk score equal or gretaer than 8\n", "\n", "calibration_by_gender <- compas_df %>%\n", "# WRITE CODE HERE\n", "\n", "# Put the recidivism rates of different races side by side\n", "calibration_by_gender %>%\n", " spread(sex, recidivism_rate) %>%\n", " group_by(risk_bin) %>%\n", " summarize(\n", " n = sum(n),\n", " Female = first(na.omit(Female)),\n", " Male = first(na.omit(Male))\n", " )"]}, {"cell_type": "code", "execution_count": 0, "metadata": {}, "outputs": [], "source": ["# Calibration plot\n", "\n", "ggplot(compas_df, \n", " aes(x = risk_bin, y = is_recid, color = sex, group=sex)) + \n", " geom_smooth(method=\"glm\", method.args=list(family=\"binomial\")) +\n", " scale_y_continuous(labels = scales::percent_format(), limits = c(0, 1))+\n", " scale_x_continuous(breaks = seq(0, 10, 2), limits = c(1, 10))+\n", " labs(x = \"\\nDiscretized risk score\",\n", " y = \"Recidivism rate\\n\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["#### Discussion\n", "Given the plot above, do you think a gender-blind model is \"fair\"?"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Exercise 2: Re-fit the model by including gender"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Note that we observe roughly up to a 1-point difference for male and female risk scores from the plot above.\n", "For example, male defendants who were scored as `4` recidivated at a rate of 40%, while females who recidivated at a similar rate were given a higher score of `5`. \n", "Because the model is \"blind\" to gender, women have lower risk compared to their male counterparts who have the same score.\n", "\n", "One way to reduce this gender disparity is to explicitly include gender (`sex`) as a variable.\n", "In this exercise, build a gender-aware ricidivism prediction model with `priors_count`, `age`, `sex` and add two columns `gender_specific_risk` and `gender_specific_risk_bin` (the prediction rounded to the nearest 10%) to the data frame."]}, {"cell_type": "code", "execution_count": 0, "metadata": {}, "outputs": [], "source": ["# Refit the model by including gender, look at the coefficients of the fitted model, \n", "# and generate gender-specific recidivism rate by risk score\n", "\n", "# WRITE CODE HERE\n"]}, {"cell_type": "code", "execution_count": 0, "metadata": {}, "outputs": [], "source": ["# compute calibration by gender\n", "calibration_by_gender <- compas_df %>%\n", " filter(gender_specific_risk_score <= 8) %>%\n", " group_by(sex, gender_specific_risk_score) %>%\n", " summarize(recidivism_rate = mean(is_recid))\n", "\n", "# Put the recidivism rates of different races side by side\n", "calibration_by_gender %>%\n", " spread(sex, recidivism_rate)"]}, {"cell_type": "code", "execution_count": 0, "metadata": {}, "outputs": [], "source": ["# Calibration plot\n", "ggplot(compas_df, \n", " aes(x = gender_specific_risk_score, y = is_recid, color = sex, group=sex)) + \n", " geom_smooth(method=\"glm\", method.args=list(family=\"binomial\")) +\n", " scale_y_continuous(labels = scales::percent_format(), limits = c(0, 1))+\n", " scale_x_continuous(breaks = seq(0, 10, 2), limits = c(1, 10))+\n", " labs(x = \"\\nDiscretized risk score\",\n", " y = \"Recidivism rate\\n\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Exercise 3: Compare gender-specific and gender-blind models"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now, let's compare our gender-specific and gender-blind models by examining the number of men and women detained at a detention risk threshold of 50%."]}, {"cell_type": "code", "execution_count": 0, "metadata": {}, "outputs": [], "source": ["# Calculate number of men and women detained for gender-specific and gender-blind models\n", "\n", "# WRITE CODE HERE\n", "compas_df %>%\n"]}, {"cell_type": "markdown", "metadata": {}, "source": ["By including gender in our model, we are able to obtain a calibrated model with fewer number of women detained.\n", "However, by explicitly using gender, we violate anti-classification.\n", "What do you think of this approach?"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Exercise 4: False positive rate and false negative rate"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We now introduce _false positive rate (FPR)_ and _false negative rate (FNR)_, two common metrics for evaluating model performance.\n", "\n", "In our application, the false positive rate is the proporition of people who are flagged as high risk by the algorithm, among those who ultimately did not recidivate. Conversely, the false negative rate is the proportion of people who are flagged as low risk by the algorithmic, among those who ultimately did recidivate. \n", "\n", "To more formally define these error rates, we introduce a few more terms:\n", "\n", "* $N_+$: the number of real positive cases in the data\n", "* $N_-$: the number of real negative cases in the data\n", "* TP: true positives; the number of predicted positive cases that were real positives \n", "* TN: true negatives; the number of predicted negative cases that were real negatives \n", "* FP: false positives; the number of predicted positives that were actually negative in the data (false alarms, Type I error)\n", "* FN: false negatives; the number of predicted negatives that were actually positive in the data (Type II error) \n", "\n", "Their definitions can be illustrated using following table:\n", "\n", "\n", "|
| Real positive | Real negative |\n", "|--------------------|---------------|---------------|\n", "| Predicted positive | TP | FP |\n", "| Predicted negative | FN | TN |\n", "\n", "\n", "Then the false positive rate (FPR) is given by: \n", "\n", "\\begin{equation}\n", " FPR = \\frac{FP}{N_-} = \\frac{FP}{FP + TN}\n", "\\end{equation}\n", "\n", "Similarly, the false negative rate (FNR) is given by: \n", "\n", "\\begin{equation}\n", " FNR = \\frac{FN}{N_+} = \\frac{FN}{TP + FN}\n", "\\end{equation}"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Coming back to the racial disparity we observed last week, in this exercise, let's calculate our model's FPR and FNR for white and Black defendants using a threshold of 50\\% for our binary prediction (`binary_recid`)."]}, {"cell_type": "code", "execution_count": 0, "metadata": {}, "outputs": [], "source": ["# Complete the function calc_fpr_fnr, which takes a data frame that has at least three columns: race, is_recid, and binary_recid,\n", "# and returns a data frame with three columns: race, FPR, and FNR\n", "\n", "calc_fpr_fnr <- function(df) {\n", "# WRITE CODE HERE\n", "}\n", "\n", "calc_fpr_fnr(compas_df)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["#### Discuss!"]}, {"cell_type": "markdown", "metadata": {}, "source": ["### Exercise 5: Equalizing false positive rates"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Some have advocated for equalizing FPR to create a \"fair\" model.\n", "One way to do that is to set different thresholds for white defendants and Black defendants.\n", "To do so, we will fix our classification threshold for white defendants at 50% and tune the threshold for Black defendants so that the false positive rates are equal for both groups.\n", "\n", "Similarly, find the threshold for Black defendants that equalizes the false negative rates for both groups."]}, {"cell_type": "code", "execution_count": 0, "metadata": {}, "outputs": [], "source": ["white_threshold = 0.5\n", "black_threshold = 0.5 # WRITE CODE HERE\n", "\n", "# Calculate detention and recidivism rate by race\n", "compas_df %>%\n", " mutate(binary_recid = risk > if_else(race == \"Caucasian\", white_threshold, black_threshold)) %>%\n", " calc_fpr_fnr()\n", "\n", "# See where the thresholds are on the risk distribution\n", "options(repr.plot.width = 7, repr.plot.height = 3.5)\n", "\n", "# Recall this risk distribution plot\n", "# Now we add our thresholds in the plots\n", "ggplot(compas_df, aes(x = risk, fill = race)) +\n", " geom_density(alpha = 0.5, color = NA) +\n", " scale_x_continuous(\"Estimated risk\", labels = scales::percent_format(), expand = c(0, 0)) +\n", " scale_y_continuous(element_blank(), expand = c(0, 0)) +\n", " theme(axis.ticks.y = element_blank(),\n", " axis.text.y = element_blank())+\n", " geom_vline(\n", " xintercept = c(black_threshold, white_threshold), \n", " color = c(\"red\", \"blue\"),\n", " alpha = 0.5\n", " )"]}, {"cell_type": "markdown", "metadata": {}, "source": ["#### Discuss."]}], "metadata": {"kernelspec": {"display_name": "R", "language": "R", "name": "ir"}, "language_info": {"codemirror_mode": "r", "file_extension": ".r", "mimetype": "text/x-r-source", "name": "R", "pygments_lexer": "r", "version": "3.6.3"}}, "nbformat": 4, "nbformat_minor": 4}