A time-series forecast predicts the next values of something measured over time, from its own history. In planning, that is usually demand: how many pieces of a material will be consumed or sold each week.
History holds a few patterns. A level (the usual amount), a trend (slowly rising or falling), seasonality (a shape that repeats every year, quarter or week) and noise (what no method can predict). A forecasting method tries to capture the first three and accept the fourth.
Two ideas matter more than the choice of method:
Beat the naive forecast. "Next week will be like last week", or "like the same week last year", costs nothing. A forecast that can't beat it isn't worth running.
Test the way you will use it. Train on older weeks, then forecast weeks the method has never seen, as far ahead as your replenishment lead time. Shuffled tests and "fitted" numbers flatter every method. If several methods compete, pick the winner on one period and report its accuracy on a later one it never saw.
In plan-to-produce, the forecast becomes demand that material requirements planning (MRP) plans against. A wrong forecast doesn't stay in a spreadsheet. It turns into purchase requisitions and planned orders.
Take the running example of MRP exceptions. Forecast too high, and MRP buys or produces too early: stock piles up and planners work through reschedule-out messages. Forecast too low, and shortages appear inside the lead time: expediting, premium freight, reschedule-in messages and missed deliveries. A better forecast doesn't remove exceptions, but it can cut the number planners must touch each week.
The value sits in three places:
Inventory. Less safety stock is needed when forecast errors are smaller and unbiased.
Service. Fewer stock-outs on the materials customers care about.
Planner time. Fewer exceptions to review, and more time for the ones that matter.
The risk is false confidence. A dashboard showing "92% accuracy" can hide a forecast that is always too low, or a measure that ignores half the materials because their demand has zero weeks. The questions below are how a leader avoids that.
SAP covers forecasting at three levels. Names and scope change, so treat this as a map as of October 2026, not a licensing guide.
SAP S/4HANA. Material forecasting predicts consumption from historical values. A publisher's guide to S/4HANA lists constant, trend, seasonal and seasonal-trend models, plus an automatic model selection. SAP Learning says classic forecast-based planning (where forecast values act directly as requirements) is in the compatibility package and will be discontinued: by the end of 2025, or under certain circumstances by the end of 2030. SAP recommends converting forecast values into planned independent requirements (PIRs) instead, and points to SAP Notes 2268095 and 2269324 for the details.
Predictive material and resource planning (pMRP) in S/4HANA starts from PIRs and simulates whether capacity can meet them, before the operative MRP run. It doesn't forecast; it tests the forecast's consequences.
SAP Integrated Business Planning (IBP) for demand. SAP Learning describes about 20 forecasting algorithms, including regression and machine learning methods such as gradient boosting. Several algorithms can run in one model, with the best fit chosen. Demand sensing adds daily or weekly short-term forecasts from recent orders and deliveries. You can also bring your own machine learning model.
SAP HANA's Predictive Analysis Library, reachable from Python through hana-ml, offers exponential smoothing with automatic model selection and forecast accuracy measures, for teams building their own forecasts on HANA data.
"AI forecasting means a neural network." For most weekly material demand, simple methods are hard to beat. SAP's own planning tools offer statistical and machine learning methods side by side, and pick per material.
"Higher accuracy on the history means a better model." A model can fit the past closely and forecast the future badly. Only weeks it never saw count.
"MAPE works for every material." Percentage errors divide by the actual value. A week with zero demand makes them undefined.
"A forecast should be right." It will be wrong. The goal is smaller, unbiased errors than the alternative, and safety stock sized for the error that remains.
"One method for the whole portfolio." A steady seller, a seasonal product and a spare part need different methods. That's why tools pick per material.
Pick one answer for each question. The explanation appears after you choose.
1Your team proposes a new forecasting tool. Which comparison tells you whether it adds value?
Answer: B. A naive forecast costs nothing, so a method must beat it to be worth running. Only weeks the method never saw show how it will do in real use; accuracy on training history flatters every method.
2Why does a forecast error matter in plan-to-produce, beyond the forecast report itself?
Answer: C. The forecast becomes demand that MRP plans against. Too high creates early stock and reschedule-out work; too low creates shortages and reschedule-in work inside the lead time.
3A dashboard shows strong MAPE for a group of spare parts that are often not used for weeks. What should you ask?
Answer: B. MAPE divides by actual demand, so zero weeks make it undefined. A strong number may mean those weeks were dropped; ask for the handling and a scaled measure such as MASE.
4Accuracy looks good, but planners still report frequent shortages on the same materials. What is the likely cause?
Answer: B. An average error can look fine while the forecast leans consistently one way. A forecast that is persistently too low shows up as shortages, so ask for bias, not only accuracy.
5Your S/4HANA design uses classic forecast-based planning. What does SAP Learning say to check?
Answer: B. SAP Learning says classic forecast-based planning is in the compatibility package and will be discontinued, with SAP Notes 2268095 and 2269324 for details. The recommended path is planned independent requirements. pMRP simulates capacity against PIRs; it doesn't forecast.
6At what horizon should forecast accuracy be measured for purchasing decisions?
Answer: D. A forecast is only useful if it is right far enough ahead to act on. Errors grow with the horizon, so measuring one week ahead understates what buyers will live with.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Deep layer · 40 min read
#Mental model: a forecast is a bet, scored later, against the cheapest alternative
Every forecast is a bet placed at a point in time, the origin, about a week in the future, the horizon. It gets scored only when that week arrives. So an honest test replays history: stand at an old origin, use only what was known then, forecast h weeks ahead, and compare with what happened.
The score means nothing on its own. "MAE 21 pieces" is good or bad only next to what a free method scores on the same weeks: the naive forecast (last week again) or the seasonal naive forecast (the same week last year). Everything in this topic follows from those two rules: test by time, at your lead time, and compare with the naive forecast.
This is the time-split idea from Predicting late deliveries with SAP data, taken further. There, rows were deliveries and time was one column. Here, time is the whole structure: the value you predict is the next point of the same series.
Weekly goods issues for one material can be read as four parts added together:
Part
What it looks like
Material example
Level
The usual weekly quantity
200 pieces of a steady seller
Trend
The level drifting up or down
A product gaining customers
Seasonality
A shape repeating every cycle (52 weeks)
A garden pump that sells in summer
Noise
Random week-to-week swings
Order timing, one-off customers
Real series add two more things: outliers (a one-off order of 420 pieces) and structural breaks (a customer lost, a material replaced). Methods handle the first four; people must handle the last two.
Naive. Next weeks equal the last week. Excellent when demand drifts; useless with seasonality.
Seasonal naive. Next weeks equal the same weeks one cycle ago. Strong for stable seasonal products; misses a trend.
Moving average. The mean of the last k weeks (here 8). Smooths noise, but lags behind trends and can't produce a season.
Exponential smoothing (Holt-Winters). Keeps a running estimate of level, trend and season, and updates each one a little every week. A smoothing weight between 0 and 1 says how much the newest week counts against the old estimate. Fitting means choosing those weights to minimize past one-step errors. A damped trend flattens the trend over the horizon, so a recent rise isn't projected forever.
flowchart LR
Y[New week's quantity] --> L[Update level]
Y --> B[Update trend]
Y --> S[Update this week's season]
L --> F[Forecast = level + damped trend + season]
B --> F
S --> F
SAP's tools use the same families. The S/4HANA material forecast models (constant, trend, seasonal, seasonal trend) map to the patterns above. The HANA Predictive Analysis Library offers single, double and triple exponential smoothing, which add level, trend and season in turn.
A single holdout (train on two years, test on the third) is a start. A rolling-origin backtest is better: within the held-back weeks, step the origin forward, refit on everything before it, and forecast the next h weeks each time. You get many honest forecasts instead of one.
flowchart TB
subgraph O1 [Origin 1]
A1[Weeks 1 to 104: fit] --> A2[Forecast weeks 105 to 108]
end
subgraph O2 [Origin 2]
B1[Weeks 1 to 108: fit] --> B2[Forecast weeks 109 to 112]
end
subgraph O13 [Origin 13]
C1[Weeks 1 to 152: fit] --> C2[Forecast weeks 153 to 156]
end
Set h to the lead time that matters. If purchasing commits four weeks ahead, a one-week-ahead error is the wrong number.
A backtest that compares several methods is also a contest. If you pick the method with the lowest error on the test weeks and then report that error, the number is too good: the winner partly won by luck on exactly those weeks. The fix is to split the held-back weeks in two.
flowchart LR
T[Training history: fit methods] --> V[Validation weeks: choose the method]
V --> X[Test weeks: report the chosen method once]
Training history. Each method fits only on weeks before each origin.
Validation weeks. Run the rolling-origin backtest for every method and choose the one with the lowest error.
Test weeks. Run the backtest again for the chosen method only, next to the naive baseline, and report that number. Nothing is chosen or tuned here.
The validation error of the winner is usually optimistic; the test error is the honest estimate. If you go back and change methods after seeing the test number, the test weeks have become validation weeks, and you need a fresh test period.
Any actual is zero (undefined) or tiny (huge); it penalizes over- and under-forecasts unevenly
WMAPE
Total error divided by total demand
A group of materials
Big sellers dominate; small ones vanish
MASE
MAE divided by a naive method's in-sample MAE
Comparing across materials, including zeros
The training period was unusually calm or wild
Bias
Total error divided by total demand, keeping the sign
Spotting a lean to over or under
Read alone; a zero bias can hide large errors that cancel
MASE comes from Hyndman and Koehler (2006). The scale is the training-period MAE of the naive forecast, or the seasonal naive for seasonal series, so MASE below 1 means better than that benchmark did in training. It stays defined when demand has zero weeks, which is why it suits mixed portfolios. Researchers at KIT note that MAPE gives infinite or undefined values when actuals are zero or near zero, and call MAPE and RMSE unsuited to lumpy and intermittent demand.
A spare part used in a quarter of all weeks breaks most intuitions. The naive forecast is often zero. Exponential smoothing produces a small fractional number every week, which is never "right" but can be right on average. Treat these materials as their own class: measure with MAE, MASE and bias, and question whether a weekly statistical forecast is the right tool at all. Reorder-point planning or a stocking policy may serve them better.
#Build it yourself: forecast weekly demand and beat the naive forecast
You will run one script that reads three years of weekly goods-issue quantities for four materials and backtests four methods at a four-week horizon. It chooses a method per material on 26 validation weeks, reports that method once on the last 26 weeks, and writes an eight-week forecast per material. Then you will cheat on purpose to see how much a "fitted" accuracy flatters a model.
Before you start: complete Set up your computer for this course and Set up for Unit 2: data science tools. They give you the orchestrate-course folder, its .venv, the unit02 folder, and numpy, pandas and matplotlib. This walkthrough doesn't repeat those steps. It adds one library, statsmodels.
flowchart LR
D[Weekly quantities per material] --> S[One series per material]
S --> V[Backtest on validation weeks]
V --> P[Choose a method per material]
P --> T[Report it once on test weeks]
T --> F[forecast_next_weeks.csv]
What success looks like:0.15.0. The script was tested with statsmodels 0.15.0, pandas 3.1 and numpy 2.5; other recent versions should work but may change a last digit.
In VS Code's file list, right-click unit02, choose New File and name it forecast_demand.py.
Paste the code below and save (Ctrl+S, or Cmd+S on Mac).
"""Time-series forecasting for SAP planning: weekly demand per material, tested against naive baselines.
How to run (from the unit02 folder, with the course .venv turned on):
python forecast_demand.py --sample # 3 years of made-up weekly goods issues for 4 materials
python forecast_demand.py --sample --peek # cheat: fit Holt-Winters on every week, test weeks included
python forecast_demand.py --sample --horizon 8 # judge forecasts 8 weeks ahead instead of 4
python forecast_demand.py --file my_demand.csv # your own extract: Material, Plant, WeekStart, Quantity
python forecast_demand.py --sample --plot # also save one chart per material (needs matplotlib)
"""
import argparse
import sys
import warnings
import numpy as np
import pandas as pd
try:
from statsmodels.tsa.holtwinters import ExponentialSmoothing
except ImportError:
sys.exit("The statsmodels library is missing. Run: pip install -r requirements.txt")
SEASON = 52 # weeks in a year: the length of one seasonal cycle
MOVING_WEEKS = 8 # how many recent weeks the moving average uses
VALID_WEEKS = 26 # half a year before the test: used to CHOOSE the method per material
TEST_WEEKS = 26 # the last half year: used once, to REPORT the chosen method; never to choose
# ---------------------------------------------------------------- data
def make_sample(seed=7):
"""Three years of weekly goods-issue quantities for four made-up materials at plant 1010."""
rng = np.random.default_rng(seed)
weeks = pd.date_range("2023-10-02", periods=156, freq="W-MON")
t = np.arange(len(weeks))
year_wave = np.sin(2 * np.pi * (t - 13) / SEASON) # peaks around early summer
patterns = {
# Material: (description, weekly quantities)
"FG-1001": ("Steady seller", 200 + rng.normal(0, 18, len(t))),
"FG-1002": ("Growing product", 120 + 0.9 * t + rng.normal(0, 14, len(t))),
"FG-2001": ("Seasonal: garden pump", 150 + 110 * year_wave + rng.normal(0, 15, len(t))),
"SP-3001": ("Spare part, intermittent", rng.poisson(3, len(t)) * (rng.random(len(t)) < 0.3)),
}
rows = []
for material, (text, qty) in patterns.items():
qty = np.clip(np.round(qty), 0, None)
if material == "FG-1001":
qty[120] = qty[120] + 420 # one big one-off order: an outlier, not a pattern
for week, q in zip(weeks, qty):
rows.append({"Material": material, "MaterialText": text, "Plant": "1010",
"WeekStart": week, "Quantity": float(q), "BaseUnit": "PC"})
return pd.DataFrame(rows)
def load_file(path):
try:
df = pd.read_csv(path)
except FileNotFoundError:
sys.exit(f"Stopped: no file called {path} in this folder. Check the name, or use --sample.")
missing = {"Material", "WeekStart", "Quantity"} - set(df.columns)
if missing:
sys.exit(f"Stopped: {path} needs the columns Material, WeekStart and Quantity. Missing: {sorted(missing)}")
df["WeekStart"] = pd.to_datetime(df["WeekStart"])
df["Material"] = df["Material"].astype(str)
if "MaterialText" not in df:
df["MaterialText"] = ""
df["MaterialText"] = df["MaterialText"].fillna("")
return df
def weekly_series(df, material):
"""One continuous weekly series; weeks with no goods issue become 0, not 'missing'."""
part = df[df["Material"] == material]
s = part.groupby("WeekStart")["Quantity"].sum().sort_index()
full = pd.date_range(s.index.min(), s.index.max(), freq="7D")
return s.reindex(full, fill_value=0.0)
# ---------------------------------------------------------------- forecasting methods
# Each method gets only the history (a pandas Series) and returns the next `h` weeks.
def naive(history, h):
return np.repeat(history.iloc[-1], h)
def seasonal_naive(history, h):
if len(history) < SEASON:
return naive(history, h)
last_year = history.iloc[-SEASON:].to_numpy()
return np.array([last_year[i % SEASON] for i in range(h)])
def moving_average(history, h):
return np.repeat(history.iloc[-MOVING_WEEKS:].mean(), h)
def fit_holt_winters(values):
"""Exponential smoothing with a damped trend, plus a yearly season once two full years exist."""
seasonal = "add" if len(values) >= 2 * SEASON else None
model = ExponentialSmoothing(
values,
trend="add",
damped_trend=True,
seasonal=seasonal,
seasonal_periods=SEASON if seasonal else None,
initialization_method="estimated",
)
with warnings.catch_warnings():
warnings.simplefilter("ignore") # convergence notes on short or spiky series
return model.fit()
def holt_winters(history, h):
fit = fit_holt_winters(history.to_numpy())
return np.clip(fit.forecast(h), 0, None) # demand can't be negative
METHODS = {
"Naive (last week)": naive,
"Seasonal naive": seasonal_naive,
f"Moving average {MOVING_WEEKS}w": moving_average,
"Holt-Winters": holt_winters,
}
# ---------------------------------------------------------------- evaluation
def backtest(series, method, h, start, end, peek=False):
"""Rolling origin from week `start` up to week `end`: at each origin, forecast h weeks from the history before it."""
actual, forecast = [], []
for origin in range(start, end - h + 1, h):
if peek and method is holt_winters:
fc = peeked_holt_winters(series, origin, h)
else:
fc = method(series.iloc[:origin], h) # only weeks before the origin
actual.extend(series.iloc[origin:origin + h])
forecast.extend(fc)
return np.array(actual), np.array(forecast)
def peeked_holt_winters(series, origin, h):
"""For --peek, Holt-Winters only: fit on ALL weeks (the scored weeks included), read off fitted values.
The three simple methods have no weights to fit, so they always use only the history before each origin."""
fit = fit_holt_winters(series.to_numpy())
return np.clip(fit.fittedvalues[origin:origin + h], 0, None)
def scores(actual, forecast, train):
"""MAE, MASE, MAPE and bias. MASE divides by the seasonal naive's error on the training weeks."""
err = forecast - actual
mae = np.mean(np.abs(err))
if len(train) > SEASON:
scale = np.mean(np.abs(train.to_numpy()[SEASON:] - train.to_numpy()[:-SEASON]))
else:
scale = np.mean(np.abs(np.diff(train.to_numpy())))
mase = mae / scale if scale > 0 else np.nan
if np.any(actual == 0):
mape = np.nan # undefined: dividing by a zero week
else:
mape = np.mean(np.abs(err) / actual) * 100
bias = err.sum() / max(actual.sum(), 1e-9) * 100 # + means over-forecast
return mae, mase, mape, bias
def print_table(rows):
print(f" {'Method':<20} {'MAE':>6} {'MASE':>6} {'MAPE':>6} {'Bias':>6}")
for name, (mae, mase, mape, bias) in rows.items():
print(f" {name:<20} {fmt(mae):>6} {fmt(mase):>6} {fmt(mape, True):>6} {bias:>+5.0f}%")
def fmt(x, pct=False):
if np.isnan(x):
return "n/a"
return f"{x:.0f}%" if pct else f"{x:.2f}" if x < 10 else f"{x:.0f}"
# ---------------------------------------------------------------- main
def main():
parser = argparse.ArgumentParser(description="Forecast weekly demand per material and test it against baselines.")
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument("--sample", action="store_true", help="use made-up SAP-shaped data")
source.add_argument("--file", help="CSV with Material, WeekStart, Quantity (Plant and MaterialText optional)")
parser.add_argument("--horizon", type=int, default=4, help="weeks ahead to judge, e.g. the replenishment lead time")
parser.add_argument("--peek", action="store_true",
help="fit Holt-Winters on every week, scored weeks included (to show why you must not)")
parser.add_argument("--plot", action="store_true", help="save forecast_<material>.png charts")
args = parser.parse_args()
if args.sample:
df = make_sample()
df.to_csv("sample_weekly_demand.csv", index=False)
print(f"Source: made-up sample. {len(df)} rows saved as sample_weekly_demand.csv.")
else:
df = load_file(args.file)
print(f"Source: {args.file}. {len(df)} rows.")
if args.peek:
print("PEEK MODE: Holt-Winters is fitted on every week, including the weeks it is scored on. Not honest.")
print(f"Validation: {VALID_WEEKS} weeks to choose a method. Test: the last {TEST_WEEKS} weeks, used once to report it.")
print(f"Every forecast is {args.horizon} weeks ahead, from history before its origin.\n")
next_rows = []
for material in sorted(df["Material"].unique()):
series = weekly_series(df, material)
text = df.loc[df["Material"] == material, "MaterialText"].iloc[0]
n = len(series)
needed = VALID_WEEKS + TEST_WEEKS + 2 * args.horizon + 8
if n < needed:
print(f"{material}: only {n} weeks of history, skipped (needs at least {needed}).\n")
continue
valid_start, test_start = n - VALID_WEEKS - TEST_WEEKS, n - TEST_WEEKS
zero_share = (series == 0).mean() * 100
print(f"{material} {text} ({n} weeks, {zero_share:.0f}% zero weeks)")
# 1. Choose on the validation weeks. The test weeks play no part in this choice.
valid = {}
for name, method in METHODS.items():
actual, forecast = backtest(series, method, args.horizon, valid_start, test_start, peek=args.peek)
valid[name] = scores(actual, forecast, series.iloc[:valid_start])
print(f" Validation (weeks {valid_start + 1}-{test_start}): choose the method")
print_table(valid)
best = min(valid, key=lambda k: valid[k][0])
baseline = min(("Naive (last week)", "Seasonal naive"), key=lambda k: valid[k][0])
print(f" Chosen: {best}" + (" (a naive baseline: nothing beat it)" if best == baseline else ""))
# 2. Report the chosen method once on the untouched test weeks, next to the naive baseline.
test = {}
for name in dict.fromkeys([best, baseline]):
actual, forecast = backtest(series, METHODS[name], args.horizon, test_start, n, peek=args.peek)
test[name] = scores(actual, forecast, series.iloc[:test_start])
print(f" Test (weeks {test_start + 1}-{n}): reported once, never used to choose")
print_table(test)
if best != baseline:
gain = (1 - test[best][0] / test[baseline][0]) * 100
word = "better" if gain >= 0 else "worse"
print(f" On the test weeks, {best} is {abs(gain):.0f}% {word} than {baseline} on MAE.")
print()
fc = METHODS[best](series, 8)
for i, q in enumerate(fc):
next_rows.append({"Material": material, "WeekStart": (series.index[-1] + pd.Timedelta(weeks=i + 1)).date(),
"Method": best, "ForecastQuantity": round(float(q))})
if args.plot:
save_plot(material, series, fc, best)
if next_rows:
out = pd.DataFrame(next_rows)
out.to_csv("forecast_next_weeks.csv", index=False)
print("Next 8 weeks, best method per material:")
print(out.pivot(index="WeekStart", columns="Material", values="ForecastQuantity").head(4).to_string())
print("...\nWrote forecast_next_weeks.csv")
else:
print("No material had enough weekly history to test. Use --sample, or a longer extract.")
def save_plot(material, series, fc, method):
try:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
except ImportError:
print(" matplotlib is missing; skipping the chart. Run: pip install -r requirements.txt")
return
future = pd.date_range(series.index[-1] + pd.Timedelta(weeks=1), periods=len(fc), freq="7D")
fig, ax = plt.subplots(figsize=(9, 3.5))
ax.plot(series.index, series.values, label="Actual weekly quantity")
ax.plot(future, fc, label=f"Forecast: {method}")
ax.axvline(series.index[-VALID_WEEKS - TEST_WEEKS], linestyle=":", color="grey", label="Start of validation")
ax.axvline(series.index[-TEST_WEEKS], linestyle="--", color="grey", label="Start of test")
ax.set_title(material)
ax.legend(loc="best", fontsize=8)
fig.tight_layout()
fig.savefig(f"forecast_{material}.png", dpi=110)
plt.close(fig)
print(f" Saved forecast_{material}.png")
if __name__ == "__main__":
main()
It takes about 5 to 15 seconds, because Holt-Winters is refitted at every origin. What success looks like:
Source: made-up sample. 624 rows saved as sample_weekly_demand.csv.
Validation: 26 weeks to choose a method. Test: the last 26 weeks, used once to report it.
Every forecast is 4 weeks ahead, from history before its origin.
FG-1001 Steady seller (156 weeks, 0% zero weeks)
Validation (weeks 105-130): choose the method
Method MAE MASE MAPE Bias
Naive (last week) 40 2.11 14% -6%
Seasonal naive 30 1.61 9% -6%
Moving average 8w 35 1.89 12% -3%
Holt-Winters 28 1.48 8% -6%
Chosen: Holt-Winters
Test (weeks 131-156): reported once, never used to choose
Method MAE MASE MAPE Bias
Holt-Winters 16 0.69 8% +2%
Seasonal naive 16 0.71 8% -0%
On the test weeks, Holt-Winters is 2% better than Seasonal naive on MAE.
FG-1002 Growing product (156 weeks, 0% zero weeks)
Validation (weeks 105-130): choose the method
Method MAE MASE MAPE Bias
Naive (last week) 16 0.39 7% -0%
Seasonal naive 53 1.26 23% -24%
Moving average 8w 12 0.28 5% -2%
Holt-Winters 17 0.40 7% -4%
Chosen: Moving average 8w
Test (weeks 131-156): reported once, never used to choose
Method MAE MASE MAPE Bias
Moving average 8w 9.85 0.22 4% -2%
Naive (last week) 15 0.33 6% +1%
On the test weeks, Moving average 8w is 35% better than Naive (last week) on MAE.
FG-2001 Seasonal: garden pump (156 weeks, 0% zero weeks)
Validation (weeks 105-130): choose the method
Method MAE MASE MAPE Bias
Naive (last week) 30 1.62 27% -21%
Seasonal naive 19 1.04 23% -0%
Moving average 8w 48 2.54 36% -32%
Holt-Winters 14 0.74 16% -0%
Chosen: Holt-Winters
Test (weeks 131-156): reported once, never used to choose
Method MAE MASE MAPE Bias
Holt-Winters 12 0.62 16% +2%
Seasonal naive 15 0.77 16% +1%
On the test weeks, Holt-Winters is 19% better than Seasonal naive on MAE.
SP-3001 Spare part, intermittent (156 weeks, 74% zero weeks)
Validation (weeks 105-130): choose the method
Method MAE MASE MAPE Bias
Naive (last week) 2.04 1.25 n/a +44%
Seasonal naive 1.12 0.69 n/a -28%
Moving average 8w 1.47 0.90 n/a -18%
Holt-Winters 1.35 0.82 n/a -34%
Chosen: Seasonal naive (a naive baseline: nothing beat it)
Test (weeks 131-156): reported once, never used to choose
Method MAE MASE MAPE Bias
Seasonal naive 1.50 1.04 n/a +38%
Next 8 weeks, best method per material:
Material FG-1001 FG-1002 FG-2001 SP-3001
WeekStart
2026-09-28 204 258 44 0
2026-10-05 210 258 42 0
2026-10-12 207 258 41 4
2026-10-19 210 258 38 4
...
Wrote forecast_next_weeks.csv
Your numbers should match, because the sample uses a fixed random seed. A different last digit is fine; library versions can cause that. Two files appear in unit02: sample_weekly_demand.csv, the input, and forecast_next_weeks.csv, the forecast.
Each material gets two tables. The validation table compares all four methods and picks one. The test table shows only the chosen method and the naive baseline, on weeks that played no part in the choice.
FG-1001, the steady seller. Holt-Winters wins validation, but every MASE there is above 1: those weeks were harder than training, because week 121 holds a one-off order of 420 extra pieces that no method should predict. On the test weeks Holt-Winters is only 2% better than seasonal naive. That's too small a gain to justify a fitted model; in a real project you'd keep seasonal naive and ask whether that order belongs in the history at all.
FG-1002, the growing product. Seasonal naive is worst in validation, with a bias of -24%: last year's quantities are too low for a growing product. The 8-week moving average is chosen and beats the naive forecast by 35% on the test weeks. Holt-Winters loses here, probably because it also fits a 52-week season to a product that has none, adding noise. More flexible isn't always better.
FG-2001, the garden pump. The moving average is worst by far in validation (MAE 48): an average of the last 8 weeks can't see summer coming. Holt-Winters is chosen and holds up on the test weeks, 19% better than seasonal naive.
SP-3001, the spare part. 74% of weeks are zero, so MAPE is n/a for every method. MAE and MASE still work. Seasonal naive wins validation with MAE 1.12, then scores 1.50 on the test weeks, with a +38% bias. That drop is the point of this design: a method that wins a contest looks better on the contest weeks than on new ones. Had you reported 1.12, you'd have promised too much. Intermittent demand is also simply hard; no method is impressive here.
The forecast file. Open forecast_next_weeks.csv. Each material gets eight weeks from its chosen method, refitted on all 156 weeks. The garden pump forecast is low because the next weeks are autumn.
Run the script with --peek. This changes exactly one thing: Holt-Winters is fitted once on all 156 weeks, and its "forecasts" for the validation and test weeks are its fitted values, one-week-ahead estimates from a model that has already seen those weeks. The naive, seasonal naive and moving-average methods have no weights to fit, so they still use only the weeks before each origin and their numbers don't change.
python forecast_demand.py --sample --peek
What success looks like (Holt-Winters validation lines and the test lines only):
PEEK MODE: Holt-Winters is fitted on every week, including the weeks it is scored on. Not honest.
FG-1001 Validation: Holt-Winters 19 1.04 6% -3%
Test: Holt-Winters 9.44 0.42 5% +1%
FG-1002 Validation: Holt-Winters 9.11 0.22 4% -1%
Test: Holt-Winters 12 0.26 5% -0%
FG-2001 Validation: Holt-Winters 9.13 0.49 11% +0%
Test: Holt-Winters 7.88 0.42 11% +2%
SP-3001 Validation: Holt-Winters 0.90 0.55 n/a -22%
Test: Holt-Winters 0.88 0.61 n/a +49%
The script prints the full tables; the lines above are trimmed to the ones that change. Holt-Winters is now chosen for all four materials, and the steady seller's test gain over seasonal naive jumps from 2% to 41%. None of that improvement is real. This is what a vendor slide showing "fitted accuracy" or "in-sample MAPE" is reporting. Always ask for the backtest, and ask which weeks were used to pick the model.
Run the backtest eight weeks ahead, as if the supplier's lead time doubled:
python forecast_demand.py --sample --horizon 8
Compare the validation tables with Step 4. For example, the naive forecast for the garden pump gets much worse (MAE 30 to 41), while seasonal naive stays at 19. A method that copies last week falls behind as the season turns; a method that copies last year doesn't care how far ahead it looks. Pick the horizon your planners commit at, not the one that looks best.
If you have weekly quantities from a system you are allowed to use, save them as a CSV file in unit02 with these columns, one row per material and week:
The script needs at least 68 weeks per material at a four-week horizon (26 validation weeks, 26 test weeks and some history before them). Holt-Winters adds a yearly season only at origins with two full years of history before them, so with less than about 2.5 years it runs without a season. Weeks missing between the first and last week are treated as zero demand.
Builds 156 weeks for four materials: steady (with one outlier), trend, yearly season and intermittent
load_file
Reads your CSV and checks the three required columns
weekly_series
One continuous weekly series per material; missing weeks become 0
naive, seasonal_naive, moving_average
The free baselines: last week, the same week last year, the last 8 weeks' mean
fit_holt_winters, holt_winters
statsmodels ExponentialSmoothing with an additive damped trend and, after two years, an additive 52-week season; negative forecasts are set to 0
backtest
Rolling origin between two weeks you give it: refit on history before each origin, forecast h weeks, collect actuals and forecasts
peeked_holt_winters
Only for --peek, only for Holt-Winters: fits on all weeks and reads off fitted values
scores
MAE, MASE (scaled by the training-period seasonal naive error), MAPE (n/a if any actual is 0) and bias
Main loop
Per material: backtests every method on the validation weeks and picks the lowest MAE; backtests only that method and the naive baseline on the test weeks; writes eight weeks to forecast_next_weeks.csv
In S/4HANA, the forecast reaches MRP as demand. SAP Learning describes classic forecast-based planning: forecasting tools predict consumption from historical values, and the forecast values act as requirements in the planning run, reduced by each goods withdrawal. SAP Learning also says this classic procedure is in the compatibility package, to be discontinued by the end of 2025, or under certain circumstances by the end of 2030, and recommends converting forecast values into planned independent requirements instead. Check SAP Notes 2268095 and 2269324 for your release.
The model choices look familiar after this topic. SAP PRESS describes constant, trend, seasonal and seasonal-trend models in the S/4HANA material forecast, and an automatic model selection (model J) that proposes a model from history and should be reviewed regularly. Confirm the model list and parameters in your release's documentation.
Where forecasts go wrong, planners see it as MRP exceptions: shortages in material coverage apps and exception messages such as rescheduling in or out in the stock/requirements list. A forecast error you measure in a backtest is a count of future exceptions.
Predictive material and resource planning (pMRP) in S/4HANA starts from PIRs, builds a simplified requirement plan and shows capacity issues early. Planners can change demand quantities, capacity or sourcing in a simulation and compare it with the reference plan; releasing the simulation creates new PIRs. Use it after forecasting: your forecast says how much, pMRP says whether you can make it.
SAP Learning's course on AI in SAP IBP describes about 20 forecasting algorithms, including regression, gradient boosting and XGBoost, several of which can run in one forecast model with the best fit chosen. It also lists external variables such as price and promotions, demand sensing for daily or weekly short-term forecasts from recent orders and deliveries, and the option to integrate your own machine learning model. IBP is a separate, licensed SAP cloud product; this topic doesn't use it.
For teams building their own forecasts on data in SAP HANA, the Predictive Analysis Library offers AutoExponentialSmoothing. With model_selection=True it chooses among single, double and triple exponential smoothing, including damped variants. accuracy_measure sets what it optimizes ('mse' or 'mape'), and seasonality_criterion sets how strong a season must be before it is used. The separate accuracy_measure function scores forecasts with mape, wmape, smape, mase, mad, rmse and more, with ignore_zero for percentage errors. It also offers spec, the stock-keeping-oriented cost measure from the KIT paper for intermittent demand.
Agree the target with the business. Consumption, sales orders or shipments differ. Shipments after a stock-out undercount true demand, so a forecast trained on them learns the shortage.
Clean history, visibly. Remove or cap one-off orders and stock-out periods before fitting, and keep the original values. Record every correction, so a planner can see why the forecast ignored a spike.
Backtest at the lead time, per material class. Report MAE, MASE and bias by class: steady, trending, seasonal, intermittent, new. Use WMAPE only for groups, and never MAPE on materials with zero weeks.
Baselines stay in production. Keep scoring seasonal naive next to your model every cycle. If the model stops beating it, switch back automatically or alert.
Human override, logged. Planners know about the new customer or the lost contract. Let them override, record who and why, and measure whether overrides improve accuracy.
Authorizations. Demand history reveals customers, volumes and pricing power. Extract with a read-only technical user limited to the plants and materials in scope, and show forecasts only to users allowed to see that material's data.
Clean core. Read history through released APIs or extraction paths, compute forecasts side by side, and write results back as PIRs through released interfaces, with a person approving the upload. Don't write into standard planning tables.
Cost and runtime. Refitting exponential smoothing per material and origin is cheap per series but adds up over tens of thousands of materials. Fit simple methods everywhere and fitted models only where they earn their keep.
No baseline. An accuracy number without the naive forecast's number on the same weeks proves nothing.
Fitted values as accuracy. In-sample errors are always smaller, as Step 6 showed.
Shuffled or random test splits. The future leaks into training. Always split by date.
The wrong horizon. One-week-ahead errors understate what buyers will face at a four- or eight-week lead time.
MAPE on zero weeks. Undefined, or silently dropped by a tool. Check how your tool treats zeros.
Seasonality from too little history. A 52-week season needs at least two full years. With less, the model fits noise.
Learning the stock-out. Weeks with zero sales because nothing was in stock aren't zero demand.
Choosing on the test weeks. If you try ten methods and keep the best on the test weeks, those weeks are now part of the selection and their error is too optimistic. Choose on validation weeks, report once on later test weeks, as the script does. If you change methods after seeing the test number, you need a fresh test period.
#Exercise: add a method and write the forecast note
You will add one more method, test it honestly, and write a note that Unit 6 can turn into an API design when you wrap the forecast in a service.
Copy the script. In the terminal, inside unit02 with (.venv) showing:
In VS Code, create unit02/forecast_results.md with these sections:
Target: what is forecast, in which unit and bucket.
Horizon: the lead time you tested at, and why.
Results: a validation table with MAE, MASE and bias for all five methods and four materials, plus the test numbers of the chosen method and the naive baseline.
Decision per material: the method chosen on validation, and whether it still beats the naive baseline on the test weeks.
What not to forecast statistically: which material, and what you'd do instead.
Save your work from the course folder:
git add unit02/forecast_demand_v2.py unit02/forecast_results.md
git commit -m "Forecast note and seasonal naive with level adjustment"
Done when:forecast_demand_v2.py runs with five methods; the note gives the target, the horizon, a results table and a decision per material made on validation numbers and reported with test numbers, never fitted ones; and git log shows the commit.
For readers who want the theory behind the methods and measures used here.
Exponential smoothing as a model. Holt-Winters started as a recipe, but it can be written as a state-space model with an error term, the ETS family (error, trend, seasonal). That view gives prediction intervals and a principled way to choose between additive and multiplicative versions. The statsmodels documentation for ExponentialSmoothing lists the parameters used here; statsmodels also has a separate ETS model class.
Why MASE. Hyndman and Koehler (2006), "Another look at measures of forecast accuracy", show that many common measures are degenerate in ordinary situations and propose scaling errors by a naive method's in-sample error. The documentation of Hyndman's accuracy function summarizes the scaling choices: naive for non-seasonal series, seasonal naive for seasonal ones.
Intermittent demand and cost. Martin, Spitzer and Kühl propose SPEC, a measure that charges for stock held and for unmet demand over time rather than for the error in each period alone. It is also one of the options of PAL's accuracy_measure, so you can compare it on HANA and in Python.
Prediction intervals over point forecasts. Safety stock is about the spread of errors, not their average. A next step is to save the backtest errors per material and use their spread at the lead time to size safety stock, then compare with what MRP uses today.
Many series at once. Global models, trained across thousands of materials with features such as material group and price, are how machine learning methods like gradient boosting enter forecasting, including in IBP. They need the same backtest discipline, with the extra risk that one material's future leaks into another's training through shared features.
Pick one answer for each question. The explanation appears after you choose.
1Why does the script compare every method with naive and seasonal naive forecasts?
Answer: C. Naive and seasonal naive cost nothing to run, so they set the bar. A fitted method that can't beat them on the same test weeks adds cost and complexity without value.
2In the rolling-origin backtest, what does the model see at each origin?
Answer: D. Each forecast must use only what was known at the origin, like a real planning run. Fitting on later weeks leaks the answer, as the --peek run showed.
3The --peek run made Holt-Winters the chosen method for every material. What explains it?
Answer: C. In peek mode, Holt-Winters is fitted on all weeks and its fitted values are read off for the validation and test weeks. It has seen the answers, so its errors shrink without any real improvement.
4Why does the script choose each material's method on validation weeks and report it on separate test weeks?
Answer: C. Picking the lowest error among several methods favors whichever got lucky on those weeks. Reporting on later, untouched weeks gives an honest estimate, as the spare part's drop from 1.12 to 1.50 showed.
5For the spare part SP-3001, MAPE shows n/a. What is the right reading?
Answer: A. MAPE divides by actual demand, which is zero in most of that part's weeks. MAE, MASE and bias stay defined, which is why they suit intermittent demand.
6All four methods score MASE above 1 on FG-1001's validation weeks. What does that tell you?
Answer: C. MASE compares the error with the seasonal naive's training error. A one-off order of 420 pieces in the validation weeks raises every method's error; the question is whether that order belongs in the history.
7A planner says buyers commit eight weeks ahead, but your report shows four-week errors. What do you do?
Answer: D. Accuracy must be measured at the horizon where decisions are made. Errors grow with the horizon, as the naive forecast's jump for the garden pump showed.
8You want to forecast with SAP HANA's Predictive Analysis Library and pick the best smoothing model per material. Which option fits?
Answer: A. AutoExponentialSmoothing with model_selection=True chooses among single, double and triple exponential smoothing, including damped variants. accuracy_measure scores forecasts after the fact, and ExponentialSmoothing is the statsmodels class.
9Your forecasts will be uploaded to S/4HANA as demand for MRP. Which design keeps the core clean and safe?
Answer: B. Forecasts change what MRP buys and produces, so a person approves the upload. Released interfaces keep the core clean, and a limited technical user keeps access narrow.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Related topics
Picked from the terms, links and tags this topic shares with others.
Orchestrate Lab is a free practice ERP in your browser, built around the fictional Calder Pumpworks. These are the places in it where you do what this topic teaches.
AI stopPaying and closingAt the end of stage 11, "Pay and close the month".
AI stopDemand planningAt the end of stage 11, "Pay and close the month".
Stages build on each other: a stage link opens the core path at that stage. To start there without doing the earlier stages yourself, use Do this stage for me on the stage before it. A chapter starts inside your own company once the core path is done (or the lab finishes the core for you first); drills and exercises start from their own prepared copy. Everything is saved in this browser only.
Sources
Applying AI in Demand and Inventory Planning (SAP Learning, Discovering AI in SAP Integrated Business Planning)— about 20 forecasting algorithms in SAP IBP, including regression and machine learning (gradient boosting, XGBoost); several algorithms in one model with best-fit selection; external variables such as price, promotions and events; demand sensing for daily or weekly short-term forecasts from orders, deliveries and other signals; own machine learning models can be integrated
Understanding the Net Requirements Calculation for Forecast-Based Planning (SAP Learning)— forecasting tools predict consumption from historical values; in classic forecast-based planning, forecast values are requirements reduced by withdrawals; classic forecast-based planning is part of the compatibility package and will be discontinued (end of 2025, or under certain circumstances end of 2030); SAP recommends converting forecast values into planned independent requirements; SAP Notes 2268095 and 2269324
Introducing Predictive MRP (pMRP) (SAP Learning)— simulation that finds capacity issues early from a simplified requirement plan; planned independent requirements are the top-level demand; releasing a simulation creates new planned independent requirements; successor to long-term planning in SAP ERP
How to Evaluate MRP Results with SAP S/4HANA (SAP PRESS blog)— Monitor and Manage Material Coverage apps for shortages; exception messages in the stock/requirements list, including rescheduling in and out. Publisher source, not SAP documentation