File size: 6,296 Bytes
74d361e cfc7962 74d361e 88ffb40 cfc7962 74d361e 88ffb40 74d361e 88ffb40 74d361e 88ffb40 c872458 74d361e 88ffb40 74d361e cfc7962 003529a bc4de5f 003529a cfc7962 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | import numpy as np
import matplotlib.pyplot as plt
import gradio as gr
# ββ style ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
BG = '#f8fafc'
PANEL = '#ffffff'
GRID = '#e2e8f0'
ACCENT = '#2563eb'
C_INC = '#d97706'
C_ZS = '#7c3aed'
C_RR = '#059669'
C_TEXT = '#1e293b'
C_DIM = '#64748b'
C_BORDER = '#cbd5e1'
plt.rcParams.update({
'figure.facecolor': BG, 'axes.facecolor': PANEL,
'axes.edgecolor': C_BORDER, 'axes.labelcolor': C_DIM,
'xtick.color': C_DIM, 'ytick.color': C_DIM,
'grid.color': GRID, 'grid.linewidth': 0.8,
'text.color': C_TEXT, 'font.family': 'monospace',
})
EP_RANGE = np.linspace(0, 15, 600)
def calc_paf(episodes, incidence, zscore, rr):
return 1 - 1 / np.exp(episodes * incidence * zscore * rr)
# ββ plot function ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def draw(ep, inc, zs, rr):
paf_curve = calc_paf(EP_RANGE, inc, zs, rr) * 100
cur_paf = calc_paf(ep, inc, zs, rr)
exponent = ep * inc * zs * rr
fig, axes = plt.subplots(1, 2, figsize=(15, 6),
gridspec_kw={'width_ratios': [3, 1]})
fig.patch.set_facecolor(BG)
# ββ left: curve βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ax = axes[0]
ax.set_facecolor(PANEL)
for spine in ax.spines.values():
spine.set_color(C_BORDER)
# shadow + main curve
ax.plot(EP_RANGE, paf_curve, color=ACCENT, linewidth=5, alpha=0.15)
ax.plot(EP_RANGE, paf_curve, color=ACCENT, linewidth=2.2)
# crosshairs
ax.axvline(ep, color=ACCENT, linestyle='--', linewidth=1.2, alpha=0.6)
ax.axhline(cur_paf*100, color=ACCENT, linestyle='--', linewidth=1.2, alpha=0.3)
# dot at current point
ax.scatter([ep], [cur_paf*100], color=ACCENT, s=80, zorder=5,
edgecolors='white', linewidths=1.5)
# shaded area under curve up to current episode
mask = EP_RANGE <= ep
ax.fill_between(EP_RANGE[mask], paf_curve[mask], color=ACCENT, alpha=0.08)
ax.set_xlim(0, 15)
ax.set_ylim(0, 102)
ax.set_xlabel('Episodes', fontsize=11, labelpad=8)
ax.set_ylabel('PAF (%)', fontsize=11, labelpad=8)
ax.set_title('Population Attributable Fraction',
color=C_TEXT, fontsize=13, pad=12, fontweight='normal')
ax.grid(True, linewidth=0.8)
# annotation
ax.annotate(
f' {cur_paf*100:.1f}%',
xy=(ep, cur_paf*100),
xytext=(min(ep + 1, 13), cur_paf*100 + 4),
color=ACCENT, fontsize=15, fontweight='bold',
arrowprops=dict(arrowstyle='->', color=ACCENT, lw=1.2)
)
# formula watermark
ax.text(0.98, 0.05,
'PAF = 1 β exp(βep Γ inc Γ Ξz Γ RR)',
transform=ax.transAxes, fontsize=11,
color=C_DIM, ha='right', va='bottom')
# ββ right: breakdown panel ββββββββββββββββββββββββββββββββββββββββββββ
ax2 = axes[1]
ax2.set_facecolor('#f1f5f9')
for spine in ax2.spines.values():
spine.set_color(C_BORDER)
ax2.set_xticks([])
ax2.set_yticks([])
rows = [
('episodes', f'{ep:.2f}', ACCENT),
('incidence', f'{inc:.3f}', C_INC),
('Ξz / ep.', f'{zs:.3f}', C_ZS),
('RR', f'{rr:.3f}', C_RR),
('β' * 10, 'β' * 6, C_BORDER),
('exponent', f'{exponent:.4f}', '#475569'),
('exp(βx)', f'{1/np.exp(exponent):.4f}', '#475569'),
('β' * 10, 'β' * 6, C_BORDER),
('PAF', f'{cur_paf*100:.2f}%', ACCENT),
]
ax2.text(0.5, 0.97, 'BREAKDOWN', transform=ax2.transAxes,
ha='center', fontsize=13, color=ACCENT, fontweight='bold')
y_start = 0.88
for i, (k, v, c) in enumerate(rows):
y = y_start - i * 0.095
ax2.text(0.08, y, k, transform=ax2.transAxes,
fontsize=11, color=C_DIM, va='center')
ax2.text(0.92, y, v, transform=ax2.transAxes,
fontsize=11, color=c, va='center', ha='right', fontweight='bold')
ax2.set_title('Parameters', color=C_TEXT, fontsize=13, pad=8)
plt.tight_layout(pad=1.5)
return fig
# ββ Gradio UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Blocks(theme=gr.themes.Soft(), title="PAF Visualizer") as demo:
gr.HTML("""
<div style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;
padding:12px 20px;margin-bottom:12px;font-family:monospace;">
<span style="color:#1e293b;font-size:20px;font-weight:600;">
Population Attributable Fraction Visualizer
</span><br>
<span style="color:#64748b;font-size:13px;">
PAF = 1 β exp(βepisodes Γ incidence Γ Ξz Γ RR)
</span>
</div>""")
with gr.Row():
with gr.Column():
ep = gr.Slider(0, 10, value=4.2, step=0.1, label="Episodes")
inc = gr.Slider(0.01, 1, value=0.2, step=0.01, label="Pathogen Incidence")
zs = gr.Slider(0.01, 1, value=0.2, step=0.01, label="Ξz per Episode")
rr = gr.Slider(1.01, 3, value=1.5, step=0.05, label="Relative Risk (RR)")
reset_btn = gr.Button("Reset Defaults", variant="secondary")
plot = gr.Plot(label="")
# live update on any slider change
for slider in [ep, inc, zs, rr]:
slider.change(fn=draw, inputs=[ep, inc, zs, rr], outputs=plot)
# reset button
def reset():
return 5.0, 0.3, 1.2, 2.5
reset_btn.click(fn=reset, outputs=[ep, inc, zs, rr])
# draw on load
demo.load(fn=draw, inputs=[ep, inc, zs, rr], outputs=plot)
demo.launch() |