Dictionary of Applied Machine Learning · trustworthy artificial intelligence (trustworthy AI)

trustworthy artificial intelligence (trustworthy AI) — Python demo

Numerical companion to the entry trustworthy artificial intelligence (trustworthy AI): it recomputes what the entry states and prints one line per check

The entry locates four of the seven key requirements (KRs) for trustworthy AI in the credit-scoring example of its Fig. 1: a learned hypothesis h approves a loan when the predicted creditworthiness h(x) exceeds the threshold tau = 3.8. 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, with a training set of five completed loans; this demo checks every number the entry states about it. Self-contained (numpy/matplotlib only), no randomness.

Run it without installing anything:
uv run https://dictionaryofml.org/terms/trustAI.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 trustAI.py into a folder and run uv run trustAI.py there. With NumPy and Matplotlib already installed, python3 trustAI.py, from any directory — it writes its output files into the current directory. Fixed seeds, so the printed numbers reproduce exactly. Download trustAI.py · Notebook · Open in Colab

The script, block by block

One cell per block of the script: the code, and what that code printed when it last ran here

setup

"""
trustAI.py — numerical companion to the glossary entry 'trustAI'
(trustworthy artificial intelligence).

The entry locates four of the seven key requirements (KRs) for
trustworthy AI in the credit-scoring example of its Fig. 1: a learned
hypothesis h approves a loan when the predicted creditworthiness h(x)
exceeds the threshold tau = 3.8. 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, with a training set of five completed
loans; this demo checks every number the entry states about it.
Self-contained (numpy/matplotlib only), no randomness.

Blocks
------
[B-curve]     The five completed loans and the drawn hypothesis: h has a
              far smaller average loss on the training set
              than a constant prediction, and the stated predictions
              h(2.3) = 2.94, h(2.8) = 3.19, h(4.6) = 3.8 and h(5.2) = 3.93
              are what the closed form gives.
[B-robust]    KR2, robustness: the perturbation of the applicant's feature
              vector x' = 2.3 by delta = 0.5 moves the prediction from
              2.94 to 3.19; the slope of h never exceeds 1.225, so no
              perturbation by delta moves any prediction by more than
              1.225 delta (h is Lipschitz continuous with that constant).
[B-fair]      KR5, fairness: the gender flip moves the applicant to
              x* = 5.2, the prediction rises above the threshold, and the
              gap h(x*) - h(x') is the stated 0.99 — the decision depends
              on the gender alone.
[B-explain]   KR4, transparency: the counterfactual x'' = 4.6 is where h
              reaches the threshold; every feature vector below it is
              rejected and every one above it approved, so x'' is the
              smallest change that flips the decision.
[B-oversight] KR1, human oversight: exactly one of the five loans has a
              prediction within eps = 0.3 of the threshold (the band
              passed to a loan officer), and a wider band holds more.

Outputs
-------
trustAI.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)))


# the cartoon's constants, as the entry states them
TAU, EPS = 3.8, 0.3                     # approval threshold, oversight band
X_PRIME, DELTA = 2.3, 0.5               # the applicant, the KR2 perturbation
X_STAR, X_CF = 5.2, 4.6                 # gender flipped, counterfactual
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])

B-curve

The five completed loans and the drawn hypothesis: h has a far smaller average loss on the training set than a constant prediction, and the stated predictions h(2.3) = 2.94, h(2.8) = 3.19, h(4.6) = 3.8 and h(5.2) = 3.93 are what the closed form gives.

print("[B-curve] the five completed loans and the drawn hypothesis")
loss_h = np.mean((LOANS_Y - h(LOANS_X)) ** 2)
loss_const = np.mean((LOANS_Y - LOANS_Y.mean()) ** 2)
print(f"    average loss on the five loans: {loss_h:.3f} "
      f"(hypothesis) vs {loss_const:.3f} (constant prediction)")
check("[B-curve] h has a far smaller average loss on the training set than "
      "a constant prediction", loss_h < 0.2 * loss_const)
stated = {2.3: 2.94, 2.8: 3.19, 4.6: 3.80, 5.2: 3.93}
for x, value in stated.items():
    check(f"[B-curve] h({x}) = {value:.2f} as the entry states",
          abs(h(x) - value) < 0.005)
[B-curve] the five completed loans and the drawn hypothesis
    average loss on the five loans: 0.089 (hypothesis) vs 1.198 (constant prediction)
  [ok] [B-curve] h has a far smaller average loss on the training set than a constant prediction
  [ok] [B-curve] h(2.3) = 2.94 as the entry states
  [ok] [B-curve] h(2.8) = 3.19 as the entry states
  [ok] [B-curve] h(4.6) = 3.80 as the entry states
  [ok] [B-curve] h(5.2) = 3.93 as the entry states

B-robust

KR2, robustness: the perturbation of the applicant's feature vector x' = 2.3 by delta = 0.5 moves the prediction from 2.94 to 3.19; the slope of h never exceeds 1.225, so no perturbation by delta moves any prediction by more than 1.225 delta (h is Lipschitz continuous with that constant).

print("[B-robust] KR2: a perturbation by delta moves the prediction little")
change = h(X_PRIME + DELTA) - h(X_PRIME)
slope_max = 3.5 * 0.35                   # the slope of h at x = 0, its maximum
grid = np.linspace(0, 8, 1601)
print(f"    h({X_PRIME}) = {h(X_PRIME):.2f} -> h({X_PRIME + DELTA}) = "
      f"{h(X_PRIME + DELTA):.2f}, change {change:.2f} <= {slope_max} * delta "
      f"= {slope_max * DELTA:.3f}")
check("[B-robust] the perturbation of x' by delta = 0.5 moves the prediction "
      "from 2.94 to 3.19", abs(change - 0.25) < 0.01)
check("[B-robust] the slope of h never exceeds 1.225",
      np.all(np.gradient(h(grid), grid) <= slope_max + 1e-9))
check("[B-robust] no perturbation by delta moves a prediction by more than "
      "1.225 delta: h is Lipschitz continuous with constant 1.225",
      np.all(np.abs(h(grid + DELTA) - h(grid)) <= slope_max * DELTA + 1e-9))
[B-robust] KR2: a perturbation by delta moves the prediction little
    h(2.3) = 2.94 -> h(2.8) = 3.19, change 0.25 <= 1.2249999999999999 * delta = 0.612
  [ok] [B-robust] the perturbation of x' by delta = 0.5 moves the prediction from 2.94 to 3.19
  [ok] [B-robust] the slope of h never exceeds 1.225
  [ok] [B-robust] no perturbation by delta moves a prediction by more than 1.225 delta: h is Lipschitz continuous with constant 1.225

B-fair

KR5, fairness: the gender flip moves the applicant to x* = 5.2, the prediction rises above the threshold, and the gap h(x*) - h(x') is the stated 0.99 — the decision depends on the gender alone.

print("[B-fair] KR5: the same applicant with the gender flipped")
gap = h(X_STAR) - h(X_PRIME)
print(f"    h(x') = {h(X_PRIME):.2f} (rejected), h(x*) = {h(X_STAR):.2f} "
      f"(approved), gap {gap:.3f}")
check("[B-fair] the applicant is rejected and the gender-flipped twin "
      "approved", h(X_PRIME) < TAU <= h(X_STAR))
check("[B-fair] the gap between the two predictions 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-fair] KR5: the same applicant with the gender flipped
    h(x') = 2.94 (rejected), h(x*) = 3.93 (approved), gap 0.998
  [ok] [B-fair] the applicant is rejected and the gender-flipped twin approved
  [ok] [B-fair] the gap between the two predictions is the stated 0.99 (the difference of the rounded predictions, within 0.01)

B-explain

KR4, transparency: the counterfactual x'' = 4.6 is where h reaches the threshold; every feature vector below it is rejected and every one above it approved, so x'' is the smallest change that flips the decision.

print("[B-explain] KR4: the counterfactual at which h reaches the threshold")
x_cf_exact = np.log(5) / 0.35             # h(x) = 3.8  <=>  exp(-0.35 x) = 1/5
print(f"    h reaches tau at x = ln(5)/0.35 = {x_cf_exact:.4f}, drawn at {X_CF}")
check("[B-explain] h reaches the threshold at the counterfactual x'' = 4.6",
      abs(h(X_CF) - TAU) < 0.005 and abs(x_cf_exact - X_CF) < 0.005)
below = np.linspace(0, x_cf_exact - 1e-6, 400)
above = np.linspace(x_cf_exact + 1e-6, 8, 400)
check("[B-explain] every feature vector below x'' is rejected and every one "
      "above it approved: x'' is the smallest change that flips the decision",
      np.all(h(below) < TAU) and np.all(h(above) > TAU))
[B-explain] KR4: the counterfactual at which h reaches the threshold
    h reaches tau at x = ln(5)/0.35 = 4.5984, drawn at 4.6
  [ok] [B-explain] h reaches the threshold at the counterfactual x'' = 4.6
  [ok] [B-explain] every feature vector below x'' is rejected and every one above it approved: x'' is the smallest change that flips the decision

B-oversight

KR1, human oversight: exactly one of the five loans has a prediction within eps = 0.3 of the threshold (the band passed to a loan officer), and a wider band holds more.

print("[B-oversight] KR1: predictions within eps of the threshold go to a "
      "loan officer")
in_band = np.abs(h(LOANS_X) - TAU) < EPS
print(f"    loans with a prediction within {EPS} of tau: {int(in_band.sum())} "
      f"of {LOANS_X.size}")
check("[B-oversight] exactly one of the five loans falls into the band",
      int(in_band.sum()) == 1)
check("[B-oversight] a wider band passes more loans to the loan officer",
      int((np.abs(h(LOANS_X) - TAU) < 2 * EPS).sum()) > int(in_band.sum()))

# ------------------------------------------------------------ preview
xs = np.linspace(0, 8, 161)
fig, ax = plt.subplots(figsize=(6.4, 4.0))
ax.axhspan(TAU - EPS, TAU + EPS, color="0.88", label="KR1: passed to a loan officer (band)")
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="KR5: gender flipped x* (open triangle)")
ax.plot([X_CF], [0], "kD", mfc="none", ms=6, label="KR4: counterfactual x'' (open diamond)")
for x in (X_PRIME, X_CF, X_STAR):
    ax.plot([x, x], [0, h(x)], "k--", lw=0.8)
ax.annotate("", xy=(X_PRIME + DELTA, h(X_PRIME)), xytext=(X_PRIME, h(X_PRIME)),
            arrowprops=dict(arrowstyle="->"))
ax.annotate("", xy=(X_PRIME + DELTA, h(X_PRIME + DELTA)),
            xytext=(X_PRIME + DELTA, h(X_PRIME)), arrowprops=dict(arrowstyle="->"))
ax.text(X_PRIME + DELTA + 0.1, h(X_PRIME) - 0.05, "KR2: perturbation", fontsize=8, va="top")
ax.set_xlabel("feature vector x (income, gender, ...; sketch)")
ax.set_ylabel("creditworthiness (label y)")
ax.set_ylim(0, 5.3)
ax.set_title("KR1, KR2, KR4, KR5 in the credit-scoring example")
ax.legend(frameon=False, fontsize=7, loc="lower right")
fig.tight_layout()
fig.savefig(OUT_DIR / "trustAI.png", dpi=110)
print(f"wrote {OUT_DIR / 'trustAI.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-oversight] KR1: predictions within eps of the threshold go to a loan officer
    loans with a prediction within 0.3 of tau: 1 of 5
  [ok] [B-oversight] exactly one of the five loans falls into the band
  [ok] [B-oversight] a wider band passes more loans to the loan officer
wrote /Users/junga1/dictionaryappliedml/pythondemos/trustAI.png
14/14 checks passed
Preview figure produced by trustAI.py
The preview figure the block B-oversight writes when the script runs