Time Series Models — Implementation
Goal
Implement ARIMA, ETS/Holt-Winters, and neural time series models with walk-forward cross-validation.
Conceptual Counterpart
- Time Series Models — stationarity, ARIMA identification, seasonal decomposition, state space models
- Demand Forecasting — production use case combining SARIMAX and LightGBM
- Inventory Optimization — downstream decision system consuming forecasts
Purpose
Practical implementation of ARIMA, ETS, and neural time series models.
Examples
- Stationarity testing and differencing
- Auto-ARIMA with pmdarima
- Holt-Winters ETS with statsmodels
- Walk-forward cross-validation
Architecture
Raw time series → Stationarity check → Differencing if needed
→ Model fitting (ARIMA / ETS / LGBM-based)
→ Walk-forward evaluation
→ Rolling forecast
Implementation
Setup
pip install statsmodels pmdarima scikit-learnStationarity Testing
from statsmodels.tsa.stattools import adfuller
import pandas as pd
def test_stationarity(series: pd.Series) -> bool:
result = adfuller(series.dropna())
p_value = result[1]
print(f"ADF Statistic: {result[0]:.4f}, p-value: {p_value:.4f}")
return p_value < 0.05 # True → stationary
# Differencing if non-stationary
diff1 = series.diff().dropna()
diff2 = diff1.diff().dropna()Auto-ARIMA (pmdarima)
import pmdarima as pm
from statsmodels.tsa.statespace.sarimax import SARIMAX
# Auto ARIMA with stepwise search
auto_model = pm.auto_arima(
train,
seasonal=True,
m=12, # seasonal period (12 = monthly)
stepwise=True,
information_criterion="aic",
suppress_warnings=True
)
print(auto_model.summary())
# Forecast
n_forecast = 12
forecast, conf_int = auto_model.predict(n_periods=n_forecast, return_conf_int=True)SARIMAX (statsmodels)
from statsmodels.tsa.statespace.sarimax import SARIMAX
model = SARIMAX(
endog=train,
order=(1, 1, 1), # (p, d, q)
seasonal_order=(1, 1, 1, 12), # (P, D, Q, s)
trend="c"
)
res = model.fit(disp=False)
print(res.summary())
# Forecast
pred = res.get_forecast(steps=12)
forecast = pred.predicted_mean
ci = pred.conf_int()Holt-Winters (ETS)
from statsmodels.tsa.holtwinters import ExponentialSmoothing
ets = ExponentialSmoothing(
train,
trend="add", # "add", "mul", None
seasonal="add", # "add", "mul", None
seasonal_periods=12,
damped_trend=True
)
ets_fit = ets.fit(optimized=True)
forecast = ets_fit.forecast(12)Prophet
Prophet (Meta/Facebook, 2017) uses an additive decomposition model:
Strengths: handles missing data, multiple seasonalities, and changepoints without manual tuning. Best for business time series with strong seasonality and known calendar effects.
from prophet import Prophet
import pandas as pd
# Prophet requires columns named 'ds' (datetime) and 'y' (target)
df_prophet = df.rename(columns={'date': 'ds', 'value': 'y'})
# Initialise and fit
m = Prophet(
seasonality_mode='multiplicative', # or 'additive'
yearly_seasonality=True,
weekly_seasonality=True,
daily_seasonality=False,
changepoint_prior_scale=0.05, # flexibility of trend (default 0.05)
interval_width=0.95 # uncertainty interval width
)
# Add custom seasonality (e.g. monthly)
m.add_seasonality(name='monthly', period=30.5, fourier_order=5)
# Add country holidays
m.add_country_holidays(country_name='US')
m.fit(df_prophet)
# Forecast 365 days into the future
future = m.make_future_dataframe(periods=365, freq='D')
forecast = m.predict(future)
# Key output columns
print(forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail())
# Decomposition plots
fig1 = m.plot(forecast)
fig2 = m.plot_components(forecast)Hyperparameter guidance:
| Parameter | Default | When to increase | When to decrease |
|---|---|---|---|
changepoint_prior_scale | 0.05 | Trend is highly irregular | Trend is smooth |
seasonality_prior_scale | 10 | Seasonality amplitude varies | Stable seasonality |
holidays_prior_scale | 10 | Holiday effects dominate | Weak holiday effects |
seasonality_mode | additive | Variance grows with level | Variance is stable |
Install: pip install prophet
Walk-Forward Validation
import numpy as np
from sklearn.metrics import mean_absolute_error
def walk_forward_cv(series, n_test=12, n_folds=5):
"""Walk-forward cross-validation for time series."""
maes = []
series_array = np.array(series)
fold_size = n_test
for i in range(n_folds):
test_end = len(series_array) - i * fold_size
test_start = test_end - fold_size
train_data = series_array[:test_start]
test_data = series_array[test_start:test_end]
if len(train_data) < 24:
continue
try:
m = pm.auto_arima(train_data, seasonal=True, m=12, stepwise=True,
suppress_warnings=True)
preds, _ = m.predict(n_periods=n_test, return_conf_int=True)
maes.append(mean_absolute_error(test_data, preds))
except Exception as e:
print(f"Fold {i} failed: {e}")
return np.mean(maes), np.std(maes)
mae_mean, mae_std = walk_forward_cv(series)
print(f"Walk-forward MAE: {mae_mean:.3f} ± {mae_std:.3f}")LGBM as a Time Series Regressor (feature-based)
import lightgbm as lgb
import pandas as pd
def create_lag_features(series: pd.Series, lags: list) -> pd.DataFrame:
df = pd.DataFrame({"y": series})
for lag in lags:
df[f"lag_{lag}"] = df["y"].shift(lag)
df["rolling_mean_4"] = df["y"].shift(1).rolling(4).mean()
df["rolling_std_4"] = df["y"].shift(1).rolling(4).std()
return df.dropna()
df = create_lag_features(series, lags=[1, 2, 3, 6, 12])
X = df.drop("y", axis=1)
y = df["y"]
# Standard train/test split by time indexTrade-offs
- ARIMA: statistically rigorous, interpretable parameters; works well for short, stationary series with up to ~100 obs.
- ETS: better for strongly seasonal series; handles multiplicative seasonality.
- LGBM / feature engineering: highest accuracy for large datasets; no uncertainty quantification without extra effort.
- Always evaluate with walk-forward (not random) CV to avoid lookahead bias.