Most bias in AI systems originates from biased training data. This chapter covers techniques for detecting bias in datasets before they poison your models. 弘益人間 - Clean data leads to fair AI.
Data bias is systematic error in the data collection, sampling, or labeling process that results in misrepresentation of the true population. Unlike random noise, data bias creates consistent patterns that ML models will learn and amplify.
Selection bias occurs when the data collection process systematically excludes or underrepresents certain groups. This is one of the most common and pernicious forms of data bias.
# Detect Selection Bias
import pandas as pd
import numpy as np
def detect_selection_bias(df, population_stats, protected_attr):
"""
Compare dataset distribution to known population statistics
"""
dataset_dist = df[protected_attr].value_counts(normalize=True)
print("Selection Bias Analysis")
print("=" * 60)
print(f"\n{'Group':<20} {'Dataset %':<15} {'Population %':<15} {'Bias'}")
print("-" * 60)
for group in population_stats.keys():
dataset_pct = dataset_dist.get(group, 0) * 100
population_pct = population_stats[group] * 100
bias = dataset_pct - population_pct
status = "⚠️" if abs(bias) > 5 else "✓"
print(f"{group:<20} {dataset_pct:<15.2f} {population_pct:<15.2f} {bias:+.2f}% {status}")
return dataset_dist
# Example
population = {'Group_A': 0.60, 'Group_B': 0.30, 'Group_C': 0.10}
# detect_selection_bias(dataset, population, 'demographic')
Measurement bias arises when features are systematically measured differently across groups or when measurement tools have different accuracy for different populations.
# Detect Measurement Bias
def analyze_measurement_quality(df, feature, protected_attr):
"""
Analyze measurement consistency across groups
"""
groups = df[protected_attr].unique()
print(f"Measurement Quality Analysis: {feature}")
print("=" * 60)
for group in groups:
group_data = df[df[protected_attr] == group][feature]
# Check for patterns suggesting measurement issues
missing_rate = group_data.isna().sum() / len(group_data)
outlier_rate = len(group_data[np.abs(group_data - group_data.mean()) > 3*group_data.std()]) / len(group_data)
print(f"\n{group}:")
print(f" Missing rate: {missing_rate:.3f}")
print(f" Outlier rate: {outlier_rate:.3f}")
print(f" Mean: {group_data.mean():.3f}")
print(f" Std: {group_data.std():.3f}")
if missing_rate > 0.05:
print(f" ⚠️ High missing rate may indicate measurement bias")
Label bias occurs when the labeling process is inconsistent or biased. This is particularly problematic in supervised learning where labels are the ground truth.
# Detect Label Bias
def detect_label_bias(df, label_col, protected_attr, feature_cols):
"""
Look for cases where labels differ despite similar features
"""
from sklearn.neighbors import NearestNeighbors
groups = df[protected_attr].unique()
# Find similar instances across groups
X = df[feature_cols].values
nbrs = NearestNeighbors(n_neighbors=5).fit(X)
disagreements = 0
total_pairs = 0
for i in range(len(df)):
distances, indices = nbrs.kneighbors([X[i]])
for idx in indices[0][1:]: # Skip self
if df.iloc[i][protected_attr] != df.iloc[idx][protected_attr]:
total_pairs += 1
if df.iloc[i][label_col] != df.iloc[idx][label_col]:
disagreements += 1
disagreement_rate = disagreements / total_pairs if total_pairs > 0 else 0
print(f"Label disagreement rate for similar cross-group pairs: {disagreement_rate:.3f}")
if disagreement_rate > 0.3:
print("⚠️ High disagreement suggests potential label bias")
return disagreement_rate
Comprehensive analysis of how data is distributed across protected groups.
# Comprehensive Dataset Audit
def audit_dataset(df, protected_attributes, target):
"""
Perform comprehensive bias audit of dataset
"""
print("DATASET BIAS AUDIT REPORT")
print("=" * 70)
# 1. Sample Size Analysis
print("\n1. SAMPLE SIZE BY GROUP")
print("-" * 70)
for attr in protected_attributes:
counts = df[attr].value_counts()
print(f"\n{attr}:")
for group, count in counts.items():
pct = count / len(df) * 100
warning = " ⚠️ UNDERREPRESENTED" if pct < 10 else ""
print(f" {group}: {count} ({pct:.2f}%){warning}")
# 2. Label Distribution by Group
print("\n2. LABEL DISTRIBUTION BY GROUP")
print("-" * 70)
for attr in protected_attributes:
print(f"\n{attr}:")
cross_tab = pd.crosstab(df[attr], df[target], normalize='index') * 100
print(cross_tab.round(2))
# Check for significant disparities
positive_rates = cross_tab[1] if 1 in cross_tab.columns else cross_tab.iloc[:, 1]
if positive_rates.max() - positive_rates.min() > 20:
print(" ⚠️ Significant label imbalance detected across groups")
# 3. Missing Data Analysis
print("\n3. MISSING DATA BY GROUP")
print("-" * 70)
for attr in protected_attributes:
print(f"\n{attr}:")
for group in df[attr].unique():
group_df = df[df[attr] == group]
missing_pct = (group_df.isna().sum() / len(group_df) * 100).mean()
print(f" {group}: {missing_pct:.2f}% average missing")
# 4. Feature Correlation with Protected Attributes
print("\n4. FEATURE CORRELATION WITH PROTECTED ATTRIBUTES")
print("-" * 70)
numeric_cols = df.select_dtypes(include=[np.number]).columns
for attr in protected_attributes:
if attr in numeric_cols:
continue
print(f"\n{attr}:")
# One-hot encode for correlation
encoded = pd.get_dummies(df[attr], prefix=attr)
for col in numeric_cols:
if col != target:
corr = df[col].corr(encoded.iloc[:, 0])
if abs(corr) > 0.3:
print(f" {col}: {corr:.3f} ⚠️ HIGH CORRELATION")
print("\n" + "=" * 70)
print("AUDIT COMPLETE")
print("Review warnings above for potential bias issues")
print("=" * 70)
Even when protected attributes are excluded, proxy features can enable discrimination.
# Detect Proxy Features
def detect_proxy_features(df, protected_attr, feature_cols, threshold=0.5):
"""
Identify features that may serve as proxies for protected attributes
"""
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
print("PROXY FEATURE DETECTION")
print("=" * 60)
X = df[feature_cols]
y = df[protected_attr]
# Train model to predict protected attribute from features
clf = RandomForestClassifier(n_estimators=100, random_state=42)
scores = cross_val_score(clf, X, y, cv=5)
mean_score = scores.mean()
print(f"Can predict {protected_attr} from features: {mean_score:.3f} accuracy")
if mean_score > threshold:
print("⚠️ WARNING: Features may contain proxies for protected attribute")
# Get feature importance
clf.fit(X, y)
importance = pd.DataFrame({
'feature': feature_cols,
'importance': clf.feature_importances_
}).sort_values('importance', ascending=False)
print("\nMost predictive features (potential proxies):")
print(importance.head(10))
return mean_score
# Example usage
# detect_proxy_features(df, 'race', ['zipcode', 'income', 'education'])
Data collected over time may reflect changing biases or become outdated.
# Temporal Bias Analysis
def analyze_temporal_bias(df, timestamp_col, protected_attr, target):
"""
Analyze how bias evolves over time
"""
df['year'] = pd.to_datetime(df[timestamp_col]).dt.year
print("TEMPORAL BIAS ANALYSIS")
print("=" * 60)
for group in df[protected_attr].unique():
group_df = df[df[protected_attr] == group]
yearly_stats = group_df.groupby('year').agg({
target: ['mean', 'count']
}).round(3)
print(f"\n{group}:")
print(yearly_stats)
# Check for trends
print("\n⚠️ Look for:")
print(" - Changing label distributions over time")
print(" - Varying representation across years")
print(" - Sudden shifts that may indicate data drift")
Bias often compounds across multiple protected attributes. Intersectional analysis examines combinations of attributes.
# Intersectional Bias Analysis
def intersectional_analysis(df, protected_attrs, target):
"""
Analyze bias across intersections of multiple protected attributes
"""
print("INTERSECTIONAL BIAS ANALYSIS")
print("=" * 70)
# Create intersection groups
df['intersection'] = df[protected_attrs].apply(
lambda x: '_'.join(x.astype(str)), axis=1
)
results = df.groupby('intersection').agg({
target: ['mean', 'count']
}).round(3)
results.columns = ['positive_rate', 'count']
results = results.sort_values('positive_rate')
print(results)
# Identify most disadvantaged intersections
min_rate = results['positive_rate'].min()
max_rate = results['positive_rate'].max()
disparity = max_rate - min_rate
print(f"\nPositive rate range: {min_rate:.3f} to {max_rate:.3f}")
print(f"Disparity: {disparity:.3f}")
if disparity > 0.2:
print("⚠️ Significant intersectional bias detected")
print("\nMost disadvantaged groups:")
print(results.head(3))
return results
# Generate Comprehensive Data Bias Report
def generate_data_bias_report(df, protected_attrs, target, feature_cols):
"""
Generate comprehensive data bias assessment report
"""
report = {
'timestamp': pd.Timestamp.now(),
'total_samples': len(df),
'issues': []
}
print("\n" + "=" * 70)
print("DATA BIAS DETECTION REPORT")
print("=" * 70)
print(f"Generated: {report['timestamp']}")
print(f"Total Samples: {report['total_samples']}")
# Run all checks
audit_dataset(df, protected_attrs, target)
for attr in protected_attrs:
proxy_score = detect_proxy_features(df, attr, feature_cols)
if proxy_score > 0.5:
report['issues'].append(f"Proxy features detected for {attr}")
# Intersectional analysis
if len(protected_attrs) > 1:
intersectional_analysis(df, protected_attrs, target)
print("\n" + "=" * 70)
print("RECOMMENDATIONS")
print("=" * 70)
print("1. Address underrepresented groups through additional data collection")
print("2. Review and standardize labeling procedures")
print("3. Investigate high-correlation features for proxy discrimination")
print("4. Consider stratified sampling for future data collection")
print("5. Implement continuous monitoring of data quality")
print("\n弘익人間 - Fair data is the foundation of fair AI")
print("=" * 70)
return report