Spaces:
Sleeping
Sleeping
| # 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() |