能源领域的机器学习,还是不仅每个人都能明天观看

在许多领域,对未来事件的准确预测是一个充满希望和有趣的任务:从天气预报到金融科技(股票报价,汇率)。今天的机器学习可以显着减少制定管理决策所需的时间和人力。  大约半年的时间里,

我们位于NORBIT的数据科学团队尝试使用各种机器学习模型来解决分类和回归问题,并优化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());


据我所知,在用于预测俄罗斯联邦电力消耗的模型中,使用了两个因素-消耗的历史和温度预测。 


在冬季和秋季,能耗较高-低温且白天相对较短。“星期几”因素也会影响所消耗的能量-在周末,能量急剧下降。消费高峰发生在周三或周四。





在工作日的一天之内,能量消耗的峰值出现在11-12小时,并在晚上9点后逐渐下降并急剧下降。 


一切都和教科书中的一样:

工业和文化中心的每日冬季和夏季每日消费时间表。资源

造型


预言家


如何尽快完成任务的第一个想法是从Facebook上带走Prophet。我已经有使用它的经验,并且我记得它是“一种简单,快速,可立即使用的东西”。先知想在熊猫数据框中看到“ 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%,因此,我没有在此框架上进行进一步的实验。

Sarimaax


解决此问题的第二种尝试是使用SARIMAX。该方法在选择系数时非常耗时,但与“先知”相比,可以将预测误差降低到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)。结果,消费预测图如下所示:


助推


在这一点上,很明显,如果不使用其他外部因素,就无法达到必要的精度。首先,这些是天气因素:温度,压力,湿度,风强度和方向,降水。 

第一次尝试是尝试购买Yandex的天气档案。这些家伙拥有出色的API和技术支持,但特别是在我们所在的城市,数据之间存在很多差距。结果,我花了10美元在openweathermap.org服务中购买了档案。值得注意的是,尽管天气是一个非常有用的因素,但预测误差显然会破坏最终的MAPE模型。我得到的信息是,解决此问题的正确方法是训练而不是使用天气的实际历史值,而是使用三天的天气预报,但不幸的是,我没有这样的机会。 

我还添加了一天中的时间,一周中的某天,节假日,一年中一天中的序列号以及以前时期的大量时滞和平均值的迹象。

缩放后的所有数据(MinMaxScale)和一个Hot Encoding(每个分类项目的值变成具有0和1的单独列,因此具有三个值的项目成为三个不同的列,并且单位将仅位于其中之一)三种流行的增强模型XGBoost,LightGBM和CatBoost的竞争。 

XGBoost


关于XGBoost的文章很多。在2016年SIGKDD会议上提出的他引起了轰动,仍然保持领先地位。对于那些没有听说过的人来说很有趣的材料

让我们为模型准备数据: 

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是来自Yandex的开放源决策树的高级梯度提升库。该模型的优势在于,可以方便地处理包含分类特征的数据集-您可以将包含类别的数据传输到模型而无需转换为数字,并且可以手工显示模式无需扭曲,预测的质量仍然很高。

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与 LightGBM与 Catboost


为了不重复大量文章,我将从此处给出一个比较表


为了计算MAPE的错误,我花费了最近的一个月(28天),然后以一个小时的分辨率移动窗口,对接下来的72小时进行了预测。

在我的即兴比赛中,奖项是:

第三名:我最喜欢的-XGBoost-预测质量最低;
第二名:CatBoost是最方便的“开箱即用”产品;
第一名:LightGBM最快,最准确。

为了更精确地比较模型,我使用了R2(R平方或确定系数,显示了模型的条件方差与实际值的方差相差多少)和RMSLE(均方根对数误差,或均方根对数误差,实际上是距离)在平面上的两个点之间-实际值和预测值)。 

指标LightgbmCatboostXGBoost预言家Sarimaax
220.941370.939840.929090.814350.73778
梅尔0.024680.024770.012190.008290.00658

当然,所有这些都与我的任务有关。根据其他方面的数据,一切都可能完全不同。 

下次,我将分享我们的另一个故事,即预测员工的离职,以完成公司的招聘计划。

顺便说一句,我们的团队正在成长,我们正在寻找人才!

Source: https://habr.com/ru/post/undefined/


All Articles