数据科学实战秘籍-全-
数据科学实战秘籍(全)
一、准备数据科学环境
一本传统的食谱包含作者感兴趣的烹饪食谱,并可以帮助读者扩大准备食物的范围。 许多人可能认为,食谱的最终产品就是菜肴本身,而且人们可以以几乎相同的方式来阅读这本书。 每章均指导读者将数据科学流水线的各个阶段应用于具有不同目标的不同数据集。 同样,就像在烹饪中一样,最终产品可以简单地是应用于特定集合的分析。
但是,我们希望您能有更广阔的视野。 数据科学家边做边学,确保每次迭代和假设都能改善从业者的知识基础。 希望通过使用两种不同的编程语言(R 和 Python)通过数据科学管道获取多个数据集,我们希望您将开始抽象出分析模式,看到更大的图景,并对这一相当模棱两可的数据领域有更深入的了解 科学。
我们还希望您知道,与烹饪食谱不同,数据科学食谱是模棱两可的。 当厨师开始制作一道菜时,他们会清楚地看到成品的外观。 对于数据科学家来说,情况通常是不同的。 在一定的时间和资源的情况下,人们并不总是知道所讨论的数据集的外观,以及可能或不可能的情况。 食谱本质上是一种挖掘数据并开始向正确的问题提问,以尽可能完成最佳菜肴的途径。
如果您来自统计学或数学背景,那么所显示的建模技术本身可能不会使您兴奋。 注意有多少方法可以克服数据科学管道中的实际问题,例如加载大型数据集和使用可伸缩工具来适应已知技术来创建数据应用,交互式图形和网页,而不是报告和论文。 我们希望这些方面能够增进您对数据科学的理解和理解,并将好的数据科学应用于您的领域。
从事实践的数据科学家需要大量多样的工具来完成工作。 数据从业人员使用各种各样的工具来抓取,清理,可视化,建模并执行一百万个不同的任务。 如果您问大多数使用数据的人,您将了解到此工具集中最重要的组件是用于执行数据分析和建模的语言。 确定特定任务的最佳编程语言类似于询问哪种世界宗教是正确的,而流血事件却略有减少。
在本书中,我们将注意力集中在用于数据分析的两种备受推崇但又截然不同的语言(R 和 Python)上,由您自己决定使用哪种语言。 我们将通过提示每种语言适合各种任务的方式来为您提供帮助,并且我们将比较和对比每种语言在同一数据集上完成的类似分析。
当您学习新的概念和技术时,总会遇到深度与广度的问题。 在固定的时间和精力下,您应该努力实现 R 和 Python 的中等熟练度,还是应该全部使用一种语言? 根据我们的专业经验,我们强烈建议您以掌握一种语言并对另一种语言有所了解为目标。 这是否意味着跳过有关特定语言的章节? 绝对不! 但是,在阅读本书时,请选择一种语言并进行更深层次的挖掘,不仅要提高会话能力,还要提高流利程度。
为了准备本章,请确保您有足够的带宽在合理的时间内下载多达几 GB 的软件。
了解数据科学管道
在开始安装任何软件之前,我们需要了解将在整本书中用于数据分析的可重复步骤。
怎么做...
以下是数据分析的五个关键步骤:
- 获取:管道中的第一步是从各种来源获取数据,包括关系数据库,NoSQL 和文档存储,Web 抓取以及分布式数据库,例如 Hadoop 平台上的 HDFS,RESTful API ,平面文件,希望不是这种情况(PDF)。
- 探索和理解:第二步是要了解您将使用的数据以及如何收集它们。 这通常需要进行大量探索。
- 修补,争执和操纵:此步骤通常是管道中最耗时且最重要的单个步骤。 数据几乎永远不会以所需的形式进行所需的分析。
- 分析和建模:这是有趣的部分,数据科学家可以探索数据中变量之间的统计关系,并从中提取出自己的机器学习技巧,以对这些因素进行聚类,分类或分类。 数据并创建预测模型以展望未来。
- 沟通和运营:在管道的最后,我们需要以引人注目的形式和结构将数据返回,有时交给我们自己以告知下一次迭代,有时又交给完全不同的读者。 生成的数据产品可以是简单的一次性报告,也可以是可扩展的 Web 产品,将被成千上万的用户交互使用。
工作原理...
尽管前面的列表是带编号的列表,但不要假定每个项目都严格遵守此精确的线性顺序。 实际上,敏捷数据科学家知道此过程是高度迭代的。 通常,数据探查会告知必须如何清理数据,从而可以进行更多的探查和更深入的了解。 这些步骤中的哪一个首先出现通常取决于您对数据的初步了解。 如果您与每天生产和捕获数据的系统一起工作,则除非生产系统出了问题,否则最初的数据探索和理解阶段可能会很短。 相反,如果您没有背景知识的数据集,则数据探索和理解阶段可能需要相当长的时间(以及许多非编程步骤,例如与系统开发人员交谈)。
下图显示了数据科学管道:

正如您现在可能已经听说过的那样,数据整理或整理通常会占用项目时间和资源的 80%或更多。 在一个完美的世界中,我们将始终获得完美的数据。 不幸的是,从来没有这样,您将看到的数据问题实际上是无限的。 有时,数据字典可能会更改或丢失,因此完全不可能了解字段值。 某些数据字段可能包含垃圾或已与其他字段切换的值。 通过测试的 Web 应用更新可能会导致一个小错误,阻止收集数据,从而导致数十万行丢失。 如果可能出错,则可能是在某个时候。 您分析的数据是所有这些错误的总和。
最后一步,沟通和运营,绝对至关重要,但其复杂性却常常不被人们充分理解。 请注意,管道中的最后一步没有标题为“数据可视化”,也没有围绕简单地创建漂亮和/或引人注目的内容而展开,这本身就是一个复杂的主题。 取而代之的是,数据可视化将成为一个更大的故事的一部分,我们将把数据与数据编织在一起。 有些人甚至说,最终结果总是一个争论,因为除非您试图说服某个人或某组特定观点,否则进行所有这些努力是没有意义的。
在 Windows,Mac OS X 和 Linux 上安装 R
直接来自 R 项目的 R 是用于统计计算和图形的语言和环境,它已成为统计和数据分析的实际语言之一。 对我们来说,这将是本书上半部分使用的默认工具。
准备工作确保您与 Internet 的宽带连接良好,因为您可能必须下载多达 200 MB 的软件。
怎么做...
安装 R 很容易; 使用以下步骤:
截至 2017 年 6 月,R 的最新版本为 2017 年 4 月起的版本 3.4.0。
-
下载后,请按照 CRAN 提供的出色说明将软件安装在您各自的平台上。 对于 Windows 和 Mac,只需双击下载的安装包。
-
安装了 R 后,继续启动它。 您应该看到一个类似于以下屏幕截图所示的窗口:

-
可在这个页面上获得对 CRAN 的重要修改,它是 Microsoft 对 R 软件的贡献。 实际上,作者是这种变体的拥护者,并强烈推荐 Microsoft 版本,因为它已经多次证明 MRAN 版本比 CRAN 版本快得多,并且所有代码在这两个版本上都运行相同 变体。 因此,有一个使用 MRAN R 版本的额外理由。
-
您可以停止下载 R,但是会错过为 R 构建的出色的集成开发环境(IDE),称为 RStudio。 访问这个页面下载 RStudio,然后按照在线安装说明进行操作。
-
安装完成后,继续运行 RStudio。 以下屏幕快照显示了我们作者的自定义 RStudio 配置之一,其中
Console面板位于左上角,编辑器位于右上角,当前变量列表位于左下角, 以及当前目录在右下角:

工作原理...
R 是出现在 1993 年的一种解释语言,它是 70 年代从 Bell Labs 出现的 S 统计编程语言的实现(S-PLUS 是 S 的商业实现)。 R,由于其开放源代码许可而有时被称为 GNU S,是一种域特定语言(DSL),致力于统计分析和可视化。 尽管您可以使用 R 进行许多操作,似乎与统计分析(包括 Web 抓取)没有直接关系,但它仍然是特定于域的语言,并不打算用于一般用途。
R 也受 CRAN(综合 R 存档网络)支持。 CRAN 包含一个可访问的 R 早期版本的存档,可根据要复制的软件的较早版本进行分析。 此外,CRAN 包含数百个免费下载的软件包,从而极大地扩展了 R 的功能。实际上,R 已成为包括统计在内的多个学术领域的默认开发平台,从而导致在 R 中首先实现了最新,最好的统计算法。 在这个页面的 Microsoft 变体中提供了更快的 R 版本。
RStudio 可在 GNU Affero 通用公共许可证 v3 下获得,并且是开源的,可以免费使用。 该公司的 RStudio,Inc.提供用于 R 的其他工具和服务以及商业支持。
另请参见
您还可以参考以下内容:
在 R 和 RStudio 中安装库
R 具有数量惊人的库,这些库增加了它的功能。 实际上,R 已经成为全国许多大学统计部门的默认语言。 因此,R 通常是将首先实现新开发的统计算法和技术的语言。 幸运的是,如以下各节所示,安装其他库很容易。
准备
只要您已安装 R 或 RStudio,就应该准备就绪。
怎么做...
R 使安装其他软件包变得简单:
- 启动 R 交互式环境,或者最好是 RStudio。
- 让我们安装
ggplot2。 键入以下命令,然后按 输入 键:
install.packages("ggplot2")
注意
请注意,在本书的其余部分中,假定当我们指定输入一行文本时,它会隐式地跟随着击回车键或输入键盘上的键
- 现在,当您向下滚动屏幕时,应该会看到类似于以下内容的文本:
trying URL 'http://cran.rstudio.com/bin/macosx/contrib/3.0/
ggplot2_0.9.3.1.tgz'Content type 'application/x-gzip' length 2650041 bytes (2.5
Mb)
opened URL
==================================================
downloaded 2.5 Mb
The downloaded binary packages are in
/var/folders/db/z54jmrxn4y9bjtv8zn_1zlb00000gn/T//Rtmpw0N1dA/
downloaded_packages
- 您可能已经注意到,您需要知道要安装的软件包的确切名称,在这种情况下为
ggplot2。 访问这个页面以确保您使用正确的名称。 - RStudio 提供了一种更简单的安装软件包的机制。 如果尚未打开 RStudio,请打开它。

- 转到菜单栏中的
Tools,然后选择Install Packages ...。 如以下屏幕截图所示,将弹出一个新窗口:

- 当您开始在
Packages字段中输入内容时,RStudio 将为您显示可能的软件包列表。 该字段的自动完成功能简化了库的安装。 更好的是,如果有一个相关的类似名称的库,或者该库的较早版本或较新版本具有相同的名称前几个字母,您将看到它。 - 让我们安装一些我们强烈推荐的软件包。 在 R 提示符下,键入以下命令:
install.packages("lubridate")
install.packages("plyr")
install.packages("reshape2")
您可以从这个页面下载从您的帐户购买的所有 Packt 书籍的示例代码文件。 如果您在其他地方购买了此书,则可以访问这个页面并注册以将文件直接通过电子邮件发送给您。
工作原理...
无论您使用 RStudio 的图形界面还是install.packages命令,您都可以执行相同的操作。 您告诉 R 搜索为特定版本的 R 构建的适当的库。发出命令时,R 报告在 CRAN 中找到与库匹配的位置的 URL 以及之后的二进制包的位置。 下载。
还有更多...
R 的社区是其优势之一,如果我们不简要提及两件事,我们将被忽略。 R-bloggers 是一个网站,汇集了 750 多个不同博客中与 R 相关的新闻和教程。 如果您对 R 有一些疑问,那么这里是寻找更多信息的好地方。 Stack Overflow 网站是一个很好的地方,可以使用标签rstats在 R 上提问和找到答案。
最后,随着您对 R 的熟练程度的提高,您可能会考虑构建其他人可以使用的 R 包。 提供有关库构建过程的深入教程超出了本书的范围,但是请记住,社区意见书是 R 运动的核心。
另请参见
您还可以参考以下内容:
- 请参阅我希望早些时候了解的《R 程序包》文章
- R-bloggers 网站
- [创建 R 包的教程](http://cran.r-project.org/doc/contrib/Leisch-CreatingPackages.pdf
- 2013 年(1 月至 5 月)的前 100 个 R 包
- Learning R 博客网站
在 Linux 和 Mac OS X 上安装 Python
对我们来说幸运的是,大多数版本的 Mac OS X 和 Linux 都预装了 Python(最新版本的 Ubuntu 和 Fedora 都开箱即用了 Python 2.7 或更高版本)。 因此,除了检查是否已安装所有东西外,我们实际上对这个食谱没有太多工作。
在本书中,我们将使用 Python 3.4.0。
准备
只要确保您有良好的 Internet 连接,以防我们需要安装任何东西。
怎么做...
在命令提示符下执行以下步骤:
- 打开一个新的“终端”窗口,然后键入以下命令:
which python
- 如果您安装了 Python,则应该看到类似以下内容的内容:
/usr/bin/python
- 接下来,使用以下命令检查您正在运行哪个版本:
python --version
工作原理...
如果您打算使用 OS X,出于某些原因,您可能希望在计算机上设置单独的 Python 发行版。 首先,每次 Apple 升级您的操作系统时,它将并且将淘汰您已安装的 Python 软件包,从而迫使所有先前安装的软件包重新安装。 其次,新版本的 Python 发布的频率要比 Apple 更新 OS X 随附的 Python 发行版本的频率高。因此,如果您想保持 Python 版本的最新优势,最好安装自己的发行版本。 最后,Apple 的 Python 版本与正式的 Python 版本略有不同,并且位于硬盘驱动器上的非标准位置。
在线上有许多教程可帮助您逐步在 Mac 上安装和设置单独的 Python 发行版。 我们建议您在这个页面上找到出色的指南,以在 Mac 上安装单独的 Python 发行版。
另请参见
您还可以参考以下内容:
在 Windows 上安装 Python
在 Windows 系统上安装 Python 很复杂,这给您提供了三个不同的选择。 首先,您可以选择在这个页面上使用标准 Windows 版本和 Python.org 的可执行安装程序。 此路由的潜在问题是目录结构以及配置和设置的路径与标准 Python 安装不同。 结果,每个已安装的 Python 软件包(将会有很多)都可能存在路径问题。 此外,大多数在线教程和答案都不适用于 Windows 环境,您将只能使用自己的设备来解决问题。 对于以这种方式在 Windows 上安装 Python 的学生,我们目睹了无数教程结束问题。 除非您是专家,否则我们建议您不要选择此选项。
第二个选项是安装预捆绑的 Python 发行版,该发行版在一次安装中包含所有与科学,数字和数据相关的软件包。 有两个合适的包,一个来自 Enthought,另一个来自 Continuum Analytics。 Enthought 提供了适用于 Windows 的 32 位和 64 位版本的 Python 3.5 的 Canopy 发行版。 该软件的免费版本 Canopy Express 附带了 50 多个预配置的 Python 软件包,因此它们可以直接使用,包括 pandas,NumPy,SciPy,IPython 和 matplotlib,它们足以满足以下目的: 这本书。 Canopy Express 还带有其自己的 IDE,使人联想到 MATLAB 或 RStudio。
Continuum Analytics 提供了 Anaconda,这是一个完全免费(甚至用于商业用途)的 Python 2.7 和 3.6 发行版,其中包含 100 多个用于科学,数学,工程和数据分析的 Python 软件包。 Anaconda 包含 NumPy,SciPy,pandas,IPython,matplotlib 等,并且对于我们在本书中所做的工作来说应该绰绰有余。
对于纯粹主义者而言,第三个也是最好的选择是使用来自 Oracle 软件的免费 VirtualBox 在 Windows 中运行虚拟 Linux 机器。 这将使您能够在所需的任何 Linux 版本中运行 Python。 这种虚拟机方法的缺点是运行速度比本地软件慢,您将不得不习惯于通过 Linux 命令行进行导航,这是任何实践的数据科学家都应该具备的技能。
怎么做...
执行以下步骤以使用 VirtualBox 安装 Python:
- 如果选择在虚拟 Linux 机器上运行 Python,请访问这个页面从 Oracle 软件免费下载 VirtualBox。
- 请遵循 Windows 的详细安装说明。
- 继续按照说明进行操作,并遍历标题为 1.6 的部分。 启动 VirtualBox,1.7 创建您的第一个虚拟机,然后 1.8 运行您的虚拟机。
- 虚拟机运行后,转到“在 Linux 和 Mac OS X 上安装 Python”配方。
如果要改为在本地安装 Continuum Analytics 的 Anaconda 发行版,请按照下列步骤操作:
- 如果您选择安装 Continuum Analytics 的 Anaconda 发行版,请转到这个页面并选择该软件的 64 位或 32 位版本(最好使用 64 位版本 )在 Windows 安装程序下。
- 请按照这个页面上 Windows 的详细安装说明进行操作。
工作原理...
对于许多读者而言,根据他们的经验,在预打包的 Python 发行版和运行虚拟机之间进行选择可能很容易。 如果您正在为这个决定而苦苦挣扎,请继续阅读。 如果您来自纯 Windows 背景并且/或者没有太多使用*nix命令行的经验,那么基于虚拟机的路由将具有挑战性,并将迫使您极大地扩展自己的技能。 这需要花费大量精力和毅力,这对整个数据科学都是有用的(请相信我们)。 如果您有时间和/或知识,那么在虚拟机中运行所有内容将使您进一步走上成为数据科学家的道路,并且很可能使您的代码更易于在生产环境中部署。 如果没有,您可以选择备份计划并使用 Anaconda 发行版,就像许多人选择的那样。
在本书的其余部分,我们将始终首先包含面向 Linux / Mac OS X 的 Python 软件包安装说明,其次是补充的 Anaconda 安装说明。 因此,对于 Windows 用户,我们将假设您已经走了 Linux 虚拟机的路线,或者使用了 Anaconda 发行版。 如果您选择走另一条路,我们为您的冒险精神鼓掌,并祝您好运! 让 Google 与您同在。
另请参见
您还可以参考以下内容:
在 Mac OS X 和 Linux 上安装 Python 数据堆栈
尽管通常说 Python 包含电池,但是有一些密钥库确实使 Python 的数据处理能力更上一层楼。 在此食谱中,我们将安装有时称为 SciPy 堆栈的堆栈,其中包括 NumPy,SciPy,pandas,matplotlib 和 Jupyter。
准备
本食谱假设您已安装标准 Python。
注意
如果在上一节中,您决定安装 Anaconda 发行版(或包含所需库的其他 Python 发行版),则可以跳过此食谱。
要检查您是否安装了特定的 Python 软件包,请启动 Python 解释器并尝试导入该软件包。 如果成功,则该软件包在您的计算机上可用。 另外,您可能需要通过sudo命令对您的计算机进行 root 访问。
怎么做...
以下步骤将允许您在 Linux 上安装 Python 数据堆栈:
- 在 Linux 上安装此堆栈时,必须知道所使用的 Linux 发行版。 Linux 的风格通常决定了您将使用的软件包管理系统,选项包括
apt-get,yum和rpm。 - 打开浏览器并导航到这个页面,其中包含大多数平台的详细说明。
- 如果不同,这些说明可能会更改,并且应取代此处提供的说明:
- 打开外壳。
- 如果您使用的是 Ubuntu 或 Debian,请输入以下内容:
sudo apt-get install build-essential python-dev python-
setuptools python-numpy python-scipy python-matplotlib ipython
ipython-notebook python-pandas python-sympy python-nose
sudo yum install numpy scipy python-matplotlib ipython python-pandas sympy python-nose
- 您可以使用多种选项在运行 OS X 的 Macintosh 上安装 Python 数据堆栈。这些选项是:
- 第一个选项是为每个工具下载预安装的安装程序(
.dmg),并像安装其他 Mac 应用一样安装它们(建议这样做)。 - 第二个选项是,如果您的系统上有 MacPorts,MacPorts 是基于命令行的安装软件系统。 您可能还需要带有已安装的命令行工具的 XCode。 如果是这样,您可以输入:
- 第一个选项是为每个工具下载预安装的安装程序(
sudo port install py27-numpy py27-scipy py27-matplotlib py27- ipython +notebook py27-pandas py27-sympy py27-nose
由于系统上将安装大量文件,因此上述所有选项都将花费一些时间。
工作原理...
由于编译依赖性(包括对 Fortran 的需求),安装 SciPy 堆栈在历史上一直具有挑战性。 因此,我们不建议您从源代码进行编译和安装,除非您对此感到满意。
现在,更好的问题是,您刚刚安装了什么? 我们安装了 NumPy,SciPy,matplotlib,IPython,IPython Notebook,熊猫,SymPy 和鼻子的最新版本。 以下是它们的描述:
- SciPy:这是一个基于 Python 的开放源码软件生态系统,用于数学,科学和工程,并且包含许多用于机器学习,科学计算和建模的有用库。
- NumPy:这是基础的 Python 程序包,可在 Python 中提供数值计算,类似于 C,并且速度非常快,尤其是在使用多维数组和线性代数运算时。 NumPy 是 Python 可以执行其他解释或脚本语言无法执行的高效,大规模数值计算的原因。
- matplotlib:这是一个完善且广泛的 Python 2D 绘图库,MATLAB 用户会很熟悉。
- IPython:这为 Python 提供了丰富而强大的交互式 shell。 它是标准 Python Read-Eval-Print Loop(REPL)的替代品,以及许多其他工具。
- Jupyter Notebook:这是一个基于浏览器的工具,可以执行和记录在 Python 中完成的工作,并支持代码,格式化文本,降价,图形,图像,声音,电影和数学表达式。
- 大熊猫:这提供了一个健壮的数据框对象和许多其他工具,可让您轻松快捷地进行传统数据和统计分析。
- 鼻子:这是一个测试工具,扩展了 Python 标准库中的单元测试框架。
还有更多...
我们将在介绍它们的章节中更详细地讨论各种程序包。 但是,如果我们至少没有提到 Python IDE,我们将被忽略。 通常,我们建议使用您喜欢的编程文本编辑器来代替功能强大的 Python IDE。 这可以包括来自 GitHub 的开源 Atom,出色的 Sublime Text 编辑器或 Ruby 人群的最爱 TextMate。 Vim 和 Emacs 都是绝佳的选择,不仅因为它们的强大功能,还因为它们可以轻松地用于在远程服务器上编辑文件,这是数据科学家的一项常见任务。 这些编辑器中的每一个都可以使用可配置代码完成,突出显示,掉毛等功能的插件进行高度配置。 如果必须具有 IDE,请从 JetBrains,Spyder 和 Ninja-IDE 的 IDE 向导中查看 PyCharm(社区版是免费的)。 您会发现大多数 Python IDE 都比数据工作更适合于 Web 开发。
另请参见
您也可以参考以下内容:
- 有关熊猫的更多信息,请参考《Python 数据分析库》文章
- NumPy 网站
- SciPy 网站
- matplotlib 网站
- IPython 网站
- 《SciPy 历史》文章
- MacPorts 主页
- XCode 网页
- XCode 下载页面
安装额外的 Python 软件包
在本书中,您还需要一些其他的 Python 库。 正如 R 为社区构建的软件包提供中央存储库一样,Python 也以 Python 软件包索引(PyPI)的形式提供。 截至 2014 年 8 月 28 日,PyPI 中有 48,054 个软件包。
准备
此食谱只需要一个合理的 Internet 连接即可。 除非另有说明,否则这些说明假定您使用的是系统随附的默认 Python 发行版,而不是 Anaconda。
怎么做...
以下步骤将向您展示如何下载 Python 软件包并从命令行安装它:
- 在您希望保留下载位置的地方下载该软件包的源代码。
- 解压缩包。
- 打开一个终端窗口。
- 导航到源代码的基本目录。
- 输入以下命令:
python setup.py install
- 如果需要 root 用户访问权限,请键入以下命令:
sudo python setup.py install
要使用 pip,这是安装 Python 软件包的最简便的现代方法,请按照以下步骤操作:
- 首先,让我们通过打开终端并启动 Python 解释器来检查是否已经安装了 pip。 在解释器中,键入:
>>>import pip
- 如果没有错误,则说明已安装 pip,然后可以继续执行步骤 5。如果看到错误,请快速安装 pip。
- 从这个页面下载文件到您的计算机上。
- 打开一个终端窗口,导航到下载的文件,然后键入:
python get-pip.py
或者,您可以键入以下命令:
sudo python get-pip.py
- 一旦安装了 pip,请确保您在系统命令提示符下。
- 如果您使用的是默认的 Python 系统发行版,请输入以下内容:
pip install networkx
或者,您可以键入以下命令:
sudo pip install networkx
- 如果使用 Anaconda 发行版,请输入以下命令:
conda install networkx
- 现在,让我们尝试安装另一个软件包
ggplot。 不管您使用什么发行版,都键入以下命令:
pip install ggplot
或者,您可以键入以下命令:
sudo pip install ggplot
工作原理...
您至少有两个选择来安装 Python 软件包。 使用先前的老式方法,您下载源代码并将其解压缩到本地计算机上。 接下来,使用install标志运行包含的setup.py脚本。 如果需要,可以在文本编辑器中打开setup.py脚本,然后更详细地了解该脚本在做什么。 您可能需要sudo命令,具体取决于当前用户的系统特权。
作为第二个选项,我们利用 pip 安装程序,该程序自动从远程存储库中获取软件包并将其安装到本地计算机上,以供系统级 Python 安装使用。 如果可用,这是首选方法。
还有更多...
pip具有功能,因此我们建议您在线阅读用户指南。 要特别注意非常有用的pip freeze > requirements.txt功能,以便您可以与同事交流有关外部依赖项的信息。
最后,conda是 Anaconda Python 发行版的软件包管理器和 pip 替代品,或者用其主页的名称是跨平台的,与 Python 无关的二进制软件包管理器。 Conda 具有超越 Python 语言的崇高抱负。 如果您使用的是 Anaconda,建议您进一步阅读conda可以作为默认软件包管理器使用的功能,而不要使用 pip。
另请参见
您还可以参考以下内容:
安装和使用 virtualenv
virtualenv 是一种可转换的 Python 工具。 一旦开始使用它,您将永远不会回头。 virtualenv 创建一个本地环境,并安装了自己的 Python 发行版。 从外壳激活此环境后,您可以使用pip install轻松地将软件包安装到新的本地 Python 中。
起初,这听起来可能很奇怪。 为什么有人要这样做? 这不仅可以帮助您处理 Python 中的软件包依赖关系和版本问题,而且还可以使您快速进行实验而不会破坏任何重要内容。 想象一下,您构建了一个需要awesome_template库版本为 0.8 的 Web 应用,但是您的新数据产品需要awesome_template库版本为 1.2。 你做什么工作? 使用 virtualenv,您可以同时拥有两者。
作为另一个用例,如果您在特定计算机上没有管理员权限,会发生什么情况? 您无法使用分析所需的sudo pip install安装软件包,那么您该怎么办? 如果您使用 virtualenv,则没关系。
虚拟环境是软件开发人员用来有效协作的开发工具。 环境确保该软件在具有不同依赖性的不同计算机(例如,从生产服务器到开发服务器)上运行。 该环境还会提醒其他开发人员要开发的软件的需求。 Python 的 virtualenv 确保所创建的软件在其自己的整体环境中,可以独立进行测试,并可以协同构建。
准备
假设您已经完成了上一个食谱,那么就可以开始使用该食谱了。
怎么做...
使用以下步骤安装和测试虚拟环境:
- 打开命令行外壳,然后键入以下命令:
pip install virtualenv
或者,您可以键入以下命令:
sudo pip install virtualenv
- 安装完成后,在命令窗口中键入
virtualenv,您将获得以下屏幕快照中显示的信息:

- 使用以下命令创建一个临时目录并将位置更改为该目录:
mkdir temp
cd temp
- 在目录中,创建第一个名为
venv的虚拟环境:
virtualenv venv
- 您应该看到类似于以下内容的文本:
New python executable in venv/bin/python
Installing setuptools, pip...done.
- 新的本地 Python 发行版现已可用。 要使用它,我们需要使用以下命令激活
venv:
source ./venv/bin/activate
- 激活的脚本不可执行,必须使用
source命令激活。 另外,请注意,您的 Shell 的命令提示符可能已更改,并以venv前缀表示您现在正在新的虚拟环境中工作。 - 要检查这一事实,请使用
which查看 Python 的位置,如下所示:
which python
您应该看到以下输出:
/path/to/your/temp/venv/bin/python
因此,在激活虚拟环境后键入python时,将运行本地 Python。
- 接下来,通过键入以下内容安装一些东西:
pip install flask
Flask 是一个用 Python 编写的微型网络框架; 前面的命令将安装 Flask 使用的许多软件包。
- 最后,我们演示了虚拟环境和 pip 提供的版本控制功能,如下所示:
pip freeze > requirements.txt
cat requirements.txt
这将产生以下输出:
Flask==0.10.1
Jinja2==2.7.2
MarkupSafe==0.19
Werkzeug==0.9.4
itsdangerous==0.23
wsgiref==0.1.2
- 请注意,不仅会捕获每个软件包的名称,还会捕获确切的版本号。
requirements.txt文件的优点在于,如果我们有一个新的虚拟环境,我们只需发出以下命令即可安装列出的 Python 软件包的每个指定版本:
pip install -r requirements.txt
- 要停用虚拟环境,只需在 shell 提示符下键入以下命令:
deactivate
工作原理...
virtualenv 使用其自己的安装目录创建自己的虚拟环境,这些目录独立于默认系统环境运行。 这使您可以尝试新库而不会污染系统级 Python 发行版。 此外,如果您有一个仅能正常运行的应用并且想要保留它,则可以通过确保该应用具有其自己的virtualenv来实现。
还有更多...
virtualenv是一种了不起的工具,对于任何 Python 程序员来说,它都是无价之宝。 但是,我们谨提请注意。 Python 提供了许多连接到 C 共享对象的工具,以提高性能。 因此,在您的虚拟环境中安装某些 Python packages(例如 NumPy 和 SciPy)可能需要编译和安装特定于系统的外部依赖项。 即使编译成功,这些编译也可能很乏味,这是维护虚拟环境的原因之一。 更糟糕的是,缺少依存关系将导致编译失败,并产生错误,要求您对外来错误消息,过时的 make 文件和复杂的依存关系链进行故障排除。 即使对于最资深的数据科学家来说,这也可能是艰巨的。
一种快速的解决方案是使用程序包管理器将复杂的库安装到系统环境中(对于 Linux 为 Aptitude 或 Yum,对于 OS X 为 Homebrew 或 MacPorts,Windows 通常已经具有已编译的安装程序)。 这些工具使用第三方软件包的预编译形式。 一旦在系统环境中安装了这些 Python 软件包,就可以在初始化virtualenv时使用--system-site-packages标志。 该标志告诉virtualenv工具使用已经安装的系统站点程序包,并且避免了需要进行编译的附加安装的需要。 为了指定可能已经存在于系统中的特定于您的环境的软件包(例如,当您希望使用较新版本的软件包时),请使用pip install -I将依赖项安装到virtualenv中,并忽略全局软件包。 当您仅在系统上安装大型软件包,但将virtualenv用于其他类型的开发时,此技术最有效。
在本书的其余部分中,我们将假定您正在使用virtualenv,并准备好使用本章中提到的工具。 因此,我们不会强制或详细讨论虚拟环境的使用。 只需将虚拟环境视为安全网,即可单独执行本书中列出的食谱。
另请参见
您还可以参考以下内容:
二、使用 R 的汽车数据可视化分析
在本章中,我们将介绍以下内容:
- 获取汽车燃油效率数据
- 为您的第一个项目准备 R
- 将汽车燃油效率数据导入 R
- 探索和描述燃油效率数据
- 分析一段时间内的汽车燃油效率
- 调查汽车的品牌和型号
简介
我们将在本书中介绍的第一个项目是对汽车燃油经济性数据的分析。 我们将用于分析此数据集的主要工具是 R 统计编程语言。 R 通常被称为数据科学的通用语言,因为它是目前最流行的统计和数据分析语言。 从本书示例中可以看到,R 是用于数据处理,分析,建模,可视化以及创建有用的脚本来完成分析任务的出色工具。
本章中的食谱将大致遵循数据科学流程中的以下五个步骤:
- 获得
- 探索与理解
- 蒙混,争吵和操纵
- 分析与建模
- 沟通与运营
在流程方面,数据科学的支柱是数据科学流水线,为了精通数据科学,您需要获得在执行此过程中的经验,同时交换各种工具和方法,以便您始终使用这些工具和方法。 适用于您正在分析的数据集。
本章的目的是通过逐步的示例指导您完成有关汽车燃油效率的分析项目,您可以从中学习并逐步应用于将来的其他数据集和分析项目。 将本章视为对以后更长且更具挑战性的章节的热身。
获取汽车燃油效率数据
每个数据科学项目都始于数据,本章以相同的方式开始。 对于本配方,我们将深入研究一个数据集,其中包含燃油效率指标,这些指标随时间推移以英里每加仑(MPG)衡量,适用于 自 1984 年以来一直是美国。此数据由美国能源部和美国环境保护署提供。 除了燃油效率数据外,该数据集还包含列出的汽车的若干特征和属性,从而提供了汇总和分组数据的机会,以确定历史上哪个组往往具有更高的燃油效率以及多年来如何改变。 可以在这个页面获得最新版本的数据集,有关数据集中变量的信息可以在这个页面找到。 数据的最新更新时间为 2013 年 12 月 4 日,下载日期为 2013 年 12 月 8 日。
注意
我们建议您使用本书代码随附的数据集副本,以确保本章中描述的结果与您的努力相匹配。
准备
要完成此食谱,您将需要一台可访问 Internet 的计算机和您选择的文本编辑器。
怎么做...
执行以下简单步骤来获取本章其余部分所需的数据:
- 从以下位置下载数据集。
- 用您选择的解压缩工具解压缩
vehicles.csv并将其移到您的工作代码目录中。 - 请花一点时间,然后使用 Microsoft Excel,Google Spreadsheet 或简单的文本编辑器打开未压缩的
vehicles.csv文件。 逗号分隔值(CSV)文件使用起来非常方便,因为可以使用非常基本且免费的工具对其进行编辑和查看。 打开文件后,滚动浏览某些数据,并了解要使用的内容。 - 导航至这个页面。
- 选择并复制数据描述下车辆标题下方的所有文本,并将其粘贴到文本文件中。 不包括排放方向。 将此文件另存为
varlabels.txt在您的工作目录中。 文件的前五行如下:
atvtype - type of alternative fuel or advanced technology vehicle
barrels08 - annual petroleum consumption in barrels for fuelType1 (1)
barrelsA08 - annual petroleum consumption in barrels for fuelType2 (1)
charge120 - time to charge an electric vehicle in hours at 120 V
charge240 - time to charge an electric vehicle in hours at 240 V
注意
请注意,为方便起见,在包含本章代码的存储库中提供了此文件。
工作原理...
在第一个简单的食谱中没有太多要解释的内容,但是请注意,我们在这里相对容易地开始。 在许多数据科学项目中,您将无法如此轻松地访问和查看数据。
为您的第一个项目准备 R
对于以下食谱,您将需要在计算机上安装 R 统计编程语言(基本 R 或 RStudio,但作者强烈建议使用功能强大且免费的 RStudio)和汽车燃油效率数据集。 此快速食谱将帮助您确保完成此分析项目所需的一切。
准备
您将需要互联网连接才能完成此食谱,并且根据上一章的说明,我们假定您已为特定平台安装了 RStudio。
怎么做...
如果您使用的是 RStudio,则以下三个步骤将使您准备就绪:
- 在计算机上启动 RStudio。
- 在 R 控制台提示下,安装此项目所需的三个 R 软件包:
install.packages("plyr")
install.packages("ggplot2")
install.packages("reshape2")
- 加载 R 软件包,如下所示:
library(plyr)
library(ggplot2)
library(reshape2)
还有更多...
R 的实力来自围绕该语言开发的社区以及由社区中的人创建和提供的软件包。 当前,您可以导入和利用 4,000 多个软件包和库,以使您的数据分析任务更加轻松。
Hadley Wickham 博士是 R 社区的著名成员,他制作了许多备受推崇且经常使用的 R 软件包。 在本章中,您将主要使用他最大的两个作品plyr和ggplot2,以及第三个名为reshape2的软件包。 软件包plyr将用于将本章稍后说明的拆分-合并-组合数据分析模式应用于我们的数据集,ggplot2将使复杂的数据可视化变得更加容易。
另请参见
将汽车燃油效率数据导入 R
一旦下载并安装了先前食谱中的所有内容,并为第一个项目准备 R,就可以将数据集导入 R 以开始进行一些初步分析并了解数据的外观。
准备
本章中的许多分析都是累积性的,以前的配方将用于后续的配方。 因此,如果您完成了上一个食谱,则应具备继续进行所需的一切。
怎么做...
以下步骤将引导您完成将数据初始导入 R 环境的过程:
- 首先,将工作目录设置为我们保存
vehicles.csv.zip文件的位置:
setwd("path")
替换实际目录的路径。
- 只要您知道要加载的 ZIP 档案中文件的文件名,我们就可以直接从压缩(ZIP)文件中加载数据:
vehicles <- read.csv(unz("vehicles.csv.zip", "vehicles.csv"),
stringsAsFactors = F)
- 要查看是否有效,让我们使用
head命令显示数据的前几行:
head(vehicles)
您应该看到屏幕上打印的数据集的前几行。
注意
请注意,我们可以使用 tail 命令,该命令将显示数据帧的最后几行,而不是前几行。
labels命令为vehicles.csv文件提供了变量标签。 请注意,由于labels是 R 中的一个函数,因此我们使用labels。快速浏览该文件将显示变量名称及其解释由-分隔。 因此,我们将尝试使用-作为分隔符来读取文件:
labels <- read.table("varlabels.txt", sep = "-", header = FALSE) ## Error: line 11 did not have 2 elements
- 这行不通! 仔细查看该错误,可以发现在数据文件的第 11 行中有两个
-符号,因此与其他行不同,它分成三个部分而不是两个部分。 我们需要更改文件读取方法以忽略带连字符的单词:
labels <- do.call(rbind, strsplit(readLines("varlabels.txt"), " - "))
- 为了检查它是否有效,我们再次使用 head 函数:
head(labels)
[,1] [,2]
[1,] "atvtype" "type of alternative fuel or advanced
technology vehicle"
[2,] "barrels08" "annual petroleum consumption in barrels for
fuelType1 (1)"
[3,] "barrelsA08" "annual petroleum consumption in barrels for
fuelType2 (1)"
[4,] "charge120" "time to charge an electric vehicle in hours
at 120 V"
[5,] "charge240" "time to charge an electric vehicle in hours
at 240 V"
工作原理...
让我们逐一分解步骤 5 中的最后一个复杂语句,从最内部开始并向外进行。 首先,让我们逐行阅读文件:
x <- readLines("varlabels.txt")
每行需要在字符串-处分割。 空格很重要,因此我们不分割带连字符的单词(例如第 11 行中的字符)。 这导致每一行作为字符串向量分成两部分,并且向量存储在单个列表中:
y <- strsplit(x, " - ")
现在,我们将这些向量堆叠在一起以形成字符串矩阵,其中第一列是变量名称,第二列是变量的描述:
labels <- do.call(rbind, y)
还有更多...
精明的读者可能已经注意到read.csv函数调用包括 stringsAsFactors = F 作为其最终参数。 默认情况下,R 将字符串转换为数据类型,在许多情况下称为因素。 因素是 R 的分类数据类型的名称,可以将其视为应用于数据的标签或标记。 在内部,R 将因子存储为整数,并映射到适当的标签。 此技术允许较早版本的 R 将因子存储在比相应字符少得多的内存中。
分类变量没有顺序感(其中一个值被认为大于另一个值)。 在以下代码段中,我们创建一个快速玩具示例,将character类的四个值转换为 factor 并进行比较:
colors <- c('green', 'red', 'yellow', 'blue')
colors_factors <- factor(colors)
colors_factors
[1] green red yellow blue
Levels: blue green red yellow
colors_factors[1] > colors_factors[2]
[1] NA
Warning message:
In Ops.factor(colors_factors[1], colors_factors[2]) :
>not meaningful for factors
但是,存在一个有序的分类变量,在统计世界中也称为序数数据。 有序数据就像分类数据一样,只有一个例外。 数据具有规模感或价值感。 可以说一个值大于另一个值,但是无法测量差异的大小。
此外,在将数据导入 R 时,我们经常会遇到这样的情况:一列数字数据可能包含非数字条目。 在这种情况下,R 可能会将数据列作为因素导入,这通常不是数据科学家想要的。 从因子到字符的转换是相对常规的,但是从因子到数字的转换可能有点棘手。
R 能够导入多种格式的数据。 在此配方中,我们处理了 CSV 文件,但也可以使用 Microsoft Excel 文件。 首选 CSV 文件,因为它们在整个操作系统中都得到普遍支持,并且具有更高的可移植性。 此外,R 可以从众多流行的统计程序(包括 SPSS,Stata 和 SAS)中导入数据。
另请参见
探索和描述燃油效率数据
现在我们已经将汽车燃油效率数据导入 R 并了解了一些有关导入的细微差别,下一步是对数据集进行一些初步分析。 该分析的目的是探索数据的外观,并使用 R 的一些最基本的命令来弄清数据。
准备
如果您已完成上一个食谱,则应具备继续进行所需的一切。
怎么做...
以下步骤将引导您完成对数据集的初步探索,并在其中计算一些基本参数:
- 首先,让我们找出数据中有多少个观测值(行):
nrow(vehicles)
## 34287
- 接下来,让我们找出数据中有多少个变量(列):
ncol(vehicles)
## 74
- 现在,让我们使用
names函数来了解数据帧中存在哪些数据列:
> names(vehicles)
前面的命令将为您提供以下输出:

幸运的是,这些列或变量名中的许多都具有很好的描述性,使我们对它们可能包含的内容有所了解。 请记住,可以在这个页面上找到有关变量的更详细说明。
- 通过计算
year列中唯一值的向量,然后计算该向量的长度,让我们找出此数据集中包含多少唯一数据年:
length(unique(vehicles[, "year"]))
## 31
- 现在,我们使用
min和max函数确定数据集中的前几年和后几年:
first_year <- min(vehicles[, "year"])
## 1984
last_year <- max(vehicles[, "year"])
## 2014
注意
请注意,我们可以使用 tail 命令,该命令将显示数据帧的最后几行,而不是前几行。
- 另外,由于我们可能会大量使用
year变量,因此,请确保我们涵盖了每年。 从 1984 年到 2014 年的年份列表应包含 31 个唯一值。 要对此进行测试,请使用以下命令:
> length(unique(vehicles$year))
[1] 31
- 接下来,让我们找出哪种类型的燃料用作汽车的主要燃料类型:
table(vehicles$fuelType1)
## Diesel Electricity Midgrade Gasoline Natural Gas
## 1025 56 41 57
## Premium Gasoline Regular Gasoline
## 8521 24587
由此可见,数据集中的大多数汽车都使用普通汽油,第二常见的燃料类型是特级汽油。
- 让我们探索这些汽车使用的变速器类型。 我们首先需要通过将其设置为
NA来处理所有丢失的数据:
vehicles$trany[vehicles$trany == ""] <- NA
- 现在
trany列为text,我们只关心汽车的变速箱是自动还是手动。 因此,我们使用substr函数提取每个trany列值的前四个字符,并确定其是否等于Auto。 如果是这样,我们将设置一个新变量trany2,该变量等于Auto;,否则,该值将设置为Manual:
vehicles$trany2 <- ifelse(substr(vehicles$trany, 1, 4) == "Auto",
"Auto", "Manual")
- 最后,我们将新变量转换为因子,然后使用表格函数查看值的分布:
vehicles$trany <- as.factor(vehicles$trany)
table(vehicles$trany2)
## Auto Manual
## 22451 11825
我们可以看到,具有自动变速箱的汽车模型的数量大约是具有手动变速箱的汽车模型的两倍。
工作原理...
数据框是 R 使用的强大数据类型,我们将在整个食谱中充分利用它。 数据框使我们能够将不同数据类型(数字,字符串,逻辑,因子等)的变量分组为相关信息行。 一个示例将是客户信息的数据框。 数据框中的每一行都可以包含人员名称(字符串),以及年龄(数字),性别(因素)和标记,以指示他们是否是当前客户(布尔值)。 如果您熟悉关系数据库,则这很像数据库中的表。
此外,在本食谱中,我们研究了几种快速读取导入到 R 中的数据集的方法。最值得注意的是,我们使用了功能强大的table函数来为fuelType1变量的出现次数进行计数。 此功能具有更多功能,包括交叉列表,如下所示:
with(vehicles, table(sCharger, year))
前面的命令将为您提供以下输出:

在这里,我们查看了有无超级充电器的年份(按年)的汽车型号(而且我们发现,超级充电器似乎比过去更受大众欢迎)。
另外,请注意,我们使用with命令。 该命令告诉 R 在执行后续命令(在本例中为 table)时,将车辆用作默认数据。 因此,我们可以省略sCharger和year列名称的开头,并在其前面加上数据框和车辆的名称,后跟美元符号。
还有更多...
为了提供有关数据导入的警告提示,让我们更仔细地查看sCharger和tCharger列。 请注意,这些列分别指示汽车是否包含超级充电器或涡轮增压器。
从sCharger开始,我们看一下变量的类和数据框中存在的唯一值:
> class(vehicles$sCharger)
[1] "character"
> unique(vehicles$sCharger)
[1] "" "S"
接下来,我们看tCharger,期望情况是一样的:
> class(vehicles$tCharger)
[1] "logical"
> unique(vehicles$tCharger)
[1] NA TRUE
但是,我们发现这两个看似相似的变量完全是不同的数据类型。 尽管tCharger变量是逻辑变量,在其他语言中也称为布尔变量,并且它用于表示TRUE和FALSE的二进制值,但sCharger变量似乎更通用 字符数据类型。 似乎有问题。 在这种情况下,由于可以,让我们检查原始数据。 幸运的是,数据位于.csv文件中,我们可以使用简单的文本编辑器来打开和读取文件(建议将 Windows 上的记事本和 Unix 系统上的 vi 用作该任务,但是请随意使用您喜欢的基本功能 文本编辑器)。 当我们打开文件时,我们可以看到sCharger和tCharger数据列为空白,或分别包含**S** 或**T** 。
因此,与字符T相对,R 已读入tCharger列中的 T 字符作为布尔TRUE变量。 这不是致命的缺陷,可能不会影响分析。 但是,诸如此类的未检测到的 bug 可能会导致分析流程之外的问题,并且需要进行大量重复工作。
分析一段时间内的汽车燃油效率
现在,我们已经成功导入了数据,并查看了一些重要的高级统计数据,这些统计数据使我们对数据集中的值以及某些要素出现的频率有了基本的了解。 通过此食谱,我们将通过研究一些随时间推移以及与其他数据点有关的燃油效率指标来继续探索。
准备
如果您完成了上一个食谱,则应具备继续进行所需的一切。
怎么做...
以下步骤将同时使用plyr和图形库ggplot2来浏览数据集:
- 让我们从平均水平来看,是否存在 MPG 随时间变化的总体趋势。 为此,我们使用
plyr包中的ddply函数获取vehicles数据帧,按年份汇总行,然后针对每个组计算平均公路,城市并结合燃油效率 。 然后将结果分配给新数据帧mpgByYr。 请注意,这是我们的 split-apply-combine 的第一个示例。 我们按年份将数据框架分为几组,将均值函数应用于特定变量,然后将结果合并为一个新的数据框架:
mpgByYr <- ddply(vehicles, ~year, summarise, avgMPG =
mean(comb08), avgHghy = mean(highway08), avgCity =
mean(city08))
- 为了更好地理解此新数据帧,我们将其传递给
ggplot函数,告诉它使用点将avgMPG变量与year变量相对应地绘制。 另外,我们指定我们希望轴标签,标题,甚至是平滑的条件mean (geom_smooth())都以图的阴影区域表示:
ggplot(mpgByYr, aes(year, avgMPG)) + geom_point() +
geom_smooth() + xlab("Year") + ylab("Average MPG") +
ggtitle("All cars")
## geom_smooth: method="auto" and size of largest group is <1000, so using
## loess. Use 'method = x' to change the smoothing method.
前面的命令将为您提供以下图表:

- 基于这种可视化,可能会得出结论,在过去几年中,所售汽车的燃油经济性有了极大的提高。 但是,这可能会引起误解,因为在随后的几年中出现了更多的混合动力和非汽油动力汽车,如下所示:
table(vehicles$fuelType1)
## Diesel Electricity Midgrade Gasoline Natural Gas
## 1025 56 41 57
## Premium Gasoline Regular Gasoline
## 8521 24587
- 让我们看一下汽油车,尽管非汽油动力车不多,然后重画前面的图。 为此,我们使用子集函数创建一个新的数据帧
gasCars,该数据帧仅包含其中fuelType1变量是值子集中之一的车辆行:
gasCars <- subset(vehicles, fuelType1 %in% c("Regular Gasoline",
"Premium Gasoline", "Midgrade Gasoline") & fuelType2 == "" & atvType != "Hybrid")
mpgByYr_Gas <- ddply(gasCars, ~year, summarise, avgMPG = mean(comb08))
ggplot(mpgByYr_Gas, aes(year, avgMPG)) + geom_point() +
geom_smooth() + xlab("Year") + ylab("Average MPG") + ggtitle("Gasoline cars")
## geom_smooth: method="auto" and size of largest group is <1000, so using
## loess. Use 'method = x' to change the smoothing method.
前面的命令将为您提供以下图表:

- 最近生产的大型发动机汽车数量减少了吗? 如果是这样,这可以解释增加的原因。 首先,让我们验证具有较大发动机的汽车的燃油效率是否较差。 我们注意到
displ变量代表发动机的排量(以升为单位),目前是一个字符串变量,我们需要将其转换为数字变量:
typeof(gasCars$displ)
## "character"
gasCars$displ <- as.numeric(gasCars$displ)
ggplot(gasCars, aes(displ, comb08)) + geom_point() +
geom_smooth()
## geom_smooth: method="auto" and size of largest group is >=1000, so using
## gam with formula: y ~ s(x, bs = "cs"). Use 'method = x' to change the
## smoothing method.
## Warning: Removed 2 rows containing missing values
(stat_smooth).
## Warning: Removed 2 rows containing missing values
(geom_point).
前面的命令将为您提供以下图表:

数据的散点图提供了令人信服的证据,表明发动机排量和燃油效率之间存在负相关,甚至呈负相关。 因此,较小的汽车往往更省油。
- 现在,让我们看看在以后的几年中是否还会生产更多的小型车,这可以解释燃油效率的急剧提高:
avgCarSize <- ddply(gasCars, ~year, summarise, avgDispl = mean(displ))
ggplot(avgCarSize, aes(year, avgDispl)) + geom_point() +
geom_smooth() + xlab("Year") + ylab("Average engine displacement (l)")
## geom_smooth: method="auto" and size of largest group is <1000, so using
## loess. Use 'method = x' to change the smoothing method.
## Warning: Removed 1 rows containing missing values (stat_smooth).
## Warning: Removed 1 rows containing missing values (geom_point).
前面的命令将为您提供以下图表:

- 从上图可以看出,自 2008 年以来,平均发动机排量已经大幅下降。为了更好地了解这可能对燃油效率的影响,我们可以将 MPG 和排量按年放在同一张图表上。 使用
ddply,我们创建一个新的数据框byYear,其中包含按年计的平均燃油效率和平均发动机排量:
byYear <- ddply(gasCars, ~year, summarise, avgMPG = mean(comb08),
avgDispl = mean(displ))
> head(byYear)
year avgMPG avgDispl
1 1984 19.12162 3.068449
2 1985 19.39469 NA
3 1986 19.32046 3.126514
4 1987 19.16457 3.096474
5 1988 19.36761 3.113558
6 1989 19.14196 3.133393
head函数向我们显示结果数据帧具有三列:year,avgMPG和avgDispl。 要使用ggplot2的分面功能在单独但对齐的图上按年份显示Average MPG和Avg engine displacement,我们必须融化数据框,将其从所谓的宽格式转换为长格式:
byYear2 = melt(byYear, id = "year")
levels(byYear2$variable) <- c("Average MPG", "Avg engine displacement")
head(byYear2)
year variable value
1 1984 Average MPG 19.12162
2 1985 Average MPG 19.39469
3 1986 Average MPG 19.32046
4 1987 Average MPG 19.16457
5 1988 Average MPG 19.36761
6 1989 Average MPG 19.14196
- 如果使用
nrow功能,我们可以看到byYear2数据帧有 62 行,byYear数据帧只有 31 行。byYear中的两个单独列(avgMPG和avgDispl) 现在已被byYear2数据框中的一个新列(value)融化。 请注意,byYear2数据框中的变量列用于标识该值表示的列:
ggplot(byYear2, aes(year, value)) + geom_point() +
geom_smooth() + facet_wrap(~variable, ncol = 1, scales =
"free_y") + xlab("Year") + ylab("")
## geom_smooth: method="auto" and size of largest group is <1000, so using
## loess. Use 'method = x' to change the smoothing method.
## geom_smooth: method="auto" and size of largest group is <1000, so using
## loess. Use 'method = x' to change the smoothing method.
## Warning: Removed 1 rows containing missing values (stat_smooth).
## Warning: Removed 1 rows containing missing values (geom_point).
前面的命令将为您提供以下图表:

从该图可以看到以下内容:
- 发动机尺寸总体上一直增加到 2008 年,而大型汽车在 2006 年到 2008 年之间突然增加。
- 自 2009 年以来,平均汽车尺寸有所减少,这在一定程度上解释了燃油效率的提高。
- 直到 2005 年,平均汽车尺寸有所增加,但燃油效率大致保持不变。 这似乎表明多年来,发动机效率有所提高。
- 2006-2008 年很有趣。 尽管平均发动机尺寸突然增加,但 MPG 仍与前几年大致相同。 这种表面上的差异可能需要更多的调查。
- 考虑到排量较小的发动机的发展趋势,让我们看看自动变速箱或手动变速箱对四缸发动机的效率是否更高,以及效率随时间的变化情况:
gasCars4 <- subset(gasCars, cylinders == "4")
ggplot(gasCars4, aes(factor(year), comb08)) + geom_boxplot() + facet_wrap(~trany2, ncol = 1) + theme(axis.text.x = element_text(angle = 45)) + labs(x = "Year", y = "MPG")
前面的命令将为您提供以下图表:

- 这次,
ggplot2用于创建箱形图,以帮助可视化每年的值分布(而不仅仅是单个值,例如平均值)。 - 接下来,让我们看一下每年可用手动车比例的变化:
ggplot(gasCars4, aes(factor(year), fill = factor(trany2))) +
geom_bar(position = "fill") + labs(x = "Year", y = "Proportion
of cars", fill = "Transmission") + theme(axis.text.x =
element_text(angle = 45)) + geom_hline(yintercept = 0.5,
linetype = 2)
前面的命令将为您提供以下图表:

工作原理...
ggplot2库是 Wilkinson,Anand 和 Grossman for R 的图形基础语法的开源实现。图形语法试图将统计数据可视化分解为组成部分,以更好地理解这种方式 图形已创建。 使用ggplot2,Hadley Wickham 采纳了这些想法并实现了分层的方法,从而使用户可以快速地从单个零件中组装复杂的可视化效果。 例如,以该配方的第一张图表为例,该图表显示了特定年份中所有型号的汽车在一段时间内的平均燃油效率:
ggplot(mpgByYr, aes(year, avgMPG)) + geom_point() + geom_smooth() +
xlab("Year") + ylab("Average MPG") + ggtitle("All cars")
要构建此图,我们首先告诉ggplot将用作该图数据的数据框(mpgByYr),然后通过美学映射图告诉ggplot2哪些变量将映射到该图的视觉特征中 阴谋。 在这种情况下,aes(year, avgMPG)隐式指定年份将映射到x轴,而avgMPG将映射到 y 轴。 geom_point()告诉库将指定数据绘制为点,然后第二个geom geom_smooth()添加阴影区域,以显示相同数据的平滑均值(默认情况下,置信区间设置为0.95)。 。 最后,xlab(),ylab()和ggtitle()函数用于在图上添加标签。 因此,我们可以在一行代码中生成一个复杂的发布质量图; ggplot2能够绘制更复杂的图。
另外,必须注意ggplot2和一般的图形语法不会告诉您如何最好地可视化数据,但可以为您提供快速执行此操作的工具。 如果您需要有关此主题的更多建议,我们强烈建议您研究 Edward Tufte 的著作,他有很多有关此事的书籍,包括经典的定量信息的可视化显示,美国印刷出版社。 此外,ggplot2不允许动态数据可视化。
还有更多...
在步骤 9 中,似乎手动变速箱比自动变速箱更有效,并且从 2008 年开始,它们平均呈现出相同的增长。但是,这里有些奇怪。 在后来的几年中,似乎有很多非常高效的汽车(不到 40 MPG)具有自动变速箱,并且几乎没有在相同的时间范围内具有类似效率的手动变速箱汽车。 在早些年,这种模式是相反的。 每年手动汽车的比例有变化吗? 是的。
通过本食谱,我们将使用两个非常重要的 R 包plyr和ggplot2将您带入 R 进行数据分析的深层次。 就像传统的软件开发具有用于通用构造的设计模式一样,在数据科学领域中也出现了一些这样的模式。 最引人注目的之一是 Hadley Wickham 博士突出显示的拆分组合模式。 在这种策略中,可以通过一些变量将问题分解为更小,更易于管理的部分。 汇总后,您可以对新的分组数据执行操作,然后将结果合并到新的数据结构中。 如您在本食谱中所见,我们反复使用了“拆分-应用-合并”的策略,因此从许多不同的角度检查了数据。
除了plyr之外,此配方还大量利用了ggplot2库,值得进一步介绍。 我们将避免提供广泛的ggplot2教程,因为在线上有许多优秀的教程。 重要的是您了解ggplot2如何使您以简洁的方式构建如此复杂的统计可视化的重要思想。
另请参见
- 《数据分析的拆分应用组合策略》
- 《图形语法》,Leland Wilkinson,Springer Science & 商业媒体
ggplot2软件包文章- 《图形分层语法》文章
调查汽车的品牌和型号
关于该数据集的第一组问题提出并回答后,让我们继续进行其他分析。
准备
如果您完成了上一个食谱,则应具备继续进行所需的一切。
怎么做...
本食谱将调查汽车的品牌和型号以及它们随着时间的变化:
- 让我们看一下汽车的品牌和型号如何随时间改变燃油效率。 首先,让我们看看这段时间在美国销售的汽车的制造商和型号的频率,并重点介绍四缸汽车:
carsMake <- ddply(gasCars4, ~year, summarise, numberOfMakes = length(unique(make)))
ggplot(carsMake, aes(year, numberOfMakes)) + geom_point() + labs(x = "Year", y = "Number of available makes") + ggtitle("Four cylinder cars")
从下图中我们可以看到,在这段时期内,可供销售的商品数量有所下降,尽管最近一段时间有所增加。

- 我们能否看一下这项研究每年可用的品牌? 我们发现在此期间,每年只有 12 家制造商生产四缸汽车:
uniqMakes <- dlply(gasCars4, ~year, function(x)
unique(x$make))
commonMakes <- Reduce(intersect, uniqMakes)
commonMakes
## [1] "Ford" "Honda" "Toyota" "Volkswagen"
"Chevrolet"
## [6] "Chrysler" "Nissan" "Dodge" "Mazda"
"Mitsubishi"
## [11] "Subaru" "Jeep"
- 这些制造商随着时间的推移在燃油效率方面做得如何? 我们发现大多数制造商在这段时间内都表现出了进步,尽管在过去五年中有几家制造商证明了燃油效率的显着提高:
carsCommonMakes4 <- subset(gasCars4, make %in% commonMakes)
avgMPG_commonMakes <- ddply(carsCommonMakes4, ~year + make,
summarise, avgMPG = mean(comb08))
ggplot(avgMPG_commonMakes, aes(year, avgMPG)) + geom_line() +
facet_wrap(~make, nrow = 3)
前面的命令将为您提供以下图表:

工作原理...
在步骤 2 中,肯定有一些有趣的魔术在起作用,仅用几行代码就可以完成很多工作。 这既是 R 的美观方面也是有问题的方面。它之所以优美,是因为它允许简洁地表达程序上复杂的思想,但是这是有问题的,因为如果您不熟悉特定的库,R 代码可能会非常难以理解。
在第一行中,我们使用dlply(不是ddply)来获取gasCars4数据帧,按年份分割它,然后将唯一函数应用于make变量。 对于每年,计算唯一可用的汽车制造商列表,然后dlply返回这些列表的列表(每年一个元素)。 注意dlply而非ddply,因为它以数据帧(d)作为输入并返回列表(l)作为输出,而ddply则以数据帧(d)作为输入 输入和输出数据帧(d):
uniqMakes <- dlply(gasCars4, ~year, function(x) unique(x$make))
commonMakes <- Reduce(intersect, uniqMakes)
commonMakes
下一行更加有趣。 它使用Reduce高阶函数,这与 Google 引入的作为 Hadoop 基础的 map reduce 编程范例中的Reduce函数和思想相同。 在某些方面,R 是一种功能编程语言,并提供一些更高阶的功能作为其核心的一部分。 高阶函数接受另一个函数作为输入。 在这一行中,我们将intersect函数传递给Reduce,该函数将intersect函数成对应用到以前创建的每年唯一品牌列表中的每个元素。 最终,这导致每年都会有一份汽车制造清单。
这两行代码表达了一个非常简单的概念(确定每年所有的汽车制造商),用两个段落来描述。
还有更多...
此配方中的最终图形是ggplot2的多面图形功能的一个很好的示例。 添加+facet_wrap(~make, nrow = 3)会告诉ggplot2,我们希望为每种汽车品牌使用一组独立的轴,并将这些子图分布在三个不同的行之间。 这是一种非常强大的数据可视化技术,因为它使我们能够清楚地看到可能仅针对某个变量的特定值而显现的模式。
在第一个数据科学项目中,我们保持了简单。 数据集本身很小-在基本的笔记本电脑上,未压缩,易于存储和处理的只有 12 MB。 我们使用 R 导入数据集,检查某些(但不是全部)数据字段的完整性,并汇总数据。 然后,我们通过询问许多问题并使用两个关键库plyr和ggplot2来操作数据并可视化结果,以探索数据。 在这个数据科学流水线中,我们的最后阶段只是编写用来总结我们的结论和ggplot2产生的可视化效果的文字。
另请参见
三、使用税务数据和 Python 创建面向应用的分析
在本章中,我们将介绍:
- 准备分析最高收入
- 导入和探索世界上最高收入的数据集
- 分析和可视化美国的最高收入数据
- 进一步分析美国的最高收入群体
- 用 Jinja2 进行报告
- 在 R 中重复分析
简介
在本书中,我们采用一种实用的方法来使用 R 和 Python 进行数据分析。 我们可以相对轻松地回答有关特定数据集的问题,生成模型并导出可视化效果。 因此,R 是快速原型设计和分析的绝佳选择,因为 R 是专为统计数据分析而设计的领域特定语言,并且表现出色。
在本书中,我们将介绍另一种更适合生产环境和应用的分析方法。 假设,获取,清理和整理,分析,建模,可视化和应用的数据科学管道无论如何都不是一个干净,线性的过程。 此外,当分析要以自动化的方式进行大规模重现时,许多新的考虑因素和要求也应运而生。 因此,许多数据应用需要更广泛的工具箱。 该工具包仍应提供快速原型制作,在所有系统上普遍可用,并为一系列计算操作(包括网络操作,数据操作和科学操作)提供全面支持。 鉴于这些要求,Python 显然是面向应用分析的首选工具。
Python 是一种解释语言(有时也称为脚本语言),与 R 十分相似。它不需要特殊的 IDE 或软件编译工具,因此使用 R 和开发原型的速度与 R 一样快。 与 R 一样,它也使用 C 共享库来提高计算性能。 此外,Python 是 Linux,Unix 和 macOS X 机器上的默认系统工具,并且也可用于 Windows。 Python 装有电池,这意味着标准库广泛包含了从多处理到压缩工具集的许多模块。 Python 是一个灵活的计算中心,可以解决任何领域问题。 如果您发现需要标准库之外的库,Python 还附带了一个软件包管理器(如 R),该软件包管理器允许下载和安装其他代码库。
Python 的计算灵活性意味着某些分析任务比 R 中的对应任务需要更多的代码行。但是,Python 确实具有允许其执行相同统计计算的工具。 这就引出了一个明显的问题:我们何时在 Python 上使用 R,反之亦然? 本章试图通过采用面向应用的方法进行统计分析来回答这个问题。
面向应用的方法简介
数据应用和数据产品正日益成为我们日常生活的一部分。 这些产品比简单的数据驱动的 Web 应用具有更广泛的应用范围,简单的数据驱动 Web 应用包括由数据库支持的各种前端 Web 和移动应用,并包括用于处理事务的中间件。 根据这个定义,简单的博客与大型电子商务站点没有本质上的区别。 取而代之的是,数据产品和设备从数据本身获取价值,并创建更多数据。
这些类型的应用可用于丰富传统应用,例如博客的语义标记或电子商务站点的推荐引擎。 另一方面,它们本身就可以成为独立的数据产品,包括从量化的自助设备到自动驾驶汽车的所有产品。
与更传统的数据挖掘或静态数据集的统计评估不同,在实时或流式上下文中对数据的处理和分析似乎是面向应用的分析的定义特征。 为了处理此类数据,需要大量的程序敏捷性或动态方法,而灵活性恰恰是 Python 在数据科学领域的亮点。
考虑报告任务的特定示例。 拍摄数据窗口快照并使用图表和可视化图表从收集的分析中手动编译报告是了解更改数据并了解较大模式的好习惯。 当该报告需要每天在较小的数据量上运行时,调度程序仅可以每天将报告转储到文件中。 但是,当报告任务变成每小时一次或按需执行时,这意味着可视化应用已变为静态 Web 应用,并且可能需要在中心位置。 随着此任务和数据大小的增长,在报表上添加约束或查询变得很重要。 这是数据应用的典型生命周期,Python 开发非常适合处理这些不断变化的需求。
我们将描述,建模和可视化包含世界上最高收入的数据集,并讨论 Python 中的统计工具包。 但是,在阅读本章时,我们还将包括有关如何在面向应用的上下文中构建我们正在使用的分析和方法的说明。
为分析最高收入做准备
对于以下食谱,您将需要在计算机上安装 Python,并且需要世界上收入最高的数据集。 此食谱将帮助确保您已设置完成此分析项目所需的一切。
准备
要逐步阅读此食谱,您将需要一台可访问互联网的计算机。 确保已下载并安装 Python 和必要的 Python 库以完成此项目。
注意
请参阅第 1 章,“准备数据科学环境”,以使用virtualenv设置 Python 开发环境并安装matplotlib和NumPy所需的库。
怎么做...
以下步骤将指导您下载世界上收入最高的数据集,并安装必要的 Python 库以完成此项目:
注意
可以从这个页面下载世界最高收入的原始数据集。 但是,该站点已进行了多次更新,从而更改了数据的输出格式(从.csv更改为.xlsx)。 该配方采用.csv文件格式。 本章的存储库包含输入数据文件的正确格式版本。
- 将全球收入最高的数据集保存到计算机上可以找到它的位置。
- 打开终端窗口并启动 Python 解释器。
- 检查并确保安装了以下三个库
NumPy,matplotlib和Jinja2; 尝试导入每个:
In [2]: import numpy as np
...: import jinja2
...: import matplotlib as plt
前面的每个库都应从 Python 导入而无需添加注释或注释。 如果他们这样做,那您就很好了。 如果不是,请参考第 1 章和“准备数据科学环境”来设置系统。
工作原理...
NumPy 是 Python 的基础科学计算库; 因此,它对于任何数据科学工具包都是必不可少的,我们将在整个 Python 章节的许多地方使用它。 但是,由于 NumPy 是必须为您的系统编译的外部库,因此我们将与 NumPy 方法一起讨论其他本机 Python 方法。
导入和探索世界上最高收入的数据集
在下载并安装了先前配方中的所有内容之后,您可以使用 Python 读取数据集,然后开始进行一些初步分析以了解您所拥有的数据的样子。
我们将在本章中探讨的数据集由 Alvaredo,Facundo,Anthony B. Atkinson,Thomas Piketty 和 Emmanuel Saez,世界最高收入数据库,2013 年 10 月 12 日。 它包含从税收记录中收集到的有关过去 100 年来每个国家/地区最高收入的全球信息。
准备
如果您已完成先前的食谱,为分析最高收入做准备,则应具备继续进行所需的一切。
怎么做...
让我们使用以下步骤序列来导入数据,并开始在 Python 中对该数据集进行探索:
- 使用以下代码段,我们将在内存中创建一个 Python 列表,其中包含每一行的字典,其中的键是列名(CSV 的第一行包含标题信息),而值是该特定行的值:
In [3]: import csv
...: data_file = "../data/income_dist.csv"
...: with open(data_file, 'r') as csvfile:
...: reader = csv.DictReader(csvfile)
...: data = list(reader)
注意
请注意,根据放置位置,输入文件income_dist.csv可能位于系统的其他目录中。
- 我们使用
len快速检查以显示记录数:
In [4]: len(data)
...:
Out[4]: 2180
- 当使用带标题的 CSV 数据时,我们会检查 CSV 阅读器本身的字段名称,并获取变量数:
In [5]: len(reader.fieldnames)
...:
Out[5]: 354
- 尽管这些数据不是太大,但是让我们在访问数据时开始使用最佳实践。 我们不是使用所有生成器将数据保存在内存中,而是使用生成器来一次访问一行数据。
生成器是 Python 表达式,可让您创建充当可迭代函数的函数。 它们不返回所有数据,而是在内存有效的迭代上下文中一次生成一个数据。 随着我们的数据集变得越来越大,使用生成器按需执行过滤并在您读取数据时清理数据很有用:
In [6]: def dataset(path):
...: with open(path, 'r') as csvfile:
...: reader = csv.DictReader(csvfile)
...: for row in reader:
...: yield row
另外,请注意 with open(path, 'r') as csvfile语句。 该语句可确保在退出 with 块时,即使(或特别是)存在异常时,也将关闭 CSV 文件。 带块的 Python 替换了 try,except 和 finally 语句,并且语法简短,而语义上更正确的编程构造。
- 使用我们的新功能,我们可以确定数据集中涉及哪些国家:
In [7]: print(set([row["Country"] for row in dataset(data_file)]))
...: set(['Canada', 'Italy', 'France', 'Netherlands', 'Ireland',...])
- 我们还可以检查此数据集涵盖的年份范围,如下所示:
In [8]: print(min(set([int(row["Year"]) for row in dataset(data_file)])))
...:
1875
In [9]: print(max(set([int(row["Year"]) for row in dataset(data_file)])))
...:
2010
- 在以上两个示例中,我们都使用 Python 列表推导生成了一个集合。 理解是一种简洁的语句,它生成可迭代的语句,与早期的内存安全生成器非常相似。 指定了一个或多个输出变量以及 for 关键字,并且可迭代地表示该变量以及一个可选的 if 条件。 在 Python 3.6 中,集合和字典的理解也存在。 先前的国家/地区设置也可以表示如下:
In [10]: {row["Country"] for row in dataset(data_file)}
...: set(['Canada', 'Italy', 'France', 'Netherlands', 'Ireland',...])
Out[10]: {'Netherlands', Ellipsis, 'Ireland', 'Canada', 'Italy', 'France'}
- 最后,让我们只过滤美国的数据,以便我们可以对其进行独家分析:
In [11]: filter(lambda row: row["Country"] == "United States",
...: dataset(data_file))
Out[11]: <filter at 0xb1aeac8>
Python 筛选器函数根据使第一个参数指定的函数为 true 的序列或可迭代(第二个参数)的所有值创建一个列表。 在这种情况下,我们使用匿名函数(Lambda 函数)来检查指定行的“国家/地区”列中的值是否等于“美国”。
- 通过对数据集的最初发现和探索,我们现在可以使用
matplotlib来查看一些数据,它是可用于 Python 的主要科学绘图软件包之一,与 MATLAB 的绘图功能非常相似:
In [12]: import csv
...: import numpy as np
...: import matplotlib.pyplot as plt
In [13]: def dataset(path, filter_field=None, filter_value=None):
...: with open(path, 'r') as csvfile:
...: reader = csv.DictReader(csvfile)
...: if filter_field:
...: for row in filter(lambda row:
...: row[filter_field]==filter_value, reader):
...: yield row
...: else:
...: for row in reader:
...: yield row
In [14]: def main(path):
...: data = [(row["Year"], float(row["Average income per tax unit"]))
...: for row in dataset(path, "Country", "United States")]
...: width = 0.35
...: ind = np.arange(len(data))
...: fig = plt.figure()
...: ax = plt.subplot(111)
...: ax.bar(ind, list(d[1] for d in data))
...: ax.set_xticks(np.arange(0, len(data), 4))
...: ax.set_xticklabels(list(d[0] for d in data)[0::4],rotation=45)
...: ax.set_ylabel("Income in USD")
...: plt.title("U.S. Average Income 1913-2008")
...: plt.show()
In [15]: if __name__ == "__main__":
...: main("income_dist.csv")
上面的代码片段将为我们提供以下输出:

在 R 的许多章节中,使用 Python 进行数据探索的上述示例似乎都很熟悉。 加载数据集,过滤和计算范围需要几行代码和特定的类型转换,但是我们以内存安全的方式快速创建了分析。
- 当我们继续创建图表时,我们开始更多地使用
NumPy和matplotlib。 NumPy 的用法与 R 非常相似,可以将数据从 CSV 文件加载到内存中的数组,并动态确定每一列的类型。 为此,可以使用以下两个模块功能:
genfromtext:此函数使用两个主循环从存储在文本文件中的表格数据创建数组。 第一个将文件的每一行转换为字符串序列,第二个将每个字符串转换为适当的数据类型。 这有点慢,并且不如内存效率高,但是结果是在内存中存储了一个方便的数据表。 此功能还可以处理丢失的数据,而其他更快更简单的功能则不能。recfromcsv:此功能是基于genfromtext的帮助程序功能,其默认参数设置为提供对 CSV 文件的访问。
看一下以下代码片段:
In [16]: import numpy as np
...: dataset = np.recfromcsv(data_file, skip_header=1)
...: dataset
array([[ nan, 1.93200000e+03, nan, ...,
nan, 1.65900000e+00, 2.51700000e+00],
[ nan, 1.93300000e+03, nan, ...,
nan, 1.67400000e+00, 2.48400000e+00],
[ nan, 1.93400000e+03, nan, ...,
nan, 1.65200000e+00, 2.53400000e+00],
...,
[ nan, 2.00600000e+03, 4.52600000e+01, ...,
1.11936337e+07, 1.54600000e+00, 2.83000000e+00],
[ nan, 2.00700000e+03, 4.55100000e+01, ...,
1.19172976e+07, 1.53000000e+00, 2.88500000e+00],
[ nan, 2.00800000e+03, 4.56000000e+01, ...,
9.14119000e+06, 1.55500000e+00, 2.80300000e+00]])
该函数的第一个参数应该是数据源。 它应该是指向本地或远程文件的字符串,或者是具有 read 方法的类似文件的对象。 URL 将在加载之前下载到当前工作目录。 此外,输入可以是文本或压缩文件。 该功能可以识别gzip和bzip2。 这些文件需要具有.gz或.bz2扩展名才能可读。 genfromtext的值得注意的可选参数包括定界符,默认情况下,recfromcsv; skip_header和skip_footer中的定界符, (comma)分别需要从顶部或底部跳过的可选行数; 和dtype,指定单元的数据类型。 默认情况下,dtype为“无”,并且 NumPy 将尝试检测正确的格式。
- 现在,我们可以大致了解数据表的范围:
In [17]: dataset.size
...:
Out[17]: 2179
In [18]: (len(dataset)+1)*len(dataset.T[1]) # works on 3.6
...:
Out[18]: 771720
In [19]: dataset.shape
...:
Out[19]: (2179,)
注意
根据您的 NumPy 版本,您可能会看到略有不同的输出。 dataset.size 语句可能会报告返回的数据行数(2179),并且形状可能会输出为(2179,)。
ndarray的 size 属性返回矩阵中的元素数。 shape 属性返回数组中尺寸的元组。 CSV 自然是二维的,因此(m, n)元组分别指示行数和列数。
但是,使用此方法有一些陷阱。 首先,请注意,我们必须跳过标题行; genfromtxt确实通过将关键字参数名称设置为 True 来允许命名列(在这种情况下,您将不会设置skip_headers=1)。 不幸的是,在这个特定的数据集中,列名可能包含逗号。 CSV 阅读器可以正确处理此问题,因为包含逗号的字符串被引号引起来,但是genfromtxt通常不是 CSV 阅读器。 要解决此问题,要么必须修复标题,要么需要添加其他一些名称。 其次,Country列已还原为NaN,并且Year列已转换为浮点整数,这是不理想的。
- 手动修复数据集是必要的,而且这种情况并不罕见。 因为我们知道有 354 列,并且前两列是
Country和Year,所以我们可以预先计算我们的列名和数据类型:
In [20]: names = ["country", "year"]
...: names.extend(["col%i" % (idx+1) for idx in range(352)])
...: dtype = "S64,i4," + ",".join(["f18" for idx in range(352)])
...:
In [21]: dataset = np.genfromcsv(data_file, dtype=np.dtype, names=names,
...: delimiter=",", skip_header=1, autostrip=2)
...:
我们分别将前两列分别命名为 country 和 year,并将它们的数据类型指定为S64或string-64,然后将 year 列指定为i4或integer-4。 对于其余的列,我们为它们分配名称coln,其中 n 是 1 到 352 之间的整数,数据类型为f18或float-18。 这些字符长度使我们能够捕获尽可能多的数据,包括指数浮点表示形式。
不幸的是,当我们浏览数据时,我们可以看到很多表示而不是数字的nan值,浮点运算中的固定装置用于表示非数字或等价于无穷大的值 。 在管道的数据整理和清理阶段,数据丢失是一个非常普遍的问题。 似乎数据集包含许多丢失或无效的条目,这对于历史数据而言是有意义的,而对于给定的列,可能没有有效收集数据的国家/地区也是如此。
- 为了清除数据,我们使用 NumPy 掩码数组,它实际上是标准 NumPy 数组和掩码的组合,一组布尔值指示是否应在该位置使用数据进行计算。 可以按照以下步骤进行操作:
import numpy.ma as ma
ma.masked_invalid(dataset['col1'])
masked_array(data = [-- -- -- ..., 45.2599983215332
45.5099983215332 45.599998474121094],
mask = [ True True True ..., False False False],fill_value =
1e+20)
工作原理...
我们的数据集功能已经过修改,可以根据需要对单个字段和值进行过滤。 如果未指定过滤器,它将生成整个 CSV。 主要的兴趣在于主要功能中发生的事情。 在这里,我们使用 matplotlib 生成了美国每年平均收入的条形图。 让我们来看一下代码。
我们在列表理解中以(year,avg_income)元组的形式收集数据,该列表利用我们的特殊数据集方法来仅过滤美国的数据。
注意
我们必须将每个税收单位的平均收入强制转换为浮点数才能进行计算。 在这种情况下,我们将年份保留为字符串,因为它只是充当标签。 但是,为了执行datetime计算,我们可能想使用datetime.strptime (row['Year'], '%Y').date()将年份转换为日期。
完成数据收集,过滤和转换后,我们将设置图表。 宽度是条的最大宽度。 ind迭代(ndarray)是指每个条的x轴位置; 在这种情况下,我们需要为集合中的每个数据点分配一个位置。 NumPy np.arange函数类似于内置的xrange函数; 它会在给定的时间间隔内返回均等值的可迭代(ndarray)。 在这种情况下,我们提供一个终止值,该终止值是列表的长度,并使用0的默认起始值和1的步长,但也可以指定这些值。 arange的使用允许浮点参数,并且通常比简单地实例化整个值数组要快得多。
figure和subplot模块功能利用matplotlab.pyplot模块分别创建基础图形和轴。 figure函数创建一个新图形,或返回对先前创建图形的引用。 subplot函数返回由网格定义定位的子图轴,并带有以下参数:行数,列数和图号。 当所有三个自变量都小于 10 时,此功能很方便。只需提供一个三位数的数值(例如plot.subplot (111)),即可在子图 1 中创建 1 x 1 轴。
然后,我们使用子图根据数据创建条形图。 请注意,另一种理解的使用将来自我们数据集的收入值以及我们使用np.arange创建的索引进行传递。 但是,在设置x轴标签时,我们注意到,如果将所有年份都添加为单独的标签,则x轴是不可读的。 取而代之的是,我们从第一年开始每 4 年添加一次滴答声。 在这种情况下,您可以看到我们在np.arange中使用4的步长来设置刻度,并且类似地,在标签中,我们使用 Python 列表上的 slice 来遍历每四个标签。 例如,对于给定的列表,我们将使用:
mylist[s:e:t]
列表的分片开始于s,结束于e,步长为t。 切片中还支持负数,以便从列表末尾开始迭代。 例如,mylist[-1]将返回列表的最后一项。
还有更多...
NumPy 是一个非常有用且功能强大的库,但是我们应该注意一些非常重要的区别。 Python 中的列表数据类型与numpy数组完全不同。 Python 列表可以包含许多不同的数据类型,包括列表。 因此,以下示例列表是完全有效的:
python_list = ['bob' , 5.1, True, 1, [5, 3, 'sam'] ]
在幕后,Python 列表包含指向列表元素存储位置的指针。 要访问列表的第一个元素,Python 转到列表的存储位置并获取存储在该位置的第一个值。 该值是第一个元素的内存地址。 然后,Python 跳转到这个新的内存位置,以获取实际第一个元素的值。 因此,获取列表的第一个元素需要两次内存查找。
NumPy 数组非常类似于 C。它们必须包含单个数据类型,这允许将数组存储在连续的内存块中,这使读取数组的速度显着提高。 当读取数组的第一个元素时,Python 转到包含要检索的实际值的适当内存地址。 当需要数组中的下一个元素时,它紧挨着内存中第一个元素的位置,这使读取速度更快。
另请参见
分析和可视化美国的最高收入数据
现在,我们已经导入并探索了最高收入数据集,现在让我们深入研究特定国家并对其收入分配数据进行一些分析。 特别是,美国拥有与百分比最高的收入相关的出色数据,因此我们将在以下练习中使用美国的数据。 如果选择其他国家/地区来利用其数据集,请注意,可能需要使用不同的字段来进行相同的分析。
准备
为了进行分析,我们将创建一些辅助方法,在本章中将不断使用这些方法。 面向应用的分析通常会产生可重复使用的代码,这些代码执行单个任务,以便快速适应不断变化的数据或分析要求。 特别地,让我们创建两个帮助器函数:一个用于按特定国家/地区提取数据的函数,以及一个用于从一组特定行中创建时间序列的函数:
In [22]: def dataset(path, country="United States"):
...: """
...: Extract the data for the country provided. Default is United States.
...: """
...: with open(path, 'r') as csvfile:
...: reader = csv.DictReader(csvfile)
...: for row in filter(lambda row: row["Country"]==country,reader):
...: yield row
...:
In [23]: def timeseries(data, column):
...: """
...: Creates a year based time series for the given column.
...: """
...: for row in filter(lambda row: row[column], data):
...: yield (int(row["Year"]), row[column])
...:
第一个函数使用 Python 的内置过滤器函数在特定国家/地区使用csv.DictReader过滤器遍历数据集。 第二个函数利用了Year列为数据创建时间序列这一事实,一个生成器为数据集中的特定列生成(年,值)元组。 请注意,此函数应在由数据集函数创建的生成器中传递。 现在,我们可以利用这两个功能对单个国家/地区的任何列进行一系列分析。
怎么做...
一般而言,美国的数据分为六组:
- 前 10%的收入份额
- 收入比例最高的 5%
- 收入比例最高的 1%
- 收入份额最高的 0.5%
- 收入份额最高的 0.1%
- 平均收入份额
这些组反映了在这些特定容器中收集的数据点的汇总。 一个简单快速的第一分析方法是,简单地绘制出每个最高收入组随时间变化的收入份额的这些百分比。 由于绘制多个时间序列将是一项常见的任务,因此让我们再次创建一个helper函数,该函数包装 matplotlib 并为传递给它的每个时间序列生成一个折线图:
In [24]: def linechart(series, **kwargs):
...: fig = plt.figure()
...: ax = plt.subplot(111)
...: for line in series:
...: line = list(line)
...: xvals = [v[0] for v in line]
...: yvals = [v[1] for v in line]
...: ax.plot(xvals, yvals)
...: if 'ylabel' in kwargs:
...: ax.set_ylabel(kwargs['ylabel'])
...: if 'title' in kwargs:
...: plt.title(kwargs['title'])
...: if 'labels' in kwargs:
...: ax.legend(kwargs.get('labels'))
...: return fig
...:
这个功能很简单。 它会创建一个matplotlib.pyplot图形以及轴子图。 对于系列中的每一行,它获得x轴值(请记住,我们的时间序列时间元组中的第一项是Year)作为元组的第一项,而 y [ 轴,这是第二个值。 它将它们分成单独的生成器,然后将它们绘制在图形上。 最后,我们想要图表的任何选项,例如标签或图例,我们都可以简单地将其作为关键字参数传递,而我们的函数将为我们处理这些选项! 以下步骤将引导您完成此面向应用分析的食谱。
为了生成图表,我们只需要在想要的列上使用时间序列函数,并将它们传递给linechart函数。 现在,这个简单的任务是可重复的,在接下来的几张图表中,我们将使用几次:
In [25]: def percent_income_share(source):
...: """
...: Create Income Share chart
...: """
...: columns = (
...: "Top 10% income share",
...: "Top 5% income share",
...: "Top 1% income share",
...: "Top 0.5% income share",
...: "Top 0.1% income share",
...: )
...: source = list(dataset(source))
...: return linechart([timeseries(source, col) for col in columns],
...: labels=columns,
...: ,
...: ylabel="Percentage")
...:
...:
注意,我也将这个图表的生成也包装在一个函数中。 这样,我们可以根据需要修改图表,该函数将包装图表的配置和生成。 该函数标识线系列的列,然后获取数据集。 对于每一列,它都会创建一个时间序列,然后使用我们的配置选项将这些时间序列传递给我们的linechart函数:
- 要生成图,我们将输入参数定义为
percent_income_source function:
In [26]: percent_income_share(data_file)
...:
Out[26]:
下面的屏幕快照显示了结果,在本章的其余部分中,您将使用类似的模式来使用函数来创建所需的图:

该图告诉我们,收入组的原始百分比倾向于朝同一方向移动。 当一组的收入增加时,另一组的收入也增加。 这似乎是一项很好的理智检查,因为收入最高的 0.1%人群也位于收入最高的 10%人群中,他们为每个分类箱的总体均值做出了很大贡献。 每条线之间也存在明显的,持久的差异。
- 查看原始百分比很有用,但我们可能还想考虑相对于该收入组的平均百分比,百分比随时间的变化情况。 为此,我们可以计算每个组百分比的平均值,然后将所有组的值除以我们刚才计算的平均值。
由于均值归一化是我们可能要在一系列数据集上执行的另一个常见函数,因此我们将再次创建一个函数,该函数接受一个时间序列作为输入并返回一个新的时间序列,其值除以均值:
In [27]: def normalize(data):
...: """
...: Normalizes the data set. Expects a timeseries input
...: """
...: data = list(data)
...: norm = np.array(list(d[1] for d in data), dtype="f8")
...: mean = norm.mean()
...: norm /= mean
...: return zip((d[0] for d in data), norm)
...:
- 现在,我们可以轻松编写另一个函数,这些函数接受这些列并计算平均归一化的时间序列:
In [28]: def mean_normalized_percent_income_share(source):
...: columns = (
...: "Top 10% income share",
...: "Top 5% income share",
...: "Top 1% income share",
...: "Top 0.5% income share",
...: "Top 0.1% income share",
...: )
...: source = list(dataset(source))
...: return linechart([normalize(timeseries(source, col)) for col in columns],
...: labels=columns,
...: ,
...: ylabel="Percentage")
...: mean_normalized_percent_income_share(data_file)
...: plt.show()
...:
请注意,除了执行归一化时,以下命令片段与上一个功能非常相似:
>>> fig = mean_normalized_percent_income_share(DATA)
>>> fig.show()
前面的命令为我们提供了下图:

此图向我们显示,该组越富裕,我们倾向于在其收入中看到的百分比波动越大。
- 数据集还将组的收入分为几类,例如包括资本收益的收益与没有资本收益的收益。 让我们看一下每个组的资本收益收入如何随时间波动。
另一个常见功能是计算两列之间的差异,并绘制结果时间序列。 计算两个 NumPy 数组之间的差异也非常容易,并且由于这对于我们的任务很常见,因此我们编写了另一个函数来完成这项工作:
In [29]: def delta(first, second):
...: """
...: Returns an array of deltas for the two arrays.
...: """
...: first = list(first)
...: years = yrange(first)
...: first = np.array(list(d[1] for d in first), dtype="f8")
...: second = np.array(list(d[1] for d in second), dtype="f8")
...: if first.size != second.size:
...: first = np.insert(first, [0,0,0,0], [None, None, None,None])
...: diff = first - second
...: return zip(years, diff)
...:
此外,以下是适当的帮助程序功能:
In [30]: def yrange(data):
...: """
...: Get the range of years from the dataset
...: """
...: years = set()
...: for row in data:
...: if row[0] not in years:
...: yield row[0]
...: years.add(row[0])
此函数再次从每个数据集创建 NumPy 数组,将数据类型转换为浮点型。 请注意,我们需要从一个数据集中获取年份列表,因此我们从第一个数据集中收集它。
- 我们还需要记住,
first.size必须与second.size相同,例如,每个数组都具有相同的维数。 计算差异,然后将年份再次压缩到数据中以形成时间序列:
In [31]: def capital_gains_lift(source):
...: """
...: Computes capital gains lift in top income percentages over time chart
...: """
...: columns = (
...: ("Top 10% income share-including capital gains",
...: "Top 10% income share"),
...: ("Top 5% income share-including capital gains",
...: "Top 5% income share"),
...: ("Top 1% income share-including capital gains",
...: "Top 1% income share"),
...: ("Top 0.5% income share-including capital gains",
...: "Top 0.5% income share"),
...: ("Top 0.1% income share-including capital gains",
...: "Top 0.1% income share"),
...: ("Top 0.05% income share-including capital gains",
...: "Top 0.05% income share"),
...: )
...: source = list(dataset(source))
...: series = [delta(timeseries(source, a), timeseries(source,b))
...: for a, b in columns]
...: return linechart(series,labels=list(col[1] for col in
...: columns),
...: ,
...: ylabel="Percentage Difference")
...: capital_gains_lift(data_file)
...: plt.show()
...:
前面的代码将列存储为第一列和第二列两列的元组,然后使用 delta 函数计算两者之间的差。 像我们之前的图形一样,它随后会创建一个折线图,如下所示:

这很有趣,因为该图显示了资本收益收入随时间推移的波动性。 如果您熟悉美国的财务历史,则可以在此图表中看到著名股票市场的兴衰对资本收益收入的影响。
工作原理...
对大型数据集执行操作的最简单方法是使用 NumPy 的array类。 正如我们已经看到的,此类允许我们执行常见的运算,包括标量和数组之间的基本数学运算。 但是,将生成器转换为数组需要我们将数据加载到内存中。 Python 的内置list函数采用一个迭代器并返回一个列表。 这是必需的,因为 NumPy 数组必须知道数据的长度才能分配正确的内存量。 使用该数组,可以很容易地计算出平均值,然后在整个数组上执行除法等于标量的运算。 这会广播除法运算,以便将数组中的每个元素均除以均值。 本质上,我们是通过均值对数组元素进行归一化。 然后,我们可以将年与新计算的数据压缩在一起,并返回时间序列。
进一步分析美国的最高收入群体
到目前为止,在本章中,我们着重分析了一段时间内的收入百分比。 接下来,我们将通过查看数据集中其他有趣的数据,继续进行分析,特别是实际收入数据和构成这些数据的收入类别。
准备
如果您已完成先前的食谱,并分析并可视化了美国的最高收入数据,则应具备继续进行所需的所有操作。
怎么做...
通过以下步骤,我们将更深入地研究数据集并检查其他收入数字:
- 该数据集还包含不同组别的按年平均收入。 让我们用图形表示它们,看看它们随着时间相对于彼此如何变化:
In [32]: def average_incomes(source):
...: """
...: Compares percentage average incomes
...: """
...: columns = (
...: "Top 10% average income",
...: "Top 5% average income",
...: "Top 1% average income",
...: "Top 0.5% average income",
...: "Top 0.1% average income",
...: "Top 0.05% average income",
...: )
...: source = list(dataset(source))
...: return linechart([timeseries(source, col) for col in
...: columns], labels=columns, ,
...: ylabel="2008 US Dollars")
...: average_incomes(data_file)
...: plt.show()
由于我们具有创建折线图的基础,因此我们可以立即使用已有的工具分析此新数据集。 我们只需选择不同的列集合,然后相应地自定义图表即可! 以下是结果图:

该图显示的结果非常引人入胜。 直到 1980 年代,富人的收入大约比低收入人群高了 1-150 万美元。 从 1980 年代开始,这种差距急剧增加。
- 我们还可以使用
delta功能来查看有钱人比普通美国人有多少钱:
In [33]: def average_top_income_lift(source):
...: """
...: Compares top percentage avg income over total avg
...: """
...: columns = (
...: ("Top 10% average income", "Top 0.1% average income"),
...: ("Top 5% average income", "Top 0.1% average income"),
...: ("Top 1% average income", "Top 0.1% average income"),
...: ("Top 0.5% average income", "Top 0.1% average income"),
...: ("Top 0.1% average income", "Top 0.1% average income"),
...: )
...: source = list(dataset(source))
...: series = [delta(timeseries(source, a), timeseries(source,
...: b)) for a, b in columns]
...: return linechart(series,labels=list(col[0] for col in columns),
...: ,ylabel="2008 US Dollars")
...:
除了选择列和利用已经添加到项目中的功能之外,我们还没有编写新代码。 这揭示了以下内容:

- 在我们的上一次分析中,我们将展示另一种图表,以查看最富有的美国人的收入构成。 由于组成是基于百分比的时间序列,因此该任务的一个很好的图表是堆积区域。 再一次,我们可以利用时间序列代码并简单地添加一个函数来创建堆积面积图,如下所示:
In [34]: def stackedarea(series, **kwargs):
...: fig = plt.figure()
...: axe = fig.add_subplot(111)
...: fnx = lambda s: np.array(list(v[1] for v in s), dtype="f8")
...: yax = np.row_stack(fnx(s) for s in series)
...: xax = np.arange(1917, 2008)
...: polys = axe.stackplot(xax, yax)
...: axe.margins(0,0)
...: if 'ylabel' in kwargs:
...: axe.set_ylabel(kwargs['ylabel'])
...: if 'labels' in kwargs:
...: legendProxies = []
...: for poly in polys:
...: legendProxies.append(plt.Rectangle((0, 0), 1, 1,
...: fc=poly.get_facecolor()[0]))
...: axe.legend(legendProxies, kwargs.get('labels'))
...: if 'title' in kwargs:
...: plt.title(kwargs['title'])
...: return fig
...:
前面的函数期望一组时间序列,其总百分比总计为 100。我们创建了一个特殊的匿名函数,该函数会将每个序列转换为 NumPy 数组。 NumPy row_stack函数创建一个垂直堆叠的数组序列; 这就是使用subplot.stackplot函数生成堆栈图的原因。 此功能的唯一另一个惊喜是需要使用图例代理来创建具有图例中堆栈图填充颜色的矩形。
- 现在,我们来看看最富有的美国人的收入构成:
In [35]: def income_composition(source):
...: """
...: Compares income composition
...: """
...: columns = (
...: "Top 10% income composition-Wages, salaries andpensions",
...: "Top 10% income composition-Dividends",
...: "Top 10% income composition-Interest Income",
...: "Top 10% income composition-Rents",
...: "Top 10% income composition-Entrepreneurial income",
...: )
...: source = list(dataset(source))
...: labels = ("Salary", "Dividends", "Interest", "Rent","Business")
...: return stackedarea([timeseries(source, col) for col in columns],
...: labels=labels, ,
...: ylabel="Percentage")
...:
前面的代码生成以下图:

如您所见,美国收入最高的 10%的人大部分收入是来自薪金收入。 但是,营业收入也起着很大的作用。 与本世纪末相比,股息在本世纪初的作用更大,利息和租金也是如此。 有趣的是,在 20 世纪上半叶,与企业家收入相关的收入百分比一直下降,直到 1980 年代,它再次开始增长,这可能是由于技术部门的原因。
工作原理...
该食谱确实有助于证明面向应用方法的价值。 我们继续提取执行单一任务的代码部分,并将其用作函数。 随着我们功能库的增加,我们的分析通常充满重复但略有不同的任务,变得比创建更复杂。 更好的是,这些单独的部分更易于测试和评估。 随着时间的流逝,我们将通过额外的分析,建立一个功能丰富且完全定制的工具库,这将大大加快未来的调查速度。
该食谱还揭示了如何创建 Python 代码以构建更多类似于 R 的分析。 在进行进一步评估时,我们利用了已经为数据集构建的功能和工具,并创建了新的功能和工具,例如以旧工具为基础的堆积面积函数。 但是,与面向分析的方法不同,这些工具现在存在于特定于数据的代码库中,该代码库可用于构建应用和报告,我们将在下一个食谱中看到。
使用 Jinja2 进行报告
可视化和图形非常适合识别数据集中的明显模式。 但是,由于趋势来自多种来源,因此需要更深入的报告,以及对不直接参与该项目的技术人员的描述。 面向应用的分析不是手动创建这些报告,而是利用模板语言在分析时动态构建文档。 Jinja2 是一个 Python 库,用于通过组合模板(通常是 HTML 文件)来生成文档,但它也可以是任何类型的文本文件,并带有上下文(用于填充模板的数据源)。 这种组合非常适合报告我们正在执行的分析。
准备
Jinja2 模板库应已安装并可以使用。
怎么做...
以下步骤将引导我们逐步使用 Jinja2 模板库创建灵活且吸引人的报告输出:
- Jinja2 很简单,并且具有熟悉的 Python 风格的语法(尽管它不是 Python)。 模板可以包括逻辑或控制流,包括迭代,条件和格式,这消除了使数据适应模板的需要。 一个简单的例子如下:
In [36]: from jinja2 import Template
...: template = Template(u'Greetings, {{ name }}!')
...: template.render(name='Mr. Praline')
Out[36]: 'Greetings, Mr. Praline!'
- 但是,我们应该将模板与 Python 代码分离,而应将模板存储为文本文件到我们的系统中。 Jinja2 提供了一个中央
Environment对象,该对象用于存储配置和全局对象以从文件系统或其他 Python 包中加载模板:
In [37]: from jinja2 import Environment, PackageLoader, FileSystemLoader
...: # 'templates' should be the path to the templates folder
...: # as written, it is assumed to be in the current directory
...: jinjaenv = Environment(loader = FileSystemLoader('templates'))
...: template = jinjaenv.get_template('report.html')
在这里,Jinja2 环境被配置为在我们的 Python 模块的 templates 目录中查找模板文件。 另一个推荐的加载程序是FileSystemLoader,应将其作为搜索路径来查找模板文件。 在这种情况下,从 Python 模块中提取了名为report.html的模板,并准备好进行渲染。
渲染可以像template.render(context)一样简单,它将返回生成的输出的 unicode 字符串。 上下文应该是 Python 字典,其关键字是将在模板中使用的变量名。 或者,可以将上下文作为关键字参数传入; template.render({'name':'Terry'})等效于template.render(name='Terry')。 但是,对于大型模板(和大型数据集),使用template.stream方法效率更高。 它不会立即渲染整个模板,而是顺序评估每个语句并将其作为生成器产生。
- 然后,可以将流传递到类似文件的对象,以将其写入磁盘或通过网络进行序列化:
template.stream(items=['a', 'b', 'c'],name='Eric').dump('report-2013.html')
这种看似简单的技术非常强大,尤其是与 JSON 模块结合使用时。 JSON 数据可以直接转储到 JavaScript 片段中,以用于 Web 上的交互式图表和可视化库,例如 D3,Highcharts 和 Google Charts。
- 让我们来看一个使用世界上最高收入数据集的完整示例:
In [38]: import csv
...: import json
...: from datetime import datetime
...: from jinja2 import Environment, PackageLoader, FileSystemLoader
...: from itertools import groupby
...: from operator import itemgetter
...:
In [39]: def dataset(path, include):
...: column = 'Average income per tax unit'
...: with open(path, 'r') as csvfile:
...: reader = csv.DictReader(csvfile)
...: key = itemgetter('Country')
...: # Use groupby: memory efficient collection by country
...: for key, values in groupby(reader, key=key):
...: # Only yield countries that are included
...: if key in include:
...: yield key, [(int(value['Year']),
...: float(value[column]))
...: for value in values if value[column]]
...:
...:
In [40]: def extract_years(data):
...: for country in data:
...: for value in country[1]:
...: yield value[0]
...:
...: datetime.now().strftime("%Y%m%d")
...: jinjaenv = Environment(loader = FileSystemLoader('templates'))
...: template = jinjaenv.get_template('report.html')
...: template.stream(context).dump(path)
...:
In [41]: def write(context):
...: path = "report-%s.html" %datetime.now().strftime("%Y%m%d")
...: jinjaenv = Environment(loader = FileSystemLoader('templates'))
...: template = jinjaenv.get_template('report.html')
template.stream(context).dump(path)
In [40]: def main(source):
...: # Select countries to include
...: include = ("United States", "France", "Italy",
...: "Germany", "South Africa", "New Zealand")
...: # Get dataset from CSV
...: data = list(dataset(source, include))
...: years = set(extract_years(data))
...: # Generate context
...: context = {
...: 'title': "Average Income per Family, %i - %i"
...: % (min(years), max(years)),
...: 'years': json.dumps(list(years)),
...: 'countries': [v[0] for v in data],
...: 'series': json.dumps(list(extract_series(data, years))),
...: }
...:
...:
In [42]: write(context)
...: if __name__ == '__main__':
...: source = '../data/income_dist.csv'
...: main(source)
这是很多代码,所以让我们逐步进行一下。 数据集功能读取我们的 CSV 文件的“平均收入”列,并根据一组包含的国家/地区进行过滤。 它使用函数迭代helper和groupby,该迭代通过国家/地区字段收集 CSV 文件的行,这意味着我们将获得每个国家/地区的数据集。 itemgetter和groupby函数都是 Python 中常用的,内存安全的辅助函数,它们在大规模数据分析过程中会产生很多繁重的工作。
提取数据集后,我们有两种帮助方法。 第一个extract_years生成每个国家/地区的所有年份值。 这是必要的,因为并非所有国家/地区都具有数据集中每年的值。 我们还将使用此功能来确定模板的年份范围。 这将我们带到第二个函数extract_series,该函数将数据标准化,用 None 值替换空年份以确保我们的时间序列正确。
write()方法包装模板编写功能。 它创建一个名为report-{date}.html的文件,并添加当前日期以供参考。 它还将加载 Environment 对象,找到报告模板,并将输出写入磁盘。 最后,main 方法将所有数据和上下文收集在一起并连接功能。
- 报告模板如下:
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<div class="container">
<h1>{{ title }}</h1>
<div id="countries">
<ul>
{% for country in countries %}
<li>{{ country }}<li>
{% endfor %}
</ul>
</div>
<div id="chart"></div>
</div>
<script type="text/javascript"
src="http://codeorigin.jquery.com/jquery-
2.0.3.min.js"></script>
<script src="http://code.highcharts.com/highcharts.js">
</script>
<script type="text/javascript">
$.noConflict();
jQuery(document).ready(function($) {
$('#chart').highcharts({
xAxis: {
categories: JSON.parse('{{ years }}'),
tickInterval: 5,
},
yAxis: {
title: {
text: "2008 USD"
}
},
plotOptions: {
line: {
marker: {
enabled: false
}
}
},
series: JSON.parse('{{ series }}')
});
});
</script>
</body>
</html>
前面的模板会在正确的位置填写标题,然后创建数据集中包含的国家的无序列表。 此外,它使用 Highcharts 创建交互式图表。 Highcharts 是基于选项的 JavaScript 图表库。 请注意,我们正在使用JSON.parse解析在 Python 中转储的 JSON 数据。 这样可以确保将 Python 数据类型转换为 JavaScript 数据类型时不会发生冲突。 在浏览器中打开报表时,它应类似于以下屏幕截图:

工作原理...
在 Python 中执行分析和数据挖掘与 R 紧密并行,尤其是在使用 NumPy 库时。 像 R 一样,NumPy 也专为科学计算而设计,在处理多维数组时具有类似的功能集。 但是,一般来说,Python 需要花费更多的代码行,尤其是在使用 matplotlib 创建图表时。 这是由 Python 的通用数据方法引起的,特别是因为它用于许多问题领域,而不是专门用于统计分析,这也是 Python 的强项。
特别是,使用 Python 进行的数据分析倾向于采用面向应用的方法,通常涉及实时更新的实时数据或流数据,而不是对单个数据集进行分析。 这通常意味着用 Python 执行的分析利用诸如 NumPy 之类的工具进行快速原型制作和统计探索,但随后利用极其包容的标准库来处理数据管道的所有阶段中的数据。
还有更多...
有几种不同的 Python 模板语言,每种都有不同的方法将预定义的模板与数据结合起来以形成易于阅读的输出。 这些模板语言中的许多旨在用作 Web 应用框架的骨干,例如 Django 和 Flask,它们用于从数据库构造动态网页。 由于这些语言非常适合生成 HTML,因此使用这些工具进行报告可以轻松地从一次性报告转换为计划报告,再到从 Web 应用按需报告。 Jinja2 是 Flask 的主要模板语言,具有类似于 Django 的语法,使其成为将来实现的绝佳选择。
另请参见
在 R 中重复分析
本简短的调查会议旨在使用 R 软件复制上一部分中讨论的大多数数据分析。 在不依赖任何 R 包的意义上,该部分是独立的。
准备
R 默认版本中可用的功能足以执行本章前面的分析。 income_dist.csv文件需要存在于当前工作目录中。
怎么做...
如下一个程序所示,可以很容易地逐步执行与income_dist.csv文件相关的分析。
- 使用
read.csv函数加载数据集income_dist.csv并使用函数nrow,str,length和unique等,以得到以下结果:
id <- read.csv("income_dist.csv",header=TRUE)
nrow(id)
str(names(id))
length(names(id))
ncol(id) # equivalent of previous line
unique(id$Country)
levels(id$Country) # alternatively
min(id$Year)
max(id$Year)
id_us <- id[id$Country=="United States",]
首先将数据存储在 R 对象 ID 中。 我们看到数据集中有 2180 个观测值/行。 该数据集具有 354 个变量,使用两个函数str和names可以看到一些变量。 变量的数量也使用ncol和长度函数进行验证。 通过代码id[id$Country=="United States",]选择与美国相关的数据。 现在,我们首先使用图函数来获得平均所得税的第一视图,这是一个糟糕的图。
- 使用
plot功能,我们得到一个简单的显示,如下所示:
plot(id_us$Year , id_us$Average.income.per.tax.unit) 0
这里没有给出输出,因为我们打算即兴进行输出。 现在,我们使用barplot函数代替绘图。
- 使用
barplot功能以及适当的标签选择,可获得优雅的显示效果:
barplot(id_us$Average.income.per.tax.unit,ylim=c(0,60000),
ylab="Income in USD",col="blue",main="U.S. Average Income 1913-2008",
names.arg=id_us$Year)

使用图形功能选项始终是一个好习惯。 例如,我们通过ylim指定 y 变量的范围,通过ylab指定 y 轴标签的范围,并通过 main 指定图形的标题。
为了进一步分析,我们继续只关注美国地区:
- 如下面的配方中对美国最高收入数据的分析,并在以下程序中的 R 中再现了的可视化,显示了美国最高收入数据。 在美国地区进行子集设置后,我们使用
[中的子集选择 10%,5%,1%,0.5%和 0.1%的特定变量。 新的 R 对象是id2_us2:
id2 <- read.csv("income_dist.csv",header=TRUE,check.names = F)
# using the check.names=F option to ensure special characters in colnames
id2_us <- id2[id$Country=="United States",]
id2_us2 <- id2_us[,c("Top 10% income share",
"Top 5% income share",
"Top 1% income share",
"Top 0.5% income share",
"Top 0.1% income share")]
row.names(id2_us2) <- id2_us$Year
- 使用
ts功能将 R 对象id2_us2转换为时间序列对象。 现在,对于这种特定的数据选择,我们使用下一部分 R 代码逐年对其进行可视化处理:
id2_us2 <- ts(id2_us2,start=1913,frequency = 1)
windows(height=20,width=10)
plot.ts(id2_us2,plot.type="single",ylab="Percentage",frame.plot=TRUE,
col=c("blue","green","red","blueviolet","purple"))
legend(x=c(1960,1980),y=c(45,30),c("Top 10%","Top 5%","Top 1%","Top 0.5%","Top 0.1%"),
col = c("blue","green","red","blueviolet","purple"),pch="-")
请注意,对象id2_us2具有五个时间序列对象。 我们使用选项plot.type="single"将它们全部绘制在一个帧中。 图例和颜色用于增强图形显示的美观性。 产生的图形输出如下:

- 使用定标数据重复前面的练习:
id2_scale <- scale(id2_us2)
windows(height=20,width=10)
plot.ts(id2_scale,plot.type="single",ylab="Percentage",frame.plot=TRUE,
col=c("blue","green","red","blueviolet","purple"))
legend(x=c(1960,1980),y=c(2,1),c("Top 10%","Top 5%","Top 1%","Top 0.5%","Top 0.1%"), col = c("blue","green","red","blueviolet","purple"),pch="-")
输出如下:

- 请注意,此显示使五个时间序列之间的比较更加容易。
- 为了在中复制 Python 分析,进一步分析 R 中的美国最高收入组和配方,我们在代码的最后一个块中提供 R 代码并输出:
id2_us3 <- id2_us[,c("Top 10% income share-including capital gains",
"Top 10% income share",
"Top 5% income share-including capital gains",
"Top 5% income share",
"Top 1% income share-including capital gains",
"Top 1% income share",
"Top 0.5% income share-including capital gains",
"Top 0.5% income share",
"Top 0.1% income share-including capital gains",
"Top 0.1% income share",
"Top 0.05% income share-including capital gains",
"Top 0.05% income share")
]
id2_us3[,"Top 10% capital gains"] <- id2_us3[,1]-id2_us3[,2]
id2_us3[,"Top 5% capital gains"] <- id2_us3[,3]-id2_us3[,4]
id2_us3[,"Top 1% capital gains"] <- id2_us3[,5]-id2_us3[,6]
id2_us3[,"Top 0.5% capital gains"] <- id2_us3[,7]-id2_us3[,8]
id2_us3[,"Top 0.1% capital gains"] <- id2_us3[,9]-id2_us3[,10]
id2_us3[,"Top 0.05% capital gains"] <- id2_us3[,11]-id2_us3[,12]
id2_us3 <- ts(id2_us3,start=1913,frequency = 1)
windows(height=20,width=10)
plot.ts(id2_us3[,13:18],plot.type="single",ylab="Percentage",frame.plot=TRUE,
col=c("blue","green","red","blueviolet","purple","yellow"))
legend(x=c(1960,1980),y=c(7,5),c("Top 10%","Top 5%","Top 1%","Top 0.5%",
"Top 0.1%","Top 0.05%"),
col = c("blue","green","red","blueviolet","purple","yellow"),pch="-")
图形输出如下:

还有更多...
R 和 Python 是最竞争,最引人注目的和最完整的软件中的两个。 在大多数情况下,可以在 R 中复制 Python 分析,反之亦然。 不用说,不可能获得另一软件的确切答案。 但是,这不是本食谱的目的。
四、股票市场数据建模
在本章中,我们将介绍:
- 获取股市数据
- 汇总数据
- 清理和浏览数据
- 产生相对估值
- 筛选库存并分析历史价格
简介
本章将引导您完成一个财务分析项目,在此项目中,您将分析股票市场数据,确定股票是被高估还是被低估,使用此信息来识别可以进行良好投资的目标股票列表,并直观地分析价格历史 的目标股票。
我们必须警告,本章的目的不是要使您成为股票分析专家或使您变得有钱。 《华尔街定量研究》研究的工程模型所执行的操作比我们将在此处涉及的操作要复杂得多。 整本书都写有关于股票市场模型和金融工程的书,但是我们只有一章专门讨论这个主题。 因此,鉴于时间和格式的限制,本章的目标将是:
- 基本了解我们将使用的数据
- 寻找有用且有趣的方式来分析和建模数据
- 要了解如何利用数据科学工具和技术来执行我们需要对数据执行的分析任务的类型
我们将在本章中使用的数据包括网站 finviz.com 跟踪的股票的当前数据以及从 Yahoo!获得的股价的每日历史记录。 金融。
如前几章所述,我们将用于该项目的工具将是 R 统计编程语言。 您可能已经注意到,R 具有强大的软件包,可以帮助我们完成所需的分析任务。 我们将在本章中利用其中一些软件包。 此外,本章中的方法将大致遵循数据科学的流程,我们将根据我们正在使用的数据类型以及我们希望对数据进行的分析类型进行调整。
要求
对于本章,您将需要一台可访问互联网的计算机。 您还需要安装 R 并安装以下软件包,然后再加载:
install.packages("XML")
install.packages("ggplot2")
install.packages("plyr")
install.packages("reshape2")
install.packages("zoo")
library(XML)
library(ggplot2 ,quietly=TRUE)
library(plyr ,quietly=TRUE)
library(reshape2 ,quietly=TRUE)
library(zoo ,quietly=TRUE)
XML软件包将帮助我们从互联网上获取数据,ggplot2将使我们能够从数据中创建漂亮的图形和可视化图像,plyr和reshape2将帮助我们汇总数据,而zoo 一揽子计划将使我们能够计算移动平均线。
您还将需要设置一个工作目录,在该目录中将保存我们生成的某些图表:
setwd("path/where/you/want/to save/charts")
正在获取股市数据
如果您在互联网上查找股票市场数据,您会很快发现自己被提供股票行情和财务数据的资源所淹没。 获取数据时一个重要且经常被忽略的因素是获取数据的效率。 在其他所有条件都相同的情况下,您不想花费数小时将您可以在更短的时间内获取的数据集拼凑在一起。 考虑到这一点,我们将尝试从最少数量的源中获取最大量的数据。 这不仅有助于保持数据尽可能一致,而且还可以提高分析的可重复性和结果的可重复性。
怎么做...
我们要获取的第一笔数据是我们要分析的股票的快照。 最好的方法之一是从现有的许多股票筛选器应用之一下载数据。 我们最喜欢的从其中下载股票数据的筛选器属于这个页面。
让我们在以下步骤的帮助下获取将在本章中使用的股票市场数据:
- 首先,让我们拉起这个页面上的 finviz 股票筛选器:

注意
如您所见,该站点具有多个可以过滤的字段。 如果单击 All 选项卡,则可以看到所有可以显示的字段。
- 对于此项目,我们要导出筛选器中所有公司的所有字段。 您可以在编写本文时通过选中 69 个复选框来自定义筛选器,也可以使用以下 URL 自动显示所有字段:
现在,您应该看到带有所有可用字段的筛选器。
- 如果一直滚动到屏幕的右下角,应该有一个
export链接。 单击此链接,然后将 CSV 文件另存为finviz.csv。 - 最后,我们将启动 RStudio,从保存文件的路径中读取
finviz.csv文件,并将其分配给数据帧,如下所示:
finviz <- read.csv("path/finviz.csv")
注意
在数据分析中,最好将执行的每个步骤都包含在代码中,而不是将其作为一系列需要人工干预的点击操作。 这样,可以更轻松,更快地重现您的结果。
- 首次执行步骤 1 至 4 (并从浏览器中聪明地读取了 URL)之后,我们可以使用以下两个命令替换前面的代码行:
url_to_open <-
'http://finviz.com/export.ashx?v=152&c=0,1,2,3,4,5,6,7,8,9,10,
11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31
,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,5
2,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68'
finviz <- read.csv(url(url_to_open))
注意
注意步骤 2 中 URL 的结构; 它包含我们要选择的复选框的逗号分隔列表。 您可以以编程方式生成此 URL,以轻松选择要下载的公司数据的任意组合。 如果要避免输入数字 0 到 68,可以结合使用sprintf和粘贴命令来完成相同的操作:url_to_open <- sprintf("http://finviz.com/export.ashx?v=152&c=%s", paste(0:68, collapse = ","))
汇总数据
现在,我们已经获取了库存数据,让我们使用几个命令找出数据中包含哪些字段,并获取有关这些字段中包含的值的一些有用信息。
准备
您将需要从以前的配方和要获取股票市场数据中下载的数据来开始汇总。
怎么做...
以下步骤将引导您快速浏览数据摘要:
- 查看使用以下命令导入的字段:
> head(finviz[,1:4])
该命令将向您显示数据的前六行,如以下代码片段所示,以便您可以查看数据中包含哪些字段以及该字段的可能值示例。 在此示例中,我们还可以看到有一些丢失的数据,由NA标识:
No. Ticker Company Sector
1 1 A Agilent Technologies Inc. Healthcare
2 2 AA Alcoa, Inc. Basic Materials
3 3 AADR WCM/BNY Mellon Focused Growth ADR ETF Financial
4 4 AAIT iShares MSCI AC Asia Information Tech Financial
5 5 AAL American Airlines Group Inc. Services
6 6 AAMC Altisource Asset Management Corporation Financial
- 下一条命令将返回每个字段的摘要。 对于数字字段,它将告诉您最小,最大,均值,中位数和四分位数;对于字符字段,它将告诉您最常出现的字段:

工作原理...
既然我们已经初步了解了数据,那么花点时间找出对我们最重要的字段并了解这些字段的含义就很重要。
前几个字段包含有关公司的标识信息。
股票代码(有时也称为股票代号)是公司股票的标识符。 没有两家公司会使用完全相同的股票代号。 因此AA始终为Alcoa,AAPL始终为Apple,依此类推。
接下来,我们有公司的名称,部门,行业和公司的母国。 行业和行业详细信息可作为对股票进行分类的方式,以告知我们每个公司的主要业务范围; 行业更一般(较高级别),行业更具体(较低级别)。 例如,Apple Inc.(AAPL)在消费品领域,主要在电子设备行业生产消费品。
还有更多...
一旦我们越过了这些字段,数据集中的其他大多数字段都是数字。 让我们定义一些最重要的:
- 价格:这表示购买公司股票的当前美元价值。
- 交易量:这表示一天内交易的股票的最新数量。
- 发行在外的股票:这是公司已发行的股票总数。
- 市盈率:市盈率是公司股票的价格除以公司在外每股收益。
- PEG:市盈率增长率是公司的市盈率除以年增长率,它使您对公司收益相对于其增长率的估值有所了解。
- 明年每股收益增长:这是公司明年的每股收益的预期增长率。
- 总债务/权益:总债务与权益之比用作财务状况的度量,方法是将公司总债务的美元价值除以公司权益。 这使您对公司如何为增长和运营进行融资提供了一种感觉。 债务比股本更具风险,因此高比率将引起人们的关注。
- Beta:这是相对于整个股票市场的股票波动(价格波动)的度量。 Beta 值为 1 表示股票与市场一样动荡。 Beta 大于 1 意味着它的波动性更大,而 Beta 小于 1 意味着它的波动性较小。
- RSI:相对强度指数是基于股票价格活动的度量标准,该指标使用的是股票收盘价高于开盘价的天数和股票收盘价低于开盘价的天数。 在最近两周内确定 0 到 100 之间的得分。较高的指数值表示该股票可能被高估了,因此价格可能很快下降; 较低的值表示股票可能被低估了,因此价格可能很快就会上涨。
如果您想了解其他一些字段的定义,这个页面是查找财务和投资术语定义的好地方。
清洁和浏览数据
现在我们已经获取了数据并了解了一些字段的含义,下一步就是清理数据并进行一些探索性分析。
准备
确保已安装本章开头提到的要求部分下的软件包,并且已按照前面几节中的步骤将FINVIZ数据成功导入到 R 中。
怎么做...
要清理和浏览数据,请严格遵循以下说明:
- 导入的数字数据通常包含特殊字符,例如百分比符号,美元符号,逗号等。 这使 R 认为该字段是字符字段而不是数字字段。 例如,我们的
FINVIZ数据集包含许多带有百分号的值,必须将其删除。 为此,我们将创建一个clean_numeric函数,该函数将使用gsub命令删除所有不需要的字符。 我们将创建一次此函数,然后在本章中多次使用它:
clean_numeric <- function(s){
s <- gsub("%|\\$|,|\\)|\\(", "", s)
s <- as.numeric(s)
}
- 接下来,我们将此功能应用于
finviz数据框中的数字字段:
finviz <- cbind(finviz[,1:6],apply(finviz[,7:68], 2, clean_numeric))
- 如果再次查看数据,所有讨厌的百分比符号都将消失,并且所有字段均为数字。
注意
在该命令中以及本章的其余部分,将有很多实例通过列号引用列。 如果列数由于某些原因发生变化,则引用的数字将需要相应地进行调整。
- 现在,我们准备好开始真正地探索我们的数据了! 首先要做的是看一下价格的分配方式,以便直观地了解什么是高股票价格,什么是低股票价格以及大多数股票的价格下跌的地方:
hist(finviz$Price, breaks=100, main="Price Distribution", xlab="Price")
您将获得以下图形作为输出:

在这里,我们遇到了第一个问题。 价格非常高的离群股票导致 R 缩放直方图的x轴,从而使该图无用。 我们根本无法看到定价更高的股票的分布情况如何。 在生成直方图时,这是一个非常常见的问题。
- 让我们在 $ 150 的
x轴上加一个上限,看看会为我们带来什么:
hist(finviz$Price[finviz$Price<150], breaks=100, main=
"Price Distribution", xlab="Price")
您将获得以下图形作为输出:

这样好多了! 它表明我们数据集中的大多数股票的价格都在 50 美元以下。 因此,以绝对值计算,定价为 100 美元的股票将被认为是昂贵的。
- 但是当然事情并不是那么简单。 也许不同的部门和行业具有不同的价格水平。 因此,从理论上讲,如果该行业中的所有其他股票的定价都在 120 美元至 150 美元之间,那么 100 美元的股票可能会便宜。 让我们获取按部门划分的平均价格,并查看它们之间的比较。 请注意,我们不排除任何库存:
sector_avg_prices <- aggregate(Price~Sector,data=finviz,FUN="mean")
colnames(sector_avg_prices)[2] <- "Sector_Avg_Price"
ggplot(sector_avg_prices, aes(x=Sector, y=Sector_Avg_Price,
fill=Sector)) + geom_bar(stat="identity") + ggtitle("Sector Avg Prices") + theme(axis.text.x = element_text(angle = 90, hjust = 1))
您将获得以下图形作为输出:

这是有趣的。 金融部门的股票似乎具有比其他部门的股票更高的平均价格。 我敢打赌,这是由于一些离群值更早干扰了我们的分布。
- 让我们深入到此! 让我们找出哪些行业和公司负责使金融部门的平均价格远高于其他所有行业和公司。
首先,我们按行业创建平均价格摘要:
industry_avg_prices <- aggregate(Price~Sector+Industry,data=finviz,FUN="mean")
industry_avg_prices <- industry_avg_prices[order( industry_avg_prices$Sector,industry_avg_prices$Industry),]
colnames(industry_avg_prices)[3] <- "Industry_Avg_Price"
然后,我们将金融部门中的行业隔离开来:
industry_chart <- subset(industry_avg_prices,Sector=="Financial")
最后,我们创建一个图表,显示金融部门每个行业的平均价格:
ggplot(industry_chart, aes(x=Industry, y=Industry_Avg_Price,
fill=Industry)) + geom_bar(stat="identity") + theme(legend.position="none") + ggtitle("Industry Avg Prices") + theme(axis.text.x = element_text(angle = 90, hjust = 1))
您将获得以下图形作为输出:

从该图可以看出,财产&伤亡保险行业是推动平均价格上涨的罪魁祸首。
- 接下来,我们将进一步深入
Property & Casualty Insurance行业,以找出哪些公司是异常公司:
company_chart <- subset(finviz,Industry=="Property & Casualty Insurance")
ggplot(company_chart, aes(x=Company, y=Price, fill=Company)) +
geom_bar(stat="identity") + theme(legend.position="none") +
ggtitle("Company Avg Prices") +
theme(axis.text.x = element_text(angle = 90, hjust = 1))
您将获得以下图形作为输出:

很难看到,因为有这么多公司,但是如果您放大图表,就会发现离群的公司是 Berkshire Hathaway,该公司的股价目前超过每股 172,000 美元。
- 由于它们的股价是如此之高,因此让我们将其从数据集中删除,然后对这些行业进行平均,以使我们能够更准确地得出金融行业的平均价格:
finviz <- subset(finviz, Ticker!="BRK-A")
sector_avg_prices <- aggregate(Price~Sector,data=finviz,FUN="mean")
colnames(sector_avg_prices)[2] <- "Sector_Avg_Price"
ggplot(sector_avg_prices, aes(x=Sector, y=Sector_Avg_Price,
fill=Sector)) + geom_bar(stat="identity") + ggtitle("Sector Avg Prices") + theme(axis.text.x = element_text(angle = 90, hjust = 1))
您将获得以下图形作为输出:

现在,我们的平均值看起来要好得多,而且我们有相当不错的基础将股票价格与其行业和行业平均值进行比较。
工作原理...
在本节中,我们使用aggregate命令来汇总我们的数据。 这是我们使用的代码的提醒:
sector_avg_prices <- aggregate(Price~Sector,data=finviz,FUN="mean")
另一种执行此方法的方法是使用plyr程序包中的ddply命令:
sector_avg_prices <- ddply(finviz, "Sector", summarise,
Price=mean(Price, na.rm=TRUE))
无论您在何处看到本章中使用的aggregate命令,都可以尝试通过使用ddply命令来汇总数据来挑战自己。
另请参见
产生相对估值
您可以使用股市数据做的最有趣的事情之一就是评估模型。 最终目标是就股票是被高估还是被低估做出决定。 有两种主要方法可以做到这一点。 内部估值通常更耗时,因为它涉及挖掘公司的财务报表以得出估值决策。 另一种方法是相对估值,它可以快速提供股票的估值方式,但不考虑一系列综合因素。 基本思想是将股票的价格和估值比率与相似的股票进行比较,以得出结论。 在本节中,我们将使用更简单的相对估值方法对股票进行估值。
准备
此食谱需要下载和清除先前食谱中的数据。
怎么办
在本节中,我们基本上将做三件事。 首先,我们计算可用于相对估值工作中的字段的行业平均值。 然后,我们在行业层面上也做同样的事情。 最后,我们将股票的统计数据与平均值进行比较,以得出每只股票的指数值,该指数值指示该股票是否可能被低估了。 以下步骤将指导您:
- 为了计算 R 中多列的平均值,我们首先需要融合数据。 这将使
Sector之后的每一列都成为一行,然后显示其值,从本质上讲使数据变长而不是变宽。 请查看以下屏幕快照,了解此食谱中的不同步骤,以更好地了解数据如何改变形状。 它从宽到长,然后又回到宽,但以摘要形式:

我们将使用以下命令执行此操作:
sector_avg <- melt(finviz, id="Sector")
- 接下来,我们需要过滤,以便数据框仅包含我们要平均的字段:
sector_avg <- subset(sector_avg,variable %in% c("Price","P.E","PEG","P.S", "P.B"))
现在您的sector_avg数据框应如下所示:

现在,每个列标题(变量)都在其值旁边垂直列出。 这使我们以后可以进行一些分组以获得每个变量的平均值。
- 并非原始数据集中的所有股票都具有所有这些值。 如果这些值为空,则我们要删除记录。 我们还想确保我们所有的值都是数字:
sector_avg <- (na.omit(sector_avg))
sector_avg$value <- as.numeric(sector_avg$value)
- 下一步是转换数据以使其再次变宽。 这将为我们过滤的每个字段产生一列,并且现在将包含按扇区划分的平均值。 我们还将重命名这些列,以便我们知道它们是部门平均值:
sector_avg <- dcast(sector_avg, Sector~variable, mean)
colnames(sector_avg)[2:6] <- c("SAvgPE","SAvgPEG","SAvgPS","SAvgPB", "SAvgPrice")
您将获得以下图作为输出:

- 现在,我们将在行业级别上做完全相同的事情:
industry_avg <- melt(finviz, id=c("Sector","Industry"))
industry_avg <- subset(industry_avg,variable %in% c("Price","P.E","PEG","P.S","P.B"))
industry_avg <- (na.omit(industry_avg))
industry_avg$value <- as.numeric(industry_avg$value)
industry_avg <- dcast(industry_avg, Sector+Industry~variable, mean)
industry_avg <- (na.omit(industry_avg))
colnames(industry_avg)[3:7] <-
c("IAvgPE","IAvgPEG","IAvgPS","IAvgPB","IAvgPrice")
- 现在,我们将部门和行业平均值列添加到原始
finviz数据集中:
finviz <- merge(finviz, sector_avg, by.x="Sector", by.y="Sector")
finviz <- merge(finviz, industry_avg, by.x=c("Sector","Industry"), by.y=c("Sector","Industry"))
您可能已经注意到,当我们执行最后一行代码时,finviz数据框中的记录数减少了。 它从数据集中删除了所有没有行业平均值的股票。 这样做很好,因为总体目标是缩小股票清单的范围,而且我们将始终没有足够的信息来生成这些股票的估值。
- 现在是时候使用这些新字段了。 首先,我们将添加 10 个包含所有
0的占位符字段。 基于低于行业或行业平均水平,这些信息将用于跟踪股票是否被低估:
finviz$SPEUnder <- 0
finviz$SPEGUnder <- 0
finviz$SPSUnder <- 0
finviz$SPBUnder <- 0
finviz$SPriceUnder <- 0
finviz$IPEUnder <- 0
finviz$IPEGUnder <- 0
finviz$IPSUnder <- 0
finviz$IPBUnder <- 0
finviz$IPriceUnder <- 0
- 接下来,只要股票的各自价值小于平均值,我们就将 0 替换为 1,以表明根据该指标这些股票可能被低估了:
finviz$SPEUnder[finviz$P.E<finviz$SAvgPE] <- 1
finviz$SPEGUnder[finviz$PEG<finviz$SAvgPEG] <- 1
finviz$SPSUnder[finviz$P.S<finviz$SAvgPS] <- 1
finviz$SPBUnder[finviz$P.B<finviz$SAvgPB] <- 1
finviz$SPriceUnder[finviz$Price<finviz$SAvgPrice] <- 1
finviz$IPEUnder[finviz$P.E<finviz$IAvgPE] <- 1
finviz$IPEGUnder[finviz$PEG<finviz$IAvgPEG] <- 1
finviz$IPSUnder[finviz$P.S<finviz$IAvgPS] <- 1
finviz$IPBUnder[finviz$P.B<finviz$IAvgPB] <- 1
finviz$IPriceUnder[finviz$Price<finviz$IAvgPrice] <- 1
- 最后,我们将对这 10 列进行求和,以创建一个新的索引值列,以 1 到 10 的比例告诉您,股票是如何根据所考虑的不同维度而被低估的:
finviz$RelValIndex <- apply(finviz[79:88],1,sum)
工作原理...
相对估值涉及将股票的统计数据与类似股票的统计数据进行比较,以确定该股票是被高估还是被低估。 在一个过于简化的示例中,相对于其行业的行业平均 P / E 比(所有其他条件都相同),具有较低市盈率的股票可以被认为价值低估,并且如果 公司财务状况良好。 一旦有了这些,我们就可以筛选出最有前途的股票,例如RelValIndex为 8 或更大的股票。
potentially_undervalued <- subset(finviz,RelValIndex>=8)
我们刚刚创建的potentially_undervalued数据框应如下所示:

我们承认这是一种过于简单的方法。 但是,它提供了扩展到更复杂的计算的框架。 例如,一旦对此过程感到满意,您可以:
- 添加自定义条件以分配
1,以表明股票被低估了 - 权衡不同的值
- 添加或删除条件
- 创建比 1 和 0 更精确的索引值,依此类推
天空是这里的极限,但是过程是一样的。
筛选库存并分析历史价格
当我们寻找股票进行投资时,我们需要一种方法来缩小列表的范围。 换句话说,我们需要消除那些我们认为不是好的投资的股票。 优质投资的定义因人而异,但是在本节中,我们将使用一些基本标准将主股票清单减少到我们认为可能具有良好前景的少数股票。 一旦适应了这一过程,我们鼓励您根据自己对有价值股票定义的观点进行修改。一旦有了前景,我们将分析其历史价格,并从中得出结论。
准备
我们将从上一节结尾处的finviz数据集开始,并与行业和行业平均值列,二进制低估列以及将二进制列中的值求和的索引值相加。
除了本章到目前为止使用的软件包之外,本节还将需要 zoo 软件包。 这将有助于我们计算将要拉升的历史股价的移动平均线。
怎么做...
您将要执行的步骤将使您可以筛选库存:
- 首先,选择一些股票筛选标准,即在
finviz数据集中选择我们认为有可能成为良好投资潜力的股票的方法。 以下是一些示例标准,以开始:- 只有美国公司
- 每股价格在 20 到 100 美元之间
- 数量大于 10,000
- 当前和未来预期的正每股收益
- 总负债权益比小于 1
- Beta 小于 1.5
- 机构所有权少于 30%
- 相对估值指数值大于 8
- 如前所述,这些只是示例。 随意删除条件,添加条件或根据您认为将为您提供最佳输出的内容进行更改。 目标是将清单缩小到少于 10 只股票。
- 接下来,我们应用我们的标准将
finviz数据帧子集化为名为target_stocks的新数据帧:
target_stocks <- subset(finviz, Price>20 & Price<100 & Volume>10000 &
Country=="USA" &
EPS..ttm.>0 &
EPS.growth.next.year>0 &
EPS.growth.next.5.years>0 &
Total.Debt.Equity<1 & Beta<1.5 &
Institutional.Ownership<30 &
RelValIndex>8)
在撰写本文时,这将产生六只股票的目标清单,如以下屏幕快照所示。 如果您从网上提取更新的数据,则可能会得到不同的数量或不同的库存:

- 现在,让我们出去获取目标股票清单的历史价格,以便我们了解其价格随时间的变化。 我们将使用
for循环来遍历符号列表并为每个价格拉价,但是我们将通过几个步骤来分解循环,并解释每个块在做什么:
counter <- 0
for (symbol in target_stocks$Ticker){
前面的命令初始化一个计数器,以跟踪我们在目标股票列表中的位置。 之后,我们通过告诉目标列表中的每个符号执行以下操作来开始 for 循环:
url <- paste0("http://ichart.finance.yahoo.com/table.csv?s=",symbol,
"&a=08&b=7&c=1984&d=01&e=23&f=2014&g=d&ignore=.csv")
**stock <- read.csv(url)**
**stock <- na.omit(stock)**
**colnames(stock)[7] <- "AdjClose"**
**stock[,1] <- as.Date(stock[,1])**
**stock <- cbind(Symbol=symbol,stock)**
此代码将 URL 分配给已嵌入当前股票代码的 URL 变量。 然后,我们读取位于该 URL 的数据,并将其分配给名为stock的数据帧。 然后,我们进行一些清理和格式化,方法是从数据框中删除所有空值,重命名最后一列,确保Date列的格式设置为 R 可以识别的日期,然后将股票代码添加到 数据框。
for循环的接下来的几行将计算一些移动平均线,以便我们可以将它们与每日股价进行比较。 对于此步骤,请确保已安装并加载了本节开头提到的zoo程序包。
第一部分将计算 50 天移动平均线和 200 天移动平均线:
**maxrow <- nrow(stock)-49**
**ma50 <- cbind(stock[1:maxrow,1:2],rollmean(stock$AdjClose, 50,align="right"))**
**maxrow <- nrow(stock)-199**
**ma200 <- cbind(stock[1:maxrow,1:2],rollmean(stock$AdjClose,200,align="right"))**
第二部分将移动平均数据框与包含历史股票价格的数据框组合在一起,以便所有内容都属于同一数据集:
stock <- merge(stock,ma50,by.x=c("Symbol","Date"),by.y=c("Symbol",
"Date"), all.x=TRUE)
colnames(stock)[9] <- "MovAvg50"
stock <- merge(stock,ma200,by.x=c("Symbol","Date"),by.y=c("Symbol", "Date"),all.x=TRUE)
colnames(stock)[10] <- "MovAvg200"
- 接下来,我们将为我们的
for循环迭代的每种股票绘制历史图表,然后保存该图表:
price_chart <- melt(stock[,c(1,2,8,9,10)],id=c("Symbol","Date"))
qplot(Date, value, data=price_chart, geom="line", color=variable, main=paste(symbol,"Daily Stock Prices"),ylab="Price")
ggsave(filename=paste0("stock_price_",counter,".png"))
生成并保存的图表应类似于以下两个图表:

循环的下一部分总结了当前股票的开盘价,最高价,最低价和收盘价:
price_summary <- ddply(stock, "Symbol", summarise, open=Open[nrow(stock)], high=max(High),low=min(Low),close=AdjClose[1])
然后,它将汇总的开盘价,最高价,最低价和收盘价汇总到一个称为股票的数据框中,以便以后可以比较不同的股票。 而且,它在称为价格摘要的数据框中单独累积了股票的所有每日历史价格,以便也可以将它们进行比较:
if(counter==0){
stocks <- rbind(stock)
price_summaries <- rbind(price_summary)
}else{
stocks <- rbind(stocks, stock)
price_summaries <- rbind(price_summaries, price_summary)
}
在循环结束时,我们将计数器加 1,然后用花括号将 for 循环关闭:
counter <- counter+1
}
注意
我们将循环分成几部分,以解释循环的每个部分的功能。 如果要查看整个 for 循环的外观,请查看本章随附的代码文件。
完整的代码块在此处以图像形式给出:

遍历所有股票代码之后,我们将得到一个名为stocks的数据框,其中包含目标列表中所有符号的历史价格,以及一个名为price_summaries的数据框,其中包含所有股票的摘要 。 让我们绘制它们的图,看看它们的外观。
- 首先,我们将绘制所有股票的历史价格:
qplot(Date, AdjClose, data=stocks, geom="line", color=Symbol, main="Daily Stock Prices")
ggsave(filename=("stock_price_combined.png"))
前面的命令将产生以下图形:

- 然后,让我们可视化价格摘要:
summary <- melt(price_summaries,id="Symbol")
ggplot(summary, aes(x=variable, y=value, fill=Symbol)) + geom_bar(stat="identity") + facet_wrap(~Symbol)
ggsave(filename=("stock_price_summaries.png"))
生成的图形应类似于以下内容:

工作原理...
每日股价图表非常尖刻或易变,这有时使它们难以阅读。 移动平均线可以平滑股票的价格波动,因此您可以更好地了解股票是随时间上涨还是下跌。
移动平均线还用于对股票投资进行计时。 换句话说,它们用作确定是立即投资股票还是等待股票的指南。 关于什么是最佳时间的信号,人们意见不一,但其中一个例子是,该股的 50 天移动平均线低于其 200 天移动平均线,但仍在上升。 有关移动平均线的更多信息,请参见这个页面。
我们在本节中生成的组合历史价格图表向我们显示了目标股票的价格串联变化的程度。 如果您打算投资多只股票,那么最好投资价格之间的相关性不高的股票。 您还可以可视化一种股票与另一种股票相比的波动性。 在我们的图形中,您可以看到符号WPZ和NRCIB的波动性很强,而其他符号的波动性较小。
查看价格比较的另一种方法是检查我们创建的价格摘要条形图。 该图表显示了所分析期间的开盘价,最高价,最低价和收盘价。 开盘价是股票交易的第一个价格,收盘价是迄今为止股票交易的最后一个价格,高价是该期间股票的最高价格,而低价是 股票在此期间的最低价格。 可以在此图上以不同的方式查看前面提到的波动率,因为您可以清楚地看到我们两个波动最大的股票的高点和低点之间的差异。 该图表还可以让您查看股票的收盘价相对于其历史最高价和历史最低价的位置,这可能有助于您了解其当前估值的公平性。
五、可视化探索就业数据
在本章中,我们将介绍:
- 准备分析
- 将就业数据导入 R
- 探索就业数据
- 获取和合并其他数据
- 添加地理信息
- 提取州和县一级的工资和就业信息
- 可视化薪酬的地理分布
- 按行业探索工作位置
- 地理空间时间序列的动画地图
- 对一些常见任务进行性能基准评测
简介
该项目将向您介绍由美国政府的劳工统计局(BLS)提供的美国就业数据。 BLS 是联邦机构,负责衡量美国经济中的劳动力市场活动以及工作条件和价格。 其任务是收集,分析和传播必要的经济信息,以支持公共和私人决策。 在本项目中,我们将使用从季度就业和工资普查(QCEW)中得出的,按地理和行业分层的 2012 年就业和薪酬年度汇总数据。
可以从这个页面将这些数据下载为压缩的逗号分隔值(CSV)文件,其中包含单个文件2012.annual.singlefile.csv。 该文件有 15 列和约 350 万行。
QCEW 是通过公司税收收集系统每季度收集的数据,这些数据与雇主报告的就业和工资有关。 这项人口普查覆盖了约 98%的美国文职工作,不包括所有人,无公司非法经营的个体经营者,无薪家庭成员以及一些农场工人和家政工人。 可以按县,大城市(MSA),州和国家/地区和行业级别作为汇总数据使用该数据。 该政府计划自 1930 年代以来已经以某种形式实施,而当前形式则自 2003 年以来一直存在。数据基于法律要求的向联邦和地方政府提交的公司报告,因此应相对不受限制。 报告偏见。 该数据汇总了按地理位置和行业划分的该国的工资和就业水平。
我们将在本章中讨论的基本问题是 2012 年美国薪资和就业的地理分布以及撰写本文时的最新可用数据。 我们将研究州和县两级,并深入探讨一些行业。 我们还将研究 2003-2012 年期间薪酬地理分布的时间变化,以及这揭示了美国就业形势的变化。
本章的目的是通过分步示例来指导您完成数据科学流程,在本例中,该示例是对美国政府 BLS 上免费提供的政府就业数据的探索。 我们将通过将数据摄取到 R 中,转换和处理数据,创建数据子集以及生成可视化效果来提供有关数据模式的一些见识的工作。 我们希望该示例可以作为另一个示例,您可以出于类似目的将其转移到其他项目。
注意
请注意,本章中的内容比前几章更高级。
我们假设您已经阅读了第 1 章,“准备数据科学环境”,并且已经在计算机上提供了 RStudio,可用于完成本章中的配方。
准备分析
本食谱将为您完成该项目所需的工具奠定基础。 如果您的计算机上未安装 R,请参阅第 1 章“准备数据科学环境”中的说明。
准备
您需要一台装有 R 的计算机并可以连接互联网。
怎么做...
通过从 BLS 网站下载数据集并确保我们具有所需的 R 包,以下步骤将为您做好本章其余部分的准备:
- 从这个页面下载 76.8 MB 压缩数据,并将其保存到您会记得的位置。
- 通过在资源管理器或 finder 中右键单击文件来解压缩文件,然后使用适当的菜单项。
如果您熟悉 Linux / Mac OS X 中终端中的命令行,则可以使用unzip 2012_annual_singlefile.zip轻松解压缩下载的文件。
- 在您的计算机上启动 RStudio IDE(对于纯粹主义者,仅启动普通 R)。
- 加载此项目所需的 R 包:
library(data.table)
library(plyr)
library(dplyr)
library(stringr)
library(ggplot2)
library(maps)
library(bit64)
library(RColorBrewer)
library(choroplethr)
如果您的计算机上没有安装这些软件包之一,则可以使用以下命令轻松安装它,将data.table替换为要安装的软件包名称:
install.packages('data.table',repos='http://cran.r-project.org')
R 软件包存储库(称为CRAN)在全球范围内具有多个镜像。 在这种用法中,镜像是在不同区域的服务器上运行的软件存储库的副本,从而为该位置附近的个人提供了更快的访问权限。 您可以并且应该选择在地理位置上与您的位置最近的镜子,以加快软件包的下载速度。 在前面的代码片段中,将http://cran.r-project.org更改为首选CRAN镜像的 URL。
- 最后,将工作目录设置为保存文件的路径。 这将告诉 R 您希望它在哪里寻找文件:
setwd('path')
工作原理...
我们将主要使用三个不同的 R 包,这些包在导入,操作和转换大型数据集时非常有用。
软件包data.table改进了 R 中的data.frame对象,从而使操作更快,并允许在数据集中指定索引变量,这是数据库专家熟悉的概念。 它还允许快速聚合大数据,包括快速排序的联接。 我们将主要使用data.table包的fread函数,该函数允许(非常)快速将大型结构化数据集导入 R。我们还将研究此包中的函数相对于我们在函数集中使用的其他函数的相对性能 本章后面的基准测试方法。
stringr程序包提供了用于文本和字符串操作的工具。 该程序包简化并在语法上统一了 R 中可用的字符串操作功能,从而使涉及字符串搜索,操作和提取的任务更加容易。 我们将在这里需要这些功能。
dplyr软件包是 Hadley Wickham 博士流行的软件包plyr的下一个迭代。 它针对矩形数据,并允许非常快速的聚合,转换,汇总,列选择和联接。 它还提供了语法糖,以允许将命令串在一起,将一个命令的结果传递到下一个命令中。 这将是我们在该项目中的主力军。
ggplot2软件包将成为我们可视化的主力军。 它在 R 中实现了图形语法范例,并提供了一种非常灵活的方式来创建可视化。
maps软件包提供了我们将在可视化中使用的有关美国的地理信息。
另请参见
dplyr参考资料,包含指向小插图和其他参考资料的链接ggplot2参考资料sqldf参考资料
将就业数据导入 R
该项目的第一步是将就业数据导入 R,以便我们可以开始评估数据并进行一些初步分析。
准备
完成上一个食谱后,您应该准备继续。
怎么做...
以下步骤将指导您通过两种不同的方式导入 CSV 文件:
- 我们可以使用以下命令将数据文件直接加载到 R 中(甚至是压缩版本):
ann2012 <- read.csv(unz('2012_annual_singlefile.zip',
'2012.annual.singlefile.csv'), stringsAsFactors=F)
但是,您会发现此文件花费很长时间。 有更好的方法。
- 我们选择将数据直接导入 R 中,因为我们将在以后进行进一步的操作和合并。 我们将使用
data.table包中的fread函数执行此操作:
library(data.table)
ann2012 <- fread('data/2012.annual.singlefile.csv')
而已。 真的! 它也比其他方法快许多倍。 默认情况下,它不会将字符变量转换为因数,因此,如果以后需要因数,则必须将其转换,在我们看来,这在任何情况下都是理想的功能。
工作原理...
我们从第 2 章,“通过汽车数据”驾驶视觉分析中熟悉了read.csv。 它基本上逐行读取数据,并以逗号分隔列。 由于我们用于该项目的数据为 350 万行,因此它仍然足够小以适合大多数现代个人计算机的内存,但是使用read.csv()导入将需要相当长的时间。
data.table程序包中的fread函数使用基础 C 级函数从文件中找出文件中的长度,字段数,数据类型和定界符,然后使用其参数读取文件 学到了。 结果,它比read.csv快得多。 R 文档中有fread的详细说明。
还有更多...
当前 R 中的限制之一是导入 R 中的数据需要适合主机计算机的内存。 对于大型数据集,使用基于 SQL 的数据库进行数据存储和处理会利用基于 SQL 的数据库的速度,从而规避了 R 的内存限制。 通常,在企业环境中,数据存储在 Oracle,SAP,MySQL 或 PostgreSQL 数据库中。
如果您来自 SQL 背景,sqldf软件包将非常有用,许多进入数据科学领域的人都具有这样的背景。 该软件包允许您在 R 中使用 SQL 命令和查询,并像对待 SQL 数据库中的表一样对待 R 中的data.frame对象。 它还允许您与您有权访问的大多数基于 SQL 的数据库进行连接,包括 SQLite,MySQL 和 PostgreSQL 等。
作为演示,我们可以将数据导入 SQLite 数据库,然后在其中读取和操作它:
sqldf('attach blsdb as new')
read.csv.sql(file='2012.annual.singlefile.csv', sql='create table main.ann2012 as select * from file', dbname='blsdb')
ann2012 <- sqldf("select * from main.ann2012", dbname='blsdb')
注意
您必须在计算机上安装并可用 SQLite 才能使上述代码起作用。 我们将把安装工作留给您。
这也将花费一些时间,但是您将在 SQLite 中获得数据。 前面的命令实际上并不将数据摄取到 R 中,而只是将其导入到 SQLite 中。
您可以使用以下命令将数据导入 R:
ann2012 <- sqldf("select * from main.ann2012", dbname='blsdb')
您还可以选择使用sqldf在 SQLite 中使用 SQL 进行操作。 对于某些用户来说,使用 SQL 操作数据然后将导入的数据仅导入 R 会更容易。
由于本书的第一部分专注于 R,因此我们不会在这里介绍的内容之外更深入地研究sqldf的使用。 但是,如果您对 SQL 更加熟悉,欢迎您使用 SQL 命令在各种配方中复制 R 中介绍的步骤。
另请参见
探索就业数据
现在将数据导入到 R 中,并且我们已经学习了一些将较大的数据集导入到 R 中的策略,我们将对数据进行一些初步分析。 目的是查看数据的外观,识别特质,并确保其余的分析计划可以继续进行。
准备
如果您完成了最后一个食谱,则应该准备好了。
怎么做...
以下步骤将引导您完成本食谱,以探索数据:
- 首先,让我们看看这些数据有多大:
> dim(ann2012)
[1] 3556289 15
很好,只有15列。
- 让我们看一下前几行,以便我们可以看到数据是什么样的:
head(ann2012)
您可以参考以下屏幕截图:

什么是变量own_code **,** industry_code等,它们是什么意思? 我们可能需要更多数据才能理解此数据。
- 在这些数据中还有一个怪异的特质。
total_annual_wages,taxable_annual_wages和annual_contributions的某些值看起来很小。 实际数据显示,这些数字似乎不正确。 但是,fread实际上可以指示正在发生的事情:
ann2012 <- fread('data/2012.annual.singlefile.csv', sep=',',
colClasses=c('character', 'integer', 'integer', 'integer',
'integer', 'integer', 'character',rep('integer',8)))
您可以参考以下屏幕截图:

- 这指出了以下事实:可能需要
bit64程序包才能正确显示这些大数字。 安装并加载此程序包,然后重新导入数据可解决此问题,如以下命令行所示:
install.packages('bit64')
library('bit64')
ann2012 <- fread('data/2012.annual.singlefile.csv', sep=',',
colClasses=c('character', 'integer', 'integer', 'integer',
'integer', 'integer', 'character',rep('integer',8)))
您可以参考以下屏幕截图:

工作原理...
head命令显示数据帧的前几行(默认为前六行)。 我们注意到,有些标题是不言自明的,但有些则暗示了我们当前无法访问的代码。 我们将必须获取其他数据才能进行有意义的分析。 我们可以查看数据而无需将其导入 R。UNIX 命令less和 Windows PowerShell 命令type可以向我们展示与head相同的内容。
另请参见
- 请参阅这个页面上可用数据集的文档。
获取和合并其他数据
在上一个配方中,我们发现需要其他数据来了解 CSV 文件中的数据实际代表什么,并且该配方将直接解决此需求。
准备
我们可以在 BLS 网站的标题 Associated Codes and Titles 下找到其他数据。 那里有五个文件,我们将其下载到我们的计算机中。 它们如下:
我们将它们下载到我们的计算机上,记住它们的存储位置。 我们需要准备将它们导入 R 并将它们与我们的原始数据合并。
怎么做...
以下步骤将引导您将这些文件加载到 R 中,并将它们连接到更大的数据框中:
- 现在,我们将使用以下命令行将这些数据文件导入 R:
for(u in c('agglevel','area','industry','ownership','size')){
assign(u,read.csv(paste('data/',u,'_titles.csv',sep=''),stringsAsFactors=
F))
}
这是一个代码示例,使我们可以更轻松地执行重复任务。
- 每个数据集都有一个与我们的原始数据
ann2012相同的变量(列标题),如以下屏幕快照所示:

- 因此,将数据集组合在一起应该相当容易。 现在,我们将其中四个数据集与
ann2012结合起来,并保存area,即area_titles.csv的数据用于下一个配方,因为我们想对它进行一些操作:
codes <- c('agglevel','industry','ownership','size')
ann2012full <- ann2012
for(i in 1:length(codes)){
eval(parse(text=paste('ann2012full <- left_join(ann2012full, ',codes[i],')', sep='')))
}
最终结果显示在以下屏幕截图中:

工作原理...
在如何操作... 部分的步骤 1 中,我们想将每个数据集分配给它自己的对象。 我们可以为每次导入编写一行代码,但是此代码使我们可以更快地执行此操作,并且将来将更易于维护。 assign命令具有两个基本输入:变量名称和要分配给变量名称的值。 这里的for循环不会遍历数字,而是遍历对象。 在第一次迭代中,u取值agglevel。 assign函数的名称为agglevel,并为其分配read.csv命令的结果。 在paste命令中,我们再次使用u的值,因为我们要导入的所有文件都具有相同的命名约定。 正是这种通用的命名约定使我们能够使用这种类型的编码。 因此,第一次迭代给出了assign('agglevel', read.csv('data/agglevel_title.csv')) , 等。
在步骤 2 中,我们将数据集连接在一起。 我们首先将原始数据复制到新名称ann2012full,以便我们可以建立新数据集而不会在出现问题的情况下不破坏原始数据。 然后,我们使用类似于宏的构造将所有新数据集连接到原始数据集,并在for循环中的数字和向量代码的索引上进行迭代。
让我们从这个复杂的命令(从总体上理解复杂代码的一种明智的策略)着手进行研究。 在paste命令中,我们创建要评估的命令。 我们想要做一个left_join(来自dplyr包),在第一次迭代中将ann2012full与agglevel结合在一起。 left_join确保保留ann2012full的所有行,并适当地复制agglevel的行以匹配ann2012full中的行数。 由于两个数据集中只有一个公共变量,因此left_join会自动选择使用此变量进行联接。
注意
通常,left_join将使用在两个数据集中发现的所有变量名称将两个数据集连接起来。 如果您不想这样做,则可以通过指定例如left_join(ann2012full, agglevel, by="agglvl_code ").来指定要用于联接的变量。
然后,eval语句评估我们使用paste命令构造的命令。 我们遍历代码中的名称,以便使四个数据集的每个都与ann2012full结合在一起。
快速检查将显示联接之后ann2012full中的行数与ann2012中的行数相同。
添加地理信息
本章的主要目的是研究全美工资的地理分布。 对此进行映射需要我们首先拥有一张地图。 幸运的是,maps软件包中提供了州和县两级的美国地图,并且可以提取制作这些地图所需的数据。 我们将本食谱中的就业数据与地图数据对齐,以便在地图上的正确位置显示正确的数据。
准备
我们已经将area数据集导入到 R 中,因此可以开始使用了。
怎么做...
以下步骤将指导您完成在 R 中创建第一个地图的过程:
- 我们首先来看
area中的数据:
head (area)
输出显示在以下屏幕截图中:

我们看到这里有一个叫做area_fips的东西。 人口普查局使用联邦信息处理标准(FIPS)代码来指定美国的县和其他地理区域。
- 根据约定,我们希望将所有名称都大写。 我们将编写一个小函数来做到这一点:
simpleCap <-function(x){
if(!is.na(x)){
s <- strsplit(x,' ')[[1]]
paste(toupper(substring(s,1,1)), substring(s,2), sep='', collapse=' ')
} else {NA}
}
maps程序包包含两个我们将要使用的数据集; 它们是county.fips和state.fips。 我们将首先进行一些转换。 如果我们查看county.fips,我们会注意到 FIPS 代码在某些代码的左侧缺少前导0。 我们的就业数据中的所有代码均由五位数字组成:
> data(county.fips)
> head(county.fips)
fips polyname
1 1001 alabama,autauga
2 1003 alabama,baldwin
3 1005 alabama,barbour
4 1007 alabama,bibb
5 1009 alabama,blount
6 1011 alabama,bullock
stringr软件包将在这里为我们提供帮助:
county.fips$fips <- str_pad(county.fips$fips, width=5, pad="0")
- 我们要从
county.fips的polyname列中分离县名。 我们将在一分钟内从state.fips获取状态名称:
county.fips$polyname <- as.character(county.fips$polyname)
county.fips$county <- sapply(
gsub('[a-z\ ]+,([a-z\ ]+)','\\1',county.fips$polyname), simpleCap)
county.fips <- unique(county.fips)
state.fips数据涉及很多细节:
> data(state.fips)
输出显示在以下屏幕截图中:

- 如有必要,我们将再次在
fips列上填充0,以便它们有两位数字,并从polyname中使用州名称大写以创建新的state列。 该代码类似于我们用于county.fips数据的代码:
state.fips$fips <- str_pad(state.fips$fips, width=2, pad="0", side='left')
state.fips$state <- as.character(state.fips$polyname)
state.fips$state <- gsub("([a-z\ ]+):[a-z\ \\']+",'\\1',state.fips$state)
state.fips$state <- sapply(state.fips$state, simpleCap)
- 我们确保我们有唯一的行。 我们在这里需要小心,因为我们只需要在
fips和state值中具有唯一性,而不需要在其他代码中具有唯一性:
mystatefips <-unique(state.fips[,c('fips','abb','state')])
unique函数应用于data.frame对象时,将返回该对象的唯一行。 您可能习惯于在单个向量上使用unique查找向量中的唯一元素。- 我们得到低 48 个州名称的列表。 我们将过滤数据以仅查看以下状态:
lower48 <- setdiff(unique(state.fips$state),c('Hawaii','Alaska'))
注意
setdiff设置操作将查找第一组中所有不在第二组中的元素。
- 我们将所有这些信息放到一个单独的数据集
myarea中:
myarea <- merge(area, county.fips, by.x='area_fips', by.y='fips', all.x=T)
myarea$state_fips <- substr(myarea$area_fips, 1,2)
myarea <- merge(myarea, mystatefips,by.x='state_fips',by.y='fips', all.x=T)
- 最后,我们将地理信息与数据集结合在一起,并对其进行过滤,以仅将数据保留在较低的 48 个州中:
ann2012full <- left_join(ann2012full, myarea)
ann2012full <- filter(ann2012full, state %in% lower48)
- 现在,我们将最终数据集存储在磁盘上的 R 数据(
rda)文件中。 这为 R 对象提供了一种有效的存储机制:
save(ann2012full, file='data/ann2014full.rda',compress=T)
工作原理...
该食谱的 12 个步骤涵盖了相当多的内容,因此,从第 2 步开始,让我们深入研究一些细节。simpleCap函数是 R 中一个函数的示例。我们使用函数封装重复的任务,从而减少了 代码重复,并确保错误具有单一起源。 如果仅重复代码,手动更改输入值,我们就很容易在转录中犯错误,破坏隐藏的假设或意外覆盖重要变量。 此外,如果要修改代码,则必须在每个重复的位置手动进行。 这很繁琐且容易出错,因此我们创建函数,这是我们强烈鼓励您遵循的最佳实践。
simpleCap功能使用三个功能:strsplit, toupper和substring。 每当strsplit函数发现要分割的字符串片段(在我们的情况下为' '或空格)时,它都会分割字符串(或字符串向量)。 substring函数从指定的字符位置之间的字符串中提取子字符串。 仅指定一个字符位置意味着从该位置提取到字符串的末尾。 toupper函数将字符串的大小写从小写更改为大写。 反向操作由tolower完成。
从步骤 3 开始,程序包通常将示例数据与它们捆绑在一起。 county.fips和state.fips是已捆绑到maps程序包中的数据集的示例。
步骤 4 中使用的stringr程序包是 Wickham 博士提供的另一种程序包,它提供字符串操作功能。 在这里,我们使用str_pad,它用字符(此处为0)填充字符串,以为字符串提供特定的宽度。
在第 5 步中,我们使用 R 中的内置正则表达式(regex)功能。在这里,我们不会过多地讨论正则表达式。 gsub函数查找第一个模式,并将第二个模式替换为指定为第三个字符串。 在这里,我们要寻找的模式包括一个或多个字母或空格([a-z\ ]+) ,,然后是逗号,然后是一个或多个字母或空格。 我们要保留第二组字母和空格,因此我们在其周围加上括号。 1 模式表示将整个模式替换为我们用括号括起来的第一个模式。 此替换发生在polyname字段的每个元素上。
由于我们希望对polyname中的每个元素都使用大写字母,因此可以使用for循环,但是可以选择使用效率更高的sapply。 polyname中的每个元素都通过simpleCap函数传递,因此在步骤 7 中被大写。
在步骤 10 中,我们将area,county.fips和mystatefips数据集连接在一起。 我们使用merge函数而不是left_join函数,因为我们要加入的变量对于不同的data.frame对象具有不同的名称。 R 标准库中的merge函数提供了这种灵活性。 为了确保左连接,我们指定all.x=TRUE。
在步骤 11 中,我们将myarea数据框连接到我们的ann2014full数据集。 然后,我们使用 filter 函数对数据进行子集处理,将其限制为来自较低 48 个州的数据。 filter功能来自dplyr程序包。 在下一个食谱中,我们将讨论dplyr中的功能。
另请参见
提取州和县一级的工资和就业信息
到目前为止,我们一直在努力使数据成形以进行分析。 现在,我们开始研究每个州和每个县的平均年薪的地理分布。
准备
如果到目前为止,您已经完全按照本章中的说明进行操作,则将以表格的形式获取数据,从中可以提取不同级别的信息。 我们很高兴去!
怎么做...
我们将首先在状态级别从ann2014full提取数据。 我们需要执行以下步骤:
- 我们来看
aggregate状态级别的数据。 窥视agglevel会告诉我们,所需数据级别的代码是50。 此外,我们只希望查看平均年薪(avg_annual_pay)和平均年就业水平(annual_avg_emplvl),而不要查看其他变量:
d.state <- filter(ann2014full, agglvl_code==50)
d.state <- select(d.state, state, avg_annual_pay, annual_avg_emplvl)
- 我们创建了两个新变量
wage和empquantile,,它们离散化了pay和employment变量:
d.state$wage <- cut(d.state$avg_annual_pay,
quantile(d.state$avg_annual_pay, c(seq(0,.8, by=.2), .9, .95, .99, 1)))
d.state$empquantile <- cut(d.state$annual_avg_emplvl,
quantile(d.state$annual_avg_emplvl, c(seq(0,.8,by=.2),.9,.95,.99,1)))
- 我们还希望这些离散变量的级别有意义。 因此,我们运行以下命令:
x <- quantile(d.state$avg_annual_pay, c(seq(0,.8,by=.2),.9, .95, .99, 1))
xx <- paste(round(x/1000),'K',sep='')
Labs <- paste(xx[-length(xx)],xx[-1],sep='-')
levels(d.state$wage) <- Labs
x <- quantile(d.state$annual_avg_emplvl, c(seq(0,.8,by=.2),.9, .95, .99, 1))
xx <- ifelse(x>1000, paste(round(x/1000),'K',sep=''),round(x))
Labs <- paste(xx[-length(xx)],xx[-1],sep='-')
levels(d.state$empquantile) <- Labs
获得 0、0.2、0.4、0.6、0.8、0.9、0.95、0.99 和 1 个分位数的年平均薪水,然后按千分之一获得。 然后重复执行该任务,以实现年度平均就业率。
- 我们在县一级重复此过程。 我们将发现适当的聚合级别代码为 70(
agglvl_code==70)。 其他一切都一样。 这次让我们变得更加聪明。 首先,我们将以相同的方式离散化变量,然后更改标签以使其匹配。 函数可能是个好主意! 以下命令行对此进行了描述:
Discretize <- function(x, breaks=NULL){
if(is.null(breaks)){
breaks <- quantile(x, c(seq(0,.8,by=.2),.9, .95, .99, 1))
if (sum(breaks==0)>1) {
temp <- which(breaks==0, arr.ind=TRUE)
breaks <- breaks[max(temp):length(breaks)]
}
}
x.discrete <- cut(x, breaks, include.lowest=TRUE)
breaks.eng <- ifelse(breaks > 1000,
paste0(round(breaks/1000),'K'),
round(breaks))
Labs <- paste(breaks.eng[-length(breaks.eng)], breaks.eng[-
1],
sep='-')
levels(x.discrete) <- Labs
return(x.discrete)
}
- 我们之前提到过
dplyr的句法糖。 现在,我们看到了它的作用。dplyr包允许您使用**%.%**运算符将不同的操作组合在一起,将一个操作的结果作为下一个输入的管道。 我们将在下一个食谱中描述dplyr的主要操作。 通过使用一些函数封装,以下代码实现了我们花费大量代码才能在1-3步骤中实现的所有功能:
d.cty <- filter(ann2012full, agglvl_code==70)%.%
select(state,county,abb, avg_annual_pay, annual_avg_emplvl)%.%
mutate(wage=Discretize(avg_annual_pay),
empquantile=Discretize(annual_avg_emplvl))
现在,我们有了可视化数据中地理图案所需的基本数据集。
工作原理...
前面的五个步骤涵盖了很多 R 代码,因此让我们开始进行分解。 filter和select这两个功能来自dplyr。 dplyr程序包提供了五个基本功能,如下所示:
filter:这将根据指定的条件创建数据的子集select:这从数据集中选择列或变量mutate:这将在数据集中创建新列或变量,这些新列或变量是从数据集中的其他变量派生的group_by:这将数据按一个变量或一组变量拆分,随后的功能将对由唯一变量值或组合定义的每个组件进行操作arrange:这将根据数据集中的变量重新排列(或排序)数据
这些功能中的每一个都可以对dplyr的一部分data.frame,data.table或tbl对象进行操作。
cut函数根据指定的断点或阈值离散化连续变量。 在这里,我们根据变量的分位数指定阈值。 我们通过分数序列指定所需的分位数:
c(seq(0, .8, by=.2), .9, .95, .99, 1)
使用seq函数可以创建规则的数字序列,其起始值,终止值以及两个连续值之间的差。
在第 3 步中,我们采用指定的阈值并对它们进行格式化。 如通常所见,大于 1,000 的数字将在数千个位置被截断并附加K。 在未指定小数位数的情况下使用round表示没有小数位数。
此外,在第 3 步中,我们希望标签代表一个范围。 因此,我们需要通过在格式阈值的连续值之间放置一个破折号(-)来创建标签。 一种方法是创建阈值向量的两个副本,一个副本不包含最后一个元素,另一个不包含第一个元素。 然后,将它们与-粘贴在一起。 请注意,此技巧允许将连续的阈值对齐并粘贴在一起。 如果您不确定,请打印xx[-length(xx)]和xx[-1]并亲自查看。
Discretize函数封装了我们在离散结果和格式化标签方面所做的工作。
此代码段使用dplyr的语法将功能串在一起。 我们首先对原始数据进行子集处理,仅保留具有agglvl_code=50的数据(请注意代码中的==)。 然后,我们将生成的缩减数据通过管道传递到第二个函数select中,该函数仅保留我们感兴趣的四个变量。这进一步缩减了数据,然后将其输入到mutate函数中,然后该函数创建了两个新的 数据对象中的变量。 然后,该最终对象与变量名d.cty一起存储。
另请参见
可视化薪酬的地理分布
我们创建了数据集,其中包含我们需要可视化各县和州的平均工资和就业情况的数据。 在本食谱中,我们将通过使用映射到特定值或值范围的颜色来阴影化地图的适当区域,从而可视化薪酬的地理分布。 这通常称为叶绿素图; 在过去的几年中,这种可视化类型变得越来越流行,因为制作这样的地图变得更加简单,尤其是在线制作。 其他地理可视化将覆盖标记或其他形状来表示数据; 无需用地理上有意义的边界填充特定形状。
准备
在完成最后一个配方后,您应该准备使用我们创建的数据集来可视化地理分布。 我们将使用ggplot2包来生成我们的可视化文件。 我们还将使用RColorBrewer程序包,该程序包提供视觉上有吸引力的调色板。 如果您目前没有RColorBrewer,请使用install.packages('RColorBrewer', repos='http://cran.r-project.org')进行安装。
怎么做...
以下步骤将引导您完成此地理空间数据可视化的创建:
- 我们首先需要在地图上获取一些数据。
ggplot2软件包提供了一个方便的功能map_data,用于从maps软件包中捆绑的数据中提取该函数:
library(ggplot2)
library(RColorBrewer)
state_df <- map_data('state')
county_df <- map_data('county')
- 现在,我们进行一些转换以使此数据与我们的数据一致:
transform_mapdata <- function(x){
names(x)[5:6] <- c('state','county')
for(u in c('state','county'){
x[,u] <- sapply(x[,u],simpleCap)
}
return(x)
}
state_df <- transform_mapdata(state_df)
county_df <- transform_mapdata(county_df)
data.frame对象state_df和county_df包含点的纬度和经度。 这是我们的主要图形数据,需要将它与我们在上一个配方中创建的数据结合起来,其中包含有效的地图颜色信息:
chor <- left_join(state_df, d.state, by='state')
ggplot(chor, aes(long,lat,group=group))+
geom_polygon(aes(fill=wage))+geom_path(color='black',size=0.2) + scale_fill_brewer(palette='PuRd') + theme(axis.text.x=element_blank(),
axis.text.y=element_blank(), axis.ticks.x=element_blank(),
axis.ticks.y=element_blank())
这使我们得到下图,该图描述了各州的平均年薪分布:

- 我们可以类似地创建县平均年薪的可视化,这将为我们提供有关工资的地理分布的更详细的信息:
chor <- left_join(county_df, d.cty)
ggplot(chor, aes(long,lat, group=group))+
geom_polygon(aes(fill=wage))+
geom_path( color='white',alpha=0.5,size=0.2)+
geom_polygon(data=state_df, color='black',fill=NA)+
scale_fill_brewer(palette='PuRd')+
labs(x='',y='', fill='Avg Annual Pay')+
theme(axis.text.x=element_blank(), axis.text.y=element_blank(), axis.ticks.x=element_blank(), axis.ticks.y=element_blank())
这产生了下图,显示了各县平均年薪的地理分布:

从上图可以明显看出,北达科他州西部,怀俄明州和内华达州西北部都有高薪工作,这很可能是由这些地区新的石油勘探机会推动的。 更明显的城市和沿海地区也表现得很好。
工作原理...
让我们深入了解前面 4 个步骤的工作原理。 ggplot2提供map_data功能,以从maps包中提取地图数据。 除了县和州以外,它还可以提取由[...] maps软件包。
county_df和state_df中包含州和县信息的列最初被命名为region和subregion。 在第 2 步中,我们需要将其名称分别更改为state和county,以使将这些数据与我们的就业数据结合起来更加容易。 我们还使用州和县的名称大写,以符合格式化employment数据集中数据的方式。
为了在第 3 步中创建地图,我们通过使用州名将state_df和d.state连接起来来创建绘图数据集。 然后,我们使用ggplot绘制美国地图,并在每个州中填入与以前的食谱中创建的工资水平和离散化的平均年薪相对应的颜色。 详细地说,我们确定该图的数据来自chor,并根据每个州的边界的经度和纬度绘制多边形(geom_polygon),并根据工资的高低用一种颜色填充它们 ,然后以黑色绘制状态的实际边界(geom_path)。 我们指定我们将使用从白色开始,经过紫色和具有对应于最高工资水平的红色的调色板。 通过指定标签并从图中删除轴注释和刻度来格式化其余代码。
对于第 4 步,代码与第 3 步基本相同,除了我们为县(而非州)的边界绘制多边形。 除了白色的县界以外,我们还添加了一层以黑色(geom_polygon(data=state_df, color='black', fill=NA))绘制州界。
另请参见
- 请参阅这个页面
按行业探索工作位置
在前面的食谱中,我们看到了如何可视化薪酬的顶级汇总数据。 就业数据集具有更细粒度的数据,按公共/私营部门和工作类型划分。 该数据中的作业类型遵循称为北美行业分类系统(NIACS)的分层编码系统。 在本食谱中,我们将考虑四个特定行业,并以可视化方式查看这些行业的就业地理分布,仅限于私营部门的工作。
我们将在此配方中研究四个工业部门:
- 农业,林业,渔业和狩猎业(NIACS 11)
- 采矿,采石与油气开采(NIACS 21)
- 金融和保险(NIACS 52)
- 专业技术服务(NIACS 54)
怎么做...
我们需要执行以下步骤来创建就业数据的子集,包括工业部门的数据,但将其限制为私营部门:
- 我们首先根据对行业和私营部门施加的条件过滤数据,并仅保留相关变量:
d.sectors <- filter(ann2012full, industry_code %in% c(11,21,54,52),
own_code==5, # Private sector
agglvl_code == 74 # county-level
) %.%
select(state,county,industry_code, own_code,agglvl_code,
industry_title, own_title, avg_annual_pay,
annual_avg_emplvl)%.%
mutate(wage=Discretize(avg_annual_pay),
emplevel=Discretize(annual_avg_emplvl))
d.sectors <- filter(d.sectors, !is.na(industry_code))
注意
在这里,我们的选择基于一组行业规范,并且我们将自己限于县级数据。 该代码与以前不同,因为我们现在正在查看行业特定的数据。
- 现在,我们使用
ggplot2创建可视化。 该可视化将由四个面板组成的阵列,每个工业部门一个。 每个面板上都会有美国的县级地图,上面的颜色表示每个特定行业在 2012 年每个县的就业水平。 我们还将为此可视化选择一个蓝色为主的调色板:
chor <- left_join(county_df, d.sectors)
ggplot(chor, aes(long,lat,group=group))+
geom_polygon(aes(fill=emplevel))+
geom_polygon(data=state_df, color='black',fill=NA)+
scale_fill_brewer(palette='PuBu')+
facet_wrap(~industry_title, ncol=2, as.table=T)+
labs(fill='Avg Employment Level',x='',y='')+
theme(axis.text.x=element_blank(),
axis.text.y=element_blank(),
axis.ticks.x=element_blank(),
axis.ticks.y=element_blank())
这将产生以下可视化效果,显示按行业划分的就业分布:

工作原理...
在此配方中,我们将dplyr函数用于数据munging。 我们的过滤条件之一是 industry_code 变量应具有11,21,52或54中的值之一。 这是通过%in%操作员完成的,它是一种设置操作。 它询问左侧的元素是否是右侧集合的成员。 filter语句中有多个条件,以逗号分隔。 这意味着AND关系,必须满足所有标准才能使数据通过过滤器。
我们注意到行业代码中缺少一些值。 这在可视化中产生了一个额外的面板,与缺少行业代码的数据相对应。 我们不想这样做,因此我们在第一步中过滤掉了这些数据。
在第二步中,用于创建可视化的命令与上一配方基本相同,但以下几行除外:
facet_wrap(~industry_title, ncol=2)
此命令按industry_title的值拆分数据,为industry_title,的每个值创建单独的可视化文件,然后将它们放回具有两列和适当数量的行的网格中。 我们还在此处使用了industry_title而不是 industry_code(它们具有相同的可视化效果),这样面板的标签是可以理解的,而不是仅包含一些需要读者看的数字 他们的意思。
还有更多...
此食谱是此数据集的冰山一角。 从私营/公共部门以及深入到不同行业的角度来看,可以使用此数据探索许多层次。 还可以提供 2012 年的其他季度数据,这些数据可以阐明时间模式。 从 1990 年起可以获取年度和季度数据。 通过将就业的时间模式与其他社会经济事件相关联,可以进行进一步的分析。 choroplethr和rMaps包提供了为这种类型的数据随时间创建动画的方法。
另请参见
- 在这个页面和这个页面中了解有关
rMaps程序包的信息 - 有关
choroplethr软件包的信息 - 查看
choropleth挑战结果 - 查看动画地图的示例
地理空间时间序列的动画地图
该项目的真正兴趣之一是了解工资模式(作为收入模式的替代)如何随时间变化。 QCEW 网站提供了 2003 年至 2012 年的数据。在此配方中,我们将查看这些年份每年各县的总体平均年薪,并创建一个动画显示该时期薪资模式的变化。
准备
对于此食谱,我们需要从 BLS 网站(HTG4)http://www.bls.gov/cew/datatoc.htm 下载 2003 至 2011 年的年度数据。 您需要在CSVs Single Files-Annual Averages列中下载与这些年份相对应的基于 QCEW NIACS 的数据文件。 将这些文件(它们是压缩的.zip文件)存储在与在此项目开始时下载的压缩 2012 数据相同的位置。 不要解压缩它们! 如果尚未安装,则还必须使用install.packages('chloroplethr')下载并安装choroplethr软件包。
注意
请注意,此配方相对来说会占用大量内存。 在 32 位计算机上运行的计算机可能会遇到内存不足的问题。
怎么做...
我们需要做的是导入从 2003 年到 2012 年的所有年份的数据,并提取每个县的县级(agglvl_code==70)平均年薪(avg_annual_pay)的数据,将其绘制出来,然后将薪水字符串化 值一起在动画中显示。 由于我们基本上需要对每年的数据执行相同的操作,因此可以在for循环中执行此操作,并且可以创建函数来封装重复的操作。 我们首先编写一年的代码,然后执行以下步骤:
- 我们从这个原型代码中称为
zipfile的 ZIP 文件中导入数据。 实际上,文件名的格式为2003_annual_singlefile.zip,而其中的 CSV 文件的格式为2003.annual.singlefile.csv。 我们将使用代码中 ZIP 和 CSV 文件中的常见模式来自动执行该过程。 对我来说,数据位于一个名为data的文件夹中,该文件夹反映在以下代码中:
unzip(file.path('data',zipfile), exdir='data') # unzips the file
csvfile <- gsub('zip','csv', zipfile) # Change file name
csvfile <- gsub('_','.',csvfile) # Change _ to . in name
dat <- fread(file.path('data', csvfile)) # read data
- 现在,我们将就业数据与
myarea中的地理数据结合起来:
dat <- left_join(dat, myarea)
- 然后,我们使用
dplyr函数提取县级汇总工资数据,并保留州和县的信息:
dat <- filter(dat, agglvl_code==70) %.% # County-level aggregate
select(state, county, avg_annual_pay) # Keep variables
- 然后,我们将步骤 1 到步骤 3 中的动作封装在一个函数中:
get_data <- function(zipfile){
unzip(file.path('data',zipfile), exdir='data') # unzips the file
csvfile <- gsub('zip','csv', zipfile) # Change file name
csvfile <- gsub('_','.',csvfile) # Change _ to . in name
dat <- fread(file.path('data', csvfile)) # read data
dat <- left_join(dat, myarea)
dat <- filter(dat, agglvl_code==70) %.% # County-level aggregate
select(state, county, avg_annual_pay) # Keep variables
return(dat)
}
- 我们现在必须在 10 年中的每一年都重复一次,并存储数据。 对于这种类型的数据,通常使用列表对象:
files <- dir('data', pattern='annual_singlefile.zip') # file names
n <- length(files)
dat_list <- vector('list',n) # Initialize the list
for(i in 1:n){
dat_list[[i]]<- get_data(files[i]) # ingest data
names(dat_list)[i] <- substr(files[i],1,4) #label list with years
}
- 接下来,我们开始创建可视化。 由于我们实际上是在创建 10 个可视化效果,因此为了比较起见,颜色在所有颜色上都必须表示相同的含义。 因此,多年来离散化必须相同:
annpay <- ldply(dat_list) # puts all the data together
breaks <- quantile(annpay$avg_annual_pay,
c(seq(0,.8,.2),.9,.95,.99,1)) # Makes a common set of breaks
- 我们将使用相同的间隔为每年创建相同的可视化。 让我们为要生成的常见可视化创建一个函数。 我们将使用
ggplot2进行可视化。 输入值是我们使用get_data创建的数据,输出是可以创建可视化效果的plot对象:
mychoro <- function(d, fill_label=''){
# d has a variable "outcome" that
# is plotted as the fill measure
chor <- left_join(county_df, d)
plt <- ggplot(chor, aes(long,lat, group=group))+
geom_polygon(aes(fill=outcome))+
geom_path(color='white',alpha=0.5,size=0.2)+
geom_polygon(data=state_df, color='black',fill=NA)+
scale_fill_brewer(palette='PuRd')+
labs(x='',y='', fill=fill_label)+
theme(axis.text.x=element_blank(),
axis.text.y=element_blank(),
axis.ticks.x=element_blank(),axis.ticks.y=element_blank())
return(plt)
}
- 现在,我们每年使用
for循环创建plot对象。 我们将这些对象存储在列表中,每个元素对应于每年。 在此过程中,我们使用公共休息时间创建了一个新变量outcome,它是离散化的薪水变量。 由于我们设计mychoro函数的方式,因此需要将此变量称为outcome:
plt_list <- vector('list',n)
for(i in 1:n){
dat_list[[i]] <- mutate(dat_list[[i]],
outcome=Discretize(avg_annual_pay,breaks=breaks))
plt_list[[i]] <-
mychoro(dat_list[[i]])+ggtitle(names(dat_list)[i])
}
choroplethr程序包具有实用程序功能choroplethr_animate,该功能可获取使用ggplot2创建的plot对象的列表,并制作一个带有动画 GIF 的网页,对我们在其中创建的绘图进行分层 命令。 默认的 Web 文件是animated_choropleth.html:
library(choroplethr)
choroplethr_animate(plt_list)
我们从此处的动画中提取三个面板,以使您了解动画的外观:

即使从有限的数据角度来看,我们也可以看到北达科他州西部,怀俄明州和内华达州东北部的就业和财富显着增长,这可能是由于该地区的页岩油的发现,勘探和开采所致。 我们还可以看到,总体来说,在此显示的八年中,全国各地的薪水都在上涨; 但是,在此期间,美国大陆中部地区的平均工资几乎没有变化。 我们还在加利福尼亚州和东北部看到高薪的明显增加和扩展。 回想一下,所有三个图都在相同的色标上,因此,它们之间的解释是一致的。
工作原理...
我们在先前的食谱中介绍了此食谱中使用的各个功能,因此我们将不再重复。 但是,从这里看大图可以为我们指出两个要点。 首先,我们必须多次执行一组通用步骤才能创建单个图像。 由于我们必须执行相同的步骤来创建最终动画中使用的每个图像,因此我们开始对代码进行一些操作,将重复的代码块重构为功能。
其次,创建每个图像所需的一组步骤是数据科学流水线各阶段的又一例证。 在第一阶段,我们通过摄取 CSV 文件来获取数据。 我们对数据集的理解已建立在先前的配方中,因此探索和理解阶段(管道的第 2 阶段)有点轻松。 我们加入了不同的数据集,并对其进行了过滤,以进入第 3 阶段,即纠缠,争吵和操纵阶段。 对于第 4 阶段,分析和建模,我们只需要离散化数据,然后将其映射到特定但一致的色标即可。 最后,最终的数据产品,动画的整体速度,用于以简明易懂的方式传达大量数据。
还有更多...
R 程序包choroplethr可以使用choropleth函数直接使用ggplot2.创建单个的弦乐。但是,我们不喜欢输出的默认外观,并且直接使用ggplot2可以更轻松地进行自定义。
流行的 rCharts 程序包的创建者 Vaidyanathan 博士也创建了rMaps程序包。 它使用 JavaScript 可视化库从 R 中创建了鹅卵石,以便在 Web 上进行演示,还可以使用ichoropleth.函数创建动画的鹅卵石。但是,在编写本文时,该包仍在开发中,因此我们没有该功能 创建县级地图。 在这个页面的rMaps博客中显示了具有状态级别映射的示例。
对一些常见任务的基准性能
R 及其程序包生态系统通常提供执行相同任务的几种替代方法。 R 还促进用户为特定任务创建自己的功能。 当执行时间很重要时,基准测试性能是必要的,以查看哪种策略最有效。 在本食谱中,我们将专注于速度。 我们将要看的两个任务是将数据加载到 R 中,并基于一个公共变量连接两个数据对象。 所有测试都是在运行 2.4 GHz Intel 处理器,8 GB RAM 的 Windows 7 台式机上完成的。
准备
我们将在此处使用ann2012full和industry数据对象进行性能实验,并使用 2012 年度就业数据 CSV 文件进行数据加载。 既然您已经拥有了这些,那么您就很好了。 如果不这样做,则需要使用install.packages()命令安装rbenchmark和microbenchmark这两个功能。
怎么做...
以下步骤将引导我们逐步完成 R 中两个不同任务的基准测试:
- 我们的首要任务是将雇佣数据加载到 R 中。
2012.annual.singlefile.csv文件包含 3,556,289 行数据和 15 列。 在本章中使用fread时,还有许多其他可能的方法可以导入此数据,如下所示:
第一种也是最标准的方法是使用read.csv读取 CSV 文件。 您也可以即时解压缩原始2012_annual_singlefile.zip数据文件,并使用read.csv读取数据。 我们可以在第一次加载数据时将数据保存到RData文件中,然后在以后加载此文件时也可以将数据导入 R 中
- 基准速度的最基本方法是使用
system.time函数,该函数测量要执行的任务所花费的时间(经过的时间和实际的计算时间):
> system.time(fread('data/2012.annual.singlefile.csv'))
user system elapsed
14.817 0.443 15.23
请注意,您看到的时间将不同于前面命令中列出的时间。
- 但是,R 中有一些软件包可以使基准测试和比较不同功能变得更加容易。 我们将介绍
rbenchmark程序包,该程序包提供benchmark功能,该功能允许同时比较不同的功能:
library(rbenchmark)
opload <- benchmark(
CSV=read.csv('data/2012.annual.singlefile.csv',
stringsAsFactors=F),
CSVZIP=read.csv(unz('data/2012_annual_singlefile.zip',
'2012.annual.singlefile.csv'), stringsAsFactors=F),
LOAD = load('data/ann2012full.rda'),
FREAD = fread('data/2012.annual.singlefile.csv'),
order='relative', # Report in order from shortest to longest
replications=5
)
您可以参考以下屏幕截图,获取上述命令的输出:

请注意,结果是有序的,并且相对时间记录在relative列下。 这表明fread比使用read.csv读取要快得多。 有趣的是,平均而言,它比从RData文件加载数据快四倍,后者是 R 数据的常用存储方法。 使用fread从文件加载数据显然比以 R 自己的序列化格式存储数据要快!
- 我们的第二个任务是执行两个数据对象的左外部联接。 我们将看一下已经执行的任务-雇佣数据与行业代码的左连接。 左联接确保操作的左上的数据行将通过该操作保留,而其他数据将通过重复或丢失数据扩展为具有相同的行数。 我们在本章中使用了
left_join,但是我们可以采用以下三种其他策略:- R 的标准库中的
merge函数 plyr程序包中的join功能data.table包中的merge函数,首先将数据转换为data.table对象
- R 的标准库中的
- 我们将再次使用
benchmark函数将这些策略与left_join进行比较:
ann2012full_dt <- data.table(ann2012full, key='industry_code')
industry_dt <- data.table(industry, key='industry_code')
op <- benchmark(
DT = data.table::merge(ann2012full_dt, industry_dt,
by='industry_code', all.x=T),
PLYR = plyr::join(ann2012full, industry,
by='industry_code',type='left'),
DPLYR = dplyr::left_join(ann2012full, industry),
DPLYR2 = dplyr::left_join(ann2012full_dt, industry_dt),
MERGE = merge(ann2012full, industry,
by='industry_code', all.x=T),
order='relative',
replications=5
)
您可以参考以下屏幕截图,获取上述命令的输出:

在这里,我们看到data.table方法比任何其他策略都快得多。 对于特定任务,使用dplyr的速度要慢 12 倍,plyr的速度要慢 100 倍,而标准的merge方法要慢 200 倍。 将data.frame对象转换为data.table对象会有一些开销,但是此任务的优势已克服了这一开销。
工作原理...
R 中时间基准测试的基本功能是system.time函数。 此函数记录表达式求值开始的时间,运行表达式,然后记下表达式完成的时间。 然后,它报告两次的差异。 默认情况下,垃圾回收会在每次评估之前进行,以便结果更加一致,并为每次评估释放最大的内存。
rbenchmark软件包中的benchmark功能提供了更大的灵活性。 它包装了system.time函数,并允许在一次运行中评估多个表达式。 它还可以进行一些基本计算(例如相对时间)来简化报告。
根据此处的任务,fread使用强大的优化 C 函数读取数据,从而实现了高度的速度优化。 read.csv函数仅逐行读取数据文件,并使用逗号分隔符解析字段。 通过使用colClasses选项在read.csv中指定列类型,我们可以在实验中提高速度,因为确定数据类型会消耗一些执行时间。 load函数从使用save函数创建的 RData 文件中读取数据,该文件存储 R 对象的二进制表示形式。 它压缩了很多数据,但是我们发现,读取数据比加载 RData 文件更有效。
我们设定为基准的第二项任务是将就业数据ann2014full与industry行业代码的数据对象一起左外连接。 前者有 3,556,289 行和 15 列,而后者有 2,469 行和两列。 它们基于公共变量industry_code合并。 在左联接中,将保留ann2014full的所有行。 为此,merge命令将使用all.x=T选项。 join功能具有用于左连接的type='left'选项。 对于 data.table 合并,我们首先将data.frame对象转换为data.table对象,并指定每个对象具有相同的键变量(请考虑数据库中的索引)industry_code。 然后,使用此键变量合并 data.table 对象。
此代码段中有一些新的代码格式。 我们使用plyr::join和dplyr::left_join,而不仅仅是join和left_join。 这种编码方式明确指定我们正在使用来自特定程序包的特定功能,以避免混淆。 有时,当您在两个都装入 R 的两个不同包中具有相同名称的函数时,这种编码风格很有用。
还有更多...
data.table软件包提供了非常快速的工具来进行数据加载,修改和联接。 data.table对象是data.frame包的派生对象,R 中输入data.frame对象的许多函数也可以导入data.table对象。 因此,data.table对象成为矩形数据的默认容器。
另请参见
- Hadley Wickham 在他的在线书籍中有一个关于基准测试的很好的阐述,可以在这个页面上找到。 他推广
microbenchmark软件包以进行基准测试。
六、汽车数据的可视化分析
在本章中,我们将介绍:
- Jupyter 入门
- 探索 Jupyter 笔记本
- 准备分析汽车燃油效率
- 使用 Python 探索和描述燃油效率数据
- 使用 Python 分析一段时间内的汽车燃油效率
- 使用 Python 研究汽车的品牌和型号
简介
在有关 R 的第一章(第 2 章,“使用 R 进行汽车数据驾驶视觉分析”)中,我们完成了一个分析项目,该项目使用 R 统计编程语言检查了汽车燃油经济性数据。 。 该数据集可从这个页面获得,其中包含美国所有品牌和型号的汽车随时间推移的燃油效率性能指标。 美国。 该数据集还包含除燃油经济性以外的汽车模型的许多其他特征和属性,从而提供了汇总和分组数据的机会,以便我们可以识别有趣的趋势和关系。
与 R 的第一章不同,我们将使用 Python 执行整个分析。 但是,我们将再次遵循数据科学流程,提出与之前相同的问题,并按照相同的步骤顺序进行操作。 通过学习,您将可以看到两种语言之间的异同,从而进行几乎相同的分析。
在这里,我们在 NumPy 和 SciPy 的帮助下使用了大多数纯 Python,它们直接来自 Python 命令行-也称为 Read-Eval-Print Loop(REPL)-或来自可执行文件 脚本文件。 在本章中,我们将采用一种非常不同的方法,以类似于 R 的交互方式将 Python 用作脚本语言。我们将向读者介绍 Python,IPython 和 Jupyter 笔记本的非正式交互环境,展示如何 生成可读性良好且有据可查的分析脚本。 此外,我们将利用相对较新但功能强大的 Pandas 库的数据分析功能及其提供的宝贵数据框数据类型。 熊猫通常允许我们用更少的代码行来完成复杂的任务。 这种方法的缺点是,虽然您不必为常见的数据处理任务重新发明轮子,但您必须学习完全不同的程序包(即 Pandas)的 API。
本章的目的不是指导您完成已经完成的分析项目,而是向您展示如何用另一种语言完成该项目。 更重要的是,我们希望让您(读者)对您自己的代码和分析更加自省。 不仅要考虑如何完成某事,还应该考虑为什么要用这种特定语言来完成某件事。 语言如何影响分析?
IPython 入门
IPython 是 Python 的交互式计算外壳,它将改变您对交互式外壳的思考方式。 它带来了许多非常有用的功能,这些功能很可能会成为默认工具箱的一部分,包括魔术功能,制表符补全,易于访问的命令行工具等等。 我们仅在这里进行介绍,强烈建议您继续探索使用 IPython 可以完成的操作,有关更多详细信息和选项,请参见这个页面。
准备
如果您已经完成了第一章中的安装说明,则应该准备解决以下问题。 请注意,主要版本 IPython 6.1 已于 2017 年 5 月启动。IPython 会话已于 2017 年 3 月在较早版本的交互式软件上运行。
怎么做...
以下步骤将使您开始使用 IPython 环境:
- 在计算机上打开一个终端窗口,然后输入
ipython。 您应该立即看到以下内容:

注意
请注意,您的版本可能与前面的命令行输出中显示的版本略有不同。
- 只是为了向您展示 IPython 的强大之处,请键入
ls,您将会看到目录清单! 是的,您可以直接在 Python 解释器中的 Python 提示符下访问常见的 Unix 命令。 - 现在,让我们尝试更改目录。 在提示符下键入
cd,打空格,然后单击 选项卡。 应该为您提供当前目录中可用目录的列表。 开始输入目标目录的前几个字母,然后再次单击 选项卡。 如果只有一个选项匹配,则自动单击 选项卡 键将插入该名称。 否则,可能性列表将仅显示与您已经键入的字母匹配的名称。 当您按下 选项卡 时,输入的每个字母都充当过滤器。

- 现在,键入
?,您将快速了解 IPython 的功能并对其进行概述。

- 让我们看一下魔术功能。 这些是 IPython 可以理解的特殊功能,并且始终以
%符号开头。%paste函数就是这样的一个例子,它对于在不丢失适当缩进的情况下将 Python 代码复制和粘贴到 IPython 中来说是惊人的。 一个简单的例子是在任何文本编辑器中键入2+3并复制它,然后在 IPython 中执行%paste。 - 我们将尝试
%timeit魔术功能,该功能可以智能地对 Python 代码进行基准测试。 输入以下命令:
In [2]: n=10000
...: %timeit range(n)
...:
我们应该得到这样的输出:
The slowest run took 4.74 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 1.83 mus per loop
请注意,Python 3.x 版本中的 range 函数是早期 Python 2.x 版本中的xrange函数。Â
- 您还可以通过在命令前面加上感叹号来轻松运行系统命令。 尝试以下命令:
In [3]: !ping www.google.com
您应该看到以下输出:
Pinging www.google.com [216.239.32.20] with 32 bytes of data:
Reply from 216.239.32.20: bytes=32 time=35ms TTL=57
Reply from 216.239.32.20: bytes=32 time=34ms TTL=57
Reply from 216.239.32.20: bytes=32 time=33ms TTL=57
Reply from 216.239.32.20: bytes=32 time=35ms TTL=57
Ping statistics for 216.239.32.20:
Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 33ms, Maximum = 35ms, Average = 34ms
- 最后,IPython 提供了出色的命令历史记录。 只需按向上箭头键即可访问先前输入的命令。 继续按向上箭头键可在会话的命令列表中向后浏览,而向下箭头键则可向前移动。 同样,神奇的
%history命令允许您跳到会话中的特定命令编号。 键入以下命令以查看您输入的第一个命令:
In [4]: %history 1
IPython 会话的完整屏幕截图如下:

现在,键入exit以退出 IPython 并返回到系统命令提示符。
工作原理...
这里没有太多要解释的东西,我们只是从头开始了解 IPython 可以做的事情。 希望我们引起了您对更深层次的兴趣,特别是 IPython 6.0 提供的众多新功能,包括动态和用户可控制的数据可视化。
另请参见
探索 Jupyter 笔记本
Jupyter Notebook 是 Python 的完美补充。在实践中,Jupyter Notebook 使您可以在代码中插入注释和图像以及其他可能有用的东西。 您可以使用 Jupyter 笔记本进行各种操作,从演示文稿(PowerPoint 的绝佳替代品)到电子实验室笔记本或教科书。
准备
如果您已经完成了第一章中的安装说明,则应该准备解决以下问题。
怎么做...
这些步骤将使您开始探索功能异常强大的 Jupyter Notebook 环境。 我们敦促您超越这些简单的步骤,以了解该工具的真正功能:
-
使用
mkdir test在终端上创建一个文件夹,例如 test,然后使用cd test将其移动到该文件夹。 -
在命令提示符下键入
jupyter notebook。 您应该在终端窗口中看到一些文本快速滚动,然后,以下屏幕应在默认浏览器中加载(对我来说,这是 Firefox)。 请注意,URL 应为localhost:8888/tree,表示浏览器已连接至本地计算机上运行的服务器,该服务器的端口为8888。 -
您不会在浏览器中看到任何笔记本(注意 Jupyter Notebook 文件具有
.ipynb扩展名),因为 Jupyter Notebook 在从其启动目录中搜索笔记本文件。 现在创建一个笔记本。 单击下拉菜单New,找到 Notebook 的选项,然后单击 Python 3 选项(尽管您可能有所不同):

- 应该会打开一个新的浏览器选项卡或窗口,向您显示类似于以下屏幕截图的内容:

-
从上至下,您可以看到基于文本的菜单,其后是用于发布常用命令的工具栏,然后是您的第一个单元格,该单元格应类似于 IPython 中的命令提示符。
-
将鼠标光标放在第一个单元格中,然后输入
5+5。 接下来,导航至Cell|Run或按 Shift + 输入 作为键盘快捷键,以使单元格的内容得以解释。 现在,您应该会看到类似于以下屏幕截图的内容。 基本上,我们只是在第一个 Jupyter Notebook 的第一个单元格中执行了一个简单的 Python 语句。 -
单击第二个单元格,然后导航到
Cell|Cell Type|Markdown。 现在,您可以轻松地在单元中编写markdown,以用于记录。 -
关闭两个浏览器窗口或选项卡(笔记本和笔记本浏览器)。
-
返回您在其中键入
jupyter notebook的终端,按 Ctrl +C,然后按Y,然后按 输入。 这将关闭 Jupyter Notebook 服务器。
工作原理...
对于那些来自更传统的统计软件包(例如 Stata,SPSS 或 SAS)或更传统的数学软件包(例如 MATLAB)的用户 ,Mathematica 或 Maple,您可能已经习惯了各自公司提供的非常图形化且功能丰富的交互式环境。 在这种背景下,Jupyter Notebook 可能看起来有些陌生,但希望它比传统的 Python 提示符更加用户友好并且不那么吓人。 此外,Jupyter Notebook 提供了交互性和顺序工作流程的有趣组合,特别适合于数据分析,特别是在原型设计阶段。 R 有一个名为Knitr的库,它提供 Jupyer Notebook 的报告生成功能。
当您输入jupyter notebook时,您将启动在本地计算机上运行的服务器,而 Jupyter Notebook 本身实际上是使用服务器-客户端体系结构的 Web 应用。 根据 ipython.org 的 Jupyter Notebook 服务器使用具有 ZeroMQ 和龙卷风的两进程内核体系结构。 ZeroMQ 是用于高性能消息传递的智能套接字库,可帮助 Jupyter 管理分布式计算集群以及其他任务。 Tornado 是一个 Python Web 框架和异步联网模块,可为 Jupyter Notebook 的 HTTP 请求提供服务。 该项目是开源的,如果您愿意,可以为源代码做贡献。
Jupyter Notebook 还允许您使用名为nbconvert。 可用的导出格式包括 HTML,LaTex,reveal.js HTML 幻灯片,Markdown,简单的 Python 脚本,以及 Sphinx 文档的reStructuredText。
最后,还有 Jupyter Notebook Viewer(nbviewer),它是一项免费的 Web 服务,您可以在其中发布并浏览托管在远程服务器上的静态 HTML 版本的笔记本文件(这些服务器目前由 Rackspace 捐赠)。 因此,如果您创建了一个想要共享的.ipynb惊人文件,则可以将其上传到这个页面,让全世界看到您的努力。
还有更多...
我们将尽量不要大声地赞美 Markdown,但如果您不熟悉该工具,我们强烈建议您尝试一下。 Markdown 实际上是两种不同的东西:一种以易于转换为结构化文档的方式格式化纯文本的语法,以及一种将所述文本转换为 HTML 和其他语言的软件工具。 基本上,Markdown 使作者可以使用任何所需的简单文本编辑器(VI,VIM,Emacs,Sublime 编辑器,TextWrangler,Crimson Editor 或 Notepad)可以捕获纯文本,但仍描述相对复杂的结构,例如不同级别的标题,有序和无序列表以及块引号以及某些格式,例如 粗体和斜体。 Markdown 基本上提供了一种非常易于阅读的 HTML 版本,类似于 JSON,并且提供了一种易于阅读的数据格式。
另请参见
准备分析汽车燃油效率
在本食谱中,我们将开始对汽车燃油效率数据进行基于 Python 的分析。
准备
如果您成功完成了第一章,则应该准备开始。
怎么做...
以下步骤将指导您设置工作目录和 IPython 以进行本章的分析:
- 创建一个名为
fuel_efficiency_python的项目目录。 - 从这个页面下载汽车燃油效率数据集,并将其存储在前面的目录中。 将
vehicles.csv文件从 zip 文件中提取到同一目录中。 - 打开一个终端窗口,然后将当前目录(
cd)更改为fuel_efficiency_python目录。 - 在终端上,键入以下命令:
jupyter notebook
- 将新页面加载到 Web 浏览器中后,单击
New Notebook。 - 单击笔记本的当前名称
untitled0,然后输入此分析的新名称(我的名称为fuel_efficiency_python)。 - 让我们将最上面的单元格用于
import语句。 输入以下命令:
In [5]: import pandas as pd
...: import numpy as np
...: from ggplot import *
...: %matplotlib inline
然后,按 Shift + 输入 以执行单元。 这将同时导入pandas和numpy库,并为它们分配本地名称以在键入命令时节省一些字符。 它还导入ggplot库。 请注意,在 Python 中使用from ggplot import *命令行不是最佳实践,它会将ggplot程序包内容倒入我们的默认名称空间中。 但是,我们这样做是为了使我们的ggplot语法与 R ggplot2语法最相似,而这并不是 Python 语言。 最后,我们使用魔术命令告诉 Jupyter Notebook 我们想要matploblib图形在笔记本中呈现。
- 在下一个单元格中,让我们导入数据并查看前几条记录:
In [6]: vehicles = pd.read_csv("vehicles.csv")
但是,请注意,出现红色警告消息,如下所示:
C:\Users\prabhanjan.tattar\AppData\Local\Continuum\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py:2717: DtypeWarning: Columns (70,71,72,73,74,76,79) have mixed types. Specify dtype option on import or set low_memory=False.
这告诉我们列70,71,72,73,74,76, and 79包含混合数据类型。 让我们使用以下命令查找对应的名称:
In [7]: column_names = vehicles.columns.values
...: column_names[[70,71,72,73,74,76,79]]
...:
Out[7]: array(['cylinders', 'displ', 'fuelType2', 'rangeA', 'evMotor', 'mfrCode'], dtype=object)
注意
混合数据类型听起来可能是有问题的,因此请牢记这些列名称。 请记住,数据清理和整理通常会占用项目时间的 90%。
工作原理...
使用此配方,我们只需设置工作目录并创建一个新的 Jupyter Notebook,即可用于分析。 我们已经导入了 Pandas 库,并非常快速地将vehicles.csv数据文件直接读取到数据框中。 从经验上来讲,熊猫强大的数据导入功能将为您节省大量时间。
尽管我们直接从逗号分隔的值文件中将数据导入到数据框中,但是 pandas 能够使用阅读器功能处理许多其他格式,包括 Excel,HDF,SQL,JSON,Stata,甚至剪贴板。 我们还可以使用从数据帧对象访问的写入器功能以多种格式从数据帧中写出数据。
使用熊猫中Data Frame类的绑定方法head,我们收到了非常有用的数据框摘要,其中包括每列非空值的计数以及各种数据类型的计数 跨列。
还有更多...
数据帧是一个非常强大的概念和数据结构。 数据框架中的思维对于许多数据分析至关重要,但与数组或矩阵运算中的思维也有很大不同(例如,如果您来自 MATLAB 或 C 作为主要开发语言)。
对于数据框,每一列代表不同的变量或特征,并且可以是不同的数据类型,例如浮点数,整数或字符串。 数据帧的每一行都是一个单独的观察值或实例,具有自己的一组值。 例如,如果每一行代表一个人,则列可以是age(整数)和gender(类别或字符串)。 通常,我们将要选择与特定特征(例如,所有雄性)相匹配的一组观察值(行),并检查该子组。 从概念上讲,数据框与关系数据库中的表非常相似。
另请参见
使用 Python 探索和描述燃油效率数据
现在,我们已经将汽车燃油效率数据集导入 Jupyter,并见证了熊猫的力量,下一步是复制前一章在 R 中执行的初步分析,以了解熊猫的一些基本功能。
准备
我们将继续发展和发展我们在上一个食谱中开始的 Jupyer Notebook。 如果您已完成上一个食谱,则应具备继续进行所需的一切。
怎么做...
- 首先,让我们使用以下命令找出数据中有多少个观测值(行):
In [8]: len(vehicles)
...:
Out[8]: 38120
如果在 R 和 Python 之间来回切换,请记住,在 R 中,函数为length,而在 Python 中,函数为len。
- 接下来,让我们使用以下命令找出数据中有多少个变量(列):
In [9]: len(vehicles.columns)
...:
Out[9]: 83
Let's get a list of the names of the columns using the following command:
In [10]: print(vehicles.columns)
...:
Index(['barrels08', 'barrelsA08', 'charge120', 'charge240', 'city08',
'city08U', 'cityA08', 'cityA08U', 'cityCD', 'cityE', 'cityUF', 'co2',
'co2A', 'co2TailpipeAGpm', 'co2TailpipeGpm', 'comb08', 'comb08U',
'combA08', 'combA08U', 'combE', 'combinedCD', 'combinedUF', 'cylinders',
'displ', 'drive', 'engId', 'eng_dscr', 'feScore', 'fuelCost08',
'fuelCostA08', 'fuelType', 'fuelType1', 'ghgScore', 'ghgScoreA',
'highway08', 'highway08U', 'highwayA08', 'highwayA08U', 'highwayCD',
'highwayE', 'highwayUF', 'hlv', 'hpv', 'id', 'lv2', 'lv4', 'make',
'model', 'mpgData', 'phevBlended', 'pv2', 'pv4', 'range', 'rangeCity',
'rangeCityA', 'rangeHwy', 'rangeHwyA', 'trany', 'UCity', 'UCityA',
'UHighway', 'UHighwayA', 'VClass', 'year', 'youSaveSpend', 'guzzler',
'trans_dscr', 'tCharger', 'sCharger', 'atvType', 'fuelType2', 'rangeA',
'evMotor', 'mfrCode', 'c240Dscr', 'charge240b', 'c240bDscr',
'createdOn', 'modifiedOn', 'startStop', 'phevCity', 'phevHwy',
'phevComb'],
dtype='object')
每个字符串前面的u字母表示这些字符串以 Unicode 表示
- 让我们找出此数据集中包含多少个独特年份的数据,以及前几年和下一年使用以下命令的内容:
In [11]: len(pd.unique(vehicles.year))
...:
Out[11]: 34
In [12]: min(vehicles.year)
...:
Out[12]: 1984
In [13]: max(vehicles["year"])
...:
Out[13]: 2017
再次注意,我们使用了两种不同的语法来引用车辆数据框中的各个列。
- 接下来,让我们找出哪种类型的燃料用作汽车的主要燃料类型。 在 R 中,我们具有
table函数,该函数将返回变量各种值出现的次数。 在大熊猫中,我们使用以下内容:
In [14]: pd.value_counts(vehicles.fuelType1)
...:
Out[14]:
Regular Gasoline 26533
Premium Gasoline 10302
Diesel 1014
Electricity 134
Midgrade Gasoline 77
Natural Gas 60
Name: fuelType1, dtype: int64
- 现在,如果我们要探索这些汽车具有哪种类型的变速器,我们立即尝试以下命令:
In [15]: pd.value_counts(vehicles.trany)
但是,这会导致一些意外且冗长的输出:
Out[15]:
Automatic 4-spd 11042
Manual 5-spd 8323
Automatic 3-spd 3151
Automatic (S6) 2684
Manual 6-spd 2448
Automatic 5-spd 2191
Manual 4-spd 1483
Automatic 6-spd 1447
Automatic (S8) 981
Automatic (S5) 827
Automatic (variable gear ratios) 702
Automatic 7-spd 675
Automatic (S7) 270
Auto(AM-S7) 266
Automatic 8-spd 259
Automatic (S4) 233
Auto(AM7) 166
Auto(AV-S6) 153
Automatic (A1) 125
Auto(AM6) 120
Automatic 9-spd 105
Auto(AM-S6) 87
Auto(AV-S7) 80
Manual 3-spd 77
Manual 7-spd 73
Automatic (S9) 29
Auto(AV-S8) 28
Manual 4-spd Doubled 17
Auto(AM5) 12
Automatic (AV-S6) 11
Automatic (S10) 8
Auto(AM-S8) 6
Auto(AM8) 5
Automatic (AV) 4
Automatic (A6) 4
Manual(M7) 3
Auto(L3) 2
Auto(L4) 2
Automatic (AM5) 2
Auto (AV) 2
Auto (AV-S8) 1
Manual 5 spd 1
Auto (AV-S6) 1
Auto(AM-S9) 1
Automatic 6spd 1
Automatic (AM6) 1
Name: trany, dtype: int64
我们真正想知道的是具有自动和手动变速箱的汽车数量。 我们注意到,trany变量表示自动变速器时始终以字母A开头,而对于手动变速器则始终以字母M开头。 因此,我们创建一个新变量trany2,它包含trany变量的第一个字符,该字符是一个字符串:
In [16]: vehicles["trany2"] = vehicles.trany.str[0]
...: pd.value_counts(vehicles.trany2)
...:
前面的命令产生的答案是我们想要的,或者是手动生成的答案的两倍:
Out[16]:
A 25684
M 12425
Name: trany2, dtype: int64
工作原理...
在本食谱中,我们介绍了 Python 和 pandas 的一些基本功能。 我们使用了两种不同的语法(vehicles['trany']和vehicles.trany)来访问数据帧内的变量。 我们还使用了一些熊猫的核心功能来探索数据,例如非常有用的unique和value_counts功能。
还有更多...
在数据科学管道方面,我们在一个配方中涉及了两个阶段:数据清理和数据探索。 通常,当使用较小的数据集来完成特定操作的时间很短并且可以在我们的笔记本电脑上完成时,我们将非常快速地进行流水线的多个阶段,然后根据结果循环返回。 通常,数据科学管道是一个高度迭代的过程。 我们完成步骤的速度越快,可以在固定时间内进行的迭代就越多,并且通常我们可以创建更好的最终分析。
另请参见
使用 Python 分析随时间推移的汽车燃油效率
在本食谱中,我们将研究一段时间内以及与其他数据点相关的一些燃油效率指标。 为此,我们将不得不在 Python 中复制两个非常流行的 R 库的功能,它们是plyr和ggplot2。 plyr R 库非常方便地涵盖了拆分应用组合数据分析功能,但是熊猫开箱即用的方式处理方式也一样。 ggplot2的数据可视化功能-图形语法的 R 库实现-不能像在本食谱中看到的那样容易处理。
准备
如果您已经完成了上一个食谱,那么您应该拥有继续进行所需的几乎所有操作。 但是,我们将为 R 使用ggplot2库的 Python 克隆,它的方便命名为ggplot。 如果您没有完成整个设置章节并且尚未安装ggplot软件包,请打开一个终端并输入以下内容:
pip install ggplot (or sudo pip install ggplot)
这也适用于 Windows 计算机。 等待安装完成。 完成此操作后,必须重新启动 IPython Notebook 服务器才能导入此新安装的ggplot库。
怎么做...
我们将通过以下步骤进入分析阶段:
- 让我们开始看看 mpg 平均是否随时间变化的总体趋势。 我们首先要按年份对数据进行分组:
In [17]: grouped = vehicles.groupby("year")
- 接下来,我们要通过前面的分组计算三个独立列的平均值:
In [18]: averaged = grouped['comb08', 'highway08','city08'].agg([np.mean])
这将产生一个新的数据帧,该数据帧具有三列,分别包含comb08,highway08和city08变量的平均值。 请注意,我们正在使用 NumPy(np)提供的mean函数。
- 为了使生活更轻松,我们将重命名这些列,然后创建一个名为
year的新列,其中包含数据框的索引:
In [19]: averaged.columns = ['comb08_mean','highway08_mean','city08_mean']
...: averaged['year'] = averaged.index
请注意,与我们在 R 中要做的相比,重命名列有多么容易! 数据框的 columns 属性包含列的名称以及为重命名列而需要修改的内容。
- 最后,我们想使用 Python 库的新
ggplot包将结果绘制为散点图:
In [20]: ggplot(averaged, aes('year', 'comb08_mean')) + geom_point(
...: color='steelblue') + xlab("Year") + ylab("Average MPG"
...: ) + ggtitle("All cars")
请参考下图:

由于近来具有出色行驶里程的混合动力汽车变得越来越流行,因此该图可能会产生误导。 让我们看看是否可以筛选出这些汽车品牌。 精明的观察者将认识到,该图在 R 章中不包括匹配图像的geom_smooth()方法。 尽管当前的ggplot库(截至 2014 年 2 月 11 日为为 0.4.7)具有可能等效的stat_smooth()方法,但当前版本仍存在一些错误,这些错误会导致绘制错误的结果(而不是错误的结果)。 如图所示)。**
- 为了删除混合动力汽车,我们创建了三个布尔数组。
criteria1数组选择fuelType1为Regular Gasoline,Premium Gasoline或Midgrade Gasoline的数据帧的那些行。criteria2数组确保行包含fuelType2的空值,并且criteria3确保atvType不是Hybrid。 我们可以对这三个布尔数组一起执行逻辑AND操作,以仅从数据帧中选择所需的行:
In [21]: criteria1 = vehicles.fuelType1.isin(["Regular Gasoline",
...: "Premium Gasoline", "Midgrade Gasoline"])
...: criteria2 = vehicles.fuelType2.isnull()
...: criteria3 = vehicles.atvType != "Hybrid"
...: vehicles_non_hybrid = vehicles[criteria1 & criteria2 &
...: criteria3]
...: len(vehicles_non_hybrid)
...:
Out[21]: 34990
- 我们将结果数据框按年份分组,然后计算每年的平均组合燃油效率,得出以下数据框:
In [22]: grouped = vehicles_non_hybrid.groupby(['year'])
...: averaged = grouped['comb08'].agg([np.mean])
...: print(averaged)
...:
前面的命令将产生以下输出:
mean
year
1984 19.121622
1985 19.394686
1986 19.320457
1987 19.164568
1988 19.367607
1989 19.141964
1990 19.031459
1991 18.838060
1992 18.861566
1993 19.137383
1994 19.092632
1995 18.872591
1996 19.530962
1997 19.368000
1998 19.329545
1999 19.239759
2000 19.169345
2001 19.075058
2002 18.950270
2003 18.761711
2004 18.967339
2005 19.005510
2006 18.786398
2007 18.987512
2008 19.191781
2009 19.738095
2010 20.466736
2011 20.838219
2012 21.407328
2013 22.228877
2014 22.279835
2015 22.424555
2016 22.749766
2017 22.804085
根据前面的数据,即使消除混合动力,我们仍可以看到每加仑的平均英里数仍显着增加。
- 我们要问的下一个问题是,最近是否有较少的大型发动机汽车制造? 如果这是真的,那可以解释每加仑平均英里数的增加。 首先,让我们验证一下,大型发动机汽车的每加仑行驶里程会变差。 为此,我们需要深入研究
displ变量,该变量代表以升为单位的发动机排量。 记住,熊猫给了我们有关包含多种数据类型的变量的警告,因此让我们计算唯一的displ值:
In [23]: pd.unique(vehicles_non_hybrid.displ)
...:
Out[23]:
array([ 2\. , 4.9, 2.2, 5.2, 1.8, 1.6, 2.3, 2.8, 4\. , 5\. , 3.3,
3.1, 3.8, 4.6, 3.4, 3\. , 5.9, 2.5, 4.5, 6.8, 2.4, 2.9,
5.7, 4.3, 3.5, 5.8, 3.2, 4.2, 1.9, 2.6, 7.4, 3.9, 1.5,
1.3, 4.1, 8\. , 6\. , 3.6, 5.4, 5.6, 1\. , 2.1, 1.2, 6.5,
2.7, 4.7, 5.5, 1.1, 5.3, 4.4, 3.7, 6.7, 4.8, 1.7, 6.2,
8.3, 1.4, 6.1, 7\. , 8.4, 6.3, nan, 6.6, 6.4, 0.9])
- 我们看到有些值可能不是数字,包括
nan值。 让我们从vehicles_non_hybrid数据帧中删除所有具有nandispl值的行,然后对comb08变量执行相同的操作。 在此过程中,让我们使用astype方法来确保每个值的类型均为float,以防万一:
In [24]: criteria = vehicles_non_hybrid.displ.notnull()
...: vehicles_non_hybrid = vehicles_non_hybrid[criteria]
...: vehicles_non_hybrid.displ = vehicles_non_hybrid.displ.astype('float')
...:
In [25]: criteria = vehicles_non_hybrid.comb08.notnull()
...: vehicles_non_hybrid = vehicles_non_hybrid[criteria]
...: vehicles_non_hybrid.comb08 = vehicles_non_hybrid.comb08.astype('float')
- 最后,我们将使用
ggplot库再次生成结果的散点图:
In [26]: ggplot(vehicles_non_hybrid, aes('displ', 'comb08')) + geom_point(
...: color='steelblue') + xlab("Engine Displacement") +ylab(
...: "Average MPG") + ggtitle("Gasoline cars")
请参考下图:

前面的曲线似乎证实了燃油经济性与发动机排量之间的负相关关系。
现在,最近生产的带有大型发动机的汽车已经减少了吗?
- 让我们看看以后几年是否平均生产了较小的汽车:
In [27]: grouped_by_year = vehicles_non_hybrid.groupby(['year'])
...: avg_grouped_by_year = grouped_by_year['displ',
...: 'comb08'].agg([np.mean])
- 接下来,让我们在同一图上绘制平均
displ值和year的平均comb08值,以查找趋势。 为此,我们需要调整avg_grouped_by_year数据框的形状,以将其从宽格式转换为长格式:
In [28]: avg_grouped_by_year['year'] = avg_grouped_by_year.index
...: melted_avg_grouped_by_year = pd.melt(avg_grouped_by_year,id_vars='year')
然后,让我们创建多面图:
In [29]: p = ggplot(aes(x='year', y='value', color = 'variable_0'),
...: data=melted_avg_grouped_by_year)
...: p + geom_point() + facet_wrap("variable_0")
请参考下图:

工作原理...
让我们忽略分析的实际发现,因为它们与本章中使用 R 的发现相同。真正有趣的部分是本章中使用的许多重要数据分析技术。 让我们分别分解每种技术。
首先,让我们看一下通常的数据分析模式,称为 split-apply-combine,该模式先前在随附的 R 章中已提到。 分析数据集时,通常希望按一个或多个特征对数据进行分组,对分组的数据子集执行操作,然后将结果放在一起。 在本章中,我们将数据按年分组,计算不同变量的平均值,然后将这些结果合并。 在上一章中,我们使用了 Hadley Wickham 的plyr程序包。 对于plyr,我们调用ddply函数,该函数传递要分析的数据帧,要分组的特征或特征集,然后在分组数据上使用该函数。 然后ddply函数返回结果数据帧。 一行代码执行 split-apply-combine 模式。
Python 中的 Pandas 库采用的方法稍有不同,并拆分了plyr中单个函数调用中包含的功能。 首先,我们可以按照指定的特征(如grouped_by_year = vehicles_non_hybrid.groupby(['year'])这行代码)将熊猫对象(如数据帧)分组-在此处,我们通过year变量将vehicles_non_hybrid数据帧的行分组。 请注意,我们不仅限于按单个变量进行分组,还可以创建具有多个特征的分组(例如,年份和汽车公司)。
一旦有了grouped_by_year对象,如果我们想要执行以下操作,便可以遍历这些组:
for (name, group) in grouped_by_year:
print name
print group
这将打印出每个组名和结果数据框。 接下来,我们使用 NumPy 库(np.mean)中的mean函数对GroupBy对象使用aggregrate方法。 在以下代码中,我们选择仅从grouped_by_year数据帧中聚合单个变量comb08:
averaged = grouped['comb08'].agg([np.mean])
在熊猫中,库中内置了非常强大的“拆分应用组合”功能,而我们仅从表面上了解了它的功能。
还有更多...
我们还使用了 yhat 中的ggplot包,而不是古老的 matplotlib。 图形语法提供了一种非常简洁的(尽管高度非 Pythonic 的)图形描述方式。 开创性书籍图形语法,Leland Wilkinson,Springer 的封底指出:
“图形语法为产生几乎所有在科学期刊,报纸,统计软件包和数据可视化系统中发现的定量图形提供了独特的基础。尽管这项工作的实质成果是多个可视化软件库,但本书着重介绍 关于从数据生成定量图形所涉及的深层结构。饼图,条形图,散点图,函数图,地图,镶嵌图和雷达图的生成所依据的规则是什么?”
R 中的ggplot2软件包是 R 的最大资产之一,并且 Python 现在具有正常运行的ggplot克隆。 不幸的是,如本章中的一些经验所示,ggplot Python 库目前功能还不完善。
由于 Python ggplot库仍在开发中(平滑功能存在一些问题),您可能想知道有一个 Python 库可以在您的 Python 程序中使用 R。 位于这个页面的rpy2软件包提供了从 Python 到 R 的低层和高层接口。 低级接口有点类似于 R 的 C API。 高级接口将 R 对象公开为 Python 类的实例。 为了使用rpy2,请确保您已在系统上安装 R。 您从 Python 调用的所有软件包都必须在 R 中可用!
另请参见
- 熊猫:建立索引并选择数据
- 《Matplotlib 和 Python 中可视化的未来》
- Hadley Wickham 的主页
- 适用于 Python 的
ggplot软件包 - 更多适用于 Python 的
ggplot - 《数据分析的拆分应用组合策略》文章
使用 Python 调查汽车的品牌和型号
为了继续研究此数据集,我们将更仔细地检查各种汽车的制造商和模型,在从 R 转换为 Python 的同时重复上一章中的许多步骤。
准备
如果您已完成上一个食谱,则应具备继续操作所需要的一切。
怎么做...
以下步骤将引导我们完成调查:
- 让我们看一下汽车的品牌和型号如何向我们介绍一段时间内的燃油效率。 首先,让我们看一下在美国销售的汽车的制造商和型号的频率,重点是四缸汽车。 要选择 4 缸汽车,我们首先使
cylinders变量唯一,以查看可能的值是:
In [30]: pd.unique(vehicles_non_hybrid.cylinders)
...:
Out[30]: array([ 4., 12., 8., 6., 5., 10., 2., 3., 16., nan])
4.0和4均被列为唯一值; 这个事实应该引起您的怀疑。 记住,当我们导入数据时,熊猫警告我们几个变量是混合类型,其中一个变量是cylinders。
- 让我们将
cylinders变量转换为float,以便我们可以轻松地对数据帧进行子集化:
In [31]: vehicles_non_hybrid.cylinders = vehicles_non_hybrid.cylinders.astype('float')
...: pd.unique(vehicles_non_hybrid.cylinders)
...:
Out[31]: array([ 4., 12., 8., 6., 5., 10., 2., 3., 16., nan])
In [32]: vehicles_non_hybrid_4 = vehicles_non_hybrid[(vehicles_non_hybrid.cylinders == 4.0)]
- 现在,让我们看一下在可用的时间范围内拥有 4 缸汽车的品牌数量:
In [35]: grouped_by_year_4_cylinder = vehicles_non_hybrid_4.groupby(['year']).make.nunique()
...: fig = grouped_by_year_4_cylinder.plot()
...: fig.set_xlabel('Year')
...: fig.set_ylabel('Number of 4-Cylinder Makes')
...: print fig
请注意,在尝试绘制序列对象时,我们已从ggplot切换到matplotlib。 在 Python 中,用随机的import语句乱七八糟的代码被认为是不好的形式。 因此,我们将import语句移至 IPython Notebook 的顶部。 请记住,如果重新启动 IPython Notebook,请确保首先执行 Notebook 顶部的import语句,以便其余代码可以运行。 请参考下图:

我们可以在上图中看到,自 1980 年以来,可用 4 缸发动机的品牌数量有所下降。但是,请注意,该图可能会产生误导作用,因为我们不知道可用总数 每年的收入在同一时间段内发生了变化。
- 我们可以看看这项研究每年提供的品牌吗? 首先,我们希望找到本研究每年都使用的 4 缸发动机汽车制造清单。 为此,我们首先计算每个型号年份的唯一品牌清单:
In [36]: grouped_by_year_4_cylinder = vehicles_non_hybrid_4.groupby(['year'])
...: from functools import reduce
...:
In [37]: unique_makes = []
...: for name, group in grouped_by_year_4_cylinder:
...: unique_makes.append(set(pd.unique(group['make'])))
...:
...: unique_makes = reduce(set.intersection, unique_makes)
...: print(unique_makes)
...:
{'Nissan', 'Chevrolet', 'Chrysler', 'Toyota', 'Honda', 'Dodge', 'Volkswagen', 'Jeep', 'Ford', 'Subaru', 'Mitsubishi', 'Mazda'}
我们发现,在此期间,每年只有 12 家制造商生产 4 缸汽车。
- 现在,我们问这些汽车制造商的模型在燃油效率方面的表现如何。 为此,我们决定走很长的路。 首先,我们创建一个空列表,最终将由布尔值填充。 然后,我们使用
iterrows生成器遍历数据帧中的每一行,该生成器同时生成索引和行(我们选择对循环中的索引不执行任何操作)。 然后,我们测试当前行的构造是否在先前计算的unique_makes集合中,并将布尔值附加到Boolean_mask集合中。 循环完成后,我们将数据帧子集化为仅包含unique_makes集合中带有 make 的行:
In [38]: boolean_mask = []
...: for index, row in vehicles_non_hybrid_4.iterrows():
...: make = row['make']
...: boolean_mask.append(make in unique_makes)
...:
...: df_common_makes = vehicles_non_hybrid_4[boolean_mask]
...:
- 接下来,我们必须按
year和make对数据帧进行分组,然后计算每次分组的平均值:
In [39]: df_common_makes_grouped = df_common_makes.groupby(['year',
...: 'make']).agg(np.mean).reset_index()
- 最后,根据
ggplot,我们使用多面图显示了我们的工作结果:
In [40]: ggplot(aes(x='year', y='comb08'), data =
...: df_common_makes_grouped) + geom_line() + facet_wrap('make')
请参考下图:

工作原理...
在配方本身中详细说明了很多步骤,因此我们不会在这里浪费要点。 但是,有些事情当然值得指出。 首先,您是否在最后一个“拆分-应用-合并”步骤的末尾注意到了.reset_index()调用? 以下命令显示了这一点:
df_common_makes.groupby(['year','make']).agg(np.mean).reset_index()
当执行groupby步骤时,pandas 返回将行分组为索引而不是简单数据列的键。 在使用多个分组键的情况下,pandas 返回一个多级索引。 不幸的是,这不适用于ggplot,我们需要告诉[pandas]使用.reset_index()方法将这些索引视为数据列。
其次,我们使用for循环,在数据帧的行上进行迭代,以确定品牌是否在感兴趣的unique_makes列表中。 如果这段代码看起来有些冗长,那是因为。 Pandas 内置了大量功能,我们可以使用.isin()方法执行行选择,如以下命令所示:
test =
vehicles_non_hybrid_4[vehicles_non_hybrid_4['make'].isin(unique_makes
)]
如果您希望对数据框执行数据分析步骤,则很可能已经构建了一种方法来执行此操作。
从性能的角度来看,通过将循环附加到列表,我们在for循环中犯了一个非常明显的错误,因此每次迭代都增加了列表的大小。 为了加快速度,我们应该将列表预先分配为填充有错误布尔值的数据框中的行数大小。 预分配数组是一种非常通用的技术,可以加速大多数语言中的代码。 此技巧在matplotlib中特别强大。
为了执行我们用来识别每年数据中存在的所有制造商的集合交集,我们需要sets Python 程序包,该程序包是 Python 发行版的一部分。 同样,我们将这个 import 语句移到脚本的顶部,以遵循最佳的 Python 做法。
另请参见
七、使用社交图
在本章中,我们将介绍:
- 准备在 Python 中使用社交网络
- 导入网络
- 探索英雄网络中的子图
- 寻找牢固的联系
- 寻找关键人物
- 探索整个网络的特征
- 社交网络中的聚类和社区检测
- 可视化图形
- R 中的社交网络
简介
得益于 Facebook 和 Twitter 等社交网站,社交网络已成为现代生活的工具。 但是,社交网络本身并不新鲜。 这种网络的研究可以追溯到 20 世纪初,特别是在社会学和人类学领域。 正是由于它们在主流应用中的盛行,才将这些类型的研究转移到了数据科学领域。
事实证明,社交网络作为人类行为的模型非常有趣。 人类文明起源于部落社会,因此,邓巴的数字[a HTG1]假设在任何给定时间我们扩展的社交网络中只能有 150 个人。 网络。 潜在的社交网络无处不在,不仅存在于流行的 Web 2.0 应用中。 我们通过与各种网络的连接来管理生活,因此,我们生成了大量相关的丰富数据,可用于对自己和我们的关系做出预测。
就像我们将在本章中讨论的那样,网络以关系为中心的世界观。 通过利用现有的人与人关系数据结构(一个社交网络),我们可以使用聚类技术对大型网络进行分析,以发现社区,揭示对图的重要成员的作用的洞察力,甚至产生行为 通过关系推理进行预测。 这些分析具有从执法到选举预测以及建议到应用优化的大量实际应用。
这些分析的数学基础来自图论。 因此,本章中的分析技术将集中于图的基数,遍历和聚类。 为了介绍这些技术,我们将使用出色的 Python 图形库 NetworkX。 我们将在网络的各个级别进行一些分析,例如在个人级别进行成对比较,在组级别进行社区检测以及在网络级别进行内聚分析。 最后,我们将看一下如何使用各种工具来可视化和绘制图形和子图形。
了解图和网络
本章进行分析的基础来自图论-图的应用和性质的数学研究,其最初是出于对机会博弈的研究。 一般而言,这涉及对网络编码和图形特性的研究。 图论可以追溯到 1735 年 Euler 在柯尼斯堡七桥问题上的工作。但是,在最近的几十年中,社交网络的兴起影响了该学科,特别是计算机科学 图形数据结构和数据库。
让我们从争论的角度开始。 网络和图形之间有什么区别? 术语图可以用来暗示变量和函数的可视化表示,一组节点和边的数学概念或基于该概念的数据结构。 同样,术语网络有多个定义。 它可以是一个相互连接的系统,也可以是一种特殊类型的数学图。 因此,在这种情况下,无论是社交网络还是社交图,都是合适的,特别是在我们指的是数学概念和数据结构时。
图是网络的符号表示,它由一组顶点(节点)及其连接(关系或边)组成。 更正式地说,图可以定义为:G =(V,E),由有限的节点集(由 V(顶点)或 V(G)表示)和集合 E(边)或 E(G)组成的实体 的无序对{u,v},其中 u,v VV。下图所示的可视示例应为读者所熟悉:

图可以是有向的或无向的。 有向图具有有序关系。 无向图可以看作是双向有向图。 社交网络中的有向图往往具有方向性的语义关系,例如,朋友,其中安倍晋三可能与简成为朋友,但简可能不会互惠。 无向社交网络具有更一般的语义关系,例如“知道”。 任何有向图都可以轻松转换为更通用的无向图。 在这种情况下,邻接矩阵变得对称,即每个关系都是互惠的。
邻接矩阵是二维图形表示,其中第 i 个节点和第个节点连接时,每个单元格或元素(i,j)为1; 否则为0。 当然,这不是存储有关图形信息的最紧凑的方式。 即使大多数节点不与大多数其他节点共享一条边,也必须为每对节点存储一个字节。 但是,此表示在计算上是有效的,并用于许多图形算法。 考虑在该方案中可以将节点表示为其边缘的向量。 下图显示了具有四个节点的无向图的小邻接矩阵示例:

最后几个术语将有助于我们的讨论。 顶点的基数称为图的顺序,而边的基数称为大小。 在上图所示的图中,顺序为 7,大小为 10。如果两个节点共享一条边,则它们是相邻的;否则,两个节点相邻。 如果是这样,则它们也称为邻居。 顶点的邻域是该顶点连接到的所有顶点的集合。 顶点的度数是其邻域的大小,即与顶点共享一条边的节点数。
考虑到这一点,图形问题通常分为几类。 存在问题试图确定是否存在节点,路径或子图,尤其是在存在约束的情况下。 构造问题集中于在给定约束下给定一组节点和路径的图的构造。 枚举问题试图确定一组约束内的顶点和关系列表。 最后,优化问题决定了两个节点之间的最短路径。
准备在 Python 中使用社交网络
值得重复的 Python 的主要优势之一是可用于该语言的出色的预制软件包的数量。 对我们来说幸运的是,网络分析也不例外。 这个简短的食谱将引导您完成本章其余部分所需的库的安装。
准备
本章中的任务所需的外部库如下:
- 网络 X
- matplotlib
- python-Louvain
怎么做...
此时,我们将使用以下应熟悉的步骤来准备其余食谱:
- 打开一个新的终端或命令提示符,然后转到您的项目目录。
- 如果您使用的是虚拟环境,请激活您的虚拟环境并键入以下内容:
pip install networkx
如果您没有使用虚拟环境,则很可能需要使用sudo,如下所示:
sudo pip install networkx
- 现在,我们必须安装
python-louvain软件包:
pip install python-louvain
工作原理...
NetworkX 是一个维护良好的 Python 库,用于创建,操作和研究复杂网络的结构。 它的工具允许快速创建图形,并且该库还包含许多常见的图形算法。 特别是,NetworkX 补充了 Python 的科学计算套件 SciPy / NumPy,matplotlib 和 Graphviz,并且可以处理内存中 10M 个节点和 100M 个链接的图形。 NetworkX 应该成为每个数据科学家工具包的一部分。
NetworkX 和 Python 是进行社交网络分析的完美组合。 NetworkX 旨在处理大规模数据。 其中包含的核心算法是在极快的旧版代码上实现的。 图非常灵活(节点可以是任何可散列的类型),并且有大量的本机 IO 格式。 最后,使用 Python,您将能够访问或使用 Internet 上数据库中的大量数据源。
Python-louvain 是一个用于单一目的的小型 Python 库,它使用 Louvain 方法进行社区检测,如大型网络中社区的快速展开论文( Journal of Statistics Mechanics:理论与实验,2008(10))。 也可以使用 C ++ 实现。
还有更多...
尽管我们将在本章中专门使用 NetworkX,但还有许多值得一提的出色的替代社交网络分析库。 首先,可以使用 R,Python 和 C/C++ 使用内置在 C 中的基础工具对 igraph 进行编程和使用。 / C ++ 以提高性能。 第二个要看的库是 graph-tool。 在它的 Python 可用性之下,它也可以用 C ++ 实现,但具有利用 OpenMP 在多核计算机之间进行并行化的额外好处。
导入网络
我们将在本章中探讨的数据集很有趣。 这是 CescRosselló,Ricardo Alberich 和 Joe Miro 构建的 Marvel Universe 社会图数据集,作为他们对无序系统和神经网络的研究的一部分。 他们通过用出现的漫画来编辑角色来创建网络。 事实证明,该网络实际上是模仿现实世界的社交网络。 从那时起,使用这个著名的数据集(以及扩展名)进行了许多可视化以及其他混搭。 在本食谱中,我们将所需的数据导入到我们的 Python 环境中。
准备
安装完前面的配方中所需的库后,将需要使用本章提供的数据集。
怎么做...
执行以下步骤来导入数据:
- 为了使该图成为 NetworkX 图表示形式,请遍历数据集并为每个英雄对添加边(自动创建节点):
import networkx as nx
import unicodecsv as csv
def graph_from_csv(path):
graph = nx.Graph(name="Heroic Social Network")
with open(path, 'rU') as data: reader = csv.reader(data)
for row in reader:
graph.add_edge(*row)
return graph
每行都是(hero,hero)元组。 使用*row表示法,我们扩展了元组,以便函数定义实际上是graph.add_edge(hero, hero)。
- 数据集很大,重达 21 MB,需要一两秒钟才能加载到内存中。 您可以如下计算图形的大小和顺序:
>>> graph.order() # graph.number_of_nodes()
6426
>>> graph.size() # graph.number_of_edges()
167219
保持此功能方便; 我们将需要它来获取本章其余大部分的图形!
- 从中导出社交网络的备用数据集包括出现角色的漫画。 这种格式需要略有不同的图形生成机制:
def graph_from_gdf(path):
graph = nx.Graph(name="Characters in Comics")
with open(path, 'rU') as data:
reader = csv.reader(data)
for row in reader:
if 'nodedef' in row[0]:
handler = lambda row,G: G.add_node(row[0],
TYPE=row[1])
elif 'edgedef' in row[0]:
handler = lambda row,G: G.add_edge(*row)
else:
handler(row, graph)
return graph
在此制表符分隔值(TSV)文件中,在节点或边定义的行之前有标语nodedef或edgedef的标语。 当我们遍历每一行时,我们将根据是否看到横幅创建一个处理程序函数lambda。 然后,对于横幅下的每一行,我们都使用定义的处理程序,就像在本节中的节点或边一样。
- 此时,我们可以使用 NetworkX 的内置方法为图形计算一些快速信息:
>>> nx.info(graph)
Name: Heroic SociaL Network
Type: Graph
Number of nodes: 6426
Number of edges: 167219
Average degree: 52.0445
请注意,图表的名称是在我们实例化nx.Graph时添加的,HTG0 是一项可选功能,可让您更轻松地在代码中跟踪多个图表。
工作原理...
在任何数据科学项目中,数据导入都是一个挑战,图形数据可以采用多种格式。 对于此配方,数据很简单; 它是一个 TSV 文件,其中包含知道关系的英雄与英雄的联系。
还有一个备用数据集,通过包括它们的关系源(英雄一起出现的漫画)来扩展已知关系。 通过通过出现在关系中来扩展漫画对英雄网络,此扩展在英雄对英雄网络之间添加了额外的跃点。 这个扩展的网络可能使我们能够计算已知关系的强度。 例如,英雄一起出现的漫画越多,他们彼此之间可能就越了解。 有趣的是,这种类型的数据集已被证明对社区发现和关系聚类有效。
graph_from_gdf函数确定我们是在读取边还是在读取节点,并且当看到名为nodedef或edgedef的行标语时,通过lambda函数实现一个新的处理程序,它可以适当地处理 TSV 文件的每一行。 ,指示其下方的行分别是节点或边。 此功能还为我们提供了创建属性图的机会。
属性图通过在节点和边上甚至可能在图本身上包含键/值对扩展了我们当前的图定义。 属性图更具表现力,并且它们是许多图数据库的基础,因为它们可以按每个节点和每个关系保存更多信息(因此可以替代传统的关系数据库)。 NetworkX 还允许您为节点和边指定其他属性。
注意
注意add_node方法; 其他关键字参数另存为属性。 在这种情况下,我们设置TYPE属性来确定该节点是漫画书还是英雄。 NetworkX 还允许使用set_node_attributes (G, name, attributes)和get_node_attributes(G, name)批量设置和检索节点上的属性,这将返回一个字典,该字典将节点映射到请求的属性(或将属性保存到节点组)。
探索英雄网络中的子图
上一食谱导入网络中的图表对于我们来说太大了,无法让人体会到个人水平的变化,尽管我们很快就会看到一些分析,这些分析告诉我们有关人口的有趣信息 和社区。 但是,为了让我们立即看到一些有趣的东西,让我们提取一个子图来玩。 特别是,我们将在数据集中为特定英雄收集一个子图。 当以一个人或一个演员为焦点生成一个子图时,它被称为自我网络,实际上,自我网络的程度可能是一个人的自我价值的量度!
准备
只要您完成了上一个食谱,您就会为这个食谱做好准备。
怎么做...
以下步骤将引导您从大型数据集中提取子图并可视化自我网络:
- 每个社交网络都具有与节点一样多的自我。 自我的邻居被称为“改变者”。 自我子图的定义受 n 步邻域的约束,n 步邻域定义了远离自我的跳数,以包括在子图中。 NetworkX 提供了一种非常简单的机制来提取自我图,如以下命令所示:
>>> ego = nx.ego_graph(graph, actor, 1)
此函数返回参与者节点的所有邻居的子图,该子图的最大路径长度由第三个参数指定。
- 要绘制自我网络图,我们使用以下函数:
def draw_ego_graph(graph, character, hops=1):
"""
Expecting a graph_from_gdf
"""
# Get the Ego Graph and Position
ego = nx.ego_graph(graph, character, hops)
pos = nx.spring_layout(ego)
plt.figure(figsize=(12,12))
plt.axis('off')
# Coloration and Configuration
ego.node[character]["TYPE"] = "center"
valmap = { "comic": 0.25, "hero": 0.54, "center": 0.87 }
types = nx.get_node_attributes(ego, "TYPE")
values = [valmap.get(types[node], 0.25) for node in
ego.nodes()]
# Draw
nx.draw_networkx_edges(ego, pos, alpha=0.4)
nx.draw_networkx_nodes(ego, pos,
node_size=80,
node_color=values,
cmap=plt.cm.hot, with_labels=False)
plt.show()
- 让我们看一下
LONGBOW/AMELIA GREER的自我网络,从单跳网络开始:
>>> graph = graph_from_gdf('comic-hero-network.gdf'))
>>> draw_ego_graph(graph, "LONGBOW/AMELIA GREER")
前面的命令将为您提供下图:

上图显示了此单跳自我网络,该网络是从扩展的漫画到英雄的社交图谱得出的。 由于存在两种不同类型的节点,因此我们在视觉上对它们进行了不同的着色。 橙色节点是角色,蓝色节点是漫画。 自我节点LONGBOW/AMELIA GREER她自己是白色的。
- 让我们为同一角色创建一个两跳自我网络:
>>> draw_ego_graph(graph, "LONGBOW/AMELIA GREER", 2)
前面的命令将为您提供以下图形:

工作原理...
阿米莉亚·格里尔(又称长弓)是雇佣军特种作战部队“ Har”的一部分。 她出现在两部 Marvel 漫画中,尤其是 1990 年 5 月的 Uncanny X-Men Vol 1#261。她的自我网络非常紧密(主要包含来自“猎兔犬”的成员),如您所见。 前一跳网络图。
前面的两跳网络大大扩展了网络的容量。 黑色节点代表到下一个漫画人物社区的第二跳。 集群很明显,即使在这个小的自我网络中也是如此。 尽管如此,即使是单跳网络也可以说出很多关于团体成员和演员的重要性的信息。 我们将在本章稍后讨论如何构建这样的图。
还有更多...
自我可以或不能成为网络的一部分。 实际上,由于子图是根据自我的邻域生成的,因此对自我的成员资格测试并不重要! 相反,去除自我可以显示网络的结构配置。 考虑下图所示的社交网络:

在上图中,自我是图表的一部分; 网络的结构似乎是统一的和有凝聚力的。
如您所见,没有自我,隔离变得很重要,明显的离群值也是如此。 跨自我聚合这些隔离是发现组成员身份和社区的方式。 我们将在下一部分中对此进行更多探讨。
找到牢固的联系
目前,我们的英雄网络只是测量两个字符是否已连接。 这种计算很简单; 它们会一起出现在同一本漫画书中吗? 我们假设在漫画的短暂时间空间中,甚至浮雕也意味着角色之间已经相互作用。 但是,这并不能告诉我们与特定角色最重要的关系是谁。
为了确定自我网络中最重要的人(或确定两个参与者之间的相对亲和力),我们需要确定边缘权重。 由于边缘代表互动,从属关系或社会关系,因此权重确定两个演员之间的距离,相对于具有相似关系的其他演员。 社交网络中边缘权重的代理包括:
- 频率,例如,两个参与者之间的交流频率
- 互惠,例如,关系是否互惠
- 类型或属性,例如,已婚演员的关系要比大学室友更强
- 邻居的结构,例如,共同朋友的数量
在我们的英勇社交图表中,我们将使用其中一对角色同时出现的漫画数量来代替它们的联系强度。 这似乎是有道理的; 如果一个角色是与英雄出现在同一漫画中的恶棍,那么他们之间的关系就是克星,那不是简单的主角/对立关系! 另一个例子是两个英雄经常一起出现。 他们可能是英雄团队的一部分(例如,复仇者联盟),或者是同伴关系(例如,Bucky 与美国队长)。
准备
如果您完成了前面的食谱,则应该准备解决这个问题。
怎么做...
以下步骤将引导我们逐步找到网络中的牢固纽带:
- 为了计算关系,我们将从漫画英雄网络数据集中重新创建英雄网络图。 由于这表示整个图形计算(例如,我们将遍历图形中的每个节点),因此我们需要使用内存安全的迭代器并将中间数据保存到磁盘。 这是完整的代码; 我们将逐行介绍它,如下所示:
def transform_to_weighted_heros(comics):
# Create new graph to fill in
heros = nx.Graph(name="Weighted Heroic Social Network")
# Iterate through all the nodes and their properties
for node, data in graph.nodes(data=True):
# We don't care about comics, only heros
if data['TYPE'] == 'comic': continue
# Add the hero and their properties (this will also update data)
heros.add_node(node, **data)
# Find all the heros connected via the comic books
for comic in graph[node]:
for alter in graph[comic]:
# Skip the hero that we're on
if alter == node: continue
# Setup the default edge
if alter not in heros[node]:
heros.add_edge(node, alter, weight=0.0,
label="knows")
# The weight of the hero is the fraction of
connections / 2
heros[node][alter]["weight"] += 1.0 /
(graph.degree(comic) *2)
return heros
- 现在让我们看看 Longbow 的社交加权图:
def draw_weighted_ego_graph(graph, character, hops=1):
# Graph and Position
ego = nx.ego_graph(graph, character, hops)
pos = nx.spring_layout(ego)
plt.figure(figsize=(12,12))
plt.axis('off')
# Coloration and Configuration
ego.node[character]["TYPE"] = "center"
valmap = { "hero": 0.0, "center": 1.0 }
types = nx.get_node_attributes(ego, "TYPE")
values = [valmap.get(types[node], 0.25) for node in
ego.nodes()]
char_edges = ego.edges(data=True, nbunch=[character,])
nonchar_edges = ego.edges(nbunch=[n for n in ego.nodes()
if n != character])
elarge=[(u,v) for (u,v,d) in char_edges if d['weight'] >=0.12]
esmall=[(u,v) for (u,v,d) in char_edges if d['weight'] < 0.12]
print set([d['weight'] for (u,v,d) in char_edges])
# Draw
nx.draw_networkx_nodes(ego, pos,
node_size=200,
node_color=values,
cmap=plt.cm.Paired,
with_labels=False)
nx.draw_networkx_edges(ego,pos,edgelist=elarge,
width=1.5, edge_color='b')
nx.draw_networkx_edges(ego,pos,edgelist=esmall,
width=1,alpha=0.5,
edge_color='b',style='dashed')
nx.draw_networkx_edges(ego,pos,edgelist=nonchar_edges,
width=0.5,alpha=0.2,style='dashed')
plt.show()
前面的命令将为您提供以下图形:

如您所见,Longbow 有两条牢固的纽带(用深蓝色的粗线表示),这是有道理的,因为她在两本漫画书中只有两个其他角色。 与她直接相关的其他角色具有淡蓝色的虚线,表示她与这些角色之间的联系较弱。
请注意,所有字符都与它们在自我网络中共享联系的其他字符相连(用浅灰色虚线表示)。 我们已经开始看到聚类了,因为来自不同漫画书的角色已经移到了 Longbow 红色节点的任一侧。 与两个漫画中的人物都具有亲和力的人物位于中间。
工作原理...
transform_to_weighted_heros功能是此配方的关键,我们在此处逐行对其进行说明。 首先,我们创建一个空图,在其中添加转换。 请注意,在 NetworkX 中,有诸如create_empty_graph和Graph.subgraph之类的功能; 但是,前者将不会传输我们需要的数据,而后者将传输过多的数据。 我们将遍历原始图中的每个节点,跳过漫画书。 请注意,我们将使用heros.add_node方法添加英雄两次,因为每次添加一条边时,都会创建一个节点(如果尚不存在)。 但是,为了确保获得所有数据,我们为每个英雄调用add_node,并且如果节点中已经存在该节点,则它将简单地更新该节点的属性。
接下来,我们将通过漫画之间的联系将英雄联系在一起。 我们将收集与当前英雄所在的同一本漫画书相关的所有英雄(跳过当前英雄以防止自我循环)。 如果没有边缘,我们将在添加权重计算之前创建一个默认边缘。 我们要分配的权重与漫画书中的字符数有关。 可以认为,如果漫画书中包含更多字符,它们之间的联系就会更加松散。 因此,我们将权重计算为漫画程度的倒数,除以二。 由于这是一个无向图,因此必须除以 2,否则我们将使所有权重加倍!
还有更多...
加权边缘对于能够在图形上进行预测至关重要,部分原因是它们允许对路径进行排名,但主要是因为它们反映了社交网络的底层语义关联。 进行预测依赖于建立同构的关系-这是具有相似兴趣的人聚集在一起的趋势,并且导致形成称为簇的同质群体。 亲密关系可以强也可以弱。 例如,一起住在同一所学校的人们比住在同一座城市的人们之间的联系更牢固。
传递性是允许我们进行预测的边的属性。 简而言之,它提供了执行三元闭合的能力。 如果节点 A 和 B 已连接,并且 A 和 C 也已连接,则 B 和 C 也可能也已连接。 及物性是牢固关系存在的证据,但这不是必要条件或充分条件,它可能不适用于脆弱关系。
另一方面,网桥是连接各组节点的边缘。 桥促进集群通信,通常是弱联系或异性关系的产物。 了解和理解与您的特定社交图表相关的这些属性对于解锁关键见解以及能够对图表可能发生的变化做出未来的预测至关重要。
寻找关键参与者
在先前的食谱中发现牢固的联系,我们开始探索自我网络和社交网络中个人之间的牢固联系。 我们开始看到与其他演员紧密联系的演员创建了以自己为中心的集群。 这就引出了一个显而易见的问题:图中的关键人物是谁,他们具有什么样的吸引力? 我们将研究几种方法来确定节点的重要性或其中心性,以尝试发现度中心性,中间性中心性,邻近性中心性和特征向量中心性。
准备
如果您完成了前面的食谱,则可以开始这一食谱了。
怎么做...
以下步骤将确定该漫画人物网络中的主要角色:
- 为了找到英雄网络中的前十个节点,我们计算节点的度并对它们进行排序:
import operator
>>> degrees = sorted(graph.degree().items(), key=operator.itemgetter(1), reverse=True)
>>> for node in degrees: print node
- 另外,我们计算图中一个节点所连接的节点的百分比; NetworkX 为我们提供了有用的功能
degree_centrality。 在进行此操作时,我们也可以将其设置为节点的属性,以便于查找:
>>> centrality = nx.degree_centrality(graph)
>>> nx.set_node_attribues(graph, 'centrality', centrality)
>>> degrees = sorted(centrality.items(), key=itemgetter(1), reverse=True)
>>> for item in degrees[0:10]: print "%s: %0.3f" % item
前面的命令为我们提供了数据集中的十大关键角色,我认为很明显,它们是 Marvel 宇宙中最有影响力的角色:
1\. CAPTAIN AMERICA: 0.297 (1908)
2\. SPIDER-MAN/PETER PAR: 0.270 (1737)
3\. IRON MAN/TONY STARK : 0.237 (1522)
4\. THING/BENJAMIN J. GR: 0.220 (1416)
5\. MR. FANTASTIC/REED R: 0.215 (1379)
6\. WOLVERINE/LOGAN : 0.213 (1371)
7\. HUMAN TORCH/JOHNNY S: 0.212 (1361)
8\. SCARLET WITCH/WANDA : 0.206 (1325)
9\. THOR/DR. DONALD BLAK: 0.201 (1289)
10\. BEAST/HENRY &HANK& P: 0.197 (1267)
考虑到平均程度为52.045,这些字符在具有大量连接的情况下具有巨大的影响力! 在进行此操作时,我们还可以创建图形的连通性的直方图。 在向您展示直方图之前,请快速注意一下:NetworkX 确实有一个函数degree_histogram,它将返回度数的列表。 但是,在这种情况下,列表的索引是度值,而 bin 宽度是1; 这意味着列表的长度可能非常大(Order(len(edges)))。 使用graph.degree().values()的效率更高,尤其是对于社交网络图,我们将在后面看到。
- 有了下面的小片段,您现在应该可以看出,就影响力而言,我们的主要角色与正常角色相距多远:
>>> import matplotlib.pyplot as plt
>>> plt.hist(graph.degree().values(), bins=500)
>>> plt.title("Connectedness of Marvel Characters")
>>> plt.xlabel("Degree")
>>> plt.ylabel("Frequency")
>>> plt.show()
上面的代码片段将为您提供以下图形:

- 实际上,您可以过滤出节点度大于 500 的字符数,即最高的百分之一。 此过滤器返回 98.8%的字符:
>>> filter(lambda v: v < 500, graph.degree().values())
如果这样做,曲线将变得更加明显:
>>> import matplotlib.pyplot as plt
>>> plt.hist(graph.degree().values(), bins=500) >>> plt.title("Connectedness of Marvel Characters")
>>> plt.xlabel("Degree")
>>> plt.ylabel("Frequency")
前面的命令将为您提供以下输出:

- 为了计算其他集中度指标,我们将使用 NetworkX 内置函数。 要计算中间性中心度,请使用以下函数:
>>> centrality = nx.betweenness_centrality(graph)
>>> normalied = nx.betweenness_centrality(graph, normalized=True)
>>> weighted = nx.betweenness_centrality(graph, weight="weight")
前面的功能使您可以计算中间性中心度,这将在后面讨论,并且可以归一化或加权。
- 要计算紧密度中心度,我们可以使用以下函数,它们类似于中间度中心度:
>>> centrality = nx.closeness_centrality(graph)
>>> normalied = nx.closeness_centrality(graph, normalized=True)
>>> weighted = nx.closeness_centrality(graph, distance="weight")
- 最后,要计算特征向量的中心性,您可以使用 NetworkX 进行两种选择:
>>> centrality = nx.eigenvector_centality(graph)
>>> centrality = nx.eigenvector_centrality_numpy(graph)
- 为了轻松地在图表上浏览这些中心度指标,让我们创建一个函数,该函数通常根据中心度指标打印前 10 个节点:
def nbest_centrality(graph, metric, n=10, attribute="centrality", **kwargs):
centrality = metric(graph, **kwargs)
nx.set_node_attributes(graph, attribute, centrality)
degrees = sorted(centrality.items(), key=itemgetter(1), reverse=True)
for idx, item in enumerate(degrees[0:n]):
item = (idx+1,) + item
print "%i. %s: %0.3f" % item
- 现在,我们可以简单地将此函数与我们的中心度度量结合使用,以根据它们的中心度找到
nbest(默认为前 10 个)节点。 用法如下:
>>> nbest_centrality(graph, nx.degree_centrality)
>>> nbest_centrality(graph, nx.betweenness_centrality, normalized=True)
>>> nbest_centrality(graph, nx.closeness_centrality)
>>> nbest_centrality(graph, nx.eigenvector_centrality_numpy)
工作原理...
显然,Marvel Universe 中的次要字符要多得多,还有 100 个左右的主要字符。 有趣的是,这与现实世界相比非常好! 在现实世界的社交图中,具有更多联系且几乎没有联系的演员,与联系紧密的顶级梯队角色也相对较少。 我们很直观地知道这一点,我们将联系比例最高的百分之一的演员称为名人。
实际上,名人是极端的离群人,他们在社交图谱上施加如此强烈的影响,因此我们倾向于称其为超级节点。 超级节点具有以下特性:对于足够远的跃点距离,图的所有遍历都将不可避免地找到通过超级节点的最短路径。 这对于计算可能是非常糟糕的。 甚至更早的 Longbow 图也经历了巨大的计算滞后,由于 Longbow 连接到了两个超级节点:金刚狼和让·格雷,我们在绘制该图时也遇到了困难。 虽然这不仅证明了凯文·培根(Kevin Bacon)的六个分离度,但处理超级节点也是图计算的重要组成部分。
另一方面,我们可以为我们的社交网络计算邓巴的数字。 Dunbar 不仅针对人类社区,而且针对灵长类动物,确定并测量了新皮层体积与社会群体规模之间的相关性。 为了计算此数字,由于此数据集中的强烈左手偏斜,我们不能简单地计算所有节点的平均度。 让我们将均值,众数和中位数作为对 Dunbar 数的推论进行比较。 稍后,我们将研究如何使用加权关系来进一步建立牢固的联系:
>>> import numpy as np
>>> import scipy.stats as st
>>> data = np.array(graph.degree().values())
>>> np.mean(data)
52.0445066916
>>> st.mode(data)
(array([ 11.]), array([ 254.]))
>>> np.median(data)
20.0
邓巴的漫画英雄人数似乎比典型的社交图要低得多,但与自然比例图成正比(考虑到您在高中时的社交图可能小于成人的社交图)。 英勇的邓巴(Dunbar)的电话号码似乎在 11 到 20 之间。 难道是因为外来或突变的新皮层体积小于人类的体积? 还是因为我们的英雄因能力而自然与世隔绝,使他们与众不同?
还有更多...
查找图的关键参与者的最常见(也许是最简单)的技术是测量每个顶点的程度。 程度是确定节点连接方式的信号,可以是影响力或受欢迎度的隐喻。 至少,连接最紧密的节点是那些以最快的速度传播信息或对其社区产生最大影响的节点。 程度的度量往往会受到稀释的影响,并受益于统计技术来规范化数据集。
中间性中心
路径是开始节点和结束节点之间的一系列节点,其中没有节点在路径上出现两次,并由所包含的边数(也称为跃点)来衡量。 对于两个给定节点,最有趣的计算路径是最短路径,例如,到达另一个节点所需的最小边数,这也称为节点距离。
请注意,路径的长度可以为 0,即从节点到其自身的距离。 以下图为例:

D与F之间的最短距离是路径 {D,C,E,F},该距离为三。 另一方面,从A到E的最短路径是两条路径 {A,C,E} 和 {A,B, E},它们的路径长度为(以紫色和红色突出显示)。
找到两个节点之间的最短路径会在上一节中提出一个问题。 如果经常遍历关键节点以找到最短路径,是否有基于最短路径的中心度度量? 答案是肯定的。 中间性中心性标识比其他节点更可能位于最短路径中的节点。 这不仅在发现社交图中的优点时非常有用,而且在发现删除中心节点时也会被切断的薄弱点非常有用。
给定节点的中间性中心度的计算如下。 对于V节点,中间性中心定义为通过 v 的所有最短路径对的分数之和。中间性中心性也可以针对图中的节点数进行归一化 ,或加权以接受边缘权重:
>>> centrality = nx.betweeenness_centrality(graph)
>>> normalied = nx.betweenness_centrality(graph, normalized=True)
>>> weighted = nx.betweenness_centrality(graph, weight="weight")
前面的命令将为您提供以下输出:

当为我们的小示例图计算中间性中心度时,B和C具有良好的中心度得分(分别为 0.1 和 0.433)。 这是因为它们位于网络的很大一部分中。 但是E的中心得分为 0.6,这是因为E连接了网络的两个不同部分(G和F)。
和以前一样,我们可以检查英雄图的中间性,并找到具有最高中间性的英雄:
注意
请注意,此计算非常昂贵,可能会在您的计算机上花费很长时间!
>>> nbest_centrality(graph, nx.betweenness_centrality, normalized=True)
以下是输出:
1\. SPIDER-MAN/PETER PAR: 0.074
2\. CAPTAIN AMERICA: 0.057
3\. IRON MAN/TONY STARK: 0.037
4\. WOLVERINE/LOGAN: 0.036
5\. HAVOK/ALEX SUMMERS: 0.036
6\. DR. STRANGE/STEPHEN: 0.029
7\. THING/BENJAMIN J. GR: 0.025
8\. HAWK: 0.025
9\. HULK/DR. ROBERT BRUC: 0.024
10\. MR. FANTASTIC/REED R: 0.024
与中心度结果相比,这些数字有很大的不同。 Spider-Man和Captain America在第一和第二位切换了位置,但仍与Iron Man共享前三名。 Wolverine已提升至第四位,Mr. Fantastic和 The Thing 已被降级。 然而,真正令人感兴趣的是Hawk,Hulk,Dr. Strange和Havok的新出现,取代了野兽,雷神,猩红色的女巫和人类的火炬,这些人虽然很受欢迎 不能将字符链接在一起!
紧密度中心
另一个中心度度量(紧密度)是对特定节点 v 的出站路径进行统计分析。从 v 到达网络中任何其他节点的平均跳数是多少? 可以简单地计算为到图中所有其他节点的平均距离的倒数,可以将其标准化为 n-1 / size(G)-1,其中n为 如果图中的所有节点都已连接,则该邻域中的所有节点都将连接。 倒数可确保距离较近(例如,跳数较少)的节点得分更高,例如,与其他中心性得分相比,距离节点更近:
>>> centrality = nx.closeness_centrality(graph)
>>> normalied = nx.closeness_centrality(graph, normalized=True)
>>> weighted = nx.closeness_centrality(graph, distance="weight")
同样,当我们在英雄社交网络上运行此指标时,您应该发现它需要一段时间才能运行; 但是,如果使用归一化方法,则可以大大加快该过程:
>>> nbest_centrality(graph, nx.closeness_centrality)
1\. CAPTAIN AMERICA: 0.584
2\. SPIDER-MAN/PETER PAR: 0.574
3\. IRON MAN/TONY STARK : 0.561
4\. THING/BENJAMIN J. GR: 0.558
5\. MR. FANTASTIC/REED R: 0.556
6\. WOLVERINE/LOGAN : 0.555
7\. HUMAN TORCH/JOHNNY S: 0.555
8\. SCARLET WITCH/WANDA : 0.552
9\. THOR/DR. DONALD BLAK: 0.551
10\. BEAST/HENRY &HANK& P: 0.549
再次,我们返回到原始列表,该列表是通过学位中心获得的。 在这种情况下,凯文·培根(Kevin Bacon)的规则适用。 这些非常受欢迎的超级节点名人将具有最大的影响力,也就是说,他们能够在最快的时间内到达所有其他节点。 但是,对于较小的图表,情况已经发生了变化:

在这里,我们看到C和E是最接近中心的节点; 它们可以平等地到达所有其他节点。B比C和E距离近,但效果很好,而A由于具有两个连接,所以性能稍好 比D,G或F能够访问网络中的所有其他节点。
特征向量中心性
节点的特征向量中心度 v 与相邻节点的中心度分数之和成正比。 例如,与您联系的人越重要,您就越重要。 这种集中性度量非常有趣,因为具有少量极具影响力的联系人的演员可能会比具有更多平庸联系人的演员更胜一筹。 对于我们的社交网络,希望它将使我们深入了解英雄团队的名人结构,并了解谁真正将社交图谱整合在一起。
要计算特征向量中心性,请计算图的成对邻接矩阵特征分解的 argmax。 本征向量中的i元素给出了i节点的中心。 如果您熟悉 Google 的 PageRank,那么听起来应该很熟悉,实际上,PageRank 是一种经过改进的特征向量中心性度量,其中 Google 不用针对邻接矩阵进行计算,而是使用概率并计算整个矩阵的本征分解。 随机矩阵。
邻接矩阵是二维矩阵,其中每个节点的向量都是一个标志,指示它和另一个节点是否共享一条边。 无向图总是具有对称的邻接矩阵,而有向图则可能更复杂。 对于本节中的简单图形示例,这是邻接矩阵:

但是,我们不会实现用于计算特征向量中心性的算法,而是将依赖于 NetworkX 中已经内置的许多图形算法。 在这种情况下,我们有两种选择来计算特征向量中心度:
>>> centrality = nx.eigenvector_centality(graph)
>>> centrality = nx.eigenvector_centrality_numpy(graph)
首选方法是使用幂方法来找到特征向量,并且只会运行到预设的最大迭代次数,无法保证收敛。 第二种选择将使用 NumPy 特征值求解器。 您应尽可能使用第二种算法,以确保获得完整的结果。 您可能会问为什么要选择? NumPy 版本将继续运行直到收敛为止,这意味着它有可能因输入数据而卡住。 也可以制作 NumPy 版本以适合该解决方案。 但是,使用幂法的第一个原因是速度。 由于固定的迭代次数,幂方法通常比 NumPy 方法更快地求解特征值。 但是,求解器的这些属性都完全取决于图形(输入数据)的大小。 对于较大的图,您可能会被迫使用幂法,因为其他任何事情都将变得棘手。
注意,还有相应的pagerank和pagerank_numpy模块功能可以计算这些分数。 让我们看看我们的英雄是如何做到的:
>>> nbest_centrality(graph, nx.eigenvector_centrality_numpy)
1\. CAPTAIN AMERICA: 0.117
2\. IRON MAN/TONY STARK: 0.103
3\. SCARLET WITCH/WANDA: 0.101
4\. THING/BENJAMIN J. GR: 0.101
5\. SPIDER-MAN/PETER PAR: 0.100
6\. MR. FANTASTIC/REED R: 0.100
7\. VISION: 0.099
8\. HUMAN TORCH/JOHNNY S: 0.099
9\. WOLVERINE/LOGAN: 0.098
10\. BEAST/HENRY &HANK& P: 0.096
再一次,我们的前 10 名名单中有一些剧变! Captain America再次名列榜首,但Spider-Man输了不止一个地方,跌落了榜单的一半。 Thing,Scarlet Witch和Iron Man在列表中上移,并且令人惊讶的是,新演员Vision在列表中移动。
现在,较小的图对节点进行了更强的排名:

现在,B和C是排名最高的节点。E的排名也很高,但没有以前那么重要,A比以前更重要。 此外,F,D和G的中心度得分均已超过零,这在确定阈值时可能很重要。
确定中心性算法
那么,您应该使用哪种集中度度量? 嗯,这取决于您要寻找的内容,因为每个中心化机制都旨在处理社交网络的不同功能。 以下是一些中心性度量:
- 程度:这是衡量受欢迎程度的方法,它在确定可以快速将信息传播到本地区域(例如邻居)的节点时很有用。 想想名人; 这些是可以直接吸引许多人的节点。
- 中间度:这表明哪些节点可能是信息的路径,并且可以用来确定如果删除该节点,图形将在何处破裂。 它还用于显示通往网络中其他群集或组的直接路径。
- 接近度:这是一种覆盖范围的度量,即信息将从该特定节点传播到所有其他节点的速度。 具有最紧密中央关系的节点在广播通信期间享有较短的持续时间。
- 特征向量:这是相关影响的量度。 谁最接近图中的最重要人物? 这可以用来显示幕后的力量,也可以用来显示超越人气的相对影响力。
对于许多分析,所有亲密性度量都可用于单个社交网络实现关键结果!
探索整个网络的特征
在下一组食谱中,我们将描述整个社交网络的特征,而不是从单个参与者的角度。 通常,此任务仅次于感受最重要的节点,但这是鸡和蛋的问题。 确定分析和拆分整个图形的技术可以通过关键参与者的分析来告知,反之亦然。
准备
如果您完成了前面的食谱,则可以继续进行此食谱了。
怎么做...
以下步骤将引导我们完成对整个图形级别的图形特征的首次探索:
- 让我们计算整个网络和自我图的密度:
>>> nx.density(graph)
0.00810031232554
>>> ego = nx.ego_graph(graph, "LONGBOW/AMELIA GREER")
>>> nx.density(ego)
0.721014492754
如您所见,我们的英勇社交网络并不十分密集。 总体来说不是很挑剔。 但是,Longbow 的社交网络非常密集且极富挑战性。 通常来说,密度是用来比较整个社交网络的子图(例如自我图),而不是用来作为特定图自身行为的模型。
- 还可以根据距离(两个节点之间的最短路径)来分析图。 图中最长的距离称为社交图的直径,它表示沿图的最长信息流。 通常,密度较低(稀疏)的社交网络的直径大于密度较高的社交网络的直径。 此外,平均距离是一个有趣的指标,因为它可以为您提供有关节点之间的距离的信息:
>>> for subgraph in nx.connected_component_subgraphs(graph):
... print nx.diameter(subgraph)
... print nx.average_shortest_path_length(subgraph)
diameter: 5
average distance: 2.638
请注意,我们的英雄社交图没有完全连接,有一些孤立的子图,因此,我们使用nx.connected_component_subgraphs生成器捕获每个子图。 您可以测试社交图是否与nx.is_connected(G)连接,并可以通过nx.number_connected_components确定组件数。 在英雄社会图中,有四个组成部分,但只有两个组成部分具有大量节点。
- 最后,我们可以计算网络的互惠性,即,往复关系的数量(例如,如果存在双向链接)与社交网络中关系总数的比率。 当前,没有内置的 NetworkX 方法来执行此计算。 但是,使用
Graph的NetworkX.DiGraph子类,此方法将起作用:
>>> unigraph = digraph.to_undirected()
>>> return len(unigraph.edges()) / len(digraph.edges())
to_undirected方法中的倒数标志可确保仅保留在两个方向上都出现的边缘。
此方法仅适用于有向图。 不幸的是,由于我们使用已知的关系,因此我们的英雄网络是完全互惠的,因此互惠为 1.00。
工作原理...
在本食谱中,我们研究了三种不同的图形特征。 网络的密度是网络中边缘的数量与网络中可能边缘的总数之比。 对于无向图,n顶点的图的可能边数为n(n-1)/2(除去有向图的除法)。 完美连接的网络(每个节点与其他节点共享一条边)的密度为 1,通常称为集团。
还可以根据距离(两个节点之间的最短路径)来分析图。 图中最长的距离称为社交图的直径,它表示沿图的最长信息流。 通常,密度较低(稀疏)的社交网络的直径大于密度较高的社交网络的直径。 此外,平均距离是一个有趣的指标,因为它可以为您提供有关节点彼此之间有多近的信息。
我们将讨论的最后一个社交网络衡量标准是互惠性。 这是往复关系(例如,存在双向链接)的关系数与社交网络中关系总数的比率。 这仅对有向图有意义。 例如,Twitter 社交网络是有向图。 您可以关注其他人,但这并不一定意味着他们也会关注您。 由于我们的英雄社交网络在语义上是无方向性的,因此我们无法执行此计算。
社交网络中的聚类和社区检测
图表现出聚类行为,而社区的识别是社交网络中的一项重要任务。 节点的聚类系数是节点邻域中的三元闭合数(闭合的三元组)。 这是传递性的表达。 具有较高可传递性的节点具有较高的子密度,如果完全封闭,则会形成可以识别为社区的集团。 在本食谱中,我们将研究社交网络中的聚类和社区检测。
准备
您将再次需要 NetworkX,并且在本章中第一次需要 python-louvain 库。
怎么做...
这些步骤将指导您检测社交网络中的社区:
- 让我们实际进入一些集群。
python-louvain库使用 NetworkX 通过louvain方法执行社区检测。 这是一个小型的内置社交网络上的集群分区的简单示例:
G = nx.karate_club_graph()
#first compute the best partition
partition = community.best_partition(G)
#drawing
pos = nx.spring_layout(G)
plt.figure(figsize=(12,12))
plt.axis('off')
nx.draw_networkx_nodes(G, pos, node_size=200, cmap=plt.cm.RdYlBu, node_color=partition.values())
nx.draw_networkx_edges(G,pos, alpha=0.5)
plt.savefig("figure/karate_communities.png")
以下是带有不同颜色的灰色和/或彩色阴影的图形,代表不同的分区:

这很整齐! 我们可以看到有黄色,浅蓝色和深红色的集团,但是深蓝色是相当同质的。 稍后,我们将详细讨论如何使用 matplotlib 创建图形可视化。
- 要对漫画人物进行分区,我们将其分区添加到它们的每个节点; 然后,我们将查看每个分区的相对大小:
>>> graph = graph_from_csv(HERO_NETWORK)
>>> partition = community.best_partition(graph)
>>> print "%i partitions" % len(set(partition.values()))
25 partitions
>>> nx.set_node_attributes(graph, 'partition', partition)
如您所见,louvain方法发现了 25 个没有我们社交图谱的社区。
- 要检查每个社区的相对规模,直方图视图可能会有所帮助。 要创建直方图,请在文件中添加以下功能:
import matplotlib.pyplot as plt
def communities_histogram(graph):
graph, partition = detect_communities(graph)
numbins = len(partition.values())
plt.hist(partition.values(), bins=numbins), color="#0f6dbc")
plt.title("Size of Marvel Communities")
plt.xlabel("Community")
plt.ylabel("Nodes")
plt.show()
这将产生下图:

有三个主要社区,每个社区包含上千个节点。 但是,我们也有大约 400 名演员组成的八个中型社区。 其他社区要小得多,但是 Marvel 社交图谱确实确实是现实世界和小世界图谱,就像人类文化中观察到的自然社交网络一样!
- 可视化 Marvel 社区规模的另一种基于区域的方法是使用气泡图。 每个圆圈的面积代表每个社区的大小。 如下所示的图通常用于将大型图折叠为基于社区的子图。 要创建此代码,请在文件中添加以下功能:
def communities_bubblechart(graph):
graph, partition = detect_communities(graph)
parts = defaultdict(int)
for part in partition.values():
parts[part] += 1
bubbles = nx.Graph()
for part in parts.items():
bubbles.add_node(part[0], size=part[1])
pos = nx.random_layout(bubbles)
plt.figure(figsize=(12,12))
plt.axis('off')
nx.draw_networkx_nodes(bubbles, pos,
alpha=0.6, node_size=map(lambda x: x*6, parts.
values()),
node_color=[random.random() for x in parts.values()],
cmap=plt.cm.RdYlBu)
plt.show()
运行时,此代码应为我们的 Hero 网络产生以下图形:

工作原理...
NetworkX 具有多种计算群集的机制:
>>> nx.transitivity(graph)
0.194539747093
>>> nx.average_clustering(graph)
0.774654121711
nx.transitivity函数使用nx.triangles函数来计算三角形数量与可能三角形数量之比。 nx.clustering函数计算每个节点的聚类系数,nx_average_clustering函数计算图形的平均系数。 较高的传递性和聚类系数意味着该图表现出较小的世界效应。
小世界是一个看起来随机的网络,但它具有较高的传递性和较短的平均路径长度。 这是社交网络非常常见的结构,因为现代网络的语义具有紧密的联系,因此具有很强的可传递性。 从本质上讲,这意味着存在一系列带有很少桥节点的大型集群。 英雄的社交网络既表现出小世界效应,又表现出优先的依恋; 大多数新边都指向高度已经高的节点,因此创建了长尾,向左偏斜的分布,正如我们在图中看到的那样。
还有更多...
Louvian 方法是一种贪婪的优化方法,用于对网络进行分区,并优化每个网络分区的模块化。 首先,该方法通过尝试优化本地模块化来识别小型社区,然后将属于同一社区的节点聚合在一起,然后再次执行该过程。 这样,将返回社区的分层数据结构。 该方法简单,高效,并且可用于具有数十亿个链接的数百万个节点的大型网络。
最初的方法是由 UCL(Louvain-la-Neuve)的 Etienne Lefebvre 开发的; 然后与 Vincent Blondel,Jean-Loup Guillaume 和 Renaud Lambiotte 共同撰写和完善。 之所以称为“ Louvain 方法”,是因为该方法是在团队的所有成员都在卢瓦尔大学天主教堂学习时才设计的。 作者在一起创建了一个 Python 方法来计算这些分层社区,该方法依赖于 NetworkX。 基本的检测算法如下:
import community
import networkx as nx
def detect_communities(graph):
partition = community.best_partition(graph)
nx.set_node_attributes(graph, 'partition', partition)
return graph, partition
该函数需要一个nx.Graph,并使用community模块来计算最佳的根分区。 分区是分层的,因此要获得子社区,我们只需遍历分区中的各个部分,并在我们的图中为节点分配一个名为partition的属性,该属性即可识别一个具有其社区的节点。 然后,该函数返回修改后的图形和分区以用于可视化。
可视化图表
在本章中,我们一直在可视化社交网络,以帮助发展我们对图形的理解和直觉。 在本食谱中,我们将更深入地研究图形可视化。
准备
确保已安装 NetworkX 和 matplotlib。
怎么做...
完成以下步骤列表,以更好地理解 Python 中的图形可视化:
- NetworkX 使用与上一章相同的图表库包装 matplotlib 或 graphviz 来绘制简单图形。 这对于较小的图形有效,但是对于较大的图形,可以快速消耗内存。 要绘制小图,只需使用
networkx.draw函数,然后使用pyplot.show显示它:
>>> import networkx as nx
>>> import matplotlib.pyplot as plt
>>> nx.draw(graph)
>>> plt.show()
- 但是,下面有一个丰富的图形库,可让您自定义图形的外观以及使用许多不同的布局算法进行布局的方式。 我们来看一个使用 NetworkX 库随附的社交图之一的示例,该图是戴维斯南方俱乐部女士图:
import networkx as nx
import matplotlib.pyplot as pl
# Generate the Graph
G=nx.davis_southern_women_graph()
# Create a Spring Layout
pos=nx.spring_layout(G)
# Find the center Node
dmin=1
ncenter=0
for n in pos:
x,y=pos[n]
d=(x-0.5)**2+(y-0.5)**2
if d<dmin:
ncenter=n
dmin=d
- 接下来,我们将为图表着色。 首先,我们必须确定中心节点,因为我们将其最深地着色。 然后,所有较远的节点将被着色为浅色,直到它们变为白色。 弹簧布局已经确定了每个节点的(x,y)坐标,因此很容易计算每个节点到图形中心的欧几里得距离(在这种情况下为点
(0.5, 0.5)),然后找到该节点 距离最短的 确定后,我们将计算每个节点到中心节点的跳数(例如,路径长度)。nx.single_source_shortest_path_length函数返回节点的字典及其与作为参数提供的节点的距离。 然后,我们将使用这些距离来确定颜色:
p=nx.single_source_shortest_path_length(G,ncenter)
- 接下来,是时候绘制图形了。 我们创建一个
matplotlib图形,然后使用 NetworkX 绘制功能绘制边缘。 然后,我们绘制节点。 此函数使用颜色图(cmap参数)来确定要使用的颜色范围,该范围由跳距确定:
plt.figure(figsize=(8,8))
nx.draw_networkx_edges(G,pos,nodelist=[ncenter],alpha=0.4)
nx.draw_networkx_nodes(G,pos,nodelist=p.keys(),
node_size=90,
node_color=p.values(),
cmap=plt.cm.Reds_r)
plt.show()
调用此函数将产生一个图形,如下图所示:

工作原理...
在前面的代码中,我们通过davis_southern_women_graph函数创建了一个图形G,它是 NetworkX 附带的许多内置图形生成函数之一。 然后,我们使用布局查找 G 中每个节点的位置。 布局是一种算法,旨在确定节点在图形上的位置,以创建有效的可视化。 默认情况下,NetworkX 带有五种定位算法。 圆形布局将节点放置在一个圆中,而外壳布局将节点放置在一个同心圆中。 随机布局使用 Laplacian 图的特征向量均匀地分布节点和频谱布局位置。
春天的布局也许是最常见的。 弹簧布局是一个受力控制的布局,这意味着每个节点都会排斥其周围的其他节点,而边将它们保持在一起。 将所有节点都放到该图上,并以递归方式计算排斥/吸引力。 对于每次迭代,节点将相互排斥并相互吸引,形成稳定的布局。 有几种力导向算法,但是 NetworkX 使用 Fruchterman-Reingold 算法。
R 中的社交网络
本简短部分的目的是帮助读者使用 R 统计软件执行本章前面完成的任务。 Luke(2015)是处理 R 中社交网络的全面说明。我们将以一个非常简单随意的社交网络开始,然后我们将详细考虑巴厘岛恐怖分子网络。
使用简单而随意的社交网络,我们设定了与 R 实施相关的目标。 首先介绍简单有用的功能。 下面详细介绍了下一个使用的社交网络。
巴厘岛恐怖组织,Stuart Koschade 的博士学位论文可在这个页面处理 ] Jemaah Islamiyah 2002 巴厘岛恐怖主义网络。 这里的联系代表了巴厘岛恐怖组织之间的接触。 数据集由作为边缘特征的序数变量 IC(IC)测量接触的频率和持续时间,其中一个表示弱关系,一个逐渐表示较好的关系,五个表示最强的关系。 从技术上讲,R 网络对象Bali.rda可以从 Luke 的 GitHub 链接获得,他的书籍 R 包 UserNetR 也可以使用。 但是,我们仅向读者提供所需的 rda 文件。
通常用于分析社交网络的 R 包是sna,statnet,network和igraph。 有关更多详细信息,请参阅 Luke(2015)。
准备
执行此配方中的任务所需的 R 软件包为sna,statnet,network和igraph。 在 R 中安装这些软件包的简单方法是运行命令install.packages(c("sna","statnet","network","igraph"))。 还要求阅读器在当前工作目录中具有Bali.rda文件。
怎么做...
- 第一项任务是加载所需的库:
library(statnet,quietly=TRUE)
library(network,quietly=TRUE)
指定选项quietly=TRUE的目的是将大量注释打印到控制台,我们可以使用此选项来抵消它。 从操作上讲,我们是否使用此选项都没有区别。
- 接下来,我们指定一个由四行和四列组成的矩阵,如预期的那样是一个正方形矩阵,然后将其用于建立网络:
netmat1 <- rbind(c(0,1,1,0),
c(1,0,1,0),
c(1,1,0,1),
c(0,0,1,0))
rownames(netmat1) <- c("A","B","C","D")
colnames(netmat1) <- c("A","B","C","D")
netmat1
- 结果是 R 控制台中的以下输出:
> netmat1
A B C D
A 0 1 1 0
B 1 0 1 0
C 1 1 0 1
D 0 0 1 0
- 下一步将使用该矩阵来建立社交网络。
- 使用相同的命名函数和包将矩阵
netmat1转换为网络。 基本功能还应用于网络对象,以获取网络的基本特征:
net1 <- network(netmat1,matrix.type="adjacency")
class(net1)
summary(net1)
network.size(net1)
network.density(net1)
components(net1)
gplot(net1, vertex.col = 2, displaylabels = TRUE)
控制台中的 R 输出如下所示:
> net1 <- network(netmat1,matrix.type="adjacency")
> class(net1)
[1] "network"
> summary(net1)
Network attributes:
vertices = 4
directed = TRUE
hyper = FALSE
loops = FALSE
multiple = FALSE
bipartite = FALSE
total edges = 8
missing edges = 0
non-missing edges = 8
density = 0.6666667
Vertex attributes:
vertex.names:
character valued attribute
4 valid vertex names
No edge attributes
Network adjacency matrix:
A B C D
A 0 1 1 0
B 1 0 1 0
C 1 1 0 1
D 0 0 1 0
> network.size(net1)
[1] 4
> network.density(net1)
[1] 0.6666667
> components(net1)
Node 1, Reach 4, Total 4
Node 2, Reach 4, Total 8
Node 3, Reach 4, Total 12
Node 4, Reach 4, Total 16
[1] 1
> gplot(net1, vertex.col = 2, displaylabels = TRUE)
在控制台上执行前面的 R 代码块,我们可以设置和检查网络的各个方面。 函数network帮助我们将矩阵netmat1转换为(社交)网络net1,此确认由class(net1)行提供,该行的输出表明对象的类别确实是network。 接下来,summary函数告诉我们网络告诉我们net1中有4vertices,网络是directed一个,并且总共有 8 个边 netmat1 矩阵中 1 的数目。 网络 net1 的大小和密度分别为4和0.6666667。 sna软件包中的gplot功能有助于可视化网络:

接下来,我们将看一个更复杂和实用的网络,并在 R 中进行分析。该网络是巴厘岛恐怖组织,其解释已在前面给出。 Bali.rda 文件来自 Luke 的软件包 UserNetR。 首先,将网络导入到 R 中,然后像对 net1 网络一样进行汇总统计。
- 加载
Bali网络并执行以下基本摘要:
load("Bali.rda")
class(Bali)
Bali
# Basic Properties
network.size(Bali)
network.density(Bali)
# VIsualization
windows(height=30,width=30)
gplot(Bali,vertex.col=2, displaylabels = TRUE)
为简洁起见,我们仅给出图形输出,因为其余解释与网络net1相同:

Bali网络的矩阵形式可以如下获得:
Bali_Matrix <- as.matrix(Bali,matrix.type="adjacency")
Bali_Matrix[1:6,1:6]
输出如下:
Muklas Amrozi Imron Samudra Dulmatin Idris
Muklas 0 1 1 1 1 1
Amrozi 1 0 0 1 0 1
Imron 1 0 0 1 1 1
Samudra 1 1 1 0 1 1
Dulmatin 1 0 1 1 0 1
Idris 1 1 1 1 1 0
操作! 能够进行基本操作非常重要,我们将通过解决删除单个顶点的一项简单任务来加以约束。
- 首先将图形
net1转换为netg类igraph,然后使用plot函数将其可视化。 然后删除顶点D,并创建并可视化新的图形对象netg_del_d。 输出遵循以下 R 程序:
netg <- asIgraph(net1)
par(mfrow=c(1,2))
plot(netg,vertex.label=LETTERS[1:5])
netg_del_d <- delete.vertices(netg,v=5)
plot(netg_del_d,vertex.label=LETTERS[c(1:3,5)])

可以使用功能度,亲密性和介于两者之间的关系在 R 中识别强者和纽带。
- 应用功能
degree,closeness和betweenness,我们可以通过下一个 R 程序段来了解 Bali 网络:
Bali %v% 'vertex.names'
sna::degree(Bali,gmode="graph")
closeness(Bali,gmode="graph")
betweenness(Bali,gmode="graph")
上一个程序的输出如下:
> Bali %v% 'vertex.names'
[1] "Muklas" "Amrozi" "Imron" "Samudra" "Dulmatin" "Idris"
[7] "Mubarok" "Husin" "Ghoni" "Arnasan" "Rauf" "Octavia"
[13] "Hidayat" "Junaedi" "Patek" "Feri" "Sarijo"
> degree(Bali,gmode="graph")
[1] 9 4 9 15 9 10 3 9 9 5 5 5 5 5 9 6 9
> closeness(Bali,gmode="graph")
[1] 0.6957 0.5517 0.6957 0.9412 0.6957 0.7273 0.5333 0.6957 0.6957
[10] 0.5714 0.5714 0.5714 0.5714 0.5714 0.6957 0.4848 0.6957
> betweenness(Bali,gmode="graph")
[1] 2.3333 0.3333 1.6667 61.1667 1.6667 6.1667 0.0000 1.6667
[9] 1.6667 0.0000 0.0000 0.0000 0.0000 0.0000 1.6667 0.0000
[17] 1.6667
网络群集识别是网络分析中的重要任务。 在下一步中,我们将使用适当的功能来获得所需的答案。
- 首先将网络转换为
igraph功能。 在网络上应用功能clique.number,cliques和largest.cliques来识别和分离集群:
Balig <- asIgraph(Bali)
clique.number(Balig)
cliques(Balig,min=8)
largest.cliques(Balig)
所需的输出如下:
> Balig <- asIgraph(Bali)
> clique.number(Balig)
[1] 9
> cliques(Balig,min=8)
[[1]]
+ 8/17 vertices:
[1] 1 3 4 5 6 8 9 15
[[2]]
+ 8/17 vertices:
[1] 1 3 4 5 6 8 9 17
[[3]]
+ 8/17 vertices:
[1] 1 3 4 5 6 8 15 17
[[4]]
+ 8/17 vertices:
[1] 1 3 4 5 6 9 15 17
[[5]]
+ 8/17 vertices:
[1] 1 3 4 5 8 9 15 17
[[6]]
+ 8/17 vertices:
[1] 1 3 4 6 8 9 15 17
[[7]]
+ 8/17 vertices:
[1] 1 3 5 6 8 9 15 17
[[8]]
+ 8/17 vertices:
[1] 1 4 5 6 8 9 15 17
[[9]]
+ 8/17 vertices:
[1] 3 4 5 6 8 9 15 17
[[10]]
+ 9/17 vertices:
[1] 1 3 4 5 6 8 9 15 17
> largest.cliques(Balig)
[[1]]
+ 9/17 vertices:
[1] 4 6 1 3 5 8 9 15 17
工作原理...
R 网络软件包简化了大多数任务,并有助于避免编写复杂代码的需要。 我们看到了 R 软件包sna,igraph,intergraph,statnet和network在建立和分析社交网络中如何有用。 我们从可视化一个简单的网络开始,然后获得摘要。 子网络的子集化和可视化的任务为我们提供了很好的早期见解,同时我们也能够理解网络中顶点/边与子网络之间的相互关系。
八、Python 大规模电影推荐
在本章中,我们将介绍以下食谱:
- 建模偏好表达式
- 了解数据
- 摄取电影评论数据
- 寻找得分最高的电影
- 改善电影分级系统
- 测量偏好空间中用户之间的距离
- 计算用户之间的相关性
- 为用户找到最佳批评家
- 预测用户的电影收视率
- 逐项协作过滤
- 建立非负矩阵分解模型
- 将整个数据集加载到内存中
- 将基于 SVD 的模型转储到磁盘
- 训练基于 SVD 的模型
- 测试基于 SVD 的模型
简介
从书籍,电影到人们在 Twitter 上关注,推荐系统将互联网上的大量信息雕刻成更加个性化的信息流,从而提高了电子商务,Web 和社交应用的性能。 鉴于亚马逊货币化推荐的成功和 Netflix 奖的获得,毫不奇怪,任何有关个性化或数据理论预测的讨论都将涉及推荐者。 令人惊讶的是,推荐者实施起来多么简单,却容易受到稀疏数据和过度拟合的影响。
考虑一种采用非算法的方法来提出建议:获得建议的最简单方法之一就是查看我们信任的人的偏好。 我们将我们的偏好与他们的偏好进行隐式比较,并且您共享的相似性越多,您就越有可能发现新颖的共享偏好。 但是,每个人都是独一无二的,我们的偏好存在于各种类别和领域中。 如果您可以利用很多人的偏好,而不仅是您信任的人的偏好,该怎么办? 总体而言,您不仅可以看到像您这样的人的模式,还可以看到反建议-远离您的事物,那些不喜欢您的人要注意的事情。 希望您还会看到在共享您自己独特体验的部分人群的共享偏好空间中的微妙划界。
在此基本前提下,使用了一组称为协同过滤的技术来提出建议。 简而言之,可以将这一前提归结为以下假设:那些具有相似的过去偏好的人将来会共享相同的偏好。 当然,这是从人类的角度出发,并且这种假设的典型推论是从事物的优先选择角度出发-同一个人喜欢的项目集在将来更可能被一起优先使用-以及 这是文献中通常所说的以用户为中心的协作过滤与以项目为中心的协作过滤的基础。
注意
协作过滤是由 David Goldberg 在名为的论文中提出的。该论文使用协作过滤来编织信息挂毯,ACM 和 他提出了一个名为 Tapestry 的系统,该系统于 1992 年在 Xerox PARC 上进行了设计,目的是为文档添加有趣或不感兴趣的注释,并向寻求良好阅读的人们提供文档建议。
协作过滤算法搜索大量的偏好表达式组,以查找与某些输入偏好或某些偏好的相似性。 这些算法的输出是建议的排序列表,这些建议是所有可能的首选项的子集,因此,它被称为过滤。 协作来自使用许多其他人的偏好来为自己找到建议。 可以将其视为对偏好空间的搜索(针对蛮力技术),聚类问题(对相似的首选项目进行分组),或者甚至是其他一些预测模型。 为了在稀疏或大型数据集上优化或解决此问题,已经进行了许多算法尝试,本章将讨论其中的一些算法。
本章的目标如下:
- 了解如何从各种来源对偏好进行建模
- 学习如何使用距离量度来计算相似度
- 使用矩阵分解对星级进行推荐的建模
这两个不同的模型将使用网络上随时可用的数据集在 Python 中实现。 为了说明本章中的技术,我们将使用明尼苏达大学经常引用的 MovieLens 数据库,该数据库包含喜欢电影的观影者的星级。
注意
请注意,本章被认为是高级章节,与以前的章节相比,完成本章的时间可能要多得多。
建模首选项表达式
我们已经指出,像 Amazon 这样的公司会跟踪购买情况和页面浏览量以提出建议,Goodreads 和 Yelp 使用五星级评级和文字评论,而 Reddit 或 Stack Overflow 这样的网站都使用简单的上下投票。 您可以看到,偏好可以在数据中以不同的方式表示,从布尔标志,投票到评分。 但是,这些首选项是通过尝试在首选项表达式中找到相似性组来表达的,在这些表达式中,您正在利用协作过滤的核心假设。
更正式地说,我们了解到,鲍勃(Bob)和爱丽丝(Alice)这两个人共享对特定商品或小部件的偏好。 如果爱丽丝也对链轮等其他物品有偏爱,那么鲍勃也比随机地也有机会分享对链轮的偏爱。 我们相信,鲍勃和爱丽丝的口味相似性可以通过大量偏好来综合表达,并且通过利用团体的协作性质,我们可以过滤产品世界。
怎么做...
我们将在接下来的几个配方中对首选项表达式进行建模,包括以下内容:
- 了解数据
- 摄取电影评论数据
- 寻找收视率最高的电影
- 完善电影分级系统
工作原理...
偏好表达式是可证明的相对选择模型的实例。 也就是说,偏好表达是用于显示一个人的一组项目之间的主观排名的数据点。 更正式地说,我们应该说偏好表达不仅是相对的,而且是时间的-例如,偏好陈述还具有固定的时间相对性和项目相对性。
注意
偏好表达是可证明的相对选择模型的一个实例。
可以认为我们可以在全球范围内主观和准确地表达自己的偏好(例如,将一部电影与其他所有电影进行比较),这很高兴,但事实上,我们的品味会随着时间而改变,而且我们只能 考虑一下我们如何对项目进行相对排名。 偏好模型必须考虑到这一点,并尝试减轻由偏好引起的偏差。 最常见的偏好表达模型类型通过使表达式在数值上变得模糊来简化排名问题,例如:
- 布尔表达式(是或否)
- 上下投票(如弃权,不喜欢)
- 加权信号(点击或操作数)
- 广泛的分类(明星,讨厌或喜爱)
想法是为个人用户创建偏好模型,为特定个人创建偏好表达集合的数值模型。 模型将各个首选项表达式构建到可以针对其进行计算的有用的特定于用户的上下文中。 可以对模型执行进一步的推理,以减轻基于时间的偏差或执行本体论推理或其他分类。
随着实体之间的关系变得越来越复杂,您可以通过将行为权重分配给每种类型的语义连接来表达它们的相对偏好。 然而,选择权重是困难的,并且需要研究来确定相对权重,这就是为什么优选模糊泛化的原因。 例如,下表显示了一些著名的排名首选项系统:

在本章的其余部分中,我们将仅考虑一个非常常见的偏好表达:星级等级(从 1 到 5)。
了解数据
了解您的数据对于所有与数据相关的工作都是至关重要的。 在本食谱中,我们将获取并首先查看用于构建推荐引擎的数据。
准备
为了准备本食谱以及本章的其余部分,请从明尼苏达大学的 GroupLens 网站下载 MovieLens 数据。 您可以在这个页面中找到数据。
在本章中,我们将使用较小的 MoveLens 100k 数据集(大小为 4.7 MB),以便轻松地将整个模型加载到内存中。
怎么做...
执行以下步骤,以更好地理解本章将要使用的数据:
- 从这个页面下载数据。 100K 数据集是您想要的数据集(
ml-100k.zip):

- 将下载的数据解压缩到您选择的目录中。
- 我们主要关注的两个文件是
u.data和u.item,其中包含用户电影分级,而u.item包含电影信息和详细信息。 要了解每个文件,在 Mac 和 Linux 的命令提示符下使用head命令,在 Windows 的情况下使用more命令:
head -n 5 u.item
注意
请注意,如果您在运行 Microsoft Windows 操作系统且未使用虚拟机的计算机上工作(不建议使用),则您无权访问head命令; 而是使用以下命令:more u.item 2 n
- 前面的命令为您提供以下输出:
1|Toy Story (1995)|01-Jan-1995||http://us.imdb.com/M/title- exact?Toy%20Story%20(1995)|0|0|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|02|GoldenEye (1995)|01-Jan-1995||http://us.imdb.com/M/title-
exact?GoldenEye%20(1995)|0|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|03|Four Rooms (1995)|01-Jan-1995||http://us.imdb.com/M/title- exact?Four%20Rooms%20(1995)|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|04|Get Shorty (1995)|01-Jan-1995||http://us.imdb.com/M/title- exact?Get%20Shorty%20(1995)|0|1|0|0|0|1|0|0|1|0|0|0|0|0|0|0|0|0|05|Copycat (1995)|01-Jan-1995||http://us.imdb.com/M/title-
exact?Copycat%20(1995)|0|0|0|0|0|0|1|0|1|0|0|0|0|0|0|0|1|0|0
以下命令将产生给定的输出:
head -n 5 u.data
对于 Windows,可以使用以下命令:
more u.item 2 n196 242 3 881250949186 302 3 89171774222 377 1 878887116244 51 2 880606923166 346 1 886397596
工作原理...
我们将使用的两个主要文件如下:
u.data:包含用户移动等级u.item:包含电影信息和其他详细信息
两者都是字符分隔文件; 主文件u.data由制表符分隔,u.item由管道分隔。
对于u.data,第一列是用户 ID,第二列是电影 ID,第三列是星级,最后一列是时间戳。 u.item文件包含更多信息,包括 ID,标题,发布日期,甚至是指向 IMDb 的 URL。 有趣的是,此文件还具有一个布尔数组,指示每个电影的流派,包括(按顺序)动作,冒险,动画,儿童,喜剧,犯罪,纪录片,戏剧,幻想,黑色电影,恐怖片,音乐剧, 神秘,浪漫,科幻,惊悚,战争和西方。
还有更多...
适用于构建推荐引擎的免费的,网络规模的数据集非常少。 结果,电影镜头数据集是执行此任务的非常受欢迎的选择,但还有其他选择。 著名的 Netflix 奖数据集已被 Netflix 下拉。 但是,可以通过 Internet 存档从 Stack Exchange 网络(包括 Stack Overflow)中转储所有用户贡献的内容。 此外,还有一个跨书数据集,其中包含一百万个(一百万个不同的书籍中约四分之一)分级。
摄取电影评论数据
推荐引擎需要大量的培训数据才能做好工作,这就是为什么它们经常被降级到大数据项目。 但是,要构建推荐引擎,我们必须首先将所需的数据存储到内存中,并且由于数据的大小,必须以一种内存安全且有效的方式进行存储。 幸运的是,Python 拥有完成任务的所有工具,此食谱向您展示了如何进行。
准备
您将需要下载适当的电影镜头数据集,如前面的配方中所述。 如果跳过了第 1 章和“准备数据科学环境”中的设置,则需要返回并确保正确安装了 NumPy。
怎么做...
以下步骤将指导您创建将数据集加载到内存中所需的功能:
- 打开您喜欢的 Python 编辑器或 IDE。 有很多代码,因此将其直接输入文本文件而不是 Read-Eval-Print Loop(REPL)应该更简单。
- 我们创建了一个导入电影评论的功能:
In [1]: import csv
...: import datetime
In [2]: def load_reviews(path, **kwargs):
...: """
...: Loads MovieLens reviews
...: """
...: options = {
...: 'fieldnames': ('userid', 'movieid', 'rating', 'timestamp'),
...: 'delimiter': '\t',
...: }
...: options.update(kwargs)
...:
...: parse_date = lambda r,k: datetime.fromtimestamp(float(r[k]))
...: parse_int = lambda r,k: int(r[k])
...:
...: with open(path, 'rb') as reviews:
...: reader = csv.DictReader(reviews, **options)
...: for row in reader:
...: row['movieid'] = parse_int(row, 'movieid')
...: row['userid'] = parse_int(row, 'userid')
...: row['rating'] = parse_int(row, 'rating')
...: row['timestamp'] = parse_date(row, 'timestamp')
...: yield row
- 我们创建一个辅助函数来帮助导入数据:
In [3]: import os
...: def relative_path(path):
...: """
...: Returns a path relative from this code file
...: """
...: dirname = os.path.dirname(os.path.realpath('__file__'))
...: path = os.path.join(dirname, path)
...: return os.path.normpath(path)
- 我们创建另一个函数来加载电影信息:
In [4]: def load_movies(path, **kwargs):
...:
...: options = {
...: 'fieldnames': ('movieid', 'title', 'release', 'video', 'url'),
...: 'delimiter': '|',...: 'restkey': 'genre',
...: }
...: options.update(kwargs)
...:
...: parse_int = lambda r,k: int(r[k])
...: parse_date = lambda r,k: datetime.strptime(r[k], '%d-%b-%Y') if r[k] else None
...:
...: with open(path, 'rb') as movies:
...: reader = csv.DictReader(movies, **options)
...: for row in reader:
...: row['movieid'] = parse_int(row, 'movieid')
...: row['release'] = parse_date(row, 'release')
...: row['video'] = parse_date(row, 'video')
...: yield row
- 最后,我们开始创建一个
MovieLens类,该类将在以后的食谱中得到增强:
In [5]: from collections import defaultdict
In [6]: class MovieLens(object):
...: """
...: Data structure to build our recommender model on.
...: """
...:
...: def __init__(self, udata, uitem):
...: """
...: Instantiate with a path to u.data and u.item
...: """
...: self.udata = udata
...: self.uitem = uitem
...: self.movies = {}
...: self.reviews = defaultdict(dict)
...: self.load_dataset()
...:
...: def load_dataset(self):
...: """
...: Loads the two datasets into memory, indexed on the ID.
...: """
...: for movie in load_movies(self.uitem):
...: self.movies[movie['movieid']] = movie
...:
...: for review in load_reviews(self.udata):...: self.reviews[review['userid']][review['movieid']] = review
- 确保已将功能导入到 REPL 或 Jupyter 工作区中,然后键入以下内容,确保数据文件的路径适合您的系统:
In [7]: data = relative_path('../data/ml-100k/u.data')
...: item = relative_path('../data/ml-100k/u.item')
...: model = MovieLens(data, item)
工作原理...
我们用于两个数据加载功能(load_reviews和load_movies)的方法很简单,但是它照顾了从磁盘解析数据的细节。 我们创建了一个函数,该函数首先获取数据集的路径,然后获取所有可选关键字的路径。 我们知道我们需要与csv模块进行交互的特定方式,因此我们创建了默认选项,将行的字段名称以及定界符\t传递进来。 options.update(kwargs)行表示我们将接受传递给此功能的所有用户。
然后,我们使用 Python 中的lambda函数创建了内部解析函数。 这些简单的解析器将一行和一个键作为输入,并返回转换后的输入。 这是将lambda用作内部可重复使用的代码块的示例,并且是 Python 中的常用技术。 最后,我们打开文件并使用我们的选项创建一个csv.DictReader函数。 遍历阅读器中的行,我们分别解析我们想成为int和datetime的字段,然后产生该行。
注意
请注意,由于我们不确定输入文件的实际大小,因此我们使用 Python 生成器以内存安全的方式执行此操作。 使用yield而不是return可确保 Python 在后台创建生成器,并且不会将整个数据集加载到内存中。
我们将使用这些方法中的每一种,通过使用该数据集的计算在不同时间加载数据集。 我们需要始终知道这些文件在哪里,这可能会很麻烦,尤其是在较大的代码库中。 在中还有更多... 部分,我们将讨论减轻该问题的 Python 专业提示。
最后,我们创建了一个数据结构,即MovieLens类,通过该结构可以保存我们的审阅数据。 此结构采用udata和uitem路径,然后将电影和评论加载到两个 Python 字典中,分别由movieid和userid索引。 要实例化此对象,您将执行以下操作:
data = relative_path('../data/ml-100k/u.data')
item = relative_path('../data/ml-100k/u.item')
model = MovieLens(data, item)
请注意,上述命令假定您将数据保存在名为data的文件夹中。 现在,我们可以将整个数据集加载到内存中,并在数据集中指定的各种 ID 上建立索引。
您是否注意到relative_path功能的使用? 当使用诸如此类的夹具来构建模型时,数据通常包含在代码中。 当您在 Python 中指定路径时,例如data/ml-100k/u.data,它将相对于您运行脚本的当前工作目录进行查找。 为了帮助减轻这种麻烦,您可以指定相对于代码本身的路径:
import os def relative_path(path): """ Returns a path relative from this code file """ dirname = os.path.dirname(os.path.realpath('__file__')) path = os.path.join(dirname, path) return os.path.normpath(path)
请记住,这会将整个数据结构保存在内存中。 对于 100k 数据集,这将需要 54.1 MB,对于现代计算机来说还算不错。 但是,我们还应该牢记,我们通常会使用远远超过 100,000 条评论的方式来建立推荐者。 这就是为什么我们以与数据库非常相似的方式配置数据结构的原因。 为了扩展系统,您将reviews和movies属性替换为数据库访问函数或属性,这将产生我们的方法所期望的数据类型。
查找得分最高的电影
如果您正在寻找一部好电影,通常会希望看到整体上最受欢迎或评分最高的电影。 最初,我们将采用天真的方法,通过平均每部电影的用户评论来计算电影的综合评分。 该技术还将演示如何访问MovieLens类中的数据。
准备
这些配方本质上是顺序的。 因此,在开始本章之前,您应该已经完成了本章中的先前食谱。
怎么做...
请按照以下步骤为数据集中的所有电影输出数字分数,并计算前 10 名列表:
- 使用新方法增强
MovieLens类,以获取特定电影的所有评论:
In [8]: class MovieLens(object):
...:
...:
...: def reviews_for_movie(self, movieid):
...: """
...: Yields the reviews for a given movie
...: """
...: for review in self.reviews.values():
...: if movieid in review:
...: yield review[movieid]
...:
- 然后,添加其他方法来计算用户评论的前十部电影:
In [9]: import heapq
...: from operator import itemgetter
...: class MovieLens(object):
...:
...: def average_reviews(self):
...: """
...: Averages the star rating for all movies. Yields a tuple of movieid,
...: the average rating, and the number of reviews.
...: """
...: for movieid in self.movies:
...: reviews = list(r['rating'] for r in self.reviews_for_movie(movieid))
...: average = sum(reviews) / float(len(reviews))
...: yield (movieid, average, len(reviews))
...:
...: def top_rated(self, n=10):
...: """
...: Yields the n top rated movies
...: """
...: return heapq.nlargest(n, self.bayesian_average(), key=itemgetter(1))
...:
注意
请注意,class MovieLens(object):下方的...表示我们将average_reviews方法附加到现有的MovieLens类上。
- 现在,让我们打印最受好评的结果:
In [10]: for mid, avg, num in model.top_rated(10):
...: title = model.movies[mid]['title']
...: print "[%0.3f average rating (%i reviews)] %s" % (avg, num,title)
- 在 REPL 中执行上述命令应产生以下输出:
Out [10]: [5.000 average rating (1 reviews)] Entertaining Angels: The Dorothy Day Story (1996)[5.000 average rating (2 reviews)] Santa with Muscles (1996)[5.000 average rating (1 reviews)] Great Day in Harlem, A (1994)[5.000 average rating (1 reviews)] They Made Me a Criminal (1939)[5.000 average rating (1 reviews)] Aiqing wansui (1994)[5.000 average rating (1 reviews)] Someone Else's America (1995)[5.000 average rating (2 reviews)] Saint of Fort Washington, The (1993) [5.000 average rating (3 reviews)] Prefontaine (1997)[5.000 average rating (3 reviews)] Star Kid (1997)[5.000 average rating (1 reviews)] Marlene Dietrich: Shadow and Light (1996)
工作原理...
添加到MovieLens类的新reviews_for_movie()方法遍历我们的审阅字典值(由userid参数索引),检查movieid值是否已由用户审阅,然后显示 那个评论字典。 下一个方法将需要此类功能。
使用average_review()方法,我们创建了另一个生成器功能,该功能遍历我们所有的电影及其所有评论,并提供电影 ID,平均评分和评论数量。 top_rated功能使用heapq模块根据平均值快速对评论进行排序。
heapq数据结构,也称为优先级队列算法,是具有有趣和有用属性的抽象数据结构的 Python 实现。 堆是构建的二叉树,因此每个父节点的值都小于或等于其任何子节点的值。 因此,最小的元素是树的根,可以在恒定时间内对其进行访问,这是非常理想的属性。 使用heapq,Python 开发人员可以有效地在有序数据结构中插入新值,并返回排序后的值。
还有更多...
在这里,我们遇到了第一个问题,一些收视率最高的电影只有一个评论(反之,收视率最低的电影也是如此)。 您如何将卡萨布兰卡的平均评分为 4.457(243 条评论)与圣诞老人与肌肉的的平均评分为 5.000(2 条评论)? 我们确定那两位评论者确实喜欢圣诞老人和肌肉,但是卡萨布兰卡的高评价可能更有意义,因为更多的人喜欢它。 大多数具有星级的推荐者将仅输出平均评分以及评论者的数量,从而使用户可以确定其质量。 但是,作为数据科学家,我们可以在下一个食谱中做得更好。
另请参见
heapq文档可从这个页面获得。
改善电影分级系统
我们不希望使用这样一种系统来构建推荐引擎,该系统认为可能带有肌肉的圣诞老人,通常比 Casablanca 更好。 因此,先前使用的天真的评分方法必须加以改进,并且是本食谱的重点。
准备
确保首先完成本章中的先前食谱。
怎么做...
以下步骤实现并测试了一种新的电影评分算法:
- 让我们实现一个新的贝叶斯电影评分算法,如以下函数所示,将其添加到
MovieLens类中:
In [11]: def bayesian_average(self, c=59, m=3):
...: """
...: Reports the Bayesian average with parameters c and m.
...: """
...: for movieid in self.movies:
...: reviews = list(r['rating'] for r in self.reviews_for_movie(movieid))
...: average = ((c * m) + sum(reviews)) / float(c + len(reviews))
...: yield (movieid, average, len(reviews))
- 接下来,我们将用以下命令中的版本替换
MovieLens类中的top_rated方法,该命令使用上一步中的新Bayesian_average方法:
In [12]: def top_rated(self, n=10):
...: """
...: Yields the n top rated movies
...: """
...: return heapq.nlargest(n, self.bayesian_average(), key=itemgetter(1))
- 打印新的前十名列表对我们来说似乎有些熟悉,现在
Casablanca的评级为4:
[4.234 average rating (583 reviews)] Star Wars (1977)[4.224 average rating (298 reviews)] Schindler's List (1993)[4.196 average rating (283 reviews)] Shawshank Redemption, The (1994)[4.172 average rating (243 reviews)] Casablanca (1942)[4.135 average rating (267 reviews)] Usual Suspects, The (1995)[4.123 average rating (413 reviews)] Godfather, The (1972)[4.120 average rating (390 reviews)] Silence of the Lambs, The (1991)[4.098 average rating (420 reviews)] Raiders of the Lost Ark (1981)[4.082 average rating (209 reviews)] Rear Window (1954)[4.066 average rating (350 reviews)] Titanic (1997)
工作原理...
如上一个食谱所示,对电影评论进行平均,根本就行不通,因为某些电影没有足够的评分,无法与具有更高评分的电影进行有意义的比较。 我们真正想要的是让每位电影评论家给每部电影评分。 鉴于这是不可能的,我们可以得出一个估计,如果有无数的人给电影定级,电影将如何定级。 从一个数据点很难推断出这一点,因此我们应该说,如果有相同人数的人给它一个平均评分(例如,根据评论数对结果进行过滤),我们希望对电影的评分进行估算 。
可以使用在bayesian_average()函数中实现的贝叶斯平均值来计算此估算值,以根据以下公式推断这些额定值:

在这里,m是我们求星星平均值的先验,C是与我们后验观测数量相等的置信度参数。
确定先验可能是一件复杂而神奇的艺术。 无需采用将 Dirichlet 分布拟合到我们的数据的复杂路径,我们可以使用五星级评级系统简单地选择 3 的m优先级,这意味着我们的先验假设星级趋向于 在中值附近进行审查。 在选择C时,您表示要离开先前的知识,需要进行多次评论; 我们可以通过查看每部电影的平均评论数来计算:
print float(sum(num for mid, avg, num in model.average_reviews())) /
len(model.movies)
这给我们提供了59.4的平均值,我们将其用作函数定义中的默认值。
还有更多...
播放C参数。 您应该发现,如果更改参数以使 C = 50,则前 10 个列表将巧妙地移动; 在这种情况下,迅达的清单和星球大战的排名会互换,失落奇兵的突袭者和后窗-注意 这两个被交换的电影都比前一个电影具有更多的评论,这意味着较高的C参数平衡了其他电影的较少收视率。
另请参见
- 请在这个页面了解 Yelp 如何应对这一挑战。
测量偏好空间中用户之间的距离
两种最可识别的协作过滤系统类型是基于用户的推荐者和基于项目的推荐者。 如果想像一下偏好空间是n维特征空间,其中绘制了用户或商品,那么我们可以说相似的用户或商品在此偏好空间中倾向于彼此靠近。 因此,这种类型的协作过滤的替代名称是最近邻居推荐器。
在此过程中,至关重要的一步是提出一个相似性或距离度量标准,通过该度量标准,我们可以将批评者彼此之间或相互喜欢的项目进行比较。 然后,该度量用于将特定用户与所有其他用户进行成对比较,或者反之,将要与所有其他项目进行比较的项目进行比较。 然后使用规范化的比较来确定建议。 尽管计算空间可能会变得非常大,但是距离度量本身并不难计算,在本食谱中,我们将探索一些并实现我们的第一个推荐系统。
在本食谱中,我们将测量用户之间的距离; 在此之后的食谱中,我们将看看另一个相似距离指示器。
准备
我们将从建模偏好表达式秒和的MovieLens类继续构建。 如果您没有机会复习本节,请准备该课程的代码。 重要的是,我们将要访问从磁盘上的 CSV 文件加载的数据结构MovieLens.movies和MovieLens.reviews。
怎么做...
以下步骤集提供了有关如何计算用户之间的欧几里得距离的说明:
- 使用新方法
shared_preferences增强MovieLens类,以提取已被A和B两位评论家评价的电影:
In [13]: class MovieLens(object):
...:
...: def shared_preferences(self, criticA, criticB):
...: """
...: Returns the intersection of ratings for two critics, A and B.
...: """
...: if criticA not in self.reviews:
...: raise KeyError("Couldn't find critic '%s' in data" % criticA)
...: if criticB not in self.reviews:
...: raise KeyError("Couldn't find critic '%s' in data" % criticB)
...:
...: moviesA = set(self.reviews[criticA].keys())
...: moviesB = set(self.reviews[criticB].keys())
...: shared = moviesA & moviesB # Intersection operator
...:
...: # Create a reviews dictionary to return
...: reviews = {}
...: for movieid in shared:
...: reviews[movieid] = (
...: self.reviews[criticA][movieid]['rating'],
...: self.reviews[criticB][movieid]['rating'],
...: )
...: return reviews
...:
- 然后,实现一个函数,使用两个评论员的共享电影首选项作为计算的向量来计算两个评论员之间的欧几里得距离。 此方法也将是
MovieLens类的一部分:
In [14]: from math import sqrt
...:
...: def euclidean_distance(self, criticA, criticB, prefs='users'):
...: """
...: Reports the Euclidean distance of two critics, A and B by
...: performing a J-dimensional Euclidean calculation of each of their
...: preference vectors for the intersection of books the critics have
...: rated.
...: """
...:
...: # Get the intersection of the rated titles in the data.
...:
...: if prefs == 'users':
...: preferences = self.shared_preferences(criticA, criticB)
...: elif prefs == 'movies':
...: preferences = self.shared_critics(criticA, criticB)
...: else:
...: raise Exception("No preferences of type '%s'." % prefs)
...:
...: # If they have no rankings in common, return 0.
...: if len(preferences) == 0: return 0
...:
...: # Sum the squares of the differences
...: sum_of_squares = sum([pow(a-b, 2) for a, b in preferences.values()])
...:
...: # Return the inverse of the distance to give a higher score to
...: # folks who are more similar (e.g. less distance) add 1 to prevent
...: # division by zero errors and normalize ranks in [0, 1]
...: return 1 / (1 + sqrt(sum_of_squares))
- 实施上述代码后,请在 REPL 中对其进行测试:
>>> data = relative_path('data/ml-100k/u.data')>>> item = relative_path('data/ml-100k/u.item') >>> model = MovieLens(data, item)>>> print model.euclidean_distance(232, 532)0.1023021629920016
工作原理...
MovieLens类的新shared_preferences()方法确定两个用户的共享首选项空间。 至关重要的是,我们只能根据他们都对用户(criticA和criticB输入参数)进行的评估来进行比较。 此函数使用 Python 集来确定A和B均已查看的电影列表(电影A已评级和电影B已评级)。 然后,该函数对该集合进行迭代,返回一个字典,该字典的键是电影 ID,并且值是收视率元组,例如,对于两个用户都已收视的每部电影,其收视率是[ratingA和ratingB。 现在,我们可以使用该数据集来计算相似性得分,这是由第二个函数完成的。
euclidean_distance()函数将两个注释符作为输入A和B,并计算偏好空间中用户之间的距离。 在这里,我们选择实现欧几里得距离度量标准(对于记住毕达哥拉斯定理的人来说,二维变化是众所周知的),但是我们也可以实现其他度量标准。 此函数会将实数从0返回到1,其中0的批评者相似度较小(相距较远),而1的批评者相似度较高(相近)。
还有更多...
曼哈顿距离是另一种非常流行的度量标准,也是一个非常简单易懂的度量标准。 它可以简单地将每个向量的元素之间的成对差的绝对值求和。 或者,在代码中,可以按以下方式执行:
manhattan = sum([abs(a-b) for a, b in preferences.values()])
此度量标准也称为城市街区距离,因为从概念上讲,这好像是在计算要在城市的两点之间行走的北/南和东/西街区的数量。 在针对本配方实现它之前,您还希望以某种方式反转和标准化该值,以返回 [0,1] 范围内的值。
另请参见
计算用户之间的相关性
在上一个食谱中,我们使用了许多可能的距离度量中的一个来捕获用户的电影评论之间的距离。 即使有五个或五百万个其他用户,两个特定用户之间的距离也不会更改。
在此配方中,我们将计算偏好空间中用户之间的相关性。 像距离度量一样,有许多相关度量。 其中最受欢迎的是 Pearson 或 Spearman 相关或余弦距离。 与距离度量不同,相关性将根据用户和电影的数量而变化。
准备
我们将继续继续先前食谱的工作,因此请确保您理解每一个食谱。
怎么做...
以下函数为两个评论者criticA和criticB实现pearson_correlation函数的计算,并将其添加到MovieLens类中:
In [15]: def pearson_correlation(self, criticA, criticB, prefs='users'):
...: """
...: Returns the Pearson Correlation of two critics, A and B by
...: performing the PPMC calculation on the scatter plot of (a, b)
...: ratings on the shared set of critiqued titles.
...: """
...:
...: # Get the set of mutually rated items
...: if prefs == 'users':
...: preferences = self.shared_preferences(criticA, criticB)
...: elif prefs == 'movies':
...: preferences = self.shared_critics(criticA, criticB)
...: else:
...: raise Exception("No preferences of type '%s'." % prefs)
...:
...: # Store the length to save traversals of the len computation.
...: # If they have no rankings in common, return 0.
...: length = len(preferences)
...: if length == 0: return 0
...:
...: # Loop through the preferences of each critic once and compute the
...: # various summations that are required for our final calculation.
...: sumA = sumB = sumSquareA = sumSquareB = sumProducts = 0
...: for a, b in preferences.values():
...: sumA += a
...: sumB += b
...: sumSquareA += pow(a, 2)
...: sumSquareB += pow(b, 2)
...: sumProducts += a*b
...:
...: # Calculate Pearson Score
...: numerator = (sumProducts*length) - (sumA*sumB)
...: denominator = sqrt(((sumSquareA*length) - pow(sumA, 2))
...: * ((sumSquareB*length) - pow(sumB, 2)))
...:
...: # Prevent division by zero.
...: if denominator == 0: return 0
...:
...: return abs(numerator / denominator)
...:
工作原理...
皮尔逊相关系数计算乘积矩,乘积矩是均值调整后的随机变量乘积的平均值,并且定义为两个变量(在我们的情况下为a和b)的协方差除以标准差的乘积 a的标准偏差和b的标准偏差。 作为公式,它看起来像下面的样子:

对于我们拥有的有限样本,在上一个函数中实现的详细公式如下:

思考皮尔逊相关性的另一种方法是度量两个变量之间的线性相关性。 它返回-1到1的得分,其中接近-1的负得分表示更强的负相关性,接近 1 的正得分。 表示更强的正相关。 分数0表示两个变量不相关。
为了使我们能够进行比较,我们想在[0,1]的空间中归一化我们的相似性指标,以便0意味着不那么相似,1意味着更不相似,因此我们 返回绝对值:
>>> print model.pearson_correlation(232, 532)0.06025793538385047
还有更多...
我们探索了两个距离度量:欧几里得距离和皮尔森相关性。 还有更多内容,包括 Spearman 相关性,Tantimoto 得分,Jaccard 距离,余弦相似度和 Manhattan 距离,仅举几例。 为推荐器的数据集选择正确的距离度量以及所使用的偏好表达的类型,对于确保这种推荐器成功至关重要。 读者可以根据自己的兴趣和特定的数据集来进一步探索该空间。
为用户找到最佳评论家
现在,我们有两种不同的方法来计算用户之间的相似距离,我们可以确定特定用户的最佳评论者,并查看它们与个人偏好的相似程度。
准备
在解决此问题之前,请确保已完成之前的食谱。
怎么做...
为MovieLens类实现一个新方法similar_critics(),该方法为用户找到最佳匹配项:
In [16]: import heapq
...:
...: def similar_critics(self, user, metric='euclidean', n=None):
...: """
...: Finds and ranks similar critics for the user according to the
...: specified distance metric. Returns the top n similar critics.
...: """
...:
...: # Metric jump table
...: metrics = {
...: 'euclidean': self.euclidean_distance,
...: 'pearson': self.pearson_correlation,
...: }
...:
...: distance = metrics.get(metric, None)
...:
...: # Handle problems that might occur
...: if user not in self.reviews:
...: raise KeyError("Unknown user, '%s'." % user)
...: if not distance or not callable(distance):
...: raise KeyError("Unknown or unprogrammed distance metric '%s'." % metric)
...:
...: # Compute user to critic similarities for all critics
...: critics = {}
...: for critic in self.reviews:
...: # Don't compare against yourself!
...: if critic == user:
...: continue
...:
...: critics[critic] = distance(user, critic)
...:
...: if n:
...: return heapq.nlargest(n, critics.items(), key=itemgetter(1))
...: return critics
工作原理...
MovieLens类中添加的similar_critics方法是此食谱的核心。 它以目标用户和两个可选参数作为参数:要使用的指标(默认为euclidean)和要返回的结果数(默认为None)。 如您所见,这种灵活的方法使用跳转表来确定要使用的算法(您可以传入euclidean或pearson以选择距离度量)。 将每个其他评论者与当前用户进行比较(用户与自己的比较除外)。 然后使用灵活的heapq模块对结果进行排序,并返回顶部的n结果。
要测试我们的实现,请打印出两个相似距离的运行结果:
>>> for item in model.similar_critics(232, 'euclidean', n=10): print "%4i: %0.3f" % item 688: 1.000 914: 1.00047: 0.50078: 0.500170: 0.500335: 0.500341: 0.500101: 0.414155: 0.414309: 0.414
>>> for item in model.similar_critics(232, 'pearson', n=10): print "%4i: %0.3f" % item33: 1.00036: 1.000155: 1.000260: 1.000289: 1.000302: 1.000309: 1.000317: 1.000511: 1.000769: 1.000
这些分数显然有很大的不同,而且皮尔逊似乎认为比欧几里得距离度量标准拥有更多相似用户。 欧几里得距离度量标准倾向于偏爱那些评分相同的项目较少的用户。 皮尔逊相关性倾向于更多的线性拟合分数,因此,皮尔逊校正了等级膨胀率,两名评论家可能对电影的评分非常相似,但一个用户对它们的评价始终如一。
如果算出每个评论家有多少个共享排名,您会发现数据非常稀疏。 这是前面的数据,其中附加了排名数量:
Euclidean scores: 688: 1.000 (1 shared rankings) 914: 1.000 (2 shared rankings) 47: 0.500 (5 shared rankings) 78: 0.500 (3 shared rankings) 170: 0.500 (1 shared rankings)Pearson scores: 33: 1.000 (2 shared rankings) 36: 1.000 (3 shared rankings) 155: 1.000 (2 shared rankings) 260: 1.000 (3 shared rankings) 289: 1.000 (3 shared rankings)
因此,仅找到相似的评论家并使用他们的评分来预测我们用户的得分是不够的; 相反,无论相似度如何,我们都必须汇总所有评论家的得分,并预测未评级电影的评级。
预测用户的电影收视率
要预测我们如何评价特定电影,我们可以计算评论家的加权平均数,这些评论家也对与用户相同的电影进行了评级。 如果评论者未对电影评分,则权重将是评论者与用户的相似性,那么他们的相似性将不会对电影的整体排名有所贡献。
准备
确保您已经完成了这一大篇累积的章节中的先前食谱。
怎么做...
以下步骤将引导您预测用户的电影收视率:
- 首先,将
predict_ranking函数添加到MovieLens类中,以预测用户可能会给具有类似评论家的特定电影的排名:
In [17]: def predict_ranking(self, user, movie, metric='euclidean', critics=None):
...: """
...: Predicts the ranking a user might give a movie according to the
...: weighted average of the critics that are similar to the that user.
...: """
...:
...: critics = critics or self.similar_critics(user, metric=metric)
...: total = 0.0
...: simsum = 0.0
...:
...: for critic, similarity in critics.items():
...: if movie in self.reviews[critic]:
...: total += similarity * self.reviews[critic][movie]['rating']
...: simsum += similarity
...:
...: if simsum == 0.0: return 0.0
...: return total / simsum
- 接下来,将
predict_all_rankings方法添加到MovieLens类中:
In [18]: def predict_all_rankings(self, user, metric='euclidean', n=None):
...: """
...: Predicts all rankings for all movies, if n is specified returns
...: the top n movies and their predicted ranking.
...: """
...: critics = self.similar_critics(user, metric=metric)
...: movies = {
...: movie: self.predict_ranking(user, movie, metric, critics)
...: for movie in self.movies
...: }
...:
...: if n:
...: return heapq.nlargest(n, movies.items(), key=itemgetter(1))
...: return movies
工作原理...
predict_ranking方法将用户和电影以及指定距离度量的字符串一起使用,并为该特定用户返回该电影的预测收视率。 第四个参数critics旨在对predict_all_rankings方法进行优化,我们将在稍后讨论。 该预测会收集与用户相似的所有评论者,并计算评论者的加权总评分,并根据实际对相关电影进行评分的人进行过滤。 权重只是它们与用户的相似性,由距离量度计算得出。 然后,通过相似度的总和对总和进行归一化,以将等级移回 1 到 5 星的空间:
>>> print model.predict_ranking(422, 50, 'euclidean') 4.35413151722>>> print model.predict_ranking(422, 50, 'pearson') 4.3566797826
在这里,我们可以看到用户422对星球大战(在我们的 MovieLens 数据集中的 ID 50)的预测。 欧几里得和皮尔逊的计算非常接近(不一定是期望值),但是预测也非常接近用户的实际评分4。
predict_all_rankings方法根据传入的指标为特定用户计算所有电影的排名预测。 可以选择使用n值来返回前n个最佳匹配项。 此功能仅执行一次,然后将发现的评论者传递给predict_ranking函数,以优化性能,从而优化了类似评论者的查找。 但是,此方法必须在数据集中的每个电影上运行:
>>> for mid, rating in model.predict_all_rankings(578, 'pearson', 10): ... print "%0.3f: %s" % (rating, model.movies[mid]['title'])5.000: Prefontaine (1997)5.000: Santa with Muscles (1996)5.000: Marlene Dietrich: Shadow and Light (1996) 5.000: Star Kid (1997)5.000: Aiqing wansui (1994)5.000: Someone Else's America (1995)5.000: Great Day in Harlem, A (1994)5.000: Saint of Fort Washington, The (1993)4.954: Anna (1996)4.817: Innocents, The (1961)
如您所见,我们现在已经计算出推荐人认为该特定用户的热门电影的内容,以及我们认为该用户将对电影进行评分的内容! 平均电影收视率排名前 10 位的清单在这里起着巨大的作用,可能的改进可能是除了相似权重之外还使用贝叶斯平均法,但这留给读者来实现。
逐项协作过滤
到目前为止,我们已经将用户与其他用户进行了比较,以便做出我们的预测。 但是,相似性空间可以通过两种方式进行划分。 以用户为中心的协作过滤可在偏好空间中绘制用户的图,并发现用户之间的相似程度。 然后,将这些相似性用于预测排名,使用户与相似的评论家保持一致。 以项目为中心的协作过滤则相反。 它在首选项空间中将各个项目绘制在一起,并根据一组项目与另一组的相似程度提出建议。
基于项目的协作过滤是一项常见的优化,因为项目的相似性变化缓慢。 一旦收集到足够的数据,评论者添加评论并不一定会改变以下事实:玩具总动员与贝贝比 The Terminator 更相似,并且用户更喜欢 玩具总动员可能更喜欢前者。 因此,您只需在单个脱机流程中计算一次项目相似度,然后将其用作建议的静态映射,即可半定期更新结果。
此食谱将引导您完成逐项协作筛选。
准备
此食谱要求完成本章中的先前食谱。
怎么做...
构造以下函数以执行逐项协作过滤:
In [19]: def shared_critics(self, movieA, movieB):
...: """
...: Returns the intersection of critics for two items, A and B
...: """
...:
...: if movieA not in self.movies:
...: raise KeyError("Couldn't find movie '%s' in data" % movieA)
...: if movieB not in self.movies:
...: raise KeyError("Couldn't find movie '%s' in data" % movieB)
...:
...: criticsA = set(critic for critic in self.reviews if movieA in self.reviews[critic])
...: criticsB = set(critic for critic in self.reviews if movieB in self.reviews[critic])
...: shared = criticsA & criticsB # Intersection operator
...:
...: # Create the reviews dictionary to return
...: reviews = {}
...: for critic in shared:
...: reviews[critic] = (
...: self.reviews[critic][movieA]['rating'],
...: self.reviews[critic][movieB]['rating'],
...: )
...: return reviews
In [20]: def similar_items(self, movie, metric='euclidean', n=None):
...: # Metric jump table
...: metrics = {
...: 'euclidean': self.euclidean_distance,
...: 'pearson': self.pearson_correlation,
...: }
...:
...: distance = metrics.get(metric, None)
...:
...: # Handle problems that might occur
...: if movie not in self.reviews:
...: raise KeyError("Unknown movie, '%s'." % movie)
...: if not distance or not callable(distance):
...: raise KeyError("Unknown or unprogrammed distance metric '%s'." % metric)
...:
...: items = {}
...: for item in self.movies:
...: if item == movie:
...: continue
...:
...: items[item] = distance(item, movie, prefs='movies')
...:
...: if n:
...: return heapq.nlargest(n, items.items(), key=itemgetter(1))
...: return items
...:
工作原理...
为了执行逐项协作过滤,可以使用相同的距离度量,但是必须对其进行更新以使用来自shared_critics而不是shared_preferences的首选项(例如,项目相似性与用户相似性)。 更新函数以接受prefs参数,该参数确定要使用的首选项,但是我将其留给读者,因为它只有两行代码(请注意,答案包含在sim.py源文件中) 在包含第 7 章,“使用社交图谱(Python)”的代码的目录中)。
如果打印出特定电影的类似项目列表,则可以看到一些有趣的结果。 例如,查看 ID 为631的 The Crying Game (1992)的相似性结果:
for movie, similarity in model.similar_items(631, 'pearson').items(): print "%0.3f: %s" % (similarity, model.movies[movie]['title']) 0.127: Toy Story (1995) 0.209: GoldenEye (1995) 0.069: Four Rooms (1995) 0.039: Get Shorty (1995) 0.340: Copycat (1995) 0.225: Shanghai Triad (Yao a yao yao dao waipo qiao) (1995) 0.232: Twelve Monkeys (1995) ...
这部犯罪惊悚片与儿童电影《玩具总动员》的玩具总动员不太相似,但与另一部犯罪惊悚片的 Copycat 更相似。 当然,对许多电影进行评级的评论家会使结果歪曲,因此需要更多的电影评论才能使结果正常化。
假定项目相似性分数是定期运行的,但是不需要实时计算它们。 给定一组计算的项目相似性,计算建议如下:
In [21]: def predict_ranking(self, user, movie, metric='euclidean', critics=None):
...: """
...: Predicts the ranking a user might give a movie according to the
...: weighted average of the critics that are similar to the that user.
...: """
...:
...: critics = critics or self.similar_critics(user, metric=metric)
...: total = 0.0
...: simsum = 0.0
...:
...: for critic, similarity in critics.items():
...: if movie in self.reviews[critic]:
...: total += similarity * self.reviews[critic][movie]['rating']
...: simsum += similarity
...:
...: if simsum == 0.0: return 0.0
...: return total / simsum
该方法仅使用倒置的项间相似度评分,而不是用户间相似度评分。 由于类似项目可以离线计算,因此通过self.similar_items方法对电影的查找应该是数据库查找,而不是实时计算:
>>> print model.predict_ranking(232, 52, 'pearson') 3.980443976
然后,您可以按照与用户到用户的建议类似的方式计算所有可能建议的排名列表。
建立非负矩阵分解模型
协同过滤的基本横向最近邻相似性评分的一般改进是矩阵分解方法,该方法也称为奇异值分解(SVD)。 矩阵分解方法试图通过发现潜在特征来解释评级,而这些潜在特征是分析师难以识别的。 例如,此技术可以在电影数据集中显示可能的功能,例如动作量,家庭友善或微调的类型发现。
这些功能特别有趣的是,它们是连续的而不是离散的值,并且可以代表个人在连续体上的偏好。 从这个意义上讲,该模型可以探索特征的阴影,例如电影评论数据集中的评论家,例如在欧洲国家/地区具有强烈女性主导的动作轻弹。 詹姆斯·邦德(James Bond)的电影可能只是那种电影的阴影,尽管它只是在欧洲国家和动作类型框中打勾。 根据评论者对电影的评价程度,女性詹姆斯·邦德的实力将决定他们对这部电影的喜欢程度。
同样,非常有用的是,矩阵分解模型在稀疏数据(即很少有推荐和电影对的数据)上表现良好。 评论数据特别稀疏,因为并非每个人都对同一部电影进行评级,并且有大量可用的电影。 SVD 也可以并行执行,因此对于更大的数据集而言,它是一个不错的选择。
怎么做...
在本章的其余部分中,我们将建立一个非负矩阵分解模型,以改善我们的推荐引擎:
- 将整个数据集加载到内存中
- 将基于 SVD 的模型转储到磁盘
- 训练基于 SVD 的模型
- 测试基于 SVD 的模型
工作原理...
矩阵分解或 SVD 通过找到两个矩阵来工作,这样当您获取它们的点积(也称为内积或标量积)时,您将获得原始矩阵的近似值。 我们已将训练矩阵表示为电影的用户的稀疏 N x M 矩阵,其中该值是 5 星评级(如果存在),否则为空白或0。 通过用我们拥有的值对模型进行因子分解,然后取因式分解产生的两个矩阵的点积,我们希望在原始矩阵中填充空白点,从而预测用户如何评价电影的质量。 那一列。
直觉是应该有一些潜在特征来确定用户对项目的评分方式,而这些潜在特征是通过其先前评分的语义来表达的。 如果我们可以发现潜在功能,就可以预测新的评分。 此外,功能应该少于用户和电影(否则,每个电影或用户都是唯一的功能)。 这就是为什么我们在提取点乘积之前,将特征矩阵按某些特征长度进行组合。
在数学上,此任务表示如下。 如果我们有一组 U 个用户和 M 个电影,则让R的大小为 | U |。 x | M | 是包含用户评分的矩阵。 假设我们具有K潜在特征,请找到两个矩阵P和Q,其中P为 | U |。 x K 和Q是 | M | x K 使得P和Q的点积转置为R。P,因此表示用户与功能之间的关联强度,Q表示电影与功能之间的关联:

有几种方法可以进行因式分解,但是我们所做的选择是执行梯度下降。 梯度下降可初始化两个随机P和Q矩阵,计算它们的点积,然后通过沿误差函数的斜率(梯度)向下移动来将误差与原始矩阵相比最小化 。 这样,算法希望找到误差在可接受阈值内的局部最小值。
我们的函数将误差计算为预测值和实际值之间的平方差:

为了使误差最小,我们通过沿当前误差斜率的斜率下降来修改值 p [ik] 和 q [kj] ,相对于p产量:

然后,我们根据以下公式对变量q产生的误差方程进行微分:

然后,我们可以得出学习规则,该规则以恒定的学习率α更新P和Q中的值。 该学习率α不应太大,因为它确定了我们迈向最小值的步长,并且有可能越过误差曲线的另一侧。 它也不应太小; 否则,将需要永远收敛:

我们继续更新P和Q矩阵,将误差最小化,直到误差平方和低于某个阈值,代码中的0.001或执行了 最大迭代次数。
矩阵分解已成为推荐系统的一项重要技术,尤其是那些利用 Likert 标度样的偏好表达式(尤其是星级)的系统。 Netflix 奖挑战赛向我们展示了矩阵分解方法对于收视率预测任务的执行具有很高的准确性。 此外,矩阵分解是模型参数空间的紧凑,内存高效的表示形式,可以并行训练,可以支持多个特征向量,并且可以通过置信度进行改进。 通常,它们用于解决稀疏评论的冷启动问题,并且在具有更复杂混合动力的合奏中建议同时计算基于内容的推荐物。
另请参见
- Wikipedia 对点积的概述可从这个页面获得。
将整个数据集加载到内存中
建立非负因式分解模型的第一步是将整个数据集加载到内存中。 对于此任务,我们将充分利用 NumPy。
准备
为了完成此食谱,您必须从明尼苏达大学的 GroupLens 页面下载 MovieLens 数据库并解压缩 将其放在您的代码所在的工作目录中。 我们还将在此代码中大量使用 NumPy,因此请确保已下载并准备好此数值分析包。 此外,我们将使用先前食谱中的load_reviews功能。 如果您没有机会查看相应的部分,请准备该功能的代码。
怎么做...
要构建矩阵分解模型,我们需要为预测变量创建一个包装器,以将整个数据集加载到内存中。 我们将执行以下步骤:
- 如图所示,我们创建以下
Recommender类。 请注意,此类取决于先前创建和讨论的load_reviews函数:
In [22]: import numpy as np
...: import csv
...:
...: class Recommender(object):
...:
...: def __init__(self, udata):
...: self.udata = udata
...: self.users = None
...: self.movies = None
...: self.reviews = None
...: self.load_dataset()
...:
...: def load_dataset(self):
...: """
...: Loads an index of users and movies as a heap and a reviews table
...: as a N x M array where N is the number of users and M is the number
...: of movies. Note that order matters so that we can look up values
...: outside of the matrix!
...: """
...: self.users = set([])
...: self.movies = set([])
...: for review in load_reviews(self.udata):
...: self.users.add(review['userid'])
...: self.movies.add(review['movieid'])
...:
...: self.users = sorted(self.users)
...: self.movies = sorted(self.movies)
...:
...: self.reviews = np.zeros(shape=(len(self.users), len(self.movies)))
...: for review in load_reviews(self.udata):
...: uid = self.users.index(review['userid'])
...: mid = self.movies.index(review['movieid'])
...: self.reviews[uid, mid] = review['rating']
- 定义好这个后,我们可以通过键入以下命令来实例化模型:
data_path = '../data/ml-100k/u.data'model = Recommender(data_path)
工作原理...
让我们逐行浏览此代码。 推荐程序的实例化需要u.data文件的路径; 为我们的用户,电影和评论列表创建所有者; 然后加载数据集。 我们需要将整个数据集保存在内存中,其原因将在以后看到。
执行矩阵分解的基本数据结构是 N x M 矩阵,其中 N 是用户数,M 是电影数。 为此,我们首先将所有电影和用户加载到有序列表中,以便我们可以通过 ID 来查找用户或电影的索引。 在MovieLens的情况下,所有 ID 与1是连续的; 但是,并非总是如此。 拥有索引查找表是一种很好的做法。 否则,您将无法从我们的计算中获取建议!
有了索引查找列表后,我们将创建一个 NumPy 数组,该数组的全零是用户列表的长度乘以电影列表的长度。 请记住,行是用户,列是电影! 然后,我们第二次浏览收视率数据,然后将收视率的值添加到矩阵的uid和mid索引位置。 请注意,如果用户尚未给电影评分,则他们的评分为0。 这个很重要! 通过输入model.reviews打印出阵列,您应该看到类似以下的内容:
[[ 5\. 3\. 4\. ..., 0\. 0\. 0.][ 4\. 0\. 0\. ..., 0\. 0\. 0.][ 0\. 0\. 0\. ..., 0\. 0\. 0.]..., [ 5\. 0\. 0\. ..., 0\. 0\. 0.][ 0\. 0\. 0\. ..., 0\. 0\. 0.]
[ 0\. 5\. 0\. ..., 0\. 0\. 0.]]
还有更多...
通过向Recommender类添加以下两种方法,让我们了解数据集的稀疏程度:
In [23]: def sparsity(self):
...: """
...: Report the percent of elements that are zero in the array
...: """
...: return 1 - self.density()
...:
In [24]: def density(self):
...: """
...: Return the percent of elements that are nonzero in the array
...: """
...: nonzero = float(np.count_nonzero(self.reviews))
...: return nonzero / self.reviews.size
将这些方法添加到Recommender类中将有助于我们评估推荐者,也将有助于我们将来确定推荐者。
打印结果:
print "%0.3f%% sparse" % model.sparsity()print "%0.3f%% dense" % model.density()
您应该看到 MovieLens 100k 数据集的稀疏率为 0.937%,密度为 0.063%。
记住评论数据集的大小非常重要。 稀疏性在大多数推荐系统中很常见,这意味着我们也许可以使用稀疏矩阵算法和优化。 另外,当我们开始保存模型时,这将有助于我们从磁盘上的序列化文件加载模型时识别模型。
将基于 SVD 的模型转储到磁盘
在构建模型(这将花费很长时间进行训练)之前,我们应该创建一种机制以供我们将模型加载和转储到磁盘上。 如果我们有保存因子矩阵矩阵参数化的方法,那么我们可以重复使用我们的模型而不必每次都要使用它时就进行训练,因为这需要花费数小时的训练时间,所以这是非常重要的! 幸运的是,Python 有一个用于对 Python 对象进行序列化和反序列化的内置工具-pickle模块。
怎么做...
如下更新Recommender类:
In [26]: import pickle
...: class Recommender(object):
...: @classmethod
...: def load(klass, pickle_path):
...: """
...: Instantiates the class by deserializing the pickle.
...: Note that the object returned may not be an exact match
...: to the code in this class (if it was saved
...: before updates).
...: """
...: with open(pickle_path, 'rb') as pkl:
...: return pickle.load(pkl)
...:
...: def __init__(self, udata, description=None):
...: self.udata = udata
...: self.users = None
...: self.movies = None
...: self.reviews = None
...:
...: # Descriptive properties
...: self.build_start = None
...: self.build_finish = None
...: self.description = None
...:
...: # Model properties
...: self.model = None
...: self.features = 2
...: self.steps = 5000
...: self.alpha = 0.0002
...: self.beta = 0.02
...: self.load_dataset()
...:
...: def dump(self, pickle_path):
...: """
...: Dump the object into a serialized file using the pickle module.
...: This will allow us to quickly reload our model in the future.
...: """
...: with open(pickle_path, 'wb') as pkl:
...: pickle.dump(self, pkl)
工作原理...
@classmethod功能是 Python 中的装饰器,用于声明类方法而不是实例方法。 传入的第一个参数是类型而不是实例(我们通常将其称为self)。 load类方法采用磁盘上包含串行化pickle对象的文件的路径,然后使用pickle模块加载该文件。 请注意,在您运行代码时,返回的类可能与Recommender类不完全匹配,这是因为pickle模块保存的类(包括方法和属性)与转储时的完全相同。 它。
说到转储,dump方法提供了相反的功能,允许您将方法,属性和数据序列化到磁盘上,以便将来再次加载。 为了帮助我们识别要从磁盘转储和加载的对象,我们还为__init__函数添加了一些描述性属性,包括描述,一些构建参数和一些时间戳。
训练基于 SVD 的模型
现在,我们准备编写我们的函数,这些函数可以将我们的训练数据集纳入考虑范围并构建推荐模型。 您可以在此食谱中看到所需的功能。
怎么做...
我们构造以下函数来训练我们的模型。 请注意,这些功能不是Recommender类的一部分:
In [27]: def initialize(R, K):
...: """
...: Returns initial matrices for an N X M matrix, R and K features.
...:
...: :param R: the matrix to be factorized
...: :param K: the number of latent features
...:
...: :returns: P, Q initial matrices of N x K and M x K sizes
...: """
...: N, M = R.shape
...: P = np.random.rand(N,K)
...: Q = np.random.rand(M,K)
...:
...: return P, Q
In [28]: def factor(R, P=None, Q=None, K=2, steps=5000, alpha=0.0002, beta=0.02):
...: """
...: Performs matrix factorization on R with given parameters.
...:
...: :param R: A matrix to be factorized, dimension N x M
...: :param P: an initial matrix of dimension N x K
...: :param Q: an initial matrix of dimension M x K
...: :param K: the number of latent features
...: :param steps: the maximum number of iterations to optimize in
...: :param alpha: the learning rate for gradient descent
...: :param beta: the regularization parameter
...:
...: :returns: final matrices P and Q
...: """
...:
...: if not P or not Q:
...: P, Q = initialize(R, K)
...: Q = Q.T
...:
...: rows, cols = R.shape
...: for step in xrange(steps):
...: for i in xrange(rows):
...: for j in xrange(cols):
...: if R[i,j] > 0:
...: eij = R[i,j] - np.dot(P[i,:], Q[:,j])
...: for k in xrange(K):
...: P[i,k] = P[i,k] + alpha * (2 * eij * Q[k,j] - beta * P[i,k])
...: Q[k,j] = Q[k,j] + alpha * (2 * eij * P[i,k] - beta * Q[k,j])
...:
...: e = 0
...: for i in xrange(rows):
...: for j in xrange(cols):
...: if R[i,j] > 0:
...: e = e + pow(R[i,j] - np.dot(P[i,:], Q[:,j]), 2)
...: for k in xrange(K):
...: e = e + (beta/2) * (pow(P[i,k], 2) + pow(Q[k,j], 2))
...: if e < 0.001:
...: break
...:
...: return P, Q.T
工作原理...
我们讨论了先前食谱建立非负矩阵分解模型的理论和数学方法,下面让我们讨论一下代码。 initialize函数创建两个矩阵P和Q,它们的大小与评论矩阵和特征数量有关,即N x K和M x K,其中 N是用户数量,M是电影数量。 它们的值被初始化为0.0和1.0之间的随机数。 factor函数使用梯度下降来计算P和Q,以使P和Q的点积在小于0.001或5000步长的均方误差内 , 以先到者为准。 特别注意,仅计算大于0的值。 这些是我们试图预测的价值; 因此,我们不想在我们的代码中尝试匹配它们(否则,模型将在零评级下进行训练)! 这也是不能使用 NumPy 内置的奇异值分解(SVD)函数(即np.linalg.svd或np.linalg.solve)的原因。
还有更多...
让我们使用这些分解函数来构建模型并将模型构建后保存到磁盘中。通过这种方式,我们可以使用类中的dump和load方法在方便时加载模型。 将以下方法添加到Recommender类:
In [29]: def build(self, output=None, alternate=False):
...: """
...: Trains the model by employing matrix factorization on our training
...: data set, the sparse reviews matrix. The model is the dot product
...: of the P and Q decomposed matrices from the factorization.
...: """
...: options = {
...: 'K': self.features,
...: 'steps': self.steps,
...: 'alpha': self.alpha,
...: 'beta': self.beta,
...: }
...:
...: self.build_start = time.time()
...: nnmf = factor2 if alternate else factor
...: self.P, self.Q = nnmf(self.reviews, **options)
...: self.model = np.dot(self.P, self.Q.T)
...: self.build_finish = time.time()
...:
...: if output:
...: self.dump(output)
此辅助函数将使我们能够快速构建模型。 请注意,我们还保存了P和Q-我们潜在功能的参数。 这不是必需的,因为我们的预测模型是两个因子矩阵的点积。 决定是否在模型中保存此信息是重新训练时间(尽管您必须提防过大的可能,但可能要从当前的P和Q参数开始)和磁盘空间之间的权衡,例如[HTG4 保存这些矩阵的磁盘上的]会更大。 要构建此模型并将数据转储到磁盘,请运行以下代码:
model = Recommender(relative_path('../data/ml-100k/u.data'))model.build('reccod.pickle')
警告! 这将需要很长时间才能完成! 在配备 2.8 GHz 处理器的 2013 MacBook Pro 上,此过程花费了大约 9 个小时 15 分钟,并且需要 23.1 MB 的内存。 对于您可能习惯于编写的大多数 Python 脚本来说,这并不是无关紧要的! 在构建模型之前,继续完成本食谱的其余部分,这不是一个坏主意。 在继续进行整个过程之前,在 100 条记录的较小测试集中测试代码也可能不是一个坏主意! 此外,如果您没有时间训练模型,则可以在本书的勘误表中找到我们模型的pickle模块。
测试基于 SVD 的模型
本食谱使有关推荐引擎的这一章结束。 现在,我们将使用基于的新的基于非负矩阵分解的模型,并查看一些预测的评论。
怎么做...
利用我们的模型的最后一步是访问基于我们模型的电影的预测评论:
In [30]: def predict_ranking(self, user, movie):
...: uidx = self.users.index(user)
...: midx = self.movies.index(movie)
...: if self.reviews[uidx, midx] > 0:
...: return None
...: return self.model[uidx, midx]
工作原理...
计算排名相对容易; 我们只需要查找用户的索引和电影的索引,并在模型中查找预测的收视率。 这就是为什么在pickle模块中保存用户和电影的有序列表如此重要的原因; 这样,如果数据发生更改(我们添加了用户或电影),但是更改未反映在我们的模型中,则会引发异常。 由于模型是历史预测,并且对时间的变化不敏感,因此我们需要确保不断用新数据对模型进行重新训练。 如果我们知道用户的排名(例如,这不是预测),则此方法还会返回None;否则,此方法将返回None。 我们将在下一步中利用它。
还有更多...
要预测排名最高的电影,我们可以利用先前的功能为用户订购预测的最高排名:
In [31]: import heapq
...: from operator import itemgetter
...:
...: def top_rated(self, user, n=12):
...: movies = [(mid, self.predict_ranking(user, mid)) for mid in self.movies]
...: return heapq.nlargest(n, movies, key=itemgetter(1))
现在,我们可以打印出尚未被用户评价的顶级电影:
>>> rec = Recommender.load('reccod.pickle')>>> for item in rec.top_rated(234):... print "%i: %0.3f" % item 814: 4.4371642: 4.3621491: 4.3611599: 4.3431536: 4.3241500: 4.3231449: 4.2811650: 4.1471645: 4.1351467: 4.1331636: 4.1331651: 4.132
然后,只需使用电影 ID 在我们的电影数据库中查找电影即可。
九、使用 Python 采集和地理定位 Twitter 数据
在本章中,我们将介绍以下食谱:
- 创建一个 Twitter 应用
- 了解 Twitter API v1.1
- 确定您的 Twitter 关注者和朋友
- 拉 Twitter 的用户资料
- 在不违反 Twitter 速率限制的情况下发出请求
- 将 JSON 数据存储到磁盘
- 设置 MongoDB 以存储 Twitter 数据
- 使用 PyMongo 在 MongoDB 中存储用户配置文件
- 探索配置文件中可用的地理信息
- 在 Python 中绘制地理空间数据
简介
在本章中,我们将通过使用 RESTful Web 服务 API 进入社交媒体分析领域。 Twitter 是一个微博客社交网络,其流对于数据挖掘(尤其是文本挖掘)而言非常宝贵,并且它们具有出色的 API,我们将学习如何通过 Python 与之交互。 我们将使用该 API 来获取 Twitter 的社交联系,并使用传统的文件存储和流行的 NoSQL 数据库 MongoDB 来收集和存储 JSON 数据。 我们的分析将尝试确定连接的地理位置,并根据数据进行可视化处理。
在本章中,您应该开始注意到有关 API 的设计方式及其预期用途的模式。 与 API 的交互是一个非常重要的数据科学主题,对 API 的深入了解将为您打开一个全新的数据世界,您可以在该世界上进行大量的分析。
API 代表应用编程接口,在传统的计算机科学中,它是指允许软件应用彼此交互的方法。 如今,大多数对 API 的引用都指 Web API,它是互联网在您的软件应用和 Web 应用(例如 Twitter)之间共享数据的使用。 数据采集和管理是数据科学流程的重要组成部分,并且知道如何使用 API对于从互联网上获取可操作的数据集至关重要。
API 的一个特殊子集称为 RESTful API,实际上是大多数 Web 应用的基础,并且无处不在。 尽管我们可以避免大多数技术术语,但我们应该指出 REST 代表表示状态转移,这是一种以文档或对象作为表示形式存在的奇特方式,并且 对状态的修改应通过 API 进行。 RESTful API 是建立在万维网上的超文本传输协议(HTTP)的直接扩展,这就是为什么它们作为 Web API 如此流行的原因。 HTTP 允许客户端通过发出动词形式的请求来连接服务器:GET,POST,DELETE和PUT。 传统上,响应是一个 HTML 文档。 同样,RESTful API 使用这些动词来发出响应为 JSON 文档的请求。 前者用于人类消费(例如,当您访问这个页面之类的网站时),而后者则用于应用消费。
在本章中,我们将仅使用 HTTP GET请求和偶发的POST请求。 GET请求就像听起来一样。 它要求服务器为您提供特定资源。 另一方面,POST请求意味着客户端正在尝试向服务器提供数据(例如,提交表单或上载文件)。 API 提供程序(例如 Twitter)允许我们向特定资源 URL 发出 HTTP GET请求,该资源 URL 通常称为端点。 例如,特定用户的所有最新推文的GET端点是https://api.twitter.com/1.1/statuses/user_timeline.json。 如果我们对此端点发出经过正确身份验证的 HTTP GET请求,则 Twitter 将以 JSON 格式提供构成当前用户时间轴的数据。
创建一个 Twitter 应用
Twitter 是无处不在的微博社交媒体平台,截至 2014 年拥有 2.53 亿活跃会员。对于我们来说,Twitter 使该服务的数据比其他任何具有类似规模和地位的社交媒体网站更具开放性,并且可供第三方使用 。 此外,Twitter 提供了丰富且用户友好的 RESTful API,我们将广泛使用它。 此食谱将向您展示如何创建新的 Twitter 应用,这是以编程方式访问 Twitter 数据的必需步骤。
准备
确保已安装 Web 浏览器,然后打开一个新的浏览器选项卡或窗口。
怎么做...
以下步骤将引导您完成新的 Twitter 应用的创建:
注意
请注意,Twitter 确实希望经常更新其用户界面(UI),并且这些步骤或基于 Web 的形式可能会相应地发生变化。
-
首先,请确保您已经创建了一个 Twitter 帐户。 如果尚未创建一个,请访问这个页面并注册。 如果您有一个帐户,只需使用 Web 浏览器登录到您的 Twitter 帐户即可。
-
接下来,转到这个页面,然后在屏幕右侧选择标记为
Create New App的浅蓝色按钮:

-
在这里,它将提示您在三个必填字段和一个可选字段中输入您的应用详细信息。 为您的应用选择一个不超过 32 个字符的名称。
-
接下来,提供您的应用的简短描述,长度在 10 到 200 个字符之间。
-
您必须为您的应用提供一个网站,尽管该网站不适用于我们的用例。 此外,要成功提交表单,还需要一种特定的格式。 输入
http://127.0.0.1。 -
最后,您可以忽略
Callback URL字段,这是表单上的最后一个字段。 -
继续并花一些时间阅读
Developer Rules of the Road部分,因为本文档以纯文本和简单文本详细介绍了您应该使用和不应该使用应用进行的操作。 -
点击
Create your Twitter Application。 片刻之后,您应该在新应用的主设置页面上,并且在屏幕顶部有一个选项卡式菜单。 当前选项卡应标记为Details。 -
单击
Keys and Access Tokens选项卡,您应该看到以下屏幕截图:

- 现在,在底部的
Token操作灰色框中单击Create my access token,以授权您自己的帐户的应用(您可能需要多次单击按钮) 。 结果应类似于以下屏幕截图:

- 在文本文件中记录
API key,API secret,Access token和Access token secret。 这些很重要,但是必须像保护电子邮件密码或 ATM PIN 一样对它们进行保护。 您可以截图以保存信息,但是现在将这些值复制并粘贴到文本文件中会更加容易。
注意
现在您已经拥有 MinGW 和 MSYS,现在不再需要嫉妒那些安装 Linux 的用户,因为它们在您的系统中实现了 Linux 开发环境中最重要的部分。
工作原理...
您可能会问自己,如果我们要做的就是从 Twitter 提取一些简单数据,为什么我们需要创建一个应用。 Twitter API 的早期版本(1.0 及更低版本)允许应用发出匿名 API 请求,从而从 Twitter 检索数据,而 Twitter 却不知道谁在实际发出请求。 由于不推荐使用的公司 API 版本 1.0 已于 2013 年 6 月 11 日停用,因此所有对 Twitter 的 API 请求都需要进行身份验证。 这使 Twitter 可以跟踪谁要求什么信息以及要求多少信息。
通常,行业中有几种迹象表明,社交媒体数据收集的平静日子可能正在减弱。 Facebook 最新的 API 集,特别是 Facebook Login 2.0 版,已经强烈锁定了可以从社交图中获取哪些数据。 此外,Twitter 在 2014 年 4 月收购了 Gnip; Gnip 是 Twitter 数据的转售商,它允许其客户购买大部分 Twitter 数据。 此举表明,Twitter API 的 2.0 版可能会限制对 Twitter 诗歌的进一步访问。
另请参见
可以参考以下文章:
了解 Twitter API v1.1
API 既是福也是祸。 应用编程接口使从 Twitter,Facebook 或 LinkedIn 等服务收集数据变得更加容易,并且可以准确地定义公司想要和不想要的数据。 不幸的是,公司在访问其 API 时设置了速率限制,以控制可以收集的数据的频率(因此,数量)。 还众所周知,它们会从一个版本到另一个版本从根本上改变他们的 API,从而导致对依赖原始 API 的所有工作进行了大量的代码重写。 Twitter 的大型 API 从 1.0 版更改为 1.1 版,提供了一个警告。
Twitter 提供了三个主要 API:搜索 API,REST API 和 Streaming API。 搜索 API 为我们提供了一种编程方法,该方法可对 Twitter 进行查询,以检索历史内容,即推文。 REST API 提供对 Twitter 核心功能的访问,包括时间表,状态更新和用户信息。 最后,流 API 是实时 API,旨在低延迟访问 Twitter 的全球 Tweet 数据流。
使用流式 API,必须保持对 Twitter 开放的持久 HTTP 连接。 为了我们的数据挖掘和分析目的,这是过大的,因为我们只会定期从 Twitter 请求数据。 因此,我们将专注于前两个 API,而不必担心流 API。
准备
在上述配方中创建了应用并复制了密钥之后,就可以继续进行了。
怎么做...
执行以下步骤,以便使用 Python 以编程方式访问 Twitter API:
- 首先,安装
twython库。 打开一个新的命令提示符,然后键入以下内容:
(sudo) pip install twython
如果您当前的用户帐户没有足够的特权,则需要sudo命令。
- 接下来,打开一个新的终端并启动默认的 Python REPL 或 IPython。 如果您想加倍努力,也可以使用 IPython Notebook。
- 输入并执行以下 Python 代码,并填写所需的应用密钥:
In [1]: '''
...: Understanding the Twitter API v1.1
...: '''
...: from twython import Twython
In [2]: API_KEY = 'INSERT HERE'
...: API_SECRET = 'INSERT HERE'
...:
...: ACCESS_TOKEN = 'INSERT HERE'
...: ACCESS_TOKEN_SECRET = 'INSERT HERE'
In [3]: twitter = Twython(API_KEY, API_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
注意
请注意,Twitter 经常更新其开发人员界面,并且API_KEY以前称为CONSUMER_KEY。 另外,请注意,代码片段中给定的键需要替换为先前配方中收集的值。
- 如果使用的是 IPython,请在 REPL 上键入以下内容,然后单击选项卡键:
In [4]:twitter
这将带来令人印象深刻的 API 调用列表,这些列表现在可供您使用。

- 作为测试,我们可以在 Python 提示符下输入以下内容:
In [4]: temp = twitter.get_user_timeline()
此命令将从您的时间轴中获取最近的 20 个状态更新,作为包含 20 个元素的 Python 字典列表。
- 此外,Twython 使我们可以访问从 Twitter 收到的响应标头:
In [5]: twitter.get_lastfunction_header('x-rate-limit-remaining')
...:
Out[5]: '899'
工作原理...
使用前面的代码,我们将设置传递的 API 密钥和访问令牌,以实例化Twython类的实例。 这个新对象用作我们 Twitter API 的主要接口,默认情况下使用OAuth v1进行身份验证。 由于这是一个非常普遍的要求,因此我们可以将此功能包装在其自己的功能中,如以下代码片段所示。 在使用以下功能代码之前,请确保输入所需的应用密钥:
In [6]: def twitter_oauth_login():
...: API_KEY = 'INSERT HERE'
...: API_SECRET = 'INSERT HERE'
...: ACCESS_TOKEN = 'INSERT HERE'
...: ACCESS_TOKEN_SECRET = 'INSERT HERE'
...:
...: twitter = Twython(API_KEY, API_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
...: return(twitter)
return(twitter)
注意
如果您要将代码签入 GitHub 或其他基于云的版本控制解决方案(例如 Bitbucket),请检查该存储库是公共的还是私有的。 GitHub 上的所有免费存储库都是公开的。 如果您的存储库是公共的,那么全世界将可以访问您的秘密 Twitter API 密钥。 我们强烈建议您仅将私有存储库用于此类事项,并请注意 bitbucket.org 免费提供私有存储库。
OAuth 代表开放式身份验证协议,它允许用户向第三方应用授予访问该用户帐户的某些方面的权限(在这种情况下,其 Twitter 帐户 ),而不会放弃用户的登录名和密码。 深入介绍 OAuth 的工作原理不在本食谱的讨论范围之内。 但是,讨论 Twitter 应用的两种不同类型的资源身份验证很重要。 最常见的身份验证是应用用户身份验证,我们将不使用它。 在这种模式下,您的应用代表已向您的应用授予所需权限的用户发出请求。 对于我们的项目,我们关心仅应用身份验证,其中我们的应用不是针对用户而是针对自身发出 API 请求。 请注意,某些 API 调用不支持仅应用的请求,并且此类请求的速率限制通常不同。
还有更多...
Twython库不是唯一可用来简化对 Twitter API 的访问的 Python 库。 以下是三个受欢迎的选择,包括受欢迎的 Python Twitter Tools,您可以自由探索或选择自己认为合适的一个:
- Python Twitter 工具:这是 Twitter 的极简 Python API,其中包括一个命令行工具,以便结识朋友。 鸣叫并发送您自己的公告。
- Twython 3.1.2:这是一个纯 Python 包装器,它同时支持搜索和流式 API,这些 API 都得到了积极维护。 这是我们将要使用的库。
- python-twitter:这是当前 v1.1 Twitter API 的纯 Python 接口。
Twitter 在这个页面上维护了跨编程语言的替代列表。 请注意,本章中的代码仅使用 Twython,但是对于读者来说,使用其他 Python 库重写示例可能是一个有用的练习。
另请参见
您还可以参考:
确定您的 Twitter 关注者和朋友
在 Twitter 社交网络中,相对于特定用户,用户被标记为关注者或朋友。 您的朋友是您追随的人,而您的追随者是跟随您的人。 在此食谱中,我们确定谁是您的朋友,谁是追随者,以及每个组中有多少重叠。
准备
对于此配方,我们将使用前两个配方的结果以及twitter_oauth_login()函数。 另外,如果您愿意的话,我们将使用 IPython 或默认的 Python REPL。 随便使用编辑器即可开始捕获和修改代码,因为代码变得越来越复杂。
怎么做...
以下步骤将使您能够确定所有 Twitter 朋友和关注者:
- 在 IPython 或您喜欢的 REPL 中,输入以下内容:
In [8]: twitter = twitter_oauth_login()
...: friends_ids = twitter.get_friends_ids(count=5000)
...: friends_ids = friends_ids['ids']
In [9]: followers_ids = twitter.get_followers_ids(count=5000)
...: followers_ids = followers_ids['ids']
- 收集了您所有的关注者和朋友的 Twitter ID 后,让我们看看您有多少:
In [10]: len(friends_ids), len(followers_ids)
...:
Out[10]: (22, 40)
- 我们将使用基于您可能在数学课程中遇到过的 Python 集的 Python 集,来检查我们的朋友和关注者的某些属性:
In [11]: friends_set = set(friends_ids)
...: followers_set = set(followers_ids)
...:
In [12]: print('Number of Twitter users who either are our friend or follow you (union):')
...: print(len(friends_set.union(followers_set)))
...:
Number of Twitter users who either are our friend or follow you (union):
56
In [13]: len(friends_set | followers_set)
...: len(set(friends_ids+followers_ids))
...:
Out[13]: 56
In [14]: print('Number of Twitter users who follow you and are your friend (intersection):')
...: print(len(friends_set & followers_set))
...:
Number of Twitter users who follow you and are your friend (intersection):
6
In [15]: print("Number of Twitter users you follow that don't follow you (set difference):")
...: print(len(friends_set - followers_set))
...:
In [16]: Number of Twitter users you follow that don't follow you (set difference):
16
print("Number of Twitter users who follow you that you don't follow (set difference):")
...: print(len(followers_set - friends_set))
...:
Number of Twitter users who follow you that you don't follow (set difference):
34
上面的代码片段应产生以下输出:

注意
上图中截屏中显示的数字很可能会根据您拥有的朋友和关注者的数量而有所不同。
工作原理...
此食谱展示了twython软件包的实用性以及完成某些任务的容易程度。 使用twitter_oauth_login功能登录后,我们使用twitter对象进行了两个基本调用,一个用于获取朋友的 ID,一个用于获取关注者的 ID。 请注意,我们将 count 参数设置为 5000,这是 Twitter API 允许的最大值。 twitter对象返回了一个字典,我们从中提取了实际的 ID。
Twython 界面的优点之一是它与 Twitter API 的镜像程度。 如果您对特定功能有疑问,只需查看 Twitter 文档。
收集好朋友和关注者 ID 的列表后,我们将使用 Python 集合类型进行快速的肚脐注视。 Python set类型是从版本 2.4 开始内置到 Python 中的,它是唯一无序对象的集合。 对我们而言,关键词是唯一。 如果我们从具有重复项的列表中创建集合,则将获得仅包含唯一元素的集合; set([1, 2, 2, 3, 3, 3])将返回{1, 2, 3}。
我们将朋友的 ID 集与关注者的 ID 结合起来,以确定跟随或跟随我们的 Twitter 用户的唯一 ID 的总集合。 在前面的代码中,我们使用set类型的 union 方法,但是还有几种其他方法可以实现此目的:
(friends_set | followers_set)
(set(friends_ids + followers_ids))
还有更多...
尽管 Twython 的精美抽象掩盖了使用 API的某些复杂性,但是如果我们不了解幕后实际发生的事情,那么这种简单性或魔术性可能会成问题。 当我们调用twitter.get_friends_ids(count=5000)方法时,我们正在将 HTTP GET 请求发送到特定的 URL。 在twitter.get_friends_ids()的情况下,URL 为https://api.twitter.com/1.1/friends/ids.json。
函数调用的count=5000输入参数在 URL 中显示为字段值对,因此 URL 变为https://api.twitter.com/1.1/friends/ids.json?count=5000。
现在,实际的 API 端点需要 Twython 为我们填写的一些默认参数值,为清楚起见,如以下 URL 所示:
https://api.twitter.com/1.1/friends/ids.json?cursor=-
1&screen_name=sayhitosean&count=5000
Twitter v1.1 API 要求使用 OAuth 对所有请求进行身份验证。 所需的信息实际上嵌入在 GET 请求的标头中,并且构造适当的标头的过程非常广泛(有关更多信息,请转到这个页面)。 因此,Twython 不仅形成了发出请求的正确 URL,而且还处理了相对痛苦的“开放授权”,因此我们不必这样做。 如果您有兴趣,可以使用更高级的请求库或您选择的替代方法,下一层并构建自己的 GET 请求。 我们将其留给读者探索。
注意
请注意,不同的 Twitter API 端点具有不同的速率限制。 对于 GET 朋友/ ID,截至 2014 年 5 月,我们仅允许在 15 分钟内对 API 1.1 版进行 15 次调用。其他端点对其数据的态度不太严格。
另请参见
您还可以参考以下内容:
提取 Twitter 用户个人资料
对于本食谱,我们将使用 Twitter API 提取有关 Twitter 用户的 JSON 数据。 由屏幕名称(例如SayHiToSean)或唯一整数标识的每个 Twitter 用户,其配置文件均包含有关某人的丰富信息集。
准备
您将需要上一个配方确定您的 Twitter 关注者和朋友的食谱中的关注者和朋友的 ID 的列表。
怎么做...
以下步骤将指导您检索一组 Twitter 用户的个人资料:
- 首先,我们创建一个函数来管理拉动 Twitter 资料:
In [18]: def pull_users_profiles(ids):
...: users = []
...: for i in range(0, len(ids), 100):
...: batch = ids[i:i + 100]
...: users += twitter.lookup_user(user_id=batch)
...: print(twitter.get_lastfunction_header('x-rate-limit-remaining'))
...: return (users)
- 我们使用此功能,提取朋友和关注者的个人资料:
In [19]: friends_profiles = pull_users_profiles(friends_ids)
...: followers_profiles = pull_users_profiles(followers_ids)
899
898
- 为了检查一切是否正常,我们使用列表推导从个人资料中提取所有朋友的屏幕名称:
In [20]: friends_screen_names = [p['screen_name'] for p in friends_profiles]
- 使用以下命令,您应该会看到朋友的屏幕名称列表:
In [21]: friends_screen_names
...:
Out[21]:
['nammamechanik',
'Ruchir78',
'nspothnis',
'jdelaney666',
'zakaas4u',
'arunkumar_n_t']
工作原理...
本食谱的第一步是创建一个管理twitter.lookup_user方法调用的函数。 Twitter 用户/查找端点一次接受 100 个用户 ID。 因此,我们需要遍历朋友或关注者 ID 的列表,并将其分为 100 个组以用于请求。 Twitter 返回一个 JSON 对象,Twython 将该对象转换为可以使用的 Python 字典列表。
有另一种检索配置文件的方法。 我们可以直接使用当前用户的屏幕名称(在本例中为@SayHiToSean)查询用户的朋友/列表端点,而不是拉出朋友和关注者的 ID,然后使用这些 ID 来请求用户个人资料。 。 然后,Twitter 将为每个请求返回多达 200 个用户个人资料。 如果您计算出 API 限制,则任一路径都将计算出与 Twitter 用于限速目的的 15 分钟默认时间窗口中提取的用户配置文件数目相同的数目。
还有更多...
我们创建的pull_users_profiles函数在循环的最后一行有一个附加功能:
print(twitter.get_lastfunction_header('x-rate-limit-remaining'))
我们从上一次 API 调用中检索响应的标头,然后检查x-rate-limit-remaining值。 该值准确地告诉我们在给定的 15 分钟窗口内,我们还剩下多少个 API 调用。 尽管我们在每个循环中都打印出了该值,但我们绝对不会采取任何措施阻止我们猛烈抨击 Twitter 的速率限制,该限制因端点而异。
此外,如果由于某种原因收到的一个 Twitter 用户配置文件没有screen_name密钥,我们在步骤 3 中使用的列表理解可能会失败。 因此,最好在理解中增加一个条件:
In [22]: friends_screen_names = [p['screen_name'] for p in friends_profiles if 'screen_name' in p]
或者,作为一种替代方法,也可能是一种更 Python 化的方法,我们可以使用字典的 GET 方法:
In [23]: friends_screen_names = [p.get('screen_name',{}) for p in friends_profiles]
在这种情况下,不跳过没有screen_name键的配置文件,而是将其替换为None。
另请参见
您还可以参考以下内容:
在不违反 Twitter 速率限制的情况下发出请求
对于此配方,我们将修改在先前配方中创建的功能拉动 Twitter 用户配置文件,以避免达到可怕的 Twitter API 速率限制。
准备
您将再次需要来自上一个配方的拉 Twitter 用户配置文件以及经过身份验证的 Twython 对象的关注者和朋友的 ID 列表。
怎么做...
以下功能演示了如何以感知速率限制的方式检索一组 Twitter 用户的配置文件:
In [25]: import time
...: import math
In [26]: rate_limit_window = 15 * 60 #900 seconds
In [27]: def pull_users_profiles_limit_aware(ids):
...: users = []
...: start_time = time.time()
...: # Must look up users in
...: for i in range(0, len(ids), 10):
...: batch = ids[i:i + 10]
...: users += twitter.lookup_user(user_id=batch)
...: calls_left = float(twitter.get_lastfunction_header('x-rate-limit-remaining'))
...: time_remaining_in_window = rate_limit_window - (time.time()-start_time)
...: sleep_duration = math.ceil(time_remaining_in_window/calls_left)
...: print('Sleeping for: ' + str(sleep_duration) + ' seconds; ' + str(calls_left) + ' API calls remaining')
...: time.sleep(sleep_duration)
...: return (users)
工作原理...
此功能是先前配方功能的修改版本,该功能提取用户的个人资料,因此我们不会违反 Twitter 无处不在的速率限制。 我们在循环的每个迭代中插入一个动态暂停,其长度由时间窗口中剩余的 API 调用次数决定。 在循环开始之前,我们在start_time变量中捕获当前系统时间。 在twitter对象进行每个 API 调用之后,我们获取响应的标头并检查 15 分钟时间窗口中剩余的 API 调用数。 我们计算从start_time开始经过的时间,然后将其从 900 秒中减去,得出 15 分钟窗口中剩余的时间。 最后,我们计算剩余的每个 API 调用所需的秒数,并在所需的时间段内进行睡眠。 我们使用math.ceil函数进行四舍五入,并确保始终提供一点额外时间,以免达到速率限制。
您可能会问,为什么有人会关心达到 Twitter API 速率限制。 为什么即使达到限制后也不要继续点击 API? 简单的答案是,Twitter 可以并且将阻止过于频繁使用规定速率限制的应用。 因此,遵守规则符合您的最大利益。 此外,一旦超过速率限制,您将无法获取任何其他信息,那么为什么要打扰呢?
将 JSON 数据存储到磁盘
就带宽和服务提供商对其 API 设置的速率限制而言,对 API 的调用可能会非常昂贵。 尽管 Twitter 对这些限制非常宽容,但其他服务却并非如此。 无论如何,优良作法是将检索到的 JSON 结构保存到磁盘以供以后使用。
准备
对于此配方,您将需要以前检索的数据,最好是从以前的配方中检索的数据。
怎么做...
以下步骤指导我们将 JSON 数据保存到磁盘,然后将其重新加载到 Python 解释器的内存中:
- 首先,我们必须导入
json包并创建两个帮助器函数:
In [31]: import json
...: def save_json(filename, data):
...: with open(filename, 'wb') as outfile:
...: json.dump(data, outfile)
In [32]: def load_json(filename):
...: with open(filename) as infile:
...: data = json.load(infile)
...: return data
- 在 Python 提示符下,让我们通过将朋友的基于 JSON 的 Twitter 配置文件保存到磁盘来测试我们的功能:
In [33]: fname = 'test_friends_profiles.json'
...: save_json(fname, friends_profiles)
- 检查以确保该文件已创建。 如果使用的是 IPython,只需键入
ls或打开 Terminal shell,转到当前目录,然后键入ls。 您应该在当前目录中看到test_friends_profiles.json。 - 现在,让我们将文件加载回我们的 Python 工作区:
In [34]: test_reload = load_json(fname)
...: print(test_reload[0])
工作原理...
json库是 Python 标准库的一部分,提供了一个简单但有效的 JSON 编码器和解码器。 通过save_json函数写入文件时,我们使用json.dump方法以默认的 UTF-8 编码将数据对象(在本例中为 Python 字典)序列化为 JSON 格式的流,并将其发送到 超出档案。 相反,load_json函数使用json.load,该函数将 infile 反序列化为 Python 对象。
设置用于存储 Twitter 数据的 MongoDB
REST API 的默认响应格式是 JSON,因此,最容易将此数据存储为 JSON,以避免不必要的数据争用。 尽管有许多不同的数据库和数据存储区可以处理 JSON 数据,但我们希望选择一种相对易于设置,本地处理 JSON 数据,免费使用且相对流行的数据库。 因此,我们将使用 MongoDB。
准备
对于此食谱,您将需要在本地计算机上下载 MongoDB,因此请确保您具有与 Internet 的宽带连接。
怎么做...
以下步骤将引导您设置 MongoDB 并通过命令外壳程序使用它:
- 此阶段的第一步是安装 MongoDB。 最简单的方法是从这个页面下载最新的二进制发行版(当前为 3.4)。 适用于 Windows,Linux,macOS X 和 Solaris 的 64 位二进制发行版。
- 下载后,请按照这个页面上的相关安装指南进行操作。
- 接下来,我们需要通过在命令提示符处键入
mongod来启动 MongoDB。 - 随着数据库的运行,我们需要通过随附的 mongo shell 连接到它。 为此,请打开另一个终端窗口或命令行,然后键入以下内容:
mongo
- 此命令假定 MongoDB 在端口
27017和本地主机上运行。 如果不是这种情况,请如图所示启动 mongo shell,并指定正确的主机地址和端口号:
mongo address_of_host:port_number
- 现在我们正在运行 mongo shell,我们可以开始工作了。 让我们创建一个名为
test的数据库,因此输入以下内容:
use test
- 创建
test数据库后,我们需要创建 tweets 集合,该集合实际上将存储我们将要收获的所有 tweet。 为此,请使用以下命令:
db.createCollection('user_profiles')
- 我们要检查是否创建了集合,因此我们必须首先使用以下命令切换到当前数据库:
use test
- 要求
mongoshell 显示此数据库中的所有集合,然后您将在本地数据库中收到一个简单的集合列表:
show collections
工作原理...
在这个简单明了的配方中,我们为如何使用流行的 MongoDB 奠定了基础。 我们已经安装并运行 MongoDB,并已使用 mongo shell 对其进行了连接。 此外,我们已经命名了一个新数据库test,并创建了一个名为user_profiles的文档集合。 MongoDB 中的集合是 MongoDB 文档的分组,这些文档在某种程度上类似于关系数据库中的表,例如Postgres。 这些文档通常在结构和目的上相似,但是与关系数据库不同,它们不必完全统一,并且可以随时间轻松发展。 就我们的目的而言,一组 Twitter 用户的个人资料构成了一个很好的集合。
就个人而言,我们不希望在后台或登录时运行mongod进程,因此我们从命令行启动 MongoDB。 这样,当我们不使用 MongoDB 时,它就不会在后台运行,从而消耗 CPU 周期或消耗电池。
还有更多...
MongoDB 并不是唯一一个非常适合 JSON 数据的 NOSQL 文档存储,并且存在许多替代方法,包括 CouchDB。 Basho 的大多数键/值存储(例如 Amazon 的 Dynamo 或 Riak)都非常适合以相对最小的设置和配置存储 JSON 数据。 借助 Amazon 的 Dynamo,您还将获得额外的好处,因为它是完全基于云的按需付费解决方案,几乎可以无限扩展。 最后,包括 Postgres 在内的一些关系数据库本机支持 JSON 数据类型,并对数据执行错误检查,以确保所存储的数据为有效 JSON。 但是,Postgres 的设置和配置往往比 MongoDB 更具挑战性。
MongoDB 的另一个优点(尽管不是排它的优点)是存在免费的托管平台即服务选项。 换句话说,您可以配置一个完整配置的在云中运行的 MongoDB 数据库,而无需做其他任何事情,而只需浏览一个非常简单的基于 Web 的界面即可登录并创建数据库。 最好的部分是 MongoLab 和 MongoHQ 都提供了免费的服务层,这意味着您无需支付任何费用即可在云中设置和使用自己的 MongoDB 数据库!
注意
自编写本节以来,作者有机会测试和使用 RethinkDB(http://rethinkdb.com/),这是一个相对较新的开源分布式数据库,可以轻松处理 JSON 数据类型。 提供跨文档表的连接,就像关系数据库一样。 如果我们要重写本章,我们将使用 RethinkDB。
另请参见
您还可以参考以下内容:
使用 PyMongo 在 MongoDB 中存储用户配置文件
检索到用户配置文件数据并安装 MongoDB 并准备好采取行动之后,我们需要将用户配置文件 JSON 存储到适当的集合中,并且我们希望从 Python 脚本中而不是使用 mongo shell 进行操作。 为此,我们将使用 PyMongo,这是 MongoDB 员工本身推荐的从 Python 使用 MongoDB 的方法。 截至 2014 年 1 月,PyMongo 的版本为 2.6.3。
准备
您必须已经安装了 MongoDB,并且已经准备好一些示例用户配置文件数据以准备使用此配方。
怎么做...
以下步骤将指导您将 Python 词典另存为 MongoDB 中的 JSON 文档:
- 首先,我们必须在系统上安装 PyMongo。 在命令行提示符下,键入以下内容:
pip install pymongo
- 根据您当前的用户权限,您可能必须在以下命令中使用
sudo:
sudo pip install pymongo
- 如果上述安装无法正常工作并报告错误,请在线访问这个页面,查看更详细的说明,因为其中可能存在 C 依赖项 可能必须单独编译,具体取决于您的系统。
- 安装 PyMongo 后,进入 Python,IPython 或 IPython Notebook 会话并输入以下内容:
In [36]: import pymongo
In [37]: host_string = "mongodb://localhost"
...: port = 27017
...: mongo_client = pymongo.MongoClient(host_string, port)
In [38]: mongo_db = mongo_client['test']
In [39]: user_profiles_collection = mongo_db['user_profiles']
In [40]: user_profiles_collection.insert(friends_profiles)
...: user_profiles_collection.insert(followers_profiles)
工作原理...
安装pymongo之后,不需要很多步骤就可以将我们连接到本地 MongoDB 数据库中,然后将其存储在 JSON 中。
我们首先创建了一个 MongoClient,它连接到主机字符串和端口中指定的mongod。 然后,我们使用字典式访问来访问所需的mongo_db数据库(在本例中为test)和特定集合(在本例中为user_profiles)。 我们调用集合的insert方法,并将其传递给 JSON 数据进行保存。 为此,我们将为新存储的对象接收一个ObjectID或一个ObjectIDs列表。
这里有很多事情要注意。 我们选择使用字典样式的访问(mongo_client['test']),以防数据库名称包含诸如-之类的字符,以防止属性样式访问数据库(client.test)。 另外,请注意,在将文档实际存储在集合中之前,MongoDB 不会执行任何操作。
或者,我们可以将前面的命令包装在一个函数中,以方便以后重用。 在以下命令中,save_json_data_to_mongo可以获取单个 JSON 文档或 JSON 文档的可迭代列表以及访问 MongoDB 中特定数据库和集合所需的规范。 host_string参数默认为localhost,端口默认为 27017:
In [41]: def save_json_data_to_mongo(data, mongo_db,
...: mongo_db_collection,
...: host_string = "localhost",
...: port = 27017):
...: mongo_client = pymongo.MongoClient(host_string, port)
...: mongo_db = mongo_client[mongo_db]
...: collection = mongo_db[mongo_db_collection]
...: inserted_object_ids = collection.insert(data)
...: return(inserted_object_ids)
我们可以通过检查 JSON 文档的数量是否与返回的 ObjectID 的数量相匹配来改进此功能,但是我们将把练习留给读者。
探索配置文件中可用的地理信息
Twitter 用户的个人资料包含两个不同的潜在地理信息来源:个人资料本身和最近发布的状态更新。 我们将在本食谱中利用这两个选项,着眼于在构建我们的朋友的地理可视化中的可用性。
准备
您将需要按照以前的食谱中的指示,从 Twitter 获取收获的朋友和/或关注者的个人资料。
怎么做...
执行以下步骤来提取我们需要的地理数据,以可视化我们的连接的大致位置:
- 我们从 IPython 或您最喜欢的 Python REPL 开始此练习。 从文件中加载朋友的个人资料:
In[1]: fname = 'test_friends_profiles.json'
In[2]: load_json(fname)
- 接下来,我们从用户个人资料数据结构中
geo_enabled字段的所有值中为我们的朋友构建列表。 然后,我们使用count方法查找将geo_enabled标志设置为true的用户个人资料的数量:
In[3]: geo_enabled = [p['geo_enabled'] for p in friends_profiles]
In[4]: geo_enabled.count(1)
Out[4]: 127
- 我们重复与第二步中使用的过程非常相似的过程,以计算有多少朋友的用户个人资料的
location字段为空白:
In[5]: location = [p['location'] for p in friends_profiles]
In [6]: location.count('')
Out[6]: 79
- 为了获得用户配置文件的
location字段中包含的数据的快速 ID,我们打印出一个唯一列表并注意数据的混乱情况。 似乎location是一个自由文本字段,这非常令人失望:
In[7]: print(set(location))
Out[7]:
...
u'Washington D.C.',
u'Washington DC',
u'iPhone: 50.122643,8.670158',
u'london',
u'new world of work',
u'san francisco',
u'seattle, wa',
u'usually in Connecticut',
u'washington, dc',
...
- 现在,我们将注意力转向
time_zone字段:
In[8]: time_zone = [p['time_zone'] for p in friends_profiles]
In[9]: time_zone.count(None)
Out[9]: 62
In[10]: print(set(time_zone))
Out[10]: {None, u'Alaska', u'Amsterdam', u'Arizona', u'Atlantic Time (Canada)', u'Berlin',
...
- 最后,由于每个用户个人资料都包含该用户的最新状态更新(或推文),因此我们要检查用户对这些推文中的多少进行了地理标记。 请注意列表理解的逻辑和条件部分。 如果这些键存在于数据结构中,我们只需要
p['status']['geo']:
In[11]: status_geo = [p['status']['geo'] for p in friends_profiles if ('status' in p and p['status']['geo'] is not None)]
In [12]: if status_geo: print status_geo[0]
Out[12]: {u'coordinates': [38.91431189, -77.0211878], u'type': u'Point'}
In[13]: len(status_geo)
Out[13]: 13
工作原理...
在此食谱中,我们使用列表推导来提取包含在朋友的用户个人资料中的潜在有用的地理信息。 对于每个数据元素,我们要问两个问题:
- 由于覆盖范围很重要,因此多少百分比的配置文件包含此信息?
- 概要文件中可用的信息的格式是什么,因此有多有用?
我们发现,大约 80%的配置文件在其配置文件中设置了位置(非常有希望),而更高比例的配置了时区(甚至是更粗糙的粒状地理指标)。 在用户个人资料数据中捕获的最新状态更新中,即使三分之一的用户个人资料(127/352)的geo_enabled设置为True,也有 90%以上未进行地理编码,这意味着用户有时可能会选择 到地理编码一条推文。 因此,如果我们为朋友收集了历史推文,那么我们最多应该能够找到其中约三分之一的位置。
建立了覆盖范围后,我们回顾了可用的实际数据,该数据描绘了该问题的更复杂图景。 用户配置文件的位置特征在大多数配置文件中都可用,但这只是内容的混乱。 有些位置是经度和纬度值(非常有用),有些是可识别的文本字符串地址,有些是城市和州,有些不是传统的地址格式,甚至不是位置。 时区数据似乎没有那么明显的问题,但是我们仍然必须确保 Twitter 捕获的时区名称清晰,明确地映射到实际时区。
还有更多...
如果我们想在 Twitter 上绘制我们的朋友,可以使用以下几种方法进行:
- 如果我们使用带有地理标记的推文的坐标,则可以立即跳至绘图。 这里唯一需要关注的是数据的稀疏性。 为了改善这种情况,我们应该为每个朋友拉动推文,以发现更多带有地理标签的推文。 这样做,我们希望能发现多达三分之一的朋友的地理数据。
- 尽管位置特征非常混乱,但是我们可以通过地理编码服务(例如 Google Maps 或 Bing)运行结果,然后看看会发生什么。 考虑到用户个人资料中某些位置的创造力,这可能不是最有效的途径。 或者,我们可以尝试使用正则表达式提取状态缩写或邮政编码,但这也将有些麻烦且耗时。
- 绘制不同时区的朋友数非常有趣,而且看起来这些数据可能很容易提取。 一个相关的问题是绘制时区有多困难?
另请参见
您也可以在这个页面上参考用户 Twitter 文档。
用 Python 绘制地理空间数据
Python 的最大优势之一就是可用软件包的数量和多样性,这些软件包使许多复杂的任务变得简单,因为其他人已经编写了大多数代码。 结果,我们有时会遇到选择的悖论,太多的选择使问题变得困惑,我们只想选择一个好的选择。 在本食谱中,我们将使用一个出色的 Python 包folium绘制一组纬度和经度坐标:folium-包装一个 JavaScript 库,即leaflet.js。 您将在食谱中进一步了解有关叶的信息。
准备
您将需要以前食谱中提取的地理数据(一组经度和纬度坐标)。 另外,我们需要安装folium软件包,如以下部分所示,因此您需要 Internet 连接。
怎么做...
以下步骤将帮助您转换必须在地图上绘制的纬度和经度数据:
- 打开您的终端。 我们需要安装 Python 软件包 folium:
(sudo) pip install folium
- 转到您的源目录,然后启动 IPython 或您喜欢的 Python REPL。 我们首先需要创建两个列表,其中将包含地理数据和将用于标签的关联屏幕名称。 以下代码与上一食谱探索配置文件中可用的地理信息非常相似,但表示为循环:
status_geo = []
status_geo_screen_names = []
for fp in friends_profiles:
if ('status' in fp and fp['status']['geo'] is not None and
'screen_name' in fp):
status_geo.append(fp['status']['geo'])
status_geo_screen_names.append(fp['screen_name'])
- 现在,我们将导入我们需要的两个库:
In [44]: import folium
...: from itertools import izip
- 我们实例化
Map对象,设置所需的初始视图位置和缩放级别,在循环中向地图添加标记和标签,最后将地图呈现为 HTML:
In [44]: map = folium.Map(location=[48, -102], zoom_start=3)
...: for sg, sn in izip(status_geo, status_geo_screen_names):
...: map.simple_marker(sg['coordinates'], popup=str(sn))
...: map.create_map(path='us_states.html')
- 现在,您的工作目录中应该有一个 HTML 文件。 双击它,您应该看到与以下屏幕截图类似的内容:

工作原理...
令人印象深刻的是,我们从不到十二行代码转变为完全交互式的地图,并配有地理标记,这些地理标记表示在 Twitter 上关注我们的某些用户的位置。 为此,我们利用了 folium Python 程序包的功能,该程序包又是leaflet.js JavaScript 库的 Pythonic 包装器。 Folium 允许我们将数据从 Python 绑定到地图上,以实现整体可视化或在特定地图位置渲染标记,就像在第四步中所做的那样。
Folium 使用 Jinja2 模板包创建一个 HTML 文件,该文件包含一个非常简单的 HTML 页面,该页面带有一个div容器,其中包含地图和自定义 JavaScript 代码,以便使用leaflet.js库。 leaflet.js库处理实际的地图生成:
<!DOCTYPE html>
<head>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-
0.5/leaflet.css" />
<script src="http://cdn.leafletjs.com/leaflet-
0.5/leaflet.js"></script>
<style>
#map {
position:absolute;
top:0;
bottom:0;
right:0;
left:0;
}
</style>
</head>
<body>
<div id="map" style="width: 960px; height: 500px"></div>
<script>
var map = L.map('map').setView([48, -102], 4);
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: 'Map data (c) <a
href="http://openstreetmap.org">OpenStreetMap</a>
contributors'
}).addTo(map);
var marker_1 = L.marker([38.56127809, -76.04610616]);
marker_1.bindPopup("Pop Text");
map.addLayer(marker_1)
</script>
</body>
我们强烈建议您仔细研究 folium(和leaflet.js)的其他功能以及该库的基础源代码。
还有更多...
可视化地理数据很复杂。 您不仅需要渲染一个或多个复杂坐标系的能力,还需要原始的地图数据,这些数据定义了国家,道路,河流以及除了您自己的数据外可能还要显示的任何其他内容。 幸运的是,Python 在解决此问题时提供了很多选择。
这些软件包通常分为两大类。 一流的地理空间可视化软件包来自数十年来一直存在的科学研究需求。 对于此软件,渲染器和所需的地理数据位于用户的本地计算机上。 为了使 Python 程序包正常工作,通常需要编译和安装该软件的其他几个层。
非常强大的 Matplotlib 底图工具包就是一个很好的例子。 浏览说明,我们看到许多依赖项。 尽管我们已经处理过 matplotlib 和 NumPy,但另外两个要求-GEOS和Proj4 map库可能会引起问题。 几何引擎-开源(GEOS)的 C ++ 源代码实际上包含在底图中,但是必须单独编译和构建。 这需要用另一种语言编译软件是一个巨大的危险信号。 对于 Python 软件包,这通常会导致一个麻烦,一个可能需要花费数小时或数天才能解决的编译错误和技术问题(缺少头文件,稍微不标准的目录结构,错误的编译器等)。 如果您在 Mac OS X 上,尤其是在 Mavericks 上,那么您会获得更多乐趣,因为 Apple 倾向于在每个发行版中进行一些细微的改动,这会破坏以前可以正常使用的 make 文件。 结果,我们从类别 2 中选择了一个映射库,其描述如下。
程序包的第二个类提供了与 JavaScript 库进行交互的 Python 方式,该 JavaScript 库实际上处理地图和数据的呈现。 这通常需要我们将结果输出为 HTML,这常常是不正确的。 这些 JavaScript 库本身分为两类。 首先,还有更多的纯制图库,例如 Google Maps 和Leaflet.js,它们是为在线呈现地图而构建的,但可以将其重新用于更特殊的地理空间数据可视化需求。 其次,有D3.js,这是一个通用库,用于基于能够进行精美地理可视化的数据来处理文档。 我们选择大叶植物是因为它简单而优雅。 当软件运行时,它使我们的生活和分析变得更加轻松。
另请参见
您还可以参考以下内容:
十、预测新西兰海外访客
在本章中,我们将介绍以下食谱:
- 创建时间序列对象
- 可视化时间序列数据
- 探索性方法和见解
- 趋势和季节分析
- ARIMA 建模
- 准确性评估
- 拟合季节性 ARIMA 建模
简介
天气预测,Sensex 预测,销售预测等是一些最常使用统计学或机器学习方法的人所感兴趣的常见问题。 当然,其目的是使用具有合理准确性的模型来获得未来几个时期的预测。 天气预测有助于规划职业旅行,Sensex 预测有助于投资计划,而销售预测则有助于优化库存计划。 这三个问题中的每一个的共同结构是,通常在等间隔的时间/纪元上可获得观测结果。 可以每天,每周或每月获取观察结果,我们将这些数据称为时间序列数据。 观察值是在过去很长时间内收集的,我们相信我们已经捕获了该系列足够的特征/特征,因此基于此类历史数据构建的分析模型将具有可预测的能力,并且我们可以获得相当准确的预测。
时间序列数据的结构面临新的挑战,无法使用本书前面讨论的方法进行分析。 主要挑战来自以下事实:定期获得的观察不能视为独立观察。 例如,由于我们有一个合理的信念,连续几天的降雨取决于最近的过去,并且也结合我们的经验经验,明天的降雨强度取决于今天的降雨,并且如果今天有雨或今天降雨不一样 天气很晴朗。 如果一个人从概念上接受观察不是彼此独立的,那么该如何指定依存关系呢? 首先,我们考虑新西兰海外游客的数据。
到国外旅行总是很有趣,尤其是在假期旅行中。 对于任何国家的旅游部门来说,重要的是要了解海外旅行者前往其国家的趋势,以便能够制定出物流方案。 许多其他工作也与海外访客有关,例如,各部委可能想知道下一季度的访客是否会增加或减少。 旅游部门需要考虑该行业的各个方面,例如,是否每年都有增长? 是否有季节性因素,例如夏季旅行最多的时间?
osvisit.dat可在多个 Web 链接上获得,这个页面包括来自新西兰的海外访客。 数据每月收集一次,始于 1977 年 1 月的,一直持续到 1995 年 12 月的。 因此,我们每月有 228 名访客。 出现以下问题,将通过本章中的食谱解决:
- 如何可视化时间序列数据?
- 您如何确定数据是否包含趋势和季节成分?
- 有哪些关系度量可以捕获时间序列数据的依存性?
- 您如何为数据建立适当的模型?
- 需要使用哪些测试来验证模型的假设?
在下一个食谱中,我们将说明 ts 对象是什么,以及如何从原始数据设置此类对象。
ts 对象
核心 R 包datasets包含许多实质上是时间序列数据的数据集:AirPassengers,BJsales,EuStockMarkets,JohnsonJohnson,LakeHuron,Nile,UKgas,UKDriverDeaths) UKLungDeaths,USAccDeaths,WWWusage,airmiles,austres,co2,discoveries,lynx,nhtemp,nottem,presidents,treering,gas,uspop和sunspots。 AirPassengers数据是最受欢迎的数据集之一,并用作基准数据集。 可以使用data(mydata)在 R 会话中加载数据,并且可以按以下时间序列验证其类别:
data(JohnsonJohnson)
class(JohnsonJohnson)
## [1] "ts"
JohnsonJohnson
## Qtr1 Qtr2 Qtr3 Qtr4
## 1960 0.71 0.63 0.85 0.44
## 1961 0.61 0.69 0.92 0.55
## 1962 0.72 0.77 0.92 0.60
## 1978 11.88 12.06 12.15 8.91
## 1979 14.04 12.96 14.85 9.99
## 1980 16.20 14.67 16.02 11.61
frequency(JohnsonJohnson)
## [1] 4
JohnsonJohnson数据集是时间序列数据集,并通过应用类函数进行了验证。 可以通过在 R 终端上运行?JohnsonJohnson获得有关数据集的详细信息。 此时间序列包含 Johnson 公司& Johnson 在 1960-80 年期间的季度收入。 由于我们有季度收入,因此时间序列的频率为每年四个,这可以通过使用 ts 对象上的函数频率进行验证。 读者可以对本节开头给出的其余数据集进行类似的练习。
现在有几个问题要解决。 如果我们有原始数据集,如何将它们导入 R 中? 由于导入的数据集将是numeric矢量或data.frame,我们如何将它们更改为 ts 对象? 如我们所见,JohnsonJohnson时间序列始于 1960 年,结束于 1980 年,它是一个季度数据,如何为新的 ts 数据对象指定这样的特征? 下一个食谱将解决所有这些问题。
准备
我们假定阅读器在 R 工作目录中具有osvisit.dat文件。 该文件是从这个页面获得的,读者也可以使用相同的来源。
怎么做
通过以下步骤将原始数据转换为所需的ts对象:
- 使用
**read.csvutils**功能将数据导入 R:
osvisit <-read.csv("osvisit.dat",header=FALSE)
- 使用以下命令将前面的
data.frame转换为ts对象:
osv <-ts(osvisit$V1,start =1977,frequency =12)
class(osv)
## [1] "ts"
- 使用
window功能显示前四年的数据:
window(osv,start=c(1977,1),end=c(1980,12))
## Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov
## 1977 48176 35792 36376 29784 21296 17032 22804 27476 21168 29928 37516
## 1978 44672 40500 36608 28524 23060 15760 20892 28992 23048 35052 40564
## 1979 49968 42068 41512 29272 25868 18216 23166 29808 25232 33780 43916
## 1980 48224 51353 46784 31284 26681 22817 26944 32902 25567 37113 44788
## Dec
## 1977 62156
## 1978 69304
## 1979 69576
## 1980 70706
工作原理...
read.csv功能有助于将数据从外部文件导入 R data.frame 对象(此处为osvisit)。 我们知道,这项研究中的数据观测是在 1977 年 1 月至 1995 年 12 月之间,并且每年我们都有 12 个月的观测值,统计来访新西兰的游客人数。 因此,我们首先使用ts函数将data.frame对象转换为时间序列 ts 对象为osv。 通过start =选项指定学习期的开始,并通过frequency = 12定义时间序列的周期性。 然后,将class函数应用于osv对象,以验证我们已成功将data.frame转换为 ts 对象,并且频率也正确。 在最后一步中,我们打算查看 1977-80 年期间的数据,并使用window函数对时间序列进行子集化,并使用参数start和end在控制台上显示数据。
因此可以看出,可以使用 ts 函数将数字矢量/数据帧转换为时间序列对象。 频率参数有助于我们定义时间序列的周期,例如每周,每月或每季度。 根据需要,也可以指定时间戳。 现在,我们将研究在下一个配方中可视化时间序列数据的方法。
可视化时间序列数据
时间序列的视觉描绘对于尽早了解数据的性质非常重要。 时间序列的可视化非常简单,因为只需将时间序列变量相对于时间本身作图就可以洞悉数据的行为。 R 函数 plot.ts 可以应用在 ts 对象上,并且时间序列可以可视化。 对于海外游客问题,我们针对该时间实例绘制了当月的游客数量。
准备
读者需要拥有当前环境中上一个会话的osv对象。
怎么做...
- 现在,我们将使用
plot.ts函数来获取海外数据的视觉描述。 - 在 R 会话中运行以下行:
plot.ts(osv, main="New Zealand Overseas Visitors",ylab="Frequency")
下图显示了通过运行 R 线给出的输出:

从图中可以看出,某种模式是重复发生的,并且该周期显示为 12 个数据点或一年。 此外,从数据来看,访问者人数逐年增加。 但是,我们想找出哪个月份的访问者人数最多,以及其他类似的模式。 在这个方向上,我们将根据月份绘制访客数,然后对所有年份重复该练习。
- 定义月份框架,并针对每年的图表绘制访问者相对于月份的数字:
mt <-1:12
names(mt) <-month.name
windows(height=20,width=30)
plot(mt,osv[1:12],"l",col=1,ylim=range(osv),ylab="Overseas Visitors",xlim=c(0,13))
for(i in2:19)points(mt,osv[mt+(i-1)*12],"l",col=i)
legend(x=4,y=190000,legend=c(1977:1982),lty=1:6,
col=1:6)
legend(x=6,y=190000,legend=c(1983:1988),lty=7:12,
col=7:12)
legend(x=8,y=190000,legend=c(1989:1995),lty=13:19,
col=13:19)
points(mt,osv[mt+(i-1)*12],pch=month.abb)
结果图如下所示:

R 程序的工作在工作原理... 部分中给出。 我们可以看到,我们在 7 月和 12 月至 2 月都有一个峰值。 尽管默认的plot.ts提供了很好的见解,但我们也可以通过绘制按年的月度销售额来更好地了解时间序列。 从这两个图表中得出的统一见解是,我们对海外游客的趋势和季节性影响都在不断变化。 R 中可能有时间序列数据中的季节性和趋势影响。 stl功能提供了这样的分析。
- 将时间序列分解为趋势,季节和不规则部分,如下所示:
osv_stl <-stl(osv,s.window=12)
plot.ts(osv_stl$time.series)
分解后的时间序列图如下所示:

STL 分解显示了多年来趋势和季节条件的变化。
工作原理...
对于第一个图,通过选项main和ylab用正确的标题和 y 轴名称增强了时间序列图。 默认设置几乎总是产生平淡的输出。 确定了多年来的趋势影响后,我们绘制了逐年逐月的图。 为了获得第二张图,我们首先创建一个 mt 变量,该变量的值分别为 1、2,...,12。 R 降价 Microsoft Word 并不同样令人印象深刻。 最初,我们只绘制 1977 年的访问者数量与月份的关系,然后使用for循环,将年度访问者数量强加于散点图。 ylim选项可确保我们在 y 轴上具有足够的范围以可视化所有时间点。 legend选项可以很好地指示色彩年份组合。 最后,使用month.abb,我们可以检查一年中的哪个月份的访问者人数最多。
在stl分解中,我们得到三个分量,季节分量,趋势分量和不规则分量。 可以看出,原始时间序列将是这三个分量的总和,运行round(rowSums(osv_stl$time.series),1)==osv。 从 STL 分解图中可以看出,每个组件中的方差随时间增加。 现在,我们将考虑一种简单的线性回归模型方法,以了解趋势和季节性因素对游客人数的影响。
简单的线性回归模型
可以建立线性回归模型以获得对趋势和季节对时间序列变量的影响的初步见解。 趋势和季节成分被指定为自变量,而访问者在此处计算的时间序列是因变量。 在构建线性回归模型时,我们做出以下假设:
- 时间序列在趋势和季节变量中是线性的。
- 趋势和季节成分彼此独立。
- 观测值,时间序列值彼此独立。
- 与观察相关的误差服从正态分布。
令 Y [t] 1 < t < T 表示在 1、2,...,T 时间点观察到的时间序列 。 例如,在我们的海外访问者数据中,我们的 T =228。在简单回归模型中,趋势变量是向量 1、2,...,T,即 X Tr =(1、2,...,T)。 我们知道,对于月度数据,我们将月份名称作为季节指标,也就是说,如果我们具有从一年的一月开始到次年的十二月结束的 24 个观测值的月度数据,那么季节性变量的值将是一月 ,2 月,...,11 月,12 月,1 月,2 月,...,11 月,12 月。 因此,季节变量将是分类变量。 同样,如果我们有季度数据,则季节变量的观测值将为季度 1,季度 2,季度 3 和季度 4。我们通过 X se 指示季节变量。 季节变量的一些示例是 X se =(1 月,2 月,...,11 月,12 月,1 月,2 月,...,11 月,12 月)和 X 本身 =(Q [1] ,Q [2] ,Q [3] ,Q [4] ,Q [1] ,Q [2] ,Q [3] ,Q [4] )。 然后,将结合趋势和季节变量的线性回归模型由下式给出:

接下来,我们将为海外游客数据集构建趋势和季节线性回归模型。 核心功能将是lm功能。
准备
osvts对象需要在当前的 R 环境中存在。
怎么做...
定义趋势和季节变量,我们将能够建立线性模型。 我们将在此处构建三个线性模型:
- 带有趋势变量,
- 具有适时的变量,以及
- 具有两个变量:
- 为趋势和季节变量创建 R 对象
osv_time和osv_mths:
osv_time <-1:length(osv)
osv_mths <-as.factor(rep(month.abb,times=19))
使用length函数,我们现在有了一个数字矢量,其值从 1 开始到 228 结束。季节变量是使用 rep 函数创建的,该函数连续将月份名称重复 19 次。 month.abb是 R 中的标准字符向量,其内容为月份缩写。
- 仅使用趋势变量创建线性模型并生成其摘要:
osv_trend <-lm(osv~osv_time)
summary(osv_trend)
##
## Call:
## lm(formula = osv ~ osv_time)
##
## Residuals:
## Min 1Q Median 3Q Max
## -33968 -13919 -3066 10497 79326
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 20028.62 2585.63 7.746 3.17e-13 ***
## osv_time 385.36 19.58 19.683 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 19460 on 226 degrees of freedom
## Multiple R-squared: 0.6316, Adjusted R-squared: 0.6299
## F-statistic: 387.4 on 1 and 226 DF, p-value: < 2.2e-16
-
与 F 统计信息关联的在前 R 输出的最后一行中的 p 值很重要,表明整个模型很重要。 与
osv_time相关的 p 值也很小,这意味着趋势影响与 0 显着不同。 R 2 约为 63%,即 访客数量的变化,如趋势变量所解释。 现在,我们来看一下适时变量。 -
使用季节变量创建线性模型并生成其摘要:
osv_season <- lm(osv~osv_mths)
summary(osv_season)
##
## Call:
## lm(formula = osv ~ osv_mths)
##
## Residuals:
## Min 1Q Median 3Q Max
## -45099 -23919 -2738 17161 79961
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 57116 6277 9.100 < 2e-16 ***
## osv_mthsAug -4226 8877 -0.476 0.6345
## osv_mthsDec 50139 8877 5.648 5.07e-08 ***
## osv_mthsFeb 20143 8877 2.269 0.0242 *
## osv_mthsJan 17166 8877 1.934 0.0544 .
## osv_mthsJul -5670 8877 -0.639 0.5236
## osv_mthsJun -15351 8877 -1.729 0.0852 .
## osv_mthsMar 13799 8877 1.555 0.1215
## osv_mthsMay -12883 8877 -1.451 0.1481
## osv_mthsNov 19286 8877 2.173 0.0309 *
## osv_mthsOct 7190 8877 0.810 0.4188
## osv_mthsSep -5162 8877 -0.582 0.5615
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 27360 on 216 degrees of freedom
## Multiple R-squared: 0.3038, Adjusted R-squared: 0.2683
## F-statistic: 8.567 on 11 and 216 DF, p-value: 1.63e-12
-
季节变量是一个因子变量,因此我们在因子变量的 11 个级别上具有变量
summary。 由于某些因子水平很重要,尽管 R 2 非常差,只有 30%,但总体变量却很显着。 现在,我们将在下一步中同时包含这两个变量。 -
创建具有趋势和季节变量的线性模型,并生成其摘要:
osv_trend_season <- lm(osv~osv_time+osv_mths)
summary(osv_trend_season)
##
## Call:
## lm(formula = osv ~ osv_time + osv_mths)
##
## Residuals:
## Min 1Q Median 3Q Max
## -17472 -5482 -1120 3805 38559
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 14181.157 2269.500 6.249 2.19e-09 ***
## osv_time 383.346 8.944 42.860 < 2e-16 ***
## osv_mthsAug -5759.017 2880.198 -2.000 0.0468 *
## osv_mthsDec 47072.650 2880.864 16.340 < 2e-16 ***
## osv_mthsOct 4890.027 2880.475 1.698 0.0910 .
## osv_mthsSep -7078.837 2880.323 -2.458 0.0148 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 8877 on 215 degrees of freedom
## Multiple R-squared: 0.927, Adjusted R-squared: 0.923
## F-statistic: 227.7 on 12 and 215 DF, p-value: < 2.2e-16
-
请注意,这里的模型和两个变量都很重要。 同样,R 2 以及 Adj R 2 增加到 93%。 因此,这两个变量都有助于理解访问者人数。
-
但是,线性模型方法存在一些障碍。 第一个问题是概念性问题,独立性的假设看起来非常不现实。 同样,如果我们查看经验残差分布,即误差项的估计值,则此处的正态性假设就显得格格不入。
-
使用拟合模型上的
residuals功能和hist图形功能来描述误差分布:
windows(height =24,width=20)
par(mfrow=c(3,1))
hist(residuals(osv_trend),main ="Trend Error")
hist(residuals(osv_season),main="Season Error")
hist(residuals(osv_trend_season),main="Trend+Season Error")

从前面的直方图中可以看出,误差的正态假设不适用于时间序列数据。
工作原理...
R 中的lm函数对于拟合线性模型很有用。 公式对于构建线性模型至关重要,其通用形式为lhs ~ rhs。 代字号 ~ 的左侧为因变量,右侧为自变量或协变量。 可以使用?lm获取lm的详细信息。 函数摘要获取了拟合模型的更多详细信息,在这里我们已经使用了三次,以查看每个模型如何对数据执行。 为了验证误差分布的正态假设,我们通过hist(residuals(lm),...)获得直方图。
另请参见
观测值之间的依赖关系是时间序列数据的主要复杂性,而忽略它会导致错误的结论。 接下来,我们将考虑一些方法和措施,这些方法和措施可帮助我们构建包含时间序列相关性的模型。
ACF 和 PACF
我们有时间序列 Y [t] 1 < t < T,可以将其概念化为在时间 1 <观察到的随机过程Y。 t <T。如果连续观察到一个过程,则在时间 t 的过程值取决于在时间 t-1,t-2,... 处的过程值也是合理的。 。 依赖关系的规范是时间序列建模的关键。 像在回归模型中一样,我们在μ [t] ,1 < t < T 中具有误差过程,通常将其假定为白噪声过程。 现在,过程/时间序列 Y [t] 1 < t < T 可能取决于其过去的值或过去的误差项。 可用于理解依赖性性质的两个度量标准是自相关函数(ACF)和部分自相关函数(PACF)。 我们首先需要滞后概念。 对于过程 Y [t] 2 < t < T,滞后1过程为 Y [t-1,] 1 < t < T-1。 通常,对于变量 Y [t] ,第 k 个滞后变量为 Y [t-k] 。 滞后 k ACF 定义为随机变量 Y [t] 与第 k 个滞后变量 Y [t-k] 之间的相关性:

在哪里

是时间序列的方差。 Y [t] 和它的 k 滞后 Y [tk] 之间的偏自相关函数 PACF 是时间序列的偏相关,同时将值控制在较短的滞后于 Y [t-1] ,Y [t-2] ,...,Y [t-k + 1]。 无法深入了解 PACF 概念的数学细节,读者可以参考 Box 等。 (2015)。 ACF 和 PACF 值有助于确定误差变量或时间序列的项。 我们将简单的随机游走作为时间序列的示例,以了解这些概念。
令白噪声(误差)的序列为μ [t] ,1 < t < T,观察到的时间序列/随机游走为
Y [i] =μ [1] +μ [2] + ... +μ [i] ,1 < i <] T
请注意,该系列还可以(重写)为
Y [i] = Y [i-1] +μ [i] ,1 < i < T。
因此,可以看到 Y [i] 值取决于先前的i错误,或 Y [i-1] 值和当前错误。 接下来,我们将模拟随机游走并获得 R 中的 ACF 和 PACF 图。
准备
只需一个开放的 R 会话。
怎么做...
首先,我们模拟随机行走的 500 次(伪)观察,然后将其可视化。 应用acf和pacf函数,然后检查图的性质。
- 首先将种子设置为数字
12345:
set.seed(12345)
- 从标准正态分布中模拟 500 个观测值,并通过取总和来计算每个时期的随机游动:
y <-rnorm(500)
rwy <-cumsum(y)
- 可视化时间序列,然后将
acf和pacf函数应用于上一步中创建的随机游走:
windows(height=24,width=8)
par(mfrow=c(3,1))
plot.ts(rwy,main="Random Walk")
acf(rwy,lag.max=100,main="ACF of Random Walk")
pacf(rwy,main="PACF of Random Walk")
- 结果图如下所示:

注意
ACF 和 PACF 是建议可能生成时间序列的模型类型的有效工具。 例如,在这里我们可以发现,过去的观测Y[t-1] 的影响正在下降,而如果我们看一下偏相关,则只有前一个 观察很重要,其余都为零。 这里的观察结果代替了理论上的 ACF 和 PACF,请参见这个页面例子。
工作原理...
set.seed功能用于确保程序的可重复性,rnorm和cumsum一起为我们提供了必要的随机游走。 函数acf和pacf是基本的时间序列函数,几乎在所有时间序列分析中都将需要这些函数。 lag.max=100选项已用于显示acf确实在下降,如果我们采用默认设置,这不是很明显。
ARIMA 模型
在上一节中,我们看到了随机游走以及 ACF 和 PACF 函数的作用。 随机游走可视为一系列,取决于过去的观察以及过去的错误。 因此,可以将时间序列可视化为过去的观察结果,错误或两者的函数。 通常,给定时间序列 Y [t] ,1 < t < T 和错误过程μ [t] ,1 [ < t < T a 线性过程 定义为:

条款

是线性过程的系数。 现在,假设我们对Y[t] 取决于过去p观察值的模型感兴趣:

先前的模型众所周知为 p 阶的自回归模型,并由 AR(p)表示。 重要的是要在这里注意 AR 系数

并非不受限制,并且我们只是注意到,如果假设时间序列是固定的,则它们的绝对值必须小于 1。 接下来,我们将 q 阶的缩写为 MA(q)的移动平均模型定义为:

MA(q)模型的参数为

。 实际上,时间序列可能取决于过去的观察结果以及错误,并且可以通过自回归移动平均模型(由 ARMA(p,q)表示)来捕获此类模型, 并由:

可以从 ARMA 模型获得理论上的 ACF 和 PACF。 下表显示了此类模型的 ACF 和 PACF 的预期行为:

为了了解 ARMA 模型中 ACF 和 PACF 的性质,我们使用arima.sim函数模拟观察结果,并绘制相应的 ACF 和 PACF 并检查上表中建议的行为。
准备
阅读器将需要 R 会话和之前创建的osv对象。
怎么做...
首先模拟AR(1),MA(1)和ARMA(1,1)模型,并获得 ACF 和 PACF 图:
- 首先将种子设置为数字
123。 产生三个时间序列AR(1),MA(1)和ARMA(1,1):
set.seed(123)
t1 <-arima.sim(list(order =c(1,0,0),ar =0.6),n =100)
t2 <-arima.sim(list(order =c(0,0,1),ma =-0.2),n =100)
t3 <-arima.sim(list(order =c(1,0,1),ar =0.6,ma=-0.2),n =100)
tail(t1);tail(t2);tail(t3)#output suppressed
t1的基础AR(1)模型是

与t2和t3类似,相关的基础模型为:

- 获得
t1,t2和t3的 ACF 和 PACF 图:
windows(height=30,width=20)
par(mfrow=c(3,2))
acf(t1);pacf(t1)
acf(t2);pacf(t2)
acf(t3);pacf(t3)
- 结果图如下:

t1对象被模拟为AR(1)过程,并且理论上的 ACF 可能会降低或终止,而理论上的 PACF 必须在滞后后才断开。 我们在 t1 的经验 ACF 和 PACF 中看到了这种行为。 同样,t2的 ACF 和 PACF 分别在滞后 1 和逐渐下降后出现截止,这表明 MA(1)是合适的模型。 同样,系列t3的 ACF 和 PACF 图显示了理论量的预期行为,因此表明 ARMA 模型可能是合适的。 请注意,关联图未反映 ARMA 模型的顺序。
- 读者应该问有关自回归系数值的绝对值大于 1 会发生什么的问题。在 R 中运行下两行,并得出一个适当的结论:
arima.sim(list(order =c(1,0,0),ar =1.6),n =100) arima.sim(list(order =c(0,0,1),ma =10.2),n =100)
接下来,我们将看一下 osv 的 ACF 和 PACF 图。
- 在
osv上应用acf和pacf功能:
windows(height=10,width=20)
par(mfrow=c(1,2))
acf(osv)
pacf(osv)
- 结果图如下:

注意
注意上图中 x 轴比例的变化。 在较早的 ACF 和 PACF 图中,我们指定了lag.max,x 轴的范围从 0 到 20 或更大。 在这里,尽管我们仍然具有从 20 个延迟中计算出的 ACF 和 PACF,但范围也是从 0 到 2,分数也分别是 0.5 和 1.5。 这是因为我们有osv时间序列对象的frequency=12。
在这里,ACF 和 PACF 图只是对 ARMA 模型的描述,如先前表中所述。 ACF 具有周期性行为,而 PACF 并不能最终说出一定的滞后后是否下降。 回想一下osv的时间序列图,我们的平均均值(以及方差)都在增加。 简而言之,时间序列的行为随时间而变化,因此我们具有非平稳数据。 在许多实际情况下,可以通过对序列 Y [t] 进行求差来获得平稳性,也就是说,可以代替对Y[t] 进行建模, 我们考虑Y[t] -Y[t-1] 的差异。Y[t] -Y[t-1] 的差异是首先的顺序差异,有时可能需要更高的顺序。 差异,并且在大多数实际情况下,已注意到直到 4 阶的差分都会保持平稳。 差异的顺序通常用字母d表示,并且在差异上应用 ARMA 模型称为自回归综合移动平均模型或 ARIMA 模型。 缩写是 ARIMA(p,d,q)。 在接下来的步骤中,我们将为海外访客数据构建 AR,MA 和 ARIMA 模型。
将ar函数应用于时间序列对象时,会自动选择p的顺序,并使用 Yule-Walker 方法拟合模型。
- 在
osv对象上应用ar函数:
osv_ar <-ar(osv)
osv_ar
## Call:
## ar(x = osv)
##
## Coefficients:
## 1 2 3 4 5 6 7 8
## 0.6976 0.1015 -0.0238 0.0315 0.0106 -0.1569 0.0183 0.1238
## 9 10 11 12 13
## 0.0223 0.0279 -0.0039 0.6398 -0.5227
##
## Order selected 13 sigma^2 estimated as 123787045
AR 阶次 p 选择为 13。接下来,我们查看拟合模型的残差并检查正态性假设是否成立。
- 获取残差的直方图,并查看残差的 ACF 图:
windows(height=10,width=20)
par(mfrow=c(1,2))
hist(na.omit(osv_ar$resid),main="Histogram of AR Residuals")
acf(na.omit(osv_ar$resid),main="ACF of AR Residuals")
直方图和 ACF 图如下所示:

直方图看起来偏斜,ACF 表明残差之间存在依赖性,这违背了 AR 模型的假设。
与ar功能不同,我们没有一种可以自动选择 q 阶的拟合移动平均模型的函数。 因此,我们首先定义一个新函数,该函数将尝试根据 aic 标准在移动平均模型中找到给定最大误差滞后的最佳模型。
- 定义
auto_ma_order函数,并为osv数据找到最佳移动平均模型,如下所示:
auto_ma_order <-function(x,q){
aicc <-NULL
for(i in1:q) {
tmodel <-arima(x,order=c(0,0,i))
aicc[i] <-as.numeric(tmodel$aic)
}
return(which.min(aicc))
}
auto_ma_order(osv,15)
## [1] 14
可能由于某种技术原因未定义自动移动平均模型拟合功能。 通常会发生auto_ma_order函数在最大延迟处找到最佳移动平均延迟的情况,如下所示。
- 对于
osv时间序列数据,找到各种最大滞后的最佳移动平均模型:
sapply(1:20,auto_ma_order,x=osv)
## [1] 1 2 3 4 5 6 6 8 9 9 9 9 13 14 14 16 17 18 18 20
由于auto_ma_order功能通常会以最大延迟找到最佳顺序,因此它不是有用的功能。 出于这种行为背后的数学原因,读者应查阅诸如 Box 等人的优质时间序列书。 (2015)。 现在,我们适合各种订单的 ARIMA 模型。
- 为
osv数据拟合三个 ARMA 模型和三个 ARIMA 模型:
# ARIMA Model Fitting
osv_arima_1 <-arima(osv,order=c(1,0,1))
osv_arima_2 <-arima(osv,order=c(2,0,1))
osv_arima_3 <-arima(osv,order=c(1,0,2))
osv_arima_4 <-arima(osv,order=c(1,1,1))
osv_arima_5 <-arima(osv,order=c(2,1,1))
osv_arima_6 <-arima(osv,order=c(1,1,2))
osv_arima_1; osv_arima_2; osv_arima_3
##
## Call:
## arima(x = osv, order = c(1, 0, 1))
##
## Coefficients:
## ar1 ma1 intercept
## 0.9030 0.0008 68427.30
## s.e. 0.0348 0.0659 10168.16
##
## sigma^2 estimated as 233640077: log likelihood = -2521.06, aic = 5050.13
##
## Call:
## arima(x = osv, order = c(2, 0, 1))
##
## Coefficients:
## ar1 ar2 ma1 intercept
## -0.0871 0.9123 0.9931 68374.33
## s.e. 0.0306 0.0306 0.0091 10714.84
##
## sigma^2 estimated as 216211876: log likelihood = -2513.14, aic = 5036.27
##
## Call:
## arima(x = osv, order = c(1, 0, 2))
##
## Coefficients:
## ar1 ma1 ma2 intercept
## 0.8755 0.0253 0.1315 67650.666
## s.e. 0.0428 0.0792 0.0607 9106.606
##
## sigma^2 estimated as 229397770: log likelihood = -2518.99, aic = 5047.98
osv_arima_4; osv_arima_5; osv_arima_6
##
## Call:
## arima(x = osv, order = c(1, 1, 1))
##
## Coefficients:
## ar1 ma1
## -0.602 0.5313
## s.e. 0.356 0.3761
##
## sigma^2 estimated as 2.41e+08: log likelihood = -2512.67, aic = 5031.35
##
## Call:
## arima(x = osv, order = c(2, 1, 1))
##
## Coefficients:
## ar1 ar2 ma1
## -0.2669 0.0912 0.2256
## s.e. 0.4129 0.0743 0.4103
##
## sigma^2 estimated as 239665946: log likelihood = -2512.07, aic = 5032.13
##
## Call:
## arima(x = osv, order = c(1, 1, 2))
##
## Coefficients:
## ar1 ma1 ma2
## -0.4164 0.3784 0.0901
## s.e. 0.2893 0.2920 0.0703
##
## sigma^2 estimated as 239508475: log likelihood = -2511.99, aic = 5031.99
六个不同模型的 AIC 值分别为 5050.13、5036.27、5047.98、5031.35、5032.13 和 5031.99。 Hyndman 的forecast程序包中提供了auto.arima函数,该函数在最大的 p,d 和 q 值内找到最佳的 ARIMA 模型。 但是,还有其他时间序列建模指标,AIC 可能不是最佳评估方法。 下一节将定义一些措施。
工作原理...
在本节中,出于仿真目的,arima.sim功能很有用,而根据需要,选项list,order,ar,ma和n也有用。 acf和pacf函数可用于获取必要的相关图。 我们使用ar函数来找到最佳的自回归模型。 可以使用arima功能并通过 order 选项指定模型顺序来拟合移动平均模型。 可以使用ar_fit$resid提取拟合的ar对象的残差。
精度测量
通过使用诸如aic,bic等度量标准,可以解决回归模型中的模型选择问题。 尽管我们早先使用了这样的模型来选择模型,但重要的是要注意时间序列的一般目的是预测。 因此,时间序列建模具有一些自定义指标,这些指标可用于预测。 在此,将实际值与拟合值进行比较。
为了获得透视图,我们具有 Y [t] ,1 < t < T 的时间序列,并假设通过使用特定模型得出的预测值是 AR(p),MA(q)或 ARIMA(p,d,q),因为时间序列是

。 然后,我们可以通过比较来获取模型的拟合度

。 由模型导致的残差定义为

。 然后定义精度测量值,如下所示:

该计算将使用原始代码执行。
准备
在当前的 R 环境中,将需要 R 对象osv。 读者将需要 ARIMA 对象osv_arima_1,osv_arima_2,osv_arima_3和osv_arima_4。 还需要 R 包forecast。
怎么做...
精度测量公式很容易计算,我们使用残差函数简化了程序。
forecast软件包中的函数mean,sqrt,abs,residuals和accuracy用于获得精度测量值:
mean(residuals(osv_arima_1))# Mean Error
## [1] 106.6978
sqrt(mean(residuals(osv_arima_1)^2))# Root Mean Square Error
## [1] 15285.29
mean(abs(residuals(osv_arima_1)))# Mean Absolute Error
## [1] 11672.7
mean(residuals(osv_arima_1)/osv)*100# Mean Percentage Error
## [1] -5.35765
mean(abs(residuals(osv_arima_1)/osv))*100# Mean Absolute Percentage Error
## [1] 19.04502
accuracy(osv_arima_1)
## ME RMSE MAE MPE MAPE MASE
## Training set 106.6978 15285.29 11672.7 -5.35765 19.04502 0.9717733
## ACF1
## Training set 0.004153521
mean(abs(residuals(osv_arima_2)/osv))*100
## [1] 19.01274
mean(abs(residuals(osv_arima_3)/osv))*100
## [1] 18.75919
mean(abs(residuals(osv_arima_4)/osv))*100
## [1] 19.01341
使用 MAPE 准则,将使用osv_arima_3,因为它在四个模型中的价值最小。 我们还可以看到,原始代码的计算结果与forecast软件包中的accuracy函数匹配。
工作原理...
精度测量公式很容易实现。 在分析时间序列数据时使用此类指标很重要。 原始代码显示了公式实现,尽管简单的accuracy函数也提供了解决方案。
通常,MAPE 度量标准更有效并得到广泛使用。 但是,这四个模型的 MAPE 在 18.76%至 19.05%的范围内变化。 季节性是一个很大的因素,我们希望建立可解释此类因素的 ARIMA 模型,这将成为下一部分讨论的主题。
拟合季节性 ARIMA 模型
ARIMA 模型对于每月海外访问者的意义在于,过去的观察和错误会影响当前的观察。 osv数据上应用的ar函数建议的顺序为 13,表明前一年的每月访问者数量也影响了本月的访问者。 但是,在过去的 13 个月中,每个访问者的人数都会产生影响,这很有趣。 同样,这增加了模型的复杂性,我们希望基于尽可能少的过去观察来创建有意义的模型。 请注意,拟合模型的方差非常大,我们也希望减少方差。
整合季节影响的一种好方法是使用季节-ARIMA 模型,请参阅 Cryer 和 Chan 的第 10 章(2008)。 为了了解季节性 ARIMA 模型的工作原理,我们将首先考虑简单的季节性 AR 模型。 在这里,我们允许过去的Y[t] 影响当前的 Y [t],然后影响过去的相应季节 ] Y [t] 的。 例如,令 p = 3,时间序列的频率为 12 个月,我们想考虑过去两个季节条件的影响。 这意味着Y[t] 现在受Y[t-1] ,Y[t- 2] ,Y[t-3] ,Y[t-12] ,Y[t -24] 。 习惯上用大写字母 P 表示季节Y[t] 滞后。类似地,具有两个移动平均滞后和三个季节移动平均滞后的季节移动平均模型由组成 &#949 [t-1] ,&#949 [t-2] ,&#949 [t-12] ,&#949 [t-24] ,&#949 [t-36]。 季节性移动平均时差用大写字母 Q 表示,类似的差异用 D 表示。季节性 ARIMA 模型通常用(p,d,q)x(P,D,Q)表示 [{ freq}]。 arima功能也可以适合季节性 ARIMA 模型,我们将在接下来看到操作。
准备
如果阅读器在 R 环境中具有 osv 对象,就足够了。
怎么做...
使用 arima 功能中的 seasonal 选项,我们将建立季节性 ARIMA 模型。
- 我们按如下方式构建季节性模型:(1、1、0)x(0、1、0) [12] ,(1、1、0)x(1、1、0) [12] ,
(0,1,1)x(0,1,1) [12] ,(1,1,0)x(0,1,1) [12] ,( 0,1,1)x(1,1,0) [12] ,(1,1,1)x(1,1,1) [12] ,
(1,1,1)x(1,1,0) [12] ,(1,1,1)x(0,1,1) [12]。
osv_seasonal_arima2 <-arima(osv,order=c(1,1,0),seasonal=c(0,1,0))
osv_seasonal_arima3 <-arima(osv,order=c(1,1,0),seasonal=c(1,1,0))
osv_seasonal_arima4 <-arima(osv,order=c(0,1,1),seasonal=c(0,1,1))
osv_seasonal_arima5 <-arima(osv,order=c(1,1,0),seasonal=c(0,1,1))
osv_seasonal_arima6 <-arima(osv,order=c(0,1,1),seasonal=c(1,1,0))
osv_seasonal_arima7 <-arima(osv,order=c(1,1,1),seasonal=c(1,1,1))
osv_seasonal_arima8 <-arima(osv,order=c(1,1,1),seasonal=c(1,1,0))
osv_seasonal_arima9 <-arima(osv,order=c(1,1,1),seasonal=c(0,1,1))
- 获取上一步中构建的所有模型的 MAPE:
accuracy(osv_seasonal_arima2)[5]
# [1] 5.525674
accuracy(osv_seasonal_arima3)[5]
## [1] 5.206777
accuracy(osv_seasonal_arima4)[5]
## [1] 4.903352
accuracy(osv_seasonal_arima5)[5]
## [1] 5.113835
accuracy(osv_seasonal_arima6)[5]
## [1] 4.946375
accuracy(osv_seasonal_arima7)[5]
## [1] 4.603231
accuracy(osv_seasonal_arima8)[5]
## [1] 4.631682
accuracy(osv_seasonal_arima9)[5]
## [1] 4.5997
- 在此处指定的模型中,顺序(1、1、1)x(0、1、1) [12] 导致租赁 MAPE。 由于通常很难在(p,d,q)x(P,D,Q) [f] 的不同可能组合中搜索最佳模型,因此我们使用
auto.arima功能来自forecast包。 - 使用
auto.arima功能可获得最佳模型:
opt_model <-auto.arima(osv,max.p=6,max.q=6,max.d=4,max.P=3,max.Q=3,max.D=3)
opt_model
## Series: osv
## ARIMA(5,1,3)(0,1,1)[12]
##
## Coefficients:
## ar1 ar2 ar3 ar4 ar5 ma1 ma2 ma3
## 0.3732 0.5802 -0.2931 -0.3063 -0.2003 -0.8537 -0.6079 0.7898
## s.e. 0.1172 0.1172 0.0931 0.0760 0.0853 0.0946 0.1348 0.0802
## sma1
## -0.4880
## s.e. 0.0636
##
## sigma^2 estimated as 17089028: log likelihood=-2093.09
## AIC=4206.17 AICc=4207.25 BIC=4239.88
accuracy(opt_model)
## ME RMSE MAE MPE MAPE MASE
## Training set 236.9483 3929.388 2820.913 0.07114353 4.592125 0.5239947
## ACF1
## Training set -0.02654602
前面的结果表明,最佳季节性 ARIMA 模型是ARIMA(5,1,3)(0,1,1)[12]。
工作原理...
seasonal选项有助于在 ARIMA 模型中设置所需的季节性订单。 auto.arima函数有助于在(p,d,q)和(P,D,Q)的指定滞后内找到最佳 ARIMA 模型。
还有更多...
时间序列建模的范围远远超出了本简短的章节。 Box 等。 (2015),Cryer and Chan(2008)和 Chatfield(2003)是对时间序列方法的一些出色展示。
十一、德国信用数据分析
在本章中,我们将介绍以下食谱:
- 转换数据
- 可视化分类数据
- 判别分析以识别默认值
- 拟合逻辑回归模型
- 德国数据的决策树
- 决策树的精细方面
简介
贷款! 借款人的负债和银行的资产! 银行当然希望只提供贷款,而不提供任何储蓄计划,例如储蓄账户,定期存款,定期存款等。 原因很简单,银行必须在一段时间后向客户付款,如果他们的收入不足,就不能放弃利息。 尽管银行希望尽可能多地发放贷款,但是有很多理由永远不会以先到先得的方式发放贷款。 显而易见的原因很简单,那就是如果客户违约,银行就会脱颖而出,并且有机会为更好的客户服务。 显而易见的问题是,如何定义更好的客户,分析方法将在这里提供帮助。 一个实用的数据集是德国数据集,其中包括客户是否已完全还清贷款的最终状态以及许多其他重要变量。
已经进行了很多分析,现在它已成为分类问题的重要基准数据集。 它已在这个页面中提供。 在许多研究工作中都已使用它,在撰写本文时,它的总访问量为 228982。 可以在[这个页面](https://archive.ics.uci.edu/ml/datasets/Statlog+(German+Credit+Data)中找到使用 R 软件从各种角度进行的分析。 可以在[这个页面](https://archive.ics.uci.edu/ml/datasets/Statlog+(German+Credit+Data)中找到更多详细信息。 有关德国信贷数据的深入分析,请参阅 Tattar(2013)。 我们将从RSADBE包中提取此数据集,并在下一部分中进行简单的转换。 此处提供了数据集的详细说明。
GC数据集包含 21 个变量的 1,000 个观察值。 该信用数据中的利息变量是贷款是好贷,客户完全还清了贷款额还是坏贷,并将此状态的信息放入变量good_bad中。 在这里,在 1,000 项观察中,有 700 项是不良贷款,而其余则是不良贷款。 还收集了大量重要的变量数据,这些数据提供了有关客户类型的信息。 附加变量包括定量(数字)变量和定性(类别)变量。 数值变量是每月持续时间(duration),信用额度(credit),按可支配收入的比例分期付款(installp),目前居住的持续时间(resident),年限( age),在银行的现有信用数(existcr)和申请人的受抚养人数(depends)。 其他 21 个变量中的其余均为类别变量。
GC 数据集简化了分类变量的因子水平的数值表示,因此,大多数将其标识为整数变量,这是不合适的。 例如,当前支票帐户(checking)的状态简单地从 1-4 编号,而它们应表示因子水平< 0 DM,0 <= ... < 200 DM,>= 200 DM和No Acc。 下一节将修复所需的因子变量级别。
简单的数据转换
可从 RSADBE 1.0 版本获得的德国信用数据有一定的局限性。 包中的数据文件名为GC。 许多类别变量存储为整数类,这会影响整体分析。 同样,某些变量在这里并不重要,并且在从整数类转换为因子类之后,需要重新标记。 例如,可以在这个页面上获取有关变量的详细信息。 在本节中,我们将使用数据集并进行必要的转换。
准备
读者将需要安装RSADBE程序包,该程序包由GC数据集组成。 如前所述,我们首先加载所有必备库:
library (data.table)
library (dplyr)
library (RSADBE)
library (rpart)
library (randomForestSRC)
library (ROCR)
library (plyr)
怎么做...
RSADBE R 包中提供GC数据集。 如前所述,数据集由许多整数变量组成,而这些整数是因子水平的指标。 例如,如前所述,我们需要将因子水平设置为< 0 DM,0 <= ... < 200 DM,>= 200 DM和No Acc,而不是 1-4。 需要使用功能as.factor和revalue来完成对 1,000 个观测值的重新标记:
- 将
checking变量从其当前的integer类转换为factor,然后应用revalue(来自plyr包)函数以获得所需的结果:
data(GC)
GC2 <-GC
GC2$checking <-as.factor(GC2$checking)
GC2$checking <-revalue(GC2$checking,c("1"="< 0 DM","2"="0 <= ... < 200 DM",
"3"=">= 200 DM","4"="No Acc"))
- 将其他
integer对象类似地转换为所需的factor对象:
GC2$history <-as.factor(GC2$history)
GC2$history <-revalue(GC2$history,c("0"="All Paid","1"="Bank paid", "2"="Existing paid","3"="Delayed", "4"="Dues Remain"))
GC2$purpose <-as.factor(GC2$purpose)
GC2$purpose <-revalue(GC2$purpose,
c("0"="New Car","1"="Old Car","2"="Furniture",
"3"="Television","4"="Appliance","5"="Repairs",
"6"="Education","8"="Retraining","9"="Business",
"X"="Others"))
GC2$savings <-as.factor(GC2$savings)
GC2$savings <-revalue(GC2$savings,
c("1"="< 100 DM ","2"="100-500 DM",
"3"="500-1000 DM","4"=">1000 DM",
"5"="Unknown"))
GC2$employed <-as.factor(GC2$employed)
GC2$employed <-revalue(GC2$employed,
c("1"="Unemployed","2"="1 Year","3"="1-4 Years",
"4"="4-7 Years","5"=">7 Years"))
GC2$marital <-as.factor(GC2$marital)
GC2$marital <-revalue(GC2$marital,
c("1"="Female S","2"="Female M/D","3"="Male M/D",
"4"="Male S"))
GC2$coapp <-as.factor(GC2$coapp)
GC2$coapp <-revalue(GC2$coapp,
c("1"="None","2"="Co-app","3"="Guarantor"))
GC2$property <-as.factor(GC2$property)
GC2$property <-revalue(GC2$property,
c("1"="Real Estate","2"="Building society",
"3"="Others","4"="Unknown"))
GC2$other <-NULL# because "none" is dominating frequency
GC2$housing <-as.factor(GC2$housing)
GC2$housing <-revalue(GC2$housing,c("1"="Rent","2"="Own",
"3"="Free"))
GC2$job <-as.factor(GC2$job)
GC2$job <-revalue(GC2$job,c("1"="Unemployed","2"="Unskilled","3"="Skilled",
"4"="Highly Qualified"))
GC2$telephon <-as.factor(GC2$telephon)
GC2$telephon <-revalue(GC2$telephon,c("1"="None","2"="Registered"))
GC2$foreign <-as.factor(GC2$foreign)
GC2$foreign <-revalue(GC2$foreign,c("1"="No","2"="Yes"))
请注意,现在已从GC2中删除了另一个对象。
工作原理...
数据并不总是采用易于分析的格式,在许多情况下可能需要进行微调。 在本食谱中,我们使用了先将整数对象转换为因子,然后适当地重新标记它的技术。 很容易进行许多数据重组,我们将根据需要使用它们。
还有更多...
data.table R 软件包也非常有用。 可以在同一对象上执行数据重组,因此,无需像在此创建GC2一样创建另一个对象。
可视化分类数据
可视化是任何探索性分析的重要方面。 假设我们没有有关客户历史的其他信息,而我们只知道在批准的千笔贷款中有 700 笔良好的贷款。 在这种情况下,对下一个客户的良好贷款还款的预测概率为 0.7。 当感兴趣的变量是数字变量时,我们可以绘制直方图,箱线图等,以更好地理解问题。 在这里,输出good_bad是一个因子变量,这样的绘图没有任何意义。 可以使用条形图获得对因子变量的分布的简单理解,在条形图中,我们将有两个条形,其长度反映了成比例的频率。 但是,从 x 轴标记为好和不好不会增加任何值的意义上来说,条形图是一维图。
当两个变量都是数值变量时,散点图将揭示两个变量之间的关系。 在我们的案例中,关注的主要变量good_bad是类别/因子变量,根据它绘制其他变量不会显示出关系的性质,因此我们需要不同类型的可视化技术。 分类变量可视化需要专门的技术,我们在此使用镶嵌图。 有关更多详细信息,请参阅 Friendly and Meyer(2015)。
准备
这里将需要使用先前配方中创建的 R 数据帧 GC2。
怎么做...
- 条形图和镶嵌图在此处获得,我们首先从前者开始。
barplot功能提供所需的图形:
barplot(table(GC2$good_bad))
- 视觉输出如下:

barplot是不良贷款和不良贷款数量的简单描述。 有关变量的其他信息总是有用的。 镶嵌图是多个变量的条形图的堆叠排列,它指示了两个类别变量之间的关系类型。 现在,我们将首先查看变量housing,employed和checking在不同类别中的好/坏贷款比例,然后使用有用的镶嵌图进行跟踪。
- 使用函数
table和prop.table,我们获得每个类别变量的好坏贷款百分比:
table(GC2$good_bad,GC2$housing)
## Rent Own Free
## bad 70 186 44
## good 109 527 64
prop.table(table(GC2$good_bad,GC2$housing),margin=2)
## Rent Own Free
## bad 0.3911 0.2609 0.4074
## good 0.6089 0.7391 0.5926
好/坏贷款比率是 700/300,现在我们看到,如果根据房屋是Rent,Own和Free将人口分为三部分,则这些部分中的好商品比例为 分别为0.6089,0.7391和0.5926。 因此,在自己的部分中,我们看到的优质贷款比例要高于人口中的较高比例。 当然,其他两个部分的不良贷款比例也有所增加,尽管可以在这些部分中进一步进行即兴创作。
- 类似地,下面给出
employed和checking与贷款变量的比例表:
prop.table(table(GC2$good_bad,GC2$employed),margin=2)
## Unemployed 1 Year 1-4 Years 4-7 Years >7 Years
## bad 0.3710 0.4070 0.3068 0.2241 0.2530
## good 0.6290 0.5930 0.6932 0.7759 0.7470
prop.table(table(GC2$good_bad,GC2$checking),margin=2)
## < 0 DM 0 <= ... < 200 DM >= 200 DM No Acc
## bad 0.4927 0.3903 0.2222 0.1168
## good 0.5073 0.6097 0.7778 0.8832
从前面的比例表中可以清楚地看出,如果申请人有 4 年以上的工作经验,那么还贷的可能性就比使用变量所确定的其他部分要高。 同样,如果checking贷款金额为>= 200 DM或No Acc,则良好的还款比例更高。 如果读者希望将数字可视化,可以在此处使用镶嵌图。
- 可以很容易地获得两个分类变量的镶嵌图:
windows(height=15,width=10)
par(mfrow=c(3,1))
mosaicplot(~good_bad+housing,GC2)
mosaicplot(~good_bad+employed,GC2)
mosaicplot(~good_bad+checking,GC2)
- 生成的镶嵌图如下所示:

比例可以很好地反映在镶嵌图上,因此我们在这里有一种有效的可视化方法。
工作原理...
R 函数table对于理解分类变量非常重要,它会根据因子水平对频率进行计数。 当prop.table应用于数组/表时,我们获得的列比例为 margin = 2。
马赛克图和条形图在分类数据分析中提供了直观的见解。 有了最初的印象,我们现在可以为统计方法建立统计技术,以识别客户最终会成为好付款人还是违约人。
判别分析
判别分析是最早的统计方法之一,用于将观察结果分类为不同的人群。 与许多早期的统计学先驱著作一样,这项技术也是伟大的 R. A. Fisher 爵士发明的。 判别分析的理论超出了本书的范围。 我们将只满足于使用MASS包中的lda函数。 在将lda R 技术应用于德国信用数据之前,我们将首先将其应用于iris数据集,这也受到费舍尔的欢迎。 在将该方法应用于iris数据集之后,我们将其应用于德国信用数据问题。
准备
默认 R 软件满足iris数据集和MASS包的要求。 读者需要像第一部分一样准备好GC2数据帧。
怎么做...
我们通过简单的功能对iris数据集有了初步的了解。 在此数据集中,我们有五个变量Sepal.Length,Sepal.Width,Petal.Length,Petal.Width和Species。 我们有三种类型的虹膜种类setosa,versicolor和virginica需要通过萼片和花瓣的长度和宽度来识别:
- 从
datasets包中加载iris对象,并使用str,summary和pairs函数获得初步了解:
data(iris)
str(iris)
## 'data.frame': 150 obs. of 5 variables:
## $ Sepal.Length: num 5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
## $ Sepal.Width : num 3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
## $ Petal.Length: num 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
## $ Petal.Width : num 0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
## $ Species : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
summary(iris)
## Sepal.Length Sepal.Width Petal.Length Petal.Width
## Min. :4.30 Min. :2.00 Min. :1.00 Min. :0.1
## 1st Qu.:5.10 1st Qu.:2.80 1st Qu.:1.60 1st Qu.:0.3
## Median :5.80 Median :3.00 Median :4.35 Median :1.3
## Mean :5.84 Mean :3.06 Mean :3.76 Mean :1.2
## 3rd Qu.:6.40 3rd Qu.:3.30 3rd Qu.:5.10 3rd Qu.:1.8
## Max. :7.90 Max. :4.40 Max. :6.90 Max. :2.5
## Species
## setosa :50
## versicolor:50
## virginica :50
##pairs(iris[,-5]) # Output suppressed
-
初步摘要给出了有关变量范围和其他基本摘要的想法。 希望有每种类型的物种的摘要,由对函数标识的散点图矩阵指示至少存在两种不同的物种。
-
现在,在
iris数据集上应用MASS包中的lda函数:
iris_lda <-lda(Species~.,iris)
iris_lda
## Call:
## lda(Species ~ ., data = iris)
##
## Prior probabilities of groups:
## setosa versicolor virginica
## 0.3333 0.3333 0.3333
##
## Group means:
## Sepal.Length Sepal.Width Petal.Length Petal.Width
## setosa 5.006 3.428 1.462 0.246
## versicolor 5.936 2.770 4.260 1.326
## virginica 6.588 2.974 5.552 2.026
##
## Coefficients of linear discriminants:
## LD1 LD2
## Sepal.Length 0.8294 0.0241
## Sepal.Width 1.5345 2.1645
## Petal.Length -2.2012 -0.9319
## Petal.Width -2.8105 2.8392
##
## Proportion of trace:
## LD1 LD2
## 0.9912 0.0088
- 通过运行行
lda(Species~.,...),我们要求 R 通过公式.~.创建lda对象,其中物类是需要使用数据集中所有其他变量进行识别的组指示符。 在这里,我们有两个线性判别函数,它是萼片和花瓣的四个变量的长度和宽度的线性组合。 第一个线性判别函数是


- 而第二个是


- 重要的是,第一个判别式本身在这里解释了 99%的痕迹,并且看起来足以识别这三个组。 现在,我们来看看在这三个物种中判别式的分数如何。
- 预测判别分数,并将直方图用于三种物种的分数:
iris_lda_values <-predict(iris_lda)
windows(height=20,width=10)
ldahist(iris_lda_values$x[,1],g=iris$Species)
- 结果图如下:

- 根据判别分数,我们可以清楚地看到三个不同的组。 最后,我们检查分类是否错误。
- 使用
table功能,评估该技术的性能:
table(predict(iris_lda)$class)
## setosa versicolor virginica
## 50 49 51
table(iris$Species,predict(iris_lda)$class)
## setosa versicolor virginica
## setosa 50 0 0
## versicolor 0 48 2
## virginica 0 1 49
- 注意,在预测中使用函数表可以得出每个物种的频率。 但是,它不能告诉我们正确地预测了哪种原始鸢尾属植物。 将表格与原始类别和预测类别一起使用会给我们带来错误分类计数。 总体而言,3/150 = 0.02%错误看起来非常令人印象深刻。 接下来,我们将此技术应用于德国信用数据。
lda功能现已应用于GC2:
GB_lda <-lda(good_bad~.,GC2)
GB_lda
## Call:
## lda(good_bad ~ ., data = GC2)
##
## Prior probabilities of groups:
## bad good
## 0.3 0.7
##
## Coefficients of linear discriminants:
## LD1
## checking0 <= ... < 200 DM 3.808e-01
## checking>= 200 DM 9.029e-01
## checkingNo Acc 1.317e+00
## telephonRegistered 2.159e-01
## foreignYes 7.212e-01
table(predict(GB_lda)$class)
##
## bad good
## 244 756
table(GC2$good_bad,predict(GB_lda)$class)
##
## bad good
## bad 158 142
## good 86 614
我们可以看到lda技术无法识别出将近 50%的不良贷款。 因此,在本章的其余部分中,我们需要对其进行改进并使用不同的方法。
工作原理...
MASS程序包中的lda功能可用于进行线性判别分析。
另请参见
有关判别分析技术的详细信息,请阅读 McLachlan(1992)。
划分数据和 ROC
如果使用整个数据集来构建模型,则可能是我们过度训练了模型。 结果是,对于未知情况,模型的真实性能没有引起注意。 从本质上讲,我们需要为信用问题建立一个良好的模型,如果在新的或无法预料的情况下业绩未知,那么怀疑论势必会渗入我们的脑海。 好的做法是将可用数据分成三个区域:(i)用于构建模型的数据,(ii)用于验证模型的数据,以及(iii)用于测试模型的数据。 因此,针对问题建立了一组模型,然后对数据的经过验证的部分进行评估,然后选择在此阶段表现最佳的模型作为数据的测试部分。 可以轻松地在三个区域中进行数据分区,并且我们可以快速显示如何对德国信贷数据进行分区。
接收操作曲线或 ROC 是访问分类模型性能的非常有效的工具。 我们从 Tattar(2013)的第 7 章中大量借用。 在许多分类模型中,如果预测的成功概率大于 0.5,则将该观察结果预测为成功观察结果,否则为失败观察结果。 至少通过训练和验证数据,我们知道观测值的真实标记,因此将真实标记与预测标记进行比较是有意义的。 在理想情况下,我们希望预测标签与实际标签完全匹配。 但是,实际上,这种情况很少发生,这意味着当真正的标签实际上是失败/成功时,有一些观察结果会被预测为成功/失败。
换句话说,我们会犯错误! 可以将这些注释以被广泛称为混淆矩阵的表格的形式放置。

我们考虑以下指标以便在各个模型之间进行比较:

现在,当我们遇到班级不平衡的问题时,具有非常高的准确性和精确度会更容易,但这将毫无用处。 例如,如果在 100 万个观察中有 100 个欺诈性交易,则将全部识别为良好交易的分类器将具有 99.99%以上的准确性和非常高的精度。 但是,此类模型甚至无法识别单个欺诈交易。 在这种情况下,ROC 方法非常有用。 ROC 构造需要两个指标:真阳性率(tpr)和假阳性率(fpr):

通过绘制 tpr 对 fpr 绘制 ROC 图。 对角线与随机分类器的性能有关,因为它仅显示是或否,而无需查看观察的任何特征。 任何好的分类器都必须位于此行上方,而不是显示在行上方。 尽管分类器未知,但它似乎比随机分类器更好。 与竞争性分类器相比,ROC 曲线很有用,因为如果一个分类器始终位于另一个分类器之上,那么我们选择前者。 与 ROC 相关的另一个重要指标是曲线下的面积(AUC)。 AUC 越高,模型越好,它是介于 0 和 1 之间的数字。
准备
正如本章前面部分所准备的,我们需要当前会话中的GC2对象。
怎么做...
可重现性是学习初期的重要方面。 我们为数据的随机分割设置了一个种子,以使读取的结果与在 R 环境中运行它们所获得的结果相同。
- 使用
set.seed可再现:
set.seed(1234567)
- 创建三个标签
Train,Validate和Test:
data_part_label <-c("Train","Validate","Test")
- 现在,从向量
data_part_label进行 1000 次采样(替换)(概率为 0.6、0.2 和 0.2),并相应地提取子集GC2:
indv_label =sample(data_part_label,size=1000,replace=TRUE,prob
=c(0.6,0.2,0.2))
GC_Train <-GC2[indv_label=="Train",]
GC_Validate <-GC2[indv_label=="Validate",]
GC_Test <-GC2[indv_label=="Test",]
现在,我们在GC_Train中大约有 600 个观测值,在GC_Validate和GC_Test中每个都有 200 个观测值。
- 从
ROCR包运行示例以提高性能:
example(performance)
在屏幕上看到的图表中,您会发现tpr与tfr相对。 理想的分类器将从 y 轴上的 1 开始。 为了进行跨模型比较,我们更喜欢 ROC 曲线始终高于其他曲线的模型。 例如,我们想为数据集的Train和Validate部分生成 ROC 曲线。 通常,Train部分的 ROC 曲线会好于Validate部分的 ROC 曲线。
拟合逻辑回归模型
仅当自变量/协变量的集合遵循多元正态分布时,判别分析技术才能很好地发挥作用。 在开始时,它不能通过排除类别变量来显示灵活性。 众所周知,许多经济变量(例如工资,储蓄等)不遵循正态分布,而且总体上也存在偏差。 因此,多元正态分布的假设相当局限,我们需要一个用于分类问题的通用框架。 Logistic 回归模型提供了非常重要的一类模型。 实际上,众所周知它具有非常好的理论特性。 例如,在理论上已知,在自变量遵循多元正态分布的情况下,逻辑回归模型可提供与判别分析一样高的准确性。 逻辑回归模型是重要指数族的成员,属于广义线性模型的一类。 给出独立观察的向量:


这里,我们有k个自变量,而随机变量Y是二元随机变量,并且是逻辑回归模型的因变量,请参阅《 Tattar(2013 年》第 7 章 )或 Tattar 等人的第 17 章。 (2016)了解更多详细信息和深度。 为了我们的目的,我们现在仅使用 R glm中的函数。 ROC 软件包ROCR和pROC对于执行与其相关的任务很有用。 首先,我们将拟合逻辑回归模型并获得摘要以找出哪些变量是重要的。
准备
除了工作会话中的GC2对象外,我们还需要 ROC 包ROCR和pROC来完成会话。
怎么做...
现在,我们将在这里构建一个逻辑回归模型:
- 使用
glm函数,我们为training数据集GC_Train建立了逻辑回归模型,然后应用summary函数来获取拟合模型的详细信息:
GC_Logistic <-glm(good_bad~.,data=GC_Train,family='binomial')
summary(GC_Logistic)
##
## Call:
## glm(formula = good_bad ~ ., family = "binomial", data = GC_Train)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -2.428 -0.725 0.374 0.715 2.222
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 1.42e+00 1.35e+00 1.05 0.2927
## checking0 <= ... < 200 DM 1.24e-01 2.86e-01 0.44 0.6634
## checking>= 200 DM 5.08e-01 4.81e-01 1.06 0.2913
## checkingNo Acc 1.65e+00 3.06e-01 5.40 6.7e-08 ***
## duration -2.95e-02 1.22e-02 -2.43 0.0153 *
## historyBank paid -2.20e-01 6.78e-01 -0.33 0.7451
## historyExisting paid 3.67e-01 5.29e-01 0.69 0.4880
## historyDelayed 3.93e-01 5.83e-01 0.67 0.4999
## historyDues Remain 1.47e+00 5.46e-01 2.69 0.0071 **
## purposeOld Car 1.58e+00 5.07e-01 3.12 0.0018 **
## purposeFurniture 8.66e-01 3.58e-01 2.42 0.0156 *
## purposeTelevision 6.42e-01 3.20e-01 2.01 0.0448 *
## purposeAppliance 6.17e-01 1.06e+00 0.58 0.5594
## purposeRepairs -3.12e-01 7.32e-01 -0.43 0.6699
## purposeEducation -5.74e-01 5.42e-01 -1.06 0.2896
## purposeRetraining 1.50e+01 8.96e+02 0.02 0.9867
## purposeBusiness 7.10e-01 4.36e-01 1.63 0.1032
## purposeOthers 1.50e+01 8.37e+02 0.02 0.9857
## amount -1.09e-04 5.74e-05 -1.90 0.0569 .
## savings100-500 DM 3.22e-01 3.77e-01 0.85 0.3931
## savings500-1000 DM 2.13e-01 4.69e-01 0.46 0.6491
## savings>1000 DM 7.19e-01 6.49e-01 1.11 0.2685
## savingsUnknown 1.24e+00 3.65e-01 3.39 0.0007 ***
## employed1 Year 1.69e-01 5.69e-01 0.30 0.7662
## employed1-4 Years 2.50e-01 5.53e-01 0.45 0.6518
## employed4-7 Years 7.69e-01 6.01e-01 1.28 0.2005
## employed>7 Years 2.62e-01 5.50e-01 0.48 0.6345
## installp -3.34e-01 1.17e-01 -2.84 0.0045 **
## maritalFemale M/D -2.30e-01 5.33e-01 -0.43 0.6661
## maritalMale M/D 5.49e-01 5.29e-01 1.04 0.2987
## maritalMale S 1.57e-01 6.18e-01 0.25 0.7992
## coappCo-app 4.02e-01 6.05e-01 0.66 0.5069
## coappGuarantor 1.34e+00 5.78e-01 2.33 0.0201 *
## resident 4.00e-02 1.10e-01 0.36 0.7162
## propertyBuilding society -4.23e-01 3.31e-01 -1.28 0.2019
## propertyOthers -2.47e-01 3.10e-01 -0.80 0.4250
## propertyUnknown -8.60e-01 5.66e-01 -1.52 0.1284
## age 1.32e-02 1.14e-02 1.16 0.2472
## housingOwn 1.28e-01 3.27e-01 0.39 0.6946
## housingFree 4.41e-01 6.27e-01 0.70 0.4824
## existcr -3.32e-01 2.57e-01 -1.29 0.1970
## jobUnskilled -5.35e-01 8.72e-01 -0.61 0.5395
## jobSkilled -4.76e-01 8.47e-01 -0.56 0.5739
## jobHighly Qualified -6.57e-01 8.65e-01 -0.76 0.4476
## depends -5.90e-01 3.25e-01 -1.81 0.0696 .
## telephonRegistered 2.99e-01 2.61e-01 1.14 0.2526
## foreignYes 1.47e+00 1.08e+00 1.36 0.1739
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 734.00 on 598 degrees of freedom
## Residual deviance: 535.08 on 552 degrees of freedom
## AIC: 629.1
##
## Number of Fisher Scoring iterations: 14
-
与常规回归模型一样,将具有
k因子的因子变量转换为 k-1 新变量。 这里,统计上无关紧要的变量是employed,marital,resident,property,age,housing,exist,job,telephone和foreign。 也就是说,在指定的 20 个变量中,有 10 个是不重要的,而其他的则是重要的。 在因子变量的情况下,如果任何一个水平是显着水平,则总体变量也将是显着水平。 经过 14 次迭代后,基础算法已收敛。 接下来,我们将研究训练部分模型的准确性。 -
使用
table函数,我们计算模型的准确性如下:
table(GC_Train$good_bad)
##
## bad good
## 181 418
table(GC_Train$good_bad, ifelse(predict(GC_Logistic,type="response")>0.5,"good","bad"))
##
## bad good
## bad 96 85
## good 46 372
- 在训练数据集中,我们有 181 笔不良贷款,逻辑回归模型可以正确识别其中的 96 笔,准确率略高于 50%。 当然,需要提高准确性。 但是,我们将首先检查模型在未用于构建模型的数据点上的性能以及进行 ROC 分析。
- 对于训练和验证分区,
predict通过拟合的 Logistic 回归模型进行类别识别。 同样,使用ROCR包中的prediction和performance功能来设置 ROC 曲线:

- 结果图如下:

-
从 ROC 曲线可以看出,在不可见的情况或验证部分中,逻辑回归的性能下降了,正如预期的那样。 接下来,我们在两个区域中为模型计算曲线度量下的面积。
-
使用
pROC软件包中的roc函数计算曲线下面积:

- 因此,逻辑回归模型在训练部分比验证部分具有更高的 AUC。 存在许多改进逻辑回归模型的方法,例如,可以研究模型选择问题,多重共线性问题等。 但是,这里不需要逻辑回归模型中的详细离题。 在此,AUC 差为 0.836-0.72 = 0.116,并且看起来非常高。 理想的是找到那些差异尽可能小的模型,这将意味着该模型已经了解了特征/模式,并且也能够很好地归纳。 在下一节中,我们将快速查看决策规则,在下一节中,我们将查看规则的生成方式。
工作原理...
glm功能非常通用,可用于拟合广义线性模型类别中的模型。 在这里,我们使用带有选项family='binomial'的逻辑回归模型进行拟合。 summary功能提供了拟合模型的详细信息。 table功能用于在训练中访问拟合模型的准确性,并验证部分数据。 给定独立变量的值,predict函数返回概率 P(Y = 1)。 ROCR软件包中的prediction和performance函数有助于设置 ROC 曲线,而pROC软件包中的roc函数用于计算 AUC。
另请参见
有关更多详细信息,请尝试?glm, library(help=ROCR)和library(help=pROC)。
决策树和规则
逻辑回归模型是一种强大的技术。 对于从业者而言,它在 p 值,预测阈值等方面存在一些困难。 决策规则提供了一个简单的框架,从业者应仅查看某些变量和值即可做出决策。 例如,如果客户致电银行服务台并试图确定他们是否有资格获得贷款,则呼叫中心员工会询问一些详细信息,例如年龄,收入,性别,现有贷款等,并告知 他们是否有资格获得贷款。 通常,使用一组决策规则来得出这样的决策。 同样,如果急诊患者在怀疑有心脏相关问题的情况下前往医院,则一套简单的规则可能有助于确定是胃病还是发作,在后一种情况下,医院可以开始必要的准备工作 因为每一分钟对诊断都至关重要。 重要的问题是如何得出这样一组决策规则。
决策规则很容易从决策树中得出,我们来看rpart示例。 我们将首先构建一个分类树,然后可视化决策树。 从树中提取决策规则,然后我们将使用表函数查看每个终端节点上的百分位数拆分。
准备
我们将需要rpart包中的kyphosis数据集。
怎么做...
- 使用
kyphosis数据,我们将了解决策树和规则: - 从
rpart包中加载kyphosis数据集:
library(rpart)
data(kyphosis)
- 使用
rpart函数构建分类树并可视化决策树:
kyphosis_rpart <-rpart(Kyphosis~.,kyphosis)
plot(kyphosis_rpart,uniform=TRUE)
text(kyphosis_rpart)
- 结果决策树如下:

- 可以使用
rattle包中的asRules函数提取树的规则:
asRules(kyphosis_rpart)
##
## Rule number: 3 [Kyphosis=present cover=19 (23%) prob=0.58]
## Start< 8.5
##
## Rule number: 23 [Kyphosis=present cover=7 (9%) prob=0.57]
## Start>=8.5
## Start< 14.5
## Age>=55
## Age< 111
##
## Rule number: 22 [Kyphosis=absent cover=14 (17%) prob=0.14]
## Start>=8.5
## Start< 14.5
## Age>=55
## Age>=111
##
## Rule number: 10 [Kyphosis=absent cover=12 (15%) prob=0.00]
## Start>=8.5
## Start< 14.5
## Age< 55
##
## Rule number: 4 [Kyphosis=absent cover=29 (36%) prob=0.00]
## Start>=8.5
## Start>=14.5
- 我们可以很容易地获得每个终端节点的案例数的计数,如下所示。
- 使用
rpart函数的where变量查找为案例分配了哪个终端节点:
kyphosis$where <-kyphosis_rpart$where
table(kyphosis$Kyphosis,kyphosis$where) ##
## 3 5 7 8 9
## absent 29 12 12 3 8
## present 0 0 2 4 11
- 现在,这些数字也可以通过简单的
table功能获得。 首先,我们将data.frame对象转换为data.table对象。 - 将
data.frame后凸畸变转换为data.table对象:
K2 <-data.table(kyphosis)
- 由于前面的树形图表明如果起始值大于 12.5,则个体中将不会出现驼背症,因此我们对其进行检查。
- 使用
data.table结构,通过Start变量找到比例计数:
K2[,prop.table(table(Kyphosis))]
## Kyphosis
## absent present
## 0.7901 0.2099
- 同样,在每个终端节点上验证比例计数:
K2[Start>=12.5,prop.table(table(Kyphosis))]
## Kyphosis
## absent present
## 0.95652 0.04348
K2[Start <12.5&Age <=35,prop.table(table(Kyphosis))]
## Kyphosis
## absent present
## 0.9 0.1
K2[Start <12.5&Age >35&Number <4.5,prop.table(table(Kyphosis))]
## Kyphosis
## absent present
## 0.5833 0.4167
K2[Start <12.5&Age >35&Number >=4.5,prop.table(table(Kyphosis))]
## Kyphosis
## absent present
## 0.3077 0.6923
因此,决策树创建数据集的分区,其中每个终端节点都具有尽可能高的因子变量大小写。 通常,决策树查看每个变量的不同值,并将数据相应地划分为不同的区域。 然后,它访问每个分区的性能,并选择该拆分,从而在每个终端节点上获得最大的精度增益。 因此,将数据递归拆分为多个区域,并结束该过程,直到每个终端节点都包含尽可能多的纯节点。
接下来,我们将为德国信贷数据构建决策树。
工作原理...
rpart软件包有助于构建分类,回归和生存树。 使用plot功能可以轻松可视化决策树。 我们已经从rattle包中使用asRules函数提取了决策树的规则。
另请参见
可以使用多个包(例如partykit,tree等)在 R 中构建决策树。
德国数据的决策树
我们为德国数据拟合了逻辑回归模型。 现在,我们将为其创建一个决策树。
准备
这里将需要GC2对象以及已分区的数据。 而且,需要拟合的逻辑回归模型。
怎么做...
我们将使用rpart包及其功能来创建决策树:
- 创建决策树并按以下方式绘制:
GC_CT <-rpart(good_bad~.,data=GC_Train)
windows(height=20,width=20)
plot(GC_CT,uniform =TRUE);text(GC_CT)
- 决策树图如下所示:

- 需要评估拟合树的属性。
- 检查复杂度参数和拟合树的重要变量:
table(GC_Train$good_bad,predict(GC_CT,type="class"))
##
## bad good
## bad 107 74
## good 32 386
GC_CT$cptable
## CP nsplit rel error xerror xstd
## 1 0.06262 0 1.0000 1.0000 0.06209
## 2 0.02486 3 0.8122 0.9503 0.06118
## 3 0.02210 6 0.7348 1.0055 0.06219
## 4 0.01842 8 0.6906 0.9724 0.06159
## 5 0.01657 12 0.6022 0.9779 0.06170
## 6 0.01000 13 0.5856 0.9724 0.06159
GC_CT$variable.importance
## checking duration amount history savings purpose coapp property
## 29.4516 15.0985 12.8465 11.2774 10.8025 9.7595 7.2114 6.7423
## employed housing age installp existcr job marital resident
## 6.7003 6.1063 3.6083 2.8071 1.6281 1.4417 1.3372 0.9983
- 因此,我们看到按重要性顺序排列的变量是检查,持续时间,数量,历史记录等。 在训练数据集中的 181 笔不良贷款中,我们使用 logistic 回归模型确定了 107 笔不良贷款,而只有 96 笔。 这是否确实是一种改进,将在以后评估。 现在,我们将完成 ROC 分析。
- 与逻辑回归模型一样,我们首先拟合 ROC 曲线,然后将其与早期的逻辑回归解决方案进行比较:
GC_CT_Train_Prob <-predict(GC_CT,newdata=GC_Train[,-21], type="prob")[,2]
GC_CT_Validate_Prob <-predict(GC_CT,newdata=GC_Validate[,-21], type="prob")[,2]
GB_CT_Train_roc <-roc(GC_Train$good_bad,GC_CT_Train_Prob)
GB_CT_Validate_roc <-roc(GC_Validate$good_bad,GC_CT_Validate_Prob)roc.test(GB_Logistic_Train_roc,GB_CT_Train_roc)
##
## DeLong's test for two correlated ROC curves
##
## data: GB_Logistic_Train_roc and GB_CT_Train_roc
## Z = 0.92, p-value = 0.4
## alternative hypothesis: true difference in AUC is not equal to 0
## sample estimates:
## AUC of roc1 AUC of roc2
## 0.8358 0.8206
roc.test(GB_Logistic_Validate_roc,GB_CT_Validate_roc)
##
## DeLong's test for two correlated ROC curves
##
## data: GB_Logistic_Validate_roc and GB_CT_Validate_roc
## Z = -0.2, p-value = 0.8
## alternative hypothesis: true difference in AUC is not equal to 0
## sample estimates:
## AUC of roc1 AUC of roc2
## 0.7198 0.7301
决策树的准确性获得更高的收益在统计上并不重要。 因此,逻辑回归模型和决策树的 ROC 曲线差别不大。 但是,如果我们忽略了 ROC 测试的结果,我们也可以说决策树具有更高的准确性。
工作原理...
rpart功能仍然有用。 此外,我们在书中首次应用了roc.test。
在决策树的上下文中已经取得了很大的进步,我们仅使用简单的决策树来限制自己。


浙公网安备 33010602011771号