""" 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()