# Entry
You can download the code on GitHub.
# Prerequisites
You will need Python 3.10 or higher and basic knowledge of pandas. Install everything you need with:
pip install sktime pmdarima statsmodels
If you prefer to have all optional dependencies at once, pip install sktime[all_extras] covers them.
# What makes sktime useful
The main data containers you will utilize are:
| Data type | Representation | Description |
|---|---|---|
| Number |
pd.Series Or pd.DataFrame
|
A single time series used in vanilla forecasting. |
| Plate |
pd.DataFrame with 2 levels MultiIndex
|
A collection of multiple independent time series. |
| Hierarchical |
pd.DataFrame with level 3+ MultiIndex
|
A structured set of time series with levels of aggregation in multiple dimensions. |
For the time index itself, sktime supports several time indexes: DatetimeIndex, PeriodIndex, Int64IndexAND RangeIndex on panda objects. The index must be monotonic. If you utilize DatetimeIndex, freq attribute should be set.
# Configuring the dataset
Let’s create a realistic dataset. Imagine an HVAC sensor in a factory that records the temperature every hour. The readings show daily seasonality (higher during working hours), a slight upward trend due to summer and some disruptions.
import numpy as np
import pandas as pd
np.random.seed(42)
# 90 days of hourly readings starting Jan 1, 2026
n_hours = 90 * 24
timestamps = pd.date_range(start="2026-01-01", periods=n_hours, freq="h")
# Trend: gradual 5-degree rise over 90 days
trend = np.linspace(0, 5, n_hours)
# Daily seasonality: temperature peaks at 2pm, dips at 4am
hour_of_day = np.arange(n_hours) % 24
daily_cycle = 4 * np.sin(2 * np.pi * (hour_of_day - 4) / 24)
# Noise
noise = np.random.normal(0, 0.8, n_hours)
# Base temperature around 20°C
temperature = 20 + trend + daily_cycle + noise
# Introduce a few missing values (sensor dropout)
dropout_indices = [300, 301, 302, 1440, 1441]
temperature[dropout_indices] = np.nan
y = pd.Series(temperature, index=timestamps, name="temp_celsius")
y.index.freq = pd.tseries.frequencies.to_offset("h")
print(y.head())
print(f"nShape: {y.shape}")
print(f"Missing values: {y.isna().sum()}")
print(f"Index type: {type(y.index)}")
Exit:
2026-01-01 00:00:00 16.933270
2026-01-01 01:00:00 17.063277
2026-01-01 02:00:00 18.522783
2026-01-01 03:00:00 20.190095
2026-01-01 04:00:00 19.821941
Freq: h, Name: temp_celsius, dtype: float64
Shape: (2160,)
Missing values: 5
Index type:
# Splitting time series data for training and testing
provided by sktime temporal_train_test_split for this purpose:
from sktime.split import temporal_train_test_split
# Hold out the last 7 days (168 hours) as the test set
y_train, y_test = temporal_train_test_split(y, test_size=168)
print(f"Train: {y_train.index[0]} → {y_train.index[-1]}")
print(f"Test: {y_test.index[0]} → {y_test.index[-1]}")
print(f"Train size: {len(y_train)}, Test size: {len(y_test)}")
Exit:
Train: 2026-01-01 00:00:00 → 2026-03-24 23:00:00
Test: 2026-03-25 00:00:00 → 2026-03-31 23:00:00
Train size: 1992, Test size: 168
This feature ensures that the split is spotless and chronological – no data from the future leaks into the training set.
# Determining the forecasting horizon
Before fitting any model you need to tell sktime which time steps you want to predict. This is ForecastingHorizon.
from sktime.forecasting.base import ForecastingHorizon
# Predict 168 steps ahead (7 days of hourly data)
# is_relative=False means we're using absolute timestamps
fh = ForecastingHorizon(y_test.index, is_relative=False)
print(f"Horizon length: {len(fh)}")
print(f"First forecast point: {fh[0]}")
print(f"Last forecast point: {fh[-1]}")
This gives:
Horizon length: 168
First forecast point: 2026-03-25 00:00:00
Last forecast point: 2026-03-31 23:00:00
You can also utilize relative horizons like fh = [1, 2, 3, ..., 168]which means “1 step forward, 2 steps forward…”. Absolute horizons are cleaner if you have actual timestamps for which you want to forecast.
# Building a preprocessing and forecasting pipeline
from sktime.forecasting.exp_smoothing import ExponentialSmoothing
from sktime.forecasting.compose import TransformedTargetForecaster
from sktime.transformations.series.impute import Imputer
from sktime.transformations.series.detrend import Deseasonalizer, Detrender
pipeline = TransformedTargetForecaster(
steps=[
# Step 1: Fill missing sensor readings using linear interpolation
("imputer", Imputer(method="linear")),
# Step 2: Remove the linear trend so the forecaster sees a stationary series
("detrender", Detrender()),
# Step 3: Remove the daily seasonality (sp=24 for hourly data with 24-hour cycles)
("deseasonalizer", Deseasonalizer(model="additive", sp=24)),
# Step 4: Forecast the cleaned, stationary residuals
("forecaster", ExponentialSmoothing(trend=None, seasonal=None)),
]
)
pipeline.fit(y_train, fh=fh)
y_pred = pipeline.predict()
print(y_pred.head())
Exit:
2026-03-25 00:00:00 21.210066
2026-03-25 01:00:00 21.788986
2026-03-25 02:00:00 22.615184
2026-03-25 03:00:00 23.688449
2026-03-25 04:00:00 24.621127
Freq: h, Name: temp_celsius, dtype: float64
Here’s what each step does:
Imputer(method="linear")fills in missing values by linearly interpolating between surrounding readings, which works well for sensor data.Detrender()fits a linear trend to the training series and subtracts it; as forecast adds the trend back.Deseasonalizer(sp=24)removes 24-hour cycle from residues;spdenotes a seasonal period.- At last,
ExponentialSmoothingpredicts detrenched, deseasonalized residues. - When
predict()is called, all inverse transformations will be automatically applied in reverse order and you will get predictions in the original temperature scale.
# Forecast assessment
sktime integrates with standard assessment metrics. For forecasting, the common choices are mean absolute error (MAE) and mean absolute percent error (MAPE).
from sktime.performance_metrics.forecasting import (
mean_absolute_error,
mean_absolute_percentage_error,
)
mae = mean_absolute_error(y_test, y_pred)
mape = mean_absolute_percentage_error(y_test, y_pred)
print(f"MAE: {mae:.3f} °C")
print(f"MAPE: {mape*100:.2f}%")
Exit:
MAE: 0.584 °C
MAPE: 2.40%
# Change in another forecast
One of the greatest advantages of the sktime interface is that replacing the underlying algorithm requires changing only one line. Let’s try the ARIMA model instead of exponential smoothing and compare.
from sktime.forecasting.arima import ARIMA
pipeline_arima = TransformedTargetForecaster(
steps=[
("imputer", Imputer(method="linear")),
("detrender", Detrender()),
("deseasonalizer", Deseasonalizer(model="additive", sp=24)),
# ARIMA(1,1,1) on the cleaned residuals
("forecaster", ARIMA(order=(1, 1, 1), suppress_warnings=True)),
]
)
pipeline_arima.fit(y_train, fh=fh)
y_pred_arima = pipeline_arima.predict()
mae_arima = mean_absolute_error(y_test, y_pred_arima)
mape_arima = mean_absolute_percentage_error(y_test, y_pred_arima)
print(f"ARIMA MAE: {mae_arima:.3f} °C")
print(f"ARIMA MAPE: {mape_arima*100:.2f}%")
Exit:
ARIMA MAE: 0.586 °C
ARIMA MAPE: 2.41%
The key point is that the preprocessing steps – imputation, detrending, de-seasonalization – remained identical. You only changed the final forecaster and everything else fell into place nicely around it.
# Cross-validation over time
SlidingWindowSplitter uses a rolling window: the training window moves forward in time, always remaining the same length. ExpandingWindowSplitter increases training sets cumulatively as you progress, which is more suitable when you want to utilize all available history.
from sktime.split import ExpandingWindowSplitter
from sktime.forecasting.model_evaluation import evaluate
# Expanding window: start with 1800-hour train set, evaluate on 168-hour windows
cv = ExpandingWindowSplitter(
initial_window=1800,
fh=list(range(1, 169)),
step_length=168,
)
results = evaluate(
forecaster=pipeline,
y=y,
cv=cv,
scoring=mean_absolute_error,
return_data=False,
)
print(results[["test__DynamicForecastingErrorMetric", "fit_time"]].round(3))
print(f"nMean CV MAE: {results['test__DynamicForecastingErrorMetric'].mean():.3f} °C")
Exit:
test__DynamicForecastingErrorMetric fit_time
0 0.627 0.274
1 0.585 0.100
Mean CV MAE: 0.606 °C
evaluate returns a DataFrame with metrics and assembly time. MAE cross-validation confirms that the model consistently generalizes to the data across different time windows.
# Next steps
This article covers the basic forecasting workflow in sktime, but the library goes far beyond basic forecasting tasks.
One of the biggest benefits of sktime is its consistent API and integration with the broader Python machine learning ecosystem, making experimentation easier for both beginners and experienced practitioners. The sktime documentation AND sample notebooks are particularly well-written and worth bookmarking if you regularly deal with forecasting or interim data issues.
Bala Priya C is a software developer and technical writer from India. He likes working at the intersection of mathematics, programming, data analytics and content creation. Her areas of interest and specialization include DevOps, data analytics and natural language processing. She enjoys reading, writing, coding and coffee! He is currently working on learning and sharing his knowledge with the developer community by writing tutorials, guides, reviews, and more. Bala also creates fascinating resource overviews and coding tutorials.
