<jupyter_start><jupyter_text>Certifying and Removing Disparate ImpactThis notebook apples the algorithm described in [Certifying and removing disparate impact](https://dl.acm.org/doi/10.1145/2783258.2783311) by <NAME> al., as implemented by the [AI Fairness 360 library](https://aif360.readthedocs.io/) from IBM.This is a pre-processing algorithm that works by adjusting the distributions of the features conditional on the protected attribute to be equal, so that a subsequently trained model can't discriminate.<jupyter_code>from pathlib import Path

import joblib
import numpy as np
import pandas as pd
from aif360.datasets import StandardDataset
from aif360.algorithms.preprocessing import DisparateImpactRemover
from fairlearn.metrics import (
    demographic_parity_difference,
    demographic_parity_ratio,
)
from helpers.plot import group_box_plots
from sklearn.neural_network import MLPClassifier  # noqa
from helpers import export_plot<jupyter_output><empty_output><jupyter_text>Load dataWe have committed preprocessed data to the repository for reproducibility and we load it here. Check out the preprocessing notebook for details on how this data was obtained.<jupyter_code>artifacts_dir = Path("../../../artifacts")
# override data_dir in source notebook
# this is stripped out for the hosted notebooks
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")<jupyter_output><empty_output><jupyter_text>`aif360` uses the following custom dataset objects. For this test we use race as the sensitive attribute.<jupyter_code>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")<jupyter_output><empty_output><jupyter_text>Train unfair modelFor maximum reproducibility we load the baseline model from disk, but the code used to train can be found in the baseline model notebook.<jupyter_code>bl_model = joblib.load(
    artifacts_dir / "models" / "recruiting" / "baseline.pkl"
)

bl_test_probs = bl_model.predict_proba(test_sds.features)[:, 1]
bl_test_pred = bl_test_probs > 0.5<jupyter_output><empty_output><jupyter_text>Perform interventionWe repair the dataset using the `DisparateImpactRemover`.<jupyter_code>di = DisparateImpactRemover(repair_level=1.0)

train_repd = di.fit_transform(train_sds)
train_repd_X = np.delete(train_repd.features, index, axis=1)
train_repd_y = train_repd.labels.flatten()

test_repd = di.fit_transform(test_sds)
test_repd_X = np.delete(test_repd.features, index, axis=1)
test_repd_y = test_repd.labels.flatten()<jupyter_output><empty_output><jupyter_text>Train model on fair dataWe use the same architecture, but the repaired data. Once again we load a trained model for reproducibility, but the code used to train the model can be found below.<jupyter_code>model = joblib.load(artifacts_dir / "models" / "recruiting" / "feldman.pkl")
# model = MLPClassifier(hidden_layer_sizes=(100, 100), early_stopping=True)
# model.fit(train_repd_X, train_repd_y)

test_probs = model.predict_proba(test_repd_X)[:, 1]
test_pred = test_probs > 0.5<jupyter_output><empty_output><jupyter_text>Analyse unfairness and accuracyWe measure the accuracy and fairness in baseline and compare it to the corrected model.<jupyter_code>bl_acc = bl_model.score(test.drop(columns="employed_yes"), test.employed_yes)
bl_dpd = demographic_parity_difference(
    test.employed_yes, bl_test_pred, sensitive_features=test.race_white,
)
bl_dpr = demographic_parity_ratio(
    test.employed_yes, bl_test_pred, sensitive_features=test.race_white,
)

acc = model.score(test_repd_X, test.employed_yes)
dpd = demographic_parity_difference(
    test.employed_yes, test_pred, sensitive_features=test.race_white,
)
dpr = demographic_parity_ratio(
    test.employed_yes, test_pred, sensitive_features=test.race_white,
)

print(f"Baseline accuracy: {bl_acc:.3f}")
print(f"Accuracy: {acc:.3f}\n")

print(f"Baseline demographic parity difference: {bl_dpd:.3f}")
print(f"Demographic parity difference: {dpd:.3f}\n")

print(f"Baseline demographic parity ratio: {bl_dpr:.3f}")
print(f"Demographic parity ratio: {dpr:.3f}")<jupyter_output><empty_output><jupyter_text>We can visualise the disparity between white and black applicants with a box plot of the scores.<jupyter_code>dp_box = group_box_plots(
    np.concatenate([bl_test_probs, test_probs]),
    np.tile(test.race_white.map(lambda x: "White" if x else "Black"), 2),
    groups=np.concatenate(
        [np.zeros_like(bl_test_probs), np.ones_like(test_probs)]
    ),
    group_names=["Baseline", "Feldman"],
    title="Distribution of scores by race",
    xlabel="Score",
    ylabel="Model",
)
dp_box
export_plot(dp_box, "feldman-dp.json")<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10479041916167664, 'fraction_numerical': 0.011976047904191617, 'mean_word_length': 6.203504043126685, 'pattern_counts': {'":': 0, '<': 34, '<?xml version=': 0, '>': 36, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '308462', 'license': 'MIT', 'n_tokens_mistral': 1859, 'n_tokens_neox': 1761, 'n_words': 411, 'path': 'src/notebooks/recruiting/interventions/feldman.ipynb', 'repo_name': 'CDEIUK/ml-bias', 'size': '5348'}