Прогнозування часових рядів у Python: ARIMA, SARIMA та SARIMAX — повний практичний посібник

Покроковий посібник з прогнозування часових рядів у Python за допомогою ARIMA, SARIMA та SARIMAX. Від перевірки стаціонарності до автоматичного підбору параметрів із pmdarima — з робочими прикладами коду.

Що таке часовий ряд і навіщо його прогнозувати

Часовий ряд (time series) — це, по суті, послідовність спостережень, записаних через рівні проміжки часу. Це можуть бути щоденні дані (курс валют, температура), щотижневі (відвідувачі сайту), щомісячні (обсяг продажів, безробіття) або навіть щосекундні показники серверів чи IoT-сенсорів. Ключова відмінність від звичайних табличних даних проста: порядок спостережень тут має значення, і кожне значення може залежати від того, що було раніше.

Прогнозування часових рядів — одна з найпопулярніших задач у Data Science. І це не дивно, бо сфер застосування справді багато:

  • Прогноз продажів — планування закупівель, складських запасів та бюджету на основі історичних даних
  • Фінансові ринки — прогнозування курсу акцій, валютних пар, криптовалют
  • Метеорологія — передбачення температури, опадів, швидкості вітру
  • Ресурсне планування — оцінка навантаження на сервери, електромережі, транспортну інфраструктуру
  • Епідеміологія — моделювання поширення захворювань

У цьому посібнику ми розберемо класичні статистичні моделі ARIMA, SARIMA та SARIMAX. Вони, може, і не найновіші, але залишаються золотим стандартом прогнозування часових рядів у Python — особливо для рядів із чіткими трендами та сезонністю. А бібліотеки statsmodels та pmdarima роблять їхню побудову доволі комфортною.

Підготовка середовища та встановлення бібліотек

Для роботи з моделями ARIMA знадобиться кілька бібліотек. Встановити їх можна однією командою:

pip install statsmodels pmdarima matplotlib pandas numpy

Переконайтеся, що версії досить свіжі: statsmodels 0.14.4+, pmdarima 2.0+ та pandas 2.2+. Це важливо — старіші версії іноді поводяться непередбачувано з часовими рядами, і потім довго шукаєш баг, якого насправді немає.

Імпортуємо все необхідне на початку скрипта:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# statsmodels — побудова моделей ARIMA/SARIMAX
from statsmodels.tsa.statespace.sarimax import SARIMAX
from statsmodels.tsa.arima.model import ARIMA
from statsmodels.tsa.stattools import adfuller
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from statsmodels.stats.diagnostic import acorr_ljungbox

# pmdarima — автоматичний підбір параметрів
import pmdarima as pm
from pmdarima import auto_arima

# Метрики якості
from sklearn.metrics import mean_absolute_error, mean_squared_error

import warnings
warnings.filterwarnings('ignore')

Завантаження та візуалізація даних

Для прикладів візьмемо класичний датасет AirPassengers — щомісячну кількість авіапасажирів з 1949 по 1960 рік. Його обожнюють у підручниках, і не дарма: він ідеально демонструє одночасну наявність тренду та сезонності.

# Завантаження датасету AirPassengers
url = 'https://raw.githubusercontent.com/jbrownlee/Datasets/master/airline-passengers.csv'
df = pd.read_csv(url, parse_dates=['Month'], index_col='Month')
df.columns = ['passengers']

# Встановлюємо частоту індексу — критично для statsmodels
df.index.freq = 'MS'  # MS = Month Start

print(f"Розмір датасету: {df.shape}")
print(f"Період: {df.index.min()} — {df.index.max()}")
print(f"\nПерші 5 рядків:")
print(df.head())
print(f"\nОсновна статистика:")
print(df.describe())

Візуалізація — це, мабуть, найважливіший крок на старті аналізу. Без неї ви просто кидаєте дані в модель наосліп. А один погляд на графік може розповісти більше, ніж десять тестів:

fig, axes = plt.subplots(2, 1, figsize=(12, 8))

# Графік часового ряду
axes[0].plot(df.index, df['passengers'], color='steelblue', linewidth=1.5)
axes[0].set_title('Кількість авіапасажирів (1949–1960)', fontsize=14)
axes[0].set_ylabel('Пасажири (тис.)')
axes[0].grid(True, alpha=0.3)

# Ковзне середнє та стандартне відхилення
rolling_mean = df['passengers'].rolling(window=12).mean()
rolling_std = df['passengers'].rolling(window=12).std()

axes[1].plot(df.index, df['passengers'], label='Оригінал', alpha=0.6)
axes[1].plot(df.index, rolling_mean, label='Ковзне середнє (12 міс.)', color='red', linewidth=2)
axes[1].fill_between(df.index,
                     rolling_mean - 2 * rolling_std,
                     rolling_mean + 2 * rolling_std,
                     alpha=0.15, color='red', label='±2σ')
axes[1].set_title('Тренд та волатильність', fontsize=14)
axes[1].set_ylabel('Пасажири (тис.)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('time_series_overview.png', dpi=150)
plt.show()

На графіку одразу впадають в око дві речі: висхідний тренд (пасажирів з кожним роком більше) та сезонність (літні піки, зимові спади — класика авіаперевезень). А ще помітно, що амплітуда сезонних коливань теж зростає. Це ознака мультиплікативної сезонності, і з нею доведеться щось робити при побудові моделі.

Перевірка стаціонарності: тест Діккі — Фуллера

Моделі ARIMA працюють коректно лише зі стаціонарними рядами. Стаціонарний — це коли статистичні властивості (середнє, дисперсія, автокореляція) залишаються більш-менш стабільними з часом. Нестаціонарний ряд із трендом чи сезонністю потрібно попередньо перетворити.

Як це перевірити формально? Для цього є розширений тест Діккі — Фуллера (Augmented Dickey-Fuller, ADF). Його нульова гіпотеза H₀ говорить, що ряд містить одиничний корінь — тобто є нестаціонарним. Якщо p-value менше 0.05, ми цю гіпотезу відкидаємо і вважаємо ряд стаціонарним. Просто і елегантно.

def adf_test(series, title=''):
    """Виконує тест Діккі — Фуллера та виводить результати."""
    result = adfuller(series.dropna(), autolag='AIC')

    print(f'=== Тест Діккі — Фуллера: {title} ===')
    print(f'Тестова статистика:  {result[0]:.4f}')
    print(f'p-value:             {result[1]:.6f}')
    print(f'Кількість лагів:    {result[2]}')
    print(f'Кількість спостережень: {result[3]}')
    print('Критичні значення:')
    for key, value in result[4].items():
        print(f'   {key}: {value:.4f}')

    if result[1] <= 0.05:
        print('► Висновок: ряд СТАЦІОНАРНИЙ (p ≤ 0.05)\n')
    else:
        print('► Висновок: ряд НЕСТАЦІОНАРНИЙ (p > 0.05)\n')

    return result[1]

# Перевірка оригінального ряду
p_orig = adf_test(df['passengers'], 'Оригінальний ряд')

# Перше диференціювання (d=1)
df['diff_1'] = df['passengers'].diff()
p_diff1 = adf_test(df['diff_1'], 'Після 1-го диференціювання')

# Сезонне диференціювання (lag=12)
df['diff_seasonal'] = df['passengers'].diff(12)
p_diff_s = adf_test(df['diff_seasonal'], 'Після сезонного диференціювання (lag=12)')

# Комбіноване: звичайне + сезонне диференціювання
df['diff_both'] = df['passengers'].diff().diff(12)
p_diff_both = adf_test(df['diff_both'], 'Після звичайного + сезонного диференціювання')

Зазвичай для цього датасету оригінальний ряд виявляється нестаціонарним (p-value суттєво більше 0.05, що нікого не дивує). Після першого диференціювання (d=1) ряд стає стаціонарним — отже, параметр d=1 для нашої ARIMA. Якщо раптом потрібні два диференціювання — ставте d=2, але чесно кажучи, на практиці d рідко буває більше 2.

Визначення параметрів моделі: ACF і PACF

Автокореляційна функція (ACF) та часткова автокореляційна функція (PACF) — це ваші основні інструменти для підбору параметрів p та q:

  • ACF (Autocorrelation Function) — показує кореляцію ряду із самим собою на різних лагах. Допомагає визначити параметр q (порядок ковзного середнього, MA)
  • PACF (Partial Autocorrelation Function) — показує «чисту» кореляцію на конкретному лагу, без впливу проміжних. Допомагає визначити параметр p (порядок авторегресії, AR)
# Побудова ACF і PACF для диференційованого ряду
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# ACF — для визначення q
plot_acf(df['diff_1'].dropna(), lags=40, ax=axes[0], title='ACF (після 1-го диференціювання)')
axes[0].set_xlabel('Лаг')
axes[0].set_ylabel('Автокореляція')

# PACF — для визначення p
plot_pacf(df['diff_1'].dropna(), lags=40, ax=axes[1], title='PACF (після 1-го диференціювання)', method='ywm')
axes[1].set_xlabel('Лаг')
axes[1].set_ylabel('Часткова автокореляція')

plt.tight_layout()
plt.savefig('acf_pacf.png', dpi=150)
plt.show()

Ось як читати ці графіки (зручна шпаргалка, яку варто тримати під рукою):

Модель Поведінка ACF Поведінка PACF
AR(p) Поступове затухання Різкий обрив після лагу p
MA(q) Різкий обрив після лагу q Поступове затухання
ARMA(p,q) Поступове затухання Поступове затухання

Якщо, скажімо, PACF різко обривається після лагу 2 — значить p=2. ACF обривається після лагу 1 — значить q=1. Для нашого ряду непоганими стартовими значеннями будуть p=1 або p=2, d=1, q=1. Але не переживайте надто — далі ми покажемо, як підібрати параметри автоматично.

Побудова моделі ARIMA

ARIMA (AutoRegressive Integrated Moving Average) складається з трьох частин: AR(p) — авторегресія порядку p, I(d) — інтеграція (диференціювання) порядку d, та MA(q) — ковзне середнє порядку q. Записується як ARIMA(p, d, q). Нічого надприродного, коли розберешся.

# Побудова моделі ARIMA(1, 1, 1)
model_arima = ARIMA(df['passengers'], order=(1, 1, 1))
result_arima = model_arima.fit()

# Виведення детального резюме моделі
print(result_arima.summary())

# Прогнозування на навчальних даних (in-sample)
df['arima_fitted'] = result_arima.fittedvalues

# Прогнозування на 24 місяці вперед (out-of-sample)
forecast_steps = 24
forecast_arima = result_arima.forecast(steps=forecast_steps)

# Візуалізація: факт vs прогноз
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(df.index, df['passengers'], label='Фактичні дані', color='steelblue')
ax.plot(df.index, df['arima_fitted'], label='ARIMA (підгонка)', color='orange', alpha=0.7)

# Прогноз на майбутнє
future_index = pd.date_range(start=df.index[-1] + pd.DateOffset(months=1),
                             periods=forecast_steps, freq='MS')
ax.plot(future_index, forecast_arima, label='Прогноз ARIMA', color='red', linestyle='--')

ax.set_title('ARIMA(1,1,1) — Факт vs Прогноз')
ax.set_ylabel('Пасажири (тис.)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('arima_forecast.png', dpi=150)
plt.show()

# Оцінка якості
from sklearn.metrics import mean_squared_error
rmse = np.sqrt(mean_squared_error(df['passengers'], df['arima_fitted']))
print(f'\nAIC: {result_arima.aic:.2f}')
print(f'BIC: {result_arima.bic:.2f}')
print(f'RMSE (in-sample): {rmse:.2f}')

Зверніть увагу на AIC (Akaike Information Criterion) та BIC (Bayesian Information Criterion). Менші значення — краща модель. Ці метрики балансують між якістю підгонки та складністю моделі, що допомагає уникнути перенавчання. Коли порівнюєте кілька варіантів ARIMA з різними параметрами, AIC — ваш найкращий друг.

Сезонна модель SARIMA: врахування сезонності

Базова ARIMA про сезонність нічого не знає. А для рядів із яскравою сезонністю (як наші авіапасажири) це критично. Тут на сцену виходить SARIMA — сезонне розширення ARIMA з додатковими параметрами (P, D, Q, m):

  • P — порядок сезонної авторегресії
  • D — порядок сезонного диференціювання
  • Q — порядок сезонного ковзного середнього
  • m — довжина сезонного циклу (12 для місячних даних, 4 для квартальних, 7 для щоденних із тижневою сезонністю)

У statsmodels SARIMA реалізована через клас SARIMAX. Так, назва трохи заплутує — але без екзогенних змінних вона працює саме як SARIMA:

# Побудова моделі SARIMA(1,1,1)(1,1,1,12)
model_sarima = SARIMAX(df['passengers'],
                       order=(1, 1, 1),
                       seasonal_order=(1, 1, 1, 12),
                       enforce_stationarity=False,
                       enforce_invertibility=False)

result_sarima = model_sarima.fit(disp=False)
print(result_sarima.summary())

# Прогнозування
df['sarima_fitted'] = result_sarima.fittedvalues

forecast_sarima = result_sarima.forecast(steps=24)

# Порівняння ARIMA vs SARIMA
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(df.index, df['passengers'], label='Фактичні дані', color='steelblue', linewidth=1.5)
ax.plot(df.index, df['arima_fitted'], label='ARIMA(1,1,1)', color='orange', alpha=0.6)
ax.plot(df.index, df['sarima_fitted'], label='SARIMA(1,1,1)(1,1,1,12)', color='green', alpha=0.8)

ax.set_title('Порівняння ARIMA та SARIMA')
ax.set_ylabel('Пасажири (тис.)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('arima_vs_sarima.png', dpi=150)
plt.show()

# Порівняння метрик
rmse_arima = np.sqrt(mean_squared_error(df['passengers'], df['arima_fitted']))
rmse_sarima = np.sqrt(mean_squared_error(df['passengers'], df['sarima_fitted']))

print(f'\n{"Метрика":<20} {"ARIMA(1,1,1)":<20} {"SARIMA(1,1,1)(1,1,1,12)":<25}')
print(f'{"AIC":<20} {result_arima.aic:<20.2f} {result_sarima.aic:<25.2f}')
print(f'{"BIC":<20} {result_arima.bic:<20.2f} {result_sarima.bic:<25.2f}')
print(f'{"RMSE":<20} {rmse_arima:<20.2f} {rmse_sarima:<25.2f}')

Різниця між ARIMA і SARIMA на сезонних даних зазвичай вражає. SARIMA виграє з великим відривом. Ось кілька практичних порад по параметрах:

  • Параметр D (сезонне диференціювання) майже ніколи не перевищує 1
  • Сума d + D ≤ 2 — більше диференціювань може «вбити» корисні патерни в даних
  • Для місячних даних m=12, для квартальних — m=4, для щоденних із тижневою сезонністю — m=7
  • Починайте з простих параметрів (P=1, D=1, Q=1) і ускладнюйте лише за потреби

Автоматичний підбір параметрів за допомогою pmdarima

Ручний підбір через ACF/PACF — чудова вправа, щоб зрозуміти, як модель працює «під капотом». Але давайте будемо чесними: на практиці (особливо коли рядів багато) набагато зручніше автоматизувати цей процес. Бібліотека pmdarima має функцію auto_arima(), яка перебирає комбінації параметрів і обирає найкращу за AIC або BIC:

# Автоматичний підбір параметрів SARIMA
auto_model = auto_arima(
    df['passengers'],
    seasonal=True,          # увімкнути сезонну компоненту
    m=12,                   # довжина сезонного циклу
    d=None,                 # автоматично визначити d
    D=None,                 # автоматично визначити D
    start_p=0, max_p=3,     # діапазон для p
    start_q=0, max_q=3,     # діапазон для q
    start_P=0, max_P=2,     # діапазон для P
    start_Q=0, max_Q=2,     # діапазон для Q
    information_criterion='aic',  # критерій вибору
    stepwise=True,          # покроковий пошук (швидший)
    suppress_warnings=True,
    trace=True              # показати процес перебору
)

print('\n=== Найкраща модель ===')
print(auto_model.summary())

print(f'\nОбрані параметри:')
print(f'  order: {auto_model.order}')
print(f'  seasonal_order: {auto_model.seasonal_order}')
print(f'  AIC: {auto_model.aic():.2f}')

Параметр stepwise=True задіює алгоритм Хіндмана — Хандакара, і це реально прискорює процес. Замість тупого перебору всіх комбінацій (grid search), алгоритм розумно звужує пошук — починає з простих моделей і поступово ускладнює. На великих даних різниця в швидкості може бути в рази.

# Прогноз за допомогою auto_arima
n_periods = 24
forecast_auto, conf_int = auto_model.predict(
    n_periods=n_periods,
    return_conf_int=True,
    alpha=0.05  # 95% довірчий інтервал
)

future_index = pd.date_range(start=df.index[-1] + pd.DateOffset(months=1),
                             periods=n_periods, freq='MS')

# Візуалізація
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(df.index, df['passengers'], label='Фактичні дані', color='steelblue')
ax.plot(future_index, forecast_auto, label=f'Прогноз {auto_model.order}x{auto_model.seasonal_order}',
        color='red', linewidth=2)
ax.fill_between(future_index, conf_int[:, 0], conf_int[:, 1],
                alpha=0.15, color='red', label='95% довірчий інтервал')
ax.set_title('Прогноз auto_arima з довірчим інтервалом')
ax.set_ylabel('Пасажири (тис.)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('auto_arima_forecast.png', dpi=150)
plt.show()

Для наочності — порівняння підходів:

Аспект Ручний підбір (ACF/PACF) auto_arima
Швидкість Повільніший, потребує аналізу Автоматичний, за секунди
Розуміння даних Глибше розуміння структури Менше контролю
Ризик помилки Залежить від досвіду Мінімальний
Найкраще застосування Навчання, дослідження Виробництво, масові прогнози

Екзогенні змінні: від SARIMA до SARIMAX

Літера X у SARIMAX означає «eXogenous» — зовнішні змінні. Це додаткові фактори, які впливають на ряд, але не є його частиною. Наприклад, при прогнозуванні продажів це може бути ціна конкурентів, витрати на рекламу або якийсь макроекономічний індекс. По суті, це спосіб сказати моделі: «Гей, ось ще інформація, яка може допомогти».

# Створення синтетичних екзогенних змінних
np.random.seed(42)
n = len(df)

exog = pd.DataFrame({
    # Економічний індикатор (наприклад, індекс споживчих настроїв)
    'economic_index': np.linspace(80, 120, n) + np.random.normal(0, 5, n),
    # Маркетингові витрати (тис. дол.)
    'marketing_spend': np.random.uniform(10, 50, n)
}, index=df.index)

print("Екзогенні змінні:")
print(exog.head())

# Побудова моделі SARIMAX з екзогенними змінними
model_sarimax = SARIMAX(
    df['passengers'],
    exog=exog,
    order=(1, 1, 1),
    seasonal_order=(1, 1, 1, 12),
    enforce_stationarity=False,
    enforce_invertibility=False
)

result_sarimax = model_sarimax.fit(disp=False)
print(result_sarimax.summary())

Критично важливий момент (і це ловить багатьох новачків): для прогнозування з SARIMAX вам потрібні значення екзогенних змінних на весь період прогнозу. Прогнозуєте продажі на 6 місяців вперед із маркетинговим бюджетом як змінною? Тоді вам потрібно знати (або хоча б приблизно оцінити) цей бюджет на ці 6 місяців наперед.

# Створення екзогенних змінних для періоду прогнозу
forecast_periods = 24
exog_forecast = pd.DataFrame({
    'economic_index': np.linspace(120, 140, forecast_periods) + np.random.normal(0, 3, forecast_periods),
    'marketing_spend': np.random.uniform(30, 60, forecast_periods)
}, index=pd.date_range(start=df.index[-1] + pd.DateOffset(months=1),
                       periods=forecast_periods, freq='MS'))

# Прогнозування з екзогенними змінними
forecast_sarimax = result_sarimax.forecast(steps=forecast_periods, exog=exog_forecast)

# Візуалізація
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(df.index, df['passengers'], label='Фактичні дані', color='steelblue')
ax.plot(exog_forecast.index, forecast_sarimax, label='Прогноз SARIMAX',
        color='darkred', linewidth=2, linestyle='--')
ax.set_title('Прогноз SARIMAX з екзогенними змінними')
ax.set_ylabel('Пасажири (тис.)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('sarimax_forecast.png', dpi=150)
plt.show()

Діагностика моделі та аналіз залишків

Побудувати модель — це тільки половина справи. Потрібно ще переконатися, що вона адекватно описує дані. Головний інструмент тут — аналіз залишків (residuals). Ідея проста: якщо модель гарна, залишки мають бути випадковим білим шумом — нормально розподіленим, без автокореляції, із нульовим середнім.

# Діагностичні графіки (вбудований метод statsmodels)
fig = result_sarima.plot_diagnostics(figsize=(14, 10))
fig.suptitle('Діагностика моделі SARIMA', fontsize=14, y=1.02)
plt.tight_layout()
plt.savefig('diagnostics.png', dpi=150)
plt.show()

Метод plot_diagnostics() будує чотири графіки, і кожен розповідає свою історію:

  • Standardized Residuals — залишки мають коливатися навколо нуля без помітних патернів
  • Histogram + KDE — розподіл залишків має нагадувати нормальний (гарний «дзвін»)
  • Q-Q Plot — точки повинні лежати приблизно вздовж діагоналі
  • Correlogram (ACF) — автокореляції залишків не повинні виходити за довірчі межі
# Тест Люнга — Бокса на автокореляцію залишків
residuals = result_sarima.resid

lb_test = acorr_ljungbox(residuals, lags=[10, 20, 30], return_df=True)
print("Тест Люнга — Бокса:")
print(lb_test)
print()

# Якщо p-value > 0.05 для всіх лагів — залишки не мають
# значущої автокореляції (це добре!)

# Додаткова перевірка: середнє залишків
print(f'Середнє залишків: {residuals.mean():.4f}')
print(f'Стандартне відхилення: {residuals.std():.4f}')

# Тест Шапіро — Вілка на нормальність залишків
from scipy.stats import shapiro
stat, p_shapiro = shapiro(residuals)
print(f'Тест Шапіро — Вілка: statistic={stat:.4f}, p-value={p_shapiro:.4f}')
if p_shapiro > 0.05:
    print('► Залишки нормально розподілені')
else:
    print('► Залишки НЕ нормально розподілені (але для великих вибірок це прийнятно)')

Що вважати хорошою діагностикою? Залишки без автокореляції (тест Люнга — Бокса з p > 0.05), розподіл більш-менш нормальний, ніяких видимих патернів на графіку. Якщо щось не так — можна спробувати змінити параметри або додати екзогенні змінні. Іноді допомагає логарифмічне перетворення ряду.

Прогнозування та оцінка якості

Ну й нарешті — найцікавіше: будуємо прогноз і дивимося, наскільки він точний. Для чесної оцінки обов'язково розділяйте дані на навчальну та тестову вибірки. Інакше ви просто обманюєте себе красивими метриками.

# Розділення на навчальну та тестову вибірки
train_size = int(len(df) * 0.8)
train = df['passengers'][:train_size]
test = df['passengers'][train_size:]

print(f'Навчальна вибірка: {len(train)} спостережень ({train.index.min()} — {train.index.max()})')
print(f'Тестова вибірка: {len(test)} спостережень ({test.index.min()} — {test.index.max()})')

# Побудова моделі на навчальних даних
model_eval = SARIMAX(train,
                     order=(1, 1, 1),
                     seasonal_order=(1, 1, 1, 12),
                     enforce_stationarity=False,
                     enforce_invertibility=False)
result_eval = model_eval.fit(disp=False)

# Прогнозування на тестовий період
forecast_obj = result_eval.get_forecast(steps=len(test))
forecast_values = forecast_obj.predicted_mean
conf_int = forecast_obj.conf_int(alpha=0.05)

# Метрики якості
mae = mean_absolute_error(test, forecast_values)
rmse = np.sqrt(mean_squared_error(test, forecast_values))
mape = np.mean(np.abs((test - forecast_values) / test)) * 100

print(f'\n=== Метрики якості на тестовій вибірці ===')
print(f'MAE  (середня абсолютна помилка):     {mae:.2f}')
print(f'RMSE (корінь середньоквадратичної):    {rmse:.2f}')
print(f'MAPE (середня абс. відносна помилка):  {mape:.2f}%')

Коротко про метрики (бо їх часто плутають):

Метрика Опис Коли використовувати
MAE Середня абсолютна помилка в одиницях ряду Коли важлива інтерпретованість
RMSE Більше штрафує за великі помилки Коли великі помилки критичні
MAPE Відсоткова помилка, не залежить від масштабу Для порівняння між різними рядами
# Візуалізація прогнозу з довірчими інтервалами
fig, ax = plt.subplots(figsize=(14, 7))

# Навчальні дані
ax.plot(train.index, train, label='Навчальні дані', color='steelblue')

# Тестові дані (факт)
ax.plot(test.index, test, label='Фактичні (тест)', color='darkblue', linewidth=2)

# Прогноз
ax.plot(test.index, forecast_values, label='Прогноз SARIMA', color='red', linewidth=2, linestyle='--')

# Довірчий інтервал
ax.fill_between(test.index,
                conf_int.iloc[:, 0],
                conf_int.iloc[:, 1],
                alpha=0.15, color='red', label='95% довірчий інтервал')

ax.set_title(f'Прогноз SARIMA на тестовій вибірці (MAPE={mape:.1f}%)', fontsize=14)
ax.set_ylabel('Пасажири (тис.)')
ax.legend(loc='upper left')
ax.grid(True, alpha=0.3)

# Додаємо вертикальну лінію розділення
ax.axvline(x=train.index[-1], color='gray', linestyle=':', alpha=0.5)
ax.text(train.index[-1], ax.get_ylim()[1] * 0.95, '  Межа train/test',
        fontsize=10, color='gray')

plt.tight_layout()
plt.savefig('forecast_evaluation.png', dpi=150)
plt.show()

Для ще надійнішої оцінки існує ковзна крос-валідація (rolling forecast). Суть проста: модель послідовно навчається на зростаючому вікні даних і прогнозує один крок вперед. Це максимально наближено до того, як модель працюватиме «в бою» — у реальному виробничому середовищі:

# Ковзний прогноз (rolling forecast)
history = list(train)
predictions = []

for t in range(len(test)):
    model_rolling = SARIMAX(history,
                            order=(1, 1, 1),
                            seasonal_order=(1, 1, 1, 12),
                            enforce_stationarity=False,
                            enforce_invertibility=False)
    result_rolling = model_rolling.fit(disp=False)
    yhat = result_rolling.forecast(steps=1)[0]
    predictions.append(yhat)
    history.append(test.iloc[t])

rmse_rolling = np.sqrt(mean_squared_error(test, predictions))
mape_rolling = np.mean(np.abs((test - predictions) / test)) * 100
print(f'Ковзний прогноз — RMSE: {rmse_rolling:.2f}, MAPE: {mape_rolling:.2f}%')

Поради та типові помилки

Працюючи з часовими рядами, з часом набираєшся досвіду — інколи через власні помилки (так, було й таке). Ось найважливіше, що варто пам'ятати:

  • Не перебільшуйте з диференціюванням. Якщо d + D більше 2, ви, ймовірно, перестаралися. Модель «загладить» дані до невпізнаваності і втратить корисні патерни. Перевіряйте стаціонарність тестом ADF після кожного диференціювання
  • Починайте з простих моделей. ARIMA(1,1,1) або SARIMA(1,1,1)(1,1,1,m) часто дають цілком непогані результати. Ускладнюйте лише коли діагностика прямо вказує на проблеми
  • Перевіряйте наявність сезонності перед вибором моделі. Погляньте на ACF з великою кількістю лагів або скористайтеся сезонною декомпозицією (seasonal_decompose()). Немає сезонності? Тоді ARIMA вистачить — SARIMA лише додасть зайвої складності
  • Завжди валідуйте на відкладених даних. Метрики на навчальній вибірці завжди виглядають краще, ніж є насправді. Тільки train/test split або ковзна крос-валідація покажуть правду
  • Встановлюйте частоту індексу. Statsmodels хоче бачити явно вказану частоту в DatetimeIndex (df.index.freq = 'MS'). Без цього починаються дивні речі — прогноз може мовчки видавати мусор
  • Обробляйте пропущені значення. ARIMA не дружить із NaN. Заповніть пропуски інтерполяцією або ковзним середнім до того, як подавати дані в модель
  • Розгляньте логарифмічне перетворення. Якщо амплітуда сезонних коливань зростає разом із рівнем ряду (мультиплікативна сезонність), спробуйте np.log() перед моделюванням та np.exp() для зворотного перетворення прогнозу. Часто це суттєво покращує результат
  • Для складних патернів — дивіться ширше. Prophet (від Meta) добре справляється з множинною сезонністю та святковими ефектами. XGBoost і LightGBM — коли є багато екзогенних змінних. LSTM та Transformer-моделі можуть захопити складні нелінійні залежності, але їм потрібно значно більше даних (і терпіння)

FAQ — Часті запитання

Яка різниця між ARIMA та SARIMA?

ARIMA(p,d,q) працює з трендом та автокореляцією, але сезонність для неї — темний ліс. SARIMA додає сезонні параметри (P,D,Q,m), які дозволяють моделювати повторювані цикли: щорічні піки продажів, щотижневе навантаження на сервери тощо. Якщо на вашому ACF видно регулярні піки на лагах m, 2m, 3m — це сигнал, що потрібна SARIMA. Якщо ж сезонності немає, базова ARIMA буде простішою і швидшою, і немає сенсу ускладнювати.

Як обрати правильні параметри p, d, q?

Параметр d визначається тестом ADF: диференціюєте ряд, поки він не стане стаціонарним (p-value < 0.05). Зазвичай це 0, 1 або максимум 2. Параметр p читається з графіку PACF — шукайте лаг, після якого PACF різко падає до нуля. Параметр q — аналогічно, але з ACF. А якщо чесно, найпрактичніший підхід — просто запустити auto_arima() з pmdarima. Вона сама все підбере за AIC. Ручний аналіз ACF/PACF корисний для розуміння, але для фінального рішення автоматика зазвичай працює краще.

Чи можна використовувати ARIMA для нестаціонарних даних?

Так, і саме для цього існує буква I (Integrated) у назві. Параметр d автоматично застосовує диференціювання, щоб привести ряд до стаціонарності. При d=1 модель обчислює різниці між сусідніми значеннями і будує AR+MA вже на них. Аналогічно D у SARIMA виконує сезонне диференціювання. Але є нюанс: якщо ряд має структурні зломи (різка зміна тренду через кризу чи зміну стратегії), одна лише ARIMA може не впоратися. У таких випадках варто дивитися в бік SARIMAX із зовнішніми регресорами або методів виявлення точок зламу (changepoint detection).

Що краще: ARIMA чи Prophet?

Залежить від ситуації — універсальної відповіді немає. ARIMA/SARIMA добре працює для коротких і середніх рядів з одним сезонним патерном, коли вам важливий контроль і інтерпретованість. Prophet зручніший, коли є множинна сезонність (тижнева + річна), свята, пропуски в даних або структурні зломи — він усе це «їсть» автоматично. Для початківців Prophet, мабуть, простіший у освоєнні. Але для серйозних проєктів, де потрібна максимальна точність, протестуйте обидва підходи і порівняйте метрики на тестовій вибірці. І так, варто також глянути на NeuralProphet — він поєднує ідеї Prophet із нейромережами.

Скільки даних потрібно для побудови моделі ARIMA?

Орієнтир такий: мінімум 50 спостережень для звичайної ARIMA і хоча б 2 повних сезонних цикли для SARIMA. Для місячних даних із річною сезонністю (m=12) це щонайменше 24-36 місяців. В ідеалі — 4-5 циклів (48-60 місяців), тоді модель зможе надійно оцінити сезонні параметри.

Занадто мало даних — отримаєте нестабільні оцінки та широкі довірчі інтервали (по суті, модель гадає на кавовій гущі). Але й тримати дуже старі дані не завжди корисно: якщо структура ряду змінилася з часом, давні спостереження можуть більше заважати, ніж допомагати. У такому разі обмежтеся останніми 5-7 роками або використовуйте ковзне вікно.

Про Автора Editorial Team

Our team of expert writers and editors.