BusinessMath Quarterly Series
11 min read
Part 9 of 12-Week BusinessMath Series
Business planning requires forecasting: Will we hit our revenue target? How many users will we have next quarter? What should our headcount plan look like?
Forecasting means understanding growth patterns:
Building robust forecasts manually requires statistical knowledge, careful data handling, and combining multiple techniques. You need systematic tools for growth analysis and forecasting.
BusinessMath provides comprehensive growth modeling including growth rate calculations, trend fitting, and seasonality extraction.
Calculate simple and compound growth:
import BusinessMath
// Simple growth rate
let growth = try growthRate(from: 100_000, to: 120_000)
// Result: 0.20 (20% growth)
// Negative growth (decline)
let decline = try growthRate(from: 120_000, to: 100_000)
// Result: -0.1667 (-16.67% decline)
Formula:
Growth Rate = (Ending / Beginning) - 1
CAGR smooths out volatility to show steady equivalent growth:
// Revenue: $100k → $110k → $125k → $150k over 3 years
let compoundGrowth = cagr(
beginningValue: 100_000,
endingValue: 150_000,
years: 3
)
// Result: ~0.1447 (14.47% per year)
// Verify: does 14.47% compound for 3 years give $150k?
let verification = 100_000 * pow((1 + compoundGrowth), 3.0)
// Result: ~150,000 ✓
Formula:
CAGR = (Ending / Beginning)^(1/years) - 1
The insight: Revenue was volatile year-to-year ($10k, then $15k, then $25k growth), but CAGR shows the equivalent steady rate: 14.47% annually.
Project future values:
// Project $100k base with 15% annual growth for 5 years
let projection = applyGrowth(
baseValue: 100_000,
rate: 0.15,
periods: 5,
compounding: .annual
)
// Result: [100k, 115k, 132.25k, 152.09k, 174.90k, 201.14k]
Different frequencies affect growth:
let base = 100_000.0
let rate = 0.12 // 12% annual rate
let years = 5
// Annual: 12% once per year
let annual = applyGrowth(baseValue: base, rate: rate, periods: years, compounding: .annual)
print(annual.last!.number(0))
// Final: ~176,234
// Quarterly: 3% four times per year
let quarterly = applyGrowth(baseValue: base, rate: rate, periods: years * 4, compounding: .quarterly)
print(quarterly.last!.number(0))
// Final: ~180,611 (higher due to more frequent compounding)
// Monthly: 1% twelve times per year
let monthly = applyGrowth(baseValue: base, rate: rate, periods: years * 12, compounding: .monthly)
print(monthly.last!.number(0))
// Final: ~181,670
// Continuous: e^(rt)
let continuous = applyGrowth(baseValue: base, rate: rate, periods: years, compounding: .continuous)
print(continuous.last!.number(0))
// Final: ~182,212 (theoretical maximum)
The insight: More frequent compounding increases final value. Continuous compounding is the mathematical limit.
Trend models fit mathematical functions to historical data for forecasting.
Models constant absolute growth:
// Historical revenue shows steady ~$5k/month increase
let periods_linearTrend = (1...12).map { Period.month(year: 2024, month: $0) }
let revenue_linearTrend: [Double] = [100, 105, 110, 108, 115, 120, 118, 125, 130, 128, 135, 140]
let historical_linearTrend = TimeSeries(periods: periods_linearTrend, values: revenue_linearTrend)
// Fit linear trend
var trend_linearTrend = LinearTrend()
try trend_linearTrend.fit(to: historical_linearTrend)
// Project 6 months forward
let forecast_linearTrend = try trend_linearTrend.project(periods: 6)
print(forecast_linearTrend.valuesArray.map({$0.rounded()}))
// Result: [142, 145, 148, 152, 155, 159] (approximately)
Formula:
y = mx + b
Where:
- m = slope (rate of change)
- b = intercept (starting value)
Best for:
Models constant percentage growth:
// Revenue doubling every few years
let periods_exponentialTrend = (0..<10).map { Period.year(2015 + $0) }
let revenue_exponentialTrend: [Double] = [100, 115, 130, 155, 175, 200, 235, 265, 310, 350]
let historical_exponentialTrend = TimeSeries(periods: periods_exponentialTrend, values: revenue_exponentialTrend)
// Fit exponential trend
var trend_exponentialTrend = ExponentialTrend()
try trend_exponentialTrend.fit(to: historical_exponentialTrend)
// Project 5 years forward
let forecast_exponentialTrend = try trend_exponentialTrend.project(periods: 5)
// Result: [407, 468, 538, 619, 713]
Formula:
y = a × e^(bx)
Where:
- a = initial value
- b = growth rate
- e = Euler's number (2.71828...)
Best for:
Models growth approaching a capacity limit (S-curve):
// User adoption: starts slow, accelerates, then plateaus
let periods_logisticTrend = (0..<24).map { Period.month(year: 2023 + $0/12, month: ($0 % 12) + 1) }
let users_logisticTrend: [Double] = [100, 150, 250, 400, 700, 1200, 2000, 3500, 5500, 8000,
11000, 14000, 17000, 19500, 21500, 23000, 24000, 24500,
24800, 24900, 24950, 24970, 24985, 24990]
let historical_logisticTrend = TimeSeries(periods: periods_logisticTrend, values: users_logisticTrend)
// Fit logistic trend with capacity of 25,000 users
var trend_logisticTrend = LogisticTrend(capacity: 25_000)
try trend_logisticTrend.fit(to: historical_logisticTrend)
// Project 12 months forward
let forecast_logisticTrend = try trend_logisticTrend.project(periods: 12)
// Result: Approaches but never exceeds 25,000
Formula:
y = L / (1 + e^(-k(x-x₀)))
Where:
- L = capacity (maximum value)
- k = growth rate
- x₀ = midpoint of curve
Best for:
Extract and apply recurring patterns.
Calculate seasonal factors:
// Quarterly revenue with Q4 holiday spike
let periods = (0..<12).map { Period.quarter(year: 2022 + $0/4, quarter: ($0 % 4) + 1) }
let revenue: [Double] = [100, 120, 110, 150, // 2022
105, 125, 115, 160, // 2023
110, 130, 120, 170] // 2024
let ts = TimeSeries(periods: periods, values: revenue)
// Calculate seasonal indices (4 quarters per year)
let indices = try seasonalIndices(timeSeries: ts, periodsPerYear: 4)
print(indices.map({"\($0.number(2))"}).joined(separator: ", "))
// Result: [~0.85, ~1.00, ~0.91, ~1.24]
Interpretation:
Combine all techniques:
// 1. Load historical data
let historical = TimeSeries(periods: historicalPeriods, values: historicalRevenue)
// 2. Extract seasonal pattern
let seasonalIndices = try seasonalIndices(timeSeries: historical, periodsPerYear: 4)
// 3. Deseasonalize to reveal underlying trend
let deseasonalized = try seasonallyAdjust(timeSeries: historical, indices: seasonalIndices)
// 4. Fit trend model to deseasonalized data
var trend = LinearTrend()
try trend.fit(to: deseasonalized)
// 5. Project trend forward
let forecastPeriods = 4 // Next 4 quarters
let trendForecast = try trend.project(periods: forecastPeriods)
// 6. Reapply seasonality to trend forecast
let seasonalForecast = try applySeasonal(timeSeries: trendForecast, indices: seasonalIndices)
// 7. Present forecast
for (period, value) in zip(seasonalForecast.periods, seasonalForecast.valuesArray) {
print("\(period.label): \(value.currency())")
}
This workflow:
Step 1: Does your data have seasonality?
Step 2: What kind of growth pattern?
Step 3: How much history do you have?
Step 4: What’s your forecast horizon?
import BusinessMath
// Simple growth rate
let growth = try growthRate(from: 100_000, to: 120_000)
// Result: 0.20 (20% growth)
// Negative growth (decline)
let decline = try growthRate(from: 120_000, to: 100_000)
// Result: -0.1667 (-16.67% decline)
// Revenue: $100k → $110k → $125k → $150k over 3 years
let compoundGrowth = cagr(
beginningValue: 100_000,
endingValue: 150_000,
years: 3
)
// Result: ~0.1447 (14.47% per year)
// Verify: does 14.47% compound for 3 years give $150k?
let verification = 100_000 * pow((1 + compoundGrowth), 3.0)
// Result: ~150,000 ✓
// Project $100k base with 15% annual growth for 5 years
let projection = applyGrowth(
baseValue: 100_000,
rate: 0.15,
periods: 5,
compounding: .annual
)
// Result: [100k, 115k, 132.25k, 152.09k, 174.90k, 201.14k]
let base = 100_000.0
let rate = 0.12 // 12% annual rate
let years = 5
// Annual: 12% once per year
let annual = applyGrowth(baseValue: base, rate: rate, periods: years, compounding: .annual)
print(annual.last!.number(0))
// Final: ~176,234
// Quarterly: 3% four times per year
let quarterly = applyGrowth(baseValue: base, rate: rate, periods: years * 4, compounding: .quarterly)
print(quarterly.last!.number(0))
// Final: ~180,611 (higher due to more frequent compounding)
// Monthly: 1% twelve times per year
let monthly = applyGrowth(baseValue: base, rate: rate, periods: years * 12, compounding: .monthly)
print(monthly.last!.number(0))
// Final: ~181,670
// Continuous: e^(rt)
let continuous = applyGrowth(baseValue: base, rate: rate, periods: years, compounding: .continuous)
print(continuous.last!.number(0))
// Final: ~182,212 (theoretical maximum)
// Historical revenue shows steady ~$5k/month increase
let periods_linearTrend = (1...12).map { Period.month(year: 2024, month: $0) }
let revenue_linearTrend: [Double] = [100, 105, 110, 108, 115, 120, 118, 125, 130, 128, 135, 140]
let historical_linearTrend = TimeSeries(periods: periods_linearTrend, values: revenue_linearTrend)
// Fit linear trend
var trend_linearTrend = LinearTrend()
try trend_linearTrend.fit(to: historical_linearTrend)
// Project 6 months forward
let forecast_linearTrend = try trend_linearTrend.project(periods: 6)
print(forecast_linearTrend.valuesArray.map({$0.rounded()}))
// Result: [142, 145, 148, 152, 155, 159] (approximately)
// Revenue doubling every few years
let periods_exponentialTrend = (0..<10).map { Period.year(2015 + $0) }
let revenue_exponentialTrend: [Double] = [100, 115, 130, 155, 175, 200, 235, 265, 310, 350]
let historical_exponentialTrend = TimeSeries(periods: periods_exponentialTrend, values: revenue_exponentialTrend)
// Fit exponential trend
var trend_exponentialTrend = ExponentialTrend()
try trend_exponentialTrend.fit(to: historical_exponentialTrend)
// Project 5 years forward
let forecast_exponentialTrend = try trend_exponentialTrend.project(periods: 5)
print(forecast_exponentialTrend.valuesArray.map({$0.rounded()}))
// Result: [407, 468, 538, 619, 713]
// User adoption: starts slow, accelerates, then plateaus
let periods_logisticTrend = (0..<24).map { Period.month(year: 2023 + $0/12, month: ($0 % 12) + 1) }
let users_logisticTrend: [Double] = [100, 150, 250, 400, 700, 1200, 2000, 3500, 5500, 8000,
11000, 14000, 17000, 19500, 21500, 23000, 24000, 24500,
24800, 24900, 24950, 24970, 24985, 24990]
let historical_logisticTrend = TimeSeries(periods: periods_logisticTrend, values: users_logisticTrend)
// Fit logistic trend with capacity of 25,000 users
var trend_logisticTrend = LogisticTrend(capacity: 25_000)
try trend_logisticTrend.fit(to: historical_logisticTrend)
// Project 12 months forward
let forecast_logisticTrend = try trend_logisticTrend.project(periods: 12)
print(forecast_logisticTrend.valuesArray.map({$0.rounded()}))
// Result: Approaches but never exceeds 25,000
// Quarterly revenue with Q4 holiday spike
let periods = (0..<12).map { Period.quarter(year: 2022 + $0/4, quarter: ($0 % 4) + 1) }
let revenue: [Double] = [100, 120, 110, 150, // 2022
105, 125, 115, 160, // 2023
110, 130, 120, 170] // 2024
let ts = TimeSeries(periods: periods, values: revenue)
// Calculate seasonal indices (4 quarters per year)
let indices = try seasonalIndices(timeSeries: ts, periodsPerYear: 4)
print(indices.map({"\($0.number(2))"}).joined(separator: ", "))
// Result: [~0.85, ~1.00, ~0.91, ~1.24]
// 1. Load historical data
let historical = TimeSeries(periods: historicalPeriods, values: historicalRevenue)
// 2. Extract seasonal pattern
let seasonalIndices = try seasonalIndices(timeSeries: historical, periodsPerYear: 4)
// 3. Deseasonalize to reveal underlying trend
let deseasonalized = try seasonallyAdjust(timeSeries: historical, indices: seasonalIndices)
// 4. Fit trend model to deseasonalized data
var trend = LinearTrend()
try trend.fit(to: deseasonalized)
// 5. Project trend forward
let forecastPeriods = 4 // Next 4 quarters
let trendForecast = try trend.project(periods: forecastPeriods)
// 6. Reapply seasonality to trend forecast
let seasonalForecast = try applySeasonal(timeSeries: trendForecast, indices: seasonalIndices)
// 7. Present forecast
for (period, value) in zip(seasonalForecast.periods, seasonalForecast.valuesArray) {
print("\(period.label): \(value.currency())")
}
→ Full API Reference: BusinessMath Docs – 3.1 Growth Modeling
Modifications to try:
A SaaS company tracking user growth notices:
Combining these insights produces a forecast that accounts for:
This is infinitely more useful than a simple “we’re growing 15%/month” projection.
★ Insight ─────────────────────────────────────
Why Deseasonalize Before Trend Fitting?
If you fit a trend to raw seasonal data, the model gets confused:
Deseasonalizing first lets you fit a clean trend, then reapply the seasonal pattern to forecasts.
Think of it like removing noise before measuring signal.
─────────────────────────────────────────────────
The hardest decision in growth modeling was: Should we make seasonality automatic, or explicit?
Some libraries auto-detect seasonal patterns. Sounds convenient! But it often gets it wrong—detecting false patterns in noise, or missing real patterns in small datasets.
We chose explicit seasonality:
periodsPerYear (4 for quarters, 12 for months)This requires one extra line of code, but prevents silent errors. When seasonality extraction fails, you know immediately and can investigate.
The lesson: Convenience features that fail silently are worse than explicit APIs that require judgment.
Related Methodology: The Master Plan (Tuesday) - Planning for API decisions
Coming up next: The Master Plan (Tuesday) - How to organize large projects with AI collaboration.
This week: Revenue modeling (Thursday) and Capital Equipment case study (Friday).
Series Progress:
Tagged with: businessmath, swift, growth, forecasting, cagr, trends, seasonality