Kolesnikov Dmitry commited on
Commit
eaf6e74
·
1 Parent(s): d5266eb

feat: Вторая лабораторка

Browse files
README.md CHANGED
@@ -1,19 +1,100 @@
1
  ---
2
- title: TimeSeriesHomework1 2
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
  app_port: 8501
8
  tags:
9
  - streamlit
 
 
 
10
  pinned: false
11
- short_description: Streamlit template space
12
  ---
13
 
14
- # Welcome to Streamlit!
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
 
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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="TS Preprocess & EDA (3.2–3.8)", layout="wide")
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
- # ---------------- Streamlit UI ----------------
339
- st.title("Временные ряды — предобработка, EDA, стационарность, лаги, ACF/PACF, декомпозиция и экспорт (3.2–3.8)")
 
 
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
- sample_option = 'russia_covid_dataset.csv'
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
- st.session_state['df_in'] = None
362
 
363
- if uploaded_file is not None:
364
- try:
365
- if uploaded_file.name.endswith('.parquet'):
366
- df_in = pd.read_parquet(uploaded_file)
367
- else:
368
- df_in = pd.read_csv(uploaded_file, low_memory=False)
369
- st.session_state['df_in'] = df_in
370
- st.success(f"Загружен файл: {uploaded_file.name} ({df_in.shape[0]}×{df_in.shape[1]})")
371
- except Exception as e:
372
- st.error(f"Ошибка загрузки: {e}")
373
- st.stop()
374
- elif sample_choice:
375
- st.session_state['df_in'] = pd.read_csv(sample_choice, low_memory=False)
376
- st.info(f"Выбран пример: {sample_choice}")
377
- else:
378
- local_path = 'russia_covid_dataset.csv'
379
- if st.session_state['df_in'] is None and os.path.exists(local_path):
380
- st.session_state['df_in'] = pd.read_csv(local_path, low_memory=False)
381
- st.info(f"Авто-загружен локальный файл {local_path}")
382
- elif st.session_state['df_in'] is None:
383
- st.info("Загрузите файл или поместите russia_covid_dataset.csv в рабочую папку.")
 
 
 
 
 
 
 
 
 
 
 
 
384
  st.stop()
385
 
386
-
387
- df_in = st.session_state['df_in']
388
- st.subheader("Preview входного датасета")
389
- st.dataframe(df_in.head(8))
390
-
391
- # detect date column
392
- detected = detect_date_column(df_in)
393
- col_for_date = st.text_input("Колонка с временной меткой", value=detected if detected else "")
394
- if not col_for_date:
395
- st.error("Укажите колонку с временной меткой.")
396
- st.stop()
397
-
398
- # Run buttons
399
- col1, col2 = st.columns([1, 1])
400
- with col1:
401
- run_btn = st.button("Run Preprocessing")
402
- with col2:
403
- force_btn = st.button("Force Recompute (пересчитать)")
404
-
405
- # session keys
406
- st.session_state.setdefault('preprocessed', False)
407
- st.session_state.setdefault('df_clean', None)
408
- st.session_state.setdefault('info', {})
409
- st.session_state.setdefault('df_lags', None)
410
-
411
- if run_btn or force_btn or (not st.session_state['preprocessed'] and st.session_state['df_clean'] is None):
412
- df_clean, info = preprocess_timeseries(
413
- df_in,
414
- date_col=col_for_date,
415
- tz_assume=tz_assume,
416
- numeric_missing_strategy=numeric_missing_strategy,
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
+