This repository has been archived by the owner on Feb 5, 2024. It is now read-only.
forked from stanfordaiethics/hw1_public
-
Notifications
You must be signed in to change notification settings - Fork 0
/
hw1.py
225 lines (151 loc) · 7.09 KB
/
hw1.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# %%
"""
CS 281. Homework 1. Fairness in the Wild
Correspondence to: khkim@cs.stanford.edu
"""
import json
from typing import Union
from matplotlib import pyplot as plt
import numpy as np
import pandas as pd
import sklearn
from sklearn.isotonic import IsotonicRegression
from sklearn.linear_model import LogisticRegression
from sklearn.calibration import CalibratedClassifierCV
## Load the COMPAS Dataset
raw_data = pd.read_csv('./compas-scores-two-years.csv')
print('Num rows: %d' % len(raw_data))
# %%
######## Q1. Data pre-processing ########
## TODO: filter out corrupted rows from raw_data and store in a variable name "df"
## df should be a pandas.DataFrame object
def pre_process_compas_data(raw_data):
# only keep rows with charge date within 30days of arrest
df = raw_data[raw_data.days_b_screening_arrest.notnull()]
df = df[np.abs(df.days_b_screening_arrest)<=30]
# discard rows with recidivist flag == -1
df = df[df.is_recid != -1]
# remove rows with c_charge_degree == 0
df = df[df.c_charge_degree != 0]
# remove rows with no proper score_text
df = df[df.score_text != 'N/A']
print('Q1: Num data points after filtering: %d' % len(df))
return df
# %%
df = pre_process_compas_data(raw_data)
# %%
######## Q2. Data Visualization ########
## TODO: Visualize decile_scores as a histogram for different races, genders.
def visualize_decile_score_histograms(df):
print('Q2: Data Visualization')
axarr_sex = df.hist(column='decile_score', by='sex', rwidth=0.9, sharey=False,
align='mid', figsize=(10,5))
for ax in axarr_sex.flatten():
ax.set_xlabel("decile_score")
ax.set_ylabel("count")
ax.set_xticks(np.arange(1,11))
axarr_race = df[df.race.isin(['Caucasian','Hispanic','African-American'])].hist(column='decile_score', by='race',
rwidth=0.9, sharey=True,
layout=(1,3),
align='mid', figsize=(15,5))
for ax in axarr_race.flatten():
ax.set_xlabel("decile_score")
ax.set_ylabel("count")
ax.set_xticks(np.arange(1,11))
axarr_race = df[df.race.isin(['Asian','Native American','Other'])].hist(column='decile_score', by='race',
rwidth=0.9, sharey=True,
layout=(1,3),
align='mid', figsize=(15,5))
for ax in axarr_race.flatten():
ax.set_xlabel("decile_score")
ax.set_ylabel("count")
ax.set_xticks(np.arange(1,11))
# %%
visualize_decile_score_histograms(df)
# %%
######## Q3. Learning a Predictor #########
## Do not remove this random seed as it will change the results
np.random.seed(0)
def get_features_target_vectors(df):
## (a). Process subset of features in df into one-hot vectors. Store result in features
target = df.two_year_recid
x_race = pd.get_dummies(df.race)
x_sex = pd.get_dummies(df.sex)
x_age_cat = pd.get_dummies(df.age_cat)
x_c_charge_degree = pd.get_dummies(df.c_charge_degree)
x_priors_count = df.priors_count
features = pd.concat((x_race, x_sex, x_age_cat, x_c_charge_degree, x_priors_count), axis=1)
return features, target
# %%
## (a). Process subset of features in df into one-hot vectors. Store result in features
features, target = get_features_target_vectors(df)
## (b). Split (features, target) into 75% training set, 25% test set
X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(features, target, train_size=0.75)
# %%
## (c). Fit a logistic regression model on training set.
print('Q3: Logistic Regression Predictor')
logistic_clf = LogisticRegression(solver='lbfgs', max_iter=10000).fit(X_train, y_train)
print('Train set acc: {:.3f}'.format(logistic_clf.score(X_train, y_train)))
print('Test set acc: {:.3f}'.format(logistic_clf.score(X_test, y_test)))
# %%
######## Q4: Compute demographic parity ########
def compute_ratio_positive_decisions(clf, X_test, protected_feature=None, thresh=0.5):
if protected_feature:
Xp = X_test[X_test[protected_feature]==1]
else:
Xp = X_test
y_pred = clf.predict_proba(Xp)
f_theta = [p[1] for p in y_pred]
d = [1 if f >= thresh else 0 for f in f_theta]
return np.sum(d)/len(d)
def evaluate_demographic_parity(clf, X_test):
print('Demographic Parity\n')
base_ratio = compute_ratio_positive_decisions(clf, X_test, protected_feature=None, thresh=0.5)
print('Base ratio: {:.2f}'.format(base_ratio))
for xp in ['African-American', 'Caucasian', 'Male', 'Female']:
xp_ratio = compute_ratio_positive_decisions(clf, X_test, protected_feature = xp)
print('{} ratio: {:.2f}'.format(xp,xp_ratio))
print('Q4: Logistic regression')
evaluate_demographic_parity(logistic_clf, X_test)
# %%
######## Q6: Calibration Fairness ########
def discretize_score(v):
for n in np.arange(5):
if 0.2*n <= v and v <=0.2*(n+1):
return n
def compute_ratio_positives_given_score(clf, score, X_test, y_test, protected_feature=None, thresh=0.5):
if protected_feature:
Xp = X_test[X_test[protected_feature]==1]
else:
Xp = X_test
y_pred = clf.predict_proba(Xp)
f_theta = [p[1] for p in y_pred]
s = np.array([discretize_score(f) for f in f_theta])
y_test = np.array(y_test)
z = y_test[np.where(s==score)]
return np.sum(z)/len(z)
def evaluate_calibration_fairness(clf, X_test, y_test):
print('Calibration Fairness\n')
for score in np.arange(5):
s_ratio = compute_ratio_positives_given_score(clf, score, X_test, y_test, protected_feature=None, thresh=0.5)
print('Pr(Y=1|s(X)={:}) = {:.2f}'.format(score, s_ratio))
s_protected = []
for xp in ['African-American', 'Caucasian', 'Male', 'Female']:
s_ratio = compute_ratio_positives_given_score(clf, score, X_test, y_test, protected_feature=xp, thresh=0.5)
print('Pr(Y=1|s(X)={:}, {}) = {:.2f}'.format(score, xp ,s_ratio))
s_protected.append(s_ratio)
print('Std dev over the 4 protected attributes for s(X)={:}: {:.2f}'.format(score, np.std(s_protected)))
print()
print('Q6: Logistic regression')
evaluate_calibration_fairness(logistic_clf, X_test, y_test)
# %%
######## Q7: Post-hoc Calibration Fairness ########
logistic_clf = LogisticRegression(solver='lbfgs', max_iter=10000) #.fit(X_train, y_train)
calibrated_clf = CalibratedClassifierCV(base_estimator=logistic_clf, cv=3).fit(X_train, y_train)
print('Q7: Post-hoc calibration')
evaluate_calibration_fairness(calibrated_clf, X_test, y_test)
# %%
######## Q8: Calibration effect on Demographic Parity ########
print('Q8: Post-hoc calibration')
evaluate_demographic_parity(calibrated_clf, X_test)
# %%