Measuring fairness is fundamental to detecting and mitigating bias in AI systems. This chapter explores the mathematical foundations of fairness metrics, their applications, limitations, and practical implementation.
Before we can address bias, we must be able to measure it. Fairness metrics provide quantitative ways to assess whether an AI system treats different groups equitably. However, fairness is not a single monolithic concept—there are multiple, sometimes competing definitions of what constitutes fair treatment.
Research has shown that except in trivial cases, it is mathematically impossible to satisfy all fairness criteria simultaneously. This means practitioners must make informed choices about which fairness definitions are most appropriate for their specific context and use case.
Group fairness metrics compare outcomes across different demographic groups. These metrics typically focus on ensuring statistical parity or equal treatment between groups defined by protected attributes.
Demographic parity requires that the proportion of positive predictions is equal across all groups. In other words, the decision should be independent of the protected attribute.
Where Ŷ is the prediction and A is the protected attribute (e.g., race, gender).
# Implementation of Demographic Parity
import numpy as np
def demographic_parity(y_pred, protected_attribute):
"""
Calculate demographic parity metric
Returns ratio of minimum to maximum positive rate
"""
groups = np.unique(protected_attribute)
positive_rates = {}
for group in groups:
mask = protected_attribute == group
positive_rate = np.mean(y_pred[mask])
positive_rates[group] = positive_rate
print(f"Group {group}: {positive_rate:.3f}")
rates = list(positive_rates.values())
parity_ratio = min(rates) / max(rates) if max(rates) > 0 else 1.0
print(f"\nDemographic Parity Ratio: {parity_ratio:.3f}")
print(f"Status: {'PASS' if parity_ratio >= 0.8 else 'FAIL'}")
return parity_ratio, positive_rates
# Example usage
y_pred = np.array([1, 1, 0, 1, 0, 1, 0, 0, 1, 1])
protected = np.array(['A', 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'B'])
ratio, rates = demographic_parity(y_pred, protected)
Advantages:
Limitations:
Equalized odds requires that both true positive rate (TPR) and false positive rate (FPR) are equal across groups. This ensures that the model's errors are distributed equally.
# Implementation of Equalized Odds
def equalized_odds(y_true, y_pred, protected_attribute):
"""
Calculate True Positive Rate and False Positive Rate
for each group to assess equalized odds
"""
groups = np.unique(protected_attribute)
results = {}
for group in groups:
mask = protected_attribute == group
# True Positive Rate
tp = np.sum((y_true[mask] == 1) & (y_pred[mask] == 1))
fn = np.sum((y_true[mask] == 1) & (y_pred[mask] == 0))
tpr = tp / (tp + fn) if (tp + fn) > 0 else 0
# False Positive Rate
fp = np.sum((y_true[mask] == 0) & (y_pred[mask] == 1))
tn = np.sum((y_true[mask] == 0) & (y_pred[mask] == 0))
fpr = fp / (fp + tn) if (fp + tn) > 0 else 0
results[group] = {'tpr': tpr, 'fpr': fpr}
print(f"Group {group}:")
print(f" TPR: {tpr:.3f}, FPR: {fpr:.3f}")
# Check disparities
tprs = [r['tpr'] for r in results.values()]
fprs = [r['fpr'] for r in results.values()]
tpr_disparity = max(tprs) - min(tprs)
fpr_disparity = max(fprs) - min(fprs)
print(f"\nTPR Disparity: {tpr_disparity:.3f}")
print(f"FPR Disparity: {fpr_disparity:.3f}")
print(f"Status: {'PASS' if (tpr_disparity < 0.1 and fpr_disparity < 0.1) else 'FAIL'}")
return results
# Example usage
y_true = np.array([1, 1, 0, 1, 0, 0, 1, 0, 1, 0])
y_pred = np.array([1, 1, 0, 1, 0, 1, 1, 0, 0, 0])
protected = np.array(['A', 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'B'])
results = equalized_odds(y_true, y_pred, protected)
Advantages:
Limitations:
Equal opportunity is a relaxed version of equalized odds that only requires equal true positive rates across groups. This is particularly relevant when false negatives are more harmful than false positives.
# Implementation of Equal Opportunity
def equal_opportunity(y_true, y_pred, protected_attribute):
"""
Calculate True Positive Rate for each group
"""
groups = np.unique(protected_attribute)
tprs = {}
for group in groups:
mask = protected_attribute == group
positives = y_true[mask] == 1
if positives.sum() == 0:
tprs[group] = None
print(f"Group {group}: No positive samples")
continue
tp = np.sum((y_true[mask] == 1) & (y_pred[mask] == 1))
tpr = tp / positives.sum()
tprs[group] = tpr
print(f"Group {group} TPR: {tpr:.3f}")
# Calculate disparity
valid_tprs = [t for t in tprs.values() if t is not None]
if len(valid_tprs) > 1:
disparity = max(valid_tprs) - min(valid_tprs)
print(f"\nTPR Disparity: {disparity:.3f}")
print(f"Status: {'PASS' if disparity < 0.1 else 'FAIL'}")
return tprs
Predictive parity requires that precision (positive predictive value) is equal across groups. This ensures that a positive prediction has the same meaning regardless of group membership.
# Implementation of Predictive Parity
def predictive_parity(y_true, y_pred, protected_attribute):
"""
Calculate Precision (PPV) for each group
"""
groups = np.unique(protected_attribute)
precisions = {}
for group in groups:
mask = protected_attribute == group
predicted_positive = y_pred[mask] == 1
if predicted_positive.sum() == 0:
precisions[group] = None
continue
tp = np.sum((y_true[mask] == 1) & (y_pred[mask] == 1))
precision = tp / predicted_positive.sum()
precisions[group] = precision
print(f"Group {group} Precision: {precision:.3f}")
# Check disparity
valid_prec = [p for p in precisions.values() if p is not None]
if len(valid_prec) > 1:
disparity = max(valid_prec) - min(valid_prec)
print(f"\nPrecision Disparity: {disparity:.3f}")
print(f"Status: {'PASS' if disparity < 0.1 else 'FAIL'}")
return precisions
Individual fairness takes a different approach from group fairness. Instead of comparing groups, it requires that similar individuals receive similar predictions.
The principle of individual fairness states: "Similar individuals should be treated similarly." The challenge lies in defining what constitutes "similarity" in a way that is both meaningful and doesn't encode bias.
# Example: Individual Fairness Check
from sklearn.metrics.pairwise import cosine_similarity
def check_individual_fairness(X, y_pred, threshold=0.9):
"""
Check if similar individuals receive similar predictions
"""
# Calculate pairwise similarity
similarities = cosine_similarity(X)
fairness_violations = 0
total_pairs = 0
for i in range(len(X)):
for j in range(i + 1, len(X)):
if similarities[i, j] > threshold:
total_pairs += 1
if y_pred[i] != y_pred[j]:
fairness_violations += 1
violation_rate = fairness_violations / total_pairs if total_pairs > 0 else 0
print(f"Similar pairs: {total_pairs}")
print(f"Fairness violations: {fairness_violations}")
print(f"Violation rate: {violation_rate:.3f}")
return violation_rate
Disparate impact (also known as adverse impact) is a legal concept used to assess discrimination. The "80% rule" or "four-fifths rule" is commonly used as a threshold.
A ratio below 0.8 (80%) is often considered evidence of disparate impact.
# Disparate Impact Calculation
def disparate_impact(y_pred, protected_attribute, reference_group):
"""
Calculate disparate impact ratio
Reference group is typically the majority or advantaged group
"""
groups = np.unique(protected_attribute)
selection_rates = {}
for group in groups:
mask = protected_attribute == group
selection_rate = np.mean(y_pred[mask])
selection_rates[group] = selection_rate
reference_rate = selection_rates[reference_group]
print("Selection Rates:")
for group, rate in selection_rates.items():
di_ratio = rate / reference_rate if reference_rate > 0 else 0
status = "PASS" if di_ratio >= 0.8 else "FAIL"
print(f" {group}: {rate:.3f} (DI Ratio: {di_ratio:.3f}) [{status}]")
return selection_rates
# Example
y_pred = np.array([1, 1, 1, 1, 0, 1, 0, 0, 1, 0])
protected = np.array(['majority', 'majority', 'majority', 'majority', 'majority',
'protected', 'protected', 'protected', 'protected', 'protected'])
rates = disparate_impact(y_pred, protected, 'majority')
For models that output probability scores rather than hard classifications, calibration is an important fairness consideration.
A model is calibrated if predictions reflect true probabilities. Group calibration requires this to hold separately for each demographic group.
# Check Calibration by Group
def calibration_by_group(y_true, y_score, protected_attribute, n_bins=10):
"""
Assess calibration separately for each group
"""
from sklearn.calibration import calibration_curve
import matplotlib.pyplot as plt
groups = np.unique(protected_attribute)
for group in groups:
mask = protected_attribute == group
prob_true, prob_pred = calibration_curve(
y_true[mask],
y_score[mask],
n_bins=n_bins,
strategy='uniform'
)
print(f"\nGroup {group} Calibration:")
for i, (true_p, pred_p) in enumerate(zip(prob_true, prob_pred)):
print(f" Bin {i+1}: Predicted={pred_p:.3f}, Actual={true_p:.3f}")
return True
Selecting appropriate fairness metrics depends on your application domain, legal requirements, and ethical considerations:
| Application | Recommended Metrics | Rationale |
|---|---|---|
| Criminal Justice | Equalized Odds, Equal Opportunity | Both false positives and false negatives have serious consequences |
| Lending/Credit | Disparate Impact, Demographic Parity | Legal compliance (ECOA), equal access requirements |
| Hiring | Disparate Impact, Equal Opportunity | Legal compliance (EEOC), focus on qualified candidates |
| Healthcare | Equal Opportunity, Calibration | Minimize missed diagnoses, reliable risk scores |
| Content Recommendation | Demographic Parity, Individual Fairness | Equal exposure, personalized treatment |
In practice, you should evaluate multiple fairness metrics to get a complete picture:
# Comprehensive Fairness Report
def fairness_report(y_true, y_pred, protected_attribute):
"""
Generate comprehensive fairness assessment
"""
print("=" * 60)
print("FAIRNESS ASSESSMENT REPORT")
print("=" * 60)
# 1. Demographic Parity
print("\n1. DEMOGRAPHIC PARITY")
print("-" * 60)
dp_ratio, _ = demographic_parity(y_pred, protected_attribute)
# 2. Equalized Odds
print("\n2. EQUALIZED ODDS")
print("-" * 60)
eo_results = equalized_odds(y_true, y_pred, protected_attribute)
# 3. Equal Opportunity
print("\n3. EQUAL OPPORTUNITY")
print("-" * 60)
eop_results = equal_opportunity(y_true, y_pred, protected_attribute)
# 4. Predictive Parity
print("\n4. PREDICTIVE PARITY")
print("-" * 60)
pp_results = predictive_parity(y_true, y_pred, protected_attribute)
# Summary
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
print("Review all metrics above to make informed fairness assessment.")
print("Remember: Different metrics may conflict. Choose based on your")
print("specific use case and stakeholder values.")
print("\n弘益人間 - Ensure your AI benefits all humanity equitably")
print("=" * 60)
return {
'demographic_parity': dp_ratio,
'equalized_odds': eo_results,
'equal_opportunity': eop_results,
'predictive_parity': pp_results
}