Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math import numpy as np import pandas as pd import gradio as gr
|
| 2 |
+
|
| 3 |
+
------------------------------
|
| 4 |
+
|
| 5 |
+
Utility helpers
|
| 6 |
+
|
| 7 |
+
------------------------------
|
| 8 |
+
|
| 9 |
+
def usd(x: float) -> str: try: return "$" + f"{x:,.0f}" except Exception: return "$0"
|
| 10 |
+
|
| 11 |
+
def usd2(x: float) -> str: try: return "$" + f"{x:,.2f}" except Exception: return "$0.00"
|
| 12 |
+
|
| 13 |
+
def pct(x: float) -> str: return f"{x*100:.1f}%"
|
| 14 |
+
|
| 15 |
+
------------------------------
|
| 16 |
+
|
| 17 |
+
Core calculator
|
| 18 |
+
|
| 19 |
+
------------------------------
|
| 20 |
+
|
| 21 |
+
def compute_roi( scans_per_month: int, # Clinical (detection) base_ppr: float, # baseline positive pickup rate (as fraction) ai_ppr: float, # with-AI positive pickup rate (as fraction) base_audit_rate: float, ai_audit_rate: float, # Operational (time) base_read_seconds: float, read_reduction_pct: float, # as fraction radiologist_hourly_cost: float, # Financial (per-scan cost) base_cost_per_scan: float, cost_reduction_pct: float, # Financial (follow-ups) followup_price: float, followup_uplift_pct: float, # as fraction of scans # Recalls / false positives base_recall_rate: float, ai_recall_rate: float, recall_cost_per_case: float, # Early detection value early_detect_uplift_per_1000: float, # extra cancers detected per 1000 treatment_cost_delta_early_vs_late: float, # savings per earlier case # Program costs ai_subscription_monthly: float, integration_overhead_monthly: float, cloud_compute_monthly: float, ): # --- Clinical metrics --- errors_reduced = max(0, scans_per_month * (base_ppr - ai_ppr)) # if AI lowers miss rate via higher pickup, set accordingly discrepant_flags = max(0, scans_per_month * (ai_audit_rate - base_audit_rate)) earlier_detections = max(0.0, scans_per_month * (early_detect_uplift_per_1000 / 1000.0)) recalls_avoided = max(0, scans_per_month * (base_recall_rate - ai_recall_rate))
|
| 22 |
+
|
| 23 |
+
# --- Operational metrics ---
|
| 24 |
+
hours_saved = scans_per_month * (base_read_seconds * read_reduction_pct) / 3600.0
|
| 25 |
+
workload_reduction_pct = read_reduction_pct
|
| 26 |
+
fte_saved = hours_saved / 160.0 # rough FTE-month heuristic
|
| 27 |
+
capacity_increase_pct = (1.0 / max(1e-6, (1.0 - read_reduction_pct)) - 1.0) # approximate capacity headroom
|
| 28 |
+
|
| 29 |
+
# --- Financial metrics ---
|
| 30 |
+
value_time_saved = hours_saved * radiologist_hourly_cost
|
| 31 |
+
|
| 32 |
+
baseline_monthly_cost = scans_per_month * base_cost_per_scan
|
| 33 |
+
new_monthly_cost = baseline_monthly_cost * (1.0 - cost_reduction_pct)
|
| 34 |
+
per_scan_cost_savings = baseline_monthly_cost - new_monthly_cost
|
| 35 |
+
|
| 36 |
+
addl_followups = scans_per_month * followup_uplift_pct
|
| 37 |
+
addl_followup_revenue = addl_followups * followup_price
|
| 38 |
+
|
| 39 |
+
recall_cost_savings = recalls_avoided * recall_cost_per_case
|
| 40 |
+
early_detection_savings = earlier_detections * treatment_cost_delta_early_vs_late
|
| 41 |
+
|
| 42 |
+
total_monthly_benefit = (
|
| 43 |
+
value_time_saved
|
| 44 |
+
+ per_scan_cost_savings
|
| 45 |
+
+ addl_followup_revenue
|
| 46 |
+
+ recall_cost_savings
|
| 47 |
+
+ early_detection_savings
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
total_monthly_costs = ai_subscription_monthly + integration_overhead_monthly + cloud_compute_monthly
|
| 51 |
+
net_roi = total_monthly_benefit - total_monthly_costs
|
| 52 |
+
roi_pct = (net_roi / total_monthly_costs) if total_monthly_costs > 0 else np.nan
|
| 53 |
+
|
| 54 |
+
# One-line summary
|
| 55 |
+
clinical_bullet = (
|
| 56 |
+
f"~{int(round(recalls_avoided))} recalls avoided, "
|
| 57 |
+
f"{earlier_detections:.1f} earlier cancers detected, "
|
| 58 |
+
f"{int(round(errors_reduced))} fewer missed positives"
|
| 59 |
+
)
|
| 60 |
+
summary = (
|
| 61 |
+
f"For your practice with {scans_per_month:,} mammography scans/month, your net benefit is "
|
| 62 |
+
f"{usd(net_roi)} per month. Clinical impact: {clinical_bullet}."
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Build Markdown sections
|
| 66 |
+
financial_md = f"""
|
| 67 |
+
|
| 68 |
+
Financial (USD/month)
|
| 69 |
+
|
| 70 |
+
Metric Value
|
| 71 |
+
|
| 72 |
+
Value of radiologist time saved {usd(value_time_saved)}
|
| 73 |
+
Savings from per-scan radiologist cost {usd(per_scan_cost_savings)}
|
| 74 |
+
Additional follow-up revenue {usd(addl_followup_revenue)}
|
| 75 |
+
Savings from avoided recalls {usd(recall_cost_savings)}
|
| 76 |
+
Savings from earlier detection {usd(early_detection_savings)}
|
| 77 |
+
Total monthly benefit {usd(total_monthly_benefit)}
|
| 78 |
+
AI subscription {usd(ai_subscription_monthly)}
|
| 79 |
+
Integration / training / overhead {usd(integration_overhead_monthly)}
|
| 80 |
+
Cloud / compute {usd(cloud_compute_monthly)}
|
| 81 |
+
Total monthly costs {usd(total_monthly_costs)}
|
| 82 |
+
Net monthly ROI {usd(net_roi)}
|
| 83 |
+
ROI % {'' if math.isnan(roi_pct) else f'{roi_pct*100:.1f}%'}
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
clinical_md = f"""
|
| 88 |
+
|
| 89 |
+
Clinical (per month)
|
| 90 |
+
|
| 91 |
+
Metric Value
|
| 92 |
+
|
| 93 |
+
Fewer missed positives (Δ pickup) {int(round(errors_reduced))}
|
| 94 |
+
Discrepant cases flagged by AI (audit uplift) {int(round(discrepant_flags))}
|
| 95 |
+
Earlier cancers detected {earlier_detections:.1f}
|
| 96 |
+
Recalls avoided {int(round(recalls_avoided))}
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
operational_md = f"""
|
| 101 |
+
|
| 102 |
+
Operational
|
| 103 |
+
|
| 104 |
+
Metric Value
|
| 105 |
+
|
| 106 |
+
Hours of radiologist reading time saved / month {hours_saved:.1f}
|
| 107 |
+
Workload reduction {pct(workload_reduction_pct)}
|
| 108 |
+
Approx. FTE-month saved {fte_saved:.2f}
|
| 109 |
+
Effective reading capacity increase {pct(capacity_increase_pct)}
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# CTA
|
| 114 |
+
cta_md = (
|
| 115 |
+
"**Want to see this live in your workflow?** "
|
| 116 |
+
"[Book a demo →](https://example.com/book-demo)"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
return summary, financial_md, clinical_md, operational_md, cta_md
|
| 120 |
+
|
| 121 |
+
------------------------------
|
| 122 |
+
|
| 123 |
+
UI Layout
|
| 124 |
+
|
| 125 |
+
------------------------------
|
| 126 |
+
|
| 127 |
+
def build_ui(): with gr.Blocks(title="Mammography AI ROI Calculator", theme=gr.themes.Soft(), css=""" .gradio-container {max-width: 1200px !important} .section-head {font-weight: 700; margin-top: .5rem} .small-note {opacity: .70; font-size: 0.9em} """) as demo: gr.Markdown("""
|
| 128 |
+
|
| 129 |
+
Mammography AI ROI Calculator
|
| 130 |
+
|
| 131 |
+
A clean, defensible ROI model for breast imaging AI — with Financial, Clinical, and Operational impact, all in USD. """) with gr.Row(): with gr.Column(scale=1): gr.Markdown("## Inputs") scans_per_month = gr.Number(value=7500, label="Scans per month", precision=0)
|
| 132 |
+
|
| 133 |
+
with gr.Accordion("Clinical inputs", open=False):
|
| 134 |
+
base_ppr = gr.Slider(0, 1, value=0.10, step=0.001, label="Baseline positive pickup rate (fraction)")
|
| 135 |
+
ai_ppr = gr.Slider(0, 1, value=0.095, step=0.001, label="With-AI positive pickup rate (fraction)")
|
| 136 |
+
base_audit_rate = gr.Slider(0, 1, value=0.00, step=0.001, label="Baseline audit flag rate (fraction)")
|
| 137 |
+
ai_audit_rate = gr.Slider(0, 1, value=0.05, step=0.001, label="With-AI audit flag rate (fraction)")
|
| 138 |
+
base_recall_rate = gr.Slider(0, 1, value=0.028, step=0.001, label="Baseline recall rate (fraction)")
|
| 139 |
+
ai_recall_rate = gr.Slider(0, 1, value=0.025, step=0.001, label="With-AI recall rate (fraction)")
|
| 140 |
+
recall_cost_per_case = gr.Number(value=250.0, label="Cost per recall case (USD)")
|
| 141 |
+
early_detect_uplift_per_1000 = gr.Number(value=0.7, label="Earlier cancers detected (extra per 1000 scans)")
|
| 142 |
+
treatment_cost_delta_early_vs_late = gr.Number(value=15000.0, label="Treatment cost savings per earlier case (USD)")
|
| 143 |
+
|
| 144 |
+
with gr.Accordion("Operational inputs", open=False):
|
| 145 |
+
base_read_seconds = gr.Number(value=100, label="Baseline read time per scan (seconds)")
|
| 146 |
+
read_reduction_pct = gr.Slider(0, 0.8, value=0.15, step=0.005, label="Reading time reduction with AI (fraction)")
|
| 147 |
+
radiologist_hourly_cost = gr.Number(value=180, label="Radiologist hourly cost (USD)")
|
| 148 |
+
|
| 149 |
+
with gr.Accordion("Financial inputs", open=False):
|
| 150 |
+
base_cost_per_scan = gr.Number(value=15, label="Radiologist cost per scan (USD)")
|
| 151 |
+
cost_reduction_pct = gr.Slider(0, 0.8, value=0.15, step=0.005, label="Per-scan radiologist cost reduction (fraction)")
|
| 152 |
+
followup_price = gr.Number(value=200, label="Price per follow-up scan (USD)")
|
| 153 |
+
followup_uplift_pct = gr.Slider(0, 0.2, value=0.0095, step=0.0005, label="Follow-up uplift (fraction of scans)")
|
| 154 |
+
|
| 155 |
+
with gr.Accordion("Program costs", open=True):
|
| 156 |
+
ai_subscription_monthly = gr.Number(value=0, label="AI subscription (USD/month)")
|
| 157 |
+
integration_overhead_monthly = gr.Number(value=0, label="Integration / training / overhead (USD/month)")
|
| 158 |
+
cloud_compute_monthly = gr.Number(value=0, label="Cloud / compute (USD/month)")
|
| 159 |
+
|
| 160 |
+
calc_btn = gr.Button("Calculate ROI", variant="primary")
|
| 161 |
+
|
| 162 |
+
with gr.Column(scale=1):
|
| 163 |
+
summary = gr.Markdown("", elem_id="summary")
|
| 164 |
+
with gr.Tab("Financial"):
|
| 165 |
+
financial_md = gr.Markdown()
|
| 166 |
+
with gr.Tab("Clinical"):
|
| 167 |
+
clinical_md = gr.Markdown()
|
| 168 |
+
with gr.Tab("Operational"):
|
| 169 |
+
operational_md = gr.Markdown()
|
| 170 |
+
gr.Markdown("---")
|
| 171 |
+
cta_md = gr.Markdown()
|
| 172 |
+
|
| 173 |
+
calc_btn.click(
|
| 174 |
+
fn=compute_roi,
|
| 175 |
+
inputs=[
|
| 176 |
+
scans_per_month,
|
| 177 |
+
base_ppr,
|
| 178 |
+
ai_ppr,
|
| 179 |
+
base_audit_rate,
|
| 180 |
+
ai_audit_rate,
|
| 181 |
+
base_read_seconds,
|
| 182 |
+
read_reduction_pct,
|
| 183 |
+
radiologist_hourly_cost,
|
| 184 |
+
base_cost_per_scan,
|
| 185 |
+
cost_reduction_pct,
|
| 186 |
+
followup_price,
|
| 187 |
+
followup_uplift_pct,
|
| 188 |
+
base_recall_rate,
|
| 189 |
+
ai_recall_rate,
|
| 190 |
+
recall_cost_per_case,
|
| 191 |
+
early_detect_uplift_per_1000,
|
| 192 |
+
treatment_cost_delta_early_vs_late,
|
| 193 |
+
ai_subscription_monthly,
|
| 194 |
+
integration_overhead_monthly,
|
| 195 |
+
cloud_compute_monthly,
|
| 196 |
+
],
|
| 197 |
+
outputs=[summary, financial_md, clinical_md, operational_md, cta_md],
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
# Auto-run with defaults
|
| 201 |
+
demo.load(
|
| 202 |
+
fn=compute_roi,
|
| 203 |
+
inputs=[
|
| 204 |
+
scans_per_month,
|
| 205 |
+
base_ppr,
|
| 206 |
+
ai_ppr,
|
| 207 |
+
base_audit_rate,
|
| 208 |
+
ai_audit_rate,
|
| 209 |
+
base_read_seconds,
|
| 210 |
+
read_reduction_pct,
|
| 211 |
+
radiologist_hourly_cost,
|
| 212 |
+
base_cost_per_scan,
|
| 213 |
+
cost_reduction_pct,
|
| 214 |
+
followup_price,
|
| 215 |
+
followup_uplift_pct,
|
| 216 |
+
base_recall_rate,
|
| 217 |
+
ai_recall_rate,
|
| 218 |
+
recall_cost_per_case,
|
| 219 |
+
early_detect_uplift_per_1000,
|
| 220 |
+
treatment_cost_delta_early_vs_late,
|
| 221 |
+
ai_subscription_monthly,
|
| 222 |
+
integration_overhead_monthly,
|
| 223 |
+
cloud_compute_monthly,
|
| 224 |
+
],
|
| 225 |
+
outputs=[summary, financial_md, clinical_md, operational_md, cta_md],
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
return demo
|
| 229 |
+
|
| 230 |
+
def main(): return build_ui()
|
| 231 |
+
|
| 232 |
+
if name == "main": app = build_ui() app.launch()
|
| 233 |
+
|