This notebook contains the implementation of the post-processing algorithm introduced in On fairness and calibration by Pleiss et al. (2017) as part of the IBM AIF360 fairness tool box github.com/IBM/AIF360.
The migitation method achieves a relaxed version of Equalised Odds while maintaining Calibration by withholding information. In particular a proportion of the advantaged group is predicted according to the base rate without considering the model inputs. This preserves Calibration but allows us to bring the error rates for the two classes closer together.
This method is attractive in that it achieves one notion of fairness and approximately achieves another. However, like the intervention of Hardt et al. it introduces randomness into decision making that might not be compatible with individual notions of fairness. Furthermore the method requires as input calibrated classifiers, it does not offer a way to achieve Calibration, only to preserve it.
from pathlib import Path
import joblib
import numpy as np
import pandas as pd
from aif360.datasets import StandardDataset
from aif360.algorithms.postprocessing.calibrated_eq_odds_postprocessing import (
CalibratedEqOddsPostprocessing,
)
from fairlearn.metrics import equalized_odds_difference
from helpers.metrics import accuracy, calibration_difference
from helpers.plot import calibration_curves, group_bar_plots
We have committed preprocessed data to the repository for reproducibility and we load it here. Check out hte preprocessing notebook for details on how this data was obtained.
artifacts_dir = Path("../../../artifacts")
data_dir = artifacts_dir / "data" / "recruiting"
train = pd.read_csv(data_dir / "processed" / "train.csv")
val = pd.read_csv(data_dir / "processed" / "val.csv")
test = pd.read_csv(data_dir / "processed" / "test.csv")
In order to process data for our fairness intervention we need to define special dataset objects which are part of every intervention pipeline within the IBM AIF360 toolbox. These objects contain the original data as well as some useful further information, e.g., which feature is the protected attribute as well as which column corresponds to the label.
train_sds = StandardDataset(
train,
label_name="employed_yes",
favorable_classes=[1],
protected_attribute_names=["race_white"],
privileged_classes=[[1]],
)
test_sds = StandardDataset(
test,
label_name="employed_yes",
favorable_classes=[1],
protected_attribute_names=["race_white"],
privileged_classes=[[1]],
)
val_sds = StandardDataset(
val,
label_name="employed_yes",
favorable_classes=[1],
protected_attribute_names=["race_white"],
privileged_classes=[[1]],
)
index = train_sds.feature_names.index("race_white")
Define which binary value goes with the (un-)privileged group
privileged_groups = [{"race_white": 1.0}]
unprivileged_groups = [{"race_white": 0.0}]
For maximum reproducibility we load the baseline model from disk, but the code used to train can be found in the baseline model notebook.
bl_model = joblib.load(
artifacts_dir / "models" / "recruiting" / "baseline.pkl"
)
Get predictions for the validation and test data
bl_test_probs = bl_model.predict_proba(test.drop("employed_yes", axis=1))[:, 1]
test_sds_pred = test_sds.copy(deepcopy=True)
test_sds_pred.scores = bl_test_probs.reshape(-1, 1)
bl_test_pred = bl_test_probs > 0.5
bl_val_probs = bl_model.predict_proba(val.drop("employed_yes", axis=1))[:, 1]
val_sds_pred = val_sds.copy(deepcopy=True)
val_sds_pred.scores = bl_val_probs.reshape(-1, 1)
bl_val_pred = bl_val_probs > 0.5
We first address equal opportunity which is achieved by setting the cost_contraint parameter method accordingly when setting up the intervention. We then learn the intervention procedure based on the true and predicted labels of the validation data. Subsequently, we apply the learnt intervention to the predictions of the test data and analyse the outcomes for fairness and accuracy.
cost_constraint = "fnr"
# Learn parameters to equal opportunity and apply to create a new dataset
cpp = CalibratedEqOddsPostprocessing(
privileged_groups=privileged_groups,
unprivileged_groups=unprivileged_groups,
cost_constraint=cost_constraint,
seed=np.random.seed(),
)
cpp = cpp.fit(val_sds, val_sds_pred)
Apply intervention to testing data.
test_sds_pred_tranf = cpp.predict(test_sds_pred)
test_probs = test_sds_pred_tranf.scores.flatten()
test_pred = test_probs > 0.5
Analyse accuracy and fairness
mask = test.employed_yes == 1
bl_acc = accuracy(test.employed_yes, bl_test_pred)
acc = accuracy(test.employed_yes, test_pred)
bl_eo = equalized_odds_difference(
test.employed_yes[mask],
bl_test_pred[mask],
sensitive_features=test.race_white[mask],
)
eo = equalized_odds_difference(
test.employed_yes[mask],
test_pred[mask],
sensitive_features=test.race_white[mask],
)
bl_calib = calibration_difference(
test.employed_yes, bl_test_probs, test.race_white
)
calib = calibration_difference(test.employed_yes, test_probs, test.race_white)
print(f"Baseline accuracy: {bl_acc:.3f}")
print(f"Accuracy: {acc:.3f}")
print(f"Baseline equal opportunity difference: {bl_eo:.3f}")
print(f"Equal opportunity difference: {eo:.3f}")
print(f"Baseline calibration: {bl_calib:.3f}")
print(f"Calibration: {calib:.3f}")
calibration_curves(
test.employed_yes,
bl_test_probs,
test.race_white.map({0: "Black", 1: "White"}),
title="Calibration by race",
xlabel="Score",
ylabel="Proportion positive outcome",
)
Here the intervention preserves calibration.
calibration_curves(
test.employed_yes,
test_probs,
test.race_white.map({0: "Black", 1: "White"}),
title="Calibration by race",
xlabel="Score",
ylabel="Proportion positive outcome",
)
mask = test.employed_yes == 1
eopp_bar = group_bar_plots(
np.concatenate([bl_test_pred[mask], test_pred[mask]]),
np.tile(test.race_white[mask].map({0: "Black", 1: "White"}), 2),
groups=np.concatenate(
[np.zeros_like(bl_test_probs[mask]), np.ones_like(test_probs[mask])]
),
group_names=["Baseline", "Pleiss"],
title="Mean prediction for high earners by race",
xlabel="Proportion predicted employed",
ylabel="Method",
)
eopp_bar
We'll now repeat the process for equalised odds, which requires us changing the underlying cost constraint parameter accordingly, so that the resulting intervention minimises a weighted average between false negative and false positive rate. There are no further parameter choices to be made.
cost_constraint = "weighted"
Learn intervention on validation data.
# Learn parameters to equalize odds and apply to create a new dataset
cpp = CalibratedEqOddsPostprocessing(
privileged_groups=privileged_groups,
unprivileged_groups=unprivileged_groups,
cost_constraint=cost_constraint,
seed=np.random.seed(),
)
cpp = cpp.fit(test_sds, test_sds_pred)
Apply intervention on testing data.
test_sds_pred_tranf = cpp.predict(test_sds_pred)
test_probs = test_sds_pred_tranf.scores.flatten()
test_pred = test_probs > 0.5
Analyse fairness and accuracy
bl_acc = accuracy(test.employed_yes, bl_test_pred)
acc = accuracy(test.employed_yes, test_pred)
bl_eo = equalized_odds_difference(
test.employed_yes, bl_test_pred, sensitive_features=test.race_white,
)
eo = equalized_odds_difference(
test.employed_yes, test_pred, sensitive_features=test.race_white,
)
bl_calib = calibration_difference(
test.employed_yes, bl_test_probs, test.race_white
)
calib = calibration_difference(test.employed_yes, test_probs, test.race_white)
print(f"Baseline accuracy: {bl_acc:.3f}")
print(f"Accuracy: {acc:.3f}")
print(f"Baseline equalised odds difference: {bl_eo:.3f}")
print(f"Equalised odds difference: {eo:.3f}")
print(f"Baseline calibration: {bl_calib:.3f}")
print(f"Calibration: {calib:.3f}")
calibration_curves(
test.employed_yes,
test_probs,
test.race_white.map({0: "Black", 1: "White"}),
title="Calibration by race",
xlabel="Score",
ylabel="Proportion positive outcome",
)
bl_eo_bar = group_bar_plots(
bl_test_pred,
test.race_white.map({0: "Black", 1: "White"}),
groups=test.employed_yes,
group_names=["Not employed", "Employed"],
title="Baseline mean prediction by race",
xlabel="Proportion predicted employed",
ylabel="Outcome",
)
bl_eo_bar
eo_bar = group_bar_plots(
test_pred,
test.race_white.map({0: "Black", 1: "White"}),
groups=test.employed_yes,
group_names=["Not employed", "Employed"],
title="Corrected mean predictions by race",
xlabel="Proportion predicted employed",
ylabel="Outcome",
)
eo_bar