In this guide
Forecasting is the machine-learning task students underestimate most. Classification gives you a clean score; forecasting gives you a model that looks brilliant on a shuffled train-test split and collapses the moment it meets the future. The gap is almost always methodological, not architectural: data leaked across time, a random split that let the model peek ahead, or an evaluation against no baseline at all. An LSTM trained correctly on honest splits will beat a Transformer trained on leaked data every time — because the second one was never actually forecasting.
This guide builds the complete student forecasting workflow: framing the problem as supervised windows, preprocessing time series without leaking the future, choosing sequence lengths, a clean LSTM implementation, walk-forward validation (the only evaluation scheme that matters here), the naive baselines your model must beat, and how to report accuracy without misleading anyone — including yourself.
Framing: from a timeline to supervised windows
A neural network does not take "a timeline" as input. It takes fixed-size vectors. The standard framing converts the series into overlapping windows: use the past LOOKBACK steps to predict the next HORIZON steps.
Worked example — hourly energy consumption, and you want to predict the next 24 hours from the past 7 days:
- Raw series: one value per hour, say 8,760 points for a year.
LOOKBACK = 168(7 days × 24 hours),HORIZON = 24.- Window 1: hours 1–168 → predict hours 169–192. Window 2: hours 2–169 → predict hours 170–193. And so on.
- Result: ~8,500 training examples, each an input of shape (168, n_features) and a target of shape (24,).
The code that builds these windows is five lines and belongs in every forecasting project:
import numpy as np
def make_windows(series, lookback, horizon):
X, y = [], []
for i in range(len(series) - lookback - horizon + 1):
X.append(series[i:i + lookback])
y.append(series[i + lookback:i + lookback + horizon])
return np.array(X), np.array(y)
Two framing decisions to make deliberately. First, univariate vs multivariate: predicting from the target's own history alone is simpler and often sufficient; adding features (temperature for energy demand, day-of-week for retail) helps when you have a causal story, not just more columns. Second, horizon: predicting 1 step ahead is dramatically easier than 24. Choose the horizon your application needs — a "next-day" energy forecast like an energy consumption forecaster — and evaluate at that horizon, not at the easier one.
Preprocessing: where the future leaks in
Time-series preprocessing has one cardinal rule: no statistic computed on the future may influence the past. Every one of these mistakes is common in student code:
- Scaling: fit the scaler (MinMax, Standard) on the training portion only, then transform validation and test with it. Fitting on the full series leaks the test range into the training data. In code,
scaler.fit(X_train)— neverfit(X_all). - Missing values: forward-fill or interpolate, but do it within each split, not across the split boundary. A value interpolated from a future observation is a leak.
- Datetime features: hour-of-day, day-of-week, and month are powerful features for human-driven series (energy, traffic, retail). Encode cyclical ones properly — hour 23 and hour 0 are adjacent, so use
sin(2π·hour/24)andcos(2π·hour/24)rather than the raw integer, which pretends midnight is far from 11 PM. - Stationarity: many classical methods need stationary data, but LSTMs do not require it — they can learn trends. Still, differencing or detrending sometimes helps training stability. Try both, measure, keep what wins. Do not difference blindly because a tutorial said to.
- Outliers: sensor glitches (a temperature reading of 999°C) will dominate an MSE loss. Clip or remove them before scaling, document the rule, and keep a count — "37 of 8,760 points removed as sensor faults" is a methodology sentence, not an embarrassment.
The leak test: after preprocessing, ask of every step — "could I have computed this knowing only data up to the split point?" If the answer is no, rework it. Shuffled cross-validation on time series fails this test structurally, which is why the next section exists.
Choosing the lookback window
The lookback is how much history the model sees. Too short and it misses the pattern; too long and training slows while the model drowns in irrelevant past:
| Series | Sensible lookback | Reasoning |
|---|---|---|
| Hourly data, daily cycle | 24–72 hours | Covers 1–3 full daily cycles |
| Hourly data, weekly cycle | 168+ hours | A week of history to learn weekday/weekend shapes |
| Daily data, yearly seasonality | 30–90 days | Captures recent trend; full-year lookbacks rarely help LSTMs |
| Stock prices (daily) | 30–60 days | Beyond ~60 days the signal-to-noise rarely justifies the compute |
| Sensor data (minutes) | Enough to cover 2–3 cycles of the fastest relevant pattern | Match the physics, not a round number |
Treat these as starting points and tune on validation — but tune once, on a proper time-based split. A useful diagnostic: if doubling the lookback does not move validation error, the extra history is noise to your model, and the shorter window trains faster.
The LSTM: what it is and why it, honestly
A vanilla RNN fails on long sequences because gradients vanish as they propagate back through time — by step 50, the network has effectively forgotten step 1. The LSTM (Long Short-Term Memory) fixes this with a cell state: a conveyor belt running through the sequence, guarded by three gates (forget, input, output) that learn what to keep, what to add, and what to emit. In practice, this means an LSTM can use patterns tens to hundreds of steps back, where a vanilla RNN manages roughly ten.
A clean student-scale LSTM forecaster in PyTorch:
import torch
import torch.nn as nn
class LSTMForecaster(nn.Module):
def __init__(self, n_features, hidden=64, layers=2,
horizon=24, dropout=0.2):
super().__init__()
self.lstm = nn.LSTM(n_features, hidden, layers,
batch_first=True, dropout=dropout)
self.head = nn.Linear(hidden, horizon)
def forward(self, x):
# x: (batch, lookback, n_features)
out, _ = self.lstm(x)
last = out[:, -1, :] # representation of the whole window
return self.head(last) # (batch, horizon)
model = LSTMForecaster(n_features=5, horizon=24)
Design notes: two layers and 64 hidden units is a sane starting point — student forecasting problems rarely need more, and bigger models overfit small series faster. Dropout between LSTM layers (0.2) is the main regulariser. The linear head maps the final hidden state to all horizon steps at once (the "direct" multi-step strategy — more on strategies below). Train with Adam, MSE or MAE loss, and early stopping on validation loss with a patience of 10–20 epochs. Forecasting models overfit quietly; early stopping is not optional.
Walk-forward validation: the only honest evaluation
Random train-test splits are invalid for time series: a random split trains on March to predict January, which is not forecasting, it is time travel. The correct scheme is walk-forward (rolling-origin) validation:
- Split the timeline into consecutive blocks: train on months 1–8, validate on month 9.
- Roll forward: train on months 1–9, validate on month 10.
- Repeat, averaging the validation scores.
Each validation block is strictly in the future relative to its training data — exactly like deployment. The averaged score estimates how the model performs on unseen future data, which is the only number that matters.
# walk-forward splits on windowed data (windows already chronological)
n = len(X)
folds = []
fold_size = n // 5
for k in range(1, 5):
train_end = k * fold_size
val_end = train_end + fold_size
folds.append((X[:train_end], y[:train_end],
X[train_end:val_end], y[train_end:val_end]))
One subtlety: because windows overlap, the last training window and the first validation window share input history. That is fine — what must not leak is the target: ensure no validation target timestamp appears in training. With the windowing code above and chronological splits, this holds automatically. Document the scheme in your report with a small diagram of the folds; examiners who know time series will look for exactly this.
Metrics: MAE, RMSE, MAPE — and their traps
| Metric | Formula idea | Good for | Trap |
|---|---|---|---|
| MAE | Mean absolute error | Interpretable ("off by 12 units on average"); robust to outliers | Underweights large errors |
| RMSE | Square root of mean squared error | Penalises large errors; standard in energy/weather | Dominated by a few bad spikes; scale-dependent |
| MAPE | Mean absolute percentage error | Scale-free; "off by 8%" reads well in reports | Explodes when actual values are near zero; undefined at zero |
| sMAPE | Symmetric MAPE | Bounded 0–200%; common in forecasting competitions | Still biased; report alongside MAE |
Report at least two metrics, always at the horizon your application needs, and always with the baseline alongside (next section). A single RMSE number with no baseline and no horizon is not a result — it is a number. And never report metrics computed on scaled data: inverse-transform predictions to original units first, or your "MAE of 0.03" means nothing to anyone.
Beat the naive baseline first — non-negotiable
Before celebrating any LSTM result, compute the persistence (naive) forecast: predict that the next value equals the last observed value (for HORIZON > 1, repeat it, or use the seasonal naive: same hour last week). This baseline is embarrassingly strong on many real series — smooth, autocorrelated data like temperature or AQI is genuinely hard to beat.
The rule: your model must beat persistence on walk-forward validation, or you do not have a forecasting model — you have an expensive random walk. Put both numbers in the report. If the LSTM loses, that is a finding, not a failure: it tells you the series is dominated by noise at your horizon, and the honest project becomes explaining why (with the baseline table as evidence) rather than hiding it. Projects like a stock price predictor live exactly here — short-horizon stock moves are close to unpredictable, and a report that demonstrates this rigorously with baselines is stronger than one that cherry-picks a lucky window. An AQI forecaster is the friendlier case: pollution series have strong daily structure that a good model genuinely captures.
Multi-step strategies
Predicting 24 steps ahead can be done three ways:
- Recursive: train a 1-step model, feed each prediction back as input for the next step. Simple, but errors compound — step 24 is built on 23 predictions, each slightly wrong. Fine for horizons under ~6.
- Direct: train one model per horizon step (or one head with HORIZON outputs, as in the code above). No error compounding, but the model does not know the steps are related. The usual student default, and usually the right one.
- Seq2Seq (encoder-decoder): an LSTM encoder reads the history, a decoder LSTM emits the future step by step. The most expressive, and the most prone to overfitting on small series. A version-two architecture, not the starting point.
Start with direct multi-output. Move to seq2seq only if direct plateaus and you have enough data to feed the extra capacity.
LSTM vs ARIMA vs Prophet: choosing honestly
| Approach | Strengths | Weaknesses | Reach for it when |
|---|---|---|---|
| LSTM | Learns nonlinear patterns; multivariate naturally; no stationarity needed | Data-hungry; many hyperparameters; overfits small series | 1,000+ points, nonlinear structure, multiple features |
| ARIMA / SARIMA | Statistically principled; strong on small data; confidence intervals | Univariate; linear; stationarity required; painful to tune by hand | A few hundred points, clear autocorrelation, report needs statistical rigour |
| Prophet | Handles seasonality + holidays automatically; fast; robust defaults | Less flexible; weaker on complex multivariate structure | Business/demand series with strong calendar effects, limited ML expertise on the team |
There is no shame in a classical baseline winning. A good report often shows all three on walk-forward validation and picks the winner with evidence — that comparison table is methodology-chapter gold. The catalogue's energy forecaster built on Prophet exists precisely because Prophet is the right tool for calendar-driven demand series.
Common failures and how to read them
| Symptom | Likely cause | Check |
|---|---|---|
| Great train loss, terrible walk-forward score | Overfitting, or leakage in the original split | Shrink the model, add dropout, verify splits are chronological |
| Model predicts an almost-flat line | MSE loss on a noisy series — the mean is the safest prediction | Switch to MAE; check whether persistence beats you (it probably does) |
| Wild oscillations in multi-step output | Recursive strategy compounding errors | Switch to direct multi-output |
| Validation loss jumps between folds | Non-stationarity — regime change mid-series (COVID-era data is classic) | Plot the series; consider splitting regimes or adding regime features |
| Loss is NaN within epochs | Unscaled inputs or exploding gradients | Verify scaler fit on train only; add gradient clipping (clip_grad_norm_) |
| Model ignores obvious seasonality | Lookback shorter than the seasonal period | Extend lookback to cover 2+ full cycles |
Honest accuracy reporting
Forecasting reports have a specific failure mode: horizon cherry-picking. "Our model achieves 4% MAPE" means nothing without the horizon — 4% at 1-hour-ahead on smooth data is trivial; at 7-days-ahead it would be remarkable. Your report must state, for every number: the horizon, the metric, the validation scheme (walk-forward, number of folds), and the baseline it beat. Report per-horizon error curves (error vs horizon step) rather than a single averaged number — the curve shows where the model degrades, which is genuinely useful information for anyone deploying it. And never, ever tune on the test set: the test block is touched once, at the end, and its number is the one in the abstract.
The pre-training checklist
- Problem framed as windows: lookback and horizon chosen for the application, not by default
- Scaler fit on train only; missing values handled within splits; outliers documented
- Cyclical datetime features encoded with sin/cos; horizon-appropriate features only
- Splits are chronological (walk-forward); no shuffled K-fold anywhere
- Persistence and seasonal-naive baselines computed on the same folds
- Model starts small (2 layers, 64 hidden); dropout and early stopping in place
- Metrics reported in original units, at the application horizon, with baselines alongside
- Test block touched exactly once, at the end
Feature engineering: the unglamorous multiplier
On small-to-medium series, well-chosen features often move the needle more than architecture changes. The features worth trying, roughly in order of return:
- Lags of the target: yesterday's value, last week's same hour. For strongly autocorrelated series these are the most predictive features you have — which is also why the persistence baseline is strong.
- Rolling statistics: 7-day rolling mean and standard deviation of the target. They give the LSTM a pre-computed sense of recent level and volatility, shortening what it must learn from raw history.
- Calendar features: hour, weekday/weekend flag, month, public-holiday flag. Energy demand on Diwali does not look like demand on a normal Tuesday; without a holiday feature the model cannot know why.
- Exogenous drivers: temperature for energy and AQI, rainfall for agriculture-adjacent series. Add them only with a causal story — every extra feature is another chance to overfit, and a feature whose future values you will not have at prediction time is useless (a forecast that needs tomorrow's temperature as input is not a forecast).
- Fourier terms: sin/cos pairs at the dominant seasonal periods (daily, weekly, yearly). They encode seasonality compactly and are the same trick Prophet uses internally.
The discipline: add features one group at a time on walk-forward validation, exactly like the augmentation ablations. A feature that does not move validation error is complexity without benefit — remove it. Your report's feature table (feature, rationale, validation delta) is concise, honest methodology.
From notebook to a running forecaster
A forecasting model only matters if it runs on new data. The deployment pattern for student projects:
- Freeze the pipeline. The scaler, the feature code, and the window builder used in training become a single
predict.py— the same functions, imported, not rewritten. Rewriting preprocessing for deployment is how train-serve skew is born. - Schedule retraining. Series drift: consumer behaviour changes, sensors degrade. A model trained once and left running decays silently. Even a simple weekly retrain on the latest data (with the walk-forward check re-run) beats a frozen model within months.
- Monitor against the naive baseline in production. Log both your model's error and the persistence baseline's error on live data. The day persistence starts winning is the day your model needs attention — this single comparison is the cheapest monitoring a student deployment can have.
- Serve simply. For a demo, a small Python API (Flask/FastAPI) that loads the checkpoint and serves the next-HORIZON forecast is enough. When you are ready to take that step, the model deployment guide covers the packaging in detail.
Frequently asked questions
How much data does an LSTM forecaster need?
A working rule: at least a few hundred windows after windowing, covering two or more full seasonal cycles. For daily data with yearly seasonality that means two-plus years; for hourly data with weekly seasonality, a few months can suffice. Fewer than ~500 windows and you should strongly consider ARIMA or Prophet instead.
My validation error varies wildly between folds. Is the model broken?
Probably not — the series likely has regime changes (a calm period and a volatile period validate very differently). Plot the per-fold errors against the timeline. If specific folds are outliers, investigate what happened in those periods (festivals, outages, policy changes) rather than tuning hyperparameters to average them away.
Should I predict one step or many steps ahead?
Predict exactly what the application needs. If the requirement is "tomorrow's demand", train and evaluate at horizon 24 (hourly) or 1 (daily) — not at the easier 1-step horizon and not at a harder one for show. The horizon is part of the problem statement.
Can I use future weather forecasts as input features?
Yes, with care: at prediction time you only have forecast weather, not actual weather. If you train on actuals and deploy on forecasts, you have train-serve skew. Either train on historical forecasts (hard to obtain) or accept the skew and measure it. Document whichever you choose.
Putting it together
A credible student forecasting project is 20% LSTM and 80% methodology: honest windowing, leak-free preprocessing, walk-forward validation, a naive baseline that the model actually beats, and reporting that names the horizon with every number. Whether the application is stock prices, energy demand, or air quality, the workflow is the same — and it is the workflow, documented in your report, that an examiner trusts. If you have not yet chosen the dataset, the ML project ideas guide points to time-series tasks sized for a semester.