library(tidyverse)
library(scales)
library(lubridate)
library(ggthemes)
library(knitr)
library(kableExtra)
library(patchwork)For this analysis I am stepping into the role of a newly hired business analyst at a mid-market SaaS company. The business sells software on a subscription basis, meaning customers pay a recurring monthly fee in exchange for access to the product. The company has been operating for a few years, has an established customer base, and is in a growth phase — adding new customers every month while managing the costs of building the product, selling it, and supporting the people who use it.
The company has five departments. Sales owns new customer acquisition. Marketing generates pipeline and manages the brand. Engineering builds and maintains the product. G&A covers finance, legal, HR, and the other functions that keep the business running. Customer Success manages relationships with existing customers and is responsible for keeping them from canceling their subscriptions.
This matters because every number we look at today connects back to one of those five departments and the decisions the people in them are making. Revenue does not just happen. It is the result of salespeople closing deals, marketers generating leads, engineers shipping features, and customer success teams keeping accounts healthy. The data tells us where those efforts are working and where they are not.
The analysis covers 24 months of P&L data from January 2023 through December 2024, 12 months of budget vs. actual data for 2024, a current headcount snapshot, and 24 months of customer acquisition and retention metrics. By the end I want a clear picture of the financial health of this business and a short list of things I need to understand better before I start building anything.
My job right now is not to have answers. It is to ask the right questions. That means working through four lenses:
I am going to walk through each one out loud, showing my code, my reasoning, and the questions each chart raises. By the end I want a short list of things to follow up on in my first week.
Note on data: The data here is synthetic but built to mirror realistic patterns. Seasonality, departmental cost structures, headcount ratios, margin profiles you would actually encounter. The code is fully reproducible and the patterns are real even if the numbers are not.
Before I touch a single chart I need to build the dataset. In a real job this is where I would open a ticket with the data engineering team, query a warehouse like Snowflake or BigQuery, or ask a financial analyst to pull the latest close from NetSuite. The data would be messy, have gaps, and require cleaning before it was usable.
Here I am generating everything synthetically in R. That means I am writing code that produces fake but realistic numbers — the kind of numbers a real business in this revenue range and growth stage might actually produce. Seasonality is built in. Noise is built in. The relationships between variables are economically logical. The goal is not to simulate a specific company but to create a dataset that behaves like one so the analysis that follows reflects real analytical thinking rather than textbook examples.
Every dataset is printed after it is built so you can see exactly what structure I am working with before any analysis begins. If you handed me a real export from your finance system this is the first thing I would do — look at the shape of the data before I build anything on top of it.
months <- seq.Date(from = as.Date("2023-01-01"), by = "month", length.out = 24)
monthly_pl <- tibble(
month = months,
revenue = 650000 * (1 + 0.015 * (1:24)) *
(1 + 0.08 * sin((month(months) - 3) * pi / 6)) +
rnorm(24, 0, 15000),
cogs = revenue * (0.52 - 0.003 * (1:24) / 24) + rnorm(24, 0, 8000),
opex_salaries = 180000 + 2500 * (1:24) + rnorm(24, 0, 5000),
opex_marketing = 45000 + 800 * (1:24) + rnorm(24, 0, 3000),
opex_tech = 28000 + 400 * (1:24) + rnorm(24, 0, 1500),
opex_facilities = 32000 + rnorm(24, 0, 1000),
opex_other = 18000 + 300 * (1:24) + rnorm(24, 0, 2000)
) %>%
mutate(
gross_profit = revenue - cogs,
gross_margin = gross_profit / revenue,
total_opex = opex_salaries + opex_marketing + opex_tech + opex_facilities + opex_other,
ebitda = gross_profit - total_opex,
ebitda_margin = ebitda / revenue,
year = year(month),
month_label = format(month, "%b %Y")
)
monthly_pl %>%
select(
month_label,
revenue, cogs, gross_profit, gross_margin,
opex_salaries, opex_marketing, opex_tech, opex_facilities, opex_other,
total_opex, ebitda, ebitda_margin
) %>%
pivot_longer(-month_label, names_to = "line_item", values_to = "value") %>%
mutate(
line_item = recode(
line_item,
revenue = "Revenue",
cogs = "COGS",
gross_profit = "Gross Profit",
gross_margin = "Gross Margin",
opex_salaries = "Opex - Salaries",
opex_marketing = "Opex - Marketing",
opex_tech = "Opex - Tech",
opex_facilities = "Opex - Facilities",
opex_other = "Opex - Other",
total_opex = "Total Opex",
ebitda = "EBITDA",
ebitda_margin = "EBITDA Margin"
),
line_item = factor(line_item, levels = unique(line_item)),
value = if_else(
str_detect(line_item, "Margin"),
scales::percent(value, accuracy = 0.01),
scales::dollar(value, accuracy = 1)
)
) %>%
pivot_wider(names_from = month_label, values_from = value) %>%
arrange(line_item) %>%
kable(
col.names = c("Line Item", names(.)[-1]),
align = c("l", rep("r", ncol(.) - 1))
) %>%
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE,
font_size = 12
) %>%
kableExtra::scroll_box(width = "100%")| Line Item | Jan 2023 | Feb 2023 | Mar 2023 | Apr 2023 | May 2023 | Jun 2023 | Jul 2023 | Aug 2023 | Sep 2023 | Oct 2023 | Nov 2023 | Dec 2023 | Jan 2024 | Feb 2024 | Mar 2024 | Apr 2024 | May 2024 | Jun 2024 | Jul 2024 | Aug 2024 | Sep 2024 | Oct 2024 | Nov 2024 | Dec 2024 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Revenue | $634,606 | $634,250 | $684,697 | $726,053 | $753,225 | $763,588 | $790,685 | $755,700 | $768,026 | $716,659 | $724,359 | $739,940 | $702,102 | $750,858 | $794,250 | $847,779 | $868,003 | $851,693 | $856,511 | $898,602 | $850,150 | $803,200 | $811,101 | $831,500 |
| COGS | $345,077 | $326,207 | $353,727 | $363,079 | $394,887 | $391,373 | $414,108 | $397,847 | $406,791 | $366,896 | $379,710 | $369,923 | $357,677 | $382,325 | $392,207 | $439,439 | $451,165 | $438,076 | $449,417 | $459,213 | $428,900 | $418,918 | $412,950 | $441,438 |
| Gross Profit | $289,528 | $308,042 | $330,969 | $362,974 | $358,338 | $372,215 | $376,577 | $357,853 | $361,236 | $349,764 | $344,649 | $370,017 | $344,426 | $368,533 | $402,043 | $408,341 | $416,838 | $413,617 | $407,094 | $439,389 | $421,250 | $384,282 | $398,152 | $390,062 |
| Gross Margin | 45.62% | 48.57% | 48.34% | 49.99% | 47.57% | 48.75% | 47.63% | 47.35% | 47.03% | 48.80% | 47.58% | 50.01% | 49.06% | 49.08% | 50.62% | 48.17% | 48.02% | 48.56% | 47.53% | 48.90% | 49.55% | 47.84% | 49.09% | 46.91% |
| Opex - Salaries | $180,343 | $188,278 | $189,110 | $186,081 | $200,379 | $198,214 | $197,949 | $201,383 | $205,896 | $205,449 | $192,535 | $211,424 | $210,664 | $215,926 | $220,409 | $226,999 | $218,864 | $231,513 | $229,179 | $235,193 | $237,104 | $238,604 | $232,284 | $239,549 |
| Opex - Marketing | $47,671 | $43,739 | $45,772 | $49,943 | $51,305 | $51,191 | $47,943 | $48,101 | $56,738 | $53,774 | $54,065 | $54,237 | $51,817 | $58,036 | $56,349 | $57,252 | $61,400 | $61,865 | $64,376 | $59,571 | $63,751 | $66,773 | $60,068 | $61,618 |
| Opex - Tech | $26,702 | $26,611 | $29,320 | $30,580 | $31,801 | $31,967 | $29,295 | $33,973 | $30,600 | $32,158 | $31,767 | $32,616 | $33,482 | $33,779 | $33,962 | $34,562 | $34,072 | $34,444 | $33,108 | $35,426 | $35,631 | $40,853 | $35,157 | $37,806 |
| Opex - Facilities | $30,506 | $30,530 | $32,125 | $31,003 | $31,998 | $31,572 | $31,386 | $29,975 | $30,775 | $32,180 | $32,568 | $31,507 | $32,000 | $33,123 | $33,440 | $30,903 | $31,883 | $33,201 | $31,530 | $31,948 | $31,914 | $31,112 | $31,555 | $31,971 |
| Opex - Other | $17,472 | $20,827 | $17,938 | $18,334 | $20,894 | $17,687 | $20,019 | $17,297 | $23,034 | $20,453 | $20,364 | $19,123 | $21,884 | $20,599 | $21,433 | $25,375 | $22,749 | $21,256 | $24,026 | $23,275 | $25,480 | $27,465 | $22,915 | $26,109 |
| Total Opex | $302,694 | $309,985 | $314,264 | $315,941 | $336,377 | $330,632 | $326,592 | $330,728 | $347,044 | $344,013 | $331,298 | $348,909 | $349,848 | $361,463 | $365,593 | $375,091 | $368,967 | $382,280 | $382,221 | $385,413 | $393,880 | $404,808 | $381,979 | $397,052 |
| EBITDA | -$13,166 | -$1,943 | $16,706 | $47,033 | $21,961 | $41,583 | $49,985 | $27,125 | $14,192 | $5,750 | $13,350 | $21,108 | -$5,422 | $7,070 | $36,450 | $33,250 | $47,871 | $31,338 | $24,873 | $53,976 | $27,370 | -$20,525 | $16,173 | -$6,991 |
| EBITDA Margin | -2.07% | -0.31% | 2.44% | 6.48% | 2.92% | 5.45% | 6.32% | 3.59% | 1.85% | 0.80% | 1.84% | 2.85% | -0.77% | 0.94% | 4.59% | 3.92% | 5.52% | 3.68% | 2.90% | 6.01% | 3.22% | -2.56% | 1.99% | -0.84% |
This is the core P&L table. Each column is one month. Revenue and COGS are the top two lines — the difference between them is gross profit. Below that are five operating expense categories that together make up total OpEx. The derived columns at the end — gross profit, gross margin, EBITDA, EBITDA margin — are calculated from those raw inputs. Nothing in the derived columns is assumed. Everything flows from the numbers above it.
current_years_months <- months[year(months) == 2024]
budget_vs_actual <- tibble(
month = rep(current_years_months, 5),
department = rep(c("Sales", "Marketing", "Engineering", "G&A", "Customer Success"), each = length(current_years_months))
) %>%
mutate(
budget = case_when(
department == "Sales" ~ 95000 + 1000 * as.integer(factor(month)),
department == "Marketing" ~ 48000 + 500 * as.integer(factor(month)),
department == "Engineering" ~ 120000 + 800 * as.integer(factor(month)),
department == "G&A" ~ 42000 + 200 * as.integer(factor(month)),
department == "Customer Success" ~ 35000 + 300 * as.integer(factor(month))
),
variance_pct = case_when(
department == "Sales" ~ rnorm(n(), 0.06, 0.04),
department == "Marketing" ~ rnorm(n(), 0.12, 0.05),
department == "Engineering" ~ rnorm(n(), -0.03, 0.03),
department == "G&A" ~ rnorm(n(), 0.02, 0.02),
department == "Customer Success" ~ rnorm(n(), -0.05, 0.03)
),
actual = budget * (1 + variance_pct),
variance = actual - budget
)
budget_vs_actual %>%
mutate(
budget = dollar(budget),
actual = dollar(actual, accuracy = .01),
variance = dollar(variance, accuracy = .01)
) %>%
kable(
col.names = c("Month", "Department", "Budget", "Variance %", "Actual", "Variance")
) %>%
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE
) %>%
kableExtra::scroll_box(width = "100%")| Month | Department | Budget | Variance % | Actual | Variance |
|---|---|---|---|---|---|
| 2024-01-01 | Sales | $96,000 | 0.0633959 | $102,086.01 | $6,086.01 |
| 2024-02-01 | Sales | $97,000 | 0.0958226 | $106,294.79 | $9,294.79 |
| 2024-03-01 | Sales | $98,000 | 0.0508089 | $102,979.27 | $4,979.27 |
| 2024-04-01 | Sales | $99,000 | 0.0934648 | $108,253.01 | $9,253.01 |
| 2024-05-01 | Sales | $100,000 | -0.0098022 | $99,019.78 | -$980.22 |
| 2024-06-01 | Sales | $101,000 | 0.1275784 | $113,885.41 | $12,885.41 |
| 2024-07-01 | Sales | $102,000 | 0.0945911 | $111,648.29 | $9,648.29 |
| 2024-08-01 | Sales | $103,000 | 0.0539690 | $108,558.80 | $5,558.80 |
| 2024-09-01 | Sales | $104,000 | 0.0020397 | $104,212.13 | $212.13 |
| 2024-10-01 | Sales | $105,000 | 0.0857203 | $114,000.64 | $9,000.64 |
| 2024-11-01 | Sales | $106,000 | 0.0793278 | $114,408.74 | $8,408.74 |
| 2024-12-01 | Sales | $107,000 | 0.0597458 | $113,392.80 | $6,392.80 |
| 2024-01-01 | Marketing | $48,500 | 0.0835391 | $52,551.65 | $4,051.65 |
| 2024-02-01 | Marketing | $49,000 | 0.1699034 | $57,325.27 | $8,325.27 |
| 2024-03-01 | Marketing | $49,500 | 0.1829241 | $58,554.74 | $9,054.74 |
| 2024-04-01 | Marketing | $50,000 | 0.1824432 | $59,122.16 | $9,122.16 |
| 2024-05-01 | Marketing | $50,500 | 0.0509681 | $53,073.89 | $2,573.89 |
| 2024-06-01 | Marketing | $51,000 | 0.2224980 | $62,347.40 | $11,347.40 |
| 2024-07-01 | Marketing | $51,500 | 0.1708436 | $60,298.45 | $8,798.45 |
| 2024-08-01 | Marketing | $52,000 | 0.1186641 | $58,170.53 | $6,170.53 |
| 2024-09-01 | Marketing | $52,500 | 0.1551804 | $60,646.97 | $8,146.97 |
| 2024-10-01 | Marketing | $53,000 | 0.0714307 | $56,785.83 | $3,785.83 |
| 2024-11-01 | Marketing | $53,500 | 0.0651922 | $56,987.78 | $3,487.78 |
| 2024-12-01 | Marketing | $54,000 | 0.1224525 | $60,612.44 | $6,612.44 |
| 2024-01-01 | Engineering | $120,800 | -0.0610308 | $113,427.48 | -$7,372.52 |
| 2024-02-01 | Engineering | $121,600 | -0.0489220 | $115,651.09 | -$5,948.91 |
| 2024-03-01 | Engineering | $122,400 | -0.0123958 | $120,882.76 | -$1,517.24 |
| 2024-04-01 | Engineering | $123,200 | -0.0424897 | $117,965.27 | -$5,234.73 |
| 2024-05-01 | Engineering | $124,000 | -0.0535466 | $117,360.22 | -$6,639.78 |
| 2024-06-01 | Engineering | $124,800 | -0.0250975 | $121,667.83 | -$3,132.17 |
| 2024-07-01 | Engineering | $125,600 | -0.0671014 | $117,172.06 | -$8,427.94 |
| 2024-08-01 | Engineering | $126,400 | 0.0013762 | $126,573.95 | $173.95 |
| 2024-09-01 | Engineering | $127,200 | -0.0445379 | $121,534.78 | -$5,665.22 |
| 2024-10-01 | Engineering | $128,000 | -0.0243261 | $124,886.25 | -$3,113.75 |
| 2024-11-01 | Engineering | $128,800 | -0.0284698 | $125,133.09 | -$3,666.91 |
| 2024-12-01 | Engineering | $129,600 | -0.0300072 | $125,711.06 | -$3,888.94 |
| 2024-01-01 | G&A | $42,200 | 0.0160869 | $42,878.87 | $678.87 |
| 2024-02-01 | G&A | $42,400 | 0.0156365 | $43,062.99 | $662.99 |
| 2024-03-01 | G&A | $42,600 | 0.0139044 | $43,192.33 | $592.33 |
| 2024-04-01 | G&A | $42,800 | 0.0319567 | $44,167.74 | $1,367.74 |
| 2024-05-01 | G&A | $43,000 | 0.0479486 | $45,061.79 | $2,061.79 |
| 2024-06-01 | G&A | $43,200 | 0.0337524 | $44,658.10 | $1,458.10 |
| 2024-07-01 | G&A | $43,400 | 0.0264038 | $44,545.92 | $1,145.92 |
| 2024-08-01 | G&A | $43,600 | 0.0139626 | $44,208.77 | $608.77 |
| 2024-09-01 | G&A | $43,800 | 0.0299670 | $45,112.55 | $1,312.55 |
| 2024-10-01 | G&A | $44,000 | 0.0090093 | $44,396.41 | $396.41 |
| 2024-11-01 | G&A | $44,200 | 0.0144149 | $44,837.14 | $637.14 |
| 2024-12-01 | G&A | $44,400 | 0.0419303 | $46,261.70 | $1,861.70 |
| 2024-01-01 | Customer Success | $35,300 | -0.0014497 | $35,248.83 | -$51.17 |
| 2024-02-01 | Customer Success | $35,600 | -0.0285743 | $34,582.75 | -$1,017.25 |
| 2024-03-01 | Customer Success | $35,900 | 0.0389760 | $37,299.24 | $1,399.24 |
| 2024-04-01 | Customer Success | $36,200 | -0.0738523 | $33,526.55 | -$2,673.45 |
| 2024-05-01 | Customer Success | $36,500 | -0.0255690 | $35,566.73 | -$933.27 |
| 2024-06-01 | Customer Success | $36,800 | 0.0129409 | $37,276.23 | $476.23 |
| 2024-07-01 | Customer Success | $37,100 | -0.0409706 | $35,579.99 | -$1,520.01 |
| 2024-08-01 | Customer Success | $37,400 | -0.0824923 | $34,314.79 | -$3,085.21 |
| 2024-09-01 | Customer Success | $37,700 | -0.0801897 | $34,676.85 | -$3,023.15 |
| 2024-10-01 | Customer Success | $38,000 | -0.0510624 | $36,059.63 | -$1,940.37 |
| 2024-11-01 | Customer Success | $38,300 | -0.0107263 | $37,889.18 | -$410.82 |
| 2024-12-01 | Customer Success | $38,600 | -0.0274880 | $37,538.96 | -$1,061.04 |
This table covers 2024 only because budget vs. actual is always a current year conversation. Each row is one department in one month. Budget is what we planned to spend. Actual is what we spent. Variance is the dollar difference and variance percent is that gap as a proportion of budget. A positive variance means we spent more than planned. A negative variance means we spent less.
headcount <- tibble(
department = c("Sales", "Marketing", "Engineering", "G&A", "Customer Success"),
headcount = c(22, 8, 31, 9, 14),
avg_salary = c(82000, 78000, 115000, 72000, 68000),
avg_tenure_yrs = c(2.1, 1.8, 3.4, 4.2, 2.7),
open_reqs = c(3, 1, 5, 0, 2),
attrition_rate = c(0.18, 0.22, 0.12, 0.08, 0.25)
) %>%
mutate(
total_labor_cost = headcount * avg_salary,
fully_loaded = total_labor_cost * 1.25
)Headcount is a snapshot not a time series. Five departments, eight columns. The two derived columns at the end — total labor cost and fully loaded — are calculated here. Fully loaded multiplies base salary cost by 1.25 to account for benefits, payroll taxes, and employer contributions. That multiplier is a standard finance rule of thumb. In a real job I would get the exact figure from HR or the finance team but 1.2 to 1.3 is the typical range.
unit_econ <- tibble(
month = months,
new_customers = round(120 + 4 * (1:24) + rnorm(24, 0, 12)),
churned_customers = round(40 + 1.5 * (1:24) + rnorm(24, 0, 4)),
sales_marketing_spend = monthly_pl$opex_salaries * 0.35 +
monthly_pl$opex_marketing
) %>%
mutate(
total_customers = cumsum(new_customers - churned_customers) + 800,
cac = sales_marketing_spend / pmax(new_customers, 1),
arpu = (monthly_pl$revenue / total_customers) * 0.25,
churn_rate = churned_customers / total_customers,
ltv = arpu / pmax(churn_rate, 0.001) * 0.70,
ltv_cac_ratio = ltv / cac,
cogs_per_customer = monthly_pl$cogs / total_customers
)Unit economics tracks how the business acquires and retains customers over time. New customers and churned customers drive the running total of active accounts. CAC is how much we spend to win each new customer. ARPU is how much each customer generates per month on average. Churn rate is the percentage of customers we lose each month. LTV is the estimated lifetime value of a customer given their ARPU and how long they typically stay. The LTV to CAC ratio is the single number that tells you whether the growth engine is healthy or not. A ratio below 3x is a warning sign. Above 5x might mean you are underinvesting in growth.
## Rows: 24
## Columns: 15
## $ month <date> 2023-01-01, 2023-02-01, 2023-03-01, 2023-04-01, 2023-…
## $ revenue <dbl> 634605.6, 634249.5, 684696.9, 726052.9, 753224.8, 7635…
## $ cogs <dbl> 345077.1, 326207.4, 353727.5, 363079.2, 394886.9, 3913…
## $ opex_salaries <dbl> 180342.8, 188278.2, 189109.6, 186080.8, 200378.6, 1982…
## $ opex_marketing <dbl> 47670.55, 43739.43, 45771.51, 49942.99, 51304.54, 5119…
## $ opex_tech <dbl> 26702.39, 26611.18, 29319.97, 30579.81, 31801.45, 3196…
## $ opex_facilities <dbl> 30506.37, 30529.56, 32124.70, 31003.36, 31998.18, 3157…
## $ opex_other <dbl> 17472.26, 20826.77, 17938.01, 18333.66, 20893.73, 1768…
## $ gross_profit <dbl> 289528.4, 308042.1, 330969.4, 362973.7, 358337.9, 3722…
## $ gross_margin <dbl> 0.4562337, 0.4856797, 0.4833809, 0.4999274, 0.4757383,…
## $ total_opex <dbl> 302694.4, 309985.2, 314263.8, 315940.6, 336376.5, 3306…
## $ ebitda <dbl> -13165.908, -1943.096, 16705.611, 47033.118, 21961.388…
## $ ebitda_margin <dbl> -0.020746600, -0.003063615, 0.024398548, 0.064779185, …
## $ year <dbl> 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, …
## $ month_label <chr> "Jan 2023", "Feb 2023", "Mar 2023", "Apr 2023", "May 2…
## Rows: 60
## Columns: 6
## $ month <date> 2024-01-01, 2024-02-01, 2024-03-01, 2024-04-01, 2024-05-…
## $ department <chr> "Sales", "Sales", "Sales", "Sales", "Sales", "Sales", "Sa…
## $ budget <dbl> 96000, 97000, 98000, 99000, 100000, 101000, 102000, 10300…
## $ variance_pct <dbl> 0.063395922, 0.095822623, 0.050808874, 0.093464763, -0.00…
## $ actual <dbl> 102086.01, 106294.79, 102979.27, 108253.01, 99019.78, 113…
## $ variance <dbl> 6086.0085, 9294.7945, 4979.2697, 9253.0115, -980.2234, 12…
## Rows: 5
## Columns: 8
## $ department <chr> "Sales", "Marketing", "Engineering", "G&A", "Customer…
## $ headcount <dbl> 22, 8, 31, 9, 14
## $ avg_salary <dbl> 82000, 78000, 115000, 72000, 68000
## $ avg_tenure_yrs <dbl> 2.1, 1.8, 3.4, 4.2, 2.7
## $ open_reqs <dbl> 3, 1, 5, 0, 2
## $ attrition_rate <dbl> 0.18, 0.22, 0.12, 0.08, 0.25
## $ total_labor_cost <dbl> 1804000, 624000, 3565000, 648000, 952000
## $ fully_loaded <dbl> 2255000, 780000, 4456250, 810000, 1190000
## Rows: 24
## Columns: 11
## $ month <date> 2023-01-01, 2023-02-01, 2023-03-01, 2023-04-01,…
## $ new_customers <dbl> 98, 120, 132, 119, 148, 114, 150, 147, 150, 157,…
## $ churned_customers <dbl> 41, 48, 42, 49, 51, 44, 51, 52, 58, 59, 56, 59, …
## $ sales_marketing_spend <dbl> 110790.5, 109636.8, 111959.9, 115071.3, 121437.1…
## $ total_customers <dbl> 857, 929, 1019, 1089, 1186, 1256, 1355, 1450, 15…
## $ cac <dbl> 1130.5155, 913.6401, 848.1809, 966.9855, 820.520…
## $ arpu <dbl> 185.12414, 170.68071, 167.98256, 166.67882, 158.…
## $ churn_rate <dbl> 0.04784131, 0.05166846, 0.04121688, 0.04499541, …
## $ ltv <dbl> 2708.682, 2312.368, 2852.904, 2593.046, 2584.595…
## $ ltv_cac_ratio <dbl> 2.395971, 2.530940, 3.363556, 2.681577, 3.149945…
## $ cogs_per_customer <dbl> 402.6571, 351.1383, 347.1320, 333.4061, 332.9569…
theme_ba <- function() {
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14,
margin = margin(b = 8)),
plot.subtitle = element_text(color = "grey40", size = 11,
margin = margin(b = 12)),
plot.caption = element_text(color = "grey55", size = 9,
margin = margin(t = 10)),
panel.grid.minor = element_blank(),
panel.grid.major = element_line(color = "grey92"),
axis.text = element_text(color = "grey40"),
legend.position = "bottom",
strip.text = element_text(face = "bold", size = 11)
)
}
palette_dept <- c(
"Sales" = "#2E86AB",
"Marketing" = "#A23B72",
"Engineering" = "#F18F01",
"G&A" = "#C73E1D",
"Customer Success" = "#3B1F2B"
)The first thing I want to see is the revenue trend. Not just the number but the shape. Is growth accelerating or decelerating? Is it smooth or lumpy? Does it show seasonality I need to account for in budget models? A single number tells you where you are. A trend tells you where you are going.
monthly_pl %>%
ggplot(aes(x = month, y = revenue)) +
geom_area(fill = "#2E86AB", alpha = 0.15) +
geom_line(color = "#2E86AB", linewidth = 1.2) +
geom_point(color = "#2E86AB", size = 2.5) +
geom_smooth(method = "lm", se = FALSE, color = "grey40",
linetype = "dashed", linewidth = 0.8) +
scale_y_continuous(labels = dollar_format(scale = 1e-3, suffix = "K"))+
scale_x_date(date_labels = "%b `%y", date_breaks = "3 months") +
labs(
title = "Monthly Revenue - 24 Month Trend",
subtitle = "Dashed line shows linear trend | Revenue softer in Q4 each year",
x = NULL,
y = "Revenue",
caption = "Source: Internal P&L"
) +
theme_ba()Revenue is growing steadily across the full 24 month period and the linear trend line confirms the direction is consistently upward. The area fill makes the growth shape easy to read at a glance. What I notice immediately is the softness in Q4 of both years — revenue dips slightly in the fall before recovering. That is not a crisis but it is a pattern worth flagging. If the business is building budgets that assume linear growth they are likely overestimating Q4 and underestimating the recovery in Q1 and Q2. I would want to understand whether this is demand driven, a timing issue in how contracts are signed, or something in how revenue is recognized.
Revenue growth is encouraging but growth that erodes margin is not growth worth celebrating. The top line going up means nothing if we are spending more to deliver the product than we are taking in. Gross margin tells me whether the core economics of the business are healthy — before we even consider what it costs to run the company.
monthly_pl %>%
ggplot(aes(x = month, y = gross_margin)) +
geom_line(color = "#27AE60", linewidth = 1.2) +
geom_point(color = "#27AE60", size = 2.5) +
geom_hline(yintercept = mean(monthly_pl$gross_margin),
linetype = "dashed", color = "grey50") +
annotate("text",
x = min(monthly_pl$month) + 30,
y = mean(monthly_pl$gross_margin) + 0.004,
label = paste0("Avg: ", percent(mean(monthly_pl$gross_margin),
accuracy = 0.1)),
color = "grey40", size = 3.5) +
scale_y_continuous(labels = percent_format(accuracy = 0.1),
limits = c(0.44, 0.52)) +
scale_x_date(date_labels = "%b '%y", date_breaks = "3 months") +
labs(
title = "Gross Margin % - 24 Month Trend",
subtitle = "Volatile in 2023, stabilizing above average through 2024",
x = NULL,
y = "Gross Margin %",
caption = "Gross Margin = (Revenue - COGS) / Revenue"
) +
theme_ba()Gross margin has averaged 48.4% across the period and has been stabilizing above that average through 2024. The volatility in 2023 is worth noting — the spread between the best and worst months was nearly 4 percentage points. That kind of swing usually means COGS has a variable component that is not fully under control, whether that is hosting costs, third party services, or delivery costs that fluctuate with volume. By 2024 the band has tightened which suggests the business has gotten better at managing those costs. That is a good sign but I would want to understand what changed.
Gross margin tells me about the core business. EBITDA tells me about the whole operation. After we pay our people, run our marketing, keep the lights on, and cover everything else it takes to operate — what is actually left? This is where I find out if the revenue growth we just saw is translating to the bottom line or getting absorbed by the cost of running the company.
monthly_pl %>%
ggplot(aes(x = month, y = ebitda_margin,
fill = ebitda_margin >= 0)) +
geom_col(alpha = 0.85) +
geom_hline(yintercept = 0, color = "black", linewidth = 0.5) +
scale_y_continuous(labels = percent_format(accuracy = 0.1)) +
scale_x_date(date_labels = "%b '%y", date_breaks = "3 months") +
scale_fill_manual(values = c("TRUE" = "#27AE60", "FALSE" = "#E74C3C"),
guide = "none") +
labs(
title = "EBITDA Margin % — Monthly",
subtitle = "Green = profitable months | Red = operating loss",
x = NULL,
y = "EBITDA Margin %",
caption = "EBITDA = Gross Profit minus all Operating Expenses"
) +
theme_ba()This is the most important chart in the section. The business was losing money at the operating level in its earliest months — EBITDA was negative in January and February of 2023. That is not unusual for a company investing heavily in headcount and infrastructure ahead of revenue growth. What matters is the direction. By mid 2023 the business had turned the corner and has been mostly EBITDA positive since, with a few exceptions that coincide with the revenue dips we saw in the first chart. The margins are thin — most positive months are in the 2 to 7 percent range — which means there is not a lot of cushion if revenue misses or costs spike unexpectedly. That is the watch item coming out of this section.
This is often where a new analyst can add immediate value. The budget was set months ago by people making assumptions about the future. Actuals are what actually happened. The gap between them is where the interesting questions live. My job here is not to assign blame but to find the patterns and ask why.
A budget variance table is one of the first things I would ask for on day one. It tells me immediately which departments are running hot, which are running cold, and whether the business is tracking to its financial plan. Before I look at a single chart I want to see the numbers in a table so I can orient myself to the scale of the gaps.
bva_summary <- budget_vs_actual %>%
group_by(department) %>%
summarize(
total_budget = sum(budget),
total_actual = sum(actual),
total_variance = sum(variance),
variance_pct = total_variance / total_budget,
.groups = 'drop'
) %>%
arrange(desc(variance_pct))
bva_summary %>%
mutate(
total_budget = dollar(total_budget),
total_actual = dollar(total_actual),
total_variance = dollar(total_variance),
variance_pct = percent(variance_pct, accuracy = 0.1)
) %>%
kable(
col.names = c(
"Department", "Budget", "Actual",
"Variance ($)", "Variance (%)"
),
align = c("l", "r", "r", "r", "r")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE
)| Department | Budget | Actual | Variance ($) | Variance (%) |
|---|---|---|---|---|
| Marketing | $615,000 | $696,477 | $81,477.11 | 13.2% |
| Sales | $1,218,000 | $1,298,740 | $80,739.68 | 6.6% |
| G&A | $519,600 | $532,384 | $12,784.31 | 2.5% |
| Customer Success | $443,400 | $429,560 | -$13,840.28 | -3.1% |
| Engineering | $1,502,400 | $1,447,966 | -$54,434.15 | -3.6% |
Three departments overspent and two underspent. Marketing is the biggest concern at 13.2% over budget — that is not a rounding error, that is a pattern. Sales is also over but commission structures are variable by nature so some overage is expected when the team is performing. G&A at 2.5% over is noise. The more interesting story is on the underspend side. Engineering is 3.6% under budget and has 5 open requisitions sitting unfilled. That is not savings — that is deferred hiring cost that will hit the moment those roles get filled. Customer Success is 3.1% under which sounds fine until you remember their attrition rate. Under budget and understaffed is not a good combination in a department responsible for keeping customers.
The bar chart below makes the relative size of these gaps easier to read at a glance.
bva_summary %>%
ggplot(aes(x = reorder(department, variance_pct),
y = variance_pct,
fill = variance_pct > 0)) +
geom_col(alpha = 0.85) +
geom_text(aes(label = paste0(round(variance_pct * 100, 1), "%"),
hjust = ifelse(variance_pct > 0, -0.1, 1.1)),
size = 4, fontface = "bold") +
coord_flip() +
scale_y_continuous(labels = percent_format(),
limits = c(-0.1, 0.15)) +
scale_fill_manual(values = c("TRUE" = "#E74C3C", "FALSE" = "#27AE60"),
guide = "none") +
labs(
title = "Budget vs. Actual — YTD Variance by Department",
subtitle = "Red = overspend | Green = underspend",
x = NULL,
y = "Variance %",
caption = "2024 YTD | Positive = actual exceeded budget"
) +
theme_ba()The visual confirms what the table showed but makes the relative scale immediately clear. Marketing’s overspend dwarfs everything else on a percentage basis. Engineering and Customer Success are the only departments running under and for reasons that deserve scrutiny rather than celebration.
A single YTD number can hide a lot. A department that blew its budget in January but has been tightening every month since is a very different conversation than one that keeps drifting further over. I want to see the monthly shape of the variance for the three most interesting departments so I can understand whether these are improving problems or worsening ones.
budget_vs_actual %>%
filter(department %in% c("Marketing", "Sales", "Customer Success")) %>%
mutate(department = factor(department,
levels = c("Marketing", "Sales",
"Customer Success"))) %>%
ggplot(aes(x = month, y = variance_pct, color = department)) +
geom_line(linewidth = 1.1) +
geom_hline(yintercept = 0, linetype = "dashed", color = "grey50") +
facet_wrap(~department, ncol = 1, scales = "free_y") +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
scale_x_date(date_labels = "%b '%y") +
scale_color_manual(values = palette_dept, guide = "none") +
labs(
title = "Monthly Variance % - Overspending Departments",
subtitle = "Positive = spending above budget | Negative = under budget",
x = NULL,
y = "Variance %"
) +
theme_ba()Marketing is consistently over budget every single month with no meaningful improvement. That is not a one-time event — it is a structural overspend that someone either approved and did not communicate or has not noticed yet. Sales is volatile, swinging above and below budget month to month, which is consistent with a variable commission structure tied to deal timing. Customer Success has been running under all year but is trending toward breakeven by December. Cross referenced with their attrition rate that trend is worth watching — if they start backfilling roles aggressively that underspend will flip quickly.
Labor is almost always the largest expense line in a business, often 60 to 70 percent of total OpEx. Understanding who we have, what they cost, and whether we are retaining them is non-negotiable on day one. The headcount data also connects directly back to what we just saw in the budget variance section. Departments that are overspending or understaffed do not exist in isolation — they show up here too.
The table below is the starting point. Before I build any charts I want to see the raw numbers — headcount, salary, tenure, open roles, and attrition — all in one place.
headcount_display <- headcount %>%
arrange(desc(fully_loaded)) %>%
mutate(
avg_salary = dollar(avg_salary),
total_labor_cost = dollar(total_labor_cost),
fully_loaded = dollar(fully_loaded),
attrition_rate = percent(attrition_rate, accuracy = 1)
)
headcount_display %>%
kable(
col.names = c("Department", "Headcount", "Avg Salary",
"Avg Tenure (Yrs)", "Open Reqs", "Attrition %",
"Total Labor", "Fully Loaded"),
align = c("l", "r", "r", "r", "r", "r", "r", "r")
) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = TRUE)| Department | Headcount | Avg Salary | Avg Tenure (Yrs) | Open Reqs | Attrition % | Total Labor | Fully Loaded |
|---|---|---|---|---|---|---|---|
| Engineering | 31 | $115,000 | 3.4 | 5 | 12% | $3,565,000 | $4,456,250 |
| Sales | 22 | $82,000 | 2.1 | 3 | 18% | $1,804,000 | $2,255,000 |
| Customer Success | 14 | $68,000 | 2.7 | 2 | 25% | $952,000 | $1,190,000 |
| G&A | 9 | $72,000 | 4.2 | 0 | 8% | $648,000 | $810,000 |
| Marketing | 8 | $78,000 | 1.8 | 1 | 22% | $624,000 | $780,000 |
A few things jump out immediately. Engineering is by far the largest cost center at $4.4 million fully loaded annually and has the most open roles at 5. G&A has the highest average tenure at 4.2 years which makes sense — finance, legal, and HR tend to be stable functions. Customer Success has the lowest average salary at $68,000 and the highest attrition at 25%. That combination is worth pausing on. The people responsible for keeping customers happy are the least compensated and the most likely to leave. The two charts below break this down further.
The first view compares fully loaded labor cost across departments. The second looks at attrition rate with a threshold line at 18% — departments above that line are losing people at a rate that creates real operational risk.
p_hc1 <- headcount %>%
ggplot(aes(x = reorder(department, fully_loaded),
y = fully_loaded,
fill = department)) +
geom_col(alpha = 0.85) +
geom_text(aes(label = dollar(fully_loaded)),
hjust = -0.1, size = 3.5, fontface = "bold") +
coord_flip() +
scale_y_continuous(labels = dollar_format(scale = 1e-6, suffix = "M"),
limits = c(0, max(headcount$fully_loaded) * 1.35)) +
scale_fill_manual(values = palette_dept, guide = "none") +
labs(
title = "Fully Loaded Labor Cost",
subtitle = "Base Salary * 1.25",
x = NULL,
y = "Annual Cost"
) +
theme_ba()
p_hc2 <- headcount %>%
ggplot(aes(x = reorder(department, attrition_rate),
y = attrition_rate,
fill = attrition_rate >= 0.18)) +
geom_col(alpha = 0.85) +
geom_text(aes(label = percent(attrition_rate, accuracy = 1)),
hjust = -0.1, size = 3.5, fontface = "bold") +
coord_flip() +
scale_y_continuous(labels = percent_format(),
limits = c(0, 0.32)) +
scale_fill_manual(values = c("FALSE" = "#27AE60", "TRUE" = "#E74C3C"),
guide = "none") +
labs(
title = "Annual Attrition Rate",
subtitle = "Red = above 18% threshold",
x = NULL,
y = "Attrition %"
) +
theme_ba()
p_hc1 + p_hc2Engineering dominates the cost chart which is expected given the combination of headcount and salary. What stands out on the attrition chart is that three of five departments are at or above the 18% threshold. That is not a one department problem — it is a company wide retention issue that finance leadership needs to be aware of.
The bar charts show each dimension separately. This next view combines them. The goal is to find departments where high cost and high attrition overlap — because that is where the business is taking the most financial risk. Every person who leaves has to be replaced, and replacement costs typically run 50 to 100 percent of annual salary when you account for recruiting, onboarding, and lost productivity.
headcount %>%
ggplot(aes(x = attrition_rate, y = avg_salary,
size = headcount, color = department, label = department)) +
geom_point(alpha = 0.75) +
geom_text(vjust = -1.2, hjust = 0.5, size = 3.5, fontface = "bold") +
geom_vline(xintercept = 0.18, linetype = "dashed", color = "grey50") +
geom_hline(yintercept = mean(headcount$avg_salary),
linetype = "dashed", color = "grey50") +
annotate("text", x = 0.185, y = 69000,
label = "Attrition threshold",
color = "grey50", size = 3, hjust = 0) +
annotate("text", x = 0.09,
y = mean(headcount$avg_salary) + 3500,
label = "Avg Salary",
color = "grey50", size = 3) +
scale_x_continuous(labels = percent_format(), limits = c(0.07, .28)) +
scale_y_continuous(labels = dollar_format(), limits = c(NA, 125000)) +
scale_color_manual(values = palette_dept, guide = "none") +
scale_size(range = c(3,10), guide = "none") +
labs(
title = "Attrition Rate vs. Average Salary",
subtitle = "Bubble size = headcount | Top right = expensive and high turnover",
x = "Annual Attrition Rate",
y = "Average Salary"
) +
theme_ba()Engineering sits in the top left — expensive but well retained. That is the ideal position. G&A sits in the bottom left — lower cost and stable. Customer Success sits in the bottom right — lower salary but the highest attrition in the company. That is the danger quadrant. You are not paying a lot per person but you are cycling through people constantly in a role where relationship continuity directly affects whether customers stay or leave. Marketing is also above the attrition threshold and above average salary which makes it the second most concerning department from a retention standpoint. Sales sits right on the threshold — worth watching but not yet a red flag on its own.
This is the section most junior analysts skip. Unit economics live at the intersection of finance, product, and operations and understanding them tells you whether the growth we saw in the revenue section is actually sustainable. A business can grow revenue every month and still be destroying value if it costs more to acquire and serve a customer than that customer is ever going to return. The summary table below gives me a snapshot of where things stand at the end of the period.
unit_econ_display <- tibble(
Metric = c(
"Total Customers",
"New Customers (Latest Month)",
"Churned Customers (Latest Month)",
"Customer Acquisition Cost",
"Monthly ARPU",
"Monthly Churn Rate",
"Estimated LTV",
"LTV:CAC Ratio"
),
Value = c(
comma(latest$total_customers),
comma(latest$new_customers),
comma(latest$churned_customers),
dollar(latest$cac),
dollar(latest$arpu),
percent(latest$churn_rate, accuracy = 0.1),
dollar(latest$ltv),
round(latest$ltv_cac_ratio, 2)
),
Interpretation = c(
"Current book of business",
"Gross new logos this month",
"Accounts lost this month",
"Sales and marketing spend per new customer",
"Average revenue per user per month",
"Percent of customers lost per month",
"ARPU divided by churn rate times 70% margin",
"Target is 3x or above for a healthy business"
)
)
unit_econ_display %>%
kable(align = c("l", "r", "l")) %>%
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE)| Metric | Value | Interpretation |
|---|---|---|
| Total Customers | 3,307 | Current book of business |
| New Customers (Latest Month) | 215 | Gross new logos this month |
| Churned Customers (Latest Month) | 84 | Accounts lost this month |
| Customer Acquisition Cost | $676.56 | Sales and marketing spend per new customer |
| Monthly ARPU | $62.86 | Average revenue per user per month |
| Monthly Churn Rate | 2.5% | Percent of customers lost per month |
| Estimated LTV | $1,732.29 | ARPU divided by churn rate times 70% margin |
| LTV:CAC Ratio | 2.56 | Target is 3x or above for a healthy business |
The LTV to CAC ratio at 2.56 is the number that concerns me most. It is below the 3x threshold that most operators treat as the minimum for a healthy acquisition engine. That means for every dollar we spend acquiring a customer we are getting back roughly $2.56 in lifetime value. That is not catastrophic but it is not comfortable either. The three charts below tell me how we got here and where things are heading.
Customer acquisition cost tells me how efficiently the sales and marketing machine is converting spend into new customers. If CAC is rising it means we are working harder and spending more to win the same number of accounts. If it is falling it means we are getting more efficient. The trend over 24 months tells a more honest story than any single month.
unit_econ %>%
ggplot(aes(x = month, y = cac)) +
geom_line(color = "#E74C3C", linewidth = 1.2) +
geom_smooth(method = "loess", se = TRUE, alpha = 0.15,
color = "#E74C3C", fill = "#E74C3C") +
scale_y_continuous(labels = dollar_format()) +
scale_x_date(date_labels = "%b '%y", date_breaks = "6 months") +
labs(
title = "Customer Acquisition Cost - 24 Month Trend",
subtitle = "Declining CAC suggests improving sales efficiency over 24 months",
x = NULL,
y= "CAC ($)"
) +
theme_ba()CAC has been declining over the period which is an encouraging sign. The business is getting more efficient at acquiring customers, likely through improving sales processes, better marketing targeting, or both. The question I would ask is whether this is intentional or whether we are simply picking lower hanging fruit and deferring the harder prospects for later. Declining CAC that is driven by cherry picking easy deals will reverse the moment those deals run out.
CAC declining is good news but it does not tell the full story on its own. What matters is whether the value of a customer over their lifetime justifies what we spent to acquire them. That is the LTV to CAC ratio and it is the single most important unit economics metric for understanding the health of the growth engine.
unit_econ %>%
ggplot(aes(x=month, y = ltv_cac_ratio)) +
geom_line(color = "#2E86AB", linewidth =1.2) +
geom_point(color = "#2E86AB", size = 2.5) +
geom_hline(yintercept = 3, linetype = 'dashed',
color = "#27ae60", linewidth = 1) +
annotate("text",
x = min(unit_econ$month) + 30,
y = 3.15,
label = "Healthy threshold (3.0x)",
color = "#27ae60", size = 3.5) +
scale_x_date(date_labels = "%b '%y", date_breaks = "6 months") +
scale_y_continuous(limits = c(2, NA)) +
labs(
title = "LTV:CAC Ratio — 24 Month Trend",
subtitle = "Below 3x means we are paying too much to acquire relative to customer value",
x = NULL,
y = "LTV:CAC"
) +
theme_ba()The ratio has been volatile across the period and is currently sitting at 2.56 which puts us below the 3x threshold. ARPU is declining as we bring on smaller accounts and CAC is only declining modestly. The gap is closing in the wrong direction. This is not a crisis today but it is the conversation I am walking into on day two. If this trend continues for another two or three quarters the business will need to either raise prices, reduce acquisition costs, or improve retention to get the ratio back above 3x.
The last unit economics view is churn. Every percentage point of monthly churn compounds. At 2% monthly you lose roughly 22% of your customer base annually. At 3% that number climbs to 31%. Churn is the leaky bucket underneath all the growth numbers we looked at in section three and it connects directly to the Customer Success attrition problem we flagged in section five. The question is not just where churn is today but where it has been and what direction it is moving.
unit_econ %>%
ggplot(aes(x = month, y = churn_rate)) +
geom_line(color = "#A23B72", linewidth = 1.2) +
geom_point(color = "#A23B72", size = 2.5) +
geom_hline(yintercept = 0.02, linetype = "dashed",
color = "grey50", linewidth = 0.8) +
annotate("text",
x = min(unit_econ$month) + 30,
y = 0.0215,
label = "2% monthly benchmark",
color = "grey50", size = 3.5) +
scale_y_continuous(labels = percent_format(accuracy = 0.1)) +
scale_x_date(date_labels = "%b '%y", date_breaks = "6 months") +
labs(
title = "Monthly Customer Churn Rate — 24 Month Trend",
subtitle = "Even small churn compounds significantly at scale",
x = NULL,
y = "Monthly Churn Rate"
) +
theme_ba()Churn started well above 5% in early 2023 which is a serious problem for any subscription business. The encouraging news is the trend is consistently downward and by late 2024 we are approaching the 2% monthly benchmark. That kind of sustained improvement does not happen by accident. Something changed — whether that was a CS process improvement, a product fix, or a change in the customer mix. I would want to know exactly what drove it because whatever it was we should be doing more of it. The concern is that with CS attrition at 25% the people who drove that improvement may not all still be here.
I have now looked at this business through four lenses. Here is what I would walk into my manager’s office to say at the end of day one. Not conclusions, not recommendations — those come after I have had more conversations. But these are the patterns I see, the questions they raise, and the things I want to understand better before I start building anything.
tibble(
Category = c(
"Revenue Growth (YoY)",
"Gross Margin (Latest)",
"EBITDA Margin (Latest)",
"Marketing Budget Variance (YTD)",
"Engineering Budget Variance (YTD)",
"Customer Success Attrition",
"LTV:CAC Ratio (Latest)",
"Monthly Churn Rate (Latest)"
),
Value = c(
percent(
(monthly_pl %>% filter(year == 2024) %>% pull(revenue) %>% mean()) /
(monthly_pl %>% filter(year == 2023) %>% pull(revenue) %>% mean()) - 1,
accuracy = 0.1),
percent(last(monthly_pl$gross_margin), accuracy = 0.1),
percent(last(monthly_pl$ebitda_margin), accuracy = 0.1),
percent(filter(bva_summary, department == "Marketing") %>%
pull(variance_pct), accuracy = 0.1),
percent(filter(bva_summary, department == "Engineering") %>%
pull(variance_pct), accuracy = 0.1),
percent(filter(headcount, department == "Customer Success") %>%
pull(attrition_rate), accuracy = 1),
as.character(round(last(unit_econ$ltv_cac_ratio), 2)),
percent(last(unit_econ$churn_rate), accuracy = 0.1)
),
Signal = c(
"Strong",
"Healthy",
"Watch",
"Investigate",
"Deferred hiring",
"Investigate",
"Below threshold",
"Improving"
)
) %>%
kable(align = c("l", "r", "c"), escape = FALSE) %>%
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE)| Category | Value | Signal |
|---|---|---|
| Revenue Growth (YoY) | 13.5% | Strong |
| Gross Margin (Latest) | 46.9% | Healthy |
| EBITDA Margin (Latest) | -0.8% | Watch |
| Marketing Budget Variance (YTD) | 13.2% | Investigate |
| Engineering Budget Variance (YTD) | -3.6% | Deferred hiring |
| Customer Success Attrition | 25% | Investigate |
| LTV:CAC Ratio (Latest) | 2.56 | Below threshold |
| Monthly Churn Rate (Latest) | 2.5% | Improving |
What looks healthy:
Revenue is growing at 13.5% year over year and gross margin is averaging above 48% across the period, though the latest month came in at 46.9% which is worth monitoring. The core business model is working — we are delivering the product at a reasonable cost and growing the top line. The churn improvement from above 5% to near 2% over two years is the most encouraging trend in the whole analysis and suggests the product and customer success teams have made real progress on retention.
What I am watching:
EBITDA margin is thin and has dipped negative in several months. The business is not consistently profitable at the operating level and there is not much cushion if revenue softens or costs spike. Engineering has 5 open roles and is running under budget — that underspend is not savings, it is deferred cost that will hit the moment hiring accelerates.
What I am flagging for week one conversations:
Marketing is 13.2% over budget every single month with no sign of the gap closing. I want to understand whether that spend was approved and whether it is producing results in the new customer numbers. Customer Success has a 25% attrition rate, 2 open roles, and is running under budget simultaneously. That is the data point that concerns me most. The churn improvement we just saw was almost certainly driven by CS team efforts. If the people who built that improvement keep leaving, churn will reverse. The LTV to CAC ratio at 2.56 is already below the 3x threshold. A churn reversal would make that worse. These three things — CS attrition, churn trajectory, and LTV to CAC — are connected and mapping that connection is the first thing I am building on day two.