MMG / app.py
SChodavarpu's picture
Update app.py
b14bac0 verified
# app.py
import math
import numpy as np
import gradio as gr
# ===============================
# CARPL Mammography AI ROI Calculator (MMG)
# Uptake fixed=100%, reading time in minutes, program costs backend.
# Outputs not trimmed; placed as Overall card + Tabs (Financial/Clinical/Operational)
# + Waterfall + Evidence + CTA. Single summary. Bars have labels.
# ===============================
# ---------- Helpers ----------
def usd(x: float) -> str:
try:
return "$" + f"{x:,.0f}"
except Exception:
return "$0"
def pct(x: float) -> str:
try:
return f"{x*100:.1f}%"
except Exception:
return "0.0%"
def clamp_nonneg(x: float) -> float:
return max(0.0, float(x))
# ---------- Core math ----------
# Uptake is assumed 100% (kept as hidden state for sensitivity if needed)
def compute(
site_type: str,
monthly_volume: float, # monthly mammography exams
read_minutes: float, # baseline read time per case (minutes)
radiologist_hourly_cost: float, # $/hr
# Sensitivity (advanced / hidden by default)
base_ppr: float, ai_ppr: float, # positive pickup rate (fraction)
base_audit_rate: float, ai_audit_rate: float, # audit uplift
base_recall_rate: float, ai_recall_rate: float,
recall_cost_per_case: float,
read_reduction_pct: float,
base_cost_per_scan: float, cost_reduction_pct: float,
followup_price: float, followup_uplift_pct: float,
early_detect_uplift_per_1000: float,
treatment_cost_delta_early_vs_late: float,
# Program costs (backend / hidden)
vendor_per_case_fee: float,
platform_annual_fee: float,
integration_overhead_monthly: float,
cloud_compute_monthly: float,
uptake_pct_hidden: float = 100.0,
):
uptake = max(0.0, min(1.0, uptake_pct_hidden/100.0))
monthly_ai_cases = clamp_nonneg(monthly_volume * uptake)
annual_ai_cases = monthly_ai_cases * 12.0
# Clinical deltas (scaled by AI cases)
errors_reduced = clamp_nonneg(monthly_ai_cases * (base_ppr - ai_ppr)) # fewer missed positives
discrepant_flags = clamp_nonneg(monthly_ai_cases * (ai_audit_rate - base_audit_rate))
recalls_avoided = clamp_nonneg(monthly_ai_cases * (base_recall_rate - ai_recall_rate))
earlier_detections = clamp_nonneg(monthly_ai_cases * (early_detect_uplift_per_1000 / 1000.0))
# Operational
base_read_seconds = read_minutes * 60.0
hours_saved = clamp_nonneg(monthly_ai_cases * (base_read_seconds * read_reduction_pct) / 3600.0)
workload_reduction_pct = read_reduction_pct
fte_saved = hours_saved / 160.0
capacity_increase_pct = (1.0 / max(1e-6, (1.0 - read_reduction_pct)) - 1.0) # approx headroom
value_time_saved_month = hours_saved * radiologist_hourly_cost
# Financial
baseline_monthly_cost = monthly_ai_cases * base_cost_per_scan
new_monthly_cost = baseline_monthly_cost * (1.0 - cost_reduction_pct)
per_scan_cost_savings_month = baseline_monthly_cost - new_monthly_cost
addl_followups = monthly_ai_cases * followup_uplift_pct
addl_followup_revenue_month = addl_followups * followup_price
recall_cost_savings_month = recalls_avoided * recall_cost_per_case
early_detection_savings_month = earlier_detections * treatment_cost_delta_early_vs_late
# Program costs (backend)
vendor_cost_month = monthly_ai_cases * vendor_per_case_fee
platform_cost_month = platform_annual_fee / 12.0
other_costs_month = integration_overhead_monthly + cloud_compute_monthly
incr_revenue_month = addl_followup_revenue_month
incr_costs_month = vendor_cost_month + platform_cost_month + other_costs_month
ops_value_month = value_time_saved_month + per_scan_cost_savings_month + recall_cost_savings_month + early_detection_savings_month
net_impact_month = incr_revenue_month - incr_costs_month + ops_value_month
roi_pct = (net_impact_month / incr_costs_month) if incr_costs_month > 0 else float("nan")
# Annual KPIs
net_impact_annual = net_impact_month * 12.0
roi_pct_annual = (net_impact_annual / (incr_costs_month*12.0)) if incr_costs_month > 0 else float("nan")
annual_program_cost = platform_annual_fee + vendor_per_case_fee*annual_ai_cases + other_costs_month*12.0
net_impact_per_ai_case_month = net_impact_month / max(1.0, monthly_ai_cases)
months_to_payback = (annual_program_cost / max(1e-6, net_impact_per_ai_case_month)) / max(1.0, monthly_ai_cases)
# One-liner (only once)
clinical_bullet = (
f"~{int(round(recalls_avoided))} recalls avoided, "
f"{earlier_detections:.1f} earlier cancers detected, "
f"{int(round(errors_reduced))} fewer missed positives"
)
summary_line = (
f"For your practice with {int(monthly_volume):,} mammography scans/month, modeled net benefit is "
f"{usd(net_impact_month)} per month. Clinical: {clinical_bullet}."
)
# ---------- Cards (HTML) ----------
# Overall Impact card with one-liner
overall_html = f"""
<div class='card'>
<div style='display:flex;justify-content:space-between;align-items:center;margin-bottom:8px'>
<div style='font-weight:700'>Overall Impact</div>
<div class='pill'>Clinical 路 Financial 路 Operational</div>
</div>
<div style='margin-bottom:8px'>{summary_line}</div>
<div class='kpi-grid'>
<div>Incremental revenue (annual)</div><div><b>{usd(incr_revenue_month*12)}</b></div>
<div>Incremental costs (annual)</div><div class='neg'><b>{usd(incr_costs_month*12)}</b></div>
<div>Operational value (annual)</div><div><b>{usd(ops_value_month*12)}</b></div>
<div class='sep'>Net impact (annual)</div><div class='sep'><b>{usd(net_impact_annual)}</b></div>
<div title="ROI = Net impact 梅 (annual platform license + vendor per-case fees)">ROI %</div><div><b>{'' if math.isnan(roi_pct_annual) else f"{roi_pct_annual*100:.1f}%"} </b></div>
<div title="(Annual AI program cost 梅 net impact per AI case) 梅 monthly AI cases">Months to payback</div><div><b>{months_to_payback:.1f}</b></div>
</div>
</div>
"""
# Financial (full metrics)
financial_html = f"""
<div class='card'>
<div style='font-weight:700;margin-bottom:6px'>Financial (monthly unless noted)</div>
<div class='kpi-grid'>
<div>Additional follow-up scans (count/mo)</div><div>{int(round(addl_followups))}</div>
<div>Additional follow-up revenue (mo)</div><div>{usd(incr_revenue_month)}</div>
<div>Value of time saved (mo)</div><div>{usd(value_time_saved_month)}</div>
<div>Per-scan radiologist cost savings (mo)</div><div>{usd(per_scan_cost_savings_month)}</div>
<div>Savings from avoided recalls (mo)</div><div>{usd(recall_cost_savings_month)}</div>
<div>Savings from earlier detection (mo)</div><div>{usd(early_detection_savings_month)}</div>
<div class='sep'>AI vendor fees (mo)</div><div class='sep'>{usd(vendor_cost_month)}</div>
<div>Platform license (mo)</div><div>{usd(platform_cost_month)}</div>
<div>Integration & cloud (mo)</div><div>{usd(other_costs_month)}</div>
<div class='sep'>Net impact (mo)</div><div class='sep'><b>{usd(net_impact_month)}</b></div>
<div>Net impact (annual)</div><div><b>{usd(net_impact_annual)}</b></div>
<div>ROI % (annual)</div><div><b>{'' if math.isnan(roi_pct_annual) else f"{roi_pct_annual*100:.1f}%"}</b></div>
<div>Months to payback</div><div><b>{months_to_payback:.1f}</b></div>
</div>
</div>
"""
# Clinical (full metrics)
clinical_html = f"""
<div class='card'>
<div style='font-weight:700;margin-bottom:6px'>Clinical (per month)</div>
<div class='kpi-grid'>
<div>Fewer missed positives (螖 pickup)</div><div>{int(round(errors_reduced))}</div>
<div>Discrepant cases flagged (audit uplift)</div><div>{int(round(discrepant_flags))}</div>
<div>Earlier cancers detected</div><div>{earlier_detections:.1f}</div>
<div>Recalls avoided</div><div>{int(round(recalls_avoided))}</div>
</div>
<div class='bars'>
<div class='bar-row'>
<div>Recall reduction</div>
<div class='bar'><span style='width:{max(0.0,(base_recall_rate-ai_recall_rate))*100:.1f}%'><em>{pct(max(0.0, base_recall_rate-ai_recall_rate))}</em></span></div>
<div>{pct(max(0.0, base_recall_rate-ai_recall_rate))}</div>
</div>
<div class='bar-row'>
<div>Pickup improvement</div>
<div class='bar'><span style='width:{max(0.0,(base_ppr-ai_ppr))*100:.1f}%'><em>{pct(max(0.0, base_ppr-ai_ppr))}</em></span></div>
<div>{pct(max(0.0, base_ppr-ai_ppr))}</div>
</div>
</div>
</div>
"""
# Operational (full metrics)
operational_html = f"""
<div class='card'>
<div style='font-weight:700;margin-bottom:6px'>Operational</div>
<div class='kpi-grid'>
<div>Hours saved / month</div><div>{hours_saved:.1f}</div>
<div>Workload reduction</div><div>{pct(workload_reduction_pct)}</div>
<div>Approx. FTE-month saved</div><div>{fte_saved:.2f}</div>
<div>Effective capacity increase</div><div>{pct(capacity_increase_pct)}</div>
</div>
</div>
"""
# Waterfall with labels inside bars
wf_rows = [
("Incremental revenue", incr_revenue_month),
("Incremental costs", -incr_costs_month),
("Operational value", ops_value_month),
]
total = incr_revenue_month - incr_costs_month + ops_value_month
def wf_row(label, val):
denom = abs(incr_revenue_month) + abs(incr_costs_month) + abs(ops_value_month)
width = 0 if denom == 0 else min(100, max(2, int(abs(val) / denom * 100)))
cls = 'pos' if val >= 0 else 'neg'
value_label = usd(val)
return f"<div class='wf-row'><div>{label}</div><div class='wf-bar {cls}' style='width:{width}%;'><span class='wf-val'>{value_label}</span></div></div>"
waterfall_html = "<div class='card'><div style='font-weight:700;margin-bottom:6px'>Waterfall (monthly)</div>" + "".join([wf_row(l,v) for l,v in wf_rows]) + f"<div class='wf-total'>Net impact: <b>{usd(total)}</b></div></div>"
# Evidence (neutral copy; placeholders)
evidence_html = """
<div class='card'>
<div style='font-weight:700;margin-bottom:6px'>Evidence snapshot</div>
<ul class='evidence'>
<li>Modeled reductions in recall rates and false positives reduce unnecessary follow-ups and costs (peer-reviewed registries).</li>
<li>Earlier cancer detection can lower treatment costs versus late-stage presentation (observational cohorts).</li>
<li>Reading-time reductions translate into throughput gains and less burnout (prospective audits).</li>
</ul>
<div class='small-note'>Neutral claims; update with citations per site.</div>
</div>
"""
# CTA (shown after first run)
cta_html = f"""
<div class='card cta'>
<div>Based on {int(annual_ai_cases):,} AI cases/yr: net impact {usd(net_impact_annual)}, ROI {'' if math.isnan(roi_pct_annual) else f"{roi_pct_annual*100:.1f}%"},
payback {months_to_payback:.1f} mo; avoids ~{int(round(recalls_avoided*12))} recalls; frees ~{hours_saved*12:.0f} clinician hours annually.</div>
<a class='cta-btn' href='https://example.com/book-demo' target='_blank' rel='noopener'>Book a 15-min walkthrough</a>
</div>
"""
# Return: Overall + Tabs content + Waterfall + Evidence + CTA
return overall_html, financial_html, clinical_html, operational_html, waterfall_html, evidence_html, cta_html
# ---------- UI ----------
def build_ui():
with gr.Blocks(theme=gr.themes.Soft(), css="""
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap');
* { font-family: Inter, ui-sans-serif, system-ui; }
.gradio-container { max-width: 1100px !important; }
.card{background:#fff;border:1px solid #eef2f7;border-radius:18px;padding:18px;box-shadow:0 8px 24px rgba(0,0,0,.08);margin-bottom:12px}
.pill{background:#ecfdf5;color:#065f46;padding:4px 10px;border-radius:999px;font-weight:700;font-size:.75rem}
.kpi-grid{display:grid;grid-template-columns:1fr auto;gap:6px 12px}
.kpi-grid .sep{border-top:1px solid #e5e7eb;padding-top:6px}
.neg{color:#b91c1c}
.small-note{opacity:.75;font-size:.9em;margin-top:6px}
.bars .bar-row{display:grid;grid-template-columns:1fr auto auto;gap:10px;align-items:center;margin:6px 0}
.bars .bar{height:12px;background:#f1f5f9;border-radius:999px;position:relative;overflow:hidden;width:100%}
.bars .bar span{display:block;height:100%;background:linear-gradient(90deg,#14b8a6,#22d3ee);position:relative}
.bars .bar span em{position:absolute;right:6px;top:-18px;font-size:.85em;opacity:.8}
.wf-row{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center;margin:8px 0}
.wf-bar{height:22px;border-radius:6px;display:flex;align-items:center;justify-content:flex-end;padding-right:8px;color:#0b1727;min-width:80px}
.wf-bar.pos{background:linear-gradient(90deg,#a7f3d0,#34d399)}
.wf-bar.neg{background:linear-gradient(90deg,#fecaca,#f87171)}
.wf-val{font-weight:700}
.wf-total{margin-top:8px;border-top:1px solid #e5e7eb;padding-top:8px;font-weight:700}
.cta{display:flex;justify-content:space-between;align-items:center}
.cta-btn{background:#0ea5e9;color:#fff;text-decoration:none;padding:10px 14px;border-radius:12px;font-weight:700}
.header{display:flex;justify-content:space-between;align-items:center}
.header .title{font-weight:800;font-size:1.1rem}
.header .pill{margin-left:8px}
""") as demo:
gr.Markdown("""
<div class='header'>
<div class='title'>CARPL ROI Calculator 路 Mammography AI</div>
<div class='pill'>Clinical 路 Financial 路 Operational</div>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
# Inputs (minimal)
site_type = gr.Dropdown(
["Hospital / Health System","Imaging Center","Academic Medical Center"],
value="Hospital / Health System", label="Site type"
)
monthly_volume = gr.Slider(0, 20000, 7500, step=50, label="Monthly volume", info="Mammography exams per month")
read_minutes = gr.Number(label="Avg reading time today (minutes)", value=1.7, info="Per case")
radiologist_hourly_cost = gr.Number(label="Radiologist cost (USD/hr)", value=180)
with gr.Accordion("Sensitivity (advanced)", open=False):
base_ppr = gr.Slider(0, 1, value=0.10, step=0.001, label="Baseline positive pickup rate")
ai_ppr = gr.Slider(0, 1, value=0.095, step=0.001, label="With-AI positive pickup rate")
base_audit_rate = gr.Slider(0, 1, value=0.00, step=0.001, label="Baseline audit flag rate")
ai_audit_rate = gr.Slider(0, 1, value=0.05, step=0.001, label="With-AI audit flag rate")
base_recall_rate = gr.Slider(0, 1, value=0.028, step=0.001, label="Baseline recall rate")
ai_recall_rate = gr.Slider(0, 1, value=0.025, step=0.001, label="With-AI recall rate")
recall_cost_per_case = gr.Number(value=250.0, label="Cost per recall case (USD)")
read_reduction_pct = gr.Slider(0, 0.8, value=0.15, step=0.005, label="Reading time reduction with AI (fraction)")
base_cost_per_scan = gr.Number(value=15, label="Radiologist cost per scan (USD)")
cost_reduction_pct = gr.Slider(0, 0.8, value=0.15, step=0.005, label="Per-scan radiologist cost reduction")
followup_price = gr.Number(value=200, label="Price per follow-up scan (USD)")
followup_uplift_pct = gr.Slider(0, 0.2, value=0.0095, step=0.0005, label="Follow-up uplift (fraction of AI scans)")
early_detect_uplift_per_1000 = gr.Number(value=0.7, label="Earlier cancers detected (extra per 1000 AI scans)")
treatment_cost_delta_early_vs_late = gr.Number(value=15000.0, label="Treatment cost savings per earlier case (USD)")
# Hidden program costs (backend)
vendor_per_case_fee = gr.State(2.5)
platform_annual_fee = gr.State(12000)
integration_overhead_monthly = gr.State(0.0)
cloud_compute_monthly = gr.State(0.0)
uptake_pct_hidden = gr.State(100.0)
run_btn = gr.Button("Calculate", variant="primary")
with gr.Column(scale=1):
overall_card = gr.HTML()
with gr.Tabs():
with gr.Tab("Financial"):
financial_card = gr.HTML()
with gr.Tab("Clinical"):
clinical_card = gr.HTML()
with gr.Tab("Operational"):
operational_card = gr.HTML()
waterfall_panel = gr.HTML()
evidence_panel = gr.HTML()
cta_panel = gr.HTML(visible=False)
inputs = [
site_type, monthly_volume, read_minutes, radiologist_hourly_cost,
base_ppr, ai_ppr, base_audit_rate, ai_audit_rate, base_recall_rate, ai_recall_rate,
recall_cost_per_case, read_reduction_pct, base_cost_per_scan, cost_reduction_pct,
followup_price, followup_uplift_pct, early_detect_uplift_per_1000, treatment_cost_delta_early_vs_late,
vendor_per_case_fee, platform_annual_fee, integration_overhead_monthly, cloud_compute_monthly,
uptake_pct_hidden
]
outputs = [overall_card, financial_card, clinical_card, operational_card, waterfall_panel, evidence_panel, cta_panel]
def _wrap_compute(*vals):
res = compute(*vals)
# Reveal CTA after first run
return (*res[:-1], gr.update(value=res[-1], visible=True))
run_btn.click(_wrap_compute, inputs=inputs, outputs=outputs)
demo.load(_wrap_compute, inputs=inputs, outputs=outputs)
return demo
def main():
return build_ui()
if __name__ == "__main__":
app = build_ui()
app.launch()