import matplotlib.pyplot as plt
import numpy as np
from io import StringIO

# The same 20 patients; every threshold t in [0, 1] flags the patients with score >= t
score = np.array([0.95, 0.90, 0.85, 0.80, 0.75, 0.70, 0.65, 0.60, 0.55, 0.45,
                  0.40, 0.35, 0.30, 0.25, 0.20, 0.15, 0.10, 0.08, 0.05, 0.02])
sick = np.array([1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0])
t = np.linspace(0.001, 0.999, 2000)
flag = score[None, :] >= t[:, None]
TP = (flag & (sick == 1)).sum(1)
FP = (flag & (sick == 0)).sum(1)
FN = 10 - TP
recall, fpr = TP / 10, FP / 10
precision = np.where(TP + FP > 0, TP / np.maximum(TP + FP, 1), np.nan)
f1 = np.where(TP > 0, 2 * TP / (2 * TP + FP + FN), np.nan)
youden = recall - fpr
cost = 10 * FN + 1 * FP                  # a missed disease costs 10 times a needless referral

fig, (left, right) = plt.subplots(1, 2, figsize=(10, 4.4))
left.plot(t, recall, color='#3fb950', lw=1.6, label='recall')
left.plot(t, precision, color='#58a6ff', lw=1.6, label='precision')
left.plot(t, f1, color='#f0883e', lw=2.2, label='F1')
left.plot(t, youden, color='gray', lw=1.6, ls='--', label='Youden J = TPR − FPR')
left.axvline(0.5, color='gray', lw=0.8, ls=':')
left.plot(0.35, 0.818, 'o', color='#f0883e', ms=8, mfc='none', mew=1.8)
left.annotate('max F1 = 0.818\nat t = 0.35', xy=(0.35, 0.818), xytext=(0.72, 0.95), fontsize=8,
              color='#f0883e', arrowprops=dict(arrowstyle='->', color='#f0883e'))
left.annotate('max J = 0.6 at t = 0.45\nand again at t = 0.35', xy=(0.42, 0.6), xytext=(0.5, 0.2),
              fontsize=8, color='gray', arrowprops=dict(arrowstyle='->', color='gray'))
left.set_ylim(0, 1.08)
left.set_title('Metrics as the threshold is lowered', fontsize=10)
left.legend(fontsize=7.5, loc='upper center', bbox_to_anchor=(0.5, -0.16), ncol=4, handlelength=1.5)

right.plot(t, cost, color='#ff7b72', lw=2)
right.axvline(0.5, color='gray', lw=0.8, ls=':')
right.axvline(1 / 11, color='#58a6ff', lw=1, ls='--')
right.text(1 / 11 + 0.015, 70, 't* = 1/(1+10) ≈ 0.09\n(optimal if the scores\nwere calibrated): cost 7',
           fontsize=8, color='#58a6ff', ha='right')
right.plot(0.175, 5, 'o', color='#ff7b72', ms=8, mfc='none', mew=1.8)
right.annotate('minimum: cost 5 (5 FP, 0 FN)\nfor t in (0.15, 0.20]', xy=(0.19, 5), xytext=(0.95, 8), fontsize=8,
               color='#ff7b72', arrowprops=dict(arrowstyle='->', color='#ff7b72'))
right.annotate('t = 0.5: cost 32\n(3 FN × 10 + 2 FP)', xy=(0.5, 32), xytext=(0.6, 46), fontsize=8,
               color='gray', arrowprops=dict(arrowstyle='->', color='gray'))
right.set_ylabel('cost = 10 · FN + 1 · FP')
right.set_ylim(0, 102)
right.set_title('Expected cost when a miss costs 10 false alarms', fontsize=10)
for ax in (left, right):
    ax.set_xlim(1, 0)                    # read left to right = lowering the threshold
    ax.set_xlabel('threshold t   (flag if score ≥ t; lowered from left to right)')
    ax.grid(color='gray', alpha=0.2, lw=0.5)

plt.tight_layout()

buffer = StringIO()
plt.savefig(buffer, format="svg", transparent=True)
print(buffer.getvalue())

plt.close()
