时间序列分析实践指南(全)
原文:zh.annas-archive.org/md5/b4cad8d79afe247e9b17f26ff108af6b
译者:飞龙
协议:CC BY-NC-SA 4.0
序言
天气、股市和心跳。它们都构成时间序列。如果您对多样化数据和预测未来感兴趣,那么您就对时间序列分析感兴趣。
欢迎阅读实用时间序列分析!如果您拿起了本书,您可能已经注意到时间序列数据随处可见。随着大数据生态系统的扩展,时间序列数据变得日益普遍和重要。无论好坏,传感器和跟踪机制无处不在,因此提供了前所未有的大量高质量时间序列数据。时间序列之所以独具魅力,是因为它们可以解答因果关系、趋势以及未来结果可能性的问题。本书将介绍最常用于时间序列的主要技术,以解决这些问题。
时间序列数据涵盖了广泛的学科和用例。它可以是从客户购买历史到纳米电子系统的电导测量,再到人类语言的数字记录。本书始终讨论的一点是,时间序列分析适用于一组惊人多样的数据。任何具有有序轴的数据都可以用时间序列方法进行分析,即使该有序轴本身并非时间。传统的时间序列数据,如股票数据和天气模式,可以用时间序列方法分析,但异类数据集如葡萄酒的光谱图,其中的“时间”轴实际上是一个频率轴,同样可以用时间序列方法分析。时间序列随处可见。
适合读者
本书的预期读者有两类。第一类也是更大的一类读者是数据科学家,可能很少处理过时间序列数据。这个人可以是行业老手或初级分析师。经验丰富的数据分析师可以快速浏览每章节的介绍性概念区域,但仍然会从本书关于时间序列数据工作的最佳实践以及陷阱的讨论中受益。新手数据分析师可能考虑全书逐章阅读,尽管我尽量使每个主题尽可能自包含。
第二类读者是在组织中负责分析工作的人员,该组织拥有大量内部数据收集。如果您属于这类人群,您仍然需要一些技术背景,但不必在职业生活中编写代码。对于这样的读者,本书有助于指出机构可以利用时间序列分析的机会,即使目前在内部并未实践。本书将指导您使用现有数据资源解决新类型的问题和分析。
预期背景
就编码而言,您应该对 R 和 Python 有一些了解,特别是某些基础包(在 Python 中:NumPy、Pandas 和scikit-learn;在 R 中:data.table)。即使没有所有背景知识,代码示例也应该是可读的,但在这种情况下,您可能需要稍作了解这些包。对于 R 的 data.table 来说,这很可能是最常见的情况,它是一个鲜为人知但性能非常出色的数据框架包,具有出色的时间功能。
在所有情况下,我都提供了相关包的简要概述、一些示例代码以及代码的描述。我还指向读者更全面的最常用包的概述。
关于统计学和机器学习,您应该对以下概念有一些了解:
初步统计
关于方差、相关性和概率分布的思想
机器学习
聚类和决策树
神经网络
它们是什么以及它们是如何训练的
对于这些情况,我在文本中提供了这些概念的简要概述,但对于未接触过的人士,在继续某些章节之前应深入阅读这些内容。对于大多数主题,我提供了推荐的免费在线资源链接,用于简要介绍给定主题或技术的基础知识。
为什么我写这本书
我写这本书有三个原因。
首先,时间序列是数据分析的重要方面,但它并未包含在标准数据科学工具包中。这是不幸的,因为时间序列数据越来越普遍,并且因为它能回答横截面数据无法回答的问题。不了解基本的时间序列分析的分析师未充分利用他们的数据。我希望这本书能填补一个现有且重要的空白。
其次,当我开始写这本书时,我没有找到一个集中讨论现代数据科学视角下时间序列分析最重要方面的中心化概述。传统时间序列分析有许多优秀的资源,尤其是统计时间序列分析的经典教材。还有许多关于传统统计方法以及关于机器学习或神经网络方法应用于时间序列的优秀博客文章。然而,我无法找到一个单一的集中资源来概述所有这些主题并将它们彼此联系起来。这本书的目标是提供这样的资源:一个涵盖时间序列数据和建模完整流程的广泛、现代和实用的时间序列分析概述。再次,我希望这本书能填补一个现有且重要的空白。
第三,时间序列是一个涉及奇怪数据问题的有趣主题。从时间序列的角度来看,与数据泄漏、前瞻性和因果关系相关的问题尤其有趣,许多技术都是针对某种时间轴上有序数据而设计的。广泛调查这些主题并找到一种分类它们的方式是撰写本书的另一个动机。
浏览本书
本书大致组织如下:
历史
第一章 展示了时间序列预测的历史,从古希腊到现代。这使我们的学习能够理解我们在本书中研究的传统而又丰富的学科背景。
关于数据的一切
第 2、3、4 和 5 章解决了获取、清理、模拟和存储时间序列数据相关的问题。这些章节关注的是在实际执行时间序列分析之前需要考虑的一切。这些主题在现有资源中很少被讨论,但在大多数数据流程中非常重要。数据识别和清理代表了大多数时间序列分析师所做工作的一大部分。
模型,模型,模型
第 6、7、8、9 和 10 章涵盖了多种可用于时间序列分析的建模技术。我们首先讨论了两章关于统计方法的内容,涵盖了诸如 ARIMA 和贝叶斯状态空间模型等标准统计模型。然后,我们将更近期开发的方法,如机器学习和神经网络,应用到时间序列数据中,突显了在使用决策树等非本质上时间感知的模型拟合时间序列数据时所面临的数据处理和数据布局方面的挑战。
模型后考虑事项
第十一章和 12 章分别介绍精度指标和性能考虑,为您在进行首次时间序列建模后应考虑的事项提供一些指导。
实际应用案例
第 13、14 和 15 章分别提供了来自医疗保健、金融和政府数据的案例研究。
对最近发生的事情的评论
第十六章和第十七章简要介绍了最近的时间序列发展和未来的预测。第十六章是各种自动化时间序列软件包的概述,一些是开源的,并作为学术努力开发的,一些来自大型科技公司。这些工具正在积极开发,努力通过自动化流程提高时间序列的预测规模。第十七章讨论了时间序列分析未来的一些预测,随着大数据生态系统的增长以及我们对大数据如何帮助时间序列分析的认识的深入,时间序列分析的未来将会如何。
一般来说,我建议在尝试使用代码之前通读一章。每章通常会引入一些新概念,在转到键盘之前给予这些概念一些关注可能会有所帮助。此外,在大多数情况下,执行某些模型的代码相对简单,因此概念理解将是你获得的主要技能,对重要软件包的 API 的了解将是一个次要的好处,如果你注意这些概念,后者将更容易掌握。
此外,这本书的写作方式最好从头到尾阅读(后面的章节会涉及书中早期介绍的概念),但是,我故意尽可能使章节自给自足,以便更有经验的读者可以方便地跳跃阅读。
在线资源
O'Reilly 为这本书托管了一个[GitHub 存储库](https://oreil.ly/time-series-repo)。存储库包括本书中讨论的大部分代码,可以在其中运行数据集,该存储库还提供了数据集。在某些情况下,变量名称、格式等可能与书中产生的不完全相同,但应该能够轻松链接(例如,在某些情况下,由于格式限制,书中的变量名称被缩短)。
如果你喜欢视频演示,我有两个在线教程,涵盖了本书部分内容,并着重于 Python。如果你想用视频演示来补充这本书,请考虑以下资源:
本书中使用的约定
这本书采用以下排版约定:
斜体
表示新术语、URL、电子邮件地址、文件名和文件扩展名。
等宽
用于程序列表,以及段落内引用程序元素,例如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
等宽粗体
显示用户应该按原样键入的命令或其他文本。
等宽斜体
显示应由用户提供值或由上下文确定值的文本。
提示
这个元素表示提示或建议。
注意
这个元素表示一般性说明。
警告
这个元素表示警告或注意事项。
使用代码示例
补充材料(代码示例、练习等)可在https://github.com/PracticalTimeSeriesAnalysis/BookRepo下载。
本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您复制了代码的大部分内容,否则无需征得我们的许可。例如,编写一个使用本书多个代码片段的程序不需要许可。售卖或分发包含 O’Reilly 书籍示例的 CD-ROM 需要许可。引用本书并引用示例代码回答问题不需要许可。将本书大量示例代码整合到您产品的文档中需要许可。
我们欣赏但不要求归属。归属通常包括标题、作者、出版商和 ISBN。例如:“Practical Time Series Analysis by Aileen Nielsen(O’Reilly)。版权所有 2020 Aileen Nielsen,978-1-492-04165-8。”
如果您认为您使用的代码示例超出了合理使用范围或上述许可,请随时联系我们,邮箱为permissions@oreilly.com。
O’Reilly 在线学习
注意
近 40 年来,O’Reilly Media为企业提供技术和商业培训、知识和见解,帮助它们成功。
我们独特的专家和创新者网络通过书籍、文章、会议和我们的在线学习平台分享他们的知识和专业知识。O’Reilly 的在线学习平台为您提供按需访问的实时培训课程,深度学习路径,交互式编码环境以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频内容。有关更多信息,请访问http://oreilly.com。
如何联系我们
请将有关本书的评论和问题发送给出版商:
我们为本书建立了一个网页,列出勘误、示例和任何额外信息。您可以在https://oreil.ly/practical-time-series-analysis访问此页面。
发邮件至bookquestions@oreilly.com对本书进行评论或提出技术问题。
欲了解更多关于我们书籍、课程、会议和新闻的信息,请访问我们的网站http://www.oreilly.com。
在 Facebook 上找到我们:http://facebook.com/oreilly
在 Twitter 上关注我们:http://twitter.com/oreillymedia
在 YouTube 上观看我们:http://www.youtube.com/oreillymedia
致谢
感谢本书的两位技术审阅者,Rob Hyndman 教授和 David Stoffer 教授。他们慷慨地花时间审阅本书,并提供了大量有益的反馈和新的想法,使本书比没有他们的参与要好得多。特别感谢 Rob 指出原始草稿中未能突出显示备选方法和许多有趣的时间序列数据来源的遗漏机会。特别感谢 David 对分析过度自动化方法的怀疑,以及在我对某些工具过于乐观时的提醒。感谢你们的所有帮助、知识和深刻的时间序列分析视角。
我感激 O’Reilly 的编辑 Jeff Bleiel,在过去一年中,他对这些页数的审阅和支持非常鼓舞人心、有帮助。我也要感谢我的制作编辑 Katie Tozer,在整理这本书和指导我解决制作中的技术问题的过程中,她非常耐心。感谢 Rachel Monaghan 进行仔细和优秀的拷贝编辑。感谢 Rebecca Demarest,在本书中创建了许多插图,并帮助我整理了许多混乱的图形。还要感谢 Jonathan Hassell,他接手了这个项目,并成功地说服 O’Reilly 出版了它。最后,感谢 Nicole Tache 几年前首次与我联系,与 O’Reilly 合作。感谢 O’Reilly 的每一位同事,感谢你们让我承担这个项目,并在整个过程中给予我的支持。
我感激许多读者在写作过程中提供的反馈或校对帮助,包括 Wenfei Tong、Richard Krajunus、Zach Bogart、Gabe Fernando、Laura Kennedy、Steven Finkelstein、Liana Hitts 和 Jason Greenberg。他们的意见非常有帮助。感谢你们的阅读和反馈。
我要感谢我的母亲(也是我的榜样),Elizabeth,她一生都给予我爱、支持和纪律。我要感谢我的父亲 John 和阿姨 Claire,多年来他们对我的教育给予了无数的爱和支持。最重要的是,我要感谢我的丈夫 Ivan 和我的儿子 Edmund Hillary,在我写作期间对本书的支持和理解,原谅了我在远离家庭时花费的时间。
本书中的任何错误或问题都是我个人的责任。欢迎您通过aileen.a.nielsen@gmail.com提供反馈。
第一章:时间序列:概述和简史
由于通过物联网、医疗数字化和智能城市的兴起而大量产生这类数据,时间序列数据及其分析变得越来越重要。在未来几年,我们可以预期时间序列数据的数量、质量和重要性将迅速增长。
随着持续监控和数据收集变得更加普遍,对具有统计和机器学习技术的时间序列分析的需求将增加。事实上,最有前途的新模型结合了这两种方法。因此,我们将详细讨论每一种方法。我们将研究和使用广泛的时间序列技术,这些技术有助于分析和预测人类行为、科学现象和私营部门的数据,因为所有这些领域都提供丰富的时间序列数据。
让我们从一个定义开始。时间序列分析是从按时间顺序排列的点中提取有意义的总结和统计信息的努力。这样做是为了诊断过去的行为以及预测未来的行为。在本书中,我们将使用各种方法,从一百年前的统计模型到新开发的神经网络架构。
技术的发展从未在真空中或出于纯理论兴趣而发展。时间序列分析的创新源于新的数据收集、记录和可视化方式。接下来我们简要讨论时间序列分析在各种应用中的出现。
多样化应用中的时间序列历史
时间序列分析常常涉及到因果关系的问题:过去是如何影响未来的?有时,这些问题(及其答案)严格地被视为各自学科的一部分,而不是时间序列分析的一般学科的一部分。因此,各种学科为时间序列数据集提供了新的思考方式。
在本节中,我们将调查一些历史上的时间序列数据和分析示例,涉及以下学科:
正如我们将看到的,这些学科中的发展速度及每个领域中的贡献与当时可用的时间序列数据的性质密切相关。
医学作为时间序列问题
医学是一个数据驱动的领域,几个世纪以来为人类知识贡献了有趣的时间序列分析。现在,让我们研究医学中几个时间序列数据源的例子,以及它们如何随着时间的推移而出现。
尽管预后在医学实践中是一个重要部分,但医学在思考未来预测的数学方面起步相对缓慢。这有多种原因。统计学和概率思维是近期的现象,这些学科在医学发展的许多世纪里并不可用。此外,大多数医生在没有便捷的专业交流和病人或人群健康的正式记录基础设施的情况下独立实践。因此,即使早期的医生接受了统计思维的训练,他们也可能没有合理的数据来得出结论。
这并不是要批评早期的医生,而是解释为什么一个早期时间序列创新在人口健康领域来自于一位卖帽子的商人而不是一位医生,这其实是合理的思考:在早期世纪,城市帽子销售商可能比医生更擅长记录和发现趋势。
创新者是约翰·格朗特,他是 17 世纪伦敦的一名帽商。格朗特进行了一项对伦敦教区自 1500 年代初以来保存的死亡记录的研究。在此过程中,他创立了人口统计学学科。1662 年,他出版了《自然与政治观察……根据死亡账单制作》(参见图 1-1)。
![]()
图 1-1. 约翰·格朗特的寿命表是将时间序列思维应用于医学问题的最早成果之一。来源:维基百科。
在这本书中,格朗特首次提出了寿命表,你可能知道它们为精算表。这些表格显示了一个特定年龄的人在下一个生日之前死亡的概率。作为第一个已知制定和出版寿命表的人,格朗特也是第一个记录人类健康统计学的人。他的寿命表看起来类似于表 1-1,取自一些莱斯大学统计学课程笔记。
表 1-1. 约翰·格朗特寿命表样本
| 年龄 |
该年龄段内死亡比例 |
开始该年龄段时生存比例 |
| 0–6 |
0.36 |
1.0 |
| 7–16 |
0.24 |
0.64 |
| 17–26 |
0.15 |
0.40 |
| 27–36 |
0.09 |
0.25 |
不幸的是,格朗特对人类生存进行数学思考的方式并没有被广泛接受。一个更加互联和数据驱动的世界开始形成——包括国家、认证、专业社团、科学期刊,以及更晚的政府强制健康记录——但医学继续关注生理而非统计学。
这有其可以理解的原因。首先,在少数研究对象中研究解剖学和生理学已经几个世纪为医学带来了主要进展,大多数人(甚至科学家)坚持尽可能长时间使用已知的方法。在生理学如此成功的情况下,没有必要进一步探索其他领域。其次,医师没有足够的报告基础设施来归纳和分享信息,使统计方法优于临床观察。
与统计学和数据分析的其他分支相比,时间序列分析进入主流医学的步伐更为缓慢,可能是因为时间序列分析对记录系统的要求更高。记录必须随时间链接在一起,并最好定期收集。因此,作为流行病学实践的时间序列分析直到最近才逐渐出现,一旦政府和科学基础设施足够,可以确保良好且持久的时间记录。
同样地,使用时间序列分析进行个性化医疗仍然是一个年轻而具有挑战性的领域,因为创建一致的数据集可能会非常困难。即使是基于小型案例研究的研究,也很难维持与个体群体的联系和参与,而且非常困难且昂贵。当这些研究进行了长时间后,它们往往会成为其领域的经典作品,尽管在资金和管理方面存在挑战,但其数据能够解决重要问题。¹
医疗仪器
对于单个患者的时间序列分析比群体级健康研究有着更早且更成功的历史。时间序列分析进入医学领域始于 1901 年发明的第一台实用心电图仪(ECG),通过记录通过心脏的电信号来诊断心脏疾病(见图 1-2)。另一种时间序列仪器脑电图(EEG),1924 年引入医学,非侵入性地测量大脑的电脉冲,为医务人员在医学诊断中应用时间序列分析创造了更多机会(见图 1-3)。
这两种时间序列仪器都是第二次工业革命中重新利用的思想和技术来增强医学的一部分。
![]()
![]()
图 1-3. 1924 年的第一份人类 EEG 记录。来源:维基百科。
ECG 和 EEG 时间序列分类工具仍然是非常实用的研究领域,比如估计突发心脏危机或癫痫的风险。这些测量数据是丰富的数据源,但这些数据的一个“问题”是,它通常只适用于特定疾病的患者。这些机器不会生成能更广泛告知人类健康和行为的长期时间序列,因为它们的测量很少在疾病出现之前或在患者身上应用很长时间。
幸运的是,从数据分析的角度来看,我们正在摆脱 ECG 等医疗时间序列数据占主导地位的时代。随着可穿戴传感器和“智能”电子医疗设备的出现,许多健康的人类自动或几乎不需要人工干预地进行常规测量,导致对患病和健康人群的长期数据持续收集。这与上个世纪几乎只在患病人群中进行、访问权限非常有限的医疗时间序列数据形成鲜明对比。
正如最近的新闻报道所显示的,从巨大的社交媒体公司到金融机构再到零售巨头,各种非传统的参与者正在进入医疗领域。他们很可能都计划利用大数据集来简化医疗保健。医疗领域不仅有新的参与者,还有新的技术。个性化的基因驱动医学意味着时间序列数据正在越来越多地被测量和重视。由于不断增长的现代医疗数据集,无论是医疗保健还是时间序列分析都有望在未来几年内发展,特别是在响应医疗部门利润丰厚的数据集方面。希望时间序列能够使每个人受益。
天气预测
出于显而易见的原因,天气预测长期以来一直是许多人关注的焦点。古希腊哲学家亚里士多德深入研究了天气,他的著作《气象学》探讨了天气的原因和顺序,这些观点一直占主导地位直到文艺复兴时期。那时,科学家们开始借助新发明的仪器,如晴雨表,收集与天气相关的数据,以测量大气状态。他们使用这些仪器每天甚至每小时记录时间序列。这些记录保存在各种地方,包括私人日记和地方城镇的日志。几个世纪以来,这仍然是西方文明跟踪天气的唯一方式。
在 19 世纪 50 年代,罗伯特·菲茨罗伊被任命为英国新政府部门的负责人,记录和发布针对水手的与天气有关的数据。[³] 菲茨罗伊创造了术语天气预报。当时,他因预测质量而受到批评,但现在他被认为在他用来发展这些预报的科学方面领先于他的时代。他建立了在报纸上印刷天气预报的习惯;它们是《伦敦时报》上首次印刷的预报。菲茨罗伊现在被誉为“预报之父”。
在 19 世纪末,即使在许多大气测量技术已经投入使用数百年后,电报也允许从许多不同地点快速编制时间序列中的大气条件。这种实践在 19 世纪 70 年代成为世界许多地区的标准,并导致了基于其他地理位置发生的事情预测本地天气的第一个有意义的数据集的创建。
到了 20 世纪初,通过这些编制的数据集积极追求用计算方法预测天气的想法。早期计算天气的努力需要大量的工作,但效果不佳。虽然物理学家和化学家对相关自然法则有很好的证明,但同时应用所有自然法则的数量太多。由此产生的方程系统如此复杂,以至于第一次有人尝试进行计算时,这是一个显著的科学突破。
几十年的研究随后进行,以简化物理方程的方式增加准确性和计算效率。这些行业内的诀窍甚至传承到当前的天气预测模型中,这些模型运行在已知物理原理和经过验证的启发式方法的混合之上。
如今,许多政府从世界各地数百甚至数千个气象站进行高度精细的气象测量,这些预测建立在具有精确气象站位置和设备信息的数据基础上。这些努力的根源可以追溯到 19 世纪 70 年代的协调数据集,甚至更早的文艺复兴时期的地方天气日记记录实践。
不幸的是,天气预报是越来越多地涉及到时间序列预测领域的科学攻击的一个例子。不仅全球温度的时间序列辩论被政治化,甚至像预测飓风路径这样的更普通的时间序列预测任务也被如此。
预测经济增长
市场中生产和效率的指标长期以来一直提供有趣的时间序列分析数据。最有趣和紧迫的是基于过去预测未来经济状态的问题。这样的预测不仅仅有助于赚钱,还有助于促进繁荣和避免社会灾难。让我们讨论一下经济预测历史上的一些重要发展。
经济预测源于 19 世纪末和 20 世纪初美国和欧洲的周期性银行危机引发的焦虑。那时,企业家和研究人员都从经济可以类比为一个周期性系统的想法中汲取灵感,就像天气被认为会表现出来一样。他们认为,通过正确的测量,可以做出预测并避免崩溃。
早期经济预测的语言甚至反映了天气预报的语言。这种类比并非无意义。在 20 世纪初,经济和天气预测确实相似:两者都相当糟糕。但经济学家的愿景创造了一个至少可以期待进展的环境,因此形成了多种公共和私人机构来跟踪经济数据。早期经济预测努力导致了经济指标的创建以及这些指标的表格化和公开历史记录,这些至今仍在使用。我们甚至会在本书中使用其中一些。
当今,美国和大多数其他国家都有成千上万的政府研究人员和记录员,他们的工作是尽可能准确地记录数据,并使其对公众可用(参见图 1-4)。这一做法被证明对经济增长和避免经济灾难以及痛苦的经济周期有着无价的贡献。此外,企业受益于数据丰富的环境,因为这些公共数据集允许交通运输提供者、制造商、小企业主甚至农民预测未来市场情况。所有这些都源于试图识别被认为是周期性银行失败原因的“商业周期”,这是经济学中时间序列分析的早期形式。
![]()
图 1-4. 美国联邦政府资助许多政府机构和相关非营利组织,记录重要统计数据并制定经济指标。来源:美国经济研究局。
政府收集的许多经济数据,特别是最具新闻价值的数据,往往是人口整体经济福祉的代理。其中一个例子是申请失业救济的人数。例如,政府对国内生产总值的估计以及在特定年份收到的总税收的报告。
由于对经济预测的渴望,政府已经成为数据的典藏家以及税收的征收者。这些数据的收集使得现代经济学、现代金融业以及数据科学普遍蓬勃发展。由于时间序列分析从经济问题中发展而来,我们现在比过去的任何政府都能够更安全地避免许多银行和金融危机。此外,已经有数百本时间序列教科书以经济学教科书的形式写成,专门用于理解这些金融指标的节奏。
交易市场
让我们回到历史的一面。随着政府在数据收集方面的成功,私人组织开始效仿政府的记录方式。随着时间的推移,商品和股票交易所变得越来越技术化。金融年鉴也变得流行起来。这种现象既因市场参与者变得更加复杂,也因新兴技术使得更大程度的自动化和对价格的新思考方式成为可能。
所有这些详细的记录使得通过数学而不是直觉从市场赚钱的追求成为可能,这种方式完全由统计数据(以及最近的机器学习)驱动。早期的先驱们手工进行这种数学工作,而当前的“量化交易员”则通过非常复杂和专有的时间序列分析方法来完成。
机械交易或通过算法进行时间序列预测的先驱之一是理查德·丹尼斯。丹尼斯是一个自成一家的百万富翁,以教导他们一些关于如何何时交易的选择性规则而闻名,从而将普通人(称为海龟)转变为明星交易员。这些规则是在 20 世纪 70 年代和 80 年代开发的,与 20 世纪 80 年代的“人工智能”思想相呼应,其中启发式规则仍然在如何构建能在现实世界中工作的智能机器的范式中占主导地位。
自那时以来,许多“机械”交易者已经采纳了这些规则,结果在竞争激烈的自动化市场中变得不那么赚钱了。机械交易者的数量和财富继续增长,他们不断寻找下一个最好的东西,因为竞争非常激烈。
天文学
天文学在很大程度上依赖于随时间绘制物体、轨迹和测量数据。因此,天文学家是时间序列的大师,既用于校准仪器,也用于研究感兴趣的对象。举个例子,关于时间序列数据的悠久历史,可以考虑到太阳黑子时间序列早在公元前 800 年就已记录在古代中国,使得太阳黑子数据收集成为有史以来记录最完善的自然现象之一。
过去一个世纪最激动人心的一些天文学发现与时间序列分析有关。发现变星(可用于推断星系距离)和观察超新星等瞬变事件(这增强了我们对宇宙如何随时间变化的理解)都是基于光的波长和强度的时间序列数据的实时监测的结果。时间序列对于我们可以了解和测量宇宙的内容产生了根本性影响。
顺便提一下,这种对天文图像的监测甚至让天文学家们能够在事件发生时捕捉到它们(或者说在我们能够观察到它们时,这可能需要数百万年)。(链接)
在过去几十年中,随着形式上带有时间戳的数据的大量涌现,天文学中的时间序列数据获得了爆炸性的增长,各种新型望远镜收集各种天体数据。一些天文学家甚至称之为时间序列的“数据洪流”。
时间序列分析腾飞
乔治·博克斯是一位开创性的统计学家,他帮助开发了一种流行的时间序列模型,是一位伟大的实用主义者。他曾经说过,“所有模型都是错误的,但有些是有用的。”
Box 提出了这个观点,以回应一种普遍的态度,即适当的时间序列建模是找到最适合数据的模型的问题。正如他解释的那样,任何模型能够描述真实世界的想法是非常不太可能的。Box 在 1978 年做出了这一声明,这在时间序列分析这样一个重要领域的历史上似乎有些晚,但事实上,这一正式学科非常年轻。
例如,使乔治·博克斯闻名的成就之一,即 Box-Jenkins 方法——被认为是时间序列分析的基本贡献——直到 1970 年才首次出现。⁴ 有趣的是,这种方法最初并非出现在学术期刊上,而是在统计学教科书《时间序列分析:预测与控制》(Wiley)中。顺便说一句,这本教材仍然很受欢迎,目前已经是第五版。
最初的 Box-Jenkins 模型是应用于从燃气炉排放的二氧化碳水平数据集。虽然燃气炉本身并不有趣,但用于展示该方法的 300 点数据集确实显得有些过时。当然,20 世纪 70 年代已经有更大的数据集可用,但请记住,那时处理这些数据集是非常困难的。那个时代还没有像 R、Python 甚至 C++这样的便利工具。研究人员有充分的理由专注于小数据集和最小化计算资源的方法。
随着计算机的发展,时间序列分析和预测也在不断进步,更大的数据集和更便捷的编码工具为更多实验和回答更有趣的问题铺平了道路。Rob Hyndman 教授的预测竞赛历史提供了恰当的例子,展示了时间序列预测竞赛如何以与计算机发展并行的速度发展。
教授 Hyndman 认为,“时间序列预测精度的最早非平凡研究”始于 1969 年在诺丁汉大学的博士论文,就在 Box-Jenkins 方法发表的一年之前。那次最初的尝试很快被有组织的时间序列预测竞赛所跟随,最早的竞赛于 20 世纪 70 年代初期举行,涵盖了约 100 个数据集。⁵ 这并不算差,但如果必要的话,完全可以手工完成。
到了 20 世纪 70 年代末,研究人员已经组织了一场拥有约 1,000 个数据集的竞赛,规模显著扩大。顺便说一句,这个时代也标志着第一款商业微处理器的出现、软盘的发展、苹果早期个人电脑和计算机语言 Pascal 的发展。可以肯定这些创新中的一些是有帮助的。到 20 世纪 90 年代末,一次时间序列预测竞赛包括了 3,000 个数据集。尽管这些数据集的收集和整理是相当庞大的,无疑反映了大量工作和创意,但与现在可用的数据量相比显得微不足道。时间序列数据无处不在,很快一切都将成为时间序列。
这种数据集规模和质量的迅速增长源于过去几十年来计算机技术的巨大进步。硬件工程师成功延续了摩尔定律的趋势——预测计算能力的指数增长——在这段时间内。随着硬件变得更小、更强大和更高效,显然更容易大量生产,价格也更加可承受——从带有传感器的微型便携式计算机到支撑现代数据密集型互联网的大型数据中心。最近,可穿戴设备、机器学习技术和 GPU 已经彻底改变了可供研究的数据的数量和质量。⁶
随着计算能力的增强,时间序列无疑会受益匪浅,因为时间序列数据的许多方面在计算上是具有挑战性的。随着计算和数据资源的增加,时间序列分析可以预期将继续快速发展。
统计时间序列分析的起源
统计学是一门非常年轻的科学。统计、数据分析和时间序列的进展始终强烈依赖于数据何时、何地、以何种方式可用及其数量。时间序列分析作为一门学科的出现,不仅与概率论的发展密切相关,同样也与稳定的国家形成有关,这些国家首次实现了记录的真实和有趣目标。我们之前已经在各种学科中覆盖了这一点。现在我们将思考时间序列本身作为一门学科。
时间序列分析作为一门学科的开始之一是将自回归模型应用于实际数据。这直到 20 世纪 20 年代才发生。剑桥大学的实验物理学家转变为统计讲师的乌德尼·尤尔,将自回归模型应用于太阳黑子数据,提供了一种新的思考数据的方式,与旨在拟合振荡频率的方法形成对比。尤尔指出,自回归模型并不假设周期性模型的开始:
当期谱分析应用于尊重任何物理现象的数据时,期望能够引出一个或多个真实的周期性时,通常,我觉得,有一种倾向,从最初的假设开始,即周期性或周期性仅被这种更或少的随机叠加波动(这些波动以任何方式都不会打扰基础周期功能或功能的稳定进程…似乎没有理由假设它是先验上最有可能的假设。
尤尔的思想是他自己的,但很可能某些历史影响促使他注意到传统模型预设了自己的结果。作为一位曾在德国(量子力学理论蓬勃发展的中心)工作过的前实验物理学家,尤尔肯定意识到最近强调了量子力学的概率性质的发展。他也会认识到,将思维狭窄到预设太多的模型中会有多大危险,正如量子力学被发现前的古典物理学家们所做的那样。
随着世界变得更有秩序、更可记录和更可预测,特别是在第二次世界大战后,早期实际时间序列分析中出现了业务部门提出的问题。以业务为导向的时间序列问题在其起源上重要,但不过度理论化。这些问题包括预测需求、估计未来原材料价格和对制造成本进行对冲。在这些工业应用案例中,技术被采用是因为它们有效,被拒绝是因为它们无效。工业工作者有时能够访问比当时学者更大的数据集(现在仍是如此)。这意味着有时候实际上是实用但在理论上尚未深入探索的技术在广泛使用之前进入了使用。
机器学习时间序列分析的起源
早期的时间序列分析中的机器学习可以追溯到几十年前。一篇常被引用的 1969 年的论文,“预测的组合”,分析了将预测组合而非选择“最佳预测”作为提高预测性能的方法。这个想法起初让传统统计学家感到憎恶,但集成方法已经成为许多预测问题中的黄金标准。集成方法拒绝了相对于所有可能模型来说完美甚至显著优越的预测模型的概念。
较近期,时间序列分析和机器学习的实际应用最早出现在 1980 年代,涵盖了各种各样的场景:
-
计算机安全专家提出异常检测作为识别黑客/入侵的方法。
-
动态时间规整,作为“测量”时间序列相似性的主要方法之一,因为计算能力终于允许对不同音频记录之间的“距离”进行相对较快的计算而进入使用。
-
递归神经网络的发明显示其在从损坏数据中提取模式方面的实用性。
时间序列分析和预测还未到达它们的黄金时期,到目前为止,时间序列分析仍然由传统统计方法和更简单的机器学习技术主导,例如树的集成和线性拟合。我们仍在等待一个能够预测未来的重大突破。
更多资源
¹ 例如,包括英国医生研究和护士健康研究。
² 参见,例如,Darrell Etherington,《亚马逊、摩根大通和伯克希尔哈撒韦将建立自己的医疗保健公司》,TechCrunch,2018 年 1 月 30 日,https://perma.cc/S789-EQGW;Christina Farr,《Facebook 派遣医生秘密任务,请求医院分享患者数据》,CNBC,2018 年 4 月 5 日,https://perma.cc/65GF-M2SJ。
³ 同一位罗伯特·菲茨罗伊曾是 HMS Beagle号的船长,正是在这次环球航行中带领查尔斯·达尔文探索世界。这次航行为达尔文提供了进化论自然选择理论的证据。
⁴ Box-Jenkins 方法已成为选择 ARMA 或 ARIMA 模型最佳参数的经典技术,用于对时间序列进行建模。更多内容请参见第六章。
⁵ 即不同领域的 100 个独立数据集,时间序列的长度各异。
⁶ 鉴于人类随身携带的各种小工具以及在购物、工作电脑门户登录、浏览互联网、检查健康指标、打电话或使用 GPS 导航时产生的时间戳,我们可以安全地说,一个普通美国人一生中可能每年产生数千个时间序列数据点。
第二章:找到和整理时间序列数据。
在本章中,我们讨论了在预处理时间序列数据时可能出现的问题。其中一些问题对经验丰富的数据分析师来说可能很熟悉,但时间戳带来了特定的困难。与任何数据分析任务一样,清理和正确处理数据通常是时间戳流水线中最重要的步骤。花哨的技术不能修复混乱的数据。
大多数数据分析师需要找到、对齐、清理和平滑他们自己的数据,无论是为了学习时间序列分析还是为了在其组织中做有意义的工作。在准备数据时,你需要完成各种任务,从合并不同列到重新采样不规则或缺失的数据,再到将时间序列与不同时间轴对齐。本章将帮助你走向一个有趣且适当准备的时间序列数据集。
我们讨论了以下对于查找和清理时间序列数据有用的技能:
阅读完本章后,你将掌握识别和准备有趣的时间序列数据源进行下游分析所需的技能。
如何找到时间序列数据。
如果你对如何找到时间序列数据和如何清理它感兴趣,那么在这一章中对你最有帮助的资源取决于这两者哪个是你的主要目标:
在第一种情况下,你应该找到已知基准的现有数据集,以便确定你是否正确地进行了分析。这些通常作为竞赛数据集(如 Kaggle)或存储库数据集来找到。在这些情况下,即使已经为你做了一些初步工作,你可能也需要为特定目的清理数据。
在第二种情况下,你应该考虑有效的方法来识别有趣的时间戳数据,将其转换为系列,清理它,并将其与其他时间戳数据对齐,以制作有趣的时间序列数据。我将这些在野数据集称为发现的时间序列(这是我自己的术语,不是技术语言)。
接下来我们讨论准备好的数据集和发现的时间序列。
准备好的数据集。
学习分析或建模技术的最佳方式是在各种数据集上运行它,并看看如何应用它以及它是否帮助你达到具体目标。在这种情况下,准备好的选项是很有帮助的。
尽管时间序列数据随处可见,但并非总是能够在需要时找到想要的数据类型。如果您经常处于这种位置,您可能希望熟悉一些常用的时间序列数据存储库,因此我们将讨论您应该考虑的几个选项。
UCI 机器学习库
UCI 机器学习库(见图 2-1)包含大约 80 个时间序列数据集,从意大利城市的每小时空气质量样本到亚马逊文件访问日志再到糖尿病患者的活动、食物和血糖信息记录。这些都是非常不同类型的数据,浏览文件显示它们反映了跨时间跟踪信息的不同方式,但每个都是时间序列。
![]()
图 2-1. UCI 机器学习库包含时间序列数据集的带注释列表。
考虑 UCI 存储库中时间序列部分下列出的第一个数据集,这是关于工作缺勤的数据集(见图 2-2)。
快速查看数据显示,时间列限制为“缺勤月份”、“星期几”和“季节”,没有年份列。存在重复的时间索引,但还有一列指示员工身份,以便我们可以区分这些重复的时间点。最后,还有各种员工属性列。
![]()
图 2-2. 工作缺勤数据集是 UCI 机器学习库时间序列数据集列表中的第一个。
这个数据集可能非常具有挑战性,因为您首先需要确定数据是否全来自一年,或者行进展中 1 到 12 个月的循环是否表明年份在变化。您还需要决定是从整体上看问题,从单位时间的缺勤率角度看数据,还是查看数据集中报告的每个 ID 的缺勤情况(见图 2-3)。在第一种情况下,您将拥有一个单一的时间序列,而在后一种情况下,您将拥有多个具有重叠时间戳的时间序列。您如何查看数据将取决于您想要回答的问题。
![]()
图 2-3. 澳大利亚手语数据集中一个文件的前几行。正如您所看到的,这是一种宽文件格式。
将缺勤数据集与列表中早期的另一个数据集进行对比,即澳大利亚手语标志数据集,其中包括使用 Nintendo PowerGlove 录制的主题使用澳大利亚手语的录音。数据格式为宽 CSV 文件,每个文件夹表示个体测量,并且文件名指示手语标志。
在这个数据集中,列没有标签,也没有时间戳。尽管如此,这是时间序列数据;时间轴计算时间步长的前进,而不管实际事件发生的时间。注意,对于将符号视为时间序列的目的来说,时间单位并不重要;重要的是事件的顺序,以及您是否可以从数据描述中假设或确认测量是按照规律间隔进行的。
从检查这两个数据集可以看出,您将遇到各种数据处理挑战。我们已经注意到的一些问题有:
-
不完整的时间戳
-
时间轴可以是数据中的水平或垂直轴
-
时间概念的变化
UEA 和 UCR 时间序列分类库
UEA 和 UCR 时间序列分类库是一个新的努力,提供一组常见的时间序列数据,可用于时间序列分类任务的实验和研究。它还展示了非常多样化的数据。我们可以通过查看两个数据集来看到这一点。
其中一个数据集是瑜伽动作分类任务。分类任务涉及区分两位表演者在录制图像时执行瑜伽姿势转换系列的能力。图像被转换为一维系列。数据存储在 CSV 文件中,标签为最左侧列,其余列表示时间步长。时间从左到右穿过列,而不是从上到下穿过行。
展示了每个性别的两个样本时间序列图,见图 2-4。
单变量与多变量时间序列
到目前为止,我们查看的数据集是单变量时间序列;即它们只有一个变量随时间变化。
多变量时间序列是在每个时间戳测量多个变量的序列。它们特别适合分析,因为通常测量的变量彼此相关,并显示彼此之间的时间依赖性。我们稍后会遇到多变量时间序列数据。
![]()
图 2-4. 展示了一个男性和一个女性演员重复执行瑜伽动作的情况。我们绘制了每个演员的两个样本时间序列。x 轴上没有显式的时间标签。重要的不是时间单位,而是 x 轴数据点是否均匀分布,正如这里所展示的那样。
也要考虑葡萄酒数据集,其中葡萄酒根据其光谱的形状被分类为不同地区。那么这与时间序列分析有何关联呢?光谱是光波长与强度的图示。在这里,我们看到一个时间序列分类任务,其中根本没有时间的流逝。然而,时间序列分析适用,因为在 x 轴上有一个独特且有意义的排序,其具体含义是沿该轴的距离。时间序列分析通过利用 x 轴的顺序提供的额外信息,无论是时间还是波长或其他东西,与横截面分析有所不同。我们可以在 图 2-5 中看到这样一个“时间”序列的图示。虽然没有时间元素,但我们仍在看一个有序的数据系列,因此时间序列的常规概念同样适用。¹
![]()
图 2-5. UCI 葡萄酒数据集中葡萄酒的样本光谱。曲线中的峰值指示具有特别高吸收率的波长区域。波长在 x 轴上均匀分布,而 y 轴表示吸收率,也是线性刻度。我们可以使用时间序列分析来比较诸如上述曲线之类的曲线。
政府时间序列数据集
几十年甚至几个世纪以来,美国政府一直是可靠的时间序列数据提供者。例如,NOAA 国家环境信息中心发布了与全国各气象站有关的各种时间序列数据,涉及温度和降水,甚至可以每 15 分钟精细化。劳工统计局每月发布全国失业率指数。疾病控制与预防中心在流感季节期间每周发布流感病例统计。圣路易斯联邦储备银行提供了一系列非常丰富和有用的经济时间序列数据。
对于初次涉足时间序列分析,我建议您仅对这些现实世界的政府数据集进行探索性分析和可视化。学习这些数据集可能会很困难,因为它们提出了极为复杂的问题。例如,许多经济学家在官方发布前整个职业生涯都在努力预测失业率,但仅有有限的成功。
对于政府面临的重要但难以解决的问题,预测未来不仅在社会上有益,而且在经济上也很有利可图。许多聪明而受过良好训练的人正在解决这些问题,尽管技术水平仍然有些令人失望。解决困难问题是很好的,但不建议在这些问题上学习。
其他有用的来源
虽然我们不能详尽地覆盖所有优秀的时间序列数据源,但还有几个其他的资源库你应该探索:
[Comp CompEngine
这个“自组织时间序列数据数据库”拥有超过 25,000 个时间序列数据库,总计接近 1.4 亿个单独的数据点。这个资源库及其在网页界面上提供的相关软件,重点是促进和推广高度比较时间序列分析(hctsa)。这种分析的目标是生成高级洞见,理解各种时间行为的多种类型,而无需特定学科的数据。
Mcomp 和 M4comp2018 R 包
这些 R 包提供了 1982 年 M 竞赛(1,001 个时间序列)、2000 年 M3 竞赛(3,003 个时间序列)和 2018 年 M4 竞赛(100,000 个时间序列)的竞赛数据。这些时间序列预测竞赛之前在第一章中提到了 Rob Hyndman 教授关于时间序列预测的历史。R 的tscompdata包中还包含了更多的时间序列预测竞赛数据。最后,还可以在 CRAN 的时间序列包资源库列表中找到更多专业的时间序列数据集,详情请见时间序列包的 CRAN 资源库列表中的“时间序列数据”头部。
发现时间序列
在本章的早些时候,我们讨论了“发现时间序列”的概念,即我们从野外数据源中自行整理出来的时间序列数据。更具体地说,这些时间序列是从没有特别为时间序列分析设置的单个数据点中整理出来的,但具有足够的信息来构建时间序列。以从存储公司交易的 SQL 数据库中为特定客户拼接的交易时间序列为例,这就是一个干净的例子。在这种情况下,只要数据库中保存了时间戳或某种时间戳的代理,就可以构建时间序列。² 我们还可以想象从相同的数据中构建的其他时间序列,例如公司每天的总交易量时间序列或每周女性客户的总美元交易量时间序列。我们甚至可以想象生成多变量时间序列数据,例如一个时间序列,分别表示每周所有未满 18 岁客户的总交易量、每周 65 岁以上女性的总支出和公司每周的广告支出。这将在每个时间步骤给我们提供三个指标,一个多变量时间序列。
在结构化数据中找到时间序列数据,即使未明确存储为时间序列,也可以很容易,因为时间戳是无处不在的。以下是一些在数据库中看到时间戳的示例:
事件的时间戳记录
如果您的数据有时间戳,您就有可能构建时间序列。即使您只记录了文件访问时的时间而没有其他信息,您也有了一个时间序列。例如,在这种情况下,您可以模拟时间戳之间的时间差,并将其标记为其后时间戳,使得您的时间序列将由时间轴上的时间和值轴上的时间差组成。您可以进一步进行聚合,将这些时间差作为更大时间段的平均值或总和,或者您可以单独记录它们。
“无时间性”测量,其中另一种测量替代时间
在某些情况下,数据中时间并不显式,但在数据集的基础逻辑中有所体现。例如,当距离由已知的实验参数引起时,您可能会将数据视为“距离与值”的关系。如果您能够将其中一个变量映射到时间上,那么您就拥有了一个时间序列。或者,如果您的一个轴具有已知的距离和排序关系(例如波长),那么您也正在查看时间序列数据,例如前面提到的葡萄酒光谱案例。
物理迹象
许多科学学科记录物理迹象,无论是医学、听力还是天气。这些曾经是通过模拟过程生成的物理迹象,但现在它们以数字格式存储。尽管它们可能存储在不显而易见的格式中,例如图像文件或数据库的单个向量字段中,但这些也是时间序列。
将一组表中的时间序列数据收集进行改装
发现的时间序列的典型例子是从存储在 SQL 数据库中的状态类型和事件类型数据中提取的。这也是最相关的示例,因为仍然有大量数据存储在传统的结构化 SQL 数据库中。
想象一下为一个大型非营利组织工作。您一直在跟踪各种可能适合进行时间序列分析的因素:
您可以使用几种时间序列技术来查看数据:
-
您可以生成成员对电子邮件响应的二维直方图以了解成员是否因电子邮件而感到疲劳的时间线。(我们将在第三章中说明使用二维直方图来理解时间序列的方法。)
-
你可以将捐赠预测转化为时间序列预测问题。(我们将在 第 4 章 中讨论经典的统计预测。)
-
你可以检查在重要情况下成员行为轨迹的典型模式是否存在。例如,是否存在一种典型的事件模式表明一个成员即将离开您的组织(也许连续三封电子邮件被删除)?在时间序列分析中,你可以将这视为基于外部动作检测成员潜在状态的方法。(我们将在 第 7 章 中讨论时间序列分析的状态空间方法。)
正如我们所看到的,在一个简单的 SQL 数据库中有许多时间序列的问题和答案。在许多情况下,组织在设计数据库架构时并未计划进行时间序列分析。在这种情况下,我们需要从不同的表和来源收集和组装时间序列。
一个案例:组装时间序列数据收集
如果你有幸有几个相关的数据源可用,你将需要将它们排列在一起,可能要处理不同的时间戳约定或数据中不同的粒度级别。让我们为我们正在使用的非营利组织示例创建一些数字。假设你有如下所示的数据:表 2-1 到 表 2-3:
表 2-1. 每位成员加入的年份和当前会员状态
| 成员 ID |
加入年份 |
成员状态 |
| 1 |
2017 |
金 |
| 2 |
2018 |
银 |
| 3 |
2016 |
无效 |
表 2-2. 一周内成员打开的电子邮件数量
| 成员 ID |
周 |
打开的邮件数 |
| 2 |
2017-01-08 |
3 |
| 2 |
2017-01-15 |
2 |
| 1 |
2017-01-15 |
1 |
表 2-3. 成员捐赠给您组织的时间
| 成员 ID |
时间戳 |
捐赠金额 |
| 2 |
2017-05-22 11:27:49 |
1,000 |
| 2 |
2017-04-13 09:19:02 |
350 |
| 1 |
2018-01-01 00:15:45 |
25 |
你可能已经用过这种表格形式的数据工作过。有了这样的数据,你可以回答许多问题,比如成员打开的邮件总数与捐赠总数之间的关系。
你还可以回答与时间有关的问题,比如一个成员是在加入后不久捐赠还是很久之后捐赠。然而,如果不将这些数据转换为更适合时间序列的格式,你就无法深入了解可能帮助你预测某人何时可能捐赠的更精细行为(比如根据他们最近是否在打开邮件)。
你需要将这些数据放入一个合理的时间序列分析格式中。你将需要解决一些挑战。
你应该从考虑我们已有数据的时间轴开始。在上述表格中,我们有三个层次的时间分辨率:
-
一个年度成员状态
-
一周内打开的电子邮件总数
-
捐赠的即时时间戳
您还需要检查数据是否意味着您认为它意味着什么。例如,您可能想要确定成员状态是年度状态还是最近的状态。回答这个问题的一个方法是检查是否有任何成员有多个条目:
## python
>>> YearJoined.groupby('memberId').count().
groupby('memberStats').count()
1000
在这里,我们可以看到所有 1,000 名成员只有一个状态,因此他们加入的年份很可能确实是YearJoined,并且伴随一个可能是成员当前状态或加入时的状态。这影响如何使用状态变量,因此,如果您打算进一步分析这些数据,您需要与了解数据管道的人澄清。如果您将成员的当前状态应用于过去数据的分析中,这将是一种预测,因为您会向时间序列模型输入在当时无法知道的内容。这就是为什么在不知道分配时间的状态变量(例如YearJoined)时,您不应该使用它的原因。
查看电子邮件表,week列及其内容都表明数据是一个周时间戳或时间段。这必须是一周的汇总,所以我们应该将这些时间戳视为每周期而不是相隔一周的时间戳。
您应该评估一些重要的特征。例如,您可以从询问时间中如何报告周开始。虽然我们可能没有信息来重新组织表格,但如果与我们的行业相比,一周的划分方式有些奇怪,我们可能也想了解这一点。对于分析人类活动,通常有意义的是查看星期日至星期六或星期一至星期日的日历周,而不是与人类活动周期不太一致的周。因此,例如,不要随意从一月一日开始您的一周。
您还可以问空周是否报告了?也就是说,成员在表中打开 0 封电子邮件的周是否有位置?当我们想要进行面向时间的建模时,这很重要。在这种情况下,我们需要始终在数据中有空周存在,因为 0 周仍然是一个数据点。
## python
>>> emails[emails.EmailsOpened < 1]
Empty DataFrame
Columns: [EmailsOpened, member, week]
Index: []
有两种可能性:要么空值没有报告,要么成员总是至少有一个电子邮件事件。任何使用电子邮件数据的人都知道,让人们打开电子邮件很难,因此成员每周至少打开一封电子邮件的假设是相当不可能的。在这种情况下,我们可以通过查看仅一个用户的历史来解决这个问题。
## python
>>> emails[emails.member == 998]
EmailsOpened member week
25464 1 998 2017-12-04
25465 3 998 2017-12-11
25466 3 998 2017-12-18
25467 3 998 2018-01-01
25468 3 998 2018-01-08
25469 2 998 2018-01-15
25470 3 998 2018-01-22
25471 2 998 2018-01-29
25472 3 998 2018-02-05
25473 3 998 2018-02-12
25474 3 998 2018-02-19
25475 2 998 2018-02-26
25476 2 998 2018-03-05
我们可以看到一些周是缺失的。在 2017 年 12 月 18 日后,2017 年 12 月没有任何电子邮件事件。
通过计算该成员的第一个和最后一个事件之间应该有多少周观察,我们可以更数学地检查这一点。首先,我们计算成员任期的长度,以周为单位。
## python
>>> (max(emails[emails.member == 998].week) -
min(emails[emails.member == 998].week)).days/7
25.0
然后我们看看该成员有多少周的数据:
## python
>>> emails[emails.member == 998].shape
(24, 3)
我们这里有 24 行,但应该有 26 行。这显示这个成员的一些周数据是缺失的。顺便说一句,我们也可以通过分组操作同时对所有成员运行此计算,但是仅考虑一个成员更易于理解示例。
现在我们已确认确实有缺失的周,我们将继续填补空白,以便我们现在拥有完整的数据集。我们不能确定识别所有缺失的周,因为有些可能发生在我们记录的日期之前或之后。然而,我们可以做的是填补成员在非空事件发生之前和之后第一个和最后一个时间之间的缺失值。
通过利用 Pandas 的索引功能,填充所有成员的所有缺失周要比编写我们自己的解决方案更容易。我们可以为 Pandas 数据框生成一个 MultiIndex,这将创建周和成员的所有组合——即笛卡尔积:
## python
>>> complete_idx = pd.MultiIndex.from_product((set(emails.week),
set(emails.member)))
我们使用此索引重新索引原始表格,并填充缺失的值——在这种情况下,假设没有记录意味着没有可记录的内容,我们还重置索引以使成员和周信息作为列可用,并将这些列命名为:
## python
>>> all_email = emails.set_index(['week', 'member']).
reindex(complete_idx, fill_value = 0).
reset_index()
>>> all_email.columns = ['week', 'member', 'EmailsOpened']
让我们再次看看成员 998:
## python
>>> all_email[all_email.member == 998].sort_values('week')
week member EmailsOpened
2015-02-09 998 0
2015-02-16 998 0
2015-02-23 998 0
2015-03-02 998 0
2015-03-09 998 0
注意我们一开始就有大量的零。这些可能是成员加入组织之前的时间,因此他们不会出现在电子邮件列表中。在我们想要保留成员真正空闲周的分析中,没有太多种类。如果我们有成员开始接收电子邮件的确切日期,我们将有一个客观的截止日期。由于现状如此,我们将让数据指导我们。对于每个成员,我们通过将电子邮件 DataFrame 按成员分组,并选择最大和最小周值来确定 start_date 和 end_date 截止日期:
## python
>>> cutoff_dates = emails.groupby('member').week.
agg(['min', 'max']).reset_index)
>>> cutoff_dates = cutoff_dates.reset_index()
我们从 DataFrame 中删除那些在时间线上没有贡献意义的行,具体来说是每个成员的第一个非零计数之前的 0 行:
## python
>>> for _, row in cutoff_dates.iterrows():
>>> member = row['member']
>>> start_date = row['min']
>>> end_date = row['max']
>>> all_email.drop(
all_email[all_email.member == member]
[all_email.week < start_date].index, inplace=True)
>>> all_email.drop(all_email[all_email.member == member]
[all_email.week > end_date].index, inplace=True)
< 还是 <= ?
我们使用 < 和 > 操作符,不包括等号,因为 start_date 和 end_date 包含了有意义的数据点,并且因为我们正在删除数据,而不是保留数据,按照我们的代码编写。在这种情况下,我们希望在分析中包括这些周,因为它们是第一个和最后一个有意义的数据点。
你最好与你的数据工程师和数据库管理员一起工作,说服他们以时间感知的方式存储数据,特别是关于如何创建时间戳及其含义。你能在上游解决的问题越多,下游数据管道的工作就越少。
现在我们已经清理了电子邮件数据,我们可以考虑新的问题。例如,如果我们想考虑成员电子邮件行为与捐赠的关系,我们可以做一些事情:
-
将DonationAmount聚合到每周,以便时间段可比。然后可以合理地询问捐赠是否与成员对电子邮件的响应有某种关联。
-
将上周的EmailsOpened作为给定周的DonationAmount的预测因子。请注意,我们必须使用上周的数据,因为EmailsOpened是一周的汇总统计数据。如果我们想要预测星期三的捐款,并且我们的EmailsOpened总结了从周一到周日的电子邮件开启行为,那么使用同一周的信息可能会告诉我们成员在我们能知道之后的行为(例如,他们是否在捐款后的星期五打开了电子邮件)。
构建一个基础时间序列。
考虑如何将电子邮件和捐款数据相互关联。我们可以将捐款数据降采样,使其变成一个每周的时间序列,可与电子邮件数据进行比较。作为一个组织,我们对每周总金额感兴趣,因此我们通过求和将时间戳聚合为每周周期。一周内捐款超过一个的情况不太可能发生,因此每周捐款金额将反映大多数捐赠者的个人捐款金额。
## python
>>> donations.timestamp = pd.to_datetime(donations.timestamp)
>>> donations.set_index('timestamp', inplace = True)
>>> agg_don = donations.groupby('member').apply(
lambda df: df.amount.resample("W-MON").sum().dropna())
在这段代码中,我们首先将字符串字符转换为适当的时间戳数据类,以便从 Pandas 的内置日期相关索引中获益。我们将时间戳设为索引,这是对数据帧进行重新采样所必需的。最后,对于从子集到每个会员得到的数据帧,我们按周分组并汇总捐款,删除没有捐款的周,然后将它们收集在一起。
请注意,我们进行了带锚定周的重新采样,以便与我们电子邮件表中已有的同一周日期匹配。还要注意,从人类角度来看,将周锚定在“星期一”是有意义的。
现在我们有了捐款信息和电子邮件信息以相同频率采样的数据,我们可以将它们连接起来。只要我们已经将周的数据锚定到同一周的某一天,Pandas 使这变得很简单。我们可以迭代每个会员,并合并数据帧:
## python
>>> for member, member_email in all_email.groupby('member'):
>>> member_donations = agg_donations[agg_donations.member
== member]
>>> member_donations.set_index('timestamp', inplace = True)
>>> member_email.set_index ('week', inplace = True)
>>> member_email = all_email[all_email.member == member]
>>> member_email.sort_values('week').set_index('week')
>>> df = pd.merge(member_email, member_donations, how = 'left',
left_index = True,
right_index = True)
>>> df.fillna(0)
>>> df['member'] = df.member_x
>>> merged_df = merged_df.append(df.reset_index()
[['member', 'week', 'emailsOpened', 'amount']])
现在我们有了按会员排列的电子邮件和捐款数据。对于每个会员,我们只包括有意义的周,而不是会员期前后的周。
我们可能会将电子邮件行为视为与捐赠行为相关的“状态”变量,但我们可能希望从上周保留状态,以避免预先看到。例如,假设我们正在构建一个模型,该模型使用电子邮件行为来预测会员的下次捐赠。在这种情况下,我们可能会考虑观察一周内电子邮件打开的模式作为潜在的指标。我们需要将给定周的捐赠与上周的电子邮件行为对齐。我们可以轻松地对我们的数据进行处理,使其按周对齐,然后根据适当的周数进行移动。例如,如果我们想将捐赠向前推移一周,我们可以使用 shift 运算符轻松实现,尽管我们需要确保对每个会员都这样做:
## python
>>> df = merged_df[merged_df.member == 998]
>>> df['target'] = df.amount.shift(1)
>>> df = df.fillna(0)
>>> df
最好将这个目标存储在一个新列中,而不是覆盖旧列,特别是如果你没有将捐赠金额的时间戳也同时前移。我们使用 Pandas 内置的 shift 功能,将捐赠金额向未来推移了一周。你也可以用负数向过去推移。通常情况下,你会有更多的预测因子而不是目标变量,因此将目标变量向后移动是合理的。我们可以在这里看到代码的结果:
amount emailsOpened member week target
0 1 998 2017-12-04 0
0 3 998 2017-12-11 0
0 3 998 2017-12-18 0
0 0 998 2017-12-25 0
0 3 998 2018-01-01 0
50 3 998 2018-01-08 0
0 2 998 2018-01-15 50
现在我们已经填补了缺失的行,为会员 998 创建了所需的 26 行。我们的数据现在更加清洁和完整。
总结一下,这些是我们用来重构数据的特定于时间序列的技术:
-
重新校准我们数据的分辨率以适应我们的问题。通常数据提供的时间信息比我们实际需要的更加具体。
-
避免使用数据来生成时间戳,从而避免预先看到数据的可用性。
-
记录所有相关时间段,即使“什么也没发生”。零计数与其他计数同样具有信息量。
-
通过不使用数据来生成我们还不应该知道的信息的时间戳,避免预先看到。
到目前为止,我们通过使捐赠和电子邮件时间序列在相同时间点和相同频率采样来创建了原始发现时间序列。然而,在进行分析之前,我们没有彻底清理这些数据或者完全探索它们。这是我们将在第三章中进行的工作。
时间戳问题
时间戳对于时间序列分析非常有帮助。通过时间戳,我们可以推断出许多有趣的特征,比如时间的具体时段或一周中的某一天。这些特征对于理解数据尤为重要,尤其是涉及人类行为的数据。然而,时间戳也有其难点。在这里,我们讨论了时间戳数据的一些困难。
哪个时间戳?
当看到时间戳时,你应该首先问的问题是:这个时间戳是由什么过程生成的,是怎样生成的,以及何时生成的。通常发生的事件并不一定与记录事件的时间一致。例如,研究人员可能会在笔记本上写下一些内容,然后稍后将其转录到用作日志的 CSV 文件中。时间戳是否表示他们写下内容的时间,还是将其转录到 CSV 文件的时间?或者移动应用程序用户可能会在他们的手机离线时执行操作,以便数据稍后通过一些时间戳的组合上传到您的服务器。这些时间戳可以反映行为发生的时间,应用程序记录行为的时间,元数据上传到服务器的时间,或者从服务器下载数据返回到应用程序时的时间(或数据管道中的任何其他事件)。
一开始,时间戳可能会提供清晰度,但如果缺乏适当的文档,这种清晰度很快就会消失。当您查看新的时间戳时,您的第一步应始终是尽可能好地了解事件时间的来源。
一个具体的例子将说明这些困难。假设您正在查看从减肥移动应用程序中获取的数据,并看到餐饮日记,其中包含如表 2-4 所示的条目。
Table 2-4. 减肥应用程序的样本餐饮日记
| 时间 |
摄入 |
| Mon, April 7, 11:14:32 |
煎饼 |
| Mon, April 7, 11:14:32 |
三明治 |
| Mon, April 7, 11:14:32 |
pizza |
可能是这位用户同时吃了煎饼、三明治和披萨,但更有可能的情况是什么?用户是否指定了这个时间,还是它是自动创建的?界面是否提供了用户可以调整或选择忽略的自动时间?对这些问题的一些答案可以更好地解释相同时间戳背后的原因,而不是一个试图减肥的用户同时吃了煎饼、披萨和三明治作为开胃菜。
即使用户确实在 11:14 时吃了所有这些食物,但世界上哪里是 11:14 呢?这是用户的本地时间还是全球时钟?即使在用户在一顿饭中吃了所有这些食物的情况下(尽管可能性很小),仅凭这些行就不能很好地了解到这顿饭的时间方面。我们不知道这是早餐、午餐、晚餐还是小吃。要向用户提供有趣的信息,我们需要能够具体讨论一天中的当地小时,而没有时区信息我们无法做到这一点。
解决这些问题的最佳方法是查看所有收集和存储数据的代码,或者与编写该代码的人交谈。在研究系统上所有可用的人类和技术数据规范后,你还应该亲自尝试整个系统,以确保数据的行为与你被告知的一致。你对数据管道了解得越深入,你就越不太可能因为时间戳实际上不是你认为的那样而问错问题。
你需全面了解数据,对于上游管道的工作人员来说,他们并不知道你为分析准备了什么。尽可能地亲自评估时间戳生成的方式。因此,如果你正在分析来自移动应用管道的数据,请下载该应用,在各种情况下触发事件,并查看你自己的数据是什么样子。在与管理数据管道的人交谈后,你可能会对你的行为如何记录感到惊讶。追踪多个时钟和不同情况很困难,因此大多数数据集会简化时间的真实情况。你需要确切了解它们的处理方式。
猜测时间戳以理解数据含义
如果你处理遗留数据管道或未记录的数据,你可能无法探索工作中的管道或与维护它的人交谈的选项。你需要进行一些实证调查,以了解是否能推断出时间戳的含义:
本地时间还是世界协调时间?
大多数时间戳都以世界协调时间(UTC)或单一时区存储,这取决于服务器的位置,而与用户的位置无关。按照本地时间存储数据是相当不寻常的。但我们应该考虑这两种可能性,因为“野外”中都有这两种情况。
我们假设,如果时间是本地时间戳(每个用户的本地时间),我们应该在数据中看到反映白天和黑夜行为的日常趋势。更具体地说,我们应该期待在夜间,即使在大多数文化中人们不会在深夜吃饭的时段,也应该看到餐次数显著减少。对于我们移动应用程序的示例,如果我们创建一天内每小时餐次计数的直方图,应该会发现几个小时内记录的餐次较少。
如果我们在显示的小时内没有看到以日为导向的模式,我们可以得出结论,数据很可能是由某个全球时钟标记的,并且用户群体必须分布在许多不同的时区。在这种情况下,推断个别用户的当地时间将非常具有挑战性(假设没有时区信息)。我们可能会考虑每个用户的个别探索,看看是否可以编写启发式代码来近似标记用户的时区,但这样的工作既计算密集又不总是准确。
即使你无法准确确定用户的时区,拥有全局时间戳仍然很有用。首先,你可以确定你的应用服务器在何时何地最频繁记录饭菜时间。你还可以计算用户记录的每餐之间的时间差异,因为时间戳是绝对时间,所以不需要担心用户是否更改了时区。除了侦探工作之外,这也是一种有趣的特征生成方式:
## python
>>> df['dt'] = df.time - df.time.shift(-1)
dt列将是你可以在分析中使用的特征。使用这种时间差异也可以让你有机会估计每个用户的时区。你可以查看用户通常具有长dt的时间,这可能指向该用户的夜间发生时间。从那里开始,你可以开始确定每个人的“夜间”时间,而不必进行峰对峰分析。
用户行为还是网络行为?
返回到我们短数据样本提出的另一个问题,我们问的是我们的用户是否吃了奇怪的饭菜,或者我们的时间戳是否与上传活动相关。
用来确定用户时区的相同分析方法也适用于确定时间戳是否是用户或网络行为的功能。一旦你有了dt列(如先前计算的),你可以寻找 0 值的聚类,并且可以定性地确定它们是单个行为事件还是单个网络事件。你还可以查看dt是否在不同的日期间隔内表现出周期性。如果它们是用户行为的功能,它们更可能是周期性的,而不是网络连接或其他软件相关行为的功能。
总结一下,以下是一些你可能能够用现有数据集解决的问题,即使几乎没有关于时间戳如何生成的信息:
什么是有意义的时间尺度?
您应该基于关于您正在研究的行为的领域知识以及您能确定的与数据收集方式相关的细节,对您接收到的时间戳的时间分辨率持保留态度。
例如,想象一下您正在查看日销售数据,但您知道在许多情况下,经理们会等到周末报告数据,估计粗略的日常数字而不是每天记录它们。由于回忆问题和内在认知偏差,测量误差可能会相当大。您可以考虑将销售数据的分辨率从日常更改为每周,以减少或平均这种系统误差。否则,您应该构建一个模型,考虑不同工作日之间可能存在的偏误。例如,可能经理在星期五报告数据时系统性地高估他们的星期一业绩。
心理时间贴现
时间贴现是一种称为心理距离的现象的表现,它描述了我们在进行距离较远的估算或评估时更加乐观(也更不现实)的倾向。时间贴现预测,与最近记忆中记录的数据相比,从较远过去报告的数据将系统性地存在偏差。这与更普遍的遗忘问题不同,并暗示了非随机误差。每当您查看手动输入但未与记录事件同时发生的人工生成数据时,请记住这一点。
另一种情况涉及对系统的物理知识。例如,一个人的血糖水平变化速度存在一定的限制,因此,如果您在几秒钟内查看一系列血糖测量值,您可能应该对它们进行平均处理,而不是将它们视为不同的数据点。任何医生都会告诉您,如果您在几秒钟内查看了许多测量值,那么您正在研究设备的误差而不是血糖变化的速率。
人类知道时间在流逝
每当您在测量人类时,请记住人们对时间的流逝有多种反应方式。例如,最近的研究显示,调整一个人视野中的时钟速度会影响该人血糖水平变化的速度。
清洗您的数据
在本节中,我们将解决时间序列数据集中的以下常见问题:
-
缺失数据
-
更改时间序列的频率(即上采样和下采样)
-
平滑数据
-
处理数据的季节性
-
防止无意识的超前看法
处理缺失数据
缺失数据非常普遍。例如,在医疗保健领域,医疗时间序列中的缺失数据可能有多种原因:
一般化地说,与横截面数据分析相比,时间序列分析中缺失数据更为常见,因为纵向采样的负担特别重:不完整的时间序列非常普遍,因此已经开发出方法来处理记录中的空缺。
处理时间序列缺失数据最常见的方法有:
插补
当我们根据对整个数据集的观察填补缺失数据时。
插值
当我们使用相邻数据点来估计缺失值时。插值也可以是一种插补形式。
删除受影响的时间段
当我们选择根本不使用具有缺失数据的时间段时。
我们将讨论插补和插值,并很快展示这些方法的机制。我们关注保留数据,而像删除具有缺失数据的时间段这样的方法会导致模型的数据减少。是否保留数据或丢弃问题时间段将取决于您的用例以及考虑到模型数据需求是否能够牺牲这些时间段。
准备数据集以测试缺失数据插补方法
我们将使用自 1948 年以来由美国政府发布的月度失业数据(可免费下载)。然后我们将从这些基础数据生成两个数据集:一个是真正随机缺失数据的情况,另一个是时间序列历史上最高失业率的月份。这将为我们提供两个测试案例,以查看在随机和系统性缺失数据存在的情况下,插补的行为如何。
提示
我们将转向 R 来进行下一个示例。在整本书中,我们将自由切换使用 R 和 Python。我假设您在使用数据框架和 R 与 Python 中的矩阵方面具有一定的背景知识。
## R
> require(zoo) ## zoo provides time series functionality
> require(data.table) ## data.table is a high performance data frame
> unemp <- fread("UNRATE.csv")
> unemp[, DATE := as.Date(DATE)]
> setkey(unemp, DATE)
> ## generate a data set where data is randomly missing
> rand.unemp.idx <- sample(1:nrow(unemp), .1*nrow(unemp))
> rand.unemp <- unemp[-rand.unemp.idx]
> ## generate a data set where data is more likely
> ## to be missing when unemployment is high
> high.unemp.idx <- which(unemp$UNRATE > 8)
> num.to.select <- .2 * length(high.unemp.idx)
> high.unemp.idx <- sample(high.unemp.idx,)
> bias.unemp <- unemp[-high.unemp.idx]
因为我们从数据表中删除了行来创建一个带有缺失数据的数据集,所以我们需要读取缺失的日期和 NA 值,对于这一点,data.table包的rolling join功能非常有用。
## R
> all.dates <- seq(from = unemp$DATE[1], to = tail(unemp$DATE, 1),
by = "months")
> rand.unemp = rand.unemp[J(all.dates), roll=0]
> bias.unemp = bias.unemp[J(all.dates), roll=0]
> rand.unemp[, rpt := is.na(UNRATE)]
## here we label the missing data for easy plotting
使用滚动连接,我们生成数据集开始和结束日期之间应该可用的所有日期序列。这为我们提供了数据集中要填充为NA的行。
现在我们有了带有缺失值的数据集,我们将看一下填充这些缺失值的几种具体方法:
我们将比较这些方法在随机缺失和系统缺失数据集中的性能。由于我们从完整数据集生成了这些数据集,因此实际上可以确定它们的表现而不是推测。当然,在现实世界中,我们永远不会有缺失数据来检查我们的数据插补。
前向填充
填补缺失值的最简单方法之一是向前传递缺失点前的最后已知值,这种方法被称为前向填充。不需要数学或复杂的逻辑。只需考虑随着可用数据的时间推移经验,您可以看到在时间的缺失点,您唯一可以确信的是已记录的数据。在这种情况下,使用最近的已知测量是有意义的。
可以使用zoo包中的na.locf轻松实现前向填充:
## R
> rand.unemp[, impute.ff := na.locf(UNRATE, na.rm = FALSE)]
> bias.unemp[, impute.ff := na.locf(UNRATE, na.rm = FALSE)]
>
> ## to plot a sample graph showing the flat portions
> unemp[350:400, plot (DATE, UNRATE,
col = 1, lwd = 2, type = 'b')]
> rand.unemp[350:400, lines(DATE, impute.ff,
col = 2, lwd = 2, lty = 2)]
> rand.unemp[350:400][rpt == TRUE, points(DATE, impute.ff,
col = 2, pch = 6, cex = 2)]
这将导致一个看起来很自然的图形,除非您看到重复的数值以解决缺失数据,如在图 2-6 中所示。正如您在图中所注意到的,前向填充的数值通常不会偏离真实数值太远。
![]()
图 2-6. 原始时间序列用实线绘制,以及用虚线绘制的具有随机缺失点的前向填充值的时间序列。前向填充的值标记有向下指向的三角形。
我们还可以通过将系列的值相互绘制来比较这些系列的值。也就是说,对于每个时间步,我们将真实已知值与具有插值值的同一时间的值进行绘制。大多数值应完全匹配,因为大多数数据是存在的。我们在图 2-7 中看到这一点体现在 1:1 线上。我们还看到一些点散布在该线外,但它们似乎并没有系统偏离。
![]()
图 2-7. 绘制真实失业率与前向填充系列的图形。这张图表明,前向填充并没有系统地扭曲数据。
向后填充
正如您可以将过去的值带入以填补缺失数据一样,您也可以选择向后传播值。然而,这是一种预测,所以只有在您不打算从数据中预测未来,并且从领域知识来看,向后填充数据比向前填充更合理时,才应该这样做。
在某些情况下,前向填充是完成缺失数据的最佳方法,即使“更高级的方法”也是可能的。例如,在医疗环境中,缺失值通常表示医务人员认为不必重新测量值,可能是因为预期患者的测量值是正常的。在许多医疗情况下,这意味着我们可以应用前向填充到缺失值,使用最后已知的值,因为这是激励医务人员不重新测量的假设。
向前填充有许多优点:它计算上不那么要求,可以轻松应用于实时流数据,并且在插补方面表现出色。我们很快会看到一个例子。
移动平均
我们还可以使用滚动均值或中位数来填充数据。被称为移动平均,它类似于向前填充,因为您使用过去的值来“预测”缺失的未来值(插补可以是一种预测形式)。然而,使用移动平均时,您将使用多个最近过去时间的输入。
在许多情况下,移动平均数据插补比向前填充更适合任务。例如,如果数据嘈杂,并且你有理由怀疑任何个别数据点相对于整体平均值的价值,你应该使用移动平均而不是向前填充。向前填充可能会包括比你感兴趣的“真实”度量更多的随机噪声,而平均化可以去除部分噪声。
为了防止前瞻,仅使用发生在缺失数据点之前的数据。因此,您的实现可能会像这样:
## R
> ## rolling mean without a lookahead
> rand.unemp[, impute.rm.nolookahead := rollapply(c(NA, NA, UNRATE), 3,
> function(x) {
> if (!is.na(x[3])) x[3] else mean(x, na.rm = TRUE)
> })]
> bias.unemp[, impute.rm.nolookahead := rollapply(c(NA, NA, UNRATE), 3,
> function(x) {
> if (!is.na(x[3])) x[3] else mean(x, na.rm = TRUE)
> })]
我们将缺失数据的值设置为其之前值的平均值(因为我们以最终值为索引,并使用此值来确定其是否缺失以及如何替换它)。
提示
移动平均不一定是算术平均。例如,指数加权移动平均会更加重视近期数据而不是过去的数据。另外,几何平均在展现强序列相关性的时间序列和值随时间复合的情况下会有帮助。
在用移动平均插补缺失数据时,考虑您是否需要仅使用前瞻数据来了解移动平均值的值,或者是否愿意构建前瞻。如果您不关心前瞻,您的最佳估计将包括缺失数据之前和之后的点,因为这将最大化输入到您估计中的信息。在这种情况下,您可以实现一个滚动窗口,如使用zoo包的rollapply()功能所示:
## R
> ## rolling mean with a lookahead
> rand.unemp[, complete.rm := rollapply(c(NA, UNRATE, NA), 3,
> function(x) {
> if (!is.na(x[2]))
> x[2]
> else
> mean(x, na.rm = TRUE)
> })]
在可视化和记录应用程序中使用过去和未来信息对于数据预处理很有用,但正如前面提到的,如果您正在准备将数据馈送到预测模型中,则不合适。
前瞻移动平均和使用未来和过去数据计算的移动平均的结果显示在图 2-8 中。
![]()
图 2-8. 虚线显示了没有前瞻的移动平均插补,而虚线则显示了具有前瞻的移动平均插补。同样,方形显示了非前瞻插补点,而倒三角形显示了具有前瞻的移动平均。
滚动平均数据插补可以减少数据集的方差。这是在计算准确性、R²统计或其他误差指标时需要记住的事情。你的计算可能会高估模型的性能,这是构建时间序列模型时经常遇到的问题。
使用数据集的平均值来填补缺失数据
在横断面的情况下,常见的方法是通过在变量缺失的地方填入平均值或中位数来填补缺失数据。虽然这可以用于时间序列数据,但大多数情况下并不合适。知道数据集的平均值涉及看向未来……这就是预先看!
插值
插值是一种根据我们希望整体数据行为的几何约束来确定缺失数据点值的方法。例如,线性插值将缺失数据限制为与已知相邻点一致的线性拟合。
线性插值特别有用和有趣,因为它允许你利用你对系统随时间行为的了解。例如,如果你知道系统以线性方式行为,你可以将这种知识应用进去,这样只有线性趋势会用于填补缺失数据。在贝叶斯术语中,它允许你向你的插补注入先验。
与移动平均类似,插值可以这样做,以便同时查看过去和未来的数据,或者只查看一个方向。通常的注意事项是:如果你接受这样做会导致预先看的影响,并且你确定这对你的任务不是问题,那么允许你的插值只有未来数据访问。
在这里,我们使用过去和未来的数据点进行插值(参见图 2-9):
## R
> ## linear interpolation
> rand.unemp[, impute.li := na.approx(UNRATE)]
> bias.unemp[, impute.li := na.approx(UNRATE)]
>
> ## polynomial interpolation
> rand.unemp[, impute.sp := na.spline(UNRATE)]
> bias.unemp[, impute.sp := na.spline(UNRATE)]
>
> use.idx = 90:120
> unemp[use.idx, plot(DATE, UNRATE, col = 1, type = 'b')]
> rand.unemp[use.idx, lines(DATE, impute.li, col = 2, lwd = 2, lty = 2)]
> rand.unemp[use.idx, lines(DATE, impute.sp, col = 3, lwd = 2, lty = 3)]
![]()
图 2-9. 虚线显示线性插值,而点线显示样条插值。
有许多情况适合使用线性(或样条)插值。考虑平均每周温度,其中根据一年中的时间,有升高或降低温度的已知趋势。或者考虑增长中的企业的年销售数据。如果趋势是每年业务量线性增长,基于该趋势填补缺失数据是合理的数据插补。换句话说,我们可以使用线性插值,这将考虑到趋势,而不是移动平均,它将不会。如果存在增值趋势,则移动平均将系统地低估缺失值。
也有很多情况不适合线性(或样条)插值。例如,如果您在天气数据集中缺少降水数据,您不应该在线性插值已知天数之间外推;我们都知道,降水并不是这样工作的。同样,如果我们看的是某人每天的睡眠小时数,但是缺少了几天的数据,我们不应该在线性外推睡眠小时数之间。例如,已知的端点之一可能包括熬夜学习后的 30 分钟小睡。这不太可能估算缺失的数据。
总体比较
现在我们已经进行了几种不同类型的插补,我们可以查看结果,比较这些不同的数据插补在这个数据集上的表现如何。
我们生成了两个具有缺失数据的数据集,一个是随机缺失数据,另一个是缺失了不利数据点(高失业率)。当我们比较我们采用的方法以确定哪种方法产生了最佳结果时,我们可以看到均方误差可能会有很大的百分比差异:
## R
> sort(rand.unemp[ , lapply(.SD, function(x) mean((x - unemp$UNRATE)²,
> na.rm = TRUE)),
> .SDcols = c("impute.ff", "impute.rm.lookahead",
> "impute.rm.nolookahead", "impute.li",
> "impute.sp")])
impute.li impute.rm.lookahead impute.sp impute.ff impute.rm.nolookahead
0.0017 0.0019 0.0021 0.0056 0.0080
> sort(bias.unemp[ , lapply(.SD, function(x) mean((x - unemp$UNRATE)²,
> na.rm = TRUE)),
> .SDcols = c("impute.ff", "impute.rm.lookahead",
> "impute.rm.nolookahead", "impute.li",
> "impute.sp")])
impute.sp impute.li impute.rm.lookahead impute.rm.nolookahead impute.ff
0.0012 0.0013 0.0017 0.0030 0.0052
请记住,前述的许多方法都包含了前瞻性。唯一不包含前瞻性的方法是前向填充和没有前瞻性的移动平均(也有带有前瞻性的移动平均)。因此,不足为奇的是在错误方面存在一系列差异,并且没有前瞻性的方法表现不如其他方法。
最终备注
在这里,我们涵盖了时间序列应用中最简单和最常用的缺失数据插补方法。数据插补仍然是数据科学研究的一个重要领域。你做出的决策越重要,就越需要仔细考虑数据缺失的潜在原因及其修正可能带来的潜在影响。以下是你应该记住的一些谨慎提示:
-
无法证明数据真的是随机缺失,而且在大多数真实世界的情况下,缺失并不真的是随机的,这也不足为奇。
-
有时测量数据缺失的概率可以通过您已经测量的变量来解释,但有时则不然。具有许多特征的广泛数据集是调查缺失数据模式可能解释的最佳方式,但这并不是时间序列分析的常规方法。
-
当您需要了解将缺失数据插入值引入的不确定性时,您应该运行各种场景,并尽可能与参与数据收集过程的人交流。
-
您应该考虑如何处理缺失数据以适应后续数据使用。您必须小心防止前瞻性,或者决定前瞻性将如何严重影响您后续工作的有效性。
上采样和下采样
常常来自不同来源的相关时间序列数据将不具有相同的采样频率。这是你可能希望改变数据采样频率的众多原因之一。当然,你不能改变信息实际被测量的速率,但你可以改变数据收集中时间戳的频率。这被称为上采样和下采样,分别用于增加或减少时间戳的频率。
我们在“从一组表中重新调整时间序列数据收集”中对时间数据进行了降采样。在这里,我们更加通用地讨论了降采样和上采样的如何和为什么。
注意
降采样是将数据子集化,使时间戳的频率低于原始时间序列。上采样则是将数据表示为如果它被更频繁地收集的情况。
降采样
任何时候,当你降低数据的频率,你就在进行降采样。这通常发生在以下情况下。
数据的原始分辨率不合理
原始数据粒度不合理的原因很多。例如,你可能测量得太频繁了。假设你有一个数据集,其中某人每秒测量一次室外空气温度。常识告诉我们,这种测量过于频繁,相对于额外的数据存储和处理负担,可能提供的新信息非常有限。事实上,测量误差可能与秒与秒之间的空气温度变化一样大。因此,你可能不希望存储这种过多且无信息量的数据。在这种情况下——也就是对定期采样的数据来说——降采样就像是选择每 n 个元素一样简单。
关注季节周期的特定部分
在时间序列中不必担心季节数据,你可以选择创建一个只关注一个季节的子系列。例如,我们可以应用降采样来创建一个子系列,就像在这种情况下,我们从原始的月度时间序列中生成了一组一月份的测量值。在这个过程中,我们将数据降低到了年度频率。
## R
> unemp[seq.int(from = 1, to = nrow(unemp), by = 12)]
DATE UNRATE
1948-01-01 3.4
1949-01-01 4.3
1950-01-01 6.5
1951-01-01 3.7
1952-01-01 3.2
1953-01-01 2.9
1954-01-01 4.9
1955-01-01 4.9
1956-01-01 4.0
1957-01-01 4.2
与低频率数据进行匹配
你可能希望降低数据的频率,以便与其他低频数据匹配。在这种情况下,你可能希望对数据进行聚合或降采样,而不仅仅是丢弃数据点。这可以是简单的平均值或总和,也可以是更复杂的加权平均,后续值具有更高的权重。我们之前在捐赠数据中看到,将一周内的所有捐款总额相加的想法可能更加有趣。
相比之下,对于我们的经济数据,最有可能感兴趣的是年均值。我们使用均值而不是滚动均值,因为我们想要总结一年的情况,而不是获取该年的最新值,强调的是最近性。我们通过将日期格式化为字符串,将其年份作为例子来分组,展示如何可以创造性地利用类似 SQL 的操作进行时间序列功能:
## R
> unemp[, mean(UNRATE), by = format(DATE, "%Y")]
format V1
1948 3.75
1949 6.05
1950 5.21
1951 3.28
1952 3.03
1953 2.93
1954 5.59
1955 4.37
1956 4.13
1957 4.30
上采样
上采样并不简单等同于下采样的逆过程。下采样在现实世界中是有意义的;决定更少频繁地进行测量是简单的。相比之下,上采样可能就像试图白嫖一样——也就是说,不进行实时测量却希望从不频繁的测量中获取高分辨率数据。引用流行的R时间序列包XTS的作者的话:
不可能将低周期性的系列转换为高周期性的系列 - 例如,从每周到每日或每日到 5 分钟柱,因为那需要魔法。
然而,有合理的理由希望以比其默认频率更高的频率标记数据。在这样做时,你需要记住数据的限制。请记住,你只是增加了更多的时间标签,而不是增加了更多信息。
让我们讨论一些上采样有意义的情况。
不规则时间序列
上采样的一个非常常见的原因是你有一个不规则采样的时间序列,你希望将其转换为定期的时间序列。这是一种上采样,因为你将所有数据转换为一个频率,这个频率可能高于数据之间滞后所指示的频率。如果出于这个原因进行上采样,你已经知道如何通过滚动连接来实现,就像我们在填补缺失的经济数据时所做的那样,通过 R:
## R
> all.dates <- seq(from = unemp$DATE[1], to = tail(unemp$DATE, 1),
> by = "months")
> rand.unemp = rand.unemp[J(all.dates), roll=0]
不同频率下的输入采样
有时,你仅仅需要上采样低频信息,以便将其与高频信息一起在需要输入对齐和同时采样的模型中传递。你必须对前瞻性保持警惕,但如果我们假设已知状态在出现新的已知状态之前是真实的,我们可以安全地上采样并传递我们的数据。例如,假设我们知道(相对地)大多数新的工作岗位都是在每月的第一天开始的。我们可能会决定使用给定月份的失业率来代表整个月份(不考虑它是前瞻性,因为我们假设失业率在整个月份内保持稳定)。
## R
> daily.unemployment = unemp[J(all.dates), roll = 31]
> daily.unemployment
DATE UNRATE
1948-01-01 3.4
1948-01-02 3.4
1948-01-03 3.4
1948-01-04 3.4
1948-01-05 3.4
时间序列动态的知识
如果你对变量通常的时间行为有基础知识,你也可能能够将上采样问题视为缺失数据问题。在这种情况下,我们已经讨论过的所有技术仍然适用。插值是产生新数据点的最有可能的方法,但你需要确保你的系统动态能够证明你的插值决策是合理的。
正如前面讨论的,即使在最干净的数据集中,上采样和下采样也会经常发生,因为你几乎总是希望比较不同时间尺度的变量。还应该指出,Pandas 具有特别方便的上采样和下采样功能,使用resample方法。
平滑数据
数据平滑可以出于各种原因进行,尤其是在进行数据可视化之前,常常会对真实世界的时间序列数据进行平滑,以便讲述数据背后的可理解故事。在本节中,我们进一步讨论为什么进行平滑以及最常见的时间序列平滑技术:指数平滑。
平滑的目的
尽管异常值检测是一个独立的主题,如果你有理由认为数据应该平滑,你可以通过移动平均值来消除测量的突变,测量误差或两者都有的情况。即使这些突变是准确的,它们可能也不反映底层过程,并且可能更多地是仪器问题的问题;这就是为什么平滑数据是相当常见的。
平滑数据与填充缺失数据密切相关,因此这里也涉及到一些技术。例如,你可以通过应用滚动均值来平滑数据,无论是否有前瞻,因为这只是计算其平滑值时所用窗口的点相对位置的问题。
当你在平滑数据时,你需要考虑一些问题:
-
为什么要平滑?平滑可以达到多种目的:
数据准备
你的原始数据是否不适用?例如,你可能知道非常高的值不太可能或不符合物理规律,但你需要一种有原则的方法来处理它们。平滑是最直接的解决方案。
特征生成
从数据中取样,无论是关于一个人、图像或其他任何内容的许多特征,然后用几个度量标准对其进行总结。通过这种方式,一个更全面的样本被折叠成几个维度或减少到几个特征。特征生成对机器学习尤为重要。
预测
对某些类型的过程来说,最简单的预测形式是均值回归,你可以通过从平滑特征进行预测来获得这种形式。
可视化
你想要在看似杂乱的散点图中增加一些信号吗?如果是这样,你这样做的意图是什么?
-
平滑或不平滑将如何影响你的结果?
-
你的模型是否假设数据是嘈杂和不相关的,从而使你的平滑可能会影响这种假设?
-
在实时生产模型中是否需要平滑?如果是的话,您需要选择一种不使用前瞻的平滑方法。
-
您是否有一个原则性的平滑方法,或者您将只进行超参数网格搜索?如果是后者,您将如何确保使用时间感知的交叉验证形式,以确保未来的数据不会向过去泄漏?
指数平滑
平滑时,通常不会将所有时间点等同看待。特别是,您可能希望将最近的数据视为信息更多的数据,这种情况下指数平滑是一个不错的选择。与我们之前讨论过的移动平均相比——在那种情况下,每个数据缺失的点可以被补充为其周围点的平均值——指数平滑更加关注时间性,将更近期的点权重更高,而不太近期的点权重则指数级减少(因此得名)。
指数平滑的工作原理如下。对于给定的时间段 t,通过计算来找到系列的平滑值:
时间 t 的平滑值
考虑这如何随时间传播。时间 (t – 1) 的平滑值本身就是同样东西的产物:
因此,我们可以看到在时间 t 的平滑值有一个更复杂的表达式:
数学倾向的读者会注意到我们有以下形式的系列:
实际上,正是因为这种形式,指数移动平均相当容易处理。更多详细信息可以在在线和教科书中广泛找到;参见我的最喜爱的摘要在“更多资源”中。
我将在 Python 中进行平滑演示,因为 Pandas 包含多种平滑选项。平滑选项在 R 中也广泛可用,包括基础 R,以及许多时间序列包中。
虽然我们一直在看美国失业率数据,我们将转向另一个常用的数据集:航空公司乘客数据集(追溯到 Box 和 Jenkins 的著名时间序列书籍并广泛可用)。原始数据集记录了按月分解的数千名航空公司乘客:
## python
>>> air
Date Passengers
0 1949-01 112
1 1949-02 118
2 1949-03 132
3 1949-04 129
4 1949-05 121
5 1949-06 135
6 1949-07 148
7 1949-08 148
8 1949-09 136
9 1949-10 119
10 1949-11 104
我们可以使用各种衰减因子以及应用 Pandas 的 ewma() 函数轻松地平滑乘客的值,如下所示:
## python
>>> air['Smooth.5'] = pd.ewma(air, alpha = .5).Passengers
>>> air['Smooth.9'] = pd.ewma(air, alpha = .9).Passengers
正如我们所见,alpha 参数的水平,也称为平滑系数,影响值更新到其当前值与保留现有平均值信息的程度。alpha 值越高,值就越快地更新到其当前价格。Pandas 接受多个参数,所有这些参数都插入相同的方程,但它们提供了多种方式来思考如何指定指数移动平均数。³
## python
>>> air
Date Passengers Smooth.5 Smooth.9
0 1949-01 112 112.000000 112.000000
1 1949-02 118 116.000000 117.454545
2 1949-03 132 125.142857 130.558559
3 1949-04 129 127.200000 129.155716
4 1949-05 121 124.000000 121.815498
5 1949-06 135 129.587302 133.681562
6 1949-07 148 138.866142 146.568157
7 1949-08 148 143.450980 147.856816
8 1949-09 136 139.718200 137.185682
9 1949-10 119 129.348974 120.818568
10 1949-11 104 116.668295 105.681857
然而,简单的指数平滑在具有长期趋势的数据(用于预测)中表现不佳。霍尔特方法和霍尔特-温特斯平滑是两种应用于具有趋势或趋势和季节性数据的指数平滑方法。
还有许多广泛使用的平滑技术。例如,卡尔曼滤波器通过将时间序列过程建模为已知动态和测量误差的组合来平滑数据。LOESS(局部估计散点平滑)是一种非参数方法,用于局部平滑数据。这些方法及其他方法逐渐提供了更复杂的理解平滑的方式,但计算成本也随之增加。需要注意的是,卡尔曼滤波和 LOESS 都包含了早期和晚期的数据,因此如果使用这些方法,要注意信息向时间向后的泄露,以及它们通常不适合用于准备用于预测应用的数据。
平滑是一种常用的预测形式,当你测试更复杂的方法是否实际上产生成功的预测时,可以使用平滑后的时间序列(无前瞻)作为一种简单的空模型。
季节性数据
数据中的季节性是任何一种频率稳定的重复行为。它可以在同一时间发生多种不同的频率。例如,人类行为倾向于具有每日季节性(每天固定时间吃午餐),每周季节性(星期一类似于其他星期一)和每年季节性(元旦交通量较低)。物理系统也展示了季节性,比如地球绕太阳公转的周期。
辨别和处理季节性是建模过程的一部分。另一方面,它也是一种数据清理的形式,例如经济上重要的美国就业报告。事实上,许多政府统计数据,特别是经济数据,在发布时都会进行非季节性调整。
要了解季节性数据平滑能做什么,我们回到经典的航空公司乘客计数数据集。通过绘图可以快速显示这是高度季节性的数据,但前提是你制作正确的图表。
注意使用 R 的默认图(使用点; 参见图 2-10)与添加参数来指示你想要一条线的区别(参见图 2-11)。
![]()
图 2-10. 从散点图中明显可见数据均值和方差的增加,但我们看不到明显的季节性趋势。
![]()
图 2-11. 线图清楚地显示了季节性。
如果您仅查看默认的 R 图表,可能会忽略数据的季节性特征。希望这种情况不会持续太久,毫无疑问,您还会做其他探索数据的工作,也许是使用自相关图(在第三章中讨论)或其他诊断工具。
人类是习惯的动物。
人类行为数据几乎总是具有某种形式的季节性,即使有几个周期(每小时的模式,每周的模式,夏冬季的模式等)。
散点图确实比线图更清楚地显示了一些信息。我们的数据的方差正在增加,均值也在增加,当我们看到数据点以锥形向外散布时,这一点尤为明显,倾向于向上。这些数据显然具有增长趋势,因此我们可能会对其进行对数转换或差分处理,具体取决于我们模型的需求。这些数据也显然具有增加方差的趋势。在建模章节中,我们将更详细地讨论特定于模型的季节性数据转换,所以这里不再详细说明。
除了线图中关于季节性的证据外,我们还获得了有用的信息。我们了解到季节性的种类。也就是说,我们看到数据不仅仅是季节性的,而且是以乘法方式的季节性。随着总体值的增加,季节波动也增加(可以将这视为从峰值到低谷的大小变化)。
我们可以像这样轻松地将数据分解为其季节性、趋势和剩余部分,只需一行 R 代码即可显示。
## R
> plot(stl(AirPassengers, "periodic"))
根据原始数据,得出的图表看起来非常合理(参见图 2-12)。我们可以想象将季节性、趋势和剩余数据重新组合以获得原始序列。我们还可以看到,这种特定的分解没有考虑到这一系列显示的是乘法季节性而不是加法季节性,因为残差在时间序列的开始和结束时最大。看起来,这种分解将平均季节变异性作为季节性分量的变异性。
![]()
图 2-12. 将原始时间序列分解为季节性组成部分、趋势和残差。请注意每个图表的 y 轴,它们非常不同。请注意,这是每个图表右侧灰色条的原因。这些灰色条的绝对大小相同(以 y 轴单位表示),因此它们的相对不同显示是对不同组成部分不同 y 轴比例的视觉提醒。
要初步了解其工作原理,我们可以查看官方的 R 文档:
季节性分量是通过 LOESS 平滑季节子序列(所有一月值的系列...)找到的。如果s.window = “periodic”,平滑实际上被取均值替代。季节性值被移除,余下的被平滑以找到趋势。总体水平从季节分量中移除并加到趋势分量中。这个过程迭代几次。余数分量是季节加趋势拟合的残差。
早先引入的 LOESS 方法是一种计算量大的平滑数据点的方法,它涉及使用移动窗口根据其邻近点估计每个点的平滑值(我希望你的前瞻警报正在响起!)。
时区
时区本质上是乏味、痛苦且难以正确处理,即使花费大量精力也是如此。这就是为什么你绝不应该使用自己的解决方案。时区自诞生以来就非常复杂,随着个人计算机的出现变得更加复杂。其中有许多原因:
-
时区受政治和社会决策的影响。
-
没有标准的方式在不同语言之间或通过 HTTP 协议传输时区信息。
-
没有单一的协议来命名时区或确定夏令时偏移的开始和结束日期。
-
因夏令时的存在,某些时区一年中会出现两次特定的时间!
大多数语言依赖于底层操作系统获取时区信息。不幸的是,Python 内置的自动时间检索函数datetime.datetime.now()不返回带时区信息的时间戳。部分原因在于设计上的考虑。标准库中的一些决定包括禁止在datetime模块中包含时区信息(因为这些信息经常更改),允许datetime对象既带有时区信息又不带有时区信息。然而,将带有时区和不带有时区的对象进行比较将引发TypeError。
一些博主声称,多数库都假定tzinfo==None。虽然这一说法难以证实,但与大多数经验一致。人们也报告难以 pickle 带有时区标记的对象,因此如果你打算使用 pickling,这也是一个早期需要检查的事项。⁴
让我们看看在 Python 中使用时区的方法。你可能会使用的主要库包括datetime、pytz和dateutil。此外,Pandas 提供了基于后两个库的便捷时区相关功能。
接下来我们将介绍最重要的时区功能。
首先注意,当你从datetime模块检索“现在”时,它并不包含时区信息,尽管它会给出适合你所在时区的正确时间。例如,注意now()和utcnow()的响应差异:
## python
>>> datetime.datetime.utcnow()
datetime.datetime(2018, 5, 31, 14, 49, 43, 187680)
>>> datetime.datetime.now()
datetime.datetime(2018, 5, 31, 10, 49, 59, 984947)
>>> # as we can see, my computer does not return UTC
>>> # even though there is no time zone attached
>>> datetime.datetime.now(datetime.timezone.utc)
datetime.datetime(2018, 5, 31, 14, 51, 35, 601355,
tzinfo=datetime.timezone.utc)
注意,如果我们确实传入一个时区,我们将得到正确的信息,但这不是默认行为。在 Python 中处理时区时,我们创建一个时区对象,例如美国太平洋时间的western时区:
## python
>>> western = pytz.timezone('US/Pacific')
>>> western.zone
'US/Pacific'
然后我们可以使用这些对象来localize一个时区,如下所示:
## python
>>> ## the API supports two ways of building a time zone–aware time,
>>> ## either via 'localize' or to convert a time zone from one locale
>>> ## to another
>>> # here we localize
>>> loc_dt = western.localize(datetime.datetime(2018, 5, 15, 12, 34, 0))
datetime.datetime(2018, 5, 15, 12, 34,
tzinfo=<DstTzInfo 'US/Pacific' PDT-1 day, 17:00:00 DST>)
请注意,然而,直接将时区传递给datetime构造函数通常不会产生我们期望的结果:
## python
>>> london_tz = pytz.timezone('Europe/London')
>>> london_dt = loc_dt.astimezone(london_tz)
>>> london_dt
datetime.datetime(2018, 5, 15, 20, 34,
tzinfo=<DstTzInfo 'Europe/London' BST+1:00:00 DST>)
>>> f = '%Y-%m-%d %H:%M:%S %Z%z'
>>> datetime.datetime(2018, 5, 12, 12, 15, 0,
tzinfo = london_tz).strftime(f)
'2018-05-12 12:15:00 LMT-0001'
>>> ## as highlighted in the pytz documentation using the tzinfo of
>>> ## the datetime.datetime initializer does not always lead to the
>>> ## desired outcome, such as with the London example
>>> ## according to the pytz documentation, this method does lead to
>>> ## the desired results in time zones without daylight savings
这一点非常重要,比如在计算时间差时。以下三个示例中的第一个是要注意的地方:
## python
>>> # generally you want to store data in UTC and convert only when
>>> # generating human-readable output
>>> # you can also do date arithmetic with time zones
>>> event1 = datetime.datetime(2018, 5, 12, 12, 15, 0,
tzinfo = london_tz)
>>> event2 = datetime.datetime(2018, 5, 13, 9, 15, 0,
tzinfo = western)
>>> event2 - event1
>>> ## this will yield the wrong time delta because the time zones
>>> ## haven't been labelled properly
>>> event1 = london_tz.localize(
datetime.datetime(2018, 5, 12, 12, 15, 0))
>>> event2 = western.localize(
datetime.datetime(2018, 5, 13, 9, 15, 0))
>>> event2 - event1
>>> event1 = london_tz.localize(
(datetime.datetime(2018, 5, 12, 12, 15, 0))).
astimezone(datetime.timezone.utc)
>>> event2 = western.localize(
datetime.datetime(2018, 5, 13, 9, 15, 0)).
astimezone(datetime.timezone.utc)
>>> event2 - event1
pytz提供了常见时区和按国家划分的时区列表,这两者都可以作为方便的参考:
## python
## have a look at pytz.common_timezones
>>> pytz.common_timezones
(long output...)
## or country specific
>>> pytz.country_timezones('RU')
['Europe/Kaliningrad', 'Europe/Moscow', 'Europe/Simferopol',
'Europe/Volgograd', 'Europe/Kirov', 'Europe/Astrakhan',
'Europe/Saratov', 'Europe/Ulyanovsk', 'Europe/Samara',
'Asia/Yekaterinburg', 'Asia/Omsk', 'Asia/Novosibirsk',
'Asia/Barnaul', 'Asia/Tomsk', 'Asia/Novokuznetsk',
'Asia/Krasnoyarsk', 'Asia/Irkutsk', 'Asia/Chita',
'Asia/Yakutsk', 'Asia/Khandyga', 'Asia/Vladivostok',
'Asia/Ust-Nera', 'Asia/Magadan', 'Asia/Sakhalin',
'Asia/Srednekolymsk', 'Asia/Kamchatka', 'Asia/Anadyr']
>>>
>>> pytz.country_timezones('fr')
>>> ['Europe/Paris']
特别棘手的问题是夏令时问题。某些人类可读的时间存在两次(在秋季落后),而其他时间根本不存在(在春季提前跳过):
## python
>>> ## time zones
>>> ambig_time = western.localize(
datetime.datetime(2002, 10, 27, 1, 30, 00)).
astimezone(datetime.timezone.utc)
>>> ambig_time_earlier = ambig_time - datetime.timedelta(hours=1)
>>> ambig_time_later = ambig_time + datetime.timedelta(hours=1)
>>> ambig_time_earlier.astimezone(western)
>>> ambig_time.astimezone(western)
>>> ambig_time_later.astimezone(western)
>>> #results in this output
datetime.datetime(2002, 10, 27, 1, 30,
tzinfo=<DstTzInfo 'US/Pacific' PDT-1 day, 17:00:00 DST>)
datetime.datetime(2002, 10, 27, 1, 30,
tzinfo=<DstTzInfo 'US/Pacific' PST-1 day, 16:00:00 STD>)
datetime.datetime(2002, 10, 27, 2, 30,
tzinfo=<DstTzInfo 'US/Pacific' PST-1 day, 16:00:00 STD>)
>>> # notice that the last two timestamps are identical, no good!
>>> ## in this case you need to use is_dst to indicate whether daylight
>>> ## savings is in effect
>>> ambig_time = western.localize(
datetime.datetime(2002, 10, 27, 1, 30, 00), is_dst = True).
astimezone(datetime.timezone.utc)
>>> ambig_time_earlier = ambig_time - datetime.timedelta(hours=1)
>>> ambig_time_later = ambig_time + datetime.timedelta(hours=1)
>>> ambig_time_earlier.astimezone(western)
>>> ambig_time.astimezone(western)
>>> ambig_time_later.astimezone(western)
datetime.datetime(2002, 10, 27, 0, 30,
tzinfo=<DstTzInfo 'US/Pacific' PDT-1 day, 17:00:00 DST>)
datetime.datetime(2002, 10, 27, 1, 30,
tzinfo=<DstTzInfo 'US/Pacific' PDT-1 day, 17:00:00 DST>)
datetime.datetime(2002, 10, 27, 1, 30,
tzinfo=<DstTzInfo 'US/Pacific' PST-1 day, 16:00:00 STD>)
## notice that now we don't have the same time happening twice.
## it may appear that way until you check the offset from UTC
对于你的工作来说,时区问题可能并不重要,因此这些知识的实用性取决于你数据的性质。当然,在某些情况下,如果出错可能会有灾难性后果(比如,在时区变更期间飞行的商业航班生成天气预报,突然发现它们的位置发生了 drastical 改变)。
防止前瞻性
前瞻性在建模流水线中特别容易引入,特别是在使用 R 和 Python 提供的向量化功能数据操作界面时。很容易将变量向错误的方向移动、移动得比预期的多或少,或者以其他方式发现自己得到的数据并非完全“诚实”,因为在你的系统中,你可能在计划拥有数据之前就有了数据。
不幸的是,对于前瞻性,并没有一个明确的统计诊断—毕竟,时间序列分析的整个尝试就是对未知数进行建模。除非系统在某种程度上是确定性的,具有已知的动力学规律,否则很难区分非常好的模型和具有前瞻性的模型—也就是说,直到你将一个模型投入生产,并意识到你计划拥有它的数据时,你是否丢失了数据,或者简单地说,在生产中得到的结果并不反映你在训练过程中看到的情况。
防止这种尴尬的最佳方法是保持持续警惕。每当你进行数据的时间转移、平滑处理、填补数据或者上采样时,都要问自己在某个特定时间点是否能知道一些信息。记住,这不仅仅包括日历时间,还包括真实的时间滞后,以反映某件事情发生后到你的组织获得相关数据之间的延迟时间。例如,如果你的组织仅每周从 Twitter 上获取情感分析数据,你需要在训练和验证数据的分割中包括这种每周周期性。同样,如果你只能每月重新训练你的模型一次,你需要确定随着时间推移哪种模型适用于哪些数据。例如,你不能仅仅为七月份训练一个模型,然后将其应用于七月份进行测试,因为在实际情况下,如果训练需要很长时间,你将来不及准备好那个模型。
这里还有一些其他的想法,可以作为一个通用的检查清单。在规划构建模型时和事后审计过程时,都要记住这些想法:
-
如果你在平滑数据或填充缺失数据,要仔细考虑是否可能通过引入一个前瞻来影响你的结果。不仅要考虑这个问题,还要像之前进行实验一样,看看填补和平滑的效果如何。它们是否看起来是向前看的?如果是,你能否证明使用它们是合理的?(可能不是。)
-
用一个非常小的数据集(只有一个data.table中的几行或者任何数据格式中的几行时间步长)构建你的整个过程。然后,在每个步骤中进行随机抽查,看看是否意外地将任何信息在时间上移动到不适当的位置。
-
对于每一种数据,找出相对于其自身时间戳的滞后时间。例如,如果时间戳是数据“发生”的时间,而不是上传到服务器的时间,你需要知道这一点。数据帧的不同列可能具有不同的滞后时间。为了解决这个问题,你可以根据数据帧自定义滞后时间,或者(更好且更现实的方法)选择最大的滞后时间并应用于所有数据。虽然你不希望过度悲观地影响你的模型,但这是一个很好的起点,之后你可以逐步放松这些过于约束的规则,小心地进行一项一项的调整!
-
使用时间感知的错误(滚动)测试或交叉验证。这将在第十一章中讨论,但要记住,对于时间序列数据,随机化训练与测试数据集是行不通的。你不希望未来的信息泄漏到过去的模型中。
-
有意引入先行查看,并查看您的模型行为。尝试不同程度的先行查看,这样您就有了模型准确性如何变化的概念。如果您有一些先行查看的准确性概念,您就知道没有未来不公平知识的真实模型的上限将会如何。请记住,许多时间序列问题非常困难,因此一个具有先行查看的模型可能看起来很棒,直到您意识到您正在处理的是高噪声/低信号数据集。
-
缓慢添加功能,特别是您可能正在处理的功能,以便您可以查找跳跃。先行查看的一个标志是当某个特定功能表现出乎意料的好,并且没有很好的解释。在您的解释列表的顶部,应该始终是“先行查看”。
注意
处理和清理与时间相关的数据可能是一个繁琐而细致的过程。
在数据清理和处理中引入先行查看是非常危险的!只有当它们是有意的时候,才应该使用先行查看,而这很少是适当的。
更多资源
-
关于缺失数据:
Steffen Moritz 等,《比较 R 中单变量时间序列插补的不同方法》,未发表的研究论文,2015 年 10 月 13 日,链接,https://perma.cc/M4LJ-2DFB。
这份详尽的 2015 年总结概述了在单变量时间序列数据情况下输入时间序列数据的可用方法。单变量时间序列数据是一个特殊的挑战,因为许多先进的缺失数据插补方法依赖于观察协变量之间的分布,而在单变量时间序列情况下这种选择是不可用的。本文总结了各种 R 包的可用性和性能,以及这些方法在各种数据集上的实证结果。
James Honaker 和 Gary King,《关于时间序列横截面数据中缺失值的处理方法》,《美国政治科学杂志》54 卷,2 期(2010 年):561-81,链接,https://perma.cc/8ZLG-SMSX。
本文探讨了在宽面板协变量时间序列中缺失数据的最佳实践。
Léo Belzile,《不规则时间序列和缺失值笔记》,未标明日期,链接,https://perma.cc/8LHP-92FP。
作者提供了处理不规则数据作为缺失数据问题的示例,以及一些常用 R 包的概述。
-
关于时区:
Tom Scott,《时区和时间问题》,Computerphile 视频,2013 年 12 月 30 日,链接,https://oreil.ly/iKHkp。
这段时长 10 分钟、观看量超过 150 万的 YouTube 视频详细描述了处理时区的危险和挑战,特别是在 Web 应用程序的背景下。
Wikipedia,《时区》,链接,https://perma.cc/J6PB-232C。
这是一个简要而迷人的历史,揭示了在上个世纪之前时间的记录方式以及从铁路开始的技术进步如何导致不同地点的人们需要协调他们的时间。还有一些有趣的时区地图。
Declan Butler,“GPS 故障威胁数千台科学仪器”,《自然》杂志,2019 年 4 月 3 日,https://perma.cc/RPT6-AQBC.
本文与时区无直接关系,而是涉及时间戳问题的更普遍问题。它描述了一个最近的问题,即美国全球定位系统中的一个错误可能导致时间戳数据问题,因为它传输了一个从 1980 年 1 月 6 日开始计数的二进制 10 位“周数”。该系统总共只能涵盖 1024 周(2 的 10 次方)。这个计数在 2019 年 4 月第二次达到。未设计以应对此限制的设备将会重置为零,并错误地为科学和工业数据标记时间戳。本文描述了一些关于时间限制与科学设备不足以预见这一问题的困难。
-
关于平滑和季节性:
Rob J. Hyndman 和 George Athanasopoulos,“指数平滑”, 收录于《预测:原理与实践》,第 2 版(墨尔本:OTexts,2018),https://perma.cc/UX4K-2V5N.
Hyndman 和 Athanasopoulos 的介绍性学术教材中的这一章节,涵盖了时间序列数据的指数平滑方法,包括指数平滑的分类以及扩展到预测应用的方法。
David Owen,“启动指数移动平均的正确方式”, Forward Motion 博客,2017 年 1 月 31 日,https://perma.cc/ZPJ4-DJJK.
正如前面的一个侧记所强调的,指数移动平均的概念简单易懂,计算起来也很容易,但是如何“启动”计算则稍显复杂。我们希望确保我们的移动平均能够根据记录信息的时间长短来适应新信息的更新。如果我们在考虑这一点时启动移动平均,即使是一个新的移动平均也会像具有无限回溯的平均值一样行事,从而在应对新信息时不公平地进行折价。有关更多详细信息和计算解决方案,请参阅本博客文章。
Avner Abrami,Aleksandr Arovkin 和 Younghun Kim,“使用指数平滑单元的时间序列”, 未发表研究论文,最后修订于 2017 年 9 月 29 日,https://perma.cc/2JRX-K2JZ.
这是一篇非常易于理解的时间序列研究论文,详细阐述了简单指数平滑的概念,以开发“指数平滑单元”。
-
关于函数数据分析:
简-王,J.-M. Chiou,和 H.-G. Müller,《功能数据分析综述》,《统计学及其应用年度评论》,2015 年,https://perma.cc/3DNT-J9EZ。
本统计文章提供了一种易于理解的数学概述,介绍了重要的功能数据分析技术。文章中还展示了一些有用的可视化技术。
Shahid Ullah 和 Caroline F. Finch,《功能数据分析应用:系统综述》,《BMC 医学研究方法学》,第 13 卷,第 43 期(2013 年),https://perma.cc/VGK5-ZEUX。
本评述采用跨学科的方法,调查了最近发表的使用功能数据分析进行的分析。作者认为功能数据分析技术具有比目前认知更广泛的适用性,并提出生物和健康科学可以通过更多地使用这些技术来查看医学时间序列数据的情况。
¹ 要了解更多关于这种数据分析的信息,还可以参考
² SQL 数据库是使用基于表的数据存储机制的传统数据库。例如,如果您想在 SQL 数据库中存储客户交易记录,您可能会有一个包含客户信息(包括唯一标识符)的表,以及另一个包含交易记录的表,每个交易记录都包含其中一个唯一的客户标识符。
³ 您可以根据自己的喜好指定阿尔法平滑因子、半衰期、跨度或质心。详情请参阅文档。
⁴ Pickling 是将 Python 对象以字节格式存储的过程。这通过流行的内置 pickle 模块完成。大多数情况下,泡菜工作得很好,但有时与时间相关的对象可能难以泡制。
第三章:时间序列的探索性数据分析
我们将把我们对时间序列探索性数据分析的讨论分解为两个不同的部分。首先,我们讨论如何在时间序列上应用常用的数据方法。特别是,我们讨论如何将直方图、绘图和分组操作应用于时间序列数据。
其次,我们强调用于时间序列分析的基本时间方法——即专门针对时间序列数据开发的方法,只在具有时间关系的数据点之间的上下文中才有意义(而不是横截面的)。
熟悉的方法
我们首先考虑如何将常用的数据探索技术应用于时间序列数据集。这个过程与您在非时间序列数据上执行的过程相同。
您需要了解可用的列、它们的值范围以及哪些逻辑单位的测量效果最佳。您需要解答与任何新数据集相关的探索性问题,比如:
要回答这些问题,您可以使用熟悉的技术,如绘图、汇总统计、应用直方图和使用有针对性的散点图。您还需要明确回答与时间相关的问题,比如:
要解决这些问题,您需要之前提到的方法——直方图、散点图和汇总统计——以考虑时间轴。为了实现这种行为,我们将时间纳入到我们的统计中,作为图表中的轴或作为直方图或散点图中的分组操作中的一组。
在本节的其余部分,我们将通过几种不同类型的时间序列数据的探索性数据分析示例来探讨如何直接使用传统的非时间序列方法或带有时间特定修改的方法。为了演示这些探索方法,我们将使用 R 语言中提供的欧洲股票市场数据,这是一个时间序列数据集。
绘图
让我们探索 R 提供的典型时间序列,EuStockMarkets 数据集。为了对其有所了解,我们可以查看数据集的开头:
## R
> head(EuStockMarkets)
DAX SMI CAC FTSE
[1,] 1628.75 1678.1 1772.8 2443.6
[2,] 1613.63 1688.5 1750.5 2460.2
[3,] 1606.51 1678.6 1718.0 2448.2
[4,] 1621.04 1684.1 1708.1 2470.4
[5,] 1618.16 1686.6 1723.1 2484.7
[6,] 1610.61 1671.6 1714.3 2466.8
这是 R 语言中的内置数据集,包含了 1991 年至 1998 年期间四个主要欧洲股票指数的每日收盘价格。数据集仅包括工作日。
这个数据集比我们迄今为止看过的更好准备,因为它已经很好地格式化并对我们进行了取样。我们不需要担心缺失值、时区或不正确的测量,因此我们可以直接进行探索性数据分析。
我们查看这些数据的第一步将与非基于时间的时间序列非常相似,尽管我们在时间数据中有一个比在横截面数据中更简单的选项。我们可以单独绘制每个值,这样的绘图是有意义的。与横截面数据相对比,那里任何个体特征与数据集中的索引的绘图将只显示数据输入或从 SQL 服务器返回的顺序(换句话说,是任意顺序),但没有显示底层情况。然而,在时间序列的情况下,绘图非常具有信息性,就像您在图 3-1 中看到的那样。
## R
> plot(EuStockMarkets)
![]()
图 3-1. 时间序列数据的简单绘图。
注意,图像会通过简单的plot()命令自动分割成不同的时间序列。事实上,正如这里展示的,我们正在使用一个 R 中的mts对象(如果只有一个时间序列,我们将使用一个ts对象):
## R
> class(EuStockMarkets)
[1] "mts" "ts" "matrix"
许多流行的包大量使用ts对象和其衍生类。这些对象配备了适当绘图函数的自动调用,正如我们在前面的例子中看到的,一个简单的plot()调用创建了一个标记齐全的多面板图。ts对象还有一些便利函数:
-
frequency用于查找数据的年度频率:
## R
> frequency(EuStockMarkets)
[1] 260
-
start和end用于找出系列中表示的第一个和最后一个时间:
## R
> start(EuStockMarkets)
[1] 1991 130
> end(EuStockMarkets)
1] 1998 169
-
window用于获取数据的时间段:
## R
> window(EuStockMarkets, start = 1997, end = 1998)
Time Series:
Start = c(1997, 1)
End = c(1998, 1)
Frequency = 260
DAX SMI CAC FTSE
1997.000 2844.09 3869.8 2289.6 4092.5
1997.004 2844.09 3869.8 2289.6 4092.5
1997.008 2844.09 3869.8 2303.8 4092.5
...
1997.988 4162.92 6115.1 2894.5 5168.3
1997.992 4055.35 5989.9 2822.9 5020.2
1997.996 4125.54 6049.3 2869.7 5018.2
1998.000 4132.79 6044.7 2858.1 5049.8
ts类有其优缺点。如前所述,ts及其衍生类在许多时间序列包中被广泛使用。此外,自动设置绘图参数可能会有所帮助。然而,索引有时可能会比较棘手,并且使用window访问数据的子段随着时间推移可能会感到不便。在本书中,您将看到多种包含和访问时间序列数据的方法,您可以根据自己的使用情况选择最方便的方式。
直方图
让我们继续比较时间序列和非时间序列数据的探索性数据分析。例如,我们想要对数据进行直方图分析,就像大多数探索性数据分析一样。我们通过对差分数据进行直方图分析,引入了一个新的问题,因为我们想要使用我们的时间轴(参见图 3-2):
## R
> hist( EuStockMarkets[, "SMI"], 30)
> hist(diff(EuStockMarkets[, "SMI"], 30))
![]()
图 3-2. 未转换数据的直方图(顶部)极其宽阔,不显示正态分布。考虑到底层数据存在趋势,这是可以预料的。我们对数据进行差分以消除趋势,这将使数据转变为更接近正态分布的形式(底部)。
在时间序列的背景下,数据差的hist()通常比未转换数据的hist()更有趣。毕竟,在时间序列的背景下,通常(特别是在金融领域)最有趣的是一个值如何从一次测量到下一次测量的变化,而不是这个值的实际测量。这在绘图中尤为明显,因为对具有趋势的数据进行直方图统计不会产生非常信息丰富的可视化效果。
注意我们从差分系列的直方图中获得的新信息。虽然前一节中股票的原始图表描绘了股票不断上涨的非常乐观的经济图景,但这个直方图向我们展示了跟随股票的日常体验。差分的直方图告诉我们,时间序列的值随时间既上升(正差分值),又下降(负差分值),而且大致相同。股票指数并没有随时间完全同样的上升和下降,因为我们知道股票指数确实有一个增长趋势。然而,从这个直方图中我们可以看到,稍微偏向正差分而不是负差分正是导致这种趋势的原因。
这是为什么我们在抽样、总结和问数据问题时需要关注时间尺度的一个很好的例子。无论是表现看起来很好(长期图)还是一般般(差分图的直方图),都取决于我们的时间尺度:我们关心日常吗,还是我们有更长期的视角?如果我们在一个以年度盈利为目标的机构工作,我们需要考虑每次向上司报告倾向于负值的“差分”的短期经历。然而,如果我们是一个大型机构投资者——也许是大学或医院——我们可能负担得起采取长期视角,预期上升。后一种情况也有其挑战:我们如何花足够长的时间来最大化利润,但不至于失去机会?这些问题使得金融行业与时间序列研究和预测息息相关。
散点图
使用散点图的传统方法对时间序列数据与其他类型的数据同样有用。我们可以使用散点图来确定两只股票在特定时间点的联系以及它们的价格变动如何随时间相关。
在这个例子中,我们绘制了两种情况(见图 3-3):
## R
> plot( EuStockMarkets[, "SMI"], EuStockMarkets[, "DAX"])
> plot(`diff`(EuStockMarkets[, "SMI"]), `diff`(EuStockMarkets[, "DAX"]))
![]()
图 3-3. 两个股票指数的简单散点图显示了强烈的相关性。然而,我们有理由对这些图表持怀疑态度。
正如我们已经看到的,实际值比相邻时间点之间的差异信息更少,因此我们在第二个散点图中绘制了这些差异。这些看起来像是非常强的相关性,但实际关系并不像它们看起来那么强。(要深入了解,请跳到 “伪相关性” 一节。)
图 3-3 中的明显相关性很有趣,但即使它们是真实的相关性(但有理由怀疑它们是否是),这些也不是我们作为股票交易员可以货币化的相关性。当我们知道一只股票是涨还是跌时,与之相关的股票也已经涨或跌,因为我们是在相同时间点的值之间进行相关性分析。我们需要做的是找出一只股票之前的变化是否能预测另一只股票之后的变化。为此,在查看散点图之前,我们将一个股票的差异向后移动 1 步。仔细阅读以下代码;请注意,我们仍在进行差异化处理,但现在我们还在一个差分时间序列上应用了滞后(参见 图 3-4):
## R
> plot(`lag`(diff(EuStockMarkets[, "SMI"]), 1),
diff(EuStockMarkets[, "DAX"]))
提示
这些代码示例中的代码行易于阅读,因为每个部分都是对齐的。在编写长行的难以阅读的代码时,特别是在像 R 或 Python 这样的函数式编程语言中,这可能是诱人的,但尽量避免!
![]()
图 3-4。在引入时间滞后后,股票之间的相关性消失了,这表明 SMI 似乎不能预测 DAX。
这个结果告诉我们一些重要的事情:
-
对于时间序列数据,虽然我们可能会使用与非时间序列数据相同的探索技术,但盲目应用是行不通的。我们需要考虑如何利用相同的技术,但应用在重塑后的数据上。
-
往往是不同时间点数据之间的关系或随时间变化的变化最能说明你的数据行为方式。
-
图 3-4 显示了为什么成为股票交易员可能会很困难。如果你是一名被动投资者并且耐心等待,你可能会从长期上升的趋势中受益。然而,如果你试图预测未来,你会发现这并不容易!
警告
R 的 lag() 函数可能不会按照你预期的时间方向移动数据。 lag() 函数是向前的时间移动。记住这一点很重要,因为在特定用例中,你不会希望将数据移动到错误的时间方向,不同的用例可能需要前向和后向的时间移动。
时间序列特定的探索方法
分析时间序列数据的几种方法侧重于同一系列不同时间点的值之间的关系,如果你之前没有处理过时间序列数据,你可能以前没见过这些。在本章的其余部分,我们将介绍一些用于分类时间序列的概念和相关技术。
我们将探讨的概念包括:
稳定性
时间序列平稳的含义及其统计检验
自相关
说一个时间序列与自身相关意味着什么,以及这样的相关性对时间序列底层动态的指示
虚假相关性
一个相关性是虚假的意味着什么,以及你应该预期何时会遇到虚假相关性
我们将学习应用的方法包括:
我们将按照从稳定性到自相关再到虚假相关性的顺序涵盖这些概念及其相应的方法。在我们深入具体内容之前,让我们讨论一下这种特定顺序背后的逻辑。
你可能会首先询问关于时间序列的一个问题,即它是否表现出反映一个“稳定”的系统还是一个不断变化的系统。稳定性的水平,或平稳性,是评估重要的,因为我们需要知道系统的长期过去行为如何反映其长期未来行为。一旦我们评估了时间序列的“稳定性”(这里并非以技术意义使用此词),我们试图确定是否存在该序列的内部动态(例如季节性变化)。也就是说,我们在寻找自相关,回答如何预测未来数据的基本问题,无论是远距离还是近距离的过去数据。最后,当我们认为已经找到了系统内部的某些行为动态时,我们需要确保我们不是基于根本不暗示我们希望发现的因果关系的动态来识别关系;因此,我们必须寻找虚假相关性。
理解稳定性
许多传统的统计时间序列模型依赖于时间序列的平稳性。一般来说,一个平稳的时间序列是指其统计性质随时间变化相对稳定,特别是在均值和方差方面。这看起来相对直观。
尽管如此,稳定性可能是一个棘手的概念,特别是在应用于实时序列数据时。这既太直觉了,也太容易让你依赖自己的直觉。在讨论稳定性的一般测试和如何应用这一概念的实际细节之前,我们将直观地和带有一定正式定义地讨论这个概念。
直觉
静止时间序列是指时间序列测量反映系统处于稳定状态。有时很难确定这究竟意味着什么,可以更容易地排除不是静止的东西,而不是说某些东西是静止的。一个明显的非静止数据的例子是我们在第二章中检查的航空乘客数据集,如图 3-5 中所示(提醒一下,在 R 中可以作为AirPassengers使用,并且广泛可供下载)。
有几个特征显示这个过程并不是静止的。首先,均值随时间增长而不是保持稳定。其次,每年的高峰和低谷之间的距离增大,因此过程的方差随时间增加。第三,该过程显示出强烈的季节性行为,与静止相反。
![]()
图 3-5. 航空乘客数据集是一个明显的非静止时间序列的典型示例。随着时间的推移,数据的均值和方差都在变化。我们还看到了季节性的证据,这本质上反映了一个非静止的过程。
静止性定义和增广迪基-富勒检验
一个静止过程的简单定义如下:如果对于所有可能的滞后k,y[t],y[t+1],...,y[t+k]的分布不依赖于t,则过程是静止的。
针对静止性的统计检验通常归结为一个问题:过程的特征方程是否存在单位根——即 1 是否是过程特征方程的解。[¹] 如果存在单位根,则线性时间序列是非静止的,尽管缺乏单位根并不证明静止性。解决静止性作为一个一般性问题仍然棘手,确定一个过程是否具有单位根仍然是当前的研究领域。
尽管如此,从随机游走的例子中可以得出单位根的简单直觉:
在这个过程中,给定时间点上的时间序列值是其前一个时间点值和一些随机误差的函数。如果ϕ等于 1,该序列具有单位根,将“逃跑”,并且不会是静止的。有趣的是,序列不是静止的并不意味着它必须具有趋势。随机游走是一个非静止时间序列的良好示例,没有潜在趋势。[²]
用于确定过程是否平稳的检验被称为假设检验。增广迪基-富勒(ADF)检验是评估时间序列平稳性问题最常用的指标。该检验提出一个空假设,即时间序列中存在单位根。根据检验结果,可以在指定的显著性水平上拒绝该空假设,这意味着在给定显著性水平下可以拒绝单位根的存在。
请注意,平稳性检验侧重于系列的均值是否变化。方差通过变换而非正式检验来处理。因此,系列是否平稳的测试实际上是对系列是否集成的测试。阶数为d的集成系列是一种必须差分d次才能变得平稳的系列。
迪基-富勒检验的框架如下:
然后,测试ϕ = 1 是否一个简单的t检验,检验滞后y[t–1]上的参数是否等于 0。ADF 检验的不同之处在于考虑更多的滞后,使得底层模型考虑更高阶的动态效应,可以写成一系列差分滞后:
这需要更多的代数来写成一系列差分滞后,并且用于测试空假设的期望分布与原始迪基-富勒检验有所不同。ADF 检验是时间序列文献中最广泛展示的平稳性检验。
不幸的是,由于多种原因,这些检验远非你解决平稳性问题的灵丹妙药:
设定替代空假设:KPSS 检验
虽然 ADF 检验假设存在单位根的空假设,但 Kwiatkowski-Phillips-Schmidt-Shin(KPSS)检验假设一个平稳过程的空假设。与 ADF 不同,KPSS 不在基础 R 中可用,但仍然广泛实施。关于这些检验的用途和正确使用方式存在一些细微差别;这些内容超出本文范围,但在网络上广泛讨论。
在实践中
在实践中,平稳性对多种原因至关重要。首先,大量模型假设一个平稳过程,如具有已知强度和统计模型的传统模型。我们将在第六章中涵盖这些模型类。
更广义的一点是,非平稳时间序列模型在时间序列的度量指标变化时精度也会变化。也就是说,如果你正在寻找一个模型来帮助你估计一个具有非平稳均值和方差的时间序列的均值,那么你的模型中的偏差和误差将随时间变化,此时你的模型的价值变得值得怀疑。
通常情况下,一个时间序列可以通过几个简单的变换使其足够平稳。对数变换和平方根变换是两种流行的选择,特别是在时间上变化方差的情况下。同样地,通过差分来去除趋势是最常见的做法。有时候一个序列必须进行多次差分。然而,如果你发现自己进行了过多的差分(超过两三次),那么用差分来解决你的平稳性问题是不太可能的。
对数还是平方根?
虽然平方根通常比对数计算复杂性低,但你应该探索两种选择。考虑你的数据范围以及你希望如何压缩大值,而不是过早优化(即过度悲观化)你的代码和分析。
平稳性不是预测模型所做的唯一假设。另一个常见但不同的假设是输入变量或预测变量的正态分布。在这种情况下,可能需要其他变换。一个常见的变换是 Box Cox 变换,在 R 的forecast包和 Python 的scipy.stats中实现。该变换使非正态分布的数据(偏斜数据)更接近正态分布。然而,仅仅因为你可以对数据进行变换并不意味着你应该这样做。在转换之前,请仔细考虑原始数据集中数据点之间距离的含义,并确保无论任务如何,变换都能保留最重要的信息。
应用窗口函数
让我们回顾一下最重要的时间序列探索图表,这些图表可能在大多数初始时间序列分析中使用。
滚动窗口
一个特定于时间序列的常见函数是窗口函数,它是任何一种在聚合数据时压缩数据(正如我们在上一章中看到的下采样的情况)或平滑数据(也在第二章中讨论过)的函数。除了已经讨论过的应用外,平滑数据和窗口聚合数据可用于信息丰富的探索性可视化。
我们可以使用基于 R 的filter()函数计算移动平均线和其他涉及一系列点的线性函数的计算,如下所示:
## R
> ## calculate a rolling average using the base R
> ## filter function
> x <- rnorm(n = 100, mean = 0, sd = 10) + 1:100
> mn <- function(n) rep(1/n, n)
> plot(x, type = 'l', lwd = 1)
> lines(filter(x, mn( 5)), col = 2, lwd = 3, lty = 2)
> lines(filter(x, mn(50)), col = 3, lwd = 3, lty = 3)
此代码生成了图表图 3-6。
![]()
图 3-6. 通过滚动均值平滑生成的两条探索曲线。我们可以使用这些曲线来寻找特别嘈杂数据中的趋势,或者决定哪些线性行为偏离是有趣的研究对象,而哪些可能只是噪音。
如果我们在寻找不是窗口中所有点的线性组合的函数,则无法使用filter(),因为它依赖于数据的线性变换。但是,我们可以使用zoo。zoo包中的rollapply()函数非常方便(见图 3-7):
## R
> ## you can also do more 'custom' functionality
> require(zoo)
> f1 <- rollapply(zoo(x), 20, function(w) min(w),
> align = "left", partial = TRUE)
> f2 <- rollapply(zoo(x), 20, function(w) min(w),
> align = "right", partial = TRUE)
> plot (x, lwd = 1, type = 'l')
> lines(f1, col = 2, lwd = 3, lty = 2)
> lines(f2, col = 3, lwd = 3, lty = 3)
![]()
图 3-7. 滚动窗口最小值,左对齐(长划线)或右对齐(短划线)。左对齐可以看到未来的事件,而右对齐只能看到过去的事件。这一点很重要,避免了预先看看。然而,有时左对齐可以用来提出探索性问题,比如“如果我事先知道这个,那么它会有用吗?”有时即使预先看也不具信息性,这意味着特定变量不具信息性。当了解一个度量的未来对于帮助无济于事时,在时间序列的任何时间中,这个度量可能都不是有用的度量。
警告
在zoo函数中使用zoo对象。如果直接将数值向量传递给rollapply(),则align参数将不会生效。您可以通过删除前面代码中对x的zoo()包装来确认这一点。您会发现这两条曲线是相同的,实际上这是一个悄无声息的失败,在时间序列分析中尤为危险,因为它可能引入意外的预看。这正是为什么您需要经常进行理性检查的例子,即使在探索性数据分析中也是如此。不幸的是,悄无声息的失败在许多流行的 R 包和其他脚本语言中并不罕见,因此保持警惕!
也有可能“自己动手”实现这种功能,这样做可以限制依赖。在这种情况下,我强烈建议从现有广泛使用的包(例如zoo)的源代码开始。因为即使对于单变量时间序列,也有许多意外情况需要考虑,比如如何处理NA以及如何处理系列的开始和结束,这些地方点数可能少于窗口指定的大小。
扩展窗口
扩展窗口在时间序列分析中使用较少,与滚动窗口相比,因其适用范围更为有限。扩展窗口仅在你估计一个稳定过程而非随时间演变或振荡显著的总结统计量时才有意义。扩展窗口从给定的最小尺寸开始,但随着时间序列的进行,窗口会逐渐扩展,包括直到特定时间的每个数据点,而不是保持一个有限且恒定的大小。
扩展窗口在随时间推移中提供了对测试统计量更大的确定性,使您能够从深入到特定时间序列中受益。但是,它仅在您假设您的基础系统是静止的情况下才有效。它可以帮助您保持“在线”摘要统计,就像在收集更多信息时实时估计它们一样。
如果您查看基础 R,您会意识到许多函数已存在作为扩展窗口的实现,例如cummax和cummin。您还可以轻松重新使用cumsum来创建累积均值。在下图中,我们展示了扩展窗口max和扩展窗口mean(参见图 3-8):
## R
> # expanding windows
> plot(x, type = 'l', lwd = 1)
> lines(cummax(x), col = 2, lwd = 3, lty = 2) # max
> lines(cumsum(x)/1:length(x), col = 3, lwd = 3, lty = 3) # mean
![]()
图 3-8. 扩展窗口“max”(长划线)和扩展窗口“mean”(短划线)。使用扩展窗口意味着最大值始终反映到那时的全局最大值,使其成为单调函数。由于扩展窗口的长“记忆”,扩展窗口均值比滚动均值低(参见图 3-7),因为扩展均值中的基础趋势不那么显著。这是好是坏还是中立,取决于我们的假设和对基础系统的知识。
如果您需要带有滚动窗口的自定义函数,您可以使用rollapply(),就像我们为滚动窗口所做的那样。在这种情况下,您需要指定一系列窗口大小而不是单个标量。运行以下代码将生成与图 3-8 相同的图,但这次是使用rollapply()而不是内置 R 函数创建的:
## R
> plot(x, type = 'l', lwd = 1)
> lines(rollapply(zoo(x), seq_along(x), function(w) max(w),
> partial = TRUE, align = "right"),
> col = 2, lwd = 3, lty = 2)
> lines(rollapply(zoo(x), seq_along(x), function(w) mean(w),
> partial = TRUE, align = "right"),
> col = 3, lwd = 3, lty = 3)
自定义滚动函数
我们只关心是否有可能应用自定义滚动函数。在实践中,这是您在分析已知具有已知基本行为规律或必要用于正确分析的有用启发式的时间序列领域时可能看到的内容。
例如,我们可能正在查看包含特定特征的窗口,这在领域知识的基础上具有信息意义。我们可能希望知道我们处于单调(比如,血糖上升)还是上下波动的情况,这表明是仪器噪声而不是趋势。我们可以为这种情况编写自定义函数,并使用移动或扩展窗口应用它。
理解和识别自相关
在其最基本的层面上,时间序列的自相关是指时间序列中的一个值在某一时间点可能与另一个时间点的值相关。请注意,“自相关”在此处是非正式使用,用于描述一个概念而非技术性描述。
举个自相关的例子,如果您拿年度时间序列的每日温度数据来说,您可能会发现,将每年的 5 月 15 日与每年的 8 月 15 日进行比较,会得到一些相关性,例如较热的 5 月 15 日倾向于与较热的 8 月 15 日相关联(或者倾向于与较冷的 8 月 15 日相关联)。这可能让您觉得已经学到了关于温度系统的潜在有趣事实,表明存在一定的长期可预测性。另一方面,您可能会发现相关性接近于零,这种情况下,您也会发现一些有趣的事实,即仅仅知道 5 月 15 日的温度并不能为您提供关于 8 月 15 日温度范围的任何信息。这就是自相关在轶事中的要点。
从这个简单的例子出发,我们将扩展到自相关,它通过不锚定到特定时间点来推广自相关。特别是,自相关提出了更一般的问题,即在特定时间序列中,任意两个点之间是否存在固定距离的相关性。接下来我们将更详细地讨论这一点,以及对偏自相关的最终阐述。
自相关函数
我们从Wikipedia对自相关的优秀定义开始:
自相关,也称为串行相关,是信号与延迟副本之间的相关性,其延迟作为延迟函数。非正式地说,它是观察之间的相似性,作为它们之间时间滞后的函数。
让我们用更简单的英语来解释一下。自相关告诉您不同时间点的数据点如何作为它们时间差的函数而线性相关。
自相关函数(ACF)可以通过绘图直观理解。我们可以在 R 中轻松绘制它(参见图 3-9):
## R
> x <- 1:100
> y <- sin(x × pi /3)
> plot(y, type = "b")
> acf(y)
![]()
图 3-9. 正弦函数及其自相关函数的绘图。
从 ACF 中,我们可以看到,时间滞后为 0 的点之间的相关性为 1(对于每个时间序列都是如此),而滞后 1 的点之间的相关性为 0.5。滞后 2 的点之间的相关性为–0.5,依此类推。
计算 ACF 非常简单。我们可以使用data.table的shift()函数自己做到这一点:
## R
> cor(y, shift(y, 1), use = "pairwise.complete.obs")
[1] 0.5000015
> cor(y, shift(y, 2), use = "pairwise.complete.obs")
[1] -0.5003747
我们的计算与图 3-9 中的图形结果大致匹配。虽然可以使用自定义代码直接计算 ACF,但通常最好使用预先定义的版本,例如 R 的acf函数。这样做有几个优点:
就数学角度而言,关于 ACF 有几个重要事实:
-
周期函数的 ACF 具有与原始过程相同的周期性。 您可以在前述正弦示例图中看到这一点。
-
周期函数的总和的自相关是每个函数单独自相关的总和。 您可以使用一些简单的代码轻松地制定一个示例。
-
所有时间序列在滞后 0 处的自相关系数为 1。
-
白噪声样本的自相关在除了 0 之外的所有滞后处大约为 0。
-
ACF 关于负滞后和正滞后对称,因此只需明确考虑正滞后。您可以尝试绘制手动计算的 ACF 来证明这一点。
-
确定显著非零 ACF 估计的统计规则由“临界区域”给出,其边界为+/–1.96 × sqrt(n)。 此规则依赖于足够大的样本量和过程的有限方差。
偏自相关函数
偏自相关函数(PACF)比 ACF 更难理解。给定滞后的时间序列的偏自相关是在两个时间点之间的所有信息下,时间序列与其自身在该滞后处的偏相关。
在这里点头表示认同听起来很合理。但是,准确来说,什么是在两个时间点之间考虑信息?这意味着您需要计算多个条件相关并从总相关中减去这些。计算 PACF 并非易事,并且有多种估计方法。我们这里不讨论这些,但您可以在相关的 R 和 Python 文档中找到讨论。
PACF 在图形上比概念上更容易理解。 它在图表中的实用性也比讨论中更为明显(见图 3-10):
## R
> y <- sin(x × pi /3)
> plot(y[1:30], type = "b")
> pacf(y)
![]()
图 3-10。 季节性无噪声过程的绘图和 PACF。
对于正弦级数的情况,PACF 与 ACF 形成鲜明对比。 PACF 显示哪些数据点是信息丰富的,哪些是较短时间周期的谐波。
对于季节性且无噪声的过程,如正弦函数,以周期 T 为周期,将在 T、2T、3T 等处看到相同的 ACF 值,直到无限大。 ACF 未能消除这些冗余相关性。 另一方面,PACF 显示了哪些相关性是特定滞后的“真实”信息相关性,而不是冗余性。 这对于知道我们何时收集了足够的信息以获得足够长的窗口在数据的适当时间尺度上是非常宝贵的。
PACF 的临界区域与 ACF 相同。 临界区域在处有界。 任何计算的 PACF 值落在临界区域内的滞后实际上为零。
到目前为止,我们只看了完全无噪声的单频过程示例。现在我们看一个稍微复杂的例子。我们将考虑无噪声、低噪声和高噪声条件下两个正弦曲线的求和。
首先,让我们查看无噪声的情况下各自的图表(见图 3-11):
## R
> y1 <- sin(x × pi /3)
> plot(y1, type = "b")
> acf (y1)
> pacf(y1)
> y2 <- sin(x × pi /10)
> plot(y2, type = "b")
> acf (y2)
> pacf(y2)
提示
静止数据的自相关函数(ACF)应快速下降至零。对于非静止数据,滞后 1 的值为正且较大。
![]()
图 3-11. 两个正弦函数、它们的自相关函数(ACF)和偏自相关函数(PACF)的图表。
我们通过求和来结合这两个系列,并为求和系列创建相同的图表(见图 3-12):
## R
> y <- y1 + y2
> plot(y, type = "b")
> acf (y)
> pacf(y)
正如我们所见,我们的 ACF 图与前述特性一致;两个周期系列的求和的 ACF 是各自 ACF 的和。您可以通过注意到 ACF 的正→负→正→负部分来清楚地看到这一点,这些部分与更慢振荡的 ACF 相关。在这些波浪中,您可以看到更高频 ACF 的快速波动。
![]()
图 3-12. 两个正弦系列的求和的图表、自相关函数(ACF)和偏自相关函数(PACF)。
PACF 不是各个分量的 PACF 函数的简单求和。一旦计算出来,PACF 就足够简单理解,但生成或预测起来并不容易。该 PACF 表明,与原始系列中的任一点相比,分离由某个滞后分隔的点的相关性,考虑它们之间点的值时,在求和系列中更具信息性。这与系列的两个不同周期有关,这导致给定点不太由相邻点的值确定,因为现在两个周期的振荡在不同频率下继续。
让我们看看相同情况,但带有更多噪音(见图 3-13):
## R
> noise1 <- rnorm(100, sd = 0.05)
> noise2 <- rnorm(100, sd = 0.05)
> y1 <- y1 + noise1
> y2 <- y2 + noise2
> y <- y1 + y2
> plot(y1, type = 'b')
> acf (y1)
> pacf(y1)
> plot(y2, type = 'b')
> acf (y2)
> pacf(y2)
> plot(y, type = 'b')
> acf (y)
> pacf(y)
![]()
图 3-13. 两个带有噪声的正弦过程及其求和的图表、自相关函数(ACF)和偏自相关函数(PACF)。
最后,我们给时间序列添加更多噪声,使得初始数据本身甚至不看起来特别像正弦波。 (我们省略了代码示例,因为它与前一个示例相同,只是rnorm的sd参数更大。)我们可以看到,这增加了进一步的解释困难,特别是对 PACF。图 3-14 中的图与之前的图唯一的区别是噪声变量的sd值更大。
![]()
图 3-14. 两个正弦过程非常嘈杂的求和的图表、自相关函数(ACF)和偏自相关函数(PACF)。
让我们以一个真实数据集的 ACF 和 PACF 来总结。在下面,我们检查AirPassengers数据。根据我们到目前为止看到的内容,思考一下为什么 ACF 有这么多“关键”值(答案:它有趋势),以及为什么 PACF 对于一个大滞后有关键值(答案:年度季节循环,即使数据中有趋势也是可以识别的)。
![图示]()
图 3-16. 对 AirPassengers 数据的自相关函数(ACF)和偏自相关函数(PACF)绘图。这里的滞后值不是单位数字,因为滞后以年的分数形式表示。这是因为 AirPassengers 数据集采用了一个 ts 对象的形式,该对象具有内置频率用于绘图(及其他目的)。
伪相关性
那些刚接触时间序列分析的人通常会从标准的探索性数据实践开始,例如将两个变量绘图并计算它们的相关性。新的分析师在数据探索过程的早期会非常兴奋,当他们注意到一个看似非常强的相关性和有益的关系时。他们会继续寻找其他令人惊讶的高相关性;多么令人惊叹的系统!他们会希望自己早些在职业生涯中开始处理时间序列数据。他们会对自己想:“我只是赢了。”
然后分析师将会稍微阅读一下时间序列分析(最佳情况),或者将他们的发现展示给其他人,并意识到并不是所有事情都说得通(并非最佳情况)。一个怀疑论者会指出,相关性太高了。似乎任何两个值都有关联。当分析师使用其他变量集重新运行分析时,会出现更多问题,并发现它们也有令人惊讶的高相关性。在某个时候,会清楚地意识到不可能有这么多真正的高相关性。
这一轨迹与计量经济学早期历史非常相似。19 世纪,当经济学家首次开始思考商业周期的概念时,其中一些人寻找了周期的外部驱动因素,比如太阳黑子(一个 11 年周期)或各种气象周期(比如假设的 4 年降水周期)。他们总是得到非常积极且强相关的结果,即使他们没有因果假设来解释这些结果。
许多经济学家和统计学家对此持怀疑态度,这是正当的。乌德尼·尤尔(Udny Yule)通过一篇题为“为什么有时我们会得到无意义的相关性?”的论文正式调查了这个问题,从而开创了一个研究领域,并继续为学术界带来麻烦和乐趣。虚假相关性仍然是一个需要警惕的重要问题,在诉讼情境中尤为热议,其中一方断言存在关联,而另一方试图驳斥。类似地,一种质疑气候变化数据的尝试依赖于一个论点,即增加的碳排放和全球变暖之间的相关性是由两个数据集的趋势造成的虚假相关性(我认为这一论点并不令人信服)。
经济学家经过时间的学习已经了解到,具有基础趋势的数据可能会产生虚假相关性。以下是一个简单的思考方式:趋势时间序列中包含的信息比静止时间序列中的信息更多,因此数据点之间有更多的机会同时变动。
除了趋势之外,时间序列的一些其他常见特征也可能引入虚假相关性:
-
季节性——例如,考虑热狗消费与溺水死亡之间的虚假相关性(夏季)。
-
数据由于时间的制度变化而产生的水平或斜率变化(产生类似哑铃形的分布,其中高相关性毫无意义)。
-
累积求和数量(这是某些行业用来使模型或相关性看起来比实际更好的技巧)。
有一个众所周知的博客(现在也有一本书),充满了关于虚假相关性的精彩例子,我在图 3-17 中分享了其中一个。每当您试图认为您发现了一个特别强的关系时,请务必检查数据是否存在明显的问题原因,比如趋势。
![]()
图 3-17。一些虚假相关性看起来出奇地令人信服。此图摘自Tyler Vigen 的网站,展示了虚假相关性。
一些有用的可视化
图表对于时间序列的彻底探索性分析至关重要。您肯定希望通过时间轴来可视化数据,最好以一种能够回答数据集中您关心的一般问题的方式,比如特定变量的行为或数据点的整体时间分布。
本章早些时候,我们探讨了一些对任何数据分析师都很熟悉的绘图技术,例如根据时间绘制值的图表或随时间散点绘制不同列的值的图表。在探索性数据分析的最后一节中,我们讨论了几种特别有助于提供关于时间序列行为的新见解的可视化方法。
我们将看一些复杂程度不同的可视化:
-
一维可视化,以理解我们在第二章中组装的时间序列的整体时间分布
-
一个二维直方图,用于理解在许多并行测量(例如测量许多年或同一现象的许多时间序列)的情况下,值随时间的典型轨迹。
-
三维可视化中,时间可以占据多达两个维度或一个维度,但仍然可以隐含地存在
一维可视化
在许多单位测量的情况下(许多用户、成员等),我们并行考虑多个时间序列。在视觉上堆叠这些序列可能会很有趣,强调个体分析单位及其各自的时间框架。我们忽略测量的值,而是将数据存在的时间范围视为感兴趣的信息。时间跨度本身成为分析单位。在这里,我们使用了 R 语言的timevis包,但还有许多其他选择。我们查看了我们在第二章中准备的donations数据的一个小子集(参见图 3-18):
## R
> require(timevis)
> donations <- fread("donations.csv")
> d <- donations[, .(min(timestamp), max(timestamp)), user]
> names(d) <- c("content", "start", "end")
> d <- d[start != end]
> timevis(d[sample(1:nrow(d), 20)])
![]()
图 3-18. 一个随机样本的甘特图可以提供一些关于用户/捐赠者“活跃”时间段分布的想法。
图 3-18 中的图表帮助我们看到,全球会员群体可能有“繁忙”的时期。我们还从中获得了一些关于会员组织中个体捐赠时间跨度分布的感觉。
甘特图已经被使用了一个多世纪,最常见的是用于项目管理任务。它们在许多不同的行业中独立产生,一旦看到它们,这个概念就显而易见。尽管起源于项目管理,甘特图在时间序列分析中也很有用,其中有许多独立的参与者,而不是单一的过程被测量。图 3-18 中的图形迅速回答了我关于整个用户群体在捐赠历史上的重叠程度的问题,这是我在仅仅阅读表格数据时发现很难理解的分布。
二维可视化
现在我们将使用AirPassengers数据来查看季节性和趋势,但我们不应将时间视为线性。特别是,时间可以在多个轴上发生。当然,有从一天到另一天和一年到另一年的时间轴,但我们也可以考虑将时间布置在一天中的小时轴或一周中的日期轴上等等。通过这种方式,我们可以更容易地思考季节性,例如某些行为发生在一天的特定时间或一年中的特定月份。我们特别关注如何以季节方式理解我们的数据,而不仅仅是根据线性、时间顺序的可视化。
我们从AirPassengers的ts对象中提取数据,并将其放入适当的矩阵形式中:
## R
> t(matrix(AirPassengers, nrow = 12, ncol = 12))
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12]
[1,] 112 118 132 129 121 135 148 148 136 119 104 118
[2,] 115 126 141 135 125 149 170 170 158 133 114 140
[3,] 145 150 178 163 172 178 199 199 184 162 146 166
[4,] 171 180 193 181 183 218 230 242 209 191 172 194
[5,] 196 196 236 235 229 243 264 272 237 211 180 201
[6,] 204 188 235 227 234 264 302 293 259 229 203 229
[7,] 242 233 267 269 270 315 364 347 312 274 237 278
[8,] 284 277 317 313 318 374 413 405 355 306 271 306
[9,] 315 301 356 348 355 422 465 467 404 347 305 336
[10,] 340 318 362 348 363 435 491 505 404 359 310 337
[11,] 360 342 406 396 420 472 548 559 463 407 362 405
[12,] 417 391 419 461 472 535 622 606 508 461 390 432
注意,我们必须对数据进行转置,以使其与ts对象呈现的方式一致。
列优先与行优先
R 默认是column major,这与 Python 的NumPy(row major)以及大多数 SQL 数据库不同。了解给定语言中默认和可用的行为是有益的,这不仅仅是为了展示目的,还要考虑如何有效地管理和访问内存。
我们在反映一年中月份进展的一组轴上绘制每年的曲线(见图 3-19):
## R
> colors <- c("green", "red", "pink", "blue",
> "yellow","lightsalmon", "black", "gray",
> "cyan", "lightblue", "maroon", "purple")
> matplot(matrix(AirPassengers, nrow = 12, ncol = 12),
> type = 'l', col = colors, lty = 1, lwd = 2.5,
> xaxt = "n", ylab = "Passenger Count")
> legend("topleft", legend = 1949:1960, lty = 1, lwd = 2.5,
> col = colors)
> axis(1, at = 1:12, labels = c("Jan", "Feb", "Mar", "Apr",
> "May", "Jun", "Jul", "Aug",
> "Sep", "Oct", "Nov", "Dec"))
![]()
图 3-19. 按年份和月份计算的每月计数。⁴
使用forecast包可以更轻松地生成相同的图表(见图 3-20):
## R
> require(forecast)
> seasonplot(AirPassengers)
x 轴是每年的月份。每年,航空公司乘客数量在七月或八月(第 7 和 8 月)达到峰值。大多数年份,三月(第 3 月)也有局部高峰。因此,该图可以显示更多关于季节性行为的细节。
![]()
图 3-20. 使用 seasonplot()函数更轻松地生成类似的季节性图表。
不同年份的曲线很少交叉。增长非常强劲,几乎不可能在同一月份不同年份有相同数量的乘客。有一些例外情况,但在高峰月份并不多见。仅凭这些观察,我们就可以为航空公司提供建议,帮助其做出关于如何规划增长的决策。
更少标准但同样有用的按年份排列的每月曲线的备选图(见图 3-21):
## R
> months <- c("Jan", "Feb", "Mar", "Apr", "May", "Jun",
> "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
> matplot(t(matrix(AirPassengers, nrow = 12, ncol = 12)),
> type = 'l', col = colors, lty = 1, lwd = 2.5)
> legend("left", legend = months,
> col = colors, lty = 1, lwd = 2.5)
![]()
图 3-21. 按年份计算的每月曲线的时间序列。⁵
多年来,增长趋势正在加速,即增长率本身在增加。此外,七月和八月两个月增长速度比其他月份快。使用forecast包提供的简易可视化函数,我们可以获得类似的可视化和洞察(见图 3-22):
## R
> monthplot(AirPassengers)
我们可以从这些图表中得出两个一般性观察:
![]()
图 3-22. 通过使用 monthplot()函数,我们可以看到每月性能随着年份的变化如何变化。
接下来我们考虑一个适当的二维直方图。在时间序列的背景下,我们可以将二维直方图看作一个轴表示时间(或时间的代理)和另一个轴表示兴趣单位。我们刚刚做的“堆叠”图已经在成为二维直方图的路上,但需要一些改变:
我们在这个小数据集上生成了二维直方图作为示例,然后转向更有意义的示例。我们从头开始构建我们自己的二维直方图函数,如下所示:
## R
> hist2d <- function(data, nbins.y, xlabels) {
> ## we make ybins evenly spaced to include
> ## minimum and maximum points
> ymin = min(data)
> ymax = max(data) × 1.0001
> ## the lazy way out to avoid worrying about inclusion/exclusion
> ybins = seq(from = ymin, to = ymax, length.out = nbins.y + 1 )
> ## make a zero matrix of the appropriate size
> hist.matrix = matrix(0, nrow = nbins.y, ncol = ncol(data))
> ## data comes in matrix form where each row
> ## represents one data point
> for (i in 1:nrow(data)) {
> ts = findInterval(data[i, ], ybins)
> for (j in 1:ncol(data)) {
> hist.matrix[ts[j], j] = hist.matrix[ts[j], j] + 1
> }
> }
> hist.matrix
> }
我们制作了一个带有热力图着色的直方图,如下所示:
## R
> h = hist2d(t(matrix(AirPassengers, nrow = 12, ncol = 12)), 5, months)
> image(1:ncol(h), 1:nrow(h), t(h), col = heat.colors(5),
> axes = FALSE, xlab = "Time", ylab = "Passenger Count")
然而,得到的图像(图 3-23)并不是很令人满意。
![]()
图 3-23. 我们自制的 AirPassengers 数据的二维直方图热力图。
这个图表是无用的,因为数据不足。我们只有 12 条曲线,并将它们分成 5 个桶。然而,更重要的问题是我们没有稳态数据。直方图的使用假设数据集是稳态的。在这种情况下,存在趋势,因此尽管我们想看到季节性,但趋势必然会妨碍。
现在我们来看一个样本数更多且没有趋势污染的数据集。这是来自 UCR 时间序列分类存档 的 FiftyWords 数据集的子集。该数据集包括 50 个不同单词的表示,每个时间序列的长度相同。用于绘制 图 3-24 的数据子集是我在有关时间序列分类的一般教程中使用的数据集的一个片段。你可以 下载该子集,但是对于这个练习的目的,你可以使用任何一个:
## R
> require(data.table)
> words <- fread(url.str)
> w1 <- words[V1 == 1]
> h = hist2d(w1, 25, 1:ncol(w1))
> colors <- gray.colors(20, start = 1, end = .5)
> par(mfrow = c(1, 2))
> image(1:ncol(h), 1:nrow(h), t(h),
> col = colors, axes = FALSE, xlab = "Time", ylab = "Projection Value")
> image(1:ncol(h), 1:nrow(h), t(`log`(h)),
> col = colors, axes = FALSE, xlab = "Time", ylab = "Projection Value")
![]()
图 3-24. 单词音频度量的二维直方图。左图使用线性计数尺度,而右图使用对数计数尺度。
Figure 3-24 的右图看起来更好,因为计数根据对数变换进行了着色,而不是直接计数。
这是与将时间序列取对数以减少方差和减少离群值的距离的相同想法的应用。使用对数变换通过不浪费相对稀疏的高计数值的大部分范围来改进我们的可视化。
我们自制的选项不会像许多预制选项那样视觉上吸引人,所以我们应该看看它们有何不同。为了利用这些预制解决方案,我们需要重新调整数据,因为这些选项期望将 x-y 值对转换为二维直方图。与我们的自制解决方案不同,用于二维直方图的预制选项并非专门为时间序列数据设计。尽管如此,它们仍提供了出色的可视化解决方案(见图 Figure 3-25):
## R
> w1 <- words[V1 == 1]
> ## melt the data to the pairs of paired-coordinates
> ## expected by most 2d histogram implementations
> names(w1) <- c("type", 1:270)
> w1 <- melt(w1, id.vars = "type")
> w1 <- w1[, -1]
> names(w1) <- c("Time point", "Value")
> plot(hexbin(w1))
![]()
图 3-25. 同一数据的替代二维直方图可视化。
3D 可视化
3D 可视化并不随 base R 附带,但有许多可用的包。这里我展示了一些用 plotly 制作的快速图,我选择它是因为它生成的图可以在 RStudio 中轻松旋转并导出到 Web 界面。而且,下载和安装 plotly 通常很简单,这在所有可视化包中并非如此。
让我们考虑AirPassengers数据。我们将其绘制在三维空间中,使用两个维度表示时间(月份和年份),一个维度表示数据值:
## R
> require(plotly)
> require(data.table)
> months = 1:12
> ap = data.table(matrix(AirPassengers, nrow = 12, ncol = 12))
> names(ap) = as.character(1949:1960)
> ap[, month := months]
> ap = melt(ap, id.vars = 'month')
> names(ap) = c("month", "year", "count")
> p <- plot_ly(ap, x = ~month, y = ~year, z = ~count,
> color = ~as.factor(month)) %>%
> add_markers() %>%
> layout(scene = list(xaxis = list(title = 'Month'),
> yaxis = list(title = 'Year'),
> zaxis = list(title = 'PassengerCount')))
这种三维可视化帮助我们感受数据的整体形状。我们之前见过很多类似的内容,但扩展到三维散点图明显比二维直方图更好,也许是因为数据的稀缺性(参见图 3-26 和 3-27)。
![]()
图 3-26. AirPassenger 数据的三维散点图。这个视角突出了季节性。⁶
![]()
图 3-27. 同一数据的另一视角更清晰地说明了从一年到下一年的增长趋势。我强烈建议在您自己的计算机上运行这段代码,您可以自行旋转它。⁷
我们并不一定需要在时间上耗费两个轴。相反,我们可能会用两个轴表示位置,一个轴表示时间。我们可以如下方式可视化二维随机漫步,稍微修改了 plotly 的演示代码(见图 3-28 和 3-29):
## R
> file.location <- 'https://raw.githubusercontent.com/plotly/datasets/master/\
_3d-line-plot.csv'
> data <- read.csv(file.location)
> p <- plot_ly(data, x = ~x1, y = ~y1, z = ~z1,
> type = 'scatter3d', mode = 'lines',
> line = list(color = '#1f77b4', width = 1))
图的交互性质是关键。不同的视角可能会误导或启示我们,这些效果在我们能够旋转数据之前是无法预知的。
![]()
图 3-28. 关于二维随机漫步随时间变化的一个视角。
![]()
图 3-29. 同一个随机漫步的这一视角更加具有启发性。再次鼓励您自己尝试这段代码!
一个很好的练习是生成两维中嘈杂的季节性运动数据,并以我们在这里可视化随机漫步数据的方式进行可视化。在您看到的图中,与随机漫步数据相比应该有显著的差异。像 plotly 这样的包可以帮助您快速实验,并提供全面的视觉反馈。
更多资源:
-
关于虚假相关:
Ai Deng, “时间序列回归中虚假统计显著性的初步介绍,” 《经济委员会通讯》 14, 第 1 期 (2015), https://perma.cc/9CQR-RWHC。
这篇关于虚假相关的行业介绍解释了数据中的虚假相关是如何出现的,对于在自己的数据集中找到和解决这个问题有很大帮助。这些材料写作水平非常容易理解。
Tyler Vigen, 《虚假相关》 (纽约: 哈赤特出版社, 2015), https://perma.cc/YY6R-SKWA。
这本记录荒谬时间序列相关性的集合对于任何时间序列分析师或思考的人都是必读。
Antonio Noriega 和 Daniel Ventosa-Santaulària, “在破碎趋势稳定性下的虚假回归,” 《时间序列分析杂志》 27, 第 5 期 (2006): 671–84, https://perma.cc/V993-SF4F。
作者们通过理论和模拟数据展示,独立和随机生成的数据集水平或趋势变化如何影响虚假相关的存在。
C.W.J. Granger 和 P. Newbold, “计量经济学中的虚假回归,” 《经济计量学杂志》 2, 第 2 期 (1974): 111–20, https://perma.cc/M8TE-AL6U。
这篇计量经济学文章因揭示处理虚假相关困难而获得了诺贝尔奖,并主张对相关时间序列的更强健方法。
-
关于探索性数据分析:
David R. Brillinger 和 Mark A. Finney, “一个关于蔓延火灾中温度波动的探索性数据分析,” 《环境计量学》 25, 第 6 期 (2014): 443–53, https://perma.cc/QB3D-APKM。
这是一篇非常详尽的示例,展示了学术界和政府研究人员如何分析具有地理时间网格的真实世界实验室数据。
Robert H. Shumway 和 David S. Stoffer, “时间序列回归与探索性数据分析,” 收录于 《带有 R 示例的时间序列分析及其应用》 (纽约: 斯普林格, 2011), https://perma.cc/UC5B-TPVS。
这是作者针对研究生的时间序列分析的经典著作中关于探索性数据分析的章节。
-
更多可视化:
Christian Tominski 和 Wolfgang Aigner, “TimeViz 浏览器,” https://perma.cc/94ND-6ZA5。
这个惊人的目录展示了学术研究论文和行业使用案例中许多有趣的时间序列可视化的示例和源代码。
Oscar Perpiñán Lamigueiro, “GitHub 仓库,用于展示 R 中的时间序列、空间和时空数据,” https://perma.cc/R69Y-5JPL。
这本书包括了基于 R 的各种时间序列可视化的源代码,包括地理空间时间序列数据。
Myles Harrison,《“R 语言中进行二维直方图的 5 种方法”》,R-bloggers,2014 年 9 月 1 日,https://perma.cc/ZCX9-FQQY。
这是一本实用指南,介绍了 R 包提供的各种选项来构建二维直方图,并赋予它们有意义的颜色和分箱。除了基本概述外,相关段落还提供了关于tidyquant包的详细说明,用于可视化股市数据,这是时间序列的重要来源。
-
在各种趋势上:
Halbert White 和 Clive W.J. Granger,《“时间序列趋势的考量”》,《时间序列计量经济学杂志》3 卷 1 期(2011 年),https://perma.cc/WF2H-TVTL。
这篇最新的学术文章指出,尽管趋势在数据中无处不在,甚至传统统计学对于描述数据中不同种类趋势的定义也并没有明确定义。作者以一种易于理解的方式,提供了关于非平稳数据可能具有潜在趋势的统计洞见,并指导如何改进数据的统计方法。
¹ 如果这对你没有印象,别担心。如果你希望深入了解,可以在“更多资源”中阅读更多内容。
² 由随机漫步生成的给定样本时间序列过程可能看起来有趋势,这在分析股票价格时间序列时特别引起了许多争论。
³ 使用 R 的cor()函数计算的样本相关性与使用 R 的acf()函数计算的样本自相关性可能不完全匹配,因为它们使用不同的除数。欲了解更多信息,请参阅StackExchange。
⁴ 访问GitHub 仓库查看原始图表,或自行绘制以获取更详细的查看。
⁵ 访问GitHub 仓库查看原始图表,或自行绘制以获取更详细的查看。
⁶ 访问GitHub 仓库查看原始图表,或自行绘制以获取更详细的查看。
⁷ 访问GitHub 仓库查看原始图表,或自行绘制以获取更详细的查看。
第四章:模拟时间序列数据
到目前为止,我们已经讨论了在哪里找到时间序列数据以及如何处理它。现在我们将看看如何通过模拟创建时间序列数据。
我们的讨论分为三部分。首先,我们将时间序列数据的模拟与其他类型的数据模拟进行比较,注意在考虑时间流逝时会出现哪些新的特别关注的领域。其次,我们看一些基于代码的模拟。第三,我们讨论时间序列模拟中的一些一般趋势。
本章的大部分内容将集中在生成各种类型时间序列数据的具体代码示例上。我们将逐个讨论以下示例:
-
我们模拟非营利组织成员多年来的电子邮件打开和捐赠行为。这与我们在"从表格集合中重塑时间序列数据收集"章节中检验的数据相关。
-
我们在一天之内模拟了 1,000 辆出租车的事件,这些出租车有不同的班次开始时间和与一天中小时相关的乘客接载频率。
-
我们使用相关物理定律,模拟给定温度和尺寸的磁性固体的逐步状态演变。
这三个代码示例对应于三类时间序列模拟:
启发式模拟
我们决定世界应该如何运作,确保它合理,然后一条规则一条规则地编写代码。
离散事件模拟
我们在我们的宇宙中构建具有特定规则的个体行为者,然后运行这些行为者,看看宇宙随时间的演变。
基于物理的模拟
我们应用物理定律来观察系统如何随时间演变。
模拟时间序列可以是一个有价值的分析练习,我们将在后续章节中演示,因为它涉及到具体的模型。
模拟时间序列的特别之处在于什么?
模拟数据是数据科学中很少被教授的一个领域,但对于时间序列数据却是一项特别有用的技能。这是因为时间数据的一个缺点是:同一时间序列中的两个数据点是不能完全比较的,因为它们发生在不同的时间。如果我们想考虑在特定时间发生了什么,我们就进入了模拟的世界。
模拟可以简单也可以复杂。在简单的一面,你会在任何关于时间序列的统计学教科书中遇到合成数据,比如随机漫步形式。这些通常通过随机过程的累积总和(如 R 的rnorm)或周期函数(如正弦曲线)生成。在更复杂的一面,许多科学家和工程师把他们的职业建立在模拟时间序列上。时间序列模拟仍然是许多领域的一个活跃研究领域,也是一个计算上要求高的领域,包括:
-
气象学
-
金融学
-
流行病学
-
量子化学
-
等离子体物理学
在某些情况下,行为的基本规则是被充分理解的,但由于方程的复杂性(气象学、量子化学、等离子物理学),仍然很难考虑到所有可能发生的事情。在其他情况下,不可能知道所有的预测变量,专家甚至不确定由于系统的随机非线性特性是否可以做出完美的预测(金融、流行病学)。
模拟与预测
模拟和预测是类似的练习。在这两种情况下,您必须形成关于基础系统动态和参数的假设,然后从这些假设中推断出数据点。
然而,在学习和开发模拟而不是预测时,需要记住重要的差异:
代码中的模拟
接下来,我们将看一看编写时间序列模拟的三个例子。在阅读这些例子时,请考虑可以模拟的广泛数据范围,以生成“时间序列”,以及时间元素可以非常具体和人为驱动,比如捐赠的星期几和一天中的时间,但也可以非常不具体,基本上是未标记的,比如物理模拟的“第 n 步”。
在本节中,我们将讨论三个模拟的例子:
-
模拟合成数据集以测试我们关于组织成员之间接受组织电子邮件和愿意捐赠之间相关行为的假设。这是最自助的例子,因为我们硬编码关系并生成包括for循环在内的表格数据。
-
模拟合成数据集以探索出租车队的聚合行为,包括轮班时间和依赖时间的乘客频率。在这个数据集中,我们利用 Python 的面向对象属性以及生成器,在我们想要启动系统并观察其行为时非常有帮助。
-
模拟磁性材料逐渐定向其个体磁元素的物理过程,这些元素最初处于无序状态,但最终会融合成一个有序的系统。在这个例子中,我们看到物理法则如何驱动时间序列模拟,并将自然的时间尺度插入到一个过程中。
自己动手完成工作
当您编写模拟程序时,需要牢记适用于系统的逻辑规则。在这里,我们通过示例详细说明了程序员通过确保数据合理性来完成大部分工作(例如,不指定按照不合逻辑顺序发生的事件)。
我们首先定义会员的成员资格宇宙,即我们有多少会员以及每个会员何时加入组织。我们还将每个会员与会员状态配对:
## python
>>> ## membership status
>>> years = ['2014', '2015', '2016', '2017', '2018']
>>> memberStatus = ['bronze', 'silver', 'gold', 'inactive']
>>> memberYears = np.random.choice(years, 1000,
>>> p = [0.1, 0.1, 0.15, 0.30, 0.35])
>>> memberStats = np.random.choice(memberStatus, 1000,
>>> p = [0.5, 0.3, 0.1, 0.1])
>>> yearJoined = pd.DataFrame({'yearJoined': memberYears,
>>> 'memberStats': memberStats})
请注意,这些行代码中已经内置了许多规则/假设来模拟。我们设定了会员加入年份的具体概率。我们还使会员的状态完全独立于他们加入的年份。在现实世界中,我们可能已经能够做得比这更好,因为这两个变量应该有一定的关联,特别是如果我们想要激励人们继续保持会员身份。
我们制作了一张表格,表明会员每周何时打开邮件。在这种情况下,我们定义了我们组织的行为:我们每周发送三封邮件。我们还定义了会员在邮件方面的不同行为模式:
-
从不打开邮件
-
邮件开启率/参与度保持不变
-
参与度的增加或减少
我们可以想象根据老兵的轶事观察或者我们对影响数据的不可观察过程的新颖假设,使这一过程变得更加复杂和微妙:
## python
>>> NUM_EMAILS_SENT_WEEKLY = 3
>>> ## we define several functions for different patterns
>>> def never_opens(period_rng):
>>> return []
>>> def constant_open_rate(period_rng):
>>> n, p = NUM_EMAILS_SENT_WEEKLY, np.random.uniform(0, 1)
>>> num_opened = np.random.binomial(n, p, len(period_rng))
>>> return num_opened
>>> def increasing_open_rate(period_rng):
>>> return open_rate_with_factor_change(period_rng,
>>> np.random.uniform(1.01,
>>> 1.30))
>>> def decreasing_open_rate(period_rng):
>>> return open_rate_with_factor_change(period_rng,
>>> np.random.uniform(0.5,
>>> 0.99))
>>> def open_rate_with_factor_change(period_rng, fac):
>>> if len(period_rng) < 1 :
>>> return []
>>> times = np.random.randint(0, len(period_rng),
>>> int(0.1 * len(period_rng)))
>>> num_opened = np.zeros(len(period_rng))
>>> for prd in range(0, len(period_rng), 2):
>>> try:
>>> n, p = NUM_EMAILS_SENT_WEEKLY, np.random.uniform(0,
>>> 1)
>>> num_opened[prd:(prd + 2)] = np.random.binomial(n, p,
>>> 2)
>>> p = max(min(1, p * fac), 0)
>>> except:
>>> num_opened[prd] = np.random.binomial(n, p, 1)
>>> for t in range(len(times)):
>>> num_opened[times[t]] = 0
>>> return num_opened
我们已经定义了模拟四种不同行为的函数:
从不打开我们发送给他们的邮件的会员
(never_opens())
每周打开邮件数量大致相同的会员
(constant_open_rate())
每周打开邮件数量逐渐减少的会员
(decreasing_open_rate())
每周打开邮件数量逐渐增加的会员
(increasing_open_rate())
我们确保那些随时间增加或减少参与度的人通过函数open_rate_with_factor_change()以及函数increasing_open_rate()和decreasing_open_rate()进行了相同的模拟。
我们还需要设计一个系统来模拟捐赠行为。我们不希望过于天真,否则我们的模拟将无法给我们带来关于我们应该期待什么的见解。也就是说,我们希望在模型中加入我们对会员行为的当前假设,然后测试基于这些假设的模拟是否与我们在真实数据中看到的相符。在这里,我们使捐赠行为与会员打开的邮件数量 loosely 但不是确定性地相关:
## python
>>> ## donation behavior
>>> def produce_donations(period_rng, member_behavior, num_emails,
>>> use_id, member_join_year):
>>> donation_amounts = np.array([0, 25, 50, 75, 100, 250, 500,
>>> 1000, 1500, 2000])
>>> member_has = np.random.choice(donation_amounts)
>>> email_fraction = num_emails /
>>> (NUM_EMAILS_SENT_WEEKLY * len(period_rng))
>>> member_gives = member_has * email_fraction
>>> member_gives_idx = np.where(member_gives
>>> >= donation_amounts)[0][-1]
>>> member_gives_idx = max(min(member_gives_idx,
>>> len(donation_amounts) - 2),
>>> 1)
>>> num_times_gave = np.random.poisson(2) *
>>> (2018 - member_join_year)
>>> times = np.random.randint(0, len(period_rng), num_times_gave)
>>> dons = pd.DataFrame({'member' : [],
>>> 'amount' : [],
>>> 'timestamp': []})
>>> for n in range(num_times_gave):
>>> donation = donation_amounts[member_gives_idx
>>> + np.random.binomial(1, .3)]
>>> ts = str(period_rng[times[n]].start_time
>>> + random_weekly_time_delta())
>>> dons = dons.append(pd.DataFrame(
>>> {'member' : [use_id],
>>> 'amount' : [donation],
>>> 'timestamp': [ts]}))
>>>
>>> if dons.shape[0] > 0:
>>> dons = dons[dons.amount != 0]
>>> ## we don't report zero donation events as this would not
>>> ## be recorded in a real world database
>>>
>>> return dons
我们在这里采取了一些步骤,以确保代码产生逼真的行为:
因为我们的会员行为与特定时间戳相关联,所以我们必须选择每个会员何时进行捐赠以及在那一周的哪个时间进行捐赠。我们编写了一个实用函数来在一周内选择随机时间:
## python
>>> def random_weekly_time_delta():
>>> days_of_week = [d for d in range(7)]
>>> hours_of_day = [h for h in range(11, 23)]
>>> minute_of_hour = [m for m in range(60)]
>>> second_of_minute = [s for s in range(60)]
>>> return pd.Timedelta(str(np.random.choice(days_of_week))
>>> + " days" ) +
>>> pd.Timedelta(str(np.random.choice(hours_of_day))
>>> + " hours" ) +
>>> pd.Timedelta(str(np.random.choice(minute_of_hour))
>>> + " minutes") +
>>> pd.Timedelta(str(np.random.choice(second_of_minute))
>>> + " seconds")
您可能已经注意到,我们只从时间戳的小时范围中提取了 11 到 23 点之间的小时(hours_of_day = [h for h in range(11, 23)])。我们假设的是一个非常有限时区范围内的人们甚至只有一个单一时区的宇宙,因为我们不允许超出给定范围的小时。在这里,我们正在构建更多关于用户行为的基础模型。
因此,我们预期会看到我们的用户表现出统一的行为,就好像他们都在一个或几个相邻的时区内,而我们进一步推测合理的捐赠行为是人们从早上稍晚到晚上捐款,但不包括过夜和早上醒来时第一件事。
最后,我们将所有开发的组件放在一起,以一种方式模拟一定数量的会员和相关事件,确保事件仅在会员加入后发生,并且会员的电子邮件事件与其捐赠事件有一定关系(但关系不是不切实际小的):
## python
>>> behaviors = [never_opens,
>>> constant_open_rate,
>>> increasing_open_rate,
>>> decreasing_open_rate]
>>> member_behaviors = np.random.choice(behaviors, 1000,
>>> [0.2, 0.5, 0.1, 0.2])
>>> rng = pd.period_range('2015-02-14', '2018-06-01', freq = 'W')
>>> emails = pd.DataFrame({'member' : [],
>>> 'week' : [],
>>> 'emailsOpened': []})
>>> donations = pd.DataFrame({'member' : [],
>>> 'amount' : [],
>>> 'timestamp': []})
>>> for idx in range(yearJoined.shape[0]):
>>> ## randomly generate the date when a member would have joined
>>> join_date = pd.Timestamp(yearJoined.iloc[idx].yearJoined) +
>>> pd.Timedelta(str(np.random.randint(0, 365)) +
>>> ' days')
>>> join_date = min(join_date, pd.Timestamp('2018-06-01'))
>>>
>>> ## member should not have action timestamps before joining
>>> member_rng = rng[rng > join_date]
>>>
>>> if len(member_rng) < 1:
>>> continue
>>>
>>> info = member_behaviorsidx
>>> if len(info) == len(member_rng):
>>> emails = emails.append(pd.DataFrame(
>>> {'member': [idx] * len(info),
>>> 'week': [str(r.start_time) for r in member_rng],
>>> 'emailsOpened': info}))
>>> donations = donations.append(
>>> produce_donations(member_rng, member_behaviors[idx],
>>> sum(info), idx, join_date.year))
然后,我们查看捐款的时间行为,以了解我们可能如何尝试进行进一步分析或预测。我们绘制了数据集每个月收到的捐款总额的总和(见图 4-1):
## python
>>> df.set_index(pd.to_datetime(df.timestamp), inplace = True)
>>> df.sort_index(inplace = True)
>>> df.groupby(pd.Grouper(freq='M')).amount.sum().plot()
![]()
图 4-1. 数据集每个月收到的捐款总额。
从 2015 年到 2018 年,捐款数和已打开的电子邮件数量看起来随着时间的推移有所增加。这并不令人惊讶,因为成员数量随时间增加,如成员的累积总和和加入年份所示。事实上,我们模型的一个内置假设是,一旦成员加入,我们就可以无限期地保留其成员资格。我们没有设定终止成员资格的任何规定,除非允许成员打开越来越少的电子邮件。即使在这种情况下,我们也留下了继续捐赠的可能性。我们在图 4-1 中看到了这种无限期持续会员资格(及相关的捐赠行为)的假设。我们可能应该回过头来改进我们的代码,因为无限期的会员资格和捐赠并不是一个现实的情景。
这不是一个经典的时间序列模拟,因此可能更像是生成表格数据的练习。但我们确实必须意识到时间序列:
-
我们不得不做出关于我们的用户处于多少时间序列中的决策。
-
我们必须对我们将随时间建模的趋势类型做出决策:
-
我们必须小心确保在成员加入组织之前没有打开电子邮件或进行捐款。
-
我们必须确保我们的数据不会进入未来,以使其对数据的消费者更具现实意义。请注意,对于模拟来说,如果我们的数据进入未来是可以的。
但它并不完美。这里提出的代码笨拙不堪,并且不能产生一个真实的宇宙。更重要的是,由于只有程序员检查了逻辑,他们可能会错过一些边界情况,导致事件按照不合逻辑的顺序发生。在运行模拟之前,建立外部度量和有效性标准是防止此类错误的一种保护措施之一。
我们需要一种能够强制实现逻辑和一致的软件宇宙。我们将在下一节中看一看 Python 生成器作为更好的选择。
构建一个自运行的模拟宇宙
有时候你有一个特定的系统,你想为该系统设置规则并观察其运行。也许你想设想一下独立成员访问你的应用程序将使用的宇宙,或者你想试图验证一个基于假设的外部行为的内部决策理论。在这些情况下,你希望看到个体代理如何随时间推移贡献到你的聚合度量中。由于生成器的可用性,Python 在这项工作中尤为合适。当你开始构建软件而不仅仅停留在分析时,即使你更喜欢 R,使用 Python 也是有道理的。
生成器允许我们创建一系列独立(或依赖的!)行为者,并让它们运转起来,观察它们的行为,而无需太多样板代码来跟踪一切。
在接下来的代码示例中,我们探索一个出租车模拟。¹ 我们想象一下,一群打算在不同时间开始工作的出租车会以何种方式进行集体行动。为了做到这一点,我们希望创建许多个体出租车,将它们释放在一个虚拟城市中,并让它们汇报它们的活动。
这样的模拟可能异常复杂。为了演示目的,我们接受将构建一个比我们想象中更简单的世界(“所有模型都是错误的…”)。我们首先尝试理解 Python 生成器是什么。
让我们首先考虑一种我编写的用于获取出租车识别号的方法:
## python
>>> import numpy as np
>>> def taxi_id_number(num_taxis):
>>> arr = np.arange(num_taxis)
>>> np.random.shuffle(arr)
>>> for i in range(num_taxis):
>>> yield arr[i]
对于那些不熟悉生成器的人,这里是前面的代码示例:
## python
>>> ids = taxi_id_number(10)
>>> print(next(ids))
>>> print(next(ids))
>>> print(next(ids))
可能会打印出以下内容:
7
2
5
这将迭代直到发出 10 个数字,此时它将退出生成器中保存的for循环,并发出StopIteration异常。
taxi_id_number()生成一次性对象,它们之间都是相互独立的,并且保持自己的状态。这是一个生成器函数。你可以把生成器想象成维护自己的一小组状态变量的小对象,当你希望许多对象并行运行时特别有用,每个对象都关注自己的变量。
在这个简单的出租车模拟案例中,我们将出租车分成不同的班次,并且使用生成器来指示班次。我们通过在特定时间开始班次设置不同的概率,白天中段调度更多的出租车,而不是在晚上或过夜的班次:
## python
>>> def shift_info():
>>> start_times_and_freqs = [(0, 8), (8, 30), (16, 15)]
>>> indices = np.arange(len(start_times_and_freqs))
>>> while True:
>>> idx = np.random.choice(indices, p = [0.25, 0.5, 0.25])
>>> start = start_times_and_freqs[idx]
>>> yield (start[0], start[0] + 7.5, start[1])
注意start_times_and_freqs。这是我们第一段代码,将有助于将其变成时间序列模拟。我们指出一天中的不同时间段有不同的出租车分配概率。此外,一天中的不同时间有不同的平均出租车次数。
现在我们创建一个更复杂的生成器,它将使用前面的生成器来建立单个出租车的参数,并创建单个出租车的时间线:
## python
>>> def taxi_process(taxi_id_generator, shift_info_generator):
>>> taxi_id = next(taxi_id_generator)
>>> shift_start, shift_end, shift_mean_trips =
>>> next(shift_info_generator)
>>> actual_trips = round(np.random.normal(loc = shift_mean_trips,
>>> scale = 2))
>>> average_trip_time = 6.5 / shift_mean_trips * 60
>>> # convert mean trip time to minutes
>>> between_events_time = 1.0 / (shift_mean_trips - 1) * 60
>>> # this is an efficient city where cabs are seldom unused
>>> time = shift_start
>>> yield TimePoint(taxi_id, 'start shift', time)
>>> deltaT = np.random.poisson(between_events_time) / 60
>>> time += deltaT
>>> for i in range(actual_trips):
>>> yield TimePoint(taxi_id, 'pick up ', time)
>>> deltaT = np.random.poisson(average_trip_time) / 60
>>> time += deltaT
>>> yield TimePoint(taxi_id, 'drop off ', time)
>>> deltaT = np.random.poisson(between_events_time) / 60
>>> time += deltaT
>>> deltaT = np.random.poisson(between_events_time) / 60
>>> time += deltaT
>>> yield TimePoint(taxi_id, 'end shift ', time)
在这里,出租车访问生成器来确定其 ID 编号、班次开始时间以及其开始时间的平均出租车次数。从那里开始,它将在自己的时间轴上运行一定数量的行程,并将这些行程传递给调用此生成器上的next()的客户端。实际上,这个生成器为单个出租车产生了时间序列点。
出租车生成器产生了TimePoint,其定义如下:
## python
>>> from dataclasses import dataclass
>>> @dataclass
>>> class TimePoint:
>>> taxi_id: int
>>> name: str
>>> time: float
>>> def __lt__(self, other):
>>> return self.time < other.time
我们使用相对较新的dataclass装饰器简化代码(这需要 Python 3.7)。我建议所有使用 Python 的数据科学家熟悉这个新的对数据友好的 Python 添加。
Python 的 Dunder 方法
Python 的dunder方法,名称以双下划线开始和结束,是每个类的内置方法集。Dunder 方法在使用给定对象时会自动调用。这些方法有预定义的实现,可以在你自己定义类时覆盖。你可能希望这样做的原因有很多,例如在前面的代码中,我们希望仅基于它们的时间而不是基于它们的taxi_id或name属性来比较TimePoint。
Dunder 源于“double under”的缩写。
除了自动生成的TimePoint初始化器外,我们只需要其他两个 dunder 方法,__lt__(用于比较TimePoint)和__str__(用于打印TimePoint,这里没有显示)。我们需要比较,因为我们将把所有生成的TimePoint放入一个数据结构中,这个数据结构将按照它们的优先级来保持它们的顺序:一个优先队列是一个抽象数据类型,可以按任何顺序插入对象,但将按其优先级发射对象。
抽象数据类型
抽象数据类型是一种由其行为定义的计算模型,其行为由一组可能的操作和输入数据的枚举集合以及对于某些数据集的操作结果组成。
一个广为人知的抽象数据类型是先进先出(FIFO)数据类型。这要求对象从数据结构中被发射出来的顺序与它们被送入数据结构的顺序相同。程序员如何选择实现这一点是实现的问题,而不是定义的问题。
我们有一个模拟类来运行这些出租车生成器并保持它们的组装。这不仅仅是一个dataclass,因为它在初始化器中有相当多的功能,来将输入整理成一个合理的信息和处理数组。请注意,唯一面向公众的功能是run()函数:
## python
>>> import queue
>>> class Simulator:
>>> def __init__(self, num_taxis):
>>> self._time_points = queue.PriorityQueue()
>>> taxi_id_generator = taxi_id_number(num_taxis)
>>> shift_info_generator = shift_info()
>>> self._taxis = [taxi_process(taxi_id_generator,
>>> shift_info_generator) for
>>> i in range(num_taxis)]
>>> self._prepare_run()
>>> def _prepare_run(self):
>>> for t in self._taxis:
>>> while True:
>>> try:
>>> e = next(t)
>>> self._time_points.put(e)
>>> except:
>>> break
>>> def run(self):
>>> sim_time = 0
>>> while sim_time < 24:
>>> if self._time_points.empty():
>>> break
>>> p = self._time_points.get()
>>> sim_time = p.time
>>> print(p)
首先,我们创建所需数量的出租车生成器来表示正确数量的出租车。然后我们遍历每一个这些出租车,当它还有TimePoint时,将所有这些TimePoint推入优先队列。对象的优先级由我们对TimePoint类的__lt__实现决定,我们在其中比较开始时间。因此,当TimePoint被推入优先队列时,它们将准备好按时间顺序被发射出去。
我们运行模拟:
## python
>>> sim = Simulator(1000)
>>> sim.run()
下面是输出的样子(你的输出将会不同,因为我们还没有设置种子——每次运行代码时都会与上次迭代不同):
id: 0539 name: drop off time: 23:58
id: 0318 name: pick up time: 23:58
id: 0759 name: end shift time: 23:58
id: 0977 name: pick up time: 23:58
id: 0693 name: end shift time: 23:59
id: 0085 name: end shift time: 23:59
id: 0351 name: end shift time: 23:59
id: 0036 name: end shift time: 23:59
id: 0314 name: drop off time: 23:59
在生成随机数时设置种子
当您编写生成随机数的代码时,您可能希望确保它是可重现的(例如,如果您想为通常是随机的代码设置单元测试,或者如果您试图调试并希望缩小变异源以便更容易调试)。为了确保随机数以相同的非随机顺序输出,您设置了一个种子。这是一个常见操作,因此有关如何在任何计算机语言中设置种子的指南。
我们已经将显示简单性舍入到最接近的分钟,尽管我们有更精细的数据可用。我们使用什么时间分辨率将取决于我们的目的:
我们决定按照“发生”的方式报告出租车的“时间点”。也就是说,我们报告出租车乘车的开始(“接人”),而不是结束时间,尽管我们很容易可以压缩这一信息。这是使时间序列更加真实的一种方式,因为在实时流中,您很可能会以这种方式记录事件。
请注意,与前一种情况类似,我们的时间序列模拟尚未生成时间序列。我们已经生成了一个日志,并可以通过多种方式使其成为时间序列,然而:
-
在运行模拟的同时,将结果输出到 CSV 文件或时间序列数据库。
-
运行某种在线模型,连接到我们的模拟以了解如何开发实时流数据处理管道。
-
将输出保存到文件或数据库中,然后进行更多的后处理,以便以一种方便的形式(但可能与前瞻性风险相关)打包数据,例如将给定乘车的起始和结束时间配对,以研究出租车乘车时间在不同时间段的行为。
除了能够测试关于出租车系统动态的假设之外,模拟此数据还有几个优点。以下是一些情况,合成时间序列数据可能会有用:
作为时间序列分析员,您能够利用生成器和面向对象编程将大有裨益。此示例仅提供了一个如何利用此类知识简化生活并提高代码质量的例子。
对于广泛的模拟,请考虑基于代理的建模
我们在这里编码的解决方案还算可以,但需要大量样板来确保逻辑条件得到尊重。如果基于离散行动者的离散事件模拟能成为模拟时间序列数据的有用来源,则应考虑采用面向模拟的模块。SimPy模块是一个有帮助的选择,具有易于访问的 API,并具有相当多的灵活性,可以执行我们在本节中处理的各种模拟任务。
物理模拟
在另一种模拟场景中,您可能完全掌握了定义系统的物理定律。然而,这不一定是物理本身,它也可以适用于许多其他领域:
一个了解简单物理系统某些规则的案例是模拟磁铁行为。这是我们要处理的情况,通过一个常见的统计力学模型称为伊辛模型²。我们将看看如何模拟其随时间的行为。我们将初始化磁性材料,使其各个磁性组件指向随机方向。然后,我们将观察这个系统如何在已知物理法则和几行代码的作用下演变成所有磁性组件指向相同方向的有序状态。
接下来我们讨论如何通过马尔可夫链蒙特卡洛(MCMC)方法实现这样的模拟,讨论该方法在一般情况下以及应用于这个特定系统时的工作原理。
在物理学中,MCMC 模拟可以用于了解个别分子中的量子跃迁如何影响该系统的聚集集合测量结果随时间的变化。在这种情况下,我们需要应用一些特定的规则:
-
在马尔可夫过程中,未来状态的转移概率仅依赖于当前状态(而不是过去信息)。
-
我们将施加一个物理特定的条件,要求能量的玻尔兹曼分布;即, 。对于大多数人来说,这只是一个实现细节,非物理学家无需担心。
我们按以下方式实施 MCMC 模拟:
-
随机选择每个个体晶格点的起始状态。
-
对于每个时间步长,选择一个个体晶格点并翻转其方向。
-
根据您正在使用的物理定律计算此翻转导致的能量变化。在这种情况下,这意味着:
持续执行步骤 2 和 3,直到收敛,以确定您正在进行的任何综合测量的最可能状态。
让我们来看一下伊辛模型的具体细节。想象我们有一个由物体网格组成的二维材料,每个物体都有一个可以指向上或向下的微小磁铁。我们在时间零时随机放置这些微小磁铁,然后记录系统从随机状态到低温有序状态的演变。³
首先,我们配置我们的系统如下:
## python
>>> ### CONFIGURATION
>>> ## physical layout
>>> N = 5 # width of lattice
>>> M = 5 # height of lattice
>>> ## temperature settings
>>> temperature = 0.5
>>> BETA = 1 / temperature
然后我们有一些实用方法,比如随机初始化我们的起始块:
>>> def initRandState(N, M):
>>> block = np.random.choice([-1, 1], size = (N, M))
>>> return block
我们还计算了相对于其邻居的中心状态对齐给定能量:
## python
>>> def costForCenterState(state, i, j, n, m):
>>> centerS = state[i, j]
>>> neighbors = [((i + 1) % n, j), ((i - 1) % n, j),
>>> (i, (j + 1) % m), (i, (j - 1) % m)]
>>> ## notice the % n because we impose periodic boundary cond
>>> ## ignore this if it doesn't make sense - it's merely a
>>> ## physical constraint on the system saying 2D system is like
>>> ## the surface of a donut
>>> interactionE = [state[x, y] * centerS for (x, y) in neighbors]
>>> return np.sum(interactionE)
我们想要确定给定状态下整个块的磁化:
## python
>>> def magnetizationForState(state):
>>> return np.sum(state)
这里是我们介绍了前面讨论过的 MCMC 步骤:
## python
>>> def mcmcAdjust(state):
>>> n = state.shape[0]
>>> m = state.shape[1]
>>> x, y = np.random.randint(0, n), np.random.randint(0, m)
>>> centerS = state[x, y]
>>> cost = costForCenterState(state, x, y, n, m)
>>> if cost < 0:
>>> centerS *= -1
>>> elif np.random.random() < np.exp(-cost * BETA):
>>> centerS *= -1
>>> state[x, y] = centerS
>>> return state
现在我们实际运行一次模拟,我们需要一些记录以及对 MCMC 调整的重复调用:
## python
>>> def runState(state, n_steps, snapsteps = None):
>>> if snapsteps is None:
>>> snapsteps = np.linspace(0, n_steps, num = round(n_steps / (M * N * 100)),
>>> dtype = np.int32)
>>> saved_states = []
>>> sp = 0
>>> magnet_hist = []
>>> for i in range(n_steps):
>>> state = mcmcAdjust(state)
>>> magnet_hist.append(magnetizationForState(state))
>>> if sp < len(snapsteps) and i == snapsteps[sp]:
>>> saved_states.append(np.copy(state))
>>> sp += 1
>>> return state, saved_states, magnet_hist
然后我们运行模拟:
## python
>>> ### RUN A SIMULATION
>>> init_state = initRandState(N, M)
>>> print(init_state)
>>> final_state = runState(np.copy(init_state), 1000)
通过观察初始状态和最终状态,我们可以从这个模拟中得到一些见解(见 图 4-2)。
![]()
图 4-2. 一个 5 × 5 模拟铁磁材料的初始状态,每个状态随机选择为向上或向下旋转,概率相等。
在 图 4-2 中,我们研究了一个随机生成的初始状态。虽然您可能期望看到这两个状态更加混合,但请记住从概率上讲,得到完美的棋盘效应并不那么可能。尝试多次生成初始状态,您会发现这种看似“随机”或“50/50”的棋盘状态并不常见。然而,请注意,我们的大约一半站点起始于每种状态。还要意识到,您在初始状态中发现的任何模式很可能是您的大脑遵循的非常人类的倾向,即在那里没有任何模式的地方也看到模式。
我们将初始状态传递到 runState() 函数中,允许 1,000 个时间步骤后检查结果,并观察 图 4-3 中的结果。
这是在第 1,000 步拍摄的状态快照。此时至少有两个有趣的观察点。首先,与第 1,000 步相比,主导状态已经反转。其次,主导状态在数字上并不比第 1,000 步的另一主导状态更主导。这表明,即使在可能偏向于其它情况的情况下,温度也可能继续翻转站点。为了更好地理解这些动态,我们应该考虑绘制整体的聚合测量,比如磁化,或制作影片,在那里我们可以以时间序列格式查看我们的二维数据。
![]()
图 4-3. 在我们模拟中运行的最终低温状态,看起来像是在 1,000 个时间步骤时。
我们通过许多先前模拟的独立运行来获得随时间变化的磁化,如 图 4-4 所示:
## python
>>> we collect each time series as a separate element in results list
>>> results = []
>>> for i in range(100):
>>> init_state = initRandState(N, M)
>>> final_state, states, magnet_hist = runState(init_state, 1000)
>>> results.append(magnet_hist)
>>>
>>> ## we plot each curve with some transparency so we can see
>>> ## curves that overlap one another
>>> for mh in results:
>>> plt.plot(mh,'r', alpha=0.2)
磁化曲线只是我们可以想象系统随时间演变的一个例子。我们还可以考虑记录 2D 时间序列,作为每个时间点总体状态的快照。或者可能有其他有趣的聚合变量在每一步测量,比如布局熵或总能量的测量。磁化或熵等量是相关的量,因为它们是每个晶格点状态几何布局的函数,但每个量略有不同的度量。
![]()
我们可以像讨论出租车数据那样使用这些数据,即使底层系统完全不同。例如,我们可以:
关于模拟的最后说明
我们已经看过许多非常不同的例子,模拟测量描述随时间变化的行为。我们看过有关消费者行为(NGO 成员资格和捐赠)、城市基础设施(出租车接载模式)以及物理定律(随机磁性材料逐渐有序化)的模拟数据。这些例子应该使你感到足够自信,可以开始阅读模拟数据的代码示例,并且可以思考你自己的工作如何从模拟中受益。
过去,你可能在不知道如何测试数据假设或替代可能性的情况下做出关于数据的假设。模拟为你提供了这样的机会,这意味着你关于数据的讨论可以扩展到包括假设的例子和来自模拟的定量指标。这将在时间序列领域以及数据科学的其他分支中为你的讨论奠定基础,并开辟新的可能性。
统计模拟
统计模拟是模拟时间序列数据的最传统途径。当我们了解随机系统的基本动态并希望估计几个未知参数或查看不同假设对参数估计过程的影响时,它们尤其有用(我们将在本书后面看到一个例子)。即使对于物理系统,有时统计模拟效果也更好。
当我们需要确定我们对模拟精度的不确定性的定量度量时,时间序列数据的统计模拟也是非常有价值的。在传统的统计模拟中,例如 ARIMA 模型(将在第六章中讨论),误差的公式是被充分验证的,这意味着为了理解一个假设的基础统计模型的系统,您不需要运行许多模拟来对误差和方差进行数值断言。
深度学习模拟
深度学习模拟时间序列是一个新兴但有前途的领域。深度学习的优势在于即使在没有完全理解动态的情况下,也能捕捉时间序列数据中非常复杂的非线性动态。然而,这也是一个缺点,因为从业者在理解系统动态方面没有原则性的基础。
深度学习模拟也在隐私成为问题时提供了希望。例如,深度学习已被用于基于实际时间序列数据生成合成的异质时间序列数据,但不会泄露私人信息。如果可以确实地生成这样的数据集而不会泄露隐私信息,这将是非常宝贵的,因为研究人员可以访问大量(否则昂贵且侵犯隐私的)医疗数据。
更多资源
Cristóbal Esteban, Stephanie L. Hyland 和 Gunnar Rätsch, “用递归条件生成对抗网络生成实值(医学)时间序列,” 未发表手稿, 最后修订于 2017 年 12 月 4 日, https://perma.cc/Q69W-L44Z.
作者展示了如何利用生成对抗网络来生成看起来逼真的异质医疗时间序列数据。这是深度学习模拟可用于创建伦理、法律和(希望是)保护隐私的医疗数据集的一个示例,以便在医疗保健背景下为机器学习和深度学习提供更广泛的有用数据访问。
Gordon Reikard 和 W. Erick Rogers, “预测海洋波浪:将基于物理模型与统计模型进行比较,” 海岸工程 58 (2011): 409–16, https://perma.cc/89DJ-ZENZ.
本文提供了一个关于用物理或统计方法建模系统的两种截然不同方式的易于理解和实用的比较。研究人员得出结论,对于他们解决的特定问题,预测者感兴趣的时间尺度应该决定应用哪种范式。虽然本文是关于预测的,但模拟与之密切相关,并且相同的见解同样适用。
Wolfgang Härdle, Joel Horowitz 和 Jens-Peter Kreiss, “时间序列的自举方法,” 国际统计评论 / International Statistical Review 71, no. 2 (2003): 435–59, https://perma.cc/6CQA-EG2E.
一篇经典的 2005 年综述文章,讨论了统计模拟时间序列数据在处理时间依赖性方面的困难。作者在一本高度技术性的统计期刊中解释了,为什么时间序列数据的自举方法落后于其他类型数据的方法,并且在写作时可用的有前景的方法是什么。由于技术发展并未有太大改变,这篇文章是一篇有用的、尽管具有挑战性的阅读材料。
¹ 本例受到 Luciano Ramalho 的书籍《流畅的 Python》(O'Reilly 2015)的启发。我强烈推荐阅读该书的完整仿真章节,以提高你的 Python 编程技能,并看到基于代理的仿真的更多精彩机会。
² Ising 模型是一个众所周知且广泛教授的经典统计力学模型,用于研究磁铁。如果你有兴趣了解更多,你可以在编程和物理上下文中找到许多此模型的代码示例和进一步讨论。
³ Ising 模型更常用于理解铁磁体的平衡态,而不是考虑铁磁体如何进入平衡态的时间方面。然而,我们将随时间的演变视为时间序列。
第五章:存储时间数据
时间序列数据的价值通常体现在回顾性而非实时流式场景中。因此,对于大多数时间序列分析来说,存储时间序列数据是必要的。
一个良好的存储解决方案应该能够在不需要大量计算资源投入的情况下,实现数据的轻松访问和可靠性。在本章中,我们将讨论设计时间序列数据存储时应考虑的数据集的各个方面。我们还将讨论 SQL 数据库、NoSQL 数据库以及各种平面文件格式的优势。
设计通用的时间序列存储解决方案是一项挑战,因为有许多不同种类的时间序列数据,每种数据都具有不同的存储、读写和分析模式。一些数据将被反复存储和检查,而其他数据只在短时间内有用,之后可以完全删除。
这里有几个时间序列存储的使用案例,它们具有不同的读、写和查询模式:
-
您正在收集生产系统的性能指标。您需要将这些性能指标存储多年,但数据变老越久,它的详细程度就越低。因此,您需要一种存储形式,能够随着信息老化自动进行降采样和剪辑。
-
您可以访问远程开源时间序列数据存储库,但需要在您的计算机上保留本地副本以减少网络流量。远程存储库将每个时间序列存储在一个可通过 Web 服务器下载的文件夹中,但您希望将所有这些文件合并到一个单一的数据库中,以简化操作。数据应该是不可变的,并且能够无限期地存储,因为它旨在是远程存储库的可靠副本。
-
您通过在不同时间尺度上整合各种数据源,并进行不同的预处理和格式化,创建了自己的时间序列数据。数据的收集和处理是费力且耗时的。您希望将数据以最终形式存储,而不是反复运行预处理步骤,但您也希望保留原始数据,以备稍后探索预处理替代方案。随着您开发新的机器学习模型并随时间添加新的、更新的原始数据,您预计会经常回顾处理和原始数据。即使您为了分析而对数据进行降采样或剪辑,您也永远不会在存储中降低数据质量。
这些使用案例在其对系统的主要需求方面相当多样化:
性能随大小变化的重要性
在第一个用例中,我们将寻找一个能够集成自动化脚本以删除旧数据的解决方案。我们不会担心系统如何为大数据集进行扩展,因为我们计划保持数据集较小。相比之下,在第二和第三个用例中,我们预期会有稳定的大型数据集(用例 2)或大型且增长中的数据集(用例 3)。
随机访问数据点与顺序访问数据点的重要性
在用例 2 中,我们预计所有数据都将以相等的方式访问,因为这些时间序列数据在插入时都是相同的“年龄”,并且都引用了有趣的数据集。相比之下,在用例 1 和 3 中(尤其是在用例 1 中,根据前述描述),我们预计最近的数据会更频繁地被访问。
自动化脚本的重要性
用例 1 看起来可能会自动化,而用例 2 不需要自动化(因为数据是不可变的)。用例 3 表明几乎没有自动化,但需要获取和处理所有数据部分,而不仅仅是最近的数据。在用例 1 中,我们希望找到一个能够与脚本或存储过程集成的存储解决方案,而在用例 3 中,我们希望找到一个允许轻松定制数据处理的解决方案。
仅仅三个例子就已经很好地展示了一种通用时间序列解决方案需要满足的众多用例。
在实际用例中,您将能够定制您的存储解决方案,而不必担心找不到适合所有用例的工具。话虽如此,您始终会在类似的可用技术范围内进行选择,这些技术往往归结为以下几种:
在本章中,我们涵盖了所有三种选项,并讨论了每种选项的优缺点。当然,具体情况取决于具体用例,但本章将为您提供在寻找适合您用例的时间序列存储选项时所需的基础。
我们首先讨论在选择存储解决方案时应该问什么问题。然后我们看一下 SQL 与 NoSQL 的大辩论,并检查一些最流行的时间序列存储解决方案。最后,我们考虑设置政策来让旧的时间序列数据过期并被删除。
定义需求
当您考虑存储时间序列数据时,您需要问自己一些问题:
-
你将存储多少时间序列数据?这些数据会以多快的速度增长? 你需要选择一个适合你预期数据增长速度的存储解决方案。从事时间序列工作的数据库管理员通常会对时间序列数据集的增长速度感到惊讶,尤其是那些从事事务性数据集工作的管理员。
-
你的测量是否趋向于无尽的更新通道(例如,持续不断的网页流量更新),还是独立的事件(例如,过去 10 年每个主要美国假期的每小时空中交通时间序列)? 如果你的数据像一个无尽的通道,你大多数情况下会查看最近的数据。另一方面,如果你的数据是一个由独立时间序列分割成单独事件的集合,那么时间上更远的事件可能仍然相当有趣。在后一种情况下,随机访问是更可能的模式。
-
你的数据是定期还是不定期分布的? 如果你的数据是定期分布的,你可以提前更准确地计算你预期收集的数据量以及数据的输入频率。如果你的数据是不定期分布的,你必须准备一个不太可预测的数据访问方式,能有效地处理静止期和写入活动期。
-
你是否将持续收集数据,还是你的项目有一个明确定义的结束? 如果你的数据收集有一个明确定义的结束,这将更容易知道你需要适应多大的数据集。然而,许多组织发现一旦开始收集某种类型的时间序列,就不想停止!
-
你将如何处理你的时间序列?你需要实时可视化吗?为神经网络预处理数据以进行数千次迭代?高度可用于大型移动用户群体的分片数据? 你的主要用例将表明你更可能需要顺序访问还是随机访问你的数据,以及延迟在你选择存储格式时应扮演的重要角色。
-
你将如何剔除或降低数据采样率?你将如何防止无限增长?一个时间序列中个别数据点的生命周期应该是什么样子? 不可能永久存储所有事件。最好是系统地和提前做出关于数据删除策略的决定,而不是临时决定。你越是能在前期做出承诺,你在存储格式选择上就能做出更好的选择。关于这点,我们将在下一节详细讨论。
这些问题的答案将表明你是否应该存储原始数据还是处理后的数据,数据是否应该根据时间或其他轴来定位在内存中,以及是否需要以便于读写的形式存储你的数据。用例会有所不同,所以你应该在每一个新数据集上做一次全新的盘点。
提示
分片 数据是一个大数据系统中的一部分,但分散到更小、更可管理的块中,通常分布在网络上的多个服务器上。
实时数据与存储数据
在考虑数据的存储选项时,理解数据的生命周期至关重要。对于数据的实际使用案例能越实际,你需要保存的数据就越少,你需要担心找到最佳存储系统的时间也就越少,因为你不会迅速积累到无法处理的数据量。通常,组织会过度记录感兴趣的事件,并害怕丢失它们的数据存储,但是将更多数据存储在无法处理的形式中远不如在有意义的时间尺度上存储聚合数据有用。
对于短暂的数据,比如仅用于确保一切正常的性能数据,可能永远不需要将数据存储在收集的形式中,至少不需要很长时间。这对于事件驱动数据尤为重要,其中没有单个事件重要,而是感兴趣的是聚合统计数据。
假设你运行一个 Web 服务器,记录并向你报告每个移动设备加载特定网页所需的时间。结果是不规则的时间序列,可能看起来像表 5-1 所示。
表 5-1. Web 服务器时间序列
| 时间戳 |
加载页面时间 |
| 2018 年 4 月 5 日晚上 10:22:24 |
23 秒 |
| 2018 年 4 月 5 日晚上 10:22:28 |
15 秒 |
| 2018 年 4 月 5 日晚上 10:22:41 |
14 秒 |
| 2018 年 4 月 5 日晚上 10:23:02 |
11 秒 |
由于多种原因,你可能对页面加载时间的任何单独测量都不感兴趣。你希望聚合数据(例如,每分钟平均加载时间),即使聚合统计数据也只有短暂的兴趣。假设你在那台服务器上通宵值班。你希望确保在你负责期间性能良好。你可以简化为你值班 12 小时的数据点,这将包含你可能需要的大部分信息,如 Table 5-2 所示。
表 5-2. 值班时段简化数据点
| 时间段 |
最流行的访问小时 |
加载次数 |
平均加载时间 |
最大加载时间 |
| 2018 年 4 月 5 日晚上 8 点至次日早上 8 点 |
23 时 |
3,470 |
21 秒 |
45 秒 |
在这种情况下,你不应计划无限期地存储单个事件。相反,你应该建立一个存储解决方案,仅提供临时存储的个体事件,直到数据进入其最终形式。通过防止数据暴增开始之前的操作,你将为自己和同事节省很多麻烦。与无人感兴趣的 3,470 个单独事件相比,你将拥有易于访问和紧凑的感兴趣数据。在可能的情况下,你应该通过聚合和去重简化数据存储。
接下来,我们考虑几种减少数据量而不丢失信息的机会。
缓慢变化的变量
如果你正在存储一个状态变量,请考虑只记录数值发生变化的数据点。例如,如果你每隔五分钟记录一次温度,你的温度曲线可能看起来像一个阶梯函数,特别是如果你只关心最接近度数的值。在这种情况下,存储重复数值是不必要的,通过不这样做你可以节省存储空间。
嘈杂的高频数据
如果你的数据很嘈杂,有理由不太关心任何特定的数据点。考虑在记录数据点之前对它们进行聚合,因为高噪声水平使得任何单个测量值的价值降低。当然,这将是相当特定于领域,并且你需要确保下游用户仍然能够评估其目的中测量数据中的噪声。
过时数据
数据越老,你的组织使用它的可能性就越小,除非以非常一般的方式。每当你开始记录一个新的时间序列数据集时,你应该预先考虑时间序列数据何时可能变得不相关:
如果你能自动化地删除数据,而不会妨碍数据分析工作,你将通过减少可扩展性的重要性或者减少庞大数据集上慢查询的负担,来改善你的数据存储选项。
到目前为止,我们已经讨论了时间序列存储的一般使用情况范围。我们还回顾了一组关于如何生成和分析时间序列数据集的查询,以便这些查询可以指导我们选择存储格式。我们现在回顾两种常见的时间序列存储选项:数据库和文件。
数据库解决方案
对于几乎所有的数据分析师或数据工程师来说,数据库是如何存储数据的直观而熟悉的解决方案。与关系型数据一样,数据库通常是处理时间序列数据的良好选择。特别是当你需要一个开箱即用的解决方案时,其中具有经典数据库特性之一:
这些是选择数据库而不是文件系统的充分理由,其中之一,特别是在处理新数据集时,你应该始终考虑数据库解决方案。数据库,特别是 NoSQL 数据库,可以帮助你保持灵活性。此外,与单独处理文件相比,数据库将使您的项目更快地启动,因为您将需要的大部分样板代码已经就位。即使最终决定采用文件存储解决方案(大多数人不会这样做),首先使用数据库可以帮助您确定在新的数据处理成熟时如何组织自己的文件结构。
在本节的其余部分,我们将介绍 SQL 和 NoSQL 数据库在时间序列中的各自优势,然后讨论目前流行的时间序列应用数据库选项。
好消息是,时间序列图表似乎是目前数据库领域中增长最快的类别,因此您可以期待在未来看到更多甚至更好的时间序列数据库解决方案选择。
SQL 与 NoSQL 的比较
SQL 与 NoSQL 在时间序列数据库社区中的辩论与更广泛的领域一样活跃。许多专家数据库管理员坚持认为 SQL 是唯一的选择,并且没有任何形式的数据不能通过一组良好的关系表格进行很好地描述。尽管如此,在实践中,当组织试图扩展 SQL 解决方案以适应大量时间序列数据时,通常会出现性能下降的情况,因此始终值得考虑 NoSQL 解决方案,特别是如果您正在寻找一个开放的解决方案,可以扩展以适应时间序列数据收集开始时看不到有限时间范围的情况。
尽管 SQL 和 NoSQL 解决方案都可以适用于时间序列数据,但我们首先通过探索时间序列数据与 SQL 数据库开发的数据类型之间的不同,来激励我们讨论将数据库逻辑应用于时间序列数据的困难。
最初启发 SQL 数据库的数据特征
通过回顾 SQL 解决方案的历史,我们可以最好地理解 SQL 式思维与时间序列数据之间的不匹配。SQL 解决方案基于事务数据,这是完全描述离散事件所需的任何数据。事务由反映许多主键的属性组成,例如产品、参与者、时间和交易的价值。请注意,时间可以作为一个主键存在,但只能作为众多主键之一,而不是作为信息的主要轴。
事务数据有两个重要特征,与时间序列需求大不相同:
-
现有数据点经常会更新。
-
数据访问有些随机,因为不需要底层排序。
时间序列数据的特征
时间序列数据详细记录了某物的整个历史,而事务记录仅告诉我们最终状态。因此,时间序列数据通常不需要更新,这意味着随机访问的写操作不是首要考虑的。
这意味着在设计数据库用于时间序列时,性能目标对几十年来 SQL 数据库设计的关键性并不重要。事实上,考虑到在设计时间序列数据库时的目标,我们有着非常不同的优先级,因为我们将如何使用时间序列数据。我们时间序列数据使用案例的主要特征包括:
这些特性支持使用 NoSQL 数据库,因为许多通用应用的 NoSQL 数据库提供了时间序列数据库所需的大部分功能,尤其是对写操作的强调优于读操作。从概念上讲,NoSQL 数据库与时间序列数据非常匹配,因为它们本身就反映了时间序列数据收集的各个方面,例如并非所有数据点都会收集所有字段。NoSQL 的灵活模式与时间序列数据天然契合。当前推动 NoSQL 数据库流行的很大一部分数据就是时间序列数据。
因此,开箱即用的 NoSQL 数据库在写操作方面往往比 SQL 数据库表现更好。图 5-1 展示了用于时间序列数据点插入(写操作)的常用 SQL 数据库与常用 NoSQL 数据库的性能比较。
![]()
图 5-1. 为了时间序列使用而设计的数据库的一个共同特征是,数据插入速率与数据库大小成正比。与传统的 SQL 存储解决方案相比,这是一个显著的优势。
如何在 SQL 和 NoSQL 之间做出选择
或许会让人觉得我在推动你使用 NoSQL,但 SQL 数据库也有许多良好的使用场景。在考虑自己的数据时,请记住这一适用于数据存储的原则,无论是在 SQL 数据库、NoSQL 数据库还是纯文本文件中:那些通常在同一时间被请求的数据应该存储在相同的位置。这是无论你的使用场景如何都是最重要的因素。
许多会议演示和博客文章把 NoSQL 解决方案宣扬为精确迎合高写入低更新情景的解决方案,其中数据不会随机排序。尽管如此,SQL 数据库仍然是一种可行且经常使用的时间序列存储选项。特别是,在一些对传统 SQL 数据库及其内存结构的重点进行架构变更后,这些挑战可以得到应对,同时保留 SQL 的一些优势。例如,如何简单且看似显而易见地构建 SQL 表的内存表示以考虑时间,可以显著提升性能。
最终,NoSQL 和 SQL 之间的区别高度依赖于实现,并不像它们被宣传的那样系统化或重要。让你的数据决定你选择这些技术中的某一个具体实现。当你考虑你的时间序列数据的属性和访问使用模式时,你可以记住一些一般的限制:
SQL 在时间序列中的优点
-
如果你的时间序列存储在 SQL 数据库中,你可以轻松地将其与存储在该数据库中的相关横截面数据关联起来。
-
分层时间序列数据与关系表是自然匹配的。适当的 SQL 模式集将帮助你组织相关的时间序列,并清晰地划分层次结构,而这些在 NoSQL 解决方案中可能会比较零散地分布。
-
如果你正在创建基于交易数据的时间序列,在这种情况下最好将数据存储在 SQL 数据库中,这样可以轻松进行验证、交叉引用等操作。
NoSQL 在时间序列中的优点
流行的时间序列数据库和文件解决方案
现在我们将讨论一些流行的时间序列数据的数据库解决方案。这些给你一种可用性的感觉,除了传统的 SQL 解决方案之外。请注意,这里讨论的技术占据了一个拥挤和碎片化的技术景观。今年常用的技术明年可能不那么流行。因此,这个讨论不应被视为具体的技术推荐,而更像是一组样本,帮助说明市场的当前状态。
时间序列专用数据库及相关监控工具
首先,我们讨论专门用于存储和监控时间序列数据的工具。特别是,我们看一下时间序列数据库(InfluxDB)和另一款性能监控工具,它也可以作为时间序列存储解决方案(Prometheus)。每个工具的优势必然反映了其独特的重点和使用模式。
InfluxDB
InfluxDB 是一个专门针对时间序列的数据库,根据其在 GitHub 项目网页上的描述:
InfluxDB 是一个开源时间序列数据库,用于记录度量、事件并进行分析。
在 InfluxDB 中,数据按时间序列组织。InfluxDB 中的数据点包括:
作为一个时序感知的数据库,InfluxDB 会自动为任何未带时间戳的数据点添加时间戳。此外,InfluxDB 使用类似于 SQL 的查询语言,例如:
SELECT * FROM access_counts WHERE value > 10000
InfluxDB 的其他优势包括:
还有许多其他的时间序列专用数据库;目前 InfluxDB 是最流行的,因此您最有可能遇到它。它提供的选项是时间序列数据存储中常见的期望属性。
作为一个数据库,InfluxDB 是一个推送式系统,这意味着在使用它时,您将数据推送到数据库进行摄取。这与我们将讨论的下一个选项 Prometheus 不同。
鉴于这些规格,像 InfluxDB 这样的解决方案从一开始就以时序感知的方式提供所有一般功能。这意味着您可以利用现有的 SQL 技能,同时还能从捕获但控制时序数据增长的需求中获益。最后,在捕获时序数据时,您还可以获得所需的快速写入。
Prometheus
Prometheus 描述自己 为一个“监控系统和时间序列数据库”,通过 HTTP 工作。这个描述表明它的一般重点:首先是监控,其次是存储。Prometheus 的一个巨大优势在于它是一个拉取式系统,这意味着用于拉取数据以创建时间序列的逻辑以及拉取的频率可以轻松调整和检查。
Prometheus 在紧急情况下是一个很好的资源,因为它是原子性和自我依赖的。然而,由于其拉取式架构,不能保证完全实时或准确。虽然它应该是快速和粗糙性能监控的首选技术,但不适用于数据必须是 100%准确的应用程序。
Prometheus 使用称为 PromQL 的功能表达语言进行查询:
access_counts > 10000
Prometheus 还通过 PromQL 提供了一个 API,用于许多常见的时间序列任务,甚至包括如何做出预测(predict_linear())和计算时间序列的每单位时间增长率(rate())等复杂功能。通过简单的接口也提供了时间段内的聚合。相比于 InfluxDB,Prometheus 更强调监控和分析,因此自动化数据整理功能较少。
Prometheus 是一个有用的时间序列存储解决方案,特别适用于实时流应用和数据可用性至关重要的场景。由于其自定义脚本语言和较少类似数据库的架构与 API,它具有较陡的学习曲线,但仍被许多开发人员广泛使用和喜爱。
通用 NoSQL 数据库
虽然时间序列特定的数据库提供了许多优势,但您也可以考虑更通用的 NoSQL 数据库的情况。这些数据库基于文档结构而不是表结构,并且通常不具备专门用于时间序列的显式功能。
尽管 NoSQL 数据库的灵活模式对于时间序列数据仍然很有用,特别是对于数据集的生命周期中数据收集的节奏和输入通道数量可能会发生变化的新项目。例如,时间序列可能最初只包含一个数据通道,但逐渐增加到包含更多种类的数据,所有数据都有时间戳。后来,可能会决定一些输入通道并不特别有用,它们可能会被停用。
在这种情况下,将数据存储在 SQL 表中将会因几个原因而困难,并导致大量 NaN,即数据不可用时的标记。相比之下,NoSQL 数据库在数据不可用时会简单地保留这些通道的缺失,而不是在矩形数据存储中标记大量 NaN。
一种受欢迎且性能出色的 NoSQL 时间序列数据库是 MongoDB。Mongo 特别关注其作为时间序列数据库的价值,并积极推动开发适合物联网的架构和指导。它提供了高级聚合特性,可用于按时间和与时间相关的分组的聚合操作,并提供了许多自动化流程来将时间分割为人类相关的标记,例如星期几或每月的日期:
-
$dayOfWeek
-
$dayOfMonth
-
$hour
此外,Mongo 专注于展示如何处理时间序列的广泛文档工作。¹ 一套准备好的文档和机构专注于时间序列数据明确意味着用户可以期待这个数据库会继续开发更多面向时间的功能。
然而,比所有这些功能更有用的是 Mongo 在随时间演变的架构上的灵活性。如果你在处理不断变化的数据收集实践的快速演变的时间序列数据集上工作,这种架构的灵活性将为你节省大量麻烦。
例如,想象一下一家医疗创业公司可能发生的时间序列数据收集实践:
-
你的创业公司推出了一款血压应用,并只收集两个指标,收缩压和舒张压,鼓励用户每天多次测量…
-
但是你的用户希望得到增强的生活方式建议,并且他们乐意为你提供更多的数据。用户提供从他们的生日到每月体重记录再到每小时步数和卡路里计数等一切。显然,这些不同类型的数据以截然不同的节奏进行收集…
-
但后来你意识到一些数据并不是很有用,所以你停止了收集它…
-
但是即使你不再使用,你的用户也会想念你的数据显示,所以你重新开始收集最受欢迎的数据…
-
然后,你最大市场之一的政府通过了关于健康数据存储的法律,你需要清除那些数据或加密它,因此你需要一个新的加密字段…
-
并且变化还在继续。
当你需要像这样的应用程序中描述的查询和架构灵活性时,你可以选择一个提供了合理平衡的通用 NoSQL 数据库,既具有时间特定性又具有更一般灵活性。
与时序特定数据库相比,通用 NoSQL 数据库的另一个优势是你可以更轻松地将非时序数据集成到同一数据库中,以便跨数据集引用相关数据。有时候,通用 NoSQL 数据库恰好是性能考量和类似 SQL 功能的完美结合,而无需为时序功能优化 SQL 数据库架构的聪明办法。
在本节中,我们已经研究了 NoSQL 数据库解决方案²,并调查了一些当前流行的选项。虽然我们可以预期主导市场的特定技术随着时间的推移而演变,但这些技术操作的一般原则和它们提供的优势将保持不变。作为一个快速回顾,我们在不同类型的数据库中发现的一些优势包括:
-
高读取或写入容量(或两者兼有)
-
灵活的数据结构
-
推送或拉取数据摄取
-
自动化数据修剪流程
时序数据库专用于时序特定任务,可以提供最多的开箱即用自动化功能,但其模式灵活性较低,整合相关的横截面数据的机会也较少,相比之下,通用的 NoSQL 数据库更具有这些特点。
一般而言,数据库比平面文件存储格式提供更大的灵活性,但这种灵活性意味着数据库不如简单的平面文件存储格式流畅,并且在 I/O 性能上也较差,接下来我们将讨论这一点。
文件解决方案
总而言之,数据库是一种集成了脚本和数据存储功能的软件。它本质上是一个包装在特殊软件中的平面文件,负责使该文件尽可能安全且易于使用。
有时候,摆脱这个外部层并全面承担数据存储的责任是有意义的。虽然这在商业应用中并不常见,但在科学研究中经常这样做,在极少数的工业应用(如高频交易)中,速度至关重要。在这种情况下,分析师将设计一个更复杂的数据管道,涉及分配存储空间、打开文件、读取文件、关闭文件、保护文件等,而不仅仅是编写一些数据库查询。
如果符合以下任何条件,平面文件解决方案是一个不错的选择:
在本节中,我们简要概述了一些常见的平面文件解决方案。你还应该记住,你始终可以创建自己的文件存储格式,尽管这相当复杂,并且通常只有在你使用高性能语言(如 C++或 Java)时才值得这样做。
如果你的系统已经成熟且对性能要求敏感到足以支持平面文件系统,那么即使可能需要将数据迁出数据库,实施平面文件系统也有几个优点。这些优点包括:
-
平面文件格式与系统无关。如果需要共享数据,只需以已知的可共享格式提供文件即可。无需请求合作者远程访问你的数据库或设置他们自己的镜像数据库。
-
平面文件格式的 I/O 开销必然比数据库少,因为这只涉及对平面文件的简单读取操作,而不是对数据库的检索和读取操作。
-
平面文件格式编码了数据应该读取的顺序,而并非所有数据库都会这样做。这强制执行了数据的串行读取,这在某些情况下是可取的,比如用于深度学习训练体制。
-
与数据库相比,您的数据将占用比较小的内存空间,因为您可以最大程度地利用压缩机会。相关地,您可以调整数据压缩程度,以明确平衡在应用程序中最小化数据存储占用和 I/O 时间的愿望。更多的压缩将意味着更小的数据占用,但更长的 I/O 等待时间。
NumPy
如果您的数据纯粹是数字,一种广泛使用的选择是 Python NumPy 数组。NumPy 数组可以轻松保存为各种格式,并且有许多基准出版物比较它们的相对性能。例如,array_storage_benchmark GitHub 仓库 专为测试各种 NumPy 文件格式的效率和速度而设计。
NumPy 数组的缺点是它们只有一个数据类型,这意味着您不能自动存储异构时间序列数据,而必须考虑是否可以将单一数据类型用于原始或处理后的数据(尽管有办法绕过此限制)。另一个缺点是,NumPy 数组不自然地为行或列添加标签,因此没有直接的方法为数组的每一行添加时间戳,例如。
使用 NumPy 数组的优点是可以使用多种选项保存,包括占用空间更小并且比数据库解决方案具有更快 I/O 的压缩二进制格式。从分析和存储的角度来看,它也是一种开箱即用的性能良好的数据结构。
Pandas
如果您希望轻松标记数据或轻松存储异构时间序列数据(或两者兼有),请考虑不那么流畅但更灵活的 Pandas 数据框架。当您有一个时间序列,其中包含许多不同类型的数据(例如事件计数(整数)、状态测量(浮点数)和标签(字符串或独热编码))时,Pandas 数据框架特别有用。在这种情况下,您可能希望坚持使用 Pandas 数据框架(还要记住,“pandas”这个名字实际上来自于“面板数据”的省略,因此对许多用例来说,这是一种自然的格式)。
Pandas 数据框架被广泛使用,有多个在线资源可用于比较用于存储此类数据的各种格式。
标准 R 等效物
存储 R 对象的本地格式是 .Rds 和 .Rdata 对象。这些都是二进制文件格式,因此它们在压缩和 I/O 方面必然比基于文本的格式更有效率,从这个角度来看,它们类似于 Python 中的 Pandas 数据框架。相关地,feather 格式 可用于在 R 和 Python 中以语言无关的文件格式保存数据框架。对于 R 用户来说,本地二进制格式(当然)是性能最佳的。表格 5-3 比较了文件格式选项。
表格 5-3. 文件格式选项的大小和性能比较
| 格式名称 |
相对大小 |
相对加载时间 |
| .RDS |
1 倍 |
1 倍 |
| feather |
2 倍 |
1 倍 |
| csv |
3 倍 |
20 倍 |
正如我们所见,本地格式明显是减少存储空间和 I/O 的优胜者,而那些使用文本文件格式而不是二进制文件的人在存储和 I/O 减速方面会付出沉重代价。
Xarray
当你的时间序列数据扩展到许多维度时,可能是时候考虑更工业化的数据解决方案了,即Xarray,这有多个理由是有用的:
-
命名维度
-
像 NumPy 这样的向量化数学操作
-
类似 Pandas 的分组操作
-
允许根据时间范围进行索引的类似数据库的功能
-
多种文件存储选项
Xarray 是一种支持许多时间序列特定操作的数据结构,例如在时间上索引和重采样、插值数据以及访问日期时间的各个组成部分。Xarray 作为一个高性能科学计算工具构建,对于时间序列分析应用来说被严重低估和轻视。
Xarray 是 Python 中的一种数据结构,它提供了多种存储选项。它实现了 pickle 和另一种称为netCDF的二进制文件格式,这是一种跨平台和多语言支持的通用科学数据格式。如果你希望在 Python 中提升你的时间序列应用能力,Xarray 是一个不错的起点。
正如您所见,有许多选项用于时间序列数据的平面文件存储,一些选项带有相关功能(如 Xarray),一些选项则具有非常简化的纯数字格式(如 NumPy)。当将数据集从面向数据库的流水线迁移到面向文件的流水线时,将会有一些与简化数据和重写脚本以将逻辑从数据库移出并放入显式 ETL(提取-转换-加载)脚本相关的成长阵痛。在性能至关重要的情况下,将数据移到文件中很可能是减少延迟的最重要一步。常见的希望采取此方法的情况包括:
另一方面,对于许多应用程序而言,数据库的便利性、可扩展性和灵活性远远超过了较高的延迟。你的最佳存储方案将在很大程度上取决于你存储的数据的性质以及你想要做什么。
更多资源
-
关于时间序列数据库技术:
Jason Moiron,《关于时间序列数据库的思考》,jmoiron 博客,2015 年 6 月 30 日,https://perma.cc/8GDC-6CTX。
这篇 2015 年的经典博文提供了一个早期时间段与时间序列数据库以及记录一切的崇拜之间的一瞥。这篇关于存储时间序列数据库选项和典型用例的高层次概述对于数据库管理和工程初学者非常有益。
Preetam Jinka, “时间序列数据库列表,” Misframe 博客,2016 年 4 月 9 日,https://perma.cc/9SCQ-9G57.
这份时间序列数据库的长列表经常更新,展示目前市场上的数据库。每个数据库条目都有重点介绍,说明特定数据库与竞争对手和前辈的关系。
Peter Zaitsev, “Percona 博客调查:您使用哪种数据库引擎存储时间序列数据?” Percona 博客,2017 年 2 月 10 日,https://perma.cc/5PXF-BF7L.
这项 2017 年的数据库工程师调查显示,关系数据库(SQL 数据库)继续作为一个群体占据主导地位,35% 的受访者表示他们使用这些数据库来存储时间序列数据。ElasticSearch、InfluxDB、MongoDB 和 Prometheus 也是受欢迎的选择。
Rachel Stephens, “时间序列数据库市场现状,” RedMonk,2018 年 4 月 3 日,https://perma.cc/WLA7-ABRU.
一位技术分析师最近的数据驱动撰写描述了通过对 GitHub 和 Stack Overflow 活动的实证调查,时间序列数据库存储的最流行解决方案。报告还显示由于时间序列数据的广泛用例以及数据库分段化的趋势,时间序列存储领域存在较高的分散度。
Prometheus.io, “Prometheus 文档:与其他备选方案的比较,” 无日期,https://perma.cc/M83E-NBHQ.
这份极其详细和全面的列表将 Prometheus 与其他流行的时间序列存储解决方案进行比较。您可以将此模型用作快速参考,了解与 Prometheus 替代方案相关的主导数据结构和存储结构。这是一个了解当前提供的选择以及各种为时序数据设计的数据库之间权衡的好地方。
-
关于调整通用数据库技术:
Gregory Trubetskoy, “高效在 PostgreSQL 中存储时间序列,” Notes to Self 博客,2015 年 9 月 23 日,https://perma.cc/QP2D-YBTS.
这篇较老但仍然相关的博文解释了如何以性能为重点在 Postgres 中存储时间序列。Trubetskoy 解释了“朴素”方法(数值列和时间戳列)的困难,并给出了基于 Postgres 数组的实用建议。
Josiah Carlson, “使用 Redis 作为时间序列数据库,” InfoQ,2016 年 1 月 2 日,https://perma.cc/RDZ2-YM22.
本文提供了有关如何使用 Redis 作为时间序列数据库的详细建议和示例。虽然 Redis 自创建以来一直被用于时间序列数据,但作者指出了几个需要注意的地方,以及可以在许多时间序列使用案例场景中应用的优势数据结构。这篇文章不仅对于学习如何使用 Redis 处理时间序列数据很有帮助,而且作为如何将更一般的工具重新用于时间序列特定用途的示例也非常有用。
迈克·弗里德曼,“时间序列数据:为什么(以及如何)使用关系数据库而不是 NoSQL”,Timescale 博客,2017 年 4 月 20 日,https://perma.cc/A6CU-6XTZ。
TimescaleDB 的创始人在这篇博客文章中描述了他的团队如何构建一个具有特定于时间数据的内存布局修改的时间序列数据库作为关系数据库。TimescaleDB 认为传统 SQL 数据库在处理时间序列数据时的主要问题是不可扩展性,导致数据在内存中进出进行时间相关查询时性能缓慢。TimescaleDB 建议布置内存和内存映射以反映数据的时间性质,并减少将不同数据交换到内存中和从内存中交换出去的次数。
希尔马尔·布赫塔,“将多个表与有效时间范围合并为单个维度”,Oraylis 博客,2014 年 11 月 17 日,https://perma.cc/B8CT-BCEK。
虽然标题不太吸引人,但对于我们这些不是 SQL 数据库专家的人来说,这篇博客文章非常有用。它展示了一种处理标记为仅在特定时间范围内有效的数据及如何跨多个表进行联合的良好方式。这是一个令人惊讶地常见且难以解决的问题。所涉及的示例包括几个人力资源图表,包括雇员的雇佣有效日期、给定部门的有效日期、特定办公室位置的有效日期以及公司汽车的有效日期。一个非常可能的任务是确定某一天雇员的状态—该人是否受雇,并且在哪个部门、在哪个位置,并且是否有公司汽车?这听起来很直观,但要回答这个问题所需的 SQL 查询却很困难,在这篇文章中有详细的覆盖。
¹ 例如,参见“MongoDB 中的时间序列数据模式设计”,“时间序列数据与 MongoDB:第一部分 – 简介”,以及“时间序列数据与 MongoDB:第二部分 – 模式设计最佳实践”。相反的观点请参考“如何在 MongoDB 中存储时间序列数据,以及为什么这是个坏主意”。
² SQL 数据库针对时间序列数据的优化超出了本书的范围,并且往往非常具体于使用情况。本章末尾的一些资源解答了这个问题。
第六章:时间序列统计模型
在本章中,我们研究了一些时间序列的线性统计模型。这些模型与线性回归有关,但考虑到同一时间序列中数据点之间的相关性,与应用于横断面数据的标准方法形成对比,在后者中假设样本中的每个数据点独立于其他数据点。
我们将讨论的具体模型包括:
这些模型传统上是时间序列预测的主力军,并继续在从学术研究到工业建模的广泛情境中应用。
为什么不使用线性回归?
作为数据分析师,您可能已经对线性回归非常熟悉。如果不熟悉,可以定义如下:线性回归假设您拥有独立同分布(iid)的数据。正如我们在前几章节中详细讨论的那样,这在时间序列数据中并非如此。在时间序列数据中,接近时间的点往往彼此强相关。事实上,如果没有时间相关性,时间序列数据几乎无法用于传统的时间序列任务,如预测未来或理解时间动态。
有时,时间序列教程和教科书会给人一种错误的印象,即线性回归对于时间序列没有用处。他们让学生认为简单的线性回归根本行不通。幸运的是,这完全不是事实。普通最小二乘线性回归可以应用于时间序列数据,只要以下条件成立:
关于时间序列行为的假设
关于误差的假设
如果这些假设成立,那么普通最小二乘回归是系数的无偏估计,即使对于时间序列数据也是如此。¹ 在这种情况下,估计量的样本方差具有与标准线性回归相同的数学形式。因此,如果你的数据符合刚刚列出的假设,你可以应用线性回归,这无疑会帮助你提供时间序列行为的清晰和简单的直觉。
刚刚描述的数据要求类似于应用于横截面数据的标准线性回归的要求。我们增加的是对数据集时间特性的强调。
警告
不要强行使用线性回归。当你的数据不满足所需的假设时,应用线性回归可能会产生一些后果:
当合适时,线性回归可以提供简单和透明性,但错误的模型肯定不是透明的!
可以质疑时间序列分析师是否过于严格地应用标准线性回归所需的假设,以至于不能使用线性回归技术。现实世界的分析师偶尔在模型假设上取得一些自由是有益的,只要了解这样做的潜在风险。
遵守模型假设的重要性在很大程度上取决于领域。有时候,模型会在明知其基本假设未被满足的情况下应用,因为相对于回报来说,后果并不严重。例如,在高频交易中,尽管没有人相信数据严格遵循所有标准假设,线性模型因为多种原因而非常受欢迎。²
什么是无偏估计量?
如果一个估计值既不是过高也不是过低,那么它使用的是一个无偏估计量。这通常是一件好事,尽管你应该注意偏差-方差权衡,这是对统计和机器学习问题的描述,在这些问题中,参数估计偏差较小的模型往往会有更高的参数估计方差。参数估计的方差反映了估计在不同数据样本中的变化程度。
如果你发现自己的情况适合使用线性回归进行预测任务,考虑利用tslm(),这是forecast包中设计的提供时间序列数据的简易线性回归方法的函数。
为时间序列开发的统计方法
我们考虑专门为时间序列数据开发的统计方法。我们首先研究为单变量时间序列数据开发的方法,从自回归模型的非常简单的情况开始,即一个模型,它表示时间序列的未来值是其过去值的函数。然后,我们逐步深入复杂的模型,最后讨论多元时间序列的向量自回归以及一些额外的专门的时间序列方法,例如 GARCH 模型和层次建模。
自回归模型
自回归(AR)模型依赖于过去预测未来的直觉,因此假设一个时间序列过程,在这个过程中,时间t的值是该系列在早期时间点的值的函数。
我们对这个模型的讨论将更为详细,以便让您了解统计学家如何考虑这些模型及其特性。因此,我们从一个相当详细的理论概述开始。如果您对如何推导时间序列的统计模型特性不感兴趣,可以略读这部分内容。
使用代数来理解 AR 过程的约束条件。
自回归看起来就像许多人在尝试拟合时间序列时会使用的第一种方法,特别是如果除了时间序列本身外没有其他信息的话。它确实如其名称所示:对过去值进行回归以预测未来值。
最简单的 AR 模型,即 AR(1)模型,描述如下:
在时间t的系列值是一个常数b[0]、其前一个时间步的值乘以另一个常数 和一个也随时间变化的误差项e[t]的函数。假定这个误差项具有恒定的方差和均值为 0。我们将仅回顾前一个时间点的自回归项称为 AR(1)模型,因为它包括一个一期滞后的查看。
顺便说一句,AR(1)模型与仅有一个解释变量的简单线性回归模型具有相同的形式。也就是说,它映射为:
如果我们知道b[0]和b[1]的值,我们可以计算y[t]的期望值和方差,给定y[t–1]。参见 Equation 6-1.³
方程式 6-1.
此表示法的推广允许 AR 过程的当前值依赖于最近的 p 个值,从而产生 AR(p) 过程。
现在我们转向更传统的符号表示,使用 ϕ 表示自回归系数:
如 第三章 中讨论的,稳定性是时间序列分析中的关键概念,因为许多时间序列模型,包括 AR 模型,都需要它。
我们可以从稳定性的定义中确定 AR 模型保持稳定的条件。我们继续关注最简单的 AR 模型,即 AR(1) 在 方程 6-2 中。
方程 6-2.
我们假设过程是稳定的,然后“向后”推导看看这对系数意味着什么。首先,根据稳定性的假设,我们知道过程的期望值在所有时间点上必须相同。我们可以按照 AR(1) 过程的方程重新表述 y[t]:
根据定义,e[t] 的期望值为 0。此外,phises 是常数,因此它们的期望值就是它们的恒定值。方程 6-2 在左侧简化为:
以及右侧为:
这简化为:
这反过来意味着 (方程 6-3)。
方程 6-3.
因此,我们找到了过程的均值与基础 AR(1) 系数之间的关系。
我们可以采取类似的步骤来查看恒定方差和协方差如何对 ϕ 系数施加条件。我们首先用 ϕ[0] 的值代替,我们可以从 方程 6-3 推导出 方程 6-4。
方程 6-4.
到 方程 6-2:
如果你检查方程 6-4,你会发现左右两边的表达式非常相似,即 和 。考虑到这个时间序列是平稳的,我们知道时间 t 的数学应与时间 t–1 的数学相同。我们将方程 6-4 重新写成一个时间步骤较早的方程 6-5。
方程式 6-5.
我们可以将其代入方程 6-4 如下:
我们为了清晰起见重新排列方程 6-6。
方程 6-6.
你应该注意到在方程 6-6 中还可以进行另一次替换,使用我们早期使用的同样递归替换,但这次不是在 y[t–1] 上工作,而是在 y[t–2] 上工作。如果你进行这种替换,模式就变得清晰了。
因此,我们可以更一般地得出结论, 。
用简单的英语来说,y[t] 减去过程均值是误差项的线性函数。
结果可以用来计算给定的期望值,假设在不同t值上的值是独立的。由此我们可以得出结论,y[t–1]和e[t]的协方差为 0,这正如预期的那样。我们可以类似地应用逻辑来计算y[t]的方差通过平方这个等式:
因为由于平稳性方程两边的方差数量必须相等,所以(var(y[t]) = var(y[t] – 1),这意味着:
鉴于方差根据定义必须大于或等于 0,我们可以看到,必须小于 1,以确保在前述方程的右侧有正值。这意味着对于平稳过程,我们必须有 –1 < ϕ[1] < 1。这是弱平稳的必要和充分条件。
我们研究了 AR(1)过程,因为它是最简单的自回归过程。在实践中,您将一直拟合更复杂的模型。可以推导出任意阶 AR(p)过程的类似平稳条件,并且有许多书籍在其中进行了演示。如果您有兴趣更详细地了解这一点,请查看本章末尾列出的资源。从这次讨论中最重要的收获是,通过一些代数和统计学,时间序列是非常易于理解的,并且平稳性不仅仅是绘制模型的问题,而是可以根据任何给定统计模型的具体情况进行计算的数学概念。
提示
分布是描述由过程生成特定值的所有可能值的概率的统计函数。虽然您可能尚未正式遇到此术语,但无疑已经遇到了此概念。例如,考虑钟形曲线,它是一个概率分布,指出大多数测量值将接近并均匀分布在平均值的两侧。在统计学中通常称为正态或高斯分布。
选择 AR(p)模型的参数
要评估 AR 模型对数据的适用性,请从绘制过程及其偏自相关函数(PACF)开始。AR 过程的 PACF 应在 AR(p)过程的阶数p之外截尾为零,从而在数据中以实证方式给出 AR 过程的阶数的明确和视觉指示。
另一方面,AR 过程不会有信息性的自相关函数(ACF),尽管它将具有 ACF 的特征形状:随着时间偏移增加呈指数衰减。
让我们通过一些实际数据来看看这个。我们使用了发布在UCI 机器学习库中的一些需求预测数据。
首先,我们按时间顺序绘制数据(图 6-1)。由于我们将其建模为 AR 过程,我们查看偏自相关函数以设置过程的阶数阈值(图 6-2)。
![]()
图 6-1. 每日银行订单数(2)。
![]()
图 6-2. 未转换订单时间序列的偏自相关图,如图 6-1 所示。
我们可以看到 PACF 的值在滞后 3 处穿过了 5%的显著性阈值。这与 R 语言stats包中的ar()函数的结果一致。如果没有指定自回归模型的阶数,ar()函数会自动选择其阶数:
## R
> fit <- ar(demand[["Banking orders (2)"]], method = "mle")
> fit
Call:
ar(x = demand[["Banking orders (2)"]], method = "mle")
Coefficients:
1 2 3
-0.1360 -0.2014 -0.3175
如果我们查看ar()函数的文档,我们可以看到所选的阶数是根据赤池信息准则(AIC)确定的(我们保持了默认参数未改变)。这是有帮助的,因为它显示我们通过检查 PACF 所做的视觉选择与通过最小化信息准则进行的选择是一致的。这两种选择模型阶数的方式是不同的,但在这种情况下它们是一致的。
注意,ar()函数还为我们提供了模型的系数。然而,我们可能希望限制这些系数。例如,观察 PACF,我们可能会想知道是否真的要包括滞后-1 项的系数,或者是否应该将该项的系数指定为强制的 0,因为其 PACF 值远低于显著性阈值。在这种情况下,我们还可以使用stats包中的arima()函数。
在这里,我们展示了如何调用函数来拟合 AR(3)模型,通过将阶数参数设置为c(3, 0, 0),其中 3 指的是 AR 分量的阶数(在后续示例中,我们将指定本章后几页中涵盖的差分和移动平均参数的其他分量):
## R
> est <- arima(x = demand[["Banking orders (2)"]],
> order = c(3, 0, 0))
> est
Call:
arima(x = demand[["Banking orders (2)"]], order = c(3, 0, 0))
Coefficients:
ar1 ar2 ar3 intercept
-0.1358 -0.2013 -0.3176 79075.350
s.e. 0.1299 0.1289 0.1296 2981.125
sigma² estimated as 1.414e+09: log likelihood = -717.42,
aic = 1444.83
要在我们的模型中注入先验知识或观点,我们可以将一个系数约束为 0。例如,如果我们想要在我们的模型中将滞后-1 项的系数约束为 0,我们可以使用以下调用:
## R
> est.1 <- arima(x = demand[["Banking orders (2)"]],
> order = c(3, 0, 0),
> fixed = c(0, NA, NA, NA))
> est.1
Call:
arima(x = demand[["Banking orders (2)"]],
order = c(3, 0, 0),
fixed = c(0, NA, NA, NA))
Coefficients:
ar1 ar2 ar3 intercept
0 -0.1831 -0.3031 79190.705
s.e. 0 0.1289 0.1298 3345.253
sigma² estimated as 1.44e+09: log likelihood = -717.96,
aic = 1443.91
将传递给arima函数的固定参数向量中的一个值设置为 0 而不是NA,将约束该值保持为 0:
## R
> fixed <- c(0, NA, NA, NA)
我们现在检查我们模型在训练数据上的表现,以评估我们模型对这个数据集的拟合程度。我们可以通过两种方式来做这件事。首先,我们绘制残差的自相关函数(即误差),以查看是否存在模型未覆盖的自相关模式。
由于arima()函数的输出,绘制残差非常简单(见图 6-3):
## R
> acf(est.1$residuals)
![]()
图 6-3. 我们拟合的 AR(3)模型的残差的 ACF,强制滞后 - 1 参数为 0。
ACF 的值都未超过显著性阈值。当然,我们不应盲目依赖显著性阈值来评估或拒绝显著性,但这一观察结果是我们已认为合理的模型中的一个有用数据点。
我们在这里的残差(即误差项)中没有看到自相关的模式。如果我们看到这样的模式,我们可能会希望回到原始模型,并考虑包括额外的项以增加复杂性,以解释残差的显著自相关。
另一个常见的测试是Ljung-Box 测试,这是对时间序列随机性的整体测试。更正式地提出了以下零假设和备择假设:
-
H0: 数据不表现出串行相关。
-
H1: 数据表现出串行相关。
这个测试通常适用于 AR(更广义地说,是 ARIMA)模型,更具体地说是适用于模型拟合的残差而不是模型本身:
## R
> Box.test(est.1$residuals, lag = 10, type = "Ljung", fitdf = 3)
Box-Ljung test
data: est.1$residuals
X-squared = 9.3261, df = 7, p-value = 0.2301
我们对我们的est.1模型应用 Ljung-Box 测试来评估其拟合优度。我们无法拒绝数据不表现出串行相关的零假设。这证实了我们刚刚通过绘制残差的 ACF 发现的结果。
使用 AR(p)过程进行预测
在接下来的章节中,我们将说明如何使用 AR 过程进行预测。我们首先探讨了一步预测的情况,然后讨论了多步预测与进一步情况的区别。好消息是从编码的角度来看,并没有太大的差异,尽管后者的基础数学更为复杂。
向前预测一步
首先考虑我们想要用已知(或估计)的 AR 模型预测一步的情况。在这种情况下,我们实际上拥有所有所需的信息。
我们继续使用需求数据的模型,将滞后 - 1 系数约束为 0(之前作为est.1拟合)。
我们使用forecast包中的fitted()函数绘制预测图。这里是完整的代码;这样做非常简单:
## R
> require(forecast)
> plot(demand[["Banking orders (2)"]], type = 'l')
> lines(fitted(est.1), col = 3, lwd = 2) ## use the forecast package
这导致了图 6-4 所示的绘图结果。
![]()
图 6-4. 这里我们看到实线表示的原始时间序列和虚线表示的拟合时间序列。
AR(p)模型是移动窗口函数。
我们除了使用forecast包的拟合功能来从我们的 AR(3)模型生成预测之外,还有一种选择:我们可以更明确地手工编写一个预测,通过使用zoo包的rollapply()函数来实现,这是我们之前讨论过的。该函数可以计算窗口函数,也可以计算 AR 过程。为此,我们将从ar()拟合中获取系数,并将这些权重应用于表示不同滞后值的输入向量,以在每个点上生成预测。我留给你作为一个练习。
现在让我们考虑预测的质量。如果我们计算预测值与实际值之间的相关性,我们得到 0.29。在某些情境下这并不差,但请记住,有时候对数据进行差分会消除看似强关系,取而代之的是一种基本上是随机的关系。
如果我们在拟合数据时数据确实不是真正稳定的,那么未识别的趋势就会伪装成良好的模型性能,而实际上它是我们在建模之前应该解决的数据特征。
我们可以对系列数据和预测值同时进行差分,以查看模型是否能够良好地预测一个时间段到下一个时间段的变化。即使在差分之后,我们的预测和数据显示出相似的模式,这表明我们的模型是有效的(参见图 6-5)。
![]()
图 6-5。差分系列和差分预测之间存在强相关性,表明模型确实识别出了一种潜在的关系。
我们还可以通过绘制差分系列并观察其相关性来测试我们是否在同一时间预测相同的移动。此图表显示了一些相关性,我们可以通过计算相关值来确认。即使用于预测从一个时间步到下一个时间步的变化,该模型也能够正常工作。
回顾预测与实际值的原始图表,我们可以看到预测与数据之间的主要区别在于预测的变化幅度比数据小。它可能能够正确预测未来的方向,但无法准确预测不同时间段之间的变化幅度。这本身并不是问题,而是反映了预测是预测分布的均值,因此必然比采样数据具有更低的变异性。
统计模型的这一属性有时会在快速可视化的便利性中被忽视,这些可视化往往倾向于表明未来会比实际情况更加稳定。在呈现可视化数据时,请务必提醒观众正在绘制的内容。在这种情况下,我们的预测表明未来会比实际情况平稳得多。
预测未来多个时间步
到目前为止,我们已经完成了单步预测。但是,我们可能希望进一步预测未来更长的时间。让我们想象一下,我们想要生成一个两步预测,而不是一个步预测。我们首先会生成一个步预测,然后使用这个预测值来确定我们需要预测的 的值,以便预测 。
请注意,在我们当前的模型中,从一步预测转变为两步预测实际上不需要这些复杂的操作,因为在预测 y[t] 时并没有使用 y[t – 1] 的值。我们知道所有需要知道的内容来进行两步预测,并且不需要估计。事实上,我们应该得到与一步预测相同的一系列值——不会有新的错误或变异源。
然而,如果我们希望预测更远的时间,我们需要生成预测的未来值作为预测的输入。让我们预测 y[t+3]。这将涉及一个模型,其系数依赖于 y[t+1] 和 y[t]。因此,我们需要预测这两个值——y[t + 1] 和 y[t]——然后依次使用这些估计值来预测 y[t+3]。与之前一样,我们可以使用 forecast 包中的 fitted() 函数来实现这一点——在编码上并不比单步预测更难。正如之前提到的,这也可以通过 rollapply() 方法来完成,但这样做需要更多的工作并且更容易出错。
现在我们使用 fitted() 函数,增加了一个额外的参数 h 作为预测的时间跨度。提醒一下,我们的对象 est.1 表示一个 AR(3) 过程,其滞后 -1 (时间减一) 的系数被限制为 0:
## R
> fitted(est.1, h = 3)
我们可以利用预测多个时间步骤的便利性,为不同的时间跨度生成多步预测。在下一个例子中,我们可以看到从同一基础模型生成的预测值随着前瞻时间增加而增加的方差。(注意,在显示中,四舍五入和逗号分隔修改了原始输出,以便更清楚地看到随着前瞻时间增加时估计方差的变化。)
正如你在图 6-6 中所见,随着预测时间的增加,预测的方差减少。这突显了模型的一个重要限制,即随着时间推移,实际数据的影响越来越小,因为输入数据的系数仅考虑有限的前几个时间点(在此模型中,仅回溯到滞后 - 3;即时间 - 3)。可以这样说,随着时间的推移,预测越来越接近无条件预测,即不受数据条件的影响。未来的预测接近于系列的均值,因此误差项和预测值的方差收缩为 0:
## R
> var(fitted(est.1, h = 3), na.rm = TRUE)
[1] 174,870,141
> var(fitted(est.1, h = 5), na.rm = TRUE)
[1] 32,323,722
> var(fitted(est.1, h = 10), na.rm = TRUE)
[1] 1,013,396
> var(fitted(est.1, h = 20), na.rm = TRUE)
[1] 1,176
> var(fitted(est.1, h = 30), na.rm = TRUE)
[1] 3.5
对于未来足够遥远的预测将仅仅预测过程的均值,这是有道理的。在未来某个时间点,我们当前的数据不会给我们提供与未来相关的具体合理信息,因此我们的预测逐渐回归到已知过程的基本特性,比如其均值。
![]()
图 6-6. 对未来的预测绘图。随着预测时间的延长,y 轴的值范围越来越窄,模型越来越接近提供过程均值的常数预测。预测的视角从上到下分别是 3、10 和 30 个时间步。
从中要记住的重要一点是,AR(以及 MA、ARMA 和 ARIMA,稍后将讨论)模型最适合进行短期预测。这些模型对于较长时间段的预测失去了预测能力。
对于剩余的模型,我们将进行类似的处理,尽管总体上的细节较少。关于时间序列分析的所有标准教科书都有详细讨论。
移动平均模型
移动平均(MA)模型依赖于一个过程的图像,在这个过程中,每个时间点的值是最近过去值的“误差”项的函数,每个项相互独立。我们将在同样的步骤中回顾这个模型,就像我们研究 AR 模型一样。
AR MA 等效性
在许多情况下,一个 MA 过程可以被表达为一个无限阶的 AR 过程。同样地,在许多情况下,一个 AR 过程可以被表达为一个无限阶的 MA 过程。要了解更多,请查看MA 过程的可逆性,Wold 表示定理,以及一般的MA/AR 对偶性过程。这涉及到的数学远远超出了本书的范围!
该模型
移动平均模型可以类似于自回归模型来表达,只是线性方程中包含的项是现在和过去的误差项,而不是过程本身的现在和过去的值。因此,一个阶数为 q 的 MA 模型表示为:
警告
不要将 MA 模型与移动平均混淆。它们并不相同。一旦您知道如何拟合移动平均过程,甚至可以将 MA 模型的拟合与底层时间序列的移动平均进行比较。我留这个作为一个练习给你。
经济学家将这些误差项称为系统的“冲击”,而具有电气工程背景的人则可能将其视为一系列脉冲和模型本身作为有限脉冲响应滤波器,这意味着任何特定脉冲的影响仅保留有限时间。措辞并不重要,但许多独立事件在不同的过去时间影响当前过程值,每个事件都作出个别贡献,这是主要思想。
MA 模型根据定义是弱平稳的,无需对其参数施加任何约束。这是因为 MA 过程的均值和方差都是有限的,并且随时间不变,因为误差项被假定为均值为 0 的独立同分布。我们可以看到这样的表达式:
用于计算过程方差的事实是 e[t] 项是独立同分布的,并且还有一个一般的统计性质,即两个随机变量的和的方差等于它们各自方差的总和加上两倍的协方差。对于独立同分布变量,协方差为 0。这导致了以下表达式:
因此,无论参数值如何,MA 过程的均值和方差都是随时间恒定的。
为 MA(q) 过程选择参数
我们对同样的数据拟合了一个 MA 模型,可以使用 ACF 来确定 MA 过程的阶数(见 Figure 6-7)。在继续阅读之前,请考虑一下 MA 过程的工作原理,并看看是否能理解为什么我们使用 ACF 而不是 PACF 来确定过程的阶数:
## R
> acf(demand[["Banking orders (2)"]])
![]()
图 6-7. 我们使用需求时间序列的 ACF 来确定 MA 模型的阶数。
ACF 和 PACF 模式与 MA 和 AR 过程不同。
不同于自回归过程,其自相关函数缓慢衰减,MA 过程的定义确保了对于大于 q 的任何值,自相关函数具有尖锐的截断。这是因为自回归过程依赖于先前的项,并且它们通过先前的冲击对系统进行了融入,而 MA 模型则通过它们的值直接融入冲击,从而有一种机制可以阻止冲击传播无限期。
我们可以看到在滞后 3 和 9 处有显著的值,因此我们拟合了包含这些滞后的 MA 模型。我们需要注意,我们不要错误地将模型中的错系数约束为 0,我们可以通过打印显示来确认:
## R
> ma.est = arima(x = demand[["Banking orders (2)"]],
order = c(0, 0, 9),
fixed = c(0, 0, NA, rep(0, 5), NA, NA))
> ma.est
Call:
arima(x = demand[["Banking orders (2)"]], order = c(0, 0, 9),
fixed = c(0, 0, NA, rep(0, 5), NA, NA))
Coefficients:
ma1 ma2 ma3 ma4 ma5 ma6 ma7 ma8 ma9 intercept
0 0 -0.4725 0 0 0 0 0 -0.0120 79689.81
s.e. 0 0 0.1459 0 0 0 0 0 0.1444 2674.60
sigma² estimated as 1.4e+09: log likelihood = -717.31,
aic = 1442.61
我们还应该检查我们的拟合,就像我们对 AR 模型所做的那样,通过绘制模型残差的 ACF,并作为模型性能的第二个单独测试,运行 Ljung-Box 测试以检查残差的整体随机性。请注意,Box.test()的输入要求我们指定自由度的数量,即可以估计的模型参数数量,而不是被约束为特定值的数量。在这种情况下,自由参数是截距以及 MA3 和 MA9 项:
## R
> Box.test(ma.est$residuals, lag = 10, type = "Ljung", fitdf = 3)
Box-Ljung test
data: ma.est$residuals
X-squared = 7.6516, df = 7, p-value = 0.3643
我们不能拒绝无残差点之间的时间相关性的原假设。同样,残差的 ACF 图表明没有时间相关性(这留作读者的练习)。
预测一个 MA(q)过程
我们可以再次使用forecast包的fitted()方法生成预测,这是用于 AR 过程的技术:
## R
> fitted(ma.est, h=1)
Time Series:
Start = 1
End = 60
Frequency = 1
[1] 90116.64 80626.91 74090.45 38321.61 74734.77 101153.20 65930.90
[8] 106351.80 104138.05 86938.99 102868.16 80502.02 81466.01 77619.15
[15] 100984.93 81463.10 61622.54 79660.81 88563.91 65370.99 104679.89
[22] 48047.39 73070.29 115034.16 80034.03 70052.29 70728.85 90437.86
[29] 80684.44 91533.59 101668.18 42273.27 93055.40 68187.65 75863.50
[36] 40195.15 82368.91 90605.60 69924.83 54032.55 90866.20 85839.41
[43] 64932.70 43030.64 85575.32 76561.14 82047.95 95683.35 66553.13
[50] 89532.20 85102.64 80937.97 93926.74 47468.84 75223.67 100887.60
[57] 92059.32 84459.85 67112.16 80917.23
MA 模型表现出强烈的均值回归,因此预测迅速收敛到过程的均值。这是合理的,因为该过程被认为是白噪声的函数。
如果您预测超出模型通过其顺序建立的范围,则预测将根据过程的定义必然是过程的均值。考虑一个 MA(1)模型:
要预测未来一个时间步长,我们对y[t+1]的估计是 。如果我们想预测未来两个时间步长,我们的估计是:
对于 MA(1)过程,我们无法在一步之后提供有根据的预测,对于一般的 MA(q)过程,我们不能在比过程发射的均值更有根据地提供超过q步的预测。通过“有根据的”预测,我指的是我们最近的测量对预测的影响。
传统符号为负
请注意,MA 模型通常不像这里描述的那样编写。传统上,θ系数前的符号是负的。这是由于推导的原因,而将 MA 模型视为 AR 模型并对其参数施加约束的一种方式。这种公式,通过大量的代数运算,导致θ的系数为负。
通过我们刚刚拟合的 MA(9)模型生成预测,我们现在寻求未来 10 个时间步长的预测:
## R
> fitted(ma.est, h=10)
Time Series:
Start = 1
End = 60
Frequency = 1
[1] NA NA NA NA NA NA NA NA
[9] NA NA 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81
[17] 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81
[25] 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81
[33] 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81
[41] 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81
[49] 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81 79689.81
[57] 79689.81 79689.81 79689.81 79689.81
当我们试图预测未来的 10 个时间步长时,我们预测每个时间步长的均值。即使没有花哨的统计模型,我们也可以做到这一点!
常识很重要。如果你在不理解其工作原理的情况下应用模型,可能会出现一些令人尴尬的事情,比如在你浪费了先前的时间和计算资源后,每天都向老板发送完全相同的预测。
自回归积分移动平均模型
现在我们已经分别检验了 AR 和 MA 模型,我们转向自回归积分移动平均(ARIMA)模型,该模型结合了这两者,认识到同一时间序列可能具有同时存在的 AR 和 MA 模型动态。这本身将导致一个 ARMA 模型,但我们扩展到 ARIMA 模型,它考虑了差分,一种去除趋势并使时间序列平稳的方法。
ARIMA 模型继续在性能上保持接近最先进的状态,特别是在数据集较小的情况下,更复杂的机器学习或深度学习模型表现不佳的情况下。然而,即使是 ARIMA 模型,尽管相对简单,也存在过拟合的危险。
模型
如果你一直在认真关注,那么此时你可能会摸不着头脑,因为我刚刚将同样的数据拟合到了 AR 和 MA 过程中,却没有加以评论。这是你在时间序列分析教材中有时会遇到的令人恼火的习惯。一些作者会承认这种数据的懒惰,而其他人则会漠然地忽视它。我们并没有深入调查我们之前的模型是否特别合适,但从我们使用的拟合过程中,似乎清楚地表明可以用 AR 或 MA 模型描述数据是有辩护理由的。这引发了一个问题:将这两种行为结合到同一个模型中是否有帮助?
表格 6-1 可以是检查时间序列过程的一个方便方法,以确定 AR、MA 或 ARMA 描述的最佳模型。
表格 6-1. 确定最佳描述我们时间序列的模型
| 绘图类型 |
AR(p) |
MA(q) |
ARMA |
| ACF 行为 |
缓慢下降 |
滞后 = q后急剧下降 |
无急剧截断 |
| PACF 行为 |
滞后 = p 后急剧下降 |
缓慢下降 |
无急剧截断 |
这将我们引向自回归移动平均(ARMA)模型,该模型适用于当 AR 和 MA 项单独描述的情况不足以充分描述经验动态时。这是一个常见情况,当 AR 和 MA 的诊断统计(PACF 和 ACF)指向非零值时,表明某个阶数的 AR 或 MA 项。这些可以结合到 ARMA 模型中。
沃尔德定理
沃尔德定理告诉我们,每个协方差平稳时间序列可以被写成两个时间序列的和,一个确定性的和一个随机的。根据这个定理,我们也可以说,一个平稳过程可以被 ARMA 模型合理地近似,尽管找到适当的模型当然可能相当困难。
在这里,我们通过对 MA 过程系数应用负号来切换到更传统的统计符号表示方式:
ARMA 过程的平稳性取决于其 AR 组分的平稳性,并由相同的特征方程控制,该特征方程决定了 AR 模型是否平稳。
从 ARMA 模型到 ARIMA 模型的转变非常简单。ARIMA 模型与 ARMA 模型的区别在于 ARIMA 模型包括“整合”术语,这指的是模拟时间序列必须进行多少次差分以产生平稳性。
ARIMA 模型在实践中被广泛应用,特别是在学术研究和预测问题中,远远超过了 AR、MA 和 ARMA 模型。快速的 Google 学者搜索显示 ARIMA 被应用于各种预测问题,包括:
-
前往台湾的入境航空旅客
-
土耳其能源需求按燃料类型
-
印度批发蔬菜市场的日销售量
-
美国西部的急诊室需求
重要的是,差分的次数不应过大。一般来说,ARIMA(p, d, q)模型的每个参数值都应尽可能保持较小,以避免不必要的复杂性和对样本数据的过度拟合。作为一个并非普遍适用的经验法则,你应对d超过 2 和p和q超过 5 或左右持怀疑态度。此外,你应该预计p或q项将主导,并且另一个将相对较小。这些都是从分析师那里收集到的实践者笔记,并非硬性数学真理。
选择参数
ARIMA 模型在参数(p, d, q)方面进行规定。我们根据手头的数据选择合适的p, d,和q值。
下面是维基百科描述 ARIMA 模型的一些知名示例:
-
ARIMA(0, 0, 0)是一个白噪声模型。
-
ARIMA(0, 1, 0)是一个随机游走,带非零常数的 ARIMA(0, 1, 0)是带漂移的随机游走。
-
ARIMA(0, 1, 1)是指数平滑模型,ARIMA(0, 2, 2)与 Holt 线性方法相同,将指数平滑扩展到带趋势的数据,以便用于具有潜在趋势的数据预测。
我们根据领域知识的结合、各种拟合评估指标(如 AIC)以及对给定基础过程的 PACF 和 ACF 应如何出现的一般知识(如表 6-1 中描述的)来选择我们模型的顺序。接下来,我们将展示如何使用基于 PACF 和 ACF 的手动迭代过程以及基于forecast包的auto.arima()函数的自动参数选择工具来拟合 ARIMA 模型。
手动拟合模型
有关选择 ARIMA 模型参数的启发法则,其中“简洁性至上”是首要考虑的。一个流行且长期存在的方法是 Box-Jenkins 方法,这是一个迭代的多步骤过程:
-
利用你的数据、可视化和基础知识来选择适合你数据的模型类别。
-
根据你的训练数据估计参数。
-
根据训练数据评估你的模型性能,并调整模型参数以解决性能诊断中发现的弱点。
让我们通过一个拟合数据的例子来逐步进行。首先,我们需要一些数据。在这种情况下,为了透明和知道正确答案,我们从 ARMA 过程生成我们的数据:
## R
> require(forecast)
> set.seed(1017)
> ## order of arima model hidden on purpose
> y = arima.sim(n = 1000, list(ar = ###, ma = ###))
暂时不要关注创建的模型顺序;让我们把这当作一个谜题来解决。首先,我们应该绘制时间序列,正如我们总是做的,看它是否呈现稳态(图 6-8)。然后,我们检查y的 ACF 和 PACF(图 6-9),并与表 6-1 进行比较。
![]()
图 6-8. 我们时间序列的绘图。
![]()
图 6-9. 我们时间序列的 ACF 和 PACF。
我们可以看到,无论是 ACF 还是 PACF 都没有明显的截尾,这表明(见表 6-1),这是一个 ARMA 过程。我们首先拟合一个相对简单的 ARIMA(1, 0, 1)模型,因为我们看不到差分的必要性,也没有证据(图 6-10)。
## R
> ar1.ma1.model = Arima(y, order = c(1, 0, 1))
> par(mfrow = c(2,1))
> acf(ar1.ma1.model$residuals)
> pacf(ar1.ma1.model$residuals)
![]()
图 6-10. ARIMA(1, 0, 1)模型残差的 ACF 和 PACF。
图 6-10 中的残差显示出特别大的 PACF 值,表明我们还没有完全描述自回归行为。因此,我们通过添加更高阶的 AR 分量来扩展模型,在下面的代码中测试 ARIMA(2, 0, 1)模型,并绘制此更复杂模型残差的 ACF 和 PACF(图 6-11):
## R
> ar2.ma1.model = Arima(y, order = c(2, 0, 1))
> plot(y, type = 'l')
> lines(ar2.ma1.model$fitted, col = 2)
> plot(y, ar2.ma1.model$fitted)
> par(mfrow = c(2,1))
> acf(ar2.ma1.model$residuals)
> pacf(ar2.ma1.model$residuals)
![]()
图 6-11. ARIMA(2, 0, 1)模型残差的 ACF 和 PACF。
图 6-11 中的残差再也不显示 ACF 或 PACF 的大值了。考虑到我们对一个简约模型的渴望以及过度拟合 ARIMA 模型的危险,一个明智的分析师可能会停在这里,因为在残差中没有进一步需要通过自回归、移动平均或差分组件进行拟合的行为。留给读者作为练习来考虑拟合更复杂的模型。尽管这里没有展示图形,我尝试用以下代码拟合更复杂的模型。我看到它们并没有显著改进模型与数据的拟合,也没有比前一个模型进一步降低 ACF 或 PACF 值。你可以自行验证:
## R
> ar2.ma2.model = Arima(y, order = c(2, 0, 2))
> plot(y, type = 'l')
> lines(ar2.ma2.model$fitted, col = 2)
> plot(y, ar2.ma2.model$fitted)
> par(mfrow = c(2,1))
> acf(ar2.ma2.model$residuals)
> pacf(ar2.ma2.model$residuals)
>
> ar2.d1.ma2.model = Arima(y, order = c(2, 1, 2))
> plot(y, type = 'l')
> lines(ar2.d1.ma2.model$fitted, col = 2)
> plot(y, ar2.d1.ma2.model$fitted)
> par(mfrow = c(2,1))
> acf(ar2.d1.ma2.model$residuals)
> pacf(ar2.d1.ma2.model$residuals)
一个快速比较模型的方法在这里展示,我看看拟合模型的预测与实际值的相关性有多好:
## R
> cor(y, ar1.ma1.model$fitted)
[1] 0.3018926
> cor(y, ar2.ma1.model$fitted)
[1] 0.4683598
> cor(y, ar2.ma2.model$fitted)
[1] 0.4684905
> cor(y, ar2.d1.ma2.model$fitted)
[1] 0.4688166
从 ARIMA(1, 0, 1)模型转换到 ARIMA(2, 0, 1)模型(前两个模型)后,我们看到了显著的改善,相关性从 0.3 增加到了 0.47。另一方面,随着我们增加复杂性,相关性并没有显著提高。这进一步支持了我们早前得出的结论,即 ARIMA(2, 0, 1)模型似乎很好地描述了模型行为,并且没有必要添加更多的 AR 或 MA 分量来改善拟合。
顺便说一句,我们可以通过比较原始拟合系数(此处显示,之前被掩盖)与拟合系数来看一下拟合效果:
## R
## original coefficients
> y = arima.sim(n = 1000, list(ar = c(0.8, -0.4), ma = c(-0.7)))
> ar2.ma1.model$coef
ar1 ar2 ma1 intercept
0.785028320 -0.462287054 -0.612708282 -0.005227573
拟合系数与用于模拟数据的实际系数之间存在良好的匹配。
手动拟合 ARIMA 模型不仅仅是我们在这里展示的那样简单。几十年来,从业者们已经发展出了一些好的经验法则,用于识别问题,例如当某种类型的项过多时,当模型被过度差分时,当残差模式指向特定问题时等等。一个很好的起步资源是宾夕法尼亚州立大学一位教授编写的在线指南,可在此处获取。
有一些对手动拟合 ARIMA 过程的合理批评。手动拟合可能是一个定义不太清晰的过程,对分析师的判断力施加了很大压力,并且可能耗时且最终结果依赖路径。几十年来,这是一个表现良好的解决方案,用于生成实际世界中使用的预测,但并非完美。
使用自动模型拟合
如今,在某些情况下,我们可以放弃手动迭代拟合过程,转而采用自动模型选择。我们的模型选择可以根据各种信息损失准则进行驱动,例如我们通过forecast包中的auto.arima()函数简要讨论的 AIC:
## R
> est = auto.arima(demand[["Banking orders (2)"]],
stepwise = FALSE, ## this goes slower
## but gets us a more complete search
max.p = 3, max.q = 9)
> est
Series: demand[["Banking orders (2)"]]
ARIMA(0,0,3) with non-zero mean
Coefficients:
ma1 ma2 ma3 mean
-0.0645 -0.1144 -0.4796 79914.783
s.e. 0.1327 0.1150 0.1915 1897.407
sigma² estimated as 1.467e+09: log likelihood=-716.71
AIC=1443.42 AICc=1444.53 BIC=1453.89
在这种情况下,我们通过比较我们早期探索中的一行配置输入的先验知识,来放置了一个一行描述。换句话说,我们指定了 AR 和 MA 过程的最大阶数,我们准备接受的,实际上,模型选择选择了一个比我们指定的更加简洁的模型,包括没有 AR 项。尽管如此,这个模型拟合效果很好,除非我们有充分的理由这样做,否则我们不应该过多地对数据进行窥探。请注意,根据 AIC 标准,我们在前一节手动选择的 MA 模型略优于这个模型,但当我们查看图表时,这种差异似乎不具有意义。
留给读者的练习是将模型绘制为预测的模型,以及检查这个自动选择的模型的残差,并确认使用的模型似乎没有留下需要通过增加更多项来解决的残差行为。代码与我们在之前的 AR 和 MA 部分使用的代码没有区别。对于制作预测的情况也是如此,因此在此处省略了,因为我们选择的 ARIMA 模型与之前讨论和用于预测的 MA 模型并没有太大不同。
我们还可以快速研究当我们手动拟合模型时,auto.arima()在我们在上一节中生成的模型上的表现如何:
## R
> auto.model = auto.arima(y)
> auto.model
Series: y
ARIMA(2,0,1) with zero mean
Coefficients:
ar1 ar2 ma1
0.7847 -0.4622 -0.6123
s.e. 0.0487 0.0285 0.0522
sigma² estimated as 1.019: log likelihood=-1427.21
AIC=2862.41 AICc=2862.45 BIC=2882.04
我们甚至没有使用auto.arima()的可选参数建议模型搜索从哪里开始,但它却收敛到了与我们相同的解决方案。因此,正如我们在这里所看到的,在某些情况下,我们会发现不同方法得出相同的解决方案。我们通过查看更简单模型的残差的 ACF 和 PACF 来驱动我们的分析,以构建更复杂的模型,而auto.arima()主要通过网格搜索来最小化 AIC 来驱动。当然,鉴于我们是从 ARIMA 过程生成原始数据,这代表了一个比许多现实数据更简单的情况。在后一种情况下,并不总是我们的手动拟合和自动模型选择得出相同的结论。
如果您将auto.arima()或类似的自动模型选择工具作为分析的重要组成部分,重要的是要阅读文档,尝试合成数据,并阅读其他分析师对此函数的使用经验。已知存在一些情况,该函数的表现不如预期简单,并且已知有一些解决方法。总体而言,这是一个很好的解决方案,但并不完美。⁴ 另外,关于auto.arima()的工作方式的良好描述,请参阅该主题的在线教科书章节,该章节由该函数的作者罗布·亨德曼教授撰写。
我们演示了两种不同的参数估计方式:要么遵循 Box-Jenkins 方法来拟合模型,要么使用forecast包中的自动拟合。实际上,从业者对此有很强烈的意见,有些人激烈主张只支持手动迭代,而另一些人则坚决支持自动选择工具。这仍然是该领域的一个持续争论。从长远来看,随着大数据在时间序列分析中占据越来越多的地位,自动探索和模型拟合可能会主导大数据集时间序列分析。
向量自回归
在现实世界中,我们经常很幸运地有几个平行的时间序列,这些序列可能相互关联。我们已经探讨了如何清理和对齐这些数据,现在我们可以学习如何最大化利用它们。我们可以通过将 AR(p)模型推广到多变量的情况来做到这一点。这种模型的美妙之处在于它考虑了变量相互影响并相互影响——也就是说,并不存在一个特权的y,而其他所有变量都被指定为x。相反,拟合是对所有变量对称的。请注意,如果系列不是平稳的,可以像以前的其他模型一样应用差分。
外生和内生变量
在统计学中,一旦我们采用了变量相互影响的模型,我们称这些变量为内生变量,意味着它们的值由模型内部的内容解释。相反,外生变量是那些在模型内部无法解释的变量——也就是说,它们的值不能通过假设来解释,因此我们接受它们的值,不质疑它们产生的动态过程。
由于每个时间序列都被认为预测其他时间序列以及自身,我们将为每个变量拥有一个方程。假设我们有三个时间序列:我们将这些时间序列在时间t的值表示为y[1, t]、y[2, t]和y[3, t]。然后,我们可以将二阶(考虑两个时间滞后)向量自回归(VAR)方程写为:
矩阵乘法
如果你熟悉线性代数,你会注意到,在矩阵表示法中,表达前面三个方程中显示的关系要简单得多。特别是,你可以以非常类似的方式编写 VAR。在矩阵形式中,这三个方程可以表示为:
其中y和ϕ[0]是 3 × 1 矩阵,而其他ϕ矩阵是 3 × 3 矩阵。
即使是简单的情况,你也可以看到模型中的参数数量增长得非常快。例如,如果我们有p个滞后和N个变量,我们可以看到每个变量的预测方程为个值。由于我们有N个值要预测,这转化为总变量,这意味着变量数量与研究的时间序列数量成比例增长。因此,我们不应仅仅因为有数据而随意引入时间序列,而应该将此方法保留给我们真正预期有关系的情况。
VAR 模型最常用于计量经济学。它们有时会受到批评,因为除了所有值互相影响的假设外,它们没有任何结构。正是因为这个原因,模型的拟合优度可能很难评估。然而,VAR 模型仍然很有用——例如,用于测试一个变量是否导致另一个变量。它们在需要预测多个变量并且分析师没有领域知识来确定任何特定关系的情况下,有时也会很有帮助。它们有时还可以帮助确定预测值的方差有多少归因于其基础“原因”。
这里有一个快速演示。让我们看看基础的 UCI 需求信息,并考虑使用第二列来预测银行订单 (2),而不仅仅是它自己的数据(注意,由于对称处理变量的方式,我们也将预测该列)。让我们考虑使用来自交通控制部门的订单。听起来这应该是完全不同的,因此它可能提供相对于财政部门自身过去订单的相当独立的信息源。我们还可以想象,每列提供了有关经济状况以及未来需求增减的基础信息。
要确定要使用的参数,我们使用 vars 包,该包带有 VARselect() 方法:
## R
> VARselect(demand[, 11:12, with = FALSE], lag.max=4,
+ type="const")
$selection
AIC(n) HQ(n) SC(n) FPE(n)
3 3 1 3
$criteria
1 2 3 4
AIC(n) 3.975854e+01 3.967373e+01 3.957496e+01 3.968281e+01
HQ(n) 3.984267e+01 3.981395e+01 3.977126e+01 3.993521e+01
SC(n) 3.997554e+01 4.003540e+01 4.008130e+01 4.033382e+01
FPE(n) 1.849280e+17 1.700189e+17 1.542863e+17 1.723729e+17
我们可以看到该函数提供了各种信息准则供选择。还请注意,我们指出我们希望拟合一个 "const" 项来适应非零均值。我们还可以选择拟合漂移项,或者二者都不拟合,但是 "const" 选项似乎最适合我们的数据。在这里,我们将从查看三个滞后开始,看看效果如何:
## R
> est.var <- VAR(demand[, 11:12, with = FALSE], p=3, type="const")
> est.var
> par(mfrow = c(2, 1))
> plot(demand$`Banking orders (2)`, type = "l")
> lines(fitted(est.var)[, 1], col = 2)
> plot(demand$`Banking orders (3)`,
> type = "l")
> lines(fitted(est.var)[, 2], col = 2)
> par(mfrow = c(2, 1))
> acf(demand$`Banking orders (2)` - fitted(est.var)[, 1])
> acf(demand$`Banking orders (3)` -
> fitted(est.var)[, 2])
此代码生成了图表显示在 6-12 和 6-13 中所示的图形。
![]()
图 6-12. 在顶部,我们看到了银行订单 (2) 的实际值(实线)与预测值(虚线),在底部,我们看到了银行订单 (3) 的同样情况。有趣的是,顶部的图表更像是一个典型的预测,其中预测相对于实际数据的变化有些“缓慢”,而在底部的图表中,我们看到预测实际上提前预测了变化。这表明银行订单 (2) “领先于” 银行订单 (3),这意味着银行订单 (2) 在预测交通控制器订单方面是有帮助的,但可能不是反过来,或者至少不是在这种程度上。
![]()
图 6-13. 我们绘制了每个时间序列残差的自相关函数。请注意,对于两个序列,误差在滞后 3 处存在一些边缘显著的自相关,可能在模型中没有完全解释。
ACF 并未如我们所希望地清楚地支持残差中的无自相关性,因此在这种情况下,我们还可以通过vars包的serial.test()方法应用Portmanteau 测试来检验串行相关性。这个测试类似于我们在单变量情况下看到的串行相关性测试:
## R
> serial.test(est.var, lags.pt = 8, type="PT.asymptotic")
Portmanteau Test (asymptotic)
data: Residuals of VAR object est.var
Chi-squared = 20.463, df = 20, p-value = 0.4293
由于p值如此之高,我们无法拒绝不存在残差中串行相关的零假设。这为我们提供了进一步的证据,证明该模型做得相当不错。
鉴于对于我们检验的单变量模型,我们逐步分析了各种模型,一直到 ARMA 和 ARIMA 版本,您可能会想知道是否存在 VARIMA 模型。确实存在,但由于 VAR 表现相对较好且已相当复杂,因此并不经常使用。在工业和学术用例中,您会发现使用 VAR 远远超过 VARIMA 的情况。
另一个相关的模型类是 CVAR 模型,即共整合向量自回归模型。这指的是个别时间序列不是平稳的,但时间序列的线性组合在不经过差分的情况下是平稳的。
统计模型的变体
存在许多其他类型的针对时间序列数据开发的统计模型。其中一些扩展了 ARIMA 模型,而其他一些则对时间动态的基本假设与 ARIMA 模型不同。在本节中,我们简要讨论一些最常用和知名的统计时间序列模型。
季节性 ARIMA
季节性 ARIMA(SARIMA)模型假设存在乘法季节性。因此,SARIMA 模型可以表示为 ARIMA(p, d, q)×(P, D, Q)m。该模型假设季节性行为本身可以被视为 ARIMA 过程,m指定每个季节周期中的时间步数。在这个因子中重要的是,该模型认识到时间相邻点可以互相影响,无论是在同一个季节内还是在不同季节内,但通过通常的时间接近方法。
确定 SARIMA 甚至比确定 ARIMA 模型更加棘手,因为您需要处理季节效应。幸运的是,在forecasts包中的auto.arima()可以像处理标准 ARIMA 估计任务一样处理这一点。正如早些时候讨论的那样,除非您有强有力的知识表明需要覆盖自动方法确定的选择模型,否则有很多理由选择自动参数选择。
ARCH、GARCH 及其众多类似模型
ARCH 代表“条件异方差自回归”,这种模型几乎专门用于金融行业。它经常出现在时间序列课程中,因此在这里值得一提。这类模型基于这样一个观察结果:股票价格的方差并非恒定,实际上方差本身在较早的方差条件下似乎是自回归的(例如,股票交易所的高波动日出现成簇)。在这些模型中,被建模的是过程的方差,而不是过程本身。
分层时间序列模型
在现实世界中,分层时间序列非常常见,尽管它们并未以此形式呈现。你可以轻松地构想出它们可能出现的情况:
-
公司产品的总月需求金额,然后可以按 SKU 编号进行细分。
-
全体选民的每周政治民意调查数据,然后根据不同的人口统计学特征(重叠或非重叠),比如女性与男性,或者西班牙裔与非裔美国人。
-
每天进入欧盟的游客总数与特定成员国每天进入的游客数量的总计
处理分层时间序列的一个方便的方法是通过 R 的hts包。这个包既可以用于可视化分层时间序列数据,也可以用于生成预测。
可以使用一些历史上已经在这个包中可用的不同方法生成预测:
-
生成最低级的预测(最个性化的),然后将其汇总以生成更高级别的预测。
-
生成最高级别的预测,然后基于聚合每个组成部分的历史比例生成低级别的预测。这种方法在进行低级别预测时往往不太准确,尽管可以通过多种技术来预测聚合比例本身随时间的变化。
-
可以尝试通过选择“中间”方法来获取每种方法的最佳效果,其中会生成中层预测(假设你有多层级)。然后在广义上向上下传播,以进行其他预测。
最终,hts包的黄金标准在于,可以为所有层次的分层生成独立的预测。然后hts结合这些预测,以保证方法的一致性,如Hyndman et al.中所述。
我们在本章讨论的许多统计模型反过来又会应用于决定预测哪个层次的时间序列的问题。分层分析部分往往是围绕这些基础模型的一个包装器。
时间序列统计方法的优缺点
当您在考虑是否将此处描述的某个统计模型应用于时间序列问题时,从优缺点的库存开始可能是个不错的方法。这些优缺点在本节中详细列出。
优点
-
这些模型简单透明,因此可以清晰地理解其参数。
-
由于定义这些模型的简单数学表达式,可以以严格的统计方式推导出它们的有趣属性。
-
您可以将这些模型应用于相对较小的数据集,仍然可以获得良好的结果。
-
这些简单模型及其相关修改表现非常出色,甚至与非常复杂的机器学习模型相比也不遑多让。因此,您可以在不过度拟合的情况下获得良好的性能。
-
发展模型的订单和估计其参数的自动化方法非常成熟,因此可以轻松生成这些预测。
缺点
-
由于这些模型相当简单,因此在处理大数据集时并不总是能提升性能。如果您正在处理极大数据集,则可能更适合使用机器学习和神经网络方法的复杂模型。
-
这些统计模型侧重于分布的均值点估计,而不是分布本身。的确,您可以推导出样本方差等作为预测不确定性的某种替代,但您的基本模型只提供了有限的表达方式来相对于您在选择模型时所做的所有选择来表达不确定性。
-
根据定义,这些模型并非用于处理非线性动态,并且在描述非线性关系占主导地位的数据时表现不佳。
更多资源
-
经典文本:
罗伯特·J·亨德曼和乔治·阿瑟纳索普洛斯,《预测:原理与实践》第二版(墨尔本:OTexts,2018),https://perma.cc/9JNK-K6US。
这本实用且极易接近的教科书免费介绍了在 R 中预处理时间序列数据的所有基础知识以及利用该数据进行预测的方法。重点是让读者迅速掌握并熟练使用时间序列预测的实用方法。
蔡瑞,《金融时间序列分析》(霍博肯,新泽西州:约翰·威利·父子,2001)。
这本经典教材介绍了各种时间序列模型,包括对开发 AR、MA 和 ARIMA 模型的非常详尽和易于理解的章节,应用于历史股票价格。还包括大量的 R 实例。就可访问性而言,这是一本中等难度的书籍,假设读者具有某些统计学和其他数学知识,但对于已掌握高中微积分和入门统计学课程的任何人来说都是相当易读的。
罗伯特·H·舒姆韦,《时间序列分析及其应用》(纽约,纽约州:斯普林格国际,2017)。
这是另一本经典教科书,内容稍微更为理论化且不易理解。最好先看看 Tsay 的书,但这本书包括了更多关于统计时间序列模型数学过程的讨论。它还包括了从科学和经济数据源中得出的更广泛的数据分析。这本书比其他列出的教科书更具挑战性,但这是因为它内容密集且信息丰富,并不是因为它需要更高的数学能力水平(实际上并不需要,尽管如果读得太快可能会有这种感觉)。
-
启发性指导:
Robert Nau,“识别 ARIMA 模型的规则总结,” 来自杜克大学 Fuqua 商学院的课程笔记,https://perma.cc/37BY-9RAZ。
这些笔记详细指导您如何选择 ARIMA 模型的三个参数。当您阅读本摘要时,您会立即注意到对简约模型的强调。
美国国家标准与技术研究院(NIST),“Box-Jenkins 模型,” 收录于 NIST/SEMATECH 统计方法 e-Handbook(华盛顿特区:NIST,美国商务部,2003 年),https://perma.cc/3XSC-Y7AG。
这部分在线 NIST 手册提供了实施 Box-Jenkins 方法的具体步骤,这是一种常用的 ARIMA 参数选择方法学。这也是 NIST 为时间序列统计分析提供的资源的一个很好的例子,作为更大型精心编纂的最佳实践手册的一部分。
Rob J. Hyndman,“ARIMAX 模型的混淆问题,” Hyndsight 博客,2010 年 10 月 4 日,https://perma.cc/4W44-RQZB。
这篇简洁的博客文章来自著名预测专家 Rob Hyndman,描述了将协变量纳入 ARIMA 模型的方法,这是处理多变量时间序列的 VAR 替代方法。
Richard Hardy,“ARIMA 模型的交叉验证和正则化问题,” Cross Validated, StackExchange 上的问题,2015 年 5 月 13 日,https://perma.cc/G8NQ-RCCU。
在自回归项较高或在 VAR 的情况下输入较多的情况下,正则化是有意义的,在许多行业中,这会带来显著的性能改进。这篇关于 Cross Validated 问答网站的帖子提供了一些初步讨论以及与计算实现和相关学术研究的链接。
¹ 请注意,即使在移除某些条件的情况下,普通最小二乘法仍然是无偏的。例如,当误差相关且异方差时,普通最小二乘法仍然可以提供系数的无偏估计,尽管存在效率问题。有关效率的更多信息,请参阅 Wikipedia。
² 在这种情况下,有一些缓解因素可以证明使用标准线性回归的合理性。首先,有人认为在足够短的时间尺度上,金融市场的波动彼此独立(iid)。其次,由于线性回归计算效率高,即使在假设不准确的情况下,快速模型在追求速度的行业中仍然是一个好模型。第三,使用这些模型的企业能够赚钱,因此他们一定是做对了什么。
³ 只有随后引用的方程式才编号。
⁴ 对于一些示例讨论,请参阅诸如 Stack Overflow 和 Rob Hyndman’s blog。
第七章:时间序列的状态空间模型
状态空间模型与我们在前一章中研究的统计模型类似,但更具有“现实世界”的动机。它们解决了在真实世界工程问题中出现的问题,例如在进行估计时如何考虑测量误差以及如何将先验知识或信念注入估计中。
状态空间模型假设一个世界,其中真实状态不能直接测量,而只能从可测量的内容推断。状态空间模型还依赖于指定系统的动态,例如世界真实状态如何随时间演变,既受内部动态的影响,也受到施加在系统上的外部力量的影响。
尽管在数学背景下你可能之前没见过状态空间模型,但你可能在日常生活中使用过它们。例如,想象一下你看到一名司机在交通中迂回。你试图确定司机要去哪里以及如何最好地保护自己。如果司机可能醉酒,你会考虑报警,而如果司机因一时分心而不会重复的原因,你可能会顾及自己的事务。在接下来的几秒或几分钟内,你将更新你对该司机的状态空间模型,然后决定该做什么。
使用状态空间模型的一个经典例子是发射到太空的火箭。我们知道牛顿定律,因此我们可以编写系统动态的规则以及随时间应该看起来如何运动。我们还知道我们的 GPS 或传感器或者我们用于跟踪位置的任何东西都会有一些测量误差,我们可以量化并试图考虑到我们计算不确定性的这种影响。最后,我们知道我们无法解释作用在特定火箭上的世界上所有力量,因为系统中存在许多未知因素,所以我们希望这个过程对其他未知噪声源具有鲁棒性,也许是太阳风或地球风或两者兼而有之。事实证明,在过去 50 年的统计和工程进步中,这些方法已被证明非常有用。
两种不同的历史趋势导致了状态空间模型的发展以及对它们所处理问题种类的兴趣。首先,在 20 世纪中期左右,我们进入了一个机械自动化的时代。天空中有火箭和航天器,潜艇的导航系统以及所有种类的其他自动发明,这些发明需要估计无法测量的系统状态。当研究人员考虑如何估计系统状态时,他们开始开发状态空间方法,最重要的是区分测量误差和系统中其他类型的不确定性。这导致了状态空间方法的首次应用。
在此期间,记录技术和相关计算也在发展。这导致了时间序列的数据集变得更大,包括更长或更详细的时间序列数据集。随着更多时间序列数据的可用性,可以与状态空间建模的新思维一起为其开发更多数据密集型方法。
在本章中,我们将学习这些常用的状态空间方法:
-
应用于线性高斯模型的卡尔曼滤波器
-
隐马尔可夫模型
-
贝叶斯结构时间序列
在每种情况下,使用这些模型是相当简便和良好实施的。对于每个模型,我们将对数学提出一些直觉,并讨论适合使用该方法的数据类型。最后,我们将看到每种方法的代码示例。
在每种情况下,我们将区分我们观察到的内容和产生我们观察结果的状态。在基于观察结果估计基础状态时,我们可以将我们的工作划分为不同的阶段或类别:
滤波
利用时间 t 处的测量更新我们对时间 t 处状态的估计
预测
利用时间 t - 1 处的测量生成时间 t 处预期状态的预测(同时允许我们推断时间 t 处的预期测量)
平滑
利用包括 t 时间点之前和之后的时间范围内的测量来估计时间 t 处的真实状态
这些操作的机制通常相似,但区别很重要。滤波是一种决定如何将最新信息与过去信息进行权衡,以更新我们对状态估计的方法。预测是在没有未来信息的情况下预测未来状态。平滑是在制定给定时间点状态的最佳估计时利用未来和过去信息的方法。
状态空间模型:优缺点
状态空间模型可用于确定性和随机应用,并且可以应用于连续采样数据和离散采样数据。¹
这一点单独就能让你对它们的效用和极大的灵活性有所了解。状态空间模型的灵活性是驱动这一类模型优缺点的根源。
状态空间模型有许多优势。状态空间模型允许对时间序列中通常最有趣的内容进行建模:产生被分析的嘈杂数据的动态过程和状态,而不仅仅是嘈杂数据本身。通过状态空间模型,我们在建模过程中注入了一个解释生成过程的模型,以解释首次产生过程的动因。这对于我们已经对系统如何运作有强有力理论或可靠知识的情况非常有用,而我们希望我们的模型帮助我们探索我们已经熟悉的一般动态的更多细节。
状态空间模型允许随时间改变系数和参数,这意味着它允许随时间改变的行为。在使用状态空间模型时,我们没有对数据施加平稳性条件。这与我们在第六章中检验的模型有很大不同,那里假设了一个稳定的过程,并且只用一个系列的系数来建模,而不是时间变化的系数。
尽管如此,状态空间模型也有一些缺点,有时状态空间模型的优点也是它的弱点:
-
由于状态空间模型如此灵活,需要设置许多参数,并且状态空间模型可以采取许多形式。这意味着特定状态空间模型的性质通常未被充分研究。当你制定一个适合你时间序列数据的状态空间模型时,你很少会在统计教科书或学术研究论文中找到其他人已经研究过该模型。这使得你在理解模型性能或可能出现错误的地方时,往往处于不太确定的领域。
-
状态空间模型在计算上可能非常耗费资源,因为涉及到许多参数。此外,某些类型的状态空间模型的参数数量非常庞大,这可能使你容易过拟合,特别是如果数据量不足的话。
卡尔曼滤波器
卡尔曼滤波器是一种成熟并广泛应用的方法,用于从时间序列中获得新信息,并与先前已知的信息以智能方式结合,以估计潜在状态。卡尔曼滤波器最初的应用之一是在阿波罗 11 号任务中,当 NASA 工程师意识到机载计算元件无法支持其他更耗费内存的位置估计技术时,选择了该滤波器。正如您将在本节中看到的那样,卡尔曼滤波器的优点在于它相对容易计算,并且不需要存储过去的数据来进行现在或未来的预测。
概述
对于初学者来说,卡尔曼滤波器的数学可能有些令人望而却步,不是因为它特别困难,而是因为需要跟踪相当数量的量,并且这是一个迭代的、有些循环的过程,涉及许多相关的量。因此,我们在这里不会推导卡尔曼滤波器的方程式,而是通过对这些方程式的高级概述来了解它们的工作原理。²
我们从线性高斯模型开始,假设我们的状态和观察具有以下动态:
x[t] = F × x[t–1] + B × u[t] + w[t]
y[t] = A × x[t] + v[t]
也就是说,时间t的状态是前一个时间步骤的状态(F × x[t–1])、外部力项(B × u[t])和随机项(w[t])的函数。同样,时间t的测量是状态在时间t和随机误差项、测量误差的函数。
让我们想象x[t]是宇宙飞船的实际位置,而y[t]是我们用传感器测量的位置。v[t]是我们传感器设备(或多个设备的范围)的测量误差。卡尔曼滤波器适用的基本方程是这个方程,它展示了如何在时间t给出新信息后更新我们的估计:
[t] = K[t] × y[t] + (1 – K[t]) × [t*–1]
我们在这里看到的是一个滤波步骤,即如何使用时间t的测量来更新我们在时间t的状态估计的决策。请记住,我们假设的情况是我们只能观察y[t]并推断状态,但不能确切知道状态。从上面我们看到,数量K[t]在我们的估计中建立了旧信息([t–1])和新信息(y[t])之间的平衡。
要深入了解更详细的机制,我们需要定义一些术语。我们使用P[t]来表示我们状态协方差的估计(这可以是标量或矩阵,取决于状态是否单变量或多变量,后者更常见)。P^–[t]是在考虑时间t的测量之前的时间t的估计。
我们还用R来表示测量误差方差,即v[t]的方差,这可以是一个标量或协方差矩阵,具体取决于测量的维度。R通常对于一个系统是很好定义的,因为它描述了特定传感器或测量设备的已知物理特性。对于w[t]的适当值Q在建模过程中较少定义并且需要调整。
然后我们从一个过程开始,该过程使我们在时间 0 处已知或估计了x和P的值。然后,在时间 0 后,我们遵循预测和更新阶段的迭代过程,预测阶段首先进行,然后是更新/过滤阶段,如此循环。
预测:
^– [t] = F × [t–1] + B × u[t]
P^–[t] = F × P[t–1] × F^T + Q
滤波:
[t] = ^– [t] + K[t] × (y[t] – A ×