4. Data preprocessing and validation strategies
Time series data rarely arrives in a form that a machine learning model can consume directly. Sensor dropouts, unit mismatches, holiday spikes, and the temptation to shuffle observations before splitting all conspire to produce models that look excellent in a notebook and fail in production. This chapter covers the four preprocessing and validation decisions that most strongly determine whether a forecasting pipeline generalizes: handling missing values, treating outliers, scaling features, and constructing honest cross-validation splits.
Missing Values: Why Interpolation Is Not Always Safe
Missingness in time series is rarely random. A sensor that fails during a heatwave, a payment gateway that goes offline on a holiday, or a retail store that closes for renovation all produce gaps correlated with the target itself. Before choosing a fill strategy, classify the gap:
- Missing completely at random (MCAR): isolated dropouts unrelated to the signal. Simple interpolation is usually acceptable.
- Missing at random (MAR): missingness depends on observed covariates (e.g., a station reports only when temperature is within range). Model-based imputation using those covariates is appropriate.
- Missing not at random (MNAR): missingness depends on the unobserved value (e.g., a meter is bypassed when load exceeds a threshold). Imputation will bias the target; consider adding a missingness indicator feature instead.
For short gaps, forward-fill followed by linear interpolation is a reasonable baseline. For seasonal series, seasonal interpolation is stronger: fill a missing value at time t using the value at t − m, where m is the seasonal period (24 for hourly data with daily seasonality, 7 for daily data with weekly seasonality). The following snippet applies both strategies and records which rows were imputed so the model can later be told about them:
import pandas as pd
import numpy as np
df = df.sort_values("timestamp").set_index("timestamp")
df["is_imputed"] = df["demand"].isna()
# Linear interpolation for short gaps
df["demand_lin"] = df["demand"].interpolate(method="linear", limit=3)
# Seasonal fill for remaining gaps (m = 24 for hourly data)
seasonal = df["demand"].shift(24)
df["demand_filled"] = df["demand_lin"].fillna(seasonal)
# Anything still missing: drop or flag
df["demand_filled"] = df["demand_filled"].fillna(df["demand"].median())
Two rules matter here. First, never interpolate across a gap that spans a regime change (a price change, a system migration) — the interpolated values will be fiction. Second, always carry the is_imputed flag into the feature set; tree-based models can learn to discount imputed regions, and linear models can be given interaction terms.
Outliers: Distinguish Errors from Events
An outlier in a time series is not automatically a mistake. A 10× spike in API calls during a product launch is a real event the model should learn from; a negative temperature reading from a faulty sensor is not. Separate the two before deciding on treatment.
Detection methods that respect temporal structure include:
- Rolling z-score: flag points where |xt − μt−w:t| > k·σt−w:t, with window w typically 24–168 for hourly data and k = 3.
- STL residual threshold: decompose into trend, seasonal, and residual; flag residuals beyond 3× the residual IQR.
- Domain bounds: hard physical limits (negative counts, humidity > 100%) are unambiguous errors.
Once flagged, choose a treatment that matches the cause. Sensor faults should be set to missing and then imputed. Genuine events should be kept but optionally capped (winsorized) at a percentile — for example, clip to the 1st and 99th percentiles — so a single extreme value does not dominate a squared-error loss. Crucially, fit the clipping thresholds on the training window only; using the full series leaks future information.
Scaling: Match the Transform to the Model
Scaling is not universally required. Tree-based models (XGBoost, LightGBM, Random Forest) split on thresholds and are invariant to monotonic transformations, so scaling changes nothing except training time. Distance-based and gradient-based models — k-NN, SVR, linear regression with regularization, and all neural networks — are sensitive to feature magnitude and benefit substantially from scaling.
For time series, the choice of scaler interacts with non-stationarity:
| Scaler | When to use | Caution |
|---|---|---|
| StandardScaler (z-score) | Roughly stationary series, Gaussian-ish residuals | Mean and std must come from the training window |
| MinMaxScaler | Bounded signals (e.g., utilization 0–1) | Extremely sensitive to outliers in the fit window |
| RobustScaler (median/IQR) | Series with heavy tails or occasional spikes | Still assumes a stable scale over time |
| Differencing + scaling | Strong trend or unit-root behavior | Differencing removes level; invert before reporting |
A practical pattern for series with slow drift is to scale within a rolling window rather than globally. For example, divide each observation by the mean of the preceding 168 hours. This keeps the model's input distribution stable even as the underlying level changes, and it is trivially invertible at prediction time.
Time Series Cross-Validation: Respect the Arrow of Time
Standard k-fold cross-validation shuffles observations, which lets the model train on Tuesday and validate on Monday. For forecasting, that is leakage. The correct scheme is rolling-origin (also called walk-forward) cross-validation: train on a contiguous prefix, validate on the immediately following block, then slide the origin forward and repeat.
Given a series of length N, an initial training size n0, a forecast horizon h, and a step size s, fold i uses:
- Training: indices [0, n0 + i·s)
- Validation: indices [n0 + i·s, n0 + i·s + h)
With N = 1000, n0 = 600, h = 24, and s = 24, you get 16 folds. Two variants are worth knowing: expanding window (training set grows each fold, as above) and sliding window (training set stays fixed size, dropping the oldest data). Expanding windows are appropriate when the process is stationary; sliding windows help when the relationship drifts and old data becomes misleading.
Scikit-learn's TimeSeriesSplit implements the expanding-window variant directly:
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5, test_size=24, gap=0)
for fold, (train_idx, val_idx) in enumerate(tscv.split(X)):
X_tr, y_tr = X.iloc[train_idx], y.iloc[train_idx]
X_va, y_va = X.iloc[val_idx], y.iloc[val_idx]
# fit scaler, imputer, and model on X_tr only
# evaluate on X_va
The gap parameter is important when features use lags: if your longest lag is 7 days, set gap=7 so that no validation feature is computed from a timestamp inside the training window. Without it, a lag-7 feature at the first validation point would read a value from the last training day, creating subtle leakage.
Putting It Together: A Validation Checklist
The following checklist captures the decisions above in the order they should be applied. It is written as a reusable artifact for any forecasting project.
| Step | Action | Failure symptom if skipped |
|---|---|---|
| 1 | Sort by timestamp; verify no duplicate timestamps | Silent row reordering corrupts lag features |
| 2 | Classify missingness (MCAR/MAR/MNAR); add indicator | Model learns biased target distribution |
| 3 | Detect outliers with rolling z-score or STL residuals | Single spike dominates MSE; unstable coefficients |
| 4 | Split into train/validation using rolling origin | Shuffled split leaks future into past |
| 5 | Fit imputer, clipper, scaler on training fold only | Optimistic validation scores that collapse in production |
| 6 | Set gap ≥ max lag length | Lag features read across the train/val boundary |
| 7 | Report per-fold metrics, not just the mean | High variance across folds goes unnoticed |
Two diagnostic signals tell you the pipeline is healthy. First, per-fold validation error should be roughly stable; a fold whose error is 3× the others usually indicates a regime change or a leak. Second, the gap between training and validation error should be small and consistent — a large gap points to overfitting, while a near-zero gap with poor absolute performance points to underfitting or a feature set that does not carry the signal. When either signal appears, revisit the imputation and scaling steps before adding model complexity, because preprocessing errors masquerade as modeling problems far more often than most practitioners expect.