Same generative model as the 3D version — 220 people per group, each with a hidden true label and a visible score in [0,1] — but drawn as two flat 2D panels instead of a rotating point cloud:
Top: population pyramid. Group A's score histogram grows upward, Group B's grows downward from the centre line; each bar is stacked by outcome (TP green / FN red / FP orange / TN slate) so the accept/reject split at each score band is visible at a glance, with the two threshold cut-lines drawn as dashed verticals.
Bottom: threshold parameter-space heatmaps. Three small maps plot every possible (Group A threshold, Group B threshold) pair on x/y axes; the colour at each point is that pair's demographic-parity, equalized-odds, or calibration gap, computed directly from cumulative counts of the two fixed populations (no resampling needed to sweep it). The crosshair marks your current slider position on all three at once — drag directly on any heatmap to move both sliders.
SelRate = (TP+FP)/N DP gap = |SelRate_A − SelRate_B|
TPR = TP/(TP+FN) FPR = FP/(FP+TN) EO gap = max(|ΔTPR|, |ΔFPR|)
PPV = TP/(TP+FP) Cal gap = |PPV_A − PPV_B|
The impossibility result (Chouldechova 2017): because the heatmaps are drawn once per population sample, drag the crosshair into the green zone of the Equalized-odds map and watch the Calibration map turn red at that same spot, and vice versa — whenever the base-rate slider moves the two populations apart, no single crosshair position sits in the green zone of both at once, except in the degenerate case of a perfect classifier. (Demographic parity is a separate, weaker constraint — in this model its green zone often does overlap Equalized odds', which is exactly why regulators and practitioners must be explicit about which of the three metrics they are optimising for.)