Measuring Bias in regression#
This notebook is a tutorial on auditing bias within a regression task. We will use the holisticai library thoughout, introducing some of the functions we have created to help study algorithmic bias.
The sections are organised as follows :
Load the data : we load the student grades dataset as a pandas DataFrame
Data Exploration : some preliminary analysis of the data
Train a Model : we train a simple linear regression model (sklearn)
Measure Bias : we compute a few bias metrics, and comment on their meaning
Load the data#
First of all, we need to import the required packages to perform our bias analysis and mitigation. You will need to have the holisticai package installed on your system, remember that you can install it by running:
!pip install holisticai[all]
[1]:
# Imports
import numpy as np
import warnings
warnings.filterwarnings("ignore")
We host a few example datasets on the holisticai library for quick loading and experimentation. Here we load and use the Student dataset. The goal of this dataset is the prediction of the numerical attribute ‘G3’ (mathematics grade of student in 3rd trimester). There are a number of sensitive attributes in this dataset, some of which are : sex, address, Mjob (mother’s job), Fjob (father’s job)…
Although the load_dataset function returns a preprocessed version, in this opportunity we will perform this processing by ourselves since the default preprocessing is more suitable for multiclassification tasks.
[2]:
from holisticai.datasets import load_dataset
dataset = load_dataset('student', preprocessed=True)
dataset
[2]:
[3]:
dataset['p_attrs']
[3]:
| sex | address | Mjob | Fjob | |
|---|---|---|---|---|
| 0 | F | U | at_home | teacher |
| 1 | F | U | at_home | other |
| 2 | F | U | at_home | other |
| 3 | F | U | health | services |
| 4 | F | U | other | other |
| ... | ... | ... | ... | ... |
| 390 | M | U | services | services |
| 391 | M | U | services | services |
| 392 | M | R | other | other |
| 393 | M | R | services | other |
| 394 | M | U | other | at_home |
395 rows × 4 columns
Data Exploration#
We import some of the holisticai plotters for quick exploration of the data.
[4]:
from holisticai.bias.plots import group_pie_plot
from holisticai.bias.plots import distribution_plot
from holisticai.bias.plots import success_rate_curves
[5]:
group_pie_plot(dataset['p_attrs']['sex'])
[5]:
<Axes: >
The data is balanced in terms of sex.
[6]:
# distribution of grades for male an female
distribution_plot(dataset['y'], dataset['p_attrs']['sex'])
[6]:
<Axes: xlabel='y', ylabel='Density'>
The Mother’s job attribute is the one that shows most difference in the densities of grades. For instance we observe students with a mother working in health have higher density at higher grades.
[7]:
p_attr = np.array(dataset['p_attrs']['Fjob'])
y = np.array(dataset['y'])
success_rate_curves(p_attr, y, groups=['at_home', 'health', 'teacher'])
[7]:
<Axes: title={'center': 'Success Rate Curves'}, xlabel='Score'>
The above shows the success rate (sucess is exceeding the given threshold) as a function of threshold for different subgroups of the population : Father’s job in [‘at_home’, ‘health’, ‘teacher’]. We can observe that student’s with a parent as a teacher are more likely to exceed high thresholds than other groups.
Preprocess Data and Train a model#
We use a sklearn linear regression model.
[8]:
from sklearn.linear_model import LinearRegression
[9]:
# Load, preprocess and split for training
datasets = dataset.train_test_split(test_size=0.3, random_state=42)
datasets
[9]:
[10]:
datasets['train']['p_attrs']
[10]:
| sex | address | Mjob | Fjob | |
|---|---|---|---|---|
| 0 | F | U | other | other |
| 1 | M | U | services | services |
| 2 | F | R | services | health |
| 3 | F | U | other | other |
| 4 | M | R | teacher | services |
| ... | ... | ... | ... | ... |
| 271 | M | U | other | other |
| 272 | F | U | other | other |
| 273 | F | U | other | services |
| 274 | F | U | health | other |
| 275 | M | U | services | other |
276 rows × 4 columns
[11]:
# G3 is the students final grade (drop G2 and G1 as well because highly correlated with G3)
X_train = datasets['train']['X']
X_test = datasets['test']['X']
y_train = datasets['train']['y']
y_test = datasets['test']['y']
p_attr_train = datasets['train']['p_attrs']
p_attr_test = datasets['test']['p_attrs']
# Train a simple linear regression model
model = LinearRegression()
model = model.fit(X_train, y_train)
# Predict values
y_pred = model.predict(X_test)
[12]:
from holisticai.efficacy.metrics import regression_efficacy_metrics
regression_efficacy_metrics(y_pred, y_test)
[12]:
| Value | Reference | |
|---|---|---|
| Metric | ||
| RMSE | 4.342198 | 0 |
| MAE | 3.493136 | 0 |
| MAPE | 0.364603 | 0 |
| Max Error | 11.080852 | 0 |
| SMAPE | 0.212643 | 0 |
Measure bias#
[13]:
# import some bias metrics
from holisticai.bias.metrics import statistical_parity_regression
from holisticai.bias.metrics import disparate_impact_regression
from holisticai.bias.metrics import mae_ratio
from holisticai.bias.metrics import rmse_ratio
[14]:
# set up vectors for gender
group_a = np.array(p_attr_test['sex']=="F")
group_b = np.array(p_attr_test['sex']=="M")
y_pred = np.array(model.predict(X_test))
y_true = np.array(y_test)
[15]:
# evaluate fairness metrics for gender
print ('Statistical Parity Q80 : ' + str(statistical_parity_regression(group_a, group_b, y_pred, q=0.8)))
print ('Disparate Impact Q80 : ' + str(disparate_impact_regression(group_a, group_b, y_pred, q=0.8)))
print ('MAE Ratio : ' + str(mae_ratio(group_a, group_b, y_pred, y_true)))
print ('RMSE Ratio : ' + str(rmse_ratio(group_a, group_b, y_pred, y_true)))
Statistical Parity Q80 : -0.1916336913510458
Disparate Impact Q80 : 0.3505747126436782
MAE Ratio : 1.0882701322683415
RMSE Ratio : 1.1341742548076124
All the above metrics are within acceptable ranges. This shows there isn’t much bias for the subgroups of the sex column. Let’s try the address attribute.
[16]:
# set up vectors for address
group_a = np.array(p_attr_test['address']=='U')
group_b = np.array(p_attr_test['address']=='R')
y_pred = np.array(model.predict(X_test))
y_true = np.array(y_test)
[17]:
# evaluate fairness metrics for address
print ('Statistical Parity Q80 : ' + str(statistical_parity_regression(group_a, group_b, y_pred, q=0.8)))
print ('Disparate Impact Q80 : ' + str(disparate_impact_regression(group_a, group_b, y_pred, q=0.8)))
print ('MAE Ratio : ' + str(mae_ratio(group_a, group_b, y_pred, y_true)))
print ('RMSE Ratio : ' + str(rmse_ratio(group_a, group_b, y_pred, y_true)))
Statistical Parity Q80 : 0.1476293103448276
Disparate Impact Q80 : 2.574712643678161
MAE Ratio : 0.9246558407488772
RMSE Ratio : 0.9438430657262673
The disparate impact at quantile 0.8 is outside of fair ranges (0.8, 1.2), students living in urban areas are 1.8 times more likely to be predicted in top 20% of grades than students living in rural areas.
[18]:
print ('Disparate Impact Q80 : ' + str(disparate_impact_regression(group_a, group_b, y_true, q=0.8)))
Disparate Impact Q80 : 4.2298850574712645
When we look at the metric computed on true values, we get an even worst pattern. Students living in urban areas are actually 4.2 times more likely to be in top 20% of grades than students living in rural areas.
Equality of outcome metrics (batch computation)
Use address as protected attribute
[19]:
# set up vectors for address
group_a = np.array(p_attr_test['address']=='U')
group_b = np.array(p_attr_test['address']=='R')
y_pred = np.array(model.predict(X_test))
[20]:
from holisticai.bias.metrics import regression_bias_metrics
regression_bias_metrics(group_a, group_b, y_pred, metric_type='equal_outcome')
[20]:
| Value | Reference | |
|---|---|---|
| Metric | ||
| Disparate Impact Q90 | 1.103448 | 1 |
| Disparate Impact Q80 | 2.574713 | 1 |
| Disparate Impact Q50 | 1.329797 | 1 |
| Statistical Parity Q50 | 0.147629 | 0 |
| No Disparate Impact Level | 13.519855 | - |
| Average Score Difference | 0.778703 | 0 |
| Average Score Ratio | 1.077290 | 1 |
| Z Score Difference | 0.350470 | 0 |
| Max Statistical Parity | 0.230603 | 0 |
| Statistical Parity AUC | 0.118537 | 0 |
Equality of opportunity metrics (batch computation)
Use address as protected attribute
[21]:
# set up vectors for address
group_a = p_attr_test['address']=='U'
group_b = p_attr_test['address']=='R'
y_pred = model.predict(X_test)
y_true = y_test
[22]:
regression_bias_metrics(group_a, group_b, y_pred, y_true, metric_type='equal_opportunity')
[22]:
| Value | Reference | |
|---|---|---|
| Metric | ||
| RMSE Ratio | 0.943843 | 1 |
| RMSE Ratio Q80 | 0.654919 | 1 |
| MAE Ratio | 0.924656 | 1 |
| MAE Ratio Q80 | 0.571965 | 1 |
| Correlation Difference | 0.193685 | 0 |
[23]:
regression_bias_metrics(group_a=group_a, group_b=group_b, y_pred=y_pred, y_true=y_true, metric_type='both')
[23]:
| Value | Reference | |
|---|---|---|
| Metric | ||
| Disparate Impact Q90 | 1.103448 | 1 |
| Disparate Impact Q80 | 2.574713 | 1 |
| Disparate Impact Q50 | 1.329797 | 1 |
| Statistical Parity Q50 | 0.147629 | 0 |
| No Disparate Impact Level | 13.519855 | - |
| Average Score Difference | 0.778703 | 0 |
| Average Score Ratio | 1.077290 | 1 |
| Z Score Difference | 0.350470 | 0 |
| Max Statistical Parity | 0.230603 | 0 |
| Statistical Parity AUC | 0.118537 | 0 |
| RMSE Ratio | 0.943843 | 1 |
| RMSE Ratio Q80 | 0.654919 | 1 |
| MAE Ratio | 0.924656 | 1 |
| MAE Ratio Q80 | 0.571965 | 1 |
| Correlation Difference | 0.193685 | 0 |
We can show all individual bias metrics by setting ‘metric_types’ as ‘individual’.
[24]:
regression_bias_metrics(group_a=group_a, group_b=group_b, y_pred=y_pred, y_true=y_true, metric_type='individual')
[24]:
| Value | Reference | |
|---|---|---|
| Metric | ||
| Jain Index | 0.64716 | 1 |