Fairness testing
Skills framework for coding agents that enforces responsible AI practices — bias assessment, fairness testing, explainability, governance documentation, and alignment review. Auto-activates when building AI systems.
npx -y skills add obielin/responsible-ai-skills --skill fairness-testingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when writing tests for any ML model, classifier, or AI system that produces outputs affecting people. Fairness tests must be written before the model is used in production.
SKILL.md
8.0 KB, as published. Nobody here has run it
Fairness Testing
Fairness is not a property you check once — it's a property you test continuously. These tests run in CI and fail the build if the model regresses on fairness metrics.
The RED-GREEN-REFACTOR Cycle for Fairness
RED: Write a fairness test. Run it. Watch it fail (or confirm the threshold matters). GREEN: Adjust the model or data until the test passes. REFACTOR: Clean up. Do not delete the test.
This is non-negotiable. If you write no fairness tests, you have no fairness guarantees.
Test Structure
Every fairness test follows this pattern:
def test_<metric>_parity_across_<attribute>(model, test_data):
"""
<metric> must not differ by more than <threshold> across <attribute> groups.
Regulatory basis: Equality Act 2010 s.149 (PSED) / EU AI Act Art. 10
"""
results = {}
for group in test_data[attribute].unique():
mask = test_data[attribute] == group
results[group] = compute_metric(
model, test_data[mask]
)
max_gap = max(results.values()) - min(results.values())
assert max_gap <= THRESHOLD, (
f"{metric} gap across {attribute} is {max_gap:.3f} "
f"(max allowed: {THRESHOLD}). "
f"Group breakdown: {results}"
)
Required Test Suite
Write ALL of these. Skip none.
Test 1: Demographic Parity
import pytest
import numpy as np
DEMOGRAPHIC_PARITY_THRESHOLD = 0.05
def test_demographic_parity(model, test_df):
"""Positive prediction rate must not vary by more than 5% across protected groups."""
protected_attrs = ['sex', 'ethnicity', 'age_group'] # adjust to your attributes
for attr in protected_attrs:
if attr not in test_df.columns:
continue
rates = {}
for group in test_df[attr].unique():
mask = test_df[attr] == group
if mask.sum() < 30: # skip groups too small to be meaningful
continue
X = test_df[mask].drop(columns=['label'])
rates[group] = model.predict(X).mean()
if len(rates) < 2:
continue
gap = max(rates.values()) - min(rates.values())
assert gap <= DEMOGRAPHIC_PARITY_THRESHOLD, (
f"Demographic parity violation on '{attr}': gap={gap:.3f} "
f"(threshold={DEMOGRAPHIC_PARITY_THRESHOLD}). "
f"Rates: {rates}"
)
Test 2: Equal Opportunity (True Positive Rate Parity)
from sklearn.metrics import recall_score
EQUAL_OPPORTUNITY_THRESHOLD = 0.05
def test_equal_opportunity(model, test_df):
"""True positive rate must not vary by more than 5% across protected groups."""
for attr in ['sex', 'ethnicity', 'age_group']:
if attr not in test_df.columns:
continue
tprs = {}
for group in test_df[attr].unique():
mask = (test_df[attr] == group) & (test_df['label'] == 1)
if mask.sum() < 20:
continue
X = test_df[mask].drop(columns=['label'])
y_true = test_df[mask]['label']
y_pred = model.predict(X)
tprs[group] = recall_score(y_true, y_pred, zero_division=0)
if len(tprs) < 2:
continue
gap = max(tprs.values()) - min(tprs.values())
assert gap <= EQUAL_OPPORTUNITY_THRESHOLD, (
f"Equal opportunity violation on '{attr}': gap={gap:.3f}. TPRs: {tprs}"
)
Test 3: False Positive Rate Parity
from sklearn.metrics import confusion_matrix
FPR_THRESHOLD = 0.05
def test_false_positive_rate_parity(model, test_df):
"""False positive rate must not vary by more than 5% across groups.
A higher FPR for one group means they are MORE LIKELY to be incorrectly
flagged — a critical fairness concern in benefits, criminal justice, and hiring.
"""
for attr in ['sex', 'ethnicity', 'age_group']:
if attr not in test_df.columns:
continue
fprs = {}
for group in test_df[attr].unique():
mask = test_df[attr] == group
if mask.sum() < 30:
continue
X = test_df[mask].drop(columns=['label'])
y_true = test_df[mask]['label']
y_pred = model.predict(X)
tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0, 1]).ravel()
fprs[group] = fp / (fp + tn) if (fp + tn) > 0 else 0
if len(fprs) < 2:
continue
gap = max(fprs.values()) - min(fprs.values())
assert gap <= FPR_THRESHOLD, (
f"FPR parity violation on '{attr}': gap={gap:.3f}. FPRs: {fprs}"
)
Test 4: Performance Does Not Degrade for Minority Groups
from sklearn.metrics import f1_score
MIN_F1_THRESHOLD = 0.70 # adjust for your use case
MIN_GROUP_SIZE = 30
def test_minority_group_performance(model, test_df):
"""No group should fall below minimum acceptable F1 score.
A model that is 95% accurate overall but 40% accurate for a minority group
is not an acceptable model.
"""
for attr in ['sex', 'ethnicity', 'age_group']:
if attr not in test_df.columns:
continue
for group in test_df[attr].unique():
mask = test_df[attr] == group
if mask.sum() < MIN_GROUP_SIZE:
continue
X = test_df[mask].drop(columns=['label'])
y_true = test_df[mask]['label']
y_pred = model.predict(X)
f1 = f1_score(y_true, y_pred, average='weighted', zero_division=0)
assert f1 >= MIN_F1_THRESHOLD, (
f"Performance below threshold for {attr}='{group}': "
f"F1={f1:.3f} (min={MIN_F1_THRESHOLD}). "
f"Group size: {mask.sum()}"
)
Test 5: Fairness Regression Guard
import json
from pathlib import Path
BASELINE_FILE = "tests/fairness_baseline.json"
def test_fairness_does_not_regress(model, test_df):
"""Current fairness metrics must not be worse than the recorded baseline.
Run `python skills/fairness-testing/scripts/record_baseline.py` to update
the baseline after intentional model changes.
"""
if not Path(BASELINE_FILE).exists():
pytest.skip("No fairness baseline recorded. Run record_baseline.py first.")
baseline = json.loads(Path(BASELINE_FILE).read_text())
for attr, metrics in baseline.items():
if attr not in test_df.columns:
continue
for metric_name, baseline_value in metrics.items():
current = compute_fairness_metric(model, test_df, attr, metric_name)
# Allow 2% degradation tolerance
assert current >= baseline_value - 0.02, (
f"Fairness regression: {metric_name} on '{attr}' "
f"dropped from {baseline_value:.3f} to {current:.3f}"
)
CI Integration
Add to your CI pipeline (pytest.ini or pyproject.toml):
[pytest]
markers =
fairness: marks tests as fairness tests (run with -m fairness)
Tag all fairness tests:
@pytest.mark.fairness
def test_demographic_parity(...):
...
Run fairness tests in CI:
- name: Run fairness tests
run: pytest -m fairness --tb=short
Fairness test failures are build failures. Do not merge code that fails fairness tests.
Completion Checklist
- Demographic parity test written and passing
- Equal opportunity test written and passing
- False positive rate parity test written and passing
- Minority group performance test written and passing
- Fairness regression guard set up with baseline recorded
- Fairness tests integrated into CI pipeline
- Tests are marked with
@pytest.mark.fairness
You may now proceed. Run alignment-review before marking the feature complete.