本文以Kaggle上一个能源需求预测的案例为基础,实战演示指数平滑(Holt-Winters)方法应用全流程。调用模型虽简单,但是入模前的数据分析、处理,模型参数的优化、效果的分析亦尤为重要,能够分析全面产出最优的方案又显得不那么简单。本文期望通过一个简单场景,回顾ML应用的基本流程。


导入模块

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.holtwinters import ExponentialSmoothing

读取数据

来自PJM超过10年的每小时能源消耗数据,单位:兆瓦。

df = pd.read_csv('data/PJME_hourly.csv')
df['Datetime'] = pd.to_datetime(df['Datetime'])
df.sort_values(by=['Datetime'], axis=0, ascending=True, inplace=True)
df.reset_index(inplace=True, drop=True)
print(df)



霍尔特双参数线性指数平滑法R语言 holt两参数指数平滑_霍尔特双参数线性指数平滑法R语言

可视化

df.set_index('Datetime').plot(style='r', figsize=(20,4), legend=False)
plt.show()



霍尔特双参数线性指数平滑法R语言 holt两参数指数平滑_机器学习_02

检查数据

从上图数据基本已经能看出数据时间范围、数据粒度、数据大体特征(周期型)、直观上是否有异常点(2012~2014貌似有一个)。

>> 那就先来看看数据是否有缺失值 -> 无。

print(df[pd.isnull(df['PJME_MW'])])

>> 再来看看是否有异常值,结合数据可视化时的序列曲线图感觉2012~2014可能有异常值。进一步探索发现异常偏低数据集中在12年的10月30日当天。

print(df[df['PJME_MW'] <= 15000])



霍尔特双参数线性指数平滑法R语言 holt两参数指数平滑_深度学习_03

>> 但同往年趋势特征对比整体水平偏低但并无明显异常,所以决定对此不再做异常处理。

ts1 = df[(df['Datetime'] >= '2012-10-29 01:00:00') & (df['Datetime'] <= '2012-10-31 23:00:00')]['PJME_MW']
ts2 = df[(df['Datetime'] >= '2011-10-29 01:00:00') & (df['Datetime'] <= '2011-10-31 23:00:00')]['PJME_MW']

plt.figure(figsize=(20,4))
ts1.reset_index(drop=True).plot(label='2012', legend=True)
ts2.reset_index(drop=True).plot(label='2011', legend=True)
plt.show()

>> 下面接着再来看看时间粒度(读取数据时能看出来应该是小时级粒度)、是否等频(有无日期缺失、有无日期重复)。发现数据日期有重复且有日期缺失情况。(下图中黄色字体均值为1表明1小时1条)。

df['SPAN'] = df['Datetime'].diff().astype('timedelta64[h]').astype(float)
print(df.head())
print(df['SPAN'].median(), df['SPAN'].min(), df['SPAN'].max())



霍尔特双参数线性指数平滑法R语言 holt两参数指数平滑_机器学习_04

>> 既然数据日期有重复、有遗漏,不免好奇哪些重复、哪些遗漏,或者说重复&遗漏下数据的表现是怎样的。

# 日期重复数据
print(pd.merge(df, df[df['SPAN'] == 0]['Datetime'], on='Datetime'))

# 漏报前后数据
index = df[df['SPAN'] == 2].index[0]
print(df.loc[index-3: index+3])

# 漏报次数
print(df[df['SPAN'] == 2].shape[0]



霍尔特双参数线性指数平滑法R语言 holt两参数指数平滑_霍尔特双参数线性指数平滑法R语言_05

清洗数据

根据以上检查,发现数据相对还是比较干净的,但有2点问题:

1.日期有重复 -> 以上检查中可以看到数据并不是完全重复,猜测可能偶有一分钟内多次上传数据,此时为了数据粒度统一,舍弃较早的一条。

2.数据有漏报 -> 因传感器/网络等设施问题难免有数据漏报的情况,当前数据漏报间隔较短且漏报次数较少,可以使用插值的方法补充漏报数据。

# 去重
df.drop_duplicates(subset='Datetime', keep='last', inplace=True)

# 插值
df.set_index('Datetime', inplace=True)
date_range = pd.date_range(start=df.index.min(), end=df.index.max(), freq='H')
df = df.reindex(date_range)
df['PJME_MW'].interpolate(method='linear', inplace=True)

print(df)

# 删除临时列
df.drop('SPAN', axis=1, inplace=True)



霍尔特双参数线性指数平滑法R语言 holt两参数指数平滑_深度学习_06

拆分训练集/验证集

需求为预测未来1年的数据,首先一个问题是不管用什么模型都至少要有一份数据能够验证模型效果。所以首先要拆分训练集和验证集,如果要更精确的验证模型效果还需拆分测试集。时间序列中一般根据时间拆分,较早的一段时间数据用于训练模型,最近的一段时间用于验证/测试模型效果。

比如我们要用过去5年的数据训练模型,可以留出最近1年的数据作为验证集,再往前推5年的数据做训练集。

train_years = 5
val_years = 1

last_date = df.index[-1]
val_start_date = last_date - pd.DateOffset(years=val_years)
train_start_date = val_start_date - pd.DateOffset(years=train_years)

print(train_start_date, val_start_date, last_date)



霍尔特双参数线性指数平滑法R语言 holt两参数指数平滑_数据分析_07

df_train = df.loc[(df.index >= train_start_date) & (df.index < val_start_date)]
df_val = df.loc[df.index >= val_start_date]



霍尔特双参数线性指数平滑法R语言 holt两参数指数平滑_深度学习_08

训练模型

模型选择

从数据的可视化图中能直观的看出来,当前数据为无趋势的周期型时间序列数据,周期大小为1年。本次限定使用指数平滑方法,有周期故使用三次指数平滑(Holt-Winters)方法,具体使用季节性加法模型还是乘法模型可进行实验根据评估指标选择。

模型训练

>> 评估指标:MAPE

def mape(y_true, y_pred):
    y_true, y_pred = np.array(y_true), np.array(y_pred)
    pe = (y_true - y_pred) / y_true
    ape = np.abs(pe)
    mape = np.mean(ape)

    return f'{mape*100:.2f}%'

>> 训练模型
周期大小为24*365(一年,每天24个数据点),无趋势不指定trend,有季节性暂指定为加法模型。

模型训练时考虑数据量较大,设置模型参数初始值不进行暴力搜索,优化算法使用‘L-BFGS-B’,目的为提高计算速度,不然实在太慢。

periods = 24*365

fit = ExponentialSmoothing(df_train['PJME_MW'],
                           seasonal_periods=periods,
                           seasonal='add').fit(method='L-BFGS-B', use_brute=False)
print(fit)
fcast = fit.forecast(len(df_val))

mape_hw = mape(y_true=df_val['PJME_MW'], y_pred=fcast)
print(f'Our Holt-Winter model has a mean average percentage error of {mape_hw}')

霍尔特双参数线性指数平滑法R语言 holt两参数指数平滑_深度学习_09

>> 参数调优

HW中的参数使用‘L-BFGS-B’优化算法自动优化,对比季节性加法模型,乘法模型效果较好MAPE为11.75%。

fit = ExponentialSmoothing(df_train['PJME_MW'],
                           seasonal_periods=periods,
                           seasonal='mul').fit(method='L-BFGS-B', use_brute=False)
                           
fcast = fit.forecast(len(df_val))

mape_hw = mape(y_true=df_val['PJME_MW'], y_pred=fcast)

print(f'Our Holt-Winter model has a mean average percentage error of {mape_hw1}')



霍尔特双参数线性指数平滑法R语言 holt两参数指数平滑_机器学习_10

模型分析

通过进一步分析,认为使用7年数据训练模型效果较好,此时在近1年上的验证集上MAPE为9.7%。
分析内容有以下两点:
(1). 分析训练数据的大小对模型效果的影响
(2). 分析模型在不同年份上的表现看模型是否稳定

def test_train_size(df):
    val_years = 1
    periods = 24*365

    for train_years in range(1,10):
        # 拆分日期
        last_date = df.index[-1]
        val_start_date = last_date - pd.DateOffset(years=val_years)
        train_start_date = val_start_date - pd.DateOffset(years=train_years)

        # 训练集/验证集
        df_train = df.loc[(df.index >= train_start_date) & (df.index < val_start_date)]
        df_val = df.loc[df.index >= val_start_date]

        # 模型训练
        fit = ExponentialSmoothing(df_train['PJME_MW'],
                                   seasonal_periods=periods,
                                   seasonal='mul').fit(method='L-BFGS-B', use_brute=False)
        # 模型预测
        fcast = fit.forecast(len(df_val))

        # 模型评估
        mape_hw = mape(y_true=df_val['PJME_MW'], y_pred=fcast)
        
        # 打印结果
        if train_years == 1:
            val_date_desc = 'val_date_range  : {}-{}'.format(val_start_date.strftime('%Y%m%d'), last_date.strftime('%Y%m%d'))
            print(val_date_desc)
        train_date_desc = 'train_date_range: {}-{}'.format(train_start_date.strftime('%Y%m%d'), val_start_date.strftime('%Y%m%d'))
        print(train_date_desc, 'train_years:', train_years, 'mape:', mape_hw)

# 分析不同年数训练数据在18年(20170803-20180803)验证集上的表现
test_train_size(df)

# 分析不同年数训练数据在17年(20160803-20170803)验证集上的表现
last_date = df.index[-1] - pd.DateOffset(years=1)  
test_train_size(df.loc[df.index<=last_date])



霍尔特双参数线性指数平滑法R语言 holt两参数指数平滑_霍尔特双参数线性指数平滑法R语言_11

模型预测

确定训练数据时间跨度及模型各项参数后,使用最新数据重新训练模型,并预测未来一段时间的能源需求。

# 起始日期
last_date = df.index[-1]
train_start_date = last_date - pd.DateOffset(years=7)

# 训练集
df_train = df.loc[df.index >= train_start_date]

# 模型训练
fit = ExponentialSmoothing(df_train['PJME_MW'],
                           seasonal_periods=periods,
                           seasonal='mul').fit(method='L-BFGS-B', use_brute=False)

# 模型预测
fcast = fit.forecast(24*365)
date_range = pd.date_range(start=df_train.index.min(), end=fcast.index.max(), freq='H')
df_train = df_train.reindex(date_range)
df_train['predict'] = fcast

print(fcast.index.min(), fcast.index.max())

# 可视化
plt.figure(figsize=(20,4))
df_train['PJME_MW'].plot()
df_train['predict'].plot()
plt.xlabel('Date & Time')
plt.ylabel('Energy Demand [MW]')
plt.title('Holt-Winter Forecast of Hourly Energy Demand')
plt.show()



霍尔特双参数线性指数平滑法R语言 holt两参数指数平滑_霍尔特双参数线性指数平滑法R语言_12