Phase 2: Bias Detection & Fairness Metrics · 45 min · Python · AIF360 · scikit-learn
The Problem
Fairlearn is excellent for computing standard fairness metrics across groups. But some bias detection tasks require more specialized tools:
- Consistency: How similar are predictions for individuals who are similar except for their protected attribute?
- Generalized entropy index: How unequal is the distribution of predictions across individuals?
- Rich subgroup analysis: When you need to analyze intersections (e.g., Black women vs. Black men vs. white women vs. white men)
- Pre/post-mitigation comparison: When you need to compare bias before and after applying a mitigation algorithm
IBM's AI Fairness 360 (AIF360) is built for these cases. It provides a different API than Fairlearn — based on BinaryLabelDataset and ClassificationMetric objects — but it covers a wider range of metrics and mitigation algorithms.
AIF360 vs. fairlearn: when to use which
| Criterion | Fairlearn | AIF360 |
|---|
| Ease of use | Simpler API, sklearn-compatible | More complex, custom data structures |
| Metric variety | Standard metrics (DP, EO, DI) | 40+ metrics including consistency, entropy |
| Mitigation algorithms | 3 (reweighing, exponentiated gradients, threshold optimization) | 10+ including adversarial debiasing, reject option classification |
| Intersectional analysis | Supported via multi-index | Native support via multiple protected attributes |
| Industry adoption | More common in job postings | Common in research and enterprise |
| Best for | Quick audits, dashboards, production pipelines | Deep analysis, research-grade metrics, complex mitigation |
The practical pattern: Use Fairlearn for the standard audit (Phase 2, Lesson 3). Use AIF360 when you need deeper analysis or more advanced mitigation. Combine both in a single report for comprehensive coverage.
AIF360's core API
AIF360 uses a custom data structure — BinaryLabelDataset — that wraps a pandas DataFrame with metadata about which columns are features, labels, and protected attributes.
from aif360.datasets import BinaryLabelDataset
from aif360.metrics import ClassificationMetric, BinaryLabelDatasetMetric
# Convert pandas DataFrame to AIF360 format
dataset = BinaryLabelDataset(
df=df,
label_names=["repaid"],
protected_attribute_names=["race"],
favorable_label=1,
unfavorable_label=0,
)
# Compute dataset-level metrics (before model prediction)
dataset_metric = BinaryLabelDatasetMetric(
dataset,
unprivileged_groups=[{"race": "Group_B"}],
privileged_groups=[{"race": "Group_A"}],
)
# Compute classification metrics (after model prediction)
classified_metric = ClassificationMetric(
dataset, # ground truth
predicted_dataset, # model predictions
unprivileged_groups=[{"race": "Group_B"}],
privileged_groups=[{"race": "Group_A"}],
)
Building the AIF360 extension
Let's extend the bias audit with AIF360 metrics.
"""
bias_audit_aif360.py
Extends the bias audit with AIF360 advanced metrics.
"""
import json
import numpy as np
import pandas as pd
def run_aif360_audit(df, y_true_col, y_pred_col, protected_attrs,
favorable_label=1, unfavorable_label=0):
"""
Run AIF360 advanced bias metrics on a dataset with predictions.
Args:
df: DataFrame containing true labels, predictions, and protected attributes
y_true_col: Column name for ground truth
y_pred_col: Column name for predictions
protected_attrs: List of protected attribute column names
favorable_label: Value representing a favorable outcome
unfavorable_label: Value representing an unfavorable outcome
Returns:
Dict with AIF360 metrics
"""
try:
from aif360.datasets import BinaryLabelDataset
from aif360.metrics import ClassificationMetric, BinaryLabelDatasetMetric
except ImportError:
return {
"error": "AIF360 not installed. Install with: pip install aif360",
"metrics": None,
}
# Define privileged and unprivileged groups
# For binary protected attributes, we infer from the data
groups = {}
for attr in protected_attrs:
values = sorted(df[attr].unique())
if len(values) == 2:
groups[attr] = {
"privileged": [{attr: values[0]}],
"unprivileged": [{attr: values[1]}],
}
else:
# For multi-class, use the most common as privileged
most_common = df[attr].value_counts().index[0]
groups[attr] = {
"privileged": [{attr: most_common}],
"unprivileged": [{attr: v} for v in values if v != most_common],
}
# Create BinaryLabelDataset for ground truth
feature_cols = [c for c in df.columns if c not in [y_true_col, y_pred_col] + protected_attrs]
gt_dataset = BinaryLabelDataset(
df=df[[*feature_cols, *protected_attrs, y_true_col]],
label_names=[y_true_col],
protected_attribute_names=protected_attrs,
favorable_label=favorable_label,
unfavorable_label=unfavorable_label,
)
# Create BinaryLabelDataset for predictions (replace labels with predictions)
pred_df = df[[*feature_cols, *protected_attrs, y_pred_col]].rename(columns={y_pred_col: y_true_col})
pred_dataset = BinaryLabelDataset(
df=pred_df,
label_names=[y_true_col],
protected_attribute_names=protected_attrs,
favorable_label=favorable_label,
unfavorable_label=unfavorable_label,
)
results = {}
for attr, group_def in groups.items():
metric = ClassificationMetric(
gt_dataset,
pred_dataset,
unprivileged_groups=group_def["unprivileged"],
privileged_groups=group_def["privileged"],
)
ds_metric = BinaryLabelDatasetMetric(
gt_dataset,
unprivileged_groups=group_def["unprivileged"],
privileged_groups=group_def["privileged"],
)
results[attr] = {
"privileged_group": group_def["privileged"],
"unprivileged_group": group_def["unprivileged"],
"metrics": {
"disparate_impact": round(float(metric.disparate_impact()), 4),
"statistical_parity_difference": round(float(metric.statistical_parity_difference()), 4),
"equal_opportunity_difference": round(float(metric.equal_opportunity_difference()), 4),
"average_odds_difference": round(float(metric.average_odds_difference()), 4),
"average_abs_odds_difference": round(float(metric.average_abs_odds_difference()), 4),
"theil_index": round(float(metric.theil_index()), 4),
"consistency": round(float(ds_metric.consistency()[0]), 4),
"generalized_entropy_index": round(float(metric.generalized_entropy_index()), 4),
},
"pass_fail": {
"disparate_impact_80_rule": float(metric.disparate_impact()) >= 0.80,
"statistical_parity": abs(float(metric.statistical_parity_difference())) < 0.05,
"equal_opportunity": abs(float(metric.equal_opportunity_difference())) < 0.05,
},
}
return results
def combine_audits(fairlearn_report, aif360_report):
"""Combine Fairlearn and AIF360 audit results into a single report."""
combined = {
"fairlearn_metrics": fairlearn_report,
"aif360_metrics": aif360_report,
"combined_summary": {},
}
# Cross-reference key metrics
for attr in fairlearn_report.get("fairness_metrics", {}):
fl = fairlearn_report["fairness_metrics"][attr]
aif = aif360_report.get(attr, {}).get("metrics", {})
combined["combined_summary"][attr] = {
"fairlearn_di_ratio": fl.get("disparate_impact_ratio"),
"aif360_di_ratio": aif.get("disparate_impact"),
"fairlearn_eo_diff": fl.get("equalized_odds_difference"),
"aif360_avg_odds_diff": aif.get("average_odds_difference"),
"aif360_consistency": aif.get("consistency"),
"aif360_theil_index": aif.get("theil_index"),
"aif360_entropy": aif.get("generalized_entropy_index"),
}
return combined
if __name__ == "__main__":
# Generate synthetic data
np.random.seed(42)
n = 3000
race = np.random.choice(["Group_A", "Group_B"], n, p=[0.6, 0.4])
income = np.where(race == "Group_A", np.random.normal(65000, 20000, n),
np.random.normal(48000, 18000, n))
credit_score = np.where(race == "Group_A", np.random.normal(720, 80, n),
np.random.normal(680, 90, n))
repaid = np.where(race == "Group_A", np.random.binomial(1, 0.85, n),
np.random.binomial(1, 0.70, n))
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
df = pd.DataFrame({
"race": race,
"income": income.clip(15000, 200000).astype(int),
"credit_score": credit_score.clip(300, 850).astype(int),
"repaid": repaid,
})
X = df[["income", "credit_score"]]
y = df["repaid"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
# Prepare data for AIF360
test_df = df.loc[X_test.index].copy()
test_df["y_pred"] = y_pred
test_df["repaid"] = y_test.values
# Run AIF360 audit
aif360_results = run_aif360_audit(
test_df,
y_true_col="repaid",
y_pred_col="y_pred",
protected_attrs=["race"],
)
if "error" in aif360_results:
print(aif360_results["error"])
else:
with open("aif360_audit_report.json", "w") as f:
json.dump(aif360_results, f, indent=2)
print("AIF360 audit report saved to aif360_audit_report.json")
for attr, data in aif360_results.items():
print(f"\n{attr}:")
for metric, value in data["metrics"].items():
print(f" {metric}: {value}")
print(f" Pass/fail: {data['pass_fail']}")
AIF360 installation challenges
AIF360 has heavy dependencies (scipy, cvxpy) that can conflict with other packages. If you get installation errors, try: pip install aif360 --no-deps then manually install missing dependencies. The fallback in the code above handles the case where AIF360 isn't installed — the Fairlearn audit from Lesson 3 still works independently.