Each of 220 people per group has a hidden true label (qualified / not) and a visible model score in [0,1]. A person is accepted if score ≥ that group's threshold. Four outcomes, colour-coded on the dots:
TP green FN red — true qualified, accepted / rejected
FP orange TN slate — true unqualified, accepted / rejected
Three classic fairness criteria are computed live from the confusion counts:
Demographic parity gap = |SelRate_A − SelRate_B|, SelRate = (TP+FP)/N
Equalized odds gap = max(|TPR_A−TPR_B|, |FPR_A−FPR_B|)
TPR = TP/(TP+FN) FPR = FP/(FP+TN)
Calibration gap = |PPV_A − PPV_B|, PPV = TP/(TP+FP)
The impossibility result (Chouldechova 2017; Kleinberg–Mullainathan–Raghavan 2016): whenever the two groups have different base rates — set the "Group B base rate" slider away from Group A's fixed 50% — no single pair of thresholds can drive equalized odds and calibration to zero at the same time, except for a perfect classifier. Try dragging Group B's threshold down to match its selection rate to Group A's (demographic parity ≈ 0): watch the equalized-odds and calibration gaps rise instead of falling together. This is a real, provable mathematical trade-off in binary classification, not a bug in any particular model — it is why lending, hiring and risk-scoring systems must explicitly choose which fairness definition to prioritise.