Orchestrate

Report an issue

Something wrong, out of date or unclear? Tell us. Corrections are prioritized over new topics. You don't need to sign in.

0 / 2000

Don't include personal or confidential company details. Reports are reviewed with AI assistance.

Time-series forecasting for SAP planning

Forecast weekly material demand, test it honestly against a naive forecast with a time-based backtest, and know which error measure to trust.

Updated Oct 23, 2026Foundational 8 minDeep 40 min
Foundational layer · 8 min read

The 60-second version

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.

Why it matters to the business

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.

How SAP does it

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.

Reading a forecast report: a decision guide

What you see What it may mean What to ask
One accuracy number for all materials Big sellers hide the rest "Show it by material group, and for the top 20 materials"
Accuracy without a baseline Nobody knows if the method adds anything "How does the naive forecast score on the same weeks?"
MAPE for spare parts Weeks with zero demand break MAPE "How are zero weeks handled? Is there a scaled measure?"
Errors measured one week ahead Lead time is longer, so real errors are bigger "What's the error at our replenishment lead time?"
Error tested on the training period The method has seen the answers "Which weeks were held back, and was anything tuned on them?"
Good accuracy, persistent shortages The forecast is biased low "What's the bias: over or under, by how much?"
A new model "beats" the old one by 2% Within normal noise "Is the gain stable across several test periods?"

Questions to ask

  1. What does the forecast predict exactly: consumption, sales orders or shipments, at which plant, in which bucket (day, week, month)?
  2. What naive forecast is it compared with, and by how much does it win, material by material?
  3. At what lead time is accuracy measured, and does that match how far ahead purchasing and production commit?
  4. Which error measures are reported, and how are materials with zero-demand weeks scored?
  5. Is the forecast biased? Over a quarter, does it run consistently high or low?
  6. Which materials are not forecast statistically (new, intermittent, project-driven), and who plans them?
  7. How do one-off events, such as a large single order or a stock-out, get cleaned out of the history?
  8. How does the forecast reach MRP: PIRs, classic forecast-based planning, or an external plan imported from IBP?
  9. Who reviews the forecast before it drives purchasing, and how is a manual override recorded?

Common misconceptions

  • "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.

Key terms

All course terms, A to Z, in the glossary

  • Time series: values measured at regular intervals, such as weekly goods issues for one material.
  • Level, trend, seasonality, noise: the usual amount, its slow drift, its repeating shape and the random rest.
  • Naive forecast: next period equals the last period. Seasonal naive: next period equals the same period one cycle ago.
  • Horizon (lead time): how far ahead a forecast must be right to be useful.
  • Backtest: replaying the past, forecasting from older data and comparing with what happened next.
  • MAE: mean absolute error, in units such as pieces.
  • MAPE: mean absolute percentage error; undefined when actual demand is zero.
  • MASE: mean absolute scaled error; below 1 means better than the naive benchmark it is scaled by.
  • Bias: whether errors lean one way, consistently over- or under-forecasting.
  • Intermittent demand: demand with many zero periods, typical of spare parts.
  • Planned independent requirements (PIRs): planned demand in S/4HANA that MRP plans against.

Check yourself

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.
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.

How it works

The patterns in a demand 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.

Four methods, from free to fitted

  1. Naive. Next weeks equal the last week. Excellent when demand drifts; useless with seasonality.
  2. Seasonal naive. Next weeks equal the same weeks one cycle ago. Strong for stable seasonal products; misses a trend.
  3. Moving average. The mean of the last k weeks (here 8). Smooths noise, but lags behind trends and can't produce a season.
  4. 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.

Testing by time: the rolling-origin backtest

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.

Choose on one period, report on another

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]
  1. Training history. Each method fits only on weeks before each origin.
  2. Validation weeks. Run the rolling-origin backtest for every method and choose the one with the lowest error.
  3. 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.

Error measures and when each misleads

Measure Formula in words Good for Misleads when
MAE Average size of the error, in pieces One material, in its own unit Comparing materials of different sizes
MAPE Average error as a percentage of actual demand Talking to business users 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.

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 you need

  • The course folder, .venv and unit02 folder from the two setup topics.
  • About 40 minutes. No account, no key, no internet after installing statsmodels: --sample builds the data on your computer.
  • Optional: your own weekly extract as a CSV file with the columns Material, WeekStart and Quantity.

Step 1: Open the course folder and turn on the environment

  1. Open VS Code, choose File > Open Folder and open orchestrate-course.

  2. Open a terminal: Terminal > New Terminal.

  3. Turn on the virtual environment if the prompt doesn't start with (.venv):

    • Windows (PowerShell):

      .venv\Scripts\Activate.ps1
    • macOS / Linux:

      source .venv/bin/activate

Step 2: Add statsmodels

  1. In VS Code, open requirements.txt in the course folder and add this line at the end:

    statsmodels==0.15.0
  2. Save the file, then install from the course folder (the same command on Windows and macOS/Linux):

    pip install -r requirements.txt
  3. Check that it imports:

    python -c "import statsmodels; print(statsmodels.__version__)"

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.

  1. Go into the Unit 2 folder:

    cd unit02

Step 3: Save the script

  1. In VS Code's file list, right-click unit02, choose New File and name it forecast_demand.py.
  2. 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()

Step 4: Run it on sample data

In the terminal, still inside unit02, run:

python forecast_demand.py --sample

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.

Step 5: Read the results

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Step 6: Cheat on purpose

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.

Step 7: Change the horizon

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.

Step 8 (optional): Use your own extract

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:

Material,WeekStart,Quantity
FG-1001,2023-10-02,200
FG-1001,2023-10-09,205

Then run:

python forecast_demand.py --file my_demand.csv

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.

Step 9: Save your work in Git

From the course folder (go up one level with cd .. first):

git add requirements.txt unit02/forecast_demand.py
git commit -m "Weekly demand forecast with naive baselines and a rolling-origin backtest"

What each part of the script does

Part What it does
make_sample 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
save_plot With --plot, saves one PNG chart per material

If something goes wrong

What you see What it means What to do
python is not recognized, or command not found Python isn't installed, or the terminal can't find it Windows: repeat Step 1 of the Unit 1 setup, then open a new terminal. macOS/Linux: use python3 until .venv is active
The statsmodels library is missing Step 2 didn't install into the Python you're using Check for (.venv) in the prompt, then run pip install -r requirements.txt from the course folder
ModuleNotFoundError: No module named 'pandas' The Unit 2 setup isn't complete in this environment Run pip install -r requirements.txt from the course folder
pip shows ProxyError, SSLError or Could not fetch URL Your network blocks PyPI Ask IT for access to pypi.org and files.pythonhosted.org, or an internal mirror
Stopped: no file called ... The CSV isn't in unit02, or the name differs Check the file name with dir (Windows) or ls (macOS/Linux)
Stopped: ... needs the columns Material, WeekStart and Quantity Your CSV headers differ Rename the columns in the first line of the file
only N weeks of history, skipped Too little history to test on a full year Expected for new materials. Use a longer extract
matplotlib is missing; skipping the chart Only matters for --plot Run pip install -r requirements.txt, or run without --plot
The run takes more than a minute Many materials, each refitted 13 times Try a few materials first; the simple methods are instant

Where this shows up in SAP

S/4HANA: from forecast to MRP

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.

pMRP: test the plan, not the forecast

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 IBP for demand

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.

SAP HANA: PAL through hana-ml

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.

Build vs. SAP

Situation Use Why
Learning, auditing a vendor's accuracy claim This script Every step is visible: baselines, horizon, backtest
Material forecasts inside S/4HANA MRP, standard patterns S/4HANA material forecast, converted to PIRs Planners stay in S/4HANA; standard integration with MRP
Checking whether capacity can meet the forecast pMRP Simulates capacity against PIRs before the MRP run
Many products, promotions, consensus demand planning SAP IBP for demand Many algorithms, external variables, planner workflows
Custom forecasts on data already in HANA PAL through hana-ml Runs where the data lives
A method none of these offer, or forecasts across SAP and non-SAP data Own model, fed by a read-only extract Full control; you own the operations

Production concerns

  • 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.

Pitfalls

  • 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.

  1. Copy the script. In the terminal, inside unit02 with (.venv) showing:

    • Windows (PowerShell):

      Copy-Item forecast_demand.py forecast_demand_v2.py
    • macOS / Linux:

      cp forecast_demand.py forecast_demand_v2.py
  2. Open forecast_demand_v2.py. Below the moving_average function, add a seasonal naive forecast adjusted for the change in level since last year:

    def seasonal_naive_drift(history, h):
        if len(history) < 2 * SEASON:
            return seasonal_naive(history, h)
        recent = history.iloc[-SEASON:].mean()
        year_before = history.iloc[-2 * SEASON:-SEASON].mean()
        base = seasonal_naive(history, h)
        return np.clip(base + (recent - year_before), 0, None)
  3. In the METHODS dictionary, add this line below the "Seasonal naive" line:

        "Seasonal naive + level": seasonal_naive_drift,
  4. Before running, write down which of the four materials you expect it to help, and why.

  5. Run both versions. Note the validation MAE, MASE and bias for every method and material, which method each version chooses, and its test numbers:

    python forecast_demand.py --sample
    python forecast_demand_v2.py --sample
  6. 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.
  7. 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.

Going further

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.

Check yourself

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.

Practice this in the lab

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.

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

Sign in to track your progress

We'll email you a one-time sign-in link. No password needed.

or

Tell us a little about you

Optional, every field. It helps us pitch answers to your questions at the right level and decide which topics to write next. It is never shown publicly, and you can change or clear it anytime from the account menu.

SAP areas you work in