TAI BUI
Back to curriculum
BuildPython

Time Series Fundamentals

Past performance does predict future results — if you check for stationarity first.

TIME: ~90 minutesPREREQUISITES: Phase 2, Lessons 01-09

Learning Objectives

  • Decompose a time series into trend, seasonality, and residual components and test for stationarity
  • Implement lag features and rolling statistics to convert a time series into a supervised learning problem
  • Build a walk-forward validation framework that prevents future data from leaking into training
  • Explain why random train/test splits are invalid for time series and demonstrate the performance gap versus proper temporal splits

The Problem

You have data ordered by time: daily sales, hourly temperature, per-minute CPU usage, weekly stock prices. You want to predict the next value, the next week, the next quarter.

So you reach for your standard ML toolkit: random train/test split, cross-validation, feature matrix in, prediction out. Every step of that is wrong.

Time series breaks the assumptions standard ML relies on. Samples aren't independent: today's temperature depends on yesterday's. Random splits leak future information into the past. Features that look great in backtest fail in production because the patterns they rely on shift over time.

A model that scores 95% accuracy under random cross-validation might score 55% under proper time-based evaluation. That gap isn't a technicality — it's the difference between a model that works on paper and one that works in production.

This lesson covers the fundamentals: what makes time data different, how to evaluate models honestly, and how to turn a time series into features that standard ML models can actually consume.

Warm-up0/3
1. A correlation coefficient between two variables can take which range of values?Warm-up
A scatter plot of 28 points with hours studied on the horizontal axis and test score on the vertical axis, next to a box for the correlation coefficient r with a question mark below it.
Simulated points, not real students.
2. A model scores far better on its training data than on data it has never seen. What is this problem called?Warm-up
Two bars on a score axis: a tall bar for training data and a short bar for data the model has never seen, next to a box asking for the name of the problem.
Schematic. Bar heights are illustrative, with no real scores.
3. Why do we measure a model on a held-out test set rather than on its training data?Warm-up
A row of 10 table rows. The first 7 are marked as used to fit the model and the last 3 as set aside and never fitted. A question asks which group the reported score is computed on.
Schematic split of 10 rows; the 7 and 3 are illustrative.
Check: The Problem0/1
1. A model scores 95% under random cross-validation but 55% under time-based evaluation. What best explains the gap?Analyze
Two bars: 95 percent for random cross-validation and 55 percent for time-based evaluation.
Scores from the stem.

The Concept

What Makes Time Series Different

Standard ML assumes i.i.d. data: independent and identically distributed. Each sample is drawn from the same distribution, independent of the others. Time series violates both halves of that assumption:

  • Not independent. Today's stock price depends on yesterday's. This week's sales correlate with last week's.
  • Not identically distributed. The distribution shifts over time. Sales in December look different from sales in March.

These aren't minor violations. They change how you build features, how you evaluate models, and which algorithms even work.

Mermaid diagram

In standard ML, samples are interchangeable: shuffle them and nothing changes. In time series, order is everything, and shuffling destroys the signal.

Components of a Time Series

Every time series is a combination of:

Mermaid diagram

  • Trend: The long-term direction. Revenue growing 10% per year. Global temperature rising.
  • Seasonality: Repeating patterns at fixed intervals. Retail sales spike in December. Air conditioning usage peaks in July.
  • Residual: Whatever is left after removing trend and seasonality. If the residual looks like white noise (random fluctuation with no leftover pattern), the decomposition captured the signal.

Stationarity

A time series is stationary if its statistical properties (mean, variance, autocorrelation) stay constant over time. Most forecasting methods assume stationarity.

Why it matters: A non-stationary series has a mean that drifts. A model trained on January's data has learned a different mean than what February will show, so it will be systematically wrong.

How to check: Compute rolling mean and rolling standard deviation over windows. If they drift, the series is non-stationary.

How to fix: Differencing. Instead of modeling the raw values, model the change between consecutive values:

diff[t] = value[t] - value[t-1]

If one round of differencing doesn't make the series stationary, apply it again (second-order differencing). Most real-world series need at most two rounds.

Example:

Original series: [100, 102, 106, 112, 120] First difference: [2, 4, 6, 8] (still trending upward) Second difference: [2, 2, 2] (constant, stationary)

The original series had a quadratic trend. First differencing turned it into a linear trend, and second differencing flattened it out. In practice, you rarely need more than two rounds.

Formal test: The Augmented Dickey-Fuller (ADF) test is the standard statistical test for stationarity. The null hypothesis is "the series is non-stationary." A p-value below 0.05 lets you reject the null and conclude stationarity. ADF isn't implemented from scratch here, since it needs asymptotic distribution tables, but the rolling-statistics approach in the code gives a practical visual check.

Autocorrelation

Autocorrelation measures how much a value at time t correlates with the value at time t-k (k steps in the past). The autocorrelation function (ACF) plots this correlation for each lag k.

ACF tells you:

  • How far back the series remembers. If ACF drops to zero after lag 5, values more than 5 steps ago are irrelevant.
  • Whether seasonality exists. If ACF spikes at lag 12 (monthly data), there is yearly seasonality.
  • How many lag features to create. Use lags up to where ACF becomes negligible.

PACF (Partial Autocorrelation Function) removes indirect correlations. If today correlates with 3 days ago only because both correlate with yesterday, PACF at lag 3 will be zero while ACF at lag 3 will not.

Lag Features: Turning Time Series into Supervised Learning

Standard ML models need a feature matrix X and a target y. Time series gives you a single column of values. The bridge is lag features: the value of the series k steps in the past, used as an input column to predict the value at the current step.

Take the series [10, 12, 14, 13, 15] and create lag-1 and lag-2 features:

lag_2lag_1target
101214
121413
141315

Now you have a standard regression problem. Any ML model (linear regression, random forest, gradient boosting) can predict the target from the lags.

Additional features you can engineer:

  • Rolling statistics: mean, std, min, max over the last k values
  • Calendar features: day of week, month, is_holiday, is_weekend
  • Differenced values: change from previous step
  • Expanding statistics: cumulative mean, cumulative sum
  • Ratio features: current value / rolling mean (how far from recent average)
  • Interaction features: lag_1 * day_of_week (weekday effects on momentum)

How many lags? Use the autocorrelation function. If ACF is significant up to lag 10, use at least 10 lags. If there is weekly seasonality, include lag 7 (and possibly 14). More lags give the model more history but also more features to fit, increasing the risk of overfitting.

The target alignment trap. When creating lag features, the target must be the value at time t, and all features must use values at time t-1 or earlier. Accidentally include the value at time t as a feature and you end up with a perfect predictor and a completely useless model — the most common bug in time series feature engineering.

Walk-Forward Validation

This is the most important concept in this lesson. Standard k-fold cross-validation randomly assigns samples to train and test. For time series, this leaks future information.

Mermaid diagram

Walk-forward validation:

  1. Train on data up to time t
  2. Predict at time t+1 (or t+1 to t+k for multi-step)
  3. Slide the window forward
  4. Repeat

Each test fold only contains data that comes after all training data. No future leakage. This gives you an honest estimate of how the model will perform when deployed.

Expanding window uses all historical data for training (window grows). Sliding window uses a fixed-size training window (window slides). Use expanding when you believe older data is still relevant. Use sliding when the world changes and old data hurts.

ARIMA Intuition

ARIMA is the classical time series model. It has three components:

  • AR (Autoregressive): Predict from past values. AR(p) uses the last p values.
  • I (Integrated): Differencing to achieve stationarity. I(d) applies d rounds of differencing.
  • MA (Moving Average): Predict from past forecast errors. MA(q) uses the last q errors.

ARIMA(p, d, q) combines all three. You choose p, d, q based on ACF/PACF analysis or automated search (auto-ARIMA).

ARIMA isn't implemented from scratch here: it needs numerical optimization beyond the scope of this lesson. What matters is understanding what each component does, so you can interpret ARIMA's results and know when to reach for it.

When to Use What

ApproachBest ForHandles SeasonalityHandles External Features
Lag features + MLTabular with many external featuresWith calendar featuresYes
ARIMASingle univariate series, short-termSARIMA variantNo (ARIMAX for limited)
Exponential smoothingSimple trend + seasonalityYes (Holt-Winters)No
ProphetBusiness forecasting, holidaysYes (Fourier terms)Limited
Neural networks (LSTM, Transformer)Long sequences, many seriesLearnedYes

For most practical problems, lag features + gradient boosting is the strongest starting point. It handles external features naturally, does not require stationarity, and is easy to debug.

Forecasting Horizons and Strategies

Single-step forecasting predicts one time step ahead. Multi-step forecasting predicts multiple steps. There are three strategies:

Recursive (iterated): Predict one step ahead, use the prediction as input for the next step. Simple, but errors accumulate: each prediction feeds on the previous prediction, so mistakes compound.

Direct: Train a separate model for each horizon. Model-1 predicts t+1, Model-5 predicts t+5. No error accumulation, but each model has fewer training samples and they do not share information.

Multi-output: Train one model that outputs all horizons simultaneously. Shares information across horizons but requires a model that supports multiple outputs (or a custom loss function).

For most practical problems, start with recursive for short horizons (1-5 steps) and direct for longer horizons.

Common Mistakes in Time Series

MistakeWhy it happensHow to fix
Random train/test splitHabit from standard MLUse walk-forward or temporal split
Using future featuresFeature at time t included by mistakeAudit every feature for temporal alignment
Overfitting to seasonalityModel memorizes calendar patternsHold out a full seasonal cycle in the test set
Ignoring scale changesRevenue doubles but patterns stayModel percentage change instead of absolute
Too many lag features"More history is better"Use ACF to determine relevant lags
Not differencing"The model will figure it out"Tree models handle trends; linear models need stationarity
Check: The Concept0/28
1. What does it mean for a time series to be stationary?Define
A noisy line chart of 120 time steps that wanders up and down around a value of about 50.
Simulated series (random noise around 50), not captured data.
2. What is the purpose of differencing a time series?Define
A row of five values 100, 102, 106, 112, 120 and below it a row of four changes between neighbouring values: 2, 4, 6, 8, with a question about the purpose of computing it.
The lesson's example series; changes computed with the lesson's difference().
3. Lag features convert a time series into a supervised learning problem. What is a lag-3 feature for predicting y[t]?Define
A row of five series values 10, 12, 14, 13, 15 labelled y[0] to y[4], with y[4] named as the target and a question mark for the lag-3 feature.
The lesson's example series.
4. What does the autocorrelation function (ACF) show?Define
A bar chart of the autocorrelation function for lags 1 to 21 of a simulated daily series, with dashed lines marking a 95 percent band.
Lesson's make_seasonal_series (weekly period), differenced once; computed with the lesson's autocorrelation().
5. What does the partial autocorrelation at lag k measure?Define
A bar chart of partial autocorrelation at lags 1 to 8 for a simulated series. Two tall bars come first and the remaining bars are close to zero.
Simulated series of 800 steps; partial autocorrelation computed with numpy regressions.
6. What is walk-forward validation?Define
Ten numbered blocks in a row with a time arrow below pointing right, and a question about how walk-forward validation assigns blocks to train and test folds.
Schematic with 10 illustrative blocks.
7. What is seasonality in a time series?Define
A line chart of a simulated series over 48 time steps that moves up and down in waves while its overall level rises.
Simulated series (rising level, wave, noise), not captured data.
8. What is the trend component of a time series?Define
A line chart of a simulated series over 48 time steps with day-to-day wiggle whose overall level climbs from left to right.
Simulated series, not captured data.
9. What is the residual component of a time series decomposition, and what does it mean if it looks like white noise?Define
Two panels. The left shows a line chart of a simulated series. The right is an empty dashed box labelled residual component with a question mark.
Simulated series built from level + wave + noise (48 steps).
10. In ARIMA(p, d, q), what do the three components represent?Define
A line of code ARIMA(series, order=(p, d, q)) above three boxes reading p = ?, d = ?, q = ?.
Typed code line, not a captured run.
11. What is an expanding window in walk-forward validation?Define
Three rows of ten blocks. In each row one orange test block sits at blocks 5, 6 and 7, with dashed unlabelled blocks to its left and empty blocks to its right.
Schematic with 10 illustrative blocks.
12. What is a sliding window in walk-forward validation?Define
Three rows of ten blocks. Row one has four blue train blocks and an orange test block at block 5. Rows two and three show an orange test block at blocks 6 and 7 with dashed unlabelled blocks to its left.
Schematic with 10 illustrative blocks.
13. What does the i.i.d. assumption behind standard ML say about samples?Define
A small table of five samples with columns sample, feature 1, feature 2 and label, and a question about what the i.i.d. assumption says about the rows.
Illustrative random values, not a real dataset.
14. Why is a random train/test split invalid for time series data?Explain
Twenty dots in time order with a time arrow, and arrows down to a train box of 16 points and a test box of 4 points. A note says the assignment is random.
Schematic with 20 illustrative points.
15. Why does a non-stationary series hurt a model trained on one period?Explain
A rising noisy line chart over 120 steps. The first 40 steps are shaded as the training period.
Simulated series, not captured data.
16. The ACF of a monthly series spikes at lag 12. What does that indicate?Explain
A bar chart of the autocorrelation of a simulated monthly series at lags 1 to 24 with the bar at lag 12 highlighted in orange.
Simulated monthly series (120 steps); ACF computed with the lesson's autocorrelation().
17. How does PACF differ from ACF?Explain
Two bar charts for the same simulated series at lags 1 to 10. The ACF bars shrink gradually. In the PACF chart only the first bar is tall.
Simulated series of 2000 steps; both computed with numpy.
18. What is the target alignment trap, which the lesson calls the most common bug in time series feature engineering?Explain
Four boxes in time order labelled y[t-3], y[t-2], y[t-1] and y[t], with y[t] outlined in orange as the target, and a question about which time indexes may be used in the feature matrix.
Schematic.
19. How does an expanding window differ from a sliding window, and when would you choose each?Explain
Two groups of two walk-forward folds on ten blocks, titled expanding window and sliding window. Each fold shows an orange test block with dashed unlabelled blocks to its left.
Schematic with 10 illustrative blocks.
20. What is the tradeoff between the recursive and direct strategies for multi-step forecasting?Explain
A row of observed values ending at y[t], followed by three dashed boxes for y[t+1], y[t+2] and y[t+3] each marked with a question mark, and a question about the recursive and direct strategies.
Schematic.
21. Why does the lesson suggest lag features with gradient boosting as the strongest starting point?Explain
Five boxes naming lag features plus ML, ARIMA, exponential smoothing, Prophet and neural networks, with the first highlighted, and a question asking for the reasons behind that choice.
Names from the lesson's comparison table.
22. A series of 5 values is differenced twice. How many values remain?Apply
Five values 100, 102, 106, 112, 120 followed by two arrows for applying the difference step twice, and a question mark for the number of values remaining.
The lesson's example series.
23. A linear model's forecasts run systematically too low in later months, and the series' rolling mean has climbed steadily. What is the likely cause?Analyze
A rising simulated series in blue with the first 60 steps shaded as training. From step 60 the orange forecast flattens below the series, while a green dotted rolling mean keeps climbing.
Simulated series; AR(3) fitted with the lesson's SimpleAR and rolled forward recursively.
24. A recursive 20-step forecast is accurate at step 1 but drifts badly by step 20. Which property explains this?Analyze
Two lines over 20 steps ahead. The blue actual series and the orange dashed forecast start almost together at step 1 and are far apart by step 20.
Simulated series (trend + 30-step wave + noise); AR(3) fitted with the lesson's SimpleAR, forecast rolled forward recursively.
25. Your gradient boosting model crushes the seasonal naive baseline on a walk-forward backtest for daily demand forecasting. Before trusting the result, what should you check first?Analyze
Two bars of backtest error: gradient boosting with a much shorter bar and the seasonal naive baseline with a tall bar.
Schematic. Bar heights are illustrative, with no real errors.
26. You must forecast daily demand for many products using promotions, weather, and holiday flags as inputs. Which design fits best?Design
A table with columns product, day, promotion, temp_c, holiday and demand, with four example rows for products P1 and P2 and a question mark in each demand cell.
Typed, illustrative rows.
27. You need 30-day-ahead forecasts and recursive predictions drift badly at long horizons. Which design fits best?Design
A bar chart of mean absolute error for steps 1 to 30 ahead. The bars start small at step 1 and grow to much larger values by step 10 and beyond.
Simulated series; AR(3) from the lesson's SimpleAR, forecast recursively from 120 starting points.
28. A retailer changed its business model two years ago, and older data now hurts forecasts. Which evaluation and training setup fits best?Design
A line chart of sales that sits at a lower level for the older part, jumps at a dashed vertical line marking the business-model change, and continues at a higher level to the right.
Simulated series, not captured data.

Build It

The code in code/time_series.py implements the core building blocks from scratch.

Lag Feature Creator

python
def make_lag_features(series, n_lags):
    n = len(series)
    X = np.full((n, n_lags), np.nan)
    for lag in range(1, n_lags + 1):
        X[lag:, lag - 1] = series[:-lag]
    valid = ~np.isnan(X).any(axis=1)
    return X[valid], series[valid]

This turns a 1D series into a feature matrix: each row holds the last n_lags values as features and the current value as the target.

Walk-Forward Cross-Validation

python
def walk_forward_split(n_samples, n_splits=5, min_train=50):
    assert min_train < n_samples, "min_train must be less than n_samples"
    step = max(1, (n_samples - min_train) // n_splits)
    for i in range(n_splits):
        train_end = min_train + i * step
        test_end = min(train_end + step, n_samples)
        if train_end >= n_samples:
            break
        yield slice(0, train_end), slice(train_end, test_end)

Each split ensures training data comes strictly before test data. The training window expands with each fold. The code's demo_random_vs_walk_forward function runs a random 80/20 split and walk-forward validation back to back on the same series and prints the ratio between their errors: that's the concrete version of the 95%-versus-55% gap described in the introduction, not just a hypothetical.

Simple Autoregressive Model

A pure AR model is just linear regression on lag features:

python
class SimpleAR:
    def __init__(self, n_lags=5):
        self.n_lags = n_lags
        self.weights = None
        self.bias = None
 
    def fit(self, series):
        X, y = make_lag_features(series, self.n_lags)
        # Solve via normal equations
        X_b = np.column_stack([np.ones(len(X)), X])
        theta = np.linalg.lstsq(X_b, y, rcond=None)[0]
        self.bias = theta[0]
        self.weights = theta[1:]
        return self

This is conceptually identical to linear regression from Lesson 02, but applied to time-lagged versions of the same variable.

Stationarity Check

The code computes rolling statistics to check stationarity, both visually and numerically:

python
def check_stationarity(series, window=50):
    rolling_mean = np.array([
        series[max(0, i - window):i].mean()
        for i in range(1, len(series) + 1)
    ])
    rolling_std = np.array([
        series[max(0, i - window):i].std()
        for i in range(1, len(series) + 1)
    ])
    return rolling_mean, rolling_std

If the rolling mean drifts or the rolling std changes, the series is non-stationary. Apply differencing and check again.

The code also checks stationarity by comparing the first half and second half of the series. If the means differ by more than half a standard deviation or the variance ratio exceeds 2x, the series is flagged as non-stationary.

Autocorrelation

python
def autocorrelation(series, max_lag=20):
    n = len(series)
    mean = series.mean()
    var = series.var()
    acf = np.zeros(max_lag + 1)
    for k in range(max_lag + 1):
        cov = np.mean((series[:n-k] - mean) * (series[k:] - mean))
        acf[k] = cov / var if var > 0 else 0
    return acf

This is the ACF from the Concept section, computed directly: correlate the series with a copy of itself shifted by k steps, for every k up to max_lag, then normalize by the variance so the result sits between -1 and 1.

Check: Build It0/6
1. make_lag_features is called on a series of 100 values with n_lags=3. How many rows does the returned X have?Apply
Three lines of code: a series of 100 values, a call to make_lag_features with n_lags=3, and X.shape with a question mark in place of the row count.
Typed call using the lesson's make_lag_features().
2. make_lag_features(np.array([10, 12, 14, 13, 15]), 2) is called. What are the first row of X and its target?Apply
Four lines of code: the series 10, 12, 14, 13, 15, a call make_lag_features(series, 2), and two comments asking for X[0] and y[0].
Typed call using the lesson's make_lag_features().
3. walk_forward_split(200, n_splits=5, min_train=50) is iterated. What does the first fold return?Apply
A call to walk_forward_split(200, n_splits=5, min_train=50) and a bar for 200 samples with the first 50 marked as min_train, plus a question about the first fold's slices.
Typed call using the lesson's walk_forward_split().
4. The autocorrelation function in the code is called on any non-constant series. What is acf[0]?Apply
A bar chart of acf[k] for k = 0 to 10 of a simulated daily series. Bars for lags 1 to 10 are drawn and the slot at lag 0 is an empty dashed box with a question mark.
Lesson's make_seasonal_series (365 days); bars from the lesson's autocorrelation().
5. SimpleAR(n_lags=5).fit is called on a series of 200 values. What does it fit?Apply
A box for a 200-value series with an arrow to a box SimpleAR(n_lags=5) and an arrow to a box asking how many training rows are used and how many weights the model has.
Schematic of the stem.
6. check_stationarity runs with window=50. What is rolling_std at the first position?Apply
A line chart of rolling_std over positions 0 to 120 for a simulated daily series, drawn from position 1 onward, with a dashed circle and question mark at position 0.
Lesson's make_seasonal_series; curve from the lesson's check_stationarity().

Use It

With sklearn, you use lag features directly with any regressor:

python
from sklearn.linear_model import Ridge
from sklearn.ensemble import GradientBoostingRegressor
 
X, y = make_lag_features(series, n_lags=10)
 
for train_idx, test_idx in walk_forward_split(len(X)):
    model = Ridge(alpha=1.0)
    model.fit(X[train_idx], y[train_idx])
    predictions = model.predict(X[test_idx])

For ARIMA, use statsmodels:

python
from statsmodels.tsa.arima.model import ARIMA
 
model = ARIMA(train_series, order=(5, 1, 2))
fitted = model.fit()
forecast = fitted.forecast(steps=30)

The code in time_series.py demonstrates both approaches and compares them using walk-forward validation.

sklearn TimeSeriesSplit

sklearn provides TimeSeriesSplit which implements walk-forward validation:

python
from sklearn.model_selection import TimeSeriesSplit
 
tscv = TimeSeriesSplit(n_splits=5)
for train_index, test_index in tscv.split(X):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]
    model.fit(X_train, y_train)
    score = model.score(X_test, y_test)

This is equivalent to the from-scratch walk_forward_split above, integrated into sklearn's cross-validation framework. You can use it with cross_val_score:

python
from sklearn.model_selection import cross_val_score
 
scores = cross_val_score(model, X, y, cv=TimeSeriesSplit(n_splits=5))
print(f"Mean score: {scores.mean():.4f} +/- {scores.std():.4f}")

Evaluation Metrics

Time series forecasting uses regression metrics, but with time-aware context:

  • MAE (Mean Absolute Error): Average of |y_true - y_pred|. Easy to interpret in original units. "On average, predictions are off by 3.2 degrees."
  • RMSE (Root Mean Squared Error): Square root of mean squared error. Penalizes large errors more than MAE. Use when big errors are worse than many small errors.
  • MAPE (Mean Absolute Percentage Error): Average of |error / true_value| * 100. Scale-independent, useful for comparing across different series. But undefined when true values are zero.
  • Naive baseline comparison: Always compare against simple baselines. The seasonal naive baseline predicts the value from one period ago (yesterday, last week). If your model cannot beat naive, something is wrong.

Rolling Features

The code demonstrates adding rolling statistics (mean, std, min, max over windows of 7 and 14 days) to lag features. These give the model information about recent trends and volatility that lag features alone do not capture.

For example, if the rolling mean is rising, it suggests an upward trend. If the rolling std is increasing, it suggests growing volatility. These are the kinds of patterns that tree-based models can learn from but linear models cannot.

Check: Use It0/3
1. You add a rolling 7-day mean and a rolling 7-day standard deviation to your lag features and walk-forward accuracy improves. What did they likely capture that plain lags could not?Explain
A table of five rows with columns lag_1, lag_2, lag_3, roll_mean_7, roll_std_7 and target, filled with numbers computed from a simulated daily series, and a question about what the rolling columns add.
Rows from the lesson's make_seasonal_series (days 150 to 154); values computed with numpy.
2. Why is MAPE useful for comparing forecasts across different series, and what is its weakness?Explain
A table with two series: series A has true value 100, forecast 105 and absolute error 5; series B has true value 10000, forecast 10500 and absolute error 500.
Illustrative numbers, worked by hand.
3. ARIMA(train_series, order=(5, 1, 2)) is fitted and forecast(steps=30) is called. What does that model use and produce?Apply
Three lines of ARIMA code with order=(5, 1, 2) and forecast(steps=30), above a line chart of a training series followed by a shaded 30-step forecast region.
Typed code and a simulated series. The lesson does not implement ARIMA itself.

Ship It

This lesson produces:

  • outputs/prompt-time-series-advisor.md, a prompt for framing time series problems
  • code/time_series.py, lag features, walk-forward validation, AR model, stationarity checks

Baselines You Must Beat

Before building any model, establish baselines:

  1. Last value (persistence). Predict that tomorrow will be the same as today. For many series, this is surprisingly hard to beat.
  2. Seasonal naive. Predict that today will be the same as the same day last week (or last year). If your model cannot beat this, it has not learned any useful pattern beyond seasonality.
  3. Moving average. Predict the average of the last k values. Smooths noise but cannot capture sudden changes.

If your fancy ML model loses to the seasonal naive baseline, you have a bug. Most commonly: future leakage in features, the wrong evaluation method, or a series that's truly random and unpredictable.

Practical Tips

  1. Start with plotting. Before any modeling, plot the raw series. Look for trends, seasonality, outliers, structural breaks (sudden changes in behavior). A 30-second visual inspection often tells you more than an hour of automated analysis.

  2. Difference first, model second. If the series has a clear trend, difference it before creating lag features. Tree-based models can handle trends, but linear models cannot, and differencing never hurts.

  3. Hold out at least one full seasonal cycle. If you have weekly seasonality, your test set needs at least one full week. If monthly, at least one full month. Otherwise you cannot evaluate whether the model captured the seasonal pattern.

  4. Monitor in production. Time series models degrade over time as the world changes. Track prediction errors on a rolling basis. When errors start increasing, retrain the model on recent data.

  5. Beware of regime changes. A model trained on pre-pandemic data will not predict post-pandemic behavior. Include indicators of known regime changes as features, or use a sliding window that forgets old data.

  6. Log-transform skewed series. Revenue, prices, and counts are often right-skewed. Taking the log stabilizes variance and makes multiplicative patterns additive, which linear models can handle. Forecast in log space, then exponentiate to get back to original units.

Check: Ship It0/3
1. Why does the lesson say a model that loses to the seasonal naive baseline probably has a bug?Explain
Two bars on an error axis: your model with a tall bar and the seasonal naive baseline with a shorter bar.
Schematic. Bar heights are illustrative, with no real errors.
2. A model beat the seasonal naive baseline in backtesting, but its errors keep rising a few months after deployment. What advice applies?Analyze
A line chart of forecast error that stays level during the backtest period and then rises steadily during the shaded in-production period.
Illustrative error curve, not measured; no units.
3. A team holds out 3 days of a series with weekly seasonality and reports that the model captured the seasonal pattern. What is wrong?Analyze
A simulated daily series over 42 days with a repeating weekly wave. The last 3 days are shaded orange as the test set.
Simulated series, not captured data.
Author's Note

The bug I run into most often in forecasting pipelines has nothing to do with the model. It's a join: some upstream table resolves 'today's row' to a version that already contains information that wouldn't exist yet at prediction time. A lag-3 feature is easy to audit by eye. A same-day feature smuggled in through a database view is not.

My rule of thumb: when a model's backtest beats the seasonal naive baseline by a wide margin, don't celebrate yet. Check every feature's timestamp against the prediction timestamp before you trust the number. More often than I'd like, that's exactly where the win came from.

Exercises

  1. Stationarity experiment. Generate a series with a linear trend. Check stationarity with rolling statistics. Apply first differencing. Check again. How many rounds of differencing does it take for a quadratic trend?

  2. Lag selection. Compute ACF on a seasonal series (period=7). Which lags have the highest autocorrelation? Create lag features using only those lags (not consecutive lags). Does accuracy improve compared to using lags 1 through 7?

  3. Walk-forward vs random split. Train a Ridge regression on lag features. Evaluate with random 80/20 split and with walk-forward validation. How much does the random split overestimate performance?

  4. Feature engineering. Add rolling mean (window=7), rolling std (window=7), and day-of-week features to the lag features. Compare accuracy with and without these extras using walk-forward validation.

  5. Multi-step forecasting. Modify the AR model to predict 5 steps ahead instead of 1. Compare two strategies: (a) predict one step, use the prediction as input for the next step (recursive), and (b) train separate models for each horizon (direct). Which is more accurate?

Key Terms

TermWhat people sayWhat it actually means
Stationarity"The stats don't change over time"A series whose mean, variance, and autocorrelation structure are constant over time
Differencing"Subtract consecutive values"Computing y[t] - y[t-1] to remove trends and achieve stationarity
Autocorrelation (ACF)"How a series correlates with itself"The correlation between a time series and a lagged copy of itself, as a function of the lag
Partial autocorrelation (PACF)"Direct correlation only"Autocorrelation at lag k after removing the effect of all shorter lags
Lag features"Past values as inputs"Using y[t-1], y[t-2], ..., y[t-k] as features to predict y[t]
Walk-forward validation"Time-respecting cross-validation"Evaluation where training data always precedes test data chronologically
ARIMA"The classic time series model"AutoRegressive Integrated Moving Average: combines past values (AR), differencing (I), and past errors (MA)
Seasonality"Repeating calendar patterns"Regular, predictable cycles in a time series tied to calendar periods (daily, weekly, yearly)
Trend"The long-term direction"A persistent increase or decrease in the series level over time
Expanding window"Use all history"Walk-forward validation where the training set grows with each fold
Sliding window"Fixed-size history"Walk-forward validation where the training set is a fixed-length window that slides forward

Further Reading

Code

Ships With