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 :

  1. Load the data : we load the student grades dataset as a pandas DataFrame

  2. Data Exploration : some preliminary analysis of the data

  3. Train a Model : we train a simple linear regression model (sklearn)

  4. 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]:
[Dataset]
Instances: 395
Features: X , y , p_attrs
[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: >
../../../../_images/gallery_tutorials_bias_measuring_bias_measuring_bias_regression_11_1.png

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'>
../../../../_images/gallery_tutorials_bias_measuring_bias_measuring_bias_regression_13_1.png

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'>
../../../../_images/gallery_tutorials_bias_measuring_bias_measuring_bias_regression_15_1.png

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]:
[DatasetDict]
train [Dataset]
Instances: 276
Features: X , y , p_attrs
test [Dataset]
Instances: 119
Features: X , y , p_attrs
[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