Skip to content

Commit 6530104

Browse files
committed
update module 27 solution
1 parent 210ae09 commit 6530104

4 files changed

Lines changed: 142 additions & 46 deletions

File tree

.github/workflows/ci-m27-fairlearn.yaml

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ on:
44
push:
55
branches:
66
- main
7+
- content
78
paths:
89
- "module-27-fairlearn/**"
910

1011
jobs:
11-
fairness-check:
12+
train:
1213
runs-on: ubuntu-latest
1314
defaults:
1415
run:
@@ -30,10 +31,52 @@ jobs:
3031
- name: Install dependencies
3132
run: pip install -r requirements.txt
3233

33-
# TODO: Run the fairness gate against the pre-trained model artifacts
34-
# (model.joblib, X_test.npy, y_test.npy, sf_test.csv).
35-
# Run train.py locally first to generate these files and commit them.
36-
# Exits with code 1 if any fairness metric exceeds its threshold.
34+
# TODO: Train the model and save the model + test data artifacts
35+
- name: Train model
36+
run: python train.py
37+
38+
# TODO: Upload the model and test data so the check-fairness job can use them
39+
- name: Upload model artifacts
40+
uses: actions/upload-artifact@v4
41+
with:
42+
name: model-artifacts
43+
path: |
44+
module-27-fairlearn/solution/model.joblib
45+
module-27-fairlearn/solution/X_test.npy
46+
module-27-fairlearn/solution/y_test.npy
47+
module-27-fairlearn/solution/sf_test.csv
48+
49+
check-fairness:
50+
runs-on: ubuntu-latest
51+
needs: train
52+
defaults:
53+
run:
54+
working-directory: module-27-fairlearn/solution
55+
56+
steps:
57+
- uses: actions/checkout@v4
58+
59+
- uses: actions/setup-python@v5
60+
with:
61+
python-version: "3.12"
62+
63+
- uses: actions/cache@v4
64+
with:
65+
path: ~/.cache/pip
66+
key: pip-fairlearn-${{ hashFiles('module-27-fairlearn/solution/requirements.txt') }}
67+
68+
# TODO: Install dependencies from requirements.txt
69+
- name: Install dependencies
70+
run: pip install -r requirements.txt
71+
72+
# TODO: Download the model and test data artifacts from the train job
73+
- name: Download model artifacts
74+
uses: actions/download-artifact@v4
75+
with:
76+
name: model-artifacts
77+
path: module-27-fairlearn/solution
78+
79+
# TODO: Run the fairness gate against the model artifacts
3780
- name: Run fairness gate
3881
run: python check_fairness.py
3982

@@ -44,11 +87,3 @@ jobs:
4487
with:
4588
name: fairness-report
4689
path: module-27-fairlearn/solution/fairness_report.json
47-
48-
deploy:
49-
runs-on: ubuntu-latest
50-
needs: fairness-check
51-
steps:
52-
# TODO: Add deployment steps here — this job only runs if fairness-check passes
53-
- name: Deploy model
54-
run: echo "Fairness checks passed... deploying model."

module-27-fairlearn/solution/check_fairness.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,16 @@
2929
model = artifacts["model"]
3030
X_test = np.load("X_test.npy")
3131
y_test = np.load("y_test.npy")
32-
sf_test = pd.read_csv("sf_test.csv").squeeze()
32+
sf_test = pd.read_csv("sf_test.csv")
33+
sf_gender = sf_test["gender"]
34+
sf_race = sf_test["race"]
3335

3436
y_pred = model.predict(X_test)
3537
overall_acc = accuracy_score(y_test, y_pred)
3638
print(f"Overall accuracy: {overall_acc:.4f}")
3739

3840
# TODO: Build a MetricFrame with accuracy, precision, and recall broken down by
39-
# the sensitive feature (gender) and print per-subgroup results
41+
# the sensitive features (gender and race) and print per-subgroup results
4042
metrics = {
4143
"accuracy": accuracy_score,
4244
"precision": lambda y_true, y_pred: precision_score(y_true, y_pred, average="weighted", zero_division=0),
@@ -50,28 +52,49 @@
5052
sensitive_features=sf_test,
5153
)
5254

53-
print(f"\nPer-subgroup metrics (gender):")
55+
print(f"\nPer-subgroup metrics (gender x race):")
5456
print(mf.by_group.to_string())
5557

5658
# TODO: Compute demographic_parity_difference and equalized_odds_difference
57-
dpd = demographic_parity_difference(y_test, y_pred, sensitive_features=sf_test)
58-
eod = equalized_odds_difference(y_test, y_pred, sensitive_features=sf_test)
59+
# for both gender and race so we can compare the gaps
60+
dpd_gender = demographic_parity_difference(y_test, y_pred, sensitive_features=sf_gender)
61+
eod_gender = equalized_odds_difference(y_test, y_pred, sensitive_features=sf_gender)
62+
dpd_race = demographic_parity_difference(y_test, y_pred, sensitive_features=sf_race)
63+
eod_race = equalized_odds_difference(y_test, y_pred, sensitive_features=sf_race)
5964

60-
print(f"\nDemographic parity difference : {dpd:.4f} (threshold: {DPD_THRESHOLD})")
61-
print(f"Equalized odds difference : {eod:.4f} (threshold: {EOD_THRESHOLD})")
65+
print(f"\nDemographic parity difference (gender) : {dpd_gender:.4f} (threshold: {DPD_THRESHOLD})")
66+
print(f"Equalized odds difference (gender) : {eod_gender:.4f} (threshold: {EOD_THRESHOLD})")
67+
print(f"Demographic parity difference (race) : {dpd_race:.4f} (threshold: {DPD_THRESHOLD})")
68+
print(f"Equalized odds difference (race) : {eod_race:.4f} (threshold: {EOD_THRESHOLD})")
6269

70+
# TODO: Save a fairness report with the per-group metrics and gap comparisons to disk
71+
report = {
72+
"sensitive_attribute": "gender",
73+
"overall_accuracy": overall_acc,
74+
"demographic_parity_difference_gender": dpd_gender,
75+
"equalized_odds_difference_gender": eod_gender,
76+
"demographic_parity_difference_race": dpd_race,
77+
"equalized_odds_difference_race": eod_race,
78+
"by_group": mf.by_group.round(4).reset_index().to_dict(orient="records"),
79+
}
80+
with open("fairness_report.json", "w") as f:
81+
json.dump(report, f, indent=2)
6382

64-
# TODO: Implement the fairness gate. Exit with code 1 if either metric exceeds its threshold.
65-
failed = []
66-
if dpd > DPD_THRESHOLD:
67-
failed.append(f"demographic_parity_difference {dpd:.4f} > {DPD_THRESHOLD}")
68-
if eod > EOD_THRESHOLD:
69-
failed.append(f"equalized_odds_difference {eod:.4f} > {EOD_THRESHOLD}")
83+
# TODO: Implement the fairness gate. Exit with code 1 if any metric exceeds its threshold.
84+
metric_names = np.array([
85+
"demographic_parity_difference (gender)",
86+
"equalized_odds_difference (gender)",
87+
"demographic_parity_difference (race)",
88+
"equalized_odds_difference (race)",
89+
])
90+
values = np.array([dpd_gender, eod_gender, dpd_race, eod_race])
91+
thresholds = np.array([DPD_THRESHOLD, EOD_THRESHOLD, DPD_THRESHOLD, EOD_THRESHOLD])
92+
exceeded = values > thresholds
7093

71-
if failed:
94+
if exceeded.any():
7295
print("\nFairness check FAILED:")
73-
for f in failed:
74-
print(f" - {f}")
96+
for name, value, threshold in zip(metric_names[exceeded], values[exceeded], thresholds[exceeded]):
97+
print(f" - {name} {value:.4f} > {threshold}")
7598
sys.exit(1)
7699

77100
print("\nFairness check passed.")

module-27-fairlearn/solution/ci-m27-fairlearn.yaml

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
- "module-27-fairlearn/**"
99

1010
jobs:
11-
fairness-check:
11+
train:
1212
runs-on: ubuntu-latest
1313
defaults:
1414
run:
@@ -30,9 +30,52 @@ jobs:
3030
- name: Install dependencies
3131
run: pip install -r requirements.txt
3232

33-
# TODO: Run the fairness gate against the pre-trained model artifacts
34-
# (model.joblib, X_test.npy, y_test.npy, sf_test.csv).
35-
# Run train.py locally first to generate these files and commit them.
33+
# TODO: Train the model and save the model + test data artifacts
34+
- name: Train model
35+
run: python train.py
36+
37+
# TODO: Upload the model and test data so the check-fairness job can use them
38+
- name: Upload model artifacts
39+
uses: actions/upload-artifact@v4
40+
with:
41+
name: model-artifacts
42+
path: |
43+
module-27-fairlearn/solution/model.joblib
44+
module-27-fairlearn/solution/X_test.npy
45+
module-27-fairlearn/solution/y_test.npy
46+
module-27-fairlearn/solution/sf_test.csv
47+
48+
check-fairness:
49+
runs-on: ubuntu-latest
50+
needs: train
51+
defaults:
52+
run:
53+
working-directory: module-27-fairlearn/solution
54+
55+
steps:
56+
- uses: actions/checkout@v4
57+
58+
- uses: actions/setup-python@v5
59+
with:
60+
python-version: "3.12"
61+
62+
- uses: actions/cache@v4
63+
with:
64+
path: ~/.cache/pip
65+
key: pip-fairlearn-${{ hashFiles('module-27-fairlearn/solution/requirements.txt') }}
66+
67+
# TODO: Install dependencies from requirements.txt
68+
- name: Install dependencies
69+
run: pip install -r requirements.txt
70+
71+
# TODO: Download the model and test data artifacts from the train job
72+
- name: Download model artifacts
73+
uses: actions/download-artifact@v4
74+
with:
75+
name: model-artifacts
76+
path: module-27-fairlearn/solution
77+
78+
# TODO: Run the fairness gate against the model artifacts
3679
- name: Run fairness gate
3780
run: python check_fairness.py
3881

@@ -43,12 +86,3 @@ jobs:
4386
with:
4487
name: fairness-report
4588
path: module-27-fairlearn/solution/fairness_report.json
46-
47-
deploy:
48-
runs-on: ubuntu-latest
49-
needs: fairness-check
50-
steps:
51-
# TODO: Add deployment steps here
52-
# this job only runs if fairness-check passes
53-
- name: Deploy model
54-
run: echo "Fairness checks passed... deploying model."

module-27-fairlearn/solution/train.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
python train.py
55
"""
66

7+
import time
8+
79
import joblib
810
import numpy as np
911
import pandas as pd
@@ -16,8 +18,8 @@
1618
data = fetch_diabetes_hospital(as_frame=True)
1719
X, y = data.data, data.target
1820

19-
# TODO: Extract the sensitive feature (gender)
20-
sensitive_feature = X["gender"].copy()
21+
# TODO: Extract the sensitive features (gender, race)
22+
sensitive_features = X[["gender", "race"]].copy()
2123
# TODO: drop protected attributes (gender, race) and the target column (readmitted) from X
2224
X = X.drop(columns=["gender", "race", "readmitted"], errors="ignore")
2325

@@ -30,18 +32,20 @@
3032
# TODO: Encode target labels
3133
y = LabelEncoder().fit_transform(y)
3234

33-
# TODO: split into train/test sets, keeping the sensitive feature aligned with the split
35+
# TODO: split into train/test sets, keeping the sensitive features aligned with the split
3436
X_train, X_test, y_train, y_test, sf_train, sf_test = train_test_split(
35-
X, y, sensitive_feature, test_size=0.2, random_state=42
37+
X, y, sensitive_features, test_size=0.2, random_state=42
3638
)
3739

3840
scaler = StandardScaler()
3941
X_train = scaler.fit_transform(X_train)
4042
X_test = scaler.transform(X_test)
4143

4244
print("Training MLP...")
45+
start_time = time.time()
4346
model = MLPClassifier(hidden_layer_sizes=(64, 32), max_iter=100, random_state=42)
4447
model.fit(X_train, y_train)
48+
print(f"Training took {time.time() - start_time:.2f} seconds")
4549

4650
joblib.dump({"model": model, "scaler": scaler}, "model.joblib")
4751
np.save("X_test.npy", X_test)

0 commit comments

Comments
 (0)