Advanced tutorial on bias detection in dalex¶
In this tutorial, we cover the more advanced aspects of the fairness module in dalex
. For the introduction, see Fairness module in dalex example.
import pandas as pd
import numpy as np
import plotly
plotly.offline.init_notebook_mode()
Data¶
Firstly we load the data, which is based on the famous ProPublica study on the COMPAS recidivism algorithm.
compas = pd.read_csv("https://raw.githubusercontent.com/propublica/compas-analysis/master/compas-scores-two-years.csv")
To get a clearer picture, we will only use a few columns of the original data frame.
compas = compas[["sex", "age", "age_cat", "race", "juv_fel_count", "juv_misd_count",
"juv_other_count", "priors_count", "c_charge_degree", "is_recid",
"is_violent_recid", "two_year_recid"]]
compas.head()
As we can see, we have a relatively compact pandas.DataFrame
. The target variable is two_year_recid which denotes if a particular person will re-offend in the next two years. For this tutorial, we will use scikit-learn
models, but these methods are model-agnostic.
age_cat = compas.age_cat
compas = compas.drop("age_cat", axis =1)
compas.dtypes
Models¶
Like in the previous example, we will make 3 basic predictive models without the hyperparameter tuning.
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.model_selection import train_test_split
# classifiers
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
X_train, X_test, y_train, y_test = train_test_split(compas.drop("two_year_recid", axis =1),
compas.two_year_recid,
test_size=0.3,
random_state=123)
categorical_features = ['sex', 'race', 'c_charge_degree']
categorical_transformer = Pipeline(steps=[
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
numerical_features = ["age", "priors_count"]
numerical_transformer = Pipeline(steps=[
('scale', StandardScaler())
])
preprocessor = ColumnTransformer(transformers=[
('cat', categorical_transformer, categorical_features),
('num', numerical_transformer, numerical_features)
])
clf_tree = Pipeline(steps=[
('preprocessor', preprocessor),
('classifier', DecisionTreeClassifier(max_depth=7, random_state=123))
])
clf_forest = Pipeline(steps=[
('preprocessor', preprocessor),
('classifier', RandomForestClassifier(n_estimators=200, max_depth=7, random_state=123))
])
clf_logreg = Pipeline(steps=[
('preprocessor', preprocessor),
('classifier', LogisticRegression())
])
clf_logreg.fit(X_train, y_train)
clf_forest.fit(X_train, y_train)
clf_tree.fit(X_train, y_train)
Explainers¶
Now, we need to create Explainer
objects with the help of dalex.
import dalex as dx
dx.__version__
exp_logreg = dx.Explainer(clf_logreg, X_test, y_test)
exp_tree = dx.Explainer(clf_tree, X_test, y_test, verbose=False)
exp_forest = dx.Explainer(clf_forest, X_test, y_test, verbose=False)
model performance¶
pd.concat([exp.model_performance().result for exp in [exp_logreg, exp_tree, exp_forest]])
permutation-based variable importance¶
Now, we will use one of the dalex
methods to assess the Variable Importance of these models
exp_tree.model_parts().plot(objects=[exp_forest.model_parts(), exp_logreg.model_parts()])
In each of classifier, the most important feature is priors_count. The sex and race seem to be not relevant at all.
Could it still somehow make our classifiers biased?
Fairness¶
In this tutorial, we will investigate the sex variable. First, compute Fairness
objects using the model_fairness
method.
protected = X_test.sex
mf_tree = exp_tree.model_fairness(protected=protected,
privileged = "Female")
mf_forest = exp_forest.model_fairness(protected=protected,
privileged = "Female")
mf_logreg = exp_logreg.model_fairness(protected=protected,
privileged = "Female")
Now, we can easily check for bias.
mf_tree.fairness_check()
mf_forest.fairness_check()
mf_logreg.fairness_check()
All classifiers are biased! How could that be? We checked the variable importance!
Yes, but... variables could be correlated with each other. After all, using the default value of epsilon=0.8
concludes that the models are not fair.
We can visualize these results:
mf_tree.plot(objects=[mf_logreg, mf_forest])
It doesn't look good - the bias is enormous. But it is hard to pick the best model based on the plot above. To summarize it, we will use plots with parity loss.
mf_tree.plot(objects=[mf_logreg, mf_forest], type='stacked')
The DecisionTreeClassifier
seems to have the least parity loss.
In the ProPublica case, they focused among others on FPR
rates. We will do the same while looking at the accuracy
, which is a performance metric.
mf_tree.plot(objects=[mf_logreg, mf_forest],
type="performance_and_fairness",
fairness_metric="FPR",
performance_metric="accuracy")
Unfortunately, the bigger the accuracy
the more bias in FPR
metric.
Is there any method that would enable us to mitigate bias? Yes, the currently implemented one is called ceteris_paribus_cutoff
, and it is a visual tool that looks for the minimum in the sum of parity loss metrics. More mitigation methods are now presented in the introduction.
mf_tree.plot(objects=[mf_logreg, mf_forest], type="ceteris_paribus_cutoff", subgroup="Male")
Let's do a controversial thing. Let's change the cutoff for Male to minimize the parity loss of metrics. Why is it controversial?
Because it might not be fair to judge similar people differently based on the subgroup. We however, for educational reasons, will take the DecisionTreeClassifier
model and change its cutoff for Male.
mf_tree_changed = exp_tree.model_fairness(protected=X_test.sex,
privileged ="Female",
cutoff={'Male': 0.62},
label="tree_changed")
mf_tree_changed.plot([mf_tree])
As we can see, we calibrated cutoffs, so they have now the lowest value possible. As mentioned before, this may be controversial.
Besides, the user must be aware that ceteris_paribus_cutoff
fits the test set, which is why the difference is this big. Nevertheless, when possible, using this method might be helpful.
mf_tree_changed.plot([mf_tree], type='performance_and_fairness', fairness_metric='FPR')
We should note however that such cutoff calibration will probably result in a worse performance.
Summary¶
The usage of fairness module in dalex
is easy and intuitive. The module provides the user with ways to detect, visualize, and mitigate the bias.
Plots¶
This package uses plotly to render the plots:
- Install extentions to use
plotly
in JupyterLab: Getting Started Troubleshooting - Use
show=False
parameter inplot
method to returnplotly Figure
object - It is possible to edit the figures and save them
Resources - https://dalex.drwhy.ai/python¶
Introduction to the
dalex
package: Titanic: tutorial and examplesKey features explained: FIFA20: explain default vs tuned model with dalex
How to use dalex with: xgboost, tensorflow, h2o (feat. autokeras, catboost, lightgbm)
More explanations: residuals, shap, lime
Introduction to the Fairness module in dalex
Introduction to the Aspect module in dalex
Introduction to Arena: interactive dashboard for model exploration
Code in the form of jupyter notebook
Changelog: NEWS
Theoretical introduction to the plots: Explanatory Model Analysis: Explore, Explain, and Examine Predictive Models