Bias mitigation with debiasing exposure, disparate impact remover and Fair top K#
This demo will show how to implement the post-processing methods “debiasing exposure”, “disparate impact remover” and “Fair top K” to enhance the fairness of a recommender system’s output.
Debiasing Exposure
Disparate Impact Remover
Fair Top K
First, install the holisticai package if you haven’t already:
!pip install holisticai[all]
Then, import the necessary libraries.
[2]:
# Base Imports
import pandas as pd
import numpy as np
from holisticai.datasets.synthetic.recruitment import generate_rankings
from holisticai.bias.mitigation.postprocessing.debiasing_exposure.algorithm_utils import exposure_metric
from holisticai.bias.mitigation.postprocessing import DebiasingExposure
np.random.seed(0)
import warnings
warnings.filterwarnings("ignore")
The dataset that we will use is a synthetic ranking dataset generated following the procedure described by Yang and Stoyanovich in their research. This algorithm creates a ranked output of protected and unprotected candidates with a certain probability.
[3]:
# Synthetic data
M = 1000
top_n = 20
p = 0.25
rankings = generate_rankings(M, top_n, p, return_p_attr=False)
baseline = exposure_metric(rankings, group_col='protected', query_col='X', score_col='score')
baseline
[3]:
| Value | |
|---|---|
| exposure_ratio | 37027.714945 |
| exposure difference | 0.047272 |
Bias mitigation#
Method: Debiasing exposure#
Apply the debiasing exposure mitigator algorithm.
[4]:
# create the DebiasingExposure class
dtr = DebiasingExposure(group_col="protected",
query_col = 'X',
doc_col = 'Y',
feature_cols = ['score', 'protected'],
score_col = 'score',
gamma=2,
number_of_iterations=100,
standardize=True,
verbose=1)
# train the model
dtr.fit(rankings)
[4]:
<holisticai.bias.mitigation.postprocessing.debiasing_exposure.transformer.DebiasingExposure at 0x7f559cea5c10>
[5]:
re_rankings = dtr.transform(rankings)
Observe the fairness metrics before and after applying the algorithm (lower is better).
[6]:
df_deb_exp = exposure_metric(re_rankings, group_col='protected', query_col='X', score_col='score')
df_deb_exp
[6]:
| Value | |
|---|---|
| exposure_ratio | 0.755373 |
| exposure difference | 0.002431 |
[7]:
result = pd.concat([baseline, df_deb_exp], axis=1).iloc[:, [0,1]]
result.columns = ['Baseline','Mitigator']
result
[7]:
| Baseline | Mitigator | |
|---|---|---|
| exposure_ratio | 37027.714945 | 0.755373 |
| exposure difference | 0.047272 | 0.002431 |
Method: Disparate impact remover#
[8]:
from holisticai.bias.mitigation import DisparateImpactRemoverRS
dir = DisparateImpactRemoverRS(query_col='X', group_col='protected', score_col='score', repair_level=1)
re_rankings = dir.transform(rankings)
df_dis_imp = exposure_metric(re_rankings, group_col='protected', query_col='X', score_col='score')
df_dis_imp
[8]:
| Value | |
|---|---|
| exposure_ratio | 1.003761 |
| exposure difference | 0.001925 |
[9]:
result = pd.concat([baseline, df_dis_imp], axis=1).iloc[:, [0,1]]
result.columns = ['Baseline','Mitigator']
result
[9]:
| Baseline | Mitigator | |
|---|---|---|
| exposure_ratio | 37027.714945 | 1.003761 |
| exposure difference | 0.047272 | 0.001925 |
Method: Fair Top-K#
Now, we will implement the Fair Top-K algorithm, this method works differently from the previous ones. Given a list of items, it will reorganize the list to ensure that the top-K items are fairer.
Let’s create a unfair list to apply the Fair Top-K algorithm.
[10]:
def create_unfair_example(ranking, n):
"""
Setting an unfair ranking where protected group examples are only the last n results.
"""
ranking = ranking.copy()
ranking['protected']=False
ranking['protected'].iloc[-n:]=True
return ranking
M = 1
k = 20
p = 0.1
ranking = generate_rankings(M, k, p)
unfair_ranking = create_unfair_example(ranking, 6)
[11]:
from holisticai.bias.mitigation.postprocessing.fair_topk.transformer import FairTopK
[12]:
# Bias Mitigation Post-processing
top_n = 20
p = 0.9
alpha = 0.15
fs = FairTopK(top_n=top_n,
p=p,
alpha=alpha,
query_col='X',
doc_col='Y',
score_col='score',
group_col='protected')
re_ranking = fs.transform(unfair_ranking)
Let’s observe how the original and unfair ranking was modified:
[13]:
def compare_results(old , new):
old = old.copy()
new = new.copy()
old.columns = pd.MultiIndex.from_tuples([['Old Rank',col] for col in old.columns])
new.columns = pd.MultiIndex.from_tuples([['New Rank',col] for col in new.columns])
return pd.concat([old.reset_index(drop=True),new.reset_index(drop=True)], axis=1)
compare_results(unfair_ranking , re_ranking)
[13]:
| Old Rank | New Rank | |||||||
|---|---|---|---|---|---|---|---|---|
| X | Y | score | protected | X | Y | score | protected | |
| 0 | 0 | 20 | 20 | False | 0 | 20 | 20 | False |
| 1 | 0 | 19 | 19 | False | 0 | 6 | 6 | True |
| 2 | 0 | 18 | 18 | False | 0 | 5 | 5 | True |
| 3 | 0 | 17 | 17 | False | 0 | 4 | 4 | True |
| 4 | 0 | 16 | 16 | False | 0 | 3 | 3 | True |
| 5 | 0 | 15 | 15 | False | 0 | 19 | 19 | False |
| 6 | 0 | 14 | 14 | False | 0 | 2 | 2 | True |
| 7 | 0 | 13 | 13 | False | 0 | 1 | 1 | True |
| 8 | 0 | 12 | 12 | False | 0 | 18 | 18 | False |
| 9 | 0 | 11 | 11 | False | 0 | 17 | 17 | False |
| 10 | 0 | 10 | 10 | False | 0 | 16 | 16 | False |
| 11 | 0 | 9 | 9 | False | 0 | 15 | 15 | False |
| 12 | 0 | 8 | 8 | False | 0 | 14 | 14 | False |
| 13 | 0 | 7 | 7 | False | 0 | 13 | 13 | False |
| 14 | 0 | 6 | 6 | True | 0 | 12 | 12 | False |
| 15 | 0 | 5 | 5 | True | 0 | 11 | 11 | False |
| 16 | 0 | 4 | 4 | True | 0 | 10 | 10 | False |
| 17 | 0 | 3 | 3 | True | 0 | 9 | 9 | False |
| 18 | 0 | 2 | 2 | True | 0 | 8 | 8 | False |
| 19 | 0 | 1 | 1 | True | 0 | 7 | 7 | False |