Машинное обучение в энергетике, или не только лишь все могут смотреть в завтрашний день

    Точное предсказание будущих событий — перспективная и интересная задача во многих сферах: от прогноза погоды до финтеха (котировки акций, курсы валют). Машинное обучение уже сегодня позволяет значительно сократить время и трудозатраты на принятие управленческих решений. 

    Наша Data Science команда в НОРБИТ около полугода экспериментировала с использованием различных моделей машинного обучения для решения задач по классификации и регрессии, и по оптимизации бизнес-процессов в сфере b2b. Но когда появилась задача по предсказанию временных рядов, оказалось, что доступных материалов на эту тема в сети недостаточно для разработки быстрого решения.


    Суть задачи заключалась в том, чтобы с максимальной точностью (средняя абсолютная ошибка в процентах, или MAPE < 3%) предсказывать почасовое потребление целого города на следующие трое суток суток (72 часа) для нужд одной крупной электрогенерирующей компании.

    Максимальная допустимая ошибка в 3% — это очень высокий показатель по точности прогнозов. До применения машинного обучения ошибка находилась в пределах 3-23%, что напрямую приводило к финансовым убыткам, так как при недостаточной выработке электрической энергии дополнительные мощности необходимо было покупать на оптовом рынке электроэнергии (что сильно дороже собственной выработки), при перепроизводстве — продавать на рынке дешевле, чем могли бы продать городу. Также негативным эффектом отклонения плановой нагрузки от фактической является изменение частоты переменного тока в сети.

    Достижению высокой точности прогнозирования мешают много факторов, например, при внезапном повышении температуры люди сразу тянутся за пультами от кондиционеров, при понижении — достают с антресолей обогреватели; во время праздников или больших футбольных матчей включают телевизоры и т.д. Если некоторые события цикличны и потенциально предсказуемы, то другие — совершенно нет. Допустим, внезапно по причине аварии перестал работать сталелитейный цех и все прогнозы превратились в тыкву (в нашем целевом городе обошлось без крупных производственных предприятий). Был довольно интересный пример проблемы при построении прогнозов в энергетике, когда один из всплесков изменений в объеме вырабатываемой электроэнергии был вызван тем, что корабль сел на мель на озере, и капитан договорился с владельцами гидроэлектростанций, чтобы они уменьшили сброс воды. Естественно, что модель машинного обучения не смогла предугадать это событие на основании накопленных исторических данных.

    Хорошая новость для нас, что заказчик предусмотрел в техническом задании такие «особенные дни», в которые была допустима средняя ошибка прогноза в 8%.

    Все, что предоставил заказчик, — это исторические данные почасового потребления электроэнергии за 3 года и название города.


    Первая задача для подготовки построения модели машинного обучения — визуализация и поиск дополнительных факторов, которые могут оказывать влияние на прогноз. Степень такого влияния удобно визуально оценивать с помощью тепловой карты корреляции признаков. Темный квадрат в данном случае означает безусловную обратную зависимость величин (чем больше одна величина, тем меньше другая, и наоборот), белый — прямую:

    import pandas as pd
    import seaborn as sns
    import matplotlib.pyplot as plt
    
    
    # df - pandas DataFrame с признаками
    sns.heatmap(df.corr());


    Насколько я знаю, в моделях, которые используют для прогнозов потребления электроэнергии в РФ, используют 2 фактора — история потребления и прогноз температуры. 


    В зимний и осенний периоды потребление энергии выше — сказывается низкая температура и сравнительно короткий световой день. Фактор «день недели» также оказывает влияние на количество расходуемой энергии — по выходным оно резко падает. Пики потребления приходятся на среду или четверг.





    Внутри одних суток по будням пиковые значения энергопотребления приходятся на 11-12 часов и постепенно падают с резким обрывом после девяти часов вечера. 


    Все как в учебниках:

    Суточные зимний и летний графики потребления промышленного и культурного центров. Источник

    Моделирование


    Prophet


    Первая идея, как сделать задачу максимально быстро — это взять Prophet от Facebook. У меня уже был опыт его использования, и я его запомнил как «штука, которая просто и быстро работает из коробки». Prophet для работы хочет видеть в pandas-датафрейме колонки «ds» и «y», т.е. дату в формате YYYY-MM-DD HH:MM:SS, и таргет — потребление в час, соответственно (примеры работы есть в документации).

    df = pd.read_pickle('full.pkl')
    pjme = df.loc[:'2018-06-01 23:00'].rename(columns={'hour_value': 'y'}) 
    pjme['ds'] = pjme.index
    split_date = '2018-03-01 23:00'
    pjme_train = pjme.loc[pjme.index <= split_date]
    pjme_test = pjme.loc[pjme.index > split_date]
    
    
    playoffs = pd.DataFrame({
      'holiday': 'playoff',
      'ds': df[(df.rest_day == 1)|(df.is_weekend == 1)].index,
      'lower_window': 0,
      'upper_window': 1,
    })
    
    
    model = Prophet(holidays=playoffs)
    model.fit(pjme_train.reset_index())
    forecast = model.predict(df=pjme_test.reset_index())



    Предсказания «пророка» выглядели объективными, но ошибка в 7–17% превышала допустимую, поэтому дальнейшие эксперименты я на этом фреймворке не проводил.

    SARIMAX


    Вторая попытка решения этой задачи была с помощью SARIMAX. Метод довольно затратный по времени подбора коэффициентов, но зато удалось снизить ошибку прогноза по сравнению с Prophet до 6-11%. 

    К сожалению, остался только результат для недельных предсказаний, но именно эти прогнозные данные я использовал в качестве дополнительных фич в бустинг-моделях.

    Для прогноза нужно подобрать параметры SARIMA(p,d,q)(P,D,Q,s):

    • p — порядок авторегрессии (AR), который позволяет добавить прошлые значения временного ряда (можно выразить как «завтра, возможно, пойдет снег, если в последние несколько дней шел снег»);
    • d — порядок интегрирования исходных данных (он определяет число предыдущих временных точек, которые нужно вычесть из текущего значения);
    • q — порядок скользящего среднего (MA), который позволяет установить погрешность модели в виде линейной комбинации наблюдавшихся ранее значений ошибок;
    • P — p, но сезонное;
    • D — d, но сезонное;
    • Q — q, но сезонное;
    • s — размерность сезонности (месяц, квартал и т.д.).

    На сезонном разложении по недельной истории можно увидеть тренд и сезонности, которые позволяет отобразить seasonal_decompose:

    import statsmodels.api as sm
    sm.tsa.seasonal_decompose(df_week).plot()


    По графикам автокорреляции и частичной автокорреляции выбрал начальные приближения параметров для модели SARIMAX: p=0, P=1, q=2, Q=2.

    sm.graphics.tsa.plot_acf(df_week.diff_week[52:].values.squeeze(), lags=60, ax=ax)
    sm.graphics.tsa.plot_pacf(df_week.diff_week[52:].values.squeeze(), lags=60, ax=ax)


    И итоговые подобранные значения (0, 2, 2)x(1, 2, 0, 52). В итоге график прогноза потребления выглядел так:


    Бустинг


    В этот момент стало очевидно, что без использования дополнительных внешних факторов не удастся достичь необходимой точности. В первую очередь, это погодные факторы: температура, давление, влажность, сила и направление ветра, осадки. 

    Первой попыткой была попытка купить архив с погодой в Яндекс. У ребят отличное API и техподдержка, но конкретно по нашему городу в данных было довольно много пропусков. В итоге я купил архив в сервисе openweathermap.org за 10$. Важно отметить, что, несмотря на то, что погода — очень полезный фактор, ошибки в прогнозе очевидно будут сильно портить итоговую MAPE модели. Я встречал информацию, что правильным решением этой проблемы было бы в обучении использовать не фактические исторические значения погоды, а прогнозы погоды за трое суток, но у меня, к сожалению, такой возможности не было. 

    Также я добавил признаки времени суток, дня недели, праздники, порядковый номер дня в году, а также большое количество временных лагов и средних значений за предыдущие периоды.

    Все эти данные после масштабирования (MinMaxScale) и One Hot Encoding (значения каждого категориального признака становятся отдельными столбцами с 0 и 1, таким образом, признак с тремя значениями становится тремя разными столбцами, и единица будет лишь в одном из них) я использовал в небольшом соревновании трех популярных моделей бустинга XGBoost, LightGBM и CatBoost. 

    XGBoost


    Об XGBoost’е написано много хорошего. Представленный на конференции SIGKDD в 2016 году, он произвел настоящий фурор и до сих пор держится на лидирующих позициях. Интересный материал для тех, кто про него не слышал.

    Подготовим данные для модели: 

    from sklearn.preprocessing import MinMaxScaler, StandardScaler
    from sklearn.model_selection import train_test_split
    
    
    def gen_data(
        stop_day = '2018-06-01 23:00', 
        drop_columns = [],
        test_size=0.15,
        is_dummies=True,
        is_target_scale=False):
    
        df = pd.read_pickle('full.pkl')
        df = df.loc[:stop_day]
        scaler = MinMaxScaler()
        target_scaler = StandardScaler()
        y = df.hour_total
        
        if is_target_scale:        
            y = target_scaler.fit_transform(y.values.reshape(-1, 1)).reshape(1,-1)[0]
            
        X = df.drop(['hour_value', *drop_columns], axis=1)
        num_columns = X.select_dtypes(include=['float64']).columns
        X[num_columns] = scaler.fit_transform(X[num_columns])
        
        if is_dummies:
            X = pd.get_dummies(X)
    
        train_count_hours = len(X) - 72
        valid_count_hours = len(X) - int(len(X) * 0.2)
        X_test  = X[train_count_hours:]
        y_test  = y[train_count_hours:]
        X = X[:train_count_hours]
        y = y[:train_count_hours]
    
        # Тут можно пробовать разные варианты разбиения на обучающую и тестовую выборки, от этого сильно зависит итоговая точность модели
        X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=test_size, random_state=42)
        train_dates = X_train.index
        val_dates = X_test.index
        return X, y, X_train, X_val, y_train, y_val, X_test, y_test, target_scaler


    Учу модель, используя лучшие подобранные ранее гиперпараметры:

    X, y, X_train, X_val, y_train, y_val, X_test, y_test, _ = gen_data(
        stop_day = stop_day, 
        drop_columns = drop_columns,
        test_size=0.1,
        is_dump=True,
        is_target_scale=False)
    
    
    params_last = {
        'base_score': 0.5, 
        'booster': 'gbtree', 
        'colsample_bylevel': 1, 
        'colsample_bynode': 1, 
        'colsample_bytree': 0.4, 
        'gamma': 0,
        'max_depth': 2, 
        'min_child_weight': 5, 
        'reg_alpha': 0, 
        'reg_lambda': 1, 
        'seed': 38,
        'subsample': 0.7, 
        'verbosity': 1,
        'learning_rate':0.01
    }
    
    
    reg = xgb.XGBRegressor(**params_last, n_estimators=2000)
    print(X_train.columns)
    reg.fit(X_train, y_train,
            eval_set=[(X_val, y_val)],
            early_stopping_rounds=20,
            verbose=False)
    
    
    y_pred = reg.predict(X_test)


    LightGBM


    LightGBM — фреймворк от Microsoft, основное преимущество которого состоит в скорости обучения на действительно больших массивах данных. А также в отличие от XGBoost’а LightGBM умеет работать с категориями, использует меньше памяти. Вот тут есть подробнее.

    import lightgbm as lgb
    from lightgbm import LGBMRegressor
    from sklearn.metrics import mean_squared_error
    
    
    stop_day = '2018-06-01 23:00'
    start_day_for_iterate = datetime.strptime(stop_day, '%Y-%m-%d %H:%M')
    test_size = 0.2
    
    X, y, X_train, X_val, y_train, y_val, X_test, y_test, _ = gen_data(
        stop_day = stop_day, 
        drop_columns = drop_columns,
        test_size=test_size,
        is_dump=True,
        drop_weekend=False,
        is_target_scale=False)
    
    
    model = LGBMRegressor(boosting_type='gbdt', 
                    num_leaves=83, 
                    max_depth=-1, 
                    learning_rate=0.008, 
                    n_estimators=15000, 
                    max_bin=255, 
                    subsample_for_bin=50000, 
                    min_split_gain=0, 
                    min_child_weight=3,
                    min_child_samples=10, 
                    subsample=0.3, 
                    subsample_freq=1, 
                    colsample_bytree=0.5, 
                    reg_alpha=0.1, 
                    reg_lambda=0, 
                    seed=38,
                    silent=False, 
                    nthread=-1)
    
    
    history = model.fit(X_train, y_train, 
                eval_metric='rmse',
                eval_set=[(X_val, y_val)],
                early_stopping_rounds=40,
                verbose = 0)
    
    
    y_pred = model.predict(X_test, num_iteration=model.best_iteration_)


    CatBoost


    CatBoost — продвинутая библиотека градиентного бустинга на деревьях решений с открытым исходным кодом" от Яндекса. Выгодное отличие модели состоит в удобстве работы с датасетами, содержащими категориальные признаки — передавать в модель содержащие категории данные можно без преобразования в числа, причем она выявляет закономерности самостоятельно, руками ничего подкручивать не требуется, и качество предсказания остается на высоте.

    cat_features=np.where(X.dtypes == 'category')[0]
    eval_dataset = Pool(X_test, y_test)
    
    model = CatBoostRegressor(learning_rate=0.5,
                              eval_metric='RMSE', 
                              leaf_estimation_iterations=3,
                              depth=3)
    
    model.fit(X_train, y_train,
              eval_set=(X_val, y_val),
              cat_features=cat_features,
              plot=True,
              verbose=True)
    
    y_pred = model.predict(X_test)


    XGBoost vs. LightGBM vs. CatBoost


    Чтобы не повторять многочисленные статьи, приведу сравнительную таблицу отсюда.


    Для вычисления ошибки по MAPE я брал последний известный месяц (28 дней) и, двигая окно с дискретностью в один час, делал прогноз на следующие 72 часа.

    А в моем импровизированном соревновании призовые места заняли:

    3-е место: мой любимчик — XGBoost — с самым низким качеством прогноза;
    2-е место: CatBoost как самый удобный и «все из коробки»;
    1-е место: LightGBM как самый быстрый и точный.

    Для более точного сравнения моделей я использовал R2 (R-квадрат или коэффициент детерминации, показывающий, как сильно условная дисперсия модели отличается от дисперсии реальных значений) и RMSLE (Root Mean Squared Logarithmic Error, или среднеквадратичная логарифмическая ошибка, которая является, по сути, расстоянием между двумя точками на плоскости — реальным значением и предсказанным). 

    Метрика LightGBM Catboost XGBoost Prophet SARIMAX
    r2 0.94137 0.93984 0.92909 0.81435 0.73778
    MSLE 0.02468 0.02477 0.01219 0.00829 0.00658

    Но это все, разумеется, применительно к моей задаче. На других данных в других руках все может быть совсем по-другому. 

    В следующий раз поделюсь другой нашей собственной интересной историей по прогнозированию оттока сотрудников для задачи планирования подбора в нашей компании.

    ГК ЛАНИТ
    Ведущая многопрофильная группа ИТ-компаний в РФ

    Comments 15

      +1
      Был довольно интересный пример проблемы при построении прогнозов в энергетике, когда один из всплесков изменений в объеме вырабатываемой электроэнергии был вызван тем, что корабль сел на мель на озере, и капитан договорился с владельцами гидроэлектростанций, чтобы они уменьшили сброс воды.
      Есть где почитать про этот «всплеск»? Уж больно байкой выглядит, чтобы ГЭС наплевали на заданный график от Системного оператора ради капитана.
        +1
        Как правило, ГЭС работают в базовой загрузке (они не участвуют в регулировании производства электроэнергии под график потребления). Регулирование производства на Саяно-Шушенской ГЭС (когда турбина дважды в день проходила через собственную частоту резонанса на валу) закончилось известно чем. Поэтому график от системного оператора тут не играет значительной роли.
        А здесь персонал мог сослаться на профилактические/ремонтные работы на одной из турбин и прикрыть сброс. Тем более, что благодарность капитана рядом, а выручка от продажи электроэнергии далеко и «у дяди»))). И уж если судно село мель, то могли сослаться и на отсутствие воды в водохранилище — это тоже важно для работы ГЭС и такие «блэк-ауты» не редкость, например, в Бразилии, где преобладает гидроэнергетика.
          0
          Как правило, ГЭС работают в базовой загрузке (они не участвуют в регулировании производства электроэнергии под график потребления)
          Ознакомьтесь, пожалуйста. Особенно с пунктом 7.1.12.
          ГЭС — основной регулятор.
          Про СШГЭС даже комментировать не хочу. Просто нет цензурных выражений.
            0
            Не могу согласиться. Если мощности ГЭС не «заперты» в локальной системе, то они системным оператором (как и АЭС) загружаются в первую очередь (т.е. покрывают базовую нагрузку). Все маневры по графику совершают как правило ТЭЦ (ТЭС). И многие, держа турбины в горячем резерве (например, кода «зеленые» проводят «час земли») жгут топливо зазря или работают в режиме «котельной». Есть такие мощности на ТЭС, которые вообще работают несколько часов в году, в периоды зимних максимумов.
              0
              Да как хотите. Кто я такой, чтобы влазить в Ваш спор с СО?
              Я привел стандарт СО. Вы считаете, что СО работает не по своему стандарту? ОК. Я тут ни_при_чем.
        0
        А если разделить модель на составляющие потребления? Например, построить типичные модели потребления населения, у которых в квартире есть свет, обогрев/кондиционирование, телевизор, чайник, электроплита итд. Тогда можно составить прогнозы по каждой составляющей смеси — например {наружная температура, отопительный сезон} => обогрев, {футбол, ДОМ2, программаВести} => телевизор, {утреннийподъем, приходсработы} => чайник итд, а потом уже суммировать эти составляющие, чтобы построить статпрофиль квартиры. То же самое сделать с предприятиями, ЖКХ итд — ведь у поставщика энергии должна быть информация о потреблении отдельно по этим каналам (так как отпускается энергия разным структурам по разным тарифам) => можно будет обучить модели поканально, и затем взвесить их простым нейроном, обучив его по минимуму ошибки на суммарных исторических данных.

        Чисто теоретически, используя больше исходной информации, вы можете улучшить качество прогноза, так как независимые модели не будут учитывать псевдокорреляции между, например, рабочими днями на предприятиях, долготой дня и временем включения уличного освещения и графиком жизни населения, или разбавлять статистические пики в одной группе за счет усреднения общей совокупности (например, телевизоры в моменты популярных утренних передач тонут на фоне увеличения потребления производствами в те же часы)
        • UFO just landed and posted this here
            0
            Это просто разный тип программирования. В обычном необходимо задавать логику обработки данных, в моделях машинного обучения только примеры. И получается так, что ML лучше справляется с генерацией логики и учетом внутренних взаимосвязей данных. Поэтому ИИ и применяют сейчас везде. )))
            0
            Все энергетики знают про наличие суточных, недельных и сезонных колебаний потребления и могут нарисовать эти графики с закрытыми глазами. И в крупных городах есть почти линейная зависимость потребляемой мощности от температуры наружного воздуха. Если потребление не отвечает этим сложившимся графикам, значит вмешался какой-то фактор, который трудно (читай невозможно) предсказать. Попытка обобщить эти непредсказуемые факторы может выдать результаты типа: «по средам января средняя температура по больнице как правило выше, чем по четвергам июля».
            В общем, сама задача видится странной, но энергетикам, давшим ТЗ, наверное, виднее.
              +1
              Вы правы, но есть нюансы ) Обычно в цели таких решений (основанных на машинном обучении) не входит победа по скорости и точности в соревновании с экспертизой опытных специалистов. ML хорошо ложится на случаи, где для принятия быстрого решения нужно анализировать большие объемы данных со сложными внутренними взаимосвязями.

              Очевидно, что если возникает новое событие нового типа, то его скорее всего не предскажет ни модель, ни человек. Но если не брать в расчет такие разовые, необоснованные и непредсказуемые события, то при достаточной глубине исторических данных в обучении и при допустимой точности иногда «экономически выгодно» заменить живого специалиста на ML модель.
              0
              Подскажите, а какой получился MAPE для указанных методов прогнозирования? Удалось попасть в требуемые 3%? Что в итоге выбрали для прода?
                +1
                В итоге в рамках пилотного проекта удалось добиться MAPE почасовой ошибки в 1.2%, модели передали заказчику, он занимался внедрением уже без нашего участия.
                +1
                На нашем предприятии мы создаём прогноз power flow на различных трансформаторах. В ходе экспериментов, мы отказались от моделей построенных на машинном обучении в пользу numerical models. Проблема заключалась в том, что модели на базе машинного обучения являются, по сути, чёрным ящиком и не дают возможности контролировать данные.
                  0
                  Интерпретируемость моделей часто становится очень важным фактором, особенно в медицине или финтехе. Причем, если какую-нибудь модель на Decision trees довольно легко визуализировать, то более сложные модели (тем более стек моделей), часто даже не пытаются.
                    0
                    Ох уж этот ИИ, бессмысленный и беспощадный

                  Only users with full accounts can post comments. Log in, please.