File size: 17,522 Bytes
dd059ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
"""
A collection of functions to calculate key health and valuation metrics
for a SaaS / micro-SaaS business based on a provided set of formulas.
"""

import gradio as gr


def calculate_arr(mrr: float) -> float:
    """
    Calculates Annual Recurring Revenue (ARR) from Monthly Recurring Revenue (MRR).
    (Formula 1)

    Args:
        mrr: Monthly Recurring Revenue.

    Returns:
        The calculated Annual Recurring Revenue.
    """
    return mrr * 12


def calculate_valuation_revenue(arr: float, multiple: float) -> float:
    """
    Calculates a simple revenue-based valuation.
    (Formula 2)

    Args:
        arr: Annual Recurring Revenue.
        multiple: The valuation multiple (e.g., 3.0 for 3x).

    Returns:
        The calculated revenue-based valuation.
    """
    return arr * multiple


def calculate_sde(
    revenue: float, owner_comp: float, cogs: float, op_ex: float
) -> float:
    """
    Calculates Seller's Discretionary Earnings (SDE).
    (Formula 3)

    Args:
        revenue: Total revenue for the period.
        owner_comp: Owner compensation to be added back.
        cogs: Cost of Goods Sold.
        op_ex: Operating Expenses.

    Returns:
        The calculated Seller's Discretionary Earnings.
    """
    return (revenue + owner_comp) - (cogs + op_ex)


def calculate_valuation_sde(sde: float, multiple: float) -> float:
    """
    Calculates an SDE-based valuation.
    (Formula 3)

    Args:
        sde: Seller's Discretionary Earnings.
        multiple: The SDE valuation multiple.

    Returns:
        The calculated SDE-based valuation.
    """
    return sde * multiple


def calculate_valuation_ebitda(ebitda: float, multiple: float) -> float:
    """
    Calculates an EBITDA-based valuation.
    (Formula 4)

    Args:
        ebitda: Earnings Before Interest, Taxes, Depreciation, and Amortization.
        multiple: The EBITDA valuation multiple.

    Returns:
        The calculated EBITDA-based valuation.
    """
    return ebitda * multiple


def calculate_customer_churn_rate(
    churned_customers: int, start_customers: int
) -> float:
    """
    Calculates the customer churn rate for a period.
    (Formula 5)

    Args:
        churned_customers: Number of customers lost during the period.
        start_customers: Number of customers at the start of the period.

    Returns:
        The customer churn rate as a decimal (e.g., 0.05 for 5%).
    """
    if start_customers == 0:
        return 0.0
    return churned_customers / start_customers


def calculate_gross_revenue_churn_rate(
    churned_revenue: float, mrr_start: float
) -> float:
    """
    Calculates the gross revenue churn rate for a period.
    (Formula 6)

    Args:
        churned_revenue: MRR lost from cancellations and downgrades.
        mrr_start: MRR at the start of the period.

    Returns:
        The gross revenue churn rate as a decimal.
    """
    if mrr_start == 0:
        return 0.0
    return churned_revenue / mrr_start


def calculate_net_revenue_churn_rate(
    churned_revenue: float, expansion_revenue: float, mrr_start: float
) -> float:
    """
    Calculates the net revenue churn rate for a period. Can be negative.
    (Formula 7)

    Args:
        churned_revenue: MRR lost from cancellations and downgrades.
        expansion_revenue: MRR gained from upgrades and upsells.
        mrr_start: MRR at the start of the period.

    Returns:
        The net revenue churn rate as a decimal.
    """
    if mrr_start == 0:
        return 0.0
    return (churned_revenue - expansion_revenue) / mrr_start


def calculate_nrr(
    mrr_start: float, expansion_revenue: float, churn_revenue: float
) -> float:
    """
    Calculates Net Revenue Retention (NRR).
    Note: 'churn_revenue' here includes all lost revenue (cancellations + contractions).
    (Formula 8)

    Args:
        mrr_start: MRR at the start of the period.
        expansion_revenue: MRR gained from existing customers.
        churn_revenue: MRR lost from existing customers.

    Returns:
        The Net Revenue Retention rate as a decimal (e.g., 1.05 for 105%).
    """
    if mrr_start == 0:
        return 0.0
    return (mrr_start + expansion_revenue - churn_revenue) / mrr_start


def calculate_cac(sm_spend: float, new_customers: int) -> float:
    """
    Calculates Customer Acquisition Cost (CAC).
    (Formula 9)

    Args:
        sm_spend: Total sales and marketing spend for the period.
        new_customers: Number of new customers acquired in the period.

    Returns:
        The average Customer Acquisition Cost.
    """
    if new_customers == 0:
        # Returning infinity is mathematically correct for 0 new customers
        # with non-zero spend, representing an infinitely high cost.
        return float("inf") if sm_spend > 0 else 0.0
    return sm_spend / new_customers


def calculate_arpa(mrr: float, active_accounts: int) -> float:
    """
    Calculates Average Revenue Per Account (ARPA).
    (Formula 10)

    Args:
        mrr: Monthly Recurring Revenue.
        active_accounts: Number of active accounts.

    Returns:
        The Average Revenue Per Account.
    """
    if active_accounts == 0:
        return 0.0
    return mrr / active_accounts


def calculate_customer_lifetime(customer_churn_rate: float) -> float:
    """
    Calculates the approximate customer lifetime in periods.
    (Formula 11)

    Args:
        customer_churn_rate: The per-period customer churn rate as a decimal.

    Returns:
        The average customer lifetime in number of periods.
    """
    if customer_churn_rate <= 0:
        # A churn rate of 0 implies infinite lifetime.
        return float("inf")
    return 1 / customer_churn_rate


def calculate_ltv(arpa: float, gross_margin: float, revenue_churn_rate: float) -> float:
    """
    Calculates the margin-adjusted Customer Lifetime Value (LTV).
    (Formula 12)

    Args:
        arpa: Average Revenue Per Account.
        gross_margin: The gross margin as a decimal (e.g., 0.80 for 80%).
        revenue_churn_rate: The revenue churn rate as a decimal.

    Returns:
        The margin-adjusted Customer Lifetime Value.
    """
    if revenue_churn_rate <= 0:
        # A churn rate of 0 implies infinite LTV.
        return float("inf")
    return (arpa * gross_margin) / revenue_churn_rate


def calculate_ltv_cac_ratio(ltv: float, cac: float) -> float:
    """
    Calculates the LTV to CAC ratio.
    (Formula 13)

    Args:
        ltv: Customer Lifetime Value.
        cac: Customer Acquisition Cost.

    Returns:
        The LTV to CAC ratio.
    """
    if cac == 0:
        # An LTV with zero acquisition cost is technically an infinite return.
        return float("inf") if ltv > 0 else 0.0
    return ltv / cac


def calculate_cac_payback_period(cac: float, arpa: float, gross_margin: float) -> float:
    """
    Calculates the CAC payback period in months (assuming ARPA is monthly).
    (Formula 14)

    Args:
        cac: Customer Acquisition Cost.
        arpa: Average Revenue Per Account (monthly).
        gross_margin: Gross margin as a decimal.

    Returns:
        The number of months to pay back CAC.
    """
    monthly_gross_profit_per_customer = arpa * gross_margin
    if monthly_gross_profit_per_customer <= 0:
        # If there's no gross profit, CAC is never paid back.
        return float("inf")
    return cac / monthly_gross_profit_per_customer


def calculate_gross_margin(revenue: float, cogs: float) -> float:
    """
    Calculates the gross margin.
    (Formula 15)

    Args:
        revenue: Total revenue.
        cogs: Cost of Goods Sold.

    Returns:
        The gross margin as a decimal.
    """
    if revenue == 0:
        return 0.0
    return (revenue - cogs) / revenue


def calculate_yoy_growth(arr_current: float, arr_prior: float) -> float:
    """
    Calculates Year-over-Year (YoY) growth rate.
    (Formula 16)

    Args:
        arr_current: ARR for the current year.
        arr_prior: ARR for the prior year.

    Returns:
        The YoY growth rate as a decimal.
    """
    if arr_prior == 0:
        return float("inf") if arr_current > 0 else 0.0
    return (arr_current - arr_prior) / arr_prior


def calculate_rule_of_40(growth_rate: float, ebitda_margin: float) -> float:
    """
    Calculates the Rule of 40 score.
    (Formula 17)

    Args:
        growth_rate: The growth rate as a percentage (e.g., 30 for 30%).
        ebitda_margin: The EBITDA margin as a percentage (e.g., 15 for 15%).

    Returns:
        The Rule of 40 score.
    """
    return growth_rate + ebitda_margin


with gr.Blocks(title="SaaS Metrics Calculator") as demo:
    gr.Markdown("# SaaS Metrics Calculator")
    gr.Markdown(
        "Calculate various key health and valuation metrics for your SaaS business. Each metric is in its own section below."
    )

    with gr.Accordion("Annual Recurring Revenue (ARR)", open=False):
        mrr_input = gr.Number(label="Monthly Recurring Revenue (MRR)")
        arr_output = gr.Number(label="Annual Recurring Revenue (ARR)")
        btn = gr.Button("Calculate")
        btn.click(calculate_arr, inputs=[mrr_input], outputs=[arr_output])

    with gr.Accordion("Revenue-Based Valuation", open=False):
        arr_input = gr.Number(label="Annual Recurring Revenue (ARR)")
        multiple_input = gr.Number(label="Valuation Multiple (e.g., 3.0 for 3x)")
        valuation_output = gr.Number(label="Revenue-Based Valuation")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_valuation_revenue,
            inputs=[arr_input, multiple_input],
            outputs=[valuation_output],
        )

    with gr.Accordion("Seller's Discretionary Earnings (SDE)", open=False):
        revenue_input = gr.Number(label="Total Revenue")
        owner_comp_input = gr.Number(label="Owner Compensation")
        cogs_input = gr.Number(label="Cost of Goods Sold (COGS)")
        op_ex_input = gr.Number(label="Operating Expenses")
        sde_output = gr.Number(label="Seller's Discretionary Earnings (SDE)")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_sde,
            inputs=[revenue_input, owner_comp_input, cogs_input, op_ex_input],
            outputs=[sde_output],
        )

    with gr.Accordion("SDE-Based Valuation", open=False):
        sde_input = gr.Number(label="Seller's Discretionary Earnings (SDE)")
        multiple_input = gr.Number(label="SDE Valuation Multiple")
        valuation_output = gr.Number(label="SDE-Based Valuation")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_valuation_sde,
            inputs=[sde_input, multiple_input],
            outputs=[valuation_output],
        )

    with gr.Accordion("EBITDA-Based Valuation", open=False):
        ebitda_input = gr.Number(label="EBITDA")
        multiple_input = gr.Number(label="EBITDA Valuation Multiple")
        valuation_output = gr.Number(label="EBITDA-Based Valuation")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_valuation_ebitda,
            inputs=[ebitda_input, multiple_input],
            outputs=[valuation_output],
        )

    with gr.Accordion("Customer Churn Rate", open=False):
        churned_customers_input = gr.Number(label="Churned Customers")
        start_customers_input = gr.Number(label="Starting Customers")
        churn_rate_output = gr.Number(label="Customer Churn Rate (decimal)")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_customer_churn_rate,
            inputs=[churned_customers_input, start_customers_input],
            outputs=[churn_rate_output],
        )

    with gr.Accordion("Gross Revenue Churn Rate", open=False):
        churned_revenue_input = gr.Number(label="Churned Revenue (MRR lost)")
        mrr_start_input = gr.Number(label="Starting MRR")
        churn_rate_output = gr.Number(label="Gross Revenue Churn Rate (decimal)")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_gross_revenue_churn_rate,
            inputs=[churned_revenue_input, mrr_start_input],
            outputs=[churn_rate_output],
        )

    with gr.Accordion("Net Revenue Churn Rate", open=False):
        churned_revenue_input = gr.Number(label="Churned Revenue (MRR lost)")
        expansion_revenue_input = gr.Number(label="Expansion Revenue (MRR gained)")
        mrr_start_input = gr.Number(label="Starting MRR")
        churn_rate_output = gr.Number(label="Net Revenue Churn Rate (decimal)")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_net_revenue_churn_rate,
            inputs=[churned_revenue_input, expansion_revenue_input, mrr_start_input],
            outputs=[churn_rate_output],
        )

    with gr.Accordion("Net Revenue Retention (NRR)", open=False):
        mrr_start_input = gr.Number(label="Starting MRR")
        expansion_revenue_input = gr.Number(label="Expansion Revenue")
        churn_revenue_input = gr.Number(label="Churn Revenue (MRR lost)")
        nrr_output = gr.Number(label="Net Revenue Retention (decimal)")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_nrr,
            inputs=[mrr_start_input, expansion_revenue_input, churn_revenue_input],
            outputs=[nrr_output],
        )

    with gr.Accordion("Customer Acquisition Cost (CAC)", open=False):
        sm_spend_input = gr.Number(label="Sales & Marketing Spend")
        new_customers_input = gr.Number(label="New Customers")
        cac_output = gr.Number(label="Customer Acquisition Cost (CAC)")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_cac,
            inputs=[sm_spend_input, new_customers_input],
            outputs=[cac_output],
        )

    with gr.Accordion("Average Revenue Per Account (ARPA)", open=False):
        mrr_input = gr.Number(label="Monthly Recurring Revenue (MRR)")
        active_accounts_input = gr.Number(label="Active Accounts")
        arpa_output = gr.Number(label="Average Revenue Per Account (ARPA)")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_arpa,
            inputs=[mrr_input, active_accounts_input],
            outputs=[arpa_output],
        )

    with gr.Accordion("Customer Lifetime", open=False):
        customer_churn_rate_input = gr.Number(label="Customer Churn Rate (decimal)")
        lifetime_output = gr.Number(label="Customer Lifetime (periods)")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_customer_lifetime,
            inputs=[customer_churn_rate_input],
            outputs=[lifetime_output],
        )

    with gr.Accordion("Customer Lifetime Value (LTV)", open=False):
        arpa_input = gr.Number(label="ARPA")
        gross_margin_input = gr.Number(label="Gross Margin (decimal)")
        revenue_churn_rate_input = gr.Number(label="Revenue Churn Rate (decimal)")
        ltv_output = gr.Number(label="Customer Lifetime Value (LTV)")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_ltv,
            inputs=[arpa_input, gross_margin_input, revenue_churn_rate_input],
            outputs=[ltv_output],
        )

    with gr.Accordion("LTV to CAC Ratio", open=False):
        ltv_input = gr.Number(label="LTV")
        cac_input = gr.Number(label="CAC")
        ratio_output = gr.Number(label="LTV to CAC Ratio")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_ltv_cac_ratio,
            inputs=[ltv_input, cac_input],
            outputs=[ratio_output],
        )

    with gr.Accordion("CAC Payback Period", open=False):
        cac_input = gr.Number(label="CAC")
        arpa_input = gr.Number(label="ARPA (monthly)")
        gross_margin_input = gr.Number(label="Gross Margin (decimal)")
        payback_output = gr.Number(label="CAC Payback Period (months)")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_cac_payback_period,
            inputs=[cac_input, arpa_input, gross_margin_input],
            outputs=[payback_output],
        )

    with gr.Accordion("Gross Margin", open=False):
        revenue_input = gr.Number(label="Total Revenue")
        cogs_input = gr.Number(label="Cost of Goods Sold (COGS)")
        margin_output = gr.Number(label="Gross Margin (decimal)")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_gross_margin,
            inputs=[revenue_input, cogs_input],
            outputs=[margin_output],
        )

    with gr.Accordion("Year-over-Year (YoY) Growth", open=False):
        arr_current_input = gr.Number(label="Current ARR")
        arr_prior_input = gr.Number(label="Prior ARR")
        growth_output = gr.Number(label="YoY Growth Rate (decimal)")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_yoy_growth,
            inputs=[arr_current_input, arr_prior_input],
            outputs=[growth_output],
        )

    with gr.Accordion("Rule of 40 Score", open=False):
        growth_rate_input = gr.Number(
            label="Growth Rate (percentage, e.g., 30 for 30%)"
        )
        ebitda_margin_input = gr.Number(
            label="EBITDA Margin (percentage, e.g., 15 for 15%)"
        )
        rule_output = gr.Number(label="Rule of 40 Score")
        btn = gr.Button("Calculate")
        btn.click(
            calculate_rule_of_40,
            inputs=[growth_rate_input, ebitda_margin_input],
            outputs=[rule_output],
        )

demo.launch()