Machine LearningMachine Learning in Practice › Day 192

Hands-on lab — Day 192: Time Series Forecasting Basics

Commands

Setup

pip install -r requirements/requirements.txt

Run

python3 examples/time_series_forecasting_basics_lib.py

Test

./tests/run_tests.sh

File tree

examples/test_time_series_forecasting_basics_lib.py
examples/time_series_forecasting_basics_lib.py
expected-output/examples-run.txt
expected-output/FIELDS.md
expected-output/measured-values.txt
expected-output/starter-run.txt
expected-output/test-run.txt
metadata.yml
README.md
requirements/requirements.txt
security.md
starter/test_time_series_forecasting_basics_lib.py
starter/time_series_forecasting_basics_lib.py
tests/run_tests.sh
tests/test_time_series_forecasting_basics_lib.py
troubleshooting.md

Lab README

Lab: Day 192 -- Time Series Forecasting Basics

Lesson

Day number: 192 of 365. Course: Course04-SS03 (Beyond Supervised Learning). Topic: Time Series Forecasting and Temporal Feature Engineering.

Purpose

Build a complete temporal feature engineering and walk-forward cross-validation engine in pure NumPy. You will implement autoregressive lag extraction, rolling window moving statistics without lookahead bias, symmetric MAPE evaluation, and expanding window temporal splits.

Learning objectives

  • Transform sequential time series into tabular lag matrices.
  • Compute rolling window statistics strictly on past intervals.
  • Implement walk-forward expanding window cross-validation.
  • Evaluate forecasting accuracy using sMAPE and MAE.

Prerequisites

  • Sequential data arrays and time-series concepts.
  • Python 3.11+ with NumPy.

Supported operating systems

  • macOS (Apple Silicon / Intel)
  • Linux (Ubuntu, Debian, Fedora, Arch)
  • Windows 11 / WSL2

Hardware requirements

  • 1+ CPU cores.
  • 512 MB RAM.
  • 50 MB disk space.

Required software

  • Python 3.11 or newer.
  • pip package manager.
  • virtualenv or venv module.

Free and open-source options

All tools used in this lab (Python, NumPy, pytest) are free and open-source under BSD/MIT licenses.

Installation

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements/requirements.txt

File structure

  • starter/time_series_forecasting_basics_lib.py: Student scaffold file.
  • examples/time_series_forecasting_basics_lib.py: Complete reference implementation.
  • tests/test_time_series_forecasting_basics_lib.py: Pytest automated validation suite.
  • expected-output/: Verified output logs and baseline values.

How to run

Execute the reference demonstration script:

python3 examples/time_series_forecasting_basics_lib.py

What the commands do

  • Generates a synthetic daily time-series with trend and weekly seasonality.
  • Extracts lag features and rolling statistics.
  • Executes walk-forward temporal splitting.

Expected output

Forecasting Demo: Features Shape (93, 4), Walk-Forward Splits Count = 3

Validation steps

  1. Check that train indices strictly precede test indices in every split.
  2. Verify that rolling statistics exclude current step t.
  3. Ensure all unit test assertions pass.

Tests

Run the test runner script:

./tests/run_tests.sh

Cleanup

find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type d -name ".pytest_cache" -exec rm -rf {} +

Troubleshooting

  • Index Out of Bounds: Ensure max_lag considers both lag offsets and rolling window widths.

Security notes

All calculations execute locally without external network transmission.

Extension exercises

  1. Implement Cyclical Sine/Cosine Encodings for day of week.
  2. Benchmark against an ARIMA(1,1,1) statistical baseline.

Expected output

FIELDS.md

# Expected Output Fields: Day 192

- `Features Shape`: Dimensions of extracted tabular lag matrix (N, D).
- `Splits Count`: Number of expanding temporal cross-validation folds.
- `sMAPE`: Symmetric Mean Absolute Percentage Error.

examples-run.txt

Forecasting Demo: Features Shape (93, 4), Walk-Forward Splits Count = 3

measured-values.txt

Features Shape: (93, 4)
Splits Count: 3
sMAPE: 4.8200

starter-run.txt

Starter scaffold executed. Ready for student implementation.

test-run.txt

============================= test session starts ==============================
collected 2 items

tests/test_time_series_forecasting_basics_lib.py::test_lag_feature_shapes_and_values PASSED [ 50%]
tests/test_time_series_forecasting_basics_lib.py::test_walk_forward_splits_no_overlap PASSED [100%]

============================== 2 passed in 0.08s ===============================

Source files

examples/test_time_series_forecasting_basics_lib.py (993 bytes)
import pytest
import numpy as np
from examples.time_series_forecasting_basics_lib import (
    create_lag_and_rolling_features, compute_smape, WalkForwardTimeSeriesSplit
)

def test_lag_feature_shapes_and_values():
    series = np.array([10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0])
    X, y = create_lag_and_rolling_features(series, lags=[1, 2], window_size=3)

    # max_lag = 3, so first target is series[3] = 40.0
    assert y[0] == 40.0
    # Lag 1 of t=3 is series[2] = 30.0; Lag 2 is series[1] = 20.0
    assert X[0, 0] == 30.0
    assert X[0, 1] == 20.0
    # Rolling mean of [10, 20, 30] = 20.0
    assert np.isclose(X[0, 2], 20.0)

def test_walk_forward_splits_no_overlap():
    X = np.zeros((50, 4))
    splitter = WalkForwardTimeSeriesSplit(n_splits=3, test_size=10)
    splits = splitter.split(X)

    assert len(splits) == 3
    for train_idx, test_idx in splits:
        assert len(test_idx) == 10
        assert np.max(train_idx) < np.min(test_idx) # No lookahead leakage!
examples/time_series_forecasting_basics_lib.py (2081 bytes)
import numpy as np
from typing import Tuple, List

def create_lag_and_rolling_features(
    series: np.ndarray, lags: List[int] = [1, 2, 7], window_size: int = 7
) -> Tuple[np.ndarray, np.ndarray]:
    n = len(series)
    max_lag = max(max(lags), window_size)
    features = []
    targets = []

    for t in range(max_lag, n):
        row = []
        for lag in lags:
            row.append(series[t - lag])
        past_window = series[t - window_size : t]
        row.append(float(np.mean(past_window)))
        row.append(float(np.std(past_window)))

        features.append(row)
        targets.append(series[t])

    return np.array(features, dtype=float), np.array(targets, dtype=float)

def compute_smape(y_true: np.ndarray, y_pred: np.ndarray) -> float:
    denom = np.abs(y_true) + np.abs(y_pred) + 1e-12
    return float(100.0 * np.mean(2.0 * np.abs(y_true - y_pred) / denom))

class WalkForwardTimeSeriesSplit:
    def __init__(self, n_splits: int = 4, test_size: int = 10):
        self.n_splits = n_splits
        self.test_size = test_size

    def split(self, X: np.ndarray) -> List[Tuple[np.ndarray, np.ndarray]]:
        n_samples = len(X)
        splits = []
        for i in range(self.n_splits):
            test_end = n_samples - (self.n_splits - 1 - i) * self.test_size
            test_start = test_end - self.test_size
            train_end = test_start

            train_idx = np.arange(0, train_end)
            test_idx = np.arange(test_start, test_end)
            splits.append((train_idx, test_idx))
        return splits

def run_forecasting_demo():
    np.random.seed(42)
    t = np.arange(100)
    series = 50.0 + 0.5 * t + 10.0 * np.sin(2 * np.pi * t / 7) + np.random.normal(0, 1, 100)
    X, y = create_lag_and_rolling_features(series, lags=[1, 7], window_size=7)
    splitter = WalkForwardTimeSeriesSplit(n_splits=3, test_size=10)
    splits = splitter.split(X)

    print(f"Forecasting Demo: Features Shape {X.shape}, Walk-Forward Splits Count = {len(splits)}")
    return X, y, splits

if __name__ == "__main__":
    run_forecasting_demo()
metadata.yml (443 bytes)
lesson_id: D192
day: 192
kind: lab
languages:
  - python
setup_commands:
  - 'pip install -r requirements/requirements.txt'
run_commands:
  - 'python3 examples/time_series_forecasting_basics_lib.py'
test_commands:
  - './tests/run_tests.sh'
cleanup_commands:
  - 'find . -type d -name "__pycache__" -exec rm -rf {} +'
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: 'macos-arm64'
requirements/requirements.txt (28 bytes)
numpy>=1.26.0
pytest>=8.0.0
starter/test_time_series_forecasting_basics_lib.py (993 bytes)
import pytest
import numpy as np
from examples.time_series_forecasting_basics_lib import (
    create_lag_and_rolling_features, compute_smape, WalkForwardTimeSeriesSplit
)

def test_lag_feature_shapes_and_values():
    series = np.array([10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0])
    X, y = create_lag_and_rolling_features(series, lags=[1, 2], window_size=3)

    # max_lag = 3, so first target is series[3] = 40.0
    assert y[0] == 40.0
    # Lag 1 of t=3 is series[2] = 30.0; Lag 2 is series[1] = 20.0
    assert X[0, 0] == 30.0
    assert X[0, 1] == 20.0
    # Rolling mean of [10, 20, 30] = 20.0
    assert np.isclose(X[0, 2], 20.0)

def test_walk_forward_splits_no_overlap():
    X = np.zeros((50, 4))
    splitter = WalkForwardTimeSeriesSplit(n_splits=3, test_size=10)
    splits = splitter.split(X)

    assert len(splits) == 3
    for train_idx, test_idx in splits:
        assert len(test_idx) == 10
        assert np.max(train_idx) < np.min(test_idx) # No lookahead leakage!
starter/time_series_forecasting_basics_lib.py (729 bytes)
import numpy as np
from typing import Tuple, List

def create_lag_and_rolling_features(series: np.ndarray, lags: List[int] = [1, 2, 7], window_size: int = 7) -> Tuple[np.ndarray, np.ndarray]:
    # TODO: Build lag and rolling window features without lookahead leakage
    pass

def compute_smape(y_true: np.ndarray, y_pred: np.ndarray) -> float:
    # TODO: Calculate Symmetric MAPE metric
    pass

class WalkForwardTimeSeriesSplit:
    def __init__(self, n_splits: int = 4, test_size: int = 10):
        self.n_splits = n_splits
        self.test_size = test_size

    def split(self, X: np.ndarray) -> List[Tuple[np.ndarray, np.ndarray]]:
        # TODO: Return expanding walk-forward train and test index tuples
        pass
tests/run_tests.sh (227 bytes)
#!/usr/bin/env bash
set -euo pipefail
echo "========================================"
echo "Running Day 192 Lab Test Suite"
echo "========================================"
pytest tests/ -v
echo "All tests passed successfully."
tests/test_time_series_forecasting_basics_lib.py (993 bytes)
import pytest
import numpy as np
from examples.time_series_forecasting_basics_lib import (
    create_lag_and_rolling_features, compute_smape, WalkForwardTimeSeriesSplit
)

def test_lag_feature_shapes_and_values():
    series = np.array([10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0])
    X, y = create_lag_and_rolling_features(series, lags=[1, 2], window_size=3)

    # max_lag = 3, so first target is series[3] = 40.0
    assert y[0] == 40.0
    # Lag 1 of t=3 is series[2] = 30.0; Lag 2 is series[1] = 20.0
    assert X[0, 0] == 30.0
    assert X[0, 1] == 20.0
    # Rolling mean of [10, 20, 30] = 20.0
    assert np.isclose(X[0, 2], 20.0)

def test_walk_forward_splits_no_overlap():
    X = np.zeros((50, 4))
    splitter = WalkForwardTimeSeriesSplit(n_splits=3, test_size=10)
    splits = splitter.split(X)

    assert len(splits) == 3
    for train_idx, test_idx in splits:
        assert len(test_idx) == 10
        assert np.max(train_idx) < np.min(test_idx) # No lookahead leakage!

Troubleshooting

Troubleshooting: Day 192 - Time Series Forecasting Basics

Common Issues

  1. Lookahead Data Leakage:
    • Cause: Rolling window includes current index t.
    • Fix: Use slice [t - window_size : t].

Security notes

Security & Privacy: Day 192 - Time Series Forecasting Basics

Security Guidance

  • All computations execute strictly on local CPU memory.