Spaces:
No application file
No application file
Kolesnikov Dmitry commited on
Commit ·
eaf6e74
1
Parent(s): d5266eb
feat: Вторая лабораторка
Browse files- README.md +91 -10
- requirements.txt +2 -0
- src/lab2_functions.py +626 -0
- src/main.py +7 -0
- src/streamlit_app.py +1152 -83
- БЫСТРЫЙ_СТАРТ.md +197 -0
- РУКОВОДСТВО.md +655 -0
- СТРУКТУРА_КОДА.md +334 -0
README.md
CHANGED
|
@@ -1,19 +1,100 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
app_port: 8501
|
| 8 |
tags:
|
| 9 |
- streamlit
|
|
|
|
|
|
|
|
|
|
| 10 |
pinned: false
|
| 11 |
-
short_description:
|
| 12 |
---
|
| 13 |
|
| 14 |
-
#
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: TimeSeriesHomework - Анализ и прогнозирование временных рядов
|
| 3 |
+
emoji: 📊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: streamlit
|
| 7 |
app_port: 8501
|
| 8 |
tags:
|
| 9 |
- streamlit
|
| 10 |
+
- timeseries
|
| 11 |
+
- forecasting
|
| 12 |
+
- machine-learning
|
| 13 |
pinned: false
|
| 14 |
+
short_description: Веб-приложение для анализа и прогнозирования временных рядов
|
| 15 |
---
|
| 16 |
|
| 17 |
+
# 📊 Анализ и прогнозирование временных рядов
|
| 18 |
|
| 19 |
+
Веб-приложение на Streamlit для выполнения двух лабораторных работ по анализу временных рядов.
|
| 20 |
|
| 21 |
+
## 🚀 Быстрый старт
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
pip install -r requirements.txt
|
| 25 |
+
streamlit run src/streamlit_app.py
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
Откройте браузер: `http://localhost:8501`
|
| 29 |
+
|
| 30 |
+
## 📚 Документация
|
| 31 |
+
|
| 32 |
+
- **[БЫСТРЫЙ_СТАРТ.md](БЫСТРЫЙ_СТАРТ.md)** - Краткая шпаргалка для быстрого начала работы
|
| 33 |
+
- **[РУКОВОДСТВО.md](РУКОВОДСТВО.md)** - Подробное руководство по использованию программы
|
| 34 |
+
- **[СТРУКТУРА_КОДА.md](СТРУКТУРА_КОДА.md)** - Описание структуры кода проекта
|
| 35 |
+
|
| 36 |
+
## 🧪 Лабораторные работы
|
| 37 |
+
|
| 38 |
+
### ЛР №1: Введение в анализ временных рядов
|
| 39 |
+
- Сбор и предобработка данных
|
| 40 |
+
- Описательная статистика и визуализация
|
| 41 |
+
- Проверка стационарности
|
| 42 |
+
- Создание лагов и скользящих статистик
|
| 43 |
+
- Анализ автокорреляции (ACF/PACF)
|
| 44 |
+
- Декомпозиция временного ряда
|
| 45 |
+
- Генерация HTML-отчёта
|
| 46 |
+
|
| 47 |
+
### ЛР №2: Прогнозирование временных рядов
|
| 48 |
+
- Углублённая декомпозиция
|
| 49 |
+
- Расширенный feature engineering
|
| 50 |
+
- Стратегии многопшагового прогнозирования
|
| 51 |
+
- Кросс-валидация для временных рядов
|
| 52 |
+
- Преобразования к стационарности (Box-Cox, дифференцирование)
|
| 53 |
+
- Модели экспоненциального сглаживания (SES, Holt)
|
| 54 |
+
- Диагностика остатков моделей
|
| 55 |
+
- Сравнительный анализ моделей
|
| 56 |
+
|
| 57 |
+
## 📁 Структура проекта
|
| 58 |
+
|
| 59 |
+
```
|
| 60 |
+
TimeSeriesHomework/
|
| 61 |
+
├── src/
|
| 62 |
+
│ ├── streamlit_app.py # Главное веб-приложение
|
| 63 |
+
│ ├── lab2_functions.py # Функции для ЛР №2
|
| 64 |
+
│ └── russia_covid_dataset.csv # Пример данных
|
| 65 |
+
├── requirements.txt # Зависимости Python
|
| 66 |
+
├── РУКОВОДСТВО.md # Подробное руководство
|
| 67 |
+
├── БЫСТРЫЙ_СТАРТ.md # Краткая шпаргалка
|
| 68 |
+
└── СТРУКТУРА_КОДА.md # Структура кода
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
## 🛠️ Технологии
|
| 72 |
+
|
| 73 |
+
- **Streamlit** - веб-интерфейс
|
| 74 |
+
- **Pandas** - работа с данными
|
| 75 |
+
- **NumPy** - численные вычисления
|
| 76 |
+
- **Plotly** - интерактивные графики
|
| 77 |
+
- **Statsmodels** - статистические модели
|
| 78 |
+
- **Scipy** - научные вычисления
|
| 79 |
+
- **Scikit-learn** - машинное обучение
|
| 80 |
+
|
| 81 |
+
## 📖 Использование
|
| 82 |
+
|
| 83 |
+
1. **Запустите приложение** (см. Быстрый старт)
|
| 84 |
+
2. **Выберите лабораторную работу** в боковой панели
|
| 85 |
+
3. **Следуйте инструкциям** в интерфейсе
|
| 86 |
+
4. **Изучите документацию** для подробного понимания
|
| 87 |
+
|
| 88 |
+
## 💡 Советы
|
| 89 |
+
|
| 90 |
+
- Начните с **БЫСТРЫЙ_СТАРТ.md** для быстрого начала
|
| 91 |
+
- Используйте **РУКОВОДСТВО.md** для подробного понимания
|
| 92 |
+
- Смотрите **СТРУКТУРА_КОДА.md** для понимания кода
|
| 93 |
+
|
| 94 |
+
## 📝 Лицензия
|
| 95 |
+
|
| 96 |
+
Проект создан для учебных целей.
|
| 97 |
+
|
| 98 |
+
---
|
| 99 |
+
|
| 100 |
+
**Вопросы?** См. документацию в файлах `РУКОВОДСТВО.md` и `БЫСТРЫЙ_СТАРТ.md`
|
requirements.txt
CHANGED
|
@@ -7,3 +7,5 @@ statsmodels
|
|
| 7 |
scikit-learn
|
| 8 |
matplotlib
|
| 9 |
pdfkit
|
|
|
|
|
|
|
|
|
| 7 |
scikit-learn
|
| 8 |
matplotlib
|
| 9 |
pdfkit
|
| 10 |
+
scipy
|
| 11 |
+
seaborn
|
src/lab2_functions.py
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Функции для лабораторной работы №2: Прогнозирование временных рядов
|
| 3 |
+
"""
|
| 4 |
+
import numpy as np
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from typing import List, Tuple, Dict, Optional
|
| 7 |
+
from scipy import stats
|
| 8 |
+
from scipy.stats import boxcox, boxcox_normmax
|
| 9 |
+
from statsmodels.tsa.holtwinters import ExponentialSmoothing
|
| 10 |
+
from statsmodels.stats.diagnostic import acorr_ljungbox
|
| 11 |
+
from statsmodels.tsa.stattools import adfuller, kpss
|
| 12 |
+
from sklearn.model_selection import TimeSeriesSplit
|
| 13 |
+
from sklearn.metrics import mean_absolute_error, mean_squared_error
|
| 14 |
+
import warnings
|
| 15 |
+
warnings.filterwarnings('ignore')
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def calculate_mape(y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
| 19 |
+
"""Вычисляет MAPE (Mean Absolute Percentage Error)"""
|
| 20 |
+
y_true = np.array(y_true)
|
| 21 |
+
y_pred = np.array(y_pred)
|
| 22 |
+
mask = y_true != 0
|
| 23 |
+
if mask.sum() == 0:
|
| 24 |
+
return np.nan
|
| 25 |
+
return np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def create_advanced_features(df: pd.DataFrame, target: str, timestamp_col: str = 'timestamp') -> pd.DataFrame:
|
| 29 |
+
"""
|
| 30 |
+
Расширенный feature engineering:
|
| 31 |
+
- Временные признаки (день недели, месяц, квартал)
|
| 32 |
+
- Циклические признаки через sin/cos
|
| 33 |
+
- Лаги: lag_1, lag_7, lag_30
|
| 34 |
+
- Скользящие статистики: mean, std, min, max по окнам 7, 30, 90
|
| 35 |
+
"""
|
| 36 |
+
df = df.copy()
|
| 37 |
+
df = df.set_index(timestamp_col).sort_index()
|
| 38 |
+
|
| 39 |
+
# Временные признаки
|
| 40 |
+
df['day_of_week'] = df.index.dayofweek
|
| 41 |
+
df['month'] = df.index.month
|
| 42 |
+
df['quarter'] = df.index.quarter
|
| 43 |
+
df['day_of_month'] = df.index.day
|
| 44 |
+
df['week_of_year'] = df.index.isocalendar().week
|
| 45 |
+
|
| 46 |
+
# Циклические признаки
|
| 47 |
+
df['day_of_week_sin'] = np.sin(2 * np.pi * df['day_of_week'] / 7)
|
| 48 |
+
df['day_of_week_cos'] = np.cos(2 * np.pi * df['day_of_week'] / 7)
|
| 49 |
+
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
|
| 50 |
+
df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)
|
| 51 |
+
|
| 52 |
+
# Лаги
|
| 53 |
+
for lag in [1, 7, 30]:
|
| 54 |
+
df[f'{target}_lag_{lag}'] = df[target].shift(lag)
|
| 55 |
+
|
| 56 |
+
# Скользящие статистики
|
| 57 |
+
windows = [7, 30, 90]
|
| 58 |
+
for w in windows:
|
| 59 |
+
df[f'{target}_rolling_mean_{w}'] = df[target].rolling(window=w, min_periods=1).mean()
|
| 60 |
+
df[f'{target}_rolling_std_{w}'] = df[target].rolling(window=w, min_periods=1).std()
|
| 61 |
+
df[f'{target}_rolling_min_{w}'] = df[target].rolling(window=w, min_periods=1).min()
|
| 62 |
+
df[f'{target}_rolling_max_{w}'] = df[target].rolling(window=w, min_periods=1).max()
|
| 63 |
+
|
| 64 |
+
# Коэффициент вариации (волатильность)
|
| 65 |
+
for w in [7, 30]:
|
| 66 |
+
rolling_mean = df[f'{target}_rolling_mean_{w}']
|
| 67 |
+
rolling_std = df[f'{target}_rolling_std_{w}']
|
| 68 |
+
df[f'{target}_rolling_cv_{w}'] = rolling_std / (rolling_mean + 1e-8)
|
| 69 |
+
|
| 70 |
+
return df.reset_index()
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def apply_boxcox_transform(series: pd.Series, lambda_param: Optional[float] = None) -> Tuple[pd.Series, float]:
|
| 74 |
+
"""
|
| 75 |
+
Применяет преобразование Бокса-Кокса.
|
| 76 |
+
Если lambda_param не указан, подбирает оптимальный.
|
| 77 |
+
"""
|
| 78 |
+
series_positive = series[series > 0]
|
| 79 |
+
if len(series_positive) == 0:
|
| 80 |
+
raise ValueError("Все значения должны быть положительными для преобразования Бокса-Кокса")
|
| 81 |
+
|
| 82 |
+
if lambda_param is None:
|
| 83 |
+
# Автоматический подбор lambda
|
| 84 |
+
lambda_param = boxcox_normmax(series_positive.values)
|
| 85 |
+
|
| 86 |
+
transformed_values, fitted_lambda = boxcox(series_positive.values, lmbda=lambda_param)
|
| 87 |
+
|
| 88 |
+
# Создаём новый Series с теми же индексами
|
| 89 |
+
result = pd.Series(index=series.index, dtype=float)
|
| 90 |
+
result.loc[series > 0] = transformed_values
|
| 91 |
+
|
| 92 |
+
return result, fitted_lambda
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def inverse_boxcox_transform(transformed_series: pd.Series, lambda_param: float) -> pd.Series:
|
| 96 |
+
"""Обратное преобразование Бокса-Кокса"""
|
| 97 |
+
if lambda_param == 0:
|
| 98 |
+
return np.exp(transformed_series)
|
| 99 |
+
else:
|
| 100 |
+
return (lambda_param * transformed_series + 1) ** (1 / lambda_param)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def inverse_transformations(
|
| 104 |
+
forecast: np.ndarray,
|
| 105 |
+
last_train_values_transformed: np.ndarray,
|
| 106 |
+
transform_info: Dict
|
| 107 |
+
) -> np.ndarray:
|
| 108 |
+
"""
|
| 109 |
+
Применяет обратные преобразования к прогнозу.
|
| 110 |
+
|
| 111 |
+
Порядок обратного преобразования должен быть обратным порядку прямого:
|
| 112 |
+
Прямое: transformation -> diff_order -> seasonal_diff
|
| 113 |
+
Обратное: seasonal_diff -> diff_order -> transformation
|
| 114 |
+
|
| 115 |
+
forecast: прогноз в преобразованном пространстве (после всех преобразований)
|
| 116 |
+
last_train_values_transformed: последние значения обучающей выборки в преобразованном пространстве (после всех преобразований)
|
| 117 |
+
transform_info: информация о применённых преобразованиях (может содержать промежуточные значения)
|
| 118 |
+
"""
|
| 119 |
+
result = forecast.copy()
|
| 120 |
+
diff_order = transform_info.get('diff_order', 0)
|
| 121 |
+
seasonal_diff = transform_info.get('seasonal_diff')
|
| 122 |
+
|
| 123 |
+
# Получаем промежуточные значения из transform_info, если они есть
|
| 124 |
+
last_values_after_diff = transform_info.get('last_values_after_diff', None)
|
| 125 |
+
last_values_after_transform = transform_info.get('last_values_after_transform', None)
|
| 126 |
+
|
| 127 |
+
# 1. Обратное сезонное дифференцирование (если было)
|
| 128 |
+
if seasonal_diff is not None and seasonal_diff > 0:
|
| 129 |
+
# Нужны последние seasonal_diff значений после transformation и diff, но до seasonal_diff
|
| 130 |
+
if last_values_after_diff is not None and len(last_values_after_diff) >= seasonal_diff:
|
| 131 |
+
last_seasonal = last_values_after_diff[-seasonal_diff:]
|
| 132 |
+
elif len(last_train_values_transformed) >= seasonal_diff:
|
| 133 |
+
# Fallback: используем последние значения (хотя это не совсем правильно)
|
| 134 |
+
last_seasonal = last_train_values_transformed[-seasonal_diff:]
|
| 135 |
+
else:
|
| 136 |
+
last_seasonal = last_train_values_transformed if len(last_train_values_transformed) > 0 else np.array([0])
|
| 137 |
+
|
| 138 |
+
for i in range(len(result)):
|
| 139 |
+
if i < len(last_seasonal):
|
| 140 |
+
result[i] = result[i] + last_seasonal[i]
|
| 141 |
+
else:
|
| 142 |
+
# Используем предыдущие прогнозы
|
| 143 |
+
result[i] = result[i] + result[i - seasonal_diff]
|
| 144 |
+
|
| 145 |
+
# 2. Обратное обычное дифференцирование (если было)
|
| 146 |
+
for _ in range(diff_order):
|
| 147 |
+
# Нужны последние diff_order значений после transformation, но до diff
|
| 148 |
+
if last_values_after_transform is not None and len(last_values_after_transform) > 0:
|
| 149 |
+
last_val = last_values_after_transform[-1]
|
| 150 |
+
elif len(last_train_values_transformed) > 0:
|
| 151 |
+
# Fallback
|
| 152 |
+
last_val = last_train_values_transformed[-1]
|
| 153 |
+
else:
|
| 154 |
+
last_val = 0
|
| 155 |
+
|
| 156 |
+
for i in range(len(result)):
|
| 157 |
+
if i == 0:
|
| 158 |
+
result[i] = result[i] + last_val
|
| 159 |
+
else:
|
| 160 |
+
result[i] = result[i] + result[i - 1]
|
| 161 |
+
|
| 162 |
+
# 3. Обратное преобразование для стабилизации дисперсии
|
| 163 |
+
if transform_info.get('transformation') == 'log':
|
| 164 |
+
result = np.exp(result)
|
| 165 |
+
elif transform_info.get('transformation') == 'boxcox':
|
| 166 |
+
lambda_param = transform_info.get('lambda')
|
| 167 |
+
if lambda_param is not None:
|
| 168 |
+
if lambda_param == 0:
|
| 169 |
+
result = np.exp(result)
|
| 170 |
+
else:
|
| 171 |
+
result = (lambda_param * result + 1) ** (1 / lambda_param)
|
| 172 |
+
|
| 173 |
+
return result
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def apply_transformations(
|
| 177 |
+
series: pd.Series,
|
| 178 |
+
transformation: str = 'none',
|
| 179 |
+
lambda_param: Optional[float] = None,
|
| 180 |
+
diff_order: int = 0,
|
| 181 |
+
seasonal_diff: Optional[int] = None
|
| 182 |
+
) -> Tuple[pd.Series, Dict]:
|
| 183 |
+
"""
|
| 184 |
+
Применяет цепочку преобразований к ряду.
|
| 185 |
+
|
| 186 |
+
transformation: 'none', 'log', 'boxcox'
|
| 187 |
+
diff_order: порядок обычного дифференцирования
|
| 188 |
+
seasonal_diff: период сезонного дифференцирования
|
| 189 |
+
|
| 190 |
+
Возвращает преобразованный ряд и словарь с информацией о преобразованиях,
|
| 191 |
+
включая промежуточные значения для обратного преобразования.
|
| 192 |
+
"""
|
| 193 |
+
result = series.copy()
|
| 194 |
+
info = {'transformation': transformation, 'lambda': None, 'diff_order': diff_order, 'seasonal_diff': seasonal_diff}
|
| 195 |
+
|
| 196 |
+
# Преобразование для стабилизации дисперсии
|
| 197 |
+
if transformation == 'log':
|
| 198 |
+
if (result <= 0).any():
|
| 199 |
+
raise ValueError("Для лог-трансформации все значения должны быть положительными")
|
| 200 |
+
result = np.log(result)
|
| 201 |
+
elif transformation == 'boxcox':
|
| 202 |
+
result, lambda_param = apply_boxcox_transform(result, lambda_param)
|
| 203 |
+
info['lambda'] = lambda_param
|
| 204 |
+
|
| 205 |
+
# Сохраняем значения после transformation (для обратного diff)
|
| 206 |
+
result_after_transform = result.copy()
|
| 207 |
+
|
| 208 |
+
# Обычное дифференцирование
|
| 209 |
+
for _ in range(diff_order):
|
| 210 |
+
result = result.diff()
|
| 211 |
+
|
| 212 |
+
# Сохраняем значения после diff (для обратного seasonal_diff)
|
| 213 |
+
result_after_diff = result.copy()
|
| 214 |
+
|
| 215 |
+
# Сезонное дифференцирование
|
| 216 |
+
if seasonal_diff is not None and seasonal_diff > 0:
|
| 217 |
+
result = result.diff(periods=seasonal_diff)
|
| 218 |
+
|
| 219 |
+
# Сохраняем промежуточные значения для обратного преобразования
|
| 220 |
+
info['last_values_after_transform'] = result_after_transform.values[-max(diff_order, 1):] if len(result_after_transform) > 0 else np.array([])
|
| 221 |
+
info['last_values_after_diff'] = result_after_diff.values[-max(seasonal_diff if seasonal_diff else 1, 1):] if len(result_after_diff) > 0 else np.array([])
|
| 222 |
+
|
| 223 |
+
return result.dropna(), info
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def recursive_forecast(
|
| 227 |
+
model_func,
|
| 228 |
+
train_data: pd.Series,
|
| 229 |
+
horizon: int,
|
| 230 |
+
alpha: Optional[float] = None,
|
| 231 |
+
**model_kwargs
|
| 232 |
+
) -> Tuple[np.ndarray, Optional[Tuple[np.ndarray, np.ndarray]]]:
|
| 233 |
+
"""
|
| 234 |
+
Рекурсивная стратегия прогнозирования:
|
| 235 |
+
Одна модель → итеративное использование прогнозов
|
| 236 |
+
|
| 237 |
+
Возвращает прогнозы и опционально доверительные интервалы (lower, upper)
|
| 238 |
+
"""
|
| 239 |
+
forecasts = []
|
| 240 |
+
conf_lower = [] if alpha is not None else None
|
| 241 |
+
conf_upper = [] if alpha is not None else None
|
| 242 |
+
current_data = train_data.copy()
|
| 243 |
+
|
| 244 |
+
# Определяем тип индекса для правильного добавления новых значений
|
| 245 |
+
is_datetime = pd.api.types.is_datetime64_any_dtype(current_data.index)
|
| 246 |
+
|
| 247 |
+
for h in range(horizon):
|
| 248 |
+
# Обучаем модель на текущих данных
|
| 249 |
+
model = model_func(current_data, **model_kwargs)
|
| 250 |
+
# Прогнозируем на 1 шаг вперёд
|
| 251 |
+
if alpha is not None:
|
| 252 |
+
try:
|
| 253 |
+
forecast_result = model.forecast(steps=1, alpha=alpha)
|
| 254 |
+
if isinstance(forecast_result, tuple):
|
| 255 |
+
forecast_value = forecast_result[0][0] if len(forecast_result[0]) > 0 else forecast_result[0]
|
| 256 |
+
if len(forecast_result) > 1:
|
| 257 |
+
conf_lower.append(forecast_result[1][0] if len(forecast_result[1]) > 0 else forecast_result[1])
|
| 258 |
+
conf_upper.append(forecast_result[2][0] if len(forecast_result[2]) > 0 else forecast_result[2])
|
| 259 |
+
else:
|
| 260 |
+
forecast_value = forecast_result[0] if hasattr(forecast_result, '__getitem__') else float(forecast_result)
|
| 261 |
+
except:
|
| 262 |
+
# Если доверительные интервалы не поддерживаются, используем обычный прогноз
|
| 263 |
+
forecast = model.forecast(steps=1)
|
| 264 |
+
forecast_value = forecast[0] if hasattr(forecast, '__getitem__') else float(forecast)
|
| 265 |
+
else:
|
| 266 |
+
forecast = model.forecast(steps=1)
|
| 267 |
+
forecast_value = forecast[0] if hasattr(forecast, '__getitem__') else float(forecast)
|
| 268 |
+
|
| 269 |
+
forecasts.append(forecast_value)
|
| 270 |
+
|
| 271 |
+
# Добавляем прогноз к данным для следующей итерации
|
| 272 |
+
if is_datetime:
|
| 273 |
+
# Для DatetimeIndex используем частоту или инференс
|
| 274 |
+
try:
|
| 275 |
+
freq = pd.infer_freq(current_data.index) or 'D'
|
| 276 |
+
# Используем pd.date_range для создания следующей даты
|
| 277 |
+
last_date = current_data.index[-1]
|
| 278 |
+
next_dates = pd.date_range(start=last_date, periods=2, freq=freq)
|
| 279 |
+
if len(next_dates) >= 2:
|
| 280 |
+
next_idx = next_dates[1] # Берём вторую дату (первая = last_date)
|
| 281 |
+
else:
|
| 282 |
+
# Fallback
|
| 283 |
+
next_idx = len(current_data)
|
| 284 |
+
is_datetime = False
|
| 285 |
+
except:
|
| 286 |
+
# Если не удалось определить частоту, используем числовой индекс
|
| 287 |
+
try:
|
| 288 |
+
# Пробуем простой способ через Timedelta
|
| 289 |
+
next_idx = current_data.index[-1] + pd.Timedelta(days=1)
|
| 290 |
+
except:
|
| 291 |
+
next_idx = len(current_data)
|
| 292 |
+
is_datetime = False
|
| 293 |
+
else:
|
| 294 |
+
# Для числового индекса просто увеличиваем на 1
|
| 295 |
+
next_idx = len(current_data)
|
| 296 |
+
|
| 297 |
+
if is_datetime:
|
| 298 |
+
current_data = pd.concat([current_data, pd.Series([forecast_value], index=[next_idx])])
|
| 299 |
+
else:
|
| 300 |
+
# Используем числовой индекс
|
| 301 |
+
current_data = pd.concat([current_data, pd.Series([forecast_value], index=[next_idx])])
|
| 302 |
+
|
| 303 |
+
result = np.array(forecasts)
|
| 304 |
+
if alpha is not None and conf_lower and conf_upper:
|
| 305 |
+
return result, (np.array(conf_lower), np.array(conf_upper))
|
| 306 |
+
return result, None
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def direct_forecast(
|
| 310 |
+
model_func,
|
| 311 |
+
train_data: pd.Series,
|
| 312 |
+
horizon: int,
|
| 313 |
+
alpha: Optional[float] = None,
|
| 314 |
+
**model_kwargs
|
| 315 |
+
) -> Tuple[np.ndarray, Optional[Tuple[np.ndarray, np.ndarray]]]:
|
| 316 |
+
"""
|
| 317 |
+
Прямая стратегия прогнозирования:
|
| 318 |
+
Отдельная модель для каждого шага t+1, ..., t+h
|
| 319 |
+
|
| 320 |
+
Возвращает прогнозы и опционально доверительные интервалы (lower, upper)
|
| 321 |
+
"""
|
| 322 |
+
forecasts = []
|
| 323 |
+
conf_lower = [] if alpha is not None else None
|
| 324 |
+
conf_upper = [] if alpha is not None else None
|
| 325 |
+
|
| 326 |
+
for h in range(1, horizon + 1):
|
| 327 |
+
# Обучаем отдельную модель для шага h
|
| 328 |
+
model = model_func(train_data, **model_kwargs)
|
| 329 |
+
# Прогнозируем на h шагов вперёд и берём последний
|
| 330 |
+
if alpha is not None:
|
| 331 |
+
try:
|
| 332 |
+
forecast_result = model.forecast(steps=h, alpha=alpha)
|
| 333 |
+
if isinstance(forecast_result, tuple):
|
| 334 |
+
forecast_value = forecast_result[0][-1] if len(forecast_result[0]) > 0 else forecast_result[0]
|
| 335 |
+
if len(forecast_result) > 1:
|
| 336 |
+
conf_lower.append(forecast_result[1][-1] if len(forecast_result[1]) > 0 else forecast_result[1])
|
| 337 |
+
conf_upper.append(forecast_result[2][-1] if len(forecast_result[2]) > 0 else forecast_result[2])
|
| 338 |
+
else:
|
| 339 |
+
forecast_value = forecast_result[-1]
|
| 340 |
+
except:
|
| 341 |
+
forecast = model.forecast(steps=h)
|
| 342 |
+
forecast_value = forecast[-1]
|
| 343 |
+
else:
|
| 344 |
+
forecast = model.forecast(steps=h)
|
| 345 |
+
forecast_value = forecast[-1]
|
| 346 |
+
|
| 347 |
+
forecasts.append(forecast_value)
|
| 348 |
+
|
| 349 |
+
result = np.array(forecasts)
|
| 350 |
+
if alpha is not None and conf_lower and conf_upper:
|
| 351 |
+
return result, (np.array(conf_lower), np.array(conf_upper))
|
| 352 |
+
return result, None
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def hybrid_forecast(
|
| 356 |
+
model_func,
|
| 357 |
+
train_data: pd.Series,
|
| 358 |
+
horizon: int,
|
| 359 |
+
recursive_steps: int = None,
|
| 360 |
+
alpha: Optional[float] = None,
|
| 361 |
+
**model_kwargs
|
| 362 |
+
) -> Tuple[np.ndarray, Optional[Tuple[np.ndarray, np.ndarray]]]:
|
| 363 |
+
"""
|
| 364 |
+
Гибридная стратегия:
|
| 365 |
+
Рекурсивная для ближайших шагов, прямая — для дальних
|
| 366 |
+
|
| 367 |
+
Возвращает прогнозы и опционально доверительные интервалы (lower, upper)
|
| 368 |
+
"""
|
| 369 |
+
if recursive_steps is None:
|
| 370 |
+
recursive_steps = max(1, horizon // 2)
|
| 371 |
+
|
| 372 |
+
forecasts = []
|
| 373 |
+
conf_lower = [] if alpha is not None else None
|
| 374 |
+
conf_upper = [] if alpha is not None else None
|
| 375 |
+
|
| 376 |
+
# Рекурсивная часть
|
| 377 |
+
recursive_result = recursive_forecast(model_func, train_data, recursive_steps, alpha=alpha, **model_kwargs)
|
| 378 |
+
if isinstance(recursive_result, tuple):
|
| 379 |
+
recursive_forecasts, recursive_conf = recursive_result
|
| 380 |
+
if recursive_conf is not None:
|
| 381 |
+
conf_lower.extend(recursive_conf[0])
|
| 382 |
+
conf_upper.extend(recursive_conf[1])
|
| 383 |
+
else:
|
| 384 |
+
recursive_forecasts = recursive_result
|
| 385 |
+
forecasts.extend(recursive_forecasts)
|
| 386 |
+
|
| 387 |
+
# Прямая часть для оставшихся шагов
|
| 388 |
+
if horizon > recursive_steps:
|
| 389 |
+
# Используем последние данные + рекурсивные прогнозы
|
| 390 |
+
is_datetime = pd.api.types.is_datetime64_any_dtype(train_data.index)
|
| 391 |
+
|
| 392 |
+
if is_datetime:
|
| 393 |
+
try:
|
| 394 |
+
freq = pd.infer_freq(train_data.index) or 'D'
|
| 395 |
+
# Используем pd.date_range для создания дат начиная с последней даты + 1 период
|
| 396 |
+
last_date = train_data.index[-1]
|
| 397 |
+
extended_index = pd.date_range(
|
| 398 |
+
start=last_date,
|
| 399 |
+
periods=len(recursive_forecasts) + 1,
|
| 400 |
+
freq=freq
|
| 401 |
+
)[1:] # Берём все даты кроме первой (которая равна last_date)
|
| 402 |
+
except:
|
| 403 |
+
# Fallback на числовой индекс
|
| 404 |
+
try:
|
| 405 |
+
# Пробуем через date_range с periods
|
| 406 |
+
last_date = train_data.index[-1]
|
| 407 |
+
extended_index = pd.date_range(
|
| 408 |
+
start=last_date,
|
| 409 |
+
periods=len(recursive_forecasts) + 1,
|
| 410 |
+
freq='D'
|
| 411 |
+
)[1:] # Берём все даты кроме первой
|
| 412 |
+
except:
|
| 413 |
+
extended_index = range(len(train_data), len(train_data) + len(recursive_forecasts))
|
| 414 |
+
else:
|
| 415 |
+
extended_index = range(len(train_data), len(train_data) + len(recursive_forecasts))
|
| 416 |
+
|
| 417 |
+
extended_data = pd.concat([
|
| 418 |
+
train_data,
|
| 419 |
+
pd.Series(recursive_forecasts, index=extended_index)
|
| 420 |
+
])
|
| 421 |
+
|
| 422 |
+
remaining_horizon = horizon - recursive_steps
|
| 423 |
+
direct_result = direct_forecast(model_func, extended_data, remaining_horizon, alpha=alpha, **model_kwargs)
|
| 424 |
+
if isinstance(direct_result, tuple):
|
| 425 |
+
direct_forecasts, direct_conf = direct_result
|
| 426 |
+
if direct_conf is not None:
|
| 427 |
+
conf_lower.extend(direct_conf[0])
|
| 428 |
+
conf_upper.extend(direct_conf[1])
|
| 429 |
+
else:
|
| 430 |
+
direct_forecasts = direct_result
|
| 431 |
+
forecasts.extend(direct_forecasts)
|
| 432 |
+
|
| 433 |
+
result = np.array(forecasts[:horizon])
|
| 434 |
+
if alpha is not None and conf_lower and conf_upper:
|
| 435 |
+
return result, (np.array(conf_lower[:horizon]), np.array(conf_upper[:horizon]))
|
| 436 |
+
return result, None
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
def create_exponential_smoothing_model(
|
| 440 |
+
train_data: pd.Series,
|
| 441 |
+
trend: Optional[str] = None,
|
| 442 |
+
seasonal: Optional[str] = None,
|
| 443 |
+
seasonal_periods: Optional[int] = None,
|
| 444 |
+
optimized: bool = True
|
| 445 |
+
) -> ExponentialSmoothing:
|
| 446 |
+
"""Создаёт и обучает модель экспоненциального сглаживания"""
|
| 447 |
+
try:
|
| 448 |
+
model = ExponentialSmoothing(
|
| 449 |
+
train_data,
|
| 450 |
+
trend=trend,
|
| 451 |
+
seasonal=seasonal,
|
| 452 |
+
seasonal_periods=seasonal_periods,
|
| 453 |
+
initialization_method='estimated' if optimized else 'simple'
|
| 454 |
+
)
|
| 455 |
+
fitted_model = model.fit(optimized=optimized)
|
| 456 |
+
return fitted_model
|
| 457 |
+
except Exception as e:
|
| 458 |
+
raise ValueError(f"Ошибка при создании модели: {e}")
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def evaluate_forecast(y_true: np.ndarray, y_pred: np.ndarray) -> Dict[str, float]:
|
| 462 |
+
"""Вычисляет метрики качества прогноза"""
|
| 463 |
+
y_true = np.array(y_true)
|
| 464 |
+
y_pred = np.array(y_pred)
|
| 465 |
+
|
| 466 |
+
mae = mean_absolute_error(y_true, y_pred)
|
| 467 |
+
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
|
| 468 |
+
mape = calculate_mape(y_true, y_pred)
|
| 469 |
+
|
| 470 |
+
return {
|
| 471 |
+
'MAE': mae,
|
| 472 |
+
'RMSE': rmse,
|
| 473 |
+
'MAPE': mape
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def naive_forecast(train_data: pd.Series, horizon: int) -> np.ndarray:
|
| 478 |
+
"""Наивный прогноз: y[t+h] = y[t]"""
|
| 479 |
+
last_value = train_data.iloc[-1]
|
| 480 |
+
return np.full(horizon, last_value)
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
def time_series_cv_sliding_window(
|
| 484 |
+
model_func,
|
| 485 |
+
data: pd.Series,
|
| 486 |
+
train_size: int,
|
| 487 |
+
test_size: int,
|
| 488 |
+
horizon: int,
|
| 489 |
+
step: int = 1,
|
| 490 |
+
**model_kwargs
|
| 491 |
+
) -> List[Dict]:
|
| 492 |
+
"""
|
| 493 |
+
Кросс-валидация со скользящим окном (фиксированная длина обучения)
|
| 494 |
+
"""
|
| 495 |
+
results = []
|
| 496 |
+
n = len(data)
|
| 497 |
+
|
| 498 |
+
for i in range(0, n - train_size - test_size + 1, step):
|
| 499 |
+
train_end = i + train_size
|
| 500 |
+
test_end = min(train_end + test_size, n)
|
| 501 |
+
|
| 502 |
+
train_data = data.iloc[i:train_end]
|
| 503 |
+
test_data = data.iloc[train_end:test_end]
|
| 504 |
+
|
| 505 |
+
try:
|
| 506 |
+
model = model_func(train_data, **model_kwargs)
|
| 507 |
+
forecast = model.forecast(steps=min(horizon, len(test_data)))
|
| 508 |
+
|
| 509 |
+
metrics = evaluate_forecast(test_data.values[:len(forecast)], forecast)
|
| 510 |
+
metrics['fold'] = len(results) + 1
|
| 511 |
+
metrics['train_start'] = train_data.index[0]
|
| 512 |
+
metrics['train_end'] = train_data.index[-1]
|
| 513 |
+
metrics['test_start'] = test_data.index[0]
|
| 514 |
+
metrics['test_end'] = test_data.index[-1]
|
| 515 |
+
results.append(metrics)
|
| 516 |
+
except Exception as e:
|
| 517 |
+
print(f"Ошибка в фолде {len(results) + 1}: {e}")
|
| 518 |
+
|
| 519 |
+
return results
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def time_series_cv_expanding_window(
|
| 523 |
+
model_func,
|
| 524 |
+
data: pd.Series,
|
| 525 |
+
initial_train_size: int,
|
| 526 |
+
test_size: int,
|
| 527 |
+
horizon: int,
|
| 528 |
+
step: int = 1,
|
| 529 |
+
**model_kwargs
|
| 530 |
+
) -> List[Dict]:
|
| 531 |
+
"""
|
| 532 |
+
Кросс-валидация с расширяющимся окном (обучение растёт со временем)
|
| 533 |
+
"""
|
| 534 |
+
results = []
|
| 535 |
+
n = len(data)
|
| 536 |
+
|
| 537 |
+
for i in range(initial_train_size, n - test_size + 1, step):
|
| 538 |
+
train_end = i
|
| 539 |
+
test_end = min(train_end + test_size, n)
|
| 540 |
+
|
| 541 |
+
train_data = data.iloc[:train_end]
|
| 542 |
+
test_data = data.iloc[train_end:test_end]
|
| 543 |
+
|
| 544 |
+
try:
|
| 545 |
+
model = model_func(train_data, **model_kwargs)
|
| 546 |
+
forecast = model.forecast(steps=min(horizon, len(test_data)))
|
| 547 |
+
|
| 548 |
+
metrics = evaluate_forecast(test_data.values[:len(forecast)], forecast)
|
| 549 |
+
metrics['fold'] = len(results) + 1
|
| 550 |
+
metrics['train_start'] = train_data.index[0]
|
| 551 |
+
metrics['train_end'] = train_data.index[-1]
|
| 552 |
+
metrics['test_start'] = test_data.index[0]
|
| 553 |
+
metrics['test_end'] = test_data.index[-1]
|
| 554 |
+
results.append(metrics)
|
| 555 |
+
except Exception as e:
|
| 556 |
+
print(f"Ошибка в фолде {len(results) + 1}: {e}")
|
| 557 |
+
|
| 558 |
+
return results
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
def diagnose_model_residuals(residuals: np.ndarray, lags: int = 10) -> Dict:
|
| 562 |
+
"""
|
| 563 |
+
Диагностика остатк��в модели:
|
| 564 |
+
- Тест Льюнга-Бокса на автокорреляцию
|
| 565 |
+
- Проверка нормальности (Shapiro-Wilk)
|
| 566 |
+
- Q-Q plot данные
|
| 567 |
+
"""
|
| 568 |
+
residuals_clean = residuals[~np.isnan(residuals)]
|
| 569 |
+
|
| 570 |
+
if len(residuals_clean) < 3:
|
| 571 |
+
return {'error': 'Недостаточно данных для диагностики'}
|
| 572 |
+
|
| 573 |
+
results = {}
|
| 574 |
+
|
| 575 |
+
# Тест Льюнга-Бокса
|
| 576 |
+
try:
|
| 577 |
+
lb_stat, lb_pvalue = acorr_ljungbox(residuals_clean, lags=min(lags, len(residuals_clean) - 1), return_df=False)
|
| 578 |
+
results['ljung_box'] = {
|
| 579 |
+
'statistic': float(lb_stat[-1]) if len(lb_stat) > 0 else None,
|
| 580 |
+
'pvalue': float(lb_pvalue[-1]) if len(lb_pvalue) > 0 else None,
|
| 581 |
+
'lags': lags
|
| 582 |
+
}
|
| 583 |
+
except Exception as e:
|
| 584 |
+
results['ljung_box'] = {'error': str(e)}
|
| 585 |
+
|
| 586 |
+
# Тест Шапиро-Уилка на нормальность
|
| 587 |
+
try:
|
| 588 |
+
if len(residuals_clean) <= 5000: # Ограничение для Shapiro-Wilk
|
| 589 |
+
shapiro_stat, shapiro_pvalue = stats.shapiro(residuals_clean)
|
| 590 |
+
results['shapiro_wilk'] = {
|
| 591 |
+
'statistic': float(shapiro_stat),
|
| 592 |
+
'pvalue': float(shapiro_pvalue)
|
| 593 |
+
}
|
| 594 |
+
else:
|
| 595 |
+
# Для больших выборок используем тест нормальности из scipy
|
| 596 |
+
k2_stat, k2_pvalue = stats.normaltest(residuals_clean)
|
| 597 |
+
results['normality_test'] = {
|
| 598 |
+
'statistic': float(k2_stat),
|
| 599 |
+
'pvalue': float(k2_pvalue),
|
| 600 |
+
'test': 'normaltest'
|
| 601 |
+
}
|
| 602 |
+
except Exception as e:
|
| 603 |
+
results['normality_test'] = {'error': str(e)}
|
| 604 |
+
|
| 605 |
+
# Статистики остатков
|
| 606 |
+
results['residual_stats'] = {
|
| 607 |
+
'mean': float(np.mean(residuals_clean)),
|
| 608 |
+
'std': float(np.std(residuals_clean)),
|
| 609 |
+
'min': float(np.min(residuals_clean)),
|
| 610 |
+
'max': float(np.max(residuals_clean)),
|
| 611 |
+
'count': len(residuals_clean)
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
# Проверка стационарности остатков
|
| 615 |
+
try:
|
| 616 |
+
adf_stat, adf_pvalue, _, _, _, _ = adfuller(residuals_clean)
|
| 617 |
+
kpss_stat, kpss_pvalue, _, _ = kpss(residuals_clean)
|
| 618 |
+
results['stationarity'] = {
|
| 619 |
+
'adf': {'statistic': float(adf_stat), 'pvalue': float(adf_pvalue)},
|
| 620 |
+
'kpss': {'statistic': float(kpss_stat), 'pvalue': float(kpss_pvalue)}
|
| 621 |
+
}
|
| 622 |
+
except Exception as e:
|
| 623 |
+
results['stationarity'] = {'error': str(e)}
|
| 624 |
+
|
| 625 |
+
return results
|
| 626 |
+
|
src/main.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Главная страница приложения - навигация между лабораторными работами
|
| 3 |
+
Этот файл не используется, так как основное приложение находится в streamlit_app.py
|
| 4 |
+
"""
|
| 5 |
+
# Этот файл оставлен для совместимости, но основное приложение находится в streamlit_app.py
|
| 6 |
+
# Для запуска используйте: streamlit run src/streamlit_app.py
|
| 7 |
+
|
src/streamlit_app.py
CHANGED
|
@@ -8,15 +8,39 @@ import streamlit as st
|
|
| 8 |
import plotly.express as px
|
| 9 |
import plotly.graph_objects as go
|
| 10 |
import matplotlib.pyplot as plt
|
|
|
|
| 11 |
from statsmodels.tsa.stattools import adfuller, kpss, acf as sm_acf, pacf as sm_pacf
|
| 12 |
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
|
| 13 |
from statsmodels.tsa.seasonal import seasonal_decompose
|
| 14 |
from statsmodels.stats.outliers_influence import variance_inflation_factor
|
| 15 |
from statsmodels.tools import add_constant
|
| 16 |
|
| 17 |
-
st.set_page_config(page_title="
|
| 18 |
MOSCOW = pytz.timezone("Europe/Moscow")
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
# ---------------- Utilities ----------------
|
| 22 |
def detect_date_column(df: pd.DataFrame) -> Optional[str]:
|
|
@@ -335,97 +359,99 @@ def generate_html_report(
|
|
| 335 |
return html
|
| 336 |
|
| 337 |
|
| 338 |
-
# ----------------
|
| 339 |
-
|
|
|
|
|
|
|
| 340 |
|
| 341 |
-
# Sidebar
|
| 342 |
-
st.sidebar.header("Настройки")
|
| 343 |
-
uploaded_file = st.sidebar.file_uploader("Загрузите CSV/Parquet", type=['csv', 'parquet'])
|
| 344 |
|
| 345 |
-
# small built-in example option (uses local file if present)
|
| 346 |
-
sample_option = None
|
| 347 |
-
if os.path.exists('russia_covid_dataset.csv'):
|
| 348 |
-
|
| 349 |
-
sample_choice = st.sidebar.selectbox('Или выбрать предзагруженный пример', options=[None, sample_option] if sample_option else [None])
|
| 350 |
|
| 351 |
-
tz_assume = st.sidebar.selectbox("Как трактовать tz-naive метки?",
|
| 352 |
options=['local', 'utc', 'keep'], index=0,
|
| 353 |
format_func=lambda x: {'local': 'локально (Europe/Moscow)', 'utc': 'UTC->Moscow', 'keep': 'не трогать'}[x])
|
| 354 |
-
numeric_missing_strategy = st.sidebar.selectbox("Заполнение пропусков (числ.)", options=['interpolate', 'drop', 'rolling'], index=0)
|
| 355 |
-
cat_missing_strategy = st.sidebar.selectbox("Заполнение пропусков (категор.)", options=['mode', 'unknown'], index=0)
|
| 356 |
-
outlier_strategy = st.sidebar.selectbox("Обработка выбросов", options=['interpolate', 'winsorize', 'drop', 'mark'], index=0)
|
| 357 |
-
resample_freq = st.sidebar.selectbox("Ресемплить к частоте (если нужно)", options=[None, 'D', 'W', 'M'], index=1)
|
| 358 |
|
| 359 |
-
# load dataset and persist
|
| 360 |
-
if 'df_in' not in st.session_state:
|
| 361 |
-
|
| 362 |
|
| 363 |
-
if uploaded_file is not None:
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
elif sample_choice:
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
else:
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
st.stop()
|
| 385 |
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
st.
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
st.
|
| 396 |
-
st.
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
st.session_state
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
cat_missing_strategy=cat_missing_strategy,
|
| 418 |
-
outlier_strategy=outlier_strategy,
|
| 419 |
-
resample_freq=resample_freq,
|
| 420 |
-
)
|
| 421 |
-
st.session_state['df_clean'] = df_clean
|
| 422 |
-
st.session_state['info'] = info
|
| 423 |
-
st.session_state['preprocessed'] = True
|
| 424 |
-
|
| 425 |
-
# Main UI after preprocess
|
| 426 |
-
if st.session_state.get('preprocessed'):
|
| 427 |
-
df_clean = st.session_state['df_clean']
|
| 428 |
-
info = st.session_state['info']
|
| 429 |
|
| 430 |
st.subheader("Финальный датасет (первые строки)")
|
| 431 |
st.dataframe(df_clean.head(10))
|
|
@@ -815,3 +841,1046 @@ if st.session_state.get('preprocessed'):
|
|
| 815 |
except Exception:
|
| 816 |
st.info(
|
| 817 |
'PDF-конверсия недоступна (pdfkit/wkhtmltopdf не установлены). Скачайте HTML и конвертируйте локально, если нужно.')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
import plotly.express as px
|
| 9 |
import plotly.graph_objects as go
|
| 10 |
import matplotlib.pyplot as plt
|
| 11 |
+
from scipy import stats as scipy_stats
|
| 12 |
from statsmodels.tsa.stattools import adfuller, kpss, acf as sm_acf, pacf as sm_pacf
|
| 13 |
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
|
| 14 |
from statsmodels.tsa.seasonal import seasonal_decompose
|
| 15 |
from statsmodels.stats.outliers_influence import variance_inflation_factor
|
| 16 |
from statsmodels.tools import add_constant
|
| 17 |
|
| 18 |
+
st.set_page_config(page_title="Анализ временных рядов", layout="wide", initial_sidebar_state="expanded")
|
| 19 |
MOSCOW = pytz.timezone("Europe/Moscow")
|
| 20 |
|
| 21 |
+
# Импорт функций для ЛР №2
|
| 22 |
+
import sys
|
| 23 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 24 |
+
try:
|
| 25 |
+
from lab2_functions import (
|
| 26 |
+
create_advanced_features, apply_transformations, apply_boxcox_transform, inverse_boxcox_transform,
|
| 27 |
+
inverse_transformations, recursive_forecast, direct_forecast, hybrid_forecast, create_exponential_smoothing_model,
|
| 28 |
+
evaluate_forecast, naive_forecast, time_series_cv_sliding_window, time_series_cv_expanding_window,
|
| 29 |
+
diagnose_model_residuals, calculate_mape
|
| 30 |
+
)
|
| 31 |
+
LAB2_AVAILABLE = True
|
| 32 |
+
except ImportError as e:
|
| 33 |
+
LAB2_AVAILABLE = False
|
| 34 |
+
st.warning(f"Функции ЛР №2 недоступны: {e}")
|
| 35 |
+
|
| 36 |
+
# Навигация между лабораторными работами
|
| 37 |
+
st.sidebar.title("🧪 Лабораторные работы")
|
| 38 |
+
lab_choice = st.sidebar.radio(
|
| 39 |
+
"Выберите лабораторную работу:",
|
| 40 |
+
["ЛР №1: Введение в анализ временных рядов", "ЛР №2: Прогнозирование временных рядов"],
|
| 41 |
+
index=0
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
|
| 45 |
# ---------------- Utilities ----------------
|
| 46 |
def detect_date_column(df: pd.DataFrame) -> Optional[str]:
|
|
|
|
| 359 |
return html
|
| 360 |
|
| 361 |
|
| 362 |
+
# ---------------- Функция для отображения ЛР №1 ----------------
|
| 363 |
+
def render_lab1():
|
| 364 |
+
st.title("🧪 Лабораторная работа №1: Введение в анализ временных рядов")
|
| 365 |
+
st.markdown("**Этапы:** Сбор, очистка, визуализация и диагностика многомерных данных")
|
| 366 |
|
| 367 |
+
# Sidebar
|
| 368 |
+
st.sidebar.header("Настройки")
|
| 369 |
+
uploaded_file = st.sidebar.file_uploader("Загрузите CSV/Parquet", type=['csv', 'parquet'])
|
| 370 |
|
| 371 |
+
# small built-in example option (uses local file if present)
|
| 372 |
+
sample_option = None
|
| 373 |
+
if os.path.exists('russia_covid_dataset.csv'):
|
| 374 |
+
sample_option = 'russia_covid_dataset.csv'
|
| 375 |
+
sample_choice = st.sidebar.selectbox('Или выбрать предзагруженный пример', options=[None, sample_option] if sample_option else [None])
|
| 376 |
|
| 377 |
+
tz_assume = st.sidebar.selectbox("Как трактовать tz-naive метки?",
|
| 378 |
options=['local', 'utc', 'keep'], index=0,
|
| 379 |
format_func=lambda x: {'local': 'локально (Europe/Moscow)', 'utc': 'UTC->Moscow', 'keep': 'не трогать'}[x])
|
| 380 |
+
numeric_missing_strategy = st.sidebar.selectbox("Заполнение пропусков (числ.)", options=['interpolate', 'drop', 'rolling'], index=0)
|
| 381 |
+
cat_missing_strategy = st.sidebar.selectbox("Заполнение пропусков (категор.)", options=['mode', 'unknown'], index=0)
|
| 382 |
+
outlier_strategy = st.sidebar.selectbox("Обработка выбросов", options=['interpolate', 'winsorize', 'drop', 'mark'], index=0)
|
| 383 |
+
resample_freq = st.sidebar.selectbox("Ресемплить к частоте (если нужно)", options=[None, 'D', 'W', 'M'], index=1)
|
| 384 |
|
| 385 |
+
# load dataset and persist
|
| 386 |
+
if 'df_in' not in st.session_state:
|
| 387 |
+
st.session_state['df_in'] = None
|
| 388 |
|
| 389 |
+
if uploaded_file is not None:
|
| 390 |
+
try:
|
| 391 |
+
if uploaded_file.name.endswith('.parquet'):
|
| 392 |
+
df_in = pd.read_parquet(uploaded_file)
|
| 393 |
+
else:
|
| 394 |
+
df_in = pd.read_csv(uploaded_file, low_memory=False)
|
| 395 |
+
st.session_state['df_in'] = df_in
|
| 396 |
+
st.success(f"Загружен файл: {uploaded_file.name} ({df_in.shape[0]}×{df_in.shape[1]})")
|
| 397 |
+
except Exception as e:
|
| 398 |
+
st.error(f"Ошибка загрузки: {e}")
|
| 399 |
+
st.stop()
|
| 400 |
+
elif sample_choice:
|
| 401 |
+
st.session_state['df_in'] = pd.read_csv(sample_choice, low_memory=False)
|
| 402 |
+
st.info(f"Выбран пример: {sample_choice}")
|
| 403 |
+
else:
|
| 404 |
+
local_path = 'russia_covid_dataset.csv'
|
| 405 |
+
if st.session_state['df_in'] is None and os.path.exists(local_path):
|
| 406 |
+
st.session_state['df_in'] = pd.read_csv(local_path, low_memory=False)
|
| 407 |
+
st.info(f"Авто-загружен локальный файл {local_path}")
|
| 408 |
+
elif st.session_state['df_in'] is None:
|
| 409 |
+
st.info("Загрузите файл или поместите russia_covid_dataset.csv в рабочую папку.")
|
| 410 |
+
st.stop()
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
df_in = st.session_state['df_in']
|
| 414 |
+
st.subheader("Preview входного датасета")
|
| 415 |
+
st.dataframe(df_in.head(8))
|
| 416 |
+
|
| 417 |
+
# detect date column
|
| 418 |
+
detected = detect_date_column(df_in)
|
| 419 |
+
col_for_date = st.text_input("Колонка с временной меткой", value=detected if detected else "")
|
| 420 |
+
if not col_for_date:
|
| 421 |
+
st.error("Укажите колонку с временной меткой.")
|
| 422 |
st.stop()
|
| 423 |
|
| 424 |
+
# Run buttons
|
| 425 |
+
col1, col2 = st.columns([1, 1])
|
| 426 |
+
with col1:
|
| 427 |
+
run_btn = st.button("Run Preprocessing")
|
| 428 |
+
with col2:
|
| 429 |
+
force_btn = st.button("Force Recompute (пересчитать)")
|
| 430 |
+
|
| 431 |
+
# session keys
|
| 432 |
+
st.session_state.setdefault('preprocessed', False)
|
| 433 |
+
st.session_state.setdefault('df_clean', None)
|
| 434 |
+
st.session_state.setdefault('info', {})
|
| 435 |
+
st.session_state.setdefault('df_lags', None)
|
| 436 |
+
|
| 437 |
+
if run_btn or force_btn or (not st.session_state['preprocessed'] and st.session_state['df_clean'] is None):
|
| 438 |
+
df_clean, info = preprocess_timeseries(
|
| 439 |
+
df_in,
|
| 440 |
+
date_col=col_for_date,
|
| 441 |
+
tz_assume=tz_assume,
|
| 442 |
+
numeric_missing_strategy=numeric_missing_strategy,
|
| 443 |
+
cat_missing_strategy=cat_missing_strategy,
|
| 444 |
+
outlier_strategy=outlier_strategy,
|
| 445 |
+
resample_freq=resample_freq,
|
| 446 |
+
)
|
| 447 |
+
st.session_state['df_clean'] = df_clean
|
| 448 |
+
st.session_state['info'] = info
|
| 449 |
+
st.session_state['preprocessed'] = True
|
| 450 |
+
|
| 451 |
+
# Main UI after preprocess
|
| 452 |
+
if st.session_state.get('preprocessed'):
|
| 453 |
+
df_clean = st.session_state['df_clean']
|
| 454 |
+
info = st.session_state['info']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
|
| 456 |
st.subheader("Финальный датасет (первые строки)")
|
| 457 |
st.dataframe(df_clean.head(10))
|
|
|
|
| 841 |
except Exception:
|
| 842 |
st.info(
|
| 843 |
'PDF-конверсия недоступна (pdfkit/wkhtmltopdf не установлены). Скачайте HTML и конвертируйте локально, если нужно.')
|
| 844 |
+
|
| 845 |
+
|
| 846 |
+
# ---------------- Функция для отображения ЛР №2 ----------------
|
| 847 |
+
def render_lab2():
|
| 848 |
+
if not LAB2_AVAILABLE:
|
| 849 |
+
st.error("Функции ЛР №2 недоступны. Убедитесь, что файл lab2_functions.py существует и все зависимости установлены.")
|
| 850 |
+
return
|
| 851 |
+
|
| 852 |
+
st.title("🧪 Лабораторная работа №2: Прогнозирование временных рядов")
|
| 853 |
+
st.markdown("**Этапы:** Стратегии прогнозирования, валидация и модели экспоненциального сглаживания")
|
| 854 |
+
|
| 855 |
+
st.markdown("""
|
| 856 |
+
---
|
| 857 |
+
## 📖 Что происходит в этой работе?
|
| 858 |
+
|
| 859 |
+
**Цель:** Научиться строить модели для прогнозирования будущих значений временного ряда.
|
| 860 |
+
|
| 861 |
+
**Простыми словами:**
|
| 862 |
+
1. У вас есть данные за прошлое (например, продажи за последние 2 года)
|
| 863 |
+
2. Вы хотите предсказать, что будет в будущем (например, продажи на следующий месяц)
|
| 864 |
+
3. Для этого мы строим модели, которые "учатся" на прошлых данных и делают прогнозы
|
| 865 |
+
|
| 866 |
+
**Этапы работы:**
|
| 867 |
+
- **Этап 1:** Разбираем ряд на части (тренд, сезонность, остатки)
|
| 868 |
+
- **Этап 2:** Создаём дополнительные признаки (день недели, лаги и т.д.)
|
| 869 |
+
- **Этап 3:** Выбираем стратегию прогнозирования
|
| 870 |
+
- **Этап 4:** Проверяем качество моделей через кросс-валидацию
|
| 871 |
+
- **Этап 5:** Приводим данные к стационарному виду (убираем тренд)
|
| 872 |
+
- **Этап 6-7:** Строим модели и сравниваем их
|
| 873 |
+
- **Этап 8:** Анализируем результаты и выбираем лучшую модель
|
| 874 |
+
|
| 875 |
+
**Как работать:**
|
| 876 |
+
1. Начните с Этапа 1 - выполните декомпозицию
|
| 877 |
+
2. Перейдите к Этапу 5 - настройте преобразования (можно оставить по умолчанию)
|
| 878 |
+
3. В Этапе 6-7 нажмите кнопку "Применить преобразования и построить модели"
|
| 879 |
+
4. Посмотрите результаты в Этапе 8
|
| 880 |
+
|
| 881 |
+
---
|
| 882 |
+
""")
|
| 883 |
+
|
| 884 |
+
# Проверка наличия данных из ЛР №1
|
| 885 |
+
if 'df_clean' not in st.session_state or st.session_state['df_clean'] is None:
|
| 886 |
+
st.warning("⚠️ Сначала выполните предобработку данных в ЛР №1 или загрузите готовый датасет.")
|
| 887 |
+
uploaded_file = st.file_uploader("Загрузите предобработанный CSV/Parquet", type=['csv', 'parquet'], key='lab2_upload')
|
| 888 |
+
if uploaded_file is not None:
|
| 889 |
+
try:
|
| 890 |
+
if uploaded_file.name.endswith('.parquet'):
|
| 891 |
+
df_clean = pd.read_parquet(uploaded_file)
|
| 892 |
+
else:
|
| 893 |
+
df_clean = pd.read_csv(uploaded_file, low_memory=False)
|
| 894 |
+
if 'timestamp' not in df_clean.columns:
|
| 895 |
+
st.error("В датасете должна быть колонка 'timestamp'")
|
| 896 |
+
return
|
| 897 |
+
df_clean['timestamp'] = pd.to_datetime(df_clean['timestamp'])
|
| 898 |
+
st.session_state['df_clean'] = df_clean
|
| 899 |
+
st.success(f"Загружен файл: {uploaded_file.name}")
|
| 900 |
+
except Exception as e:
|
| 901 |
+
st.error(f"Ошибка загрузки: {e}")
|
| 902 |
+
return
|
| 903 |
+
else:
|
| 904 |
+
st.stop()
|
| 905 |
+
|
| 906 |
+
df_clean = st.session_state['df_clean'].copy()
|
| 907 |
+
numeric_cols = df_clean.select_dtypes(include=[np.number]).columns.tolist()
|
| 908 |
+
|
| 909 |
+
if len(numeric_cols) == 0:
|
| 910 |
+
st.error("Нет числовых колонок для анализа")
|
| 911 |
+
return
|
| 912 |
+
|
| 913 |
+
# Выбор целевой переменной
|
| 914 |
+
st.sidebar.header("Параметры прогнозирования")
|
| 915 |
+
target_col = st.sidebar.selectbox("Целевая переменная", options=numeric_cols, index=0)
|
| 916 |
+
horizon = st.sidebar.number_input("Горизонт прогнозирования (h)", min_value=1, max_value=365, value=7, step=1)
|
| 917 |
+
|
| 918 |
+
# Разделение на train/test
|
| 919 |
+
st.header("Этап 1: Углублённая декомпозиция и анализ остатков")
|
| 920 |
+
|
| 921 |
+
if len(df_clean) < 500:
|
| 922 |
+
st.warning(f"⚠️ Рекомендуется не менее 500 наблюдений для обучения. У вас {len(df_clean)}")
|
| 923 |
+
|
| 924 |
+
train_size = st.sidebar.number_input("Размер обучающей выборки", min_value=100, max_value=len(df_clean)-50, value=min(500, len(df_clean)-50))
|
| 925 |
+
test_size = len(df_clean) - train_size
|
| 926 |
+
|
| 927 |
+
if test_size < 50:
|
| 928 |
+
st.error(f"Тестовая выборка слишком мала ({test_size}). Уменьшите размер обучающей выборки.")
|
| 929 |
+
return
|
| 930 |
+
|
| 931 |
+
df_clean = df_clean.sort_values('timestamp').reset_index(drop=True)
|
| 932 |
+
train_data = df_clean.iloc[:train_size]
|
| 933 |
+
test_data = df_clean.iloc[train_size:]
|
| 934 |
+
|
| 935 |
+
st.info(f"Обучающая выборка: {len(train_data)} наблюдений ({train_data['timestamp'].min()} - {train_data['timestamp'].max()})")
|
| 936 |
+
st.info(f"Тестовая выборка: {len(test_data)} наблюдений ({test_data['timestamp'].min()} - {test_data['timestamp'].max()})")
|
| 937 |
+
|
| 938 |
+
# Декомпозиция
|
| 939 |
+
with st.expander("Этап 1: Декомпозиция и анализ остатков", expanded=True):
|
| 940 |
+
decomp_model = st.selectbox("Модель декомпозиции", options=['additive', 'multiplicative'], index=0, key='decomp_model')
|
| 941 |
+
decomp_period = st.number_input("Период сезонности", min_value=2, max_value=365, value=7, key='decomp_period')
|
| 942 |
+
|
| 943 |
+
if st.button("Выполнить декомпозицию", key='btn_decomp'):
|
| 944 |
+
s_train = train_data.set_index('timestamp')[target_col].dropna()
|
| 945 |
+
if len(s_train) < decomp_period * 2:
|
| 946 |
+
st.error(f"Недостаточно данных для декомпозиции (нужно >= {decomp_period * 2}, есть {len(s_train)})")
|
| 947 |
+
else:
|
| 948 |
+
try:
|
| 949 |
+
decomp = seasonal_decompose(s_train, period=int(decomp_period), model=decomp_model, extrapolate_trend='freq')
|
| 950 |
+
st.session_state['decomp'] = decomp
|
| 951 |
+
st.session_state['saved_decomp_period'] = int(decomp_period) # Сохраняем период для использования в моделях (используем другой ключ, чтобы не конфликтовать с виджетом)
|
| 952 |
+
|
| 953 |
+
comp_df = pd.DataFrame({
|
| 954 |
+
'timestamp': s_train.index,
|
| 955 |
+
'observed': decomp.observed,
|
| 956 |
+
'trend': decomp.trend,
|
| 957 |
+
'seasonal': decomp.seasonal,
|
| 958 |
+
'resid': decomp.resid
|
| 959 |
+
})
|
| 960 |
+
st.session_state['decomp_df'] = comp_df
|
| 961 |
+
|
| 962 |
+
st.subheader("Графики компонентов декомпозиции")
|
| 963 |
+
col1, col2 = st.columns(2)
|
| 964 |
+
with col1:
|
| 965 |
+
st.plotly_chart(px.line(comp_df, x='timestamp', y='observed', title='Observed'), use_container_width=True)
|
| 966 |
+
st.plotly_chart(px.line(comp_df, x='timestamp', y='trend', title='Trend'), use_container_width=True)
|
| 967 |
+
with col2:
|
| 968 |
+
st.plotly_chart(px.line(comp_df, x='timestamp', y='seasonal', title='Seasonal'), use_container_width=True)
|
| 969 |
+
st.plotly_chart(px.line(comp_df, x='timestamp', y='resid', title='Residuals'), use_container_width=True)
|
| 970 |
+
|
| 971 |
+
# Анализ остатков
|
| 972 |
+
resid = comp_df['resid'].dropna()
|
| 973 |
+
if len(resid) > 3:
|
| 974 |
+
st.subheader("Анализ остатков декомпозиции")
|
| 975 |
+
adf_r = run_adf(resid)
|
| 976 |
+
kpss_r = run_kpss(resid)
|
| 977 |
+
col1, col2 = st.columns(2)
|
| 978 |
+
with col1:
|
| 979 |
+
st.write("**ADF (остатки):**", adf_r)
|
| 980 |
+
with col2:
|
| 981 |
+
st.write("**KPSS (остатки):**", kpss_r)
|
| 982 |
+
|
| 983 |
+
# ACF/PACF остатков
|
| 984 |
+
try:
|
| 985 |
+
acf_vals, acf_conf, pacf_vals, pacf_conf = get_acf_pacf_with_conf(resid, nlags=min(40, len(resid)//4), alpha=0.05)
|
| 986 |
+
fig_acf, fig_pacf = plotly_acf_pacf(acf_vals, acf_conf, pacf_vals, pacf_conf, max_lag=min(40, len(resid)//4), title_prefix='Residuals')
|
| 987 |
+
st.plotly_chart(fig_acf, use_container_width=True)
|
| 988 |
+
st.plotly_chart(fig_pacf, use_container_width=True)
|
| 989 |
+
except Exception as e:
|
| 990 |
+
st.warning(f"Не удалось построить ACF/PACF остатков: {e}")
|
| 991 |
+
|
| 992 |
+
st.success("Декомпозиция выполнена")
|
| 993 |
+
except Exception as e:
|
| 994 |
+
st.error(f"Ошибка декомпозиции: {e}")
|
| 995 |
+
|
| 996 |
+
# Этап 2: Feature Engineering
|
| 997 |
+
with st.expander("Этап 2: Расширенный feature engineering", expanded=False):
|
| 998 |
+
if st.button("Создать расширенные признаки", key='btn_features'):
|
| 999 |
+
df_features = create_advanced_features(train_data, target_col, timestamp_col='timestamp')
|
| 1000 |
+
st.session_state['df_features'] = df_features
|
| 1001 |
+
st.success(f"Создано признаков: {len(df_features.columns)}")
|
| 1002 |
+
st.dataframe(df_features.head(10))
|
| 1003 |
+
st.download_button("Скачать датасет с признаками", data=df_features.to_csv(index=False).encode('utf-8'), file_name='dataset_with_features.csv', mime='text/csv')
|
| 1004 |
+
|
| 1005 |
+
# Этап 3: Стратегии прогнозирования
|
| 1006 |
+
st.header("Этап 3: Стратегии многопшагового прогнозирования")
|
| 1007 |
+
|
| 1008 |
+
# Этап 4: Кросс-валидация
|
| 1009 |
+
with st.expander("Этап 4: Кросс-валидация для временных рядов", expanded=False):
|
| 1010 |
+
cv_method = st.selectbox("Метод кросс-валидации", options=['sliding_window', 'expanding_window', 'TimeSeriesSplit'], index=0)
|
| 1011 |
+
cv_train_size = st.number_input("Размер обучающей выборки для CV", min_value=50, max_value=train_size, value=min(300, train_size), key='cv_train_size')
|
| 1012 |
+
cv_test_size = st.number_input("Размер тестовой выборки для CV", min_value=10, max_value=100, value=30, key='cv_test_size')
|
| 1013 |
+
cv_step = st.number_input("Шаг для CV", min_value=1, max_value=50, value=10, key='cv_step')
|
| 1014 |
+
|
| 1015 |
+
if st.button("Выполнить кросс-валидацию", key='btn_cv'):
|
| 1016 |
+
s_full = df_clean.set_index('timestamp')[target_col].dropna()
|
| 1017 |
+
|
| 1018 |
+
# Функция-обёртка для создания модели
|
| 1019 |
+
def create_model_wrapper(data, **kwargs):
|
| 1020 |
+
return create_exponential_smoothing_model(data, trend='add', seasonal=None, optimized=True)
|
| 1021 |
+
|
| 1022 |
+
try:
|
| 1023 |
+
if cv_method == 'sliding_window':
|
| 1024 |
+
cv_results = time_series_cv_sliding_window(
|
| 1025 |
+
create_model_wrapper, s_full, train_size=cv_train_size,
|
| 1026 |
+
test_size=cv_test_size, horizon=horizon, step=cv_step
|
| 1027 |
+
)
|
| 1028 |
+
elif cv_method == 'expanding_window':
|
| 1029 |
+
cv_results = time_series_cv_expanding_window(
|
| 1030 |
+
create_model_wrapper, s_full, initial_train_size=cv_train_size,
|
| 1031 |
+
test_size=cv_test_size, horizon=horizon, step=cv_step
|
| 1032 |
+
)
|
| 1033 |
+
else: # TimeSeriesSplit
|
| 1034 |
+
from sklearn.model_selection import TimeSeriesSplit
|
| 1035 |
+
tscv = TimeSeriesSplit(n_splits=min(5, (len(s_full) - cv_train_size) // cv_test_size))
|
| 1036 |
+
cv_results = []
|
| 1037 |
+
for fold, (train_idx, test_idx) in enumerate(tscv.split(s_full), 1):
|
| 1038 |
+
train_cv = s_full.iloc[train_idx]
|
| 1039 |
+
test_cv = s_full.iloc[test_idx]
|
| 1040 |
+
try:
|
| 1041 |
+
model = create_model_wrapper(train_cv)
|
| 1042 |
+
forecast = model.forecast(steps=min(horizon, len(test_cv)))
|
| 1043 |
+
metrics = evaluate_forecast(test_cv.values[:len(forecast)], forecast)
|
| 1044 |
+
metrics['fold'] = fold
|
| 1045 |
+
cv_results.append(metrics)
|
| 1046 |
+
except Exception as e:
|
| 1047 |
+
st.warning(f"Ошибка в фолде {fold}: {e}")
|
| 1048 |
+
|
| 1049 |
+
if cv_results:
|
| 1050 |
+
cv_df = pd.DataFrame(cv_results)
|
| 1051 |
+
st.subheader("Результаты кросс-валидации")
|
| 1052 |
+
st.dataframe(cv_df)
|
| 1053 |
+
|
| 1054 |
+
# Средние метрики
|
| 1055 |
+
st.subheader("Средние метрики по фолдам")
|
| 1056 |
+
avg_metrics = cv_df[['MAE', 'RMSE', 'MAPE']].mean()
|
| 1057 |
+
st.dataframe(avg_metrics.to_frame('Среднее значение'))
|
| 1058 |
+
|
| 1059 |
+
# Визуализация метрик по фолдам
|
| 1060 |
+
fig_cv = go.Figure()
|
| 1061 |
+
fig_cv.add_trace(go.Scatter(x=cv_df['fold'], y=cv_df['MAE'], name='MAE', mode='lines+markers'))
|
| 1062 |
+
fig_cv.add_trace(go.Scatter(x=cv_df['fold'], y=cv_df['RMSE'], name='RMSE', mode='lines+markers'))
|
| 1063 |
+
fig_cv.update_layout(title='Метрики по фолдам кросс-валидации', xaxis_title='Фолд', yaxis_title='Значение метрики')
|
| 1064 |
+
st.plotly_chart(fig_cv, use_container_width=True)
|
| 1065 |
+
|
| 1066 |
+
st.session_state['cv_results'] = cv_results
|
| 1067 |
+
except Exception as e:
|
| 1068 |
+
st.error(f"Ошибка кросс-валидации: {e}")
|
| 1069 |
+
import traceback
|
| 1070 |
+
st.code(traceback.format_exc())
|
| 1071 |
+
|
| 1072 |
+
# Этап 5: Преобразования к стационарности
|
| 1073 |
+
st.header("📊 Этап 5: Приведение к стационарности и преобразования")
|
| 1074 |
+
st.markdown("""
|
| 1075 |
+
**Что это значит?**
|
| 1076 |
+
|
| 1077 |
+
Временные ряды часто имеют тренд (растут или падают) и меняющуюся дисперсию.
|
| 1078 |
+
Многие модели требуют стационарных данных (без тренда, с постоянной дисперсией).
|
| 1079 |
+
|
| 1080 |
+
**Что нужно сделать:**
|
| 1081 |
+
1. Выберите тип преобразования (или оставьте 'none' для начала)
|
| 1082 |
+
2. Если нужно, укажите порядок дифференцирования (обычно 1)
|
| 1083 |
+
3. Нажмите кнопку "Применить преобразования и построить модели" в следующем разделе
|
| 1084 |
+
""")
|
| 1085 |
+
|
| 1086 |
+
with st.expander("⚙️ Настройки преобразований", expanded=True):
|
| 1087 |
+
transform_type = st.selectbox(
|
| 1088 |
+
"Тип преобразования",
|
| 1089 |
+
options=['none', 'log', 'boxcox'],
|
| 1090 |
+
index=0,
|
| 1091 |
+
key='transform_type',
|
| 1092 |
+
help="none = без преобразования, log = логарифм (для стабилизации дисперсии), boxcox = автоматический подбор преобразования"
|
| 1093 |
+
)
|
| 1094 |
+
lambda_param = None
|
| 1095 |
+
if transform_type == 'boxcox':
|
| 1096 |
+
lambda_param = st.number_input(
|
| 1097 |
+
"Lambda для Бокса-Кокса (0 = авто)",
|
| 1098 |
+
min_value=-5.0, max_value=5.0, value=0.0, step=0.1,
|
| 1099 |
+
key='lambda_param',
|
| 1100 |
+
help="Оставьте 0 для автоматического подбора оптимального значения"
|
| 1101 |
+
)
|
| 1102 |
+
if lambda_param == 0.0:
|
| 1103 |
+
lambda_param = None
|
| 1104 |
+
|
| 1105 |
+
diff_order = st.number_input(
|
| 1106 |
+
"Порядок дифференцирования",
|
| 1107 |
+
min_value=0, max_value=3, value=0,
|
| 1108 |
+
key='diff_order',
|
| 1109 |
+
help="0 = без дифференцирования, 1 = первая разность (убирает тренд), 2 = вторая разность"
|
| 1110 |
+
)
|
| 1111 |
+
seasonal_diff = st.number_input(
|
| 1112 |
+
"Сезонное дифференцирование (период, 0 = отключено)",
|
| 1113 |
+
min_value=0, max_value=365, value=0,
|
| 1114 |
+
key='seasonal_diff',
|
| 1115 |
+
help="Укажите период сезонности (например, 7 для недельной, 30 для месячной). 0 = отключено"
|
| 1116 |
+
)
|
| 1117 |
+
|
| 1118 |
+
st.info("""
|
| 1119 |
+
💡 **Рекомендации для улучшения прогноза:**
|
| 1120 |
+
|
| 1121 |
+
1. **Если наивный прогноз лучше моделей:**
|
| 1122 |
+
- Попробуйте добавить сезонность: выберите модели Holt-Winters и укажите период сезонности
|
| 1123 |
+
- Используйте период из декомпозиции (Этап 1)
|
| 1124 |
+
- Попробуйте diff_order=1 для устранения тренда
|
| 1125 |
+
- Попробуйте преобразование Бокса-Кокса для стабилизации дисперсии
|
| 1126 |
+
|
| 1127 |
+
2. **Для данных с сезонностью:**
|
| 1128 |
+
- Обязательно используйте модели Holt-Winters
|
| 1129 |
+
- Период сезонности должен совпадать с периодом из декомпозиции
|
| 1130 |
+
|
| 1131 |
+
3. **Для данных с трендом:**
|
| 1132 |
+
- Используйте diff_order=1
|
| 1133 |
+
- Или выберите модели с трендом (Holt_add, Holt_mul)
|
| 1134 |
+
|
| 1135 |
+
4. **Начните с простого:** 'none' и diff_order=0, затем постепенно добавляйте сложность
|
| 1136 |
+
""")
|
| 1137 |
+
|
| 1138 |
+
# Этап 6-7: Модели и стратегии
|
| 1139 |
+
st.header("🔮 Этап 6-7: Модели экспоненциального сглаживания и стратегии прогнозирования")
|
| 1140 |
+
st.markdown("""
|
| 1141 |
+
**Что здесь происходит?**
|
| 1142 |
+
|
| 1143 |
+
Здесь мы строим модели для прогнозирования будущих значений временного ряда.
|
| 1144 |
+
|
| 1145 |
+
**Стратегии прогнозирования:**
|
| 1146 |
+
- **recursive (рекурсивная):** Одна модель, которая использует свои предыдущие прогнозы
|
| 1147 |
+
- **direct (прямая):** Отдельная модель для каждого шага вперёд
|
| 1148 |
+
- **hybrid (гибридная):** Комбинация рекурсивной и прямой
|
| 1149 |
+
|
| 1150 |
+
**Модели:**
|
| 1151 |
+
- **SES:** Простое экспоненциальное сглаживание (без тренда и сезонности)
|
| 1152 |
+
- **Holt_add:** Модель Хольта с аддитивным трендом (линейный рост/падение, без сезонности)
|
| 1153 |
+
- **Holt_mul:** Модель Хольта с мультипликативным трендом (экспоненциальный рост/падение, без сезонности)
|
| 1154 |
+
- **Holt-Winters_add:** Модель Хольта-Винтерса с аддитивным трендом и сезонностью (рекомендуется для данных с сезонностью!)
|
| 1155 |
+
- **Holt-Winters_mul:** Модель Хольта-Винтерса с мультипликативным трендом и сезонностью
|
| 1156 |
+
""")
|
| 1157 |
+
|
| 1158 |
+
strategy_choice = st.multiselect(
|
| 1159 |
+
"Выберите стратегии прогнозирования",
|
| 1160 |
+
options=['recursive', 'direct', 'hybrid'],
|
| 1161 |
+
default=['recursive'],
|
| 1162 |
+
key='strategy_choice',
|
| 1163 |
+
help="Можно выбрать несколько для сравнения"
|
| 1164 |
+
)
|
| 1165 |
+
|
| 1166 |
+
model_types = st.multiselect(
|
| 1167 |
+
"Выберите модели",
|
| 1168 |
+
options=['SES', 'Holt_add', 'Holt_mul', 'Holt-Winters_add', 'Holt-Winters_mul'],
|
| 1169 |
+
default=['Holt_add', 'Holt-Winters_add'],
|
| 1170 |
+
key='model_types',
|
| 1171 |
+
help="Можно выбрать несколько для сравнения. Holt-Winters учитывает сезонность."
|
| 1172 |
+
)
|
| 1173 |
+
|
| 1174 |
+
# Настройка сезонности для моделей Holt-Winters
|
| 1175 |
+
use_seasonal = any('Holt-Winters' in m for m in model_types)
|
| 1176 |
+
seasonal_period = None
|
| 1177 |
+
|
| 1178 |
+
# Пытаемся автоматически определить период из декомпозиции
|
| 1179 |
+
default_seasonal_period = 7
|
| 1180 |
+
if 'saved_decomp_period' in st.session_state:
|
| 1181 |
+
default_seasonal_period = st.session_state.get('saved_decomp_period', 7)
|
| 1182 |
+
elif 'decomp' in st.session_state:
|
| 1183 |
+
# Пытаемся извлечь период из декомпозиции
|
| 1184 |
+
try:
|
| 1185 |
+
decomp = st.session_state.get('decomp')
|
| 1186 |
+
if hasattr(decomp, 'seasonal') and len(decomp.seasonal) > 0:
|
| 1187 |
+
# Пытаемся определить период по длине сезонной компоненты
|
| 1188 |
+
seasonal_len = len(decomp.seasonal.dropna())
|
| 1189 |
+
# Округляем до ближайшего разумного значения
|
| 1190 |
+
if 6 <= seasonal_len <= 8:
|
| 1191 |
+
default_seasonal_period = 7
|
| 1192 |
+
elif 28 <= seasonal_len <= 32:
|
| 1193 |
+
default_seasonal_period = 30
|
| 1194 |
+
elif 360 <= seasonal_len <= 370:
|
| 1195 |
+
default_seasonal_period = 365
|
| 1196 |
+
else:
|
| 1197 |
+
default_seasonal_period = min(seasonal_len, 365)
|
| 1198 |
+
except:
|
| 1199 |
+
pass
|
| 1200 |
+
|
| 1201 |
+
if use_seasonal:
|
| 1202 |
+
seasonal_period = st.number_input(
|
| 1203 |
+
"Период сезонности для моделей Holt-Winters",
|
| 1204 |
+
min_value=2,
|
| 1205 |
+
max_value=365,
|
| 1206 |
+
value=int(default_seasonal_period),
|
| 1207 |
+
key='seasonal_period',
|
| 1208 |
+
help=f"Используйте период из декомпозиции (например, 7 для недельной, 30 для месячной). Автоопределено: {default_seasonal_period}"
|
| 1209 |
+
)
|
| 1210 |
+
if 'saved_decomp_period' in st.session_state:
|
| 1211 |
+
st.info(f"💡 Подсказка: В декомпозиции использовался период {st.session_state.get('saved_decomp_period')}. Рекомендуется использовать тот же пери��д.")
|
| 1212 |
+
|
| 1213 |
+
# Настройка доверительных интервалов
|
| 1214 |
+
use_conf_int = st.sidebar.checkbox("Показать доверительные интервалы", value=False, key='use_conf_int')
|
| 1215 |
+
conf_alpha = st.sidebar.slider("Уровень значимости для доверительных интервалов", min_value=0.01, max_value=0.5, value=0.05, step=0.01, key='conf_alpha') if use_conf_int else None
|
| 1216 |
+
|
| 1217 |
+
if st.button("Применить преобразования и построить модели", key='btn_models'):
|
| 1218 |
+
s_train = train_data.set_index('timestamp')[target_col].dropna()
|
| 1219 |
+
s_test = test_data.set_index('timestamp')[target_col].dropna()
|
| 1220 |
+
|
| 1221 |
+
# Сохраняем исходные данные для обратного преобразования
|
| 1222 |
+
s_train_original = s_train.copy()
|
| 1223 |
+
s_test_original = s_test.copy()
|
| 1224 |
+
|
| 1225 |
+
# Применяем преобразования
|
| 1226 |
+
try:
|
| 1227 |
+
# Проверяем наличие неположительных значений для log и boxcox
|
| 1228 |
+
has_nonpositive = (s_train <= 0).any()
|
| 1229 |
+
negative_count = (s_train <= 0).sum() if has_nonpositive else 0
|
| 1230 |
+
min_value = float(s_train.min())
|
| 1231 |
+
max_value = float(s_train.max())
|
| 1232 |
+
|
| 1233 |
+
# АВТОМАТИЧЕСКИЙ СДВИГ - если есть неположительные значения и нужен log/boxcox
|
| 1234 |
+
if (transform_type == 'log' or transform_type == 'boxcox') and has_nonpositive:
|
| 1235 |
+
shift_value = abs(min_value) + 1 # Сдвигаем так, чтобы минимум стал 1
|
| 1236 |
+
|
| 1237 |
+
# Автоматически применяем сдвиг БЕЗ ВСЯКИХ КНОПОК
|
| 1238 |
+
s_train = s_train + shift_value
|
| 1239 |
+
s_test = s_test + shift_value
|
| 1240 |
+
s_train_original = s_train_original + shift_value
|
| 1241 |
+
s_test_original = s_test_original + shift_value
|
| 1242 |
+
|
| 1243 |
+
st.info(f"✅ Автоматически применен сдвиг: +{shift_value:.2f} (было {negative_count} нулевых значений)")
|
| 1244 |
+
|
| 1245 |
+
s_train_transformed, transform_info = apply_transformations(
|
| 1246 |
+
s_train, transformation=transform_type, lambda_param=lambda_param,
|
| 1247 |
+
diff_order=diff_order, seasonal_diff=seasonal_diff if seasonal_diff > 0 else None
|
| 1248 |
+
)
|
| 1249 |
+
|
| 1250 |
+
st.info(f"Применено преобразование: {transform_info}")
|
| 1251 |
+
|
| 1252 |
+
# Проверка стационарности после преобразования
|
| 1253 |
+
st.subheader("Проверка стационарности после преобразования")
|
| 1254 |
+
adf_res = run_adf(s_train_transformed)
|
| 1255 |
+
kpss_res = run_kpss(s_train_transformed)
|
| 1256 |
+
col1, col2 = st.columns(2)
|
| 1257 |
+
with col1:
|
| 1258 |
+
st.write("**ADF:**", adf_res)
|
| 1259 |
+
with col2:
|
| 1260 |
+
st.write("**KPSS:**", kpss_res)
|
| 1261 |
+
|
| 1262 |
+
# Модели экспоненциального сглаживания
|
| 1263 |
+
st.subheader("Результаты моделей")
|
| 1264 |
+
|
| 1265 |
+
all_results = []
|
| 1266 |
+
all_forecasts = {}
|
| 1267 |
+
all_forecasts_transformed = {} # Прогнозы в преобразованном пространстве
|
| 1268 |
+
all_conf_intervals = {} # Доверительные интервалы
|
| 1269 |
+
all_models = {}
|
| 1270 |
+
|
| 1271 |
+
# Функция-обёртка для стратегий
|
| 1272 |
+
def create_ses_model(data):
|
| 1273 |
+
return create_exponential_smoothing_model(data, trend=None, seasonal=None, optimized=True)
|
| 1274 |
+
|
| 1275 |
+
def create_holt_add_model(data):
|
| 1276 |
+
return create_exponential_smoothing_model(data, trend='add', seasonal=None, optimized=True)
|
| 1277 |
+
|
| 1278 |
+
def create_holt_mul_model(data):
|
| 1279 |
+
return create_exponential_smoothing_model(data, trend='mul', seasonal=None, optimized=True)
|
| 1280 |
+
|
| 1281 |
+
def create_hw_add_model(data):
|
| 1282 |
+
return create_exponential_smoothing_model(
|
| 1283 |
+
data,
|
| 1284 |
+
trend='add',
|
| 1285 |
+
seasonal='add',
|
| 1286 |
+
seasonal_periods=seasonal_period if seasonal_period and seasonal_period > 1 else None,
|
| 1287 |
+
optimized=True
|
| 1288 |
+
)
|
| 1289 |
+
|
| 1290 |
+
def create_hw_mul_model(data):
|
| 1291 |
+
return create_exponential_smoothing_model(
|
| 1292 |
+
data,
|
| 1293 |
+
trend='mul',
|
| 1294 |
+
seasonal='mul',
|
| 1295 |
+
seasonal_periods=seasonal_period if seasonal_period and seasonal_period > 1 else None,
|
| 1296 |
+
optimized=True
|
| 1297 |
+
)
|
| 1298 |
+
|
| 1299 |
+
for model_name in model_types:
|
| 1300 |
+
if model_name == 'SES':
|
| 1301 |
+
model_func = create_ses_model
|
| 1302 |
+
model_display = 'SES'
|
| 1303 |
+
elif model_name == 'Holt_add':
|
| 1304 |
+
model_func = create_holt_add_model
|
| 1305 |
+
model_display = 'Holt (additive)'
|
| 1306 |
+
elif model_name == 'Holt_mul':
|
| 1307 |
+
if not (s_train_transformed > 0).all():
|
| 1308 |
+
st.warning("Holt multiplicative требует положительные значения - пропущено")
|
| 1309 |
+
continue
|
| 1310 |
+
model_func = create_holt_mul_model
|
| 1311 |
+
model_display = 'Holt (multiplicative)'
|
| 1312 |
+
elif model_name == 'Holt-Winters_add':
|
| 1313 |
+
if seasonal_period is None or seasonal_period < 2:
|
| 1314 |
+
st.warning(f"Holt-Winters требует период сезонности >= 2. Пропущено {model_name}")
|
| 1315 |
+
continue
|
| 1316 |
+
if len(s_train_transformed) < seasonal_period * 2:
|
| 1317 |
+
st.warning(f"Holt-Winters требует минимум {seasonal_period * 2} наблюдений. У вас {len(s_train_transformed)}. Пропущено {model_name}")
|
| 1318 |
+
continue
|
| 1319 |
+
model_func = create_hw_add_model
|
| 1320 |
+
model_display = f'Holt-Winters (additive, period={seasonal_period})'
|
| 1321 |
+
elif model_name == 'Holt-Winters_mul':
|
| 1322 |
+
if not (s_train_transformed > 0).all():
|
| 1323 |
+
st.warning("Holt-Winters multiplicative требует положительные значения - пропущено")
|
| 1324 |
+
continue
|
| 1325 |
+
if seasonal_period is None or seasonal_period < 2:
|
| 1326 |
+
st.warning(f"Holt-Winters требует период сезонности >= 2. Пропущено {model_name}")
|
| 1327 |
+
continue
|
| 1328 |
+
if len(s_train_transformed) < seasonal_period * 2:
|
| 1329 |
+
st.warning(f"Holt-Winters требует минимум {seasonal_period * 2} наблюдений. У вас {len(s_train_transformed)}. Пропущено {model_name}")
|
| 1330 |
+
continue
|
| 1331 |
+
model_func = create_hw_mul_model
|
| 1332 |
+
model_display = f'Holt-Winters (multiplicative, period={seasonal_period})'
|
| 1333 |
+
else:
|
| 1334 |
+
continue
|
| 1335 |
+
|
| 1336 |
+
for strategy in strategy_choice:
|
| 1337 |
+
try:
|
| 1338 |
+
alpha_param = conf_alpha if use_conf_int else None
|
| 1339 |
+
if strategy == 'recursive':
|
| 1340 |
+
forecast_result = recursive_forecast(model_func, s_train_transformed, horizon=min(horizon, len(s_test)), alpha=alpha_param)
|
| 1341 |
+
elif strategy == 'direct':
|
| 1342 |
+
forecast_result = direct_forecast(model_func, s_train_transformed, horizon=min(horizon, len(s_test)), alpha=alpha_param)
|
| 1343 |
+
elif strategy == 'hybrid':
|
| 1344 |
+
forecast_result = hybrid_forecast(model_func, s_train_transformed, horizon=min(horizon, len(s_test)), alpha=alpha_param)
|
| 1345 |
+
else:
|
| 1346 |
+
continue
|
| 1347 |
+
|
| 1348 |
+
# Извлекаем прогноз и доверительные интервалы
|
| 1349 |
+
# Функции возвращают либо (forecast, None), либо (forecast, (lower, upper))
|
| 1350 |
+
if isinstance(forecast_result, tuple) and len(forecast_result) == 2:
|
| 1351 |
+
forecast_transformed, conf_int = forecast_result
|
| 1352 |
+
# Если conf_int это кортеж из двух массивов, оставляем как есть
|
| 1353 |
+
# Если это None, оставляем None
|
| 1354 |
+
else:
|
| 1355 |
+
# Если функция вернула просто массив (старый формат)
|
| 1356 |
+
forecast_transformed = forecast_result
|
| 1357 |
+
conf_int = None
|
| 1358 |
+
|
| 1359 |
+
# Применяем обратное преобразование к прогнозу
|
| 1360 |
+
if transform_info.get('transformation') != 'none' or diff_order > 0 or (seasonal_diff and seasonal_diff > 0):
|
| 1361 |
+
# Для обратного преобразования нужны последние значения преобразованного ряда
|
| 1362 |
+
# Промежуточные значения уже сохранены в transform_info
|
| 1363 |
+
last_train_vals_transformed = s_train_transformed.values
|
| 1364 |
+
|
| 1365 |
+
forecast = inverse_transformations(
|
| 1366 |
+
forecast_transformed,
|
| 1367 |
+
last_train_vals_transformed,
|
| 1368 |
+
transform_info
|
| 1369 |
+
)
|
| 1370 |
+
|
| 1371 |
+
# Применяем обратное преобразование к доверительным интервалам, если они есть
|
| 1372 |
+
if conf_int is not None:
|
| 1373 |
+
conf_lower_transformed = conf_int[0]
|
| 1374 |
+
conf_upper_transformed = conf_int[1]
|
| 1375 |
+
conf_lower = inverse_transformations(
|
| 1376 |
+
conf_lower_transformed,
|
| 1377 |
+
last_train_vals_transformed,
|
| 1378 |
+
transform_info
|
| 1379 |
+
)
|
| 1380 |
+
conf_upper = inverse_transformations(
|
| 1381 |
+
conf_upper_transformed,
|
| 1382 |
+
last_train_vals_transformed,
|
| 1383 |
+
transform_info
|
| 1384 |
+
)
|
| 1385 |
+
conf_int = (conf_lower, conf_upper)
|
| 1386 |
+
else:
|
| 1387 |
+
forecast = forecast_transformed
|
| 1388 |
+
|
| 1389 |
+
# Оцениваем метрики в исходных единицах
|
| 1390 |
+
test_values = s_test_original.values[:len(forecast)]
|
| 1391 |
+
metrics = evaluate_forecast(test_values, forecast)
|
| 1392 |
+
metrics['model'] = model_display
|
| 1393 |
+
metrics['strategy'] = strategy
|
| 1394 |
+
all_results.append(metrics)
|
| 1395 |
+
all_forecasts[f"{model_display}_{strategy}"] = forecast
|
| 1396 |
+
all_forecasts_transformed[f"{model_display}_{strategy}"] = forecast_transformed
|
| 1397 |
+
if conf_int is not None:
|
| 1398 |
+
all_conf_intervals[f"{model_display}_{strategy}"] = conf_int
|
| 1399 |
+
|
| 1400 |
+
# Сохраняем модель для диагностики
|
| 1401 |
+
fitted_model = model_func(s_train_transformed)
|
| 1402 |
+
all_models[f"{model_display}_{strategy}"] = fitted_model
|
| 1403 |
+
|
| 1404 |
+
except Exception as e:
|
| 1405 |
+
st.warning(f"{model_display} ({strategy}): {e}")
|
| 1406 |
+
import traceback
|
| 1407 |
+
st.code(traceback.format_exc())
|
| 1408 |
+
|
| 1409 |
+
# Наивный прогноз (в исходных единицах)
|
| 1410 |
+
naive_pred = naive_forecast(s_train_original, min(horizon, len(s_test)))
|
| 1411 |
+
naive_metrics = evaluate_forecast(s_test_original.values[:len(naive_pred)], naive_pred)
|
| 1412 |
+
naive_metrics['model'] = 'Naive'
|
| 1413 |
+
naive_metrics['strategy'] = 'naive'
|
| 1414 |
+
all_results.append(naive_metrics)
|
| 1415 |
+
all_forecasts['Naive'] = naive_pred
|
| 1416 |
+
|
| 1417 |
+
# Сравнение моделей
|
| 1418 |
+
if all_results:
|
| 1419 |
+
st.subheader("Сравнение моделей и стратегий")
|
| 1420 |
+
results_df = pd.DataFrame(all_results)
|
| 1421 |
+
results_pivot = results_df.pivot_table(
|
| 1422 |
+
values=['MAE', 'RMSE', 'MAPE'],
|
| 1423 |
+
index='model',
|
| 1424 |
+
columns='strategy',
|
| 1425 |
+
aggfunc='first'
|
| 1426 |
+
)
|
| 1427 |
+
st.dataframe(results_pivot)
|
| 1428 |
+
|
| 1429 |
+
# Визуализация прогнозов - показываем весь ряд с прогнозами
|
| 1430 |
+
fig = go.Figure()
|
| 1431 |
+
|
| 1432 |
+
# 1. Обучающая выборка (исторические данные)
|
| 1433 |
+
fig.add_trace(go.Scatter(
|
| 1434 |
+
x=s_train_original.index,
|
| 1435 |
+
y=s_train_original.values,
|
| 1436 |
+
name='Исторические данные (train)',
|
| 1437 |
+
line=dict(color='#1f77b4', width=2),
|
| 1438 |
+
mode='lines'
|
| 1439 |
+
))
|
| 1440 |
+
|
| 1441 |
+
# 2. Тестовая выборка (фактические значения)
|
| 1442 |
+
test_len = min(horizon, len(s_test_original))
|
| 1443 |
+
fig.add_trace(go.Scatter(
|
| 1444 |
+
x=s_test_original.index[:test_len],
|
| 1445 |
+
y=s_test_original.values[:test_len],
|
| 1446 |
+
name='Фактические значения (test)',
|
| 1447 |
+
line=dict(color='black', width=3),
|
| 1448 |
+
mode='lines'
|
| 1449 |
+
))
|
| 1450 |
+
|
| 1451 |
+
# 3. Прогнозы от каждой модели
|
| 1452 |
+
colors = px.colors.qualitative.Set3
|
| 1453 |
+
color_idx = 0
|
| 1454 |
+
|
| 1455 |
+
for name, forecast in all_forecasts.items():
|
| 1456 |
+
if name == 'Naive':
|
| 1457 |
+
continue # Наивный прогноз обработаем отдельно
|
| 1458 |
+
|
| 1459 |
+
color = colors[color_idx % len(colors)]
|
| 1460 |
+
|
| 1461 |
+
# Создаём индекс�� для прогноза (продолжение после обучающей выборки)
|
| 1462 |
+
if len(forecast) > 0:
|
| 1463 |
+
# Определяем частоту временного ряда
|
| 1464 |
+
try:
|
| 1465 |
+
freq = pd.infer_freq(s_train_original.index) or pd.infer_freq(s_test_original.index) or 'D'
|
| 1466 |
+
# Создаём даты для прогноза - используем pd.date_range напрямую
|
| 1467 |
+
last_train_date = s_train_original.index[-1]
|
| 1468 |
+
# Создаём даты начиная с последней даты + 1 период
|
| 1469 |
+
# Используем periods=len(forecast)+1 и берём все кроме первого
|
| 1470 |
+
forecast_dates = pd.date_range(
|
| 1471 |
+
start=last_train_date,
|
| 1472 |
+
periods=len(forecast) + 1,
|
| 1473 |
+
freq=freq
|
| 1474 |
+
)[1:] # Берём все даты кроме первой (которая равна last_train_date)
|
| 1475 |
+
except Exception as e:
|
| 1476 |
+
# Если не удалось определить частоту, используем индексы тестовой выборки
|
| 1477 |
+
try:
|
| 1478 |
+
forecast_dates = s_test_original.index[:len(forecast)]
|
| 1479 |
+
except:
|
| 1480 |
+
# Последний вариант - создаем простой числовой индекс
|
| 1481 |
+
forecast_dates = range(len(s_train_original), len(s_train_original) + len(forecast))
|
| 1482 |
+
|
| 1483 |
+
# Добавляем доверительные интервалы, если они есть
|
| 1484 |
+
if name in all_conf_intervals:
|
| 1485 |
+
conf_lower, conf_upper = all_conf_intervals[name]
|
| 1486 |
+
|
| 1487 |
+
# Преобразуем hex цвет в RGB для rgba
|
| 1488 |
+
try:
|
| 1489 |
+
if color.startswith('#'):
|
| 1490 |
+
r = int(color[1:3], 16)
|
| 1491 |
+
g = int(color[3:5], 16)
|
| 1492 |
+
b = int(color[5:7], 16)
|
| 1493 |
+
else:
|
| 1494 |
+
r, g, b = 100, 100, 100
|
| 1495 |
+
fillcolor = f'rgba({r}, {g}, {b}, 0.15)'
|
| 1496 |
+
except:
|
| 1497 |
+
fillcolor = 'rgba(100, 100, 100, 0.15)'
|
| 1498 |
+
|
| 1499 |
+
# Верхняя граница доверительного интервала
|
| 1500 |
+
fig.add_trace(go.Scatter(
|
| 1501 |
+
x=forecast_dates,
|
| 1502 |
+
y=conf_upper,
|
| 1503 |
+
mode='lines',
|
| 1504 |
+
line=dict(width=0),
|
| 1505 |
+
showlegend=False,
|
| 1506 |
+
hoverinfo='skip',
|
| 1507 |
+
name=f'{name} CI upper'
|
| 1508 |
+
))
|
| 1509 |
+
|
| 1510 |
+
# Нижняя граница с заливкой
|
| 1511 |
+
fig.add_trace(go.Scatter(
|
| 1512 |
+
x=forecast_dates,
|
| 1513 |
+
y=conf_lower,
|
| 1514 |
+
mode='lines',
|
| 1515 |
+
line=dict(width=0),
|
| 1516 |
+
fill='tonexty',
|
| 1517 |
+
fillcolor=fillcolor,
|
| 1518 |
+
name=f'{name} (доверительный интервал)',
|
| 1519 |
+
showlegend=True,
|
| 1520 |
+
legendgroup=name
|
| 1521 |
+
))
|
| 1522 |
+
|
| 1523 |
+
# Линия прогноза
|
| 1524 |
+
fig.add_trace(go.Scatter(
|
| 1525 |
+
x=forecast_dates,
|
| 1526 |
+
y=forecast,
|
| 1527 |
+
name=f'{name} (прогноз)',
|
| 1528 |
+
line=dict(dash='dash', color=color, width=2.5),
|
| 1529 |
+
mode='lines',
|
| 1530 |
+
legendgroup=name
|
| 1531 |
+
))
|
| 1532 |
+
|
| 1533 |
+
color_idx += 1
|
| 1534 |
+
|
| 1535 |
+
# Наивный прогноз (если есть)
|
| 1536 |
+
if 'Naive' in all_forecasts:
|
| 1537 |
+
naive_forecast_vals = all_forecasts['Naive']
|
| 1538 |
+
try:
|
| 1539 |
+
freq = pd.infer_freq(s_train_original.index) or pd.infer_freq(s_test_original.index) or 'D'
|
| 1540 |
+
last_train_date = s_train_original.index[-1]
|
| 1541 |
+
# Создаём даты начиная с последней даты + 1 период
|
| 1542 |
+
naive_dates = pd.date_range(
|
| 1543 |
+
start=last_train_date,
|
| 1544 |
+
periods=len(naive_forecast_vals) + 1,
|
| 1545 |
+
freq=freq
|
| 1546 |
+
)[1:] # Берём все даты кроме первой
|
| 1547 |
+
except Exception as e:
|
| 1548 |
+
try:
|
| 1549 |
+
naive_dates = s_test_original.index[:len(naive_forecast_vals)]
|
| 1550 |
+
except:
|
| 1551 |
+
naive_dates = range(len(s_train_original), len(s_train_original) + len(naive_forecast_vals))
|
| 1552 |
+
|
| 1553 |
+
fig.add_trace(go.Scatter(
|
| 1554 |
+
x=naive_dates,
|
| 1555 |
+
y=naive_forecast_vals,
|
| 1556 |
+
name='Naive (прогноз)',
|
| 1557 |
+
line=dict(dash='dot', color='gray', width=2),
|
| 1558 |
+
mode='lines'
|
| 1559 |
+
))
|
| 1560 |
+
|
| 1561 |
+
# Вертикальная линия, разделяющая train и test
|
| 1562 |
+
if len(s_train_original) > 0:
|
| 1563 |
+
split_date = s_train_original.index[-1]
|
| 1564 |
+
# Преобразуем Timestamp в строку для plotly
|
| 1565 |
+
if isinstance(split_date, pd.Timestamp):
|
| 1566 |
+
split_date_str = split_date.strftime('%Y-%m-%d %H:%M:%S')
|
| 1567 |
+
else:
|
| 1568 |
+
split_date_str = str(split_date)
|
| 1569 |
+
|
| 1570 |
+
# Используем add_shape вместо add_vline для лучшей совместимости
|
| 1571 |
+
fig.add_shape(
|
| 1572 |
+
type="line",
|
| 1573 |
+
x0=split_date,
|
| 1574 |
+
x1=split_date,
|
| 1575 |
+
y0=0,
|
| 1576 |
+
y1=1,
|
| 1577 |
+
yref="paper",
|
| 1578 |
+
line=dict(dash="dot", color="red", width=1)
|
| 1579 |
+
)
|
| 1580 |
+
# Добавляем аннотацию отдельно
|
| 1581 |
+
fig.add_annotation(
|
| 1582 |
+
x=split_date,
|
| 1583 |
+
y=1,
|
| 1584 |
+
yref="paper",
|
| 1585 |
+
text="Разделение train/test",
|
| 1586 |
+
showarrow=False,
|
| 1587 |
+
xanchor="center",
|
| 1588 |
+
yanchor="bottom",
|
| 1589 |
+
bgcolor="rgba(255,255,255,0.8)",
|
| 1590 |
+
bordercolor="red",
|
| 1591 |
+
borderwidth=1
|
| 1592 |
+
)
|
| 1593 |
+
|
| 1594 |
+
fig.update_layout(
|
| 1595 |
+
title=f'Прогнозирование временного ряда (horizon={horizon})',
|
| 1596 |
+
height=600,
|
| 1597 |
+
xaxis_title='Дата',
|
| 1598 |
+
yaxis_title='Значение',
|
| 1599 |
+
hovermode='x unified',
|
| 1600 |
+
legend=dict(
|
| 1601 |
+
orientation="v",
|
| 1602 |
+
yanchor="top",
|
| 1603 |
+
y=1,
|
| 1604 |
+
xanchor="left",
|
| 1605 |
+
x=1.02
|
| 1606 |
+
),
|
| 1607 |
+
template='plotly_white'
|
| 1608 |
+
)
|
| 1609 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 1610 |
+
|
| 1611 |
+
# Экспорт прогнозов
|
| 1612 |
+
st.subheader("Экспорт прогнозов")
|
| 1613 |
+
export_model = st.selectbox("Выберите модель для экспорта", options=list(all_forecasts.keys()), key='export_model')
|
| 1614 |
+
if export_model in all_forecasts:
|
| 1615 |
+
forecast_export = all_forecasts[export_model]
|
| 1616 |
+
# Создаём DataFrame с прогнозами
|
| 1617 |
+
forecast_dates = pd.date_range(
|
| 1618 |
+
start=s_test_original.index[0],
|
| 1619 |
+
periods=len(forecast_export),
|
| 1620 |
+
freq=pd.infer_freq(s_test_original.index) or 'D'
|
| 1621 |
+
)
|
| 1622 |
+
forecast_df = pd.DataFrame({
|
| 1623 |
+
'date': forecast_dates,
|
| 1624 |
+
'forecast': forecast_export,
|
| 1625 |
+
'actual': s_test_original.values[:len(forecast_export)] if len(s_test_original) >= len(forecast_export) else None
|
| 1626 |
+
})
|
| 1627 |
+
forecast_csv = forecast_df.to_csv(index=False).encode('utf-8')
|
| 1628 |
+
st.download_button(
|
| 1629 |
+
f"Скачать прогноз ({export_model})",
|
| 1630 |
+
data=forecast_csv,
|
| 1631 |
+
file_name=f'forecast_{export_model.replace(" ", "_")}.csv',
|
| 1632 |
+
mime='text/csv'
|
| 1633 |
+
)
|
| 1634 |
+
|
| 1635 |
+
# Экспорт параметров модели
|
| 1636 |
+
if export_model in all_models:
|
| 1637 |
+
model = all_models[export_model]
|
| 1638 |
+
params_dict = {
|
| 1639 |
+
'model': export_model,
|
| 1640 |
+
'horizon': horizon,
|
| 1641 |
+
'transformation': transform_info.get('transformation', 'none'),
|
| 1642 |
+
'lambda': transform_info.get('lambda', None),
|
| 1643 |
+
'diff_order': transform_info.get('diff_order', 0),
|
| 1644 |
+
'seasonal_diff': transform_info.get('seasonal_diff', None),
|
| 1645 |
+
}
|
| 1646 |
+
# Добавляем параметры модели, если доступны
|
| 1647 |
+
if hasattr(model, 'params'):
|
| 1648 |
+
params_dict['model_params'] = str(model.params)
|
| 1649 |
+
if hasattr(model, 'aic'):
|
| 1650 |
+
params_dict['aic'] = model.aic
|
| 1651 |
+
if hasattr(model, 'bic'):
|
| 1652 |
+
params_dict['bic'] = model.bic
|
| 1653 |
+
|
| 1654 |
+
params_df = pd.DataFrame([params_dict])
|
| 1655 |
+
params_csv = params_df.to_csv(index=False).encode('utf-8')
|
| 1656 |
+
st.download_button(
|
| 1657 |
+
f"Скачать параметры модели ({export_model})",
|
| 1658 |
+
data=params_csv,
|
| 1659 |
+
file_name=f'model_params_{export_model.replace(" ", "_")}.csv',
|
| 1660 |
+
mime='text/csv'
|
| 1661 |
+
)
|
| 1662 |
+
|
| 1663 |
+
# Сохраняем результаты в session
|
| 1664 |
+
st.session_state['forecast_results'] = all_results
|
| 1665 |
+
st.session_state['forecasts'] = all_forecasts
|
| 1666 |
+
st.session_state['forecasts_transformed'] = all_forecasts_transformed
|
| 1667 |
+
st.session_state['models'] = all_models
|
| 1668 |
+
st.session_state['s_train_transformed'] = s_train_transformed
|
| 1669 |
+
st.session_state['s_train_original'] = s_train_original
|
| 1670 |
+
st.session_state['s_test'] = s_test_original
|
| 1671 |
+
st.session_state['transform_info'] = transform_info
|
| 1672 |
+
|
| 1673 |
+
except Exception as e:
|
| 1674 |
+
st.error(f"❌ Ошибка при построении моделей: {e}")
|
| 1675 |
+
st.info("""
|
| 1676 |
+
**Возможные причины ошибки:**
|
| 1677 |
+
1. Недостаточно данных для обучения модели
|
| 1678 |
+
2. Проблемы с преобразованиями (например, отрицательные значения при логарифме)
|
| 1679 |
+
3. Несовместимость параметров модели с данными
|
| 1680 |
+
|
| 1681 |
+
**Что попробовать:**
|
| 1682 |
+
- Уменьшите размер обучающей выборки
|
| 1683 |
+
- Измените параметры преобразований (попробуйте 'none' и diff_order=0)
|
| 1684 |
+
- Попробуйте другую модель (например, только SES)
|
| 1685 |
+
""")
|
| 1686 |
+
import traceback
|
| 1687 |
+
with st.expander("🔍 Детали ошибки (для отладки)"):
|
| 1688 |
+
st.code(traceback.format_exc())
|
| 1689 |
+
|
| 1690 |
+
# Этап 7: Диагностика остатков
|
| 1691 |
+
st.header("Этап 7: Диагностика адекватности моделей")
|
| 1692 |
+
|
| 1693 |
+
if 'models' in st.session_state and st.session_state['models']:
|
| 1694 |
+
model_for_diagnosis = st.selectbox(
|
| 1695 |
+
"Выберите модель для диагностики",
|
| 1696 |
+
options=list(st.session_state['models'].keys()),
|
| 1697 |
+
key='model_diagnosis'
|
| 1698 |
+
)
|
| 1699 |
+
|
| 1700 |
+
if st.button("Выполнить диагностику остатков", key='btn_diagnosis'):
|
| 1701 |
+
try:
|
| 1702 |
+
model = st.session_state['models'][model_for_diagnosis]
|
| 1703 |
+
s_train_transformed = st.session_state['s_train_transformed']
|
| 1704 |
+
|
| 1705 |
+
# Получаем остатки
|
| 1706 |
+
fitted_values = model.fittedvalues
|
| 1707 |
+
residuals = s_train_transformed - fitted_values
|
| 1708 |
+
residuals = residuals.dropna()
|
| 1709 |
+
|
| 1710 |
+
if len(residuals) > 3:
|
| 1711 |
+
# Диагностика
|
| 1712 |
+
diagnosis = diagnose_model_residuals(residuals.values, lags=min(20, len(residuals)//4))
|
| 1713 |
+
|
| 1714 |
+
st.subheader("Результаты диагностики остатков")
|
| 1715 |
+
|
| 1716 |
+
# Тест Льюнга-Бокса
|
| 1717 |
+
if 'ljung_box' in diagnosis:
|
| 1718 |
+
lb = diagnosis['ljung_box']
|
| 1719 |
+
if 'pvalue' in lb:
|
| 1720 |
+
st.write(f"**Тест Льюнга-Бокса:**")
|
| 1721 |
+
st.write(f"- Статистика: {lb.get('statistic', 'N/A'):.4f}")
|
| 1722 |
+
st.write(f"- p-value: {lb.get('pvalue', 'N/A'):.4f}")
|
| 1723 |
+
if lb.get('pvalue', 1) < 0.05:
|
| 1724 |
+
st.warning("Остатки имеют автокорреляцию (p < 0.05)")
|
| 1725 |
+
else:
|
| 1726 |
+
st.success("Остатки не имеют значимой автокорреляции (p >= 0.05)")
|
| 1727 |
+
|
| 1728 |
+
# Тест нормальности
|
| 1729 |
+
if 'shapiro_wilk' in diagnosis:
|
| 1730 |
+
sw = diagnosis['shapiro_wilk']
|
| 1731 |
+
st.write(f"**Тест Шапиро-Уилка (нормальность):**")
|
| 1732 |
+
st.write(f"- Статистика: {sw.get('statistic', 'N/A'):.4f}")
|
| 1733 |
+
st.write(f"- p-value: {sw.get('pvalue', 'N/A'):.4f}")
|
| 1734 |
+
if sw.get('pvalue', 0) < 0.05:
|
| 1735 |
+
st.warning("Остатки не распределены нормально (p < 0.05)")
|
| 1736 |
+
else:
|
| 1737 |
+
st.success("Остатки распределены нормально (p >= 0.05)")
|
| 1738 |
+
elif 'normality_test' in diagnosis:
|
| 1739 |
+
nt = diagnosis['normality_test']
|
| 1740 |
+
st.write(f"**Тест нормальности ({nt.get('test', 'N/A')}):**")
|
| 1741 |
+
st.write(f"- Статистика: {nt.get('statistic', 'N/A'):.4f}")
|
| 1742 |
+
st.write(f"- p-value: {nt.get('pvalue', 'N/A'):.4f}")
|
| 1743 |
+
|
| 1744 |
+
# Стационарность остатков
|
| 1745 |
+
if 'stationarity' in diagnosis:
|
| 1746 |
+
st.write(f"**Стационарность остатков:**")
|
| 1747 |
+
stn = diagnosis['stationarity']
|
| 1748 |
+
if 'adf' in stn:
|
| 1749 |
+
st.write(f"- ADF p-value: {stn['adf'].get('pvalue', 'N/A'):.4f}")
|
| 1750 |
+
if 'kpss' in stn:
|
| 1751 |
+
st.write(f"- KPSS p-value: {stn['kpss'].get('pvalue', 'N/A'):.4f}")
|
| 1752 |
+
|
| 1753 |
+
# Статистики остатков
|
| 1754 |
+
if 'residual_stats' in diagnosis:
|
| 1755 |
+
rs = diagnosis['residual_stats']
|
| 1756 |
+
st.write(f"**Статистики остатков:**")
|
| 1757 |
+
st.write(f"- Среднее: {rs.get('mean', 'N/A'):.6f}")
|
| 1758 |
+
st.write(f"- Стд. отклонение: {rs.get('std', 'N/A'):.6f}")
|
| 1759 |
+
st.write(f"- Min: {rs.get('min', 'N/A'):.4f}, Max: {rs.get('max', 'N/A'):.4f}")
|
| 1760 |
+
|
| 1761 |
+
# Визуализация остатков
|
| 1762 |
+
st.subheader("Визуализация остатков")
|
| 1763 |
+
|
| 1764 |
+
col1, col2 = st.columns(2)
|
| 1765 |
+
|
| 1766 |
+
with col1:
|
| 1767 |
+
# График остатков vs прогнозов (гомоскедастичность)
|
| 1768 |
+
fig_resid = go.Figure()
|
| 1769 |
+
fig_resid.add_trace(go.Scatter(
|
| 1770 |
+
x=fitted_values.values,
|
| 1771 |
+
y=residuals.values,
|
| 1772 |
+
mode='markers',
|
| 1773 |
+
name='Остатки'
|
| 1774 |
+
))
|
| 1775 |
+
fig_resid.add_hline(y=0, line_dash="dash", line_color="red")
|
| 1776 |
+
fig_resid.update_layout(
|
| 1777 |
+
title='Остатки vs Прогнозы (гомоскедастичность)',
|
| 1778 |
+
xaxis_title='Прогноз',
|
| 1779 |
+
yaxis_title='Остаток'
|
| 1780 |
+
)
|
| 1781 |
+
st.plotly_chart(fig_resid, use_container_width=True)
|
| 1782 |
+
|
| 1783 |
+
# Гистограмма остатков
|
| 1784 |
+
fig_hist = px.histogram(
|
| 1785 |
+
x=residuals.values,
|
| 1786 |
+
nbins=30,
|
| 1787 |
+
title='Распределение остатков'
|
| 1788 |
+
)
|
| 1789 |
+
st.plotly_chart(fig_hist, use_container_width=True)
|
| 1790 |
+
|
| 1791 |
+
with col2:
|
| 1792 |
+
# Q-Q plot
|
| 1793 |
+
qq_data = scipy_stats.probplot(residuals.values, dist="norm")
|
| 1794 |
+
fig_qq = go.Figure()
|
| 1795 |
+
fig_qq.add_trace(go.Scatter(
|
| 1796 |
+
x=qq_data[0][0],
|
| 1797 |
+
y=qq_data[0][1],
|
| 1798 |
+
mode='markers',
|
| 1799 |
+
name='Остатки'
|
| 1800 |
+
))
|
| 1801 |
+
fig_qq.add_trace(go.Scatter(
|
| 1802 |
+
x=qq_data[0][0],
|
| 1803 |
+
y=qq_data[1][1] + qq_data[1][0] * qq_data[0][0],
|
| 1804 |
+
mode='lines',
|
| 1805 |
+
name='Теоретическая линия',
|
| 1806 |
+
line=dict(color='red', dash='dash')
|
| 1807 |
+
))
|
| 1808 |
+
fig_qq.update_layout(
|
| 1809 |
+
title='Q-Q Plot (нормальность)',
|
| 1810 |
+
xaxis_title='Теоретические квантили',
|
| 1811 |
+
yaxis_title='Выборочные квантили'
|
| 1812 |
+
)
|
| 1813 |
+
st.plotly_chart(fig_qq, use_container_width=True)
|
| 1814 |
+
|
| 1815 |
+
# Временной ряд остатков
|
| 1816 |
+
fig_time = px.line(
|
| 1817 |
+
x=residuals.index,
|
| 1818 |
+
y=residuals.values,
|
| 1819 |
+
title='Временной ряд остатков'
|
| 1820 |
+
)
|
| 1821 |
+
fig_time.add_hline(y=0, line_dash="dash", line_color="red")
|
| 1822 |
+
st.plotly_chart(fig_time, use_container_width=True)
|
| 1823 |
+
|
| 1824 |
+
except Exception as e:
|
| 1825 |
+
st.error(f"Ошибка диагностики: {e}")
|
| 1826 |
+
import traceback
|
| 1827 |
+
st.code(traceback.format_exc())
|
| 1828 |
+
else:
|
| 1829 |
+
st.info("Сначала постройте модели, чтобы выполнить диагностику остатков")
|
| 1830 |
+
|
| 1831 |
+
# Этап 8: Выводы и рекомендации
|
| 1832 |
+
st.header("📈 Этап 8: Сравнительный анализ и выводы")
|
| 1833 |
+
st.markdown("""
|
| 1834 |
+
**Что здесь происходит?**
|
| 1835 |
+
|
| 1836 |
+
В этом разделе вы видите итоговые результаты всех построенных моделей и можете сравнить их качество.
|
| 1837 |
+
|
| 1838 |
+
**Что означают метрики:**
|
| 1839 |
+
- **MAE (Mean Absolute Error):** Средняя абсолютная ошибка. Чем меньше, тем лучше.
|
| 1840 |
+
- **RMSE (Root Mean Squared Error):** Корень из средней квадратичной ошибки. Чем меньше, тем лучше. Более чувствительна к большим ошибкам.
|
| 1841 |
+
- **MAPE (Mean Absolute Percentage Error):** Средняя абсолютная процентная ошибка. Показывает ошибку в процентах. Чем меньше, тем лучше.
|
| 1842 |
+
|
| 1843 |
+
**Что нужно сделать:**
|
| 1844 |
+
1. Посмотрите на таблицу метрик - какая модель и стратегия показали лучшие результаты?
|
| 1845 |
+
2. Обратите внимание на рекомендации ниже
|
| 1846 |
+
3. Используйте эту информацию для выбора лучшей модели для ваших данных
|
| 1847 |
+
""")
|
| 1848 |
+
|
| 1849 |
+
if 'forecast_results' in st.session_state:
|
| 1850 |
+
st.subheader("📊 Итоговая таблица метрик")
|
| 1851 |
+
final_df = pd.DataFrame(st.session_state['forecast_results'])
|
| 1852 |
+
st.dataframe(final_df.set_index(['model', 'strategy']))
|
| 1853 |
+
|
| 1854 |
+
# Лучшая модель по каждой метрике
|
| 1855 |
+
st.subheader("🏆 Лучшие модели по метрикам")
|
| 1856 |
+
for metric in ['MAE', 'RMSE', 'MAPE']:
|
| 1857 |
+
if metric in final_df.columns:
|
| 1858 |
+
best_idx = final_df[metric].idxmin()
|
| 1859 |
+
best = final_df.loc[best_idx]
|
| 1860 |
+
st.write(f"**{metric}:** {best['model']} ({best['strategy']}) = {best[metric]:.4f}")
|
| 1861 |
+
|
| 1862 |
+
st.subheader("💡 Рекомендации")
|
| 1863 |
+
st.info("""
|
| 1864 |
+
**Общие рекомендации:**
|
| 1865 |
+
- **Короткий горизонт (h < 7):** Рекурсивная стратегия обычно работает лучше
|
| 1866 |
+
- **Длинный горизонт (h >= 30):** Прямая или гибридная стратегия могут быть предпочтительнее
|
| 1867 |
+
- **Преобразование Бокса-Кокса:** Используйте, если дисперсия нестабильна
|
| 1868 |
+
- **Дифференцирование:** Применяйте, если ряд нестационарен (попробуйте diff_order=1)
|
| 1869 |
+
- **Диагностика остатков:** Убедитесь, что остатки не имеют автокорреляции и распределены нормально
|
| 1870 |
+
|
| 1871 |
+
**Как выбрать модель:**
|
| 1872 |
+
1. Посмотрите, какая модель имеет наименьшие MAE, RMSE и MAPE
|
| 1873 |
+
2. Проверьте диагностику остатков для этой модели (Этап 7)
|
| 1874 |
+
3. Если остатки имеют проблемы, попробуйте другую модель или добавьте преобразования
|
| 1875 |
+
""")
|
| 1876 |
+
else:
|
| 1877 |
+
st.warning("⚠️ Сначала постройте модели в разделе 'Этап 6-7', чтобы увидеть результаты сравнения.")
|
| 1878 |
+
|
| 1879 |
+
|
| 1880 |
+
# ---------------- Главный код: выбор лабораторной работы ----------------
|
| 1881 |
+
if lab_choice == "ЛР №1: Введение в анализ временных рядов":
|
| 1882 |
+
render_lab1()
|
| 1883 |
+
elif lab_choice == "ЛР №2: Прогнозирование временных рядов":
|
| 1884 |
+
render_lab2()
|
| 1885 |
+
else:
|
| 1886 |
+
st.info("Выберите лабораторную работу в боковой панели")
|
БЫСТРЫЙ_СТАРТ.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ⚡ Быстрый старт
|
| 2 |
+
|
| 3 |
+
## 🚀 Запуск программы
|
| 4 |
+
|
| 5 |
+
```bash
|
| 6 |
+
pip install -r requirements.txt
|
| 7 |
+
streamlit run src/streamlit_app.py
|
| 8 |
+
```
|
| 9 |
+
|
| 10 |
+
Откройте браузер: `http://localhost:8501`
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## 📋 ЛР №1: Пошаговая инструкция
|
| 15 |
+
|
| 16 |
+
### Шаг 1: Загрузка данных
|
| 17 |
+
- Загрузите CSV файл или выберите пример
|
| 18 |
+
- Укажите колонку с датами
|
| 19 |
+
|
| 20 |
+
### Шаг 2: Предобработка
|
| 21 |
+
- Нажмите **"Run Preprocessing"**
|
| 22 |
+
- Оставьте настройки по умолчанию (или измените при необходимости)
|
| 23 |
+
|
| 24 |
+
### Шаг 3: Статистика
|
| 25 |
+
- Посмотрите таблицу дескриптивной статистики
|
| 26 |
+
- Изучите гистограммы и матрицу корреляций
|
| 27 |
+
|
| 28 |
+
### Шаг 4: Стационарность
|
| 29 |
+
- Выберите целевую переменную
|
| 30 |
+
- Нажмите **"Run stationarity tests"**
|
| 31 |
+
- Если нестационарен → нажмите **"Apply diff & Re-test"** с `diff_order=1`
|
| 32 |
+
|
| 33 |
+
### Шаг 5: Лаги
|
| 34 |
+
- Укажите лаги: `1,7,30`
|
| 35 |
+
- Укажите окна: `7,30`
|
| 36 |
+
- Нажмите **"Generate lags & rolls"**
|
| 37 |
+
|
| 38 |
+
### Шаг 6: ACF/PACF
|
| 39 |
+
- Выберите целевую переменную
|
| 40 |
+
- Установите `max_lag=40`
|
| 41 |
+
- Посмотрите графики и значимые лаги
|
| 42 |
+
|
| 43 |
+
### Шаг 7: Декомпозиция
|
| 44 |
+
- Выберите модель: `additive`
|
| 45 |
+
- Укажите период: `7` (для недельной) или `30` (для месячной)
|
| 46 |
+
- Нажмите **"Run decomposition"**
|
| 47 |
+
|
| 48 |
+
### Шаг 8: Отчёт
|
| 49 |
+
- Нажмите **"Сгенерировать и показать отчёт"**
|
| 50 |
+
- Скачайте HTML-отчёт
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
|
| 54 |
+
## 🔮 ЛР №2: Пошаговая инструкция
|
| 55 |
+
|
| 56 |
+
### Шаг 1: Подготовка
|
| 57 |
+
- Убедитесь, что данные из ЛР №1 загружены
|
| 58 |
+
- Выберите целевую переменную
|
| 59 |
+
- Установите горизонт прогнозирования (h): `7` или `30`
|
| 60 |
+
|
| 61 |
+
### Шаг 2: Декомпозиция
|
| 62 |
+
- Выберите период сезонности: `7` или `30`
|
| 63 |
+
- Нажмите **"Выполнить декомпозицию"**
|
| 64 |
+
|
| 65 |
+
### Шаг 3: Преобразования (опционально)
|
| 66 |
+
- Начните с `none` и `diff_order=0`
|
| 67 |
+
- Если модель плохая → попробуйте `diff_order=1`
|
| 68 |
+
|
| 69 |
+
### Шаг 4: Модели
|
| 70 |
+
- Выберите стратегии: `recursive` (для начала)
|
| 71 |
+
- Выберите модели: `SES`, `Holt_add`
|
| 72 |
+
- Нажмите **"Применить преобразования и построить модели"**
|
| 73 |
+
|
| 74 |
+
### Шаг 5: Результаты
|
| 75 |
+
- Посмотрите таблицу метрик
|
| 76 |
+
- Найдите модель с наименьшими MAE, RMSE, MAPE
|
| 77 |
+
- Посмотрите график прогнозов
|
| 78 |
+
|
| 79 |
+
### Шаг 6: Диагностика
|
| 80 |
+
- Выберите лучшую модель
|
| 81 |
+
- Нажмите **"Выполнить диагностику остатков"**
|
| 82 |
+
- Проверьте, что p-value тестов > 0.05
|
| 83 |
+
|
| 84 |
+
### Шаг 7: Выводы
|
| 85 |
+
- Посмотрите итоговую таблицу
|
| 86 |
+
- Выберите лучшую модель
|
| 87 |
+
- Скачайте прогнозы и параметры модели
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## 📊 Что означают метрики
|
| 92 |
+
|
| 93 |
+
| Метрика | Что означает | Чем меньше, тем лучше |
|
| 94 |
+
|---------|-------------|------------------------|
|
| 95 |
+
| **MAE** | Средняя абсолютная ошибка | ✅ |
|
| 96 |
+
| **RMSE** | Корень из средней квадратичной ошибки | ✅ |
|
| 97 |
+
| **MAPE** | Средняя абсолютная процентная ошибка (%) | ✅ |
|
| 98 |
+
|
| 99 |
+
**Пример:** MAPE = 10% означает, что в среднем ошибка 10%
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
## 🎯 Тесты стационарности
|
| 104 |
+
|
| 105 |
+
| Тест | Стационарен если | Нестационарен если |
|
| 106 |
+
|------|------------------|---------------------|
|
| 107 |
+
| **ADF** | p-value < 0.05 ✅ | p-value >= 0.05 ❌ |
|
| 108 |
+
| **KPSS** | p-value > 0.05 ✅ | p-value <= 0.05 ❌ |
|
| 109 |
+
|
| 110 |
+
**Если нестационарен:** примените `diff_order=1`
|
| 111 |
+
|
| 112 |
+
---
|
| 113 |
+
|
| 114 |
+
## 🔍 Корреляции
|
| 115 |
+
|
| 116 |
+
| Значение | Интерпретация |
|
| 117 |
+
|----------|---------------|
|
| 118 |
+
| **1.0** | Полная прямая связь |
|
| 119 |
+
| **0.7-1.0** | Сильная связь |
|
| 120 |
+
| **0.3-0.7** | Умеренная связь |
|
| 121 |
+
| **0.0-0.3** | Слабая связь |
|
| 122 |
+
| **0.0** | Нет связи |
|
| 123 |
+
| **-1.0** | Полная обратная связь |
|
| 124 |
+
|
| 125 |
+
**Проблема:** Если два признака коррелируют > 0.8 → мультиколлинеарность
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
## 🎨 Модели экспоненциального сглаживания
|
| 130 |
+
|
| 131 |
+
| Модель | Когда использовать |
|
| 132 |
+
|--------|-------------------|
|
| 133 |
+
| **SES** | Стационарный ряд без тренда |
|
| 134 |
+
| **Holt Additive** | Ряд с линейным трендом |
|
| 135 |
+
| **Holt Multiplicative** | Ряд с экспоненциальным трендом |
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
## 🚦 Стратегии прогнозирования
|
| 140 |
+
|
| 141 |
+
| Стратегия | Когда использовать |
|
| 142 |
+
|-----------|-------------------|
|
| 143 |
+
| **Recursive** | Короткий горизонт (h < 7) |
|
| 144 |
+
| **Direct** | Длинный горизонт (h >= 30) |
|
| 145 |
+
| **Hybrid** | Средний горизонт (7-30) |
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
## ⚠️ Частые ошибки
|
| 150 |
+
|
| 151 |
+
1. **"Недостаточно данных"**
|
| 152 |
+
- Решение: уменьшите размер обучающей выборки
|
| 153 |
+
|
| 154 |
+
2. **"Для лог-трансформации все значения должны быть положительными"**
|
| 155 |
+
- **Причина:** В данных есть нули или отрицательные значения
|
| 156 |
+
- **Решения:**
|
| 157 |
+
- ✅ **Используйте Box-Cox** вместо логарифма (автоматически обработает проблему)
|
| 158 |
+
- ✅ **Используйте только дифференцирование** (diff_order=1) без логарифма
|
| 159 |
+
- ✅ **Включите автоматический сдвиг** - программа предложит сдвинуть данные
|
| 160 |
+
- **Как исправить:**
|
| 161 |
+
1. Посмотрите статистику данных в сообщении об ошибке
|
| 162 |
+
2. Включите чекбокс "Автоматически сдвинуть данные"
|
| 163 |
+
3. Или измените тип преобразования на "boxcox"
|
| 164 |
+
|
| 165 |
+
3. **"Ошибка при преобразовании"**
|
| 166 |
+
- Решение: убедитесь, что все значения положительные (для log/boxcox)
|
| 167 |
+
|
| 168 |
+
4. **"Модель работает плохо"**
|
| 169 |
+
- Решение: попробуйте `diff_order=1` или другую модель
|
| 170 |
+
|
| 171 |
+
5. **"Остатки имеют автокорреляцию"**
|
| 172 |
+
- Решение: попробуйте другую модель или стратегию
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
## 💡 Советы
|
| 177 |
+
|
| 178 |
+
1. ✅ Начинайте с простого (SES, recursive, без преобразований)
|
| 179 |
+
2. ✅ Сравнивайте с наивным прогнозом (baseline)
|
| 180 |
+
3. ✅ Проверяйте диагностику остатков
|
| 181 |
+
4. ✅ Экспериментируйте с разными моделями
|
| 182 |
+
5. ✅ Сохраняйте результаты (CSV, HTML-отчёты)
|
| 183 |
+
|
| 184 |
+
---
|
| 185 |
+
|
| 186 |
+
## 📁 Что скачивать
|
| 187 |
+
|
| 188 |
+
- **final_dataset.csv** - очищенные данные (ЛР №1)
|
| 189 |
+
- **dataset_with_lags.csv** - данные с лагами (ЛР №1)
|
| 190 |
+
- **ts_report.html** - HTML-отчёт (ЛР №1)
|
| 191 |
+
- **forecast_*.csv** - прогнозы моделей (ЛР №2)
|
| 192 |
+
- **model_params_*.csv** - параметры моделей (ЛР №2)
|
| 193 |
+
|
| 194 |
+
---
|
| 195 |
+
|
| 196 |
+
**Подробное руководство:** см. `РУКОВОДСТВО.md`
|
| 197 |
+
|
РУКОВОДСТВО.md
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 📚 Руководство по использованию программы для анализа временных рядов
|
| 2 |
+
|
| 3 |
+
## 🎯 Общая структура проекта
|
| 4 |
+
|
| 5 |
+
Ваш проект состоит из двух лабораторных работ, объединённых в одно веб-приложение:
|
| 6 |
+
|
| 7 |
+
### Файлы проекта:
|
| 8 |
+
- **`src/streamlit_app.py`** - главный файл веб-приложения (1655 строк)
|
| 9 |
+
- **`src/lab2_functions.py`** - функции для лабораторной работы №2 (604 строки)
|
| 10 |
+
- **`src/main.py`** - не используется (можно игнорировать)
|
| 11 |
+
- **`russia_covid_dataset.csv`** - пример данных (COVID-19 по России)
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## 🚀 Как запустить программу
|
| 16 |
+
|
| 17 |
+
1. **Установите зависимости:**
|
| 18 |
+
```bash
|
| 19 |
+
pip install -r requirements.txt
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
2. **Запустите приложение:**
|
| 23 |
+
```bash
|
| 24 |
+
streamlit run src/streamlit_app.py
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
3. **Откройте браузер:**
|
| 28 |
+
- Программа автоматически откроется на `http://localhost:8501`
|
| 29 |
+
- Или откройте вручную этот адрес
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## 📖 ЛАБОРАТОРНАЯ РАБОТА №1: Введение в анализ временных рядов
|
| 34 |
+
|
| 35 |
+
### Что делает эта работа?
|
| 36 |
+
|
| 37 |
+
Эта работа учит вас **"читать"** временной ряд - понимать его структуру, находить закономерности, выявлять проблемы.
|
| 38 |
+
|
| 39 |
+
### Порядок работы (по этапам):
|
| 40 |
+
|
| 41 |
+
#### **Этап 1: Загрузка данных**
|
| 42 |
+
1. В боковой панели нажмите "Загрузите CSV/Parquet"
|
| 43 |
+
2. Или выберите предзагруженный пример (если есть `russia_covid_dataset.csv`)
|
| 44 |
+
3. Укажите колонку с датами (программа попытается найти её автоматически)
|
| 45 |
+
|
| 46 |
+
**Что происходит:** Программа загружает ваш файл и показывает первые строки.
|
| 47 |
+
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
#### **Этап 2: Предобработка данных (Preprocessing)**
|
| 51 |
+
|
| 52 |
+
**Настройки в боковой панели:**
|
| 53 |
+
- **Как трактовать tz-naive метки?** - как обрабатывать даты без временной зоны
|
| 54 |
+
- **Заполнение пропусков (числ.)** - что делать с пропущенными числами:
|
| 55 |
+
- `interpolate` - заполнить интерполяцией (рекомендуется)
|
| 56 |
+
- `drop` - удалить строки
|
| 57 |
+
- `rolling` - заполнить скользящим средним
|
| 58 |
+
- **Обработка выбросов** - что делать с аномальными значениями:
|
| 59 |
+
- `interpolate` - заменить интерполяцией
|
| 60 |
+
- `winsorize` - обрезать экстремальные значения
|
| 61 |
+
- `drop` - удалить
|
| 62 |
+
- `mark` - только отметить
|
| 63 |
+
- **Ресемплить к частоте** - изменить частоту данных (D=день, W=неделя, M=месяц)
|
| 64 |
+
|
| 65 |
+
**Что делать:**
|
| 66 |
+
1. Нажмите кнопку **"Run Preprocessing"**
|
| 67 |
+
2. Программа покажет:
|
| 68 |
+
- Сколько строк было до/после обработки
|
| 69 |
+
- Сколько пропусков найдено и обработано
|
| 70 |
+
- Сколько выбросов обнаружено
|
| 71 |
+
|
| 72 |
+
**Результат:** Очищенный датасет, готовый к анализу.
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
#### **Этап 3: Описательная статистика и визуализация**
|
| 77 |
+
|
| 78 |
+
**Что показывается:**
|
| 79 |
+
|
| 80 |
+
1. **Таблица дескриптивной статистики:**
|
| 81 |
+
- `count` - количество наблюдений
|
| 82 |
+
- `mean` - среднее значение
|
| 83 |
+
- `median` - медиана (середина)
|
| 84 |
+
- `std` - стандартное отклонение (разброс)
|
| 85 |
+
- `min/max` - минимум/максимум
|
| 86 |
+
- `q1/q3` - первый и третий квартили (25% и 75%)
|
| 87 |
+
- `skew` - асимметрия (если >0, хвост справа)
|
| 88 |
+
- `kurtosis` - эксцесс (острота распределения)
|
| 89 |
+
|
| 90 |
+
2. **Гистограммы и Boxplot:**
|
| 91 |
+
- **Гистограмма** - показывает распределение значений (как часто встречается каждое значение)
|
| 92 |
+
- **Boxplot** - показывает медиану, квартили и выбросы
|
| 93 |
+
|
| 94 |
+
3. **Матрица корреляций:**
|
| 95 |
+
- Показывает, насколько связаны между собой признаки
|
| 96 |
+
- Значения от -1 до 1:
|
| 97 |
+
- **1** = полная прямая связь
|
| 98 |
+
- **0** = нет связи
|
| 99 |
+
- **-1** = полная ��братная связь
|
| 100 |
+
- **Важно:** Если два признака сильно коррелируют (>0.8), это может быть проблемой (мультиколлинеарность)
|
| 101 |
+
|
| 102 |
+
**Как интерпретировать:**
|
| 103 |
+
- Если `std` большой относительно `mean` - данные сильно разбросаны
|
| 104 |
+
- Если `skew` далёк от 0 - распределение несимметрично
|
| 105 |
+
- Сильные корреляции (>0.7) указывают на зависимость признаков
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
#### **Этап 4: Проверка на стационарность**
|
| 110 |
+
|
| 111 |
+
**Что такое стационарность?**
|
| 112 |
+
Стационарный ряд = ряд без тренда, с постоянной дисперсией. Многие модели требуют стационарности.
|
| 113 |
+
|
| 114 |
+
**Что показывается:**
|
| 115 |
+
|
| 116 |
+
1. **График ряда с rolling mean:**
|
| 117 |
+
- Если линия скользящего среднего не горизонтальна → есть тренд (нестационарен)
|
| 118 |
+
- Если линия скользящего среднего горизонтальна → тренда нет (стационарен)
|
| 119 |
+
|
| 120 |
+
2. **График rolling std:**
|
| 121 |
+
- Если линия не горизонтальна → дисперсия меняется (нестационарен)
|
| 122 |
+
- Если горизонтальна → дисперсия постоянна
|
| 123 |
+
|
| 124 |
+
3. **Статистические тесты:**
|
| 125 |
+
- **ADF (Augmented Dickey-Fuller):**
|
| 126 |
+
- p-value < 0.05 → ряд стационарен ✅
|
| 127 |
+
- p-value >= 0.05 → ряд нестационарен ❌
|
| 128 |
+
- **KPSS:**
|
| 129 |
+
- p-value > 0.05 → ряд стационарен ✅
|
| 130 |
+
- p-value <= 0.05 → ряд нестационарен ❌
|
| 131 |
+
|
| 132 |
+
**Что делать, если ряд нестационарен:**
|
| 133 |
+
1. Нажмите "Apply diff & Re-test" с `diff_order=1`
|
| 134 |
+
2. Это применит дифференцирование (убирает тренд)
|
| 135 |
+
3. Повторите тесты
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
#### **Этап 5: Создание лагов и скользящих статистик**
|
| 140 |
+
|
| 141 |
+
**Что такое лаги?**
|
| 142 |
+
Лаг = значение переменной в прошлом. Например, `target_lag_7` = значение target 7 дней назад.
|
| 143 |
+
|
| 144 |
+
**Что создаётся:**
|
| 145 |
+
- **Лаги:** `target_lag_1`, `target_lag_7`, `target_lag_30`
|
| 146 |
+
- **Скользящие статистики:**
|
| 147 |
+
- `target_rolling_mean_7` - среднее за последние 7 дней
|
| 148 |
+
- `target_rolling_std_7` - стандартное отклонение за последние 7 дней
|
| 149 |
+
|
| 150 |
+
**Как использовать:**
|
| 151 |
+
1. Укажите целевую переменную (target)
|
| 152 |
+
2. Укажите лаги через запятую (например: `1,7,30`)
|
| 153 |
+
3. Укажите окна для скользящих (например: `7,30`)
|
| 154 |
+
4. Нажмите "Generate lags & rolls"
|
| 155 |
+
|
| 156 |
+
**Что показывается:**
|
| 157 |
+
- Таблица корреляций лагов с target - какие лаги наиболее информативны
|
| 158 |
+
- Heatmap корреляций - визуализация всех корреляций
|
| 159 |
+
- VIF (Variance Inflation Factor) - проверка мультиколлинеарности:
|
| 160 |
+
- VIF < 5 → нормально
|
| 161 |
+
- VIF 5-10 → умеренная мультиколлинеарность
|
| 162 |
+
- VIF > 10 → сильная мультиколлинеарность (проблема)
|
| 163 |
+
|
| 164 |
+
---
|
| 165 |
+
|
| 166 |
+
#### **Этап 6: ACF и PACF**
|
| 167 |
+
|
| 168 |
+
**Что это такое?**
|
| 169 |
+
- **ACF (Autocorrelation Function)** - корреляция ряда с его лагами
|
| 170 |
+
- **PACF (Partial Autocorrelation Function)** - "чистая" корреляция с лагом
|
| 171 |
+
|
| 172 |
+
**Как интерпретировать графики:**
|
| 173 |
+
|
| 174 |
+
1. **ACF:**
|
| 175 |
+
- Плавное затухание → возможный порядок MA(q)
|
| 176 |
+
- Резкий обрыв → возможный порядок AR(p)
|
| 177 |
+
|
| 178 |
+
2. **PACF:**
|
| 179 |
+
- Резкий обрыв на лаге p → возможный порядок AR(p)
|
| 180 |
+
- Плавное затухание → возможный порядок MA(q)
|
| 181 |
+
|
| 182 |
+
3. **Значимые лаги:**
|
| 183 |
+
- Лаги, выходящие за доверительный интервал (синие линии) → статистически значимы
|
| 184 |
+
- Эти лаги важны для моделирования
|
| 185 |
+
|
| 186 |
+
**Что делать:**
|
| 187 |
+
- Посмотрите, какие лаги значимы
|
| 188 |
+
- Запомните порядок обрыва в PACF - это может быть порядок AR модели
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
#### **Этап 7: Декомпозиция временного ряда**
|
| 193 |
+
|
| 194 |
+
**Что такое декомпозиция?**
|
| 195 |
+
Разложение ряда на компоненты:
|
| 196 |
+
- **Observed** - исходный ряд
|
| 197 |
+
- **Trend** - тренд (долгосрочная тенденция)
|
| 198 |
+
- **Seasonal** - сезонность (периодические колебания)
|
| 199 |
+
- **Residual** - остатки (случайные колебания)
|
| 200 |
+
|
| 201 |
+
**Модели декомпозиции:**
|
| 202 |
+
- **Additive (аддитивная):** `value = trend + seasonal + residual`
|
| 203 |
+
- Используйте, если амплитуда сезонности постоянна
|
| 204 |
+
- **Multiplicative (мультипликативная):** `value = trend × seasonal × residual`
|
| 205 |
+
- Используйте, если амплитуда сезонности растёт со временем
|
| 206 |
+
|
| 207 |
+
**Как использовать:**
|
| 208 |
+
1. Выберите целевую переменную
|
| 209 |
+
2. Выберите модель (additive/multiplicative)
|
| 210 |
+
3. Укажите период сезонности:
|
| 211 |
+
- 7 - для недельной сезонности
|
| 212 |
+
- 30 - для месячной
|
| 213 |
+
- 365 - для годовой
|
| 214 |
+
4. Нажмите "Run decomposition"
|
| 215 |
+
|
| 216 |
+
**Что показывается:**
|
| 217 |
+
- Графики всех компонентов
|
| 218 |
+
- Анализ тренда (растёт/падает)
|
| 219 |
+
- Амплитуда сезонности
|
| 220 |
+
- Диагностика остатков (должны быть стационарны и случайны)
|
| 221 |
+
|
| 222 |
+
**Как интерпретировать:**
|
| 223 |
+
- Если остатки стационарны (ADF/KPSS тесты) → декомпозиция хорошая ✅
|
| 224 |
+
- Если остатки нестационарны → попробуйте другой период или модель ❌
|
| 225 |
+
|
| 226 |
+
---
|
| 227 |
+
|
| 228 |
+
#### **Этап 8: Генерация отчёта**
|
| 229 |
+
|
| 230 |
+
**Что делает:**
|
| 231 |
+
Собирает все графики и таблицы в один HTML-отчёт.
|
| 232 |
+
|
| 233 |
+
**Как использовать:**
|
| 234 |
+
1. Настройте параметры в разделе "Параметры для отчёта"
|
| 235 |
+
2. Нажмите "Сгенерировать и показать отчёт"
|
| 236 |
+
3. Просмотрите результаты во вкладках
|
| 237 |
+
4. Скачайте HTML-отчёт
|
| 238 |
+
|
| 239 |
+
---
|
| 240 |
+
|
| 241 |
+
## 🔮 ЛАБОРАТОРНАЯ РАБОТА №2: Прогнозирование временных рядов
|
| 242 |
+
|
| 243 |
+
### Что делает эта работа?
|
| 244 |
+
|
| 245 |
+
Эта работа учит строить **модели для прогнозирования** будущих значений временного ряда.
|
| 246 |
+
|
| 247 |
+
### Порядок работы:
|
| 248 |
+
|
| 249 |
+
#### **Этап 1: Декомпозиция и анализ остатков**
|
| 250 |
+
|
| 251 |
+
**Что делать:**
|
| 252 |
+
1. Убедитесь, что данные из ЛР №1 загружены
|
| 253 |
+
2. Выберите целевую переменную в боковой панели
|
| 254 |
+
3. Укажите горизонт прогнозирования (h) - на сколько шагов вперёд прогнозировать
|
| 255 |
+
4. Настройте размер обучающей выборки (рекомендуется ≥500)
|
| 256 |
+
5. Нажмите "Выполнить декомпозицию"
|
| 257 |
+
|
| 258 |
+
**Что показывается:**
|
| 259 |
+
- Графики компонентов (observed, trend, seasonal, residuals)
|
| 260 |
+
- Анализ остатков (должны быть стационарны)
|
| 261 |
+
- ACF/PACF остатков (не должно быть значимых лагов)
|
| 262 |
+
|
| 263 |
+
**Как интерпретировать:**
|
| 264 |
+
- Если остатки стационарны и не имеют автокорреляции → декомпозиция хорошая ✅
|
| 265 |
+
- Если нет → попробуйте другой период или модель
|
| 266 |
+
|
| 267 |
+
---
|
| 268 |
+
|
| 269 |
+
#### **Этап 2: Feature Engineering**
|
| 270 |
+
|
| 271 |
+
**Что создаётся:**
|
| 272 |
+
- Временные признаки (день недели, месяц, квартал)
|
| 273 |
+
- Циклические признаки (sin/cos для периодичности)
|
| 274 |
+
- Лаги (lag_1, lag_7, lag_30)
|
| 275 |
+
- Скользящие статистики (mean, std, min, max)
|
| 276 |
+
|
| 277 |
+
**Как использовать:**
|
| 278 |
+
1. Нажмите "Создать расширенные признаки"
|
| 279 |
+
2. Просмотрите созданные признаки
|
| 280 |
+
3. Скачайте датасет с признаками (опционально)
|
| 281 |
+
|
| 282 |
+
---
|
| 283 |
+
|
| 284 |
+
#### **Этап 3: Стратегии прогнозирования**
|
| 285 |
+
|
| 286 |
+
**Три стратегии:**
|
| 287 |
+
|
| 288 |
+
1. **Recursive (рекурсивная):**
|
| 289 |
+
- Одна модель
|
| 290 |
+
- Использует свои предыдущие прогнозы
|
| 291 |
+
- ✅ Хорошо для короткого горизонта (h < 7)
|
| 292 |
+
- ❌ Ошибка накапливается на длинном горизонте
|
| 293 |
+
|
| 294 |
+
2. **Direct (прямая):**
|
| 295 |
+
- Отдельная модель для каждого шага
|
| 296 |
+
- Прогнозы независимы
|
| 297 |
+
- ✅ Хорошо для длинного горизонта (h >= 30)
|
| 298 |
+
- ❌ Требует больше вычислений
|
| 299 |
+
|
| 300 |
+
3. **Hybrid (гибридная):**
|
| 301 |
+
- Комбинация: рекурсивная для ближних шагов, прямая для дальних
|
| 302 |
+
- ✅ Баланс ме��ду точностью и скоростью
|
| 303 |
+
|
| 304 |
+
**Как использовать:**
|
| 305 |
+
- Выберите стратегии в разделе "Этап 6-7"
|
| 306 |
+
- Можно выбрать несколько для сравнения
|
| 307 |
+
|
| 308 |
+
---
|
| 309 |
+
|
| 310 |
+
#### **Этап 4: Кросс-валидация**
|
| 311 |
+
|
| 312 |
+
**Что такое кросс-валидация?**
|
| 313 |
+
Проверка качества модели на разных частях данных без утечки будущего.
|
| 314 |
+
|
| 315 |
+
**Методы:**
|
| 316 |
+
1. **Sliding window (скользящее окно):**
|
| 317 |
+
- Фиксированная длина обучения
|
| 318 |
+
- Окно "скользит" по времени
|
| 319 |
+
|
| 320 |
+
2. **Expanding window (расширяющееся окно):**
|
| 321 |
+
- Длина обучения растёт
|
| 322 |
+
- Более реалистично для реальных задач
|
| 323 |
+
|
| 324 |
+
3. **TimeSeriesSplit:**
|
| 325 |
+
- Стандартный метод из sklearn
|
| 326 |
+
- Прогрессивное разбиение
|
| 327 |
+
|
| 328 |
+
**Как использовать:**
|
| 329 |
+
1. Выберите метод
|
| 330 |
+
2. Настройте размеры train/test
|
| 331 |
+
3. Нажмите "Выполнить кросс-валидацию"
|
| 332 |
+
|
| 333 |
+
**Что показывается:**
|
| 334 |
+
- Таблица метрик по фолдам
|
| 335 |
+
- Средние метрики
|
| 336 |
+
- График метрик по фолдам
|
| 337 |
+
|
| 338 |
+
**Как интерпретировать:**
|
| 339 |
+
- Если метрики стабильны по фолдам → модель надёжна ✅
|
| 340 |
+
- Если метрики сильно меняются → модель нестабильна ❌
|
| 341 |
+
|
| 342 |
+
---
|
| 343 |
+
|
| 344 |
+
#### **Этап 5: Преобразования к стационарности**
|
| 345 |
+
|
| 346 |
+
**Зачем нужно?**
|
| 347 |
+
Многие модели требуют стационарных данных.
|
| 348 |
+
|
| 349 |
+
**Типы преобразований:**
|
| 350 |
+
|
| 351 |
+
1. **None (без преобразования):**
|
| 352 |
+
- Используйте, если данные уже стационарны
|
| 353 |
+
|
| 354 |
+
2. **Log (логарифм):**
|
| 355 |
+
- Стабилизирует дисперсию
|
| 356 |
+
- Требует положительные значения
|
| 357 |
+
- Используйте, если дисперсия растёт со временем
|
| 358 |
+
|
| 359 |
+
3. **Box-Cox:**
|
| 360 |
+
- Автоматический подбор преобразования
|
| 361 |
+
- Включает логарифм как частный случай
|
| 362 |
+
- ✅ Рекомендуется, если не уверены
|
| 363 |
+
|
| 364 |
+
4. **Дифференцирование:**
|
| 365 |
+
- `diff_order=1` - убирает тренд (первая разность)
|
| 366 |
+
- `diff_order=2` - убирает квадратичный тренд
|
| 367 |
+
- `seasonal_diff=7` - убирает сезонность (для недельной)
|
| 368 |
+
|
| 369 |
+
**Как использовать:**
|
| 370 |
+
1. Начните с `none` и `diff_order=0`
|
| 371 |
+
2. Если модель плохая, попробуйте `diff_order=1`
|
| 372 |
+
3. Если дисперсия нестабильна, попробуйте `log` или `boxcox`
|
| 373 |
+
|
| 374 |
+
**Что показывается:**
|
| 375 |
+
- Результаты тестов ADF/KPSS после преобразования
|
| 376 |
+
- Если p-value ADF < 0.05 и p-value KPSS > 0.05 → стационарен ✅
|
| 377 |
+
|
| 378 |
+
---
|
| 379 |
+
|
| 380 |
+
#### **Этап 6-7: Модели экспоненциального сглаживания**
|
| 381 |
+
|
| 382 |
+
**Три модели:**
|
| 383 |
+
|
| 384 |
+
1. **SES (Simple Exponential Smoothing):**
|
| 385 |
+
- Простое сглаживание без тренда
|
| 386 |
+
- ✅ Хорошо для стационарных рядов
|
| 387 |
+
- ❌ Не учитывает тренд
|
| 388 |
+
|
| 389 |
+
2. **Holt Additive:**
|
| 390 |
+
- С аддитивным трендом (линейный рост/падение)
|
| 391 |
+
- ✅ Хорошо для рядов с линейным трендом
|
| 392 |
+
- Формула: `level + trend`
|
| 393 |
+
|
| 394 |
+
3. **Holt Multiplicative:**
|
| 395 |
+
- С мультипликативным трендом (экспоненциальный рост/падение)
|
| 396 |
+
- ✅ Хорошо для рядов с экспоненциальным трендом
|
| 397 |
+
- ❌ Требует положительные значения
|
| 398 |
+
- Формула: `level × trend`
|
| 399 |
+
|
| 400 |
+
**Как использовать:**
|
| 401 |
+
1. Выберите стратегии прогнозирования
|
| 402 |
+
2. Выберите модели (можно несколько)
|
| 403 |
+
3. Нажмите "Применить преобразования и построить модели"
|
| 404 |
+
|
| 405 |
+
**Что показывается:**
|
| 406 |
+
- Таблица сравнения метрик (MAE, RMSE, MAPE)
|
| 407 |
+
- График прогнозов всех моделей
|
| 408 |
+
- Сравнение с наивным прогнозом (baseline)
|
| 409 |
+
|
| 410 |
+
**Метрики качества:**
|
| 411 |
+
|
| 412 |
+
1. **MAE (Mean Absolute Error):**
|
| 413 |
+
- Средняя абсолютная ошибка
|
| 414 |
+
- Чем меньше, тем лучше
|
| 415 |
+
- Интерпретация: среднее отклонение прогноза от реальности
|
| 416 |
+
|
| 417 |
+
2. **RMSE (Root Mean Squared Error):**
|
| 418 |
+
- Корень из средней квадратичной ошибки
|
| 419 |
+
- Чем меньше, те�� лучше
|
| 420 |
+
- Более чувствительна к большим ошибкам
|
| 421 |
+
- Интерпретация: типичное отклонение (с учётом больших ошибок)
|
| 422 |
+
|
| 423 |
+
3. **MAPE (Mean Absolute Percentage Error):**
|
| 424 |
+
- Средняя абсолютная процентная ошибка
|
| 425 |
+
- Чем меньше, тем лучше
|
| 426 |
+
- Интерпретация: ошибка в процентах
|
| 427 |
+
- Пример: MAPE=5% означает, что в среднем ошибка 5%
|
| 428 |
+
|
| 429 |
+
**Как интерпретировать:**
|
| 430 |
+
- Сравните метрики моделей
|
| 431 |
+
- Выберите модель с наименьшими MAE, RMSE, MAPE
|
| 432 |
+
- Убедитесь, что модель лучше наивного прогноза
|
| 433 |
+
|
| 434 |
+
---
|
| 435 |
+
|
| 436 |
+
#### **Этап 7: Диагностика остатков**
|
| 437 |
+
|
| 438 |
+
**Что проверяется:**
|
| 439 |
+
|
| 440 |
+
1. **Тест Льюнга-Бокса:**
|
| 441 |
+
- Проверяет автокорреляцию в остатках
|
| 442 |
+
- p-value < 0.05 → есть автокорреляция (плохо) ❌
|
| 443 |
+
- p-value >= 0.05 → нет автокорреляции (хорошо) ✅
|
| 444 |
+
|
| 445 |
+
2. **Тест Шапиро-Уилка:**
|
| 446 |
+
- Проверяет нормальность распределения остатков
|
| 447 |
+
- p-value < 0.05 → не нормально (плохо) ❌
|
| 448 |
+
- p-value >= 0.05 → нормально (хорошо) ✅
|
| 449 |
+
|
| 450 |
+
3. **Графики:**
|
| 451 |
+
- **Остатки vs Прогнозы:** должны быть случайными (гомоскедастичность)
|
| 452 |
+
- **Q-Q Plot:** точки должны лежать на прямой (нормальность)
|
| 453 |
+
- **Временной ряд остатков:** не должно быть тренда
|
| 454 |
+
|
| 455 |
+
**Как использовать:**
|
| 456 |
+
1. Выберите модель для диагностики
|
| 457 |
+
2. Нажмите "Выполнить диагностику остатков"
|
| 458 |
+
|
| 459 |
+
**Как интерпретировать:**
|
| 460 |
+
- Если все тесты пройдены → модель адекватна ✅
|
| 461 |
+
- Если есть проблемы → попробуйте другую модель или добавьте преобразования
|
| 462 |
+
|
| 463 |
+
---
|
| 464 |
+
|
| 465 |
+
#### **Этап 8: Сравнительный анализ и выводы**
|
| 466 |
+
|
| 467 |
+
**Что показывается:**
|
| 468 |
+
- Итоговая таблица метрик всех моделей
|
| 469 |
+
- Лучшие модели по каждой метрике
|
| 470 |
+
- Рекомендации по выбору модели
|
| 471 |
+
|
| 472 |
+
**Как использовать:**
|
| 473 |
+
1. Посмотрите на таблицу метрик
|
| 474 |
+
2. Найдите модель с наименьшими ошибками
|
| 475 |
+
3. Проверьте диагностику остатков для этой модели
|
| 476 |
+
4. Используйте эту модель для прогнозирования
|
| 477 |
+
|
| 478 |
+
**Рекомендации:**
|
| 479 |
+
- **Короткий горизонт (h < 7):** рекурсивная стратегия
|
| 480 |
+
- **Длинный горизонт (h >= 30):** прямая или гибридная стратегия
|
| 481 |
+
- **Нестационарный ряд:** используйте дифференцирование (diff_order=1)
|
| 482 |
+
- **Нестабильная дисперсия:** используйте Box-Cox преобразование
|
| 483 |
+
|
| 484 |
+
---
|
| 485 |
+
|
| 486 |
+
## 📊 Что означают числа и метрики
|
| 487 |
+
|
| 488 |
+
### Статистические метрики:
|
| 489 |
+
|
| 490 |
+
- **Mean (среднее):** среднее значение всех наблюдений
|
| 491 |
+
- **Median (медиана):** значение в середине (50% наблюдений меньше)
|
| 492 |
+
- **Std (стандартное отклонение):** мера разброса данных
|
| 493 |
+
- Маленькое std → данные сконцентрированы
|
| 494 |
+
- Большое std → данные разбросаны
|
| 495 |
+
- **Min/Max:** минимальное и максимальное значения
|
| 496 |
+
- **Q1/Q3 (квартили):** 25% и 75% наблюдений меньше этого значения
|
| 497 |
+
- **Skew (асимметрия):**
|
| 498 |
+
- 0 = симметричное распределение
|
| 499 |
+
- >0 = хвост справа (больше больших значений)
|
| 500 |
+
- <0 = хвост слева (больше маленьких значений)
|
| 501 |
+
- **Kurtosis (эксцесс):**
|
| 502 |
+
- 0 = нормальное распределение
|
| 503 |
+
- >0 = более острое распределение (больше экстремальных значений)
|
| 504 |
+
- <0 = более плоское распределение
|
| 505 |
+
|
| 506 |
+
### Метрики качества прогноза:
|
| 507 |
+
|
| 508 |
+
- **MAE:** средняя абсолютная ошибка (в тех же единицах, что и данные)
|
| 509 |
+
- **RMSE:** корень из средней квадратичной ошибки (более чувствительна к большим ошибкам)
|
| 510 |
+
- **MAPE:** средняя абсолютная процентная ошибка (в процентах)
|
| 511 |
+
|
| 512 |
+
**Пример интерпретации:**
|
| 513 |
+
- Если MAPE = 10%, это означает, что в среднем прогноз отклоняется на 10% от реальности
|
| 514 |
+
- Если MAE = 100, это означает, что в среднем прогноз отклоняется на 100 единиц
|
| 515 |
+
|
| 516 |
+
### Тесты стационарности:
|
| 517 |
+
|
| 518 |
+
- **ADF p-value:**
|
| 519 |
+
- < 0.05 → ряд стационарен ✅
|
| 520 |
+
- >= 0.05 → ряд нестационарен ❌
|
| 521 |
+
- **KPSS p-value:**
|
| 522 |
+
- > 0.05 → ряд стационарен ✅
|
| 523 |
+
- <= 0.05 → ряд нестационарен ❌
|
| 524 |
+
|
| 525 |
+
### Корреляции:
|
| 526 |
+
|
| 527 |
+
- **1.0:** полная прямая связь (когда один растёт, другой тоже растёт)
|
| 528 |
+
- **0.0:** нет связи
|
| 529 |
+
- **-1.0:** полная обратная связь (когда один растёт, другой падает)
|
| 530 |
+
- **0.7-1.0:** сильная связь
|
| 531 |
+
- **0.3-0.7:** умеренная связь
|
| 532 |
+
- **0.0-0.3:** слабая связь
|
| 533 |
+
|
| 534 |
+
---
|
| 535 |
+
|
| 536 |
+
## 🎓 Типичный workflow (порядок работы)
|
| 537 |
+
|
| 538 |
+
### Для ЛР №1:
|
| 539 |
+
|
| 540 |
+
1. Загрузите данные
|
| 541 |
+
2. Запустите предобработку
|
| 542 |
+
3. Посмотрите описательную статистику
|
| 543 |
+
4. Проверьте стационарность
|
| 544 |
+
5. Создайте лаги и скользящие статистики
|
| 545 |
+
6. Постройте ACF/PACF
|
| 546 |
+
7. Выполните декомпозицию
|
| 547 |
+
8. Сгенерируйте отчёт
|
| 548 |
+
|
| 549 |
+
### Для ЛР №2:
|
| 550 |
+
|
| 551 |
+
1. Убедитесь, что данные из ЛР №1 загружены
|
| 552 |
+
2. Выполните декомпозицию
|
| 553 |
+
3. (Опционально) Создайте расширенные признаки
|
| 554 |
+
4. (Опционально) Выполните кросс-валидацию
|
| 555 |
+
5. Настройте преобразования (начните с none, diff_order=0)
|
| 556 |
+
6. Постройте модели (выберите несколько для сравнения)
|
| 557 |
+
7. Проверьте диагностику остатков для лучшей модели
|
| 558 |
+
8. Посмотрите итоговые результаты и выберите лучшую модель
|
| 559 |
+
|
| 560 |
+
---
|
| 561 |
+
|
| 562 |
+
## ⚠️ Частые проблемы и решения
|
| 563 |
+
|
| 564 |
+
### Проблема: "Недостаточно данных"
|
| 565 |
+
**Решение:** Уменьшите размер обучающей выборки или увеличьте тестовую
|
| 566 |
+
|
| 567 |
+
### Проблема: "Для лог-трансформации все значения должны быть положительными"
|
| 568 |
+
**Причина:**
|
| 569 |
+
Логарифм можно применять только к положительным числам. Если в данных есть нули или отрицательные значения, логарифм не может быть вычислен.
|
| 570 |
+
|
| 571 |
+
**Что покажет программа:**
|
| 572 |
+
- Количество неположительных значений
|
| 573 |
+
- Минимальное и максимальное значения
|
| 574 |
+
- График распределения данных
|
| 575 |
+
- Предложение автоматического сдвига
|
| 576 |
+
|
| 577 |
+
**Решения:**
|
| 578 |
+
|
| 579 |
+
1. **Используйте Box-Cox преобразование** (рекомендуется):
|
| 580 |
+
- Измените тип преобразования с `log` на `boxcox`
|
| 581 |
+
- Box-Cox автоматически обработает проблему (требует только положительные значения, но может работать с нулями через сдвиг)
|
| 582 |
+
|
| 583 |
+
2. **Используйте только дифференцирование**:
|
| 584 |
+
- Оставьте `transformation='none'`
|
| 585 |
+
- Установите `diff_order=1`
|
| 586 |
+
- Это уберёт тренд без необходимости логарифма
|
| 587 |
+
|
| 588 |
+
3. **Автоматический сдвиг данных**:
|
| 589 |
+
- Включите чекбокс "Автоматически сдвинуть данные"
|
| 590 |
+
- Программа добавит константу ко всем значениям, чтобы сделать их положительными
|
| 591 |
+
- Минимум станет равным 1 (или больше)
|
| 592 |
+
|
| 593 |
+
4. **Вручную сдвиньте данные** (перед загрузкой):
|
| 594 |
+
- Если данные содержат отрицательные значения или нули
|
| 595 |
+
- Добавьте константу ко всем значениям в CSV файле
|
| 596 |
+
- Например: `new_value = old_value + abs(min_value) + 1`
|
| 597 |
+
|
| 598 |
+
**Как выбрать решение:**
|
| 599 |
+
- Если данные близки к нулю → используйте Box-Cox
|
| 600 |
+
- Если нужно сохранить интерпретацию → используйте только дифференцирование
|
| 601 |
+
- Если данные уже в логарифмической шкале → проверьте, не применяли ли в�� логарифм дважды
|
| 602 |
+
|
| 603 |
+
### Проблема: "Ошибка при преобразовании" (общая)
|
| 604 |
+
**Решение:**
|
| 605 |
+
- Для логарифма: убедитесь, что все значения положительные (см. выше)
|
| 606 |
+
- Для Box-Cox: убедитесь, что все значения положительные (или используйте автоматический сдвиг)
|
| 607 |
+
|
| 608 |
+
### Проблема: "Модель работает плохо"
|
| 609 |
+
**Решение:**
|
| 610 |
+
1. Попробуйте дифференцирование (diff_order=1)
|
| 611 |
+
2. Попробуйте Box-Cox преобразование
|
| 612 |
+
3. Попробуйте другую модель (например, Holt вместо SES)
|
| 613 |
+
4. Проверьте, стационарен ли ряд
|
| 614 |
+
|
| 615 |
+
### Проблема: "Остатки имеют автокорреляцию"
|
| 616 |
+
**Решение:**
|
| 617 |
+
1. Попробуйте другую модель
|
| 618 |
+
2. Добавьте больше лагов
|
| 619 |
+
3. Попробуйте другую стратегию прогнозирования
|
| 620 |
+
|
| 621 |
+
---
|
| 622 |
+
|
| 623 |
+
## 💡 Советы
|
| 624 |
+
|
| 625 |
+
1. **Начинайте просто:** сначала попробуйте модель без преобразований
|
| 626 |
+
2. **Сравнивайте с baseline:** убедитесь, что ваша модель лучше наивного прогноза
|
| 627 |
+
3. **Проверяйте диагностику:** хорошая модель должна иметь "хорошие" остатки
|
| 628 |
+
4. **Экспериментируйте:** пробуйте разные комбинации моделей и стратегий
|
| 629 |
+
5. **Документируйте:** записывайте, что пробовали и какие результаты получили
|
| 630 |
+
|
| 631 |
+
---
|
| 632 |
+
|
| 633 |
+
## 📝 Что делать с результатами
|
| 634 |
+
|
| 635 |
+
1. **Сохраните очищенные данные:** скачайте `final_dataset.csv`
|
| 636 |
+
2. **Сохраните прогнозы:** скачайте прогнозы лучшей модели
|
| 637 |
+
3. **Сохраните параметры модели:** скачайте параметры для воспроизведения
|
| 638 |
+
4. **Создайте отчёт:** используйте HTML-отчёт из ЛР №1
|
| 639 |
+
5. **Задокументируйте выводы:** запишите, какая модель лучше и почему
|
| 640 |
+
|
| 641 |
+
---
|
| 642 |
+
|
| 643 |
+
## 🎯 Ключевые выводы
|
| 644 |
+
|
| 645 |
+
- **ЛР №1** учит понимать структуру данных
|
| 646 |
+
- **ЛР №2** учит строить модели для прогнозирования
|
| 647 |
+
- **Метрики** показывают качество модели
|
| 648 |
+
- **Диагностика** проверяет адекватность модели
|
| 649 |
+
- **Преобразования** нужны для стационарности
|
| 650 |
+
- **Стратегии** влияют на качество прогноза
|
| 651 |
+
|
| 652 |
+
---
|
| 653 |
+
|
| 654 |
+
**Удачи в работе! 🚀**
|
| 655 |
+
|
СТРУКТУРА_КОДА.md
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🏗️ Структура кода проекта
|
| 2 |
+
|
| 3 |
+
## 📁 Файловая структура
|
| 4 |
+
|
| 5 |
+
```
|
| 6 |
+
TimeSeriesHomework/
|
| 7 |
+
├── src/
|
| 8 |
+
│ ├── streamlit_app.py # Главное веб-приложение (1655 строк)
|
| 9 |
+
│ ├── lab2_functions.py # Функции для ЛР №2 (604 строки)
|
| 10 |
+
│ ├── main.py # Не используется (можно игнорировать)
|
| 11 |
+
│ └── russia_covid_dataset.csv # Пример данных
|
| 12 |
+
├── requirements.txt # Зависимости Python
|
| 13 |
+
├── README.md # Описание проекта
|
| 14 |
+
├── РУКОВОДСТВО.md # Подробное руководство (этот файл)
|
| 15 |
+
├── БЫСТРЫЙ_СТАРТ.md # Краткая шпаргалка
|
| 16 |
+
└── СТРУКТУРА_КОДА.md # Этот файл
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## 🔍 streamlit_app.py - Главное приложение
|
| 22 |
+
|
| 23 |
+
### Структура файла:
|
| 24 |
+
|
| 25 |
+
```
|
| 26 |
+
1. Импорты (строки 1-34)
|
| 27 |
+
├── Библиотеки: pandas, numpy, streamlit, plotly
|
| 28 |
+
├── Статистика: statsmodels, scipy
|
| 29 |
+
└── Импорт функций из lab2_functions.py
|
| 30 |
+
|
| 31 |
+
2. Утилиты (строки 45-94)
|
| 32 |
+
├── detect_date_column() - поиск колонки с датами
|
| 33 |
+
├── try_parse_dates() - парсинг дат
|
| 34 |
+
├── localize_to_moscow() - приведение к часовому поясу
|
| 35 |
+
├── detect_outliers_iqr() - поиск выбросов
|
| 36 |
+
└── winsorize_series() - обработка выбросов
|
| 37 |
+
|
| 38 |
+
3. Предобработка (строки 96-195)
|
| 39 |
+
└── preprocess_timeseries() - основная функция очистки данных
|
| 40 |
+
├── Парсинг дат
|
| 41 |
+
├── Обработка пропусков
|
| 42 |
+
├── Обработка выбросов
|
| 43 |
+
└── Ресемплирование
|
| 44 |
+
|
| 45 |
+
4. Описательная статистика (строки 198-217)
|
| 46 |
+
└── descriptive_statistics() - расчёт статистик
|
| 47 |
+
|
| 48 |
+
5. Стационарность (строки 220-234)
|
| 49 |
+
├── run_adf() - тест Дики-Фуллера
|
| 50 |
+
└── run_kpss() - тест KPSS
|
| 51 |
+
|
| 52 |
+
6. Лаги и скользящие (строки 237-274)
|
| 53 |
+
├── create_lags_and_rolls() - создание лагов
|
| 54 |
+
├── compute_lag_correlations() - корреляции лагов
|
| 55 |
+
└── compute_vif() - проверка мультиколлинеарности
|
| 56 |
+
|
| 57 |
+
7. ACF/PACF (строки 277-316)
|
| 58 |
+
├── get_acf_pacf_with_conf() - расчёт ACF/PACF
|
| 59 |
+
├── significant_lags_from_conf() - значимые лаги
|
| 60 |
+
└── plotly_acf_pacf() - визуализация
|
| 61 |
+
|
| 62 |
+
8. Генерация отчёта (строки 319-359)
|
| 63 |
+
└── generate_html_report() - создание HTML-отчёта
|
| 64 |
+
|
| 65 |
+
9. Функция ЛР №1 (строки 362-844)
|
| 66 |
+
└── render_lab1() - весь интерфейс ЛР №1
|
| 67 |
+
├── Загрузка данных
|
| 68 |
+
├── Предобработка
|
| 69 |
+
├── Описательная статистика
|
| 70 |
+
├── Стационарность
|
| 71 |
+
├── Лаги и скользящие
|
| 72 |
+
├── ACF/PACF
|
| 73 |
+
├── Декомпозиция
|
| 74 |
+
└── Генерация отчёта
|
| 75 |
+
|
| 76 |
+
10. Функция ЛР №2 (строки 846-1646)
|
| 77 |
+
└── render_lab2() - весь интерфейс ЛР №2
|
| 78 |
+
├── Декомпозиция
|
| 79 |
+
├── Feature Engineering
|
| 80 |
+
├── Кросс-валидация
|
| 81 |
+
├── Преобразования
|
| 82 |
+
├── Модели экспоненциального сглаживания
|
| 83 |
+
├── Диагностика остатков
|
| 84 |
+
└── Сравнительный анализ
|
| 85 |
+
|
| 86 |
+
11. Главный код (строки 1648-1654)
|
| 87 |
+
└── Выбор лабораторной работы через sidebar
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
### Поток данных в ЛР №1:
|
| 91 |
+
|
| 92 |
+
```
|
| 93 |
+
Загрузка CSV
|
| 94 |
+
↓
|
| 95 |
+
preprocess_timeseries()
|
| 96 |
+
↓
|
| 97 |
+
df_clean (очищенные данные)
|
| 98 |
+
↓
|
| 99 |
+
descriptive_statistics() → Таблица статистик
|
| 100 |
+
↓
|
| 101 |
+
run_adf() / run_kpss() → Тесты стационарности
|
| 102 |
+
↓
|
| 103 |
+
create_lags_and_rolls() → Датасет с лагами
|
| 104 |
+
↓
|
| 105 |
+
get_acf_pacf_with_conf() → Графики ACF/PACF
|
| 106 |
+
↓
|
| 107 |
+
seasonal_decompose() → Декомпозиция
|
| 108 |
+
↓
|
| 109 |
+
generate_html_report() → HTML-отчёт
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
### Поток данных в ЛР №2:
|
| 113 |
+
|
| 114 |
+
```
|
| 115 |
+
df_clean (из ЛР №1)
|
| 116 |
+
↓
|
| 117 |
+
Разделение на train/test
|
| 118 |
+
↓
|
| 119 |
+
seasonal_decompose() → Декомпозиция
|
| 120 |
+
↓
|
| 121 |
+
create_advanced_features() → Расширенные признаки (опционально)
|
| 122 |
+
↓
|
| 123 |
+
apply_transformations() → Преобразования к стационарности
|
| 124 |
+
↓
|
| 125 |
+
create_exponential_smoothing_model() → Модель
|
| 126 |
+
↓
|
| 127 |
+
recursive_forecast() / direct_forecast() / hybrid_forecast() → Прогноз
|
| 128 |
+
↓
|
| 129 |
+
inverse_transformations() → Обратное преобразование
|
| 130 |
+
↓
|
| 131 |
+
evaluate_forecast() → Метрики (MAE, RMSE, MAPE)
|
| 132 |
+
↓
|
| 133 |
+
diagnose_model_residuals() → Диагностика остатков
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
## 🔧 lab2_functions.py - Функции для ЛР №2
|
| 139 |
+
|
| 140 |
+
### Структура файла:
|
| 141 |
+
|
| 142 |
+
```
|
| 143 |
+
1. Метрики (строки 18-25)
|
| 144 |
+
└── calculate_mape() - расчёт MAPE
|
| 145 |
+
|
| 146 |
+
2. Feature Engineering (строки 28-70)
|
| 147 |
+
└── create_advanced_features() - создание признаков
|
| 148 |
+
├── Временные признаки (день недели, месяц)
|
| 149 |
+
├── Циклические признаки (sin/cos)
|
| 150 |
+
├── Лаги (lag_1, lag_7, lag_30)
|
| 151 |
+
└── Скользящие статистики (mean, std, min, max)
|
| 152 |
+
|
| 153 |
+
3. Преобразования (строки 73-223)
|
| 154 |
+
├── apply_boxcox_transform() - преобразование Бокса-Кокса
|
| 155 |
+
├── inverse_boxcox_transform() - обратное преобразование
|
| 156 |
+
├── apply_transformations() - цепочка преобразований
|
| 157 |
+
└── inverse_transformations() - обратная цепочка
|
| 158 |
+
|
| 159 |
+
4. Стратегии прогнозирования (строки 226-413)
|
| 160 |
+
├── recursive_forecast() - рекурсивная стратегия
|
| 161 |
+
├── direct_forecast() - прямая стратегия
|
| 162 |
+
└── hybrid_forecast() - гибридная стратегия
|
| 163 |
+
|
| 164 |
+
5. Модели (строки 416-435)
|
| 165 |
+
└── create_exponential_smoothing_model() - создание модели
|
| 166 |
+
|
| 167 |
+
6. Оценка качества (строки 438-457)
|
| 168 |
+
├── evaluate_forecast() - метрики качества
|
| 169 |
+
└── naive_forecast() - наивный прогноз (baseline)
|
| 170 |
+
|
| 171 |
+
7. Кросс-валидация (строки 460-535)
|
| 172 |
+
├── time_series_cv_sliding_window() - скользящее окно
|
| 173 |
+
└── time_series_cv_expanding_window() - расширяющееся окно
|
| 174 |
+
|
| 175 |
+
8. Диагностика (строки 538-602)
|
| 176 |
+
└── diagnose_model_residuals() - диагностика остатков
|
| 177 |
+
├── Тест Льюнга-Бокса (автокорреляция)
|
| 178 |
+
├── Тест Шапиро-Уилка (нормальность)
|
| 179 |
+
└── Тесты стационарности (ADF/KPSS)
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
### Зависимости между функциями:
|
| 183 |
+
|
| 184 |
+
```
|
| 185 |
+
apply_transformations()
|
| 186 |
+
↓
|
| 187 |
+
create_exponential_smoothing_model()
|
| 188 |
+
↓
|
| 189 |
+
recursive_forecast() / direct_forecast() / hybrid_forecast()
|
| 190 |
+
↓
|
| 191 |
+
inverse_transformations()
|
| 192 |
+
↓
|
| 193 |
+
evaluate_forecast()
|
| 194 |
+
↓
|
| 195 |
+
diagnose_model_residuals()
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
---
|
| 199 |
+
|
| 200 |
+
## 🔄 Как работает программа
|
| 201 |
+
|
| 202 |
+
### 1. Запуск приложения
|
| 203 |
+
|
| 204 |
+
```python
|
| 205 |
+
# streamlit_app.py, строка 1648
|
| 206 |
+
if lab_choice == "ЛР №1: ...":
|
| 207 |
+
render_lab1()
|
| 208 |
+
elif lab_choice == "ЛР №2: ...":
|
| 209 |
+
render_lab2()
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
### 2. Выбор лабораторной работы
|
| 213 |
+
|
| 214 |
+
Пользователь выбирает в sidebar (боковая панель):
|
| 215 |
+
- "ЛР №1: Введение в анализ временных рядов"
|
| 216 |
+
- "ЛР №2: Прогнозирование временных рядов"
|
| 217 |
+
|
| 218 |
+
### 3. Сохранение состояния
|
| 219 |
+
|
| 220 |
+
Программа использует `st.session_state` для сохранения:
|
| 221 |
+
- `df_in` - исходные данные
|
| 222 |
+
- `df_clean` - очищенные данные
|
| 223 |
+
- `df_lags` - данные с лагами
|
| 224 |
+
- `decomp` - результат декомпозиции
|
| 225 |
+
- `forecast_results` - результаты прогнозирования
|
| 226 |
+
- `models` - обученные модели
|
| 227 |
+
|
| 228 |
+
### 4. Обработка данных
|
| 229 |
+
|
| 230 |
+
Все преобразования применяются последовательно:
|
| 231 |
+
1. Загрузка → `df_in`
|
| 232 |
+
2. Предобработка → `df_clean`
|
| 233 |
+
3. Создание лагов → `df_lags`
|
| 234 |
+
4. Преобразования → `s_train_transformed`
|
| 235 |
+
5. Прогнозирование → `forecast`
|
| 236 |
+
6. Обратное преобразование → `forecast_original`
|
| 237 |
+
|
| 238 |
+
---
|
| 239 |
+
|
| 240 |
+
## 📊 Ключевые функции и их назначение
|
| 241 |
+
|
| 242 |
+
### Предобработка:
|
| 243 |
+
|
| 244 |
+
| Функция | Что делает |
|
| 245 |
+
|---------|------------|
|
| 246 |
+
| `preprocess_timeseries()` | Основная функция очистки данных |
|
| 247 |
+
| `detect_outliers_iqr()` | Находит выбросы методом IQR |
|
| 248 |
+
| `winsorize_series()` | Обрезает экстремальные значения |
|
| 249 |
+
|
| 250 |
+
### Анализ:
|
| 251 |
+
|
| 252 |
+
| Функция | Что делает |
|
| 253 |
+
|---------|------------|
|
| 254 |
+
| `descriptive_statistics()` | Расчёт статистик (mean, std, etc.) |
|
| 255 |
+
| `run_adf()` | Тест стационарности ADF |
|
| 256 |
+
| `run_kpss()` | Тест стационарности KPSS |
|
| 257 |
+
| `get_acf_pacf_with_conf()` | Расчёт ACF/PACF с доверительными интервалами |
|
| 258 |
+
|
| 259 |
+
### Прогнозирование:
|
| 260 |
+
|
| 261 |
+
| Функция | Что делает |
|
| 262 |
+
|---------|------------|
|
| 263 |
+
| `apply_transformations()` | Применяет преобразования (log, boxcox, diff) |
|
| 264 |
+
| `create_exponential_smoothing_model()` | Создаёт модель экспоненциального сглаживания |
|
| 265 |
+
| `recursive_forecast()` | Рекурсивная стратегия прогнозирования |
|
| 266 |
+
| `direct_forecast()` | Прямая стратегия прогнозирования |
|
| 267 |
+
| `hybrid_forecast()` | Гибридная стратегия прогнозирования |
|
| 268 |
+
| `inverse_transformations()` | Обратное преобразование прогнозов |
|
| 269 |
+
|
| 270 |
+
### Оценка:
|
| 271 |
+
|
| 272 |
+
| Функция | Что делает |
|
| 273 |
+
|---------|------------|
|
| 274 |
+
| `evaluate_forecast()` | Расчёт метрик (MAE, RMSE, MAPE) |
|
| 275 |
+
| `diagnose_model_residuals()` | Диагностика остатков модели |
|
| 276 |
+
|
| 277 |
+
---
|
| 278 |
+
|
| 279 |
+
## 🎯 Где что искать
|
| 280 |
+
|
| 281 |
+
### Если нужно изменить предобработку:
|
| 282 |
+
→ `streamlit_app.py`, функция `preprocess_timeseries()` (строки 97-195)
|
| 283 |
+
|
| 284 |
+
### Если нужно добавить новую метрику:
|
| 285 |
+
→ `lab2_functions.py`, функция `evaluate_forecast()` (строки 438-451)
|
| 286 |
+
|
| 287 |
+
### Если нужно изменить визуализацию:
|
| 288 |
+
→ `streamlit_app.py`, функции `render_lab1()` или `render_lab2()`
|
| 289 |
+
|
| 290 |
+
### Если нужно добавить новую модель:
|
| 291 |
+
→ `lab2_functions.py`, функция `create_exponential_smoothing_model()` (строки 416-435)
|
| 292 |
+
|
| 293 |
+
### Если нужно изменить стратегию прогнозирования:
|
| 294 |
+
→ `lab2_functions.py`, функции `recursive_forecast()`, `direct_forecast()`, `hybrid_forecast()`
|
| 295 |
+
|
| 296 |
+
---
|
| 297 |
+
|
| 298 |
+
## 🔗 Связи между модулями
|
| 299 |
+
|
| 300 |
+
```
|
| 301 |
+
streamlit_app.py
|
| 302 |
+
│
|
| 303 |
+
├── Импортирует функции из lab2_functions.py
|
| 304 |
+
│ ├── create_advanced_features()
|
| 305 |
+
│ ├── apply_transformations()
|
| 306 |
+
│ ├── recursive_forecast()
|
| 307 |
+
│ ├── direct_forecast()
|
| 308 |
+
│ ├── hybrid_forecast()
|
| 309 |
+
│ ├── create_exponential_smoothing_model()
|
| 310 |
+
│ ├── evaluate_forecast()
|
| 311 |
+
│ └── diagnose_model_residuals()
|
| 312 |
+
│
|
| 313 |
+
└── Использует библиотеки:
|
| 314 |
+
├── pandas - работа с данными
|
| 315 |
+
├── numpy - численные вычисления
|
| 316 |
+
├── streamlit - веб-интерфейс
|
| 317 |
+
├── plotly - интерактивные графики
|
| 318 |
+
├── statsmodels - статистические модели
|
| 319 |
+
└── scipy - научные вычисления
|
| 320 |
+
```
|
| 321 |
+
|
| 322 |
+
---
|
| 323 |
+
|
| 324 |
+
## 📝 Примечания
|
| 325 |
+
|
| 326 |
+
1. **session_state** используется для сохранения состояния между перезагрузками страницы
|
| 327 |
+
2. **Обработка ошибок** - большинство функций имеют try/except блоки
|
| 328 |
+
3. **Визуализация** - используется Plotly для интерактивных графиков
|
| 329 |
+
4. **Модульность** - функции разделены по назначению (предобработка, анализ, прогнозирование)
|
| 330 |
+
|
| 331 |
+
---
|
| 332 |
+
|
| 333 |
+
**Для подробного понимания работы программы см. `РУКОВОДСТВО.md`**
|
| 334 |
+
|