Algorithmic Fairness in R
Tutorial on using the fairness R package
 1. Overview
 2. Installation
 3. Data description
 4. Train a classifier
 5. Algorithmic fairness metrics
 6. Closing words
1. Overview
How to measure fairness of a machine learning model?
To date, a number of algorithmic fairness metrics have been proposed. Demographic parity, proportional parity and equalized odds are among the most commonly used metrics to evaluate fairness across sensitive groups in binary classification problems. Multiple other metrics have been proposed based on performance measures extracted from the confusion matrix (e.g., false positive rate parity, false negative rate parity).
Together with Tibor V. Varga, we developed fairness
package for R. The package provides tools to calculate algorithmic fairness metrics across different sensitive groups. It also provides opportunities to visualize and compare other prediction metrics between the groups.
This blogpost provides a tutorial on using the fairness
package on a COMPAS data set. The blogpost is also available as an interactive Kaggle notebook. The package is published on CRAN and GitHub.
The package provides functions to compute the commonly used metrics of algorithmic fairness:
 Demographic parity
 Proportional parity
 Equalized odds
 Predictive rate parity
In addition, the following comparisons are also implemented:
 False positive rate parity
 False negative rate parity
 Accuracy parity
 Negative predictive value parity
 Specificity parity
 ROC AUC comparison
 MCC comparison
#collapseshow
install.packages('fairness')
library(fairness)
You may also install the development version from Github:
#collapseshow
devtools::install_github('kozodoi/fairness')
library(fairness)
compas
In this tutorial, you will be able to use a simplified version of the landmark COMPAS data set containing the criminal history of defendants from Broward County. You can read more about the data here. To load the data set, all you need to do is:
#collapseshow
data('compas')
head(compas)
The data set contains nine variables. The outcome variable is Two_yr_Recidivism
, which is a binary indicator showing whether an individual commited a crime within the twoyear period. The data also includes features on prior criminal record (Number_of_Priors
, Misdemeanor
) and other features describing age (Age_Above_FourtyFive
, Age_Below_TwentyFive
), sex and ethnicity (Female
, ethnicity
).
You don’t really need to delve into the data much. To simplify illustration, we have already trained a classifier that uses all available features to predict Two_yr_Recidivism
and concatenated the predicted probabilities (probability
) and predicted classes (predicted
) to the data frame. You will be able to use these columns with predictions directly in your analysis to test different metrics before using a real model.
germancredit
The second data set included in the package is a credit scoring data set labeled as germancredit
. The data set includes 20 features describing the loan applicants and an outcome variable named BAD
, which is a binary indicator showing whether the applicant defaulted on a loan. Similarly to the compas data set, this data also includes two columns with model predictions named probability
and predicted
.
Feel free to play with this data as well. You can load it with:
#collapseshow
data('germancredit')
4. Train a classifier
For the purpose of this tutorial, we will train two new models using different sets of features:
 model that uses all features as input
 model that uses all features except for ethnicity
We partition the COMPAS data into training and validation subsets and use logistic regression as base classifier.
#collapseshow
# extract data
compas < fairness::compas
df < compas[, !(colnames(compas) %in% c('probability', 'predicted'))]
# partitioning params
set.seed(77)
val_percent < 0.3
val_idx < sample(1:nrow(df))[1:round(nrow(df) * val_percent)]
# partition the data
df_train < df[val_idx, ]
df_valid < df[ val_idx, ]
# check dim
print(nrow(df_train))
print(nrow(df_valid))
#collapseshow
# fit logit models
model1 < glm(Two_yr_Recidivism ~ .,
data = df_train,
family = binomial(link = 'logit'))
model2 < glm(Two_yr_Recidivism ~ . ethnicity,
data = df_train,
family = binomial(link = 'logit'))
Let's append model predictions to the validation set. Later, we will evaluate fairness of the two models based on these predictions.
#collapseshow
# produce predictions
df_valid$prob_1 < predict(model1, df_valid, type = 'response')
df_valid$prob_2 < predict(model2, df_valid, type = 'response')
head(df_valid)
5. Algorithmic fairness metrics
The package currently includes nine fairness metrics and two other performance comparisons. Many of these metrics are mutually exclusive: results from a given classification problem most often cannot be fair in terms of all group fairness metrics. Depending on a context, it is important to select an appropriate metric to evaluate fairness.
Below, we intrdocue functions that are used to compute the implemented metrics. Every function has a similar set of arguments:

data
: data.frame with the features and predictions 
outcome
: name of the outcome variable 
group
: name of the sensitive group, which needs to be a factor variable included in the data.frame. 
base
: name of the base group (factor level ofgroup
) that serves as a base for fairness metrics
We also need to supply model predictions. Depending on a metric, we need to provide either porbabilistic predictions as probs
or class predictions as preds
. The model predictions can be appended to the original data.frame or provided as a vector. In this tutorial, we will use probabilistic predctions with all functions.
When working with probabilistic predictions, some metrics also require a cutoff value to convert probabilities into class precictions supplied as cutoff
. Finally, we also need to specifiy factor levels to indicate the reference classes using the preds_levels
argument. The first level refers to the base class, whereas the second level indicates the predicted class for which probabilities are provided.
An outlook on the confusion matrix
Most fairness metrics are calculated based on a confusion matrix produced by a classification model. The confusion matrix is comprised of four distinct classes:
 True positives (TP): the true class is positive and the prediction is positive (correct classification)
 False positives (FP): the true class is negative and the prediction is positive (incorrect classification)
 True negatives (TN): the true class is negative and the prediction is negative (correct classification)
 False negatives (FN): the true class is positive and the prediction is negative (incorrect classification)
The fairness metrics are calculated by comparing one or more of these measures computed for different sensitive subgroups (e.g., male and female). For a detailed overview of various measures coming from the confusion matrix and precise definitions, please click here or here.
Predictive rate parity
Let's demonstrate the fairness pipeline using predictive rate parity as an example. Predictive rate parity is achieved if the precisions (or positive predictive values) in the subgroups are close to each other. The precision stands for the number of the true positives divided by the total number of examples predicted positive within a group.
Formula: TP / (TP + FP)
Let's compute predictive rate parity for the first model that uses all features:
#collapseshow
res1 < pred_rate_parity(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'ethnicity',
probs = 'prob_1',
preds_levels = c('no','yes'),
cutoff = 0.5,
base = 'Caucasian')
res1$Metric
The first row shows the raw precision values for all ethnicities. The second row displays the relative precisions compared to Caucasian defendants.
In a perfect world, all predictive rate parities should be equal to one, which would mean that the precision in every group is the same as in the base group. In practice, values are going to be different. The paritiy above one indicates that precision in this group is relatively higher, whereas a lower parity implies a lower precision. Observing a large variance in parities should hint us that the model is not performing equally well for different sensitive groups.
If the other ethnic group is set as a base group (e.g. Hispanic), the raw precision values do not change, only the relative metrics:
#collapseshow
res1h < pred_rate_parity(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'ethnicity',
probs = 'prob_1',
preds_levels = c('no','yes'),
cutoff = 0.5,
base = 'Hispanic')
res1h$Metric
Overall, results suggest that the model precision varies between 0.6489 and 0.7857. Apart from the "other" category, the lowest precision is observed for AfricanAmerican defendants. This implies that there are more cases where the model mistakingly predicts that a person will commit a crime among AfricanAmericans than among, e.g., Asian defendants.
A standard output of every fairness metric function includes a barchart that visualizes the relative metrics for all subgroups:
#collapseshow
res1h$Metric_plot
Some fairness metrics do not require probabilistic predictions and can work with class predictions. When predicted probabilities are supplied, an extra density plot will be output displaying the distributions of probabilities of all subgroups and the userdefined cutoff:
#collapseshow
res1h$Probability_plot
Let's now compare the results to the second model that does not use ethnicity as a feature:
#collapseshow
# model 2
res2 < pred_rate_parity(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'ethnicity',
probs = 'prob_2',
preds_levels = c('no','yes'),
cutoff = 0.5,
base = 'Caucasian')
res2$Metric
We can see two things.
First, excluding ethnicity
from the features slightly increases precision for AfricanAmerican defendants but results in a lower precision for a number of other groups. This illustrates that improving a model for one group may cost a fall in the predictive performance for the general population. Depending on the context, it is a task of a decisionmaker to decide what is best.
Second, excluding ethnicity
does not align the predictive rate parities substantially closer to one. This illustrates another important research finding: removing a sensitive variable does not guarantee that a model stops discriminating. Ethnicity correlates with other features and is still implicitly included in the input data. In order to make the classifier more fair, one would neet to consider more sophisticated techniques than simply dropping the sensitive attribute.
In the rest of this tutorial, we will go through the functions that cover the remaining implemented fairness metrics, illustrating the corresponding equations and outputs. You can find more details on each of the fairness metric functions in the package documentation. Please don't hesitate to use the builtin helper to see further details and examples on the implemented metrics:
#collapseshow
?fairness::pred_rate_parity
Demographic parity
Demographic parity is one of the most popular fairness indicators in the literature. Demographic parity is achieved if the absolute number of positive predictions in the subgroups are close to each other. This measure does not take true class into consideration and only depends on the model predictions.
Formula: (TP + FP)
#collapseshow
res_dem < dem_parity(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'ethnicity',
probs = 'prob_1',
preds_levels = c('no','yes'),
cutoff = 0.5,
base = 'Caucasian')
res_dem$Metric
#collapseshow
res_dem$Metric_plot
Of course, comparing the absolute number of positive predictions will show a high disparity when the number of cases within each group is different, which artificially boosts the disparity. This is true in our case:
#collapseshow
table(df_valid$ethnicity)
To address this, we can use proportional parity.
Proportional parity
Proportional parity is very similar to demographic parity but modifies it to address the issue discussed above. Proportional parity is achieved if the proportion of positive predictions in the subgroups are close to each other. Similar to the demographic parity, this measure also does not depend on the true labels.
Formula: (TP + FP) / (TP + FP + TN + FN)
#collapseshow
res_prop < prop_parity(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'ethnicity',
probs = 'prob_1',
preds_levels = c('no','yes'),
cutoff = 0.5,
base = 'Caucasian')
res_prop$Metric
#collapseshow
res_prop$Metric_plot
The proportional parity still shows that AfricanAmerican defendants are treated unfairly by our model. At the same time, the disparity is lower compared to the one observed with the demographic parity.
All the remaining fairness metrics account for both model predictions and the true labels.
#collapseshow
res_eq < equal_odds(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'ethnicity',
probs = 'prob_1',
preds_levels = c('no','yes'),
cutoff = 0.5,
base = 'African_American')
res_eq$Metric
#collapseshow
res_acc < acc_parity(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'ethnicity',
probs = 'prob_1',
preds_levels = c('no','yes'),
cutoff = 0.5,
base = 'African_American')
res_acc$Metric
#collapseshow
res_fnr < fnr_parity(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'ethnicity',
probs = 'prob_1',
preds_levels = c('no','yes'),
cutoff = 0.5,
base = 'African_American')
res_fnr$Metric
#collapseshow
res_fpr < fpr_parity(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'ethnicity',
probs = 'prob_1',
preds_levels = c('no','yes'),
cutoff = 0.5,
base = 'African_American')
res_fpr$Metric
Negative predictive value parity
Negative predictive value parity is achieved if the negative predictive values in the subgroups are close to each other. The negative predictive value is computed as a ratio between the number of true negatives and the total number of predicted negatives. This function can be considered the ‘inverse’ of the predictive rate parity.
Formula: TN / (TN + FN)
#collapseshow
res_npv < npv_parity(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'ethnicity',
probs = 'prob_1',
preds_levels = c('no','yes'),
cutoff = 0.5,
base = 'African_American')
res_npv$Metric
#collapseshow
res_sp < spec_parity(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'ethnicity',
probs = 'prob_1',
preds_levels = c('no','yes'),
cutoff = 0.5,
base = 'African_American')
res_sp$Metric
Apart from the paritybased metrics presented above, two additional comparisons are implemented: ROC AUC comparison and Matthews correlation coefficient comparison.
#collapseshow
res_auc < roc_parity(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'Female',
probs = 'prob_1',
preds_levels = c('no','yes'),
base = 'Male')
res_auc$Metric
Apart from the standard outputs, the function also returns ROC curves for each of the subgroups:
#collapseshow
res_auc$ROCAUC_plot
Matthews correlation coefficient comparison
The Matthews correlation coefficient (MCC) takes all four classes of the confusion matrix into consideration. MCC is sometimes referred to as the single most powerful metric in binary classification problems, especially for data with class imbalances.
Formula: (TP×TNFP×FN)/√((TP+FP)×(TP+FN)×(TN+FP)×(TN+FN))
#collapseshow
res_mcc < mcc_parity(data = df_valid,
outcome = 'Two_yr_Recidivism',
group = 'Female',
probs = 'prob_1',
preds_levels = c('no','yes'),
cutoff = 0.5,
base = 'Male')
res_mcc$Metric
6. Closing words
You have read through the fairness R package tutorial! By now, you should have a solid grip on algorithmic group fairness metrics.
We hope that you will be able to use the R package in your data analysis! Please let me know if you run into any issues while working with the package in the comment window below or on GitHub. Please also feel free to contact the authors if you have any feedback.
Acknowlegments:
 Calders, T., & Verwer, S. (2010). Three naive Bayes approaches for discriminationfree classification. Data Mining and Knowledge Discovery, 21(2), 277292.
 Chouldechova, A. (2017). Fair prediction with disparate impact: A study of bias in recidivism prediction instruments. Big data, 5(2), 153163.
 Feldman, M., Friedler, S. A., Moeller, J., Scheidegger, C., & Venkatasubramanian, S. (2015, August). Certifying and removing disparate impact. In Proceedings of the 21th ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (pp. 259268). ACM.
 Friedler, S. A., Scheidegger, C., Venkatasubramanian, S., Choudhary, S., Hamilton, E. P., & Roth, D. (2018). A comparative study of fairnessenhancing interventions in machine learning. arXiv preprint arXiv:1802.04422.
 Zafar, M. B., Valera, I., Gomez Rodriguez, M., & Gummadi, K. P. (2017, April). Fairness beyond disparate treatment & disparate impact: Learning classification without disparate mistreatment. In Proceedings of the 26th International Conference on World Wide Web (pp. 11711180). International World Wide Web Conferences Steering Committee.