Python 时间序列数据预测(一)
原文:
annas-archive.org/md5/b7bdd7fe65b0a510d6efd9f37e4dc576译者:飞龙
前言
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_changepoints和changepoint_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/err…,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
请通过copyright@packt.com发送链接到该材料。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《使用 Prophet 预测时间序列数据》,我们非常乐意听到您的想法!请点击此处直接转到此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本。
感谢您购买此书!
您喜欢在路上阅读,但无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日邮箱访问权限。
按照以下简单步骤获取优惠:
- 扫描下面的二维码或访问以下链接
packt.link/free-ebook/9781837630417
-
提交您的购买证明
-
就这样!我们将直接将您的免费 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 – 一个虚假的时间序列关联(www.tylervigen.com/spurious-co…
显然,每年在游泳池中溺水的人数与尼古拉斯·凯奇出演的电影数量完全无关。它们之间没有任何影响。然而,通过将时间序列数据视为独立数据的谬误,Vigen 已经表明,纯粹是随机机会,这两组数据确实存在显著的关联。当忽略时间序列数据中的依赖性时,这种随机机会更有可能发生。
现在您已经了解了时间序列数据究竟是什么,以及它与其他数据集的区别,让我们来看看模型发展历程中的几个里程碑,从最早的模型到 Prophet。
移动平均和指数平滑
可能最简单的预测形式是 移动平均(MA)。通常,移动平均被用作 平滑技术,以在变化很大的数据中找到一条更直的线。每个数据点都调整到周围 n 个数据点的平均值,其中 n 被称为窗口大小。例如,窗口大小为 10 时,我们会调整一个数据点,使其成为之前 5 个值和之后 5 个值的平均值。在预测环境中,未来值被计算为 n 个先前值的平均值,因此,窗口大小为 10 时,这意味着 10 个先前值的平均值。
使用移动平均的平衡行为是,您希望有一个大的窗口大小来平滑噪声并捕捉实际趋势,但随着窗口大小的增大,您的预测将显著滞后趋势,因为您需要回溯得更远来计算平均值。指数平滑背后的思想是对随时间平均的值应用指数递减的权重,给近期值更多的权重,给较远期值更少的权重。这允许预测对变化更加敏感,同时仍然忽略大量噪声。
如您在以下模拟数据的图中所示,移动平均线的行为比指数平滑线更粗糙,但两条线仍然同时调整趋势变化:
图 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),其中p、d和q都是非负整数。
ARIMA 模型的一个问题是它们不支持季节性,或具有重复周期的数据,例如白天温度上升而夜晚下降,或夏季上升而冬季下降。季节性 ARIMA(SARIMA)是为了克服这一缺点而开发的。与 ARIMA 表示法类似,SARIMA 模型的表示法为SARIMA(p, d, q)(P, D, Q)m,其中P是季节性自回归阶数,D是季节性差分阶数,Q是季节性移动平均阶数,而m是单个季节周期的时间步数。
你还可能遇到 ARIMA 模型的其它变体,包括向量 ARIMA(VARIMA),用于具有多个时间序列作为向量的情况;分数 ARIMA(FARIMA)或自回归分数积分移动平均。
PD:作为 P 关键字的风格(ARFIMA),两者都包括分数差分度,允许在时间上相隔较远的观测值具有非可忽略的依赖性;以及SARIMAX,这是一个季节性 ARIMA模型,其中X代表添加到模型中的外生或额外变量,例如将降雨预报添加到温度模型中。
ARIMA 通常表现出非常好的结果,但其缺点是复杂性。调整和优化 ARIMA 模型通常计算成本高昂,成功的结果可能取决于预测者的技能和经验。这不是一个可扩展的过程,更适合熟练从业者进行临时分析。
ARCH/GARCH
当数据集的方差随时间变化时,ARIMA 模型在建模时会遇到问题。特别是在经济学和金融学中,这是常见的。在金融时间序列中,大回报往往伴随着大回报,而小回报往往伴随着小回报。前者称为高波动性,后者称为低波动性。
自回归条件异方差(ARCH)模型是为了解决这个问题而开发的。异方差性是一种说法,意味着数据的变化或分布在整个过程中不是恒定的,其对立术语是同方差性。差异在此可视化:
图 1.3 – 斯凯迪斯提
罗伯特·恩格尔于 1982 年首次介绍了 ARCH 模型,通过将条件方差描述为先前值的函数。例如,白天用电量的不确定性远大于夜间用电量。因此,在电力使用模型中,我们可能会假设白天的小时数具有特定的方差,而夜间使用则具有较低的方差。
1986 年,蒂姆·博勒尔塞夫和斯蒂芬·泰勒在他们的广义 ARCH(GARCH)模型中引入了移动平均成分。在电力示例中,使用量方差是时间的函数,但波动性的波动可能并不一定发生在特定的时间,波动本身是随机的。这就是 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.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 通过 Python 或 R 编程语言访问,并依赖于许多额外的开源库。这使得它具有很大的灵活性,因为用户可以调整功能或甚至添加全新的功能以适应他们特定的需求,但这也带来了潜在的可用性困难。这正是本书旨在简化的目标。
在本章中,我们将根据您使用的操作系统,向您展示整个安装过程,然后我们将通过模拟过去几十年大气二氧化碳水平来共同构建我们的第一个预测模型。
本章将全面涵盖以下内容:
-
安装 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 版本
一旦安装了 Anaconda 或 Miniconda,你将能够访问 gcc,这是 PyStan 所需的编译器集合,然后通过在命令提示符中运行以下两个命令来安装 Prophet 本身,这将自动安装 PyStan:
conda install gcc
conda install -c conda-forge prophet
第二个命令包含额外的语法,指示 conda 在 conda-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,那么你对它的复杂性应该也很熟悉。
只需确保你已经安装了 gcc、g++ 和 build-essential 编译器,以及 python-dev 和 python3-dev Python 开发工具。如果你的 Linux 发行版是 Red Hat 系统,请安装 gcc64 和 gcc64-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 – 凯林曲线,显示大气中二氧化碳的浓度
由于其季节性和上升趋势,这条曲线是尝试使用 Prophet 的良好候选。这个数据集包含 53 年间的超过 19,000 个每日观测值。二氧化碳的测量单位是百万分之一(PPM),表示每百万个空气分子中的二氧化碳分子数。
要开始我们的模型,我们需要导入必要的库,pandas 和 matplotlib,并从 prophet 包中导入 Prophet 类:
import pandas as pd
import matplotlib.pyplot as plt
from prophet import Prophet
作为输入,Prophet 总是需要一个包含两列的 pandas DataFrame:
-
ds,日期戳,应该是 pandas 预期格式的datestamp或timestamp列 -
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 的范式,首先创建模型类的实例,然后再调用 fit 和 predict 方法:
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)。
我们使用可选的 xlabel 和 ylabel 参数来标记坐标轴,但对于可选的 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 预测
就这样!在这 12 行代码中,我们已经得到了我们的 10 年预测。
解释预测 DataFrame
现在,让我们通过显示前三个行(我已经将其转置,以便更好地在页面上查看列名)来查看那个 forecast DataFrame,并了解这些值是如何在前面的图表中使用的:
forecast.head(3).T
执行该命令后,你应该会看到以下表格打印出来:
图 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 组件图
趋势持续增加,但随着时间的推移似乎有一个变陡的斜率——大气中二氧化碳浓度的加速。趋势线还显示了预测年份中很小的不确定性区间。从这条曲线中,我们了解到 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 – 分析师在循环预测
分析师在循环预测是一个迭代的过程。分析师首先使用 Prophet 以默认参数构建模型。Prophet 已经针对速度进行了优化,所以(通常)只需几秒钟,它就能输出一个非常可接受的预测。然后 Prophet 可以评估预测并揭示潜在问题,在将其交还给分析师进行快速视觉检查之前。如果预测符合分析师的预期,他们的工作就完成了!但是,当 Prophet 揭示出较差的性能或分析师的视觉检查提供不满意的结果时,分析师可以直观地调整模型以提高性能并更好地使结果与预期一致。
这个周期可以根据需要重复进行。Prophet 的美妙之处在于,由于预测非常快,一个完整的周期通常可以在不到一分钟内完成。因此,具有广泛领域知识但统计知识有限的分析师能够创建高度定制的预测。分析师可能希望调整的参数包括以下内容:
-
容量:预测可以渐近接近的上限或下限。容量的一个例子可能是特定时间点的总市场规模。
-
变化点:这些是预测趋势突然改变方向的时间点。这些可能是由重大产品更新甚至引起显著关注的媒体报道引起的。
-
节假日和季节性:由于节假日和季节性的影响,任何预测的行为都会有所不同。例如,火鸡的销售在感恩节前一周达到顶峰,而沙滩球在仲夏时节销量最好。理解自己产品的分析师可以轻松地将这种情报输入到他们的模型中。
-
平滑参数:在视觉检查模型后,分析师可以直观地看出模型是否过度或欠拟合数据。平滑参数可以用来减少模型的噪声,或者指导模型未来可以预期多少季节性变化。
经常会有关于统计预测和判断性预测之间差异的讨论。统计预测是一个数学上拟合历史数据的模型,而判断性预测(有时也称为管理预测)是一个通过人类专家利用他们通过时间序列经验所学到的知识来产生预测的过程。判断性预测可以包含比统计预测更多的信息,并且可以更快速地响应变化条件,但它们扩展性不好,需要分析师做大量工作。统计预测更容易自动化,并且可以扩展以满足预测需求,但它在可以整合的领域知识量上有限。
Facebook 的分析师在环范式是这两种不同方法最佳品质的结合:强大自动化,但简单直观易于调整。然而,尽管这种简单性,Prophet 实际上在观察其内部工作时相当复杂。虽然创建准确的预测不需要理解 Prophet 模型背后的数学,但了解 Prophet 正在做什么将只会提高你的预测能力。如果你准备好了,现在让我们看看 Prophet 构建预测所使用的方程组。
Prophet 背后的数学
在 第一章,时间序列预测的历史与发展 中,我们介绍了 Prophet 作为一个加性回归模型。该章节的 图 1.4 和 1.5 通过展示如何将代表模型组件的几个不同曲线简单相加以得到最终模型来说明这一点。从数学上讲,这可以用以下方程表示:
| (1) |
|---|
模型在时间 的预测
由
给出。这个函数由四个部分组成,相加(或相乘;参见第五章,处理季节性,了解更多信息):
-
是增长成分,或一般趋势,它是非周期的
-
是季节性成分——即所有周期性成分的总和
-
是假日成分,代表所有一次性特殊事件
-
是误差项
这四个组件(实际上,只有前三个组件——误差项只是为了解释模型无法容纳的噪声)的组合是 Prophet 构建预测所需的一切。然而,这个方程式的简单性掩盖了许多复杂性。要真正理解正在发生的事情,我们需要逐个分析这些组件。
线性增长
首先,我们将查看增长项。Prophet 引入了两种增长模式,线性和对数,分析师在设置模型时需要选择其中之一。(分析师如何选择?我们将在第七章,控制 增长模式!)这个选择指示 Prophet 使用两个方程之一来表示这个项。我们将从查看线性版本开始:
| (2) |
|---|
变量是增长率,线的斜率。熟悉回归的人会认出,一条线的基本方程是
。我们看到方程*(2)*与这个基本方程有相似之处,如果你把括号里的所有东西收集起来并放在一起。但简单线和 Prophet 的分段线性模型之间的一个关键区别就在其名称中:它是分段的。斜率可以随
函数变化:
图 3.2 – Prophet 模型中的垂直虚线是一个变化点,斜率在这里发生变化
这就是为什么,斜率,有所增加:
。
变量是一个调整率的向量(即在每个变化点发生的斜率变化),其中
是在时间
发生的斜率变化。
向量标识了每个变化点的位置,并定义如下:
| (3) |
|---|
简单来说,这意味着线的斜率是恒定的,但允许斜率进行调整。在任何时间上,斜率等于
基本率加上到该点为止的所有斜率调整。
为了使线条连续,变化点之间的每个区间必须通过偏移参数进行调整,以便区间的端点相连。在方程*(2)*中,是偏移参数。就像斜率一样,这个偏移参数是一个基础偏移量加上直到时间
的所有偏移量。从数学上讲,这是通过将
加上一个变化点位置向量,
,乘以一个偏移调整向量,
来完成的。在这个线性模型中,
被设置为
:
| (4) |
|---|
这就是 Prophet 的线性模型中对数的定义!现在让我们看看如何对对数模型进行修改。
对数增长
与一般直线的方程类似,对数曲线的一般方程由以下方程给出:
| (5) |
|---|
与线性模型中的方程*(2)一样,是增长率,而
是一个偏移参数。方程(5)需要我们对方程(2)*中做出的许多调整,以允许变化点。Prophet 还允许
,即承载能力,随时间变化。这个值本质上是一个曲线趋近但永远不会完全达到的渐近线:
图 3.3 – Prophet 的对数模型,承载能力设置为 500
注意,的承载能力是时间的函数。这意味着渐近线不必是恒定的,而可以是任何任意曲线。在这里,我们演示了一个恒定承载能力切换到线性增加的承载能力:
图 3.4 – 承载能力可能不一定保持恒定
如果我们对方程*(5)中的和
进行与方程(2)*中相同的调整,并允许
成为时间的函数,那么我们得到以下方程:
| (6) |
|---|
这就是 Prophet 的对数增长模型。在线性模型中,,但在对数模型中,
必须采取更复杂的形式:
| (7) |
|---|
虽然方程*(7)比(4)*更复杂,但它本质上执行相同的任务:确保在每个变化点,趋势曲线的每个区间的端点相连,并且线条是连续的。
增长项,,是完整 Prophet 模型中最复杂的部分。从这里开始会变得简单!现在,我们将继续了解
,即季节性项。
季节性
时间序列数据通常表现出周期性,尤其是在商业数据中,其中经常存在年度周期、周周期和日周期。Prophet 可以在其季节性项中接受无限多个这样的周期性组件,如图*(1)*所示。
Prophet 使用傅里叶级数来模拟这个术语。傅里叶级数简单地说就是多个正弦曲线的总和。这个最终曲线的形状由每个组成曲线的振幅、相位和周期决定。傅里叶级数可以包含无限多个组件,因此可以拟合几乎任何任意的周期函数,如下所示:
图 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 拥有了构建预测所需的一切!它只需简单地将增长成分、
季节性成分和
假日成分相加,以提供最终的预测
。
摘要
本章介绍了 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
数据按月报告,每月有一个测量值。乘客数量按千计,这意味着第一行表示 1949 年 1 月 1 日开始的那个月有 112,000 名商业乘客乘坐飞机。
正如我们在上一章关于莫纳罗亚山的讨论中做的那样,我们接下来将实例化我们的模型并对其进行拟合。使用这个 Air Passengers 数据集,我们将 seasonality_mode 设置为 'multiplicative',但你现在不必担心这一点——我们将在 第五章,处理季节性 中讨论它。接下来,我们将数据发送到 fit 方法,然后创建一个 future DataFrame。让我们预测 5 年。最后,我们将使用 predict 与 future 结合,然后绘制预测图以查看我们的结果:
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 – 以日频率进行的未来预测
我们可以通过指示 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 – 以月频率进行的未来预测
这好多了,这正是我们可能期望的预测图的样子。通过将 freq 参数传递给 make_future_dataframe 方法,我们避免了要求 Prophet 预测它没有训练知识的日期。默认情况下,频率设置为 'D',即 每日,我们的周期是我们想要预测的天数。每次更改频率到其他设置时,请确保将您的周期设置为相同的比例。
现在,让我们看看使用子日数据时会发生什么变化。为了做到这一点,我将引入一个新的数据集:Divvy。
使用子日数据
在本节中,我们将使用来自伊利诺伊州芝加哥的 Divvy 自行车共享计划 的数据。这些数据包含了从 2014 年初到 2018 年底每小时骑行的自行车次数,并显示出一种普遍的增长趋势以及非常强烈的年度季节性。由于这是按小时的数据,并且夜间骑行次数非常少(有时每小时为零),数据确实显示了在低端的测量密度:
图 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.6所示的成分图:
图 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.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 预测
在这里,我们看到未来期间的无约束估计值导致预测的波动很大。这与我们在使用月度数据预测每日预测时观察到的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.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.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 天预测
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 日季节性
如你所见,上午 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 – 加法季节性与乘法季节性
上曲线展示了加法季节性——追踪季节性边界的虚线是平行的,因为季节性的幅度没有变化,只有趋势在变化。然而,在下曲线中,这两条虚线并不平行。当趋势低时,季节性引起的扩散低;但当趋势高时,季节性引起的扩散高。这可以用乘法季节性来建模。
让我们通过使用上一章中引入的 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 – 具有加法季节性的航空旅客
如您所见,在数据早期,在 1949、1951 和 1952 年,Prophet 的预测值(实线)具有比数据(点)指示更极端的季节性波动。在序列后期,在 1958、1959 和 1960 年,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 – 具有乘法季节性的航空旅客
这是一个更好的拟合!现在,Prophet 与整体趋势的增长匹配了季节性波动的增长。此外,比较 图 5.2 和 图 5.3(围绕实线的浅色区域)之间的误差估计。当 Prophet 尝试将加法季节性拟合到包含乘法季节性的数据序列时,它显示更宽的不确定性区间。Prophet 知道在先前的模型中拟合不佳,并且对其预测不太确定。
这里还有最后一件事我想让你注意。让我通过绘制成分来展示给你看:
fig_a2 = model_a.plot_components(forecast_a)
plt.show()
这张图展示了 model_a 的加法季节性成分:
图 5.4 – 带有加法季节性的组件图
现在,让我们绘制model_m的组件图:
fig_m2 = model_m.plot_components(forecast_m)
plt.show()
将以下图表与图 5.4中显示的图表进行比较:
图 5.5 – 带有乘法季节性的组件图
它们看起来几乎完全相同。趋势相同,从1949 年开始略高于100,到1961 年时刚好低于500,在1954 年有一个轻微的转折点,趋势加速。年季节性表现正如我们所预期的那样,夏季乘客数量达到峰值,圣诞节假期和春季假期的局部峰值较小。两个图表之间的区别在于季节性曲线的Y轴。
在加法模型中,Y轴的值是绝对数值。在乘法模型中,它们是百分比。这是因为,在加法季节性模式中,季节性被建模为趋势的附加因素,值只是简单地加到或从它中减去。但在乘法季节性模式中,季节性代表相对于趋势的相对偏差,因此季节性效应的大小将取决于趋势在该点预测的值;季节性效应是趋势的百分比。
小贴士
当你的数据表示随时间变化的某种计数,例如每月的航空公司乘客计数时,你通常会使用乘法季节性来建模。使用加法季节性可能会导致预测出负值(例如,每月负 100 名乘客是不可能的),而乘法季节性只会将值缩小到接近零。
选择加法或乘法季节性可能一开始有点棘手,但如果你只是记住季节性可能是一个绝对因素或相对因素,并观察数据是否具有恒定的分布,你应该不会在模型上遇到任何麻烦。
现在你已经了解了这两种季节性模式之间的区别,让我们将其应用于一个新的数据集,Divvy 自行车共享,并继续在 Prophet 中学习季节性。
在本书的许多例子中,我们将使用芝加哥 Divvy 自行车共享计划的数据来创建示例。在前一章中,我们使用了 Divvy 的小时数据,但在这个部分,我们将使用每日数据。
小贴士
我们在第四章中使用了小时级的 Divvy 数据,处理非每日数据,以展示每日成分图以及如何处理数据中的常规间隔;在本章中,当我们查看条件季节性时,我们还将再次使用小时级数据。但除此之外,在本书的其余部分,我们将使用每日的 Divvy 数据,如下所示。在这些情况下,我们不需要小时级数据的额外粒度,改为每日数据可以将处理时间从分钟减少到秒。此外,每日数据集还包含相关的天气和温度列,这些列在小时级数据集中缺失,我们将在第九章中包括额外的回归因子。
下面是每日 Divvy 数据的样子:
图 5.6 – Divvy 每日骑行次数
这是一种计数数据,因为它代表了每天的骑行次数,你也可以看到季节性的幅度随着趋势的增长而增长(如果我们从图 5.1中绘制那些虚线,追踪数据的上下界限,线条将会发散)。正如我们刚刚学到的,这些都是乘法季节性的指示,所以让我们确保在实例化我们的模型时设置这一点。在先前的例子中,我们已经导入了必要的 Python 库,因此我们可以从这个例子开始加载数据。
此数据集包含一些额外的天气和温度条件列,我们将使用这些列在第九章中丰富我们的预测,包括额外的回归因子。一旦我们加载了数据,我们就可以看到这些额外的列:
df = pd.read_csv('divvy_daily.csv')
df.head()
在 Jupyter 笔记本或 IPython 实例中运行此命令将显示以下 DataFrame:
图 5.7 – Divvy DataFrame
目前,我们只需要date和rides列。让我们将这些列加载到我们的 Prophet DataFrame 中,并使用适当的列名。我们将在第九章中处理weather和temperature,包括额外的回归因子:
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 预测
我们可以看到,预测趋势确实随着实际数据增加,年度季节性也与之匹配。现在,让我们绘制我们的组件图,看看它们揭示了什么:
fig2 = model.plot_components(forecast)
plt.show()
如您在输出图中所见,Prophet 已将此数据集中的三个组件隔离出来:趋势、每周季节性和年度季节性:
图 5.9 – Divvy 组件图
默认情况下,Prophet 会识别一个包含至少 2 年完整数据的'ds'列。'ds'列少于 1 天(在此情况下不适用)。
趋势在前两年内线性增长相对较快,但随后弯曲并略微放缓,剩余两年,预测年份继续遵循这一斜率。我们可以看到,Divvy 网络在此期间的平均使用量从 2014 年的每天约 3500 次增长到 2018 年底的每天约 8500 次。
每周的季节性表明,周末每天的骑行次数大约减少了 30%——也许所有这些骑行者都是上班族——而工作日的骑行次数比趋势高 10-20%。这符合我们的直觉,即工作日和周末可能表现出不同的模式。
现在,观察年度季节性可以发现,夏季的骑行次数比趋势高约 60%,而冬季的骑行次数低 80%。同样,这也符合直觉。那些上班族在天气寒冷和下雨时会开车或乘坐公共交通。
您会注意到这个年度季节性曲线相当波动,就像我们在上一章中注意到的小时 Divvy 数据一样。您可能期望得到一条更平滑的曲线,而不是有这么多拐点的曲线。这是由于我们的年度季节性过于灵活——它有太多的自由度或太多的数学参数控制曲线。在 Prophet 中,控制季节性曲线的参数数量称为傅里叶阶数。
使用傅里叶阶数控制季节性
季节性是 Prophet 工作原理的核心,傅里叶级数用于模拟季节性。为了理解傅里叶级数是什么,以及傅里叶阶数如何与之相关,我将使用线性回归的一个类比。
你可能知道,在线性回归中增加多项式方程的阶数总会提高你的拟合优度。例如,简单的线性回归方程是 ,其中
是直线的斜率,
是
截距。将你的方程阶数增加到,比如说,
,总会提高你的拟合,但风险是过拟合和捕捉噪声。你可以通过任意增加多项式方程的阶数来达到一个
值为 1(完美拟合)。以下图示说明了高阶拟合开始变得相当不切实际和过拟合的情况:
图 5.10 – 高阶多项式线性回归
线性实线确实正确地得到了数据的上升趋势,但它似乎遗漏了一些细微的细节。二次虚线是一个更好的拟合(实际上,这些数据是从具有随机噪声的二次方程中模拟出来的)。然而,五次和十次曲线正在对随机噪声进行过拟合。如果我们从这个分布中采样更多的数据点,它们很可能会使五次和十次曲线剧烈变化以适应新的数据,而线性和二次曲线只会略有偏移。我们可以说多项式的阶数与曲线可以有多少个弯曲来拟合数据成正比。
傅里叶级数简单地说就是正弦波的求和。通过改变这些单个正弦波的形状——振幅,即波的高度;周期,即从峰值到峰值的距离;以及相位,即波沿长度的哪个位置开始一个周期——我们可以创建一个新的非常复杂的波形。
在线性域中,我们改变多项式的阶数来控制曲线的灵活性,我们改变 β 系数来控制曲线的实际形状。同样,在周期域中,我们改变傅里叶级数中的正弦波数量——这就是傅里叶阶数——来控制最终曲线的灵活性,而我们(或者更准确地说,Prophet 的拟合方程)改变单个波的振幅、周期和相位来控制我们最终曲线的实际形状。你可以在以下图中看到这个求和是如何工作的:
图 5.11 – 四阶傅里叶级数
实线简单地是四个正弦波各自的和。通过在模型中任意增加傅里叶阶数,我们总能达到任何一组数据的完美拟合。但就像在线性域中一样,这种方法不可避免地会导致过拟合。
记得在图 5**.9中,当我们绘制 Divvy 预测的组成部分时,年度季节性过于波动?这是傅里叶阶数过高造成的。默认情况下,Prophet 使用 10 阶数拟合年度季节性,3 阶数拟合周季节性,如果提供了子日数据,则使用 4 阶数拟合日季节性。通常,这些默认值工作得非常好,不需要调整。然而,在 Divvy 的情况下,我们需要降低年度季节性的傅里叶阶数以更好地拟合数据。让我们看看如何做到这一点。
我们已经从上一个示例中导入了必要的库并将数据加载到我们的df DataFrame 中,因此为了继续,我们需要实例化一个新的 Prophet 对象,并带有修改后的年度季节性。和之前一样,我们将季节性模式设置为乘法,但这次我们将包括yearly_seasonality参数并将其设置为4。这就是我们设置傅里叶阶数的地方。
您可以自己尝试不同的值;我发现4在大多数情况下提供了一个干净的曲线,没有太多的灵活性,这正是我所需要的。同样,如果我们想改变weekly_seasonality或daily_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 年度季节性
我们成功消除了之前尝试中的波动,同时仍然保持了季节性的清晰形状。这似乎更加合理!
到目前为止,我们只使用过 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 – 每月太阳黑子数量
数据看起来相当嘈杂;似乎有几个异常值,循环并不完全干净。每个周期的峰值变化很大。为了看看 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 – 太阳黑子成分图
该图仅显示趋势和 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。然后,我们将使用pandas的apply方法创建一个表示周末的新列,并使用波浪号(~)运算符为另一个表示周中的新列取反。最后,让我们输出 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 条件季节性数据框
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 参数必须传递训练数据框中列的名称,并包含布尔值以标识要应用季节性的行 – 我们的 weekend 和 weekday 列。就像我们在太阳黑子示例中所做的那样,我们还需要命名季节性并标识周期和傅里叶阶数:
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 – 每日工作日成分图
在工作日,趋势与我们使用默认每日季节性时看到的情况非常相似 – 早上 8 点左右有一个峰值,下午 6 点左右另一个峰值,午夜过后有一个小峰。尽管如此,我们假设周末将看到一个非常不同的模式。让我们看看图表来了解:
图 5.17 – 每日周末成分图
的确,我们看到了差异!正如你的直觉所暗示的,在周末,Divvy 骑行者比在工作日更晚开始,直到中午逐渐增加客流量,然后逐渐减少到午夜。在工作日我们没有看到中午的低谷。
到目前为止,在本章中,您使用了 Air Passengers 数据来学习加性和乘性季节性的区别。后来,您使用了 Divvy 数据来学习如何添加自定义季节性和条件季节性。您还使用了 Divvy 数据来发现傅里叶阶数,并学习了如何控制季节性曲线的灵活性。然而,Prophet 还为您提供了一个控制季节性的杠杆:正则化。
正则化季节性
通常,在用机器学习解决问题时,涉及的数据非常复杂,一个简单的模型往往不足以捕捉到要找到的模式的全部微妙之处。简单的模型往往会欠拟合数据。相比之下,一个更复杂的模型,具有许多参数和很大的灵活性,可能会倾向于过拟合数据。使用更简单的模型并不总是容易,或者可能。在这些情况下,正则化是一种很好的技术,可以用来控制过拟合。
Prophet 是一个非常强大的预测工具,如果不加注意,有时很容易过拟合数据。这就是为什么理解 Prophet 的正则化参数非常有用的原因。
小贴士
如果一个模型没有完全捕捉到输入特征和输出特征之间的真实关系,那么它就被说成是欠拟合。在训练数据和任何未见过的测试数据上的性能都较低。
如果一个模型超出了捕捉真实关系的范围,开始捕捉数据噪声中的随机趋势,那么它就被说成是过拟合。在训练数据上的性能可能非常高,但在未见过的测试数据上的性能可能很低。
一个拟合良好的模型将在训练数据和测试数据上表现同样好。
正则化是一种通过迫使模型变得不那么灵活来控制过拟合的技术。例如,在图 5.18中,我模拟了一组带有随机噪声的点(我使用的真实关系是![img/019630_05_F07.png])并使用 8 次多项式回归拟合了两条线(实际上,你很少会选择如此高的阶数作为回归模型;我在这里只是为了夸张这个观点)。一条线完全没有正则化,而另一条线是:
图 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_scale 值 10 对这个数据集进行了预测。所以,这次我们将先验尺度设置为 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.8 进行比较显示,我们的预测中的季节性波动确实已经减弱。年度季节性和周季节性都显示出更少的波动。不过,两个模型之间的不确定性区间大致相同,因为现在数据中的方差现在由 Prophet 模型的噪声项而不是季节性项来处理。
现在,让我们看看组件图:
图 5.20 – 正则化组件图
将此图与图 5.9进行比较,我们可以看到趋势非常相似。我们只限制了季节性,而没有限制趋势。趋势确实有所变化(峰值略高),因为 Prophet 试图通过趋势捕捉一些季节性变化,但形状几乎相同。每周和年度季节性看起来相同,但它们的y轴显示,幅度已经减少到其正则化水平的三分之一到四分之一。这就是季节性正则化的效果:它减少了曲线值的幅度。
为了说明不同季节性先验尺度的效果,让我们比较使用不同先验尺度建模的此数据集的年度和每周季节性曲线。首先,这是年度季节性图:
图 5.21 – 不同先验尺度的年度季节性
这是每周季节性图:
图 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**.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 预测
然而,细心的读者可能会注意到年中和大年底附近有一些向下的峰值。为了辨别这些是什么,我们将查看以下 components 图:
fig2 = model.plot_components(forecast)
plt.show()
在这些命令的输出中,趋势和每周和年度季节性再次包括在内,看起来几乎相同。然而,这里显示了一个新的图:假日,如这里所示(注意,前面的代码生成了一个完整的 components 图;下面的图是那个图像的裁剪):
图 6.2 – Divvy 美国假日组成部分
这显示了 Divvy 与趋势偏离的峰值,每个峰值对应一个假日。除了每年最后一个季度发生的一个假日外,每个假日都显示出使用量的减少。让我们来调查一下。
我们可以使用此命令查看我们模型中包含的假日:
model.train_holiday_names
这将输出一个包含索引和模型中包含的假日名称的 Python 对象:
图 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 – 节假日影响值
现在,我们可以清楚地看到哥伦布日为 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数据框包含两列,ds和holiday,分别表示节假日的日期和名称:
图 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,您将看到四个特定于伊利诺伊州的额外节假日,这些节假日不是官方的美国节假日:林肯诞辰、卡西米尔·普拉斯基日、选举日和林肯诞辰(观察日)。
创建自定义节假日
美国的默认假日包括感恩节和圣诞节,因为它们是官方假日。然而,黑色星期五和圣诞夜可能会产生与预期趋势不同的客流量。因此,我们自然决定将它们包含在我们的预测中。
在这个例子中,我们将以与之前创建伊利诺伊州假日数据框相似的方式创建一个包含默认美国假日的数据框,然后将其添加到其中。要创建自定义假日,你只需创建一个包含两列的数据框:holiday 和 ds。像之前做的那样,它必须包括过去(至少,远至你的训练数据)和未来我们打算预测的假日所有发生情况。
在这个例子中,我们将首先创建一个包含默认美国假日的 holidays 数据框,并使用之前示例中的 year_list:
holidays = make_holidays_df(year_list=year_list,
country='US')
我们将用我们自定义的假日来丰富这个默认假日的列表,因此现在我们将创建两个包含指定列(holiday 和 ds)的数据框,一个用于 黑色星期五,另一个用于 平安夜:
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,就像之前处理伊利诺伊州假日时一样,并继续调用 fit 和 predict:
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 – 带窗口的节日
小贴士
如果你对前面表格中的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 – 芝加哥美食节对乘客量的影响
我们可以看到,活动的第一天比没有活动时预期的客流量少了 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 – 芝加哥风味假日影响
活动的第一天显示客流量减少,接下来的四天客流量增加。现在我们已经了解了您可以将假日添加到预测中的各种方法,让我们再看看一个用于控制假日影响的工具:正则化。
正则化假日
将模型的灵活性约束以帮助它更好地泛化到新数据的过程被称为正则化。第五章,处理季节性,在 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 中最长的条形,我们在本章前面发现其对所有节日的效应最强)将乘客量减少了大约 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 – 强正则化的节假日成分
当进行规范化时,图表看起来与未规范化的假日图表相似,但有一些不同。首先,我们看到尺度变化很大。当规范化时,最强的假日效应是乘客量减少了 11.5%,而未规范化的模型中减少了 65%。要注意的第二件事是假日并没有按相同的比例减少:现在,圣诞节的效果最强,而不是感恩节。这不是错误,只是这么多变量相互作用时规范化的效果。
选择先验尺度的值可能更像是一门艺术而非科学。如果你认为假日的效应比你直觉所暗示的更强或更弱,你可以使用你的领域知识来调整这个值。如果有疑问,进行实验并看看什么效果最好。最严谨的方法是使用网格搜索和交叉验证,这个话题我们将在本书的结尾部分进行介绍。
使用我们之前使用的holidays_prior_scale参数调整所有假日是全球性的;每个假日都是按相同的方式规范化的。为了有更多的控制,Prophet 提供了通过自定义假日接口调整每个个别假日先验尺度的功能。在下一个例子中,我们将看到如何做到这一点。
个人假日规范化
当添加一个新的假日时,我们创建了一个包含两个必需列ds和holiday以及两个可选列lower_window和upper_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 – 带有先验尺度的假日
在我们构建了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 数据并专注于增长。
我们将再次导入pandas、matplotlib和Prophet,但这次,我们还将从 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 预测
记住,Prophet 是一个加性回归模型。因此,趋势是我们预测的最基本构建块。我们通过添加季节性、假日和额外的回归因子来增加其细节和变化。前面图中(穿过每个正弦周期中点的实线)的趋势是去除季节性的 Divvy 图(在这个例子中我们从未添加过假日)。
如你所见,趋势是从2014年到晚2015年的直线段,然后是轻微的弯曲和另一个斜率较浅的直线段,从2016年开始。尽管有这个弯曲,它本质上还是线性的。
现在我们来看下一个增长模式,逻辑增长。要理解这种增长模式,你首先需要了解逻辑函数。
理解逻辑函数
逻辑函数生成一个 S 形曲线;方程具有以下形式:
在这里,是曲线的最大值,
是曲线的逻辑增长率或陡度,而
是曲线中点的x值。
以,
,和
为例,逻辑函数产生了标准逻辑函数
,如下面的图中所示:
图 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 – 伊莎贝拉皇家岛上的狼和驼鹿的种群数量
在 20 世纪 60 年代和 70 年代,不断增长的驼鹿种群提供了食物,这允许狼群数量翻倍。但在 1980 年,人类意外引入了犬细小病毒,这种疾病导致狼群数量崩溃。随着其唯一捕食者的数量下降,驼鹿种群再次增加,但于 1996 年在创纪录的最严重冬季和不可预测的驼鹿蜱虫爆发双重压力下崩溃。
在 20 世纪 90 年代,狼群数量过低,无法进行健康繁殖,导致近亲繁殖水平极高,这抑制了它们的种群数量,直到 1990 年代末一只狼通过穿越来自加拿大的冬季冰层到达岛屿时,种群数量才有所回升。此后,尽管驼鹿数量下降,狼群数量在整个 21 世纪初仍然在增加。所有这些都说明,小型、孤立种群代表一个非常动态的系统,当它们不与自然外部事件隔离时,无法准确预测。
增加逻辑增长
为了在黄石公园合成一个相对现实的狼群种群,让我们假设 1995 年引入了 100 只狼。公园生态学家对该地区进行了调查,并确定这片土地可以支持总共 500 只狼的种群。
在线性增长示例中,我们导入了pandas、matplotlib、Prophet和add_changepoints_to_plot函数,因此为了继续,我们只需要导入numpy和random库来创建我们的数据集。务必设置随机种子,以确保每次运行代码时我们都得到相同的伪随机结果:
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 – 黄石公园模拟的狼群数量
让我们从拟合一个具有线性增长的 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 – 使用线性增长的狼群数量预测
显然,这是不现实的。狼能吃的食物是有限的;在某个点上,食物将不足,狼将开始饿死。现在让我们用逻辑增长来模拟这种情况,看看会发生什么。
使用逻辑增长建模
使用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 – 使用逻辑增长的狼群数量预测
在逻辑增长的情况下,狼群的数量被允许以大致相同的速率增长数年。当它接近饱和点,即自然资源能够支持的最大人口时,增长率会放缓。在此之后,增长率保持平稳,仅略有季节性变化,因为老狼在冬春季节死亡,而春季幼狼出生。
非常数上限
重要的是要注意,上限值不一定需要是恒定的。例如,如果你在预测销售额,你的饱和极限将是市场大小。但这个市场大小可能会随着各种因素导致更多消费者考虑购买你的产品而增长。让我们快速看一下如何建模这个例子。我们假设黄石公园的狼群数量受到公园大小的限制。现在,让我们创建一个假设情况,从 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 – 非恒定上限下的狼群数量预测
上限是针对 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 – 黄石公园模拟的狼群数量下降
在本例的预测中,我们将数据截断到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 – 下降的指数增长下的狼群数量预测
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 – 一个世纪内的模拟狼群人口
经过这么长时间,狼群已经达到饱和点并完全稳定。我们现在将创建我们的训练数据框,但然后只限制我们的数据到范围的最后十年,那里的整体趋势已经很好地饱和:
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 – 模拟的稳定狼群人口
让我们首先使用默认的线性增长来看看可能会出错:
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 – 使用线性增长的稳定狼群人口预测
如您所见,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 – 稳定的狼群人口预测,增长平稳
无论我们预测得多远,趋势都将保持稳定。在这个例子中,Prophet 模型的唯一变化来自年度季节性,因为我们没有添加假日,也没有包括每日或每周的季节性。
这三种增长模式——线性、逻辑和平坦——在行业中是最常用的,几乎可以覆盖大多数分析师将看到的几乎所有预测任务。然而,有时分析师需要自定义增长模式。尽管这不是最简单的任务,但 Prophet 确实具有接受任何你可以数学定义的增长模式的能力。
创建自定义趋势
开源软件的一个关键优势是任何用户都可以下载源代码,并根据他们的使用案例对软件进行自己的修改以更好地适应。尽管几乎所有常见的时间序列都可以用 Prophet(分段线性、分段逻辑和平坦)中实现的三个趋势模式进行适当建模,但可能存在需要不同于提供的趋势模型的情况;由于 Prophet 是开源的,因此相对容易创建你需要的任何内容。但有一个快速警告:这只是在概念上相对容易。从数学上讲,它可能相当复杂,你必须具备扎实的软件工程技能才能成功修改代码。
让我们看看一个可能的例子。考虑一家小型服装零售商,它为每个季节更新其收藏品:
df = pd.read_csv('../data/clothing_retailer.csv')
df['ds'] = pd.to_datetime(df['ds'])
每日销售额高度依赖于当前可用的收藏品的热度,因此趋势大多是平稳的,但每三个月当新收藏品发布时,会看到戏剧性的阶梯式变化:
图 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 – 不合适的线性预测
预测期间的置信区间爆炸,因为模型预计有更多的潜在变化点;它还忽略了数据在前三个月已经平稳的事实,预测了一个荒谬的持续曲棍球棒增长速率。一个平稳增长模型会更好,但在 Prophet 中实现时,它无法处理变化点。
这里的过程变得复杂,并且非常依赖软件工程技能。我们需要创建一个自定义趋势模型。简而言之,必要的代码需要从 Prophet 的源代码中复制 Prophet 类,该类位于forecaster.py文件中,在github.com/facebook/prophet/blob/main/python/prophet/forecaster.py。然后进行一些修改。特别是,这个新类(在我们 GitHub 仓库的示例中,我们称之为ProphetStepWise)继承了基类Prophet的所有方法和属性,并在几个方面进行了修改:
-
它修改了原始
Prophet类的fit函数,以初始化新的步进增长模式。 -
它创建了一个新的函数,
stepwise_growth_init,类似于当前的flat_growth_init函数,它使用平稳增长初始化趋势。当前的flat_growth_init函数将偏移参数设置为历史值的平均值,但这个新的stepwise_growth_init函数考虑变化点的位置,并在每个变化点之间应用不同的偏移参数。 -
它创建了一个新的函数,
stepwise_trend,类似于现有的flat_trend函数,它评估新的步进趋势。 -
它修改了现有的
sample_predictive_trend函数,将'flat'增长模式重新定义为使用新的stepwise_trend函数。 -
最后,它修改了现有的
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 – 我们的新步进趋势
然而,你会注意到,尽管对整个季节的预测相当准确,但置信区间却非常宽泛。为了解决这个问题,你还需要修改prophet.stan文件。由于本书只使用 Python 代码,因此 Stan 模型中的这些更改不在此讨论范围之内。然而,对于那些感兴趣的人来说,官方 Prophet GitHub 仓库中有一个很好的逐步趋势模型示例,其中包含了正确实现的 Stan 更改,你可以通过以下链接查看:github.com/facebook/prophet/pull/1466/files。实际上,本节中的大部分代码都来自那个示例。
摘要
在本章中,你了解到本书前几章构建的模型都具备线性增长的特点。你学习了逻辑函数是如何被开发出来以模拟人口增长的,然后学习了如何在 Prophet 中通过模拟 1995 年狼群重新引入黄石公园后的增长来实施这一功能。
在 Prophet 中,逻辑增长可以模拟为增加到饱和极限,称为上限,或者减少到饱和极限,称为下限。最后,你学习了如何模拟平坦(或无增长)趋势,其中趋势在整个数据期间固定为一个值,但季节性仍然允许变化。在本章中,你使用了add_changepoints_to_plot函数来在你的预测图上叠加趋势线。
选择正确的发展模式很重要,尤其是在进行未来预测时。在本章中,我们查看了一些例子,其中错误的发展模式很好地拟合了实际数据,但未来的预测却变得非常不切实际。最后,我们看到了创建自定义趋势模型的例子。这个过程是本书中介绍的最先进的技术,但也是最强大的,因为它展示了如何利用 Prophet 的开源代码来完全定制该包以满足你的特定需求。在下一章中,你将了解所有关于变化点的内容以及如何使用它们来获得对趋势线更多的控制。