初识时间序列预测神器 NeuralProphet 实战预测股票指数

更多原创AI内容,请关注 公众号 MyEncyclopedia

NeuralProphet深度学习版Prophet

NeuralProphet 负有盛名,是 Facebook开发的新一代 Prophet 时间序列预测框架,堪称时间序列预测神器。但是它的API使用,调参,原理不太为大家所知,我们会花几期文章和视频,我们将由浅入深,由实践至原理,揭开其神秘面纱。

NeuralProphet 继承了 Prophet 模块可接受性的特点,将预测的值分解到趋势、季节性、AR、事件(节日)几个模块。其中 AR 部分的神经网络实现由 <u>AR-Net: A simple Auto-Regressive Neural Network for time-series</u> 这篇论文详细描述。此外,NeuralProphet 整体用 PyTorch 重新实现,主要特性如下

  • 使用 PyTorch 的优化,性能比原始 Prophet 快不少
  • 引入 AR-Net 建模时间序列自回归,并配有非线性层
  • 自定义损失和指标
  • 滞后协变量(lagged covariates) 和 AR 本地上下文特性 (local context)

尽管 NeuralProphet 有不少优势,但是使用起来小问题不断,主要表现为文档不甚详细,API 设计的比较智能(隐晦),坑不少。这一期我们来实战体验一下,后续会深入代码和论文。

<u>相关论文链接:</u>

安装NeuralProphet

使用命令通过 pip 就可以安装 NeuralProphet。

pip install neuralprophet==0.5.0

如果在 Jupyter Notebook 中使用 NeuralProphet,最好安装实时版本,允许你实时可视化模型损失。

pip install neuralprophet[live]==0.5.0

要注意一点,安装 neuralprophet 会关联安装 Pytorch CPU版本库,如果你需要使用 GPU 或者不希望覆盖原有的 Pytorch 版本,请手动安装。

此外,MyEncyclopedia 和往常一样,为大家准备了一个 docker 镜像,预装最新的 NeuralProphet 库,镜像中还包含预下载的数据集和本文所有的 Jupyter Notebook 代码。大家关注 MyEncyclopedia公众号,执行下面命令后网页打开 http://localhost:8888/ 开箱即用

docker pull myencyclopedia/neuralprophet-tut
docker run -p 8888:8888 myencyclopedia/neuralprophet-tut bash -c 'jupyter notebook --port 8888 --NotebookApp.token='' --NotebookApp.password='' --ip 0.0.0.0 --allow-root'

标准普尔 500 指数数据集

这次实战我们使用过去 10 年标准普尔 500 指数的每日股价数据。可以通过如下命令下载数据集,使用 docker 镜像的同学无需额外下载。

import pandas_datareader as pdr
from datetime import datetime
import matplotlib.pyplot as plt
%matplotlib inlinestart = datetime(2010, 12, 13)
end = datetime(2020, 12, 11)
df_sp500 = pdr.get_data_fred('sp500', start, end)
plt.figure(figsize=(10, 7))
plt.plot(df_sp500)
plt.title('S&P 500 Prices')
image

从图中我们可以清楚地看到标准普尔 500 指数总体呈上升趋势,其中有几个点的价格大幅上涨或下跌。我们可以将这些点视为趋势变化点。鉴于此,我们先从一个仅有趋势模块的 NeuralProphet 模型开始,逐渐加入季节性,AR和节日模块,观察其预测表现和API 具体使用。

使用 NeuralProphet,我们必须确保数据的格式包含如下两列:日期列名ds,目标变量列名 y

df_sp500 = df_sp500.reset_index().rename(columns={'DATE': 'ds', 'sp500': 'y'})

[图片上传失败...(image-bd5fa0-1672716734981)]

[图片上传失败...(image-4b770e-1672716734981)]

len(df_sp500[~df_sp500.y.isnull()])
>>> 2007

总结 SP 500 数据观察到的特点,后面会反复和过程变量做对比:

  • 总共2080条数据中非空数据有2007

  • 开始日期为 2012-12-24,结束日期 2020-12-11

  • 在上述有效时间段内,非交易的日期(周末,节日)没有在列。

模块一:趋势

使用 NeuralProphet,我们可以通过指定几个重要参数来对时间序列数据中的趋势进行建模。

  • n_changepoints — 指定趋势发生变化的点数。
  • trend_reg — 控制趋势变化点的正则化参数。较大的值 (~1–100) 将惩罚更多的变化点。较小的值 (~0.001–1.0) 将允许更多的变化点。
  • changepoints_range — 默认值 0.8,表示后20%的训练数据无 changepoints
model = NeuralProphet(n_changepoints=100,
                      trend_reg=0.05,
                      yearly_seasonality=False,
                      weekly_seasonality=False,
                      daily_seasonality=False,
                      epochs=100)

训练模型

df_train, df_val = model.split_df(df_sp500, freq="D", valid_p=0.2)
metrics = model.fit(df_train,
                    freq='D',
                    validation_df=df_val,
                    progress="plot"
                    )

[站外图片上传中...(image-b1612e-1672716734981)]

训练最终趋于稳定。我们来看看 split_df API 的细节。

image
image

df_train 共1606 行,为前 80% 记录,df_val 共401 行,为后20% 记录,两者没有交集,合计 2007 行数据,等于 df_sp500 有效数据数。原来默认情况下 split_df 会扔掉 y 值为 NaN 数据。这里两者没有交集,大家注意对比启用AR后的数据切分两者会有交集。原有是启用自回归后,预测需要过去 k 个点作为输入。

预测验证集

接着来看看验证集,即 df_val 上的预测表现。

future = model.make_future_dataframe(df_sp500, periods=60, n_historic_predictions=True)
forecast = model.predict(future)
fig = model.plot(forecast)
fig.show()

[图片上传失败...(image-6f9fed-1672716734981)]

make_future_dataframe 准备好待预测的数据格式,参数 periods=60n_historic_predictions=True 意义扩展 df_sp500 到未来60天后,同时保留所有所有现有 df_sp500 的数据点,这些历史点也将做预测。我们 dump 出 make_future_dataframe 后的 future 变量。

image
image

future 序列扩展了 df_sp500,有 y 值的共2007条,和 df_sp500 一致。时间扩展到了 2021-02-09,大约是 2021-12-11 后的60天,这个也和总条数 2140 一致,等于 df_sp500 总条数 2080 加上 periods=60 的部分。

接着来看 predict 后的 forecast 变量。y 列依然有 2007 条,多了 yhat1 和 trend 两列。

image

最后,model.plot(forecast) 会绘制出事实点和预测点的曲线,注意图中预测值比实际值要稍长一些,因为预测值到 2021-02-09,实际值仅到 2020-12-11

模块归因

fig_components = model.plot_components(forecast)
image

由于只启用了趋势,只有一个模块输出。

很明显,我们的模型捕捉到了标准普尔 500 指数的总体上涨趋势,但该模型存在欠拟合问题,尤其是当我们查看未知未来的60天的预测,更能发现问题。

仅预测未来

同样的预测代码,将n_historic_predictions 改成 False 会只预测未知未来60天。

future = model.make_future_dataframe(df_sp500, periods=60, n_historic_predictions=False)
forecast = model.predict(future)
fig = model.plot(forecast)
fig.show()
image
print(len(future), len(forecast))
>>> 60 60

根据上图,我们可以看到模型对未来的预测遵循一条直线,天天上涨的股票,还在这里看什么,还不赶紧去买!

模块二:季节性

真实世界的时间序列数据通常涉及季节性模式。即使对于股票市场也是如此,一月效应等趋势可能会逐年出现。我们可以通过添加年度季节性来使之前的模型更加完善。

model = NeuralProphet(n_changepoints=100,
                      trend_reg=0.05,
                      yearly_seasonality=True,
                      weekly_seasonality=False,
                      daily_seasonality=False,
                      epochs=100)

df_train, df_val = model.split_df(df_sp500, freq="D", valid_p=0.2)

metrics = model.fit(df_train,
                    freq='D',
                    validation_df=df_val,
                    progress="bar"
                    )

预测验证集

[图片上传失败...(image-9c5c70-1672716734981)]

和之前一条直线相比,现在对数据的预测显得更现实些。

模块归因

现在预测的 Y 值是两个部分的模块的加和了。

fig_components = model.plot_components(forecast)
image

标准普尔 500 指数预测具有年度季节性,包括历史数据。

仅预测未来

[图片上传失败...(image-88a2bd-1672716734981)]

根据上图,我们可以看到这个模型更真实一些,但仍然存在欠拟合问题。因此,我们再引入自回归模型 AR 来进一步拟合。

模块三:自回归 AR

AR-Net 是一种用于时间序列预测的自回归神经网络。自回归模型使用来自过去历史数据点来预测后续点,这就是自回归一词的来源。

例如,为了预测标准普尔 500 指数的价格,我们可以训练一个模型,使用过去 60 天的价格来预测未来 60 天的价格。分别对应以下代码中的n_lagsn_forecasts参数。

model = NeuralProphet(
    n_forecasts=60,
    n_lags=60,
    changepoints_range=0.95,
    n_changepoints=100,
    yearly_seasonality=True,
    weekly_seasonality=False,
    daily_seasonality=False,
    batch_size=32,
    epochs=100,
    learning_rate=1.0,
)

训练模型

df_train, df_val = model.split_df(df_sp500, freq="D", valid_p=0.2)
metrics = model.fit(df_train,
                    freq='D',
                    validation_df=df_val,
                    progress="plot"
                    )

切分训练和验证集代码一样,但是由于引入 AR,df_traindf_val 之间有60条数据重合,这是因为,在验证或者预测过程中,传入的 dataframe 前60条不做预测,从61条开始预测,预测会使用当前日期前60条作为 AR 模块的输入。

len(set(df_train.ds.tolist()).intersection(set(df_val.ds.tolist())))
>>> 60

不过奇怪的是,df_train 加上 df_val 总共有 2305 + 665 = 2970 条记录,时间跨度依然是 2012-12-24 至 2020-12-11。但是去除重复的60条记录后居然剩余2910 条, 比 df_sp500 2080 条记录数还要多不少。

image
image

这里笔者稍微花了点时间终于弄清楚:df_traindf_val 会填充 2012-12-24 至 2020-12-11 所有的 missing 日期,并使用插值填充 y!

image

预测验证集

这一次,我们将 periods 设成 0,也就是不扩展 df_sp500 时间到未知的未来。

future = model.make_future_dataframe(df_sp500, periods=0, n_historic_predictions=True)
image
forecast = model.predict(future)
image

forecast 格式变得复杂,引入了 yhat1, yhat2, ...,yhat60,ar1, ar2, ...,ar60 等众多列,这里的60对应于 n_forecasts=60

第一个预测值开始于 forecast 的第61条记录,对应于 n_lags = 60

forecast[~forecast.yhat1.isnull()]
image
fig = model.plot(forecast)
fig.show()
image

模块归因

fig_components = model.plot_components(forecast)

[图片上传失败...(image-13de76-1672716734982)]

仅预测未来

future = model.make_future_dataframe(df_sp500, periods=60, n_historic_predictions=False)
forecast = model.predict(future)
fig = model.plot(forecast)
fig.show()

[图片上传失败...(image-6ae562-1672716734982)]

模块四:事件(节日)

我们还可以配置模型以考虑节假日因素,因为节假日很可能会影响股市走势。

model = NeuralProphet(
    n_forecasts=60,
    n_lags=60,
    changepoints_range=0.95,
    n_changepoints=100,
    yearly_seasonality=True,
    weekly_seasonality=False,
    daily_seasonality=False,
    batch_size=32,
    epochs=100,
    learning_rate=1.0,
)

model = model.add_country_holidays("US", mode="additive", lower_window=-1, upper_window=1)

只需 add_country_holidays 一条语句就可以启用预定义的美国节假日。

预测验证集

image

模块归因

fig_components = model.plot_components(forecast)
image

仅预测未来

future = model.make_future_dataframe(df_sp500, periods=60, n_historic_predictions=False)
forecast = model.predict(future)
fig = model.plot(forecast)
fig.show()
image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容