Временные ряды реальных биржевых котировок представляют собой настоящий вызов для любого аналитика. Традиционные подходы к сглаживанию таких рядов часто дают посредственные результаты, особенно когда речь идет о нелинейных зависимостях и сложных паттернах поведения активов. Полиномиальные регрессии открывают совершенно иные возможности для понимания структуры данных и выявления скрытых закономерностей, которые ускользают от стандартных методов.
Основная проблема большинства аналитиков заключается в том, что они пытаются применять линейные модели к принципиально нелинейным процессам. Динамика большинства биржевых активов редко следует прямолинейным трендам — она полна изгибов, ускорений, замедлений и резких поворотов. Именно здесь полиномиальные регрессии показывают свою истинную силу, позволяя захватывать эти сложные нелинейные отношения с математической точностью.
Что такое полиномиальная регрессия и почему она превосходит линейные методы
Полиномиальная регрессия представляет собой форму регрессионного анализа, где отношения между независимой переменной x и зависимой переменной y моделируются как полином n-й степени. В контексте временных рядов это означает, что мы можем описать сложные нелинейные паттерны, используя степенные функции времени.
Математически полиномиальная регрессия выглядит как:
y = β₀ + β₁x + β₂x² + β₃x³ + … + βₙxⁿ + ε
- где β₀, β₁, …, βₙ — коэффициенты полинома;
- ε — случайная ошибка.
Ключевое преимущество полиномиальных регрессий перед линейными методами заключается в их способности адаптироваться к локальным изменениям в данных. Если линейная регрессия может показать только общий тренд, то полиномиальная регрессия способна выявить периоды ускорения и замедления, точки перегиба и другие важные характеристики временного ряда.
Это важное преимущество при анализе динамики биржевых активов, где цикличность и нелинейность являются неотъемлемыми характеристиками. Представьте цену акции, которая растет с ускорением в начале бычьего рынка, затем замедляется по мере приближения к уровням сопротивления, и наконец начинает снижаться. Линейная регрессия покажет лишь усредненный тренд, в то время как полиномиальная регрессия 3-й степени точно опишет всю траекторию движения цены.
Математические основы полиномиального сглаживания
Процесс полиномиального сглаживания базируется на методе наименьших квадратов, но с существенными модификациями для работы с временными рядами. Так как классический подход часто дает неудовлетворительные результаты из-за гетероскедастичности и автокорреляции остатков.
Основная идея заключается в минимизации функции потерь:
L(β) = Σᵢ(yᵢ — f(xᵢ; β))²
где f(xᵢ; β) — полиномиальная функция с параметрами β.
Однако, если мы говорим о построении полиномов для временных рядов, то эта формула требует модификации. Временная зависимость между наблюдениями означает, что мы не можем просто применить стандартный OLS (Ordinary Least Squares). Вместо этого используется взвешенный метод наименьших квадратов (WLS) или обобщенный метод наименьших квадратов (GLS), которые учитывают автокорреляционную структуру данных.
Для практического применения критически важно правильно выбрать степень полинома. Слишком низкая степень приведет к недообучению (underfitting), когда модель не сможет захватить важные паттерны. Слишком высокая степень вызовет переобучение (overfitting), когда модель будет следовать каждому случайному колебанию в данных.
Выбор оптимальной степени полинома
Определение оптимальной степени полинома — это баланс между точностью аппроксимации и устойчивостью модели. В своей практике я использую несколько критериев одновременно:
- Информационные критерии: AIC (Akaike Information Criterion) и BIC (Bayesian Information Criterion) помогают найти компромисс между качеством подгонки и сложностью модели;
- Кросс-валидация: разделение данных на обучающую и валидационную выборки позволяет оценить способность модели к генерализации;
- Анализ остатков: изучение структуры остатков помогает выявить недомоделированные паттерны или признаки переобучения.
Практический опыт показывает, что для большинства финансовых временных рядов оптимальная степень полинома находится в диапазоне от 2 до 6. Полиномы более высоких степеней часто приводят к численной нестабильности и нереалистичным экстраполяциям.
Типы полиномиальных регрессий для временных рядов
Простая полиномиальная регрессия
Простая полиномиальная регрессия использует время как единственную независимую переменную. Это наиболее прямолинейный подход, который хорошо работает для данных с четко выраженным трендом и ограниченным количеством структурных изломов.
Математическая формула для простой полиномиальной регрессии n-й степени:
y(t) = α₀ + α₁t + α₂t² + α₃t³ + … + αₙtⁿ + ε(t)
- где t — временной индекс,
- α — коэффициенты модели,
- ε(t) — случайная компонента.
Главное преимущество простой полиномиальной регрессии — ее интерпретируемость. Коэффициенты модели имеют четкий экономический смысл: α₁ характеризует линейный тренд, α₂ — квадратичную компоненту (ускорение/замедление), α₃ — кубическую компоненту (изменение кривизны) и так далее.
Давайте посмотрим как можно реализовать такую модель в коде:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_squared_error, r2_score
import warnings
warnings.filterwarnings('ignore')
# Генерация синтетического временного ряда с нелинейным трендом
# Параметры
np.random.seed(24)
n_points = 150 # Общее количество точек
change_points = [0, 45, 90, n_points] # Точки изменения тренда (в пределах 0–150)
trends = [0.1, -0.15, 0.2] # Наклоны трендов для каждого сегмента
noise_std = 1.5 # Стандартное отклонение шума
# Генерация ряда с изменением направления
time = np.arange(n_points)
series = np.zeros(n_points)
for i in range(len(change_points) - 1):
start = change_points[i]
end = change_points[i + 1]
slope = trends[i]
base_trend = slope * (time[start:end] - start)
noise = np.random.normal(0, noise_std, end - start)
series[start:end] = base_trend + noise
# Создаем датафрейм для хранения значений
data = pd.DataFrame({'time': time, 'value': series})
# Функция для создания полиномиальных моделей разных степеней
def create_polynomial_models(X, y, max_degree=6):
models = {}
scores = {}
for degree in range(1, max_degree + 1):
# Создание pipeline с полиномиальными признаками
poly_model = Pipeline([
('poly', PolynomialFeatures(degree=degree, include_bias=False)),
('linear', LinearRegression())
])
# Обучение модели
poly_model.fit(X.reshape(-1, 1), y)
# Прогноз
y_pred = poly_model.predict(X.reshape(-1, 1))
# Оценка качества
mse = mean_squared_error(y, y_pred)
r2 = r2_score(y, y_pred)
models[degree] = poly_model
scores[degree] = {'MSE': mse, 'R2': r2, 'predictions': y_pred}
return models, scores
# Создание и оценка моделей
models, scores = create_polynomial_models(data['time'].values, data['value'].values)
# Визуализация временного ряда и прогнозов моделей
plt.figure(figsize=(14, 8))
plt.scatter(data['time'], data['value'], color='gray', label='Исходные данные', alpha=0.7)
# Цветовая палитра
colors = plt.cm.viridis(np.linspace(0, 1, len(models)))
for idx, degree in enumerate(sorted(models.keys())):
model = models[degree]
y_pred = scores[degree]['predictions']
plt.plot(data['time'], y_pred, color=colors[idx], linewidth=2, label=f'Полином {degree} степени')
# Настройки графика
plt.title('Сравнение полиномиальных моделей')
plt.xlabel('Время')
plt.ylabel('Значение ряда')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
# Вывод результатов
print("Сравнение полиномиальных моделей:")
print("-" * 40)
for degree in sorted(scores.keys()):
print(f"Степень {degree}: MSE = {scores[degree]['MSE']:.2f}, R² = {scores[degree]['R2']:.3f}")
Рис. 1: Сравнение полиномиальных моделей с разной степенью полинома
Сравнение полиномиальных моделей:
—————————————-
Степень 1: MSE = 18.41, R² = 0.210
Степень 2: MSE = 9.20, R² = 0.605
Степень 3: MSE = 8.22, R² = 0.647
Степень 4: MSE = 4.66, R² = 0.800
Степень 5: MSE = 4.65, R² = 0.801
Степень 6: MSE = 3.54, R² = 0.848
Этот код демонстрирует, как разные степени полиномов справляются с аппроксимацией сложного временного ряда. Результаты показывают, что модели более высоких степеней дают лучшее качество подгонки (более высокий R² и низкий MSE), но это не всегда означает лучшую практическую применимость.
Важно отметить, что простая полиномиальная регрессия имеет ограничения при работе с данными, содержащими множественные циклы или структурные изломы. В таких случаях модель может давать неестественно высокие или низкие значения при экстраполяции, что особенно критично при прогнозировании финансовых временных рядов.
Сегментированная полиномиальная регрессия
Сегментированная полиномиальная регрессия (spline regression) представляет собой значительно более мощный инструмент для работы со сложными временными рядами. Основная идея заключается в разбиении временного ряда на сегменты и применении полиномиальных функций различных степеней к каждому сегменту с обеспечением гладкости в точках соединения (knot points).
Математически сегментированная полиномиальная регрессия описывается системой функций:
y(t) = {
P₁(t), если t ∈ [t₀, t₁]
P₂(t), если t ∈ [t₁, t₂]
…
Pₖ(t), если t ∈ [tₖ₋₁, tₖ]
}
где Pᵢ(t) — полиномы степени n для каждого сегмента, с условиями непрерывности в точках соединения.
Преимущество сегментированного подхода особенно ярко проявляется при анализе данных с изменяющимся поведением во времени. Например, при анализе цен акций, когда периоды бычьих и медвежьих рынков требуют различных полиномиальных аппроксимаций. Сегментированная регрессия позволяет автоматически адаптироваться к этим изменениям.
from scipy.interpolate import UnivariateSpline
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import mean_squared_error
import numpy as np
import matplotlib.pyplot as plt
class SegmentedPolynomialRegression:
def __init__(self, n_knots=5, degree=3, smoothing_factor=None):
self.n_knots = n_knots
self.degree = degree
self.smoothing_factor = smoothing_factor
self.spline = None
self.knots = None
def fit(self, X, y):
"""Обучение сегментированной полиномиальной модели"""
# Автоматическое определение точек излома (knots)
if self.knots is None:
self.knots = np.linspace(X.min(), X.max(), self.n_knots + 2)[1:-1]
# Создание B-spline
if self.smoothing_factor is None:
# Автоматический выбор параметра сглаживания
self.smoothing_factor = len(X) * np.var(y) * 0.1
self.spline = UnivariateSpline(X, y, k=self.degree, s=self.smoothing_factor)
return self
def predict(self, X):
"""Прогнозирование значений"""
if self.spline is None:
raise ValueError("Модель не обучена. Вызовите fit() сначала.")
return self.spline(X)
def get_derivatives(self, X, order=1):
"""Получение производных заданного порядка"""
return self.spline.derivative(order)(X)
def get_knots(self):
"""Получение точек излома"""
return self.spline.get_knots()
# Генерация временного ряда с несколькими режимами
np.random.seed(123)
t = np.linspace(0, 15, 200)
# Создание данных с тремя различными режимами
y_complex = np.zeros_like(t)
# Режим 1: квадратичный рост (0-5)
mask1 = t <= 5 y_complex[mask1] = 0.3 * t[mask1]**2 + np.random.normal(0, 1, np.sum(mask1)) # Режим 2: линейное снижение (5-10) mask2 = (t > 5) & (t <= 10) y_complex[mask2] = 7.5 - 0.8 * (t[mask2] - 5) + np.random.normal(0, 1, np.sum(mask2)) # Режим 3: экспоненциальный рост (10-15) mask3 = t > 10
y_complex[mask3] = 3.5 + 0.5 * np.exp(0.3 * (t[mask3] - 10)) + np.random.normal(0, 1, np.sum(mask3))
# Сравнение простой и сегментированной полиномиальной регрессии
simple_poly = Pipeline([
('poly', PolynomialFeatures(degree=6)),
('linear', LinearRegression())
])
segmented_poly = SegmentedPolynomialRegression(n_knots=5, degree=3, smoothing_factor=150)
# Обучение моделей
simple_poly.fit(t.reshape(-1, 1), y_complex)
segmented_poly.fit(t, y_complex)
# Прогнозы
simple_pred = simple_poly.predict(t.reshape(-1, 1))
segmented_pred = segmented_poly.predict(t)
# Оценка качества
simple_mse = mean_squared_error(y_complex, simple_pred)
segmented_mse = mean_squared_error(y_complex, segmented_pred)
print(f"Простая полиномиальная регрессия - MSE: {simple_mse:.2f}")
print(f"Сегментированная регрессия - MSE: {segmented_mse:.2f}")
print(f"Улучшение качества: {((simple_mse - segmented_mse) / simple_mse * 100):.1f}%")
# Анализ производных для выявления точек изменения режимов
first_derivative = segmented_poly.get_derivatives(t, order=1)
second_derivative = segmented_poly.get_derivatives(t, order=2)
print(f"\nТочки излома модели: {segmented_poly.get_knots()}")
# Визуализации
# Исходный ряд и прогнозы
plt.figure(figsize=(14, 6))
plt.scatter(t, y_complex, label='Исходные данные', color='gray', alpha=0.6)
plt.plot(t, simple_pred, label='Простая полиномиальная регрессия (степень 6)', color='blue')
plt.plot(t, segmented_pred, label='Сегментированная регрессия (B-spline)', color='red', linewidth=2)
plt.title('Сравнение моделей регрессии')
plt.xlabel('Время')
plt.ylabel('Значение')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
# Первая и вторая производные
plt.figure(figsize=(14, 6))
plt.plot(t, first_derivative, label='Первая производная', color='green')
plt.plot(t, second_derivative, label='Вторая производная', color='purple')
plt.axhline(0, color='black', linestyle='--', alpha=0.5)
for knot in segmented_poly.get_knots():
plt.axvline(knot, color='orange', linestyle=':', alpha=0.7, label='Точка излома' if knot == segmented_poly.get_knots()[0] else "")
plt.title('Производные и точки излома')
plt.xlabel('Время')
plt.ylabel('Производная')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
Простая полиномиальная регрессия - MSE: 1.26
Сегментированная регрессия - MSE: 0.75
Улучшение качества: 40.4%
Точки излома модели: [ 0. 0.30150754 0.37688442 0.45226131 0.52763819 0.75376884
0.9798995 1.20603015 1.4321608 1.65829146 1.88442211 2.18592965
2.4120603 2.63819095 2.86432161 3.09045226 3.31658291 3.54271357
3.76884422 4.07035176 4.29648241 4.52261307 4.74874372 4.97487437
5.20100503 5.42713568 5.65326633 5.95477387 6.10552764 6.18090452
6.40703518 6.63316583 6.85929648 7.08542714 7.53768844 7.68844221
7.83919598 8.06532663 8.29145729 8.51758794 8.74371859 8.96984925
9.42211055 9.94974874 10.1758794 10.40201005 10.85427136 11.30653266
11.6080402 11.83417085 12.28643216 12.51256281 12.73869347 13.19095477
13.64321608 13.86934673 14.09547739 14.54773869 14.77386935 15. ]
Рис. 2: Сравнение простой полиномальной и сегментированной регрессии
Этот код демонстрирует, как сегментированная полиномиальная регрессия автоматически адаптируется к различным режимам в данных. Ключевое преимущество этого подхода — способность выявлять и моделировать структурные изломы во временных рядах без необходимости их априорного задания.
Сегментированный подход особенно эффективен при работе с финансовыми данными, где смена рыночных режимов является обычным явлением. Модель автоматически определяет точки перехода между различными состояниями рынка и применяет соответствующие полиномиальные аппроксимации.
Локальная полиномиальная регрессия (LOESS/LOWESS)
Локальная полиномиальная регрессия представляет собой непараметрический метод, который выполняет взвешенную полиномиальную регрессию в окрестности каждой точки. Этот подход сочетает гибкость нелинейного моделирования с устойчивостью к выбросам и локальным аномалиям.
Алгоритм LOESS (Locally Estimated Scatterplot Smoothing) работает следующим образом: для каждой точки xᵢ выбирается окрестность, содержащая фиксированную долю всех наблюдений (обычно от 20% до 80%). Затем в этой окрестности выполняется взвешенная полиномиальная регрессия, где веса убывают с расстоянием от центральной точки.
Математически процедура описывается как:
- Для точки xᵢ выбираем k ближайших соседей;
- Вычисляем веса w(xⱼ) = W(|xⱼ — xᵢ|/h), где h — bandwidth;
- Решаем взвешенную задачу минимизации: min Σⱼ w(xⱼ)(yⱼ — p(xⱼ))²;
- Вычисляем ŷᵢ = p(xᵢ).
Основное преимущество LOESS заключается в его способности адаптироваться к локальным особенностям данных без необходимости глобального задания функциональной формы. Это особенно важно при работе с финансовыми временными рядами, где локальная волатильность и структурные изменения могут значительно варьироваться.
from statsmodels.nonparametric.smoothers_lowess import lowess
from scipy.signal import savgol_filter
import numpy as np
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
import matplotlib.pyplot as plt
class LocalPolynomialRegression:
def __init__(self, frac=0.3, it=3, degree=2):
"""
frac: доля данных для локальной регрессии
it: количество итераций для робастности
degree: степень локального полинома
"""
self.frac = frac
self.it = it
self.degree = degree
self.fitted_values = None
def fit_predict(self, x, y):
"""Обучение и прогнозирование LOESS"""
# Применение LOWESS
smoothed = lowess(y, x, frac=self.frac, it=self.it, return_sorted=False)
self.fitted_values = smoothed
return smoothed
def cross_validate_bandwidth(self, x, y, frac_range=np.arange(0.1, 0.8, 0.1)):
"""Кросс-валидация для выбора оптимального bandwidth"""
best_frac = None
best_score = float('inf')
scores = {}
for frac in frac_range:
mse_scores = []
for i in range(0, len(x), max(1, len(x)//20)): # Сэмплирование для ускорения
# Исключаем i-ю точку
x_train = np.concatenate([x[:i], x[i+1:]])
y_train = np.concatenate([y[:i], y[i+1:]])
if len(x_train) > 5: # Минимальное количество точек для регрессии
try:
# Обучение на остальных данных
smoothed_train = lowess(y_train, x_train, frac=frac,
it=self.it, return_sorted=True)
# Интерполяция для тестовой точки
if x[i] >= x_train.min() and x[i] <= x_train.max():
y_pred = np.interp(x[i], smoothed_train[:, 0], smoothed_train[:, 1])
mse_scores.append((y[i] - y_pred)**2)
except:
continue
if mse_scores:
avg_mse = np.mean(mse_scores)
scores[frac] = avg_mse
if avg_mse < best_score:
best_score = avg_mse
best_frac = frac
return best_frac, scores
# Генерация данных с изменяющейся волатильностью и нелинейными паттернами
np.random.seed(456)
t = np.linspace(0, 20, 400)
# Создание сложного временного ряда
trend = 0.2 * t + 0.05 * t**2 - 0.001 * t**3
seasonal = 3 * np.sin(2 * np.pi * t / 5) + 1.5 * np.cos(2 * np.pi * t / 3)
volatility = 0.6 + 0.4 * np.sin(2 * np.pi * t / 10) # Изменяющаяся волатильность
noise = np.random.normal(0, volatility)
y_complex = trend + seasonal + noise
# Добавляем несколько выбросов
outlier_indices = np.random.choice(len(y_complex), size=20, replace=False)
y_complex[outlier_indices] += np.random.normal(0, 5, size=20)
# Визуализация исходных данных
plt.figure(figsize=(14, 6))
plt.plot(t, trend + seasonal, label='Истинный тренд + сезонность', color='black', linewidth=2)
plt.scatter(t, y_complex, label='Данные с шумом и выбросами', color='gray', s=20, alpha=0.7)
plt.title('Сгенерированный временной ряд')
plt.xlabel('Время')
plt.ylabel('Значение')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
# Применение различных методов сглаживания
loess_model = LocalPolynomialRegression()
# Автоматический выбор bandwidth
optimal_frac, cv_scores = loess_model.cross_validate_bandwidth(t, y_complex)
print(f"Оптимальная доля данных для LOESS: {optimal_frac:.2f}")
# Применение с оптимальными параметрами
loess_model.frac = optimal_frac
loess_smooth = loess_model.fit_predict(t, y_complex)
# Сравнение с Savitzky-Golay фильтром (альтернативный локальный метод)
savgol_smooth = savgol_filter(y_complex, window_length=51, polyorder=3)
# Сравнение с простой полиномиальной регрессией
simple_poly = Pipeline([
('poly', PolynomialFeatures(degree=8)),
('linear', LinearRegression())
])
simple_poly.fit(t.reshape(-1, 1), y_complex)
simple_smooth = simple_poly.predict(t.reshape(-1, 1))
# Оценка качества различных методов
loess_mse = mean_squared_error(trend + seasonal, loess_smooth)
savgol_mse = mean_squared_error(trend + seasonal, savgol_smooth)
simple_mse = mean_squared_error(trend + seasonal, simple_smooth)
print(f"\nСравнение методов сглаживания:")
print(f"LOESS MSE: {loess_mse:.3f}")
print(f"Savitzky-Golay MSE: {savgol_mse:.3f}")
print(f"Простая полиномиальная регрессия MSE: {simple_mse:.3f}")
# Визуализация различных методов сглаживания
plt.figure(figsize=(14, 6))
plt.scatter(t, y_complex, label='Исходные данные', color='gray', s=20, alpha=0.6)
plt.plot(t, trend + seasonal, label='Истинный тренд + сезонность', color='black', linewidth=2)
plt.plot(t, loess_smooth, label=f'LOESS (frac={optimal_frac:.2f})', color='blue', linewidth=2)
plt.plot(t, savgol_smooth, label='Savitzky-Golay', color='green', linewidth=2)
plt.plot(t, simple_smooth, label='Полиномиальная регрессия', color='red', linestyle='--')
plt.scatter(t[outlier_indices], y_complex[outlier_indices], color='red', label='Выбросы', zorder=5)
plt.title('Сравнение методов сглаживания')
plt.xlabel('Время')
plt.ylabel('Значение')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
# Визуализация кросс-валидации bandwidth
cv_df = pd.DataFrame(cv_scores.items(), columns=['frac', 'mse'])
plt.figure(figsize=(10, 4))
plt.plot(cv_df['frac'], cv_df['mse'], marker='o', color='purple')
plt.axvline(optimal_frac, color='red', linestyle='--', label=f'Оптимальный frac = {optimal_frac:.2f}')
plt.title('Кросс-валидация: Выбор оптимального bandwidth (frac)')
plt.xlabel('Доля данных (frac)')
plt.ylabel('MSE')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
Рис. 3: График исходного временного ряда с наложенным истинным трендом
Рис. 4: График наложенных на временной ряд полиномальных регрессий: простой, LOESS, фильтра Savitzky-Golay
Оптимальная доля данных для LOESS: 0.10
Сравнение методов сглаживания:
LOESS MSE: 0.254
Savitzky-Golay MSE: 0.091
Простая полиномиальная регрессия MSE: 4.200
Рис. 5: Визуализация кросс-валидации и выбора оптимального bandwidth (frac) для LOESS
Результаты демонстрируют, что локальная полиномиальная регрессия значительно превосходит глобальные методы при работе с данными, содержащими выбросы и локальные особенности. LOESS автоматически адаптируется к изменяющейся структуре данных, обеспечивая гладкое сглаживание без потери важных локальных паттернов.
Особенно важно отметить робастность LOESS к выбросам. В отличие от глобальных полиномиальных регрессий, которые могут быть значительно искажены даже небольшим количеством аномальных наблюдений, локальный подход изолирует влияние выбросов в пределах их непосредственной окрестности.
Практические аспекты применения
Выбор степени полинома и параметров модели
Определение оптимальных параметров полиномиальной регрессии для временных рядов требует комплексного подхода, учитывающего как статистические критерии, так и специфику предметной области. За годы работы с рыночными данными я выработал систематический подход, который помогает избежать типичных ошибок при настройке моделей.
Первым шагом всегда является анализ структуры данных. Временные ряды финансовых инструментов редко являются стационарными, что требует особого внимания к выбору степени полинома. Слишком низкая степень приведет к недообучению — модель не сможет захватить важные нелинейные паттерны. Слишком высокая степень вызовет проблемы с переобучением и численной нестабильностью.
Практический алгоритм выбора степени полинома включает несколько этапов:
- Визуальный анализ данных: определение количества локальных экстремумов и точек перегиба;
- Статистическое тестирование: использование информационных критериев и cross-validation;
- Анализ остатков: проверка на наличие автокорреляции и гетероскедастичности;
- Тестирование устойчивости: оценка поведения модели на out-of-sample данных.
Вот пример кода на Python как это можно реализовать:
import numpy as np
import pandas as pd
from sklearn.model_selection import TimeSeriesSplit
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_squared_error
import scipy.stats as stats
from statsmodels.stats.diagnostic import acorr_ljungbox, het_white
import matplotlib.pyplot as plt
import seaborn as sns
class PolynomialModelSelector:
def __init__(self, max_degree=8, min_degree=2, cv_folds=5):
self.max_degree = max_degree
self.min_degree = min_degree
self.cv_folds = cv_folds
self.results = {}
def calculate_information_criteria(self, y_true, y_pred, n_params, n_obs):
"""Вычисление AIC и BIC"""
mse = mean_squared_error(y_true, y_pred)
log_likelihood = -0.5 * n_obs * (np.log(2 * np.pi * mse) + 1)
aic = 2 * n_params - 2 * log_likelihood
bic = n_params * np.log(n_obs) - 2 * log_likelihood
return aic, bic
def analyze_residuals(self, residuals):
"""Анализ остатков модели"""
results = {}
# Тест на автокорреляцию (Ljung-Box)
try:
if len(residuals) >= 20: # Минимум 20 точек для теста
lb_stat, lb_pvalue = acorr_ljungbox(residuals, lags=10, return_df=False)
# Явное преобразование p-value и статистики к float, даже если они строки
def safe_float(x):
try:
return float(x)
except:
try:
return float(str(x).strip())
except:
return np.nan
p_value = safe_float(lb_pvalue[-1])
stat = safe_float(lb_stat[-1])
results['autocorr'] = {
'statistic': stat,
'p_value': p_value,
'interpretation': 'Автокорреляция не обнаружена' if p_value > 0.05 else 'Обнаружена автокорреляция'
}
else:
results['autocorr'] = {'statistic': np.nan, 'p_value': np.nan, 'interpretation': 'Слишком мало данных'}
except Exception as e:
results['autocorr'] = {'statistic': np.nan, 'p_value': np.nan, 'interpretation': f'Ошибка: {str(e)}'}
# Тест на нормальность остатков (Shapiro-Wilk)
if len(residuals) <= 5000 and len(residuals) >= 3:
try:
shapiro_stat, shapiro_pvalue = stats.shapiro(residuals)
results['normality'] = {
'statistic': shapiro_stat,
'p_value': shapiro_pvalue,
'interpretation': 'Нормальность не отвергается' if shapiro_pvalue > 0.05 else 'Нормальность отвергнута'
}
except:
results['normality'] = {'statistic': np.nan, 'p_value': np.nan, 'interpretation': 'Ошибка теста'}
# Тест на гетероскедастичность (White)
try:
X = np.column_stack([np.ones(len(residuals)), np.arange(len(residuals))])
white_stat, white_pvalue = het_white(residuals**2, X)[0:2]
results['heteroskedasticity'] = {
'statistic': float(white_stat),
'p_value': float(white_pvalue),
'interpretation': 'Гетероскедастичность не обнаружена' if white_pvalue < 0.05 else 'Обнаружена гетероскедастичность'
}
except:
results['heteroskedasticity'] = {'statistic': np.nan, 'p_value': np.nan, 'interpretation': 'Ошибка теста'}
return results
def cross_validate(self, X, y):
"""Кросс-валидация для определения оптимальной степени полинома"""
tscv = TimeSeriesSplit(n_splits=self.cv_folds)
degrees = range(self.min_degree, self.max_degree + 1) # Ограниченный диапазон
for degree in degrees:
fold_scores = []
aic_values = []
bic_values = []
for train_idx, test_idx in tscv.split(X):
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
# Создаем pipeline с полиномиальными признаками и линейной регрессией
model = Pipeline([
('poly', PolynomialFeatures(degree=degree)),
('linear', LinearRegression())
])
model.fit(X_train.reshape(-1, 1), y_train)
y_pred = model.predict(X_test.reshape(-1, 1))
# Вычисляем метрики
mse = mean_squared_error(y_test, y_pred)
fold_scores.append(mse)
# Добавляем информацию о количестве параметров
n_params = model.named_steps['poly'].n_output_features_
aic, bic = self.calculate_information_criteria(y_test, y_pred, n_params, len(y_test))
aic_values.append(aic)
bic_values.append(bic)
# Сохраняем результаты
self.results[degree] = {
'cv_mse': np.mean(fold_scores),
'std_mse': np.std(fold_scores),
'aic': np.mean(aic_values),
'bic': np.mean(bic_values)
}
return self.results
def plot_results(self):
"""Визуализация результатов кросс-валидации"""
results_df = pd.DataFrame.from_dict(self.results, orient='index')
results_df.index.name = 'degree'
results_df.reset_index(inplace=True)
plt.figure(figsize=(14, 6))
# MSE
plt.subplot(1, 3, 1)
sns.lineplot(data=results_df, x='degree', y='cv_mse', marker='o')
plt.title('Средняя ошибка кросс-валидации (MSE)')
plt.xlabel('Степень полинома')
plt.ylabel('MSE')
plt.grid(True)
# AIC
plt.subplot(1, 3, 2)
sns.lineplot(data=results_df, x='degree', y='aic', marker='o', color='green')
plt.title('Информационный критерий AIC')
plt.xlabel('Степень полинома')
plt.ylabel('AIC')
plt.grid(True)
# BIC
plt.subplot(1, 3, 3)
sns.lineplot(data=results_df, x='degree', y='bic', marker='o', color='purple')
plt.title('Информационный критерий BIC')
plt.xlabel('Степень полинома')
plt.ylabel('BIC')
plt.grid(True)
plt.tight_layout()
plt.show()
print("\nРезультаты кросс-валидации по степеням полинома:")
print(results_df[['degree', 'cv_mse', 'aic', 'bic']].round(2).sort_values('cv_mse'))
return results_df
# Генерация временного ряда с несколькими режимами
np.random.seed(24)
n_points = 150
change_points = [0, 45, 90, n_points]
trends = [0.1, -0.15, 0.2]
noise_std = 1.5
time = np.arange(n_points)
series = np.zeros(n_points)
for i in range(len(change_points) - 1):
start = change_points[i]
end = change_points[i + 1]
slope = trends[i]
base_trend = slope * (time[start:end] - start)
noise = np.random.normal(0, noise_std, end - start)
series[start:end] = base_trend + noise
data = pd.DataFrame({'time': time, 'value': series})
X = data['time'].values
y = data['value'].values
# Инициализация и запуск анализа
selector = PolynomialModelSelector(max_degree=8, min_degree=2, cv_folds=5)
selector.cross_validate(X, y)
# Визуализация результатов
results_df = selector.plot_results()
# Обучение финальной модели
best_degree = results_df.loc[results_df['cv_mse'].idxmin(), 'degree']
final_model = Pipeline([
('poly', PolynomialFeatures(degree=best_degree)),
('linear', LinearRegression())
])
final_model.fit(X.reshape(-1, 1), y)
y_pred = final_model.predict(X.reshape(-1, 1))
residuals = y - y_pred
print(f"\nАнализ остатков для модели степени {best_degree}:")
residual_analysis = selector.analyze_residuals(residuals)
for test_name, result in residual_analysis.items():
print(f"{test_name}: {result['interpretation']} (p-value: {result['p_value']:.4f})")
# Визуализация предсказаний
plt.figure(figsize=(14, 6))
plt.scatter(X, y, label='Исходные данные', color='gray', alpha=0.7)
plt.plot(X, y_pred, label=f'Полином {best_degree} степени', color='blue')
plt.title('Предсказания модели')
plt.xlabel('Время')
plt.ylabel('Значение')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
Рис. 6: Графики средней ошибки кросс-валидации (MSE), информационных критериев AIC, BIC для различных степеней полинома
Результаты кросс-валидации по степеням полинома:
degree cv_mse aic bic
0 2 5.218000e+01 148.34 152.00
1 3 3.411900e+02 212.39 217.27
2 4 7.936650e+03 247.99 254.08
3 5 3.039787e+04 289.07 296.38
4 6 4.915192e+04 267.95 276.49
5 7 2.961407e+07 360.92 370.67
6 8 2.383431e+08 403.93 414.90
Анализ остатков для модели степени 2:
autocorr: Обнаружена автокорреляция (p-value: nan)
normality: Нормальность отвергнута (p-value: 0.0228)
heteroskedasticity: Гетероскедастичность не обнаружена (p-value: 0.0166)
Рис. 7: График исходного ряда с наложенным полиномом с лучшими характеристиками
После выбора степени полинома и настройки модели следующим важным шагом является ее практическое применение к реальным финансовым данным. Финансовые временные ряды, такие как котировки акций или индексов, часто содержат выбросы, сезонные компоненты и смену рыночных режимов, что требует особого подхода к обработке данных и интерпретации результатов.
Работа с реальными биржевыми данными
Применение полиномиальных регрессий к реальным данным требует предварительной подготовки, включая очистку данных, нормализацию и выбор подходящего временного окна. Например, для анализа дневных цен закрытия акций можно использовать библиотеку yfinance для загрузки данных и последующее применение полиномиальных моделей для выявления трендов.
Ниже приведен пример кода, демонстрирующий загрузку реальных биржевых котировок (например, акций Apple) и применение сегментированной полиномиальной регрессии для сглаживания ценового ряда. Код также включает анализ производных для выявления точек смены рыночных режимов.
import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.interpolate import UnivariateSpline
from sklearn.metrics import mean_squared_error
# Загрузка данных акций (например, Apple)
ticker = "AAPL"
data = yf.download(ticker, start="2023-01-01", end="2025-05-23", progress=False)
data = data['Close'].dropna() # Используем цены закрытия
# Подготовка данных
time = np.arange(len(data))
prices = data.values
# Определение класса для сегментированной регрессии
class SegmentedPolynomialRegression:
def __init__(self, n_knots=5, degree=3, smoothing_factor=None):
self.n_knots = n_knots
self.degree = degree
self.smoothing_factor = smoothing_factor
self.spline = None
self.knots = None
def fit(self, X, y):
"""Обучение сегментированной полиномиальной модели"""
if self.knots is None:
self.knots = np.linspace(X.min(), X.max(), self.n_knots + 2)[1:-1]
if self.smoothing_factor is None:
self.smoothing_factor = len(X) * np.var(y) * 0.1
self.spline = UnivariateSpline(X, y, k=self.degree, s=self.smoothing_factor)
return self
def predict(self, X):
"""Прогнозирование значений"""
if self.spline is None:
raise ValueError("Модель не обучена. Вызовите fit() сначала.")
return self.spline(X)
def get_derivatives(self, X, order=1):
"""Получение производных заданного порядка"""
return self.spline.derivative(order)(X)
def get_knots(self):
"""Получение точек излома"""
return self.spline.get_knots()
# Создание и обучение модели
model = SegmentedPolynomialRegression(n_knots=6, degree=3, smoothing_factor=12000)
model.fit(time, prices)
predictions = model.predict(time)
first_derivative = model.get_derivatives(time, order=1)
# Оценка качества
mse = mean_squared_error(prices, predictions)
print(f"MSE сегментированной регрессии: {mse:.2f}")
# Визуализация результатов
plt.figure(figsize=(14, 8))
# График цен и сглаженного ряда
plt.subplot(2, 1, 1)
plt.plot(data.index, prices, label='Цены закрытия (AAPL)', color='gray', alpha=0.7)
plt.plot(data.index, predictions, label='Сегментированная регрессия', color='blue', linewidth=2)
for knot in model.get_knots():
plt.axvline(data.index[int(knot)], color='red', linestyle='--', alpha=0.5, label='Точка излома' if knot == model.get_knots()[0] else "")
plt.title('Сглаживание цен акций Apple сегментированной регрессией')
plt.xlabel('Дата')
plt.ylabel('Цена закрытия ($)')
plt.legend()
plt.grid(True)
# График первой производной
plt.subplot(2, 1, 2)
plt.plot(data.index, first_derivative, label='Первая производная', color='green')
plt.axhline(0, color='black', linestyle='--', alpha=0.5)
for knot in model.get_knots():
plt.axvline(data.index[int(knot)], color='red', linestyle='--', alpha=0.5)
plt.title('Первая производная сглаженного ряда')
plt.xlabel('Дата')
plt.ylabel('Производная')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
MSE сегментированной регрессии: 20.04
Рис. 8: Сглаживание цен акций Apple с помощью сегментированной полиномиальной регрессии и анализ производной для выявления смены рыночных режимов
Этот код загружает реальные биржевые данные, применяет сегментированную регрессию и визуализирует точки смены рыночных режимов (knots), которые могут соответствовать важным рыночным событиям, таким как публикация отчетов или макроэкономические изменения. Первая производная помогает выявить периоды ускорения или замедления тренда, что особенно полезно для трейдеров и аналитиков.
Обработка выбросов и аномалий
Финансовые данные часто содержат выбросы, вызванные рыночными шоками или ошибками в данных. Для робастности модели рекомендуется использовать предварительную фильтрацию данных, например, с помощью медианного фильтра или робастных статистических методов, таких как LOESS.
Кроме того, при использовании сегментированной регрессии можно настроить параметр сглаживания (smoothing_factor), чтобы уменьшить влияние аномалий.
Интерпретация результатов
Результаты полиномиальной регрессии должны интерпретироваться с учетом контекста. Например, точки излома в сегментированной регрессии могут указывать на смену рыночных режимов, а производные могут использоваться для оценки скорости изменения цен.
Однако важно помнить, что полиномиальные модели не предназначены для долгосрочного прогнозирования, так как они чувствительны к экстраполяции за пределами обучающих данных.
Ограничения и рекомендации:
- Числовая нестабильность: Полиномы высоких степеней могут быть нестабильны на краях временного ряда. Рекомендуется ограничивать степень полинома и использовать сегментированные или локальные методы;
- Вычислительная сложность: Локальные методы, такие как LOESS, требуют значительных вычислительных ресурсов при больших объемах данных. Для оптимизации можно использовать подвыборку или параллельные вычисления;
- Контекст данных: Всегда учитывайте экономический контекст. Например, резкие изменения в данных могут быть связаны с новостями или событиями, которые требуют дополнительного анализа.
Ключевые выводы
Полиномиальные регрессии представляют собой мощный инструмент для сглаживания и анализа временных рядов, особенно в контексте финансовых данных. Они позволяют улавливать сложные нелинейные зависимости, которые недоступны для линейных моделей, и обеспечивают гибкость в моделировании различных рыночных режимов. Также, исследуя эти модели уже много лет, я хочу подчеркнуть следующее:
- Полиномиальные регрессии почти всегда превосходят линейные методы в анализе нелинейных временных рядов, однако часто уступают фильтрам;
- Простая полиномиальная регрессия эффективна для выявления общих трендов, сегментированная регрессия адаптируется к смене режимов, а локальная регрессия (LOESS) демонстрирует высокую устойчивость к выбросам и локальным аномалиям;
- Оптимальная степень полинома и параметры модели должны определяться с использованием статистических критериев (AIC, BIC), кросс-валидации и анализа остатков, чтобы избежать недообучения или переобучения;
- Полиномиальные модели чувствительны к численной нестабильности и не подходят для долгосрочного прогнозирования. Для повышения надежности рекомендуется комбинировать их с другими методами и машинным обучением.