# 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"""
Overall Impact
Clinical · Financial · Operational
{summary_line}
Incremental revenue (annual)
{usd(incr_revenue_month*12)}
Incremental costs (annual)
{usd(incr_costs_month*12)}
Operational value (annual)
{usd(ops_value_month*12)}
Net impact (annual)
{usd(net_impact_annual)}
ROI %
{'' if math.isnan(roi_pct_annual) else f"{roi_pct_annual*100:.1f}%"}
Months to payback
{months_to_payback:.1f}
""" # Financial (full metrics) financial_html = f"""
Financial (monthly unless noted)
Additional follow-up scans (count/mo)
{int(round(addl_followups))}
Additional follow-up revenue (mo)
{usd(incr_revenue_month)}
Value of time saved (mo)
{usd(value_time_saved_month)}
Per-scan radiologist cost savings (mo)
{usd(per_scan_cost_savings_month)}
Savings from avoided recalls (mo)
{usd(recall_cost_savings_month)}
Savings from earlier detection (mo)
{usd(early_detection_savings_month)}
AI vendor fees (mo)
{usd(vendor_cost_month)}
Platform license (mo)
{usd(platform_cost_month)}
Integration & cloud (mo)
{usd(other_costs_month)}
Net impact (mo)
{usd(net_impact_month)}
Net impact (annual)
{usd(net_impact_annual)}
ROI % (annual)
{'' if math.isnan(roi_pct_annual) else f"{roi_pct_annual*100:.1f}%"}
Months to payback
{months_to_payback:.1f}
""" # Clinical (full metrics) clinical_html = f"""
Clinical (per month)
Fewer missed positives (Δ pickup)
{int(round(errors_reduced))}
Discrepant cases flagged (audit uplift)
{int(round(discrepant_flags))}
Earlier cancers detected
{earlier_detections:.1f}
Recalls avoided
{int(round(recalls_avoided))}
Recall reduction
{pct(max(0.0, base_recall_rate-ai_recall_rate))}
{pct(max(0.0, base_recall_rate-ai_recall_rate))}
Pickup improvement
{pct(max(0.0, base_ppr-ai_ppr))}
{pct(max(0.0, base_ppr-ai_ppr))}
""" # Operational (full metrics) operational_html = f"""
Operational
Hours saved / month
{hours_saved:.1f}
Workload reduction
{pct(workload_reduction_pct)}
Approx. FTE-month saved
{fte_saved:.2f}
Effective capacity increase
{pct(capacity_increase_pct)}
""" # 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"
{label}
{value_label}
" waterfall_html = "
Waterfall (monthly)
" + "".join([wf_row(l,v) for l,v in wf_rows]) + f"
Net impact: {usd(total)}
" # Evidence (neutral copy; placeholders) evidence_html = """
Evidence snapshot
Neutral claims; update with citations per site.
""" # CTA (shown after first run) cta_html = f"""
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.
Book a 15-min walkthrough
""" # 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("""
CARPL ROI Calculator · Mammography AI
Clinical · Financial · Operational
""") 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()