FairUP / src /models /RHGN /fairness.py
erasmopurif's picture
First commit
d2a8669
import numpy as np
import pandas as pd
class Fairness(object):
"""
Compute fairness metrics
"""
def __init__(self, G, test_nodes_idx, targets, predictions, sens_attr, neptune_run,
multiclass_pred=False, multiclass_sens=False):
self.multiclass_pred = multiclass_pred
self.multiclass_sens = multiclass_sens
self.sens_attr = sens_attr
self.neptune_run = neptune_run
self.neptune_run["sens_attr"] = self.sens_attr
self.G = G
self.test_nodes_idx = test_nodes_idx.cpu().detach().numpy()
self.true_y = np.asarray(targets) # target variables
self.pred_y = np.asarray(predictions) # prediction of the classifier
self.sens_attr_array = self.G.nodes["user"].data[self.sens_attr].cpu().detach().numpy() # sensitive attribute values
self.sens_attr_values = self.sens_attr_array[self.test_nodes_idx]
if self.multiclass_pred and self.multiclass_sens: # Classifier: multiclass - Sens.attr: multiclass
self.class_range = list(set(self.true_y))
self.y_hat = []
self.yneq_hat = []
for y_hat_idx in self.class_range:
self.y_hat.append(self.pred_y == y_hat_idx)
self.yneq_hat.append(self.pred_y != y_hat_idx)
self.sens_attr_range = list(set(self.sens_attr_values))
self.s = []
for s_idx in self.sens_attr_range:
self.s.append(self.sens_attr_values == s_idx)
self.y_s = []
self.yneq_s = []
for y_idx in self.class_range:
self.y_s.append([])
self.yneq_s.append([])
for s_idx in self.sens_attr_range:
self.y_s[y_idx].append(np.bitwise_and(self.true_y == y_idx, self.s[s_idx]))
self.yneq_s[y_idx].append(np.bitwise_and(self.true_y != y_idx, self.s[s_idx]))
self.y_s = np.array(self.y_s)
self.yneq_s = np.array(self.yneq_s)
elif self.multiclass_sens: # Classifier: binary - Sens.attr: multiclass
self.sens_attr_range = list(set(self.sens_attr_values))
self.s = []
self.y1_s = []
self.y0_s = []
for s_idx in self.sens_attr_range:
self.s.append(self.sens_attr_values == s_idx)
self.y1_s.append(np.bitwise_and(self.true_y == 1, self.s[s_idx]))
self.y0_s.append(np.bitwise_and(self.true_y == 0, self.s[s_idx]))
else: # Classifier: binary - Sens.attr: binary
self.s0 = self.sens_attr_values == 0
self.s1 = self.sens_attr_values == 1
self.y1_s0 = np.bitwise_and(self.true_y == 1, self.s0)
self.y1_s1 = np.bitwise_and(self.true_y == 1, self.s1)
self.y0_s0 = np.bitwise_and(self.true_y == 0, self.s0)
self.y0_s1 = np.bitwise_and(self.true_y == 0, self.s1)
def statistical_parity(self):
if self.multiclass_pred and self.multiclass_sens: # Classifier: multiclass - Sens.attr: multiclass
"""
P(y^=0|s=0) = P(y^=0|s=1) = ... = P(y^=0|s=N)
[...]
P(y^=M|s=0) = P(y^=M|s=1) = ... = P(y^=M|s=N)
"""
stat_parity = []
for y_hat_idx in self.class_range:
stat_parity.append([])
for s_idx in self.sens_attr_range:
stat_parity[y_hat_idx].append(
sum(np.bitwise_and(self.y_hat[y_hat_idx], self.s[s_idx])) /
sum(self.s[s_idx])
)
self.neptune_run["fairness/SP_y^" + str(y_hat_idx) + "_s" + str(s_idx)] = stat_parity[y_hat_idx][s_idx]
elif self.multiclass_sens: # Classifier: binary - Sens.attr: multiclass
''' P(y^=1|s=0) = P(y^=1|s=1) = ... = P(y^=1|s=N) '''
stat_parity_s = []
for s_idx in self.sens_attr_range:
stat_parity_s.append(sum(self.pred_y[self.s[s_idx]]) / sum(self.s[s_idx]))
self.neptune_run["fairness/SP_s" + str(s_idx)] = stat_parity_s[s_idx]
else: # Classifier: binary - Sens.attr: binary
''' P(y^=1|s=0) = P(y^=1|s=1) '''
# stat_parity = abs(sum(self.pred_y[self.s0]) / sum(self.s0) - sum(self.pred_y[self.s1]) / sum(self.s1))
stat_parity_s0 = sum(self.pred_y[self.s0]) / sum(self.s0)
stat_parity_s1 = sum(self.pred_y[self.s1]) / sum(self.s1)
stat_parity_diff = stat_parity_s0 - stat_parity_s1
self.neptune_run["fairness/SP_s0"] = stat_parity_s0
self.neptune_run["fairness/SP_s1"] = stat_parity_s1
print("Statistical Parity Difference (SPD): {:.4f}".format(np.abs(stat_parity_diff)))
self.neptune_run["fairness/SPD"] = stat_parity_diff
def equal_opportunity(self):
if self.multiclass_pred and self.multiclass_sens: # Classifier: multiclass - Sens.attr: multiclass
"""
P(y^=0|y=0,s=0) = P(y^=0|y=0,s=1) = ... = P(y^=0|y=0,s=N)
[...]
P(y^=M|y=M,s=0) = P(y^=M|y=M,s=1) = ... = P(y^=M|y=M,s=N)
"""
equal_opp = []
for y_hat_idx in self.class_range:
equal_opp.append([])
for s_idx in self.sens_attr_range:
try:
equal_opp[y_hat_idx].append(
sum(np.bitwise_and(self.y_hat[y_hat_idx], self.y_s[y_hat_idx][s_idx])) /
sum(self.y_s[y_hat_idx][s_idx])
)
except ZeroDivisionError:
equal_opp[y_hat_idx].append(0)
self.neptune_run["fairness/EO_y" + str(y_hat_idx) + "_s" + str(s_idx)] = equal_opp[y_hat_idx][s_idx]
elif self.multiclass_sens: # Classifier: binary - Sens.attr: multiclass
''' P(y^=1|y=1,s=0) = P(y^=1|y=1,s=1) = ... = P(y^=1|y=1,s=N) '''
equal_opp_s = []
for s_idx in self.sens_attr_range:
equal_opp_s.append(sum(self.pred_y[self.y1_s[s_idx]]) / sum(self.y1_s[s_idx]))
self.neptune_run["fairness/EO_s" + str(s_idx)] = equal_opp_s[s_idx]
else: # Classifier: binary - Sens.attr: binary
''' P(y^=1|y=1,s=0) = P(y^=1|y=1,s=1) '''
# equal_opp = abs(sum(self.pred_y[self.y1_s0]) / sum(self.y1_s0) - sum(self.pred_y[self.y1_s1]) / sum(self.y1_s1))
equal_opp_s0 = sum(self.pred_y[self.y1_s0]) / sum(self.y1_s0)
equal_opp_s1 = sum(self.pred_y[self.y1_s1]) / sum(self.y1_s1)
equal_opp_diff = equal_opp_s0 - equal_opp_s1
self.neptune_run["fairness/EO_s0"] = equal_opp_s0
self.neptune_run["fairness/EO_s1"] = equal_opp_s1
print("Equal Opportunity Difference (EOD): {:.4f}".format(np.abs(equal_opp_diff)))
self.neptune_run["fairness/EOD"] = equal_opp_diff
def overall_accuracy_equality(self):
if self.multiclass_pred and self.multiclass_sens: # Classifier: multiclass - Sens.attr: multiclass
''' P(y^=0|y=0,s=0) + ... + P(y^=M|y=M,s=0) = ... = P(y^=0|y=0,s=N) + ... + P(y^=M|y=M,s=N) '''
oae_s = []
for s_idx in self.sens_attr_range:
oae_temp = 0.0
for y_hat_idx in self.class_range:
try:
oae_temp += (
sum(np.bitwise_and(self.y_hat[y_hat_idx], self.y_s[y_hat_idx][s_idx])) /
sum(self.y_s[y_hat_idx][s_idx])
)
except ZeroDivisionError:
oae_temp += 0.0
oae_s.append(oae_temp)
self.neptune_run["fairness/OAE_s" + str(s_idx)] = oae_s[s_idx]
elif self.multiclass_sens: # Classifier: binary - Sens.attr: multiclass
''' P(y^=0|y=0,s=0) + P(y^=1|y=1,s=0) = ... = P(y^=0|y=0,s=N) + P(y^=1|y=1,s=N)'''
oae_s = []
for s_idx in self.sens_attr_range:
oae_s.append(
np.count_nonzero(self.pred_y[self.y0_s[s_idx]]==0) / sum(self.y0_s[s_idx]) +
sum(self.pred_y[self.y1_s[s_idx]]) / sum(self.y1_s[s_idx])
)
self.neptune_run["fairness/OAE_s" + str(s_idx)] = oae_s[s_idx]
else: # Classifier: binary - Sens.attr: binary
''' P(y^=0|y=0,s=0) + P(y^=1|y=1,s=0) = P(y^=0|y=0,s=1) + P(y^=1|y=1,s=1) '''
oae_s0 = np.count_nonzero(self.pred_y[self.y0_s0]==0) / sum(self.y0_s0) + sum(self.pred_y[self.y1_s0]) / sum(self.y1_s0)
oae_s1 = np.count_nonzero(self.pred_y[self.y0_s1]==0) / sum(self.y0_s1) + sum(self.pred_y[self.y1_s1]) / sum(self.y1_s1)
# oae_diff = abs(oae_s0 - oae_s1)
oae_diff = oae_s0 - oae_s1
self.neptune_run["fairness/OAE_s0"] = oae_s0
self.neptune_run["fairness/OAE_s1"] = oae_s1
print("Overall Accuracy Equality Difference (OAED): {:.4f}".format(np.abs(oae_diff)))
self.neptune_run["fairness/OAED"] = oae_diff
def treatment_equality(self):
if self.multiclass_pred and self.multiclass_sens: # Classifier: multiclass - Sens.attr: multiclass
"""
P(y^=0|y/=0,s=0) / P(y^/=0|y=0,s=0) = ... = P(y^=0|y/=0,s=N) / P(y^/=0|y=0,s=N)
[...]
P(y^=M|y/=M,s=0) / P(y^/=M|y=M,s=0) = ... = P(y^=M|y/=M,s=N) / P(y^/M|y=M,s=N)
"""
te_fp_fn = []
te_fn_fp = []
te = []
for y_hat_idx in self.class_range:
te_fp_fn.append([])
te_fn_fp.append([])
abs_te_fp_fn = 0.0
abs_te_fn_fp = 0.0
te.append([])
for s_idx in self.sens_attr_range:
try:
te_fp_fn[y_hat_idx].append(
(sum(np.bitwise_and(self.y_hat[y_hat_idx], self.yneq_s[y_hat_idx][s_idx])) / sum(self.yneq_s[y_hat_idx][s_idx])) /
(sum(np.bitwise_and(self.yneq_hat[y_hat_idx], self.y_s[y_hat_idx][s_idx])) / sum(self.y_s[y_hat_idx][s_idx]))
)
except ZeroDivisionError:
te_fp_fn[y_hat_idx].append(0)
try:
te_fn_fp[y_hat_idx].append(
(sum(np.bitwise_and(self.yneq_hat[y_hat_idx], self.y_s[y_hat_idx][s_idx])) / sum(self.y_s[y_hat_idx][s_idx])) /
(sum(np.bitwise_and(self.y_hat[y_hat_idx], self.yneq_s[y_hat_idx][s_idx])) / sum(self.yneq_s[y_hat_idx][s_idx]))
)
except ZeroDivisionError:
te_fn_fp[y_hat_idx].append(0)
abs_te_fp_fn += abs(te_fp_fn[y_hat_idx][s_idx])
abs_te_fn_fp += abs(te_fn_fp[y_hat_idx][s_idx])
if abs_te_fp_fn < abs_te_fn_fp:
te[y_hat_idx].append(te_fp_fn[y_hat_idx][s_idx])
else:
te[y_hat_idx].append(te_fn_fp[y_hat_idx][s_idx])
for y_idx in self.class_range:
for s_idx in self.sens_attr_range:
self.neptune_run["fairness/TE_y" + str(y_idx) + "_s" + str(s_idx)] = te[y_idx][s_idx]
elif self.multiclass_sens: # Classifier: binary - Sens.attr: multiclass
''' P(y^=1|y=0,s=0) / P(y^=0|y=1,s=0) = ... = P(y^=1|y=0,s=N) / P(y^=0|y=1,s=N) '''
te1_s = []
te0_s = []
abs_te1 = []
abs_te0 = []
for s_idx in self.sens_attr_range:
te1_s.append(
(sum(self.pred_y[self.y0_s[s_idx]]) / sum(self.y0_s[s_idx])) /
(np.count_nonzero(self.pred_y[self.y1_s[s_idx]]==0) / sum(self.y1_s[s_idx]))
)
te0_s.append(
(np.count_nonzero(self.pred_y[self.y1_s[s_idx]]==0) / sum(self.y1_s[s_idx])) /
(sum(self.pred_y[self.y0_s[s_idx]]) / sum(self.y0_s[s_idx]))
)
abs_te1.append(abs(te1_s[s_idx]))
abs_te0.append(abs(te0_s[s_idx]))
if sum(abs_te1) < sum(abs_te0):
te_s = te1_s
else:
te_s = te0_s
#for i in self.sens_attr_range:
self.neptune_run["fairness/TE_s" + str(i)] = te_s[i]
else: # Classifier: binary - Sens.attr: binary
''' P(y^=1|y=0,s=0) / P(y^=0|y=1,s=0) = P(y^=1|y=0,s=1) / P(y^=0|y=1,s=1) '''
te1_s0 = (sum(self.pred_y[self.y0_s0]) / sum(self.y0_s0)) / (np.count_nonzero(self.pred_y[self.y1_s0]==0) / sum(self.y1_s0))
te1_s1 = (sum(self.pred_y[self.y0_s1]) / sum(self.y0_s1)) / (np.count_nonzero(self.pred_y[self.y1_s1]==0) / sum(self.y1_s1))
te_diff_1 = te1_s0 - te1_s1
abs_ted_1 = abs(te_diff_1)
''' P(y^=0|y=1,s=0) / P(y^=1|y=0,s=0) = P(y^=0|y=1,s=1) / P(y^=1|y=0,s=1) '''
te0_s0 = (np.count_nonzero(self.pred_y[self.y1_s0]==0) / sum(self.y1_s0)) / (sum(self.pred_y[self.y0_s0]) / sum(self.y0_s0))
te0_s1 = (np.count_nonzero(self.pred_y[self.y1_s1]==0) / sum(self.y1_s1)) / (sum(self.pred_y[self.y0_s1]) / sum(self.y0_s1))
te_diff_0 = te0_s0 - te0_s1
abs_ted_0 = abs(te_diff_0)
# te_diff = min(te_diff_1, te_diff_0)
if abs_ted_0 < abs_ted_1:
te_s0 = te0_s0
te_s1 = te0_s1
te_diff = te_diff_0
else:
te_s0 = te1_s0
te_s1 = te1_s1
te_diff = te_diff_1
self.neptune_run["fairness/TE_s0"] = te_s0
self.neptune_run["fairness/TE_s1"] = te_s1
print("Treatment Equality Difference (TED): {:.4f}".format(np.abs(te_diff)))
self.neptune_run["fairness/TED"] = te_diff
def disparate_impact(self):
#num_of_priv = sum(self.s0)
#num_of_unpriv = sum(self.s1)
#unpriv_ratio = sum(self.pred_y[self.]/num_of_unpriv
#stat_parity_s0 = sum(self.pred_y[self.s0]) / sum(self.s0)
#stat_parity_s1 = sum(self.pred_y[self.s1]) / sum(self.s1)
#stat_parity_diff = stat_parity_s0 - stat_parity_s1
print('true_y:', self.true_y)
print('pred_y:', self.pred_y)