精通-Pandas-第二版-全-
精通 Pandas 第二版(全)
原文:
annas-archive.org/md5/cc15886c82662bbefcd80b2317d3496d译者:飞龙
前言
pandas 是一个流行的 Python 库,全球的数据科学家和分析师都在使用它来处理和分析数据。本书介绍了 pandas 中一些实用的数据操作技术,用于在不同领域进行复杂的数据分析。它提供的功能和特性使得数据分析变得比许多其他流行语言(如 Java、C、C++和 Ruby)更加容易和快速。
本书适用对象
本书适合那些希望使用 pandas 探索高级数据分析和科学计算技术的数据科学家、分析师和 Python 开发人员。你只需要对 Python 编程有一些基本了解,并且熟悉基本的数据分析概念,就能开始阅读本书。
本书内容
第一章,pandas 和数据分析简介,将介绍 pandas,并解释它在数据分析流程中的作用。我们还将探讨 pandas 的一些流行应用,以及 Python 和 pandas 如何用于数据分析。
第二章,pandas 及其支持软件的安装,将介绍如何安装 Python(如有必要)、pandas 库以及所有必要的依赖项,适用于 Windows、macOS X 和 Linux 平台。我们还将探讨 pandas 的命令行技巧、选项和设置。
第三章,使用 NumPy 和 pandas 的数据结构,将快速介绍 NumPy 的强大功能,并展示它如何在使用 pandas 时让工作变得更加轻松。我们还将使用 NumPy 实现一个神经网络,并探索多维数组的一些实际应用。
第四章,使用 pandas 处理不同数据格式的 I/O,将教你如何读取和写入常见格式,如逗号分隔值(CSV),以及所有选项,还会介绍一些更为特殊的文件格式,如 URL、JSON 和 XML。我们还将从数据对象创建这些格式的文件,并在 pandas 中创建一些特殊的图表。
第五章,在 pandas 中索引和选择数据,将向你展示如何从 pandas 数据结构中访问和选择数据。我们将详细介绍基础索引、标签索引、整数索引、混合索引以及索引的操作。
第六章,在 pandas 中分组、合并和重塑数据,将考察能够重新排列数据的各种功能,并通过实际数据集来使用这些功能。我们还将学习数据分组、合并和重塑的技巧。
第七章,pandas 中的特殊数据操作,将讨论并详细介绍 pandas 中一些特殊数据操作的方法、语法和用法。
第八章,使用 Matplotlib 处理时间序列和绘图,将介绍如何处理时间序列和日期。我们还将探讨一些必要的主题,这些是您在使用 pandas 时需要了解的内容。
第九章,在 Jupyter 中使用 pandas 制作强大的报告,将探讨一系列样式和 pandas 格式化选项的应用。我们还将学习如何在 Jupyter Notebook 中创建仪表盘和报告。
第十章,使用 pandas 和 NumPy 进行统计学入门,将深入探讨如何利用 pandas 执行统计计算,涉及包和计算方法。
第十一章,贝叶斯统计与最大似然估计简要介绍,将探讨一种替代的统计方法——贝叶斯方法。我们还将研究关键的统计分布,并展示如何使用各种统计包在 matplotlib 中生成和绘制分布图。
第十二章,使用 pandas 进行数据案例研究,将讨论如何使用 pandas 解决实际的数据案例研究。我们还将学习如何使用 Python 进行网页抓取以及数据验证。
第十三章,pandas 库架构,将讨论 pandas 库的架构和代码结构。本章还将简要演示如何使用 Python 扩展提高性能。
第十四章,pandas 与其他工具的比较,将重点比较 pandas 与 R 以及其他工具,如 SQL 和 SAS。我们还将探讨切片和选择的相关内容。
第十五章,机器学习简要介绍,将通过简要介绍 scikit-learn 库进行机器学习,并展示 pandas 如何融入该框架中,从而结束本书内容。
为了从本书中获得最大的收益
执行代码时将使用以下软件:
-
Windows/macOS/Linux
-
Python 3.6
-
pandas
-
IPython
-
R
-
scikit-learn
对于硬件,没有特别的要求。Python 和 pandas 可以在 Mac、Linux 或 Windows 机器上运行。
下载示例代码文件
您可以从您的账户在 www.packt.com 下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问 www.packt.com/support 并注册,文件将直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
登录或注册并访问 www.packt.com。
-
选择 SUPPORT 选项卡。
-
点击 Code Downloads & Errata。
-
在搜索框中输入书名并按照屏幕上的说明操作。
下载文件后,请确保使用以下最新版本解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Mastering-Pandas-Second-Edition。如果代码有更新,将会在现有的 GitHub 仓库中进行更新。
我们还从我们丰富的书籍和视频目录中提供其他代码包,您可以在 github.com/PacktPublishing/ 查看它们!快去看看吧!
下载彩色图像
我们还提供了包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以在此下载: static.packt-cdn.com/downloads/9781789343236_ColorImages.pdf。
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。示例:“Python 具有内置的array模块用于创建数组。”
一段代码设置如下:
source_python("titanic.py")
titanic_in_r <- get_data_head("titanic.csv")
所有命令行输入或输出均按如下方式编写:
python --version
粗体:表示新术语、重要单词或屏幕上显示的单词。例如,菜单或对话框中的单词在文本中会以这种方式出现。示例:“其他目录中的任何笔记本可以通过‘上传’选项传输到 Jupyter Notebook 的当前工作目录中。”
警告或重要备注以此方式显示。
小贴士和技巧以此方式显示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提到书名,并通过电子邮件联系我们,地址为 customercare@packtpub.com。
勘误:虽然我们已尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能将错误报告给我们。请访问 www.packt.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并填写相关详情。
盗版:如果您在互联网上发现我们作品的任何非法复制形式,我们将不胜感激,如果您能提供其位置地址或网站名称。请通过电子邮件联系我们,地址为 copyright@packt.com,并附上链接。
如果您有兴趣成为作者:如果您在某个领域有专长,并且有意撰写或贡献书籍,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了本书,为什么不在您购买书籍的网站上留下评论呢?潜在读者可以看到并利用您的公正意见来做出购买决策,我们在 Packt 也能了解您对我们产品的看法,而我们的作者也能看到您对他们书籍的反馈。谢谢!
如需了解更多关于 Packt 的信息,请访问packt.com。
第一部分:数据分析与 pandas 概述
在本节中,我们将为你快速概述数据分析过程的概念,以及 pandas 在这一过程中的角色。你还将学习如何安装和设置 pandas 库,以及构建企业级数据分析管道所需的其他支持库和环境。
本节包含以下章节:
-
第一章,pandas 和数据分析简介
-
第二章,安装 pandas 及支持软件
第一章:pandas 和数据分析简介
我们从讨论当代数据分析的景观开始,介绍 pandas 在其中的角色。pandas 是数据科学家进行数据预处理任务的首选工具。我们将在后续章节中学习 pandas 的技术细节。本章涵盖了 pandas 的背景、起源、历史、市场份额和当前地位。
本章节被分成了以下标题:
-
数据分析的动机
-
如何使用 Python 和 pandas 进行数据分析
-
pandas 库的描述
-
使用 pandas 的好处
数据分析的动机
在本节中,我们讨论了使数据分析在当今快速发展的技术景观中成为越来越重要的趋势。
我们生活在一个大数据的世界里
大数据这个术语在过去两年中已成为最热门的技术关键词之一。我们现在越来越多地在各种媒体上听到有关大数据的报道,大数据初创公司也越来越多地吸引风险投资。在零售领域的一个好例子是 Target Corporation,该公司在大数据投资方面投入了大量资源,现在能够利用大数据分析顾客的在线购物习惯,识别潜在顾客;参见相关文章:nyti.ms/19LT8ic.
简单来说,大数据指的是数据量超过数据接收者处理能力的现象。以下是一篇关于大数据的文章,概括得很好:www.oracle.com/in/big-data/guide/what-is-big-data.html。
大数据的四个 V
开始思考大数据复杂性的一个好方法是称为四维或大数据的四个 V。这个模型最初由 Gartner 分析师 Doug Laney 在 2001 年提出。最初的三个 V 代表数据量、速度和多样性,第四个 V,真实性,后来由 IBM 添加。Gartner 的官方定义如下:
"大数据是指高数据量、高速率和/或高多样性信息资产,需要新的处理方式以实现增强决策、洞察发现和流程优化。"
Laney, Douglas. "大数据的重要性:定义", Gartner
大数据的数据量
大数据时代的数据量令人惊讶。根据 IBM 的数据,到 2020 年,地球上的数据总量将激增至 40 泽字节。没错!40 泽字节相当于 43 万亿吉字节。有关更多信息,请参阅维基百科关于泽字节的页面:en.wikipedia.org/wiki/Zettabyte。
要了解这么大的数据量,让我引用 2010 年 EMC 发布的新闻稿,该新闻稿说明了 1 泽字节大约等于多少:
“每个地球上的男人、女人和孩子在 100 年内‘连续发推’所创造的数字信息”或“75 亿个满载 16GB 的苹果 iPad,将填满温布利体育场 41 次、蒙特布朗隧道 84 次、CERN 的大型强子对撞机隧道 151 次、北京国家体育场 15.5 次或台北 101 大楼 23 次……”
EMC 研究预测到 2020 年数据将增长 45 倍
数据增长的速度主要受以下几个因素的推动:
-
互联网的快速增长。
-
从模拟到数字媒体的转变,加上数据捕捉和存储能力的增强,这一切得益于更便宜且更高效的存储技术。数字数据输入设备的普及,如相机和可穿戴设备,以及大容量数据存储成本的迅速下降,都是这一趋势的体现。亚马逊网络服务是这一趋势的典型例子,它推动了更便宜存储的普及。
设备的互联网化,即物联网现象,是指普通家用设备,如冰箱和汽车,将连接到互联网。这个现象将进一步加速上述趋势。
大数据的速度
从纯技术角度来看,速度指的是大数据的吞吐量,即数据进入和处理的速度。这直接影响到接收方处理数据的速度,以跟上数据流的进度。实时分析是处理这一特征的一种尝试。能够支持这一过程的工具包括亚马逊网络服务的弹性 MapReduce。
从更宏观的角度看,数据的速度也可以看作是数据和信息传输和处理速度的提升,现在的数据传输和处理速度比以往任何时候都要快,且可以传输更远的距离。
高速数据和通信网络的普及,以及手机、平板电脑和其他联网设备的出现,是推动信息速度的主要因素。一些速度的衡量标准包括每秒推文数和每分钟电子邮件数。
大数据的多样性
大数据的多样性源于多个数据源的存在,这些数据源产生数据并以不同格式输出数据。
这对数据接收方构成了技术挑战,因为他们需要处理这些数据。数字相机、传感器、网络、手机等都是产生不同格式数据的数据生成者,挑战在于如何处理这些格式并从数据中提取有意义的信息。随着大数据时代的到来,数据格式的不断变化促使数据库技术行业发生了革命,NoSQL 数据库应运而生,这些数据库能够处理所谓的非结构化数据,或是那些格式可变或不断变化的数据。
大数据的真实性
大数据的第四个特征——真实性,是后来添加的——指的是需要验证或确认数据的正确性,即数据是否代表事实。数据来源必须得到验证,且错误应保持在最小范围内。根据 IBM 的估计,差的数据质量每年使美国经济损失约 3.1 万亿美元。例如,2008 年,美国的医疗错误导致的损失为 195 亿美元;你可以参考这篇相关文章,了解更多信息:www.wolterskluwerlb.com/health/resource-center/articles/2012/10/economics-health-care-quality-and-medical-errors。
以下链接提供了 IBM 的一个信息图,概述了大数据的四个 V:www.ibmbigdatahub.com/infographic/four-vs-big-data。
数据如此庞大,分析时间却如此有限
数据分析被谷歌前 CEO 埃里克·施密特描述为一切的未来。欲了解更多信息,请查看 YouTube 视频《为什么数据分析是未来的一切》,链接:www.youtube.com/watch?v=9hDnO_ykC7Y。
在大数据时代,数据的量和速度将继续增加。能够高效地收集、筛选和分析数据,从而获得有助于他们更快满足客户需求的信息的公司,将在竞争中占据显著优势。例如,数据分析(度量文化)在亚马逊的商业战略中起着至关重要的作用。欲了解更多信息,请参阅 Smart Insights 提供的亚马逊案例研究,链接:bit.ly/1glnA1u。
向实时分析的转变
随着技术和工具的不断发展,以应对企业日益增长的需求,出现了所谓的实时分析趋势。更多信息可参考英特尔发布的随处可得的洞察白皮书,链接:intel.ly/1899xqo。
在大数据互联网时代,以下是一些大数据实时分析的例子:
-
在线企业要求即时了解他们在网上推出的新产品/功能的表现,并根据反馈调整在线产品组合。亚马逊是这一点的典型例子,特别是其查看此商品的客户还查看了功能。
-
在金融领域,风险管理和交易系统几乎要求进行即时分析,以便根据数据驱动的洞察做出有效决策。
数据分析管道
数据建模是利用数据构建预测模型的过程。数据还可以用于描述性分析和规范性分析。但在我们使用数据之前,必须先从多个来源获取数据,存储、整合、清洗并处理,以适应我们的目标。对数据进行的顺序操作类似于制造流水线,每个后续步骤为潜在的最终产品增加价值,并且每一步进展都需要新的人员或技能。
数据分析流程中的各个步骤如下图所示:

数据分析流程中的步骤
-
提取数据
-
转换数据
-
加载数据
-
读取与处理数据
-
探索性数据分析
-
创建特征
-
构建预测模型
-
验证模型
-
构建产品
这些步骤可以归纳为三个高级类别:数据工程、数据科学和产品开发。
-
数据工程:前图中的步骤 1到步骤 3属于此范畴。它涉及从各种来源获取数据,创建适合的数据库和表结构,并将数据加载到合适的数据库中。根据以下因素,可以有多种方法来执行此步骤:
-
数据类型:结构化数据(表格数据)与非结构化数据(如图像和文本)与半结构化数据(如 JSON 和 XML)
-
数据升级速度:批处理与实时数据流
-
数据量:分布式(或基于集群的)存储与单实例数据库
-
数据种类:文档存储、二进制大对象存储或数据湖
-
-
数据科学:图 1.2 中的步骤 4到步骤 8属于数据科学范畴。这是数据被处理为可用数据并用于预测未来、学习模式和推断这些模式的阶段。数据科学还可以进一步分为两个阶段。
第 4 步到第 6 步构成了第一阶段,其目标是更好地理解数据并使其可用。使数据可用需要付出大量的努力来清理数据,包括删除无效字符和缺失值。这还涉及对数据的细致理解——数据的分布是什么,不同数据变量之间有什么关系,输入和输出变量之间是否存在因果关系,等等。这还包括探索能够更好解释这种因果关系(输入和输出变量之间)的数值转换(特征)。这个阶段涉及的是最终使用数据的真正法医工作。用一个类比来说,竹子种子在土壤中埋藏多年,表面看不到任何幼苗的迹象,但突然间一颗幼苗长出来,并且几个月内一棵完整的竹子就准备好了。数据科学的这一阶段就类似于竹子种子在迅速生长之前所经历的地下准备工作。这就像是初创公司的隐形模式,在这个阶段投入了大量的时间和努力。而这也是本书主角pandas库找到其存在意义和最佳定位的地方。
第 7 步到第 8 步是学习历史数据中的模式(数学表达式的参数)并将其外推到未来数据的部分。这需要大量的实验和迭代以达到最佳结果。但是,如果第 4 步到第 6 步已经非常小心地完成,这一阶段可以通过 Python、R 及许多其他数据科学工具中的多个包快速实现。当然,这也需要对所应用模型背后的数学和算法有扎实的理解,以便微调其参数,达到完美效果。
- 产品开发:这是所有辛勤工作结出果实的阶段,所有的洞察、结果和模式都以用户能够消费、理解并付诸实践的方式呈现。它可能包括从构建带有附加派生字段的数据仪表板,到一个调用训练模型并在接收到数据时返回输出的 API。一个产品还可以构建成涵盖数据管道所有阶段的形式,从提取数据到构建预测模型或创建互动式仪表板。
除了这些管道步骤外,还有一些额外的步骤可能会出现在过程中。这是由于数据领域的高度发展性。例如,深度学习广泛用于构建围绕图像、文本和音频数据的智能产品,通常需要将训练数据进行分类标注,或者当数据量太小无法构建准确模型时进行增强。
例如,视频数据中的目标检测任务可能需要使用一些工具或甚至手动创建训练数据,标注目标边界和目标类别。数据增强通过创建轻微变动的数据(例如旋转或颗粒化的图像)并将其添加到训练数据中,帮助改善图像数据。在监督学习任务中,标签是必需的。这个标签通常是与数据一起生成的。例如,要训练一个客户流失预测模型,需要一个包含客户描述及其流失时间的数据集。这些信息通常可以通过公司的 CRM 工具获得。
Python 和 pandas 如何融入数据分析流程
Python编程语言是当前数据科学和分析新兴领域中增长最快的语言之一。Python 由 Guido van Rossum 于 1991 年创建,其主要特点包括:
-
解释型语言,而非编译型语言
-
动态类型系统
-
通过值传递与对象引用
-
模块化功能
-
综合性库
-
对其他语言的扩展性
-
面向对象
-
支持大多数主要的编程范式:过程式、面向对象式,以及在一定程度上,函数式编程
欲了解更多信息,请参阅以下关于 Python 的文章:www.python.org/about/
使 Python 在数据科学中受欢迎的特点之一是其非常友好的(人类可读的)语法,此外,它是解释型语言而非编译型语言(这意味着开发时间更短),并且拥有非常全面的库来解析和分析数据,以及其在数值和统计计算方面的能力。Python 拥有提供完整数据科学和分析工具包的库,主要包括以下几种:
-
NumPy: 强调数值计算的一般数组功能
-
SciPy: 数值计算
-
Matplotlib: 图形绘制
-
pandas: 序列和数据框(1D 和 2D 数组类型)
-
Scikit-learn: 机器学习
-
NLTK: 自然语言处理
-
Statstool: 统计分析
本书将重点介绍前述列表中的第四个库——pandas。
什么是 pandas?
本书中我们将重点研究的 pandas 可不是那些可爱且懒惰的动物,尽管它们有时也能在需要时施展功夫。
pandas是由 Wes McKinney 于 2008 年开发的一个高性能开源数据分析库,专为 Python 设计。pandas 代表面板数据,是指它处理数据时使用的表格格式。它是免费的,并在开源倡议下以 3 条款 BSD 许可证发布。
多年来,pandas 已经成为使用 Python 进行数据分析的事实标准库。该工具得到了广泛的应用,背后有着庞大的社区支持(1,200+位贡献者,17,000+次提交,23 个版本,15,000+个星标),并且该库在快速迭代、功能和增强方面持续更新。
pandas 的一些关键功能包括:
-
它可以处理各种格式的数据集:时间序列、异构表格数据和矩阵数据。
-
它便于从各种来源加载/导入数据,例如 CSV 和 SQL 数据库。
-
它可以对数据集执行各种操作:子集、切片、过滤、合并、分组、重新排序和重塑。
-
它可以根据用户/开发者定义的规则处理缺失数据,例如忽略、转换为 0 等。
-
它可以用于数据的解析和转换(munging),以及建模和统计分析。
-
它与其他 Python 库(如 statsmodels、SciPy 和 scikit-learn)集成良好。
-
它提供快速的性能,并且通过使用 Cython(Python 的 C 扩展)可以进一步加速。
如需更多信息,请查阅官方 pandas 文档:pandas.pydata.org/pandas-docs/stable/。
pandas 在数据分析管道中扮演什么角色?
如前所述,pandas 可用于在管道中执行 第 4 步 到 第 6 步。第 4 步 到 第 6 步 是任何数据科学过程、应用或产品的核心。

pandas 在数据分析管道中扮演什么角色?
第 1 步 到 第 6 步 可以通过 pandas 的某些方法执行。第 4 步 到 第 6 步 是主要任务,而 第 1 步 到 第 3 步 也可以通过某种方式在 pandas 中完成。
pandas 是处理数据时不可或缺的库,几乎不可能找到不导入 pandas 的数据建模代码。Python 中易于使用的语法以及类似电子表格的数据结构——DataFrame 的存在,使其即便是对于那些习惯于并不愿意脱离 Excel 的用户来说也很友好。同时,它深受科学家和研究人员的喜爱,能够处理诸如 Parquet、Feather 文件等特殊文件格式。它可以批量读取数据,而不会占用所有机器的内存。难怪著名新闻聚合网站 Quartz 称它为 *数据科学中的最重要工具。
pandas 非常适用于以下类型的数据集:
-
具有异构类型列的表格数据
-
有序和无序时间序列
-
带有标签或未标签行列的矩阵/数组数据
pandas 能够巧妙地执行以下数据操作:
-
简单处理缺失和 NaN 数据
-
列的添加和删除
-
自动和显式的数据对齐与标签
-
使用 split-apply-combine 进行数据的聚合和转换(GroupBy)
-
将不同索引的 Python 或 NumPy 数据转换为 DataFrame
-
切片、索引、层次化索引和数据子集的操作
-
合并、连接和拼接数据
-
支持平面文件、HDF5、Feather 和 Parquet 格式的 I/O 方法
-
时间序列功能
使用 pandas 的好处
pandas 是 Python 数据分析库的核心组成部分。pandas 的一个显著特点是它提供的一系列数据结构天然适用于数据分析,主要是 DataFrame,以及在较小程度上,series(一维向量)和 panel(三维表格)。
简单来说,pandas 和 statstools 可以被描述为 Python 对 R 语言的回应,R 是一种用于数据分析和统计编程的语言,提供了数据结构(如 R 的数据框)和丰富的统计库来进行数据分析。
与使用像 Java、C 或 C++这样的语言进行数据分析相比,pandas 的好处是显而易见的:
-
数据表示:它可以轻松以自然适合数据分析的形式表示数据,通过其 DataFrame 和 series 数据结构简洁地展示。用 Java/C/C++做相同的事需要编写大量的自定义代码,因为这些语言并不是为数据分析而设计的,而是为网络和内核开发而设计的。
-
数据子集和过滤:它允许轻松对数据进行子集化和过滤,这是数据分析中常见的操作。
-
简洁清晰的代码:它简洁明了的 API 让用户能够更加专注于核心目标,而不需要编写大量的模板代码来执行常规任务。例如,将一个 CSV 文件读取到 DataFrame 数据结构中只需要两行代码,而用 Java/C/C++做同样的任务则需要更多行代码或调用非标准库,下面将会展示。假设我们有以下数据需要读取:
| 国家 | 年份 | CO2 排放 | 能源消耗 | 生育率 | 每千人互联网使用率 | 预期寿命 | 人口 |
|---|---|---|---|---|---|---|---|
| 白俄罗斯 | 2000 | 5.91 | 2988.71 | 1.29 | 18.69 | 68.01 | 1.00E+07 |
| 白俄罗斯 | 2001 | 5.87 | 2996.81 | 43.15 | 9970260 | ||
| 白俄罗斯 | 2002 | 6.03 | 2982.77 | 1.25 | 89.8 | 68.21 | 9925000 |
| 白俄罗斯 | 2003 | 6.33 | 3039.1 | 1.25 | 162.76 | 9873968 | |
| 白俄罗斯 | 2004 | 3143.58 | 1.24 | 250.51 | 68.39 | 9824469 | |
| 白俄罗斯 | 2005 | 1.24 | 347.23 | 68.48 | 9775591 |
在 CSV 文件中,我们希望读取的数据会像下面这样:
Country,Year,CO2Emissions,PowerConsumption,FertilityRate,
InternetUsagePer1000, LifeExpectancy, Population
Belarus,2000,5.91,2988.71,1.29,18.69,68.01,1.00E+07
Belarus,2001,5.87,2996.81,,43.15,,9970260
Belarus,2002,6.03,2982.77,1.25,89.8,68.21,9925000
...
Philippines,2000,1.03,514.02,,20.33,69.53,7.58E+07
Philippines,2001,0.99,535.18,,25.89,,7.72E+07
Philippines,2002,0.99,539.74,3.5,44.47,70.19,7.87E+07
...
Morocco,2000,1.2,489.04,2.62,7.03,68.81,2.85E+07
Morocco,2001,1.32,508.1,2.5,13.87,,2.88E+07
Morocco,2002,1.32,526.4,2.5,23.99,69.48,2.92E+07
..
这些数据来自世界银行经济数据,网址为data.worldbank.org。
在 Java 中,我们需要编写以下代码:
public class CSVReader {
public static void main(String[] args) {
String[] csvFile=args[1];
CSVReader csvReader = new csvReader();
List<Map>dataTable=csvReader.readCSV(csvFile);
}
public void readCSV(String[] csvFile)
{
BufferedReader bReader=null;
String line="";
String delim=",";
//Initialize List of maps, each representing a line of the csv file
List<Map> data=new ArrayList<Map>();
try {
bufferedReader = new BufferedReader(new FileReader(csvFile));
// Read the csv file, line by line
while ((line = br.readLine()) != null){
String[] row = line.split(delim);
Map<String,String> csvRow=new HashMap<String,String>();
csvRow.put('Country')=row[0];
csvRow.put('Year')=row[1];
csvRow.put('CO2Emissions')=row[2]; csvRow.put('PowerConsumption')=row[3];
csvRow.put('FertilityRate')=row[4];
csvRow.put('InternetUsage')=row[1];
csvRow.put('LifeExpectancy')=row[6];
csvRow.put('Population')=row[7];
data.add(csvRow);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return data;
}
但是,使用 pandas,只需两行代码:
import pandas as pd
worldBankDF=pd.read_csv('worldbank.csv')
此外,pandas 是建立在 NumPy 库之上,因此继承了该库在数值和科学计算方面的许多性能优势。使用 Python 的一个常被提及的缺点是,作为一种脚本语言,相较于 Java/C/C++等语言,它的性能比较慢。然而,对于 pandas 来说,这种情况并不成立。
pandas 历史
pandas 的基础版本由 Wes McKinney 于 2008 年构建,他是麻省理工学院的毕业生,拥有丰富的量化金融经验。如今,由于他的开源贡献以及广受欢迎的书籍《使用 Python 进行数据分析》,他已成为名副其实的名人。据报道,他在工作中不得不使用当时流行的工具进行简单的数据操作任务(例如读取 CSV 文件)时感到非常沮丧。他表示,在发现 Excel 和 R 不适合自己的需求后,他很快爱上了 Python,因为它直观且易于上手。但他也发现,Python 缺少一些关键特性,这些特性本可以使它成为数据分析的首选工具——例如,处理电子表格数据的直观格式,或从现有列中创建新计算列的能力。
根据他接受 Quartz 采访时所述,在创建该工具时,他心中有以下设计考虑和愿景:
-
数据质量远比任何华丽的分析更为重要
-
将内存中的数据视作 SQL 表或 Excel 电子表格
-
使用简洁且优雅的代码进行直观分析和探索
-
与用于数据管道中相同或不同步骤的其他库的兼容性更强
在构建基础版本后,他继续攻读杜克大学的博士学位,但在追求将他所创建的工具成为数据科学和 Python 的基石的过程中辍学。凭借他专注的贡献,再加上 Matplotlib 等流行的 Python 可视化库的发布,随后是 Scikit-Learn 等机器学习库和 Jupyter、Spyder 等交互式用户界面的出现,pandas 以及最终的 Python 成为了任何数据科学家武器库中最炙手可热的工具。
Wes 全身心投入到他从零开始创建的工具的不断改进中。他协调新特性的发展以及现有功能的改进。数据科学社区欠他一份大恩。
pandas 的使用模式和普及
Python 的流行度近年来飙升,特别是 2012 年之后;这在很大程度上要归功于 pandas 的流行。Python 相关的问题占 Stack Overflow 这一开发者问答平台上来自高收入国家问题总数的约 12%。Stack Overflow 是一个开发者互相提问和获取解决方案的平台,涵盖如何完成任务和修复不同编程语言中的错误。考虑到有数百种编程语言,一个语言占据 12%的市场份额是一个非凡的成就:

2017-18 年 Kaggle 用户调查结果显示的最受欢迎的数据分析工具
根据 Kaggle 进行的一项调查,60%的受访者表示他们了解或已经使用 Python 从事数据科学工作。
根据 Stack Overflow 记录的数据,关于平台上提问的类型,Python 和 pandas 每年都在稳步增长,而其他一些编程语言,如 Java 和 C,已经逐渐失去人气,正处于追赶阶段。Python 几乎赶上了 Java 在该平台上的提问数量,而 Java 的提问数量呈负增长趋势。pandas 的提问数量则一直在稳步增长。
以下图表基于从 Stack Overflow 的 SQL API 收集的数据。y轴表示在特定年份,Stack Overflow 上关于该话题的提问数量:

基于 Stack Overflow 提问数量的工具流行度,按年份统计
Google Trends 也展示了 pandas 流行度的激增,以下图表展示了这一点。图中的数字表示相对于该地区和时间的历史最高点,pandas 兴趣的激增程度。

基于 Google Trends 数据,pandas 的流行度
pandas 的地理分布更为有趣。最高的关注度来自中国,这可能表明开源工具的广泛采用和/或对构建强大数据科学技术的高度倾向:

基于 Google Trends 数据,pandas 在不同地区的流行程度
除了在用户中广受欢迎外,pandas(由于其开源的起源)还有一个充满活力的社区,致力于不断改进它并使用户更容易找到相关问题的答案。以下图表显示了贡献者每周对 pandas 源代码的修改(增加/删除)情况:

贡献者对 pandas 源代码所做的增加/删除次数
pandas 在技术采用曲线中的位置
根据一个叫做Gartner Hype Cycle的流行框架,技术的普及和采用过程分为五个阶段:
-
技术触发
-
期望膨胀的顶峰
-
幻灭低谷
-
启蒙的斜坡
-
生产力高原
以下链接包含一张图表,展示了不同技术及其在技术采用曲线中的阶段:blogs-images.forbes.com/gartnergroup/files/2012/09/2012Emerging-Technologies-Graphic4.gif。
如图所示,预测分析已经达到了生产力的稳定高原,这是从技术中提取最佳和稳定投资回报的阶段。由于 pandas 是大多数预测分析项目的关键组成部分,可以安全地说,pandas 已经达到了生产力的高原。
pandas 的流行应用
pandas 构建于 NumPy 之上。除了所有其他数据科学项目外,pandas 的一些显著应用包括:
-
pandas 是 statsmodels 的依赖库(
www.statsmodels.org/stable/index.html),使其成为 Python 数值计算生态系统的重要组成部分。 -
pandas 已被广泛应用于许多金融应用程序的开发中。
摘要
我们生活在一个大数据时代,其特点是四个 V —— 数据的体量(volume)、速度(velocity)、多样性(variety)和真实性(veracity)。数据的体量和速度在可预见的未来将持续增长。能够利用和分析大数据,从中提取信息并根据这些信息做出可行决策的公司,将在市场竞争中获胜。Python 是一种快速发展的、用户友好的、可扩展的语言,在数据分析领域非常受欢迎。
pandas 是 Python 数据分析工具包中的核心库。它提供了许多功能,使其在许多流行的编程语言(如 Java、C、C++ 和 Ruby)中显得更加易用且高效。
因此,考虑到本章中阐述的 Python 作为数据分析工具的优势,以及它在用户、贡献者和行业领袖中的广泛应用,使用 Python 进行数据分析的从业者应该熟练掌握 pandas,以提高效率。本书旨在帮助你实现这一目标。
在下一章中,我们将通过首先搭建所需的基础设施来实现这一目标,以便在你的计算机上运行 pandas。我们还将看到 pandas 可以应用和运行的不同方式和场景。
参考文献
-
activewizards.com/blog/top-20-python-libraries-for-data-science-in-2018/ -
qz.com/1126615/the-story-of-the-most-important-tool-in-data-science/
第二章:pandas 和支持软件的安装
在我们开始使用 pandas 进行数据分析之前,我们需要确保软件已安装且环境正常工作。本章涉及 Python(如有必要)、pandas 库及 Windows、macOS/X 和 Linux 平台所需的所有依赖项的安装。我们将讨论的主题包括选择 Python 版本、安装 Python 和安装 pandas 等内容。
以下部分列出的步骤大部分应该是适用的,但根据设置的不同,效果可能有所不同。在不同操作系统版本中,脚本可能并不总是能完美运行,而且系统中已经安装的第三方软件包有时可能会与提供的说明发生冲突。
本章将涵盖以下主题:
-
选择使用的 Python 版本
-
使用 Anaconda 安装 Python 和 pandas
-
pandas 的依赖包
-
查看使用 Anaconda 安装的项目
-
跨工具整合——将 pandas 的强大功能与 R、Julia、H20.ai、Azure ML Studio 命令行技巧结合使用
-
Pandas 的选项和设置
选择使用的 Python 版本
这是 Python 开发者之间的经典之争——Python 2.7.x 还是 Python 3.x,哪个更好?直到一年前,Python 2.7.x 一直是排名第一的版本;原因是它是一个稳定的版本。2016 年,超过 70% 的项目使用 Python 2.7。这个比例开始下降,到 2017 年降至 63%。这种趋势的转变是因为宣布从 2018 年 1 月 1 日起,Python 2.7 将不再维护,意味着不会有更多的错误修复或新版本发布。一些在此公告后发布的库仅与 Python 3.x 兼容。许多企业已经开始向 Python 3.x 迁移。因此,从 2018 年开始,Python 3.x 成为首选版本。
如需更多信息,请参见 wiki.python.org/moin/Python2orPython3。
Python 2.x 和 3.x 之间的主要区别包括 Python 3 更好的 Unicode 支持,print 和 exec 被更改为函数,以及整数除法。详细信息请参见 Python 3.0 中的新特性,网址为 docs.python.org/3/whatsnew/3.0.html。
然而,对于科学、数值或数据分析工作,推荐使用 Python 2.7 而不是 Python 3,原因如下:Python 2.7 是大多数当前发行版的首选版本,而 Python 3.x 在某些库的支持上并不强劲,尽管这个问题正逐渐得到解决。
作为参考,可以查看名为科学家们会转向 Python 3 吗?的文档,网址为bit.ly/1DOgNuX。因此,本书将在需要时混合使用 Python 2.7 和 3.x 版本。将 Python 2.7 的代码转换为 3.x,或反之并不困难,以下文档可以作为参考:将 Python 2 代码移植到 Python 3,网址为docs.python.org/2/howto/pyporting.html。
然而,还是有一个中间方案,可以兼顾两者的优点。可以使用 Python 中的virtualenv包,它允许你在已安装的 Python 环境中创建独立的轻量级虚拟环境。这使得例如你可以在机器上安装 2.7 版本,并通过启动虚拟环境在计算机上访问和运行 3.x 版本的代码。这个虚拟环境只是一个位于不同位置的 Python 独立安装/实例。可以安装与该版本兼容的包,并在运行时引用该版本/安装进行所有计算。你可以创建任意多个虚拟环境。这个包在 Anaconda 发行版中预装。你可以访问以下网站,了解更多关于如何使用virtualenv的信息:docs.python-guide.org/dev/virtualenvs/。
pandas 的最新主要版本是 pandas 0.23.4,发布于 2018 年 8 月。以下是一些有趣的功能升级:
-
现在,读取和写入 JSON 的操作变得更加优雅,因为在设置
orient = True选项时,元数据将被保留。 -
对于 Python 3.6 及以上版本,字典将根据插入实体的顺序分配顺序。这个顺序会被传递到由字典创建的 DataFrame 或系列中。
-
合并和排序现在可以同时使用索引名称和列名称的组合。
-
之前,
DataFrame.apply()函数使用axis = 1时返回的是一个类似列表的对象。最新的 pandas 改进修改了输出,使其具有一致的形状——要么是系列,要么是 DataFrame。 -
现在,可以通过
observed = True设置,在groupby函数中控制没有任何观察值的类别。 -
DataFrame.all()和DataFrame.any()现在接受axis=None,以便在所有轴上汇总为一个标量。
但在我们开始使用 pandas 之前,先花些时间在我们的计算机上安装 Python 吧。
独立安装 Python
在这里,我们详细介绍了在多个平台(Linux、Windows 和 macOS/X)上独立安装 Python 的方法。独立安装指的是只安装 IDLE IDE、解释器和一些基本包。另一个选择是从发行版中下载,这是一种更丰富的版本,预安装了许多实用工具。
Linux
如果你使用的是 Linux,Python 很可能会预装。如果不确定,可以在命令提示符下输入以下命令:
which python
Python 可能会出现在 Linux 系统的以下文件夹之一,具体取决于你的发行版和安装方式:
-
/usr/bin/python -
/bin/python -
/usr/local/bin/python -
/opt/local/bin/python
你可以通过在命令提示符下输入以下命令来确定已安装的 Python 版本:
python --version
在极少数情况下,如果 Python 没有预装,你需要弄清楚你使用的是哪种 Linux 版本,然后下载并安装。以下是安装命令以及各种 Linux Python 发行版的链接:
- Debian/Ubuntu(14.04):
sudo apt-get install python2.7
sudo apt-get install python2.7-devel
如需更多信息,请参见 Debian Python 页面:wiki.debian.org/Python。
- Redhat Fedora/Centos/RHEL:
sudo yum install python
sudo yum install python-devel
要安装 Fedora 软件,请访问 docs.fedoraproject.org/en-US/Fedora/13/html/User_Guide/chap-User_Guide-Managing_software.html。
- openSUSE:
sudo zypper install python
sudo zypper install python-devel
有关安装软件的更多信息,请访问 en.opensuse.org/YaST_Software_Management。
- Slackware:对于这个 Linux 发行版,最好从源代码下载压缩的 tarball 并安装,具体操作请参见以下部分。
从压缩 tarball 安装 Python
如果以上方法都无法解决问题,你还可以下载压缩的 tarball(XZ 或 Gzip 格式),然后进行安装。以下是简要的步骤概述:
#Install dependencies
sudo apt-get install build-essential
sudo apt-get install libreadline-gplv2-dev libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev
#Download the tarball
mkdir /tmp/downloads
cd /tmp/downloads
wget http://python.org/ftp/python/2.7.5/Python-2.7.5.tgz
tar xvfz Python-2.7.5.tgz
cd Python-2.7.5
# Configure, build and install
./configure --prefix=/opt/python2.7 --enable-shared
make
make test
sudo make install
echo "/opt/python2.7/lib" >> /etc/ld.so.conf.d/opt-python2.7.conf
ldconfig
cd ..
rm -rf /tmp/downloads
有关此操作的信息可以在 Python 下载页面找到:www.python.org/download/。
Windows
与 Linux 和 Mac 发行版不同,Python 在 Windows 上并不预装。
核心 Python 安装
标准方法是使用来自 CPython 团队的 Windows 安装程序,这些程序是 MSI 包。可以在此处下载 MSI 包:www.python.org/download/releases/2.7.6/。
根据你的 Windows 系统是 32 位还是 64 位,选择相应的 Windows 包。Python 默认安装到包含版本号的文件夹中,因此在这种情况下,它将被安装到以下位置:C:\Python27。
这使得你可以无问题地运行多个版本的 Python。安装后,以下文件夹应该被添加到 PATH 环境变量中:C:\Python27\ 和 C:\Python27\Tools\Scripts。
安装第三方 Python 和包
有一些 Python 工具需要安装,以便更轻松地安装其他包(如 pandas)。安装 Setuptools 和 pip。Setuptools 对安装其他 Python 包(如 pandas)非常有用。它增强了标准 Python 分发版中 distutils 工具所提供的打包和安装功能。
要安装 Setuptools,请从以下链接下载 ez_setup.py 脚本:bitbucket.org/pypa/setuptools/raw/bootstrap。
然后,将其保存到 C:\Python27\Tools\Scripts。
然后,运行 ez_setup.py:C:\Python27\Tools\Scripts\ez_setup.py。
pip 相关命令为开发者提供了一个易于使用的命令,能快速、轻松地安装 Python 模块。请从以下链接下载 get-pip 脚本:www.pip-installer.org/en/latest/。
然后,从以下位置运行它:C:\Python27\Tools\Scripts\get-pip.py。
作为参考,你还可以阅读名为 在 Windows 上安装 Python 的文档:docs.python-guide.org/en/latest/starting/install/win/。
在 Windows 上,也有一些第三方 Python 提供商,使得安装任务更加简便。它们列举如下:
-
Enthought:
enthought.com/ -
Continuum Analytics:
www.continuum.io/ -
Active State Python:
www.activestate.com/activepython
macOS/X
Python 2.7 已预装在当前和近五年的 macOS X 版本中。预装的由 Apple 提供的版本可以在 Mac 的以下文件夹中找到:
-
/System/Library/Frameworks/Python.framework -
/usr/bin/python
然而,你也可以从 www.python.org/download/ 安装自己的版本。需要注意的是,这样你将有两个 Python 安装版本,你需要小心确保路径和环境的分隔清晰。
使用包管理器进行安装
Python 也可以通过包管理器在 Mac 上安装,例如 Macports 或 Homebrew。我将讨论使用 Homebrew 安装的过程,因为它看起来是最用户友好的。作为参考,你可以阅读名为 在 macOS X 上安装 Python 的文档:docs.python-guide.org/en/latest/starting/install/osx/。以下是步骤的摘要:
- 安装 Homebrew 并运行以下命令:
ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)"
-
然后,你需要将 Homebrew 文件夹添加到
PATH环境变量的顶部。 -
在 Unix 提示符下安装 Python 2.7:
brew install python
- 安装第三方软件 – distribute 和 pip。安装 Homebrew 会自动安装这些包。distribute 和 pip 使你能够轻松下载并安装/卸载 Python 包。
使用 Anaconda 安装 Python 和 pandas
在单独安装 Python 后,每个库都必须单独安装。确保新安装的库与相关依赖项的版本兼容会稍显麻烦。这时,像 Anaconda 这样的第三方发行版就派上用场了。Anaconda 是 Python/R 最广泛使用的发行版,专为开发可扩展的数据科学解决方案而设计。
什么是 Anaconda?
Anaconda 是一个开源的 Python/R 发行版,旨在无缝管理包、依赖关系和环境。它兼容 Windows、Linux 和 macOS,并需要 3 GB 的磁盘空间。由于需要下载和安装大量 IDE 和超过 720 个包,因此需要这一内存。例如,NumPy 和 pandas 是 Anaconda 中预装的两个包。
下图总结了 Anaconda 发行版的组成部分。每个组件已在列表中总结:
-
ANACONDA NAVIGATOR:一个入口,访问所有 IDE 和工具
-
ANACONDA PROJECT:使用带有文本指南、代码片段及其输出的笔记本保存可重复的实验
-
数据科学库:包括预安装的 IDE、科学计算、可视化和机器学习的包
-
CONDA:一个基于命令行的包管理器,用于安装、卸载和升级包/库:

Anaconda 下的工具
为什么选择 Anaconda?
Anaconda 使得程序开发变得更加容易,是初学者的首选工具。Anaconda 提供了一个简单的系统,来设置和管理独立的编程环境,确保不同包在应用程序中保持兼容性。这有助于顺利的协作和部署。在安装或更新包时,Anaconda 会确保依赖关系兼容,并在需要时自动更新依赖项。
安装 Anaconda
Anaconda 安装程序分别提供了 32 位操作系统和 64 位操作系统的版本。此外,Python 2.7 和 Python 3.7 也有不同的安装程序。可以从 www.anaconda.com/download/ 下载。该网站会显示以下选项,如截图所示:

针对不同平台的 Anaconda 安装程序
下载完成后,按照以下步骤在 Windows 机器上安装 Anaconda。
Windows 安装
下载 Windows 安装程序(32 位/64 位版本)。
-
下载完成后,启动
.exe文件。 -
按照指示操作并接受许可协议。
-
指定安装目标路径。
这样的对话框应该会引导你完成接下来的步骤。除非你希望自定义目标文件夹或不想安装某些功能,否则选择默认选项:

Windows 版 Anaconda 安装
如果你是 Mac 用户,完成下载后,按照以下步骤在 Windows 机器上安装 Anaconda。
macOS 安装
下载 macOS 安装程序(32 位/64 位版本):
-
下载完成后,通过双击
.pkg文件启动安装。 -
按照指示操作并接受许可协议。
-
推荐将 Anaconda 安装在用户的主目录中。
安装时,应该会出现这样的对话框。只需按照指示操作即可:

macOS 版 Anaconda 安装
按照以下步骤进行 Linux 安装。
Linux 安装
下载 macOS 安装程序(32 位/64 位版本):
- 在终端输入以下命令开始安装。
对于 Python 3.7 安装程序,输入以下命令:
bash ~/Downloads/Anaconda3-5.3.0-Linux-x86_64.sh
对于 Python 2.7 安装程序,输入以下命令:
bash ~/Downloads/Anaconda2-5.3.0-Linux-x86_64.sh
-
输入
Yes以接受许可协议。 -
接受默认的安装位置或选择一个新位置来完成安装。
云端安装
Anaconda 也可以安装在 AWS 和 Azure 等流行云基础设施提供商提供的云机器上。根据你在云账户中选择的实例类型(Linux 或 Windows),可以按照之前描述的类似步骤在云机器上安装 Anaconda。
你可以通过两种方式在云机器上运行 Anaconda:
-
选择一台预先安装了 Anaconda 的机器——AWS 和 Azure 提供许多预装了软件包的机器。你可以选择一台已安装 Anaconda 的机器。以下链接提供了更多详情:
aws.amazon.com/marketplace/seller-profile?id=29f81979-a535-4f44-9e9f-6800807ad996 -
选择一台 Linux/Windows 机器并在其上安装 Anaconda。这是一个更好的、更经济的选项,提供更多的灵活性,只需要稍微的安装工作。一旦启动云机器,安装步骤与之前的步骤类似。以下链接提供了使用云进行安装的完整步骤,
chrisalbon.com/aws/basics/run_project_jupyter_on_amazon_ec2/。
其他以数字和分析为重点的 Python 发行版
以下概述了除了 Anaconda 之外的各种第三方数据分析相关的 Python 发行版。所有以下发行版都包括 pandas:
-
Enthought Canopy:这是一个综合性的 Python 数据分析环境。欲了解更多信息,请访问
www.enthought.com/products/canopy/。 -
Python(x,y):这是一个免费的、面向科学计算、数据分析和可视化的 Python 发行版,适用于数值计算。它基于 Qt GUI 包和 Spyder 交互式科学开发环境。欲了解更多信息,请参阅
python-xy.github.io/。 -
WinPython:这是一个面向 Windows 平台的免费的开源 Python 发行版,专注于科学计算。欲了解更多信息,请参阅
winpython.sourceforge.net/。
有关 Python 发行版的更多信息,请访问 bit.ly/1yOzB7o。
pandas 的依赖包
请注意,如果你使用的是 Anaconda 发行版,则无需单独安装 pandas,因此也不需要担心安装依赖项。但了解 pandas 背后使用的依赖包仍然是有益的,有助于更好地理解其功能。
截至撰写时,pandas 的最新稳定版本为 0.23.4 版本。各种依赖包及其相关下载位置如下:
| Package | 必需 | 描述 | 下载位置 |
|---|---|---|---|
NumPy : 1.9.0 或更高版本 |
必需 | 用于数值运算的 NumPy 库 | www.numpy.org/ |
python-dateutil 2.5.0 |
必需 | 日期操作和工具库 | labix.org/ |
Pytz |
必需 | 时区支持 | sourceforge.net/ |
Setuptools 24.2.0 |
必需 | 打包 Python 项目 | setuptools.readthedocs.io/en/latest/ |
Numexpr |
可选,推荐 | 加速数值运算 | code.google.com/ |
bottleneck |
可选,推荐 | 性能相关 | berkeleyanalytics.com/ |
Cython |
可选,推荐 | 用于优化的 Python C 扩展 | cython.org/ |
SciPy |
可选,推荐 | Python 科学工具集 | scipy.org/ |
PyTables |
可选 | 用于 HDF5 存储的库 | pytables.github.io/ |
matplotlib |
可选,推荐 | 类似 Matlab 的 Python 绘图库 | sourceforge.net/ |
statsmodels |
可选 | Python 的统计模块 | sourceforge.net/ |
Openpyxl |
可选 | 用于读取/写入 Excel 文件的库 | www.python.org/ |
xlrd/xlwt |
可选 | 用于读取/写入 Excel 文件的库 | python-excel.org/ |
Boto |
可选 | 用于访问 Amazon S3 的库 | www.python.org/ |
BeautifulSoup 和 html5lib 或 lxml |
可选 | read_html() 函数所需的库 |
www.crummy.com/ |
html5lib |
可选 | 用于解析 HTML 的库 | pypi.python.org/pypi/html5lib |
Lmxl |
可选 | 用于处理 XML 和 HTML 的 Python 库 | lxml.de/ |
Anaconda 安装项回顾
Anaconda 安装了超过 200 个包和多个 IDE。一些广泛使用的包包括:NumPy、pandas、scipy、scikit-learn、matplotlib、seaborn、beautifulsoup4、nltk 和 dask。
通过 Conda(Anaconda 的包管理器)可以手动安装未随 Anaconda 一起安装的包。任何包的升级也可以通过 Conda 进行。Conda 会从 Anaconda 仓库获取包,Anaconda 仓库非常庞大,包含超过 1400 个包。以下命令将通过 conda 安装和更新包:
-
安装方法:使用
conda install pandas -
更新方法:使用
conda update pandas
以下是 Anaconda 自带的 IDE:
-
JupyterLab
-
Jupyter Notebook
-
QTConsole
-
Spyder
这些 IDE 可以通过 Conda 或 Anaconda Navigator 启动。
Anaconda Navigator 是一个图形用户界面(GUI),可以帮助你管理环境和包,并启动 Jupyter Notebook 和 Spyder 等应用程序。本质上,Navigator 提供了一个无需命令行编码的简易界面,并且适用于 Windows、Linux 和 macOS 系统。
如下图所示,Anaconda 提供了一个一站式平台,可以访问 Python 的 Jupyter/Spyder/IPython IDE 以及 RStudio IDE:

Anaconda Navigator
JupyterLab
JupyterLab 是一个用于整合笔记本、文档和活动的工具。其一些显著特点如下:
-
它具有拖放功能,可以在笔记本之间重新排列、移动和复制单元格。
-
它可以从文本文件(
.py、.R、.md、.tex等)中交互式地运行代码块,支持在 Jupyter Notebook 中运行。 -
它可以将代码控制台链接到笔记本内核,允许交互式地探索代码,而不必在笔记本中堆积临时的草稿工作。
-
它可以支持实时预览并编辑流行的文件格式,如 Markdown、JSON、CSV、Vega、VegaLite 等。
GlueViz
Glue 是一个有用的 Python 库,用于探索相关数据集之间及其内部的关系。其主要特点包括:
-
关联统计图形:Glue 帮助用户从数据中创建散点图、直方图和二维或三维图像。
-
灵活的数据关联:Glue 使用逻辑链接来叠加不同数据集之间的不同数据可视化,并跨数据集传递选择。这些链接需要由用户指定,并且可以灵活定义。
-
完整脚本功能:Glue 是用 Python 编写的,并基于其标准科学库(即 NumPy、Matplotlib 和 Scipy)构建。对于数据输入、清理和分析,用户可以轻松地结合自己的 Python 代码。
-
Orange:Orange 提供开源机器学习、数据可视化和互动数据分析工作流。它的独特卖点是通过基于图形界面的环境进行互动数据可视化和可视化编程。
-
Visual Studio Code:VS Code 或 Visual Studio Code 是一个轻量级但功能强大的源代码编辑器,可在桌面上运行,并且适用于 Windows、macOS 和 Linux。它内置支持 JavaScript、TypeScript 和 Node.js,并且拥有丰富的扩展生态系统,支持其他语言(如 C++、C#、Java、Python、PHP 和 Go)及运行时(如 .NET 和 Unity)。
Jupyter Notebook 和 Spyder 演示
让我们快速了解一下两款广泛使用的 Python IDE——Jupyter Notebook 和 Spyder。
Jupyter Notebook
Jupyter 在安装 Anaconda 时一起安装。如果没有 Anaconda,您可以在终端执行以下命令来安装 Jupyter:
pip install jupyter
可以通过 Anaconda Navigator 打开 Jupyter Notebook,或者点击开始菜单中的图标,或在 Conda 中输入以下命令来打开:
jupyter notebook
Jupyter Notebook 在浏览器中打开。启动目录中的所有文件夹都可以从 Jupyter 访问。然而,Jupyter 启动后,主目录是无法更改的。Jupyter 启动时会创建一个本地 Python 服务器:

Jupyter 首页
可以通过点击 新建 按钮打开新的笔记本。新笔记本会被创建为 Untitled.ipynb,这与其他 Python IDE 不同,在其他 IDE 中脚本是以 .py 后缀存储的。在这里,ipynb 代表 IPython Notebook。.ipynb 文件只是一个文本文件,它将所有内容——代码、Markdown 文本以及任何图像或图表——转换为 JSON 格式的元数据:

Jupyter Notebook
Jupyter Notebook 由多个单元组成;单元是存放代码的地方。单元可以用来显示 Markdown 代码或执行代码。在上面的截图中,前三个单元已经转换为 Markdown 单元,而接下来的三个单元是代码单元。可以通过点击 运行 按钮或按 Ctrl + Enter 来运行单元中的代码。
Jupyter Notebook 具有 保存和检查点 选项(快捷键:Ctrl + S)。Jupyter 会每 120 秒自动保存并创建一个检查点。这个检查点有助于恢复未保存的工作,也有助于回退到之前的检查点。
Spyder
Spyder 可以通过 pip 或 Anaconda 安装,就像 Jupyter 一样。然而,Spyder 的开发者推荐通过 Anaconda 安装。
Spyder 也可以通过与 Jupyter Notebook 类似的方法启动,或者在终端中输入spyder来启动:

Spyder
Spyder 具有脚本编辑器和 IPython 控制台。右上角的窗格可以在帮助浏览器、变量资源管理器和文件资源管理器之间切换。编辑器可以分割成多个单元格,更加系统化地编程。IPython 控制台在使用小代码片段时非常有用。变量资源管理器提供了所有 Python 会话中全局对象的摘要。
跨工具操作 – 将 pandas 的强大与 R、Julia、H20.ai 和 Azure ML Studio 结合
在数据操作、数据清理或处理时间序列数据等应用中,pandas 可以视为一种“神奇工具”。它非常快速高效,且强大到足以处理小到中等规模的数据集。最棒的是,pandas 的使用不仅仅局限于 Python。通过一些方法,可以在其他框架中利用 pandas 的强大功能,如 R、Julia、Azure ML Studio 和 H20.ai。这种在其他工具中使用优越框架的好处的方法被称为跨工具操作,且在实践中应用非常广泛。其存在的主要原因之一是,几乎不可能有一个工具能具备所有功能。假设某个任务有两个子任务:子任务 1 可以在 R 中完成,而子任务 2 可以在 Python 中完成。可以通过在 R 中完成子任务 1 并通过调用 Python 代码来完成子任务 2,或者在 Python 中完成子任务 2 并通过调用 R 代码来完成子任务 1 来处理。
这个选项使得 pandas 变得更强大。让我们看看 pandas 方法和/或 Python 代码如何与其他工具一起使用。
使用 pandas 与 R
R 有一种叫做DataFrame的对象类,这与 pandas DataFrame 相同。然而,R 中的 DataFrame 速度比 pandas 慢了好几倍。因此,学习 pandas 也有助于解决 R 中的数据处理问题。不过,在 R 中使用data.table数据类型来处理巨大的 DataFrame 是最佳的解决方案。
reticulate包帮助在 R 中访问和使用 Python 包。例如,你可以在 R 中运行以下 Python 代码片段:
library(reticulate)
# Installing a python package from R
py_install("pandas")
# Importing pandas
pd <- import("pandas", convert = FALSE)
# Some basic pandas operations in R
pd_df <- pd$read_csv("train.csv")
pd_head <- pd_df$head()
pd_dtypes <- pd_df$dtypes
也可以在其他包(如 NumPy)中执行相同的操作:
numpy <- import("numpy")
y <- array(1:4, c(2, 2))
x <- numpy$array(y)
如果你已经在 Python 中编写了具体的 pandas 函数,你可以通过 reticulate 包在 R 中使用它。
考虑以下 Python 代码片段:
import pandas
def get_data_head(file):
data = pandas.read_csv(file)
data_head = data.head()
return(data_head)
现在,前面的脚本已保存为titanic.py。这个脚本可以在 R 中使用,如下所示:
source_python("titanic.py")
titanic_in_r <- get_data_head("titanic.csv")
可以使用repl_python()从 R 创建一个交互式 Python 会话。
例如,你可以写如下代码:
library(reticulate)
repl_python()
import pandas as pd
[i*i for i in range(10)]
它会将结果直接返回到 R shell 中,就像是在 Python IDE 中一样。
在 Python 会话中创建的 Python 对象(列表、字典、DataFrame 和数组)可以通过 R 访问。假设df是一个 Python DataFrame,需要通过 R 来获取其摘要。可以按如下方式进行操作:
summary(py$df)
使用 pandas 与 Azure ML Studio
Azure ML Studio 通过拖放界面提供预测分析解决方案。它具有添加 Python 脚本的能力,脚本可以读取数据集、执行数据操作,然后交付输出数据集。pandas 在 Azure ML Studio 的数据处理模块中可能发挥关键作用:

Azure ML Studio – 流程图
从流程图中可以看出,数据被传入执行 Python 脚本模块。该模块可以在三个输入端口中的两个接收数据集,并在两个输出端口中的一个输出 DataFrame。
下图展示了执行 Python 脚本模块。此模块只接受 DataFrame 作为输入。它允许在输出端口生成单个 DataFrame 之前进行进一步的数据处理步骤。此处 pandas 及其众多强大功能发挥了作用:

Azure ML Studio 的 Python 执行模块
pandas 与 Julia
Julia 拥有一个 DataFrame 包,用于处理 DataFrame 的操作。基准测试结果表明,在速度和计算效率方面,pandas 显然是赢家。与 R 类似,Julia 允许我们在脚本中集成 pandas。
安装完成后,pandas 可以直接加载到 Julia 环境中,如下所示:
Pkg.add("Pandas")
using Pandas
# Creating a dataframe object
df = DataFrame(Dict(:score=>[67, 89, 32], :name=>["A", "B", "C"]))
# Any Pandas function or method could be applied on the created dataframe.
head(df)
describe(df)
pandas 与 H2O
H2O 是 H2O.ai 的超强大数据分析产品,封装了多个独立模块,用于处理数据科学模型的各个方面,包括数据操作和模型训练。
H2O 处理数据作为 H2O 框架,并且这些数据完全位于指定的 H2O 集群内。因此,数据不像 pandas DataFrame 那样存储在内存中。
H2O 有一个as_data_frame()方法,允许将 H2O 框架转换为 pandas DataFrame。完成转换后,可以对转换后的 DataFrame 执行所有 pandas 操作。
pandas 的命令行技巧
命令行是 pandas 用户的重要工具。命令行可以作为高效且快速的补充,但由于其操作繁琐,很多数据操作,如将一个大文件拆分成多个部分、清理数据文件中的不支持字符等,可以在命令行中完成,再将数据传递给 pandas 处理。
pandas 的 head 函数非常有用,可以快速评估数据。一个命令行函数使得 head 更具实用性:
# Get the first 10 rows
$ head myData.csv
# Get the first 5 rows
$ head -n 5 myData.csv
# Get 100 bytes of data
$ head -c 100 myData.csv
translate(tr)函数具有替换字符的能力。以下命令将文本文件中的所有大写字符转换为小写字符:
$ cat upper.txt | tr "[:upper:]" "[:lower:]" >lower.txt
阅读巨大的数据文件既繁琐又有时不可行。在这种情况下,需要将大型文件系统地拆分为多个小文件。命令行中的 split 函数正是做这件事。它根据每个文件中可以包含的行数将一个文件拆分成多个文件:
$ split -l 100 huge_file.csv small_filename_
使用 split -b 按指定的字节大小进行拆分,而不是按行数。
sort 是另一个有用的类似 pandas 的命令行函数。它可以按字母顺序、数字值或按任何列的逆序进行排序。可以在命令行函数中指定首选的排序顺序和列键。让我们来看一下以下示例:
# Sort the 5th column alphabetically
$ sort -t, -k5 orderfile.csv
# Sort the 3rd column in reverse numerical order
$ sort -t, -k3nr orederfile.csv
-t 表示文件是以逗号分隔的。
在应用这些方法之前,当前工作目录应更改为存放相关数据文件的目录。
pandas 的选项和设置
pandas 允许用户修改一些显示和格式化选项。
get_option() 和 set_option() 命令让用户查看当前设置并进行更改:
pd.get_option("display.max_rows")
Output: 60
pd.set_option("display.max_rows", 120)
pd.get_option("display.max_rows")
Output: 120
pd.reset_option("display.max_rows")
pd.get_option("display.max_rows")
Output: 60
上述选项设置和重置了打印 DataFrame 时显示的行数。以下是其他一些有用的显示选项:
-
max_columns:设置要显示的列数。 -
chop_threshold:小于此限制的浮动值将显示为零。 -
colheader_justify:设置列头的对齐方式。 -
date_dayfirst:将此设置为'True'时,显示日期时间值时优先显示日期。 -
date_yearfirst:将此设置为 True 时,显示日期时间值时优先显示年份。 -
precision:设置显示浮动值的小数精度。
以下是一个数字格式化选项的示例,用于设置精度并决定是否使用前缀:

Pandas 中的数字格式化
我们将在后续章节中详细讨论这一内容。
总结
在我们深入探讨 pandas 的强大功能之前,正确安装 Python 和 pandas,选择合适的 IDE,并设置正确的选项是至关重要的。在本章中,我们讨论了这些内容及更多。以下是本章的关键要点总结:
-
Python 3.x 已经可用,但许多用户仍然偏爱使用版本 2.7,因为它更稳定且对科学计算更友好。
-
版本 2.7 的支持和 bug 修复现已停止。
-
从一个版本翻译代码到另一个版本非常简单。还可以使用
virtualenv包同时使用两个版本,该包预装在 Anaconda 中。 -
Anaconda 是一个流行的 Python 发行版,内置 700 多个库/包,并提供多个流行的 IDE,如 Jupyter 和 Spyder。
-
Python 代码可以在其他工具中调用和使用,如 R、Azure ML Studio、H20.ai 和 Julia。
-
一些日常数据操作,如将大文件拆分为更小的块、
读取几行数据等,也可以在命令行/终端中执行。 -
pandas 的默认设置选项可以通过
get_option()和set_option()命令查看和更改。一些可以更改的选项包括显示的最大行数和列数、浮动变量的小数位数等。
在下一章中,我们将稍微扩展一下范围,超出 pandas,探索诸如 NumPy 等工具,这些工具丰富了 pandas 在 Python 生态系统中的功能。这将是一个详尽的 NumPy 教程,并结合实际案例研究。
进一步阅读
第二部分:pandas 中的数据结构和输入输出
pandas 的关键特性在于其能够为你提供灵活性和易用性,以处理不同的数据结构,例如 1D、2D,甚至 3D 数组,以及各种文件格式——从文本文件到 CSV 文件。在本节中,我们将学习如何处理这些不同的文件类型,以及使用 pandas 对数据执行的各种操作。
本节包括以下章节:
-
第三章,使用 NumPy 和 pandas 中的数据结构
-
第四章,pandas 中的不同数据格式输入输出
第三章:使用 NumPy 和 pandas 中的数据结构
本章是本书中最重要的章节之一。现在我们将开始深入探讨 pandas 的细节。我们从了解 NumPy 的ndarrays开始,这是一个不在 pandas 中的数据结构,而是 NumPy 的一部分。了解 NumPy ndarrays是非常有用的,因为它们是构建 pandas DataFrame 的基础。NumPy 数组的一个关键优势是,它们可以执行所谓的矢量化操作,这些操作是需要遍历/循环 Python 数组的操作,而且速度要快得多。
在本章中,我将通过多个使用 Jupyter 的示例来呈现这些内容。
本章将涵盖的主题包括对numpy.ndarray数据结构的介绍,pandas.Series一维(1D)pandas 数据结构,pandas.DataFrame二维(2D)pandas 表格数据结构,以及pandas.Panel三维(3D)pandas 数据结构。
本章将涵盖以下主题:
-
NumPy
ndarrays -
使用 NumPy 实现神经网络
-
多维数组的实际应用
-
pandas 中的数据结构
NumPy 的 ndarrays
数组在数据分析中是至关重要的对象。数组允许对跨行和列堆叠的元素进行结构化处理。数组的元素必须遵守一个规则:它们应该具有相同的数据类型。例如,五个病人的医疗记录可以按如下方式呈现为一个数组:
| 血糖水平 | 心率 | 胆固醇水平 | |
|---|---|---|---|
| 彼得·帕克 | 100 | 65 | 160 |
| 布鲁斯·韦恩 | 150 | 82 | 200 |
| 托尼·斯塔克 | 90 | 55 | 80 |
| 巴里·艾伦 | 130 | 73 | 220 |
| 史蒂夫·罗杰斯 | 190 | 80 | 150 |
可以看到,所有 15 个元素的数据类型都是int。数组也可以由字符串、浮点数或复数构成。数组可以由列表构造——列表是 Python 中广泛使用且多功能的数据结构:
array_list = [[100, 65, 160],
[150, 82, 200],
[90, 55, 80],
[130, 73, 220],
[190, 80, 150]]
可以通过以下代码访问数组或矩阵中第i行和第j列的元素(例如,在第一个示例中的第一行第二列)。请注意,Python 中的索引从 0 开始:
In [2]: array_list[1][2]
Out[2]: 200
In [3]: array_list[3][0]
Out[3]: 130
Python 有一个内建的array模块来创建数组。然而,这个数组模块更像是一个经过美化的列表,要求所有元素具有相同的数据类型。可以通过提供两个参数来使用array模块创建数组——数据类型的类型代码,以及列表、字符串或任何可迭代对象中的元素。让我们创建一个浮点数组。这里,d是双精度浮点数的类型代码:
import array as arr
arr_x = arr.array("d", [98.6, 22.35, 72.1])
使用array模块无法创建具有行和列的二维实体。这可以通过嵌套列表来实现。该模块没有定义与矩阵或数组相关的特殊函数,如矩阵乘法、行列式和特征值。
NumPy 是创建和操作数组类型对象的首选包。NumPy 允许创建多维数组。多维数组提供了一个系统化和高效的数据存储框架。在这些多维数组上,可以快速进行复杂的计算,这些计算是 NumPy 包中的内置矢量化操作,无需使用循环。考虑之前的示例,我们创建了一个二维数组来存储五个患者的医疗记录。在这种情况下,患者的姓名和临床指标是两个维度。现在,如果记录了相同患者在三年(2016 到 2018 年)内的临床参数,那么所有这些信息可以方便地表示为一个三维数组。记录的年份将作为第三维度。结果数组的维度为 3 x 5 x 3,完全由整数组成:
| 2016 | 2017 | 2018 |
|---|---|---|
| 100 | 65 | 160 |
| 150 | 82 | 200 |
| 90 | 55 | 80 |
| 130 | 73 | 220 |
| 190 | 80 | 150 |
在 NumPy 中,这些多维数组被称为 ndarrays (n 维数组)。所有 NumPy 数组对象都是 numpy.ndarray 类型。
让我们将前面的数据视为一个 ndarray:
In [4]: ndarray_1
Out[4]:
array([[[100, 65, 160],
[150, 82, 200],
[ 90, 55, 80],
[130, 73, 220],
[190, 80, 150]],
[[ 95, 68, 140],
[145, 80, 222],
[ 90, 62, 100],
[150, 92, 200],
[140, 60, 90]],
[[110, 72, 160],
[160, 95, 185],
[100, 80, 110],
[140, 92, 120],
[100, 55, 100]]])
ndarray 的属性,如数据类型、形状、维度数量和大小,可以通过数组的不同属性进行访问。在以下代码中,探讨了 ndarray ndarray_1 的一些属性:
# Data type of the array
In [5]: ndarray_1.dtype
Out[5]: dtype('int32')
# Shape of the array
In [6]: ndarray_1.shape
Out[6]: (3, 5, 3)
# Number of dimensions in the array
In [7]: ndarray_1.ndim
Out[7]: 3
# Size of the array (number of elements in the array)
In [8]: ndarray_1.size
Out[8]: 45
NumPy 的 ndarray 使用了步长索引方案来进行内部内存布局。单独的内存段只能容纳一维结构。因此,像步长索引这样的特定内存分配方案是必要的,以便方便地对 ndarray 进行索引和切片。步长表示从当前元素跳到下一个元素所需跳过的字节数。每个步长所占的字节数由数组的数据类型决定。让我们通过前面探讨的数组来理解步长。每个元素占用的字节数可以通过以下代码来确定:
In [9]: ndarray_1.itemsize
Out[9]: 4
In [10]: ndarray_1.nbytes
Out[10]: 180
可以看到,每个元素占用 4 个字节,整个数组占用 180 个字节。数组的步长表示如下:
In [11]: ndarray_1.strides
Out[11]: (60, 12, 4)
数组的形状由元组(3, 5, 3)给出。元组中的值分别代表有数据的年份数、患者数和临床参数数。对于每个年份或第一维度,存在 15 条记录,因此在数组中从一个年份跳到另一个年份时,需要跳过 60 个字节。同样地,每个不同的患者在给定的年份中有 3 条记录,要跳过 12 个字节才能到达下一个患者。
NumPy 数组创建
可以通过调用各种 NumPy 方法以多种方式创建 NumPy 数组。这些数组可以使用列表或其他数据结构中的数据创建,或者通过指定数值范围来获得均匀间隔的值,或生成随机样本。
创建数组的最简单方法是通过 array 函数。此函数接受任何序列对象,如列表或元组,并将其转换为数组。以下代码片段演示了如何通过 array 函数创建一个一维数组:
In [12]: array1d = np.array([1, 2, 3, 4])
In [13]: array1d
Out [13]: array([1, 2, 3, 4])
同样,可以通过将列表的列表传递给 array 函数来创建一个多维数组:
In [14]: array2d = np.array([[0, 1, 2],[2, 3, 4]])
In [15]: array2d
Out [15]:
array([[0, 1, 2],
[2, 3, 4]])
与列表不同,元组、元组的列表或元组的元组也能达到相同的结果。
一维零一数组
对数组进行的几种操作需要创建包含零和一的数组或矩阵。NumPy 中的一些特殊函数可以方便地创建此类数组。通常,这些函数接受作为输入参数的结果数组形状,形式为元组:
# Creating an array of ones with shape (2, 3, 4)
In [27]: np.ones((2, 3, 4))
Out [27]:
array([[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]],
[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]]])
# Creating an array of zeros with shape (2, 1, 3)
In [28]: np.zeros((2, 1, 3))
Out [28]:
array([[[0., 0., 0.]],
[[0., 0., 0.]]])
该单位矩阵函数返回一个二维的 n x n 方阵,其中 n 是作为输入参数传入的矩阵的阶数:
In [29]: np.identity(3)
Out [29]:
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
eye 函数也可以用来创建单位矩阵。它与单位矩阵在两个方面有所不同:
-
eye 函数返回一个二维矩形矩阵,并接受行数和列数(可选参数)作为输入。如果未指定列数,将仅使用传入的行数返回一个方阵。
-
对角线可以偏移到上三角或下三角的任意位置。
请看以下代码:
# Creating an identity matrix of order 3 with the eye function
In [39]: np.eye(N = 3)
Out [39]:
array([[1., 0., 0.],
[0., 1., 0.],[0., 0., 1.]])
# Creating a rectangular equivalent of identity matrix with 2 rows and 3 columns
In [40]: np.eye(N = 2, M = 3)
Out [40]:
array([[1., 0., 0.],
[0., 1., 0.]])
# Offsetting the diagonal of ones by one position in the upper triangle
In [41]: np.eye(N = 4, M = 3, k = 1)
Out [41]:
array([[0., 1., 0.],
[0., 0., 1.],
[0., 0., 0.],
[0., 0., 0.]])
# Offsetting the diagonal of ones by two positions in the lower triangle
In [42]: np.eye(N = 4, M = 3, k = -2)
Out [42]:
array([[0., 0., 0.],
[0., 0., 0.],
[1., 0., 0.],
[0., 1., 0.]])
默认情况下,k 在 eye 函数中保持值 0。
基于数值范围的数组
NumPy 的 arange 函数在功能上类似于 Python 的 range 函数。根据起始值、结束值和步长值来增量或减量后续值,arange 函数生成一组数字。与 range 函数类似,这里的起始值和步长值是可选的。但不同于 range 函数生成列表,arange 会生成一个数组:
# Creating an array with continuous values from 0 to 5
In [44]: np.arange(6)
Out [44]: array([0, 1, 2, 3, 4, 5])
# Creating an array with numbers from 2 to 12 spaced out at intervals of 3
In [45]: np.arange(2, 13, 3)
Out [45]: array([ 2, 5, 8, 11])
linspace 函数生成在给定起点和终点之间线性分布的样本数组。与指定增量/减量的 arrange 函数不同,linspace 函数接受生成样本的数量作为可选参数。默认情况下,会为给定的起点和终点生成 50 个样本:
# Creating a linearly spaced array of 20 samples between 5 and 10
In [47]: np.linspace(start = 5, stop = 10, num = 20)
Out [47]:
array([ 5\. , 5.26315789, 5.52631579, 5.78947368, 6.05263158,
6.31578947, 6.57894737, 6.84210526, 7.10526316, 7.36842105,
7.63157895, 7.89473684, 8.15789474, 8.42105263, 8.68421053,
8.94736842, 9.21052632, 9.47368421, 9.73684211, 10\. ])
同样,logspace 和 geomspace 函数可以创建遵循对数和几何序列的数字数组。
arange 函数和 linspace 函数本身不允许指定任何形状,并生成包含给定数字序列的一维数组。我们完全可以使用一些形状操作方法,将这些数组塑造成所需的形状。这些方法将在本章最后部分讨论。
随机数组与空数组
NumPy 包的 random 模块内置了一整套用于随机抽样的函数,可以执行从创建简单的随机数数组到从分布函数中抽取随机样本的操作。
函数 random.rand 生成从 0 到 1 的随机值(均匀分布),以创建给定形状的数组:
# Creating a random array with 2 rows and 4 columns, from a uniform distribution
In [49]: np.random.rand(2, 4)
Out [49]:
array([[0.06573958, 0.32399347, 0.60926818, 0.99319404],
[0.46371691, 0.49197909, 0.93103333, 0.06937098]])
函数 random.randn 从标准正态分布中抽样数值以构建给定形状的数组。如果未指定形状参数,则返回单个值作为输出:
# Creating a 2X4 array from a standard normal distribution
In [50]: np.random.randn(2, 4)
Out [50]:
array([[ 1.29319502, 0.55161748, 0.4660141 , -0.72012401],
[-0.64549002, 0.01922198, 0.04187487, 1.35950566]])
# Creating a 2X4 array from a normal distribution with mean 10 and standard deviation 5
In [51]: 5 * np.random.randn(2, 4) + 10
Out [51]:
array([[ 6.08538069, 12.10958845, 15.27372945, 15.9252008 ],
[13.34173712, 18.49388151, 10.19195856, 11.63874627]])
函数 random.randint 生成介于指定下限和上限之间的整数数组,具有给定的形状。上限不包括在内。如果未提及上限,则认为上限比定义的下限大 1:
# Creating an array of shape (2, 3) with random integers chosen from the interval [2, 5)
In [52]: np.random.randint(2, 5, (2, 3))
Out [52]:
array([[2, 4, 3],
[3, 4, 4]])
函数 empty 返回具有给定形状的带有任意值的数组。此数组不需要初始化,并且在需要填充所有数组值时,执行速度比诸如零和一的函数更快。在使用此函数时需要谨慎,并且仅在确定数组中所有值都将被填充时才使用:
# Creating an uninitialized empty array of 4X3 dimensions
In [58]: np.empty([4,3])
Out [58]:
array([[0., 0., 0.],
[0., 0., 0.],
[1., 0., 0.],
[0., 1., 0.]])
基于现有数组的数组
一些 NumPy 数组创建例程非常有用,可执行诸如构造对角矩阵(diag)、上三角矩阵(triu)和下三角矩阵(tril)之类的矩阵操作。
函数 diag 仅适用于 1D 和 2D 数组。如果输入数组为 2D,则输出为输入数组的对角元素的 1D 数组。如果输入为 1D 数组,则输出为具有输入数组沿其对角线的矩阵。此处,参数 k 可以帮助偏移从主对角线的位置,并且可以是正数或负数:
# The 2D input matrix for diag function
In [68]: arr_a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
In [69]: arr_a
Out [69]:
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# Getting the diagonal of the array
In [70]: np.diag(arr_a)
Out [70]: array([1, 5, 9])
# Constructing the diagonal matrix from a 1D array
# diag returns a 1D array of diagonals for a 2D input matrix. This 1D array of diagonals can be used here.
In [71]: np.diag(np.diag(arr_a))
Out [71]:
array([[1, 0, 0],
[0, 5, 0],
[0, 0, 9]])
# Creating the diagonal matrix with diagonals other than main diagonal
In [72]: np.diag(np.diag(arr_a, k = 1))
Out [72]:
array([[2, 0],
[0, 6]])
函数 triu 和 tril 具有类似的参数 k,可以帮助偏移对角线。这些函数适用于任何 ndarray。
给定 n 维数组,可以通过沿每个轴重复此数组多次来创建新数组。这可以通过 tile 函数完成。此函数接受两个输入参数——输入数组和重复次数:
# Repeating a 1D array 2 times
In [76]: np.tile(np.array([1, 2, 3]), 2)
Out [76]: array([1, 2, 3, 1, 2, 3])
# Repeating a 2D array 4 times
In [77]: np.tile(np.array([[1, 2, 3], [4, 5, 6]]), 4)
Out [77]:
array([[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3],
[4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6]])
# Repeating a 2D array 4 times along axis 0 and 1 time along axis 1
In [78]: np.tile(np.array([[1, 2, 3], [4, 5, 6]]), (4,1))
Out [78]:
array([[1, 2, 3],
[4, 5, 6],
[1, 2, 3],
[4, 5, 6],
[1, 2, 3],
[4, 5, 6],
[1, 2, 3],
[4, 5, 6]])
NumPy 数据类型
之前描述的所有数组创建函数(除了基于现有数组的函数——diag、triu、tril 和 tile)都有一个参数 dtype,用于定义数组的数据类型。
让我们创建一个不预定义数据类型的数组,然后检查其数据类型:
In [80]: np.array([-2, -1, 0, 1, 2]).dtype
Out [80]: dtype('int32')
现在,让我们定义同一个数组,并将其数据类型设置为 float:
In [81]: np.array([-2, -1, 0, 1, 2], dtype = "float")
Out [81]: array([-2., -1., 0., 1., 2.])
可见数组的元素全部被转换为浮点数。还可以将此数组转换为字符串:
In [83]: np.array([-2, -1, 0, 1, 2], dtype = "str")
Out[83]: array(['-2', '-1', '0', '1', '2'], dtype='<U2'
在这个例子中,元素被转换为字符串。输出还显示了数据类型为<U2。这表示数组的元素是 Unicode 字符串,并且该数组的最大字符串长度为 2。这个长度阈值是根据数组中最长字符串的长度来决定的。让我们通过另一个例子来理解这一点:
In [87]: np.array(["a", "bb", "ccc", "dddd", "eeeee"])
Out[87]: array(['a', 'bb', 'ccc', 'dddd', 'eeeee'], dtype='<U5')
这种类型转换在包含字符串的数组中较为常见,因为数组需要为其分配最佳的内存空间。一个字符占用四个字节。根据最大字符串长度,每个元素将被分配一个大小为最大字符串长度四倍的内存块。
NumPy 数组还支持布尔型和复数型等数据类型:
# Boolean array
In [89]: np.array([True, False, True, True]).dtype
Out[89]: dtype('bool')
In [90]: np.array([0, 1, 1, 0, 0], dtype = "bool")
Out[90]: array([False, True, True, False, False])
In [91]: np.array([0, 1, 2, 3, -4], dtype = "bool")
Out[91]: array([False, True, True, True, True])
# Complex array
In [92]: np.array([[1 + 1j, 2 + 2j], [3 + 3j, 4 + 4j]])
Out[92]:
array([[1.+1.j, 2.+2.j],
[3.+3.j, 4.+4.j]])
In [93]: np.array([[1 + 1j, 2 + 2j], [3 + 3j, 4 + 4j]]).dtype
Out[93]: dtype('complex128')
ndarray的数据类型可以像在其他语言(如 Java 或 C/C++)中进行类型转换一样进行更改。ndarray.astype方法有助于进行类型转换:
# Int to float conversion
In [94]: int_array = np.array([0, 1, 2, 3])
In [95]: int_array.astype("float")
Out[95]: array([0., 1., 2., 3.])
# Float to int conversion
In [97]: float_array = np.array([1.56, 2.95, 3.12, 4.65])
In [98]: float_array.astype("int")
Out[98]: array([1, 2, 3, 4])
关于类型转换的更多信息,请参考官方文档:docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.astype.html。
NumPy 索引与切片
NumPy 中的数组索引从0开始,和 Python、Java、C++等语言类似,而不同于 Fortran、Matlab 和 Octave,它们的索引从1开始。数组可以像索引其他 Python 序列一样按标准方式进行索引:
# print entire array, element 0, element 1, last element.
In [36]: ar = np.arange(5); print ar; ar[0], ar[1], ar[-1]
[0 1 2 3 4]
Out[36]: (0, 1, 4)
# 2nd, last and 1st elements
In [65]: ar=np.arange(5); ar[1], ar[-1], ar[0]
Out[65]: (1, 4, 0)
数组可以使用::-1惯用法进行反转,如下所示:
In [24]: ar=np.arange(5); ar[::-1]
Out[24]: array([4, 3, 2, 1, 0])
多维数组使用整数元组进行索引:
In [71]: ar = np.array([[2,3,4],[9,8,7],[11,12,13]]); ar
Out[71]: array([[ 2, 3, 4],
[ 9, 8, 7],
[11, 12, 13]])
In [72]: ar[1,1]
Out[72]: 8
这里,我们将row1和column1的元素设置为5:
In [75]: ar[1,1]=5; ar
Out[75]: array([[ 2, 3, 4],
[ 9, 5, 7],
[11, 12, 13]])
获取第 2 行:
In [76]: ar[2]
Out[76]: array([11, 12, 13])
In [77]: ar[2,:]
Out[77]: array([11, 12, 13])
获取第 1 列:
In [78]: ar[:,1]
Out[78]: array([ 3, 5, 12])
如果指定的索引超出了数组的范围,将会引发IndexError错误:
In [6]: ar = np.array([0,1,2])
In [7]: ar[5]
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-7-8ef7e0800b7a> in <module>()
----> 1 ar[5]
IndexError: index 5 is out of bounds for axis 0 with size 3
因此,对于二维数组,第一维表示行,第二维表示列。冒号(:)表示选择该维度中的所有元素。
数组切片
数组可以使用语法ar[startIndex: endIndex: stepValue]进行切片:
In [82]: ar=2*np.arange(6); ar
Out[82]: array([ 0, 2, 4, 6, 8, 10])
In [85]: ar[1:5:2]
Out[85]: array([2, 6])
请注意,如果我们希望包含endIndex值,需要超出它,如下所示:
In [86]: ar[1:6:2]
Out[86]: array([ 2, 6, 10])
使用ar[:n]获取前nelements:
In [91]: ar[:4]
Out[91]: array([0, 2, 4, 6])
隐含的假设是startIndex=0, step=1。
从元素 4 开始并选择直到末尾的所有元素:
In [92]: ar[4:]
Out[92]: array([ 8, 10])
使用stepValue=3切片数组:
In [94]: ar[::3]
Out[94]: array([0, 6])
为了说明 NumPy 中索引的范围,参见以下图示,该图示来自 2013 年在 SciPy 会议上进行的 NumPy 讲座,详细信息可参见:scipy-lectures.github.io/_images/numpy_indexing.png:

NumPy 索引的图示
现在让我们来分析前面图示中表达式的含义:
-
表达式
a[0,3:5]表示从第 0 行开始,选择第 3 到第 5 列(不包括第 5 列)。 -
在表达式
a[4:,4:]中,第一个 4 表示从第 4 行开始,显示所有列,即数组[[40, 41, 42, 43, 44, 45] [50, 51, 52, 53, 54, 55]]。第二个 4 表示在第 4 列开始截断,从而产生数组[[44, 45], [54, 55]]。 -
表达式
a[:,2]会返回所有行中的第 2 列。 -
现在,在最后一个表达式
a[2::2,::2]中,2::2表示从第 2 行开始,步长为 2。这会给我们生成数组[[20, 21, 22, 23, 24, 25], [40, 41, 42, 43, 44, 45]]。进一步地,::2指定我们以步长为 2 的方式提取列,从而得到最终结果数组[[20, 22, 24], [40, 42, 44]]。
赋值和切片可以结合使用,如下代码片段所示:
In [96]: ar
Out[96]: array([ 0, 2, 4, 6, 8, 10])
In [100]: ar[:3]=1; ar
Out[100]: array([ 1, 1, 1, 6, 8, 10])
In [110]: ar[2:]=np.ones(4);ar
Out[110]: array([1, 1, 1, 1, 1, 1])
数组掩码
NumPy 数组可以用作更大原始数组的过滤器。将数组用作过滤器的过程称为数组掩码。例如,以下代码片段演示了这一过程:
In [146]: np.random.seed(10)
ar=np.random.random_integers(0,25,10); ar
Out[146]: array([ 9, 4, 15, 0, 17, 25, 16, 17, 8, 9])
In [147]: evenMask=(ar % 2==0); evenMask
Out[147]: array([False, True, False, True, False, False, True, False, True, False], dtype=bool)
In [148]: evenNums=ar[evenMask]; evenNums
Out[148]: array([ 4, 0, 16, 8])
在以下示例中,我们随机生成一个包含 10 个整数(范围为 0 到 25)的数组。然后,我们创建一个布尔掩码数组,用于仅过滤出偶数。这个掩码功能非常有用,例如,如果我们希望通过用默认值替换来消除缺失值。在这里,缺失值''被默认的国家'USA'替换。请注意,''也是一个空字符串:
In [149]: ar=np.array(['Hungary','Nigeria',
'Guatemala','','Poland',
'','Japan']); ar
Out[149]: array(['Hungary', 'Nigeria', 'Guatemala',
'', 'Poland', '', 'Japan'],
dtype='|S9')
In [150]: ar[ar=='']='USA'; ar
Out[150]: array(['Hungary', 'Nigeria', 'Guatemala',
'USA', 'Poland', 'USA', 'Japan'], dtype='|S9')
整型数组也可以用作索引来访问数组,从而生成另一个数组。注意,这会产生多个值,因此输出必须是ndarray类型的数组。以下代码片段中进行了演示:
In [173]: ar=11*np.arange(0,10); ar
Out[173]: array([ 0, 11, 22, 33, 44, 55, 66, 77, 88, 99])
In [174]: ar[[1,3,4,2,7]]
Out[174]: array([11, 33, 44, 22, 77])
在前面的代码中,选择对象是一个列表,选中了索引为 1、3、4、2 和 7 的元素。现在,假设我们将其更改为以下内容:
In [175]: ar[1,3,4,2,7]
由于数组是 1D 的,而我们指定了过多的索引来访问它,因此会引发IndexError错误:
IndexError Traceback (most recent call last)
<ipython-input-175-adbcbe3b3cdc> in <module>()
----> 1 ar[1,3,4,2,7]
IndexError: too many indices
这种赋值操作也可以通过数组索引实现,如下所示:
In [176]: ar[[1,3]]=50; ar
Out[176]: array([ 0, 50, 22, 50, 44, 55, 66, 77, 88, 99])
当通过使用数组索引列表从另一个数组创建一个新数组时,新数组将具有相同的形状。
复杂索引
这里,我们通过使用复杂的索引来将较小数组的值赋值给较大的数组:
In [188]: ar=np.arange(15); ar
Out[188]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
In [193]: ar2=np.arange(0,-10,-1)[::-1]; ar2
Out[193]: array([-9, -8, -7, -6, -5, -4, -3, -2, -1, 0])
切片出ar的前 10 个元素,并用ar2中的元素替换它们,如下所示:
In [194]: ar[:10]=ar2; ar
Out[194]: array([-9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 10, 11, 12, 13, 14])
副本与视图
NumPy 数组的视图只是以特定方式呈现其包含的数据。创建视图并不会生成数组的新副本,而是可能以特定的顺序排列其中的数据,或只显示某些数据行。因此,如果在底层数组的数据上替换了数据,任何通过索引访问数据时,视图都会反映出这些变化。
在切片时,初始数组不会被复制到内存中,因此效率更高。可以使用np.may_share_memory方法查看两个数组是否共享同一内存块。然而,应谨慎使用该方法,因为它可能会产生假阳性结果。修改视图会修改原始数组:
In [118]:ar1=np.arange(12); ar1
Out[118]:array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
In [119]:ar2=ar1[::2]; ar2
Out[119]: array([ 0, 2, 4, 6, 8, 10])
In [120]: ar2[1]=-1; ar1
Out[120]: array([ 0, 1, -1, 3, 4, 5, 6, 7, 8, 9, 10, 11])
要强制 NumPy 复制一个数组,我们使用 np.copy 函数。如以下数组所示,当修改复制的数组时,原始数组保持不变:
In [124]: ar=np.arange(8);ar
Out[124]: array([0, 1, 2, 3, 4, 5, 6, 7])
In [126]: arc=ar[:3].copy(); arc
Out[126]: array([0, 1, 2])
In [127]: arc[0]=-1; arc
Out[127]: array([-1, 1, 2])
In [128]: ar
Out[128]: array([0, 1, 2, 3, 4, 5, 6, 7])
操作
NumPy 数组的许多方法需要在数组上运行数学运算符,如加法、减法、乘法、除法等。以下部分将解释这些运算符如何应用于数组。
基本运算符
NumPy 在性能上非常高效,因为它基于向量化操作工作,避免了循环的需要,使得处理速度快了好几倍。所有基本的算术运算(如 +、-、*、/)都以元素为单位进行,并且是向量化的:
# Arithmetic operation on arrays with scalars
In [71]: array_1 = np.array([[1, 2, 3], [4, 5, 6]])
In [72]: array_1
Out[72]:
array([[1, 2, 3],
[4, 5, 6]])
In [73]: array_1 + 5
Out[73]:
array([[ 6, 7, 8],
[ 9, 10, 11]])
In [74]: array_1 * 5
Out[74]:
array([[ 5, 10, 15],
[20, 25, 30]])
In [75]: array_1 ** 2
Out[75]:
array([[ 1, 4, 9],
[16, 25, 36]], dtype=int32)
涉及两个数组的操作,如加法或乘法,也是以向量化方式进行的:
# Element-wise addition of two arrays
In [76]: array_1 + array_1
Out[76]:
array([[ 2, 4, 6],
[ 8, 10, 12]])
# Element-wise multiplication of two arrays
In [77]: array_1 * array_1
Out[77]:
array([[ 1, 4, 9],
[16, 25, 36]])
# Matrix multiplication of an array and its transpose
In [78]: array_1 @ array_1.T
Out[78]:
array([[14, 32],
[32, 77]])
Python 的 timeit 函数可以让我们了解向量化操作与遍历项进行循环时的效率差异:
# Computing the cube of each element in an array, for an array with 1000 elements
In [79]: %timeit np.arange(1000) ** 3
5.05 µs ± 195 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
# Computing the cube of each number from 0 to 1000, using a for loop
In [80]: array_list = range(1000)
...: %timeit [array_list[i]**3 for i in array_list]
533 µs ± 8.06 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
这表明,numpy 操作比 for 循环快大约 100 倍。
数学运算符
NumPy 的数学运算符主要支持三角函数运算、算术运算,以及指数和对数运算。
这类运算符,如 prod、sum 等,会在数组内执行计算并实现矩阵的缩减。例如,sum 函数计算沿给定轴的和。输出将是该轴上元素的和。这些函数可以作为 numpy.function 或 ndarray.method 调用:
# Sum of all elements in an array
In [62]: np.array([[1, 2, 3], [4, 5, 6]]).sum()
Out[62]: 21
# Column sum of elements
In [63]: np.array([[1, 2, 3], [4, 5, 6]]).sum(axis = 0)
Out[63]: array([5, 7, 9])
# Cumulative sum of elements along axis 0
In [64]: np.array([[1, 2, 3], [4, 5, 6]]).cumsum(axis = 0)
Out[64]:
array([[1, 2, 3],
[5, 7, 9]], dtype=int32)
# Cumulative sum of all elements in the array
In [65]: np.array([[1, 2, 3], [4, 5, 6]]).cumsum()
Out[65]: array([ 1, 3, 6, 10, 15, 21], dtype=int32)
统计运算符
可以使用现有的统计运算符计算 NumPy 数组的各种统计操作,例如计算均值、中位数、方差和标准差。可以按如下代码计算整个数组的聚合值,例如均值、中位数、方差和标准差:
In [16]: array_x = np.array([[0, 1, 2], [3, 4, 5]])
In [17]: np.mean(array_x)
Out[17]: 2.5
In [18]: np.median(array_x)
Out[18]: 2.5
In [19]: np.var(array_x)
Out[19]: 2.9166666666666665
In [20]: np.std(array_x)
Out[20]: 1.707825127659933
默认情况下,这些统计参数是通过展平数组来计算的。要沿某个轴计算统计参数,可以在调用这些函数时定义 axis 参数。我们以 mean 函数为例来查看这种行为:
In [27]: np.mean(array_x, axis = 0)
Out[27]: array([1.5, 2.5, 3.5])
In [28]: np.mean(array_x, axis = 1)
Out[28]: array([1., 4.])
这些函数有专门的实现来处理包含缺失值或 NA 的数组。这些函数是 nanmean、nanmedian、nanstd、nanvar:
In [30]: nan_array = np.array([[5, 6, np.nan], [19, 3, 2]])
# The regular function returns only nan with a warning
In [31]: np.median(nan_array)
C:\Users \Anaconda3\lib\site-packages\numpy\lib\function_base.py:3250: RuntimeWarning: Invalid value encountered in median
r = func(a, **kwargs)
Out[31]: nan
In [32]: np.nanmedian(nan_array)
Out[32]: 5.0
corrcoeff 和 cov 函数帮助计算给定数组或两个给定数组的 Pearson 相关系数和协方差矩阵:
In [35]: array_corr = np.random.randn(3,4)
In [36]: array_corr
Out[36]:
array([[-2.36657958, -0.43193796, 0.4761051 , -0.11778897],
[ 0.52101041, 1.11562216, 0.61953044, 0.07586606],
[-0.17068701, -0.84382552, 0.86449631, 0.77080463]])
In [37]: np.corrcoef(array_corr)
Out[37]:
array([[ 1\. , -0.00394547, 0.48887013],
[-0.00394547, 1\. , -0.76641267],
[ 0.48887013, -0.76641267, 1\. ]])
In [38]: np.cov(array_corr)
Out[38]:
array([[ 1.51305796, -0.00207053, 0.48931189],
[-0.00207053, 0.18201613, -0.26606154],
[ 0.48931189, -0.26606154, 0.66210821]])
逻辑运算符
逻辑运算符帮助比较数组、检查数组的类型和内容,并进行数组之间的逻辑比较。
all 和 any 函数帮助评估沿指定轴上的所有或任何值是否为 True。根据评估结果,它返回 True 或 False:
In [39]: array_logical = np.random.randn(3, 4)
In [40]: array_logical
Out[40]:
array([[ 0.79560751, 1.11526762, 1.21139114, -0.36566102],
[ 0.561285 , -1.27640005, 0.28338879, 0.13984101],
[-0.304546 , 1.58540957, 0.1415475 , 1.53267898]])
# Check if any value is negative along each dimension in axis 0
In [42]: np.any(array_logical < 0, axis = 0)
Out[42]: array([ True, True, False, True])
# Check if all the values are negative in the array
In [43]: np.all(array_logical < 0)
Out[43]: False
对于前面描述的 all 和 any 方法,axis 是一个可选参数。如果没有提供,则会将数组展平并用于计算。
一些函数用来测试数组中是否存在NAs或无限值。这些功能是数据处理和数据清理中的重要组成部分。这些函数接受一个数组或类数组对象作为输入,并返回布尔值作为输出:
In [44]: np.isfinite(np.array([12, np.inf, 3, np.nan]))
Out[44]: array([ True, False, True, False])
In [45]: np.isnan((np.array([12, np.inf, 3, np.nan])))
Out[45]: array([False, False, False, True])
In [46]: np.isinf((np.array([12, np.inf, 3, np.nan])))
Out[46]: array([False, True, False, False])
如大于、小于和等于等运算符帮助在形状相同的两个数组之间执行逐元素比较:
# Creating two random arrays for comparison
In [50]: array1 = np.random.randn(3,4)
In [51]: array2 = np.random.randn(3, 4)
In [52]: array1
Out[52]:
array([[ 0.80394696, 0.67956857, 0.32560135, 0.64933303],
[-1.78808905, 0.73432929, 0.26363089, -1.47596536],
[ 0.00214663, 1.30853759, -0.11930249, 1.41442395]])
In [54]: array2
Out[54]:
array([[ 0.59876194, -0.33230015, -1.68219462, -1.27662143],
[-0.49655572, 0.43650693, -0.34648415, 0.67175793],
[ 0.1837518 , -0.15162542, 0.04520202, 0.58648728]])
# Checking for the truth of array1 greater than array2
In [55]: np.greater(array1, array2)
Out[55]:
array([[ True, True, True, True],
[False, True, True, False],
[False, True, False, True]])
# Checking for the truth of array1 less than array2
In [56]: np.less(array1, array2)
Out[56]:
array([[False, False, False, False],
[ True, False, False, True],
[ True, False, True, False]])
广播
通过使用广播,我们可以处理形状不完全相同的数组。以下是一个示例:
In [357]: ar=np.ones([3,2]); ar
Out[357]: array([[ 1., 1.],
[ 1., 1.],
[ 1., 1.]])
In [358]: ar2=np.array([2,3]); ar2
Out[358]: array([2, 3])
In [359]: ar+ar2
Out[359]: array([[ 3., 4.],
[ 3., 4.],
[ 3., 4.]])
因此,我们可以看到ar2通过将其添加到ar的每一行,实现了在ar的行上进行广播,得到了上面的结果。以下是另一个示例,展示了广播如何跨维度工作:
In [369]: ar=np.array([[23,24,25]]); ar
Out[369]: array([[23, 24, 25]])
In [368]: ar.T
Out[368]: array([[23],
[24],
[25]])
In [370]: ar.T+ar
Out[370]: array([[46, 47, 48],
[47, 48, 49],
[48, 49, 50]])
这里,行数组和列数组都进行了广播,最终得到了一个 3 × 3 的数组。
数组形状操作
通常,在数据可以用于分析之前,需要进行转换。数组也不例外。NumPy 提供了一些专门的函数集,帮助重新塑造和转换数组。
改变形状
reshape函数帮助修改数组的形状。它接受两个主要输入参数——需要处理的数组和期望的形状(整数或整数元组)。
在本章前面,我们看到np.arange应该依赖外部函数将数据从一维转换:
In [78]: reshape_array = np.arange(0,15)
In [79]: np.reshape(reshape_array, (5, 3))
Out[79]:
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]]
np.reshape函数返回的是数据的视图,这意味着底层数组保持不变。然而,在特殊情况下,形状不能改变而不复制数据。有关详细信息,请参见docs.scipy.org/doc/numpy/reference/generated/numpy.reshape.html的文档。
转置
transpose函数会反转数组的维度:
In [80]: trans_array = np.arange(0,24).reshape(4, 6)
In [82]: trans_array
Out[82]:
array([[ 0, 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10, 11],
[12, 13, 14, 15, 16, 17],
[18, 19, 20, 21, 22, 23]])
In [83]: trans_array.T
Out[83]:
array([[ 0, 6, 12, 18],
[ 1, 7, 13, 19],
[ 2, 8, 14, 20],
[ 3, 9, 15, 21],
[ 4, 10, 16, 22],
[ 5, 11, 17, 23]])
对多维数组应用transpose后的结果如下:
In [84]: trans_array = np.arange(0,24).reshape(2, 3, 4)
In [85]: trans_array.T.shape
Out[85]: (4, 3, 2)
拉平
拉平帮助将数据从多维数组压缩为一维数组:
In [86]: ravel_array = np.arange(0,12).reshape(4, 3)
In [87]: ravel_array.ravel()
Out[87]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
可以设置数组拉平的顺序。顺序可以是"C"、"F"、"A"或"K"。"C"是默认顺序,表示沿着行主序展开数组,而使用"F"时,数组沿列主序展开。"A"按类似 Fortran 的索引顺序读取数组元素,而"K"则按照元素在内存中存储的顺序读取:
In [88]: ravel_array.ravel(order = "F")
Out[88]: array([ 0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11])
添加新轴
NumPy 提供了newaxis方法,用来在现有数组中添加额外的轴:
# Creating a 1D array with 7 elements
In [98]: array_x = np.array([0, 1, 2, 3, 4, 5, 6])
In [99]: array_x.shape
Out[99]: (7,)
# Adding a new axis changes the 1D array to 2D
In [100]: array_x[:, np.newaxis]
Out[100]:
array([[0],
[1],
[2],
[3],
[4],
[5],
[6]])
In [101]: array_x[:, np.newaxis].shape
Out[101]: (7, 1)
# Adding 2 new axis to the 1D array to make it 3D
In [102]: array_x[:, np.newaxis, np.newaxis]
Out[102]:
array([[[0]],
[[1]],
[[2]],
[[3]],
[[4]],
[[5]],
[[6]]])
In [103]: array_x[:, np.newaxis, np.newaxis].shape
Out[103]: (7, 1, 1)
基本的线性代数运算
线性代数是矩阵和数组的重要运算集合。NumPy 包内置了一个名为linalg的特殊模块,用于处理所有线性代数的需求。以下部分将详细讨论linalg模块中一些常用的函数。
linalg模块的点乘函数帮助进行矩阵乘法。对于二维数组,它的行为与矩阵乘法完全相同。它要求第一个数组的最后一个维度与第二个数组的最后一个维度相等。数组的维度不必相等。对于 N 维数组,输出将具有 2N-2 个维度:
# For 2D arrays
In [23]: array_1 = np.random.randn(2, 4)
In [24]: array_2 = np.random.randn(4, 2)
In [25]: np.dot(array_1, array_2)
Out[25]:
array([[-2.89783151, 5.34861977],
[-0.98078998, -3.47603638]])
# For N dimensional arrays
In [37]: array_1 = np.random.randn(2, 4, 2)
In [38]: array_2 = np.random.randn(1, 1, 2, 1)
In [39]: np.dot(array_1, array_2).shape
Out[39]: (2, 4, 1, 1, 1)
linalg.multidot函数可以帮助一次性计算多个数组的乘积,而不是使用嵌套的点乘函数序列。此函数会自动找到最有效的计算顺序来评估乘积序列。
linalg.svd函数帮助进行奇异值分解,并返回分解后的三个数组。它接受一个具有两个或更多维度的数组作为输入:
In [42]: array_svd = np.random.randn(4, 3)
In [43]: np.linalg.svd(array_svd)
Out[43]:
(array([[-0.31366226, 0.27266983, 0.17962633, -0.89162858],
[ 0.72860587, 0.51810374, 0.44793275, -0.00763141],
[-0.59309456, 0.61499855, 0.26103908, 0.44930416],
[-0.13779807, -0.52820115, 0.83603183, 0.05537156]]),
array([1.68668514, 0.91044852, 0.65293131]),
array([[ 0.43322222, 0.10710679, 0.89490035],
[-0.73052453, 0.62326903, 0.27905131],
[-0.52787538, -0.77463789, 0.34825813]]))
数组的特征值和特征向量可以通过linalg.eig函数计算。eig函数要求输入数组的最后两个维度是方阵。该函数返回特征值和特征向量:
In [50]: np.linalg.eig(np.random.randn(5, 5))
Out[50]:
(array([ 2.52146488+0.j , -2.80191144+0.j ,
0.57756977+0.j , -0.65032217+1.22149327j,
-0.65032217-1.22149327j]),
array([[-0.85628289+0.j , -0.04688595+0.j ,
-0.71887813+0.j , -0.51046122-0.03158232j,
-0.51046122+0.03158232j],
[ 0.15793025+0.j , 0.7517844 +0.j ,
0.45393309+0.j , 0.52887467+0.j ,
0.52887467-0.j ],
[-0.35226803+0.j , 0.33640372+0.j ,
0.51482125+0.j , 0.40554944-0.02802925j,
0.40554944+0.02802925j],
[ 0.08722806+0.j , -0.07904384+0.j ,
-0.03872718+0.j , -0.41252898+0.16212983j,
-0.41252898-0.16212983j],
[ 0.33186767+0.j , 0.55964858+0.j ,
0.10304501+0.j , 0.14346541-0.27643973j,
0.14346541+0.27643973j]]))
linalg模块还提供了求解线性方程的函数。linalg.solve函数接受一个系数矩阵和因变量,并求解出精确解。它要求系数矩阵的所有行必须是线性独立的:
In [51]: a = np.array([[1, 2, 3], [5, 4, 2], [8, 9, 7]])
In [52]: b = np.array([6, 19, 47])
In [53]: np.linalg.solve(a, b)
Out[53]: array([-6.27272727, 15.81818182, -6.45454545])
如果需要最优解而非精确解,可以通过linalg.lstsq函数获得最小二乘解。
linalg.det函数计算方阵的行列式。如果输入数组有超过两个维度,它会被视为矩阵的堆栈,并计算每个堆栈的行列式。不过,最后两个维度必须对应一个方阵:
In [55]: np.linalg.det(np.random.randn(3,3))
Out[55]: -0.08292700167707867
In [56]: np.linalg.det(np.random.randn(2,3,3))
Out[56]: array([-0.22575897, 1.47647984])
数组排序
数组可以通过多种方式进行排序:
- 沿着某一轴对数组进行排序;首先,我们来讨论沿着y轴的排序:
In [43]: ar=np.array([[3,2],[10,-1]])
ar
Out[43]: array([[ 3, 2],
[10, -1]])
In [44]: ar.sort(axis=1)
ar
Out[44]: array([[ 2, 3],
[-1, 10]])
- 在这里,我们将解释沿着x轴的排序:
In [45]: ar=np.array([[3,2],[10,-1]])
ar
Out[45]: array([[ 3, 2],
[10, -1]])
In [46]: ar.sort(axis=0)
ar
Out[46]: array([[ 3, -1],
[10, 2]])
-
通过就地排序(
np.array.sort)和非就地排序(np.sort)函数进行排序。 -
其他可用于数组排序的操作包括以下内容:
-
np.min():此函数返回数组中的最小元素 -
np.max():此函数返回数组中的最大元素 -
np.std():此函数返回数组中元素的标准差 -
np.var():此函数返回数组中元素的方差 -
np.argmin():此函数返回数组中最小值的索引 -
np.argmax():此函数返回数组中最大值的索引 -
np.all():此函数返回数组中所有元素的逐元素逻辑“与” -
np.any():此函数返回数组中所有元素的逐元素逻辑“或”
到目前为止,我们已经熟悉了 NumPy 的功能。在接下来的部分中,我们将查看两个实际应用示例,其中广泛使用 NumPy 数组来执行复杂的计算。之后,我们将深入探讨 pandas 的核心数据结构,如 DataFrame、Series 和 Panel——它们是如何创建、修改和使用的。
使用 NumPy 实现神经网络
尽管 NumPy 并不是训练神经网络的首选包,但通过在 NumPy 中实现它,能展示 NumPy 在执行复杂矩阵计算方面的灵活性和强大功能,并且有助于更好地理解神经网络。
首先,让我们合成一个用于二分类问题的数据集,将用于训练神经网络。这些数据来自两个不同的高斯分布,模型将被训练来将数据分类为这两类中的任何一类。我们将为每个类别生成 1000 个样本:
N = 1000
X1 = np.random.randn(N, 2) + np.array([0.9, 0.9])
X2 = np.random.randn(N, 2) + np.array([-0.9, -0.9])
现在我们有两个 1000 x 2 的数组。对于预测变量,我们可以使用zeros和ones函数来创建两个不同的 1D 数组:
Y1 = np.zeros((N, 1))
Y2 = np.ones((N, 1))
四个数组——X1、X2、Y1 和 Y2——必须堆叠在一起,形成维度为 2000 x 3 的完整训练集:
X = np.vstack((X1, X2))
Y = np.vstack((Y1, Y2))
train = np.hstack((X, Y))
我们的目标是构建一个简单的神经网络,包含一个隐藏层和三个神经元。暂时让我们先不使用 NumPy,来理解我们将要从零开始构建的神经网络架构。
以下是一个简单神经网络架构的示意图:

简单神经网络架构示意图
输入层有两个神经元,隐藏层有三个神经元,输出层有一个神经元。方框表示偏置。为了实现神经网络,独立变量和预测变量已存储在x和t中:
x = train[:, 0:2]
t = train[:, 2].reshape(2000, 1)
由于这是一个二分类问题,sigmoid 函数是激活函数的理想选择:
def sigmoid(x, derive = False):
if (derive == True):
return x * (1 - x)
return 1 / (1 + np.exp(-x))
上述函数执行 sigmoid 变换,并计算其导数(用于反向传播)。训练过程包含两种传播模式——前馈传播和反向传播。
前馈传播的第一阶段是从输入层到隐藏层。这个阶段可以通过以下方程组来总结:
ah1 = sigmoid(x1w_ih11 + x2w_ih21 + 1b_ih1)*
ah2 = sigmoid(x1w_ih12 + x2w_ih22 + 1b_ih2)*
ah3 = sigmoid(x1w_ih13 + x2w_ih23 + 1b_ih3)*
在这里,ah1、ah2和ah3是前馈网络下一阶段的输入,从隐藏层到输出层。这涉及将维度为 2000 x 2 的输入矩阵与维度为 2 x 3 的权重矩阵w_ih(有三个隐藏神经元,因此是 3)相乘,然后加上偏置。与其单独处理偏置分量,不如将它们作为权重矩阵的一部分处理。可以通过向输入矩阵添加单位列向量,并将偏置值作为权重矩阵的最后一行来实现。因此,输入矩阵和权重矩阵的新维度将分别为 2000 x 3 和 3 x 3:
x_in = np.concatenate([x, np.repeat([[1]], 2000, axis = 0)], axis = 1)
w_ih = np.random.normal(size = (3, 3))
权重矩阵初始化为随机值:
y_h = np.dot(x_in, w_ih)
a_h = sigmoid(y_h)
在这里,*a_h* 是前馈阶段第二阶段的输入矩阵。就像输入矩阵 x 的情况一样,a_h* 应该附加单位列向量作为偏置,并且第二个权重矩阵应该初始化:
a_hin = np.concatenate([a_h, np.repeat([[1]], 2000, axis = 0)], axis = 1)
w_ho = np.random.normal(size = (4, 1)
现在,可以对这一阶段进行矩阵乘法和 Sigmoid 转换:
y_o = np.dot(a_hin, w_ho)
a_o = sigmoid(y_o)
为了简单起见,我们使用均方误差作为损失函数,尽管对于分类问题来说,使用对数损失函数会更合适:
E = ((1 / 2) * (np.power((a_o - t), 2)))
这标志着前馈的结束和反向传播的开始。反向传播的目标是找到应对权重和偏置进行的增量或差异,从而使得误差 E 减少。整个反向传播过程可以通过以下两个方程式总结。
第一个计算损失函数 E 关于 w_ho 的变化,第二个计算损失函数 E 关于 w_ih 的变化:


现在,在 NumPy 中实现这些方程式就像计算所有必要的导数并找到相应的乘积一样简单:
# Output layer
delta_a_o_error = a_o - t
delta_y_o = sigmoid(a_o, derive=True)
delta_w_ho = a_hin
delta_output_layer = np.dot(delta_w_ho.T,(delta_a_o_error * delta_y_o))
# Hidden layer
delta_a_h = np.dot(delta_a_o_error * delta_y_o, w_ho[0:3,:].T)
delta_y_h = sigmoid(a_h, derive=True)
delta_w_ih = x_in
delta_hidden_layer = np.dot(delta_w_ih.T, delta_a_h * delta_y_h)
需要对权重进行的变化已经计算出来。我们来使用这些增量值来更新权重:
eta = 0.1
w_ih = w_ih - eta * delta_hidden_layer
w_ho = w_ho - eta * delta_output_layer
在这里,*eta* 是模型的学习率。使用更新后的权重将再次进行前馈操作。反向传播将再次跟随,以减少误差。因此,前馈和反向传播应在设定的迭代次数内反复进行。完整代码如下:
### Neural Network with one hidden layer with feedforward and backpropagation
x = train[:,0:2]
t = train[:,2].reshape(2000,1)
x_in = np.concatenate([x, np.repeat([[1]], 2000, axis = 0)], axis = 1)
w_ih = np.random.normal(size = (3, 3))
w_ho = np.random.normal(size = (4, 1))
def sigmoid(x, derive = False):
if (derive == True):
return x * (1 - x)
return 1 / (1 + np.exp(-x))
epochs = 5000
eta = 0.1
for epoch in range(epochs):
# Feed forward
y_h = np.dot(x_in, w_ih)
a_h = sigmoid(y_h)
a_hin = np.concatenate([a_h, np.repeat([[1]], 2000, axis = 0)], axis = 1)
y_o = np.dot(a_hin, w_ho)
a_o = sigmoid(y_o)
# Calculate the error
a_o_error = ((1 / 2) * (np.power((a_o - t), 2)))
# Backpropagation
## Output layer
delta_a_o_error = a_o - t
delta_y_o = sigmoid(a_o, derive=True)
delta_w_ho = a_hin
delta_output_layer = np.dot(delta_w_ho.T,(delta_a_o_error * delta_y_o))
## Hidden layer
delta_a_h = np.dot(delta_a_o_error * delta_y_o, w_ho[0:3,:].T)
delta_y_h = sigmoid(a_h, derive=True)
delta_w_ih = x_in
delta_hidden_layer = np.dot(delta_w_ih.T, delta_a_h * delta_y_h)
w_ih = w_ih - eta * delta_hidden_layer
w_ho = w_ho - eta * delta_output_layer
print(a_o_error.mean())
神经网络已经运行了 5,000 次迭代。这是一个简单而高效的模型,非常适合解决各种问题。通过选择合适的迭代次数、学习率、损失函数和激活函数,可以获得良好的准确率。要进行测试和验证,仅使用前馈模块即可。
多维数组的实际应用
面板数据(类似电子表格的数据,具有几个可区分的行和列;我们通常遇到的数据类型)最好由 pandas 和 R 中的 DataFrame 数据结构处理。也可以使用数组,但那样会很繁琐。
那么,现实生活中一个可以最好地用数组表示的数据的好例子是什么呢?图像通常表示为像素的多维数组,是一个很好的例子。在本节中,我们将看到图像的多维表示示例以及为什么它是合理的。
任何在图像上执行的物体检测或图像处理算法都要求将其表示为数值数组格式。对于文本数据,使用术语-文档矩阵和词频-逆文档频率(TF-IDF)来将数据向量化(创建数值数组)。在图像的情况下,像素值用于表示图像。
对于一个 100 x 50 像素的 RGB 图像,将会有以下内容:
-
一个通道中的 5,000 个像素值
-
每个通道有红色、蓝色和绿色
因此,如果将图像像素展平为一个单一的向量,它的长度将是 15,000(每个通道各 5,000)。灰度图像则只有一个通道。每个像素值代表每个通道的亮度程度。
一组多张图像的数据集会变成四维数据,表示如下:
-
图像的宽度(以像素为单位)
-
图像的高度(以像素为单位)
-
通道数
-
图像的序列号
让我们通过读取图像来验证结果图像像素数组的形状。为了处理图像,Python 中有一个叫opencv(cv2)的库非常有帮助:
# reading images using opencv package
import cv2
import matplotlib.pyplot as plt
import os
os.chdir('')
img=cv2.imread('view.jpg')
img2=cv2.imread('rose.jpg')
imread方法返回一个像素数组。让我们检查返回的对象img的类型:
print(type(img))
这将返回<class 'numpy.ndarray'>,这确认它返回的是一个numpy数组。
接下来,让我们看看数组的形状。它应该返回像素宽度、像素高度和通道数:
img.shape
这将返回 (183, 275, 3)。
它是一个包含三个数字的元组,分别表示图像的高度(以像素为单位)、图像的宽度(以像素为单位)和通道数。因此,这张图像的高度为 183 像素,宽度为 275 像素,并且有三个通道,维度为 183 x 275,分别表示红色、蓝色和绿色。
打印时,img对象如下所示:

作为多维数组的图像像素
现在问题来了,为什么有人会想要子集化一个图像像素数组呢?这可能有多个原因:
-
选择和/或操作图像中的兴趣区域。这可以是表示物体的图像中的一个小块。
-
仅从图像中选择一个颜色通道。
可以将像素数组视为一个图像/绘图,其中像素高度和像素宽度作为坐标轴标签,代码如下所示:
plt.imshow(img)
看一下以下输出:

作为图像绘制的图像像素数组
仅选择一个通道
如前所示,第三维表示 RGB 通道。因此,要过滤某一通道中的所有像素,我们应选择前两维的所有像素,仅选择感兴趣的通道。另外,Python 中的索引从 0 开始,因此 0 表示红色,1 表示绿色,2 表示蓝色。牢记这些,接下来让我们看看选择图像中红色、绿色和蓝色通道的代码片段。
选择红色通道的代码片段如下:
img_r=img[:,:,0]
plt.imshow(img_r)
以下是输出结果:

仅选择红色通道中的像素构成的图像像素数组,已作为图像可视化
选择绿色通道的代码片段如下:
img_g=img[:,:,1]
plt.imshow(img_g)
以下是输出结果:

仅选择绿色通道中的像素构成的图像像素数组,已作为图像可视化
选择蓝色通道的代码片段如下:
img_b=img[:,:,2]
plt.imshow(img_b)
以下是输出结果:

仅选择蓝色通道的像素所构成的图像像素数组,已被可视化为图像
选择图像的兴趣区域
让我们尝试选择前面截图中的树。通过查看带有坐标轴标签的图像,可以明显看出树的垂直范围在 50 到 155 像素之间,水平方向的范围在 95 到 190 像素之间。让我们尝试通过所有通道来子集该区域:
img_tree=img[50:155,95:190,:]
plt.imshow(img_tree)
以下图像展示了所选的兴趣区域(ROI):

选择图像中的 ROI
该操作类似于裁剪图像。
某个 ROI 或通道的像素值可以被赋予不同的值。这个方法可以用于以下操作:
-
移除某些通道(如果我们将该通道的值替换为 0)
-
将某个 ROI 复制并粘贴到图像的另一部分
以下代码展示了后者的示例:
img3=img
img3[50:155,1:96,:]=img_tree
plt.imshow(img3)
以下图像展示了将所选 ROI 粘贴到另一个图像区域的情况:

将选定的 ROI 粘贴到另一个图像区域
在这个示例中,我们将树的 ROI 复制并粘贴到所选 ROI 左侧的区域。此操作通过将粘贴目标位置的像素值设置为复制源的像素值来实现。
多通道选择并抑制其他通道
要显示仅某种颜色的像素,其他颜色的像素需要被抑制或赋值为 0。通道选择可以通过索引或传递列表来实现:
-
索引:请记住,在进行索引时,冒号右边的值表示通道的上限。它还计算到 n-1。例如,
img[:,:,1:3]将选择直到通道 2 的通道,即蓝色通道,从通道 1(绿色)开始,但不包括通道 0(红色)。代码片段img[:,:,0:2]会选择通道 0(红色通道)和通道 1(绿色),但不包括通道 2(蓝色)。 -
列表:像[0,2]这样的列表表示选择通道 0 和通道 2,即红色和蓝色。
在下面的示例中,我们抑制非红色、非绿色和非蓝色的像素,因此结果图像中的像素分别呈现红色、绿色和蓝色:
fig, axes = plt.subplots(1, 3)
# Red Channel
imgR = img.copy()
imgR[:, :, 1:3] = 0 # Assigning Green and Blue channel pixels to 0
axes[0].imshow(imgR)
# Green Channel
imgG = img.copy()
imgG[:, :, [0,2]] = 0 # Assigning Red and Blue channel pixels to 0
axes[1].imshow(imgG)
# Blue Channel
imgB = img.copy()
imgB[:, :, 0:2] = 0 0 # Assigning Red and Green channel pixels to 0
axes[2].imshow(imgB)
以下是输出结果:

面板显示通过抑制两个通道只显示第三种颜色的图像(从左到右,分别抑制了绿色和蓝色、红色和蓝色、红色和绿色)
音频数据也可以表示为跨水平方向的压力读数数组。在这里也可以使用类似的数组操作技巧。
pandas 中的数据结构
pandas 包是由 Wes McKinney 在 2008 年创建的,起因是他在使用 R 处理时间序列数据时遇到的种种困境。它建立在 NumPy 之上,并提供了 NumPy 中没有的功能。它提供了快速、易于理解的数据结构,并弥补了 Python 和像 R 这样的语言之间的空白。NumPy 处理的是同质的数据块,而使用 pandas 可以帮助处理由不同数据类型组成的表格结构数据。
pandas 的官方文档可以在 pandas.pydata.org/pandas-docs/stable/dsintro.html 找到。
pandas 中有三种主要的数据结构:
-
序列—1D
-
DataFrame—2D
-
Panel—3D
序列
Series 本质上是一个 1D 的 NumPy 数组。它由一个 NumPy 数组和一个标签数组组成。就像一个 NumPy 数组,Series 可以完全由任何数据类型组成。标签一起被称为 Series 的索引。Series 包含两个部分——1D 数据和索引。
Series 创建
创建序列数据结构的一般构造如下:
import pandas as pd
ser = pd.Series(data, index = idx)
在这里,数据可以是以下之一:
-
一个
ndarray -
一个 Python 字典
-
一个标量值
如果未指定索引,将创建以下默认索引 [0,... n-1],其中 n 是数据的长度。
Series 可以从多种来源创建,以下小节将介绍这些方式。
使用 ndarray
在这种情况下,索引必须与数据的长度相同。以下示例创建了一个包含七个介于 0 和 1 之间的随机数的 Series 结构;索引没有指定:
In [4]: ser = pd.Series(np.random.randn(7))
In [5]: ser
Out[5]:
0 3.063921
1 0.097450
2 -1.660367
3 -1.221308
4 -0.948873
5 0.454462
6 0.586824
dtype: float64
索引也可以是字符串对象。以下示例创建了一个包含前五个月名称的 Series 结构,并指定了月份名称作为索引:
In [6]: import calendar as cal
In [7]: monthNames=[cal.month_name[i] for i in np.arange(1,6)]
In [8]: months = pd.Series(np.arange(1,6), index = monthNames)
In [10]: months
Out[10]:
January 1
February 2
March 3
April 4
May 5
dtype: int32
In [11]: months.index
Out[11]: Index(['January', 'February', 'March', 'April', 'May'], dtype='object')
使用 Python 字典
字典由键值对组成。当使用字典创建 Series 时,字典的键形成索引,值则构成 Series 的 1D 数据:
In [12]: currDict={'US' : 'dollar', 'UK' : 'pound', 'Germany': 'euro', 'Mexico':'peso', 'Nigeria':'naira', 'China':'yuan', 'Japan':'yen'}
In [13]: currSeries = pd.Series(currDict)
In [14]: currSeries
Out[14]:
US dollar
UK pound
Germany euro
Mexico peso
Nigeria naira
China yuan
Japan yen
dtype: object
pandas Series 结构的索引类型是 pandas.core.index.Index,可以视为一个有序的多重集合。
如果在创建 Series 时同时指定了索引,则该指定的索引将覆盖字典中的键。如果指定的索引包含原字典中不存在的键,则会在 Series 中对应的索引位置填充 NaN:
In [18]: stockPrices = {'GOOG':1180.97, 'FB':62.57, 'TWTR': 64.50, 'AMZN':358.69, 'AAPL':500.6}
# "YHOO" is not a key in the above dictionary
In [19]: stockPriceSeries = pd.Series(stockPrices, index=['GOOG','FB','YHOO','TWTR','AMZN','AAPL'], name='stockPrices')
In [20]: stockPriceSeries
Out[20]:
GOOG 1180.97
FB 62.57
YHOO NaN
TWTR 64.50
AMZN 358.69
AAPL 500.60
Name: stockPrices, dtype: float64
注意,Series 还有一个名称属性,可以像前面的代码片段那样设置。名称属性在将多个 Series 对象合并为 DataFrame 结构时非常有用。
使用标量值
Series 也可以仅用标量值初始化。对于标量数据,必须提供索引。该值将为尽可能多的索引值重复。此方法的一种可能用途是提供一种快速且简便的初始化方法,之后再填充 Series 结构。让我们看看如何使用标量值创建一个 Series:
In [21]: dogSeries=pd.Series('chihuahua', index=['breed', 'countryOfOrigin', 'name', 'gender'])
In [22]: dogSeries = pd.Series('chihuahua', index=['breed', 'countryOfOrigin', 'name', 'gender'])
In [23]: dogSeries
Out[23]:
breed chihuahua
countryOfOrigin chihuahua
name chihuahua
gender chihuahua
dtype: object
对 Series 的操作
Series 的行为与本章前面讨论的 NumPy 数组非常相似,唯一的区别是操作(如切片)也会切片 Series 的索引。
赋值
可以通过类似字典的方式使用索引标签来设置和访问值:
# Accessing value from series using index label
In [26]: currDict['China']
Out[26]: 'yuan'
# Assigning value to series through a new index label
In [27]: stockPriceSeries['GOOG'] = 1200.0
In [28]: stockPriceSeries
Out[28]:
GOOG 1200.00
FB 62.57
YHOO NaN
TWTR 64.50
AMZN 358.69
AAPL 500.60
Name: stockPrices, dtype: float64
与 dict 相同,如果你尝试检索一个缺失的标签,会引发 KeyError:
In [29]: stockPriceSeries['MSFT']
KeyError: 'MSFT'
通过显式使用 get 来避免此错误,示例如下:
In [30]: stockPriceSeries.get('MSFT, np.NaN)
Out[30]: nan
在这种情况下,当 Series 结构中不存在该键时,默认值 np.NaN 被指定为返回的值。
切片
切片操作与 NumPy 数组的切片方式相同。可以使用索引数字进行切片,如以下代码所示:
# Slice till the 4th index (0 to 3)
In [31]: stockPriceSeries[:4]
Out[31]:
GOOG 1200.00
FB 62.57
YHOO NaN
TWTR 64.50
Name: stockPrices, dtype: float64
Logical slicing also works as follows:
In [32]: stockPriceSeries[stockPriceSeries > 100]
Out[32]:
GOOG 1200.00
AMZN 358.69
AAPL 500.60
Name: stockPrices, dtype: float64
其他操作
算术和统计操作可以像在 NumPy 数组中一样应用。这样的操作以矢量化方式在 Series 中进行,就像在 NumPy 数组中一样,不需要通过循环:
# Mean of entire series
In [34]: np.mean(stockPriceSeries)
Out[34]: 437.27200000000005
# Standard deviation of entire series
In [35]: np.std(stockPriceSeries)
Out[35]: 417.4446361087899
也可以对 Series 执行逐元素操作:
In [36]: ser
Out[36]:
0 3.063921
1 0.097450
2 -1.660367
3 -1.221308
4 -0.948873
5 0.454462
6 0.586824
dtype: float64
In [37]: ser * ser
Out[37]:
0 9.387611
1 0.009496
2 2.756819
3 1.491593
4 0.900359
5 0.206535
6 0.344362
dtype: float64
Series 的一个重要特性是数据会自动根据标签对齐:
In [40]: ser[1:]
Out[40]:
1 0.097450
2 -1.660367
3 -1.221308
4 -0.948873
5 0.454462
6 0.586824
dtype: float64
In [41]: ser[1:] + ser[:-2]
Out[41]:
0 NaN
1 0.194899
2 -3.320734
3 -2.442616
4 -1.897745
5 NaN
6 NaN
dtype: float64
因此,我们可以看到,对于不匹配的标签,会插入 NaN。默认情况下,未对齐的 Series 结构会生成索引的并集。这是首选的行为,因为信息会被保留下来而非丢失。我们将在本书后面的章节中处理 pandas 中的缺失值。
DataFrame
DataFrame 是一个由行和列组成的二维数据结构——就像一个简单的电子表格或 SQL 表格。DataFrame 的每一列都是 pandas Series。这些列应具有相同的长度,但可以是不同的数据类型——如浮点型、整型、布尔型等。DataFrame 既是值可变的,又是大小可变的。这使我们能够执行修改 DataFrame 中值的操作,或者在 DataFrame 中添加/删除列。
类似于一个具有名称和索引作为属性的 Series,DataFrame 具有列名和行索引。行索引可以由数字值或字符串(如月份名称)组成。索引对于快速查找以及数据的正确对齐和连接是必需的,pandas 中也支持多级索引。以下是一个简单的 DataFrame 视图,包含五行三列。通常情况下,索引不算作列:
| 索引 | 事件类型 | 总参与人数 | 学生参与比例 |
|---|---|---|---|
| 星期一 | C | 42 | 23.56% |
| 星期二 | B | 58 | 12.89% |
| 星期三 | A | 27 | 45.90% |
| 星期四 | A | 78 | 47.89% |
| 星期五 | B | 92 | 63.25% |
DataFrame 创建
DataFrame 是 pandas 中最常用的数据结构。构造函数接受多种不同类型的参数:
-
由 1D ndarray、列表、字典或 Series 结构组成的字典
-
2D NumPy 数组
-
结构化或记录型 ndarray
-
Series
-
另一个 DataFrame
行标签索引和列标签可以与数据一起指定。如果没有指定,它们将通过直观的方式从输入数据中生成,例如,从 dict 的键中(对于列标签)或使用 np.range(n)(对于行标签),其中 n 是行数。
DataFrame 可以从多种来源创建,以下小节将详细讨论这些方法。
使用包含 Series 的字典
字典中的每个实体都是一个键值对。DataFrame 本质上是将多个 Series 组合在一起的字典。Series 的名称对应键,而 Series 的内容对应值。
第一步,应该定义一个包含所有 Series 的字典:
stockSummaries = {
'AMZN': pd.Series([346.15,0.59,459,0.52,589.8,158.88],
index=['Closing price','EPS',
'Shares Outstanding(M)',
'Beta', 'P/E','Market Cap(B)']),
'GOOG': pd.Series([1133.43,36.05,335.83,0.87,31.44,380.64],
index=['Closing price','EPS','Shares Outstanding(M)',
'Beta','P/E','Market Cap(B)']),
'FB': pd.Series([61.48,0.59,2450,104.93,150.92],
index=['Closing price','EPS','Shares Outstanding(M)',
'P/E', 'Market Cap(B)']),
'YHOO': pd.Series([34.90,1.27,1010,27.48,0.66,35.36],
index=['Closing price','EPS','Shares Outstanding(M)',
'P/E','Beta', 'Market Cap(B)']),
'TWTR':pd.Series([65.25,-0.3,555.2,36.23],
index=['Closing price','EPS','Shares Outstanding(M)',
'Market Cap(B)']),
'AAPL':pd.Series([501.53,40.32,892.45,12.44,447.59,0.84],
index=['Closing price','EPS','Shares Outstanding(M)','P/E',
'Market Cap(B)','Beta'])}
上述字典总结了六只不同股票的表现,并表示 DataFrame 将有六列。可以观察到,每个 Series 都有不同的索引集合且长度不同。最终的 DataFrame 将包含每个索引中唯一的一组值。如果某一列在某一行索引位置没有值,系统会自动在该单元格中添加 NA。接下来的步骤将此字典转换为 DataFrame:
stockDF = pd.DataFrame(stockSummaries)
让我们打印出前面步骤中创建的 DataFrame:

DataFrame 不一定需要包含原始字典中的所有行和列标签。有时,可能只需要其中的一部分行和列。在这种情况下,可以限制行和列索引,如下所示:
stockDF = pd.DataFrame(stockSummaries,
index=['Closing price','EPS',
'Shares Outstanding(M)',
'P/E', 'Market Cap(B)','Beta'],
columns=['FB','TWTR','SCNW'])
在这里,添加了一个新的列名 SCNW,该列名在原始字典中不存在。这将导致生成一个名为 SCNW 的列,其中所有值为 NA。同样,手动传递一个在原始数据结构中不存在的索引名,将导致该行全部为 NA。
让我们打印出前面的 DataFrame:

可以通过 DataFrame 的属性访问行索引和列名称:
In [47]: stockDF.index
Out[47]:
Index(['Closing price', 'EPS', 'Shares Outstanding(M)', 'P/E', 'Market Cap(B)',
'Beta'],
dtype='object')
In [48]: stockDF.columns
Out[48]: Index(['FB', 'TWTR', 'SCNW'], dtype='object')
使用包含 ndarray/列表的字典
在前面的示例中,字典中的值是 Series,作为键值对的值。也可以通过包含列表的字典来构建 DataFrame,而不是使用包含 Series 的字典。与之前的情况不同,行索引在字典中并未定义。因此,行标签索引是通过 np.range(n) 生成的。因此,在这种情况下,字典中的所有列表或数组必须具有相同的长度。如果不满足这一条件,将会发生错误。
列表的字典在以下代码中定义:
algos = {'search': ['DFS','BFS','Binary Search',
'Linear','ShortestPath (Djikstra)'],
'sorting': ['Quicksort','Mergesort', 'Heapsort',
'Bubble Sort', 'Insertion Sort'],
'machine learning': ['RandomForest', 'K Nearest Neighbor',
'Logistic Regression', ''K-Means Clustering', 'Linear Regression']}
现在,让我们将这个字典转换为 DataFrame 并打印出来:
algoDF = pd.DataFrame(algos)
请查看以下输出:

在这里,行索引被分配了从 0 到 4 的连续值。也可以如下面的代码所示,为行索引指定自定义值:
pd.DataFrame(algos,index=['algo_1','algo_2','algo_3','algo_4','algo_5'])
请查看以下输出:

使用结构化数组
结构化数组与 ndarrays 稍有不同。结构化数组中的每个字段可以具有不同的数据类型。有关结构化数组的更多信息,请参考以下链接:docs.scipy.org/doc/numpy/user/basics.rec.html。
以下是一个结构化数组的示例:
memberData = np.array([('Sanjeev',37,162.4),
('Yingluck',45,137.8),
('Emeka',28,153.2),
('Amy',67,101.3)],
dtype = [('Name','a15'),
('Age','i4'),
('Weight','f4')])
这个结构化数组有三个字段,其数据类型已在一个包含字段名称的元组列表中定义。可以使用相同的 DataFrame 函数从结构化数组构建 DataFrame:
memberDF = pd.DataFrame(memberData)
看一下以下输出结果:

默认情况下,连续的整数值被分配给索引。也可以替换这些索引:
pd.DataFrame(memberData, index=['a','b','c','d'])
看一下以下输出结果:

可以通过 DataFrame 函数的 columns 参数重新排序列:
pd.DataFrame(memberData, columns = ["Weight", "Name", "Age"])
看一下以下输出结果:

使用字典列表
当一个字典列表被转换为 DataFrame 时,列表中的每个字典对应 DataFrame 中的一行,而每个字典中的键表示列标签。
让我们定义一个字典列表:
demographicData = [{"Age": 32, "Gender": "Male"}, {"Race": "Hispanic", "Gender": "Female", "Age": 26}]
现在,字典列表可以转换为 DataFrame,如下所示:
demographicDF = pd.DataFrame(demographicData)
以下是输出结果:

使用元组字典进行多级索引
一个元组字典可以创建一个具有分层索引行和列的结构化 DataFrame。以下是一个元组字典:
salesData = {("2012", "Q1"): {("North", "Brand A"): 100, ("North", "Brand B"): 80, ("South", "Brand A"): 25, ("South", "Brand B"): 40},
("2012", "Q2"): {("North", "Brand A"): 30, ("South", "Brand B"): 50},
("2013", "Q1"): {("North", "Brand A"): 80, ("North", "Brand B"): 10, ("South", "Brand B"): 25},
("2013", "Q2"): {("North", "Brand A"): 70, ("North", "Brand B"): 50, ("South", "Brand A"): 35, ("South", "Brand B"): 40}}
与常规的键值对不同,键是一个包含两个值的元组,表示行索引中的两个级别,值是一个字典,其中每个键值对表示一列。在这里,键同样是一个元组,表示两个列索引。
现在,这个元组字典可以转换为 DataFrame 并打印:
salesDF = pd.DataFrame(salesData)
以下是输出结果:

使用 Series
考虑以下系列:
In [12]: currDict={'US' : 'dollar', 'UK' : 'pound', 'Germany': 'euro', 'Mexico':'peso', 'Nigeria':'naira', 'China':'yuan', 'Japan':'yen'}
In [13]: currSeries = pd.Series(currDict)
Out[13]:
US dollar
UK pound
Germany euro
Mexico peso
Nigeria naira
China yuan
Japan yen
Name: Currency, dtype: object
在这里,Series 定义了索引和名称。在转换为 DataFrame 时,保留此索引,并且 Series 的名称被分配为列名:
currDF = pd.DataFrame(currSeries)
以下是输出结果:

还有其他替代的 DataFrame 构造方法,可以总结如下:
DataFrame.from_dict:它接受一个字典的字典或序列,并返回一个 DataFrame。由于可以指定顺序的参数,它与前面讨论的方法略有不同。虽然另一种方法总是将字典的键转换为列,但这个构造函数提供了一个选项,将键转换为行标签:
# Default setting
pd.DataFrame.from_dict(algos, orient = "columns")
以下是输出结果:

另一种方法如下:
pd.DataFrame.from_dict(algos, orient = "index", columns = ["A", "B", "C", "D", "E"])
以下是输出结果:

DataFrame.from_records:它接受一个元组列表或结构化ndarray来构建 DataFrame。与之前提到的结构化数组方法不同,这个函数允许你将数组的某个字段设置为索引:
pd.DataFrame.from_records(memberData, index="Name")
以下是输出结果:

对 pandas DataFrame 的操作
可以对 DataFrame 执行许多操作,例如列/行索引、赋值、拼接、删除等。我们将在以下小节中详细介绍这些操作。
列选择
可以通过列名从 DataFrame 中选择出一个特定的列,作为一个 Series:
In [60]: memberDF["Name"]
Out[60]:
0 b'Sanjeev'
1 b'Yingluck'
2 b'Emeka'
3 b'Amy'
Name: Name, dtype: object
添加新列
可以通过插入一个标量值到 DataFrame 中来添加新列。将标量值插入到 DataFrame 的任何列中,将会使整个列填充为该标量值:
In [61]: memberDF['Height'] = 60
In [62]: memberDF
以下是输出结果:

可以将一组值(而不是标量值)分配给列:
In [63]: memberDF['Height2'] = [57, 62, 65, 59]
In [64]: memberDF
以下是输出结果:

也可以使用 insert 方法将列插入到指定位置。此方法需要三个参数:插入位置、新列名以及传入的值:
In [65]: memberDF.insert(1, "ID", ["S01", "S02", "S03", "S04"])
In [66]: memberDF
以下是输出结果:

删除列
del 命令可以用来删除单个列,如下面的代码所示:
In [67]: del memberDF["Height"]
In [68]: memberDF
以下是输出结果:

除了 del,也可以使用 pop 方法,类似于字典中的操作:
In [65]: height2 = memberDF.pop("Height2")
In [66]: memberDF
以下是输出结果:

DataFrame 对齐
两个 DataFrame 的联合是基于行和列的索引进行的。通过一个例子来理解这个过程。考虑以下两个 DataFrame:
ore1DF=pd.DataFrame(np.array([[20,35,25,20],
[11,28,32,29]]),
columns=['iron','magnesium',
'copper','silver'])
ore2DF=pd.DataFrame(np.array([[14,34,26,26],
[33,19,25,23]]),
columns=['iron','magnesium',
'gold','silver'])
+ 运算符会在两个 DataFrame 中相同标签的列之间进行加法:
ore1DF + ore2DF
以下是输出结果:

在这两个 DataFrame 中未找到列——铜和金。因此,这些列中被填充为 NA。
如果将 DataFrame 对象与 Series 对象结合使用,默认行为是将 Series 对象广播到所有行:
ore1DF + pd.Series([25,25,25,25], index=['iron', 'magnesium', 'copper', 'silver'])
以下是输出结果:

其他数学运算
基本数学运算符适用于 DataFrame。例如,两个列相加、相乘、相减或相除可以得到一个新列:
In [67]: ore1DF["add_iron_copper"] = ore1DF["iron"] + ore1DF["copper"]
以下是输出结果:

逻辑运算符如 |(或)、&(与)和 ^(非)适用于 DataFrame。考虑以下两个 DataFrame:
logical_df1 = pd.DataFrame({'Col1' : [1, 0, 1], 'Col2' : [0, 1, 1] }, dtype=bool)
logical_df2 = pd.DataFrame({'Col1' : [1, 0, 0], 'Col2' : [0, 0, 1] }, dtype=bool)
现在,对这两列进行逻辑“或”运算,得到以下结果:
logical_df1 | logical_df2
以下是输出结果:

还可以使用 NumPy 函数在 DataFrame 上执行操作:
np.sqrt(ore1DF)
以下是输出结果:

面板
面板是一个 3D 数组。它不像 Series 或 DataFrame 那样广泛使用。由于其 3D 的特性,它不像另外两个那样容易显示在屏幕上或进行可视化。面板数据结构是 pandas 中数据结构的最后一块拼图。它的使用相对较少,通常用于 3D 时间序列数据。三个轴的名称如下:
-
items:这是轴 0,每个项目对应于一个 DataFrame 结构。 -
major_axis:这是轴 1,每个项目对应于 DataFrame 结构的行。 -
minor_axis:这是轴 2,每个项目对应于每个 DataFrame 结构的列。
面板已被弃用,并且在未来的版本中将不再提供。因此,建议在 DataFrame 中使用多重索引,替代面板。
与 Series 和 DataFrame 一样,有多种方法可以创建面板对象。它们将在接下来的章节中讲解。
使用带有轴标签的 3D NumPy 数组
在这里,我们展示了如何从 3D NumPy 数组构造面板对象。定义完 3D 数组后,可以通过定义三个轴中的每一个来创建面板:
In [68]: stockData = np.array([[[63.03,61.48,75],
[62.05,62.75,46],
[62.74,62.19,53]],
[[411.90, 404.38, 2.9],
[405.45, 405.91, 2.6],
[403.15, 404.42, 2.4]]])
In [69]: stockHistoricalPrices = pd.Panel(stockData,
items=['FB', 'NFLX'], major_axis=pd.date_range('2/3/2014', periods=3),
minor_axis=['open price', 'closing price', 'volume'])
In [70]: stockHistoricalPrices
Out[70]:
<class 'pandas.core.panel.Panel'>
Dimensions: 2 (items) x 3 (major_axis) x 3 (minor_axis)
Items axis: FB to NFLX
Major_axis axis: 2014-02-03 00:00:00 to 2014-02-05 00:00:00
Minor_axis axis: open price to volume
使用 Python 字典形式的 DataFrame 对象
面板由多个 DataFrame 组成。为了创建面板,我们来定义两个 DataFrame:
USData = pd.DataFrame(np.array([[249.62 , 8900],
[ 282.16,12680],
[309.35,14940]]),
columns=['Population(M)','GDP($B)'],
index=[1990,2000,2010])
ChinaData = pd.DataFrame(np.array([[1133.68, 390.28],
[ 1266.83,1198.48],
[1339.72, 6988.47]]),
columns=['Population(M)','GDP($B)'],
index=[1990,2000,2010])
现在,可以创建这些 DataFrame 的字典:
In [73]: US_ChinaData={'US' : USData, 'China': ChinaData}
In [74]: pd.Panel(US_ChinaData)
Out[74]:
<class 'pandas.core.panel.Panel'>
Dimensions: 2 (items) x 3 (major_axis) x 2 (minor_axis)
Items axis: US to China
Major_axis axis: 1990 to 2010
Minor_axis axis: Population(M) to GDP($B)
使用 DataFrame.to_panel 方法
一个多重索引的 DataFrame 与面板相似。因此,一个多重索引的 DataFrame 可以直接转换为面板:
In [75]: mIdx = pd.MultiIndex(levels = [['US', 'China'], [1990,2000, 2010]], labels=[[1,1,1,0,0,0],[0,1,2,0,1,2]])
In [76]: ChinaUSDF = pd.DataFrame({'Population(M)' : [1133.68, 1266.83, 1339.72, 249.62, 282.16, 309.35], GDB($B)': [390.28, 1198.48, 6988.47, 8900, 12680,14940]}, index=mIdx)
In [77]: ChinaUSDF.to_panel()
Out[77]:
<class 'pandas.core.panel.Panel'>
Dimensions: 2 (items) x 2 (major_axis) x 3 (minor_axis)
Items axis: Population(M) to GDB($B)
Major_axis axis: China to US
Minor_axis axis: 1990 to 2010
美国/中国经济数据的来源网站如下:
其他操作
插入、删除和逐项操作与 DataFrame 的情况相同。面板结构可以通过转置进行重新排列。面板操作的功能集相对不成熟,且不如 Series 和 DataFrame 那样丰富。
摘要
本章简要介绍了 NumPy 的强大功能,并展示了它如何在使用 pandas 时让工作变得更轻松。本章的一些亮点如下:
-
NumPy 数组是一个多功能的数据结构,用于存储多维的同质数据。
-
NumPy 包中提供了多种方法用于切片、切分、创建和操作数组。
-
NumPy 数组具有实际应用,例如作为线性代数运算的构建模块,或作为处理多维数组数据(如图像和音频)的工具。
-
数组(或矩阵)是用于高级数学模型(如神经网络)的计算模块。
-
NumPy 数组是 pandas 中一些基本数据结构的前身,特别是 Series。
-
Series 与数组非常相似。Series 是一维的,可以传递自定义索引给 Series。数组或列表可以转换为 Series,带有索引的 Series 也可以转换为 DataFrame。
-
Series、DataFrame 和 Panel 是 pandas 中常用的其他数据结构,其中 DataFrame 最为流行。
-
可以使用元组字典创建多重索引的 DataFrame。也可以使用简单的字典或包含列表/数组的字典来创建 DataFrame。
在下一章,我们将专注于 pandas 中各种数据源的 I/O 操作。pandas 支持多种数据结构和数据源,可以进行读写操作。我们将在下一章中学习所有这些内容及更多内容。
参考资料
第四章:不同数据格式的 I/O 操作与 pandas
数据科学家需要处理来自各种来源的数据,因此数据格式也各异。最常见的格式是无处不在的电子表格、Excel 表格、CSV 和文本文件。但也有许多其他格式,如 URL、API、JSON、XML、HDF、Feather 等,具体取决于数据访问的方式。本章我们将涉及以下几个主题:
-
数据源和 pandas 方法
-
CSV 和 TXT
-
URL 和 S3
-
JSON
-
读取 HDF 格式
开始吧!
数据源和 pandas 方法
数据科学项目的数据源可以分为以下几类:
-
数据库:大多数 CRM、ERP 及其他业务操作工具都将数据存储在数据库中。根据数据的体积、速度和种类,可能是传统的数据库或 NoSQL 数据库。为了连接到大多数流行的数据库,我们需要 Python 的
JDBC/ODBC驱动程序。幸运的是,所有流行数据库的驱动程序都有提供。使用这些数据库中的数据需要通过 Python 连接到数据库,通过 Python 查询数据,然后使用 pandas 进行数据操作。我们将在本章后面查看如何实现这一过程的示例。 -
Web 服务:许多业务操作工具,尤其是 软件即服务(SaaS)工具,通过 应用程序编程接口(API)而非数据库提供其数据访问。这减少了永久托管数据库的基础设施成本。相反,数据作为服务按需提供。可以通过 Python 发起
API调用,返回格式为JSON或XML的数据包。然后解析这些数据并使用 pandas 进行进一步处理。 -
数据文件:许多用于原型数据科学模型的数据以数据文件的形式存在。一个例子是来自物联网传感器的数据——这些传感器的数据通常存储在平面文件中,如
.txt文件或.csv文件。另一个数据文件来源是从数据库中提取并存储在这些文件中的示例数据。许多数据科学和机器学习算法的输出也存储在此类文件中,如 CSV、Excel 和.txt文件。另一个例子是深度学习神经网络模型的训练权重矩阵,可以存储为 HDF 文件。 -
网页和文档抓取:另一个数据来源是网页上存在的表格和文本。通过使用如 BeautifulSoup 和 Scrapy 等 Python 包,从这些页面中提取数据,并将其放入数据文件或数据库中以供进一步使用。存在于其他非数据格式文件中的表格和数据,如 PDF 或 Docs,也是一个重要的数据来源。这些数据通过如 Tesseract 和 Tabula-py 等 Python 包进行提取。
在本章中,我们将研究如何使用 pandas 和附属库读取和写入这些格式/源的数据。我们还将讨论这些格式的简要介绍,它们的用途以及可以对它们执行的各种操作。
以下是 Python 中用于读取和写入某些数据格式的方法总结,这些格式将在本章中讨论:

pandas 中用于不同数据文件格式的读取和写入方法及其来源
这些部分标题意味着我们正在处理该文件类型的 I/O 操作。
CSV 和 TXT
CSV 代表逗号分隔值,意味着逗号是这些文件的默认分隔符。然而,它们也接受其他分隔符。
CSV 由列和行组成,单元格值以表格形式排列。它们可以带有或不带有列名和行索引。CSV 文件存在的主要原因包括手动收集的数据、从数据库中提取并下载的数据、从工具或网站直接下载的数据、网页抓取的数据以及运行数据科学算法的结果。
读取 CSV 和 TXT 文件
read_csv是 pandas 中读取 CSV 文件的首选方法。它也可以用来读取txt文件。使用read_csv的语法如下:
pd.read_csv(filepath, sep=', ', dtype=None, header=None, names=None, skiprows=None, index_col=None, skip_blank_lines=TRUE, na_filter=TRUE)
read_csv方法的参数如下:
-
filepath: 字符串或带有或不带有文件路径的文件名。 -
dtype: 可以作为字典传递,字典包含名称和类型的键值对。指定列名的数据类型。通常,pandas 会根据前几行来猜测列的类型。 -
header: True/False。指定数据的第一行是否为标题。 -
names: 列表。指定数据集所有列的列名。 -
skiprows: 列表。通过指定行索引跳过某些数据行。 -
index_col: Series/列表。指定可以作为行号/标识符的列。 -
skip_blank_lines: True/False。指定是否跳过空行。 -
na_filter: True/False。指定是否过滤 NA 值。 -
usecols: 列表。返回传递列表中的列的数据子集。
read_csv方法返回一个 DataFrame。以下是使用read_csv方法读取文件的一些示例。
读取 CSV 文件
我们可以使用以下代码读取 CSV 文件:
import pandas as pd
import os
os.chdir(' ')
data=pd.read_csv('Hospital Cost.csv')
为数据集指定列名
以下代码将指定数据集的列名:
column_names=pd.read_csv('Customer Churn Columns.csv')
column_names_list=column_names['Column Names'].tolist()
data=pd.read_csv('Customer Churn Model.txt',header=None,names=column_names_list)
请注意,列名是从文件中读取的,然后转换成列表,并传递给read_csv中的 names 参数。
从数据字符串中读取
下面是我们如何使用read_csv从字符串列表创建一个 DataFrame:
from io import StringIO
data = 'col1,col2,col3\na,b,1\na,b,2\nc,d,3\nc,e,4\ng,f,5\ne,z,6'
pd.read_csv(StringIO(data))
跳过某些行
我们还可以跳过某些行。假设我们只想要索引是 3 的倍数的行:
from io import StringIO
data = 'col1,col2,col3\na,b,1\na,b,2\nc,d,3\nc,e,4\ng,f,5\ne,z,6'
pd.read_csv(StringIO(data),skiprows=lambda x: x % 3 != 0)
我们得到以下输出:

演示如何在read_csv中使用skiprows参数。右侧面板显示了通过skiprows筛选的数据(仅保留行号是 3 的倍数的行)
左侧图示显示了结果 DataFrame,没有跳过任何行,而右侧图示显示了过滤掉行索引不是 3 的倍数的同一 DataFrame。注意,这种方法是根据实际索引(从 1 开始的第 3 行和第 6 行)来过滤行,而不是根据 Python 索引(从 0 开始)。
行索引
如果文件的列数比列名的数量多一列数据,第一列将作为 DataFrame 的行名:
data = 'a,b,c\n4,apple,bat,5.7\n8,orange,cow,10'
pd.read_csv(StringIO(data), index_col=0)
我们得到以下输出:

具有值但没有对应列名的列被用作行索引。
阅读文本文件
read_csv也可以帮助读取文本文件。通常,数据存储在具有不同分隔符的.txt文件中。sep参数可用于指定特定文件的分隔符,如以下代码所示:
data=pd.read_csv('Tab Customer Churn Model.txt',sep='/t')
上述文件的分隔符为Tab,通过sep参数指定。
阅读时的子集化
在读取时,可以使用usecols参数仅选择部分列进行子集化和加载:
data=pd.read_csv('Tab Customer Churn Model.txt',sep='/t',usecols=[1,3,5])
data=pd.read_csv('Tab Customer Churn Model.txt',sep='/t',usecols=['VMail Plan','Area Code'])
数字列表以及带有列名的显式列表都可以使用。数字索引遵循 Python 索引规则,即从 0 开始。
将千位格式的数字作为数字读取
如果数据集中包含一个数字列,该列的千位数字以逗号或其他分隔符格式化,那么该列的默认数据类型将是字符串或对象。问题是它实际上是一个数字字段,需要作为数字字段进行读取,以便进一步使用:
pd.read_csv('tmp.txt',sep='|')
我们得到以下输出:

带有千位格式数字的列数据
data.level.dtype returns dtype('O')
为了克服这个问题,可以在读取时使用thousands参数:
pd.read_csv('tmp.txt',sep='|',thousands=',')
data.level.dtype now returns dtype('int64')
索引和多重索引
index_col可用于指定某一列作为行索引。可以传递列的列表作为索引,这将导致多重索引。我们来看一个例子:
pd.read_csv('mindex.txt')
pd.read_csv('mindex.txt',index_col=[0,1])
请看下面的截图:

单一索引(左)和多重索引(右)在同一数据上的应用
这种多重索引使得基于某一索引或两个索引进行子集化变得容易:
data.loc[1977]
data.loc[(1977,'A')]
我们得到以下输出:

使用单一索引(左)和两个索引(右)对子集化多重索引数据
分块读取大文件
一次性将大文件读取到内存中可能会占用整个计算机的内存,并可能导致错误。在这种情况下,按块划分数据变得尤为重要。这些数据块可以按顺序读取并处理。通过使用chunksize参数,在read_csv中实现了这一点。
结果的块可以通过 for 循环进行迭代。在以下代码中,我们正在打印块的形状:
for chunks in pd.read_csv('Chunk.txt',chunksize=500):
print(chunks.shape)
然后,这些块可以使用 concat 方法连接在一起:
data=pd.read_csv('Chunk.txt',chunksize=500)
data=pd.concat(data,ignore_index=True)
print(data.shape)
处理列数据中的分隔符字符
有时,列分隔符字符作为数据的一部分出现在某一列中。这会导致数据解析错误,因为它会将本应作为一个读取的列分割成两个。为了避免这种情况,应该在指定的列数据周围使用引号字符。这个引号字符强制 read_csv 忽略数据中引号字符内的分隔符,并且不将其分成两部分。
可以通过 read_csv 的 quotechar 参数指定引号字符。例如,考虑以下数据集。在这里,使用空格作为分隔符,并使用双引号作为分组元素:

使用 quotechar 关键字 1——输入数据集
要解析此数据,我们可以使用以下代码:
d1 =
pd.read_csv('t1.txt',index_col=0, delim_whitespace=True,quotechar="\"")
d1.head()
我们将得到以下输出:

使用 quotechar 关键字 2——输出数据集
写入 CSV
DataFrame 是内存中的对象。通常,DataFrame 需要保存为物理文件以便后续使用。在这种情况下,DataFrame 可以写入为 CSV 或 TXT 文件。
让我们使用随机数创建一个合成的 DataFrame:
import numpy as np
import pandas as pd
a=['Male','Female']
b=['Rich','Poor','Middle Class']
gender=[]
seb=[]
for i in range(1,101):
gender.append(np.random.choice(a))
seb.append(np.random.choice(b))
height=30*np.random.randn(100)+155
weight=20*np.random.randn(100)+60
age=10*np.random.randn(100)+35
income=1500*np.random.randn(100)+15000
df=pd.DataFrame({'Gender':gender,'Height':height,'Weight':weight,'Age':age,'Income':income,'Socio-Eco':seb})
这可以通过 to_csv 方法写入 .csv 或 .txt 文件,如以下代码所示:
df.to_csv('data.csv')
df.to_csv('data.txt')
这些文件将被写入当前工作目录。
写入文件时可以提供自定义的分隔符:
df.to_csv('data.csv',sep='|')
df.to_csv('data.txt',sep='/')
还有许多其他有用的选项,如下所示:
-
index: True/False。指示是否需要行索引。 -
index_label: 字符串/列名。用作行索引的列。 -
header: True/False。指定是否写入列名。 -
na_rep: 字符串。缺失值的字符串表示。
Excel
Excel 文件与 CSV 文件相似,但不同之处在于它们可以包含多个工作表、格式化的数据和表格、图表以及公式。在许多情况下,需要从 Excel 文件中读取数据。
xlrd 是处理 Excel 工作表时首选的包。xlrd 包的一些主要功能总结在下表中:
| 代码片段 | 目标达成 |
|---|---|
import xlrd |
导入 xlrd 库 |
book=xlrd.open_workbook('SRS Career.xlsx') |
读取 Excel 工作簿 |
n=book.nsheets |
查找工作簿中的工作表数量 |
book.sheet_names() |
查找工作簿中工作表的名称 |
last_sheet=book.sheet_by_index(n-1) |
通过工作表索引读取工作表 |
last_sheet.row_values(0) |
获取工作表的第一行 |
last_sheet.cell(0,0) |
获取工作表的第一个单元格 |
last_sheet.row_slice(rowx=0,start_colx=1,end_colx=5) |
获取第一行的第 1 列到第 5 列 |
URL 和 S3
有时,数据可以直接通过 URL 获取。在这种情况下,可以直接使用 read_csv 从这些 URL 中读取数据:
pd.read_csv('http://bit.ly/2cLzoxH').head()
或者,为了通过 URL 获取数据,我们可以使用一些尚未使用的 Python 包,如 .csv 和 .urllib。只需知道 .csv 提供了一系列处理 .csv 文件的方法,而 urllib 用于访问和获取 URL 中的信息即可。以下是实现方法:
import csv
import urllib2
url='http://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
response=urllib2.urlopen(url)
cr=csv.reader(response)
for rows in cr:
print rows
AWS S3 是一个流行的文件共享和存储库,许多企业将其业务运营数据以文件形式存储在 S3 上,这些数据需要直接读取和处理,或者被移入数据库。Python 允许我们直接从 S3 读取文件,如下所示的代码所示:
Python 3.4 及以上版本在使用 pandas 之外,还需要使用 s3fs 包来直接从 S3 读取文件。AWS 配置文件需要放置在当前工作目录中。需要传入存储桶名称以及路径和文件名来进行读取:
import os
import pandas as pd
from s3fs.core import S3FileSystem
os.environ['AWS_CONFIG_FILE'] = 'aws_config.ini'
s3 = S3FileSystem(anon=False)
key = 'path\to\your-csv.csv'
bucket = 'your-bucket-name'
df = pd.read_csv(s3.open('{}/{}'.format(bucket, key),
mode='rb')
)
可以将 DataFrame 写入 CSV 文件并直接保存在 S3 中,如下所示:
import s3fs
bytes_to_write = df.to_csv(None).encode()
fs = s3fs.S3FileSystem(key=key, secret=secret)
with fs.open('s3://bucket/path/to/file.csv', 'wb') as f:
f.write(bytes_to_write)
HTML
HTML 是一种流行的文件格式,用于创建和包装网页元素和页面。有时,表格数据会存储在文件中。在这种情况下,可以直接使用 read_html 方法读取这些数据。此功能解析 HTML 文件中的表格元素,并将表格读取为 DataFrame:
pd.read_html('http://www.fdic.gov/bank/individual/failed/banklist.html')
你可以通过以下代码查找包含特定匹配词的所有表格元素:
match = 'Malta National Bank'
df_list = pd.read_html('http://www.fdic.gov/bank/individual/failed/banklist.html', match=match)
可以将 DataFrame 转换为 HTML 表格元素,以便将其嵌入 HTML 文件中,如下所示:
data=pd.read_csv('http://bit.ly/2cLzoxH')
print(data.to_html())
我们得到如下输出:

从 DataFrame 创建的 HTML 表格元素
可以选择性地筛选和转换某些列为 HTML,如下所示:
print(data.to_html(columns=['country','year']))
写入 HTML 文件
HTML 文件可以保存为一个物理文件,如下所示:
data=pd.read_csv('http://bit.ly/2cLzoxH')
print(data.to_html('test.html'))
看一下以下截图:

使用一个索引(左)和两个索引(右)对子集化多重索引数据
默认情况下,行和列的名称是粗体的。可以通过以下代码进行更改:
data.to_html('test.html',bold_rows=False)
JSON
JSON 是一种流行的字典型、基于键值对的数据结构,适用于从 SaaS 工具暴露数据为 API。address、postalCode、state、streetAddress、age、firstName、lastName 和 phoneNumber 是键,它们的值显示在它们的右侧。JSON 文件也可以是嵌套的(键的值本身也是 JSON)。在这里,address 有嵌套值:

JSON 数据示例(字典;键值对)
DataFrame 可以使用 to_json 转换为 JSON 格式:
import numpy as np
pd.DataFrame(np.random.randn(5, 2), columns=list('AB')).to_json() "
看一下以下截图:

将 DataFrame 转换为 JSON 格式
在将 DataFrame 转换为 JSON 文件时,可以设置方向。
如果我们想保持列名作为主要索引,行索引作为次要索引,那么我们可以选择将方向设置为 columns:
dfjo = pd.DataFrame(dict(A=range(1, 4), B=range(4, 7), C=range(7, 10)),columns=list('ABC'), index=list('xyz'))
dfjo.to_json(orient="columns")
我们得到以下输出:

将一个 DataFrame 转换为具有列方向的 JSON
如果我们想保持行索引作为主要索引,列名作为次要索引,那么我们可以选择将方向设置为 index:
dfjo = pd.DataFrame(dict(A=range(1, 4), B=range(4, 7), C=range(7, 10)),columns=list('ABC'), index=list('xyz'))
dfjo.to_json(orient="index")
我们得到以下输出:

将一个 DataFrame 转换为具有索引方向的 JSON
另一种选择是将一个 DataFrame 转换为一个 JSON 数组。这在将数据传递给可视化库时非常有用,如下所示:
d3.js. dfjo = pd.DataFrame(dict(A=range(1, 4), B=range(4, 7), C=range(7, 10)),columns=list('ABC'), index=list('xyz')) dfjo.to_json(orient="records")
我们得到以下输出:

将一个 DataFrame 转换为具有记录方向的 JSON
我们还可以将纯值作为值的列表包含,而不需要任何行或列索引:
dfjo = pd.DataFrame(dict(A=range(1, 4), B=range(4, 7), C=range(7, 10)),columns=list('ABC'), index=list('xyz')) dfjo.to_json(orient="values")
我们得到以下输出:

将一个 DataFrame 转换为具有值方向的 JSON
最后,我们还可以调整转换后的 JSON,以便分离行索引、列名和数据值:
dfjo = pd.DataFrame(dict(A=range(1, 4), B=range(4, 7), C=range(7, 10)),columns=list('ABC'), index=list('xyz')) dfjo.to_json(orient="split")
我们得到以下输出:

将一个 DataFrame 转换为具有拆分方向的 JSON
将 JSON 写入文件
JSON 可以像这样写入物理文件:
import json with open('jsonex.txt','w') as outfile: json.dump(dfjo.to_json(orient="columns"), outfile)
读取 JSON
json_loads 用于读取包含 JSON 的物理文件:
f=open('usagov_bitly.txt','r').readline() json.loads(f)
我们得到以下输出:

JSON 列表中的第一个记录
文件可以通过 open 和 readline 方法一次读取一个 JSON:
records=[] f=open('usagov_bitly.txt','r') for i in range(1000): fiterline=f.readline() d=json.loads(fiterline) records.append(d) f.close()
现在,records 包含一个 JSON 列表,我们可以从中提取出某个特定键的所有值。例如,在这里,我们提取出所有具有非零值的 latlong('ll' 列):
latlong=[rec['ll'] for rec in records if 'll' in rec]
将 JSON 写入 DataFrame
一个 JSON 对象列表可以转换为一个 DataFrame(就像字典一样)。我们之前创建的 records 元素是一个 JSON 列表(我们可以通过使用 records[0:3] 或 type(records) 来检查):
df=pd.DataFrame(records)
df.head()
df['tz'].value_counts()
在最后一行,我们尝试查找 'tz' 列中包含的不同时区的数量。
对 JSON 进行子集筛选
让我们来看看一个新的 JSON 文件:
with open('product.json') as json_string:
d=json.load(json_string)
d
我们得到以下输出:

加载一个具有多个嵌套层级的 JSON 文件
这是一个具有多个嵌套层级的 JSON。hits 键包含一个值为 JSON 的键值对,其中该 JSON 的键值为 hits,该 JSON 的值是一个包含另一个 JSON 的列表。
假设我们想从这个 JSON 中找出分数值:
d['hits']['hits'][0]['_score']
同样,图像 URL 可以如下找到:
d['hits']['hits'][0]['_source']['r']['wayfair']['image_url']
遍历 JSON 键
可以遍历 JSON 数据的键和值:
for keys,values in d['hits']['hits'][0].items():
print(keys)
我们得到以下输出:

通过遍历键和值来打印加载的 JSON 的键
我们也可以将键和值一起打印出来。
for keys,values in d['hits']['hits'][0].items():
print(keys,values)
我们接收到以下输出:

通过遍历键和值来打印加载的 JSON 的键和值
现在,我们将学习如何使用读写操作处理不同的文件格式。
读取 HDF 格式
层次数据格式(HDF)在处理大规模和复杂数据模型时非常高效。HDF 在数据存储中的多功能性和灵活性使其成为存储科学数据的热门格式。事实上,HDF 被 NASA 选为地球观测系统中的标准数据与信息系统。HDF5 是当前由 HDF 文件格式使用的技术套件,并取代了较旧的 HDF4。
以下是 HDF5 的一些独特功能:
-
HDF5 对文件大小和文件中的对象没有设定限制。
-
HDF5 可以对文件中的对象进行分组和链接,从而为数据中的复杂关系和依赖提供支持机制。
-
HDF5 还支持元数据。
-
HDF5 在支持多种预定义和用户定义的数据类型的同时,还能够在 HDF 文件中存储和共享数据类型描述。
为了提高数据传输过程的效率,HDF5 集成了标准(Posix)、并行和网络 I/O 文件驱动程序。还可以开发并将其他文件驱动程序与 HDF5 集成,以满足任何自定义的数据传输和存储需求。HDF5 通过压缩、可扩展性和分块等技术优化了数据存储。能够在数据传输过程中执行数据转换、修改数据类型以及选择数据子集,使得读写过程更加高效。
现在,让我们使用 pandas 读取一个 HDF 文件:
pd.read_hdf('stat_df.h5','table')
我们接收到以下输出:

read_hdf 的输出
在读取过程中,可以通过索引参数提取数据的一个子集:
pd.read_hdf('stat_df.h5', 'table', where=['index>=2'])
我们接收到以下输出:

带索引的 read_hdf 输出
读取 Feather 文件
Feather 格式是一种二进制文件格式,用于存储数据,采用 Apache Arrow 作为内存中的列式数据结构。它由 RStudio 的首席科学家 Wes Mckinney 和 Hadley Wickham 开发,旨在为 Python 和 R 之间的数据共享基础设施提供支持。Feather 文件中数据的列式序列化使得读写操作更加高效,比存储按记录方式排列的 CSV 和 JSON 文件要快得多。
Feather 文件具有以下特点:
-
快速的输入/输出操作。
-
Feather 文件可以在除 R 或 Python 之外的其他语言中进行读写,例如 Julia 和 Scala。
-
它们与所有 pandas 数据类型兼容,例如 Datetime 和 Categorical。
Feather 当前支持以下数据类型:
-
所有数值数据类型
-
逻辑数据
-
时间戳
-
分类数据
-
UTF-8 编码的字符串
-
二进制
由于 feather 只是 Arrow 的简化版,它有一些相关的限制。以下是使用 feather 文件的一些限制:
-
不推荐用于长期数据存储,因为它们在版本之间的稳定性无法保证。
-
Feather 格式不支持任何非默认索引或多重索引。
-
Python 数据类型如 Period 不被支持。
-
不支持列名重复。
在 pandas 中读取 feather 文件的方法如下:
pd.read_feather("sample.feather")
这将产生以下输出:

read_feather 输出
读取 parquet 文件
Apache Parquet 是另一种文件格式,利用列式压缩进行高效的读写操作。它设计上与 Hadoop 等大数据生态系统兼容,并能处理嵌套数据结构和稀疏列。虽然 parquet 和 feather 格式有相似的基础,但 parquet 的压缩方法比 feather 更优。压缩后的文件在 parquet 中比在 feather 中小。具有相似数据类型的列使用相同的编码进行压缩。不同编码方案的使用使得 parquet 的压缩效率更高。像 feather 一样,parquet 是一种二进制文件格式,可以与所有 pandas 数据类型良好配合,并在多个语言中得到支持。Parquet 可以用于长期数据存储。
以下是 parquet 文件格式的一些限制:
-
虽然 parquet 可以接受多级索引,但它要求索引级别名称必须是字符串格式。
-
Python 数据类型如 Period 不被支持。
-
不支持列名重复。
-
当分类对象序列化到 parquet 文件时,它们会作为对象数据类型进行反序列化。
pandas 中的 parquet 文件的序列化或反序列化可以通过 pyarrow 和 fastparquet 引擎完成。这两个引擎有不同的依赖关系。Pyarrow 不支持 Timedelta。
让我们使用 pyarrow 引擎读取一个 parquet 文件:
pd.read_parquet("sample.paraquet",engine='pyarrow')
这将产生以下输出:

read_parquet 输出
Parquet 允许我们在读取文件时选择列,这可以节省时间:
pd.read_parquet("sample.paraquet",engine='pyarrow',columns=["First_Name","Score"])
fastparquet 引擎也适用相同的方法:
pd.read_parquet("sample.paraquet",engine='fastparquet')
读取 SQL 文件
通过 pandas 与 SQL 数据库交互需要安装 sqlalchemy 依赖。
首先,让我们定义获取连接参数的引擎:
engine = create_engine('sqlite:///:memory:')
现在,让我们从 SQL 数据库中读取 data_sql 表:
with engine.connect() as conn, conn.begin():
print(pd.read_sql_table('data_sql', conn))
这将产生以下输出:

read_sql_table 输出
read_sql_table() 函数用于读取给定表名的整个表。读取时,可以将特定的列设置为索引:
pd.read_sql_table('data_sql', engine, index_col='index')
这将产生以下输出:

使用索引的 read_sql_table 输出
columns 参数允许我们在读取数据时选择特定的列,通过传递列名列表来实现。任何日期列都可以在读取过程中解析为特定格式,如下代码所示:
pd.read_sql_table('data_sql', engine, parse_dates={'Entry_date': '%Y-%m-*%d*'})
pd.read_sql_table('data_sql', engine, parse_dates={'Entry_date': {'format': '%Y-%m-*%d* %H:%M:%S'}})
此函数中的 schema 参数有助于指定要从中提取表格的模式。
除了读取整个表格外,还可以使用 SQL 查询以所需格式获取数据。我们可以通过 read_sql_query() 函数来实现:
pd.read_sql_query("SELECT Last_name, Score FROM data_sql", engine)
这将导致以下输出:

read_sql_query 的输出
要执行 INSERT 和 CREATE 查询,这些查询不会返回任何输出,可以使用 sql.execute() 函数。这需要导入一个 pandas.io 的 sql 文件:
from pandas.io import sql
sql.execute("INSERT INTO tablename VALUES (90, 100, 171)", engine)
使用 sqlite 数据库时,必须按照如下方式定义与引擎的连接,以便可以在 read_sql_table() 或 read_sql_query() 函数中使用。必须先导入 sqlite 模块:
import sqlite3
conn = sqlite3.connect(':memory:')
读取 SAS/Stata 文件
Pandas 可以读取来自 SAS 的两种文件格式——SAS xports(.XPT)和 SAS 数据文件(.sas7bdat)。
read_sas() 函数帮助读取 SAS 文件。这里已读取一个 SAS 数据文件,并作为 pandas 数据框显示:
df = pd.read_sas('sample.sas7bdat')
df
这将导致以下输出:

read_sas 的输出
chunksize 和 iterator 参数有助于按相同大小的组读取 SAS 文件。如果之前使用的 SAS 数据文件以 chunksize 10 进行读取,则 51 条记录将被分为六组,如下代码所示:
rdr = pd.read_sas('sample.sas7bdat', chunksize=10)
for chunk in rdr:
print(chunk.shape)
看一下以下输出:

使用 chunksize 的 read_sas 输出
然而,这些 SAS 文件无法使用 pandas 写入。
Pandas 还支持读取和写入从 Stata 生成的文件。Stata 只支持有限的数据类型:int8、int16、int32、float32、float64,以及长度小于 244 的字符串。在通过 pandas 写入 Stata 数据文件时,会在适当的地方应用类型转换。
让我们使用 pandas 读取一个 Stata 数据文件:
df = pd.read_stata('sample.dta')
df
看一下以下输出:

read_stata 的输出
read_stata() 函数还具有 chunksize 和 iterator 参数,用于按较小的组读取数据。以下是可用的 stata 读取函数参数:
-
convert_categoricals:将合适的列转换为分类数据类型 -
index_col:指定要定义为索引的列 -
convert_missing:指定是否将缺失值表示为 NaN 或 Stata 的缺失值对象 -
columns:要从数据集中选择的列
从 Google BigQuery 读取数据
BigQuery 是 Google 提供的一个极其强大的数据仓储解决方案。Pandas 可以直接连接到 BigQuery,并将数据带入 Python 环境进行进一步分析。
以下是从 BigQuery 读取数据集的示例:
pd.read_gbq("SELECT urban_area_code, geo_code, name, area_type, area_land_meters
FROM `bigquery-public-data.utility_us.us_cities_area` LIMIT 5", project_id, dialect = "standard")
看一下以下输出:

read_gbq 的输出
read_gbq() 函数接受查询和 Google Cloud 项目 ID(作为密钥)以便访问数据库并提取数据。dialect 参数处理 SQL 语法的使用:BigQuery 的旧版 SQL 方言或标准 SQL 方言。此外,还有允许设置索引列(index_col)、重新排序列(col_order)以及启用重新认证(reauth)的参数。
从剪贴板读取数据
这是 pandas 中一个非常有趣的功能。任何已经复制到剪贴板的表格数据都可以作为 DataFrame 在 pandas 中读取。
我们可以使用常规的 ctrl + C 键盘命令复制以下表格数据:
| 性别 | 入职日期 | 标记 | |
|---|---|---|---|
| A | M | 2012-01-19 | True |
| B | F | 2012-12-30 | False |
| C | M | 2012-05-05 | False |
调用 read_clipboard() 函数会将剪贴板中的数据转换为 pandas DataFrame:
pd.read_clipboard()
看一下以下输出:

read_clipboard 的输出
该函数默认也会将 Flag 列识别为布尔类型,并将未命名的列作为索引:

从剪贴板读取后的数据类型
管理稀疏数据
稀疏数据指的是像数组、系列、DataFrame 和面板等数据结构,其中有很大一部分数据缺失或为 NaN。
我们来创建一个稀疏的 DataFrame:
df = pd.DataFrame(np.random.randn(100, 3))
df.iloc[:95] = np.nan
该 DataFrame 中 95% 的记录包含 NaN。可以通过以下代码估算该数据的内存使用情况:
df.memory_usage()
看一下以下输出:

含有 95% NaN 的 DataFrame 的内存使用情况
如我们所见,每个元素消耗 8 字节的内存,无论它是实际数据还是 NaN。Pandas 提供了一种内存高效的解决方案来处理稀疏数据,如下代码所示:
sparse_df = df.to_sparse()
sparse_df.memory_usage()
看一下以下输出:

稀疏数据的内存使用情况
现在,内存使用量已经减少,NaN 不再分配内存。这也可以通过定义一个 fill_value 来代替 NaN 实现:
df.fillna(0).to_sparse(fill_value = 0)
df.fillna(0).to_sparse(fill_value = 0).memory_usage()
看一下以下输出:

填充数据后的稀疏数据内存使用情况
稀疏数据也可以转换回原始的密集形式,如下代码所示:
sparse_df.to_dense()
这种处理稀疏数据的方式也可以类似地应用于系列、面板和数组。
将 JSON 对象写入文件
to_json() 函数允许将任何 DataFrame 对象转换为 JSON 字符串,或者如果指定文件路径,则写入 JSON 文件:
df = pd.DataFrame({"Col1": [1, 2, 3, 4, 5], "Col2": ["A", "B", "B", "A", "C"], "Col3": [True, False, False, True, True]})
df.to_json()
看一下以下输出:

JSON 输出
JSON 数据的方向可以被更改。to_json() 函数具有一个 orient 参数,可以设置为以下模式:columns、index、record、value 和 split。默认的方向设置是列(columns):
df.to_json(orient="columns")
请看以下输出:

JSON 输出 – 列方向
沿索引方向的设置就像是前一个情况的转置,JSON 字典中的行和列索引被反转:
df.to_json(orient="index")
请看以下输出:

JSON 输出 – 索引方向
将 orient 设置为记录(records)会创建一个 JSON 结构,其中原 DataFrame 中的每个记录或行保留其结构形式:
df.to_json(orient="records")
请看以下输出:

JSON 输出 – 记录方向
当 orient 选项设置为值(values)时,行索引和列索引都会从图像中消失:
df.to_json(orient="values")
请看以下输出:

JSON 输出 – 值方向
拆分方向定义了一个由实体如列、索引和数据组成的 JSON:
df.to_json(orient="split")
请看以下输出:

JSON 输出 – 拆分方向
将 orient 设置为表格会展示出如架构和字段等方面:
df.to_json(orient="table")
请看以下输出:

JSON 输出 – 表格方向
to_json() 的 date_format 参数允许将 DataFrame 中的时间戳转换为 epoch 格式或 iso 格式。
对于像 complex 这样的不支持的数据类型,可以通过 default_handler 参数指定要执行的类型转换来处理。
序列化/反序列化
序列化是将 数据结构 或 对象 的状态转换为一种可以存储(例如,在 文件 或内存 缓冲区 中)或通过 网络 连接传输的格式,并在之后重新构建(可能在不同的计算环境中)。[1] 当根据序列化格式重新读取该位序列时,它可以用来创建一个语义上与原始对象完全相同的克隆。
数据结构如 JSON、数组、DataFrame 和 Series 有时需要作为物理文件存储或通过网络传输。这些序列化可以理解为数据的转储,数据可以以任何格式(文本、CSV 等)或结构存储,但所有重要的数据点可以通过加载/反序列化恢复。
这些的例子包括存储统计模型的训练模型对象的参数。这个包含训练参数的序列化文件可以被加载,并且测试数据可以通过它进行预测。这是将统计模型投入使用的一种流行方法。
序列化数据格式的其他用途包括通过网络传输数据、在数据库或硬盘中存储对象、进行远程过程调用,以及检测时间变化数据的变化。
让我们创建一个示例 DataFrame,来理解 Pandas 支持的各种文件格式的序列化:
df = pd.DataFrame({"First_Name":["Mike","Val","George","Chris","Benjamin"],
"Last_name":["K.","K.","C.","B.","A."],
"Entry_date":pd.to_datetime(["June 23,1989","June 16,1995","June 20,1997","June 25,2005","March 25,2016"],format= "%B %d,%Y"),
"Score":np.random.random(5)})
df
看一下以下输出:

用于序列化的 DataFrame
写入特殊文件类型
存储数据结构或对象的格式有多种。让我们来看看其中一些格式。
to_pickle()
当一个 Python 对象被 pickle 时,它会被保存到磁盘。Pickling 首先序列化对象,然后写入磁盘。它包括将列表、字典、DataFrame 和训练的机器学习模型等对象转换为字符流。
让我们将之前定义的 DataFrame 转换成 pickle 格式:
df.to_pickle('pickle_filename.pkl')
还可以在写入之前压缩 pickle 文件。支持gzip、bz2和xz等压缩方案:
df.to_pickle("pickle_filename.compress", compression="gzip")
默认情况下,压缩类型是根据提供的扩展名推断的:
df.to_pickle("pickle_filename.gz")
read_pickle()函数将反序列化pickle文件。压缩仅支持读取单个文件,不支持写入。
to_parquet()
正如我们在读取 parquet 文件部分中讨论的那样,也可以使用两种引擎进行反序列化:
df.to_parquet('sample_pyarrow.parquet', engine='pyarrow')
df.to_parquet('sample_fastparquet.parquet', engine='fastparquet')
to_hdf()
HDF 文件像字典一样,可以存储多个对象。to_hdf()函数将 Pandas 对象转换为 HDF 文件:
df.to_hdf('store.h5', append = True, format='table')
当一行的所有列都是 NaN 时,它们不会自动删除。可以通过将dropna参数设置为True来在写入 HDF 时删除这些行。
to_sql()
通过sqlalchemy包的支持,数据可以通过 pandas 传输到数据库:
from sqlalchemy import create_engine
engine = create_engine('sqlite:///:memory:')
df.to_sql('data_sql',engine)
还可以通过使用chunksize参数迭代地批量推送数据。
在将数据推送到数据库时,任何列的数据类型也可以更改,如以下代码所示:
from sqlalchemy.types import String
df.to_sql('data_dtype', engine, dtype={'Score': String})
Timedelta数据类型(在数据库中不支持)会在存储到数据库之前,转换为等效的纳秒整数值。
to_feather()
将 pandas 对象序列化为 feather 格式只需要调用to_feather()函数:
df.to_feather('sample.feather')
to_html()
to_html()函数将 DataFrame 转换为原始 HTML 格式:
df.to_html()
这将产生以下输出:

HTML 格式的 DataFrame
to_html()函数中有一大堆选项,可以丰富原始的 HTML。通过使用columns和escape参数,可以选择列并控制转义序列。
to_msgpack()
Msgpack 提供快速高效的二进制序列化。
单个对象可以像这样直接转换为msgpack格式:
df.to_msgpack("sample.msg")
如果我们有多个对象,它们可以像这样序列化为一个msgpack文件:
arr = np.random.randint(1,10,7)
lst = [10,20,40,60,60]
strg = "Data"
pd.to_msgpack("msg_all.msg",df,arr,lst,strg)
to_latex()
to_latex()函数将 DataFrame 转换为兼容latex文档的美观表格结构:
df.to_latex()
看一下以下输出:

LaTeX 格式的 DataFrame
to_stata()
Pandas 可以帮助创建带有.dta扩展名的stata数据文件,如下代码所示:
df.to_stata('stata_df.dta')
to_clipboard()
to_clipboard()函数将 DataFrame 从 Python 环境转移到剪贴板。然后可以通过ctrl + V键盘命令将对象粘贴到其他地方:
df.to_clipboard()
这个 DataFrame 也可以以更兼容 CSV 的格式复制到剪贴板,如下所示:
df.to_clipboard(excel=True,sep=",")
GeoPandas
GeoPandas 是一个基于 pandas 的 Python 包,用于处理地理空间数据。它旨在与现有工具兼容,如桌面 GIS、地理空间数据库、网页地图和 Python 数据工具。
GeoPandas 允许你轻松地在 Python 中执行本来需要空间数据库(如 PostGIS)才能完成的操作。
什么是地理空间数据?
空间数据、地理空间数据、GIS 数据和地理数据是指通过地理坐标系统标识物理对象(如建筑物、街道、城镇、城市、国家等)的地理位置的数值数据。 除了地理位置,地理空间数据通常还存储每个位置的社会经济数据、交易数据等。
安装与依赖
可以通过 pip 或 Anaconda,或直接通过 GitHub 安装 GeoPandas。最常见的安装方法是通过pip和 Anaconda 在终端窗口中进行:
pip install geopandas
conda install -c conda-forge geopandas
GeoPandas 依赖于以下 Python 库:
-
pandas -
numpy -
shapely -
fiona -
pyproj -
six -
rtree
使用 GeoPandas
我们可以使用 GeoPandas 库通过geopandas.read_file函数读取许多 GIS 文件格式(依赖于fiona库,这是 GDAL/OGR 的接口)。
数据也可以通过 shapefiles 读取。在本节中,我们将通过一个示例演示如何使用 GeoPandas。我们将解释如何读取包含地理空间数据的 shapefile,对其进行聚合、排序,最后绘制所需的 Geo DataFrame。
使用以下代码调用所需的先决库:
import pandas as pd
import geopandas
import matplotlib as plt
使用以下代码读取包含地理空间信息数据的 shapefile:
countries = geopandas.read_file("ne_110m_admin_0_countries.shp")
使用以下代码访问数据集的前五行,就像我们使用 pandas 一样:
countries.head(5)
前面的代码片段将产生以下输出:

作为 DataFrame 读取的地理空间 shapefile
让我们快速绘制数据的基本可视化:
countries.plot()
这将产生以下输出:

在地图上绘制的形状文件中的国家
要检查我们地理空间数据的类型,可以使用以下代码:
type(countries)
这将产生以下输出:

在转换后,断言国家形状文件的数据类型是 GeoDataFrame
在这里,我们可以看到该 DataFrame 是一个 GeoDataFrame。现在,让我们讨论一下 GeoDataFrame 是什么。
GeoDataFrames
GeoDataFrame 包含一个地理空间数据集。它就像一个 pandas DataFrame,但具有一些额外的功能,用于处理地理空间数据。这些附加功能如下:
-
.geometry属性始终返回包含几何信息的列(返回一个 GeoSeries)。列名本身不一定需要是.geometry,但它将始终可以作为.geometry属性访问。 -
它有一些额外的方法来处理空间数据(如面积、距离、缓冲区、交集等),这些方法我们将在后面的章节中逐一探讨。
GeoDataFrame 仍然是一个 DataFrame,因此我们可以在 GeoDataFrame 中执行 DataFrame 中所有可用的功能,比如聚合、排序、过滤等。
使用以下代码执行 GeoPandas 的简单聚合:
countries['POP_EST'].mean()
POP_EST 是 countries GeoDataFrame 中的一列,数据类型为数字。 这将产生以下输出:

对 GeoDataFrame 中的数字列进行聚合,证明它的工作方式与普通 DataFrame 完全相同
或者,我们可以使用布尔过滤来基于条件选择 DataFrame 的子集:
africa = countries[countries['CONTINENT'] == 'Africa']
现在,我们将尝试使用 plot() 函数绘制过滤后的 GeoDataFrame:
africa.plot()
这将产生以下输出:

对一个大洲进行子集化并绘制以提高可视化效果
GeoPandas 还可以帮助将普通 DataFrame 转换为 GeoDataFrame,前提是你拥有 Latitude 和 Longitude 坐标。让我们来看一下这个例子。
假设我们有一个简单的 DataFrame,像这样:
df = pd.DataFrame(
{'City': ['Buenos Aires', 'Brasilia', 'Santiago', 'Bogota', 'Caracas'],
'Country': ['Argentina', 'Brazil', 'Chile', 'Colombia', 'Venezuela'],
'Latitude': [-34.58, -15.78, -33.45, 4.60, 10.48],
'Longitude': [-58.66, -47.91, -70.66, -74.08, -66.86]})
上面的代码将产生以下输出:

创建一个包含国家首都及其经纬度的普通 DataFrame
让我们添加一个新列,命名为 'Coordinates',它将经纬度列连接起来:
df['Coordinates'] = list(zip(df.Longitude, df.Latitude))
这将产生以下输出:

一个将纬度和经度合并在一列中的普通 DataFrame
使用 shapely 包中的 Point 函数,我们可以正确地将这些标识为位置坐标或点元组参数,它们是 GeoDataFrame 的骨架。
from shapely.geometry import Point
df['Coordinates'] = df['Coordinates'].apply(Point)
这将产生以下输出:

将压缩的经度和纬度转换为点,以便 GeoPandas 可以使用它将其转换为 GeoDataFrame
一切准备就绪后,让我们将其转换为 GeoDataFrame:
gdf = geopandas.GeoDataFrame(df, geometry='Coordinates')
让我们打印出gdf类型,看看它的 GeoDataFrame 类型:
type(gdf)
这将产生以下输出:

确认新创建的 DataFrame 的类型是 GeoDataFrame
这为我们提供了关于 GeoPandas 的基本概念,以及它是如何工作的。它的应用范围广泛,你可以跨越其各种功能并从中获益。
开源 API – Quandl
Python 可以用来从开源和商业 API 获取数据。我们可以用它获取多种格式的数据。有些数据以 JSON 格式输出,有些以 XML 格式输出,还有一些以表格格式,如 CSV 和 DataFrame 输出。数据转换为 DataFrame 后,通常会在 pandas 中进行处理。
在本节中,我们将展示一个从 Quandl API 获取数据的示例,Quandl 是一个开源 API,包含各种主题的数据,如金融、经济和替代数据。你可以在这里查看这个著名的数据仓库:www.quandl.com/。
api密钥是一种应用程序接口,充当开发者或任何希望通过计算机代码访问网站数据的用户之间的中介。api密钥是一段代码,用于识别用户及其关联的账户。
要开始这个示例,你需要注册一个免费的 Quandl 账号。注册后,你可以在账户设置选项中找到 API 密钥,该选项会显示在个人资料图片的下拉菜单中。
Python 通过提供一个可以与最新版本的 Quandl 数据仓库交互的包,使得使用 Quandl API 变得更加容易。这个包兼容 Python 2.7 及以上版本。
首先,你需要通过pip或conda安装 Quandl 包,使用以下命令:
pip install quandl
conda install quandl
你可以获取任何你希望使用的数据集。在这里,我使用的是 2022 年 7 月的巴西雷亚尔期货数据集作为示例。你需要找到你想要下载的数据集的代码。这个代码可以从 Quandl 网站获取,如下图所示:

在 Quandl 网站上查找数据集的数据代码
现在,让我们来看一下如何使用 Quandl API 来获取我们想要的数据:
# import quandl into your code
import quandl
# setting your api key
quandl.ApiConfig.api_key = "[YOUR API KEY]"
# BCCME/L6N2022 is the code for the dataset and you can see it on the right below the table
data = quandl.get("BCCME/L6N2022", start_date="2015-12-31", end_date="2019-06-23")
data.head()
这将产生以下输出:

通过 Quandl API 获取的巴西雷亚尔期货数据
API 还可以用来获取数据的子集,而不是一次性获取所有数据。例如,在这里,我们筛选了某个日期范围内的数据:
transform = quandl.get("BCCME/L6N2022", start_date="2015-12-31", end_date="2019-06-23",transformation='diff')
transform.head()
这将产生以下输出:

通过 Quandl API 获取的巴西雷亚尔期货数据,带有日期范围过滤
一旦数据被读取,pandas 可以用来对数据进行所有转换,这将对进一步的分析非常有帮助。
数据集也可以通过提供数据集的网址来下载。通过下载一个文件列出可用的数据集,您可以检查这一点。让我们尝试通过 Python 的urllib包来下载一个文件,而不是按照Quandl包的方法。
要获取可以在此方法中使用的网址,请按照以下步骤操作:
- 点击数据集的标题/链接(标记在红色框中):

数据主题链接以获取更多关于该主题的详细信息
- 点击链接将带您到下一个页面,您将在该页面看到这些选项。在“用法”选项下选择 API,如下图所示:

数据主题文档
- 在此选择之后,您应该稍微向下滚动一下,找到以下网址,可以在代码中使用它们来获取数据:

从数据主题文档中获取的 Quandl 数据的 API 链接
- 由于一个主题可能包含多个数据集,主题会作为
.zip文件下载,文件中包含所有数据集。它提供了一个元数据表,并且列出了每个数据集的详细信息(包括数据集键):
import urllib.request
print('Beginning file download with urllib...')
url = 'https://www.quandl.com/api/v3/databases/EOD/metadata?api_key=[YOUR API KEY]'
urllib.request.urlretrieve(url,'file location.zip')
# We will read the zip file contents through the zipfile package.
import zipfile
archive = zipfile.ZipFile('[Name of the zip file].zip', 'r')
# lists the contents of the zip file
archive.namelist()
['EOD_metadata.csv']
df = pd.read_csv(archive.open('EOD_metadata.csv'))
df.head()
这会产生以下输出:

下载的数据主题的元数据表输出
read_sql_query
Python 支持很多数据库操作,使用如psycopg2和sqlalchemy等库。它们在从 Python 接口操作数据库时非常全面和有用。然而,它们有自己的繁琐内容,有时会对简单的查询任务产生过多的信息。幸运的是,pandas 中有一个隐藏的宝石,那就是read_sql_query方法。它的功能如下:
-
运行包含 select、where 等的简单查询。
-
运行所有返回表格或其子集的查询,并以表格形式展示。
-
无法使用 INSERT、UPDATE 和 DELETE 语句。
-
输出是一个数据框,因此可以使用所有 pandas 方法进行进一步的数据处理。
让我们看看如何利用这个方法。为了说明这一点,我们将把一个数据集作为表格插入到数据库中。为此,您需要在本地目录中安装 PostgreSQL 或 SQL 数据库。如果您已经设置好了数据库,可以跳过表格创建过程,直接跳到查询过程。
让我们从 Kaggle 下载 2019 年世界幸福感数据集,将其推送到db,并对其进行各种 DB 和 pandas 操作:
import pandas as pd
df = pd.read_csv('F:/world-happiness-report-2019.csv')
df.head()
以下数据展示了世界幸福报告作为一个数据框:

世界幸福报告作为一个数据框
由于我们将直接从之前生成的 DataFrame 创建一个表,因此需要将列名更改为 postgresql,因为它不支持带空格的列名:
# rename Country (region) to region
df= df.rename(columns={'Country (region)':'region'})
# push the dataframe to postgresql using sqlalchemy
# Syntax:engine = db.create_engine('dialect+driver://user:pass@host:port/db')
from sqlalchemy import create_engine
engine = create_engine('postgresql://postgres:1128@127.0.0.1:5433/postgres')
df.to_sql('happy', engine,index=False)
为了使用read_sql_query方法,我们需要通过psycopg2或sqlalchemy与数据库建立连接。连接建立后,就可以完整地使用read_sql_query方法:
import psycopg2
try:
connection = psycopg2.connect(user="[db-user_name]",
password="[db-pwd]",
host="127.0.0.1",
port="5433",
database="postgres")
happy= pd.read_sql_query("select * from happy;",con=connection).head()
这将生成以下输出:

从 PostgreSQL 数据库中的表查询出的世界幸福报告数据作为 DataFrame
看一下下面的代码。它有助于运行 SQL 查询。
posgrt40 = pd.read_sql_query('select * from happy where "Positive affect" > 40;',con=connection).head()
这将生成以下输出:

使用从 PostgreSQL 数据库中的表查询出的带过滤器的世界幸福报告数据作为 DataFrame
except (Exception, psycopg2.Error) as error :
print ("Error while fetching data from PostgreSQL", error)
pd.read_sql_query()方法将结果返回为 DataFrame,而无需程序员介入并将数据转换为所需格式。
Pandas 绘图
一图胜千言。这就是为什么图表通常用于直观地展示数据之间的关系。图表的目的是呈现那些过于庞大或复杂,以至于无法通过文本充分描述的数据,并且能在更小的空间内展示。使用 Python 的绘图函数,只需要几行代码就可以创建出生产质量的图形。
我们将开始安装所需的包:
import pandas as pd
import numpy as np
我们在这里使用mtcars数据来解释这些图表:
mtcars = pd.DataFrame({
'mpg':[21,21,22.8,21.4,18.7,18.1,18.3,24.4,22.8,19.2],
'cyl':[6,6,4,6,8,6,8,4,4,4],
'disp':[160,160,108,258,360,225,360,146.7,140.8,167.7],
'hp':[110,110,93,110,175,105,245,62,95,123],
'category':['SUV','Sedan','Sedan','Hatchback','SUV','Sedan','SUV','Hatchback','SUV','Sedan']
})
mtcars
这将生成以下输出:

mtcars DataFrame
让我们详细讨论pandas.plotting中的各种绘图。
Andrews 曲线
Andrews 曲线是一种用于可视化多维数据的方法。它通过将每个观察值映射到一个函数来实现。在这里,每种使用的颜色代表一个类别,我们可以很容易地看到,代表同一类别的样本线具有相似的曲线。这种曲线在分析时间序列和信号数据时非常有用。
基本上,每个数据点都根据傅里叶函数进行傅里叶变换。以下图表中的每条线代表一个单独的数据点。可以使用下面的代码片段绘制它。
andrew = pd.plotting.andrews_curves(mtcars,'Category')
以下是图表:

Andrews 曲线图
并行图
并行绘图最适合在需要比较每个点的多个变量并理解它们之间关系时使用,例如,当你需要比较一组具有相同属性但不同数值的变量时(例如,比较不同型号的摩托车规格)。
每条连接线代表一个数据点。垂直线表示已为每个数据点绘制的列或变量的值。拐点(用红色标记)表示这些变量在这些点上的值。可以通过以下代码非常轻松地绘制并行图:
parallel = pd.plotting.parallel_coordinates(mtcars,'Category')
这会生成以下输出:

并行图
Radviz 图
Radviz 图允许探索多任务分类问题。它以二维投影展示三个或更多变量的数据。这个图像类似于一个圆形,数据点位于圆形内部。变量则位于圆圈的周边。
每个点的位置由所有构成它的变量值决定。创建一个虚拟圆圈,并将变量放置在这个圆圈上。点位于圆圈的边缘内。点的确切位置由各个变量施加的力的总和为零的位置决定。每个变量施加的力可以看作是弹簧力,并遵循胡克定律(F = kx):

Radviz 图解释
上图可以通过运行下面的代码片段来获得。
rad_viz = pd.plotting.radviz(mtcars, 'Category')
这会生成以下输出:

Radviz 图
散点矩阵图
散点矩阵图由多个变量的图表组成,所有这些图表都以矩阵格式呈现。基本上,会创建一个 2 x 2 的变量矩阵,其中每个单元格表示两个变量的组合。然后,为每个组合生成一个散点图。它可以用来确定变量之间的相关性,广泛应用于维度缩减的情况:
scatter = pd.plotting.scatter_matrix(mtcars,alpha = 0.5)
这会生成以下输出:

散点矩阵图
延迟图
延迟图是一种特殊类型的散点图,包含变量(X、X-滞后等)。
X-滞后是由 X 衍生出的带时间滞后的变量。该图绘制了两个变量之间的关系,用于确定数据的随机性、模型适应性、异常值和序列相关性——特别是时间序列数据:
s = pd.Series(np.random.uniform(size=100))
这会生成以下输出:

延迟图
自助法图
自助法图用于确定统计量的统计不确定性,例如均值、中位数、最小-最大值等。它依赖于带有替换的随机抽样方法。通过从同一数据中随机抽取多次样本并计算每次抽样的结果平均值,这一过程称为自助抽样。自助法图基本上绘制了从每个随机样本中得到的所有结果值。它计算所有样本的均值、中位数和众数,并将它们绘制为条形图和折线图。
从数据中选择一个随机样本,重复该过程若干次,以获得所需的指标。最终得到的图表就是一个自助法图:
fig = pd.plotting.bootstrap_plot(s)
这将产生以下输出:

延迟图
pandas-datareader
我们可以使用 pandas 不仅从本地 CSV 或文本文件中读取数据,还可以从各种流行的远程数据源中读取数据,如 Yahoo Finance、世界银行等。如果没有 pandas 的支持,这将变得十分繁琐,我们可能不得不依赖网页抓取。通过pandas-datareader,这个简单且强大的功能得到了提供。
它为我们提供了一种通过 pandas 生态系统直接连接到各种数据源的方式,无需深入 HTML/JavaScript 代码的复杂性,在这些代码中数据被嵌套。只需提供数据源名称和数据代码即可访问这些数据源,且仅能获取数据的一个子集。
让我们深入了解,看看如何使用它:
- 通过以下命令使用
pip安装pandas-datareader:
pip install pandas-datareader
您也可以通过以下命令集通过conda进行安装:
- 首先,我们需要将
conda-forge添加到我们的频道:
conda config --add channels conda-forge
- 启用
pandas-datareader后,可以使用以下代码进行安装:
conda install pandas-datareader
现在,让我们动手使用一些远程数据源,并通过 pandas 库提供的各种功能,了解它是如何工作的。
Yahoo Finance
如果您对了解商业世界的趋势感兴趣,并渴望获取关于股票和债券的最新动态,或者您是其中的投资者,您可能会渴望每分钟更新一次。Google Finance 是由 Google 开发的一个金融网站,它通过提供所需的信息并允许我们根据兴趣自定义需求,使这一过程变得更加简便。
由于 API 中存在较大的中断,Google 的 API 在 2017 年以后变得不那么可靠,并且由于没有稳定的替代品,已经变得高度弃用。
其替代方案是Yahoo Finance,它与 Google Finance 类似,并且因其强大的数据和一致性而在用户中颇受欢迎。
现在,让我们使用pandas-datareader通过 Google Finance API 获取与股票、共同基金以及任何与金融相关的信息。
import pandas as pd
from pandas_datareader import data
symbols=['AAPL','GOOGL','FB','TWTR']
# initializing a dataframe
get_data = pd.DataFrame()
stock = pd.DataFrame()
for ticker in symbols:
get_data = get_data.append(data.DataReader(ticker,
start='2015-1-1',
end='2019-6-23', data_source='yahoo'))
for line in get_data:
get_data['symbol'] = ticker
stock = stock.append(get_data)
get_data = pd.DataFrame()
stock.head()
上述代码将产生以下输出:

苹果、谷歌、Facebook 和 Twitter 的日期过滤股票数据
stock.describe()
这将产生以下输出:

股票数据的汇总统计
请查看以下代码:
# get the list of column names
cols = [ col for col in stock.columns]
cols
这将产生以下输出:

请查看以下代码:
# returns the symbol of the highest traded value among the symbols
stock.loc[stock['High']==stock['High'].max(), 'High']
这将得到以下输出:

上述代码将返回一个 DataFrame,提供Apple[AAPL]、Google[GOOGL]、FB[FB] 和 Twitter[TWTR] 在这两个日期之间的每一天的股票价格详细信息。
在进行任何分析之前,了解你的数据非常重要。请考虑以下几点:
-
High 是该特定日期股票的最高交易价格。
-
Low 是该特定日期股票的最低交易价格。
-
Open 是该日期开始时股票的价格。
-
Close 是该日期市场收盘时的股票价格。
-
Volume 是该特定股票的交易量,即交易的实际股票数量。
-
Adj Close 是市场关闭后股票的价格。
世界银行
世界银行是一个提供财务咨询并在经济状况方面帮助各国的组织。它还提供各种数据,包括时间序列、地理空间数据、财务数据等,这些都对分析非常有帮助。
在开始从世界银行网站获取数据之前,我们必须先注册。这允许我们获得我们想要下载的数据集的指标代码。注册世界银行是免费的,且不会占用太多时间。
出于本示例的目的,我使用了世界发展指标数据集。你可以选择任何你喜欢的数据集并开始使用:
from pandas_datareader import wb
要获取指标代码,请选择 Databank 标签页,并从左侧面板中显示的列表中选择 Metadata Glossary。你可以在每个数据集下方的左侧面板找到该指标(在附图中用红色标出):

数据集的指标代码,用于获取数据
dat = wb.download(indicator='FP.CPI.TOTL.ZG', start=2005, end=2019)
返回的 DataFrame 采用多重索引行格式:
dat.head()
这将得到以下输出:

世界银行指标数据的多重索引 DataFrame 输出
现在,我们来展示一个特定国家的数据,如下所示:
dat.loc['Canada']
这将得到以下输出:

世界银行指标数据的多重索引 DataFrame 输出,针对一个国家。它变为单重索引。
我们还可以仅返回一个特定年份的价格通货膨胀数据,如下所示:
dat.loc['Canada'].loc['2015']
这将得到以下输出:

按两个索引子集化的数据
总结
阅读完本章后,观察到以下几点:
-
pandas提供了强大的方法,让我们可以从各种数据结构和多个数据源读取和写入数据。 -
pandas 中的
read_csv方法可以用来读取 CSV 文件、TXT 文件和表格。此方法有许多参数,用于指定分隔符、跳过的行、分块读取文件等。 -
pandas 可以用来直接从 URL 或 S3 中读取数据。
-
DataFrame 可以转换为 JSON,反之亦然。JSON 可以存储在文本文件中,并且可以读取。
-
JSON 具有类似字典的结构,可以无限次嵌套。这些嵌套数据可以像字典一样通过键进行子集提取。
-
Pandas 提供了方法,允许我们从 HD5、HTML、SAS、SQL、parquet、feather 和 Google BigQuery 等数据格式中读取数据。
-
序列化有助于将数据结构或对象转储到物理文件中、存储到数据库,或通过消息进行传输。
在下一章中,我们将学习如何访问和选择 pandas 数据结构中的数据。我们还将详细讨论基本索引、标签索引、整数索引和混合索引。
第三部分:精通 pandas 中的不同数据操作
本节将介绍你可以使用 pandas 的强大功能执行的不同数据分析任务。这些实用的章节将使你成为 pandas 中不同数据操作技术的专家,从数据访问和选择到与其他数据的合并与分组。如果你想精通使用 pandas 进行专业的数据分析,这些内容是必不可少的。
本节包含以下章节:
-
第五章,pandas 中的索引和选择
-
第六章,pandas 中的数据分组、合并和重塑
-
第七章,pandas 中的特殊数据操作
-
第八章,使用 Matplotlib 进行时间序列和绘图
第五章:pandas 中的索引和选择
在上一章中,你学习了如何通过 pandas 从任何来源读取并将结构化数据存储为 pandas 对象——Series、DataFrame 或 Panel。本章详细介绍了如何对这些对象进行切片操作。行标签和列标签作为标识符,帮助我们选择数据的子集。除了标签外,位置标识符(如行索引和列索引)也可以使用。索引和选择是对数据进行的最基本但至关重要的操作。本章将讨论的主题包括以下内容:
-
基本索引
-
标签、整数和混合索引
-
多重索引
-
布尔索引
-
索引操作
基本索引
如果你接触过 Python 中的列表,你会知道一对方括号([])用于索引和子集化列表。方括号运算符在切片 NumPy 数组时也很有用。方括号 [] 也是 pandas 中的基本索引运算符。
让我们创建一个 Series、DataFrame 和 Panel 来了解在 pandas 中如何使用方括号运算符:
# Creating a series with 6 rows and user-defined index
ser = pd.Series(["Numpy", "Pandas", "Sklearn", "Tensorflow", "Scrapy", "Keras"],
index = ["A", "B", "C", "D", "E", "F"])
# Creating a 6X3 dataframe with defined row and column labels
df = pd.DataFrame(np.random.randn(6, 3), columns = ["colA", "colB", "colC"],
index = ["R1", "R2", "R3", "R4", "R5", "R6"])
# Creating a panel with 3 items
pan = pd.Panel({"Item1": df+1, "Item2": df, "Item3": df*2})
对于 Series,方括号运算符可以通过指定标签或位置索引来进行切片。以下代码块展示了这两种用法:
# Subset using the row-label
In: ser["D"]
Out: 'Tensorflow'
# Subset using positional index
In: ser[1]
Out: 'Pandas'
在 DataFrame 中使用方括号运算符确实有一些限制。它只允许传递列标签,而不允许传递位置索引或甚至行标签。传递任何不代表列名的其他字符串会引发 KeyError:
# Subset a single column by column name
df["colB"]
这将产生以下输出:

按列名称子集化单列
可以使用一系列方括号运算符,在列属性后指定行索引或行标签:
# Accessing a single element in a DataFrame
df["colB"]["R3"], df["colB"][1]
这将产生以下输出:

使用方括号运算符切片单个元素
适用于 DataFrame 的规则也适用于 Panel——每个项都可以通过指定项名称从 Panel 中切片。方括号运算符仅接受有效的项名称:
# Subset a panel
pan["Item1"]
这将产生以下输出:

Panel 的子集
要子集化多个值,应该将实体标签的列表传递给方括号运算符。让我们使用 DataFrame 来检查这一点。这对 Series 和 Panel 同样适用:
df[["colA", "colB"]]
这将产生以下输出:

从 DataFrame 中切片多个列
当传入的字符串不是列名时,会引发异常。可以通过使用 get() 方法来克服这个问题:
In: df.get("columnA", "NA")
Out: 'NA'
方括号运算符在 DataFrame 中也用于插入新列,以下代码块演示了这一点:
# Add new column "colD"
df["colD"] = list(range(len(df)))
df
这将产生以下输出:

向 DataFrame 添加新列
也可以通过这里显示的方法向 Series 和 Panels 中添加新值。
使用点操作符访问属性
要访问单个实体(列、值或项),可以将方括号操作符替换为点操作符。让我们使用点(.)操作符来选择 DataFrame 中的 colA:
df.colA
这将产生以下输出:

使用点操作符切片列
通过在链中使用两个点操作符,可以访问单个元素:
In: df.colA.R3
Out: -2.089066
这同样适用于 Panels 和 Series。然而,与 Series 中的方括号操作符不同,这里不能使用位置索引。为了使用点操作符,行标签或列标签必须具有有效的名称。有效的 Python 标识符必须遵循以下词法规则:
identifier::= (letter|"_") (letter | digit | "_")*
Thus, a valid Python identifier cannot contain a space. See the Python Lexical Analysis documents for more details at http://docs.python.org/2.7/reference/lexical_analysis.html#identifiers.
使用点操作符,现有列的值可以被更改。然而,不能创建新列。
范围切片
通过提供起始和结束位置来切片以获取一系列值,像在 NumPy 数组中一样,这在 pandas 对象中也适用。[ : ] 操作符有助于范围切片。
让我们切片之前创建的 Series,选取第二、第三和第四行:
ser[1:4]
这将产生以下输出:

使用索引范围切片 Series
与 Python 中的范围一样,切片时冒号后的值会被排除。
范围切片可以通过提供起始或结束索引来完成。如果没有提供结束索引,值将从给定的起始索引切片到数据结构的末尾。同样,当只提供结束索引时,第一行会被视为切片的起始位置:
# End provided for range slicing
df[:2]
这将产生以下输出:

使用定义范围末端的范围切片
当给定起始索引时,选择该索引值对应的行作为切片的起始位置:
# Start provided for range slicing
df[2:]
这将产生以下输出:

使用定义范围起始端的范围切片
通过属性使范围切片变得更有趣,可以选择间隔均匀的行。例如,你可以通过这种方式选择奇数行或偶数行:
# Select odd rows
df[::2]
这将产生以下输出:

使用范围切片选择奇数行
要选择偶数行,可以使用以下代码:
# Select even rows
df[1::2]
这将产生以下输出:

使用范围切片选择偶数行
如果你想反转行的顺序,可以使用以下命令:
# Reverse the rows
df[::-1]
这将产生以下输出:

使用范围切片反转行的顺序
标签、整数和混合索引
除了标准的索引操作符 [] 和属性操作符外,pandas 还提供了一些操作符,使得索引更加简便和高效。通过标签索引,通常是指使用列标题进行索引,这些标题通常是字符串类型的值。这些操作符包括:
-
.loc操作符:该操作符允许基于标签的索引。 -
.iloc操作符:该操作符允许基于整数的索引。 -
.ix操作符:该操作符允许混合标签和基于整数的索引。
现在,我们将重点介绍这些操作符。
基于标签的索引
.loc 操作符支持纯标签基础的索引。它接受以下作为有效输入:
-
单个标签,如
["colC"]、[2]或["R1"]—— 请注意,在标签是整数的情况下,它并不代表索引的位置,而是整数本身作为标签。 -
标签列表或数组,例如
["colA", "colB"]。 -
带标签的切片对象,例如
"colB":"colD"。 -
一个布尔数组。
让我们分别查看这四种情况,针对以下两个 Series —— 一个是基于整数标签,另一个是基于字符串标签:
ser_loc1 = pd.Series(np.linspace(11, 15, 5))
ser_loc2 = pd.Series(np.linspace(11, 15, 5), index = list("abcde"))
# Indexing with single label
In: ser_loc1.loc[2]
Out: 13.0
In: ser_loc2.loc["b"]
Out: 12.0
# Indexing with a list of labels
ser_loc1.loc[[1, 3, 4]]
这将生成以下输出:

使用 loc1.loc 对整数标签列表进行索引的输出
ser_loc2.loc[["b", "c", "d"]]

使用 loc2.loc 对标签列表进行索引的输出
# Indexing with range slicing
ser_loc1.loc[1:4]

使用 loc 进行范围切片(整数标签)的输出
ser_loc2.loc["b":"d"]

使用 loc 进行范围切片的输出
请注意,与 Python 中的范围不同,后端端点不被排除,这里两个端点都包括在所选数据中。pandas 对象还可以基于对对象内部值应用的逻辑条件进行过滤:
# Indexing with Boolean arrays
ser_loc1.loc[ser_loc1 > 13]

使用布尔数组进行索引的 loc 输出
现在,这些切片技巧可以应用到 DataFrame 中。它的工作方式相同,只是有一个额外的要求:可以为每个轴提供两个标签集合。
# Create a dataframe with default row-labels
df_loc1 = pd.DataFrame(np.linspace(1, 25, 25).reshape(5, 5), columns = ["Asia", "Europe", "Africa", "Americas", "Australia"])
# Create a dataframe with custom row labels
df_loc2 = pd.DataFrame(np.linspace(1, 25, 25).reshape(5, 5), columns = ["Asia", "Europe", "Africa", "Americas", "Australia"], index = ["2011", "2012", "2013", "2014", "2015"])
# Indexing with single label
df_loc1.loc[:,"Asia"]

使用 loc 对单列进行切片的输出
df_loc1.loc[2, :]

使用 loc 对单行(整数标签)进行切片的输出
在前面的例子中,“2”并没有代表位置,而是代表索引标签:
df_loc2.loc["2012", :]

使用 loc 对单行进行切片的输出
# Indexing with a list of labels
df_loc1.loc[:,["Africa", "Asia"]]

使用 loc 通过标签列表进行选择的输出
# Indexing with range slicing
df_loc1.loc[:,"Europe":"Americas"]

使用 loc 进行范围切片的输出
# Indexing with Boolean array
df_loc2.loc[df_loc2["Asia"] > 11, :]

使用 loc 进行基于布尔数组切片的输出
整数基础的索引
整数基础的索引可以实现与基于标签的索引相同的四种情况:单个标签、一组标签、范围切片和布尔数组。
让我们使用与前一节相同的 DataFrame 来理解面向整数的索引。在这里,我们使用两个值——分别针对每个轴——来检查基于整数的索引。也允许仅传递一个轴的索引。通过loc运算符也可以做到这一点,只需传入行和列标签:
# Indexing with single values.
In: df_loc1.iloc[3, 2]
Out: 18.0
# Indexing with list of indices
df_loc1.iloc[[1, 4], [0, 2, 3]]

使用索引列表进行切片时的 iloc 输出
# Indexing with ranged slicing
df_loc2.iloc[3:,:3]

使用范围切片时的 iloc 输出
# Indexing with Boolean array
df_loc2.iloc[(df_loc2["Asia"] > 11).values, :]

使用布尔数组进行切片时的 iloc 输出
对于基于布尔数组的iloc运算符索引,必须通过围绕数组值的逻辑条件提取数组。
.iat和.at运算符
.iat和.at运算符等同于.iloc和.loc运算符——前者用于基于位置的索引,后者用于基于标签的索引。虽然.loc和.iloc支持选择多个值,但.at和.iat只能提取单个标量值。因此,它们需要行和列索引进行切片:
In: df_loc2.at["2012", "Americas"]
Out: 9.0
In: df_loc1.iat[2, 3]
Out: 14.0
.iat和.at运算符的性能比.iloc和.loc要快得多:

对.iat与.iloc的基准测试
使用.ix 运算符进行混合索引
.ix运算符既支持基于标签的索引,也支持基于位置的索引,并且被认为是.loc和.iloc运算符的通用版本。由于存在歧义,这个运算符已经被弃用,并且在未来的版本中将不再可用。因此,建议不要使用.ix运算符。让我们来了解一下.ix运算符。
这里,行索引是基于标签的,列索引是基于位置的:
df_loc2.ix["2012":"2014", 0:2]

在 DataFrame 中使用.ix 进行混合索引
多重索引
现在我们将转向多重索引的话题。多级或层次化索引非常有用,因为它允许 pandas 用户通过使用诸如 Series 和 DataFrame 等数据结构,在多个维度中选择和处理数据。为了开始,让我们将以下数据保存到文件stock_index_prices.csv,并读取它:
In[950]:sharesIndexDataDF=pd.read_csv('./stock_index_prices.csv')
In [951]: sharesIndexDataDF
Out[951]:
TradingDate PriceType Nasdaq S&P 500 Russell 2000
0 2014/02/21 open 4282.17 1841.07 1166.25
1 2014/02/21 close 4263.41 1836.25 1164.63
2 2014/02/21 high 4284.85 1846.13 1168.43
3 2014/02/24 open 4273.32 1836.78 1166.74
4 2014/02/24 close 4292.97 1847.61 1174.55
5 2014/02/24 high 4311.13 1858.71 1180.29
6 2014/02/25 open 4298.48 1847.66 1176.00
7 2014/02/25 close 4287.59 1845.12 1173.95
8 2014/02/25 high 4307.51 1852.91 1179.43
9 2014/02/26 open 4300.45 1845.79 1176.11
10 2014/02/26 close 4292.06 1845.16 1181.72
11 2014/02/26 high 4316.82 1852.65 1188.06
12 2014/02/27 open 4291.47 1844.90 1179.28
13 2014/02/27 close 4318.93 1854.29 1187.94
14 2014/02/27 high 4322.46 1854.53 1187.94
15 2014/02/28 open 4323.52 1855.12 1189.19
16 2014/02/28 close 4308.12 1859.45 1183.03
17 2014/02/28 high 4342.59 1867.92 1193.50
在这里,我们通过TradingDate和PriceType列创建一个多重索引:
In[958]:sharesIndexDF=sharesIndexDataDF.set_index(['TradingDate','PriceType'])
In [959]: mIndex=sharesIndexDF.index; mIndex
Out[959]: MultiIndex
[(u'2014/02/21', u'open'), (u'2014/02/21', u'close'), (u'2014/02/21', u'high'), (u'2014/02/24', u'open'), (u'2014/02/24', u'close'), (u'2014/02/24', u'high'), (u'2014/02/25', u'open'), (u'2014/02/25', u'close'), (u'2014/02/25', u'high'), (u'2014/02/26', u'open'), (u'2014/02/26', u'close'), (u'2014/02/26', u'high'), (u'2014/02/27', u'open'), (u'2014/02/27', u'close'), (u'2014/02/27', u'high'), (u'2014/02/28', u'open'), (u'2014/02/28', u'close'), (u'2014/02/28', u'high')]
In [960]: sharesIndexDF
Out[960]: Nasdaq S&P 500 Russell 2000 TradingDate PriceType
2014/02/21 open 4282.17 1841.07 1166.25
close 4263.41 1836.25 1164.63
high 4284.85 1846.13 1168.43
2014/02/24 open 4273.32 1836.78 1166.74
close 4292.97 1847.61 1174.55
high 4311.13 1858.71 1180.29
2014/02/25 open 4298.48 1847.66 1176.00
close 4287.59 1845.12 1173.95
high 4307.51 1852.91 1179.43
2014/02/26 open 4300.45 1845.79 1176.11
close 4292.06 1845.16 1181.72
high 4316.82 1852.65 1188.06
2014/02/27 open 4291.47 1844.90 1179.28
close 4318.93 1854.29 1187.94
high 4322.46 1854.53 1187.94
2014/02/28 open 4323.52 1855.12 1189.19
close 4308.12 1859.45 1183.03
high 4342.59 1867.92 1193.50
经检查,我们发现多重索引由一组元组组成。使用适当参数应用get_level_values函数,生成每个索引级别的标签列表:
In [962]: mIndex.get_level_values(0)
Out[962]: Index([u'2014/02/21', u'2014/02/21', u'2014/02/21', u'2014/02/24', u'2014/02/24', u'2014/02/24', u'2014/02/25', u'2014/02/25', u'2014/02/25', u'2014/02/26', u'2014/02/26', u'2014/02/26', u'2014/02/27', u'2014/02/27', u'2014/02/27', u'2014/02/28', u'2014/02/28', u'2014/02/28'], dtype=object)
In [963]: mIndex.get_level_values(1)
Out[963]: Index([u'open', u'close', u'high', u'open', u'close', u'high', u'open', u'close', u'high', u'open', u'close', u'high', u'open', u'close', u'high', u'open', u'close', u'high'], dtype=object)
如果传递给get_level_values()的值无效或超出范围,将抛出IndexError:
In [88]: mIndex.get_level_values(2)
---------------------------------------------------------
IndexError Traceback (most recent call last)
...
你可以通过多重索引的 DataFrame 实现层次化索引:
In [971]: sharesIndexDF.ix['2014/02/21']
Out[971]: Nasdaq S&P 500 Russell 2000
PriceType
open 4282.17 1841.07 1166.25
close 4263.41 1836.25 1164.63
high 4284.85 1846.13 1168.43
In [976]: sharesIndexDF.ix['2014/02/21','open']
Out[976]: Nasdaq 4282.17
S&P 500 1841.07
Russell 2000 1166.25
Name: (2014/02/21, open), dtype: float64
我们可以使用多重索引进行切片:
In [980]: sharesIndexDF.ix['2014/02/21':'2014/02/24']
Out[980]: Nasdaq S&P 500 Russell 2000
TradingDate PriceType
2014/02/21 open 4282.17 1841.07 1166.25
close 4263.41 1836.25 1164.63
high 4284.85 1846.13 1168.43
2014/02/24 open 4273.32 1836.78 1166.74
close 4292.97 1847.61 1174.55
high 4311.13 1858.71 1180.29
我们可以尝试在更低级别进行切片:
In [272]:
sharesIndexDF.ix[('2014/02/21','open'):('2014/02/24','open')]
------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-272-65bb3364d980> in <module>()
----> 1 sharesIndexDF.ix[('2014/02/21','open'):('2014/02/24','open')]
...
KeyError: 'Key length (2) was greater than MultiIndex lexsort depth (1)'
然而,这会导致KeyError,并伴随一个相当奇怪的错误信息。这里的关键教训是,目前版本的多重索引要求标签已排序,才能确保低级切片操作正确执行。
为了实现这一点,你可以使用sortlevel()方法,它对多重索引中的轴标签进行排序。为了安全起见,先排序再进行多重索引切片。因此,我们可以这样做:
In [984]: sharesIndexDF.sortlevel(0).ix[('2014/02/21','open'):('2014/02/24','open')]
Out[984]: Nasdaq S&P 500 Russell 2000
TradingDate PriceType
2014/02/21 open 4282.17 1841.07 1166.25
2014/02/24 close 4292.97 1847.61 1174.55
high 4311.13 1858.71 1180.29
open 4273.32 1836.78 1166.74
我们还可以传递一个元组列表:
In [985]: sharesIndexDF.ix[[('2014/02/21','close'),('2014/02/24','open')]]
Out[985]: Nasdaq S&P 500 Russell 2000 TradingDate PriceType 2014/02/21 close 4263.41 1836.25 1164.63 2014/02/24 open 4273.32 1836.78 1166.74 2 rows × 3 columns
请注意,通过指定一个元组列表而不是范围,如前面的示例所示,我们仅显示TradingDate = 2014/02/24时的PriceType的开盘值,而不是显示所有三个值。
交换和重新排序级别
swaplevel函数允许在多重索引中交换级别:
In [281]: swappedDF=sharesIndexDF[:7].swaplevel(0, 1, axis=0)
swappedDF
Out[281]: Nasdaq S&P 500 Russell 2000
PriceType TradingDate
open 2014/02/21 4282.17 1841.07 1166.25
close 2014/02/21 4263.41 1836.25 1164.63
high 2014/02/21 4284.85 1846.13 1168.43
open 2014/02/24 4273.32 1836.78 1166.74
close 2014/02/24 4292.97 1847.61 1174.55
high 2014/02/24 4311.13 1858.71 1180.29
open 2014/02/25 4298.48 1847.66 1176.00
7 rows × 3 columns
reorder_levels函数更加通用,允许你指定级别的顺序:
In [285]: reorderedDF=sharesIndexDF[:7].reorder_levels(['PriceType', 'TradingDate'],axis=0)
reorderedDF
Out[285]: Nasdaq S&P 500 Russell 2000
PriceType TradingDate
open 2014/02/21 4282.17 1841.07 1166.25
close 2014/02/21 4263.41 1836.25 1164.63
high 2014/02/21 4284.85 1846.13 1168.43
open 2014/02/24 4273.32 1836.78 1166.74
close 2014/02/24 4292.97 1847.61 1174.55
high 2014/02/24 4311.13 1858.71 1180.29
open 2014/02/25 4298.48 1847.66 1176.00
7 rows × 3 columns
交叉截面
xs方法提供了一种基于特定索引级别值选择数据的快捷方式:
In [287]: sharesIndexDF.xs('open',level='PriceType')
Out[287]:
Nasdaq S&P 500 Russell 2000
TradingDate
2014/02/21 4282.17 1841.07 1166.2x5
2014/02/24 4273.32 1836.78 1166.74
2014/02/25 4298.48 1847.66 1176.00
2014/02/26 4300.45 1845.79 1176.11
2014/02/27 4291.47 1844.90 1179.28
2014/02/28 4323.52 1855.12 1189.19
6 rows × 3 columns
前面命令的冗长替代方法是使用swaplevel在TradingDate和PriceType级别之间切换,然后执行如下选择:
In [305]: sharesIndexDF.swaplevel(0, 1, axis=0).ix['open']
Out[305]: Nasdaq S&P 500 Russell 2000
TradingDate
2014/02/21 4282.17 1841.07 1166.25
2014/02/24 4273.32 1836.78 1166.74
2014/02/25 4298.48 1847.66 1176.00
2014/02/26 4300.45 1845.79 1176.11
2014/02/27 4291.47 1844.90 1179.28
2014/02/28 4323.52 1855.12 1189.19
6 rows × 3 columns
使用.xs实现的效果与在前一节关于整数索引的内容中获取交叉截面相同。
布尔索引
我们使用布尔索引来过滤或选择数据的部分内容。操作符如下:
| 操作符 | 符号 |
|---|---|
| 或 | | |
| 与 | & |
| 非 | ~ |
当这些操作符一起使用时,必须用括号将它们分组。使用前一节中的 DataFrame,我们在这里显示了纳斯达克收盘价高于 4,300 的交易日期:
In [311]: sharesIndexDataDF.ix[(sharesIndexDataDF['PriceType']=='close') & \
(sharesIndexDataDF['Nasdaq']>4300) ]
Out[311]: PriceType Nasdaq S&P 500 Russell 2000
TradingDate
2014/02/27 close 4318.93 1854.29 1187.94
2014/02/28 close 4308.12 1859.45 1183.03
2 rows × 4 columns
你还可以创建布尔条件,使用数组来筛选数据的部分,如下代码所示:
highSelection=sharesIndexDataDF['PriceType']=='high' NasdaqHigh=sharesIndexDataDF['Nasdaq']<4300 sharesIndexDataDF.ix[highSelection & NasdaqHigh]
Out[316]: TradingDate PriceType Nasdaq S&P 500 Russell 2000
2014/02/21 high 4284.85 1846.13 1168.43
因此,上面的代码片段显示了数据集中唯一一个在整个交易会话中,纳斯达克综合指数始终保持低于 4,300 点的日期。
isin和any all方法
这些方法使用户通过布尔索引比前面章节中使用的标准操作符能够实现更多功能。isin方法接受一个值列表,并返回一个布尔数组,其中在 Series 或 DataFrame 中与列表中的值匹配的位置标记为True。这使得用户能够检查 Series 中是否存在一个或多个元素。下面是一个使用Series的示例:
In[317]:stockSeries=pd.Series(['NFLX','AMZN','GOOG','FB','TWTR'])
stockSeries.isin(['AMZN','FB'])
Out[317]:0 False
1 True
2 False
3 True
4 False
dtype: bool
在这里,我们使用布尔数组来选择一个包含我们感兴趣值的子系列:
In [318]: stockSeries[stockSeries.isin(['AMZN','FB'])]
Out[318]: 1 AMZN
3 FB
dtype: object
对于我们的 DataFrame 示例,我们切换到一个更有趣的数据集,对于那些从事生物人类学研究的人来说,这是分类澳大利亚哺乳动物的数据集(这是我的一个兴趣爱好):
In [324]: australianMammals=
{'kangaroo': {'Subclass':'marsupial',
'Species Origin':'native'},
'flying fox' : {'Subclass':'placental',
'Species Origin':'native'},
'black rat': {'Subclass':'placental',
'Species Origin':'invasive'},
'platypus' : {'Subclass':'monotreme',
'Species Origin':'native'},
'wallaby' : {'Subclass':'marsupial',
'Species Origin':'native'},
'palm squirrel' : {'Subclass':'placental',
'Origin':'invasive'},
'anteater': {'Subclass':'monotreme', 'Origin':'native'},
'koala': {'Subclass':'marsupial', 'Origin':'native'}
}
更多关于哺乳动物的信息:有袋类动物是有袋哺乳动物,单孔目是卵生的,胎盘动物则是生育活幼。该信息来源于:en.wikipedia.org/wiki/List_of_mammals_of_Australia.

前面图像的来源是 Bennett 的袋鼠,网址:bit.ly/NG4R7N.
让我们读取澳大利亚哺乳动物数据集,将其转换为 DataFrame,并在使用之前转置它:
In [328]: ozzieMammalsDF=pd.DataFrame(australianMammals)
In [346]: aussieMammalsDF=ozzieMammalsDF.T; aussieMammalsDF
Out[346]: Subclass Origin
anteater monotreme native
black rat placental invasive
flying fox placental native
kangaroo marsupial native
koala marsupial native
palm squirrel placental invasive
platypus monotreme native
wallaby marsupial native
8 rows × 2 columns
让我们尝试选择原产于澳大利亚的哺乳动物:
In [348]: aussieMammalsDF.isin({'Subclass':['marsupial'],'Origin':['native']})
Out[348]: Subclass Origin
anteater False True
black rat False False
flying fox False True
kangaroo True True
koala True True
palm squirrel False False
platypus False True
wallaby True True
8 rows × 2 columns
传递给 isin 的值集可以是数组或字典。虽然这样做在某种程度上是有效的,但通过将 isin 和 all() 方法结合使用创建掩码,我们可以获得更好的结果:
In [349]: nativeMarsupials={'Mammal Subclass':['marsupial'],
'Species Origin':['native']}
nativeMarsupialMask=aussieMammalsDF.isin(nativeMarsupials).all(True)
aussieMammalsDF[nativeMarsupialMask]
Out[349]: Subclass Origin
kangaroo marsupial native
koala marsupial native
wallaby marsupial native
3 rows × 2 columns
因此,我们看到袋鼠、考拉和沙袋鼠是我们数据集中原生的有袋动物。any() 方法返回布尔型 DataFrame 中是否有任何元素为 True。all() 方法则会筛选返回布尔型 DataFrame 中所有元素是否都为 True。
更多关于 pandas 方法的信息,可以从其官方文档页面查看:pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.any.html.
使用 where() 方法
where() 方法用于确保布尔过滤的结果与原始数据的形状相同。首先,我们将随机数生成器的种子设置为 100,以便用户可以生成相同的值,如下所示:
In [379]: np.random.seed(100)
normvals=pd.Series([np.random.normal() for i in np.arange(10)])
normvals
Out[379]: 0 -1.749765
1 0.342680
2 1.153036
3 -0.252436
4 0.981321
5 0.514219
6 0.221180
7 -1.070043
8 -0.189496
9 0.255001
dtype: float64
In [381]: normvals[normvals>0]
Out[381]: 1 0.342680
2 1.153036
4 0.981321
5 0.514219
6 0.221180
9 0.255001
dtype: float64
In [382]: normvals.where(normvals>0)
Out[382]: 0 NaN
1 0.342680
2 1.153036
3 NaN
4 0.981321
5 0.514219
6 0.221180
7 NaN
8 NaN
9 0.255001
dtype: float64
这个方法似乎只在 Series 的情况下有用,因为在 DataFrame 的情况下,我们已经可以免费获得这个行为:
In [393]: np.random.seed(100)
normDF=pd.DataFrame([[round(np.random.normal(),3) for i in np.arange(5)] for j in range(3)],
columns=['0','30','60','90','120'])
normDF
Out[393]: 0 30 60 90 120
0 -1.750 0.343 1.153 -0.252 0.981
1 0.514 0.221 -1.070 -0.189 0.255
2 -0.458 0.435 -0.584 0.817 0.673
3 rows × 5 columns
In [394]: normDF[normDF>0]
Out[394]: 0 30 60 90 120
0 NaN 0.343 1.153 NaN 0.981
1 0.514 0.221 NaN NaN 0.255
2 NaN 0.435 NaN 0.817 0.673
3 rows × 5 columns
In [395]: normDF.where(normDF>0)
Out[395]: 0 30 60 90 120
0 NaN 0.343 1.153 NaN 0.981
1 0.514 0.221 NaN NaN 0.255
2 NaN 0.435 NaN 0.817 0.673
3 rows × 5 columns
where 方法的逆操作是 mask:
In [396]: normDF.mask(normDF>0)
Out[396]: 0 30 60 90 120
0 -1.750 NaN NaN -0.252 NaN
1 NaN NaN -1.070 -0.189 NaN
2 -0.458 NaN -0.584 NaN NaN
3 rows × 5 columns
索引操作
为了完成本章内容,我们将讨论索引操作。当我们希望重新对齐数据或以不同方式选择数据时,有时需要操作索引。这里有多种操作:
请注意,set_index 允许在现有的 DataFrame 上创建索引,并返回一个已索引的 DataFrame,正如我们之前所看到的:
In [939]: stockIndexDataDF=pd.read_csv('./stock_index_data.csv')
In [940]: stockIndexDataDF
Out[940]: TradingDate Nasdaq S&P 500 Russell 2000
0 2014/01/30 4123.13 1794.19 1139.36
1 2014/01/31 4103.88 1782.59 1130.88
2 2014/02/03 3996.96 1741.89 1094.58
3 2014/02/04 4031.52 1755.20 1102.84
4 2014/02/05 4011.55 1751.64 1093.59
5 2014/02/06 4057.12 1773.43 1103.93
现在,我们可以按如下方式设置索引:
In [941]: stockIndexDF=stockIndexDataDF.set_index('TradingDate')
In [942]: stockIndexDF
Out[942]: Nasdaq S&P 500 Russell 2000
TradingDate
2014/01/30 4123.13 1794.19 1139.36
2014/01/31 4103.88 1782.59 1130.88
2014/02/03 3996.96 1741.89 1094.58
2014/02/04 4031.52 1755.20 1102.84
2014/02/05 4011.55 1751.64 1093.59
2014/02/06 4057.12 1773.43 1103.93
此外,reset_index 会逆转 set_index 的操作:
In [409]: stockIndexDF.reset_index()
Out[409]:
TradingDate Nasdaq S&P 500 Russell 2000
0 2014/01/30 4123.13 1794.19 1139.36
1 2014/01/31 4103.88 1782.59 1130.88
2 2014/02/03 3996.96 1741.89 1094.58
3 2014/02/04 4031.52 1755.20 1102.84
4 2014/02/05 4011.55 1751.64 1093.59
5 2014/02/06 4057.12 1773.43 1103.93
6 rows × 4 columns
阅读完这一章后,你在使用 pandas 进行数据整理方面已经取得了长足的进步。在下一章中,我们将继续学习更多有用的数据整理工具。
总结
在本章中,我们学习了如何访问和选择 pandas 数据结构中的数据。我们还详细了解了基本索引和标签导向、整数导向及混合索引方法。我们还学习了如何使用布尔/逻辑索引。在本章结束时,我们讨论了索引操作。
若要了解有关 pandas 中索引的更多信息,请查阅官方文档:pandas.pydata.org/pandas-docs/stable/indexing.html。
在下一章中,我们将探讨使用 pandas 进行分组、重塑和合并数据的主题。
第六章:在 pandas 中分组、合并和重塑数据
本章将探讨如何在我们的数据结构中重新排列和重塑数据。我们将通过对真实世界数据集应用不同的函数来审视使我们能够重新排列数据的各种函数。这些函数包括groupby、concat、aggregate、append等。
本章将讨论以下主题:
-
聚合/分组数据
-
合并和连接数据
-
数据重塑
-
重塑 DataFrame 的其他方法
分组数据
数据分组对于在初步探索性分析阶段得出关键结论至关重要。例如,当你处理一个零售数据集,数据集包含OrderID、CustomerID、Shipping Date、Product Category、Sales Region、Quantity Ordered、Cancelation Status、Total Sales、Profit、Discount等变量时,分组数据并进行聚合可以帮助你回答以下类似问题:
-
哪个地区最具盈利性?
-
哪个产品类别的取消率最高?
-
哪些客户贡献了 80%的利润?
分组涉及在每个类别下进行聚合。聚合可能包括计数、求和、求指数或实现复杂的用户定义函数。pandas 的groupby函数有助于分组。这与 SQL 中的groupby查询并没有太大区别。
groupby 操作
通过groupby函数,一系列操作被执行:拆分、应用和合并。拆分将每个类别从所需的分组变量中分离出来,以便对其执行进一步操作。然后,函数可以单独应用于这些拆分后的每个组。这些函数可能涉及聚合(对组进行求和或求均值)、转换(填充组内的 NA 值或排序)、过滤(在组内应用条件以删除行),甚至是这三种操作的组合。最后,在每个拆分组应用函数后,得到的结果会被合并在一起。
让我们使用来自一个虚构全球零售商的示例数据。以 CSV 格式提供的数据被读取为 pandas DataFrame:
sales_data = pd.read_csv("salesdata.csv", encoding = "ISO-8859-1")
head函数将让我们快速浏览刚导入的数据集:
sales_data.head()
以下是输出结果:

示例销售数据的快照
虽然在前面的输出中展示了五行数据,但数据总共包含 51,290 行和 15 列。
现在,为了理解groupby如何拆分数据,我们按Category变量进行拆分。所创建的对象不是 DataFrame,而是groupby函数特有的对象类型:
category_grouped = sales_data.groupby("Category")
type(category_grouped)
pandas.core.groupby. DataFrameGroupBy
分组对象被称为键。在这里,Category就是键。前一步创建的groupby对象下的各个组如图所示。你可以看到,每个Category中的组都映射到每个类别所覆盖的行索引标签:
category_grouped.groups
以下是输出结果:

每个组的信息
数据有四个定量变量:Quantity(数量)、Sales(销售额)、Discount(折扣)和Profit(利润)。使用groupby,我们来计算每个Category(类别)中的这四个变量的总和。这是聚合与groupby结合的应用:
sales_data.groupby("Category").sum()
以下是输出结果:

分组和求和的结果
稍微修改代码,如下所示,来计算仅销售额的总和。这涉及在应用groupby之前对数据进行子集选择:
sales_data[["Category", "Sales"]].groupby("Category").sum()
以下是输出结果:

根据一个变量进行groupby和求和
聚合不一定仅限于定量变量。现在,使用groupby,让我们找出每个类别首次下单的Country(国家):
sales_data[["Category", "Country"]].groupby("Category").first()
以下是输出结果:

先使用聚合再进行分组
size()函数有助于找到每个Category(类别)的出现次数。在计算size之后,我们通过排序结果来探索groupby的变换能力:
sales_data.groupby("Category").size().sort_values(ascending = True)
以下是输出结果:

排序后的聚合大小
分组的键或分组对象不一定是现有的列;它也可以是定义分组规则的函数。例如,我们可以从OrderDate(订单日期)中提取年份,然后按年份进行groupby。为此,首先将索引设置为OrderDate:
index_by_date = sales_data.set_index('OrderDate')
index_by_date.groupby(lambda OrderDate: OrderDate.split('-')[2]).sum()
以下是输出结果:

使用自定义函数进行分组
也可以根据多个键进行分组。在这里,我们通过ShipMode(运输方式)和Category(类别)进行分组,以按观察数量进行聚合。groupby函数接受一个包含多个变量的列表:
sales_data.groupby(["ShipMode","Category"]).size()
以下是输出结果:

根据两个分组变量进行大小聚合
groupby函数的get_group()属性允许根据一个类别过滤数据,从所有类别中选择:
sales_data.groupby("ShipMode").get_group("Same Day")
以下是输出结果:

groupby的get_group属性
groupby函数生成的groupby对象是可迭代的。让我们遍历一个简单的groupby对象:
for name, group in sales_data.groupby("ShipMode"):
print(name)
print(group.iloc[0:5,0:5])
以下是输出结果:

遍历 groupby 对象
不仅可以根据列名进行分组,也可以使用索引进行分组。在使用索引时,可以指定索引名的层级。让我们设置Region(地区)为索引来演示这一点:
region_index_df = sales_data.set_index("Region", drop = True)
region_index_df.groupby(level = 0).sum()
以下是输出结果:

使用索引进行分组
groupby 聚合操作不一定总是沿着某一列进行。如果需要,可以通过更改 axis 参数将项目沿着行进行分组和聚合。axis 参数的默认设置是 0。将其更改为 axis = 1 会将项目沿行进行分组:
sales_data.groupby("ShipMode", axis = 0).size()
在 MultiIndex 上使用 groupby
让我们探索 groupby 函数如何在分层索引数据中工作。
首先,我们可以为示例销售数据分配两个索引,如下所示:
multiindex_df = sales_data.set_index(["ShipMode", "Category"])
multiindex_df.head()
以下是输出结果:

多级索引数据的快照
按索引分组可以通过指定级别数字或索引名称来实现:
multiindex_df.groupby(level = 0).sum()
以下是输出结果:

groupby 的 level 属性
level 参数也可以使用名称而不是数字,如下所示:
multiindex_df.groupby(level = "Category").sum()
以下是输出结果:

使用级别名称进行分组
可以直接将索引名称用作键,如下所示:
multiindex_df.groupby("Category").sum()
这会产生以下输出结果:

将索引名称作为键提供
也可以通过 groupby 的 level 参数传递多个索引,以获得与前述相同的结果:
multiindex_df.groupby(level = ["ShipMode", "Category"]).sum()
以下是输出结果:

使用 groupby 进行多索引操作
当按索引分组时,聚合函数可以直接使用 level 参数来实现按组拆分。在这里,我们通过指定 level 数字实现了对两个级别的分组。除了 level 数字,还可以使用索引名称来指定:
multiindex_df.sum(level = [0, 1])
以下是输出结果:

使用 level 参数的多级索引分组
若要按索引和列名同时进行分组,可以使用以下方法。此处提供的级别数字也可以用级别名称替代。在不使用 Grouper 函数的情况下,可以将索引名称和列名作为键的列表提供:
multiindex_df.groupby([pd.Grouper(level = 1), "Region"]).size()
multiindex_df.groupby(["Category", "Region"]).size()
以下是输出结果:

使用普通列和索引列一起进行分组
让我们将 groupby 提升到一个新层次,并对结果应用数据转换。我们将开始计算总销售额、数量、利润和折扣相对于总体 Sales、Quantity、Profit 和 Discount 的比例:
sum_all = multiindex_df.groupby(level = 1).sum()
sum_all.ix["Furniture"]/(sum_all.ix["Furniture"] + sum_all.ix["Technology"] + sum_all.ix["Office Supplies"])
以下是输出结果:

使用 groupby 进行复杂计算评估
这会生成一个系列。还记得 NumPy 中的 transpose 函数吗?同样,DataFrame 也可以进行转置。然而,刚才得到的输出是一个系列,而不是 DataFrame。转置之前,必须先将系列转换为 DataFrame:
furniture_ratio = sum_all.ix["Furniture"]/(sum_all.ix["Furniture"] + sum_all.ix["Technology"] + sum_all.ix["Office Supplies"])
pd.DataFrame(furniture_ratio).T
以下是输出结果:

数据转换的中间结果
结果中的索引标签是 0。我们可以使用以下代码将其重命名为更合适的标签,输出结果也显示在后续截图中:
furniture_ratio_df = pd.DataFrame(furniture_ratio).T
furniture_ratio_df.rename(index = {0 : "FurniturePercent"})
请查看以下截图:

数据转换结果
使用聚合方法
在所有前面的使用案例中,我们使用了求和聚合。我们能够直接使用 sum,而无需经过 Python 的 aggregate 函数。我们使用的 sum() 函数是 Cython 优化的实现。其他一些 Cython 优化的实现包括 mean、std 和 sem(标准误差)。为了实现其他函数或聚合的组合,aggregate 函数非常有用:
sales_data.groupby("Category").aggregate(np.sum)
以下是输出结果:

使用聚合函数
在处理多个键和索引的章节中讨论的所有规则在此处同样适用。
请注意,在使用多个键或多重索引时,结果在索引中会有层次顺序。为了克服这一点,您可以使用 DataFrame 的 reset_index 属性:
sales_data.groupby(["ShipMode", "Category"]).aggregate(np.sum)
以下是输出结果:

多列的聚合函数
可以使用以下代码重置输出的索引:
sales_data.groupby(["ShipMode", "Category"]).aggregate(np.sum).reset_index()
以下是输出结果:

多重分组变量的聚合函数
为了实现相同的结果,可以将 groupby 的 as_index 参数设置为 False,而不使用 reset_index:
sales_data.groupby(["ShipMode", "Category"], as_index = False).aggregate(np.sum)
与 sum 函数的实现类似,以下是可以应用于 groupby 对象的其他函数列表:
| 函数 | 描述 |
|---|---|
mean() |
计算组的平均值 |
sum() |
计算组值的总和 |
size() |
计算组的大小 |
count() |
计算组的数量 |
std() |
计算组的标准差 |
var() |
计算组的方差 |
sem() |
计算组的标准误差 |
describe() |
生成描述性统计 |
first() |
计算组值中的第一个 |
last() |
计算组值中的最后一个 |
nth() |
获取第 n 个值,或者如果 n 是列表,则获取子集 |
min() |
计算组值的最小值 |
max() |
计算组值的最大值 |
表 6.1:所有聚合函数列表
应用多个函数
对于任何 DataFrame,在应用 groupby 后可以执行一系列聚合操作。在下面的示例中,我们计算了 Sales 和 Quantity 的平均值和标准差:
sales_data[["Sales", "Quantity", "Category"]].groupby("Category").agg([np.mean, np.std])
以下是输出结果:

多重聚合
请注意,列索引中也引入了层次结构。agg 是聚合的缩写。这些聚合会排除任何 NA 值进行计算。
在前面的示例中,创建了带有 mean 和 std 标签的列。让我们尝试重命名这些列。rename 参数将新名称映射到旧名称:
sales_data[["Sales", "Quantity", "Category"]].groupby("Category").agg([np.mean, np.std]).rename(columns = {"mean": "Mean", "std": "SD"})
以下将是输出结果:

每列的不同聚合
要对选定的列应用选定的函数,可以使用以下约定。例如,这里计算了 Sales 的总和和 Quantity 的均值:
sales_data[["Sales", "Quantity", "Category"]].groupby("Category").agg({"Sales":"sum", "Quantity":"mean"})
以下是输出结果:

聚合后重命名列
transform() 方法
groupby 中的 transform 函数用于对 groupby 对象执行转换操作。例如,我们可以使用 fillna 方法替换 groupby 对象中的 NaN 值。使用 transform 后的结果对象与原始 groupby 对象大小相同。
让我们向示例销售数据中引入 NAs。以下代码将 NAs 注入 25% 的记录中:
na_df = sales_data[["Sales", "Quantity", "Discount", "Profit", "Category"]].set_index("Category").mask(np.random.random(sales_data[["Sales", "Quantity", "Discount", "Profit"]].shape) &lt; .25)
na_df.head(10)
以下将是输出结果:

插入 NAs 后的数据快照
现在,四个定量变量在 25% 的行中包含 NA,Category 被设置为索引。简单的 groupby 和 count 聚合将显示每个类别中每列非 NA 值的数量:
na_df.groupby("Category").count()
以下将是输出结果:

非 NA 值的计数
transform() 函数用每组的均值填充 NAs:
transformed = na_df.groupby("Category").transform(lambda x: x.fillna(x.mean()))
transformed.head(10)
以下将是输出结果:

使用 transform 填充 NAs
结果显示,transform() 执行了按组处理 NA。可以看到非 NA 的计数有所增加:
transformed.groupby("Category").count()
以下将是输出结果:

转换后的非 NA 值计数
为了验证操作,让我们比较转换前后各组的均值。通过以下代码可以看到两种方法的输出结果相等:
na_df.groupby("Category").mean()
以下将是输出结果:

转换前的组均值
使用从 transform 方法获得的对象来计算均值,可以按如下方式进行:
transformed.groupby("Category").mean()
以下将是输出结果:

转换后的组均值
一些函数,如 bfill()(向后填充)、ffill()(向前填充)、fillna() 和 shift() 可以自行执行转换,而不需要 transform() 函数:
na_df.groupby("Category").bfill()
以下将是输出结果:

向后填充的转换
rolling()、resample() 和 expanding() 等操作也可以作为方法在 groupby 上使用。rolling() 对值进行移动窗口聚合,expanding() 累积聚合,resample() 帮助将时间序列数据转换为规则的频率,支持向前填充或向后填充:
sales_data[["Sales", "Category"]].groupby("Category").expanding().sum()
上一个 expanding() 示例计算了每个组内的累积和。
过滤
filter 方法使我们能够对 groupby 对象应用过滤,从而得到初始对象的子集。
让我们对示例销售数据应用 filter,仅计算那些组的总和,其长度大于 10000,当按 Category 分组时:
filtered_df = sales_data[["Category", "Quantity"]].set_index("Category").groupby("Category").filter(lambda x: len(x) &gt; 10000)
filtered_df.groupby("Category").sum()
以下将是输出:

使用 groupby 进行过滤
现在,正如你所看到的,过滤移除了 Furniture 类别,因为它的长度小于 10000。
合并与连接
有多种函数可以用来合并和连接 pandas 数据结构,其中包括以下函数:
-
concat -
append -
join
concat 函数
concat 函数用于在指定的轴上连接多个 pandas 数据结构,并可能在其他轴上执行并集或交集操作。以下命令解释了 concat 函数:
concat(objs, axis=0, , join='outer', join_axes=None, ignore_index=False, keys=None, levels=None, names=None, verify_integrity=False)
concat 函数的元素可以总结如下:
-
objs函数:一个 Series、DataFrame 或 Panel 对象的列表或字典,待拼接。 -
axis函数:指定应执行拼接的轴。0是默认值。 -
join函数:在处理其他轴的索引时执行的连接类型。'outer'是默认值。 -
join_axes函数:用于指定其余索引的精确索引,而不是执行外连接/内连接。 -
keys函数:指定用于构建 MultiIndex 的键列表。
有关其余选项的解释,请参考文档:pandas.pydata.org/pandas-docs/stable/merging.html。
下面是使用我们前面章节中的股票价格示例来说明 concat 的工作原理:
In [53]: stockDataDF=pd.read_csv('./tech_stockprices.csv').set_index(
['Symbol']);stockDataDF
Out[53]:
Closing price EPS Shares Outstanding(M) P/E Market Cap(B) Beta
Symbol
AAPL 501.53 40.32 892.45 12.44 447.59 0.84
AMZN 346.15 0.59 459.00 589.80 158.88 0.52
FB 61.48 0.59 2450.00 104.93 150.92 NaN
GOOG 1133.43 36.05 335.83 31.44 380.64 0.87
TWTR 65.25 -0.30 555.20 NaN 36.23 NaN
YHOO 34.90 1.27 1010.00 27.48 35.36 0.66
现在我们对数据进行不同的切片:
In [83]: A=stockDataDF.ix[:4, ['Closing price', 'EPS']]; A
Out[83]: Closing price EPS
Symbol
AAPL 501.53 40.32
AMZN 346.15 0.59
FB 61.48 0.59
GOOG 1133.43 36.05
In [84]: B=stockDataDF.ix[2:-2, ['P/E']];B
Out[84]: P/E
Symbol
FB 104.93
GOOG 31.44
In [85]: C=stockDataDF.ix[1:5, ['Market Cap(B)']];C
Out[85]: Market Cap(B)
Symbol
AMZN 158.88
FB 150.92
GOOG 380.64
TWTR 36.23
在这里,我们通过指定外连接来执行拼接,该操作会将三个 DataFrame 连接并执行并集,并包括没有所有列值的条目,通过插入 NaN 来填充这些列:
In [86]: pd.concat([A,B,C],axis=1) # outer join
Out[86]: Closing price EPS P/E Market Cap(B)
AAPL 501.53 40.32 NaN NaN
AMZN 346.15 0.59 NaN 158.88
FB 61.48 0.59 104.93 150.92
GOOG 1133.43 36.05 31.44 380.64
TWTR NaN NaN NaN 36.23
我们还可以指定内连接,执行拼接但仅包括包含所有列值的行,从而去除缺少列的行;也就是说,它取的是交集:
In [87]: pd.concat([A,B,C],axis=1, join='inner') # Inner join
Out[87]: Closing price EPS P/E Market Cap(B)
Symbol
FB 61.48 0.59 104.93 150.92
GOOG 1133.43 36.05 31.44 380.64
第三个案例使我们可以使用原始 DataFrame 中的特定索引进行连接:
In [102]: pd.concat([A,B,C], axis=1, join_axes=[stockDataDF.index])
Out[102]: Closing price EPS P/E Market Cap(B)
Symbol
AAPL 501.53 40.32 NaN NaN
AMZN 346.15 0.59 NaN 158.88
FB 61.48 0.59 104.93 150.92
GOOG 1133.43 36.05 31.44 380.64
TWTR NaN NaN NaN 36.23
YHOO NaN NaN NaN NaN
在最后的这个案例中,我们看到 YHOO 行被包含进来了,尽管它没有包含在任何被拼接的切片中。然而,在这种情况下,所有列的值都是 NaN。这是 concat 的另一个示例,但这次它是在随机统计分布上进行的。请注意,如果没有 axis 参数,拼接的默认轴是 0:
In[135]: np.random.seed(100)
normDF=pd.DataFrame(np.random.randn(3,4));normDF
Out[135]: 0 1 2 3
0 -1.749765 0.342680 1.153036 -0.252436
1 0.981321 0.514219 0.221180 -1.070043
2 -0.189496 0.255001 -0.458027 0.435163
In [136]: binomDF=pd.DataFrame(np.random.binomial(100,0.5,(3,4)));binomDF
Out[136]: 0 1 2 3
0 57 50 57 50
1 48 56 49 43
2 40 47 49 55
In [137]: poissonDF=pd.DataFrame(np.random.poisson(100,(3,4)));poissonDF
Out[137]: 0 1 2 3
0 93 96 96 89
1 76 96 104 103
2 96 93 107 84
In [138]: rand_distribs=[normDF,binomDF,poissonDF]
In [140]: rand_distribsDF=pd.concat(rand_distribs,keys=['Normal', 'Binomial', 'Poisson']);rand_distribsDF
Out[140]: 0 1 2 3
Normal 0 -1.749765 0.342680 1.153036 -0.252436
1 0.981321 0.514219 0.221180 -1.070043
2 -0.189496 0.255001 -0.458027 0.435163
Binomial 0 57.00 50.00 57.00 50.00
1 48.00 56.00 49.00 43.00
2 40.00 47.00 49.00 55.00
Poisson 0 93.00 96.00 96.00 89.00
1 76.00 96.00 104.00 103.00
2 96.00 93.00 107.00 84.00
使用 append
append 是 concat 的简化版本,按 axis=0 进行拼接。以下是它的使用示例,我们将 stockData DataFrame 中的前两行和前三列切片出来:
In [145]: stockDataA=stockDataDF.ix[:2,:3]
stockDataA
Out[145]: Closing price EPS Shares Outstanding(M)
Symbol
AAPL 501.53 40.32 892.45
AMZN 346.15 0.59 459.00
其余的行可以按如下方式获取:
In [147]: stockDataB=stockDataDF[2:]
stockDataB
Out[147]:
Closing price EPS Shares Outstanding(M) P/E Market Cap(B) Beta
Symbol
FB 61.48 0.59 2450.00 104.93 150.92 NaN
GOOG 1133.43 36.05 335.83 31.44 380.64 0.87
TWTR 65.25 -0.30 555.20 NaN 36.23 NaN
YHOO 34.90 1.27 1010.00 27.48 35.36 0.66
现在,我们使用 append 将前面命令中的两个 DataFrame 合并:
In [161]:stockDataA.append(stockDataB)
Out[161]:
Beta Closing price EPS MarketCap(B) P/E Shares Outstanding(M)
Symbol
AMZN NaN 346.15 0.59 NaN NaN 459.00
GOOG NaN 1133.43 36.05 NaN NaN 335.83
FB NaN 61.48 0.59 150.92 104.93 2450.00
YHOO 27.48 34.90 1.27 35.36 0.66 1010.00
TWTR NaN 65.25 -0.30 36.23 NaN 555.20
AAPL 12.44 501.53 40.32 0.84 447.59 892.45
为了保持列的顺序与原始 DataFrame 相似,我们可以应用 reindex_axis 函数:
In [151]: stockDataA.append(stockDataB).reindex_axis(stockDataDF.columns, axis=1)
Out[151]:
Closing price EPS Shares Outstanding(M) P/E Market Cap(B) Beta
Symbol
AAPL 501.53 40.32 892.45 NaN NaN NaN
AMZN 346.15 0.59 459.00 NaN NaN NaN
FB 61.48 0.59 2450.00 104.93 150.92 NaN
GOOG 1133.43 36.05 335.83 31.44 380.64 0.87
TWTR 65.25 -0.30 555.20 NaN 36.23 NaN
YHOO 34.90 1.27 1010.00 27.48 35.36 0.66
请注意,对于前两行,最后两列的值为 NaN,因为第一个 DataFrame 仅包含前三列。append 函数在某些地方无法正常工作,但它会返回一个新的 DataFrame,将第二个 DataFrame 追加到第一个 DataFrame 中。
向 DataFrame 中追加单行数据
我们可以通过将一个系列或字典传递给 append 方法,将单行数据追加到 DataFrame 中:
In [152]:
algos={'search':['DFS','BFS','Binary Search','Linear'],
'sorting': ['Quicksort','Mergesort','Heapsort','Bubble Sort'],
'machine learning':['RandomForest','K Nearest Neighbor','Logistic Regression','K-Means Clustering']}
algoDF=pd.DataFrame(algos);algoDF
Out[152]: machine learning search sorting
0 RandomForest DFS Quicksort
1 K Nearest Neighbor BFS Mergesort
2 Logistic Regression Binary Search Heapsort
3 K-Means Clustering Linear Bubble Sort
In [154]:
moreAlgos={'search': 'ShortestPath' , 'sorting': 'Insertion Sort',
'machine learning': 'Linear Regression'}
algoDF.append(moreAlgos,ignore_index=True)
Out[154]: machine learning search sorting
0 RandomForest DFS Quicksort
1 K Nearest Neighbor BFS Mergesort
2 Logistic Regression Binary Search Heapsort
3 K-Means Clustering Linear Bubble Sort
4 Linear Regression ShortestPath Insertion Sort
为了使此操作生效,必须传递 ignore_index=True 参数,以便忽略 algoDF 中的 index [0,1,2,3]。
类似 SQL 的 DataFrame 对象合并/连接
merge 函数用于连接两个 DataFrame 对象,类似于 SQL 数据库查询。它返回一个合并后的 DataFrame。DataFrame 对象类似于 SQL 表。以下命令解释了这一点:
merge(left, right, how='inner', on=None, left_on=None,
right_on=None, left_index=False, right_index=False,
sort=True, suffixes=('_x', '_y'), copy=True)
以下是 merge 函数的总结:
-
left参数:这是第一个 DataFrame 对象。 -
right参数:这是第二个 DataFrame 对象。 -
how参数:这是连接的类型,可以是内连接、外连接、左连接或右连接。默认值为内连接。 -
on参数:此参数显示作为连接键的列名。 -
left_on和right_on参数:这两个参数显示左侧和右侧DataFrame中的列名,用于连接。 -
left_index和right_index参数:这些参数是布尔值。如果为True,则使用左侧或右侧DataFrame的索引/行标签进行连接。 -
sort参数:该参数是布尔值。默认的True设置会进行字典顺序排序。将其设置为False可能会提高性能。 -
suffixes参数:这是一个包含字符串后缀的元组,用于重叠列的命名。默认值为'_x'和'_y'。 -
copy参数:默认的True值会从传递的DataFrame对象中复制数据。
上述信息的来源是 pandas.pydata.org/pandas-docs/stable/merging.html。
我们来创建两个 DataFrame —— 左侧和右侧 —— 来理解合并操作:
left
以下是输出结果:

合并的左侧 DataFrame
可以使用以下方式查看右侧 DataFrame:
right
以下是输出结果:

合并的右侧 DataFrame
这两个 DataFrame 各有五行,Category 和 Region 作为连接键。在这五行数据中,每个 DataFrame 中有两行共享相同的连接键。我们来根据这两个键进行合并:
pd.merge(left, right, on = ["Category", "Region"])
以下是输出结果:

默认内连接
默认情况下,how 参数设置为 inner,因此在这种情况下会执行内连接。现在,我们来进行一个 left 连接:
pd.merge(left, right, how = "left", on = ["Category", "Region"])
以下是输出结果:

左连接
在左连接中,所有在左 DataFrame 中找到的行都会包含在结果中。那些在 right 中没有找到的 left 行,会将 right DataFrame 中的 Discount 和 Profit 列补充为 NAs,这些列在左 DataFrame 中找不到对应的键。右连接则恰好相反:结果将包含右 DataFrame 中的所有行,且在 left 中找到的键但在右 DataFrame 中找不到时,Sales 和 Quantity 列将被补充为 NAs:
pd.merge(left, right, how = "right", on = ["Category", "Region"])
以下是输出结果:

右连接
在外连接的情况下,不会排除任何行,缺失值将根据需要被补充为 NAs:
pd.merge(left, right, how = "outer", on = ["Category", "Region"])
以下是输出结果:

外连接
让我们来研究当找到键的重复项时,外连接的行为。以下命令会复制 left DataFrame 中最后一个键的组合。带有 Office Supplies 类别和 Canada 区域的键出现了两次:
left.loc[5,:] =["Office Supplies", "Canada", 111, 111]
left
以下是输出结果:

向左 DataFrame 插入重复项
外连接的结果如下:
pd.merge(left, right, how = "outer", on = ["Category", "Region"])
以下是输出结果:

带重复数据的外连接
如你所见,right DataFrame 在每次遇到键时会与 left DataFrame 合并,并且不会删除重复项。这种行为在处理大数据集时可能不太理想。在这种情况下,可能需要在合并之前删除重复项。对于这种情况,merge 的 validate 参数有助于检查并仅支持一对一的合并:
pd.merge(left, right, how = "outer", on = ["Category", "Region"], validate = "one_to_one")
以下是输出结果:

合并时指示 DataFrame 中重复项的错误
merge 的 indicator 参数表示行的来源——left、right 或两者都有:
pd.merge(left, right, how = "outer", on = ["Category", "Region"], indicator = "Indicator")
以下是输出结果:

merge 的 indicator 参数
合并函数
DataFrame.join 函数用于合并两个没有共同列的 DataFrame。实质上,这是对两个 DataFrame 进行纵向连接。以下是一个例子:
df_1 = sales_data.iloc[0:5, 0:3]
df_2 = sales_data.iloc[3:8, 3:6]
df_1.join(df_2)
以下是输出结果:

默认左连接
join与merge几乎相同,区别在于,merge适用于具有相同键的 DataFrame,而join是通过行索引来合并 DataFrame。默认情况下,join函数执行左连接。其他类型的连接可以通过how参数指定:
df_1.join(df_2, how = "right")
以下是输出结果:

右连接
内连接可以按如下方式执行:
df_1.join(df_2, how = "inner")
以下是输出结果:

内连接
外连接可以按如下方式执行:
df_1.join(df_2, how = "outer")
以下是输出结果:

外连接
如果要连接的两个 DataFrame 具有共同的列作为连接键,则可以在join函数的on参数中指定该键或键的列表。这与merge函数完全相同。
透视和重塑数据
本节讨论如何重塑数据。有时,数据以堆叠格式存储。以下是使用PlantGrowth数据集的堆叠数据示例:
In [344]: plantGrowthRawDF=pd.read_csv('./PlantGrowth.csv')
plantGrowthRawDF
Out[344]: observation weight group
0 1 4.17 ctrl
1 2 5.58 ctrl
2 3 5.18 ctrl
...
10 1 4.81 trt1
11 2 4.17 trt1
12 3 4.41 trt1
...
20 1 6.31 trt2
21 2 5.12 trt2
22 3 5.54 trt2
这些数据来自一项实验,实验比较了在对照(ctrl)和两种不同处理条件(trt1和trt2)下获得的植物干重产量。假设我们想根据组值对这些数据进行一些分析。实现此目标的一种方法是对数据框使用逻辑筛选:
In [346]: plantGrowthRawDF[plantGrowthRawDF['group']=='ctrl']
Out[346]: observation weight group
0 1 4.17 ctrl
1 2 5.58 ctrl
2 3 5.18 ctrl
3 4 6.11 ctrl
...
这可能会很繁琐,因此我们希望对这些数据进行透视/解堆叠,并以更有利于分析的形式展示它们。我们可以使用DataFrame.pivot函数如下操作:
In [345]: plantGrowthRawDF.pivot(index='observation',columns='group',values='weight')
Out[345]: weight
group ctrl trt1 trt2
observation
1 4.17 4.81 6.31
2 5.58 4.17 5.12
3 5.18 4.41 5.54
4 6.11 3.59 5.50
5 4.50 5.87 5.37
6 4.61 3.83 5.29
7 5.17 6.03 4.92
8 4.53 4.89 6.15
9 5.33 4.32 5.80
10 5.14 4.69 5.26
在这里,创建一个 DataFrame,其列对应于组的不同值,或者用统计学术语来说,就是因子的各个水平。
关于pivot的一些更多示例,使用salesdata.csv文件如下:
datastr=pd.read_csv('salesdata.csv')
table=pd.pivot_table(datastr,index=['Customer Segment'])# the aggregate values are average by default
以下是输出结果。这是所有列的结果:

如果我们指定columns参数并给定一个变量名,那么该变量的所有类别将成为单独的列:
table2=pd.pivot_table(datastr,values='Sales',index=['Customer Segment'],columns=['Region'])
例如,前面代码的输出如下所示:

也可以进行多级索引的透视,如下所示:
table4=pd.pivot_table(datastr,values='Sales',index=['Customer Segment','Ship Mode'],columns=['Region'])
以下是输出结果:

可以为聚合应用不同的聚合函数,或者使用自定义函数,示例如下:
table5=pd.pivot_table(datastr,values='Sales',index=['Customer Segment','Ship Mode'],columns=['Region'],aggfunc=sum)
以下是输出结果:

使用pivot_tables时需要记住的一些重要提示和技巧如下:
- 如果您预期透视表中有缺失值,则可以使用
fill.values=0:
table4=pd.pivot_table(datastr,values='Sales',index=['Customer Segment','Ship Mode'],columns=['Region'],fill_values=0)
- 如果您想在末尾得到总计,可以使用
margins=TRUE:
table4=pd.pivot_table(datastr,values='Sales',index=['Customer Segment','Ship Mode'],columns=['Region'],fill_values=0,margins=TRUE)
- 您可以为不同的数值列传递不同的聚合函数:
table6=pd.pivot_table(datastr,values=['Sales','Unit Price'],index=['Customer Segment','Ship Mode'],columns=['Region'],aggfunc={"Sales":sum,"Unit Price":len})
堆叠和解堆叠
除了透视函数外,stack和unstack函数也适用于 Series 和 DataFrame,它们可以作用于包含多重索引的对象。
stack()函数
堆叠时,一组列标签会转换为索引级别。为了进一步探索堆叠,让我们使用一个在行索引和列索引上都有多重索引的 DataFrame:
multi_df = sales_data[["Sales", "Quantity", "Category", "ShipMode"]].groupby(["Category", "ShipMode"]).agg([np.sum, np.mean])
multi_df
以下是输出结果:

层次数据用于堆叠和 unstack
应用stack()使得宽格式的 DataFrame 变得更长。让我们在前面的 DataFrame 上应用stack()。最后级别的列标签会添加到多重索引中:
multi_df.stack()
以下是输出结果:

堆叠的结果
stack()函数接受一个level参数。在这种情况下,默认的级别设置是1。让我们尝试在级别0上进行堆叠:
multi_df.stack(level = 0)
以下是输出结果:

使用level参数进行堆叠
在堆叠时,可以通过指定层级名称来代替指定层级编号。要堆叠多个级别,可以将层级名称或层级编号的列表传递给level参数。不过,列表不能同时包含层级名称和层级编号的组合:
multi_df.stack(level = [0,1])
以下是输出结果:

一次堆叠多个级别
让我们探索堆叠后索引的属性。DataFrame 的index属性帮助我们了解每个索引的不同级别、标签和名称:
multi_df.stack(level = 0).index
以下是输出结果:

堆叠后的索引属性
有时,当某个索引和列名的组合没有值时,堆叠操作会引入缺失值。考虑以下 DataFrame:
multicol = pd.MultiIndex.from_tuples([('Male', 'M'),
('Female', 'F')])
missing_info = pd.DataFrame([[20, None], [34, 78]],
index=['ClassA', 'ClassB'],
columns=multicol)
missing_info
以下是输出结果:

堆叠时处理缺失值
堆叠时,stack函数的dropna参数默认设置为True,它会自动丢弃所有的 NA:
missing_info.stack(dropna = False)
以下是输出结果:

堆叠时将dropna设置为 False
默认情况下,它会丢弃所有缺失值的行,如下所示:
missing_info.stack()
以下是输出结果:

默认情况下,堆叠时会丢弃 NA
unstack()函数
unstack函数执行stack函数的逆操作。它将长格式的 DataFrame 转换为宽格式。让我们对具有多重索引的销售数据进行 unstack 操作,默认情况下,最后一个级别会被 unstack:
multi_df.unstack()
以下是输出结果:

Unstack 操作
和stack一样,unstack也有一个level参数。这个level参数可以接受层级编号、层级名称或层级名称/层级编号的列表。
在解堆叠时创建的任何缺失值都可以使用 unstack 函数的 fill_value 参数进行处理。考虑以下 DataFrame:
multi_df.iloc[[0,5,6],[0,2]]
以下是输出结果:

数据快照
对前述 DataFrame 进行解堆叠会引入 NAs:
multi_df.iloc[[0,5,6],[0,2]].unstack()
以下是输出结果:

在不处理缺失数据的情况下进行解堆叠
我们可以使用 fill_value 方法用我们选择的值填充缺失的单元格。以下是缺失值已被替换为 0 的示例:
multi_df.iloc[[0,5,6],[0,2]].unstack(fill_value = 0)

在解堆叠时填充 NA 为 0
其他 DataFrame 重塑方法
还有其他多种与 DataFrame 重塑相关的方法,我们将在此讨论它们。
使用 melt 函数
melt 函数使我们能够通过指定某些列作为 ID 列来转换 DataFrame,确保它们作为列保持不变,而其余的非 ID 列被视为 变量 列并进行透视,最终形成一个名称-值两列的方案。ID 列唯一标识 DataFrame 中的一行。
这些非 ID 列的名称可以通过提供 var_name 和 value_name 参数来定制。使用 melt 的方法通过以下示例来说明,效果最好:
In [385]: from pandas.core.reshape import melt
In [401]: USIndexDataDF[:2]
Out[401]: TradingDate Nasdaq S&P 500 Russell 2000 DJIA
0 2014/01/30 4123.13 1794.19 1139.36 15848.61
1 2014/01/31 4103.88 1782.59 1130.88 15698.85
In [402]: melt(USIndexDataDF[:2], id_vars=['TradingDate'], var_name='Index Name', value_name='Index Value')
Out[402]:
TradingDate Index Name Index value
0 2014/01/30 Nasdaq 4123.13
1 2014/01/31 Nasdaq 4103.88
2 2014/01/30 S&P 500 1794.19
3 2014/01/31 S&P 500 1782.59
4 2014/01/30 Russell 2000 1139.36
5 2014/01/31 Russell 2000 1130.88
6 2014/01/30 DJIA 15848.61
7 2014/01/31 DJIA 15698.85
pandas.get_dummies() 函数
该函数用于将分类变量转换为指示符 DataFrame,实际上是分类变量可能值的真值表。以下命令是一个示例:
In [408]: melted=melt(USIndexDataDF[:2], id_vars=['TradingDate'], var_name='Index Name', value_name='Index Value') melted
Out[408]: TradingDate Index Name Index Value
0 2014/01/30 Nasdaq 4123.13
1 2014/01/31 Nasdaq 4103.88
2 2014/01/30 S&P 500 1794.19
3 2014/01/31 S&P 500 1782.59
4 2014/01/30 Russell 2000 1139.36
5 2014/01/31 Russell 2000 1130.88
6 2014/01/30 DJIA 15848.61
7 2014/01/31 DJIA 15698.85
In [413]: pd.get_dummies(melted['Index Name'])
Out[413]: DJIA Nasdaq Russell 2000 S&P 500
0 0 1 0 0
1 0 1 0 0
2 0 0 0 1
3 0 0 0 1
4 0 0 1 0
5 0 0 1 0
6 1 0 0 0
7 1 0 0 0
前述数据的来源是 vincentarelbundock.github.io/Rdatasets/csv/datasets/PlantGrowth.csv。
透视表
pandas 的 pivot_table 函数在多个方面比 pivot 函数更为高级。让我们讨论一些 pivot_table 函数的有趣参数:
-
data:要重塑的 DataFrame 对象 -
values:需要聚合的列或列的列表 -
index:用于分组的透视表索引的键 -
columns:用于分组的透视表列的键 -
aggfunc:用于聚合的函数,例如np.mean
让我们透视示例销售数据,在 Category 和 ShipMode 上切分 Sales。请注意,当 aggfunc 为空时,会计算均值:
pd.pivot_table(sales_data, values = "Sales", index = "Category", columns = "ShipMode")
以下是输出结果:

pandas 透视表
现在,values、index、column 或 aggfunc 可以有多个值。这些多个值可以作为列表传递。我们来计算 Sales 的 mean 和 Quantity 的 sum:
pd.pivot_table(sales_data, values = ["Sales", "Quantity"], index = "Category", columns = "ShipMode", aggfunc = {"Sales": np.mean, "Quantity": np.sum})
以下是输出结果:

带有多个聚合的透视表
通过pivot_table,可以创建具有层次化索引的 DataFrame。pivot_table函数的fill_value和dropna参数有助于处理缺失值。
pandas 中的转置
pandas 中的transpose函数与 NumPy 中的类似。它可以交换行和列。让我们找出以下 DataFrame 的转置:
sales_data.groupby("Category").sum()
以下是输出结果:

需要转置的数据
在 DataFrame 的转置中,列标签和行索引被交换:
sales_data.groupby("Category").sum().transpose()
以下是输出结果:

转置的输出
T作为transpose函数的访问器,可以像下面这样使用:
sales_data.groupby("Category").sum().T
Swaplevel and swapaxes
swaplevel函数有助于交换任何轴内的级别。考虑以下 DataFrame:
multi_df
以下是输出结果:

swaplevel 和 swapaxes 的数据
现在,让我们交换Category和ShipMode索引级别的位置。可以将级别的数字或名称作为参数提供:
multi_df.swaplevel(i = 1, j = 0, axis = 0)
以下是输出结果:

交换的级别
同样,您也可以通过将axis设置为1来交换列标签:
multi_df.swaplevel(i = 0, j = 1, axis = 1)
以下是输出结果:

沿轴 1 交换的级别
swapaxes函数在功能上与transpose函数类似。以下是swapaxes函数的示例:
multi_df.swapaxes(axis1 = 0, axis2 = 1)
以下是输出结果:

交换轴
压缩
squeeze有助于将一个 1D DataFrame 转换为 Series。让我们考虑一个 1D DataFrame:
dim1_df = sales_data[["Sales","OrderID"]].set_index("OrderID")
dim1_df
以下是输出结果:

压缩数据
前面对象的类型已被解码——它是一个 DataFrame:
type(dim1_df)
以下是输出结果:

压缩前的对象类型
现在,让我们应用squeeze函数并查找对象类型。所需的代码段和输出如下所示:
type(dim1_df.squeeze())

压缩后的对象类型
如您所见,squeeze将 DataFrame 转换为 Series。
nsmallest 和 nlargest
nsmallest和nlargest函数非常有用,可以在按指定列排序后返回 n 个最小和 n 个最大的行。
在示例销售数据中,让我们通过Profit找到3个最小记录:
sales_data.nsmallest(3, "Profit")
以下是输出结果:

图 6.73:nsmallest 函数的结果
排序也可以基于多个列进行,如下所示:
sales_data.nlargest(3, ["Quantity", "Profit"])
以下是输出结果:

nlargest 函数的结果
这些函数——nsmallest 和 nlargest——有一个 keep 参数,用于决定如何处理重复项。它有助于选择第一个出现的、最后一个出现的,或者保留所有重复项。
总结
本章为我们的 pandas 技巧库增添了新的内容,用于聚合、连接和转换数据。以下是本章的快速回顾:
-
groupby会创建一组行——每个类别变量中的一个组(或多个类别变量的组合)。 -
使用
groupby,可以高效地对不同组进行相同的分析。 -
形状相似的 DataFrame 可以连接或追加,以便同时对整个数据集进行分析。
-
也可以在 DataFrame 之间进行类似 SQL 的连接或合并操作。
-
宽格式数据可以根据需求转换为长格式数据,反之亦然。
-
pandas 能够处理多重索引数据,并且提供了将多重索引数据转换为单重索引数据及其反向操作的函数。
-
像数据透视表和转置这样的电子表格操作是可能的,并且提供了比电子表格更多的灵活性。
在下一章,我们将讨论并详细说明 pandas 中一些特殊数据操作的方法、语法和用法。
第七章:pandas 中的特殊数据操作
pandas 提供了一系列特殊操作符,用于生成、聚合、转换、读取和写入来自不同数据类型(如数字、字符串、日期、时间戳和时间序列)的数据。pandas 中的基本操作符已在前一章介绍。本章将继续这一讨论,并详细说明一些操作符的方法、语法和用法。
阅读本章后,你将能够自信地完成以下任务:
-
编写自定义函数并将其应用于列或整个 DataFrame
-
理解缺失值的性质并进行处理
-
使用函数转换和计算序列
-
对数据进行的其他数值操作
让我们立即深入探讨。大部分情况下,我们将生成自己的数据来演示这些方法。
本章将涵盖以下主题:
-
编写并应用一行自定义函数
-
处理缺失值
-
关于序列的方法调查
-
pandas 字符串方法
-
在 DataFrame 和序列上进行二元操作
-
对值进行分箱
-
在 DataFrame 上使用数学方法
编写并应用一行自定义函数
Python 提供了 lambda 函数,它是一种编写一行自定义函数的方法,使我们能够在 DataFrame 的列或整个 DataFrame 上执行某些任务。lambda 函数类似于使用 def 关键字定义的传统函数,但更加优雅,更适用于 DataFrame 列的应用,且语法简洁清晰,类似于在列表上实现 for 循环的列表推导式。让我们看看如何定义和应用 lambda 函数。
lambda 和 apply
为了查看 lambda 关键字如何使用,我们需要创建一些数据。我们将创建包含日期列的数据。处理日期列本身是一个话题,但我们将在这里简要了解这一过程。
在以下代码中,我们正在创建两列日期:
-
开始日期:从 2016-01-15 开始的 300 个连续日期
-
结束日期:从 2010 到 2025 年之间的任意一天随机选取的 300 个日期
以下代码块中使用了一些日期/时间方法来创建这些日期。请注意它们,并确保你理解它们:
### Importing required libraries
import datetime
import pandas as pd
from random import randint
### Creating date sequence of 300 (periods=300) consecutive days (freq='D') starting from 2016-01-15
D1=pd.date_range('2016-01-15',periods=300,freq='D')
### Creating a date sequence with of 300 days with day (b/w 1-30), month (b/w 1-12) and year (b/w 2010-2025) chosen at random
date_str=[]
for i in range(300):
date_str1=str(randint(2010,2025))+'-'+str(randint(1,30))+'- '+str(randint(3,12))
date_str.append(date_str1)
D2=date_str
### Creating a dataframe with two date sequences and call them as Start Date and End Date
Date_frame=pd.DataFrame({'Start Date':D1,'End Date':D2})
Date_frame['End Date'] = pd.to_datetime(Date_frame['End Date'], format='%Y-%d-%m')
输出的 DataFrame 有两列,如下所示:

输出的 DataFrame 包含 开始日期 和 结束日期
使用这些数据,我们将创建一些 lambda 函数来查找以下内容:
-
今天与开始日期或结束日期之间的天数
-
开始日期和结束日期之间的天数
-
开始日期或结束日期中早于给定日期的天数
在以下代码块中,我们编写了 lambda 函数来执行这些任务:
f1=lambda x:x-datetime.datetime.today()
f2=lambda x,y:x-y
f3=lambda x:pd.to_datetime('2017-28-01', format='%Y-%d-%m')>x
注意 x 和 y 被用作占位符参数,即函数的参数。在将这些函数应用于一列数据时,这些占位符会被列名替换。
Lambda 仅仅帮助定义一个函数。我们需要用实际参数来调用这些函数以执行它们。我们来看一下怎么做。例如,要执行我们之前定义的函数,我们可以这样做:
Date_frame['diff1']=Date_frame['End Date'].apply(f1)
Date_frame['diff2']=f2(Date_frame['Start Date'],Date_frame['End Date'])
Date_frame['Before 28-07-17']=Date_frame['End Date'].apply(f3)
以下将是输出结果:

输出包含日期列计算字段的 DataFrame
应当注意,这些函数可以这样调用:
-
像简单函数一样:使用函数名和必要的参数
-
使用 apply 方法:首先是 DataFrame 列名,接着是
apply,它将函数名作为参数
在这个例子中,map 也可以代替 apply 使用。尝试以下操作,并比较 diff1 和 diff3 的结果。它们应该是相同的:
Date_frame['diff3']=Date_frame['End Date'].map(f1)
有三个相关的方法,它们执行类似的工作,但有一些细微的区别:
| 名称 | 功能是什么? |
|---|---|
map |
对一列或一列列列表应用一个函数。 |
apply |
对列、行或列/行列表应用一个函数。 |
applymap |
对整个 DataFrame 应用一个函数,即对每个单元格应用。当函数可以作用于每一列时,将会生效。 |
以下是这些方法非常有用的一些使用场景:
假设数据集中的每一行代表一个零售公司每年每个 SKU 的日销售额,每一列代表一个 SKU。我们将把这个数据称为 sku_sales。让我们开始:
- 为了查找每个 SKU 的年销售额,我们将使用以下代码:
sku_sales.apply(sum,axis=0) # axis=0 represents summing across rows
- 为了找出每个 SKU 每天的销售额,我们将使用以下代码:
sku_sales.apply(sum,axis=1) # axis=1 represents summing across columns
- 为了找出
SKU1和SKU2的日均销售额,我们将使用以下代码:
sku_sales[['SKU1','SKU2']].map(mean)
- 为了找到所有 SKU 的每日销售额的均值和标准差,我们将使用以下代码:
sku_sales.applymap(mean)
sku_sales.applymap(sd)
现在,你将能够编写并应用单行的自定义 Lambda 函数。接下来,我们将研究如何处理缺失值。
处理缺失值
缺失值和 NAN 是数据集中常见的现象,在数据使用之前需要处理它们。我们将在接下来的部分中探讨缺失值的不同来源、类型以及如何处理它们。
缺失值的来源
缺失值可能在以下过程中进入数据集:
数据提取
这指的是数据是可用的,但在从源中提取时我们错过了它。它涉及到以下工程任务:
-
从网站抓取数据
-
从数据库查询
-
从平面文件提取数据
缺失值的来源有很多,以下是其中的一些:
-
正则表达式可能导致错误或非唯一的结果
-
错误查询
-
不同的数据类型存储
-
下载不完整
-
处理不完整
数据收集
这包括无法获取或难以收集的数据点。假设你正在调查 100,000 人拥有何种电动汽车。如果遇到一个不拥有电动汽车的人,那么该人的汽车类型就会缺失。
由于数据提取导致的缺失值,理论上可以通过识别导致缺失值的问题并重新运行提取过程来修正。而由于数据收集问题导致的缺失值则难以修正。
如何判断数据中是否有缺失值?最简单的方法是运行数据集摘要,它会给出行数的计数。由于包含缺失值的行不被计数,包含缺失值的列行数会较少。请看下面的图表,它展示了著名的titanic数据集的摘要,以说明这一点:

数据汇总表显示列计数差异,表示缺失值
年龄和体重列有缺失值,因为它们的行数比其他列少。
处理缺失值至关重要,因为它们会将缺失值传播到数值运算的结果中,可能导致错误的数据解释。缺失值会阻止许多数值计算的进行。如果只使用数据的一个样本,也可能导致错误的假设。
还有其他方式可以分类缺失值的来源。现在我们来逐一讲解。
随机缺失数据
在这种情况下,数据缺失没有特定原因。以电动汽车为例,缺失的汽车类型属于随机缺失数据的情况。
非随机缺失数据
在这种情况下,数据缺失可能有特定原因。继续使用之前的例子,假设在拥有汽车的人群中,某些车牌号缺失,因为在某个区域,他们的车牌使用了奇特的字体,导致 OCR 软件无法正确识别,结果返回缺失值。这就是非随机缺失数据的一个例子。
不同类型的缺失值
以下是不同类型的缺失值:
-
非数字(NaN):NaN 是任何数据类型缺失值的占位符。这些可以通过
numpy.nan来创建。使用numpy.nan创建的 NaN 可以分配给可空整数数据类型。整数类型的缺失值保存为 NaN。它是 Python 中缺失值的默认标识符。 -
NA:NA 主要来源于 R 语言,在 R 中,NA 是缺失值的标识符。
-
NaT:这相当于时间戳数据点的 NaN(非数字)。
-
None:表示非数值数据类型的缺失值。
-
Null:当函数没有返回值或值未定义时,产生 Null。
-
Inf: Inf是无穷大——一个大于任何其他数值的值。因此,
inf比任何其他值都要小。它是由所有计算产生的,导致非常大或非常小的值。通常,我们需要将inf视为缺失值。这可以通过在pandas中指定以下选项来完成:
pandas.options.mode.use_inf_as_na = True
也可以生成一个占位符的无穷大变量用于比较,如下所示:
import math
test = math.inf
test>pow(10,10) #Comparing whether Inf is larger than 10 to the power 10
它返回True。
缺失值的杂项分析
为了了解缺失值问题的严重程度,你可能想了解以下信息:
-
一列中有多少个单元格包含缺失值
-
哪些单元格在一列中有缺失值
-
哪些列有缺失值
这些任务可以如下执行:
- 查找包含缺失值的单元格:
pd.isnull(data['body']) *#*returns TRUE if a cell has missing values
pd.notnull(data['body']) *#*returns TRUE if a cell doesn't have missing values
- 查找一列中缺失值的数量:
pd.isnull(data['body']).values.ravel().sum() #returns the total number of missing values
pd.nottnull(data['body']).values.ravel().sum()#returns the total number of non-missing values
第三个问题留给你作为练习。
处理缺失值的策略
以下是处理缺失值的主要策略:
删除
这将删除包含缺失值的整行或整列。
删除会导致数据丢失,除非别无选择,否则不推荐使用。
删除可以如下执行:
- 删除所有单元格都包含缺失值的行:
data.dropna(axis=0,how='all')# axis=0 means along rows
- 删除所有单元格有缺失值的行:
data.dropna(axis=0,how='any')
插补
这将把缺失值替换为一个合理的数字。
插补可以通过多种方式进行。以下是其中一些方法:
- 用 0 插补数据集中所有缺失值:
data.fillna(0)
- 用指定文本插补所有缺失值:
data.fillna('text')
- 仅用 0 插补
body列中的缺失值:
data['body'].fillna(0)
- 使用非缺失值的均值进行插补:
data['age'].fillna(data['age'].mean())
- 使用向前填充进行插补——这种方法特别适用于时间序列数据。在这里,缺失值会被替换为前一行(周期)的值:
data['age'].fillna(method='ffill')
以下是输出结果:

输出 DataFrame,其中缺失值使用向前填充方法进行了插补
- 使用向后填充进行插补——这种方法特别适用于时间序列数据。在这里,缺失值会被替换为前一行(周期)的值。你可以通过
pad选项控制在第一个 NaN 之后填充的行数。Pad=1意味着只会填充 1 行:
data['age'].fillna(method='backfill')
以下是输出结果:

输出 DataFrame,其中缺失值使用向后填充方法进行了插补
插值
插值是一种技术,它使用连续缺失值两端的端点,创建一个粗略的数学关系来填补缺失值。默认情况下,它执行线性插值(假设数据点之间有线性关系),但还有许多其他方法,如多项式插值、样条插值、二次插值和 Akima 插值(假设数据关系是多项式或分段多项式关系)。
interpolate方法可以直接应用于序列或 DataFrame 中的所有列:
import numpy as np
import pandas as pd
A=[1,3,np.nan,np.nan,11,np.nan,91,np.nan,52]
pd.Series(A).interpolate()
以下是输出结果:

使用简单插值填充缺失值后的输出 DataFrame
相反,可以使用其他方法,例如spline,该方法假定分段多项式关系:
pd.Series(A).interpolate(method='spline',order=2)
以下是输出结果:

使用样条插值填充缺失值后的输出 DataFrame
类似地,可以进行多项式插值,如下所示:
pd.Series(A).interpolate(method='polynomial',order=2)
可以为每种插值方法在同一个 DataFrame 中创建不同的列,以比较它们的结果,如下所示:
#Needed for generating plot inside Jupyter notebook
%matplotlib inline
#Setting seed for regenerating the same random number
np.random.seed(10)
#Generate Data
A=pd.Series(np.arange(1,100,0.5)**3+np.random.normal(5,7,len(np.arange(1,100,0.5))))
#Sample random places to introduce missing values
np.random.seed(5)
NA=set([np.random.randint(1,100) for i in range(25)])
#Introduce missing values
A[NA]=np.nan
#Define the list of interpolation methods
methods=['linear','quadratic','cubic']
#Apply the interpolation methods and create a DataFrame
df = pd.DataFrame({m: A.interpolate(method=m) for m in methods})
#Find the mean of each column (each interpolation method)
df.apply(np.mean,axis=0)
以下是输出结果:

使用不同方法进行插值后的均值比较
如我们所见,每列的均值略有不同,因为使用了不同的插值方法。
你还可以检查插值的值,看看它们有多么不同/相似。可以按如下方式进行:
np.random.seed(5)
NA1=[np.random.randint(1,100) for i in range(25)]
df.iloc[NA1,:]
KNN
K-近邻(KNN)是一种无监督的基于局部的回归和分类方法。它将每行数据视为 n 维空间中的一个点,并根据它们之间的距离(例如,对于数值数据使用欧几里得距离,对于分类数据使用汉明距离)找到 k 个相似(邻近)点。为了找到该行和该列的值,它取所有邻近行在该列的平均值,并将平均值作为该值。
总结来说,可以说它定义了一个围绕某点的局部区域,并计算局部平均值,而不是全局平均值。大多数情况下,这种方法是合理的,通常用来代替全局平均值,因为邻域行为比样本点的全局行为更能逼近。
由于这个特性,KNN 可以用于填充缺失值。其直觉是,缺失值应与其邻近点的值相似。这是一种局部填充方法,与fillna方法(全局方法)不同。
可以使用 scikit-learn 中的kNeighborsClassifier或kNeighborsRegressor进行 KNN,并将结果用于填充。以下是一个插图示例,其中按照顺序发生以下操作:
-
生成了样本训练数据。
-
数据中引入了 NaN 值。
-
一个 KNN 模型在样本训练数据上进行了拟合。
-
拟合的模型用于预测/填充缺失值。
这在以下代码块中有所体现:
#Creating training dataset
A=np.random.randint(2,size=100)
B=np.random.normal(7,3,100)
C=np.random.normal(11,4,100)
X=(np.vstack((A,B,C))).T
#Creating testing data by replacing column A (outcome variable) with NaNs
X_with_nan = np.copy(X)
X_with_nan[:,0]=np.nan
# Load libraries
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
# Train KNN learner
clf = KNeighborsClassifier(3, weights='uniform')
trained_model = clf.fit(X[:,1:], X[:,0])
#Predicting/Imputing on test dataset
imputed_values = trained_model.predict(X_with_nan[:,1:])
imputed_values
你可以打印A和imputed_values,查看它们之间的差异,或评估填充值的准确性。以下截图显示了 A 列的实际值:

A 列的实际值
以下截图显示了 A 列的imputed_values:

A 列的填充值
一项关于序列方法的调查
我们来使用以下 DataFrame 来了解一些可以与系列一起使用的方法和函数:
sample_df = pd.DataFrame([["Pulp Fiction", 62, 46], ["Forrest Gump", 38, 46], ["Matrix", 26, 39], ["It's a Wonderful Life", 6, 0], ["Casablanca", 5, 6]], columns = ["Movie", "Wins", "Nominations"])
sample_df
以下是输出:

示例 DataFrame — IMDB 数据库
items() 方法
items() 方法提供了一种迭代访问系列或 DataFrame 中每一行的方式。它采用惰性求值,将每行的值与索引以元组的形式存储。可以通过诸如 for 循环等迭代过程获得这种惰性求值的结果。我们来在 DataFrame 的 Wins 列上应用 items 方法:
for item in sample_df["Wins"].items():
print(item)
以下是输出:

使用 items 方法进行循环
iteritems() 方法的行为与 items() 方法类似:
for item in sample_df["Wins"].iteritems():
print(item)

使用 iteritems 方法进行循环
items 和 iteritems 方法返回一个 zip 类型的对象。我们需要一个迭代过程来解压这个对象。在 DataFrame 上应用 items 或 iteritems 方法会得到不同的结果。在这种情况下,每一列会被堆叠在一个元组中,并带有列名:
for col in sample_df.items():
print(col)
以下是输出:

在 DataFrame 上使用 items 方法
keys() 方法
在系列中使用时,keys() 方法返回系列的行标签或索引,并且与访问 DataFrame 或系列的索引属性时执行的功能相同。keys() 方法在用于 DataFrame 和系列时表现不同;当用于 DataFrame 时,它返回列标签;而用于系列时,它返回行索引:
In: sample_df["Wins"].keys()
Out: RangeIndex(start=0, stop=5, step=1)
In: sample_df.keys()
Out: Index(['Movie', 'Wins', 'Nominations'], dtype='object')
pop() 方法
如果你熟悉 Python 中的列表,pop() 方法一定会让你有所了解。系列和 DataFrame 中的 pop() 方法与列表中的表现完全相同。它帮助我们从 DataFrame 中移除整列,或者从系列中移除特定行。调用 pop() 方法后,它会返回被弹出的实体(行或列)。
以下代码片段展示了如何在系列中使用 pop()。让我们从 "Wins" 列中弹出索引为 2 的项:
In: sample_df["Wins"].pop(2)
Out: 26
现在,让我们打印 Wins 系列:

pop 方法的输出
可以看到,索引 2 在系列中不再存在。同样的方法也可以应用于 DataFrame。让我们弹出 Nominations 列来理解这一点:
sample_df.pop("Nominations")
以下是输出:

在 DataFrame 上应用 Pop
以下命令用于显示弹出后的 DataFrame 结果:
sample_df
以下是输出:

弹出后的 DataFrame 结果
apply() 方法
apply() 方法为我们提供了一种快速高效的方式,将一个函数应用到系列中的所有值。这个函数可以是一个内置函数,比如 NumPy 函数,也可以是一个用户自定义的函数。
在以下代码片段中,apply() 被用来计算 Wins 序列中所有行的指数值:
sample_df["Wins"].apply(np.exp)
以下是输出:

在序列上使用 Apply 方法
你还可以定义自己的函数并将其应用于序列。让我们用一个简单的函数演示,将值除以 100:
def div_by_100(x):
return x/100
sample_df["Nominations"].apply(div_by_100)
以下是输出:

对用户定义函数应用 Apply 方法
map() 方法
map() 方法类似于 apply 方法,因为它有助于进行元素级别的修改,这些修改由函数定义。然而,map 函数除了这一点外,还接受一个序列或字典来定义这些元素级别的修改。
使用 map 函数,我们来更改 "Wins" 列中的一些值:
sample_df["Wins"].map({62 : 60, 38 : 20})
以下是输出:

在序列上使用 Map 方法
映射未定义的值将被替换为 NAs。
drop() 方法
drop() 方法是另一个有用的方法,用于删除 DataFrame 或序列中的整行或整列。可以沿任意轴删除索引 —— 行索引可以沿轴 0 删除,列索引可以沿轴 1 删除。
让我们从 Wins 序列中删除索引 0 和 2。要删除的索引可以通过列表定义。默认情况下,axis 设置为 0,因此在此情况下无需更改:
sample_df["Wins"].drop([0, 2])
以下是输出:

在序列上使用 Drop 方法
正如我们所看到的,结果中没有索引 0 和 2。以下代码片段展示了如何使用 drop 方法从 DataFrame 中删除列:
sample_df.drop(labels=["Wins", "Nominations"], axis = 1)
以下是输出:

在 DataFrame 上使用 drop 方法
当存在多级索引时,也可以有效地删除索引。drop 的 level 参数可以帮助我们做到这一点。请看以下具有多级索引的 DataFrame:

多级索引的 DataFrame
要从 Movie 列中删除特定的电影,level 参数应设置为 1。默认情况下,level 被设置为 0。以下代码片段移除了电影 Matrix:
multidf.drop(["Matrix"], level = 1 )
以下是输出:

删除层次索引的索引
equals() 方法
equals() 方法检查两个序列或 DataFrame 在值、数据类型和形状上是否相等。列标题可以具有不同的数据类型。输出结果是布尔值。此函数的一个实际应用如下所示。让我们创建一个新的序列,并将其与现有的 sample_df DataFrame 进行比较:
In: compare_series = pd.Series([62, 38, 26, 6, 5])
In: sample_df["Wins"].equals(compare_series)
Out: True
此函数可以这样应用,以比较两个 DataFrame 或同一个 DataFrame 中的两个序列。
sample() 方法
sample()方法可用于对 DataFrame 或系列进行随机采样。sample函数的参数支持在任意轴上进行采样,并且支持有放回或无放回的采样。进行采样时,必须指定要采样的记录数量或采样的记录比例。
让我们从Movie系列中采样三条记录:
sample_df["Movie"].sample(3)\
以下是输出结果:

适用于系列的sample函数
现在,让我们从 DataFrame 中随机采样 50%的列:
sample_df.sample(frac = 0.5, axis = 1)
以下是输出结果:

带有fraction参数的列采样
replace参数可以设置为True或False,从而可以选择是否进行有放回采样;默认值为False。random_state参数有助于设置随机数生成器的种子,它依赖于 NumPy 包的Random模块。
ravel()函数
ravel()函数将一系列数据展平为一维数组。它与numpy.ravel函数本质相似。ravel函数不能应用于 DataFrame:
In: sample_df["Wins"].ravel()
Out: array([62, 38, 26, 6, 5], dtype=int64)
value_counts()函数
value_counts()函数仅适用于系列,而不适用于 DataFrame。它统计每个变量出现的次数,并提供类似频率表的输出:
pd.Series(["Pandas", "Pandas", "Numpy", "Pandas", "Numpy"]).value_counts()
以下是输出结果:

类别系列的频次统计
value_counts()函数也可以应用于数值列。它将统计每个值出现的次数:
sample_df["Nominations"].value_counts()
以下是输出结果:

在数值列上使用的value_counts函数
统计数值在分箱范围内的出现更为有用。value_counts的bins参数会在统计前将数据分成不同的箱:
sample_df["Nominations"].value_counts(bins = 2)
以下是输出结果:

value_counts函数用于对numeric列进行分箱统计
interpolate()函数
interpolate()函数提供了一种有效处理缺失数据的方法。通过此方法,NaN 值可以通过线性插值、多项式插值或简单的填充进行替换。该函数将系列拟合到如spline或quadratic等函数上,然后计算可能缺失的数据。
考虑以下系列:
lin_series = pd.Series([17,19,np.NaN,23,25,np.NaN,29])
由于值是均匀分布的,因此线性插值是最合适的方法。线性插值是interpolate函数中method参数的默认值:
lin_series.interpolate()
以下是输出结果:

线性插值
可以指定插值应进行的方向。让我们考虑前面的例子,并通过后向填充来填充 NaN,如下所示:
lin_series.interpolate(method = "pad", limit_direction = "backward")
以下是输出结果:

带填充的反向插值
align() 函数
align() 函数接受两个对象,基于 join 条件(如内连接、外连接等)重新索引两个对象,并返回一个包含两个对象的元组:
s1 = pd.Series([5,6,7,8,9], index = ["a","b","c","d","e"])
s2 = pd.Series([1,2,3,4,5], index = ["d","b","g","f","a"])
s1.align(s2, join="outer")
以下是输出结果:

使用外连接对齐
由于对齐是基于外连接的,因此输出中显示了在两个系列中找到的索引。对于内连接,只有公共的索引会被返回,如 a、b 和 d:
s1.align(s2, join="inner")
以下是输出结果:

使用内连接对齐
pandas 字符串方法
本节讨论了 pandas 字符串方法。这些方法在处理混乱的文本数据时非常有用。它们可以清理文本数据、结构化数据、分割数据并搜索其中的重要部分。让我们了解这些方法,并找出它们各自包含的功能。
upper()、lower()、capitalize()、title() 和 swapcase()
字符串方法如 upper()、lower()、capitalize()、title() 和 swapcase() 在我们希望将所有字符串元素转换为整个系列时非常有用。upper 和 lower 方法将整个字符串转换为大写或小写。以下命令展示了将一系列数据转换为大写字母:
sample_df["Movie"].str.upper()
以下是输出结果:

将系列数据转换为大写字母
以下命令展示了将一系列数据转换为小写字母:
sample_df["Movie"].str.lower()
以下是输出结果:

将系列数据转换为小写字母
capitalize() 方法将第一个字母转换为大写,其他字母转换为小写:
pd.Series(["elon musk", "tim cook", "larry page", "jeff bezos"]).str.capitalize()
以下是输出结果:

针对系列数据的 Capitalize 函数
title() 方法确保字符串中的每个单词的首字母大写,其余字母小写:
pd.Series(["elon musk", "tim cook", "larry page", "jeff bezos"]).str.title()

字符串的标题大小写转换
swapcase() 方法将大写字母转换为小写字母,反之亦然:
sample_df["Movie"].str.swapcase()
以下是输出结果:

swapcase() 函数
contains()、find() 和 replace()
contains() 方法检查子串或模式是否存在于系列的所有元素中,并返回一个布尔值系列:
sample_df["Movie"].str.contains("atr")
以下是输出结果:

针对字符串类型系列的 Contains 函数
由于 Matrix 是唯一包含 atr 子串的电影,True 被返回在索引 2 处。子串也可以是正则表达式模式。要使用正则表达式模式进行字符串匹配,应将 regex 参数设置为 True。例如,我们可以查找包含 atr 或 der 的字符串:
sample_df["Movie"].str.contains("atr|der", regex = True)
以下是输出结果:

使用正则表达式的 Contains 函数
如我们所见,已经识别出两个匹配项。将case参数设置为True可以确保在模式匹配执行时区分大小写:
sample_df["Movie"].str.contains("cas", case = True)
以下是输出结果:

在contains函数中处理大小写敏感性
flags参数可用于指定任何正则表达式条件,例如忽略大小写:
import re
sample_df["Movie"].str.contains("MATrix", flags = re.IGNORECASE, regex=True)
以下是输出结果:

contains函数的正则表达式标志
请注意,在定义任何正则表达式标志之前,应先导入re包。
find函数返回可以找到子字符串的最小索引位置。考虑以下系列:
find_series = pd.Series(["abracadabra", "mad man"])
让我们使用find函数从之前的系列中获取ra子字符串的索引:
find_series.str.find("ra")
以下是输出结果:

一系列的find函数
在第一个元素abracadabra中,ra的第一次出现位于索引位置2。因此,返回2。第二个元素mad man没有找到匹配的字符串,因此返回-1。
find函数有一个start参数,可以用来指定从哪个最左侧的索引开始查找。相应地,还存在一个end参数,用于定义查找允许的最右侧索引。默认情况下,start设置为0,end设置为None:
find_series.str.find("a", start = 2)
以下是输出结果:

指定了起始限制的find函数
在前面的示例中,我们可以看到,通过指定起始索引a,索引 0 和 1 被忽略。
replace函数可以被视为contains函数的扩展,因为大多数参数是相似的。从功能上讲,replace在一系列字符串中查找子字符串并用替换字符串替换它。contains函数中也有的参数,如flags、case和regex,在这里也会出现,作用是相同的。让我们用rep子字符串替换系列中的字母I:
sample_df["Movie"].str.replace("i","'rep'")
以下是输出结果:

一系列的replace函数
请注意,如果i出现多次,则会进行多次替换。可以使用n参数控制替换的次数:
sample_df["Movie"].str.replace("i","'rep'", n = 1)
以下是输出结果:

带有指定替换次数的replace函数
strip()和split()
strip()函数在数据清理中非常有用。它可以从系列中的文本内容中删除尾部空格或任何特定的字符串模式。如果要删除的子字符串未指定,则默认会删除尾部空格。以下示例演示了strip函数在清理多余空格时的应用:
strip_series = pd.Series(["\tChina", "U.S.A ", "U\nK"])
strip_series
以下是输出结果:

含有多余空格的系列
以下示例演示了 strip() 函数在尾部空格中的应用:
strip_series.strip()
以下是输出结果:

去除尾部空格
这表明 strip() 仅移除尾部的空格,而不会移除中间的空格。现在,让我们使用 strip() 来移除特定字符串:
sample_df["Movie"].str.strip("opnf")
以下是输出结果:

用于移除字符串序列的 strip 函数
在前面的示例中,strip() 函数移除了系列元素尾部中的任何子字符串字符。
split() 函数按指定的分隔符拆分字符串。考虑以下系列:
split_series = pd.Series(["Black, White", "Red, Blue, Green", "Cyan, Magenta, Yellow"])
split_series
以下是输出结果:

拆分函数的示例系列
每个元素有两到三个项目,用 , 分隔。让我们用这个作为分隔符,将堆叠在每行中的项目分开:
split_series.str.split(", ")
以下是输出结果:

作为列表拆分
结果是每行的项目列表。expand() 参数为每个项目创建一个单独的列。默认情况下,expand 设置为 False,这会导致在每行中创建一个列表:
split_series.str.split(", ", expand = True)
以下是输出结果:

拆分多个列
startswith() 和 endswith()
虽然 contains() 函数帮助评估子字符串是否出现在系列的每个元素中,startswith() 和 endswith() 函数则分别专门检查子字符串是否出现在字符串的开始和结束位置:
start_series = pd.Series(["strange", "stock", "cost", "past", "state"])
start_series.str.startswith("st")
以下是输出结果:

startswith 函数
同样,endswith 可以像这样使用:
end_series= pd.Series(["ramen", "program", "cram", "rammed", "grammer"])
end_series.str.endswith("ram")
以下是输出结果:

endswith 函数
然而,与 contains() 不同,这些函数不接受正则表达式。
各种 is...() 函数
以下表格列出了帮助确认系列字符串元素其他属性的一组函数。例如,isupper 函数如果字符串中的所有字符都是大写字母,则返回 True。这些函数会返回与系列中每行对应的布尔值输出:
| 函数 | 当所有字符为...时返回真 |
|---|---|
isalnum() |
字母数字 |
isalpha() |
字母 |
isdigit() |
数字字符 |
isspace() |
空白字符 |
islower() |
小写字母 |
isupper() |
大写字母 |
istitle() |
标题大小写 |
isnumeric() |
数字 |
isdecimal() |
十进制 |
以下是前述函数的一些示例:
pd.Series(["ui26", "ui", "26"]).str.isalpha()
以下是输出结果:

isalpha 函数
这是 isalnum 函数的示例:
pd.Series(["ui26", "ui", "26"]).str.isalnum()
以下是输出结果:

isalnum 函数
这是isnumeric函数的示例:
pd.Series(["ui26", "ui", "26"]).str.isnumeric()
以下是输出结果:

isnumeric 函数
这些函数仅适用于字符串,不能应用于其他数据类型。与其他数据类型一起使用时,会返回NaN。
以下是isdigit()函数的示例:
pd.Series(["ui26", "ui", 26]).str.isdigit()
以下是输出结果:

isdigit 函数
对数据框和系列进行的二元操作
一些二元函数,如add、sub、mul、div、mod和pow,执行涉及两个数据框或系列的常见算术运算。
以下示例展示了两个数据框的加法。一个数据框的形状是(2,3),另一个的形状是(1,3)。add函数执行逐元素加法。当某个数据框中缺少对应的元素时,缺失的值会被填充为 NaN:
df_1 = pd.DataFrame([[1,2,3],[4,5,6]])
df_2 = pd.DataFrame([[6,7,8]])
df_1.add(df_2)
以下是输出结果:

对两个数据框进行逐元素相加
我们可以选择使用fill_value参数来填充 NaN,而不是直接使用 NaN。让我们通过mul函数进行乘法来探索这一点:
df_1.mul(df_2, fill_value = 0)
以下是输出结果:

pandas 中二元操作符的fill_value参数
用于算术运算的第二个值不一定需要是数据框或系列;它也可以是标量,如下所示:
df_1.sub(2)
以下是输出结果:

与标量的二元操作
在前面的例子中,两个数据框是统一索引的。元素可以在相同的索引标签下进行操作:
pd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd']).pow(pd.Series([4, 3, 2, 1], index=['a', 'b', 'd', 'e']))
以下是输出结果:

对具有不同索引的系列进行的二元操作
对于在两个系列中不存在的索引,返回NaN。在列标签方面也会表现出类似的行为。只有共享相同列名的元素才能一起使用。以下代码演示了这一点:
pd.DataFrame([[27, 33, 44]], columns=["a", "b", "c"]).mod(pd.DataFrame([[7, 6, 2]], columns=["b", "a", "d"])).
以下是输出结果:

对具有不同列的数据框进行的二元操作
现在,考虑以下两个数据框,其中一个显示了层次索引:
df = pd.DataFrame([[1, 4, 6], [np.NaN, 5, 3], [2, 7, np.NaN], [5, 9, 4], [1, np.NaN, 11]], columns = ["ColA", "ColB", "ColC"], index = ["a", "b", "c", "d", "e"])
df
以下是输出结果:

示例数据框
以下代码块是多重索引数据框的示例:
df_multi = df.iloc[0:4, :]
df_multi.index = pd.MultiIndex.from_tuples([(1, 'a'), (1, 'b'), (1, 'c'), (2, 'a')])
df_multi
以下是输出结果:

多重索引数据框
要将df除以df_multi的元素,或执行任何前述的二元操作,可以使用level参数来指定两个数据框共享的索引级别:
df.div(df_multi, level = 1)
以下是输出结果:

对具有层次索引的数据框使用的二元操作
lt、le、gt和ge函数通过建立“小于”、“小于或等于”、“大于”和“大于或等于”比较,帮助 DataFrame 进行比较。它们具有与我们之前讨论的函数相同的参数,并在所有这些场景中显示出类似的行为。我们来比较df和df_multi:
df.lt(df_multi, level = 1)
以下是输出:

小于函数
以下代码块展示了le函数:
df.le(df_multi, level = 1)
以下是输出:

小于或等于函数
round函数根据decimals参数指定的小数位数四舍五入:
pd.Series([6.78923, 8.02344, 0.1982]).round()
以下是输出:

round函数
默认情况下,会进行四舍五入操作,将输入值变成整数(decimals = 0):
pd.Series([6.78923, 8.02344, 0.1982]).round(2)
以下是输出:

设置小数位数的round函数
combine函数接受两个重叠的 DataFrame 并执行其中定义的函数。我们来合并两个序列并找到两者的最大值。注意,这里进行的是一个比较,以索引为参考:
pd.Series([9, 10, 11]).combine(pd.Series([7, 15, 12]), max)
以下是输出:

combine函数
分箱值
pandas cut()函数将值分箱到一个一维数组中。考虑以下包含 10 个值的一维数组。我们将其分为三个区间:
bin_data = np.array([1, 5, 2, 12, 3, 25, 9, 10, 11, 4])
pd.cut(bin_data, bins = 3)
以下是输出:

pandas cut函数与三个区间
这 10 个元素中的每个都映射到三个区间中的一个。cut函数将元素映射到一个区间,并提供每个区间的信息。除了指定区间数之外,还可以通过序列提供区间的边界:
pd.cut(bin_data, bins = [0.5, 7, 10, 20, 30])
以下是输出:

pandas cut函数与区间值
通过 pandas 的interval_range函数,可以直接定义分箱的区间。考虑以下示例,演示如何创建 pandas 的IntervalIndex对象:
interval = pd.interval_range(start = 0, end = 30, periods = 5)
interval
以下是输出:

pandas IntervalIndex
这个区间可以直接传递给cut函数:
pd.cut(bin_data, bins = interval)
以下是输出:

pandas cut函数与区间索引
设置right参数为True表示包括右区间,设置为False表示排除右区间。默认设置为True:
pd.cut(bin_data, bins = [0.5, 7, 10, 20, 30], right = False)
以下是输出:

开区间
等效地,include_lowest参数决定是否包含最小区间。默认情况下,它设置为False:
pd.cut(bin_data, bins = [0.5, 7, 10, 20, 30], include_lowest= True)
以下是输出:

包括左范围中的最低值
当 retbins 设置为 True 时,函数返回箱:
pd.cut(bin_data, bins = 5, retbins = True)
以下是输出:

返回箱
输出元组中的第二个值是箱值的数组。
可以通过将标签名称列表传递给 labels 参数来为箱分配标签:
pd.cut(bin_data, bins = 3, labels = ["level1", "level2", "level3"])
以下是输出:

标记箱
当传递的箱包含重复值时,默认情况下会引发错误。这是因为默认情况下重复参数设置为 raise。将其设置为 drop 将删除重复项:
pd.cut(bin_data, bins = [0.5, 7, 10, 30, 30], duplicates = "drop")
以下是输出:

处理箱中的重复项
可以通过 precision 参数设置创建和存储箱的精度:
pd.cut(bin_data, bins = 3, precision = 1)
以下是输出:

设置箱中的精度
qcut 函数类似于 cut 函数,唯一的区别在于可以通过指定基于哪些分位数创建箱来创建箱:
pd.qcut(bin_data, q = 5)
以下是输出:

qcut 函数
在 DataFrames 上使用数学方法
可以使用 pandas 库中内置的数学方法在 pandas DataFrames 上轻松执行诸如求和、平均值和中位数等计算。让我们利用销售数据的子集来探索 pandas 库中的数学函数和方法。在应用这些数学函数时,应确保所选列是数值型的。以下截图显示了具有五行和三列的数据,所有这些数据将在本节中使用:

样本销售数据
abs() 函数
abs() 函数返回 DataFrame 中记录的绝对值。对于形式为 x+yj 的复数值列,绝对值计算为
:
abs_df = pd.DataFrame({"Integers": [-1, -2, -3, 0, 2], "Complex": [5+2j, 1+1j, 3+3j, 2+3j, 4+2j]})
abs_df.abs()
以下是输出:

abs() 函数
corr() 和 cov()
corr() 函数返回 DataFrame 中每个变量组合的相关系数。如果存在任何 NAs,则将其排除在相关计算之外。corr() 函数接受 Pearson、Kendall 和 Spearman 方法。默认情况下,计算 Pearson 相关系数:
sales_df.corr()
以下是输出:

corr() 函数
就像相关函数一样,cov() 函数返回协方差矩阵:
sales_df.cov()
以下是输出:

cov() 函数
corr() 和 cov() 中的 min_periods 参数决定了 DataFrame 中非 NA 值的最小存在。
cummax()、cumin()、cumsum() 和 cumprod()
cummax()、cummin()、cumsum() 和 cumprod() 函数分别计算累计最大值、最小值、和、乘积。通过在示例数据框上应用 cummax() 函数来理解这一点:
sales_df.cummax()
以下是输出结果:

cummax() 函数
这些函数中的 skipna 参数提供了对处理缺失值(NAs)的控制。默认情况下,它设置为 True,即排除缺失值。考虑以下包含缺失值(NAs)的数据框,以了解此参数的功能:
sales_df_na
以下是输出结果:

包含缺失值的示例数据
cumsum() 方法可以如下所示应用:
sales_df_na.cumsum()
以下是输出结果:

cumsum() 函数
我们可以选择不忽略缺失值(NAs)来进行累计求和,通过将 skipna 设置为 False:
sales_df_na.cumsum(skipna=False)
以下是输出结果:

跳过缺失值的累计函数
默认情况下,聚合是沿行轴进行的,因为 axis 参数默认设置为 0。通过使用 axis 参数,也可以沿列轴执行累计聚合:
sales_df.cumprod(axis = 1)
以下是输出结果:

cumprod() 函数
describe() 函数
describe() 函数提供数据分布的表示,并计算一些有用的汇总统计信息。它对于探索性数据分析(EDA)技术非常有用:
sales_df.describe()
以下是输出结果:

describe() 函数
describe() 函数可以应用于数值型和类别型变量。describe 函数的 include 和 exclude 参数设置了该函数应评估的数据类型。默认情况下,include 设置为 numeric,因此数据框中的任何类别型变量都会被忽略。我们通过将 include 参数设置为 object 数据类型,来应用 describe 到以下数据框:
sales_df_full
以下是输出结果:

包含混合数据类型的示例数据
请查看以下内容:
sales_df_full.describe(include = np.object)
以下是输出结果:

类别型变量的 describe() 函数
通过将 include 设置为 all 可以包含所有数据类型。类似地,也可以使用 exclude 参数来排除某些数据类型。
对于数值变量,describe 函数会评估 0.25、0.5、0.75 和 1 的分位数。可以按如下方式自定义:
sales_df.describe(percentiles = [0.1, 0.2, 0.3, 0.4, 0.5])
以下是输出结果:

在 describe 函数中使用自定义分位数
diff() 函数
diff() 函数计算同一列中的后续行之间的差异或同一行中后续列之间的差异。通过设置默认为 0 的 axis 参数,可以沿着行或列评估 diff()。因此,计算是按行进行的:
sales_df.diff()
以下是输出:

diff() 函数
可以应用 diff 方法如下所示:
sales_df.diff(axis = 1)
以下是输出:

沿轴 1 应用的 diff()函数
periods() 参数可用于找到第 n 个前一行的差异。负值允许我们找到接下来的第 n 行的差异:
sales_df.diff(periods = 2)
以下是输出:

在不同周期间隔使用 diff()
rank() 函数
rank() 函数返回一个 DataFrame,其中包含沿指定轴估计的每个值的排名。默认情况下,排名是按升序进行的:
sales_df.rank()
以下是输出:

排名函数结果
rank() 方法可以如下应用:
sales_df.rank(ascending = False)
以下是输出:

降序排名
排名也可以以百分比的形式获得,如下所示:
sales_df.rank(pct = True)
以下是输出:

百分位数排名
method() 参数有助于解决平局。默认情况下,显示了平局中可能被项目占据的排名范围的平均值。也可以修改为显示最小排名、最大排名、值出现的顺序或密集排名:
sales_df.rank(method = "min")
以下是输出:

使用排名方法找到平局的最小值
quantile() 函数
quantile() 函数返回指定分位数的每列的值。它接受单个分位数值或分位数值数组:
sales_df.quantile(q = [0.1, 0.9])
以下是输出:

查找 DataFrame 的分位数
时间戳也可以计算分位数。我们可以通过将 numeric_only 参数设置为 False 来实现这一点:
time_col = pd.DataFrame({"time": pd.date_range("2017-01-01", "2017-12-31")})
time_col.quantile(q = [0.1, 0.5], numeric_only = False)
以下是输出:

日期时间值的分位数
round() 函数
round() 函数用于四舍五入小数。默认情况下,值会被四舍五入为整数:
sales_df.round()
以下是输出:

round() 函数的结果
可以应用 round 函数如下所示:
sales_df.round(decimals = 10)
以下是输出:

decimals = 10 的 round() 函数
pct_change() 函数
pct_change() 函数类似于 diff 函数,计算 DataFrame 中两个不同值之间的百分比差异。就像在 diff() 中,periods 参数提供了灵活性,可以评估之间相隔几行的不同元素:
sales_df.pct_change()
以下是输出结果:

跨行的百分比变化
fill_method 参数允许通过诸如填充等方法在计算前处理 NA 值。limit 参数帮助设置 NA 值的允许数量阈值。
min()、max()、median()、mean() 和 mode()
这些函数接受类似的参数集,并根据轴参数设置计算每列或每行的聚合(最小值、最大值、中位数或众数):
sales_df.min()
以下是输出结果:

min() 结果
max 方法可以按照如下方式应用:
sales_df.max(axis = 1)
以下是输出结果:

max() 结果
skipna 参数帮助我们处理 NA。考虑以下 DataFrame:
sales_df_na
以下是输出结果:

带有 NA 的 DataFrame
默认情况下,评估时会跳过 NA 值,因为 skipna 参数设置为 True:
sales_df_na.median()
以下是输出结果:

median() 函数
默认情况下,NA 值在均值计算中会被忽略。如果 skipna 设置为 False,则计算结果也会因为缺失值而返回 NA:
sales_df_na.median(skipna = False)
以下是输出结果:

带有 skipna 的 median() 函数
考虑以下多重索引 DataFrame。让我们计算它的均值:
multileveldf
以下是输出结果:

多重索引 DataFrame
这个多重索引数据集的均值可以通过如下方式获得:
multileveldf.mean()
以下是输出结果:

多重索引 DataFrame 的均值
level 参数计算多重索引 DataFrame 中任意级别的聚合:
multileveldf.mean(level = 0)
以下是输出结果:

针对特定索引级别的 mean()
all() 和 any()
all() 和 any() 函数帮助我们检测 DataFrame 中是否存在 False 值或零值。如果所选轴上的所有值都是 True,则 all() 函数返回 True。any() 函数需要至少一个值为 True 才返回 True。让我们在以下 DataFrame 上应用 all() 和 any():
all_any = pd.DataFrame({"A": [True, False, False, True, True], "B": [1, 1, 1, 1, 1], "C": [10, 11, 20, 22, 33], "D": ["abc", "xyz", "pqr", "ijk", "def"], "E": [False, False, False, False, False]})
all_any
这会得到以下输出:

示例 DataFrame
B、C 和 D 列中的所有值都是 True,因此 all() 对这些列返回 True。没有 True 值的 E 列则会在 any() 中返回 False:
all_any.all()
以下是输出结果:

all() 结果
同样,any() 可以如下应用:
all_any.any()
以下是输出结果:

any() 结果
all() 和 any() 函数具有 axis、skipna 和 level 参数,像我们之前讨论的某些函数一样。bool_only 参数可以用于包括或排除布尔值以外的数据类型:
all_any.all(bool_only = True)
以下是输出结果:

bool_only 参数
clip() 函数
clip() 函数指定了下限和上限。DataFrame 中超过上限的值将被减少到上限,低于下限的值将被提高到下限:
sales_df.clip(8, 3000)
以下是输出结果:

clip() 结果
count() 函数
count() 函数帮助统计 DataFrame 中非 NA 值的总数:
sales_df_na
以下是输出结果:

带有 NA 的 DataFrame
count 方法可以如下应用:
sales_df_na.count()
以下是输出结果:

count() 结果
count() 函数具有 axis、level 和 numeric_only 参数,像我们之前讨论的几个其他函数一样。
小结
本章提供了一些特殊方法,展示了 pandas 的灵活性和实用性。本章就像一本插图词典,每个函数都有非常独特的用途。现在,你应该对如何在 pandas 中创建和应用单行函数有了一定了解,并且理解了缺失值的概念以及处理它们的方法。这也是一本汇总了可以应用于序列的各种杂项方法和可以应用于任何 Python 数据结构的数值方法的大全。
在下一章,我们将看看如何处理时间序列数据并使用 matplotlib 绘制图表。我们还将通过观察滚动、重采样、偏移、滞后和时间元素分离来了解如何操作时间序列数据。
第八章:使用 Matplotlib 的时间序列和绘图
时间序列数据由多种过程生成,包括物联网(IoT)传感器、机器/服务器日志以及来自客户关系管理(CRM)系统的月度销售数据。时间序列数据的一些常见特点是数据点以固定频率生成,并且数据中固有的趋势和季节性。
在本章中,我们将介绍一些在使用 pandas 时必须掌握的主题。掌握这些主题的知识对于准备作为数据分析、预测或可视化程序输入的数据非常有用。
本章将讨论的主题如下:
-
处理时间序列数据和日期
-
时间序列数据的操作——滚动、重采样、平移、滞后和时间元素分离
-
格式化——更改日期格式并将文本转换为日期
-
使用
matplotlib绘制时间序列
本章结束时,你应该在这些关键领域掌握得很熟练。
处理时间序列数据
在本节中,我们将展示如何处理时间序列数据。处理涉及读取、创建、重采样和重新索引时间戳数据。要使其可用,需要对时间戳数据执行这些任务。我们将首先展示如何使用从csv文件读取的数据创建时间序列数据。
读取时间序列数据
在本节中,我们将演示多种读取时间序列数据的方法,从简单的read_csv方法开始:
In [7]: ibmData=pd.read_csv('ibm-common-stock-closing-prices-1959_1960.csv')
ibmData.head()
Out[7]: TradeDate closingPrice
0 1959-06-29 445
1 1959-06-30 448
2 1959-07-01 450
3 1959-07-02 447
4 1959-07-06 451
5 rows 2 columns
此信息的来源可以在datamarket.com找到。
我们希望TradeDate列成为一系列datetime值,以便可以索引并创建时间序列:
- 让我们首先检查
TradeDate系列中的值类型:
In [16]: type(ibmData['TradeDate'])
Out[16]: pandas.core.series.Series
In [12]: type(ibmData['TradeDate'][0])
Out[12]: str
- 接下来,我们将这些值转换为
Timestamp类型:
In [17]: ibmData['TradeDate']=pd.to_datetime(ibmData['TradeDate'])
type(ibmData['TradeDate'][0])
Out[17]: pandas.tslib.Timestamp
- 现在我们可以将
TradeDate列用作索引:
In [113]: #Convert DataFrame to TimeSeries
#Resampling creates NaN rows for weekend dates,
hence use dropna
ibmTS=ibmData.set_index('TradeDate').resample('D' ['closingPrice'].dropna()
ibmTS
Out[113]: TradeDate
1959-06-29 445
1959-06-30 448
1959-07-01 450
1959-07-02 447
1959-07-06 451
...
Name: closingPrice, Length: 255
在下一节中,我们将学习如何将日期列作为索引,然后基于该索引进行子集提取。为了这一节,我们将使用Object Occupancy数据集,该数据集记录了某些房间参数在几周内每隔几分钟的观测值以及相应的房间占用情况。该数据集分为三个单独的文件。
在时间序列数据中分配日期索引并进行子集提取
让我们读取它们并将其连接成一个文件:
import pandas as pd
import os os.chdir(' ')
ts1=pd.read_csv('datatraining.txt')
ts2=pd.read_csv('datatest.txt')
ts3=pd.read_csv('datatest2.txt')
ts=pd.concat([ts1,ts2,ts3]
在将日期列用作索引之前,我们将其转换为datetime格式并删除实际的日期列:
ts['datetime'] = pd.to_datetime(ts['date'])
ts = ts.set_index('datetime')
ts.drop(['date'], axis=1, inplace=True)
一旦新的datetime列设置为索引,它就可以用于子集提取。例如,要过滤某一天的所有记录,我们只需将数据放入子集提取(方括号[])中:
ts['2015-02-05']
输出结果类似于以下截图:

过滤某一天的所有记录
要过滤所有天数中特定小时的所有记录,以下代码片段可以完成此任务:
ts[ts.index.hour==4]
以下是输出结果:

过滤某一特定小时的所有记录
我们还可以使用以下代码片段过滤两个时间戳之间的所有记录:
ts['2015-02-05':'2015-02-06']
以下是输出结果:

过滤两个时间戳之间的所有记录
绘制时间序列数据
为了更好地理解数据中的趋势和任何季节性变化,可以使用基本的绘图函数进行绘制。在这里,数据集的湿度和 CO[2]变量已被绘制出来:
ts.plot(y="Humidity",style='.',figsize=(15,1))
ts.plot(y="CO2",style='.',figsize=(15,1))
以下是输出结果:

使用 matplotlib 在同一图表中绘制湿度和 CO2 水平随时间变化
时间序列数据的重新采样和滚动
重新采样意味着改变观察到的时间序列的频率。例如,在此数据集中,每隔几秒钟观察到一个数据点。该数据集可以重新采样为按小时的频率,其中每小时的所有数据点将使用选定的聚合函数进行聚合,最终形成一个小时的一个数据点。也可以在每日层面进行重新采样,将一天中的所有数据点聚合。重新采样也可以看作是数据平滑,因为它平滑或平均化数据中的波动。
在 pandas 中,重新采样时间序列数据非常简单,因为有一个内置函数可以实现这一点。让我们看看如何使用它。
例如,要按小时进行重新采样,我们编写以下代码:
ts[["Humidity"]].resample("1h").median().plot(figsize=(15,1))
以下是输出结果:

使用中位数作为聚合度量值,按小时重新采样数据
同样地,要按天重新采样,我们编写以下代码:
ts[["Humidity"]].resample("1d").median().plot(figsize=(15,1))
以下是输出结果:

使用中位数作为聚合度量值,按天重新采样数据
请注意,按小时采样的数据比按天采样的数据变化更大,后者更加平滑。
滚动也是一种类似的概念,用于聚合数据点,尽管它更加灵活。可以提供一个滚动窗口,即聚合的数据显示点数量,以控制聚合或平滑的程度。
如果仔细查看datetime列,你会发现每分钟观察到一个数据点。因此,60 个这样的数据点构成一个小时。让我们看看如何使用滚动方法来聚合数据。
对于滚动 60 个数据点,从每个数据点开始作为一个记录,我们提供 60 作为滚动窗口,如下所示。这应该会返回一个类似于之前按小时重新采样的图表:
ts[["Humidity"]].rolling(60).median().plot(figsize=(15,1))
以下是输出结果:

滚动每 60 个连续数据点并聚合它们,最终给出中位数作为最终值
对于按天滚动,滚动窗口应该是60 x 24:
ts[["Humidity"]].rolling(60*24).median().plot(figsize=(15,1))
以下是输出结果:

每 60*24 个连续点进行滚动并聚合,给出其中位数作为最终值;这相当于为分钟级别数据找到日聚合值
请注意,已经使用中位数进行聚合。你也可以使用其他函数,如均值或总和。
分离时间戳组件
一个时间戳对象由多个组件组成,即年、月、日、小时、分钟和秒。对于许多时间序列分析来说,将这些组件分离并作为新列保存以备后用是非常重要的。
由于我们已经将日期列设置为索引,因此变得更加简便。可以如下创建每个组件的单独列:
ts['Year']=ts.index.year
ts['Month']=ts.index.month
ts['Day']=ts.index.day
ts['Hour']=ts.index.hour
ts['Minute']=ts.index.minute
ts['Second']=ts.index.second
以下是输出结果:

作为单独列创建的时间序列组件
DateOffset 和 TimeDelta 对象
DateOffset对象表示时间上的变化或偏移。DateOffset对象的主要特点如下:
-
这可以加到/从
datetime对象中减去,以获得平移后的日期。 -
这可以与一个整数(正数或负数)相乘,以便多次应用增量。
-
它具有
rollforward和rollback方法,可以将日期向前移动到下一个偏移日期,或者向后移动到上一个偏移日期。
让我们使用pandas中的datetime方法创建一些日期对象:
In [371]: xmasDay=pd.datetime(2014,12,25)
xmasDay
Out[371]: datetime.datetime(2014, 12, 25, 0, 0)
In [373]: boxingDay=xmasDay+pd.DateOffset(days=1)
boxingDay
Out[373]: Timestamp('2014-12-26 00:00:00', tz=None)
In [390}: today=pd.datetime.now()
today
Out[390]: datetime.datetime(2014, 5, 31, 13, 7, 36, 440060)
请注意,datetime.datetime与pd.Timestamp不同。前者是 Python 类且效率较低,而后者是基于numpy.datetime64数据类型的。
pd.DateOffset对象与pd.Timestamp一起使用,将其加到datetime.datetime函数上,会将该对象转换为pd.Timestamp对象。
以下命令演示了从今天起的 1 周:
In [392]: today+pd.DateOffset(weeks=1)
Out[392]: Timestamp('2014-06-07 13:07:36.440060', tz=None)
以下命令演示了从今天起的 5 年:
In [394]: today+2*pd.DateOffset(years=2, months=6)
Out[394]: Timestamp('2019-05-30 13:07:36.440060', tz=None)
下面是使用rollforward功能的一个例子。QuarterBegin是一个DateOffset对象,用于将给定的datetime对象递增到下一个日历季度的开始:
In [18]: lastDay=pd.datetime(2013,12,31)
In [24]: from pandas.tseries.offsets import QuarterBegin
dtoffset=QuarterBegin()
lastDay+dtoffset
Out[24]: Timestamp('2014-03-01 00:00:00', tz=None)
In [25]: dtoffset.rollforward(lastDay)
Out[25]: Timestamp('2014-03-01 00:00:00', tz=None)
因此,我们可以看到 2013 年 12 月 31 日后的下一个季度从 2014 年 3 月 1 日开始。Timedelta类似于DateOffset,但它与datetime.datetime对象一起使用。使用这些对象的方法通过以下命令进行了说明:
In [40]: weekDelta=datetime.timedelta(weeks=1)
weekDelta
Out[40]: datetime.timedelta(7)
In [39]: today=pd.datetime.now()
today
Out[39]: datetime.datetime (2014, 6, 2, 3, 56, 0, 600309)
In [41]: today+weekDelta
Out[41]: datetime.datetime (2014, 6, 9, 3, 56,0, 600309)
到目前为止,我们已经学习了数据类型、数据类型之间的转换、日期偏移、从时间戳中分离时间组件等内容。接下来,我们将看到如何应用一些数学运算符,例如滞后、平移等。
时间序列相关的实例方法
本节中,我们将探索各种时间序列对象的操作方法,如平移、频率转换和重采样。
平移/滞后
有时,我们可能希望将时间序列中的值向前或向后移动。一种可能的场景是,当数据集包含公司中新员工的入职日期列表时,公司的 HR 程序希望将这些日期向前移动一年,以便激活员工的福利。我们可以通过以下方式使用 shift() 函数来实现:
In [117]: ibmTS.shift(3)
Out[117]: TradeDate
1959-06-29 NaN
1959-06-30 NaN
1959-07-01 NaN
1959-07-02 445
1959-07-06 448
1959-07-07 450
1959-07-08 447
...
这会偏移所有日历天数。然而,如果我们只希望偏移工作日,我们必须使用以下命令:
In [119]: ibmTS.shift(3, freq=pd.datetools.bday)
Out[119]: TradeDate
1959-07-02 445
1959-07-03 448
1959-07-06 450
1959-07-07 447
1959-07-09 451
在前面的代码片段中,我们指定了 freq 参数来进行偏移;这告诉函数仅偏移工作日。shift 函数有一个 freq 参数,其值可以是 DateOffset 类、类似 TimeDelta 的对象或偏移别名。因此,使用 ibmTS.shift(3, freq='B') 也会产生相同的结果。
频率转换
时间序列通常具有固定的频率,例如每微秒、每秒、每分钟等。这些频率可以相互转换。
我们可以使用 asfreq 函数来更改频率,如以下代码片段所示:
In [131]: # Frequency conversion using asfreq
ibmTS.asfreq('BM')
Out[131]: 1959-06-30 448
1959-07-31 428
1959-08-31 425
1959-09-30 411
1959-10-30 411
1959-11-30 428
1959-12-31 439
1960-01-29 418
1960-02-29 419
1960-03-31 445
1960-04-29 453
1960-05-31 504
1960-06-30 522
Freq: BM, Name: closingPrice, dtype: float64
在这种情况下,我们只获取 ibmTS 时间序列中与每月最后一天对应的值。这里,bm 代表工作月末频率。有关所有可能频率别名的列表,请访问 pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases。
如果我们指定的频率小于数据的粒度,空缺值将会填充为 NaN:
In [132]: ibmTS.asfreq('H')
Out[132]: 1959-06-29 00:00:00 445
1959-06-29 01:00:00 NaN
1959-06-29 02:00:00 NaN
1959-06-29 03:00:00 NaN
...
1960-06-29 23:00:00 NaN
1960-06-30 00:00:00 522
Freq: H, Name: closingPrice, Length: 8809
我们还可以将 asfreq 方法应用于 Period 和 PeriodIndex 对象,类似于在 datetime 和 Timestamp 对象上的操作。Period 和 PeriodIndex 是稍后引入的,用于表示时间区间。
asfreq 方法接受一个方法参数,允许你使用前向填充(ffill)或后向填充来填补空缺,类似于 fillna:
In [140]: ibmTS.asfreq('H', method='ffill') Out[140]: 1959-06-29 00:00:00 445 1959-06-29 01:00:00 445 1959-06-29 02:00:00 445 1959-06-29 03:00:00 445 ... 1960-06-29 23:00:00 522 1960-06-30 00:00:00 522 Freq: H, Name: closingPrice, Length: 8809
数据重采样
TimeSeries.resample 函数使我们能够基于采样间隔和采样函数对更精细的数据进行汇总/聚合。
下采样是一个源自数字信号处理的术语,指的是降低信号采样率的过程。在数据处理中,我们使用它来减少我们希望处理的数据量。
相反的过程是上采样,它用于增加需要处理的数据量,并且需要插值来获得中间数据点。
关于下采样和上采样的更多信息,请参考 上采样和下采样的实际应用,链接为 bit.ly/1JC95HD 以及 为视觉表示进行时间序列下采样,链接为 bit.ly/1zrExVP。
在这里,我们查看一些逐笔数据,以用于重采样。在查看数据之前,我们需要先准备它。通过这一步,我们将学习一些关于时间序列数据的有用技巧,具体如下:
-
纪元时间戳
-
时区处理
下面是一个使用 2014 年 5 月 27 日星期二谷歌股票的逐笔数据的示例:
In [150]: googTickData=pd.read_csv('./GOOG_tickdata_20140527.csv')
In [151]: googTickData.head()
Out[151]: Timestamp close high low open volume
0 1401197402 555.008 556.41 554.35 556.38 81100
1 1401197460 556.250 556.30 555.25 555.25 18500
2 1401197526 556.730 556.75 556.05 556.39 9900
3 1401197582 557.480 557.67 556.73 556.73 14700
4 1401197642 558.155 558.66 557.48 557.59 15700
5 rows 6 columns
前述数据的来源可以在 chartapi.finance.yahoo.com/instrument/1.0/GOOG/chartdata;type=quote;range=1d/csv 找到。
如您在前面的代码中看到的,我们有一个 Timestamp 列,以及表示谷歌股票的收盘价、最高价、最低价、开盘价和交易量的列。
那么,为什么 Timestamp 列看起来有点奇怪呢?其实,逐笔数据的时间戳通常以纪元时间表示(有关更多信息,请参见 en.wikipedia.org/wiki/Unix_epoch),这是一种更紧凑的存储方式。我们需要将其转换为更易于阅读的时间格式,可以按如下方式进行转换:
In [201]: googTickData['tstamp']=pd.to_datetime(googTickData['Timestamp'],unit='s',utc=True)
In [209]: googTickData.head()
Out[209]:
Timestamp close high low open volume tstamp
0 14011974020 555.008 556.41 554.35 556.38 81100 2014-05-27 13:30:02
1 1401197460 556.250 556.30 555.25 555.25 18500 2014-05-27 13:31:00
2 1401197526 556.730 556.75 556.05 556.39 9900 2014-05-27 13:32:06
3 1401197582 557.480 557.67 556.73 556.73 14700 2014-05-27 13:33:02
4 1401197642 558.155 558.66 557.48 557.59 15700 2014-05-27 13:34:02
5 rows 7 columns
现在我们想将 tstamp 列设置为索引,并删除纪元的 Timestamp 列:
In [210]: googTickTS=googTickData.set_index('tstamp')
googTickTS=googTickTS.drop('Timestamp',axis=1)
googTickTS.head()
Out[210]: close high low open volume
tstamp
2014-05-27 13:30:02 555.008 556.41 554.35 556.38 811000
2014-05-27 13:31:00 556.250 556.30 555.25 555.25 18500
2014-05-27 13:32:06 556.730 556.75 556.05 556.39 9900
2014-05-27 13:33:02 557.480 557.67 556.73 556.73 14700
2014-05-27 13:34:02 558.155 558.66 557.48 557.59 15700
5 rows 5 columns
请注意,tstamp 索引列的时间是 世界协调时间 (UTC),我们可以通过两个操作符 tz_localize 和 tz_convert 将这些时间转换为美国东部时间:
In [211]: googTickTS.index=googTickTS.index.tz_localize('UTC').tz_convert('US/Eastern')
In [212]: googTickTS.head()
Out[212]: close high low open volume
tstamp
2014-05-27 09:30:02-04:00 555.008 556.41 554.35 556.38 81100
2014-05-27 09:31:00-04:00 556.250 556.30 555.25 555.25 18500
2014-05-27 09:32:06-04:00 556.730 556.75 556.05 556.39 9900
2014-05-27 09:33:02-04:00 557.480 557.67 556.73 556.73 14700
2014-05-27 09:34:02-04:00 558.155 558.66 557.48 557.59 15700
5 rows 5 columns
In [213]: googTickTS.tail()
Out[213]:
close high low open volume
tstamp
2014-05-27 15:56:00-04:00 565.4300 565.48 565.30 565.385 14300
2014-05-27 15:57:00-04:00 565.3050 565.46 565.20 565.400 14700
2014-05-27 15:58:00-04:00 565.1101 565.31 565.10 565.310 23200
2014-05-27 15:59:00-04:00 565.9400 566.00 565.08 565.230 55600
2014-05-27 16:00:00-04:00 565.9500 565.95 565.95 565.950 126000
5 rows 5 columns
In [214]: len(googTickTS)
Out[214]: 390
从之前的输出(**Out[213]**)中,我们可以看到交易日中每分钟的逐笔数据——从早上 9:30,股市开盘,到下午 4:00,股市闭盘。由于从 9:30 到 16:00 有 390 分钟,所以数据集中有 390 行。
假设我们希望每 5 分钟获取一次快照,而不是每分钟一次?我们可以通过降采样来实现,如下所示:
In [216]: googTickTS.resample('5Min').head(6)
Out[216]: close high low open volume
tstamp
2014-05-27 09:30:00-04:00 556.72460 557.15800 555.97200 556.46800 27980
2014-05-27 09:35:00-04:00 556.93648 557.64800 556.85100 557.34200 24620
2014-05-27 09:40:00-04:00 556.48600 556.79994 556.27700 556.60678 8620
2014-05-27 09:45:00-04:00 557.05300 557.27600 556.73800 556.96600 9720
2014-05-27 09:50:00-04:00 556.66200 556.93596 556.46400 556.80326 14560
2014-05-27 09:55:00-04:00 555.96580 556.35400 555.85800 556.23600 12400
6 rows 5 columns
重采样时默认使用的函数是均值。然而,我们也可以指定其他函数,例如最小值,并且可以通过 how 参数进行重采样。
In [245]: googTickTS.resample('10Min', how=np.min).head(4)
Out[245]: close high low open volume
tstamp
2014-05-27 09:30:00-04:00 555.008 556.3000 554.35 555.25 9900
2014-05-27 09:40:00-04:00 556.190 556.5600 556.13 556.35 3500
2014-05-27 09:50:00-04:00 554.770 555.5500 554.77 555.55 3400
2014-05-27 10:00:00-04:00 554.580 554.9847 554.45 554.58 1800
可以将各种函数名称传递给 how 参数,例如 sum、ohlc、max、min、std、mean、median、first 和 last。
ohlc 函数返回时间序列数据的开盘、最高、最低和收盘值,分别是第一值、最大值、最小值和最后值。要指定左侧或右侧区间是否闭合,我们可以传递 closed 参数,如下所示:
In [254]: pd.set_option('display.precision',5) googTickTS.resample('5Min', closed='right').tail(3) Out[254]: close high low open volume tstamp 2014-05-27 15:45:00-04:00 564.3167 564.3733 564.1075 564.1700 12816.6667 2014-05-27 15:50:00-04:00 565.1128 565.1725 565.0090 565.0650 13325.0000 2014-05-27 15:55:00-04:00 565.5158 565.6033 565.3083 565.4158 40933.3333 3 rows 5 columns
因此,在前面的命令中,我们可以看到最后一行显示的是 15:55 的逐笔数据,而不是 16:00。
对于上采样,我们需要通过 fill_method 参数指定填充方法,以确定如何填充间隙:
In [263]: googTickTS[:3].resample('30s', fill_method='ffill')
Out[263]: close high low open volume
tstamp
2014-05-27 09:30:00-04:00 555.008 556.41 554.35 556.38 81100
2014-05-27 09:30:30-04:00 555.008 556.41 554.35 556.38 81100
2014-05-27 09:31:00-04:00 556.250 556.30 555.25 555.25 18500
2014-05-27 09:31:30-04:00 556.250 556.30 555.25 555.25 18500
2014-05-27 09:32:00-04:00 556.730 556.75 556.05 556.39 9900
5 rows 5 columns
In [264]: googTickTS[:3].resample('30s', fill_method='bfill')
Out[264]:
close high low open volume
tstamp
2014-05-27 09:30:00-04:00 555.008 556.41 554.35 556.38 81100
2014-05-27 09:30:30-04:00 556.250 556.30 555.25 555.25 18500
2014-05-27 09:31:00-04:00 556.250 556.30 555.25 555.25 18500
2014-05-27 09:31:30-04:00 556.730 556.75 556.05 556.39 9900
2014-05-27 09:32:00-04:00 556.730 556.75 556.05 556.39 9900
5 rows 5 columns
fill_method 参数目前仅支持两种方法——forwardfill 和 backfill。不过,也可以支持插值方法,具体方法会有所不同。
时间序列频率的别名
要指定偏移量,可以使用许多别名;以下是一些最常用的别名:
-
B, BM:表示工作日,工作月。这是指一个月的工作日,即任何不是假期或周末的日子。
-
D, W, M, Q, A:分别表示日历日、周、月、季度和年末。
-
H, T, S, L, U:分别表示小时、分钟、秒、毫秒和微秒。
这些别名也可以组合使用。在以下的例子中,我们以每 7 分钟 30 秒进行重采样:
In [267]: googTickTS.resample('7T30S').head(5)
Out[267]:
close high low open volume
tstamp
2014-05-27 09:30:00-04:00 556.8266 557.4362 556.3144 556.8800 28075.0
2014-05-27 09:37:30-04:00 556.5889 556.9342 556.4264 556.7206 11642.9
2014-05-27 09:45:00-04:00 556.9921 557.2185 556.7171 556.9871 9800.0
2014-05-27 09:52:30-04:00 556.1824 556.5375 556.0350 556.3896 14350.0
2014-05-27 10:00:00-04:00 555.2111 555.4368 554.8288 554.9675 12512.5
5 rows x 5 columns
可以将后缀应用于频率别名,以指定在频率周期中何时开始。这些被称为锚定偏移量:
-
W – SUN, MON, ... 示例:W-TUE 表示每周频率,从星期二开始。
-
Q – JAN, FEB, ... DEC 示例:Q-MAY 表示一个季度频率,并且该季度的年末是 5 月。
-
A – JAN, FEB, ... DEC 示例:A-MAY 表示一个年度频率,并且该年度的年末是 5 月。
这些偏移量可以作为参数用于 date_range 和 bdate_range 函数,以及用于 PeriodIndex 和 DatetimeIndex 等索引类型的构造函数。关于这一点的详细讨论可以在 pandas 文档中找到,网址为 pandas.pydata.org/pandas-docs/stable/timeseries.html#。
时间序列概念和数据类型
处理时间序列时,必须考虑两个主要概念:时间范围中的点和时间跨度。在 pandas 中,前者由Timestamp数据类型表示,它等价于 Python 的datetime.datetime(datetime)数据类型,并且可以互换使用。后者(时间跨度)由Period数据类型表示,这是 pandas 特有的。
每种数据类型都有与之关联的索引数据类型:Timestamp/Datetime 对应 DatetimeIndex,Period 对应 PeriodIndex。这些索引数据类型基本上是 numpy.ndarray 的子类型,包含对应的 Timestamp 和 Period 数据类型,可以用作 Series 和 DataFrame 对象的索引。
Period 和 PeriodIndex
Period 数据类型用于表示时间范围或时间跨度。以下是一些示例:
# Period representing May 2014
In [287]: pd.Period('2014', freq='A-MAY')
Out[287]: Period('2014', 'A-MAY')
# Period representing specific day - June 11, 2014
In [292]: pd.Period('06/11/2014')
Out[292]: Period('2014-06-11', 'D')
# Period representing 11AM, Nov 11, 1918
In [298]: pd.Period('11/11/1918 11:00',freq='H')
Out[298]: Period('1918-11-11 11:00', 'H')
我们可以向 Period 数据类型添加整数,以按所需的频率单位数量推进时间周期:
In [299]: pd.Period('06/30/2014')+4
Out[299]: Period('2014-07-04', 'D')
In [303]: pd.Period('11/11/1918 11:00',freq='H') - 48
Out[303]: Period('1918-11-09 11:00', 'H')
我们还可以计算两个 Period 数据类型之间的差异,并返回它们之间的频率单位数量:
In [304]: pd.Period('2014-04', freq='M')-pd.Period('2013-02', freq='M')
Out[304]: 14
PeriodIndex
PeriodIndex 是一个用于 Period 对象的索引类型,可以通过两种方式创建:
- 你可以通过使用
period_range函数从一系列Period对象中创建一个类似于date_range的对象:
In [305]: perRng=pd.period_range('02/01/2014','02/06/2014',freq='D')
perRng
Out[305]: <class 'pandas.tseries.period.PeriodIndex'>
freq: D
[2014-02-01, ..., 2014-02-06]
length: 6
In [306]: type(perRng[:2])
Out[306]: pandas.tseries.period.PeriodIndex
In [307]: perRng[:2]
Out[307]: <class 'pandas.tseries.period.PeriodIndex'>
freq: D
[2014-02-01, 2014-02-02]
正如我们从前面的命令中确认的,当你展开 PeriodIndex 时,它实际上是一个包含Period对象的ndarray。
- 也可以通过直接调用
Period构造函数来实现:
In [312]: JulyPeriod=pd.PeriodIndex(['07/01/2014','07/31/2014'], freq='D')
JulyPeriod
Out[312]: <class 'pandas.tseries.period.PeriodIndex'>
freq: D
[2014-07-01, 2014-07-31]
从前面的输出可以看出,这两种方法之间的区别在于,period_range 会填充结果中的 ndarray,而 Period 构造函数不会,你必须指定所有应包含在索引中的值。
时间序列数据类型之间的转换
我们可以通过 to_period 和 to_timestamp 函数将 Period 和 PeriodIndex 数据类型转换为 Datetime/Timestamp 和 DatetimeIndex 数据类型,如下所示:
In [339]: worldCupFinal=pd.to_datetime('07/13/2014',
errors='raise')
worldCupFinal
Out[339]: Timestamp('2014-07-13 00:00:00')
In [340]: worldCupFinal.to_period('D')
Out[340]: Period('2014-07-13', 'D')
In [342]: worldCupKickoff=pd.Period('06/12/2014','D')
worldCupKickoff
Out[342]: Period('2014-06-12', 'D')
In [345]: worldCupKickoff.to_timestamp()
Out[345]: Timestamp('2014-06-12 00:00:00', tz=None)
In [346]: worldCupDays=pd.date_range('06/12/2014',periods=32,
freq='D')
worldCupDays
Out[346]: <class 'pandas.tseries.index.DatetimeIndex'>
[2014-06-12, ..., 2014-07-13]
Length: 32, Freq: D, Timezone: None
In [347]: worldCupDays.to_period()
Out[347]: <class 'pandas.tseries.period.PeriodIndex'>
freq: D
[2014-06-12, ..., 2014-07-13]
length: 32
在前面的示例中,注意如何将周期转换为时间戳,反之亦然。
时间序列相关对象的总结
pandas 中有许多与时间序列相关的对象,用于操作、创建和处理时间戳数据。以下表格总结了这些时间序列相关的对象:
| 对象 | 总结 |
|---|---|
datetime.datetime |
这是一个标准的 Python datetime 类。 |
Timestamp |
这是一个派生自 datetime.datetime 的 pandas 类。 |
DatetimeIndex |
这是一个 pandas 类,实现为不可变的 numpy.ndarray 类型,元素为 Timestamp/datetime 对象。 |
Period |
这是一个表示时间段的 pandas 类。 |
PeriodIndex |
这是一个 pandas 类,实现为不可变的 numpy.ndarray 类型,元素为 Period 对象。 |
DateOffset |
DateOffset 用于将日期向前移动一定数量的有效日期(天、周、月等)。 |
timedelta |
Timedelta 计算两个日期之间的时间差。 |
字符串和时间戳之间的相互转换
考虑以下包含表示日期的字符串列和包含数值的列的 DataFrame:
ts_df = pd.DataFrame({"ts_col": ["2013-01-01", "2015-02-10", "2016-10-24"], "value": [5, 6, 9]})
ts_df
以下是输出结果:

创建一个包含日期列的 DataFrame
可以看到,时间序列列的数据类型是对象而不是时间戳。以下代码及其输出确认了这一点:
ts_df.dtypes
以下是输出结果:

to_datetime 函数有助于将字符串转换为 datetime:
ts_df["ts_col"] = pd.to_datetime(ts_df["ts_col"], format = "%Y-%m-%d")
ts_df.dtypes
以下是输出结果:

将字符串格式的日期列转换为 datetime 格式
pandas 的 to_datetime 函数根据字符串的格式将字符串列转换为 datetime。该函数的 infer_datetime_format 参数可以自动检测格式并将字符串解析为 datetime。当将 exact 参数设置为 False 时,它会查找最接近的匹配格式,帮助解决格式无法完全匹配的情况。
如下所示的转换也可以使用来自 datetime 库的 strptime 函数来完成:
import datetime as dt
ts_df["ts_col"] = ts_df["ts_col"].apply(lambda x:
dt.datetime.strptime(x,'%Y-%m-%d'))
从 datetime 转换为字符串可以借助 strftime 函数,该函数接受输出字符串的格式:
ts_df["ts_col"] = ts_df["ts_col"].dt.strftime("%d/%m/%Y")
ts_df
以下是输出结果:

将日期列的 datetime 格式转换为字符串格式
在这里,原始的datetime值是%Y-%m-%d格式。然而,strftime函数允许格式更改。
字符串和datetime之间的相互转换也可以通过astype()方法实现,具体如下:
ts_df["ts_col"] = ts_df["ts_col"].astype("datetime64[ns]")
ts_df["ts_col"] = ts_df["ts_col"].astype("object")
时间序列数据的数据处理技术
本节处理的是在应用机器学习技术之前,常用于时间序列数据的常见数据处理或特征工程技术。
数据转换
请考虑以下单个数据集的各个部分:
ts_complete_df = pd.read_csv("sensor_df.csv")
以下截图显示了包含不等长时间序列组件的传感器数据头部:

包含不等长时间序列组件的传感器数据头部
以下截图显示了包含不等长时间序列组件的传感器数据尾部:

包含不等长时间序列组件的传感器数据尾部
这里的数据集由 314 个不同设备的 10 分钟间隔时间序列数据组成。所有这些 314 个设备的数据捕获持续时间各不相同。我们来看看每个设备的数据捕获持续时间:
ts_complete_df.groupby("ID").size().describe()
以下是输出结果:

传感器数据摘要
每个设备的数据长度差异很大。像 Shapelet 变换和长短期记忆(LSTM)等多个时间序列问题要求每个设备的数据长度相同。以下代码片段将每个设备的数据截断到可能的最大长度:
truncate_df = pd.DataFrame()
min_len = ts_complete_df.groupby("ID").size().min()
for i in range(1,315):
df = ts_complete_df[ts_complete_df["ID"] == i].iloc[0:min_len, :]
truncate_df = truncate_df.append(df)
截断后,长度已统一。可以通过运行以下代码进行验证:
truncate_df.groupby("ID").size().describe()
以下是输出结果:

所有时间序列组件长度统一后的传感器数据摘要
让我们对以下单变量时间序列数据进行特征提取:
ts = pd.read_csv("D:datatest.txt").iloc[:,0:2].set_index("date") ts
以下是输出结果:

读取占用数据并将日期时间列设置为索引
特征提取对于使用时间序列数据进行机器学习至关重要,以便获得更好的性能指标。在这里,我们提取温度数据的滚动均值、滚动标准差和梯度:
feat_ext = pd.concat([ts.rolling(5).mean(), ts.rolling(5).std(), (ts - ts.shift(-5))/ts], axis=1).iloc[5:,:]
feat_ext.columns = ['5_day_mean', '5_day_std', '5_day_gradient']
feat_ext.head(5)
以下是输出结果:

使用滚动函数生成特征(5_day_mean, 5_day_std)
在特征提取过程中,前五行包含NA值的数据已被删除。在此,特征已被提取用于 5 天的滚动窗口。使用类似的方法,可以从时间序列变量中提取数百个特征。
使用 matplotlib 绘图
本节简要介绍了如何使用matplotlib在pandas中绘图。matplotlib的 API 是通过标准惯例导入的,如下所示的命令:
In [1]: import matplotlib.pyplot as plt
Series和DataFrame都有一个绘图方法,它实际上是plt.plot的一个封装。这里,我们将讨论如何绘制正弦和余弦函数的简单图形。假设我们希望在区间从π到π绘制以下函数:
-
f(x) = cos(x) + sin (x)
-
g(x) = cos (x) - sin (x)
这将给出以下区间:
In [51]: import numpy as np
In [52]: X = np.linspace(-np.pi, np.pi, 256,endpoint=True)
In [54]: f,g = np.cos(X)+np.sin(X), np.sin(X)-np.cos(X)
In [61]: f_ser=pd.Series(f)
g_ser=pd.Series(g)
In [31]: plotDF=pd.concat([f_ser,g_ser],axis=1)
plotDF.index=X
plotDF.columns=['sin(x)+cos(x)','sin(x)-cos(x)']
plotDF.head()
Out[31]: sin(x)+cos(x) sin(x)-cos(x)
-3.141593 -1.000000 1.000000
-3.116953 -1.024334 0.975059
-3.092313 -1.048046 0.949526
-3.067673 -1.071122 0.923417
-3.043033 -1.093547 0.896747
5 rows × 2 columns
我们现在可以使用plot()命令绘制 DataFrame,并使用plt.show()命令显示它:
In [94]: plotDF.plot()
plt.show()
We can apply a title to the plot as follows:
In [95]: plotDF.columns=['f(x)','g(x)']
plotDF.plot(title='Plot of f(x)=sin(x)+cos(x), \n g(x)=sinx(x)-cos(x)')
plt.show()
以下是前述命令的输出:

使用 matplotlib 绘制时间序列数据
我们还可以分别在不同的子图中绘制这两个序列(函数),使用以下命令:
In [96]: plotDF.plot(subplots=True, figsize=(6,6))
plt.show()
以下是前述命令的输出:

使用 matplotlib 绘制更多时间序列数据
在 pandas 中使用matplotlib的绘图功能还有很多其他用法。欲了解更多信息,请查看文档:pandas.pydata.org/pandas-docs/dev/visualization.html。
可视化多变量时间序列数据中的所有变量通常非常有用。让我们在单一图表中绘制以下数据的所有变量。请注意,date列在此处是索引:

占用数据集
matplotlib 中的子图功能允许我们一次绘制所有变量:
axes = plot_ts.plot(figsize=(20,10), title='Timeseries Plot', subplots=True, layout=(plot_ts.shape[1],1), xticks = plot_ts.index)
# Get current axis from plot
ax = plt.gca()
import matplotlib.dates as mdates
# Set frequency of xticks
ax.xaxis.set_major_locator(mdates.DayLocator(interval = 1))
# Format dates in xticks
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
plt.show()
以下是输出结果:

占用数据集中所有变量的时间序列图
总结
本章讨论了时间序列数据及其处理和操作步骤。可以将date列指定为Series或DataFrame的索引,然后可以根据索引列对子集进行操作。时间序列数据可以重新采样——既可以增加也可以减少时间序列的频率。例如,每毫秒生成的数据可以重新采样为每秒捕获一次数据,或者每秒的 1,000 毫秒可以取平均值。类似地,每分钟生成的数据可以通过向前或向后填充重新采样为每秒一次(使用该分钟的最后一个或下一个分钟的值填充所有秒数据)。
字符串到日期时间的转换可以通过datetime、strptime和strftime包来完成,每种日期输入格式(例如,22^(nd) July、7/22/2019 等)需要根据约定采用不同的解码方式。pandas 有以下类型的时间序列对象——datetime.datetime、Timestamp、DateIndex、Period、PeriodIndex、timedelta等。某些时间序列分类算法,如形状特征(shapelets)和 LSTM,需要时间序列组件(一个可分离的数据实体,包含多个时间序列数据条目)的长度相同。这可以通过将所有组件截断为最小长度,或者扩展到最大长度并用零或其他值进行填充来实现。Matplotlib 可以用于绘制基本的时间序列数据。移位、滞后和滚动函数用于计算移动平均,检测时间序列组件变动点的行为变化。
在下一章,我们将学习如何在 Jupyter Notebooks 中利用 pandas 的强大功能来制作强大且互动的报告。
第四章:使用 pandas 更进一步
现在我们已经了解了 pandas 在数据处理方面提供的基础和高级功能,是时候更进一步,探索如何充分利用 pandas 生成有效的洞察力,挖掘更多的可能性。
本节包括以下章节:
-
第九章,在 Jupyter 中使用 pandas 制作强大的报告
-
第十章,使用 pandas 和 NumPy 进行统计分析
-
第十一章,贝叶斯统计与最大似然估计简要介绍
-
第十二章,使用 Pandas 的数据案例研究
-
第十三章,pandas 库架构
-
第十四章,与其他工具对比 pandas
-
第十五章,机器学习简要介绍
第十章:使用 pandas 在 Jupyter 中制作强大的报告
pandas 和 Jupyter Notebook 可用于创建格式化良好的输出、报告和/或教程,且这些内容易于与广泛的受众共享。在本章中,我们将研究多种样式的应用以及 pandas 提供的格式化选项。我们还将了解如何在 Jupyter Notebook 中创建仪表板和报告。
本章将涵盖以下主题:
-
pandas 样式
-
浏览 Jupyter Notebook
-
使用 Jupyter Notebooks 制作报告
pandas 样式
pandas 允许在 DataFrame 上执行多种操作,使得处理结构化数据变得更加容易。DataFrame 的另一个有趣的特性是,它们允许我们格式化和设置常规行列的样式,这些样式有助于提高表格数据的可读性。Dataframe.style 方法返回一个 Styler 对象。所有需要在显示 DataFrame 之前应用的格式化可以在这个 Styler 对象上进行。样式设置可以使用内置函数,这些函数为格式化提供了预定义的规则,也可以使用用户自定义的规则。
让我们考虑以下 DataFrame,以便查看 pandas 的样式属性:
df = pd.read_csv("titanic.csv")
df
以下截图展示了前述的 DataFrame 已加载到 Jupyter Notebook 中:

DataFrame 加载到 Jupyter Notebook 中
让我们来看一下如何为 Jupyter Notebook 元素设置样式。
内置样式选项
pandas 具有预定义的格式化规则,这些规则作为函数编写并存储,能够方便地使用。
highlight_null 方法会用指定的颜色高亮数据中的所有 NaN 或 Null 值。在讨论的 DataFrame 中,Age 和 Cabin 列有 NaN 值。因此,在以下截图中,这些列中的 NaN 值会以蓝色标记。
以下代码片段高亮显示了这些列中的 NaN 值:
df.style.highlight_null(null_color = "blue")
这将产生以下输出:

图 9.2:用蓝色高亮显示 Null 和 NaN
highlight_max 和 highlight_min 方法分别对最大值或最小值应用高亮(使用选定的颜色),并可以跨任一轴进行操作。在以下示例中,每一列中的最小值已被高亮显示:
df.iloc[0:10, :].style.highlight_max(axis = 0)
请注意,只有数值类型的列才会被高亮显示。
以下截图高亮显示了每一列的最大值:

图 9.3:跨行高亮显示最大值(在数值列中),使用黄色标记
在前面的代码中,highlight_max 被用来高亮显示每一列中的最大值。
接下来,我们使用相同的函数来查找每一列的最大值,同时更改轴参数的值:
df.style.highlight_max(axis = 1)
以下截图显示了跨列的最大值已被高亮显示:

使用黄色突出显示每列中的最大值(在数值列中)
现在,让我们使用highlight_min函数用自定义颜色突出显示最小值。highlight_min和highlight_max的语法相同,接受相同的参数集:

使用绿色突出显示最小值
基于条件格式化的背景颜色渐变可以应用于列,以便通过颜色表现出高、中、低值的区别。背景颜色会根据值的高低使用不同的颜色。
表格的背景渐变可以通过background_gradient()样式函数进行控制。可以使用任何现有的颜色映射或用户定义的颜色映射作为渐变。像low和high这样的参数可以帮助我们使用颜色映射的部分颜色范围。此外,还可以设置axis和subset参数来沿某一轴和某些列子集变化渐变:
df.style.background_gradient(cmap='plasma', low = 0.25, high = 0.5)
这将得到以下输出:

为每个数字列分别创建背景颜色渐变,基于其高低值
样式也可以独立于数值进行设置。我们来修改属性以改变字体颜色、背景颜色和边框颜色。你可以通过以下代码来实现。
df.style.set_properties(**{'background-color': 'teal',
'color': 'white',
'border-color': 'black'})
这将得到以下输出:

为输出的 DataFrame 改变背景色、字体颜色、字体类型和字体大小
样式选项还帮助我们控制数值精度。请查看以下 DataFrame:

DataFrame 中的数字不进行精度四舍五入
看一下以下代码,它将精度设置为 2 位小数,或者将数字四舍五入到 2 位小数。
rand_df.style.set_precision(2)
这将得到以下输出:

DataFrame 中的数字四舍五入到 2 位小数
现在,我们来为前面的 DataFrame 设置一个标题:
rand_df.style.set_precision(2).set_caption("Styling Dataframe : Precision Control")
这将得到以下输出:

DataFrame 的数字四舍五入到 2 位小数,并添加表格标题
set_table_styles函数也可以用来独立于数据修改表格。它接受一个table_styles列表。每个table_style应为一个包含选择器和属性的字典。table_styles可以用来定义基于动作的自定义样式。例如,以下样式将所选单元格的背景色设置为lawngreen:
df.style.set_table_styles([{'selector': 'tr:hover','props': [('background-color', 'lawngreen')]}]
这将得到以下输出:

table_style输出显示了所选单元格的草绿色背景色
hide_index 和 hide_columns 样式选项允许我们在显示时隐藏索引或指定的列。在以下代码中,我们已经隐藏了默认的索引列:
df.style.hide_index()
以下截图显示了没有索引的输出 DataFrame:

从输出的 DataFrame 中隐藏 Index 列
现在,我们使用 hide_columns 选项来隐藏 "Name"、"Sex"、"Ticket" 和 "Cabin" 列:
df.style.hide_columns(["Name", "Sex", "Ticket", "Cabin"])
以下截图展示了在从 DataFrame 中隐藏某些列后,显示的列:

从输出的 DataFrame 中隐藏某些列
用户定义的样式选项
除了内置函数,pandas 还提供了编写我们自己的函数以用于样式设置的选项。我们来编写一个函数,将负值的背景色改为红色:
def color_negative(val):
color = 'red' if val < 0 else 'green'
return 'background-color: %s' % color
这样的函数可以通过 pandas 的 apply() 和 applymap 方法作为样式选项应用。applymap 方法按元素应用函数。apply() 方法可以通过设置 axis 参数为 1 或 0 来按行或按列应用样式。将 axis 设置为 None 会按表格应用函数。在这里,我们的操作是按元素应用。我们来使用 applymap:
rand_df.style.applymap(color_negative)
这会生成以下输出:

基于用户定义样式选项的所有列的自定义条件格式
apply() 和 applymap 方法也允许我们对数据的某个切片进行样式设置。要设置样式的列可以通过 subset 参数以列表的形式传递。我们来尝试将样式应用于第 1 列和第 3 列:
rand_df.style.applymap(color_negative, subset = [1, 3])
这会生成以下输出:

基于用户定义样式选项的子集列的自定义条件格式
这也可以通过传递适当的基于标签的索引器来完成。在下列示例中,样式已经在第 1 列和第 4 列的第 0 行、第 1 行和第 2 行应用:
rand_df.style.applymap(color_negative, subset=pd.IndexSlice[0:2, [1, 4]])
这会生成以下输出:

基于用户定义样式选项的子集行和列的自定义条件格式
format() 函数允许按指定的格式对字符串进行格式化。以下代码展示了应用格式以显示限制的小数位数:
rand_df.style.format("{:.2f}")
这会生成以下输出:

图 9.17:对所有列应用相同的两位小数格式
可以对不同的列应用不同的格式,如下所示:
rand_df.style.format({0: "{:.3%}", 3: '{:.2f}'})
这会生成以下输出:

图 9.18:通过样式字典应用不同的格式到不同的列,列索引作为键,格式选项作为值
lambda 函数可以用来跨多个列应用格式化条件:
rand_df.style.format(lambda x: "±{:.2f}".format(abs(x)))
这会生成以下输出:

图 9.19:应用 lambda 函数一次性样式化多个列
浏览 Jupyter Notebook
Jupyter Notebook,之前被称为IPython Notebook,是一个非常棒的报告工具。它允许我们将常规代码与丰富的样式、格式化、Markdown 和特殊文本(如方程式图和实时编码)结合起来。这个部分将帮助你理解 Jupyter Notebook 的本质。
Jupyter Notebook 可以通过 Anaconda Navigator 启动,也可以通过终端使用 Jupyter Notebook 命令启动。它会在浏览器中打开。启动时会打开以下窗口:

图 9.20:Jupyter Notebook 启动后显示的界面
Jupyter Notebook 可以在目录中的任何文件夹中创建。点击“新建”选项可以创建一个新的笔记本、文件夹或终端。这个选项最有趣的功能是它让我们能够轻松切换多个 Conda 环境。例如,如果已经安装了 Python 2 和 Python 3 环境,就可以通过 Jupyter 访问它们。通过“上传”选项,其他目录中的任何笔记本都可以传输到当前 Jupyter Notebook 的工作目录中。
一个笔记本由菜单栏、工具栏和单元格区域组成。一个单一的笔记本可以包含多个单元格:

图 9.21:Jupyter Notebook 的菜单栏和工具栏
探索 Jupyter Notebook 的菜单栏
菜单栏提供了控制内核和笔记本区域的选项。文件菜单帮助创建新的笔记本、打开已保存的笔记本、保存笔记本中的检查点,并恢复到之前保存的稳定版本的检查点。编辑菜单包含了一系列针对整个单元格执行的操作:复制单元格、删除单元格、拆分或合并单元格,以及上下移动单元格。查看菜单可以用来切换标题、行号和工具栏,并编辑元数据、附件和标签。可以通过插入菜单在现有单元格上方或下方插入新单元格。单元格菜单允许我们运行单个单元格或将多个单元格一起运行。通过内核菜单可以修改内核状态,包括清除输出、重启内核、中断内核以及关闭内核。Jupyter Notebook 允许我们创建和使用小部件。小部件菜单帮助我们保存、清除、下载小部件状态,并将小部件嵌入到 HTML 内容中。帮助菜单提供了快速参考和快捷键。
编辑模式和命令模式
Jupyter Notebook 可以处于编辑模式或命令模式。在编辑模式下,单元格的内容可以被更改;单元格会被高亮显示为绿色,如下图所示:

Jupyter Notebook 中的单元格编辑模式
然后你会注意到在右上角出现一个铅笔图标:

Jupyter 笔记本中单元格的编辑模式
点击单元格或按Enter键即可进入编辑模式。
Esc键帮助我们从编辑模式切换到命令模式。也可以通过点击单元格外的任何地方来完成此操作。单元格周围的灰色边框和左侧的蓝色边距表示命令模式:

Jupyter 笔记本中单元格的命令模式
命令模式允许我们编辑整个笔记本,而编辑模式则更像是一个文本编辑器。当我们处于命令模式时,Enter键帮助我们进入编辑模式。在编辑模式和命令模式中都有多种快捷键可用,命令模式的快捷键数量比编辑模式更多:

命令模式下的快捷键

命令模式下的快捷键 - 2

编辑模式下的快捷键
前面的截图中的快捷键帮助我们在 Jupyter 笔记本中导航。
鼠标导航
通过鼠标进行导航时,最基本的操作是点击一个单元格进行选择和编辑。通过工具栏选项可以进一步辅助鼠标导航。工具栏中可用的不同选项如下:
-
保存并设置检查点:此选项在 Jupyter 笔记本中保存更改,并设置一个检查点,若需要,我们可以稍后恢复到此检查点。 -
在下方插入单元格:在当前选中的单元格下方创建一个新单元格。 -
剪切选中的单元格:剪切并删除选中的单元格。此操作可以通过编辑菜单撤销。 -
复制选中的单元格:轻松复制整个单元格的内容。 -
粘贴单元格到下方:粘贴之前剪切或复制的单元格内容。 -
将选中的单元格上下移动:将选中的单元格移到当前位置的上下。每次移动一个单元格。 -
运行:运行选中的单元格以执行代码。这是Ctrl+Enter的替代方式。 -
中断内核:取消当前正在执行的操作。 -
重启内核:在弹出框提示是否可以重启内核后,内核会重新启动。 -
重启内核并重新运行笔记本:在重启内核后重新运行整个笔记本。 -
代码,Markdown,原始 NBConvert,标题:更改单元格内容的文本格式。 -
打开命令面板:显示可用的快捷键选项。
Jupyter Notebook 仪表板
Jupyter 通过 Jupyter Dashboard 提供交互式报告功能。它允许创建小部件,使可视化更加互动。仪表板体验将充满代码的笔记本转变为具有用户友好界面的应用程序。
Ipywidgets
小部件是 Jupyter Dashboard 的重要组成部分。以下部分将更详细地探讨Ipywidgets。首先,需要从Ipywidgets库中导入小部件:
from ipywidgets import widgets
以下截图展示了如何创建一个文本输入小部件:

获取文本输入的小部件
现在,让我们打印在小部件中输入的值:

图 9.39:获取文本输入并打印输出的小部件
使用类似的方法,可以创建其他小部件。以下截图展示了一个按钮小部件:

创建一个点击按钮小部件
每个小部件都有两个部分:用户界面(UI)和事件处理器。事件处理器通常是一个 Python 脚本,它指导我们根据用户输入做出响应。在之前的示例中,事件处理器根据用户输入打印了一条消息。
与默认的小部件不同,interact是一种特殊的小部件,它根据用户输入选择小部件的形式。在以下截图中,用户通过小部件向函数提供了一个单一的值。交互式小部件决定创建一个滑块输入:

创建数值滑块小部件
现在,让我们将输入改为布尔值,即 True。小部件变成了一个复选框:

创建一个布尔值选择器小部件
交互式可视化
上述示例中的小部件很简单,执行了一个print命令。除了print命令,响应还可以通过可视化来呈现。
以下是一个示例,两个来自不同滑块输入的值被用来控制坐标轴和线图的输入:

两个滑块输入,通过图表反映数值的变化
以下是一个交互式可视化的示例,使用了 Seaborn 图表,用户可以改变图例和颜色变量来影响图表:

选择颜色变量的下拉选择器小部件
在 Jupyter Notebook 中编写数学公式
Jupyter Notebook 是一个全面的工具,用于制作包含复杂数学表达式和算法的强大报告和教程。这是因为 Jupyter Notebook 提供了强大的排版功能,用于文本格式化和数学方程式输入。具有这些功能的 Jupyter Notebook 单元称为 Markdown 单元,与之相对的是代码单元,代码在其中编写和执行。Jupyter Notebook 的排版源自一个多功能的 JavaScript 库 MathJax,该库用于在 Web 产品中输入科学方程式。它还支持 LaTex 语法,实际上,我们接下来要讨论的大部分语法都支持。
在本节中,我们将讨论如何编写这些方程式并格式化文本。我们将首先快速介绍如何编写数学方程式。
编写 Jupyter Notebook 方程式时需要牢记的一些高级指南如下:
-
选择单元类型为 Markdown,如下图所示。新单元的默认类型是
Code。 -
将方程式包含在
$$之间。 -
诸如 frac(分数)、times(乘法)、leq(小于或等于)、alpha、beta 等关键词和符号前面都加上反斜杠
\。 -
请注意,双击渲染后的 Markdown 单元会将其恢复为 LaTex/MathJax 代码片段:

选择单元类型为 Markdown
有了这些指导原则,我们开始学习如何编写方程式。本节采用了食谱格式,其中我们将看到 LaTex/MathJax 代码片段源以及输出方程式。
简单的数学运算,如加法、乘法、除法等,可以如下编写。\times 和 \over 是乘法和除法操作符的关键词。注意方程式是如何以 $$ 开始和结束的:

LaTex 代码片段及简单数学运算的输出方程式
幂和指数运算可以如下编写。^ 是 LaTex 语法中的幂或指数符号:

LaTex 代码片段及幂和指数运算的输出方程式
数学方程式通常涉及复杂的分数。这些可以按如下方式编写。\frac 关键词在编写复杂分数时提供了更多的灵活性:

LaTex 代码片段及分数运算的输出方程式
现在,我们来看一下如何编写不等式。需要注意的关键词是 \geq 和 \leq,分别表示大于或等于和小于或等于:

LaTex 代码片段及不等式的输出方程式
希腊字母和符号在数学方程式和表达式中被广泛使用。这里,我们提供了一个符号词汇表,并说明了我们可以用来书写它们的指令。请注意,如何通过在上标前加_(即下划线)来书写上标:

LaTex 片段和符号与指数的输出方程式
根号和对数是数学方程式中的重要组成部分。我们来看一下如何书写它们。\sqrt是根号的主要关键字,提供两个参数——根号的类型,即 2(nd)根,3(rd)根或 4^(th)根,以及应用根号的表达式或数字。对于对数,底数前面有一个_,也就是下划线:

LaTex 片段和根号与对数的输出方程式
经常需要处理数据向量元素的求和与积的操作。我们来看一下如何书写这些操作。\sum和\prod是主要关键字,并且具有一个\limit属性,用于输入求和或积的下限和上限:

LaTex 片段和求和与积的输出方程式
组合与统计有一套独立的符号。让我们看看如何书写它们。\choose是组合的关键字:

LaTex 片段和概率与统计的输出方程式
微积分是一个广泛的领域,是许多数据科学算法中的数学表达式和方程式的来源。\lim是书写极限表达式的关键字,并提供\limits和\to参数关键字来表示一个变量趋向某个值。\partial关键字用于书写偏导数,而\frac关键字用于书写常规导数。\int用于书写积分,它带有\limits参数,用于提供积分的上下限:

LaTex 片段和微积分的输出方程式
线性代数在数据科学算法中被广泛应用,我们在处理许多矩阵时涉及到线性代数。我们来看一下如何书写矩阵。\matrix是书写矩阵的主要关键字。元素按行书写;同一行的元素通过&分隔,而新的一行用换行符标记,也就是//:

LaTex 片段和矩阵的输出方程式
还经常遇到具有不同定义的函数,这些函数在不同的变量范围内有不同的定义。让我们学习如何书写这些函数定义。以下是写这些定义所需的关键字和元素的高层次概述。
以下是用于多重周期函数中的新格式选项:
-
大括号:使用
\left和\right关键字分别表示方程的开始和结束。 -
方程组对齐:
begin{},end{}。 -
换行符:使用
\符号将文本移到下一行。 -
文本对齐框:使用
\mbox{text}对文本进行对齐。
它们可以按如下方式使用:

LaTex 代码片段和输出多周期函数的方程
在 Jupyter Notebook 中格式化文本
Markdown 单元格提供了很多文本格式化选项。在本节中,我们将逐一介绍这些选项。
标题
通过在 Markdown 单元格中的任何文本前加 #,可以将其指定为标题。一个 # 表示标题 1,两个 # 表示标题 2,依此类推。如下面的截图所示。我们遵循与 LaTex 代码片段源相同的格式,后跟输出的格式化文本:

LaTex 代码片段和输出格式化文本用于标题
粗体和斜体
要将文本格式化为粗体,可以将其包含在 ** 中,例如 **<text>**。
要将文本格式化为斜体,可以将其包含在 * 中,例如 *<text>*:

LaTex 代码片段和输出格式化文本用于粗体和斜体格式化
对齐
可以通过使用类似 HTML 的 <center> 标签将文本居中对齐,如下所示:

LaTex 代码片段和输出格式化文本用于对齐
字体颜色
可以按如下方式指定文本的字体颜色。它必须写在另一个类似 HTML 的 <font> 标签内:

LaTex 代码片段和输出格式化文本用于字体颜色
项目符号列表
使用星号 * 和空格可以创建项目符号列表。列表也可以嵌套,如下图所示:

LaTex 代码片段和输出格式化文本用于项目符号列表
表格
可以通过组合 |、空格、--- 和 : 创建表格。它们如下所示:
-
|: 用作列分隔符。
-
空格:用于填充和对齐列和行。
-
---:用于创建实心水平线。
-
::用于单元格中的文本对齐。如果它出现在开头,则文本左对齐:

LaTex 代码片段和输出格式化文本用于表格
表格
水平线用于将不同部分分开。*** 生成普通水平线,而 - - - 提供实心水平线:

LaTex 代码片段和输出格式化文本用于表格
HTML
Markdown 单元格也可以用来呈现代码,如下图所示:

LaTex 代码片段和输出格式化文本用于 HTML
引用
在许多情况下,报告中的引用和摘录需要注明来源。这是通过在每行文本前加上 > 来实现的。这会产生缩进文本,并在结尾显示引用:

LaTex 代码片段及输出的格式化文本用于引用和缩进文本
Jupyter Notebook 中的杂项操作
除了文本格式化和方程式外,还需要进行一些杂项操作,例如加载图像、将单元格写入 Python 文件等,这些对于制作有效的报告非常重要。在本节中,我们将探讨这些操作并学习如何使用它们。
加载图像
最流行的图像格式,如 .viz、.jpg、.png、.gif 等,可以加载到 Jupyter Notebook 中,以更好地展示报告。甚至可以加载 .gif 并将其作为动画展示。
这些图像文件需要保存在当前工作目录中。当前工作目录可以通过在 Notebook 中的代码块中运行 os.getcwd() 来找到。要加载的图像应保存在该目录中。可以通过使用 os.chdir(目录路径)来更改工作目录。请注意,这些命令假设已经运行了 import os。以下代码用于显示 .jpg 图像及其输出:

LaTex 代码片段及输出的 .jpg 图像
以下代码用于显示 .gif 图像及其输出:

LaTex 代码片段及输出的 .gif 图像
超链接
超链接通常用于将用户导航到相关资源,例如输入数据、算法说明、进一步阅读、视频等。执行此操作的语法非常简单:

这将导致以下输出:

LaTex 代码片段及输出的超链接
写入 Python 文件
代码单元的内容可以写入 Python 文件。在将 Jupyter Notebook 中的原型代码移植到生产环境中的 Python 文件时,这非常有用。这些文件会写入当前工作目录:

图 9.69:用于将代码单元写入 Python 文件的 LaTex 代码片段
运行 Python 文件
外部 Python 文件可以直接从 Jupyter Notebook 中运行。这可以用于加载已保存在 Python 文件中的函数和类,以便稍后在 Notebook 中使用。它们还可以用于运行 Python 文件,而无需使用命令提示符,从而快速查看输出。再次提醒,这些 Python 文件需要存在于当前工作目录中:

从 Jupyter Notebook 的单元格运行 Python 文件后的 LaTex 代码片段及输出
加载 Python 文件
Python 文件的内容可以加载到 Notebook 单元中。这是为了在交互式 Notebook 环境中编辑、修改和测试代码:

从 Jupyter Notebook 中的单元加载 Python 文件后的 LaTex 代码片段和输出
内部链接
可以创建内部(超)链接以便从目录摘要索引跳转到笔记本的不同章节。在进行内部链接之后,点击索引中的项目会将你带到特定章节。
在 Jupyter Notebook 中使内部链接生效包括两个步骤:
- 为一个章节创建 ID 或标识符:

创建章节 ID 标识符的 LaTex 代码片段
章节的 ID 在<a>标签中给出。在这种情况下,Simple_Operations是简单操作章节的 ID。此 ID 将在第二步中用于创建指向该章节的链接。
运行前两个单元后,以下内容将作为输出。第一个包含 ID 定义的单元会变得不可见。确保在点击超链接之前已经运行过该单元。如果没有运行,链接将无法工作。另一个重要点是,这个 ID 定义需要在创建章节标题之前进行:

创建章节 ID 标识符的 LaTex 代码片段输出
- 使用此 ID 来创建内部链接。
创建内部链接的语法如下:
Text to Appear
例如,对于简单操作章节,我们需要做以下操作:

创建内部链接后的 LaTex 代码片段和输出
请注意,简单操作现在已显示为超链接。点击它将把用户带到简单操作章节。
类似地,我们可以为所有其他章节定义章节 ID:

不同章节 ID 的 LaTex 代码片段
请注意,章节 ID 的定义应紧跟在章节标题的 Markdown 单元格之前,以将该单元格标记为该章节的开始。这在大宗定义中已示例,仅为说明目的:

用于不同内部链接的 LaTex 代码片段,指向章节 ID
注意如何在每行的末尾使用了<br>标签。此标签表示换行,并将它后面的文本移到下一行:

不同内部链接指向章节 ID 的 LaTex 输出
分享 Jupyter Notebook 报告
一旦报告被创建,它们就需要与受众共享以供使用。分享这些报告有几个选项。现在我们来看一下这些选项。
使用 NbViewer
NbViewer 是一个用于查看ipynb文件的在线查看器。如果我们希望使用此选项分享 Jupyter Notebook 报告,需要按照以下步骤操作:
-
将报告保存为
ipynb文件。 -
将
ipynb文件上传到 GitHub 并获取该文件的 URL。 -
将步骤 2中的 URL 粘贴到 NbViewer 中。NbViewer 可以通过 www.nbviewer.jupyter.org 访问。
-
使用这个 GitHub URL,我们在本章中使用的 Notebook 已经共享:
github.com/ashishbt08b004/Experiments/blob/master/writing_equations_in_jupyter.ipynb。
使用浏览器
报告也可以保存为 HTML 文件。这些 HTML 文件可以直接在任何普通浏览器中查看,只需双击它们并选择一个浏览器作为 HTML 文件的默认程序。可以通过这个链接获得一个示例文件。
github.com/ashishbt08b004/Experiments/blob/master/writing_equations_in_jupyter.html。
使用 Jupyter Hub
Jupyter Hub 是一个 Python 程序,可以用于部署和共享 Jupyter Notebook 报告给多个用户。它可以看作是普通 Jupyter Notebook 的多用户版本,通过 URL 进行访问;它通常被公司、研究小组和课程讲师用来与大规模的群体分享实验和知识,提供交互式环境。
该程序在 Linux 机器上运行,通常部署在具有强大计算能力的机器上。这可以是云端或本地机器。
JupyterHub 由四个子系统组成:
-
一个Hub(tornado 进程),是 JupyterHub 的核心。
-
一个可配置的 http 代理(node-http-proxy),接收来自客户端浏览器的请求。
-
多个单用户 Jupyter notebook 服务器(Python/IPython/tornado),由生成器进行监控。
-
一个认证类,管理用户如何访问系统:

Jupyter Hub 架构
安装 Jupyter Hub 有一些前提条件:
-
一台 Linux 机器(云端或本地)
-
Python 3.5+
-
Nodejs/npm
-
TLS 证书和用于 HTTPS 通信的密钥
-
机器/服务器的域名
Jupyter Hub 提供了一个服务器-客户端类型的多用户环境,数据和 ipynb 文件可以与多个用户共享。它提供了多个强大的安全性和登录认证功能。
以下指南可以用于多用户的 Notebook 报告安装和部署:tljh.jupyter.org/en/latest/.
总结
本章集中讨论了三个主要主题:pandas 中的样式和结果格式选项、在 Jupyter Notebook 中创建交互式仪表板,以及探索 Jupyter Notebook 中的格式化和排版选项以创建强大的报告。
输出格式化,例如条件格式、粗体和斜体输出、突出显示某些部分等,可以通过 pandas 的样式选项完成。基本的交互式仪表板可以在 Jupyter Notebook 中创建。LaTex 和 MathJax 提供了强大的排版和 Markdown 选项,用于编写方程式和格式化文本。报告可以作为ipynb文件分享到 GitHub,并通过名为 NbViewer 的在线查看器查看。Jupyter Hub 是一种基于服务器的多用户部署方式。
在下一章中,我们将探讨如何使用 pandas 进行统计计算,包括使用包进行计算;我们还将从零开始进行计算。
第十一章:使用 pandas 和 NumPy 进行统计学概览
在本章中,我们将简要介绍经典统计学(也称为频率学派方法),并展示如何使用 pandas 配合 numpy 和 stats 包(例如 scipy.stats 和 statsmodels)来进行统计分析。我们还将学习如何在 Python 中从头开始编写这些统计背后的计算。本章及后续章节并非统计学入门教材;它们只是展示如何结合使用 pandas、stats 和 numpy 包的示例。在下一章中,我们将探讨经典统计学的替代方法——即贝叶斯统计。
本章我们将涉及以下主题:
-
描述性统计学与推断性统计学
-
集中趋势和变异度的度量
-
假设检验 – 零假设和备择假设
-
z 检验
-
t 检验
-
卡方检验
-
方差分析(ANOVA)检验
-
置信区间
-
相关性与线性回归
描述性统计学与推断性统计学
在描述性统计学或总结性统计学中,我们尝试以定量的方式描述一组数据的特征。这与推断性或归纳性统计学不同,因为其目的是总结样本,而不是利用数据推断或得出关于从中抽取样本的总体的结论。
集中趋势和变异度的度量
描述性统计学中使用的部分度量包括集中趋势度量和变异度度量。
集中趋势度量是通过指定数据中的中心位置来描述数据集的单个值。三种最常见的集中趋势度量是均值、中位数和众数。
变异度度量用于描述数据集中的变异性。变异度度量包括方差和标准差。
集中趋势的度量
接下来,我们将通过以下小节来了解集中趋势的度量,并附上相应的示例。
均值
均值或样本均值是最常见的集中趋势度量。它等于数据集中所有值的总和,除以数据集中的值的个数。因此,在一个包含 n 个值的数据集中,均值的计算公式如下:

如果数据值来自样本,我们使用!;如果数据值来自总体,我们使用µ。
样本均值和总体均值是不同的。样本均值是所谓的无偏估计量,用于估计真实的总体均值。通过反复从总体中随机抽取样本来计算样本均值,我们可以获得样本均值的均值。然后,我们可以调用大数法则和中心极限定理(CLT),并将样本均值的均值作为总体均值的估计值。
总体均值也被称为总体的期望值。
均值作为一个计算得出的值,通常并不是数据集中观察到的值之一。使用均值的主要缺点是它非常容易受到异常值的影响,或者当数据集非常偏斜时也会受到影响。
中位数
中位数是将排序后的数据值分成两半的数值。它的左侧恰好有一半的总体数据,右侧也有一半。当数据集中的值的个数是偶数时,中位数是两个中间值的平均值。它受异常值和偏斜数据的影响较小。
众数
众数是数据集中出现频率最高的值。它更常用于分类数据,以便找出哪个类别最常见。使用众数的一个缺点是它不是唯一的。一个具有两个众数的分布被称为双峰分布,而一个具有多个众数的分布被称为多峰分布。以下代码演示了一个双峰分布,其中众数出现在 2 和 7 处,因为它们在数据集中都出现了四次:
In [4]: import matplotlib.pyplot as plt
%matplotlib inline
In [5]: plt.hist([7,0,1,2,3,7,1,2,3,4,2,7,6,5,2,1,6,8,9,7])
plt.xlabel('x')
plt.ylabel('Count')
plt.title('Bimodal distribution')
plt.show()
生成的双峰分布如下所示:

在 Python 中计算数据集的集中趋势度量
为了说明这一点,我们来考虑以下数据集,它由 15 名学生在一项满分为 20 分的测试中获得的分数组成:
In [18]: grades = [10, 10, 14, 18, 18, 5, 10, 8, 1, 12, 14, 12, 13, 1, 18]
均值、中位数和众数可以通过以下方式获得:
In [29]: %precision 3 # Set output precision to 3 decimal places
Out[29]:u'%.3f'
In [30]: import numpy as np
np.mean(grades)
Out[30]: 10.933
In [35]: %precision
np.median(grades)
Out[35]: 12.0
In [24]: from scipy import stats
stats.mode(grades)
Out[24]: (array([ 10.]), array([ 3.]))
In [39]: import matplotlib.pyplot as plt
In [40]: plt.hist(grades)
plt.title('Histogram of grades')
plt.xlabel('Grade')
plt.ylabel('Frequency')
plt.show()
以下是前面代码的输出:

为了说明数据的偏斜或异常值如何显著影响均值作为集中趋势度量的有效性,考虑以下数据集,它显示了某工厂员工的工资(以千美元为单位):
In [45]: %precision 2
salaries = [17, 23, 14, 16, 19, 22, 15, 18, 18, 93, 95]
In [46]: np.mean(salaries)
Out[46]: 31.82
基于均值31.82,我们可能会假设数据围绕均值分布。然而,我们会错。为了说明这一点,我们使用条形图展示该数据的经验分布:
In [59]: fig = plt.figure()
ax = fig.add_subplot(111)
ind = np.arange(len(salaries))
width = 0.2
plt.hist(salaries, bins=xrange(min(salaries),
max(salaries)).__len__())
ax.set_xlabel('Salary')
ax.set_ylabel('# of employees')
ax.set_title('Bar chart of salaries')
plt.show()
以下是前面代码的输出:

从前面的条形图中,我们可以看到大多数薪资远低于 30K,并且没有人接近 32K 的均值。现在,如果我们看看中位数,我们会发现它在这种情况下是一个更好的集中趋势度量:
In [47]: np.median(salaries)
Out[47]: 18.00
我们还可以查看数据的直方图:
In [56]: plt.hist(salaries, bins=len(salaries))
plt.title('Histogram of salaries')
plt.xlabel('Salary')
plt.ylabel('Frequency')
plt.show()
以下是前面代码的输出:

直方图实际上更好地表示了数据,因为条形图通常用于表示分类数据,而直方图更适合用于定量数据,这正是薪资数据的情况。有关何时使用直方图与条形图的更多信息,请参见onforb.es/1Dru2gv。
如果分布是对称的且单峰的(即只有一个峰值),那么三种衡量值——均值、中位数和众数——将是相等的。如果分布是偏斜的,情况则不相同。在这种情况下,均值和中位数将彼此不同。对于负偏态分布,均值将低于中位数,而对于正偏态分布,情况则相反:

图表来源:http://www.southalabama.edu/coe/bset/johnson/lectures/lec15_files/iage014.jpg。
变异性、分散度或扩展度的衡量指标
描述统计学中我们衡量的另一个分布特征是变异性。
变异性表示数据点之间的差异或分散程度。变异性衡量指标非常重要,因为它们提供了关于数据性质的见解,而这些见解是集中趋势衡量指标所无法提供的。
举个例子,假设我们进行了一项研究,旨在检验一项学前教育计划在提升经济困难儿童测试成绩方面的效果。我们可以通过不仅仅关注整个样本的平均测试成绩,还可以通过成绩的分散程度来衡量效果。这对某些学生有用,而对其他学生则不太有用吗?数据的变异性可能有助于我们找出改进计划有效性的措施。
范围
最简单的分散度衡量指标是范围。范围是数据集中最低值和最高值之间的差异。这是最简单的扩展度衡量方法,可以按如下方式计算:
范围 = 最高值 - 最低值
四分位数
一个更重要的分散度衡量指标是四分位数及其相关的四分位距。四分位数也代表季度百分位数,意味着它是在测量尺度上,按排序后的数据集中的 25%、50%、75%和 100%数据所低于的值。四分位数是将数据集分为四组的三个点,每组包含四分之一的数据。为了说明这一点,假设我们有一个包含 20 个测试成绩的排名数据集,如下所示:
In [27]: import random
random.seed(100)
testScores = [random.randint(0,100) for p in
xrange(0,20)]
testScores
Out[27]: [14, 45, 77, 71, 73, 43, 80, 53, 8, 46, 4, 94, 95, 33, 31, 77, 20, 18, 19, 35]
In [28]: #data needs to be sorted for quartiles
sortedScores = np.sort(testScores)
In [30]: rankedScores = {i+1: sortedScores[i] for i in
xrange(len(sortedScores))}
In [31]: rankedScores
Out[31]:
{1: 4,
2: 8,
3: 14,
4: 18,
5: 19,
6: 20,
7: 31,
8: 33,
9: 35,
10: 43,
11: 45,
12: 46,
13: 53,
14: 71,
15: 73,
16: 77,
17: 77,
18: 80,
19: 94,
20: 95}
第一四分位数(Q1)位于第五和第六个得分之间,第二四分位数(Q2)位于第十和第十一得分之间,第三四分位数(Q3)位于第十五和第十六得分之间。因此,通过线性插值并计算中位数,我们得到以下结果:
Q1 = (19+20)/2 = 19.5
Q2 = (43 + 45)/2 = 44
Q3 = (73 + 77)/2 = 75
在 IPython 中查看这个内容时,我们可以使用scipy.stats或numpy.percentile包:
In [38]: from scipy.stats.mstats import mquantiles
mquantiles(sortedScores)
Out[38]: array([ 19.45, 44\. , 75.2 ])
In [40]: [np.percentile(sortedScores, perc) for perc in [25,50,75]]
Out[40]: [19.75, 44.0, 74.0]
值与我们之前的计算结果不完全一致的原因是由于不同的插值方法。四分位距是第三四分位数减去第一四分位数(Q3 - Q1)。它代表了数据集中的中间 50%的值。
有关统计度量的更多信息,请参见statistics.laerd.com/statistical-guides/measures-central-tendency-mean-mode-median.php。
有关scipy.stats和numpy.percentile函数的更多详细信息,请参见以下文档:docs.scipy.org/doc/scipy/reference/generated/scipy.stats.mstats.mquantiles.html 和 docs.scipy.org/doc/numpy-dev/reference/generated/numpy.percentile.html。
偏差与方差
在讨论变异性时,一个基本概念是偏差。简单来说,偏差度量告诉我们一个给定值与分布均值的偏离程度——即!。
为了找到一组值的偏差,我们定义方差为平方偏差之和,并通过除以数据集的大小来标准化它。这被称为方差。我们需要使用平方偏差的和。由于围绕均值的偏差之和为零,因为负偏差和正偏差相互抵消,因此方差是定义为以下内容:

上述表达式等价于以下内容:

严格来说,方差的定义如下:
- 对于样本方差,使用以下公式:

- 对于总体方差,使用以下公式:

样本方差的分母使用N-1而不是N的原因在于,为了获得无偏估计量,样本方差需要使用无偏估计。有关更多详细信息,请参见en.wikipedia.org/wiki/Bias_of_an_estimator。
该度量的值是以平方单位表示的。这强调了我们计算的方差是平方偏差。因此,为了得到与数据集原始点相同单位的偏差,我们必须取平方根,这样我们就得到了标准差。因此,样本的标准差由以下公式给出:

然而,对于总体,标准差由以下公式给出:

假设检验——零假设和备择假设
在前面的部分,我们简要讨论了所谓的描述性统计学。在本节中,我们将讨论推断统计学,通过它我们尝试利用样本数据集的特征来推断关于更广泛人群的结论。
推断统计学中最重要的方法之一是假设检验。在假设检验中,我们尝试确定某个假设或研究问题是否在某种程度上为真。一个假设的例子是:食用菠菜能够改善长期记忆。
为了使用假设检验来调查这个说法,我们可以选择一组人作为研究对象,并将他们分成两组或样本。第一组将是实验组,在预定的时间段内食用菠菜。第二组不食用菠菜,将作为对照组。在选定的时间段内,两组个体的记忆力将进行测量并统计。
我们实验的最终目标是能够做出类似“食用菠菜能够改善长期记忆,这不是由于偶然因素造成的”这样的陈述。这也被称为显著性。
在前述情境中,研究中的受试者集合被称为样本,而我们希望对其得出结论的更广泛人群则是总体。
我们研究的最终目标是确定我们在样本中观察到的任何效应是否可以推广到整个群体。因此,为了进行假设检验,我们需要提出所谓的零假设和备择假设。
零假设和备择假设
参考前面的菠菜例子,零假设将是“食用菠菜对长期记忆表现没有影响”。
零假设就是它的意思——它否定了我们通过实验想要证明的内容。它通过断言一个统计度量(稍后将解释)为零来实现这一点。
备择假设是我们希望支持的假设。它是零假设的对立面,我们假设它为真,直到数据提供足够的证据表明相反。因此,在这个例子中,我们的备择假设是“食用菠菜能改善长期记忆”。
在符号上,零假设被称为H0,备择假设被称为H1。你可能希望将前述的零假设和备择假设重新表述为更加具体和可衡量的形式,例如,我们可以将H0重新表述如下:
“在 1,000 名受试者中,每天食用 40 克菠菜,持续 90 天的样本的平均记忆分数,将与 1,000 名不食用菠菜的对照组在相同时间段内的平均记忆分数相同。”
在进行我们的实验/研究时,我们专注于试图证明或反驳零假设。这是因为我们可以计算我们的结果是由于偶然机会导致的概率。然而,计算替代假设的概率没有简单的方法,因为改善长期记忆可能不仅仅是因为吃菠菜等因素。
我们通过假设零假设为真并计算我们收集到的结果是由偶然机会造成的概率来测试零假设。我们设定一个阈值水平——alpha(α),如果计算得到的概率较小,则我们可以拒绝零假设;如果较大,则可以接受。拒绝零假设等同于接受备选假设,反之亦然。
alpha 和 p 值
为了进行支持或反驳我们的零假设的实验,我们需要提出一种能够以具体和可衡量的方式做出决定的方法。为了进行显著性检验,我们必须考虑两个数字——检验统计量的 p 值和显著性水平的阈值,也称为alpha。
p 值是我们观察到的结果在假设零假设为真的情况下仅由偶然机会发生的概率。
p 值也可以看作是在假设零假设为真的情况下,获得与得到的检验统计量一样极端或更极端的概率。
Alpha 值是我们比较 p 值的阈值。这为我们提供了一个可以接受或拒绝零假设的切断点。这是一个衡量我们观察到的结果必须有多极端才能拒绝我们实验的零假设的指标。最常用的 alpha 值为 0.05 或 0.01。
一般规则如下:
-
如果 p 值小于或等于 alpha(p < .05),则我们拒绝零假设,并声明结果具有统计显著性。
-
如果 p 值大于 alpha(p > .05),则我们未能拒绝零假设,并声明结果没有统计显著性。
换句话说,规则如下:
-
如果检验统计量值大于或小于两个临界检验统计量值(双尾检验),那么我们拒绝零假设,并声明(备选)结果具有统计显著性。
-
如果检验统计量值位于两个临界检验统计量值之间,则我们未能拒绝零假设,并声明(备选)结果没有统计显著性。
在频率学派方法中,使用中alpha的看似任意的值是其一大缺点,并且这一方法存在许多问题。《自然》杂志的一篇文章强调了这些问题;你可以通过以下链接找到它:www.nature.com/news/scientific-method-statistical-errors-1.14700。
想了解更多关于这个主题的细节,请参考以下链接:
courses.washington.edu/p209s07/lecturenotes/Week%205_Monday%20overheads.pdf
I 型和 II 型错误
错误有两种类型:
-
I 型错误:在这种错误中,我们拒绝了H0,而事实上,H0是真实的。一个例子就是陪审团错误地定罪一个无辜的人,尽管该人并未犯下该罪行。
-
II 型错误:在这种错误中,我们未能拒绝H0,而事实上,H1是正确的。这相当于一个有罪的人逃脱了定罪。
这里有一张表格,显示了导致错误的原假设条件:

统计假设检验
统计假设检验是我们用来做决策的一种方法。我们通过使用来自统计研究或实验的数据来进行决策。在统计学中,如果某个结果基于预定的阈值概率或显著性水平不太可能仅由偶然因素造成,那么这个结果被称为统计上显著。统计检验有两种类型:单尾检验和双尾检验。
在双尾检验中,我们将一半的alpha用于检验一个方向上的统计显著性,另一半则用于检验另一个方向上的统计显著性。
在单尾检验中,检验仅在一个方向上进行。
想了解更多关于这个主题的细节,请参考www.ats.ucla.edu/stat/mult_pkg/faq/general/tail_tests.htm。
背景
为了应用统计推断,理解什么是抽样分布的概念非常重要。抽样分布是指从一个总体中随机抽取样本时,假设原假设成立,统计量的所有可能值及其概率的集合。
一个更简化的定义是:抽样分布是指如果我们从总体中反复抽取样本,统计量可以取的所有值(分布)以及这些值的相关概率。
统计量的值是从统计量的抽样分布中随机抽取的样本。均值的抽样分布是通过获得不同大小的多个样本并计算它们的均值来确定的。
中心极限定理指出,如果原始或原始分数总体是正态分布,或者样本容量足够大,则采样分布是正态分布。通常,统计学家定义足够大的样本容量为 N ≥ 30——即样本容量为 30 或更多。然而,这仍然是一个争议话题。
有关此主题的更多细节,请参考stattrek.com/sampling/sampling-distribution.aspx。
采样分布的标准差通常称为均值的标准误差,简称标准误差。
z 检验
在以下条件下,z 检验是适用的:
-
研究涉及单一样本均值,且原假设总体的参数µ和
*已知 -
样本均值的采样分布是正态分布
-
样本的大小为N ≥ 30
当总体均值已知时,我们使用 z 检验。在 z 检验中,我们问的问题是总体均值µ是否与假设值不同。在 z 检验中,原假设如下:

在这里,µ是总体均值,
是假设值。
备择假设,
,可以是以下之一:



前两个是单尾检验,而最后一个是双尾检验。具体来说,为了检验µ,我们计算检验统计量:

这里,
是
的真实标准差。如果
成立,z 检验统计量将服从标准正态分布。
让我们通过一个快速的 z 检验示例来了解一下。
假设我们有一家虚构的公司,Intelligenza,声称他们已经提出了一种能够提高记忆保持和学习的新方法。他们声称,相比传统的学习方法,他们的技巧可以提高成绩。假设在使用传统学习技巧的基础上,成绩提高了 40%,标准差为 10%。
对 100 名学生使用 Intelligenza 方法进行了随机测试,结果显示平均成绩提高了 44%。Intelligenza 的这一主张是否成立?
本研究的原假设表明,使用 Intelligenza 方法相比传统学习技巧并没有提高成绩。备择假设是使用 Intelligenza 方法相比传统学习技巧有所提高。
原假设由以下方程给出:

备择假设由以下方程给出:

标准误差 = 10/sqrt(100) = 1
z = (43.75-40)/(10/10) = 3.75 标准误差
记住,如果原假设为真,那么检验统计量 z 将服从标准正态分布,分布形态如下所示:

图表来源于 http://mathisfun.com/data/images/normal-distrubution-large.gif。
该 z 值将是来自标准正态分布的随机样本,即如果原假设为真,则 z 的分布。
观察到的z=43.75值对应于标准正态分布曲线上的极端离群点 p 值,远小于 0.1%。
p 值是前面标准正态分布曲线中3.75值右侧区域下的面积。
这表明,如果我们从标准正态分布中抽样,那么获得观察到的检验统计量值的可能性极低。
我们可以使用 Python 中的scipy.stats包查找实际的 p 值,如下所示:
In [104]: 1 - stats.norm.cdf(3.75)
Out[104]: 8.841728520081471e-05
因此,P(z ≥ 3.75 = 8.8e-05)——也就是说,如果检验统计量服从正态分布,那么获得观察到的值的概率为8.8e-05,接近于零。因此,如果原假设真实,获得我们观察到的值几乎是不可能的。
更正式地说,我们通常会定义一个阈值或α值,当 p 值≤α时拒绝原假设,否则不拒绝。
α的典型值为 0.05 或 0.01。以下是不同α值的说明:
-
p 值 <0.01:有强烈证据反对H0
-
0.01 < p 值 < 0.05:有强烈证据反对H0
-
0.05 < p 值 < 0.1:有微弱证据反对H0
-
p 值 > 0.1:几乎没有证据反对H0
因此,在这种情况下,我们将拒绝原假设,并认可 Intelligenza 的声明,认为他们的声明具有高度显著性。在这种情况下,反对原假设的证据是显著的。我们有两种方法来决定是否拒绝原假设:
-
p 值方法
-
拒绝区域方法
在前面的例子中,我们使用的是后一种方法。
p 值越小,原假设为真的可能性越小。在拒绝区域方法中,我们有以下规则:
如果
,则拒绝原假设;否则,保留原假设。
t 检验
z 检验适用于已知总体标准差的情况。然而,在大多数实际案例中,总体标准差是未知的。对于这些情况,我们使用 t 检验来判断显著性。
对于 t 检验,鉴于总体的标准差未知,我们用样本的标准差 s 来代替。现在的均值标准误差如下:

样本的标准差,s,计算公式如下:

分母是 N-1 而不是 N。这个值被称为自由度。我们现在声明(无需解释)根据中心极限定理(CLT),当 N 增加时,t 分布会逐渐逼近正态分布、高斯分布或 z 分布——即,随着自由度(df)的增加,N-1 也会增加。当 df = ∞ 时,t 分布与正态分布或 z 分布完全相同。这是直观的,因为随着 df 的增加,样本量也增加,s 趋近于
,即总体的真实标准差。t 分布有无限多个,每一个都对应着不同的 df 值。
这可以从以下图表中看到:

图表来源:http://zoonek2.free.fr/UNIX/48_R/g593.png
关于 t 分布、z 分布与自由度之间关系的更详细技术解释,可以参见en.wikipedia.org/wiki/Student's_t-distribution。
t 检验的类型
有多种类型的 t 检验。以下是最常见的一种。它们通常提出一个关于分布均值的零假设:
- 单样本独立 t 检验:用于将样本的均值与已知的总体均值或已知值进行比较。假设我们是澳大利亚的健康研究人员,关注土著人群的健康,想了解低收入土著母亲所生婴儿的出生体重是否低于正常值。
一个单样本 t 检验的零假设测试例子是:我们从低收入的土著母亲中选取 150 例足月活产婴儿的出生体重,假设该样本的均值与澳大利亚普通人群婴儿的均值无异——即 3,367 克。
该信息的参考来源可在www.healthinfonet.ecu.edu.au/health-facts/overviews/births-and-pregnancy-outcome找到。
- 独立样本 t 检验:用于比较来自独立样本的均值。一个独立样本 t 检验的例子是自动变速器和手动变速器车辆的油耗比较。这正是我们现实世界示例的重点。
t 检验的零假设是:手动变速器和自动变速器车辆的平均燃油效率在其综合城市/高速公路油耗方面没有差异。
- 配对样本 t 检验:在配对/依赖样本 t 检验中,我们将一个样本中的每个数据点与另一个样本中的数据点以有意义的方式配对。做这个的一种方式是测量同一样本在不同时间点的变化。例如,可以通过比较参与者在节食前后的体重来检查节食减肥的效果。
在这种情况下,零假设是:参与者在节食前后的平均体重没有差异,或者更简洁地说,配对观测值之间的平均差异为零。
该信息可以在en.wikiversity.org/wiki/T-test找到。
t 检验示例
简单来说,要进行零假设显著性检验(NHST),我们需要做以下几步:
-
构建我们的零假设。零假设是我们对系统的模型,假设我们希望验证的效应实际上是由于偶然造成的。
-
计算我们的 p 值。
-
比较计算得出的 p 值与我们的 alpha(阈值)值,并决定是否拒绝或接受零假设。如果 p 值足够低(低于 alpha 值),我们将得出零假设可能为假的结论。
对于我们的现实世界示例,我们希望调查手动变速器车辆是否比自动变速器车辆更节能。为此,我们将利用美国政府在 2014 年发布的燃油经济性数据,网址为www.fueleconomy.gov:
In [53]: import pandas as pd
import numpy as np
feRawData = pd.read_csv('2014_FEGuide.csv')
In [54]: feRawData.columns[:20]
Out[54]: Index([u'Model Year', u'Mfr Name', u'Division', u'Carline', u'Verify Mfr Cd', u'Index (Model Type Index)', u'Eng Displ', u'# Cyl', u'Trans as listed in FE Guide (derived from col AA thru AF)', u'City FE (Guide) - Conventional Fuel', u'Hwy FE (Guide) - Conventional Fuel', u'Comb FE (Guide) - Conventional Fuel', u'City Unadj FE - Conventional Fuel', u'Hwy Unadj FE - Conventional Fuel', u'Comb Unadj FE - Conventional Fuel', u'City Unrd Adj FE - Conventional Fuel', u'Hwy Unrd Adj FE - Conventional Fuel', u'Comb Unrd Adj FE - Conventional Fuel', u'Guzzler? ', u'Air Aspir Method'], dtype='object')
In [51]: feRawData = feRawData.rename(columns={'Trans as listed in FE Guide (derived from col AA thru AF)' :'TransmissionType', 'Comb FE (Guide) - Conventional Fuel' : 'CombinedFuelEcon'})
In [57]: transType=feRawData['TransmissionType']
transType.head()
Out[57]: 0 Auto(AM7)
1 Manual(M6)
2 Auto(AM7)
3 Manual(M6)
4 Auto(AM-S7)
Name: TransmissionType, dtype: object
现在,我们希望修改之前的系列,使得其中的值只包含Auto和Manual字符串。我们可以按以下方式进行:
In [58]: transTypeSeries = transType.str.split('(').str.get(0)
transTypeSeries.head()
Out[58]: 0 Auto
1 Manual
2 Auto
3 Manual
4 Auto
Name: TransmissionType, dtype: object
现在,让我们从包含变速类型和综合燃油经济性数据的系列创建一个最终修改后的 DataFrame:
In [61]: feData=pd.DataFrame([transTypeSeries,feRawData['CombinedFuelEcon']]).T
feData.head()
Out[61]: TransmissionType CombinedFuelEcon
0 Auto 16
1 Manual 15
2 Auto 16
3 Manual 15
4 Auto 17
5 rows × 2 columns
现在,我们可以按以下方式将自动变速器和手动变速器的车辆数据分开:
In [62]: feData_auto=feData[feData['TransmissionType']=='Auto']
feData_manual=feData[feData['TransmissionType']=='Manual']
In [63]: feData_auto.head()
Out[63]: TransmissionType CombinedFuelEcon
0 Auto 16
2 Auto 16
4 Auto 17
6 Auto 16
8 Auto 17
5 rows × 2 columns
这显示了 987 辆自动变速器的车辆与 211 辆手动变速器的车辆:
In [64]: len(feData_auto)
Out[64]: 987
In [65]: len(feData_manual)
Out[65]: 211
In [87]: np.mean(feData_auto['CombinedFuelEcon'])
Out[87]: 22.173252279635257
In [88]: np.mean(feData_manual['CombinedFuelEcon'])
Out[88]: 25.061611374407583
In [84]: import scipy.stats as stats
stats.ttest_ind(feData_auto['CombinedFuelEcon'].tolist(),
feData_manual['CombinedFuelEcon'].tolist())
Out[84]: (array(-6.5520663209014325), 8.4124843426100211e-11)
In [86]: stats.ttest_ind(feData_auto['CombinedFuelEcon'].tolist(),
feData_manual['CombinedFuelEcon'].tolist(),
equal_var=False)
Out[86]: (array(-6.949372262516113), 1.9954143680382091e-11)
卡方检验
在本节中,我们将学习如何从头开始在 Python 中实现卡方检验,并在示例数据集上运行它。
卡方检验用于确定两个分类变量之间因果关系的统计显著性。
例如,在以下数据集中,可以使用卡方检验来确定颜色偏好是否会影响人格类型(内向和外向),反之亦然:

卡方检验的两个假设如下:
-
H0: 颜色偏好与人格类型无关
-
Ha: 颜色偏好与人格类型相关
计算卡方统计量时,我们假设原假设为真。如果两个变量之间没有关系,我们可以将该列的贡献(比例)视为总和,并将其与该单元格的行总和相乘;这样就可以得到预期的单元格值。换句话说,缺乏特定的关系意味着一种简单的比例关系和分布。因此,我们按照以下方式计算每个子类别的预期数值(假设原假设为真):
Expected Frequency = (Row Total X Column Total) / Total:

一旦计算出预期频率,就可以计算预期频率和观察频率之间差异的平方与预期频率的比值:
Chi_Square_Stat = Sum( (Expected Frequency - Observed Frequency)**2 / Expected Frequency)
这些统计量遵循卡方分布,具有一个称为自由度(DOF)的参数。自由度由以下公式给出:
DOF = (Number of Rows - 1) * (Number of Column - 1)
每个自由度都有不同的分布,如下图所示:

不同自由度下的卡方分布
像我们研究过的其他任何检验一样,我们需要决定一个显著性水平,并找到与卡方统计量相关的 p 值,这个 p 值与自由度有关。
如果 p 值小于 alpha 值,则可以拒绝原假设。
这个完整的计算过程可以通过编写一些 Python 代码来实现。以下两个函数计算卡方统计量和自由度:
#Function to calculate the chi-square statistic
def chi_sq_stat(data_ob):
col_tot=data_ob.sum(axis=0)
row_tot=data_ob.sum(axis=1)
tot=col_tot.sum(axis=0)
row_tot.shape=(2,1)
data_ex=(col_tot/tot)*row_tot
num,den=(data_ob-data_ex)**2,data_ex
chi=num/den
return chi.sum()
#Function to calculate the degrees of freedom
def degree_of_freedom(data_ob):
dof=(data_ob.shape[0]-1)*(data_ex.shape[1]-1)
return dof
# Calculting these for the observed data
data_ob=np.array([(20,6,30,44),(180,34,50,36)])
chi_sq_stat(data_ob)
degree_of_freedom(data_ob)
卡方统计量为 71.99,自由度为 3。p 值可以通过此处的表格计算:people.smp.uq.edu.au/YoniNazarathy/stat_models_B_course_spring_07/distributions/chisqtab.pdf。
从表格中可以看出,71.99 的 p 值非常接近 0。即使我们选择一个较小的 alpha 值,如 0.01,p 值仍然更小。基于此,我们可以说,在统计上有较高的置信度下,原假设可以被拒绝。
方差分析(ANOVA)检验
现在我们来谈谈另一个常见的假设检验方法,称为方差分析(ANOVA)。它用于检验来自不同组或不同实验设置下的相似数据点是否在统计上相似或不同——例如,学校不同班级的平均身高,或者不同种族群体中发现的某种蛋白质的肽段长度。
方差分析计算两项指标来进行检验:
-
不同组之间的方差
-
每组内的方差
基于这些指标,计算具有不同组间方差的统计量作为分子。如果这是一个足够大的统计量,意味着不同组间的方差大于组内方差,这意味着来自不同组的数据点是不同的。
让我们看看如何计算不同组之间的方差和每个组内方差。假设我们有k组,数据点来自这些组:
第一组数据点为X[11], X[12], ......., X[1n.]
第二组数据点为X[21], X[22], ......., X[2n.]
这意味着第k组的数据点为X[k1], X[k2], ......., X[kn.]
让我们使用以下缩写和符号来描述这些数据的某些特征:
-
不同组间的方差由 SSAG 表示
-
每个组内的方差由 SSWG 表示
-
第 k 组中的元素数量由n[k]表示
-
组中数据点的平均值由µ[k]表示
-
所有组的数据点的平均值由µ表示
-
组数由k表示
让我们为我们的统计测试定义两个假设:
-
零假设: µ[1] = µ[2] = .....= µ[k]
-
零假设: µ[1] != µ[2] != .....= µ[k]
换句话说,零假设表明所有组中数据点的平均值相同,而备择假设表明至少一个组的平均值与其他组不同。
这导致以下方程:
SSAG = (∑ n[k] * (X[k] - µ)**2) / k-1
SSWG = (∑∑(X[ki]-µ[k])**2) / nk-k-1*
在 SSAG 中,求和是在所有组上进行的。
在 SSWG 中,第一次求和是在来自特定组的数据点上进行的,第二次求和是在各个组之间进行的。
在两种情况下的分母均表示自由度。对于 SSAG,我们处理 k 组,最后一个值可以从其他k-1个值中推导出来。因此,自由度为k-1。对于 SSWG,有nk个数据点,但k-1个均值受到这些选择的限制(或固定),因此自由度为nk-k-1。
一旦计算出这些数字,测试统计量将如下计算:
检验统计量 = SSAG/SSWG
这个 SSAG 和 SSWG 的比例遵循一种称为 F 分布的新分布,因此统计量称为 F 统计量。它由两个不同的自由度定义,每个组合都有一个单独的分布,如下图所示:

基于两个自由度的 F 分布,其中 d1 =k-1 和 d2=n*k-k-1
就像我们看过的其他任何测试一样,我们需要确定显著性水平,并找到与这些自由度对应的 F 统计量的 p 值。如果 p 值小于α值,则可以拒绝原假设。整个计算可以通过编写一些 Python 代码来完成。
让我们看一些示例数据,看看如何应用 ANOVA:
import pandas as pd
data=pd.read_csv('ANOVA.csv')
data.head()
这将产生以下输出:

按批次和运行分组的 OD 数据的头部
我们关心的是是否不同批次和运行的平均 OD 相同。为此,我们将应用方差分析(ANOVA),但在此之前,我们可以绘制箱线图,以直观地了解不同批次和运行的分布差异:

按批次分组的 OD 箱线图
同样,可以绘制按运行分组的 OD 箱线图:

按运行分组的 OD 箱线图
现在,让我们编写 Python 代码来执行计算:
# Calculating SSAG
group_mean=data.groupby('Lot').mean()
group_mean=np.array(group_mean['OD'])
tot_mean=np.array(data['OD'].mean())
group_count=data.groupby('Lot').count()
group_count=np.array(group_count['OD'])
fac1=(group_mean-tot_mean)**2
fac2=fac1*group_count
DF1=(data['Lot'].unique()).size-1
SSAG=(fac2.sum())/DF1
SSAG
#Calculating SSWG
group_var=[]
for i in range((data['Lot'].unique()).size):
lot_data=np.array(data[data['Lot']==i+1]['OD'])
lot_data_mean=lot_data.mean()
group_var_int=((lot_data-lot_data_mean)**2).sum()
group_var.append(group_var_int)
group_var_sum=(np.array(group_var)).sum()
DF2=data.shape[0]-(data['Lot'].unique()).size-1
SSAW=group_var_sum/DF2
SSAW
F=SSAG/SSAW
F
F 统计量的值为 3.84,而自由度分别为 4 和 69。
在显著性水平,即α值为 0.05 时,F 统计量的临界值位于 2.44 和 2.52 之间(来自 F 分布表:socr.ucla.edu/Applets.dir/F_Table.html)。
由于 F 统计量的值(3.84)大于临界值 2.52,因此 F 统计量位于拒绝区间内,零假设可以被拒绝。因此,可以得出结论,不同批次组的平均 OD 值是不同的。在显著性水平为 0.001 时,F 统计量变得小于临界值,因此零假设不能被拒绝。我们必须接受不同组的 OD 均值在统计学上是相同的。对于不同的运行组,可以执行相同的检验。这部分作为练习留给你进行实践。
置信区间
在本节中,我们将讨论置信区间的问题。置信区间允许我们对一个总体的给定样本数据的均值进行概率估计。
这种估计称为区间估计,它由一系列值(区间)组成,这些值作为对未知总体参数的良好估计。
置信区间由置信限界定。95%的置信区间定义为一个区间,其中区间包含总体均值的概率为 95%。那么我们如何构建置信区间呢?
假设我们有一个双尾 t 检验,并且我们想构建一个 95%的置信区间。在这种情况下,我们希望样本 t 值
与均值相对应,并满足以下不等式:

给定
,我们可以将其代入前面的不等式关系,得到以下方程:

区间就是我们的 95%置信区间。
一般化任何置信区间的百分比,y,可以表示为
,其中
是t—t的 t 分布值——即,
与y所需置信区间的相关性。
现在我们将利用一个机会,说明如何使用流行的统计环境 R 中的数据集来计算置信区间。stats模块提供通过get_rdataset函数访问 R 核心数据集包中的数据集。
一个示例说明
我们将考虑一个名为 faithful 的数据集,该数据集来自对美国黄石国家公园老忠实间歇泉喷发的观察。数据集中的两个变量是 eruptions(喷发持续时间)和 waiting(到下次喷发的等待时间)。该数据集包含 272 个观测值:
In [46]: import statsmodels.api as sma
faithful=sma.datasets.get_rdataset("faithful")
faithful
Out[46]: <class 'statsmodels.datasets.utils.Dataset'>
In [48]: faithfulDf=faithful.data
faithfulDf.head()
Out[48]: eruptions waiting
0 3.600 79
1 1.800 54
2 3.333 74
3 2.283 62
4 4.533 85
5 rows × 2 columns
In [50]: len(faithfulDf)
Out[50]: 272
让我们计算间歇泉的 95%置信区间。为此,我们必须获得数据的样本均值和标准差:
In [80]: mean,std=(np.mean(faithfulDf['waiting']),
np.std(faithfulDf['waiting']))
现在,我们将利用scipy.stats包来计算置信区间:
In [81]: from scipy import stats
N=len(faithfulDf['waiting'])
ci=stats.norm.interval(0.95,loc=mean,scale=std/np.sqrt(N))
In [82]: ci
Out[82]: (69.28440107709261, 72.509716569966201)
因此,我们可以以 95%的置信度声明,[69.28, 72.51]区间包含间歇泉的实际平均等待时间。
这些信息可以在statsmodels.sourceforge.net/devel/datasets/index.html和docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.stats.norm.html找到。
相关性与线性回归
统计学中最常见的任务之一是确定两个变量之间是否存在依赖关系。相关性是我们在统计学中用来描述相互依赖的变量的术语。
然后,我们可以使用这个关系来尝试从一组变量预测另一组变量的值。这就是回归。
相关性
表示统计依赖关系的相关性关系并不意味着两个变量之间存在因果关系;关于这一点的著名表述是:相关性不代表因果关系。因此,两个变量或数据集之间的相关性仅表示一种偶然关系,而不是因果关系或依赖关系。例如,购买冰淇淋的数量与当天的天气之间存在相关性。
有关相关性和依赖性的更多信息,请参见en.wikipedia.org/wiki/Correlation_and_dependence。
相关性度量,即相关系数,是描述两个变量之间关系的大小和方向的数字。其方向范围从 -1 到 +1,大小范围从 0 到 1。关系的方向通过符号表示,+ 符号表示正相关,- 符号表示负相关。大小越大,相关性越强,1 被视为完美相关。
最常用和广泛使用的相关系数是 Pearson 积矩相关系数,简称 r。它衡量两个 x 和 y 变量之间的线性相关性或依赖性,取值范围为 -1 到 +1。
样本相关系数,r,定义如下:

这也可以写成如下形式:

在这里,我们省略了求和的范围。
线性回归
如我们之前提到的,回归分析侧重于使用两个变量之间的关系来进行预测。为了利用线性回归进行预测,必须计算出最佳拟合直线。
如果所有的点(变量的值)都位于一条直线上,那么这种关系被认为是完美的。实际中这种情况很少发生,点并不完全整齐地落在一条直线上。因此,这种关系是不完美的。在某些情况下,线性关系仅发生在对数变换后的变量之间。这就是对数-对数模型。这样的关系的一个例子是物理中的幂律分布,其中一个变量以另一个变量的幂次方变化。
因此,像这样的表达式会产生线性关系。
欲了解更多信息,请参阅 en.wikipedia.org/wiki/Power_law.
为了构造最佳拟合直线,采用最小二乘法。在这种方法中,最佳拟合直线是通过数据点构造的最优直线,其特点是从每个点到直线的平方距离之和最小。这被认为是我们试图使用线性回归模型化的变量之间关系的最佳线性近似。此时,最佳拟合直线称为最小二乘回归线。
更正式地说,最小二乘回归线是使得从数据点到直线的垂直距离平方和最小的直线。这些垂直距离也被称为残差。
因此,通过构造最小二乘回归线,我们的目标是最小化以下表达式:

一个示例
我们现在通过一个例子来说明所有的前述要点。假设我们正在进行一项研究,旨在展示温度对蟋蟀鸣叫频率的影响。这个例子的数据显示来自乔治·W·皮尔斯(George W Pierce)于 1948 年编写的《昆虫之歌》一书。乔治·皮尔斯测量了不同温度下地面蟋蟀的鸣叫频率。
我们希望调查蟋蟀鸣叫频率与温度之间的关系,因为我们怀疑它们之间存在某种联系。数据包含 16 个数据点,我们将其读取到一个 DataFrame 中。
数据来源于college.cengage.com/mathematics/brase/understandable_statistics/7e/students/datasets/slr/frames/slr02.html。我们来看看它:
In [38]: import pandas as pd
import numpy as np
chirpDf= pd.read_csv('cricket_chirp_temperature.csv')
In [39]: chirpDf
Out[39]:chirpFrequency temperature
0 20.000000 88.599998
1 16.000000 71.599998
2 19.799999 93.300003
3 18.400000 84.300003
4 17.100000 80.599998
5 15.500000 75.199997
6 14.700000 69.699997
7 17.100000 82.000000
8 15.400000 69.400002
9 16.200001 83.300003
10 15.000000 79.599998
11 17.200001 82.599998
12 16.000000 80.599998
13 17.000000 83.500000
14 14.400000 76.300003
15 rows × 2 columns
首先,我们绘制数据的散点图,并加入回归线或最优拟合线:
In [29]: plt.scatter(chirpDf.temperature,chirpDf.chirpFrequency,
marker='o',edgecolor='b',facecolor='none',alpha=0.5)
plt.xlabel('Temperature')
plt.ylabel('Chirp Frequency')
slope, intercept = np.polyfit(chirpDf.temperature,chirpDf.chirpFrequency,1)
plt.plot(chirpDf.temperature,chirpDf.temperature*slope + intercept,'r')
plt.show()
从下面的图表中可以看到,温度和鸣叫频率之间似乎存在线性关系:

我们现在可以继续使用statsmodels.ols(普通最小二乘法)方法进一步研究:
[37]: chirpDf= pd.read_csv('cricket_chirp_temperature.csv')
chirpDf=np.round(chirpDf,2)
result=sm.ols('temperature ~ chirpFrequency',chirpDf).fit()
result.summary()
Out[37]: OLS Regression Results
Dep. Variable: temperature R-squared: 0.697
Model: OLS Adj. R-squared: 0.674
Method: Least Squares F-statistic: 29.97
Date: Wed, 27 Aug 2014 Prob (F-statistic): 0.000107
Time: 23:28:14 Log-Likelihood: -40.348
No. Observations: 15 AIC: 84.70
Df Residuals: 13 BIC: 86.11
Df Model: 1
coef std err t P>|t| [95.0% Conf. Int.]
Intercept 25.2323 10.060 2.508 0.026 3.499 46.966
chirpFrequency 3.2911 0.601 5.475 0.000 1.992 4.590
Omnibus: 1.003 Durbin-Watson: 1.818
Prob(Omnibus): 0.606 Jarque-Bera (JB): 0.874
Skew: -0.391 Prob(JB): 0.646
Kurtosis: 2.114 Cond. No. 171.
我们将忽略大部分前述的结果,仅保留R-squared、Intercept和chirpFrequency的值。
从前述结果中我们可以得出回归线的斜率为3.29,并且温度轴的截距为25.23。因此,回归线的方程为temperature = 25.23 + 3.29 * chirpFrequency。
这意味着,当鸣叫频率增加 1 时,温度大约增加 3.29 华氏度。然而,请注意,截距值并不具有实际意义,因为它超出了数据的范围。我们只能对数据范围内的值进行预测。例如,我们无法预测在 32 华氏度时的chirpFrequency,因为这超出了数据的范围;而且,在 32 华氏度时,蟋蟀已经冻死。R 值,即相关系数,计算结果如下:
In [38]: R=np.sqrt(result.rsquared)
R
Out[38]: 0.83514378678237422
因此,我们的相关系数为R = 0.835。这表明大约 84%的鸣叫频率可以通过温度的变化来解释。
包含这些数据的书籍《昆虫之歌》可以在www.hup.harvard.edu/catalog.php?isbn=9780674420663找到。
如需更深入了解单变量和多变量回归分析,请参考以下网站:
-
回归分析(第一部分):
bit.ly/1Eq5kSx -
回归分析(第二部分):
bit.ly/1OmuFTV
总结
在这一章,我们简要介绍了经典的或频率主义的统计方法,并展示了如何将 pandas 与numpy和stats包(scipy.stats和statsmodels)结合使用——以便计算、解释和从统计数据中推断结论。
在下一章,我们将探讨一种替代的统计方法——贝叶斯方法。想要更深入了解我们触及的统计学主题,请参阅《行为科学中的统计学理解》,可以在www.amazon.com/Understanding-Statistics-Behavioral-Sciences-Robert/dp/0495596523找到。
第十二章:贝叶斯统计与最大似然估计简述
本章将简要介绍一种替代性的统计推断方法,称为贝叶斯统计。这并不是一本完整的入门书籍,而是作为贝叶斯方法的简介。我们还将探讨相关的 Python 库,并学习如何使用pandas和matplotlib来辅助数据分析。以下是将要讨论的各种主题:
-
贝叶斯统计简介
-
贝叶斯统计的数学框架
-
概率分布
-
贝叶斯统计与频率统计
-
PyMC 和蒙特卡洛模拟简介
-
贝叶斯分析示例 – 转折点检测
贝叶斯统计简介
贝叶斯统计学的领域建立在 18 世纪统计学家、哲学家和长老会牧师托马斯·贝叶斯的工作基础上。他著名的贝叶斯定理,构成了贝叶斯统计学的理论基础,并在 1763 年死后发表,作为逆向概率问题的解决方案。更多详细信息请参见en.wikipedia.org/wiki/Thomas_Bayes。
逆向概率问题在 18 世纪初非常流行,通常以以下形式提出。
假设你和朋友玩一个游戏。袋子 1 里有 10 个绿色球和 7 个红色球,袋子 2 里有 4 个绿色球和 7 个红色球。你的朋友抛硬币(没有告诉你结果),随机从一个袋子里抽出一个球,并展示给你。球是红色的。那么,球是从袋子 1 里抽出来的概率是多少?
这些问题被称为逆向概率问题,因为我们试图根据随后的结果(球是红色的)来估计一个已经发生的事件的概率(球是从哪个袋子里抽出来的):

贝叶斯球示意图
让我们快速说明如何解决之前提到的逆向概率问题。我们想计算在知道球是红色的情况下,球是从袋子 1 里抽出来的概率。这可以表示为P(袋子 1 | 红球)。
让我们从计算选择红球的概率开始。这个概率可以通过按照前述图中的两条红色路径来计算。因此,我们有以下结果:

现在,选择袋子 1 中的红球的概率只通过走上面这条路径来展示,计算方法如下:

从袋子 2 中选择红球的概率计算如下:

注意,这个概率也可以写成如下形式:

我们可以看到,P(袋子 1) = 1/2,树的最后一个分支仅在球首先在袋子 1 中并且是红球时才会被遍历。因此,我们将得到以下结果:



贝叶斯统计的数学框架
贝叶斯方法是一种替代的统计推断方法。我们将首先介绍贝叶斯定理,这是所有贝叶斯推断的基本方程。
在我们开始之前,需要做一些关于概率的定义:
-
A,B:这些是可以以某种概率发生的事件。
-
P(A)** 和 P(B)**:这是某一特定事件发生的概率。
-
P(A|B):这是在给定 B 发生的情况下,A 发生的概率。这称为条件概率**。
-
P(AB) = P(A 和 B)**:这是 A 和 B 同时发生的概率。
我们从以下基本假设开始:
P(AB) = P(B) * P(A|B)
上述方程显示了P(AB)的联合概率与条件概率P(A|B)以及所谓的边际概率P(B)之间的关系。如果我们重写方程,就得到了条件概率的如下表达式:
P(A|B) = P(AB)/P(B)
这在某种程度上是直观的——给定 B 的情况下,A的概率是通过将A和B同时发生的概率除以 B 发生的概率得到的。其思想是,已知 B 发生,因此我们除以其概率。有关该方程更严格的处理,可以参考bit.ly/1bCYXRd,标题为概率:联合、边际和条件概率。
类似地,由于对称性,我们有P(AB) = P(BA) = P(A) * P(B|A)。因此,我们有P(A) * P(B|A) = P(B) * P(A|B)。通过在两边同时除以P(B)并假设P(B) != 0,我们得到如下结果:

上述公式被称为贝叶斯定理,它是所有贝叶斯统计推断的基石。为了将贝叶斯定理与推理统计联系起来,我们将该方程改写为所谓的历时性 解释,如下所示:

这里,H代表假设,D代表已经发生的事件,我们在统计研究中使用它,也称为数据。
表达式 (H) 是我们在观察数据之前对假设的概率。这被称为 先验概率。贝叶斯统计学家常常将先验概率作为一个优势来宣传,因为先前结果的先验知识可以作为当前模型的输入,从而提高准确性。有关更多信息,请参考 www.bayesian-inference.com/advantagesbayesian。
P(D)** 是无论假设如何,观察到的数据的概率。这被称为 归一化常数。归一化常数并不总是需要计算,特别是在许多流行的算法中,如 马尔科夫链蒙特卡洛(MCMC),我们将在本章稍后讨论。
P(H|D)** 是给定我们观察到的数据后,假设成立的概率。这被称为 后验。
P(D|H)** 是考虑我们的假设后,获得数据的概率。这被称为 似然性。
因此,贝叶斯统计就是应用贝叶斯定理来解决推理统计问题,H 代表我们的假设,D 代表数据。
贝叶斯统计模型是以参数为基础的,这些参数的不确定性通过概率分布来表示。这与频率主义方法不同,后者将值视为确定性的。另一种表示方式如下:

这里,Θ 代表我们的未知数据,x 代表我们的观察数据。
在贝叶斯统计中,我们对先验数据做出假设,并利用似然性通过贝叶斯定理更新后验概率。作为说明,下面是一个经典问题,也被称为 urn 问题:
-
两个 urn 中包含有色球
-
urn 1 包含 50 颗红球和 50 颗蓝球
-
urn 2 包含 30 颗红球和 70 颗蓝球
-
从两个 urn 中随机选择一个(50% 的概率),然后从其中一个 urn 中随机抽取一颗球
如果抽到一颗红球,那么它来自 urn 1 的概率是多少?我们想要的是 P(H|D) —— 即 P(球来自 urn 1 | 抽到红球)。
这里,H 表示球是从 urn 1 中抽出的,D 表示抽到的球是红色的:

我们知道 P(H|D) = P(H) * P(D|H)/P(D), P(D|H) = 0.5, P(D) = (50 + 30)/(100 + 100) = 0.4。这也可以表述为:

因此,我们得出结论 P(H|D) = 0.5 * 0.5/0.4 = 0.25/0.4 = 0.625。
贝叶斯理论和赔率
贝叶斯定理有时可以通过使用一种称为赔率的替代概率公式来以更自然和方便的形式表示。赔率通常以比率的形式表示,并且被广泛使用。马匹赢得比赛的 3 比 1 赔率(通常写作 3:1)表示马匹有 75%的概率会获胜。
给定一个概率p,赔率可以计算为 odds = p:(1 - p),当p=0.75时,结果为 0.75:0.25,即 3:1。
使用赔率,我们可以将贝叶斯定理重写如下:

贝叶斯统计的应用
贝叶斯统计可以应用于我们在经典统计学中遇到的许多问题,如下所示:
-
参数估计
-
预测
-
假设检验
-
线性回归
学习贝叶斯统计学有许多令人信服的理由,比如使用先验信息来更好地优化当前模型。贝叶斯方法基于概率分布而非点估计,因此能够提供更现实的预测。贝叶斯推断基于现有数据对假设进行推理——P(假设|数据)。而频率主义方法则尝试根据假设拟合数据。可以说,贝叶斯方法更具逻辑性和实证性,因为它试图基于事实而非假设来建立信念。有关更多信息,请参见www.bayesian-inference.com/advantagesbayesian。
概率分布
在本节中,我们将简要介绍各种概率分布的特性。许多这些分布用于贝叶斯分析,因此在我们继续之前,需要简要概述它们。我们还将演示如何使用matplotlib生成和显示这些分布。为了避免在每个代码片段中重复import语句,我们将在以下命令中展示一组标准的 Python 代码导入,在任何后续代码片段中都需要先运行这些导入。每个会话只需运行一次这些导入。导入语句如下:
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import colors
import matplotlib.pyplot as plt
import matplotlib
%matplotlib inline
拟合分布
在贝叶斯分析中,我们必须进行的一个步骤是将我们的数据拟合到一个概率分布中。选择正确的分布有时需要一些技巧,通常需要统计知识和经验,但我们可以遵循一些指导原则来帮助我们。以下是这些指导原则:
-
确定数据是离散的还是连续的
-
检查数据的偏斜/对称性,如果有偏斜,确定其方向
-
确定是否存在上下限
-
确定在分布中观察到极端值的可能性
统计试验是一个可重复的实验,具有一组已定义的结果,这些结果组成了样本空间。伯努利试验是一个“是/否”实验,其中随机X变量在“是”的情况下赋值为 1,在“否”的情况下赋值为 0。抛硬币并查看是否正面朝上的实验就是一个伯努利试验的例子。
概率分布有两种类型:离散分布和连续分布。在接下来的部分中,我们将讨论这两类分布的区别,并了解主要的分布。
离散概率分布
在这种情况下,变量只能取特定的离散值,如整数。一个离散随机变量的例子是,当我们抛掷硬币五次时获得的正面次数:可能的值为{0,1,2,3,4,5}——例如,不能获得 3.82 次正面。随机变量可以取的值的范围由被称为概率质量函数(PMF)的函数来指定。
离散均匀分布
离散均匀分布是一种分布,用于模拟具有有限可能结果的事件,其中每个结果的出现概率相等。对于 n个结果,每个结果的发生概率为1/n。
这方面的一个例子是投掷一个公平的骰子。六个结果中任何一个的概率都是1**/6。概率质量函数(PMF)由1/n给出,期望值和方差分别由 (max + min)/2 和 (n²-1)/12 给出:
from matplotlib import pyplot as plt
import matplotlib.pyplot as plt
X=range(0,11)
Y=[1/6.0 if x in range(1,7) else 0.0 for x in X]
plt.plot(X,Y,'go-', linewidth=0, drawstyle='steps-pre',
label="p(x)=1/6")
plt.legend(loc="upper left")
plt.vlines(range(1,7),0,max(Y), linestyle='-')
plt.xlabel('x')
plt.ylabel('p(x)')
plt.ylim(0,0.5)
plt.xlim(0,10)
plt.title('Discrete uniform probability distribution with
p=1/6')
plt.show()
以下是输出:

离散均匀分布
伯努利分布
伯努利分布用于衡量试验中的成功概率,例如,抛硬币正面或反面朝上的概率。它可以通过一个随机X变量来表示,如果硬币是正面朝上,则取值为 1;如果是反面朝上,则取值为 0。正面朝上的概率用p表示,反面朝上的概率用q=1-p表示。
这可以通过以下概率质量函数(PMF)表示:

期望值和方差由以下公式给出:


如需更多信息,请访问 en.wikipedia.org/wiki/Bernoulli_distribution。
我们现在将使用matplotlib和scipy.stats绘制伯努利分布,如下所示:
In [20]:import matplotlib
from scipy.stats import bernoulli
a = np.arange(2)
colors = matplotlib.rcParams['axes.color_cycle']
plt.figure(figsize=(12,8))
for i, p in enumerate([0.0, 0.2, 0.5, 0.75, 1.0]):
ax = plt.subplot(1, 5, i+1)
plt.bar(a, bernoulli.pmf(a, p), label=p, color=colors[i], alpha=0.5)
ax.xaxis.set_ticks(a)
plt.legend(loc=0)
if i == 0:
plt.ylabel("PDF at $k$")
plt.suptitle("Bernoulli probability for various values of $p$")
以下是输出:

伯努利分布输出
二项分布
二项分布用于表示在n次独立伯努利试验中成功的次数,表达式如下:
Y = X[1] + X[2] + ..**. + X[n]
使用抛硬币的例子,这个分布描述了在n次试验中获得X个正面的概率。对于 100 次抛掷,二项分布模型描述了获得 0 个正面(极不可能)到 50 个正面(最有可能)再到 100 个正面(也极不可能)的几率。这导致当概率完全均等时,二项分布是对称的,而当概率不均等时,则呈偏斜分布。PMF 由以下表达式给出:

期望和方差分别由以下表达式给出:


这通过以下代码块显示:
from scipy.stats import binom
clrs = ['blue','green','red','cyan','magenta'] plt.figure(figsize=(12,6))
k = np.arange(0, 22)
for p, color in zip([0.001, 0.1, 0.3, 0.6, 0.999], clrs):
rv = binom(20, p)
plt.plot(k, rv.pmf(k), lw=2, color=color, label="$p$=" + str(round(p,1)))
plt.legend()
plt.title("Binomial distribution PMF")
plt.tight_layout()
plt.ylabel("PDF at $k$")
plt.xlabel("$k$")
以下是输出结果:

二项分布
泊松分布
泊松分布模型描述了在给定时间间隔内事件发生的概率,假设这些事件以已知的平均速率发生,并且每次事件发生与上次事件发生后的时间间隔无关。
一个可以通过泊松分布建模的具体过程的例子是,如果一个人每天平均收到 23 封电子邮件。假设这些邮件的到达时间是相互独立的,那么一个人每天收到的邮件总数就可以用泊松分布来建模。
另一个例子可以是每小时在特定车站停靠的火车数量。泊松分布的概率质量函数(PMF)由以下表达式给出:

其中,λ是速率参数,表示单位时间内期望的事件/到达次数,而k是表示事件/到达次数的随机变量。
期望和方差分别由以下表达式给出:


更多信息请参考en.wikipedia.org/wiki/Poisson_process。
使用matplotlib绘制不同值的 PMF,如下所示:
In [11]: %matplotlib inline
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from scipy.stats import poisson
colors = matplotlib.rcParams['axes.color_cycle']
k=np.arange(15)
plt.figure(figsize=(12,8))
for i, lambda_ in enumerate([1,2,4,6]):
plt.plot(k, poisson.pmf(k, lambda_), '-o',
label="$\lambda$=" + str(lambda_), color=colors[i])
plt.legend()
plt.title("Possion distribution PMF for various $\lambda$")
plt.ylabel("PMF at $k$")
plt.xlabel("$k$")
plt.show()
以下是输出结果:

泊松分布
几何分布
几何分布用于独立的伯努利试验,并衡量获得一次成功所需的试验次数(X)。它也可以表示第一次成功之前的失败次数(Y = X-1)。
PMF 表示为以下形式:

上述表达式是有道理的,因为f(k) = P(X =k),如果成功需要进行k次试验(p),那么这意味着我们必须有k-1次失败,失败的概率是(1-p)。
期望和方差分别给出如下:


以下命令清晰地解释了前面的公式:
In [12]: from scipy.stats import geom
p_vals=[0.01,0.2,0.5,0.8,0.9]
x = np.arange(geom.ppf(0.01,p),geom.ppf(0.99,p))
colors = matplotlib.rcParams['axes.color_cycle']
for p,color in zip(p_vals,colors):
x = np.arange(geom.ppf(0.01,p),geom.ppf(0.99,p))
plt.plot(x,geom.pmf(x,p),'-o',ms=8,label='$p$=' + str(p))
plt.legend(loc='best')
plt.ylim(-0.5,1.5)
plt.xlim(0,7.5)
plt.ylabel("Pmf at $k$")
plt.xlabel("$k$")
plt.title("Geometric distribution PMF")
以下是输出结果:

几何分布
负二项分布
负二项分布用于独立的伯努利试验,衡量在发生指定次数的成功(r)之前所需的试验次数(X=k)。一个例子是,投掷硬币直到得到五次正面所需的次数。其概率质量函数(PMF)如下所示:

期望值和方差分别由以下公式给出:


我们可以看到,负二项分布是几何分布的推广,几何分布是负二项分布的一个特例,其中r=1。
代码和图表如下所示:
In [189]: from scipy.stats import nbinom
from matplotlib import colors
clrs = matplotlib.rcParams['axes.color_cycle']
x = np.arange(0,11)
n_vals = [0.1,1,3,6]
p=0.5
for n, clr in zip(n_vals, clrs):
rv = nbinom(n,p)
plt.plot(x,rv.pmf(x), label="$n$=" + str(n), color=clr)
plt.legend()
plt.title("Negative Binomial Distribution PMF")
plt.ylabel("PMF at $x$")
plt.xlabel("$x$")
以下是输出结果:

负二项分布
连续概率分布
在连续概率分布中,变量可以取任何实数值。它不像离散概率分布那样被限制为有限的取值集合;例如,健康新生儿的平均体重大约在 6 到 9 磅之间(例如,其体重可以是 7.3 磅)。连续概率分布的特点是概率密度函数(PDF)。
随机变量可能取的所有概率之和为 1。因此,概率密度函数图形下的面积加起来为 1。
连续均匀分布
均匀分布模型表示一个随机变量X,其值可以在区间[a, b]内均匀分布。
概率密度函数由以下公式给出:
,当a ≤ x ≤ b时为非零值,否则为零。
期望值和方差分别由以下公式给出:


以下代码生成并绘制了不同样本大小的连续均匀概率分布:
In [11]: np.random.seed(100) # seed the random number generator
# so plots are reproducible
subplots = [111,211,311]
ctr = 0
fig, ax = plt.subplots(len(subplots), figsize=(10,12))
nsteps=10
for i in range(0,3):
cud = np.random.uniform(0,1,nsteps) # generate distrib
count, bins, ignored = ax[ctr].hist(cud,15,normed=True)
ax[ctr].plot(bins,np.ones_like(bins),linewidth=2, color='r')
ax[ctr].set_title('sample size=%s' % nsteps)
ctr += 1
nsteps *= 100
fig.subplots_adjust(hspace=0.4)
plt.suptitle("Continuous Uniform probability distributions for various sample sizes" , fontsize=14)
以下是输出结果:

连续均匀分布
指数分布
指数分布描述了在泊松过程中两个事件之间的等待时间。泊松过程是一个符合泊松分布的过程,事件以已知的平均速率不可预测地发生。指数分布可以视为几何分布的连续极限,并且也是马尔科夫过程(无记忆过程)。
一个无记忆随机变量具有以下特性:它的未来状态仅依赖于当前时刻的相关信息,而不依赖于更久远过去的信息。对马尔科夫/无记忆随机变量建模的一个例子是基于随机游走理论对短期股价行为的建模。这引出了金融学中的有效市场假说。更多信息,请参阅en.wikipedia.org/wiki/Random_walk_hypothesis。
指数分布的概率密度函数(PDF)为 f(x) = λe^(-λx)。期望值和方差分别由以下公式给出:
E(X) = 1/λ**Var(X) = 1/λ²
参考资料,请参阅en.wikipedia.org/wiki/Exponential_distribution。
分布图和代码如下所示:
In [15]: import scipy.stats
clrs = colors.cnames
x = np.linspace(0,4, 100)
expo = scipy.stats.expon
lambda_ = [0.5, 1, 2, 5]
plt.figure(figsize=(12,4))
for l,c in zip(lambda_,clrs):
plt.plot(x, expo.pdf(x, scale=1./l), lw=2,
color=c, label = "$\lambda = %.1f$"%l)
plt.legend()
plt.ylabel("PDF at $x$")
plt.xlabel("$x$")
plt.title("Pdf of an Exponential random variable for various $\lambda$");
以下是输出结果:

指数分布
正态分布
在统计学中,最重要的分布可能就是正态/高斯分布。它模拟了围绕一个中心值的概率分布,且没有左右偏差。有许多现象符合正态分布,例如以下这些:
-
婴儿的出生体重
-
测量误差
-
血压
-
测试分数
正态分布的重要性通过中心极限定理得到了强调,该定理指出,从同一分布中独立抽取的许多随机变量的均值大致呈正态分布,无论原始分布的形态如何。其期望值和方差分别如下:
E(X) = μ**Var(X) = σ²
正态分布的概率密度函数(PDF)由以下公式给出:

以下代码和图解释了这个公式:
In [54]: import matplotlib
from scipy.stats import norm
X = 2.5
dx = 0.1
R = np.arange(-X,X+dx,dx)
L = list()
sdL = (0.5,1,2,3)
for sd in sdL:
f = norm.pdf
L.append([f(x,loc=0,scale=sd) for x in R])
colors = matplotlib.rcParams['axes.color_cycle']
for sd,c,P in zip(sdL,colors,L):
plt.plot(R,P,zorder=1,lw=1.5,color=c,
label="$\sigma$=" + str(sd))
plt.legend()
ax = plt.axes()
ax.set_xlim(-2.1,2.1)
ax.set_ylim(0,1.0)
plt.title("Normal distribution Pdf")
plt.ylabel("PDF at $\mu$=0, $\sigma$")
以下是输出结果:

正态分布
用于绘制分布的 Python 代码参考可以在 bit.ly/1E17nYx 找到。
正态分布也可以看作是二项分布的连续极限,以及其他分布,如
。我们可以在以下代码和图中看到这一点:
In [18]:from scipy.stats import binom
from matplotlib import colors
cols = colors.cnames
n_values = [1, 5,10, 30, 100]
subplots = [111+100*x for x in range(0,len(n_values))]
ctr = 0
fig, ax = plt.subplots(len(subplots), figsize=(6,12))
k = np.arange(0, 200)
p=0.5
for n, color in zip(n_values, cols):
k=np.arange(0,n+1)
rv = binom(n, p)
ax[ctr].plot(k, rv.pmf(k), lw=2, color=color)
ax[ctr].set_title("$n$=" + str(n))
ctr += 1
fig.subplots_adjust(hspace=0.5)
plt.suptitle("Binomial distribution PMF (p=0.5) for various values of n", fontsize=14)
以下是输出结果:

随着 n 增加,二项分布趋近于正态分布。事实上,这在前面的 n>=30 的图中可以清楚地看到。
贝叶斯统计与频率派统计
在当今的统计学中,对于如何解释数据并进行统计推断,有两种学派。迄今为止,经典且占主导地位的方法是所谓的频率学派方法(参见第七章,统计学概览 – 经典方法)。我们在这一章中探讨的是贝叶斯方法。
什么是概率?
贝叶斯学派与频率学派世界观之间争论的核心问题是如何定义概率。
在频率学派的世界观中,概率是通过重复事件的频率推导出来的概念——例如,我们将公平硬币投掷时出现正面的概率定义为一半。这是因为当我们反复投掷公平硬币时,正面朝上的次数与总投掷次数的比值会随着投掷次数的增加接近 0.5。
贝叶斯的世界观不同,概率的概念是与一个人对事件发生的信念程度相关。因此,对于贝叶斯统计学家来说,相信公平骰子出现五点的概率是1/6,这与我们对该事件发生的概率信念有关。
模型如何定义
从模型定义的角度来看,频率学派通过反复实验来分析数据和计算指标如何变化,同时保持模型参数不变。贝叶斯学派则利用固定的实验数据,但在模型参数上变化他们的信念程度。具体解释如下:
-
频率学派:如果模型是固定的,数据是变化的。
-
贝叶斯学派:如果数据是固定的,模型是变化的。
频率学派的方法是使用最大似然法来估计模型参数。它涉及从一组独立同分布的观测数据中生成数据,并将观察到的数据拟合到模型中。最能拟合数据的模型参数值就是最大似然估计器(MLE),它有时可以是观测数据的一个函数。
贝叶斯主义从概率框架的角度以不同的方式处理问题。它使用概率分布来描述值的不确定性。贝叶斯实践者通过观察数据来估计概率。为了计算这些概率,他们使用一个单一的估计器,即贝叶斯公式。这产生了一个分布,而不仅仅是像频率学派方法那样的点估计。
置信区间(频率学派)与可信区间(贝叶斯学派)
让我们比较一下 95% 置信区间(频率学派使用的术语)和 95% 可信区间(贝叶斯实践者使用的术语)的含义。
在频率主义框架中,95%的置信区间意味着,如果你将实验重复无限次,每次生成区间,95%的这些区间将包含我们试图估计的参数,通常称为θ。在这种情况下,区间是随机变量,而不是参数估计值θ,后者在频率主义视角中是固定的。
在贝叶斯可信区间的情况下,我们有一个与频率主义置信区间的传统解释更一致的解释。因此,我们可以得出结论,Pr(a(Y) < θ < b(Y)|θ) = 0.95。在这种情况下,我们可以合理地得出结论,θ有 95%的概率位于该区间内。
欲了解更多信息,请参考《频率主义与贝叶斯主义:有什么大不了的?》(Jake VanderPlas,SciPy,2014)在www.youtube.com/watch?v=KhAUfqhLakw。
进行贝叶斯统计分析
进行贝叶斯统计分析包括以下步骤:
-
指定概率模型:在这一步中,我们使用概率分布完全描述模型。基于我们所取样本的分布,我们尝试拟合一个模型并为未知参数分配概率。
-
计算后验分布:后验分布是我们基于观测数据计算出的分布。在这种情况下,我们将直接应用贝叶斯公式。它将作为我们在前一步中指定的概率模型的函数进行指定。
-
检查我们的模型:这是一个必要的步骤,我们在做出推断之前,检查我们的模型及其输出。贝叶斯推断方法使用概率分布为可能的结果分配概率。
似然函数的蒙特卡洛估计与 PyMC
贝叶斯统计不仅仅是另一种方法。它是一个完全不同的统计学实践范式。它使用概率模型来进行推断,基于收集到的数据。这可以通过一个基本的表达式来表示,即P(H|D)。
在这里,H是我们的假设,即我们试图证明的内容,D是我们的数据或观察结果。
为了提醒我们之前的讨论,贝叶斯定理的历时形式如下:

在这里,P(H)是无条件的先验概率,表示我们在进行实验之前所知道的内容。P(D|H)是我们的似然函数,即在假设为真时,获得我们观察到的数据的概率。
P(D)是数据的概率,也称为归一化常数。它可以通过对H上的分子进行积分得到。
似然函数是我们贝叶斯计算中最重要的部分,它封装了关于数据中未知信息的所有信息。它与反向概率质量函数有一些相似之处。
反对采用贝叶斯方法的一个论点是先验的计算可能具有主观性。支持这种方法的有许多论点,其中之一是可以加入外部先验信息,如前所述。
似然值表示一个未知的积分,在简单的情况下可以通过解析积分获得。
蒙特卡洛 (MC) 积分在涉及更复杂的高维积分的使用案例中是必需的,并且可以用于计算似然函数。
MC 积分可以通过多种抽样方法计算,例如均匀抽样、分层抽样和重要性抽样。在蒙特卡洛积分中,我们可以如下近似积分:

以下是有限和:

这里,x 是来自 g 的样本向量。可以通过大数法则并确保仿真误差较小来证明这个估计是有效的。
在 Python 中进行贝叶斯分析时,我们需要一个模块来帮助我们使用蒙特卡洛方法计算似然函数。PyMC 库满足了这个需求。它提供了一种常用的蒙特卡洛方法,称为 MCMC。我们不会进一步探讨 MCMC 的技术细节,但有兴趣的读者可以通过以下来源了解有关 PyMC 中 MCMC 实现的更多信息:
-
贝叶斯估计中的蒙特卡洛积分:
bit.ly/1bMALeu -
马尔科夫链蒙特卡洛最大似然:
bit.ly/1KBP8hH -
使用 Python 进行贝叶斯统计分析–第一部分,SciPy 2014,Chris Fonnesbeck:
www.youtube.com/watch?v=vOBB_ycQ0RA
MCMC 并非万能的灵丹妙药;这种方法存在一些缺点,其中之一就是算法收敛较慢。
贝叶斯分析示例 – 转折点检测
在这里,我们将尝试使用贝叶斯推理并对一个有趣的数据集进行建模。该数据集由作者的Facebook (FB) 发帖历史构成。我们已经清洗了 FB 历史数据并将日期保存在 fb_post_dates.txt 文件中。文件中的数据如下所示:
head -2 ../fb_post_dates.txt
Tuesday, September 30, 2014 | 2:43am EDT
Tuesday, September 30, 2014 | 2:22am EDT
因此,我们看到一个日期时间序列,表示作者在 Facebook 上发帖的日期和时间。首先,我们将文件读取到 DataFrame 中,并将时间戳拆分成 Date 和 Time 两列:
In [91]: filePath="./data/fb_post_dates.txt"
fbdata_df=pd.read_csv(filePath, sep='|', parse_dates=[0], header=None,names=['Date','Time'])
接下来,我们按如下方式检查数据:
In [92]: fbdata_df.head() #inspect the data
Out[92]: Date Time
0 2014-09-30 2:43am EDT
1 2014-09-30 2:22am EDT
2 2014-09-30 2:06am EDT
3 2014-09-30 1:07am EDT
4 2014-09-28 9:16pm EDT
现在,我们按 Date 索引数据,创建一个 DatetimeIndex,以便我们可以对其进行重采样并按月计数,如下所示:
In [115]: fbdata_df_ind=fbdata_df.set_index('Date')
fbdata_df_ind.head(5)
Out[115]:
Date Time
2014-09-30 2:43am EDT
2014-09-30 2:22am EDT
2014-09-30 2:06am EDT
2014-09-30 1:07am EDT
2014-09-28 9:16pm EDT
然后,我们按如下方式显示关于索引的信息:
In [116]: fbdata_df_ind.index
Out[116]: <class 'pandas.tseries.index.DatetimeIndex'>
[2014-09-30, ..., 2007-04-16]
Length: 7713, Freq: None, Timezone: None
接下来,我们使用重采样来按月获取帖子数量:
In [99]: fb_mth_count_=fbdata_df_ind.resample('M', how='count')
fb_mth_count_.rename(columns={'Time':'Count'},
inplace=True) # Rename
fb_mth_count_.head()
Out[99]: Count
Date
2007-04-30 1
2007-05-31 0
2007-06-30 5
2007-07-31 50
2007-08-31 24
Date 格式显示的是每月的最后一天。现在,我们创建一个从 2007 年到 2015 年的 FB 帖子数量散点图,并使点的大小与 matplotlib 中的值成正比:
In [108]: %matplotlib inline
import datetime as dt
#Obtain the count data from the DataFrame as a dictionary
year_month_count=fb_bymth_count.to_dict()['Count']
size=len(year_month_count.keys())
#get dates as list of strings
xdates=[dt.datetime.strptime(str(yyyymm),'%Y%m')
for yyyymm in year_month_count.keys()]
counts=year_month_count.values()
plt.scatter(xdates,counts,s=counts)
plt.xlabel('Year')
plt.ylabel('Number of Facebook posts')
plt.show()
以下是输出结果:

我们希望调查的问题是,是否在某个时刻行为发生了变化。具体来说,我们想要识别是否存在一个特定时期,FB 帖子的平均数量发生了变化。这通常被称为时间序列中的切换点或变化点。
我们可以利用泊松分布来建模这个。你可能记得,泊松分布可以用来建模时间序列的计数数据。(更多信息请参考 bit.ly/1JniIqy。)
如果我们用 C[i] 表示每月的 FB 帖子数,我们可以将我们的模型表示如下:

r[i] 参数是泊松分布的速率参数,但我们不知道它的值。如果我们查看 FB 时间序列的散点图,可以看到 2010 年中到晚些时候,帖子数量出现了跳跃,可能与 2010 年南非世界杯的开始时间重合,而作者也参加了这场世界杯。
s 参数是切换点,即速率参数发生变化的时刻,而 e 和 l 分别是切换点前后 r[i] 参数的值。可以表示如下:

请注意,这里指定的变量—C, s, e, r 和 l—都是贝叶斯随机变量。对于表示某人对其值的信念的贝叶斯随机变量,我们需要使用概率分布来建模它们。我们希望推断 e 和 l 的值,这些值是未知的。在 PyMC 中,我们可以使用随机和确定性类来表示随机变量。我们注意到,指数分布表示的是泊松事件之间的时间。因此,对于 e 和 l,我们选择使用指数分布来建模它们,因为它们可以是任何正数:


对于 s,我们选择使用均匀分布来建模它,这反映了我们认为切换点可能在整个时间段内的任何一天发生的概率是相等的。这意味着我们有如下假设:

在这里,t[0], t[f]分别对应年份的下限和上限,i。现在让我们使用PyMC来表示我们之前开发的模型。我们将使用PyMC来看看是否能在 FB 发布数据中检测到转折点。除了散点图,我们还可以在条形图中显示数据。为此,我们首先需要获取按月份排序的 FB 发布计数列表:
In [69]: fb_activity_data = [year_month_count[k] for k in
sorted(year_month_count.keys())]
fb_activity_data[:5]
Out[70]: [1, 0, 5, 50, 24]
In [71]: fb_post_count=len(fb_activity_data)
我们使用matplotlib来渲染条形图:
In [72]: from IPython.core.pylabtools import figsize
import matplotlib.pyplot as plt
figsize(8, 5)
plt.bar(np.arange(fb_post_count),
fb_activity_data, color="#49a178")
plt.xlabel("Time (months)")
plt.ylabel("Number of FB posts")
plt.title("Monthly Facebook posts over time")
plt.xlim(0,fb_post_count);
以下是输出结果:

从前面的条形图来看,我们能否得出结论认为 FB 频率发布行为在一段时间内发生了变化?我们可以在已开发的模型上使用PyMC来帮助我们找出变化,方法如下:
In [88]: # Define data and stochastics
import pymc as pm
switchpoint = pm.DiscreteUniform('switchpoint',
lower=0,
upper=len(fb_activity_data)-1,
doc='Switchpoint[month]')
avg = np.mean(fb_activity_data)
early_mean = pm.Exponential('early_mean', beta=1./avg)
late_mean = pm.Exponential('late_mean', beta=1./avg)
late_mean
Out[88]:<pymc.distributions.Exponential 'late_mean' at 0x10ee56d50>
在这里,我们为速率参数r定义了一个方法,并使用之前讨论的泊松分布来建模计数数据:
In [89]: @pm.deterministic(plot=False)
def rate(s=switchpoint, e=early_mean, l=late_mean):
''' Concatenate Poisson means '''
out = np.zeros(len(fb_activity_data))
out[:s] = e
out[s:] = l
return out
fb_activity = pm.Poisson('fb_activity', mu=rate,
value=fb_activity_data, observed=True)
fb_activity
Out[89]: <pymc.distributions.Poisson 'fb_activity' at 0x10ed1ee50>
在前面的代码片段中,@pm.deterministic是一个装饰器,表示速率函数是确定性的,即其值完全由其他变量(在本例中为e、s和l)决定。该装饰器是必要的,它告诉PyMC将速率函数转换为确定性对象。如果我们不指定装饰器,将会发生错误。有关 Python 装饰器的更多信息,请参考bit.ly/1zj8U0o。
更多信息,请参考以下网页:
现在,我们将使用 FB 计数数据(fb_activity)和e, s, l(分别为early_mean、late_mean和rate)参数来创建模型。
接下来,使用PyMC,我们创建一个MCMC对象,使我们能够使用 MCMC 方法来拟合数据。然后,我们调用该MCMC对象上的 sample 函数进行拟合:
In [94]: fb_activity_model=pm.Model([fb_activity,early_mean,
late_mean,rate])
In [95]: from pymc import MCMC
fbM=MCMC(fb_activity_model)
In [96]: fbM.sample(iter=40000,burn=1000, thin=20)
[-----------------100%-----------------] 40000 of 40000
complete in 11.0 sec
使用 MCMC 拟合模型涉及利用马尔可夫链蒙特卡洛方法生成后验的概率分布,P(s,e,l | D)。它使用蒙特卡洛过程反复模拟数据采样,并在算法似乎收敛到稳定状态时停止,收敛依据多个标准。这是一个马尔可夫过程,因为连续的样本仅依赖于前一个样本。有关马尔可夫链收敛的更多信息,请参考bit.ly/1IETkhC。
生成的样本被称为轨迹。我们可以通过查看其轨迹的直方图来观察参数的边际后验分布的形态:
In [97]: from pylab import hist,show
%matplotlib inline
hist(fbM.trace('late_mean')[:])
Out[97]: (array([ 15., 61., 214., 421., 517., 426., 202.,
70., 21., 3.]),
array([ 102.29451192, 103.25158404, 104.20865616,
105.16572829, 106.12280041, 107.07987253,
108.03694465, 108.99401677, 109.95108889,
110.90816101, 111.86523313]),
<a list of 10 Patch objects>)
以下是输出结果:

接下来,我们计算早期的均值:
In [98]:plt.hist(fbM.trace('early_mean')[:]) Out[98]: (array([ 20., 105., 330., 489., 470., 314., 147.,
60., 3., 12.]),
array([ 49.19781192, 50.07760882, 50.95740571,
51.83720261, 52.71699951, 53.59679641,
54.47659331, 55.35639021, 56.2361871 ,
57.115984 , 57.9957809 ]),
<a list of 10 Patch objects>)
以下是输出结果:

在这里,我们看到切换点在月份数上的表现:
In [99]: fbM.trace('switchpoint')[:] Out[99]: array([38, 38, 38, ..., 35, 35, 35])
In [150]: plt.hist(fbM.trace('switchpoint')[:]) Out[150]: (array([ 1899., 0., 0., 0., 0., 0.,
0., 0., 0., 51.]),
array([ 35\. , 35.3, 35.6, 35.9, 36.2, 36.5, 36.8,
37.1, 37.4, 37.7, 38\. ]),
<a list of 10 Patch objects>)
以下是输出结果:

切换点的月份数直方图
我们可以看到,切换点大致位于 35 到 38 个月之间。这里,我们使用matplotlib来展示e、s和l的边际后验分布,并将它们绘制在一张图中:
In [141]: early_mean_samples=fbM.trace('early_mean')[:]
late_mean_samples=fbM.trace('late_mean')[:]
switchpoint_samples=fbM.trace('switchpoint')[:]
In [142]: from IPython.core.pylabtools import figsize
figsize(12.5, 10)
# histogram of the samples:
fig = plt.figure()
fig.subplots_adjust(bottom=-0.05)
n_mths=len(fb_activity_data)
ax = plt.subplot(311)
ax.set_autoscaley_on(False)
plt.hist(early_mean_samples, histtype='stepfilled',
bins=30, alpha=0.85, label="posterior of $e$",
color="turquoise", normed=True)
plt.legend(loc="upper left")
plt.title(r"""Posterior distributions of the variables
$e, l, s$""",fontsize=16)
plt.xlim([40, 120])
plt.ylim([0, 0.6])
plt.xlabel("$e$ value",fontsize=14)
ax = plt.subplot(312)
ax.set_autoscaley_on(False)
plt.hist(late_mean_samples, histtype='stepfilled',
bins=30, alpha=0.85, label="posterior of $l$",
color="purple", normed=True)
plt.legend(loc="upper left")
plt.xlim([40, 120])
plt.ylim([0, 0.6])
plt.xlabel("$l$ value",fontsize=14)
plt.subplot(313)
w = 1.0 / switchpoint_samples.shape[0] *
np.ones_like(switchpoint_samples)
plt.hist(switchpoint_samples, bins=range(0,n_mths), alpha=1,
label=r"posterior of $s$", color="green",
weights=w, rwidth=2.)
plt.xlim([20, n_mths - 20])
plt.xlabel(r"$s$ (in days)",fontsize=14)
plt.ylabel("probability")
plt.legend(loc="upper left")
plt.show()
以下是输出结果:

边际后验分布
PyMC还具有绘图功能,因为它使用了matplotlib。在以下图表中,我们展示了时间序列图、自相关图(acorr)和为早期均值、晚期均值以及切换点所绘制的样本的直方图。直方图有助于可视化后验分布。自相关图显示了前一时期的值是否与当前时期的值有较强的关系:
In [100]: from pymc.Matplot import plot
plot(fbM)
Plotting late_mean
Plotting switchpoint
Plotting early_mean
以下是晚期均值图:

pymc_comprehensive_late_mean 的图表
在这里,我们展示了切换点图:

PyMC 综合切换点
在这里,我们展示了早期均值图:

PyMC 综合早期均值
从PyMC的输出结果中,我们可以得出结论,切换点大约在时间序列开始后的 35 到 38 个月之间。这个时间段大致对应于 2010 年 3 月到 7 月之间。作者可以证实,这是他使用 Facebook 的一个标志性年份,因为那年是南非举办 FIFA 世界杯决赛的年份,而他也亲自参加了。
最大似然估计
最大似然估计(MLE)是一种用于从现有样本数据中估计总体分布参数的方法。MLE 方法也可以被视为贝叶斯的替代方法。
概率分布给出了在给定分布参数(如均值、标准差和自由度)下观察到数据点的概率。
给定分布参数下数据点的概率表示为 Prob(X|µ,α) -------1。
MLE 处理的是逆问题。它用于在给定数据点的情况下,找到最可能的分布参数值。为此,定义了一个称为似然度的统计量。似然度被定义为在给定数据点的情况下观察到分布参数的概率。
给定数据点的分布参数的概率表示为 L(µ,α|X)----2。
方程 1 和 2 中的量是相同的概率,只是表述方式不同。因此,我们可以写出以下公式:

为了更好地理解这个概念,请看以下图表:

正态分布 MLE 估计示例,包含两个数据点
该图展示了三个具有不同均值和标准差的正态分布。两条竖直线分别表示值 V[1]=-2.5 和 V[2]=2.5。
假设我们表示数据点 V[1]=-2.5 属于红色分布(均值=0,标准差=1.5)的概率为P(red|V[1]=-2.5)。类似地,数据点 V[1]=-2.5 属于蓝色分布(均值=0,标准差=4)的概率为P(blue|V[1]=-2.5)。
现在,查看这里展示的图表,我们可以得出以下结论:



如果我们必须仅根据现有数据点做出决策,那么我们会决定V[1]=2.5 属于蓝色分布,因为V[1]B[1] > V[1]R[1] > V[1]G[1],我们选择具有该数据点属于该分布最大概率的分布。
但是如果我们有一个或更多的数据点呢?对于这种情况,让我们向数据集中添加另一个数据点,称为V[2]。V[2]的单个概率如下:



由于现在有两个数据点,我们不能仅凭单个概率做出决策,而必须计算联合概率。如果我们假设一个数据点的发生事件与另一个数据点的发生事件是独立的,那么联合概率将等于单个概率。
假设给定两个数据点时,这两个点属于红色分布的联合概率表示为P(red|V[1]=-2.5, V[2]=2.5)。那么以下等式成立:



我们应该选择具有最大联合概率的分布。在这种情况下,蓝色的分布具有最高的联合概率,我们可以从图表中得出这一结论。
随着数据集中的点数增加,单凭观察前面的图表已经无法再有效地推断最大联合概率。我们需要求助于代数和微积分方法,找出能够最大化数据集属于某个分布的联合概率的分布参数。
如果我们假设单个数据点之间是独立的,那么在给定所有数据点的情况下观察分布参数的似然性或概率可以通过以下公式表示:

在最大似然估计(MLE)计算中,我们尝试找到能够最大化 L(µ,α|X[1], X[2], X[3], ...., X[n]) 的 µ 和 α 的值。在这个过程中,对两边取对数非常有帮助。因为对数是单调递增的函数,它不会改变目标函数,但能使计算变得更加简单。似然函数的对数通常被称为对数似然函数,计算方式如下:

为了找到对数似然函数的最大值,log(L(µ,α|X[1], X[2], X[3], ...., X[n])),我们可以执行以下操作:
-
对 log(L(µ,α|X[1], X[2], X[3], ...., X[n])) 函数关于 µ 和 α 求一阶导数,并令其等于零
-
对 log(L(µ,α|X[1], X[2], X[3], ...., X[n])) 函数关于 µ 和 α 求二阶导数,并确认其为负值
MLE 计算示例
现在我们来看两个最大似然估计(MLE)计算的例子。
均匀分布
假设我们有一个 X 的概率分布,这意味着以下内容成立:
Pr(X=x) = 1/b-a 对于所有 a < X < b
Pr(X=x) = 0 对于所有其他 X
这里,a 和 b 是均匀分布的参数。
由于所有值的概率相同(或均匀分布),因此它被称为均匀分布。
假设数据集中有 n 个数据点,并假设这些数据点符合均匀分布。基于这些数据点,我们的目标是找到 a 和 b 的值,以定义这些数据点最有可能属于的分布。为此,我们可以使用最大似然估计方法:



我们需要找到能最大化 log(L(a,b|x[1],x[2],x[3],.....,x[n]) 的 a 和 b。
为此,我们将对 log(L(a,b|x[1],x[2],x[3],.....,x[n]) 关于 b-a 进行求导。
这给出了 -n/(b-a),它始终小于零,表明 log(L(a,b|x[1],x[2],x[3],.....,x[n]) 是一个单调递减的函数,且其值随着 (b-a) 的增加而减少。因此,较大的 b-a 会最大化概率。
考虑到这一点,我们得到 b = max(X), a = min(X)。
泊松分布
泊松分布在本章的前面部分已有解释。简而言之,泊松分布是一个具有无限大样本数的二项分布,这么大以至于二项分布的离散性转变为了泊松分布。泊松分布也处理事件发生的概率。但不同于考虑每次试验中事件发生的概率,我们更多的是从时间间隔的角度来思考,问自己在这个时间间隔内,事件发生的次数是多少。参数也从每次试验成功的概率转变为在给定时间间隔内的成功次数。
这是总结:
-
二项分布:给定每次试验的成功概率,在给定次数的试验中取得一定次数成功的概率。
-
泊松:给定到达率或成功率的情况下,在特定时间间隔内取得一定次数成功的概率——也就是给定时间间隔内的成功平均次数。
泊松概率分布可以用以下公式表示:

这里,λ是到达率或成功率。
这个表达式给出了在给定时间间隔内观察到x次成功的概率(即到达率定义的相同时间间隔)。
我们关注的是给定一组数据集,估计符合泊松分布的λ的最大似然估计:


泊松分布的最大似然估计(MLE)计算的数学公式
注意,取对数可以在代数计算上简化计算。然而,这也引入了一些数值上的挑战——例如,确保似然值永远不为 0,因为对数不能对 0 求值。如果对数似然值无限小,数值方法也会导致无效值。
MLE 发现到达率的估计值等于数据集的均值——也就是说,过去给定时间间隔内观测到的到达次数。前面的计算可以使用 Python 中的 NumPy 和其他支持包来完成。
为了在 Python 中执行此计算,我们需要进行几个步骤:
- 编写一个函数,计算每个点的泊松概率:
import numpy as np
import math as mh
np.seterr(divide='ignore', invalid='ignore') #ignore division by zero and invalid numbers
def poissonpdf(x,lbd):
val = (np.power(lbd,x)*np.exp(-lbd))/(mh.factorial(x))
return val
- 编写一个函数,根据给定的到达率计算数据的对数似然:
def loglikelihood(data,lbd):
lkhd=1
for i in range(len(data)):
lkhd=lkhd*poissonpdf(data[i],lbd)
if lkhd!=0:
val=np.log(lkhd)
else:
val=0
return val
- 编写一个函数,计算到达率λ的对数似然的导数:
def diffllhd(data,lbd):
diff = -len(data) + sum(data)/lbd
return diff
- 生成 100 个数据点的测试数据——单位时间内的到达次数为 3 到 12 之间的随机数:
data=[randint(3, 12) for p in range(100)]
- 计算不同到达率(1 到 9)下的对数似然,并绘制图形以找到最大化的到达率:
y=[loglikelihood(data,i) for i in range(1,10)]
y=[num for num in y if num ]
x=[i for i in range(1,10) if loglikelihood(data,i)]
plt.plot(x,y)
plt.axvline(x=6,color='k')
plt.title('Log-Likelihoods for different lambdas')
plt.xlabel('Log Likelihood')
plt.ylabel('Lambda')
由此我们得到以下图形,显示当到达率为 6/单位时间时,测试数据上对数似然的最大值:

在不同的λ值(即到达率)下的对数似然值
- 使用牛顿-拉夫森方法求解对数似然的全局最大值:
def newtonRaphson(data,lbd):
h = loglikelihood(data,lbd) / diffllhd(data,lbd)
while abs(h) >= 0.0001:
if diffllhd!=0:
h = loglikelihood(data,lbd) / diffllhd(data,lbd)
# x(i+1) = x(i) - f(x) / f'(x)
lbd = lbd - h
else:
lbd=lbd
return lbd
注意:函数定义中的lbd参数是开始搜索的初始值。
牛顿-拉夫森方法是一种常用的计算方法,用于求解复杂方程的根。它是一个迭代过程,通过不断地求出自变量的不同值,直到因变量达到 0。更多信息可以参考www.math.ubc.ca/~anstee/math104/newtonmethod.pdf。
结果受初始参数值的影响很大,搜索的方向可能会因起始值的不同而大相径庭,因此在使用时要小心。
最大似然估计(MLE)概念可以扩展到执行基于分布的回归。假设我们假设到达率是一个或多个参数的函数,那么 lambda 将由这些参数的函数定义:

在这种情况下,到达率的计算方法如下:
-
在对数似然计算中使用之前方程中得到的到达率值。
-
找到对* w [0]、 w [1]和 w *[2]的对数似然的偏导数。
-
将所有偏导数等于 0,找到* w [0]、 w [1]和 w *[2]的最优值。
-
根据这些参数找到到达率的最优值。
要使用 MLE 计算,请执行以下步骤:
-
从样本参数中找到总体参数,如均值、标准差、到达率和密度。
-
对于简单线性回归无法有效拟合的数据,可以使用基于分布的回归模型,例如之前讨论的基于参数的到达率示例,或逻辑回归权重。
它在拟合回归模型中的应用使其与 OLS、梯度下降、Adam 优化、RMSprop 等优化方法处于同一类别。
参考文献
如果想更深入了解我们涉及的其他贝叶斯统计主题,请查阅以下参考文献:
-
《黑客的概率编程与贝叶斯方法》:
github.com/CamDavidsonPilon/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers -
《贝叶斯数据分析》,第三版,Andrew Gelman:
www.amazon.com/Bayesian-Analysis-Chapman-Statistical-Science/dp/1439840954 -
《贝叶斯选择》,Christian P Robert(这本书更偏理论):
www.springer.com/us/book/9780387952314
总结
本章中,我们对过去几年统计学和数据分析中最热门的趋势之一——贝叶斯统计推断方法进行了快速浏览。我们覆盖了许多内容。
我们讨论了贝叶斯统计方法的核心内容,并讨论了贝叶斯观点的多种吸引人的原因,比如它重视事实而非信仰。我们解释了关键的统计分布,并展示了如何使用各种统计包生成并在matplotlib中绘制它们。
我们在没有过度简化的情况下处理了一个相当复杂的话题,并展示了如何使用PyMC包和蒙特卡罗模拟方法,展示贝叶斯统计的强大功能,来制定模型、执行趋势分析并对真实世界数据集(Facebook 用户帖子)进行推断。最大似然估计的概念也被介绍并通过多个示例进行了说明。它是一种用于估计分布参数并将概率分布拟合到给定数据集的流行方法。
在下一章,我们将讨论如何使用 pandas 解决现实生活中的数据案例研究。
第十三章:使用 pandas 的案例研究
到目前为止,我们已经涵盖了 pandas 的广泛功能。接下来,我们将尝试在一些案例研究中实现这些功能。这些案例研究将使我们全面了解每个功能的使用,并帮助我们确定处理 DataFrame 时的关键点。此外,案例研究的逐步方法有助于加深我们对 pandas 函数的理解。本章提供了实际示例和代码片段,确保在最后,你能够理解 pandas 解决 DataFrame 问题的方法。
我们将涵盖以下案例研究:
-
从头到尾的探索性数据分析
-
使用 Python 进行网页抓取
-
数据验证
从头到尾的探索性数据分析
探索性数据分析是指理解数据特征的关键过程——如异常值、包含最相关信息的列,并通过统计和图形表示确定变量之间的关系。
让我们考虑以下 DataFrame,进行探索性数据分析:
df = pd.read_csv("data.csv")
df
以下截图展示了在 Jupyter Notebook 中加载的 DataFrame:

在 Jupyter Notebook 中加载的 DataFrame
数据概述
上述的 DataFrame 是一个汽车维修公司的客户数据。他们基本上按周期为客户提供服务。DataFrame 中的每一行对应一个独特的客户。因此,这是客户级别的数据。以下是从数据中获得的一个观察结果:

DataFrame 的形状
我们可以观察到数据包含 27,002 条记录和 26 个特征。
在开始对任何数据进行探索性数据分析之前,建议尽可能多地了解数据——包括列名及其相应的数据类型,是否包含空值(如果有,多少空值),等等。以下截图展示了通过 pandas 的info函数获得的一些基本信息:

DataFrame 的基本信息
使用info()函数,我们可以看到数据仅包含浮动和整数值。此外,没有任何列包含空值。
pandas 中的describe()函数用于获取所有数值列的各种汇总统计信息。该函数返回所有数值列的计数、均值、标准差、最小值、最大值和四分位数。以下表格展示了通过describe函数获取的数据描述:

描述数据
特征选择
如果你有一个包含多个变量的数据集,检查各列之间相关性的一个好方法是通过将相关性矩阵可视化为热图。我们可以识别并去除那些高度相关的变量,从而简化我们的分析。可视化可以通过 Python 中的seaborn库实现:

以下将是输出结果:

数据框的相关性热图
我们可以在之前的热图中观察到以下几点:
-
soldBy和days_old之间存在高度负相关 -
age_median和income_median之间存在正相关
同样地,我们可以推导出不同变量集之间的相关性。因此,基于相关性结果,我们可以通过仅选择重要特征来最小化独立特征的数量。
特征提取
除了选择有用的特征外,我们还需要从现有变量中提取显著的变量。这种方法被称为特征提取。在当前示例中,已经从现有变量中提取了一个名为new_tenure的新特征。该变量告诉我们客户在公司待了多长时间:
data['new_tenure']=data['active_status']*data['days_old']+(1-data['active_status'])*data['days_active_tenure']
以下数据框展示了新提取的变量:

含有新提取变量的数据框
数据汇总
如前所述,所呈现的数据是客户级别的数据。对汇总数据进行分析会更加可行且容易,在这种情况下,汇总数据是按区域划分的。首先,我们需要了解客户在每个区域的分布情况。因此,我们将使用groupby函数来查找每个邮政编码中的客户数量。以下代码展示了代码片段及其输出:
data.groupby('zip')['zip'].count().nlargest(10)
以下是输出结果:

基于邮政编码的汇总数据
这将给出前 10 个拥有最多客户的邮政编码。
因此,我们可以通过聚合将客户级数据转换为邮政编码级数据。在对值进行分组后,我们还必须确保去除 NA。可以使用以下代码对整个数据框进行聚合:
data_mod=data.groupby('zip')
data_clean=pd.DataFrame()
for name,data_group in data_mod:
data_group1=data_group.fillna(method='ffill')
data_clean=pd.concat([data_clean,data_group1],axis=0)
data_clean.dropna(axis=0, how='any')
以下截图是去除 NA 后的汇总数据框:

去除 NA 后的汇总数据框
data_clean将成为我们样本数据框的清理版本,该版本将传递给模型进行进一步分析。
使用 Python 进行网页抓取
网页抓取涉及从网站提取大量数据,形式可以是结构化的或非结构化的。例如,网站可能已经有一些数据以 HTML 表格元素或 CSV 文件的形式存在。这是网站上结构化数据的一个例子。但是,在大多数情况下,所需的信息会分散在网页的内容中。网页抓取有助于收集这些数据并将其存储为结构化的形式。有多种方式可以抓取网站,如在线服务、API,或者编写自己的代码。
以下是关于网页抓取的一些重要说明:
-
阅读网站的条款和条件,了解如何合法使用数据。大多数网站禁止将数据用于商业目的。
-
确保不要过快下载数据,因为这样可能会导致网站崩溃。你也有可能被网站封锁。
使用 pandas 进行网页抓取
Python 提供了不同的库来进行抓取:
-
pandas
-
BeautifulSoup
-
Scrapy
在本节中,我们将看到如何利用 pandas 和 BeautifulSoup 的强大功能进行数据抓取。首先,pandas 足以从网站上提取结构化数据,而不需要 BeautifulSoup 的帮助。在前面的章节中,我们学习了如何从不同格式(.csv、.xlsx和.xls)加载数据到 Python 中。类似于这些,pandas 有一个专门用于从 HTML 文件加载表格数据的函数。要读取 HTML 文件,pandas 的 DataFrame 会查找一个标签。这个标签称为<td> </td>标签,用于定义 HTML 中的表格。
pandas 使用read_html()来读取 HTML 文档。这个函数将 URL 中的所有结构化数据加载到 Python 环境中。因此,每当你传递一个 HTML 文件给 pandas 并希望它输出一个漂亮的 DataFrame 时,确保 HTML 页面中有一个表格。
我们可以尝试在一个示例网址上使用这个功能(www.bseindia.com/static/members/TFEquity.aspx):

示例网页
上述网页包含多个表格。使用 pandas,我们可以提取所有表格,并将其存储在一个列表中:

包含多个 DataFrame 的列表
在下图中,正在提取网页中的第二个表格:

网页和 pandas DataFrame 的对比
清洗后,提取的 DataFrame 完全复制了网站上的内容:

清洗后的 DataFrame
通过适当的索引,所有来自网页的表格都可以通过read_html函数提取。
使用 BeautifulSoup 进行网页抓取
BeautifulSoup 是一个 Python 库(www.crummy.com/software/BeautifulSoup/),用于从 HTML 和 XML 文件中提取数据。它提供了导航、访问、搜索和修改网页 HTML 内容的方式。了解 HTML 的基础知识对于成功抓取网页内容非常重要。为了解析内容,首先我们需要做的是确定在哪里可以找到我们想要下载的文件的链接,这些文件位于 HTML 标签的多层级中。简而言之,网页上有大量代码,而我们要做的就是找到包含数据的相关代码段。
在网站上,右键点击并选择检查。这将允许你查看网站背后的原始代码。点击检查后,你应该能看到以下控制台弹出:

浏览器的检查菜单
注意我们所提到的表格被包裹在一个叫做 table 的标签中。每一行都位于 <tr> 标签之间。同样,每个单元格都位于 <td> 标签之间。理解这些基本差异可以让数据提取变得更容易。
我们首先导入以下库:

导入库
接下来,我们使用 requests 库请求 URL。如果访问成功,你应该能看到以下输出:

从网站获得成功的响应
然后,我们使用BeautifulSoup解析html,以便能够处理更整洁、嵌套的BeautifulSoup数据结构。通过一些 HTML 标签的知识,解析后的内容可以使用for循环和 pandas DataFrame 轻松转换为 DataFrame。使用 BeautifulSoup 的最大优势在于,它甚至可以从非结构化的来源中提取数据,并通过支持的库将其转化为表格,而 pandas 的read_html函数只能处理结构化的数据来源。因此,根据需求,我们使用了BeautifulSoup:

使用 BeautifulSoup 提取的 DataFrame
数据验证
数据验证是检查数据质量的过程,确保数据既正确又适用于分析。它使用称为验证规则的例程来检查输入模型的数据的真实性。在大数据时代,计算机和其他技术形式生成大量信息,这些信息推动着数据产生的数量,如果这些数据缺乏质量,那么使用它们会显得不专业,这也突出了数据验证的重要性。
在这个案例研究中,我们将考虑两个 DataFrame:
-
来自平面文件的测试 DataFrame
-
来自 MongoDB 的验证 DataFrame
在测试 DataFrame 上执行验证例程,同时将其对应的数据框作为参考。
数据概览
这里考虑的数据集是 学习管理系统(LMS)数据的一部分。它们展示了与学生注册、跟踪、报告以及教育课程的交付相关的信息。我们将从平面文件加载测试 DataFrame:

从平面文件加载测试 DataFrame
pymongo 库用于将 MongoDB 连接到 Python。通常,MongoDB 会监听端口 27017:

从 Python 连接 MongoDB
我们可以在以下截图中看到连接参数。由于数据库安装在本地,我们通过 localhost 进行连接。加载的数据库名称是 lms_db:

从 MongoDB 读取数据
结构化数据库与非结构化数据库
由于 MongoDB 属于非结构化数据库类别,因此其使用的术语与结构化数据库(如 MySQL 和 PostgreSQL)大不相同。下表展示了各种 SQL 术语和概念以及相应的 MongoDB 术语和概念:
| SQL 术语/概念 | MongoDB 术语/概念 |
|---|---|
| 数据库 | 数据库 (https://docs.mongodb.com/manual/reference/glossary/#term-database) |
| 表 | 集合 (docs.mongodb.com/manual/reference/glossary/#term-collection) |
| 行 | 文档或 BSON 文档 |
| 列 | 字段 (docs.mongodb.com/manual/reference/glossary/#term-field) |
| 索引 | 索引 (docs.mongodb.com/manual/reference/glossary/#term-index) |
| 表连接 | $lookup,嵌入式文档 (docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup) |
| 主键 | 主键 (docs.mongodb.com/manual/reference/glossary/#term-primary-key) |
| 将任何唯一的列或列组合指定为主键。 | 在 MongoDB 中,主键会自动设置为_id字段。(docs.mongodb.com/manual/reference/glossary/#term-id) |
| 聚合(例如 group by) | 聚合管道 |
| 事务 | 事务 (docs.mongodb.com/manual/core/transactions/) |
SQL 与 MongoDB 术语对比视图
验证数据类型
数据类型是变量的一种属性,Python 使用它来理解如何存储和处理数据。例如,程序需要理解存储 5 和 10 的变量是数字型的,以便能够将它们相加得到 15;或者理解存储 cat 和 hat 的变量是字符串类型,以便它们可以连接(加在一起)得到 cathat。因此,它成为任何 pandas DataFrame 的初步和基本属性。
可以使用用户定义的比较函数来验证测试 DataFrame 的数据类型:

验证测试 DataFrame 的数据类型
File1 和 File2 分别对应测试数据集和验证数据集。从输出中可以明显看出,测试 DataFrame 的所有数据类型与验证 DataFrame 的数据类型匹配。如果存在不匹配,输出将显示不一致的列数。
验证维度
DataFrame 是一种二维数据结构,其中数据以表格形式呈现,类似于关系型数据库表格,按行和列排列。检查测试集和验证集是否匹配的基本方法之一是比较行数和列数。如果 DataFrame 的形状不匹配,那么测试 DataFrame 与验证 DataFrame 之间的差异就显而易见了。以下是一个截图,展示了如何验证维度:

验证维度
验证单个条目
一旦前两个测试用例满足要求,扫描单个条目以查找虚假数据就变得非常重要。前面图示中的验证过程描述了从数据采集过程中获得的值与真实值之间的差异。随着数据量的增加,验证条目变得越来越困难。通过高效使用 pandas,可以减轻这种效果。在以下示例中,使用循环(暴力法)和 pandas 索引扫描了单个条目。
使用 pandas 索引
以下截图展示了如何使用 pandas 验证单元格:

使用 pandas 索引验证单元格
使用循环
以下截图展示了如何通过使用循环验证单元格:

使用循环验证单元格
当我们使用 pandas 索引时,结果非常令人鼓舞。验证一个包含 200,000 行和 15 列的 DataFrame 只用了 0.53 秒,而使用循环完成相同的验证流程则花费了超过 7 分钟。因此,始终建议利用 pandas 的强大功能,避免使用迭代编程。
总结
pandas 对许多辅助数据活动非常有用,例如探索性数据分析、验证两个数据源之间数据的有效性(如数据类型或计数),以及构建和塑造从其他来源获取的数据,比如抓取网站或数据库。在这一章中,我们处理了这些主题的一些案例研究。数据科学家每天都会进行这些活动,本章应该能让你大致了解在真实数据集上执行这些活动的体验。
在下一章,我们将讨论 pandas 库的架构和代码结构。这将帮助我们全面了解该库的功能,并使我们能够更好地进行故障排除。
第十四章:pandas 库架构
在本章中,我们将探讨 pandas 用户可以使用的各种库。本章旨在成为一本简短的指南,帮助用户浏览和了解 pandas 提供的各种模块和库。它详细描述了库代码的组织结构,并简要介绍了各种模块。这对于希望了解 pandas 内部工作原理的用户,以及希望为代码库做出贡献的用户,将是非常有价值的。我们还将简要演示如何使用 Python 扩展来提高性能。以下是本章将讨论的各个主题:
-
pandas 库层次结构简介
-
pandas 模块和文件的描述
-
使用 Python 扩展提高性能
理解 pandas 文件层次结构
一般来说,在安装时,pandas 会作为 Python 模块安装在第三方 Python 模块的标准位置。在下表中,您可以看到 Unix/macOS 和 Windows 平台的标准安装位置:
| 平台 | 标准安装位置 | 示例 |
|---|---|---|
| Unix/macOS | prefix/lib/pythonX.Y/site-packages |
/usr/local/lib/python2.7/site-packages |
| Windows | prefix\Lib\site-packages |
C:\Python27\Lib\site-packages |
如果通过 Anaconda 安装 Python,那么 pandas 模块可以在 Anaconda 目录下找到,类似的文件路径为:Anaconda3\pkgs\pandas-0.23.4-py37h830ac7b_0\Lib\site-packages\pandas。
现在我们已经了解了第三方 Python 模块部分,接下来将了解文件层次结构。已安装的 Pandas 库包含八种类型的文件。这些文件遵循特定的层次结构,下面描述了这一结构:
-
pandas/core:该文件包含用于基本数据结构(如 Series/DataFrame)及相关功能的文件。 -
pandas/src:该文件包含用于实现基本算法的 Cython 和 C 代码。 -
pandas/io:该文件包含处理不同文件格式的输入/输出工具,如平面文件、Excel、HDF5 和 SQL。 -
pandas/tools:该文件包含辅助数据算法、合并和连接例程、拼接、透视表等。该模块主要用于数据操作。 -
pandas/sparse:该文件包含稀疏版本的数据结构,如系列、DataFrame、Panels 等。 -
pandas/stats:该文件包含线性回归、面板回归、移动窗口回归以及其他一些统计函数。它应该被 statsmodels 中的功能所替代。 -
pandas/util:该文件包含实用工具以及开发和测试工具。 -
pandas/rpy:该文件包含 RPy2 接口,用于连接 R,从而扩展数据分析操作的范围。
更多信息,请参见:pandas.pydata.org/developers.html。
pandas 模块和文件的描述
在本节中,我们简要描述了构成 pandas 库的各个子模块和文件。
pandas/core
该模块包含了 pandas 的核心子模块,具体讨论如下:
-
api.py:该模块导入了一些关键模块和警告信息,以供后续使用,例如索引、groupby和重塑函数。 -
apply.py:该模块包含一些类,帮助将函数应用到 DataFrame 或 series 上。 -
arrays:该模块隔离了 pandas 对numpy的依赖——即所有直接使用numpy的操作。array子模块中的base.py处理所有面向数组的操作,例如ndarray值、形状和ndim,而categorical.py子模块专门处理分类值。 -
base.py:该模块定义了诸如StringMixin和PandasObject等基础类,PandasObject是多个 pandas 对象的基类,例如Period、PandasSQLTable、sparse.array.SparseArray/SparseList、internals.Block、internals.BlockManager、generic.NDFrame、groupby.GroupBy、base.FrozenList、base.FrozenNDArray、io.sql.PandasSQL、io.sql.PandasSQLTable、tseries.period.Period、FrozenList、FrozenNDArray: IndexOpsMixin和DatetimeIndexOpsMixin。 -
common.py:该模块定义了处理数据结构的常用工具方法。例如,isnull对象用于检测缺失值。 -
config.py:该模块用于处理整个包的可配置对象。它定义了以下类:OptionError、DictWrapper、CallableDynamicDoc、option_context和config_init。 -
datetools.py:这是一个处理 Python 中日期的函数集合。它还利用了 pandas 中tseries模块的一些函数。 -
frame.py:该模块定义了 pandas 的 DataFrame 类及其各种方法。DataFrame 继承自 NDFrame(见下文)。它从pandas-core模块下的多个子模块借用函数,来定义 DataFrame 的功能性操作。 -
generic.py:该模块定义了通用的 NDFrame 基类,这是 pandas 中 DataFrame、series 和 panel 类的基类。NDFrame 继承自PandasObject,后者在base.py中定义。NDFrame 可以视为 pandas DataFrame 的 N 维版本。有关更多信息,请访问nullege.com/codes/search/pandas.core.generic.NDFrame。 -
categorical.py:该模块定义了categorical,它是从PandasObject派生的一个类,用于表示类似于 R/S-plus 的分类变量(稍后我们会进一步扩展这一点)。 -
groupby.py:该模块定义了多种类,用于实现groupby功能:-
Splitter 类:包括
DataSplitter、ArraySplitter、SeriesSplitter、FrameSplitter和NDFrameSplitter。 -
Grouper/grouping 类:包括
Grouper、GroupBy、BaseGrouper、BinGrouper、Grouping、SeriesGroupBy和NDFrameGroupBy。
-
-
ops.py:此文件定义了一个内部 API,用于对PandasObjects执行算术运算。它定义了为对象添加算术方法的函数。它定义了一个_create_methods元方法,用于通过算术、比较和布尔方法构造器创建其他方法。add_methods方法接受一个新方法列表,将它们添加到现有方法列表中,并将它们绑定到适当的类。add_special_arithmetic_methods、add_flex_arithmetic_methods、call _create_methods和add_methods用于向类中添加算术方法。
它定义了_TimeOp类,这是一个用于日期时间相关算术运算的封装类。它包含了对 series、DataFrame 和 panel 函数进行算术、比较和布尔操作的封装函数:_arith_method_SERIES(..)、_comp_method_SERIES(..)、_bool_method_SERIES(..)、_flex_method_SERIES(..)、_arith_method_FRAME(..)、_comp_method_FRAME(..)、_flex_comp_method_FRAME(..)、_arith_method_PANEL(..)和_comp_method_PANEL(..)。
-
index.py:此文件定义了索引类及其相关功能。Index 是所有 pandas 对象(如 series、DataFrame 和 panel)用来存储轴标签的工具。它下面是一个不可变的数组,提供一个有序的集合,可以进行切片操作。 -
indexing.py:此模块包含一系列函数和类,使得多重索引操作更加简便。 -
missing.py:此文件定义了诸如掩蔽和插值等技术,用于处理缺失数据。 -
internals.py:此文件定义了多个对象类,具体如下所示:-
Block:这是一个同质类型的 N 维numpy.ndarray对象,具有额外的 pandas 功能——例如,它使用__slots__来限制对象的属性为ndim、values和_mgr_locs。它作为其他 Block 子类的基类。 -
NumericBlock:这是一个用于处理数值类型区块的基类。 -
FloatOrComplexBlock:这是FloatBlock和ComplexBlock的基类,继承自`NumericBlock`。 -
ComplexBlock:这是处理复数类型区块对象的类。 -
FloatBlock:这是处理浮动类型区块对象的类。 -
IntBlock:这是处理整数类型区块对象的类。 -
TimeDeltaBlock、BoolBlock和DatetimeBlock:这些是处理时间差、布尔值和日期时间的区块类。 -
ObjectBlock:这是处理用户定义对象的区块类。 -
SparseBlock:这是处理相同类型稀疏数组的类。 -
BlockManager:这是管理一组区块对象的类,它不是公开的 API 类。 -
SingleBlockManager:这是管理单个区块的类。 -
JoinUnit:这是一个区块对象的实用类。
-
-
nanops.py:这个子模块包含一组用于专门处理 NaN 值的类和功能。 -
ops.py:该文件定义了 pandas 对象的算术运算。它不是公开 API。 -
panel.py、panel4d.py和panelnd.py:这些提供了 pandas 面板对象的功能。 -
resample.py:该文件定义了用于时间间隔分组和聚合的自定义groupby类。 -
series.py:该文件定义了 pandas Series 类及其从 NDFrame 和IndexOpsMixin继承的各种方法,以适应一维数据结构和一维时间序列数据。 -
sorting.py:该文件定义了排序所需的所有工具。 -
sparse.py:该文件定义了处理稀疏数据结构的导入。稀疏数据结构通过省略匹配 NaN 或缺失值的数据点来进行压缩。有关此的更多信息,请访问pandas.pydata.org/pandas-docs/stable/sparse.html。 -
strings.py:这些函数用于处理字符串操作,如str_replace、str_contains和str_cat。 -
window.py:该模块帮助对数据结构进行窗口处理并计算滚动窗口中的聚合值。
以下图示概述了 Pandas 核心的结构:

现在,让我们继续下一个子模块。
pandas/io
该模块包含多个数据 I/O 模块,具体如下:
-
api.py:该文件定义了数据 I/O API 的各种导入。 -
common.py:该文件定义了 I/O API 的通用功能。 -
clipboards.py:该文件包含跨平台剪贴板方法,支持通过键盘启用复制和粘贴功能。pandas I/O API 包含如pandas.read_clipboard()和pandas.to_clipboard(..)等函数。 -
date_converters.py:该文件定义了日期转换函数。 -
excel.py:该模块用于解析和转换 Excel 数据。它定义了ExcelFile和ExcelWriter类。 -
feather_format.py:该模块读取和写入 Feather 格式的数据。 -
gbq.py:这是用于 Google BigQuery 的模块。 -
html.py:这是用于处理 HTML I/O 的模块。 -
json.py:这是用于处理 pandas 中 JSON I/O 的模块。它定义了Writer、SeriesWriter、FrameWriter、Parser、SeriesParser和FrameParser类。 -
msgpack:该模块读取和写入msgpack格式的数据。 -
packer.py:该文件是一个msgpack序列化程序,支持将 pandas 数据结构读写到磁盘。 -
parquet.py:该模块读取和写入 Parquet 格式的数据。 -
parsers.py:这是定义用于解析和处理文件以创建 pandas DataFrame 的各种函数和类的模块。以下列出的三个read_*函数都有多种可配置选项用于读取。详细信息,请参见bit.ly/1EKDYbP:-
read_csv(..):该函数定义了pandas.read_csv(),用于将 CSV 文件的内容读入 DataFrame。 -
read_table(..):这个方法用于将制表符分隔的表文件读取到 DataFrame 中。 -
read_fwf(..):这个方法用于将固定宽度格式的文件读取到 DataFrame 中。 -
TextFileReader:这是用于读取文本文件的类。 -
ParserBase:这是解析器对象的基类。 -
CParserWrapper、PythonParser:这些分别是 C 和 Python 的解析器。它们都继承自ParserBase。 -
FixedWidthReader:这是用于读取固定宽度数据的类。固定宽度数据文件包含在文件中特定位置的字段。 -
FixedWithFieldParser:这是用于解析固定宽度字段的类,该类继承自PythonParser。
-
-
pickle.py:该模块提供了方法来 pickle(序列化)pandas 对象,方法如下:-
to_pickle(..):该方法用于将对象序列化到文件。 -
read_pickle(..):这个方法用于从文件中读取序列化的对象到 pandas 对象。仅应使用受信任的源。
-
-
pytables.py:这是用于 PyTables 模块的接口,用于将 pandas 数据结构读写到磁盘上的文件。 -
sql.py:这是一个类和函数的集合,旨在从关系型数据库中检索数据,并尽量做到与数据库无关。以下是这些类和函数:-
PandasSQL:这是用于将 pandas 与 SQL 接口的基类。它提供了虚拟的read_sql和to_sql方法,这些方法必须由子类实现。 -
PandasSQLAlchemy:这是PandasSQL的子类,能够使用 SQLAlchemy 在 DataFrame 和 SQL 数据库之间进行转换。 -
PandasSQLTable:这是将 pandas 表(DataFrame)映射到 SQL 表的类。 -
pandasSQL_builder(..):根据提供的参数返回正确的 PandasSQL 子类。 -
PandasSQLTableLegacy:这是PandasSQLTable的遗留支持版本。 -
PandasSQLLegacy:这是PandasSQLTable的遗留支持版本。 -
get_schema(..):这个方法用于获取给定数据框架的 SQL 数据库表架构。 -
read_sql_table(..):这个方法用于将 SQL 数据库表读取到 DataFrame 中。 -
read_sql_query(..):这个方法用于将 SQL 查询读取到 DataFrame 中。 -
read_sql(..):这个方法用于将 SQL 查询/表读取到 DataFrame 中。
-
-
stata.py:这个模块包含用于将 Stata 文件处理为 pandas DataFrame 的工具。 -
sas:此模块包含子模块,用于从 SAS 输出中读取数据。 -
S3.py:这个模块提供与 S3 存储桶的远程连接功能。
pandas/tools
该模块的详细信息如下:
-
plotting.py:这是用于绘图模块的包装器,最近版本中已被弃用。 -
merge.py:这个模块提供用于合并序列、DataFrame 和面板对象的函数,如merge(..)和concat(..),并在最近的版本中已被弃用。
pandas/util
该pandas/util是提供实用功能的模块,模块的详细信息如下:
-
testing.py:该文件提供了用于测试的assertion、debug、unit test和其他类/函数。它包含许多特殊的断言函数,使得检查系列、DataFrame 或面板对象是否相等变得更加容易。一些这些函数包括assert_equal(..)、assert_series_equal(..)、assert_frame_equal(..)和assert_panelnd_equal(..)。pandas.util.testing模块对 pandas 代码库的贡献者尤其有用,它定义了一个util.TestCase类,还为潜在的代码库贡献者提供了处理区域设置、控制台调试、文件清理、比较器等的工具。 -
doctools.py:该子模块包含TablePlotter类,用于为 DataFrame 定义布局。 -
validators.py:该子模块帮助验证传递给函数的参数。例如,它帮助评估参数的长度、默认值和参数值。 -
print_versions.py:该文件定义了get_sys_info()函数,它返回系统信息字典;以及show_versions(..)函数,它显示可用 Python 库的版本。 -
misc.py:该文件定义了一些杂项工具。 -
decorators.py:该文件定义了一些装饰器函数和类。 -
替换和附加类是对函数文档字符串进行替换和附加的装饰器。有关 Python 装饰器的更多信息,请访问
www.artima.com/weblogs/viewpost.jsp?thread=240808。 -
test_decorators.py:该子模块提供了用于测试对象的装饰器。
pandas/tests
这个pandas/tests是提供 pandas 中各种对象测试的模块。具体的库文件名基本上是自解释的,这里我不再进一步详细说明;而是邀请读者自行探索。
pandas/compat
与兼容性相关的功能如下所述:
-
chainmap.py和chainmap_impl.py:这些文件提供了一个ChainMap类,可以将多个dict或映射组合在一起,产生一个可以更新的单一视图。 -
pickle_compat.py:该文件提供了在 0.12 版本之前的 pandas 对象序列化功能。
pandas/computation
这个pandas/computation是提供计算功能的模块,具体内容如下:
-
expressions.py:该文件通过numexpr提供快速的表达式计算。numexpr函数用于加速某些数值操作。它使用多个核心以及智能分块和缓存加速。它定义了evaluate(..)和where(..)方法。该模块在最新版本的 pandas 中已被弃用,替代用法将通过pandas.get_option实现。 -
有关
numexpr的更多信息,请访问code.google.com/p/numexpr/。有关此模块的使用,请访问pandas.pydata.org/pandas-docs/version/0.15.0/computation.html。
pandas/plotting
pandas/plotting 是处理所有 pandas 绘图功能的模块:
-
compat.py:此模块检查版本兼容性。 -
converter.py:此模块有助于处理用于绘图的日期时间值。它可以执行自动缩放时间序列轴和格式化日期时间轴刻度等功能。 -
core.py:此文件定义了一些帮助创建图形的类,如条形图、散点图、六边形箱形图和箱线图。 -
misc.py:此文件提供了一组绘图函数,可以将系列或 DataFrame 作为参数。此模块包含以下子模块,用于执行各种杂项任务,如绘制散点矩阵和 Andrews 曲线:-
scatter_matrix(..):此函数绘制散点图矩阵。 -
andrews_curves(..):此函数将多变量数据绘制为曲线,这些曲线使用样本作为傅里叶级数的系数。 -
parallel_coordinates(..):这是一种绘图技术,可以帮助你观察数据中的聚类并直观估算统计量。 -
lag_plot(..):此函数用于检查数据集或时间序列是否为随机的。 -
autocorrelation_plot(..):此函数用于检查时间序列中的随机性。 -
bootstrap_plot(..):此图用于以可视化的方式确定统计量(如均值或中位数)的不确定性。 -
radviz(..):此图用于可视化多变量数据。
-
-
style.py:此文件提供了一组绘图样式选项。 -
timeseries.py:此文件定义了时间序列绘图的辅助类。 -
tools.py:此文件包含一些辅助函数,用于从 DataFrame 和系列创建表格布局。
pandas/tseries
本节内容涉及 pandas/tseries 模块,它赋予 pandas 处理时间序列数据的功能:
-
api.py:这是一个方便的导入集合。 -
converter.py:此文件定义了一组用于格式化和转换的类。 -
datetime:导入 pandas 后,它会通过register()函数将一组单位转换器注册到matplotlib。具体方法如下:
In [1]: import matplotlib.units as munits
In [2]: munits.registry
Out[2]: {}
In [3]: import pandas
In [4]: munits.registry
Out[4]:
{pandas.tslib.Timestamp: <pandas.tseries.converter.DatetimeConverter instance at 0x7fbbc4db17e8>,
pandas.tseries.period.Period: <pandas.tseries.converter.PeriodConverter instance at 0x7fbbc4dc25f0>,
datetime.date: <pandas.tseries.converter.DatetimeConverter instance at 0x7fbbc4dc2fc8>,
datetime.datetime: <pandas.tseries.converter.DatetimeConverter instance at 0x7fbbc4dc2a70>,
datetime.time: <pandas.tseries.converter.TimeConverter instance at 0x7fbbc4d61e18>}
-
Converter:此类包括TimeConverter、PeriodConverter和DateTimeConverter。 -
Formatters:此类包括TimeFormatter、PandasAutoDateFormatter和TimeSeries_DateFormatter。 -
Locators:此类包括PandasAutoDateLocator、MilliSecondLocator和TimeSeries_DateLocator。
Formatter 和 Locator 类用于处理 matplotlib 绘图中的刻度。
-
frequencies.py:此文件定义了指定时间序列对象频率的代码——如每日、每周、每季度、每月、每年等。此子模块依赖于 pandas/core 模块的dtypes子模块。 -
holiday.py:该文件定义了处理假期的函数和类——如Holiday、AbstractHolidayCalendar和USFederalHolidayCalendar等类。 -
offsets.py:该文件定义了各种类,包括处理与时间相关的偏移类。这些类的具体说明如下:-
DateOffset:这是一个接口,供提供时间段功能的类使用,如Week、WeekOfMonth、LastWeekOfMonth、QuarterOffset、YearOffset、Easter、FY5253和FY5253Quarter。 -
BusinessMixin:这是一个混合类,用于商业对象提供与时间相关的功能。它被BusinessDay类继承。BusinessDay子类继承自BusinessMixin和SingleConstructorOffset,并提供在工作日上的偏移。 -
MonthOffset:这是提供月时间段功能的类的接口,如MonthEnd、MonthBegin、BusinessMonthEnd和BusinessMonthBegin。 -
MonthEnd和MonthBegin:这两个提供了在月末或月初的日期偏移,偏移量为一个月。 -
BusinessMonthEnd和BusinessMonthBegin:这两个提供了在商业日历的月末或月初的日期偏移,偏移量为一个月。 -
YearOffset:这个偏移量是由提供年周期功能的类继承的——如YearEnd、YearBegin、BYearEnd和BYearBegin。 -
YearEnd和YearBegin:这两个提供了在年末或年初的日期偏移,偏移量为一年。 -
BYearEnd和BYearBegin:这两个提供了在商业日历的结束或开始时的日期偏移,偏移量为一年。 -
Week:它提供了一个星期的偏移。 -
WeekDay:它提供了从星期几(例如,Tue)到一周中的任何一天(例如,=2)的映射。 -
WeekOfMonth和LastWeekOfMonth:这描述了每个月中某周的日期。 -
QuarterOffset:这是提供季度周期功能的类的基类——如QuarterEnd、QuarterBegin、BQuarterEnd和BQuarterBegin。 -
QuarterEnd、QuarterBegin、BQuarterEnd和BQuarterBegin:这些类与Year*类似,不同之处在于其周期是季度,而不是年份。 -
FY5253和FY5253Quarter:这些类分别描述了 52 周和 53 周的财政年度,也称为 4-4-5 日历。 -
Easter:这是DateOffset类,用于复活节假期。 -
Tick:这是时间单位类的基类,例如Day、Hour、Minute、Second、Milli、Micro和Nano。
-
-
plotting.py:该文件从pandas-plotting模块导入tsplot(..)子模块。
接下来,我们将看到如何使用 Python 扩展来提高 Python 代码的性能。
使用 Python 扩展提高性能
Python 和 pandas 用户的一个抱怨是,虽然语言和模块的易用性和表达性非常强大,但也伴随着显著的缺点——性能。这种情况尤其在数值计算时显现。
根据编程基准标准,Python 在许多算法或数据结构操作上通常比编译语言(如 C/C++)慢。例如,在一个模拟实验中,Python3 的运行速度比最快的 C++ 实现的 n 体模拟计算慢了 104 倍。
那么,我们如何解决这个合法却令人烦恼的问题呢?我们可以在保持 Python 中我们喜爱的东西——清晰性和生产力——的同时,减缓 Python 的运行速度。我们可以通过编写代码中的性能敏感部分(例如,数字处理、算法等)为 C/C++ 代码,并通过编写 Python 扩展模块让 Python 调用这些代码来实现。更多详细信息,请访问 docs.python.org/2/extending/extending.html。
Python 扩展模块使我们能够从 Python 调用用户定义的 C/C++ 代码或库函数,从而提高代码性能,并享受 Python 使用的便利。
为了帮助我们理解 Python 扩展模块是什么,让我们考虑在 Python 中导入一个模块时发生了什么。导入语句导入一个模块,但这究竟意味着什么呢?有三种可能性,具体如下:
-
一些 Python 扩展模块在构建解释器时与之链接。
-
导入语句会导致 Python 加载
.pyc文件到内存中。.pyc文件包含 Python 的字节码,如以下代码片段所示:
In [3]: import pandas
pandas.__file__
Out[3]: '/usr/lib/python2.7/site-packages/pandas/__init__.pyc'
- 导入语句会导致 Python 扩展模块加载到内存中。
.so(共享对象)文件包含了机器码,如以下代码片段所示:
In [4]: import math
math.__file__
Out[4]: '/usr/lib/python2.7/lib-dynload/math.so'
我们将重点讨论第三种可能性,因为这是最常见的情况。即使我们处理的是从 C 编译出来的二进制共享对象,我们仍然可以将其作为 Python 模块导入。这展示了 Python 扩展的强大——应用程序可以从 Python 机器码或机器码导入模块,接口是相同的。Cython 和 SWIG 是用 C 和 C++ 编写扩展的两种最流行的方法。在编写扩展时,我们将 C/C++ 机器码封装起来,并将其转化为表现得像纯 Python 代码的 Python 扩展模块。在这次简短的讨论中,我们将只关注 Cython,因为它是专为 Python 设计的。
Cython 是 Python 的一个超集,旨在通过允许我们调用外部编译的 C/C++ 代码,并声明变量的类型,显著提高 Python 的性能。
Cython 命令从 Cython 源文件生成优化的 C/C++ 源文件,并将此优化后的 C/C++ 源文件编译成 Python 扩展模块。它为 NumPy 提供了内置支持,并将 C 的性能与 Python 的可用性结合起来。
我们将快速演示如何使用 Cython 显著加速我们的代码。让我们定义一个简单的斐波那契函数:
In [17]: def fibonacci(n):
a,b=1,1
for i in range(n):
a,b=a+b,a
return a
In [18]: fibonacci(100)
Out[18]: 927372692193078999176L
In [19]: %timeit fibonacci(100)
100000 loops, best of 3: 18.2 µs per loop
使用 timeit 模块,我们发现每次循环需要 18.2 微秒。
现在让我们通过以下步骤,在 Cython 中重写函数,并为变量指定类型:
- 首先,我们在 iPython 中导入 Cython 魔法函数,如下所示:
In [22]: %load_ext cythonmagic
- 接下来,我们在 Cython 中重写我们的函数,并为我们的变量指定类型:
In [24]: %%cython
def cfibonacci(int n):
cdef int i, a,b
for i in range(n):
a,b=a+b,a
return a
- 让我们测试一下我们新的 Cython 函数的执行时间:
In [25]: %timeit cfibonacci(100)
1000000 loops, best of 3: 321 ns per loop
In [26]: 18.2/0.321
Out[26]: 56.69781931464174
- 我们可以看到,Cython 版本比纯 Python 版本快了 57 倍!
有关使用 Cython/SWIG 或其他选项编写 Python 扩展的更多信息,请参考以下来源:
-
Pandas 文档,标题为 提升性能,链接:
pandas.pydata.org/pandas-docs/stable/enhancingperf.html -
ScipPy 讲义,标题为 与 C 接口,链接:
scipy-lectures.github.io/advanced/interfacing_with_c/interfacing_with_c.html -
Cython 文档,链接:
docs.cython.org/index.html -
SWIG 文档,链接:
www.swig.org/Doc2.0/SWIGDocumentation.html
总结
总结本章内容,我们浏览了 pandas 库的层次结构,试图说明该库的内部构造。理解这些内容对于从 pandas 代码构建自定义模块或作为开源贡献者改进 pandas 的功能将非常有帮助。我们还讨论了通过使用 Python 扩展模块来加速代码性能的好处。
在下一章中,我们将看到 pandas 与其他数据分析工具在各种分析操作方面的比较。
第十五章:pandas 与其他工具的比较
本章重点比较 pandas 与 R(许多 pandas 功能的模型工具)、SQL 和 SAS 等工具的异同,后者与 pandas 有着显著的重叠。本章旨在为希望使用 pandas 的 R、SQL 和 SAS 用户提供指南,也为希望在 pandas 中重现其代码功能的用户提供帮助。本章重点介绍 R、SQL 和 SAS 用户可用的一些关键功能,并通过一些示例演示如何在 pandas 中实现类似功能。本章假设您已安装 R 统计软件包。如果没有,您可以从此处下载并安装:www.r-project.org/。
在本章结束时,数据分析用户应该能够很好地掌握这些工具相对于 pandas 的数据分析能力,从而在需要时能够顺利过渡到或使用 pandas。工具比较的各个因素包括:
-
数据类型及其 pandas 对应物
-
切片与选择
-
数据类型列的算术运算
-
聚合与 GroupBy
-
匹配
-
分割-应用-合并
-
数据重塑与熔化
-
因子和分类数据
与 R 的比较
R 是 pandas 设计灵感的工具。两者在语法、用法和输出方面非常相似。主要的差异出现在某些数据类型上,例如 R 中的矩阵与 pandas 中的数组,R 中的 aggregate 函数与 pandas 中的 GroupBy 操作,以及一些功能相似的函数在语法上的细微差别,如 melt 和 cut。
R 中的数据类型
R 具有五种原始类型或原子类型:
-
字符
-
数值型
-
整数
-
复数
-
逻辑/布尔值
它还具有以下更复杂的容器类型:
-
向量:这与
numpy.array相似。它只能包含相同类型的对象。 -
列表:这是一个异构容器。它在 pandas 中的对应物是一个序列(series)。
-
数据框(DataFrame):这是一个异构的二维容器,相当于 pandas 的 DataFrame。
-
矩阵:这是一个同质的二维版本的向量。它类似于
numpy.array。
本章我们将重点介绍列表和数据框,它们在 pandas 中的对应物是:序列(series)和数据框(DataFrame)。
有关 R 数据类型的更多信息,请参考以下文档:www.statmethods.net/input/datatypes.html。
有关 NumPy 数据类型的更多信息,请参考以下文档:docs.scipy.org/doc/numpy/reference/generated/numpy.array.html 和 docs.scipy.org/doc/numpy/reference/generated/numpy.matrix.html。
R 列表
R 列表可以通过显式的列表声明创建,如下所示:
>h_lst<- list(23,'donkey',5.6,1+4i,TRUE)
>h_lst
[[1]]
[1] 23
[[2]]
[1] "donkey"
[[3]]
[1] 5.6
[[4]]
[1] 1+4i
[[5]]
[1] TRUE
>typeof(h_lst)
[1] "list"
以下代码块展示了其在 pandas 中的系列等效版本,其中包括列表的创建,随后从中创建 Series:
In [8]: h_list=[23, 'donkey', 5.6,1+4j, True]
In [9]: import pandas as pd
h_ser=pd.Series(h_list)
In [10]: h_ser
Out[10]: 0 23
1 donkey
2 5.6
3 (1+4j)
4 True
dtype: object
在 pandas 中,数组的索引从 0 开始,而在 R 中,索引从 1 开始。以下是一个示例:
In [11]: type(h_ser)
Out[11]: pandas.core.series.Series
R DataFrames
我们可以通过调用data.frame()构造函数来构造 R DataFrame,然后如下面所示展示它:
>stocks_table<- data.frame(Symbol=c('GOOG','AMZN','FB','AAPL',
'TWTR','NFLX','LINKD'),
Price=c(518.7,307.82,74.9,109.7,37.1,
334.48,219.9),
MarketCap=c(352.8,142.29,216.98,643.55,23.54,20.15,27.31))
>stocks_table
Symbol PriceMarketCap
1 GOOG 518.70 352.80
2 AMZN 307.82 142.29
3 FB 74.90 216.98
4 AAPL 109.70 643.55
5 TWTR 37.10 23.54
6 NFLX 334.48 20.15
7 LINKD 219.90 27.31
在以下代码块中,我们构造了一个 pandas DataFrame 并展示出来:
In [29]: stocks_df=pd.DataFrame({'Symbol':['GOOG','AMZN','FB','AAPL',
'TWTR','NFLX','LNKD'],
'Price':[518.7,307.82,74.9,109.7,37.1,
334.48,219.9],
'MarketCap($B)' : [352.8,142.29,216.98,643.55,
23.54,20.15,27.31]
})
stocks_df=stocks_df.reindex_axis(sorted(stocks_df.columns,reverse=True),axis=1)
stocks_df
Out[29]:
Symbol PriceMarketCap($B)
0 GOOG 518.70 352.80
1 AMZN 307.82 142.29
2 FB 74.90 216.98
3 AAPL 109.70 643.55
4 TWTR 37.10 23.54
5 NFLX 334.48 20.15
6 LNKD219.90 27.31
切片和选择
在 R 中,我们通过以下三种方式进行对象切片:
-
[: 这总是返回与原始对象相同类型的对象,并且可以用来选择多个元素。 -
[[:用于提取列表或 DataFrame 的元素,只能用于提取单个元素。返回的元素类型不一定是列表或 DataFrame。 -
$:用于通过名称提取列表或 DataFrame 的元素,类似于[[。
以下是 R 中一些切片示例及其在 pandas 中的等效形式:
比较 R 矩阵和 NumPy 数组
让我们看一下 R 中的创建和选择:
>r_mat<- matrix(2:13,4,3)
>r_mat
[,1] [,2] [,3]
[1,] 2 6 10
[2,] 3 7 11
[3,] 4 8 12
[4,] 5 9 13
要选择第一行,我们写出如下代码:
>r_mat[1,]
[1] 2 6 10
要选择第二列,我们使用如下命令:
>r_mat[,2]
[1] 6 7 8 9
现在,我们来看 NumPy 数组的创建和选择:
In [60]: a=np.array(range(2,6))
b=np.array(range(6,10))
c=np.array(range(10,14))
In [66]: np_ar=np.column_stack([a,b,c])
np_ar
Out[66]: array([[ 2, 6, 10],
[ 3, 7, 11],
[ 4, 8, 12],
[ 5, 9, 13]])
要选择第一行,我们使用如下命令:
In [79]: np_ar[0,]
Out[79]: array([ 2, 6, 10])
R 和 pandas/NumPy 的索引方式不同。
在 R 中,索引从 1 开始,而在 pandas/NumPy 中,索引从 0 开始。因此,在将 R 转换为 pandas/NumPy 时,我们需要将所有索引减去 1。
要选择第二列,我们使用如下命令:
In [81]: np_ar[:,1]
Out[81]: array([6, 7, 8, 9])
另一种方法是先转置数组,然后选择列,如下所示:
In [80]: np_ar.T[1,]
Out[80]: array([6, 7, 8, 9])
比较 R 列表和 pandas 系列
在 R 中,列表的创建和选择如下所示:
>cal_lst<- list(weekdays=1:8, mth='jan')
>cal_lst
$weekdays
[1] 1 2 3 4 5 6 7 8
$mth
[1] "jan"
>cal_lst[1]
$weekdays
[1] 1 2 3 4 5 6 7 8
>cal_lst[[1]]
[1] 1 2 3 4 5 6 7 8
>cal_lst[2]
$mth
[1] "jan"
在 pandas 中,Series 的创建和选择如下所示:
In [92]: cal_df= pd.Series({'weekdays':range(1,8), 'mth':'jan'})
In [93]: cal_df
Out[93]: mthjan
weekdays [1, 2, 3, 4, 5, 6, 7]
dtype: object
In [97]: cal_df[0]
Out[97]: 'jan'
In [95]: cal_df[1]
Out[95]: [1, 2, 3, 4, 5, 6, 7]
In [96]: cal_df[[1]]
Out[96]: weekdays [1, 2, 3, 4, 5, 6, 7]
dtype: object
在这里,我们可以看到 R 列表和 pandas 系列在[]和[[]]运算符下的不同。通过考虑第二个项目——一个字符字符串,我们可以看出区别。
在 R 中,[]运算符返回容器类型,即一个包含字符串的列表,而[[]]运算符返回原子类型,在这个例子中是字符类型,如下所示:
>typeof(cal_lst[2])
[1] "list"
>typeof(cal_lst[[2]])
[1] "character"
在 pandas 中,情况正好相反:[]返回原子类型,而[[]]返回复杂类型,即 Series,如下所示:
In [99]: type(cal_df[0])
Out[99]: str
In [101]: type(cal_df[[0]])
Out[101]: pandas.core.series.Series
在 R 和 pandas 中,都可以通过列名来指定元素。
在 R 中指定列名
在 R 中,可以通过在列名前加上$运算符来完成,如下所示:
>cal_lst$mth
[1] "jan"
> cal_lst$'mth'
[1] "jan"
在 pandas 中指定列名
在 pandas 中,我们可以像往常一样使用方括号中的列名来获取子集:
In [111]: cal_df['mth']
Out[111]: 'jan'
R 和 pandas 在嵌套元素的子集操作上有所不同。例如,要从工作日中获取第四天,我们必须在 R 中使用[[]]运算符:
>cal_lst[[1]][[4]]
[1] 4
>cal_lst[[c(1,4)]]
[1] 4
然而,在 pandas 中,我们只需使用双[]:
In [132]: cal_df[1][3]
Out[132]: 4
R DataFrame 与 pandas DataFrame
在 R DataFrame 和 pandas DataFrame 中选择数据遵循类似的脚本。以下部分解释了我们如何从两者中进行多列选择。
R 中的多列选择
在 R 中,我们通过在方括号内指定向量来选择多个列:
>stocks_table[c('Symbol','Price')]
Symbol Price
1 GOOG 518.70
2 AMZN 307.82
3 FB 74.90
4 AAPL 109.70
5 TWTR 37.10
6 NFLX 334.48
7 LINKD 219.90
>stocks_table[,c('Symbol','Price')]
Symbol Price
1 GOOG 518.70
2 AMZN 307.82
3 FB 74.90
4 AAPL 109.70
5 TWTR 37.10
6 NFLX 334.48
7 LINKD 219.90
在 pandas 中的多列选择
在 pandas 中,我们按照通常的方式使用列名进行子集选择,即列名放在方括号中:
In [140]: stocks_df[['Symbol','Price']]
Out[140]:Symbol Price
0 GOOG 518.70
1 AMZN 307.82
2 FB 74.90
3 AAPL 109.70
4 TWTR 37.10
5 NFLX 334.48
6 LNKD 219.90
In [145]: stocks_df.loc[:,['Symbol','Price']]
Out[145]: Symbol Price
0 GOOG 518.70
1 AMZN 307.82
2 FB 74.90
3 AAPL 109.70
4 TWTR 37.10
5 NFLX 334.48
6 LNKD 219.90
列上的算术操作
在 R 和 pandas 中,我们可以以类似的方式在数据列中应用算术运算。因此,我们可以对两个或多个 DataFrame 中对应位置的元素执行加法或减法等算术操作。
在这里,我们构建一个 R 中的 DataFrame,列标为 x 和 y,并将列 y 从列 x 中减去:
>norm_df<- data.frame(x=rnorm(7,0,1), y=rnorm(7,0,1))
>norm_df$x - norm_df$y
[1] -1.3870730 2.4681458 -4.6991395 0.2978311 -0.8492245 1.5851009 -1.4620324
R 中的 with 运算符也具有与算术操作相同的效果:
>with(norm_df,x-y)
[1] -1.3870730 2.4681458 -4.6991395 0.2978311 -0.8492245 1.5851009 -1.4620324
在 pandas 中,相同的算术操作可以在列上执行,相应的运算符是 eval:
In [10]: import pandas as pd
import numpy as np
df = pd.DataFrame({'x': np.random.normal(0,1,size=7), 'y': np.random.normal(0,1,size=7)})
In [11]: df.x-df.y
Out[11]: 0 -0.107313
1 0.617513
2 -1.517827
3 0.565804
4 -1.630534
5 0.101900
6 0.775186
dtype: float64
In [12]: df.eval('x-y')
Out[12]: 0 -0.107313
1 0.617513
2 -1.517827
3 0.565804
4 -1.630534
5 0.101900
6 0.775186
dtype: float64
聚合与 GroupBy
有时,我们希望将数据拆分成子集,并对每个子集应用一个函数,如平均值、最大值或最小值。在 R 中,我们可以通过 aggregate 或 tapply 函数来实现。
在这里,我们有一个数据集,包含了 2014 年欧洲冠军联赛足球赛半决赛四支球队中五位前锋的统计数据。我们将使用它来说明 R 中的聚合及其在 pandas 中的等效 GroupBy 功能。
R 中的聚合
在 R 中,聚合是通过以下命令实现的:
> goal_stats=read.csv('champ_league_stats_semifinalists.csv')
>goal_stats
Club Player Goals GamesPlayed
1 Atletico Madrid Diego Costa 8 9
2 Atletico Madrid ArdaTuran 4 9
3 Atletico Madrid RaúlGarcía 4 12
4 Atletico Madrid AdriánLópez 2 9
5 Atletico Madrid Diego Godín 2 10
6 Real Madrid Cristiano Ronaldo 17 11
7 Real Madrid Gareth Bale 6 12
8 Real Madrid Karim Benzema 5 11
9 Real Madrid Isco 3 12
10 Real Madrid Ángel Di María 3 11
11 Bayern Munich Thomas Müller 5 12
12 Bayern Munich ArjenRobben 4 10
13 Bayern Munich Mario Götze 3 11
14 Bayern Munich Bastian Schweinsteiger 3 8
15 Bayern Munich Mario Mandzukić 3 10
16 Chelsea Fernando Torres 4 9
17 Chelsea Demba Ba 3 6
18 Chelsea Samuel Eto'o 3 9
19 Chelsea Eden Hazard 2 9
20 Chelsea Ramires 2 10
我们现在计算每个前锋的每场比赛进球比率,以此来衡量他们在门前的致命性:
>goal_stats$GoalsPerGame<- goal_stats$Goals/goal_stats$GamesPlayed
>goal_stats
Club Player Goals GamesPlayedGoalsPerGame
1 Atletico Madrid Diego Costa 8 9 0.8888889
2 Atletico Madrid ArdaTuran 4 9 0.4444444
3 Atletico Madrid RaúlGarcía 4 12 0.3333333
4 Atletico Madrid AdriánLópez 2 9 0.2222222
5 Atletico Madrid Diego Godín 2 10 0.2000000
6 Real Madrid Cristiano Ronaldo 17 11 1.5454545
7 Real Madrid Gareth Bale 6 12 0.5000000
8 Real Madrid Karim Benzema 5 11 0.4545455
9 Real Madrid Isco 3 12 0.2500000
10 Real Madrid Ángel Di María 3 11 0.2727273
11 Bayern Munich Thomas Müller 5 12 0.4166667
12 Bayern Munich ArjenRobben 4 10 0.4000000
13 Bayern Munich MarioGötze 3 11 0.2727273
14 Bayern Munich Bastian Schweinsteiger 3 8 0.3750000
15 Bayern Munich MarioMandzukić 3 10 0.3000000
16 Chelsea Fernando Torres 4 9 0.4444444
17 Chelsea Demba Ba 3 6 0.5000000
18 Chelsea Samuel Eto'o 3 9 0.3333333
19 Chelsea Eden Hazard 2 9 0.2222222
20 Chelsea Ramires 2 10 0.2000000
假设我们想知道每个球队的最高进球比率。我们可以通过以下方式计算:
>aggregate(x=goal_stats[,c('GoalsPerGame')], by=list(goal_stats$Club),FUN=max)
Group.1 x
1 Atletico Madrid 0.8888889
2 Bayern Munich 0.4166667
3 Chelsea 0.5000000
4 Real Madrid 1.5454545
tapply 函数用于对由一个或多个列定义的数组或向量的子集应用函数。tapply 函数也可以如下使用:
>tapply(goal_stats$GoalsPerGame,goal_stats$Club,max)
Atletico Madrid Bayern Munich Chelsea Real Madrid
0.8888889 0.4166667 0.5000000 1.5454545
pandas 中的 GroupBy 运算符
在 pandas 中,我们可以通过使用 GroupBy 函数来实现相同的结果:
In [6]: import pandas as pd
importnumpy as np
In [7]: goal_stats_df=pd.read_csv('champ_league_stats_semifinalists.csv')
In [27]: goal_stats_df['GoalsPerGame']= goal_stats_df['Goals']/goal_stats_df['GamesPlayed']
In [27]: goal_stats_df['GoalsPerGame']= goal_stats_df['Goals']/goal_stats_df['GamesPlayed']
In [28]: goal_stats_df
Out[28]: Club Player Goals GamesPlayedGoalsPerGame
0 Atletico Madrid Diego Costa 8 9 0.888889
1 Atletico Madrid ArdaTuran 4 9 0.444444
2 Atletico Madrid RaúlGarcía 4 12 0.333333
3 Atletico Madrid AdriánLópez 2 9 0.222222
4 Atletico Madrid Diego Godín 2 10 0.200000
5 Real Madrid Cristiano Ronaldo 17 11 1.545455
6 Real Madrid Gareth Bale 6 12 0.500000
7 Real Madrid Karim Benzema 5 11 0.454545
8 Real Madrid Isco 3 12 0.250000
9 Real Madrid Ángel Di María 3 11 0.272727
10 Bayern Munich Thomas Müller 5 12 0.416667
11 Bayern Munich ArjenRobben 4 10 0.400000
12 Bayern Munich Mario Götze 3 11 0.272727
13 Bayern Munich BastianSchweinsteiger 3 8 0.375000
14 Bayern Munich MarioMandzukić 3 10 0.300000
15 Chelsea Fernando Torres 4 9 0.444444
16 Chelsea Demba Ba 3 6 0.500000
17 Chelsea Samuel Eto'o 3 9 0.333333
18 Chelsea Eden Hazard 2 9 0.222222
19 Chelsea Ramires 2 10 0.200000
In [30]: grouped = goal_stats_df.groupby('Club')
In [17]: grouped['GoalsPerGame'].aggregate(np.max)
Out[17]: Club
Atletico Madrid 0.888889
Bayern Munich 0.416667
Chelsea 0.500000
Real Madrid 1.545455
Name: GoalsPerGame, dtype: float64
In [22]: grouped['GoalsPerGame'].apply(np.max)
Out[22]: Club
Atletico Madrid 0.888889
Bayern Munich 0.416667
Chelsea 0.500000
Real Madrid 1.545455
Name: GoalsPerGame, dtype: float64
比较 R 和 pandas 中的匹配运算符
在这里,我们演示了 R 中的匹配运算符 (%in%) 和 pandas 中的匹配运算符 (isin()) 之间的等效性。在这两种情况下,都会生成一个逻辑向量(R)或系列(pandas),它表示找到匹配的位置信息。
R 中的 %in% 运算符
在这里,我们演示了如何在 R 中使用 %in% 运算符:
>stock_symbols=stocks_table$Symbol
>stock_symbols
[1] GOOG AMZN FB AAPL TWTR NFLX LINKD
Levels: AAPL AMZN FB GOOG LINKD NFLX TWTR
>stock_symbols %in% c('GOOG','NFLX')
[1] TRUE FALSE FALSE FALSE FALSE TRUE FALSE
pandas 中的 isin() 函数
下面是一个使用 pandas isin() 函数的例子:
In [11]: stock_symbols=stocks_df.Symbol
stock_symbols
Out[11]: 0 GOOG
1 AMZN
2 FB
3 AAPL
4 TWTR
5 NFLX
6 LNKD
Name: Symbol, dtype: object
In [10]: stock_symbols.isin(['GOOG','NFLX'])
Out[10]: 0 True
1 False
2 False
3 False
4 False
5 True
6 False
Name: Symbol, dtype: bool
逻辑子集选择
在 R 和 pandas 中,有多种方式可以执行逻辑子集选择。假设我们希望显示所有平均每场比赛进球数大于或等于 0.5 的球员;也就是说,平均每两场比赛至少进一球。
R 中的逻辑子集选择
这是我们在 R 中如何实现的:
- 使用逻辑切片:
>goal_stats[goal_stats$GoalsPerGame>=0.5,]
Club Player Goals GamesPlayedGoalsPerGame
1 Atletico Madrid Diego Costa 8 9 0.8888889
6 Real Madrid Cristiano Ronaldo 17 11 1.5454545
7 Real Madrid Gareth Bale 6 12 0.5000000
17 Chelsea Demba Ba 3 6 0.5000000
- 使用
subset()函数:
>subset(goal_stats,GoalsPerGame>=0.5)
Club Player Goals GamesPlayedGoalsPerGame
1 Atletico Madrid Diego Costa 8 9 0.8888889
6 Real Madrid Cristiano Ronaldo 17 11 1.5454545
7 Real Madrid Gareth Bale 6 12 0.5000000
17 Chelsea Demba Ba 3 6 0.5000000
pandas 中的逻辑子集选择
在 pandas 中,我们做类似的操作:
- 逻辑切片:
In [33]: goal_stats_df[goal_stats_df['GoalsPerGame']>=0.5]
Out[33]: Club Player Goals GamesPlayedGoalsPerGame
0 Atletico Madrid Diego Costa 8 9 0.888889
5 Real Madrid Cristiano Ronaldo 17 11 1.545455
6 Real Madrid Gareth Bale 6 12 0.500000
16 Chelsea Demba Ba 3 6 0.500000
DataFrame.query()运算符:
In [36]: goal_stats_df.query('GoalsPerGame>= 0.5')
Out[36]:
Club Player Goals GamesPlayedGoalsPerGame
0 Atletico Madrid Diego Costa 8 9 0.888889
5 Real Madrid Cristiano Ronaldo 17 11 1.545455
6 Real Madrid Gareth Bale 6 12 0.500000
16 Chelsea Demba Ba 3 6 0.500000
分割-应用-合并
R 有一个叫做plyr的库,用于拆分-应用-合并数据分析。plyr库有一个函数叫做ddply,它可以用来将一个函数应用到 DataFrame 的子集上,然后将结果合并成另一个 DataFrame。
有关ddply的更多信息,请参考以下链接:www.inside-r.org/packages/cran/plyr/docs/ddply。
为了说明,让我们考虑一个最近创建的 R 数据集的子集,包含 2013 年从纽约市出发的航班数据:cran.r-project.org/web/packages/nycflights13/index.html。
在 R 中的实现
在这里,我们在 R 中安装该包并实例化库:
>install.packages('nycflights13')
...
>library('nycflights13')
>dim(flights)
[1] 336776 16
>head(flights,3)
year month day dep_timedep_delayarr_timearr_delay carrier tailnum flight
1 2013 1 1 517 2 830 11 UA N14228 1545
2 2013 1 1 533 4 850 20 UA N24211 1714
3 2013 1 1 542 2 923 33 AA N619AA 1141
origindestair_time distance hour minute
1 EWR IAH 227 1400 5 17
2 LGA IAH 227 1416 5 33
3 JFK MIA 160 1089 5 42
> flights.data=na.omit(flights[,c('year','month','dep_delay','arr_delay','distance')])
>flights.sample<- flights.data[sample(1:nrow(flights.data),100,replace=FALSE),]
>head(flights.sample,5)
year month dep_delayarr_delay distance
155501 2013 3 2 5 184
2410 2013 1 0 4 762
64158 2013 11 -7 -27 509
221447 2013 5 -5 -12 184
281887 2013 8 -1 -10 937
ddply函数使我们能够按年份和月份总结出发延迟(均值和标准差):
>ddply(flights.sample,.(year,month),summarize, mean_dep_delay=round(mean(dep_delay),2), s_dep_delay=round(sd(dep_delay),2))
year month mean_dep_delaysd_dep_delay
1 2013 1 -0.20 2.28
2 2013 2 23.85 61.63
3 2013 3 10.00 34.72
4 2013 4 0.88 12.56
5 2013 5 8.56 32.42
6 2013 6 58.14 145.78
7 2013 7 25.29 58.88
8 2013 8 25.86 59.38
9 2013 9 -0.38 10.25
10 2013 10 9.31 15.27
11 2013 11 -1.09 7.73
12 2013 12 0.00 8.58
让我们将flights.sample数据集保存为 CSV 文件,这样我们就可以使用这些数据向我们展示如何在 pandas 中做相同的操作:
>write.csv(flights.sample,file='nycflights13_sample.csv', quote=FALSE,row.names=FALSE)
在 pandas 中的实现
为了在 pandas 中做同样的操作,我们读取前面章节保存的 CSV 文件:
In [40]: flights_sample=pd.read_csv('nycflights13_sample.csv')
In [41]: flights_sample.head()
Out[41]: year month dep_delayarr_delay distance
0 2013 3 2 5 184
1 2013 1 0 4 762
2 2013 11 -7 -27 509
3 2013 5 -5 -12 184
4 2013 8 -1 -10 937
我们通过使用GroupBy()操作符实现了与ddply相同的效果,如下所示的代码和输出:
In [44]: pd.set_option('precision',3)
In [45]: grouped = flights_sample_df.groupby(['year','month'])
In [48]: grouped['dep_delay'].agg([np.mean, np.std])
Out[48]: mean std
year month
2013 1 -0.20 2.28
2 23.85 61.63
3 10.00 34.72
4 0.88 12.56
5 8.56 32.42
6 58.14 145.78
7 25.29 58.88
8 25.86 59.38
9 -0.38 10.25
10 9.31 15.27
11 -1.09 7.73
12 0.00 8.58
使用 melt 进行重塑
melt函数将宽格式数据转换为由唯一 ID-变量组合组成的单列数据。
R 中的 melt 函数
这里,我们展示了在 R 中使用melt()函数。它生成了长格式数据,其中每一行都是独特的变量-值组合:
>sample4=head(flights.sample,4)[c('year','month','dep_delay','arr_delay')]
> sample4
year month dep_delayarr_delay
155501 2013 3 2 5
2410 2013 1 0 4
64158 2013 11 -7 -27
221447 2013 5 -5 -12
>melt(sample4,id=c('year','month'))
year month variable value
1 2013 3 dep_delay 2
2 2013 1 dep_delay 0
3 2013 11 dep_delay -7
4 2013 5 dep_delay -5
5 2013 3 arr_delay 5
6 2013 1 arr_delay 4
7 2013 11 arr_delay -27
8 2013 5 arr_delay -12
>
更多信息请参考以下链接:www.statmethods.net/management/reshape.html。
pandas 的 melt 函数
在 pandas 中,melt函数类似:
In [55]: sample_4_df=flights_sample_df[['year','month','dep_delay', \
'arr_delay']].head(4)
In [56]: sample_4_df
Out[56]: year month dep_delayarr_delay
0 2013 3 2 5
1 2013 1 0 4
2 2013 11 -7 -27
3 2013 5 -5 -12
In [59]: pd.melt(sample_4_df,id_vars=['year','month'])
Out[59]: year month variable value
0 2013 3 dep_delay 2
1 2013 1 dep_delay 0
2 2013 11 dep_delay -7
3 2013 5 dep_delay -5
4 2013 3 arr_delay 5
5 2013 1 arr_delay 4
6 2013 11 arr_delay -27
7 2013 5 arr_delay -12
该信息的参考来源如下:pandas.pydata.org/pandas-docs/stable/reshaping.html#reshaping-by-melt。
分类数据
在 R 中,分类变量称为因子,cut()函数使我们能够将连续的数值变量分割成不同的范围,并将这些范围视为因子或分类变量,或者将分类变量划分到更大的区间中。
R 示例使用 cut()
以下代码块展示了 R 中的一个示例:
clinical.trial<- data.frame(patient = 1:1000,
age = rnorm(1000, mean = 50, sd = 5),
year.enroll = sample(paste("19", 80:99, sep = ""),
1000, replace = TRUE))
>clinical.trial<- data.frame(patient = 1:1000,
+ age = rnorm(1000, mean = 50, sd = 5),
+ year.enroll = sample(paste("19", 80:99, sep = ""),
+ 1000, replace = TRUE))
>summary(clinical.trial)
patient age year.enroll
Min. : 1.0 Min. :31.14 1995 : 61
1st Qu.: 250.8 1st Qu.:46.77 1989 : 60
Median : 500.5 Median :50.14 1985 : 57
Mean : 500.5 Mean :50.14 1988 : 57
3rd Qu.: 750.2 3rd Qu.:53.50 1990 : 56
Max. :1000.0 Max. :70.15 1991 : 55
(Other):654
>ctcut<- cut(clinical.trial$age, breaks = 5)> table(ctcut)
ctcut
(31.1,38.9] (38.9,46.7] (46.7,54.6] (54.6,62.4] (62.4,70.2]
15 232 558 186 9
pandas 解决方案
以下代码块包含了之前解释的cut()函数在 pandas 中的等效实现(仅适用于版本 0.15 及以上):
In [79]: pd.set_option('precision',4)
clinical_trial=pd.DataFrame({'patient':range(1,1001),
'age' : np.random.normal(50,5,size=1000),
'year_enroll': [str(x) for x in np.random.choice(range(1980,2000),size=1000,replace=True)]})
In [80]: clinical_trial.describe()
Out[80]: age patient
count 1000.000 1000.000
mean 50.089 500.500
std 4.909 288.819
min 29.944 1.000
25% 46.572 250.750
50% 50.314 500.500
75% 53.320 750.250
max 63.458 1000.000
In [81]: clinical_trial.describe(include=['O'])
Out[81]: year_enroll
count 1000
unique 20
top 1992
freq 62
In [82]: clinical_trial.year_enroll.value_counts()[:6]
Out[82]: 1992 62
1985 61
1986 59
1994 59
1983 58
1991 58
dtype: int64
In [83]: ctcut=pd.cut(clinical_trial['age'], 5)
In [84]: ctcut.head()
Out[84]: 0 (43.349, 50.052]
1 (50.052, 56.755]
2 (50.052, 56.755]
3 (43.349, 50.052]
4 (50.052, 56.755]
Name: age, dtype: category
Categories (5, object): [(29.91, 36.646] < (36.646, 43.349] < (43.349, 50.052] < (50.052, 56.755] < (56.755, 63.458]]
In [85]: ctcut.value_counts().sort_index()
Out[85]: (29.91, 36.646] 3
(36.646, 43.349] 82
(43.349, 50.052] 396
(50.052, 56.755] 434
(56.755, 63.458] 85
dtype: int64
前面章节中的比较可以通过以下表格进行总结:

R 和 pandas 中数据结构与操作的比较
与 SQL 的比较
pandas 在许多方面与 SQL 类似,它用于数据选择、数据过滤、数据聚合、数据生成和数据修改。SQL 对数据库表执行的操作类似于 pandas 对 DataFrame 所执行的操作。在本节中,我们将比较 SQL 中的功能与其在 pandas 中的等效功能。
SELECT
SELECT 用于选择或子集化表中某些列的数据。假设你有一个名为 DallasData 的表/DataFrame。这些数据可以附在你的书包中,或者从书中的云盘访问。要从三个给定列中选择五行数据,你可以写出如下命令:
SQL
在 SQL 中,你可以使用以下命令:
select state_name,active_status,services_due from DallasData LIMIT 5;
pandas
在 pandas 中,你可以使用以下命令:
DallasData[['state_name','active_status','services_due']].head(5)
以下是前述命令的输出结果:

DallasData 上的选择结果
Where
Where 语句在 SQL 中用于应用过滤条件,根据特定标准筛选行。在 pandas 中,相应的操作是基于条件的逻辑子集选择。
假设我们想找出 active_status == 1 的行。这可以在这两种工具中如下进行。
SQL
在 SQL 中,你可以使用以下命令:
select * from DallasData where active_status ==1 LIMIT 5;
pandas
在 pandas 中,你可以使用以下命令:
DallasData[DallasData['active_status']==1].head(5);
以下是前述命令的输出结果:

过滤掉只有活跃客户后的 DallasData
假设我们想找出活跃的客户(active_status == 1)且已完成的服务少于九项(services_completed < 9)的行。这可以在这两种工具中如下进行。
SQL
在 SQL 中,你可以使用以下命令:
select * from DallasData where active_status ==1 AND services_completed <9 LIMIT 5;
pandas
在 pandas 中,你可以使用以下命令:
DallasData[(DallasData['active_status']==1) & (DallasData['services_completed'] <9)].head(5)
以下是前述命令的输出结果:

过滤掉活跃且已完成超过九项服务的客户后的 DallasData
假设我们想找出活跃的客户(active_status == 1)的行,但只查找这些行的客户 ID、邮政编码和卖家 ID。这可以在这两种工具中如下进行。
SQL
在 SQL 中,你可以使用以下命令:
select customerID,zip,soldBy from DallasData where active_status ==1 LIMIT 5;
pandas
在 pandas 中,你可以使用以下命令:
DallasData[DallasData['active_status']==1][['customerID','zip','soldBy']].head(5)
以下是前述命令的输出结果:

过滤掉只有活跃客户且只选择特定列后的 DallasData
group by
group by 语句用于聚合数据,并查找数值列的聚合值。执行此操作的关键字相同,但语法略有不同。我们来看看几个示例。
假设我们要查找数据集中活跃和非活跃客户的数量。这可以在这两种工具中如下进行。
SQL
在 SQL 中,你可以使用以下命令:
select active_status, count(*) as number from DallasData group by active_status;
pandas
在 pandas 中,你可以使用以下命令:
DallasData.groupby('active_status').size();
以下是前述命令的输出结果:

使用 Python 中的 groupby 统计活跃和非活跃客户的数量
不同的聚合操作可以同时应用于两个不同的列,下面的示例展示了如何操作。
SQL
在 SQL 中,你可以使用以下命令:
select active_status, sum(services_complted), mean(age_median) from DallasData group by active_status;
pandas
在 pandas 中,你可以使用以下命令:
DallasData.groupby('active_status').agg({'services_completed':np.sum,'age_median':np.mean})

按照活跃和非活跃客户分组,统计已完成的服务总数和平均客户年龄,使用 Python 中的 groupby
对多个列进行聚合或多重索引聚合也是可行的。假设我们希望按邮政编码获取活跃和非活跃客户的详细信息。
SQL
在 SQL 中,你可以使用以下命令:
select active_status, sum(services_complted), mean(days_old) from DallasData group by active_status,zip;
pandas
在 pandas 中,你可以使用以下命令:
DallasData.groupby(['active_status','zip']).agg({'services_completed':np.sum,'days_old':np.mean}).head(5)
以下是前述命令的输出:

使用 Python 中的 groupby 按客户的活跃状态和邮政编码进行多重索引分组
update
SQL 中的update语句用于根据某些条件筛选数据行,并更新或修改这些行中的某些值。在 pandas 中,没有特定的关键字或函数来执行此操作;相反,它是通过直接赋值来完成的。让我们看几个例子。
假设已经确定数据管理员在数据收集过程中犯了错误。由于这个错误,实际上年龄为 45 的数据点被随机赋值为大于 35 的数值。为了解决这个问题,我们将把所有这些行(age>35)更新为 45。
SQL
在 SQL 中,你可以使用以下命令:
update DallasData set age_median=45 where age_median>35
pandas
在 pandas 中,你可以使用以下命令:
DallasData[DallasData['age_median']>35]=45
以下两个截图展示了执行更新操作前后的数据:

更新所有年龄大于 35 岁为 45 岁之前和之后的数据
delete
SQL 中的delete语句用于根据某些条件从数据库表中删除数据行。在 pandas 中,我们并不删除行,而是选择取消选中它们。让我们来看一些示例。
假设我们想查看那些在系统中至少存在 500 天的客户(days_old>500)。
SQL
在 SQL 中,你可以使用以下命令:
delete DallasData where days_old<500
pandas
在 pandas 中,你可以使用以下命令:
DallasData1 = DallasData[DallasData['days_old']>500]
运行以下命令以检查是否执行了预期的操作。
DallasData1[DallasData1['days_old']<400]
如果删除操作执行正确,这应返回 0 行数据。
JOIN
join语句用于合并数据库中的不同表,并提取分散在多个表中的重要信息。在 pandas 中,merge 操作符完成了相同的工作。唯一的区别是语法略有不同。
让我们创建两个数据集,用来演示 SQL 和 pandas 中的不同连接及其语法:
df1 = pd.DataFrame({'key': ['IN', 'SA', 'SL', 'NZ'],'Result':['W','L','L','W']})
df2 = pd.DataFrame({'key': ['IN', 'SA', 'SA', 'WI'],'Score':[200,325,178,391]})
以下是前述命令的输出:

两个示例数据集
假设我们想在这两者之间进行内连接。可以按照前述的方法在这两个工具中实现。
SQL
在 SQL 中,你可以使用以下命令:
SELECT * FROM df1 INNER JOIN df2 ON df1.key = df2.key;
pandas
在 pandas 中,你可以使用以下命令:
pd.merge(df1,df2,on='key')
以下是前面命令的输出:

两个 DataFrame 的内连接输出
正如内连接所预期的,只有在两个表中都存在的键值才会出现在合并后的数据集中。
假设我们要在两者之间实现左连接。这可以通过以下两种工具来完成:
SQL
在 SQL 中,你可以使用以下命令:
SELECT * FROM df1 LEFT JOIN df2 ON df1.key = df2.key;
pandas
在 pandas 中,你可以使用以下命令:
pd.merge(df1,df2,on='key',how='left')
以下是前面命令的输出:

两个表的左连接输出
正如左连接所预期的,它会检索左表(此处为 df1)中存在的所有唯一键值及其在右表中的相应值。对于左表中的键值,如果在右表中没有找到匹配项,则返回 NaN。
假设我们要在两者之间实现右连接。这可以通过以下两种工具来完成:
SQL
在 SQL 中,你可以使用以下命令:
SELECT * FROM df1 RIGHT JOIN df2 ON df1.key = df2.key;
pandas
在 pandas 中,你可以使用以下命令:
pd.merge(df1,df2,on='key',how='right')
以下是前面命令的输出:

两个表的右连接输出
正如右连接所预期的,它会检索右表(此处为 df2)中存在的所有唯一键值及其在左表中的相应值。对于右表中的键值,如果在左表中没有找到匹配项,则返回 NaN。
与 SAS 的比较
SAS 是过去的分析利器。在 R 和 Python 这两个开源运动的代表工具取代它之前,SAS 是分析解决方案的市场领导者,曾居于头号地位。然而,尽管其成本过高,许多企业仍然依赖它处理所有的分析需求。
在本节中,我们将所有比较都以表格的形式展示。SAS 和 pandas 的对应关系总结在下表中:
| Pandas | SAS |
|---|---|
| DataFrame | dataset |
| column | variable |
| row | observation |
| groupby | BY-group |
| NaN | . |
现在,让我们看看如何在 pandas 和 SAS 中执行基本的数据操作:
| 任务 | Pandas | SAS |
|---|---|---|
| 创建数据集 | pd.DataFrame({'odds': [1, 3, 5, 7, 9], 'evens': [2, 4, 6, 8, 10]}) |
data df; input x y; datalines; 1 2 3 4 5 6 7 8 9 10; run; |
| 读取数据集 | pd.read_csv('DallasData.csv') |
proc import datafile='DallasData.csv' dbms=csv out=tips replace; getnames=yes; run; |
| 导出数据集 | DallasData.to_csv('dallas.csv') |
proc export data=DallasData outfile='dallas.csv' dbms=csv; run; |
| 列操作 | DallasData['days_old_year'] = DallasData['days_old']/365 |
data DallasData;`` set DallasData;`` days_old_year = days_old / 365;``run; |
| 过滤 | DallasData[DallasData['days_old']>800].head() |
data tips;`` set DallasData;`` if days_old > 800;``run; |
| If-else | DallasData['income_class'] = np.where(DallasData['income_average'] < 40000, 'low', 'high') |
data DallasData;`` set dallas;`` format income_average $5.;`` if days_old < 40000 then bucket = 'low';`` else bucket = 'high';``run; |
| 列选择 | DallasData[['zip','customerID','days_old','services_due']].head() |
data dallas;`` set DallasData;`` keep zip CustomerID days_old services_due;``run; |
| 排序 | dallas = DallasData.sort_values(['days_old','services_completed']) |
proc sort data=DallasData;`` by days_old services_completed;``run; |
| 字符串长度 | DallasData['state_name'].str.len().head() |
data _null_;``set DallasData;``put(LENGTHN(state_name));``put(LENGTHC(state_name));``run; |
| 分组聚合 | dallas_grouped = DallasData.groupby(['zip', 'customerID'])['days_old', 'services_completed'].sum() |
proc summary data=DallasData nway;`` class zip customerID;`` var days_old services_completed;`` output out=dallas_summed sum=;``run; |
| 联接 | df1 = pd.DataFrame({'key': ['A', 'B', 'C', 'D'], 'value': np.random.randn(4)})``df2 = pd.DataFrame({'key': ['B', 'D', 'D', 'E'],'value': np.random.randn(4)})``inner_join = df1.merge(df2, on=['key'], how='inner')``left_join = df1.merge(df2, on=['key'], how='left')``right_join = df1.merge(df2, on=['key'], how='right') |
proc sort data=df1;`` by key;``run;``proc sort data=df2;`` by key;``run;``data left_join inner_join right_join outer_join;`` merge df1(in=a) df2(in=b);`` if a and b then output inner_join;`` if a then output left_join;`` if b then output right_join;`` if a or b then output outer_join;``run; |
总结
在本章中,我们尝试将 R 和 SQL 中的关键功能与 pandas 中的对应功能进行比较,以实现以下目标:
-
帮助 R、SQL 和 SAS 用户,他们可能希望在 pandas 中复制相同的功能
-
为了帮助任何阅读 R、SQL 和 SAS 代码的用户,他们可能希望将代码重写为 pandas 代码
在下一章中,我们将通过简要介绍 scikit-learn 库来进行机器学习,并演示 pandas 如何融入该框架,从而总结本书。本章的参考文档可以在此找到:pandas.pydata.org/pandas-docs/stable/comparison_with_r.html。
第十六章:机器学习概览
本章将带您快速了解机器学习,重点介绍如何使用pandas库作为预处理机器学习程序数据的工具。它还将向您介绍scikit-learn库,这是 Python 中最受欢迎的机器学习工具包。
在本章中,我们将通过将机器学习技术应用于一个著名的问题来说明机器学习,即对哪些乘客在世纪之交的 Titanic 灾难中幸存进行分类。本章将讨论的主题包括:
-
pandas 在机器学习中的作用
-
安装
scikit-learn -
机器学习概念介绍
-
应用机器学习——Kaggle Titanic 竞赛
-
使用 pandas 进行数据分析和预处理
-
针对 Titanic 问题的朴素方法
-
scikit-learn的 ML 分类器接口 -
监督学习算法
-
无监督学习算法
pandas 在机器学习中的作用
我们将考虑的机器学习库是scikit-learn。scikit-learn Python 库是一个广泛的机器学习算法库,可用于创建能够从数据输入中学习的自适应程序。
然而,在这些数据可以被scikit-learn使用之前,它必须经过一些预处理。这正是 pandas 的作用。pandas 可以在将数据传递给scikit-learn实现的算法之前进行预处理和筛选。
在接下来的部分中,我们将看到如何使用scikit-learn进行机器学习。所以,第一步,我们将学习如何在我们的机器上安装它。
scikit-learn的安装
如第二章所述,从第三方供应商安装 Python 和 pandas,安装 pandas 及其附带库的最简单方法是使用像 Anaconda 这样的第三方分发包,安装完成即可。安装scikit-learn应该没有什么不同。我将简要介绍在不同平台和第三方分发包上安装的步骤,首先从 Anaconda 开始。scikit-learn库需要以下库:
-
Python 2.6.x 或更高版本
-
NumPy 1.6.1 或更高版本
-
SciPy 0.9 或更高版本
假设您已经按照第二章中描述的内容安装了 pandas,安装 pandas 和支持软件,那么这些依赖项应该已经就绪。在接下来的部分中,将讨论在不同平台上安装scikit-learn的各种选项。
通过 Anaconda 安装
您可以通过运行conda Python 包管理器在 Anaconda 上安装scikit-learn:
conda install scikit-learn
在 Unix(Linux/macOS)上的安装
对于 Unix 系统,最好从源代码安装(需要 C 编译器)。假设 pandas 和 NumPy 已经安装并且所需的依赖库已就绪,您可以通过 Git 运行以下命令来安装scikit-learn:
git clone https://github.com/scikit-learn/scikit-learn.git cd scikit-learn python setup.py install
可以通过使用pip从PyPi在 Unix 上安装 pandas 库:
pip install pandas
在 Windows 上安装
要在 Windows 上安装,您可以打开控制台并运行以下命令:
pip install -U scikit-learn
要了解更多关于安装的详细信息,您可以查看官方的scikit-learn文档,网址是scikit-learn.org/stable/install.html。
您还可以查看scikit-learn Git 仓库的 README 文件,网址是github.com/scikit-learn/scikit-learn/blob/master/README.rst。
机器学习简介
机器学习是创建从数据中学习的软件程序的艺术。更正式地说,它可以定义为构建自适应程序的实践,这些程序通过可调参数来提高预测性能。它是人工智能的一个子领域。
我们可以根据机器学习程序尝试解决的问题类型来将其分类。这些问题被称为学习问题。广义上讲,这些问题分为两大类——监督学习问题和无监督学习问题。此外,还有一些混合问题,它们涉及监督学习和无监督学习的多个方面。
学习问题的输入由一个包含n行的数据集组成。每一行代表一个样本,可能涉及一个或多个称为属性或特征的字段。数据集可以被标准化描述为包含n个样本,每个样本由n个特征组成。
监督学习与无监督学习
对于监督学习问题,学习问题的输入是一个由已标记数据组成的数据集。这里的“已标记”意味着我们知道输出的值。学习程序会接收输入样本及其对应的输出,目标是解码它们之间的关系。这种输入称为已标记数据。监督学习问题包括以下几种:
-
分类:学习的属性是类别型(名义型)或离散型
-
回归:学习的属性是数值型/连续型
在无监督学习或数据挖掘中,学习程序会接受输入,但没有相应的输出。这些输入数据被称为未标记数据。在这种情况下,机器学习的目标是学习或解码隐藏的标签。这类问题包括以下几种:
-
聚类
-
降维
以文档分类为例说明
机器学习技术的常见应用之一是在文档分类领域。机器学习的两大主要类别可以应用于这个问题——监督学习和无监督学习。
监督学习
输入集合中的每个文档都被分配到一个类别,也就是一个标签。学习程序/算法使用输入集合中的文档来学习如何为另一组没有标签的文档做出预测。这种方法被称为分类。
无监督学习
输入集合中的文档没有被分配到类别中,因此它们是未标注的。学习程序将这些作为输入,尝试聚类或发现相关或相似文档的组。这种方法被称为聚类。
机器学习系统如何学习
机器学习系统利用一种被称为分类器的工具从数据中学习。分类器是一个接口,它接受一个被称为特征值的矩阵,并生成一个输出向量,也就是类别。这些特征值可以是离散的,也可以是连续的。分类器的三个核心组成部分如下:
-
表示:它是什么类型的分类器?
-
评估:分类器的表现如何?
-
优化:你如何在备选方案中进行搜索?
机器学习应用 – Kaggle 泰坦尼克号比赛
为了说明如何使用 pandas 帮助我们启动机器学习旅程,我们将应用它于一个经典问题,该问题托管在 Kaggle 网站上(www.kaggle.com)。Kaggle是一个机器学习问题的竞赛平台。Kaggle 的理念是,允许有兴趣通过数据解决预测分析问题的公司将其数据发布到 Kaggle,并邀请数据科学家提出解决方案。竞赛可以在一段时间内进行,竞争者的排名会公布在排行榜上。在竞赛结束时,排名靠前的竞争者将获得现金奖励。
我们将研究的经典问题是为了说明如何使用 pandas 进行机器学习,并结合scikit-learn,这是 Kaggle 上托管的经典入门机器学习问题——泰坦尼克号:灾难中的机器学习问题。该问题涉及的数据集是一个原始数据集。因此,pandas 在数据预处理和清洗方面非常有用,帮助在将数据输入到scikit-learn实现的机器学习算法之前对其进行整理。
泰坦尼克号:灾难中的机器学习问题
泰坦尼克号的数据集包括这次命运多舛旅行的乘客名单,以及各种特征和一个指示变量,告诉我们乘客是否在船沉没时幸存。问题的核心是,给定一位乘客及其相关特征,能否预测该乘客是否幸存于泰坦尼克号的沉船事件中。特征如下所示。
数据包括两个数据集:一个训练数据集和一个测试数据集。训练数据集包含 891 个乘客案例,测试数据集包含 491 个乘客案例。
训练数据集也包含 11 个变量,其中 10 个是特征,1 个是因变量/指示变量Survived,它指示乘客是否在灾难中幸存。
特征变量如下:
-
乘客 ID
-
舱位
-
性别
-
船舱等级(Pclass)
-
票价
-
父母和子女数量(Parch)
-
年龄
-
兄弟姐妹数量(Sibsp)
-
上船港口
我们可以利用 pandas 来帮助我们通过以下方式进行数据预处理:
-
数据清理和某些变量的分类
-
排除明显与乘客生存性无关的无关特征;例如,姓名
-
处理缺失数据
我们可以使用多种算法来解决这个问题。它们如下:
-
决策树
-
神经网络
-
随机森林
-
支持向量机
过拟合问题
过拟合是机器学习中的一个著名问题,程序会记住输入的数据,导致在训练数据上表现完美,但在测试数据上却表现糟糕。
为了防止过拟合,可以使用十折交叉验证技术,在训练阶段引入数据的变异性。
使用 pandas 进行数据分析和预处理
在本节中,我们将利用 pandas 对数据进行一些分析和预处理,然后将其作为输入提交给scikit-learn。
检查数据
为了开始我们的数据预处理,让我们读取训练数据集并检查它的样子。
在这里,我们将训练数据集读取到一个 pandas DataFrame 中并显示前几行:
In [2]: import pandas as pd
import numpy as np
# For .read_csv, always use header=0 when you know row 0 is the header row
train_df = pd.read_csv('csv/train.csv', header=0)
In [3]: train_df.head(3)
输出结果如下:

因此,我们可以看到各个特征:PassengerId、Survived、PClass、Name、Sex、Age、Sibsp、Parch、Ticket、Fare、Cabin 和 Embarked。一个立刻浮现在脑海中的问题是:哪些特征可能会影响乘客是否幸存?
看起来显而易见的是,PassengerID、票号和姓名不应当影响生存性,因为它们是标识符变量。我们将在分析中跳过这些变量。
处理缺失值
在机器学习的数据集处理过程中,我们必须解决的一个问题是如何处理训练集中的缺失值。
让我们直观地识别特征集中缺失值的位置。
为此,我们可以使用 Tom Augspurger 编写的 R 语言missmap函数的等效版本。下一个截图直观地展示了各个特征缺失数据的情况:

欲了解更多信息及生成此数据的代码,请参阅以下链接:tomaugspurger.github.io/blog/2014/02/22/Visualizing%20Missing%20Data/。
我们还可以计算每个特征缺失的数据量:
In [83]: missing_perc=train_df.apply(lambda x: 100*(1-x.count().sum()/(1.0*len(x))))
In [85]: sorted_missing_perc=missing_perc.order(ascending=False)
sorted_missing_perc
Out[85]: Cabin 77.104377
Age 19.865320
Embarked 0.224467
Fare 0.000000
Ticket 0.000000
Parch 0.000000
SibSp 0.000000
Sex 0.000000
Name 0.000000
Pclass 0.000000
Survived 0.000000
PassengerId 0.000000
dtype: float64
因此,我们可以看到大部分Cabin数据缺失(77%),而约 20%的Age数据缺失。我们决定从学习特征集中删除Cabin数据,因为该数据过于稀疏,无法提供有用的信息。
让我们进一步细分我们希望检查的各个特征。对于分类/离散特征,我们使用条形图;对于连续值特征,我们使用直方图。生成图表的代码如下所示:
In [137]: import random
bar_width=0.1
categories_map={'Pclass':{'First':1,'Second':2, 'Third':3},
'Sex':{'Female':'female','Male':'male'},
'Survived':{'Perished':0,'Survived':1},
'Embarked':{'Cherbourg':'C','Queenstown':'Q','Southampton':'S'},
'SibSp': { str(x):x for x in [0,1,2,3,4,5,8]},
'Parch': {str(x):x for x in range(7)}
}
colors=['red','green','blue','yellow','magenta','orange']
subplots=[111,211,311,411,511,611,711,811]
cIdx=0
fig,ax=plt.subplots(len(subplots),figsize=(10,12))
keyorder = ['Survived','Sex','Pclass','Embarked','SibSp','Parch']
for category_key,category_items in sorted(categories_map.iteritems(),
key=lambda i:keyorder.index(i[0])):
num_bars=len(category_items)
index=np.arange(num_bars)
idx=0
for cat_name,cat_val in sorted(category_items.iteritems()):
ax[cIdx].bar(idx,len(train_df[train_df[category_key]==cat_val]), label=cat_name,
color=np.random.rand(3,1))
idx+=1
ax[cIdx].set_title('%s Breakdown' % category_key)
xlabels=sorted(category_items.keys())
ax[cIdx].set_xticks(index+bar_width)
ax[cIdx].set_xticklabels(xlabels)
ax[cIdx].set_ylabel('Count')
cIdx +=1
fig.subplots_adjust(hspace=0.8)
for hcat in ['Age','Fare']:
ax[cIdx].hist(train_df[hcat].dropna(),color=np.random.rand(3,1))
ax[cIdx].set_title('%s Breakdown' % hcat)
#ax[cIdx].set_xlabel(hcat)
ax[cIdx].set_ylabel('Frequency')
cIdx +=1
fig.subplots_adjust(hspace=0.8)
plt.show()
请查看以下输出:

从前面的数据和截图中,我们可以观察到以下几点:
-
死亡的乘客数量大约是幸存者的两倍(62%对 38%)。
-
男性乘客大约是女性乘客的两倍(65%对 35%)。
-
第三舱的乘客比第一和第二舱的总和多约 20%(55%对 45%)。
-
大多数乘客是单独旅行的;即,船上没有孩子、父母、兄弟姐妹或配偶。
这些观察结果可能促使我们深入研究是否存在生还机会与性别和票价等级之间的某种相关性,特别是如果我们考虑到泰坦尼克号实行“先妇女儿童后”政策以及泰坦尼克号所搭载的救生艇(20 艘)比设计容量(32 艘)要少这一事实。
鉴于此,让我们进一步考察生还与这些特征之间的关系。我们从性别开始:
In [85]: from collections import OrderedDict
num_passengers=len(train_df)
num_men=len(train_df[train_df['Sex']=='male'])
men_survived=train_df[(train_df['Survived']==1 ) & (train_df['Sex']=='male')]
num_men_survived=len(men_survived)
num_men_perished=num_men-num_men_survived
num_women=num_passengers-num_men
women_survived=train_df[(train_df['Survived']==1) & (train_df['Sex']=='female')]
num_women_survived=len(women_survived)
num_women_perished=num_women-num_women_survived
gender_survival_dict=OrderedDict()
gender_survival_dict['Survived']={'Men':num_men_survived,'Women':num_women_survived}
gender_survival_dict['Perished']={'Men':num_men_perished,'Women':num_women_perished}
gender_survival_dict['Survival Rate']= {'Men' :
round(100.0*num_men_survived/num_men,2),
'Women':round(100.0*num_women_survived/num_women,2)}
pd.DataFrame(gender_survival_dict)
Out[85]:
请查看以下表格:
| 性别 | 幸存 | 遇难 | 生还率 |
|---|---|---|---|
| 男性 | 109 | 468 | 18.89 |
| 女性 | 233 | 81 | 74.2 |
现在我们将以条形图的形式展示这些数据:
In [76]: #code to display survival by gender
fig = plt.figure()
ax = fig.add_subplot(111)
perished_data=[num_men_perished, num_women_perished]
survived_data=[num_men_survived, num_women_survived]
N=2
ind = np.arange(N) # the x locations for the groups
width = 0.35
survived_rects = ax.barh(ind, survived_data, width,color='green')
perished_rects = ax.barh(ind+width, perished_data, width,color='red')
ax.set_xlabel('Count')
ax.set_title('Count of Survival by Gender')
yTickMarks = ['Men','Women']
ax.set_yticks(ind+width)
ytickNames = ax.set_yticklabels(yTickMarks)
plt.setp(ytickNames, rotation=45, fontsize=10)
## add a legend
ax.legend((survived_rects[0], perished_rects[0]), ('Survived', 'Perished') )
plt.show()
上述代码生成了以下条形图:

从前面的图表中可以看到,大多数女性幸存(74%),而大多数男性遇难(仅 19%幸存)。
这使我们得出结论:乘客的性别可能是决定乘客是否生还的一个因素。
接下来,让我们看一下乘客舱位。首先,我们生成每个舱位的幸存和遇难数据,并计算生还率:
In [86]:
from collections import OrderedDict
num_passengers=len(train_df)
num_class1=len(train_df[train_df['Pclass']==1])
class1_survived=train_df[(train_df['Survived']==1 ) & (train_df['Pclass']==1)]
num_class1_survived=len(class1_survived)
num_class1_perished=num_class1-num_class1_survived
num_class2=len(train_df[train_df['Pclass']==2])
class2_survived=train_df[(train_df['Survived']==1) & (train_df['Pclass']==2)]
num_class2_survived=len(class2_survived)
num_class2_perished=num_class2-num_class2_survived
num_class3=num_passengers-num_class1-num_class2
class3_survived=train_df[(train_df['Survived']==1 ) & (train_df['Pclass']==3)]
num_class3_survived=len(class3_survived)
num_class3_perished=num_class3-num_class3_survived
pclass_survival_dict=OrderedDict()
pclass_survival_dict['Survived']={'1st Class':num_class1_survived,
'2nd Class':num_class2_survived,
'3rd Class':num_class3_survived}
pclass_survival_dict['Perished']={'1st Class':num_class1_perished,
'2nd Class':num_class2_perished,
'3rd Class':num_class3_perished}
pclass_survival_dict['Survival Rate']= {'1st Class' : round(100.0*num_class1_survived/num_class1,2),
'2nd Class':round(100.0*num_class2_survived/num_class2,2),
'3rd Class':round(100.0*num_class3_survived/num_class3,2),}
pd.DataFrame(pclass_survival_dict)
Out[86]:
然后,我们将这些数据展示在表格中:
| 舱位 | 幸存 | 遇难 | 生还率 |
|---|---|---|---|
| 第一舱 | 136 | 80 | 62.96 |
| 第二舱 | 87 | 97 | 47.28 |
| 第三舱 | 119 | 372 | 24.24 |
然后,我们可以使用matplotlib以类似于之前描述的按性别统计幸存者数量的方式来绘制数据:
In [186]:
fig = plt.figure()
ax = fig.add_subplot(111)
perished_data=[num_class1_perished, num_class2_perished, num_class3_perished]
survived_data=[num_class1_survived, num_class2_survived, num_class3_survived]
N=3
ind = np.arange(N) # the x locations for the groups
width = 0.35
survived_rects = ax.barh(ind, survived_data, width,color='blue')
perished_rects = ax.barh(ind+width, perished_data, width,color='red')
ax.set_xlabel('Count')
ax.set_title('Survivor Count by Passenger class')
yTickMarks = ['1st Class','2nd Class', '3rd Class']
ax.set_yticks(ind+width)
ytickNames = ax.set_yticklabels(yTickMarks)
plt.setp(ytickNames, rotation=45, fontsize=10)
## add a legend
ax.legend( (survived_rects[0], perished_rects[0]), ('Survived', 'Perished'),
loc=10 )
plt.show()
这将生成以下条形图:

从前面的数据和图表中可以清晰地看出,乘客的票价等级越高,乘客的生还几率越大。
鉴于性别和票价等级似乎会影响乘客的生还几率,让我们看看当我们将这两个特征结合起来并绘制它们的组合时会发生什么。为此,我们将使用 pandas 中的crosstab函数:
In [173]: survival_counts=pd.crosstab([train_df.Pclass,train_df.Sex],train_df.Survived.astype(bool))
survival_counts
Out[173]: Survived False True
Pclass Sex
1 female 3 91
male 77 45
2 female 6 70
male 91 17
3 female 72 72
male 300 47
现在,让我们使用matplotlib来显示这些数据。首先,为了展示方便,我们做一些重新标记:
In [183]: survival_counts.index=survival_counts.index.set_levels([['1st', '2nd', '3rd'], ['Women', 'Men']])
In [184]: survival_counts.columns=['Perished','Survived']
现在,我们使用 pandas DataFrame的plot函数绘制乘客数据:
In [185]: fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_xlabel('Count')
ax.set_title('Survivor Count by Passenger class, Gender')
survival_counts.plot(kind='barh',ax=ax,width=0.75,
color=['red','black'], xlim=(0,400))
Out[185]: <matplotlib.axes._subplots.AxesSubplot at 0x7f714b187e90>

一种对泰坦尼克号问题的简单方法
我们对泰坦尼克号数据的首次分类尝试是使用一种简单且非常直观的方法。该方法包括以下步骤:
-
选择一组特征,S,来影响一个人是否生还。
-
对于每种可能的特征组合,使用训练数据来指示大多数情况下是否生还。这可以通过所谓的生还矩阵来评估。
-
对于我们希望预测生还的每个测试样本,查找与其特征值对应的特征组合,并将其预测值分配给生还表中的生还值。该方法是一种简单的 K 近邻方法。
根据我们之前在分析中看到的,三个特征似乎对生还率有最大的影响:
-
乘客舱位
-
性别
-
乘客票价(按类别划分)
我们包括了乘客票价,因为它与乘客舱位有关。
生还表大致如下所示:
NumberOfPeople Pclass PriceBucket Sex Survived
0 0 1 0 female 0
1 1 1 0 male 0
2 0 1 1 female 0
3 0 1 1 male 0
4 7 1 2 female 1
5 34 1 2 male 0
6 1 1 3 female 1
7 19 1 3 male 0
8 0 2 0 female 0
9 0 2 0 male 0
10 35 2 1 female 1
11 63 2 1 male 0
12 31 2 2 female 1
13 25 2 2 male 0
14 4 2 3 female 1
15 6 2 3 male 0
16 64 3 0 female 1
17 256 3 0 male 0
18 43 3 1 female 1
19 38 3 1 male 0
20 21 3 2 female 0
21 24 3 2 male 0
22 10 3 3 female 0
23 5 3 3 male 0
为了查看我们如何使用这个表格,让我们来看一下我们测试数据的一部分:
In [192]: test_df.head(3)[['PassengerId','Pclass','Sex','Fare']]
Out[192]: PassengerId Pclass Sex Fare
0 892 3 male 7.8292
1 893 3 female 7.0000
2 894 2 male 9.6875
对于乘客892,我们看到他是男性,票价为 7.8292,他乘坐的是第三舱。因此,这个乘客生还表查找的关键字是{Sex='male', Pclass=3, PriceBucket=0(因为 7.8292 属于 0 号桶)}。如果我们在生还表中查找这个关键字对应的生还值(第 17 行),我们看到该值为0,即未能生还;这是我们将要预测的值。
类似地,对于乘客893,我们有key={Sex='female', Pclass=3, PriceBucket=0}。这对应于第 16 行,因此我们会预测1,即生还,而她的预测生还值是1,即生还。
因此,我们的结果类似于以下命令:
> head -4 csv/surv_results.csv
PassengerId,Survived
892,0
893,1
894,0
该信息的来源在 www.markhneedham.com/blog/2013/10/30/kaggle-titanic-python-pandas-attempt/。
使用之前概述的生还表方法,我们可以在 Kaggle 上实现 0.77990 的准确度(www.kaggle.com)。尽管生还表方法直观且简单,但它只是机器学习中可能性冰山一角的非常基础的方法。
在接下来的部分中,我们将快速浏览几种机器学习算法,帮助读者了解机器学习领域中的各种可用工具。
scikit-learn 的 ML/分类器接口
我们将深入探讨机器学习的基本原理,并通过 scikit-learn 的基础 API 演示这些原理的应用。
scikit-learn 库具有估计器接口。我们通过使用线性回归模型来说明这一点。例如,考虑以下内容:
In [3]: from sklearn.linear_model import LinearRegression
估计器接口被实例化以创建一个模型,在这种情况下是线性回归模型:
In [4]: model = LinearRegression(normalize=True)
In [6]: print model
LinearRegression(copy_X=True, fit_intercept=True, normalize=True)
在这里,我们指定 normalize=True,表示在回归之前将对 x 值进行归一化处理。超参数(估计器参数)作为模型创建时的参数传入。这是创建具有可调参数模型的示例。
估计参数是通过拟合估计器时从数据中获得的。让我们首先创建一些样本训练数据,这些数据围绕 y = x/2 正态分布。我们首先生成 x 和 y 的值:
In [51]: sample_size=500
x = []
y = []
for i in range(sample_size):
newVal = random.normalvariate(100,10)
x.append(newVal)
y.append(newVal / 2.0 + random.normalvariate(50,5))
sklearn 接受一个 num_samples × num_features 的二维数组作为输入,因此我们将我们的 x 数据转换为二维数组:
In [67]: X = np.array(x)[:,np.newaxis]
X.shape
Out[67]: (500, 1)
在此示例中,我们有 500 个样本和 1 个特征,x。现在我们训练/拟合模型,并显示回归线的斜率(系数)和截距,即预测值:
In [71]: model.fit(X,y)
print "coeff=%s, intercept=%s" % (model.coef_,model.intercept_)
coeff=[ 0.47071289], intercept=52.7456611783
这可以通过以下方式进行可视化:
In [65]: plt.title("Plot of linear regression line and training data")
plt.xlabel('x')
plt.ylabel('y')
plt.scatter(X,y,marker='o', color='green', label='training data');
plt.plot(X,model.predict(X), color='red', label='regression line')
plt.legend(loc=2)
Out[65]: [<matplotlib.lines.Line2D at 0x7f11b0752350]

总结估计器接口的基本使用方法,请遵循以下步骤:
-
定义你的模型:
LinearRegression、SupportVectorMachine、DecisionTrees等。你可以在此步骤中指定所需的超参数;例如,如前所述,normalize=True。 -
一旦模型被定义,你可以通过调用在前一步定义的模型上的
fit(..)方法,在数据上训练模型。 -
一旦我们拟合了模型,就可以在测试数据上调用
predict(..)方法进行预测或估算。 -
对于监督学习问题,
predict(X)方法接受未标记的观测值X,并返回预测标签y。
欲了解更多信息,请参见以下链接:scikit-learn.org/stable/tutorial/statistical_inference/supervised_learning.html
监督学习算法
我们将简要介绍一些著名的监督学习算法,并看看如何将它们应用于前面提到的泰坦尼克号生存预测问题。
使用 Patsy 为 scikit-learn 构建模型
在开始介绍机器学习算法之前,我们需要了解一些关于Patsy库的知识。我们将利用Patsy来设计与scikit-learn配合使用的特征。Patsy是一个用于创建设计矩阵的包。设计矩阵是我们输入数据特征的变换。这些变换由公式表示,公式对应于我们希望机器学习程序在学习过程中使用的特征规范。
一个简单的例子如下:假设我们希望对一个变量 y 进行线性回归,回归变量包括其他变量x、a和b,以及a与b的交互作用;那么,我们可以按如下方式指定模型:
import patsy as pts
pts.dmatrices("y ~ x + a + b + a:b", data)
在前一行代码中,公式通过以下表达式指定:y ~ x + a + b + a:b。
有关更多信息,请查看patsy.readthedocs.org/en/latest/overview.html。
通用模板代码解释
在这一部分中,我们将介绍使用Patsy和scikit-learn实现以下算法的模板代码。之所以这样做,是因为以下算法的大部分代码是可重复的。
在接下来的章节中,将描述算法的工作原理,并提供每种算法的特定代码:
- 首先,让我们通过以下命令确保我们位于正确的文件夹中。假设工作目录位于
~/devel/Titanic,我们有以下内容:
In [17]: %cd ~/devel/Titanic
/home/youruser/devel/sandbox/Learning/Kaggle/Titanic
- 在这里,我们导入所需的包并读取训练集和测试集数据:
In [18]: import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import patsy as pt
In [19]: train_df = pd.read_csv('csv/train.csv', header=0)
test_df = pd.read_csv('csv/test.csv', header=0)
- 接下来,我们指定希望提交给
Patsy的公式:
In [21]: formula1 = 'C(Pclass) + C(Sex) + Fare'
formula2 = 'C(Pclass) + C(Sex)'
formula3 = 'C(Sex)'
formula4 = 'C(Pclass) + C(Sex) + Age + SibSp + Parch'
formula5 = 'C(Pclass) + C(Sex) + Age + SibSp + Parch +
C(Embarked)'
formula6 = 'C(Pclass) + C(Sex) + Age + SibSp + C(Embarked)'
formula7 = 'C(Pclass) + C(Sex) + SibSp + Parch + C(Embarked)'
formula8 = 'C(Pclass) + C(Sex) + SibSp + Parch + C(Embarked)'
In [23]: formula_map = {'PClass_Sex_Fare' : formula1,
'PClass_Sex' : formula2,
'Sex' : formula3,
'PClass_Sex_Age_Sibsp_Parch' : formula4,
'PClass_Sex_Age_Sibsp_Parch_Embarked' :
formula5,
'PClass_Sex_Embarked' : formula6,
'PClass_Sex_Age_Parch_Embarked' : formula7,
'PClass_Sex_SibSp_Parch_Embarked' : formula8
}
我们将定义一个函数来帮助处理缺失值。以下函数会查找数据框中包含空值的单元格,获取相似乘客的集合,并将空值设置为该集合中相似乘客特征的均值。相似乘客定义为性别和乘客舱位与缺失特征值的乘客相同的乘客:
In [24]:
def fill_null_vals(df,col_name):
null_passengers=df[df[col_name].isnull()]
passenger_id_list = null_passengers['PassengerId'].tolist()
df_filled=df.copy()
for pass_id in passenger_id_list:
idx=df[df['PassengerId']==pass_id].index[0]
similar_passengers = df[(df['Sex']==
null_passengers['Sex'][idx]) &
(df['Pclass']==null_passengers['Pclass'][idx])]
mean_val = np.mean(similar_passengers[col_name].dropna())
df_filled.loc[idx,col_name]=mean_val
return df_filled
在这里,我们创建了填充版本的训练和测试数据框。
我们的测试数据框是经过拟合的scikit-learn模型将在其上生成预测,并将输出提交给 Kaggle 进行评估:
In [28]: train_df_filled=fill_null_vals(train_df,'Fare')
train_df_filled=fill_null_vals(train_df_filled,'Age')
assert len(train_df_filled)==len(train_df)
test_df_filled=fill_null_vals(test_df,'Fare')
test_df_filled=fill_null_vals(test_df_filled,'Age')
assert len(test_df_filled)==len(test_df)
下面是实际调用scikit-learn进行训练并生成预测的实现。请注意,尽管这段代码是模板代码,但为了说明目的,实际调用了一个特定算法——在此例中为DecisionTreeClassifier。
输出数据会写入带有描述性名称的文件,例如csv/dt_PClass_Sex_Age_Sibsp_Parch_1.csv和csv/dt_PClass_Sex_Fare_1.csv:
In [29]:
from sklearn import metrics,svm, tree
for formula_name, formula in formula_map.iteritems():
print "name=%s formula=%s" % (formula_name,formula)
y_train,X_train = pt.dmatrices('Survived ~ ' + formula,
train_df_filled,return_type='dataframe')
y_train = np.ravel(y_train)
model = tree.DecisionTreeClassifier(criterion='entropy',
max_depth=3,min_samples_leaf=5)
print "About to fit..."
dt_model = model.fit(X_train, y_train)
print "Training score:%s" % dt_model.score(X_train,y_train)
X_test=pt.dmatrix(formula,test_df_filled)
predicted=dt_model.predict(X_test)
print "predicted:%s" % predicted[:5]
assert len(predicted)==len(test_df)
pred_results = pd.Series(predicted,name='Survived')
dt_results = pd.concat([test_df['PassengerId'],
pred_results],axis=1)
dt_results.Survived = dt_results.Survived.astype(int)
results_file = 'csv/dt_%s_1.csv' % (formula_name)
print "output file: %s\n" % results_file
dt_results.to_csv(results_file,index=False)
前面的代码遵循了一个标准的流程,摘要如下:
-
读取训练集和测试集数据。
-
填充我们希望在两个数据集中考虑的特征的任何缺失值。
-
在
Patsy中定义我们希望为其生成机器学习模型的各种特征组合的公式。 -
对于每个公式,执行以下步骤:
1. 调用Patsy为我们的训练特征集和训练标签集(分别由X_train和y_train指定)创建设计矩阵。
2. 实例化适当的scikit-learn分类器。在本例中,我们使用DecisionTreeClassifier。
3. 通过调用fit(..)方法来拟合模型。
4. 调用Patsy创建我们的预测输出的设计矩阵(X_test),通过调用patsy.dmatrix(..)。
5. 在X_test设计矩阵上进行预测,并将结果保存在变量 predicted 中。
6. 将我们的预测结果写入输出文件,提交给 Kaggle。
我们将考虑以下监督学习算法:
-
逻辑回归
-
支持向量机
-
随机森林
-
决策树
逻辑回归
在逻辑回归中,我们尝试根据一个或多个输入预测变量预测一个类别的结果,即离散值的因变量。
逻辑回归可以看作是将线性回归应用于离散或类别变量的等价方法。然而,在二元逻辑回归(应用于 Titanic 问题)的情况下,我们试图拟合的函数并不是线性的,因为我们要预测的结果只有两个可能的取值——0 和 1. 使用线性函数进行回归没有意义,因为输出值不能介于 0 和 1 之间。理想情况下,我们需要为二值输出的回归建模一个阶跃函数,用于 0 和 1 之间的值。然而,这样的函数没有明确的定义,且不可微分,因此定义了一个具有更好性质的近似函数:逻辑函数。逻辑函数的值介于 0 和 1 之间,但它偏向于 0 和 1 的极值,可以作为类别变量回归的良好近似。逻辑回归函数的正式定义如下:
* f(x) = 1/((1+e^(-ax))*
以下图示很好地说明了为何逻辑函数适用于二元逻辑回归:

我们可以看到,随着我们增加参数a的值,结果值逐渐接近 0 到 1 之间的值,并且逐步逼近我们希望建模的阶跃函数。前面函数的简单应用是,如果f(x) < 0.5,则将输出值设为 0,否则设为 1。
绘制该函数的代码包含在plot_logistic.py中。
对逻辑回归的更详细检查可以参考en.wikipedia.org/wiki/Logit 和 logisticregressionanalysis.com/86-what-is-logistic-regression。
在将逻辑回归应用于 Titanic 问题时,我们希望预测一个二元结果,即乘客是否幸存。
我们调整了模板代码,使用 scikit-learn 的 sklearn.linear_model.LogisticRegression 类。
提交数据到 Kaggle 后,得到以下结果:
| 公式 | Kaggle 分数 |
|---|---|
| C(Pclass) + C(Sex) + Fare | 0.76077 |
| C(Pclass) + C(Sex) | 0.76555 |
| C(Sex) | 0.76555 |
| C(Pclass) + C(Sex) + Age + SibSp + Parch | 0.74641 |
| C(Pclass) + C(Sex) + Age + Sibsp + Parch + C(Embarked) | 0.75598 |
实现逻辑回归的代码可以在 run_logistic_regression_titanic.py 文件中找到。
支持向量机
支持向量机(SVM)是一种强大的监督学习算法,用于分类和回归。它是一种判别分类器——它在数据的不同聚类或分类之间画出边界,因此新点可以根据它们所属的聚类来分类。
SVM 不仅仅是找到一条边界线;它还试图为边界的两侧确定边际。SVM 算法试图找到边界的最大可能边际。
支持向量是定义边界周围最大边际的点——如果移除这些点,可能会找到一个更大的边际。因此,称其为“支持”,因为它们支撑着边界线周围的边际。支持向量非常重要。如下图所示:

有关此更多信息,请参见 winfwiki.wi-fom.de/images/c/cf/Support_vector_2.png。
要使用 SVM 算法进行分类,我们需要指定以下三种核函数之一:线性(linear)、多项式(poly)和 rbf(也称为 径向基函数)。
然后,我们导入 支持向量分类器(SVC):
from sklearn import svm
然后,我们实例化一个 SVM 分类器,拟合模型,并预测以下内容:
model = svm.SVC(kernel=kernel)
svm_model = model.fit(X_train, y_train)
X_test = pt.dmatrix(formula, test_df_filled)
. . .
提交数据到 Kaggle 后,得到以下结果:
| 公式 | 核函数类型 | Kaggle 分数 |
|---|---|---|
| C(Pclass) + C(Sex) + Fare | poly | 0.71292 |
| C(Pclass) + C(Sex) | poly | 0.76555 |
| C(Sex) | poly | 0.76555 |
| C(Pclass) + C(Sex) + Age + SibSp + Parch | poly | 0.75598 |
| C(Pclass) + C(Sex) + Age + Parch + C(Embarked) | poly | 0.77512 |
| C(Pclass) + C(Sex) + Age + Sibsp + Parch + C(embarked) | poly | 0.79426 |
| C(Pclass) + C(Sex) + Age + Sibsp + Parch + C(Embarked) | rbf | 0.7512 |
代码的完整内容可以在以下文件中查看:run_svm_titanic.py。
在这里,我们看到使用多项式(poly)核类型的 SVM,以及Pclass、Sex、Age、Sibsp和Parch特征的组合,在提交到 Kaggle 时产生了最佳结果。令人惊讶的是,登船地点(Embarked)以及乘客是否单独旅行或与家人同行(Sibsp + Parch)似乎对乘客的生存机会有显著影响。
后者的影响可能是由于泰坦尼克号的“妇女与儿童优先”政策。
决策树
决策树的基本思想是利用训练数据集创建一个决策树来进行预测。
它基于单一特征的值递归地将训练数据集划分为多个子集。每次划分对应决策树中的一个节点。划分过程会一直持续,直到每个子集都纯净;即,所有元素都属于同一类。除非存在属于不同类的重复训练样本,否则该方法总是有效。在这种情况下,最大类将胜出。
最终结果是一个用于对测试数据集进行预测的规则集。
决策树通过模拟人类如何分类事物的过程来编码一系列二元选择,但通过使用信息准则来决定每一步中哪个问题最有用。
一个例子是,如果你想确定动物x是哺乳动物、鱼类还是爬行动物;在这种情况下,我们会提出以下问题:
- Does x have fur?
Yes: x is a mammal
No: Does x have feathers?
Yes: x is a bird
No: Does x have scales?
Yes: Does x have gills?
Yes: x is a fish
No: x is a reptile
No: x is an amphibian
这会生成一个类似于以下内容的决策树:

欲了解更多信息,请参见以下链接:labs.opendns.com/wp-content/uploads/2013/09/animals.gif。
每个节点处的二元问题划分是决策树算法的精髓。决策树的一个主要缺点是它们可能会过拟合数据。它们的灵活性非常高,当树的深度很大时,它们能够记住输入,从而导致在对未见过的数据进行分类时效果不佳。
解决这个问题的方法是使用多个决策树,这称为使用集成估计器。集成估计器的一个例子是随机森林算法,接下来我们将讨论这个算法。
要在scikit-learn中使用决策树,我们需要导入tree模块:
from sklearn import tree
然后我们实例化一个 SVM 分类器,拟合模型,并进行以下预测:
model = tree.DecisionTreeClassifier(criterion='entropy',
max_depth=3,min_samples_leaf=5)
dt_model = model.fit(X_train, y_train)
X_test = dt.dmatrix(formula, test_df_filled)
#. . .
提交我们的数据到 Kaggle 后,得到以下结果:
| 公式 | Kaggle 得分 |
|---|---|
| C(Pclass) + C(Sex) + Fare | 0.77033 |
| C(Pclass) + C(Sex) | 0.76555 |
| C(Sex) | 0.76555 |
| C(Pclass) + C(Sex) + Age + SibSp + Parch | 0.76555 |
| C(Pclass) + C(Sex) + Age + Parch + C(Embarked) | 0.78947 |
| C(Pclass) + C(Sex) + Age + Sibsp + Parch + C(Embarked) | 0.79426 |
随机森林
随机森林是一个非参数模型的例子,决策树也是如此。随机森林基于决策树,决策边界是从数据本身学习的。它不必是直线、多项式或径向基函数。随机森林模型在决策树的基础上发展,通过生成大量决策树,或一片决策树森林。它从数据中随机抽取样本,并识别一组特征来生长每棵决策树。模型的错误率在多个决策树集合中进行比较,以找到产生最强分类模型的特征集合。
要在scikit-learn中使用随机森林,我们需要导入RandomForestClassifier模块:
from sklearn import RandomForestClassifier
然后我们实例化一个随机森林分类器,拟合模型并进行预测:
model = RandomForestClassifier(n_estimators=num_estimators,
random_state=0)
rf_model = model.fit(X_train, y_train)
X_test = dt.dmatrix(formula, test_df_filled)
. . .
提交数据到 Kaggle(使用公式:C(Pclass) + C(Sex) + Age + Sibsp + Parch + C(Embarked))后,得到以下结果:
| 公式 | Kaggle 分数 |
|---|---|
| 10 | 0.74163 |
| 100 | 0.76077 |
| 1000 | 0.76077 |
| 10000 | 0.77990 |
| 100000 | 0.77990 |
无监督学习算法
在无监督学习中,我们主要关注的两个任务是:降维和聚类。
降维
降维用于帮助系统地可视化高维数据。这非常有用,因为人类大脑只能可视化三维空间(可能还包括时间维度),但大多数数据集涉及更高的维度。
降维中常用的技术是主成分分析(PCA)。PCA 通过使用线性代数技术将高维数据投影到低维空间。这不可避免地会导致信息的丧失,但通常,通过沿着正确的维度和数量投影,可以最小化信息损失。一种常见的降维技术是找到解释数据中最大方差(信息的代理)的变量组合,并沿这些维度进行投影。
在无监督学习问题中,我们没有标签集(Y),因此我们只对输入数据X本身调用fit(),对于 PCA,我们调用transform()而不是predict(),因为我们正在尝试将数据转换为新的表示形式。
我们将用于演示无监督学习的一个数据集是鸢尾花数据集,可能是所有机器学习中最著名的数据集。
scikit-learn库提供了一组预打包的数据集,可以通过sklearn.datasets模块访问。其中,鸢尾花数据集就是其中之一。
鸢尾花数据集由 150 个样本组成,来自三种不同的鸢尾花——变色鸢尾、剑藜鸢尾和维吉尼亚鸢尾——每种类型有 50 个样本。该数据集包含四个特征/维度:
-
花瓣长度
-
花瓣宽度
-
花萼长度
-
花萼宽度
长度和宽度的值以厘米为单位。它可以按如下方式加载:
from sklearn.datasets import load_iris
iris = load_iris()
在我们对无监督学习的探讨中,我们将专注于如何可视化和聚类这些数据。
在讨论无监督学习之前,让我们先来看看鸢尾花数据。load_iris()命令返回一个所谓的“bunch”对象,实际上它是一个字典,除了包含数据的键外,还包括其他键。因此,我们得到如下内容:
In [2]: iris_data.keys()
Out[2]: ['target_names', 'data', 'target', 'DESCR', 'feature_names']
此外,数据本身看起来类似于以下内容:
In [3]: iris_data.data.shape
Out[3]: (150, 4)
这对应于 150 个样本的四个特征。以下是这四个特征:
In [4]: print iris_data.feature_names
['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
我们还可以查看实际数据:
In [9]: print iris_data.data[:2]
[[ 5.1 3.5 1.4 0.2]
[ 4.9 3\. 1.4 0.2]]
我们的目标名称(我们试图预测的内容)看起来如下所示:
In [10]: print iris_data.target_names
['setosa' 'versicolor' 'virginica']
如前所述,鸢尾花特征集对应的是五维数据,我们无法在彩色图上可视化它。我们可以做的一件事是选择两个特征,并将它们相互绘制,同时使用颜色来区分不同物种的特征。接下来,我们将为所有可能的特征组合进行此操作,每次选择两个特征,共有六种不同的组合。它们如下所示:
-
萼片宽度与萼片长度
-
萼片宽度与花瓣宽度
-
萼片宽度与花瓣长度
-
萼片长度与花瓣宽度
-
萼片长度与花瓣长度
-
花瓣宽度与花瓣长度

这段代码可以在以下文件中找到:display_iris_dimensions.py。从前面的图表中,我们可以观察到 setosa 物种的点倾向于聚集在一起,而 virginica 和 versicolor 物种的点之间有一些重叠。这可能使我们得出结论,后两者的物种关系比与 setosa 物种的关系更紧密。
然而,这只是数据的二维切片。如果我们想要对数据有一个更全面的视图,展示所有四个萼片和花瓣的维度呢?如果在这四个维度之间存在某些我们二维图表未能展示的尚未发现的联系呢?有没有办法可视化这个呢?引入降维技术。我们将使用降维技术提取出萼片和花瓣维度的两个组合来帮助我们进行可视化。
我们可以通过以下方式应用降维技术来实现这一点:
In [118]: X, y = iris_data.data, iris_data.target
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
pca.fit(X)
X_red=pca.transform(X)
print "Shape of reduced dataset:%s" % str(X_red.shape)
Shape of reduced dataset:(150, 2)
因此,我们看到降维后的数据现在是二维的。我们将如下所示地在二维空间中可视化这些数据:
In [136]: figsize(8,6)
fig=plt.figure()
fig.suptitle("Dimensionality reduction on iris data")
ax=fig.add_subplot(1,1,1)
colors=['red','yellow','magenta']
cols=[colors[i] for i in iris_data.target]
ax.scatter(X_red[:,0],X[:,1],c=cols)
Out[136]:
<matplotlib.collections.PathCollection at 0x7fde7fae07d0>

我们可以通过以下方式检查这两个主成分分析(PCA)降维后的数据维度:
In [57]:
print "Dimension Composition:"
idx=1
for comp in pca.components_:
print "Dim %s" % idx
print " + ".join("%.2f x %s" % (value, name)
for value, name in zip(comp, iris_data.feature_names))
idx += 1
Dimension Composition:
Dim 1
0.36 x sepal length (cm) + -0.08 x sepal width (cm) + 0.86 x petal length (cm) + 0.36 x petal width (cm)
Dim 2
-0.66 x sepal length (cm) + -0.73 x sepal width (cm) + 0.18 x petal length (cm) + 0.07 x petal width (cm)
因此,我们可以看到这两个降维后的维度是所有四个萼片和花瓣维度的线性组合。
该信息的来源请访问github.com/jakevdp/sklearn_pycon2014。
K 均值聚类
聚类的思想是根据给定的标准将相似的点聚集在一起,从而在数据中找到聚类。
K-means 算法旨在将一组数据点划分为 K 个簇,使得每个数据点都属于与其最近的均值点或质心的簇。
为了说明 K-means 聚类,我们可以将其应用于通过 PCA 获得的降维后的鸢尾花数据集,但在这种情况下,我们不会像监督学习那样将实际标签传递给 fit(..) 方法:
In [142]: from sklearn.cluster import KMeans
k_means = KMeans(n_clusters=3, random_state=0)
k_means.fit(X_red)
y_pred = k_means.predict(X_red)
现在,我们显示聚类后的数据如下:
In [145]: figsize(8,6)
fig=plt.figure()
fig.suptitle("K-Means clustering on PCA-reduced iris data,
K=3")
ax=fig.add_subplot(1,1,1)
ax.scatter(X_red[:, 0], X_red[:, 1], c=y_pred);

请注意,我们的 K-means 算法聚类与通过 PCA 获得的维度并不完全对应。源代码可在github.com/jakevdp/sklearn_pycon2014中找到。
有关 K-means 聚类的更多信息,您可以在 scikit-learn 官方文档和以下链接中找到:scikit-learn.org/stable/auto_examples/cluster/plot_cluster_iris.html 和 en.wikipedia.org/wiki/K-means_clustering。
XGBoost 案例研究
XGBoost 是一种因其出色性能而广受欢迎的集成算法。集成算法涉及多个模型,而不仅仅是一个。集成算法分为两种类型:
-
Bagging:在这里,算法的结果是来自各个模型结果的平均值。
-
Boosting:在这里,我们从一个基础学习模型开始。每个后续模型都基于更好训练的参数创建。新参数的学习通过优化算法(如梯度下降)进行。
接下来,我们将通过一个数据集来应用 XGBoost,以预测新制造的汽车的测试时间。
这是一个逐步指南,你可以按照它进行操作:
- 导入所需的包:
# In[1]:
import os
import pandas as pd
import numpy as np
from sklearn import preprocessing
from sklearn.decomposition import PCA
from sklearn.preprocessing import scale
import matplotlib.pyplot as plt
import xgboost
- 更改工作目录:
# In[24]:
os.chdir('C:/')
- 读取训练和测试数据:
# In[19]:
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
- 准备删除具有零方差的列:
# In[31]:
#train.dtypes[train.dtypes=='float64'or train.dtypes == 'int64']
varcs = train.var(axis=0)
varcs = varcs[varcs == 0]
to_drop = list(varcs.index)
dt = train.drop(to_drop, axis=1)
print("The variables {} have been dropped as they have zero variance".format(to_drop))
- 输入删除零方差列的函数:
# In[20]:
# drops the variables with zero variance in a given dataset
def drop_zerovarcs(data):
varcs = data.var(axis=0)
varcs = varcs[varcs == 0]
to_drop = list(varcs.index)
#data_new = data.drop(to_drop, axis=1, inplace=True)
print("The variables {} have zero variance".format(to_drop))
return to_drop
- 获取训练和测试数据集中零方差列的列表:
# drops columns from test where variance is zero in test data as well as the columns for which variance is zero in train data
# In[21]:
test_drops = drop_zerovarcs(test)
train_drops = drop_zerovarcs(train)
test_train_drop = [x for x in train_drops if x not in test_drops]
# train and test have different columns which have zero variance
# Hence dropping the same columns in train and test data. Dropping the columns with zero variance in train data from test data.
test.drop(test_train_drop, axis=1,inplace=True)
- 从测试数据中移除训练数据中的零方差列:
# In[22]:
# drop the columns in test for which variance is zero in train data train.drop(train_drops, axis=1,inplace=True)
#len(list(train.drop(train_drops,axis=1).columns))
test.drop(train_drops,axis=1,inplace=True)
#len(list(test.drop(train_drops,axis=1).columns))
- 查找
Unique, Total Count 和 NAs并写入 CSV 文件:
# In[25]:
# Find Unique, Total Count and NAs
def uni_ct_na(data):
unique = data.apply(lambda x: x.nunique(), axis=0)
count = data.apply(lambda x: x.count(), axis=0)
null = data.isnull().sum()
na = data.isna().sum()
summary_df = pd.DataFrame([unique, count, null, na],index=['Unique', 'Count', 'Null', 'NA'])
summary_df.T.to_csv('summary_df.csv')
# In[26]:
uni_ct_na(train)
- 查找分类变量的列表:
# In[27]: #Finding the list of categorical variables
obj = list(train.dtypes[train.dtypes=='object'].index)
- 从分类变量中创建虚拟变量:
# In[28]: #Dummy variables using categorical variables
obj_dum_train = pd.get_dummies(train[obj])
train = pd.concat([train,obj_dum_train],axis=1).drop(obj,axis=1)
obj_dum_test = pd.get_dummies(test[obj])
test = pd.concat([test,obj_dum_test],axis=1).drop(obj,axis=1)
- 删除训练和测试数据中的分类变量:
# In[29]: # Keeping only numeric variables to apply PCA
train_cols = train.columns
train_not_obj = [x for x in train_cols if x not in obj]
train = train[train_not_obj]
test_cols = test.columns
test_not_obj = [x for x in test_cols if x not in obj]
test = test[test_not_obj]
- 绘制碎石图,以确定能够解释数据中 90% 方差的主成分数量:
# In[30]: # Plotting Scree plot to get the number of components which will
explain 90% variance in data
X=train.iloc[:,1:].values
X = scale(X)
pca = PCA()
pca.fit(X)
var= pca.explained_variance_ratio_
var1=np.cumsum(np.round(pca.explained_variance_ratio_, decimals=4)*100)
plt.plot(var1)
- 对训练和测试数据执行 PCA:
# In[31]: # Performing PCA on train and test data
X=train.iloc[:,1:].values
X = scale(X)
pca = PCA(n_components=300)
pca.fit(X)
train_pca = pca.transform(X)
train_pca.shape
X_test=train.iloc[:,1:].values
X_test = scale(X)
test_pca = pca.transform(X_test)
- 将
x和y变量从训练数据中分开,准备传递给xgboost:
# In[32]:
# Separating x and y variables to be passed to xgboost
train_y = train_pca[:,1]
train_x = train_pca[:,2:]
test_y = test_pca[:,1]
test_x = test_pca[:,2:]
- 定义
xgboost模型:
# In[33]: # Fitting a xgboost model with default options
model = xgboost.XGBRegressor()
model.fit(train_x, train_y)
- 从
xgboost模型进行预测:
# In[34]: # Predict from the model on test data
pred_y = model.predict(test_x)
# In[189]:
test_y
- 计算均方根误差:
# In[35]: # Calculating Root Mean Square Error
rmse = np.sqrt(np.sum((pred_y-test_y)**2)/len(pred_y))
rmse
熵
熵是数据同质性(或异质性)的度量。数据越同质,它的熵就越大。请记住,为了做出更好的分类决策,异质性数据更为重要。
例如,考虑一个数据集,其中对 1,000 人进行了关于是否吸烟的调查。在第一个情况下,假设 500 人回答“是”,500 人回答“否”。在第二个情况下,假设 800 人回答“是”,200 人回答“否”。在哪种情况下,熵会更大?
是的,你猜对了。是第一个情况,因为它更均匀,换句话说,决策是均匀分布的。如果一个人必须猜测调查参与者是否回答“是”或“否”,而不知道实际答案,那么在第一个情况下,他们猜对的几率更低。因此,我们说该数据在分类信息方面更混乱,因此具有更高的熵。
任何分类问题的目标,特别是决策树(因此也包括随机森林和 XGBoost),都是减少这个熵并获取信息。接下来,让我们看看如何量化这个看似定性的术语。
计算整个数据集熵的公式在数学上定义如下:

这里,p[i] 是数据集中具有i^(th)类别的比例。
例如,在我们之前提到的第一个案例中,p[yes] 将是 500/1,000,p[no] 将是 500/1,000。
以下图表显示了熵(y变量)如何随着p[i](x变量)从 0 变化到 1 而变化。请注意,p[i] 已乘以 100 以便于绘图:

熵与比例/分数(0 到 1)的关系图
观察以下图表:
-
它几乎是对称的,关于p=0.5。
-
它在p=0.5时最大,这也是合理的,因为当两个类别均匀分布时,混乱度最大。
用于生成此图的代码如下:
import matplotlib.pyplot as plt %matplotlib inline entropies = [-(p/100)*np.log2(p/100) for p in range(1,101)] plt.plot(entropies)
接下来,让我们看看如何使用 pandas 编写一个函数,来计算数据集自身的熵以及数据集中的一列的熵。为此,我们可以首先创建一个包含两列的虚拟数据:purchase(y变量)和se_status(预测变量)。
定义类别变量的唯一值:
se_status_u = ['Rich','Poor','Affluent']
purchase_u = ['yes','no','yes']
# Creating the dataframe with 10,000 rows and 2 columns viz. purchase and se_status
import random
import pandas as pd
se_status = []
purchase = []
for i in range(10000):
se_status.append(random.choice(se_status_u))
purchase.append(random.choice(purchase_u))
df = pd.DataFrame({'se_status':se_status,'purchase':purchase})
接下来,我们编写一个函数来计算给定数据集和y变量名称的数据集的初始熵:
# Function for calculating initial entropy of the dataframe
def int_entropy(df, ycol):
y_u = list(df[ycol].unique())
p = [df[df[ycol] == res].shape[0]/df.shape[0] for res in y_u]
entropy = np.sum([-(e*np.log2(e)) for e in p])
return entropy
df_int_entropy = int_entropy(df,'purchase')
df_int_entropy
一旦我们获得了初始熵,下一步的目标是找到假设使用某个预测变量进行分类的情况下的熵。计算这种情况下的熵,我们需要遵循以下步骤:
-
基于特定预测列中的类别对子集数据进行划分——每个类别对应一个数据集。
-
计算每个数据集的熵,这样你就可以为变量的每个类别得到一个熵值。
-
对这些熵值进行加权平均。权重由该类别在数据集中的比例给出。
在数学上,它可以表示为以下公式:

计算列熵的公式
在这里,f[j] 代表数据集中第 i^(th)* 类别的比例,p[ij] 代表数据集中第 i^(th)* 类别的预测变量列中 y 变量的第 j^(th)* 类别的比例。接下来,我们看看如何编写一个函数来计算给定数据集、y 变量和预测变量的熵:
**# Function for calculating entropy of a particular column of the dataframe**
def col_entropy(df,ycol,col):
y_u = df[ycol].unique()
col_u = df[col].unique()
ent_colval = []
final_ent_col = 0
for colval in col_u:
p = [(df[(df[ycol] == yval) & (df[col] == colval)]).shape[0]/(df[col] == colval).shape[0] for yval in y_u] ent_colval = np.sum([-(e*np.log2(e)) for e in p])
final_ent_col += ent_colval* ((df[df[col] == colval]).shape[0]/(df.shape[0])) return final_ent_col
信息增益被定义为当我们从仅基于 y 变量分布做分类决策到基于某一列做决策时,熵的减少。它可以按如下方式计算:
df_se_entropy = col_entropy(df,'purchase','se_status')
print(df_int_entropy)
information_gain = df_int_entropy - df_se_entropy
print(information_gain)
对于我这个例子中的数据集,我得到了大约 0.08 的信息增益。在构建决策树时,会对每一列计算信息增益,信息增益最大的列将被选作树中下一个分支节点。
总结
在本章中,我们快速浏览了机器学习的基本内容,探讨了 pandas 在特征提取、选择和工程中的作用,同时了解了机器学习中的一些关键概念,如有监督学习与无监督学习。我们还简单介绍了两种机器学习方法中的一些关键算法,并使用 scikit-learn 包来利用这些算法学习并对数据做出预测。本章并不打算全面讲解机器学习,而是旨在展示 pandas 如何在机器学习领域中为用户提供帮助。


保存并设置检查点:此选项在 Jupyter 笔记本中保存更改,并设置一个检查点,若需要,我们可以稍后恢复到此检查点。
在下方插入单元格:在当前选中的单元格下方创建一个新单元格。
剪切选中的单元格:剪切并删除选中的单元格。此操作可以通过编辑菜单撤销。
复制选中的单元格:轻松复制整个单元格的内容。
粘贴单元格到下方:粘贴之前剪切或复制的单元格内容。
将选中的单元格上下移动:将选中的单元格移到当前位置的上下。每次移动一个单元格。
运行:运行选中的单元格以执行代码。这是Ctrl+Enter的替代方式。
中断内核:取消当前正在执行的操作。
重启内核:在弹出框提示是否可以重启内核后,内核会重新启动。
重启内核并重新运行笔记本:在重启内核后重新运行整个笔记本。
代码,Markdown,原始 NBConvert,标题:更改单元格内容的文本格式。
打开命令面板:显示可用的快捷键选项。
*已知
浙公网安备 33010602011771号