Dictionary of Applied Machine Learning · fairness (machine learning)
Numerical companion to the entry fairness (machine learning): it recomputes what the entry states and prints one line per check
The entry's Fig. 1 shows the counterfactual fairness criterion in a credit-scoring example: a learned hypothesis h approves a loan when the predicted creditworthiness h(x) exceeds the threshold tau = 3.8, and the same applicant obtains a different decision once only the gender is flipped. The figure is a cartoon drawn from the closed form h(x) = 1 + 3.5 (1 - exp(-0.35 x)) over a one-dimensional sketch of the feature vector; this demo checks the numbers the entry states about it. Self-contained (numpy/matplotlib only), no randomness.
Run it without installing anything:uv run https://dictionaryofml.org/terms/fairness.py
uv downloads this script and the pinned NumPy and Matplotlib it needs, then runs it; the script fetches any input file it uses. To keep the output files, download fairness.py into a folder and run uv run fairness.py there. With NumPy and Matplotlib already installed, python3 fairness.py, from any directory — it writes its output files into the current directory. Fixed seeds, so the printed numbers reproduce exactly. Download fairness.py · Notebook · Open in Colab
One cell per block of the script: the code, and what that code printed when it last ran here
"""
fairness.py — numerical companion to the glossary entry 'fairness'
(fairness in machine learning).
The entry's Fig. 1 shows the counterfactual fairness criterion in a
credit-scoring example: a learned hypothesis h approves a loan when the
predicted creditworthiness h(x) exceeds the threshold tau = 3.8, and the
same applicant obtains a different decision once only the gender is
flipped. The figure is a cartoon drawn from the closed form
h(x) = 1 + 3.5 (1 - exp(-0.35 x)) over a one-dimensional sketch of the
feature vector; this demo checks the numbers the entry states about it.
Self-contained (numpy/matplotlib only), no randomness.
Blocks
------
[B-flip] The applicant x' = 2.3 is rejected, h(x') = 2.94; the same
applicant with the gender flipped, x* = 5.2, is approved,
h(x*) = 3.93; the gap is the stated 0.99. The decision
depends on the gender alone: the counterfactual criterion
fails.
[B-individual] Under a metric that ignores the gender, x' and x* are the
same data point at distance zero, yet their predictions lie
on opposite sides of the threshold: the individual
criterion fails under that metric.
[B-model] Excluding the gender from the model makes the two feature
vectors coincide, so the gap vanishes; the prediction for
the applicant is then unchanged by the flip.
Outputs
-------
fairness.png : preview figure (checking only); Fig. 1 of the entry is
drawn from the same closed form in TikZ.
"""
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from pathlib import Path
OUT_DIR = Path(__file__).parent
report = []
def check(name, ok):
report.append((name, bool(ok)))
print(f" [{'ok' if ok else 'FAIL'}] {name}")
def h(x):
"""The drawn hypothesis: predicted creditworthiness."""
return 1 + 3.5 * (1 - np.exp(-0.35 * np.asarray(x, dtype=float)))
TAU = 3.8 # approval threshold
X_PRIME, X_STAR = 2.3, 5.2 # the applicant, the gender flipped
LOANS_X = np.array([0.7, 1.8, 3.4, 4.9, 6.4]) # the training set
LOANS_Y = np.array([1.5, 2.4, 3.7, 3.7, 4.6])
The applicant x' = 2.3 is rejected, h(x') = 2.94; the same applicant with the gender flipped, x* = 5.2, is approved, h(x*) = 3.93; the gap is the stated 0.99. The decision depends on the gender alone: the counterfactual criterion fails.
print("[B-flip] the counterfactual criterion: the same applicant, other gender")
gap = h(X_STAR) - h(X_PRIME)
print(f" h(x') = {h(X_PRIME):.2f} < tau = {TAU} (rejected); "
f"h(x*) = {h(X_STAR):.2f} >= tau (approved); gap {gap:.3f}")
check("[B-flip] h(x') = 2.94 and h(x*) = 3.93 as the entry states",
abs(h(X_PRIME) - 2.94) < 0.005 and abs(h(X_STAR) - 3.93) < 0.005)
check("[B-flip] the applicant is rejected and the gender-flipped twin approved",
h(X_PRIME) < TAU <= h(X_STAR))
check("[B-flip] the gap is the stated 0.99 (the difference of the rounded "
"predictions, within 0.01)",
abs(round(h(X_STAR), 2) - round(h(X_PRIME), 2) - 0.99) < 1e-9
and abs(gap - 0.99) < 0.01)
[B-flip] the counterfactual criterion: the same applicant, other gender
h(x') = 2.94 < tau = 3.8 (rejected); h(x*) = 3.93 >= tau (approved); gap 0.998
[ok] [B-flip] h(x') = 2.94 and h(x*) = 3.93 as the entry states
[ok] [B-flip] the applicant is rejected and the gender-flipped twin approved
[ok] [B-flip] the gap is the stated 0.99 (the difference of the rounded predictions, within 0.01)
Under a metric that ignores the gender, x' and x* are the same data point at distance zero, yet their predictions lie on opposite sides of the threshold: the individual criterion fails under that metric.
print("[B-individual] a metric that ignores the gender")
# The feature vector is the pair (income, gender); the sketch collapses it
# to one coordinate. Under a metric that ignores the gender, the applicant
# and the gender-flipped twin have the same income and are at distance 0.
income = 2.3
applicant = (income, 0)
twin = (income, 1)
distance_ignoring_gender = abs(applicant[0] - twin[0])
print(f" distance between x' and x* under the metric: "
f"{distance_ignoring_gender}; predictions {h(X_PRIME):.2f} and "
f"{h(X_STAR):.2f}")
check("[B-individual] x' and x* are at distance zero under the metric",
distance_ignoring_gender == 0)
check("[B-individual] yet their predictions lie on opposite sides of the "
"threshold: the individual criterion fails",
(h(X_PRIME) < TAU) != (h(X_STAR) < TAU))
[B-individual] a metric that ignores the gender
distance between x' and x* under the metric: 0.0; predictions 2.94 and 3.93
[ok] [B-individual] x' and x* are at distance zero under the metric
[ok] [B-individual] yet their predictions lie on opposite sides of the threshold: the individual criterion fails
Excluding the gender from the model makes the two feature vectors coincide, so the gap vanishes; the prediction for the applicant is then unchanged by the flip.
print("[B-model] excluding the gender from the model closes the gap")
# With the gender excluded, the sketch coordinate of the twin equals the
# applicant's: both are predicted at the same point of the curve.
x_star_without_gender = X_PRIME
gap_without_gender = h(x_star_without_gender) - h(X_PRIME)
print(f" gap with the gender excluded from the model: {gap_without_gender:.2f}")
check("[B-model] the gap vanishes once the gender is excluded",
gap_without_gender == 0)
check("[B-model] the applicant's decision is then unchanged by the flip",
(h(x_star_without_gender) < TAU) == (h(X_PRIME) < TAU))
# ------------------------------------------------------------ preview
xs = np.linspace(0, 8, 161)
fig, ax = plt.subplots(figsize=(6.4, 4.0))
ax.axhline(TAU, ls=":", c="k", label="approval threshold (dotted line)")
ax.plot(xs, h(xs), "k-", label="learned hypothesis h (solid curve)")
ax.plot(LOANS_X, LOANS_Y, "ko", ms=4, label="training set, five loans (filled circles)")
ax.plot([X_PRIME], [0], "ks", mfc="none", ms=6, label="applicant x' (open square)")
ax.plot([X_STAR], [0], "k^", mfc="none", ms=7, label="gender flipped x* (open triangle)")
for x in (X_PRIME, X_STAR):
ax.plot([x, x, 6.6], [0, h(x), h(x)], "k--", lw=0.8)
ax.annotate("", xy=(6.6, h(X_STAR)), xytext=(6.6, h(X_PRIME)),
arrowprops=dict(arrowstyle="<->"))
ax.text(6.7, (h(X_PRIME) + h(X_STAR)) / 2, f"{gap:.2f}", fontsize=8, va="center")
ax.annotate("", xy=(X_STAR - 0.2, 0.35), xytext=(X_PRIME + 0.2, 0.35),
arrowprops=dict(arrowstyle="->"))
ax.text((X_PRIME + X_STAR) / 2, 0.45, "same person, other gender", fontsize=8, ha="center")
ax.set_xlabel("feature vector x (income, gender, ...; sketch)")
ax.set_ylabel("creditworthiness (label y)")
ax.set_ylim(0, 5.3)
ax.set_title("The counterfactual fairness criterion in credit scoring")
ax.legend(frameon=False, fontsize=7, loc="lower right")
fig.tight_layout()
fig.savefig(OUT_DIR / "fairness.png", dpi=110)
print(f"wrote {OUT_DIR / 'fairness.png'}")
failed = [n for n, ok in report if not ok]
print(f"{len(report) - len(failed)}/{len(report)} checks passed"
+ (f"; FAILED: {failed}" if failed else ""))
[B-model] excluding the gender from the model closes the gap
gap with the gender excluded from the model: 0.00
[ok] [B-model] the gap vanishes once the gender is excluded
[ok] [B-model] the applicant's decision is then unchanged by the flip
wrote /Users/junga1/dictionaryappliedml/pythondemos/fairness.png
7/7 checks passed

B-model writes when the script runs