精通-Python-数据分析-全-
精通 Python 数据分析(全)
一、数据分析工具
本章向您概述了 Python 中可用于数据分析的工具,并详细介绍了将在本书中使用的 Python 包和库。给出了一些安装技巧,本章以一个简单的例子结束。我们将专注于如何读取数据文件,选择数据,并产生简单的图,而不是钻研数字数据分析。
开始之前
我们假设您熟悉 Python,并且已经开发并运行了一些脚本或交互使用了 Python,无论是在 shell 中还是在另一个界面上,例如 Jupyter Notebook(以前称为 IPython notebook )。因此,我们还假设您有一个运行良好的 Python 安装。在本书中,我们假设您已经安装了 Python 3.4 或更高版本。
我们还假设您已经基于需求和可用环境,使用 Python 开发了自己的工作流。按照本书中的示例,您应该能够访问 Python 3.4 或更高版本的工作安装。有两种方法可以开始,如下表所示:
- 从头开始使用 Python 安装。这个可以从https://www.python.org下载。这将需要为每个所需的库单独安装。
- 安装包含科学和数据计算库的预打包发行版。两个流行的发行版是 Anaconda 科学巨蟒(https://store.continuum.io/cshop/anaconda)和 entsuren 发行版(https://www.enthought.com)。
型式
即使您有一个正在运行的 Python 安装,您也可能想要尝试一个预打包的发行版。它们包含适用于数据分析和科学计算的全面的包和模块集合。如果选择此路径,默认情况下将包括下一个列表中的所有库。
我们还假设您拥有以下列表中的库:
numpy和scipy:在http://www.scipy.org都有。这些是计算工作中必不可少的 Python 库。NumPy 定义了一个快速灵活的数组数据结构,SciPy 有大量用于数值计算的函数集合。列表中提到的一些库需要它们。matplotlib:这个在http://matplotlib.org有售。这是一个建立在 NumPy 之上的交互式图形库。我推荐高于 1.5 的版本,这是默认情况下包含在 Anaconda Python 中的内容。pandas:这个在http://pandas.pydata.org有售。它是一个 Python 数据分析库。它将在整本书中广泛使用。pymc:这是一个库,让 Python 中的贝叶斯模型和拟合变得简单易懂。在http://pymc-devs.github.io/pymc/有售。本包主要用于本书 第六章**贝叶斯方法。scikit-learn:这个在http://scikit-learn.org有售。它是 Python 中的机器学习库。本包用于 第七章**监督和非监督学习。IPython:这个在http://ipython.org有售。它是一个库,从命令行为 Python 中的交互式计算提供了增强的工具。Jupyter:这个在https://jupyter.org/有售。它是在 IPython(和其他编程语言)之上工作的笔记本界面。笔记本界面最初是 IPython 项目的一部分,是一个基于网络的计算和数据科学*台,允许轻松集成本书中使用的工具。
请注意,前面列表中的每个库都可能有几个依赖项,这些依赖项也必须单独安装。要测试任何包的可用性,启动一个 Python shell 并运行相应的import语句。例如,要测试 NumPy 的可用性,请运行以下命令:
import numpy
如果您的系统中没有安装 NumPy,这将产生一条错误消息。另一种不需要启动 Python shell 的方法是运行命令行:
python -c 'import numpy'
我们还假设您有一个程序员编辑器或 Python IDE。有几个选项,但在基本层面上,任何能够处理无格式文本文件的编辑器都可以。
使用笔记本界面
本书中的大多数示例将使用 Jupyter Notebook 界面。这是一个基于浏览器的界面,集成了计算、图形和其他形式的媒体。笔记本可以轻松共享和发布,例如http://nbviewer.ipython.org/提供了简单的发布路径。
然而,使用 Jupyter 接口来运行本书中的示例并不是绝对必要的。但是,我们强烈建议您至少尝试一下笔记本电脑及其许多功能。Jupyter Notebook 接口可以将格式化的描述性文本与同时计算的代码单元格混合在一起。这个特性使它适合于教育目的,但是它对于个人使用也是有用的,因为它使得在编写完整的报告之前更容易添加评论和共享部分进度。我们有时会称 Jupyter 笔记本为笔记本。
要启动笔记本界面,请从 shell 或 Anaconda 命令提示符运行以下命令行:
jupyter notebook
笔记本服务器将在发出命令的目录中启动。过一会儿,笔记本界面将出现在您的默认浏览器中。确保您使用的是符合标准的浏览器,例如 Chrome、火狐、Opera 或 Safari。一旦 Jupyter 仪表盘显示在浏览器上,点击页面右上角的新建按钮,选择 Python 3 。几秒钟后,新笔记本将在浏览器中打开。了解笔记本界面的一个有用的地方是http://jupyter.org。
进口
在每个项目开始时,我们都需要加载一些模块。假设您运行的是 Jupyter 笔记本,所需的导入如下:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
在单个笔记本单元格中输入前面的所有命令,然后按 Shift + Enter 运行整个单元格。当您正在运行的单元格之后没有单元格时,将创建一个新单元格;但是,如果你想自己创建一个,菜单或快捷键 Ctrl +M+A/B 就很方便了(上面的 A ,下面的当前单元格 B )。在附录、更多关于 Jupyter 笔记本和 matplotlib 样式中,我们介绍了 Jupyter 笔记本的一些可用和可安装的键盘快捷键扩展(即插件)。
语句%matplotlib inline是 Jupyter Notebook 魔法的一个例子,设置了内嵌显示剧情的界面,即嵌入在笔记本中。脚本中不需要这一行(这会导致错误)。接下来,可选地输入以下命令:
import os
plt.style.use(os.path.join(os.getcwd(), 'mystyle.mplstyle') )
如前所述,按Shift+回车运行单元格。这段代码有选择 matplotlib 样式表mystyle.mplstyle的效果。这是我创建的自定义样式表,它与笔记本位于同一个文件夹中。这是一个可以做什么的相当简单的例子;你可以根据自己的喜好修改它。随着我们在整本书中获得绘制图形的经验,我鼓励您在文件中使用这些设置。还有内置的样式,你可以通过在新的单元格中输入plt.style.available来设置。
就是这里!我们都准备好开始有趣的部分了!
一个使用 Pandas 库的例子
这个例子的目的是检查在你的安装中是否一切正常,并给出即将到来的味道。我们专注于 Pandas 库,这是 Python 数据分析中使用的主要工具。
我们将使用电影网络 50K 电影分级数据集,该数据集可从https://github.com/sidooms/MovieTweetings下载。数据来源于研究《电影网络:从推特上收集的电影分级数据集》,作者是杜姆斯、德·佩塞米尔和马腾斯,在推荐系统众包和人工计算研讨会上发表,该研讨会在 RecSys 的 CrowdRec 上举行(2013 年)。数据集分布在几个文本文件中,但我们将只使用以下两个文件:
ratings.dat:这是一个双冒号分隔的文件,包含每个用户和电影的评分movies.dat:这个文件包含电影的信息
要查看这些文件的内容,可以使用标准文本编辑器打开它们。数据按列组织,每行一个数据项。列的含义在与数据集一起分发的README.md文件中描述。数据有一个特殊的方面:一些列使用双冒号(::)字符作为分隔符,而其他列使用竖线(|)。这强调了现实世界数据中的一个常见现象:我们无法控制数据的收集和格式化方式。对于存储在文本文件中的数据,例如这个,在文本编辑器或电子表格软件中打开文件来查看数据并识别不一致和不规则的地方总是一个好策略。
要读取分级文件,请运行以下命令:
cols = ['user id', 'item id', 'rating', 'timestamp']
ratings = pd.read_csv('data/ratings.dat', sep='::',
index_col=False, names=cols,
encoding="UTF-8")
第一行代码用数据集中的列名创建一个 Python 列表。下一个命令读取文件,使用read_csv()功能,这是 Pandas 的一部分。这是一个从文本文件中读取面向列的数据的通用函数。调用中使用的参数如下:
data/ratings.dat:这是包含数据的文件的路径(这个参数是必需的)。sep='::':这是分隔符,这里是双冒号字符。index_col=False:我们不希望任何一列作为索引。这将导致数据被从 1 开始的连续整数索引。names=cols:这些是要与列关联的名称。
read_csv()函数返回一个 DataFrame 对象,这是表示表格数据的 Pandas 数据结构。我们可以使用以下命令查看数据的前几行:
ratings[:5]
这将输出一个表格,如下图所示:

要开始处理数据,让我们找出每个评级在表格中出现的次数。这可以通过以下命令来完成:
rating_counts = ratings['rating'].value_counts()
rating_counts
第一行代码计算计数并将它们存储在rating_counts变量中。为了获得计数,我们首先使用ratings['rating']表达式从表评级中选择rating列。然后,调用value_counts()方法计算计数。请注意,我们在单元格的末尾重新键入了变量名rating_counts。这是一个常见的笔记本(和 Python)习惯用法,用于在每个单元格后面的输出区域中打印变量值。在一个剧本里,它没有效果;我们也可以用 print 命令(print(rating_counts))来打印它。输出显示在下图中:

请注意,输出按照计数值降序排序。value_counts返回的对象是 Series 类型,这是 Pandas 数据结构,用于表示一维、索引的数据。Pandas 中广泛使用系列对象。例如,数据框对象的列可以被认为是共享一个公共索引的系列对象。
在我们的例子中,根据评级对行进行排序更有意义。这可以通过以下命令实现:
sorted_counts = rating_counts.sort_index()
sorted_counts
这通过调用 Series 对象的sort_index()方法rating_counts来实现。结果存储在sorted_counts变量中。现在,我们可以使用以下命令快速可视化评分分布:
sorted_counts.plot(kind='bar', color='SteelBlue')
plt.title('Movie ratings')
plt.xlabel('Rating')
plt.ylabel('Count')
第一行通过调用sorted_counts对象的plot()方法生成图。我们指定kind='bar'选项来生成条形图。请注意,我们添加了color='SteelBlue'选项来选择直方图中条形的颜色。SteelBlue是 matplotlib 中可用的 HTML5 颜色名称之一(例如,http://matplotlib.org/examples/color/named_colors.html)。接下来的三个语句分别设置标题、横轴标签和纵轴标签。这将产生以下图:

竖线显示有多少投票者给出了一定的评分,涵盖了数据库中的所有电影。评级的分布并不十分令人惊讶:计数增加到8级,而9-10级的计数较小,因为大多数人不愿意给出最高的评级。如果您检查每个等级的条值,您可以看到它对应于我们之前打印rating_counts对象时的值。要想知道如果不先对收视率进行排序会发生什么,请绘制rating_counts对象,也就是说,在一个单元格中运行rating_counts.plot(kind='bar', color='SteelBlue')。
假设我们想知道特定电影类型的收视率分布,比如Crime Drama,是否与整体分布相似。我们需要将收视率信息与movies.dat文件中包含的电影信息进行交叉引用。要读取该文件并将其存储在 Pandas 数据帧对象中,请使用以下命令:
cols = ['movie id','movie title','genre']
movies = pd.read_csv('data/movies.dat', sep='::',
index_col=False, names=cols,
encoding="UTF-8")
型式
下载示例代码
下载代码包的详细步骤在本书的前言中提到。请看看。该书的代码包也托管在 GitHub 上,网址为https://GitHub . com/packt publishing/Mastering-Python-Data-Analysis。我们还有来自丰富的图书和视频目录的其他代码包,可在https://github.com/PacktPublishing/获得。看看他们!
我们再次使用read_csv()功能读取数据。列名是从与数据一起分发的README.md文件中获得的。请注意,该文件中使用的分隔符也是双冒号(::)。表格的前几行可以通过命令显示:
movies[:5]
请注意流派是如何表示的,用竖线|聚集在一起作为分隔符。这是因为一部电影可以属于不止一种类型。我们现在可以使用以下几行只选择犯罪剧电影:
drama = movies[movies['genre']=='Crime|Drama']
请注意,这使用了带方括号的标准索引符号movies[...]。然而,我们使用布尔movies['genre']=='Crime|Drama'表达式作为索引,而不是指定数字或字符串索引。要了解这是如何工作的,请在单元格中运行以下代码:
is_drama = movies['genre']=='Crime|Drama'
is_drama[:5]
这将显示以下输出:

movies['genre']=='Crime|Drama'表达式返回一个 Series 对象,其中每个条目或者是True或者是False,分别表示对应的电影是不是犯罪剧。
因此,drama = movies[movies['genre']=='Crime|Drama']赋值的净效果是选择电影表中genre列中的条目等于Crime|Drama的所有行,并将结果存储在drama变量中,该变量是数据帧类型的对象。
我们只需要这个表的movie id列,可以用下面的语句选择:
drama_ids = drama['movie id']
这也是使用标准的字符串索引从表中选择一列。
下一步是从ratings表中提取那些对应戏剧的条目。这需要另一个索引技巧。代码包含在以下几行中:
criterion = ratings['item id'].map(lambda x:(drama_ids==x).any())
drama_ratings = ratings[criterion]
这段代码如何工作的关键是变量criterion的定义。我们要查找ratings表的每一行,检查item id条目是否在drama_ids表中。这可以通过map()方法方便地完成。此方法将函数应用于 Series 对象的所有条目。在我们的示例中,函数如下:
lambda x:(drama_ids==x).any()
该功能只是检查一个项目是否出现在drama_ids中,如果出现,则返回True。产生的对象criterion将是一个序列,该序列仅在对应于戏剧的行中包含True值。您可以使用以下代码查看前几行:
criterion[:10]
然后我们使用criterion对象作为索引从ratings表中选择行。
我们现在已经完成了对所需数据的选择。为了生成速率计数和条形图,我们使用与之前相同的命令。详细信息在以下代码中,这些代码可以在单个执行单元中运行:
rating_counts = drama_ratings['rating'].value_counts()
sorted_counts = rating_counts.sort_index()
sorted_counts.plot(kind='bar', color='SteelBlue')
plt.title('Movie ratings for dramas')
plt.xlabel('Rating')
plt.ylabel('Count')
和以前一样,这段代码首先计算计数,根据评级对计数进行索引,然后生成条形图。这将生成一个类似于总体评分分布的图表,如下图所示:

总结
在本章中,我们已经看到了 Python 中有哪些工具可用于数据分析,回顾了与安装和工作流相关的问题,并考虑了一个需要读取和操作数据文件的简单示例。
在下一章中,我们将介绍使用 Pandas 模块提供的一些主要工具以图形和数字方式探索数据的技术。
二、探索数据
当开始处理一个新的数据集时,首先了解可以从数据中得出什么结论是至关重要的。在我们能够进行推理和假设检验之前,我们需要了解手头的数据能够回答什么问题。这是探索性数据分析的关键,探索性数据分析是开发直觉和识别数据中统计模式的技能和科学。在本章中,我们将介绍有助于这项任务的图形和数字方法。你会注意到,对于如何进行每一步,没有硬性的规则,但相反,我们给出了在每种情况下适合的技术建议。发展成为数据探索者专家所需技能的最佳方式是查看大量示例,或许更重要的是,在我们自己的数据集上工作。更具体地说,本章将涵盖以下主题:
- 执行数据的初始探索和清理
- 绘制一元分布的直方图、核密度估计、概率和箱线图
- 绘制二元关系的散点图,并给出数据的各种点估计的初步概述,如*均值、标准偏差等
在开始阅读本章中的示例之前,请启动 Jupyter 笔记本并运行与上一章中提到的相同的初始命令。记住笔记本所在的目录。示例的数据文件夹需要存储在同一个目录中。
社会概况
为了在本章中给出具体的数据示例,我们将使用一般社会调查 ( GSS )。GSS 是芝加哥大学 T4 国家观点研究中心进行的一项大型社会趋势调查。由于这是一个非常复杂的数据集,我们将使用数据的子集,即 2012 年调查的汇编。对于 5.5 MB 的大小,按照当前的标准,这是一个很小的数据大小,但是仍然非常适合本章中说明的那种探索。(史密斯、汤姆·W、彼得·马斯登、迈克尔·豪特和金·吉布姆。一般社会调查,1972-2014 年[机器可读数据文件]/首席调查员,汤姆·史密斯;彼得·马斯登共同首席调查员;联合首席调查员迈克尔·豪特;国家科学基金会主办。-NORC·艾德。-芝加哥:NORC 在芝加哥大学[制片人];康涅狄格州斯托尔斯:康涅狄格州大学罗珀民意研究中心[发行人],2015 年。)
获取数据
示例中使用的 GSS 子集可以在该书的网站上找到,但也可以直接从 NORC 网站下载。请注意,除了数据本身之外,还需要获取带有元数据的文件,其中包含调查中考虑的变量的缩写列表。
要下载数据,请按照以下步骤进行:
- 前往http://www3.norc.org。
- 在搜索栏中,输入
GSS 2012 merged with all cases and variables。 - 点击标题为 SPSS | NORC 的链接。
- 向下滚动至合并的单年数据集部分。点击名为的链接,GSS 2012 合并了所有案例和变量。如果有多个版本,请选择最新版本。
- 按照程序将文件下载到计算机。文件将被命名为
gss2012merged_stata.zip。解压缩文件将创建GSS2012merged_R5.dta数据文件。(不同版本的文件名可能略有不同。) - 如有必要,将数据文件移动到笔记本所在的目录。
我们还需要描述数据中变量缩写的文件。这可以通过以下步骤完成:
- 前往http://gss.norc.org/Get-Documentation。
- 点击名为数据集索引的链接。这将下载一个 PDF 文件,其中包含变量缩写及其相应含义的列表。浏览此文件可以让您了解本次调查中所提问题的范围。
你可以随意浏览 GSS 网站上的信息。使用 GSS 的研究人员可能需要熟悉与数据集相关的所有细节。
读取数据
我们的下一步是确保我们可以将数据读入笔记本。数据采用 STATA 格式。STATA 是一个众所周知的统计分析包,其格式在数据文件中的使用非常广泛。幸运的是,Pandas 允许我们以一种简单的方式读取 STATA 文件。
如果您还没有这样做,请启动一个新的笔记本并运行默认命令来导入我们需要的库。(参考 第一章**交易工具。)
接下来,执行以下命令:
gss_data = pd.read_stata('data/GSS2012merged_R5.dta',
convert_categoricals=False)
gss_data.head()
读取数据可能需要几秒钟,所以我们要求您耐心一点。第一行代码调用 Pandas 的read_stata()函数读取数据,然后将结果存储在gss_data变量中,该结果是 DataFrame 类型的对象。convert_categoricals=False选项指示 Pandas 不要试图将列数据转换为分类数据,有时称为因子数据。由于数据集中的列只是数字,需要支持文档来解释其中的许多列(例如,性别,1 =男性,2 =女性),因此转换为分类变量没有意义,因为数字是有序的,但转换后的变量可能不是有序的。分类数据是有两个或更多,通常是有限数量的可能值的数据。它有两种类型:有序的(例如大小)和无序的(例如颜色或性别)。
注
这里需要指出的是,分类数据是 Pandas 的数据类型,不同于统计分类变量。统计分类变量只适用于无序变量(如前所述);有序变量称为统计序数变量。这方面的两个例子是教育和收入水*。注意,水*之间的距离(间隔)不需要固定。第三个相关的统计变量是统计区间变量,它与序数变量相同,只是水*之间有固定的区间;这方面的一个例子是固定间隔的收入水*。
在继续之前,让我们对数据导入的方式做一点改进。默认情况下,read_stata()函数会用从 0 开始的整数对数据记录进行索引。GSS 数据在标有id的列中包含自己的索引。要更改 DataFrame 对象的索引,我们只需为索引字段分配一个新值,如以下代码行所示(在单独的笔记本单元格中输入):
gss_data.set_index('id')
gss_data.drop('id', 1, inplace=True)
gss_data.head()
前面代码的第一行将gss_data字段的索引设置为标记为id的列。由于数据中不再需要该列,我们使用drop()方法将其从表中删除。inplace=True选项会导致gss_data自行修改。(默认情况下,返回一个包含更改的新数据框对象。)
现在让我们将表格保存到 CSV 格式的文件中。严格来说,这一步不是必需的,但如果有必要,它简化了重新加载数据的过程。要保存文件,请运行以下代码:
gss_data.to_csv('GSS2012merged.csv')
这段代码使用to_csv()方法,使用默认选项将表输出到名为GSS2012merged.csv的文件中。CSV 格式实际上没有正式的标准,但是由于规则简单,每行中的条目都由一些分隔符(例如,逗号)分隔的文件,它运行得相当好。然而,像往常一样,在读入数据时,我们需要检查它,以确保我们已经正确读取了它。包含数据的文件现在可以用标准电子表格软件打开,因为数据集不是很大。
单变量数据
我们现在准备开始玩数据。获得数据的初步感觉的一个好方法是创建图形表示,目的是了解其分布的形状。“分布”这个词在数据分析中有技术含义,但我们现在不关心这种细节。我们在的非正式意义上使用这个词,表示我们数据中的一组值是如何分布的。
从最简单的情况开始,我们单独查看数据中的变量,首先不用担心变量之间的关系。当我们观察单个变量时,我们说我们在处理单变量数据。因此,这是我们将在本节中考虑的情况。
直方图
直方图是显示定量数据分布的标准方式,即可以用实数或整数表示的数据。(请注意,整数也可以用来表示某些类型的分类数据。)直方图将数据分成若干个面元,这些面元只是值的间隔,并计算每个面元中有多少数据点。
让我们把注意力集中在标有age的栏上,它记录了被调查者的年龄。要显示数据直方图,请运行以下代码行:
gss_data['age'].hist()
plt.grid()
plt.locator_params(nbins=5);
在这段代码中,我们使用gss_data['age']来引用名为 age 的列,然后调用hist()方法绘制直方图。不幸的是,这个情节包含了一些多余的元素,比如网格。因此,我们通过调用plt.grid()触发函数来移除它,之后,我们重新定义通过plt.locator_params(nbins=5)调用放置多少个刻度定位器。运行代码将产生下图,其中 y 轴是箱中的元素数量, x 轴是年龄:

直方图的关键特征是放置数据的面元数量。如果面元太少,可能会隐藏分布的重要特征。另一方面,过多的面元会导致直方图在视觉上强调样本中的随机差异,从而难以识别一般模式。上图的直方图似乎过于*滑,我们怀疑可能隐藏了分布的细节。我们可以通过在hist()的调用中添加选项箱来将箱的数量增加到 25 个,如下面的代码所示:
gss_data['age'].hist(bins=25)
plt.grid()
plt.locator_params(nbins=5);
将显示以下屏幕截图中的图:

请注意,现在直方图看起来不同了;我们在数据中看到了更多的结构。然而,分辨率仍然足以显示剧情的主要特征:
- 分布*似为单峰,即只有一个显著的峰。请注意,在进行此评估时,我们没有考虑很可能由样本随机性引起的小间隙和峰值。
- 分布是不对称的,有点向右倾斜,也就是说,它有一个向高值延伸的更长的尾巴。
- 分布范围大约从 20 年到 90 年,中心在 50 年左右。不清楚分布的模式或最高点是什么。
- 没有异常特征,例如异常值、间隙或聚类。
请注意,从这些观察中,我们已经可以说一些关于数据收集的事情:很可能对受访者有一个最低年龄要求。这可能是分布不对称的原因。在采样中要求下限通常会使分布的上尾部突出。
将这种分布与包含在realrinc栏中的被调查者的收入分布进行比较是有用的。然而,那里有一个小陷阱。让我们从创建一个只包含两个感兴趣的列的数据框开始,并通过运行以下命令显示结果的前几行:
inc_age = gss_data[['realrinc','age']]
inc_age.head(10)
请注意前面代码第一行中的双括号。我们正在使用 Pandas 提供的众多复杂索引功能之一,并将 Python 列表['realinc','age']作为gss_data数据框的索引。这具有选择 Python 列表中指定的两列的预期效果。
查看上一条命令的输出,我们可以看到realinc列有很多缺失值,由NaN值表示,这是 Pandas 缺省使用的缺失数据。这可能是由于几个原因,但一些受访者只是选择不透露他们的收入。因此,为了比较两列的分布,我们可以省略这些行,如下面的代码所示:
inc_age = gss_data[['realrinc','age']].dropna()
inc_age.head(10)
我们使用相同的索引来选择两列,但是现在我们调用dropna()方法来排除缺少数据的行。检查输出时,请注意 Pandas 巧妙地从提取值的原始数据帧中保留了行索引 ID。这样,如果需要,我们可以将数据与原始表进行交叉引用。
现在,使用下面几行代码生成两个变量的并排直方图非常简单:
ax_list = inc_age.hist(bins=40, figsize=(8,3), xrot=45)
for ax in ax_list[0]:
ax.locator_params(axis='x', nbins=6)
ax.locator_params(axis='y', nbins=3)
注意我们在hist()方法中使用的选项。除了设置箱数,我们使用figsize=(8,3)选项,将图形大小设置为 8 英寸乘 3 英寸和xrot=45选项,使 x 轴标签旋转 45 度,提高可读性。该命令返回图形的轴对象列表。我们将其保存到ax_list变量中。接下来,我们遍历这个列表来修改轴的对象(也就是我们正在绘制的图)。像以前一样,我们使用 matplotlib 的面向对象界面来更改刻度线的数量,这次是用不同的函数。玩玩 nbins 设置,看看会发生什么。
检查得到的直方图,我们可以看到它们有很大的不同:收入的分布严重倾斜,更重要的是,在 30 万美元以上的地区,与一个孤立的酒吧有很大的差距。让我们计算一下这个区域中有多少值,如下面的代码所示:
inc_age[inc_age['realrinc'] > 3.0E5].count()
在这段代码中,我们在数据框中使用了一个布尔索引inc_age['realrinc'] > 3.0E5。这将选择realinc列中值大于 300,000 美元的所有行(3.0E5相当于3.0 * 10<sup>5</sup>)。count()方法只是计算有多少值满足条件。
看产量,收入在 30 万美元以上的有 80 行。像往常一样,当有看起来不寻常的事情时,我们应该更仔细地看数据。让我们通过运行以下代码来显示相应值的数据:
inc_age[inc_age['realrinc'] >3.0E5].head(10)
这与上一个示例中的命令行非常相似,但是现在我们获取对应于前十行的一部分数据。输出包含一个惊喜:所有数据值都相等!
为了理解正在发生的事情,我们必须深入挖掘 GSS 调查中的假设。这项调查并没有要求受访者给出他们收入的数值。取而代之的是,向被调查者提供收入类别,这为问卷中的收入设定了上限。也就是说,所有收入超过一定值的受访者被集中在一起。顺便说一句,估计这些类别的实际收入是一个不简单的问题,在这个问题上有相当多的研究。
无论如何,就我们的目的而言,简单地排除 30 万美元以上的价值是合法的。要根据这一假设生成直方图,请运行下面几行所示的代码:
inc_age = gss_data[['realrinc','age']].dropna()
lowinc_age = inc_age[inc_age['realrinc'] <3.0E5]
ax_list = lowinc_age.hist(bins=20, figsize=(8,3), xrot=45)
for ax in ax_list[0]:
ax.grid()
ax.locator_params(axis='x' ,nbins=6)
ax.locator_params(axis='y' ,nbins=3)
请注意前面代码中的第二行,它选择inc_age数据框中与realrinc列中的值小于 300,000 的条目相对应的行,并将其存储在新的lowinc_age对象中。这将产生如下图所示的直方图:

通过查看输出,我们可以看到分布非常明显。收入分配明显向大价值倾斜,似乎有几个差距。请注意,差距可能是由于调查结构造成的,特别是关于从收入范围计算实际收入的方式。
让事情变得美好
到目前为止,示例中显示的直方图足以进行数据探索,但在视觉上不太吸引人。Pandas 用 matplotlib 做图表。Matplotlib 是一个广泛的技术绘图库,能够生成高质量的、演示就绪的图形(http://matplotlib.org)。为了说明这种可能性,运行以下代码(lowinc_age存储在前面的代码中):
ax_list = lowinc_age.hist(bins=20, figsize=(8,3),
xrot=45, color='SteelBlue')
ax1, ax2 = ax_list[0]
ax1.set_title('Age (years)')
ax2.set_title('Real Income ($)')
for ax in ax_list[0]:
ax.grid()
ax.locator_params(axis='x' ,nbins=6)
ax.locator_params(axis='y' ,nbins=4)
让我们从分析前面代码第三行中对hist()方法的调用开始。使用 matplotlib 进行绘图时,每个命令有几种方法。在这里,我们展示了面向对象的方式,其中ax1, ax2 = ax_list[0]获取并存储两个轴。然后,我们设置标题,并使用这些对象关闭每个轴的背景网格。
表征
我们现在考虑尝试将数据拟合到经典统计中的标准模型之一的问题。这可能是一个复杂的问题,因为真实数据可能不符合任何预定义的模型。第一步可能是试图*似分布的密度,即在这种情况下某个年龄的一小部分人(假设它是连续的)。常用的方法是核密度估计 ( KDE ),可以认为是*滑直方图。Pandas 可以很容易地生产 KDE 地块,如下面的代码所示:
age = gss_data['age'].dropna()
age.plot(kind='kde', lw=2, color='green')
plt.title('KDE plot for Age')
plt.xlabel('Age (years)')
在这段代码中,我们首先从数据中选择年龄列,删除缺失的值。然后我们用kind='kde'选项调用plot()方法(用kde作为选项,SciPy 包是一个依赖项)。请注意,这与我们用于直方图的界面略有不同。还要注意,我们使用选项来设置绘图的线宽和颜色。最后两行设置了 x 轴的标题和标签,虽然这次是直接函数方法,而不是前面显示的面向对象方法。你将如何用面向对象的方法来设计这个?
需要注意的一个重要因素是 Pandas 不支持设置 KDE 图的参数,特别是*滑过程中使用的带宽。带宽有点类似于直方图中的面元宽度,不同的带宽会产生明显不同的*似值。Pandas 使用启发式*似,在大多数情况下会产生接*最佳的拟合,但在某些情况下可能不会产生最佳结果。我们预计未来版本的 Pandas 在这里会更加灵活。
运行前面的代码,我们得到如下显示:

虽然这一切都很好,但我们可能想在柱状图上绘制 KDE。这可以通过以下代码来完成:
ax = age.hist(bins=30, color='LightSteelBlue', normed=True)
age.plot(kind='kde', lw=2, color='Green', ax=ax)
plt.title('Histogram and KDE for Age')
plt.xlabel('Age (years)');
这将产生一个带有直方图的图形,归一化后就像 KDE 图一样,上面是 KDE 曲线。该图如下所示:

KDE 曲线有点钟形,但是如果你熟悉正态分布,你会注意到曲线似乎在尾部下降得太快而归零。为了直观地了解数据偏离正态分布的程度,我们可以使用正态分布图。在正态分布图中,数据与正态分布值一起绘制,具有与数据相似的特征。这种情节在 Pandas 的当前版本中不受支持,但是我们可以使用 SciPy 来创建情节,如下面几行代码所示:
import scipy.stats as stats
stats.probplot(age, dist='norm', plot=plt)
第一个参数age是要绘制的数据。然后我们使用dist='norm'选项将数据与正态分布进行比较。最后一个选项plot=plt指定plt模块应用于绘图。这可以是现有的轴实例(对象)或绘图模块;在这种情况下,我们只需将其发送到plt,也就是matplotlib.pyplot模块。结果图如下图所示:

请注意,数据明显偏离了绘图末端的直线:它位于左侧直线的上方和右侧直线的下方。与正态分布的相似性越多,数据值就越符合直线。这里呈现的年龄数据与尾部比正态分布短的数据一致,这与我们早期的观察一致。因此,我们会得出结论,正态分布对于这些数据是不够的。
统计推断的概念
在分析的这一点上,我们已经弄清楚了样品的一些情况。它不是正态分布的。在进一步的探索中,我们将发现更多关于样本的信息。有了来自全部人口的充分抽样数据,我们可以通过对数据样本进行分析来得出关于人口的结论。这是统计推断的基础,下图说明了这个概念。该概念在以下步骤中提到:
- 我们从人群中抽取了一个希望没有偏见的样本。
- 通过数据分析,我们对样本数据进行了表征。
- 通过统计测试、参数估计和类似的工具,我们可以得出关于样本的结论。
- 通过推断,我们现在可以对整个人口得出结论。

数值汇总和箱线图
我们现在朝着用数字描述数据的方向前进。在我们开始之前,我们需要提醒一下。简单地使用数据中的一组数字来得出结论并不是好的做法。依靠确定的数字提供一定程度的确定性的感觉是非常诱人的。然而,没有上下文和进一步分析的数值不是很有用,可能会误导。在这一章中,我们只考虑初步研究的方法,试图熟悉数据,以便了解它的表现。
当考虑数字数据时,我们可能会问以下问题:
- 数据的范围有多大?也就是说,最小和最大的值是多少?
- 数据值的中心在哪里?我们将考虑中心性的两个衡量标准,意味着和中间值。
- 数据从其中心扩散了多少?我们将考虑标准偏差以及四分位数和百分位数的概念作为传播的度量。
所有这些量都可以通过describe()方法在 Pandas 中轻松计算,适用于系列和数据帧类型的对象。回到收入分配,我们可以绘制如下数据汇总:
inc = gss_data['realrinc'].dropna()
lowinc = inc[inc <3.0E5]
lowinc.describe()
在第一行中,我们使用dropna()方法从表中选择realrinc列来丢弃丢失的数据。然后,我们只选择低于 30 万美元的收入,因为该栏中报告的收入可能无法可靠地反映高收入的分布,这一点我们之前已经讨论过。最后,我们使用describe()方法生成数据摘要。运行这段代码,我们获得如下行所示的输出:
count 2751.000000
mean 18582.194656
std 14841.581333
min 245.000000
25% 6737.500000
50% 15925.000000
75% 26950.000000
max 68600.000000
Name: realrinc, dtype: float64
该输出中提供的信息描述如下:
count是数据点的数量。因此,调查中有 2751 人的收入低于 30 万美元。mean是数据的*均值。因此,受访者的*均收入约为 18,582 美元。standard deviation(std)是数据如何围绕*均值传播的一种度量。标准差的公式有些技术性,将在后面的章节中介绍。minimum(min)和maximum(max)是数据中最小和最大的值。它们一起规定了数据的范围。在我们的例子中,收入从最低值 245 美元到最高值 68,600 美元。median(50%)是对应于数据集中点的值,即一半的值低于中值,一半的值高于中值。例如,根据我们的数据,一半的报告收入低于 15,925 美元,一半高于这个值。
注
这里导出的单个值代表我们的数据的某些特征,称为点估计。最常见和广泛使用的点估计是样本均值,它通过统计推断给出总体均值的点估计。点估计是对区间估计的恭维。这些由两个或多个数字定义,例如,如果样本均值位于某个区间,这表明总体均值也位于该区间。
- The
quartiles(25% and 75%) together with the median give a more specific view of how the data is distributed. In our data, they can be interpreted as follows:- 25%的收入低于 6737 美元
- 25%的收入在 6737 美元到 15925 美元之间
- 25%的收入在 15,925 美元到 26,950 美元之间
- 25%的收入在 26,950 美元到 15,925 美元之间
如果需要更详细的分布视图,我们可以请求输出更多的percentiles,如下面的代码所示:
lowinc.describe(percentiles=np.arange(0, 1.0, 0.1))
在这段代码中,我们使用了describe()方法的百分位数选项。np.arange(0, 1.0, 0.1)表达式用数字0.0, 0.1,0.2,...,0.9表示一个 NumPy 数组。前面示例的输出将提供将数据分成 10 个区间的值,每个区间包含报告收入的 10%。
诚然,数字总结很难想象。一个非常古老但仍然有用的图形工具是箱线图。尽管有些过时,但箱线图仍然可以用来显示数据的中心和分布。我们可以用以下代码显示收入的箱线图:
lowinc.plot(kind='box');
或者,可以运行以下程序:
lowinc.plot.box();
运行这些命令中的任何一个都会产生以下结果:

该箱线图可以解释如下:
- 用十字符号标记的点是异常值。异常值是远离分布中心的值。什么是离群值没有明确的普遍接受的定义,所以 Pandas 以启发式的方式确定它们(http://matplotlib . org/API/pyplot _ API . html # matplotlib . pyplot . box plot)。
- 底部和顶部的水*条(箱线图的须)分别代表最小值和最大值。
- 方框的底部和顶部分别代表 25%和 75%的四分位数。
- 框内的线代表中位数或 50%四分位数。
解释箱线图的一个快速方法是记住,排除异常值后,50%的数据值在盒子里面,25%在盒子下面,25%在盒子上面。请注意,在我们的示例中,数据的不对称性很明显,箱线图偏向高值。
箱线图对于比较不同亚群的数据也很有用。比方说,我们想比较男性和女性的收入。这可以通过以下代码来实现:
inc_gen = gss_data[['realrinc','sex']]
inc_gen = inc_gen[inc_gen['realrinc'] < 3.0E5]
inc_gen.boxplot(column='realrinc', by='sex');
前两条线选择要绘制的数据,不包括 30 万以上的收入。然后,调用 DataFrame 对象的boxplot()方法。我们用column='realrinc'选项指定要绘制的数据,用by='sex'选项指定组。执行时,将显示以下图形:

这些方框图讲述了一个关于收入如何分配的有趣故事。男性的收入( 1 )比女性的收入( 2 )分布更广,男性的收入明显高于女性。例如,女性收入分布的前四分之一略高于男性收入的中位数。也就是说,几乎 75%的女性收入相当于男性收入的后一半。分布的上层讲述了一个类似的故事。男性分布的四分位数以上的箱线图部分比女性分布的要长得多。请特别注意女性分布中的异常值。似乎肯定有一种玻璃天花板效应,很少有女性能够控制男性获得的最高工资。
变量之间的关系–散点图
当我们研究不同变量之间的关系时,数据分析的真正力量就显现出来了。在上一节的最后,我们将收入和性别联系起来,也就是说,一个量化变量和一个分类变量联系起来。在本节中,我们将研究散点图,散点图是两个定量变量之间关系的图形表示。
为了说明 Pandas 如何被用来探索两个变量之间的关系,我们将使用天文学历史上的一个重要例子。天文学家埃德温·哈勃在 1929 年发表了一篇非常重要的论文,他发现河外星云的距离和速度之间存在*似线性的关系。这是后来成为大爆炸理论的基础。
文章的重印版可在http://apod.nasa.gov/diamond_jubilee/d_1996/hub_1929.html获得,数据来源于此。请注意,数据集非常小,只需在文章本身中打印即可!对数据进行了一些小的格式化和清理,使其更易于使用。特别是,速度值被手动更改为全正,因为只有速度的大小才重要。要绘制数据的散点图,请输入并运行以下代码:
hubble_data = pd.read_csv('data/hubble-data.csv')
hubble_data.plot(kind='scatter', x='r', y='v');
在这段代码中,在使用read_csv()函数读取数据后,我们使用带有kind='scatter'选项的plot()方法绘制数据。x='r'和y='v'选项分别告诉 Pandas 在 x 和 y 轴上绘制哪些列。
观察图,可以清楚地看到距离和速度之间有关系,这与当时流行的宇宙是静止的观点相矛盾。为了使关系更清晰,我们可以在图中添加一条趋势线。
注
统计推断的一个核心部分是假设检验,即一个数据集/样本对照另一个数据集或模型生成的数据集进行检验。然后使用统计假设检验来研究两个数据集之间的关系。将所提出的关系与理想化的零假设进行比较,即两个数据集之间不存在关系。只有当零假设为真的概率低于某个显著水*时,它才会被拒绝。也就是说,假设检验只能给出零假设的意义,而不能给出所提出的模型。这是一个非常奇怪且通常很难理解的概念。本章我们将触及假设检验,并在 第 4 章**回归中进行更深入的探讨。
我们首先需要计算关系的线性回归线。我们使用 SciPy 通过以下代码来实现:
from scipy.stats import linregress
rv = hubble_data.as_matrix(columns=['r','v'])
a, b, r, p, stderr = linregress(rv)
print(a, b, r, p, stderr)
我们首先从scipy.stats模块导入linregress功能。该模块不支持 Pandas,因此我们首先使用as_matrix()方法将数据转换为 NumPy 数组。接下来,我们调用linregress函数,返回如下内容:
a:这是回归线的斜率b:这是回归线的截距r:这是相关系数p:这是假设检验的双边 p 值——假设斜率为零的零假设stderr:这是估算的标准误差
举个例子,四舍五入到两位小数,a=454.16、b=-40.78、r=0.79、p=4.48E-6和stderr=75.24。
线性回归涵盖在 第四章**回归中,但我们将在这里解读结果。0.79 的相关系数表明存在很强的关系,非常小的 p 值表明应该拒绝零假设,从而支持变量之间存在关系。r 的*方是 0.62,因此数据中 62%的可变性由线性模型解释,而不是随机变化。
所有这些都表明,对于宇宙中的星系,线性模型可以将速度的增加描述为距离的函数。为了直观地显示这一点,我们可以使用以下代码绘制回归线和数据:
hubble_data.plot(kind='scatter', x='r', y='v')
rdata = hubble_data['r']
rmin, rmax = min(rdata), max(rdata)
rvalues = np.linspace(rmin, rmax, 200)
yvalues = a * rvalues + b
plt.plot(rvalues, yvalues, color='red', lw=1.5)
plt.locator_params(nbins=5);
由于 Pandas 目前不支持在散点图上绘制回归线的选项,我们利用 matplotlib 被 Pandas 在后台用来构建图的事实。绘制散点图后,我们计算数据的最大值和最小值,并通过调用linspace()函数生成一个距离值相等的 NumPy 数组。然后, yvalues = a右值+ b* 语句计算直线上的点。最后,我们调用 matplotlib 的plot()函数来绘制直线。结果图形显示在下图中:

根据这个模型,哈勃继续假设宇宙正在膨胀,这个想法最终产生了宇宙学中目前接受的宇宙模型。
总结
在本章中,您学习了如何使用 Pandas 对数据进行初步探索。您学习了数据显示,包括直方图、单变量分布的 KDE 图和箱线图,以及双变量关系的散点图。我们还讨论了数据汇总,包括*均值、标准偏差、范围、中位数、四分位数和百分位数。
在下一章中,您将了解数据的统计模型。
三、了解模型
在最普通的意义上,一个模型是对现实的一部分的*似描述。模型对于科学,事实上对于任何知识领域都是必不可少的:只有通过一次专注于世界的一小部分并进行适当的简化,才有可能理解世界。
在本章中,我们将讨论以下主题:
- 在数据分析中使用基本模型
- 使用累积分布函数和概率密度函数来表征变量
- 使用前面的函数和各种工具进行点估计,生成具有一定分布的随机数
- 讨论离散和连续随机变量的例子以及多元分布的概述
模型和实验
模型可以采取多种形式:口头描述、一组数学方程或一段计算机代码。在这本书里,我们对一种特定的模型感兴趣,概率或统计模型,它代表发生在不确定性实验中的可变性。
注
我们在本书中使用术语实验在某种程度上是非技术性的。对我们来说,实验是对感兴趣的事件的任何观察。实验的例子是观察网站访问者的数量,或者进行民意测验或临床试验。对我们来说,实验的主要特点是可以重复,并且具有随机性,即同一实验的每次重复都可能导致不同的结果。
我们将考虑的模型采用随机变量的形式。随机变量是具有数值结果的概率结果的理想化表示。重要的是要认识到随机变量是一种抽象:它不代表特定实验的结果,它只是模拟一旦实验实际执行,我们期望得到什么结果。
在本章的剩余部分,我们将讨论统计模型是如何制定的,并描述数据分析中最重要的模型。
在运行本章中的示例之前,请启动 Jupyter 笔记本。默认导入后,在单元格中运行以下命令:
from pandas import Series, DataFrame
import numpy.random as rnd
import scipy.stats as st
您现在可以开始运行本章的代码了。
累计分布函数
在前一章中,当讨论数字数据的可视化表示时,我们引入了直方图,它表示数据在多个区间中的分布方式。直方图的一个缺点是箱的数量总是被任意选择,不正确的选择可能会给出关于数据分布的无用或误导的信息。
我们说直方图抽象了数据的一些特征。也就是说,直方图允许我们忽略数据中的一些细粒度可变性,从而使一般模式更加明显。
一般来说,在分析数据集时,抽象是一件好事,但我们希望对所有数据点有一个视觉上引人注目且计算上有用的精确表示。这是由累积分布函数提供的。这个函数对于统计计算一直很重要,在计算机出现之前,累积分布表实际上是一个必不可少的工具。然而,作为一个图形工具,累积分布函数通常不会在介绍性统计文本中得到强调。在我看来,这部分是由于历史偏见,因为在没有计算机的帮助下绘制累积分布函数是不方便的。
*举一个具体的例子,让我们首先用下面的代码段生成一组服从正态分布的随机值:
mean = 0
sdev = 1
nvalues = 10
norm_variate = mean + sdev * rnd.randn(nvalues)
print(norm_variate)
在这段代码中,我们使用 NumPy numpy.random模块(缩写为rnd)来生成随机化的值。随机模块的文档可在http://docs.scipy.org找到。具体来说,我们使用randn()函数,该函数生成均值为 0、方差为 1 的正态分布伪随机数,然后通过加法将分布移至均值,并通过乘法将分布扩大sdev。这个函数把我们想要的数值的数量作为一个参数,存储在nvalues变量中。
注
请注意,我们使用术语伪随机。当生成随机数时,计算机实际上使用一个公式来产生根据给定分布*似分布的值。这些值不可能是真正随机的,因为它们是由确定性公式生成的。可以使用真实随机性的来源,例如由网站https://www.random.org/提供的来源。然而,伪随机数通常足以用于计算机模拟和大多数数据分析问题。
结果存储在norm_variates变量中,该变量是一个 NumPy 数组。我们可以假设这些数字代表 10 克藏红花包装相对于目标重量的克数偏移量,也许是为了更好地理解这些数字。这意味着-0.1意味着包装中含有的0.1克藏红花比它应该含有的要少,+0.2意味着它含有的藏红花比它应该含有的要多。
运行此单元格将生成一个包含 10 个数字的数组。运行具有更多值的代码,比如 100(也就是说,nvalues=100),将产生更接*正态分布的分布。这个数组应该*似地遵循均值为 0、标准差为 1 的正态分布。从随机变量采样得到的值,例如norm_variate数组中的值,称为随机变量。根据定义,数据集的累积分布函数是这样一个函数:给定一个值x,返回不超过x的数据点数,归一化为 0 到 1。举一个具体的例子,让我们对之前生成的值进行排序,并使用以下代码打印它们:
for i, v in enumerate(sorted(norm_variates), start=1):
print('{0:2d} {1:+.4f}'.format(i, v))
这段代码使用for循环结构来遍历随机变量的排序列表。我们使用enumerate(),它提供了一个 Python 迭代器,在每次迭代中都从列表中返回索引i和相应的值v。start=1参数使迭代次数从1开始。然后,对于每一对,打印i和v。在 print 语句中,我们使用格式说明符,它是用花括号“{...}”括起来的表达式。在本例中,{0:2d}指定将i打印为两位十进制值,{1:+.4f}指定将v打印为指定的四位精度浮点值。结果,我们获得了数据的排序列表,从1开始编号。
我获得的值如下:
1 -0.1412
2 +0.6152
3 +0.6852
4 +2.2946
5 +3.2791
6 +3.4699
7 +3.6961
8 +4.2375
9 +4.4977
10 +5.3756
型式
当我们从(伪)随机变量中采样时,您将获得一组不同的值。
从这个列表中,很容易计算出累积分布函数值。例如,考虑值 2.2946。小于等于给定值的数据点有四个:-0.1412、0.6152、0.6852和2.2946本身。我们现在用数值 4 除以点数,结果在 0 和 1 之间。因此,对于这 10 个值,累积分布函数值为 4/10=0.4 。根据数学表达式,我们写下以下内容:

惯例是用 cdf 来缩写累计分布函数,我们从现在开始这样做。
需要注意的重要一点是,cdf 不是只在数据集中的值上定义的。事实上,它是为任何数值定义的!让我们假设,例如,我们想要在x=2.5找到 cdf 的值。注意数字2.5在数据集中的第四个和第五个值之间,所以仍然有四个数据值小于或等于 2.5。因此,2.5 的函数值也是4/10=0.4。事实上,可以看出,对于2.2946和3.2791之间的所有数字,cdf 将具有值0.4。
稍微思考一下这个过程,我们可以推断出 cdf 的以下行为:它在数据集中的值之间保持不变(即*坦)。对于每个数据值,函数将跳跃,跳跃的大小是数据点数量的倒数。下图说明了这一点:

在上图中, v 是数据集中的一个点,该图显示了 v 附*的 cdf。请注意数据点之间的*坦间隔和数据点 v 处的跳跃。间断处的实心圆表示该点的函数值。这些是离散数据集的任何累积分布函数的特征。
现在让我们定义一个可以用来绘制 cdf 图的 Python 函数。这是通过以下代码完成的:
def plot_cdf(data, plot_range=None, scale_to=None, **kwargs):
num_bins = len(data)
sorted_data = np.array(sorted(data), dtype=np.float64)
data_range = sorted_data[-1] - sorted_data[0]
coutns, bin_edges = np.histogram(sorted_data, bins=num_bins)
xvalues = bin_edges[:1]
yvalues = np.cumsum(counts)
if plot_range is None:
xmin = sorted_data[0]
xmax = sorted_data[-1]
else:
xmin, xmax = plot_range
#pad the arrays
xvalues = np.concatenate([[xmin, xvalues[0]], xvalues, [xmax]])
yvalues = np.concatenate([[0.0, 0.0], yvalues, [yvalues.max()]])
if scale_to is not None:
yvalues = yvalues / len(data) * scale_to
plt.axis([xmin, xmax, 0, yvalues.max()])
return plt.plot(xvalues, yvalues, **kwargs)
请注意,运行这段代码不会产生任何输出,因为我们只是在定义plot_cdf()函数。代码有些复杂,但我们所做的只是定义存储在xvalues和yvalues数组中的点列表。这些值是楼梯的前缘和特定台阶的高度。plt.step()功能将这些绘制成阶梯图。我们使用 NumPy 的concatenate()函数填充数组,从零开始,到数组的最大(或最后)值结束。要绘制数据集的 cdf,我们可以运行以下代码:
nvalues = 20
norm_variates = rnd.randn(nvalues)
plot_cdf(norm_variates, plot_range=[-3,3], scale_to=1.0,
lw=2.5, color='Brown')
for v in [0.25, 0.5, 0.75]:
plt.axhline(v, lw=1, ls='--', color='black')
在这段代码中,我们首先生成一组新的数据值。然后,我们调用plot_cdf()函数生成图形。函数调用的参数是plot_range,指定 x 轴中的范围,以及scale_to,指定我们希望值( y 轴)从 0 到 1 规范化。plot_cdf()函数的剩余参数被传递给 Pyplot 函数plot()。在本例中,我们使用lw=2.5选项设置线宽,使用color="Brown"选项设置线条颜色。
型式
scale_to选项的目的是允许在绘图中为yvalues设置不同的范围。使用scale_to=100.0, y 轴以百分比进行缩放,scale_to=None表示没有任何缩放的数据点计数。
运行此代码将生成一个类似于下面的图像:

由于数据是随机生成的,您获得的图表会有些不同。在上图中,我们还在yvalues``0.25``0.5和0.75处画了水*线。这些分别对应于数据中的第一个四分位数、中位数和第三个四分位数。查看 x 轴中的相应值,我们看到这些值大约对应于-0.5、0 和 0.5。这是有意义的,因为理论正态分布的实际值是-0.68、0.0和0.68。请注意,由于随机性和样本量小,我们预计无法准确恢复这些值。
现在,我邀请你进行下面的实验。通过更改前面代码中nvalues变量的值来增加数据集中的值数量,并再次运行单元格。将会注意到,随着数据值数量的增加,曲线变得更*滑,并向围绕其中心对称的 S 形曲线收敛。还要注意,四分位数和中位数将趋向于接*正态分布、-0.68、0.0和0.68的理论值。正如我们将在下一节中看到的,这些是标准正态分布的一些特征。
让我们研究真实数据集的 cdf。housefly-wing-lengths.txt文件包含了家蝇小样本的十分之一毫米的翅膀长度。数据来自生物统计学(第 109 页)杂志上索卡尔,R. R. 和罗尔夫,J. R. 的一篇 1968 年论文和索卡尔、 R. R. 和体育猎人杂志上的一篇 1955 年论文。社会主义者美洲(第 48 卷,第 499 页)。数据也可以在http://www.seattlecentral.edu/qelp/sets/057/057.html在线获得。
要读取数据,请确保housefly-wing-lengths.txt文件与 Jupyter 笔记本在同一个目录中,然后运行以下代码段:
wing_lengths = np.fromfile('data/housefly-wing-lengths.txt',
sep='\n', dtype=np.int64)
print(wing_lengths)
这个数据集是一个纯文本文件,每行一个值,所以我们简单地使用 NumPy 的fromfile()函数将数据加载到一个数组中,我们将其命名为wing_lengths。sep='\n'选项告诉 NumPy 这是一个文本文件,其值由新的行字符\n分隔。最后,dtype=np.int64选项指定我们要将值视为整数。数据集很小,我们可以打印所有的点,这是通过重复单元格末尾的wing_lengths数组名称来实现的。
现在,让我们通过在一个单元格中运行以下代码来生成该数据的 cdf 图:
plot_cdf(wing_lengths, plot_range=[30, 60],
scale_to=100, lw=2)
plt.grid(lw=1)
plt.xlabel('Housefly wing length (x.1mm)', fontsize=18)
plt.ylabel('Percent', fontsize=18);
我们再次使用先前定义的plot_cdf()函数。请注意,现在我们使用scale_to=100来代替比例,我们可以读取垂直轴上的百分比。我们还向图中添加网格和轴标签。我们获得了以下图:

请注意,cdf 大致具有我们之前观察到的相同的对称 S 形图案。这表明正态分布可能是该数据的合适模型。事实上,这个数据非常符合正态分布。
作为可以从这个情节中提取的信息类型的一个例子,让我们假设我们想设计一个能捕捉 80%苍蝇的网。也就是说,网格应该只允许翅膀长度在底部 20%的苍蝇通过。从上图中,我们可以看到第 20百分位对应的机翼长度约为十分之四十二毫米。这并不是一个现实的应用,如果我们真的在构建这个网络,我们可能想做一个更仔细的分析,但是它显示了我们如何从累积分布图中快速获得数据信息。
使用发行版
我们强调累积分布函数的主要原因是,一旦我们获得了它,我们就可以计算与模型相关的任何概率。这是因为 cdf 是指定随机变量的通用方法。特别是连续或离散数据的描述没有区别。随机变量的密度函数也是一个重要的概念,所以我们将在下一节介绍它。在本节中,我们将看到如何使用 cdf 来进行与随机变量相关的计算。
我们将在发行版中使用的函数是 SciPy 的一部分,包含在scipy.stats模块中,我们使用以下代码导入该模块:
import scipy.stats as st
之后,我们可以用缩写st来引用包中的函数。
本模块包含大量预定义的发行版,我们鼓励您访问位于http://docs.scipy.org/doc/scipy/reference/stats.html的官方文档,了解有哪些可用的发行版。对我们来说幸运的是,该模块的组织方式使得所有分发都以统一的方式进行处理,如下行所示:
st.<rv_name>.<function>(<arguments>)
该表达式的组成部分如下:
st是我们为统计包选择的缩写。<rv_name>是分布的名称(rv代表随机变量)。<function>是我们要计算的具体函数。<arguments>是需要传递给每个函数的值。这些可能包括每个分布的形状参数,以及依赖于被调用函数的其他必需参数。
下表列出了每个随机变量可用的一些函数:
| **功能** | **描述** | | `rvs()` | 随机变量,即伪随机数的产生 | | `cdf()` | 累积分布函数 | | `pdf()`或`pmf()` | 概率密度函数(对于连续变量)和概率质量函数(对于离散变量) | | `ppf()` | 百分比点函数,累积分布函数的反函数 | | `stats()` | 计算分布的统计(矩) | | `mean()`、`std()`或`var()` | 分别计算*均值、标准偏差和方差 | | `fit()` | 使数据符合分布,并从数据中返回形状、位置和比例参数 |每当我们指定一个分布时,我们需要设置表征感兴趣的随机变量的参数。大多数模型至少有一个位置和刻度,它们通常被称为随机变量的shape参数。该位置指定分布的偏移,而比例代表值的重新缩放(例如单位改变时)。
在下面的例子中,我们将集中讨论正态分布,因为这肯定是一个重要的情况。然而,你应该知道我们给出的计算模式可以用于任何分布。因此,例如,如果需要使用对数-拉普拉斯分布,他们所要做的就是用loglaplace替换下面例子中的norm。当然,有必要查阅文档,以确保正确的参数被用于兴趣分布。
型式
统计技术的宝贵资源是 NIST 工程统计手册,可在 http://www.itl.nist.gov/div898/handbook/index.htm 获得。第 1.3.6 、概率分布一节包含了随机变量和分布的精彩介绍。另一个快速参考是维基百科概率分布列表,位于http://en . Wikipedia . org/wiki/List _ of _ probability _ distributions。
对于正态分布,位置参数给出分布的*均值,刻度表示分布的标准差。这些术语将在本章后面定义,但它们是衡量分布中心和分布范围的数字。为了给出正态分布随机变量的一个具体例子,让我们考虑 20 岁以上女性的身高,如《国家健康统计报告》,第 10 期,2008 年 10 月,可在http://www.cdc.gov/nchs/data/nhsr/nhsr010.pdf在线获得。该报告包含按年龄组划分的美国人口的人体测量参考数据。在第 14 页,20 岁以上女性的*均身高为 63.8 英寸,标准误差为 0.06。样本量为 4,857。
我们将假设高度是正态分布的(碰巧这是一个合理的假设)。为了描述分布的特征,我们需要*均值和标准偏差。*均值直接在报告中给出。标准偏差不直接报告,但可以根据标准误差和样本量,按照以下公式计算:

标准误差是将在 第 4 章**回归中引入的样本可变性的度量。我们还忽略了一个问题,即我们使用的是样本标准差而不是总体标准差,但样本足够大,足以证明这种方法是正确的,这将在 第 4 章**回归中看到。我们首先使用以下代码定义要用作这种情况的模型的分布:
N = 4857
mean = 63.8
serror = 0.06
sdev = serror * np.sqrt(N)
rvnorm = st.norm(loc=mean, scale=sdev)
在这段代码中,我们首先定义变量来表示样本大小、*均值和标准误差。然后,根据前面的公式计算标准偏差。在最后一行代码中,我们调用norm()函数,传递均值和标准差作为参数。函数返回的对象被赋给rvnorm变量,我们就是通过这个变量来访问包的功能的。
型式
不需要先构造一个对象就可以进行所有的计算,但是如果我们想对同一个随机变量进行多次计算,这是推荐的方法。
从任何分布开始的一个好地方是制作图表,这样我们就可以对分布的形状有一个概念。这可以通过以下代码来实现:
xmin = mean-3*sdev
xmax = mean+3*sdev
xx = np.linspace(xmin,xmax,200)
plt.figure(figsize=(8,3))
plt.subplot(1,2,1)
plt.plot(xx, rvnorm.cdf(xx))
plt.title('Cumulative distribution function')
plt.xlabel('Height (in)')
plt.ylabel('Proportion of women')
plt.axis([xmin, xmax, 0.0, 1.0])
plt.subplot(1,2,2)
plt.plot(xx, rvnorm.pdf(xx))
plt.title('Probability density function')
plt.xlabel('Height (in)')
plt.axis([xmin, xmax, 0.0, 0.1]);
前面代码的大部分内容都是关于设置和格式化绘图的。最重要的两段代码是对plot()函数的两次调用。第一次通话如下:
plt.plot(xx, rvnorm.cdf(xx))
xx数组先前被定义为包含要绘制的 x 坐标。rvnorm.cdf(xx)函数调用计算 cdf 的值。第二个调用类似如下:
plt.plot(xx, rvnorm.pdf(xx))
唯一不同的是,现在我们叫rvnorm.pdf(xx)来计算概率密度函数。代码的总体效果是生成 cdf 和密度函数的并排图。这些是常见的 S 形和钟形曲线,它们是正态分布的特征:

现在让我们看看如何使用 cdf 来计算与分布相关的量。假设一家服装厂使用女性身高分类,如下表所示:
| **尺寸** | **高度** | | 娇小的 | 59 英寸到 63 英寸 | | *均的 | 63 英寸到 68 英寸 | | 高的 | 68 英寸到 71 英寸 |女性属于*均类的比例是多少?我们可以直接从 cdf 获得这些信息。回想一下,这个函数给出了数据值与给定值的比例。因此,身高达到 68 英寸的女性比例由以下表达式计算:
rvnorm.cdf(68)
同样,身高达到 63 英寸的女性比例用以下表达式计算:
rvnorm.cdf(63)
身高在 63 英寸到 68 英寸之间的女性比例就是这些数值之间的差值。由于我们想要一个百分比,我们必须将结果乘以 100,如下面一行代码所示:
100 * (rvnorm.cdf(68) - rvnorm.cdf(63))
计算结果显示,根据这一分类,大约 42.8%的女性是*均值。要计算三个类别的百分比并以一种良好的格式显示它们,我们可以使用以下代码:
categories = [
('Petite', 59, 63),
('Average', 63, 68),
('Tall', 68, 71),
]
for cat, vmin, vmax in categories:
percent = 100 * (rvnorm.cdf(vmax) - rvnorm.cdf(vmin))
print('{:>8s}: {:.2f}'.format(cat, percent))
在这段代码中,我们从创建描述类别的 Python 列表开始。每个类别由一个三元组表示,包含类别名称、最小高度和最大高度。然后,我们使用for循环来迭代类别。对于每个类别,我们首先使用 cdf 计算该类别中女性的百分比,然后打印结果。
这种分类的一个有点出乎意料的特点是,它对女性的身高施加了最低和最高的值。我们可以用以下代码计算出太矮或太高而不适合任何类别的女性的百分比:
too_short = 100 * rvnorm.cdf(59)
too_tall = 100 * (1 - rvnorm.cdf(71))
unclassified = too_short + too_tall
print(too_short, too_tall, unclassified)
运行这段代码,我们得出结论,几乎 17%的女性是未分类的!这似乎不算太多,但任何忽视这部分客户的行业都将失去利润。假设我们被雇来想出一个更有效的分类。假设我们同意分布中心 50%的女性应该被归为*均,前 25%应该被认为高,后 25%应该被认为娇小。换句话说,我们想用分布的四分位数来定义身高类别。请注意,这是一个武断的决定,可能不现实。
我们可以用 cdf 的倒数找到类别之间的阈值。这是通过ppf()方法计算的,表示百分比点函数。这在以下代码中显示:
a = rvnorm.ppf(0.25)
b = rvnorm.ppf(0.75)
print(a, b)
型式
这个计算告诉你如何计算任何分布的第一个和第三个四分位数。一般来说,要找到分布的百分位数c,我们可以使用rvnorm.ppf(c/100.)表达式。
根据前面的计算,根据我们的标准,如果女性的身高在大约 61 英寸到 67 英寸之间,那么她们应该被认为是*均身高。看来原来的分类是偏向高个子女性的(有很大比例的矮个子女性不符合这个分类)。我们只能推测为什么会这样。行业标准有时是从传统继承而来的,可能是在没有考虑实际数据的情况下制定的。
值得注意的是,在我们所做的所有计算中,我们使用了累积分布函数,而不是概率密度函数,这将在下一节中介绍。事实上,这将是推理中所需的大多数计算的情况。事实上,大多数人在定义分布时更喜欢使用概率密度,这可能只是由于历史偏见。事实上,随机变量的两个视图都很重要,在理论和应用中都有一席之地。
为了完成这个例子,让我们使用下面的代码计算高度分布的一些相关参数(即点估计值):
mean, variance, skew, kurtosis = rvnorm.stats(moments='mvks')
print(mean, variance, skew, kurtosis)
这将打印*均值的63.8,方差的17.4852,以及偏斜度和峰度的 0 值。这些值解释如下:
mean是分布的*均值。由于分布是对称的,它与中位数一致。variance是标准差的*方。它被定义为偏离*均值的*方的*均值。skew测量分布的不对称性。因为正态分布是对称的,所以偏斜为零。kurtosis表示分布是如何达到峰值的:它是有一个尖锐的峰值还是一个*坦的凸起?正态分布的峰度值为零,因为它用作参考分布。
对于我们的下一个示例,假设我们需要为一些设备的故障时间构建一个模拟。这种情况下经常使用的模型是威布尔分布,以沃罗迪·威布尔命名,他在 1951 年仔细研究了该分布。这个分布用两个数字来描述,称为scale参数,用η ( eta)表示,和shape参数,用β ( beta)表示。两个参数都必须是正数。
注
威布尔分布有一个三参数版本,引入了一个位置参数。我们假设设备从运行开始就可能出现故障,因此位置参数为零,可以忽略。
shape参数与设备的故障率如何取决于其年龄(或运行时间)有关,如下所示:
- 如果形状参数小于
1,则故障率降低久而久之。例如,如果有大量项目存在缺陷,并且在投入使用时往往会提前失效,就会出现这种情况。 - 如果形状参数等于
1,故障率在时间上是恒定的。这就是众所周知的指数分布。在这个模型中,设备在给定时间间隔内发生故障的几率并不取决于它已经运行了多长时间。在大多数情况下,这是一个不现实的假设。 - 如果
shape参数大于1,则设备故障率增加久而久之。这反映了一个老化过程,在这个过程中,较旧的设备更容易出现故障。
另一方面,比例参数决定了分布的扩散程度。用直观的方式来说,比例参数的较大值对应于故障时间预测的更多不确定性。请注意,在这种情况下,比例参数不能解释为模型的标准偏差。
假设我们要用shape参数βpara和比例参数ηand来模拟威布尔分布。在这一点上,我们鼓励您通过修改我们以前用于正态分布的代码来生成威布尔分布的累积分布和概率密度函数的图。要创建分发,可以使用以下代码:
eta = 1.0
beta = 1.5
rvweib = st.weibull_min(beta, scale=eta)
定义eta和beta参数后,我们调用weibull_min()函数,生成合适的对象。生成图表后,您会注意到威布尔分布明显不对称,在达到峰值后有一个长的右尾。
现在让我们回到模拟分布的问题。为了产生符合威布尔分布的样本大小500,我们使用以下代码:
weib_variates = rvweib.rvs(size=500)
print(weib_variates[:10])
在这段代码中,我们简单地调用rvs()方法,传递所需的样本大小作为参数。缩写 rvs 代表随机变量。由于生成的样本非常大,我们只需打印前 10 个值。我们可以使用直方图来可视化样本,如以下代码所示:
weib_df = DataFrame(weib_variates,columns=['weibull_variate'])
weib_df.hist(bins=30);
在这段代码中,我们首先将数据转换为 Pandas DataFrame 对象,因为我们想要使用 Pandas 的绘图功能。然后,我们简单地调用hist()方法,传递箱数作为参数。这将产生以下直方图:

注意 0.5 附*的峰值,后面是一个向右衰减的尾巴。还要注意直方图最右边的几个值。与大部分数据相比,这些数据代表了很长的存活时间。
最后,我们想评估模拟有多好。为此,我们可以绘制样本的累积分布函数,与理论分布进行比较。这是通过以下代码实现的:
xmin = 0
xmax = 3.5
xx = np.linspace(xmin,xmax,200)
plt.plot(xx, rvweib.cdf(xx), color='orange', lw=5)
plot_cdf(weib_variates, plot_range=[xmin, xmax], scale_to=1, lw=2, color='green')
plt.axis([xmin, xmax, 0, 1])
plt.title('Weibul distribution simulation', fontsize=14)
plt.xlabel('Failure Time', fontsize=12);
这段代码本质上是我们之前看到的代码段的组合。我们使用适当的参数调用plot()函数来生成理论 cdf 的图,然后使用本章前面定义的plot_cdf()函数来绘制数据的 cdf。可以看出,两条曲线显示出相当好的一致性。
作为本节的最后一个例子,让我们探索fit()方法,它试图将分布拟合到数据。让我们回到家蝇翅膀长度的数据集。如前所述,我们怀疑数据是正态分布的。我们可以用下面的代码找到最适合数据的正态分布:
wing_lengths = np.fromfile('data/housefly-wing-lengths.txt',
sep='\n', dtype=np.int64)
mean, std = st.norm.fit(wing_lengths)
print(mean, std)
为了安全起见,我们使用fromfile()功能再次读取数据。然后我们使用st.norm.fit()函数来拟合数据集的正态分布。fit()函数返回拟合正态分布的均值和标准差。这是参数估计(点估计)的一个例子。下一个问题是:契合度有多好?我们可以通过生成分位数图来进行图形化评估,如 第二章**探索数据所示。以下是代码:
st.probplot(wing_lengths, dist='norm', plot=plt)
plt.grid(lw=1.5, lw='dashed');
该代码将产生以下图:

请注意,样本非常接*直线,这表明正态分布非常适合此数据。但是,请注意,数据中存在一些聚类,这是重复值(测量不精确和舍入)的结果。
注
将模型拟合到样本是一个复杂的主题,应该小心处理。在这一节中,我们集中学习如何使用 NumPy 和 SciPy 提供的工具,而没有深入探讨我们使用的方法有多合适的问题。对一些主题的更仔细的讨论出现在本书的其余章节中。
我们通过鼓励您访问stats模块的 SciPy 文档来完成这一部分。这是一个包含大量功能的广泛模块。这些文献组织得非常好,非常全面,包括对所用方法的讨论以及与理论的相关链接。
概率密度函数
到目前为止,我们认为累积分布函数是描述随机变量的主要方法。然而,对于一大类重要模型来说,概率密度函数 ( pdf )是一个重要的替代表征。
为了理解 cdf 和 pdf 之间的区别,我们需要概率的概念。在随机变量的上下文中,概率仅仅意味着随机结果落在某个值范围内的可能性,归一化为 0 到 1 之间的数字。例如,让我们考虑上一节讨论的女性身高的例子。我们得出结论,42.8%的女性身高在 63 英寸到 68 英寸之间。另一种表达方式是说,对于代表女性身高的随机变量,结果在 63 到 68 之间的概率为. 428 。
cdf 和 pdf 之间的主要区别在于它们各自表示概率的方式:
- 对于 cdf,结果在范围内的概率计算为范围端点处 cdf 值之间的差值
- 对于 pdf,结果在范围内的概率计算为由范围确定的曲线下的区域
为了阐明这些概念,让我们考虑下图:

此图显示了标准正态分布的 cdf 和 pdf。在这两个图中,我们在水*轴上有一系列由值 a 和 b 定义的值。该图用图表说明了每种情况下结果落入该范围的概率:
- 在 cdf 的情况下,概率由 F(b)-F(a) 给出,其对应于 y 轴上高亮显示的线段的长度
- 在 cdf 的情况下,概率由数值 a 和 b 之间的曲线所限定的区域给出
这一观察事实上解释了为什么 cdf 在计算上更有用。要使用 cdf 计算概率,我们只需要两个值之间的差值,而要使用 pdf 进行同样的计算,就需要找到一个区域的值。由于这不是一个简单的形状,我们需要微积分来计算面积。事实上,在正态分布的情况下,这是一个即使用微积分课程中常见的方法也无法计算的区域!当然,当我们用 Python 进行计算时,这种复杂性仍然存在,但幸运的是,细节对我们来说是隐藏的。
这是一个很好的时间来简要提及 pdf 是如何与分布的重要特征(如*均值和标准差)相关联的。当谈到随机变量时,*均值的概念在技术上被称为随机变量的期望值或均值。直观地说,如果观察到大量具有相同分布的试验,这是我们期望看到的值的*均值。同样,随机变量的方差是*均值的*均*方偏差。
最后,标准差是方差的*方根。不幸的是,为了给连续随机变量给出这些概念的数学定义,我们再次需要微积分。由于这本书专注于 Python 在数据分析中的实际应用,我们将满足于这些概念的直观含义,让计算机在幕后做肮脏的计算工作。
我们最后指出,在本节中,我们集中于以*滑 cdf 为特征的连续分布。另一个极端是离散分布,它有一个看起来像阶梯的 cdf,很像上一节看到的例子。离散随机变量不能用 pdf 表示。相反,它们是根据概率质量函数 ( pmf )来定义的。下一节将讨论离散随机变量的一个例子。
模型从何而来?
在本节中,我们将考虑模型最重要的实用点。模型是如何构思的,我们如何知道在给定的情况下使用什么样的模型是正确的?
这些都不是简单的问题,设计和选择合适模型的过程既是一门艺术,也是一门科学。冒着过于简单化的风险,我们可以说概率模型可以来自两个来源:
- 在先验模型中,研究者考虑相关因素,识别重要的量和关系,并创建适合所考虑问题的描述
- 在极限模型中,研究者试图找到一个过于复杂的模型的*似值,无论是在概念上还是在计算上
在这两种情况下,生成的模型可能采取几种不同的形式。例如,它可以是数学公式、模拟或算法。在进行实验或观察后,必须始终根据真实数据验证模型。
最需要强调的一点是,所有模型都有假设。这些建立了模型有效的边界。识别所做的假设可能是成功选择模型的第一个重要步骤。
作为例子,我们将考虑两个具有历史和实际重要性的模型:二项分布和德莫伊弗*似。
二项式分布是离散随机变量的一个例子,它最初是在赌博的背景下构思的,但对许多情况具有广泛的适用性。以下是该模型的基本假设:
- 进行一系列 N 试验,每个试验只能有两个结果中的一个。我们将这两种可能的结果称为 0 和 1(失败和成功)。
- 每个试验都有已知的有结果的概率 p 和相应的有结果的概率1-p**0。
- 这些试验是独立的,即每个试验的结果不受其他试验结果的影响。
- 正在观察的变量是在 N 试验中等于
1的结果数。
很容易理解为什么这种模式对赌徒有吸引力:在一个碰运气的游戏中,一个人要么赢,用 1 表示,要么输,用 0 表示。每个结果的概率都是已知的。赌徒反复玩游戏,有兴趣知道会赚多少钱,这是由赢的次数决定的。
在更实际的应用中,我们可以想到质量控制系统。在这种情况下,1 代表好的项目,0 代表有缺陷的项目。我们知道一个项目好或有缺陷的概率 p ,并且想知道 N 项目生产时好项目数量的可变性。
具体来说,假设我们在玩一个公*的游戏,在这个游戏中,赢和输的概率都是0.5。我们假设玩了 20 场游戏。二项式分布也是scipy.stats模块的一部分,我们可以用下面的代码创建一个表示分布的对象:
N = 20
p = 0.5
rv_binom = st.binom(N, p)
由于该游戏被玩了 20 次,所以该系列中的获胜次数是 0 到 20 之间的整数。例如,为了计算我们在 20 场比赛中赢 12 场的概率,我们使用概率质量函数,如下式所示:
rv_binom.pmf(12)
评估这段代码,我们得出结论,20 场比赛中恰好有 12 场获胜的概率约为 0.12 或 12%。
在许多情况下,我们对确切获胜次数的概率不感兴趣,而是对范围的概率感兴趣。例如,让我们假设在特定的一天,我们在 20 场比赛中只赢了 7 场,并想知道我们是否被欺骗了。评估这一点的一种方法是计算赢得七场或更少比赛的概率。如果这个概率很小,很可能游戏公*的假设是不成立的。为了计算这个概率,我们需要累积分布函数,可以用下面一行代码来计算:
rv_binom.cdf(7)
结果表明,这一事件发生的概率约为 0.13,即 13%的时间。这不是一个很小的数字,所以我们预计,有时,我们实际上只会赢得 20 场比赛中的 7 场,即使是在一场公*的比赛中。所以,似乎没有理由怀疑作弊,至少在这个孤立的案例中。
为了了解分布的行为,让我们绘制cdf和pmf的图。这可以通过以下代码来完成:
xx = np.arange(N+1)
cdf = rv_binom.cdf(xx)
pmf = rv_binom.pmf(xx)
xvalues = np.arange(N+1)
plt.figure(figsize=(9,3.5))
plt.subplot(1,2,1)
plt.step(xvalues, cdf, lw=2, color='brown')
plt.grid(lw=1, ls='dashed')
plt.title('Binomial cdf, $N=20$, $p=0.5$', fontsize=16)
plt.subplot(1,2,2)
left = xx - 0.5
plt.bar(left, pmf, 1.0, color='CornflowerBlue')
plt.title('Binomial pmf, $N=20$, $p=0.5$', fontsize=16)
plt.axis([0, 20, 0, .18]);
这段代码类似于我们在本章前面显示的图表中使用的方法,但是我们在这里重复它,因为您可能会发现有一个模型来绘制离散分布图很有用。对于 cdf,我们简单地绘制出xvalues和yvalues数组,其中包含从rv_binom对象的cdf方法获得的 cdf 的阶梯。因为我们希望它以这种方式显示(即楼梯),所以我们使用 step 函数来绘制它。对于 pmf,我们使用稍微不同的方法,bar()函数,它是一个 matplotlib 函数,绘制一个通用的条形图。这个函数的参数是两个数组,包含每个条的左坐标和高度,以及一个指定条宽度的数字。下图显示了这些图:

这里有一点需要注意:pmf 是一个只为 1 到 20 之间的整数值定义的函数。然而,为了可视化的目的,而不仅仅是绘制离散点,我们绘制宽度为 1 的条,因为数据是离散的。每个条形以对应于胜的整数为中心。通过这些选择,概率模型中的概率对应于条形的面积,这与连续分布的概率模型的解释相同。
你可能注意到前面的图和正态分布有惊人的相似之处。法国数学家德·莫伊弗第一个注意到,如果试验次数 N 很大,二项式分布的 pmf 图*似于*滑曲线。他意识到,如果他能够找到这条曲线的公式,他将有一个更简单的方法来计算大量试验的二项式概率。他找到了曲线的公式,并利用这个公式计算了 3600 次试验的二项式概率,这在当时是一个了不起的壮举。这就是正态分布的诞生。
为了理解德莫尔*似,我们必须首先计算二项式分布的*均值和标准差。我们将以两种方式做到这一点。首先,让我们使用scipy.stats中提供的功能,如下代码所示:
mean = rv_binom.mean()
std = rv_binom.std()
print(mean, std)
在这里,我们简单地调用mean()和std()方法来计算*均值和标准差。另一种方法是使用理论公式,如以下代码所示:
mean = N * p
std = np.sqrt(N * p * (1 - p))
print(mean, std)
不管怎样,我们得到*均值为 10.0,标准差约为 2.236。德莫伊弗*似定理可以非正式地表述如下:
对于大 N,二项式分布*似为均值和标准差相同的正态分布。
下图显示了二项式分布的 pmf,叠加了具有相同*均值和标准偏差的正态分布图。可以看出,这种一致是显著的:

多元分布
到目前为止,在这一章,我们只考虑了一个随机实验的情况下,有一个单一的数字结果。在这个框架内,我们只能对单个变量建模。在大多数数据分析问题中,我们可能对变量之间的关系感兴趣。例如,我们可能想了解一个人的身高和体重之间的关系,或者收入和教育水*之间的关系。在另一种情况下,我们可能会反复观察一个变量。例如,我们可能对一个地区冬季的日降雪量感兴趣。
为了处理这些情况,我们需要由多元分布描述的模型。我们有类似的 cdf 和 pdf(或离散分布的 pmf),但现在我们必须使用依赖于几个变量的函数。我们在前面几节中讨论的单变量分布被用作构建块,但是我们有额外的复杂性,即必须指定不同变量如何相互作用。
典型的例子是二元正态分布。在这个模型中,我们观察到两个正态分布的随机变量。这两个变量都有各自的*均值和标准差。然而,我们也必须说这两个变量是如何相互作用的。
在最简单的情况下,一个变量的结果对另一个变量的结果没有任何影响。例如,考虑一下伦敦的降雪和悉尼足球比赛比分之间的关系。除非我们相信某种超自然的联系,否则我们不会期望这些变量之间有任何联系。在这种情况下,我们说变量是独立的。
另一方面,更有趣的是,变量可能是相关的;从某种意义上说,一个观测结果将影响另一个观测结果的概率。例如,我们期望一个人的体重和身高是相关的。在这种情况下,我们将有兴趣知道相关性有多强,并可能使用其中一个变量来预测另一个变量。
多元正态分布也是scipy.stats包的一部分。现在,我们将只看到如何根据二元正态分布生成随机变量。我们将在本书后面详细讨论多元分布。让我们运行以下代码来生成随机变量:
binorm_variates = st.multivariate_normal.rvs(mean=[0,0], size=300)
df = DataFrame(binorm_variates, columns=['Z1', 'Z2'])
df.head(10)
在这段代码中,我们使用multivariate_normal.rvs()函数从均值为零和默认协方差1的二元正态中生成大小为 300 的样本。然后,我们将 NumPy 数组转换为 Pandas 数据帧,并打印数据帧的前 10 个组件。我们现在可以使用下面几行代码创建变量的散点图:
df.plot(kind='scatter', x='Z1', y='Z2')
plt.title('Bivariate Normal Distribution')
plt.axis([-4,4,-4,4]);
这里,我们使用的是df对象的plot()方法。kind=scatter选项用于产生散点图。我们必须用相应的选项为散点图指定x和y组件。之后,我们设置图的标题并调整轴的范围:

总结
在本章中,您学习了数据分析中使用的基本模型。我们研究了如何使用累积分布函数和概率密度函数来表征随机变量,以及如何使用这些工具进行计算,包括计算*均值、标准偏差和方差以及生成随机变量。
我们已经看到了连续和离散随机变量的例子,并研究了两个重要的情况:二项式分布及其正态分布的逼*。本章最后概述了多元分布。
在下一章中,我们将仔细研究回归的各种方法。*
四、回归
线性回归是实验技术一般介绍的一部分;它构成了过去几个世纪许多科学突破的基础。我们之前对线性回归做了一些简短的探索,看看哈勃定律和其他一些东西。前一章包括研究分布,这是探索性数据分析不可或缺的一部分,也是深入了解数据的第一步。正如你将看到的,我们到目前为止所经历的所有事情在这一章中也是有用的。强烈鼓励你结合前几章学到的知识,尝试这些新事物。在本章中,我们将讨论以下形式的回归:
- 线性回归
- 多次回归
- 逻辑回归
在最简单的公式中,线性回归处理从另一个变量估计一个变量。在多元回归中,一个变量是从两个或多个其他变量中估计出来的。当然,只有当变量之间存在某种相关性时,这种方法才有效。在这一点上需要指出的是,相关性并不意味着因果关系;仅仅因为两个或更多的变量显示出相互依赖,并不意味着它们在现实生活中实际上相互影响/依赖。逻辑回归将模型拟合到一个或多个离散变量,这些变量有时是二元的(也就是说,只能取值 0 或 1)。
在本章中,我们将从线性回归的快速介绍开始,然后我们直接进入获取数据和检验两个变量之间简单关系的假设。在此之后,我们将介绍对多个变量的扩展,我们只需将数据添加到上一节的内容中。逻辑回归在这一章的最后部分讨论。
强烈鼓励好奇心,并利用我们在前面章节中学到的知识来探索数据。在运行本章中的示例之前,启动 Jupyter 笔记本并运行默认导入。
引入线性回归
最简单的线性回归形式由关系 y = k x + k 0 给出,其中k0T10】称为截距,即x=0和 k 时 y 的值为斜率。通过将每个点看作前面的关系加上一个错误 ε ,可以找到这个问题的一般表达式。然后,对于 N 点,这将如下所示:





我们可以用矩阵形式来表达:

这里,各种矩阵/向量表示如下:

执行矩阵和向量的乘法和加法应该产生这里定义的相同的方程组。在这种情况下,回归的目标是估计参数 k。有许多类型的参数估计方法——普通最小二乘法是最常见的方法之一——但也有最大似然法、贝叶斯法、混合模型法和其他几种方法。在普通最小二乘最小化中,残差的*方最小化,即 r T r 最小化( T 表示转置, r 表示残差,即 Y 数据 -Y 拟合 )。求解矩阵方程是不*凡的;然而,如果时间允许的话,至少这样做一次是有益的。在下面的例子中,我们将使用最小二乘法。大多数情况下,基础计算使用矩阵来计算参数的估计值和确定中的不确定性。分析还得出变量之间的相关性,即变量之间存在线性关系的可能性有多大。
获取数据集
在我们开始估计线性关系的参数之前,我们需要一个数据集。在第一个例子中,我们将查看来自世界卫生组织在 http://www.who.int T4 的自杀率数据。数据分析的一个非常重要和复杂的部分是将数据放入适合我们分析的可管理的数据结构中。因此,我们将看到如何获取数据并将其映射到我们想要的数据结构。数据集的第一部分是每个国家和性别的年龄标准化自杀率(每 10 万居民)。
注
年龄标准化(也称为年龄调整)是一种技术,用于在群体的年龄分布不同时对群体进行比较。
以下代码下载数据并将其存储在文件中:
importurllib.request
payload='target=GHO/MH_12&profile=crosstable&filter=COUNTRY:*;REGION:*&x-sideaxis=COUNTRY&x-topaxis=GHO;YEAR;SEX'
suicide_rate_url='http://apps.who.int/gho/athena/data/xmart.csv?'
local_filename, headers = urllib.request.urlretrieve(suicide_rate_url+payload, filename='data/who_suicide_rates.csv')
urllib模块是 Python 标准库(https://docs.python.org/3/library)的一部分。如果没有输入文件名,文件将存储在磁盘上的临时位置。如果出现问题,也可以直接转到网址下载文件。或者,经合组织数据库包含的自杀率也可以追溯到 1960 年(http://stats.oecd.org)。
和以前一样,我们使用 Pandas 数据阅读器中的 Pandas 功能。在这里,我们给列命名;只能发送header=2参数。这将告诉您列名在标题中给出;然而,这可能并不总是我们想要的:
LOCAL_FILENAME = 'data/who_suicide_rates.csv_backup'
rates = pandas.read_csv(LOCAL_FILENAME, names=['Country','Both', 'Female', 'Male'], skiprows=3)
rates.head(10)

为了方便您,我们告诉它跳过前三行,文件的元数据存储在这三行中。如前所述,CSV 文件格式缺乏适当的标准,因此很难正确解释所有内容,因此在这种情况下跳过标题会使其更加健壮。然而,我们鼓励尝试不同的输入参数,这可能会很有启发性。如前几章所示,我们从探索这个数据集开始:
rates.plot.hist(stacked=True, y=['Male', 'Female'],
bins=30, color=['Coral', 'Green'])
plt.xlabel('Rate');

直方图现在绘制了名称匹配男性和女性的列,它们堆叠在一起。这表明男性和女性的自杀率略有不同。打印出男女的*均自杀率表明,男性的自杀率明显更高:
print(rates['Male'].mean(), rates['Female'].mean())
14.69590643274854 5.070602339181275
为了更详细地了解一些基本的费率统计数据,我们使用 boxplot 命令:
rates.boxplot();

很明显,与女性相比,男性的自杀率更高。查看箱线图和组合分布(即和键),我们可以看到有一个异常值(即交叉)的比率非常高。每 10 万人中有 40 多人自杀;这是哪个国家?我们来看看:
print(rates[rates['Both']>40])
Country Both Female Male
66 Guyana 44.2 22.1 70.8
这里,我们通过说我们只想要Both列中的比率高于 40 的指数来过滤。显然,圭亚那的自杀率非常高。围绕这一点,有一些有趣的、显然是麻烦的事实。一个快速的网络搜索显示,尽管解释高比率的理论已经提出,研究还没有揭示显著高于*均比率的潜在原因。
如前面的直方图所示,自杀率都有相似的分布(形状)。让我们首先使用前面几章的例子中定义的 CDF 绘图函数:
def plot_cdf(data, plot_range=None, scale_to=None, nbins=False, **kwargs):
if not nbins:
nbins = len(data)
sorted_data = np.array(sorted(data), dtype=np.float64)
data_range = sorted_data[-1] - sorted_data[0]
counts, bin_edges = np.histogram(sorted_data, bins=nbins)
xvalues = bin_edges[1:]
yvalues = np.cumsum(counts)
if plot_range is None:
xmin = xvalues[0]
xmax = xvalues[-1]
else:
xmin, xmax = plot_range
# pad the arrays
xvalues = np.concatenate([[xmin, xvalues[0]], xvalues, [xmax]])
yvalues = np.concatenate([[0.0, 0.0], yvalues, [yvalues.max()]])
if scale_to:
yvalues = yvalues / len(data) * scale_to
plt.axis([xmin, xmax, 0, yvalues.max()])
return plt.step(xvalues, yvalues, **kwargs)
有了这个,我们可以再次研究自杀率的分布。对组合费率运行该功能,即Both栏:
plot_cdf(rates['Both'], nbins=50, plot_range=[-5, 70])

我们可以首先测试正态分布,因为它是最常见的分布:
st.probplot(rates['Both'], dist='norm', plot=plt);

对比我们之前在创作这样的情节时看到的,契合度一点都不好。回忆上一章的威布尔分布;它可能更适合向较低值倾斜。让我们试试看:
beta = 1.5
eta = 1\.
rvweib = st.weibull_min(beta, scale=eta)
st.probplot(rates['Both'], dist=rvweib, plot=plt);

威布尔分布似乎很好地再现了数据。r值或皮尔逊相关系数是衡量线性模型在多大程度上代表了两个变量之间的关系。此外,st.probplot给出的r*方值是r值的*方,在这种情况下。可以根据数据拟合分布。这里,我们用floc=0将位置参数固定为 0:
beta, loc, eta = st.weibull_min.fit(rates['Both'],
floc=0, scale = 12)
这给出了1.49的beta、loc的 0、10.76的scale。利用拟合的参数,我们可以绘制数据的直方图,并绘制分布图。我包含了一个固定的随机种子值,因此它应该以同样的方式为您再现结果:
rates['Both'].hist(bins=30)
np.random.seed(1100)
rvweib = st.weibull_min(beta, scale=eta) plt.hist(rvweib.rvs(size=len(rates.Both)),bins=30, alpha=0.5);

然后,比较威布尔分布的 CDF 和我们的数据,很明显它们是相似的。能够拟合分布的参数非常有用:
plot_cdf(rates['Both'], nbins=50,scale_to=1)
np.random.seed(1100)
plot_cdf(rvweib.rvs(size=50),scale_to=1)
plt.xlim((-2,50));

现在我们已经有了数据集的第一部分,我们可以开始尝试理解这一点。自杀率有哪些可能的参数?也许是经济指标或者与抑郁症相关的变量,比如一年的日照量?在接下来的部分中,我们将测试一个人得到的阳光越少,离赤道越远,是否会显示出与自杀率的相关性。然而,如圭亚那的情况所示,一些异常值可能不属于任何已发现趋势的一般解释。
线性回归检验
使用线性回归,可以测试两个变量之间的拟议相关性。在前一节中,我们得到了一个关于每个国家自杀率的数据集。我们有一个 Pandas 数据框架,有三栏:国名、男女自杀率和男女*均水*。为了检验自杀率取决于国家获得多少阳光的假设,我们将使用国家坐标质心,即每个国家的纬度(和经度)。我们假设每个国家的日照量与纬度成正比。获得世界上每个国家的质心比人们想象的要困难。这方面的一些资源如下:
- 一个简单的国家质心可以在高托斯的网页上找到:http://gothos.info/2009/02/centroids-for-countries/
- 一个更复杂的表格,在分析之前需要更多的处理,在开放地理编码:http://www.opengeocode.org/download.php#cow
- OpenGeocode 拥有免费的公共领域地理位置数据库
我们使用的是 Gothos 版本,所以您应该确保您有数据文件(CSV 格式):
coords=pandas.read_csv('data/country_centroids/
country_centroids_primary.csv', sep='\t')
coords.keys()

有很多列名可以使用!首先,我们看一眼桌子:
coords.head()

有趣的栏目是SHORT_NAME和LAT。我们现在将coords数据框中的SHORT_NAME与rates数据框中的Country进行匹配,并在国家名称匹配时存储Lat和Lon值。理论上,世卫组织表格最好也有国际标准化组织国家代码,这是由国际标准化组织 ( 国际标准化组织)制定的标准:
rates['Lat'] = ''
rates['Lon'] = ''
for i in coords.index:
ind = rates.Country.isin([coords.SHORT_NAME[i]])
val = coords.loc[i, ['LAT', 'LONG']].values.astype('float')
rates.loc[ind,['Lat','Lon']] = list(val)
这里,我们在coords数据框中循环索引,rates.Country.isin([coords.SHORT_NAME[i]])找到我们从 rates 对象中的 coords 对象获取的国家。因此,我们在我们找到的国家的行列中。然后,我们获取在coords对象中找到的LAT和LONG值,并将其放入Lat和Lon列中的费率对象中。为了检查是否一切正常,我们打印出前几行:
rates.head()

一些值仍然是空的,Pandas、matplotlib 和许多其他模块不能很好地处理空值。所以我们找到它们,并将空值设置为 NaN(不是数字)。这些是空的,因为我们没有Country质心或者名称不匹配:
rates.loc[rates.Lat.isin(['']), ['Lat']] = np.nan
rates.loc[rates.Lon.isin(['']), ['Lon']] = np.nan
rates[['Lat', 'Lon']] = rates[['Lat', 'Lon']].astype('float')
同时,我们将值转换为浮点数(代码的最后一行)而不是字符串;这使得绘制和执行其他例程变得更加容易。否则,例程必须转换为 float,并且可能会遇到我们必须解决的问题。手动转换它可以确保我们知道自己拥有什么。在我们的简单*似中,阳光的量与离赤道的距离成正比,但是我们有纬度。因此,我们创建一个新的列,并计算出到赤道 ( DFE )的距离,这只是纬度的绝对值:
rates['DFE'] = ''
rates['DFE'] = abs(rates.Lat)
rates['DFE'] = rates['DFE'].astype('float')
此外,距离赤道+/-23.5 度范围内的国家全年都获得等量的阳光,因此根据我们的假设,它们应该被认为具有相同的自杀率。为了首先说明我们的新数据集,我们绘制了速率与 DFE 的关系图:
import matplotlib.patches as patches
import matplotlib.transforms as transforms
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(rates.DFE, rates.Both, '.')
trans = transforms.blended_transform_factory(
ax.transData, ax.transAxes)
rect = patches.Rectangle((0,0), width=23.5, height=1,
transform=trans, color='yellow', alpha=0.5)
ax.add_patch(rect)
ax.set_xlabel('DFE')
ax.set_ylabel('Both');

首先,我们绘制速率与密度函数的关系图,然后我们添加一个矩形,对其坐标进行混合变换。通过混合变换,我们可以定义x坐标跟随数据坐标,定义y坐标跟随轴(0 为下边缘,1 为上边缘)。DFE 等于或小于 23.5 度的区域用黄色矩形标记。似乎有某种趋势倾向于更高的 DFE 和更高的自杀率。虽然对于居住在离赤道更远的人来说,这是一个悲惨的前景,但它支持了我们的假设。
为了检查我们是否在 DFE 上大致均匀地采样,我们在 DFE 上画了一个直方图。统一的覆盖范围确保我们有较低的样本偏差风险:
rates.DFE.hist(bins=13)
plt.xlabel('DFE')
plt.ylabel('Counts');

DFE > 50 度的国家较少;然而,似乎仍然有足够的对比和回归。目测一下数值,似乎我们拥有的 DFE > 50 的国家和 DFE < 50 的国家一样多。现阶段的一种可能性是绑定数据。基本上,这是取直方图仓和该仓内所有速率的*均值,让仓的中心代表位置和*均值,即值。为了绑定数据,我们使用 Pandas 中的groupby方法和 NumPy 的数字化功能:
bins = np.arange(23.5, 65+1,10, dtype='float')
groups_rates = rates.groupby(np.digitize(rates.DFE, bins))
数字化查找每个库的输入数组的索引。groupby方法然后获取索引列表,从输入数组中获取这些位置,并将它们放在一个单独的数据组中。现在,我们准备绘制未绑定和绑定的数据:
import matplotlib.patches as patches
import matplotlib.transforms as transforms
fig = plt.figure()
ax = fig.add_subplot(111)
ax.errorbar(groups_rates.mean().DFE,
groups_rates.mean().Both,
yerr=np.array(groups_rates.std().Both),
marker='.',
ls='None',
lw=1.5,
color='g',
ms=1)
ax.plot(rates.DFE, rates.Both, '.', color='SteelBlue', ms=6)
trans = transforms.blended_transform_factory(
ax.transData, ax.transAxes)
rect = patches.Rectangle((0,0), width=23.5, height=1,
transform=trans, color='yellow', alpha=0.5)
ax.add_patch(rect)
ax.set_xlabel('DFE')
ax.set_ylabel('Both');

我们现在进行线性回归,检验我们的假设,即较少的阳光意味着较高的自杀率。和以前一样,我们使用 SciPy 中的linregress:
From scipy.stats import linregress
mindfe = 30
selection = ~rates.DFE.isnull() * rates.DFE>mindfe
rv = rates[selection].as_matrix(columns=['DFE','Both'])
a, b, r, p, stderr = linregress(rv.T)
print('slope:{0:.4f}\nintercept:{1:.4f}\nrvalue:{2:.4f}\npvalue:{3:.4f}\nstderr:{4:.4f}'.format(a, b, r, p, stderr))

这里引入mindfe参数只是为了拟合一条 DFE 高于此的直线;你可以用这个值做实验。从逻辑上讲,我们将从DFE是23.5的地方开始;对于不同的值,您会得到稍微不同的结果。在我们的例子中,我们使用 30 度。如果你愿意,你可以用linregress的输出绘制结果,就像上一章一样。
作为linregress的替代拟合方法,我们可以使用强大的statsmodels包。默认情况下,它安装在 Anaconda 3 Python 发行版中。statsmodels包有一个简单的方法来输入变量之间的假设关系;与 R 型公式相同,从 0.5.0 版本开始包含在statsmodels中。我们想测试rates.DFE和rates.Both之间的线性关系,所以我们用DFE ~ Both把这个告诉statsmodels。我们只是给出了数据框的键/列名之间的关系。
注
公式框架使得表达你想要适合的关系变得非常简单明了。除了Y ~ X + Z等关系,还可以在公式中加入函数,如Y ~ X + np.log10(Z),考察更复杂的关系。
该函数采用普通最小二乘 ( OLS )方法拟合,基本上使拟合与数据之差的*方最小(也称为loss函数):
import statsmodels.formula.api as smf
mod = smf.ols("DFE ~ Both", rates[selection]).fit()
print(mod.summary())

第一部分,即顶部表格的左侧给出了一般信息。因变量( Dep。变量)表示拟合变量的名称。模型说明我们在拟合中使用了什么模型;除了 OLS,还有其他几款车型如加权最小二乘 ( WLS )。列出观测值的数量(号观测值)和残差的自由度( Df 残差,即观测值的数量(59)减去通过拟合 2 确定的参数( k )和 k )。 Df 模型显示确定了多少参数(除了常数,即截距)。顶部表格右侧的表格显示了模型与数据吻合程度的信息。r *方以前被覆盖过;这里还列出了调整后的 R *方值( Adj. R *方),这是针对数据点数量和自由度校正后的 R *方值。 F 统计数字给你一个拟合有多重要的估计。实际上,它是模型的均方误差除以残差的均方误差。下一个值,Prob(F-统计量),给出了如果零假设为真,即变量不相关,得到 F-统计量的概率。之后是三组log-likelihood函数值:拟合的对数似然值、阿卡克信息准则 ( AIC )和贝叶斯信息准则 ( BIC )。AIC 和 BIC 是调整对数似然函数的多种方法,用于观察数和模型类型。
在此之后,有一个确定参数的表格,其中,对于每个参数,显示估计值(coeff)、估计值的标准误差(std err)、t 统计值(t)、P 值(P > |t|)和 95%置信区间。低于固定置信水*的 P 值,这里为 0.05(即 5%),表明data和model参数之间存在统计上显著的关系。
最后一部分显示了与拟合残差分布相关的几个统计测试的结果。关于这些的信息可以在文献和 statsmodels 文档(http://statsmodels.sourceforge.net/)中找到。一般来说,误差(残差)形状(偏斜度)的前几个测试:偏斜度、峰度、综合和 Jarque-Bera。剩下的测试误差是否独立(即自相关)——杜宾-沃森——或者拟合参数如何相互关联(对于多元回归)——T2 条件数 ( 条件数。否)。
由此,我们现在可以说,每 100,000 名居民中,每增加 30 度以上的绝对纬度,自杀率大约增加 0.32+/-0.07。与绝对纬度(DFE)的相关性较弱。很低的 Prob(F-statistics)值表明我们可以以相当高的确定性拒绝两个变量不相关的零假设,低的 P > |t| 值表明 DFE 与自杀率之间存在关系。
我们现在可以用数据绘制拟合曲线。为了获得图中绘制的拟合的不确定性,我们使用内置的wls_prediction_std函数,该函数计算具有 1 个标准偏差不确定性的下限和上限。这里的 WLS 代表加权最小二乘法,一种类似于 OLS 的方法,但是输入变量的不确定性是已知的并且被考虑在内。这是 OLS 更普遍的情况;为了计算不确定性的界限,它是相同的:
from statsmodels.sandbox.regression.predstd import wls_prediction_std
prstd, iv_l, iv_u = wls_prediction_std(mod)
fig = plt.figure()
ax = fig.add_subplot(111)
rates.plot(kind='scatter', x='DFE', y='Both', ax=ax)
xmin, xmax = min(rates['DFE']), max(rates['DFE'])
ax.plot([mindfe, xmax],
[mod.fittedvalues.min(), mod.fittedvalues.max()],
'IndianRed', lw=1.5)
ax.plot([mindfe, xmax], [iv_u.min(), iv_u.max()], 'r--', lw=1.5)
ax.plot([mindfe, xmax], [iv_l.min(), iv_l.max()], 'r--', lw=1.5)
ax.errorbar(groups_rates.mean().DFE,
groups_rates.mean().Both,
yerr=np.array(groups_rates.std().Both),
ls='None',
lw=1.5,
color='Green')
trans = transforms.blended_transform_factory(
ax.transData, ax.transAxes)
rect = patches.Rectangle((0,0), width=mindfe, height=1,
transform=trans, color='Yellow',
alpha=0.5)
ax.add_patch(rect)
ax.grid(lw=1, ls='dashed')
ax.set_xlim((xmin,xmax+3));

有相当多的研究显示了自杀率和纬度的相关性(例如, Davis GE 和 Lowell WE , Can J 精神病学。 2002 年 8 月;47(6):572-4 。纬度与自杀率变化直接相关的证据。).然而,一些研究使用较少的国家(20 个),所以他们可能会遭受选择偏差,也就是说,不小心选择了那些倾向于强相关性的国家。然而,从这些数据中有趣的是,在更高的纬度似乎有一个最低自杀率,这有利于某种关系。
这里需要记住的几件事如下:
- 围绕这一趋势有一个明显的扩散。这表明这不是世界范围内自杀率蔓延的主要原因之一。然而,它确实表明这是可能影响它的事情之一。
- 一开始,我们取坐标质心;有些国家跨越很长的纬度范围。因此,该国的利率可能会有所不同。
- 虽然我们假设与一个人每年接收的阳光量直接相关的是与纬度成正比的,天气当然也在我们看到和获得的阳光量中起作用。
- 很难解释性别数据;也许女人和男人一样尝试自杀,但失败的次数更多,然后得到适当的帮助。这将使数据产生偏差,因此我们认为男性更倾向于自杀。
从长远来看,暴露在阳光下的时间会影响体内维生素 D 的产生。许多研究试图弄清楚所有这些因素是如何影响人体的。这种复杂性的一个迹象来自于研究表明,越靠*赤道,自杀率的季节性变化越大(坎特,希基和德利奥,精神病理学 2000;33:303-306),表明日光照射的增加和突然变化会增加自杀风险。人体对日晒变化的反应是释放或抑制各种激素(褪黑素、血清素、L-色氨酸等)的释放;荷尔蒙水*的突然变化似乎增加了自杀的风险。
对自杀率的另一个潜在影响是经济指标。因此,我们现在将尝试通过多元回归来看看这三个变量之间是否存在相关性。
多元回归
在本节中,我们将向之前构建的线性模型添加第二个变量。我们现在用来表示相关性的函数基本上变成了一个*面,y = k2x2+k1x1+k0。请记住 x 1 和 x 2 是不同的轴/尺寸。澄清一点,我们也可以写成z = k2y+k1x+k0。正如本章开头所述,我们可以把它写成矩阵乘法。我们选择纳入的变量是一个经济变量,即国内生产总值 ( GDP )。正如之前假设的那样,该国的经济可能会影响自杀率,因为预防自杀需要一个发达的医疗系统来隔离有需要的人并提供帮助,这是昂贵的。
增加经济指标
幸运的是,在这种情况下,Pandas 有一个内置的远程数据模块,可以用来获取某些指标(http://Pandas . py data . org/Pandas-docs/stable/remote _ data . html)。目前,可以从 Pandas 直接查询的服务如下:
- 雅虎!金融
- 谷歌金融
- 圣路易美联储(弗雷德)
- 肯尼斯·弗伦奇的数据库
- 世界银行
- 谷歌分析
要向世界银行查询人均国内生产总值指标,我们只需搜索一下:
from pandas.io import wb
wb.search('gdp.*capita.*').iloc[:,:2]

注
在 Pandas 即将推出的版本中,pandas.io.data模块将是一个名为pandas-datareader的独立包。如果您在更新时碰巧看到此内容,请安装pandas-datareader ( conda install pandas-datareader)并用from pandas_datareader import wb替换进口from pandas.io import wb。
我们要找的指标是人均国内生产总值(目前为美元)。现在我们可以非常简单的方式下载这个数据集,直接询问它的ID、NY.GDP.PCAP.PP.CD:
dat = wb.download(indicator='NY.GDP.PCAP.PP.CD', country='all', start=2014, end=2014)
dat.head()

数据结构有点复杂,所以我们需要让它对我们来说更容易访问。我们通过将数据放入数组并创建一个名为数据的新 Pandas 数据帧来实现这一点:
country = np.array(dat.index.tolist())[:,0]
gdp = np.array(np.array(dat['NY.GDP.PCAP.PP.CD']))
data = pd.DataFrame(data=np.array([country,gdp]).T, columns=['country', 'gdp'])
print(dat['NY.GDP.PCAP.PP.CD'].head())
print(data.head())

数据现在对我们来说更容易访问,并且格式与以前的数据集相同。就像坐标质心一样,我们需要匹配国家名称,并将相关数据放入 rates 对象中。我们以与之前完全相同的方式进行:
rates['GDP_CD'] = ''
for i in np.arange(len(data)):
ind = rates.Country.isin([data.country[i]
val = data.loc[i, ['gdp']].values.astype('float')
rates.loc[ind]), ['GDP_CD'] ] = val
rates.loc[rates.GDP_CD.isin(['']), ['GDP_CD']] = np.nan
为了检查是否一切正常,我们可以打印其中一个项目。在这个例子中,我们看看瑞典:
print(rates[rates.Country=='Sweden'])
print(data[data.country=='Sweden'])
print(data.loc[218, ['gdp']].values.astype('float'))
rates.loc[rates.Country.isin(['Sweden'])]

看起来我们做对了。当我们使用 DFE 时,我们定义了一个mindfe变量。我们在这里使用它来选择那些满足高于我们之前设置的最小纬度的国家,并且也应该有 DFE 的值。然后我们将有国内生产总值的行添加到选择中:
selection = ~rates.DFE.isnull() * rates.DFE>mindfe
selection *= ~rates.GDP_CD.isnull()
首先检查国内生产总值和自杀率之间是否有明显的相关性:
plt.plot(rates[selection].GDP_CD.values,rates[selection].Both.values, '.', ms=10)
plt.xlabel('GDP')
plt.ylabel('Suicide rate');

这似乎是一种非常广泛的关系。我们添加了 DFE 变量,并将标记的大小绘制为自杀率:
plt.scatter(rates[selection].GDP_CD.values/1000,
rates[selection].DFE.values, s=rates[selection].Both.values**1.5)
plt.xlabel('GDP/1000')
plt.ylabel('DFE')

似乎高 GDP 国家的 DFE 高,但自杀率也高。现在,当然,我们想用一个线性模型来拟合。基本上,模型将是一个适合这些点的*面。为此,我们可以再次使用statsmodels,对于绘图,我们导入 matplotlib 3D 轴:
import statsmodels.api as sm
A = rates[selection][['DFE', 'GDP_CD']].astype('float')
A['GDP_CD'] = A['GDP_CD']/1000
b = rates[selection]['Both'].astype('float')
A = sm.add_constant(A)
est = sm.OLS(b, A).fit()
首先,我们选择拟合所需的数据,DFE和GDP_CD用于A矩阵。然后,我们运行拟合,假设自杀率依赖于A(即 DFE 和 GDP)。为了让它工作,我们必须添加一个常量值的列(1);statsmodels开发者提供了这样一个我们可以使用的功能。注意,我们展示了如何在statsmodels中使用另一种定义拟合函数的方式,这种方式不是 R 公式方法,而是使用 NumPy 数组。被拟合的线性函数正是我们在本节开头所讨论的,它是下面的关系:

这里,我们用矩阵乘法表示变量之间的关系(即通过拟合例程找到 k 0 、 k 1 、 k 2 )。注意这里的不同导入;该方法被称为 OLS (大写字母),而不是 ols ,后者与上例中的公式一起使用。我们现在可以用数据绘制拟合曲线:
from mpl_toolkits.mplot3d import Axes3D
X, Y = np.meshgrid(np.linspace(A.DFE.min(), A.DFE.max(), 100), np.linspace(A.GDP_CD.min(), A.GDP_CD.max(), 100))
Z = est.params[0] + est.params[1] * X + est.params[2] * Y
fig = plt.figure(figsize=(12, 8))
ax = Axes3D(fig, azim=-135, elev=15)
surf = ax.plot_surface(X, Y, Z, cmap=plt.cm.RdBu, alpha=0.6, linewidth=0)
ax.scatter(A.DFE, A.GDP_CD, y, alpha=1.0)
ax.set_xlabel('DFE')
ax.set_ylabel('GDP_CD/1000')
ax.set_zlabel('Both');

在 Axes3D 对象创建过程中,可以使用azim和evel关键字控制查看方向。为了绘制*面,我们使用 NumPy 中的meshgrid函数来创建坐标网格,然后使用拟合例程中确定的参数来获得*面的值。再一次,为了得到合适的摘要,我们打印它:
print(est.summary())

得到的 r *方类似于仅使用 DFE 时得到的 r *方。这个假设并非完全错误;然而,情况比这两个变量复杂得多。这反映在拟合结果中。国内生产总值的系数很小,非常接*于零(-6.238 × 10 -5 ,也就是说,对国内生产总值的依赖没有纬度那么明显。那么>| t |的价值观呢——你能从中得出什么结论?此外,由于条件数较高,还会出现多重共线性警告。因此,一些拟合变量可能是相互依赖的。
后退一步
记住我们把 DFE 值低的东西都剪掉;让我们检查完整数据集的外观。在这个剧情中,你也看到我们使用了另一个魔法命令(以%开头);我们使用 notbook 绘图界面,而不是 matplotlib 内联。这为您提供了直接在 Jupyter 笔记本中*移、缩放和保存数据的交互式控件:
%matplotlib notebook
selection2 = ~rates.DFE.isnull()
plt.scatter(rates[selection2].GDP_CD.values/1000, rates[selection2].DFE.values, s=rates[selection2].Both.values**1.5)
plt.xlabel('GDP/1000')
plt.ylabel('DFE');

根据自杀率引入标记的大小,我们可以看到数据中似乎至少有两个自杀率较高的主要集群——一个是国内生产总值和绝对纬度较低的集群,一个是自杀率较高的集群。我们将在下一章中使用它来继续我们的分析,并尝试识别集群。因此,我们应该保存这些数据,但是我们首先创建一个只包含所需列的新数据框:
data=pd.DataFrame(data=rates[['Country','Both','Male','Female','GDP_CD', 'DFE']][~rates.DFE.isnull()])
data.head()

我们只包括有 DFE 的数据;尽管如此,一些行仍然缺乏国内生产总值。现在我们已经创建了一个新的数据框,保存它非常容易。这一次,我们以更标准化的格式保存它,即 HDF 格式:
TABLE_FILE = 'data_ch4.h5'
data.to_hdf(TABLE_FILE, 'ch4data', mode='w', table=True)
该代码将数据保存到当前目录的data_ch4.h5文件中。
注
HDF 代表分层数据格式;它是由国家超级计算应用中心 ( NCSA )开发的科学数据格式,专门针对大型数据集。它在从/向磁盘读写大数据方面非常快。大多数主要的编程语言都有与 HDF 文件交互的库。当前的旧版格式版本是 HDF 版本 5。
要读取 HDF 数据,我们只需使用 Pandas 的read_hdf功能:
d2 = pd.read_hdf(TABLE_FILE)
您现在可以运行d2.head()进行健全性检查,是否与我们写入文件的内容相同。记住在下一章把这个文件放在手边。
逻辑回归
迄今为止的例子都是连续变量。但是,其他变量是离散的,可以是二进制类型。离散二进制变量的一些常见例子是某个城市在某一天是否下雪,病人是否携带病毒,等等。二元逻辑回归和线性回归的主要区别之一是,在二元逻辑回归中,我们在给定测量(离散或连续)变量的情况下拟合结果的概率,而线性回归模型处理两个或多个连续变量相互依赖的特征。逻辑回归给出了给定一些观察变量的发生概率。概率有时表示为 P(Y|X) ,读作给定变量 X 值为 Y 的概率。
猜测离散结果的算法被称为分类算法,是机器学习技术的一部分,这将在本书后面介绍。
逻辑回归模型可以表示如下:

求解 P 的这个方程,我们得到逻辑概率:



就像线性回归一样,我们可以给问题增加几个维度(因变量):

为了说明这个函数是什么样子,以及与拟合线性模型的区别,我们绘制了两个函数:
k = 1\.
m = -5\.
y = lambda x: k*x + m
#p = lambda x: np.exp(k*x+m) / (1+np.exp(k*x+m))
p = lambda x: 1 / (1+np.exp(-1*(k*x+m)))
xx = np.linspace(0,10)
plt.plot(xx,y(xx), label='linear')
plt.plot(xx,p(xx), label='logistic')
plt.plot([0,abs(m)], [0.5,0.5], dashes=(4,4), color='.7')
plt.plot([abs(m),abs(m)], [-.1,.5], dashes=(4,4), color='.7')
# limits, legends and labels
plt.ylim((-.1,1.1))
plt.legend(loc=2)
plt.ylabel('P')
plt.xlabel('xx')

可以清楚地看到,S 形曲线,我们的逻辑拟合函数(更一般地称为 sigmoid 函数,可以更好地说明二元逻辑概率。玩转k、m,我们很快意识到k决定坡度的陡度,m左右移动曲线。我们还注意到 P(Y|xx=5) = 0.5 ,即在 xx=5 时,结果 Y(对应P=1)有 50%的概率。
想象一下,我们问学生他们为了考试学习了多长时间。我们能查一下你需要学习多长时间才能相当肯定地通过吗?为了研究这个,我们需要使用逻辑回归。考试只有通过或失败的可能,也就是说,它是一个二进制变量。首先,我们需要创建数据:
studytime=[0,0,1.5,2,2.5,3,3.5,4,4,4,5.5,6,6.5,7,7,8.5,9,9,9,10.5,10.5,12,12,12,12.5,13,14,15,16,18]
passed=[0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,1]
data = pd.DataFrame(data=np.array([studytime, passed]).T, columns=['Time',
'Pass'])
data.Time.hist(bins=6)
plt.xlabel('Time')
plt.ylabel('No. students');

绘制的第一件事是学生花了多少时间准备考试的柱状图。这似乎是一个相当*坦的分布。在这里,我们将检查他们的考试成绩:
plt.plot(data.Time, data.Pass,'o', mew=0, ms=7,)
plt.ylim(-.1,1.1)
plt.xlim(-0.2,16.2)
plt.xlabel('Time studied')
plt.ylabel('Pass? (0=no, 1=yes)');
这个图现在将显示某人学习了多少时间以及考试的结果——如果他们通过了,给出的值是1.0 ( yes)或者如果他们失败了,给出的值是0.0 ( no)。 x 轴从 0 小时(一个完全不学习的学生)到 18 小时(一个学习更多的学生)。

通过简单地检查数字,似乎至少需要 5-10 个小时才能通过考试。我们再次使用 statsmodels 将数据与我们的模型进行拟合。在 statsmodels 中,有一个执行逻辑回归的logit函数:
import statsmodels.api as sm
probfit = sm.Logit(data.Pass, sm.add_constant(data.Time, prepend=True))

优化成功了;如果出现任何问题,就会打印错误信息。运行拟合后,检查摘要是一个好主意:
fit_results = probfit.fit()
print(fit_results.summary())

const变量是拟合函数的截距,即k,时间是截距,m。协方差参数可用于通过取协方差矩阵对角线的*方根来估计标准差:
logit_pars = fit_results.params
intercept_err, slope_err = np.diag(fit_results.cov_params())**.5
fit_results.cov_params()

然而,statsmodels 也给出了参数的不确定性,这可以从拟合总结输出中看出:
intercept = logit_pars['const']
slope = logit_pars['Time']
print(intercept,slope)
-5.79798670884 0.801979232718

也可以直接打印出置信区间:
fit_results.conf_int()

现在,在数据的顶部绘制拟合曲线是合适的。我们已经估计了拟合函数的参数:
plt.plot(data.Time, data.Pass,'o', mew=0, ms=7, label='Data')
p = lambda x,k,m: 1 / (1+np.exp(-1*(k*x+m)))
xx = np.linspace(0,data.Time.max())
l1 = plt.plot(xx, p(xx,slope,intercept), label='Fit')
plt.fill_between(xx, p(xx,slope+slope_err**2, intercept+intercept_err), p(xx,slope-slope_err**2, intercept-intercept_err), alpha=0.15, color=l1[0].get_color())
plt.ylim(-.1,1.1)
plt.xlim(-0.2,16.2)
plt.xlabel('Time studied')
plt.ylabel('Pass? (0=no, 1=yes)')
plt.legend(loc=2, numpoints=1);

这里,我们不仅绘制了最佳拟合曲线,还绘制了一个标准差对应的曲线。不确定性包含许多价值。现在,通过估计的参数,可以计算出我们应该学习多长时间才能获得 50%的成功机会:
target=0.5
x_prob = lambda p,k,m: (np.log(p/(1-p))-m)/k
T_max = x_prob(target, slope-slope_err, intercept-intercept_err)
T_min = x_prob(target, slope+slope_err, intercept+intercept_err)
T_best = x_prob(target, slope, intercept)
print('{0}% sucess rate: {1:.1f} +{2:.1f}/- {3:.1f}'.format(int(target*100),T_best,T_max-T_best,T_best-T_min))
50% success rate: 7.2 +8.7/-4.0
所以这次考试学习 7.2 小时,通过的几率大概是 50%。不确定性相当大,50%的通过几率也可能是 15 个小时左右的学习,或者少到 3 个小时。当然,为考试而学习不仅仅是投入的绝对小时数。然而,由于根本不学习,通过的机会非常渺茫。
一些注意事项
逻辑回归假设拐点处,即 S 曲线的中途,概率为 0.5。没有真正的理由假设这总是真的;因此,使用允许拐点移动的模型可能是更普遍的情况。然而,这将增加另一个参数来估计,并且,考虑到输入数据的质量,这可能不会使其更容易或增加可靠性。
总结
在本章中,我们研究了线性回归、多元回归和逻辑回归。我们从网上获取数据,清理数据,并将其映射到我们感兴趣的数据结构。统计学的世界是巨大的,甚至对于这些有些简单的概念和方法也有许多特殊的领域。对于回归分析,需要注意的是,相关性并不总是意味着因果关系,也就是说,仅仅因为两个变量之间存在相关性,并不意味着它们在本质上相互依赖。有些网站显示了这些虚假的相关性;其中一些相当有趣(http://www.tylervigen.com/spurious-correlations)。
在下一章中,我们将研究聚类技术来发现数据中的相似之处。我们将从一个示例开始,该示例使用了我们在本章中执行多元回归分析时保存的相同数据。
五、聚类
对于由几个独立分布组成的数据,我们如何发现和表征它们?在本章中,我们将研究一些识别数据中聚类的方法。具有相似特征的点组形成簇。有许多不同的算法和方法来实现这一点,有好有坏。我们希望检测数据中的多个独立分布,并确定每个点与另一个点或聚类的关联度(或相似度)。如果它们属于一个集群,则关联度需要高;如果它们不属于一个集群,则关联度需要低。当然,就像以前一样,这可以是一维问题或多维问题。聚类发现的一个固有困难是确定数据中有多少个聚类。对此有各种不同的定义方法;有些情况下,用户需要输入聚类的数量,然后算法找到哪些点属于哪个聚类,有些情况下,开始的假设是每个点都是一个聚类,然后在试验的基础上迭代组合两个附*的聚类,看看它们是否属于一起。
在本章中,我们将涵盖以下主题:
- 集群发现的简短介绍—提醒您一般问题—以及解决它的算法
- Analysis of a dataset in the context of cluster finding-the Cholera outbreak in central London 1854
- 通过简单的零阶分析,计算整个数据集的质心
- 通过为每个记录的霍乱相关死亡找到最*的水泵
- 使用 K-均值最*邻算法进行聚类发现,将其应用于来自 第 4 章**回归的数据,并识别两个独立的分布
- 通过分析宇宙有限区域中星系的分布来使用层次聚类
这里涉及的算法和方法集中在 SciPy 中可用的算法和方法上。
像以前一样,启动一个新的笔记本,并放入默认导入。也许你想换成交互式笔记本,尝试一下。对于本章,我们将添加以下特定导入。与聚类相关的来自 SciPy,而稍后,我们将需要一些包来转换天文坐标。这些包都预装在 Anaconda Python 3 发行版中,并在那里进行了测试:
import scipy.cluster.hierarchy as hac
import scipy.cluster.vq as vq
聚类发现介绍
有许多不同的聚类识别算法。他们中的许多人试图以最好的方式解决特定的问题。因此,您想要使用的特定算法可能取决于您试图解决的问题,也取决于您正在使用的特定包中有哪些可用的算法。
一些最初的聚类算法包括简单地找到质心位置,使到每个聚类中所有点的距离最小。每个簇中的点比其他簇的质心更靠*该质心。在这一点上可能很明显,最困难的部分是计算出有多少个集群。如果我们可以确定这一点,那么尝试各种移动簇形心的方法,计算到每个点的距离,然后找出簇形心在哪里,这是相当简单的。也有明显的情况,这可能不是最好的解决方案,例如,如果你有两个非常长的集群彼此相邻。
通常,该距离是欧几里得距离:

这里, p 是一个包含所有点位置的向量,也就是簇 C k 中的{p1,p2,...,pN-1,pN},距离是从簇质心计算出来的, i 。我们必须找到使点到点的绝对距离之和最小的聚类质心:

在第一个例子中,我们将首先处理固定的簇形心。
从简单开始——约翰·斯诺研究霍乱
1854 年,伦敦西北部布罗德街附*爆发了霍乱。当时的主要理论声称霍乱通过污浊的空气传播,就像人们认为瘟疫是通过污浊的空气传播一样。当时的医生约翰·斯诺假设霍乱是通过饮用水传播的。在疫情期间,约翰追踪了死亡人数,并将其绘制在该地区的地图上。通过他的分析,他得出结论,大多数案例都集中在布罗德街水泵上。有传言称,他随后拆除了水泵的手柄,从而阻止了一场疫情。今天,我们知道霍乱通常通过被污染的食物或水传播,从而证实了约翰的假设。我们将对约翰·斯诺的数据做一个简短但有启发性的再分析。
数据来源于国家地理信息与分析中心公共数据档案(http://www.ncgia.ucsb.edu/http://www.ncgia.ucsb.edu/pubs/data.php)。数据文件的清理图和副本以及数据的地理空间信息分析示例也可以在https://www.udel.edu/johnmack/frec682/cholera/cholera2.html找到。关于医生和科学家约翰·斯诺的生活和工作的大量信息可以在http://johnsnow.matrix.msu.edu找到。
为了开始分析,我们把数据读入 Pandas 数据框;这些数据已经被格式化成了 Pandas 可读的 CSV 文件:
deaths = pd.read_csv('data/cholera_deaths.txt')
pumps = pd.read_csv('data/cholera_pumps.txt')
每个文件包含两列,一列为X坐标,一列为Y坐标。让我们看看它是什么样子的:
deaths.head()

pumps.head()

有了这些信息,我们现在可以绘制所有泵和死亡的图表,以便可视化数据:
plt.figure(figsize=(4,3.5))
plt.plot(deaths['X'], deaths['Y'],
marker='o', lw=0, mew=1, mec='0.9', ms=6)
plt.plot(pumps['X'],pumps['Y'],
marker='s', lw=0, mew=1, mec='0.9', color='k', ms=6)
plt.axis('equal')
plt.xlim((4.0,22.0));
plt.xlabel('X-coordinate')
plt.ylabel('Y-coordinate')
plt.title('John Snow's Cholera')

很容易看出中间的泵很重要。作为第一次数据探索,我们将简单地计算分布的*均质心,并在图中将其绘制为椭圆。我们计算沿 x 和 y 轴的*均值和标准偏差作为质心位置:
fig = plt.figure(figsize=(4,3.5))
ax = fig.add_subplot(111)
plt.plot(deaths['X'], deaths['Y'],
marker='o', lw=0, mew=1, mec='0.9', ms=6)
plt.plot(pumps['X'],pumps['Y'],
marker='s', lw=0, mew=1, mec='0.9', color='k', ms=6)
from matplotlib.patches import Ellipse
ellipse = Ellipse(xy=(deaths['X'].mean(), deaths['Y'].mean()),
width=deaths['X'].std(), height=deaths['Y'].std(),
zorder=32, fc='None', ec='IndianRed', lw=2)
ax.add_artist(ellipse)
plt.plot(deaths['X'].mean(), deaths['Y'].mean(),
'.', ms=10, mec='IndianRed', zorder=32)
for i in pumps.index:
plt.annotate(s='{0}'.format(i), xy=(pumps[['X','Y']].loc[i]),
xytext=(-15,6), textcoords='offset points')
plt.axis('equal')
plt.xlim((4.0,22.5))
plt.xlabel('X-coordinate')
plt.ylabel('Y-coordinate')
plt.title('John Snow's Cholera')

这里,我们还绘制了泵指数,它可以通过pumps.index方法从数据框中获得。分析的下一步是看哪个泵最接*每个点。我们通过计算从所有泵到所有点的距离来做到这一点。然后,我们想弄清楚每个点,哪个泵最接*。
我们将距离每个点最*的泵保存在死亡数据框的单独一列中。有了这个数据集,for 循环运行得相当快。但是,与sum()和idxmin()方法链接的 DataFrame 减法需要几秒钟。我强烈建议你用各种方法来加速这个过程。我们还使用数据框的.apply()方法对值进行*方和*方根。这种简单的暴力第一次尝试花了一分多钟来运行。内置的函数和方法帮助很大:
deaths_tmp = deaths[['X','Y']].as_matrix()
idx_arr = np.array([], dtype='int')
for i in range(len(deaths)):
idx_arr = np.append(idx_arr,
(pumps.subtract(deaths_tmp[i])).apply(lambda
x:x**2).sum(axis=1).apply(lambda x:x**0.5).idxmin())
deaths['C'] = idx_arr
打印出表格的前几行,快速检查是否一切正常:
deaths.head()

现在我们想想象我们所拥有的。通过颜色,我们可以显示出我们把每一次死亡与哪个水泵联系在一起。为了做到这一点,我们使用彩色地图;在这种情况下,喷射色图。通过用 0 到 1 之间的值调用 colormap,它返回一个颜色;因此,我们给它泵索引,然后用泵的总数除以它-在我们的例子中是 12:
fig = plt.figure(figsize=(4,3.5))
ax = fig.add_subplot(111)
np.unique(deaths['C'].values)
plt.scatter(deaths['X'].as_matrix(), deaths['Y'].as_matrix(),
color=plt.cm.jet(deaths['C']/12.),
marker='o', lw=0.5, edgecolors='0.5', s=20)
plt.plot(pumps['X'],pumps['Y'],
marker='s', lw=0, mew=1, mec='0.9', color='0.3', ms=6)
for i in pumps.index:
plt.annotate(s='{0}'.format(i), xy=(pumps[['X','Y']].loc[i]),
xytext=(-15,6), textcoords='offset points',
ha='right')
ellipse = Ellipse(xy=(deaths['X'].mean(), deaths['Y'].mean()),
width=deaths['X'].std(),
height=deaths['Y'].std(),
zorder=32, fc='None', ec='IndianRed', lw=2)
ax.add_artist(ellipse)
plt.axis('equal')
plt.xlim((4.0,22.5))
plt.xlabel('X-coordinate')
plt.ylabel('Y-coordinate')
plt.title('John Snow's Cholera')

大多数死亡病例主要是因为水泵靠*中心。这个水泵位于布罗德大街。
现在,记住我们已经使用了集群质心的固定位置。在这种情况下,我们基本上假设水泵与霍乱病例有关。此外,欧几里得距离并不是真实的距离。人们沿着道路去取水,那里的路不一定是直的。因此,人们必须绘制出街道图,并计算出从那里到每个水泵的距离。即便如此,已经到了这个程度,很明显中心泵与霍乱病例有关。你如何解释不同的距离?为了计算距离,你可以做所谓的成本分析(c.f .当你点击卫星导航上的方向去一个地方)。做成本分析有很多不同的方法,它也涉及到在迷宫中找到正确方法的问题。
除了这些事情之外,我们没有任何时域数据,也就是说,随着时间的推移,霍乱可能会传播到其他水泵,疫情可能从布罗德街水泵开始,并传播到附*的其他水泵。没有时间数据,想弄清楚发生了什么就特别困难。
这是集群发现的一般方法。坐标可能是属性,例如狗的长度和重量,以及群集质心的位置,我们会反复移动,直到找到最佳位置。
K-均值聚类
K 均值算法也被称为向量量化。该算法所做的是找到使到聚类中所有点的距离最小的聚类(质心)位置。这是迭代完成的;算法的问题是它可能有点贪婪,这意味着它会很快找到最*的最小值。这通常通过某种跳盆方法来解决,其中找到的最*的最小值被随机扰动,并且算法重新开始。由于这个事实,算法依赖于良好的初始猜测作为输入。
自杀率对国内生产总值对绝对纬度
如 第四章**回归所述,我们将针对集群分析自杀率对比 GDP 对比绝对纬度或离赤道度数 ( DFE )的数据。我们从视觉检查中得出的假设是,至少有两个不同的集群,一个具有较高的自杀率、国内生产总值和绝对纬度,另一个具有较低的自杀率。我们在 第 4 章**回归中保存了一个 HDF 文件,现在我们将其作为数据帧读入。这一次,我们希望丢弃一个或多个列条目为 NaN 或空的所有行。因此,我们对此使用适当的 DataFrame 方法:
TABLE_FILE = 'data/data_ch4.h5'
d2 = pd.read_hdf(TABLE_FILE)
d2 = d2.dropna()
接下来,虽然数据帧是一种非常方便的格式,我们将在后面使用,但 SciPy 中集群算法的输入并不处理 Pandas 的数据类型。因此,我们将数据传输到 NumPy 数组:
rates = d2[['DFE','GDP_CD','Both']].as_matrix().astype('float')
接下来,概括一下,我们用一个国内生产总值直方图和一个所有数据的散点图来可视化数据。我们这样做是为了帮助我们对集群质心位置进行初步猜测:
plt.subplots(12, figsize=(8,3.5))
plt.subplot(121)
plt.hist(rates.T[1], bins=20,color='SteelBlue')
plt.xticks(rotation=45, ha='right')
plt.yscale('log')
plt.xlabel('GDP')
plt.ylabel('Counts')
plt.subplot(122)
plt.scatter(rates.T[0], rates.T[2],
s=2e5*rates.T[1]/rates.T[1].max(),
color='SteelBlue', edgecolors='0.3');
plt.xlabel('Absolute Latitude (Degrees, 'DFE')')
plt.ylabel('Suicide Rate (per 100')')
plt.subplots_adjust(wspace=0.25);

右边的散点图显示了 y 轴上的自杀率和 x 轴上的绝对纬度。每个点的大小与该国的国内生产总值成正比。运行聚类 k-means 的函数采用一种特殊的规范化输入。数据数组(列)必须通过数组的标准偏差进行规范化。虽然这很简单,但模块中包含了一个名为whiten的函数。它将使用标准差来缩放数据:
w = vq.whiten(rates)
为了展示它对数据的作用,我们再次绘制了前面的图,但是使用了whiten函数的输出:
plt.subplots(12, figsize=(8,3.5))
plt.subplot(121)
plt.hist(w[:,1], bins=20, color='SteelBlue')
plt.yscale('log')
plt.subplot(122)
plt.scatter(w.T[0], w.T[2], s=2e5*w.T[1]/w.T[1].max(),
color='SteelBlue', edgecolors='0.3')
plt.xticks(rotation=45, ha='right');

如您所见,所有数据都是根据上图进行缩放的。然而,如上所述,比例只是标准差。让我们计算缩放比例并将其保存到sc变量中:
sc = rates.std(axis=0)
现在,我们准备好估计聚类质心的初始猜测。读取第一个数据图,我们猜测质心在 20 DFE,20 万 GDP,10 次自杀,第二个在 45 DFE,10 万 GDP,15 次自杀。我们将它放在一个数组中,并用我们的缩放参数将其缩放到与whiten函数的输出相同的比例。然后发送到 SciPy 的kmeans2功能:
init_guess = np.array([[20,20E3,10],[45,100E3,15]])
init_guess /= sc
z2_cb, z2_lbl = vq.kmeans2(w, init_guess, minit='matrix',
iter=500)
还有另一个函数kmeans(没有2),它是一个不太复杂的版本,当到达局部极小值时不会停止迭代;当两次迭代之间的变化低于某个水*时,它就停止了。因此,标准的 k-means 算法在 SciPy 中由kmeans2函数表示。该函数输出质心的缩放位置(这里是z2_cb)和一个查找表(z2_lbl),告诉我们哪一行属于哪个质心。为了得到我们理解的单位的质心位置,我们简单地乘以我们的比例值:
z2_cb_sc = z2_cb * sc
此时,我们可以绘制结果。下面的部分相当长,包含了许多不同的部分,所以我们将一节一节地讨论它们。但是,代码应该在笔记本的一个单元格中运行:
# K-means clustering figure START
plt.figure(figsize=(6,4))
plt.scatter(z2_cb_sc[0,0], z2_cb_sc[0,2],
s=5e2*z2_cb_sc[0,1]/rates.T[1].max(),
marker='+', color='k',
edgecolors='k', lw=2, zorder=10, alpha=0.7);
plt.scatter(z2_cb_sc[1,0], z2_cb_sc[1,2],
s=5e2*z2_cb_sc[1,1]/rates.T[1].max(),
marker='+', color='k', edgecolors='k', lw=3,
zorder=10, alpha=0.7);
第一步相当简单;我们设置图形大小并绘制聚类质心的点。我们假设了两个集群,因此我们用对plt.scatter的两个不同调用来绘制它们。这里,z2_cb_sc[1,0]从数组中获取第二个簇x坐标(DFE),然后将 0 切换为 1,就得到y坐标(速率)。我们将标记的大小设置为与第三个数据轴(国内生产总值)的值成比例。我们还对数据做了进一步的处理,就像前面的图一样,这样更容易比较和区分聚类。zorder关键字给出了绘制元素的深度顺序;高的zorder会把它们放在其他东西上面,负的zorder会把它们送到后面。
s0 = abs(z2_lbl==0).astype('bool')
s1 = abs(z2_lbl==1).astype('bool')
pattern1 = 5*'x'
pattern2 = 4*'/'
plt.scatter(w.T[0][s0]*sc[0],
w.T[2][s0]*sc[2],
s=5e2*rates.T[1][s0]/rates.T[1].max(),
lw=1,
hatch=pattern1,
edgecolors='0.3',
color=plt.cm.Blues_r(
rates.T[1][s0]/rates.T[1].max()));
plt.scatter(rates.T[0][s1],
rates.T[2][s1],
s=5e2*rates.T[1][s1]/rates.T[1].max(),
lw=1,
hatch=pattern2,
edgecolors='0.4',
marker='s',
color=plt.cm.Reds_r(
rates.T[1][s1]/rates.T[1].max()+0.4))
在本节中,我们绘制了簇的点。首先,我们得到选择数组。它们只是布尔数组,即对应于簇 0 或簇 1 的值为真的数组。因此,当簇 id 为 0 时 s0 为真,当簇 id 为 1 时 s1 为真。接下来,我们为散点图标记定义阴影图案,稍后我们给出绘图函数作为输入。填充图案的乘数给出图案的密度。点的散点图是以类似于质心的方式创建的,只是标记稍微复杂一些。它们都是彩色编码的,就像前面霍乱死亡的例子一样,但是都是渐变的,而不是所有点都是完全相同的颜色。梯度由国内生产总值定义,国内生产总值也定义了点的大小。发送到图中的 x 和 y 数据在集群之间是不同的,但是它们最终访问相同的数据,因为我们乘以了我们的比例因子。
p1 = plt.scatter([],[], hatch='None',
s=20E3*5e2/rates.T[1].max(),
color='k', edgecolors='None',)
p2 = plt.scatter([],[], hatch='None',
s=40E3*5e2/rates.T[1].max(),
color='k', edgecolors='None',)
p3 = plt.scatter([],[], hatch='None',
s=60E3*5e2/rates.T[1].max(),
color='k', edgecolors='None',)
p4 = plt.scatter([],[], hatch='None',
s=80E3*5e2/rates.T[1].max(),
color='k', edgecolors='None',)
labels = ["20'", "40'", "60'", ">80'"]
plt.legend([p1, p2, p3, p4], labels, ncol=1,
frameon=True, #fontsize=12,
handlelength=1, loc=1,
borderpad=0.75,labelspacing=0.75,
handletextpad=0.75, title='GDP', scatterpoints=1.5)
plt.ylim((-4,40))
plt.xlim((-4,80))
plt.title('K-means clustering')
plt.xlabel('Absolute Latitude (Degrees, 'DFE')')
plt.ylabel('Suicide Rate (per 100 000)');
对绘图的最后一个调整是通过创建自定义图例进行的。我们想展示这些点的不同规模以及它们对应的国内生产总值。由于从低到高有一个连续的梯度,我们不能使用绘制的点。因此,我们创建自己的坐标,但将x和y输入坐标保留为空列表。这不会在剧情中显示任何东西,但是我们可以用它们在传说中注册。图例功能的各种调整控制图例布局的不同方面。我鼓励你尝试一下,看看会发生什么:

至于最后的分析,确定了两个不同的集群。就像我们之前的假设一样,有一个集群具有明显的线性趋势,GDP 相对较高,也位于较高的绝对纬度。虽然辨识度比较弱,但很明显这两个群体是分开的。国内生产总值低的国家聚集在赤道附*。当您添加更多集群时会发生什么?尝试为低 DFE 高比率国家添加一个集群,将其可视化,并思考这对结论意味着什么。
层次聚类分析
层次聚类是基于连通性的聚类。它假设集群是连接的,或者换句话说,是链接的。例如,我们可以根据这个假设对动物和植物进行分类。我们都是从共同点发展而来的。这使得我们有可能一方面假设每个观测都是它自己的群,另一方面假设所有观测都在同一个群中。这也形成了两种层次聚类算法的基础,凝聚和分裂:
- 凝聚聚类从自身聚类中的每个点开始,然后合并相异度最低的两个聚类,即自下而上的方法
- 分裂集群顾名思义,是一种自上而下的方法,我们从一个单独的集群开始,这个集群被分成越来越小的集群
与 k-means 相反,它为我们提供了一种识别聚类的方法,而无需对聚类的数量或聚类位置进行初始猜测。对于这个例子,我们将在 SciPy 中运行一个凝聚聚类算法。
读入并减少数据
宇宙中的星系不是随机分布的,它们形成星团和细丝。这些结构暗示了宇宙复杂的运动和历史。星系团有许多不同的目录,尽管对星系团进行分类的技术各不相同,对此有几种观点。我们将使用更新的兹维奇星表,它包含 19,367 个星系(Falco 等人,1999,PASP 111,438)。文件可以从http://tdc-www.harvard.edu/uzc/index.html下载。第一份《兹维奇星系和星系团目录》于 1961 年发布(兹维奇等人,1961-1968 年。星系和星系团目录,第 1-6 卷。加州理工学院)。
首先,我们导入一些必需的包,将文件读入到一个数据框中,并研究我们所拥有的:
import astropy.coordinates as coord
import astropy.units as u
import astropy.constants as c
Astropy 是一个社区开发的天文软件包,帮助天文学家分析和创建强大的软件来处理他们的数据(http://www.astropy.org/)。我们导入可以处理天文坐标(世界坐标系 - WCS )并转换的坐标包。单位和常数包是处理物理单位(转换等)和常数(带有单位)的包;在单位很重要的情况下,两者都非常便于计算:
uzcat = pd.read_table('data/uzcJ2000.tab/uzcJ2000.tab',
sep='\t', header=16, dtype='str',
names=['ra', 'dec', 'Zmag', 'cz', 'cze', 'T', 'U',
'Ne', 'Zname', 'C', 'Ref', 'Oname', 'M', 'N'],
skiprows=[17])
让我们用 head 方法来看看数据:
uzcat.head()

前两列ra和dec是赤道坐标系中的坐标。基本上,如果你想象地球的经纬度系统扩大了,我们就在里面了。赤纬是经度,赤纬是纬度。这样做的一个结果是,当我们在里面的时候,东方是西方,西方是东方。第三列是 Z 星等,这是测量星系在某个光波长下的亮度(以对数为单位)。第四列是相对于我们太阳的红移距离,单位为千米/秒(第五列是不确定性)(即日心说距离)。这个奇数单位是红移乘以光速(v = cz、z: redshift)。由于其简单性,v参数的速度可以超过光速,即非物理速度。它假设宇宙中每个星系的径向速度都由宇宙的膨胀所支配。回忆起在 第三章**关于模型的学习中,我们看了哈勃定律,宇宙的膨胀随着距离线性增加。虽然哈勃常数在短距离内是恒定的,但今天我们知道宇宙的膨胀速度(即哈勃常数,H 0 )在大距离时会发生变化,这种变化取决于宇宙学假设的情况。稍后我们将把这个距离转换成更容易掌握的距离。
其余各栏在随附的自述文件中或在线上的http://tdc-www.harvard.edu/uzc/uzcjformat.html中进行了描述。
首先,我们想把坐标翻译成比字符串更易读的东西(即ra和dec列)。赤道坐标以小时、分钟和秒为单位,以度、分钟和秒为单位。要从小时中获得度数,只需将小时乘以 15(即 360 度除以 24 小时)。选择此数据集作为示例的首要原因之一是,由于坐标系的原因,距离不是欧几里德距离。为了能够使用它,我们必须将坐标转换成笛卡尔坐标,我们很快就会这样做。如前所述,我们现在用坐标解决第一件事;我们将它们转换成可以理解的字符串:
df['ra'] = df['ra'].apply(lambda x: '{0}h{1}m{2}s'.format(
x[:2],x[2:4],x[4:]))
df['dec'] = df['dec'].apply(lambda x: '{0}d{1}m{2}s'.format(
x[:3],x[3:5],x[5:]))
df.head()

接下来,我们需要将np.nan放在条目为空的地方(我们正在检查它是否是一个带有空格的空字符串)。使用apply,您可以将函数应用于某一列/行,applymap将函数应用于每个表条目:
uzcat = uzcat.applymap(lambda x: np.nan if
isinstance(x, str) and
x.isspace() else x)
uzcat['cz'] = uzcat['cz'].astype('float')
我们还通过运行mycat.Zmag = mycat.Zmag.astype('float')将星等列转换为浮点数。为了对数据进行初步可视化,我们需要将坐标转换为弧度或角度,matplotlib 理解这一点。为此,我们使用方便的天文坐标包:
coords_uzc = coord.SkyCoord(uzcat['ra'], uzcat['dec'], frame='fk5',
equinox='J2000')
我们现在可以访问一个对象中的坐标,并将它们转换为不同的单位。例如,coords_uzc.ra.deg.min()将以度为单位返回最小 RA 坐标;将deg替换为rad将返回弧度。在这个层面上将其可视化有几个原因;这样做的一个原因是,我们想要检查坐标覆盖了什么;我们在看天空的哪一部分。为此,我们使用投影方法;否则,这些坐标没有意义,因为它们不是常见的 x 、 y 、 z 坐标(在本例中,是莫勒维德投影),所以我们看到的是整个天空变*了:
color_czs = (uzcat['cz']+abs(uzcat['cz'].min())) /
(uzcat['cz'].max()+abs(uzcat['cz'].min()))
from matplotlib.patheffects import withStroke
whitebg = withStroke(foreground="w", linewidth=2.5)
fig = plt.figure(figsize=(8,3.5))
ax = fig.add_subplot(111, projection="mollweide")
ax.scatter(coords_uzc.ra.radian-np.pi, coords_uzc.dec.radian,
color=plt.cm.Blues_r(color_czs), alpha=0.6,
s=4, marker='.', zorder=-1)
plt.grid()
for tick in ax.get_xticklabels():
tick.set_path_effects([whitebg])
由于散射点是暗的,我还修改了路径效果的刻度标签,这是在 matplotlib 1.4 中引入的。这使得区分坐标标签变得更加容易:

我们可以看到,我们只有天空上部的数据。我们也看到了银河系的范围,它的气体和尘埃挡住了我们的视线,在数据集中没有发现星系。为了尽量减少我们看到的数据,我们将沿着 12 月 15 到 30 度之间的方向进行切割。让我们检查一下覆盖的距离分布:
uzcat['cz'].hist(bins=50)
plt.yscale('log')
plt.xlabel('CZ-distance')
plt.ylabel('Counts')
plt.xticks(rotation=45, ha='right');

峰值约为 10,000 公里/秒,我们以 12,500 公里/秒的速度切断了它。让我们自上而下地想象一下这个切口。我们不看RA和Dec,而是看RA和cz。首先,我们创建选择:
uzc_czs = uzcat['cz'].as_matrix()
uzcat['Zmag'] = uzcat['Zmag'].astype('float')
decmin = 15
decmax = 30
ramin = 90
ramax = 295
czmin = 0
czmax = 12500
selection_dec = (coords_uzc.dec.deg>decmin) *
(coords_uzc.dec.deg<decmax)
selection_ra = (coords_uzc.ra.deg>ramin) *
(coords_uzc.ra.deg<ramax)
selection_czs = (uzc_czs>czmin) * (uzc_czs<czmax)
selection= selection_dec * selection_ra * selection_czs
为了方便起见,我们从数据框中导出cz列;我们为每个选择创建一个单独的布尔数组。这样,我们可以过滤任何我们想要的。比如调用coords_uzc.ra.radian[selection_dec*selection_ra]只会过滤掉我们要找的RA和Dec。接下来,我们只使用Dec滤镜来绘制,然后想象我们将要切入cz和RA的位置。我没有解释这里选择的RA和cz中的切割,但是看了下面的图片后做的:
fig = plt.figure( figsize=(6,6))
ax = fig.add_subplot(111, polar=True)
sct = ax.scatter(coords_uzc.ra.radian[selection_dec],
uzc_czs[selection_dec],
color='SteelBlue',
s=uzcat['Zmag'][selection_dec*selection_czs],
edgecolors="none",
alpha=0.7,
zorder=0)
ax.set_rlim(0,20000)
ax.set_theta_offset(np.pi/-2)
ax.set_rlabel_position(65)
ax.set_rticks(range(2500,20001,5000));
ax.plot([(ramin*u.deg).to(u.radian).value,
(ramin*u.deg).to(u.radian).value], [0,12500],
color='IndianRed', alpha=0.8, dashes=(10,4))
ax.plot([ramax*np.pi/180., ramax*np.pi/180.], [0,12500],
color='IndianRed', alpha=0.8, dashes=(10,4))
theta = np.arange(ramin, ramax, 1)
ax.plot(theta*np.pi/180., np.ones_like(theta)*12500,
color='IndianRed', alpha=0.8, dashes=(10,4))

这里,当我们实例化子剧情时,我们通过传递polar=True来使用极坐标图,并将selection_dec过滤器应用于所有高于30和低于15度的Dec值。坐标需要以弧度给出,因此我们按照前面的描述,通过简单地要求以弧度表示的坐标,就可以转到弧度。接下来,我们自定义绘图,顺时针旋转绘图 90 度,弧度为π/2。为了更容易阅读径向距离轴标签,我们将它们设置为以 65 度绘制,并设置它们应该绘制的距离。最后两个函数调用绘制了虚线区域,我将该区域设置为selection_ra和selection_czs。接下来,我们只绘制选择点并放大一点:
fig = plt.figure( figsize=(6,6))
ax = fig.add_subplot(111, polar=True)
sct = ax.scatter(coords_uzc.ra.radian[selection],
uzc_czs[selection],
color='SteelBlue',
s=uzcat['Zmag'][selection],
edgecolors="none",
alpha=0.7,
zorder=0)
ax.set_rlim(0,12500)
ax.set_theta_offset(np.pi/-2)
ax.set_rlabel_position(65)
ax.set_rticks(range(2500,12501,2500));

需要注意的是,大多数坐标都在 90 到 270 度形成的直线之上。这将对笛卡尔坐标产生影响。对于总目录的一个子部分,最好创建一个单独的数据框来存储其中的所有内容,包括以度为单位的坐标:
mycat = uzcat.copy(deep=True).loc[selection]
mycat['ra_deg'] = coords_uzc.ra.deg[selection]
mycat['dec_deg'] = coords_uzc.dec.deg[selection]
虽然RA、Dec、cz对于天文学家来说是完全可以理解的坐标格式,但对于大多数人来说却不是(对于天文学家来说甚至很难消化)。所以我们现在将这些球面坐标(天球赤道坐标系)转换为X、Y、Z。为此,我们在 Astropy 坐标包中使用了一些非常方便的函数,我们已经使用过了。首先,我们计算到星系的实际距离。为此,我们使用Distance函数,该函数可以用不同的宇宙几何/宇宙学来完成,但缺省值(当前值)对我们来说没问题:
zs = (((mycat['cz'].as_matrix()*u.km/u.s) / c.c).decompose())
dist = coord.Distance(z=zs)
print(dist)
mycat['dist'] = dist
我们现在有了计算星系笛卡尔坐标的一切:
coords_xyz = coord.SkyCoord(ra=mycat['ra_deg']*u.deg,
dec=mycat['dec_deg']*u.deg,
distance=dist*u.Mpc,
frame='fk5',
equinox='J2000')
现在是将这些笛卡尔坐标保存到我们目录中的好时机:
mycat['X'] = coords_xyz.cartesian.x.value
mycat['Y'] = coords_xyz.cartesian.y.value
mycat['Z'] = coords_xyz.cartesian.z.value
我建议在我们创建的当前数据框架目录(即mycat)上运行head()和describe()。请注意,大多数X坐标都是负的。这是为什么?还记得我们大多数选择的RA坐标吗?回去看看我们画的极坐标图。RA在 90 度到 270 度之间;基本上反方向 0 度,导致他们现在有负 X 坐标。现在我想把这个情节;由于它实际上是三维数据,我将使用两个图来可视化三维:
fig, axs = plt.subplots(1,2, figsize=(14,6))
plt.subplot(121)
plt.scatter(mycat['Y'], -1*mycat['X'], s=8,
color=plt.cm.OrRd_r(10**(mycat.Zmag
-mycat.Zmag.max())),
edgecolor='None')
plt.xlabel('Y (Mpc)'); plt.ylabel('X (Mpc)')
plt.axis('equal');
plt.subplot(122)
plt.scatter(-1*mycat['X'],mycat['Z'], s=8,
color=plt.cm.OrRd_r(10**(mycat.Zmag
-mycat.Zmag.max())),
edgecolor='None')
lstyle = dict(lw=1.5, color='k', dashes=(6,4))
plt.plot([0,150], [0,80], **lstyle)
plt.plot([0,150], [0,45], **lstyle)
plt.plot([0,-25], [0,80], **lstyle)
plt.plot([0,-25], [0,45], **lstyle)
plt.xlabel('X (Mpc)'); plt.ylabel('Z (Mpc)')
plt.axis('equal')
plt.subplots_adjust(wspace=0.25);

像这样可视化它会给你一个很好的概述,即使数据是三维的。我们本可以在转换到X、Y和Z坐标后进行选择,并切割出一个立方体。试试这个,看看这个练习的结果有什么不同。再者,我建议你在这个阶段尝试制作一个立体的剧情;代码参考前一章( 第四章 、回归)。我们现在已经缩小了目录,以覆盖我们感兴趣的区域,并将一些列映射到易于函数读取的值。现在是时候保存它,以便更容易地从我们停止的地方开始。就像上一章一样,我们使用 HDF 文件格式:
TABLE_FILE = 'data/data_ch5_clustering.h5'
mycat.to_hdf(TABLE_FILE, 'ch5data', mode='w', table=True)
作为替代方案,如果您对 HDF 库有问题或者只是想要一个替代方案,您也可以使用 pickle 模块保存它,这是 Python 中的一个标准模块:
mycat.to_pickle('data/data_ch5_clustering.pick')
我们将在 第 7 章**监督和非监督学习中阅读这些数据。现在,我们已经减少了数据,以便对其进行聚类分析。
分层聚类算法
分层凝聚聚类算法通过linkage函数在 SciPy 中运行,该数组作为输入。链接函数中有两个主要参数需要设置,方法和度量:
- 方法定义要使用的链接算法,即我们如何估计两个聚类之间的差异,从而定义聚类是如何形成的
- 度量定义距离度量;在这种情况下,我们使用的是非分类变量,其中距离是有意义的
在我们的例子中,我们已经将数据转换为笛卡尔坐标,这样我们就可以使用公共的欧几里得距离。可以定义自己的距离函数。链接功能中可能的方法和指标在 http://docs.scipy.org/的 SciPy 文档中列出。
联动函数取一个 N 乘 2 的数组(N 个数据点),所以这里只使用 X 和 Y 坐标:
galpos = np.array([mycat.X,mycat.Y]).T
z_centroid = hac.linkage(galpos, metric = 'euclidean',
method = 'centroid')
这里的输出是链接矩阵。这是整个运行的结果;它包含四列。为了快速说明它包含的内容,我们运行了一个受控且小得多的示例。为了可视化各个组级别,我们还绘制了一个树形图,这是通过hac.dendrogram函数完成的,该函数将链接输出作为输入。它以一种便捷的方式可视化了聚类序列。根级别是整个数据集位于一个集群中的级别。另一端是较低层次的集群;但是,它们连接到根级别。每个节点代表一组集群,每个节点连接到两个子节点。如果绘制了所有级别,则末端节点(最低级别的节点)称为叶节点,并且只包含一个数据点(观察)。这些是如何连接的由链接定义(相异度测量算法)决定:
x = np.array([1,2,2,1,6,7,8,5])
y = np.array([8,6,8,7,1,2,2,3])
a = np.array([x,y]).T
z = hac.linkage(a, metric = 'euclidean', method = 'centroid')
fig, axs = plt.subplots(1,2,figsize=(7,3))
axs[0].scatter(x,y, marker='o', s=40, c='IndianRed')
axs[0].set_xlabel('X'); axs[0].set_ylabel('Y');
for i in range(len(x)):
axs[0].annotate(s=str(i), xy=(x[i]+0.1,y[i]+0.1))
ellipse1 = Ellipse(xy=(1.6,8.2),
width=2., height=1.2,
zorder=32, fc='None', ec='k', lw=1.5)
axs[0].add_artist(ellipse1)
d_temp = hac.dendrogram(z, ax=axs[1])
axs[1].annotate(s='8',
xy=(np.mean(d_temp['icoord'][0][1:-1]),
d_temp['dcoord'][0][1]),
xytext=(3,3), textcoords='offset points')
axs[1].annotate(s='9',
xy=(np.mean(d_temp['icoord'][3][1:-1]),
d_temp['dcoord'][3][1]),
xytext=(3,3), textcoords='offset points')
axs[1].annotate(s='10',
xy=(np.mean(d_temp['icoord'][1][1:-1])-2,
d_temp['dcoord'][1][1]),
xytext=(3,3), textcoords='offset points',
ha='right')
axs[1].annotate(s='Root',
xy=(np.mean(d_temp['icoord'][-1][1:-1]),
d_temp['dcoord'][-1][1]-0.3),
xytext=(5,5), textcoords='offset points',
va='top', ha='center')
axs[1].set_xlabel('Leafs')

在联动输出z中,第一行是[0., 2., 1., 2.],表示如下:簇索引 0 和 2 组成一个组,它们的距离是1,组中的叶数(数据点)是2。这组被包围在图像中。组号是原始点数加上迭代次数(n+i);在这种情况下,我们形成集群 8 (8+0)。第三排(NB,不是第二排!)里面有数字[3., 8., 1.11803399, 3.]。它结合了簇索引 3,在这种情况下,这只是一个点,因为每个小于 8 的簇号都是簇 8 的点/叶,就像我们在第一次迭代中创建的一样。它们之间的距离(使用给定的度量和方法)是1.111803399并且它包含三个叶子。形成第 10簇,即 8 + 2 次迭代。我已经在示例树图中说明了这些节点,并标记了集群组编号 10。想想它如何适合这里描述的链接输出。
有了这些知识,我们现在可以绘制主要数据分析的树形图:
fig, ax = plt.subplots(1, figsize=(8,6))
d0 = hac.dendrogram(z_centroid, p=6, truncate_mode='level',
orientation='right', ax=ax)

这一次,我给绘图功能增加了一些参数——我们将整个树形图倾斜了 90 度。两个节点之间的高度与节点之间的不同程度成正比(距离法)。将这个树形图切开会产生一定数量的簇。有趣的是,根节点只分裂一次成两个簇,其中只有一个继续分裂。为了得到某个级别的聚类,我们使用fcluster函数,这也是 SciPy 聚类模块的一部分。我建议你尝试不同数量的集群并绘制它们。在下面的例子中,我使用了20集群:
nclust = 20
part_centroid = hac.fcluster(z_centroid, 20, criterion='maxclust')
这里,标准设定了形成集群的约束。为了检查每个聚类/组中的点的划分,我们绘制了一个直方图:
plt.figure(figsize=(7,6))
otpt = plt.hist(part_centroid, color='SteelBlue', bins=nclust);
plt.xlabel('Cluster no.'); plt.ylabel('Counts');

第 11集群有很多点。当然,这并不能告诉我们太多,所以现在我们绘制所有集群。作为每个簇的位置,我只是用数组对象的均值法计算质心。这只是为了更清楚地标记集群的位置:
plt.figure(figsize=(6,5))
plt.subplot(111)
part = part_centroid
levels = np.arange(nclust)
colors = plt.cm.rainbow(np.random.rand(len(levels)))
for n, color in zip(levels, colors):
plt.scatter(mycat['Y'][part==n], -1*mycat['X'][part==n], s=12,
color=color, edgecolor='None')
plt.plot(mycat['Y'][part==n].mean(),
-1*mycat['X'][part==n].mean(),
'o', c='0.7', mec='k', ms=6,
ls='None', mew=1.5, alpha=0.7)
plt.xlabel('Y (Mpc)'); plt.ylabel('X (Mpc)')
plt.scatter(mycat['Y'], -1*mycat['X'], s=10,
color='0.7', edgecolor='None',zorder=-1)
plt.title('A slice of the Universe - Clusters identified')
plt.axis('equal');

集群划分看起来很稳固。然而,我们通过目视检查改变集群数量的效果来确定集群的数量。这可能不是真实或理想的集群数量。在前面的例子中,我们有一个我们想要测试的两个集群的假设,但是即使在这种情况下,数据也可能更好地由例如三个集群或者可能没有集群(或者一个具有异常值的集群)来表示。我们希望有一种可重复的方法来确定集群的数量。有几种方法可以做到这一点;甚至还有一篇专门的维基百科文章介绍如何找到集群的数量(https://en . Wikipedia . org/wiki/decising _ the _ number _ of _ clusters _ in _ a _ data _ set)。确定最佳簇数的主要问题是我们必须假设一些关于簇形状的东西。分层聚类通过检查一个聚类的所有级别,假设每个聚类被分成更小的聚类,从而稍微避免了这个问题。
一种方法是通过计算每个聚类的*方距离(即方差)的归一化和来测量聚类的紧密度,然后用它来估计聚类大小的方差可以描述多少百分比的数据。逐渐增加聚类的数量,通过计算方差,你会得到一个聚类覆盖率增加的图表。当达到群集的true数量时,该方差(或覆盖率)将停止增加并变*。然而,这并不适合所有的丛形状;例如,想象一下,非常细长的椭圆彼此靠*。在我们的例子中,中等距离的团簇更多的是丝状的,而不是团簇状的(或高斯状的)。
总结
我们现在已经使用一系列方法来识别聚类,从手动计算简单的质心到 SciPy 中的高级层次聚类算法。当然,Python 中还有更多包。我们将在 第 7 章**监督和非监督学习中查看一个替代方案,即机器学习包 Scikit-learn,以识别聚类。SciPy 有这两个聚类框架,即向量量化和层次聚类,它们为聚类分析奠定了基础,在许多一般的数据分析问题中非常有用。在下一章中,我们将了解贝叶斯分析以及如何使用 Python 中的 PyMC 贝叶斯推理包来表征数据中的各种事物。
六、贝叶斯方法
贝叶斯推理是统计学的不同范式;它不是一种方法或算法,如聚类分析或线性回归。它仅次于经典统计分析。到目前为止,我们在这本书里所做的一切,以及你在经典(或频繁)统计分析中所能做的一切,你都可以在贝叶斯统计中做。经常性统计(经典)和贝叶斯统计的主要区别在于,经常性统计假设模型参数是固定的,而贝叶斯假设它们有一个范围,一个分布。因此,从频率计方法中,直接从数据中创建点估计值(均值、方差或固定模型参数)是很容易的。点估计对于数据是唯一的;每个新数据集都需要新的点估计。
在本章中,我们将涵盖以下主题:
- 贝叶斯分析的例子:一个是我们试图识别时间序列中的一个转换点,另一个是线性回归,我们比较了来自 第 4 章**回归的方法
- 如何从贝叶斯分析评估多芯片组件运行
- 关于在地图上绘制坐标的非常简短的介绍,这在呈现和调查数据时非常重要
贝叶斯方法
从贝叶斯方法来看,数据被视为固定的。一旦我们测量了东西,数值就固定了。另一方面,参数可以用概率分布来描述。概率分布描述了对某个参数的了解程度。如果我们获得新数据,这种描述可能会改变,但模型本身不会改变。关于这方面的文献很多,对于什么时候使用频繁者分析或者什么时候使用贝叶斯分析没有经验法则。
对于简单且表现良好的数据,我想说,当您需要快速估算时,频繁方法是可以的。为了获得更多的见解和更多的约束问题,也就是说,当我们更多地了解我们的参数并且可以用比简单的一致先验更多的先验来估计先验分布时,最好使用贝叶斯方法。由于贝叶斯分析中对事物的处理略显直观,因此更容易构建更复杂的模型,回答更复杂的问题。
可信区间与置信区间
突出差异的一个好的常见方法是将常客的置信区间与贝叶斯统计中的相应概念,可信区间进行比较。置信区间来自于频繁者方法,其中参数是固定的。置信区间基于观察的重复。98%的置信区间是指重复实验大量测量参数并计算每个实验的区间,98%的区间将包含参数的值。这可以追溯到数据是随机的这一事实。
可信(或概率)区间源于概率,即贝叶斯方法。这意味着参数是随机的,我们可以说,给定数据,参数的真实值有 98%的概率在区间内。
贝叶斯公式
贝叶斯分析归结为贝叶斯公式;因此,一个关于贝叶斯分析的章节,如果没有提到贝叶斯公式,就没有多大价值。在贝叶斯分析中,所做的一切都可以用概率陈述来表达:

这被理解为给定b的概率,其中b是数据,a是你试图估计的参数。通过贝叶斯分析,我们建立了可以用数据进行测试的模型。贝叶斯分析(推理)使用概率分布作为我们正在构建(测试)的模型(假设)的输入。
有了行李统计的一些先验知识,我们写出了贝叶斯公式:

贝叶斯公式来自条件概率。用文字描述,后验概率是观测值(给定参数)乘以参数先验的概率除以整个参数空间的积分。正是这个分母给分析带来了麻烦,因为为了计算它,我们需要使用一些高级分析;在这些例子中,使用了马尔可夫链蒙特卡罗 ( MCMC )算法。我们假设您熟悉贝叶斯公式的基础知识。我们将看到如何在 PyMC 包的帮助下用 Python 实现分析。
Python 包
Python 中一个流行的贝叶斯分析包是 PyMC(http://pymc-devs.github.io/pymc/)。PyMC 是一个积极开发的包,使贝叶斯模型和 Python 中的拟合变得容易理解和直接。可用的拟合算法之一是 MCMC,它也是使用最多的算法之一。还有其他套餐,如特色丰富的司仪套餐(http://dan.iel.fm/emcee/current/)。我们将在本书中使用 PyMC。要在 Anaconda 中安装 PyMC,请打开 Anaconda 命令提示符(或终端窗口)并使用 conda 命令安装 PyMC:
conda install pymc
它将检查 Anaconda Python 包索引,并下载和安装/升级 PyMC 的必要依赖项。接下来,我们可以启动一个新的 Jupyter Notebook,并输入默认导入。
美国航空旅行安全记录
在本例中,我们将查看来自美国国家运输安全委员会 ( NTSB )的数据集。NTSB 有一个开放的数据库,可以从他们的网页http://www.ntsb.gov下载。数据中有一点很重要,那就是包含了美国境内的民航事故和选定的事件,其领土和属地,以及国际水域,也就是说,它不是针对整个世界的。基本上,它只适用于与美国相关的事故,这对于美国的国家组织来说是有意义的。有包含整个世界的数据库,但其中的字段较少。例如,NTSB 数据集包含有关事故轻伤的信息。为了进行比较,并作为练习的开始,在对 NTSB 数据进行贝叶斯分析后,我们将加载并快速查看由 Socrata(https://opendata.socrata.com)从 OpenData 获得的覆盖全球的数据集。我们想在这一部分调查的问题是,飞机事故的统计数据是否随时间有任何跳跃。另一个来源是航空安全网(https://aviation-safety.net)。在开始分析之前,有一点很重要,那就是我们应该再次读入真实的原始数据,并清除掉不需要的部分,这样我们才能专注于分析。这需要几行代码,但是覆盖这一点非常重要,因为这向您展示了真正发生的事情,并且您将比我给您完整的清理和减少的数据(或者甚至用随机噪声创建的数据)更好地理解结果和数据。
获取 NTSB 数据库
从 NTSB 下载数据,进入他们的网页(http://www.ntsb.gov,点击航空事故数据库,选择全部下载(文本)。现在应该下载并保存数据文件AviationData.txt,供您读入。
数据集包含特定事故发生时间的日期戳。为了能够将日期读入 Python 理解的格式,我们需要用 datetime 包解析日期字符串。日期时间包是 Python 附带的标准包(以及 Anaconda 发行版):
from datetime import datetime
现在让我们读入数据。为了节省您的时间,我重新定义了列名,以便更容易访问。现在,您应该已经熟悉了非常有用的 Pandasread_csv功能:
aadata = pd.read_csv('data/AviationData.txt',
delimiter='|',
skiprows=1,
names=['id', 'type', 'number', 'date',
'location', 'country', 'lat', 'long', 'airport_code',
'airport_name', 'injury_severity', 'aircraft_damage',
'aircraft_cat', 'reg_no', 'make', 'model',
'amateur_built', 'no_engines', 'engine_type', 'FAR_desc',
'schedule', 'purpose', 'air_carrier', 'fatal',
'serious', 'minor', 'uninjured',
'weather', 'broad_phase', 'report_status',
'pub_date', 'none'])
列出列名表明这里有丰富的数据,例如事故的位置、纬度和经度、机场代码和名称等等:
aadata.columns

在查看数据并尝试以下一些操作后,您会发现一些日期条目是空的,也就是说,只包含空格()。像以前一样,我们可以使用apply ( -map)函数找到带有空白的条目,并替换这些值。在这种情况下,我将使用匹配表达式!=,即not equal to产生的布尔数组快速过滤掉它们(我们只需要带有日期的行)。这是因为,如开头所述,我们想知道事故是如何随时间变化的:
selection = aadata['date'] != ' '
aadata = aadata[selection]
现在实际的日期字符串有点棘手。他们有MONTH/DAY/YEAR格式(对我这个欧洲人来说,这是不合逻辑的)。标准的datetime模块可以做这项工作,只要我们告诉它日期是什么格式。实际上,Pandas 可以在读取带有parse_dates=X标志的数据时解析这一点,其中X要么是列索引整数,要么是列名字符串。
然而,有时它在没有投入大量工作的情况下不能很好地工作,就像在我们的例子中一样,所以我们将自己解析它。这里,我们将其解析到新的列datetime,中,其中每个日期都通过strptime函数转换成一个datetime对象。其中一个原因是日期是用空格括起来的。所以给出的是02/18/2016而不是02/18/2016,这也是为什么我们给出的日期格式规范是%m/%d/%Y,也就是四周有空白。这样,strptime功能就知道它是什么样子了:
aadata['datetime'] = [datetime.strptime(x, ' %m/%d/%Y ') for x in
aadata['date']]
现在我们有了datetime对象,Python 知道日期意味着什么。只检查年份或月份可能会在以后变得有趣。为了方便使用,我们将它们保存在单独的列中。有了这样一个适中的数据集,我们可以创建许多列,而不会明显减慢速度:
aadata['month'] = [int(x.month) for x in aadata['datetime']]
aadata['year'] = [int(x.year) for x in aadata['datetime']]
现在我们也希望日期是十进制的年份,这样我们就可以一年一年地绑定它们,并计算年度统计数据。为了做到这一点,我们想看看某个日期已经过去了一年的哪一部分。在这里,我们写了一个小函数,这个解决方案的灵感部分来自于网上的各种答案。我们调用datetime函数来创建代表一年的开始和结束的对象。我们需要把year+1放在这里,如果我们不这样做,month=12和day=31将在新年前后产生无意义的值(例如,2017 年,而现在仍然是 2016 年)。如果你在谷歌上搜索这个问题,有很多不同的好答案和方法(有些需要安装额外的软件包):
def decyear(date):
start = datetime(year=date.year, month=1, day=1)
end = datetime(year=date.year+1, month=1, day=1)
decimal = (date-start)/(end-start)
return date.year+decimal
有了这个函数,我们可以将其应用到我们表格的datetime列中的每个元素。由于所有的列行已经包含一个datetime对象,我们刚刚创建的函数愉快地接受输入:
aadata['decyear'] = aadata['datetime'].apply(decyear)
现在,列Latitude, Longitude, uninjured, fatalities,和serious and minor injuries应该都是浮点数而不是字符串,以便于计算和其他操作。所以我们用applymap方法把它们转换成浮点数。以下代码将把空字符串转换成Nan值,把数字转换成浮点数:
cols = ['lat', 'long',
'fatal',
'serious',
'minor',
'uninjured']
aadata[cols] = aadata[cols].applymap(
lambda x: np.nan if isinstance(x, str)
and x.isspace() else float(x))
我们只是将 lambda 函数应用于给定列中的所有条目,该函数将值转换为浮点数,将空字符串转换为 NaN。现在让我们绘制数据图,看看我们是否需要进一步修整或处理它:
plt.figure(figsize=(9,4.5))
plt.step(aadata['decyear'], aadata['fatal'],
lw=1.75, where='mid', alpha=0.5, label='Fatal')
plt.step(aadata['decyear'], aadata['minor']+200,
lw=1.75,where='mid', label='Minor')
plt.step(aadata['decyear'], aadata['serious']+200*2,
lw=1.75, where='mid', label='Serious')
plt.xticks(rotation=45)
plt.legend(loc=(0.01,.4),fontsize=15)
plt.ylim((-10,600))
plt.grid(axis='y')
plt.title('Accident injuries {0}-{1}'.format(
aadata['year'].min(), aadata['year'].max()))
plt.text(0.15,0.92,'source: NTSB', size=12,
transform=plt.gca().transAxes, ha='right')
plt.yticks(np.arange(0,600,100), [0,100,0,100,0,100])
plt.xlabel('Year')
plt.ylabel('No injuries recorded')
plt.xlim((aadata['decyear'].min()-0.5,
aadata['decyear'].max()+0.5));

1980 年前后的可用数据非常稀少,不适合统计解释。在决定做什么之前,让我们检查这些条目的数据。为了查看记录的事故数量,我们绘制了当时的柱状图。这里,我们通过组合两个布尔数组来使用过滤器。我们还通过给出年份来改变bins参数,从 1975 年到 1990 年;这样,我们就知道了bins将是每年:
plt.figure(figsize=(9,3))
plt.subplot(121)
year_selection = (aadata['year']>=1975) & (aadata['year']<=2016)
plt.hist(aadata[year_selection]['year'].as_matrix(),
bins=np.arange(1975,2016+2,1), align='mid')
plt.xlabel('Year'); plt.grid(axis='x')
plt.xticks(rotation=45);
plt.ylabel('Accidents recorded')
plt.subplot(122)
year_selection = (aadata['year']>=1976) & (aadata['year']<=1986)
plt.hist(aadata[year_selection]['year'].as_matrix(),
bins=np.arange(1976,1986+2,1), align='mid')
plt.xlabel('Year')
plt.xticks(rotation=45);

事故的绝对数量在 35 年里大约减少了一半;鉴于乘客人数肯定增加了,这是非常好的。右边的图显示,在 1983 年之前,NTSB 记录的事故很少。用该标准列出的表格显示了六个记录的事故:
aadata[aadata['year']<=1981]
经过这次彻底检查,我认为删除 1981 年以前(包括 1981 年)的条目是安全的。我不知道缺乏数据的原因;也许 NTSB 就是在这个时候建立的?他们的任务被重新制定,包括存储事件的数据库?无论如何,让我们排除这些条目:
aadata = aadata[ aadata['year']>1981 ]
创建与之前相同的图形,这是我们可以看到的:
plt.figure(figsize=(10,5))
plt.step(aadata['decyear'], aadata['fatal'],
lw=1.75, where='mid', alpha=0.5, label='Fatal')
plt.step(aadata['decyear'], aadata['minor']+200,
lw=1.75,where='mid', label='Minor')
plt.step(aadata['decyear'], aadata['serious']+200*2,
lw=1.75, where='mid', label='Serious')
plt.xticks(rotation=45)
plt.legend(loc=(0.8,0.74),fontsize=15)
plt.ylim((-10,600))
plt.grid(axis='x')
plt.title('Accidents {0}-{1}'.format(
aadata['year'].min(), aadata['year'].max()))
plt.text(0.135,0.95,'source: NTSB', size=12,
transform=plt.gca().transAxes, ha='right')
plt.yticks(np.arange(0,600,100), [0,100,0,100,0,100])
plt.xlabel('Year')
plt.ylabel('No injuries recorded')
plt.xlim((aadata['decyear'].min()-0.5,
aadata['decyear'].max()+0.5));

我们现在有了一个可以使用的干净数据集。目前仍很难区分任何趋势,但在千年转移前后,趋势发生了变化。然而,为了进一步了解可能发生的情况,我们希望绑定数据并查看各种关键数字。
宁滨各项数据
在本节中,我们将按年份绑定数据。这是为了让我们更好地了解数据的总体趋势,这将带我们进入表征和分析的下一步。
如前所述,我们希望绑定每年的数据来查看每年的趋势。我们在 第四章**回归之前做过宁滨;我们用 Pandas 的groupby方法DataFrame。这里有两种方法来定义垃圾箱,我们可以使用 NumPy 的digitize功能,也可以使用 Pandas cut 功能。我在这里使用了数字化功能,因为它通常更有用;您可能不会总是使用 Pandas 作为您的数据(出于某些原因):
bins = np.arange(aadata.year.min(), aadata.year.max()+1, 1 )
yearly_dig = aadata.groupby(np.digitize(aadata.year, bins))
我们现在可以计算每个箱子的统计数据。我们可以得到总和、最大值、*均值等等:
yearly_dig.mean().head()

此外,请确保您度过了这些年:
np.floor(yearly_dig['year'].mean()).as_matrix()

更重要的是,我们可以把它形象化。下面的函数将绘制条形图,并将各个字段堆叠在一起。作为输入,它采用 Pandas groups对象、字段名列表(< 3)以及哪个字段名用作 x 轴。然后,有一些定制和调整,使它看起来更好。其中值得注意的是fig.autofmt_xdate(rotation=90, ha='center')功能,它会自动为您格式化日期。可以给它发送各种参数;在这种情况下,我们使用它来旋转和水*对齐 x 刻度标签(作为日期):
def plot_trend(groups, fields=['Fatal'], which='year', what='max'):
fig, ax = plt.subplots(1,1,figsize=(9,3.5))
x = np.floor(groups.mean()[which.lower()]).as_matrix()
width = 0.9
colors = ['LightSalmon', 'SteelBlue', 'Green']
bottom = np.zeros( len(groups.max()[fields[0].lower()]) )
for i in range(len(fields)):
if what=='max':
ax.bar(x, groups.max()[fields[int(i)].lower()],
width, color=colors[int(i)],
label=fields[int(i)], align='center',
bottom=bottom, zorder=4)
bottom += groups.max()[
fields[int(i)].lower()
].as_matrix()
elif what=='mean':
ax.bar(x, groups.mean()[fields[int(i)].lower()],
width, color=colors[int(i)],
label=fields[int(i)],
align='center', bottom=bottom, zorder=4)
bottom += groups.mean()[
fields[int(i)].lower()
].as_matrix()
ax.legend(loc=2, ncol=2, frameon=False)
ax.grid(b=True, which='major',
axis='y', color='0.65',linestyle='-', zorder=-1)
ax.yaxis.set_ticks_position('left')
ax.xaxis.set_ticks_position('bottom')
for tic1, tic2 in zip(
ax.xaxis.get_major_ticks(),
ax.yaxis.get_major_ticks()
):
tic1.tick1On = tic1.tick2On = False
tic2.tick1On = tic2.tick2On = False
for spine in ['left','right','top','bottom']:
ax.spines[spine].set_color('w')
xticks = np.arange(x.min(), x.max()+1, 1)
ax.set_xticks(xticks)
ax.set_xticklabels([str(int(x)) for x in xticks])
fig.autofmt_xdate(rotation=90, ha='center')
ax.set_xlim((xticks.min()-1.5, xticks.max()+0.5))
ax.set_ylim((0,bottom.max()*1.15))
if what=='max':
ax.set_title('Plane accidents maximum injuries')
ax.set_ylabel('Max value')
elif what=='mean':
ax.set_title('Plane accidents mean injuries')
ax.set_ylabel('Mean value')
ax.set_xlabel(str(which))
return ax
现在,让我们用它来绘制从 1982 年到 2016 年时间跨度内每年最大的致命、严重和轻微伤害:
ax = plot_trend(yearly_dig, fields=['Fatal','Serious','Minor'],
which='Year')

占主导地位的酒吧是致命的酒吧——至少从这个视觉检查来看是如此——所以当事情在飞机事故中变得非常糟糕时,大多数人都会受到致命的伤害。这里奇怪的是,最大值似乎有一个变化,在 1991 年和 1996 年之间的某个地方有一个跳跃。之后,看起来最大死亡人数的*均值更高。这就是我们将在下一节中尝试用贝叶斯推理建模的内容。
数据的贝叶斯分析
现在我们可以深入到这个分析的贝叶斯部分。您应该始终按照我们在前面练习中所做的方式检查数据。这些步骤通常在文本中被跳过,甚至使用捏造的数据;这描绘了一幅简化的数据分析图。我们必须与数据一起工作才能有所收获,并了解什么样的分析是可行的。首先,我们需要从 Matplot 子模块导入 PyMC 包和绘图函数。该函数绘制了参数后验分布、轨迹(即每次迭代)和自相关的摘要:
import pymc
from pymc import Matplot as mcplt
为了开始分析,我们将x和y值、年份和最大死亡人数存储在数组中:
x = np.floor(yearly_dig.mean()['year']).as_matrix()
y = yearly_dig.max()['fatal'].as_matrix()
现在我们通过定义一个包含所有参数和数据的函数来开发我们的模型。这是一个离散的过程,所以我们使用泊松分布。此外,我们使用早期和晚期*均利率的指数分布;这适用于这个随机过程。试着画出死亡人数的直方图,看看它有什么分布。对于发生跳跃/切换的年份,我们使用离散的均匀分布,也就是说,它对于上下界之间的所有值都是*坦的,其他地方为零。由各种随机变量决定的变量,后期、前期均值,切换点是跳跃前后的均值。因为它依赖于(随机)变量,所以被称为确定性变量。在代码中,这是由@pymc.deterministic()装饰器标记的。这就是我们试图模拟的过程。PyMC 内置了几个发行版,但您也可以定义自己的发行版。然而,对于大多数问题,内置的应该可以解决。各种可用的分布在pymc.distributions子模块中:
def model_fatalities(y=y):
s = pymc.DiscreteUniform('s', lower=5, upper=18, value=14)
e = pymc.Exponential('e', beta=1.)
l = pymc.Exponential('l', beta=1.)
@pymc.deterministic(plot=False)
def m(s=s, e=e, l=l):
meanval = np.empty(len(y))
meanval[:s] = e
meanval[s:] = l
return meanval
D = pymc.Poisson('D', mu=m, value=y, observed=True)
return locals()
return locals()是一种将所有局部变量发送回去的简单方法。因为我们对它们有一个很好的概述,所以使用起来不成问题。我们现在已经定义了模型;为了在 MCMC 采样器中使用它,我们将模式作为 MCMC 类的输入:
np.random.seed(1234)
MDL = pymc.MCMC(model_fatalities(y=y))
为了使用标准采样器,我们可以简单地调用MDL.sample(N)方法,其中N是要运行的迭代次数。还有其他参数;你可以给它一个磨合期,一个不考虑结果的时期。这是 MCMC 算法的一部分,有时让它运行几次被丢弃的迭代会很好,这样它就可以开始收敛。第二,我们可以给出一个单薄的论据;这就是保存迭代结果的频率。在我们的例子中,我运行了 50,000 次,其中 5,000 次迭代是老化和减二。尝试用各种数字运行,看看结果是否改变,如何改变,以及参数估计得如何:
MDL.sample(5e4, 5e3, 2)

step 方法,即如何在参数空间中移动,也可以改变。要检查我们有什么步骤方法,我们运行以下命令:
MDL.step_method_dict

要更改步长方法,可能是使用自适应步长(长度)的自适应 Metropolis 算法,我们将导入它并运行以下内容:
from pymc import AdaptiveMetropolis
MDL = pymc.MCMC(model_fatalities(y=y))
MDL.use_step_method(AdaptiveMetropolis, MDL.e)
MDL.use_step_method(AdaptiveMetropolis, MDL.l)
MDL.sample(5e4, 5e3, 2)
不过,我们不会在这里这样做;这是针对变量高度相关的问题。我把这个作为一个练习留给你们测试,还有不同的先验参数分布。
现在我们在MDL对象中有了整个运行。从这个对象,我们可以估计参数并绘制它们的后验分布。所有这些都有方便的功能。下面的代码向您展示了如何提取后验分布和标准差的*均值。这是可信区间出现的地方;我们有可信的参数区间,而不是置信区间:
early = MDL.stats()['e']['mean']
earlyerr = MDL.stats()['e']['standard deviation']
late = MDL.stats()['l']['mean']
lateerr = MDL.stats()['l']['standard deviation']
spt = MDL.stats()['s']['mean']
spterr = MDL.stats()['s']['standard deviation']
在绘制结果和所有数字之前,我们必须检查 MCMC 运行的结果,为此,我们绘制了所有随机参数的轨迹、后验分布和自相关。我们使用开始时导入的pymc.Matplot模块中的plot功能来完成此操作:
mcplt.plot(MDL)

该函数将这三件事绘制在一个图形中。这对于快速评估结果非常方便。看所有的情节很重要;他们给出了跑步进展如何的线索:

对于每个随机变量,绘制轨迹、自相关和后验分布:

在我们的例子中,这给出了e、l和s各一个数字。在每个图中,左上角的图显示了轨迹或时间序列。跟踪是每次迭代的值。可以通过late参数的MDL.trace('l')[:]进入。尝试获取跟踪,并为其绘制跟踪与迭代和直方图;它们看起来应该和这些一样。对于一个好的模型设置,跟踪应该围绕最佳估计随机波动,就像早期和later参数的跟踪一样。自相关图应该在 0 处有一个峰值;如果曲线图在较高的x值处显示大量值,则表明您需要增加 thin 变量。跳跃/切换点的轨迹和后验分布看起来不同。然而,因为它是一个离散变量,并且被限制在一年内,所以它只显示一个峰值,并且轨迹遵循这个峰值。因此,切换点受到非常小的可信区间的良好约束。在pymc.Matplot模块(mcplt中,可以使用相关功能单独绘制图中的每个图。可以使用以下命令生成l变量的自相关图:
mcplt.autocorrelation(MDL.l)

现在,我们已经构建了一个模型,运行了采样器,并提取了最佳估计参数,我们可以绘制结果。模型识别的跳转/切换点只是年数组中的一个指标,所以我们需要找到该指标/位置对应的年份。为此,我们使用 NumPy 的floor函数向下舍入,然后我们可以将其转换为整数并对x数组进行切片,这是我们在开始创建的年份数组:
s = int(np.floor(spt))
print(spt, spterr, x[s])
前面的代码给出了12.524、0.499423667841和1994.0作为输出。
为了用结果构建图,我们可以再次使用前面定义的函数,但是这次我们只关注致命伤害,所以我们给出了不同的场参数。为了绘制可信区间,我在这里使用fill_between函数;这是一个非常方便的功能,并完全按照它所说的去做。此外,我使用了更强大的注释功能,而不是图中的text功能,我们可以用一个漂亮的框来定制它:
ax = plot_trend(yearly_dig, fields=['Fatal'], which='Year')
ax.plot([x[0]-1.5,x[s]],[early,early], 'k', lw=2)
ax.fill_between([x[0]-1.5,x[s]],
[early-3*earlyerr,early-3*earlyerr],
[early+3*earlyerr,early+3*earlyerr],
color='0.3', alpha=0.5, zorder=2)
ax.plot([x[s],x[-1]+0.5],[late,late], 'k', lw=2)
ax.fill_between([x[s],x[-1]+0.5],
[late-3*lateerr,late-3*lateerr],
[late+3*lateerr,late+3*lateerr],
color='0.3', alpha=0.5, zorder=2)
ax.axvline(int(x[s]), color='0.4', dashes=(3,3), lw=2)
bbox_args = dict(boxstyle="round", fc="w", alpha=0.85)
ax.annotate('{0:.1f}$\pm${1:.1f}'.format(early, earlyerr),
xy=(x[s]-1,early),
bbox=bbox_args, ha='right', va='center')
ax.annotate('{0:.1f}$\pm${1:.1f}'.format(late, lateerr),
xy=(x[s]+1,late),
bbox=bbox_args, ha='left',va='center')
ax.annotate('{0}'.format(int(x[s])),xy=(int(x[s]),300),
bbox=bbox_args, ha='center',va='center');

给定数据,参数在范围104.5+/-3.0和164.7+/- 2.7内,可信度为 95%(即+/-1σ)。一年中*均最高死亡人数有所增加,并在 1994 年左右从104.5人跃升至164.7人。虽然从图中可以看到 2004 年和 2012-2013 年,但最大值并没有遵循同样的趋势。即使这是一个简单的模型,也很难主张一个更复杂的模型,并且结果是显著的。当然,目标应该是每年零死亡。
我们分析了每年的统计数据;如果我们看看这一年的统计数据呢?为此,我们需要每月绑定。所以在下一部分,这就是我们要做的。
宁滨按月
重复我们的宁滨程序,但是按月重复:
bins = np.arange(1, 12+1, 1 )
monthly_dig = aadata.groupby(np.digitize(aadata.month, bins))
monthly_dig.mean().head()

现在我们可以做同样的事情,但是每个月;只需将正确的参数发送到我们创建的plot_trend函数:
ax = plot_trend(monthly_dig, fields=['Fatal', 'Serious', 'Minor'],
which='Month')
ax.set_xlim(0.5,12.5);

虽然没有强烈的趋势,但我们可以注意到,第 7 和第 8 个月、7 月和 8 月,以及第 11 和第 12 个月、11 月和 12 月,都有较高的值。夏天和圣诞节是一年中最受欢迎的旅游时间,所以这可能反映了每年游客数量的变化。更多的旅行者意味着事故数量的增加(风险不变),因此发生高死亡率事故的可能性更大。另一个问题是均值变异是什么样子的;到目前为止,我们所做的是最大限度的。我在绘图功能中增加了一个参数what,并将其设置为mean,这将导致*均值被绘制。我把这个作为练习;你会看到一些奇怪的意思。试着创造不同的情节来调查!另外,不要忘记检查每月和每年的*均值。
最后一个情节也突出了一些其他的东西——乘客的总数可能会影响结果。要获取美国的乘客总数,您可以运行以下命令:
from pandas.io import wb
airpasstot = wb.download(indicator='IS.AIR.PSGR',
country=['USA'], start=1982, end=2014)
但是,查看这些数据并将其与我们在这里处理的数据进行比较,这只是留给您的一个练习。
为了让您体验 Python 可能实现的一些强大的可视化,我想在地图上快速绘制每个事故的坐标。在这种情况下,这当然不是必须的,但是有时可视化结果是很好的,在 Python 中,如何做到这一点并不明显。所以这一部分对于本章来说不是必须的,但是知道如何用 Python 在地图上绘制东西的基础知识是很好的,因为我们研究的很多东西都取决于地球上的位置。
标绘坐标
为了绘制坐标,我们(不幸地)必须安装软件包。更不幸的是,这取决于你运行的是什么操作系统和 Python 发行版。我们将在这里快速介绍的两个包是 mpl_toolkits 包的底图模块和制图包。
第一个,mpl_toolkits,在 Windows 上不工作(截至 2016 年 4 月)。要在 Mac 和 Linux 上安装 Anaconda 中 mpl_toolkits 的底图,请运行conda install -c https://conda.anaconda.org/anaconda basemap。这将安装底图及其所有依赖项。
第二个包 cartopy 依赖于 GEOS 和 proj.4 库,所以需要先安装它们。这可能有点繁琐,但是一旦安装了 GEOS(大于 3.3.3 版本)和 proj.4 库(大于 4.8.0 版本),cartopy 就可以用pip命令行工具pip install cartopy安装了。同样,在 Windows 中,proj.4 的预构建二进制文件是 4.4.6 版本,这使得安装 cartopy 也非常困难。
对于这个快速练习,我们在单独的数组中获取每个事故的纬度和经度,因为绘图命令可能对输入格式敏感,并且可能不支持 Pandas 系列:
lats, lons = aadata['lat'].as_matrix(), aadata['long'].as_matrix()
迦太基
首先是漫画,我们首先创建一个图形,然后给它添加轴,在这里我们必须指定图形内轴的左下和右上边缘,此外,给它一个投影,这是从漫画的 CRS 模块中获取的。在导入 CRS 模块时,我们还会导入经度和纬度的格式化程序,这只会在x和y的刻度标签上添加N、S、E和W。导入 matplotlib ticker 模块,我们可以指定 tick 标签的确切位置。我们可以用与 ticks 函数相同的方式来实现这一点。
当我们绘制事故的坐标时,有一个显示地球的背景是很好的。因此,我们用ax.stock_img()命令加载地球的图像。可以装载海岸线、国家和其他东西。要查看包括不同投影在内的示例和其他可能性,请查看漫画网站(http://scitools.org.uk/cartopy)。然后,我们创建一个以经纬度为坐标的散点图,并根据总死亡人数按比例缩放标记的大小。然后,我们绘制网格线、经线和纬度大圆。之后,我们只需使用导入的格式化程序和刻度位置设置程序自定义刻度位置和标签:
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
import matplotlib.ticker as mticker
fig = plt.figure(figsize=(12,10))
ax = fig.add_axes([0,0,1,1], projection=ccrs.PlateCarree())
ax.stock_img()
ax.scatter(aadata['long'],aadata['lat'] ,
color='IndianRed', s=aadata['fatal']*2,
transform=ccrs.Geodetic())
gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True,
linewidth=2, color='gray',
alpha=0.5, linestyle='--')
gl.xlabels_top = False
gl.ylabels_right = False
gl.xlocator = mticker.FixedLocator(np.arange(-180,180+1,60))
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER

似乎大多数登记的事故发生在陆地上。我把这个数字的进一步修改留给你。查看世界各地的统计数据,能够绘制参数的空间分布图是非常有用的。
Mpl 工具包–底图
正如承诺的那样,我们现在在 mpl_toolkits 的底图模块中生成完全相同的图。这里,我们只需要导入底图模块,不需要其他的勾号修改功能。在创建轴时不会设置投影,而是通过使用所需投影调用底图函数来创建投影。以前,我们称之为卡雷投影;这只是等距圆柱投影,在底图中,这是通过给它圆柱的投影字符串cyl获得的。分辨率参数为c,为粗略。要获得相同的背景图像,请调用shadedrelief命令。还有其他的背景,比如黑夜中的地球。只画海岸线或国界是可能的。底图中,很多东西都有内置功能;因此,我们现在不再调用 matplotlib 函数,而是调用map对象的方法来创建经线和纬线。我还包括了绘制海岸线和国家边界的功能,但是把它们注释掉了。尝试取消它们的注释,并可能注释掉背景图像的绘制:
from mpl_toolkits.basemap import Basemap
fig = plt.figure(figsize=(11,10))
ax = fig.add_axes([0,0,1,1])
map = Basemap(projection='cyl', resolution='c')
map.shadedrelief()
#map.drawcoastlines()
#map.drawcountries()
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0],
color='grey')
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),
labels=[0,0,0,1], color='grey')
x, y = map(lons, lats)
map.scatter(x, y, color='IndianRed', s=aadata['fatal']*2);

我把坐标图的进一步修改留给你。但是,这里有第一步。由于本章的主要目标是贝叶斯分析,我们现在将继续数据分析示例。
气候变化-大气中的一氧化碳 2
有了贝叶斯分析,我们可以拟合任何模型;任何我们可以用常客统计或经典统计做的事情,我们都可以用贝叶斯统计做。在下一个例子中,我们将使用贝叶斯推理和频繁方法执行线性回归。由于我们已经介绍了模型创建和数据解析,在这个例子中,我们将会更快地完成一些事情。我们要用的数据是大约 1000 年跨度的大气 CO 2 和过去 40 年的增长率,然后用线性函数拟合过去 50-60 年的增长率。
获取数据
最* 50-60 年的数据来自国家海洋和大气管理局 ( 国家海洋和大气管理局)海洋站、地面站。可以在http://www.esrl.noaa.gov/gmd/ccgg/trends/global.html找到,这里可以下载两个数据集,增长率和年均值。数据表格的直接链接是增长率(即 gr)的ftp://aftp.cmdl.noaa.gov/products/trends/co2/co2_gr_gl.txt和全球*均值(本例中为年度)的。数据参考资料是美国国家海洋和大气管理局/ESRL 的 Ed Dlugokencky 和 Pieter Tans(hhtp://www . esrl . NOAA . gov/gmd/ccgg/trends/)。
要追溯到更远的地方,我们需要来自南极的冰芯样本,即大约 200 年前的 SIPLE 站冰芯。在http://cdiac.ornl.gov/trends/co2/siple.html,有更多的信息和数据的直接链接,http://cdiac.ornl.gov/ftp/trends/co2/siple2.013。数据参考为 1994 年的内福特、 A. 、 H .弗里德里希、 E .摩尔、 H .罗彻、 H .奥斯切格、 U .希根塔尔、 B .斯陶弗。历史 CO 2 记录来自锡普尔站冰芯。《趋势:全球变化数据概要》。美国能源部橡树岭国家实验室二氧化碳信息分析中心美国田纳西州橡树岭。对于更远的数据,1000 年前,我们使用来自法律穹顶的冰芯;更多信息可在http://cdiac.ornl.gov/trends/co2/lawdome.html找到,直接链接到数据,http://cdiac.ornl.gov/ftp/trends/co2/lawdome.smoothed.yr75,参考号为 D.M. Etheridge 、 L.P. Steele 、 R.L. Langenfelds 、 R.J. Francey 、j-m . Barnola和 V.I. Morgan 历史 CO 2 记录来自 Law Dome DE08、DE08-2 和 DSS 冰芯。《趋势:全球变化数据概要》。美国田纳西州橡树岭美国能源部橡树岭国家实验室二氧化碳信息分析中心、。**
**正如我们已经做了几次的那样,我们用 Pandas csv 阅读器读取数据:
co2_gr = pd.read_csv('data/co2_gr_gl.txt',
delim_whitespace=True,
skiprows=62,
names=['year', 'rate', 'err'])
co2_now = pd.read_csv('data/co2_annmean_gl.txt',
delim_whitespace=True,
skiprows=57,
names=['year', 'co2', 'err'])
co2_200 = pd.read_csv('data/siple2.013.dat',
delim_whitespace=True,
skiprows=36,
names=['depth', 'year', 'co2'])
co2_1000 = pd.read_csv('data/lawdome.smoothed.yr75.dat',
delim_whitespace=True,
skiprows=22,
names=['year', 'co2'])
SIPLE 冰芯文件的最后几行还有一些附加注释:
co2_200.tail()

我们通过对数据帧进行切片(不包括最后三行)来删除它们:
co2_200 = co2_200[:-3]
由于 Pandas csv 阅读器无法将最后三行解析为浮点数/整数,dtype不对;因为它也读取文本,它将使用最通用和可接受的数据类型。首先,检查所有数据集的数据类型,以确保我们不必修复任何其他数据集:
print( co2_200['year'].dtype, co2_1000['co2'].dtype,
co2_now['co2'].dtype, co2_gr['rate'].dtype)

不出所料,co2_200 DataFrame有错误的dtype。我们用 Pandas 的to_numeric功能来改变它,并检查它是否有效:
co2_200['year'] = pd.to_numeric(co2_200['year'])
co2_200['co2'] = pd.to_numeric(co2_200['co2'])
co2_200['co2'].dtype,co2_200['year'].dtype

64 位整数和浮点现在分别是年度和co2列的新数据类型,这正是我们需要的。
创建和采样模型
现在让我们想象一下这一切。如前所述,数据集可以分为两部分——一部分是绝对一氧化碳 2 浓度,另一部分是增长率。CO 2 浓度的单位表示为干燥空气中的摩尔分数(南极是地球上最干燥的地方之一);在这种情况下,分数以百万分之几表示:
fig, axs = plt.subplots(1,2,figsize=(10,4))
ax2 = axs[0]
ax2.errorbar(co2_now['year'], co2_now['co2'],
#yerr=co2_now['err'],
color='SteelBlue',
ls='None',
elinewidth=1.5,
capthick=1.5,
marker='.',
ms=6)
ax2.plot(co2_1000['year'], co2_1000['co2'],
color='Green',
ls='None',
marker='.',
ms=6)
ax2.plot(co2_200['year'], co2_200['co2'],
color='IndianRed',
ls='None',
marker='.',
ms=6)
ax2.axvline(1800, lw=2, color='Gray', dashes=(6,5))
ax2.axvline(co2_gr['year'][0], lw=2,
color='SteelBlue', dashes=(6,5))
print(co2_gr['year'][0])
ax2.legend(['Recent',
'LAW ice core',
'SIPLE ice core'],fontsize=15, loc=2)
labels = ax2.get_xticklabels()
plt.setp(labels, rotation=33, ha='right')
ax2.set_ylabel('CO$_2$ (ppm)')
ax2.set_xlabel('Year')
ax2.set_title('Past CO$_2$')
ax1 = axs[1]
ax1.errorbar(co2_gr['year'], co2_gr['rate'],
yerr=co2_gr['err'],
color='SteelBlue',
ls='None',
elinewidth=1.5,
capthick=1.5,
marker='.',
ms=8)
labels = ax1.get_xticklabels()
plt.setp(labels, rotation=33, ha='right')
ax1.set_ylabel('CO$_2$ growth (ppm/yr)')
ax1.set_xlabel('Year')
ax1.set_xlim((1957,2016))
ax1.set_title('Growth rate since 1960');

左侧显示绝对一氧化碳 2 水*的图显示冰芯与当前测量值非常吻合。第一条垂直虚线标志着工业革命(1800 年)的大致开始。尽管花了大约 50 年的时间才真正让蒸汽开始流动(双关语),但这表明曲线从哪里开始指数增长,并一直持续到今天。在引入燃煤蒸汽机后,从相当稳定的测量值到指数增长的急剧变化之间的这种相关性是人为气候变化的有力证据。第二条垂直线表示现代直接测量的起点,即 1959 年。它们与从冰芯中提取的历史记录非常吻合。在右边的图中,我们基本上放大了那个现代时期,从 1959 年到今天;然而,它显示了大气中二氧化碳 2 以 ppm 为单位的增长(如前所述)。数据似乎在不确定性中有一些分布(表明测量技术/仪器的升级)。出于好奇,我们先来看看这个:
_ = plt.hist(co2_gr['err'], bins=20)
plt.xlabel('Uncertainty')
plt.ylabel('Count');

事实上,它有两个峰值,最老的值最不确定。就像我们前面的例子一样,让我们首先将我们想要的值转换成 NumPy 数组:
x = co2_gr['year'].as_matrix()
y = co2_gr['rate'].as_matrix()
y_error = co2_gr['err'].as_matrix()
现在我们已经做到了这一点,我们用与飞机事故相同的方法定义我们的线性斜坡模型。它只是一个返回随机和确定变量的函数。在我们的例子中,它是一个线性函数,取斜率和截距;这一次,我们假设它们是正态分布的,这不是一个不合理的假设。正态分布最少需要两个参数mu和tau(来自 PyMC 文档),这就是高斯正态分布的位置和宽度:
def model(x, y):
slope = pymc.Normal('slope', 0.1, 1.)
intercept = pymc.Normal('intercept', -50., 10.)
@pymc.deterministic(plot=False)
def linear(x=x, slope=slope, intercept=intercept):
return x * slope + intercept
f = pymc.Normal('f', mu=linear,
tau=1.0/y_error, value=y, observed=True)
return locals()
像以前一样,我们通过调用 MCMC 来启动模型,然后从中取样 50 万次,老化 5 万次,这次减少 100 次。我建议您首先运行一些低的东西,例如 4,甚至省略它(即MDL.sample(5e5, 5e4)),绘制诊断图(如下所示),并比较结果:
MDL = pymc.MCMC(model(x,y))
MDL.sample(5e5, 5e4, 100)

你刚刚在不到一分钟的时间里运行了 50 万次迭代!由于变薄,采样后分析会更快一些:
y_min = MDL.stats()['linear']['quantiles'][2.5]
y_max = MDL.stats()['linear']['quantiles'][97.5]
y_fit = MDL.stats()['linear']['mean']
slope = MDL.stats()['slope']['mean']
slope_err = MDL.stats()['slope']['standard deviation']
intercept = MDL.stats()['intercept']['mean']
intercept_err = MDL.stats()['intercept']['standard deviation']
我们还应该为时间序列、后验分布和自相关创建图:
mcplt.plot(MDL)

轨迹、后验分布和自相关图看起来都非常好——清晰、明确的峰值和稳定的时间序列。截距变量也是如此:

在绘制结果之前,我还想让我们使用另一个包,statsmodels 普通最小二乘拟合。就像以前一样,我们导入公式包,这样我们就可以简单地给 Pandas 列名,我们希望找到它们之间的关系:
import statsmodels.formula.api as smf
from statsmodels.sandbox.regression.predstd import wls_prediction_std
ols_results = smf.ols("rate ~ year", co2_gr).fit()
然后,我们获取最佳拟合参数及其不确定性。这里,为了方便起见,我翻转了参数元组:
prstd, iv_l, iv_u = wls_prediction_std(ols_results)
ols_params = np.flipud(ols_results.params)
ols_err = np.flipud(np.diag(ols_results.cov_params())**.5)
我们现在可以比较两种方法,最小二乘法和贝叶斯模型拟合:
print('OLS: slope:{0:.3f}, intercept:{1:.2f}'.format(*ols_params))
print('Bay: slope:{0:.3f}, intercept:{1:.2f}'.format(slope, intercept))

贝叶斯方法似乎能找到仅次于普通最小二乘法的最佳参数估计值。这些参数足够接*,我们可以称之为均匀,但始终接*零——这是一个有趣的观察。当我们在下一章中研究机器学习算法时,我们将回到这个相同的数据集。我还想看看置信区间和可信区间。我们用以下方法对 OLS 和贝叶斯模型进行拟合:
ols_results.conf_int(alpha=0.05)

alpha=0.05给出置信区间水*,表示 95%置信区间(即1-0.05=0.95):
MDL.stats(['intercept','slope'])

因此 OLS 拟合的截距置信水*为[-66.5, -37.1],贝叶斯拟合的可信区间为[-50.6,-49.4]。这突出了两种方法的区别。现在让我们最后画出结果,我想在同一个图中画出两个拟合:
plt.figure(figsize=(10,6))
plt.title('Growth rate since 1960');
plt.errorbar(x,y,yerr=y_error,
color='SteelBlue', ls='None',
elinewidth=1.5, capthick=1.5,
marker='.', ms=8,
label='Observed')
plt.xlabel('Year')
plt.ylabel('CO$_2$ growth rate (ppm/yr)')
plt.plot(x, y_fit,
'k', lw=2, label='pymc')
plt.fill_between(x, y_min, y_max,
color='0.5', alpha=0.5,
label='Uncertainty')
plt.plot([x.min(), x.max()],
[ols_results.fittedvalues.min(), ols_results.fittedvalues.max()],
'r', dashes=(13,2), lw=1.5, label='OLS', zorder=32)
plt.legend(loc=2, numpoints=1);

这里我用 matplotlib 函数,fill_between,来表示函数的可信区间。数据的拟合看起来很好,看了几次之后,你可能会意识到它看起来像是被分成了两部分——大约在 1985 年被分成了两个带有一些偏移的线性部分。你的一个练习是测试这个假设:创建一个在某一年有一个中断的两个线性段的函数,然后尝试约束模型并比较结果。为什么会这样?也许他们改变了乐器;如果你还记得,这两部分数据也有不同的不确定性,所以系统误差的差异也不是完全不可能的。
总结
我们已经讨论了如何使用 Python 包 PyMC 用贝叶斯分析测试模型和假设。这是一个强大的包,它给出了更直观的结果,您可以看到参数是如何表征的。并非所有后验分布的形状都像高斯分布,但是对于约束良好的参数,迹线和自相关应该看起来相似。
在下一章中,我们将深入研究 Python 中可用的一些机器学习算法,并看看它们如何识别集群、对数据进行分类以及进行线性回归。在本章中,我们将把线性拟合与贝叶斯分析和 OLS 进行比较。我们将把集群的发现与我们在 第五章**集群中对宇宙中的星系所做的分析进行比较。**
七、监督和非监督学习
在过去的几十年里,社会上为各种目的收集的数据量大幅增加。机器学习是一种利用我们对数据的了解来理解所有这些数据的方法。在机器学习的广义图景中,计算机首先从给定的数据集(训练)中学习,并创建一个广义模型来表示它。使用这个模型,可以预测各种结果、结果和分组(类)。在本章中,我们将涵盖以下主题:
- 带有机器学习算法的线性回归
- 用机器学习算法进行聚类
- 特征选择—一种选择最重要特征的预处理方法
- 不同机器学习算法和核的分类
在开始之前,我将向您简要介绍机器学习以及我们将使用的软件包:Scikit-learn。
机器学习入门
机器学习主要有三类:有监督的、无监督的和强化的。给定一个具有输入x和输出y的简单数据集,监督学习是当x和y都有已知的标签时。该算法将x映射到y,经过训练后,可以以x为输入预测y值。与此相反,无监督学习是当只有x被标记并且算法为y本身找到一个标签时。强化学习是指计算机在学习时不需要将输入映射到结果,而是对输入做出反应。下棋或其他游戏的算法就是这样工作的。
他们试图在没有明确量化结果的情况下预测如何对输入做出反应,而是寻求强化;一个例子是连续地玩游戏,直到游戏结束而不出错(即获胜)。Python 中一个功能丰富且流行的机器学习包是 Scikit-learn。
科学学习
Scikit-learn 是 SciPy 工具包的一部分,它是 SciPy 的附属包。更多关于 SciPy 工具包的信息和可用工具包的列表可以在https://www.scipy.org/scikits.html找到。Scikit-learn 的第一个版本出现在 2007 年,但 2011 年第一个展示该包的出版物是 Python 中的机器学习,Pedregosa 等人,JMLR 12,第 2825-2830 页,2011 年。有关大量示例、文档和阅读,请参见 Scikit-learn 网页(http://scikit-learn.org)。这个包维护得很好,文档很好,覆盖面很广。
在这个简短的介绍之后,我们像前面几章一样——我们启动一个 Jupyter Notebook 并运行标准导入。
现在我们当然也想导入 Scikit-learn。下面的代码导入它,并打印出安装的 Scikit-learn 的版本号:
import sklearn
sklearn.__version__
接下来,我创建了一个函数,它删除了图或图网格中的右轴和顶轴。它在制作数字时很方便;能够以清晰和集中的方式呈现数据和分析结果非常重要。删除绘图中不必要的行是其中的一部分,也节省了文本空间。despine功能的名称灵感来源于优秀套装 Seaborn 中的等效功能,也可以帮你做出好看的身材(https://stanford.edu/~mwaskom/software/seaborn/):
def despine(axs):
# to be able to handle subplot grids
# it assumes the input is a list of
# axes instances, if it is not a list,
# it puts it in one
if type(axs) != type([]):
axs = [axs]
for ax in axs:
ax.yaxis.set_ticks_position('left')
ax.xaxis.set_ticks_position('bottom')
ax.spines['bottom'].set_position(('outward', 10))
ax.spines['left'].set_position(('outward', 10))
线性回归
Scikit-learn 内置了很多不同的线性回归模型,普通最小二乘 ( OLS )和最小绝对收缩和选择算子 ( LASSO )仅举两个例子。这两者之间的差异可以通过不同的损失函数来*似,损失函数是由机器学习算法处理的函数。在 LASSO 中,有一个远离拟合函数的附加惩罚,而 OLS 只是最小二乘方程。然而,常规仍然不同于我们前面介绍的 OLS;得出答案的底层算法是机器学习算法。其中一个常见的算法是梯度下降法。在这里,我们将采用前一章的气候数据,并用两种方法拟合一个线性函数,然后将 OLS 模型的结果与 PyMC 的贝叶斯推断( )第 6 章 、贝叶斯方法)和 statsmodels 的 OLS ( )第 4 章 、回归的结果进行比较。
气候数据
我们从过去 60 年的二氧化碳 2 增长率数据开始:
co2_gr = pd.read_csv('data/co2_gr_gl.txt',
delim_whitespace=True,
skiprows=62,
names=['year', 'rate', 'err'])
为了刷新你的记忆,我们再次绘制数据。我们现在使用despine功能移除两个轴:
fig, ax = plt.subplots(1,1)
ax.errorbar(co2_gr['year'], co2_gr['rate'],
yerr=co2_gr['err'],
ls='None',
elinewidth=1.5,
capthick=1.5,
marker='.',
ms=8)
despine(ax)
plt.minorticks_on()
labels = ax.get_xticklabels()
plt.setp(labels, rotation=33, ha='right')
ax.set_ylabel('CO$_2$ growth')
ax.set_xlabel('Year')
ax.set_xlim((1957,2016))
ax.set_title('CO$_2$ growth rate');

数据在这个范围的中间有很大的差异,大约在 1980 年到 2000 年之间。从某种意义上说,看起来两条在 1985 年跳跃的线可以被拟合。然而,不清楚为什么会这样;也许不确定性的两种分布与此有关?
在 Scikit-learn 中,学习是通过首先启动估计器来完成的,估计器是一个我们称之为 fit 方法来训练数据集的对象。这意味着我们首先要引入估计量。在这个例子中,我将向您展示两种不同的估计量以及它们产生的拟合结果。第一个是简单的线性模型,第二个是 LASSO 估计量。Scikit-learn 中有许多不同的线性模型:RANSAC、Theil-Sen,以及基于随机梯度下降 ( SGD )学习的线性模型,仅举几例。看完这个例子后,你应该看看另一个估算器,并尝试一下。我们首先导入将要使用的函数,以及cross_validation函数,我们将使用该函数将数据集分成两部分——训练和测试:
from sklearn.linear_model import LinearRegression, Lasso
from sklearn import cross_validation
因为我们希望能够验证我们的适合度,看看它有多好,所以我们没有使用所有的数据。我们使用cross_validation中的train_test_split函数将 25%的数据放入测试集中,将 75%的数据放入训练集中。玩转不同的价值观,看看最终的契合度如何变化。然后,我们将x和y值存储在适当的结构中。x值必须有一个额外的轴。这也可以通过x_train.reshape(-1,1)代码来完成;我们在这里做的方式给出了同样的效果。我们还创建了一个数组,稍后用我们知道的跨整个范围和更大范围的x值绘制拟合:
x_test, x_train, y_test, y_train = cross_validation.train_test_split(
co2_gr['year'], co2_gr['rate'],
test_size=0.75,
random_state=0)
X_train = x_train[:, np.newaxis]
X_test = x_test[:, np.newaxis]
line_x = np.array([1955, 2025])
在训练和测试数据分割中,我们还利用random_state参数,使得随机种子是相同的,并且我们通过多次运行它来获得训练和测试集的相同划分(为了精确的再现性)。我们现在准备训练数据,首先是简单的线性回归模型。为了在训练集上运行机器学习算法,我们首先创建一个估计器对象/类,然后我们简单地通过调用fit方法来训练模型,其中训练x和y值作为输入:
est_lin = LinearRegression()
est_lin.fit(X_train, y_train)
lin_pred = est_lin.predict(line_x.reshape(-1, 1))
在这里,我还添加了对我们之前创建的数组中预测的y值的计算,为了展示一种重构输入数组的替代方法,我使用了重塑方法。接下来是 LASSO 模型,我们做的完全一样,只是在创建模型对象时,我们现在可以选择给它额外的参数。alpha参数基本上是这个模型区别于前面简单线性回归模型的地方。如果设置为零,模型将与线性模型相同。LASSO 模型 alpha 输入修改损失函数,默认值为1。尝试不同的值,虽然 0 不是很好的选择,因为没有损失函数的附加惩罚,模型就不能运行:
est_lasso = Lasso(alpha=0.7)
est_lasso.fit(X_train, y_train)
lasso_pred = est_lasso.predict(line_x.reshape(2, 1))
为了查看结果,我们首先打印出系数、均方误差或误差(均方残差)和方差得分的估计值。方差分数是我们创建的 Scikit-learn 模型(估计器)中的一种方法。虽然方差得分为 1 意味着它能够完美地预测值,但得分为 0 意味着没有预测值,并且变量之间没有(线性)关系。系数通过estimator.coeff_和estimator.intercept_访问。为了得到均方误差,我们简单地取预测值和观测值之间的差值,用estimator.predict(x)计算,其中x是x值,你要预测y值。这应该根据测试数据而不是训练集来计算。我们首先创建一个函数来计算并打印相关诊断:
def printstuff(estimator, A, b):
name = estimator.__str__().split('(')[0]
print('+'*6, name, '+'*6)
print('Slope: {0:.3f} Intercept:{1:.2f} '.format(
estimator.coef_[0], estimator.intercept_))
print("Mean squared residuals: {0:.2f}".format(
np.mean((estimator.predict(A) - b)**2)) )
print('Variance score: {0:.2f}'.format(
estimator.score(A, b)) )
使用该函数,我们现在可以打印拟合结果,给出估计斜率和截距、均方残差和方差得分:
printstuff(est_lin, X_test, y_test)
printstuff(est_lasso, X_test, y_test)

LASSO 模型估计斜率和截距的较低值,但给线性回归模型一个类似的均方残差和方差得分。数据的差异太大,无法断定它们中是否有任何一个产生了更可靠的估计。当然,我们可以将所有这些结果与数据一起绘制成图表:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(X_train, y_train, marker='s',
label='Train', color='IndianRed')
ax.scatter(X_test, y_test, label='Test',
color='SteelBlue')
ax.plot(line_x, lin_pred, color='Green',
label='Linreg', lw=2)
ax.plot(line_x, lasso_pred, color='Coral',
dashes=(5,4), label='LASSO', lw=2)
ax.set_xlabel('Year')
ax.set_ylabel('CO$_2$ growth rate')
ax.legend(loc=2, fontsize=10, numpoints=1)
despine(ax)
plt.minorticks_on()
ax.locator_params(axis='x', nbins=5)
ax.locator_params(axis='y', nbins=7)
ax.set_xlim(1950,2030)
ax.set_title('CO$_2$ growth rate');

拟合略有不同,但绝对在彼此的不确定性范围内。数据点的分布相对较大。正方形表示训练集,圆圈表示测试(验证)集。然而,在这个范围之外进行外推会预测出明显不同的值。
我们可以计算出 R 2 分数,就像以前用经典 OLS 回归一样:
from sklearn.metrics import r2_score
r2_lin = r2_score(co2_gr['rate'],
est_lin.predict(
co2_gr['year'].reshape(-1,1)))
r2_lasso = r2_score(co2_gr['rate'],
est_lasso.predict(
co2_gr['year'].reshape(-1,1)))
print('LinearSVC: {0:.2f}\nLASSO:\
\t {1:.2f}'.format(r2_lin, r2_lasso))

R 2 值相对较高,尽管数据点分布明显且数据大小有限。
用贝叶斯分析和 OLS 检验
我们将很快用线性模型与 statsmodels 的 OLS 回归和贝叶斯推断进行比较。贝叶斯推断和 OLS 拟合与 第六章**贝叶斯方法相同,这里重复一个小版本:
import pymc
x = co2_gr['year'].as_matrix()
y = co2_gr['rate'].as_matrix()
y_error = co2_gr['err'].as_matrix()
def model(x, y):
slope = pymc.Normal('slope', 0.1, 1.)
intercept = pymc.Normal('intercept', -50., 10.)
@pymc.deterministic(plot=False)
def linear(x=x, slope=slope, intercept=intercept):
return x * slope + intercept
f = pymc.Normal('f', mu=linear, tau=1.0/y_error, value=y, observed=True)
return locals()
MDL = pymc.MCMC(model(x,y))
MDL.sample(5e5, 5e4, 100)
y_fit = MDL.stats()['linear']['mean']
slope = MDL.stats()['slope']['mean']
intercept = MDL.stats()['intercept']['mean']
对于 OLS 模型,我们再次使用公式框架来表示变量之间的关系:
import statsmodels.formula.api as smf
from statsmodels.sandbox.regression.predstd import wls_prediction_std
ols_results = smf.ols("rate ~ year", co2_gr).fit()
ols_params = np.flipud(ols_results.params)
现在我们已经得到了这三种方法的结果,让我们打印出它们的斜率和截距:
print(' Slope Intercept \nML : \
{0:.3f} {1:.3f} \nOLS: {2:.3f} \
{3:.3f} \nBay: {4:.3f} \
{5:.3f}'.format(est_lin.coef_[0], est_lin.intercept_,
ols_params[0],ols_params[1],
slope, intercept) )

虽然总体结果相似,但贝叶斯推断估计的绝对值似乎低于其他方法。我们现在可以将这些不同的估计值与数据一起可视化:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.errorbar(x, y, yerr=y_error, ls='None',
elinewidth=1.5, capthick=1.5,
marker='.', ms=8, label='Observed')
ax.set_xlabel('Year')
ax.set_ylabel('CO$_2$ growth rate')
ax.plot([x.min(), x.max()],
[ols_results.fittedvalues.min(),
ols_results.fittedvalues.max()],
lw=1.5, label='OLS',
dashes=(13,5))
ax.plot(x, y_fit, lw=1.5,
label='pymc')
ax.plot([x.min(), x.max()],
est_lin.predict([[x.min(), ], [x.max(), ]]),
label='Scikit-learn', lw=1.5)
despine(ax)
ax.locator_params(axis='x', nbins=7)
ax.locator_params(axis='y', nbins=4)
ax.set_xlim((1955,2018))
ax.legend(loc=2, numpoints=1)
ax.set_title('CO$_2$ growth rate');

这可能看起来是一个非常小的差别;然而,如果我们用这些不同的结果来推断 30 年、50 年甚至 100 年后的未来,这些将产生明显不同的结果。这个例子向你展示了在 Scikit-learn 中尝试不同的方法和模型是多么简单。也可以自己创造。接下来,我们将看看 Scikit-learn 中的一个集群识别模型,DBSCAN。
聚类
在这个例子中,我们将在 Scikit-learn 中看到一个名为 DBSCAN 的聚类发现算法。DBSCAN 代表有噪声的应用程序的基于密度的空间聚类,是一种有利于点组的聚类算法,可以将这些组(聚类)之外的点识别为噪声(异常值)。与线性机器学习方法一样,Scikit-learn 使使用它变得非常容易。我们首先从 第五章**聚类中读取数据,带有 Pandas 的read_pickle功能:
TABLE_FILE = 'data/test.pick'
mycat = pd.read_pickle(TABLE_FILE)
与前面的数据集一样,为了刷新您的内存,我们绘制了数据。它包含了映射的附*宇宙的一部分,也就是具有确定位置(方向和与我们的距离)的星系。和以前一样,我们用 Z 来缩放颜色,如数据表所示:
fig,ax = plt.subplots(1,2, figsize=(10,2.5))
plt.subplot(121)
plt.scatter(mycat['Y'], -1*mycat['X'],
s=8,
color=plt.cm.viridis_r(
10**(mycat.Zmag-mycat.Zmag.max()) ),
edgecolor='None')
plt.xlabel('Y (Mpc)'); plt.ylabel('X (Mpc)')
ax = plt.gca()
despine(ax)
ax.locator_params(axis='x', nbins=5)
ax.locator_params(axis='y', nbins=5)
plt.axis('equal')
plt.subplot(122)
c_arr = 10**(mycat.Zmag-mycat.Zmag.max())
plt.scatter(-1*mycat['X'],mycat['Z'],
s=8,
color=plt.cm.viridis_r(c_arr),
edgecolor='None')
lstyle = dict(lw=1.5, color='k', dashes=(6,4))
ax = plt.gca()
despine(ax)
ax.locator_params(axis='x', nbins=5)
ax.locator_params(axis='y', nbins=5)
plt.plot([0,150], [0,80], **lstyle)
plt.plot([0,150], [0,45], **lstyle)
plt.plot([0,-25], [0,80], **lstyle)
plt.plot([0,-25], [0,45], **lstyle)
plt.xlabel('X (Mpc)'); plt.ylabel('Z (Mpc)')
plt.subplots_adjust(wspace=0.3)
plt.axis('equal');
plt.ylim((-10,110));

这些数据跨越了极大的尺度和给定方向上的许多星系。为了开始使用机器学习进行聚类查找,我们从 Scikit-learn 导入相关对象:
from sklearn.cluster import DBSCAN
from sklearn import metrics
from sklearn.preprocessing import StandardScaler
第一个导入是简单的 DBSCAN 方法,第二个导入是度量模块,我们可以用它来计算聚类算法的各种统计数据。StandardScaler类只是对数据进行缩放,如 第 5 章**聚类。接下来,我们设置输入数据;每一行都应该包含缩放后的要素/点的坐标。然后,该缩放坐标列表被输入到数据库扫描方法中:
A = np.array([mycat['Y'], -1*mycat['X'], mycat['Z']]).T
A_scaled = StandardScaler().fit_transform(A)
dbout = DBSCAN(eps=0.15, min_samples=5).fit(A_scaled)
DBSCAN 对象是用几个参数实例化的。eps参数根据至少min_samples必须位于的距离来限制聚类的大小(记住,以比例单位)。dbout对象现在存储拟合的所有结果。dbout.labels_数组包含每个点的所有标签;不在任何簇中的点被赋予一个-1标签。让我们看看是否有:
(dbout.labels_==-1).any()
它打印出True,所以我们有噪音。输出对象还有一个重要的方法就是core_sample_indices_。它包含核心样本,每个集群都是从这些样本扩展和形成的。这几乎就像 k-means 聚类中的质心位置。我们现在为核心样本索引创建一个布尔数组,并在结果中创建一个唯一标签列表。根据 Scikit-learn 文档,这是推荐的方法。
csmask = np.zeros_like(dbout.labels_, dtype=bool)
csmask[dbout.core_sample_indices_] = True
unique_labels = set(dbout.labels_)
没有集群的真正标签,衡量集群发现的成功是很棘手的。通常,您会计算轮廓分数,这是一个根据质心和同一聚类和附*聚类中的样本之间的距离进行缩放的分数。轮廓得分越高,聚类发现在定义聚类时就越好。然而,这假设了以一个点为中心的簇,而不是丝状结构。为了向您展示如何计算和解释剪影分数,我们在本例中对其进行了介绍,但请记住,在这种情况下,它可能不是一种有代表性的方法。我们将计算轮廓分数,并打印出找到的簇的数量。请记住,标签数组还包含噪声标签(即-1):
n_clusters = len(set(labels)) - [0,1][-1 in labels]
print('Estimated number of clusters: %d' % n_clusters)
print("Silhouette Coefficient: %0.3f"
% metrics.silhouette_score(A_scaled, dbout.labels_))

接*零的轮廓得分值表示聚类重叠。现在我们将绘制所有结果,并检查它看起来像什么。我试图通过增加核心样本的标记大小和减少非核心样本的大小来绘制不同的核心样本。我还对颜色进行了洗牌,试图让不同的集群在它们的邻居面前脱颖而出:
colors = plt.cm.viridis(np.linspace(0.3, 1, len(unique_labels)))
np.random.seed(0)
np.random.shuffle(colors)
for lbl, col in zip(unique_labels, colors):
if lbl == -1:
# Black used for noise.
col = 'DarkRed'; m1=m2= '+'; s = 10; a = 0.5
else:
m1='.';m2='.'; s=5; a=1
cmmask = (dbout.labels_ == lbl)
xy = A[cmmask & csmask]
plt.scatter(xy[:, 0], xy[:, 1], color=col,
marker=m1,
s=s+1,
alpha=a)
xy = A[cmmask & ~csmask]
plt.scatter(xy[:, 0], xy[:, 1], color=col,
marker=m2,
s=s-2,
alpha=a)
despine(plt.gca())
noiseArtist = plt.Line2D((0,1),(0,0),
color='DarkRed',
marker='+',
linestyle='',
ms=4, mew=1,
alpha=0.7)
clusterArtist = plt.Line2D((0,1),(0,0),
color='k',
marker='.',
linestyle='',
ms=4, mew=1)
plt.legend([noiseArtist, clusterArtist],
['Outliers','Clusters'],
numpoints=1)
plt.title('A slice of the Universe')
plt.xlabel('X [Mpc]')
plt.ylabel('Y [Mpc]');

该算法还可以找到噪声(异常值),在这里用红色十字标出。尝试调整核心和非核心样本的显示,使它们更加突出。此外,您应该尝试 DBSCAN 方法的不同参数,看看结果会受到什么影响。另一件事是回到 第 5 章**聚类,将 66 个聚类放入我们在那里用相同数据集尝试的分层聚类算法中进行比较。
种子分类
我们现在来看三组主要的分类(学习)模型:支持向量机 ( SVM )、最*邻和随机森林。SVM 简单地将空间划分为和两个区域,由一个边界隔开。可以允许边界具有不同的形状,例如,有线性边界或二次边界。最*邻分类识别 k 个最*邻,并根据 k 个最*邻属于哪个类别对当前数据点进行分类。随机森林分类器是一种决策树学习方法,简单来说,它根据给定的训练数据创建规则,以便能够对新数据进行分类。一行中的一组 if 语句赋予了它决策树的名称。
我们将要使用的数据来自 UCI 机器学习知识库(李奇曼,m .(2013)-http://archive.ics.uci.edu/ml。(加州欧文:加州大学信息与计算机科学学院)。该数据集包含三种不同类型小麦籽粒的若干测量属性(M. Charytanowicz,J. Niewczas,P. Kulczycki,P.A. Kowalski,s .卢卡西克,S. Zak,)X 射线图像特征分析的完全梯度聚类算法,载于:生物医学中的信息技术,Ewa Pietka,亚采克·卡瓦(eds。),斯普林格-弗拉格,柏林-海德堡,2010 年,第 15-24 页。).
我们想创建一个分类器,如果我们测量种子的特定参数,它可以告诉我们它是什么类型的种子。对于数据集,提供了列的描述,这也可以在数据集的 UCI 网页上找到。共有八列,七列用于参数,一列用于种子的已知类型(即标签)。我已经为此创建了一个文本文件;如果您运行的是基于 Linux 的系统,您可以使用 Jupyter magic 列出内容:
%%bash
less data/seeds.desc

型式
如果你运行的是微软的 Windows,可以使用more命令,也就是more data/seeds.desc,给你同样的输出,但是在一个弹出窗口,不太方便但是还是有用的。
现在我们知道有哪些列了,我们可以把它读入 Pandas 数据框:
seeds = pd.read_csv('data/seeds_dataset.txt',
delim_whitespace=True,
names=['A', 'P', 'C', 'lkern', 'wkern',
'asym', 'lgro', 'gr'])
一如既往,列出所读内容:
seeds.head()

可视化数据
我们可以对数据集中的所有七个参数运行整个分类过程。这在计算上是昂贵的,并且当增加数据量时,成本增加得非常快。为了第一次尝试只选择对分类重要的属性,我想直观地检查不同类型颗粒的属性值的分布。为此,我们首先为不同的组创建一个选择过滤器:
gr1 = seeds.gr == 1
gr2 = seeds.gr == 2
gr3 = seeds.gr == 3
为了只绘制相关参数,我们还创建了一个我们想要查看的参数列表(也就是说,不是颗粒的类型):
pars = ['A','C','P','asym','lgro','lkern','wkern']
借助 Pandas 内置的histogram功能,我们可以绘制出每组的属性。我添加了一些额外的命令,使图形看起来更好看,更整洁:
axes = seeds[pars][gr1].hist(figsize=(8,6))
despine(list(axes.flatten()))
_ = [ax.grid() for ax in list(axes.flatten())]
_ = [ax.locator_params(axis='x', nbins=4) for ax in
list(axes.flatten())]
_ = [ax.locator_params(axis='y', nbins=2) for ax in
list(axes.flatten())]
plt.subplots_adjust(wspace=0.5, hspace=0.7)
我们再一次使用despine功能使图更清晰。前面的代码将绘制第一个组gr1的所有属性。直方图将显示这些值是如何分布的:

使用选择过滤器,可以轻松绘制其他组:
axes = seeds[pars][gr2].hist(figsize=(8,6))
despine(list(axes.flatten()))
_ = [ax.grid() for ax in list(axes.flatten())]
_ = [ax.locator_params(axis='x', nbins=4) for ax in
list(axes.flatten())]
_ = [ax.locator_params(axis='y', nbins=2) for ax in
list(axes.flatten())]
plt.subplots_adjust(wspace=0.5, hspace=0.7)
通过绘制所有的组,我们可以看到属性值的分布是如何不同的。我们试图找出不同群体之间最大的差异:

axes = seeds[pars][gr3].hist(figsize=(8,6))
despine(list(axes.flatten()))
_ = [ax.grid() for ax in list(axes.flatten())]
_ = [ax.locator_params(axis='x', nbins=5) for ax in
list(axes.flatten())]
_ = [ax.locator_params(axis='y', nbins=2) for ax in
list(axes.flatten())]
plt.subplots_adjust(wspace=0.5, hspace=0.7)
绘制完最后一个组后,我们可以查看每个属性,并找到每个组的分布分开的属性。这样,我们就可以用它来区分各种群体(种子的类型),也就是把它们分类:

由此,我认为孔隙率和凹槽长度是很好的参数,因为它们的定义相当明确,并且对于三组来说,它们是分开的峰。为了核实这一点,我们让他们互相攻击。我们还想标记不同种类的谷物:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(seeds.P[gr1], seeds.lgro[gr1],
color='LightCoral')
ax.scatter(seeds.P[gr2], seeds.lgro[gr2],
color='SteelBlue', marker='s')
ax.scatter(seeds.P[gr3], seeds.lgro[gr3],
color='Green', marker='<');
ax.text(seeds.P[gr1].mean(), seeds.lgro[gr1].mean(),
'1', bbox=dict(color='w', alpha=0.7,
boxstyle="Round"))
ax.text(seeds.P[gr2].mean(), seeds.lgro[gr2].mean(),
'2', bbox=dict(color='w', alpha=0.7,
boxstyle="Round"))
ax.text(seeds.P[gr3].mean(), seeds.lgro[gr3].mean(),
'3', bbox=dict(color='w', alpha=0.7,
boxstyle="Round"))
ax.set_xlabel('Porosity')
ax.set_ylabel('Groove length')
ax.set_title('Seed parameters')
despine(ax)
plt.minorticks_on()
ax.locator_params(axis='x', nbins=5)
ax.locator_params(axis='y', nbins=4)
ax.set_xlim(11.8,18)
ax.set_ylim(3.8,7.1);

现在,这只是其中两个参数;我们还有几个。但是,从这一点来看,似乎最难把第 1 组和第 3 组,也就是圆和三角形分开。
特征选择
Scikit-learn 内置了几种确定最佳观察参数的方法。这有时被称为特征选择,它试图确定哪些参数彼此之间具有最大的差异,并且最适合将各种组描述为完全不同的组。在这里,我们使用一个我们可以给出一个数字的地方, K (不要与 K-means 中的 K 混淆),这决定了它应该选择多少个特征中最好的。
首先,我们将种子表存储为一个矩阵,一个 NumPy 数组,然后我们将数据和标签分开:
X_raw = seeds.as_matrix()
X_pre, labels = X_raw[:,:-1], X_raw[:,-1]
现在我们可以导入选择算法并在数据上运行它。请注意,我们还导入了 chi2 估计器,并将其提供给选择对象。这意味着卡方最小化将用于确定最佳参数:
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
X_best = SelectKBest(chi2, k=2).fit_transform(X_pre, labels)
它现在已经选择了两列;为了检查哪些列,我们打印出选择的前几行和原始数据:
X_best[:5]

seeds.head()

面积(A)和不对称(asym)系数是根据该选择算法可以使用的两个最佳参数。在我们通过机器学习算法之一进行分类之前,我们再次绘制所有数据,但这一次特征是由算法选择的:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(seeds.A[gr1], seeds.asym[gr1],
color='LightCoral')
ax.text(seeds.A[gr1].mean(), seeds.asym[gr1].mean(),
'1', bbox=dict(color='w', alpha=0.7,
boxstyle="Round"))
ax.scatter(seeds.A[gr2], seeds.asym[gr2],
color='SteelBlue',
marker='s')
ax.text(seeds.A[gr2].mean(), seeds.asym[gr2].mean(),
'2', bbox=dict(color='w', alpha=0.7,
boxstyle="Round"))
ax.scatter(seeds.A[gr3], seeds.asym[gr3],
color='Green',
marker='<')
ax.text(seeds.A[gr3].mean(), seeds.asym[gr3].mean(),
'3', bbox=dict(color='w', alpha=0.7,
boxstyle="Round"))
ax.set_xlabel('Area')
ax.set_ylabel('Asymmetry')
ax.set_title('Seed parameters')
despine(ax)
plt.minorticks_on()
ax.locator_params(axis='x', nbins=5)
ax.locator_params(axis='y', nbins=3)
ax.set_xlim(9.6,22)
ax.set_ylim(-0.6,10);

与之前类似的情节相比,点更分散,也许圆圈和三角形更分离,重叠更少——这很难评估。
对数据进行分类
为了开始对数据进行分类,我们首先准备一些东西。我们引入了 SVM 模和 K *邻以及随机森林估计。在 SVM 模块中是支持向量分类 ( 支持向量机)估计器,SVM 的主要估计器。SVC 可以用不同的内核运行;我们将讨论线性、径向基函数和多项式核。在我们运行它们之前,我将对它们做一个简短的解释。
为了可视化分类,我想绘制边界,我们将使用等高线来实现这一点。为此,我们需要创建一个点网格,并用我们训练好的分类器对它们进行评估:
from sklearn import svm
from sklearn.neighbors import KNeighborsClassifier
res = 0.01
#X, y = X_best[::2], labels[::2]
X, y = X_best, labels
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, res),
np.arange(y_min, y_max, res))
在这里,我们编写一个函数来绘制结果。为了绘制边界,使用了我们之前创建的x和y网格。它被传递给估计者的predict(xxyy)方法。这里,输入是估计器、机器学习分类模型的输出(即不同的支持向量机、最*邻和随机森林)以及图的标题。等高线图绘制边界,您可以将ax.contour更改为ax.contourf以获得填充的等高线。现在我们有了一个处理可视化的功能,我们可以专注于测试不同的模型(称为内核):
def plot_results(clf, title):
fig = plt.figure()
ax = fig.add_subplot(111)
plt.subplots_adjust(wspace=0.2, hspace=0.4)
xxyy = np.vstack((xx.flatten(), yy.flatten())).T
Z = clf.predict(xxyy)
Z = Z.reshape(xx.shape)
ax.contour(xx, yy, Z,
colors=['Green','LightCoral', 'SteelBlue'],
alpha=0.7, zorder=-1)
ax.scatter(seeds.A[gr1], seeds.asym[gr1],
color='LightCoral')
ax.scatter(seeds.A[gr2], seeds.asym[gr2],
color='SteelBlue', marker='s')
ax.scatter(seeds.A[gr3], seeds.asym[gr3],
color='Green', marker='<')
ax.text(seeds.A[gr1].mean(), seeds.asym[gr1].mean(),
'1', bbox=dict(color='w', alpha=0.7,
boxstyle="Round"))
ax.text(seeds.A[gr2].mean(), seeds.asym[gr2].mean(),
'2', bbox=dict(color='w', alpha=0.7,
boxstyle="Round"))
ax.text(seeds.A[gr3].mean(), seeds.asym[gr3].mean(),
'3', bbox=dict(color='w', alpha=0.7,
boxstyle="Round"))
despine(ax)
plt.minorticks_on()
ax.locator_params(axis='x', nbins=5)
ax.locator_params(axis='y', nbins=3)
ax.set_xlabel('Area')
ax.set_ylabel('Asymmetry')
ax.set_title(title, size=10)
ax.set_xlim(9.6,22)
ax.set_ylim(-0.6,10);
奇异值分解线性核
SVC 中一个简单的核是线性核,它假设线性边界。作为输入,取C,决定对噪声数据敏感度的参数;对于非常嘈杂的数据,您可以降低该参数。为了得到线性核,对我们的数据运行分类,并用我们的函数绘制结果,我们运行以下内容:
svc = svm.SVC(kernel='linear', C=1.).fit(X, y)
plot_results(svc, 'SVC-Linear')

正如你所看到的,边界是线性的,它合理地将不同的点分成与研究人员创建的点相对应的组。
支持向量机径向基函数
下一个内核径向基函数 ( 径向基函数)是在没有输入内核给 SVC 调用的情况下使用的内核;它基本上是一个高斯核。结果是一个由高斯线性组合而成的核(区域)。除了C参数,这里还可以给出伽马参数;它是高斯宽度的倒数,因此它给出了边界的陡度:
rbf_svc = svm.SVC(kernel='rbf', gamma=0.4, C=1.).fit(X, y)
plot_results(rbf_svc, 'SVC-Radial Basis Function')

在这里,边界更加*滑。中心的边界与线性内核大致相同,但远离密集区域时会有所不同。
奇异值分解多项式
我们将讨论的最后一个 SVC 内核是多项式,它听起来就是多项式。作为输入,它需要以下程度:
poly_svc = svm.SVC(kernel='poly', degree=3, C=1.).fit(X, y)
plot_results(poly_svc, 'SVC-Polynomial')

因此边界用多项式来表示。我建议你试着把度数换成别的,看看边界会发生什么。
K-最*邻
现在我们使用 K *邻。作为输入,这需要比较邻居的权重和数量。默认权重是假设所有n_neighbors附*点的权重一致;将权重关键字更改为distance假设权重随着距离的增加而减少:
knn = KNeighborsClassifier(weights = 'uniform', n_neighbors=5).fit(X, y)
plot_results(knn, 'k-Nearest Neighbours')

这类似于一些支持向量机内核,但适用于更小的变化。尝试将权重更改为“距离”,同时更改n_neighbors参数,看看结果如何变化。把n_neighbors换成大于 30 的东西会怎么样;它几乎完全复制了其他哪个分类器?
随机森林
作为最后一个示例分类器,我们使用随机森林方法。你可以把它想象成 第五章**聚类中进行层次聚类的树形图;但是,每个分支都是对这里的数据进行分类的规则。我们给对象三个输入:max_depth、n_estimators和max_features。第一个,max_depth决定每棵决策树应该走多远,n_estimators给出森林中有多少棵决策树。这并不是真正的直观,所以要展示给大家是什么,先放n_estimators=1,运行代码,看输出。然后,换成另一个更高的数字,看看新的输出:
rfc = RandomForestClassifier(
max_depth=3,
n_estimators=10,
max_features='auto').fit(X, y)
plot_results(rfc, 'Random Forest Classifier')

森林中的树的数量是用来建立分类器的决策树的数量。结果显示,随机森林分类器很简单,但能够对相当复杂的问题进行分类。SVC 和线性核也很简单,但是你可以想象随机森林分类器如何能够用相当低的深度和很少的估计量来分类更复杂的问题。我建议你花点时间,摆弄一下所有分类器的输入参数,看看结果如何变化。
选择你的分类器
前面的例子向您展示了 SVC(具有各种核)、kNN 和随机森林分类器的不同结果。但是,什么时候应该用一个而不是另一个呢?一般来说,尝试给定问题的所有方法。SVC 的主要优势在这些例子中得到强调——它非常通用,有许多不同的内核。决策树像随机森林一样,有变得过于复杂从而过度拟合数据的风险。同样在前面的例子中突出显示的是,kNN 非常适合于边界明显不是线性的分类,正如你可以在前面的 kNN 图像的结果中看到的。还有其他内核和分类器。例如,对文章进行了 179 个分类器的比较,我们需要数百个分类器来解决现实世界的分类问题吗?(费尔南德斯-德尔加多,2014,JMLR,15,3133-3181)。本研究涵盖了所有常见的量词。然而,如前所述,重要的是在您的数据上尝试各种分类器,看看什么有效。
总结
在这一章中,我们研究了各种机器学习方法来进行回归、聚类和分类。我们将使用机器学习工具的线性回归与贝叶斯推断和标准 ols 中的相同问题进行了比较。此外,我们将聚类机器学习算法 DBSCAN 的结果与我们在 第 5 章**聚类中获得的结果进行了比较。最后,我们查看了 Scikit-learn 中可用的几种分类算法,以及它们在同一数据集上的表现。
有了 UCI 机器学习知识库,找到实践数据并不难。我建议你访问http://archive.ics.uci.edu/ml并寻找一个数据集来尝试我们在这里经历的任何新事物。
八、时间序列分析
有时我们要分析的数据是以固定时间间隔测量的变量;当我们有这样的数据时,我们谈论的是一个时间序列。更具体地说,在时间序列的每一步,都有不止一个可能的结果,每一步的部分结果是随机的,可能只取决于时间上的前几步。由于这些原因,简单的线性回归不起作用。在时间序列分析中,我们建立模型来解释时间上的变化,这有时被称为纵向分析。
本章涵盖时间序列分析中的以下主题:
- 时间序列建模,它的有用性,以及 Pandas 如何处理数据
- 时间序列中的各种常见模式
- *稳性的概念以及如何测试和使数据*稳
- 重采样、*滑和计算滚动统计
- 如何模拟已知的变化并做出短期预测
我们从一些关于时间序列的更多信息开始,分析它可以给出什么样的见解。
简介
时间序列分析在几种情况下很重要;例如,它可以用来描述变量在时间上的变化,通过对已知变化建模来预测或预测,然后在时间上向前外推这些变化,或者评估某些外部刺激如何影响某个时间序列变量。
有三种主要的建模和预测方法:
- 外推,这是我们在本章重点关注的时间序列分析。这种方法简单地使用历史数据来构建模型,然后用于预测未来。
- 判断性,例如,在决策中使用,在需要结合判断或信念(即概率)的地方很常见。当不存在历史时间序列数据时,可能会出现这种情况。
- 计量经济学,这是一种基于回归的方法,通常试图量化某些变量/事件如何以及在多大程度上影响时间序列的结果。顾名思义,这有时用于经济研究。
还有其他方法,如天真的方法(使用最后的一个或多个历史值作为预测);然而,我们将集中讨论对时间序列分析最有用的方法——外推法。大多数行业在其工作流程中的某个点使用时间序列分析。两个明显的例子如下:
- 零售:某个产品应该库存多少,会卖出多少?
- 金融:管理资产,给定前几个月的股票数据,明天股票会涨还是跌?
型式
这里重要的一点是,我们试图模拟部分随机的变化,因此有些事情是不可能模拟的。在时间序列完全随机化的情况下,最好的预测和模型只是一个*均值和分布。
一个时间序列数据集可以看作是在固定时间间隔内的一系列 y 值,因此没有 x 轴值是数据的一部分。这可以表达如下:

这里,集合中的每个 y 只是某个时间点的每个值。了解了这些内容后,您就可以使用 Pandas 和 statsmodels 学习 Python 中的时间序列分析了。
像往常一样,打开 Jupyter,启动一个新的笔记本,并输入默认导入。我添加了一些导入,因为我们将在整个章节中使用它们。除了默认导入(在第 1 章、交易工具中描述)之外,额外导入如下:
from pandas.io import data, wb
import scipy.stats as st
from statsmodels.tsa import stattools as stt
from statsmodels import tsa
import statsmodels.api as smapi
在这里,就像前面提到的,如果你有 Pandas 版本,它被分成一个单独的包,你必须用pandas_datareader代替pandas.io。此外,我将使用我们之前定义的despine()函数,所以请确保您将它放在单元格中。如您所见,我将使用的主要包是 statsmodels 它有一些很好的功能,使时间序列分析变得更加容易。statsmodels 开发人员正在致力于升级时间序列分析,以包含更多高级功能,因此请密切关注更新。作为分析的开始,我将阅读第一个数据,并了解 Pandas 时间序列对象所具有的一些独特的方法和特征。
Pandas 与时间序列数据
在 Pandas 中,时间序列数据有一定的数据类型。这是一个普通的 Pandas 数据帧或系列,其中索引是一列datetime对象。Pandas 必须是这种类型的物体,才能识别它是日期,才能理解如何处理日期。为了向您展示它是如何工作的,让我们读入一个时间序列数据集。
我们读到的第一个数据是 1988 年 1 月 1 日至 1991 年 12 月 31 日美国达拉斯附*费希尔河的日*均气温。数据可以多种格式从 DataMarket(https://datamarket.com/data/set/235d/)下载,也可以从http://FTP . uni-bayreuth . de/math/stat lib/datasets/hipel-mcleod获取。这里,我有 CSV 格式的数据。数据来自时间序列数据库(https://datamarket.com/data/list/?q=provider:tsdl)并起源于 Hipel 和 McLeod (1994)。
数据有两列:第一列是日期,第二列是当天的*均测量温度。要读入日期,我们需要给 Pandas CSV 数据读取器一个日期解析函数,它以字符串格式获取日期并将其转换为datetime对象,就像我们在前几章中讨论的那样(例如,第 6 章、贝叶斯方法)。打开数据文件,可以看到日期的格式是年-月-日。因此,我们为此创建了一个日期解析函数:
dateparse = lambda d: pd.datetime.strptime(d, '%Y-%m-%d')
有了这个日期解析函数,我们现在可以像以前一样读入数据:
temp = pd.read_csv('data/mean-daily-temperature-fisher-river.csv',
parse_dates=['Date'],
index_col='Date',
date_parser=dateparse)
由于文件中的列是命名的,即数据的第一行显示日期和温度,我们让读者知道索引列——作为索引的列——是具有日期名称的列。我们还告诉它用日期解析函数解析这个列,这也是我们给出的。查看前几个条目,我们可以看到它是一个完整的 DataFrame 对象:
temp.head()

为了使我们的分析更容易,并且因为我们使用的是单变量数据集,所以我们只能从中提取 Pandas 系列。这只是数据框中的列。下面,我们得到一个 Series 对象:
temp = temp.iloc[:,0]
Series 对象仍然以日期作为索引;事实上,打印出index属性表明我们确实将日期解析为索引:
temp.index

dtype='datetime64[ns]'值表示我们以非常高的精度将索引存储为日期。像往常一样,我们首先可视化数据,看看我们在处理什么:
temp.plot(lw=1.5)
despine(plt.gca())
plt.gcf().autofmt_xdate()
plt.ylabel('Temperature');
像往常一样,这些线简单地调用 Pandas 系列方法图,改变线宽(lw),然后得到当前轴(plt.gca()),发送到despine()功能,然后设置 y 标签:

如你所见,数据中有一个很强的模式。因为它每年都在重复,所以它是一种特殊类型的周期性模式,一种季节性模式。为了检查一些统计数据,我们调用我们拥有的 Series 对象的describe()方法:
temp.describe()

在打印的统计数据中,我们可以看到最低温度为 -35 ,相当冷。我们还看到有很多测量,足够我们使用,在时间序列分析中,拥有足够的数据非常重要。当我们没有足够的数据时,我们不得不采用更复杂的模型,如引言中描述的判断模型。我们现在有时间序列数据可以使用,我们将从研究如何在 Pandas 中分割这些物体开始。
索引和切片
Pandas 中的时间序列可以用许多不同的方式进行索引和切片,但不能用整数索引。我们的索引是日期,记得吗?因此,要获得 1988 年的所有数据,我们只需将该年作为一个字符串进行索引。在下面的代码中,我们以 1988 年为索引,然后绘制值:
temp['1988'].plot(lw=1.5)
despine(plt.gca())
plt.gcf().autofmt_xdate()
plt.minorticks_off()
plt.ylabel('Temperature');

该图显示了 1988 年气温的变化,从接*-30 度到大约+25 度,然后在 10 月下旬回到零下。正如您可能怀疑的那样,您也可以通过给出年和月来索引整个月:
temp['1988-01'].plot(ls='dotted', marker='.')
despine(plt.gca())
plt.gcf().autofmt_xdate()
plt.ylabel('Temperature');

一个月内的变化,这里是一月,相当大,从最小到最大大约 20 度。您也可以使用两个索引进行切片,就像普通数组一样。试着切片做几个月的剧情,比如temp['1988-06': '1989-06']。
正如您可以用普通数组过滤掉某些值一样,您也可以过滤掉时间序列中的某些值。为了只在温度严格低于-25 时获取值,您可以像往常一样——进行一个返回布尔数组的比较:
temp[temp < -25].head()

为了完成这一部分,绘制一个图表,每个年份绘制在相同的图中,如下所示:

从该数据的第一个图中可以强烈暗示,它具有季节性成分,对于这样的数据集(室外温度)来说一点也不奇怪。不过,尽量自己切片制作这个剧情。还可以加上 x 轴上的月份吗?也许试着只做一个月,但要做一整年,比如四月或五月。我们将介绍的下一步是如何操纵和计算时间序列的各种估计。
重采样、*滑和其他估计
另一种可视化和对数据进行一些初始分析的有用方法是重采样、*滑和其他滚动估计。重采样时,需要向函数传递一个 frequency 关键字。这是整数和字母的组合,其中字母表示整数的类型。给你一个想法,一些频率说明符如下:
B,营业日,或D,日历日
W,每周
M,日历月结束或MS为开始
Q,日历季度结束或QS开始
A,日历年结束,或AS为开始
H,每小时,T,每分钟
其中大多数可以通过在说明符的开头添加B来修改,将其更改为业务(月、季度、年等),还有一些其他的关键字/描述符可以在 Pandas 文档中找到。现在让我们在下面的例子中尝试其中的一些。因为这一章包含了几个真实世界的数据示例,我们用它们来突出不同的东西,所以请随意摆弄数据分析。要按年份对数据进行重新采样,我们只需将A传递给resample()方法:
temp.resample('A').head()

在这里,数值基本上是一年的*均值,标签在年底。现在让我们用一些重采样选项绘制一个图,清楚地显示这些年发生的变化。
注
resample()方法的工作原理可能会在即将发行的《Pandas》中有所改变。如果您运行的版本高于 0.17.1,您应该参考 Pandas 文档了解更多信息。
首先,我们绘制原始数据,然后将重新采样的数据绘制为每周一次,最后绘制为每年一次。但是,如果我们按年给出频率描述符A,那就是年底了。如果能显示出一年与一年之间的变化就好了,这里的中心点不是在年初,而是在年中。为了实现这一点,我们使用AS描述符,给我们在一年内重新采样的数据加上开头的标签,然后用loffset='178 D'关键字添加大约半年的偏移量:
temp.plot(lw=1.5, color='SkyBlue')
temp.resample('W').plot(lw=1, color='Green')
temp.resample('AS', loffset='178 D').plot(color='k')
plt.ylim(-50,30)
plt.ylabel('Temperature')
plt.title('Fisher River Mean Temperature')
plt.legend(['Raw', 'Binned Weekly', 'Binned Yearly'], loc=3)
despine(plt.gca());

为了使图例更加清晰可见,我简单地用plt.ylim()功能添加了一些空间。现在试着绘制一个类似下图的图,在原始数据上绘制一个月和六个月的重采样图:

有时我们想计算某个东西的滚动值。虽然重采样看起来像滚动*均值,但在 Pandas 中有一个特定的函数。我们可以做的一件事是将时间序列上的滚动*均值与它周围区域的最小值和最大值相结合,以突出变化,得到一个漂亮的数字。在下图中,我们在 60 的窗口中绘制了滚动*均值,这意味着如果以天为单位对数据进行采样,它将是 60 天。此外,我们已经告诉滚动装置位于窗口的中心。为了从原始数据中获得最小值和最大值,我们重新采样到月份,取最小值和最大值,并填充它们之间的图:
temp.plot(lw=1, alpha=0.5)
pd.rolling_mean(temp, center=True, window=60).plot(color='Green')
plt.fill_between(temp.resample('M', label='left',
loffset='15 D').index,
y1=temp.resample('M', how='max').values,
y2=temp.resample('M', how='min').values,
color='0.85')
plt.gcf().autofmt_xdate()
plt.ylabel('Temperature')
despine(plt.gca())
plt.title('Fisher River Temperature');

这已经看起来很好了;滚动*均值再现了大规模的温度年复一年的变化。虽然这是一种时间序列分析,可能足以进行一阶分析和处理数据,但我们将研究一些更复杂的方法来模拟变化。您可以计算其他滚动值,例如协方差:
pd.rolling_cov(temp, center=True, window=10).plot(color='Green')
despine(plt.gca());

在这种情况下,协方差在 10 天的窗口内,在一年的转变前后似乎非常高。另一个要计算的滚动值是方差:
pd.rolling_var(temp, center=True, window=14).plot(color='Green')
despine(plt.gca());

正如我们将在后面讨论的,分析随时间变化的方差对于时间序列分析非常重要。尝试改变协方差和方差的窗口,看看它们有什么不同。
我们之前计算了滚动*均值,发现它似乎遵循了数据的大规模逐年变化。让我们计算残差,从原始数据中减去滚动*均值:
temp_residual = temp-pd.rolling_mean(temp, center=True, window=60)
将残差可视化,我们可以看到它仍然有一些周期性。为了分析时间序列,我们需要数据包含尽可能少的这些大规模模式:
temp_residual.plot(lw=1.5, color='Coral')
despine(plt.gca())
plt.gcf().autofmt_xdate()
plt.title('Residuals')
plt.ylabel('Temperature');

时间序列分析主要基于这样一个事实,即当前值可能只取决于以前的几个值,并且程度不同。所以要分析数据,我们需要去掉这些。这自然将我们引向下一个话题——*稳性。在下一节中,我们将讨论这一点,向您展示如何测试您的数据是否是静态的,以及如果不是静态的,使其成为静态的几种方法。
*稳性
大多数时间序列建模依赖于数据的*稳性。*稳时间序列最简单的定义是,它的大部分统计特征都是随时间大致恒定的。对于统计特征,*均值、方差和自相关最常被提及。为了使这成为事实,我们不能有任何趋势,也就是说,数据不能随着时间单调增加。也不能有长周期的起伏。如果这些事情中的任何一个是真的,*均值会随着时间而改变,方差也会改变。还有其他更复杂的数学测试,如下面的(扩充的)迪基-富勒测试。我们将重点放在这个测试上,因为它在 statsmodels 中很方便。
事实是,在进行时间序列分析时,我们首先需要确保数据是*稳的。在 Python 中检查数据是否稳定的最简单方法是做一个增强的 Dickey-Fuller 测试。这是一个统计测试,用于估计数据集是否稳定。statsmodels 包有一个功能,可以对此进行测试并发回诊断结果。测试值(我们称之为 ADF 值)需要与 1、5 和 10%的临界值进行比较。如果 ADF 值在 5%时低于临界值,并且 p 值(是的,统计 p 值)很小,大约小于 0.05,我们可以拒绝数据在 95%置信水*下不稳定的零假设。
为了更容易判断结果是否显示时间序列是*稳的,让我们编写一个小函数来运行该函数并总结输出:
def is_stationary(df, maxlag=15, autolag=None, regression='ct'):
"""Run the Augmented Dickey-Fuller test from Statsmodels
and print output.
"""
outpt = stt.adfuller(df,maxlag=maxlag, autolag=autolag,
regression=regression)
print('adf\t\t {0:.3f}'.format(outpt[0]))
print('p\t\t {0:.3g}'.format(outpt[1]))
print('crit. val.\t 1%: {0:.3f}, \
5%: {1:.3f}, 10%: {2:.3f}'.format(outpt[4]["1%"],
outpt[4]["5%"], outpt[4]["10%"]))
print('stationary?\t {0}'.format(['true', 'false']\
[outpt[0]>outpt[4]['5%']]))
return outpt
我们现在准备测试数据集的*稳性,所以让我们来读一个。
该数据集可从数据市场(https://datamarket.com/data/set/22n4/)下载。这些数据来自时间序列数据库(https://datamarket.com/data/list/?q=provider:tsdl)并起源于亚伯拉罕和莱多尔特(1983)。它显示了魁北克从 1960 年到 1968 年的汽车月销量。和以前一样,我们使用日期解析器直接获取 Pandas 时间序列数据帧:
carsales = pd.read_csv('data/monthly-car-sales\
-in-quebec-1960.csv',
parse_dates=['Month'],
index_col='Month',
date_parser=lambda d: \
pd.datetime.strptime(d, '%Y-%m'))
要转到 Pandas 系列对象而不是数据帧,我们要做和以前一样的事情:
carsales = carsales.iloc[:,0]
绘制数据集显示了一些有趣的事情。数据有一些强烈的季节性趋势,即每年内的周期性模式。它也有一个缓慢的上升趋势,但更多的是在后来:
plt.plot(carsales)
despine(plt.gca())
plt.gcf().autofmt_xdate()
plt.xlim('1960','1969')
plt.xlabel('Year')
plt.ylabel('Sales')
plt.title('Monthly Car Sales');

我们现在可以运行我们的小包装来测试它是否是信纸:
is_stationary(carsales);

不是的!嗯,这不是一个巨大的惊喜,因为它有所有这些模式。这将带我们进入下一部分,在这里我们将看到时间序列组成的各种模式和组件。
图案和组件
在时间序列中,主要有四种不同的模式或组成部分:
- 趋势:数值随时间缓慢但显著的变化
- 季节:周期性的变化,周期不到一年
- 周期:周期变化,周期超过一年
- 随机:随机的成分;纯随机数据的最佳模型是*均值,假设它具有对应于正态分布的分布
因此,在我们能够分析我们的数据之前,它需要是稳定的,为了使它稳定,我们需要注意模式:趋势、季节和周期。您将执行的分析将基于不符合这些模式的时间序列的一部分,随机分量是模型不确定性的一部分。
分解成分
一种处理各种成分并使时间序列*稳的方法是分解。有不同的方法来识别组件;在 statsmodels 中,有一个函数可以一次性分解所有的模型。让我们导入它,并运行我们的时间序列:
from statsmodels.tsa.seasonal import seasonal_decompose
carsales_decomp = seasonal_decompose(carsales, freq=12)
该函数以频率为输入;这与季节有关,因此输入您认为数据具有的季节周期。在这种情况下,我以12为例,通过查看数据,它看起来像是一个年度周期。返回的对象包含 Pandas 系列的几个属性,所以让我们从返回的对象中提取它们:
carsales_trend = carsales_decomp.trend
carsales_seasonal = carsales_decomp.seasonal
carsales_residual = carsales_decomp.resid
为了可视化这些不同的组件,我们将它们绘制成一个图形:
def change_plot(ax):
despine(ax)
ax.locator_params(axis='y', nbins=5)
plt.setp(ax.get_xticklabels(), rotation=90, ha='center')
plt.figure(figsize=(9,4.5))
plt.subplot(221)
plt.plot(carsales, color='Green')
change_plot(plt.gca())
plt.title('Sales', color='Green')
xl = plt.xlim()
yl = plt.ylim()
plt.subplot(222)
plt.plot(carsales.index,carsales_trend,
color='Coral')
change_plot(plt.gca())
plt.title('Trend', color='Coral')
plt.gca().yaxis.tick_right()
plt.gca().yaxis.set_label_position("right")
plt.xlim(xl)
plt.ylim(yl)
plt.subplot(223)
plt.plot(carsales.index,carsales_seasonal,
color='SteelBlue')
change_plot(plt.gca())
plt.gca().xaxis.tick_top()
plt.gca().xaxis.set_major_formatter(plt.NullFormatter())
plt.xlabel('Seasonality', color='SteelBlue', labelpad=-20)
plt.xlim(xl)
plt.ylim((-8000,8000))
plt.subplot(224)
plt.plot(carsales.index,carsales_residual,
color='IndianRed')
change_plot(plt.gca())
plt.xlim(xl)
plt.gca().yaxis.tick_right()
plt.gca().yaxis.set_label_position("right")
plt.gca().xaxis.tick_top()
plt.gca().xaxis.set_major_formatter(plt.NullFormatter())
plt.ylim((-8000,8000))
plt.xlabel('Residuals', color='IndianRed', labelpad=-20)
plt.tight_layout()
plt.subplots_adjust(hspace=0.55)

在这里,我们可以看到所有不同的组成部分——原始销售额显示在左上角,而数据的总体趋势显示在右上角。识别出的季节性在左下方,将这两者分开后的残差在右下方。季节性成分有多个周期性峰值。这种季节性因素非常有趣,因为它占了销售年度变化的大部分。让我们通过绘制去趋势数据(即从原始销售数据中减去的趋势)和季节性变化(一年内)来进一步了解它:
fig = plt.figure(figsize=(7,1.5) )
ax1 = fig.add_axes([0.1,0.1,0.6,0.9])
ax1.plot(carsales-carsales_trend,
color='Green', label='Detrended data')
ax1.plot(carsales_seasonal,
color='Coral', label='Seasonal component')
kwrds=dict(lw=1.5, color='0.6', alpha=0.8)
d1 = pd.datetime(1960,9,1)
dd = pd.Timedelta('365 Days')
[ax1.axvline(d1+dd*i, dashes=(3,5),**kwrds) for i in range(9)]
d2 = pd.datetime(1960,5,1)
[ax1.axvline(d2+dd*i, dashes=(2,2),**kwrds) for i in range(9)]
ax1.set_ylim((-12000,10000))
ax1.locator_params(axis='y', nbins=4)
ax1.set_xlabel('Year')
ax1.set_title('Sales Seasonality')
ax1.set_ylabel('Sales')
ax1.legend(loc=0, ncol=2, frameon=True);
ax2 = fig.add_axes([0.8,0.1,0.4,0.9])
ax2.plot(carsales_seasonal['1960':'1960'],
color='Coral', label='Seasonal component')
ax2.set_ylim((-12000,10000))
[ax2.axvline(d1+dd*i, dashes=(3,5),**kwrds) for i in range(1)]
d2 = pd.datetime(1960,5,1)
[ax2.axvline(d2+dd*i, dashes=(2,2),**kwrds) for i in range(1)]
despine([ax1, ax2])
import matplotlib.dates as mpldates
yrsfmt = mpldates.DateFormatter('%b')
ax2.xaxis.set_major_formatter(yrsfmt)
labels = ax2.get_xticklabels()
plt.setp(labels, rotation=90);

如你所见,季节性因素非常重要。虽然这对于这个数据集来说是显而易见的,但它是时间序列分析的一个良好开端,因为您可以将数据分解成这样的片段,并且它提供了对正在发生的事情的丰富见解。让我们将这一季节性因素保留一年:
carsales_seasonal_component = carsales_seasonal['1960'].values
减去趋势和季节性后剩下的残差现在应该是*稳的,对吗?它们看起来像是静止的。让我们检查一下包装函数。为此,我们首先需要摆脱NaN值:
carsales_residual.dropna(inplace=True)
is_stationary(carsales_residual.dropna());

它现在是静止的;这意味着我们可以继续分析时间序列并开始建模。当试图在残差模型中重新包含季节和趋势成分时,当前版本的 statsmodels 中可能存在一些错误。正因为如此,我们试图用不同的方式来做这件事。
在我们开始制作时间序列模型之前,我想多看看残差。我们可以利用前几章学到的知识,检查残差是否正态分布。首先,我们绘制值的直方图,并过度拟合高斯概率密度分布:
loc, shape = st.norm.fit(carsales_residual)
x=range(-3000,3000)
y = st.norm.pdf(x, loc, shape)
n, bins, patches = plt.hist(carsales_residual, bins=20, normed=True)
plt.plot(x,y, color='Coral')
despine(plt.gca())
plt.title('Residuals')
plt.xlabel('Value'); plt.ylabel('Counts');

我们使用的另一个检查是概率图,所以我们也在这个上面运行它。但是,我们不会像以前那样让它绘制图形,而是自己动手。为此,我们捕捉probplot()的输出,不给予它任何轴或绘图功能作为输入。得到变量后,我们只需用给定的系数画出它们和一条线:
(osm,osr), (slope, intercept, r) = st.probplot(carsales_residual,
dist='norm', fit=True)
line_func = lambda x: slope*x + intercept
plt.plot(osm,osr,
'.', label='Data', color='Coral')
plt.plot(osm, line_func(osm),
color='SteelBlue',
dashes=(20,5), label='Fit')
plt.xlabel('Quantiles'); plt.ylabel('Ordered Values')
despine(plt.gca())
plt.text(1, -14, 'R$^2$={0:.3f}'.format(r))
plt.title('Probability Plot')
plt.legend(loc='best', numpoints=4, handlelength=4);

残差看起来像是正态分布——高的 R 2 值也表明它具有统计学意义。现在我们已经检查了自动分解的残差,我们可以继续下一个使数据稳定的方法。
差异
通过求差,我们简单地取两个相邻值之间的差。要做到这一点,Pandas 有一个方便的diff()方法。下图显示了数据移动一个周期后的差异:
carsales.diff(1).plot(label='1 period', title='Carsales')
plt.legend(loc='best')
despine(plt.gca())

请记住,这是基于原始数据——具有强烈趋势和季节性的数据。虽然这种趋势已经消失,但似乎一些季节性仍然存在;但是,让我们检查一下这是否是一个*稳的时间序列:
is_stationary(carsales.diff(1).dropna())

不,p 值高于 0.05,ADF 值至少高于 5%,所以我们不能拒绝零假设。让我们再次运行diff(),但是同时运行1和12周期(即 12 个月,一年):
carsales.diff(1).plot(label='1 period', title='Carsales',
dashes=(15,5))
carsales.diff(1).diff(12).plot(label='1 and 12 period(s)',
color='Coral')
plt.legend(loc='best')
despine(plt.gca())
plt.xlabel('Date')

由此很难判断它是多还是少是静止的。我们必须对输出运行包装器来检查:
is_stationary(carsales.diff(1).diff(12).dropna());

这样好多了;我们似乎已经摆脱了季节性和趋势性的成分。
我鼓励您使用第一个示例数据集,并检查我们在本节中介绍的一些内容。各种周期性/季节性成分是如何分解的?你必须用什么价值观才能让它发挥作用?在下一节中,我们将介绍时间序列的一些通用模型,以及它们在 statsmodels 中的使用方式。
时间序列模型
建模时间序列会变得非常复杂;在这里,我们将逐一介绍一些最常用的模型,并解释它们背后的一些想法。我们将从自回归模型开始,继续移动*均模型,最后是组合自回归综合移动*均模型。首先,导入 statsmodel 时间序列模型框架:
from statsmodels.tsa.arima_model import ARIMA
ARIMA函数以 Pandas 时间序列和模型参数作为输入,并发回一个模型对象。为了使用分解和差分相结合的方法来使时间序列*稳,我首先去除了 statsmodels 函数分解出的季节分量,然后取第一个差值并检查它是否*稳:
is_stationary((carsales-carsales_seasonal).diff(1).dropna());

它是*稳的 ADF 值低于 5%临界值,p 值小于 0.05。我将把这些步骤保存在单独的数据结构中,用于建模:
ts = carsales-carsales_seasonal
tsdiff = ts.diff(1)
我们现在准备检查各种型号。
自回归–AR
对于自回归模型,我们使用前面几个步骤的值来建模一个值。重要的参数是模型要使用多少个先前的步骤;这里,这是参数 p 。有多种方法可以预先估计 p 参数,但有时运行不同值的模型并检查可用数据的正常拟合程序是在参数之间进行选择的好方法。AR(p)(p的 AR)模型可以用简单的方式表达如下:
*
这里 j 从 1 运行到 p 。注意,它显示当前值 y i 是 p 先前值和随机/不确定性贡献 ε 的函数。也就是说,每个值都有一个我们可以建模的部分和一个我们想要最小化的随机贡献。此外,重要的是要注意 a 参数-这些参数/系数(有时也称为权重)可以通过拟合进行调整,以使模型更好地再现值。他们基本上控制了每个先前的值对模型的重要性。虽然 p 参数很重要,但是将模型拟合到可用的历史数据来调整参数也很重要。
要运行这个模型,我们只需将order变量作为 ARIMA 函数的输入。它应该是一个有三个值的元组——第一个值给出了 p 值,第二个给出了中间值 d 求数据差的次数,第三个也是最后一个输入是 q 求*均模型的移动。要运行带有 p=1 和 d=1 的 AR 模型,然后将其与数据进行拟合,我们只需在一个单元格中运行以下内容:
model = ARIMA(ts, order=(1, 1, 0))
arres = model.fit()
现在我们已经拟合了前面方程中显示的参数。为了可视化拟合,在拟合结果对象中有一个方便的函数,在这种情况下为arres:
arres.plot_predict(start='1961-12-01', end='1970-01-01', alpha=0.10)
plt.legend(loc='upper left')
despine(plt.gca())
plt.xlabel('Year')
print(arres.aic, arres.bic)

您可以清楚地看到,当 AR 模型预测现有的历史数据时,它是如何移动这些数据的;重要的是超越样本的预测。90%置信区间也绘制在灰色区域;预测主要是前几个值。此后,它收敛到一个恒定的趋势。在前面代码的最后一行,我还打印了 AIC 和 BIC ( print(arres.aic, arres.bic)),这是阿凯克信息准则和贝叶斯信息准则。它们都被用来评估一个模型相对于另一个模型有多好。与其他模型相比,这些值越低,模型就越好(相对而言)。在这种情况下,打印输出为1870.3331809826666和1878.35166749。
虽然这个模型并不完美,但它给了你一些对未来销量的估计。在下一节中,我们将创建一个移动*均模型。
移动*均线–毫安
在移动*均模型中,计算先前 q 值的*均值(称为),并以与自回归模型相同的方式对随机贡献进行建模,假设当前值是 q 先前值的*均值的函数,以及用参数/系数修改的小随机变化,这里为 b 。这可以表达如下:

这里, j 从 1 到 q 运行,之前值的*均值可以表示如下:

该模型本质上假设当前值由 q 先前值的*均值很好地建模,因此它被称为移动*均模型。
就像之前一样,我们用ARIMA函数初始化模型,但是这次用不同的输入,这样我们就可以使用 MA 模型:
model = ARIMA(ts, order=(0, 1, 1))
mares = model.fit()
绘制这张图并打印 AIC 和 BIC 表明,它实际上是预测未来价值的更好模型:
mares.plot_predict(start='1961-12-01', end='1970-01-01', alpha=0.10)
plt.legend(loc='upper left')
despine(plt.gca())
plt.xlabel('Year')
print(mares.aic, mares.bic)

AIC 和 BIC 是1853.0753124033156和1861.09379891,略低于 AR 模型。从外观上看,这种模型更善于将历史数据的大致趋势外推至未来。在下一节中,我们将制作一个 AR 和 MA 模型的复合模型。在此之前,我将向您展示两种选择 p 和 q 参数的方法。
选择 p 和 q
AR 和 MA 模型的 p 和 q 参数应根据所得模型与历史数据的拟合程度进行选择。应该在结果之间比较 AIC 或 BIC,并且应该选择具有最低值的结果。
自动功能
statsmodels 包当然为此提供了便利功能。我们将看到如何在数据上运行:
tsa.stattools.arma_order_select_ic(tsdiff.dropna(), max_ar=2, max_ma=2,
ic='aic')

它打印的输出显示我们应该使用 AR(1)和 MA(1)模型,作为输入的是tsdiff, d 也应该是1。
(部分)自相关函数
虽然方便的自动功能做得很好,但它运行各种参数输入的整个建模。估算 p 和 q 的另一种方法是绘制自相关函数 ( ACF )和偏自相关函数 ( PACF )。为此,我们首先计算它们:
acf = stt.acf(tsdiff.dropna(), nlags=10)
pacf = stt.pacf(tsdiff.dropna(), nlags=10)
在此之后,我们可以绘制两者以及临界极限。对于*稳时间序列,值的限制应为(部分)自相关的 5%,即 1.96/(N-d) ,其中 N 是数据点的数量, d 是您对数据求差的次数。让我们来画出 ACF 和 PACF。在这个图中,我还绘制了自动程序建议的值,1和1:
fig, (ax1, ax2) = plt.subplots(1,2, figsize=(8,2))
ax1.axhline(y=0,color='gray')
ax1.axhline(y=-1.96/ (len(ts)-1)**.5,
linestyle='--',color='gray')
ax1.axhline(y=1.96/ (len(ts)-1) **.5,
linestyle='--',color='gray')
ax1.axvline(x=1,ls=':',color='gray')
ax1.plot(acf)
ax1.set_title('ACF')
ax2.axhline(y=0,color='gray')
ax2.axhline(y=-1.96/ (len(ts)-1) **.5,
linestyle='--',color='gray')
ax2.axhline(y=1.96/ (len(ts)-1) **.5,
linestyle='--',color='gray')
ax2.axvline(x=1,ls=':',color='gray')
ax2.plot(pacf)
ax2.set_title('PACF')
despine([ax1,ax2])

如你所见,两条曲线都在 p=1 和 q=1 时越过临界极限。这也是你如何估计 p 和 q 参数的方法,通过检查它们在哪里越过临界值并稳定在极限内。现在我们准备运行复合模型。
自回归综合移动*均线–ARIMA
我们要看的最后一个模型是自回归综合移动*均线 ( ARIMA )模型。顾名思义,它是前面两种模式的结合。它与之前运行的功能相同,但是对于输入元组,我们使用所有的函数。为了说明一些功能,我将使用与前面型号不同的 d 值来运行它:
model = ARIMA(ts, order=(1, 0, 1))
arimares = model.fit()
下图显示了对历史值的预测和估计:
arimares.plot_predict(start='1961-12-01', end='1970-01-01', alpha=0.10)
plt.legend(loc='upper left')
despine(plt.gca())
plt.xlabel('Year')
print(arimares.aic, arimares.bic)

AIC 和 BIC 是1880.0061559512924和1890.73468086,比之前的 MA 车型略差。我建议您尝试更改输入的 p 、 d 、 q 参数,或许更改为 (1,1,1) 。用不同的值玩一玩,把数据再差一次,看看会发生什么。
总结
在本章中,我们用 Pandas 和 statsmodels 查看了 Python 中时间序列分析的许多有趣的方面,它们如何处理数据,以及一些可用的基本操作函数。我们还研究了*稳性的概念,如何测试你的时间序列,以及如何将非*稳序列转化为*稳序列。您还发现了时间序列可以构建的各种模式和组成部分,最后,我们介绍了如何创建 ARIMA 模型,并根据以前的历史数据预测未来的值。
这一章结束了这本书。我们已经介绍了许多不同的分析技术和一般的统计知识,以及如何在 Python 中使用它们来帮助您。有了这本书里的知识,你可以开始探索数据,任何种类的数据。除了这些章节,还有一个附录。在附录、更多关于 Jupyter 笔记本和 matplotlib 样式中,我将查看 Jupyter 笔记本的提示和扩展(插件)。我还将提供一些进一步资源的链接,包括各种数据存储库,供您查找要下载的数据,并创建您自己的假设进行测试。
如前所述,花时间处理数据,尝试不同的算法并比较结果是很重要的。希望大家也已经意识到,数据分析中的很多工作都是在实际将分析方法/算法应用到数据中,用结果做一个图之前进行的,做好能显示所有结果而不显得臃肿的图是非常困难的。一般来说,对于现实世界的数据分析,这个过程是困难的,当完成时,你必须以一种其他人都能理解的简单方式呈现出来。根据本书教授的内容,你应该能够对报告中几乎任何数据和有吸引力的数字进行可靠的分析,从而清楚地突出你的结果。*
九、附录 a:关于 Jupyter 笔记本和 matplotlib 样式的更多信息
在本附录中,我们将介绍在 Jupyter Notebook 中进行数据分析和编译报告时可以帮助您的几件事。本附录涵盖以下主题:
- Jupyter 通用笔记本提示和技巧:
- 加快工作流程的有用键盘快捷键
- 编辑文本单元格的标记语法简介
- 其他一些有用的提示
- Jupyter 笔记本扩展
- Matplotlib 风格从一开始就非常适合绘图
- 有用的资源,如数据存储库、Python 包等
各种提示和技巧对于 Python 中的数据分析来说并不重要,但是让工作流变得更好、更容易,就像在项目中停下来一样,这是非常有用的。让我们直接开始,仔细看看 Jupyter Notebook 的一些优点。
Jupyter 笔记本电脑
Jupyter Notebook 是一个交互式网络应用程序,它从编程语言内核发送/接收数据。在这本书里,我们用 Python 工作过;在 Jupyter Notebook 中也可以用其他几种编程语言工作。笔记本格式支持它所称的检查点——当您保存时,它将创建一个检查点,并且您总是可以从菜单中的文件 | 恢复到检查点回滚到上一个检查点。
Jupyter Notebook 解决的最重要的问题之一是它提供了您的数据分析会话的完整记录;任何人都需要这个记录和数据文件来重现你的分析。除了代码之外,记录可能包含(结构化的)文本、图像、视频、等式,甚至交互式小部件。笔记本可以编译成其他更容易共享的格式,如 PDF 和 HTML。除了这些,还可以用扩展来扩展 Jupyter Notebook 的功能。在看了一些更有用的键盘快捷键之后,我们将浏览其中的一些扩展。
有用的键盘快捷键
首先,我想介绍几个最有用的键盘快捷键。Jupyter Notebook 中键盘快捷键的一般方法非常简单。它有两个主模式:命令和编辑模式。正如您可能已经怀疑的那样,编辑模式是当您编辑单元格中的文本时,命令模式是当您在笔记本中运行命令时。可用的键盘快捷键当然会反映在您所处的模式中。但是,在这两种模式下, Shift + 回车 将运行当前单元格,Ctrl+S将保存笔记本(并创建检查点)。
命令模式快捷方式
进入命令模式后,按下 Ctrl + M 或 Esc 可使用以下键盘快捷键:
- B / A :这创建了一个新的单元格,T7】BT9】下方或T11】AT13】上方的当前单元格。**
- X/C/V**:这个剪切、复制、粘贴单元格,就像你在其他程序中习惯的一样。将单元格粘贴到此处会将其粘贴到当前单元格的下方。
- DD**:这将删除一个单元格。
- 【Z】:此操作撤销删除。
- 【L】:此处显示行号。当获取引用代码中行号的错误消息时,这尤其有用。
- M :这将当前单元格转换为一个标记单元格。
- Shift + M :这将当前单元格与下面的单元格合并。
- 【O】:切换显示/隐藏单元格正下方显示的输出。
- H :这里显示了所有的键盘快捷键。
- 进入 :进入所选单元格的编辑模式。
编辑模式快捷方式
当您处于编辑模式时,通过在选择您想要编辑的单元格时按下 进入 ,您可以执行以下操作:
- 制表符 :缩进,或制表符补全;也就是说,开始键入命令时,tab 将列出名称空间中存在的可用命令/方法/对象/变量。
- Ctrl+Shift+-**:这会在当前行拆分一个单元格
- Ctrl + A :选择单元格内的所有内容
- Ctrl + Z :这是撤销
- Ctrl+Shift+Z**:这是重做
- Esc :进入命令模式
如上所述,这些是一些可用的键盘快捷键。在我看来,这些是最有用的。如果想查看全部,进入命令模式,按 H 。
*## 标记细胞
在通过选择现有单元格并按下【M】创建的标记单元格中,您可以执行以下功能:
-
通过在文本前面加一个散列和空格“
#”来创建标题。 -
像在任何文本编辑器中一样,键入普通文本。您可以按如下方式设置文本的样式:
- 斜体通过用星星包围文本,即,text
- 加粗通过在文字周围加上两颗星,也就是,**文字* *
-
通过在每个项目符号前加一个星号来制作项目符号列表,如下所示:
* Item1 * Item 2 * Sub-item1 -
通过键入
[your link text](http://your-url.com)包括一个网址。 -
包含带有
的图像。 -
通过在列表中的每一项前面加上一个数字来制作一个编号列表。
如果您将单元格转换为标记文字,但想将其转换回代码单元格,只需按 Ctrl + M 或 Esc 进入命令模式,然后按 Y 转换所选单元格。
Markdown 语法非常广泛,Jupyter Notebook 遵循与 GitHub 相同的语法;因此,关于可以做什么的更多信息,请参见https://help . github . com/articles/basic-writing-and-formatting-syntax/。本附录随附的笔记本中也显示了一些可能性。
笔记本 Python 扩展
Jupyter 的功能可以通过扩展进行扩展。一些扩展仅依赖于 Jupyter,而另一些依赖于外部库和软件。其中一些灵感来自 CodeMirror 在线 JavaScript 编辑器(https://codemirror.net)的插件或功能。可以从 GitHub 上的 IPython-contrib 存储库中安装一组特定于 Python 的扩展。收藏的网址是https://github . com/IPython-contrib/IPython-notebook-extensions。在本附录中,我们将介绍其中的一些扩展。
安装延伸部分
要安装扩展集合以及 Anaconda 存储库中的扩展管理器,请执行以下步骤:
-
启动 Anaconda 命令提示符并运行以下命令:
conda install -c https://conda.binstar.org/juhasch nbextensions -
要激活我们想要使用的一些扩展,请启动 Jupyter 笔记本。
-
打开一个新的浏览器选项卡,转到
http://localhost:8888/nbextensions(其中8888是 Jupyter 监听的端口)。 -
The page that you are presented with should look something like the following screenshot. The page is basically a list of the available extensions with checkboxes to activate them. If you click on the name of an extension, the page will load details about that extension:
![Installing the extensions]()
-
现在,通过单击名称旁边的复选框,激活我们将浏览的以下扩展(按字母顺序):
- 代码折叠
- 可折叠标题
- 帮助面板
- 初始化单元格
- NbExtensions 菜单项
- 尺子
- 跳过追溯
- 目录(2)
完成这些操作后,每个扩展旁边的复选框都会被标记,如下图所示:

注
要安装 GitHub 的最新版本,而不是 Anaconda 存储库中的版本(即前面的步骤 2),您可以运行以下命令:
pip install https://github.com/ipython-contrib/
IPython-notebook-extensions/archive/master.zip --user
根据我的经验,点击响应有点错误,所以要确保它们都被标记了。选择所有要激活的指定扩展后,您还可以配置其中一些扩展。我们将分别查看它们,但是通过单击每个扩展的名称显示的总体布局如下:
- 扩展名的名称
- 简短的描述
- 它与哪些版本的 Jupyter 笔记本兼容
- 激活/去激活按钮
- 右边的图像,大致显示了它的功能
- 扩展的可能参数/设置
之后,界面将抓取并输出自述文件,该文件采用 Markdown 语法。在这个文件中,扩展名的作者放入了任何可能有用的附加信息。在接下来的部分中,我们将一个接一个地浏览扩展。
代码折叠
代码折叠扩展是一个简单但非常有用的扩展。它将折叠缩进的代码行,例如,函数或类可以折叠。此外,它还会给你的选择折叠在评论。此扩展的信息窗格顶部显示如下:

作为您在自述文件中看到的示例,我将向您展示 Jupyter Notebook 在此输出的代码折叠扩展自述文件的顶部:

自述文件只是一个更广泛的描述,附有图片和外部链接。使用代码折叠扩展,可以在单元格中隐藏长代码片段和函数。这在下面的示例中显示。第一张图片展示了 Jupyter 笔记本中任意函数的外观:

单击左边距中的小箭头会将代码折叠成一行。接下来会是这样的:

如本节第一张显示该扩展参数的图片所示,快捷键 Alt + F 将切换折叠。折叠也适用于嵌套函数和语句;对于每个缩进级别,您可以折叠代码。您也可以将注释作为第一行来折叠代码单元格:

再次单击箭头,您将折叠其下整个单元格中的其余代码:

当您倾向于编写长函数或代码时,这是一个非常有用的扩展,可能是一个包含许多不同组件的图,或者您在笔记本中编写了帮助函数。
可折叠标题
使用可折叠标题扩展,可以通过创建标记单元格和定义标题来对单元格的整个部分进行分组。通常,这只会将文本显示为标题。扩展使标题及其下的所有单元格可折叠,它将折叠其下的所有内容,直到遇到相同或更高级别的标题。这里显示了设置页面中的可用参数:

您可以将键盘快捷键设置为(取消)折叠选定的标题、添加工具栏按钮以及切换键盘快捷键的使用。使用扩展的结果示例如下所示:

单击标题左侧的小箭头将折叠标题及其下的所有内容,使其位于同一部分下。它将看起来像下图:

当您对相似或相同的数据进行多次分析时,这非常有用。试着打开我们在本书中使用扩展的一章,你会看到它的用处。
帮助面板
当您开始在 Jupyter Notebook 中编写自己的代码时,帮助面板非常有用,因为它可以在笔记本旁边的面板中显示所有键盘快捷键。扩展的详细信息页面顶部如下所示:

在这里,可以勾选 框添加一个工具栏按钮,打开 快捷方式对话框/面板。然后,您将有一个按钮,如前图中右侧所示。
初始化单元格
分析会话开始时的大部分代码都是您希望在每次打开时运行的。初始化单元格扩展通过添加两件事缓解了这个问题——一个允许您标记初始化单元格的单元格工具栏和一个重新运行所有这些标记的初始化单元格的按钮。下图显示了扩展的详细信息页面,右侧是触发初始化单元格重新运行的按钮:

要使用此扩展,请执行以下步骤:
-
激活后,打开一个笔记本,创建你想要的细胞作为开始。附带的示例笔记本中有一些初始化单元格。
-
To change cells into initialization cells, you navigate to View | Cell Toolbar | Initialization Cell. When you have clicked this, each cell will get a toolbar (that is, cell toolbar) with a checkbox in the upper right corner, as shown in the following image:
![Initialization cells]()
-
点按您想要在打开笔记本时自动运行的单元格的复选框,例如,包含导入、数据读取和数据清理的单元格。
-
现在,关闭笔记本,再次打开它,并观看选中的单元格自动运行。你也可以通过点击看起来像计算器的按钮来触发它;请参见本节的第一张图片。
这个扩展非常有用,因为有时我们必须重启内核或笔记本,当这种情况发生时,重新运行所有简单导入模块和加载数据的单元就没那么有趣了。
NbExtensions 菜单项
NbExtensions 菜单项扩展非常简单;它添加了一个菜单项来打开扩展设置页面,您可以在其中激活/停用扩展。菜单项可以在编辑项下找到。以下是扩展详细信息页面的屏幕截图,左侧显示了菜单项:

尺子
尺子是一个简单的扩展,是为了美观,这样你就知道什么时候包装你的代码,让它遵循标准。可用的参数是标尺的列宽、颜色及其线条样式,如下图所示:

该扩展将在每个单元格中以参数中给定的列宽绘制一条垂直线。下图显示了它的外观:

跳过追溯
有时在单元格中运行的代码中会引发异常。当异常的堆栈跟踪很长时,Jupyter Notebook 仍然会显示整个跟踪。滚动到单元格输出的底部以获取导致异常的原因可能有点乏味。没有可为此扩展设置的参数。举一个很好的例子,我在当前版本的 NumPy 中发现了一个已归档的 bug,给出了一个很长的跟踪。你可以在https://github.com/numpy/numpy/issues/7547上看到这个 bug。要测试跳过回溯扩展,请遵循以下说明:
-
标准导入后(如前所述激活扩展),运行以下命令:
values = (1+np.array([0, 1.e-15]))*1.e27 plt.plot(values) -
You should now see something like the following screenshot:
![Skip-traceback]()
-
痕迹真的很长;您必须滚动一长串指针和文件。现在,点击工具栏上显示三角形和感叹号的按钮(见上图和下图);它切换回溯的隐藏。
-
Run the code again and you get the following:
![Skip-traceback]()
这要好得多,也不那么令人困惑,并显示了为什么跳过回溯有时非常有用。当然,在某些情况下,查看完整的跟踪很有用,例如,当您想要报告错误时。
目录
当使用带有多个部分的长笔记本时,可折叠标题扩展很有用。在这类笔记本中浏览时,目录很有用。这个插件只有几个参数。您可以让它对部分进行编号,选择目录的深度,并切换是否在笔记本顶部显示浮动窗口或表格。其中一些也可以在浮动窗口中设置:

在笔记本中,您可以通过按按钮来切换带有目录的浮动窗口。如下图所示:

按下按钮后,浮动窗口将出现在右侧。对于本附录的示例笔记本,它将如下所示:

这里目录旁边有四个按钮,除了表格的可点击标题。点击标题会带你到笔记本的那一部分。第一个按钮 [-] ,会简单的折叠目录,旁边的按钮会重新加载; n 将在笔记本中切换章节编号;最后, t 将在笔记本顶部的单独单元格中切换目录。点击最后一个按钮的输出如下所示:

其他 Jupyter 笔记本提示
在这里,我会给你一些使用 Jupyter 笔记本的额外提示。你可以用它做很多事情,这就是它如此优秀的原因。
外部连接
用额外的标志-ip *或实际的 IP 而不是*启动 Jupyter Notebook,将允许外部连接,即与您的计算机在同一个网络上(如果直接连接,则是互联网)。它将允许其他人编辑笔记本,并在您的计算机上实际运行代码,因此要非常小心。完整的调用如下所示:
jupyter notebook -ip *
它在教育环境中很有用,在教育环境中,您希望人们能够专注于编码而不是安装东西,或者他们没有某个软件包的正确版本。
出口
所有笔记本都可以导出为 PDF、HTML 和其他格式。为此,导航至菜单中的文件 | 下载为。如果以 PDF 格式导出,则可能需要将以下内容放在笔记本开头的单元格中。它将首先尝试制作图形的 PDF 版本,这将是基于向量的图形,因此当您调整它们的大小时是无损的,并且最终在合并到 PDF 中时质量会更好:
ip = get_ipython()
ibe = ip.configurables[-1]
ibe.figure_formats = { 'pdf', 'png'}
print(ibe.figure_formats)
要导出到 PDF,您需要其他外部软件——Latex 发行版(https://www.latex-project.org)和 Pandoc(http://pandoc.org)。安装后,应该可以将笔记本导出为 PDF 格式;任何 Latex 编译错误都应该出现在您启动 Jupyter Notebook 的终端中。
附加文件类型
也可以用 Jupyter 编辑任何其他文本文件。在 Jupyter 仪表板中,即启动时打开的主页面中,您可以创建非笔记本的新文件:

为了给你一个想法,我在附录数据文件中加入了额外的文件——一个是 Markdown 格式的文本文件(以.md结尾),还有一个名为helpfunctions.py的文件,带有我们在前面章节中创建的despine()函数。除了这两个,你还有mystyle.mplstyle文件要编辑。在编辑器中,您可以选择文件的格式,您将得到高亮显示。
Matplotlib 样式
在整本书中,我们使用了我们的自定义样式文件mystyle.mplstyle。如前所述,在 matplotlib 中,已经包含了许多样式文件。要打印发行版中可用的样式,只需打开 Jupyter 笔记本并运行以下命令:
import matplotlib.pyplot as plt
print(plt.style.available())
我正在运行 matplotlib 1.5,因此我将获得以下输出:
['seaborn-deep', 'grayscale', 'dark_background', 'seaborn-whitegrid', 'seaborn-talk', 'seaborn-dark-palette', 'seaborn-colorblind', 'seaborn-notebook', 'seaborn-dark', 'seaborn-paper', 'seaborn-muted', 'seaborn-white', 'seaborn-ticks', 'bmh', 'fivethirtyeight', 'seaborn-pastel', 'ggplot', 'seaborn-poster', 'seaborn-bright', 'seaborn-darkgrid', 'classic']
为了了解其中一些样式的外观,让我们创建一个测试绘图函数:
def test_plot():
x = np.arange(-10,10,1)
p3 = np.poly1d([-5,2,3])
p4 = np.poly1d([1,2,3,4])
plt.figure(figsize=(7,6))
plt.plot(x,p3(x)+300, label='x$^{-5}$+x$^2$+x$^3$+300')
plt.plot(x,p4(x)-100, label='x+x$^2$+x$^3$+x$^4$-100')
plt.plot(x,np.sin(x)+x**3+100, label='sin(x)+x$^{3}$+100')
plt.plot(x,-50*x, label='-50x')
plt.legend(loc=2)
plt.ylabel('Arbitrary y-value')
plt.title('Some polynomials and friends',
fontsize='large')
plt.margins(x=0.15, y=0.15)
plt.tight_layout()
return plt.gca()
它将绘制几个不同的多项式和一个三角函数。这样,我们可以创建应用了不同样式的地块,并直接进行比较。如果你没有做什么特别的事情,只是叫它,也就是test_plot(),你会得到类似于下图的东西:

这是 matplotlib 1.5 中的默认样式;现在我们要测试前面列表中的一些不同风格。由于 Jupyter Notebook 内联图形显示使用的样式参数不同(即rcParams),我们无法像运行普通 Python 提示一样重置每个样式设置的参数。因此,如果没有在新样式中设置参数,我们就不能在一行中绘制不同的样式,而不保留旧样式中的一些参数。我们可以做的如下,我们用'fivethirtyeight'样式集调用绘图函数:
with plt.style.context('fivethirtyeight'):
test_plot()
通过放入with语句,我们限制了在该语句中设置的任何内容,因此,不改变任何整体参数:

这就是'fivethirtyeight'风格的样子,灰色背景,彩色线条很粗。灵感来自统计网站http://fivethirtyeight.com。为了给你留下一些展示几种不同风格的图表,我建议你自己运行一些。一个有趣的事情是'dark-background'风格,例如,如果您通常在深色背景下运行演示文稿,可以使用该风格。我将很快向您展示with声明让我们做了什么。取我们的mystyle.mplstyle文件,绘制如下:
import os
stylepath = os.path.join(os.getcwd(), 'mystyle.mplstyle')
with plt.style.context(stylepath):
test_plot()

你可能并不总是对图形的外观完全满意——字体太小,情节周围的大框架也没有必要。为了做出一些改变,我们仍然可以像往常一样在with语句中调用函数来修复问题:
from helpfunctions import despine
plt.rcParams['font.size'] = 15
with plt.style.context(stylepath):
plt.rcParams['legend.fontsize'] ='Small'
ax = test_plot()
despine(ax)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['left'].set_color('w')
ax.spines['bottom'].set_color('w')
plt.minorticks_on()
输出如下所示:

这看起来更好更清晰。你能把这些额外的修改直接合并到mystyle.mplstyle文件中吗?尝试这样做——大部分都是可能的——最终,你会有一个不错的样式文件可以使用。
关于样式文件的最后一点重要意见。可以连续连锁几个。这意味着您可以创建一种改变事物(轴、线等)大小的样式和另一种改变颜色的样式。这样,如果您在演示文稿或书面报告中使用该图形,就可以适应不断变化的尺寸。
有用资源
在线上有大量关于数据分析主题的资源,特别关注 Python。我在这里试着编了几个,希望对你有用。你会发现一些我列出了资源的部分,一个简短的描述,和一个你可以找到更多信息的链接。
一般资源
Python 相关资源的常规链接:
连续分析
Python 分布的制造者。在他们的网页上,你可以找到文档和支持。
Python 和 IPython
https://python.org和http://ipython.org
真的没必要解释。我们非常感谢世界上的这两个项目。
Jupyter 笔记型电脑
Jupyter 笔记本项目网页,您可以在其中找到更多信息、文档和帮助。
Python 周报
每周(电子邮件)时事通讯,让您更容易了解 Python 世界的最新动态。
堆栈溢出
http://stack overflow . com
基本上所有内容的问答页面。如果你在网上搜索任何类型的 Python 编程问题,你很有可能会登陆他们的某个网页。注册并提问或回答问题!
想法
恩托林冠的制造者,就像一个 Anaconda 发行版,一个完整的 Python 发行版。entorn 还为任何感兴趣的人提供了许多课程和培训。
黑桃
https://pypi . python . org/pypi
大多数 Python 包的存储库,pip寻找包的第一个地方。
Scipy 工具包
https://www . scipy . org/scikits . html
SciPy 工具包(Scikits)的门户,Scipy 的附属包。scikit-learn是一个 Scikit 包。
GitHub
https://github . com
一个代码存储库,它使用著名的 Git 版本系统来跟踪代码的变化。只要公开代码,就可以免费注册和上传自己的代码。代码可以是 Python 或任何其他编程语言。
包装
这是一个有用的 Python 包列表。大部分可以通过conda或pip包装系统安装。
PyMC
https://pymc-devs . github . io/pymc/
或者,https://github.com/pymc-devs/pymc
Python 中的贝叶斯推理/建模分析包;用在第 6 章、贝叶斯方法中,在本书中。
司仪
PyMC 的一个替代品,一个用于贝叶斯推理的 MCMC 包。
科学学习
一个用 Python 进行机器学习数据分析的工具;用于本书第 7 章、监督与非监督学习。
天体 ML
一个机器学习包,专注于天文应用。
开赛体育馆
一个公开发布的开发和测试强化学习算法的工具包。
当时
一个访问金融和经济数据的中心——他们有一个 Python 应用编程接口,你可以用它来安装和访问大量数据。
海伯恩
https://stanford.edu/~mwaskom/software/seaborn/
用 Python 实现统计数据可视化的软件包。它有一些独特的绘图功能,但还没有进入 matplotlib 包。
数据仓库
这里,我列出了一些在线可用的数据存储库。
UCI 机器学习资源库
http://archive . ics . UCI . edu/ml
加州大学欧文分校机器学习和智能系统中心数据集存储库,针对机器学习问题。
世卫组织-全球卫生观察站数据库
http://apps.who.int/gho/data/node.home
来自全球的关键健康相关数据的大型数据库。
欧统局
http://EC . Europa . eu/Eurostat
欧洲联盟所有国家各种关键统计数据的数据库。
NTSB
国家运输安全委员会网页,这是一个关于美国汽车、铁路、航空和海上事故的统计数据库。
Socrata 开放数据
https://open data . socrata . com
各种数据集的大数据库(例如,全世界的航空事故统计数据),易于探索和查找数据。
综合社会调查(美国)
美国的年度调查,开放和可下载的数据集和在线数据探索工具。
疾控中心
http://www.cdc.gov/datastatistics/
疾病控制和预防中心(CDC)有很多关于各种疾病和健康相关统计的公开数据。
开放数据初始(+2500 个来源)
http://open data perception . I
显示开放数据资源的位置和链接的地图。
Data.gov.in
印度政府公共数据门户。它包含一组丰富而广泛的公开可用数据来练习您的数据分析技能。
Census.gov
美国人口普查局在美国就各种主题进行了调查并收集了数据。
日期。欧洲
https://data . Europa . eu/euodp
欧洲联盟开放数据门户提供了从所有欧盟国家获取数据的单一途径。
数据可视化
以下是一些对可视化有用的资源列表(这里重叠的是 Seaborn,之前已经列出)。
五点三十八分
说到数据可视化,这是一个很好的启发。该网站提供了世界各地数据的统计分析和展示。
剧情
数据分析和可视化在线完成。他们的 Python 工具现在是开源的,在自我托管时可以免费使用。
mpld3
创建交互式 Python 图并导出到浏览器供其他人探索。
总结
在本附录中,我们介绍了在 Jupyter Notebook 中进行数据分析和工作时有用的几件事。希望你能很好地利用这些资源和知识。社会的许多不同部分有如此多的数据等待分析。鉴于生产和存储的数据量的增加,我们需要更多的人能够以一种可理解的方式分析和呈现数据。*
十、附录 b:参考书目
本课程是文本和测验的混合,所有的包装都是为了记住你的旅程。它包括来自以下 Packt 产品的内容:
- Python 数据分析入门,Phuong Vo。马丁·奇根
- Python 数据分析食谱,Ivan Idris
- 掌握 Python 数据分析,Magnus VilhelmPersson,路易斯·费利佩 Martins






浙公网安备 33010602011771号