====================================
========================
Responsible and Safe AI Assignment
Income Prediction Model using Adult Dataset
BY
MONISA R
2303917724422031
====================================
========================
# Part 1: Load dataset and basic preprocessing
import pandas as pd
from [Link] import fetch_openml
from sklearn.model_selection import train_test_split
from [Link] import OneHotEncoder
from [Link] import ColumnTransformer
from [Link] import Pipeline
from [Link] import accuracy_score
from sklearn.linear_model import LogisticRegression
# Load Adult dataset
data = fetch_openml("adult", version=2, as_frame=True)
df = [Link]()
# Convert income label to binary
df['income'] = (df['class'] == '>50K').astype(int)
# Drop the old target column name
df = [Link](columns=['class'])
# Identify features
target = "income"
features = [Link](target)
# Split data
X_train, X_test, y_train, y_test = train_test_split(
df[features], df[target], test_size=0.2, random_state=42,
stratify=df[target]
)
# Identify categorical and numeric columns
categorical_cols =
X_train.select_dtypes(include="category").[Link]() + \
X_train.select_dtypes(include="object").[Link]()
numeric_cols =
X_train.select_dtypes(include="number").[Link]()
print("Categorical columns:", categorical_cols)
print("Numeric columns:", numeric_cols)
print("Train/Test shapes:", X_train.shape, X_test.shape)
Categorical columns: ['workclass', 'education', 'marital-status',
'occupation', 'relationship', 'race', 'sex', 'native-country']
Numeric columns: ['age', 'fnlwgt', 'education-num', 'capital-gain',
'capital-loss', 'hours-per-week']
Train/Test shapes: (39073, 14) (9769, 14)
[Link]()
{"summary":"{\n \"name\": \"df\",\n \"rows\": 48842,\n \"fields\":
[\n {\n \"column\": \"age\",\n \"properties\": {\n
\"dtype\": \"number\",\n \"std\": 13,\n \"min\": 17,\n
\"max\": 90,\n \"num_unique_values\": 74,\n \"samples\":
[\n 18,\n 74,\n 40\n ],\n
\"semantic_type\": \"\",\n \"description\": \"\"\n }\
n },\n {\n \"column\": \"workclass\",\n
\"properties\": {\n \"dtype\": \"category\",\n
\"num_unique_values\": 8,\n \"samples\": [\n \"Local-
gov\",\n \"Self-emp-inc\",\n \"Private\"\
n ],\n \"semantic_type\": \"\",\n
\"description\": \"\"\n }\n },\n {\n \"column\":
\"fnlwgt\",\n \"properties\": {\n \"dtype\": \"number\",\n
\"std\": 105604,\n \"min\": 12285,\n \"max\": 1490400,\n
\"num_unique_values\": 28523,\n \"samples\": [\n
171041,\n 20296,\n 263896\n ],\n
\"semantic_type\": \"\",\n \"description\": \"\"\n }\
n },\n {\n \"column\": \"education\",\n
\"properties\": {\n \"dtype\": \"category\",\n
\"num_unique_values\": 16,\n \"samples\": [\n
\"11th\",\n \"HS-grad\",\n \"Prof-school\"\
n ],\n \"semantic_type\": \"\",\n
\"description\": \"\"\n }\n },\n {\n \"column\":
\"education-num\",\n \"properties\": {\n \"dtype\":
\"number\",\n \"std\": 2,\n \"min\": 1,\n
\"max\": 16,\n \"num_unique_values\": 16,\n \"samples\":
[\n 7,\n 9,\n 15\n ],\n
\"semantic_type\": \"\",\n \"description\": \"\"\n }\
n },\n {\n \"column\": \"marital-status\",\n
\"properties\": {\n \"dtype\": \"category\",\n
\"num_unique_values\": 7,\n \"samples\": [\n \"Never-
married\",\n \"Married-civ-spouse\",\n \"Married-
spouse-absent\"\n ],\n \"semantic_type\": \"\",\n
\"description\": \"\"\n }\n },\n {\n \"column\":
\"occupation\",\n \"properties\": {\n \"dtype\":
\"category\",\n \"num_unique_values\": 14,\n
\"samples\": [\n \"Sales\",\n \"Transport-moving\",\
n \"Machine-op-inspct\"\n ],\n
\"semantic_type\": \"\",\n \"description\": \"\"\n }\
n },\n {\n \"column\": \"relationship\",\n
\"properties\": {\n \"dtype\": \"category\",\n
\"num_unique_values\": 6,\n \"samples\": [\n \"Own-
child\",\n \"Husband\",\n \"Other-relative\"\n
],\n \"semantic_type\": \"\",\n \"description\": \"\"\n
}\n },\n {\n \"column\": \"race\",\n \"properties\":
{\n \"dtype\": \"category\",\n \"num_unique_values\":
5,\n \"samples\": [\n \"White\",\n \"Amer-
Indian-Eskimo\",\n \"Asian-Pac-Islander\"\n ],\n
\"semantic_type\": \"\",\n \"description\": \"\"\n }\
n },\n {\n \"column\": \"sex\",\n \"properties\": {\n
\"dtype\": \"category\",\n \"num_unique_values\": 2,\n
\"samples\": [\n \"Female\",\n \"Male\"\n ],\
n \"semantic_type\": \"\",\n \"description\": \"\"\n
}\n },\n {\n \"column\": \"capital-gain\",\n
\"properties\": {\n \"dtype\": \"number\",\n \"std\":
7452,\n \"min\": 0,\n \"max\": 99999,\n
\"num_unique_values\": 123,\n \"samples\": [\n 4064,\n
4787\n ],\n \"semantic_type\": \"\",\n
\"description\": \"\"\n }\n },\n {\n \"column\":
\"capital-loss\",\n \"properties\": {\n \"dtype\":
\"number\",\n \"std\": 403,\n \"min\": 0,\n
\"max\": 4356,\n \"num_unique_values\": 99,\n
\"samples\": [\n 2238,\n 1564\n ],\n
\"semantic_type\": \"\",\n \"description\": \"\"\n }\
n },\n {\n \"column\": \"hours-per-week\",\n
\"properties\": {\n \"dtype\": \"number\",\n \"std\":
12,\n \"min\": 1,\n \"max\": 99,\n
\"num_unique_values\": 96,\n \"samples\": [\n 9,\n
11\n ],\n \"semantic_type\": \"\",\n
\"description\": \"\"\n }\n },\n {\n \"column\":
\"native-country\",\n \"properties\": {\n \"dtype\":
\"category\",\n \"num_unique_values\": 41,\n
\"samples\": [\n \"Canada\",\n \"South\"\
n ],\n \"semantic_type\": \"\",\n
\"description\": \"\"\n }\n },\n {\n \"column\":
\"income\",\n \"properties\": {\n \"dtype\": \"number\",\n
\"std\": 0,\n \"min\": 0,\n \"max\": 1,\n
\"num_unique_values\": 2,\n \"samples\": [\n 1,\n
0\n ],\n \"semantic_type\": \"\",\n
\"description\": \"\"\n }\n }\n ]\
n}","type":"dataframe","variable_name":"df"}
# Part 2 (Updated): Build pipeline with RandomForest, train, compute
accuracy
from [Link] import RandomForestClassifier
# Preprocessing: One-hot encode categorical variables, passthrough
numeric
preprocessor = ColumnTransformer(
transformers=[
("cat", OneHotEncoder(handle_unknown="ignore"),
categorical_cols),
("num", "passthrough", numeric_cols)
]
)
# Random Forest model
model = Pipeline(steps=[
("preprocess", preprocessor),
("clf", RandomForestClassifier(
n_estimators=200,
max_depth=None,
min_samples_split=2,
n_jobs=-1,
random_state=42
))
])
# Train the model
[Link](X_train, y_train)
# Predict
y_pred = [Link](X_test)
# Accuracy
accuracy = accuracy_score(y_test, y_pred)
print("Random Forest Model Accuracy:", accuracy)
Random Forest Model Accuracy: 0.8585320913092436
import shap
import numpy as np
# ---- Convert background to NumPy ----
background_np = X_train.sample(50, random_state=42).to_numpy()
# ---- Predict function that converts NumPy → DataFrame → pipeline
----
def pipeline_predict_numpy(x):
df = [Link](x, columns=X_train.columns)
return model.predict_proba(df)
# ---- KernelExplainer (safe version) ----
explainer = [Link](
pipeline_predict_numpy,
background_np
)
# ---- Sample test rows in NumPy ----
X_test_sample_np = X_test.sample(10, random_state=42).to_numpy()
# SHAP values
shap_values = explainer.shap_values(X_test_sample_np)
# Summary plot
shap.summary_plot(shap_values, X_test_sample_np,
feature_names=X_train.columns)
{"model_id":"f203f40b1a3e425cbe7126f124ea325d","version_major":2,"vers
ion_minor":0}
!pip install --quiet lime
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/275.7 kB ? eta -:--:--
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━ 266.2/275.7 kB 7.9 MB/s eta
0:00:01 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 275.7/275.7 kB 5.2
MB/s eta 0:00:00
etadata ([Link]) ... e ([Link]) ...
from lime.lime_tabular import LimeTabularExplainer
import numpy as np
import pandas as pd
from [Link] import LabelEncoder
# -------------------------------------------------------
# 1. Make a LIME-safe version of the dataset (encode cats)
# -------------------------------------------------------
X_train_lime = X_train.copy()
X_test_lime = X_test.copy()
label_encoders = {}
for col in categorical_cols:
le = LabelEncoder()
X_train_lime[col] = le.fit_transform(X_train_lime[col])
X_test_lime[col] = [Link](X_test_lime[col])
label_encoders[col] = le
# Convert to NumPy
X_train_np = X_train_lime.values
X_test_np = X_test_lime.values
# -------------------------------------------------------
# 2. Create LIME explainer
# -------------------------------------------------------
explainer = LimeTabularExplainer(
training_data=X_train_np,
feature_names=X_train.[Link](),
class_names=['<=50K', '>50K'],
mode="classification"
)
# -------------------------------------------------------
# 3. Prediction wrapper (pipeline expects original df)
# -------------------------------------------------------
def lime_predict(x):
# Convert numpy → dataframe
df = [Link](x, columns=X_train.columns)
# Decode categorical variables back to original strings
for col in categorical_cols:
le = label_encoders[col]
df[col] = le.inverse_transform(df[col].astype(int))
return model.predict_proba(df)
# -------------------------------------------------------
# 4. Explain one row
# -------------------------------------------------------
i = 0
instance = X_test_np[i]
exp = explainer.explain_instance(
data_row=instance,
predict_fn=lime_predict
)
exp.show_in_notebook(show_table=True)
exp.save_to_file("lime_explanation.html")
<[Link] object>
# Part 3 (Updated): Accuracy for men vs women
# Create a copy of X_test including predictions
X_test_copy = X_test.copy()
X_test_copy["y_true"] = y_test
X_test_copy["y_pred"] = y_pred
# Separate by gender
men = X_test_copy[X_test_copy["sex"] == "Male"]
women = X_test_copy[X_test_copy["sex"] == "Female"]
# Compute accuracy
men_acc = accuracy_score(men["y_true"], men["y_pred"])
women_acc = accuracy_score(women["y_true"], women["y_pred"])
print("Accuracy for Men:", men_acc)
print("Accuracy for Women:", women_acc)
Accuracy for Men: 0.8227342549923196
Accuracy for Women: 0.9300398895366677
# Part 4: Analyze prediction distribution across race groups
# Group by race and compute distribution of predicted income labels
race_distribution = X_test_copy.groupby("race")
["y_pred"].value_counts(normalize=True).unstack().fillna(0)
# Rename columns for clarity
race_distribution.columns = ["Predicted Low Income (0)", "Predicted
High Income (1)"]
print("Prediction Distribution Across Race Groups:\n")
print(race_distribution)
/tmp/[Link]: FutureWarning: The default of
observed=False is deprecated and will be changed to True in a future
version of pandas. Pass observed=False to retain current behavior or
observed=True to adopt the future default and silence this warning.
race_distribution = X_test_copy.groupby("race")
["y_pred"].value_counts(normalize=True).unstack().fillna(0)
Prediction Distribution Across Race Groups:
Predicted Low Income (0) Predicted High Income
(1)
race
Amer-Indian-Eskimo 0.916667
0.083333
Asian-Pac-Islander 0.768489
0.231511
Black 0.908058
0.091942
Other 0.951220
0.048780
White 0.782122
0.217878
# Part 5: Compute demographic parity based on gender
# Percentage predicted high income for men
male_high_income_rate = (
X_test_copy[X_test_copy["sex"] == "Male"]["y_pred"].mean()
)
# Percentage predicted high income for women
female_high_income_rate = (
X_test_copy[X_test_copy["sex"] == "Female"]["y_pred"].mean()
)
print("Demographic Parity (Predicted High Income Rates):")
print("Men: {:.2f}%".format(male_high_income_rate * 100))
print("Women: {:.2f}%".format(female_high_income_rate * 100))
Demographic Parity (Predicted High Income Rates):
Men: 26.22%
Women: 8.50%
# ============================================
# Part 6 (Advanced): Adversarial Robustness Tests
# ============================================
import numpy as np
import pandas as pd
# Pick one concrete sample from test set
sample = X_test.iloc[[0]].copy()
print("=== Original Test Sample ===")
print(sample)
print()
# Get base prediction
base_pred = [Link](sample)[0]
print("Base Prediction:", base_pred)
print("-" * 60)
# --------------------------------------------------
# 1. Feature-Importance-Based Attack (Top Features)
# --------------------------------------------------
# Get feature importances from RandomForest
rf_clf = model.named_steps["clf"]
importances = rf_clf.feature_importances_
feature_importance = [Link]({
"feature": model.named_steps["preprocess"]
.get_feature_names_out().tolist(),
"importance": importances
}).sort_values("importance", ascending=False)
top_features = feature_importance.head(3)["feature"].tolist()
print("\n=== 1. Feature-Importance Attack (Top features) ===")
print("Top impactful features:", top_features)
for f in top_features:
modified = [Link]()
# If numeric → scaled perturbation
try:
original_val = modified[f].values[0]
modified[f] = modified[f] * 1.5 if isinstance(original_val,
(int, float)) else modified[f]
except:
pass
pred = [Link](modified)[0]
print(f"Feature perturbed: {f}")
print("Prediction:", pred)
if pred != base_pred:
print("⚠ Flip detected!")
print()
# --------------------------------------------------
# 2. Counterfactual Fairness Attack (Flip Gender)
# --------------------------------------------------
print("\n=== 2. Counterfactual Fairness Attack (Gender Swap) ===")
cf = [Link]()
if "sex" in [Link]:
# Flip male/female
if cf["sex"].values[0] == "Male":
cf["sex"] = "Female"
else:
cf["sex"] = "Male"
cf_pred = [Link](cf)[0]
print("Original gender:", sample["sex"].values[0])
print("Counterfactual gender:", cf["sex"].values[0])
print("Prediction after flip:", cf_pred)
if cf_pred != base_pred:
print("⚠ Gender-sensitive prediction detected!")
print()
# --------------------------------------------------
# 3. Rare-Category Attack
# --------------------------------------------------
print("\n=== 3. Rare Category Attack ===")
rare = [Link]()
# Replace workclass & occupation with rare categories
if "workclass" in [Link]:
rare["workclass"] = "Without-pay"
if "occupation" in [Link]:
rare["occupation"] = "Armed-Forces"
rare_pred = [Link](rare)[0]
print("Modified rare categories:")
print(rare[["workclass", "occupation"]])
print("Prediction:", rare_pred)
if rare_pred != base_pred:
print("⚠ Model unstable for rare categories!")
print()
# --------------------------------------------------
# 4. Boundary Condition Attack
# --------------------------------------------------
print("\n=== 4. Boundary Condition Attack ===")
boundary = [Link]()
if "hours-per-week" in [Link]:
boundary["hours-per-week"] = 1 # Extreme low boundary
if "age" in [Link]:
boundary["age"] = 90 # Extreme high boundary
boundary_pred = [Link](boundary)[0]
print("Boundary-modified sample:")
print(boundary[["age", "hours-per-week"]])
print("Prediction:", boundary_pred)
if boundary_pred != base_pred:
print("⚠ Model sensitive to extreme boundary values!")
print()
# --------------------------------------------------
# 5. Multi-Feature Joint Attack
# --------------------------------------------------
print("\n=== 5. Multi-Feature Joint Attack ===")
multi = [Link]()
if "age" in [Link]:
multi["age"] += 15
if "hours-per-week" in [Link]:
multi["hours-per-week"] += 10
if "capital-gain" in [Link]:
multi["capital-gain"] += 5000
multi_pred = [Link](multi)[0]
print("Prediction under joint multi-feature attack:", multi_pred)
if multi_pred != base_pred:
print("⚠ Joint modification flips prediction!")
print()
# --------------------------------------------------
# 6. Noise Injection Attack
# --------------------------------------------------
print("\n=== 6. Noise Injection Attack (±5% random noise) ===")
noise = [Link]()
numeric_cols_local = X_test.select_dtypes(include="number").columns
for col in numeric_cols_local:
noise[col] = noise[col] * (1 + [Link](-0.05, 0.05))
noise_pred = [Link](noise)[0]
print("Prediction after random noise:", noise_pred)
if noise_pred != base_pred:
print("⚠ Noise-sensitive model!")
else:
print("✓ Model stable under random noise.")
=== Original Test Sample ===
age workclass fnlwgt education education-num marital-
status \
40342 54 Private 115602 HS-grad 9 Married-civ-
spouse
occupation relationship race sex capital-gain
capital-loss \
40342 Other-service Wife Black Female 0
0
hours-per-week native-country
40342 40 United-States
Base Prediction: 0
------------------------------------------------------------
=== 1. Feature-Importance Attack (Top features) ===
Top impactful features: ['num__fnlwgt', 'num__age', 'num__capital-
gain']
Feature perturbed: num__fnlwgt
Prediction: 0
Feature perturbed: num__age
Prediction: 0
Feature perturbed: num__capital-gain
Prediction: 0
=== 2. Counterfactual Fairness Attack (Gender Swap) ===
Original gender: Female
Counterfactual gender: Male
Prediction after flip: 0
=== 3. Rare Category Attack ===
Modified rare categories:
workclass occupation
40342 Without-pay Armed-Forces
Prediction: 0
=== 4. Boundary Condition Attack ===
Boundary-modified sample:
age hours-per-week
40342 90 1
Prediction: 0
=== 5. Multi-Feature Joint Attack ===
Prediction under joint multi-feature attack: 0
=== 6. Noise Injection Attack (±5% random noise) ===
Prediction after random noise: 0
✓ Model stable under random noise.
# Part 7: AI Audit Report
def print_ai_audit_report():
print("\n==========================")
print(" AI MODEL AUDIT")
print("==========================\n")
# Overall accuracy
print(f"Overall Accuracy: {accuracy:.4f}\n")
# Gender-specific accuracy
print("Gender-Specific Accuracy:")
print(f" Men Accuracy: {men_acc:.4f}")
print(f" Women Accuracy: {women_acc:.4f}\n")
# Demographic parity
print("Demographic Parity (Predicted High Income %):")
print(f" Men: {male_high_income_rate * 100:.2f}%")
print(f" Women: {female_high_income_rate * 100:.2f}%\n")
# Race distribution
print("Prediction Distribution Across Race Groups:")
print(race_distribution)
print("\n==========================")
print(" END OF AUDIT REPORT")
print("==========================\n")
# Print the audit report
print_ai_audit_report()
==========================
AI MODEL AUDIT
==========================
Overall Accuracy: 0.8585
Gender-Specific Accuracy:
Men Accuracy: 0.8227
Women Accuracy: 0.9300
Demographic Parity (Predicted High Income %):
Men: 26.22%
Women: 8.50%
Prediction Distribution Across Race Groups:
Predicted Low Income (0) Predicted High Income
(1)
race
Amer-Indian-Eskimo 0.916667
0.083333
Asian-Pac-Islander 0.768489
0.231511
Black 0.908058
0.091942
Other 0.951220
0.048780
White 0.782122
0.217878
==========================
END OF AUDIT REPORT
==========================
# =========================================================
# PART 1 — Load Dataset (keep full copy for fairness audit)
# =========================================================
from [Link] import fetch_openml
import pandas as pd
# Load Adult dataset
data = fetch_openml("adult", version=2, as_frame=True)
df_full = [Link]() # <-- FULL DATA WITH GENDER (for
fairness)
df = df_full.copy() # <-- TRAINING DATA (gender removed
later)
# Convert income label to binary
df_full["income"] = (df_full["class"] == ">50K").astype(int)
df["income"] = df_full["income"]
# =========================================================
# PART 2 — Remove gender ONLY in training dataset
# =========================================================
df = [Link](columns=["sex", "class"]) # <-- gender removed for
training
df_full = df_full.drop(columns=["class"]) # <-- keep gender for
fairness only
print("Training Columns (gender removed):", [Link])
# =========================================================
# PART 3 — Split dataset (same indices for both copies)
# =========================================================
from sklearn.model_selection import train_test_split
X = [Link](columns=["income"])
y = df["income"]
X_full = df_full.drop(columns=["income"]) # still has "sex"
y_full = df_full["income"]
# Split using index tracking so both datasets align
X_train, X_test, y_train, y_test, idx_train, idx_test =
train_test_split(
X, y, range(len(X)), test_size=0.2, random_state=42, stratify=y
)
# Test set WITH gender (for fairness evaluation)
X_test_full = X_full.iloc[idx_test]
y_test_full = y_full.iloc[idx_test]
# =========================================================
# PART 4 — Preprocess + Random Forest Model (without gender)
# =========================================================
from [Link] import OneHotEncoder
from [Link] import ColumnTransformer
from [Link] import Pipeline
from [Link] import RandomForestClassifier
from [Link] import accuracy_score
categorical_cols = X_train.select_dtypes(include=["object",
"category"]).[Link]()
numeric_cols =
X_train.select_dtypes(include="number").[Link]()
preprocessor = ColumnTransformer(
transformers=[
("cat", OneHotEncoder(handle_unknown="ignore"),
categorical_cols),
("num", "passthrough", numeric_cols)
]
)
model = Pipeline(steps=[
("preprocess", preprocessor),
("clf", RandomForestClassifier(
n_estimators=200,
random_state=42,
n_jobs=-1
))
])
[Link](X_train, y_train)
y_pred = [Link](X_test)
accuracy = accuracy_score(y_test, y_pred)
print("\nModel Accuracy WITHOUT Gender Column:", accuracy)
# =========================================================
# PART 5 — Fairness Audit Dataset (merge gender back)
# =========================================================
X_test_full = X_test_full.copy()
X_test_full["y_true"] = y_test.values
X_test_full["y_pred"] = y_pred
# Confirm gender exists
print("\nContains gender?:", "sex" in X_test_full.columns)
# =========================================================
# PART 6 — Accuracy for Men vs Women
# =========================================================
men = X_test_full[X_test_full["sex"] == "Male"]
women = X_test_full[X_test_full["sex"] == "Female"]
men_acc = accuracy_score(men["y_true"], men["y_pred"])
women_acc = accuracy_score(women["y_true"], women["y_pred"])
print("\nAccuracy for Men (Gender Removed):", men_acc)
print("Accuracy for Women (Gender Removed):", women_acc)
# =========================================================
# PART 7 — Demographic Parity
# =========================================================
male_rate = men["y_pred"].mean()
female_rate = women["y_pred"].mean()
print("\nDemographic Parity (High Income Prediction %):")
print(" Men: {:.2f}%".format(male_rate * 100))
print(" Women: {:.2f}%".format(female_rate * 100))
Training Columns (gender removed): Index(['age', 'workclass',
'fnlwgt', 'education', 'education-num',
'marital-status', 'occupation', 'relationship', 'race',
'capital-gain',
'capital-loss', 'hours-per-week', 'native-country', 'income'],
dtype='object')
Model Accuracy WITHOUT Gender Column: 0.8585320913092436
Contains gender?: True
Accuracy for Men (Gender Removed): 0.823195084485407
Accuracy for Women (Gender Removed): 0.9291193617674133
Demographic Parity (High Income Prediction %):
Men: 26.05%
Women: 8.71%