Part of the eXplainable Machine Learning course for Machine Learning (MSc) studies at the University of Warsaw. @pbiecek @hbaniecki
v0.1.0: 2022-11-28
https://github.com/mim-uw/eXplainableMachineLearning-2023/tree/main/Homeworks/HW6
import dalex as dx
import xgboost
import sklearn
from sklearn.model_selection import train_test_split
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings("ignore")
import platform
print(f'Python {platform.python_version()}')
{package.__name__: package.__version__ for package in [dx, xgboost, sklearn, pd, np]}
We use the same XGBoost classifier trained on the Titanic dataset as in the previous materials towards Homework 3, Homework 4, Homework 5.
df = dx.datasets.load_titanic()
df.loc[:, df.dtypes == 'object'] =\
df.select_dtypes(['object'])\
.apply(lambda x: x.astype('category'))
X = df.drop(columns='survived')
# convert gender to binary only because the `max_cat_to_onehot` parameter in XGBoost is yet to be working properly..
X = pd.get_dummies(X, columns=["gender"], drop_first=True)
y = df.survived
X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(X, y, test_size=0.33, random_state=42)
model = xgboost.XGBClassifier(
n_estimators=50,
max_depth=2,
use_label_encoder=False,
eval_metric="logloss",
enable_categorical=True,
tree_method="hist"
)
model.fit(X_train, y_train)
def pf_xgboost_classifier_categorical(model, df):
df.loc[:, df.dtypes == 'object'] =\
df.select_dtypes(['object'])\
.apply(lambda x: x.astype('category'))
return model.predict_proba(df)[:, 1]
explainer = dx.Explainer(model, X_test, y_test, predict_function=pf_xgboost_classifier_categorical)
explainer.model_performance()
See the API documentation for all the possible values of the specific parameters:
Explainer.model_fairness()
, which returns an object of class
GroupFairnessClassification
, which can be visualized using
GroupFairnessClassification.plot()
, and printed using
To compute group fairness metrics, we need to choose: protected variable and privileged group.
Note that the protected variable doesn't need to be contained in data. It is sometimes advised not to use sensitive attributes in modelling, but still check for model bias with respect to the privileged group.
protected_variable = X_test.gender_male.apply(lambda x: "male" if x else "female")
privileged_group = "male"
fobject = explainer.model_fairness(
protected=protected_variable,
privileged=privileged_group
)
Fairness objects have a convenient form of describing model bias using the fairness_check()
method.
Several metrics are computed and checked automatically:
For a broad description of these methods, consider refering to the following article and its references:
J. Wiśniewski & P. Biecek. fairmodels: a Flexible Tool for Bias Detection, Visualization, and Mitigation in Binary Classification Models. The R Journal, 2022.
More resources are available at https://fairmodels.drwhy.ai and specifically for Python at https://dalex.drwhy.ai/python#fairness.
fobject.fairness_check()
fobject.plot()
We clearly observe high bias towards the privileged group in the model. Let's construct a model without the protected variable.
X_train_without_prot, X_test_without_prot = X_train.drop("gender_male", axis=1), X_test.drop("gender_male", axis=1)
model_without_prot = xgboost.XGBClassifier(
n_estimators=50,
max_depth=2,
use_label_encoder=False,
eval_metric="logloss",
enable_categorical=True,
tree_method="hist"
)
model_without_prot.fit(X_train_without_prot, y_train)
explainer_without_prot = dx.Explainer(
model_without_prot,
X_test_without_prot,
y_test,
predict_function=pf_xgboost_classifier_categorical,
label="XGBClassifier without the protected attribute",
verbose=False
)
fobject_without_prot = explainer_without_prot.model_fairness(protected_variable, privileged_group)
Now compare the two models.
fobject.plot(fobject_without_prot, show=False).\
update_layout(autosize=False, width=800, height=450, legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99))
We managed to improve on 3 fairness metrics, at a cost of worse Predictive parity ratio.
This comes at a cost of model performance:
pd.concat([explainer.model_performance().result, explainer_without_prot.model_performance().result], axis=0)
Can we decrease model bias without decreasing model performance?
This is the goal of bias mitigation methods:
resample
- returns indices used to pick relevant samples of datareweight
- returns sample (case) weights for model trainingroc_pivot
- returns the Explainer
with a changed y_hat
predictionLet's compare all three.
from dalex.fairness import resample, reweight, roc_pivot
from copy import copy
protected_variable_train = X_train.gender_male.apply(lambda x: "male" if x else "female")
# resample
indices_resample = resample(
protected_variable_train,
y_train,
type='preferential', # uniform
probs=model_without_prot.predict_proba(X_train_without_prot)[:, 1], # requires probabilities
verbose=False
)
model_resample = copy(model_without_prot)
model_resample.fit(X_train_without_prot.iloc[indices_resample, :], y_train.iloc[indices_resample])
explainer_resample = dx.Explainer(
model_resample,
X_test_without_prot,
y_test,
label='XGBClassifier with Resample mitigation',
verbose=False
)
fobject_resample = explainer_resample.model_fairness(
protected_variable,
privileged_group
)
# reweight
sample_weight = reweight(
protected_variable_train,
y_train,
verbose=False
)
model_reweight = copy(model_without_prot)
model_reweight.fit(X_train_without_prot, y_train, sample_weight=sample_weight)
explainer_reweight = dx.Explainer(
model_reweight,
X_test_without_prot,
y_test,
label='XGBClassifier with Reweight mitigation',
verbose=False
)
fobject_reweight = explainer_reweight.model_fairness(
protected_variable,
privileged_group
)
# roc_pivot
explainer_roc_pivot = roc_pivot(
copy(explainer_without_prot),
protected_variable,
privileged_group,
verbose=False
)
explainer_roc_pivot.label = 'XGBClassifier with ROC pivot mitigation'
fobject_roc_pivot = explainer_roc_pivot.model_fairness(
protected_variable,
privileged_group
)
fobject_without_prot.plot([fobject_resample, fobject_reweight, fobject_roc_pivot], show=False).\
update_layout(autosize=False, width=800, height=450, legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99))
We can see the tradeoff between different fairness metrics.
Final conclusions will differ depending on the importance of a given metric and the epsilon
threshold.
for fobj in [fobject_without_prot, fobject_resample, fobject_reweight, fobject_roc_pivot]:
print("\n========== " + fobj.label + " ==========")
fobj.fairness_check(epsilon=0.66)
Finaly, let's check the bias-performance tradeoff.
pd.concat([
explainer_without_prot.model_performance().result,
explainer_resample.model_performance().result,
explainer_reweight.model_performance().result,
explainer_roc_pivot.model_performance().result
], axis=0)