Python Central HubSign in

Python Projects / Advanced

Real-Time Demand Forecasting

A Python application for forecasting demand in real-time using machine learning, featuring data preprocessing, model training, and a CLI interface.

Abstract

Demand arrives one period at a time, so the forecaster may only use what it has already seen. This project replays a 200-period series, refits a rolling-window linear model on the last seven lags before each new value is revealed, and scores itself against the naive "same as last period" baseline. Measured on this machine: MAE 5.202 against the baseline's 7.726, a 32.7% improvement.

Prerequisites

  • Python 3.8 or above
  • A code editor or IDE
  • Basic understanding of ML and analytics
  • Required libraries: pandas, scikit-learn, matplotlib

Before you Start

Install Python and the required libraries:

Shell
pip install pandas scikit-learn matplotlib

Getting Started

Create a Project

  1. Create a folder named real-time-demand-forecasting.
  2. Open the folder in your code editor or IDE.
  3. Create a file named real_time_demand_forecasting.py.
  4. Copy the code below into your file.

Write the Code

Real-Time Demand ForecastingSource
Real-Time Demand Forecasting
"""Real-time demand forecasting.

Demand arrives one period at a time. The forecaster may only use what it has
already seen, refits on a rolling window as each new point lands, and is scored
against a naive baseline that predicts "same as last period". If the model
cannot beat that baseline it is not earning its keep.
"""

import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression


class RealTimeDemandForecaster:
    """Rolling-window linear forecaster over recent lags."""

    def __init__(self, window=40, lags=7):
        self.window = window
        self.lags = lags
        self.model = LinearRegression()
        self.history = []

    def observe(self, value):
        self.history.append(float(value))

    def _design(self):
        series = np.asarray(self.history[-self.window:], dtype=float)
        rows, targets = [], []
        for index in range(self.lags, len(series)):
            rows.append(series[index - self.lags:index])
            targets.append(series[index])
        return np.asarray(rows), np.asarray(targets)

    def ready(self):
        return len(self.history) >= self.lags + 5

    def forecast(self):
        """Predict the next period from the lags seen so far."""
        if not self.ready():
            return self.history[-1] if self.history else 0.0
        rows, targets = self._design()
        self.model.fit(rows, targets)
        recent = np.asarray(self.history[-self.lags:], dtype=float)
        return float(self.model.predict(recent.reshape(1, -1))[0])


def demand_series(periods=200, seed=0):
    """Weekly seasonality, a slow trend and noise -- no leakage of the future."""
    rng = np.random.default_rng(seed)
    time = np.arange(periods)
    seasonal = 12.0 * np.sin(2 * np.pi * time / 7.0)
    trend = 0.08 * time
    noise = rng.normal(0, 4.0, periods)
    return 100.0 + seasonal + trend + noise


def replay(series, forecaster):
    """Walk the series forward, forecasting before each value is revealed."""
    model_errors, naive_errors, predictions = [], [], []
    for index, actual in enumerate(series):
        predicted = forecaster.forecast()
        naive = forecaster.history[-1] if forecaster.history else actual
        if forecaster.ready():
            model_errors.append(abs(predicted - actual))
            naive_errors.append(abs(naive - actual))
            predictions.append((index, predicted))
        forecaster.observe(actual)
    return np.asarray(model_errors), np.asarray(naive_errors), predictions


def main():
    series = demand_series()
    forecaster = RealTimeDemandForecaster()
    model_errors, naive_errors, predictions = replay(series, forecaster)

    model_mae = model_errors.mean()
    naive_mae = naive_errors.mean()
    print("Real-Time Demand Forecasting")
    print(f"  periods replayed      : {len(series)}")
    print(f"  scored forecasts      : {len(model_errors)}")
    print(f"  rolling model MAE     : {model_mae:.3f}")
    print(f"  naive (last value) MAE: {naive_mae:.3f}")
    improvement = (naive_mae - model_mae) / naive_mae
    print(f"  improvement over naive: {improvement:.1%}")
    if model_mae >= naive_mae:
        print("  the model does NOT beat the baseline on this series")

    steps, values = zip(*predictions)
    figure, axes = plt.subplots(2, 1, figsize=(9, 5), height_ratios=(2, 1))
    axes[0].plot(series, label="actual demand", linewidth=1.4)
    axes[0].plot(steps, values, label="one-step forecast", linewidth=1.2)
    axes[0].set_ylabel("units")
    axes[0].set_title("forecasting one period ahead, refitting as data arrives")
    axes[0].legend(fontsize=8)
    axes[1].plot(steps, model_errors, label=f"model (MAE {model_mae:.2f})",
                 linewidth=1.0)
    axes[1].plot(steps, naive_errors, label=f"naive (MAE {naive_mae:.2f})",
                 linewidth=1.0, alpha=0.7)
    axes[1].set_xlabel("period")
    axes[1].set_ylabel("absolute error")
    axes[1].legend(fontsize=8)
    figure.tight_layout()
    plt.savefig("real_time_demand_forecasting.png", dpi=120,
                bbox_inches="tight")
    print("saved real_time_demand_forecasting.png")


if __name__ == "__main__":
    main()

Example Usage

Shell
python real_time_demand_forecasting.py

What it produces

Running the file exactly as it ships takes 3.7 s and prints:

Text
Real-Time Demand Forecasting
  periods replayed      : 200
  scored forecasts      : 188
  rolling model MAE     : 5.202
  naive (last value) MAE: 7.726
  improvement over naive: 32.7%
saved real_time_demand_forecasting.png
figureProduced by this project, not drawn for the pagematplotlib
Output of real_time_demand_forecasting.py, produced by running the file.
Written by the run above. If the project stops producing it, the page's figure asset goes missing and check_docs reports it — which is the point of generating it rather than drawing it.

How it fits together

Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.

diagram Diagram mermaid

Explanation

Key Features

  • Walk-forward evaluation: every forecast is made before its actual value is revealed, so nothing is scored on data the model had already seen.
  • A baseline it has to beat: persistence scores 7.726 MAE; a model that cannot beat that is not earning its keep.
  • Rolling refit: the window slides, so the model tracks the trend instead of fitting the whole history once.
  • Measured output: the run prints both MAEs and saves the forecast and error curves.

Code Breakdown

  1. What it imports (lines 9–11)
Python
import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
  1. RealTimeDemandForecaster — the class (lines 14–44)
Python
class RealTimeDemandForecaster:
    """Rolling-window linear forecaster over recent lags."""
 
    def __init__(self, window=40, lags=7):
        self.window = window
        self.lags = lags
        self.model = LinearRegression()
        self.history = []
 
    def observe(self, value):
        self.history.append(float(value))
 
    def _design(self):
        series = np.asarray(self.history[-self.window:], dtype=float)
        rows, targets = [], []
        for index in range(self.lags, len(series)):
            rows.append(series[index - self.lags:index])
            targets.append(series[index])
        # ... 7 more lines in the file ...
        if not self.ready():
            return self.history[-1] if self.history else 0.0
        rows, targets = self._design()
        self.model.fit(rows, targets)
        recent = np.asarray(self.history[-self.lags:], dtype=float)
        return float(self.model.predict(recent.reshape(1, -1))[0])
  1. demand_series — the function (lines 47–54)
Python
def demand_series(periods=200, seed=0):
    """Weekly seasonality, a slow trend and noise -- no leakage of the future."""
    rng = np.random.default_rng(seed)
    time = np.arange(periods)
    seasonal = 12.0 * np.sin(2 * np.pi * time / 7.0)
    trend = 0.08 * time
    noise = rng.normal(0, 4.0, periods)
    return 100.0 + seasonal + trend + noise
  1. replay — the function (lines 57–68)
Python
def replay(series, forecaster):
    """Walk the series forward, forecasting before each value is revealed."""
    model_errors, naive_errors, predictions = [], [], []
    for index, actual in enumerate(series):
        predicted = forecaster.forecast()
        naive = forecaster.history[-1] if forecaster.history else actual
        if forecaster.ready():
            model_errors.append(abs(predicted - actual))
            naive_errors.append(abs(naive - actual))
            predictions.append((index, predicted))
        forecaster.observe(actual)
    return np.asarray(model_errors), np.asarray(naive_errors), predictions
  1. main — the function (lines 71–105)
Python
def main():
    series = demand_series()
    forecaster = RealTimeDemandForecaster()
    model_errors, naive_errors, predictions = replay(series, forecaster)
 
    model_mae = model_errors.mean()
    naive_mae = naive_errors.mean()
    print("Real-Time Demand Forecasting")
    print(f"  periods replayed      : {len(series)}")
    print(f"  scored forecasts      : {len(model_errors)}")
    print(f"  rolling model MAE     : {model_mae:.3f}")
    print(f"  naive (last value) MAE: {naive_mae:.3f}")
    improvement = (naive_mae - model_mae) / naive_mae
    print(f"  improvement over naive: {improvement:.1%}")
    if model_mae >= naive_mae:
        print("  the model does NOT beat the baseline on this series")
 
    steps, values = zip(*predictions)
    # ... 11 more lines in the file ...
    axes[1].set_ylabel("absolute error")
    axes[1].legend(fontsize=8)
    figure.tight_layout()
    plt.savefig("real_time_demand_forecasting.png", dpi=120,
                bbox_inches="tight")
    print("saved real_time_demand_forecasting.png")

The file defines 4 top-level symbols in all; the whole thing is above under Write the Code.

Features

  • Demand Forecasting: Real-time data preprocessing and forecasting
  • Modular Design: Separate functions for each task
  • Error Handling: Manages invalid inputs and exceptions
  • Production-Ready: Scalable and maintainable code

Next Steps

Enhance the project by:

  • Integrating with more demand APIs
  • Supporting advanced ML models
  • Creating a GUI for forecasting
  • Adding real-time analytics
  • Unit testing for reliability

Educational Value

This project teaches:

  • Walk-forward validation: why a time series cannot be scored with a random split.
  • Baselines for forecasting: persistence is the number every forecaster is judged against.
  • Rolling windows: trading history length against adaptability.

Real-World Applications

  • E-commerce Platforms
  • Analytics Tools
  • Forecasting Engines

Conclusion

Real-Time Demand Forecasting demonstrates how to build a scalable and accurate demand forecasting tool using Python. With modular design and extensibility, this project can be adapted for real-world applications in e-commerce, analytics, and more. For more advanced projects, visit Python Central Hub.

Pitfalls

  • "Beats the naive baseline" depends entirely on which naive baseline. The project improves on last-value by 32.7% (MAE 5.202 against 7.726). In the exercise below, the same rolling-mean model is 17.8% better than last-value and 231% worse than same-day-last-week on one series — same model, same data, opposite conclusions.
  • On a random walk, last-value is unbeatable in principle. Measured: every rolling window tested scores worse than doing nothing, and longer windows score worse still — a mean of the last k values estimates where the walk was.
  • Scoring fewer points than you replayed is correct and worth stating. 200 periods replayed, 188 forecasts scored: the first 12 have no history to forecast from. A score computed over a different denominator is not comparable.
  • MAE is in the units of the series. 5.202 means nothing without knowing the scale of what is being forecast.
  • Rolling evaluation is not cross-validation. Each forecast uses only past data, which is the only honest arrangement for a time series — a shuffled split lets the model see the future.

Recap

  • Measured: 200 periods replayed, 188 scored, rolling model MAE 5.202 against naive 7.726 — 32.7% better.
  • The naive forecast is last-value, which is the right baseline for a series that drifts and the wrong one for a series with a weekly shape.
  • No baseline wins everywhere: on a noisy constant the rolling mean wins, on a random walk last-value wins, with a weekly shape same-day-last-week wins.
  • An improvement percentage without its baseline named is not a result.
quizCheck yourself
  1. The same rolling-mean model measured 17.8% better than one baseline and 231% worse than another, on identical data. What follows?

    Show answer

    B — An improvement figure is a statement about a pair — model and baseline — so quoting it without naming the baseline conveys nothing

  2. On a random walk, every rolling window scored worse than simply repeating the last value, and longer windows scored worse. Why?

    Show answer

    B — The next value of a random walk is the last value plus noise, so the last value is the best possible estimate — averaging older values only adds lag

  3. 200 periods were replayed and 188 forecasts were scored. Why not 200?

    Show answer

    B — The first 12 periods have no history to forecast from, so they cannot be scored — and a comparison against a model scored over 200 would be measuring different things

Try it yourself

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Was this page helpful?

Let us know how we did