文章
问答
冒泡
时序数据预测算法研究:Prophet

1 前言

指标(Metric)、日志(Log)、调用链(Trace)是运维领域产生的三种最基础的数据类型,而指标数据中时序数据又占据了主体地位,它是构建运维观测能力的基础。在AIOps中,时序数据主要有以下两个应用:

  • 异常检测
  • 指标预测

下面介绍一种时序数据预测算法,叫做Prophet。Prophet是一种用于时间序列预测的算法,由Facebook开发并开源。它基于加法模型,可以自动检测数据中的趋势和季节性,并将它们组合在一起以获得预测值。

2 算法介绍

2.1 算法原理

Prophet算法使用了一个可分解的时间序列模型,该模型主要有三个模型组成部分:trend,seasonality,以及holidays,因此重点在于如何计算这三部分。其公式如下: y(t)=g(t)+s(t)+h(t)+ϵ(t) 。

其中:

  • g(t) 是模拟时间序列值的非周期性变化的趋势函数
  • s(t) 表示周期性变化(例如,每周和每年的季节性)
  • h(t) 表示假期的影响在一天或几天内以潜在的不规则时间表发生
  • ϵ(t) 误差项代表模型不适应的特殊变化,假设其服从正态分布

2.2 算法优势

Prophet算法在支持时间序列训练,预测的基础上,相比于其他的时间序列预测算法,还具有一定的优势在于:

  • 对缺失值或异常值的包容性强
  • 支持周期和趋势的多尺度性
  • 支持对法定假日或特殊日期的针对性训练

2.3 算法流程

  1. 通过对Prophet对象进行实例化来拟合模型,任何影响预测过程的设置都将在构造模型时被指定(用fit拟合)。
  2. 预测过程需要建立在包含日期ds列的数据基础之上,通过使用辅助的方法 ,Prophet.make_future_dataframe将未来的日期扩展指定的天数,得到一个合规的数据框。默认情况下,这样做会自动包含历史数据的日期,因此我们也可以用来查看模型对于历史数据的拟合效果。
  3. predict方法将会对每一行未来future日期得到一个预测值(称为yhat)。若传入了历史数据的日期,它将会提供样本的模型拟合值。forecast创建的对象是一个新的数据框,其中包含一列预测值yhat,以及成分的分析和置信区间。
  4. 通过 Prophet.plot方法传入预测得到的数据框,可以对预测的效果进行绘图。
import pandas as pd
from Prophet import Prophet
df = pd.read_csv(file)
m = Prophet()                  # 实例化
m.fit(df)                      # 拟合模型

future = m.make_future_dataframe(periods=365) # 构建待预测日期数据框,periods = 365代表除历史数据的日期外再往后推365天
future.tail()                  # 这里仅含ds列,不仅包含了历史日期,也包含了预测日期

forecast = m.predict(future)   # 预测数据集
forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail()

m.plot(forecast)               # 可视化
m.plot_components(forecast)    # 预测的成分分析绘图

3 参数调节

Prophet()有以下一些参数,对于这些参数该如何理解和调节?

Prophet(
    growth='linear', 
    changepoints=None,
    n_changepoints=25,
    changepoint_range=0.8,
    yearly_seasonality='auto',
    weekly_seasonality='auto',
    daily_seasonality='auto',
    holidays=None,
    seasonality_mode='additive',
    seasonality_prior_scale=10.0,
    holidays_prior_scale=10.0,
    changepoint_prior_scale=0.05,
    mcmc_samples=0,
    interval_width=0.8,
    uncertainty_samples=1000,
    stan_backend=None,
)

3.1 趋势突变点

在真实的时间序列数据中,往往在趋势中存在一些突变点。默认情况下,Prophet 将自动监测到这些点,并对趋势做适当地调整。不过,要是对趋势建模时发生了一些问题,例如:Prophet不小心忽略了一个趋势速率的变化或者对历史数据趋势变化存在过拟合现象, 若我们希望对趋势的调整过程做更好地控制,可以通过人工指定或者算法自动选择得到,可采用以下方法:

  • 自动监测突变点:n_changepoints参数设定
    默认Prophet会识别出25个潜在的突变点(均匀分布在前80%的时间序列数据中)。只有在时间序列的前80%才会推断出突变点,以便有足够的长度来预测未来的趋势,并避免在时间序列的末尾出现过度拟合的波动。
  • 自动监测突变点范围:changepoints_range参数设定
    默认Prophet(changepoints_range = 0.8),该默认值表明将在时间序列的前80%处寻找潜在的变化点。
  • 调节趋势灵活性:changepoint_prior_scale参数设定
    默认情况下Prophet(changepoint_prior_scale = 0.05),降低该值会导致趋势拟合的灵活性降低。
  • 人工指定突变点的位置:changepoints参数
from fbProphet.plot import add_changepoints_to_plot
m = Prophet(changepoints=['2014-01-01']) # 设定变点的时间点
m.fit(df)
future = m.make_future_dataframe(periods=365)
forecast = m.predict(future)
fig = m.plot(forecast)
a = add_changepoints_to_plot(fig.gca(), m, forecast) # 查看显著的突变点

3.2 对假期和特征事件建模

若需要专门对节假日或其他事件进行建模,需要为此创建一个新的dataframe,该dataframe的要求如下:

  • 该dataframe包含两列(节假日holiday和日期戳ds),每行分别记录了每个出现的节假日
  • 该dataframe必须包含所有出现的节假日,即存在在历史数据集以及待预测的时期中的节假日

若希望将节假日的时间扩展成一个区间,可以在此数据框基础上再新建两列lower_windowupper_window

playoffs = pd.DataFrame({
  'holiday': 'playoff',
  'ds': pd.to_datetime(['2008-01-13', '2009-01-03', '2010-01-16',
                        '2010-01-24', '2010-02-07', '2011-01-08',
                        '2013-01-12', '2014-01-12', '2014-01-19',
                        '2014-02-02', '2015-01-11', '2016-01-17',
                        '2016-01-24', '2016-02-07']),
  'lower_window': 0,
  'upper_window': 1,
})
superbowls = pd.DataFrame({
  'holiday': 'superbowl',
  'ds': pd.to_datetime(['2010-02-07', '2014-02-02', '2016-02-07']),
  'lower_window': 0,
  'upper_window': 1,
})
holidays = pd.concat((playoffs, superbowls))

m = Prophet(holidays=holidays)
m.fit(df)
future = m.make_future_dataframe(periods=365)
forecast = m.predict(future)

forecast[(forecast['playoff'] + forecast['superbowl']).abs() > 0][['ds', 'playoff', 'superbowl']][-10:]  # 节假日效应展示

注意:上述代码中,将superbowl的日期既记录在了playoff中,也记录在了superbowl中,会造superbowl的效应在playoff的作用下叠加两次。 此时的holiday

假日

3.3 季节性的傅里叶级数

傅里叶级数能够用于估计季节性。当当季节性需要适应更高频率的变化时,增加傅里叶级数可以让季节性需要适应更高频率的变化,但同时也可能导致过拟合。

傅里叶级数默认的情况下:

傅里叶级数默认

傅里叶级数增加的情况下:

傅里叶级数增加

3.4 预测区间

默认情况下返回结果中会包括预测值yhat的预测区间。调节预测区间的范围可以通过调节interval_width参数来实现。

在预测时,不确定性主要来源于三个部分:趋势中的不确定性、季节效应估计中的不确定性和观测值的噪声影响。 趋势的不确定性的最大来源是未来趋势改变的不确定性。这种衡量不确定性的方法可以通过调节变化速率的灵活性来实现:变化速率灵活性更大是(通过增大参数 changepoint_prior_scale的值),预测的不确定性也会随之增大。原因在于如果将历史数据中更多的变化速率加入了模型,也就代表,我们认为未来也会变化得更多,就会使得预测区间成为反映过拟合的标志。

季节的不确定性可以通过贝叶斯取样来得到,可通过设mcmc.samples参数(默认下取0 )来实现。

3.5 异常值

异常值主要通过两种方式影响Prophet预测结果:

  • 时间间隔不完整的数据;
  • 当加入的数据出现了新的异常值再预测建模。

时间间隔不完整的数据

异常值的存在会造成趋势预测看似合理,预测区间的估计却过于广泛。 Prophet 虽能够处理历史数据中的异常值,但仅仅是将它们与趋势的变化拟合在一起,认为未来也会有类似的趋势变化。解决办法就是移除这些异常值。

m = Prophet()
m.fit(df)
future = m.make_future_dataframe(periods=1096)
forecast = m.predict(future)
m.plot(forecast) # 上图

df.loc[(df['ds'] > '2010-01-01') & (df['ds'] < '2011-01-01'), 'y'] = None
model = Prophet().fit(df)
model.plot(model.predict(future))  # 下图
异常值移除之前
异常值移除之后

当加入的数据出现了新的异常值再预测建模

异常值的存在会破坏了季节效应的估计,因此未来的预测也会永久受到这个影响。解决办法就是移除这些异常值。

df = pd.read_csv('examples/example_wp_log_R_outliers2.csv')
m = Prophet()
m.fit(df)
future = m.make_future_dataframe(periods=1096)
forecast = m.predict(future)
m.plot(forecast) # 上图


df.loc[(df['ds'] > '2015-06-01') & (df['ds'] < '2015-06-30'), 'y'] = None  # 将2015年前半年的数据设为缺失
m = Prophet().fit(df)
m.plot(m.predict(future))  # 下图
异常值移除之前
异常值移除之后

4 算法实践

下面是Prophet算法在时序预测和异常检测中的两个典型用例。

4.1 时序预测

拟合一段历史数据,预测未来一段时间的数值。

 

预测图

上图是过去一个月的公司用电情况。由于工作日员工上班,所以导致用电量较高,且持续处于一个相似的水平。而周末时,员工放假,公司仅需要维持日常的机器使用,服务器运行,用电量大幅降低。

Prophet算法基于历史用电数据,准确地预测了公司未来几天的用电情况。在上图中,橘色是拟合值,绿色是预测值,蓝色是实际值,预测值和实际值几乎重叠。

4.2 异常检测

拟合一段历史数据,预测未来一段时间的数值以及置信区间,当未来这段时刻的值超出置信区间,则判定异常,并告警。

异常图

上图是某个web服务的访问量,我们利用Prophet算法拟合过去几天的数据,得到置信区间。而在29日上午9点的时候,由于服务器断电,导致了该时刻请求数为0,引发告警;同时当天下午,该该服务再次出现问题导致请求数减半,同样引发告警。图中红色点为异常值。

5 总结

Prophet算法在异常检测和时序预测两个方面都有较多的应用。

当 Prophet算法被用于时序预测时,支持节假日的输入,按照不同时间间隔进行预测,变点的设置等操作,这些操作在时序预测算法方面都具有一定的优势,且该算法有数学理论支撑,可解释性较强。

同时,Prophet算法经过改造也能用于异常检测。比如我们T+1计算得到未来1天的置信区间,然后基于流式计算判断实际值是否在置信区间内,从而判定是否异常。

 

关于作者

xuxilin
获得点赞
文章被阅读