Прогнозирование нагрузки на энергосистему с визуализацией
В этом руководстве мы покажем, как использовать NeuralProphet для прогнозирования нагрузки энергосистемы и визуализации результатов в интерактивном инструменте. Прогноз основан на наборе данных RE-Europe, который моделирует континентальную электроэнергетическую систему Европы, включая потребности в энергии и притоки возобновляемой энергии за период с 2012 по 2014 год. Общий набор данных содержит 1494 узла, которые мы сократим до 5 узлов для целей данного примера.
# Импорт библиотек для работы с данными:import pandas as pd # Библиотека pandas для анализа и обработки данныхimport numpy as np # Библиотека numpy для численных вычислений# Импорт библиотек NeuralProphet для прогнозирования временных рядов:from neuralprophet import NeuralProphet, set_log_level # Класс NeuralProphet и функция set_log_level# Импорт библиотек Plotly для создания визуализаций:import plotly.graph_objects as go # Модуль go из Plotly для создания статических элементов визуализацииimport plotly.express as px # Модуль px из Plotly для создания выразительных диаграмм с минимальным кодомfrom plotly.subplots import make_subplots # Функция make_subplots из Plotly для создания подсюжетов# Импорт библиотек ipywidgets для создания интерактивных элементов:import ipywidgets as widgets # Модуль widgets из ipywidgets для создания интерактивных элементов в Jupyter Notebookfrom ipywidgets import interact_manual # Функция interact_manual из ipywidgets для создания интерактивных элементов управления
# Установка режима визуализации по умолчанию:plotting_backend = "plotly-static" # Установить режим визуализации по умолчанию на "plotly-static" для статических изображений
Набор данных включает для каждого автобуса и каждого часа года следующую информацию:
нагрузка (МВт)
фактическое производство солнечной энергии (МВт)
прогнозируемое производство солнечной энергии (МВт)
The forecasts are at hour 00 of every day for the next 24 hours.
# Загрузка данныхdf = pd.read_csv( "https://raw.githubusercontent.com/ourownstory/neuralprophet-data/main/datasets/multivariate/ER_Europe_subset_10nodes.csv",
# Чтение данных из CSV файла по ссылке)df["ID"]= df["ID"].astype(str)# Преобразование столбца "ID" в строковый типIDs = df["ID"].unique()# Получение уникальных значений из столбца "ID"df["ds"]= pd.to_datetime(df["ds"])# Преобразование столбца "ds" в формат даты и времени# Использование данных за один год для ускорения обученияdf = df[df["ds"]>"2014-01-01"]# Фильтрация данных с датой позже 01.01.2014# Вывод первых 5 строк измененного dataframedf.head()
ds
ID
y
solar
solar_fcs
17545
2014-01-01 01:00:00
1
72.3142
0.0
0.0
17546
2014-01-01 02:00:00
1
67.5617
0.0
0.0
17547
2014-01-01 03:00:00
1
63.0677
0.0
0.0
17548
2014-01-01 04:00:00
1
59.9401
0.0
0.0
17549
2014-01-01 05:00:00
1
58.9296
0.0
0.0
# Построение графика первого месяца данныхfig = px.line( # Создать линейный график с помощью plotly.express df[df["ds"] <"2014-02-01"], # Фильтрация данных за январь 2014 x="ds", # Значения по оси X - дата y="y", # Значения по оси Y - энергопотребление color="ID", # Цвет линии определяется по столбцу "ID")# Настройка оформления графикаfig.update_layout( xaxis_title="Дата", # Заголовок оси X - Дата yaxis_title="Потребление энергии", # Заголовок оси Y - Потребление энергии legend_title="ID", # Заголовок легенды - ID height=600, # Высота графика width=1000, # Ширина графика)# Отображение графика в зависимости от режима визуализацииif plotting_backend =="plotly-static": fig.show("svg")# Показать график в формате SVG (статическое изображение)
1. Подготовка данных
Чтобы лучше улавливать разные времена года и дни недели, мы добавляем условную сезонность. Для каждого времени года (лето, зима, осень и весна) и будни/выходные моделируется отдельная сезонность.
# Условная сезонность: 4 сезона для годовой и недельной сезонностиdf["summer"]=0# Создать столбец "summer" (лето) с начальными нулямиdf.loc[df["ds"].dt.month.isin([6, 7, 8]),"summer"]=1# Лето - июнь, июль, августdf["winter"]=0# Создать столбец "winter" (зима) с начальными нулямиdf.loc[df["ds"].dt.month.isin([12, 1, 2]),"winter"]=1# Зима - декабрь, январь, февральdf["fall"]=0# Создать столбец "fall" (осень) с начальными нулямиdf.loc[df["ds"].dt.month.isin([9, 10, 11]),"fall"]=1# Осень - сентябрь, октябрь, ноябрьdf["spring"]=0# Создать столбец "spring" (весна) с начальными нулямиdf.loc[df["ds"].dt.month.isin([3, 4, 5]),"spring"]=1# Весна - март, апрель, май# Условная сезонность: 4 сезона, различие будний день/выходные для каждого сезона, для суточной сезонностиdf["summer_weekday"]=0# Создать столбец "summer_weekday" (будний день летом) с начальными нулямиdf.loc[(df["ds"].dt.month.isin([6, 7, 8])) & (df["ds"].dt.dayofweek.isin([0, 1, 2, 3, 4])),"summer_weekday"]=1# Лето будний день - пн, вт, ср, чт, пт (dt.dayofweek - 0 - понедельник)df["summer_weekend"]=0# Создать столбец "summer_weekend" (выходные летом) с начальными нулямиdf.loc[(df["ds"].dt.month.isin([6, 7, 8])) & (df["ds"].dt.dayofweek.isin([5, 6])),"summer_weekend"]=1# Лето выходные - сб, вс (dt.dayofweek - 5 - суббота, 6 - воскресенье)df["winter_weekday"]=0# Аналогично для зимы (winter*)df.loc[(df["ds"].dt.month.isin([12, 1, 2])) & (df["ds"].dt.dayofweek.isin([0, 1, 2, 3, 4])),"winter_weekday"]=1df["winter_weekend"]=0df.loc[(df["ds"].dt.month.isin([12, 1, 2])) & (df["ds"].dt.dayofweek.isin([5, 6])),"winter_weekend"]=1df["spring_weekday"]=0# Аналогично для весны (spring*)df.loc[(df["ds"].dt.month.isin([3, 4, 5])) & (df["ds"].dt.dayofweek.isin([0, 1, 2, 3, 4])),"spring_weekday"]=1df["spring_weekend"]=0df.loc[(df["ds"].dt.month.isin([3, 4, 5])) & (df["ds"].dt.dayofweek.isin([5, 6])),"spring_weekend"]=1df["fall_weekday"]=0# Аналогично для осени (fall*)df.loc[(df["ds"].dt.month.isin([9, 10, 11])) & (df["ds"].dt.dayofweek.isin([0, 1, 2, 3, 4])),"fall_weekday"]=1df["fall_weekend"]=0df.loc[(df["ds"].dt.month.isin([9, 10, 11])) & (df["ds"].dt.dayofweek.isin([5, 6])),"fall_weekend"]=1# Вывод первых 5 строк измененного dataframedf.head()
ds
ID
y
solar
solar_fcs
summer
winter
fall
spring
summer_weekday
summer_weekend
winter_weekday
winter_weekend
spring_weekday
spring_weekend
fall_weekday
fall_weekend
17545
2014-01-01 01:00:00
1
72.3142
0.0
0.0
0
1
0
0
0
0
1
0
0
0
0
0
17546
2014-01-01 02:00:00
1
67.5617
0.0
0.0
0
1
0
0
0
0
1
0
0
0
0
0
17547
2014-01-01 03:00:00
1
63.0677
0.0
0.0
0
1
0
0
0
0
1
0
0
0
0
0
17548
2014-01-01 04:00:00
1
59.9401
0.0
0.0
0
1
0
0
0
0
1
0
0
0
0
0
17549
2014-01-01 05:00:00
1
58.9296
0.0
0.0
0
1
0
0
0
0
1
0
0
0
0
0
2. Определение модели
Лучшие параметры для модели можно получить с помощью настройки гиперпараметров. Установив квантили, моделируется интервал прогнозирования, а не точный прогноз.
# Модель и прогнозированиеquantiles = [0.015,0.985] # Установить квантили для прогнозных интервалов (1.5% и 98.5%)# Параметры моделиparams ={"n_lags":7*24,# Количество прошлых наблюдений для прогноза (7 дней * 24 часа)"n_forecasts":24,# Количество прогнозируемых шагов (24 часа)"n_changepoints":20,# Количество точек изменения тренда"learning_rate":0.01,# Скорость обучения"ar_layers": [32,16,16,32],# Слои авторегрессии с шириной 32, 16, 16, 32 единиц"yearly_seasonality":10,# Учитывать сезонность с периодом 10 (вероятно, подразумевается год)"weekly_seasonality":False,# Игнорировать недельную сезонность"daily_seasonality":False,# Игнорировать суточную сезонность"epochs":40,# Количество эпох обучения"batch_size":1024,# Размер пакета данных для обучения"quantiles": quantiles,# Квантили для прогнозных интервалов}# Создать модель NeuralProphet с указанными параметрамиm =NeuralProphet(**params)# Установить режим визуализации для моделиm.set_plotting_backend(plotting_backend)# Отключить вывод сообщений ниже уровня ERRORset_log_level("ERROR")
2.1 Запаздывающие и будущие регрессоры
С дополнительной информацией наша модель может стать лучше. Мы используем данные о фактическом производстве солнечной энергии за последние 10 дней в качестве отстающего регрессора и прогноз солнечной активности в качестве будущего регрессора.
# Добавление запаздывающего регрессораm.add_lagged_regressor(names="solar", n_lags=10*24)# Добавить солнечную активность за 10 дней (24 часа/день)# Добавление регрессора для будущих значенийm.add_future_regressor(name="solar_fcs")# Добавить прогноз солнечной активности ("solar_fcs")
<neuralprophet.forecaster.NeuralProphet at 0x12cc57b20>
<neuralprophet.forecaster.NeuralProphet at 0x12cc57b20>
3. Обучение модели
Для последующего тестирования нашей модели данные разделяются на обучающие и тестовые. Тестовые данные составляют последние 5% от всего массива данных. Модель обучается на обучающих данных.
# Разделение данных на обучающую и тестовую выборкиdf_train, df_test = m.split_df(df, valid_p=0.05, local_split=True)# Разделить данные на 95% обучающих и 5% тестовыхprint(f"Размер обучающей выборки: {df_train.shape}")# Вывод формы обучающей выборкиprint(f"Размер тестовой выборки: {df_test.shape}")# Вывод формы тестовой выборки
Train shape: (41545, 17)
Test shape: (3090, 17)
# Обучение моделиmetrics_fit = m.fit(df_train, freq="H", metrics=True) # Обучить модель на обучающей выборке (freq - частота данных - часы)
# Прогнозированиеforecast = m.predict(df)# Сделать прогнозы на основе всего dataframe (df)
# Извлечение столбцов с сезонностью и трендом и преобразование в числовой форматseason_cols = [col for col in forecast.columns if "season" in col or "trend" in col] # Поиск столбцов с "season" или "trend" в названии
# Преобразование значений в столбцах season_cols в числовой форматforecast[season_cols]= forecast[season_cols].apply(pd.to_numeric)
4. Визуализация
4.1 Модельные участки
NeuralProphet включает функции построения графиков для изучения прогнозируемых данных. Для обзора функций построения графиков см. учебное пособие по визуализации
Давайте изучим данные более интерактивно. Выбрав дату, будет показан прогноз на следующие 24 часа, что соответствует стандартам индустрии.
defget_day_ahead_format(date,ID,column_name):""" Функция извлекает прогноз на следующие сутки для указанной даты, ID узла и названия столбца. Args: date (pandas.Timestamp): Дата, для которой требуется прогноз. ID (str): ID узла, для которого требуется прогноз. column_name (str): Название столбца с прогнозируемой величиной (например, "y" или "yhat"). Returns: pandas.DataFrame: Таблица с прогнозами на следующие сутки. Raises: ValueError: Ошибка, если запрашиваемая дата отсутствует в прогнозе. """# Создать пустой DataFrame для хранения значений прогноза values = pd.DataFrame(columns=["ds", column_name])# Отфильтровать прогнозы по ID узла forecast_ID = forecast[forecast["ID"]== ID]# Цикл по 24 часам для формирования прогноза на следующие суткиfor i inrange(0, 24):# Формирование названия столбца с прогнозом для i-го часаif"%"in column_name: step_name = f"yhat{i+1} {column_name}"# Например, yhat10 1.5% (прогноз с 10-го часа, 1.5% квантиль)else: step_name = f"{column_name}{i+1}"# Например, yhat10 (прогноз на 10-й час)# Дата следующего часа ds = date + pd.Timedelta(hours=i +1)# Проверка наличия даты в прогнозах для данного IDif ds notin forecast_ID["ds"].values:raiseValueError( f'Дата {ds} отсутствует в прогнозе. Выберите дату в диапазоне от {forecast_ID["ds"].min()} до {forecast_ID["ds"].max()}'
)# Получение значения прогноза для i-го часа step_value = forecast_ID[forecast_ID["ds"]== ds][step_name].values[0]# Добавление строки с прогнозом для i-го часа в DataFrame values = pd.concat([values, pd.DataFrame({"ds": ds, column_name: step_value}, index=[0])], ignore_index=True)return values
@interact_manual# с этой строкой кода прогноз становится интерактивным, позволяя вам выбирать идентификатор и датуdefshow_forecast_uncertainty(ID=IDs,date=widgets.DatePicker()): start = pd.to_datetime(date) end = start + pd.DateOffset(days=1)# получаем прогноз np_yhats =get_day_ahead_format(start, ID, "yhat") np_yhats_lower =get_day_ahead_format(start, ID, "1.5%") np_yhats_upper =get_day_ahead_format(start, ID, "98.5%") np_ar =get_day_ahead_format(start, ID, "ar")# получаем фактические значения, температуру и прогноз температуры forecast_window = forecast[forecast["ID"]== ID] forecast_window = forecast_window[(forecast_window["ds"]>= start) & (forecast_window["ds"]< end)] df_window = df[df["ID"]== ID] df_window = df_window[(df_window["ds"]>= start) & (df_window["ds"]< end)]# получаем компоненты seasonalities_to_plot = []for season in season_cols:if np.abs(forecast_window[season].sum())>0: seasonalities_to_plot.append(season) fig =make_subplots( rows=3, cols=1, shared_xaxes=True, subplot_titles=("Фактические значения и прогноз", "Солнечная энергия", "Компоненты прогноза"), )# рисуем фактические значения fig.add_trace( go.Scatter( x=forecast_window["ds"], y=forecast_window["y"], name="Фактические значения", mode="markers", marker=dict(symbol="x")
), row=1, col=1, )# рисуем прогноз и неопределенность fig.add_trace( go.Scatter( x=np_yhats_lower["ds"], y=np_yhats_lower["1.5%"], fill=None, mode="lines", line_color="#A6B168", name="1.5% квантиль", ), row=1, col=1, ) fig.add_trace( go.Scatter( x=np_yhats_upper["ds"], y=np_yhats_upper["98.5%"], fill="tonexty", mode="lines", line_color="#A6B168", name="98.5% квантиль", ), row=1, col=1, ) fig.add_trace(go.Scatter(x=np_yhats["ds"], y=np_yhats["yhat"], name="Прогноз", line_color="#7A863B"), row=1, col=1)# рисуем солнечную энергию fig.add_trace( go.Scatter(x=df_window["ds"], y=df_window["solar"], mode="lines", name="Фактическое производство солнечной энергии"), row=2, col=1
) fig.add_trace( go.Scatter(x=df_window["ds"], y=df_window["solar_fcs"], mode="lines", name="Прогноз производства солнечной энергии"),
row=2, col=1, )# рисуем различные сезоны из seasonalities_to_plotfor season in seasonalities_to_plot: fig.add_trace(go.Scatter(x=forecast_window["ds"], y=forecast_window[season], name=season), row=3, col=1) fig.add_trace(go.Scatter(x=np_ar["ds"], y=np_ar["ar"], name="Авторегрессия"), row=3, col=1) fig.update_layout( title=f"Прогноз для ID {ID} на {date}", yaxis=dict(title="Нагрузка (МВт)"), width=1200, height=800, )if plotting_backend =="plotly-static": fig.show("svg")else: fig.show()