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.

Leakage trap: Computing means, standard deviations, or percentile thresholds over the entire dataset before splitting is one of the most common causes of over-optimistic backtest results. Every statistic used for imputation, outlier clipping, or scaling must be estimated from the training fold alone.

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:

ScalerWhen to useCaution
StandardScaler (z-score)Roughly stationary series, Gaussian-ish residualsMean and std must come from the training window
MinMaxScalerBounded signals (e.g., utilization 0–1)Extremely sensitive to outliers in the fit window
RobustScaler (median/IQR)Series with heavy tails or occasional spikesStill assumes a stable scale over time
Differencing + scalingStrong trend or unit-root behaviorDifferencing 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.

Rolling-Origin Cross-Validation with Lag Gap time → Fold 1 train [0, 600) val Fold 2 train [0, 624) val Fold 3 train [0, 648) val Fold 4 train [0, 672) val gap training window validation window gap (lag buffer) Fit on training fold only 1. Impute missing values 2. Clip outliers (p1–p99) 3. Fit scaler (μ, σ) 4. Apply to validation fold Never refit on validation data

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.

StepActionFailure symptom if skipped
1Sort by timestamp; verify no duplicate timestampsSilent row reordering corrupts lag features
2Classify missingness (MCAR/MAR/MNAR); add indicatorModel learns biased target distribution
3Detect outliers with rolling z-score or STL residualsSingle spike dominates MSE; unstable coefficients
4Split into train/validation using rolling originShuffled split leaks future into past
5Fit imputer, clipper, scaler on training fold onlyOptimistic validation scores that collapse in production
6Set gap ≥ max lag lengthLag features read across the train/val boundary
7Report per-fold metrics, not just the meanHigh 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.