The Bias Taxonomy
Algorithmic bias is not a single problem — it manifests at multiple stages of the AI lifecycle:
| Bias Type | Where It Originates | Example |
|---|---|---|
| Historical bias | Reflects past discrimination in data | Resume model trained on historical hires where women were underrepresented |
| Representation bias | Underrepresentation of groups in training data | Facial recognition trained on 80% lighter-skinned faces |
| Measurement bias | Proxy variables or flawed measurements | Using zip code as a proxy for creditworthiness (correlated with race) |
| Aggregation bias | One model for all groups when subgroup differences matter | Single diabetes prediction model ignoring ethnic differences in HbA1c levels |
| Evaluation bias | Benchmarks that don't represent all groups | Testing OCR only on English text from Western documents |
| Deployment bias | Model used in contexts it wasn't designed for | Using a sentiment model trained on product reviews for political speech |
Protected Characteristics
Anti-discrimination law protects specific sensitive attributes. AI systems must not discriminate — directly or via proxies — based on:
- EU GDPR / Human Rights: race, ethnicity, gender, age, disability, religion, sexual orientation, national origin
- US Civil Rights: race, color, religion, sex, national origin, age (40+), disability
Proxy discrimination: even without using protected attributes directly, a model can discriminate if it uses correlated features (zip code ↔ race; name ↔ gender; job title ↔ gender).
ISO 42001 — A.7.3 (Bias in Data)
ISO 42001 control A.7.3 requires organizations to:
- Assess data for potential bias and representational gaps
- Document data provenance, collection methods, and known limitations
- Implement bias detection before and during model training
- Maintain records of bias assessments for audit
Fairness Metrics
Let = sensitive attribute (e.g., gender: female, male), = prediction, = true label.
Demographic Parity (Statistical Parity)
The positive prediction rate must be equal across groups. Used when ground truth labels may themselves be biased.
Disparate Impact Ratio (80% rule, EEOC):
A DIR below 0.8 signals potential illegal discrimination in US employment law.
Equal Opportunity
Equal true positive rates — qualified individuals of all groups are equally likely to be identified as qualified. Prioritized when false negatives are the main concern (not hiring a qualified candidate).
Equalized Odds
Both TPR and FPR must be equal across groups. Stronger than equal opportunity. Prioritized in criminal justice (equal false positive rates = equal wrongful conviction rates).
The Fairness-Accuracy Trade-off
Chouldechova (2017) and Kleinberg et al. (2016) proved that when base rates differ between groups, it is mathematically impossible to simultaneously satisfy:
- Demographic parity
- Equal opportunity
- Calibration (predicted probabilities are accurate)
Organizations must explicitly choose which fairness criteria matter most for their use case, document that choice, and accept the trade-offs.
# pip install fairlearn aif360 scikit-learn pandas numpy matplotlib
# ─── 1. Load and Inspect Data for Bias ───────────────────────
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
# Adult Income dataset (classic fairness benchmark)
from sklearn.datasets import fetch_openml
data = fetch_openml("adult", version=2, as_frame=True)
df = data.frame.copy()
# Target: income >50K (1) or <=50K (0)
df['income_binary'] = (df['class'] == '>50K').astype(int)
# Sensitive attribute: sex
df['sex_binary'] = (df['sex'] == 'Male').astype(int) # 1=Male, 0=Female
features = ['age', 'educational-num', 'hours-per-week', 'capital-gain', 'capital-loss']
X = df[features].fillna(0)
y = df['income_binary']
sensitive = df['sex_binary']
X_train, X_test, y_train, y_test, s_train, s_test = train_test_split(
X, y, sensitive, test_size=0.3, random_state=42, stratify=y)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# ─── 2. Train Baseline Model ──────────────────────────────────
model = LogisticRegression(max_iter=1000, random_state=42)
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
y_prob = model.predict_proba(X_test_scaled)[:, 1]
# ─── 3. Compute Fairness Metrics ─────────────────────────────
from fairlearn.metrics import (
MetricFrame,
demographic_parity_difference,
demographic_parity_ratio,
equalized_odds_difference,
selection_rate,
true_positive_rate,
false_positive_rate,
)
from sklearn.metrics import accuracy_score, precision_score, recall_score
metrics = {
"accuracy": accuracy_score,
"precision": precision_score,
"recall": recall_score,
"selection_rate": selection_rate,
"true_positive_rate": true_positive_rate,
"false_positive_rate": false_positive_rate,
}
mf = MetricFrame(
metrics=metrics,
y_true=y_test,
y_pred=y_pred,
sensitive_features=s_test,
)
print("Fairness Metrics by Gender:")
print(mf.by_group.to_string())
print(f"\nDemographic Parity Difference: {demographic_parity_difference(y_test, y_pred, sensitive_features=s_test):.4f}")
print(f"Demographic Parity Ratio: {demographic_parity_ratio(y_test, y_pred, sensitive_features=s_test):.4f}")
print(f"Equalized Odds Difference: {equalized_odds_difference(y_test, y_pred, sensitive_features=s_test):.4f}")
# Check 80% rule (EEOC Disparate Impact)
sel_male = y_pred[s_test == 1].mean()
sel_female = y_pred[s_test == 0].mean()
dir_ratio = sel_female / sel_male
print(f"\nSelection rates: Male={sel_male:.3f}, Female={sel_female:.3f}")
print(f"Disparate Impact Ratio: {dir_ratio:.3f} {'(PASS ≥0.8)' if dir_ratio >= 0.8 else '(FAIL <0.8 — potential discrimination)'}")
# ─── 4. Pre-processing Mitigation: Reweighting ────────────────
from aif360.datasets import BinaryLabelDataset
from aif360.algorithms.preprocessing import Reweighing
# Prepare AIF360 dataset
df_aif = pd.DataFrame(X_train, columns=features)
df_aif['income'] = y_train.values
df_aif['sex'] = s_train.values
aif_dataset = BinaryLabelDataset(
df=df_aif,
label_names=['income'],
protected_attribute_names=['sex'],
)
# Reweighing: assign instance weights to equalize representation
RW = Reweighing(unprivileged_groups=[{'sex': 0}],
privileged_groups=[{'sex': 1}])
RW.fit(aif_dataset)
rw_dataset = RW.transform(aif_dataset)
sample_weights = rw_dataset.instance_weights
model_rw = LogisticRegression(max_iter=1000, random_state=42)
model_rw.fit(X_train_scaled, y_train, sample_weight=sample_weights)
y_pred_rw = model_rw.predict(X_test_scaled)
print("\nAfter Reweighing:")
dpd_rw = demographic_parity_difference(y_test, y_pred_rw, sensitive_features=s_test)
print(f" Demographic Parity Difference: {dpd_rw:.4f} (was {demographic_parity_difference(y_test, y_pred, sensitive_features=s_test):.4f})")
print(f" Accuracy: {accuracy_score(y_test, y_pred_rw):.4f} (was {accuracy_score(y_test, y_pred):.4f})")
# ─── 5. Post-processing Mitigation: Threshold Optimization ────
from fairlearn.postprocessing import ThresholdOptimizer
from fairlearn.reductions import ExponentiatedGradient, DemographicParity
# Exponentiated Gradient (in-processing): fair constraint during training
constraint = DemographicParity()
mitigator = ExponentiatedGradient(
LogisticRegression(max_iter=1000),
constraints=constraint,
)
mitigator.fit(X_train_scaled, y_train, sensitive_features=s_train)
y_pred_eg = mitigator.predict(X_test_scaled)
print("\nAfter Exponentiated Gradient (DemographicParity constraint):")
dpd_eg = demographic_parity_difference(y_test, y_pred_eg, sensitive_features=s_test)
print(f" Demographic Parity Difference: {dpd_eg:.4f}")
print(f" Accuracy: {accuracy_score(y_test, y_pred_eg):.4f}")
# ─── 6. Fairness Report (ISO 42001 A.6.2.3 evidence) ─────────
print("\n" + "="*60)
print("FAIRNESS AUDIT REPORT — ISO 42001 Control A.6.2.3")
print("="*60)
print(f"Model: Logistic Regression | Dataset: Adult Income (UCI)")
print(f"Sensitive Attribute: sex (Male=1, Female=0)")
print(f"\nBefore mitigation:")
print(f" Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(f" DPD: {demographic_parity_difference(y_test, y_pred, sensitive_features=s_test):.4f}")
print(f" DIR: {dir_ratio:.4f} {'PASS' if dir_ratio >= 0.8 else 'FAIL (requires mitigation)'}")
print(f"\nAfter mitigation (Exponentiated Gradient):")
print(f" Accuracy: {accuracy_score(y_test, y_pred_eg):.4f}")
print(f" DPD: {dpd_eg:.4f}")
print(f"\nRecommendation: {'Continue to monitoring' if abs(dpd_eg) < 0.05 else 'Further review required'}")Mitigation Strategies
Pre-processing (Fix the Data)
| Technique | How It Works | Pros | Cons |
|---|---|---|---|
| Resampling | Oversample minority group; undersample majority | Simple | May lose data; may overfit |
| Reweighting | Assign higher loss weights to underrepresented groups | Preserves data | Weight selection can be tricky |
| Data augmentation | Generate synthetic samples for underrepresented groups | More data | Quality of synthetic data |
| Label correction | Correct known label biases using domain knowledge | Directly addresses root cause | Requires expert knowledge |
In-processing (Fix the Model)
| Technique | How It Works |
|---|---|
| Fairness constraints | Add fairness as an optimization constraint (Exponentiated Gradient) |
| Adversarial debiasing | Jointly train predictor + adversary that tries to predict sensitive attribute from representations |
| Fair representation learning | Learn embeddings that are invariant to sensitive attributes |
| Meta-fair algorithm | Directly optimize a chosen fairness metric |
Post-processing (Fix the Outputs)
| Technique | How It Works |
|---|---|
| Threshold calibration | Set different decision thresholds per group to equalize FPR/TPR |
| Reject option classification | Human review for borderline predictions near the decision boundary |
| Calibrated equal odds | Platt-scale per group then adjust thresholds |
Implementation Checklist (ISO 42001 A.7.3 / A.6.2.3)
- Document all sensitive attributes relevant to the use case
- Compute group-level metrics (selection rate, TPR, FPR) before deployment
- Calculate Disparate Impact Ratio — flag if < 0.8
- Apply appropriate mitigation technique(s)
- Re-evaluate metrics after mitigation and document trade-offs
- Set up automated fairness monitoring in production
- Establish appeals/redress process for affected individuals
Knowledge check
A loan approval model achieves 92% accuracy overall, but approves 78% of majority-group applicants who qualify versus only 61% of minority-group applicants who qualify. Which fairness metric is violated?
Summary
- Bias has six root causes: historical, representation, measurement, aggregation, evaluation, and deployment bias — each requiring different interventions
- Fairness definitions are incompatible: demographic parity, equal opportunity, and equalized odds cannot all be satisfied simultaneously when base rates differ
- Disparate Impact Ratio < 0.8 triggers legal concern under US employment law; document DIR for every high-stakes model
- Mitigation strategies span the full pipeline: reweighting data (pre-processing), fairness constraints during training (in-processing), and threshold calibration (post-processing)
- ISO 42001 A.7.3 mandates bias assessment in training data; A.6.2.3 requires fairness testing throughout the lifecycle
- Fairness monitoring must continue in production — distribution shifts can introduce new biases after deployment
Next: Transparency & Explainability — making AI decisions understandable with LIME, SHAP, and model cards.