SChodavarpu commited on
Commit
3ab0eff
·
verified ·
1 Parent(s): 6cd2c9b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +401 -0
app.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from dataclasses import dataclass, asdict
3
+ import gradio as gr
4
+
5
+ """
6
+ FFR-CT AI ROI Calculator
7
+ - Built for Hugging Face Spaces (Gradio)
8
+ - Focus: Clinical, Financial, and Operational ROI
9
+
10
+ Assumptions & Notes
11
+ - "With AI" refers to adopting FFR-CT + (optionally) AI-QPA billing.
12
+ - "Without AI" baseline assumes a CCTA-first pathway without FFR-CT.
13
+ - Savings from reduced unnecessary diagnostic ICA leverage the provided 69% reduction figure,
14
+ scaled by adoption (uptake).
15
+ - Additional testing costs are approximated via stress-test costs when one-test diagnosis fails.
16
+ - Downstream revascularization (PCI/CABG) revenue is optional (off by default), given it depends on site type.
17
+ - Ops value accounts for 2 parts: (a) faster decision-making (bed-hours value) and (b) clinician touch-time reduction.
18
+
19
+ You can tune any assumptions in the Advanced section.
20
+ """
21
+
22
+ @dataclass
23
+ class Defaults:
24
+ # Volumes
25
+ monthly_eligible_ccta: int = 100 # Monthly eligible CCTA patients
26
+ uptake_pct: float = 60.0 # % of eligible getting FFR-CT (policy/site dependent)
27
+
28
+ # Reimbursement (USD)
29
+ reimb_ffrct: float = 1017.0
30
+ reimb_ai_qpa: float = 950.0
31
+ pct_billed_ai_qpa: float = 60.0 # % of FFR-CT also billed with AI-QPA
32
+
33
+ # Core performance
34
+ one_test_dx_pct: float = 97.0 # with FFR-CT (i.e., 3% need add'l testing)
35
+ dec_unnec_ica_pct: float = 69.0 # reduction in unnecessary diagnostic ICAs
36
+ more_likely_revasc_pct: float = 78.0 # relative lift in identifying revasc candidates
37
+ revasc_prevalence_pct: float = 10.0 # true prevalence among eligible
38
+
39
+ # Costs (USD)
40
+ vendor_per_case_cost: float = 350.0
41
+ platform_monthly_cost: float = 1000.0 # = $12k/yr
42
+ stress_test_cost: float = 400.0
43
+ net_cost_per_diag_ica: float = 5000.0 # allow negative if ICA is profit-positive
44
+
45
+ # Ops economics (USD)
46
+ bed_hour_value: float = 100.0
47
+ clinician_hour_cost: float = 150.0
48
+
49
+ # Time effects
50
+ ai_time_to_decision_min: float = 90.0 # faster time from CCTA -> decision (minutes)
51
+ clinician_touch_reduction_pct: float = 30.0
52
+ baseline_clinician_touch_min: float = 30.0
53
+
54
+ # Baseline (without AI) assumptions
55
+ baseline_diag_ica_rate_pct: float = 30.0 # % of eligible that go to diagnostic ICA
56
+ baseline_additional_testing_rate_pct: float = 30.0 # % needing add'l testing without FFR-CT
57
+
58
+ # Optional downstream value
59
+ include_revasc_value: bool = False
60
+ value_per_revasc: float = 0.0 # Only used if include_revasc_value is True
61
+
62
+ DEFAULTS = Defaults()
63
+
64
+
65
+ def _pct(x):
66
+ return max(0.0, min(100.0, float(x)))
67
+
68
+
69
+ def _pos(x):
70
+ return max(0.0, float(x))
71
+
72
+
73
+ def compute_roi(
74
+ monthly_eligible_ccta,
75
+ uptake_pct,
76
+ reimb_ffrct,
77
+ reimb_ai_qpa,
78
+ pct_billed_ai_qpa,
79
+ one_test_dx_pct,
80
+ dec_unnec_ica_pct,
81
+ more_likely_revasc_pct,
82
+ revasc_prevalence_pct,
83
+ vendor_per_case_cost,
84
+ platform_monthly_cost,
85
+ stress_test_cost,
86
+ net_cost_per_diag_ica,
87
+ bed_hour_value,
88
+ clinician_hour_cost,
89
+ ai_time_to_decision_min,
90
+ clinician_touch_reduction_pct,
91
+ baseline_clinician_touch_min,
92
+ baseline_diag_ica_rate_pct,
93
+ baseline_additional_testing_rate_pct,
94
+ include_revasc_value,
95
+ value_per_revasc,
96
+ ):
97
+ # Sanitize
98
+ monthly_eligible_ccta = _pos(monthly_eligible_ccta)
99
+ uptake = _pct(uptake_pct) / 100.0
100
+ reimb_ffrct = float(reimb_ffrct)
101
+ reimb_ai_qpa = float(reimb_ai_qpa)
102
+ pct_billed_ai_qpa = _pct(pct_billed_ai_qpa) / 100.0
103
+ one_test_dx = _pct(one_test_dx_pct) / 100.0
104
+ need_addl_with_ai = 1.0 - one_test_dx
105
+ dec_unnec_ica = _pct(dec_unnec_ica_pct) / 100.0
106
+ more_likely_revasc = _pct(more_likely_revasc_pct) / 100.0
107
+ revasc_prev = _pct(revasc_prevalence_pct) / 100.0
108
+
109
+ vendor_per_case_cost = float(vendor_per_case_cost)
110
+ platform_monthly_cost = float(platform_monthly_cost)
111
+ stress_test_cost = float(stress_test_cost)
112
+ net_cost_per_diag_ica = float(net_cost_per_diag_ica)
113
+
114
+ bed_hour_value = float(bed_hour_value)
115
+ clinician_hour_cost = float(clinician_hour_cost)
116
+ ai_time_to_decision_min = _pos(ai_time_to_decision_min)
117
+ clinician_touch_reduction_pct = _pct(clinician_touch_reduction_pct) / 100.0
118
+ baseline_clinician_touch_min = _pos(baseline_clinician_touch_min)
119
+
120
+ baseline_diag_ica_rate = _pct(baseline_diag_ica_rate_pct) / 100.0
121
+ baseline_additional_testing_rate = _pct(baseline_additional_testing_rate_pct) / 100.0
122
+
123
+ include_revasc_value = bool(include_revasc_value)
124
+ value_per_revasc = float(value_per_revasc)
125
+
126
+ # Volumes
127
+ annual_eligible = monthly_eligible_ccta * 12.0
128
+ annual_uptake_cases = annual_eligible * uptake
129
+
130
+ # Baseline (without AI)
131
+ baseline_additional_tests = annual_eligible * baseline_additional_testing_rate
132
+ baseline_additional_tests_cost = baseline_additional_tests * stress_test_cost
133
+
134
+ baseline_diag_ica_total = annual_eligible * baseline_diag_ica_rate
135
+ baseline_revasc_true = annual_eligible * revasc_prev
136
+ baseline_unnecessary_ica = max(0.0, baseline_diag_ica_total - baseline_revasc_true)
137
+ baseline_unnecessary_ica_cost = baseline_unnecessary_ica * net_cost_per_diag_ica
138
+
139
+ baseline_revenue = 0.0 # no FFR-CT/AI-QPA billing without AI
140
+ baseline_costs = baseline_additional_tests_cost + baseline_unnecessary_ica_cost
141
+ baseline_ops_value = 0.0
142
+ baseline_net = baseline_revenue - baseline_costs + baseline_ops_value
143
+
144
+ # With AI
145
+ with_ai_revenue = (
146
+ annual_uptake_cases * reimb_ffrct
147
+ + annual_uptake_cases * pct_billed_ai_qpa * reimb_ai_qpa
148
+ )
149
+
150
+ with_ai_vendor_costs = annual_uptake_cases * vendor_per_case_cost
151
+ with_ai_platform_costs = platform_monthly_cost * 12.0
152
+
153
+ with_ai_additional_tests = annual_uptake_cases * need_addl_with_ai
154
+ with_ai_additional_tests_cost = with_ai_additional_tests * stress_test_cost
155
+
156
+ # Unnecessary ICA reduction scaled by uptake
157
+ avoided_unnec_ica = baseline_unnecessary_ica * dec_unnec_ica * uptake
158
+ with_ai_unnecessary_ica = max(0.0, baseline_unnecessary_ica - avoided_unnec_ica)
159
+ with_ai_unnecessary_ica_cost = with_ai_unnecessary_ica * net_cost_per_diag_ica
160
+
161
+ # Downstream revasc capture (optional; conservative default = off)
162
+ extra_revasc_identified = annual_eligible * revasc_prev * more_likely_revasc * uptake
163
+ revasc_value = extra_revasc_identified * value_per_revasc if include_revasc_value else 0.0
164
+
165
+ # Ops value
166
+ time_value = (annual_uptake_cases * (ai_time_to_decision_min / 60.0) * bed_hour_value)
167
+ clinician_time_saved_hours = annual_uptake_cases * (baseline_clinician_touch_min / 60.0) * clinician_touch_reduction_pct
168
+ clinician_time_value = clinician_time_saved_hours * clinician_hour_cost
169
+
170
+ with_ai_ops_value = time_value + clinician_time_value
171
+
172
+ with_ai_costs = with_ai_vendor_costs + with_ai_platform_costs + with_ai_additional_tests_cost + with_ai_unnecessary_ica_cost
173
+ with_ai_net = with_ai_revenue - with_ai_costs + with_ai_ops_value + revasc_value
174
+
175
+ # Incremental view (AI program impact)
176
+ incr_revenue = with_ai_revenue - baseline_revenue
177
+ incr_costs = with_ai_costs - baseline_costs
178
+ incr_ops_value = with_ai_ops_value - baseline_ops_value
179
+ incr_revasc_value = revasc_value
180
+
181
+ net_impact = incr_revenue - incr_costs + incr_ops_value + incr_revasc_value
182
+
183
+ ai_program_costs = with_ai_vendor_costs + with_ai_platform_costs # denominator for ROI
184
+ roi_pct = (net_impact / ai_program_costs * 100.0) if ai_program_costs > 0 else 0.0
185
+
186
+ # Payback
187
+ per_case_net_impact = (net_impact / annual_uptake_cases) if annual_uptake_cases > 0 else 0.0
188
+ cases_to_payback = (ai_program_costs / per_case_net_impact) if per_case_net_impact > 0 else math.inf
189
+ months_to_payback = (cases_to_payback / (monthly_eligible_ccta * uptake)) if (monthly_eligible_ccta * uptake) > 0 and cases_to_payback != math.inf else math.inf
190
+
191
+ # Build summaries
192
+ with_ai_summary = {
193
+ "Annual revenue (FFR-CT + AI-QPA)": with_ai_revenue,
194
+ "Program costs (vendor + platform)": with_ai_vendor_costs + with_ai_platform_costs,
195
+ "Add'l testing cost": with_ai_additional_tests_cost,
196
+ "Unnecessary ICA cost": with_ai_unnecessary_ica_cost,
197
+ "Ops value (time + clinician)": with_ai_ops_value,
198
+ "Downstream revasc value": revasc_value,
199
+ "Net (with AI)": with_ai_net,
200
+ }
201
+
202
+ without_ai_summary = {
203
+ "Annual revenue": baseline_revenue,
204
+ "Add'l testing cost": baseline_additional_tests_cost,
205
+ "Unnecessary ICA cost": baseline_unnecessary_ica_cost,
206
+ "Ops value": baseline_ops_value,
207
+ "Net (without AI)": baseline_net,
208
+ }
209
+
210
+ overall = {
211
+ "Incremental revenue": incr_revenue,
212
+ "Incremental costs (incl. tests/ICA)": incr_costs,
213
+ "Incremental ops value": incr_ops_value,
214
+ "Downstream revasc value": incr_revasc_value,
215
+ "Net impact": net_impact,
216
+ "ROI % (on AI program costs)": roi_pct,
217
+ "Cases to payback": cases_to_payback,
218
+ "Months to payback": months_to_payback,
219
+ }
220
+
221
+ # Pretty text blocks
222
+ def money(x):
223
+ if x == math.inf:
224
+ return "∞"
225
+ return f"${x:,.0f}"
226
+
227
+ def num(x):
228
+ if x == math.inf:
229
+ return "∞"
230
+ return f"{x:,.1f}"
231
+
232
+ with_ai_md = f"""
233
+ ### With AI
234
+ - **Annual revenue (FFR-CT + AI-QPA):** {money(with_ai_revenue)}
235
+ - **Program costs (vendor + platform):** {money(with_ai_vendor_costs + with_ai_platform_costs)}
236
+ - **Add'l testing cost:** {money(with_ai_additional_tests_cost)}
237
+ - **Unnecessary ICA cost:** {money(with_ai_unnecessary_ica_cost)}
238
+ - **Ops value (time + clinician):** {money(with_ai_ops_value)}
239
+ - **Downstream revasc value:** {money(revasc_value)}
240
+ - **Net (with AI):** **{money(with_ai_net)}**
241
+ """
242
+
243
+ without_ai_md = f"""
244
+ ### Without AI
245
+ - **Annual revenue:** {money(baseline_revenue)}
246
+ - **Add'l testing cost:** {money(baseline_additional_tests_cost)}
247
+ - **Unnecessary ICA cost:** {money(baseline_unnecessary_ica_cost)}
248
+ - **Ops value:** {money(baseline_ops_value)}
249
+ - **Net (without AI):** **{money(baseline_net)}**
250
+ """
251
+
252
+ overall_md = f"""
253
+ ### Overall Impact
254
+ - **Incremental revenue:** {money(incr_revenue)}
255
+ - **Incremental costs (incl. tests/ICA):** {money(incr_costs)}
256
+ - **Incremental ops value:** {money(incr_ops_value)}
257
+ - **Downstream revasc value:** {money(incr_revasc_value)}
258
+ - **Net impact:** **{money(net_impact)}**
259
+ - **ROI % (on AI program costs):** **{roi_pct:,.1f}%**
260
+ - **Cases to payback:** {num(cases_to_payback)}
261
+ - **Months to payback:** {num(months_to_payback)}
262
+ """
263
+
264
+ return with_ai_md, without_ai_md, overall_md
265
+
266
+
267
+ def build_ui():
268
+ with gr.Blocks(theme=gr.themes.Soft(), css="""
269
+ .card {border: 1px solid #e5e7eb; border-radius: 16px; padding: 16px; box-shadow: 0 1px 2px rgba(0,0,0,0.05);}
270
+ .title {font-weight: 700; font-size: 1.1rem;}
271
+ .muted {color: #6b7280;}
272
+ """) as demo:
273
+ gr.Markdown("""
274
+ # FFR‑CT AI ROI Calculator
275
+ **For providers, imaging centers, and AMCs.**
276
+ Use your site's numbers or try the defaults. Results show **With AI**, **Without AI**, and **Overall** impact across **clinical, financial, and operational** dimensions.
277
+ """)
278
+
279
+ with gr.Row():
280
+ with gr.Column():
281
+ monthly_eligible_ccta = gr.Slider(
282
+ label="Monthly eligible CCTA volume",
283
+ minimum=0,
284
+ maximum=2000,
285
+ step=10,
286
+ value=DEFAULTS.monthly_eligible_ccta,
287
+ info="Approx. number of CCTA patients per month who would be eligible for FFR‑CT"
288
+ )
289
+ uptake_pct = gr.Slider(
290
+ label="Uptake (eligible getting FFR‑CT, %)",
291
+ minimum=0,
292
+ maximum=100,
293
+ step=1,
294
+ value=DEFAULTS.uptake_pct,
295
+ info="Share of eligible patients who actually get FFR‑CT"
296
+ )
297
+ with gr.Accordion("Reimbursement & Costs", open=True):
298
+ reimb_ffrct = gr.Number(label="Reimbursement: FFR‑CT ($)", value=DEFAULTS.reimb_ffrct, info="CMS 2025")
299
+ reimb_ai_qpa = gr.Number(label="Reimbursement: AI‑QPA ($)", value=DEFAULTS.reimb_ai_qpa, info="CMS 2025")
300
+ pct_billed_ai_qpa = gr.Slider(label="% billed AI‑QPA",
301
+ minimum=0, maximum=100, step=5, value=DEFAULTS.pct_billed_ai_qpa,
302
+ info="Share of FFR‑CT cases where AI‑QPA is billed")
303
+ vendor_per_case_cost = gr.Number(label="Vendor per‑case cost ($)", value=DEFAULTS.vendor_per_case_cost)
304
+ platform_monthly_cost = gr.Number(label="Platform cost / month ($)", value=DEFAULTS.platform_monthly_cost)
305
+ stress_test_cost = gr.Number(label="Stress test cost ($)", value=DEFAULTS.stress_test_cost)
306
+ net_cost_per_diag_ica = gr.Number(label="Net cost per diagnostic ICA ($)", value=DEFAULTS.net_cost_per_diag_ica,
307
+ info="Allow negative if ICA is net profit‑positive at your site")
308
+
309
+ with gr.Accordion("Clinical Performance & Ops (Advanced)", open=False):
310
+ one_test_dx_pct = gr.Slider(label="One‑test diagnosis with FFR‑CT (%)",
311
+ minimum=80, maximum=100, step=1, value=DEFAULTS.one_test_dx_pct,
312
+ info="Percent of cases resolved with CCTA+FFR‑CT, no extra tests")
313
+ dec_unnec_ica_pct = gr.Slider(label="Decrease in unnecessary diagnostic ICAs (%)",
314
+ minimum=0, maximum=100, step=1, value=DEFAULTS.dec_unnec_ica_pct,
315
+ info="Relative reduction vs baseline, scaled by uptake")
316
+ more_likely_revasc_pct = gr.Slider(label="More likely to identify revasc candidates (%)",
317
+ minimum=0, maximum=200, step=1, value=DEFAULTS.more_likely_revasc_pct,
318
+ info="Relative lift, used only if downstream value is enabled")
319
+ revasc_prevalence_pct = gr.Slider(label="True revasc prevalence among eligible (%)",
320
+ minimum=0, maximum=100, step=1, value=DEFAULTS.revasc_prevalence_pct)
321
+
322
+ bed_hour_value = gr.Number(label="Bed‑hour value ($/hour)", value=DEFAULTS.bed_hour_value)
323
+ clinician_hour_cost = gr.Number(label="Clinician hour cost ($/hour)", value=DEFAULTS.clinician_hour_cost)
324
+ ai_time_to_decision_min = gr.Number(label="AI time CCTA → decision saved (min)", value=DEFAULTS.ai_time_to_decision_min)
325
+ clinician_touch_reduction_pct = gr.Slider(label="Clinician touch‑time reduction with AI (%)",
326
+ minimum=0, maximum=100, step=5, value=DEFAULTS.clinician_touch_reduction_pct)
327
+ baseline_clinician_touch_min = gr.Number(label="Baseline clinician touch‑time per case (min)",
328
+ value=DEFAULTS.baseline_clinician_touch_min)
329
+
330
+ baseline_diag_ica_rate_pct = gr.Slider(label="Baseline diagnostic ICA rate (%)",
331
+ minimum=0, maximum=100, step=1, value=DEFAULTS.baseline_diag_ica_rate_pct,
332
+ info="Share of eligible patients who go to diagnostic ICA without AI")
333
+ baseline_additional_testing_rate_pct = gr.Slider(label="Baseline additional testing rate (%)",
334
+ minimum=0, maximum=100, step=1, value=DEFAULTS.baseline_additional_testing_rate_pct,
335
+ info="Share needing extra tests without AI")
336
+
337
+ include_revasc_value = gr.Checkbox(label="Include downstream revascularization value", value=DEFAULTS.include_revasc_value)
338
+ value_per_revasc = gr.Number(label="Value per revascularization captured ($)", value=DEFAULTS.value_per_revasc,
339
+ info="Set only if your site captures revasc value; else leave 0")
340
+
341
+ with gr.Column():
342
+ with gr.Row():
343
+ with_ai_card = gr.Markdown(elem_classes=["card"]) # With AI
344
+ without_ai_card = gr.Markdown(elem_classes=["card"]) # Without AI
345
+ overall_card = gr.Markdown(elem_classes=["card"]) # Overall impact
346
+
347
+ inputs = [
348
+ monthly_eligible_ccta,
349
+ uptake_pct,
350
+ reimb_ffrct,
351
+ reimb_ai_qpa,
352
+ pct_billed_ai_qpa,
353
+ one_test_dx_pct,
354
+ dec_unnec_ica_pct,
355
+ more_likely_revasc_pct,
356
+ revasc_prevalence_pct,
357
+ vendor_per_case_cost,
358
+ platform_monthly_cost,
359
+ stress_test_cost,
360
+ net_cost_per_diag_ica,
361
+ bed_hour_value,
362
+ clinician_hour_cost,
363
+ ai_time_to_decision_min,
364
+ clinician_touch_reduction_pct,
365
+ baseline_clinician_touch_min,
366
+ baseline_diag_ica_rate_pct,
367
+ baseline_additional_testing_rate_pct,
368
+ include_revasc_value,
369
+ value_per_revasc,
370
+ ]
371
+
372
+ def _update(*args):
373
+ return compute_roi(*args)
374
+
375
+ # Initial compute & reactive updates
376
+ for comp in inputs:
377
+ comp.change(_update, inputs=inputs, outputs=[with_ai_card, without_ai_card, overall_card])
378
+
379
+ # Run once on load with defaults
380
+ demo.load(_update, inputs=inputs, outputs=[with_ai_card, without_ai_card, overall_card])
381
+
382
+ gr.Markdown(
383
+ """
384
+ ---
385
+ **How to use this app**
386
+ - Start with your **monthly eligible** volume and **uptake**.
387
+ - Check or edit **reimbursement** and **costs** if your site differs.
388
+ - Optionally expand **Advanced** to fine-tune baseline and ops assumptions.
389
+ - The cards update live with **With AI**, **Without AI**, and **Overall** impact, including **ROI** and **payback**.
390
+ """
391
+ )
392
+
393
+ return demo
394
+
395
+
396
+ def main():
397
+ demo = build_ui()
398
+ demo.queue().launch()
399
+
400
+ if __name__ == "__main__":
401
+ main()