ANI

Building Machine Learning Models of Time Series with sktime in Python

# Introduction

If you work with sensor readings, server metrics, or any data that comes in over time, you already know that level scikit-learn pipes do not fit properly. Time series data has structure that tabular models ignore: seasonality, trend, temporal order, and the fact that future values ​​depend on the past.

sktime is a Python library built specifically for this. It gives you a scikit-learn style API – fit, predict, transform – but designed from the ground up for time series. You can perform forecasting, segmentation, regression, and clustering on time series, all with a consistent interface.

In this article, you will deal with an example problem: predicting temperature readings from an industrial HVAC sensor. You will learn how sktime handles time series data, how to build preprocessing pipelines, how to fit predictors, and how to test them.

You can find the code on GitHub.

# What is required

You will need Python 3.10 or higher and basic familiarity with pandas. Install everything you need with:

pip install sktime pmdarima statsmodels

If you would like to have all optional dependencies in one place, pip install sktime[all_extras] it covers us.

# What makes sktime useful

It helps to understand the problem that sktime solves. In scikit-learn, your data is a 2D table – rows are samples, columns are features. Time series data violates this assumption because each “row” is a sequence of values ​​over time, and the order of those values ​​is important.

The main data containers you will use are:

Data Type Representation Explanation
Series pd.Series or pd.DataFrame

A single time series used in vanilla forecasting.

The panel pd.DataFrame with 2-level MultiIndex

A collection of multiple independent time series.

Hierarchical pd.DataFrame at level 3+ MultiIndex

An organized set of time series with integration levels across multiple dimensions.

In the time index itself, sktime supports several time indexes: DatetimeIndex, PeriodIndex, Int64Indexagain RangeIndex to your pandas stuff. The index must be monotonic. If you use DatetimeIndexi freq attribute must be set.

# Sets the DataSet

Let's create a virtual dataset. Consider an HVAC sensor in a factory that records the temperature every hour. Readings have a seasonal daily pattern (higher during business hours), a slight upward trend due to summer, and some noise.

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)}")

Output:

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: 

# Classifying Time Series Data for Training and Testing

Split time series data is different from tabular data — you can't shuffle rows. You should always divide chronologically: train on earlier data, test on later data.

sktime offers 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)}")

Output:

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

The function ensures that the classification is clean and chronological — there is no leakage of data from the future to the training set.

# Defining the Forecast 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 provides:

Horizon length: 168
First forecast point: 2026-03-25 00:00:00
Last forecast point:  2026-03-31 23:00:00

You can also use related horizons as well fh = [1, 2, 3, ..., 168]which means “1 step forward, 2 steps forward, …”. Perfect horizons are neat if you have the real time stamps you want for predictions.

# Building a Processing and Forecasting Pipeline

Real sensor data has missing values, seasonal patterns, and trends — you need to handle all of these before or during forecasting. sktime TransformedTargetForecaster allows you to combine a variable and a predictor into a single estimator. The transformation is applied to the target string y before input, and is automatically reversed in the output mode during prediction.

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())

Output:

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 interpolating the surrounding readings, which works well for sensor data.
  • Detrender() matches a definite trend in the training series and removes it; in the forecast adds a backward trend.
  • Deseasonalizer(sp=24) removes the 24-hour cycle from residues; sp represents the season.
  • Finally, ExponentialSmoothing it predicts suspended, obsolete residuals.
  • When predict() is called, all inverted variables are applied in reverse order by default, and you get predictions back to the original temperature scale.

# Forecast Testing

sktime also includes standard test metrics. For forecasting, mean absolute error (MAE) and absolute percentage error (MAPE) are common choices.

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}%")

Output:

MAE:  0.584 °C
MAPE: 2.40%

# Alternative Forecast Exchange

One of the major advantages of the sktime interface is that changing the underlying algorithm requires changing just one line. Let's try 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}%")

Output:

ARIMA MAE:  0.586 °C
ARIMA MAPE: 2.41%

The important point is that the pre-processing steps – imputation, detrending, deseasonization – remained the same. He only changed the last sentence, and everything else was built cleanly around it.

# Always Cross Validate

Holding a single test window can be misleading. sktime provides cross-validation time series with separators that respect temporal ordering.

SlidingWindowSplitter uses a rolling window: the training window moves forward in time, always equal in length. ExpandingWindowSplitter it increases the training set cumulatively as you progress, which is ideal if you want to use 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")

Output:

   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 wrap metrics and time. MAE cross-validation verifies that the model performs normally across different time windows in the data.

# Next Steps

This article covers basic prediction workflows during sktime, but the library goes beyond basic prediction functions.

It also supports time series segmentation, probabilistic forecasting with uncertainty estimates, training shared models across multiple correlated time series, customizing machine learning algorithms for sequential forecasting, and automated model selection and tuning workflows.

One of the biggest strengths of sktime is its consistent API and integration with the extensive Python machine learning ecosystem, making testing easy for both beginners and experienced practitioners. The sktime documentation and example notebooks are particularly well written and worth referencing if you regularly work with forecasting or temporal data problems.

Count Priya C is an engineer and technical writer from India. He loves working at the intersection of mathematics, programming, data science, and content creation. His areas of interest and expertise include DevOps, data science, and natural language processing. She enjoys reading, writing, coding, and coffee! Currently, he works to learn and share his knowledge with the engineering community by authoring tutorials, how-to guides, ideas, and more. Bala also creates engaging resource overviews and code tutorials.

Source link

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button