上一篇咱们讲解了如何在2小时快速完成时间序列的预测。采用的算法是facebook的prophet。文末,咱们留了一个问题:如何知道咱们快速建立的prophet的模型相对是好仍是坏?html
我一直信奉的原则是:机器学习,尤为是优化问题,必定要先找到一个baseline。以后咱们就能够找到咱们改善的力度有多大?或者是我咱们的差距有多大。这样你就能说服客户为你的解决方案付款。redis
因为本文中涉及一些信号处理相关的知识,因此会混用信号和时间序列两个词。算法
上文中咱们采用的数据集是AEP_hourly.csv。很遗憾,我在有限的时间内没有找到网上有人作过该数据集预测。不少人都预测的是PJME_hourly.csv。bash
为了对比标杆,咱们也用这个数据集快速作预测。 代码仍是和上一篇 代码一致,只是更改数据集的名称以及columns的名称,属于很基本的操做。这里就只贴出代码和结果,不作详细的解释。机器学习
from statsmodels.tsa.seasonal import seasonal_decompose
import numpy as np
from scipy.fft import fft
import pandas as pd
import matplotlib.pyplot as plt
from fbprophet import Prophet
from sklearn.metrics import mean_absolute_error
# we only analyze the dataset pjm_hourly_est.csv
file = 'data/PJME_hourly.csv'
raw_df = pd.read_csv(file)
ax = raw_df.plot(kind='line')
plt.show()
# predict new dataset with prophet
dt_format = '%Y-%m-%d %H:%M:%S'
raw_df['Datetime']= pd.to_datetime(raw_df['Datetime'],format=dt_format)
raw_df.rename({'PJME_MW':'y','Datetime':'ds'},axis=1,inplace=True)
split_dt = pd.Timestamp('2015-01-01 00:00:00')
train_df = raw_df[raw_df['ds']< split_dt]
test_df = raw_df[raw_df['ds']>= split_dt]
model = Prophet()
model= model.fit(train_df)
y_test = model.predict(test_df)
fig = model.plot(y_test)
print(mean_absolute_error(test_df['y'].values,y_test['yhat']))
model.plot_components(y_test)
复制代码
MAE 偏差为5183。看到kaggle上置顶的prophet预测,ta的结果是5181.782050398612。ide
也就是说,咱们两个小时速成的预测模型,和“大神”预测的差很少。或许prophet的模型基本成绩就是这个水平。那么这个水平在模型界属于什么地位呢?函数
Convergence detected: relative gradient magnitude is below tolerance
5183.653998193845
复制代码
咱们能够试试传统的季节性信号分解的模型预测该时间集。prophet的核心思想也是信号分解,将一个时间序列中抽取不一样周期的季节性信号。工具
时间序列的圣经中提到,时间序列通常会被分解为trend + seasonal + remiander (趋势+季节性+残余信号)。post
statsmodels模块中的seasonal_decompose 已经有成熟的分解方法:移动平均值法。学习
下面咱们对该函数进行说明:
statsmodels.tsa.seasonal.seasonal_decompose(x, model='additive', filt=None, period=None, two_sided=True, extrapolate_trend=0)[source]¶
Seasonal decomposition using moving averages.
复制代码
主要参数:
输出的结果为:
基本了解了函数的用法,咱们就能够进行实战了。
首先咱们准备好数据,该数据和prophet采用的数据集一致。
咱们先分解出半天的信号。由于白天和晚上的用电量很明显不一样,这是常识。因此有了这个先验知识,咱们尝试去分解,period 设为12。
half_day_result = seasonal_decompose(raw_df['PJME_MW'], model='additive', period=12)
half_day_result.plot()
复制代码
half_day_result.seasonal.iloc[0:24].plot()
复制代码
回到咱们信号分解的基本思路:若是咱们能预测分解后的每一个信号,那么合并起来的信号,咱们就能预测。 seasonal信号,是正(余)弦信号,只须要知道幅值,频率,相位和偏移量便可。
关于傅里叶变换这个深奥的信号分析工具,网上有太多的讲解。这里咱们只须要知道的是:傅里叶变换能够将时域信号变成频域信号。白话文就是:傅里叶变换能够帮咱们快速求解幅值,频率。 我写了一个函数,思路是:
def find_period(x, plot=True):
yf = fft(x)
xf = np.linspace(0, len(x) // 2, len(x) // 2)
yf = 2.0 / len(x) * np.abs(yf[0:len(x) // 2])
all_df = pd.DataFrame([])
all_df['time'] = xf
all_df['freq'] = yf
period = len(x) / all_df.loc[all_df['freq'].idxmax(), 'time']
all_df.sort_values(by='freq',inplace=True,ascending=False)
all_df.reset_index(inplace=True)
all_df['period']= len(x)/all_df['time']
all_df.replace([np.inf, -np.inf], 0,inplace=True)
period = len(x) / all_df.loc[all_df['freq'].idxmax(), 'time']
if plot:
plt.plot(all_df['period'], yf)
return period,all_df['freq'].max(),np.mean(x)
复制代码
咱们来调用函数对seaonal信号进行变换。
half_day_season_period = find_period(half_day_result_seasonal)
print('*' * 30)
print(f'half_day_season_period: {half_day_season_period}')
复制代码
周期为11.99 小时,也就是12小时,幅值为554,偏移量为0。咱们能够用数学表达式表示这个信号了,可是还缺了相位信息。
******************************
half_day_season_period: (11.999669803533104, 554.1702081757178, 5.666447331079827e-05)
复制代码
seaonal信号咱们(几乎)能够表示了,其余的信号怎么办? 看不懂??继续分解!! 直到看懂(信号简单明了,或者单调,或者周期)。
后面咱们会继续分解天天的信号,每周的信号,每个月,每半年,每一年,而后对seasonal 信号进行fft变换,求出幅值和频率。代码几乎雷同,因此不一一讲解。
# split day compose
day_result = seasonal_decompose(half_day_result.trend.dropna(), model='additive', period=24)
day_result.plot()
# find period of day cyclic
day_result_seasonal = day_result.seasonal.values
day_season_period = find_period(day_result_seasonal)
print('*' * 30)
print(f'day_season_period: {day_season_period}')
# find the period of redis
day_redis_period = find_period(day_result.resid.dropna().values)
print(f'day_redis_period: {day_redis_period}')
# find period of week cyclic
week_result = seasonal_decompose(
day_result.trend.dropna(),
model='additive',
period=7 * 24)
week_result.plot()
week_season_period = find_period(week_result.seasonal.values)
print('*' * 30)
print('week split result')
print(f'week_season_period: {week_season_period}')
week_redis_period = find_period(week_result.resid.dropna().values)
print(f'week_redis_period: {week_redis_period}')
month_result = seasonal_decompose(
day_result.trend.dropna(),
model='additive',
period=30 * 24)
month_result.plot()
month_season_period = find_period(month_result.seasonal.values)
print('*' * 30)
print('month split result')
print(f'month_season_period: {month_season_period}')
month_redis_period = find_period(month_result.resid.dropna().values)
print(f'month_redis_period: {month_redis_period}')
half_year_result = seasonal_decompose(
month_result.trend.dropna(),
model='additive',
period=365 * 12)
half_year_result.plot()
half_year_season_period = find_period(half_year_result.seasonal.values)
print('*' * 30)
print('half year split result')
print(f'half_year_season_period: {half_year_season_period}')
half_year_redis_period = find_period(half_year_result.resid.dropna().values)
print(f'half_year_redis_period: {half_year_redis_period}')
year_result = seasonal_decompose(
half_year_result.trend.dropna(),
model='additive',
period=365 * 24)
year_result.plot()
year_season_period = find_period(year_result.seasonal.values)
print('*' * 30)
print('yearly split result')
print(f'year_season_period: {year_season_period}')
复制代码
咱们能够看一下信号分解的结果。
每日份量分解后的信号图:
每个月份量分解的信号图:
fft结果以下:
******************************
day_season_period: (24.00132100396301, 655.682736730418, 0.022879188988473007)
day_redis_period: (23.993396070662044, 2129.021843848859, 0.006176697324649406)
******************************
week split result
week_season_period: (168.0092485549133, 169.52905012773715, 0.010345771248929707)
week_redis_period: (169.1841491841492, 1035.9007684637975, 0.34519449979470296)
******************************
month split result
month_season_period: (719.4455445544555, 148.40567700918697, -0.09128365805888033)
month_redis_period: (169.13216374269007, 1144.5380532729368, -1.1388580520215974)
******************************
half year split result
half_year_season_period: (4382.060606060606, 4156.997946762282, 0.6291791975756035)
half_year_redis_period: (2921.416666666667, 848.789097533505, 22.611620632847078)
******************************
yearly split result
year_season_period: (8764.25, 837.7800370550049, 0.3809718680342369)
复制代码
提到数学模型,我会头大。不过这里的数学模型很简单:原始时间信号=咱们分解出来的全部seasonal信号+ trend份量。 trend份量咱们就用简单的线性拟合,seasonal信号咱们假定都是正弦信号,可是相位信息也很重要,这里咱们采用优化求解的算法,获得每一个正弦信号的相位。 具体见代码
from sklearn.linear_model import LinearRegression
print(np.linspace(0,year_result.trend.shape[0], year_result.trend.shape[0]).shape)
y = year_result.trend.dropna()
lnr = LinearRegression().fit(np.linspace(0,y.shape[0], y.shape[0]).reshape(-1,1),y)
print(f'{lnr.coef_},{lnr.intercept_}')
from scipy.optimize import basinhopping
from sklearn.metrics import mean_squared_error
def energy_consumption_math_model(coefs, X, y): # kwargs without ** to make it as positional para
y_hat = -0.01029388 * X + 32815.50 \
+ 2*1280.4696176751042 * np.sin(X / 24 * np.pi * 2 + coefs[0]) \
+ 2 * 2129.021843848859 * np.sin(X / 24 * np.pi * 2 + coefs[1]) \
+ 2 * 1035.9007684637975 * np.sin(X / 24 / 7 * np.pi * 2 + coefs[2]) \
+ 2 * 1144.5380532729368 * np.sin(X / 24 / 7 * np.pi * 2 + coefs[3]) \
+ 2*4156.997946762282 * np.sin(X / 24 / 183 * np.pi * 2 + coefs[4]) + 22\
+2*848.789097533505 * np.sin(X / 2921 * np.pi * 2 + coefs[5]) + \
+2*837.7800370550049 * np.sin(X / 24 / 365 * np.pi * 2 + coefs[6]) + coefs[7]
error = mean_squared_error(y_hat,y)
return error
coef = np.zeros(8)
minimizer_kwargs = {"method": "BFGS"}
minimizer_kwargs['args'] = (raw_df.index.to_numpy(), raw_df['PJME_MW'])
opt_result = basinhopping(energy_consumption_math_model, coef, minimizer_kwargs=minimizer_kwargs)
print(opt_result)
复制代码
优化的结果以下:
coefs = np.array([ 0.92350449, -2.21809036, 4.51689507, 7.65849104,
2.54063322, -0.67270678, -0.92850058, -48.15463512])
复制代码
咱们将求解出来的coef 从新带入y_hat 公式:
y_hat = -0.01029388 * X + 32815.50 \
+ 2 * 1280.4696176751042 * np.sin(X / 24 * np.pi * 2 + coefs[0]) \
+ 2 * 2129.021843848859 * np.sin(X / 24 * np.pi * 2 + coefs[1]) \
+ 2 * 1035.9007684637975 * np.sin(X / 24 / 7 * np.pi * 2 + coefs[2]) \
+ 2 * 1144.5380532729368 * np.sin(X / 24 / 7 * np.pi * 2 + coefs[3]) \
+ 2 * 4156.997946762282 * np.sin(X / 24 / 183 * np.pi * 2 + coefs[4]) + 22 \
+ 2 * 848.789097533505 * np.sin(X / 2921 * np.pi * 2 + coefs[5]) + \
+2 * 837.7800370550049 * np.sin(X / 24 / 365 * np.pi * 2 + coefs[6]) + coefs[7]
复制代码
以上就是咱们创建的数学模型(包含1个线性趋势+7个正弦信号+常量),只要输入x的值,就会计算(预测)出对应的y值(用电量)。
由于咱们有了数学模型,能够很方便对全部的数据进行预测。咱们对比整个数据集上的预测结果与原始数据,看起来拟合效果还不错,周期捕捉的很准确,幅值也相差不大。
4743.114944224841
复制代码
本文咱们采用传统的季节性信号分解方法来分解信号,对分解后的季节性信号用正弦函数拟合,对于最后的趋势信号,采用线性拟合。
一样的测试集,传统季节性信号分解方法居然赛过了prophet。
prophet真的这么弱吗? 可否挽回这个模型的颜面?后面咱们能够作成尝试。