Python-统计模型构建指南-全-
Python 统计模型构建指南(全)
原文:
annas-archive.org/md5/60a7ce9fba25ab267b2031ea2a27f285译者:飞龙
前言
统计学是一门应用分析方法的学科,用于在学术和行业环境中使用数据来回答问题和解决问题。许多方法已经存在了几个世纪,而其他方法则更为新颖。统计分析和结果对于向技术和非技术受众展示来说相当直接。此外,使用统计分析产生结果并不一定需要大量数据或计算资源,而且可以相当快速地完成,尤其是在使用 Python 等易于处理和实现的编程语言时。
随着计算能力的提升和可访问性的增加,近年来人工智能(AI)和高级机器学习(ML)工具变得更加突出和受欢迎。在进行统计分析作为使用 AI 和 ML 开发更大规模项目的前置条件时,这可以使从业者在使用更大计算资源和项目架构开发之前评估可行性和实用性。
本书提供了一系列常用的工具,这些工具可以用于测试假设并为分析师和数据科学家提供基本的预测能力。在探索不同测试及其适用条件之前,读者将了解理解本书中统计工具所需的基本概念和术语。此外,读者还将获得评估测试性能的知识。在整个过程中,将通过 Python 编程语言提供示例,帮助读者开始使用所提供的工具理解他们的数据,这些工具将适用于数据分析行业面临的一些最常见问题。我们将探讨以下主题:
-
统计学简介
-
回归模型
-
分类模型
-
时间序列模型
-
生存分析
理解这些部分提供的工具将为读者提供一个坚实的基础,从而更容易在统计学领域实现进一步的独立增长。
本书面向的对象
大多数行业的专业人士都可以从本书中的工具中受益。提供的工具主要用于更高层次的推理分析,但根据从业者希望应用的行业,也可以应用于更深的层次。本书的目标受众包括:
-
拥有限统计或编程知识但希望学习如何使用数据来测试他们在商业领域持有的假设的行业专业人士
-
希望扩展他们的统计知识并找到用于执行各种数据导向任务的工具及其实现的数据分析师和科学家
本书自下而上的方法旨在为广泛的受众提供进入知识库的途径,因此不应使新手级从业者感到气馁,也不应排除高级从业者从所提供材料中获益。
本书涵盖的内容
第一章, 抽样与泛化,描述了抽样和泛化的概念。抽样讨论涵盖了从总体中抽取数据的几种常见方法,并讨论了泛化的影响。本章还讨论了如何设置本书所需的软件。
第二章, 数据分布,详细介绍了数据类型、描述数据的常用分布和统计量。本章还涵盖了用于改变分布的常见转换。
第三章, 假设检验,介绍了统计检验的概念,作为回答感兴趣问题的方法。本章涵盖了进行检验的步骤、测试中遇到的错误类型,以及如何使用 Z 检验选择功效。
第四章, 参数检验,进一步讨论了统计检验,提供了常见参数统计检验的详细描述、参数检验的假设以及如何评估参数检验的有效性。本章还介绍了多重检验的概念,并提供了多重检验校正的详细信息。
第五章, 非参数检验,讨论了当参数检验的假设被违反时如何进行统计检验,即使用无假设的测试类别,称为非参数检验。
第六章, 简单线性回归,通过简单线性回归模型介绍了统计模型的概念。本章首先讨论简单线性回归的理论基础,然后讨论如何解释模型的结果以及评估模型的有效性。
第七章, 多元线性回归,在前一章的基础上,将简单线性回归模型扩展到更多维度。本章还讨论了使用多个解释变量建模时出现的问题,包括多重共线性、特征选择和降维。
第八章, 离散模型,介绍了分类的概念,并开发了一个将变量分类到分类响应变量的离散水平中的模型。本章首先开发二元分类模型,然后将模型扩展到多元分类。最后,介绍了泊松模型和负二项式模型。
第九章,判别分析,讨论了几个额外的分类模型,包括线性判别分析和二次判别分析。本章还介绍了贝叶斯定理。
第十章,时间序列简介,介绍了时间序列数据,讨论了时间序列的自相关概念和时间序列的统计度量。本章还介绍了白噪声模型和稳定性。
第十一章,ARIMA 模型,讨论了单变量模型。本章首先讨论了平稳时间序列的模型,然后将讨论扩展到非平稳时间序列。最后,本章提供了模型评估的详细讨论。
第十二章,多元时间序列,在前两章的基础上介绍了多元时间序列的概念,并将 ARIMA 模型扩展到多个解释变量。本章还讨论了时间序列的交叉相关性。
第十三章,生存分析,介绍了生存数据,也称为事件时间数据。本章讨论了截尾的概念和截尾对生存数据的影响。最后,本章讨论了生存函数、风险率和风险比。
第十四章,生存模型,在前一章的基础上,概述了几个生存数据模型,包括 Kaplan-Meier 模型、指数模型和 Cox 比例风险模型。
为了充分利用本书
您需要访问下载和安装 Python 编程语言中实现的开放源代码包,这些包可通过 PyPi.org 或 Anaconda Python 发行版访问。虽然统计学背景有帮助,但不是必需的,本书假设您有扎实的代数基础知识。本书的每个单元都是独立的,但每个单元内的章节是相互关联的。因此,我们建议您从每个单元的第一章开始,以了解 内容 。
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Python 版本 ≥ 3.8 | Windows, macOS, 或 Linux |
| Statsmodels 0.13.2 | |
| SciPy 1.8.1 | |
| lifelines 0.27.4 | |
| scikit-learn 1.1.1 | |
| pmdarima 2.02 | |
| Sktime 0.15.0 | |
| Pandas 1.4.3 | |
| Matplotlib 3.5.2 | |
| Numpy 1.23.0 |
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将有助于避免与代码复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载此书的示例代码文件github.com/PacktPublishing/Building-Statistical-Models-in-Python。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包可供选择,可在github.com/PacktPublishing/找到。查看它们!
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”
代码块设置如下:
A = [3,5,4]
B = [43,41,56,78,54]
permutation_testing(A,B,n_iter=10000)
任何命令行输入或输出都按照以下方式编写:
pip install SomePackage
粗体:表示新术语、重要单词或你在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“从管理面板中选择系统信息。”
小贴士或重要注意事项
看起来是这样的。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对此书的任何方面有疑问,请通过 customercare@packtpub.com 给我们发邮件,并在邮件主题中提及书名。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在此书中发现错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。
盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packtpub.com 与我们联系,并提供材料的链接。
如果你有兴趣成为作者:如果你在某个领域有专业知识,并且你感兴趣的是撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享你的想法
读完《使用 Python 构建统计模型》后,我们很乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
你喜欢在路上阅读,但无法携带你的印刷书籍到处走?你的电子书购买是否与你的选择设备不兼容?
别担心,现在每购买一本 Packt 图书,你都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日邮箱访问权限
按照以下简单步骤获取好处:
- 扫描下面的二维码或访问以下链接

packt.link/free-ebook/978-1-80461-428-0
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件
第一部分:统计学简介
本部分将涵盖统计建模的基础统计概念。
它包括以下章节:
-
第一章,抽样与推广
-
第二章,数据分布
-
第三章,假设检验
-
第四章,参数检验
-
第五章,非参数检验
第一章:采样和泛化
在本章中,我们将描述种群的概念以及从种群中进行采样的方法,包括一些常见的采样策略。关于采样的讨论将引出描述泛化的部分。泛化将讨论其与使用样本对其各自种群做出结论的关系。在统计推断建模时,确保样本可以推广到种群是必要的。我们将通过本章的主题深入概述这一桥梁。
我们将涵盖以下主要主题:
-
软件和环境设置
-
总体与样本
-
从样本中进行总体推断
-
采样策略 – 随机、系统性和分层
软件和环境设置
Python 是数据科学和机器学习中最受欢迎的编程语言之一,这得益于推动这些库发展的庞大开源社区。Python 的易用性和灵活性使其成为数据科学领域的首选语言,在数据科学领域,实验和迭代是开发周期的关键特征。尽管有新的语言正在开发中用于数据科学应用,例如 Julia,但由于其广泛的开源项目,支持从统计建模到深度学习等应用,Python 目前仍然是数据科学的关键语言。我们选择在本书中使用 Python,因为它在数据科学中的地位重要,并且在就业市场上需求量大。
Python 可用于所有主要操作系统:Microsoft Windows、macOS 和 Linux。此外,安装程序和文档可在官方网站找到:www.python.org/。
本书是为 Python 3.8 版本(或更高版本)编写的。建议您使用可用的最新 Python 版本。本书中的代码不太可能与 Python 2.7 兼容,并且大多数活跃的库已经从 2020 年官方支持结束以来开始停止对 Python 2.7 的支持。
本书使用的库可以使用 Python 包管理器 pip 安装,它是当代 Python 标准库的一部分。有关 pip 的更多信息,请参阅此处:docs.python.org/3/installing/index.xhtml。安装 pip 后,可以在命令行中使用 pip 安装包。以下是一些基本用法:
使用最新版本安装新包:
pip install SomePackage
使用特定版本安装包,例如本例中的版本 2.1:
pip install SomePackage==2.1
已安装的包可以使用 --upgrade 标志进行升级:
pip install SomePackage –upgrade
通常情况下,建议在项目之间使用 Python 虚拟环境,并将项目依赖项与系统目录分开。Python 提供了一个虚拟环境工具 venv,它像 pip 一样是 Python 当代版本标准库的一部分。虚拟环境允许您创建独立的 Python 二进制文件,其中每个 Python 二进制文件都有其自己的安装依赖项集。使用虚拟环境可以防止在处理多个 Python 项目时出现包版本问题和冲突。有关设置和使用虚拟环境的详细信息,请参阅:docs.python.org/3/library/venv.xhtml。
虽然我们推荐使用 Python 和 Python 的虚拟环境进行环境设置,但一个高度推荐的替代方案是 Anaconda。Anaconda 是由 Anaconda Inc.(之前为 Continuum Analytics)提供的免费(企业级)分析型 Python 发行版。Anaconda 发行版包含了许多核心数据科学包、常见的 IDE(如 Jupyter 和 Visual Studio Code)以及用于管理环境的图形用户界面。Anaconda 可以通过在 Anaconda 网站上找到的安装程序进行安装:www.anaconda.com/products/distribution。
Anaconda 自带其自己的包管理器 conda,它可以像 pip 一样用于安装新包。
使用最新版本安装新包:
conda install SomePackage
升级已安装的包:
conda upgrade SomePackage
在本书的整个过程中,我们将使用 Python 数据科学生态系统中的一些核心库,例如 NumPy 用于数组操作,pandas 用于高级数据操作,以及 matplotlib 用于数据可视化。本书使用的包版本包含在以下列表中。请确保您环境中安装的版本等于或高于列表中列出的版本。这将有助于确保代码示例能够正确运行:
-
statsmodels 0.13.2 -
Matplotlib 3.5.2 -
NumPy 1.23.0 -
SciPy 1.8.1 -
scikit-learn 1.1.1 -
pandas 1.4.3
本书代码中使用的包在此处以 图 1**.1 的形式展示。可以使用 __version__ 方法在代码中打印包版本。

图 1.1 – 本书使用的包版本
在为本书搭建好技术环境之后,让我们进入统计学的讨论。在接下来的章节中,我们将讨论总体和抽样的概念。我们将通过代码实现来演示抽样策略。
总体与样本
通常,统计模型的目标是通过对该群体进行推断来回答关于该群体的问题。我们进行推断的群体可能是生产工厂中的机器、选举中投票的人,或者不同地块上的植物。整个群体,每个单独的项目或实体,被称为总体。在大多数情况下,感兴趣的总体非常大,以至于收集总体中每个实体的数据既不实际也不可能。例如,使用投票的例子,可能无法对选举中投票的每个人进行调查。即使有可能接触到感兴趣的选举的所有选民,许多选民可能不会同意调查,这将阻止对整个总体的收集。另一个考虑因素是调查如此大群体的费用。这些因素使得在我们的投票调查示例中收集总体统计数据实际上是不可能的。在我们可能想要评估总体属性的情况下,存在许多这类禁止性因素。幸运的是,我们不需要收集感兴趣总体中的所有数据。可以使用总体的一部分来进行关于总体的推断。这个总体的子集被称为样本。这是统计模型的主要思想。将使用样本创建模型,并对总体进行推断。
为了使用样本对感兴趣的总体进行有效的推断,样本必须代表感兴趣的总体,这意味着样本应该包含在总体中发现的变异。例如,如果我们对田野中的植物感兴趣,那么来自田野一个角落的样本可能不足以对更大的总体进行推断。整个田野中植物特征可能会有所不同。我们可以考虑各种可能存在变异的原因。对于这个例子,我们将考虑来自图 1.2的一些例子。

图 1.2 – 植物田地
该图显示样本 A靠近森林。这个样本区域可能受到森林存在的影响;例如,该样本中的一些植物可能比其他样本中的植物接收到的阳光更少。样本 B显示位于主要灌溉线之间。可以想象这个样本平均接收到的水可能比其他两个样本多,这可能会影响这个样本中的植物。最后的样本 C靠近道路。这个样本可能看到在样本 A或样本 B中看不到的其他影响。
如果样本只从这些部分中的一个抽取,那么从这些样本中得出的推断将会是有偏的,并且不会提供关于人群的有效参考。因此,需要从整个领域抽取样本,以创建一个更有可能代表植物人群的样本。在从人群抽取样本时,确保抽样方法对可能的问题具有鲁棒性,例如前例中灌溉和遮荫的影响,是至关重要的。每次从人群抽取样本时,都重要的是要识别和减轻可能的偏差影响,因为数据中的偏差会影响你的模型并歪曲你的结论。
在下一节中,将讨论从数据集中进行抽样的各种方法。另一个考虑因素是样本大小。样本大小影响我们可以使用的统计工具类型,关于样本的分布假设,以及推断和预测的置信度。样本大小的影响将在第二章**,数据分布和第三章**,假设检验*中深入探讨。
从样本中进行人群推断
当使用统计模型从该人群的样本子集中对人群进行推断性结论时,研究设计必须考虑到其变量与人群中的变量具有相似的不确定性程度。这就是本章前面提到的变异。为了适当地对人群进行推断性结论,任何统计模型都必须围绕一个随机机制来构建。围绕这些随机机制构建的研究被称为随机实验,并提供了对相关性和因果关系的理解。
随机实验
随机实验有两个主要特征:
-
随机抽样,俗称随机选择
-
治疗的随机分配,这是研究的本质
随机抽样
随机抽样(也称为随机选择)旨在创建一个代表总体人口的样本,以便统计模型能够足够好地推广总体,从而分配因果关系的结果。为了使随机抽样成功,感兴趣的总体必须定义良好。从总体中抽取的所有样本都必须有被选中的机会。在考虑民意调查选民的情况时,所有选民都必须愿意接受调查。一旦所有选民都进入彩票,就可以使用随机抽样来对选民进行子集建模。仅从愿意接受调查的选民中进行抽样会引入统计模型中的抽样偏差,这可能导致结果偏斜。只有一些选民愿意参与的情景中的抽样方法被称为自我选择。从自我选择的样本——或任何非随机样本——中获得并建模的信息不能用于推断。
治疗的随机分配
治疗的随机分配涉及两个动机:
-
第一个动机是了解特定的输入变量及其对响应的影响——例如,了解将治疗 A 分配给特定个体是否可能比安慰剂产生更理想的结果。
-
第二个动机是消除外部变量对研究结果的影响。这些被称为混杂变量(或混杂因素)的外部变量很重要,因为它们往往难以控制。它们可能具有不可预测的值,甚至可能对研究者来说是未知的。包括混杂变量的后果是,研究的结果可能无法复制,这可能是代价高昂的。虽然混杂变量可以影响结果,但它们也可以影响输入变量,以及这些变量之间的关系。
回顾前一小节中的例子,即总体与样本,考虑一位农民决定开始在他的作物上使用杀虫剂,并想要测试两种不同的品牌。这位农民知道土地上有三个不同的区域;地块 A、地块 B 和地块 C。为了确定杀虫剂的成功率并防止作物受损,农民从每个地块随机选择 60 株植物进行测试(这被称为分层随机抽样,即在每个地块上进行分层随机抽样)。这个选择代表了植物总体。从这个选择中,农民对他的植物进行标记(标记不需要是随机的)。对于每个地块,农民将标签随机放入袋子中,以随机化它们,然后开始选择 30 株植物。前 30 株植物接受一种处理,另外 30 株接受另一种处理。这是随机分配的处理。假设三个单独的地块代表了一组不同的混杂变量对作物产量的影响,农民将拥有足够的信息来对每个杀虫剂品牌的作物产量进行推断。
观察性研究
常见的另一种统计研究类型是观察性研究,其中研究人员通过观察已有的数据来寻求了解。观察性研究有助于理解输入变量及其与目标和彼此之间的关系,但不能像随机实验那样提供因果关系理解。观察性研究可能具有随机实验的两个组成部分之一——要么是随机抽样,要么是随机分配的处理——但没有这两个组成部分,将不会直接产生推断。有许多原因可能导致进行观察性研究而不是随机实验,例如以下情况:
-
随机实验成本过高
-
实验的伦理限制(例如,确定怀孕期间吸烟导致出生缺陷发生率的实验)
-
使用先前随机实验的数据,从而消除了进行另一项实验的需要
从观察性研究中推导出一些因果关系的 一种方法是进行随机抽样和重复分析。重复随机抽样和分析可以帮助最小化混杂变量随时间的影响。这个概念在大数据和机器学习的有用性中扮演着巨大的角色,这两者在本世纪许多行业中都获得了很大的重视。虽然几乎任何可以用于观察性分析的工具也可以用于随机实验,但本书主要关注的是观察性分析的工具,因为在大多数行业中这更为常见。
可以说,统计学是一门帮助在存在可量化不确定性时做出最佳决策的科学。所有统计检验都包含一个零假设和一个备择假设。也就是说,一个假设数据之间没有统计学上显著的差异(零假设)或者数据之间存在统计学上显著的差异(备择假设)。统计学上显著差异这一术语意味着存在一个基准——或阈值——超过这个基准,一个度量就会发生并表明其显著性。这个基准被称为临界值。
应用到这个临界值上的度量被称为检验统计量。临界值是基于数据中的行为(如平均值和变异)量化的静态值,并基于假设。如果有两条可能的路径可以拒绝零假设——例如,我们相信某些输出要么小于平均值,要么大于平均值——将有两个临界值(这种检验称为双尾假设检验),但如果只有一条反对零假设的论据,则只有一个临界值(这称为单尾假设检验)。无论临界值的数量有多少,在给定的假设检验中,每个组内都只有一个检验统计量度量。如果检验统计量超过临界值,就有统计学上显著的依据来支持拒绝零假设,并得出数据之间存在统计学上显著差异的结论。
有必要理解假设检验可以检验以下内容:
-
一个变量对另一个变量(例如,在 t 检验中)
-
多个变量对一个变量(例如,线性回归)
-
多个变量对多个变量(例如,多变量方差分析)
在下面的图中,我们可以直观地看到检验统计量与双尾假设检验中的临界值之间的关系。

图 1.3 – 双尾假设检验中临界值与检验统计量之间的关系
根据图示,我们现在对检验统计量超过临界值表明拒绝零假设有了直观的认识。
然而,仅使用将测试统计量与假设中的临界值进行比较的方法,存在一个担忧,即测试统计量可能过大,不切实际。这种情况可能发生在结果范围很广,而这些结果被认为不属于治疗效果范围时。是否可能得到与测试统计量一样极端或更极端的结果尚不确定。为了防止错误地拒绝零假设,使用了p 值。p 值表示仅凭偶然导致观察到的值(表明拒绝零假设的值)的概率。如果 p 值相对于显著性水平较低,则可以拒绝零假设。常见的显著性水平为 0.01、0.05 和 0.10。在做出关于假设的决定之前,评估临界值与测试统计量之间的关系以及 p 值是有益的。更多内容将在第三章**,假设检验中讨论,当我们开始讨论假设检验时。
抽样策略 – 随机、系统、分层和聚类
在本节中,我们将讨论在研究中使用的不同抽样方法。总的来说,在现实世界中,由于许多原因,很难或不可能获取整个总体数据。例如,收集数据的成本在金钱和时间上都很昂贵。在许多情况下,收集所有数据是不切实际的,而且还要考虑伦理问题。从总体中抽取样本可以帮助我们克服这些问题,并且是收集数据的一种更有效的方法。通过为研究收集适当的样本,我们可以对总体属性进行统计结论或统计推断。推断性统计分析是统计思维的基本方面。本节将讨论从概率策略到在研究和工业中使用的非概率策略的不同抽样方法。
实际上存在两种类型的抽样方法:
-
概率抽样
-
非概率抽样
概率抽样
在概率抽样中,样本是根据概率理论从总体中选择的,或者是通过随机选择随机选择的。在随机选择中,总体中每个成员被选中的机会是相等的。例如,考虑一个有 10 张相似纸张的游戏。我们写上数字 1 到 10,每张纸对应一个数字。然后这些数字在一个盒子里被打乱。游戏要求随机挑选这三张纸张中的三张。因为纸张是使用相同的过程准备的,所以任何一张纸张被选中的机会(或数字 1 到 10)对每一张都是相等的。总的来说,这 10 张纸张被视为一个总体,而选出的 3 张纸张构成一个随机样本。这个例子是我们将在本章讨论的概率抽样方法之一。

图 1.4 – 随机抽样示例
我们可以使用之前描述的(并在图 1.4**.4中展示的)抽样方法,使用numpy实现。我们将使用choice方法从给定的人口中选取三个样本。注意在choice中使用replace==False。这意味着一旦一个样本被选中,它将不会再次被考虑。注意,在以下代码中使用随机生成器是为了可重复性:
import numpy as np
# setup generator for reproducibility
random_generator = np.random.default_rng(2020)
population = np.arange(1, 10 + 1)
sample = random_generator.choice(
population, #sample from population
size=3, #number of samples to take
replace=False #only allow to sample individuals once
)
print(sample)
# array([1, 8, 5])
随机选择的目的在于避免当人口中的某些单位在样本中被选中的概率低于或高于其他单位时,产生偏差的结果。如今,可以通过使用计算机随机化程序来完成随机选择过程。
这里将要讨论的四种主要类型的概率抽样方法如下:
-
简单随机抽样
-
系统抽样
-
分层抽样
-
群体抽样
让我们逐一来看它们。
简单随机抽样
首先,简单随机抽样是从人口中随机选择样本的方法。通过无偏选择方法,子集(或样本)中的每个成员都有相同的机会被选中。当人口中的所有成员都与重要变量(重要特征)有相似的性质时,这种方法被使用,并且这是概率抽样的最直接方法。这种方法的优势在于最小化偏差并最大化代表性。然而,虽然这种方法有助于限制偏差的方法,但简单随机抽样存在错误的风险。这种方法也有一些局限性。例如,当人口非常大时,可能需要很高的成本和大量时间。当样本不能代表人口且研究需要再次进行此抽样过程时,需要考虑抽样误差。此外,并不是人口中的每个成员都愿意自愿参与研究,这使得获取代表大量人口的良好信息成为一个巨大的挑战。选择 10 张纸中的 3 张纸的先前例子是一个简单随机样本。
系统抽样
在这里,人口中的成员以固定的采样间隔在随机起点被选中。我们首先通过将人口中的成员数除以研究中进行的样本成员数来选择一个固定的采样间隔。然后,在采样间隔的成员数之间选择一个介于第一个成员和成员数之间的随机起点。最后,通过重复此采样过程来选择后续成员,直到收集到足够的样本。当成本和时间是研究需要考虑的主要因素时,这种方法比简单的随机抽样更快、更可取。另一方面,在简单随机抽样中,每个成员被选中的机会是均等的,而在系统抽样中,使用采样间隔规则从样本中选择一个成员进行研究。可以说,系统抽样比简单随机抽样更不随机。同样,在简单随机抽样中,成员属性与重要变量/特征的相关性也类似。让我们通过以下例子来讨论我们如何进行系统抽样。在德克萨斯州达拉斯的一所高中的班级中,有 50 名学生,但只有 10 本书要分给这些学生。通过将班级中的学生数除以书籍数(50/10 = 5)来固定采样间隔。我们还需要生成一个介于 1 和 50 之间的随机数作为随机起点。例如,取数字 18。因此,被选中获得书籍的 10 名学生如下:
18, 23, 28, 33, 38, 43, 48, 3, 8, 13
自然会提出一个问题,即区间抽样是否是一个分数。例如,如果我们有 13 本书,那么采样间隔将是 50/13 ~ 3.846。然而,我们不能选择这个分数作为代表学生数量的采样间隔。在这种情况下,我们可以选择数字 3 或 4 作为采样间隔(我们也可以选择 3 或 4 作为采样间隔)。让我们假设生成的随机起始点是 17。那么,选出的 13 名学生是这些:
17, 20, 24, 27, 31, 34, 38, 41, 45, 48, 2, 5, 9
观察前面的数字序列,在达到数字 48 之后,由于加上 4 会产生一个大于学生人数(50 名学生)的数字,因此序列从 2 重新开始(48 + 4 = 52,但由于 50 是最大值,所以我们从 2 重新开始)。因此,序列中的最后三个数字是 2、5 和 9,分别对应 4、3 和 4 的采样间隔(通过数字 50 并返回到数字 1,直到我们选出 13 名学生进行系统抽样)。
在系统抽样中,当人口成员的列表组织以匹配采样间隔时,存在偏差风险。例如,回到 50 名学生的案例,研究人员想知道学生对数学课的看法。然而,如果数学成绩最好的学生对应于数字 2、12、22、32 和 42,那么如果随机起始点是 2 且采样间隔是 10,调查可能会存在偏差。
分层抽样
这是一个基于将人口划分为同质子种群称为层的概率抽样方法。每个层根据明显不同的属性进行划分,例如性别、年龄、颜色等。这些子种群必须是不同的,以便每个层中的每个成员都有相同的机会被简单随机抽样选中。图 1.5说明了如何从两个子种群(一组数字和一组字母)中进行分层抽样以选择样本:

图 1.5 – 分层抽样示例
以下代码示例展示了如何使用图 1.5中的示例通过numpy实现分层抽样。首先,实例被分割到相应的层:数字和字母。然后,我们使用numpy从每个层中抽取随机样本。就像之前的代码示例一样,我们利用choice方法抽取随机样本,但每个层的样本大小是基于每个层中实例的总数,而不是整个种群中的总数;例如,抽取数字的 50%和字母的 50%:
import numpy as np
# setup generator for reproducibility
random_generator = np.random.default_rng(2020)
population = [
1, "A", 3, 4,
5, 2, "D", 8,
"C", 7, 6, "B"
]
# group strata
strata = {
'number' : [],
'string' : [],
}
for item in population:
if isinstance(item, int):
strata['number'].append(item)
else:
strata['string'].append(item)
# fraction of population to sample
sample_fraction = 0.5
# random sample from stata
sampled_strata = {}
for group in strata:
sample_size = int(
sample_fraction * len(strata[group])
)
sampled_strata[group] = random_generator.choice(
strata[group],
size=sample_size,
replace=False
)
print(sampled_strata)
#{'number': array([2, 8, 5, 1]), 'string': array(['D', 'C'], dtype='<U1')}
这种方法的主要优势是,样本中的关键群体特征更好地代表了所研究的群体,并且与整体群体成比例。这种方法有助于减少样本选择偏差。另一方面,当将群体中的每个成员分类到不同的子群体中不明显时,这种方法变得不可用。
聚类抽样
在这里,一个群体被划分为不同的子群体,称为聚类。每个聚类具有同质特征。不是在每个聚类中随机选择个别成员,而是随机选择整个聚类,并且这些聚类有同等的机会被选中作为样本的一部分。如果聚类很大,那么我们可以通过使用之前提到的抽样方法之一来选择每个聚类内的个别成员,从而进行多阶段抽样。现在讨论一个聚类抽样的例子。一家当地比萨饼店计划在其社区内扩展业务。店主想知道有多少人从他的比萨饼店订购比萨饼,以及最受欢迎的比萨饼是什么。然后他将社区划分为不同的区域,并随机选择客户形成聚类样本。他向选定的客户发送调查问卷以进行其业务研究。另一个例子与多阶段聚类抽样相关。一家连锁零售店进行一项研究,以查看连锁店中每家店铺的表现。店铺根据位置划分为子群体,然后随机选择样本形成聚类,并将样本聚类用作其店铺的表现研究。这种方法既简单又方便。然而,样本聚类并不保证能代表整个群体。
非概率抽样
另一种抽样方法是非概率抽样,在这种方法中,一个群体中的某些或所有成员没有平等的机会被选中作为样本参与研究。当无法进行随机概率抽样,且与概率抽样方法相比,这种方法更快、更容易获取数据时,会使用这种方法。使用这种方法的一个原因是因为成本和时间考虑。它允许我们通过基于便利性或某些标准进行非随机选择来轻松收集数据。这种方法可能导致比概率抽样方法更高的偏差风险。这种方法通常用于探索性和定性研究。例如,如果一组研究人员想要了解客户对其产品相关公司意见,他们会向购买并使用该产品的客户发送调查问卷。这是一种获取意见的便捷方式,但这些意见仅来自已经使用过产品的客户。因此,样本数据仅代表一组客户,不能作为公司所有客户的意见进行推广。

图 1.6 – 一项调查研究的示例
之前的例子是我们想要在此处讨论的两种非概率抽样方法之一。这种方法是便利抽样。在便利抽样中,研究人员从总体中选择最易于接触的成员来形成一个样本。这种方法简单且成本低,但将获得的结果推广到整个总体是有疑问的。
配额抽样是另一种非概率抽样,其中通过非随机方式选择一个样本组,使其代表更大的总体。例如,时间有限的招聘人员可以使用配额抽样方法从专业社交网络(LinkedIn、Indeed.com 等)中寻找潜在候选人并进行面试。这种方法既经济又节省时间,但在选择过程中存在偏差。
在本节中,我们概述了概率抽样和非概率抽样。每种策略都有其优势和劣势,但它们有助于我们最小化风险,例如偏差。一个精心规划的抽样策略还将有助于减少预测模型中的错误。
摘要
在本章中,我们讨论了安装和设置 Python 环境以运行Statsmodels API 和其他必需的开源软件包。我们还讨论了总体与样本以及从样本中获得推断的要求。最后,我们解释了在统计和机器学习模型中常用的几种不同抽样方法。
在下一章中,我们将开始讨论统计分布及其对构建统计模型的影响。在第三章**假设检验中,我们将深入讨论假设检验,扩展本章观察性研究部分讨论的概念。我们还将讨论功效分析,这是一种基于现有样本数据参数和所需统计显著性水平的工具,用于确定样本量。
第二章:数据分布
在本章中,我们将涵盖数据和分布的基本方面。我们将从介绍数据类型和数据分布开始。在介绍分布的基本度量后,我们将描述正态分布及其重要特性,包括中心极限定理。最后,我们将涵盖重采样方法,如排列,以及变换方法,如对数变换。本章涵盖了开始统计建模所需的基础知识。
在本章中,我们将涵盖以下主要主题:
-
理解数据类型
-
测量和描述分布
-
正态分布和中心极限定理
-
自举
-
排列
-
变换
技术要求
本章将使用 Python 3.8。
本章的代码可以在以下位置找到 – github.com/PacktPublishing/Building-Statistical-Models-in-Python – 在ch2文件夹中。
请设置一个虚拟环境或 Anaconda 环境,并安装以下包:
-
numpy==1.23.0 -
scipy==1.8.1 -
matplotlib==3.5.2 -
pandas==1.4.2 -
statsmodels==0.13.2
理解数据类型
在讨论数据分布之前,了解数据类型会有所帮助。理解数据类型至关重要,因为数据类型决定了可以使用哪些分析,因为数据类型决定了可以使用哪些操作来处理数据(这将在本章的示例中变得更加清晰)。有四种不同的数据类型:
-
名义数据
-
序数数据
-
区间数据
-
比率数据
这些类型的数据也可以分为两组。前两种数据类型(名义和序数)是定性数据,通常是非数字类别。最后两种数据类型(区间和比率)是定量数据,通常是数值。
让我们从名义数据开始。
名义数据
名义数据是对具有不同分组进行标记的数据。例如,考虑一个标志工厂中的机器。工厂通常从不同的供应商那里采购机器,这也会有不同的型号号。例如,示例工厂可能有 3 台型号 A和 5 台型号 B(见图 2.1)。机器将构成一组名义数据,其中型号 A和型号 B是不同的分组标签。对于名义数据,只有一个操作可以执行:相等。组内的每个成员都是相等的,而来自不同组的成员是不相等的。在我们的工厂示例中,一台型号 A机器将等于另一台型号 A机器,而一台型号 B机器将不等于一台型号 A机器。

图 2.1 – 工厂中的两组机器
如我们所见,使用这种类型的数据,我们只能在标签下将项目分组。在下一类型的数据中,我们将介绍一个新特性:顺序。
序数数据
下一种类型的数据类似于名义数据,但表现出顺序。数据可以被标记成不同的组,并且这些组可以排序。我们称这种类型的数据为序数数据。继续以工厂为例,假设有一个Model C型号的机器,Model C与Model B由相同的供应商提供。然而,Model C是高性能版本,产生更高的输出。在这种情况下,Model B和Model C是序数数据,因为Model B是低输出机器,而Model C是高输出机器,这创造了一个自然顺序。例如,我们可以按性能的升序排列模型标签:Model B,Model C。大学教育水平也是序数数据的另一个例子,有 BS、MS 和 PhD 等级。如前所述,这种类型数据的新操作是排序,意味着数据可以被排序。因此,序数数据支持排序和相等性。虽然这种类型的数据可以按升序或降序排序,但我们不能对数据进行加减运算,这意味着Model B + Model C不是一个有意义的陈述。我们将讨论的下一类型数据将支持加减运算。
区间数据
下一种类型的数据,区间数据,用于描述存在于区间尺度上的数据,但没有明确的零点定义。这意味着两个数据点之间的差异是有意义的。以摄氏温度尺度为例。数据点是数值的,并且数据点在区间内均匀分布(例如,20 和 40 都距离 30 有 10 度)。在这个温度尺度的例子中,0 的定义是任意的。对于摄氏度来说,0 恰好被设定在水的冰点,但这是尺度设计者做出的一个任意选择。因此,区间数据类型支持相等性、排序和加减运算。
比例数据
最后一种数据类型是比例数据。与区间数据一样,比例数据是有序的数值数据,但与区间数据不同,比例数据有一个绝对零点。绝对零点意味着如果一个比例类型变量的值为零,那么该变量不存在或不存在。例如,考虑游乐园的等待时间。如果没有人在等待游乐设施,等待时间就是 0;新客人可以立即乘坐游乐设施。等待时间没有有意义的负值。0 是绝对最小值。比例数据也支持有意义的乘除运算,这使得比例数据成为支持操作最多的数据类型。
数据类型的可视化
数据可视化是理解分布和识别数据属性的关键步骤。在本章(以及整本书中),我们将使用matplotlib来可视化数据。虽然可以使用其他 Python 库进行数据可视化,但matplotlib是 Python 事实上的标准绘图库。在本节中,我们将开始使用matplotlib来可视化之前讨论的四种类型的数据。
绘制定性数据类型
由于前两种数据类型是分类的,我们将使用柱状图来可视化这些数据的分布。示例柱状图显示在图 2.2 中。

图 2.2 – 柱状图中的名义数据(左侧)和有序数据(右侧)
图 2.2 左侧的柱状图显示了工厂示例中给出的模型 A机器和模型 B机器的分布。右侧的柱状图显示了工程师团队的教育水平分布示例。请注意,在教育水平柱状图中,x轴标签按教育水平从低到高排序。
生成图 2.2 所使用的代码如下所示。
代码有三个主要部分。
- 库导入:
在这个例子中,我们只从matplotlib中导入pyplot,它通常以plt的形式导入。
- 创建数据的代码:
在import语句之后,有一些语句用于创建我们将要绘制的图形所需的数据。第一个图形的数据存储在两个 Python 列表中:label和counts,分别包含机器标签和机器数量。值得注意的是,这两个列表包含相同数量的元素(两个元素)。教育数据以类似的方式存储。虽然在这个例子中,我们使用的是简单的示例数据,但在后面的章节中,我们将有额外的步骤来检索和格式化数据。
- 绘制数据的代码:
最后一步是绘制数据。由于在这个例子中我们要绘制两组数据,我们使用 subplots 方法,这将创建一个图表网格。subplots 方法的两个参数是网格中图表的行数和列数。在我们的情况下,行数是 1,列数是 2。subplots 方法返回两个对象;图,fig,和坐标轴,ax。返回的第一个对象,fig,对图有高级控制,例如保存图、在新窗口中显示图等。第二个对象,ax,将是一个单独的坐标轴对象或坐标轴对象的数组。在我们的情况下,ax 是坐标轴对象的数组——因为我们的网格有两个图表,对 ax 进行索引给我们坐标轴对象。我们使用坐标轴对象的 bar 方法创建柱状图。bar 方法有两个必需的参数。第一个必需的参数是标签列表。第二个参数是与每个标签对应的柱高,这就是为什么两个列表必须有相同长度。其他三个方法,set_title、set_ylabel 和 set_xlabel,设置相应图表属性的值:title、ylabel 和 x-label。
最后,使用 fig.show() 创建图:
import matplotlib.pyplot as plt
label = ['model A', 'model B']
counts = [3, 5]
edu_label = ['BS', 'MS', 'PhD']
edu_counts = [10, 5, 2]
fig, ax = plt.subplots(1, 2, figsize=(12, 5))
ax[0].bar(label, counts)
ax[0].set_title('Counts of Machine Models')
ax[0].set_ylabel('Count')
ax[0].set_xlabel('Machine Model')
ax[1].bar(edu_label, edu_counts)
ax[1].set_title('Counts of Education Level')
ax[1].set_ylabel('Count')
ax[1].set_xlabel('Education Level')
fig.show()
现在我们来看如何绘制其他两种数据类型的数据。
绘制定量数据类型
由于最后两种数据类型是数值型的,我们将使用直方图来可视化分布。图 2.3 中显示了两个示例直方图。

图 2.3 – 柱状图中的名义数据(左侧)和有序数据(右侧)
左侧的直方图是合成等待时间数据(比率数据),可能代表游乐园的等待时间。右侧的直方图是 2022 年 4 月和 5 月达拉斯-沃斯堡地区的温度数据(来自 www.iweathernet.com/texas-dfw-weather-records)。
生成 图 2.3 所使用的代码如下。同样,代码有三个主要部分,库导入、数据创建代码和绘图代码。
与前面的例子一样,matplotlib 被导入为 plt。在这个例子中,我们还从 scipy 中导入了一个函数;然而,这个函数仅用于生成用于工作的样本数据,我们不会在这里详细讨论它。对于我们的目的,只需将 skewnorm 视为一个生成数字数组的函数即可。这个代码块与前面的代码块非常相似。
主要区别在于用于绘制数据的图表方法,hist,它创建直方图。hist方法有一个必需的参数,即要在直方图中绘制的数字序列。本例中使用的第二个参数是bins,它实际上控制了直方图的粒度 – 粒度随着更多的 bins 而增加。直方图的 bin 计数可以根据所需的视觉效果进行调整,通常通过实验设置来绘制数据:
from scipy.stats import skewnorm
import matplotlib.pyplot as plt
a = 4
x = skewnorm.rvs(a, size=3000) + 0.5
x = x[x > 0]
dfw_highs = [
85, 87, 75, 88, 80, 86, 90, 94, 93, 92, 90, 92, 94,
93, 97, 90, 95, 96, 96, 95, 92, 70, 79, 73, 88, 92,
94, 93, 95, 76, 78, 86, 81, 95, 77, 71, 69, 88, 86,
89, 84, 82, 77, 84, 81, 79, 75, 75, 91, 86, 86, 84,
82, 68, 75, 78, 82, 83, 85]
fig, ax = plt.subplots(1,2, figsize=(12, 5))
ax[0].hist(x, bins=30)
ax[0].set_xlabel('Wait Time (hr)')
ax[0].set_ylabel('Frequency')
ax[0].set_title('Wait Times');
ax[1].hist(dfw_highs, bins=7)
ax[1].set_title('High Temperatures for DFW (4/2022-5/2022)')
ax[1].set_ylabel('Frequency')
ax[1].set_xlabel('Temperature (F)')
fig.show()
在本节中,我们简要了解了数据和分布的多样性。由于数据分布在外部世界中以许多形状和大小出现,因此拥有描述分布的方法是有用的。在下一节中,我们将讨论可用于分布的测量方法,这些测量的执行方式以及可以测量的数据类型。
测量和描述分布
在野外发现的数据分布形状和大小各异。本节将讨论如何测量分布以及哪些测量适用于四种类型的数据。这些测量将提供比较和对比不同分布的方法。本节讨论的测量可以划分为以下类别:
-
中心趋势
-
变异性
-
形状
这些测量被称为描述性统计。本节中讨论的描述性统计在数据统计摘要中常用。
测量中心趋势
中心趋势的测量有三种类型:
-
模式
-
中位数
-
均值
让我们逐一讨论它们。
模式
我们将首先讨论的中心趋势测量是模式。数据集的模式简单地说是最常见的实例。以工厂中的机器为例(见图 2.1),数据集的模式将是型号 B。在例子中,有 3 台型号 A 和 5 台型号 B,因此,使型号 B 成为最常见的 – 模式。
数据集可以是以下之一:
-
单模态 – 具有一个模式
-
多模态 – 具有多个模式
在前面的例子中,数据是单模态的。
再次使用工厂的例子,让我们想象有 3 台型号 A,5 台型号 B和 5 台型号 D(一种新型号)。那么,数据集将有两个模式:型号 B和型号 D,如图图 2.4所示。

图 2.4 – 工厂中机器的多模态分布
因此,这个数据集是多模态的。
模式和数据类型
这些模式的例子使用了名义数据,但所有四种数据类型都支持模式,因为所有四种数据类型都支持等价操作。
虽然“众数”指的是最常见的实例,但在连续数据的多个峰值的多元情况下,术语“众数”通常被用来表示一个不那么严格的概念。例如,图 2.5 中的分布通常被称为多峰分布,尽管分布的峰值大小并不相同。然而,在名义数据和有序数据中,当提到分布的模态时,更常见的是使用更严格的“最常见”的定义。

图 2.5 – 数据的多峰分布
现在,我们将看看如何使用 scipy 代码计算众数。scipy 库包含 stats 模块中的描述性统计函数。在这个例子中,我们从 scipy.stats 导入 mode 并计算以下数字的众数,1, 2, 3, 4, 4, 4, 5, 5:
from scipy.stats import mode
m = mode([1,2,3,4,4,4,5,5])
print(
f"The mode is {m.mode[0]} with a count of"
f" {m.count[0]} instances"
)
# The mode is 4 with a count of 3 instances
mode 函数返回一个包含 mode 和 count 成员的 mode 对象。不出所料,mode 和 count 成员分别包含数据集的众数和众数出现的次数。请注意,mode 和 count 成员是可索引的(就像列表一样),因为数据集可以包含多个众数。
中位数
中心测量的下一个指标是中位数。中位数是在将数值按顺序排列时出现的中间值。
中位数和数据类型
这种测量可以在有序数据、区间数据和比率数据上执行,但不能在名义数据上执行。
我们将在这里讨论两种情况。
当实例数量为奇数时寻找中位数
在 图 2.6 中显示了某些数值数据的中位数。数据被排序后,中位数被识别。

图 2.6 – 使用奇数个实例识别中位数
在前面的例子中,实例数量为奇数(7 个实例),有一个中心值。然而,如果实例数量是偶数,在排序值之后,就不能简单地取中间的数字了。
当实例数量为偶数时寻找中位数
当实例数量为偶数时,取两个中间值的最小值。与众数不同,对于同一系列数据,没有多个中位数的概念。一个具有偶数个实例(8 个实例)的例子在 图 2.7 中显示。

图 2.7 – 使用偶数个实例识别中位数
现在,让我们看看如何使用 numpy 计算数据集的中位数。与 scipy 一样,numpy 包含用于计算描述性统计的函数。我们将计算前面例子中列出的八个数字的中位数:
import numpy as np
values = [85, 99, 70, 71, 86, 88, 94, 105]
median = np.median(values)
print(f"The median value is {median:.2f}")
# The median value is 87.00
计算中位数的结果是 87,正如预期的那样。请注意,median 函数返回一个单一值,与前面代码示例中的 mode 函数不同。
平均值
下一个中心度量是均值,通常被称为平均值。均值由以下方程定义:
_ x = ∑ i=0 n x i _ N
让我用文字解释一下这个方程。要计算均值,我们必须将所有值相加,然后将总和除以值的数量。请参考以下示例。首先将 7 个数字相加,总和达到 593。然后将这个总和除以实例数量,得到 84.7 的值。

图 2.8 – 寻找均值
注意,这些值的均值和中位数(分别为 84.7 和 86)并不相同。一般来说,均值和中位数不会是相同的值,但存在一些特殊情况,均值和中位数会收敛。
均值和数据类型
至于支持的数据类型,均值适用于区间和比率数据,因为值是可以相加的。
现在,我们将看看如何使用numpy计算均值。以下代码示例显示了之前示例中值的均值的计算:
import numpy as np
values = [85, 99, 70, 71, 86, 88, 94]
mean = np.mean(values)
print(f"The mean value is {mean:.1f}")
# The mean value is 84.7
与median函数一样,mean函数返回一个单一的数字。
在结束关于中心度量的这一节之前,讨论均值和中位数在各种情况下的使用是值得的。如前所述,中位数和均值通常会是不同的值。这是由分布的形状驱动的效应。
形状对均值和中位数的影响
如果分布是对称的,均值和中位数往往会收敛。然而,如果分布不是对称的,均值和中位数会发散。
度量之间的差异程度由分布的不对称性驱动。图 2.6给出了四个示例分布来展示这一效果。分布 1 和 2 显示均值被拉向比中位数更高的值。均值会被拉向绝对值更大的值。当数据集包含(或可能包含)异常值(通常称为异常值或影响点)时,这是一个均值的重要影响,这些异常值会倾向于将其拉向它们的方向。与均值不同,如果异常值占数据比例较小,中位数不会受到异常值的影响。异常值将在测量变异性部分进一步讨论。

图 2.9 – 两个不对称分布和两个对称分布
分布的下一个测量类别是变异性度量。
测量变异性
通过变异性,我们本质上是指分布的宽度。这一类别的测量如下:
-
范围
-
四分位距
-
Tukey fences
-
方差
让我们讨论每一个。
范围
范围是分布中最大值和最小值之间的差值。像均值一样,范围会受到异常值的影响,因为它依赖于最大值和最小值。然而,还有一种变异方法,就像中位数一样,对异常值的存在具有鲁棒性。
让我们用代码和 numpy 来计算一个范围:
import numpy as np
values = [85, 99, 70, 71, 86, 88, 94, 105]
max_value = np.max(values)
min_value = np.min(values)
range_ = max_value - min_value
print(f"The data have a range of {range_}"
f" with max of {max_value}"
f" and min of {min_value}")
# The data have a range of 35 with max of 105 and min of 70
虽然 numpy 没有范围函数,但可以使用 numpy 提供的 min 和 max 函数来计算范围。
四分位距
下一个变异度量是通过排序数据然后将数据分成四个相等的部分来确定的。四个部分的边界是四分位数,它们被称为以下:
-
下四分位数(Q1)
-
中间四分位数(Q2)
-
上四分位数(Q3)
以下是一个四分位数的例子。像中位数一样,只要异常值占数据集的比例很小,四分位数对异常值就具有鲁棒性。请注意,中间四分位数实际上就是中位数。一种对异常值比 范围 部分中讨论的正常范围更不敏感的调整范围测量方法是中间四分位数,即 四分位距(IQR)。IQR 是上四分位数(Q3)和下四分位数(Q1)之间的差值。虽然这个范围对异常值不太敏感,但它只包含 50% 的数据。因此,四分位距很可能是不太能代表数据总变异的。

图 2.10 – Q1, Q2 和 Q3
我们可以使用 numpy 和 scipy 来计算四分位数和 IQR 范围。在下面的代码示例中,我们使用 quantiles 函数来计算四分位数。我们在这里不会讨论 quantiles,除了提到 quantiles 是一种泛化,其中数据可以被分成任何数量的相等部分。由于我们是将数据分成四个相等的部分来计算四分位数,所以用于计算的 quantiles 值是 0.25、0.5 和 0.75。然后可以使用 Q1 和 Q3 来计算 IQR。但是,我们也可以使用 scipy 中的 iqr 函数来进行计算:
import numpy as np
from scipy import stats
values = [85, 99, 70, 71, 86, 88, 94]
quartiles = np.quantile(values, [0.25, 0.5, 0.75],
method="closest_observation")
print(f"The quartiles are Q1: {quartiles[0]},
Q2: {quartiles[1]}, Q3: {quartiles[2]}")
iqr = stats.iqr(values,interpolation='closest_observation')
print(f"The interquartile range is {iqr}")
# The quartiles are Q1: 71, Q2: 85, Q3: 88
# The interquartile range is 17
注意 quantiles 函数和 iqr 函数中 method 和 interpolation 关键字参数的使用。可以对这些关键字参数使用几种选项,这将导致不同的结果。
四分位数通常用箱线图来可视化。以下 *图 2**.11 展示了箱线图的主要部分。箱线图由两个主要部分组成:
-
箱体
-
须线
箱体部分代表包含在 IQR 内的 50% 的数据。须线从箱体的边缘延伸到长度为 k * IQR,其中 k 通常选择为 1.5。任何超出须线的值都被认为是异常值。

图 2.11 – 箱线和须图的部分
图 2.12展示了直方图和箱线图如何可视化对称和非对称分布的变异。注意非对称数据的箱线图在左侧被压缩,在右侧被扩展,而另一个箱线图则明显对称。虽然箱线图有助于可视化数据的对称性和异常值的存在,但分布的模态可能不会明显。

图 2.12 – 对称和非对称分布的箱线图和直方图比较
在探索时,通常使用多种可视化方式,因为每种类型的可视化都有其自身的优势和劣势。通常使用多种可视化方式,因为每种类型的可视化都有其自身的优势和劣势。
Tukey fences
在最后几节关于测量的内容中,异常值的概念出现了几次。异常值是与主要分布相比不典型或异常的值。虽然有一些方法可以将数据点分类为异常值,但没有一种通用的方法可以用来分类数据点为异常值。定义异常值通常应该根据数据分析的使用情况进行,因为根据应用领域会有不同的因素需要考虑。然而,值得提及的是,在箱线图示例中展示的常见技术,称为 Tukey fences。下限和上限 Tukey fences 基于四分位数间距(IQR)并定义如下:
-
下限围栏:Q1 − k(IQR)
-
上限围栏:Q3 + k(IQR)
如前所述,k 通常被选为默认值 1.5,但针对特定应用领域可能有一个更合适的值。
现在我们来看一下如何使用numpy和scipy计算 Tukey fences。这个代码示例将基于前面的示例,因为没有直接计算围栏的函数。我们再次使用numpy和scipy计算四分位数和四分位数间距(IQR)。然后,我们将这些操作应用于前面方程中列出的值:
import numpy as np
from scipy import stats
values = stats.norm.rvs(10, size=3000)
q1, q3 = np.quantile(values, [.25, .75],
method='closest_observation')
iqr = stats.iqr(values,interpolation='closest_observation')
lower_fence = q1 - iqr * 1.5
upper_fence = q3 + iqr * 1.5
# may vary due to randomness in data generation
print(f"The lower fence is {lower_fence:.2f} and the upper
fence is {upper_fence:.2f}")
# The lower fence is 7.36 and the upper fence is 12.67
在这个例子中,我们使用了numpy和scipy;然而,如前所述,scipy的计算可以用Q3-Q1来替换。
方差
在本节中将要介绍的最后一个变异度量是方差。方差是衡量离散程度的度量,可以理解为数值与平均值之间的分散程度。方差的公式,用 S 2 表示,如下所示:
S 2 = ∑ (x i − _ x ) 2 _ N − 1
在这个方程中,项(x i − _ x )被认为是与平均值的偏差,这导致另一个与方差密切相关的度量——标准差,它是方差的平方根。标准差的公式,用σ表示,如下给出:
σ = √ _ S 2 = √ ___________ ∑ (x i − _ x ) 2 _ N − 1
通常情况下,分布范围越宽,其方差和标准差也会越大,但这些值不如范围或四分位距(IQR)那样容易解释。这些概念将在下一节中更详细地介绍,在正态分布的背景下,这将提供对这些值所测量的更清晰的直观理解。
同样,这些值将通过代码使用numpy进行计算。方差和标准差的函数分别是var和std:
import numpy as np
values = [85, 99, 70, 71, 86, 88, 94]
variance = np.var(values)
standard_dev = np.std(values)
print(f"The variance is {variance:.2f} and the standard
deviation is {standard_dev:.2f}")
# The variance is 101.06 and the standard deviation is 10.05
测量形状
下一种类型的测量与分布的形状有关。具体如下:
-
偏度
-
偏度
让我们逐一讨论它们。
偏度
第一项测量是偏度。简单来说,偏度是测量不对称性[1]。图 2.13展示了偏斜分布的例子。
存在两种类型的偏斜分布:
-
左偏斜
-
右偏斜
分布在主导尾的方向上偏斜,意味着具有右侧主导尾的分布是右偏斜的,而具有左侧主导尾的分布是左偏斜的(如图 2.13所示)。

图 2.13 – 展示偏度的分布
由于现代软件包可以轻易计算,因此此处不会展示偏度的公式。偏度计算的结果可以用来确定偏度和偏斜的方向。如果偏度值为 0 或接近 0,则分布没有表现出强烈的偏斜。如果偏度值为正,则分布是右偏斜的;如果偏度值为负,则分布是左偏斜的。偏度值的绝对值越大,分布的偏斜程度越明显。以下代码示例展示了如何使用scipy计算偏度:
from scipy.stats import skewnorm, norm
from scipy.stats import skew as skew_calc
# generate data
skew_left = -skewnorm.rvs(10, size=3000) + 4
skew_right = skewnorm.rvs(10, size=3000) + 3
symmetric = norm.rvs(10, size=3000)
# calculate skewness
skew_left_value = skew_calc(skew_left)
skew_right_value = skew_calc(skew_right)
symmetric_value = skew_calc(symmetric)
# Output may vary some due to randomness of generated data
print(f"The skewness value of this left skewed
distribution is {skew_left_value:.3f}")
print(f"The skewness value of this right skewed
distribution is {skew_right_value:.3f}")
print(f"The skewness value of this symmetric distribution
is {symmetric_value:.3f}")
本节中涵盖的另一种形状测量是峰度。
偏度
峰度是衡量分布尾部相对于正态分布的轻重程度[2]。虽然正态分布尚未深入探讨,但峰度的概念仍然可以讨论。轻尾分布意味着更多的数据接近或围绕分布的众数。相反,重尾分布意味着比接近众数的数据,更多的数据位于分布的边缘。图 2.14展示了轻尾分布、正态分布和重尾分布。

图 2.14 – 以正态分布为参考的尾部分布
由于现代软件包可以轻易地计算出来,这里将不展示峰度的公式。如果峰度值为 0 或接近 0,则分布不显示峰度。如果峰度值为负,则分布显示轻尾性;如果峰度值为正,则分布显示重尾性。以下代码示例展示了如何使用scipy计算峰度:
from scipy.stats import norm
from scipy.stats import gennorm
from scipy.stats import kurtosis
# generate data
light_tailed = gennorm.rvs(5, size=3000)
symmetric = norm.rvs(10, size=3000)
heavy_tailed = gennorm.rvs(1, size=3000)
# calculate skewness
light_tailed_value = kurtosis(light_tailed)
heavy_tailed_value = kurtosis(heavy_tailed)
symmetric_value = kurtosis(symmetric)
# Output may vary some due to randomness of generated data
print(f"The kurtosis value of this light-tailed
distribution is {light_tailed_value:.3f}")
print(f"The kurtosis value of this heavy_tailed
distribution is {heavy_tailed_value:.3f}")
print(f"The kurtosis value of this normal
distribution is {symmetric_value:.3f}")
在本节中,我们介绍了用于测量和描述数据分布的常见描述性统计方法。这些测量提供了描述和比较分布的共同语言。本章讨论的概念是未来章节中许多概念的基础。在下一节中,我们将讨论正态分布,并使用这些测量来描述正态分布。
正态分布和中心极限定理
讨论正态分布时,我们指的是钟形的标准正态分布,它正式同义于高斯分布,以 18 世纪和 19 世纪的数学家和物理学家卡尔·弗里德里希·高斯的名字命名。高斯在许多方面做出了贡献,包括近似概念,并在 1795 年发明了最小二乘法和正态分布,这些在统计建模技术(如最小二乘回归)中常用[3]。标准正态分布也被称为参数分布,其特征是分布对称,数据点分散的概率在均值周围一致——也就是说,数据出现在均值附近比出现在远离均值的频率更高。由于这个分布中的位置数据遵循概率定律,我们可以称之为标准正态概率分布。顺便提一下,在统计学中,不是概率分布的分布是通过基于非随机选择的非概率抽样生成的,而概率分布是基于随机抽样生成的。基于概率和非概率的分布都可以有标准正态分布。标准正态分布既不偏斜也不显示峰度。它在整个分布中具有相同的方差,并且在自然界中经常出现。经验法则用于描述这个分布具有围绕均值μ的三个相关的标准差。关于这个分布有两个不同的假设:
-
第一、第二和第三标准差分别包含 68%、95%和 99.7%的测量值。
-
均值、中位数和众数都相等

图 2.15 – 标准正态分布
正态分布的两种常见形式如下:
-
概率密度分布
-
累积密度分布
如前所述,概率密度分布基于随机抽样,而累积密度分布基于累积数据,这些数据不一定随机。
标准正态分布的双尾概率密度函数如下:
f(x) = e −(x−μ) 2 _ 2 σ 2 _ σ √ _ 2π
标准正态分布的左尾累积函数如下:
f(x) = ∫ −∞ x e −x 2 _ 2 _ √ _ 2π
在统计建模方面,正态分布代表着平衡和对称。这在构建统计模型时非常重要,因为许多模型都假设正态分布,并且对许多偏离该假设的情况不稳健,因为它们是围绕平均值构建的。因此,如果此类模型中的变量不是正态分布的,模型的误差将会增加且不一致,从而降低模型稳定性。当考虑统计模型中的多个变量时,如果两者都是正态分布的,它们的相互作用更容易被近似。
在以下 图 2**.16 中,左边的图中,变量 X 和 Y 相互作用并在平均值周围形成集中分散。在这种情况下,使用 X 和均值线或线性距离对 Y 进行建模可以做得相当好。然而,如果两个变量的分布是偏斜的,如右边的图所示,这会导致两个变量之间的方差不是常数,从而导致误差分布不均,输出不可靠。

图 2.16 – 双变量正态分布(左)和偏斜分布(右)
在线性分类和回归模型的情况下,这意味着一些结果会比其他结果更好,而一些结果可能会非常糟糕。有时使用基本的模型指标很难评估这一点,需要更深入地分析模型以防止相信可能最终会误导的结果。此外,部署到生产环境将非常危险。关于这一点将在 第六章 中进一步讨论。
中心极限定理
在采样数据时,经常会遇到非正态数据的问题。这可能是由于多个原因造成的,例如总体没有正态分布或者样本不能代表总体。中心极限定理,在统计推断中非常重要,它假设如果从具有特定均值μ和标准差σ的总体中随机抽取 n 个观察值的样本,那么从随机选择的子样本分布的均值构建的抽样分布将接近一个具有大致相同的均值μ和标准差σ的正态分布,计算公式为√ _ ∑ (x i − μ) 2 _ N ,与总体相同。下一节将使用自助法来展示中心极限定理的实际应用。在后面的章节中,将讨论变换技术,提供重塑不符合正态分布的数据分布的方法,以便仍然可以有效地应用需要正态分布的工具。
自助法
自助法是一种重采样方法,它使用随机抽样——通常是放回抽样——通过从样本分布的子集中重采样来生成关于总体的统计估计,如下所示:
-
置信区间
-
标准误
-
相关系数(皮尔逊相关系数)
理念是,通过重复采样样本分布的不同随机子集并每次取平均值,给定足够的重复次数,将开始使用每个子样本的平均值来近似真实的总体。这直接遵循中心极限定理的概念,即重申,采样均值开始近似围绕原始分布均值的正态采样分布,随着样本量和计数增加。当分布中样本数量相对于特定测试所需的样本量有限时,自助法很有用,但需要进行推断。
如第一章中所述,采样与泛化,时间和费用等限制是获取样本而不是总体的常见原因。因为自助法的基本概念是使用样本对总体进行假设,所以当总体的真实统计参数(如百分位数和方差)已知时,将此技术应用于总体并不有利。至于样本制备,样本中属性的平衡应代表总体的真实近似。否则,结果可能会误导。例如,如果动物园中物种的总体是 40%的爬行动物和 60%的哺乳动物,而我们想通过自助法来估计它们的寿命并确定其置信区间,那么必须确保应用于自助法的数据集包含 40%的爬行动物和 60%的哺乳动物的分布;例如,15%的爬行动物和 85%的哺乳动物的分布会导致误导性结果。换句话说,样本分层应与总体的比例相平衡。
置信区间
如前所述,自助法的一个有用应用是围绕稀疏定义或有限的数据集创建置信区间——也就是说,具有广泛值范围但没有许多样本的数据集。考虑一个使用statsmodels中的"Duncan"数据集进行假设检验的例子,该数据集包含按职业、类型、教育和声望划分的收入。虽然这是一个完整的数据集,但考虑到采样方法未提及,并且不太可能考虑所有职业和类型的所有工人的所有收入,我们可以将此数据集视为样本。为了获取数据集,我们首先加载matplotlib、statsmodels、pandas和numpy库。然后我们下载数据集并将其存储在pandas DataFrame 的df_duncan变量中。在此之后,我们将“prof"、“wc"和“bc"类型分别重新编码为“professional"、“white-collar"和“blue collar"。最后,我们创建了两个独立的pandas DataFrame;一个用于专业工作类型,另一个用于蓝领工作类型,因为这是我们打算使用自助法分析的两组子集:
import matplotlib.pyplot as plt, statsmodels.api as sm, pandas as pd, numpy as np, scipy.stats
df_duncan = sm.datasets.get_rdataset("Duncan",
"carData").data
df_duncan.loc[df_duncan['type'] == 'prof',
'type'] = 'professional'
df_duncan.loc[df_duncan['type'] == 'wc',
'type'] = 'white-collar'
df_duncan.loc[df_duncan['type'] == 'bc',
'type'] = 'blue-collar'
df_professional = df_duncan.loc[(
df_duncan['type'] == 'professional')]
df_blue_collar = df_duncan.loc[(
df_duncan['type'] == 'blue-collar')]

图 2.17 – 显示 statsmodels Duncan 数据的前五行表格
然后,我们构建了一组绘图函数,如接下来所示。在 plot_distributions() 中,我们表示 p=5,这意味着在显著性水平为 0.05(1.00 - 0.05 = 0.95,因此,95% 置信度)时,p 值将是显著的。然后我们将此值除以 2,因为这将是一个双尾测试,意味着我们想要知道整个区间而不是仅仅一个边界(在 第一章 中作为代表性统计量的讨论)。至于绘图,我们使用 matplotlib 中的直方图(hist() 函数)可视化数据,然后使用我们用 numpy 函数 percentile() 构建的 axvline() 函数绘制 95% 的抽样置信区间。
自助抽样中的百分位数
当应用于原始数据时,百分位数只是那样,但当应用于自助抽样分布时,它就是置信区间。
要简单地说明置信区间,95% 置信区间意味着在取出的每个 100 个样本均值中,有 95 个将落在该区间内。在 numpy 的 percentile() 函数中,我们使用 p=5 来支持 1-p 是置信水平,其中 p 是显著性水平(想想 p-value,其中任何等于或低于 p 的值都是显著的)。由于测试是双尾的,我们将 p 除以 2,并在左尾和右尾各分割 2.5,因为我们有一个对称的标准正态分布。subplot(2,1,...) 代码创建了两行一列。图形的轴 0 用于专业收入,轴 1 用于蓝领收入:
def plot_distributions(n_replicas, professional_sample, blue_collar_sample, professional_label, blue_collar_label, p=5):
fig, ax = plt.subplots(2, 1, figsize=(10,8))
ax[0].hist(professional_sample, alpha=.3, bins=20)
ax[0].axvline(professional_sample.mean(),
color='black', linewidth=5)
# sampling distribution mean
ax[0].axvline(np.percentile(professional_sample, p/2.),
color='red', linewidth=3, alpha=0.99)
# 95% CI Lower limit (if bootstrapping)
ax[0].axvline(np.percentile(professional_sample,
100-p/2.), color='red', linewidth=3, alpha=0.99)
# 95% CI Upper Limit (if bootstrapping)
ax[0].title.set_text(str(professional_label) +
"\nn = {} Resamples".format(n_replicas))
ax[1].hist(blue_collar_sample, alpha=.3, bins=20)
ax[1].axvline(blue_collar_sample.mean(), color='black',
linewidth=5) # sampling distribution mean
ax[1].axvline(np.percentile(blue_collar_sample, p/2.),
color='red', linewidth=3, alpha=0.99)
# 95% CI Lower limit (if bootstrapping)
ax[1].axvline(np.percentile(blue_collar_sample,
100-p/2.), color='red', linewidth=3, alpha=0.99)
# 95% CI Upper Limit (if bootstrapping)
ax[1].title.set_text(str(blue_collar_label) +
"\nn = {} Resamples".format(n_replicas))
if n_replicas > 1:
print("Lower confidence interval limit: ",
np.percentile(round(professional_sample,4),
p/2.))
print("Upper confidence interval limit: ",
np.percentile(round(professional_sample,4),
100-p/2.))
print("Mean: ", round(professional_sample,
4).mean())
print("Standard Error: ",
round(professional_sample.std() /
np.sqrt(n_replicas), 4) )
print("Lower confidence interval limit: ",
np.percentile(round(blue_collar_sample,4),
p/2.))
print("Upper confidence interval limit: ",
np.percentile(round(blue_collar_sample,4),
100-p/2.))
print("Mean: ", round(blue_collar_sample,4).mean())
print("Standard Error: ",
round(blue_collar_sample.std() /
np.sqrt(n_replicas), 4) )
else:
print("At least two samples required to create the following statistics:\nConfidence Intervals\nMean\nStandard Error")
在原始数据集中,有 18 个 professional 职业类型的收入数据点和 21 个 blue-collar 职业类型的收入数据点。专业职业类型的 95% 置信区间从 29.50 到 79.15,平均值为 60.06。该区间对于蓝领职业类型从 7.00 到 64.00,平均值为 23.76。根据 图 2.18,收入差异之间存在合理的重叠,这导致了重叠的置信区间。因此,可以合理地假设蓝领和专业人士之间的收入没有统计学上的显著差异。然而,这个数据集的样本量非常有限:
n_replicas=0
plot_distributions(n_replicas=n_replicas,
professional_sample=df_professional['income'],
blue_collar_sample=df_blue_collar['income'],
professional_label="Professional",
blue_collar_label="Blue Collar")

图 2.18 – 带有 95% 百分位数线的原始数据分布
在以下代码中,使用 pandas 的 .sample() 函数,我们随机重新抽取每个分布中 50%(frac=0.5)的收入值 1,000 次,并每次计算一个新的均值,将其附加到以 _bootstrap_means 结尾的 Python 列表中。使用这些列表,我们推导出新的 95%置信区间。图 2**.19 显示,与数据集中的标准差和收入值相比,使用每个重新抽样的子集的平均值的新样本分布。replace=True 参数允许多次重新抽样相同的记录(在随机发生的情况下),这是 bootstrap 的要求。
在执行 bootstrap 过程之后,我们可以看到收入已经开始以大致标准正态、高斯形式分布。值得注意的是,从这个实验中,置信区间不再重叠。专业和蓝领群体之间置信区间分离的含义是,在 95%的置信水平下,可以证明两种工作类型的收入之间存在统计学上的显著差异。专业收入水平的置信区间现在是 48.66 到 69.89,平均值为 60.04,而蓝领的置信区间是 14.60 到 35.90,平均值为 23.69:
n_replicas = 1000
professional_bootstrap_means = pd.Series(
[df_professional.sample(frac=0.5, replace=True)
['income'].mean() for i in range(n_replicas)])
blue_collar_bootstrap_means = pd.Series(
[df_blue_collar.sample(frac=0.5, replace=True)
['income'].mean() for i in range(n_replicas)])

图 2.19 – 1,000 次 bootstrap 抽样的 1,000 个样本均值的 95%置信区间分布
在这里,你可以注意到分布更紧密地聚集在平均值周围,置信区间更紧密。
如前所述,bootstrap 可以用来获得分布的除置信区间之外的不同统计参数。
标准误差
另一个常用的指标是标准误差,σ √ n。我们可以使用最后几个变量,professional_bootstrap_means 和 blue\_collar\_bootstrap\_means 来计算这个值,因为这些变量包含了通过 bootstrap 过程获得的新均值分布。我们还可以看到,标准误差——通过将标准差除以样本数量的平方根(在我们的情况下,n\_replicas,代表从每个随机重新抽样的样本中获得的平均值数量)——随着重新抽样的体积增加而减小。我们使用以下代码来计算专业和蓝领类型收入 bootstrap 均值的标准误差。下表,图 2**.20 显示,随着 n 的增加,标准误差降低:
scipy.stats.sem(professional_bootstrap_means)
scipy.stats.sem(blue_collar_bootstrap_means)
| n | 专业 标准误差 | 蓝领 标准误差 |
|---|---|---|
| 10 个副本 | 0.93 | 2.09 |
| 10,000 个副本 | 0.03 | 0.04 |
图 2.20 – n = 10 和 n = 10,000 bootstrap 副本的标准误差表
Bootstrap 的另一个用例是皮尔逊相关系数,我们将在下一节讨论。
相关系数(皮尔逊相关)
通常,使用小样本量很难找到,因为相关性取决于两个变量的协方差。随着变量重叠得更加显著,它们的相关性就越高。然而,如果重叠是样本量小或抽样误差的结果,这种相关性可能是有代表性的。图 2.21显示了不同引导子样本计数的相关性表。随着分布形成更多的原生区分,相关性从小的正相关减少到接近零的量。
要测试来自原始数据集的 10 个记录样本的相关性,请参阅以下内容:
df_prof_corr = df_professional.sample(n=10)
df_blue_corr = df_blue_collar.sample(n=10)
corr, _ = scipy.stats.pearsonr(df_prof_corr['income'],
df_blue_corr['income'])
要测试引导平均值样本的相关性:
n_replicas = n_replicas
professional_bootstrap_means = pd.Series([df_prof_corr.sample(frac=0.5,replace=False).income.mean()for i in range(n_replicas)])
blue_collar_bootstrap_means = pd.Series([df_blue_corr.sample(frac=0.5, replace=False).income.mean() for i in range(n_replicas)])
corr, _ = scipy.stats.pearsonr(
professional_bootstrap_means,
blue_collar_bootstrap_means)
print(corr)
| n | Pearson 相关系数 |
|---|---|
| 原始数据中的 10 个样本 | 0.32 |
| 10 个副本 | 0.22 |
| 10,000 个副本 | -0.003 |
图 2.21 – Pearson 相关系数表与原始样本并排
通常,运行大约 1,000 到 10,000 个引导副本是很常见的。然而,这取决于正在引导的数据类型。例如,如果从人类基因组序列数据集中引导数据,引导样本 1 亿次可能是有用的,但如果引导一个简单的数据集,引导 1,000 次或更少可能是有用的。最终,研究人员应进行视觉检查平均值的分布,以确定结果与预期相比是否合理。与统计学一样,最好有一些领域知识或专业知识来帮助验证发现,因为这可能是决定引导复制次数的最佳方法。
引导程序(Bootstrapping)也应用于机器学习,它是自助聚合(bootstrap aggregation)概念的基础,也称为Bagging,这是一个结合基于自助子样本分布构建的预测模型输出的过程。随机森林(Random Forest)是执行此操作的一种流行算法。在 Bagging 算法中,引导程序的目的在于保持非参数(将在后续章节中进一步讨论)分类的低偏差行为,同时减少方差,因此将引导程序作为一种最小化建模误差中偏差-方差权衡重要性的方法。
在下一节中,我们将考虑另一种非参数测试,即使用重采样数据的排列测试。
排列
在跳入这个测试分析之前,我们将回顾一些排列组合的基本知识。
排列和组合
排列和组合是两种数学技术,用于从一组对象中创建子集,但以两种不同的方式。在排列中,对象的顺序很重要,而在组合中则不重要。
为了轻松理解这些概念,我们将考虑两个例子。晚会上有 10 个人。派对的组织者想要随机给 3 个人颁发价值 1000 美元、500 美元和 200 美元的奖品。问题是有多少种分配奖品的方式?另一个例子是,组织者将从派对的 10 个人中选出 3 个人,每人颁发价值 500 美元的等额奖品。组织者并不关心这 3 个被选中的人中谁得到哪个奖品。在两个例子中,胡伊、保罗和斯图尔特是我们的获奖者,但在第一个例子中,可能会有不同的结果,例如,如果保罗赢得 200 美元的奖品、500 美元的奖品或 1000 美元的奖品。
| $****1,000 | $****500 | $****200 |
|---|---|---|
| 胡伊 | 保罗 | 斯图尔特 |
| 保罗 | 胡伊 | 斯图尔特 |
| 保罗 | 斯图尔特 | 胡伊 |
| 胡伊 | 斯图尔特 | 保罗 |
| 斯图尔特 | 胡伊 | 保罗 |
| 斯图尔特 | 保罗 | 胡伊 |
图 2.22 – 分配给胡伊、保罗和斯图尔特的奖品分布表
然而,在第二个例子中,由于 3 个奖品的价值相同,为 500 美元,奖品排列的顺序并不重要。
让我们更仔细地看看这两个排列组合的例子。第一个例子是一个排列例子。由于有 10 个人,我们有 10 种可能性从这 10 个人中选择一个人来颁发 1000 美元的奖品。如果这个人被选中赢得 1000 美元的奖品,那么选择另一个人颁发 500 美元的奖品就只剩下 9 种可能性,最后,我们有 8 种可能性从这 10 个人中选择一个人来颁发 200 美元的奖品。然后,我们有 1098 = 720 种分配奖品的方式。排列的数学公式如下:
P(n, r) = n! / (n-r)!
在这里,P(n, r)是排列的数量,n 是集合中的总对象数,r 是从集合中选择的对象数。在这个例子中,n = 10 且 r = 3,所以我们看到:
P(10,3) = 10! / (10-3)! = 109 * 87 * 65 * 43 * 21 / 76 * 54 * 32 * 1 = 10*9 * 8 = 720
从晚会上 10 个人中选出 3 个人来分配价值 1000 美元、500 美元和 200 美元的 3 个奖品,共有 720 种选择方式。
在 Python 中,有一个名为itertools的包可以帮助我们直接找到排列。读者可以查看以下链接 – docs.python.org/3/library/itertools.xhtml – 获取有关此包的更多信息。我们需要将此包导入 Python 环境以进行排列:
from itertools import permutations
# list of 10 people in the party
people = ['P1','P2','P3','P4','P5','P6','P7','P8','P9','P10']
# all the ways that the 3 prizes are distributed
perm = permutations(people, 3)
list_perm = list(perm)
print(f"There are {len(list_perm)} ways to distribute the prizes!")
在前面的 Python 代码中,我们创建了一个包含 10 个人的列表 people,分别是 P1 到 P10,然后使用 itertools 中的 permutations 函数来获取所有分配奖项的方式。这种方法接受一个包含 10 个人的列表作为输入,并返回一个包含所有可能性的元组列表,即从这 10 个人中选择 3 个人来分配 1,000 美元、500 美元和 200 美元的奖项。因为共有 720 种分配奖项的方式,这里我们只打印 Python 代码生成的 10 种第一种方式:
print(f"The 10 first ways to distribute the prizes: \n
{list_perm[:10]} ")
前面代码的输出是分配奖项的 10 种第一种方式:
[('P1', 'P2', 'P3'), ('P1', 'P2', 'P4'), ('P1', 'P2', 'P5'), ('P1', 'P2', 'P6'), ('P1', '``P2', 'P7')]
如果我们有 10 件不同的礼物,每个参加派对的人都可以带一件礼物回家。有多少种分配这些礼物的方式?共有 3,628,800 种方式。这是一个非常大的数字!读者可以用以下代码进行验证:
#list of 10 people in the party
people = ['P1','P2','P3','P4','P5','P6','P7','P8','P9','P10']
# all the ways that the 10 different gifts are distributed
perm = permutations(people)
list_perm = list(perm)
print(f"There are {len(list_perm)}
ways to distributed the gifts!")
回到第二个例子,因为 3 个奖项的价值都是 500 美元,所以 3 个被选中的人的顺序并不重要。那么,如果选中的 3 个人是 Huy、Paul 和 Stuart,如 图 2**.22 所示,在第一个例子中分配奖项的方式有 6 种。然后,将相同数量的 500 美元分配给 Huy、Paul 和 Stuart 的方式只有 1 种。组合的数学公式如下:
C(n, r) = n! / (r! * (n - r)!)
在这里,C(n, r) 表示组合数,n 是集合中对象的总数,r 是可以从集合中选择的对象数量。同样,我们可以计算出有
10! / (3! * (10 - 3)!) = 10 * 9 * 8 / (1 * 2 * 3) = 720 / 6 = 120
分配 3 个价值 500 美元的奖项的方式。
在 Python 中,我们同样使用 itertools 包,但不是使用 permutations 函数,而是导入 combinations 函数:
from itertools import combinations
# list of 10 people in the party
people = ['P1','P2','P3','P4','P5','P6','P7','P8','P9','P10']
# all the ways that the 3 prizes are distributed
comb = combinations(people, 3)
list_comb = list(comb)
print(f"There are {len(list_comb)} ways to distribute the prizes!")
排列测试
排列测试是一种非参数测试,它不要求数据呈正态分布。自助法和排列法都适用于重采样技术,但适用于不同的用途,一个用于估计统计参数(自助法),另一个用于假设检验。排列测试用于测试来自同一总体生成的两个样本之间的零假设。它有不同的名称,如 精确测试、随机化测试和重新随机化测试。
在实现 Python 代码之前,我们先来看一个简单的例子,以便更好地理解。我们假设有两组人,一组代表儿童(A),另一组代表 40 岁以上的人(B),如下所示:
A = [3,5,4] 和 B = [43,41,56,78,54]
样本 A 和 B 之间年龄的平均差异是
43 + 41 + 56 + 78 + 54 __________________ 5 - 3 + 5 + 4 * 3 = 50.4
我们将 A 和 B 合并成一个单一集合,记为 P,如下所示:
P = [3,5, 4,43,41,56,78,54].
然后,我们取 P 的一个排列,例如以下内容:
P_new = [3,54, 78, 41, 4, 43, 5, 56]
然后,我们将P_new重新划分为两个子集,分别称为A_new和B_new,它们的大小分别与 A 和 B 相同:
A_new = [3,54,78] 和 B_new = [41,4,43,5,56]
然后,A_new和B_new之间的年龄均值差异为 15.2,这低于 A 和 B 之间的原始年龄均值差异(50.4)。换句话说,排列后的P_new对 p 值没有贡献。我们可以观察到,从 P 的所有可能排列中抽取的只有一个排列的均值差异大于或等于原始的均值差异 P。现在我们将代码实现于 Python 中:
import numpy as np
# create permutation testing function
def permutation_testing(A,B,n_iter=1000):
#A, B are 2 lists of samples to test the hypothesis,
#n_iter is number of iterations with the default is 1000
differences = []
P = np.array(A+B)
original_mean = np.array(A).mean()- np.array(B).mean()
for i in range(n_iter):
np.random.shuffle(P)#create a random permutation of P
A_new = P[:len(A)] # having the same size of A
B_new = P[-len(B):] # having the same size of B
differences.append(A_new.mean()-B_new.mean())
#Calculate p_value
p_value = round(1-(float(len(np.where(
differences<=original_mean)[0]))/float(n_iter)),2)
return p_value
在前面的 Python 代码中,A 和 B 是两个样本,我们想知道它们是否来自同一个更大的总体;n_ter是我们想要执行的迭代次数;这里,1,000 是默认的迭代次数。
让我们以 10,000 次迭代对示例中的两组人员进行置换检验:
A = [3,5,4]
B = [43,41,56,78,54]
permutation_testing(A,B,n_iter=10000)
得到的 p 值为 0.98。这意味着我们未能拒绝零假设,或者说没有足够的证据来确认样本 A 和 B 来自同一个更大的总体。
接下来,我们将探讨许多需要正态分布假设的统计测试中的一个重要且必要的步骤。
转换
在本节中,我们将考虑三种转换:
-
对数转换
-
平方根转换
-
立方根转换
首先,我们将导入numpy包以创建一个从 Beta 分布中抽取的随机样本。Beta 分布的文档可以在这里找到:
numpy.org/doc/stable/reference/random/generated/numpy.random.beta.xhtml
样本df有 10,000 个值。我们还使用matplotlib.pyplot创建不同的直方图。其次,我们通过使用对数转换、平方根转换和立方根转换来转换原始数据,并绘制四个直方图:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42) # for reproducible purpose
# create a random data
df = np.random.beta(a=1, b=10, size = 10000)
df_log = np.log(df) #log transformation
df_sqrt = np.sqrt(df) # Square Root transformation
df_cbrt = np.cbrt(df) # Cube Root transformation
plt.figure(figsize = (10,10))
plt.subplot(2,2,1)
plt.hist(df)
plt.title("Original Data")
plt.subplot(2,2,2)
plt.hist(df_log)
plt.title("Log Transformation")
plt.subplot(2,2,3)
plt.hist(df_sqrt)
plt.title("Square Root Transformation")
plt.subplot(2,2,4)
plt.hist(df_cbrt)
plt.title("Cube Root Transformation")
plt.show()
以下为代码的输出:

图 2.23 – 原始数据和转换数据的直方图
通过转换,我们可以看到转换后的直方图比原始直方图更接近正态分布。似乎在这个例子中,最佳转换是立方根转换。在现实世界的数据中,确定是否需要转换以及应该使用哪种转换非常重要。
其他数据转换方法,例如查找重复数据、处理缺失值和特征缩放,将在以下章节的 Python 实际应用案例中讨论。
摘要
在本章的第一节中,我们学习了数据的类型以及如何可视化这些类型的数据。然后,我们介绍了如何描述和测量数据分布的属性。我们学习了标准正态分布,为什么它很重要,以及通过展示自助法来展示中心极限定理在实践中的应用。我们还学习了如何利用自助法利用非正态分布的数据,通过置信区间来测试假设。接下来,我们介绍了排列组合等数学知识,并介绍了排列测试作为自助法之外的另一种非参数检验。我们以在执行需要正态分布数据的统计测试的许多情况下有用的不同数据转换方法结束本章。
在下一章中,我们将详细探讨假设检验,并讨论如何从测试结果中得出统计结论。我们还将探讨统计测试中可能出现的错误以及如何选择统计功效。
参考文献
-
[1] 偏度 –
www.itl.nist.gov/div898/handbook/eda/section3/eda35b.htm -
[2] 峰度 – [
www.itl.nist.gov/div898/handbook/eda/section3/eda35b.htm#:~:text=峰度%20 是%20 一个%20 衡量,%20 会是%20 极端情况](https://www.itl.nist.gov/div898/handbook/eda/section3/eda35b.htm#:~:text=峰度 是%20 一个%20 衡量,%20 会是%20 极端情况). -
[3] 正态分布 – C.F. 高斯和最小二乘法,ŚLĄSKI PRZEGLĄD STATYSTYCZNY 西里西亚统计评论,第 12(18)期,O. Sheynin,1999 年 9 月
第三章:假设检验
在本章中,我们将开始讨论从数据中得出统计结论,结合来自第一章**,抽样和推广和来自第二章**,数据分布的分布。我们主要使用统计模型来回答数据中的问题。假设检验提供了一个正式的框架,通过不确定性的度量来回答感兴趣的问题。首先,我们将介绍假设检验的目标和结构。然后,我们将讨论假设检验可能出现的错误并定义预期的错误率。接着,我们将通过 z 检验的过程来讲解假设检验。最后,我们将讨论统计功效分析。
在本章中,我们将涵盖以下主要主题:
-
假设检验的目标
-
第一类和第二类错误
-
z 检验的基础 – z 分数、z 统计量、临界值和 p 值
-
均值和比例的单样本和双样本 z 检验
-
选择错误率和功效分析
-
将功效分析应用于 z 检验
假设检验的目标
简单来说,假设检验的目标是判断我们所拥有的数据是否足以支持一个特定的假设。假设检验提供了一个正式的框架,基于我们的数据来检验假设,而不是试图通过视觉检查来做出决定。在本节中,我们将讨论假设检验的过程。在下一节中,z 检验的基础 – z 分数、z 统计量、临界值和 p 值,我们将通过详细分析一个例子来应用这个过程。
均值假设检验概述
为了理解假设检验过程,让我们从一个简单的例子开始。假设我们有一个工厂,工厂里有机器生产小部件,我们期望我们的机器以一定的速率(每小时 30 个小部件)生产小部件。我们首先构建两个假设,即零假设和备择假设。零假设和备择假设分别用以下符号表示:H 0 和 H a。为了创建零假设,我们将首先假设我们想要测试的是真实的。在我们的例子中,零假设将是机器的平均输出为每小时 30 个小部件。一旦我们确定了零假设,然后我们创建备择假设,它只是零假设的矛盾。在我们的例子中,备择假设是机器的平均输出不是每小时 30 个小部件。请注意,我们的假设没有指示任何方向性,也就是说,备择假设包含低于和高于预期值的值。这被称为双尾测试,意味着有两个备择假设。我们还有单尾测试。例如,如果我们说零假设是机器的平均输出超过每小时 30 个小部件,那么备择假设将是机器的平均输出低于每小时 30 个小部件。这一组假设将是一个单尾测试。
我们例子中的双尾零假设和备择假设可以用以下数学方式表述:
H 0 : x = 30
H a : x ≠ 30
一旦我们有了零假设和备择假设以及数据,我们将使用软件进行测试。现在我们先暂时搁置测试的实现细节(这些将在下一节中详细讨论)。统计测试有两种可能的结果:拒绝零假设或未能拒绝零假设。如果我们的机器的平均输出在零假设中声明的值上具有统计学上的差异,那么我们将拒绝零假设。这意味着,根据数据,零假设中声明的值不是平均值的合理值。然而,如果我们的机器的平均输出在零假设中列出的值上没有统计学上的差异,我们将未能拒绝零假设。这意味着,根据数据,零假设中声明的值是平均值的合理值。在运行零假设测试并得出结论后,我们将提供一个置信区间(下一节将讨论),并确定推理的范围。
推理范围
推理范围由讨论在第一章中的抽样设计决定。有两个问题需要考虑 – 总体是什么,以及样本是如何从总体中选择的? 在这个例子中,让我们假设我们正在测试一个大工厂(可能是数百台机器)的机器的平均输出。那么,总体就是工厂中的机器。如果我们随机抽取机器样本,那么我们的结论可以推广到整个总体。
尽管我们的当前例子是现实的,但它相当简单。在其他情况下,可能会有额外的考虑。例如,工厂中的机器可能有不同的型号和不同的年龄,这可能会影响输出。在这种情况下,我们可以使用分层随机抽样并对每个层进行推断。
假设检验步骤
本节概述了从提出假设到得出结论的假设检验过程。随着我们继续本章的学习,请记住以下关键假设检验步骤:
-
陈述零假设和备择假设
-
执行统计测试
-
确定结论:拒绝或未能拒绝零假设
-
提供统计结论、置信区间和推理范围
这些步骤适用于任何统计测试,我们将继续遵循这一系列步骤进行假设检验。在下一节中,我们将讨论假设检验可能导致的错误类型。
第一类错误和第二类错误
虽然数据可以给我们一个关于分布特性的良好概念,但假设检验可能导致错误。错误可能发生,因为我们是从总体中抽取随机样本。虽然随机化使得样本包含抽样偏差的可能性降低,但并不能保证随机样本能够代表总体。假设检验可能导致两种可能的错误:
-
第一类错误:当零假设实际上为真时拒绝零假设
-
第二类错误:当零假设实际上为假时未能拒绝零假设
第一类错误
当假设检验导致拒绝零假设,但实际上零假设是正确的时,会发生第一类错误。例如,假设我们有一个数据分布,总体均值为 30。我们陈述的零假设为 H 0 : _ x = 30。我们为测试随机抽取样本,但样本中的随机值恰好位于分布的高侧。因此,测试结果建议我们应该拒绝零假设。在这种情况下,我们犯了一个第一类错误。这种类型的错误也称为假阳性。
当我们进行统计测试时,由于从目标总体中抽取的样本数据,我们总是有可能得出错误的结论。犯第一类错误的概率由 α指定。换句话说,α代表我们期望犯错误(预期错误率)的频率。这是一个我们可以为我们的测试选择的自由参数(α也称为显著性水平)。通常使用 0.05 作为α,但没有使用 0.05 的证据基础;在其他情况下,不同的值可能更合适。在本章的后面部分,我们将讨论选择第一类错误率。
第二类错误
我们可能犯的另一种错误称为第二类错误。在这种情况下,我们未能拒绝实际上为假的零假设。让我们考虑另一个例子。假设我们有一组数据分布,我们想要测试该分布的均值是否为 30。我们随机抽取样本进行测试,测试结果表明我们不应该拒绝零假设。然而,真正的总体均值是 35。在这种情况下,我们犯了一个第二类错误。这种错误也称为假阴性。
如前所述,统计测试可能导致错误的结论总是有可能的。因此,我们希望控制犯错误的概率。然而,与α不同,第二类错误率β不是一个我们可以简单选择的自由参数。为了理解犯第二类错误的可能性的大小,我们通常将进行功效分析,这将显示各种因素,如样本大小,将如何影响第二类错误率。在下一节中,我们将讨论选择错误率和功效分析。
我们可以用图 3.1中的表格来总结假设检验的可能结果。
| 零假设是: | ||
|---|---|---|
| 真实 | ||
| 对零假设的决策 | 不拒绝 | 正确推断(1- α) |
| 拒绝 | 第一类错误(α) | 正确推断(1-β) |
图 3.1 – 假设检验结果
在本节中,我们讨论了从统计测试中得出结论时可能出现的错误类型。在下一节中,我们将通过 z 检验的假设检验示例进行讲解,在本章的后面部分,我们将讨论如何选择错误率以及如何分析统计功效和相关因素。
z 检验的基础 – z 分数,z 统计量,临界值和 p 值
在本节中,我们将讨论一种称为 z 检验的假设检验类型。这是一种使用假设为正态分布的样本数据来确定与总体参数值相关的统计陈述是否应该被拒绝的统计程序。该测试可以在以下方面进行:
-
单样本(左尾 z 检验,右尾 z 检验或双尾 z 检验)
-
双样本(双样本 z 检验)
-
比例(单比例 z 检验或双比例 z 检验)
该测试假设标准差是已知的,并且样本量足够大。在实践中,样本量应大于 30。
在讨论不同类型的 z 检验之前,我们将讨论 z 分数和 z 统计量。
z 分数和 z 统计量
要衡量一个特定值与均值之间的距离,我们可以使用 z 分数或 z 统计量作为统计技术,结合均值和标准差来确定相对位置。
z 分数的计算公式如下:
z i = x i − _ x _ σ
在这里,z i 是 x i 的 z 分数,_x 是样本均值,σ 是样本标准差。z 分数也称为 z 值、标准化值或标准分数。让我们考虑几个例子。标准差告诉我们样本与分布均值的距离。如果 z i = 1.8,那么这个点距离均值 1.8 个标准差。同样,如果 z i = − 1.5,那么这个点距离均值 1.5 个标准差。符号确定它是大于还是小于样本均值。z i 为 -1.5 表示小于均值,而 z i 为 1.8 表示大于均值。现在让我们通过一个例子来解释。在德克萨斯州达拉斯的一所高中(在美国),我们要求学生参加匿名智商测试,进行一些统计研究。从这所学校收集的数据呈正态分布,智商分数的总体均值 μ = 98,总体标准差 σ = 12。一名学生参加了智商测试,他的分数是 110。他的智商分数高于均值,但他想知道自己是否位于前 5%。首先,我们将使用 z 分数公式来计算它:
z student = 110 − 98 _ 12 = 12 _ 12 = 1.
学生可以检查 z 表(图 3**.2),例如,从网站www.z-table.com获取 0.8413 的值。他位于前 1-0.8413 = 0.1587 或 15.87%的学校智商分数中。

图 3.2 – z 表
在 Python 中,我们可以使用累积分布函数(CDF)来计算它:
import scipy
round(scipy.stats.norm.cdf(1),4)
# 0.8413
我们在 Python 中得到的值与 z 表检查中的值相同;在这个例子中,z 分数=1 时的值为 0.8413。
另一个例子是一个从智商调查中随机抽取的 10 个分数样本:
90, 78, 110, 110, 99, 115, 130, 100, 95, 93
要计算样本中每个智商分数的 z 分数,我们需要计算这个样本的均值和标准差,然后应用 z 分数公式。幸运的是,我们再次可以使用scipy库,如下所示:
import pandas as pd
import numpy as np
import scipy.stats as stats
IQ = np.array([90, 78,110, 110, 99, 115,130, 100, 95, 93])
z_score = stats.zscore(IQ)
# Create dataframe
data_zscore = {
"IQ score": IQ,
"z-score": z_score
}
IQ_zscore = pd.DataFrame(data_zscore)
IQ_zscore
我们创建了一个名为IQ的智商分数数组,并使用scipy.stats中的z-score来计算z_score。最后,我们创建了以下输出 DataFrame。

图 3.3 – 输出 DataFrame
在讨论 z 统计量之前,我们将介绍抽样分布的概念。回顾上一个例子,作为一个经验法则,我们反复从高中智商分数池中选取 35 个智商分数的简单随机样本,所需次数取决于研究。然后,我们计算每个样本的平均分数,称为 x。因为我们有各种选出的样本,所以我们也有 x 的各种可能值。x 的期望值如下:
E(x) = μ
在这里,μ 表示总体均值。σ_x 表示 x 的标准分布。实际上,在许多抽样情况下,总体相对于较小的样本量来说相对较大。那么,x 的标准差可以表示如下:
σ_x = σ / √n
这里,σ 是总体标准差,n 是有限或无限总体中的样本量,总体较大,样本量相对较小。注意,σ_x 也被称为均值标准误,帮助我们确定样本均值与总体均值之间的距离。注意,E(x) = μ,与样本量无关。样本量和标准误成反比:当样本量增加时,标准误减小。由于假设 x 的抽样分布是正态分布的,样本分布由以下公式给出:
z_x = x̄ − μ / σ / x̄
在关于总体均值的假设检验中,我们使用测试统计量,其公式如下:
z = z_x = x − μ / σ / √n
在这里,x 是样本均值,μ 是总体均值,σ 是总体标准差,n 是样本量。
再次考虑智商测试的例子。智商分数数据的均值为 μ = 98,标准差 σ = 12。假设数据服从正态分布。设 x 为从智商数据中随机抽取的分数。x 在 95 和 104 之间的概率是多少?我们将计算当 x = 95 和 x = 104 时的 z 分数如下:
z_95 = 95 − 98 / 12 = −0.25,
z_100 = 104 − 98 / 12 = 0.5.
因此,所抽取的分数在 95 和 104 之间的概率是:
P(95 < x < 104) = P(−0.25 < z < 0.5) = 0.6915 − 0.4013 = 0.2902.
然后,这个概率大约是 29.02%,即从数据中随机抽取的智商分数在 95 和 104 之间的概率。我们还可以从 z 表中获取以下值。

图 3.4 – z 表

图 3.5 – z 表
在 Python 中,我们可以按照以下方式实现代码:
#calculate z scores at x=95 and 104
zscore_95 = round((95-98)/12,2)
zscore_104 = round((104-98)/12,2)
#calculate cdf and probability
cdf_95 = stats.norm.cdf(zscore_95)
cdf_104 = stats.norm.cdf(zscore_104)
prob = abs(cdf_95-cdf_104)
#print the probability
print(f"The probability that the taken score between 95 and 104 is {round(prob*100,2)}%!")
提出的另一个问题是,随机抽取四个分数的平均分 x 在 95 和 104 之间的概率是多少?为了解决这个问题,我们使用 z 统计量的概念。我们假设 x 的均值也是 98,且 x 服从正态分布。那么,x 的标准误为:
σ_x = σ / √n = 12 / √4 = 6,
然后:
z = _ x − μ _ x _ σ _ x = _ x − 98 _ 6 .
很容易计算出 z 95 = − 0.5 和 z 104 = 1。通过使用 z 表,我们可以得到随机抽取六个分数的平均分数 x 在 95 和 104 之间的概率如下:
0.8413 – 0.3085 = 0.5328
或者,大约 53.28%。为了使用这个想法,我们可以在 Python 中实现以下代码:
import pandas as pd
import numpy as np
import scipy.stats as stats
import math
# standard error
n= 4
sigma = 12
se = sigma/math.sqrt(n)
#calculate z scores at x=95 and 104
zscore_95 = round((95-98)/se,2)
zscore_104 = round((104-98)/se,2)
#calculate cdf and probability
cdf_95 = stats.norm.cdf(zscore_95)
cdf_104 = stats.norm.cdf(zscore_104)
prob = abs(cdf_95-cdf_104)
#print the probability
print(f"The probability that the taken score between 95 and 104 is {round(prob*100,2)}%!")
注意,在上述代码中,我们还需要 math 库来计算平方根函数,math.sqrt()。
在下一节中,我们将讨论均值 z 测试。
均值 z 测试
在这部分,我们将考虑与总体均值或两个总体的均值相关的单样本和双样本 z 测试。
单样本 z 测试
从总体中选择的研究样本应该是正态分布的。总体标准差在实用目的上应该是已知的。
当总体不能假设为正态分布,但根据研究参与者的经验,样本大小需要足够大时,此测试仍然适用。为了进行假设检验,我们需要制定零假设和备择假设。以下图示了与左尾、右尾和双尾 z 测试相对应的三个零假设和备择假设。

图 3.6 – 左尾、右尾和双尾假设检验
H 0 : μ ≥ μ 0 H 0 : μ ≤ μ 0 H 0 : μ = μ 0
H a : μ < μ 0 H a : μ > μ 0 H a : μ ≠ μ 0
接下来,我们需要指定显著性水平 α,即在零假设为真时拒绝零假设的概率。换句话说,这是我们在上一节中讨论的第一类错误的概率。然后,我们计算测试统计量的值。有两种方法用于假设检验:使用 p 值或临界值。
在 p 值方法中,我们使用测试统计量的值来计算一个概率,称为 p 值,它可能具有与从样本中得到的测试统计量一样极端或更极端的值。p 值越小,就越表明有证据反对零假设,换句话说,这是用来确定是否应该拒绝 H 0 的概率。拒绝规则(拒绝 H 0)是 p 值小于或等于研究中的指定显著性水平 α。为了根据测试统计量的值在 Python 中找到 p 值,我们使用以下语法:
scipy.stats.norm.sf(abs(x))
在这里,x 是 z 分数。例如,我们想要找到一个左尾测试中与 z 分数 -2.67 相关的 p 值。Python 实现如下:
import scipy.stats
#find p-value
round(scipy.stats.norm.sf(abs(-2.67)),4)
输出将是 0.0038。在右尾测试的情况下,使用类似的 Python 代码。对于双尾测试,我们需要将值乘以 2:
#find p-value for two-tailed test
scipy.stats.norm.sf(abs(2.67))*2
下图说明了如何计算每种类型检验中的 p 值。

图 3.7 – 假设检验中的 p 值
最后一步是解释统计结论。
另一方面,在临界值方法中,我们需要使用显著性水平来计算检验统计量的临界值。临界值是临界区域的边界,我们可以据此拒绝零假设。

图 3.8 – 假设检验中的临界区域
在 Python 中计算临界值时,我们使用以下语法:
scipy.stats.norm.ppf(alpha)
这里,alpha 是要使用的显著性水平。以下是在 Python 中实现左尾、右尾和双尾检验的代码:
import scipy.stats
alpha = 0.05 # level of significance
#find Z critical value for left-tailed test
print(f" The critical value is {scipy.stats.norm.ppf(alpha)}")
#find Z critical value for left-tailed test
print(f" The critical value is {scipy.stats.norm.ppf(1-alpha)}")
##find Z critical value for two-tailed test
print(f" The critical values are {-scipy.stats.norm.ppf(1-alpha/2)} and {scipy.stats.norm.ppf(1-alpha/2)}")
这是前面代码的输出:
临界值 是 -1.6448536269514729
临界值 是 1.6448536269514722
临界值是 -1.959963984540054 和 1.959963984540054
对于左尾检验,显著性水平 α = 0.05,临界值约为 -1.64485。由于这是一个左尾检验,如果检验统计量小于或等于这个临界值,我们拒绝零假设。同样,对于右尾检验,如果检验统计量大于或等于 1.64485,我们拒绝零假设。对于双尾检验,如果检验统计量大于或等于 1.95996 或小于或等于 -1.95996,我们拒绝零假设。
确定是否拒绝零假设后,我们解释统计结论。
让我们再次讨论达拉斯高中的智商测试分数。智商分数数据平均值为 μ = 98,标准差为 σ = 12。一位研究人员想知道智商分数是否会受到某些智商训练的影响。他招募了 30 名学生,每天训练他们 2 小时回答智商问题,共训练 30 天,并在训练结束后记录他们的智商水平。训练阶段后 30 名学生的智商分数为 95, 110, 105, 120, 125, 110, 98, 90, 99, 100, 110, 112, 106, 92, 108, 97, 95, 99, 100, 100, 103, 125, 122, 110, 112, 102, 92, 97, 89, 和 102。他们的平均分数是 104.17。我们可以轻松地在 Python 中实现以下计算:
IQscores = [95,110, 105, 120, 125, 110, 98, 90, 99, 100,
110, 112, 106, 92, 108, 97, 95, 99, 100, 100,
103, 125, 122, 110, 112, 102, 92, 97, 89, 102]
IQmean = np.array(IQscores).mean()
我们定义零假设和备择假设:
H 0 : μ after = μ = 98
H a : μ after > μ = 98
我们选择显著性水平 α=0.05。之前,右尾检验的临界值为 1.64485。我们现在计算问题中的检验统计量:
z = x − μ σ/ √n = 104.17 − 98 12/ √30 = 2.8162
它在 Python 中的实现如下:
n=30 #number of students
sigma =12 #population standard deviation
IQmean = 104.17 # IQ mean of 30 students after the training
mu = 98 # population mean
z = (IQmean-mu)/(sigma/math.sqrt(n))
由于检验统计量值为 2.8162 > 1.64485,我们拒绝零假设。这意味着训练确实影响了这些学生的智商水平,并帮助他们提高了智商分数。
我们还可以使用来自statsmodels包的ztest()函数(Seabold, Skipper, 和 Josef Perktold, “statsmodels: Econometric and statistical modeling with python.” Proceedings of the 9th Python in Science Conference. 2010)来执行单样本或双样本 z 检验(我们将在下一部分讨论)。语法如下:
statsmodels.stats.weightstats.ztest(x1, x2=None,
value=0, alternative='two-sided')
这里,我们看到以下内容:
-
x1: 两个独立样本中的第一个 -
x2: 两个独立样本中的第二个(如果执行双样本 z 检验) -
value: 在单样本情况下,value是零假设下x1的均值。在双样本情况下,value是零假设下x1的均值与x2的均值之差。检验统计量是x1_mean - x2_mean - value。 -
alternative: 替代假设:
'two-sided': 双侧检验
'larger': 右侧检验
'smaller': 左侧检验
以下 Python 代码展示了我们如何执行单样本 z 检验:
from statsmodels.stats.weightstats import ztest as ztest
#IQ scores after training sections
IQscores = [95,110, 105, 120, 125, 110, 98, 90, 99, 100,
110, 112, 106, 92, 108, 97, 95, 99, 100, 100,
103, 125, 122, 110, 112, 102, 92, 97, 89, 102]
#perform one sample z-test
z_statistic, p_value = ztest(IQscores, value=98, alternative = 'larger')
print(f"The test statistic is {z_statistic} and the
corresponding p-value is {p_value}.")
检验统计量为 3.3975,p 值=0.00034 < 0.05,其中 0.05 是显著性水平。因此,我们有足够的证据拒绝零假设。这意味着训练确实影响了这些学生的智商水平。
双样本 z 检验
我们考虑两个正态分布且独立的总体。让Ω表示两个总体均值μ1 和μ2 之间的假设差异。同样,正如单样本 z 检验的情况一样,我们有三种零假设和替代假设的形式:
H0: μ1 − μ2 ≥ Ω H0: μ1 − μ2 ≤ Ω H0: μ1 − μ2 = Ω
H0: μ1 − μ2 < Ω H0: μ1 − μ2 > Ω H0: μ1 − μ2 ≠ Ω
在许多问题中,Ω = 0。这意味着对于双尾检验的情况,零是零假设,换句话说,μ1 和μ2 是相等的。假设检验的检验统计量计算如下:
z = (̅x1 − ̅x2) − Ω / √(σ1²/n1 + σ2²/n2)
在这里,_x1 和 _x2 是样本均值,n1 和 n2 是从两个具有均值μ1 和μ2 的总体中随机抽取的样本大小。σ1 和σ2 是这两个总体的标准差。对于两个独立的简单随机样本,点估计量 _x1 − _x2 的标准误差如下:
σ̅x1−̅x2 = √(σ1²/n1 + σ2²/n2)
当样本量足够大时,_x1 − _x2 的分布可以被认为是正态分布。双样本 z 检验假设检验的逐步方法与单样本 z 检验类似。
让我们考虑一个例子。我们研究了达拉斯两所学校 A 和 B 的学生的智商分数,并想知道这两所学校的平均智商水平是否不同。记录了每所学校 30 名学生的简单随机样本。
A= [95,110, 105, 120, 125, 110, 98, 90, 99, 100,110, 112, 106, 92, 108, 97, 95, 99, 100, 100, 103, 125, 122, 110, 112, 102, 92, 97, 89, 102]
B = [98, 90, 100, 93, 91, 79, 90, 100, 121, 89, 101, 98, 75, 90, 95, 99, 100, 120, 121, 95,
96, 89, 115, 99, 95, 121, 122, 98, 97, 97]
研究人员收集了一个样本数据,观察到的样本比例 p = 0.84,假设的总体比例 p0 = 0.8,样本大小 n = 500。零假设和替代假设如下:
H0 : μ1 − μ2 = 0
Ha : _p ≠ p0.
我们选择显著性水平 α=0.05。在 Python 中,通过使用 statsmodels 包的 ztest() 函数,我们进行以下计算:
from statsmodels.stats.weightstats import ztest as ztest
#IQ score
A= [95,110, 105, 120, 125, 110, 98, 90, 99, 100,
110, 112, 106, 92, 108, 97, 95, 99, 100, 100,
103, 125, 122, 110, 112, 102, 92, 97, 89,102] #school A
B = [98, 90, 100, 93, 91, 79, 90, 100, 121, 89,
101, 98, 75, 90, 95, 99, 100, 120, 121, 95,
96, 89, 115, 99, 95, 121, 122, 98, 97, 97] # school B
#perform two- sample z-test
z_statistic, p_value = ztest(A, B, value=0, alternative = 'two-sided')
print(f"The test statistic is {z_statistic} and the corresponding p-value is {p_value}.")
H0 : _p = p0,
比例的 z-test
H0 : _p ≥ p0 H0 : _p ≤ p0 H0 : _p = p0
单比例 z-test
在这里,n 是样本大小。让我们考虑一个例子。在休斯顿的一所社区学院,一位研究人员想知道学生是否支持一些等于 80% 的变化。他将在显著性水平 α = 0.05 下使用单比例 z-test。为了在 Python 中实现代码,他可以使用 statsmodel 库中的 proportions_ztest。语法如下:
nobs: 试验或观察的数量
单比例 z-test 用于比较样本比例 _p 与假设比例 p0 之间的差异。同样,正如在均值 z-test 中,我们对零假设和替代假设有三种形式 - 左尾、右尾和双尾测试:
测试统计量计算如下:
z = _p − p0___________√___________p0(1 − p0)_________n
在前面的代码中,我们选择了 alternative = 'two-sided',这与研究的零假设和替代假设相关。Python 代码产生的测试统计量和 p-value 分别为 1.757 和 0.079。使用显著性水平 0.05,由于 p-value > 0.05,我们未能拒绝零假设。换句话说,我们没有足够的证据表明来自两所学校的学生的智商平均分数不同。
statsmodels.stats.proportion.proportions_ztest(count,
nobs, value=None, alternative='two-sided')
-
我们也可以测试比例的差异。让我们看看如何进行比例的 z-test。
-
零假设和替代假设如下:
-
value: 这是零假设的值,在单样本测试的情况下等于比例。在双样本测试的情况下,零假设是prop[0] - prop[1] = value,其中prop是两个样本中的比例。如果没有提供,value= 0,零假设是prop[0] = prop[1]。-
'two-sided': 双尾测试 -
'larger': 右尾测试 -
'smaller': 左尾测试
-
Ha : μ1 − μ2 ≠ 0
alternative: 替代假设:
Ha : _p < p0 Ha : _p > p0 Ha : _p ≠ p0
我们将在 Python 中实现一个单比例双尾 z-test 来计算测试统计量和 p-value:
#import proportions_ztest function
from statsmodels.stats.proportion import proportions_ztest
count = 0.8*500
nobs = 500
value = 0.84
#perform one proportion two-tailed z-test
z_statistic, p_value = proportions_ztest(count, nobs,
value, alternative = 'two-sided')
print(f"The test statistic is {z_statistic} and the
corresponding p-value is {p_value}.")
测试统计量为-2.236067977499786,相应的 p 值为 0.0253473186774685。由于 p 值 < 0.05(显著性水平),我们拒绝零假设。有足够的证据表明,支持变化的学生的比例与 0.8 不同。
双比例 z 检验
此检验用于检验两个总体比例之间的差异。还有三种形式的零假设和备择假设。测试统计量的计算如下:
z = ‾ p 1 − ‾ p 2 ____________ √ _____________ _ p (1 − _ p )( 1 _ n 1 + 1 _ n 2)
在这里,_ p 1 是从总体 1 的简单随机样本中的样本比例,_ p 2 是从总体 2 的简单随机样本中的样本比例,n 1 和 n 2 是样本大小,_ p 是总合并比例,计算如下:
_ p = n 1‾ p 1 + n 2‾ p 2 _ n 1 + n 2 .
如果 A 校支持变化的学生的比例与 B 校支持变化的学生的比例存在差异,我们将考虑一个类似的例子。在这里,n 1 = 100,n 2 = 100,_ p 1 = 0.8,_ p 2 = 0.7。你可以使用statsmodels库中的proportions_ztest函数(Seabold, Skipper, 和 Josef Perktold, “statsmodels: Econometric and statistical modeling with python.” Proceedings of the 9th Python in Science Conference. 2010)来进行假设检验。在这里,我们将直接使用测试统计量来计算 p 值。零假设和备择假设如下:
H 0 : _ p 1 = _ p 2 (两个总体比例相等)
H a : _ p 1 ≠ _ p 2 (两个总体比例不同)
接下来,我们指定双尾检验的显著性水平为α = 0.05。然后我们计算测试统计量和 p 值如下:
scipy.stats.norm.sf(abs(z))*2
测试统计量为 1.633,双尾检验的 p 值为 0.10。因为 p 值大于指定的显著性水平 0.05,所以我们未能拒绝零假设。有足够的证据表明,支持变化的 A 校和 B 校学生的比例是不同的。
最后,实现上述计算的 Python 代码如下:
import math
import scipy
p_1bar = 0.8
p_2bar = 0.7
n1 = 100.0
n2 = 100.0
p= (p_1bar*n1 + p_2bar*n2)/(n1+n2) # the total pooled proportion
z = (p_1bar-p_2bar)/math.sqrt(p*(1-p)*(1/n1+1/n2))
pval = scipy.stats.norm.sf(abs(z))*2
print(f"The test statistic is {z} and the p-value for two tailed test is {pval}.")
在本节中,我们讨论了不同的重要统计概念,例如 z 分数、z 统计量、临界值、p 值以及均值和比例的 z 检验。我们将在下一节讨论选择错误率和功效分析。
选择错误率和功效分析
统计学概括了围绕形成从数据行为中得出可接受结论的近似。因此,统计错误是不可避免的。统计模型发现的重要性基本上由错误率决定。一种可以用来最小化建模错误的方法,尤其是在样本量较低的情况下,是功率测试。统计功率是正确拒绝零假设的概率,从而最小化 II 型错误。其中α(α)是 I 型错误,β(β)是 II 型错误,功率的公式是1 – β。为了回顾,I 型错误是错误拒绝零假设的概率。
正如所注,功率在样本量较小时更为重要,因为大数定律通常有助于最小化误差,前提是选择了适当的抽样方法。当比较的差异相对较小时,功率也很重要。一种功率分析可能特别有用的场景是抽样成本较高时,例如在人类研究中。功率分析可以用来根据所需的功率、I 型错误率和效应大小找到适当的最小样本量。这些参数之间的关系可以根据需要进行探索。效应大小是在假设检验中比较的数据之间的差异或相似性,例如平均值的标准化差异或相关性。在假设检验中,常见的显著性水平(I 型错误)通常在 0.01 到 0.1 之间,常见的功率水平在 0.8 到 0.9 之间,尽管这些指标是案例依赖的(https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7745163/#:~:text=The ideal power of a,high as 0.8 or 0.9)。
功率的一些显著特性如下:
-
必须有足够的功率来检测有意义的不同
-
功率随样本量增加
-
功率随效应大小增加
-
随着功率的增加,标准差(以及方差和标准误)减小
在以下图中,我们可以看到 I 型错误(α)是我们可能错误地假设零假设并得出没有统计显著差异的结论的地方,而实际上数据点不属于数据源 2(零假设),而是属于数据源 1。II 型错误(β)是我们可能错误地假设该区域的数据属于数据源 1(备择假设)的地方,而实际上它属于数据源 2。

图 3.9 – 左尾双样本 z 检验中的误差可视化
如前图所示,我们看到了一个左尾 t 检验,将右侧的零分布与左侧的备择分布进行比较。在此图中,我们可以看到两个突出的概念:
-
随着α的减小,临界值向左滑动,更多地接近分布的异常值,β变大,反之亦然
-
统计功效是数据源 2 中β右侧的区域,即功效 = (1-β)
一类错误是基于预先选择的阈值确定的;研究者可能对 90%、95%或 99%的置信水平感到最舒适。然而,二类错误是基于参数化(标准差、效应量、样本量等)。因此,我们可以使用功效分析来确定样本量,反之亦然。各种假设检验的功效分析将在以下两章中实现。
双样本合并 z 检验的功效分析
让我们看看两个不同学科(学科 A 和学科 B)的教授薪资数据集。我们想知道根据我们拥有的数据,这两个学科之间是否存在统计上显著的差异。首先,我们需要进行功效分析,以了解我们是否有足够的样本来信任我们可能进行的 z 检验的结果。我们需要的功效分析组件包括效应量、一类错误率、期望的二类错误率、备择假设的方向(组 2 预计比组 1 大、小,或者可能比组 1 大或小),以及较大样本中观测值的比例相对于较小样本。学科 A 有 181 个薪资数据,学科 B 有 216 个。因此,216 将是我们的分子,对应于我们将考虑的组 1(学科 A 将是组 2)。
让我们假设我们不确定一个组是否会比另一个组大或小;我们将考虑这是一个双尾假设检验。这个 z 检验的效应量是两组之间的差异。我们将使用Cohen 的 d。为了计算它,我们需要将两组均值的差异除以合并标准差。在这里计算,合并标准差是组 1 样本数量乘以组 1 的方差,加上组 2 的相同值,所有这些除以两组的合并样本量。以下是合并标准差的方程:
√ _____________ n 1 σ 1 2 + n 2 σ 2 2 _ n 1 + n 2
对于效应量,我们需要将均值差除以效应量,如下所示:
|μ 1 − μ 2| ______________ √ ___________ n 1 σ 1 2 + n 2 σ 2 2 _ n 1 + n 2
注意,在效应量的方程中,我们取均值差的绝对值。这是因为,对于 Cohen 的 d,我们总是需要这个测试的正均值差。
如此计算,如果我们想要 80%的功效(II 型错误率是 20%)和 0.05 的 I 型错误率(显著性水平),我们需要在组 1 中 172 个样本和在组 2 中 145 个样本。然而,如果我们想要 90%的功效和 0.01 的显著性水平(99%置信度),我们需要在组 1 中 325 个样本和在组 2 中 274 个样本:
import statsmodels.api as sm
import math
df_prof = sm.datasets.get_rdataset("Salaries", "carData").data
df_prof_A = df_prof.loc[df_prof['discipline'] == 'A']
df_prof_B = df_prof.loc[df_prof['discipline'] == 'B']
def pooled_standard_deviation(dataset1, dataset2, column) -> float:
pooledSD = math.sqrt(((len(dataset1) - 1)*(dataset1[column].std()**2)+(len(dataset2) - 1)*(dataset2[column].std()**2))/(len(dataset1) + len(dataset2) - 2))
return pooledSD;
stdDeviation = pooled_standard_deviation(
dataset1 = df_prof_A, dataset2=df_prof_B,
column='salary')
from statsmodels.stats.power import NormalIndPower
effect = abs(df_prof_B['salary'].mean() –
df_prof_A['salary'].mean() ) / stdDeviation
# The difference between two means divided by std if pooled 2-sample
alpha = 0.05
power = 0.8
ratio=1.19 # # of obs in sample 2 relative to sample 1
analysis = NormalIndPower()
result = analysis.solve_power(effect, power=power, nobs1=None, ratio=ratio, alpha=alpha, alternative='two-sided')
print('Sample Size Required in Sample 1: {:.3f}'.format(
result*ratio)) # nobs1 is the sample size.
print('Sample Size Required in Sample 2: {:.3f}'.format(
result)) # nobs2 is the sample size.
此代码的输出如下:
Sample Size Required in Sample 1: 171.620
Sample Size Required in Sample 2: 144.218
effect = abs(df_prof_B['salary'].mean() –
df_prof_A['salary'].mean() ) / stdDeviation
alpha = 0.01
power = 0.9
ratio=1.19 # # of obs in sample 2 relative to sample 1
analysis = NormalIndPower()
result = analysis.solve_power(effect, power=power,
nobs1=None, ratio=ratio, alpha=alpha,
alternative='two-sided')
print('Sample Size Required in Sample 1: {:.3f}'.format(
result*ratio)) # nobs1 is the sample size.
print('Sample Size Required in Sample 2: {:.3f}'.format(
result)) # nobs2 is the sample size.
此代码的输出如下:
样本所需样本量 1: 325.346
样本所需样本量 2: 273.400
摘要
在本章中,我们介绍了假设检验的概念。我们从一个基本的假设检验轮廓开始,包含四个关键步骤:
-
陈述假设
-
执行测试
-
确定是否拒绝或未能拒绝零假设
-
在推断范围内得出统计结论
然后我们讨论了可能发生的潜在错误以及假阳性和假阴性,并定义了测试的预期错误率(alpha)和测试的效力(beta)。
我们还讨论了名为 z 检验的统计程序。这是一种使用假设为正态分布的样本数据进行假设检验的类型。在关于不同类型 z 检验的章节中,也介绍了 z 分数和 z 统计量,例如用于均值或比例的单样本或双样本 z 检验。
最后,我们讨论了功效分析的原理和动机,功效分析可以用来识别错误拒绝零假设的概率以及确定样本大小。我们还探讨了双总体合并 z 检验的分析参数。在这里,我们简要考察了效应量,这是我们在进行假设检验时寻找的影响值(治疗的效应)。我们将在下一章讨论功效分析,随着我们对假设检验不同应用的迭代。
在下一章中,我们将讨论更多的参数假设检验。虽然这些测试仍然需要分布假设,但假设将比 z 检验的假设不那么严格。
第四章:参数检验
在上一章中,我们介绍了假设检验的概念,并展示了 z 检验的几个应用。z 检验是称为参数检验的假设检验家族中的一类检验。参数检验是强大的假设检验,但参数检验的应用需要数据满足某些假设。虽然 z 检验是一个有用的检验,但它受到所需假设的限制。在本章中,我们将讨论几个更多的参数检验,这将扩展我们的参数工具集。更具体地说,我们将讨论 t 检验的各种应用,当存在多于两个数据子组时如何进行测试,以及皮尔逊相关系数的假设检验。我们将以参数检验的效力分析讨论结束本章。
在本章中,我们将涵盖以下主要主题:
-
参数检验的假设
-
t 检验——一种参数假设检验
-
多于两个组的测试和方差分析(ANOVA)
-
皮尔逊 相关系数
-
效力分析示例
参数检验的假设
参数检验对人口数据做出假设,要求统计实践者在建模之前分析数据,尤其是在使用样本数据时,因为当真正的总体参数未知时,样本统计量被用作总体参数的估计。这些是参数假设检验的三个主要假设:
-
正态分布的人口数据
-
样本独立
-
等方差人口(在比较两个或更多组时)
在本章中,我们讨论了 z 检验、t 检验、ANOVA 和皮尔逊相关。这些测试用于连续数据。除了这些假设之外,皮尔逊相关要求数据包含成对样本。换句话说,每个被比较的组中必须有相等数量的样本,因为皮尔逊相关基于成对比较。
虽然这些假设是理想的,但有许多情况下这些假设无法得到保证。因此,了解这些假设具有一定的稳健性,这取决于测试是有用的。
正态分布的人口数据
因为在参数假设检验中,我们感兴趣的是对总体参数进行推断,例如均值或标准差,我们必须假设所选择的参数代表分布,并且可以假设数据有一个中心趋势。我们还必须假设统计量(从样本或抽样分布中取出的参数值)代表其相应的总体参数。因此,由于我们在参数假设检验中假设总体是正态分布的,样本也应该同样是正态分布的。否则,假设样本代表总体是不安全的。
参数假设检验高度依赖于均值,并假设它强烈地代表了数据的中心点(所有总体数据都围绕均值中心分布)。考虑两个分布的均值正在被比较以测试它们之间是否存在统计上显著的差异。如果分布是偏斜的,均值就不会是数据的中心点,因此不能很好地代表分布。由于这种情况,从比较均值得到的推断将不可靠。
对正态分布数据的稳健性
许多假设检验在用样本对总体进行估计时指定了自由度。自由度迫使模型假设所使用的分布中存在比实际存在的额外方差。尽管分析中的统计参数保持不变,但假设的额外方差使得集中趋势的度量更接近。换句话说,使用自由度迫使集中趋势的度量更中心地代表计算它们的分布。这是因为假设样本——虽然代表了它们的总体——但以误差范围代表它们的总体。因此,使用自由度的参数假设检验对违反正态分布数据的要求具有一定的稳健性。
在 *图 4**.1 中显示的图表中,我们有一个略微偏斜的分布。一个应用了自由度,而另一个没有。我们可以看到,无论是否使用自由度,均值和中位数之间的距离都是相同的。然而,使用自由度的分布出现了更多的误差(更多的方差):

图 4.1 – 展示自由度的影响
当使用考虑均值的假设检验时,我们可以看到,均值,虽然不是中心(如中位数那样),当使用自由度时,相对于所有数据点,更接近分布的中心。由于参数假设检验使用均值作为中心点,这对于模型的有用性很重要,因为当使用自由度时,均值更能代表数据的中心点。这是对正态性有一定稳健性的主要原因。还有一些其他稳健性在于统计解释,例如在确定置信水平时;如果分布不是完美正态分布,例如,使用 90% 的置信水平而不是 99% 的置信水平可能更有益。
测试正态分布数据
确定一个分布是否为正态分布,从而可以用于参数假设测试,有多种方法。通常,对正态性的遵守程度由研究人员自行决定。本节中的方法在基于视觉检查以及应用统计显著性水平方面留下了一些讨论正态性的空间。
视觉检查
识别一个分布是否为正态分布的最佳测试方法是基于视觉检查。我们可以使用 分位数-分位数(QQ)图和直方图——以及其他测试方法——来对分布进行视觉检查。
在以下代码片段中,我们使用 scipy.stats 模块的 probplot 函数生成原始数据的图表以及 QQ 图:
import matplotlib.pyplot as plt
import scipy.stats as stats
import numpy as np
mu, sigma = 0, 1.1
normally_distributed = np.random.normal(mu, sigma, 1000)
在 *图 4**.2 中,我们可以看到第一列是指数分布数据的直方图,在其下方是它的 QQ 图。由于点与接近 45 度红色线的近似程度非常远,该红色线代表纯正态分布,我们可以得出结论,数据不是正态分布的。通过视觉检查第二列中的数据,我们可以看到直方图显示了近似正态分布的数据集。这由其下方的 QQ 图得到证实,其中点主要近似于 45 度红色线。至于 QQ 图的尾部,这些数据点代表了偏斜度的密度。我们期望在正态分布的数据集中,大部分数据点将趋向于红色线的中心。在指数分布中,我们可以看到向左的重密度,红色线的左侧尾部,以及向右上方的线上的点稀疏分布。QQ 图可以从左到右阅读,与直方图中的分布相对应,其中最小值出现在 x 轴的左侧,最大值出现在右侧:

图 4.2 – 使用 QQ 图和直方图评估正态性
QQ 图和直方图的视觉检查应该足以帮助研究人员得出正态性假设是否被违反的结论。然而,在可能不想进行视觉检查的情况下——例如在构建数据科学管道时——有其他方法可以提供关于正态性的具体测量。最常用的三种测试方法是 Kolmogorov-Smirnov、Anderson-Darling 和 Shapiro-Wilk 测试。
Kolmogorov-Smirnov 测试更关注数据的中心性。然而,如果数据在中心周围有较大的方差,则测试的效力会降低。Anderson-Darling 测试比中心更关注数据的尾部,如果数据有重尾和极端异常值,则更有可能识别出与正态性的不符。这两个测试在大样本量上表现良好,但样本量较小时效力并不强。我们考虑的第三个测试,Shapiro-Wilk,比 Kolmogorov-Smirnov 和 Anderson-Darling 测试更通用,因此对小样本量更稳健。基于这些特性,使用 Shapiro-Wilk 测试在自动化管道中可能更有用。或者,降低所应用测试的置信水平可能更好。
Kolmogorov-Smirnov
scipy.stats模块中的kstest函数,使用stats.norm.cdf(scipy的累积密度函数)执行这种单样本测试版本。双样本版本测试与指定的分布进行比较,以确定两个分布是否匹配。在双样本情况下,要测试的分布必须作为numpy数组提供,而不是下面代码片段中使用的stats.norm.cdf函数(图 4**.3)。然而,这超出了测试正态性的范围,所以我们将不会探讨这一点。
Kolmogorov-Smirnov 将计算出的测试统计量与基于表格的临界值进行比较(kstest内部计算这个值)。与其他假设检验一样,如果测试统计量大于临界值,则可以拒绝给定分布是正态分布的零假设。如果 p 值足够低,也可以进行这种评估。测试统计量是给定分布中所有数据点与累积密度函数之间最大距离的绝对值。
Kolmogorov-Smirnov 特殊要求
Kolmogorov-Smirnov 测试要求数据围绕零中心并缩放到标准差为 1。所有数据都必须进行转换才能进行测试,但可以应用于预转换的分布;中心化和缩放后的分布不需要用于进一步的统计测试或分析。
在以下代码片段中,我们测试以确认正态分布数据集normally_distributed是否为正态分布。该数据集的平均值为 0,标准差为 1。输出确认数据是正态分布的。图 4**.3中的图表显示了围绕均值为 0、标准差为 1 的正态分布,其右侧是相同分布的指数转换版本:
from scipy import stats
import numpy as np
mu, sigma = 0, 1
normally_distributed = np.random.normal(mu, sigma, 1000)

图 4.3 – 正态分布和指数分布数据
在这里,我们运行 Kolmogorov-Smirnov 测试:
stats.kstest(normally_distributed,
stats.norm.cdf)
statsmodels的科尔莫哥洛夫-斯米尔诺夫检验为我们数据产生了以下结果:
KstestResult(statistic=0.0191570377833315, pvalue=0.849436919292824)
如果我们使用相同的数据,但将其指数变换为右偏态,相同的检验表明数据不再呈正态分布:
stats.kstest(np.exp(normally_distributed), stats.norm.cdf)
显著的 p 值确认了非正态性:
KstestResult(statistic=0.5375205782404135, pvalue=9.59979841227121e-271)
接下来,让我们取一个均值为 100,标准差为 2 的 1000 个样本的分布。我们需要将其中心化到均值为 0,单位方差(标准差为 1)。在下面的代码片段中,我们生成数据,然后执行缩放并将其保存到normally_distributed_scaled变量中:
mu, sigma = 100, 2
normally_distributed = np.random.normal(mu, sigma, 1000)
normally_distributed_scaled = (
normally_distributed-normally_distributed.mean()) /
normally_distributed.std()
现在数据已按要求中心化和缩放,我们使用科尔莫哥洛夫-斯米尔诺夫检验进行检查。正如预期的那样,数据被确认呈正态分布:
stats.kstest(normally_distributed_scaled, stats.norm.cdf)
这是输出:
KstestResult(statistic=0.02597307287070466, pvalue=0.5016041053535877)
安德森-达尔林
与科尔莫哥洛夫-斯米尔诺夫检验类似,scipy的anderson检验,我们可以测试其他分布,但默认参数指定正态分布,dist="norm",假设零假设是给定的分布与正态分布在统计上是相同的。对于每个测试的分布,必须计算不同的临界值集。
安德森-达尔林与科尔莫哥洛夫-斯米尔诺夫比较
注意,虽然安德森-达尔林和科尔莫哥洛夫-斯米尔诺夫检验都使用累积密度频率分布来检验正态性,但安德森-达尔林检验与科尔莫哥洛夫-斯米尔诺夫检验不同,因为它对累积密度频率分布尾部的方差赋予比中间更大的权重。这是因为分布尾部的方差可以以比分布中间更小的增量来测量。因此,安德森-达尔林检验比科尔莫哥洛夫-斯米尔诺夫检验对尾部更敏感。与科尔莫哥洛夫-斯米尔诺夫检验一样,计算一个检验统计量,并将其与临界值进行比较。如果检验统计量大于临界值,则可以在指定的显著性水平上拒绝给定分布是正态分布的零假设。
这里,我们使用安德森-达尔林检验来检验一个均值为 19,标准差为 1.7 的随机正态概率分布。我们还测试了该数据的指数变换版本:
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
mu, sigma = 19, 1.7
normally_distributed = np.random.normal(mu, sigma, 1000)
not_normally_distributed = np.exp(normally_distributed);
图 4**.4 展示了数据的分布图:

图 4.4 – 正态分布与重尾指数分布
在下面的代码和输出中,在图 4.5中,我们可以看到在所有显著性水平上分布都是正态分布的。回想一下,显著性水平是 p 值(即,显著性水平 = 15.0 表示 p 值为 0.15 或更小是显著的):
from scipy import stats
import pandas as pd
import numpy as np
def anderson_test(data):
data = np.array(data)
test_statistic, critical_values, significance_levels = stats.anderson(normally_distributed, dist='norm')
df_anderson = pd.DataFrame({'Test Statistic':np.repeat(test_statistic, len(critical_values)), 'Critical Value':critical_values, 'Significance Level': significance_levels})
df_anderson.loc[df_anderson['Test Statistic'] >= df_anderson['Critical Value'], 'Normally Distributed'] = 'No'
df_anderson.loc[df_anderson['Test Statistic'] <df_anderson['Critical Value'], 'Normally Distributed'] = 'Yes'
return df_anderson;
mu, sigma = 19, 1.7
normally_distributed = np.random.normal(mu, sigma, 1000)
anderson_test(normally_distributed)
在这里,通过 numpy 的 random.normal 函数生成数据,使用 Anderson-Darling 方法进行测试,并确认其为正态分布:
| 检验统计量 | 临界值 | 显著性水平 | 正态分布 |
|---|---|---|---|
| 0.191482344 | 0.574 | 15 | 是 |
| 0.191482344 | 0.653 | 10 | 是 |
| 0.191482344 | 0.784 | 5 | 是 |
| 0.191482344 | 0.914 | 2.5 | 是 |
| 0.191482344 | 1.088 | 1 | 是 |
图 4.5 – 正态分布数据的 Anderson-Darling 结果
在这里,我们测试了正态分布数据的指数变换,以检查其正态性。数据呈指数分布,应在所有显著性水平上被拒绝。然而,我们在图 4.6中看到,它在 0.01 的显著性水平(99% 置信度)上未能被拒绝。因此,根据具体的使用情况,检查所有显著性水平、使用不同的测试或基于多个测试的结果做出决定可能是谨慎的:
not_normally_distributed = np.exp(normally_distributed)
anderson_test(not_normally_distributed)
我们的非正态分布数据的 Anderson-Darling 测试结果如下所示在图 4.6:
| 检验统计量 | 临界值 | 显著性水平 | 正态分布 |
|---|---|---|---|
| 0.96277351 | 0.574 | 15 | 否 |
| 0.96277351 | 0.653 | 10 | 否 |
| 0.96277351 | 0.784 | 5 | 否 |
| 0.96277351 | 0.914 | 2.5 | 否 |
| 0.96277351 | 1.088 | 1 | 是 |
图 4.6 – 非正态分布数据的 Anderson-Darling 结果
Shapiro-Wilk
scipy.stats shapiro 模块,因此输入数据在测试之前不需要更改。在 scipy 中,此测试的显著性水平为 0.05。
Shapiro-Wilk 与 Kolmogorov-Smirnov 和 Anderson-Darling 的比较
与 Kolmogorov-Smirnov 和 Anderson-Darling 相比,Shapiro-Wilk 对于测试大约小于 50 的较小样本量是理想的。然而,一个缺点是,由于 Shapiro-Wilk 使用重复抽样和测试来通过蒙特卡洛模拟应用计算出的检验统计量,大数定律的风险在于,随着样本量的增加,存在固有的风险增加,遇到第二类错误(功率损失)并未能拒绝零假设,其中零假设表示给定的分布是正态分布的。
使用与 Anderson-Darling 测试相同的分布,我们使用 Shapiro-Wilk 进行测试。我们可以看到,具有均值为 19 和标准差为 1.7 的随机正态分布,Shapiro-Wilk 测试已确认,p 值为 0.99,不应拒绝输入分布是正态分布的零假设:
mu, sigma = 19, 1.7
normally_distributed = np.random.normal(mu, sigma, 1000)
stats.shapiro(normally_distributed)
这是输出结果:
ShapiroResult(statistic=0.9993802905082703, pvalue=0.9900037050247192)
当使用正态分布数据的指数变换版本进行测试时,我们发现显著的 p 值(p = 0.0),这表明我们有足够的证据拒绝零假设,并得出结论分布不是正态分布:
not_normally_distributed = np.exp(normally_distributed)
stats.shapiro(not_normally_distributed)
这是输出:
ShapiroResult(statistic=0.37320804595947266, pvalue=0.0)
独立样本
在参数假设检验中,样本的独立性是另一个重要假设。非独立抽样可能导致两种影响。一种影响发生在进行子群抽样时。这里的问题是,总体中一个子群的响应可能与另一个子群的响应不同,甚至可能更接近于不同总体的响应。然而,当抽样代表总体时,这种子群差异可能并不非常具有代表性。
非独立抽样的另一个影响是,当样本在时间上足够接近,以至于一个样本的发生会阻止或排除另一个样本的发生时。这被称为序列(或自)相关性。
参数检验通常对违反此要求不具鲁棒性,因为它对测试结果的可解释性有直接、分类的影响。关于子群抽样,可以通过诸如在第一章**,抽样与泛化中概述的良好结构化抽样方法来防止这种情况。然而,关于序列效应,我们可以在数据中测试自回归相关性(也称为序列相关性)。
杜宾-沃森
评估抽样独立性缺失的最常见测试之一是一阶自回归测试,称为杜宾-沃森测试。自回归意味着使用先前数据点来预测当前数据点。一阶意味着最后一个抽样数据点(滞后一)是与序列中最近抽样数据点(滞后零)最显著相关的点。在一阶自相关中,每个数据点的相关性最强的是前一个数据点。杜宾-沃森测试并不测试任何值是否与它之前的值相关,而是测试每个值与它之前值之间是否存在足够强的关系,以至于可以得出存在显著自相关性的结论。从这个意义上说,对非独立抽样有一定的鲁棒性,即一次或两次意外事件可能不会完全使假设检验无效,但此类违规的持续发生将会。
杜宾-沃森值为 2 表示没有显著的自相关性,介于 0 到 2 之间的值表示正(直接)自相关性,而介于 2 到 4 之间的值表示负(反向)自相关性。
在以下示例中,我们有两个分布,每个分布都有 1,000 个样本。左边的分布是一个正弦分布,表现出强烈的自回归相关性,而右边的分布是一组随机生成数据,显示出白噪声方差(围绕均值为 0 的随机点)。使用statsmodels.stats模块中的durbin_watson()函数,我们能够确认正弦模式中的直接、正的一阶自相关性(非常小的 Durbin-Watson 值)和与随机噪声的 Durbin-Watson 统计量为 2.1,表明没有自相关性。因此,在图 4.7 中,左边的图不是由独立样本组成,而右边的图是:
from statsmodels.stats.stattools import durbin_watson
import matplotlib.pyplot as plt
import numpy as np
mu, sigma = 0, 1.1
independent_samples = np.random.normal(mu, sigma, 1000)
correlated_samples = np.linspace(-np.pi, np.pi, num=1000)
fig, ax = plt.subplots(1,2, figsize=(10,5))
ax[0].plot(correlated_samples, np.sin(correlated_samples))
ax[0].set_title('Durbin Watson = {}'.format(
durbin_watson(correlated_samples)))
ax[1].plot(independent_samples)
ax[1].set_title('Durbin Watson = {}'.format(
durbin_watson(independent_samples)))

图 4.7 – 串联相关和正态分布的序列数据
等样本方差
与正态分布数据的假设类似,等样本方差的假设——也称为方差齐性——是关于比较的分布的物理属性形状。假设等样本方差有助于提高参数测试的功效。这是因为当均值被识别为不同时,我们有信心;我们还知道潜在分布重叠的程度。当测试对整个分布的位置有直观认识——实际上,对效应大小的真正了解——功效会增加。相反,随着方差的发散,功效会降低。
对等样本方差的鲁棒性
虽然等样本方差在参数测试中很有用,但存在对这些测试的修改,有助于使结果对偏离等方差的偏差具有鲁棒性。这些测试的一个突出修改版本是使用Welch-Satterthwaite自由度调整。因为当每个组有不同的方差时,将相同的自由度应用于每个组会导致数据的误表示,Welch-Satterthwaite 调整在分配假设等方差的参数测试的自由度时考虑了方差差异。使用 Welch-Satterthwaite 调整的两个常见测试是 Welch 的 t 检验和 Welch 的方差分析测试。当用于小样本时,这些测试可能不可靠,但当用于足够大的样本量以具有足够功效时,结果应与它们的非 Welch 对应物大致相同。
测试等方差
在测试分布之间的方差相等时,我们有两种突出的测试:Levene 方差齐性检验和Fisher 的 F 检验。
Levene 方差齐性检验
Levene 方差齐性检验在测试两个或更多组的方差齐性时很有用。在下面的代码片段中 图 4**.8,我们使用三个分布进行测试,每个分布的样本量为 100,均值为 0,标准差为 0.9、1.1 和 2。图 4**.8 是使用上述 图 4**.8 代码输出的数据生成的三个分布的图。
from scipy.stats import levene
np.random.seed(26)
mu1, sigma1, mu2, sigma2, mu3, sigma3 = 0,0.9,0,1.1,0,2
distro1, distro2, distro3 = pd.DataFrame(), pd.DataFrame(),
pd.DataFrame()
distro1['x'] = np.random.normal(mu1, sigma1, 100)
distro2['x'] = np.random.normal(mu2, sigma2, 100)
distro3['x'] = np.random.normal(mu3, sigma3, 100)
我们可以看到它们不同的标准差如何影响它们的范围。

图 4.8 – 多重方差齐性检验的分布
我们可以看到,由于这个结果是一个具有统计学意义的 p 值,表明分布不具有同质性,因此测试对非同质性方差的违反很敏感:
f_statistic, p_value = levene(distro1['x'], distro2['x'], distro3['x'])
if p_value <= 0.05:
print('The distributions do not have homogenous variance. P-value = %.4f, F-statistic = %.4f'%(p_value, f_statistic))
else:
print('The distributions have homogenous variance.P-value = %.4f, F-statistic = %.4f'%(p_value, f_statistic))
这是输出结果:
分布不具有同质性。P 值 = 0.0000
Fisher 的 F 检验
当同时测试两组的方差齐性时,Fisher 的 F 检验很有用。这个检验将一个检验统计量与临界值进行比较,以确定方差是否在统计学上相同。计算出的 F 统计量是第一组的方差除以第二组的方差。第一组总是具有较大方差的组。使用前面的数据,让我们比较分布 1 和分布 3。分布 3 的方差较大,为 2,因此该组的方差将是计算 F 统计量时的分子。由于每个组都有 100 个样本量,它们的自由度在查表时各为 99。然而,由于我们将使用 scipy Python 包来计算检验,这里不需要查表,因为 scipy 使用 f.cdf() 函数为我们完成这项工作。与 Levene 检验的结果一致,F 检验表明分布 1 和分布 3 不具有同质性:
from scipy.stats import f
def f_test(inputA, inputB):
group1 = np.array(inputA)
group2 = np.array(inputB)
if np.var(group1) > np.var(group2):
f_statistic = np.var(group1) / np.var(group2)
numeratorDegreesOfFreedom = group1.shape[0] - 1
denominatorDegreesOfFreedom = group2.shape[0] - 1
else:
f_statistic = np.var(group2)/np.var(group1)
numeratorDegreesOfFreedom = group2.shape[0] - 1
denominatorDegreesOfFreedom = group1.shape[0] - 1
p_value = 1 - f.cdf(f_statistic,numeratorDegreesOfFreedom, denominatorDegreesOfFreedom)
if p_value <= 0.05:
print('The distributions do not have homogenous variance. P-value = %.4f, F-statistic = %.4f'%(p_value, f_statistic))
else:
print('The distributions have homogenous variance. P-value = %.4f, F-statistic = %.4f'%(p_value, f_statistic))
f_test(distro3['x'], distro1['x'])
这个 F 检验输出如下:
分布不具有同质性。P 值 = 0.0000,F 统计量 = 102622.9745
t 检验 – 参数假设检验
在上一章中,当总体标准差已知时,应用了均值 z 检验。然而,在现实世界中,获得总体标准差并不容易(或几乎不可能)。在本节中,我们将讨论另一种称为 t 检验的假设检验,当总体标准差未知时使用。通过取代表性总体样本数据的均值和标准差来估计总体均值和标准差。
从广义上讲,均值 t 检验的方法与均值 z 检验的方法非常相似,但检验统计量和 p 值的计算方法与 z 检验不同。检验统计量通过以下公式计算:
t = _x − μ _ s/ √ _ n
在这里,x,μ,s 和 n 分别是样本均值、总体均值、样本标准差和样本大小,当样本数据 x 正态分布时,它具有 t-分布。以下代码展示了标准正态分布(蓝色曲线)和两个样本大小(3 和 16 个样本)的 t-分布(绿色和红色曲线):
# libraries
import numpy as np
import scipy.stats as stats
# creating normal distribution
x =np.linspace(-5, 5, 1000) #create 1000 point from -5 to 5
y = stats.norm.pdf(x) # create probability density for each point x - normal distribution
# creating Student t distributions for 2 sample sizes n =3 and n =15
degree_freedom1 = 2
t_dis1 = stats.t.pdf(x, degree_freedom1)
degree_freedom2 = 15
t_dis2 = stats.t.pdf(x, degree_freedom2)
以下可视化是针对考虑的这 3 个分布。

图 4.9 – 正态分布和 t-分布
观察到三条曲线具有相似的对称性和形状,但样本大小较小时,变异性更大(或者说,尾部更重)。历史上,当样本大小大于 30 时,研究人员认为样本标准差可以代表总体,即当 n > 30 时,红色曲线近似于蓝色曲线。如果样本分布与标准正态分布重叠,也常用 z-检验。这种做法背后有一些理由,因为以前,临界值表存储到样本大小为 50,但现在,随着计算能力和互联网的强大,任何样本大小的 t 值都可以轻松获得。
均值 t-检验
在这部分将考虑与总体均值或两个总体均值相关的单样本和双样本 t-检验,其中总体方差或总体标准差是未知的。
要进行 t-检验,需要满足以下假设:
正态性:样本是正态分布的
独立性:观察值是从总体中随机选取以形成样本,换句话说,它们是独立的
让我们在下一节考虑单样本 t-检验。
单样本 t-检验
与单样本 z-检验类似,为了进行假设检验,需要考虑零假设和备择假设。以下列出了对应于左尾、右尾和双尾 t-检验的三个零假设和备择假设:
H 0 : μ ≥ μ 0 H 0 : μ ≤ μ 0 H 0 : μ = μ 0
H a : μ < μ 0 H a : μ > μ 0 H a : μ ≠ μ 0
接下来,需要根据研究目的指定显著性水平 α。有两种方法:p 值方法和临界值方法。在 p 值方法中,拒绝规则(拒绝 H 0——零假设)是当 p 值小于或等于所选的指定显著性水平时。在临界值方法中,拒绝规则是当检验统计量小于或等于左尾 t-检验的临界值 - t α,对于右尾 t-检验,检验统计量大于或等于 t α,对于双尾检验,检验统计量小于或等于 - t α/2 或大于或等于 t α/2。最后一步是对假设检验的统计结论进行解释。
要根据学生 t 分布的值找到 p 值,我们可以使用以下语法:
scipy.stats.t.sf(abs(x), df)
在这里,x 是检验统计量,df 是自由度(df = n-1,其中 n 是样本大小)在公式中。
例如,为了找到一个左尾检验中自由度为 14 的 t 值为 1.9 的 p 值,这将是 Python 的实现:
import scipy.stats
round(scipy.stats.t.sf(abs(1.9), df=14),4)
输出将是 0.0391。如果显著性水平 α = 0.05,那么我们拒绝零假设,因为 p 值小于 α。对于右尾 t 检验,与左尾 t 检验类似的 Python 代码被实现以找到 p 值。对于双尾检验,我们需要将值乘以 2,如下所示:
scipy.stats.t.sf(abs(t), df)*2
在这里,t 是检验统计量,df 是自由度(df = n-1,其中 n 是样本大小)。
要在 Python 中计算临界值,我们使用以下语法:
scipy.stats.t.ppf(q, df)
在这里,q 是显著性水平,df 是公式中要使用的自由度。以下是 Python 中左尾、右尾和双尾检验的代码实现:
import scipy.stats as stats
alpha = 0.05 # level of significance
df= 15 # degree of freedom
#find t critical value for left-tailed test
print(f" The critical value is {stats.t.ppf(q= alpha, df =df)}")
#find t critical value for right-tailed test
print(f" The critical value is {stats.t.ppf(q= 1-alpha, df =df)}")
##find t critical value for two-tailed test
print(f" The critical values are {-stats.t.ppf(q= 1-alpha/2, df =df)} and {stats.t.ppf(q= 1-alpha/2, df =df)}")
这是前面代码的输出:
-
临界值为 -1.7530503556925552
-
临界值为 1.7530503556925547
-
临界值为 -2.131449545559323 和 2.131449545559323
在显著性水平 α = 0.05 下,对于左尾检验,临界值约为 -1.753。由于这是一个左尾检验,如果检验统计量小于或等于这个临界值,我们拒绝零假设。同样,对于右尾检验,如果检验统计量大于或等于 1.753,我们拒绝零假设。对于双尾检验,如果检验统计量大于或等于 2.1314 或小于或等于 -2.1314,我们拒绝零假设。最后,我们解释假设检验的统计结论。
让我们从一所高中随机选择 30 名学生并评估他们的智商。我们想测试这个高中学生智商分布的平均值是否高于 100。这意味着我们将执行右尾 t 检验。30 名学生的智商分数如下:
IQscores = [113, 107, 106, 115, 103, 103, 107, 102, 108, 107, 104, 104, 99, 102, 102, 105, 109, 97, 109, 103, 103, 100, 97, 107,116, 117, 105, 107, 104, 107]
在进行假设检验之前,我们将检查正态性和独立性假设。如果样本是从该学校高中学生的总体中随机选择的,则满足独立性假设。对于正态性,我们将检查智商分数的直方图和 QQ 图:

图 4.10 – 视觉评估学生智商分数的正态性
从直方图和 QQ 图来看,几乎没有证据表明该高中学生智商分布的总体不是正态分布。由于假设分布是正态的,我们将继续进行 t 检验。
首先,我们定义零假设和备择假设:
H 0 : μ ≤ 100
H a : μ > 100
我们选择显著性水平α=0.05。你可以通过手动使用其数学公式或通过实现 Python 来计算检验统计量。对于临界值和 p 值,之前部分展示的实现代码如下。在这里,我们将使用scipy库中的另一个函数来找到检验统计量和 p 值:
scipy.stats.ttest_1samp(data, popmean, alternative='greater')
这里适用以下规则:
-
data: 样本中的观测值 -
popmean: 零假设中的期望值 -
alternative: 对于双尾 t 检验为'two-sided',对于左尾 t 检验为'less',对于右尾 t 检验为'greater'
Python 代码实现如下:
import scipy.stats as stats
#perform one sample t-test
t_statistic, p_value = stats.ttest_1samp(IQscores, popmean =100, axis=0, alternative='greater')
print(f"The test statistic is {t_statistic} and the corresponding p-value is {p_value}.")
这是输出:
检验统计量为 6.159178830896832,相应的 p 值为 5.15076734562176e-07。
因为 p 值<0.05,其中 0.05 是显著性水平,我们有足够的证据拒绝零假设,并得出结论,该学校学生的真实平均 IQ 分数高于 100。
此外,以 95%的置信度,平均 IQ 分数介于 104.08 和 107.12 之间。我们可以在 Python 中如下进行置信区间的计算:
IQmean = np.array(IQscores).mean() # sample mean
IQsd = np.array(IQscores).std() # sample standard deviation
sample_size = len(np.array(IQscores)) # sample size
df = sample_size-1 # degree of freedom
alpha = 0.05 # level of significance
t_crit = stats.t.ppf(q=1-alpha, df =df) # critical
confidence_interval = (IQmean-IQsd*t_crit/np.sqrt(sample_size), IQmean+IQsd*t_crit/np.sqrt(sample_size))
在 Python 中使用单尾 t 检验进行假设检验的步骤与右尾和双尾 t 检验的步骤相似。
双样本 t 检验 – 池化 t 检验
与第三章**假设检验(均值的双样本 z 检验)中所述类似,均值的双样本 t 检验对于零假设和备择假设有三种形式。在进行测试之前,需要满足一些假设,如下所示:
-
正态性:两个样本是从它们的正态分布总体中抽取的
-
独立性:一个样本的观测值相互独立
-
方差齐性:两个总体假设具有相似的标准差
对于正态性,我们使用视觉直方图和两个样本的 QQ 图进行比较。我们假设独立性得到满足。为了检查两个样本的方差是否相等,我们可以通过观察它们的直方图进行可视化,如果可视化结果不明确,还可以使用 F 检验来获得额外的证据。这是一个假设检验,用于检查两个样本方差是否相等。
让我们看看两所高中 A 和 B 之间的 IQ 分数。以下是从每所学校随机选取的 30 名学生的分数:
IQscoresA=[113, 107, 106, 115, 103, 103, 107, 102,108, 107,
104, 104, 99, 102, 102, 105, 109, 97, 109, 103,
103, 100, 97, 107, 116, 117, 105, 107, 104, 107]
IQscoresB = [102, 108, 110, 101, 98, 98, 97, 102, 102, 103,
100, 99, 97, 97, 94, 100, 104, 98, 92, 104,
98, 95, 92, 111, 102, 112, 100, 103, 103, 100]
图 4.11 中显示的直方图和 QQ 图是由上述 IQ 数据生成的。

图 4.11 – 评估两所学校 IQ 分数的正态性
我们可以看到正态性假设得到满足。我们还可以通过观察直方图假设方差齐性假设得到支持。如果需要,以下是一个额外的 F 检验来检查方差齐性假设:
# F-test
import numpy as np
import scipy.stats as stats
IQscoresA = np.array(IQscoresA)
IQscoresB = np.array(IQscoresB)
f = np.var(IQscoresA, ddof=1)/np.var(IQscoresB, ddof=1) # F statistic
dfA = IQscoresA.size-1 #degrees of freedom A
dfB = IQscoresB.size-1 #degrees of freedom B
p = 1-stats.f.cdf(f, dfA, dfB) #p-value
前述代码的输出告诉我们 F 检验统计量是 0.9963,相应的 p 值是 0.50394 > 0.05(0.05 是显著性水平),因此我们未能拒绝零假设。这意味着有足够的证据表明这两个样本的标准差是相等的。
我们现在定义零假设和备择假设:
H 0 : μ A = μ B,
H a : μ A ≠ μ B.
我们选择显著性水平 α=0.05。我们使用 statsmodels.stats.weightstats.ttest_ind 函数进行 t 检验。文档可以在这里找到:www.statsmodels.org/dev/generated/statsmodels.stats.weightstats.ttest_ind.xhtml。
我们可以使用此函数通过 alternative='two-sided','larger' 或 'smaller' 来执行备择假设的三种形式。在合并方差 t 检验中,当满足方差相等的假设时,检验统计量的计算如下:
t = (‾ x 1 − ‾ x 2) − (μ 1 − μ 2) _____________ s p √ _ 1 _ n 1 + 1 _ n 2
在这里,_ x 1, _ x 2, μ 1, μ 2, n 1, 和 n 2 分别是两个样本 1 和 2 的样本均值、总体均值和样本大小,这里给出了合并标准差:
s p = √ ________________ (n 1 − 1) s 1 2 + (n 2 − 1) s 2 2 ________________ n 1 + n 2 − 2
自由度在这里显示:
df = n 1 + n 2 − 2.
让我们回到例子:
from statsmodels.stats.weightstats import ttest_ind as ttest
t_statistic, p_value, degree_freedom = ttest(IQscoresA,
IQscoresB, alternative='two-sided', usevar='pooled')
输出返回的检验统计量是 3.78,p 值是 0.00037,t 检验中使用的自由度是 58(每个样本大小有 30 个观测值,因此自由度计算为 30 + 30 - 2 = 58)。
因为 p 值 <0.05,我们拒绝零假设。有足够的证据表明,高中 A 和 B 的学生平均智商分数之间存在差异。为了进行置信水平测试,你可以适应单样本 t 检验最后一部分中的 Python 代码。
回想一下,在某些情况下,如果直方图和 QQ 图显示一些偏斜的证据,我们可以考虑在假设检验中测试中位数而不是均值。如第二章**,数据分布所示,我们可以执行数据转换(例如,对数转换)以获得正态性假设。转换后,log(data) 的中位数等于 log(data) 的均值。这意味着测试是在转换数据的均值上进行的。
双样本 t 检验 – Welch 的 t 检验
这是一个实际的双样本 t 检验,当数据呈正态分布但总体标准差未知且不相等时。我们对于正态性的假设与合并 t 检验相同,但在进行 Welch 的 t 检验时,我们可以放宽方差相等的假设。让我们考虑以下例子,其中我们有两个样本数据集:
sample1 = np.array([2,3,4,2,3,4,2,3,5,8,7,10])
sample2 = np.array([30,26,32,34,28,29,31,35,36,33,32,27])
我们假设独立性得到满足,但我们将检查这两个样本的正态性和等方差假设,如下所示:

图 4.12 – 检查 Welch 的 t 检验的等方差
通过查看图 4.12中直方图的x轴刻度,可以强烈地看出标准差不等的证据。让我们假设正态性假设得到满足。在这种情况下,进行双样本合并 t 检验不是一个好主意,但 Welch 的 t 检验就足够了。这里给出了零假设和备择假设:
H 0 : μ 1 = μ 2
H a : μ 1 ≠ μ 2
我们指定显著性水平为 0.05。为了计算检验统计量和 p 值,我们实现以下代码:
import scipy.stats as stats
t_statistic, p_value = stats.ttest_ind(sample1, sample2,
equal_var = False)
检验统计量为-22.47,p 值<0.05(显著性水平)。我们拒绝零假设。有强有力的证据表明样本数据 1 的均值与样本数据 2 的均值不同。
配对 t 检验
配对 t 检验也称为匹配对或相关 t 检验,在研究样本中的每个元素被测试两次(前测和后测或重复测量)且研究人员认为存在某些相似性(如家庭)时使用。这里列出了假设:
-
差异呈正态分布
-
差异在观察之间是独立的,但从一个测试到另一个测试是相关的
配对 t 检验在许多研究中被使用,尤其是在与前后治疗相关的医学推理测试中。让我们回顾一下智商测试分数——一位研究人员招募了一些学生,以查看训练前后是否有分数差异,如下表所示:
| 学生 | 训练前分数 | 训练后分数 | 差异 |
|---|---|---|---|
| A | 95 | 95 | 0 |
| B | 98 | 110 | 12 |
| C | 90 | 97 | 7 |
| D | 115 | 112 | -3 |
| E | 112 | 117 | 5 |
图 4.13 – 训练前和训练后分数
在这种情况下,我们不应使用独立的双样本 t 检验。应该检验差异的均值。我们可以通过直方图和 QQ 图来检查关于正态分布的假设,如下所示:

图 4.14 – 检查配对 t 检验的正态性
通过查看 QQ 图,比直方图更能明显地看出数据呈正态分布的证据。
假设差异是独立的。这里给出了零假设和备择假设:
H 0 : μ pos − μ pre = 0
H a : μ pos − μ pre > 0
d i 表示每个学生的训练前分数和训练后分数之间的差异。零假设和备择假设可以重写如下:
H 0 : μ d = 0
H a : μ d > 0
然后,计算检验统计量如下:
t = _ d − μ d _ s d _ √ _ n
在这里,d 是差异的样本均值,s_d 是差异的样本标准差。换句话说,配对 t 检验被简化为单样本 t 检验。然而,我们可以直接在 scipy 中使用以下函数:
stats.ttest_rel(data_pos, data_pre, alternative = {'two-sided', '``less', 'greater'})
备择假设对应于左尾、右尾或双尾测试。以下是 IQ 测试分数研究示例的 Python 实现:
from scipy import stats
IQ_pre = [95, 98, 90, 115, 112]
IQ_pos = [95, 110, 97, 112, 117]
t_statistic, p_value = stats.ttest_rel(IQ_pos, IQ_pre, alternative = 'greater')
测试统计量是 1.594,p 值是 0.093。因此,鉴于 p 值<0.05 和显著性水平α=0.05,我们拒绝零假设。有足够的证据表明,培训对智商分数有显著影响。
多组测试和方差分析
在前一章和前几节中,我们介绍了两组之间的测试。在本节中,我们将介绍两种测试组间差异的方法,如下:
-
使用Bonferroni 校正的成对测试
-
方差分析
当测试两个以上组之间的差异时,我们不得不使用多个测试,这会影响我们的I 型错误率。有几种方法可以控制错误率。我们将看到如何利用 Bonferroni 校正来控制I 型错误率。我们还将在本节中讨论方差分析,它用于测试多个组之间的均值差异。
多次显著性测试
在前几节中,我们探讨了如何比较两组。在本节中,我们将考虑在存在两个以上组时如何进行测试。让我们再次考虑工厂示例,其中我们在工厂地面上有几种机器模型(模型 A、模型 B 和模型 C),这些机器在工厂中用于执行相同的操作。一个有趣的问题可能是:是否有一种机器模型比其他两种模型的平均输出更高? 为了做出这个判断,我们需要进行三个测试,比较每个模型的均值差异与其他模型,测试均值差异是否不同于零。这些是我们需要测试的零假设:
μ_output,A − μ_output,B = 0
μ_output,B − μ_output,C = 0
μ_output,A − μ_output,C = 0
在进行多重测试时,我们需要应用 p 值校正以控制预期的错误率。回想一下,在 第三章“假设检验”中,我们定义了假设检验的预期错误率为 α。这是我们预期一个 单个 假设检验会导致 I 类 错误的比率。在我们的工厂机器示例中,我们进行了三个假设检验,这意味着 我们犯 I 类错误的概率是三倍。虽然我们的例子具体考虑了均值差异的多重测试,但这适用于任何类型的假设检验。在这些多重测试的情况下,我们通常会定义一个 家族错误率(FWER)并应用 p 值校正以控制 FWER。FWER 是从一组假设检验中犯 I 类 错误的概率。该组内测试的错误率是 个体错误率(IER)。我们将如下定义 IER 和 FWER:
-
IER: 个体假设检验的预期 I 类 错误率
-
FWER: 一组假设检验的预期 I 类 错误率
我们将在本节中讨论一种 p 值校正方法,以提供对推理的直观理解。
Bonferroni 校正
调整 p 值以控制多重假设检验的一种方法是 Bonferroni 校正。Bonferroni 校正通过均匀降低测试系列中每个单个测试的显著性水平来控制 FWER。鉴于我们在一个系列中有 m 个测试,每个测试的 p 值为 p_i,那么 p 值校正如下所示:
p_i ≤ α_m
以我们之前提到的三种机器模型为例,我们有一个包含三个测试的测试系列,使得 m = 3。如果我们让 FWER 为 0.05,那么,使用 Bonferroni 校正,三个单个测试的显著性水平如下:
0.05 * 3 = 0.0167
因此,在这个例子中,任何单个测试都需要有一个 p 值为 0.0167 才能被认为是显著的。
对 I 类和 II 类错误的影响
如前所述,Bonferroni 校正降低了单个测试的显著性水平,以控制家族级别的 I 类 错误率。我们还应该考虑这种变化如何影响 II 类 错误率。一般来说,降低单个测试的显著性水平会增加该测试犯 II 类 错误的可能性(正如 Bonferroni 校正所做的那样)。虽然我们只在本节中讨论了 Bonferroni 校正,但还有其他 p 值校正方法,它们提供了不同的权衡。请查看 statsmodels 中 multipletests 的文档,以查看 statsmodels 中实现的 p 值校正列表。
让我们通过一个例子来看看如何使用来自UCI 机器学习仓库的Auto MPG数据集的每加仑英里数(MPG)数据,该数据集的链接为:archive.ics.uci.edu/ml/datasets/Auto+MPG [1]。此数据集包含各种属性,包括origin、mpg、cylinders和displacement,涵盖了 1970 年至 1982 年间生产的车辆。我们在这里展示分析的简略形式;完整分析包含在本章代码仓库中的笔记本中。
对于这个例子,我们将使用mpg和origin变量,并在显著性水平 0.01 下测试不同来源的mpg是否存在差异。组均值如下表所示(在此数据集中,origin是一个整数编码标签)。
origin |
mpg |
|---|---|
| 1 | 20.0 |
| 2 | 27.9 |
| 3 | 30.5 |
图 4.15 – 每个来源组的车辆 MPG 均值
对每个均值进行 t 检验,我们得到以下 p 值:
| 零假设 | 未校正的 p 值 |
|---|---|
| μ1 − μ2 = 0 | 7.946116336281346e-12 |
| μ1 − μ3 = 0 | 4.608511957238898e-19 |
| μ2 − μ3 = 0 | 0.0420926104552266 |
图 4.16 – 每组之间 t 检验的未校正 p 值
对 p 值应用 Bonferroni 校正,我们得到以下 p 值:
| 零假设 | 校正后的 p 值(Bonferroni) |
|---|---|
| μ1 − μ2 = 0 | 2.38383490e-11 |
| μ1 − μ3 = 0 | 1.38255359e-18 |
| μ2 − μ3 = 0 | 0.126277831 |
图 4.17 – 每组之间 t 检验的校正 p 值
在先前的 p 值基础上,在显著性水平 0.01 下,拒绝组 1 和组 2 均值差异以及组 1 和组 3 均值差异的零假设,但未能拒绝组 2 和组 3 均值差异的零假设。
其他 p 值校正方法
在本节中,我们只讨论了一种 p 值校正方法——Bonferroni 校正,以提供对 p 值校正理由的直观理解。然而,还有其他校正方法可能更适合你的问题。要查看statsmodels中实现的 p 值校正方法列表,请检查statsmodels.stats.multitest.multipletests的文档。
方差分析
在上一节关于多重显著性检验中,我们看到了如何执行多重检验以确定组间均值是否存在差异。当处理组均值时,一个有用的第一步是进行方差分析。ANOVA 是一种用于确定多个组均值之间是否存在差异的统计检验。零假设是没有均值差异,备择假设是均值不全相等。由于 ANOVA 检验均值差异,它通常在执行成对假设检验之前使用。如果 ANOVA 的零假设未能被拒绝,则不需要执行成对检验。然而,如果 ANOVA 的零假设被拒绝,则可以进行成对检验以确定哪些特定的均值存在差异。
ANOVA 与成对检验的比较
虽然成对检验是检验组间差异的一般程序,但 ANOVA 只能用于检验均值差异。
在这个例子中,我们将再次考虑来自 Auto MPG 数据集的车辆 MPG。由于我们已运行成对检验并发现基于来源的车辆平均 mpg 存在显著差异,我们预计 ANOVA 将提供积极的检验结果(拒绝零假设)。执行 ANOVA 计算后,我们得到以下输出。小的 p 值表明我们应该拒绝零假设:
anova = anova_oneway(data.mpg, data.origin, use_var='equal')
print(anova)
# statistic = 98.54179491075868
# pvalue = 1.915486418412936e-35
这里展示的方差分析(ANOVA)是简化的。要查看完整的代码,请参阅本章代码仓库中相关的笔记本。
在本节中,我们介绍了对多于两组数据进行假设检验的方法。第一种方法是带有 p 值校正的成对检验,这是一种通用方法,可用于任何类型的假设检验。我们介绍的另一种方法是 ANOVA,这是一种用于检验组均值差异的特定检验。这不是一种像成对检验那样的通用方法,但可以用作在执行均值差异的成对检验之前的一个步骤。在下一节中,我们将介绍另一种可以用来确定两组数据是否相关的参数检验类型。
皮尔逊相关系数
皮尔逊相关系数,也称为皮尔逊 r(当应用于总体数据时称为皮尔逊 rho (ρ))或皮尔逊积矩样本相关系数(PPMCC),是一种双变量检验,用于衡量两个变量之间的线性相关性。该系数产生一个介于-1 到 1 之间的值,其中-1 表示强烈的负相关性,1 表示强烈的正相关性。零值的系数表示两个变量之间没有相关性。弱相关性通常被认为是介于 +/- 0.1 和 +/- 0.3 之间,中等相关性是介于 +/- 0.3 和 +/- 0.5 之间,强相关性是介于 +/- 0.5 到 +/- 1.0 之间。
这个测试被认为是参数测试,但不需要假设正态分布或方差齐性。然而,数据必须是独立采样的(既随机选择又无序列相关性),具有有限的方差——例如具有非常重尾部的分布——并且是连续数据类型。该测试不指示输入变量和响应变量;它只是两个变量之间线性关系的度量。该测试使用标准化协方差来推导相关性。回想一下,标准化需要将一个值除以标准差。
这里展示了总体皮尔逊系数ρ的方程:
ρ = σ xy _ σ x σ y
这里,σ xy 是总体协方差,计算如下:
σ xy = ∑ i=1 N (x i − μ x)(y i − μ y) ______________ N
这里展示了样本皮尔逊系数 r 的方程:
r = S xy _ S x S y
这里,S xy 是样本协方差,计算如下:
S xy = ∑ i=1 n (x i − _ x )(y i − _ y ) _____________ n − 1
在 Python 中,我们可以使用scipy的scipy.stats.pearsonr函数执行此测试。在下面的代码片段中,我们使用numpy生成两个正态分布的随机数数据集。我们想要测试两个组之间是否存在相关性,因为它们有一些显著的重叠:
from scipy.stats import pearsonr
import matplotlib.pyplot as plt
import scipy.stats as stats
import seaborn as sns
import pandas as pd
import numpy as np
mu1, sigma1 = 0, 1.1
normally_distributed_1 = np.random.normal(mu1, sigma1, 1000)
mu2, sigma2 = 0, 0.7
normally_distributed_2 = np.random.normal(mu2, sigma2,
1000)
df_norm = pd.DataFrame({'Distribution':['Distribution 1' for i in range(len(normally_distributed_1))] + ['Distribution 2' for i in range(len(normally_distributed_2))], 'X':np.concatenate([normally_distributed_1, normally_distributed_2])})
在图 4**.18中,我们可以观察到两个相关分布的重叠方差:

图 4.18 – 相关分布
在图4**.18中显示的图中,我们可以看到总体之间的重叠。现在,我们想要使用pearsonr()函数测试相关性,如下所示:
p, r = pearsonr(df_norm.loc[df_norm['Distribution'] == 'Distribution 1', 'X'], df_norm.loc[df_norm['Distribution'] == 'Distribution 2', 'X'])
print("p-value = %.4f"%p)
print("Correlation coefficient = %.4f"%r)
下面的输出表明,在 0.05 的显著性水平下,我们有一个 0.9327 的相关性水平(p 值为 0.0027):
p-value = 0.0027
相关系数 = 0.9327
为了以不同的方式表达相关性,我们可以说,在分布 2 中,由分布 1 解释的方差水平(r 2,也称为拟合优度或确定系数)为 0.9327 2 = 87%,假设我们知道分布 2 是对分布 1 的反应。否则,我们可以说两个变量之间存在 0.93 的相关性或 87%的方差解释水平。
现在,让我们看看 R 中的Motor Trend Car Road Tests数据集,我们使用statsmodels datasets.get_rdataset函数导入它。这里,我们有前五行,它们包含每加仑英里数(mpg)、气缸数(cyl)、发动机排量(disp)、马力(hp)、后轴齿轮比(drat)、重量(wt)、驾驶四分之一英里所需的最短时间(qsec)、发动机形状(vs=0表示 v 形,vs=1表示直列),变速器(am=0表示自动,am=1表示手动)、齿轮数(gear)和化油器数(carb)(如果不是喷射燃料):
import statsmodels.api as sm
df_cars = sm.datasets.get_rdataset("mtcars","datasets").data
在图 4.19 中,我们可以看到数据集的前五行,其中包含适合皮尔逊相关分析的数据。
| mpg | cyl | disp | hp | drat | wt | qsec | Vs | am | gear | carb |
|---|---|---|---|---|---|---|---|---|---|---|
| 21 | 6 | 160 | 110 | 3.9 | 2.62 | 16.46 | 0 | 1 | 4 | 4 |
| 21 | 6 | 160 | 110 | 3.9 | 2.875 | 17.02 | 0 | 1 | 4 | 4 |
| 22.8 | 4 | 108 | 93 | 3.85 | 2.32 | 18.61 | 1 | 1 | 4 | 1 |
| 21.4 | 6 | 258 | 110 | 3.08 | 3.215 | 19.44 | 1 | 0 | 3 | 1 |
| 18.7 | 8 | 360 | 175 | 3.15 | 3.44 | 17.02 | 0 | 0 | 3 | 2 |
图 4.19 – mtcars 数据集的前五行
使用数据集,我们可以使用以下代码绘制相关矩阵,该代码显示了数据集中所有特征的成对相关,以了解它们之间的关系:
sns.set_theme(style="white")
corr = df_cars.corr()
f, ax = plt.subplots(figsize=(15, 10))
cmap = sns.diverging_palette(250, 20, as_cmap=True)
sns.heatmap(corr, cmap=cmap, vmax=.4, center=0,
square=True, linewidths=.5, annot=True)
假设我们对于对四分之一英里时间(qsec)最有意义的变量感到好奇。在图 4**.20中,通过观察qsec的线条,我们可以看到vs(V 形)与 0.74 的正相关。由于这是一个二元变量,我们可以根据这个数据集假设,直列发动机比 V 形发动机更快。然而,还有其他与显著相关的协变量。例如,与发动机形状几乎同样强烈相关的是马力,即随着马力的增加,四分之一英里运行时间会下降:

图 4.20 – 相关矩阵热图
相关矩阵对于探索多个变量之间的关系非常有用。它也是构建统计和机器学习(ML)模型,如线性回归时进行特征选择的有用工具。
功率分析示例
功率分析是一种统计方法,用于确定假设检验所需的适当样本大小,以便在防止第二类错误(即当零假设应该被拒绝时未能拒绝零假设)时具有足够的功效。功率分析还可以根据样本大小确定可检测的效果量(或差异)之间的差异。换句话说,基于特定的样本大小和分布,功率分析可以为分析师提供研究人员可能能够可靠地识别的特定最小差异。在本节中,我们将通过单样本 t 检验演示功率分析。
单样本 t 检验
假设一家制造商销售一种每月能生产 100,000 个单位的机器,标准差为 2,800 个单位。一家公司购买了这些机器中的一批,并发现它们只能生产 90,000 个单位。该公司想知道需要多少台机器才能以高置信度确定这些机器无法生产 100,000 个单位。以下功率分析表明,对于 t 检验,需要三个机器的样本,以 85% 的概率防止未能识别出实际机器性能与市场宣传机器性能之间的统计显著差异:
from statsmodels.stats.power import TTestPower
import numpy as np
# Difference of distribution mean and the value to be assessed divided by the distribution standard deviation
effect_size = abs(100000-90000) / 2800
powersTT = TTestPower()
result = powersTT.solve_power(effect_size, nobs=3, alpha=0.05, alternative='two-sided')
print('Power based on sample size:{}'.format(round(result,2)))
# Power based on sample size: 0.85
额外的功率分析示例
要查看 Python 中功率分析的更多示例,请参阅本书的 GitHub 仓库。在那里,我们有额外的 t 检验和 F 检验的示例,这些示例专注于分析样本组之间的方差。
摘要
本章涵盖了参数检验的主题。从参数检验的假设开始,我们确定了测试这些假设被违反的方法,并讨论了在所需假设未满足时可以假设稳健性的场景。然后,我们研究了 z 检验的最流行替代方案之一,即 t 检验。我们通过多次应用此测试,包括使用合并、配对和 Welch 的非合并版本进行双样本分析的单样本和双样本版本的测试。接下来,我们探讨了方差分析技术,其中我们研究了如何使用来自多个组的数据来识别它们之间的统计显著差异。这包括当存在大量组时对 p 值的最流行调整之一——Bonferroni 校正,它有助于在执行多个测试时防止 I 类错误 的膨胀。然后,我们研究了使用 Pearson 相关系数对连续数据进行相关性分析,以及如何使用相关性矩阵和相应的热图来可视化相关性。最后,我们简要概述了功率分析,并举例说明了如何使用单样本 t 检验进行此分析。在下一章中,我们将讨论非参数假设检验,包括在本章中与参数检验配对的测试以及当假设不能安全假设时的新测试。
参考文献
[1] Dua, D. 和 Graff, C. (2019). UCI 机器学习仓库 [archive.ics.uci.edu/ml]. Irvine, CA: 加州大学信息与计算机科学学院。
第五章:非参数检验
在上一章中,我们讨论了参数检验。参数检验在满足测试假设时是有用的。然而,也存在一些情况,这些假设并不成立。在本章中,我们将讨论几种非参数检验,这些检验可以作为上一章中介绍的参数检验的替代方案。我们首先介绍非参数检验的概念。然后,我们将讨论几种在 t 检验或 z 检验假设不满足时可以使用的非参数检验。
在本章中,我们将涵盖以下主要主题:
-
当违反参数检验假设时
-
秩和检验
-
有符号秩检验
-
克鲁斯卡尔-沃利斯检验
-
卡方检验
-
斯皮尔曼相关分析
-
卡方功效分析
当违反参数检验假设时
在上一章中,我们讨论了参数检验。参数检验具有强大的统计功效,但也需要遵守严格的假设。当假设不满足时,测试结果无效。幸运的是,我们有替代测试可以在参数检验的假设不满足时使用。这些测试被称为非参数测试,这意味着它们对数据的潜在分布不做任何假设。虽然非参数测试不需要分布假设,但这些测试仍然要求样本是独立的。
排列检验
对于第一个非参数测试,让我们更深入地看看 p 值的定义。p 值是在零假设的假设下,获得至少与观察值一样极端的测试统计量的概率。然后,为了计算 p 值,我们需要零分布和观察到的统计量。p 值是具有比观察到的统计量更极端的测试统计量的样本比例。结果是我们可以使用数据排列来构建零分布。让我们看看如何使用以下数据集构建零分布。这个数据集可以代表低温和高温下的机器故障计数。我们假设样本是独立的:
low_temp = np.array([0, 0, 0, 0, 0, 1, 1])
high_temp = np.array([1, 2, 3, 1])
为了构建零假设,我们首先需要确定一个统计量。在这种情况下,我们将寻找两个分布的均值差异。因此,我们的统计量将如下所示:
_ x lowtemp − _ x hightemp
现在,为了计算分布值,计算数据集所有排列的统计量。这里有几个数据集排列的例子。
| 标签 | 观察值 | P1 | P2 | P3 | P4 | P5 | … |
|---|---|---|---|---|---|---|---|
| 低 | 0 | 1 | 0 | 1 | 0 | 1 | … |
| 低 | 0 | 1 | 3 | 0 | 3 | 0 | … |
| 低 | 0 | 0 | 0 | 1 | 1 | 3 | … |
| 低 | 0 | 1 | 1 | 0 | 2 | 0 | … |
| 低 | 0 | 0 | 1 | 2 | 0 | 1 | … |
| 低 | 1 | 1 | 1 | 3 | 0 | 1 | … |
| 低 | 1 | 0 | 1 | 1 | 1 | 0 | … |
| 高 | 1 | 0 | 2 | 0 | 1 | 2 | … |
| 高 | 2 | 3 | 0 | 0 | 0 | 1 | … |
| 高 | 3 | 0 | 0 | 1 | 1 | 0 | … |
| 高 | 1 | 2 | 0 | 0 | 0 | 0 | … |
| 平均差值 | -1.46 | -0.68 | 0.5 | 0.89 | 0.5 | 0.11 | … |
图 5.1 – 观察数据的头五个排列及平均差值
表格显示了带有五个随机生成的值排列的观察数据。我们计算每个排列的平均差值。平均差值是零分布的值。一旦我们有了分布,我们就可以计算 p 值,作为比观察值更极端的值的比例。
排列计算的缩放
通常,由于分布的大小与排列的数量迅速增长,排列检验的计算可能非常昂贵。例如,3、5 和 7 个样本大小的数据集对应于 6、120 和 5,040 个排列大小。这里所示的数据集有超过 3,900 万个排列!在大型数据集上进行排列检验的运行性能可能会因为计算零分布所需的排列数量而变得缓慢。
我们可以使用 scipy 中的 permutation_test 函数在 Python 中执行排列检验。此函数计算检验统计量、零分布和 p 值:
def statistic_function(set_one, set_two):
return np.mean(set_one) - np.mean(set_two)
random_gen=42
perm_result = sp.stats.permutation_test(
(low_temp, high_temp) ,
statistic_function, random_state=random_gen,
)
此函数从数据集中生成以下分布。

图 5.2 – 排列检验的零分布
排列检验的零分布和两组平均差值从 图 5.2 中显示。排列检验的 p 值为 0.036。排列检验是本节将要介绍的非参数检验中的第一个。再次强调,当不满足参数检验的假设时,这些类型的检验是有用的。然而,如果可以使用参数检验,则应使用它;非参数检验不应作为默认方法。
在本节中,我们介绍了使用排列检验的非参数检验。排列检验是一种广泛适用的非参数检验,但随着数据集大小的增加,排列的计算量会迅速增长,这可能在某些情况下使其使用变得不切实际。在接下来的几节中,我们将介绍其他几种不需要计算零分布的非参数检验。
排序和总和检验
当 t 检验的假设不成立时,Rank-Sum 检验通常是一个很好的非参数替代检验。虽然 t 检验可以用来检验两个分布的均值差异,但 Rank-Sum 检验用于检验两个分布的位置差异。这种检验效用上的差异是由于 Rank-Sum 检验中缺乏参数假设。Rank-Sum 检验的零假设是第一个样本的分布与第二个样本相同。如果样本分布看起来相似,这允许我们使用 Rank-Sum 检验来检验两个样本的位置差异。如前所述,Rank-Sum 检验不能专门用于检验均值差异,因为它不需要关于样本分布的假设。
检验统计量程序
检验程序很简单。过程在此概述,以下表格中展示了示例:
-
将所有样本值合并成一个集合,并按升序排序样本,同时记录它们的标签。
-
将排名分配给所有样本,从最低样本值开始,排名为 1。
-
当出现平局时,将平局值的排名替换为平局值的平均排名。
-
将最小样本组的排名求和,该样本组是检验统计量 T。
一旦计算出检验统计量,就可以计算 p 值。p 值可以通过正态近似法或精确方法来计算。通常,精确方法仅在样本量较小(小于 8 个样本)时使用。
正态近似
一旦计算出 Rank-Sum 检验的 T 统计量,我们就可以使用精确方法或正态近似法来确定 p 值。在这里,我们将介绍近似方法,因为精确方法需要使用排列检验,这将需要使用软件。我们将使用 z 分数来近似 p 值(回忆一下第三章**, 假设检验):
Z = T - Mean(T) * STDEV(T)
其中
Mean(T) = n * T * R
and
STDEV(T) = s * R / √(n * T * n * O + n * T + n * O)
在这里的方程中,n * T 是用于计算 T 的组中样本的数量,n * O 是另一组中的样本数量,_R 是修正排名的平均值,s * R 是修正排名的标准差。一旦计算出 Z,就可以在 z 分布中查找相应的 p 值。在描述了方法之后,让我们来看一个例子。以下表格中的数据可以在附带的 Jupyter 笔记本中找到。
Rank-Sum 示例
下表展示了在两组标记为“L”和“H”的数据集上执行此过程,将使用秩和检验来检验两组样本分布的位置差异。此表的检验统计量为 42.5,即组 L 校正秩的和。此检验统计量对应于约 0.00194 的近似 p 值(双尾检验)。此表的数据是从github.com/OpenIntroStat/openintro/raw/master/data/gpa_iq.rda下载的。
| IQ | Group | Rank | Corrected Rank |
|---|---|---|---|
| 77 | L | 1 | 1 |
| 79 | L | 2 | 2 |
| 93 | L | 3 | 3 |
| 96 | L | 4 | 4 |
| 104 | L | 5 | 5 |
| 105 | H | 6 | 6 |
| 106 | H | 7 | 7 |
| 107 | L | 8 | 8 |
| 109 | L | 9 | 9 |
| IQ | Group | Rank | Corrected Rank |
| 111 | H | 10 | 10.5 |
| 111 | L | 11 | 10.5 |
| 112 | H | 12 | 12 |
| 116 | H | 13 | 13 |
| 118 | H | 14 | 14 |
| 124 | H | 15 | 15 |
| 126 | H | 16 | 16 |
| 127 | H | 17 | 17 |
| 128 | H | 18 | 18.5 |
| 128 | H | 19 | 18.5 |
图 5.3 – 秩和检验的校正秩
我们也可以使用软件通过mannwhitenyu从scipy执行此测试。对于以下代码示例,前表中对应于 L 和 H 的值包含在lower_score_iqs和higher_score_iqs变量中,分别:
mannwhitneyu(higher_score_iqs, lower_score_iqs).pvalue
# 0.00222925216588146
在本节中,我们讨论了秩和检验,它是一种非参数的 t 检验替代方法。秩和检验用于检验两组样本数据的分布位置是否存在差异。在下一节中,我们将探讨一种类似的基于秩的检验,该检验用于比较配对数据,例如来自第四章**,参数检验*中的配对 t 检验。
符号秩检验
Wilcoxon 符号秩检验是非参数配对 t 检验的替代版本,当违反正态性假设时使用。由于在零假设和备择假设中使用秩和中位数而不是均值,因此该检验对异常值具有鲁棒性。正如检验的名称所示,它使用两个阶段之间差异的幅度及其符号。
在研究中,零假设考虑阶段 1 和阶段 2 之间的中位数差异为零。同样,在配对 t 检验中,对于备择假设,对于双尾检验,阶段 1 和阶段 2 之间的中位数差异被认为不是零,或者对于单尾检验,阶段 1 和阶段 2 之间的中位数差异大于(或小于)零。
虽然正态性要求放宽,但该检验要求配对观测值之间相互独立,并且这些观测值来自同一总体。此外,要求因变量是连续的。
为了计算检验统计量,有以下步骤:
-
计算两个阶段之间每对的差异。
-
如果存在,则删除差值为零的配对。
-
计算每对之间的绝对差值,并按从小到大的顺序对它们进行排序。
-
通过对具有正号的秩求和来计算符号秩统计量 S。
让我们考虑以下简单示例。我们考虑两个阶段(治疗前后)的九个样本的通用数据。
| 配对 | 治疗前的 | 治疗后的 | 绝对差值 | 符号 | 秩 |
|---|---|---|---|---|---|
| 1 | 37 | 38 | 1 | - | 1 |
| 2 | 14 | 17 | 3 | - | 2.5 |
| 3 | 22 | 19 | 3 | + | 2.5 |
| 4 | 12 | 7 | 5 | + | 4 |
| 5 | 24 | 15 | 9 | + | 5 |
| 6 | 35 | 25 | 10 | + | 6 |
| 7 | 35 | 24 | 11 | + | 7 |
| 8 | 51 | 38 | 13 | + | 8 |
| 9 | 39 | 19 | 20 | + | 9 |
图 5.4 – 符号秩检验的秩计算演示
通过观察前面的表格,我们可以看到第二对和第三对具有相同的绝对差值。因此,它们的秩通过平均原始秩来计算,(2+3)/2 = 2.5. 单侧检验的零假设和备择假设如下:
H 0 : 治疗前后中位数差异为零
H a : 治疗前后中位数差异为正
符号秩统计量是正差分的秩之和,如下所示:
S = 2.5 + 4 + 5 + 6 + 7 + 8 + 9 = 41.5.
S 的均值为:
Mean(S) = n(n + 1) _ 4 = 9.10 _ 4 = 22.5
S 的标准差为:
SD(S) = √ ____________ n(n + 1)(2n + 1) ____________ 24 = √ _ 9 * 10 * 19 _ 24 = 8.44.
然后,通过以下公式计算检验统计量:
Z 统计量 = S − Mean(S) _ SD(S) = 41.5 − 22.5 _ 8.44 = 2.2511.
参考第三章**,假设检验,我们可以使用 scipy.stats.norm.sf() 通过 Z 统计量计算近似单侧 p-value 0.012。
在 α = 0.05 – 显著性水平 – p-value < α 的条件下,我们拒绝零假设。有强有力的证据表明,治疗前的中位数大于治疗后的中位数。在 Python 中,实现该测试很简单,如下所示:
import scipy.stats as stats
import numpy as np
before_treatment = np.array([37, 14, 22, 12, 24, 35, 35, 51, 39])
after_treatment = np.array([38,17, 19, 7, 15, 25, 24, 38, 19])
# Signed Rank Test
stats.wilcoxon(before_treatment, after_treatment, alternative = 'greater')
该测试的文档可以在以下链接找到:
docs.scipy.org/doc/scipy/reference/generated/scipy.stats.wilcoxon.xhtml
Kruskal-Wallis 检验
另一种我们现在将讨论的非参数检验是 Kruskal-Wallis 检验。当不满足正态性假设时,它是单因素方差分析测试的替代方案。它使用中位数而不是均值来检验两个或更多独立组之间是否存在统计上显著差异。让我们考虑三个独立组的通用示例:
group1 = [8, 13, 13, 15, 12, 10, 6, 15, 13, 9]
group2 = [16, 17, 14, 14, 15, 12, 9, 12, 11, 9]
group3 = [7, 8, 9, 9, 4, 15, 13, 9, 11, 9]
如下所述,陈述了零假设和备择假设。
H 0 : 这三个组的中位数相等
H a : 这三个组的中位数不相等
在 Python 中,通过使用scipy.stats.kruskal函数很容易实现。文档可以在以下链接中找到:
docs.scipy.org/doc/scipy/reference/generated/scipy.stats.kruskal.xhtml
from scipy import stats
group1 = [8, 13, 13, 15, 12, 10, 6, 15, 13, 9]
group2 = [16, 17, 14, 14, 15, 12, 9, 12, 11, 9]
group3 = [7, 8, 9, 9, 4, 15, 13, 9, 11, 9]
#Kruskal-Wallis Test
stats.kruskal(group1, group2, group3)
上述代码的输出如下:
KruskalResult(statistic=5.7342701722574905, pvalue=0.056861597028239855)
由于α = 0.05 – 显著性水平 – 且p-value > α,我们未能拒绝零假设。没有强有力的证据表明这三个组的中位数不相等。
卡方分布
研究人员经常面临需要对分类数据进行假设检验的需求。在第四章**,参数检验中涵盖的参数检验通常对这类分析帮助不大。在上一章中,我们讨论了使用 F 检验来比较样本方差。扩展这个概念,我们可以考虑非参数和非对称的卡方概率分布,这是一种有用的分布,用于比较样本方差抽样分布的均值与总体方差,特别是在假设检验下,预期样本方差抽样分布的均值等于总体方差的情况下。因为方差不能为负,所以分布从 0 开始。在这里,我们可以看到卡方分布:

图 5.5 – 具有七个自由度的卡方分布
卡方分布的形状不代表百分位数固定在标准差上,就像标准正态分布那样;它预期会随着每个额外样本方差的计算而变化。当可以假设用于计算方差抽样分布的原始总体数据是正态分布时,卡方标准化检验统计量计算如下:
χ² = (n - 1) s² / σ²
其中(n-1)用于计算自由度,这些自由度用于解释在构建统计量时样本误差。临界值基于使用自由度和所需显著性水平的表格查找。零假设始终是 H0: σχ² = σ0²,其中σχ²是观察到的分布方差,σ0²是期望方差。对于单尾检验的备择假设是,当测试右尾时 H1: σχ² ≥ σ0²,当测试左尾时 H1: σχ² ≤ σ0²。对于双尾检验的备择假设是 H1: σχ² ≠ σ0²。当总体数据可以假设为正态分布时,这个测试与 F 测试相似,因为两个方差正在被比较,当两者都相同时结果为 1。区别在于,卡方检验统计量考虑了自由度,因此对差异的敏感性较低。
理解这个测试如何用于比较方差对于理解卡方分布如何与分类数据的出现频率相关是有帮助的。在前面的卡方统计量中,我们可以将样本统计量作为观察值,而方差的总参数是我们期望发生的。在确定分类因素水平出现的统计显著性时,我们可以比较观察到的发生次数与期望的发生次数。在接下来的几节中,我们将举例说明这个测试的两个最广泛使用的版本,即卡方拟合优度检验和卡方独立性检验。这些测试被认为是非参数的,因为它们不需要在第四章**,参数检验*中所述的假设。
卡方拟合优度和独立性检验的尾性
在接下来的两节中,卡方拟合优度检验和卡方独立性检验总是右尾检验。这些测试中的零假设陈述观察频率和期望频率之间的差异为零。备择假设是观察频率和期望频率不相同。χ²检验统计量越接近 0,观察频率和期望频率越可能接近。
卡方拟合优度
卡方拟合优度检验比较单个变量(因素)多个因素水平的出现次数,以确定这些水平是否在统计学上相等。例如,一个供应商向客户提供三种手机型号——单个因素(手机)的三个水平(品牌)——客户每周总共购买平均 90 台手机。我们可以这样说,预期的频率是 1/3——因此,平均每周每种型号销售 30 台手机。皮尔逊卡方检验统计量,通过测量观察到的频率与预期频率之间的差异来计算,是卡方拟合优度检验中使用的检验统计量。此检验统计量的线性方程如下:
χ 2 = ∑ (O i − E i) 2 _ E i , 自由度 = k-1
其中 O i 是观察到的频率,E i 是预期的频率,k是因素水平的数量。以我们的手机为例,我们从供应商那里了解到预期的频率 1/3 实际上并不是观察到的;供应商平均销售 45、30 和 15 台 A、B 和 C 型号的手机。假设我们想知道观察到的频率是否与预期的频率有统计学上的显著差异。零假设是频率相等。我们以下列方式制定皮尔逊卡方检验统计量:
χ2 = (45 − 30) 2 _ 30 + (30 − 30) 2 _ 30 + (15 − 30) 2 _ 30 = 15
假设我们想使用显著性水平为 0.05 的假设检验(右侧尾),从而形成观察到的频率和预期的频率不相等的备择假设。卡方临界值表显示,对于显著性水平为 0.05,df : 3 − 1 = 2 个自由度,临界值是 5.9915。因为 15 > 5.9915,我们可以根据临界值测试得出拒绝零假设的结论。
在 Python 中执行此操作,我们可以使用statsmodels.stats.gof模块的chisquare函数。每个组体积需要传递到一个列表或数组中,包括观察到的频率和预期的频率。statsmodels的chisquare测试将自动根据观察到的(f_obs)列表中的值计数来计算 k-1 个自由度,但必须为scipy chi2.ppf函数提供自由度,该函数提供临界值,从而避免了手动查找表格的需要:
from statsmodels.stats.gof import chisquare
from scipy.stats import chi2
chi_square_stat, p_value = chisquare(f_obs=[45, 30, 15],
f_exp=[30, 30, 30])
chi_square_critical_value = chi2.ppf(1-.05, df=2)
print('Chi-Square Test Statistic: %.4f'%chi_square_stat)
print('Chi-Square Critical Value: %.4f'%chi_square_critical_value)
print('P-Value: %.4f'%p_value)
如前所述,由于 p 值低于 0.05 的显著性水平,并且检验统计量大于临界值,我们可以假设我们有足够的证据来拒绝零假设,并得出统计显著性结论,表明手机型号不是以相等的数量购买的:
Chi-Square Test Statistic: 15.0000
Chi-Square Critical Value: 5.9915
P-Value: 0.0006
然后,我们可以重新运行该测试,比较两个频率,例如 45 与 30 或 45 与 15,期望频率分别为 37.5 和 37.5 或 30 和 30,以确定哪部电话可能是销售不平衡的最大影响因素。
独立性卡方检验
假设我们有一个德克萨斯州 2021 年观察到的车辆事故数据集,Restraint Use by Injury Severity and Seat Position (www.txdot.gov/data-maps/crash-reports-records/motor-vehicle-crash-statistics.xhtml),并想知道使用安全带是否导致了死亡率在统计学上的显著差异。我们有以下观察值表:
| 受限制 | 不受限制 | 总计 | |
|---|---|---|---|
| 致命 | 1,429 | 1,235 | 2,664 |
| 非致命 | 1,216,934 | 22,663 | 1,239,597 |
| 总计 | 1,218,363 | 23,898 | 1,242,261 |
图 5.6 – 独立性卡方检验“观察”表
让我们现在使用此方程创建一个期望值表:
E_ij = T_i * T_j / N
其中 T_i 是第 i 行的总数,T_j 是第 j 列的总数,N 是观察总数。这得出以下结果:
| 受限制 | 不受限制 | 总计 | |
|---|---|---|---|
| 致命 | (2,664 *1,218,363)/1,242,261 = 2,612.75 | (2,664 *23,898)/1,242,261 = 51.25 | 2,664 |
| 非致命 | (1,216,934* 1,239,597) / 1,242,261 = 1,214,324.31 | (1,239,597 * 23,898) / 1,242,261 = 23,846.75 | 1,239,597 |
| 总计 | 1,218,363 | 23,898 | 1,242,261 |
图 5.7 – 独立性卡方检验“期望”表
在拟合优度检验中,我们使用的修改版皮尔逊卡方检验统计量,扩展到独立性卡方检验,如下所示:
χ² = ∑ (O_ij − E_ij)² / E_ij,自由度 = (r-1)(c-1)
下标 j 对应于列数据,下标 i 对应于观察值和期望值的行数据,分别表示为 O 和 E。在自由度计算中的 r 和 c 值对应于表格中的行数和列数(这是 2x2,因此自由度将等于 (2-1)(2-1)=1。
使用观察值和期望值表中的值,我们找到卡方检验统计量等于以下值:
χ² = (1,429 − 2,612.75)² / 2,612.75 + (1,216,934 − 1,214,324.31)² / 1,214,324.31 + (1,235 − 51.25)² / 51.25 +
(22,663 − 23,846.75)² / 23,846.75 = 27,942.43
使用此处计算的 1 个自由度计算出的查表检验统计量为 3.84。我们可以合理地得出结论,因为检验统计量 27,942.43 大于临界值 3.84,因此我们可以得出结论,在 2021 年德克萨斯州,使用安全带与未使用安全带相比,在车辆事故中的死亡率存在统计学上的显著差异。
卡方列联表
我们进行的测试使用了两个 2x2 的列联表。卡方独立性检验通常被称为卡方列联检验。拒绝零假设的失败取决于观察到的表格值在统计置信水平内与预期表格值的匹配。然而,卡方独立性检验可以扩展到任何行和列组合的表格。然而,随着行和列值的增加,表格可能对解释不那么有用。
要在 Python 中执行此测试,我们可以使用来自scipy stats模块的chi2_contingency函数。仅输入作为 2x2 numpy数组的观察频率,chi2_contingency测试提供如果零假设为真时的预期频率——p 值、使用的自由度以及卡方检验统计量:
from scipy.stats import chi2_contingency
from scipy.stats import chi2
import numpy as np
observed_frequencies = np.array([[1429, 1235], [1216934, 22663]])
chi_Square_test_statistic, p_value, degrees_of_freedom, expected_frequencies = chi2_contingency(observed_frequencies)
chi_square_critical_value = chi2.ppf(1-.05, df=degrees_of_freedom)
print('Chi-Square Test Statistic: %.4f'%chi_Square_test_statistic)
print('Chi-Square Critical Value: %.4f'%chi_square_critical_value)
print('P-Value: %.4f'%p_value)
在这里,我们可以看到卡方检验统计量在 0.05 的显著性水平上远大于卡方临界值,p 值在 p < 0.0000 时显著。因此,我们可以得出结论,使用安全带与不使用安全带相比,在汽车碰撞死亡人数方面存在很大的统计学差异:
卡方检验 统计量:27915.1221
卡方临界值:3.8415
P 值:0.0000
Yates 连续性校正
可以选择性地向chi2_contingency测试添加一个额外的参数,即带有correction =参数和一个布尔值(True或False)的输入。这应用了 Yates 连续性校正,根据scipy.stats文档,将每个观察值调整 0.5 个单位,以接近相应的预期值。调整的目的在于避免由于预期样本量小而错误地检测到统计显著性。主观上,这对于预期频率单元格中的样本量少于 10 的情况是有用的。然而,许多实践者和研究人员反对使用它。
卡方拟合优度检验功效分析
让我们用一个例子来说明,一个手机销售商销售四种流行的手机型号,型号 A、B、C 和 D。我们想要确定需要多少样本才能产生 0.8 的功效,以便我们可以了解不同手机流行度之间是否存在统计学上的显著差异,这样销售商就可以更恰当地投资于手机采购。在这种情况下,零假设断言每个型号的手机中有 25%被售出。实际上,售出的手机中有 20%是型号 A,30%是型号 B,19%是型号 C,31%是型号 D。
测试nobs(观测数)参数的不同值,我们发现至少需要 224 个样本才能产生略大于 0.801 的效力。增加更多样本只会提高这一点。如果真实分布与假设的 25%的均等分割差异更大,则可能需要更少的样本。然而,由于分割相对接近 25%,需要大量的样本:
from statsmodels.stats.power import GofChisquarePower
from statsmodels.stats.gof import chisquare_effectsize
# probs0 asserts 25% of each brand are sold
# In reality, 12% of Brand A, 25% of Brand B, 33% sold were Brand C, and 1% were Brand D.
effect_size = chisquare_effectsize(probs0=[25, 25, 25, 25], probs1=[20, 30, 19, 31], cohen=True)
alpha = 0.05
n_bins=4 # 4 brands of phones
analysis = GofChisquarePower()
result = analysis.solve_power(effect_size, nobs=224, alpha=alpha, n_bins=n_bins)
print('Sample Size Required in Sample 1: {:.3f}'.format(
result))
# Sample Size Required in Sample 1: 0.801
斯皮尔曼等级相关系数
在第四章**,参数检验中,我们研究了参数相关性系数,皮尔逊相关系数,其中系数是从独立采样的连续数据中计算得出的。然而,当我们有排名的有序数据,例如满意度调查中的数据时,我们不想使用皮尔逊相关系数,因为它不能保证保持顺序。与皮尔逊相关系数一样,斯皮尔曼相关系数也产生一个系数,r,其范围从-1 到 1,-1 表示强烈的负相关,1 表示强烈的正相关。斯皮尔曼相关系数是通过将两个变量等级的协方差除以它们标准差的乘积得到的。相关性系数r的方程如下:
r s = S xy _ √ _ S xx S yy
其中
S xy = ∑ (x i − _ x )(y i − _ y )
S xx = ∑ (x i − _ x )²
S yy = ∑ (y i − _ y )²
在前面的相关性方程中,在所有排名场景下都是安全的。然而,当没有平局或与总体样本量相比平局比例极低时,可以使用以下方程:
r s = 6∑ d i² _ n(n² − 1),其中 d i = (x i − y i)
然而,我们将使用 Python 进行此测试。因此,无论公式多么复杂、错误率多么低,都将应用该公式,因为计算能力使我们能够完全绕过手动过程。
假设我们在比赛中由两位评委对学生进行评判,并且担心其中一位评委可能基于混淆因素(如家庭关系)而不是仅基于表现对某些参赛者存在偏见。我们决定对分数进行相关性分析,以检验两位评委对每个参赛者的评分相似的假设:
from scipy.stats import spearmanr
import pandas as pd
df_scores = pd.DataFrame({'Judge A':[1, 3, 5, 7, 8, 3, 9],
'Judge B':[2, 5, 3, 9, 6, 1, 7]})
我们有以下参赛者的表格:
| 学生 1 | 学生 2 | 学生 3 | 学生 4 | 学生 5 | 学生 6 | 学生 7 | |
|---|---|---|---|---|---|---|---|
| 判定者 A | 1 | 3 | 5 | 7 | 8 | 3 | 9 |
| 判定者 B | 2 | 5 | 3 | 9 | 6 | 1 | 7 |
图 5.8 – 斯皮尔曼相关性分析中评委对学生参赛者的排名
correlation, p_value = spearmanr(df_scores['Judge A'],
df_scores['Judge B'])
print('Spearman Correlation Coefficient: %.4f'%correlation)
print('P-Value: %.4f'%p_value)
根据这里的 p 值为 0.04,小于 0.05 的显著性水平,我们可以说产生的相关性在随机程度之外是显著的。如果 p 值大于 0.05,我们可以以 95%的置信水平说,相关性可能是完全虚假的,可能没有足够的统计证据来支持确定性。Spearman 的相关系数(r)为 0.7748。因此,r_squared大约为 0.6003,这意味着大约 60.3%的分数变化是由评委的评分决定的:
Spearman 相关系数:0.7748
P-值:0.0408
由于 p 值显著且相关系数为 0.77 – 而一个强相关系数大约从 0.7 开始 – 我们可以得出结论,评委的评分与直接相关性足够强,可以假设在存在相对客观的排名方法的情况下,评分中没有偏见;更主观的方法可能不适合相关性分析。
摘要
在本章中,我们讨论了在无法谨慎保证参数假设测试所需假设时,最常用的非参数假设检验。我们讨论了双样本 Wilcoxon 秩和检验(也称为 Mann-Whitney U 检验),当无法进行双样本 t 检验时,用于从中位数中得出推论。接下来,我们介绍了当无法进行配对 t 检验比较均值时,Wilcoxon 符号秩检验的中位数配对比较。之后,我们研究了非参数卡方拟合优度检验和卡方独立性检验,用于比较观察频率与期望频率,两者都用于识别分类数据计数中存在统计显著差异的情况。此外,我们还讨论了 Kruskal-Wallis 检验,它是方差分析(ANOVA)的非参数替代方法。最后,我们讨论了 Spearman 相关系数以及如何在参数假设无法安全支持使用 Pearson 相关系数时,根据排名推导相关性。在本章结束时,我们提供了一个使用卡方检验的功率分析示例。
在下一章中,我们将开始讨论预测分析,从简单的线性回归开始。在那里,我们将讨论拟合模型的方法、解释线性系数、理解线性回归所需的假设、评估模型性能以及考虑线性回归的修改版本。
第二部分:回归模型
在本部分,我们讨论了可以使用回归、相关系数和确定系数、多元建模、模型选择和正则化变量调整解决的问题。
它包括以下章节:
-
第六章,简单线性回归
-
第七章,多元线性回归
第六章:简单线性回归
在前面的章节中,我们研究了单变量分布。现在我们将讨论变量之间的关系。在本章和下一章中,我们将使用线性回归来研究两个或更多变量之间的关系。在本章中,我们将讨论在 statsmodels 框架内的简单线性回归,最后我们将讨论序列相关性和模型验证的情况。正如所强调的,本章的主要主题遵循以下框架:
-
使用 OLS 的简单线性回归
-
相关系数和确定系数
-
必要的模型假设
-
显著性检验
-
处理模型误差
-
验证模型
使用 OLS 的简单线性回归
我们将研究最简单的机器学习模型之一——简单线性回归。我们将在 OLS 的背景下提供其概述,其目标是使误差平方和最小化。这是一个与因变量(定量响应)y 及其自变量 x 相关的简单概念,它们之间的关系可以近似地表示为一条直线。数学上,简单线性回归模型可以写成以下形式:
y = β 0 + β 1 x + ϵ
在这里,β 0 是截距项,β 1 是线性模型的斜率。误差项在先前的线性模型中表示为 ϵ。我们可以看到,在误差项为零的理想情况下,β 0 代表当 x = 0 时因变量 y 的值。在自变量 x 的范围内,β 1 代表 x 单位变化引起的 y 的结果增加。在文献中,自变量 x 可以称为解释变量、预测变量、输入或特征,而因变量 y 可以称为响应变量、输出或目标。问题是,“如果我们有一个来自数据集的样本(x i, y i),其中 i = 1,2, … , n 个点),我们如何通过截距项和斜率确定最佳拟合线?”通过估计这些项,我们将拟合最佳线通过数据,以显示自变量和因变量之间的线性关系。

图 6.1 – x 和 y 之间的关系 – 最佳拟合线
在前面的图表示例中,蓝色点表示实际值,代表自变量 x 和因变量 y 之间的关系。红色线是通过所有这些数据点的最佳拟合线。我们的目标是现在制定预测线(最佳拟合线):
ˆ y = ˆ β 0 + ˆ β 1x
仔细观察预测值和真实值之间的关系。为了找到最佳拟合线,我们将最小化垂直距离,这意味着我们将最小化点与数据线之间的误差。换句话说,我们将最小化平方误差之和,它由以下计算得出:
Σ(i=1 to n) eᵢ²

图 6.2 – 数据点与数据线之间的误差
这里,
eᵢ = yᵢ − ˆyᵢ,
其中 yᵢ 是第 i 个观测的响应变量 y 的观测值,ˆyᵢ 是预测值。误差平方和由以下给出:
S = Σ(i=1 to n) (yᵢ − ˆβ₀ − ˆβ₁ xᵢ)²
为了最小化 S,我们将对 ˆβ₀ 和 ˆβ₁ 求偏导数。然后,我们看到:
∂ S / ∂ ˆβ₀ = − 2Σ(yᵢ − ˆβ₀ − ˆβ₁ xᵢ) = 0,
∂ S / ∂ ˆβ₁ = − 2Σ xᵢ(yᵢ − ˆβ₀ − ˆβ₁ xᵢ) = 0.
因此,很容易看出:
nˆβ₀ + (Σ(i=1 to n) xᵢ)ˆβ₁ = Σ(i=1 to n) xᵢ yᵢ,
(Σ(i=1 to n) xᵢ)ˆβ₀ + (Σ(i=1 to n) xᵢ²)ˆβ₁ = Σ(i=1 to n) xᵢ yᵢ,
这意味着:
ˆβ₀ = Σ(i=1 to n) yᵢ / n − ˆβ₁ / Σ(i=1 to n) xᵢ / n,
Σ(i=1 to n) xᵢ Σ(i=1 to n) yᵢ / n − (Σ(i=1 to n) xᵢ)² / n ˆβ₁ + (Σ(i=1 to n) xᵢ²)ˆβ₁ = Σ(i=1 to n) xᵢ yᵢ.
因为 ȳ = Σ(i=1 to n) yᵢ / n 和 x̄ = Σ(i=1 to n) xᵢ / n,我们可以将斜率和截距重写如下:
ˆβ₀ = ȳ − ˆβ₁ / x̄,
ˆβ₁ = Σ(xᵢ − x̄)(yᵢ − ȳ) / √(Σ(xᵢ − x̄)²).
在 Python 中,我们可以通过以下代码来找到 ˆβ₀ 和 ˆβ₁:
def least_squares_method(x,y):
x_mean=x.mean()
y_mean=y.mean()
beta1 = ((x-x_mean)*(y-y_mean)).sum(axis=0)/ ((x-x.mean())**2).sum(axis=0)
beta0 = y_mean-(beta1*x_mean)
return beta0, beta1
相关系数和确定系数
在本节中,我们将讨论两个相关的概念——相关系数和确定系数。
相关系数
相关系数是衡量两个变量之间统计线性关系的指标,可以使用以下公式计算:
r = 1 / (n - 1) Σ(i=1 to n) (xᵢ − x̄) / sₓ (yᵢ − ȳ) / sᵧ
读者可以在此处shiny.rit.albany.edu/stat/corrsim/模拟两个变量之间的相关关系。

图 6.3 – 模拟的双变量分布
通过观察散点图,我们可以看到两个变量及其异常值之间线性关系的方向和强度。如果方向是正的(r>0),那么两个变量同时增加或减少。

图 6.4 – 模拟的双变量分布
如果方向是负的,那么一个变量增加而另一个变量减少。我们将在下面的散点图中说明这一点:

图 6.5 – 模拟的双变量分布
相关系数存在于-1 和 1 之间的值。当 r = 1 时,我们有一个完美的正相关,而当 r = -1 时,我们有一个完美的负相关。注意,如果,例如,变量 x 和变量 y 互换,或者任一变量的所有值都进行线性缩放,相关系数的值不会改变。一般来说,相关性并不表示因果关系。
决定系数
决定系数就是 r²,其中 r 是相关系数,它是变量 y 的变异中由变量 x 解释的比例。r²的值在 0 和 1 之间,两个变量之间的线性关系随着其值的接近 1 而变得更强。r²的值也可以使用以下公式计算:
r² = 1 − SSRes / SSTot
在这里,SSRes 是误差平方和,SSTot 是实际值与平均值之间差异的平方的总和。在statsmodels中,可以从 OLS 回归结果的输出中获得决定系数的值。这个输出将在本章的下一节中讨论。
必要的模型假设
与我们在第四章**参数测试中讨论的参数测试一样,线性回归是一种参数方法,并且需要满足某些假设才能使结果有效。对于线性回归,有四个假设:
-
变量之间的线性关系
-
残差的正态性
-
残差的同方差性
-
独立样本
让我们分别讨论这些假设。
变量之间的线性关系
当考虑将线性模型拟合到数据时,我们首先应该考虑模型是否适合数据。当处理两个变量时,应该使用散点图来评估变量之间的关系。让我们看一个例子。图6**.6中显示了三个散点图。数据被绘制,并且用于生成数据的实际函数被绘制在数据点上。最左边的图显示了表现出线性关系的数据。中间的图显示了表现出二次关系的数据。最右边的图显示了两个不相关的变量。

图 6.6 – 描述两个变量之间三种不同关系的三个散点图
在图 6**.6的示例数据中,线性模型仅适用于最左边的数据。在这个例子中,我们有了解两个变量实际关系的优势,但在实际问题中,你必须确定线性模型是否合适。一旦我们评估了线性模型是否合适并拟合了线性模型,我们就可以评估依赖于模型残差的两个假设。
残差的正态性
在前两章中,我们详细讨论了正态性,并查看了一些正态分布的数据示例和一些非正态分布的数据示例。在这种情况下,我们进行相同的评估,但这是针对模型残差而不是变量。实际上,与之前的参数方法不同,变量本身不需要正态分布,只需残差。让我们来看一个例子。我们将对两个变量拟合一个线性模型,每个变量都表现出偏斜的分布。如图6.7所示,尽管每个变量都有偏斜的分布,但变量之间是线性相关的。

图 6.7 – X, Y 及其散点图的分布
现在,让我们看看模型拟合的残差。模型拟合的残差显示在图 6.8中。正如我们所预期的那样,这个模型拟合的残差看起来是正态分布的。这个模型似乎满足正态分布残差的假设。

图 6.8 – 模型拟合的残差
在验证了正态分布残差的假设后,让我们继续到残差同方差性的假设。
残差的同方差性
不仅残差应该是正态分布的,而且残差在线性模型中应该具有恒定的方差。同方差性指的是残差具有相同的分散度。相反,表现出相同分散度的残差被称为异方差性。同方差性通常通过散点图来评估残差与模型预测的关系。残差应该看起来是随机分布的,均值为 0,并且在x轴上均匀分散。图 6.9显示了模型残差与预测值Y的散点图。这些残差看起来表现出同方差性。

图 6.9 – 模型残差与预测值的散点图
存在几种常见的残差同方差性可能被违反的方式:
-
残差显示出方差系统性的变化
-
存在一个极端的异常值
图 6.10展示了这些违反的两种示例。左图显示了一个极端的异常值,右图显示了残差中的非恒定方差。在这两种情况下,模型假设都被违反了。

图 6.10 – 表现出不良行为的示例残差
当残差显示出系统性模式时,这可能表明另一种类型的模型可能更合适,或者对其中一个或两个变量应用转换可能有所帮助。当存在极端异常值时,可能值得验证和调查该数据点,以确保它应该包含在分析中。在下一节中,我们将讨论极端异常值如何影响模型拟合。
样本独立性
我们在之前的章节中讨论了样本独立性。没有图形或统计方法可以用来确定样本是否独立。评估样本独立性需要对抽样方法和样本来源的总体进行仔细分析。例如,违反独立性假设的常见数据类型是时间序列数据。时间序列数据是一种随时间采样的数据类型,使其具有序列相关性。我们将在后面的章节中讨论时间序列数据的分析方法。
在本节中,我们讨论了线性回归模型的假设。在下一节中,我们将讨论如何验证线性模型,这又将利用模型残差。
检验显著性和验证模型
到目前为止,本章我们已经讨论了线性回归建模的 OLS 方法的概念;线性模型中的系数;相关系数和确定系数;以及使用线性回归建模所需的假设。现在,我们将开始讨论检验显著性以及模型验证。
检验显著性
为了检验显著性,让我们加载statsmodels宏数据集,这样我们就可以构建一个测试实际国内私人总投资realinv和实际私人可支配收入realdpi之间关系的模型:
import numpy as np
import pandas as pd
import seaborn as sns
import statsmodels.api as sm
import matplotlib.pyplot as plt
from statsmodels.nonparametric.smoothers_lowess import lowess
df = sm.datasets.macrodata.load().data
最小二乘回归需要一个常数系数,以便在这里推导出statsmodels的add_constant函数:
df = sm.add_constant(df, prepend=False)
df_mod = df[['realinv','realdpi','const']]
输入数据不需要服从正态分布即可进行最小二乘回归。然而,假设模型的残差是正态分布的。然而,查看数据的分布以了解其统计数据(如均值、中位数和范围)以及是否存在异常值是有用的。如果模型后的残差分析表明可能存在一些问题,检查模型变量将有助于分析师了解潜在的根本原因。

图 6.11 – 可视化realinv和realdpi变量的分布
当可视化realinv和realdpi之间的关系时,我们可以看到一个强烈的线性关系,但数据中也存在潜在的序列相关性,因为数据似乎存在某种周期性振荡,在图表的右上角这种振荡变得更强烈。

图 6.12 – 可视化realinv和realdpi变量之间的关系
在进行更多步骤之后,我们将分析前图中观察到的潜在序列相关性。首先,让我们将输入变量和系数拟合到模型中:
ols_model = sm.OLS(df_mod['realdpi'], df_mod[['const','realinv']])
compiled_model = ols_model.fit()
接下来,我们将打印出模型性能的摘要,以便我们可以开始评估涉及项的重要性:
print(compiled_model.summary())
参考图 6.13的结果,我们可以看到const(常数)和输入变量realinv均不含零。因此,除了这些变量的显著 p 值外,我们还可以得出结论,它们是目标系数提供的显著贡献者。现在我们已经确认截距和输入变量及其系数的统计显著性,我们想知道它们与目标的相关程度。我们可以看到R 平方统计量为 0.944。由于这两个变量是常数和输入变量,这个 R 平方统计量仅解释了输入变量。
R 平方与调整后的 R 平方
对于多元回归,我们将查看调整后的 R 平方值,我们也可以看到,在单变量/简单回归的情况下,它与 R 平方值相同。
确定系数(也称为拟合优度)为 0.944,意味着实际国内私人总投资解释了实际私人可支配收入中 94.4%的方差。
根据表格,我们可以看到截距和realinv变量在预测目标变量时都具有显著性,且 p 值非常低。我们还可以看到,95%的置信区间均不包含 0,这证实了它们 p 值的关联性。然而,对于常数(截距)的置信区间表明,模型可能存在不确定性的风险,可能需要更多变量来减少这种不确定性并提高模型性能。换句话说,我们可以认为常数的不确定性越高,模型中剩余可解释的方差就越多,当前模型就过于偏颇。
验证模型
Durbin-Watson 检验
重要的是要承认,由于数据是在 50 年的时间跨度内观察到的,并且没有进行随机抽样,因此可能存在序列相关性。我们在 Durbin-Watson 检验统计量 0.095 中观察到序列相关性的可能性,这表明残差表现出正自相关性。Durbin-Watson 统计量检验是否存在一阶自相关性。这意味着 Durbin-Watson 检验每个数据点之间的变化是否与先前数据点的值相关。如果存在这种相关性,则被认为是一阶自相关性。
Durbin-Watson 检验统计量
通常情况下,Durbin-Watson 统计量低于大约 2.0(许多人认为 1.5 到 2.0 是合理的)被视为正自相关的一个标志,而高于大约 2.0(许多人认为 2.0 到 2.5 是合理的)则表明可能存在负自相关。在程序上,需要通过查表来确定与测试统计量应进行比较的关键值。这个过程遵循任何假设检验的相同步骤,即比较测试统计量与临界值。Durbin-Watson 测试通常被解释为单尾测试,因为从双尾测试中得出的推断并不特别有用。Durbin-Watson 表的版本可以在www.real-statistics.com/statistics-tables/durbin-watson-table/找到。此表产生正自相关的临界值。要找到负自相关的临界值,可以将正临界值从 4 中减去。
尽管我们有一个 Durbin-Watson 统计量表明数据中可能存在序列相关,但我们将通过将其与对应的正自相关 statsmodels 的 OLS 回归进行比较来确认其显著性。我们发现 k=1 输入变量的界限为 [1.758, 1.779]。在此范围内的值应被解释为不确定。低于下限的值表明有足够的证据拒绝零假设,从而得出存在统计上显著的积极自相关的结论。高于上限的值表明没有足够的证据拒绝零假设,因此没有自相关。根据我们模型的结果,我们可以有 95% 的置信水平得出结论,我们的残差中存在正序列相关的证据。

图 6.13 – 为 realdpi 的 OLS 回归模型输出
在这一点上,分析师可能想要在继续前进之前解决序列相关问题。然而,为了遵循端到端模型验证的过程,我们将继续进行下一步。
用于分析模型错误的诊断图
如前所述,y_i = β_0 + β_1 x_i + e_i,其中在回归中,β_0 + β_1 x_i 是给定 x_i 的 y_i 的预测均值,而 e_i 是误差项——也称为残差项,它是通过观察值减去该数据点的预测值来计算的。最小二乘线性回归中的残差必须近似于以均值为中心、具有标准差的正态分布。推导拟合模型的残差的方法是从每个观察的实际值中减去预测值。以下是最小二乘回归模型拟合的四个常见可视化方法:
-
残差与拟合图,用于检查输入和目标变量之间线性关系的必要性
-
分位数图,用于检查残差正态分布的假设
-
尺度-位置图,用于检查同方差性的假设,或变化的同质性分布
-
残差与杠杆影响图,有助于识别异常值可能对模型拟合产生的影响
要生成这些图,我们可以使用以下 Python 代码实现:
model_residuals = compiled_model.resid
fitted_value = compiled_model.fittedvalues
standardized_residuals = compiled_model.resid_pearson # Residuals, normalized to have unit variance.
sqrt_standardized_residuals = np.sqrt(np.abs(compiled_model.get_influence().resid_studentized_internal))
influence = compiled_model.get_influence()
leverage = influence.hat_matrix_diag
cooks_distance = compiled_model.get_influence().cooks_distance[0]
fig, ax = plt.subplots(2, 2, figsize=(10,8))
# Residuals vs. Fitted
ax[0, 0].set_xlabel('Fitted Values')
ax[0, 0].set_ylabel('Residuals')
ax[0, 0].set_title('Residuals vs. Fitted')
locally_weighted_line1 = lowess(model_residuals, fitted_value)
sns.scatterplot(x=fitted_value, y=model_residuals, ax=ax[0, 0])
ax[0, 0].axhline(y=0, color='grey', linestyle='--')
ax[0,0].plot(locally_weighted_line1[:,0], locally_weighted_line1[:,1], color = 'red')
# Normal Q-Q
ax[0, 1].set_title('Normal Q-Q')
sm.qqplot(model_residuals, fit=True, line='45',ax=ax[0, 1], c='blue')
# Scale-Location
ax[1, 0].set_xlabel('Fitted Values')
ax[1, 0].set_ylabel('Square Root of Standardized Residuals')
ax[1, 0].set_title('Scale-Location')
locally_weighted_line2 = lowess(sqrt_standardized_residuals, fitted_value)
sns.scatterplot(x=fitted_value, y=sqrt_standardized_residuals, ax=ax[1, 0])
ax[1,0].plot(locally_weighted_line2[:,0], locally_weighted_line2[:,1], color = 'red')
# Residual vs. Leverage Influence
ax[1, 1].set_xlabel('Leverage')
ax[1, 1].set_ylabel('Standardized Residuals')
ax[1, 1].set_title('Residuals vs. Leverage Influence')
locally_weighted_line3 = lowess(standardized_residuals, leverage)
sns.scatterplot(x=leverage, y=standardized_residuals, ax=ax[1, 1])
ax[1, 1].plot(locally_weighted_line3[:,0], locally_weighted_line3[:,1], color = 'red')
ax[1, 1].axhline(y=0, color='grey', linestyle='--')
ax[1, 1].axhline(3, color='orange', linestyle='--', label='Outlier Demarkation')
ax[1, 1].axhline(-3, color='orange', linestyle='--')
ax[1, 1].legend(loc='upper right')
leverages = []
for i in range(len(cooks_distance)):
if cooks_distance[i] > 0.5:
leverages.append(leverage[i])
ax[1, 1].annotate(str(i) + " Cook's D > 0.5",xy=(leverage[i], standardized_residuals[i]))
if leverages:
ax[1, 1].axvline(min(leverages), color='red', linestyle='--', label="Cook's Distance")
for i in range(len(standardized_residuals)):
if standardized_residuals[i] > 3 or standardized_residuals[i] < -3:
ax[1, 1].annotate(i,xy=(leverage[i], standardized_residuals[i]))
fig.tight_layout()
以下输出包含四个常见的诊断图。

图 6.14 – 线性回归诊断图
残差与拟合
最小二乘回归的一个要求是输入和目标变量之间的线性关系。简单来说,输入和目标变量之间存在强烈的关联关系。这个图帮助分析师评估两者之间的线性关系。
我们应该期望拟合线——代表模型——是水平的,表示输入和目标变量之间线性关系在所有数据点上的强度。残差应该在大致合理的距离内均匀地分布在直线周围。如果这些属性无法得到证实,两个变量之间可能存在非线性关系。至少对其中一个变量进行转换——如果变量是偏斜的——可能有助于解决这类问题。然而,也可能是一个非参数模型更为合适,这在存在分类特征——或编码后的分类特征——的情况下通常是这种情况。
我们生成的图没有显示均匀分布在水平线周围的残差。相反,线是向下凹的,这表明多项式或非参数回归可能更为合适。此外,残差在线的右侧形成了一种模式,这表明数据可能存在一些序列相关性,因为它表现出一些正弦行为。我们能够使用 Durbin-Watson 统计量识别出序列相关性的存在。这很可能是由于数据不是随机抽取的,而是在很长的一段时间内收集的。
残差的 QQ 图
QQ 图的正态性评估遵循与在第四章**,参数检验中使用 QQ 图评估正态性的相同程序。此图的主要目的是评估误差的正态分布所需的假设。残差在图中 45 度线附近的轻微偏差是正常的,但极端偏斜或偏离线可能表明拟合度不佳。这种情况可能表明统计功效差。在出现偏斜的情况下,数据转换(如对数转换)可能有助于解决问题。删除异常值可能是一种合适的解决方案。然而,与其直接删除异常值,不如确保没有在数据处理中发生错误,这些错误可以通过使异常值符合要求来解决。
在我们模型残差的 QQ 图中,我们可以看到一些适度的左侧偏斜和极端的右侧偏斜,这表明右侧尾部的三个值可能是极端异常值。除此之外,我们可以假设总体上,残差误差近似正态分布。
尺度-位置
标准化残差的平方根的尺度-位置图有助于分析师通过视觉检查确定残差是否存在同方差性——或非恒定方差,这是最小二乘回归模型所必需的。该图可视化标准化残差的绝对值的平方根。分析同方差性有时有助于识别与样本独立性相关的潜在问题,这是最小二乘回归的另一个要求。然而,在测试此假设时,应更重视 Durbin-Watson 测试。
在评估我们的模型时,我们可以看到线与完全水平的状态有一些偏差。此外,残差虽然看似具有大量恒定的方差,但有时会表现出模式,这与同方差性的行为相冲突。此外,似乎有三个非常明显的异常值。基于这些信息,我们可以合理地假设模型残差中存在异方差性——或恒定方差。
残差与杠杆影响
评估残差与杠杆影响是评估模型拟合度的一种另一种方法。除了衡量杠杆作用外,此图还显示了库克距离何时超出合理水平。
杠杆作用用于确定一个点与其他所有点之间的距离。对于残差的杠杆作用较高可能意味着相应的数据点是强异常值,强烈影响模型以更不精确地拟合整体数据,并给予该特定值更多的权重。残差应在-2 到 2 之间,否则不应被视为潜在的异常值。介于+/-2.5 到 3 之间的值表明数据点是极端异常值。总的来说,这个图有助于将没有对模型产生显著负面影响的异常值与那些确实有影响的异常值区分开来。
库克距离是一种同时使用观测值的杠杆作用和残差值的测量方法。随着杠杆作用的提高,其计算的库克距离也更高。一般来说,库克距离值大于 0.5 意味着残差具有杠杆作用,通过过度表示对模型产生负面影响——本质上是一个异常值。库克距离包含在代码中,并标记出距离大于 0.5 的残差。库克距离特别有用,因为移除异常值可能不会对模型产生重大影响。然而,如果异常值的库克距离值较高,这强烈表明解决异常值将有助于模型。库克距离是通过从模型中移除每个数据点并计算误差差异除以均方误差乘以模型系数数量加一来计算的。其公式如下:
D i = ∑ j=1 n (ŷ j − ŷ j(i)) 2 ___________ (p + 1)σ ̂ 2
其中 ŷ j 是响应的 j 次估计,ŷ j(i) 是移除 i 次拟合值后的 j 次拟合值。p 是模型中的系数数量,σ ̂ 2 是所有观测值的残差方差,包括那些被移除的,也称为平方误差。
在我们模型的图中,我们可以观察到三个极端异常值。然而,我们也可以看到库克距离的值没有大于 0.5 的。与尺度-位置图一样,标准化残差应围绕一条水平平滑线均匀分布,这表明方差是恒定的。我们可以从我们模型的输出中看到,情况并非如此。
解决残差问题
单独来看,每个图都可以被认为是足以否定模型的有效性。然而,考虑所有图是有用的,因为如果诊断图中只有一些与预期不符的偏差,模型仍然可能是有用的。以下是一些解决残差问题的方法:
-
调查和解决可能导致异常值或数据不一致性的任何数据收集问题
-
直接移除异常值
-
使用随机或分层抽样等方法改进采样方法
-
执行数据转换,例如对数或平方根转换
-
包含额外的输入变量,这些变量可以帮助解释目标变量或可能存在的任何序列相关性
处理序列相关性
如果对残差中是否存在序列相关性不确定,一个有用的下一步是分析偏自相关函数(PACF)图,以评估模型在显著水平上是否存在序列相关性,这可能会解释模型残差的问题。
ACF 和 PACF
PACF通过控制滞后之间的差异,提供了点值与先前点(称为滞后)之间的部分相关性。控制滞后之间的差异是“部分”一词的来源。如果一个数据集中滞后零处的特定点与第三个滞后点的值强烈相关,那么它将与滞后 3 有显著相关性,但不会与滞后 2 或 1 有相关性。在时间序列建模中,PACF 用于直接推导自回归阶数,表示为AR(n),其中n是滞后。自相关函数(ACF),用于确定移动平均阶数,表示为MA(n),考虑了在滞后零的数据点与所有先前滞后点之间的相关性,而不控制它们之间的关系。换句话说,ACF 图在滞后 3 处的自相关性将提供从点 0 到点 3 的跨点自相关性,而 PACF 图在滞后 3 处的相关性将只提供点 0 和点 3 之间的自相关性,排除了滞后 1 和 2 处值的影响。
我们可以通过以下代码生成使用最近 50 个数据点(按索引位置排序,在本例中按日期排序)的 PACF 图,并带有 95%置信区间:
from statsmodels.graphics.tsaplots import plot_pacf
plot_pacf(compiled_model.resid, alpha=0.05, lags=50);

图 6.15 – 模型残差的偏自相关图
在图 6.15 的 PACF 图中,我们看到在滞后 0 和 1 处存在非常强烈且显著的关联。我们观察到滞后 2 和 36 的显著关联水平较低(大约为-0.25),并延伸到 95%置信区间之外。这告诉我们,我们可能更适合用一阶自回归(AR(1))时间序列模型来建模数据集,而不是线性回归模型。
当存在显著的自相关时,使用最小二乘法回答回归问题会带来固有的风险。当检测到自相关时,有方法可以调整模型的结果,例如采用一阶差分——或另一种类型的低通滤波器——或者应用 Cochrane-Orcutt、Hildreth-Lu 或 Yule-Walker 调整。然而,进行时间序列分析是理想的,因为这种类型建模的结果对时间依赖性模式更稳健。我们将在第十章中介绍时间序列,时间序列简介,我们将讨论近似时间序列以产生更有用结果的方法。
让我们考虑数据的一阶差分。关于*图 6.16,我们首先查看输入变量realdpi的 ACF 图和原始数据。我们可以看到一个指数增长的线(原始数据),它显示了一个指数衰减的 ACF 图。这种 ACF 行为对于趋势数据是典型的,这类数据通常从一阶差分**中受益。在 PACF 中识别出的低自回归阶数表明,这可能足以使回归模型继续进行。如果 ACF 显示出不同的行为,例如正弦波(季节性)自相关,或者 PACF 显示出更多的部分自相关,我们将有证据表明一阶差分不是解决序列相关并继续进行最小二乘回归的充分解决方案。对于最小二乘回归,ACF 和 PACF 的理想情况是没有显著相关的滞后。然而,当 ACF 表现出这种行为,而 PACF 没有显示出显著性,或者当相关程度非常低时,回归可能表现良好。最好是两个图表中都没有模式或峰值。无论如何,在两种情况下,都应该评估模型误差,以及残差的自相关性,以确定最小二乘回归是否按需执行。
使用diff() numpy函数,我们对原始数据进行了首次低通差分,并绘制了 ACF、PACF 和原始数据与差分数据的线图。结果我们可以看到自相关性的显著降低。顺便提一下,差分后的自相关性表明,一阶自回归移动平均(ARMA)模型,AR 阶数为 1,MA 阶数为 1,可能比最小二乘回归更适合。然而,我们将忽略这一点,继续进行:
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.graphics.tsaplots import plot_pacf
fig, ax = plt.subplots(2,3, figsize=(15,10))
plot_acf(df_mod['realdpi'], alpha=0.05, lags=50, ax=ax[0,0])
ax[0,0].set_title('Original ACF')
plot_pacf(df_mod['realdpi'], alpha=0.05, lags=50, ax=ax[0,1])
ax[0,1].set_title('Original PACF')
ax[0,2].set_title('Original Data')
ax[0,2].plot(df_mod['realdpi'])
plot_acf(np.diff(df_mod['realdpi'], n=1), alpha=0.05, lags=50, ax=ax[1,0])
ax[1,0].set_title('Once-Differenced ACF')
plot_pacf(np.diff(df_mod['realdpi'], n=1), alpha=0.05, lags=50, ax=ax[1,1])
ax[1,1].set_title('Once-Differenced PACF')
ax[1,2].set_title('Once-Differenced Data')
ax[1,2].plot(np.diff(df_mod['realdpi'], n=1))
我们得到了以下结果:

图 6.16 – 实 inv 变量在一阶差分前后的绘图
这为我们提供了可能更适合使用回归模型建模的数据。然而,当使用差分作为解决最小二乘回归模型中序列相关性的方法时,我们还必须对输入变量进行微分。观察输入变量realinv在变换前后的情况。在这里的图 6.17中,我们可以看到与realdpi相似的行为:

图 6.17 – realdpi变量在一阶差分前后的绘图
我们现在可以使用差分数据构建一个新的回归模型。首先需要注意的是,一阶差分从数据集中移除了一个数据点,因此设计矩阵中的常数项也必须移除一个值:
ols_model_1diff = sm.OLS(np.diff(df_mod['realdpi'], n=1), pd.concat([df_mod['const'].iloc[:-1], pd.Series(np.diff(df_mod['realinv'], n=1))], axis=1))
compiled_model_1diff = ols_model_1diff.fit()
这个模型的输出明显不同。我们现在看到一个 r-squared 值为0.045。观察差分数据,我们可以看到可支配收入和投资之间的差分数据有不同的方差。这意味着尽管realinv变量在预测realdpi时似乎很重要,但还有更多因素影响着实际可支配的毛个人收入。我们观察到新的 Durbin-Watson 统计量现在是2.487。使用之前表中查找的临界值[1.758, 1.779],我们看到 Durbin-Watson 统计量现在高于上限[2.221, 2.242]。因为新的统计量2.487大于2.221,我们可以确认现在模型残差中存在负自相关。根据 PACF 图以及与临界值范围的接近程度,我们可以看到自相关比之前显著减少,但仍然存在。然而,可以争辩说残差的自相关现在足够小,可以被认为是解决了;如前所述,Durbin-Watson 统计量小于 2.5 可以被认为是正常的。尽管如此,与时间序列建模相比,最小二乘回归在生成预测时仍然存在风险。我们将在第十章“时间序列简介”中开始对这个主题进行深入概述。
然而,为了通过回归模型验证的步骤,我们假设数据中没有自相关。下一步将是测试我们的模型。
模型验证
假设分析师已经开发了一个产生强大结果且根据之前提到的图表看起来有用的模型,下一步是测试该模型。一种传统的方法是在用于原始模型的全部数据上执行训练和测试分割,其中我们拟合所有数据。在下面的代码中,我们进行了一个分割,其中训练数据集包含 75%的数据,测试数据集包含 25%。目的是评估这两个数据集之间以及与原始模型之间性能是否存在显著差异。可接受的差异由分析师决定,但除了评估已讨论的指标之间的差异外,分析师还应注意斜率和输入变量的系数,因为这些将用于预测目标。
在这里,我们分割数据。我们使用shuffle参数来随机打乱数据,以便如果数据存在某种顺序,例如按时间排序,数据将被随机分割:
from sklearn.model_selection import train_test_split
train, test = train_test_split(df_mod, train_size=0.75, shuffle=True)
现在,我们使用训练数据构建一个模型:
ols_model_train = sm.OLS(train['realdpi'], train[['const','realinv']])
compiled_model_train = ols_model_train.fit()
print(compiled_model_train.summary())
最后,我们使用测试数据构建一个模型:
ols_model_test = sm.OLS(test['realdpi'], test[['const','realinv']])
compiled_model_test = ols_model_test.fit()
print(compiled_model_test.summary())
这里的想法是,两个模型应该在数据的两个不同部分上产生相似的结果。
验证模型的另一种方法是将其与朴素模型进行比较。让我们使用平均绝对误差(MAE)。使用这个模型指标,我们可以比较朴素线性模型的误差,其中误差是每个数据点减去平均值与模型的预测值。有用的模型 MAE 需要低于朴素模型的 MAE。在这里,我们使用拟合模型来预测输入值,然后比较模型的预测值与朴素模型的预测值:
下面我们可以观察到训练模型的 MAE:
from sklearn.metrics import mean_absolute_error
mae = mean_absolute_error(train['realdpi'], compiled_model_train.predict(train[['const','realinv']]))
mae
#438.5872081797031
在这里,我们可以看到朴素模型的 MAE,该模型除了假设均值将继续保持真实值外,不做任何假设:
import numpy as np
errors = []
for i in range(len(train)):
errors.append(abs(train['realdpi'].iloc[i] - train['realdpi'].mean()))
np.mean(errors)
#2065.440235457064
我们可以看到模型与朴素模型相比表现良好。
另一种流行的测试方法是使用保留数据集进行测试。为此,在训练数据上构建并训练一个模型。然后,使用该模型,将其应用于测试数据。然后,我们将比较指标与训练数据的指标。使用训练模型的 MAE 为 438,我们将其与以下进行比较:
mae_test = mean_absolute_error(test['realdpi'], compiled_model_train.predict(test[['const','realinv']]))
mae_test
#408.0253171549187
我们可以看到测试数据的误差低于训练数据的误差。然而,由于假设训练数据和测试数据具有不同的值和均值,我们应该重新评估朴素模型对于测试数据仍然产生较高的 MAE:
errors = []
for i in range(len(test)):
errors.append(abs(test['realdpi'].iloc[i] - test['realdpi'].mean()))
np.mean(errors)
#1945.5873125720873
在这里,我们可以看到模型在测试数据和训练数据上提供了与朴素模型大致相同的 MAE,因此我们预计线性回归模型比朴素模型更好。
摘要
在本章中,我们讨论了单变量解释变量和单变量响应变量之间简单线性回归的概述。我们涵盖的主题包括以下内容:
-
简单线性回归的 OLS 方法
-
相关系数和确定系数及其计算和意义
-
最小二乘回归所需的前提假设
-
模型和参数显著性的分析方法
-
模型验证
我们仔细研究了误差平方的概念以及误差平方和对于构建和验证线性回归模型的意义。然后,我们探讨了使线性回归成为一个稳定解所需的四个相关假设。之后,我们概述了四个诊断图及其在评估与异方差性、线性、异常值和序列相关性相关的问题时的解释。然后,我们通过一个使用 ACF 和 PACF 评估序列相关性的例子,以及一个使用一阶差分从数据中移除序列相关性约束并使用差分数据构建 OLS 模型的例子。最后,我们提供了测试和验证最小二乘回归模型的方法。
在下一章中,我们将扩展这一概念,包括一个以上的解释变量,这种技术称为多元线性回归。我们还将讨论与多元线性回归相关的各种主题,例如变量选择和正则化。
第七章:多元线性回归
在上一章中,我们讨论了使用一个变量来解释目标变量的简单线性回归(SLR)。在本章中,我们将讨论多元线性回归(MLR),这是一个利用多个解释变量来建模响应变量的模型。多元建模面临的主要难题是多重共线性以及偏差-方差权衡。在概述 MLR 之后,我们将介绍用于评估和最小化多重共线性的方法。然后,我们将讨论利用偏差-方差权衡来为分析师带来好处的方法。最后,我们将讨论使用主成分回归(PCR)来处理多重共线性,以最小化过拟合,而不是删除特征,而是转换它们。
在本章中,我们将涵盖以下主要主题:
-
多元线性回归
-
特征选择
-
收敛方法
-
维度缩减
多元线性回归
在上一章中,我们讨论了简单线性回归(SLR)。通过 SLR,我们能够使用另一个变量(通常称为解释变量,表示为 x)来预测一个变量的值(通常称为响应变量,表示为 y)。SLR 模型通过以下方程表示,其中 β 0 是截距项,β 1 是线性模型的斜率。
y = β 0 + β 1 x + ϵ
虽然这是一个有用的模型,但在许多问题中,可以使用多个解释变量来预测响应变量。例如,如果我们想要预测房价,我们可能需要考虑许多变量,这些变量可能包括地块大小、卧室数量、浴室数量和总体大小。在这种情况下,我们可以扩展先前的模型以包括这些额外的变量。这被称为 MLR。MLR 模型可以用以下方程表示。
y = β 0 + β 1 x 1 + β 2 x 2 + … + β p x p + ϵ
与前一个方程类似,β 0 代表截距。在这个方程中,除了截距值之外,我们还有每个解释变量的 β 参数。每个解释变量用带数字下标的 x 表示。我们可以在模型中包含尽可能多的解释变量,这就是为什么方程显示了最后的下标 p。
数据规模对 MLR 的影响
通常,我们可以在模型中包含尽可能多的解释变量。然而,有一些现实限制。这些潜在限制之一是样本数量与解释变量数量的相对关系。MLR 在样本数量(N)远小于解释变量数量(P)时效果最佳。当 P 接近 N 时,模型变得难以估计。如果 P 相对于 N 很大,那么考虑降维是明智的,这将在本章后面讨论。
让我们来看一个例子。Python 包 scikit-learn 包含了多个用于建模练习的数据集。我们将使用 diabetes 数据集,该数据集包括了一年后疾病进展的重要测量和定量指标。更多关于这个数据集的信息可以在以下链接找到:scikit-learn.org/stable/datasets/toy_dataset.xhtml#diabetes-dataset。我们将尝试建立生命统计与疾病进展之间的关系。
该数据集中的解释变量包括 age(年龄)、bmi(体质指数)、bp(血压)、s1、s2、s3、s4、s5 和 s6。响应变量是疾病进展的测量值。以下是这种模型数学表达的方式:
progression = β 0 + β 1 x age + β 2 x bmi + β 3 x bp + β 4 x s1 + β 5 x s2
- β 6 x s3 + β 7 x s4 + β 8 x s5 + β 9 x s6 + ϵ
如前所述,模型中每个变量都有一个 β 值。
对糖尿病数据集的转换
该数据集中的解释变量已经从原始测量值转换过来。参考数据集用户指南,我们可以看到每个变量都已经进行了均值中心化和缩放。此外,sex 变量已经被从分类变量转换为数值变量。
添加分类变量
到目前为止,我们只考虑了模型中的连续变量,即可以在数轴上具有任何值的变量。回顾一下 第二章**,数据分布,我们之前考虑的变量现在是比率数据。然而,线性回归模型并不限于使用连续变量。我们可以在模型中包含分类变量。回顾一下 第二章**,数据分布,分类变量与项目组相关联。在统计学习中,这些组通常被称为变量的 水平。例如,如果我们有五个学生,其中三个拥有硕士学位,一个拥有博士学位,一个拥有学士学位,那么我们可以说学生的分类变量有三个水平:BS、MS 和 PhD。让我们看看如何将分类变量纳入模型。
我们将通过包含额外的 β 项将分类变量纳入模型。具体来说,我们将添加水平数(L)减一的额外 β 项。对于有三个水平的示例,我们将添加两个额外的 β 项。使用少于水平数的一个值将允许我们选择一个水平作为参考(或基线),并将其他水平与该参考水平进行比较。我们将像其他 β 项一样在模型中包含 β 项。比如说,在这个例子中,我们正在建模收入水平,除了教育水平外,我们还有个人的经验。那么,我们的模型将构建如下:
收入 = β 0 + β 1 x 经验 + β 2 x ms + β 3 x phd
在添加了β项之后,我们还需要进行数据转换。我们必须为分类变量中的每个非参考级别创建虚拟变量。这个过程被称为分类变量的虚拟化。在虚拟化过程中,我们为每个非参考级别创建一列,并在原始变量中该级别出现的地方插入 1,而在该级别未出现的地方插入 0。这个过程在图 7.1中得到了演示。

图 7.1 – 分类变量的虚拟化
原始变量包含 BS、MS 和 PhD 级别。BS 被选为参考级别,并为 MS 和 PhD 级别创建了两个虚拟变量。为分类变量创建额外列的过程称为编码。有许多类型的变量编码,编码是统计学习和机器学习中广泛使用的技术。
分类级别是常数位移
让我们更深入地看看分类变量对线性回归模型的影响。之前,我们讨论了如何使用取值为 0 和 1 的虚拟变量对分类变量进行编码。本质上,这意味着我们根据级别切换相关的β项,这将使响应位移一个常数。例如,在收入模型中,当我们想要计算具有 BS 学位的个人收入时,模型将解析为以下:
收入 = β 0 + β 1 x 经验
当我们想要计算具有 MS 学位的个人收入时,模型将解析为以下:
收入 = β 0 + β 1 x 经验 + β 2
这意味着分类级别的水平只会使模型的输出从参考水平位移一个常数,即与该级别相关的β。
回到我们之前的例子,我们的数据集包括一个分类变量:患者的性别。数据集包括性别的两个级别。我们将选择一个级别作为参考,并为另一个级别创建一个虚拟变量。有了这些知识,让我们将数据拟合到线性回归模型中,并讨论多元线性回归的假设。
评估模型拟合度
每当我们拟合一个参数模型时,我们都应该验证模型假设是否得到满足。正如前一章所讨论的,如果模型假设没有得到满足,模型可能会提供误导性的结果。在前一章中,我们讨论了简单线性回归的四个假设:
-
响应变量和解释变量之间的线性关系
-
残差的正态性
-
残差的同方差性
-
独立样本
这些假设也适用于此处,但在这种新的建模环境中,我们还有一个额外的假设:没有或几乎没有多重共线性。多重共线性发生在 MLR 模型中的两个或多个解释变量高度相关时。多重共线性存在会影响独立变量的统计显著性。理想情况下,所有解释变量都将是不相关的(或线性独立的)。然而,我们可以接受一定程度的多重共线性,而对模型影响不大。
让我们评估这些假设中的每一个模型。
线性关系
要检查响应变量与解释变量之间的线性关系,我们可以查看每个变量与响应变量的散点图。根据图 7.2 中的图表,包括 bmi、bp、s3、s4 和 s5 在内的几个变量可能与响应变量表现出线性关系。

图 7.2 – 响应变量与解释变量之间的散点图
虽然如果任何变量与响应变量显示出强烈的线性关系将是理想的,但在 MLR 模型中,我们有变量组合的好处。我们可以添加看起来更有用的变量,并移除看起来没有用处的变量。这被称为特征选择,我们将在下一节中介绍。
残差正态性
回顾上一章,我们期望来自拟合良好的模型的残差应呈现随机分布。我们可以通过直方图或 QQ 图来查看这一点。我们模型的残差在图 7.3 中显示。

图 7.3 – 残差值
根据图 7.3 中的直方图,我们不能排除残差呈正态分布的可能性。一般来说,在评估这个假设时,我们是在检查是否存在明显的违反情况。结果证明,线性回归对这一假设相对稳健。但这并不意味着这个假设可以被忽略。
残差同方差性
在上一章中,我们也讨论了同方差性。对于一个拟合良好的模型,我们期望残差应表现出同方差性。图 7.4 展示了残差与模型预测值之间的散点图。

图 7.4 – 预测值与实际值之间模型残差的散点图
看起来没有明显的方差变化模式或显著的异常值,这会违反同方差性的假设。如果存在模式,那将是一个信号,表明一个或多个变量可能需要转换,或者响应变量和解释变量之间可能存在非线性关系。
独立样本
在上一章中,我们讨论了独立采样及其对这类模型的影响。然而,如果我们不知道抽样方法,就无法确定样本是否独立。由于我们不知道这个数据集的抽样策略,我们将假设这个假设得到满足,并继续进行模型。在实际建模环境中,这个假设永远不应被想当然。
多重共线性
对于多元线性回归(MLR)的新假设是,解释变量中几乎没有多重共线性。多重共线性是当两个或更多变量强线性相关时出现的情况。我们通常使用 方差膨胀因子(VIF)来检测多重共线性。VIF 是衡量解释变量系数受其他解释变量影响程度的一个度量。VIF 越低越好,其中最小值为 1,表示没有相关性。我们通常认为 VIF 为 5 或更高是过高的。当在一组解释变量中检测到高 VIF 时,我们反复移除具有最高 VIF 的变量,直到每个变量的 VIF 值都低于 5。让我们看看我们当前数据的例子。移除具有高 VIF 变量的过程在 图 7**.5 中展示。

图 7.5 – 从数据集中移除高 VIF 变量
图 7**.5 展示了从糖尿病数据集中移除具有高 VIF 变量的过程。图中最左边的表格显示了原始数据集,其中最高的 VIF 为 59.2,对应变量 S1。然后,我们从数据集中移除这个变量并重新计算 VIF。现在我们看到最高的 VIF 为 7.8,对应变量 s4。我们移除这个变量并重新计算 VIF。现在,所有 VIF 值都低于 5,表明剩余变量之间相关性较低。移除这些变量后,我们需要再次拟合模型。
在模型拟合和模型假设得到验证后,让我们看看拟合结果并讨论如何解释这些结果。
解释结果
拟合模型后,我们从 statsmodels 获得了以下结果。输出分为三个部分:
-
顶部部分包含关于模型的高级统计数据
-
中间部分包含关于模型系数的详细信息
-
底部部分包含关于数据和残差的诊断测试
让我们逐一查看这个模型的每个部分。

图 7.6 – statsmodels OLS 回归的结果
高级统计和指标(顶部部分)
在拟合结果的顶部部分,我们有模型级别的信息。顶部部分的左侧包含有关模型的信息,例如自由度(df)和观测数。顶部部分的右侧包含模型指标。模型指标对于比较模型很有用。我们将在特征选择部分讨论更多关于模型指标的内容。
模型系数详情
中间部分包含有关模型系数(之前列出的方程中的β项)的详细信息。在本节的目的上,我们将关注中间部分的两个列:coef和P>|t|。coef列是针对模型方程估计的模型系数(β的估计或称为ˆβ)。标记为P>|t|的列是对系数进行显著性检验的 p 值。我们对这两列都感兴趣,以解释模型。让我们从 p 值列开始。
对于这个测试的零假设是β参数的值等于零。回顾以下内容来自第三章**, 假设检验:
-
p 值低于显著性阈值意味着我们拒绝零假设
-
p 值高于显著性阈值意味着我们未能拒绝零假设
在这个例子中,我们会拒绝bmi变量的零假设,但对于年龄变量,我们未能拒绝零假设。一旦我们确定了显著的变量,我们就可以继续解释显著变量的含义。我们无法提供其他变量的解释,因为我们不能拒绝它们的系数值可能为零。如果系数值为零,则该变量对模型没有贡献。让我们看看如何解释系数。
通常,当我们构建一个模型时,我们想要了解模型的各个部分如何影响输出。在 MLR 模型中,这归结为理解模型的系数。我们有两种类型的变量,连续的和分类的,相关的系数有不同的含义。
解释连续变量系数
对于连续变量,例如BMI,随着变量值的增加,其对模型输出的重要性也增加。变量值的单位增加与因变量均值的增加相关,这种增加的大小等于系数的大小,同时保持其他变量不变。以bmi变量为例。由于bmi变量的系数大约为 526,我们可以这样说:“bmi的单位增加与其他变量保持不变时,将导致糖尿病测量均值增加 526。”当然,系数也可以取负值。
解释分类变量系数
对于分类变量,例如sex,回想一下,与连续变量不同,分类变量的值是虚拟编码的,因此只能取两个值:零和一。同时,回想一下,选择了一个水平作为参考水平。在这种情况下,sex水平 0 是参考水平,我们可以将这个参考水平与sex水平 1 进行比较。当我们使用参考水平时,系数不会影响模型的输出。因此,分类水平的变化与因变量均值的改变相关,这种改变的大小等于系数的大小,同时保持其他变量不变。由于sex变量的系数大约为-22,我们可以这样说:“sex水平与相对于参考水平,其他变量保持不变时,糖尿病测量均值减少 22 相关。”
诊断测试
拟合结果的最下面部分包含了数据和残差的诊断统计量。浏览一下列表,其中几个应该来自第六章**,简单线性回归。Durbin-Watson 检验是对序列相关性的检验(数据按时间顺序进行采样)。结果在 2 左右并不表明存在序列相关性。偏度和峰度是残差分布形状的测量。这些结果几乎表明没有偏度,但可能存在一些峰度。这些很可能是与正态分布的小偏差,并且正如我们在前面的图表中看到的那样,不会引起担忧。
在本节中,我们研究了第一个可以使用多个解释变量来预测响应变量的模型。然而,我们注意到模型中包含的几个变量在统计上并不显著。对于这个模型,我们仅通过移除具有高 VIF 分数的特征来选择特征,但在选择特征时还有其他方法可以考虑。在下一节中,我们将讨论比较模型和特征选择。
特征选择
影响模型成功或失败的因素有很多,例如采样、数据质量、特征创建和模型选择,其中一些我们没有涉及。这些关键因素之一是特征选择。特征选择简单来说就是从现有特征集中选择或系统地确定最佳特征的过程。我们已经进行了一些简单的特征选择。在上一节中,我们移除了具有高 VIF 的特征。在本节中,我们将探讨一些特征选择的方法。本节中介绍的方法分为两类:特征选择的统计方法和基于性能的方法。让我们从统计方法开始。
特征选择的统计方法
特征选择的统计方法依赖于我们在前几章中一直使用的工具:统计显著性。本子节中介绍的方法将基于特征本身的统计特性。我们将介绍两种特征选择的统计方法:相关性和统计显著性。
相关性
我们将首先讨论的统计方法是相关性。我们在本章和前几章中讨论过相关性;回想一下,相关性是描述两个变量之间关系的一种描述。变量可以是正相关、不相关或负相关。在特征选择方面,我们希望移除与响应变量不相关的特征。与响应变量不相关的特征与响应变量没有关系。因此,一个不相关的特征不会是一个好的响应变量预测因子。
回想一下第四章**,参数检验,我们可以使用皮尔逊相关系数来衡量两个变量之间的线性相关性。实际上,我们可以计算所有特征与目标变量之间的相关系数。完成这些计算后,我们可以构建一个如图 7.7所示的关联排名。

图 7.7 – 特征关联排名
当根据相关性评估特征时,我们最感兴趣的是具有高绝对值相关性的特征。例如,图 7.7中的关联排名显示了以下内容:
-
bmi和s5与响应变量表现出强烈的关联 -
bp、s4、s6和s3与响应变量表现出中等相关性 -
s1、age和s2与响应变量表现出弱相关性
虽然sex可能看起来与响应变量没有相关性,但皮尔逊相关系数不能用于分类特征。从这一相关性排名中,我们可以看到,至少,bmi、s5和bp可能是这个数据集中预测响应的最佳特征之一。实际上,这些特征在我们的模型中被认为是统计显著的。现在让我们讨论使用统计显著性进行选择。
统计显著性
使用相关性来评估特征通常是特征选择的一个良好开端。我们可以轻松地消除与响应变量不相关的特征。然而,根据问题,我们可能仍然会剩下许多与响应变量相关的特征。我们可以进一步使用特征在模型中的统计显著性来选择特征。然而,近年来,这些特征选择方法在社区中已经有些不受欢迎。因此,我们将不会专注于这些方法,但仅为了理解而描述它们。
回想当我们拟合 MLR 模型时,结果包括对模型中每个特征的统计显著性测试。我们可以使用这个测试来选择特征。基于统计显著性选择特征有三种著名的算法:前向选择、后向选择和逐步回归。在前向选择中,我们从模型中没有变量开始,然后每次迭代只添加一个变量,使用 p 值来选择每个迭代中要添加的最佳特征。一旦模型中任何特征的 p 值超过一个预定义的阈值,例如 0.05,我们就停止。后向选择采取相反的方法。我们从模型中的所有特征开始,然后每次迭代使用 p 值来确定最不重要的特征,逐个移除特征。最后的算法是逐步回归(也称为双向消除)。逐步回归使用前向和后向测试。从模型中没有特征开始,然后在每个迭代中,执行一次前向选择步骤,然后进行一次后向选择遍历。
这些选择方法在过去被广泛使用。然而,近年来,基于性能的方法已经变得更加普遍。现在让我们来讨论基于性能的方法。
特征选择的基于性能的方法
前面提到的统计特征选择方法的主要问题是它们倾向于创建过拟合模型。过拟合模型是一种恰好拟合给定数据但无法推广到新数据的模型。基于性能的方法通过使用称为交叉验证的方法来克服过拟合。在交叉验证中,我们有两个数据集:一个用于拟合模型的训练数据集和一个用于评估模型的测试数据集。我们可以从多个特征集构建模型,在训练集上拟合所有这些潜在模型,并最终根据测试集上给定指标的性能对它们进行排名。
模型比较
在我们讨论特征选择方法之前,让我们首先讨论如何比较模型。我们使用指标来比较模型。在基本层面上,我们可以使用指标来帮助我们确定一组特征是否比另一组特征更好,这是基于模型性能的。可以用于比较模型的指标有很多。我们将讨论两个指标,均方误差(MSE)和平均绝对百分比误差(MAPE),它们是目前回归模型中最常用的两个指标。
MSE
MSE 由以下公式给出,其中N是样本数量,y 是响应变量,ˆy 是响应变量的预测值。
MSE = 1/N ∑(i=1 to N) (y_i - ˆy_i)²
换句话说,取响应值与预测响应值之间的差异,平方这些差异,并最终取平方差异的平均值。这个指标的一个常见扩展是均方根误差(RMSE),它仅仅是 MSE 的平方根。当希望指标与响应变量具有相同的单位时,使用 RMSE。
MAPE
MAPE 由以下公式给出,其中N是样本数量,y 是响应变量,ˆy 是响应变量的预测值:
MAPE = 100% × N ∑(i=1 to N) |y_i - ˆy_i| / y_i
这个公式类似于 MSE 的公式,但不同的是,我们取的是百分比误差的平均值,而不是平方误差的平均值。这使得 MAPE 比 MSE 更容易解释,这是相对于 MSE 的一个显著优势。
现在我们已经讨论了模型验证和指标,让我们将这些概念结合起来,使用模型性能作为指标来进行特征选择。
递归特征消除
递归特征消除(RFE)是一种使用指标在模型中选择最佳特征数量的方法。与前面提到的向后选择方法类似,RFE 算法从模型中的所有特征开始,然后移除对模型影响最小的特征。在每一步,都会进行交叉验证。当 RFE 完成后,我们将能够看到模型在各种特征集上的交叉验证性能。
在这个例子中,我们使用了scikit-learn (sklearn)中的线性回归实现和 RFE 实现,它是 Python 生态系统中用于机器学习的主要包。在下面的代码示例中,我们设置了 RFE 使用 MAPE 作为评分指标(make_scorer(mape ,greater_is_better=False)),每一步移除一个特征(step=1),并通过cv=2指示使用两个交叉验证集来评分模型:
linear_model = LinearRegression()
linear_model.fit(X, y)
rfecv = RFECV(
estimator=linear_model,
step=1,
cv=2,
scoring=make_scorer(mape ,greater_is_better=False),
min_features_to_select=1
)
rfecv.fit(X,y)
一旦我们拟合了 RFE 对象,我们就可以查看结果,以了解模型在各个特征集上的评分情况。使用 MAPE 评分的模型性能在图 7.8中显示。

图 7.8 – 在 RFE 步骤中线性回归的性能,使用 MAPE 评分
很明显,包含所有特征会产生性能最好的模型,但包含所有 10 个特征仅比只包含五个特征的性能略有提高。虽然性能最好的模型包含所有 10 个特征,但我们仍然需要考虑模型假设并验证模型是否拟合良好。
在本节中,我们探讨了多种特征选择方法,包括统计方法和基于性能的方法。我们还讨论了指标以及如何比较模型,包括为什么需要将数据分为训练集和验证集。在下一节中,我们将探讨线性回归收敛方法。这类模型使用一种称为正则化的方法,它在某些方面类似于基于模型的特征选择。
收敛方法
偏差-方差权衡是所有统计和机器学习实践者在进行建模时必须平衡的一个决策点。过多的一方都会使结果变得无用。为了捕捉这些问题,我们查看测试结果和残差。例如,假设已经选择了一个有用的特征集和适当的模型,一个在验证集上表现良好但在测试集上表现不佳的模型可能表明方差过多,反之,一个在所有方面都表现不佳的模型可能有过多的偏差。在两种情况下,这两个模型都无法很好地泛化。然而,虽然模型中的偏差可以从一开始的模型性能不佳中识别出来,但高方差可能非常具有欺骗性,因为它在训练期间甚至验证期间都有可能表现得非常好,这取决于数据。高方差模型经常使用不必要的高的系数值,而实际上可以从不高的系数值中获得非常相似的结果。此外,在使用不必要的高的系数值时,模型更有可能处于偏差-方差平衡状态,这为它在未来的数据上良好泛化提供了更好的机会。此外,当模型系数没有夸张时,可以提供对当前因素对给定目标影响的更可靠的见解,这有助于进行更有用的描述性分析。这使我们来到了收缩的概念。
收缩是一种通过将模型参数系数缩小到零来减少模型方差的方法。这些系数是通过将最小二乘回归应用于模型考虑的所有变量来导出的。收缩的程度基于参数对最小二乘估计的贡献;对高平方误差有贡献的参数将会有它们的系数推向零或完全为零。在这种情况下,收缩可以用于变量消除。对于那些在整个模型拟合中不贡献高平方误差的变量,其系数值将会有最小的减少,因此对于模型拟合是有用的,假设它们包含的实际目的是经过检验的。收缩(也称为正则化)之所以重要,是因为它有助于模型包含有用的变量,同时防止它们引入过多的方差,从而避免过拟合。防止过多的方差对于确保模型在时间上具有良好的泛化能力特别有用。让我们来看看一些最常见的收缩技术,岭回归和最小绝对收缩和选择算子(LASSO)回归。
Ridge 回归
回想一下,使用最小二乘回归对多元线性回归的残差平方和(RSS)的公式如下:
RSS = ∑ i=1 n (y_i − ˆβ_0 − ∑ j=1 p ˆβ_j x_ij)²
其中 n 是样本数量,p 是参数数量。岭回归添加了一个标量 λ,称为调整参数——它必须大于或等于 0——并将其乘以模型参数系数估计 ˆβj²,以创建一个收缩惩罚。这被添加回 RSS 方程中,使得新的最小二乘回归的拟合过程现在定义为以下:
RSS + 𝝀∑j=1pˆβj²
当λ=0 时,相应的岭回归惩罚项为 0。然而,当λ→∞时,模型系数趋向于零。新的拟合过程可以重写如下:
‖y − Xβ‖²² + 𝝀 ‖β‖²²
其中 RSS = ‖y − Xβ‖²²,‖β‖²²是回归系数数组的 L2 范数(欧几里得范数),X 是设计矩阵。进一步的简化将此简化为以下:
‖y − Xβ‖²² + 𝝀 βT β
这可以通过使用调整参数的标量乘以单位矩阵来重写为闭式形式,以推导出岭回归系数估计 ˆβR,如下所示:
ˆβR = (X†X + 𝝀I)†XT y
简而言之,岭回归系数估计是使最小二乘回归输出最小化的系数集合如下:
ˆβR = argmin{∑i=1n(yi − ˆβ0 − ∑j=1pˆβjxij)² + 𝝀∑j=1pˆβj²}
在应用岭回归之前标准化系数
因为岭回归旨在最小化整个数据集的错误,并且用于此目的的调整参数是在对平方和的根进行归一化过程中应用 L2 范数,所以对于模型中的每个变量应用标准缩放器非常重要,这样所有变量都在相同的尺度上,在应用岭回归之前。如果不这样做,岭回归几乎肯定无法帮助模型在数据集之间泛化。
总结来说,岭回归通过使用 L2惩罚来减少模型中的方差,该惩罚惩罚系数的平方和。然而,需要注意的是 L2范数,因此岭回归永远不会产生零值的系数。因此,当分析师寻求在模型中使用所有项时,岭回归是减少方差的一个优秀工具。然而,岭回归不能用于变量消除。对于变量消除,我们可以使用 LASSO 回归收缩方法,它使用 L1惩罚来正则化系数,该惩罚使用 L1范数,它使用绝对值将值收缩到包括零。
让我们通过一个 Python 中岭回归实现的例子来了解一下。
首先,让我们从 scikit-learn 加载波士顿房价数据集。在最后一行,我们添加截距的常数。请注意,这个过程可以通过使用加利福尼亚房价数据集重复进行,尽管输入变量不同,但结果相似,可以通过运行 from sklearn.datasets import fetch_california_housing 代替 from sklearn.datasets import load_boston:
from sklearn.metrics import mean_squared_error as MSE
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_boston
import statsmodels.api as sm
import pandas as pd
boston_housing = load_boston()
df_boston = pd.DataFrame(boston_housing.data, columns = boston_housing.feature_names)
df_boston['PRICE'] = boston_housing.target
df_boston = sm.add_constant(df_boston, prepend=False
这里给出了前三条记录:

图 7.9 – 波士顿房价数据的前三条记录
我们将 PRICE 设置为目标。回想一下,岭回归在参数多于样本或存在过多方差时最有用。让我们假设这两个假设都成立:
X = df_boston.drop('PRICE', axis=1)
y = df_boston['PRICE']
如前所述,scikit-learn 的 StandardScaler 函数:
sc = StandardScaler()
X_scaled = sc.fit_transform(X)
接下来,让我们将数据分为 75/25 的训练/测试集。我们使用 shuffle=True 来随机打乱数据,以便测试一个随机样本,这更有可能代表总体:
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y, test_size=0.25, shuffle=True)
现在我们可以比较 fit_regularized,我们将 method 参数的必需参数设置为 'elastic_net'。我们将在稍后讨论弹性网络,但在此期间,请注意 L1_wt 参数在设置为 0 时应用岭回归。Alpha 是岭回归惩罚项中的调整参数 λ。小的 alpha 允许系数较大,而大的 alpha 将系数推向零。在这里,我们拟合训练数据以推导出均方误差:
ols_model = sm.OLS(y_train, X_train)
compiled_model = ols_model.fit()
compiled_model_ridge = ols_model.fit_regularized(method = 'elastic_net', L1_wt=0, alpha=0.1,refit=True)
print('OLS Error: ', MSE(y_train,
compiled_model.predict(X_train)) )
print('Ridge Regression Error: ', MSE(y_train,
compiled_model_ridge.predict(X_train)))
我们可以看到岭回归的误差略高
OLS 误差: ``530.7235449265926
岭回归误差: ``533.2278083730833
接下来,我们拟合测试数据以查看模型在未见数据上的泛化情况。我们再次使用均方误差进行测量:
print('OLS Error: ', MSE(y_test, compiled_model.predict(X_test)) )
print('Ridge Regression Error: ', MSE(y_test, compiled_model_ridge.predict(X_test)))
我们可以看到在测试数据上,两种方法的错误数量都增加了。然而,与岭回归方法相比,OLS 回归产生的误差略高:
OLS 误差: ``580.8138216493896
岭回归误差: ``575.5186673728349
在这里,我们可以观察到 图 7**.10 中的 OLS 回归系数和正则化系数:

图 7.10 – 岭回归正则化前后 OLS 回归系数
LASSO 回归
在本节前面,我们提到了变量系数的不必要高值如何导致模型方差,并且在这个过程中,减少了模型泛化的能力,如果系数合理,模型本可以更好地泛化。我们展示了如何应用岭回归来实现这一点。然而,岭回归的一个非常流行的替代方法是 LASSO 回归。LASSO 回归遵循向模型残差平方和添加惩罚项的类似程序,并试图最小化结果值(错误)。但是,LASSO 使用 L1 范数而不是 L2 范数。因此,当调整参数达到足够大时,可以获得绝对零系数值,从而作为特征选择和收缩工具。
LASSO 方程通过以下方法通过缩小每个变量的系数来最小化整体模型错误:
ˆβL = argmin{∑i=1n(yi − ˆβ0 − ∑j=1pˆβjxij)² + 𝝀∑j=1p|ˆβj|}
LASSO 与岭回归之间的唯一区别是惩罚项|ˆβj|。
选择λ
调整参数λ的值最好使用交叉验证来选择。虽然调整参数在理论上可以无限大,但通常从小于 1 的值开始,例如从 0.1 开始,以十分之一为增量增加到 1。之后,通常使用整数值。
使用与岭回归相同的数据,我们应用 LASSO 回归。在这里,我们设置L1_wt=1,表示将应用 L1 范数:
ols_model = sm.OLS(y_train, X_train)
compiled_model = ols_model.fit()
compiled_model_lasso = ols_model.fit_regularized(method='elastic_net', L1_wt=1, alpha=0.1,refit=True)
我们将遵循与岭回归相同的步骤来处理 LASSO,即我们首先检查训练数据上的错误,然后再次在测试数据上检查,以查看模型如何泛化:
print('OLS Error: ', MSE(y_train, compiled_model.predict(X_train)) )
print('LASSO Regression Error: ', MSE(y_train, compiled_model_lasso.predict(X_train)))
输出显示 OLS 回归略优于 LASSO。这可能是因为更高的方差。然而,从实际的角度来看,结果是一样的:
OLS 错误:530.7235449265926
LASSO 回归 错误:531.2440812254207
现在,我们需要检查模型在保留数据上的性能:
print('OLS Error: ', MSE(y_test, compiled_model.predict(X_test)) )
print('LASSO Regression Error: ', MSE(y_test, compiled_model_lasso.predict(X_test)))
我们可以看到,两个模型基本上具有相同的错误。然而,由于特征更少,LASSO 模型可能更容易被信任以泛化到未来的数据。然而,这取决于研究者的专业知识水平:
OLS 错误:546.1338399374557
LASSO 回归 错误:546.716239805892
与岭回归一样,一些系数在 L1 范数正则化过程中已经被最小化。然而,我们还可以看到,使用相同的 alpha 值,四个变量已经被最小化到零,从而完全从模型中消除。其中三个特征在岭回归中被相对缩小到几乎为零,但不是ZN,我们在图 7**.11中看到它已经被减少到0。模型错误略有改善,这可能看起来并不显著,但考虑到消除了四个变量,我们可以认为模型具有更多的泛化能力,对内生变量的依赖性更少。

图 7.11 – LASSO 回归正则化前后 OLS 回归系数
弹性网络
弹性网络是另一种常见的收缩方法,可以应用于管理偏差-方差权衡。这种方法将调整参数应用于岭回归和 LASSO 回归的组合,其中来自任一回归的影响比例由超参数α确定。弹性网络最小化的方程如下:
( ˆ β 0, ˆ β ) = argmin⎧ ⎪ ⎨ ⎪ ⎩ 1 _ 2n ∑ i=1 n (y i − ˆ β 0 − ∑ j=1 p ˆ β j x ij) 2 + λ( 1 − α _ 2 ∑ j=1 p ˆ β j 2 + α∑ j=1 p | ˆ β j|)⎫ ⎪ ⎬ ⎪ ⎭
自然地,根据α的值,弹性网络也可以生成绝对零值的系数参数估计,其中它抵消了岭回归惩罚项。当所有输入变量都需要时——例如,如果它们已经通过如本章特征选择部分中概述的程序进行了剪枝——弹性网络很可能优于岭回归和 LASSO 回归,特别是在数据集中有高度相关的特征必须包含以捕捉必要方差的情况下。在下一节中,我们将讨论降维。具体来说,我们提供了一个关于主成分回归(PCR)的深入概述,它使用主成分分析(PCA)从包含相关特征的系统提取有用信息,这些特征是评估目标所必需的。
首先,让我们通过使用与岭回归和 LASSO 回归相同的相同数据,来回顾一个使用弹性网络的回归示例。遵循之前的弹性网络最小化方程,我们设置Lt_wt=0.5,这意味着岭回归和 LASSO 回归之间的平衡是相等的,50/50。然而,不同之处在于,我们使用了alpha=8而不是在岭回归和 LASSO 回归中使用的0.1,以在普通最小二乘回归系数上获得改进。回想一下,当调整参数接近无穷大时,系数接近 0。因此,我们可以根据弹性网络系数得出结论,8 是一个非常高的调整参数:
ols_model = sm.OLS(y_train, X_train)
compiled_model = ols_model.fit()
compiled_model_elastic = ols_model.fit_regularized(method='elastic_net', L1_wt=0.5, alpha=8,refit=True)
让我们在训练数据上测试模型:
print('OLS Error: ', MSE(y_train, compiled_model.predict(X_train)) )
print('Elastic Net Regression Error: ', MSE(y_train, compiled_model_elastic.predict(X_train)))
在这里,我们可以看到与普通最小二乘回归相比,弹性网络向模型中添加了误差:
OLS 误差:530.7235449265926
弹性网络回归 误差:542.678919923863
现在,让我们检查保留数据的模型误差:
print('OLS Error: ', MSE(y_test, compiled_model.predict(X_test)) )
print('Elastic Net Regression Error: ', MSE(y_test, compiled_model_elastic.predict(X_test)))
观察图7.12 中的结果,我们可以看到弹性网络是如何在方差(这增加了训练误差)和偏差之间进行权衡的——偏差使得模型在保留数据上具有更好的泛化能力。与保留数据上的普通最小二乘回归相比,我们可以看到弹性网络有更好的结果。然而,误差的改善表明增加的偏差提供了降低误差的更好机会,但这并不是可以期待的事情:
OLS 误差:546.1338399374557
弹性网络回归 误差:514.8301731640446
在这里,我们可以看到弹性网络实施前后的系数:

图 7.12 – 弹性网络正则化前后 OLS 回归系数
当调整参数值足够大(alpha=8)时,我们可以看到,大多数平衡弹性网络中的系数都被迫变为绝对零。唯一剩下的系数与 OLS 回归相比具有相对较大的值。值得注意的是,对于保留系数的变量(RM和LSTAT),弹性网络增加了这两个系数,而岭回归和 LASSO 要么略微减少了它们。
降维
在本节中,我们将使用一种特定的技术——PCR——来研究 MLR。当我们需要处理多重共线性数据问题时,这种技术是有用的。多重共线性发生在独立变量高度相关于另一个独立变量时,或者一个独立变量可以在回归模型中从另一个独立变量预测出来。高相关性在拟合模型时可能会对结果产生不良影响。
PCR 技术基于 PCA,在无监督机器学习中用于数据压缩和探索性分析。其背后的思想是使用降维技术 PCA 对这些原始变量进行处理,以创建新的不相关变量。在这些新变量上获得的信息帮助我们理解它们之间的关系,然后应用 MLR 算法到这些新变量上。PCA 技术也可以用于分类问题,我们将在下一章中讨论。
PCA – 实践入门
PCA 是一种基于线性代数的降维技术,通过使用原始变量的线性组合线性变换数据到新的坐标系。这些新的线性组合被称为主成分(PCs)。PCA 技术与之前章节中提到的特征选择技术或收缩方法的区别在于,原始独立变量被保留,但新的 PC 变量被转换到新的坐标系中。换句话说,PCA 使用原始数据来达到新的表示或新的结构。PC 变量的数量与原始独立变量的数量相同,但这些新的 PC 变量之间是不相关的。PC 变量是从最大到最小变异量进行创建和排序的。原始独立变量和新的转换 PC 变量之间的方差和是相同的。
在进行 PCA 之前,我们通过对每个数据点减去均值并归一化每个独立变量的标准差对数据集进行预处理。在高级结构中,PCA 技术的目标是找到向量 v1, v2, … , vk,使得数据集中的任何数据点 x 都可以近似表示为以下线性组合:
x = ∑ i=1 k a_i v_i
对于一些常数 ai,其中 i = 1, k。假设我们有这些 k 个向量;那么,每个数据点都可以写成 Rk 空间中的一个向量,对应于投影:
x = <x, v1> ⋅ v1 + <x, v2> ⋅ v2 + … + <x, vk> ⋅ vk
换句话说,如果我们有 d 个原始独立变量,我们将构建一个 d × (k−1)维度的变换矩阵,可以将任何数据点映射到一个新的 k 维变量子空间,其中 k 小于 d。这意味着我们通过原始独立变量的线性变换进行了降维。PCA 有几个应用,但在这里,我们将引用一篇发表在Nature (2008)上的论文,欧洲内的基因镜像地理 (pubmed.ncbi.nlm.nih.gov/18758442/)。作者考虑了一个样本,其中 3,000 名欧洲个体在人类基因组中超过 500 万个可变 DNA 位点进行了基因分型;然后,每个个体使用超过 500 万个遗传标记来表示。这意味着它产生了一个维度大于 3,000 x 500,000 的矩阵。他们对数据集进行了 PCA 分析,以找到最有意义的向量 v1 和 v2(第一和第二成分),其中每个人只对应两个数字。作者根据两个数字在二维平面上的位置绘制了每个人的点,并根据他们来自的国家对每个点进行着色。

图 7.13 – 欧洲基因镜像地理的 PCA 分析

图 7.14 – 欧洲基因镜像地理的 PCA 分析
令人惊讶的是,PCA 技术在图表中表现良好,显示了与欧洲地图相似的遗传相似性。
在下一部分,我们将讨论如何在实际中利用 PCA 进行 PCR 分析。
PCR – 一项动手的薪资预测研究
要进行 PCR 分析,我们首先进行 PCA 以获得主成分,然后决定保留包含最多可解释变异量的前 k 个主成分。在这里,k 是新 PC 变量空间的维度。最后,我们在这些新变量上拟合多元线性回归(MLR)。
我们将考虑一个来自开源 Kaggle 数据的动手薪资预测任务 – www.kaggle.com/datasets/floser/hitters – 以说明 PCR 方法。如果想要跟随,请从 Kaggle URL 下载数据集:
- 设置和加载数据
导入在本研究中要使用的必要库,并加载 Hitters 数据。为了简单起见,我们将删除数据集中的所有缺失值。有 19 个独立变量(16 个数值型和 3 个分类型)以及目标变量‘Salary’。分类独立变量‘League’,‘Division’,和‘NewLeague’被转换为虚拟变量。我们在进行 PCA 步骤之前对特征进行预处理和标准化,并创建训练集和测试集:
# Import libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import scale
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import KFold, cross_val_score, train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.decomposition import PCA
#location of dataset
url = "/content/Hitters.csv"
#read in data
data = pd.read_csv(url).dropna() # to simply the analysis, we drop all missing values
# create dummies variables
dummies_variables = pd.get_dummies(data[['League', 'Division', 'NewLeague']])
# create features and target
target = data['Salary']
feature_to_drop = data.drop(['Salary', 'League', 'Division', 'NewLeague'],axis=1).astype('float64')
X = pd.concat([feature_to_drop, dummies_variables[['League_N', 'Division_W', 'NewLeague_N']]], axis=1)
#scaled data - preprocessing
X_scaled = scale(X)
# train test split
X_train, X_test, y_train, y_test = train_test_split(X_scaled, target, test_size=0.2, random_state=42)
- 生成所有主成分
作为下一步,我们为训练集生成所有主成分。这会产生 19 个新的 PC 变量,因为原始有 19 个独立变量:
# First generate all the principal components
pca = PCA()
X_pc_train = pca.fit_transform(X_train)
X_pc_train.shape
- 确定要使用的最佳主成分数量
下一步是执行 10 折交叉验证 MLR,并使用 RMSE 选择最佳主成分数量:
# Define cross-validation folds
cv = KFold(n_splits=10, shuffle=True, random_state=42)
model = LinearRegression()
rmse_score = []
# Calculate MSE score - based on 19 PCs
for i in range(1, X_pc_train.shape[1]+1):
rmse = -cross_val_score(model, X_pc_train[:,:i], y_train, cv=cv, scoring='neg_root_mean_squared_error').mean()
rmse_score.append(rmse)
# Plot results
plt.plot(rmse_score, '-o')
plt.xlabel('Number of principal components in regression')
plt.ylabel('RMSE')
plt.title('Salary')
plt.xlim(xmin=-1)
plt.xticks(np.arange(X_pc_train.shape[1]), np.arange(1, X_pc_train.shape[1]+1))
plt.show()
这是生成的图表:

图 7.15 – 主成分数量
从这里我们可以看到最佳主成分数量是 6,对应于最低的交叉验证 RMSE。
- 重新训练模型并进行预测
我们将使用这个数量在训练数据上训练一个回归模型,并在测试数据上进行预测:
# Train regression model on training data
model = LinearRegression()
model.fit(X_pc_train[:,:6], y_train)
pcr_score_train = -cross_val_score(model, X_pc_train[:,:6], y_train, cv=cv, scoring='neg_root_mean_squared_error').mean()
# Prediction with test data
X_pc_test = pca.fit_transform(X_test)[:,:6]
pred = model.predict(X_pc_test)
pcr_score_test = mean_squared_error(y_test, pred, squared=False)
注意,PCR 分析比特征选择或收缩方法更难解释结果,并且如果前几个主成分能够捕捉到最大可解释的变异性,这种分析的性能会更好。
摘要
在本章中,我们讨论了 MLR 的概念及其实现的相关主题。这些主题包括特征选择方法、收缩方法以及主成分回归(PCR)。使用这些工具,我们能够展示出减少模型过度方差风险的方法。在这样做的同时,我们也能够引入模型偏差,使得模型在未见数据上有更好的泛化机会,并尽可能减少过度拟合时经常遇到的问题。
在下一章中,我们将通过介绍逻辑回归开始对分类进行讨论,逻辑回归将 sigmoid 曲线拟合到线性回归模型中,以推导出二元类成员的概率。
第三部分:分类模型
在本部分,我们讨论了可以使用分类解决的问题类型、相关系数和决定系数、多元建模、模型选择以及使用正则化的变量调整。
它包括以下章节:
-
第八章,离散模型
-
第九章,判别分析
第八章:离散模型
在前两个章节中,我们讨论了预测连续响应变量的模型。在本章中,我们将开始讨论预测离散响应变量的模型。我们将首先讨论用于预测二元结果变量(具有两个级别的分类变量)的 probit 和 logit 模型。然后,我们将这个想法扩展到预测具有多个级别的分类变量。最后,我们将探讨预测计数变量,这些变量类似于分类变量,但只取整数值,并且有无限多个级别。
在本章中,我们将讨论以下主要主题:
-
Probit 和 logit 模型
-
多项式 logit 模型
-
泊松模型
-
负二项式回归模型
Probit 和 logit 模型
在之前,我们讨论了可以用回归模型解决的问题的不同类型。特别是,因变量是连续的,例如房价、薪水等。一个自然的问题是,如果因变量不是连续的——换句话说,如果它们是分类的——我们如何调整我们的回归方程来预测一个分类响应变量?例如,一家公司的人力资源部门想要进行员工流失率研究,以预测员工是否会留在公司,或者一家汽车经销商想要知道基于价格、车型、颜色等因素,一辆车是否能够售出。
首先,我们将研究二元分类。在这里,结果(因变量)是一个二元响应,如是/否或做/不做。让我们回顾一下简单的线性回归模型:
y = β 0 + β 1 x+ ϵ
在这里,预测的结果是一条穿过数据点的线。使用这个模型,我们可以构建一个模型,根据不同的自变量(如地区、单元、楼层或距离)来预测房价(因变量)。同样的想法不能应用于二元分类问题。让我们考虑一个简单的例子,我们只想根据房价预测房屋是否售出。让我们基于线性回归构建一个模型:
Sold = β 0 + β 1 * Price + ϵ (1)
在这里,如果房屋售出,Sold = 1,如果没有售出,Sold = 0。通过使用线性回归进行可视化,我们可以画出穿过数据点的最佳拟合线。看起来当价格增加时,房屋售出的可能性会降低:

图 8.1 – 使用线性回归的最佳拟合线
因此,我们不会考虑方程模型(1),而是考虑以下内容:
Prob(Sold) = β 0 + β 1 * Price + ϵ
在这里,Prob(Sold) 表示出售房屋的概率。概率值将在 0 和 1 之间;当出售房屋的概率等于不出售房屋的概率时,其范围的中间值为 0.5。然而,当房屋价格非常高时,Prob(Sold) 的值可以是负数。为了避免这个问题,我们可以用以下方程来建模问题:
P * (1 - P) = β_0 + β_1 * Price + ϵ (2)
在这里,P 是出售房屋的概率,1 - P 是不出售房屋的概率。P * (1 - P) 的值在 [0, ∞) 范围内,当出售汽车的概率与不出售汽车的概率相同时,(P=0.5),则 P * (1 - P) = 1,或者其范围的中间值为 1。这也被称为 优势比。要解释优势比,当 P * (1 - P) = 1 时,对于每出售一栋房屋,就有一栋房屋未出售。当 P = 0.75 时,优势比为 3。在这种情况下,解释是能够出售房屋的概率是未能出售房屋概率的三倍。回到方程 (2),优势比的范围是 [0, ∞),其下限为 0,但正如我们之前讨论方程 (1) 时所指出的,当房屋价格非常高时,优势比的估计值可能是负数,这与概率属性相矛盾。此外,由于中间值为 1 但优势比的范围是从 0 到无穷大,因此分布非常偏斜,右尾较长。然而,我们期望它符合线性回归的一个假设,即正态分布。因此,我们不会使用方程 (2),而是考虑以下:
log(odds) = log(P * (1 - P)) = β_0 + β_1 * Price + ϵ
log(P * (1 - P)) 的值在 (-∞, ∞) 范围内,中间值为 0。类似于线性回归,我们可以使用以下公式:
log(odds) = log(P * (1 - P)) = β_0 + β_1 * X_1 + β_2 * X_2 + … = z (3)
我们可以用此来解释解释变量与分类结果之间的关系。我们可以将之前的方程 (3) 重新写为以下形式:
F(z) = P = e^(z - 1) + e^z,
前面的公式下限为 0,上限为 1(预测结果发生的概率 P),这被称为 logit 模型。它是逻辑分布的累积分布函数。
模型二元因变量的概率的另一种方法是 probit 回归。我们使用标准正态分布的累积分布函数,而不是逻辑回归的累积分布函数:
F(z) = ∫_{-∞}^z ϕ(u) du = ϕ(z)。
前面的公式下限为 0,上限为 1(预测结果发生的概率 P),这被称为 probit 模型。记住以下:
ϕ(z) = P(Z ≤ z),其中 Z ~ 𝒩(0,1)。
logit 和 probit 模型都是通过使用 statsmodels 进行估计的,我们有 probit 和 logit 模型的类。这些类的文档可以在 www.statsmodels.org/dev/generated/statsmodels.discrete.discrete_model.Probit.xhtml 和 www.statsmodels.org/dev/generated/statsmodels.discrete.discrete_model.Logit.xhtml 找到。
现在,让我们创建一个训练数据集来展示如何使用 statsmodels 中的 Logit 类进行 logit 研究:
import pandas as pd
# create gpa train data
train = pd.DataFrame({'Admitted': [1, 1, 1,1, 1, 0, 1, 1, 0, 1,1,1, 1,1,0, 1, 0, 0, 0, 0, 0, 0, 0, 0 ,0 ,0, 1,1,1,1, 0],
'GPA': [2.8, 3.3, 3.7, 3.7, 3.7, 3.3, 3.7, 3, 1.7, 3.6, 3.3, 4, 3.2, 3.4, 2.8, 4, 1.5, 2.7, 2.3, 2.3, 2.7, 2.2, 3.3,3.3, 4, 2.3, 3.6, 3.4, 4, 3.7, 2.3],
'Exp': [8, 6, 5, 5, 6, 3, 4, 2, 1, 5, 5, 3, 6,5, 4, 4, 4, 1, 1, 2, 2, 2, 1, 4, 4, 4, 5, 2, 4, 6, 3]})
train.head()
在这里,Admitted 是因变量,有两种可能性(1 – 被录取和 0 – 未录取)。GPA 是 GPA 成绩,Exp 是经验年数。前面代码的输出如下:

图 8.2 – GPA 成绩和经验年数上的训练数据集
我们还必须为这个模型创建一个测试数据集:
test = pd.DataFrame({'Admitted': [1, 0, 1, 0, 1],
'GPA': [2.9, 2.4, 3.8, 3, 3.3],
'Exp': [9, 1, 6, 1,4 ]})
test.head()
得到的输出如下:

图 8.3 – GPA 成绩和经验年数上的测试数据集
我们将使用来自 statsmodels 的 logit:
import statsmodels.formula.api as smf
#fit logistic regression
model = smf.logit('Admitted ~ GPA + Exp', data =train).fit()
#summary
model.summary()
这将打印以下摘要:

图 8.4 – Logit 回归输出
从这个输出中,我们可以看到 GPA 和 Exp 在 α = 0.05 的显著性水平上是显著的。
[0.025 0.975] 下的值分别是 Intercept、GPA 和 Exp 的 95% 置信区间。下一步是使用 confusion_matrix 和 accuracy_score 来计算模型在测试集上的准确率:
from sklearn.metrics import confusion_matrix, accuracy_score, ConfusionMatrixDisplay
# X_test and y_test
X_test = test[['GPA', 'Exp']]
y_test = test['Admitted']
#
y_hat = model.predict(X_test)
pred = list(map(round, y_hat))
# confusion matrix
cm = confusion_matrix(y_test, pred)
ConfusionMatrixDisplay(cm).plot()
# Accuracy
print('Test accuracy = ', accuracy_score(y_test, pred))
输出如下:

图 8.5 – 测试数据集上的混淆矩阵
通过使用这些训练和测试数据集,模型可以完美地预测结果。在下一节中,我们将讨论使用与二元逻辑回归类似思想的多类回归。
多项 logit 模型
在实践中,有许多情况中,结果(因变量)不是二元的,而是有超过两种可能性。来自 statsmodels 的 MNLogit 类:www.statsmodels.org/dev/generated/statsmodels.discrete.discrete_model.MNLogit.xhtml。
爱丽丝数据(archive.ics.uci.edu/ml/datasets/iris)是教育中最著名的统计和机器学习数据集之一。自变量是萼片长度(厘米),萼片宽度(厘米),花瓣长度(厘米)和花瓣宽度(厘米)。因变量是一个有三个级别的分类变量:Iris Setosa(0),Iris Versicolor(1)和 Iris Virginia(2)。以下 Python 代码展示了如何使用sklearn和statsmodels进行这一操作:
# import packages
import numpy as np
import pandas as pd
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, accuracy_score, ConfusionMatrixDisplay
import statsmodels.discrete.discrete_model as sm
# import Iris data
iris = datasets.load_iris()
print(iris.feature_names)
print(iris.target_names)
#create dataframe
df = pd.DataFrame(iris.data, columns = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width'])
df['target'] = iris.target
df.head()
# check missing values
df.isna().sum()
# create train and test data
X = df.drop('target', axis=1)
y = df['target']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size =0.2, random_state =1)
# fit the model using sklearn
model_sk = LogisticRegression(solver = 'newton-cg', multi_class = 'multinomial')
model_sk.fit(X_train, y_train)
y_hat_sk = model_sk.predict(X_test)
pred_sk = list(map(round, y_hat_sk))
# confusion matrix
cm_sk = confusion_matrix(y_test, pred_sk)
ConfusionMatrixDisplay(cm_sk).plot()
# Accuracy
print('Test accuracy = ', accuracy_score(y_test, pred_sk))
#fit the model using statsmodels
model_stat = sm.MNLogit(y_train, X_train).fit(method='bfgs')
model_stat.summary()
y_hat_stat = model_stat.predict(X_test)
pred_stat = np.asarray(y_hat_stat).argmax(1)
# confusion matrix
cm_stat = confusion_matrix(y_test, pred_stat)
ConfusionMatrixDisplay(cm_stat).plot()
# Accuracy
print('Test accuracy = ', accuracy_score(y_test, pred_stat))
这两种方法都给出了相同的测试准确率值(96.67%),混淆矩阵如下所示:

图 8.6 – 使用 sklearn(左)和 statsmodels(右)的混淆矩阵
泊松模型
在上一节中,我们讨论了响应变量为分类变量的模型。在本节中,我们将探讨一个针对计数数据的模型。计数数据类似于分类数据(类别为整数),但是有无限多个级别(0、1、2、3 等等)。我们使用泊松分布来建模计数数据。在本节中,我们将首先检查泊松分布及其性质。然后,我们将使用泊松模型用解释变量对计数变量进行建模。
泊松分布
泊松分布由以下公式给出:
P(k) = λ^k e^(-λ) / k!
在这里,λ是事件的平均数量,k 是我们想要计算概率的事件数量。P(k)是 k 个事件发生的概率。这个分布用于计算在固定时间间隔或定义空间内发生 k 个事件的概率。
分布的形状随着λ值的改变而变化。当λ大于 10 时,分布看起来近似于正态分布。然而,当λ接近 0 时,分布变得右偏斜。这是因为计数数据不能为负。图 8.7*展示了三个泊松分布的例子,其平均值分别为 12、5 和 2。注意,平均值为 12 的分布近似于正态分布,而平均值为 5 和 2 的分布则是右偏斜的:

图 8.7 – 平均值为 12、5 和 2 的泊松分布示例
分布的另一个有趣方面是均值和方差相等。这意味着随着均值的增加,分布的分散度也会增加。我们可以在图 8.7*的例子中看到这一点。均值为 2 的分布具有较小的分散度和在均值处的大峰值,而均值为 12 的分布具有更宽的分散度和在均值处较低峰值。
现在我们已经讨论了泊松分布,让我们看看如何设置泊松模型。
计数数据建模
现在,让我们看看如何使用泊松模型对计数响应变量进行建模。如前所述,计数数据通常遵循泊松分布。泊松模型用以下数学公式表示:
y = e^b 0 + b 1x 1 + … + b nx n
在这里,y 是响应变量,b 值是模型系数,x 变量代表解释变量。这应该看起来与我们在上一章中使用的方程式相似,但增加了解释变量的指数。这种类型的模型称为对数线性模型,它是一种响应变量的对数由变量的线性组合建模的模型。我们可以通过将方程式的两边应用自然对数来重写此方程,使其更明确:
ln(y) = b 0 + b 1 x 1 + … + b n x n
现在,我们将响应变量的对数(ln(y))表示为解释变量的线性组合。
自然对数
泊松模型使用一种特殊的对数,称为自然对数。一个数的自然对数是以数学常数 e 为底的对数。自然对数通常写作 ln(x),log_e(x),或 log(x)(前两种是明确的,但第三种可能是有歧义的)。对数运算是对数的逆运算。在这种情况下,自然对数是对数函数的逆:ln(e^x) = x = e^ln(x)。自然对数和对数函数在统计学、数学和科学中常用。
让我们来看一个例子。我们将使用 UCI 的共享单车数据集(archive.ics.uci.edu/ml/datasets/bike+sharing+dataset)。在这个数据集中,我们有每天租赁自行车的数量。有两种租赁类型:预先注册(注册)和按需在地点租赁(因果)。在这个例子中,我们将对给定年份的每周平均随意租赁自行车数量进行建模。数据集提供了几个解释变量,包括环境因素,如温度,以及日历信息,如是否为假日。
我们将首先设置模型方程,然后查看使用statsmodels拟合模型的结果。模型方程遵循我们之前讨论的形式:
ln(每周 _ 平均 _ 租赁 _ 数量) = b 0 + b 1(温度) + b 1(季节)
- b 2(天气 _ 情况) + b 3(湿度) + b 4(风速) + b 5(假日)
我们可以使用statsmodels根据给定数据拟合此模型,这与我们在上一章中做的方法类似。拟合模型的代码片段如下(有关预处理详情,请参阅 Jupyter Notebook):
# select variables
X = df.groupby('isoweek').mean()[['atemp', 'season', 'weathersit','hum','windspeed', 'holiday']]
# transform holiday variable as an indicator that a holiday occurs within that week
X['holiday'] = X['holiday'].apply(lambda x: 1 if x > 0.1 else 0)
# add a constant for the model
X = sm.add_constant(X)
# get the response variable
y = df.groupby('isoweek').mean()['casual']
fit_model = sm.Poisson(y, X).fit()
fit_model.summary()
在模型拟合完成后,我们可以使用summary()方法获取系数的详细信息。对于这个模型,我们得到以下系数的输出:

图 8.8 – 泊松模型摘要
就像线性回归的建模示例一样,这些系数值是我们模型中列出的参数的估计值。模型中的所有解释变量在模型中似乎都显著。有趣的是,根据系数估计值,温度(atemp)似乎是最有影响力的因素,其次是湿度(hum)和风速。由于模型拟合良好且无需删除不显著的变量,我们可以评估模型的表现。此模型具有 155 的 MAE,对应于 36%的 MAPE。
泊松模型强烈依赖于响应变量具有泊松分布的假设。在下一节中,我们将查看一个类似类型的计数数据模型,但具有较弱的分布假设。
负二项回归模型
另一种有用的离散回归方法是对数线性负二项回归模型,它使用负二项概率分布。从高层次来看,负二项回归对于过度分散的计数数据很有用,其中计数条件均值小于计数条件方差。模型过度分散是指目标变量的方差大于模型假设的均值。在回归模型中,均值是回归线。我们根据目标变量计数分析(均值与方差)来决定使用负二项模型,并向负二项模型提供一个度量模型过度分散的量,以调整过度分散,我们将在下面讨论这一点。
重要的是要注意,负二项模型不仅用于建模简单的离散数据,而是专门用于与固定数量的随机试验相关的计数数据,例如在随机抽样场景中建模事件发生前(或未发生)的尝试次数。该模型仅对计数数据进行操作,其中目标变量的每个计数响应都基于有限的结果集。此外,由于计数数据是重复的二项试验的结果,计数排列的顺序并不重要。
负二项分布
以下是一个负二项分布的失败计数示例,条件均值为 17,条件方差为 52:

图 8.9 – 负二项分布示例
二项分布是固定次数随机试验(X = ( X 1, X 2, … ))中伯努利随机变量成功次数的构建,伯努利随机变量是一个具有两个结果值:0 或 1 的变量。负二项分布是伯努利随机变量固定次数随机抽取的失败次数的构建。这种区别很重要,因为使用二项回归模型的模型在观测之间建模二元结果,而负二项回归模型在观测之间建模计数结果。负二项分布是二项分布的逆。负二项分布的概率质量函数如下:
P(X − x) = (x + n − 1 n − 1 ) p n (1 − p) x
在这里,有一组 x + n − 1 次试验,其中 x 次失败和 n − 1 次成功,在第 x + n 次试验中达到成功。
关于泊松分布,负二项分布不需要严格遵循条件方差等于条件均值的假设,因为它包含一个额外的参数 α 来解释额外的方差,而泊松模型假设方差等于均值 μ。负二项模型假设方差,此处基于泊松-伽马混合分布,等于以下:
μ(1 + αμ)
这简化为以下形式:
(μ + α μ 2)
用成功概率 p 来表述,负二项分布的方差计算如下:
n 1 − p _ p 2
它具有以下均值:
n 1 − p _ p
由于负二项模型中方差不一定等于均值,因此当计数足够大,响应的方差超过均值时,负二项分布可能优于泊松方法。同样,与泊松一样,负二项分布是一个对数线性模型,置信区间基于 Wald 和 drop-in-deviance 似然比。负二项分布回归方程的形式,如此处所示,与泊松回归相同:
ln(y) = b 0 + b 1 x 1 + … + b n x n
它简化为以下形式:
y = e b 0 + e b 1x 1 + … + e b nx n
对于分布的给定样本,成功概率的最大似然估计如下:
n _ n + _ x ′
最大似然估计
最大似然估计(MLE)是统计建模中对数优势(或 logit)和对数似然方法的基础。似然是在给定特定的回归系数值的情况下,观察到的回归模型已知结果的概率。默认情况下,如果一组变量比另一组变量提供更高的观察结果概率,则该组变量将具有更高的似然。似然的对数被用作回归模型拟合优度的度量。对于每个变量的潜在系数值集合,具有每个变量最大对数似然值的系数集合被称为最大似然估计。这些值是通过迭代方法获得的,该方法生成多个可能的值。如果样本量足够大,并且已经为模型获得了一组适当的变量,则最大似然估计可以被认为是无偏的。
计算负二项回归的置信区间与逻辑回归类似。计算中使用的Wald方法利用了z 比率。在模型中有j个变量时,z 比率计算如下:
( ˆβj − βj) _ SE( ˆβj)
在这里,SE是标准误差。置信区间是变量的系数估计值加减半宽置信区间百分位数乘以系数估计值的标准误差。可以使用 z 比率,因为假设估计值具有标准正态抽样分布。因此,我们可以推导出变量的估计系数的 95%置信区间如下:
下限 95%置信限:
ˆβj − 0.475 × SE( ˆβj)
上限 95%置信限:
ˆβj + 0.475 × SE( ˆβj)
负二项回归有三个特定的假设要求:
-
样本间的独立性。
-
目标变量的对数与输入变量之间存在线性关系(对数线性模型)。
-
条件方差大于或等于条件均值。
样本间的独立性意味着没有序列相关性,也没有任何样本间的聚类或其他条件依赖性。目标变量的对数与输入变量之间存在线性关系意味着目标变量对数的对数与每个输入变量的变化之间的关系是线性缩放的。除了我们在本节开头讨论的第 3 个要求外,其他要求基本上与泊松模型相同。
让我们通过 Python 中的 statsmodels 库来举一个例子。为此,我们需要加载 statsmodels 库中的 affairs 数据集,使用剩余变量来建模儿童数量(children变量)。在第三行,我们必须添加生成截距系数所需的常数:
import statsmodels.api as sm
data = sm.datasets.fair.load().data
data = sm.add_constant(data, prepend=False)
首先,让我们通过数值方法确认目标变量中存在过度离散:
print('Mean count of children per marriage: ', data['children'].mean())
print('Variance of the count of children per marriage: ', data['children'].var())
我们可以看到方差大于均值:
每对婚姻的平均孩子数: 1.3968740182218033
每对婚姻孩子数的方差: 2.054838616333698
在这里,我们可以看到每对婚姻的条件均值小于条件方差。虽然这不是一个巨大的差异,但足以考虑使用负二项式回归。让我们直观地观察响应的分布:

图 8.10 – 孩子的分布
可以在这里看到数据的前五行。请注意,设计矩阵的第一列应始终包含常数:

图 8.11 – 示例数据集的前五条记录,包括添加的常数
在我们对图 8**.10中孩子分布的视觉检查中,我们识别出孩子的值为 5.5。这可能是由平均值计算或错误导致的。一个主题专家可能有助于确定这一点,但为了我们的分析,让我们假设这是一个错误,并将孩子数四舍五入到整数,因为人们不是分数。让我们设置目标数组y和设计矩阵X:
y = round(data['children'])
X = data[['const','age','religious','yrs_married','educ','occupation','occupation_husb','affairs','rate_marriage']]
现在,让我们为回归建模创建一个训练集和测试集分割。请注意,shuffle=True将提供不同的结果。为了获得代表性样本,数据应该随机打乱:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, shuffle=True)
因为负二项式模型基于泊松-伽马混合模型,我们需要使用泊松模型来估计模型的过度分散度量。A. Colin Cameron 和 Pravin K. Trivedi 在微观计量经济学:方法和应用中提供了一个称为辅助 OLS 回归(无常数项)的方法。作者提出了创建一个过度分散检验统计量,其中零假设是α=0,备择假设是α ≠ 0,其中α是过度分散的估计。辅助 OLS 回归公式如下:
( y i − ˆ μ i) 2 − y i _ ˆ μ i = α g( ˆ μ i) _ ˆ μ i + μ i
这里,μ i 是一个误差项,g( ˆ μ i) 是 ˆ μ i 2. 因此,右手边的操作数简化为 α ˆ μ i + μ i。在负二项式回归的术语中,我们考虑误差为零,因此我们可以将α作为过度分散估计因子。
在以下代码中,我们使用泊松模型对链接进行了广义线性模型的拟合。然后,我们使用模型的回归均值构建了估计的辅助目标变量。因为方法是“无常数项”,所以我们减去 1 以从过程中移除常数:
from statsmodels.formula.api import ols as OLS
import statsmodels.api as sm
poisson_model = sm.GLM(y_train, X_train, family=sm.families.Poisson()).fit()
df_aux = pd.DataFrame()
df_aux['y_mu_hat'] = poisson_model.mu
df_aux['children'] = y_train
df_aux['y_auxiliary'] = df_aux.apply(lambda x: ((x['children'] - x['y_mu_hat'])**2 - x['y_mu_hat']) / x['y_mu_hat'], axis=1)
ols_model = OLS('y_auxiliary ~ y_mu_hat - 1', df_aux).fit()
print(ols_model.params)
如我们所见,负二项式模型的估计分散度为0.622034。现在,我们需要评估辅助估计的统计显著性。我们可以使用 OLS 模型的 p 值来完成此操作:
print(ols_model.summary())
这将打印以下输出:

图 8.12 – OLS 回归结果
因为系数显著且大于 0,我们可以根据目标确认模型具有过度分散。该系数可以用作在负二项模型中衡量这种过度分散的指标,我们可以用它来调整此处alpha参数中的方差:
from statsmodels.genmod.families.family import NegativeBinomial
negative_binomial_model = sm.GLM(y_train, X_train, family=NegativeBinomial(alpha=ols_model.params.values)).fit()
print(negative_binomial_model.summary())
输出如下生成:

图 8.13 – 广义线性模型回归结果
最后,让我们使用我们在训练数据上构建的模型来预测训练数据,然后再在测试数据上预测,以便我们可以使用残差误差作为比较的基础来比较未见数据的泛化能力:
from sklearn.metrics import mean_squared_error as RMSE
print('Training Root Mean Squared Error: ', RMSE(y_train, negative_binomial_model.predict(X_train)) )
print('Testing Root Mean Squared Error: ', RMSE(y_test, negative_binomial_model.predict(X_test)))
我们可以从均方根误差中观察到,模型的性能在训练数据和测试数据中大致保持不变,表明模型是一致的:
训练均方根误差:1.2553439918425695
测试均方根误差:1.266620561303553
摘要
在本章中,我们解释了在严格基于线性回归构建二元分类概率模型时遇到负的原始概率的问题,其中期望概率在[0, 1]范围内。我们分别概述了使用标准正态分布和对数逻辑分布的累积分布函数的对数优势比和 probit 及 logit 建模。我们还展示了将逻辑回归应用于解决二元和多项分类问题的方法。最后,我们介绍了使用对数线性泊松和负二项模型进行的基于计数的回归,这些模型也可以在不做修改的情况下逻辑上扩展到速率数据。我们提供了它们实现的示例。
在下一章中,我们将介绍使用贝叶斯定理的条件概率,以及使用线性判别分析和二次判别分析进行降维和分类建模。
第九章:判别分析
在上一章中,我们讨论了离散回归模型,包括使用逻辑回归的分类。在本章中,我们将从概率概述开始,扩展到条件概率和独立概率。然后,我们将讨论这两种理解概率定律的方法如何构成贝叶斯定理的基础,贝叶斯定理直接用于扩展称为贝叶斯统计的方法。在此主题之后,我们将深入研究线性判别分析(LDA)和二次判别分析(QDA),这两种强大的分类器使用贝叶斯概率建模方法来建模数据。
在本章中,我们将涵盖以下主要内容:
-
贝叶斯定理
-
LDA
-
QDA
贝叶斯定理
在本节中,我们将讨论贝叶斯定理,该定理用于本章后面描述的分类模型。我们将从讨论概率的基本知识开始本章。然后,我们将探讨相关事件,并讨论贝叶斯定理如何与相关事件相关联。
概率
概率是衡量事件发生或特定结果发生的可能性的一种度量。通常,我们可以将事件分为两种类型的事件:独立事件和相关事件。事件类型之间的区别在于名称。独立事件是指不受其他事件发生或影响的事件,而相关事件则受其他事件发生或影响。
让我们考虑一些这些事件的例子。对于第一个例子,考虑一个公平的硬币抛掷。抛掷硬币可以导致两种状态之一:正面和反面。如果硬币是公平的,抛掷硬币得到正面或反面的概率是二分之一。这意味着在公平的硬币抛掷中,正面或反面的概率是 0.5。当事件可计数时,我们可以使用以下公式来计算概率:
P(E) = 期望结果的数量 / 总结果数量
在这里,P(E)是期望结果发生的概率。在抛硬币的例子中,期望结果是正面或反面,每个结果在每次抛掷中只能发生一次。因此,对于抛硬币,期望事件的数量是一个。总结果的数量是两个,因为抛硬币有两种可能的状态。将这些数字代入前面的方程,得到正面和反面的概率都是 0.5。现在,运用你的经验,之前的抛硬币是否给你关于下一次抛掷同一硬币的任何知识?它不会,因为每次抛掷都是相互独立的;它是一个独立事件。
现在,让我们考虑另一个例子。假设我们有一个装有三个红弹珠和五个蓝弹珠的袋子。让我们计算从袋子中抽取红弹珠的概率:P(红)。总共有八个弹珠,这是方程中的总结果数。我们想要抽取一个红弹珠,这意味着期望结果的数量是三个。这意味着从袋子中抽取红弹珠的概率是 3 / 8。现在,让我们计算从袋子中抽取蓝弹珠的概率:P(蓝)。袋中有五个蓝弹珠和八个总弹珠,所以从袋子中抽取蓝弹珠的概率是 5 / 8。注意,这两个概率的和是一:P(红) + P(蓝) = 3 / 8 + 5 / 8 = 1。这提出了概率的一个重要特性:总概率必须等于一。当然,这意味着一旦我们计算了抽取红弹珠的概率,我们就可以通过从一中减去抽取红弹珠的概率来计算抽取蓝弹珠的概率。概率的特性非常有用,并且在统计学和机器学习(ML)中经常使用。
回到红蓝弹珠的例子,让我们思考这个例子所代表的事件类型。我们从袋子中抽取一个弹珠。现在,有两个可能的选择:1)我们替换所抽取的弹珠,然后再次从袋子中抽取一个弹珠,2)我们不替换弹珠,然后从袋子中剩余的七个弹珠中抽取一个新的弹珠。第一种情况称为带替换的选择。带替换的选择是一个独立事件,因为每次从袋子中抽取之前袋子都会被重置。第二种情况称为不带替换的选择。不带替换的选择是相关事件所在的地方。第一次抽取弹珠的概率与之前描述的相同(一个独立事件)。然而,如果弹珠没有被替换,袋子的状态已经改变,下一次弹珠抽取的概率取决于上一次抽取的是哪一个。然后,在不带替换的选择中,每次后续的弹珠抽取都是一个相关事件。在讨论相关事件时,使用条件概率来描述事件是有用的。现在,让我们来讨论条件概率。
条件概率
让我们从正式的定义开始。当我们有相关事件时,我们可能对在事件 A 发生后事件 B 的概率感兴趣,这意味着我们想知道事件 A 发生后事件 B 的概率。这被称为事件 A 给定事件 B 的条件概率,表示为 P(B | A)。这可以理解为“在事件 A 已经发生的情况下事件 B 的概率。”这可能有点模糊,所以让我们从我们上一个例子来探讨条件概率。
假设事件 A 是抽出一个红弹珠,事件 B 是抽出一个蓝弹珠。我们想要找到在已经抽出一个红弹珠的条件下抽出一个蓝弹珠的条件概率,我们可以表示为 P(蓝 | 红)。让我们通过这个例子来解决这个问题。在从袋子里抽出一个红弹珠后,剩下七个弹珠:两个红弹珠和五个蓝弹珠。我们使用相同的方程来计算从新的集合中选择蓝弹珠的概率,这给我们带来了 5/7 的概率。这意味着在已经抽出一个红弹珠的条件下抽出一个蓝弹珠的条件概率是 5/7。
现在我们已经讨论了条件概率,让我们来看看从袋子里抽取两次的概率。正式地说,我们想要计算事件 A 和事件 B 的概率。继续跟随我们的例子,让我们计算先抽出一个红弹珠然后抽出一个蓝弹珠的概率。我们可以用以下方程来描述两个连续事件的概率:
P(A 和 B) = P(A) * P(B | A)
用话来说,A 后跟 B 的概率是事件 A 的概率乘以在事件 A 发生的条件下事件 B 的概率。在我们的例子中,我们已经计算了 P(红) 和 P(蓝 | 红)。我们可以使用这些值来计算先抽出一个红弹珠然后抽出一个蓝弹珠的概率 P(红和蓝),如下所示:
P(红和蓝) = P(红) * P(蓝 | 红) = 3/8 * 5/7 = 15/56 ≅ 0.27
进行计算后,我们发现 P(红和蓝) 大约是 0.27。
现在,出于好奇,让我们再次进行计算,但交换颜色的顺序;先抽出一个蓝弹珠然后抽出一个红弹珠的概率或 P(蓝和红)。遵循与上一个例子中相同的逻辑,我们将发现 P(蓝) = 5/8 和 P(红 | 蓝) = 3/7。因此,方程看起来是这样的:
P(蓝和红) = P(蓝) * P(红 | 蓝) = 5/8 * 3/7 = 15/56 ≅ 0.27
注意到 P(红和蓝) 和 P(蓝和红) 具有相同的概率。这并非巧合。这被称为贝叶斯定理,我们将在下一节讨论。
讨论贝叶斯定理
在上一节关于条件概率的部分,我们在从袋子里抽弹珠的例子中偶然遇到了贝叶斯定理。让我们更详细地讨论贝叶斯定理。
贝叶斯定理是统计学中一个称为贝叶斯统计的领域的基石。贝叶斯统计的核心思想是,给定一个先验概率,我们使用新信息来更新概率估计。虽然贝叶斯统计的完整描述超出了本书的范围,但我们将讨论贝叶斯定理与本书中讨论的概念的相关性。我们通常用以下方程来描述贝叶斯定理:
P(B|A) = P(A|B) * P(B) / P(A)
在贝叶斯术语中,P(B)被称为先验,P(B | A)被称为后验。后验给出了给定另一个事件时的更新。让我们用一个现实世界的例子来利用这个定理。
假设我们想要根据测试结果来确定某人是否患有疾病。我们可以在贝叶斯定理中这样表示:
P(sick|test pos) = P(test pos|sick)P(sick) __________________ P(test pos)
在这里,P(sick)是患病的概率,P(test pos)是获得阳性测试结果的概率,P(test pos|sick)是当测试的患者患病时的阳性测试结果的条件概率(也称为测试的准确度)。
对于这个例子,我们将假设患病的概率为 0.2,测试的准确度为 0.95,假阳性率为 0.3。在这个例子中,先验概率 P(sick)=0.2。我们可以通过测试来提高我们对某人是否患有疾病的认识。我们想要找到后验概率:P(sick|test pos)。
由于我们知道测试的准确度为 0.95,并且已知先验概率,我们已知方程分子中各项的值。为了找到 P(test pos),我们需要考虑所有可能获得阳性测试结果的情况,这可能是真阳性或假阳性。我们如下计算阳性测试结果的概率:
P(test pos) = P(test pos|sick)P(sick) + P(test pos|not sick)P(not sick)
回想一下,可能状态的总概率必须等于 1:P(not sick) = 1 − P(sick) = 0.8。考虑到这一点,我们可以计算 P(sick|test pos):
P(sick|test pos) = 0.95(0.2) ______________ 0.95(0.2) + 0.3(0.8) ≅ 0.44
给定一个测试结果,我们看到在额外信息的情况下,患者生病的可能性从 0.2 增加到 0.44。0.44 不是一个特别令人信服的结果。这主要是因为测试的假阳性率相对较高,为 0.3,尽管测试的准确度很高。如果我们还不信服,我们可以再次使用 0.44 作为新的先验概率来运行测试。假设第二次测试结果为阳性,我们会得到以下结果:
P(sick|test pos) = 0.95(0.441) __________________ 0.95(0.441) + 0.3(1 − 0.441) ≅ 0.71
第二次阳性测试提供了一个更有说服力的结果 0.71。我们可以继续迭代地通过结合额外的测试结果来改进患者生病的概率估计。这种概率估计的增量改进是贝叶斯统计的基本思想。
本节包含了对概率的介绍。我们涵盖了独立事件和依赖事件,然后提供了关于依赖事件的更多细节,这导致了贝叶斯定理的引入。在下一节中,我们将讨论一种使用贝叶斯定理的分类模型。
线性判别分析
在上一章中,我们讨论了逻辑回归作为一种分类模型,它利用线性回归直接对给定输入分布的目标分布概率进行建模。这种方法的另一种选择是 LDA。LDA 使用贝叶斯定理构建的决策边界来建模给定每个类别的输入变量分布的目标分布类成员概率。在我们有k个类别的情况下,使用贝叶斯定理,LDA 类成员的概率密度函数简单地为 P(Y = k|X = x),对于任何离散随机变量X。这依赖于变量X中观察值x属于第k个类别的后验概率。
在继续之前,我们必须首先注意 LDA 做出三个相关的假设:
-
每个输入变量都是正态分布的。
-
在所有目标类别之间,预测变量之间存在相同的协方差。换句话说,所有输入变量的共享方差是均匀的。因此,将每个输入变量标准化以具有 0 的均值和缩放到单位方差(标准差为 1)是有用的。
-
假设样本是独立的;随机抽样对于避免序列或****集群效应带来的复杂情况至关重要。
在满足这些假设的情况下,我们可以假设类概率密度函数是高斯(正态)分布的,因此,在一维形式(一个输入变量)中,我们有以下密度函数:
1 _ √ _ 2π σ k e −1 _ 2σ k 2(x−μ k) 2
这里,μ k 是第k个类别的均值,σ k 2 是第k个类别的方差。在多变量情况下,正态概率密度函数如下:
1 _ 2π p/2 √ _ |D| e −1 _ 2 (x−μ) TD −1(x−μ)
在这里,D是输入变量的协方差矩阵,p是输入变量的数量(或参数),π是先验概率。这里展示了后验概率的单变量计算(其中k对应于第k个类别,K对应于类别数量):
π k 1 _ √ _ 2π σ e −1 _ 2σ 2(x−μ k) 2 ____________ ∑ i=1 K π i 1 _ √ _ 2π σ e −1 _ 2σ 2(x−μ i) 2
对于多变量情况,计算如下:
π k 1 _ √ _ |D| e −1 _ 2 (x−μ) TD −1(x−μ) _______________ ∑ i=1 K π i 1 _ √ _ |D| e −1 _ 2 (x−μ) TD −1(x−μ) .
单变量线性判别函数,δ k(x),可以写成以下形式:
δ k(x) = x * μ k _ σ 2 − μ k 2 _ 2 σ 2 + log(π k)
这里,具有δ k(x)最大值的类别将其标签分配给给定的观察值。这使用了K个类别的贝叶斯决策边界:
x = μ 1 2 − μ 2 2 − … − μ K 2 ____________ K(μ 1 − μ 2) .
对于多变量情况,遵循相同贝叶斯决策边界的线性判别函数看起来如下:
δ k(x) = x T D −1 μ k − 1 _ 2 μ k T D −1 μ k + log(π k).
现在我们已经确定了概率密度函数,计算类别成员后验概率、贝叶斯决策边界和线性判别函数,我们可以理解为什么 LDA 所需的假设非常重要。虽然可以使用变换来使数据符合所需的假设,但这些变换也必须能够适应未来的数据集。因此,如果数据频繁变化,并且需要大量的变换来满足 LDA 在训练和测试中产生有用结果所需的参数假设,这可能不是这个任务的正确分类方法(例如,QDA 可能更有用)。然而,如果这些假设可以在研究者的舒适范围内得到满足,并且有足够的主题知识来确认这一点,LDA 是一个出色的算法,能够在非常大的数据集上产生高度可靠和稳定的结果,这由大的特征空间和高观测计数定义。LDA 在较小的数据集上表现也很好。
让我们看看一个实际例子。让我们从 statsmodels 加载 affairs 数据集,这样我们可以检查类别不平衡:
import statsmodels.api as sm
df_affairs = sm.datasets.fair.load().data
total_count = df_affairs.shape[0]
positive_count = df_affairs.loc[df_affairs['affairs'] > 0].shape[0]
positive_pct = positive_count / total_count
negative_pct = 1 - positive_pct
print("Class 1 Balance: {}%".format(round(positive_pct*100, 2)))
print("Class 2 Balance: {}%".format(round(negative_pct*100, 2)))
在这里,我们可以看到以下类别不平衡:
Class 1 Balance: 32.25%
Class 2 Balance: 67.75%
我们可以看到类别不平衡为 67.75/32.25。类别不平衡通常不会成为一个大问题,直到它接近 90/10,所以我们将保持现状,不做任何额外的工作,例如上采样或下采样。我们将任何大于 0 的 affairs 值重新编码为 1,使其成为一个二元分类问题:
df_affairs['affairs'] = np.where(df_affairs['affairs'] > 0, 1, 0)
让我们选择我们想要使用的特征:
X=df_affairs[['rate_marriage','age','yrs_married','children','religious','educ','occupation','occupation_husb']]
y=df_affairs['affairs']
请记住,对于 LDA 有效地产生线性分离边界,输入特征的标准差必须在两个类别中相同。使用下面显示的方法,我们可以看到所有输入特征在两个目标类别中都共享相同的标准差,除了 rate_marriage。这里的标准差差异大约为 20%。考虑到我们有八个输入特征,并且模型中的所有特征都将进行缩放,这很可能不会成为一个问题。如果模型表现不佳,根据这个信息,我们可以合理地假设这不是算法的错,因为所需的假设已经得到满足。相反,更有可能的是我们没有足够多的特征或观测值来完全解释目标。我们排除了 affairs,因为那是目标变量,以及 occupation 和 occupation_husb,因为它们是分类编码,由于我们根据分析范围不认为它们是序数,所以将进行独热编码:
df_affairs_sd = pd.concat([X, y], axis=1)
for col in df_affairs_sd.columns:
if col not in ['affairs','occupation','occupation_husb']:
print('Affairs = 0, Feature = {}, Standard Deviation = {}'.format(col, round(np.std(df_affairs_sd.loc[df_affairs_sd['affairs'] == 0, col]), 2)))
print('Affairs = 1, Feature = {}, Standard Deviation = {}'.format(col, round(np.std(df_affairs_sd.loc[df_affairs_sd['affairs'] == 1, col]), 2)))
Affairs = 0, Feature = rate_marriage, Standard Deviation = 0.82
Affairs = 1, Feature = rate_marriage, Standard Deviation = 1.07
Affairs = 0, Feature = age, Standard Deviation = 6.81
Affairs = 1, Feature = age, Standard Deviation = 6.7
Affairs = 0, Feature = yrs_married, Standard Deviation = 7.1
Affairs = 1, Feature = yrs_married, Standard Deviation = 7.18
Affairs = 0, Feature = children, Standard Deviation = 1.42
Affairs = 1, Feature = children, Standard Deviation = 1.41
Affairs = 0, Feature = religious, Standard Deviation = 0.89
Affairs = 1, Feature = religious, Standard Deviation = 0.84
Affairs = 0, Feature = educ, Standard Deviation = 2.21
Affairs = 1, Feature = educ, Standard Deviation = 2.09
在上一章中,我们确定occupation和occupation_husb在解释婚外情方面没有捕捉到太多差异,因此我们将从数据集中删除occupation_husb以最小化 one-hot 编码的体积,如下所示:
import pandas as pd
pd.options.mode.chained_assignment = None
X['occupation'] = X['occupation'].map({1:'Occupation_One',
2:'Occupation_Two',
3:'Occupation_Three',
4:'Occupation_Four',
5:'Occupation_Five',
6:'Occupation_Six'})
X = pd.get_dummies(X, columns=['occupation'])
X.drop('occupation_husb', axis=1, inplace=True)
让我们构建一个包含 33%总体数据的测试集的训练/测试分割:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
我们希望保留原始数据的一个副本,但如前所述,我们还需要对数据进行中心化和缩放,以便 LDA 有最佳成功的机会。因此,我们将复制X数据并对其进行缩放。然而,请注意,我们不想缩放 one-hot 编码的数据,因为 one-hot 编码不能缩放并保留其意义。因此,我们将使用 scikit-learn 的ColumnTransformer()管道函数将StandardScaler()应用于除了 one-hot 编码列之外的所有列,如下所示:
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler
X_train_sc = X_train.copy()
X_test_sc = X_test.copy()
ct = ColumnTransformer([
('', StandardScaler(), ['rate_marriage','age','yrs_married','children','religious','educ'])], remainder='passthrough')
X_train_sc = ct.fit_transform(X_train_sc)
ct = ColumnTransformer([
('', StandardScaler(), ['rate_marriage','age','yrs_married','children','religious','educ'])], remainder='passthrough')
X_test_sc = ct.fit_transform(X_test_sc)
现在,让我们将 LDA 模型拟合到训练数据上。我们将模型拟合到训练数据上,然后使用它来预测训练数据以获得性能基准。然后,我们将使用该模型来预测测试数据,以查看模型在未见数据上的泛化能力如何。
按照如下方式拟合数据:
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
lda = LinearDiscriminantAnalysis()
lda.fit(X_train_sc, y_train)
在这里,我们构建函数来测量精确度和召回率。精确度是一个基本告诉你模型在找到目标(affairs = 1)的正值方面做得有多好,并且不包含其他任何值(affairs = 0)。召回率是一个实际告诉你模型在找到所有目标正值方面做得有多好,不管它找到多少其他值。理想的情况是这两个指标都非常高。然而,这可能并不可能。如果两者都不是非常高的,使用案例应该确定哪个指标更重要:
def precision_score(true_positives:int, false_positives:int):
precision = true_positives / (true_positives + false_positives)
return precision;
def recall_score(true_positives:int, false_negatives:int):
recall = true_positives / (true_positives + false_negatives)
return recall;
现在,让我们运行模型并验证性能:
from sklearn.metrics import confusion_matrix
import seaborn as sns
y_train_pred = lda.predict(X_train_sc)
cf_train = confusion_matrix(y_train, y_train_pred, labels=[0,1])
tn_train, fp_train, fn_train, tp_train = cf_train.ravel()
cf_matrix = sns.heatmap(cf_train, annot=True, fmt='g', cbar=False)
cf_matrix.set(xlabel='Predicted', ylabel='Actual', title='Confusion Matrix - Train');
print('Precision on Train: ', round(precision_score(tp_train, fp_train), 4))
print('Recall on Train: ', round(recall_score(tp_train, fn_train), 4))
在这里,我们可以看到我们的模型在训练数据上的结果:
训练集精确度: 0.6252
训练集召回率: 0.3444
在这里,我们可以使用混淆矩阵可视化性能,以比较训练数据上的实际目标值与预测值:

图 9.1 – LDA 训练数据的混淆矩阵
让我们在测试数据上重复这个过程:
y_test_pred = lda.predict(X_test_sc)
cf_test = confusion_matrix(y_test, y_test_pred, labels=[0,1])
tn_test, fp_test, fn_test, tp_test = cf_test.ravel()
cf_matrix = sns.heatmap(cf_test, annot=True, fmt='g', cbar=False)
cf_matrix.set(xlabel='Predicted', ylabel='Actual', title='Confusion Matrix - Test');
print('Precision on Test: ', round(precision_score(tp_test, fp_test), 4))
print('Recall on Test: ', round(recall_score(tp_test, fn_test), 4))
在这里,我们可以看到我们的模型在测试数据上的结果:
测试集精确度: 0.6615
测试集召回率: 0.3673
在这里,我们可以使用混淆矩阵可视化性能,以比较测试数据上的实际目标值与预测值:

图 9.2 – LDA 测试数据的混淆矩阵
从这些结果中,我们可以看到使用我们在训练数据上构建的模型,在训练集和测试集上,分数在精确度和召回率方面是一致的,因此我们可以得出结论,与基准(训练)性能相比,该模型在未见过的数据上具有良好的泛化能力。然而,性能并不出色。问题可能在于我们的目标编码并不十分有用(也许事务的离散化并不有用,应该采用统计方法来离散化值)。然而,也有可能特征没有充分解释response变量中的方差,以至于无法构建一个非常有用的模型(也可能事务根本无法预测,并且有太多的随机噪声难以建模)。然而,为了本章的目的,我们能够展示如何构建和测试使用 LDA 的模型,以及在进行数据建模之前必须采取的预处理步骤。
监督维度降低
与 PCA 不同,PCA 可以用于执行无监督的维度降低,而 LDA 可以用来执行监督的维度降低。也就是说,在训练模型以学习输入方差与输出目标之间的关系后,可以得到一组派生特征。这些差异在以下图中展示:

图 9.3 – 维度降低:LDA 与 PCA 的比较
假设我们使用 LDA 进行分类时数据产生了良好的结果。那么,我们可以自信地使用相同的数据来构建一个监督维度降低技术。为了执行这个操作,我们会使用fit_transform()方法来转换输入数据,使其与目标相关(而不是我们之前用来拟合分类器的fit()函数)。与分类器一样,数据仍然应该符合 LDA 模型的假设。数据根据响应变量的类别数量进行降低。当类别数量为C时,降低后的特征数量将是C-1。因为我们目标变量y中只有两个类别,所以 LDA 将输入特征降低到一维:
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
lda = LinearDiscriminantAnalysis()
X_reduced = lda.fit_transform(X_train_sc, y_train)
我们可以看到数据现在已经被简化为一维特征:
print('Input data dimensions: ', X_train_sc.shape)
print('Transformed data dimensions: ', X_reduced.shape)
在这里,我们可以看到我们的数据从 12 个特征降低到了 1 个:
输入数据维度:(``4265, 12)`
转换后数据维度:(``4265, 1)`
二次判别分析
在上一节中,我们讨论了 LDA。每个类中的数据需要来自多变量高斯分布,并且协方差矩阵在不同类之间是相同的。在本节中,我们考虑另一种类型的判别分析,称为 QDA,但 QDA 的假设可以在协方差矩阵假设上放宽。在这里,我们不需要协方差矩阵在不同类之间相同,但每个类都需要有自己的协方差矩阵。对于观察到的每个类,仍然需要具有类特定均值向量的多变量高斯分布来执行 QDA。我们假设来自第 k 类的观察值满足以下公式:
X~N(μ k, Σ k)
因此,我们将考虑以下生成分类器:
p(X | y = k, θ) = N(X | μ k, Σ k)
然后,其相应的类后验是:
p(y = k | X, θ) ∝ π k N(X | μ k, Σ k).
在这里,π k = p(y = k) 是类 k 的先验概率。在文献中,这被称为高斯判别分析(GDA)。然后,通过计算 k 个类别的对数后验来计算判别函数,如下所示:
logp(y = k | X, θ) = log π k − log|2π Σ k| _ 2 − (X − μ k) T Σ k −1(X − μ k) _______________ 2 + C
在这里,C 是一个常数。与 LDA 不同,X 的数量在前一个公式中表现为二次函数,这被称为 QDA。LDA 或 QDA 的选择与偏差-方差权衡有关。如果假设 k 个类共享相同的协方差矩阵并不好,那么在 LDA 中高偏差可能很重要。换句话说,当类之间的边界是线性的时,使用 LDA,而在类之间非线性边界的情况下,QDA 表现更好。
现在,我们考虑一个例子,使用我们在上一章研究的相同的Iris数据集。要执行 QDA,一个选项是使用 sklearn 中的QuadraticDiscriminantAnalysis。与上一章一样,我们使用train_test_split函数创建一个训练集和一个测试集(80%对 20%),然后在训练集上拟合一个 QDA 模型,并使用此模型进行预测:
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis
# fit the model using sklearn
model_qda = QuadraticDiscriminantAnalysis(store_covariance=True)
model_qda.fit(X_train, y_train)
y_hat_qda = model_qda.predict(X_test)
pred_qda = list(map(round, y_hat_qda))
这里显示了与测试集相关的混淆矩阵:

图 9.4 – QDA 混淆矩阵
对于这个数据集,准确率是 100%。这个分析基于所有变量(花瓣长度(cm),花瓣宽度(cm),花瓣长度(cm),花瓣宽度(cm)),以及三个因变量(目标):setosa,versicolor,和virginica。为了可视化教育目的,我们将只考虑使用花瓣长度和花瓣宽度作为自变量的二维分析:

图 9.5 – 花瓣长度 和 花瓣宽度
使用以下 Python 代码可以重现花瓣长度和花瓣宽度之间的二维可视化:
import matplotlib.pyplot as plt
FIGSIZE = (8,8)
Xax= np.array(df1["sepal_length"])
Yax= np.array(df1["sepal_width"])
labels= np.array(df1["target"])
cdict={0:'red',1:'green', 2 :'blue'}
labl={0:'setosa',1:'versicolor', 2: 'virginica'}
marker={0:'*',1:'o', 2:'p'}
alpha={0:.3, 1:.5, 2:.3}
fig,ax=plt.subplots(figsize=FIGSIZE)
fig.patch.set_facecolor('white')
for l in np.unique(labels):
ix=np.where(labels==l)
ax.scatter(Xax[ix],Yax[ix],c=cdict[l],s=40,
label=labl[l],marker=marker[l],alpha=alpha[l])
plt.xlabel("Sepal Length",fontsize=14)
plt.ylabel("Sepal Width",fontsize=14)
plt.legend()
plt.show()
可视化如下:

图 9.6 – 爱丽丝花种类的散点图
观察到类别之间(setosa、versicolor和virginica)没有线性分离,因此 QDA(二次判别分析)比 LDA(线性判别分析)是一个更好的方法。
min和max值分别为sepal_length的(4.3, 7.9)和sepal_width的(2.0, 4.4):

图 9.7 – sepal_length和sepal_width的最小和最大值
我们使用仅包含这两个变量sepal_length和sepal_width的所有观测值,在爱丽丝数据集上训练一个新的 QDA 模型,如下所示:
df1['species'] = df1['target']
df1['species'] = df1['species'].apply(lambda x: 'setosa'
if x==0 else('versicolor' if x==1 else 'virginica'))
X = df1.drop(['target','species'], axis=1)
y = df1['target']
model_qda = QuadraticDiscriminantAnalysis(store_covariance=True)
model_qda.fit(X,y)
然后,我们创建一个通用的数据集,包含 500 个观测值,用于预测每个变量的最小和最大范围内的sepal_length和sepal_width:
sepal_length = np.linspace(4, 8, 500)
sepal_width = np.linspace(1.5, 4.5, 500)
sepal_length,sepal_width = np.meshgrid(sepal_length, sepal_width)
prediction = np.array([model_qda.predict( np.array([[x,y]])) for x,y in zip(np.ravel(sepal_length), np.ravel(sepal_width)) ]).reshape(sepal_length.shape)
以下 Python 代码用于可视化类别之间的边界分离:
fig = sns.FacetGrid(df1, hue="species", size=8, palette = 'colorblind').map(plt.scatter, "sepal_length", "sepal_width").add_legend()
figax = fig.ax
figax.contourf(sepal_length,sepal_width, prediction, 2, alpha = .1, colors = ('red','green','blue'))
figax.contour(sepal_length,sepal_width, prediction, 2, alpha = 1, colors = ('red','green','blue'))
figax.set_xlabel('Sepal Length')
figax.set_ylabel('Sepal Width')
figax.set_title('QDA Visualization')
plt.show()
我们得到以下结果图:

图 9.8 – 爱丽丝数据集的 QDA 决策边界
setosa与其他两个类别之间有很好的分离,但versicolor和virginica类别之间没有。在更高维度的类别之间分离会更好,这意味着在这个例子中,我们考虑所有四个独立变量:sepal_length、sepal_width、petal_length和petal_width。换句话说,这种分析最好在四个维度上进行,但对于人类来说,不可能有一个四维的可视化。
摘要
在本章中,我们首先概述了概率论。我们讨论了条件概率和独立概率之间的区别,以及贝叶斯定理如何利用这些概念为概率建模提供独特的方法。接下来,我们讨论了 LDA,其假设以及如何使用该算法将贝叶斯统计应用于分类建模和监督降维。最后,我们介绍了 QDA,当线性决策边界无效时,它是 LDA 的替代方案。
在下一章中,我们将介绍时间序列分析的基础知识,包括对这种方法在回答统计问题方面的深度和局限性的概述。
第四部分:时间序列模型
本部分的目标是学习如何分析和创建单变量和多变量时间序列数据的预测。
它包括以下章节:
-
第十章,时间序列简介
-
第十一章,ARIMA 模型
-
第十二章,多元时间序列
第十章:时间序列简介
在第九章中,判别分析,我们通过引入贝叶斯定理的条件概率、线性判别分析(LDA)和二次判别分析(QDA)来结束我们对统计分类建模的概述。在本章中,我们将介绍时间序列、其背后的统计概念以及如何在日常分析中应用它们。我们将通过区分时间序列数据与本书到目前为止所讨论的内容来引入这个主题。然后,我们概述了时间序列建模可以期待的内容以及它可以实现的目标。在时间序列的背景下,我们重新引入了均值和方差统计参数,以及相关性。我们概述了线性差分、互相关和自回归(AR)以及移动平均(MA)属性,以及如何使用自相关函数(ACF)和偏自相关函数(PACF)图来识别它们的阶数。之后,我们概述了入门级白噪声模型。我们以对平稳性概念的详细、正式概述来结束本章,平稳性可以说是成功进行时间序列预测的最重要先决条件之一。
在本章中,我们将涵盖以下主要内容:
-
什么是时间序列?
-
时间序列分析的目标
-
统计测量
-
白噪声模型
-
平稳性
什么是时间序列?
在本章和接下来的几章中,我们将处理一种称为时间序列数据的数据类型。到目前为止,我们一直在处理独立数据——也就是说,由不相关的样本组成的数据。时间序列通常是同一样本随时间的测量,这使得此类数据中的样本相关。我们周围每天都有许多时间序列。时间序列的一些常见例子包括每日温度测量、股票价格波动和海洋潮汐的高度。虽然时间序列不需要在固定间隔进行测量,但在这本书中,我们将主要关注固定间隔的测量,例如每日或每秒。
让我们来看看一些符号。在下面的方程中,我们有一个变量 x,它在时间上被反复采样。下标枚举了采样点(从样本 1 到样本 t),整个样本系列表示为 X。下标值通常被称为变量的滞后。例如,x²可以被称为变量 x 的第二个滞后:
X = x₁, x₂, … xₜ₋₁, xₜ
通常,x 点可以是单变量或多变量。例如,我们可以取一段时间内的温度读数,这将导致一个单变量时间序列,意味着每个 x 项将对应一个单独的温度值。我们也可以取一个更全面的天气读数集,如温度、湿度、降雨量和阳光,这将导致一个多变量时间序列,意味着每个 x 项将对应温度、湿度、降雨量和阳光值。
我们在本章和第十一章ARIMA 模型中开始讨论时间序列,但就像回归章节一样,我们将从单变量方法开始,然后转向处理多变量时间序列。正如回归章节一样,我们可能会发现时间序列中的多个变量与感兴趣变量的结果相关。在第十二章多变量时间序列中,我们将处理多变量时间序列的额外复杂性,并将第十一章中讨论的单变量模型扩展到第十二章中的多个变量。在本章中,我们将介绍时间序列的基本知识。
时间序列通常具有一个称为序列相关性的性质,这意味着先前的知识可以提供一些关于时间序列未来的知识。我们可以通过比较时间序列的当前值与序列中的先前值来确定当前值是否与先前值相关,从而衡量序列相关性。这种相关性也称为自相关性。在本章的后面,我们将讨论更多关于自相关性的内容,包括如何确定一个序列是否表现出自相关性。在本章的后面,我们将探讨如何进行计算以确定数据是否表现出序列相关性以及如何计算自相关性。然而,让我们首先讨论我们希望通过时间序列分析实现的目标。
时间序列分析的目标
时间序列分析有两个目标:
-
识别时间序列中的任何模式
-
预测时间序列的未来值
我们可以使用时间序列分析方法来揭示时间序列的本质。在最基本层面上,我们可能想知道一个序列是否看起来是随机的,或者时间序列是否显示出某种模式。如果一个时间序列有模式,我们可以确定它是否有季节性行为、周期性模式或表现出趋势行为。我们将通过观察和模型拟合的结果来研究时间序列的行为。模型可以提供关于序列本质的见解,并允许我们预测时间序列的未来值。
时间序列分析的另一个目标是预测。我们在许多常见情况下都能看到预测的例子,例如天气预报和股价预测。重要的是要记住,本书中涵盖的预测方法并非完美无缺。在传达预测模型的结果时,应格外小心。模型预测总是不确定的。预测应始终与对模型不确定性的理解相结合。我们将通过使用预测区间和模型错误率来努力强化这一概念。
现在,在了解我们希望通过时间序列分析实现的目标的背景下,让我们开始探讨分析单变量时间序列的工具。
统计测量
当使用时间序列模型处理序列相关数据集时,我们需要理解在时间背景下的均值和方差,除了自相关和互相关。理解这些变量有助于建立对时间序列模型如何工作以及它们何时比不考虑时间的模型更有用的直觉。
均值
在时间序列分析中,一个时间序列的样本均值是该序列中每个时间点的所有值的总和除以值的数量。其中 t 代表每个离散的时间步长,n 是总的时间步长数,我们可以如下计算时间序列的样本均值:
_ X = 1 _ n ∑ t=1 n x t
生成时间序列的过程有两种类型;一种是有遍历性的过程,另一种是非遍历性的。有遍历性的过程具有独立于时间的恒定输出,而非遍历性的过程则不一定在时间上有恒定的输出。有遍历性的过程的样本均值随着样本量的增加会收敛到真实总体均值。然而,非遍历性的过程的样本均值不会随着样本量的增加而收敛;一个过程输出的一端到端的样本均值可能不会像另一个相同过程的一端到端的样本均值那样收敛到过程总体均值。非遍历性过程的例子之一是,由于湿度或振动等因素导致输出质量下降时需要频繁校准的机器。本章中介绍的工具,以及下一章中扩展的工具,将帮助分析师克服由过程驱动的时间序列数据中这种自然约束带来的局限性。
在时间序列分析中,平均值通常被称为信号。就预测而言,平均值必须随时间保持恒定。我们在第六章“简单线性回归”中讨论了这一点——我们将在本章和下一章中进一步讨论——第一阶差分的概念,它是一种低通线性滤波器,用于去除输出中的高频数据并通过低频数据。当信号不是恒定的,例如它单调增加或减少时,通常可以应用第一阶差分——并且根据需要重复应用——以产生一个恒定的值。这是平稳时间序列的一个要求。我们将在本章的“平稳性”部分讨论平稳性的所有组成部分。一旦平均值是恒定的,就可以评估其周围的方差以进行自相关分析。如果平均值周围的方差存在自相关,我们可以生成模型来学习产生该过程的模式。我们还可以预测该过程未来的模式。具有恒定平均值和无自相关的模型通常可以使用白噪声模型使用平均值进行预测。对于序列中的所有时间t,第一阶线性差分的公式如下:
Yt′ = Yt − Yt−1.
由于第一阶差分是数值微分,它会导致从时间序列中移除一个数据点,因此必须在任何建模之前应用。以下是一个表格数据中第一阶差分的示例:
| 原始数据 | 第一阶差分 |
|---|---|
| 1.7 | |
| 1.4 | -0.3 |
| 1.9 | 0.5 |
| 2.3 | 0.4 |
| 2.1 | -0.2 |
图 10.1 – 表格数据中的第一阶差分
在这里,我们有一个使用第一阶差分转换的数据示例。可以使用numpy的diff()函数,其中n=1指定第一阶差分:
numpy.diff(array_x, n=1)
方差
方差是衡量给定分布中数据围绕平均值分散程度的统计量。在时间序列分析的背景下,方差随时间分布在平均值周围。如果我们有一个离散且平稳的过程,我们可以按以下方式计算样本均值的方差:
Var(X) = σ2n ∑k=−(n−1)n−1(1 − |k|n)ρk
在这里,n 是时间序列的长度,k 是要包含在序列自相关(序列相关)计算中的滞后数,ρ_k 是该回望范围的自相关。这种方差计算可以通过模型构建获得,我们将在第十一章**,ARIMA 模型中介绍。在模型构建——即我们为时间序列构建特征方程的地方——测量方差变得最为重要。否则,我们关注自相关以建立对方差及其产生过程的直觉。通常,我们有什么被称为白噪声方差,这是从随机过程中生成的随机方差分布。白噪声方差在时间范围内没有自相关。在白噪声方差的情况下,我们只有以下方差计算,这与非序列相关数据的计算相同:
Var( _ X _ ) = σ² _ n
表现出白噪声方差的数据通常可以用平均值来建模,因为没有相关误差。然而,可能需要进行变换才能使用平均值,例如一阶差分或季节性差分。
在时间序列分析中,用于评估序列方差是否为白噪声的常见假设检验是 Ljung-Box 测试,由 Greta Ljung 和 George Box 创建。Ljung-Box 测试有以下假设:
-
H_0 : 数据点独立分布,没有序列相关误差
-
H_a : 数据点不是独立分布的,因此存在序列相关误差
此测试可以应用于任何时间序列模型的残差——例如,使用时间作为输入的线性模型或自回归积分移动平均(ARIMA)模型。如果 Ljung-Box 测试的结果是验证零假设,则假定测试的模型是有效的。如果拒绝零假设,可能需要不同的模型。Ljung-Box 测试统计量如下所示:
Q = n(n + 2)∑ k=1 h ˆρ k² _ n − k
在这里,n 是样本大小,k 对应于测试中的每个滞后,h 是正在测试的总时间范围,ˆρ_k 是每个滞后的样本自相关。该测试遵循卡方(χ²)分布,因此比远期滞后的更重视近期滞后。Ljung-Box 测试统计量与具有 h 个自由度的χ² 分布进行比较,如下所示:
Q > χ^(1−α)_{h,2}
在这里,h是测试的滞后。如果 Q 统计量大于χ 2 临界值,则拒绝零假设。Ljung-Box 测试也可以应用于没有模型的数据,以测试它是否是具有白噪声方差的零均值数据。否则,测试是在模型的残差上进行的。让我们生成一个具有均值为 0 和标准差为 1 的 1,000 个数据点的随机正态分布样本,以便我们可以使用 Ljung-Box 测试来检查数据是否是平稳 白噪声:
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.graphics.tsaplots import plot_acf
random_white_noise = np.random.normal(loc=0, scale=1, size=1000)
我们可以根据原始数据观察到均值是恒定的。我们还可以看到,自相关结构似乎没有显著的滞后,这是一个强烈的迹象,表明方差是随机分布的白噪声。在模型开发中,这种缺乏自相关是您希望从残差中看到的,以帮助验证模型很好地拟合了过程的数据:
fig, ax = plt.subplots(1,2, figsize=(10, 5))
ax[0].plot(random_white_noise)
ax[0].axhline(0, color='r')
ax[0].set_title('Raw Data')
plot_acf(random_white_noise, ax=ax[1])
在Figure 10**.2中,我们可以看到原始白噪声数据和 ACF 图,该图在滞后期之间显示出没有统计上显著的自相关:

Figure 10.2 – 随机白噪声的视觉分析
现在,让我们使用 Ljung-Box 测试来检查我们对自相关的假设。我们使用statsmodels中的acorr_ljungbox()函数进行此测试。我们应用lags=[50]参数来测试自相关是否在 50 个滞后期之外为 0:
from statsmodels.stats.diagnostic import acorr_ljungbox
acorr_ljungbox(random_white_noise, lags=[50], return_df=True)
测试返回了一个不显著的 p 值,如这里所示。因此,我们可以断言,在 95%的置信水平下,数据没有自相关,因此是白噪声:
lb_stat |
lb_pvalue |
|
|---|---|---|
50 |
51.152656 |
0.428186 |
Figure 10.3 – 白噪声数据自相关 Ljung-Box 测试结果
自相关
如我们之前所提到的,自相关,也称为序列相关,是衡量数据时间序列中对应于先前点的给定点的相关性的度量。它被称为“自相关”,因为它指的是变量在先前滞后期与其自身的相关性。在指定范围内所有先前滞后期的自相关(而不是特定滞后期的自相关)被称为自相关结构。在这里,对于任何大于零的给定滞后k,我们有自相关函数,r k:
r k = ∑t=k+1n(yt − _y)(yt−k − _y) __________________ ∑t=1n(yt − _y)2
我们在第第六章 简单线性回归中讨论了自相关,是在识别自相关作为线性回归所需假设的样本独立性的违反的背景下。在这里,我们指出自相关是时间序列数据的核心组成部分。我们之前在第六章和本章中使用了 ACF 图来直观地探索了这些数据。我们还讨论了一阶差分。让我们使用statsmodels中的美国宏观经济数据集深入探讨这两个概念,我们在第六章中使用了这个数据集。在这里,我们选择realinv和realdpi,将这两个变量都转换为 32 位浮点数:
import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
df = sm.datasets.macrodata.load().data
df['realinv'] = round(df['realinv'].astype('float32'), 2)
df['realdpi'] = round(df['realdpi'].astype('float32'), 2)
df_mod = df[['realinv','realdpi']]
接下来,让我们使用 50 个滞后(lags=50)和 5%的显著性水平(alpha=0.05)来绘制数据和其 ACF:
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.graphics.tsaplots import plot_pacf
fig, ax = plt.subplots(2,2, figsize=(15,10))
plot_acf(df_mod['realinv'], alpha=0.05, lags=50, ax=ax[0,1])
ax[0,1].set_title('Original ACF')
ax[0,0].set_title('Original Data')
ax[0,0].plot(df_mod['realinv'])
plot_acf(np.diff(df_mod['realinv'], n=1), alpha=0.05, lags=50, ax=ax[1,1])
ax[1,1].set_title('Once-Differenced ACF')
ax[1,0].set_title('Once-Differenced Data')
ax[1,0].plot(np.diff(df_mod['realinv'], n=1))
我们得到以下结果图:

图 10.4 – realinv:原始数据和一阶差分数据及其 ACF
我们可以在图 10.4 的第一幅图中观察到,投资整体呈上升趋势的确定性信号,并且可以观察到数据周围的一些方差,尤其是在接近滞后 0 的时间附近。这些数据成分是我们最初尝试建模的。在原始数据的 ACF 中,我们可以看到一个显著的、衰减的自相关结构,这是指数增长的典型特征。然而,由于主导自相关的强烈趋势,我们无法观察到关于方差相关性的任何信息,例如潜在的季节性等。为了消除强烈的信号,我们使用numpy.diff()函数应用一阶差分,这样我们就可以观察到方差的 autocorrelation 结构。观察差分数据的 ACF,我们可以看到在应用 95%置信水平时,方差的自相关延伸到滞后 3。
在这一点上,值得提及自回归移动平均(ARMA)模型,我们将在第十一章 ARIMA 模型中使用它。正如我们在 ACF 图中可以看到的,存在一个长的自相关范围。回到我们之前提到的自相关方程,我们可以看到计算并没有控制时间序列中特定的滞后,因为每个点的误差在除以方差之前都被加在一起。这就是为什么原始 ACF 显示了衰减效应;合理地假设滞后 0 不是与每个之前的滞后平滑地序列相关的。因为 ACF 没有控制特定滞后之间的自相关,我们可以使用这种相关性信息来构建一个移动平均函数,这有助于在长时间段内建模数据的噪声成分。
然而,如果我们想要能够构建一个模型成分,使我们能够在 ARMA 模型的框架内定义点与点之间的关系,我们还将想要观察 PACF,因为它确实控制了滞后之间的相关性。注意在图 10.5中原始数据和它的 PACF。它显示的粒度水平与 ACF 图大不相同;而在 PACF 中,我们直到接近滞后 30 时才看到与滞后 0 以及滞后 4 之后的持续显著相关性,而在 ACF 中,我们直到接近滞后 20 时才看到显著相关性。然而,原始数据并不很有帮助,因为它不是平稳的,这就是为什么我们看到滞后 0 和滞后 45 之间的相关性几乎与滞后 0 和滞后 1 之间的相关性一样多,这表明我们可能无法构建一个不经过变换(在这种情况下是一阶差分)就能收敛的模型:

图 10.5 – realinv:原始数据和一阶差分数据以及偏自相关函数(PACF)
图 10.4 中对于转换(差分)数据的 ACF 结果表明,一个高达 3 阶(MA(3))的移动平均成分可能是有用的(尽管 MA(1)可能更好)。图 10.5 中对于转换数据的 PACF 结果表明,一个一阶自回归成分(AR(1))可能是有用的。使用 PACF,我们还可以得出 AR(10)可能是有用的结论,但选择如此高的阶数通常会导致过度拟合。一个经验法则是,在过程建模中不要使用阶数大于大约 5 的 AR 或 MA 成分。然而,模型阶数取决于分析师和过程细节。虽然我们将在第十一章**,ARIMA 模型中深入讨论这个概念,如果我们为这些数据构建 ARIMA 模型,我们将有一个根据我们选择的带有积分差分d的一阶差分构建的模型,即AR(p)和MA(q)结构,如下所示(p,d,q)模型。这些阶数用于根据模型构建特征多项式方程,然后我们可以使用这些方程的因式分解形式的根来评估平稳性和可逆性,这有助于识别模型唯一性并评估其收敛到解的能力。
交叉相关
继续前进,对于任何大于零的给定滞后 k,我们有一个交叉相关函数(CCF),它可以用来识别两个变量在不同时间点之间的相关性。这种分析可以用来帮助识别领先或滞后指标。对于给定滞后 k 的两个一维向量 i 和 j 的交叉相关函数 ˆ p i,j(k) 如下所示:
ˆ p i,j(k) = ∑ t=1 n−k (x t,i − _ x i)(x t+k,j − _ x j) _______________________ √ ____________ ∑ t=1 n (x t,i − _ x i) 2 √ _____________ ∑ t=1 n (x t+k,j − _ x j) 2
在考虑线性回归的案例中,我们有两个按顺序排列的基于序列的输入变量预测一个按顺序排列的基于序列的因变量时,我们同时评估输入和输出变量,即t。然而,如果使用交叉相关来识别领先或滞后指标,我们可以识别出领先变量领先时的滞后,即k,并通过k个索引修改其序列位置。例如,如果我们的模型的时间单位是周,并且我们构建一个回归模型来预测使用广告作为输入的销售,我们可能会发现,在任何给定的一周中,广告对销售没有影响。然而,在执行交叉相关分析后,我们发现广告和销售之间存在 1 周的强烈滞后,这意味着在 1 周内投资的广告直接影响到下一周的销售。然后我们使用这个信息将广告支出变量提前 1 周,并重新运行回归模型,利用广告支出和销售之间的强相关性来预测市场行为。这可以被称为滞后效应。
让我们用一个 Python 的实际例子来看一下。首先,我们需要评估数据。让我们加载数据开始。我们需要将值转换为float类型。我们可以四舍五入到两位小数,并选择realinv和realdpi,分别是实际国内总投资和实际私人可支配收入的变量:
import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
df = sm.datasets.macrodata.load().data
df['realinv'] = round(df['realinv'].astype('float32'), 2)
df['realdpi'] = round(df['realdpi'].astype('float32'), 2)
df_mod = df[['realinv','realdpi']]
在绘制数据后,我们可以看到两个序列似乎都有一种强烈的、确定性的信号,这是自相关性的主要影响因素。可以很容易地论证这两个变量是正相关的,并且两者都在随时间增加,这在很大程度上是正确的。然而,在这一点上,数据还有更多值得注意的地方;均值(信号)是确定的,但我们需要评估均值周围的方差,才能真正理解两个过程在趋势成分之外的关联程度。注意,在下面的代码中,我们在计算 ACF 时使用了 50 个滞后。在测量 ACF 时的一般规则是不要超过 50 个滞后点,尽管这可能会根据上下文而有所不同:
from statsmodels.graphics.tsaplots import plot_acf
fig, ax = plt.subplots(2,2, figsize=(20,8))
fig.suptitle('Raw Data')
ax[0,0].plot(df_mod['realinv'])
ax[0,0].set_title('Realization')
ax[1,0].set_xlabel('realinv')
ax[0,1].plot(df_mod['realdpi'])
ax[0,1].set_title('Realization')
ax[1,1].set_xlabel('realdpi')
plot_acf(df_mod['realinv'], alpha=0.05, lags=50, ax=ax[1,0])
plot_acf(df_mod['realdpi'], alpha=0.05, lags=50, ax=ax[1,1])
我们得到了以下图表:

图 10.6 – 比较 realinv 与 realdpi:原始数据和 ACF 图
我们可以在图 10.6中看到,除了强烈的确定性信号外,两个变量的 ACF 图都有指数衰减的自相关,在 95%的置信水平(在阴影区域外)上是显著的。基于 ACF 中的这些信息,我们应该至少执行一阶差分。如果 ACF 在执行一次一阶****线性差分后表现出相同的行为,我们可能需要执行两次一阶差分。
让我们创建差分数据:
df_diff = pd.DataFrame()
df_diff['realinv'] = np.diff(df_mod['realinv'], n=1)
df_diff['realdpi'] = np.diff(df_mod['realdpi'], n=1)
现在,重新使用之前的绘图代码,我们可以看到图 10.7中的数据:

图 10.7 – 比较差分realinv与差分realdpi
根据图 10.7,我们可以看到之前在图 10.6中主导自相关性的确定性信号现在已被移除。有几个点稍微超出了 95%的置信区间,尽管显著性水平很小(我们不考虑滞后 0,因为滞后 0 与自身 100%相关)。由于一些自相关仍然存在,我们可以认为它们的行为并非完全随机。因此,它们可能是互相关的。由于我们已经解决了确定性信号的问题,我们现在可以评估两个序列之间的互相关性。请注意,两个序列的均值已经被差分到常数零。这是平稳性的一个条件。另一个条件是常数方差。我们可以看到差分数据具有最小的自相关性,但它超出了 95%的置信限制。因此,我们可以确定额外的 ARIMA 建模可能是有用的,但我们也可以主张使用平均值进行建模。然而,我们也可以看到平稳性的第三个要求——期间内常数协方差——似乎没有满足;随着序列随时间步长继续,方差波动。我们将在下一节中更深入地讨论平稳性。现在,让我们将注意力转向互相关性。
现在我们已经移除了确定性信号,方差在 ACF 图中占主导地位,让我们比较这两个时间序列,看看它们之间是否存在滞后或领先关系。在这里,我们构建一个 CCF 来图形化地展示这一点。我们使用字典zscore_vals来构建 z 分数,从而构建三个置信区间选项——90%,95%,和 99%:
from scipy.signal import correlate
import matplotlib.pyplot as plt
def plot_ccf(data_a, data_b, lag_lookback, percentile):
n = len(data_a)
ccf = correlate(data_a - np.mean(data_a), data_b - np.mean(data_b), method='direct') / (np.std(data_a) * np.std(data_b) * n)
_min = (len(ccf)-1)//2 - lag_lookback
_max = (len(ccf)-1)//2 + (lag_lookback-1)
zscore_vals={90:1.645,
95:1.96,
99:2.576}
plt.figure(figsize=(15, 5))
markers, stems, baseline = plt.stem(np.arange(-lag_lookback,(lag_lookback-1)), ccf[_min:_max], markerfmt='o', use_line_collection = True)
plt.setp(baseline, color='r', linewidth=1)
baseline.set_xdata([0,1])
baseline.set_transform(plt.gca().get_yaxis_transform())
z_score_95pct = zscore_vals.get(percentile)/np.sqrt(n) #1.645 for 90%, 1.96 for 95%, and 2.576 for 99%
plt.title('Cross-Correlation')
plt.xlabel('Lag')
plt.ylabel('Correlation')
plt.axhline(y=-z_score_95pct, color='b', ls='--')# Z-statistic for 95% CL LL
plt.axhline(y=z_score_95pct, color='b', ls='--')# Z-statistic for 95% CL UL
plt.axvline(x=0, color='black', ls='-')
;
import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
df = sm.datasets.macrodata.load().data
df['realinv'] = round(df['realinv'].astype('float32'), 2)
df['realdpi'] = round(df['realdpi'].astype('float32'), 2)
df_mod = df[['realinv','realdpi']]
df_diff = pd.DataFrame()
df_diff['realinv'] = np.diff(df_mod['realinv'], n=1)
df_diff['realdpi'] = np.diff(df_mod['realdpi'], n=1)
plot = plot_ccf(data_a=df_diff['realdpi'], data_b=df_diff['realinv'], lag_lookback=50, percentile=95)
在图 10.8中,我们可以观察到许多点超过了我们应用的 95%置信区间。然而,我们观察到最高的相关性水平是在滞后 0 处。根据数据域或模型应用时的预测误差,可能会有多个正确答案,但使用统计数据,我们的研究表明,序列在滞后 0 处相关性最高,因此,它们既不是领先指标也不是滞后指标。重要的是要注意,大约 0.20 的相关性的实际意义很低;它解释了很少的方差。因此,短期影响很小:

图 10.8 – realinv与realdpi之间的互相关
由于我们已经评估出两个时间序列方差之间的短期交叉相关性最小,而我们可能仍然对整体的相关强度感兴趣,因此在此阶段我们可以使用皮尔逊相关系数来比较这两个趋势,这将衡量两个长期线性趋势之间的相关性。回想一下皮尔逊相关系数的公式,以观察其与长期平均数的关系:
r = ∑ i=1 n (x i − _ x)(y i − _ y) _________________________ √ _______________________ ∑ i=1 n (x i − _ x) 2 ∑ i=1 n (y i − _ y) 2
假设,然而,我们的数据中存在一个领先指标。让我们将realdpi向前移动一个位置。注意这里的pandas函数shift()执行此操作:
plot = plot_ccf(data_a=df_diff['realdpi'].shift(1).iloc[1:], data_b=df_diff['realinv'].iloc[1:], lag_lookback=50, percentile=95)
我们得到以下结果:

图 10.9 – 带有领先指标的位移交叉相关性
在将realdpi向前移动一个位置后,我们可以在图 10.9中识别出realinv现在是一个领先指标,滞后一个单位。如果这是我们原始的、差分后的数据,我们可能会决定对realinv变量应用一个位移——同时考虑到相关性的实用性——然后在用一个变量预测另一个变量的值时使用realdpi和位移后的realinv变量。
白噪声模型
任何时间序列都可以被认为是处理两个基本元素:信号和噪声。我们可以用以下数学公式表示这一点:
y(t) = signal(t) + noise(t)
信号是我们可以用数学函数建模的可预测模式。但时间序列中的噪声元素是不可预测的,因此不能建模。以这种方式思考时间序列会导致两个重要的观点:
-
在尝试建模之前,我们应该验证时间序列不是与噪声一致的。
-
一旦我们对时间序列拟合了一个模型,我们应该验证残差是与噪声一致的。
关于第一个问题,如果一个时间序列与噪声一致,那么就没有可预测的模式可以建模,尝试建模时间序列可能会导致误导性的结果。关于第二个问题,如果一个时间序列模型的残差与噪声不一致,那么我们还可以进一步建模其他模式,并且当前模型不足以解释信号中的模式。当然,为了做出这些评估,我们首先需要了解噪声是什么。在本节中,我们将讨论白噪声模型。
白噪声是一个时间序列,其中样本是独立的,具有固定的方差和零均值。这意味着时间序列的每个样本都是随机的。那么,我们如何评估一个序列是否是随机的呢?让我们通过一个例子来分析。看看图 10.10中的序列。你认为这个序列是随机的还是信号?

图 10.10 – 一个样本时间序列
图 10.10 中的时间序列是使用 numpy 生成的随机序列。仅从观察时间序列中无法看出这一点。让我们看看如何确定一个序列是否为噪声。
如前所述,时间序列中的样本是独立的。这意味着样本不应自相关。我们可以通过 ACF 图来检查这个序列的自相关性。结果如图 图 10.11 所示:

图 10.11 – 图 10.10 中时间序列的 ACF 图
ACF 图显示该序列似乎没有自相关性。这是一个强烈的迹象,表明该时间序列没有可建模的模式,可能仅仅是噪声。
另一种评估方法是 Ljung-Box 自相关检验。这是对时间序列滞后自相关性的统计检验。零假设是没有自相关性,备择假设是有相关性。由于自相关性可能出现在时间序列的任何滞后中,Ljung-Box 检验为每个滞后提供了自相关的 p 值。对这个序列进行检验,我们得到前 10 个滞后的以下 p 值:[0.41, 0.12, 0.21, 0.31, 0.44, 0.53, 0.5, 0.57, 0.26, 0.2]。这些值中的每一个都很大,这表明这个序列很可能是噪声。
之前讨论的两种方法都表明显示的序列是噪声,这是正确的结果(我们知道这个序列是随机生成的)。随着我们进行时间序列建模,我们将使用这些方法来确定一个序列是否为噪声,作为模型评估的一部分。我们将以对时间序列平稳性的概念讨论来结束这一章。
静态性
在本节中,我们概述了静态和非静态时间序列。总的来说,这两种类型时间序列的主要区别在于统计特性,如均值、方差和自相关性。在静态时间序列中,这些特性不会随时间变化,但在非静态时间序列中会随时间变化。特别是,具有趋势或季节性的时间序列是非静态的,因为趋势或季节性会影响统计特性。以下示例说明了静态与非静态时间序列的行为 [1]:

图 10.12 – 静态和非静态时间序列的示例
为了检查静态特性,我们将检查以下三个条件:
- 均值与时间无关:
E[X t] = μ 对于所有 t
- 方差与时间无关:
Var[X t] = σ 2 对于所有 t
- 与时间无自相关性——X t 1 和 X t 2 之间的相关性仅取决于它们在时间上的距离,t 2 − t 1
现在,我们将使用Air Passengers数据集(www.kaggle.com/datasets/chirag19/air-passengers)进行 Python 分析,该数据集提供了 1949 年至 1960 年美国航空公司乘客的月度总计,可以从Kaggle下载,也可以在本书的 GitHub 仓库中找到(github.com/PacktPublishing/Building-Statistical-Models-in-Python/blob/main/chapter_10/airline-passengers.csv)。首先,我们将数据导入 Python 笔记本,将索引类型更改为datetime,并绘制数据集:
import pandas as pd
import matplotlib.pyplot as plt
data = pd.read_csv('airline-passengers.csv', header=0, index_col =0)
data.index = pd.to_datetime(data.index, format='%Y-%m-%d')
plt.plot(data)

图 10.13 – 1949-1960 年美国航空公司乘客可视化
从图中,我们可以看到这里存在趋势和季节性效应。然后,它显然是非平稳的。在statsmodels包中,有一个名为seasonal_decompose的函数,可以帮助我们将原始数据分解成不同的图表进行可视化。您可以在以下操作中看到它:
from statsmodels.tsa.seasonal import seasonal_decompose
season_trend = seasonal_decompose(data)
season_trend.plot()
plt.show()
我们得到以下结果图:

图 10.14 – 1949-1960 年美国航空公司乘客趋势和季节性可视化
趋势图显示,使用美国航空公司的乘客数量随时间增加。它似乎季节性地波动,夏季达到峰值。数据点和时间之间存在一定的依赖性,方差似乎在早期较小(20 世纪 50 年代初使用美国航空公司的乘客较少)和后期较大(20 世纪 50 年代末使用服务的乘客较多)。这些观察结果表明,条件 1 和 2(均值和方差随时间保持恒定)被违反。从非恒定方差的不同角度来看,我们可以为 1949 年至 1960 年的每一年创建箱线图进行可视化,如下所示:
import seaborn as sns
fig, ax = plt.subplots(figsize=(24,10))
sns.boxplot(x = data.index.year,y = data['Passengers'], ax = ax, color = "cornflowerblue")
ax.set(xlabel='Year', ylabel='Number of Passengers')
对于前面的代码,我们得到以下结果:

图 10.15 – 1949-1960 年美国航空公司乘客箱线图
要检查自相关,我们使用以下代码:
from statsmodels.graphics.tsaplots import plot_acf
plot_acf(data, lags= 20, alpha=0.05)
plt.show()
我们得到的结果如图图 10.16所示:

图 10.16 – 1949-1960 年美国航空公司乘客的 ACF 可视化
然而,趋势在数据中占主导地位。在去除趋势并重新运行代码后,我们得到图 10.17自相关函数(ACF)图。似乎存在一个强烈的季节性成分。自相关周期在经过一些滞后计数和时间步长后重复:

图 10.17 – 1949-1960 年美国航空公司乘客数量的 ACF 可视化
我们在方差和自相关部分讨论的 Ljung-Box 检验也可以用来检查自相关。使用该测试,我们得到lb_pvalue = 0。因此,数据存在自相关。
摘要
本章从时间序列的介绍开始。我们概述了时间序列是什么以及如何用它来满足特定目标。我们还讨论了区分时间序列数据与不依赖于时间的数据的标准。我们还讨论了平稳性,哪些因素对平稳性很重要,如何衡量它们,以及如何解决不存在平稳性的情况。从那里,我们能够理解 ACF 和 PACF 分析的主要功能以及如何通过围绕均值的方差对过程进行推断。此外,我们还介绍了时间序列建模,概述了白噪声模型以及自回归和移动平均组件背后的基本概念,这些有助于形成 ARIMA 和季节性自回归积分移动平均(SARIMA)时间序列模型的基础。
在第十一章**中,ARIMA 模型,我们将进一步深入讨论自回归、移动平均和 ARMA 模型,包括概念概述和 Python 中的逐步示例。我们还研究了集成 SARIMA 模型,以及评估这些模型拟合度的方法。
参考文献
[1] André Bauer,自动化混合时间序列预测:设计、基准测试和用例,芝加哥大学,2021 年。
第十一章:ARIMA 模型
在本章中,我们将讨论单变量时间序列模型。这些模型只考虑一个变量,并仅基于时间序列中的先前样本创建预测。我们将首先查看平稳时间序列数据的模型,然后过渡到非平稳时间序列数据的模型。我们还将讨论如何根据时间序列的特征识别适当的模型。这将提供一组强大的模型,用于时间序列预测。
在本章中,我们将涵盖以下主要主题:
-
稳定时间序列模型
-
非平稳时间序列模型
-
模型评估的更多内容
技术要求
在本章中,我们使用两个额外的 Python 库进行时间序列分析:sktime和pmdarima。请安装以下版本的这些库以运行提供的代码。有关安装库的说明,请参阅第一章,采样 和泛化。
-
sktime==0.15.0 -
pmdarima==2.02
关于sktime的更多信息可以在以下链接中找到:www.sktime.org/en/stable/get_started.xhtml
关于pmdarima的更多信息可以在以下链接中找到:alkaline-ml.com/pmdarima/
稳定时间序列模型
在本节中,我们将讨论对平稳数据有用的自回归(AR)、移动平均(MA)和自回归移动平均(ARMA)模型。这些模型在建模过程均值周围的模式和方差时很有用。当我们有不具有自相关性的数据时,我们可以使用不假设时间的统计和机器学习模型,例如逻辑回归或朴素贝叶斯,只要数据支持此类 用例。
自回归(AR)模型
AR(p)模型
在第十章,时间序列简介中,我们考虑了偏自相关函数(PACF)如何将一个数据点与另一个滞后点相关联,同时控制这些滞后点之间的差异。我们还讨论了检查 PACF 图是评估自回归模型顺序的常用方法。因此,自回归模型是一种考虑过去特定点与零滞后给定点的值直接相关的模型。假设我们有一个具有随机、正态分布的白色噪声的过程 y t,ϵ t,其中 t = ± 1,± 2,……。如果我们使用实常数ϕ 1,ϕ 2,……,ϕ p(其中ϕ p ≠ 0)来制定过程,我们可以以下述方式制定过程:
y t − μ − ϕ 1(y t−1 − μ) − ϕ 2(y t−2 − μ) − … − ϕ p(y p − μ) = ϵ t
让μ代表整体过程样本均值(在我们的例子中,我们将考虑零均值过程),我们可以将其视为一个p阶自回归过程,或 AR(p) [1]。我们可以定义 AR(p)模型的自相关如下:
ρ k = ϕ 1 |k|
还有这个例子:
ρ k = ϕ 1 ρ k−1 + … + ϕ p ρ k−p
在前面的例子中,其中ρ k 是滞后k自相关。ϕ 1 是 AR(1)过程的斜率和自相关。
AR(p)模型结构和组件
为了避免混淆,请注意,在方程 y t − μ − ϕ 1(y t−1 − μ) − ϕ 2(y t−2 − μ) − … − ϕ p(y p − μ) = ϵ t 中,我们试图构建一个数学模型来表示过程,如果完美建模,则剩下的只是随机、正态分布的白噪声,ϵ t。这实际上意味着模型留下零残差误差(换句话说,是一个完美的拟合)。每个 y t−k 项(其中k是时间滞后)代表该时间点的值,每个相应的φ值是 y t−k 所需的系数值,当与其他所有y值结合时,模型统计上近似零误差。
AR(1)模型
后移算子符号,或简称算子符号,是一种简化的、简写的方法来制定模型。它被称为“后移”,因为它将时间向后移动一个滞后,从t到t-1。其目的是避免在每一个φ系数后面都写上下标(y t−k),而是写上 B k−1,同时只包括一次 y t,这在编写高阶p的 AR(p)模型时很方便。在以下方程中,AR(1)的零均值形式遵循以下结构:
y t − μ − ϕ 1(y t−1 − μ) = ϵ t
方程简化为以下示例:
y t − ϕ 1(y t−1) = ϵ t
在后移算子符号中,我们可以这样说:
( 1 − ϕ 1 B)y t = 𝝐 t
关于 AR(1)中的|𝝓 1|的注释
在这一点上值得注意,如果|ϕ 1| < 1,则 AR(1)过程是平稳的。也就是说,当滞后一阶自相关系数的绝对值小于 1 时,AR(1)过程是平稳的。当|ϕ 1| = 1 时,ARIMA 模型可能仍然有用,但当|ϕ 1| > 1 时,该过程被认为是爆炸性的,不应进行建模。这是因为|ϕ 1| < 1 的值意味着根位于单位圆之外,而不是被单位圆所限制。|ϕ 1| = 1 的值位于单位圆上,但可以通过差分来消除单位根。AR(1)情况的根可以计算为 z = 1/ϕ 1。产生|ϕ 1| > 1 的数据集不能以使其根位于单位圆之外的方式进行过滤。
当 AR(p)的所有根都位于单位圆之外时,给定的实现(从随机过程中抽取的一个时间序列样本)将收敛到均值,具有恒定的方差,并且与时间无关。这是时间序列数据的一个理想场景。
让我们通过一个 AR(1)过程的例子来探讨,其中|ϕ_1| < 1,因此有一个平稳根。假设我们已经识别了以下一阶自回归过程:
y_t − 0.5 y_{t−1} = ϵ_t
这被转换为算子符号:
(1 − 0.5B) y_t = ϵ_t
当寻找根时,我们可以使用以下符号:
(1 − 0.5z) = 0
这给出了z的根:
z = 1 / ϕ_1 = 1 / 0.5 = 2
因此,由于根大于 1 并且因此位于单位圆外,该过程的 AR(1)表示是平稳的。在 Python 中,我们可以使用即将到来的代码构建此过程。首先,我们构建 AR(1)参数,我们希望它为 0.5。因为我们把 0.5 代入模型 X_t − ϕ_1(y_{t−1}) = ϵ_t,所以我们插入0.5而不是-0.5作为arparams。另外,根据ρ_k = ϕ_1|k|,0.5是滞后 1 的自相关。我们构建的过程将具有任意的样本大小nsample=200。我们使用np.r_[1, -arparams]步骤构建(1 − 0.5B)的(1 − 0.5B) y_t = ϵ_t 部分:
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
import matplotlib.pyplot as plt
import statsmodels.api as sm
import numpy as np
arparams = np.array([0.5])
ar = np.r_[1, -arparams]
ar_process = sm.tsa.ArmaProcess(ar)
y = ar_process.generate_sample(nsample=200)
现在我们有了创建我们查看的 AR(1)方程的代码,让我们看看根并与我们手动计算的z进行比较:
ar_process.arroots
array([2.])
我们可以看到 Python 输出2.与我们进行的计算相同。我们知道,由于根的绝对值大于 1,AR(1)过程是平稳的,但让我们用 Python 来确认:
ar_process.isstationary
True
通过观察 PACF,我们可以观察到这是一个 p = 5 的自回归。我们还可以通过观察 ACF 观察到ϕ_1 的值大约为 0.5。对于自回归模型,PACF 用于确定作为 AR 阶数的显著滞后数量,ACF 用于确定包含在该阶数中的系数,ϕ_k 的值。使用 ACF 观察 AR(1)的值很简单,但当 p > 1 时不太明显,因为 ACF 与最接近的点(滞后 0)相比,不控制单个滞后,而 PACF 则控制。让我们用 Python 生成这些图:
fig, ax = plt.subplots(1,3, figsize=(20,5))
ax[0].set_title('Realization')
ax[0].plot(y)
plot_acf(y, alpha=0.05, lags=50, ax=ax[1])
ax[1].set_title('ACF')
plot_pacf(y, alpha=0.05, lags=50, ax=ax[2])
ax[2].set_title('PACF')

图 11.1 – AR(1)过程
对于 AR(1)过程,我们在图 11.1中看到,除了滞后 0 之外,PACF 图上只有一个显著的偏自相关。注意,当我们接近滞后 45 时,有一些显著性,但由于滞后 1 和这些点之间的不显著性,包括这些滞后并构建如 AR(50)之类的模型会导致极端过拟合;从滞后 2 到大约滞后 45 的系数将介于大约 0 和±0.15 之间。如第四章中所述,参数检验,大约±0.1 和±0.3 之间的相关性通常被认为是一种弱相关性。
AR(2)模型
让我们看看以下平稳 AR(2)过程:
y_t − 0.8 y_{t−1} − 0.48 y_{t−2} = ϵ_t
转换为后移算子符号,我们得到以下内容:
(1 − 0.8B − 0.48 B²) y_t = ϵ_t
我们还有以下内容:
(1 − 0.8z − 0.48 z²) = 0
由于我们在这本书中专注于 Python,我们不会详细介绍步骤,但了解二次多项式(如 AR(2))遵循二次方程 a x² + bx + c(对于我们的过程是 -0.48 z² − 0.8z + 1)可能是有用的。因此,我们可以使用二次公式:
− b ± √ b² − 4ac ___________ 2a
这是我们用来找到根的依据。在 Python 中,我们可以使用以下方法找到该模型的根:
arparams = np.array([-0.8, -0.48])
ar = np.r_[1, -arparams]
ar_process = sm.tsa.ArmaProcess(ar)
print('AR(2) Roots: ', ar_process.arroots)
print('AR(2) Stationarity: ', ar_process.isstationary)
在这里,我们可以看到使用 statmodels ArmaProcess 函数识别的单位根:
AR(2) 根:[-0.83333333-1.1785113j -0.83333333+1.1785113j]
AR(2) 站立性: True
我们可以观察到根以复共轭形式 a ± bi 存在。当一个过程具有复共轭形式的根时,我们预期自相关将表现出振荡模式,这在 图 11.2 中的 ACF 图中可以看到。我们还可以观察到 PACF 中的两个显著滞后,这支持了 p=2 的阶数:

图 11.2 – 具有复共轭根的 AR(2)
为了从数学上测试复共轭根是否稳定(位于单位圆外),我们取每个根的实部和虚部的向量的大小,并检查它是否大于 1。复共轭根,z,按照形式 a ± bi 的模量如下方程:
‖z‖ = √ a² + b²
我们根的模量如下:
√ _________________ − 0.8333²± 1.1785² = 1.4433
由于 1.4433 > 1,我们知道我们的 AR(2) 模型是稳定的。
使用 PACF 识别 AR 模型的阶数 p
当根据 PACF 图识别自回归过程 AR(p) 的滞后阶数 p 时,我们取存在显著偏自相关的最大滞后作为 p 的阶数。在观察 图 11.3 时,因为 PACF 在滞后 4 后减弱,并通过大约滞后 30,我们将在滞后 4 后停止阶数考虑,因为使用更多的滞后(将它们视为时间序列模型的特征)可能会导致过拟合。使用 PACF 选择的阶数是基于偏自相关减弱前的最后一个显著滞后。虽然滞后 2 和 3 看起来很小,可能不显著,但滞后 4 是显著的。因此,我们可能使用阶数为 4 的 AR 模型获得最佳模型。通常,我们使用信息准则(如 AIC 或 BIC)的错误来测试我们的假设。

图 11.3 – AR(p) 阶数识别
AR(p) 端到端示例
让我们通过一个 Python 中 AR(p) 模型端到端示例。首先,我们需要生成一个由 AR(4) 过程产生的数据集。我们将使用这些数据作为我们将尝试建模的过程:
arparams = np.array([1.59, -0.544, -0.511, 0.222])
ar = np.r_[1, -arparams]
ar_process = sm.tsa.ArmaProcess(ar)
y = ar_process.generate_sample(nsample=200)
对于以下步骤,让我们假设数据 y 是一个机器的输出,我们对它一无所知。
第 1 步 - 视觉检查
我们首先使用本章前面提供的代码可视化原始数据及其 ACF 和 PACF 图:

图 11.4 – 模型开发步骤 1:视觉检查
根据 PACF 图,我们可以看到似乎是一个 AR(2),但可能是 AR(4)。在滞后 4 之后,偏自相关系数在 5% 的显著性水平上失去了统计显著性。然而,当我们考虑 PACF 中滞后 4 的统计显著性时,尽管很微小,ACF 中滞后 4 是显著的。虽然滞后 4 的值不是系数的值,但其显著性有助于确定阶数 p。尽管如此,AR(4) 可能会过拟合,并且不如 AR(2) 一样很好地泛化。接下来,我们将使用 赤池信息量准则(AIC)和 贝叶斯信息量准则(BIC)来帮助我们做出决定。
根据常数均值和我们没有指数衰减(这也需要是显著的)ACF 的事实,似乎没有趋势。
步骤 2 - 选择 AR(p) 的阶数
由于我们根据对 AR(p) 模型应使用阶数的视觉检查不确定,我们将使用 AIC 和 BIC 来帮助我们做出决定。AIC 和 BIC 过程将使用从零阶到即将在代码中提供的 max_ar 值的所有阶数来拟合模型。这些模型将拟合整个数据集。误差最低的阶数通常是最好的。它们的误差计算如下:
AIC = 2k − 2ln(ˆL)
BIC = kln(n) − 2ln(ˆL)
这里的 k 是数据的滞后数 - 最多到测试的最大阶数,ˆL 是最大似然估计,n 是样本大小(或正在测试的数据集的长度)。对于这两个测试,误差越低越好。
我们将从 Statsmodels 导入 arma_order_select_ic 并根据我们在 图 11**.4 中的 PACF 图中的观察结果,使用最多 4 个滞后项进行测试。正如所注,根据我们的视觉检查,我们似乎没有趋势。然而,我们可以通过一个基于 OLS 的单位根测试,称为 Dickey-Fuller 测试 来进行统计验证。Dickey-Fuller 测试的 零假设 是在测试的最大滞后数(maxlag)中的某个点存在单位根(因此,趋势)。备择假设是数据中没有单位根(没有趋势)。为了参考,备择假设表明数据是一个零阶 - I(0) - 集成过程,而零假设表明数据是一个一阶 - I(1) - 集成过程。如果测试统计量的绝对值大于临界值或 p 值显著,我们可以得出没有趋势(没有单位根)的结论。
Dickey-Fuller 测试考虑回归测试中包含的滞后数目的每个数据点。我们需要分析 ACF 图来做到这一点;因为我们希望考虑趋势可能存在的最远范围,我们必须选择具有显著性的最长滞后。想法是,如果我们数据中有一个强烈的趋势,比如增长,那么在趋势存在期间,每个连续的值都将导致另一个后续值的增加。在我们的情况下,ACF 图中的最大显著滞后大约是 25。由于 Dickey-Fuller 测试的统计功效相对较低(容易犯第二类错误或当应该拒绝零假设时未能拒绝),因此只要它是实用的,高阶滞后并不令人担忧;风险是未能包含足够的滞后。
Dickey-Fuller 单位根
Dickey-Fuller 测试仅当存在趋势单位根时才进行,但如果有季节性单位根则不进行。我们将在本章的 ARIMA 部分讨论趋势和季节性单位根之间的区别。
在接下来的代码块中,我们在 图 11**.4 中的自相关图(ACF)中添加了 maxlag=25,对应于我们的 25 个滞后项。我们还将包括 regression='c',这将在进行的 OLS 回归中添加一个常数(或截距);在这种情况下,我们不需要手动添加常数:
from statsmodels.tsa.stattools import adfuller
dicky_fuller = adfuller(y, maxlag=25, regression='c')
print('Dickey-Fuller p-value: ', dicky_fuller[1])
print('Dickey-Fuller test statistic: ', dicky_fuller[0])
print('Dickey-Fuller critical value: ', dicky_fuller[4].get('5%'))
根据 Dickey-Fuller 测试,我们应该拒绝零假设,并得出结论该过程是零阶积分的,因此没有趋势:
Dickey-Fuller p 值: 1.6668842047161513e-06
Dickey-Fuller 测试统计量: -5.545206445371327
Dickey-Fuller 临界值: -2.8765564361715534
因此,我们可以将 trend='n' 插入到我们的 arma_order_select_ic 函数中(否则,我们可能想要对数据进行差分,我们将在本章的 ARIMA 部分展示):
from statsmodels.tsa.stattools import arma_order_select_ic
model_ar = arma_order_select_ic(y=y, max_ar=4, max_ma=0,
ic=['aic','bic'], trend='n')
print('AIC Order Selection: ', model_ar.aic_min_order)
print('AIC Error: ', round(model_ar.aic.min()[0], 3))
print('BIC Order Selection: ', model_ar.bic_min_order)
print('BIC Error: ', round(model_ar.bic.min()[0], 3))
在这里,我们可以看到根据我们的 AIC 和 BIC 测试,识别出的 AR 和 MA 阶数,以产生最低的整体误差:
AIC 阶数选择: (4, 0)
AIC 错误: 586.341
BIC 阶数选择: (2, 0)
BIC 错误: 597.642
我们可以看到 AIC 选择了一个 AR(4),而 BIC 选择了一个 AR(2)。最好是两个测试都选择相同的项阶数。然而,正如我们之前提到的,AR(2) 可能不太可能过度拟合。由于最佳阶数并不完全清楚,我们将通过比较它们的误差和对数似然估计来测试这两个模型(使用 AR(2) 和 AR(4))。
第 3 步 - 构建 AR(p)模型
在这一步,我们可以将我们的参数添加到statsmodels的 ARIMA 函数中,并使用我们指定的 AR(4)拟合数据。为了明确,AR(4)等同于 ARIMA(4,0,0)。我们希望包含enforce_stationarity=True以确保我们的模型将产生有用的结果。如果不是,我们将收到警告,并需要通过差分、使用不同的模型(如 SARIMA)、改变我们的采样方法、改变我们的时间分箱(例如从天到周)或完全放弃时间序列建模来解决这个问题:
from statsmodels.tsa.arima.model import ARIMA
ar_aic = ARIMA(y, order=(4,0,0),
enforce_stationarity=True).fit()
print(ar_aic.summary())
在我们的模型输出中,我们可以看到SARIMAX 结果标题和模型:ARIMA(4,0,0)。这可以忽略。没有季节成分和没有外生变量(在我们的情况下)的 SARIMAX 只是一个 ARIMA。此外,阶数为(4,0,0)的 ARIMA 是一个AR(4):

图 11.5 – AR(4)模型结果
我们建模的 AR(4)过程(在步骤 1 之前构建的模拟过程)是:
y_{t-1} - 1.59 y_{t-1} + 0.544 y_{t-2} + 0.511 y_{t-3} - 0.222 y_{t-4} = ϵ_{t}
我们使用输入过程的数据生成的 AR(4)模型如下:
y_{t-1} - 1.6217 y_{t-1} + 0.6877 y_{t-2} + 0.3066 y_{t-3} - 0.1158 y_{t-4} = ϵ_{t}
在后移算子符号表示法中,我们得到以下方程:
(1 - 1.6217B + 0.6877 B² + 0.3066 B³ - 0.1158 B⁴) y_{t} = ϵ_{t}
值得注意的是,滞后 4 的项并不显著,置信区间包含 0。因此,包括这个项是过度拟合的已知风险,如果考虑替代模型,这是一个值得权衡的因素。如果基于 AIC 和 BIC 比较 AR(2)甚至 AR(3)与我们的 AR(4)的结果有显著改善,那么选择不同的模型将是谨慎的,但为了节省时间,我们将跳过这个过程。
关于模型摘要指标,我们在上一章讨论了Ljung-Box 检验,所以这里不再详细说明,但该检验的高 p 值(Prob(Q))表明在滞后 1 处没有相关误差。通常,如果模型拟合的残差中存在序列相关性,残差将具有滞后 1 的自相关性。Jarque-Bera 检验假设在零假设下误差是正态分布的,而在备择假设下不是正态分布的。该检验的高 p 值(Prob(JB))表明误差是正态分布的。异方差性检验检验的是残差是否恒定(同方差),备择假设是它们不是恒定的,这是时间序列回归拟合的问题。在这里,我们的异方差性 p 值(Prob(H))较高,因此我们可以假设我们的模型残差具有恒定的方差。偏度分数在 [-0.5, 0.5] 之间被认为是未偏斜的,而在 [-1, -0.5] 或 [0.5, 1] 之间是中度偏斜的,而 > ±2 是高度偏斜的。峰度的完美分数是 3。峰度 > ±7 是高度偏斜的。因为我们的偏度为 0.04,我们的峰度分数为 2.58,我们可以假设我们的残差是正态分布的。
步骤 4 - 测试预测
验证模型的另一种方法是使用那些点之前的数据来预测现有点。在这里,我们使用模型来预测最后 5 个点,使用的是除了最后 5 个点之外的全数据集。然后我们进行比较,以了解模型性能。请注意,我们生成了 200 个样本,这些样本的索引从 0 开始。因此,我们的第 200 个样本位于位置索引 199:
df_pred = ar_aic.get_prediction(start=195, end=199).summary_frame(alpha=0.05)
df_pred.index=[195,196,197,198,199] # reindexing for 0 index
在以下表格中,mean 列是预测值。我们手动将 actuals 列添加到我们数据的最后 5 个值中,以与预测值进行比较。mean_se 是我们估计值与实际值相比的均方误差。y 是我们的索引,ci 列是我们 95% 预测置信区间,因为我们之前在代码中使用 alpha=0.05。
| y | mean | mean_se | mean_ci_lower | mean_ci_upper | actuals |
|---|---|---|---|---|---|
| 195 | 24.70391 | 0.99906 | 22.74579 | 26.662035 | 25.5264 |
| 196 | 19.36453 | 0.99906 | 17.4064 | 21.322652 | 18.8797 |
| 197 | 7.525904 | 0.99906 | 5.567779 | 9.484028 | 7.4586 |
| 198 | -5.8744 | 0.99906 | -7.83252 | -3.916274 | -7.1316 |
| 199 | -19.5785 | 0.99906 | -21.5366 | -17.620356 | -17.9268 |
图 11.6 – AR(4) 模型输出与实际值对比
根据我们的均方误差(0.999062),我们可以看到我们的模型在测试数据上对 5 个点的预测范围内提供了合理的拟合。使用以下代码,我们绘制了我们的测试预测与相应的实际值:
fig, ax = plt.subplots(1,1,figsize=(20,5))
ax.plot(y, marker='o', markersize=5)
ax.plot(df_pred['mean'], marker='o', markersize=4)
ax.plot(df_pred['mean_ci_lower'], color='g')
ax.plot(df_pred['mean_ci_upper'], color='g')
ax.fill_between(df_pred.index, df_pred['mean_ci_lower'], df_pred['mean_ci_upper'], color='g', alpha=0.1)
ax.set_title('Test Forecast for AR(4)')

图 11.7 – AR(4) 测试预测
步骤 5 - 构建预测
确定合理的预测范围高度依赖于至少数据及其所代表的过程,以及用于建模的滞后,以及模型误差。在向利益相关者提供预测之前,时间序列从业者应权衡模型性能和业务需求与风险的所有因素。添加以下代码,我们重新运行图表以查看具有 5 点预测范围的真正预测:
df_forecast = ar_aic.get_prediction(start=200, end=204).summary_frame(alpha=0.05)
df_forecast.index=[200, 201, 202, 203, 204]
forecast = np.hstack([np.repeat(np.nan, len(y)), df_pred['mean']])

图 11.8 – AR(4)预测范围=5
我们将在本章的模型评估部分介绍额外的步骤。
移动平均(MA)模型
MA(q)模型
而 AR(p)模型是时间上滞后零与特定个体阶数p的滞后之间的相关性的直接函数,阶数p的移动平均模型,MA(q),是滞后零与包括在阶数p中的所有先前滞后之间的自相关函数。它作为一个低通滤波器,通过建模误差来提供对数据的有效拟合。
让我们考虑一个过程,y t,它具有零均值和一个随机、正态分布的白噪声成分,ϵ t,其中 t = ± 1, ± 2, …。如果我们能将此过程写成
y t − μ = ϵ t − ϴ 1 ϵ t−1 − … − ϴ q ϵ t−q
并且 ϴ 1, ϴ 2, … , ϴ q 是实常数且 ϴ q ≠ 0,那么我们可以称这是一个具有阶数q的移动平均过程,或 MA(q)。在向后移位算子记法中,我们有以下:
y t − μ = (1 − ϴ 1 B − … − ϴ q B q) ϵ t
我们可以这样定义 MA(q)模型的自相关(ρ k):
ρ k = − ϴ k + ∑ j=1 q−k ϴ j ϴ j+k _____________ 1 + ∑ j=1 q ϴ j 2
对于所有滞后k在 1,2, … , q 中。当 k > q 时,我们有ρ k = 0。
当讨论 AR(p)模型时,我们解释了 AR 模型的根必须位于单位圆外部。当考虑 MA(q)模型时,我们有可逆性的概念。可逆性本质上确保了与过去的逻辑和稳定的关联。通常,这意味着当前时间点与过去附近的时间点比那些更远的时间点更紧密相关。无法使用可逆根来建模过程意味着我们无法确保我们的模型为模型自相关集提供唯一解。如果过去较远的时间点比附近的时间点对当前点更相关,那么过程中存在无法可靠建模或预测的随机性。
识别 MA(q)模型的可逆性
为了使移动平均模型可逆,所有根必须位于单位圆外部且非虚数;所有根必须大于 1。对于 MA(1)过程,当|ϴ 1| < 1 时,它是可逆的。一个可逆的 MA(q)过程等价于一个无限阶、收敛的 AR(p)过程。如果 AR(p)模型的系数随着滞后 k 接近 p 而收敛到零,则该模型收敛。如果一个 MA(q)是可逆的,我们可以说 y t = ϴ(B) ϵ t 和 ϴ −1(B) y t = ϵ t [1]。
MA(1)模型
对于一阶 MA(q)模型,我们有以下过程:
ρ 0 = 1
ρ 1 = ϴ 1 _ 1 + ϴ 1 2
ρ k>1 = 0
对于具有零自相关函数的 MA(q)模型,我们试图建模的过程模式是随机的,服从正态分布的白噪声方差,这最多只能通过其均值来建模。重要的是要注意,当 ϴ₁ → 0 时,ρ₁ → 0,对于 MA(1)过程,这意味着过程可以近似为白噪声。
让我们考虑以下 MA(1)零均值模型,其形式为 y_t - μ = ϵ_t - ϴ₁ ϵ_{t-1}:
y_t - 0 = a_t - 0.8 ϵ_{t-1}
在后移记号法中,我们有以下内容:
y_t = (1 - 0.8B) ϵ_t
我们知道这个过程是可逆的,因为 |ϴ₁| < 1。让我们使用 Python 中的statsmodels.tsa模块的ArmaProcess函数来确认这一点:
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
import matplotlib.pyplot as plt
import statsmodels.api as sm
import numpy as np
maparams = np.array([0.8])
ma = np.r_[1, -maparams]
ma_process = sm.tsa.ArmaProcess(ma=ma)
print('MA(1) Roots: ', ma_process.maroots)
print('MA(1) Invertibility: ', ma_process.isinvertible)
MA(1) Roots: [1.25]
MA(1) 可逆性: True
与 AR(p)模型相反,MA(q)模型的阶数是通过 ACF 图来确定的。因为 ACF 没有控制滞后,并且是考虑到的滞后阶数及其自相关测度的一个综合自相关度量,所以该函数用于确定移动平均成分的相关滞后阶数。在图 11.9中,我们可以看到我们的 MA(1)过程在滞后 1 处的显著相关性。在滞后 6 和 7 处还有两个额外的显著相关性,但使用如此远的滞后通常会导致过度拟合,尤其是当考虑到滞后 2 至 5 在 5%的显著性水平上并不显著:

图 11.9 – MA(1)图
消减 ACF 和 PACF
对于可逆移动平均模型,我们可以观察到自相关函数(ACF)将在显著性阶数处截断,但偏自相关函数(PACF)通常会继续并随时间逐渐衰减至统计上的零。这并不一定期望会平滑且一次性发生,因为所有数据集都是不同的,但预计随着时间的推移,越来越多的滞后项将衰减至零。我们将在本章的 ARMA 部分解释这种行为的理由,但值得注意的是,对于可逆过程,这是可以预期的。相反,平稳自回归过程在 PACF 图中预计会在显著性阶数处截断,而 ACF 随时间衰减至零。
MA(2)模型
对于二阶的 MA(q)模型,我们有以下公式:
ρ₀ = 1
ρ₁ = - ϴ₁ + ϴ₁ ϴ₂_1 + ϴ₁² + ϴ₂²
ρ² = - ϴ²_1 + ϴ₁² + ϴ²_2
ρ_k>2 = 0
下面是一个我们将要研究的 MA(2)示例:
y_t = (1 - 1.6B + 0.9B²) ϵ_t
使用二次方程的模型多项式,我们可以使用二次公式找到近似根:
0.888 ± 0.567i
由于我们有两个共轭复根,我们可以使用与 AR(p)过程相同的 L²范数,使用形式为 a ± bi:
√(0.888² + 0.567²) ≈ 1.054
因为 1.054 大于 1,我们可以确认 MA(2) 有可逆根,因此能够产生一个唯一解和一个模型,其值在逻辑上是与过去值序列相关的。让我们在 Python 中进行同样的分析:
maparams = np.array([1.6, -0.9])
ma = np.r_[1, -maparams]
ma_process = sm.tsa.ArmaProcess(ma=ma)
print('MA(2) Roots: ', ma_process.maroots)
print('MA(2) Invertibility: ', ma_process.isinvertible)
我们用绿色突出显示的输出确认了我们的计算结果和事实,即由于复共轭根的幅度大于 0,我们有一个可逆的 MA(2) 过程:
MA(2) 根: [0.88888889-0.56655772j 0.88888889+0.56655772j]
MA(2) 可逆性: True
我们可以在图 11.10 的自相关图中看到,这是一个二阶移动平均过程:

图 11.10 – MA(2) 图
对于 MA(q) 模型,识别模型阶数、构建模型和生成预测的过程与 AR(p) 模型相同。我们讨论过,对于 MA(q),基于视觉检查的阶数选择是通过 ACF 进行的,而对于 AR(p),这是通过 PACF 进行的,这是两个模型之间过程的主要区别。除此之外,对于 MA(q) 模型,应将 enforce_invertibility 设置为 True 而不是 enforce_stationarity=True。在 arma_order_select_ic 函数中提供比有用阶数更高或更低的 max_ar 或 max_ma 阶数可能会导致收敛警告或可逆性警告。这些警告的一个原因是提供了比可能拟合的更高阶数(例如,当没有可能的阶数时)。另一个原因是存在一个单位根。如果数据中存在明显的趋势,则在建模之前必须将其去除。如果没有趋势,由于数据中的季节性,可能会收到此错误,这表现为不同的单位根阶数。我们将在本章的 ARIMA 和季节性 ARIMA 部分讨论与趋势和季节性相关的单位根的建模。还值得指出的是,由于移动平均过程可能受到趋势的影响,因此可以使用 Dickey-Fuller 测试来移动平均数据。
自回归移动平均 (ARMA) 模型
在自回归模型部分,我们讨论了如何使用 AR(p) 模型通过自相关控制个体滞后来建模过程输出值。AR(p) 模型的目标是使用过去某个特定滞后下的值来估计未来对应滞后点的确切值。例如,未来两个点的值与过去两个点的值高度相关。在移动平均模型部分,我们讨论了 MA(q) 模型如何作为低通滤波器,帮助模型解释过程中的噪声。我们不是寻求建模确切点,而是使用 MA(q) 来建模过程周围的方差。
考虑一个四缸汽车发动机的例子,该发动机产生恒定的输出。让我们假设我们有一个磨损的发动机支架靠近第四缸。我们可以预期与每个气缸点火相关的连续输出振动,但振动会随着接近磨损的发动机支架的每一下冲而略有增加。使用仅 AR 模型会假设每个气缸振动一定量并能解释这一点,但我们会丢失信息。添加一个 MA 成分将能够模拟从第一缸开始,每个后续冲程直到第四缸都会有与磨损的发动机支架相关的额外振动,从而解释更多的整体过程。这合理地是一个 ARMA(4,4)模型。假设我们用磨损程度与其他支架相同的支架替换磨损的发动机支架;那么我们就会有 ARMA(4,0)(或 AR(4))过程。
在许多情况下,我们发现自相关和偏自相关都有显著的峰值。而不是只使用 MA(q)或 AR(p)模型,我们可以将两者结合起来。这种组合,表示为 ARMA(p,q),使我们能够模拟过程以及可能与特定滞后相关的过程周围的任何噪声成分。因为 ARMA(p,q)通常比 AR 或 MA 模型具有更少的参数(更低阶),ARMA 被认为是一个简约模型,这是一个使用尽可能少的解释变量(在这种情况下,时间滞后)来实现所需性能水平的模型。当 y t 是一个可逆和平稳的过程时,我们可以将其定义为 ARMA(p,q):
y t − μ = Φ 1(y t−1 − μ) − … − Φ p(y t−p − μ) = ϵ t − ϴ 1 ϵ t−1 − … − ϴ q ϵ t−q
当Φ p ≠ 0 和ϴ q ≠ 0 时,我们可以用后移算子符号重新写出 ARMA(p,q)的方程:
ΦB(y t − μ) = ϴ(B) ϵ t
从实际角度来说,我们可以预期对于一个可逆的移动平均过程,在自相关函数(ACF)中我们会看到显著的滞后,直到阶数 q 的量级,但随后偏自相关函数(PACF)将逐渐减小,通常在 ACF 中识别出的移动平均过程阶数之外。这是因为有限阶的移动平均过程可以被表示为一个无限阶的自回归过程。相反,由于具有这种行为的移动平均过程是可逆的,其逆过程也必须成立;即有限阶的自回归过程可以被表示为一个无限阶的移动平均过程。因此,对于可逆的移动平均过程,偏自相关函数将衰减到零,而对于平稳的自回归过程,自相关函数也将衰减到零。因为可逆性是 ARMA 过程的要求,它允许我们将方程重写为一般线性形式下的无限阶 MA 过程:
y t = Φ −1(B)ϴ(B) ϵ t
它还允许我们将它作为一个无限阶的自回归过程:
ϴ −1(B)Φ(B) y t = ϵ t
让我们通过一个 Python 中的例子来演示。首先,使用本章前面相同的导入,让我们生成一个可逆且平稳的 ARMA(2,1) 过程的虚拟数据集,该数据集满足以下方程:
(1 − 1.28B + 0.682 B²) y_t = (1 − 0.58B) ε_t
arparams = np.array([1.2, -0.6])
ar = np.r_[1, -arparams]
maparams = np.array([0.5])
ma = np.r_[1, -maparams]
arma_process = sm.tsa.ArmaProcess(ar=ar, ma=ma)
让我们确认平稳性和可逆性:
print('AR(2) Roots: ', arma_process.arroots)
print('AR(2) Invertibility: ', arma_process.isstationary)
print('MA(1) Roots: ', arma_process.maroots)
print('MA(1) Invertibility: ', arma_process.isinvertible)
我们可以使用二次公式来测试,但我们可以相信代码来确认:
AR(2) 根: [1.-0.81649658j 1.+0.81649658j]
AR(2) 平稳性: True
MA(1) 根: [2.]
MA(1) 可逆性: True
既然我们已经有一个平稳且可逆的过程,让我们从中生成 200 个样本:
y = arma_process.generate_sample(nsample=200)
第 1 步 – 视觉检查
让我们看看我们一直在使用的图表,以构建关于生成数据的过程的直觉:

图 11.11 – ARMA(p,q) 过程样本数据
我们可以通过 ACF 看出,似乎有一个 MA(1) 的成分。根据 PACF,看起来我们可能有一个 AR(2) 或 AR(4)。实现看起来是一个满足平稳性的过程。
第 2 步 – 选择 ARMA(p,q) 的阶数
在我们决定 ARMA 模型的阶数之前,让我们使用 Dickey-Fuller 测试来检查我们的数据是否存在趋势:
from statsmodels.tsa.stattools import adfuller
dicky_fuller = adfuller(y, maxlag=25, regression='c')
print('Dickey-Fuller p-value: ', dicky_fuller[1])
print('Dickey-Fuller test statistic: ', dicky_fuller[0])
print('Dickey-Fuller critical value: ', dicky_fuller[4].get('5%'))
我们可以看到统计显著性,这证实了我们提供的滞后项中没有单位根(记住,H_0:数据有一个单位根 和 H_1:数据没有单位根)。因此,我们可以使用 ARMA 模型而不需要任何一阶差分,这至少需要一个 ARIMA:
Dickey-Fuller p 值: 6.090665062133195e-16
Dickey-Fuller 测试统计量: -9.40370671340928
Dickey-Fuller 临界值: -2.876401960790147
现在,让我们使用 statmodels 的 arma_order_select_ic 来查看 AIC 和 BIC 对 ARMA(p,q) 阶数的选择。我们知道 MA(q) 的最大阶数是 1,但由于我们不确定这是 AR(2) 还是 AR(4),我们可以使用 max_ar=4:
from statsmodels.tsa.stattools import arma_order_select_ic
model_arma = arma_order_select_ic(y=y, max_ar=4, max_ma=1, ic=['aic','bic'], trend='n')
print('AIC Order Selection: ', model_arma.aic_min_order)
print('AIC Error: ', round(model_arma.aic.min()[0], 3))
print('BIC Order Selection: ', model_arma.bic_min_order)
print('BIC Error: ', round(model_arma.bic.min()[0], 3))
我们可以看到 AIC 选择了一个 ARMA(4,1),而 BIC 选择了一个 ARMA(2,1):
AIC 阶数选择: (4, 1)
AIC 错误: 548.527
BIC 阶数选择: (2, 1)
BIC 错误: 565.019
ARMA(4,1) 具有较低的误差,但我们知道从这本书的此章节和前面的章节中,具有较低训练数据误差的模型可能更有可能具有更大的方差,因此更有可能过拟合。但是,让我们使用 ARMA(4,1)。
第 3 步 – 构建 AR(p) 模型
现在,让我们构建我们的 ARMA(4,1) 模型。注意,0 是 ARIMA(p,d,q) 模型中一阶差分的积分。由于我们没有基于趋势的单位根,我们不需要差分来去除任何趋势。因此,d=0:
from statsmodels.tsa.arima.model import ARIMA
arma_aic = ARIMA(y, order=(4,0,1),
enforce_stationarity=True, enforce_invertibility=True).fit()
print(arma_aic.summary())
在这里,我们可以看到模型基于模型指标提供了一个合理的拟合。然而,有一个问题;我们前三个 AR 系数没有统计显著性(高 p 值和包含零的置信区间)。这是一个大问题,证实了我们的模型过度拟合。我们的模型包含了它没有从中获得益处的项。因此,我们的模型在未见过的数据上很可能无法很好地泛化,应该重建:

图 11.12 – ARIMA(4,0,1) 模型结果
让我们重新运行这个 ARMA(2,1),它与 ARIMA(2,0,1) 相同,因为没有差分需要积分。这是我们通过视觉识别的,BIC 选择以下代码:
from statsmodels.tsa.arima.model import ARIMA
arma_aic = ARIMA(y, order=(2,0,1),
enforce_stationarity=True, enforce_invertibility=True).fit()
print(arma_aic.summary())
我们现在可以看到我们的变量有了更好的拟合。所有系数都是显著的,模型指标仍然足够。我们识别出的模型对应以下方程:
(1 − 1.2765B + 0.6526 B 2) y t = (1 + 0.58B) ϵ t
我们可以将这些与我们的虚拟过程进行比较:
(1 − 1.28B + 0.682 B 2) y t = (1 − 0.58B) ϵ t

图 11.13 – ARIMA(2,0,1) 模型结果
第 4 步 – 测试预测
现在,让我们通过在最后五个数据点上训练模型来交叉验证我们的模型,然后预测最后五个点,以便我们可以将它们与实际值进行比较。记住,我们的索引从 0 开始,所以我们的数据集在索引 199 结束:
df_pred = arma_aic.get_prediction(start=195, end=199).summary_frame(alpha=0.05)
df_pred.index=[195,196,197,198,199]
我们可以在以下表格的 均值 列中看到我们的预测值:
| y | mean | mean_se | mean_ci_lower | mean_ci_upper | actuals |
|---|---|---|---|---|---|
| 195 | -0.01911 | 0.932933 | -1.84762 | 1.80940631 | 0.559875 |
| 196 | 0.58446 | 0.932933 | -1.24406 | 2.412975242 | 0.778127 |
| 197 | 0.479364 | 0.932933 | -1.34915 | 2.307879057 | 1.695218 |
| 198 | 0.914009 | 0.932933 | -0.91451 | 2.74252465 | 2.041826 |
| 199 | 0.80913 | 0.932933 | -1.01939 | 2.637645206 | 0.578695 |
图 11.14 – AR(4) 模型输出与实际值对比
让我们打印出我们模型的 平均平方 误差 (ASE):
print('Average Squared Error: ', np.mean((df_pred['mean'] - y[195:])**2))
这里我们看到 ASE:
平均平方误差: 0.6352208223437921
我们的测试预测图显示在 图 11**.15 中。注意,我们的估计看起来比较保守。使用 ARMA(4,1) 可能会产生更接近的拟合,但泛化性较差。改进预测的一个方法是通过仅使用最近的数据点(相对于过程的主题知识)来构建模型。包括更大的数据集将产生一个更适合整体过程的拟合,而不是可能更相关的时段:

图 11.15 – ARMA(2,1) 测试预测
第 5 步 – 构建预测
现在,让我们预测五个点:
df_forecast = arma_aic.get_prediction(start=200, end=204).summary_frame(alpha=0.05)
df_forecast.index=[200, 201, 202, 203, 204]
forecast = np.hstack([np.repeat(np.nan, len(y)), df_pred['mean']])

图 11.16 – ARMA(2,1) 预测范围 = 5
最后关于 ARMA 模型的一点,我们总是假设过程平稳性。如果无法假设平稳性,则既不能使用自回归模型也不能使用移动平均模型。在本章的下一节中,我们将讨论将一阶差分整合到 ARMA 模型中,作为条件平稳化过程以克服非平稳性局限性的方法。
非平稳时间序列的模型
在上一节中,我们讨论了适用于平稳时间序列数据的 ARMA 模型。在本节中,我们将探讨非平稳时间序列数据,并将我们的模型扩展到可以处理非平稳数据。让我们首先查看一些样本数据(如图11.17所示)。有两个序列:美国 GDP(左)和航空旅客量(右)。

图 11.17 – 美国 GDP(左)和航空旅客(右)时间序列
美国 GDP 序列似乎表现出上升趋势,序列中也有一些变化。航空旅客量序列也表现出上升趋势,但序列中似乎也存在重复的模式。航空序列中的重复模式称为季节性。由于明显的趋势,这两个序列都是非平稳的。此外,航空旅客量序列似乎表现出非恒定的方差。我们将使用 ARIMA 模型来模拟 GDP 序列,并将模拟季节性 ARIMA。让我们来看看这些模型。
ARIMA 模型
ARIMA是自回归积分移动平均的缩写。这个模型是 ARMA 模型的一种推广,可以应用于非平稳时间序列数据。这个模型新增的部分是“积分”,它是对时间序列进行差分操作以平稳化(使时间序列平稳)。时间序列平稳化后,我们可以对差分数据进行 ARMA 模型的拟合。让我们来看看这个模型的数学原理。我们将从理解差分是如何工作的开始,然后构建整个 ARIMA 模型。
差分
差分数据是计算连续数据点之间的差异。差分后的数据代表每个数据点之间的变化。我们可以将差分写成如下形式:
y′t = yt − yt−1
这个方程是一阶差分,意味着它是数据点之间的第一个差分。可能需要在对数据点进行额外的差分以使序列平稳化。二阶差分代表数据点之间的变化的改变。二阶差分可以写成如下形式:
y″t = y′t − y′t−1 = (yt − yt−1) − (yt−1 − yt−2)
“阶数”简单地指差分操作应用的次数。
ARIMA 模型
如前所述,ARIMA 模型是 ARMA 模型,通过添加差分使时间序列平稳(使时间序列平稳化)。然后我们可以将 ARIMA 模型数学上表示如下,其中 y′ t 是差分序列,差分 d 次直到它平稳:
y′ t = c + ϕ 1 y′ t−1 + … + ϕ p y ′ t−p + ϵ t + θ 1 ϵ t−1 + … + ϕ q ϵ t−q
ARIMA 模型有三个阶数,分别表示为 ARIMA(p,d,q):
-
p – 自回归阶数
-
d – 差分阶数
-
q – 移动平均阶数
对于像 ARIMA 这样的更复杂模型,我们倾向于用后移符号来描述它们,因为用后移符号表达这些模型更容易。使用后移符号,ARIMA 模型将采取以下形式:
(1 − ϕ 1 B − … − ϕ p B p) (1 − B) d y t = c + (1 + θ 1 B + … + θ q B q) 𝝐 t
↑ ↑ ↑
AR(p) d 差分 MA(q)
注意方程中差异项的表示:(1 − B) d。在上一节中,我们讨论了与平稳模型相关的根。在那个背景下,根始终位于单位圆之外。在 ARIMA 模型中,我们向模型中添加单位根。为了理解单位根的影响,让我们模拟一个 AR(1)模型,并观察当模型根值接近 1 时会发生什么。这些模拟显示在图 11.18 中。

图 11.18 – 根值接近 1 的 AR(1)模拟
从图 11.18 所示的模拟中,我们可以得出两个观察结果。第一个观察结果是,随着根值逐渐接近 1,时间序列似乎表现出更多的游走行为。例如,中间时间序列相对于顶部时间序列,从平均值出发的游走行为更多。底部时间序列(根值为 1)似乎不像其他两个模拟那样回归到平均值。第二个观察结果是关于自相关性的。随着 AR(1)的根值接近 1,自相关性变得更强,并且随着滞后时间的增加而减慢。这两个观察结果是根值接近或等于 1 的序列的特征。此外,单位根的存在将主导时间序列的行为,使得从自相关图上容易识别。
适配 ARIMA 模型
适配 ARIMA 模型有两个步骤:(1)通过差分使序列平稳以确定差分阶数,(2)将 ARMA 模型拟合到得到的序列。在上一节中,我们讨论了如何拟合 ARMA 模型,因此在本节中,我们将重点关注第一步。
在本节的开始,我们展示了一个美国 GDP 的时间序列。我们将使用这个时间序列作为拟合 ARIMA 模型的案例研究。首先,让我们再次查看该序列及其自相关性。序列和自相关性显示在图 11.19 中。

图 11.19 – 美国 GDP 时间序列和自相关图
从图 11.19所示的图中可以看出,美国 GDP 数据的时间序列是非平稳时间序列。时间序列表现出游走行为,自相关性强且缓慢下降。正如我们讨论的那样,这是单位根的特征行为。作为次要证据,我们可以使用 Dickey-Fuller 测试来检验单位根。Dickey-Fuller 测试的零假设是时间序列中存在单位根。以下代码显示了如何使用pmdarima中的 Dickey-Fuller 测试。测试返回的 p 值为 0.74,这意味着我们不能拒绝零假设,这意味着时间序列应该进行差分:
Import pmdarima as pm
from sktime import datasets
y_macro_economic = datasets.load_macroeconomic()
adf_test = pm.arima.ADFTest()
adf_test.should_diff(y_macro_economic.realgdp.values)
# (0.7423236714537164, True)
我们可以使用numpy中的diff函数对时间序列进行第一次差分:
first_diff = np.diff(y_macro_economic.realgdp.values, n=1)
进行第一次差分后,我们得到一个新的时间序列,如图 11.20所示:

图 11.20 – 美国 GDP 时间序列的第一差分
美国 GDP 时间序列图 11.20中显示的第一个差异似乎具有平稳性。实际上,它似乎与 AR(2)模型一致。我们通过在第一差分数据上使用 Dickey-Fuller 测试来双重检查是否需要额外的差分:
first_diff = np.diff(y_macro_economic.realgdp.values, n=1)
adf_test.should_diff(first_diff)
# (0.01, False)
Dickey-Fuller 测试对第一差分数据返回的 p 值为 0.01,这意味着我们可以拒绝零假设,我们可以停止对数据进行差分。这意味着我们的 ARIMA 模型对于这些数据将有一个差分阶数为 1(d = 1)。
在找到差分阶数后,我们可以将 ARMA 模型拟合到差分数据上。由于我们已经讨论了 ARMA 模型的拟合,我们将使用pmdarima提供的自动拟合方法。pm.auto_arima是一个用于自动将 ARIMA 模型拟合到数据的函数,然而,在这种情况下,我们将使用它来拟合差分序列的 ARMA 部分。以下代码块显示了pm.auto_arima对第一差分数据的输出:
pm.auto_arima(
first_diff, error_action='ignore', trace=True,
suppress_warnings=True, maxiter=5, seasonal=False,
test='adf'
)
Performing stepwise search to minimize aic
ARIMA(2,0,2)(0,0,0)[0] : AIC=2207.388, Time=0.03 sec
ARIMA(0,0,0)(0,0,0)[0] : AIC=2338.346, Time=0.01 sec
ARIMA(1,0,0)(0,0,0)[0] : AIC=2226.760, Time=0.02 sec
ARIMA(0,0,1)(0,0,0)[0] : AIC=2284.220, Time=0.01 sec
ARIMA(1,0,2)(0,0,0)[0] : AIC=2206.365, Time=0.02 sec
ARIMA(0,0,2)(0,0,0)[0] : AIC=2253.267, Time=0.02 sec
ARIMA(1,0,1)(0,0,0)[0] : AIC=2203.917, Time=0.01 sec
ARIMA(2,0,1)(0,0,0)[0] : AIC=2208.521, Time=0.02 sec
ARIMA(2,0,0)(0,0,0)[0] : AIC=2208.726, Time=0.02 sec
ARIMA(1,0,1)(0,0,0)[0] intercept : AIC=2193.482, Time=0.04 sec
ARIMA(0,0,1)(0,0,0)[0] intercept : AIC=2208.669, Time=0.03 sec
ARIMA(1,0,0)(0,0,0)[0] intercept : AIC=2195.212, Time=0.02 sec
ARIMA(2,0,1)(0,0,0)[0] intercept : AIC=2191.810, Time=0.03 sec
ARIMA(2,0,0)(0,0,0)[0] intercept : AIC=2190.196, Time=0.02 sec
ARIMA(3,0,0)(0,0,0)[0] intercept : AIC=2191.589, Time=0.03 sec
ARIMA(3,0,1)(0,0,0)[0] intercept : AIC=2193.567, Time=0.03 sec
Best model: ARIMA(2,0,0)(0,0,0)[0] intercept
Total fit time: 0.349 seconds
由于差分数据的 ARMA 拟合是 ARMA(2,0),原始时间序列的 ARIMA 阶数将是 ARIMA(2,1,0)。接下来,我们将查看从 ARIMA 模型进行预测。
使用 ARIMA 进行预测
一旦我们有一个拟合的模型,我们就可以用该模型进行预测。如前几章所述,在做出预测时,我们应该创建一个训练-测试分割,以便我们有数据可以与预测进行比较。模型应该只拟合训练数据以避免数据泄露。我们可以使用pmdarima中的train_test_split函数来分割数据。然后我们进行常规步骤:分割、训练和预测。以下代码块显示了这一过程:
from pmdarima.model_selection import train_test_split
train, test =
train_test_split(y_macro_economic.realgdp.values,
train_size=0.9
)
arima = pm.auto_arima(
train, out_of_sample_size=10,
suppress_warnings=True, error_action='ignore',
test='adf'
)
preds, conf_int = arima.predict(
n_periods=test.shape[0], return_conf_int=True
)
以下代码使用auto_arima拟合 ARIMA 模型,然后使用 ARIMA 对象的predict方法预测测试集的大小。由代码生成的序列预测结果如图 11.21所示:

图 11.21 – 测试分割上的美国 GDP ARIMA 预测
在图 11.21中,美国 GDP 的预测似乎遵循数据的趋势,但没有捕捉到序列中的小变化。然而,这种变化在预测区间(标记为“区间”)中被捕捉到。这个模型似乎为测试数据提供了相当好的预测。请注意,区间随时间增加。这是因为预测在未来的不确定性增加。一般来说,短期预测更可能准确。
在本节中,我们基于 ARMA 模型,并使用差分将其扩展到非平稳数据,从而形成了 ARIMA 模型。在下一节中,我们将查看包含季节性影响的不平稳时间序列,并将进一步扩展 ARIMA 模型。
季节性 ARIMA 模型
让我们看看时间序列的另一个特征,称为季节性。季节性是指时间序列中存在一个在固定间隔重复的模式。季节性时间序列在自然界中很常见。例如,年天气模式和日阳光模式都是季节性模式。回到非平稳性部分的开始,我们展示了一个具有季节性的非平稳时间序列的例子。这个时间序列再次在图 11.22中展示,并附有它的 ACF 图。

图 11.22 – 航空客流量数据和 ACF 图
图 11.22中显示的时间序列是 1949 年至 1960 年国际航空旅客的月度总计[3]。在这个时间序列中有一个明显的重复模式。为了模拟这类数据,我们需要在 ARIMA 模型中添加一个额外的项来解释季节性。
季节性差分
我们将使用与 ARIMA 相同的方法来建模这种类型的时间序列。我们首先将使用差分来使数据平稳,然后对差分数据进行 ARMA 模型拟合。对于季节性时间序列,我们需要使用季节性差分来消除季节性影响,这可以通过数学公式表示:
y′t = yt − yt−T
其中 T 是季节的周期。例如,图 11.22中的时间序列表现出月度季节性,每个数据点代表一个月;因此,航空客流量数据的 T = 12。然后,对于航空数据,我们将使用以下差分方程来消除季节性:
y′t = yt − yt−12
我们还可以通过观察 ACF 图中峰值出现的位置来识别季节性。图 11.22中的 ACF 图显示在 12 处有一个峰值,表明季节周期为 12,这与我们对时间序列的了解一致。
在本节稍后,我们将使用pmdarima展示如何应用季节差分。让我们看看季节性是如何包含在模型中的。
季节性 ARIMA
如在 ARIMA 部分所述,我们将对原始序列进行差分,然后对差分数据进行平稳模型拟合。然后我们的时间序列将由以下方程描述,其中 y′t 是差分序列(包括季节性和顺序差分):
y′t = c + ϕ1 y′t−1 + … + ϕp y′t−p + ϵt + θ1 ϵt−1 + … + ϕq ϵt−q
我们可以使用后移符号表示整个模型:
(1 − ϕ1 B − … − ϕp Bp) (1 − B) d (1 − Bs)y t = c + (1 + θ1 B + … + θq Bq) 𝝐t
↑ ↑ ↑ ↑
AR(p) d 差分 季节差分 MA(q)
我们在方程中添加了一个考虑季节性的新项:(1 − Bs)。我们向模型添加了一个新的阶数参数:s。此模型通常表示为 ARUMA(p,d,q,s)。
SARIMA 模型
在本节中,我们仅涵盖季节差分。存在更复杂的模型,允许移动平均季节性和自回归季节性,称为 SARIMA,表示为 SARIMA(p,d,q)(P,D,Q)[m]。这些模型超出了本章的范围。然而,我们鼓励读者在掌握本章和下一章的主题后进一步探索这些模型。本章中涵盖的 ARIMA 模型是 SARIMA 模型的一个子集,它考虑了季节差分,即 SARIMA(p,d,q)(P,D,Q)[m]中的“D”阶数。
正如我们为 ARIMA 添加的(1 − B)d 项一样,(1 − Bs)项向单位圆添加了根。然而,与(1 − B)d 的根不同,(1 − Bs)的根均匀分布在单位圆周围。这些根可以使用numpy和matplotlib或使用计算智能工具(如 Wolfram Alpha www.wolframalpha.com/)进行计算和绘制。
拟合具有季节性的 ARIMA 模型
我们将采取以下步骤来拟合具有季节性的 ARIMA 模型:
-
使用差分去除季节性。
-
使用差分去除额外的非平稳性。
-
将平稳模型拟合到结果序列。
这本质上是我们用来拟合 ARIMA 模型的过程,但有一个额外的步骤来处理季节性成分。让我们通过一个使用航空公司数据的例子来讲解。
我们将首先使用差分来去除时间序列的季节性成分。回想一下,航空公司时间序列的季节周期是 12,这意味着我们需要在滞后 12 处进行差分,如下方程所示:
y′t = yt − yt−12
我们可以使用pmdarima中的diff函数执行此差分。以下代码显示了如何在航空公司数据上执行第 12 滞后差分:
import pmdarima as pm
from sktime import datasets
y_airline = datasets.load_airline()
series = pm.utils.diff( y_airline.values, lag=12)
在进行季节差分后,我们得到图 11.23中显示的差分序列以及 ACF 图。时间序列的季节部分似乎已经完全去除。差分序列似乎没有显示出任何重复的模式。此外,ACF 图没有显示原始数据 ACF 图中存在的季节性峰值:

图 11.23 – 季节差分后的航空公司数据
在去除时间序列的季节部分后,我们需要确定是否需要取任何额外的差分来使新的时间序列平稳化。图 11.23中的差分序列似乎显示出趋势。原始数据也显示出趋势。与之前一样,我们可以使用 Dickey-Fuller 测试来获取更多证据,以确定我们是否应该应用额外的差分。对这个序列运行 Dickey-Fuller 测试将得到一个 p 值为 0.099,这表明我们应该对序列取差分以解释单位根:
adf_test = pm.arima.ADFTest()
adf_test.should_diff(series)
# (0.09898694171553156, True)
对序列取一阶差分将导致图 11.24中显示的序列。在取这两个差分之后,序列似乎已经足够平稳化。

图 11.24 – 季节差分和一阶差分后的航空公司数据
图 11.24中的序列显示了航空公司数据的平稳化版本。根据 ACF 图,我们应该能够对平稳化序列拟合一个相对简单的 ARMA 模型。我们将使用auto_arima函数进行自动拟合,就像我们在 ARIMA 部分所做的那样:
pm.auto_arima(
series, error_action='ignore', trace=False,
suppress_warnings=True, maxiter=5, seasonal=False,
test='adf'
)
# ARIMA(1,0,0)(0,0,0)[0]
使用auto_arima对差分数据进行拟合返回一个 AR(1)模型。正如我们所期望的,这是一个简单的模型。
将所有这些放在一起,我们得到的模型是 ARUMA(1,1,0,12)。与之前的 ARIMA 示例一样,我们可以使用auto_arima来拟合这个模型,但我们在本例中详细说明了差分步骤,以帮助建立对每个差分元素对序列影响的理解。现在让我们看看auto_arima的直接拟合结果:
pm.auto_arima(
y_airline.values, error_action='ignore', trace=True,
suppress_warnings=True, maxiter=5, seasonal=True, m=12,
test='adf'
)
# Best model: ARIMA(1,1,0)(0,1,0)[12]
我们看到auto_arima找到了与我们使用手动差分得到相同的模型。注意模型以 SARIMA 格式表示(参见关于 SARIMA 的早期说明)。(0,1,0)[12]表示当季节性有一个差分时,季节性为 12。现在我们有了拟合模型,让我们看看我们对季节性模型的预测。
具有季节性的 ARIMA 预测
一旦我们有了拟合模型,我们就可以用这个模型进行预测。如 ARIMA 预测部分所述,我们应该何时进行训练-测试集划分以便我们有数据来比较预测结果?我们将使用相同的程序:分割数据,训练模型,并在测试集大小上进行预测:
train, test = train_test_split(y_airline.values, train_size=0.9)
sarima = pm.auto_arima(
train, error_action='ignore', trace=True,
suppress_warnings=True, maxiter=5, seasonal=True, m=12,
test='adf'
)
preds, conf_int = sarima.predict(n_periods=test.shape[0], return_conf_int=True)
前面的代码使用auto_arima拟合了一个完整的 SARIMA 模型,然后使用predict方法预测测试集的大小。由代码生成的序列预测显示在图 11.25中。

图 11.25 – 航空公司数据的 SARIMA 预测
图 11.25中的航空公司数据预测似乎很好地捕捉了数据的变动。这可能是由于时间序列中季节性成分的强度。请注意,预测区间随着时间的推移而增加,就像 ARIMA 预测区间一样,但区间遵循序列的一般模式。这是由于对序列中季节性的额外知识的影响。
在本节中,我们讨论了具有季节性的 ARIMA 模型,并展示了如何去除季节性成分。我们还研究了具有季节性的模型预测。在下一节中,我们将更详细地探讨验证时间序列模型。
更多关于模型评估
在前面的章节中,我们讨论了其他准备数据、测试和验证模型的方法。在本节中,我们将讨论如何验证时间序列模型,并介绍几种验证时间序列模型的方法。我们将涵盖以下模型评估方法:重采样、移动、优化持久预测和滚动窗口预测。
本节考虑的实际数据集是从 Yahoo Finance 数据库中收集的 1962 年 1 月 19 日至 2021 年 12 月 19 日的可口可乐股票数据,用于股票价格预测。这是一项时间序列分析,用于预测给定股票的未来股票价值。读者可以从 Kaggle 平台下载该数据集进行分析。为了激发研究兴趣,我们首先去探索可口可乐股票数据集:
data = pd.read_csv("COCO COLA.csv", parse_dates=["Date"], index_col="Date")

图 11.26 – 可口可乐数据集
日期索引与从 1962 年 1 月 19 日到 2021 年 12 月 19 日的 15096 个交易日相关。这里的High和Low列分别指每个交易日的最高和最低价格。Open和Close分别指在同一个交易日的市场开盘和收盘时的股价。每天交易的股票总量指的是Volume列,而最后一列(Adj Close)指的是调整后的值(结合股票分割、股息等)。为了说明重采样、移动、滚动窗口和扩展窗口的性能,我们仅使用 2016 年的Open列:
data= data[data.index>='2016-01-01'][['Open']]
数据是根据交易日期收集的。然而,我们将按月进行这项研究。使用重采样技术将数据从日聚合到月。这个想法激励我们引入这项技术。
重采样
resample()函数用于更改时间频率。以下代码展示了 Python 中的重采样技术:
fig, ax = plt.subplots(4,1, figsize=(12,8))
ax[0].set_title('Original stock price from 2016')
ax[0].plot(data)
ax[1].plot(data.resample('7D').mean())
ax[1].set_title('7 days - Downsampling stock price from 2016')
ax[2].plot(data.resample('M').mean())
ax[2].set_title('Monthly Downsampling stock price from 2016')
ax[3].plot(data.resample('Y').mean())
ax[3].set_title('Yearly Downsampling stock price from 2016')
fig.tight_layout(pad=5.0)
plt.show()
这是前一段代码的输出:

图 11.27 – Coco Cola 数据集的重采样
我们观察到当时间频率降低时,图表变得更加平滑。接下来,我们将讨论时间序列中使用的平移方法。
平移
在时间序列分析中,使用shift()函数创建新特征并不罕见:
data["price_lag_1"] = data["Open"].shift(1)
data.head()

图 11.28 – Coco Cola 股票数据的前五行,价格已调整一次
观察到price_lag_1列的第一行填充了一个 NaN 值。我们可以用fill_value参数替换缺失值:
data["price_lag_1"] = data["Open"].shift(1, fill_value = data['Open'].mean())
最后,我们讨论预测方法,如优化持久性和滚动窗口预测。有关这些方法的另一个资源可以在[3]中找到。
优化持久性预测
我们将使用重采样将 Coco Cola 股票价格的时间频率从 2016 年开始转换为月度频率,然后我们应用优化持久性预测技术,使用之前的观测值来预测未来的值。RMSE 得分用于评估持久性模型:
from sklearn.metrics import mean_squared_error
import math
train, test = data.resample('M').mean()['Open'][0:-24], data.resample('M').mean()['Open'][-24:]
persistence = range(1, 25)
RMSE_scores = []
for p in persistence:
history = [x for x in train]
pred = []
for i in range(len(test)):
# Prediction on test set
yhat = history[-p]
pred.append(yhat)
history.append(test[i])
# RMSE score performance
rmse = math.sqrt(mean_squared_error(test, pred))
RMSE_scores.append(rmse)
print(f'p={p} RMSE={rmse}')
输出如下:

图 11.29 – 优化持久性预测的 RMSE 得分
我们观察到当 p=6 时,RMSE 得分是最小的:

图 11.30 – 优化持久性预测,测试与预测对比
再次运行持久性测试,使用 p=6,我们可以看到以下结果:
history = [x for x in train]
pred = []
for i in range(len(test)):
# Prediction
yhat = history[-6]
pred.append(yhat)
history.append(test[i])
# Plots
plt.plot(list(test))
plt.plot(pred)
plt.show()
然后,我们可以生成一个可视化:

图 11.31 – 优化持久性预测
蓝色曲线是测试值,橙色曲线是预测值。
滚动窗口预测
这种技术创建了一个具有指定窗口大小的滚动窗口,然后在这个窗口内进行统计计算,用于预测,该预测会滚动通过研究中使用的数据。我们使用 2016 年的 Coco Cola 股票价格数据集,采用月度重采样数据集进行类似的研究:
from numpy import mean
train, test = data.resample('M').mean()['Open'][0:-24], data.resample('M').mean()['Open'][-24:]
window = range(1, 25)
RMSE_scores = []
for w in window:
history = [x for x in train]
pred = []
for i in range(len(test)):
# Prediction on test set
yhat = mean(history[-w:])
pred.append(yhat)
history.append(test[i])
# RMSE score performance
rmse = math.sqrt(mean_squared_error(test, pred))
RMSE_scores.append(rmse)
print(f'w={w} RMSE={rmse}')
plt.plot(window, RMSE_scores)
plt.title('Rolling Forecasting')
plt.xlabel('Windows sizes')
plt.ylabel('RMSE scores')
plt.show()

图 11.32 – 滚动窗口预测
当窗口大小为 9 时,RMSE 为 3.808 是最小的。再次运行 Python 代码,使用窗口大小=9,我们得到与优化持久性预测相似的可视化效果。
概述
在本章中,我们讨论了从平稳时间序列模型(如 ARMA)到非平稳模型(如 ARIMA)的各种建模方法来建模一元时间序列数据。我们从平稳模型开始,讨论了如何根据时间序列的特征来识别建模方法。然后,我们在平稳模型的基础上通过在模型中添加一个项来使时间序列平稳化。最后,我们讨论了季节性和如何在 ARIMA 模型中考虑季节性。虽然这些方法在预测方面非常强大,但它们并没有结合来自其他外部变量的潜在信息。正如前一章所述,外部变量可以帮助提高预测。在下一章中,我们将探讨时间序列数据的多元方法,以利用其他解释变量。
参考文献
请参考最终文档,了解参考文献的格式。
-
使用 R 进行应用时间序列分析,W. Woodward, H. Gray, A. Elliott. Taylor & Francis Group, LLC. 2017.
-
Box, G. E. P., Jenkins, G. M. 和 Reinsel, G. C. (1976) 时间序列分析、预测与控制。第三版。Holden-Day. 系列 G.
-
Brownlee, J, (2017) 简单时间序列预测模型以测试您不要欺骗自己 (
machinelearningmastery.com/simple-time-series-forecasting-models/).
第十二章:多变量时间序列
我们在上一章讨论的模型只依赖于感兴趣的单个变量的先前值。当我们的时间序列中只有一个变量时,这些模型是合适的。然而,在时间序列数据中通常会有多个变量。通常,这些系列中的其他变量可以改善感兴趣变量的预测。在本章中,我们将讨论具有多个变量的时间序列模型。我们将首先讨论时间序列变量之间的相关性关系,然后讨论我们如何对多变量时间序列进行建模。虽然存在许多多变量时间序列数据模型,但我们将讨论两种既强大又广泛使用的模型:带有外生变量的自回归积分移动平均(ARIMAX)和向量自回归(VAR)。理解这两个模型将扩展读者的模型工具箱,并为读者学习更多关于多变量时间序列模型提供构建块。
在本章中,我们将涵盖以下主要主题:
-
多变量时间序列
-
ARIMAX
-
VAR 模型
多变量时间序列
在上一章中,我们讨论了单变量时间序列或一个变量的时间序列的模型。然而,在许多建模情况下,通常会有多个同时测量的时间变化变量。由多个时间变化变量组成的时间序列被称为多变量时间序列。时间序列中的每个变量被称为协变量。例如,天气数据的时间序列可能包括温度、降雨量、风速和相对湿度。在天气数据集中,这些变量中的每一个都是单变量时间序列,而作为一个整体,多变量时间序列和每一对变量都是协变量。
从数学上讲,我们通常将多变量时间序列表示为一个向量值序列,如下所示:
X = x 0,0 x 0,1 ⋮ , x 1,0 x 1,1 ⋮ , … , x t,0 x t,1 ⋮
在这里,每个 X 实例在每个时间步长都有多个值,并且序列中有 t 个步骤。前面方程中的第一个索引代表时间步长(从 0 到 t),第二个索引代表单个序列(从 0 到 N 个变量)。例如,如果 X 是股票价格的时间序列,那么在 X 的每个时间步长,都会有每个股票价格的一个值。在这种情况下,如果序列中的前两个股票代码是 AAPL 和 GOOGL,那么 X 可能看起来像以下方程:
X = x 0, AAPL x 0, GOOGL ⋮ , x 1, AAPL x 1, GOOGL ⋮ , … , x t, APPL x t, GOOGL ⋮
当我们有一个多变量时间序列时,一个问题随之而来:我们能用这些额外的 信息做什么呢? 为了回答这个问题,我们需要了解这些时间序列变量是否相关。我们可以通过观察它们的交叉相关性来确定时间序列之间的关系。接下来,让我们讨论交叉相关性。
时间序列交叉相关性
我们需要了解时间序列如何相关,以便确定协变量时间序列是否可能对预测感兴趣的时间序列有用。在前面的一章中,我们讨论了如何使用图表和相关性系数来理解两个变量之间的关系;我们将对时间序列数据采取类似的方法。回想一下在第十章**,时间序列导论中讨论的以下交叉相关函数(CCF):
ˆ p k(X, Y) = ∑ t=1 n−k (X t − _ X )(Y t−k − _ Y ) ______________________ √ ____________ ∑ t=1 n (X t − _ X ) 2 √ ____________ ∑ t=1 n (Y t − _ Y ) 2
在这里,X 是一个一元时间序列,Y 也是一个一元时间序列。这个函数告诉我们 X 和 Y 在 k 滞后时的关系。在这个方程中,Y 比 X 滞后 k 个时间步。在这种情况下,我们说 X 领先 Y k 步或 Y 滞后 X k 步。我们将使用这个 CCF 来帮助我们确定两个时间序列是否相关以及它们在哪个时间步相关。请注意,如果 X = Y,那么 CCF 将简化为自相关函数(ACF)。
交叉相关性显著性
正如 ACF 一样,交叉相关性有一个统计显著性的阈值。我们认为任何绝对值大于 1.96 / √ _ N(其中 N 是时间序列的样本大小)的交叉相关性值与零显著不同。
让我们通过一些数据来观察交叉相关性。这些数据[1]来自UCI 机器学习仓库 [2]。这些数据是作为在中国进行的一项污染研究的一部分收集的天气数据。这些数据在多年的时间里每小时采样一次。我们将数据子集缩小到前 1,000 个数据点。这个数据集中的天气数据包括温度(TEMP)、气压(PRES)、露点温度(DEWP)、降雨量(RAIN)和风速(WSPM)。我们将从试图预测风速的角度来观察交叉相关性,这意味着我们将观察风速与其他变量的交叉相关性。这可能是一个想要预测风力涡轮机发电的人的重要问题。风速数据和风速的 ACF 在图 12.1中绘制:

图 12.1 – 风速时间序列及其 ACF 的绘图
从图 12.1中的图表可以看出,风速可能可以通过我们在上一章讨论的平稳单变量模型进行建模。虽然似乎没有证据表明存在自回归积分移动平均(ARIMA)行为,但在滞后 24 处似乎存在季节性的证据。由于数据是按小时采样的,季节性可能是一个日周期模式。我们在图 12.2中绘制了其他变量的时间序列。这些是我们可能想要用来帮助预测风速值的其他变量:

图 12.2 – 风速时间序列和三个其他天气变量的图
尽管我们没有绘制图 12.2中所示的所有时间序列的 ACF 图,但这些时间序列具有表明它们可能是平稳的特征。然而,仅从时间序列本身来看,并不能确定这些时间序列是否相关。这就是时间序列交叉相关性的作用所在。我们可以使用交叉相关图来确定其他时间序列是否与风速相关。我们在图 12.3中绘制了风速与其他变量的交叉相关性。CCF 图绘制到 48 个滞后。由于数据是按小时采样的,这给我们提供了 2 天的滞后时间来观察交叉相关性:

图 12.3 – 风速和三个其他变量的 CCF 图
图12.3中的图表显示风速与其他变量之间存在交叉相关性。虽然我们不太关注交叉相关性的形状,但这些模式可能为变量之间的关系提供一些深度。例如,风速和温度的交叉相关性显示出大约 12 小时的振荡模式;这可能对应于阳光对风速的影响。主要发现是这些其他变量与风速存在关联,可能帮助我们预测风速的值。让我们看看我们如何可以使用额外的变量进行建模。
ARIMAX
在上一章中,我们讨论了 ARIMA 模型族,并演示了如何对单变量时间序列数据进行建模。然而,正如我们在上一节中提到的,许多时间序列是多变量的,例如股票数据、天气数据或经济数据。在本节中,我们将讨论在建模时间序列数据时如何整合协变量变量的信息。
当我们建模多变量时间序列时,我们通常有一个我们感兴趣的预测变量。这个变量通常被称为内生变量。多变量时间序列中的其他协变量被称为外生变量。回想一下第十一章**,ARIMA 模型,代表 ARIMA 模型的方程:
y′ t = c + ϕ 1 y′ t−1 + … + ϕ p y ′ t−p + ϵ t + θ 1 ϵ t−1 + … + ϕ q ϵ t−q
在这里,y′ t 是差分序列,差分 d 次直到它平稳。在这个模型中,y t 是我们感兴趣的预测变量;它是内生变量。让我们以更紧凑的形式重写这个方程。我们将与 y′ t 相关的项收集到一个求和项中,将与ϵ t 相关的项收集到另一个求和项中。让我们从与 y′ t 相关的项开始:
ϕ 1 y′ t−1 + … + ϕ p y ′ t−p = ∑ i=1 p ϕ i y′ t−i
然后,我们将ϵ t 项也收集到不同的求和项中:
ϵ t + θ 1 ϵ t−1 + … + ϕ q ϵ t−q = ϵ t + ∑ j=1 q ϕ j ϵ t−j
将这些求和项合并,我们得到以下更紧凑的 ARIMA 方程版本:
y′ t = c + ∑ i=1 p ϕ i y′ t−i + ϵ t + ∑ j=1 q ϕ j ϵ t−j
现在,让我们将外生变量添加到模型中。我们将通过向模型中添加一个求和项来添加外生变量:
y′ t = c + ∑ i=1 p ϕ i y′ t−i + ϵ t + ∑ j=1 q ϕ j ϵ t−j + ∑ k=1 r β k X tk
在这里,向量值变量 X 中包含 r 个外生变量。回想一下本章前面的内容,我们可以用以下方式表示向量值变量,其中第一个索引表示时间步,第二个索引表示向量值变量中的单个序列:
X = x 0,0 ⋮ x 0,k, x 1,0 ⋮ x 1,k, … , x t,0 ⋮ x t,k
这就是我们如何将外生变量纳入 ARIMA 模型的方法。现在,让我们谈谈如何选择外生变量。在第十一章**,ARIMA 模型中,我们使用 ACF 图看到,对于建模单变量时间序列,时间序列的某些滞后比其他滞后更有影响力。我们将使用 CCF 图对外生变量做出类似的判断。让我们看一下示例 CCF 图并讨论如何做出这些判断。
风速和温度的 CCF 图显示在图 12**.4中。我们已标记出在 48 小时滞后期内最显著的交叉相关发生的位置。回想一下,这些数据是每小时采样的;因此,每个滞后时间步对应 1 小时的滞后。该图显示最显著的交叉相关发生在大约滞后 13、滞后 24 和滞后 37 处:

图 12.4 – 风速和温度变量的 CCF 图
根据对图 12**.4中交叉相关图观察,我们预计温度在 13、24 和 37 个滞后处可以提供对当前预测风速有用的信息。也就是说,变量 x t−13,temperature、x t−24,temperature 和 x t−37,temperature 可能对预测 y t 有用。我们可以对其他两个外生变量执行相同的 CCF 分析,以确定模型中要使用哪些滞后。我们发现压力(PRES)在滞后 14 和 37 处有显著的交叉相关性,露点(DEWP)在滞后 2 和 20 处有显著的交叉相关性。现在我们已经评估了模型中想要使用的外生变量的滞后,让我们继续预处理数据和拟合模型。
预处理外生变量
在拟合模型之前,我们需要预处理数据,以便制作之前提到的外生变量。我们需要创建之前描述的时间序列变量的滞后版本。我们可以使用pandas DataFrame的shift()方法来滞后时间序列。让我们看看一个代码示例。
以下代码示例展示了如何使用shift()方法将压力变量滞后14个时间步。代码的前几行导入库并加载数据。代码示例中的最后一行将PRES变量向后移动 14 个时间步:
import pandas as pd
# code to download the data
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00501/PRSA2017_Data_20130301-20170228.zip'
file_name = url.split('/')[-1]
r = requests.get(url)
with open(file_name, 'wb') as fout:
fout.write(r.content)
with zipfile.ZipFile(file_name, 'r') as zip_file:
zip_file.extractall('./')
df = pd.read_csv(
'./PRSA_Data_20130301-20170228/PRSA_Data_Aotizhongxin_20130301-20170228.csv'
, sep=','
)
pres_lag_14 = df.PRES.shift(-14)
shift()方法可以用来将序列向前或向后移动时间。在这种情况下,我们希望我们的外生变量滞后我们想要预测的变量。因此,我们需要提供一个负的移动值来将序列向后移动时间。我们将对其他外生变量使用类似的预处理步骤来创建其他滞后变量。这将在以下代码示例中展示。所有这些使用shift()方法创建的滞后变量都被收集到一个新的DataFrame中,称为X,我们将在拟合步骤中使用它:
temp_lag_13 = df.TEMP.shift(-13)
temp_lag_24 = df.TEMP.shift(-24)
temp_lag_37 = df.TEMP.shift(-37)
pres_lag_14 = df.PRES.shift(-14)
pres_lag_24 = df.PRES.shift(-37)
dewp_lag_13 = df.DEWP.shift(-2)
dewp_lag_24 = df.DEWP.shift(-20)
X = pd.DataFrame({
"temp_lag_13": temp_lag_13[:1000],
"temp_lag_24": temp_lag_24[:1000],
"temp_lag_37": temp_lag_37[:1000],
"pres_lag_14": pres_lag_14[:1000],
"pres_lag_24": pres_lag_24[:1000],
"dewp_lag_2": dewp_lag_13[:1000],
"dewp_lag_20": dewp_lag_24[:1000],
})
在前面的代码示例中,我们正在取滞后变量(前1,000个数据点)的子集,以限制模型的计算时间。在收集到新的X DataFrame中的变量后,预处理完成,我们可以继续进行模型的拟合和评估。
拟合模型
如前一章所述,我们将使用auto_arima来拟合我们的 ARIMA 模型。在这种情况下,主要区别是我们需要向auto_arima函数提供我们创建的外生变量。拟合带有外生变量的 ARIMA 模型的代码在以下代码示例中展示:
y = df.WSPM[:1000]
arima = pm.auto_arima(
y, X,
error_action='ignore',
trace=True,
suppress_warnings=True,
maxiter=5,
seasonal=False,
test='adf'
)
这段代码应该看起来与*第十一章**中展示的代码非常相似,即 ARIMA 模型。事实上,唯一的区别是auto_arima函数中添加了X变量。运行此代码将产生以下输出:
Performing stepwise search to minimize aic
ARIMA(2,0,2)(0,0,0)[0] : AIC=2689.456, Time=0.24 sec
ARIMA(0,0,0)(0,0,0)[0] : AIC=8635.103, Time=0.16 sec
ARIMA(1,0,0)(0,0,0)[0] : AIC=2742.728, Time=0.29 sec
ARIMA(0,0,1)(0,0,0)[0] : AIC=2923.324, Time=0.32 sec
ARIMA(1,0,2)(0,0,0)[0] : AIC=2685.581, Time=0.23 sec
ARIMA(0,0,2)(0,0,0)[0] : AIC=2810.239, Time=0.37 sec
ARIMA(1,0,1)(0,0,0)[0] : AIC=2683.888, Time=0.23 sec
ARIMA(2,0,1)(0,0,0)[0] : AIC=2682.988, Time=0.28 sec
ARIMA(2,0,0)(0,0,0)[0] : AIC=2681.309, Time=0.26 sec
ARIMA(3,0,0)(0,0,0)[0] : AIC=2682.416, Time=0.32 sec
ARIMA(3,0,1)(0,0,0)[0] : AIC=2684.608, Time=0.32 sec
ARIMA(2,0,0)(0,0,0)[0] intercept : AIC=2683.328, Time=0.30 sec
Best model: ARIMA(2,0,0)(0,0,0)[0]
Total fit time: 3.347 seconds
再次强调,这个输出应该与我们之前在 第十一章“ARIMA 模型”中看到的 auto_arima 输出非常相似。它表明我们应该使用基于 auto_arima 适配的 AR(2) 模型,因为仅需要为模型中的每个外生变量适配一个系数。不需要在 auto_arima 返回的对象上适配 summary() 方法来查看外生变量的系数和显著性检验。运行 summary() 方法将输出以下 带有外生因素的季节自回归积分移动平均(SARIMAX)结果:

图 12.5 – AR(2) 模型的 SARIMAX 摘要
在前面代码片段中显示的 summary() 方法的输出显示了大量的信息。让我们关注输出的中间部分。中间部分显示了每个外生变量和每个 ARIMA 系数的系数和显著性检验。系数和检验的 p 值分别标记为 coef 和 P>|z|。显著性检验可以帮助我们确定哪些变量应该包含在模型中。
回想一下在 第七章“多元线性回归”中,我们讨论了多重共线性这一概念,它本质上意味着两个或更多预测变量高度相关。我们在这里可能会遇到类似的情况。例如,考虑 temp_lag_13、temp_lag_24 和 temp_lag_37 变量。由于温度时间序列表现出自相关性,这些温度时间序列的滞后版本可能是高度相关的,甚至很可能是这样。当我们观察到多重共线性时,这是一个我们可以从模型中移除变量的信号。我们建议使用在第七章[*]中描述的特征选择方法之一,例如递归特征选择、基于性能的选择或基于统计显著性的选择。
前面的输出只显示了当前模型中的三个外生变量,这些变量被突出显示。这意味着我们应该考虑减少模型中的变量。我们可以通过多种方法做到这一点;在这个例子中,我们将选择基于 p 值消除特征。我们将迭代地移除具有最高 p 值的特征,直到所有特征的 p 值都低于 0.05。这个选择过程的代码如下所示:
# initial model
arima = pm.auto_arima(
y, X,
error_action='ignore',
trace=False,
suppress_warnings=True,
maxiter=5,
seasonal=False,
test='adf'
)
pvalues = arima.pvalues()
iterations = 0
while (pvalues > 0.05).any():
# get the variable name with the largest p-value
variable_with_max_pval = pvalues.idxmax()
# drop that variable from the exogenous variables
X = X.drop(variable_with_max_pval, axis=1)
arima = pm.auto_arima(
y, X,
error_action='ignore',
trace=False,
suppress_warnings=True,
maxiter=5,
seasonal=False,
test='adf'
)
pvalues = arima.pvalues()
print(f"fit iteration {iterations}")
iterations += 1
执行这个选择过程将导致一个只包含 temp_lag_24、temp_lag_37、pres_lag_24 和 dewp_lag_2 外生变量的模型。模型的 ARIMA 部分仍然是 AR(2) 模型。在模型选择完成后,让我们评估模型的表现。
评估模型性能
我们将通过绘制预测与实际数据点的对比图,并计算误差估计来评估模型的性能。让我们首先查看 ARIMAX 模型对下一个200个观测值的预测。预测和测试数据点在图 12.6中绘制:

图 12.6 – 使用 ARIMAX 模型预测风速
我们可以看到使用我们在图 12.6中创建的 ARIMAX 模型预测的风速。从绘制的预测中,有两个观察结果突出:
-
预测捕捉了
风速变量的振荡 -
预测未能捕捉到
风速中的高峰值
为了了解提供的自变量是否在模型中提供了物质价值,我们应该将图 12.6中显示的 ARIMAX 模型的预测与单变量模型的预测进行比较。单变量模型的预测显示在i 图 12.7:

图 12.7 – 使用 ARIMA 模型预测风速
如图 12.7中所示,ARIMA 模型的预测似乎没有捕捉到随着预测长度增长的时间序列中的很大变化。这实际上是 ARMA 模型的一个特性。从两个图中应该很明显,ARIMAX 模型比 ARIMA 模型提供了更好的预测。最后,让我们比较两个模型的均方误差(MSE)。ARIMAX 模型的 MSE 大约为 1.5,而 ARIMA 模型的 MSE 约为 1.7。MSE 值证实了我们从图中看到的情况——即 ARIMAX 模型为这个多元时间序列提供了更好的预测。
在本节中,我们讨论了 ARIMA 模型的扩展,使我们能够将协变量信息纳入多元时间序列。这仅仅是多元时间序列的许多方法之一。在下一节中,我们将讨论另一种处理多元时间序列数据的方法。
VAR 建模
在上一章中,我们研究的 AR(p),MA(q),ARMA(p,q),ARIMA(p,d,q)m 和 SARIMA(p,d,q) 模型构成了多元 VAR 模型的基础。在本章中,我们讨论了带有外生变量的 ARIMA(ARIMAX)。现在,我们将开始讨论 VAR 模型。首先,重要的是要理解,虽然 ARIMAX 需要外生变量的领先(未来)值,VAR 模型不需要这些变量的未来值,因为它们彼此都是自回归的——因此得名向量自回归——并且根据定义不是外生的。为了开始,让我们考虑两个变量,或双变量,的情况。考虑一个过程 y_t,它是两个不同输入变量 y_t¹ 和 y_t² 的输出。请注意,在矩阵形式中,我们讨论的是 nxm 矩阵(y_n,m)的情况,其中 n 对应时间点,m 对应涉及的变量(变量 1,2,…,m)。我们将在后续的符号中省略逗号,但重要的是要理解,当我们讨论多元自回归过程时,我们讨论的是多维矩阵中的数据,而不仅仅是用于单变量时间序列分析的单维向量形式。我们可以如下用向量符号表示我们的过程 y_t:
y_t = [y_t¹ y_t²].
我们可以进一步扩展输入变量的定义,这里,对于之前显示的 y_t 中的两个变量,作为 VAR(1) 过程:
y_t¹ = (1 − ϕ_11) μ_1 − ϕ_12 μ_2 + ϕ_11 y_{t−1}¹ + ϕ_12 y_{t−1}² + ε_t¹
y_t² = − ϕ_21 μ_1 + (1 − ϕ_22) μ_2 + ϕ_21 y_{t−1}¹ + ϕ_22 y_{t−1}² + ε_t².
项 (1 − ϕ_11) μ_1 − ϕ_12 μ_2 和 − ϕ_21 μ_1 − (1 − ϕ_22) μ_2 是我们的模型常数(β 系数)。
我们可以将前面的过程(使用零均值形式)简化为矩阵简化形式,如下所示:
y_t = Φ_1 y_{t−1} + ε_t
在这里,我们有以下内容:
Φ_1 = [ϕ_11 ϕ_12 ϕ_21 ϕ_22]
我们对 VAR(1) 模型的预测如下:
ˆy_t¹(l) = (1 − ϕ_11) ˆy_1 − ϕ_12 ˆy_2 + ϕ_11 ˆy_t¹(l − 1) + ϕ_12 ˆy_t²(l − 1)
ˆy_t²(l) = − ϕ_21 ˆy_1 + (1 − ϕ_22) ˆy_2 + ϕ_21 ˆy_t¹(l − 1) + ϕ_22 ˆy_t²(l − 1)
它还包括以下内容:
ˆy_t⁰(l) = [ˆy_t¹(l) ˆy_t²(l)]
在这里,l 代表前向滞后(t + 1,t + 2,…,t + n),ˆy_t⁰m 代表变量 m 在时间 t 的预测值,t_0 对应时间上最接近的点(通常是数据的长度)。还应注意 mxm 协方差矩阵如下:
ˆΓ(k) = ˆγ_11(k) … ˆγ_1m(k) ⋮⋱⋮ ˆγ_m1(k) … ˆγ_mm(k)
在这里,每个协方差 ˆγ_ij(k),其中 k 对应滞后,计算如下:
ˆγ_ij(k) = 1/n ∑_{t=1}^{n−k} (y_t^i − ˆy_i)(y_{t+k,j} − ˆy_j).
因此,我们计算每个变量 y_t^i 和 y_t^j 之间的估计互相关如下:
ˆρ_ij(k) = ˆγ_ij(k) √___________ ˆγ_ii(0) ˆγ_jj(0)
理解交叉相关和,尤其是协方差的计算方法有助于理解变量之间的关系及其对模型拟合的影响。将两个模型结合以生成一个整体过程预测的方法是通过加权两个输入均值及其相关性到一个响应中。正如我们之前在 VAR(1)预测中看到的那样,每个给定变量与其他变量在不同时间点的相关性可以使用 VAR 模型进行建模。值得注意的是,在 VAR 模型中没有真正的独立和依赖变量;只是每个变量之间存在相互作用。重要的是要注意,所有使用 VAR 模型建模的过程都应该是平稳的。
使用在www.statsmodels.org/dev/datasets/generated/macrodata.xhtml找到的美国宏观经济数据集,我们可以观察到在 VAR 建模的零均值形式下,实际国内私人投资(realinv)、实际个人消费支出(realcons)和实际私人可支配收入(realdpi)之间的关系。
realcons(t) = ϕ 11 realcons t−11 + ϕ 12 realinv t−12 + ϕ 13 realdpi t−13 + ε t1
realinv(t) = ϕ 21 realcons t−11 + ϕ 22 realinv t−12 + ϕ 23 realdpi t−13 + ε t2
realdpi(t) = ϕ 31 realcons t−11 + ϕ 32 realinv t−12 + ϕ 33 realdpi t−13 + ε t3
让我们用一个例子来看看如何在 Python 中使用statsmodels VARMAX 模型对这个数据进行 VAR 过程分析。首先,让我们加载数据:
import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
from statsmodels.tsa.api import VAR
data=sm.datasets.macrodata.load_pandas().data
data.sort_values(by=['year', 'quarter'], inplace=True)
data['yr_qtr'] = data['year'].astype(str) + data['quarter'].astype(str)
print('{}% of yearly quarters are unique'.format(round(100*(data['yr_qtr'].nunique() / len(data)), 1)))
从输出中我们可以看到,我们每年的季度都是唯一的,因此不存在时间戳重复:
100.0% of yearly quarters are unique
在执行基于回归的时间序列建模时,我们首先需要确保数据的平稳性。在 VAR 建模中,这意味着所有变量都必须是平稳的。第一步是通过按索引进行线绘图来可视化过程实现。如果数据点按要建模的间隔聚合,则可以保留索引不变,但如果需要不同的时间索引,则需要指定这一点。在这里,我们将构建一个模型来确定数据长度对应的季度,因为这是时间的最小单位,并且每个索引都有一个年度季度,正如我们之前检查的那样。然后我们将索引设置为季度(这就是为什么季度出现了两次;一次作为索引,一次作为列名):
data['quarter'] = range(1, len(data)+1)
data.drop('yr_qtr', axis=1, inplace=True)
data.index = data['quarter']
注意更新的索引和季度值:
| 季度 | 年份 | 季度 | 实际消费 | 实际投资 | 实际可支配收入 |
|---|---|---|---|---|---|
| 1 | 1959 | 1 | 1707.4 | 286.898 | 1886.9 |
| 2 | 1959 | 2 | 1733.7 | 310.859 | 1919.7 |
| 3 | 1959 | 3 | 1751.8 | 289.226 | 1916.4 |
| 4 | 1959 | 4 | 1753.7 | 299.356 | 1931.3 |
| 5 | 1960 | 5 | 1770.5 | 331.722 | 1955.5 |
图 12.8 – VAR 建模的宏观数据集的前五行
在这里,我们将通过六个步骤来使用我们导入和准备的数据集构建一个 VAR 模型,部分内容如图12.8所示。
第 1 步 – 视觉检查
在图 12.9的数据线图中,我们可以看到似乎有一个强烈的线性趋势。我们需要检查 ACF 图来评估这一点。之后,我们将运行Dickey-Fuller 测试来确认:

图 12.9 – realcons、realdpi 和 realinv 的线图
图 12.10中的 ACF 图显示出强烈的线性趋势和至少一个单位根的迹象,这很可能是导致这种趋势的原因:

图 12.10 – VARMAX 输入变量的 ACF 图
在运行 Dickey-Fuller 测试后,我们可以看到每个变量都包含一个单位根,因此是趋势性的。因此,我们需要对所有三个变量应用一阶差分,以便我们可以朝着使数据平稳化的方向发展。回想一下,Dickey-Fuller 的零假设是存在一个单位根,备择假设是不存在。我们将执行以下代码:
from statsmodels.tsa.stattools import adfuller
for col in ['realcons','realinv','realdpi']:
adfuller_test = adfuller(data[col], autolag='AIC')
print('ADF p-value for {}: {}'.format(col, adfuller_test[1]))
在这里,我们可以看到每个变量的 Dickey-Fuller 结果。回想一下,零假设是存在一个单位根。因此,由于 p 值不高,我们不会拒绝零假设。这为我们提供了统计证据,假设存在趋势:
ADF p-value for realcons: 0.9976992503412904
ADF p-value for realinv: 0.6484956579101141
ADF p-value for realdpi: 1.0
让我们用循环来区分这里的数据:
import numpy as np
data_1d = pd.DataFrame()
for col in ['realcons','realinv','realdpi']:
data_1d[col] = np.diff(data[col], n=1)
第 2 步 – 选择 AR(p)的阶数
现在,我们可以重新运行 ACF 图来检查是否仍然看到趋势自相关。因为原始的线图看起来没有季节性,我们将继续绘制部分 ACF(PACFs)来了解可能有用的 AR(p)阶数。如果我们怀疑存在季节性,检查样本自相关的频谱密度来量化季节周期性可能是有用的,这个过程我们在第十一章“ARIMA 模型”中演示过。在检查 PACF 图时,我们将使用Yule-Walker 方法。
我们可以在 ACF 图中看到没有明显的季节性。在使用 VAR 模型时,所有项都用于估计所有其他项,因为任何项的预测都是通过估计所有项来发展的。换句话说,为了预测一个,我们必须预测所有。这之所以重要,是因为当我们查看图 12.11中的 PACFs 时,我们可以看到一个变量可以用 AR(1)(实际投资)模型拟合,另一个可以用 AR(2)(实际可支配收入)模型拟合,另一个可以用 AR(3)(实际消费)模型拟合。使用对每个变量都显著的阶数是使 VAR 建模工作的重要方面之一。
在 VAR 模型中选择排序的一个重要方面是交叉相关。例如,我们可能有一个变量的最高阶数为 3 阶,但如果目标变量与输入变量之间的交叉相关在滞后 5 期,我们很可能需要使用滞后 5 期。然而,使用滞后 3 期或 4 期的模型也可能是有用的。然而,与任何回归模型一样,我们总是希望在模型训练后检查系数项的显著性:

图 12.11 – 差分 VARMAX 变量;自相关函数(ACF)和偏自相关函数(PACF)
第 3 步 – 评估交叉相关
对于 VAR 模型,我们考虑的输入变量和目标变量之间的交叉相关最为重要。这可以通过多个滞后或使用最显著的滞后来实现,如果担心使用不同位移的同一变量的多次迭代会导致过拟合。我们必须确认在预测模型中使用的交叉相关分析是针对每个输入变量和因变量的。一个显示输入变量作为目标变量的领先指标的交叉相关不是主要问题;这意味着使用输入的历史值来预测目标。然而,如果我们有一个输入是滞后指标的场景——意味着目标预测输入——并且我们想使用输入来预测目标,我们需要将输入向前移动,以便相关性在滞后 0 期,并通过移动的量来裁剪数据。
在许多情况下,存在显著的领先和滞后交叉相关;这可能会表明季节性。在季节性的 VAR 模型中,应该包含一个指示变量,该变量在频谱密度峰值时提供值为 1,在其他情况下提供值为 0。交叉相关的一个特殊情况是输入变量具有显著的统计显著性,但仅在时间上非常远的滞后。在这种情况下,可能需要将数据向前移动,以便模型不包括(自相关)不显著的项,以避免过拟合的风险。为了演示基于 CCF 的移动,我们将在下一个过程中将其纳入我们的流程。
在 图 12.12 中,我们观察到 realdpi 和 realinv 之间最强的相关性出现在滞后 0,但在图中 x=-27 和 x=-37 处至少还有两个其他类似相关性的滞后。正如我们在 ARIMAX 部分所看到的,我们可以将同一变量的多个平移版本作为新变量包括进来,以提高模型的预测能力。这将使我们能够利用多个相关性。然而,出于模型演示的目的,我们不会将其包括在内,因为 VAR 汇总的结果可能会变得很大。此外,正如我们提到的,包括太多相同变量的滞后可能会导致过拟合。最终决定使用多少滞后取决于实践者。这必须通过评估模型拟合和偏差/方差权衡来完成。这里给出的函数代码也在 第十章**,时间序列导论 中:
# code imported from chapter 10
def plot_ccf(data_a, data_b, lag_lookback, percentile, ax, title=None):
n = len(data_a)
ccf = correlate(data_a - np.mean(data_a), data_b - np.mean(data_b), method='direct') / (np.std(data_a) * np.std(data_b) * n)
_min = (len(ccf)-1)//2 - lag_lookback
_max = (len(ccf)-1)//2 + (lag_lookback-1)
zscore_vals={90:1.645,
95:1.96,
99:2.576}
markers, stems, baseline = ax.stem(np.arange(-lag_lookback,(lag_lookback-1)), ccf[_min:_max], markerfmt='o', use_line_collection = True)
z_score_95pct = zscore_vals.get(percentile)/np.sqrt(n) #1.645 for 90%, 1.96 for 95%, and 2.576 for 99%
ax.set_title(title)
ax.set_xlabel('Lag')
ax.set_ylabel('Correlation')
ax.axhline(y=-z_score_95pct, color='b', ls='--')# Z-statistic for 95% CL LL
ax.axhline(y=z_score_95pct, color='b', ls='--')# Z-statistic for 95% CL UL
ax.axvline(x=0, color='black', ls='-');
plot = plot_ccf(data_a=data_1d['realdpi'], data_b=data_1d['realinv'], lag_lookback=50, percentile=95)

图 12.12 – 实际对数和实际逆的互相关
在 图 12.13 中,我们可以看到 realcons 是 realinv 的领先指标,两者呈正相关。这意味着消费的增加会导致投资增加。尽管如此,我们仍然可以使用 realinv 的下移版本来预测 realcons——如果这是我们的目标——因为这两个变量预计将无限期地继续:
plot = plot_ccf(data_a=data_1d['realcons'], data_b=data_1d['realinv'], lag_lookback=50, percentile=95)

图 12.13 – 实际常数和实际逆的互相关
为了解决 realcons 对 realinv 的一滞后领先问题,让我们将 realcons 向前移动一个索引,以便最强的相关性出现在滞后 0。这对于 VAR 模型不是必需的,但在最显著的滞后远在时间上并且可能导致包括不合理的模型阶数以包含必要的变量关系的情况下可能是有用的。
关于数据平移的说明
在平移数据时,数据集中将包含空值。因此,数据集必须根据平移的长度进行裁剪。这可能会以模型性能为代价,因此值得通过比较模型误差来评估平移的需要。
在以下代码片段中对 realcons 应用前移平移并重新运行 CCF 后,我们可以在 图 12.14 中看到变量的最强相关性现在出现在滞后 0:
data_1d['realcons'] = data_1d['realcons'].shift(1)
data_1d = data_1d.iloc[1:]
plot = plot_ccf(data_a=data_1d['realcons'], data_b=data_1d['realinv'], lag_lookback=50, percentile=95)

图 12.14 – 实际逆和前移实际常数的互相关
接下来,我们想要运行一个函数来追加 p 和 q 的可能值(AR(p) 和 MA(q)):
from statsmodels.tsa.statespace.varmax import VARMAX
import warnings
warnings.simplefilter('error')
results_aic=[]
x_axis_list=[]
for p in range(1,6):
for q in range(0,6):
try:
model = VARMAX(data_1d,
order=(p,q),
trend='c',
enforce_stationarity=True,
enforce_invertibility=True)
results = model.fit()
results_aic.append(results.aic)
# x_axis_list.append(p,q,results.aic)
print('(p,q): ({},{}), AIC: {}'.format(p,q,results.aic))
except Exception as e:
# print('(p,q): ({},{}), error: {}'.format(p,q,e))
Pass
在这里,我们可以看到 VAR(1,0) 和 VAR(2,0) 模型的 AIC 错误:
(p,q): (1,0), AIC: 6092.740258665657
(p,q): (2,0), AIC: 6050.444583720871
通过 AIC 选择方法,我们有两种可能的模型。首先,我们构建了一个p=2和q=0的模型,但在运行后,我们观察到在预测realinv的七个系数中,只有两个在 0.10 的显著性水平上是显著的,这并不好。在重新运行并选择p=1和q=0后,我们发现四个系数中有两个在 0.05 的显著性水平上显著,并且在 0.10 的显著性水平上全部显著。因此,我们可以得出结论,realinv。
第 4 步 – 构建 VAR(p,q)模型
我们将跳过上一章中介绍过的模型验证步骤,因为在这里的过程将是相同的。我们将在这里重新排序列,以便当我们运行get_prediction()函数时,结果是对第一列的。由于所有变量都用于预测所有其他变量,因此模型将保持不变。重新索引后,我们将运行带有realcons上移一个位置的VARMAX模型,并预测realinv。对于我们的模型,我们使用了 AR(1)和 MA(0)。请注意,我们使用trend='c'来表示常数,因为我们进行差分时移除了trend;trend参数用于趋势确定性。趋势差分应在模型之外处理,因为并非所有变量都可能具有单位根趋势。在模型之外处理趋势还有助于我们识别季节性或移除trend后的真实自相关结构的显著性。值得注意的是,季节性并没有直接通过 VAR(和 VARMAX)模型来处理;应包括表示季节性的指示(虚拟)变量。
关于模型拟合,我们可以参考第十一章**,ARIMA 模型,以获取有关下一节中显示的Dep. Variable列表的信息。每个模型变量都列出了用于预测每个变量的其他变量的项的系数。例如,我们可以在Results for equation realcons中观察到realinv对realcons的预测能力贡献不大,但realdpi却贡献较大。我们还可以观察到realcons对预测realinv是显著的。我们还可以根据realinv和realdpi中的 p 值和置信区间进行观察。如果我们有一个特征中的方差由另一个特征中的方差解释,我们可以期望看到更大的系数值:
model = VARMAX(data_1d.reindex(columns=['realinv','realcons','realdpi']), order=(1,0), trend='c',
enforce_stationarity=True,enforce_invertibility=True)
results = model.fit()
print(results.summary())

图 12.15 – VAR(1)模型输出
第 5 步 – 测试预测
需要注意的是,与长期历史表现相比,VAR 模型的预测实际上相当稳定。我们看到预测结束时出现如此极端差异的原因是,这个时间段对应于大衰退,该衰退从 2007 年持续到 2009 年。我们测试预测的最后五个点对应于 2008 年的最后两个季度和 2009 年的前三个季度:
df_pred = results.get_prediction(start=195, end=200).summary_frame(alpha=0.05)
fig, ax = plt.subplots(1,1,figsize=(20,5))
ax.plot(data_1d['realinv'], marker='o', markersize=5)
ax.plot(df_pred['mean'], marker='o', markersize=4)
ax.plot(df_pred['mean_ci_lower'], color='g')
ax.plot(df_pred['mean_ci_upper'], color='g')
ax.fill_between(df_pred.index, df_pred['mean_ci_lower'], df_pred['mean_ci_upper'], color='g', alpha=0.1)
ax.set_title('Test Forecast for VAR(1)')

图 12.16 – VARMAX(p=1)的测试预测
我们可以将我们的预测(均值)与投资变量的实际值(realinv)进行比较,如下所示:
pd.concat([df_pred,data_1d['realinv'].iloc[195:]], axis=1)
在图 12**.17中,我们可以看到 VAR(来自VARMAX函数)模型测试预测:
mean |
mean_se |
mean_ci_lower |
mean_ci_upper |
realinv |
|
|---|---|---|---|---|---|
| 196 | -12.4348 | 40.79603 | -92.3935 | 67.52398 | -56.368 |
| 197 | -6.26154 | 40.79603 | -86.2203 | 73.69722 | -35.825 |
| 198 | -33.5406 | 40.79603 | -113.499 | 46.41821 | -133.032 |
| 199 | -53.6481 | 40.79603 | -133.607 | 26.31066 | -299.167 |
| 200 | -81.514 | 40.79603 | -161.473 | -1.55526 | -101.816 |
| 201 | -10.1039 | 40.79603 | -90.0627 | 69.85483 | 29.72 |
图 12.17 – VAR(1)模型测试预测
第 6 步 – 构建预测
我们可以观察到,因为这个模型是一个自回归阶数 p=1的模型,预测非常快地趋向于均值。包括更高阶数可能会导致更多的模型方差,这我们可以将其视为更自信但风险更高的预测。多元自回归(1)模型产生更多的偏差,我们可以在图 12**.18中观察到我们提到的趋向均值的趋势。我们可以运行以下代码来生成数据并绘制预测图:
df_forecast = results.get_prediction(start=201, end=207).summary_frame(alpha=0.05)
forecast = np.hstack([np.repeat(np.nan, len(data_1d)+1), df_forecast['mean']])
fig, ax = plt.subplots(1,1,figsize=(20,5))
ax.plot(data_1d['realinv'], marker='o', markersize=5)
ax.plot(forecast, marker='o', markersize=4)
ax.plot(df_forecast['mean_ci_lower'], color='g')
ax.plot(df_forecast['mean_ci_upper'], color='g')
ax.fill_between(df_forecast.index, df_forecast['mean_ci_lower'], df_forecast['mean_ci_upper'], color='g', alpha=0.1)
ax.set_title('Forecast for VAR(1)');

图 12.18 – VAR(1)模型预测,h=7
在这里,我们可以看到预测点以及预测的置信区间:
df_forecast
mean |
mean_se |
mean_ci_lower |
mean_ci_upper |
|
|---|---|---|---|---|
| 202 | -16.3442 | 40.79603 | -96.3029 | 63.6146 |
| 203 | -10.9571 | 43.69828 | -96.6041 | 74.68998 |
| 204 | -3.24668 | 44.4028 | -90.2746 | 83.7812 |
| 205 | 1.301495 | 44.56735 | -86.0489 | 88.65189 |
| 206 | 3.576619 | 44.60344 | -83.8445 | 90.99775 |
| 207 | 4.650973 | 44.61108 | -82.7851 | 92.08709 |
| 208 | 5.147085 | 44.61268 | -82.2922 | 92.58633 |
图 12.19 – VAR(1)模型输出数据
注意,我们的结果是基于差分数据的。此模型告诉我们变量之间关于它们各自信号的统计显著性水平,但不会给我们提供原始数据方面的预测值。要反转差分数据,我们需要估计一个积分常数,然后根据该常数进行反差分,或者使用允许我们进行反变换的不同变换方法,例如可以指数化的对数变换,以进行反转。通常,VAR 模型用于识别在交叉相关分析以及经济冲击分析中作为高度相关变量识别的变量之间的潜在因果关系。
可以认为,根据预测所需的详细程度,可能不需要对变量进行差分和移位。然而,由于这是一个没有外生变量的完全内生模型,如果我们对任何变量应用差分,我们也必须对包含非平稳行为的所有变量应用差分。至于移位,简单地应用更高的自回归滞后阶数可能比应用移位更有用;移位的缺点是在过程的早期阶段就会丢失样本。然而,使用更高滞后阶数的缺点是在时间维度中包含更多变量,这可能会增加过拟合的可能性。从逻辑上讲,随着模型中包含的变量增多,我们也必须增加样本量。与任何模型一样,我们必须应用严格的交叉验证以确保性能稳定并最小化风险。
摘要
在本章中,我们概述了多元时间序列及其与单变量情况的不同之处。然后,我们介绍了使用多元时间序列模型解决问题的两种流行方法背后的数学和直觉——ARIMAX 和 VAR 模型框架。我们通过逐步方法对每个模型进行了示例分析。本章总结了我们对时间序列分析和预测的讨论。到这一点,你应该能够识别和评估时间序列的统计特性,根据需要对其进行转换,并构建适用于拟合和预测单变量和多变量情况的有用模型。
在下一章中,我们将从事件时间(TTE)变量的介绍开始讨论生存分析。
参考文献
[1] Liang, X., Zou, T., Guo, B., Li, S., Zhang, H., Zhang, S., Huang, H. 和 Chen, S. X. (2015). 评估北京的 PM2.5 污染:严重程度、天气影响、APEC 和冬季供暖。皇家学会学报 A,471,20150257。
[2] Dua, D. 和 Graff, C. (2019). UCI 机器学习仓库 [archive.ics.uci.edu/ml]. 加州大学欧文分校,信息与计算机科学学院,加州,欧文市。
第五部分:生存分析
本部分将通过分析事件时间结果变量来介绍另一种统计方法,即生存分析。在介绍生存分析和截尾数据之后,我们将研究具有生存响应的模型。
它包括以下章节:
-
第十三章, 事件时间变量 - 简介
-
第十四章, 生存模型
第十三章:事件发生时间变量 - 简介
在这个简短的章节中,我们将介绍统计学的一个分支,称为生存分析,它与生存和时间审查研究相关。生存分析也称为事件发生时间变量分析,这是一种特定的统计结果类型,需要使用比我们在前面几章中研究的技术更多的技术。事件发生时间变量分析研究,例如,在研究时间段内参与者是否发生了感兴趣的事件。换句话说,我们研究的是在特定时间点后样本的存活比例以及存活样本比例失败或死亡的速度,或者不同治疗组之间是否存在生存差异。生存分析中的生存一词最初是基于医学领域中从治疗到死亡的时间。然而,生存分析可以轻松应用于许多领域,包括工程(在那里它被称为可靠性理论或可靠性分析)、社会学(事件历史分析)和经济学(持续时间分析)。在医学研究中,当发生死亡时,生存研究可能并不模糊,尽管某些事件,如器官衰竭,可能定义不清。然而,在其他研究领域中确实存在模糊性:例如,在工程领域,故障或系统损坏可能是部分的,或者并不严格由时间决定。我们还将在本章中检查生存数据、生存函数、风险和风险比。
在本章中,我们将涵盖以下主要主题:
-
审查
-
生存数据
-
生存函数、风险和风险比
什么是审查?
在统计学领域,审查指的是一个情况,即测量或观察的完整程度或精确值并不完全为人所知。在生存分析中,这种情况发生在我们有关于样本观察的信息,但不知道给定事件发生的时间,这被认为是生存分析中的一个关键问题,区分了时间到事件分析与其他在前面章节中提到的统计分析。审查发生有几个原因;例如,一个人退出研究或提前退出,或者所讨论的事件在研究开始之前就已经发生。审查的事件是非信息性的,也就是说,审查导致研究失败是由于除失败时间之外的其他原因。换句话说,由审查引起的失败与事件发生的概率无关。 信息性审查发生在由于研究原因而失去观察结果时。信息性审查有三种类型:右端审查、左端审查和区间审查。在这本书中,我们专注于非信息性的右端审查。
左端审查
当我们知道事件在收集观察数据之前已经发生时,那么观察就是左截尾。一个左截尾的例子是我们研究中学生开始青春期的年龄。可能有学生在中学生入学之前就达到了这个发育阶段。所有这些学生都被认为是左截尾。另一个例子是针对一个风险患者组的 HIV 研究,到研究开始时,一些患者已经感染了 HIV。
右截尾
当事件在某个时间点之后发生,但我们不知道确切的时间时,这种情况被认为是右截尾。应用之前的场景,想象有一些学生在青春期发育阶段研究中退出,或者在我们跟进之前(例如,他们转学到另一所学校)或者他们有延迟的青春期(在他们从初中毕业进入高中之前,发育阶段还没有发生)。另一个例子是癌症研究,其中一组患者被招募参加临床试验,以观察新的治疗方法对他们的影响,但组内的一个患者在两年后因车祸去世。在这种情况下,我们知道这位患者的生存期至少有 2 年,但在研究期间,我们不知道这位患者能够存活多少年。
间隔截尾
左截尾和右截尾是间隔截尾的特殊情况。一个清楚地说明间隔截尾的例子是,一位患者在时间 t1 时进行了 HIV 检测,结果为阴性,但在时间 t2 时检测结果为阳性。然而,我们不知道这位患者在时间间隔[t1, t2]之间确切感染的时间。这位特定的患者在去年和今年都进行了年度体检。实验室结果显示,他去年是 HIV 阴性,但今年是 HIV 阳性。因此,感染发生在去年和今年的体检之间的某个时间点,但我们不知道确切的时间。
一类和二类截尾
右端截断有两种类型:I 型截断和II 型截断。在I 型截断中,研究被设计为在某个时间点 t 后终止。固定截断发生在对象在时间 t 之后没有发生事件时。这个对象被认为在时间 t 被截断。随机截断发生在截断对象没有相同截断时间时。在II 型截断中,研究在发生预定的某些事件后终止。这种截断类型在寿命测试实验中经常被用来节省时间和金钱。例如,我们随机选择 n 个对象作为样本来测试新的创新是否真正有效,但我们不是测试所有这些 n 个对象,而是定义如果发生 r 次故障,研究就终止以节省时间和成本。在这种情况下,前 r 个对象在时间 t 1、t 2、…、t r 发生事件,但剩余的 n − r 个对象在时间 T = max(t 1, t 2, … , t r)没有发生事件,因此它们被认为是截断的。
让我们可视化一个为期 5 年的癌症研究,测试一种新的治疗方法。研究的开始时间是 0,即研究开始时,结束时间是 5,即研究结束时。

图 13.1 – 癌症研究中生存分析的右端截断
第一位患者在研究开始时被招募,但在两年半时去世。第二位患者也是在研究开始时被招募的,但在第 5 年,他仍然存活。这位患者已经存活了超过 5 年,但我们不知道他最终可以存活多少年,这意味着这位患者发生了右端截断。第三位患者在第 1 年被招募,但在第 4 年去世。第四位患者在第 2 年加入了研究,但在第 4 年,我们无法继续跟踪这位患者。原因可能是他搬到了另一个城市,例如。患者 2 和患者 4 发生了截断。
这项研究看起来像是一个回归问题,但在本例中,当研究结束时,第二位患者被认为已经存活。这使得生存分析不同于常规的统计分析。我们也没有从研究数据中移除第二位患者的数据,因为信息是有价值的:患者可以至少存活 5 年。此外,患者随访可能会被防止,因为他们可能搬到了另一个地方或者非常病重,但我们不是移除数据,而是记录他们退出研究的原因。同样,性别考虑可能会存在偏差,因为例如,当男性患者生病时,他们可能比女性患者更有可能退出研究,从而得出一个可能错误的结论,即女性的存活时间比男性长。
生存数据
生存数据关注的是研究中的对象是否经历了某个事件。此外,还会考虑随访时间。时间零点或时间起点是研究开始的时间。根据研究的目的,时间零点或时间起点可能不同。例如,在前列腺癌研究中,研究人员招募 40 岁及以上的男性参与者,而在青春期发育年龄的研究中,则招募 12 岁及以上的男性和女性青少年。如果研究持续一段时间(可能是几个月或几年),那么在研究期间记录时间起点和随访时间至关重要。
最后,我们讨论生存时间和删失时间之间的关系以及如何在删失的情况下记录生存数据。假设对于样本中的每个对象,我们知道其真实事件时间 T 和真实删失时间 C。那么,该对象的生存时间是指事件发生之前的时间段,而该对象的删失时间是指删失发生的时刻,例如,研究结束或我们无法继续跟踪该对象时。用δ i 表示对象 i 的状态指示符:
δ i = { 1 如果事件被观察到 , 0 如果观察到删失。
换句话说,当事件 T i 在删失 C i 之前发生时,δ i = 1,当事件 T i 在删失 C i 之后发生时,δ i = 0。观察到的结果变量是
Y i = min(T i, C i)
以及对象 i 的删失生存数据可以表示为(Y i, δ i)。
以下示例考虑了一个癌症研究中的三名患者。在这里,时间以年为单位,研究持续 5 年。
| 患者 | T | C | δ |
|---|---|---|---|
| 1 | 3 (患者在第 3 年去世) | 无删失 – 患者在第 3 年去世 | 1 (因为观察到事件) |
| 2 | 7 (患者在第 7 年去世,但研究在第 5 年结束) | 5 (研究在第 5 年结束) | 0 (因为观察到删失) |
| 3 | 无信息 | 4 (患者退出研究) | 0 (因为观察到删失) |
图 13.2 – 癌症研究的生存分析
以下图展示了癌症研究中 3 名患者的信息。

图 13.3 – 癌症研究的生存分析
生存函数、风险率和风险比
让我们先讨论生存函数。函数的公式定义为
S(t) = P(T > t)
表示对象在时间 t 之后存活的概率。生存函数是一个非递增函数,其中 t 的范围从 0 到∞。当 t = 0 时,S(t) = 1,当 t = ∞时,S(t) = S(∞) = 0。理论上,这是一个平滑的函数,但在实践中,事件发生在离散的时间尺度上(天数、周数、年数)。

图 13.4 – 生存函数示意图
在这个例子中,我们回顾了跨越 5 年的癌症研究。在研究开始的时间零点,生存概率是 1 或 100%,但在第 5 年,生存概率接近 0.2 或 20%。
现在我们考虑斯坦福心脏移植数据集。该数据集包含了 103 名参与实验性心脏移植项目的患者信息(见图 13.5)。患者病情严重,有可能从成为项目的一部分中获得一些好处,因为他们可能需要等待几周到几个月才能从捐献者那里获得新的心脏。CSV 数据集可以在www.openintro.org/data/csv/heart_transplant.csv下载。

图 13.5 – 斯坦福移植数据的前五行
我们在这里关注两个特征:survived(记录死亡与存活的状态)和survtime(患者被确定为心脏移植候选人后到研究结束为止存活的天数)。当我们按survtime排序数据时,在前 15 个观察值中,第 13 行的患者仍然存活,如图 13.6所示。因此,第 13 行的观察结果是时间截断的。

图 13.6 – 前 15 名移植患者的状态
在这个例子中,为了计算survtime = 3之后的生存概率,我们只需要计算每个时间间隔[t 0, t 1],(t 1, t 2],(t 2, t 3]中的生存概率,该概率通过以下方式计算
P(Survival in (t i−1, t i]) = 1 − P(Dead in (t i−1, t i] | Alive in t i−1)
其中
P(Dead in (t i−1, t i] | Alive in t i−1) = (t i−1, t i] 内的死亡人数 ___________________ t i−1 时的存活人数 ,
然后
S(t 3) = P(Survival > t 3) = (1 − 0 _ 103 )(1 − 1 _ 103 )(1 − 3 _ 102 ) = 96.12 % .
接下来,我们讨论风险函数或风险率。风险函数定义为
h(t) = lim 𝛥t→0 Pr(t < T ≤ t + Δt | T > t) ______________ Δt ,
t 表示在给定时间点之前生存的情况下事件发生的瞬时率。换句话说,它就是现在活着的情况下,在接下来的几秒或第二天内死亡的概率。风险比是死亡概率的比率。例如,当风险比是 3 时,那么给定组死亡的概率是另一组的 3 倍。
摘要
在本章中,我们概述了时间至事件变量分析及其与我们在前几章中研究过的其他统计分析的差异。我们涵盖了截尾直觉(左截尾、右截尾和区间截尾)并讨论了一类和二类截尾。本章还讨论了非信息性事件和信息性事件。然后,我们讨论了生存数据以及生存与截尾时间之间的关系,以及如何记录带有截尾的生存数据。本章的最后部分还提到了生存函数、风险率和风险比。
在下一章中,我们将考虑非参数 Kaplan-Meier 模型、参数指数模型,以及半参数 Cox 比例风险模型。我们将通过应用这些模型进行生存分析,在 Python 中执行实际数据分析。
第十四章:生存模型
在第十三章,时间事件变量中,我们介绍了生存分析、删失和时间事件(TTE)变量的主题。在本章中,我们将深入概述并演示这些技术在三个主要模型框架中的应用:
-
Kaplan-Meier 模型
-
指数模型
-
Cox 比例风险模型
我们将讨论每种方法如何通过单变量 Kaplan-Meier 和指数方法以及多变量 Cox 比例风险回归模型,提供对研究受试者生存和风险风险的概率洞察。我们将通过使用真实数据的示例来演示,并讨论结果,以便读者了解如何评估性能并将测试输出转换为有用的信息。最后,我们将展示如何使用训练好的模型为未见数据提供预测概率。
技术要求
在本章中,我们使用了一个额外的 Python 库进行生存分析:lifelines。请安装以下版本的这些库以运行提供的代码。有关安装库的说明,请参阅第一章**,采样 和泛化:
lifelines== 0.27.4
更多关于lifelines的信息可以在以下链接找到:
lifelines.readthedocs.io/en/latest/index.xhtml
Kaplan-Meier 模型
我们将讨论的第一个生存分析模型是lifelines库。让我们开始吧。
模型定义
Kaplan-Meier 估计量由以下公式定义:
ˆ S (t) = ∏ i:t i≤t n i − d i _ n i
在这里,n i 是时间 t 之前处于风险中的受试者数量,d i 是时间 t 的死亡事件数量,ˆ S (t)(生存函数)是寿命超过 t 的概率。公式中使用的Π符号类似于Σ符号;然而,Π表示乘法。这意味着前面的公式将在每个时间步 t 产生一系列分数的乘积。这意味着 Kaplan-Meier 估计量是一系列递减的水平步骤。为了使这一点更清晰,让我们通过一些示例数据来查看其应用。
假设我们有以下数据,其中duration列表示受试者存活的时间,death列表示研究中是否观察到死亡。让我们通过这个数据的 Kaplan-Meier 估计量进行计算:
| 持续时间 | 死亡 |
|---|---|
5 |
1 |
7 |
1 |
3.5 |
1 |
8 |
0 |
2 |
1 |
5 |
1 |
图 14.1 – 死亡和持续时间示例数据
初始时(t = 0),所有受试者都处于风险之中,但没有人死亡。这意味着当前的生存率是 1。我们按照以下方式计算 t = 0 时生存函数的值:
ˆ S (0) = n 0 − d 0 _ n 0 = 6 − 0 _ 6 = 1
第一个事件发生在 t = 2。此时,所有受试者(六个受试者)在事件发生前都处于风险中,并且此时有一个受试者死亡。我们计算此点的当前生存率,然后乘以所有前面的值。t = 2 时的生存率如下:
n2 - d2 / n2 = (6 - 1) / 6 ≈ 0.83
然后,我们计算 t = 2 时的生存函数:
ˆ S (2) = n0 - d0 * n0 * (n2 - d2) / n2 = (6 - 0) * (6 - 1) / 6 ≈ 0.83
下一个事件发生在 t = 3.5。我们重复之前概述的过程。然而,此时只有五个受试者处于风险中,因为已经有一个人已经死亡。因此,t = 3.5 时的当前生存率如下:
n3 - d3 / n3 = (5 - 1) / 5 = 0.8
然后,我们计算此点的生存函数:
ˆ S (3.5) = (n0 - d0) * (n0 * (n2 - d2) * (n3.5 - d3.5)) / n3.5 = (6 - 0) * (6 - 1) * (6 - 1) * (5 - 1) / 5 ≈ 0.67
下一个事件发生在 t = 5,此时有两个受试者死亡。在 t = 5 时,有四个受试者处于风险中,并且有两个受试者死亡。因此,t = 5 时的生存率为 0.5。生存函数的新值计算如下:
ˆ S (5) = 1 * 0.83 * 0.67 * 0.5 ≈ 0.33
最后一个事件发生在 t = 7。此时,生存率为 0.5,生存函数在此点的值大约为 0.167:
ˆ S (7) = 1 * 0.83 * 0.67 * 0.5 * 0.5 ≈ 0.167
由于我们的研究在这里结束,这是生存函数的最终值,并且函数值在整个研究结束时保持不变。
如您所见,这个生存模型易于计算。注意,我们也没有提及假设或潜在的统计分布。Kaplan-Meier 估计器是一种非参数生存模型。正如我们在第五章**,非参数检验中讨论的,这意味着当分析的数据不符合参数模型的假设时,它可以作为参数模型的替代方案使用。Kaplan-Meier 估计器因此是一个值得拥有的工具。
现在我们已经了解了这个模型的工作原理,让我们看看如何使用 Python 的 lifelines 包计算 Kaplan-Meier 估计器生存函数。
模型示例
对于这个例子,我们将使用来自 lifelines 的 larynx 数据集。这个数据集包括六个变量:时间、年龄、死亡、Stage_II、Stage_III 和 Stage_IV。关于这个数据集的信息有限,因此我们将为此示例对变量做出以下假设:
-
时间表示患者在喉癌诊断后存活的时间 -
年龄表示患者在诊断时的年龄 -
死亡表示在研究期间是否观察到死亡 -
Stage_II表示在诊断时癌症处于第二阶段 -
Stage_III表示在诊断时癌症处于第三阶段 -
Stage_IV表示癌症在诊断时处于第四期 -
当患者未指示在第二期或以上时,他们在诊断时处于第一期
让我们先计算研究中所有患者的生存函数。我们将使用来自 lifelines 的 KaplanMeierFitter。这是一个我们将要讨论的 Kaplan-Meier 模型。为了拟合这个模型,我们将使用 fit() 方法,它接受两个参数:duration 和 event_observed。duration 是患者在研究中的生存时间。event_observed 是死亡是否被观察到:观察到为 True,未观察到为 False。数据已经设置好,可以与这两个参数一起使用。我们可以如下拟合模型:
import matplotlib.pyplot as plt
from lifelines import datasets
from lifelines.fitters.kaplan_meier_fitter import KaplanMeierFitter
data = datasets.load_larynx()
kmf = KaplanMeierFitter()
kmf.fit(data.time, data.death)
# this prints the first 5 elements of the survival function
kmf.survival_function_.head()
前一段代码的输出显示在 图 14.2 中,这是生存函数的前五个元素:

图 14.2 – 喉生存函数的前五个元素
图 14.2 中显示的生存函数元素缓慢下降,这表明随着时间的推移患者正在死亡。然而,从一系列数字中很难得到完整的故事。让我们绘制这个序列。我们还可以从 KaplanMeierFitter 对象中获取置信区间,如下一个代码块所示:
fig, ax = plt.subplots(1)
kmf.survival_function_.plot(ax=ax)
ci = kmf.confidence_interval_survival_function_
index = ci.index
ci_low, ci_high = ci.values[:, 0], ci.values[:, 1]
ax.fill_between(index, ci_low, ci_high, color='gray', alpha=0.3)
这段代码生成了 图 14.3 中所示的图表,这是随时间变化的生存函数:

图 14.3 – 使用 Kaplan-Meier 估计的喉生存函数
喉部数据的 Kaplan-Meier 估计生存函数显示在 图 14.3 中。这个函数随时间下降,直到在时间步 8 的最后观察到死亡,函数保持恒定,直到研究结束。
现在,假设我们在这个数据集中有一些特征,我们可能想要比较。例如,我们有初始诊断时的癌症阶段,以及个体的年龄。让我们先比较两个年龄组的生存函数。然后,让我们比较基于癌症阶段的组别的生存函数。
研究中个体的年龄范围大约在 40 到 85 岁之间,其中大部分个体年龄在 60 到 80 岁之间。让我们将个体按 65 岁分成两组。任何小于 65 岁的人将属于年轻组,任何 65 岁或以上的人将属于老年组。两组的生存函数显示在 图 14.4 中:

图 14.4 – 老年和年轻组的喉生存函数
在图 14.4中显示的老年组和年轻组的生存函数似乎在时间步 7 之前几乎没有差异。然而,置信区间似乎从未发散。根据这些数据,年龄似乎并不是生存的强影响因素。
现在,让我们对各个阶段进行类似的生存分析。如前所述,我们将假设任何未在第二阶段或以上阶段中指出的个体被诊断为第一阶段。这些组的生存函数显示在图 14.5中:

图 14.5 – 根据阶段划分的喉部生存函数
根据阶段划分的组的生存函数显示在图 14.5中。此图为了绘图清晰,省略了置信区间。根据图示,似乎第一阶段和第二阶段之间没有显著差异。我们开始看到第三阶段有差异。第四阶段与其他阶段之间的差异明显。
在本节中,我们讨论了生存的 Kaplan-Meier 模型,这是一个针对生存数据的非参数模型。我们还讨论了如何在集合内比较多个组的生存函数。在下一节中,我们将讨论指数模型,这是一个针对生存数据的参数模型。
指数模型
在最后一节中,我们研究了非参数的Kaplan-Meier 生存模型。现在,我们将参数模型与指数模型相结合,然后在下一节中讨论半参数模型,即Cox 比例风险模型。在考虑指数模型之前,我们将回顾指数分布是什么以及为什么在本节中提到它。这种分布基于泊松过程。在这里,事件随时间独立发生,事件率λ通过单位时间内的发生次数计算,如下所示:
λ = Y _ t
泊松分布是关于在指定时间段内发生事件数量的统计离散分布。其定义如下。设Y为时间t内发生的次数。如果概率质量函数由以下公式给出,则 Y 遵循参数λ的泊松分布:
f(Y) = Pr(y = Y) = e −λ λ Y _ Y !
离散变量 y 的均值和方差等于事件发生率。我们不是关注在特定时间段内发生了多少事件,而是考虑事件持续了多长时间;这就是为什么在本节中考虑指数分布的原因。设 T 为事件发生的时间(Y = 1)。概率密度函数通过以下公式计算:
f(t) = P(T = t) = λ e −λt
累积分布函数的计算方法如下:
F(t) = P(T ≤ t) = 1 − e −λt
函数 F(t) 表示超过时间 t 的生存概率或生存时间小于 t 的概率。我们可以在图 14.6中看到以下概率形式:

图 14.6 – 超过时间 t 生存概率的曲线
相比之下,生存函数与超过一定时间 t 的生存概率相关:
S(t) = P(T > t) = 1 − P(T ≤ t) = e −λt
通过可视化,生存函数将呈现图 14.7中看到的形式:

图 14.7 – 生存函数的曲线
累积风险函数是 H(t) = λt。然后我们可以用风险函数重新表示生存函数,如下所示:
S(t) = e −H(t)
给定 S(t) 和 f(t),我们可以使用对数似然方程的方法,并通过最大似然估计来估计 λ 的值。
模型示例
我们将使用与上一节相同的来自 lifeline 的 larynx 数据集。在这个例子中,我们将使用 lifelines 中的 ExponentialFitter。同样,fit() 方法接受两个参数:duration 和 event_observed。我们可以如下拟合模型:
from lifelines import ExponentialFitter
from lifelines import datasets
data = datasets.load_larynx()
exf = ExponentialFitter()
exf.fit(data.time, data.death)
# this print the first 5 elements of the survival function
exf.survival_function_.head()
在 Kaplan-Meier 模型中,曲线看起来像阶梯状,但使用指数方法,曲线是逐渐连续下降的,正如我们在这里可以看到的:

图 14.8 – 使用指数分布的喉部生存函数
在时间步 8,Kaplan-Meier 模型中的函数保持恒定,直到研究结束,但在指数模型中,曲线继续下降直到研究结束。我们还对年轻组(小于 65 岁)和老年组(65 岁及以上)进行了类似的研究(见图 14.9):

图 14.9 – 老年和年轻组的喉部生存函数
我们还在喉部案例的各个阶段进行生存分析(见图 14.10):

图 14.10 – 基于阶段的喉部生存函数
指数模型和 Kaplan-Meier 模型都给我们提供了类似的信息。这两个模型之间有优缺点。在 Kaplan-Meier 模型中,结果解释更简单;我们可以估计两个模型的生存函数值,但指数模型的一个优点是能够估计风险比。指数模型的两个缺点是结果并不总是现实的,并且我们必须假设一个恒定的风险比,这并不总是实用的。在下一节中,我们将讨论另一个生存模型,即半参数的 Cox 比例风险回归模型。
Cox 比例风险回归模型
生存分析,也称为 TTE 分析,正如我们在第十三章“时间到事件变量”中讨论的,是一种使用概率来估计事件发生前剩余时间的分析方法。我们看到了如何通过在诸如估计预期寿命、机械故障和客户流失等应用中包含适当的协变量,这有助于确定优先事项并更有效地分配资源。正如我们在第十三章中深入讨论的,截尾是使生存分析区别于其他可以使用回归等技术解决的统计问题的独特方面。因此——并且由于由于截尾而删除的观察值几乎肯定会误导我们的模型并提供我们无法信任的结果——我们在估计事件发生前的时间之前插入了一个称为事件状态指示器的变量,以帮助解释事件是否会在事件发生前发生或未发生。回想一下,对象 i 的事件状态指示器 δ 的描述如下:
δ i = { 1 如果观察到事件, 0 如果观察到截尾。
我们注意到,当真实事件 T i 在截尾时间 C i 之前发生时,δ i = 1,而当真实事件 T i 在截尾时间 C i 之后发生时,δ i = 0,因此,对象 i 的结果 Y 如下:
Y i = min(T i, C i)
对象 i 的截尾生存数据可以表示为 (Y i, δ i)。在本章中,我们讨论了非参数的Kaplan-Meier 生存模型,该模型使用每个时间点的样本体积的原始条件生存概率计数,而不考虑协变量。然后我们介绍了参数的指数生存模型,与 Kaplan-Meier 模型不同,它假设数据适合特定的分布,并且还包括一个风险比来比较组别。现在我们将讨论半参数的Cox 比例风险生存模型,也称为Cox 比例风险回归模型。
Cox 比例风险模型的主要目的和特点是利用多个协变量风险因素来建模生存时间。这些协变量可以是分类的或连续的。Cox 比例风险模型能够通过利用风险比来解释不同组之间生存时间的差异。正如我们之前讨论的,风险比是在任何时间点,对于一组相对于另一组的事件发生率的比率,前提是两组在该时间点之前都保持了生存。
风险比可以按照以下方式计算:

风险比的问题在于,当我们无法在组间共享的协变量之间获得值的平衡时,我们就失去了对混杂变量的控制。当我们无法控制混杂变量时,我们无法可靠地获得推断。为了解释生存分析中可能存在的协变量值的不平衡,我们可以使用Cox 比例风险模型来引入系数,这些系数有助于解释不同水平的外生影响。该模型遵循以下形式:
h(t) = h0(t) e^(β1X1 + β2X2 + … + βnXn)
在这里,h(t)是基础时间 t 的预期风险值,h0(t)是当变量矩阵 X 中的所有协变量都等于零时,t 的基线风险值,而 n 是模型中包含的变量数量。系数βn 提供了对 X 中每个列变量的影响(效应)的量化。因此,我们能够通过将输入变量的特定值乘以其相应的估计系数来预测生存。
Cox 比例风险模型有三个相关假设:
-
假设风险比在随访期间保持恒定。如果一个组输入的性质可能发生变化,这可能会对模型的结果产生负面影响;例如,我们比较两种机器的性能,但其中一种机器在天气变暖时可能会改善,而另一种则不会,因此我们的模型可能无法正确地模拟机械生存性。
-
完成生存独立性。这意味着一个研究参与者或对象的生存不受另一个对象的影响。考虑两个独立的机器。如果一个机器的振动可能导致早期故障,而这种振动也被另一个机器感受到,并且对另一个机器也有害,那么这两个机器之间可能没有生存独立性。然而,如果第一个机器的振动没有被另一个机器感受到,在其他混杂因素得到控制的情况下,很可能存在生存独立性。
-
模型假设任何发生的数据截断都是非信息的。因此,任何发生的数据截断都不会引入模型中的混杂因素。由于数据截断在随访前失去的研究对象必须与生存风险的改变无关。例如,当在一个设施的具体区域测试两种机器的机械生存性时,如果一个机器被转移到设施的其他区域以最小化其自身的自毁性振动,这是信息性截断。然而,如果机器被移动到不同的区域只是为了腾出空间,并且移动不会影响机器的生存风险或结果,那么截断就是非信息的。因此,它不会引入模型混杂。Cox 比例风险模型适用于右截尾数据。
在执行 Cox 比例风险回归建模时需要采取的步骤将在下面概述。顺序很重要,因为测试应该在可行时才进行。这些步骤遵循国家卫生研究院(NIH)提供的概述(www.ncbi.nlm.nih.gov/pmc/articles/PMC7876211/)。
步骤 1
设置要检验的零假设(多变量情况下的假设,我们将对其进行研究)。我们将使用斯坦福心脏移植数据集,该数据集列出了斯坦福心脏移植项目中等待移植的患者的生存情况。我们将使用age和year(与项目的开始日期相关)以及transplant状态和先前存在的旁路手术作为生存的预测因子,生存结果要么是死亡,要么是移植。生存持续时间是stop变量,即研究的退出时间。event是确认右端截断的变量,对应于死亡或移植。对于每个变量的零假设是组间在生存上没有统计学上显著的差异。例如,年龄对生存没有影响,而等待移植将是零假设。以下代码片段展示了代码:
import statsmodels.api as sm
data = sm.datasets.get_rdataset('heart', package='survival').data
train_data = data.iloc[0:171]
test_data = data.iloc[171:]
exog=train_data[['age','year', 'transplant','surgery']]#df_test[['age','year','surgery','transplant']]
endog=train_data['stop']
data.head(2)
我们需要注意我们数据中的两个额外方面:(1) 年龄值是负数;起始年龄值是 48 岁,因此-1 岁的年龄对应于 47 岁的患者;(2) 年份从 1967 年 11 月 1 日开始。id是每个患者身份的唯一标识。我们不会对这个变量进行建模,但可以将其用作参考。在图 14.11中,我们可以看到,作为参考,数据的前两行,包括我们将用于建模生存的所有变量:
| 开始 | 结束 | 事件 | 年龄 | 年份 | 手术 | 移植 | id |
|---|---|---|---|---|---|---|---|
| 0 | 50 | 1 | -17.1554 | 0.123203 | 0 | 0 | 1 |
| 0 | 6 | 1 | 3.835729 | 0.25462 | 0 | 0 | 2 |
图 14.11 – 斯坦福心脏移植数据集的前两行
步骤 2
我们使用stop=1来估计生存函数,对应于索引列event_at等于 1,我们有三个患者的右端截断。我们观察到事件发生在一位患者身上(observed=1),而其他两位患者被截断。在这里,我们展示了相应的数据。id=45的患者停止但同一天重新开始,然后存活到第 45 天。对于id=3的患者,他们在接受移植后存活到第 16 天。id=15的患者没有重新进入随访;我们假设由于他们没有接受移植,所以他们没有存活。
在下面的代码示例中,durations变量对应于生存时间,而event_observed对应于导致右端截断的事件(移植或死亡):
from lifelines import KaplanMeierFitter
import matplotlib.pyplot as plt
KaplanMeierFitter = KaplanMeierFitter()
KaplanMeierFitter.fit(durations=train_data['stop'], event_observed=train_data['event'])
KaplanMeierFitter.event_table.reset_index()
从 Kaplan-Meier 结果中可以看出,event=1 且 stop=1 导致 observed=1,而 event=0 且 stop=1 导致 censored=1,stop=1 导致 removed=1。所有这三种情况都会导致相应的样本从那天计算中移除,但与经历事件的病人不同,那些被截尾的病人被认为至少存活到研究继续的时间。我们可以通过考虑 Kaplan-Meier 生存估计概率函数 S t 来看到这一点:
S t = 起始时患者数量 - 时间 t 时事件发生数量 = 1 的患者数量 ______________________________________ 起始时患者数量
在这里,任何时间 t 的生存概率表示为 S t。
在查看 KaplanMeierFitter 函数的事件表时,我们有以下结果:
S t = 风险患者数量 - 时间 t 时观察到的患者数量 _________________________________ 风险患者数量
我们可以理解,除非他们也被标记为 observed=1,即 event=1,否则 removed 患者对生存估计的影响有限。我们可以将这种情况归因于 event=1。
让我们考虑患者 3、15 和 45 来更好地理解这种关系。根据 图 14**.12 和 图 14**.13 中的计数,我们可以看到这些患者在第 1 天 (event_at=1) 被从分母中移除。然而,通过查看 图 14**.14,我们可以看到患者 15 和 45 有后续的停止时间,对应的事件被纳入生存函数中。让我们首先看看 图 14**.12,以便我们可以看到第 1 天被移除的三个患者:
| 事件发生时间 | 移除 | 观察 | 截尾 | 进入 | 风险 | |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 171 | 171 |
| 1 | 1 | 3 | 1 | 2 | 0 | 171 |
| 2 | 2 | 6 | 3 | 3 | 0 | 168 |
| 3 | 3 | 6 | 3 | 3 | 0 | 162 |
| 4 | 4 | 2 | 0 | 2 | 0 | 156 |
| … | … | … | … | … | … | … |
| 107 | 1401 | 1 | 0 | 1 | 0 | 5 |
| 108 | 1408 | 1 | 0 | 1 | 0 | 4 |
| 109 | 1572 | 1 | 0 | 1 | 0 | 3 |
| 110 | 1587 | 1 | 0 | 1 | 0 | 2 |
| 111 | 1800 | 1 | 0 | 1 | 0 | 1 |
图 14.12 – 斯坦福心脏移植数据集事件表摘要
现在,我们可以在 图 14**.13 中看到第 1 天所有有停止的患者。
train_data.loc[train_data['stop']==1]
第 1 天有停止的患者是 id 为 3、15 和 45 的患者:
| 开始 | 停止 | 事件 | 年龄 | 年份 | 手术 | 移植 | id |
|---|---|---|---|---|---|---|---|
| 0 | 1 | 0 | 6.297057 | 0.265572 | 0 | 0 | 3 |
| 0 | 1 | 1 | 5.815195 | 0.991102 | 1 | 0 | 15 |
| 0 | 1 | 0 | -11.8166 | 3.263518 | 0 | 0 | 45 |
图 14.13 – 第 1 天退出研究的患者训练数据值
为了更好地理解这种分析是如何工作的,我们可以具体查看 id 为 3、15 和 45 的患者的所有记录:
train_data.loc[train_data['id'].isin([3, 15,45])]
这些患者在第 1 天注册了停止,但患者 3 和 45 分别在第 16 天和第 45 天也注册了停止:
| 开始 | 停止 | 事件 | 年龄 | 年份 | 手术 | 移植 | id |
|---|---|---|---|---|---|---|---|
| 0 | 1 | 0 | 6.297057 | 0.265572 | 0 | 0 | 3 |
| 1 | 16 | 1 | 6.297057 | 0.265572 | 0 | 1 | 3 |
| 0 | 1 | 1 | 5.815195 | 0.991102 | 1 | 0 | 15 |
| 0 | 1 | 0 | -11.8166 | 3.263518 | 0 | 0 | 45 |
| 1 | 45 | 1 | -11.8166 | 3.263518 | 0 | 1 | 45 |
图 14.14 – 患者第 3、15 和 45 天的训练数据值
我们还可以看到,患者 45 是唯一一个在 45 天有停止值(对应于事件发生时间)的患者。因此,我们知道移植会导致观察和移除。要识别 45 天的值,请运行以下代码:
KaplanMeierFitter.event_table.loc[KaplanMeierFitter.event_table.index==45.0]
代码输出图 14**.15中的表格:
| 事件发生时间 | 移除 | 观察 | 截尾 | 进入 | 在风险中 |
|---|---|---|---|---|---|
| 45 | 1 | 1 | 0 | 0 | 87 |
图 14.15 – 45 天的事件
现在我们可以通过运行以下代码来查看事件表,该表已过滤到 45 天停止的事件:
train_data.loc[train_data['stop']==45]
这给我们带来了图 14**.16中看到的表格输出:
| 开始 | 停止 | 事件 | 年龄 | 年份 | 手术 | 移植 | id |
|---|---|---|---|---|---|---|---|
| 1 | 45 | 1 | -11.8166 | 3.263518 | 0 | 1 | 45 |
图 14.16 – 45 天停止的事件
我们可以通过在以下代码中将两组分开来重复此步骤,以查看接受移植和未接受移植的人在整个研究结束时的生存率是否有差异:
from lifelines import KaplanMeierFitter
import matplotlib.pyplot as plt
KaplanMeierFitter_n = KaplanMeierFitter()
KaplanMeierFitter_y = KaplanMeierFitter()
KaplanMeierFitter_n.fit(durations=data.loc[data['transplant']==0]['stop'], event_observed=data.loc[data['transplant']==0]['event'], label='no transplant')
KaplanMeierFitter_y.fit(durations=data.loc[data['transplant']==1]['stop'], event_observed=data.loc[data['transplant']==1]['event'], label='transplant');
我们在Kaplan-Meier 生存函数(图 14**.17)中观察到,两组的生存概率都在稳步下降,但未接受移植的组的下降更为陡峭。我们还可以看到,总体上,未接受移植的组如果与接受移植的组相比,是无法存活的。到项目结束时,没有非移植患者存活。阴影部分对应于每个概率区间的 95%置信区间带:

图 14.17 – Kaplan-Meier 生存估计概率函数
要生成任何一天(对应于停止时间)的确切生存概率,我们可以运行以下代码:
print('Non-Transplant Survival Probability at day 300: ', KaplanMeierFitter_n.predict(300))
print('Transplant Survival Probability at day 300: ', KaplanMeierFitter_y.predict(300))
在这里,我们可以看到移植组和未移植组在第 300 天的生存概率:
Non-Transplant Survival Probability at day 300: 0.3545098269168237
Transplant Survival Probability at day 300: 0.4911598783770023
第 3 步
现在,我们将使用log-rank 检验来确定生存曲线在统计上是否存在差异。在这一步,我们调查移植和非移植组之间是否存在任何统计意义上的生存差异。如果没有统计意义上的差异,那么理解协变量及其对生存的影响之间的关系将没有意义。我们在书中之前已经使用卡方独立性检验进行了列联表分析。这本质上就是这里的 log-rank 检验,其中我们比较两组(移植与非移植)的生存分布。我们采用这种方法是因为数据是右截尾的,因此是右偏斜的;因此,我们正在处理非参数分布。零假设是两组之间的风险比等于 1,这意味着他们在生存或风险风险方面是相等的。
在以下代码示例中,我们使用两种不同的患者分布(有移植和无移植的患者)作为logrank_test函数中的组 A 和 B:
from lifelines.statistics import logrank_test
lr_results = logrank_test(durations_A=data.loc[data['transplant']==0]['stop'],
durations_B=data.loc[data['transplant']==1]['stop'],
event_observed_A=data.loc[data['transplant']==0]['event'],
event_observed_B=data.loc[data['transplant']==1]['event'])
我们可以看到使用了卡方分布:
print(lr_results.null_distribution)
这表明默认分布是卡方分布:
chi squared
我们可以运行以下代码来查看测试结果:
lr_results.summary
我们还可以看到在 5%的显著性水平下,两组之间存在统计意义上的显著差异。因此,我们可以假设继续使用 Cox 比例风险模型来理解我们之前概述的协变量与生存风险之间的关系是有意义的:
| test_statistic | P | -****log2(p) |
|---|---|---|
| 4.02651 | 0.044791 | 4.480663 |
图 14.18 – Log-rank 检验结果
第 4 步
在这里,我们运行 Cox 比例风险检验,并分析包含项的 p 值和置信区间,以确定它们是否与生存率的影响具有统计意义上的显著性。我们查看风险比以确定它们产生的任何影响:
from lifelines import CoxPHFitter
CoxPHFitter = CoxPHFitter()
CoxPHFitter.fit(df=train_data[['age','year','surgery','transplant','stop','event']], duration_col='stop', event_col='event')
CoxPHFitter.print_summary()
我们可以看到,我们四个项中的三个在 5%的显著性水平下对预测生存是显著的;我们的surgery变量似乎并不基于 p 值。然而,手术的 95%置信区间不包含 0,这表明统计意义上的显著性。这种差异是因为 p 值是基于与临界值相关的测试统计量计算的,临界值是使用风险比的对数估计的,而置信区间是基于风险比本身的。这种差异可能意味着surgery变量是显著的,但也可能不是。由于我们不是在统计上确定的,我们应该谨慎行事,在预测未来生存时不应将其视为有用。
注意,exp(coef)对应于风险比,而coef是风险比的对数。这遵循我们之前提供的在基础时间t的预期风险值的方程。
我们可以说,基于危险比(exp(coef))为 1.03,年龄在 48 岁以上每增加一个单位,危险比增加 1.03 倍,即 3%。至于我们的transplant变量,我们可以以 95%的置信度说,进行心脏移植将危险比降低了 0.54 倍,相当于 46%。
在图 14.19中,我们可以看到 Cox 比例风险回归模型的结果:
| 模型 | lifelines.CoxPHFitter |
||||
|---|---|---|---|---|---|
| 持续时间列 | stop |
||||
| 事件列 | event |
||||
| 基线估计 | breslow |
||||
| 观察数量 | 171 | ||||
| 观察到的事件数量 | 74 | ||||
| 部分对数似然 | -298.47 | ||||
| 运行时间 | 2023-03-18 16:23:26 UTC | ||||
| coef | exp (coef) | se (coef) | coef lower 95% | coef upper 95% | |
| 年龄 | 0.03 | 1.03 | 0.01 | 0.01 | 0.06 |
| 年份 | -0.16 | 0.85 | 0.07 | -0.3 | -0.02 |
| 手术 | -0.64 | 0.53 | 0.37 | -1.36 | 0.08 |
| 移植 | -0.62 | 0.54 | 0.27 | -1.15 | -0.08 |
| 一致性 | 0.67 | ||||
| 部分 AIC | 604.95 | ||||
| 对数似然比检验 | 21.22 on 4 df | ||||
| -log2(p) of ll-ratio test | 11.77 |
图 14.19 – Cox 比例风险模型输出
在图 14.20中,我们可以看到age,它只增加了 3%的危险,在结果中的方差解释不多。年龄范围大约从 9 岁到 64 岁。没有其他协变量,我们无法确切地说为什么可能是这样,但包括更多协变量可能有助于更深入地理解这个变量。在显著变量中,transplant对生存结果的影响最为显著:
plt.title('Coefficients within Confidence Intervals')
CoxPHFitter.plot()

图 14.20 – 危险比项的置信区间
我们可以在图 14.21的生存函数图中看到,所有患者之间危险比是恒定的假设。这就是为什么防止信息性截断很重要的原因。让我们假设这个假设得到了满足。
注意到在预测时,我们没有起始和结束时间。因此,我们只能输入给定患者的可量化协变量,如下所示:
CoxPHFitter.predict_survival_function(train_data[['age','year','surgery','transplant']]).plot()
plt.xlabel('Survival Time')
plt.ylabel('Survival Probability')
plt.title('Survival Function for All Patients')
plt.legend().set_visible(False)

图 14.21 – 所有患者的生存函数
第 5 步
在图 14.22中,我们可以使用 Cox 比例风险模型来预测我们保留的测试患者的生存函数:
CoxPHFitter.predict_survival_function(test_data[['age','year','surgery','transplant']]).plot()
plt.xlabel('Survival Time')
plt.ylabel('Survival Probability')
plt.title('Survival Function for Holdout')

图 14.22 – 测试患者的生存函数,id=171
要生成这位患者生存的概率,使用他们的协变量,在每一个潜在的停止点,我们可以运行以下代码:
CoxPHFitter.predict_survival_function(test_data[['age','year','surgery','transplant','stop','event']])
摘要
在本章中,我们深入讨论了三种生存分析模型:Kaplan-Meier 模型、指数模型和 Cox 比例风险回归模型。使用这些框架,我们建立了生存函数,并估计了各种 TTE(时间至事件)和右截尾研究的生存概率和风险比。对于多元情况,我们使用 Cox 比例风险回归模型来对依赖变量的协变量进行分析,以建模风险比。对于所有模型,我们展示了使用置信区间来评估显著性,以及相应的 p 值。到此为止,读者应该能够自信地识别出在哪些场景下每个模型会优于其他模型,并适当地拟合和实现该模型,以获得战略成功所需的必要结果。


浙公网安备 33010602011771号