Yehor's picture
Create app.py
dd059ea verified
raw
history blame
17.5 kB
"""
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()