Fairness

Fairness asks whether an AI system creates unjustified differences in benefits, burdens, or errors across affected groups. In model work it is measured with explicit group metrics; in governance it is tied to compliance, appeal routes, and human oversight because a metric gap alone does not decide what outcome is justified.

Group fairness metrics

For binary label , prediction , and protected attribute , demographic parity requires equal selection rates:

Equal opportunity requires equal true-positive rates among people with :

Equalized odds is stricter: prediction must be conditionally independent of given , which in binary classification means both true-positive and false-positive rates match:

These are diagnostic tests, not moral axioms. Demographic parity ignores labels and can hide quality-of-service failures; equalized odds depends on whether the label is a valid proxy for the real construct. That is why a fairness review should also inspect explainability, segment-specific error taxonomies, and calibration.

Executed metric check

This snippet computes group selection rates, true-positive rates, false-positive rates, and fairness gaps before and after a group-adjusted threshold.

import numpy as np
 
y_true = np.array([1,1,1,1,1,1,0,0,0,0,0,0, 1,1,1,1,1,1,0,0,0,0,0,0])
group = np.array(["A"] * 12 + ["B"] * 12)
y_pred_base = np.array([1,1,1,1,1,0,0,0,0,0,1,0, 1,1,1,0,0,0,1,1,0,0,0,0])
y_pred_equalized = np.array([1,1,1,0,0,0,0,0,0,0,1,1, 1,1,1,0,0,0,1,1,0,0,0,0])
 
def rates(y, pred, g):
    out = {}
    for value in sorted(set(g)):
        m = g == value
        tp = int(((pred[m] == 1) & (y[m] == 1)).sum())
        fn = int(((pred[m] == 0) & (y[m] == 1)).sum())
        fp = int(((pred[m] == 1) & (y[m] == 0)).sum())
        tn = int(((pred[m] == 0) & (y[m] == 0)).sum())
        out[value] = {
            "selection": float(pred[m].mean()),
            "tpr": tp / (tp + fn),
            "fpr": fp / (fp + tn),
            "confusion": (tp, fn, fp, tn),
        }
    dp = abs(out["A"]["selection"] - out["B"]["selection"])
    tpr_gap = abs(out["A"]["tpr"] - out["B"]["tpr"])
    fpr_gap = abs(out["A"]["fpr"] - out["B"]["fpr"])
    return out, dp, tpr_gap, fpr_gap
 
for name, pred in [("base_threshold", y_pred_base), ("group_adjusted", y_pred_equalized)]:
    out, dp, tpr_gap, fpr_gap = rates(y_true, pred, group)
    print(name)
    for value in ["A", "B"]:
        vals = out[value]
        print(value, "selection", round(vals["selection"], 3), "TPR", round(vals["tpr"], 3), "FPR", round(vals["fpr"], 3), "confusion(tp,fn,fp,tn)", vals["confusion"])
    print("demographic_parity_diff", round(dp, 3), "equal_opportunity_diff", round(tpr_gap, 3), "equalized_odds_diff", round(max(tpr_gap, fpr_gap), 3))

Observed output:

base_threshold
A selection 0.5 TPR 0.833 FPR 0.167 confusion(tp,fn,fp,tn) (5, 1, 1, 5)
B selection 0.417 TPR 0.5 FPR 0.333 confusion(tp,fn,fp,tn) (3, 3, 2, 4)
demographic_parity_diff 0.083 equal_opportunity_diff 0.333 equalized_odds_diff 0.333
group_adjusted
A selection 0.417 TPR 0.5 FPR 0.333 confusion(tp,fn,fp,tn) (3, 3, 2, 4)
B selection 0.417 TPR 0.5 FPR 0.333 confusion(tp,fn,fp,tn) (3, 3, 2, 4)
demographic_parity_diff 0.0 equal_opportunity_diff 0.0 equalized_odds_diff 0.0

The group-adjusted predictions equalize the measured rates, but they do it by lowering group A’s true-positive rate from 0.833 to 0.5 and raising its false-positive rate from 0.167 to 0.333. This is the practical fairness trade-off: a dashboard should show both the disparity reduction and the lost utility, using ordinary evaluation metrics beside the fairness metrics.

Review controls

A useful fairness artifact records the protected or proxy groups evaluated, the construct the label is supposed to measure, base rates, selection rates, TPR/FPR/FNR by group, sample sizes, confidence intervals where possible, the chosen intervention, and residual risk. For high-impact systems, auditability should preserve the exact model, threshold, dataset slice, and approval that produced the review.

References