Time-Series Forecasting: Baselines, Backtesting, Calendar Features, Prediction Intervals and the Newsvendor Rule
Hands-on lab · IDE in your browser

Time-Series Forecasting: Baselines, Backtesting, Calendar Features, Prediction Intervals and the Newsvendor Rule

Forecast a bakery's daily sales two weeks ahead. Beat naive and seasonal-naive baselines, backtest with rolling origins that never see the future, build calendar and lag features a 14-day forecast can actually know, turn backtest errors into prediction intervals and check their coverage, and choose how much to bake from the cost of a loaf left over versus a customer turned away.

Time
50 min
Checked steps
5
Level
Intermediate
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit50 min · 5 stepsSession running
4 / 5 steps passingHow many loaves · step 5 of 5
forecast.py▶ Run✓ Check
# ---------- Step 5: how many loaves to bake ---------- def critical_ratio(cost_leftover, cost_short):    """The service level that minimises expected cost: cost_short / (cost_short + cost_leftover)."""  def bake_plan(forecast, h, quantiles, q):    """Loaves to bake: forecast + the residual quantile q for each day's horizon step, rounded up, never below 0,    and 0 on days the forecast is 0 (the shop is closed)."""     def daily_cost(actual, baked, cost_leftover, cost_short):    """Mean cost per day: leftover loaves x cost_leftover + loaves short x cost_short."""  
TerminalOutput

The job

Crumb & Co's head baker orders flour two weeks ahead and bakes every morning. Too many loaves go in the bin; too few and customers leave empty-handed. You build the forecast, find out how far to trust it, and turn it into a baking plan.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Baselines

    Crumb & Co's Galway shop bakes every morning, and the head baker orders flour two weeks ahead.

    You writemae()
  2. 2

    Backtesting

    One fortnight is one draw of luck.

    You writeorigins()
  3. 3

    A model that knows the calendar

    Much of the future is known in advance: the weekday, the season and every bank holiday.

    You writefeatures()
  4. 4

    How sure is the forecast

    A forecast of 620 loaves is a guess, and planning needs its spread too.

    You writeresidual_quantiles()
  5. 5

    How many loaves

    The forecast is the middle of the range, and baking exactly that leaves the shop short half the time.

    You writecritical_ratio()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Baselines

Crumb & Co's Galway shop bakes every morning, and the head baker orders flour two weeks ahead. sales.csv holds three years of daily loaves sold. The shop is closed on bank holidays, shown by bank_holiday and 0 sales. The job is a 14-day forecast.

Every forecast starts from baselines, because a model that cannot beat them is not worth running. Naive repeats the last day. Seasonal naive repeats the last week, day for day, since Saturdays look like Saturdays.

Do this

1. Write mae(), naive() and seasonal_naive() in forecast.py.

2. Run it for the fortnight from Monday 3 November 2025. Before you look at the MAEs, predict which baseline wins. Then look at the Mondays.

forecast.py, the file you edit104 lines
"""Two-week sales forecasts for Crumb & Co's bakery. You write the functions marked TODO, one step at a time.

A forecaster is a function forecaster(history, future) -> numpy array: history is every row before the first
forecast day (date, loaves, bank_holiday); future holds the days to forecast, with date and bank_holiday only,
since the calendar is known in advance and the sales are not."""
import numpy as np
import pandas as pd
from sklearn.linear_model import Ridge

HORIZON = 14


def load(path="sales.csv"):
    df = pd.read_csv(path, parse_dates=["date"])
    return df


# ---------- Step 1: baselines ----------

def mae(actual, forecast):
    """Mean absolute error."""
    # TODO (Step 1): np.mean(np.abs(actual - forecast)) as a float.
    raise NotImplementedError("Step 1: write mae()")


def naive(history, future):
    """Every future day gets the last value seen."""
    # TODO (Step 1): np.full(len(future), the last loaves value).
    raise NotImplementedError("Step 1: write naive()")


def seasonal_naive(history, future):
    """Every future day gets the value of the same weekday in the last week of history (so day i of the future,
    counted from 0, gets history[-7 + i % 7])."""
    # TODO (Step 1): take the last 7 values of history["loaves"]; day i gets last_week[i % 7].
    raise NotImplementedError("Step 1: write seasonal_naive()")


# ---------- Step 2: backtesting ----------

def origins(df, first, last, step=7):
    """Forecast start dates from first to last (inclusive, pd.Timestamps or strings), every `step` days, keeping only
    those with HORIZON days of data from the start date on."""
    raise NotImplementedError("origins() arrives in Step 2")


def backtest(df, forecaster, starts, horizon=HORIZON):
    """For each start date: forecaster(history = rows before it, future = the next `horizon` rows without "loaves").
    A DataFrame with one row per forecast day: start, date, h (1..horizon), actual, forecast."""
    raise NotImplementedError("backtest() arrives in Step 2")


def score(bt):
    """MAE over the backtest days the shop was open (bank_holiday days have 0 sales and are forecast by rule)."""
    raise NotImplementedError("score() arrives in Step 2")


# ---------- Step 3: a model that knows the calendar ----------

LAGS = (14, 21, 28)   # days back; none shorter than HORIZON, or the value is not known when the forecast is made


def features(frame, sales):
    """Model inputs for the rows of frame (date, bank_holiday), a DataFrame with the columns
    dow_0 .. dow_6 (weekday one-hot, Monday = 0), sin and cos (of 2 pi day-of-year / 365.25), t (days since
    2023-01-01), bank_holiday, eve (the next day is a bank holiday, in frame or a zero-sales day in sales), and
    lag_14, lag_21, lag_28: sales on date - lag, looked up in `sales` (a Series of loaves indexed by date; a missing
    or zero value, as on a bank holiday, is replaced by the value a further 7 days back)."""
    raise NotImplementedError("features() arrives in Step 3")


def ridge_forecaster(history, future):
    """Ridge(alpha=1.0) on features() of the history's open days, predicting future; bank holidays forecast 0."""
    raise NotImplementedError("ridge_forecaster() arrives in Step 3")


# ---------- Step 4: how sure is the forecast ----------

def residual_quantiles(bt, qs):
    """{h: {q: quantile}} of actual - forecast over the open days of a backtest, per horizon step h."""
    raise NotImplementedError("residual_quantiles() arrives in Step 4")


def coverage(bt, quantiles, lo, hi):
    """Fraction of open backtest days whose actual lies within [forecast + quantiles[h][lo], forecast + quantiles[h][hi]]."""
    raise NotImplementedError("coverage() arrives in Step 4")


# ---------- Step 5: how many loaves to bake ----------

def critical_ratio(cost_leftover, cost_short):
    """The service level that minimises expected cost: cost_short / (cost_short + cost_leftover)."""
    raise NotImplementedError("critical_ratio() arrives in Step 5")


def bake_plan(forecast, h, quantiles, q):
    """Loaves to bake: forecast + the residual quantile q for each day's horizon step, rounded up, never below 0,
    and 0 on days the forecast is 0 (the shop is closed)."""
    raise NotImplementedError("bake_plan() arrives in Step 5")


def daily_cost(actual, baked, cost_leftover, cost_short):
    """Mean cost per day: leftover loaves x cost_leftover + loaves short x cost_short."""
    raise NotImplementedError("daily_cost() arrives in Step 5")
Provided for you:calendar_2026.csvsales.csvtry_it.py

Frequently asked questions

Why not use k-fold cross-validation for time series?

Shuffled folds train on later data to predict earlier data, which a real forecast can never do. A rolling-origin backtest forecasts from many past start dates using only the history before each one.

What lag features can a 14-day-ahead forecast use?

Only values at least 14 days old, because on the day the forecast is made the more recent sales for the later target days have not happened yet. Calendar features such as weekday, season and holidays are known in advance and can always be used.

What is the newsvendor rule?

When over- and under-supply cost different amounts, the cost-minimising quantity covers demand with probability cost_short / (cost_short + cost_over). With a shortage costing 2.10 and a leftover 0.90, you plan for the 70th percentile of demand, not the forecast median.

Forecasting that survives contact with the future

A forecast is only as good as its evaluation. This lab builds one the way it will be used: from data that exists on the day it is made. You compare naive and seasonal-naive baselines, write a rolling-origin backtest, engineer calendar and lag features that respect the forecast horizon, derive empirical prediction intervals from backtest residuals, and pick an order quantity with the newsvendor critical ratio.