ds@mcpanalytics.aiEmail your data and your question and get an answer about your own data, with the method shown.Email your data and question. Get an answer.

SaaS Revenue Forecasting

By MCP Analytics Team | Published , updated

Every board meeting needs a revenue projection. Every investor asks "what does the next 6 months look like?" And every SaaS founder builds that forecast in a spreadsheet with a growth assumption they picked because it felt right. The result is either embarrassingly optimistic or embarrassingly conservative. This analysis replaces the guesswork with a statistical time series forecast with a seasonal component and prediction intervals. Upload your revenue data and get a chart with projections and their ranges.

Worked Example (Simulated): A Six-Month Revenue Forecast

Question: What could sales be each month for the next six months if the past trend and yearly cycle hold?

Inputs: Simulated data, seed 23. The script makes 48 months of made-up monthly revenue. It builds in a rise of $600 a month, a yearly sine cycle of $4,000, and random noise with a standard deviation of $1,800. This is monthly revenue. The script does not compute ARR.

Method: Fit a straight trend plus yearly sine and cosine terms. First fit on the first 42 months and test on the last 6, which were left out of that fit. Then refit on all 48 months to make the forecast and its 95% prediction range.

import numpy as np
from scipy.stats import t

SEED = 23
N_MONTHS = 48
HOLDOUT = 6
HORIZON = 6
rng = np.random.default_rng(SEED)
t_index = np.arange(N_MONTHS, dtype=float)
season = 4000 * np.sin(2 * np.pi * t_index / 12)
revenue = 30000 + 600 * t_index + season + rng.normal(0, 1800, N_MONTHS)


def design(index):
    index = np.asarray(index, dtype=float)
    return np.column_stack([
        np.ones(index.size),
        index,
        np.sin(2 * np.pi * index / 12),
        np.cos(2 * np.pi * index / 12),
    ])


def fit(x, y):
    beta, _, _, _ = np.linalg.lstsq(x, y, rcond=None)
    resid = y - x @ beta
    dof = len(y) - x.shape[1]
    mse = float(resid @ resid / dof)
    cov_base = np.linalg.inv(x.T @ x)
    return beta, resid, dof, mse, cov_base


train_end = N_MONTHS - HOLDOUT
x_train = design(t_index[:train_end])
beta, resid, dof, mse, cov_base = fit(x_train, revenue[:train_end])
hold_idx = t_index[train_end:]
hold_pred = design(hold_idx) @ beta
hold_actual = revenue[train_end:]
mae = float(np.mean(np.abs(hold_actual - hold_pred)))
mape = float(np.mean(np.abs((hold_actual - hold_pred) / hold_actual)) * 100)

x_all = design(t_index)
beta_all, resid_all, dof_all, mse_all, cov_base_all = fit(x_all, revenue)
future_idx = np.arange(N_MONTHS, N_MONTHS + HORIZON, dtype=float)
x_future = design(future_idx)
forecast = x_future @ beta_all
leverage = np.einsum("ij,jk,ik->i", x_future, cov_base_all, x_future)
critical = float(t.ppf(0.975, dof_all))
pred_se = np.sqrt(mse_all * (1 + leverage))
lower = forecast - critical * pred_se
upper = forecast + critical * pred_se

print(f"Simulated data, seed {SEED}")
print(f"Input: {N_MONTHS} monthly revenue values; "
      f"first value ${revenue[0]:,.0f}; last value ${revenue[-1]:,.0f}")
print("Method: linear trend plus sine/cosine yearly season terms; "
      f"hold out last {HOLDOUT} months")
print(f"Holdout diagnostics: MAE ${mae:,.0f}; MAPE {mape:.1f}%")
print(f"Forecast: next {HORIZON} months, with 95% prediction intervals "
      "based on regression residuals and t errors")
for j, (point, lo, hi) in enumerate(zip(forecast, lower, upper), start=1):
    print(f"Month +{j}: forecast ${point:,.0f}; "
          f"95% interval ${lo:,.0f} to ${hi:,.0f}")
print(f"Training residual standard error: ${np.sqrt(mse_all):,.0f}; "
      f"residual degrees of freedom: {dof_all}")

Printed output:

Simulated data, seed 23
Input: 48 monthly revenue values; first value $30,996; last value $55,728
Method: linear trend plus sine/cosine yearly season terms; hold out last 6 months
Holdout diagnostics: MAE $1,471; MAPE 2.7%
Forecast: next 6 months, with 95% prediction intervals based on regression residuals and t errors
Month +1: forecast $58,829; 95% interval $54,623 to $63,035
Month +2: forecast $61,127; 95% interval $56,890 to $65,364
Month +3: forecast $62,977; 95% interval $58,716 to $67,238
Month +4: forecast $64,047; 95% interval $59,773 to $68,320
Month +5: forecast $64,210; 95% interval $59,936 to $68,483
Month +6: forecast $63,585; 95% interval $59,320 to $67,849
Training residual standard error: $1,958; residual degrees of freedom: 44

Result: The first forecast month is $58,829, with a 95% range of $54,623 to $63,035. On the 6 test months, the average error size is $1,471, or 2.7%. That low error is expected here. The fitted model has the same form as the rule that made the data (a line plus a yearly cycle), and the script builds in about $58,800 for month 49. A real series will not match a model this neatly, so expect larger errors.

Limit: The data are made up and this is one run. The model leaves out price changes, lost buyers, and other events. The low end of the range is not a floor.

Sources: Hyndman and Athanasopoulos, Forecasting: Principles and Practice (3rd ed.), for regression with seasonal terms and prediction intervals.

The Problem With Spreadsheet Forecasts

The typical SaaS revenue forecast works like this: take last month's MRR, apply a growth rate (usually a round number like 10% or 15% because it sounds good), and extend it for 6 months. The result is a hockey stick that tells your board what they want to hear but gives you nothing to plan against. When Q2 revenue comes in 30% below the projection, nobody knows whether the forecast was wrong, growth decelerated, churn spiked, or all three.

Dedicated FP&A platforms such as Causal, Runway, or Mosaic can be more than a Series A company needs when it wants one good revenue chart for the board deck.

What makes a forecast defensible is not a bigger number -- it is quantified uncertainty. A projection of $420,000 next month with a 95% prediction interval of $380,000 to $460,000 lets you plan for the pessimistic scenario in conservative budgets while knowing the upside. That range is what separates a planning tool from a guess.

How the Forecast Works

The forecast separates your revenue into three components (the worked example above uses a simpler method, a straight trend plus yearly sine and cosine terms; ETS exponential smoothing is a related approach that also splits a series into these parts):

ETS-family models differ in whether the error, trend, and seasonal components are additive or multiplicative, and whether the trend is damped. Comparing such variants with a criterion like AIC is a standard way to choose among them.

The output is not a single line extending into the future. It is a forecast with 80% and 95% prediction intervals -- shaded bands that show the range of likely outcomes. The 80% band is a narrower range for operational planning. The 95% band is wider. Neither is a guarantee: the low end of a band is not a sure floor, and actual revenue can land outside it.

What the Report Shows You

Revenue forecast chart -- historical monthly revenue as a solid line, extended into the future with point forecasts and shaded confidence bands. This is the centerpiece -- the chart that goes in the board deck. It communicates both the projection and the uncertainty in a single visual.

Seasonal decomposition -- your revenue broken into trend, seasonal pattern, and remainder using STL (Seasonal and Trend decomposition using Loess). The trend line answers "is our business genuinely growing?" stripped of seasonal noise. The seasonal component shows repeating patterns -- which months consistently over- or under-perform. If the remainder shows large spikes, those are months where something unusual happened that the model cannot explain.

Forecast detail table -- each forecasted month with point estimate, lower bound, and upper bound. This is the table you paste into your budget spreadsheet. Conservative scenario (lower bound), expected case (point forecast), and optimistic scenario (upper bound) -- all from the same statistical model.

Category breakdowns -- if you include a product line or plan tier column, the analysis shows per-category trends. You can see which segments drive growth and which are declining, which matters for portfolio decisions and resource allocation.

Model performance -- MAPE (Mean Absolute Percentage Error) tells you how accurate the model is on historical data. A lower MAPE means a closer fit on past months, and a high one can point to volatile data or too little history. The Ljung-Box test checks whether the model captured all predictable structure -- a passing result means the test found no significant autocorrelation in the residuals, which is what you want. It does not prove the residuals are random noise.

When to Use Revenue Forecasting

What Data Do You Need?

A CSV with two columns: a date (transaction date, invoice date, or month) and a revenue amount. The tool aggregates transaction-level records into monthly totals automatically. You do not need to pre-aggregate.

Ideal data

Minimum requirements

Data sources: Stripe revenue export, QuickBooks/Xero P&L export, internal billing database export, or any transaction-level data with dates and amounts. If you have already run MRR analysis, feed the monthly MRR values from that report directly into this forecast.

What to Do With the Results

For the board deck

For operations

When to Use Something Else

Your turn

Bring your own data and the question you actually need answered.

MCP AnalyticsData science by email Send us your data and your question, and we will reply. ds@mcpanalytics.ai