Energy Consumption (能源消耗)数据集预测(3): lightgbm再立标杆

2,607 阅读6分钟

前言

前两篇文章,我们分析分别用了prophetseasonal_decompose 对信号进行了分解。机器学习中一直流程的一段话:模型和特征工程决定了结果的极限,调参只是逼近这个极限。

所以本人一般不喜欢简单粗暴的调参,而是喜欢在模型上多试几次,或者在特征上变一下。

今天我们来用lightgbm 进行该数据集的预测。

为什么是ligbtgbm呢? 我的理由有以下几点:

  • lightgbm 属于tree 类型的模型,并且采用boosting技术
  • lightbgbm相比于同行xgboost,更快,适合我的CPU only 的笔记本 (重点)

lightgbm 简介

既然实战,我们就假设你听过lightgbm。如果是纯新手,可以看看官方文档。 听说有中文版的翻译,不过里面有少许的坑,比如因为更新不及时,导致缺少一些metrics的翻译等。 我们之前有用ligtgbm 硬train 不平衡数据集。有兴趣的可以在阅读完本文去翻阅。两个文章属于不同的用法,一个是分类,一个是回归。

数据准备

首先导入一些库。这几个库都是必须的, lightgbm 可以创造模型,pandas处理数据,pyplot 画图,metrics 用来评判我们的模型。

import lightgbm as lgb
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.metrics import mean_absolute_error

首先读入数据,并且将时间字符串转换为Timestamp 格式。

file = 'data/PJME_hourly.csv'
raw_df = pd.read_csv(file)
dt_format = '%Y-%m-%d %H:%M:%S'
raw_df['Datetime']= pd.to_datetime(raw_df['Datetime'],format=dt_format)
print(raw_df.head())

预览数据:

             Datetime  PJME_MW
0 2002-12-31 01:00:00  26498.0
1 2002-12-31 02:00:00  25147.0
2 2002-12-31 03:00:00  24574.0
3 2002-12-31 04:00:00  24393.0
4 2002-12-31 05:00:00  24860.0

特征工程

我们可以看到特征其实只有一个Datetime,PJME_MW 是我们要预测的目标(target)。很明显这样的特征个数无法达到我们的理想要求。 所以我们要提取特征。 对于时间序列的特征,我们一般的处理方法就是从中提取其他周期的信息,比如只要小时,只要日期,只要星期。 为什么呢?

我们回想之前prophet 和seasonal_decompose的算法,这两个算法都是想在原始信号中分解出某个特定周期的信号,但是对于树类型的模型,它是基于分割点的,无法提取到周期信息。

所以我们需要显式的告诉数模型,可能有哪些周期,然后让树对这个周期特征进行分割。

这里重点需要介绍pandas 库中给的series.dt 类,这个类其实基本类是CombinedDatetimelikeProperties, 它综合了三大类属性:DatetimeProperties, TimedeltaProperties, PeriodProperties。 今天我们主要用到的PeriodProperties(周期属性)。 因为这个数据集只有一个特征,所以这里我们尽可能多的创造更多的特征,好让lightgbm发挥他的特长。数据集最小的采集频率是每小时,所以我们可以依次创建:

  • 小时级别的(这是每天的第几个小时)
  • 天级别的(这是每周的第几天,每月的第几天,每年的第几天)
  • 周级别(星期几,每年的第几周)
  • 月级别(哪一月,是否是月初,是否是月末)
  • 季度级别
  • 年度级别

具体可以参考代码,我们们直接写了一个小函数。输入dataframe,返回带有更多特征的dataframe。

# create time series period features
def time_period_features(df,time_col):
    # in hour level
    df['hour'] = df[time_col].dt.hour
    # in day level
    df['dayofweek'] = df[time_col].dt.dayofweek
    df['days_in_month'] = df[time_col].dt.days_in_month
    df['dayofyear'] = df[time_col].dt.dayofyear
    # week levels
    df['weekday'] = df[time_col].dt.weekday
    df['week'] = df[time_col].dt.week
    df['weekofyear'] = df[time_col].dt.weekofyear
    # month level
    df['month'] = df[time_col].dt.month
    df['is_month_start'] = df[time_col].dt.is_month_start
    df['is_month_end'] = df[time_col].dt.is_month_end
    # quarter level
    df['quarter'] = df[time_col].dt.quarter
    # year level
    df['year'] = df[time_col].dt.year
    return df

然后我们调用该函数进行特征工程。

raw_df = time_period_features(raw_df,'Datetime')
raw_df = raw_df.sort_values(by=['Datetime'])

训练数据集和测试集划分

这个和之前文章的思路一致,直接上代码。

split_dt = pd.Timestamp('2015-01-01 00:00:00')
train_df = raw_df[raw_df['Datetime']< split_dt]
test_df = raw_df[raw_df['Datetime']>= split_dt]

train_Y = train_df['PJME_MW'].copy()
test_Y = test_df['PJME_MW'].copy()
train_df.drop(['Datetime','PJME_MW'],axis=1,inplace=True)
test_df.drop(['Datetime','PJME_MW'],axis=1,inplace=True)

lightgbm 依然“硬train一发”

我们几乎采用模型所有默认的参数,来“硬train一发”。 为什么?

因为我会用这个作为该模型的baseline。之后我们可以有方向的调整参数达到更好的效果。

这里仅仅要注意的是objective 参数,值为regression,因为我们要做的是一个回归模型。 另外,metric 我会采用l1, 因为l1 其实就是mean_absolute_error(MAE)。 采用这个metric,我们可以和其他模型的结果进行对比。

代码就是经典的lightgbm代码示例,这里不做更多解释,详细官方文档会写的更清楚。

# initial trial
dtrain = lgb.Dataset(train_df.values, label=train_Y.values)
dtest = lgb.Dataset(test_df.values, label=test_Y.values)
param = {
    'max_depth': 6,
    'eta': 0.05,
    'objective': 'regression',
    'verbose': 0,
    'metric': ['l1'],
}
evals_result = {}
valid_sets = [dtrain, dtest]
valid_name = ['train', 'eval']
feature_name = list(train_df.columns)
model = lgb.train(param, dtrain, num_boost_round=500, feature_name=feature_name,
                  valid_sets=valid_sets, valid_names=valid_name, evals_result=evals_result)
print(f'fitting done')
y_hat = model.predict(test_df.values)

print(mean_absolute_error(test_Y.values,y_hat))
fig,ax = plt.subplots(2,1)
ax[0].plot(y_hat)
ax[1].plot(test_Y.values)
plt.show()
metric = 'l1'
fig, ax = plt.subplots()
ax.plot(evals_result['train'][metric], label='Train')
ax.plot(evals_result['eval'][metric], label='Test')
ax.legend()
plt.ylabel(f'{metric}')
plt.title(f'XGBoost {metric}')
plt.show()

结果展示:

fitting done
3032.011693891576

这几乎是让我震惊的结果,因为它比蓝老大(facebook)的模型结果5183.653998193845 提高了好多。

我们在plot以下训练和测试的error历史曲线。

lightgbm再创新高

我们用了默认的参数,结果已经很出乎我的意料了。主要是因为baseline弱爆了!! lightgbm我们还可以再提高一下: 很明显从趋势来看,我们出现了过拟合训练误差还有下降的趋势,但是测试误差已经反弹了。 降低过拟合,最快的方法就是调整max_depth。 另外一个方法,就是花点时间看看lightgbm官方文档的tuning 指南。 我们不是用来参赛或者提高给最终客户的成品,这里我们简单调参。

param = {
    'boosting':'dart',
    'max_depth': 4,
    'num_leaves':32,
    'eta': 0.05,
    'objective': 'regression',
    'verbose': 0,
    'metric': ['l1'],
}

结果果然好于默认值,2666.2777024354377这个值是什么水平呢?我们同样对比kaggle上的“大神”的结果。kaggle上kernals 第一名分享的结果是采用xgboost ( xgboost 一般会比lightgbm结果要更好),它的预测结果是2848.891429322955

看来我们的结果还不错。而且还666。。。 作为一篇学习类的文章,我们不就用继续调参了。

2666.2777024354377

我们贴上主要特征图,也可以看到每天影响很大,再次是每小时的耗电量。

总结

本文中我们采用lightgbm预测能源消耗数据集,取得了相对不错的效果。相比于prophet,lightgbm的结果更好一些。这主要是因为我们显式的提取了一些特征。

  • 本文可以学会如何使用lightgbm对一元时间序列进行预测
  • 可以学习到如何对一元时间序列提取周期特征
  • 简单的调整参数来优化lightgbm的效果。

后记

蓝老大说: 我可以再试一试!(请见下一篇,救活prohet)。