Python-时间序列数据预测-全-

Python 时间序列数据预测(全)

原文:annas-archive.org/md5/b7bdd7fe65b0a510d6efd9f37e4dc576

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

2017 年,Facebook(现在是 Meta)发布了其 Prophet 软件作为开源。这个强大的工具是由 Facebook 工程师开发的,因为其分析师被经理们要求的大量业务预测所淹没。Prophet 的开发者希望同时解决两个问题:1)完全自动化的预测技术太脆弱、太不灵活,无法处理额外的知识,2)能够持续产生高质量预测的分析师很少,并且需要广泛的专长。Prophet 成功地解决了这两个问题。

Prophet 被设计成,使用没有任何参数调整或其他优化的预测通常质量非常高。尽管如此,只需一点训练,任何人都可以直观地调整模型并显著提高性能。

从最基本模型开始,逐步深入到 Prophet 内部工作的最复杂技术,这本书将教你关于 Prophet 的一切知识。这里讨论了许多连官方文档都没有涵盖的高级功能,并为每个主题提供了完整的示例。这本书的目的不是让你从头开始构建 Prophet 的克隆,但它会教你如何使用 Prophet,甚至可能比 Meta 自己高度训练的工程师还要好。

自从这本书的第一版出版以来,世界发生了巨大的变化。全球 COVID 大流行打乱了每一个预测者的预测,我们都还在努力学习如何在这个新世界中预测。本书的第二版包括了对如何在这些意外事件期间进行预测的理解更新。

此外,自第一版以来,Prophet 经历了许多更新,包括从测试版毕业并发布官方版本 1!我们更新了本书的每个部分和代码块,包括自第一版发布以来 Prophet 的所有新功能和变化。

近年来,其他几家公司的数据科学团队也开源了他们自己的预测包,我们包括了对 NeuralProphet、LinkedIn 的 Greykite 和 Uber 的 Orbit 的新讨论,以及与 Prophet 相比它们的优缺点。由于第一版众多读者的鼓励反馈,我们撰写了一个全新的章节,全部关于 Prophet 背后的数学。这个新章节将使你对如何为你的领域构建最佳的预测有更深入的理解,并提供知识来向利益相关者解释你的预测是如何开发的。

这第二版与第一版相比是一个很大的更新,我迫不及待地想听听你们关于预测工作的反馈!

这本书面向谁

本书面向希望使用 Python 或 R 构建时间序列预测的商业经理、数据科学家、数据分析师、机器学习工程师和软件工程师。为了最大限度地利用本书,您应该对时间序列数据有基本的了解,并能够将其与其他类型的数据区分开来。对预测技术的了解是加分项。

本书涵盖的内容

第一章时间序列预测的历史与发展,将向您介绍理解时间序列数据的最早努力以及迄今为止的主要算法发展。

第二章Prophet 入门,将带您了解如何在您的机器上运行 Prophet 的过程,然后通过构建您的第一个模型来测试您的安装。

第三章Prophet 的工作原理,将讨论为什么 Facebook(现在称为 Meta)决定构建自己的预测包,以及“分析师在循环”预测哲学如何应用于 Prophet。本章还将介绍 Prophet 中预测算法背后的数学方程式。

第四章处理非每日数据,将涵盖如何修改在 第二章Prophet 入门 中采用的方法,以便处理记录在除每日以外的规模上的数据,这样您就可以准备好在后续章节中处理所有示例。

第五章处理季节性,将讨论在 Prophet 中控制季节性的所有方法。季节性是 Prophet 模型的基础之一,包含最多的控制参数,因此这一章是最长的,也是最重要的一章。

第六章预测假日效应,将教您如何将假日效应添加到您的预测中。您将学习如何包括一组基本默认假日,如何为不同地区更改该组,如何添加您自己的自定义假日,以及如何控制效应的强度。

第七章控制增长模式,将描述 Prophet 中趋势线可以遵循的三个增长模式:线性、逻辑和水平。您将学习将这些模式应用于哪些场景,以及它们对未来预测有何影响。

第八章影响趋势变化点,将讨论如何控制最终模型的刚性。您将学习如何创建一个可以经常改变方向的灵活模型或遵循恒定线的刚性模型,为什么您可能选择其中之一,以及这对您在未来的数据上使用模型的不确定性有何影响。

第九章, 包含额外的回归因子,将教你如何在模型中包含额外的数据列。与多元回归类似,Prophet 能够结合多个输入向量进行预测性预报。

第十章, 处理异常值和特殊事件,将展示异常值在 Prophet 模型中可能引起的两种类型的问题,并教你几种自动化的技术来识别异常值以及如何使用 Prophet 处理它们。

第十一章, 管理不确定性区间,将涵盖如何使用不同的统计方法量化模型中的不确定性,每种方法的优缺点,以及如何可视化模型中的风险量。

第十二章, 执行交叉验证,将教你如何在 Prophet 中执行交叉验证。你可能已经熟悉机器学习中的交叉验证技术,但在时间序列数据中,需要不同的方法。本章将教你这种方法以及如何在 Prophet 中实现它。

第十三章, 评估性能指标,将在前一章的基础上介绍 Prophet 的特性性能指标。你将学习如何将交叉验证与所选性能指标结合,进行网格搜索并优化你的模型以获得最高的预测准确性。

第十四章, 将 Prophet 投入生产,是最后一章,将教你一些在生产环境中使用 Prophet 时非常有用的额外技术。你将学习如何保存模型以供以后使用,如何随着新数据的到来更新模型,以及如何使用 Prophet 的 Plotly 绘图函数构建高度交互式的图表,适合在基于网络的仪表板上共享。

为了充分利用这本书

要运行本书中的代码示例,你需要安装 Python 3.x。本书中的所有示例都是使用 Prophet 版本 1.1 在 Jupyter 笔记本中制作的。macOS、Windows 和 Linux 都受到支持。尽管本书中的所有示例都将使用 Python 编写,但所有内容也与 R 语言完全兼容,如果你更喜欢使用 R 语言,你也可以使用它,尽管本书不会涵盖 R 语言的语法。请参考官方 Prophet 文档了解 R 语言的语法(facebook.github.io/prophet/))。

第二章使用 Prophet 入门,将指导您安装 Prophet,并且强烈建议安装 Anaconda 或 Miniconda 以正确安装 Prophet 的所有依赖项。虽然可以在不使用 Anaconda 的情况下安装 Prophet,但这取决于您的机器的具体配置,可能会非常困难,本书假设将使用 Anaconda。

为了跟随示例,您至少需要熟悉用于数据处理的数据处理库pandas和用于制作图表的 Matplotlib。在少数情况下,将使用numpy库来模拟随机数据,但跟随示例不需要您了解 NumPy 语法。所有这些库都将作为 Prophet 依赖项自动安装,如果尚未安装。本书中使用的所有数据集都托管在此书的 GitHub 仓库中,并可在此处下载:github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition

本书中涵盖的软件/硬件 操作系统要求
Prophet Windows、macOS 或 Linux(任何)
Python 3.7+

Prophet 支持与 Dask 的并行化,但设置 Prophet 在 Dask 集群上运行的内容将涵盖,安装和使用 Dask 超出了本书的范围。如果您感兴趣,我鼓励您参考 Dask 文档:docs.dask.org/en/stable/。同样,本书将涵盖如何在 Plotly 中构建交互式 Prophet 可视化,但将这些内容组合成 Dash 仪表板将留给读者在其他地方学习。Dash 文档:dash.plotly.com/是一个很好的起点。

如果您正在使用这本书的数字版,我们建议您亲自输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将帮助您避免与复制/粘贴代码相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

本书中使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“要控制 Prophet 的自动变化点检测,您可以在模型实例化期间使用n_changepointschangepoint_range参数修改这两个值。”

代码块设置如下:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                n_changepoints=5)

当我们希望将您的注意力引到代码块的一部分时,相关的行或项目将以粗体显示:

model = Prophet()
model.fit(df)
future = model.make_future_dataframe(periods=60, freq='MS')
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

任何命令行输入或输出都应如下编写:

pip install pystan
pip install prophet

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“在以下季节性图中,我使用了工具栏中的切换峰值线比较数据按钮来添加更多到悬停工具提示的信息。”

小贴士或重要注意事项

显示如下。

联系我们

我们始终欢迎读者的反馈。

customercare@packtpub.com

勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在此书中发现错误,我们将非常感谢您向我们报告。请访问www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

请通过copyright@packt.com发送链接到该材料。

如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了《使用 Prophet 预测时间序列数据》,我们非常乐意听到您的想法!请点击此处直接转到此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载此书的免费 PDF 副本。

感谢您购买此书!

您喜欢在路上阅读,但无法携带您的印刷书籍到处走吗?

您的电子书购买是否与您选择的设备不兼容?

别担心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠远不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日邮箱访问权限。

按照以下简单步骤获取优惠:

  1. 扫描下面的二维码或访问以下链接

packt.link/free-ebook/9781837630417

  1. 提交您的购买证明

  2. 就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的邮箱。

第一部分:Prophet 入门

本书的第一部分将向您介绍导致 Prophet 诞生的时序预测技术的历史发展,然后指导您进行程序的安装。本节接着将带您了解一个基本的 Prophet 预测模型,并介绍此类模型产生的输出。第一部分以对 Prophet 构建预测所使用的数学方法的描述结束。

本节包括以下章节:

  • 第一章, 时间序列预测的历史与发展

  • 第二章, Prophet 入门

  • 第三章, Prophet 的工作原理

第一章:时间序列预测的历史与发展

Prophet 是一个强大的工具,用于创建、可视化和优化您的预测!使用 Prophet,您将能够理解哪些因素将推动您的未来结果,这将使您能够做出更有信心的决策。您将通过一个直观但非常灵活的编程界面来完成这些任务和目标,这个界面旨在为初学者和专家 alike。

您不需要对时间序列预测技术背后的数学或统计知识有深入的了解,就可以利用 Prophet 的强大功能。尽管如果您具备这些知识,Prophet 还包括丰富的功能集,让您能够将您的经验发挥到极致。您将在一个结构化的范式下工作,其中每个问题都遵循相同的模式,这样您可以花更少的时间去优化您的预测,更多的时间去发现关键见解,从而增强您的决策。

本章介绍了时间序列预测背后的基本思想,并讨论了一些关键模型迭代,这些迭代最终导致了 Prophet 的开发。在本章中,您将了解时间序列数据是什么,以及为什么它必须与非时间序列数据不同处理,然后您将发现其中最强大的创新,其中 Prophet 是最新的之一。具体来说,我们将涵盖以下主题:

  • 理解时间序列预测

  • 移动平均和指数平滑

  • ARIMA

  • ARCH/GARCH

  • 神经网络

  • Prophet

  • 近期发展

理解时间序列预测

时间序列是一组按时间顺序收集的数据。例如,想想任何图表,其中x轴是时间的某种度量——从大爆炸以来宇宙中的星星数量,到每次核反应中每纳秒释放的能量。这两者背后的数据都是时间序列。您手机上天气应用中显示的接下来 7 天的预期温度?这也是一个时间序列的图。

在这本书中,我们主要关注人类尺度上的事件,如年、月、日和小时,但所有这些都是时间序列数据。预测未来值就是预测的行为。

预测天气显然对人类来说自古以来就很重要,尤其是在农业出现之后。事实上,2300 多年前,希腊哲学家亚里士多德撰写了一篇名为《气象学》的论文,其中讨论了早期的天气预报。实际上,“预测”这个词是在 19 世纪 50 年代由一位英国气象学家罗伯特·菲茨罗伊创造的,他在查尔斯·达尔文的开拓性航行中作为“贝格尔号”的船长而闻名。

然而,时间序列数据并不仅限于天气。医学领域在 1901 年由荷兰医生威廉·埃因托芬发明了第一台实用的心电图ECG)后,采用了时间序列分析技术。心电图产生了我们现在在医疗剧中看到的患者床边的熟悉的心跳模式。

今天,最被讨论的预测领域之一是经济学。有整个电视频道致力于分析股市的趋势。政府使用经济预测来咨询中央银行政策,政治家使用经济预测来发展他们的平台,而商业领袖使用经济预测来指导他们的决策。

在这本书中,我们将预测各种主题,如大气中的二氧化碳水平、芝加哥公共自行车共享项目的骑行者数量、黄石公园狼群的增长、太阳黑子周期、当地降雨量,甚至某些热门账户的 Instagram 点赞数。

依赖数据的难题

那么,为什么时间序列预测需要独特的方法呢?从统计学的角度来看,你可能会看到时间序列的散点图,其中有一个相对清晰的趋势,并尝试使用标准回归——将直线拟合到数据的技术来拟合一条线。问题是这违反了线性回归所要求的独立性假设。

为了用例子说明时间序列的依赖性,让我们假设一个赌徒正在掷一个公平的骰子。我告诉你他们刚刚掷出了一个 2,然后问你下一个值会是什么。这些数据是独立的;之前的掷骰子对未来的掷骰子没有影响,所以知道之前的掷骰子是 2 并不提供关于下一个掷骰子的任何信息。

然而,在另一种情况下,比如我从一个未公开的地球上的地点给你打电话,让你猜测我所在地点的温度。你最好的猜测是猜测当天的平均全球温度。现在,想象一下我告诉你昨天我所在地点的温度是 90°F。这为你提供了大量的信息,因为你直觉上知道昨天的温度和今天的温度以某种方式相关联;它们不是独立的。

在时间序列数据中,你不能随机打乱数据的顺序而不破坏趋势,在合理的误差范围内。数据的顺序很重要;它不是独立的。当数据像这样依赖时,回归模型可以通过随机机会显示出统计显著性,即使没有真正的相关性,也比你所选择的置信水平所暗示的更频繁。

因为高值往往跟随高值,低值往往跟随低值,所以时间序列数据集更有可能显示出比其他情况下更多的高值或低值集群,而这反过来又可能导致出现比其他情况下更多的相关性。

Tyler Vigen 的网站 Spurious Correlations 专门指出看似重要但实际上荒谬的时间序列关联的例子。以下是一个例子:

图 1.1 – 一个虚假的时间序列关联(https://www.tylervigen.com/spurious-correlations)

图 1.1 – 一个虚假的时间序列关联(https://www.tylervigen.com/spurious-correlations)

显然,每年在游泳池中溺水的人数与尼古拉斯·凯奇出演的电影数量完全无关。它们之间没有任何影响。然而,通过将时间序列数据视为独立数据的谬误,Vigen 已经表明,纯粹是随机机会,这两组数据确实存在显著的关联。当忽略时间序列数据中的依赖性时,这种随机机会更有可能发生。

现在您已经了解了时间序列数据究竟是什么,以及它与其他数据集的区别,让我们来看看模型发展历程中的几个里程碑,从最早的模型到 Prophet。

移动平均和指数平滑

可能最简单的预测形式是 移动平均MA)。通常,移动平均被用作 平滑技术,以在变化很大的数据中找到一条更直的线。每个数据点都调整到周围 n 个数据点的平均值,其中 n 被称为窗口大小。例如,窗口大小为 10 时,我们会调整一个数据点,使其成为之前 5 个值和之后 5 个值的平均值。在预测环境中,未来值被计算为 n 个先前值的平均值,因此,窗口大小为 10 时,这意味着 10 个先前值的平均值。

使用移动平均的平衡行为是,您希望有一个大的窗口大小来平滑噪声并捕捉实际趋势,但随着窗口大小的增大,您的预测将显著滞后趋势,因为您需要回溯得更远来计算平均值。指数平滑背后的思想是对随时间平均的值应用指数递减的权重,给近期值更多的权重,给较远期值更少的权重。这允许预测对变化更加敏感,同时仍然忽略大量噪声。

如您在以下模拟数据的图中所示,移动平均线的行为比指数平滑线更粗糙,但两条线仍然同时调整趋势变化:

图 1.2 – 移动平均与指数平滑

图 1.2 – 移动平均与指数平滑

指数平滑法起源于 20 世纪 50 年代,其最初形式为简单指数平滑法,不允许趋势或季节性。查尔斯·霍尔特在 1957 年将技术提升到允许趋势,他称之为双指数平滑法;与彼得·温特斯合作,霍尔特在 1960 年增加了季节性支持,这通常被称为霍尔特-温特斯指数平滑法

这些预测方法的缺点是它们对新趋势的调整可能较慢,因此预测值落后于现实——它们在较长的预测时间框架中表现不佳,并且有许多超参数需要调整,这可能是一个困难且非常耗时的过程。

ARIMA

在 1970 年,数学家乔治·博克斯和 Gwilym Jenkins 发表了《时间序列:预测与控制》,其中描述了现在所知的博克斯-詹金斯模型。这种方法通过开发ARIMA将 MA 的概念进一步发展。作为一个术语,ARIMA 通常与博克斯-詹金斯互换使用,尽管技术上,博克斯-詹金斯指的是 ARIMA 模型的参数优化方法。

ARIMA 是一个缩写,代表三个概念:自回归AR)、积分I)和移动平均MA)。我们已经理解了 MA 部分。AR 意味着模型使用数据点与一定数量的滞后数据点之间的依赖关系。也就是说,模型基于先前值预测未来的值。这与预测因为整个星期到目前为止都很暖和,所以明天将会很暖和相似。

积分部分意味着不是使用任何原始数据点,而是使用该数据点与先前数据点之间的差值。本质上,这意味着我们将一系列值转换为一系列值的变化。直观地,这表明明天的温度将与今天大致相同,因为整个星期温度变化不大。

ARIMA 模型的 AR、I 和 MA 各个组成部分在模型中都被明确指定为一个参数。传统上,p用于表示要使用的滞后观测值的数量,也称为滞后阶数。原始观测值差分的次数或差分的程度称为d,而q代表 MA 窗口的大小。因此,ARIMA 模型的标准表示为ARIMA(p, d, q),其中pdq都是非负整数。

ARIMA 模型的一个问题是它们不支持季节性,或具有重复周期的数据,例如白天温度上升而夜晚下降,或夏季上升而冬季下降。季节性 ARIMASARIMA)是为了克服这一缺点而开发的。与 ARIMA 表示法类似,SARIMA 模型的表示法为SARIMA(p, d, q)(P, D, Q)m,其中P是季节性自回归阶数,D是季节性差分阶数,Q是季节性移动平均阶数,而m是单个季节周期的时间步数。

你还可能遇到 ARIMA 模型的其它变体,包括向量 ARIMAVARIMA),用于具有多个时间序列作为向量的情况;分数 ARIMAFARIMA)或自回归分数积分移动平均

PD:作为 P 关键字的风格ARFIMA),两者都包括分数差分度,允许在时间上相隔较远的观测值具有非可忽略的依赖性;以及SARIMAX,这是一个季节性 ARIMA模型,其中X代表添加到模型中的外生或额外变量,例如将降雨预报添加到温度模型中。

ARIMA 通常表现出非常好的结果,但其缺点是复杂性。调整和优化 ARIMA 模型通常计算成本高昂,成功的结果可能取决于预测者的技能和经验。这不是一个可扩展的过程,更适合熟练从业者进行临时分析。

ARCH/GARCH

当数据集的方差随时间变化时,ARIMA 模型在建模时会遇到问题。特别是在经济学和金融学中,这是常见的。在金融时间序列中,大回报往往伴随着大回报,而小回报往往伴随着小回报。前者称为高波动性,后者称为低波动性

自回归条件异方差ARCH)模型是为了解决这个问题而开发的。异方差性是一种说法,意味着数据的变化或分布在整个过程中不是恒定的,其对立术语是同方差性。差异在此可视化:

图 1.3 – 斯凯迪斯提

图 1.3 – 斯凯迪斯提

罗伯特·恩格尔于 1982 年首次介绍了 ARCH 模型,通过将条件方差描述为先前值的函数。例如,白天用电量的不确定性远大于夜间用电量。因此,在电力使用模型中,我们可能会假设白天的小时数具有特定的方差,而夜间使用则具有较低的方差。

1986 年,蒂姆·博勒尔塞夫和斯蒂芬·泰勒在他们的广义 ARCHGARCH)模型中引入了移动平均成分。在电力示例中,使用量方差是时间的函数,但波动性的波动可能并不一定发生在特定的时间,波动本身是随机的。这就是 GARCH 发挥作用的时候。

尽管 ARCH 和 GARCH 模型都无法处理趋势或季节性,但在实践中,通常首先构建 ARIMA 模型以提取时间序列的季节变化和趋势,然后使用 ARCH 模型来模拟预期的方差。

神经网络

时间序列预测中相对较新的发展是使用循环神经网络RNNs)。这得益于 Sepp Hochreiter 和 Jürgen Schmidhuber 在 1997 年开发的长短期记忆LSTM)单元。本质上,LSTM 单元允许神经网络处理一系列数据,如语音或视频,而不是单个数据点,如图像。

标准的循环神经网络(RNN)被称为“循环”是因为它内部有循环结构,这赋予了它记忆能力,也就是说,它能够访问之前的信息。一个基本的神经网络可以通过学习从之前的图像中识别行人的样子来训练识别街道上的行人图像,但它不能通过观察视频之前帧中行人的接近来训练识别视频中行人即将过马路。它没有关于导致行人走上马路的图像序列的知识。短期记忆是网络需要暂时提供上下文的部分,但这种记忆很快就会退化。

早期的 RNN 存在一个记忆问题:它的记忆时间非常短。在句子“飞机在空中飞……”中,一个简单的 RNN 可能能够猜测下一个词将是“天空”,但在“我去年夏天去了法国度假。这就是为什么我在春天学习说……”中,RNN 猜测下一个词是“法语”就不再那么容易了;它理解语言应该接下来,但它忘记了短语是以提到法国开始的。然而,LSTM 具有这种必要的上下文。它为网络的短期记忆提供了更长的寿命。在时间序列数据的情况下,其中模式可以在长时间尺度上重复,LSTM 可以表现得非常好。

与这里讨论的其他预测方法相比,使用 LSTM 进行时间序列预测仍然处于初级阶段;然而,它显示出希望。与其他预测技术相比的一个强大优势是神经网络能够捕捉非线性关系,但与任何深度学习问题一样,LSTM 预测需要大量的数据和计算能力,以及较长的处理时间。

此外,还有许多关于模型架构和要使用的超参数的决定需要做出,这需要一个非常经验丰富的预测者。在大多数实际问题上,必须考虑预算和截止日期,ARIMA 模型通常是更好的选择。

Prophet

Prophet 是由 Facebook(现更名为Meta)内部开发的,由 Sean J. Taylor 和 Ben Letham 开发,旨在克服其他预测方法中经常遇到的两个问题:更自动化的预测工具往往过于不灵活,无法适应额外的假设,而更稳健的预测工具则需要具有专业数据科学技能的经验分析师。Facebook 对高质量商业预测的需求超过了他们的分析师能够提供的。2017 年,Facebook 将 Prophet 作为开源软件发布给公众。

Prophet 被设计用来最优地处理商业预测任务,这些任务通常具有以下任何属性:

  • 以每小时、每日或每周的级别捕获的时间序列数据,理想情况下至少有一年的历史数据

  • 每日、每周和/或每年出现的强烈季节性效应

  • 假日和其他特殊的一次性事件,这些事件不一定遵循季节性模式,但发生不规则

  • 缺失数据和异常值

  • 重大趋势变化,例如在推出新功能或产品时可能发生

  • 趋势逐渐接近上限或下限

默认情况下,Prophet 通常会产生非常高质量的预测,但它也非常可定制,对于没有时间序列数据专业知识的数据分析师来说,它也是易于接近的。正如您将在后面的章节中看到的,调整 Prophet 模型是非常直观的。

实际上,Prophet 是一个加性回归模型。这意味着模型仅仅是几个(可选)组件的总和,例如以下内容:

  • 线性或逻辑增长趋势曲线

  • 每年季节性曲线

  • 每周季节性曲线

  • 每日季节性曲线

  • 假日和其他特殊事件

  • 例如,额外的用户指定季节性曲线,如每小时或每季度

以一个具体的例子来说,假设我们正在模拟一家小型在线零售店在 4 年内的销售情况,从 2000 年 1 月 1 日到 2003 年底。我们观察到整体趋势随着时间的推移而不断上升,从每天1,000次销售增加到时间周期结束时的约1,800次。我们还看到春季的销售量比平均水平高出约50个单位,而秋季的销售量比平均水平低约50个单位。每周,销售量通常在星期二最低,整个星期逐渐增加,星期六达到峰值。最后,在一天中的各个时段,销售量在中午达到峰值,然后平稳下降到午夜最低。这就是那些个别曲线的样子(注意每个图表上的不同x轴刻度):

图 1.4 – 模型组件

图 1.4 – 模型组件

一个加性模型会将这四个曲线简单相加,以得到销售多年的最终模型。随着子组件的累加,最终曲线变得越来越复杂:

图 1.5 – 加性模型

图 1.5 – 加性模型

此前的图表仅显示了前一年,以便更好地看到每周和每日的变化,但完整的曲线延伸了 4 年。

在底层,Prophet 是用Stan编写的,这是一种概率编程语言(有关 Stan 的更多信息,请访问mc-stan.org/)。这有几个优点。它允许 Prophet 优化拟合过程,使其通常在不到一秒内完成。Stan 也与 Python 和 R 兼容,因此 Prophet 团队能够在两种语言实现之间共享相同的核心拟合过程。此外,通过使用贝叶斯统计,Stan 允许 Prophet 为未来预测创建不确定性区间,从而添加数据驱动的预测风险估计。

Prophet 能够以典型的结果与更复杂的预测技术相媲美,但只需付出一小部分努力。它适合每个人。初学者只需几行代码就能构建一个高度准确的模型,而不必 necessarily 理解一切工作的细节,而专家可以深入研究模型,添加更多功能,调整超参数以获得更好的性能。

最近的发展

Prophet 的公开发布激发了围绕预测包的大量开源活动。尽管 Prophet 仍然是使用最广泛的工具,但仍有一些竞争包需要关注。

NeuralProphet

由于其易于学习、快速从数据中预测以及可定制性,Prophet 已经变得非常流行。然而,它确实有一些缺点;其中关键的一个是它是一个线性模型。正如本章前面所讨论的,当预测任务需要非线性模型时,通常会使用神经网络,尽管分析师必须非常了解时间序列和应用的机器学习,才能有效地应用这些模型。NeuralProphet (github.com/ourownstory/neural_prophet)旨在弥合这一差距,并允许只有时间序列专业知识的分析师构建一个非常强大的神经网络模型。

斯坦福大学的 Oskar Triebe 在开源社区的帮助下,已经构建和优化了 NeuralProphet 多年,但截至写作时,NeuralProphet 仍处于测试阶段。它用 PyTorch 替换了 Prophet 对 Stan 语言的依赖,从而实现了深度学习方法。NeuralProphet 使用自回归网络AR-Net)来模拟时间序列自相关,并使用前馈神经网络来模拟滞后回归器。编程接口的设计与 Prophet 几乎相同,因此对于已经熟悉 Prophet 的人来说,学习如何在 NeuralProphet 中构建模型将会非常熟悉。

Google 的“大规模稳健时间序列预测”

为了不甘落后,2017 年 4 月,在 Facebook 宣布 Prophet 开源两个月后,谷歌在其博客文章《我们追求大规模稳健的时间序列预测》(Our quest for a robust time series forecasting at scale)中描述了他们解决预测问题的方案(www.unofficialgoogledatascience.com/2017/04/our-quest-for-robust-time-series.html)。与 Prophet 不同,谷歌的包不是开源的,因此公开可用的细节很少。Prophet 和谷歌方法之间的一个关键区别是,谷歌的预测包使用集成方法来预测增长趋势。在时间序列的背景下,这意味着谷歌拟合多个预测模型,去除任何异常值,并取每个单独模型的加权平均值,以得到最终的模型。截至写作时,谷歌尚未宣布任何计划将其预测包开源。

LinkedIn 的 Silverkite/Greykite

与 Facebook 和谷歌相比,LinkedIn 是开源预测社区的新来者。2021 年 5 月,LinkedIn 宣布了他们的Greykite预测库,用于 Python (github.com/linkedin/greykite),该库使用他们自己的Silverkite算法(Prophet 算法也是 Greykite 建模框架内的选项)。Greykite 的开发是为了为 LinkedIn 的预测提供一些关键好处:解决方案必须灵活、直观且快速。如果这听起来很熟悉,那是因为这正是 Facebook 在开发 Prophet 时追求的品质。

与 Prophet 使用贝叶斯方法来拟合模型不同,Silverkite 使用更传统的模型,如岭回归、弹性网络和提升树。Prophet 和 Silverkite 都可以建模线性增长,但只有 Silverkite 可以处理平方根和二次增长。然而,Prophet 可以建模逻辑增长,这是 Silverkite 无法做到的。从分析师的角度来看,Silverkite 最令人兴奋的方面可能是可以通过外部变量轻松地将领域专业知识添加到模型中。Silverkite 使用sklearn作为其 API,因此任何熟悉该库的用户在使用 Silverkite 时应该不会有任何困难。

Uber 的 Orbit

当 LinkedIn 宣布 Greykite 库的同时,Uber 也宣布了他们自己的预测包,面向对象的贝叶斯时间序列(Orbit) (github.com/uber/orbit)。正如其名所示,Orbit 与 Prophet 一样是贝叶斯方法。然而,Orbit 被设计得比 Prophet 更具通用性,弥合了典型商业问题与更复杂的统计解决方案之间的差距。

尽管 Uber 的基准测试表明 Orbit 在所有类型的预测问题上都表现良好,但其核心用途是在营销组合模型中,这是一种量化几个营销输入对销售影响的技巧。Orbit 通过两种主要的贝叶斯结构时间序列实现:sklearn范式以帮助新用户入门。

摘要

通过对时间序列的简要概述,你了解到如果不用专门的技术分析,时间序列数据可能会出现问题。你跟随数学家和统计学家的发展,他们创造了新技术以实现更高的预测精度或更大的使用便捷性。你还了解到是什么激励了 Prophet 团队为这个传统做出自己的贡献,以及他们在方法上做出了哪些决策,你还了解到开源社区是如何对此做出反应并开始研究不同方法的。

在下一章中,你将学习如何在你的机器上运行 Prophet 并构建你的第一个模型。到这本书的结尾,你将理解每一个特性,无论大小,并将它们全部纳入你的工具箱,以增强你自己的预测能力。

第二章:开始使用 Prophet

Prophet 是一个开源软件,这意味着其底层代码的整个内容对任何人都是免费可检查和修改的。这使得 Prophet 具有很大的力量,因为任何用户都可以添加功能或修复错误,但它也有其缺点。许多封闭源代码软件包,如 Microsoft Word 或 Tableau,都包含在其独立的安装文件中,具有整洁的图形用户界面,不仅可以帮助用户完成安装,而且一旦安装完毕,还可以与软件进行交互。

与之相反,Prophet 通过 PythonR 编程语言访问,并依赖于许多额外的开源库。这使得它具有很大的灵活性,因为用户可以调整功能或甚至添加全新的功能以适应他们特定的需求,但这也带来了潜在的可用性困难。这正是本书旨在简化的目标。

在本章中,我们将根据您使用的操作系统,向您展示整个安装过程,然后我们将通过模拟过去几十年大气二氧化碳水平来共同构建我们的第一个预测模型。

本章将全面涵盖以下内容:

  • 安装 Prophet

  • 在 Prophet 中构建简单模型

  • 解读预测 DataFrame

  • 理解组件图

技术要求

本章示例的数据文件和代码可以在 github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition 找到。在本章中,我们将介绍安装许多要求的过程。因此,为了开始本章,您只需要拥有一台能够运行 Anaconda 和 Python 3.7+的 Windows、macOS 或 Linux 机器。

安装 Prophet

在您的机器上安装 Prophet 是一个简单的过程。然而,在底层,Prophet 依赖于 Stan 编程语言,而安装其 Python 接口 PyStan 并不简单,因为它需要许多非标准编译器。

但别担心,因为有一个非常简单的方法来安装 Prophet 及其所有依赖项,无论您使用哪种操作系统,那就是通过 Anaconda。

Anaconda 是一个免费的 Python 发行版,它捆绑了数百个对数据科学有用的 Python 包,以及包管理系统 conda。这与从 www.python.org/ 的源代码安装 Python 语言形成对比,后者将包括默认的 Python 包管理器,称为 pip

pip 安装一个新的包时,它将安装所有依赖项,而不会检查这些依赖的 Python 包是否会与其他包冲突。当其中一个包依赖于一个版本,而另一个包需要不同版本时,这可能会成为一个特别的问题。例如,你可能有一个 Google 的 TensorFlow 包的工作安装,该包需要 NumPy 包来处理大型多维数组,并使用 pip 安装一个指定不同 NumPy 版本作为依赖项的新包。

然后,不同的 NumPy 版本将覆盖其他版本,你可能会发现 TensorFlow 突然无法按预期工作,甚至完全无法工作。相比之下,conda 将分析当前环境,并自行确定如何为所有已安装的包安装兼容的依赖集,如果无法完成,将提供警告。

PyStan 以及许多其他 Python 工具,实际上都需要用 C 语言编写的编译器。这类依赖无法使用 pip 安装,但 Anaconda 已经包含了它们。因此,强烈建议首先 安装 Anaconda

如果你已经有一个你满意的 Python 环境,并且不想安装完整的 Anaconda 发行版,有一个更小的版本可供选择,称为 conda,Python 以及一小部分必需的包。虽然技术上可以在没有 Anaconda 的情况下安装 Prophet 及其所有依赖项,但这可能非常困难,而且过程会因使用的机器而大不相同,因此编写一个涵盖所有场景的单个指南几乎是不可能的。

本指南假设你将从一个 Anaconda 或 Miniconda 安装开始,使用 Python 3 或更高版本。如果你不确定是否想要 Anaconda 或 Miniconda,选择 Anaconda。请注意,由于包含了所有包,完整的 Anaconda 发行版将需要你的电脑上大约 3 GB 的空间,因此如果空间是个问题,你应该考虑 Miniconda。

重要提示

截至 Prophet 版本 0.6,Python 2 已不再受支持。在继续之前,请确保你的机器上已安装 Python 3.7+。强烈建议安装 Anaconda。

macOS 上的安装

如果你还没有安装 Anaconda 或 Miniconda,那么这应该是你的第一步。安装 Anaconda 的说明可以在 Anaconda 文档中找到,网址为 docs.anaconda.com/anaconda/install/mac-os/。如果你知道你想要 Miniconda 而不是 Anaconda,从这里开始:docs.conda.io/projects/continuumio-conda/en/latest/user-guide/install/macos.html。在两种情况下,都使用安装的默认设置。

安装 Anaconda 或 Miniconda 后,可以使用 conda 来安装 Prophet。只需在终端中运行以下两个命令,首先安装 PyStan 所需的编译器集合 gcc,然后安装 Prophet 本身,这将自动安装 PyStan:

conda install gcc
conda install -c conda-forge prophet

之后,你应该可以开始使用了!你可以跳过到 在 Prophet 中构建简单模型 部分,我们将看到如何构建你的第一个模型。

Windows 上的安装

与 macOS 类似,第一步是确保已安装 Anaconda 或 Miniconda。Anaconda 安装说明可在 docs.anaconda.com/anaconda/install/windows/ 找到,而 Miniconda 的说明则在此:docs.conda.io/projects/continuumio-conda/en/latest/user-guide/install/windows.html

在 Windows 上,你必须勾选复选框以将 Anaconda 注册为默认的 Python 版本。这是正确安装 PyStan 所必需的。你可能看到的是除这里显示的版本之外的 Python 版本,例如 Python 3.8:

图 2.1 – 将 Anaconda 注册为默认 Python 版本

图 2.1 – 将 Anaconda 注册为默认 Python 版本

一旦安装了 Anaconda 或 Miniconda,你将能够访问 gcc,这是 PyStan 所需的编译器集合,然后通过在命令提示符中运行以下两个命令来安装 Prophet 本身,这将自动安装 PyStan:

conda install gcc
conda install -c conda-forge prophet

第二个命令包含额外的语法,指示 condaconda-forge 通道中查找 Prophet 文件。conda-forge 是一个社区项目,允许开发者将他们的软件作为 conda 包提供。Prophet 不包含在默认的 Anaconda 发行版中,但通过 conda-forge 通道,Facebook 团队直接通过 conda 提供了访问权限。

这样就应该成功安装了 Prophet!

Linux 上的安装

在 Linux 上安装 Anaconda 与 macOS 或 Windows 相比只需额外几步,但它们不应造成任何问题。完整说明可在 Anaconda 的文档中找到,网址为 docs.anaconda.com/anaconda/install/linux/。Miniconda 的说明可在 docs.conda.io/projects/continuumio-conda/en/latest/user-guide/install/linux.html 找到。

由于 Linux 由各种发行版提供,因此无法编写一个全面详尽的 Prophet 安装指南。然而,如果你已经在使用 Linux,那么你对它的复杂性应该也很熟悉。

只需确保你已经安装了 gccg++build-essential 编译器,以及 python-devpython3-dev Python 开发工具。如果你的 Linux 发行版是 Red Hat 系统,请安装 gcc64gcc64-c++。之后,使用 conda 安装 Prophet:

conda install -c conda-forge prophet

如果一切顺利,你现在应该已经准备好了!让我们通过构建你的第一个模型来测试它。

在 Prophet 中构建一个简单的模型

直接测量大气中二氧化碳CO2)的最长记录始于 1958 年 3 月,由斯克里普斯海洋研究所的查尔斯·大卫·凯林(Charles David Keeling)开始。凯林位于加利福尼亚州的拉霍亚,但他获得了国家海洋和大气管理局NOAA)的许可,在夏威夷岛上的火山马乌纳洛亚(Mauna Loa)北部斜坡上 2 英里高的设施中收集二氧化碳样本。在这个海拔高度,凯林的测量不会受到附近工厂等局部二氧化碳排放的影响。

1961 年,凯林(Keeling)发布了迄今为止收集的数据,确立了二氧化碳水平存在强烈季节性变化,并且它们正在稳步上升的趋势,这一趋势后来被称为凯林曲线。到 1974 年 5 月,NOAA 已经开始进行自己的平行测量,并且一直持续到现在。凯林曲线图如下:

图 2.2 – 凯林曲线,显示大气中二氧化碳的浓度

图 2.2 – 凯林曲线,显示大气中二氧化碳的浓度

由于其季节性和上升趋势,这条曲线是尝试使用 Prophet 的良好候选。这个数据集包含 53 年间的超过 19,000 个每日观测值。二氧化碳的测量单位是百万分之一PPM),表示每百万个空气分子中的二氧化碳分子数。

要开始我们的模型,我们需要导入必要的库,pandasmatplotlib,并从 prophet 包中导入 Prophet 类:

import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet

作为输入,Prophet 总是需要一个包含两列的 pandas DataFrame:

  • ds,日期戳,应该是 pandas 预期格式的 datestamptimestamp

  • y,一个包含我们希望预测的测量的数值列

在这里,我们使用 pandas 导入数据,在这种情况下,一个 .csv 文件,并将其加载到一个 DataFrame 中。请注意,我们还把 ds 列转换成 pandas 的 datetime 格式,以确保 pandas 正确地将其识别为包含日期,而不是简单地将其作为字母数字字符串加载:

df = pd.read_csv('co2-ppm-daily_csv.csv')
df['date'] = pd.to_datetime(df['date'])
df.columns = ['ds', 'y']

如果你熟悉 scikit-learn (sklearn) 包,你会在 Prophet 中感到非常自在,因为它被设计成以类似的方式运行。Prophet 遵循 sklearn 的范式,首先创建模型类的实例,然后再调用 fitpredict 方法:

model = Prophet()
model.fit(df)

在那个单一的 fit 命令中,Prophet 分析了数据,并独立地识别了季节性和趋势,而无需我们指定任何额外的参数。尽管如此,它还没有做出任何未来的预测。为了做到这一点,我们首先需要创建一个包含未来日期的 DataFrame,然后调用 predict 方法。make_future_dataframe 方法要求我们指定我们打算预测的天数。在这种情况下,我们将选择 10 年,即 365 天乘以 10

future = model.make_future_dataframe(periods=365 * 10)
forecast = model.predict(future)

到目前为止,forecast DataFrame 包含了 Prophet 对未来 10 年 CO2 浓度的预测。我们稍后将探索这个 DataFrame,但首先,让我们使用 Prophet 的 plot 功能来绘制数据。plot 方法是基于 Matplotlib 构建的;它需要一个来自 predict 方法的 DataFrame 输出(在这个例子中是我们的 forecast DataFrame)。

我们使用可选的 xlabelylabel 参数来标记坐标轴,但对于可选的 figsize 参数则保持默认设置。注意,我还使用原始的 Matplotlib 语法添加了一个标题;因为 Prophet 图表是基于 Matplotlib 构建的,所以你可以在这里执行任何对 Matplotlib 图表的操作。另外,不要被带有美元符号的奇怪 ylabel 文本弄混淆;这只是为了告诉 Matplotlib 使用其自己的类似 TeX 的引擎来标记 CO2 下的下标:

fig = model.plot(forecast, xlabel='Date',
                 ylabel=r'CO$_2$ PPM')
plt.title('Daily Carbon Dioxide Levels Measured at Mauna Loa')
plt.show()

图形如下所示:

图 2.3 – Prophet 预测

图 2.3 – Prophet 预测

就这样!在这 12 行代码中,我们已经得到了我们的 10 年预测。

解释预测 DataFrame

现在,让我们通过显示前三个行(我已经将其转置,以便更好地在页面上查看列名)来查看那个 forecast DataFrame,并了解这些值是如何在前面的图表中使用的:

forecast.head(3).T

执行该命令后,你应该会看到以下表格打印出来:

图 2.4 – 预测 DataFrame

图 2.4 – 预测 DataFrame

以下是对 forecast DataFrame 中每一列的描述:

  • 'ds':该行中值相关的日期戳或时间戳

  • 'trend':趋势成分的值

  • 'yhat_lower':最终预测的不确定性区间的下限

  • 'yhat_upper':最终预测的不确定性区间的上限

  • 'trend_lower':趋势成分的不确定性区间的下限

  • 'trend_upper':趋势成分的不确定性区间的上限

  • 'additive_terms':所有加性季节性的总和值

  • 'additive_terms_lower':加性季节性的不确定性区间的下限

  • 'additive_terms_upper':加性季节性的不确定性区间的上限

  • 'weekly':每周季节性成分的值

  • 'weekly_lower':每周成分的不确定性区间的下限

  • 'weekly_upper':围绕每周组件的不确定性区间的上限

  • 'yearly':每年季节性组件的值

  • 'yearly_lower':围绕每年组件的不确定性区间的下限

  • 'yearly_upper':围绕每年组件的不确定性区间的上限

  • 'multiplicative_terms':所有乘法季节性的综合值

  • 'multiplicative_terms_lower':围绕乘法季节性的不确定性区间的下限

  • 'multiplicative_terms_upper':围绕乘法季节性的不确定性区间的上限

  • 'yhat':最终的预测值;由 'trend''multiplicative_terms''additive_terms' 组合而成

如果数据包含每日季节性,那么 'daily''daily_upper''daily_lower' 这几列也会被包含在内,遵循 'weekly''yearly' 列所建立的模式。后面的章节将包括关于加法/乘法季节性和不确定性区间的讨论和示例。

小贴士

yhat 发音为 why hat。它来自统计符号,其中 ŷ 变量代表 y 变量的预测值。一般来说,在真实参数上放置一个帽子或撇号表示它的估计值。

在 *图 2**.3 中,黑色点代表我们拟合的实际记录的 y 值(df['y'] 列中的那些),而实线代表计算的 yhat 值(forecast['yhat'] 列)。请注意,实线延伸到了黑色点的范围之外,我们预测到了未来。在预测区域中,围绕实线的较浅阴影表示不确定性区间,由 forecast['yhat_lower']forecast['yhat_upper'] 限制。

现在,让我们将这个预测分解成其组件。

理解组件图

第一章 时间序列预测的历史与发展 中,Prophet 被介绍为一个加法回归模型。图 1.4 和 1.5 展示了趋势和不同季节性的单个组件曲线是如何相加以形成一个更复杂的曲线。Prophet 算法本质上做的是相反的操作;它将一个复杂的曲线分解为其组成部分。掌握 Prophet 预测的更大控制权的第一步是理解这些组件,以便可以单独操作它们。Prophet 提供了一个 plot_components 方法来可视化这些组件。

继续我们的 Mauna Loa 模型进展,绘制组件就像运行以下命令一样简单:

fig2 = model.plot_components(forecast)
plt.show()

正如你在输出图中可以看到的,Prophet 已经将这个数据集隔离成三个组件:趋势每周季节性每年季节性

图 2.5 – Mauna Loa 组件图

图 2.5 – Mauna Loa 组件图

趋势持续增加,但随着时间的推移似乎有一个变陡的斜率——大气中二氧化碳浓度的加速。趋势线还显示了预测年份中很小的不确定性区间。从这条曲线中,我们了解到 1965 年大气中的二氧化碳浓度约为 320 PPM。到 2015 年增长到约 400 PPM,我们预计到 2030 年将达到约 430 PPM。然而,这些确切数字将因季节性效应的存在而根据一周中的某一天和一年中的某个时间而有所不同。

每周季节性表明,根据一周中的某一天,值将变化约 0.01 PPM——这是一个微不足道的数量,很可能是纯粹由于噪声和随机机会。确实,直觉告诉我们,二氧化碳水平(当测量距离人类活动足够远时,如莫纳罗亚山的高坡上)并不太关心一周中的哪一天,并且不受其影响。

我们将在第五章,“处理季节性”中学习如何指导 Prophet 不要拟合每周季节性,正如在这个案例中那样谨慎。在第十一章,“管理不确定性区间”中,我们将学习如何绘制季节性的不确定性,并确保可以忽略像这样的季节性。

现在,观察年度季节性可以发现,二氧化碳在整个冬季上升,大约在 5 月份达到峰值,而在夏季下降,10 月份达到低谷。根据一年中的时间,二氧化碳的测量值可能比仅根据趋势预测的值高 3 PPM 或低 3 PPM。如果你回顾原始数据中图 2.2所绘制的曲线,你会想起曲线有一个非常明显的周期性,这正是通过这种年度季节性捕捉到的。

就像那个模型那么简单,这通常就是你需要用 Prophet 做出非常准确的预测的所有!我们没有使用比默认参数更多的参数,却取得了非常好的结果。

摘要

希望你在本章开头安装 Prophet 时没有遇到任何问题。使用 Python 的 Anaconda 发行版大大减轻了安装 Stan 依赖项的潜在挑战。安装后,我们查看了在夏威夷莫纳罗亚山 2 英里以上的太平洋大气层中测量的二氧化碳水平。我们构建了第一个 Prophet 模型,并且仅用 12 行代码就能预测未来 10 年的二氧化碳水平。

之后,我们检查了forecast数据框,并看到了 Prophet 输出的丰富结果。最后,我们绘制了预测的组成部分——趋势、年度季节性和每周季节性——以更好地理解数据的行为。

Prophet 远不止这个简单的例子那么简单。在下一章中,我们将深入探讨 Prophet 模型背后的方程,以了解它是如何工作的。

第三章:Prophet 的工作原理

有时,Prophet 可能感觉像魔法,只需极少的用户指令就能创建复杂的预测!但如果你理解 Prophet 背后的方程,你会发现它根本不是魔法,而实际上是一个非常灵活的算法,用于从数据中提取多个同时存在的模式。

对于没有强大统计背景的人来说,所有这些数学可能感觉令人畏惧,但实际上相当容易接近,并且对数学的理解将有助于你开始预测更复杂的数据集。在本章中,我们将一起走过所有相关的方程。如果你开始感到迷茫,不要担心!随着你越来越多地使用 Prophet,一切都会变得清晰。

本章将介绍 Facebook(现在称为 Meta)选择开发自己的预测包而不是依赖现有的许多工具的原因。接下来,你将了解 Facebook 的预测哲学:分析师的知识与计算自动化的结合。最后,你将查看 Prophet 用于构建模型的方程,然后将其拆解以了解每个项在预测中的作用。

在本章中,我们将讨论以下内容:

  • Facebook 构建 Prophet 的动机

  • 循环分析预测

  • Prophet 背后的数学

技术要求

本章中的数据文件和代码示例可在github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition找到。

请参阅本书的前言,了解运行代码示例所需的技术要求。

Facebook 构建 Prophet 的动机

如在介绍 Prophet 的第一章《时间序列预测的历史与发展》中提到的,Facebook 注意到内部对商业预测的需求正在增加。其预测技术扩展性不佳,分析师们感到不堪重负。

Facebook 在文献中搜寻可扩展的预测方法。当时,Facebook 的预测主要使用 Rob Hyndman 的 forecast 包,需要具有预测和大量产品经验的 R 分析师。此外,随着 Python 在新员工中越来越受欢迎,Facebook 发现自己缺乏能够制作高质量预测的分析师。不幸的是,Facebook 考虑的完全自动化的预测工具过于脆弱,往往不够灵活,无法融入有价值的领域知识。

Facebook 需要使专家和非专家都能更容易地做出高质量的预测,这些预测能够跟上需求的变化。因此,Prophet 被设计成一种更直接的方法,以创建合理、准确的预测,这些预测可以通过非专家直观的方式进行定制。Facebook 使用它所说的“分析师在循环预测”来解决这个问题。

分析师在循环预测

在开发 Prophet 时,Facebook 付出了极大的努力,以确保所有参数的默认设置都能为各种业务案例提供出色的结果。然而,总是存在边缘情况、具有挑战性的数据集,或者仅仅是与预期不太匹配的预测。在这些预测不满意的情况下,分析师不会陷入完全自动化的结果。任何分析师,即使是初学者预测者,都可以通过调整各种易于理解的参数来改进预测。Facebook 将这个过程称为分析师在循环预测(见图 3.1)。

图 3.1 – 分析师在循环预测

图 3.1 – 分析师在循环预测

分析师在循环预测是一个迭代的过程。分析师首先使用 Prophet 以默认参数构建模型。Prophet 已经针对速度进行了优化,所以(通常)只需几秒钟,它就能输出一个非常可接受的预测。然后 Prophet 可以评估预测并揭示潜在问题,在将其交还给分析师进行快速视觉检查之前。如果预测符合分析师的预期,他们的工作就完成了!但是,当 Prophet 揭示出较差的性能或分析师的视觉检查提供不满意的结果时,分析师可以直观地调整模型以提高性能并更好地使结果与预期一致。

这个周期可以根据需要重复进行。Prophet 的美妙之处在于,由于预测非常快,一个完整的周期通常可以在不到一分钟内完成。因此,具有广泛领域知识但统计知识有限的分析师能够创建高度定制的预测。分析师可能希望调整的参数包括以下内容:

  • 容量:预测可以渐近接近的上限或下限。容量的一个例子可能是特定时间点的总市场规模。

  • 变化点:这些是预测趋势突然改变方向的时间点。这些可能是由重大产品更新甚至引起显著关注的媒体报道引起的。

  • 节假日和季节性:由于节假日和季节性的影响,任何预测的行为都会有所不同。例如,火鸡的销售在感恩节前一周达到顶峰,而沙滩球在仲夏时节销量最好。理解自己产品的分析师可以轻松地将这种情报输入到他们的模型中。

  • 平滑参数:在视觉检查模型后,分析师可以直观地看出模型是否过度或欠拟合数据。平滑参数可以用来减少模型的噪声,或者指导模型未来可以预期多少季节性变化。

经常会有关于统计预测判断性预测之间差异的讨论。统计预测是一个数学上拟合历史数据的模型,而判断性预测(有时也称为管理预测)是一个通过人类专家利用他们通过时间序列经验所学到的知识来产生预测的过程。判断性预测可以包含比统计预测更多的信息,并且可以更快速地响应变化条件,但它们扩展性不好,需要分析师做大量工作。统计预测更容易自动化,并且可以扩展以满足预测需求,但它在可以整合的领域知识量上有限。

Facebook 的分析师在环范式是这两种不同方法最佳品质的结合:强大自动化,但简单直观易于调整。然而,尽管这种简单性,Prophet 实际上在观察其内部工作时相当复杂。虽然创建准确的预测不需要理解 Prophet 模型背后的数学,但了解 Prophet 正在做什么将只会提高你的预测能力。如果你准备好了,现在让我们看看 Prophet 构建预测所使用的方程组。

Prophet 背后的数学

第一章时间序列预测的历史与发展 中,我们介绍了 Prophet 作为一个加性回归模型。该章节的 图 1.41.5 通过展示如何将代表模型组件的几个不同曲线简单相加以得到最终模型来说明这一点。从数学上讲,这可以用以下方程表示:

图 1 (1)

模型在时间 时间点 的预测 预测值函数 给出。这个函数由四个部分组成,相加(或相乘;参见第五章处理季节性,了解更多信息):

  • 增长成分 是增长成分,或一般趋势,它是非周期的

  • 季节性成分 是季节性成分——即所有周期性成分的总和

  • 假日成分 是假日成分,代表所有一次性特殊事件

  • 错误项 是误差项

这四个组件(实际上,只有前三个组件——误差项只是为了解释模型无法容纳的噪声)的组合是 Prophet 构建预测所需的一切。然而,这个方程式的简单性掩盖了许多复杂性。要真正理解正在发生的事情,我们需要逐个分析这些组件。

线性增长

首先,我们将查看增长项。Prophet 引入了两种增长模式,线性和对数,分析师在设置模型时需要选择其中之一。(分析师如何选择?我们将在第七章控制 增长模式!)这个选择指示 Prophet 使用两个方程之一来表示这个项。我们将从查看线性版本开始:

图片 B19630_03_F09.png (2)

图片 B19630_03_F10.png变量是增长率,线的斜率。熟悉回归的人会认出,一条线的基本方程是图片 B19630_03_F11.png。我们看到方程(2)与这个基本方程有相似之处,如果你把括号里的所有东西收集起来并放在一起。但简单线和 Prophet 的分段线性模型之间的一个关键区别就在其名称中:它是分段的。斜率可以随图片 B19630_03_F12.png函数变化:

图 3.2 – The vertical dashed line is a changepoint in Prophet’s model, where the slope changes

图 3.2 – Prophet 模型中的垂直虚线是一个变化点,斜率在这里发生变化

这就是为什么图片 B19630_03_F13.png,斜率,有所增加:图片 B19630_03_F14.png图片 B19630_03_F15.png变量是一个调整率的向量(即在每个变化点发生的斜率变化),其中图片 B19630_03_F16.png是在时间图片 B19630_03_F17.png发生的斜率变化。图片 B19630_03_F18.png向量标识了每个变化点的位置,并定义如下:

图片 B19630_03_F19.png (3)

简单来说,这意味着线的斜率是恒定的,但允许斜率进行调整。在任何时间图片 B19630_03_F20.png上,斜率等于图片 B19630_03_F21.png基本率加上到该点为止的所有斜率调整。

为了使线条连续,变化点之间的每个区间必须通过偏移参数进行调整,以便区间的端点相连。在方程(2)中,是偏移参数。就像斜率一样,这个偏移参数是一个基础偏移量加上直到时间的所有偏移量。从数学上讲,这是通过将加上一个变化点位置向量,,乘以一个偏移调整向量,来完成的。在这个线性模型中,被设置为

(4)

这就是 Prophet 的线性模型中对数的定义!现在让我们看看如何对对数模型进行修改。

对数增长

与一般直线的方程类似,对数曲线的一般方程由以下方程给出:

(5)

与线性模型中的方程(2)一样,是增长率,而是一个偏移参数。方程(5)需要我们对方程(2)中做出的许多调整,以允许变化点。Prophet 还允许,即承载能力,随时间变化。这个值本质上是一个曲线趋近但永远不会完全达到的渐近线:

图 3.3 – Prophet 的对数模型,承载能力设置为 500

图 3.3 – Prophet 的对数模型,承载能力设置为 500

注意,的承载能力是时间的函数。这意味着渐近线不必是恒定的,而可以是任何任意曲线。在这里,我们演示了一个恒定承载能力切换到线性增加的承载能力:

图 3.4 – 承载能力可能不一定保持恒定

图 3.4 – 承载能力可能不一定保持恒定

如果我们对方程(5)中的进行与方程(2)中相同的调整,并允许成为时间的函数,那么我们得到以下方程:

(6)

这就是 Prophet 的对数增长模型。在线性模型中,,但在对数模型中,必须采取更复杂的形式:

(7)

虽然方程(7)(4)更复杂,但它本质上执行相同的任务:确保在每个变化点,趋势曲线的每个区间的端点相连,并且线条是连续的。

增长项,,是完整 Prophet 模型中最复杂的部分。从这里开始会变得简单!现在,我们将继续了解,即季节性项。

季节性

时间序列数据通常表现出周期性,尤其是在商业数据中,其中经常存在年度周期、周周期和日周期。Prophet 可以在其季节性项中接受无限多个这样的周期性组件,如图(1)所示。

Prophet 使用傅里叶级数来模拟这个术语。傅里叶级数简单地说就是多个正弦曲线的总和。这个最终曲线的形状由每个组成曲线的振幅、相位和周期决定。傅里叶级数可以包含无限多个组件,因此可以拟合几乎任何任意的周期函数,如下所示:

图 3.5 – 四个正弦曲线的总和演示了傅里叶级数

图 3.5 – 四个正弦曲线的总和演示了傅里叶级数

在 Prophet 中,这个求和采用以下形式:

![img/B19630_03_F47.png] (8)

这是![img/B19630_03_F48.png]个不同曲线的总和,被称为傅里叶阶数。在这个公式中,![img/B19630_03_F49.png]是时间序列的常规周期(例如,年度数据的 365.25,周数据的 7,或日数据的 1,当时间序列按天数缩放时)。Prophet 的拟合过程的一部分是计算![img/B19630_03_F50.png]和![img/B19630_03_F51.png]的值,这些仅仅是拟合参数。

保持![img/B19630_03_F52.png]相对较低实际上是在数据上应用一个低通滤波器,并禁止模型过度拟合数据的能力。然而,增加![img/B19630_03_F53.png]有时是可取的,因为它允许拟合变化更快的季节性模式。Prophet 的开发者已经考虑到这一点,并仔细选择了似乎表现相当好的默认值。我们将在第五章处理季节性中更详细地探讨这一点。

假日

为了理解 Prophet 的完整预测模型,我们需要查看的最后一个组件是假日组件。这可能是最容易理解的组件。分析师向 Prophet(或加载默认列表)提供一组假日名称和日期,包括未来的日期,然后 Prophet 估计在之前日期的趋势和季节性预测中的偏差,并将相同的改变应用到未来日期。

这可以用回归者的矩阵来数学表示,![img/B19630_03_F54.png]:

![img/B19630_03_F55.png] (9)

在这个方程中,![img/B19630_03_F56.png]是每个假日的过去和未来日期集合,![img/B19630_03_F57.png]。由于假日![img/B19630_03_F58.png]的预测变化被捕捉在![img/B19630_03_F59.png]参数中。这使得整个假日组件可以表示为![img/B19630_03_F60.png]矩阵和![img/B19630_03_F61.png]向量的乘积:

![img/B19630_03_F62.jpg] (10)

有了这些,Prophet 拥有了构建预测所需的一切!它只需简单地将img/B19630_03_F63.png增长成分、img/B19630_03_F64.png季节性成分和img/B19630_03_F65.png假日成分相加,以提供最终的预测img/B19630_03_F66.png

摘要

本章介绍了 Prophet 的发展历程,从想法的起源到理论公式的形成。然而,本章仅提供了描述 Prophet 工作原理的数学公式的摘要。对于完整细节,请参阅描述 Prophet 的原始论文:Taylor, S. J. 和 Letham, B. 2017. 规模化预测. PeerJ Preprints 5:e3190v2 (doi.org/10.7287/peerj.preprints.3190v2)。

现在你已经了解了 Prophet 的工作原理,本书的剩余部分将用于展示所有可用的参数和附加功能,这些功能可以帮助你更好地控制你的预测。在下一章中,我们将探讨非每日数据,看看需要采取哪些预防措施和调整,从而为我们处理具有不同时间粒度的数据集做好准备。

第二部分:季节性、调整和高级功能

本节将介绍 Prophet 的高级功能。每个可调整的参数都将通过示例和讨论其修改原因和方式来探索。每一章都是在前一章的基础上构建的,以增加预测模型的复杂性和功能。到本节结束时,你将能够构建利用 Prophet 预测工具集全部功能的模型。

本节包括以下章节:

  • 第四章, 处理非每日数据

  • 第五章, 处理季节性

  • 第六章, 假日效应预测

  • 第七章, 控制增长模式

  • 第八章, 影响趋势变化点

  • 第九章, 包含额外的回归因子

  • 第十章, 考虑异常值和特殊事件

  • 第十一章, 管理不确定性区间

第四章:处理非每日数据

当 Prophet 首次发布时,假设所有数据都是按日收集的,每天有一行数据。它现在已经发展到可以处理许多不同粒度的数据,但由于其历史惯例,当在 Prophet 中处理非每日数据时,有一些事情需要谨慎处理。

在本章中,你将查看月度数据(实际上,这适用于任何以大于一天的时间框架测量的数据),并了解如何更改预测频率以避免意外结果。你还将查看小时数据,并观察组件图中额外的组件。最后,你将学习如何处理沿时间轴有规律间隔的数据。

本章将涵盖以下内容:

  • 使用月度数据

  • 使用亚日数据

  • 使用有规律间隔的数据

技术要求

本章示例的数据文件和代码可以在 github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition 找到。

请参阅本书的 前言 了解运行代码示例所需的技术要求。

使用月度数据

第二章 中,使用 Prophet 入门,我们使用 Mauna Loa 数据集构建了我们的第一个 Prophet 模型。数据是按天报告的,这是 Prophet 默认期望的,因此我们不需要更改 Prophet 的任何默认参数。然而,在这个下一个例子中,让我们看看一组新的数据,这些数据不是每天报告的,即 Air Passengers 数据集,看看 Prophet 如何处理这种数据粒度的差异。

这是一个经典的时间序列数据集,涵盖了 1949 年至 1960 年间的数据。它记录了该行业爆炸性增长期间每个月商业航空公司乘客的数量。与 Mauna Loa 数据集相比,Air Passengers 数据集每月只有一个观测值。如果我们尝试预测未来的日期会发生什么?

让我们创建一个模型并绘制预测图,看看会发生什么。我们像 Mauna Loa 示例那样开始,导入必要的库并将我们的数据加载到一个格式正确的 DataFrame 中:

import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet
df = pd.read_csv('AirPassengers.csv')
df['Month'] = pd.to_datetime(df['Month'])
df.columns = ['ds', 'y']

在构建我们的模型之前,让我们先查看前几行,以确保我们的 DataFrame 看起来符合预期:

df.head()

你现在应该看到以下输出:

图 4.1 – 空中旅客 DataFrame

图 4.1 – 空中旅客 DataFrame

数据按月报告,每月有一个测量值。乘客数量按千计,这意味着第一行表示 1949 年 1 月 1 日开始的那个月有 112,000 名商业乘客乘坐飞机。

正如我们在上一章关于莫纳罗亚山的讨论中做的那样,我们接下来将实例化我们的模型并对其进行拟合。使用这个 Air Passengers 数据集,我们将 seasonality_mode 设置为 'multiplicative',但你现在不必担心这一点——我们将在 第五章处理季节性 中讨论它。接下来,我们将数据发送到 fit 方法,然后创建一个 future DataFrame。让我们预测 5 年。最后,我们将使用 predictfuture 结合,然后绘制预测图以查看我们的结果:

model = Prophet(seasonality_mode='multiplicative')
model.fit(df)
future = model.make_future_dataframe(periods=365 * 5)
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

如您所见,我们使用 5 年的每日数据创建了 future DataFrame,只向 Prophet 提供了月度数据。Prophet 能够在每月的第一天适当地应用其季节性计算,因为它有良好的训练数据。然而,对于剩余的天数,它并不完全知道该怎么办,并且以非常混乱和不可预测的方式过度拟合其季节性曲线,如下面的图表所示:

图 4.2 – 以日频率进行的未来预测

图 4.2 – 以日频率进行的未来预测

我们可以通过指示 Prophet 仅在月度上进行预测,以匹配其训练的月度数据来解决这个问题。我们需要在 make_future_dataframe 方法中指定一个频率,这是通过传递 freq 参数来完成的。我们还必须更新 periods,因为尽管我们仍然在预测 5 年后的未来,但我们每年只想有 12 个条目,每个月一个:

model = Prophet(seasonality_mode='multiplicative')
model.fit(df)
future = model.make_future_dataframe(periods=12 * 5,
                                     freq='MS')
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

freq 参数接受 pandas 识别为频率字符串的任何内容。在这种情况下,我们使用了 'MS',意味着 月起始日。以下是该代码块的输出,显示了 Prophet 被指示仅在每月的第一天进行预测后的预测图:

图 4.3 – 以月频率进行的未来预测

图 4.3 – 以月频率进行的未来预测

这好多了,这正是我们可能期望的预测图的样子。通过将 freq 参数传递给 make_future_dataframe 方法,我们避免了要求 Prophet 预测它没有训练知识的日期。默认情况下,频率设置为 'D',即 每日,我们的周期是我们想要预测的天数。每次更改频率到其他设置时,请确保将您的周期设置为相同的比例。

现在,让我们看看使用子日数据时会发生什么变化。为了做到这一点,我将引入一个新的数据集:Divvy。

使用子日数据

在本节中,我们将使用来自伊利诺伊州芝加哥的 Divvy 自行车共享计划 的数据。这些数据包含了从 2014 年初到 2018 年底每小时骑行的自行车次数,并显示出一种普遍的增长趋势以及非常强烈的年度季节性。由于这是按小时的数据,并且夜间骑行次数非常少(有时每小时为零),数据确实显示了在低端的测量密度:

图 4.4 – 每小时 Divvy 骑行次数

图 4.4 – 每小时 Divvy 骑行次数

使用之前提到的Air Passengers数据。作为分析师,你需要使用freq参数并在make_future_dataframe方法中调整周期,然后 Prophet 会完成剩余的工作。如果 Prophet 看到至少两天数据,并且数据之间的间隔小于一天,它将拟合日季节性。

让我们通过进行一个简单的预测来实际看看。在先前的示例中,我们已经导入了必要的库,所以让我们继续加载新数据并将其添加到我们的数据框中:

data = pd.read_csv('divvy_hourly.csv')
df = pd.DataFrame({'ds': pd.to_datetime(data['date']),
                   'y': data['rides']})

接下来,我们继续按照前一个示例中的方法进行,在拟合模型之前实例化我们的模型(再次使用seasonality_mode='multiplicative',并且现在我们暂时不考虑它)。当我们创建future数据框时,我们再次需要设置频率,但这次我们将使用'h',表示每小时。

由于我们的频率是每小时,我们需要调整我们的周期以匹配,所以我们将我们想要的365天预测乘以每天24小时:

model = Prophet(seasonality_mode='multiplicative')
model.fit(df)
future = model.make_future_dataframe(periods=365 * 24,
                                     freq='h')

最后,我们将预测我们的future数据框。随着预测完成,我们将使用第一个plot函数绘制它,然后使用第二个plot函数绘制成分:

forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()
fig2 = model.plot_components(forecast)
plt.show()

上述两个图表中的第一个是这里显示的预测:

图 4.5 – Divvy 预测图

图 4.5 – Divvy 预测图

预测包括相当大的不确定性。要理解原因,我们需要查看如图图 4.6所示的成分图:

图 4.6 – Divvy 成分图

图 4.6 – Divvy 成分图

关于这一系列图表,有几个需要注意的地方。从最上面的图表开始,即趋势图,我们可以看到它仍然表现出年度周期性。为什么这没有在年度季节性图表中捕捉到?遗憾的是,这些数据包含一些非常复杂的季节性,Prophet 无法完全建模。

特别是,季节性本身在一年中也是季节性的。这是季节性中的季节性。日季节性在白天上升,在夜间下降,但增加的量取决于一年中的时间,而 Prophet 并没有设计来捕捉这种季节性。这就是造成预测不确定性的原因。在后面的章节中,我们将学习一些控制这种季节性的技术。

接下来,我们来看一下包含几个直线段的Mauna Loa图。此外,该图从周日到周日,而Mauna Loa图则是从周日到周六。这两个变化都反映了每小时数据的连续性。

当我们只有每日数据,就像我们在 Mauna Loa 所做的那样,每周季节性只需要显示每一天的效果(尽管在底层,它仍然是一个连续模型)。但现在我们有了小时数据,看到连续效果很重要。我们展示了从周日的午夜 12:00:00 到周六晚上 11:59:59,总共 8 天少 1 秒。Mauna Loa 图本质上显示了每天单次时刻的每日效果,正好是 7 天,这就是两个图表之间的差异。

现在看看年度季节性。它相当波动。现在先注意这一点。当我们学习傅里叶级数时,我们将在第五章 处理季节性中讨论它。

最后,是每日季节性图。这是一个新特性,仅在 Prophet 模型处理亚日数据时出现。但在这个数据集中,它却非常揭示。看起来 Divvy 网络中的骑行者在早上 8 点左右骑行很多,可能是上下班途中。下午 5 点后有一个更大的峰值,可能是骑行者回家。最后,午夜后有一个小峰,这一定是那些熬夜的人,他们晚上和朋友出去玩,现在回家睡觉。

我还想提到关于预测的另一件事:模型预测了一些负值,尽管 Divvy 在任何给定小时内都不可能有负数的骑行次数。Prophet 的开发者正在积极解决这个问题,并将在未来的更新中发布解决方案。

在前两节中,你了解到超级日数据和亚日数据并不构成难以克服的难题;我们只需调整未来预测的频率即可。但现在假设 Divvy 每天只收集从早上 8 点到下午 6 点的数据。本章最后要讨论的话题是如何处理具有规律间隔的数据。

使用具有规律间隔的数据

在你的职业生涯中,你可能会遇到具有规律间隔的报表数据集,尤其是在数据由有工作时间、个人时间和睡眠时间的人类收集时。可能根本无法以完美的周期性收集测量数据。

当我们在后面的章节中查看异常值时,你会看到 Prophet 在处理缺失值方面非常稳健。然而,当缺失数据以规律间隔出现时,Prophet 在这些间隔期间将没有任何训练数据来进行估计。在存在数据的时期,季节性会受到约束,但在间隔期间则不受约束,Prophet 的预测可能会显示出比实际数据显示更大的波动。让我们看看实际操作中的情况。

假设 Divvy 的数据每天只收集从早上 8 点到下午 6 点之间的数据。我们可以通过从我们的 DataFrame 中移除这些时间之外的数据来模拟这种情况:

df = df[(df['ds'].dt.hour >= 8) & \
        (df['ds'].dt.hour < 18)]

现在比较以下新 DataFrame 的图与我们在图 4中看到的完整数据集:

图 4.7 – 上午 8 点到下午 6 点每小时的 Divvy 骑行次数

图 4.7 – 上午 8 点到下午 6 点每小时的 Divvy 骑行次数

这个图比图 4.4要稀疏得多,尤其是在低y轴值处。我们失去了所有夜间数据,因为骑行人数下降。现在每天只有 10 个数据点,每个小时从上午 8 点到下午 6 点有一个。现在,让我们像上一节那样构建一个预测模型,用一年每小时频率的future DataFrame,但不采取任何额外的预防措施:

model = Prophet(seasonality_mode='multiplicative')
model.fit(df)
future = model.make_future_dataframe(periods=365 * 24,
                                     freq='h')
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

绘制的预测显示未来期间的每日波动比历史训练数据要宽得多:

图 4.8 – 修复了常规间隔的 Divvy 预测

图 4.8 – 修复了常规间隔的 Divvy 预测

在这里,我们看到未来期间的无约束估计值导致预测的波动很大。这与我们在使用月度数据预测每日预测时观察到的Air Passengers数据中的相同效应。我们可以通过重新绘制并使用 Matplotlib 来约束x轴和y轴的极限,来放大 2018 年 8 月的 3 天,以更清楚地了解正在发生的情况:

fig = model.plot(forecast)
plt.xlim(pd.to_datetime(['2018-08-01', '2018-08-04']))
plt.ylim(-2000, 4000)
plt.show()

与之前的预测图显示了 5 年的预测不同,这个图只显示了 3 天,因此你可以清楚地看到正在发生的情况:

图 4.9 – 3 天内的 Divvy 预测

图 4.9 – 3 天内的 Divvy 预测

在上一节中,当我们查看图 4.6时,我们注意到每日季节性成分显示在上午 8 点之前骑行人数增加,并在上午 8 点达到局部峰值。中午时分有一个低谷,然后在下午 6 点后有一个大峰值。我们在图 4.9中也看到了相同的情况,只不过 Prophet 在上午 8 点之前和下午 6 点之后做出了疯狂的预测,在这些时间段内它没有训练数据。这个区域是不受约束的,只要中午存在数据,它几乎可以遵循任何模式。

解决这个问题的方法很简单,就是修改future DataFrame,排除那些我们训练数据中存在常规间隔的时间。我们甚至不需要实例化一个新的模型或重新拟合;我们只需重复使用我们之前的工作。所以,继续进行,我们创建一个新的future2 DataFrame,移除早于上午 8 点或晚于下午 6 点的时间,然后预测我们的预测并绘制结果:

future2 = future[(future['ds'].dt.hour >= 8) &
                 (future['ds'].dt.hour < 18)]
forecast2 = model.predict(future2)
fig = model.plot(forecast2)
plt.show()

现在我们看到了一个好的预测:

图 4.10 – 修复了常规间隔的 Divvy

图 4.10 – 修复了常规间隔的 Divvy

预测的未来每日波动与我们的历史训练数据的幅度相同。与图 4.8进行对比,其中未来期间显示了更广泛的预测范围。让我们再次绘制 8 月份的相同 3 天,以将输出与图 4.9进行比较:

fig = model.plot(forecast2, figsize=(10, 4))
plt.xlim(pd.to_datetime(['2018-08-01', '2018-08-04']))
plt.ylim(-2000, 4000)
plt.show()

我们看到与前文相同的时间段(上午 8 点到下午 6 点)的曲线,但这次 Prophet 只是用一条直线将它们连接起来。实际上,在我们的forecast数据框中,这些时间段并没有数据;Prophet 只是忽略了它们:

图 4.11 – 修复常规间隔后的 Divvy 3 天预测

图 4.11 – 修复常规间隔后的 Divvy 3 天预测

Prophet 是一个连续时间模型,因此尽管forecast数据框忽略了这些排除的时间,但支撑模型的方程是连续定义的。我们可以通过使用plot_seasonality函数来观察这一点。这个函数包含在 Prophet 的plot包中,因此我们首先需要导入它。它需要两个必需的参数,即模型和一个标识要绘制的季节性的字符串,我们还传递了一个可选参数来更改图形大小:

from prophet.plot import plot_seasonality
plot_seasonality(model, 'daily', figsize=(10, 3))
plt.show()

记住,我们没有创建一个新的模型来解决常规间隔问题;我们只是在第二次处理时从我们的forecast数据框中移除了那些空期。由于这两个例子中我们只使用了一个模型,当然组件是相同的。因此,我们绘制的日季节性与两个版本相同:

图 4.12 – Divvy 日季节性

图 4.12 – Divvy 日季节性

如你所见,上午 8 点到下午 6 点的时间段与图 4.9图 4.11都相匹配,尽管这两个图表在夜间显示了截然不同的结果。由于我们没有训练或未来数据来覆盖这个范围之外的时间,因此可以忽略日季节性图上的这些时间。它们仅仅是创建中午曲线的方程的数学上的附属品。

摘要

在本章中,你从你在第二章“使用 Prophet 入门”中构建的基本Mauna Loa模型中学到了经验教训,并了解了当你的数据周期不是每日时需要做出哪些改变。具体来说,你使用了Air Passengers数据集来模拟月度数据,并在创建future数据框时使用了freq参数来阻止 Prophet 做出每日预测。

然后,你使用了 Divvy 自行车共享计划的每小时数据来设置未来的频率为每小时,这样 Prophet 就会增加其预测时间尺度的粒度。最后,你在 Divvy 数据集中模拟了周期性缺失数据,并学习了一种不同的方法来匹配future数据框的日程安排与训练数据,以防止 Prophet 做出不受约束的预测。

现在你已经知道了如何处理这本书中会遇到的不同数据集,你准备好学习下一个主题了!在下一章中,你将学习所有关于季节性的知识。季节性是 Prophet 力量的核心,这是一个很大的主题,所以请做好准备!

第五章:与季节性一起工作

时间序列与其他数据集区别开来的一个特点是,数据往往具有某种节奏,但这种节奏并非总是存在的。这种节奏可能是年度的,可能由于地球围绕太阳旋转,或者是日度的,如果它根植于地球围绕其轴的旋转。潮汐周期遵循月球围绕地球的旋转。

交通拥堵遵循全天和 5 天工作周的人类活动周期,随后是 2 天的周末;金融活动遵循季度商业周期。你的身体由于心跳、呼吸速率和昼夜节律而遵循周期。在非常小的物理和非常短的时间尺度上,原子的振动是数据周期性的原因。Prophet 将这些周期称为 季节性

在本章中,你将了解 Prophet 默认拟合的所有不同类型的季节性,如何添加新的季节性,以及如何控制它们。特别是,我们将涵盖以下主题:

  • 理解加法与乘法季节性

  • 使用傅里叶阶数控制季节性

  • 添加自定义季节性

  • 添加条件季节性

  • 正则化季节性

技术要求

本章中示例的数据文件和代码可以在github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition找到。

理解加法与乘法季节性

在我们 第二章 Mauna Loa 示例中,使用 Prophet 入门,年度季节性在趋势线上的所有值都是恒定的。我们将季节性曲线预测的值添加到趋势曲线预测的值中,以得出我们的预测。然而,还有一种季节性的替代模式,我们可以将趋势曲线乘以季节性。看看这个图:

图 5.1 – 加法季节性与乘法季节性

图 5.1 – 加法季节性与乘法季节性

上曲线展示了加法季节性——追踪季节性边界的虚线是平行的,因为季节性的幅度没有变化,只有趋势在变化。然而,在下曲线中,这两条虚线并不平行。当趋势低时,季节性引起的扩散低;但当趋势高时,季节性引起的扩散高。这可以用乘法季节性来建模。

让我们通过使用上一章中引入的 Air Passengers 数据集来具体看一下。这些数据记录了从 1949 年到 1960 年每月的商业航空公司乘客数量。我们首先将使用 Prophet 的默认 seasonality_mode 来建模它,即我们在 Mauna Loa 示例中使用的加法模式,然后将其与乘法模式进行对比。

我们将像上一章那样开始,导入必要的库并将数据加载到 DataFrame 中:

import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet
df = pd.read_csv('AirPassengers.csv')
df ['Month'] = pd.to_datetime(df['Month'])
df.columns = ['ds', 'y']

让我们继续构建我们的模型。我将这个命名为 model_a 以表明它是一个加法模型;下一个模型我将命名为 model_m,表示乘法模型:

model_a = Prophet(seasonality_mode='additive',
                  yearly_seasonality=4)
model_a.fit(df)
forecast_a = model_a.predict()
fig_a = model_a.plot(forecast_a)
plt.show()

当我们实例化 Prophet 对象时,我们明确声明 seasonality_mode'additive' 以便清晰。默认情况下,如果没有声明 seasonality_mode,Prophet 将自动选择 'additive'。此外,请注意我们设置了 yearly_seasonality=4。这仅仅设置了曲线的傅里叶阶数,但现在不用担心这个问题 – 我们将在下一节中讨论它。

在创建 Prophet 模型后,我们就像在 Mauna Loa 示例中那样拟合并预测它,然后绘制了预测图。注意,然而,在这个例子中,我们从未创建一个未来的 DataFrame – 如果没有将未来的 DataFrame 发送到 predict 方法,它将仅创建在 fit 方法中接收到的历史数据的预测值,但没有未来的预测值。由于我们只对查看 Prophet 如何处理季节性感兴趣,我们不需要未来的预测。

这里是我们刚刚创建的图:

图 5.2 – 具有加法季节性的航空旅客

图 5.2 – 具有加法季节性的航空旅客

如您所见,在数据早期,在 194919511952 年,Prophet 的预测值(实线)具有比数据(点)指示更极端的季节性波动。在序列后期,在 195819591960 年,Prophet 的预测季节性比数据指示的更不极端。数据的季节性分布正在增加,但我们预测它将是恒定的。这就是在需要乘法季节性时选择加法季节性的错误。让我们再次运行模型,但这次我们将使用乘法季节性:

model_m = Prophet(seasonality_mode='multiplicative',
                  yearly_seasonality=4)
model_m.fit(df)
forecast_m = model_m.predict()
fig_m = model_m.plot(forecast_m)
plt.show()

我们与上一个例子中的操作完全相同,只是这次我们将 seasonality_mode 设置为 'multiplicative'。我们可以在我们生成的图中看到这种变化:

图 5.3 – 具有乘法季节性的航空旅客

图 5.3 – 具有乘法季节性的航空旅客

这是一个更好的拟合!现在,Prophet 与整体趋势的增长匹配了季节性波动的增长。此外,比较 图 5.2图 5.3(围绕实线的浅色区域)之间的误差估计。当 Prophet 尝试将加法季节性拟合到包含乘法季节性的数据序列时,它显示更宽的不确定性区间。Prophet 知道在先前的模型中拟合不佳,并且对其预测不太确定。

这里还有最后一件事我想让你注意。让我通过绘制成分来展示给你看:

fig_a2 = model_a.plot_components(forecast_a)
plt.show()

这张图展示了 model_a 的加法季节性成分:

图 5.4 – 带有加法季节性的组件图

图 5.4 – 带有加法季节性的组件图

现在,让我们绘制model_m的组件图:

fig_m2 = model_m.plot_components(forecast_m)
plt.show()

将以下图表与图 5.4中显示的图表进行比较:

图 5.5 – 带有乘法季节性的组件图

图 5.5 – 带有乘法季节性的组件图

它们看起来几乎完全相同。趋势相同,从1949 年开始略高于100,到1961 年时刚好低于500,在1954 年有一个轻微的转折点,趋势加速。年季节性表现正如我们所预期的那样,夏季乘客数量达到峰值,圣诞节假期和春季假期的局部峰值较小。两个图表之间的区别在于季节性曲线的Y轴。

在加法模型中,Y轴的值是绝对数值。在乘法模型中,它们是百分比。这是因为,在加法季节性模式中,季节性被建模为趋势的附加因素,值只是简单地加到或从它中减去。但在乘法季节性模式中,季节性代表相对于趋势的相对偏差,因此季节性效应的大小将取决于趋势在该点预测的值;季节性效应是趋势的百分比。

小贴士

当你的数据表示随时间变化的某种计数,例如每月的航空公司乘客计数时,你通常会使用乘法季节性来建模。使用加法季节性可能会导致预测出负值(例如,每月负 100 名乘客是不可能的),而乘法季节性只会将值缩小到接近零。

选择加法或乘法季节性可能一开始有点棘手,但如果你只是记住季节性可能是一个绝对因素或相对因素,并观察数据是否具有恒定的分布,你应该不会在模型上遇到任何麻烦。

现在你已经了解了这两种季节性模式之间的区别,让我们将其应用于一个新的数据集,Divvy 自行车共享,并继续在 Prophet 中学习季节性。

在本书的许多例子中,我们将使用芝加哥 Divvy 自行车共享计划的数据来创建示例。在前一章中,我们使用了 Divvy 的小时数据,但在这个部分,我们将使用每日数据。

小贴士

我们在第四章中使用了小时级的 Divvy 数据,处理非每日数据,以展示每日成分图以及如何处理数据中的常规间隔;在本章中,当我们查看条件季节性时,我们还将再次使用小时级数据。但除此之外,在本书的其余部分,我们将使用每日的 Divvy 数据,如下所示。在这些情况下,我们不需要小时级数据的额外粒度,改为每日数据可以将处理时间从分钟减少到秒。此外,每日数据集还包含相关的天气和温度列,这些列在小时级数据集中缺失,我们将在第九章中包括额外的回归因子

下面是每日 Divvy 数据的样子:

图 5.6 – Divvy 每日骑行次数

图 5.6 – Divvy 每日骑行次数

这是一种计数数据,因为它代表了每天的骑行次数,你也可以看到季节性的幅度随着趋势的增长而增长(如果我们从图 5.1中绘制那些虚线,追踪数据的上下界限,线条将会发散)。正如我们刚刚学到的,这些都是乘法季节性的指示,所以让我们确保在实例化我们的模型时设置这一点。在先前的例子中,我们已经导入了必要的 Python 库,因此我们可以从这个例子开始加载数据。

此数据集包含一些额外的天气和温度条件列,我们将使用这些列在第九章中丰富我们的预测,包括额外的回归因子。一旦我们加载了数据,我们就可以看到这些额外的列:

df = pd.read_csv('divvy_daily.csv')
df.head()

在 Jupyter 笔记本或 IPython 实例中运行此命令将显示以下 DataFrame:

图 5.7 – Divvy DataFrame

图 5.7 – Divvy DataFrame

目前,我们只需要daterides列。让我们将这些列加载到我们的 Prophet DataFrame 中,并使用适当的列名。我们将在第九章中处理weathertemperature,包括额外的回归因子

df = df[['date', 'rides']]
df['date'] = pd.to_datetime(df['date'])
df.columns = ['ds', 'y']

如前所述,在调用fit方法之前,我们需要创建 Prophet 类的实例。请注意,我们将seasonality_mode设置为'multiplicative',因为我们注意到在绘制原始数据时,季节性波动随着趋势的增加而增长。在拟合模型后,我们将再次创建一个包含 1 年预测的未来 DataFrame,然后调用predict来创建forecast DataFrame 并将其发送到plot方法:

model = Prophet(seasonality_mode='multiplicative')
model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

运行上述代码后,你应该会发现 Prophet 创建了以下图表:

图 5.8 – Divvy 预测

图 5.8 – Divvy 预测

我们可以看到,预测趋势确实随着实际数据增加,年度季节性也与之匹配。现在,让我们绘制我们的组件图,看看它们揭示了什么:

fig2 = model.plot_components(forecast)
plt.show()

如您在输出图中所见,Prophet 已将此数据集中的三个组件隔离出来:趋势每周季节性和年度季节性:

图 5.9 – Divvy 组件图

图 5.9 – Divvy 组件图

默认情况下,Prophet 会识别一个包含至少 2 年完整数据的'ds'列。'ds'列少于 1 天(在此情况下不适用)。

趋势在前两年内线性增长相对较快,但随后弯曲并略微放缓,剩余两年,预测年份继续遵循这一斜率。我们可以看到,Divvy 网络在此期间的平均使用量从 2014 年的每天约 3500 次增长到 2018 年底的每天约 8500 次。

每周的季节性表明,周末每天的骑行次数大约减少了 30%——也许所有这些骑行者都是上班族——而工作日的骑行次数比趋势高 10-20%。这符合我们的直觉,即工作日和周末可能表现出不同的模式。

现在,观察年度季节性可以发现,夏季的骑行次数比趋势高约 60%,而冬季的骑行次数低 80%。同样,这也符合直觉。那些上班族在天气寒冷和下雨时会开车或乘坐公共交通。

您会注意到这个年度季节性曲线相当波动,就像我们在上一章中注意到的小时 Divvy 数据一样。您可能期望得到一条更平滑的曲线,而不是有这么多拐点的曲线。这是由于我们的年度季节性过于灵活——它有太多的自由度或太多的数学参数控制曲线。在 Prophet 中,控制季节性曲线的参数数量称为傅里叶阶数

使用傅里叶阶数控制季节性

季节性是 Prophet 工作原理的核心,傅里叶级数用于模拟季节性。为了理解傅里叶级数是什么,以及傅里叶阶数如何与之相关,我将使用线性回归的一个类比。

你可能知道,在线性回归中增加多项式方程的阶数总会提高你的拟合优度。例如,简单的线性回归方程是 ,其中 是直线的斜率, 截距。将你的方程阶数增加到,比如说, ,总会提高你的拟合,但风险是过拟合和捕捉噪声。你可以通过任意增加多项式方程的阶数来达到一个 值为 1(完美拟合)。以下图示说明了高阶拟合开始变得相当不切实际和过拟合的情况:

图 5.10 – 高阶多项式线性回归

图 5.10 – 高阶多项式线性回归

线性实线确实正确地得到了数据的上升趋势,但它似乎遗漏了一些细微的细节。二次虚线是一个更好的拟合(实际上,这些数据是从具有随机噪声的二次方程中模拟出来的)。然而,五次十次曲线正在对随机噪声进行过拟合。如果我们从这个分布中采样更多的数据点,它们很可能会使五次十次曲线剧烈变化以适应新的数据,而线性二次曲线只会略有偏移。我们可以说多项式的阶数与曲线可以有多少个弯曲来拟合数据成正比。

傅里叶级数简单地说就是正弦波的求和。通过改变这些单个正弦波的形状——振幅,即波的高度;周期,即从峰值到峰值的距离;以及相位,即波沿长度的哪个位置开始一个周期——我们可以创建一个新的非常复杂的波形。

在线性域中,我们改变多项式的阶数来控制曲线的灵活性,我们改变 β 系数来控制曲线的实际形状。同样,在周期域中,我们改变傅里叶级数中的正弦波数量——这就是傅里叶阶数——来控制最终曲线的灵活性,而我们(或者更准确地说,Prophet 的拟合方程)改变单个波的振幅、周期和相位来控制我们最终曲线的实际形状。你可以在以下图中看到这个求和是如何工作的:

图 5.11 – 四阶傅里叶级数

图 5.11 – 四阶傅里叶级数

实线简单地是四个正弦波各自的和。通过在模型中任意增加傅里叶阶数,我们总能达到任何一组数据的完美拟合。但就像在线性域中一样,这种方法不可避免地会导致过拟合。

记得在图 5**.9中,当我们绘制 Divvy 预测的组成部分时,年度季节性过于波动?这是傅里叶阶数过高造成的。默认情况下,Prophet 使用 10 阶数拟合年度季节性,3 阶数拟合周季节性,如果提供了子日数据,则使用 4 阶数拟合日季节性。通常,这些默认值工作得非常好,不需要调整。然而,在 Divvy 的情况下,我们需要降低年度季节性的傅里叶阶数以更好地拟合数据。让我们看看如何做到这一点。

我们已经从上一个示例中导入了必要的库并将数据加载到我们的df DataFrame 中,因此为了继续,我们需要实例化一个新的 Prophet 对象,并带有修改后的年度季节性。和之前一样,我们将季节性模式设置为乘法,但这次我们将包括yearly_seasonality参数并将其设置为4。这就是我们设置傅里叶阶数的地方。

您可以自己尝试不同的值;我发现4在大多数情况下提供了一个干净的曲线,没有太多的灵活性,这正是我所需要的。同样,如果我们想改变weekly_seasonalitydaily_seasonality的傅里叶阶数,我们也会在这里进行。

在实例化我们的模型后,我们只需将其拟合到数据中即可绘制季节性。在这种情况下不需要预测:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4)
model.fit(df)

我们将在这里使用一个新的函数来绘制仅包含年度成分的图表 – 来自 Prophet 的plot包中的plot_yearly函数。我们首先需要导入它:

from prophet.plot import plot_yearly

注意,还有一个plot_weekly函数,它的工作方式几乎相同。这两个函数都需要第一个参数是模型;这里,我们还将包括可选的图形大小参数,以便它与图 5**.9中包含的我们之前的图表的刻度相匹配:

fig3 = plot_yearly(model, figsize=(10.5, 3.25))
plt.show()

将此输出与图 5**.9中的年度季节性曲线进行比较。

图 5.12 – 使用傅里叶阶数为 4 的 Divvy 年度季节性

图 5.12 – 使用傅里叶阶数为 4 的 Divvy 年度季节性

我们成功消除了之前尝试中的波动,同时仍然保持了季节性的清晰形状。这似乎更加合理!

到目前为止,我们只使用过 Prophet 的默认季节性。然而,有许多周期性数据集的周期并不完美地落在年度、周或日季节性分类中。Prophet 正是为了这个目的支持自定义季节性的。让我们在下一节中查看它们。

添加自定义季节性

到目前为止,我们使用的唯一季节性是 Prophet 的默认值:年度、周和日。但没有任何理由限制我们自己只使用这些季节性。如果您的数据包含一个比 365.25 天的年度周期、7 天的周周期或 1 天的日周期更长或更短的周期,Prophet 可以让您轻松地自己建模这种季节性。

一个非标准季节性的好例子是太阳黑子的 11 年周期。太阳黑子是太阳表面暂时表现出大幅降低温度的区域,因此看起来比周围区域要暗得多。

大约从 1609 年开始,伽利略·伽利莱开始系统地观测太阳黑子,在过去的 400 多年里,这一现象一直被持续记录。太阳黑子代表了任何自然现象中连续记录时间最长的时序数据。通过这些观测,科学家们确定了一个 11 年的准周期循环,在此期间太阳黑子出现的频率会变化。他们称之为“准周期”是因为循环长度似乎在周期之间有所变化——并不是每次都是完美的 11 年。然而,平均周期长度是 11 年,因此我们将使用这个数字来建模。

太阳影响数据分析中心SIDC),位于布鲁塞尔的比利时皇家天文台的部门,在其世界数据中心——太阳黑子指数和长期太阳观测WDC-SILSO)项目中提供了从 1750 年到现在的太阳黑子活动数据集。这个数据集将很好地展示如何向 Prophet 添加新的季节性。我们将首先加载数据:

df = pd.read_csv('sunspots.csv',\
                 usecols=['Date', 'Monthly Mean Total\
                          Sunspot Number'])
df['Date'] = pd.to_datetime(df['Date'])
df.columns = ['ds', 'y']

让我们可视化这些数据,看看它的样子:

图 5.13 – 每月太阳黑子数量

图 5.13 – 每月太阳黑子数量

数据看起来相当嘈杂;似乎有几个异常值,循环并不完全干净。每个周期的峰值变化很大。为了看看 Prophet 如何处理这些数据,我们首先需要实例化我们的模型。这是计数数据,因此我们将选择乘法季节性。

我们还将考虑的一个因素是,太阳如此之大,以至于在我们围绕我们的恒星轨道运行时几乎感觉不到地球引力的微小牵引;因此,太阳根本不会体验到我们所说的年季节性。我们将指示 Prophet 不要尝试拟合年季节性。由于我们提供的是月度数据,Prophet 不会尝试拟合周或日季节性。

在本章前面,我们学习了如何通过传递整数给yearly_seasonality参数来调整年季节性的傅里叶阶数。这是我们用来关闭默认季节性的参数;只需传递一个布尔值即可。我们传递yearly_seasonality=False来指示 Prophet 不要拟合年季节性:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=False)

一旦我们的模型被实例化,我们就可以添加季节性。我们可以使用add_seasonality方法来做这件事。此方法要求我们传递季节性的名称(我们将称之为'11-year cycle'),周期(11 年乘以 365.25 天,因为period是以天为单位的),以及傅里叶阶数(在这种情况下我们将使用5,但请随意实验)。这就是所有内容看起来是什么样的:

model.add_seasonality(name='11-year cycle',
                      period=11 * 365.25,
                      fourier_order=5)

说明周期可能会有些棘手;只需记住,它总是按天数计算。因此,周期长于一天的季节性将具有大于 1 的数字,而周期短于一天的季节性将具有小于 1 的周期。

这个例子中的其余部分与之前的例子完全一样;我们在训练 DataFrame 上拟合,创建一个未来 DataFrame,然后对其进行预测:

model.fit(df)
future = model.make_future_dataframe(periods=240, freq='M')
forecast = model.predict(future)
fig2 = model.plot_components(forecast)
plt.show()

让我们检查组件图,看看我们创建了什么:

图 5.14 – 太阳黑子成分图

图 5.14 – 太阳黑子成分图

该图仅显示趋势和 11 年周期,这正是我们预期的。趋势呈锯齿状;事实上,科学家将1814年左右的低谷称为达尔顿最小值,以纪念英国气象学家约翰·达尔顿。20 世纪 50 年代的峰值被称为现代最大值。但我们对这里的 11 年周期感兴趣。

对于这个不规则周期,Prophet 以天为单位绘制x轴,因此每个刻度比前一个刻度晚约 1.5 年。整个周期确实是 11 年。我们可以看到,低点比高点略平坦,并且比平均值少约 60%的太阳黑子。高点比平均值多约 80%的太阳黑子。

要查看模型当前的所有季节性以及控制该季节性的参数,只需调用模型的seasonalities属性:

model.seasonalities

这会输出一个字典,其中键是季节性的名称,值是参数。在这个例子中,我们只有一个季节性,这是输出字典:

OrderedDict([('11-year cycle',
              {'period': 4017.75,
               'fourier_order': 5,
               'prior_scale': 10.0,
               'mode': 'multiplicative',
               'condition_name': None})])

重要提示

当指定季节性的周期时,它总是按天数指定。因此,10 年的季节性将具有 10(年)x 365.25(每年天数)= 3652.5 天的周期。如果数据按分钟测量,则每小时季节性将是 1(天)/ 24(每天小时数)= 0.04167 天。

注意不要将季节性的周期与make_future_dataframe中使用的周期混淆。季节性的周期总是按天数指定,而make_future_dataframe中的周期由freq参数指定。

如果 Prophet 中不存在数据中的季节性,则添加季节性可能会导致 Prophet 拟合速度非常慢,因为它在找不到模式的情况下会努力寻找模式。这可能会损害你的预测,因为 Prophet 最终会将不存在的季节性拟合到噪声中。然而,你可能经常添加的其他季节性包括如果数据按分钟测量,则添加每小时季节性,如下所示:

model.add_seasonality(name='hourly',
                      # an hour is 0.04167 days
                      period=1 / 24,
                      # experiment with this value
                      fourier_order=5)

季节性业务周期将按以下方式创建:

model.add_seasonality(name='quarterly',
                      # a quarter is 91.3125 days
                      period=365.25 / 4
                      # experiment with this value
                      fourier_order=5)

这就是如何添加自定义季节性的!在本章中,我们将更详细地使用这个add_seasonality方法,从下一节关于依赖于其他因素的季节性开始。

添加条件季节性

假设你在一个大学城的一家公用事业公司工作,并被要求预测下一年度的电力使用情况。电力使用在一定程度上将取决于城镇的人口,作为一个大学城,数千名学生只是临时居民!你如何设置 Prophet 来处理这种情况?条件季节性就是为了这个目的而存在的。

条件季节性是指仅在训练和未来 DataFrames 的部分日期中存在的那些。一个条件季节性必须有一个比其活跃周期更短的周期。所以,例如,如果只有几个月是活跃的,那么有一个只活跃几个月的年度季节性就没有意义。

在大学城预测电力使用需要你设置每日或每周季节性——甚至可能是两者;根据使用模式,在学生返回家乡的夏季月份设置一个每日/每周季节性,以及全年剩余时间的另一个每日/每周季节性。理想情况下,条件季节性在每次活跃时至少应有两个完整的周期。

要了解如何构建条件季节性,我们将回到我们在上一章中探索的小时 Divvy 数据。基于我们在那个例子中观察到的每周季节性,我们知道周末的乘客量比周中显著较低,这表明大多数乘客是在上下班途中。

我们在每日季节性图中看到,骑手在早上 8 点和晚上 6 点有使用高峰,这是在上下班高峰时段。这可能会让你怀疑,整个白天的使用模式在周中和周末可能会有不同的模式。也就是说,我们早上 8 点看到的高峰可能。

在周末,上午 6 点和下午 6 点以及中午的低谷都将消失,整个白天的活动水平更加均匀。为了测试这个假设,让我们使用周末和周中不同的每日季节性来构建一个预测模型。

添加这种条件季节性的基本步骤是在你的训练 DataFrame 中添加新的布尔列(稍后,在未来的 DataFrame 中添加匹配的列),表示该行是周末还是周中。然后,禁用默认的每周季节性,并添加两个新的每周季节性,指定那些新的布尔列作为条件。让我们看看如何做这件事。

我们已经加载了必要的库,所以首先,我们需要使用 Divvy 小时数据创建我们的 Prophet DataFrame:

df = pd.read_csv('divvy_hourly.csv')
df['date'] = pd.to_datetime(df['date'])
df.columns = ['ds', 'y']

现在,这是我们确定季节性条件的地方。让我们创建一个函数,如果给定的日期是周末则输出True,否则输出False。然后,我们将使用pandasapply方法创建一个表示周末的新列,并使用波浪号(~)运算符为另一个表示周中的新列取反。最后,让我们输出 DataFrame 的这一点的第一行,这样我们就可以看到我们得到了什么:

def is_weekend(ds):
    date = pd.to_datetime(ds)
    return (date.dayofweek == 5 or date.dayofweek == 6)
df['weekend'] = df['ds'].apply(is_weekend)
df['weekday'] = ~df['ds'].apply(is_weekend)
df.head()

如果你的函数正确地识别了日期,你应该会看到这个输出:

图 5.15 – Divvy 条件季节性数据框

图 5.15 – Divvy 条件季节性数据框

2014 年 1 月 1 日是星期三,所以输出符合我们的预期。接下来,我们需要实例化我们的模型。利用本章前面学到的知识,我们将季节性模式设置为 multiplicative,因为 Divvy 数据代表计数值。我们还将年季节性和周季节性的傅里叶阶数都设置为 6;我的测试表明在这个数据集上这是一个很好的值。最后,因为我们正在添加条件每日季节性,所以我们将禁用默认的每日季节性:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=6,
                weekly_seasonality=6,
                daily_seasonality=False)

要创建条件季节性,我们将使用我们在模拟太阳黑子周期时学到的相同的 add_seasonality 方法,但在这个案例中,我们将使用可选的 condition_name 参数来指定新的季节性是条件性的。

condition_name 参数必须传递训练数据框中列的名称,并包含布尔值以标识要应用季节性的行 – 我们的 weekendweekday 列。就像我们在太阳黑子示例中所做的那样,我们还需要命名季节性并标识周期和傅里叶阶数:

model.add_seasonality(name='daily_weekend',
                      period=1,
                      fourier_order=3,
                      condition_name='weekend')
model.add_seasonality(name='daily_weekday',
                      period=1,
                      fourier_order=3,
                      condition_name='weekday')

模型设置到此为止!接下来,我们将像以前一样在训练数据上拟合模型并创建 future 数据框,现在我们使用的是每小时数据,所以要注意将频率设置为 hourly。设置条件季节性的最后一步是确定在 future 数据框中条件将应用在哪里。

我们已经创建了 is_weekend 函数并将其应用于我们的训练数据框 df。我们只需在调用 predict 之前重复该过程于 future 数据框上以创建我们的预测:

model.fit(df)
future = model.make_future_dataframe(periods=365 * 24,
                                     freq='h')
future['weekend'] = future['ds'].apply(is_weekend)
future['weekday'] = ~future['ds'].apply(is_weekend)
forecast = model.predict(future)

我们将两个条件季节性命名为 'daily_weekend''daily_weekday',所以让我们导入我们在上一章中发现的 plot_seasonality 函数,并绘制这两个季节性:

from prophet.plot import plot_seasonality
fig3 = plot_seasonality(model, 'daily_weekday',
                        figsize=(10, 3))
plt.show()
fig4 = plot_seasonality(model, 'daily_weekend',
                        figsize=(10, 3))
plt.show()

如果一切运行正确,你应该会有两个新的图表:

图 5.16 – 每日工作日成分图

图 5.16 – 每日工作日成分图

在工作日,趋势与我们使用默认每日季节性时看到的情况非常相似 – 早上 8 点左右有一个峰值,下午 6 点左右另一个峰值,午夜过后有一个小峰。尽管如此,我们假设周末将看到一个非常不同的模式。让我们看看图表来了解:

图 5.17 – 每日周末成分图

图 5.17 – 每日周末成分图

的确,我们看到了差异!正如你的直觉所暗示的,在周末,Divvy 骑行者比在工作日更晚开始,直到中午逐渐增加客流量,然后逐渐减少到午夜。在工作日我们没有看到中午的低谷。

到目前为止,在本章中,您使用了 Air Passengers 数据来学习加性和乘性季节性的区别。后来,您使用了 Divvy 数据来学习如何添加自定义季节性和条件季节性。您还使用了 Divvy 数据来发现傅里叶阶数,并学习了如何控制季节性曲线的灵活性。然而,Prophet 还为您提供了一个控制季节性的杠杆:正则化。

正则化季节性

通常,在用机器学习解决问题时,涉及的数据非常复杂,一个简单的模型往往不足以捕捉到要找到的模式的全部微妙之处。简单的模型往往会欠拟合数据。相比之下,一个更复杂的模型,具有许多参数和很大的灵活性,可能会倾向于过拟合数据。使用更简单的模型并不总是容易,或者可能。在这些情况下,正则化是一种很好的技术,可以用来控制过拟合。

Prophet 是一个非常强大的预测工具,如果不加注意,有时很容易过拟合数据。这就是为什么理解 Prophet 的正则化参数非常有用的原因。

小贴士

如果一个模型没有完全捕捉到输入特征和输出特征之间的真实关系,那么它就被说成是欠拟合。在训练数据和任何未见过的测试数据上的性能都较低。

如果一个模型超出了捕捉真实关系的范围,开始捕捉数据噪声中的随机趋势,那么它就被说成是过拟合。在训练数据上的性能可能非常高,但在未见过的测试数据上的性能可能很低。

一个拟合良好的模型将在训练数据和测试数据上表现同样好。

正则化是一种通过迫使模型变得不那么灵活来控制过拟合的技术。例如,在图 5.18中,我模拟了一组带有随机噪声的点(我使用的真实关系是![img/019630_05_F07.png])并使用 8 次多项式回归拟合了两条线(实际上,你很少会选择如此高的阶数作为回归模型;我在这里只是为了夸张这个观点)。一条线完全没有正则化,而另一条线是:

图 5.18 – 正则化效果

图 5.18 – 正则化效果

如您在图中所示,未正则化的线是过拟合的,它在尝试拟合噪声的同时围绕着真实关系摇摆。相比之下,通过正则化,线的灵活性受到限制,并被迫绘制出一条更加平滑的曲线。由于真实曲线基本上是![img/019630_05_F08.png],很明显,正则化的线,尽管仍然不完美,但在近似关系方面做得更好,并且在新数据上表现会更好。

完整的 Prophet 包有多个可调整的正则化参数。对于季节性,该参数称为先验尺度。

在统计学中,你可能有一个不确定的量,你打算找到它的值。这个量的 先验概率分布,通常简称为先验,是在学习额外信息之前你期望的值的概率分布。

例如,假设我让你猜测一个特定男性的人类身高。在你的脑海中,你想象所有可能的男性身高。这个身高范围是先验概率分布。接下来,我告诉你这个男性是 NBA 篮球运动员。你知道篮球运动员通常比普通男性高得多,所以你更新这个分布,使其更偏向于高身高,因为我所提供的额外信息更好地帮助你猜测。

先验是你的起点,在你收到额外信息之前你相信是正确的。让我们学习如何将这个想法应用到 Prophet 的季节性中。

全局季节性正则化

应用季节性正则化的第一种方式是全局性的,这会影响模型中所有季节性的同等程度。seasonality_prior_scale 是你的 Prophet 模型实例的一个属性,并在你实例化模型时设置。如果你没有设置它,默认值将是 10。减少这个数值将应用更多的正则化,这将控制你的模型季节性。让我们看看实际效果。

在这个例子中,我们将使用 Divvy 每日数据,因此我们需要首先将其加载到我们的 Prophet DataFrame 中,因为必要的库应该已经从之前的例子中加载:

df = pd.read_csv('divvy_daily.csv')
df = df[['date', 'rides']]
df['date'] = pd.to_datetime(df['date'])
df.columns = ['ds', 'y']

现在,我们需要实例化我们的模型,将季节性模式设置为 multiplicative。在了解傅里叶阶数时,你使用默认的 seasonality_prior_scale10 对这个数据集进行了预测。所以,这次我们将先验尺度设置为 0.01。我们还发现,使用傅里叶阶数 4 更好地模拟了年度季节性,因此我们也将它设置为 4。你可以参考 图 5.8图 5.9 来查看未正则化的模型以进行比较:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                seasonality_prior_scale=.01)

设置正则化后,剩下的工作就是完成模型,就像我们之前做的那样:

model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()
fig2 = model.plot_components(forecast)
plt.show()

首先,我们将查看预测,然后是组件:

图 5.19 – 正则化预测

图 5.19 – 正则化预测

图 5.19图 5.8 进行比较显示,我们的预测中的季节性波动确实已经减弱。年度季节性和周季节性都显示出更少的波动。不过,两个模型之间的不确定性区间大致相同,因为现在数据中的方差现在由 Prophet 模型的噪声项而不是季节性项来处理。

现在,让我们看看组件图:

图 5.20 – 正则化组件图

图 5.20 – 正则化组件图

将此图与图 5.9进行比较,我们可以看到趋势非常相似。我们只限制了季节性,而没有限制趋势。趋势确实有所变化(峰值略高),因为 Prophet 试图通过趋势捕捉一些季节性变化,但形状几乎相同。每周和年度季节性看起来相同,但它们的y轴显示,幅度已经减少到其正则化水平的三分之一到四分之一。这就是季节性正则化的效果:它减少了曲线值的幅度。

为了说明不同季节性先验尺度的效果,让我们比较使用不同先验尺度建模的此数据集的年度和每周季节性曲线。首先,这是年度季节性图:

图 5.21 – 不同先验尺度的年度季节性

图 5.21 – 不同先验尺度的年度季节性

这是每周季节性图:

图 5.22 – 不同先验尺度的每周季节性

图 5.22 – 不同先验尺度的每周季节性

两个图中的实线是默认的10倍尺度;虚线和虚点线显示正则化量的增加。而修改傅里叶阶数有助于通过减少允许曲线弯曲的数量来控制季节性曲线,而修改季节性先验尺度有助于通过减少它可以实现的变化量来控制季节性。

在本节中,您学习了如何同时正则化所有季节性。接下来,您将学习如何单独正则化季节性。

本地季节性正则化

假设您对默认正则化设置下的年度季节性曲线感到满意,但您的每周曲线过于极端且过拟合。在这种情况下,您可以使用add_seasonality方法创建一个新的具有自定义先验尺度的每周季节性。

让我们继续并实例化一个新的模型,再次使用乘法季节性和应用于年度季节性的傅里叶阶数4。不过,这次我们将添加一个新的每周季节性,所以让我们在实例化时将其设置为False

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                weekly_seasonality=False)

如您在添加自定义季节性部分所学,我们现在将添加一个周期为7天的季节性,并将其命名为'weekly'。我们对默认的每周傅里叶阶数4感到满意,所以我们将再次使用它,但我们需要比默认值更多的正则化,因此我们将使用prior_scale参数将其设置为0.01

model.add_seasonality(name='weekly',
                      period=7,
                      fourier_order=4,
                      prior_scale=0.01)

现在,正如我希望这已经成为您的第二天性,我们将拟合模型并在未来的 DataFrame 上进行预测。这次我们只绘制组件:

model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)
fig2 = model.plot_components(forecast)
plt.show()

您应该看到这个图,它与图 5.20几乎相同:

图 5.23 – 每周正则化成分图

图 5.23 – 每周正则化成分图

你现在会看到未正则化的年季节性的幅度与图 5**.9的幅度相匹配,但正则化的周季节性减少了大约一半,正如预期的那样。你可以通过重复调用add_seasonality来为所有的季节性应用不同的正则化强度。这些先验尺度的合理值从大约 10 到大约 0.01 不等。

摘要

季节性确实是 Prophet 的核心。这一章涵盖了大量的内容;你在这里学到的基础知识将在本书剩余的章节中得到应用。实际上,你几乎在 Prophet 中构建的任何模型都将考虑季节性,而许多即将到来的章节涵盖了可能或可能不适用于你特定问题的特殊情况。

你通过学习加性和乘性季节性的区别,以及如何识别你的数据集特征是其中之一开始了这一章。然后我们简要讨论了傅里叶级数,并展示了如何通过允许更多的或更少的自由度在其路径上弯曲来控制季节性的傅里叶阶数,从而构建一个非常复杂的周期曲线。使用这些想法,你学习了如何通过允许更多的或更少的自由度在其路径上弯曲来控制季节性的形状。

接下来,你学习了太阳黑子 11 年周期的建模,并学会了如何添加自定义季节性。当你学习如何使用条件季节性来建模 Divvy 网络中骑手在不同工作日和周末的行为时,这些自定义季节性再次被使用。最后,我们探讨了正则化技术,包括全局正则化,即应用于所有季节性,以及局部正则化,再次使用自定义季节性课程来仅对周季节性进行正则化。

在下一章中,你将学习 Prophet 包中所有关于节假日的知识,其中还包括关于正则化的更多细节,正则化是应用于 Prophet 的。

第六章:预测假期效应

由于 Prophet 是为了处理商业预测案例而设计的,因此包含假期效应是很重要的,这在商业活动中自然起着重要作用。就像共享单车通勤者在夏天比冬天骑得更多,或者在星期二比星期日骑得更多一样,合理地假设他们在感恩节等节日骑行的次数会少于预期。

幸运的是,Prophet 包括对在预测中包含假期效应的强大支持。此外,Prophet 用于包含假期效应的技术可以用来添加任何类似假期的活动,例如我们在本章中将要建模的食物节。

与你在上一章中学到的季节性效应类似,Prophet 包含默认的假期,你可以将其应用于你的模型,以及你可以自己创建的自定义假期。本章将涵盖这两种情况。此外,你还将学习如何使用你用于季节性的技术来控制假期效应的强度:正则化。

在本章中,你将学习如何进行以下操作:

  • 添加默认国家假期

  • 添加默认州或省假期

  • 创建自定义假期

  • 创建多日假期

  • 正则化假期

技术要求

本章中示例的数据文件和代码可以在github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition找到。

添加默认国家假期

Prophet 使用 Python 的 holidays 包根据国家填充默认的假期列表,可选地还可以根据州或省。为了指定为哪个地区构建假期列表,Prophet 需要该国家的名称或 ISO 代码。所有可用的国家及其 ISO 代码的完整列表,以及可以包含的任何州或省,可以在包的 README 文件中查看:github.com/dr-prodigy/python-holidays#available-countries

要添加默认假期,Prophet 包含一个 add_country_holidays 方法,它只需提供该国家的 ISO 代码。让我们通过再次使用 Divvy 数据集的例子来演示,首先添加美国的假期,然后包括一些特定于伊利诺伊州的额外假期,因为 Divvy 位于芝加哥。

我们将像我们在本书中学习使用其他模型一样开始,通过导入必要的库,加载数据,并实例化我们的模型。正如你在第五章中学习的那样,处理季节性,我们将设置季节性模式为乘法,并将年度季节性设置为傅里叶阶数为 4

import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet
df = pd.read_csv('divvy_daily.csv')
df = df[['date', 'rides']]
df['date'] = pd.to_datetime(df['date'])
df.columns = ['ds', 'y']
model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4)

下一条线就是填充模型所需的,列出美国的假日列表:

model.add_country_holidays(country_name='US')

现在,为了完成模型,我们只需要像往常一样在训练 DataFrame 上调用 fit,创建我们的未来 DataFrame,然后对其调用 predict。我们将绘制预测和组成部分以查看我们的结果:

model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

输出的预测图看起来与 图 5.7 非常相似,来自 第五章处理季节性

图 6.1 – 包含美国假日的 Divvy 预测

图 6.1 – 包含美国假日的 Divvy 预测

然而,细心的读者可能会注意到年中和大年底附近有一些向下的峰值。为了辨别这些是什么,我们将查看以下 components 图:

fig2 = model.plot_components(forecast)
plt.show()

在这些命令的输出中,趋势和每周和年度季节性再次包括在内,看起来几乎相同。然而,这里显示了一个新的图:假日,如这里所示(注意,前面的代码生成了一个完整的 components 图;下面的图是那个图像的裁剪):

图 6.2 – Divvy 美国假日组成部分

图 6.2 – Divvy 美国假日组成部分

这显示了 Divvy 与趋势偏离的峰值,每个峰值对应一个假日。除了每年最后一个季度发生的一个假日外,每个假日都显示出使用量的减少。让我们来调查一下。

我们可以使用此命令查看我们模型中包含的假日:

model.train_holiday_names

这将输出一个包含索引和模型中包含的假日名称的 Python 对象:

图 6.3 – 美国假日

图 6.3 – 美国假日

这些假日都包含在第二章 入门 Prophet 中提到的 forecast DataFrame 中。对于每个假日,都添加了三个新列,用于预测该假日的影响,以及不确定性的上下限,例如,"New Year's Day""New Year's Day_lower""New Year's Day_upper"。使用这些新列,我们可以通过打印 forecast DataFrame 中每个假日的第一个非零值来精确地看到每个假日对我们预测的影响。

为了做到这一点,让我们创建一个名为 first_non_zero 的快速函数。该函数接受一个 forecast DataFrame 和一个假日的名称;它返回该假日第一个不等于零的值。然后,我们将使用 Python 列推导式遍历每个假日名称并调用 first_non_zero 函数:

def first_non_zero(fcst, holiday):
    return fcst[fcst[holiday] != 0][holiday].values[0]
pd.DataFrame({'holiday': model.train_holiday_names,
              'effect': [first_non_zero(forecast, holiday)
                         for holiday in \
                         model.train_holiday_names]})

因为 forecast DataFrame 的每一行都是一个日期,所以每个假日列中的大多数值都将为零,因为假日不会影响这些日期。在假日发生的日期,值将是正的,表示比预期更多的乘客,或者负的,表示乘客更少。

预测模型将每个节假日视为每年都有相同的影响,因此这个值将逐年保持不变。因为我们在这个情况下设置了seasonality_mode='multiplicative',这些影响被计算为趋势的百分比偏差(只是为了说明:全局的seasonality_mode也会影响节假日)。下表显示了这些影响:

图 6.4 – 节假日影响值

图 6.4 – 节假日影响值

现在,我们可以清楚地看到哥伦布日为 Divvy 的客流量增加了 5%。所有其他节假日都有负面影响,其中劳动节的影响最强,比趋势预测的客流量少了 69%。

你刚刚学习到的这个过程是 Prophet 的基本节假日功能;它类似于在向 Prophet 提供没有额外参数时产生的默认季节性。它在许多情况下都工作得很好,并且通常是模型所需的所有内容。但是,正如分析师可以更精细地控制季节性影响一样,分析师可以使用几种技术来控制节假日,而不仅仅是默认设置。在下一节中,我们将介绍添加特定于州或省的节假日的流程。

添加默认州/省节假日

添加伊利诺伊州特有的节假日并不那么简单,因为add_country_holidays方法只接受一个国家参数,但不接受州或省。要添加州或省级别的节假日,我们需要使用一个新的 Prophet 函数,make_holidays_df。让我们在这里导入它:

from prophet.make_holidays import make_holidays_df

这个函数接受一个年份列表作为输入,用于填充节假日,以及国家和州或省的参数。请注意,您必须在您的训练数据框中使用所有年份,以及您打算预测的所有年份。这就是为什么在下面的代码中,我们构建一个年份列表来包含训练数据框中的所有唯一年份。然后,因为我们的make_future_dataframe命令将为预测添加一年,我们需要扩展这个年份列表以包含一个额外的年份:

year_list = df['ds'].dt.year.unique().tolist()
# Identify the final year, as an integer, and increase it by 1
year_list.append(year_list[-1] + 1)
holidays = make_holidays_df(year_list=year_list,
                            country='US',
                            state='IL')

在继续之前,让我们快速查看一下这个holidays数据框的格式,通过打印前五行:

holidays.head()

如您从以下输出中可以看到,holidays数据框包含两列,dsholiday,分别表示节假日的日期和名称:

图 6.5 – 伊利诺伊州节假日

图 6.5 – 伊利诺伊州节假日

要将这些节假日加载到我们的 Prophet 模型中,我们只需在实例化模型时传递holidays数据框,然后像以前一样继续:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                holidays=holidays)
model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)

如果您继续调用model.train_holiday_names,您将看到四个特定于伊利诺伊州的额外节假日,这些节假日不是官方的美国节假日:林肯诞辰、卡西米尔·普拉斯基日、选举日和林肯诞辰(观察日)。

创建自定义节假日

美国的默认假日包括感恩节和圣诞节,因为它们是官方假日。然而,黑色星期五和圣诞夜可能会产生与预期趋势不同的客流量。因此,我们自然决定将它们包含在我们的预测中。

在这个例子中,我们将以与之前创建伊利诺伊州假日数据框相似的方式创建一个包含默认美国假日的数据框,然后将其添加到其中。要创建自定义假日,你只需创建一个包含两列的数据框:holidayds。像之前做的那样,它必须包括过去(至少,远至你的训练数据)和未来我们打算预测的假日所有发生情况。

在这个例子中,我们将首先创建一个包含默认美国假日的 holidays 数据框,并使用之前示例中的 year_list

holidays = make_holidays_df(year_list=year_list,
                            country='US')

我们将用我们自定义的假日来丰富这个默认假日的列表,因此现在我们将创建两个包含指定列(holidayds)的数据框,一个用于 黑色星期五,另一个用于 平安夜

black_friday = pd.DataFrame({'holiday': 'Black Friday',
                             'ds': pd.to_datetime(
                                 ['2014-11-28',
                                  '2015-11-27',
                                  '2016-11-25',
                                  '2017-11-24',
                                  '2018-11-23'])})
christmas_eve = pd.DataFrame({'holiday': 'Christmas Eve',
                              'ds': pd.to_datetime(
                                  ['2014-12-24',
                                   '2015-12-24',
                                   '2016-12-24',
                                   '2017-12-24',
                                   '2018-12-24'])})

当然,你可以创建一个只包含两个假日作为单独行的数据框,但为了清晰起见,我已经将它们分开。

最后,我们只需要将这些三个 holiday 数据框连接成一个:

holidays = pd.concat([holidays, black_friday,
                      christmas_eve]).sort_values('ds')\
                      .reset_index(drop=True)

并非绝对必要对值进行排序或重置索引,就像之前代码中做的那样,但如果你想检查它,这样做会使数据框在视觉上更清晰。

在我们完成 holidays 数据框后,我们现在将其传递给 Prophet,就像之前处理伊利诺伊州假日时一样,并继续调用 fitpredict

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                holidays=holidays)
model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)

现在,如果你检查 forecast 数据框或你的 components 图表,你确实会看到每年增加两个额外的假日,一个是黑色星期五,另一个是感恩节。

以这种方式创建假日允许对个别假日有更精细的控制。接下来,我们将看看你可以用来调整假日的其他一些参数。

创建多日假日

有时,一个假日或其他特殊事件会跨越几天。幸运的是,Prophet 通过 window 参数提供了处理这些场景的功能。我们之前构建的 holidays 数据框,用于填充之前的示例中的假日,可以包括可选的列 'lower_window''upper_window'。这些列指定了主假日之前或之后额外的天数,Prophet 将对其进行建模。

例如,在上一个例子中,我们将圣诞节和圣诞前夕建模为两个不同的节日。另一种方法就是只建模圣诞节,但包括一个'lower_window'参数为1,告诉 Prophet 将圣诞节前一天作为节日的一部分。当然,这假设圣诞前夕总是在圣诞节前一天。然而,如果圣诞前夕是一个浮动节日,并不总是立即在圣诞节之前,那么这个window方法就不会被使用。

每年七月,芝加哥举办一个为期 5 天的节日,称为芝加哥美食节。这是世界上最大的美食节,也是芝加哥任何类型最大的节日。每年有超过一百万的人参加,尝试来自近 100 个不同摊位的食物,或者每天参加受欢迎的音乐会。由于如此多的人群在城市中流动,如果它对 Divvy 的乘客量没有任何影响,那就令人惊讶了。在这个例子中,我们将芝加哥美食节建模为 5 天的节日,看看这对 Divvy 的预测有什么影响。

如前所述,我们首先创建包含默认美国假期的holidays DataFrame。接下来,我们创建一个taste_of_chicago DataFrame,将日期设置为历史数据和预测期间每年活动的第一天。然而,与前一个例子不同的是,我们还包含了'lower_window''upper_window'列,将下限设置为0(因此我们不包含活动第一天之前的日期),上限设置为4(这包括活动第一天之后的四天,总共五天)。然后,我们按照以下方式将 DataFrame 连接在一起:

holidays = make_holidays_df(year_list=year_list,
                            country='US')
taste_of_chicago = \
pd.DataFrame({'holiday':'Taste of Chicago',
              'ds': pd.to_datetime(['2014-07-09',
                                    '2015-07-08',
                                    '2016-07-06',
                                    '2017-07-05',
                                    '2018-07-11']),
              'lower_window': 0,
              'upper_window': 4})
holidays = pd.concat([holidays, taste_of_chicago])\
                     .sort_values('ds')\
                     .reset_index(drop=True)

现在,让我们看一下 DataFrame 的前 10 行:

holidays.head(10)

在输出中,我们可以看到额外的列,以及Taste of Chicago假期的包含:

图 6.6 – 带窗口的节日

图 6.6 – 带窗口的节日

小贴士

如果你对前面表格中的NaN值不熟悉,它代表非数字。在这种情况下,它只是一个占位符,没有任何影响。

现在,我们将继续拟合我们的模型:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                holidays=holidays)
model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)

要查看Taste of Chicago对 Divvy 乘客量的影响,让我们看一下带有此print语句的forecast DataFrame:

print(forecast[forecast['ds'].isin(['2018-07-11',
                                    '2018-07-12',
                                    '2018-07-13',
                                    '2018-07-14',
                                    '2018-07-15']
                                  )][['ds',
                                      'Taste of Chicago']])

输出是forecast DataFrame 的内容,但仅限于 2018 年活动的五天,以及日期和Taste of Chicago对乘客量的影响列:

图 6.7 – 芝加哥美食节对乘客量的影响

图 6.7 – 芝加哥美食节对乘客量的影响

我们可以看到,活动的第一天比没有活动时预期的客流量少了 3.6%,第二天多了 1.9%,第三天多了 6.8%。最后两天都有大约 2%的客流量增加。这些数字的幅度可能不像您预期的那么大,尤其是 7 月 4 日导致客流量减少了 55%。而且,考虑到其中一个是负数,另一个是正数,这个结果可能不是一个有意义的信号,而是仅仅由于随机噪声。在第十一章管理不确定性区间中,您将学习如何验证这个结果是否有意义。

然而,我们可以使用 Prophet 的plot包中的plot_forecast_component函数仅可视化这个假日影响。我们首先需要导入它:

from prophet.plot import plot_forecast_component

该函数需要第一个参数是模型,第二个参数是forecast DataFrame,第三个参数是一个字符串,用于命名要绘制的组件;这里,我们将使用'Taste of Chicago'

fig3 = plot_forecast_component(model,
                               forecast,
                               'Taste of Chicago',
                               figsize=(10.5, 3.25))
plt.show()

在输出中,我们可以可视化图 6.7中显示的表格(这次,我们显示所有年份):

图 6.8 – 芝加哥风味假日影响

图 6.8 – 芝加哥风味假日影响

活动的第一天显示客流量减少,接下来的四天客流量增加。现在我们已经了解了您可以将假日添加到预测中的各种方法,让我们再看看一个用于控制假日影响的工具:正则化。

正则化假日

将模型的灵活性约束以帮助它更好地泛化到新数据的过程被称为正则化第五章处理季节性,在 Prophet 中详细讨论了正则化季节性影响。在 Prophet 下,正则化假日和季节性影响的数学过程是相同的,因此我们可以使用季节性章节中的相同概念并将其应用于假日。

通常,如果您作为分析师发现您的假日对模型的影响比您预期的更大,也就是说,如果它们的绝对幅度高于您认为准确或必要来建模您的问题,那么您将想要考虑正则化。正则化将简单地压缩假日影响的幅度,并禁止它们产生比其他情况下更大的影响。Prophet 包含一个holidays_prior_scale参数来控制这一点。

这与我们在上一章中用于正则化季节性的seasonality_prior_scale参数背后的理论是相同的。正如季节性可以全局或局部正则化一样,假日也可以。让我们看看如何做到这一点。

全球假日正则化

Prophet 实际上有一个默认的先验概率分布,用于猜测节假日可能产生的影响,并使用这个分布来尝试找到最佳拟合数据的价值。然而,如果这个先验猜测范围与现实相差甚远,Prophet 将难以找到最佳值。你可以通过提供有关预期哪些值的信息来极大地帮助它,这样它就可以更新其先验分布以更好地指导猜测。修改节假日先验尺度就是向 Prophet 提供这种额外信息的方式。

对于holidays_prior_scale的值,不幸的是,它们并没有太多直观的意义。它们与 lasso 回归中的正则化参数类似,因为它们控制了收缩量。然而,你只需要记住,较小的值意味着更少的灵活性——节假日效应将通过更多的正则化而减弱。默认情况下,Prophet 将此值设置为 10。合理的值范围从 10 降至大约 0.001。

然而,每个数据集都是不同的,所以你会发现实验将非常有帮助,但就像季节性的先验尺度一样,你会发现大多数情况下,节假日先验尺度在 10 到 0.01 之间将工作得很好。为了看到这个变量的效果,让我们使用默认值 10 和一个更小的值 0.05 构建一个模型。

让我们也使用我们在绘制芝加哥美食节事件时了解到的plot_forecast_component函数,但这次,将'节假日'成分传递给它,以绘制所有综合的节假日效应。首先,我们使用默认的先验尺度值构建模型(在这里,我们明确将其设置为10以增强清晰度),然后仅绘制节假日成分以查看节假日效应:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                holidays_prior_scale=10)
model.add_country_holidays(country_name='US')
model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)
fig = plot_forecast_component(model, forecast, 'holidays')
plt.show()

那段代码的输出将仅仅是节假日成分:

图 6.9 – 无正则化的节假日成分

图 6.9 – 无正则化的节假日成分

在没有正则化的情况下,感恩节(图 6.9 中最长的条形,我们在本章前面发现其对所有节日的效应最强)将乘客量减少了大约 65%。

现在,让我们构建另一个模型,除了具有强正则化之外,其他方面都相同,并绘制节假日成分:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                holidays_prior_scale=0.05)
model.add_country_holidays(country_name='US')
model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)
fig = plot_forecast_component(model, forecast, 'holidays')
plt.show()

再次,我们使用了plot_forecast_component函数来仅显示节假日成分:

图 6.10 – 强正则化的节假日成分

图 6.10 – 强正则化的节假日成分

当进行规范化时,图表看起来与未规范化的假日图表相似,但有一些不同。首先,我们看到尺度变化很大。当规范化时,最强的假日效应是乘客量减少了 11.5%,而未规范化的模型中减少了 65%。要注意的第二件事是假日并没有按相同的比例减少:现在,圣诞节的效果最强,而不是感恩节。这不是错误,只是这么多变量相互作用时规范化的效果。

选择先验尺度的值可能更像是一门艺术而非科学。如果你认为假日的效应比你直觉所暗示的更强或更弱,你可以使用你的领域知识来调整这个值。如果有疑问,进行实验并看看什么效果最好。最严谨的方法是使用网格搜索和交叉验证,这个话题我们将在本书的结尾部分进行介绍。

使用我们之前使用的holidays_prior_scale参数调整所有假日是全球性的;每个假日都是按相同的方式规范化的。为了有更多的控制,Prophet 提供了通过自定义假日接口调整每个个别假日先验尺度的功能。在下一个例子中,我们将看到如何做到这一点。

个人假日规范化

当添加一个新的假日时,我们创建了一个包含两个必需列dsholiday以及两个可选列lower_windowupper_window的数据框。我们还可以在这个数据框中包含一个最终的可选列,即prior_scale。如果任何假日在这个列中没有值(或者如果这个列在数据框中甚至不存在),那么假日将回退到我们在上一个例子中看到的全局holidays_prior_scale值。在下面的例子中,我们将添加这个列并单独修改一些假日的先验尺度。

正如我们之前所做的那样,我们将构建默认的假日列表并添加一些额外的假日。这次,我们将添加Black Friday圣诞节前夕,先验尺度为1,以及芝加哥美食节5 天的活动,先验尺度为0.1。所有其他假日将保持默认的先验尺度10。首先,我们将使用之前创建的相同的year_list来创建我们的holidays数据框:

holidays = make_holidays_df(year_list=year_list,
                            country='US')

这是 Prophet 为美国提供的默认假日列表;我们希望用我们额外的三个假日来丰富这个列表,所以现在我们将为每个假日创建一个数据框。请注意,我们为每个假日指定了'prior_scale'

black_friday = pd.DataFrame({'holiday': 'Black Friday',
                             'ds': pd.to_datetime(
                                 ['2014-11-28',
                                  '2015-11-27',
                                  '2016-11-25',
                                  '2017-11-24',
                                  '2018-11-23']),
                             'prior_scale': 1})
christmas_eve = pd.DataFrame({'holiday': 'Christmas Eve',
                              'ds': pd.to_datetime(
                                  ['2014-12-24',
                                   '2015-12-24',
                                   '2016-12-24',
                                   '2017-12-24',
                                   '2018-12-24']),
                              'prior_scale': 1})
taste_of_chicago = \
pd.DataFrame({'holiday': 'Taste of Chicago',
              'ds': pd.to_datetime(['2014-07-09',
                                    '2015-07-08',
                                    '2016-07-06',
                                    '2017-07-05',
                                    '2018-07-11']),
              'lower_window': 0,
              'upper_window': 4,
              'prior_scale': 0.1})

最后一步是将这四个数据框合并:

holidays = pd.concat([holidays,
                      black_friday,
                      christmas_eve,
                      taste_of_chicago]
                    ).sort_values('ds')\
                     .reset_index(drop=True)

Black Friday圣诞节前夕芝加哥美食节的数据框中,我们添加了额外的prior_scale列。让我们打印holidays数据框的前 16 行来确认这一点:

holidays.head(16)

如下表所示,我们有 10 个默认假日,没有添加先验尺度或窗口。我们有芝加哥美食节活动,上限窗口为 4 天,先验尺度为0.1黑色星期五平安夜的先验尺度均为1。当 Prophet 构建模型时,如果缺失,它将应用默认的先验尺度。记住,NaN,代表非数字,在这种情况下意味着一个空单元格:

图 6.11 – 带有先验尺度的假日

图 6.11 – 带有先验尺度的假日

在我们构建了holidays DataFrame 之后,我们只需继续实例化我们的模型,对其进行拟合,并预测以构建预测:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                holidays=holidays,
                holidays_prior_scale=10)
model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)

现在已经创建了forecast DataFrame,你可以使用到目前为止所学的绘图工具进行实验,以探索结果。

选择适当的先验尺度,对于假日和季节性来说,有时可能很困难。Prophet 的默认值在大多数情况下都工作得非常好,但有时你可能需要更改它们,并且很难找到最佳值。在这些情况下,交叉验证是你的最佳方法。你将在第十三章中学习如何使用交叉验证以及适当的性能指标来优化你的 Prophet 模型,评估性能指标

摘要

在本章中,你首先学习了如何添加一个国家的默认假日,然后通过添加任何州或省的假日进一步深入。之后,你学习了如何添加自定义假日,并将这种技术扩展到调整跨越多天的假日。最后,你学习了正则化的概念以及它是如何用于控制过拟合的,以及如何将其全局应用于模型中的所有假日或更细致地通过为每个单独的假日指定不同的正则化来实现。

假日往往会导致时间序列出现巨大的峰值,忽略它们的影响将导致 Prophet 在预测结果中表现非常糟糕。本章中的工具将允许你的模型适应这些外部事件,并提供一种预测未来影响的方法。

在下一章中,我们将探讨 Prophet 中可用的不同增长模式。到目前为止,我们所有的模型都采用了线性增长,但在你的预测工作中可能会遇到不止这一种模式!

第七章:控制增长模式

到目前为止,在这本书中,我们构建的每一个预测都只遵循一种增长模式:线性。趋势有时会有一些小的弯曲,斜率要么增加要么减少,但本质上,趋势由线性段组成。然而,Prophet 有另外两种增长模式:逻辑平坦

使用非最佳增长模式对时间序列进行建模通常可以很好地拟合实际数据。但,正如你将在本章中看到的,即使拟合是现实的,未来的预测也可能变得非常不现实。有时,数据的形状会告诉我们选择哪种增长模式,有时你需要领域知识和一点常识。本章将帮助你做出适当的选择。此外,你将学习何时以及如何应用这些不同的增长模式。具体来说,本章将涵盖以下内容:

  • 应用线性增长

  • 理解逻辑函数

  • 满足预测

  • 应用平坦增长

  • 创建自定义趋势

技术要求

本章中示例的数据文件和代码可以在github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition找到。

应用线性增长

我们在前几章中构建的所有模型都默认使用线性增长模式。这意味着趋势由一条直线或几条直线组成,这些直线在变化点处相连——我们将在第八章影响趋势变化点中探讨这种情况。然而,现在,让我们再次加载我们的 Divvy 数据并专注于增长。

我们将再次导入pandasmatplotlibProphet,但这次,我们还将从 Prophet 的plot包中导入一个新函数,add_changepoints_to_plot,如下所示:

import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet
from prophet.plot import add_changepoints_to_plot

这个新功能将使我们能够轻松地将我们的趋势线直接绘制在我们的预测图中。

正如我们之前所做的那样,让我们打开 Divvy 数据并将其加载到我们的训练 DataFrame 中:

df = pd.read_csv('divvy_daily.csv')
df = df[['date', 'rides']]
df['date'] = pd.to_datetime(df['date'])
df.columns = ['ds', 'y']

我们已经在第五章处理季节性中了解到,这个数据集应该用乘法季节性来建模,并且通过将傅里叶阶数设置为4来稍微约束年度季节性。我们将在实例化我们的模型时设置这些值。我们还将明确设置growth='linear'。这是默认值,之前我们只是隐式地接受它,但为了清晰起见,我们在这里包括它:

model = Prophet(growth='linear',
                seasonality_mode='multiplicative',
                yearly_seasonality=4)

正如我们在第五章处理季节性中建模每日 Divvy 数据时所做的,接下来,我们将拟合模型,构建一个包含一年预测的future DataFrame,预测未来值,并绘制预测图。然而,这次我们将使用add_changepoints_to_plot函数。

函数要求我们指定要使用哪个绘图坐标轴,识别我们创建的模型,以及识别从predict方法输出的预测 DataFrame。对于坐标轴,我们使用 Matplotlib 的gca方法,即获取当前坐标轴,并在绘制预测时创建的图上调用它。你可以在以下代码中看到语法。我们在这里只使用绘图变化点函数来绘制趋势,所以我们现在将使用cp_linestyle=''移除变化点标记:

model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast,
                         cp_linestyle='')
plt.show()

作为输出,你现在应该看到与图 5**.8中类似的预测,但这次,趋势线将叠加在图上:

图 7.1 – 带趋势的 Divvy 预测

图 7.1 – 带趋势的 Divvy 预测

记住,Prophet 是一个加性回归模型。因此,趋势是我们预测的最基本构建块。我们通过添加季节性、假日和额外的回归因子来增加其细节和变化。前面图中(穿过每个正弦周期中点的实线)的趋势是去除季节性的 Divvy 图(在这个例子中我们从未添加过假日)。

如你所见,趋势是从2014年到晚2015年的直线段,然后是轻微的弯曲和另一个斜率较浅的直线段,从2016年开始。尽管有这个弯曲,它本质上还是线性的。

现在我们来看下一个增长模式,逻辑增长。要理解这种增长模式,你首先需要了解逻辑函数。

理解逻辑函数

逻辑函数生成一个 S 形曲线;方程具有以下形式:

img/B19630_07_F01.jpg

在这里,img/B19630_07_F02.png是曲线的最大值,img/B19630_07_F03.png是曲线的逻辑增长率或陡度,而img/B19630_07_F04.png是曲线中点的x值。

img/B19630_07_F05.pngimg/B19630_07_F06.png,和img/B19630_07_F07.png为例,逻辑函数产生了标准逻辑函数img/B19630_07_F08.png,如下面的图中所示:

图 7.2 – 标准逻辑函数

图 7.2 – 标准逻辑函数

如果你研究过逻辑回归或神经网络,你可能认出这实际上是S 形函数。任何从-∞到∞的输入值x,都会被压缩到 0 到 1 之间的输出值y。这个方程使得逻辑回归模型能够接受任何输入值并输出 0 到 1 之间的概率。

该方程由比利时数学家皮埃尔·弗朗索瓦·弗赫尔斯特(Pierre François Verhulst)在 1838 年至 1847 年间发表的三篇论文中开发。弗赫尔斯特正在努力模拟比利时的种群增长。

种群增长大致遵循一个初始的指数增长速率,然后是一个线性(也称为算术)增长速率,直到种群达到饱和点,此时增长速度减慢至零。这就是你在前面的图表中看到的形状,从曲线的中点开始向右移动。Verhulst 发明了“逻辑”这个词,与“算术”和“几何”类似,但源自“对数”。不要将这个词与“后勤”混淆,它指的是处理细节。它们有完全不同的起源。

预言者的逻辑增长模式遵循这条一般曲线。曲线的饱和水平是曲线渐近接近的上限和下限。

除了在统计学和机器学习中的应用,其中逻辑曲线用于逻辑回归和神经网络,逻辑函数也常用于模拟人口增长,无论是人类(如 Verhulst 的比利时)还是动物,正如我们在本章中所做的那样。它常用于医学中模拟肿瘤的生长、感染者的细菌或病毒载量,或在流行病期间人们的感染率。

在经济学和社会学中,该曲线用于描述新创新的采用率。语言学家用它来模拟语言变化。它甚至可以用来模拟谣言或新观点在整个群体中的传播。

让我们看看如何在 Prophet 中应用这一点。

满足预测

在 19 世纪初,美国向西扩张使许多定居者和他们的牲畜与本土狼群接触。这些狼开始捕食家畜,这导致定居者为了保护自己的动物而猎杀和杀死狼。灰狼仍然存在于这片土地上(当它在 1872 年建立时成为黄石国家公园),但在接下来的几十年里,它们在该地区以及下 48 个州几乎被猎杀至灭绝。

在 20 世纪 60 年代,公众开始理解生态系统和物种之间相互联系的概念,1975 年,决定将狼群恢复到黄石公园,1995 年最终有 31 只灰狼从加拿大迁移到公园,这为公园内自然种群增长提供了一个几乎完美的实验。

我们将在接下来的几个例子中查看这个种群。然而,我们将使用模拟数据,因为真实数据在历史记录中分布不均。由于狼倾向于避免与人类接触,它们的数量计数永远无法精确,因此缺乏准确的数据。此外,还有许多复合因素(例如天气)我们不会建模(而且通常是不可预测的)。

为了理解这些复合因素,考虑一下密歇根湖苏必利尔湖上的伊莎贝拉皇家岛上的例子,自 1959 年以来,该岛上的驼鹿和狼种群一直处于持续研究之中。这实际上是世界上任何捕食者-猎物种群系统的最长连续研究。如下面的图表所示,这至少不是一个可预测的系统:

图 7.3 – 伊莎贝拉皇家岛上的狼和驼鹿的种群数量

图 7.3 – 伊莎贝拉皇家岛上的狼和驼鹿的种群数量

在 20 世纪 60 年代和 70 年代,不断增长的驼鹿种群提供了食物,这允许狼群数量翻倍。但在 1980 年,人类意外引入了犬细小病毒,这种疾病导致狼群数量崩溃。随着其唯一捕食者的数量下降,驼鹿种群再次增加,但于 1996 年在创纪录的最严重冬季和不可预测的驼鹿蜱虫爆发双重压力下崩溃。

在 20 世纪 90 年代,狼群数量过低,无法进行健康繁殖,导致近亲繁殖水平极高,这抑制了它们的种群数量,直到 1990 年代末一只狼通过穿越来自加拿大的冬季冰层到达岛屿时,种群数量才有所回升。此后,尽管驼鹿数量下降,狼群数量在整个 21 世纪初仍然在增加。所有这些都说明,小型、孤立种群代表一个非常动态的系统,当它们不与自然外部事件隔离时,无法准确预测。

增加逻辑增长

为了在黄石公园合成一个相对现实的狼群种群,让我们假设 1995 年引入了 100 只狼。公园生态学家对该地区进行了调查,并确定这片土地可以支持总共 500 只狼的种群。

在线性增长示例中,我们导入了pandasmatplotlibProphetadd_changepoints_to_plot函数,因此为了继续,我们只需要导入numpyrandom库来创建我们的数据集。务必设置随机种子,以确保每次运行代码时我们都得到相同的伪随机结果:

import numpy as np
import random
random.seed(42)  # set random seed for repeatability

我们将通过首先创建一系列从 1995 年到 2004 年的月度日期来模拟狼群数量。在每一个日期,我们将从我们的逻辑方程中计算出输出。然后,我们将添加一些正弦变化来考虑年度季节性,最后,一些随机噪声。然后,我们只需要将我们的曲线放大:

x = pd.to_datetime(pd.date_range('1995-01', '2004-02',
                                 freq='M')\
                   .strftime("%Y-%b").tolist())
y = [1 / (1 + np.e ** (-.03 * (val - 50))) for val in \
     range(len(x))]  # create logistic curve
# add sinusoidal variation
y = [y[idx] + y[idx] * .01 * np.sin((idx - 2) * (360 / 12)\
     * (np.pi / 180)) for idx in range(len(y))]
# add noise
y = [val + random.uniform(-.01, .01) for val in y]
y = [int(500 * val) for val in y]  # scale up

让我们绘制曲线以确保一切如预期进行:

plt.figure(figsize=(10, 6))
plt.plot(x, y)
plt.show()

如果一切顺利,你应该会看到这个图表:

图 7.4 – 黄石公园模拟的狼群数量

图 7.4 – 黄石公园模拟的狼群数量

让我们从拟合一个具有线性增长的 Prophet 模型开始分析这些数据。这个例子将演示在选择不适当增长模式时可能会出现什么问题。

使用线性增长建模

正如我们之前所做的那样,我们首先将我们的数据组织到一个 DataFrame 中,用于 Prophet:

df = pd.DataFrame({'ds': pd.to_datetime(x), 'y': y})

除了线性增长外,让我们将年季节性的傅里叶阶数设置为3,并将季节性模式设置为乘法。然后,我们拟合我们的 DataFrame 并创建future DataFrame。我们以月度频率模拟了这些数据,所以我们将预测10年并将freq='M'。在预测未来之后,我们将绘制预测图,并使用add_changepoints_to_plot函数来叠加趋势:

model = Prophet(growth='linear',
                yearly_seasonality=3,
                seasonality_mode='multiplicative')
model.fit(df)
future = model.make_future_dataframe(periods=12 * 10,
                                     freq='M')
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast,
                         cp_linestyle='')
plt.show()

立刻,你应该看到在预测将自然饱和在某个水平的情况下使用线性趋势会出现什么问题。预测值将随着预测时间周期的延长而不断上升,趋向于无穷大:

图 7.5 – 使用线性增长的狼群数量预测

图 7.5 – 使用线性增长的狼群数量预测

显然,这是不现实的。狼能吃的食物是有限的;在某个点上,食物将不足,狼将开始饿死。现在让我们用逻辑增长来模拟这种情况,看看会发生什么。

使用逻辑增长建模

使用cap并在我们的future DataFrame 中模拟它。

通常,确定上限可能会带来一些困难。如果你的曲线已经接近饱和水平,你可以更好地看到它接近的值并选择它。然而,如果没有,那么一点领域知识将真正是你的最佳解决方案。在你可以建模逻辑增长率之前,你必须对饱和水平最终在哪里有一些想法。通常,这个上限是使用数据或对市场规模有特殊专业知识来设置的。在我们的例子中,我们将上限设置为500,因为这是生态学家估计的值:

df['cap'] = 500

接下来,我们继续像上一个例子中那样做,但这次,在拟合和创建future DataFrame 之前,让我们将增长模式设置为logistic

model = Prophet(growth='logistic',
                yearly_seasonality=3,
                seasonality_mode='multiplicative')
model.fit(df)
future = model.make_future_dataframe(periods=12 * 10,
                                     freq='M')

我们还需要将上限添加到我们的future DataFrame 中:

future['cap'] = 500

现在,当我们预测并绘制预测图时,你会看到一个非常不同形状的曲线:

forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast,
                         cp_linestyle='')
plt.show()

默认情况下,Prophet 将上限(如果有,则还包括下限)显示为你的图表中的水平虚线:

图 7.6 – 使用逻辑增长的狼群数量预测

图 7.6 – 使用逻辑增长的狼群数量预测

在逻辑增长的情况下,狼群的数量被允许以大致相同的速率增长数年。当它接近饱和点,即自然资源能够支持的最大人口时,增长率会放缓。在此之后,增长率保持平稳,仅略有季节性变化,因为老狼在冬春季节死亡,而春季幼狼出生。

非常数上限

重要的是要注意,上限值不一定需要是恒定的。例如,如果你在预测销售额,你的饱和极限将是市场大小。但这个市场大小可能会随着各种因素导致更多消费者考虑购买你的产品而增长。让我们快速看一下如何建模这个例子。我们假设黄石公园的狼群数量受到公园大小的限制。现在,让我们创建一个假设情况,从 2007 年开始,公园大小逐渐增加,创造条件允许每月增加两只狼。

让我们创建一个函数来设置上限。对于 2007 年之前的日期,我们将保持公园的饱和极限为500。然而,对于所有从 2007 年开始的日期,我们将每月增加上限两只:

def set_cap(row, df):
    if row.year < 2007:
        return 500
    else:
        pop_2007 = 500
        idx_2007 = df[df['ds'].dt.year == 2007].index[0]
        idx_date = df[df['ds'] == row].index[0]
        return pop_2007 + 2 * (idx_date - idx_2007)

现在,让我们为我们的训练 DataFrame,df,设置上限:

df['cap'] = df['ds'].apply(set_cap, args=(df,))

上限应该在整个过程中保持为500,因为我们的训练数据在 2004 年结束。现在,让我们像以前一样创建我们的模型,但使用set_cap函数设置我们的future DataFrame:

model = Prophet(growth='logistic',
                yearly_seasonality=3,
                seasonality_mode='multiplicative')
model.fit(df)
future = model.make_future_dataframe(periods=12 * 10,
                                     freq='M')
future['cap'] = future['ds'].apply(set_cap, args=(future,))
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast,
                          cp_linestyle='')
plt.show()

现在,你可以看到狼群数量正在趋向于我们不断增加的上限:

图 7.7 – 非恒定上限下的狼群数量预测

图 7.7 – 非恒定上限下的狼群数量预测

上限是针对 DataFrame 中的每一行设置的值;对于每个日期,你可以设置任何有意义的值。上限可能是恒定的,就像我们的第一个例子一样,它可能线性变化,就像我们在这里所做的那样,或者它可能遵循你选择的任何任意曲线。

现在,让我们看看相反的情况,一个假设的情况,狼群数量正在悲哀地下降,并接近灭绝。

下降的指数增长

在这个例子中,唯一的区别是我们必须除了cap值外,还要声明一个floor值。让我们构建另一个伪随机数据集,但具有负增长:

x = pd.to_datetime(pd.date_range('1995-01','2035-02',
                                 freq='M')\
                   .strftime("%Y-%b").tolist())
y = [1 - 1 / (1 + np.e ** (-.03 * (val - 50))) for val in \
     range(len(x))]  # create logistic curve
# add sinusoidal variation
y = [y[idx] + y[idx] * .05 * np.sin((idx - 2) * (360 / 12)\
     * (np.pi / 180)) for idx in range(len(y))]
# add noise
y = [val + 5 * val * random.uniform(-.01, .01) for val \
     in y]
y = [int(500 * val) for val in y]  # scale up
plt.figure(figsize=(10, 6))
plt.plot(x, y)
plt.show()

增长曲线应该看起来像这样:

图 7.8 – 黄石公园模拟的狼群数量下降

图 7.8 – 黄石公园模拟的狼群数量下降

在本例的预测中,我们将数据截断到2006年,并尝试预测狼群数量何时会降至零。在创建我们的 DataFrame 时,我们指定了与之前相同的cap值,以及一个floor值:

df2 = pd.DataFrame({'ds': pd.to_datetime(x), 'y': y})
df2 = df2[df2['ds'].dt.year < 2006]
df2['cap'] = 500
df2['floor'] = 0

我们将一步完成模型。一切与之前的例子相同,只是这次我们在future DataFrame 中也设置了floor

model = Prophet(growth='logistic',
                yearly_seasonality=3,
                seasonality_mode='multiplicative')
model.fit(df2)
future = model.make_future_dataframe(periods=12 * 10,
                                     freq='M')
future['cap'] = 500
future['floor'] = 0
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast,
                         cp_linestyle='')
plt.show()

毫不奇怪,Prophet 可以轻松处理这种情况:

图 7.9 – 下降的指数增长下的狼群数量预测

图 7.9 – 下降的指数增长下的狼群数量预测

Prophet 将预测精确的小数值,当然,狼的数量是整数,但这个图表显示,在 2010 年和 2014 年之间,狼群将灭绝。在现实场景中,最后几只剩余的狼是否是繁殖对的一部分也非常重要,但我们在这里忽略了这个因素。

注意,因为我们已经指定了上限和下限,Prophet 将它们都绘制为水平虚线。当逻辑增长下降时,即使没有相关的上限,就像这里的情况一样,你也必须在你的模型中包含它。你可以选择一个任意高的上限,这对你的模型没有影响,但请注意,它将被包含在你的图表中,可能会使 Prophet 的预测看起来非常低。

然而,你可以通过包括plot_cap参数来将其排除在图表之外,就像这里所做的那样:fig = model.plot(forecast, plot_cap=False),这会修改上限和下限。Prophet 目前不支持从你的图表中排除其中一个。

Prophet 目前支持一种更多增长模式:无增长(或平稳)。然而,Prophet 团队在撰写本文时正在努力开发其他模式,这些模式可能很快就会可用,所以请密切关注文档。让我们看看这种最终的增长模式。

应用平稳增长

平稳增长是指趋势线在整个数据中完全恒定。数据值的不同仅由于季节性、假日、额外回归因子或噪声。要了解如何建模平稳增长,让我们继续使用我们的狼群数据,但这次,考虑远期未来,当人口已经完全稳定时。

让我们先创建一个新的数据集——本质上与我们的逻辑增长数据集相同,但时间跨度更长:

x = pd.to_datetime(pd.date_range('1995-01','2096-02',
                                 freq='M')\
                   .strftime("%Y-%b").tolist())
# create logistic curve
y = [1 / (1 + np.e ** (-.03 * (val - 50))) for val in \
     range(len(x))]
 # add sinusoidal variation
y = [y[idx] + y[idx] * .01 * np.sin((idx - 2) * (360 / 12)\
     * (np.pi / 180)) for idx in range(len(y))]
# add noise
y = [val + 1 * val * random.uniform(-.01, .01) for val \
     in y]
y = [int(500 * val) for val in y]  # scale up
plt.figure(figsize=(10, 6))
plt.plot(x, y)
plt.show()

我们现在正在展望从狼群被重新引入公园的一个世纪之后:

图 7.10 – 一个世纪内的模拟狼群人口

图 7.10 – 一个世纪内的模拟狼群人口

经过这么长时间,狼群已经达到饱和点并完全稳定。我们现在将创建我们的训练数据框,但然后只限制我们的数据到范围的最后十年,那里的整体趋势已经很好地饱和:

df = pd.DataFrame({'ds': pd.to_datetime(x), 'y': y})
df = df[df['ds'].dt.year > 2085]
plt.figure(figsize=(10, 6))
plt.plot(df['ds'], df['y'])
plt.show()

绘制这些数据应显示没有整体增长,只是非常嘈杂的季节性:

图 7.11 – 模拟的稳定狼群人口

图 7.11 – 模拟的稳定狼群人口

让我们首先使用默认的线性增长来看看可能会出错:

model = Prophet(growth='linear',
                yearly_seasonality=3,
                seasonality_mode='multiplicative')
model.fit(df)
future = model.make_future_dataframe(periods=12 * 10,
                                     freq='M')
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast,
                         cp_linestyle='')
plt.show()

由于数据中的随机噪声,Prophet 会在看似存在趋势的短暂区域找到趋势,无论是正的还是负的。如果这些时期发生在训练数据的末尾,那么这条曲线将延续到整个预测的未来数据的输出:

图 7.12 – 使用线性增长的稳定狼群人口预测

图 7.12 – 使用线性增长的稳定狼群人口预测

如您所见,Prophet 预测狼群数量正在减少,尽管它相当稳定。此外,不确定性区间正在扩大;Prophet 足够聪明,知道这并不完全正确。现在让我们用平稳增长来正确地建模。由于趋势将是恒定的,设置季节性模式是不相关的。它仍然会被计算为加法或乘法,但无论哪种情况,最终结果都将相同。我们在这里将忽略它。

在模型实例化期间设置growth='flat'即可创建具有平稳增长的模型:

model = Prophet(growth='flat',
                yearly_seasonality=3)
model.fit(df)
future = model.make_future_dataframe(periods=12 * 10,
                                     freq='M')
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast,
                         cp_linestyle='')
plt.show()

现在,Prophet 的趋势线是完全平坦的:

图 7.13 – 使用平稳增长的稳定狼群人口预测

图 7.13 – 稳定的狼群人口预测,增长平稳

无论我们预测得多远,趋势都将保持稳定。在这个例子中,Prophet 模型的唯一变化来自年度季节性,因为我们没有添加假日,也没有包括每日或每周的季节性。

这三种增长模式——线性、逻辑和平坦——在行业中是最常用的,几乎可以覆盖大多数分析师将看到的几乎所有预测任务。然而,有时分析师需要自定义增长模式。尽管这不是最简单的任务,但 Prophet 确实具有接受任何你可以数学定义的增长模式的能力。

创建自定义趋势

开源软件的一个关键优势是任何用户都可以下载源代码,并根据他们的使用案例对软件进行自己的修改以更好地适应。尽管几乎所有常见的时间序列都可以用 Prophet(分段线性、分段逻辑和平坦)中实现的三个趋势模式进行适当建模,但可能存在需要不同于提供的趋势模型的情况;由于 Prophet 是开源的,因此相对容易创建你需要的任何内容。但有一个快速警告:这只是在概念上相对容易。从数学上讲,它可能相当复杂,你必须具备扎实的软件工程技能才能成功修改代码。

让我们看看一个可能的例子。考虑一家小型服装零售商,它为每个季节更新其收藏品:

df = pd.read_csv('../data/clothing_retailer.csv')
df['ds'] = pd.to_datetime(df['ds'])

每日销售额高度依赖于当前可用的收藏品的热度,因此趋势大多是平稳的,但每三个月当新收藏品发布时,会看到戏剧性的阶梯式变化:

图 7.14 – 服装零售商的每日销售额(以千为单位)

图 7.14 – 服装零售商的每日销售额(以千为单位)

我们有新季节两周的数据,我们想要预测本季节剩余的销售情况。Prophet 可用的趋势模型都无法很好地捕捉这一点。我们最好的选择是使用默认的增长模型,线性。不过,让我们看看当我们尝试这样做会发生什么:

model = Prophet()
model.fit(df)
future = model.make_future_dataframe(76)
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()

结果预测有太多的戏剧性变化点:

图 7.15 – 不合适的线性预测

图 7.15 – 不合适的线性预测

预测期间的置信区间爆炸,因为模型预计有更多的潜在变化点;它还忽略了数据在前三个月已经平稳的事实,预测了一个荒谬的持续曲棍球棒增长速率。一个平稳增长模型会更好,但在 Prophet 中实现时,它无法处理变化点。

这里的过程变得复杂,并且非常依赖软件工程技能。我们需要创建一个自定义趋势模型。简而言之,必要的代码需要从 Prophet 的源代码中复制 Prophet 类,该类位于forecaster.py文件中,在github.com/facebook/prophet/blob/main/python/prophet/forecaster.py。然后进行一些修改。特别是,这个新类(在我们 GitHub 仓库的示例中,我们称之为ProphetStepWise)继承了基类Prophet的所有方法和属性,并在几个方面进行了修改:

  1. 它修改了原始Prophet类的fit函数,以初始化新的步进增长模式。

  2. 它创建了一个新的函数,stepwise_growth_init,类似于当前的flat_growth_init函数,它使用平稳增长初始化趋势。当前的flat_growth_init函数将偏移参数设置为历史值的平均值,但这个新的stepwise_growth_init函数考虑变化点的位置,并在每个变化点之间应用不同的偏移参数。

  3. 它创建了一个新的函数,stepwise_trend,类似于现有的flat_trend函数,它评估新的步进趋势。

  4. 它修改了现有的sample_predictive_trend函数,将'flat'增长模式重新定义为使用新的stepwise_trend函数。

  5. 最后,它修改了现有的predict_trend函数,当设置'flat'增长时,使用stepwise_trend而不是现有的flat_trend函数。

所有这些步骤的完整代码太长且定制化,无法在此完全重现,但所有代码都位于我们之前链接的 GitHub 仓库中的Chapter07文件夹内。

一旦创建了新的ProphetStepWise类,我们就可以像使用标准的Prophet类一样使用它来做出预测。在这里,我们将增长声明为'flat',并手动提供每个变化点的位置(变化点都与每个新服装季节的第一天相吻合——但现在不必担心这些细节;变化点将在下一章中讨论!):

model = ProphetStepWise(growth='flat',
                        changepoints= ['2021-04-01',
                                       '2021-07-01',
                                       '2021-10-01',
                                       '2022-01-01',
                                       '2022-04-01',
                                       '2022-07-01',
                                       '2022-10-01'])
model.fit(df)
future = model.make_future_dataframe(76)
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast, threshold=0.00);

结果预测看起来更加合理!

图 7.16 – 我们的新步进趋势

图 7.16 – 我们的新步进趋势

然而,你会注意到,尽管对整个季节的预测相当准确,但置信区间却非常宽泛。为了解决这个问题,你还需要修改prophet.stan文件。由于本书只使用 Python 代码,因此 Stan 模型中的这些更改不在此讨论范围之内。然而,对于那些感兴趣的人来说,官方 Prophet GitHub 仓库中有一个很好的逐步趋势模型示例,其中包含了正确实现的 Stan 更改,你可以通过以下链接查看:github.com/facebook/prophet/pull/1466/files。实际上,本节中的大部分代码都来自那个示例。

摘要

在本章中,你了解到本书前几章构建的模型都具备线性增长的特点。你学习了逻辑函数是如何被开发出来以模拟人口增长的,然后学习了如何在 Prophet 中通过模拟 1995 年狼群重新引入黄石公园后的增长来实施这一功能。

在 Prophet 中,逻辑增长可以模拟为增加到饱和极限,称为上限,或者减少到饱和极限,称为下限。最后,你学习了如何模拟平坦(或无增长)趋势,其中趋势在整个数据期间固定为一个值,但季节性仍然允许变化。在本章中,你使用了add_changepoints_to_plot函数来在你的预测图上叠加趋势线。

选择正确的发展模式很重要,尤其是在进行未来预测时。在本章中,我们查看了一些例子,其中错误的发展模式很好地拟合了实际数据,但未来的预测却变得非常不切实际。最后,我们看到了创建自定义趋势模型的例子。这个过程是本书中介绍的最先进的技术,但也是最强大的,因为它展示了如何利用 Prophet 的开源代码来完全定制该包以满足你的特定需求。在下一章中,你将了解所有关于变化点的内容以及如何使用它们来获得对趋势线更多的控制。

第八章:影响趋势变化点

在开发 Prophet 的过程中,工程团队认识到现实世界的时间序列经常会在其轨迹上表现出突然的变化。作为一个基本的线性回归模型,Prophet 如果不能特别小心,将无法捕捉这些变化。然而,你可能会在之前的章节中注意到,当我们绘制示例中的预测成分时,趋势线并不总是完全直线。显然,Prophet 团队已经开发了一种方法,使 Prophet 能够捕捉线性模型中的这些弯曲。这些弯曲的位置被称为变化点

Prophet 将自动识别这些变化点,并允许趋势适当地适应。然而,如果你发现 Prophet 在拟合这些变化率时欠拟合或过拟合,你可以使用几个工具来控制这种行为。在本章中,我们将探讨 Prophet 的自动变化点检测,以帮助你理解在默认设置下模型中发生了什么。然后,我们将探讨如果你需要更精细地控制变化点过程,你可以使用的两种进一步的技术。

具体来说,在本章中,你将学习以下内容:

  • 自动趋势变化点检测

  • 正则化变化点

  • 指定自定义变化点位置

技术要求

本章示例的数据文件和代码可以在github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition找到。

自动趋势变化点检测

趋势变化点是你时间序列中趋势成分突然改变斜率的位置。这些变化点出现的原因很多,这取决于你的数据集。例如,Facebook(现在称为 Meta)开发了 Prophet 来预测其自身的业务问题;它可能会对每日活跃用户数量进行建模,并在新功能的发布上看到趋势的突然变化。

由于规模经济允许航班价格大幅降低,航空公司乘客数量可能会突然变化。大气中二氧化碳的趋势在过去数万年中相对平坦,但在工业革命期间突然发生了变化。

在前几章中,我们通过 Divvy 数据集的工作看到,大约两年后增长放缓。让我们更仔细地看看这个例子,以了解自动变化点检测。

默认变化点检测

Prophet 通过首先指定可能发生变化点的潜在日期数量来设置变化点。然后 Prophet 会计算这些点中每个点的变化幅度,试图在尽可能保持这些幅度最低的情况下拟合趋势曲线。你可以通过调整changepoint_prior_scale来调整 Prophet 的灵活性。你可能已经从之前的内容中认识到了这个参数——季节性和节假日都有自己的正则化先验尺度。

使用变化点,它有几乎相同的效果,我们将在本章后面探讨它。在 Prophet 的默认设置中,大多数这些潜在变化点的幅度几乎为零,因此对我们的趋势曲线的影响可以忽略不计。

要开始我们的代码,我们需要进行必要的导入并加载我们的 Divvy 数据。在这里,我们将使用每日的 Divvy 数据。我们还将导入add_changepoints_to_plot函数,这个函数在第七章 控制增长模式 中被介绍过;我们将在这里大量使用它:

import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet
from prophet.plot import add_changepoints_to_plot
df = pd.read_csv('divvy_daily.csv')
df = df[['date', 'rides']]
df['date'] = pd.to_datetime(df['date'])
df.columns = ['ds', 'y']

使用默认设置,Prophet 将在数据的第一个 80%中均匀放置 25 个潜在变化点,在确定它们的幅度之前。在这个 Divvy 数据中,这 25 个位置由图中垂直虚线表示:

图 8.1 – 带有潜在变化点位置的 Divvy 数据

图 8.1 – 带有潜在变化点位置的 Divvy 数据

现在,让我们拟合我们的 Prophet 模型。在这个步骤中,Prophet 将确定在每个潜在变化点应用哪些幅度。从前几章的例子中,我们已经学会了如何使用乘法季节性来建模这些数据,并稍微降低年季节性的傅里叶阶数。当你实例化我们的 Prophet 对象时,你可以在这里看到这一点。

拟合模型后,我们将不带future DataFrame 指定地调用predict,这将导致 Prophet 构建其模型并预测历史值,但不进行任何预测:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4)
model.fit(df)
forecast = model.predict()

在这一点上,我们将绘制模型。我们使用add_changepoints_to_plot函数来查看显著变化点的位置。正如你在第七章 控制增长模式中看到的,add_changepoints_to_plot函数需要三个必需的参数。第一个参数是要添加变化点的坐标轴。我们指定在第一次绘图调用中创建的fig,使用gca()方法,代表获取当前坐标轴。第二个参数是我们的模型,第三个参数是我们的预测。

第七章 控制增长模式中,我们使用了cp_linestyle参数来强制 Prophet 不绘制变化点,只绘制趋势;在这个例子中,我们不会使用这个参数:

fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()

现在,你应该可以看到 Prophet 确定这 25 个潜在变化点中有 5 个实际上是显著的。这些 5 个在这个图中用垂直的虚线表示:

图 8.2 – 分配变化点图

图 8.2 – 分配变化点图

从第一个变化点很难看出趋势实际上是在弯曲,但从接下来的四个来看,就非常明显了。在每个这些变化点,趋势的斜率被允许变得较浅。

每个 25 个潜在变化点的幅度都存储在model.params中,但这些值已经被归一化,因此它们的绝对值没有意义,但它们的相对幅度是有意义的。模型参数存储在一个字典中,其中'delta'是变化点幅度的键。让我们看一下:

print(model.params['delta'])

在这个模型中,这些变化点幅度应该与图 8.3中显示的变化点幅度相似。因为这些数字是使用优化过程而不是确定性方程计算出来的,所以你可能会得到不同的确切值,但指数应该是大致相同的:

图 8.3 – 分配变化点幅度

图 8.3 – 分配变化点幅度

这些幅度中的大多数都有一个指数为-08 或-09,这意味着在标准记数法中,你应该将小数点向左移动那么多位,8 位或 9 位,也就是说这些数字非常接近零。你可以通过绘制它们来可视化这些幅度。在这里,我正在叠加趋势线和所有变化点的显著变化点:

图 8.4 – 变化点幅度

图 8.4 – 变化点幅度

让我稍微解释一下这个图。左侧的轴与图 8.2中的y轴相同。趋势线——从左下角穿过到右上角的实线——绘制在这个轴上。垂直的虚线是 Prophet 识别出的显著变化点。实心的垂直条是变化点幅度;这些绘制在右侧的轴上,趋势 变化率

再次,这些幅度中的大多数几乎为零,所以它们没有出现在图上。水平的虚线表示变化点幅度为零。从这里向上延伸的条表示具有正幅度的变化点,趋势向上弯曲,从这里向下延伸的条表示具有负幅度的变化点,趋势向下弯曲。

add_changepoints_to_plot函数只会绘制绝对幅度大于 0.01 的变化点。两条水平虚线位于幅度水平 0.01 和-0.01;Prophet 只绘制超出这些限制的幅度。你可以使用函数中的threshold参数更改此阈值;例如,add_changepoints_to_plot(fig.gca(), model, forecast, threshold=0.1)将阈值扩大到上限为0.1和下限为-0.1。这只会影响绘图可视化,不会影响你的实际变化点。

因此,图 8.4展示了 Prophet 成功地将几乎所有潜在变化点的影响降至不显著。总共有八个变化点的幅度足够大,可以在我们的图表中看到,但其中只有五个超过了 Prophet 的绘图阈值(尽管它们对趋势的小幅度影响仍然存在)。尽管可能难以看出,唯一正值的变化点,位于2015-01之后,确实使趋势在该点变得更加陡峭。在其他显著变化点的位置,趋势变得较为平缓。

上述示例展示了 Prophet 在完全自动设置下的变化点行为。在下一节中,我们将探讨你可以使用的杠杆来获得对变化点的一些控制。

正则化变化点

如前所述,Prophet 默认将在时间序列的前 80%中放置 25 个潜在变化点。要控制 Prophet 的自动变化点检测,你可以在模型实例化期间修改这两个值,使用n_changepointschangepoint_range参数。例如,将潜在变化点的数量更改为5的操作如下:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                n_changepoints=5)

这导致在数据的前 80%中出现了五个均匀分布的潜在变化点,如图所示:

图 8.5 – 五个潜在变化点

图 8.5 – 五个潜在变化点

或者,你也可以强制所有 25 个变化点不在数据的前 80%,而是在前 50%:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                changepoint_range=.5)

现在,我们看到潜在变化点仅位于数据范围的前半部分:

图 8.6 – 数据前 50%的变化点

图 8.6 – 数据前 50%的变化点

当然,你可以在一个模型中使用这两个参数。在两种情况下,重要的是要记住,你并没有指示 Prophet 在这些位置放置变化点,只是潜在变化点。它仍然会尝试将尽可能多的它们强制为零,并且确实在这两种情况下都留下了与使用默认值构建的示例几乎相同的预测趋势。

此外,请记住 Prophet 从不会在将来放置变化点。这就是为什么默认情况下,Prophet 只会使用前 80% 的数据——以防止它选择一个错误的变化点,因为即将到来的数据点很少,无法纠正其错误。然而,Prophet 在创建不确定性区间时,会估计未来的变化点;因此,具有许多大型变化点的模型也会看到更大的预测不确定性。

通常,在序列的非常晚的时刻设置变化点有更高的过拟合可能性。为了了解原因,我构建了 Divvy 数据的两年预测,并强迫 Prophet 只选择一个变化点,放置在数据的最后两个月。在十一月,由于冬季使用量下降,每天的骑行次数迅速下降。Prophet 看到了这种下降,并决定这必须是一个负趋势变化,因此相应地调整了其未来的预测:

图 8.7 – Prophet 过度晚的变化点

图 8.7 – Prophet 过度晚的变化点

我可能并不比 Prophet 更擅长预测未来,但我对 Prophet 在这种情况下未来的预测不太准确有很高的信心。

说了这么多,你通常不需要经常调整变化点的数量或变化点范围。默认值几乎总是非常合适。如果你发现 Prophet 要么过度拟合,要么欠拟合变化点,最好是通过对正则化进行控制。就像我们在 第五章处理季节性,和 第六章预测节假日影响 中所做的那样,我们使用 先验尺度 进行正则化。

如果你还记得 第五章处理季节性,和 第六章预测节假日影响,先验尺度用于控制 Prophet 的灵活性。一个过于灵活的模型有很大的可能性会过度拟合数据,即除了真实信号外,还建模了太多的噪声。一个不够灵活的模型有很大的可能性会欠拟合数据或无法捕捉到所有可用的信号。

默认情况下,seasonality_prior_scaleholidays_prior_scale 都被设置为 10。然而,changepoint_prior_scale 默认设置为 0.05。但就像季节性和节假日先验尺度一样,增加这个值会使趋势更加灵活,而减少它会使趋势不那么灵活。合理的值通常在 0.0010.5 之间。

让我们拟合并绘制一个将 changepoint_prior_scale 增加到 1 的模型。这应该会使 Prophet 的趋势具有很大的灵活性:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                changepoint_prior_scale=1)
model.fit(df)
forecast = model.predict()
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()

在这里,我们可以看到 Prophet 的趋势现在过度拟合了:

图 8.8 – Prophet 过度缺乏趋势正则化

图 8.8 – Prophet 过度缺乏趋势正则化

当我们放宽正则化参数时,Prophet 开始过度拟合趋势线,并试图捕捉一些年度季节性。我们给了 Prophet 太多的趋势拟合灵活性。

另一方面,现在让我们看看当我们过于严格地进行正则化时会发生什么。在这个例子中,我们将changepoint_prior_scale从默认值降低到0.007

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4,
                changepoint_prior_scale=.007)
model.fit(df)
forecast = model.predict()
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()

减少了changepoint_prior_scale后,以下图表显示 Prophet 的趋势不够灵活:

图 8.9 – 过度趋势正则化的 Prophet

图 8.9 – 过度趋势正则化的 Prophet

将本章开头的图 8.9图 8.2进行比较。尽管图 8.9中变化点的位置与图 8.2大致相同,但我们使用的正则化水平过度限制了变化点的幅度。图 8.2中明显的弯曲现在在图 8.9中变得如此微小,以至于难以辨认。

控制 Prophet 中的变化点还有另一种方法:通过指定您自己的自定义变化点位置。我们将查看一个新的数据集来探讨这个主题,足球运动员詹姆斯·罗德里格斯的 Instagram 账户@jamesrodriguez10。这些数据是在 2019 年 11 月 22 日收集的。

指定自定义变化点位置

詹姆斯·罗德里格斯是一位哥伦比亚足球运动员,他参加了 2014 年和 2018 年的世界杯。他在两次世界杯中都表现出色,但在 2014 年因进球数超过其他任何参赛球员而赢得了金靴奖。我选择他的账户,因为它展示了一些非常有趣的行为,这些行为在没有变化点的情况下将非常难以建模:

图 8.10 – 詹姆斯·罗德里格斯每日 Instagram 点赞数

图 8.10 – 詹姆斯·罗德里格斯每日 Instagram 点赞数

他 Instagram 帖子获得的点赞数随着时间的推移逐渐增加,但有两个值得注意的峰值,分别在 2014 年和 2018 年夏季,那时他正在参加世界杯。很明显,2014 年的峰值导致了显著的趋势变化。他在世界杯期间帖子获得的点赞数大幅增加,之后有所下降,但没有回到之前的基线。在此期间,他获得了大量新粉丝,并且每篇帖子的点赞数持续增加。

类似地,在 2018 年,他的个人资料在世界杯期间夏季的点赞量大幅上升,但比赛结束后并不清楚是否存在显著的趋势变化。此外,你还可以看到 2017 年夏季的另一个峰值。在那一年 7 月 11 日,罗德里格斯宣布他已与拜仁慕尼黑队签约。我们也将这一事实纳入我们的模型中。

为了模拟这种行为,我们首先需要考虑世界杯的特殊事件以及新球队的公告。我们将通过为它们创建自定义假日来实现这一点。其次,我们需要考虑趋势变化;我们将通过设置自定义趋势变化点来完成这项工作。数据中似乎没有太多季节性,为了简化我们的模型,我们将指示 Prophet 不要拟合任何。

我们已经完成了必要的导入,因此我们首先需要将数据加载到我们的 Prophet DataFrame 中:

df = pd.read_csv('instagram_jamesrodriguez10.csv')
df['Date'] = pd.to_datetime(df['Date'])
df.columns = ['ds', 'y']

接下来,我们需要为特殊事件创建一个 DataFrame。这与你在第六章预测假日效应中学习到的相同程序。在这种情况下,我们需要添加三个事件:2014 年世界杯、2017 年为拜仁慕尼黑签约,以及 2018 年世界杯。每个事件必须在'holiday'列中有一个名称,在'ds'列中有一个日期。

两次世界杯都持续了 31 天,因此我们将指定第一个日期并将'upper_window'设置为31。我们将'lower_window'保持为0。对于最后一个事件,与一支新球队签约,我们将添加两周的窗口,以便慷慨地假设签约拜仁慕尼黑的影响将持续影响他的帖子几天:

wc_2014 = pd.DataFrame({'holiday': 'World Cup 2014',
                       'ds':pd.to_datetime(['2014-06-12']),
                       'lower_window': 0,
                       'upper_window': 31})
wc_2018 = pd.DataFrame({'holiday': 'World Cup 2018',
                       'ds›:pd.to_datetime(['2018-06-14']),
                       'lower_window': 0,
                       'upper_window': 31})
signing = pd.DataFrame({'holiday': 'Bayern Munich',
                       'ds':pd.to_datetime(['2017-07-11']),
                       'lower_window': 0,
                       'upper_window': 14})
special_events = pd.concat([wc_2014, wc_2018, signing])

现在,我们需要指定我们的自定义变化点。我们可以简单地传递 Prophet 一个日期列表。任何 pandas 识别为有效日期时间格式的日期都可以使用:

changepoints = ['2014-06-12',
                '2014-07-13',
                '2017-07-11',
                '2017-07-31',
                '2018-06-14',
                '2018-07-15']

对于这些特殊事件中的每一个,我们在事件开始时添加一个潜在变化点,在事件结束时添加一个。这个决定的理由是我们需要考虑到每张照片的点赞数将遵循某种趋势,这种趋势与账户的关注者数量成比例,直到趋势被特殊事件打破。

在特殊事件期间,关注者的数量将以更高的速度增加,因此每张照片的点赞数也将增加,需要一条新的趋势线。在活动结束后,新关注者的增加速度将显著放缓,因此我们需要在这个时候添加第三条趋势线——结果是三条不同的趋势斜率,由两个趋势变化点将它们连接起来。

在创建特殊事件并确定潜在变化点后,我们接下来实例化 Prophet 对象,同时传递这些特殊事件和变化点。在这个例子中,我们将季节性设置为乘法。这是计数数据,如第五章处理季节性中讨论的那样,计数数据通常是乘法的。

然而,在这个案例中使用加性季节性是有道理的——有可能增加的点赞数来自因世界杯而访问罗德里格斯个人资料的未关注者,但他们并没有随后关注,这将是一个加性效应,而不是来自当前关注者的活动增加,这可能是由于 Instagram 的算法性信息流排序,这将是一个乘性效应。在任何情况下,以下程序都是相同的。

我们决定通过去除季节性来简化我们的模型,因此我们将yearly_seasonalityweekly_seasonality都设置为False。你可能想知道为什么我们没有季节性还要设置seasonality_mode——这是因为seasonality_mode也会影响节假日。

最后,我们将变化点的先验尺度设置为1,因为我们想稍微放松一下正则化(你可以自由地尝试这个数字;我发现默认值对这份数据来说过于严格),并将我们的变化点列表传递给changepoints参数:

model = Prophet(seasonality_mode='multiplicative',
                holidays=special_events,
                yearly_seasonality=False,
                weekly_seasonality=False,
                changepoint_prior_scale=1,
                changepoints=changepoints)

我们现在将继续像之前的例子一样,通过调用模型的fitpredict方法来继续。在这个例子中,我们不是在预测未来,但如果你想要预测,你需要添加任何你预期的未来特殊事件。最后,让我们绘制我们的预测和成分图来观察结果:

model.fit(df)
forecast = model.predict()
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()
fig2 = model.plot_components(forecast)
plt.show()

首先,是我们的预测:

图 8.11 – 詹姆斯·罗德里格斯预测

图 8.11 – 詹姆斯·罗德里格斯预测

尽管我们在模型中做了简化,但趋势与数据拟合得非常好。现在,让我们看看我们的成分图:

图 8.12 – 詹姆斯·罗德里格斯成分图

图 8.12 – 詹姆斯·罗德里格斯成分图

节假日图中可以看出,两次世界杯都为詹姆斯·罗德里格斯账户的每篇帖子带来了大约 200%的点赞数增长。当他签约拜仁慕尼黑时,他看到了一个更加适度但仍然令人印象深刻的点赞数翻倍。趋势线反映了这些变化。

在每次世界杯期间,他在帖子上的点赞数迅速增加,而点赞数的增长速度放缓,但保持了比事件之前更高的基线。Prophet 确定在每个世界杯前后需要两个变化点,但发现新球队的公告只对趋势产生了显著的影响。

处理变化点位置还有另一种方法,这是一种混合技术,将自定义变化点与 Prophet 的默认行为相结合。使用这种方法,你会创建一个均匀分布的变化点网格,就像 Prophet 默认做的那样,并用你的自定义变化点来丰富它。让我们再举一个例子来看看如何做到这一点。

在 Prophet 的源代码中,有一个创建潜在变化点网格的类方法,称为 set_changepoints。如果在 fit 命令中没有指定变化点,这个方法会在自动调用。以下函数模仿了 set_changepoints 方法,使我们能够在 Prophet 类外部创建潜在变化点网格。我们还需要导入 numpy 库来在这个函数中使用:

import numpy as np
def set_changepoints(df, n_changepoints=25,
                     changepoint_range=0.8):
    df = df.sort_values('ds').reset_index(drop=True)
    hist_size = int(np.floor(df.shape[0] * \
                             changepoint_range))
    if n_changepoints + 1 > hist_size:
        n_changepoints = hist_size - 1
        print(‹n_changepoints greater than number of '+
              'observations. Using {}.'\
              .format(n_changepoints))
    if n_changepoints > 0:
        cp_indexes = (np.linspace(0,
                                  hist_size - 1,
                                  n_changepoints + 1).
                      round().astype(np.int))
        changepoints = df.iloc[cp_indexes]['ds'].tail(-1)
    else:
        # set empty changepoints
        changepoints = pd.Series(pd.to_datetime([]),
                                 name=›ds›)
    return changepoints

这个函数需要三个参数。第一个是你的 Prophet DataFrame,包含 'ds''y' 列。第二个参数是要创建的变化点数量,默认值与 Prophet 使用的相同,为 25,第三个参数是变化点范围,同样默认为 Prophet 的 0.8。这将返回一个包含潜在变化点位置的 pandas 系列对象。你只需将你的自定义变化点追加到它上面即可。

使用这个函数,让我们在数据的第一个 80% 中创建五个等间距的变化点,然后使用前一个例子中的六个特殊事件变化点丰富自动变化点:

changepoints = set_changepoints(df, 5, 0.8)
new_changepoints = pd.Series(pd.to_datetime(['2014-05-02',
                                            '2014-08-25',
                                            '‹2017-07-31',
                                            '2018-06-14',
                                            '2018-06-04',
                                            '2018-07-03']))
changepoints = changepoints = pd.concat([changepoints, 
new_changepoints])
changepoints = \
changepoints.sort_values().reset_index(drop=True)

现在,让我们重新创建我们之前的模型,但这次,发送我们新的变化点列表:

model = Prophet(seasonality_mode='multiplicative',
                holidays=special_events,
                yearly_seasonality=False,
                weekly_seasonality=False,
                changepoint_prior_scale=1,
                changepoints=changepoints)
model.fit(df)
forecast = model.predict()
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()

现在,我们可以看到 Prophet 使用的变化点比以前多得多:

图 8.13 – 使用混合自动/手动潜在变化点的预测

图 8.13 – 使用混合自动/手动潜在变化点的预测

我们还有一个非常灵活的趋势线;也许它过度拟合了。这是你需要,作为分析师,去确定的事情,但作为如何将你自己的自定义变化点与自动选择的潜在变化点网格结合的演示,这个例子就足够了。

摘要

在本章中,你学习了如何通过使用变化点来控制趋势线的拟合。首先,你使用了 Divvy 数据来查看 Prophet 如何自动选择潜在变化点位置,以及你如何通过修改默认的潜在变化点数量和变化点范围来控制这一点。

然后,你学习了一种更稳健的方式来通过正则化控制 Prophet 的变化点选择。就像季节性和节假日一样,变化点通过设置先验尺度进行正则化。接着,你研究了詹姆斯·罗德里格斯在 Instagram 上的数据,学习了如何在 2014 年和 2018 年世界杯期间及之后如何对他收到的每条帖子的点赞数进行建模。最后,你学习了如何将这两种技术结合起来,并用你自定义的变化点丰富自动选择的潜在变化点网格。

在下一章中,我们再次查看 Divvy 数据,但这次,我们将包括温度和天气条件等附加列,以便学习如何在 Prophet 预测中包含额外的回归因子。

第九章:包含额外回归因子

在你的第一个模型中,第二章使用 Prophet 入门,你仅使用日期(但没有其他信息)来预测未来的二氧化碳水平。后来,在第六章预测节假日影响中,你学习了如何将节假日作为附加信息添加,以进一步细化你在芝加哥 Divvy 自行车共享网络中对自行车骑行量的预测。

在 Prophet 中实现节假日的方式实际上是添加二元回归因子的一个特殊情况。实际上,Prophet 包括了一种通用的方法来添加任何额外的回归因子,包括二元和连续的。

在本章中,你将通过将其作为额外回归因子包括进来,用天气信息丰富你的 Divvy 数据集。首先,你将添加二元天气条件来描述阳光、云或雨的存在或不存在,然后你将引入连续的温度测量。使用额外的回归因子可以使你包含更多信息来告知你的模型,这会导致更强的预测能力。在本章中,你将学习以下主题:

  • 添加二元回归因子

  • 添加连续回归因子

  • 解释回归系数

技术要求

本章中示例的数据文件和代码可以在github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition找到。

添加二元回归因子

在考虑额外回归因子时,无论是二元还是连续的,首先需要考虑的是你必须知道整个预测期间的未来值。这并不是节假日的问题,因为我们确切地知道每个未来节假日的具体时间。所有未来值必须已知,就像节假日一样,或者必须单独进行预测。然而,当使用已经被预测的数据构建预测时,你必须小心:第一次预测的错误会累积到第二次预测中,错误会持续累积。

然而,如果一个变量的预测比另一个变量容易得多,那么这可能是这些堆叠预测有意义的案例。分层时间序列是一个可能有用的情况的例子:你可能通过预测一个时间序列更可靠的日值,例如,并使用这些值来预测另一个更难预测的时间序列的小时值。

在本章的例子中,我们将使用天气预报来丰富我们的 Divvy 预测。这种额外的回归器是可能的,因为我们通常确实有一周左右的相当可靠的天气预报可用。在这本书的其他例子中,当我们使用 Divvy 数据时,我们经常预测整整一年。然而,在本章中,我们只会预测两周。让我们对芝加哥的天气预报员慷慨一些,并假设他们将在这一时间段内提供准确的预报。

首先,让我们导入必要的包并加载数据:

import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet
df = pd.read_csv('divvy_daily.csv')

请参考第五章中的图 5.6处理季节性,以查看此数据中每天骑行的图表。第五章中的图 5.7显示了此文件中包含的数据的摘录。到目前为止,在这本书中,我们总是排除了此数据集中关于天气和温度的两个列,但这次我们将使用它们。对于我们的第一个例子,让我们考虑天气条件。通过计算每个条件在数据集中出现的次数,我们可以看到它们的频率:

print(df.groupby('weather')['weather'].count())

前面的print语句的输出如下:

图 9.1 – Divvy 数据集中天气条件的计数

图 9.1 – Divvy 数据集中天气条件的计数

通过按天气分组数据并按计数聚合,我们可以看到每种条件报告的天数。晴朗的天气出现了41天,而多云的天气远远是最常见的,有1346次出现。Not clear只报告了两次,而rain or snow出现了69次。

既然我们已经了解了我们正在处理的数据,让我们将其加载到我们的 DataFrame 中。我们还将加载temperature列,尽管我们直到下一个例子查看连续列(其值可能存在于连续体上)时才会使用它。

要加载weather列,我们将使用 pandas 的get_dummies方法将其转换为四个10 – 实际上,这是一个标志,表示条件是否存在:

df['date'] = pd.to_datetime(df['date'])
df.columns = ['ds', 'y', 'temp', 'weather']
df = pd.get_dummies(df, columns=['weather'], prefix='',
                    prefix_sep='')

我们现在可以显示 DataFrame 的前五行,以查看前面的代码做了什么:

df.head()

head语句的输出应该如下所示:

图 9.2 – 带有虚拟天气列的 DataFrame

图 9.2 – 带有虚拟天气列的 DataFrame

现在,你可以看到weather列中的每个唯一值都已转换为一个新的列。现在让我们实例化我们的模型,将季节性模式设置为乘法,并将年季节性设置为具有4个傅里叶阶数,就像我们在前面的章节中所做的那样。

我们还将使用add_regressor方法添加额外的回归器。作为此方法的参数,您必须传递回归器的名称,这是 DataFrame 中相应列的名称。您还可以使用prior_scale参数来正则化回归器,就像您对假日、季节性和趋势变化点所做的那样。如果没有指定先验尺度,则将使用holidays_prior_scale,默认值为10

您还可以指定回归器应该是加法还是乘法。如果没有指定,则回归器将采用seasonality_mode中声明的值。最后,该方法有一个standardize参数,默认情况下取'auto'字符串。这意味着如果列不是二元的,则将对列进行标准化。您可以通过将其设置为TrueFalse来显式设置标准化。在这个例子中,所有默认值都将很好地工作。

为了使其清晰,我将在第一个add_regressor调用中明确声明所有参数,而对于其余的,我们只将声明回归器的名称,并接受所有默认值。

我们必须为每个额外的回归器调用一次add_regressor,但请注意,我们省略了cloudy回归器。为了 Prophet 能够得到准确的预测结果,这并不是严格必要的。然而,由于包含所有四个二元列将引入多重共线性,这使得解释每个条件的个体效应变得困难,因此我们将排除其中一个。但是,Prophet 对额外回归器中的多重共线性相当稳健,所以它不应该对您的最终结果产生重大影响。

当我们之前调用pd.get_dummies时,我们可以指定drop_first=True参数来排除一个条件,但我决定不这样做,这样我们就可以自己选择要排除的列。cloudy条件是最频繁的,因此,通过排除它,我们实际上是在声明cloudy默认天气条件,其他条件将作为对其的偏差来陈述:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4)
model.add_regressor(name='clear',
                    prior_scale=10,
                    standardize='auto',
                    mode='multiplicative')
model.add_regressor('not clear')
model.add_regressor('rain or snow')

记住我们需要为我们的额外回归器提供未来数据,而我们只预测两周,我们需要人为地将我们的训练数据减少两周,以模拟有两个未来周的天气数据但没有乘客数据。为此,我们需要从 Python 的内置datetime包中导入timedelta

使用 pandas 中的布尔索引,我们将通过选择所有小于最终日期(df['ds'].max())减去两周(timedelta(weeks=2))的日期来创建一个新的训练数据 DataFrame,称为train

from datetime import timedelta
# Remove final 2 weeks of training data
train = df[df['ds'] < df['ds'].max() - timedelta(weeks=2)]

到目前为止,我们实际上是在说我们的数据不是在 2017 年 12 月 31 日结束(正如我们的df DataFrame 所做的那样),而是在 2017 年 12 月 16 日结束,并且我们有这两周缺失的天气预报。我们现在在这个train数据上拟合我们的模型,并创建一个包含 14 天的future DataFrame。

在这一点上,我们需要将这些附加回归器列添加到我们的future DataFrame 中。因为我们创建的是train DataFrame 而不是直接修改我们的原始df DataFrame,所以这些天气值存储在df中,我们可以将它们用于我们的future DataFrame。最后,我们将对未来进行预测。

预测图将类似于我们之前的 Divvy 预测,所以我们就跳过它,直接看components图:

model.fit(train)
future = model.make_future_dataframe(periods=14)
future['clear'] = df['clear']
future['not clear'] = df['not clear']
future['rain or snow'] = df['rain or snow']
forecast = model.predict(future)
fig2 = model.plot_components(forecast)
plt.show()

这次,您将看到一个新的子图与其它组件一起显示。以下图像是完整components图的裁剪,只显示了年度季节性和这个新组件:

图 9.3 – 二元附加回归器的裁剪组件图

图 9.3 – 二元附加回归器的裁剪组件图

被裁剪的趋势、每周季节性和年度季节性看起来与我们之前在这个数据集中看到的基本相同。然而,我们在components图中增加了一个新元素,称为extra_regressors_multiplicative。如果我们指定了一些回归器为additive,我们在这里将看到第二个子图,称为extra_regressors_additive

在值为 0%的日期上,这些是我们的基准日期,当时天气多云,我们将其排除在附加回归器之外。其他日期是那些天气偏离多云的日期,我们将其包括在内。我们将在稍后更深入地探讨这个问题。但首先,让我们将温度纳入我们的模型,并添加一个连续回归器

添加连续回归器

在这个例子中,我们将从上一个例子中取出所有内容,并简单地添加一个温度回归器。让我们首先查看温度数据:

图 9.4 – 时间上的芝加哥温度

图 9.4 – 时间上的芝加哥温度

前面的图表并没有什么令人惊讶的地方;夏季气温上升,冬季气温下降。它看起来很像第五章中的图 5.6处理季节性,但没有那个上升趋势。显然,Divvy 的骑行量和气温的升降是一致的。

添加温度,一个连续变量,与添加二元变量没有区别。我们只需在 Prophet 实例中添加另一个add_regressor调用,指定名称为'temp',并将温度预测包含在我们的future DataFrame 中。像之前一样,我们在我们创建的train DataFrame 上拟合我们的模型,该 DataFrame 排除了最后两周的数据。最后,我们绘制组件图以查看结果:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=4)
model.add_regressor('temp')
model.add_regressor('clear')
model.add_regressor('not clear')
model.add_regressor('rain or snow')
model.fit(train)
future = model.make_future_dataframe(periods=14)
future['temp'] = df['temp']
future['clear'] = df['clear']
future['not clear'] = df['not clear']
future['rain or snow'] = df['rain or snow']
forecast = model.predict(future)
fig2 = model.plot_components(forecast)
plt.show()

现在,extra_regressors_multiplicative图显示了与我们的temperature图相同的波动:

图 9.5 – 二元和连续附加回归器的裁剪组件图

图 9.5 – 二元和连续附加回归器的裁剪组件图

还要注意,在图 9.3中,yearly图达到了 60%的效果幅度峰值。然而,现在我们可以看到温度占了一些效果。图 9.5中的yearly图显示了一个 30%的峰值效果,而extra_regressors_multiplicative图显示在某些夏日日期上增加了 40%,而在某些冬日日期上乘客量大幅下降了 80%。为了进一步分析,我们现在需要讨论如何解释这些数据。

解释回归器系数

现在我们来看看如何检查这些额外回归器的影响。Prophet 包括一个名为utilities的包,其中有一个在这里非常有用的函数,称为regressor_coefficients。现在让我们导入它:

from prophet.utilities import regressor_coefficients

使用它很简单。只需将模型作为参数传递,它将输出一个 DataFrame,其中包含关于模型中包含的额外回归器的某些有用信息:

regressor_coefficients(model)

让我们看看这个 DataFrame:

图 9.6 – 回归器系数 DataFrame

图 9.6 – 回归器系数 DataFrame

它为模型中的每个额外回归器提供一行。在这种情况下,我们有一个用于温度的,还有三个用于我们包含的天气条件。regressor_mode列自然会有additivemultiplicative的字符串,这取决于每个特定回归器对'y'的影响。预标准化回归器的平均值(即原始输入数据)保存在center列中。如果回归器没有标准化,那么该值将为零。

coef列是你真正需要关注的。它表示系数的预期值 – 即,回归器增加一个单位对'y'的预期影响。在前面的 DataFrame 中,tempcoef0.012282。这个系数告诉我们,对于比center(在本例中为53.4)高出的每一度,对乘客量的预期影响将是0.012282,即增加 1.2%。

对于雨或雪行,这是一个二元回归器,它告诉我们,在那些雨天或雪天,乘客量将比多云天气低 20.6%,因为这是我们留下的回归器。如果我们包括了所有四种天气条件,为了解释这个值,你会说乘客量将比如果不包括天气条件预测的同一天的值低 20.6%。

最后,coef_lowercoef_upper列分别表示围绕系数的不确定性区间的下限和上限。只有当mcmc_samples设置为大于零的值时,它们才有意义。mcmc_samples保留默认值,在这些例子中,coef_lowercoef_upper将等于coef

现在,为了总结,我们可以使用我们最初在第六章,“预测假日效应”中使用的plot_forecast_component函数单独绘制这些额外回归变量。在从 Prophet 的plot包中导入它之后,我们将遍历regressor_coefficients DataFrame 中的每个回归变量来绘制它:

from prophet.plot import plot_forecast_component
fig, axes = plt.subplots(
                        len(regressor_coefficients(model)),
                        figsize=(10, 15))
for i, regressor in enumerate(
    regressor_coefficients(model)['regressor']):
    plot_forecast_component(model,
                            forecast,
                            regressor,
                            axes[i])
plt.show()

我们将这些数据作为子图绘制在一个图中,结果如下所示:

图 9.7 – Divvy 额外回归变量图

图 9.7 – Divvy 额外回归变量图

最后,我们可以单独可视化这些回归变量的影响。这些图的幅度应该与使用regressor_coefficients函数在图 9**.6中创建的 DataFrame 中的coef值相匹配。

关于 Prophet 中的额外回归变量还有一个最后的注意事项:它们总是被建模为线性关系。这意味着,例如,我们发现温度这个额外回归变量每增加一度,乘客量就会增加 1.2%,这是在建模一个将持续到无限的趋势。也就是说,如果温度突然升高到华氏 120 度,我们无法改变线性关系并告知 Prophet,由于天气变得非常热,乘客量可能会减少。

虽然这在目前 Prophet 的设计中是一个限制,但在实际应用中,这通常并不是一个大问题。线性关系通常很好地代表了实际关系,尤其是在数据范围较小的情况下,并且会给你的模型添加很多额外的信息来丰富你的预测。

摘要

在本章中,你学习了一种通用的方法来添加任何额外的回归变量,这些变量是在之前学习的。你了解到添加二元回归变量(如天气条件)和连续回归变量(如温度)都使用相同的add_regressor方法。你还学习了如何使用 Prophet 的utilities包中的regressor_coefficients函数来检查你的额外回归变量的影响。

尽管你现在可能想要向你的预测中添加各种额外的回归变量,但你同时也了解到 Prophet 要求所有额外的回归变量在未来的定义值,否则就没有信息来告知预测。这就是为什么我们使用天气数据时只预测了 2 周。

在下一章中,我们将探讨 Prophet 如何处理异常值,以及你如何可以自己更多地控制这个过程。

第十章:考虑异常值和特殊事件

异常值是指任何在一条或多条不同轴上显著偏离其他数据点的数据点。异常值可能是由于传感器校准不当产生无效数据,或者在数据输入时键盘上的手指滑动造成的错误数据,或者它们可能是准确记录的数据,但由于各种原因(例如,龙卷风经过风速传感器)意外地与历史趋势相差甚远。

这些不寻常的测量值会动摇任何统计或机器学习模型,因此纠正异常值是数据科学和统计学中的一个挑战。幸运的是,Prophet 通常在处理轻微异常值方面很稳健。然而,对于极端异常值,Prophet 可能会遇到两个问题——一个与季节性有关,另一个与不确定性区间有关。

在本章中,您将看到这两种问题的示例,并学习如何减轻它们对您预测的影响。您还将学习一些自动化异常值检测的技术,最后,您将应用在第八章中学习到的经验,即影响趋势变化点,以在模型中保留异常值,但指示 Prophet 不要修改趋势或季节性以适应它们。

本章将涵盖以下主题:

  • 纠正导致季节性波动的异常值

  • 纠正导致宽不确定性区间的异常值

  • 自动检测异常值

  • 将异常值建模为特殊事件

  • 建模冲击,例如 COVID-19 封锁

技术要求

本章中示例的数据文件和代码可以在github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition找到。

纠正导致季节性波动的异常值

本章我们将使用一个新的数据集来查看异常值——国家地理 Instagram 账户@NatGeo上每天帖子平均点赞数,这些数据是在 2019 年 11 月 21 日收集的。

我选择这个数据集是因为它显示了几个显著的异常值,这些异常值在以下图中标记:

图 10.1 – 国家地理 Instagram 账户上的异常值

图 10.1 – 国家地理 Instagram 账户上的异常值

每条虚线垂直线都表示时间序列发生了显著偏差的时刻。从左数第二条线表示2015 年夏季发生了根本性的趋势变化,但其他四条线表示异常值,最后两个异常值跨越了较宽的时间范围。我们将特别关注发生在2016 年中期的线条,具体来说是 8 月份。这代表了最极端的异常值。2014 年的异常值可以安全忽略,因为它们对预测的影响不大。2017 年2019 年的异常值看起来可能是季节性影响,所以我们将让年度季节性来捕捉它们。

事实上,在 2016 年 9 月,国家地理杂志(National Geographic)出版了一本书,NatGeo: 最受欢迎的 Instagram 照片。似乎在这之前的一个月,国家地理杂志进行了一些营销活动,从而提高了其 Instagram 账号的点赞数。

正如我们在第八章中看到的,影响趋势变化点,詹姆斯·罗德里格斯(James Rodríguez)的账号在他的世界杯亮相期间也看到了点赞数的增加。然而,在他的情况下,这些事件在其结束时都伴随着更高的点赞基数——发生了显著的趋势变化。相比之下,国家地理杂志在 8 月份的营销工作并没有产生持久性的趋势变化,尽管它确实增加了点赞数。

峰值代表了先知(Prophet)中异常值可能引起的第一种问题——它们可以主导季节性曲线。让我通过绘制先知预测图来展示我的意思。让我们导入必要的库,加载数据,并绘制预测图。我们将使用乘法季节性并将年度季节性的傅里叶阶数降低到6

import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet
from prophet.plot import add_changepoints_to_plot
df = pd.read_csv('instagram_natgeo.csv')
df['Date'] = pd.to_datetime(df['Date'])
df.columns = ['ds', 'y']
model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=6)
model.fit(df)
future = model.make_future_dataframe(periods=365 * 2)
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

这些异常值导致 Prophet 模型在每年的 8 月份出现点赞数的峰值:

图 10.2 – 带有异常值的国家地理(NatGeo)预测

图 10.2 – 带有异常值的国家地理(NatGeo)预测

确实,在2013 年2015 年2017 年2019 年的 8 月份也看到了点赞数的增加期,但偶数年份并没有。虽然可以预期会有一些季节性变化,但不会这么多。更糟糕的是,这种影响将永远影响未来。你可以通过查看年度季节性图来了解这种影响的显著性:

from prophet.plot import plot_yearly
plot_yearly(model, figsize=(10.5, 3.25))
plt.show()

在这里,你可以清楚地看到 8 月的峰值:

图 10.3 – 先知(Prophet)的年度季节性及异常值

图 10.3 – 先知(Prophet)的年度季节性及异常值

在尝试将年度季节性拟合到2016 年的这些异常值时,Prophet 允许8 月对预期点赞数的增加贡献超过 20%。我们看到那些频繁的 8 月增加,所以我们确实希望 Prophet 能够模拟它们,但2016 年的异常值占主导地位。

解决方案很简单,就是移除这些点。Prophet 处理缺失数据非常出色,因此引入一个小间隔不会造成任何问题。在第四章《处理非每日数据》中,你学习了如何通过从future DataFrame 中移除这些间隔来处理常规间隔。然而,在这种情况下,只要我们有其他年份的八月数据,我们就不需要采取这种预防措施。

看起来第一个主要异常是在 7 月 29 日,最后一个是在 9 月 1 日,所以我们将使用pandas的布尔索引排除这些日期之间的数据:

df2 = df[(df['ds'] < '2016-07-29') |
         (df['ds'] > '2016-09-01')]

这个新的df2与我们的原始df完全相同,只是排除了那些异常值。让我们像之前一样构建相同的 Prophet 模型,但只是将之前的 DataFrame df替换为这个新的df2

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=6)
model.fit(df2)
future = model.make_future_dataframe(periods=365 * 2)
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

你可以从这张图中看到 2016 年八月的月度间隔。预报简单地穿过它:

图 10.4 – 移除异常值后的 NatGeo 预报

图 10.4 – 移除异常值后的 NatGeo 预报

这个新的预报也显示出显著的季节性,但我们确实预料到这一点,因为 NatGeo 的点赞在夏季通常更高。为了量化这个预报与之前的差异,我们还绘制了年季节性:

plot_yearly(model, figsize=(10.5, 3.25))
plt.show()

它的形状与图 10.3非常相似,但八月的峰值不那么夸张:

图 10.5 – 移除异常值后的 Prophet 年季节性

图 10.5 – 移除异常值后的 Prophet 年季节性

现在,八月的峰值几乎减半;它只是增加了 10%以上。这更接近于在没有外部(且非重复)营销推动下发布国家地理书籍之前预期的结果。

现在,让我们看看第二种异常问题。

纠正导致不确定性区间过宽的异常值

在我们之前查看的第一种异常类型中,问题是季节性受到影响,并且永远改变了预报中的yhat(如果你还记得从第二章《使用 Prophet 入门》中,yhat是 Prophet 的forecast DataFrame 中包含的未来日期的预测值)。在这个第二个问题中,yhat的影响最小,但不确定性区间显著扩大。

为了模拟这个问题,我们需要稍微修改一下我们的 NatGeo 数据。假设 Instagram 在其代码中引入了一个错误,将每篇帖子的点赞上限设置为 100,000。这个错误在一年后才被发现并修复,但不幸的是,所有超过 100,000 的点赞都丢失了。这样的错误看起来是这样的:

图 10.6 – 国家地理 Instagram 账号上的点赞上限

图 10.6 – 国家地理 Instagram 账号上的点赞上限

你可以使用以下代码自己模拟这个新的数据集:

df3 = df.copy()
df3.loc[df3['ds'].dt.year == 2016, 'y'] = 100000

这将 2016 年所有帖子的点赞都设置为 100,000。为了看看这会造成什么问题,我们再次构建与之前相同的模型:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=6)
model.fit(df3)
future = model.make_future_dataframe(periods=365 * 2)
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()

在这个例子中,我们将变化点添加到图表中,因为这就是引入错误的地方,如下面的图表所示:

图 10.7 – 包含异常值的 NatGeo 预测

图 10.7 – 包含异常值的 NatGeo 预测

随着时间的推移,未来的不确定性急剧增加。在之前的例子中,Prophet 使用季节性来模拟异常值,向年度季节性组件添加极端数据。然而,在这个例子中,Prophet 使用趋势变化点来模拟异常值。季节性不受影响。

我们将在第十一章“管理不确定性区间”中全面讨论不确定性,但简而言之,Prophet 所做的是查看历史变化点的频率和幅度,并预测未来的不确定性,假设未来的变化点可能以相同的频率和幅度发生。因此,如您在图 10.7中看到的戏剧性历史变化点将导致戏剧性的未来不确定性,因为 Prophet 不确定它们是否会再次发生。

幸运的是,解决方案与之前的情况相同——简单地移除不良数据。在之前的例子中,我们移除了包含不良数据的 DataFrame 中的行,但在这个例子中,我们将'y'值设置为None

df3.loc[df3['ds'].dt.year == 2016, 'y'] = None

这对我们趋势或季节性没有影响。它产生影响的方面是,现在,我们不再跳过forecast DataFrame 中的那些日期,而是在这些日期上预测值。您可以在即将出现的图 10.8中看到这一点。与直接穿过缺失数据的预测线不同,它遵循季节性。

让我们再次重建我们的模型,使用df3 DataFrame:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=6)
model.fit(df3)
future = model.make_future_dataframe(periods=365 * 2)
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()

图 10.7相比,我们现在已经驯服了预测的不确定性,如下面的图表所示:

图 10.8 – 移除异常值的 NatGeo 预测

图 10.8 – 移除异常值的 NatGeo 预测

如前所述,我们在 2016 年有缺失数据,但 Prophet 仍然做出了预测并绘制了预测值。这是将那些缺失值设置为None而不是删除它们的结果。将图 10.8图 10.4进行比较,其中缺失数据没有预测值,图表直接穿过它们形成一条直线。

从数学上讲,这对您的未来预测没有影响;它只是将预测值应用于那些缺失的值。是否希望这些缺失值在future DataFrame 中被预测或忽略,完全取决于您。

自动检测异常值

在到目前为止的这些例子中,我们通过简单的数据可视化检测到异常值,并应用常识。在一个完全自动化的环境中,定义我们作为人类直观行为的相关逻辑规则可能很困难。异常值检测是分析师时间的好用途,因为我们人类能够比计算机使用更多的直觉、领域知识和经验。但是,由于 Prophet 是为了减少分析师的工作量并尽可能实现自动化而开发的,我们将探讨一些自动识别异常值的技术。

Winsorizing

第一种技术被称为Winsorization,以统计学家查尔斯·P·Winsor 的名字命名。有时它也被称为截尾。Winsorization 是一种钝工具,通常不适用于非平稳趋势。Winsorization 要求分析师指定一个百分位数;所有高于或低于该百分位数的数值都被迫保持在百分位数的值上。

Trimming是一种类似的技术,不同之处在于移除了极端值。这些技术之间的差异可以通过这个简单的例子看到,在这个例子中,异常值是三个图表每侧最极端的两个点:

图 10.9 – Winsorization 与 Trimming 的比较

图 10.9 – Winsorization 与 Trimming 的比较

小贴士

在统计学中,单词stationary意味着均值、方差和自相关结构不会随时间变化。在具有平稳趋势的时间序列中,均值不会随时间变化,因此满足了一个(以及可能每个)平稳性的要求。对于平稳数据,异常值通常可以用均值来替换;然而,由于平稳性的要求,这种技术通常不适用于缺乏平稳趋势的时间序列。

为了举一个具体的例子,请参考第 2.2 节中的图 2.2,即第二章,“Prophet 入门”,并查看莫纳罗亚山二氧化碳水平的 Keeling 曲线,想象用全数据集的均值替换其中一个最终值——比如说,2015 年的值。这将导致 2015 年一个荒谬的低值,大约为 360,这是 20 年来未见过的。

让我们看看如何将 Winsorization 应用于我们的国家地理数据。stats包有一个 Winsorization 工具,所以我们将使用它。请注意,我们正在丢弃所有空值,因为这些值不适用于此函数。我们将下限设置为0,因此下限不受影响,并将上限设置为.05,因此影响的是第五个百分位数:

from scipy import stats
df4 = df.copy().dropna()
df4['y'] = stats.mstats.winsorize(df4['y'],
                                  limits=(0, .05), axis=0)

Winsorized 国家地理数据看起来是这样的,受影响的数据点用x标记:

图 10.10 – Winsorized 数据

图 10.10 – Winsorized 数据

标准差

由于 Winsorization 的限制是通过百分位数设置的,因此没有考虑到数据的自然方差——也就是说,一些数据集非常紧密地围绕平均值分布,而另一些则非常分散。设置百分位数限制不会考虑这一点。因此,有时使用标准差比使用百分位数更有意义。这与 Winsorization 非常相似,如果设置限制得当,可以产生相同的效果。

在上一节中我们进行 Winsorization 时,迫使异常值采用上限的值。在这种情况下,我们将简单地移除异常值。我们正在使用 SciPy stats 包中的 zscore 函数来消除那些比平均值高 1.65 个标准差的数据点;在正态分布中,这个上限值将划分 95% 的数据,这是我们之前设置的相同限制:

df5 = df.copy().dropna()
df5 = df5[(stats.zscore(df5['y']) < 1.65)]

在这种情况下,这两种技术几乎具有相同的结果,只是在这里,我们正在修剪数据:

图 10.11 – 使用标准差修剪的数据

图 10.11 – 使用标准差修剪的数据

当数据具有趋势时,这种方法也是一个不良的拟合。显然,在具有上升趋势的时间序列中,位于后面的点比位于前面的点更有可能被修剪。下一项技术将考虑这一点。

移动平均

我们刚刚查看的是从整个数据集的平均值向外扩展的标准差数量,并看到了为什么在存在趋势时它失败的原因。在这个方法中,我们将使用移动平均,这样我们实际上是在本地化我们的平均值和标准差计算,只将这些计算应用于时间上彼此接近的数据点。

在这个例子中,我们将再次使用标准差 1.65 的值来修剪数据的上下限。分析师还需要决定窗口大小。这是用于计算时收集的周围数据点的数量。设置得太小,一组异常值将不会被移除。设置得太大,我们将接近之前忽略趋势的技术。

我们在这里使用 300。我们将使用 pandas 的 rolling 方法来使用滚动窗口找到平均值和标准差。然后,我们使用这些值计算上下限,并使用这些界限过滤我们的 DataFrame:

df6 = df.copy().dropna()
df6['moving_average'] = df6.rolling(window=300,
                                    min_periods=1,
                                    center=True,
                                    on='ds')['y'].mean()
df6['std_dev'] = df6.rolling(window=300,
                             min_periods=1,
                             center=True,
                             on='ds')['y'].std()
df6['lower'] = df6['moving_average'] - 1.65 * \
               df6['std_dev']
df6['upper'] = df6['moving_average'] + 1.65 * \
               df6['std_dev']
df6 = df6[(df6['y'] < df6['upper']) & \
          (df6['y'] > df6['lower'])]

现在我们正在获得更精细的异常值移除,如下面的图表所示:

图 10.12 – 使用移动平均修剪的数据

图 10.12 – 使用移动平均修剪的数据

这种方法的强大优势是它考虑到了趋势。

错误标准差

我们将考虑的最终方法是最精确的。让我们回到定义异常值的问题——它是一个你不期望的值。直观上,当我们通过视觉检查数据并移除点时,我们就知道了这一点。那么,你如何告诉计算机期望什么?当然,你构建一个预测。

Prophet 的 forecast DataFrame 在 yhat 列中进行预测,但它还包括 yhat_upperyhat_lower 列。这些不确定性区间默认设置为 80%,但您将在 第十一章管理不确定性区间 中学习如何修改它们。如果我们接受不确定性区间内包含的任何误差,我们可以将超出这些界限的任何异常值宣布为异常值,因为它是不预期的。

事实上,移动平均是一种粗略的预测技术;前一种方法确实基于误差项的偏差来去除异常值。通过使用 Prophet 来识别误差,我们允许季节性和其他效应包含在我们的预期结果中。

作为最精确的方法,这不幸也是最容易过度拟合的方法。如果您确实希望使用这种方法,请确保在使用新数据集时谨慎行事,并在完全自动化之前确保您喜欢结果。话虽如此,让我们看看如何编写代码。

我们的方法将是首先去除空值,以避免在将我们的 forecast DataFrame 与原始 DataFrame 进行比较时出现下游问题:

df7 = df.copy().dropna().reset_index()

接下来,我们在这些数据上构建一个 Prophet 模型,包括强大的正则化以确保我们不会过度拟合。请注意,没有必要预测未来。我们在这里包含 interval_width 参数是为了增加不确定性区间,以便更好地与之前的示例对齐;我们将在下一章中介绍这个参数:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=6,
                seasonality_prior_scale=.01,
                changepoint_prior_scale=.01,
                interval_width=.90)
model.fit(df7)
forecast = model.predict()

最后,我们创建一个 DataFrame,排除了那些 y 值大于 yhat_upper 或小于 yhat_lower 的值。这些将是我们的异常值:

df8 = df7[(df7['y'] > forecast['yhat_lower']) &
          (df7['y'] < forecast['yhat_upper'])]

最终的 DataFrame 将用于构建一个新的 Prophet 模型,无需担心异常值。这就是我们现在的数据看起来像这样:

图 10.13 – 从预测中去除误差的数据

图 10.13 – 从预测中去除误差的数据

我们确实去除了可能被认为是异常值的部分。如果我们使用了 Prophet 的默认不确定性区间,那么在这种情况下异常值去除可能过于激进。如果您将 图 10.13 与我们其他方法的图表进行比较,这个方法看起来最为精确 – 例如,它允许我们预期在夏季的高值,但去除那些不寻常的高值。

使用这种方法隐含地假设数据是平稳的并且具有恒定的方差,这在整个国家地理数据集中似乎是一个较差的假设,但在仅考虑 2016 年之后的数据时是一个合理的假设。随着时间的推移,完整的数据变得更加分散。这就是为什么在较晚的日期比早期日期删除了更多的数据点 – 这是在使用此方法时需要考虑的又一件事。

在本章中,我们已经从我们的数据中移除了异常值。然而,如果你认为这些异常值在你的模型中提供了一些有价值的信号,但你想控制其影响,你可以使用一种技术来保留这些异常值。这个技术使用 Prophet 的节假日功能。让我们看看如何操作。

将异常值建模为特殊事件

在 Prophet 中处理异常值还有最后一种方法;这是一种我们在第八章中使用的技巧,影响趋势变化点 – 我们可以将异常值声明为特殊事件,本质上是一个假期。通过将异常值放入holidays DataFrame 中,我们实际上指示 Prophet 将趋势和季节性应用于数据点,就像它们不是异常值一样,并在节假日项中捕捉趋势和季节性之外的额外变化。

如果你知道极端观测值是由于一些你预料不到的外部因素造成的,这将是有用的。这些外部因素可能是世界杯或大型营销活动,但也可能是神秘和未知的。你可以在模型中保留这些数据,但基本上可以忽略它们。一个额外的优点是,你可以模拟如果事件重复会发生什么。

我们将再次使用国家地理数据,但这次,将 2016 年 8 月的异常值系列标记为假期。如果这些额外的点赞是由于围绕他们书籍发布的营销活动,我们可以预测如果他们在稍后的日期重复类似的营销活动会发生什么。

我们在第六章中介绍了自定义假期的创建,预测节假日效应,因此这一步应该是一个复习。我们只是为 2016 年 8 月的营销活动创建两个假期,以及一个相同的、假设的 2020 年 6 月的营销活动。

注意,这两个事件具有相同的名称,'Promo event',所以 Prophet 知道将相同的效果应用于每个。它们持续的天数相同,尽管不必如此 – 假设事件每一天的节假日效应将与测量事件每一天的效应相匹配。

如果假设事件较短,效应将提前结束。如果假设事件较长,则效应将在测量事件的长度达到时结束。

我们首先以定义假期的相同方式定义促销活动:

promo = pd.DataFrame({'holiday': 'Promo event',
                      'ds': pd.to_datetime(['2016-07-29']),
                      'lower_window': 0,
                      'upper_window': 34})
future_promo = pd.DataFrame({'holiday': 'Promo event',
                      'ds': pd.to_datetime(['2020-06-01']),
                      'lower_window': 0,
                      'upper_window': 34})
promos = pd.concat([promo, future_promo])

接下来,我们使用本章中相同的参数构建我们的模型,除了将第一个promo DataFrame 发送到holidays参数:

model = Prophet(seasonality_mode='multiplicative',
                holidays=promo,
                yearly_seasonality=6)
model.fit(df)
future = model.make_future_dataframe(periods=365 * 2)
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

我们的预测完美地模拟了异常值的峰值,既没有让季节性失控(我们在本章中首先考虑的问题)或未来不确定性爆炸(第二个问题):

图 10.14 – 将异常值建模为特殊事件的 NatGeo 预测

图 10.14 – 将异常值建模为特殊事件的 NatGeo 预测

为了结束这个例子,让我们再尝试一个模型,但这次包括那个假设的促销活动:

model = Prophet(seasonality_mode='multiplicative',
                holidays=promos,
                yearly_seasonality=6)
model.fit(df)
future = model.make_future_dataframe(periods=365 * 2)
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

如果国家地理频道在 2020 年复制了这些促销活动,他们可以预期的未来预测如下:

图 10.15 – 假设促销活动下的 NatGeo 预测

图 10.15 – 假设促销活动下的 NatGeo 预测

仅用一个假期的实例进行训练,Prophet 已经完美地将假期效应与数据匹配,这可能是过度拟合的好方法。如果国家地理频道有几次类似的营销活动,他们可以将所有这些活动都建模为同一个假期,这样就可以平均化效应。

将异常值建模为特殊事件的技术甚至可以用来建模对整个时间序列的巨大冲击。在下一节中,我们将看到如何应用这些原则来建模 COVID-19 封锁对行人活动的影响。

建模如 COVID-19 封锁这样的冲击

到 2020 年中旬,世界各地的预测者对于未来几个月和几年的预测都感到迷茫。COVID-19 大流行彻底改变了全球的生活,以及许多时间序列。在线购物在 2020 年初的预测之外急剧飙升;Netflix 和 YouTube 等媒体的消费量急剧增加,而现场活动的人数急剧减少。

虽然 Prophet 在预测方面非常出色,但它并不能简单地预测未来。在疫情期间,Prophet 在预测疫情何时结束以及时间序列在封锁期间和封锁之后如何表现方面,会与预测专家一样感到困难。然而,我们可以在事后对系统中的这种冲击进行建模,以了解它们的影响。就像我们在上一节中建模的 NatGeo 促销活动一样,我们可以预测假设这种冲击重复发生的结果。在本节中,我们将使用一个新的数据集,即澳大利亚墨尔本 Bourke Street Mall 的行人数量。

自 2009 年以来,墨尔本市通过自动化传感器在全市多个地点统计行人数量。数据在城市的网站上共享,每月更新,包含每个传感器的每小时行人计数。为了使我们的分析更简单,本例中我们将使用的数据已经预先汇总为每日计数,我们只使用一个传感器的数据——最南端的 Bourke Street Mall 传感器。

Bourke Street 是墨尔本的主要街道之一,传统上是城市的娱乐中心。它是一个受欢迎的旅游目的地,拥有许多餐厅和主要零售店。由于大流行封锁对旅游业、餐厅和面对面零售的影响最为强烈,这个位置似乎是一个观察封锁影响的理想地点。此外,维多利亚州政府宣布了四个不同长度的官方封锁期。我们可能预计这些将标志着行为发生的明显而突然的变化。让我们加载数据集并查看它:

df = pd.read_csv('pedestrian_counts.csv')
df['Date'] = pd.to_datetime(df['Date'])
plt.figure(figsize=(10, 6))
plt.scatter(x=df['Date'],
            y=df['Daily_Counts'],
            c='#0072B2')
plt.xlabel('Date')
plt.ylabel('Pedestrians per day')
plt.show()

数据显示一个适度平坦的趋势,有明显的季节性影响,当然,从 2020 年开始的一个严重异常持续存在:

图 10.16 – Bourke Street Mall 的每日行人计数

图 10.16 – Bourke Street Mall 的每日行人计数

数据集包含一些对我们这次分析不感兴趣的列,因此在我们能够看到 Prophet 如何处理预测之前,我们需要提取仅包含DateDaily_Counts列,并在 Prophet 的格式中重命名它们:

df = df[['Date', 'Daily_Counts']]
df.columns = ['ds', 'y']

现在,让我们构建一个基本的预测,展望整整一年:

model = Prophet(seasonality_mode='multiplicative')
model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

预测似乎在努力适应 COVID 冲击的趋势:

图 10.17 – 未考虑冲击的预测

图 10.17 – 未考虑冲击的预测

此模型并未为我们提供关于我们可以归因于封锁的具体影响的洞察。为了模拟这种封锁冲击,我们将创建假期来代表封锁的日子,并将冲击处理方式与我们在上一节中处理 NatGeo 促销的方式相似。为此,我们首先需要定义我们的封锁假期(有点讽刺的矛盾!)。

第六章“预测假期影响”中,你学习了如何使用lower_windowupper_window创建多日假期。我们在这里将再次这样做,为每个四个官方封锁定义一个开始日期,并使用upper_window来设置封锁的长度:

lockdowns = pd.DataFrame([
    {'holiday':'lockdown1',
     'ds': pd.to_datetime('2020-03-21'),
     'lower_window': 0,
     'upper_window': 77},
    {'holiday':'lockdown2',
     'ds': pd.to_datetime('2020-07-09'),
     'lower_window': 0,
     'upper_window': 110},
    {'holiday':'lockdown3',
     'ds': pd.to_datetime('2021-02-13'),
     'lower_window': 0,
     'upper_window': 4},
    {'holiday':'lockdown4',
     'ds': pd.to_datetime('2021-05-28'),
     'lower_window': 0,
     'upper_window': 13}])

我们没有指定任何未来的日期,因此 Prophet 不会在未来任何时刻尝试重复这些封锁。当我们创建下一个模型时,我们将这个lockdown DataFrame 传递给holidays参数:

model = Prophet(seasonality_mode='multiplicative',
                holidays=lockdowns)
model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

我们得到的预测看起来与图 10.17中的预测非常相似:

图 10.18 – 将封锁建模为假期的预测

图 10.18 – 将封锁建模为假期的预测

现在,然而,当我们查看“组成部分”图时,我们将看到那些封锁的具体影响:

fig2 = model.plot_components(forecast)
plt.show()

我们在“假期”图中看到行人数量减少了大约 100%!

图 10.19 – 展示 COVID-19 封锁影响的组成部分图

图 10.19 – 展示 COVID-19 封锁影响的组成部分图

图 10.19中节假日的分布图展示了封锁如何有效地使行人交通几乎停滞。封锁的另一个可能甚至更持久的影响是远程工作的转变。全世界的许多工人现在能够在家中履行工作职责,不再像以前那样严格地遵循周一至周五,9:00 至 17:00 的日程安排。我们可以假设这可能会在一定程度上改变每周的季节性。图 10.19中显示的每周高峰表明周五是 Bourke Street 上最受欢迎的一天,其次是周六。这种模式在 COVID-19 之后是否仍然成立?

你在第五章“处理季节性”中学习了如何创建条件季节性;现在让我们用同样的原则来创建 COVID-19 之前的每周季节性和 COVID-19 之后的每周季节性。我们将为第一个封锁开始之前的所有日期定义一个pre_covid季节性,为最终封锁结束之后的所有日期定义一个post_covid季节性。我们还可以为两者之间的日期创建一个during_covid季节性,但由于行人流量几乎停滞且没有数据可用,从这种季节性中获得的任何见解至多是无意义的,甚至可能具有误导性:

df['pre_covid'] = df['ds'] < '2020-03-21'
df['post_covid'] = df['ds'] > '2021-06-10'

现在,我们将对此数据进行第三次预测,但这次我们将关闭默认的每周季节性,并添加我们的两个条件每周季节性。记住,我们必须将这些条件添加到future数据框中!

model = Prophet(seasonality_mode='multiplicative',
                weekly_seasonality=False,
                holidays=lockdowns)
model.add_seasonality(
    name='weekly_pre_covid',
    period=7,
    fourier_order=3,
    condition_name='pre_covid',
)
model.add_seasonality(
    name='weekly_post_covid',
    period=7,
    fourier_order=3,
    condition_name='post_covid',
)
model.fit(df)
future = model.make_future_dataframe(periods=365)
future['pre_covid'] = future['ds'] < '2020-03-21'
future['post_covid'] = future['ds'] > '2021-06-10'
forecast = model.predict(future)
fig = model.plot(forecast)
plt.show()

运行此代码将生成以下图表:

图 10.20 – 带有条件每周季节性的预测

图 10.20 – 带有条件每周季节性的预测

最后,让我们看一下components图,看看封锁产生了哪些持久影响:

fig2 = model.plot_components(forecast)
plt.show()

趋势、节假日和年度季节性与图 10.19中的几乎相同,因此它们已被裁剪出以下图表,该图表仅显示 COVID-19 之前和之后的封锁的两种每周季节性:

图 10.21 – 一个显示条件季节性的裁剪成分图

图 10.21 – 一个显示条件季节性的裁剪成分图

正如我们在图 10.19中提到的,COVID-19 之前周五是 Bourke Street 上最受欢迎的一天。然而,COVID-19 之后,似乎周六是最受欢迎的一天。随着越来越多的人在家工作,周五可能的工作后欢乐时光似乎减少了,取而代之的是人们选择待在家里!也许 Netflix 在其消费数据中看到了相反的模式……

摘要

异常值是任何数据分析的事实,但它们并不总是需要引起头痛。Prophet 在处理大多数异常值时非常稳健,无需任何特殊考虑,但有时可能会出现问题。在本章中,你学习了 Prophet 中与异常值最常见的问题——不受控制的季节性和爆炸性的不确定性区间。

在这两种情况下,简单地删除数据是解决问题的最佳方法。只要在那些删除数据的位置,季节性周期中的其他时期存在数据,Prophet 就没有问题找到良好的拟合。

你还学习了多种自动异常值检测技术,从基本的 Winsorization 和 trimming 技术,这些技术通常对表现出趋势的时间序列效果不佳,到更高级的堆叠预测技术,使用第一个模型中的误差来为第二个模型移除异常值。

最后,你学习了如何将异常值和重大的、持久的冲击,如 COVID-19 封锁,作为特殊事件进行建模,这具有与删除数据同时保留该异常值信息相同的效果。这种技术的好处是允许你模拟未来时间序列中发生的类似冲击。

在下一章中,我们将探讨与异常值相关的一个概念——不确定性区间。

第十一章:管理不确定性区间

预测本质上是对未来的预测,任何预测都会有特定的不确定性。量化这种不确定性可以帮助分析师了解他们的预测有多可靠,并为他们的经理提供在决策上投入大量资本的信心。

Prophet 从一开始就是为了考虑不确定性建模而设计的。虽然你可以使用PythonR与之交互,但其底层模型是用Stan编程语言构建的,这是一种概率语言,允许 Prophet 以高效的方式执行贝叶斯抽样,从而更深入地理解模型中的不确定性,以及预测的业务风险。

有三个不确定性来源会影响到你的 Prophet 模型中的总不确定性:

  • 趋势中的不确定性

  • 季节性、节假日以及额外回归因子中的不确定性

  • 数据噪声引起的不确定性

其中最后一个是不管你使用什么数据都固有的属性,但前两个是可以进行建模和检验的。在本章中,你将学习到 Prophet 如何建模不确定性,如何在你的模型中控制它,以及如何使用这些不确定性估计来量化风险。具体来说,本章将涵盖以下主题:

  • 对趋势中的不确定性进行建模

  • 对季节性中的不确定性进行建模

技术要求

本章中示例的数据文件和代码可以在github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition找到。

对趋势中的不确定性进行建模

你可能已经注意到在这本书的不同组件图中,趋势显示了不确定性界限,而季节性曲线则没有。默认情况下,Prophet 只估计趋势中的不确定性,以及数据中随机噪声引起的不确定性。噪声被建模为围绕趋势的正态分布,而趋势不确定性则使用最大后验估计MAP估计

MAP 估计是一个优化问题,它通过蒙特卡洛模拟来解决。以摩纳哥著名的赌场命名,蒙特卡洛方法使用重复的随机抽样来估计一个未知值,通常在封闭形式的方程不存在或计算困难时使用。

第六章“预测节假日效应”中,我们讨论了model.fit(df)调用。

让我们看看一些可以用来控制趋势不确定性的 Prophet 参数。在本章中,我们将使用一个新的数据集,涵盖从 2011 年到 2019 年巴尔的摩警察局每天报告的犯罪数量。让我们从导入和加载数据开始:

import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet
from prophet.plot import add_changepoints_to_plot
import numpy as np
np.random.seed(42)
df = pd.read_csv('baltimore_crime.csv')
df.columns = ['ds', 'y']

如果你绘制数据,你会看到它有一个相对平坦的趋势、季节性和几个异常值。特别是,我在以下图表中画了一条虚线,其水平为每天 250 起犯罪。在这条线以上的有两个数据点:

图 11.1 – 巴尔的摩犯罪数据

图 11.1 – 巴尔的摩犯罪数据

虽然这些点可能不会像我们在第十章中查看的 National Geographic 数据中的异常值那样影响我们的预测,但让我们将它们移除,并避免任何潜在的问题,如下所示:

df.loc[df['y'] > 250, 'y'] = None

重要提示

注意,我们导入了 NumPy 并设置了随机种子。在 Prophet 中,MAP 估计是一种确定性计算,所以你将始终得到相同或几乎相同的结果(由于不同机器处理浮点数的方式略有不同,因此可能略有差异)。然而,不确定性区间是随机生成的。通过 1,000 次迭代,它们在重复实验中应该非常相似,但如果未设置随机种子,那么你的图表可能不会与本书中的图表匹配。此外,马尔可夫链蒙特卡洛MCMC)采样,当需要季节性不确定性估计时使用,将在本章后面讨论,它确实为趋势计算添加了随机性。设置随机种子将确保你得到与本书相同的结果。

Prophet 预测中的最大不确定性来源是未来趋势变化的潜在可能性。在训练模型时,Prophet 会对未来进行许多蒙特卡洛模拟,假设未来趋势变化点将以与历史变化点相同的频率和幅度发生。因此,具有历史变化点幅度大的时间序列将看到非常宽的趋势不确定性;我们在第十章的图 10.7中看到了这一点,该图来自第十章处理异常值和特殊事件

Prophet 运行的蒙特卡洛模拟次数是通过在模型实例化时设置uncertainty_samples参数来确定的。默认情况下,它设置为1000,因此 Prophet 模拟了 1,000 条不同的未来趋势线,并使用这些线来估计不确定性。

让我们明确地构建我们的第一个模型,通过在实例化模型时设置uncertainty_samples=1000来指定这个默认值。然后我们将拟合模型,创建一个五年的预测,并带有变化点的图表。对于这个巴尔的摩犯罪数据,我们可以在模型实例化期间保持所有其他默认设置:

model = Prophet(uncertainty_samples=1000)
model.fit(df)
future = model.make_future_dataframe(periods=365 * 5)
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()

从数据集的开始,看起来 2011 年巴尔的摩的犯罪率上升,在接下来的几年里略有下降,然后再次上升,最后又下降。Prophet 将继续这一趋势,并延伸到未来:

图 11.2 – 带有 1,000 个不确定性样本的巴尔的摩犯罪预测

图 11.2 – 使用 1,000 个不确定性样本的巴尔的摩犯罪预测

不确定性(图中较浅的阴影区域)存在于历史和未来的所有点。现在,让我们绘制成分:

fig2 = model.plot_components(forecast)
plt.show()

趋势不确定性仅存在于预测的未来中;历史数据中没有任何不确定性:

图 11.3 – 使用 1,000 个不确定性样本的巴尔的摩犯罪成分图

图 11.3 – 使用 1,000 个不确定性样本的巴尔的摩犯罪成分图

这是因为所有历史不确定性都归因于噪声。正如我之前所说,噪声被建模为围绕预测的正态分布。由于趋势不确定性是由于未来趋势变化点的不确定性,因此趋势不确定性仅存在于未来。在图 11**.2中看到的总不确定性是噪声不确定性和趋势不确定性之和。此外,Prophet 团队指出,他们关于未来趋势变化永远不会比先前趋势变化更大的假设是一个非常限制性的假设,因此你不应该期望在不确定性区间上获得极端精确的覆盖。

当 Prophet 运行 1,000 次迭代以估计未来的趋势变化时,它将每个结果保存在模型的predictive_samples属性中。这是一个键为'yhat''trend'的字典,分别存储每个迭代的总预测估计值和仅趋势的预测估计值:

samples = model.predictive_samples(future)

通过将samples['trend']future['ds']绘制在一起,对于每个样本,你可以看到 Prophet 的 1,000 个潜在趋势模拟中的每一个:

图 11.4 – 使用 1,000 个不确定性样本的巴尔的摩犯罪趋势

图 11.4 – 使用 1,000 个不确定性样本的巴尔的摩犯罪趋势

默认情况下,Prophet 的不确定性区间为 80%。在 80%的置信水平内,那 1,000 条可能的趋势线是等可能的。因为未来的不确定性是从未来的潜在变化点估计的,而这些变化点又是从先前的变化点估计的,通过使用changepoint_prior_scale来增加或减少先前变化点的数量将对不确定性界限产生匹配的影响。

通常没有必要将不确定性样本的数量更改为默认值以外的任何值。增加它将给你更好的不确定性估计,但会以计算时间为代价;但 1,000 个样本通常足以得到一个好的估计。将参数设置为uncertainty_samples=0uncertainty_samples=False是一个特殊情况,这会禁用不确定性估计并显著加快计算速度。

通过interval_width参数可以控制不确定性水平。如果你想对自己的不确定性水平更有信心,你可能想增加这个值;减小它将给你更紧的界限,但信心会降低。让我们将宽度增加到0.99,以达到 99%的置信水平:

model = Prophet(interval_width=0.99)
model.fit(df)
future = model.make_future_dataframe(periods=365 * 5)
forecast = model.predict(future)

我只会绘制趋势图,因为这是这种变化影响最明显的地方:

from prophet.plot import plot_forecast_component
plot_forecast_component(model,
                        forecast,
                        ‹trend›,
                        figsize=(10.5, 3.25))
plt.show()

将以下图表与图 11.3 中的趋势组件进行比较:

图 11.5 – 巴尔的摩犯罪趋势图,包含 99% 的不确定性区间宽度

图 11.5 – 巴尔的摩犯罪趋势图,包含 99% 的不确定性区间宽度

在这个图表中,不确定性的宽度要大得多。因为我们希望有更高的信心保证边界包含真实趋势,所以我们不得不扩大边界以提供这种更高的确定性。

你在这本书中一直在建模趋势不确定性。但现在你希望看到季节性中的不确定性边界。在下一节中,你将学习如何实现这一点。

对季节性进行不确定性建模

MAP 估计非常快,这就是为什么它是 Prophet 的默认模式,但它不适用于季节性,因此需要不同的方法。为了对季节性不确定性进行建模,Prophet 需要使用 MCMC 方法。马尔可夫链是一个描述一系列事件的模型,每个事件发生的概率取决于前一个事件的状态。Prophet 使用这个连锁序列来建模季节性不确定性,并使用在上一节开头描述的蒙特卡洛方法重复序列多次。

缺点是 MCMC 样本采样速度慢;在 macOS 或 Linux 机器上,你应该预计拟合时间为几分钟而不是几秒钟。不幸的是,在 Windows 机器上,与 Prophet 的 Stan 语言模型接口的 PyStan API 存在上游问题,这意味着 MCMC 样本采样非常慢。根据数据点的数量,在 Windows 机器上拟合模型有时可能需要几个小时。

Prophet 团队建议 Windows 机器上的用户使用 R 或在 Linux 虚拟机中使用 Python 与 Prophet 一起工作。另一种选择是使用 Google 的 Colab 笔记本,它们类似于云托管的 Jupyter 笔记本。它们是免费使用的,并且内置在 Linux 中,因此它们不会遇到 Windows 面临的 PyStan 问题。您可以通过 colab.research.google.com/ 访问它们。

在排除这些注意事项之后,让我们看看如何对季节性进行不确定性建模。我们将保留迄今为止使用的默认 1000 不确定性样本,并为 mcmc_samples 添加不同的参数。如果你将此参数设置为 0,Prophet 将回退到 MAP 估计,并且只提供趋势组件的不确定性,回退到本章前面示例中创建的模型。我们将使用 300 个 MCMC 样本:

model = Prophet(mcmc_samples=300)
model.fit(df)
future = model.make_future_dataframe(periods=365 * 5)
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()

在拟合和预测之后,我们绘制了预测图。你可能会首先注意到变化点的数量,如下面的图表所示:

图 11.6 – 巴尔的摩犯罪预测图,包含 300 个 MCMC 样本

图 11.6 – 巴尔的摩犯罪预测图,包含 300 个 MCMC 样本

我们将在稍后处理那个变化点问题。现在,让我们看看成分图:

fig2 = model.plot_components(forecast)
plt.show()

你现在应该看到weeklyyearly季节性的不确定性区间如下:

图 11.7 – 巴尔的摩犯罪成分图,包含 300 个 MCMC 样本

图 11.7 – 巴尔的摩犯罪成分图,包含 300 个 MCMC 样本

如果我们添加了假日或任何其他回归因子,你也会在那里看到不确定性区间。现在就试试吧,使用第九章中的 Divvy 示例,即包括其他回归因子。

在这个模型中,我们接受了默认的uncertainty_samples=1000参数,并设置了mcmc_samples=300。当 Prophet 运行其 MCMC 方法时,它总共使用四个链(尽管这个值可以通过fit调用中的chains关键字参数来更改)。mcmc_samples参数是每个链生成的总样本数。这与uncertainty_samples参数不同,后者是生成总趋势线样本数的总数。

mcmc_samples=0时,Prophet 将生成与uncertainty_samples参数中定义的潜在趋势线数量完全一致的数量。然而,当mcmc_samples大于零的任何值时,Prophet 将生成至少与uncertainty_samples参数中定义的潜在趋势线数量一样多的趋势线,但可能更多,因为它需要每个链有相同数量的迭代。这可能会有些令人困惑,但这只是一个小的技术细节。你可能注意到的唯一实际影响是model.predictive_samples(future)可能比你在uncertainty_samples中指定的行数多出几分之一。

现在,让我们回到那些变化点。为什么在执行 MCMC 采样时会有这么多变化点?如果你还记得第八章中的内容,即影响趋势变化点,Prophet 设置了一个较高的潜在变化点数量,并尝试将它们的幅度设置得尽可能低。这对于 MAP 估计来说效果很好。然而,在贝叶斯分析中,正如 MCMC 采样中那样,有一个众所周知的现象会导致参数不会以相同的方式收缩。

这是我们第一个模型的变化点幅度图,如图图 11.2所示,以及我们最新的模型,如图图 11.6所示:

图 11.8 – 不同不确定性估计导致的变化点幅度

图 11.8 – 不同不确定性估计导致的变化点幅度

我们遇到的问题不是模型本身的问题,而是可视化的问题。前图中两条虚线显示了add_changepoints_to_plot函数中绘制变化点幅度的默认0.01阈值(虚线表示提高了阈值的0.1)。

超出此线的改变点在图 11.2中进行了绘制。图 11.6中绘制的模型有更多超出此线的改变点,因此也进行了绘制。然而,额外的改变点相互抵消。它们先是负的,然后是正的。整体效果是,图 11.2图 11.6的趋势几乎相同:

图 11.9 – 不同变化点不确定性估计的结果趋势线

图 11.9 – 不同变化点不确定性估计的结果趋势线

从这个例子中我们得到的教训不是过分担心它。如果你想在你图表上得到一个更合理的数量变化点,当添加变化点时,你可以自由地更改threshold参数。在这里,我们将其更改为0.1,这是图 11.8中用虚线标记的水平:

fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast,
                         threshold=0.1)
plt.show()

现在我们看到以下类似的改变点数量:

图 11.10 – 增加变化点阈值的巴尔的摩犯罪预测

图 11.10 – 增加变化点阈值的巴尔的摩犯罪预测

它们是不同的变化点,但请记住,这只是可视化中出现的问题。两个模型最终的趋势非常相似。出现的小差异是由于不同的统计抽样技术;这两种技术都没有一种是比另一种更正确的 – 它们都是数据估计。

但这并不总是如此。有时你会看到 MCMC 采样中趋势变化的太多。如果发生这种情况,你可以简单地降低你的changepoint_prior_scale值,并稍微控制一下变化点的大小。例如,让我们将其从我们一直在使用的0.05默认值降低到0.03

model = Prophet(changepoint_prior_scale=0.03,
                mcmc_samples=300)
model.fit(df)
future = model.make_future_dataframe(periods=365 * 5)
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast,
                         threshold=0.1)
plt.show()

在这个层面上,我们比图 11.6中的变化点更少,如下面的图表所示:

图 11.11 – 增加变化点正则化的巴尔的摩犯罪预测

图 11.11 – 增加变化点正则化的巴尔的摩犯罪预测

如果我们比较趋势线,我们会看到这条正则化线与原始 MAP 估计非常吻合:

图 11.12 – 不同变化点先验尺度的结果趋势线

图 11.12 – 不同变化点先验尺度的结果趋势线

如果你使用 MCMC 抽样,只需确保注意增加的变化点数量。如果你的趋势线看起来过度拟合,你可以简单地减少变化点先验尺度来控制它。

摘要

不确定性区间是理解你的预测的重要工具。对未来没有任何预测能够带有绝对的信心。通过明确地说明你模型中的置信水平,你为你的观众提供了对模型预测中涉及的风险的理解,以便更好地指导他们的决策。

在本章中,你了解到之前章节中构建的所有模型都使用了 MAP 估计来创建置信水平。这种方法比替代方法 MCMC 采样计算时间更短,但只能对趋势成分中的不确定性进行建模。通常,这已经足够了。然而,在那些你还需要对季节性、假日或额外回归器的不确定性进行说明的时候,你也学会了如何在 Prophet 中应用 MCMC 采样来构建一个更全面的不确定性模型。

最后,你学习了 MCMC 采样在趋势变化点应用正则化方面的固有弱点。在使用 MCMC 采样构建的 Prophet 模型中,你通常会看到比使用 MAP 估计构建的模型中更多的显著变化点。正因为如此,你学会了在使用 MCMC 采样时密切注意趋势过拟合,并相应地调整变化点先验尺度。

在下一章中,你将学习 Prophet 中的交叉验证。你可能对其他机器学习应用中的 k 折交叉验证很熟悉;k 折交叉验证在时间序列中是失败的。我们将介绍一种不同的方法,称为前向链式。

第三部分:诊断与评估

本节的最后部分将关于模型评估和下一步操作。你将学习如何使用 Prophet 内置的性能指标以统计稳健的方式比较不同的模型,以及如何可视化它们的性能。最后,本节将以一些 Prophet 的附加功能结束,这些功能可以在将 Prophet 部署到现实世界的用例中使用,在这些用例中,更新模型和共享结果可能是频繁发生的。

本节包括以下章节:

  • 第十二章执行交叉验证

  • 第十三章评估性能指标

  • 第十四章将 Prophet 投入生产

第十二章:执行交叉验证

在机器学习和统计学中,保持训练数据和测试数据分离的概念是神圣不可侵犯的。你不应该在同一数据上训练模型并测试其性能。虽然为测试目的设置数据有缺点:这些数据包含你希望包含在训练中的有价值信息。交叉验证是一种用来规避这个问题的技术。

你可能熟悉k 折交叉验证,但如果你不熟悉,我们将在本章中简要介绍它。然而,k 折交叉验证不适用于时间序列数据。它要求数据是独立的,这是一个时间序列数据不成立的假设。理解 k 折交叉验证将帮助你学习正向链交叉验证是如何工作的,以及为什么它是时间序列数据所必需的。

在学习如何在 Prophet 中执行交叉验证之后,你将学习如何通过 Prophet 并行化多个过程的能力来加速交叉验证的计算。总的来说,本章将涵盖以下主题:

  • 执行 k 折交叉验证

  • 执行正向链交叉验证

  • 创建 Prophet 交叉验证 DataFrame

  • 并行化交叉验证

技术要求

本章中示例的数据文件和代码可以在github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition找到。

.

执行 k 折交叉验证

在本章中,我们将使用一个新的数据集——英国在线零售商的销售额。这些数据已被匿名化,但它代表了三年来的每日销售额,如下所示图表所示:

Figure 12.1 – 匿名在线零售商的每日销售额

Figure 12.1 – 匿名在线零售商的每日销售额

该零售商在过去三年的数据中并未看到显著增长,但在每年的年底都看到了销售额的巨大提升。该零售商的主要客户是批发商,他们通常在工作日进行采购。这就是为什么当我们绘制 Prophet 预测的组成部分时,你会看到周六和周日的销售额最低。我们将使用这些数据在 Prophet 中执行交叉验证。

在我们进行建模之前,让我们首先回顾一下用于调整模型超参数和报告性能的传统验证技术。最基本的方法是将你的完整数据集分成三个子集:一个训练集、一个验证集和一个测试集,在随机打乱后。这有时被称为保留验证。通常,训练集是最大的,验证集和测试集较小。例如,60/20/20 的分割看起来是这样的:

Figure 12.2 – 传统训练/验证/测试数据集

图 12.2 – 传统的训练/验证/测试集

在完整数据被分割后,你的模型在训练集上训练,并在验证集上评估性能。为给定的算法选择一组新的超参数,并在训练集上重新训练模型,并在验证集上重新评估。这个过程会重复进行,直到尝试多少种超参数组合。

在验证集上性能最高的超参数集被选为模型;训练和验证集合并以训练最终模型,并在测试集上评估这个最终模型。这个评估结果随后报告为模型的性能。

然而,使用这种技术,你只有 60%的完整数据可用于调整模型。使用更多数据进行调整可能会有所帮助,但使用较小的验证和测试集可能会引入模型偏差。

为了解决这个问题,开发了 k 折交叉验证。在 k 折交叉验证中,数据仍然随机洗牌,并分割出一个测试集,可能是 20%的数据。剩余的 80%数据全部用于训练。这 80%的数据被分成k个部分,每个部分称为一个。以下是五个折的过程:

图 12.3 – 五折交叉验证

图 12.3 – 五折交叉验证

对于你想要评估的每一组超参数,你将模型训练五次。第一次,你预留第一个折,并在剩余的四个折上训练。你在第一个折上进行评估。你重复这个过程,对每个折进行操作,并取五个折的性能指标的平均值。然后,你继续到下一组超参数,并重复。

由于每个折的训练,调整超参数的过程在这种情况下会花费更长的时间。然而,优势在于你可以在不引入模型偏差的情况下使用更多数据进行训练。

正如你所知,时间序列数据是顺序和相关的。你不能对其进行洗牌。你不能在未来的数据上训练以预测以前的数据。这就是为什么刚刚展示的两种方法都不适用的原因。我们需要一种方法来保持我们数据的顺序,同时仍然留出一部分用于测试和验证。这就是为什么开发了正向链。

执行正向链交叉验证

正向链交叉验证,也称为滚动起点交叉验证,与 k 折交叉验证类似,但更适合序列数据,如时间序列。一开始没有对数据进行随机洗牌,但可以预留一个测试集。测试集必须是数据的最后部分,所以如果每个折将占你数据的 10%(就像在 10 折交叉验证中那样),那么你的测试集将是数据范围的最后 10%。

在剩余的数据中,你选择一个初始数据量用于训练,例如,在这个例子中是五个折,然后你在第六个折上评估并保存那个性能指标。现在你在前六个折上重新训练,并在第七个折上评估。你重复此过程,直到所有折都耗尽,然后再次取性能指标的平均值。使用这种技术的方法将如下所示:

图 12.4 – 前向链式交叉验证的五折

图 12.4 – 前向链式交叉验证的五折

以这种方式,你能够在序列数据点上训练你的数据,并评估未见过的数据,同时也能够通过在多种样本上训练和测试来最小化偏差。

Prophet 有一个内置的诊断工具用于执行前向链式交叉验证。现在让我们看看如何使用我们的零售销售数据集来使用它。

创建 Prophet 交叉验证 DataFrame

在 Prophet 中执行交叉验证之前,首先你需要一个拟合好的模型。因此,我们将从本书中完成过的相同程序开始。这个数据集非常合作,因此我们将能够使用 Prophet 的大量默认参数。我们将绘制变化点,所以请确保在加载数据之前将此功能与其他导入项一起包含:

import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet
from prophet.plot import add_changepoints_to_plot
df = pd.read_csv('online_retail.csv')
df['date'] = pd.to_datetime(df['date'])
df.columns = ['ds', 'y']

此数据集的季节性并不复杂,因此在我们实例化模型时,我们将降低年季节性的傅里叶阶数,但保持其他所有设置默认,在拟合、预测和绘图之前。我们将使用 1 年的未来预测:

model = Prophet(yearly_seasonality=4)
model.fit(df)
future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()

如预期的那样,此图显示了与图 12.1中引入数据相同的相同数据。没有识别出显著的趋势变化点,并且有一个非常平缓的上升趋势。夏季似乎有轻微的销售增长,但在冬季假日季节有显著增长,如以下图所示:

图 12.5 – 在线零售销售额预测

图 12.5 – 在线零售销售额预测

让我们绘制组成部分,以更好地理解我们的季节性:

fig2 = model.plot_components(forecast)
plt.show()

trendweekly季节性和yearly季节性显示出明显的模式:

图 12.6 – 在线零售销售额组成部分图

图 12.6 – 在线零售销售额组成部分图

正如我们所预测的,yearly季节性反映了冬季的峰值。正如我在介绍此数据时提到的,零售商主要服务于批发商,而不是消费者。因此,他们的购买行为在商业周中远多于周末。与一周中的其他日子相比,周五的销售甚至有所下降。

现在,让我们执行实际的交叉验证。为此,我们首先需要从 Prophet 的diagnostics包中导入函数:

from prophet.diagnostics import cross_validation

在我们了解如何使用该函数之前,有一些术语我们需要讨论:

  • initial 是第一个训练周期。在 图 12.5 中,它将是第一个折叠中的前五个数据块。这是开始训练所需的最小数据量。

  • horizon 是你想要评估预测的时间长度。比如说,这个零售店正在构建一个模型,以便它可以预测下一个月的销售。将预测范围设置为 30 天在这里是有意义的,这样他们就可以在希望使用的相同参数设置上评估他们的模型。

  • period 是每个折叠之间的时间量。它可以大于或小于预测范围,甚至等于它。

  • cutoffs 是每个 horizon 开始的日期。

这个词汇表在这里得到了说明:

图 12.7 – 交叉验证术语

图 12.7 – 交叉验证术语

对于每个 cutoff,模型将在截止日期之前的所有数据上进行训练,然后对 horizon 期间进行预测。这个预测将与已知值进行比较和评估。然后,模型将在截止日期之前的所有数据上进行重新训练,这个过程将重复进行。最终的性能评估将是每个截止点的性能的平均值。

让我们想象这个零售店希望有一个能够预测下一个月每日销售的模型,并且他们计划在每个季度的开始运行这个模型。他们有 3 年的数据,并且希望(正如 Prophet 所推荐的那样)至少有 2 个完整的季节周期,由于他们正在模拟年度季节性,这将需要 2 年。

他们将初始训练数据设置为 2 年。他们想要预测下一个月的销售,因此将 horizon 设置为 30 天。他们计划每个业务季度运行模型,因此将周期设置为 90 天。这就是之前在 图 12.7 中所示的内容。现在,让我们将此应用于 Prophet。

cross_validation 函数接受两个必需的参数,即拟合的模型和 horizonperiodinitial 也可以指定,但不是必需的。如果保留为默认值,periodhorizon 的一半,而 initial 将是 horizon 的三倍。该函数的输出是交叉验证的 DataFrame。让我们创建这个 DataFrame,并将其命名为 df_cv

df_cv = cross_validation(model,
                         horizon='90 days',
                         period='30 days',
                         initial='730 days')

每个 horizonperiodinitial 参数都接受一个与 pandas 的 Timedelta 格式兼容的字符串,例如,'5 days''3 hours''10 seconds'。在这个例子中,我们将 horizonperiod 的值从 图 12.7 中所示的内容切换过来。零售店希望预测 3 个月的每日销售,并且每月更新他们的预测(这可能是对预测的更现实的使用;这些参数在图像中仅被反转,以避免重叠预测范围,使图像更清晰)。

我们从 2 年的初始周期开始训练,即'730 days'。我们将horizon='90 days'设置为评估 90 天的预测间隔。最后,我们设置period='30 days',这样我们每 30 天重新训练和重新评估我们的模型。这导致总共 10 个预测与最终一年的数据进行比较。

你也可以指定cutoff值,但这通常是不必要的。然而,我们将在第十三章中介绍一个特定实例,评估性能指标,在那里你将需要自己设置它们。Prophet 的默认行为是从时间序列的末尾自动设置它们。

现在,让我们通过显示前五行来查看这个 DataFrame:

df_cv.head()

如果你在一个 Jupyter 笔记本中运行这段代码,你会看到以下格式化的输出(由于优化算法中的随机性,你的yhat_loweryhat_upper的值可能略有不同):

图 12.8 – 交叉验证 DataFrame

图 12.8 – 交叉验证 DataFrame

对于 DataFrame 中的每个唯一的cutoff,你将在ds列中找到 90 天,对应于 90 天的预测范围。ds中的每个日期都有一个真实值y,这是你的训练数据df['y']中的相同值,以及在该折叠中对该日期在yhat列中预测的值。

注意,这与forecast DataFrame 中的yhat不同,因为那些值是用完整的数据集计算的,而不是用交叉验证的折叠计算的。交叉验证 DataFrame 还包含这些预测的不确定性区间,在yhat_upperyhat_lower中。

这个 DataFrame 允许你比较你的数据中日期时间值范围内的预测值与实际值。在forecast DataFrame 中,对于未来的日期,显然没有真正的y值进行比较。对于过去的日期,有一个对应的df['y']值来比较你的forecast['yhat']值,但预测是针对这个值训练的。forecast['yhat']值是有偏的,而df_cv['yhat']值是无偏的,因此将提供更准确的关于你期望模型在新、未见数据上预测的表示。

并行化交叉验证

在交叉验证过程中有很多迭代,这些是可以并行化以加快速度的任务。要利用这一点,你只需要使用parallel关键字。你可以选择以下四个选项之一:None'processes''threads''dask'

df_cv = cross_validation(model,
                         horizon='90 days',
                         period=›30 days›,
                         initial='730 days',
                         parallel='processes›'

设置parallel='processes'使用 Python 的concurrent.futures.ProcessPoolExecutor类,而parallel='threads'使用concurrent.futures.ThreadPoolExecutor。如果你不确定使用哪一个,选择'processes'。它将在单台机器上提供最佳性能。

None将不会执行并行处理,如果你计划在 Prophet 计算时在机器上做其他工作,且不希望 Prophet 占用你机器的所有资源,这可能是好的。如果你使用'dask',你需要单独安装 Dask 并使用dask.distributed中的Client来连接到集群(如果 Dask 没有单独安装和设置,以下代码将导致错误):

from dask.distributed import Client
client = Client()
df_cv = cross_validation(model,
                         horizon='90 days',
                         period='30 days',
                         initial='730 days',
                         parallel='dask')

虽然你可以在你的笔记本电脑上使用 Dask,但它的威力真正体现在使用多台机器上的多个计算集群时。如果你无法访问这种类型的计算能力,parallel='processes'通常会是更快的选项。

摘要

我们本章开始时讨论了为什么 k 折交叉验证在传统的机器学习应用中被开发出来,然后我们学习了为什么它不适用于时间序列。然后你学习了前向链,也称为滚动起点交叉验证,用于时间序列数据。

你学习了initialhorizonperiodcutoff这些关键字,它们用于定义你的交叉验证参数,并且你学习了如何在 Prophet 中实现它们。最后,你学习了 Prophet 在并行化方面提供的不同选项,以便加快模型评估。

这些技术为你提供了一种统计上稳健的方式来评估和比较模型。通过隔离用于训练和测试的数据,你消除了过程中的任何偏差,并且可以更有信心地认为你的模型在做出新预测时将表现良好。

在下一章中,你将应用在这里学到的知识来衡量你模型的性能,并对其进行调整以获得最佳结果。

第十三章:评估性能指标

任何真实世界现象的模型都不是完美的。关于基础数据做出了无数统计假设,测量中存在噪声,还有未知和未建模的因素会影响输出。尽管如此,一个好的模型仍然是信息丰富且宝贵的。那么,您如何知道您是否拥有这样一个好的模型?您如何确保您的未来预测是可信的?交叉验证通过提供一种比较无偏预测与实际值的技术,使我们前进了一步。本章全部关于如何比较不同的模型。

Prophet 提供了几个不同的指标,用于比较您的实际值与预测值,因此您可以量化模型的性能。这告诉您模型实际上有多好,您是否可以信任预测,并帮助您比较不同模型的性能,以便您选择最好的一个。

本章将教授您以下内容:

  • 理解 Prophet 的指标

  • 创建 Prophet 性能指标 DataFrame

  • 处理不规则截止点

  • 使用网格搜索调整超参数

技术要求

本章示例的数据文件和代码可以在github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition找到。

理解 Prophet 的指标

Prophet 的diagnostics包提供了六个不同的指标,您可以使用这些指标来评估您的模型。这些指标包括均方误差、均方根误差、平均绝对误差、平均绝对百分比误差、中位数绝对百分比误差和覆盖率。我们将依次讨论这些指标。

均方根误差

均方误差MSE)是每个预测值与实际值之间平方差的和,如下公式所示:

图片 (1)

前面的方程式中,样本数量由表示,其中是实际值,是预测值。

MSE 可能是最常用的性能指标,但它确实有其缺点。因为它没有缩放到数据,其值不易解释——MSE 的单位是y单位的平方。它也容易受到异常值的影响,尽管这可能是所希望的也可能是所不希望的,这取决于您的数据和解释。

然而,它仍然很受欢迎,因为它可以证明均方误差等于偏差平方加方差,因此最小化这个指标可以减少偏差和方差。均方误差永远不会是负数,它越接近零,模型就越好。

均方根误差

如果您通过取平方根将 MSE 缩放到与数据相同的单位,您将得到均方根****误差RMSE):

(2)

RMSE 与 MSE 具有相同的优缺点,尽管其单位更具可解释性。与 MSE 一样,它对误差大的点的重视程度高于误差小的点。

均值绝对误差

均值绝对误差MAE)与 MSE 相似,但它取误差的绝对值而不是平方:

(3)

与 MSE 和 RMSE 不同,MAE 对每个误差的权重相同;它不对异常值或误差异常高的点给予更多重视。尽管如此,MAE 与 MSE 一样,并没有根据数据来缩放。因此,如果你发现你的模型报告的 MAE 为 10,那么这是好是坏?如果你的数据集的平均值为 1,000,那么 10 的误差仅占 1%。然而,如果你的数据平均值为 1,那么 10 的 MAE 意味着你的预测误差为 1,000%!

为了将 MAE 缩放到数据,它通常会被除以数据的平均值,从而得到一个百分比:

(4)

这种格式的 MAE 在 Prophet 中不受支持,尽管你可以自己创建它。

均值绝对百分比误差

均值绝对百分比误差MAPE)尽管其表示模型性能的能力较差,但仍然是一个非常常见的指标。不要与将总 MAE 除以平均值混淆,MAPE 将每个误差除以该误差处的数据点值:

(5)

这使得该指标偏向于过度表示数据值较低时发生的误差。因此,MAPE 被认为是不对称的——它对负误差(预测值高于实际结果)的惩罚比对正误差的惩罚更重。优化 MAPE 通常会使得你的模型未能达到目标值。此外,由于你正在除以每个值,如果其中任何一个为零,则计算将产生除以零的错误。非常小的值也会导致浮点计算问题。Prophet 将检测是否有任何值在或接近零,如果发现,它将简单地跳过 MAPE 计算并继续进行其他指标。然而,MAPE 的优点是它具有自然的可解释性——它很容易直观理解。

中值绝对百分比误差

中值绝对百分比误差MdAPE)与 MAPE 相同,只是它使用中位数而不是平均值。当 MAPE 可能是首选指标但存在太多异常值时,它可能很有用。例如,重要的节假日可能会在数据中产生大的峰值,而中位数能够平滑出 MAPE 可能遇到的问题。

对称均值绝对百分比误差

对称均值绝对百分比误差SMAPE)试图克服之前描述的 MAPE 的不对称缺陷。

(6)

SMAPE 以百分比的形式表示,这使得它可以比较不同规模数据集之间的性能。然而,SMAPE 的一个缺点是,当实际值和预测值都接近零时,它会变得不稳定。方程式的上限是 200%,这在直观上可能感觉有点奇怪。因此,一些方程式的公式在分母中省略了除以 2 的操作。

覆盖率

最终的 Prophet 指标是覆盖率。覆盖率简单地表示实际值位于预测的上限和下限不确定性界限之间的百分比。默认情况下,不确定性界限覆盖 80%的数据,因此您的覆盖率值应该是 0.8。

如果您发现覆盖率值不等于在模型实例化期间设置的interval_width,这意味着您的模型没有很好地校准到不确定性。在实践中,这仅仅意味着您可能无法信任未来预测部分中声明的不确定性区间,并可能希望根据覆盖率值进行调整。

当然,交叉验证 DataFrame 包含您所有的实际img/B19630_13_F13.png值和您模型的预测img/B19630_13_F14.png值,因此您可以计算出任何其他指标来比较这些值,并自行计算和使用。

选择最佳指标

决定使用哪个性能指标来优化您的模型不是一个简单的选择。它会对您的最终模型产生重大影响,具体取决于数据的特征。从数学上分析,可以证明优化您的模型以 MSE 为目标将创建一个预测值接近数据平均值的模型,而优化以 MAE 为目标将创建预测值接近中值。优化以 MAPE 为目标往往会产生异常低的预测,因为它在数据低点发生的错误上施加了如此高的权重。

那么,在 MSE(或 RMSE)和 MAE 之间,哪个更好?RMSE 旨在对平均数据点正确,而 MAE 旨在尽可能多地超过实际值,同时也尽可能少地低于实际值。这种差异只有在您的数据平均值和中位数不同时才会显现出来——在高度偏斜的数据中。由于中位数在偏斜数据中比平均值更远离尾部,MAE 将倾向于对数据的大部分产生偏差,而远离尾部。偏差模型是 MAE 的最大缺点。

MSE 的缺点是对异常值敏感。想象一下,一个时间序列总体上很平坦,除了几个极端的异常值。MSE 将真正关注这些异常值的预测误差,因此它往往会比 MAE 更频繁地偏离目标。一般来说,中位数比平均值更能抵抗异常值。

那么,我们应该将异常值的鲁棒性视为好事吗?不一定。如果你的时间序列是间歇性的——也就是说,如果大多数日期的 值为 0 ——你不想针对中位数值,而是均值。中位数将是 0!在这种情况下,你将希望 MSE(均方误差)精确地因为它是敏感于异常值的。

很遗憾,没有简单的答案来确定哪个指标是最好的。分析师必须注意偏差、偏斜度和异常值,以确定哪个指标将工作得最好。而且,你完全可以用多个指标尝试,看看哪个预测对你来说看起来最合理!

创建 Prophet 性能指标 DataFrame

现在你已经了解了 Prophet 中性能指标的不同选项,让我们开始编码,看看如何访问它们。我们将使用与我们在 第十二章 中使用的相同的在线零售销售数据,执行交叉验证。除了我们通常的导入之外,我们还将添加来自 Prophet 的 diagnostics 包中的 performance_metrics 函数和来自 plot 包的 plot_cross_validation_metric 函数:

import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet
from prophet.plot import add_changepoints_to_plot
from prophet.diagnostics import cross_validation
from prophet.diagnostics import performance_metrics
from prophet.plot import plot_cross_validation_metric

接下来,让我们加载数据,创建我们的预测,并绘制结果:

df = pd.read_csv('online_retail.csv')
df.columns = ['ds', 'y']
model = Prophet(yearly_seasonality=4)
model.fit(df)
forecast = model.predict()
fig = model.plot(forecast)
add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()

因为我们对任何未来的预测不感兴趣,所以我们不需要创建 future DataFrame。我们只需关注我们拥有的 3 年数据:

图 13.1 – 在线零售销售预测

图 13.1 – 在线零售销售预测

performance_metrics 函数需要一个交叉验证 DataFrame 作为输入,所以我们将以你学习在 第十二章 中相同的方式创建一个。我们将 horizon 设置为 90 天,因此交叉验证中的每个折叠将是 90 天period30 天 是开始一个新折叠的频率,而 initial 被设置为 730 天是我们的第一个两年训练期,未受到验证的影响:

df_cv = cross_validation(model,
                         horizon='90 days',
                         period='30 days',
                         initial='730 days',
                         parallel='processes')

接下来,我们将 df_cv 发送到 performance_metrics 函数。默认情况下,此函数将计算五个可用的指标中的每一个。你可以通过传递一个指标名称列表到 metrics 参数来指定这些指标的一个子集。让我们包括所有五个,并显示结果 DataFrame 的前几行:

df_p = performance_metrics(df_cv)
df_p.head()

输出的 DataFrame 以 horizon 中的天数为索引,因此每一行代表当模型被要求预测这么多天时的这些指标值。这只是前五行(由于优化算法中的随机性,你的结果可能会有所不同):

图 13.2 – 性能指标 DataFrame

图 13.2 – 性能指标 DataFrame

你可能想知道为什么horizon列的第一行是9 days。DataFrame 中的每个指标值都是其计算到指定日期的滚动平均值。performance_metrics函数接受一个rolling_window参数,你可以更改窗口大小,但默认值是0.1。这个数字是窗口中包含horizon的分数。在我们的 90 天horizon中,10%是 9 天,这是 DataFrame 的第一行。

你可以使用这个 DataFrame 单独使用,或者使用 Prophet 的plot_cross_validation_metric函数来可视化它。这个函数实际上会调用performance_metrics函数本身,所以你不需要首先创建一个df_p,只需要一个df_cv。在这里,我们将通过将'mae'传递给metric参数来绘制 MAE:

fig = plot_cross_validation_metric(df_cv, metric='mae')
plt.show()

生成的图显示了沿预测范围的每个 MAE 测量值及其滚动平均值:

图 13.3 – 交叉验证图

图 13.3 – 交叉验证图

我们的交叉验证设置是 horizon='90 days', period='30 days', initial='730 days',这对于初始训练期后剩余的 1 年数据,总共产生了十个 90 天的预测。因此,对于我们预测范围中的每一天,前一个图将会有 10 个 MAE 测量值。如果你数一下那个图上的所有点,应该是 900 个。实线是滚动平均值,窗口大小与performance_metrics DataFrame 中的默认值相同,即0.1

你可以通过在plot_cross_validation_metric函数中使用相同的rolling_window参数来指定这一点。为了非常清楚地说明这个窗口大小如何影响图,让我们比较两个 RMSE 图,一个窗口大小为 1%,另一个为 10%:

fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(111)
plot_cross_validation_metric(df_cv,
                             metric='rmse',
                             rolling_window=.01,
                             ax=ax)
plot_cross_validation_metric(df_cv,
                             metric='rmse',
                             rolling_window=.1,
                             ax=ax)
plt.show()

我们使用ax参数在同一张图上绘制两条线:

图 13.4 – 比较不同的窗口大小

图 13.4 – 比较不同的窗口大小

线条更平滑的是窗口大小更宽的那个,即默认窗口大小。因为窗口不是居中的,而是设置在右边,所以在使用 10%的预测范围时,前 8 天不会显示滚动平均值线。将窗口设置为 1%将包括所有数据,但代价是噪声更大。

现在你已经学会了如何使用交叉验证图,让我们用它来看看当让 Prophet 自动选择每个交叉验证折的截止日期时可能出现的问题。

处理不规则截止点

在这个例子中,我们将使用一个新的数据集。世界粮食计划署WFP)是联合国专注于饥饿和粮食安全的部门。WFP 跟踪的发展中国家粮食安全问题的一个最大的影响因素是降雨,因为它会影响农业生产。因此,预测降雨对于规划援助的分配至关重要。

这份数据代表 WFP 监测的一个地区 30 年内接收到的降雨量。这个数据集的独特之处在于 WFP 每月记录了三次累积降雨量,分别在 1 日、11 日和 21 日。从 1 日到 11 日的累积是一个 10 天的周期。从 11 日到 21 日也是如此。但一个月的 21 日到下一个月的 1 日的周期会根据月份的不同而变化。在正常的二月,将是 8 天。在闰年,是 9 天。30 天和 31 天的月份将分别有 10 天和 11 天的周期。

让我们按照你之前学到的进行交叉验证,看看这会有什么效果。首先,我们需要在数据上训练一个 Prophet 模型。如果你是从上一个例子继续的,你应该已经导入了所有内容:

df = pd.read_csv('rainfall.csv')
df.columns = ['ds', 'y']
model = Prophet(yearly_seasonality=4)
model.fit(df)
future = model.make_future_dataframe(periods=365 * 5)
future = future[future['ds'].dt.day.isin([1, 11, 21])]
forecast = model.predict(future)
fig = model.plot(forecast)
a = add_changepoints_to_plot(fig.gca(), model, forecast)
plt.show()

如果你记得,交叉验证并不关心任何未来的、未知的时间段。因此,构建一个 future DataFrame 并对其预测是不必要的。我在这个例子中这样做只是为了提醒你,在 第四章 处理非每日数据 中我们使用有规律间隔的数据时学到的第一个潜在陷阱。我们需要调整我们的 future DataFrame 以避免无约束的预测,我们在这里再次通过仅将未来日期限制在每月的 1 日、11 日和 21 日来做到这一点。以下是预测的结果:

图 13.5 – 降雨量预测

图 13.5 – 降雨量预测

它有一个几乎平坦的趋势,略微上升直到 2010 年,然后转向下降。正如你可能预料的那样,模型主要由年度季节性主导,12 月(南半球的夏季)降雨量几乎为零,而在 6 月降雨量达到最大。

现在我们来构建一个交叉验证图。我们将预测 90 天(horizon),并且每 30 天(period)创建一个新的折叠。我们的初始训练周期将是 1826 天,即 5 年。最后,让我们绘制 RMSE 图:

df_cv = cross_validation(model,
                         horizon='90 days',
                         period='30 days',
                         initial='1826 days',
                         parallel='processes')
df_p = performance_metrics(df_cv)
fig = plot_cross_validation_metric(df_cv, metric='rmse')
plt.show()

Prophet 使用 horizonperiodinitial 来计算一组均匀分布的截止点。然后 horizon 再次用来设置每个折叠预测的长度,但 periodinitial 在选择截止点后不再需要。

让 Prophet 自动设置截止点的影响是,它们与我们的数据相比位置不太方便。我们每月只有 3 天的数据,而且这 3 天并不是均匀分布的。这意味着在我们的交叉验证中,每个折叠实际上是从数据中的某个随机位置开始的,产生了一个似乎表明每个天都有数据的图:

图 13.6 – 带自动截止点的交叉验证

图 13.6 – 带自动截止点的交叉验证

cross_validation 函数将接受一个 cutoffs 参数,它接受一个用户指定的截止日期列表来使用。这也意味着 initialperiod 不再是必要的。此代码块将使用列表推导式遍历每年的每个月,然后是每个月的 1 日、11 日或 21 日,并创建一个包含 pandas Timestamp 的列表:

cutoffs = [pd.Timestamp('{}-{}-{}'.format(year, month,
                                          day))
           for year in range(2005, 2019)
           for month in range(1, 13)
           for day in [1, 11, 21]]

现在,如果我们重新绘制交叉验证,但发送这个截止日期列表,我们会看到一些显著不同的结果:

df_cv = cross_validation(model,
                         horizon='90 days',
                         parallel='processes',
                         cutoffs=cutoffs)
df_p = performance_metrics(df_cv)
fig = plot_cross_validation_metric(df_cv, metric='rmse')
plt.show()

现在,每个折叠都是从我们有数据的那一天开始的。接下来有数据的那一天将在 8、9、10 或 11 天后。因此,图表显示了 horizon 中发生预测的 4 个离散天数:

图 13.7 – 使用自定义截止点的交叉验证

图 13.7 – 使用自定义截止点的交叉验证

图 13.6图 13.7 都显示了略高于 20 的平均 RMSE,所以结果非常相似。区别仅仅是解释的简便性和一致性。如果你的数据是按月或任何月份增量记录的,你可能会经常遇到这种情况,因为它们的持续时间不一致。

使用网格搜索调整超参数

在本章的最后部分,我们将探讨网格搜索并通过一个例子来操作,继续使用这些降雨数据。如果你不熟悉网格搜索的概念,它是一种彻底检查所有合理的超参数组合与性能指标的方法,并选择最佳的组合来训练你的最终模型。使用 Prophet,你可能会决定选择以下超参数和值:

图 13.8 – Prophet 网格搜索参数

图 13.8 – Prophet 网格搜索参数

使用这些参数,网格搜索将遍历每个唯一组合,使用交叉验证来计算和保存性能指标,然后输出导致最佳性能的参数值集。

Prophet 没有像 sklearn 那样的网格搜索方法。不过,在 Python 中自己构建一个也很容易,所以让我们看看如何设置它。第一步是定义我们的参数网格。我们将使用 图 13.8 中显示的网格,但我们不包括假日在我们的模型中(天气不会定期查看日历并在发现假日时调整降雨量!),所以我们将省略这一点:

param_grid = {'changepoint_prior_scale': [0.5, 0.1, 0.01,
                                          0.001],
              'seasonality_prior_scale': [10.0, 1.0, 0.1,
                                          0.01],
              'seasonality_mode': ['additive',
                                   'multiplicative']}

接下来,我们将使用 Python 的 itertools 包来遍历该网格并创建每个唯一组合的列表。我们首先需要导入 itertools;同时,让我们也导入 numpy,因为我们稍后会用到它。我们还将创建一个空列表来保存所有的 RMSE 值,假设这是我们选择的性能指标:

import numpy as np
import itertools
all_params = [dict(zip(param_grid.keys(), value))
              for value in itertools.product(
                                  *param_grid.values())]
rmse_values= []

我们可以允许 Prophet 定义我们的截止期,但由于我们使用的是这些降雨数据,让我们自己设置 cutoffs

cutoffs = [pd.Timestamp('{}-{}-{}'.format(year, month,
                                          day))
           for year in range(2010, 2019)
           for month in range(1, 13)
           for day in [1, 11, 21]]

在评估结果之前,运行我们的网格搜索的最终一步是遍历我们在all_params列表中保存的每个组合,并构建一个模型、一个交叉验证数据框和一个性能指标数据框。

假设我们知道我们想要yearly_seasonality=4以保持曲线平滑,我们将使用该迭代的参数组合来完成模型实例化。在performance_metrics函数中,我们使用rolling_window=1。这意味着我们正在计算该折叠中 100%的数据的平均值来计算指标,因此我们只得到一个值,而不是一系列值:

for params in all_params:
    model = Prophet(yearly_seasonality=4, **params).fit(df)
    df_cv = cross_validation(model,
                             cutoffs=cutoffs,
                             horizon='30 days',
                             parallel='processes')
    df_p = performance_metrics(df_cv, rolling_window=1)
    rmse_values.append(df_p['rmse'].values[0])

那段代码将需要很长时间才能运行。毕竟,我们的all_params列表的长度是 32,这意味着你将训练和交叉验证 32 个总模型。我确实说过网格搜索是详尽的!(在典型的笔记本电脑上,你完成它可能需要大约 8-12 小时;为了加快示例,你可能考虑减少param_grid字典中的参数数量,例如,例如,param_grid = {'changepoint_prior_scale': [0.1, 0.01], 'seasonality_prior_scale': [1.0, 0.1]},这将只训练和交叉验证四个总模型。确保在更改param_grid后重新创建你的all_params字典。)为了检查结果,我们将构建一个包含参数组合及其相关 RMSE 的数据框,然后显示其中的一部分:

results = pd.DataFrame(all_params)
results['rmse'] = rmse_values
results.head()

完整的数据框有 32 行,每行对应一组参数,但在这里我们看到前五行:

图 13.9 – 网格搜索数据框

图 13.9 – 网格搜索数据框

最后,让我们使用 NumPy 找到具有最低 RMSE 值的参数,然后打印它们:

best_params = all_params[np.argmin(rmse_values)]
print(best_params)

打印best_params应该显示以下输出:

'changepoint_prior_scale': 0.01,
'seasonality_prior_scale': 1.0,
'seasonality_mode': 'additive'}

与通过网格搜索找到的最佳参数和之前我们使用的参数之间最大的区别是,变化点正则化应该设置得更加严格。使用较低的先验尺度,变化点的幅度会更小,趋势曲线会更平坦。直观上看,这似乎是合适的;尤其是对于更长期的预测,允许更大的趋势变化将会导致对未来远期的不切实际的降雨量预测。

可能最关键的参数调整是changepoint_prior_scale。如果这个值太小,趋势将无法很好地拟合方差。本应与趋势一起建模的方差将转而由噪声项建模。如果先验尺度太大,趋势将表现出过多的灵活性,并可能开始捕捉到一些年度季节性。在大多数情况下,0.50.001的范围将适用。

seasonality_prior_scale参数可能是影响第二大的参数。典型的范围通常是10,基本上没有正则化,直到0.01。任何更小的值,季节性可能被正则化到可以忽略不计的效果。你也可以选择将每个季节性设置为False,并使用add_seasonality来单独选择先验尺度,但这会导致你的网格搜索计算时间呈指数级增加。

你还可能想将fourier_order添加到你的网格搜索中,但我发现使用默认值构建快速模型、检查组件并选择符合直觉的四阶傅里叶顺序效果很好。在完全自动化的设置中,保持傅里叶阶数在默认值可能就足够了。

holidays_prior_scale也是一个可调整的参数,具有与seasonality_prior_scale许多相同的特性。只需记住,许多模型可能没有节假日,因此不需要包含此参数。

最后,应该始终考虑的关键参数之一是seasonality_mode。在这本书中,你已经学到了一些经验法则来帮助决定使用哪种模式,但往往并不清楚。最好的做法是简单地检查你的时间序列图,看看季节性波动的幅度是否随着趋势增长或保持不变。如果你无法判断,就继续将seasonality_mode添加到网格中。

通常,changepoint_range的默认值 80%将很好。它提供了一个很好的平衡,允许趋势在适当的地方改变,但不会允许它在最后 20%的数据中过度拟合,因为错误无法纠正。如果你是分析师并且密切关注,很容易看出默认范围是否不合适。但在完全自动化的设置中,可能最好是保守一些,将其保持在 80%。

剩余的参数最好不包含在网格搜索中。对于'growth',可以是'linear''logistic''flat',你应该作为分析师来选择。将其设置为'logistic'将需要设置'cap''floor'。许多剩余的参数,如n_changepoints和年度、周度和日季节性,最好通过搜索中已包含的参数来控制:对于变化点,是changepoint_prior_scale,对于季节性,是seasonality_prior_scale

最后的参数,mcmc_samplesinterval_widthuncertainty_samples,不会以任何方式影响你的yhat,因此不会影响你的性能指标。它们只控制不确定性区间。

在网格搜索中使用常识——这是一个非常漫长的过程,所以不要将每个参数和每个可能的值都包含在你的超参数网格中。通常,分析师可以采取的最佳方法是为这个过程提供直觉和人性化的触感,并让计算机进行数值计算。

摘要

在本章中,你学习了如何使用 Prophet 的性能指标来扩展交叉验证的有用性。你了解了 Prophet 自带六个指标,即 MSE、RMSE、MAE、MAPE、MdAPE 和覆盖率。你还了解了这些指标的优势和劣势,以及你可能想要使用或避免其中任何一个指标的情况。

接下来,你学习了如何创建 Prophet 的性能指标 DataFrame,并使用它来创建你偏好的交叉验证指标的图表,以便能够评估你的模型在一系列预测时间范围内的未见数据上的性能。然后,你使用这个图表和 WFP 的降雨数据来观察 Prophet 自动选择截止日期的情况并不理想,以及如何创建自定义截止日期。

最后,你通过全面搜索 Prophet 的超参数来整合所有这些内容。这个过程使你能够使用数据驱动技术来微调你的模型,并优化它以适应你选择的指标。

在下一章,这本书的最后一章,你将了解 Prophet 中的一些更多技巧,以帮助将你的模型投入生产环境。

第十四章:将 Prophet 产品化

如果您已经读完了这本书的所有章节,恭喜您!您已经为处理 Prophet 可以处理的任何预测任务做好了充分的准备。最后一章将介绍一些在生产环境中可能很有用的附加功能。

在本章中,您将学习如何保存训练好的模型以便稍后重用,您还将了解当新数据可用时如何加快模型拟合的速度。为了结束本章,您将发现一系列新的交互式图表,这些图表可以用于网络仪表板,以便与更广泛的受众分享您的工作。本章涵盖的主题如下:

  • 保存模型

  • 更新拟合模型

  • 使用 Plotly 制作交互式图表

技术要求

本章中示例的数据文件和代码可以在 github.com/PacktPublishing/Forecasting-Time-Series-Data-with-Prophet-Second-Edition 找到。

保存模型

第十一章 管理不确定性区间 中,您使用 马尔可夫链蒙特卡洛MCMC)抽样预测了巴尔的摩市每天的犯罪数量。这是一个计算量很大的过程,而你只使用了每日数据。如果你使用的是 Divvy 每小时数据,一个比每日数据大 10 倍的数据集,计算量将会更大。而且这两个数据集肯定比你在现实世界中遇到的大多数数据集都要小。如果 Prophet 没有提供保存您工作的方法,每次您训练一个模型时,您都必须将模型留在您的计算机内存中,直到您想要使用它为止。

也许您熟悉 Python 中的 pickle 模块——这对于在 sklearn 中保存训练好的模型来说效果很好。然而,Prophet 在后端使用 Stan 来构建其模型,这些 Stan 对象并不适合 pickle。幸运的是,Prophet 包含了一些将您的模型序列化为 JSON 并稍后重新打开的函数。因此,一旦您的模型训练完成,您就可以将其存放在一边,并在需要预测未来日期时再取出来。

我们将再次使用巴尔的摩犯罪数据来查看如何保存您的模型。我们需要导入 pandas 来读取 .csv 文件;当然,导入 Prophet 来构建我们的模型;我们还需要导入 json 来保存和重新打开文件。将模型对象转换为 JSON 并再次转换的函数是从 Prophet 的 serialize 包中导入的:

import pandas as pd
from prophet import Prophet
import json
from prophet.serialize import model_to_json, \
model_from_json

现在,我们将熟悉的过程运行一遍,打开我们的数据和训练一个模型。我们也在像在第十一章 管理不确定性区间 中做的那样,丢弃数据中的异常值:

df = pd.read_csv('baltimore_crime.csv')
df.columns = ['ds', 'y']
df.loc[df['y'] > 250, 'y'] = None
model = Prophet()
model.fit(df)

我们现在有了我们的训练模型。以前,你需要保持你的 Python 内核运行,并将模型保存在内存中,直到你想访问它为止。在一天结束时,你想要保存它,关闭你的机器,然后回家过夜,但你将失去所有这些工作。

在下面的代码中,你将使用with语句创建一个上下文管理器,这样你就可以打开一个 JSON 文件,Python 会在你完成后自动关闭它。以下语句中使用的'w''r'参数仅代表写入读取。此代码块使用 Prophet 的model_to_json函数将模型对象转换为 JSON 文件,并将其保存到你的硬盘上:

with open('baltimore_crime_model.json', 'w') as file_out:
    json.dump(model_to_json(model), file_out)

现在文件已保存,你可以安全地关闭 Python。要将 JSON 文件转换回模型对象,只需使用json_to_model函数:

with open('baltimore_crime_model.json', 'r') as file_in:
    model = model_from_json(json.load(file_in))

重新加载模型后,你可以像使用任何拟合模型一样使用它;例如,你可以绘制一个预测图:

forecast = model.predict()
fig = model.plot(forecast)

没有创建未来,这只是一个拟合模型:

图 14.1 – 巴尔的摩犯罪预测

图 14.1 – 巴尔的摩犯罪预测

保存并重新打开你的工作当然很有帮助,但真正的价值在于你保留了一个模型,并且每天用新数据更新它,正如我们接下来将要做的。

更新拟合模型

预测在预测模型中是独特的,因为数据的价值在于其新鲜度,每一刻的过去都会产生一组新的、有价值的数据来使用。对于预测模型来说,一个常见的情况是需要随着数据的增加而重新拟合模型。例如,巴尔的摩市可能会使用犯罪模型来预测他们可能期望明天发生的犯罪数量,以便提前更好地部署他们的警官。一旦明天到来,他们可以记录实际数据,重新训练他们的模型,并为第二天进行预测。

Prophet 无法处理在线数据,这意味着它不能添加单个新的数据观察值并快速更新模型。Prophet 必须离线训练——新的观察值将被添加到现有数据中,并且模型将完全重新训练。但不必从头开始完全重新训练,以下技术将在重新训练时节省大量时间。

Prophet 本质上是一个优化问题。在代码深处有一些设置,用于选择一组初始参数,Prophet 认为这些参数将接近实际参数,这些参数是建模预测曲线所需的。然后它创建其曲线,用现有数据点测量误差,更新参数以减少误差,并重复。

当 Prophet 试图越来越接近最佳参数集时,可能会发生数百或数千次迭代。你可以通过使用昨天模型的已经优化的参数来大大加快这个优化问题,将它们作为今天模型的更好初始化。假设今天的观测点不会显著改变整体模型,这通常是一个非常好的假设。让我们看看这个技术是如何工作的。

我们将首先创建一个包含最终观测值的 Baltimore 犯罪数据的 DataFrame。这是昨天的数据:

df_yesterday = df[df['ds'] < df['ds'].max()]

现在,我们将在这个数据上拟合model1

model1 = Prophet().fit(df_yesterday)

巴尔的摩市可以使用这个模型来预测第二天活动的预测,例如。现在,假设第二天已经到来;我们记录当天的犯罪水平,并希望用df今天的数据更新我们的模型,该数据包含最终的数据点。让我们首先从头开始,并使用 IPython 的timeit魔法函数来查看它需要多长时间:

%timeit model2 = Prophet().fit(df)

在我写这篇文章的时候,在我的当前机器上,这个过程根据输出大约花费了 865 毫秒:

865 ms ± 183 ms per loop (mean ± std. dev. of 7 runs, \
                          1 loop each)

现在,让我们再次尝试,但这次不是从头开始,我们将通过传递昨天的模型参数来为 Prophet 提供一个预热启动。我们首先需要定义一个类来正确格式化这些参数:

import numpy as np
class StanInit:
    def __init__(self, model):
        self.params = {
            'k': np.mean(model.params['k']),
            'm': np.mean(model.params['m']),
            'sigma_obs': \
             np.mean(model.params['sigma_obs']),
            'delta': np.mean(model.params['delta'],
                             axis=0),
            'beta': np.mean(model.params['beta'], axis=0)
        }
    def __call__(self):
        return self.params

这个类只是打开model.params字典,并将相关值保存到一个新字典中,该字典格式符合 Stan 后端的要求。我们现在使用这个类从model1中提取参数,并将这个初始化传递给fit方法,再次计时这个过程:

%timeit model2 = Prophet().fit(
     df,
     inits=StanInit(model1)())

当我运行那个命令时,我看到了训练速度超过 4 倍的提升:

195 ms ± 90 ms per loop (mean ± std. dev. of 7 runs, \
                         1 loop each)

0.195 秒与 0.865 秒相比是一个显著的改进。节省的时间取决于许多因素,并且即使你再次重复实验,也可能会发生变化。

尽管这种方法有一个注意事项:如果变化点的位置发生变化,更新后的模型实际上可能需要更长的时间来拟合,而不是从头开始拟合。因此,当相对于现有数据添加非常少量的新数据时,这种方法效果最好,正如我们在这里通过添加一天的数据到几年的数据中所做的那样。

使用 MAP 估计,正如我们在上一个例子中所做的那样,每次迭代都是一个优化问题。这意味着更好的初始化将大大加快速度。然而,在使用 MCMC 采样时,每次迭代必须完全运行通过马尔可夫链中的每个链接(回顾第十一章管理不确定性区间,以了解 MAP 估计与 MCMC 采样的区别)。

这意味着预热将显著加快 MAP 估计的速度,但不会加快 MCMC 样本的速度。然而,预热会增加每个马尔可夫链迭代的品质。所以,如果你使用 MCMC 样本进行预热,你可能会在结果质量显著降低的情况下减少 mcmc_samples 的数量。

减少 mcmc_samples 的数量为加快任何新模型的 MCMC 样本速度创造了机会。想法是使用 MAP 估计训练一个初始模型,然后使用该模型以 MCMC 样本预热一个模型,但使用的 mcmc_samples 比你通常选择的要少:

model1 = Prophet().fit(df)
model2 =
Prophet(mcmc_samples=200).fit(
    df,
    inits=StanInit(model1)())

在前面的代码块中,我们使用 MAP 估计和所有数据创建了一个初始 model1。然后,我们使用 model1 的参数预热 model2,它使用 MCMC 样本,但只有 mcmc_samples=200,而不是我们在 第十一章 中选择的 300管理不确定性区间。这将导致一个具有与之前相似性能的 MCMC 样本模型,但训练时间缩短了三分之二。

总结来说,使用 MAP 估计预热(即当 mcmc_samples=0 时)将加快你的模型训练速度。但是,当 mcmc_samples 大于 0 时,预热不会加快模型的速度,但在此情况下,你可以快速使用 MAP 估计训练一个模型,然后将你的模型预热到 mcmc_samples 设置为降低的值,而不会损失太多质量。现在,让我们学习如何使用 Prophet 制作交互式图表。

使用 Plotly 制作交互式图表

在本节的最后,我们将使用 Plotly 库构建一些交互式图表。Plotly 是一个完全独立的可视化包,与我们在本书中使用的 Matplotlib 包不同。使用 Plotly 制作的图表具有丰富的交互性,允许鼠标悬停时显示工具提示,可以放大和缩小图表,以及所有 sorts 的其他交互性。

如果你熟悉 Tableau 或 Power BI,Plotly 将类似的交互性带到了 Python 中。此外,Plotly 团队还构建了 Dash,这是一个用于创建基于网页的仪表板的库。创建此类仪表板的全教程超出了本书的范围,但我鼓励你学习这个有价值的工具,如果你希望与更广泛的受众分享你的 Prophet 预测。

Prophet 不会自动安装 Plotly 作为依赖项,所以在我们开始之前,你需要在你的机器上安装它。这是一个简单的过程,可以通过 condapip 完成。以下是 conda 安装命令:

conda install -c plotly plotly=5.11.0

如果你还没有安装 Anaconda 或 Miniconda,你将不得不使用 pip

pip install plotly==5.11.0

如果你倾向于在 Jupyter Notebook 或 JupyterLab 中工作,你还需要安装一些支持包。这可以通过 conda 完成,如下所示:

# Jupyter Notebook support
conda install "notebook>=5.3" "ipywidgets>=7.5"
# JupyterLab support
conda install "jupyterlab>=3" "ipywidgets>=7.6"
# JupyterLab renderer support
jupyter labextension install jupyterlab-plotly@5.11.0
# OPTIONAL: Jupyter widgets extension
jupyter labextension install @jupyter-widgets/jupyterlab-\
manager plotlywidget@5.11.0

如果你没有 conda,你也可以使用 pip

# Jupyter Notebook support
pip install "notebook>=5.3" "ipywidgets>=7.5"
# JupyterLab support
pip install "jupyterlab>=3" "ipywidgets>=7.6"
# JupyterLab renderer support
jupyter labextension install jupyterlab-plotly@5.11.0
# OPTIONAL: Jupyter widgets extension
jupyter labextension install @jupyter-widgets/jupyterlab-\
manager plotlywidget@5.11.0

如果你遇到这些命令中的任何问题,最好的资源是 Plotly 自己的文档:plotly.com/python/getting-started/

你已经在本书的例子中学习了 Prophet 的plot包中的许多绘图函数。还有四个我们还没有涉及到的函数;这些函数与你已经学习过的 Matplotlib 对应函数具有许多相同的参数,但输出的是 Plotly 图表。

重要提示

这本书将包含 Plotly 图表的静态图像,但如果你在 Jupyter 笔记本中运行示例代码,你将能够在丰富交互的环境中操作图像。

为了演示这些工具,让我们再次使用 Divvy 数据,并使用温度作为额外的回归器。在这个部分中我们根本不会使用 Matplotlib,所以不需要导入它。我们已经从前面的部分中导入了 pandas 和 Prophet,但在这里我们还需要进行一些额外的导入。

如果你还记得从第九章中的包括额外的回归器,我们人为地减少了我们的训练数据两周,这样我们就可以在同时使用天气条件作为额外回归器的情况下预测两周。我们在这里也会这样做,因此我们需要导入timedelta来帮忙。但最重要的是,我们将导入plotly.offline并初始化笔记本模式:

from datetime import timedelta
import plotly.offline as py
py.init_notebook_mode()

现在,让我们读取我们的数据并将其放入一个数据框中。在这个例子中,我们只会使用一个额外的回归器,即temperature

df = pd.read_csv('divvy_daily.csv')
df = df[['date', 'rides', 'temperature']]
df['date'] = pd.to_datetime(df['date'])
df.columns = ['ds', 'y', 'temp']

最后,我们就像以前一样构建我们的模型。我们创建一个温度回归器,然后在数据上拟合模型,同时排除最后两周。接下来,我们使用future数据框中的未拟合的 2 周temperature数据进行两周的未来预测:

model = Prophet(seasonality_mode='multiplicative',
                yearly_seasonality=6)
model.add_regressor('temp')
model.fit(df[df['ds'] < df['ds'].max() - \
          timedelta(weeks=2)])
future = model.make_future_dataframe(periods=14)
future['temp'] = df['temp']
forecast = model.predict(future)

到目前为止,这些都应该是复习内容(除了导入和初始化 Plotly)。但现在,我们将从plot包中导入那四个最终函数:

from prophet.plot import (
    plot_plotly,
    plot_components_plotly,
    plot_forecast_component_plotly,
    plot_seasonality_plotly
)

让我们逐一运行这些函数。

Plotly 预测图表

首先是plot_plotly函数。要使用此函数,你只需传入模型和预测。我还包括trend=True参数以在图表中包含趋势线。你也可以添加changepoints=True,这将完全模仿 Matplotlib 的add_changepoints_to_plot函数。py.iplot(fig)行与 Matplotlib 的plt.show()类似:

fig = plot_plotly(model, forecast, trend=True)
py.iplot(fig)

此截图还显示了在悬停在2015 年 5 月 10 日的点上的工具提示:

图 14.2 – Plotly 图表

图 14.2 – Plotly 图表

Plotly 组件图表

接下来,我们将查看 Plotly 组件图表。这与 Matplotlib 版本非常相似,但它还包括交互性。我还包括figsize参数来稍微减小这个图表的大小:

fig = plot_components_plotly(model, forecast ,
                             figsize=(800, 175))
py.iplot(fig)

此图表显示了与plot_components相同的子图:

图 14.3 – Plotly 组件图表

图 14.3 – Plotly 组件图表

Plotly 单个组件图表

我想使用这些 Divvy 数据,这样我们就可以使用额外的温度回归器。我们可以使用这个下一个函数在 图 14.3 中的任何子图上绘制,但所有这些都可以用其他函数处理,除了额外的回归器。仅绘制这些回归器需要使用 plot_forecast_components_plotly 函数。在这里,我们传递了 'temp' 组件:

fig = plot_forecast_component_plotly(model, forecast,
                                     'temp')
py.iplot(fig)

与本节中的其他图表一样,静态图像并不能完全展示它们的魅力。Plotly 是为了在交互式环境中使用;这些图表迫切需要放置在仪表板上,而不是打印在书中。在这里,我再次展示了悬停工具提示:

图 14.4 – Plotly 温度图

图 14.4 – Plotly 温度图

Plotly 季节性图

在最终的 Plotly 函数中,我们将使用 plot_seasonality_plotly 函数绘制年度季节性:

fig = plot_seasonality_plotly(model, 'yearly')
py.iplot(fig)

为了节省空间,Plotly 工具栏被省略在组件图中,但包含在其他所有图表中;您可以在 图 14.214.414.5 的右上角看到它。在下面的季节性图中,我使用了工具栏中的 切换峰值线比较数据 按钮来向悬停工具提示添加更多信息,如下所示:

图 14.5 – Plotly 季节性 图

图 14.5 – Plotly 季节性图

我强烈建议您在 Jupyter 笔记本中探索这些图表,如果您觉得它们很有用,可以考虑使用 Dash 将它们组合到一个仪表板中。网上有很多相关的教程。

摘要

这本书的最后一章是所有章节中最可选的,但对于那些经常在生产环境中工作的人来说,这些工具将非常有价值。

在本章中,您学习了如何使用 JSON 序列化将模型保存到您的硬盘上,这样您就可以在以后无需重新训练模型的情况下分享或打开它。您还学习了如何更新已经拟合好的模型,这是另一种旨在节省您时间的程序。最后,您考察了一种新的图表格式,这是一个令人印象深刻的工具,可以使您的图表在浏览器中交互式显示,希望您已经看到了将这些信息打包到仪表板中的潜力。

本章所学的内容将帮助您随着时间的推移和新数据的到来更新您的模型,并通过基于网络的交互式仪表板共享该模型。

posted @ 2025-09-03 10:21  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报