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.

Revenue Forecasting for Board Reporting

By MCP Analytics Team | Published , updated

Your board wants a revenue projection for next quarter. Your FP&A team builds it in Excel using trailing averages and gut-adjusted growth rates. Then a board member asks "what's the confidence interval on that?" and the room goes quiet. A spreadsheet formula gives one number and says nothing about how far off it might be. This page shows one way to forecast revenue and show prediction ranges. The time it takes depends on the data and the work needed to prepare it.

Worked Example (Simulated): What Might the Next Three Months Look Like?

Question: What might the next three monthly revenue values be, and how far off could a simple seasonal forecast run?

Inputs: Made-up (simulated) data, seed 2026. The script makes 36 monthly values with a built-in rise of 1.5 revenue units a month (18 a year), a seasonal pattern that repeats each year, and random noise with a spread of 5. Values are revenue units, not dollars.

Method: For each month, two forecasts use earlier data only. The seasonal-naive forecast takes the value from the same month a year earlier. The second adds the average 12-month change seen so far. To check them, forecast months 25 to 36. MAPE is average absolute percentage error: the mean absolute error as a share of actual values. Prediction ranges come from the size of the second forecast's backtest errors (the 80% and 95% points of the absolute errors).

Complete Python script:

import numpy as np

seed = 2026
rng = np.random.default_rng(seed)
months = np.arange(1, 37)
season = np.array(
    [0.82, 0.84, 0.94, 0.98, 1.07, 1.16, 1.18, 1.10, 1.02, 0.98, 0.94, 0.97]
)
noise = rng.normal(0, 5, size=36)
revenue = 100 + 1.5 * months + 12 * season[(months - 1) % 12] + noise

# Two forecasts for month t, both from data before t:
#   seasonal naive: the value 12 months earlier
#   seasonal naive plus change: that value plus the average 12-month
#   change seen so far
def forecast(t, with_change):
    base = revenue[t - 13]
    if not with_change:
        return base
    last = min(t - 1, len(revenue))
    changes = revenue[12:last] - revenue[:last - 12]
    return base + changes.mean()

# Backtest months 25-36. The 12-month change needs months 13-24 to exist.
test_months = np.arange(25, 37)
actual = revenue[24:36]
report = {}
for label, flag in (
    ("Seasonal naive", False),
    ("Seasonal naive plus yearly change", True),
):
    pred = np.array([forecast(t, flag) for t in test_months])
    abs_errors = np.abs(actual - pred)
    mape = np.mean(abs_errors / actual) * 100
    mean_error = np.mean(actual - pred)
    q80 = np.quantile(abs_errors, 0.80, method="higher")
    q95 = np.quantile(abs_errors, 0.95, method="higher")
    report[label] = (mape, mean_error, q80, q95)

print(f"Simulated data, seed {seed}")
print(
    "Input: 36 monthly revenue values; trend = 1.5 units/month, seasonal "
    "multipliers repeated yearly, normal noise SD = 5 units."
)
print(
    "Backtest: forecast months 25-36 from earlier data only; "
    "no model refit needed."
)
for label, (mape, mean_error, q80, q95) in report.items():
    print(
        f"{label}: MAPE {mape:.2f}%; mean error (actual minus forecast) "
        f"{mean_error:.2f}; error size at 80% {q80:.2f}, at 95% {q95:.2f} "
        "revenue units"
    )
print("Built-in rise per year: 18.00 revenue units (1.5 per month x 12)")
q80, q95 = report["Seasonal naive plus yearly change"][2:]
print(
    "Months 37-39 are not in the data. The no-noise built-in level is "
    "shown only to check the forecasts."
)
print(
    "Forecast month | seasonal naive | with yearly change | "
    "80% range for the second | 95% range for the second | built-in level"
)
for t in (37, 38, 39):
    plain = forecast(t, False)
    adjusted = forecast(t, True)
    level = 100 + 1.5 * t + 12 * season[(t - 1) % 12]
    print(
        f"{t:>13} | {plain:6.2f} | {adjusted:6.2f} | "
        f"{adjusted - q80:5.2f} to {adjusted + q80:5.2f} | "
        f"{adjusted - q95:5.2f} to {adjusted + q95:5.2f} | {level:6.2f}"
    )

Printed output:

Simulated data, seed 2026
Input: 36 monthly revenue values; trend = 1.5 units/month, seasonal multipliers repeated yearly, normal noise SD = 5 units.
Backtest: forecast months 25-36 from earlier data only; no model refit needed.
Seasonal naive: MAPE 12.83%; mean error (actual minus forecast) 20.57; error size at 80% 27.55, at 95% 31.63 revenue units
Seasonal naive plus yearly change: MAPE 3.66%; mean error (actual minus forecast) 2.41; error size at 80% 9.96, at 95% 13.82 revenue units
Built-in rise per year: 18.00 revenue units (1.5 per month x 12)
Months 37-39 are not in the data. The no-noise built-in level is shown only to check the forecasts.
Forecast month | seasonal naive | with yearly change | 80% range for the second | 95% range for the second | built-in level
           37 | 150.55 | 169.38 | 159.42 to 179.34 | 155.56 to 183.20 | 165.34
           38 | 158.20 | 177.03 | 167.07 to 186.99 | 163.21 to 190.85 | 167.08
           39 | 148.21 | 167.04 | 157.08 to 177.00 | 153.22 to 180.86 | 169.78

Result: The plain seasonal-naive forecast ignores the rise built into the data, so it runs low: the mean error (actual minus forecast) is 20.57 revenue units, and its backtest MAPE of 12.83% comes mostly from that missed rise. Adding the average yearly change cuts the backtest MAPE from 12.83% to 3.66% and the mean error to 2.41. Months 37 to 39 are not in the data. For them the plain forecasts are 150.55, 158.20 and 148.21, and the forecasts with the yearly change are 169.38, 177.03 and 167.04. The last column shows the no-noise built-in level, which the script uses only to check the forecasts (165.34, 167.08, 169.78). Each 80% range for the second forecast contains its built-in level; the plain forecasts sit 15, 9 and 22 below it.

Limit: This is a small simulated example with one noise draw. The ranges are set from the same 12 backtest errors, so they cover at least their stated share of those 12 errors by construction. That is not an independent check and does not promise coverage for future months. The example uses the simplest seasonal method, not the ETS model described below.

Sources

Why Your Board Stopped Trusting the Spreadsheet

The core problem with a typical spreadsheet revenue forecast is not that it is wrong: it is that it does not tell you how wrong it might be. A projection of "$2.4M next quarter" gives the board one number to plan around. If the actual number comes in at $2.1M or $2.7M, the CFO looks either overly cautious or overly optimistic. Neither builds credibility.

Statistical forecasting solves this by producing three numbers instead of one: a point estimate ($2.4M), a conservative scenario ($2.15M at the 80% lower bound), and an optimistic scenario ($2.65M at the 80% upper bound). The board can plan against the conservative case for expenses, budget to the expected case for hiring, and prepare for the optimistic case in terms of capacity. That is the difference between a guess and a planning tool.

What This Analysis Does

The revenue forecasting module uses ETS (Error, Trend, Seasonality) exponential smoothing. It separates your historical revenue into three components:

The algorithm evaluates possible ETS model variants (additive or multiplicative error, trend, and seasonal components, damped or undamped) and selects the one that best fits your historical patterns using AIC (Akaike Information Criterion). You get the benefit of model selection without needing to understand the math.

The output is a month-by-month forecast with 80% and 95% prediction ranges and a forecast table.

Who This Is For

This analysis is built for finance teams at companies with $2M to $50M in annual revenue: large enough to have meaningful seasonal patterns in their data, but too small to justify an enterprise planning platform. The typical user is an FP&A analyst or controller who currently builds quarterly projections in Excel using the FORECAST function or trailing averages.

It works for any company with recurring or semi-recurring revenue: SaaS, professional services, e-commerce, manufacturing, or subscription businesses. The key requirement is 24+ months of historical revenue data so the model can observe at least two full seasonal cycles.

What Data You Need

A CSV export from your accounting system: QuickBooks, Xero, NetSuite, or a Stripe transaction export. You need two columns at minimum:

Optional columns that unlock richer analysis:

Minimum: 24 months of data (24 rows if pre-aggregated monthly, or 500+ transaction rows spanning 24 months). The more history you provide, the more stable the seasonal estimates. Date formats are flexible: YYYY-MM-DD, MM/DD/YYYY, and most other common formats all work.

How to Read the Report

Revenue Forecast Chart: the centerpiece. Historical revenue appears as a solid line, then extends into the future with shaded confidence bands. The narrower 80% band is your planning range. Across many forecasts, about 80 in 100 actual values should land inside it, if the model fits the business. The wider 95% band shows the extreme scenarios. Use the 80% band for operational planning and the 95% for stress testing.

Seasonal Decomposition: breaks revenue into trend, seasonal pattern, and remainder using STL (Seasonal and Trend decomposition using Loess). The trend line shows whether your business is genuinely growing after removing holiday spikes and seasonal dips. If the seasonal component shows a December peak that has been growing 15% per year while baseline growth is only 5%, that is strategically important.

Category Trends, if you mapped a category column, this chart shows monthly revenue by product line or segment. It reveals which categories are driving overall growth and which are flat or declining. A total revenue increase of 12% that comes entirely from one product line while three others contract is a very different story than broad-based growth.

Forecast Detail Table: each forecasted month with point estimate, lower bound, and upper bound. This is the table you paste into your board deck or budget spreadsheet. Each row is a month, each column is a number you can plan against.

Model Performance: MAPE (Mean Absolute Percentage Error) tells you how accurate the model would have been on historical data. In the worked example above, MAPE was 12.83% for the plain method and 3.66% after adding the average yearly change. The Ljung-Box test checks residual autocorrelation. A result that does not flag autocorrelation does not show that all useful patterns were captured.

What To Do With the Results

Board Reporting

Operational Planning

Ongoing Use

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