Pandas-实战-全-

Pandas 实战(全)

原文:Pandas in Action

译者:飞龙

协议:CC BY-NC-SA 4.0

前置材料

前言

说实话,我是完全偶然发现 pandas 的。

2015 年,我在世界上最大的招聘网站 Indeed.com 应聘数据运营分析师职位。在最后的技能挑战中,我被要求使用微软 Excel 电子表格软件从内部数据集中提取洞察。为了给对方留下深刻印象,我从我的数据分析工具箱中拉出了我能想到的所有技巧:列排序、文本操作、交叉表,当然还有标志性的 VLOOKUP 函数。(好吧,也许“标志性的”有点夸张。)

虽然听起来可能有些奇怪,但当时我并没有意识到除了 Excel 之外还有其他数据分析工具。Excel 几乎无处不在:我的父母用它,我的老师用它,我的同事也用它。它感觉像是一个既定的标准。所以当我收到一份工作邀请时,我立刻购买了价值约 100 美元的 Excel 书籍并开始学习。是时候成为一名电子表格专家了!

我带着打印出来的 50 个最常用 Excel 函数的清单开始了我的第一天工作。在我几乎完成登录工作电脑后不久,我的经理把我拉进会议室,告诉我优先级已经改变。团队的数据集已经膨胀到 Excel 无法支持的大小。我的队友们也在寻找自动化他们日常和每周报告中重复步骤的方法。幸运的是,我的经理已经找到了解决这两个问题的方案。他问我是否听说过 pandas。

“毛茸茸的动物?”我困惑地问道。

“不,”他说。“Python 数据分析库。”

在做好所有准备之后,是时候从头开始学习一项新技术了。我有点紧张;我以前从未写过一行代码。我不是一个 Excel 的人吗?我能够做到这一点吗?只有一个方法可以找到答案。我开始深入研究 pandas 的官方文档,YouTube 视频教程,书籍,研讨会,Stack Overflow 问题和我能接触到的任何数据集。我很高兴地发现,开始使用 pandas 是如此简单和愉快。代码感觉直观且直接。这个库运行速度快。功能开发得很好,而且非常全面。有了 pandas,我可以用很少的代码完成大量的数据处理。

我的故事在 Python 社区中很常见。过去十年中,这种语言天文数字的增长通常归因于新开发者能够轻松上手。我坚信,如果你处于与我类似的位置,你也能同样学好 pandas。如果你想要将数据分析技能扩展到 Excel 表格之外,这本书就是你的邀请函。

当我对 pandas 感到舒适时,我继续探索 Python,然后是其他编程语言。在许多方面,pandas 引领了我向全职软件工程师的转变。我非常感激这个强大的库,我很高兴将知识的火炬传递给你。我希望你能发现代码为你带来的魔法。

致谢

要让《Pandas in Action》完成需要付出很多努力,我想对在它的两年写作过程中支持我的人表示最深切的感激。

首先,我要向我的美好女友 Meredith 表示衷心的感谢。从第一句话开始,她就坚定不移地支持我。她是一个充满活力、幽默和善良的灵魂,总是在我遇到困难时支持我。这本书因为有她而更加出色。谢谢你,Merbear。

感谢我的父母 Irina 和 Dmitriy,他们为我提供了一个温馨的家,我总能在这里找到慰藉。

感谢我的双胞胎姐妹 Mary 和 Alexandra。她们在她们这个年龄非常聪明、好奇和勤奋,我为她们感到无比自豪。祝你们在大学好运!

感谢我们的金毛犬沃森。他并不是一个真正的 Python 专家,但他的风趣和友好弥补了这一点。

非常感谢我的编辑 Sarah Miller,和她一起工作是一种绝对的享受。我非常感激她在整个过程中的耐心和洞察力。她是真正的船长,她让一切顺利航行。

如果没有 Indeed 提供的机会,我就不会成为一名软件工程师。我想向我的前经理 Srdjan Bodruzic 表达衷心的感谢,感谢他的慷慨和指导(以及雇佣我!)。感谢我的 CX 团队成员——Tommy Winschel、Danny Moncada、JP Schultz 和 Travis Wright——他们的智慧和幽默。感谢在我任职期间伸出援手的其他 Indeed 员工:Matthew Morin、Chris Hatton、Chip Borsi、Nicole Saglimbene、Danielle Scoli、Blairr Swayne 和 George Improglou。感谢我在 Sophie 的古巴菜餐厅共进晚餐的每一个人!

我是以软件工程师的身份在 Stride Consulting 开始写这本书的。我想感谢许多 Stride 员工在整个过程中的支持:David “The Dominator” DiPanfilo、Min Kwak、Ben Blair、Kirsten Nordine、Michael “Bobby” Nunez、Jay Lee、James Yoo、Ray Veliz、Nathan Riemer、Julia Berchem、Dan Plain、Nick Char、Grant Ziolkowski、Melissa Wahnish、Dave Anderson、Chris Aporta、Michael Carlson、John Galioto、Sean Marzug-McCarthy、Travis Vander Hoop、Steve Solomon 和 Jan Mlčoch。

感谢我作为软件工程师和顾问有机会与之共事的友好面孔:Francis Hwang、Inhak Kim、Liana Lim、Matt Bambach、Brenton Morris、Ian McNally、Josh Philips、Artem Kochnev、Andrew Kang、Andrew Fader、Karl Smith、Bradley Whitwell、Brad Popiolek、Eddie Wharton、Jen Kwok 以及我最喜欢的咖啡团队:Adam McAmis 和 Andy Fritz。

感谢以下这些人为我的生活增添了价值:Nick Bianco、Cam Stier、Keith David、Michael Cheung、Thomas Philippeau、Nicole DiAndrea 和 James Rokeach。

感谢我最喜欢的乐队 New Found Glory,他们为许多写作会议提供了背景音乐。流行朋克并未死去!

感谢帮助该项目顺利完成并协助营销工作的 Manning 团队:Jennifer Houle、Aleksandar Dragosavljević、Radmila Ercegovac、Candace Gillhoolley、Stjepan Jureković和 Lucas Weber。还要感谢负责内容的 Manning 团队:Sarah Miller,我的发展编辑;Deirdre Hiam,我的生产编辑;Keir Simpson,我的校对编辑;以及 Jason Everett,我的校对员。

感谢帮助我消除技术问题的技术审稿人:Al Pezewski、Alberto Ciarlanti、Ben McNamara、Björn Neuhaus、Christopher Kottmyer、Dan Sheikh、Dragos Manailoiu、Erico Lendzian、Jeff Smith、Jérôme Bâton、Joaquin Beltran、Jonathan Sharley、Jose Apablaza、Ken W. Alger、Martin Czygan、Mathijs Affourtit、Matthias Busch、Mike Cuddy、Monica E. Guimaraes、Ninoslav Cerkez、Rick Prins、Syed Hasany、Viton Vitanis 和 Vybhavreddy Kammireddy Changalreddy。感谢你们的努力,使我成为了一名更好的作家和教育者。

最后,感谢过去六年我居住的城市霍布肯。我在其公共图书馆、当地咖啡馆和珍珠奶茶店写下了这本书的许多部分。我在这个城镇取得了许多人生上的进步,它永远刻在了我的历史中。谢谢,霍布肯!

关于本书

适合阅读本书的人群

《Pandas 实战》是 pandas 库数据分析的全面介绍。Pandas 使您能够轻松执行多种数据操作:排序、连接、转换、清理、去重、聚合等。本书逐步介绍主题,从其较小的构建块开始,逐步过渡到较大的数据结构。

《Pandas 实战》是为那些对电子表格软件(如 Microsoft Excel、Google Sheets 和 Apple Numbers)有中级经验的数据分析师以及/或对替代数据分析工具(如 R 和 SAS)有经验的人编写的。它也适合那些对数据分析感兴趣的 Python 开发者。

本书组织结构:路线图

《Pandas 实战》由两大部分组成,共 14 章。

第一部分“核心 pandas”以递增的方式介绍了 pandas 库的基本机制:

  • 第一章使用 pandas 分析样本数据集,以展示库的功能概述。

  • 第二章介绍了Series对象,这是 pandas 的核心数据结构,用于存储有序数据集合。

  • 第三章深入探讨了Series对象。我们探讨了各种Series操作,包括排序值、删除重复项、提取最小值和最大值等。

  • 第四章介绍了DataFrame,这是一个二维数据表。我们将前几章的概念应用于新的数据结构,并介绍了额外的操作。

  • 第五章展示了如何使用各种逻辑条件(如相等、不等、比较、包含、排除等)从DataFrame中筛选行子集。

第二部分,“应用 pandas”,专注于更高级的 pandas 功能和它们在现实世界数据集中解决的问题:

  • 第六章教你如何在 pandas 中处理不完美的文本数据。我们讨论了如何解决诸如删除空白字符、修复字符大小写以及从一个单独的列中提取多个值等问题。

  • 第七章讨论了MultiIndex,它允许我们将多个列值组合成一个数据行的唯一标识符。

  • 第八章描述了如何在交叉表中聚合我们的数据,将标题从行轴移到列轴,并将我们的数据从宽格式转换为窄格式。

  • 第九章探讨了如何将行分组到桶中,并通过GroupBy对象聚合结果集合。

  • 第十章通过使用各种连接将多个数据集合并成一个。

  • 第十一章演示了如何在 pandas 中处理日期和时间。它涵盖了诸如排序日期、计算持续时间以及确定日期是否位于月份或季度的开始等主题。

  • 第十二章展示了如何将额外的文件类型导入到 pandas 中,包括 Excel 和 JSON。我们还学习了如何从 pandas 导出数据。

  • 第十三章专注于配置库的设置。我们深入探讨了如何修改显示的行数、改变浮点数的精度、四舍五入低于阈值的值等等。

  • 第十四章探讨了使用 matplotlib 库进行数据可视化。我们看到了如何使用 pandas 数据创建折线图、条形图、饼图等等。

每一章都是基于前一章构建的。对于那些从头开始学习 pandas 的人来说,我建议按线性顺序阅读章节。同时,为了确保本书作为参考指南的有用性,我已将每一章编写为具有自己数据集的独立教程。我们在每一章的开始从零开始编写代码,这样你可以从任何你喜欢的章节开始。

大多数章节都以一个编码挑战结束,这个挑战允许你练习其概念。我强烈建议尝试这些练习。

Pandas 建立在 Python 编程语言之上,建议在开始之前对语言机制有基本了解。对于那些 Python 经验有限的人来说,附录 B 提供了对该语言的详细介绍。

关于代码

本书包含许多源代码示例,这些示例以固定宽度字体like this格式化,以将其与普通文本区分开来。

本书示例的源代码可在以下 GitHub 仓库中找到:github.com/paskhaver/pandas-in-action。对于 Git 和 GitHub 新手,请在仓库页面上寻找下载 ZIP 按钮。对于熟悉 Git 和 GitHub 的用户,欢迎从命令行克隆仓库。

仓库还包括文本的完整数据集。当我学习 pandas 时,我最大的挫折之一是教程总是喜欢依赖于随机生成数据。没有一致性,没有上下文,没有故事,没有乐趣。在这本书中,我们将使用许多真实世界的数据集,涵盖从篮球运动员的薪水到宝可梦类型再到餐馆卫生检查的各个方面。数据无处不在,pandas 是今天可用的最佳工具之一,可以帮助我们理解这些数据。我希望您喜欢数据集的轻松焦点。

liveBook 讨论论坛

购买《Pandas in Action》包括免费访问由曼宁出版社运行的私人网络论坛,您可以在论坛中就本书发表评论,提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请访问livebook.manning.com/#!/book/pandas-in-action/discussion。您还可以在live book.manning.com/#!/discussion了解更多关于曼宁论坛和行为的规则。

曼宁出版社对读者的承诺是提供一个场所,让读者之间以及读者与作者之间可以进行有意义的对话。这并不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向作者提出一些挑战性的问题,以免他们的兴趣转移!只要这本书有售,论坛和以前讨论的存档将可通过出版社的网站访问。

其他在线资源

关于作者

博里斯·帕斯哈弗(Boris Paskhaver)是一位位于纽约市的全栈软件工程师、顾问和在线教育者。他在 e-learning 平台 Udemy 上有六门课程,超过 140 小时的视频,300,000 名学生,20,000 条评论,以及每月消耗 1,000 万分钟的课程内容。在成为软件工程师之前,博里斯曾担任数据分析师和系统管理员。他于 2013 年毕业于纽约大学,主修商业经济学和市场营销。

关于封面插图

《熊猫行动》封面上的插图被标注为“加莱夫人”,或称“加莱女士”。这幅插图取自雅克·格拉塞·德·圣索沃尔(1757–1810)的作品集,名为《不同国家的服饰》,于 1797 年在法国出版。每一幅插图都是手工精心绘制和着色的。格拉塞·德·圣索沃尔收藏中的丰富多样性生动地提醒我们,200 年前世界的城镇和地区在文化上是如何截然不同的。他们彼此孤立,说着不同的方言和语言。在街道或乡村,仅凭他们的服饰就能轻易识别他们居住的地方以及他们的职业或社会地位。

自那以后,我们的着装方式已经改变,而当时区域间的多样性已经消失。现在很难区分不同大陆的居民,更不用说不同的城镇、地区或国家了。也许我们用文化多样性换取了更加丰富多彩的个人生活——当然,是更加丰富多彩和快节奏的技术生活。

在难以区分一本计算机书和另一本计算机书的今天,曼宁通过基于两百年前区域生活的深刻多样性设计的书封面,庆祝了计算机行业的创新精神和主动性,这些多样性被格拉塞·德·圣索沃尔的图画重新赋予了生命。

第一部分:核心 pandas

欢迎来到本节!在这里,我们将熟悉 pandas 的核心机制及其两种主要数据结构:一维的 Series 和二维的 DataFrame。第一章从使用 pandas 分析一个数据集开始,这样你可以立即感受到这个库所能实现的功能。从那里,我们继续深入探讨第二章和第三章中的 Series。我们学习如何从头创建一个 Series;从外部数据集中导入它;并对它应用一系列数学、统计和逻辑操作。在第四章中,我们介绍了表格形式的 DataFrame 以及从其数据中提取行、列和值的各种方法。最后,第五章专注于通过应用逻辑标准来提取 DataFrame 行的子集。在这个过程中,我们将处理八个数据集,涵盖从票房总收入到 NBA 球员再到宝可梦的各个方面。

本部分涵盖了 pandas 的基础知识,这是你需要了解以有效使用库的基本知识。我已经尽最大努力从可能的最小构建块开始,逐步过渡到更大、更复杂的部分。以下五个章节为你掌握 pandas 打下了基础。祝你好运!

1. 介绍 pandas

本章涵盖

  • 21 世纪数据科学的发展

  • 数据分析库 pandas 的历史

  • pandas 及其竞争对手的优缺点

  • 使用 Excel 进行数据分析与使用编程语言进行数据分析的比较

  • 通过一个工作示例浏览库的功能

欢迎来到《Pandas 实战》!Pandas 是一个基于 Python 编程语言构建的数据分析库。一个 (也称为 )是用于解决特定领域问题的代码集合。Pandas 是数据操作工具箱:排序、过滤、清理、去重、聚合、转置等等。Python 广大数据科学生态系统的中心,pandas 与其他用于统计、自然语言处理、机器学习、数据可视化等方面的库配合良好。

在本章的引言部分,我们将探讨现代数据分析工具的历史和演变。我们将看到 pandas 如何从一个金融分析师的宠物项目成长为被 Stripe、Google 和 J.P. Morgan 等公司采用的行业标准。我们将将该库与其竞争对手进行比较,包括 Excel 和 R。我们将讨论使用编程语言与使用图形电子表格应用程序之间的区别。最后,我们将使用 pandas 分析一个真实世界的数据集。请将本章视为对你在整本书中将要掌握的概念的预览。让我们开始吧!

1.1 21 世纪的数据

“在没有数据之前就进行理论化是一个严重的错误,”夏洛克·福尔摩斯在阿瑟·柯南·道尔的经典短篇小说《波希米亚丑闻》中向他的助手约翰·华生建议。“不知不觉中,人们开始扭曲事实以适应理论,而不是让理论适应事实。”

在道尔作品发表一个多世纪之后,这位明智的侦探的话语依然真实,在一个数据在我们生活的各个方面变得越来越普遍的世界里。“世界上最宝贵的资源不再是石油,而是数据,”2017 年,《经济学人》在一篇评论文章中宣称。数据是 证据,证据对于企业、政府、机构和个人解决我们互联世界中日益复杂的问题至关重要。在众多行业中,从 Facebook 到 Amazon 到 Netflix,世界上最成功的企业都将数据视为其组合中最宝贵的资产。联合国秘书长安东尼奥·古特雷斯将准确的数据称为“良好政策和决策的生命线。”数据推动着从电影推荐到医疗治疗,从供应链物流到减贫倡议的一切。21 世纪社区、公司和甚至国家成功将取决于他们获取、汇总和分析数据的能力。

1.2 介绍 pandas

过去十年中,用于处理数据的技术生态系统已经大幅增长。如今,开源的 pandas 库是数据分析和操作中最受欢迎的解决方案之一。“开源”意味着库的源代码是公开可下载、使用、修改和分发的。其许可证赋予用户比 Excel 等专有软件更多的权限。Pandas 是免费使用的。一个全球志愿者软件开发团队维护这个库,你可以在 GitHub 上找到其完整的源代码(github.com/pandas-dev/pandas)。

Pandas 可以与微软的 Excel 电子表格软件和谷歌的浏览器内 Sheets 应用程序相媲美。在这三种技术中,用户与由行和列组成的数据表进行交互。一行代表一条记录,或者等价地,一列值的集合。通过应用转换,将数据转换为所需的状态。

图 1.1 展示了数据集的一个示例转换。分析师对左侧的四行数据集应用操作,得到右侧的两行数据集。他们可以选择符合标准的行,例如,或者从原始数据集中删除重复的行。

图片

图 1.1 表格数据集的示例转换

使 pandas 独特的是它在处理能力和用户生产力之间取得的平衡。通过依赖 C 等底层语言进行许多计算,该库可以高效地将百万行数据集在毫秒内转换。同时,它保持了一套简单直观的命令。在 pandas 中,用少量代码就能完成很多事情。

图 1.2 展示了导入和排序 CSV 数据集的 pandas 代码示例。现在不用担心代码,但请花点时间注意,整个操作只需要两行代码。

图片

图 1.2 pandas 导入和排序数据集的代码示例

Pandas 可以无缝地与数字、文本、日期、时间、缺失数据等协同工作。随着我们继续研究本书附带超过 30 个数据集,我们将探索其令人难以置信的通用性。

Pandas 的第一个版本是由软件工程师 Wes McKinney 在 2008 年开发的,当时他在纽约的 AQR Capital Management 投资公司工作。对 Excel 和统计编程语言 R 都不满意,McKinney 寻找一种工具,可以轻松解决金融行业中的常见数据问题,特别是清理和聚合。由于找不到理想的产品,他决定自己开发一个。当时,Python 还远未成为今天的强大语言,但语言的美丽启发了 McKinney 在其基础上构建他的库。“我喜欢 [Python] 因为它的表达式经济,”他在石英杂志(mng.bz/w0Na)中说道。“你可以在 Python 中用很少的代码表达复杂的思想,而且它非常容易阅读。”

Pandas 自 2009 年 12 月向公众发布以来,持续且广泛地增长。用户数量估计在五百万到一千万之间¹。截至 2021 年 6 月,pandas 已从 PyPi(Python 包的集中在线仓库)下载超过七亿五千万次(pepy.tech/project/pandas)。其 GitHub 代码仓库拥有超过 30,000 个星标(星标相当于平台上的“点赞”)。Pandas 的问题在问答聚合器 Stack Overflow 上的比例不断增长,这表明用户兴趣的增加。

我认为我们甚至可以将 Python 本身的快速增长归功于 Pandas。由于其在数据科学领域的广泛应用,Python 的流行度急剧上升,而 Pandas 对此做出了巨大贡献。Python 现在是大学和学院中最常见的第一编程语言。TIOBE 指数,根据搜索引擎流量对编程语言流行度进行排名,宣布 Python 是 2018 年增长最快的语言²。“如果 Python 能保持这种速度,它可能在 3 到 4 年内取代 C 和 Java,从而成为世界上最流行的编程语言,”TIOBE 在一份新闻稿中写道。随着你学习 Pandas,你也会学习 Python,这是库的另一个优点。

1.2.1 Pandas 与图形电子表格应用比较

Pandas 需要不同于 Excel 这样的图形电子表格应用的不同思维方式。编程本质上是比视觉更口语化的。我们通过命令与计算机交流,而不是点击。由于它对你的目标假设较少,编程语言往往更不宽容。它需要被明确告知要做什么,不能有不确定性。我们需要以正确的顺序提供正确的输入和正确的指令;否则,程序将无法运行。

由于这些更严格的要求,pandas 的学习曲线比 Excel 或 Sheets 更陡峭。但如果你在 Python 或编程方面经验有限,无需担心!当你忙于 Excel 中的 SUMIFVLOOKUP 函数时,你已经在像程序员一样思考了。过程是一样的:确定正确的函数并按正确的顺序提供正确的输入。Pandas 需要相同的一套技能;区别在于我们正在用更冗长的语言与计算机进行交流。

当你熟悉其复杂性时,Pandas 会赋予你在数据处理方面更大的权力和灵活性。除了扩展你可用程序的范围外,编程还允许你自动化它们。你可以编写一段代码,然后在整个多个文件中重复使用——这对于那些讨厌的日常和周报来说非常完美。需要注意的是,Excel 随附 Visual Basic for Applications (VBA) 编程语言,它也能让你自动化电子表格程序。然而,我认为 Python 比 VBA 更容易上手,并且用途不仅限于数据分析,这使得它是你时间的更好投资。

从 Excel 跳转到 Python 还有其他好处。与 pandas 经常搭配使用的 Jupyter Notebook 编码环境允许创建更动态、交互性和全面的报告。Jupyter Notebook 由单元格组成,每个单元格都包含一段可执行的代码。分析师可以将这些单元格与标题、图表、描述、注释、图片、视频、图表等集成。读者可以跟随分析师的逐步逻辑,了解他们是如何得出结论的,而不仅仅是他们的最终结果。

Pandas 的另一个优点是 Python 的庞大数据科学生态系统。Pandas 可以轻松与用于统计、自然语言处理、机器学习、网络爬取、数据可视化等方面的库集成。每年都会出现新的库。实验受到欢迎。创新是持续的。这些强大的工具在缺乏大型、全球贡献者社区支持的竞争对手中有时没有得到充分开发。

当数据集增长时,图形电子表格应用也开始遇到困难;在这一点上,Pandas 比 Excel 强大得多。库的容量仅受计算机内存和处理能力的限制。在大多数现代机器上,当开发者知道如何利用所有性能优化时,pandas 可以很好地处理具有数百万行和数吉字节数据集。在描述库限制的一篇博客文章中,创建者 Wes McKinney 写道:“如今,我对于 pandas 的经验法则是,你应该有比你的数据集大 5 到 10 倍的 RAM” (mng.bz/qeK6)。

选择最适合工作的最佳工具的一部分挑战在于定义数据分析和大数据等术语对你所在的组织和项目意味着什么。全球大约有 7.5 亿个工作专业人士使用 Excel,其电子表格限制在 1,048,576 行数据³。对于一些分析师来说,100 万行数据已经超过了任何报告的需求;而对于其他人来说,100 万行数据只是触及了表面。

我建议您将 pandas 视为不是最佳的数据分析解决方案,而是一个与其他现代技术一起使用的强大选项。Excel 仍然是快速、轻松进行数据操作的一个优秀选择。电子表格应用程序通常会假设你的意图,这就是为什么只需点击几下就可以导入 CSV 文件或排序 100 个值的列。对于这些简单任务,使用 pandas 并没有真正的优势(尽管它完全能够完成这些任务)。但是,当你需要清理两个各包含一千万行数据的文本值,删除它们的重复记录,将它们合并,并为 100 批次的文件复制这种逻辑时,你会使用什么?在这些情况下,使用 Python 和 pandas 来完成工作既容易又节省时间。

1.2.2 Pandas 与其竞争对手

数据科学爱好者经常将 pandas 与开源编程语言 R 和专有软件套件 SAS 进行比较。每种解决方案都有自己的支持者社区。

R 是一种以统计学为基础的专用语言,而 Python 则是一种在多个技术领域被广泛使用的通用语言。不出所料,这两种语言往往吸引着特定领域内的专家用户。Hadley Wickham,R 社区的一位杰出开发者,他构建了一个名为 tidyverse 的数据科学包集合,建议用户将这两种语言视为合作伙伴而非竞争对手。“这些事物独立存在,并以不同的方式都很出色,”他在《石英》杂志(mng.bz/Jv9V)中说。“我观察到的一个模式是,公司的数据科学团队使用 R,而数据工程团队使用 Python。Python 人员通常拥有软件工程背景,并且对自己的编程技能非常自信……[R 用户] 真的喜欢 R,但无法与工程团队争论,因为他们没有语言来支持这种争论。”一种语言可能具有另一种语言所不具备的高级功能,但在数据分析的常见任务方面,两者已经达到了近乎对等的状态。开发者和数据科学家只是简单地倾向于使用他们最擅长的工具。

一套支持统计学、数据挖掘、计量经济学等互补的软件工具,SAS 是由北卡罗来纳州立大学的 SAS Institute 开发的商业产品。它根据所选软件包的不同收取年度用户订阅费。由企业支持的产品带来的优势包括工具之间的技术和视觉一致性、强大的文档,以及面向企业客户需求的产品路线图。像 pandas 这样的开源技术则采用更自由的方法;开发者根据自身和其他开发者的需求进行工作,有时会错过市场趋势。

某些技术与 pandas 具有相似的功能,但本质上服务于不同的目的。SQL 是一个例子。SQL(结构化查询语言)是一种与关系数据库通信的语言。关系数据库由通过公共键链接的数据表组成。我们可以使用 SQL 进行基本的数据操作,例如从表中提取列和根据标准过滤行,但其功能范围更广,并且本质上围绕数据管理展开。数据库是为了 存储 数据而构建的;数据分析是次要的使用案例。SQL 可以创建新表,用新值更新现有记录,删除现有记录等。相比之下,pandas 完全是为了数据分析而构建的:统计计算、数据处理、数据合并等。在典型的工作环境中,这两个工具通常作为互补使用。分析师可能会使用 SQL 提取初始数据集,然后使用 pandas 对其进行操作。

总结来说,pandas 不是市场上唯一的工具,但它是一个强大、流行且宝贵的解决方案,用于解决大多数数据分析问题。再次强调,Python 在其简洁和高效方面真正闪耀。正如其创造者 Guido van Rossum 所言,“编写 Python 的乐趣在于看到简洁、紧凑、易读的 [数据结构],这些结构在少量清晰的代码中表达了大量的操作” (mng.bz/7jo7)。Pandas 符合这一标准,并且是那些渴望通过强大的现代数据分析工具包提升编程技能的电子表格分析师的绝佳下一步。

1.3 pandas 概览

要真正掌握 pandas 的强大功能,最好的方式是看到它在实际中的应用。让我们快速浏览这个库,通过分析史上票房最高的 700 部电影的数据集来进行。我希望你会对 pandas 的语法如何直观感到惊喜,即使你是编程新手。

在阅读本章的其余部分时,请尽量不要过度分析代码示例;你甚至不需要复制它们。我们现在的目标是获得 pandas 特性和功能的鸟瞰图。想想这个库能做什么;我们将在稍后更详细地关注如何实现。

我们将在本书中使用的 Jupyter Notebook 开发环境中编写代码。如果您需要在您的计算机上设置 pandas 和 Jupyter Notebook,请参阅附录 A。您可以在 www.github.com/paskhaver/pandas-in-action 下载所有数据集和完成后的 Jupyter Notebook。

1.3.1 导入数据集

让我们开始吧!首先,我们将在与 movies.csv 文件相同的目录内创建一个新的 Jupyter Notebook;然后我们将导入 pandas 库以访问其功能:

In  [1] import pandas as pd

代码左侧的框(在先前的示例中显示数字 1)标记了单元格相对于 Jupyter Notebook 的启动或重启的执行顺序。您可以按任何顺序执行单元格,并且可以多次执行相同的单元格。

阅读本书时,鼓励你在 Jupyter 单元中执行不同的代码片段进行实验。因此,如果你的执行次数与文本中的不一致是完全可以接受的。

我们的数据存储在一个单独的 movies.csv 文件中。CSV(逗号分隔值)文件是一种纯文本文件,它使用换行符分隔每一行数据,使用逗号分隔每一行值。文件的第一行包含数据的列标题。以下是 movies.csv 文件前三行的预览:

Rank,Title,Studio,Gross,Year
1,Avengers: Endgame,Buena Vista,"$2,796.30",2019
2,Avatar,Fox,"$2,789.70",2009

第一行列出了数据集中的五个列:排名、标题、工作室、总收入和年份。第二行包含第一条记录,或者说第一条电影的记录。这部电影排名为 1,标题为 "复仇者联盟:终局之战",工作室为 "Buena Vista",总收入为 "$2,796.30",年份为 2019。下一行包含下一部电影的值,数据集中的剩余 750 多行按此模式重复。

Pandas 可以导入各种文件类型,每种类型在库的顶层都有一个相关的导入函数。在 Pandas 中,函数相当于 Excel 中的函数。它是一个我们发出的命令,无论是针对库还是库中的实体。在这种情况下,我们将使用 read_csv 函数来导入 movies.csv 文件:

In  [2] pd.read_csv("movies.csv")

Out [2]

 **Rank                         Title            Studio      Gross   Year**
  0     1             Avengers: Endgame       Buena Vista  $2,796.30   2019
  1     2                        Avatar               Fox  $2,789.70   2009
  2     3                       Titanic         Paramount  $2,187.50   1997
  3     4  Star Wars: The Force Awakens       Buena Vista  $2,068.20   2015
  4     5        Avengers: Infinity War       Buena Vista  $2,048.40   2018
 ...   ...                      ...                 ...        ...      ...
777   778                     Yogi Bear   Warner Brothers    $201.60   2010
778   779           Garfield: The Movie               Fox    $200.80   2004
779   780                   Cats & Dogs   Warner Brothers    $200.70   2001
780   781      The Hunt for Red October         Paramount    $200.50   1990
781   782                      Valkyrie               MGM    $200.30   2008

782 rows × 5 columns

Pandas 将 CSV 文件的全部内容导入一个名为 DataFrame 的对象中。将对象视为存储数据的容器。不同的对象针对不同类型的数据进行了优化,并且我们以不同的方式与它们交互。Pandas 使用一种类型的对象(DataFrame)来存储多列数据集,另一种类型的对象(Series)来存储单列数据集。DataFrame 可以与 Excel 中的多列表格相媲美。

为了避免屏幕混乱,pandas 只显示 DataFrame 的前五行和后五行。一行省略号(...)标记了数据缺失的位置。

这个DataFrame由五个列(排名、标题、工作室、总收入、年份)和一个索引组成。索引是DataFrame左侧上升数字的范围。索引标签作为数据行标识符。我们可以将任何列设置为DataFrame的索引。当我们没有明确告诉 pandas 使用哪一列时,库会生成从 0 开始的数字索引。

哪一列是作为索引的好候选?它是指可以充当每行主标识符或参考点的值。在我们的五列中,排名和标题是两个最佳选项。让我们将自动生成的数字索引与标题列的值交换。我们可以在 CSV 导入期间直接这样做:

In  [3] pd.read_csv("movies.csv", index_col = "Title")

Out [3]

                              Rank            Studio       Gross   Year
**Title** 
           Avengers: Endgame     1       Buena Vista   $2,796.30   2019
                      Avatar     2               Fox   $2,789.70   2009
                     Titanic     3         Paramount   $2,187.50   1997
Star Wars: The Force Awakens     4       Buena Vista   $2,068.20   2015
      Avengers: Infinity War     5       Buena Vista   $2,048.40   2018
                         ...   ...               ...        ...     ...
                   Yogi Bear   778   Warner Brothers     $201.60   2010
         Garfield: The Movie   779               Fox     $200.80   2004
                 Cats & Dogs   780   Warner Brothers     $200.70   2001
    The Hunt for Red October   781         Paramount     $200.50   1990
                    Valkyrie   782               MGM     $200.30   2008

782 rows × 4 columns

接下来,我们将DataFrame分配给一个名为movies的变量,这样我们就可以在程序的其它地方引用它。变量是程序中对象的用户分配的名称:

In  [4] movies = pd.read_csv("movies.csv", index_col = "Title")

更多关于变量的信息,请参阅附录 B。

1.3.2 操作 DataFrame

我们可以从DataFrame的多个角度来观察。我们可以从开头提取几行:

In  [5] movies.head(4)

Out [5]

                              Rank        Studio      Gross   Year
**Title** 
Avengers: Endgame                1   Buena Vista  $2,796.30   2019
Avatar                           2           Fox  $2,789.70   2009
Titanic                          3     Paramount  $2,187.50   1997
Star Wars: The Force Awakens     4   Buena Vista  $2,068.20   2015

或者我们可以查看数据集的末尾:

In  [6] movies.tail(6)

Out [6]

                          Rank           Studio     Gross   Year
**Title** 
21 Jump Street             777             Sony   $201.60   2012
Yogi Bear                  778  Warner Brothers   $201.60   2010
Garfield: The Movie        779              Fox   $200.80   2004
Cats & Dogs                780  Warner Brothers   $200.70   2001
The Hunt for Red October   781        Paramount   $200.50   1990
Valkyrie                   782              MGM   $200.30   2008

我们可以找出DataFrame有多少行:

In  [7] len(movies)

Out [7] 782

我们可以询问DataFrame中的行数和列数。这个数据集有 782 行和 4 列:

In  [8] movies.shape

Out [8] (782, 4)

我们可以询问总共有多少个单元格:

In  [9] movies.size

Out [9] 3128

我们可以询问四个列的数据类型。在以下输出中,int64表示整数列,而object表示文本列:

In  [10] movies.dtypes

Out [10]

Rank       int64
Studio    object
Gross     object
Year       int64
dtype: object

我们可以通过数据集的行号(也称为索引位置)来提取一行。在大多数编程语言中,索引从 0 开始计数。因此,如果我们想提取数据集中的第 500 部电影,我们将目标设置为索引位置 499:

In  [11] movies.iloc[499]

Out [11] Rank           500
         Studio         Fox
         Gross      $288.30
         Year          2018
         Name: Maze Runner: The Death Cure, dtype: object

Pandas 在这里返回一个新的对象,称为Series,它是一个一维带标签的值数组。将其视为带有每行标识符的单列数据。注意,Series的索引标签(排名、工作室、总收入和年份)是movies DataFrame中的四列。Pandas 已经改变了原始行值的展示方式。

我们也可以使用索引标签来访问DataFrame中的行。作为提醒,我们的DataFrame索引包含电影的标题。让我们提取每个人最喜欢的催泪电影《阿甘正传》的行值。下一个示例通过索引标签而不是数字位置提取行:

In  [12] movies.loc["Forrest Gump"]

Out [12] Rank            119
         Studio    Paramount
         Gross       $677.90
         Year           1994
         Name: Forrest Gump, dtype: object

索引标签可以包含重复项。例如,DataFrame中有两部电影标题为"101 Dalmatians"(1961 年的原版和 1996 年的重拍版):

In  [13] movies.loc["101 Dalmatians"]

Out [13]

                Rank        Studio     Gross   Year 
**Title** 
101 Dalmatians   425   Buena Vista   $320.70   1996
101 Dalmatians   708   Buena Vista   $215.90   1961

虽然 pandas 允许重复项,但如果可能的话,我建议保持索引标签唯一。唯一的标签集合可以加快 pandas 定位和提取特定行的速度。

CSV 中的电影按排名列的值排序。如果我们想看到最近上映的五部电影,怎么办?我们可以按另一列的值对DataFrame进行排序,例如年份:

In  [14] movies.sort_values(by = "Year", ascending = False).head()

Out [14]

                                 Rank                  Studio   Gross  Year
**Title** 
Avengers: Endgame                   1             Buena Vista  2796.3  2019
John Wick: Chapter 3 - Parab...   458               Lionsgate   304.7  2019
The Wandering Earth               114  China Film Corporation   699.8  2019
Toy Story 4                       198             Buena Vista   519.8  2019
How to Train Your Dragon: Th...   199               Universal   519.8  2019

我们还可以根据多个列的值对DataFrame进行排序。让我们首先按 Studio 列的值对movies进行排序,然后按 Year 列的值进行排序。现在我们可以看到按工作室和发行日期字母顺序组织的电影:

In  [15] movies.sort_values(by = ["Studio", "Year"]).head()

Out [15]

                         Rank       Studio    Gross  Year
**Title** 
The Blair Witch Project   588      Artisan  $248.60  1999
101 Dalmatians            708  Buena Vista  $215.90  1961
The Jungle Book           755  Buena Vista  $205.80  1967
Who Framed Roger Rabbit   410  Buena Vista  $329.80  1988
Dead Poets Society        636  Buena Vista  $235.90  1989

我们还可以对索引进行排序,如果我们想按字母顺序查看电影,这很有帮助:

In  [16] movies.sort_index().head()

Out [16]

                  Rank           Studio    Gross  Year
**Title** 
10,000 B.C.        536  Warner Brothers  $269.80  2008
101 Dalmatians     708      Buena Vista  $215.90  1961
101 Dalmatians     425      Buena Vista  $320.70  1996
2 Fast 2 Furious   632        Universal  $236.40  2003
2012                93             Sony  $769.70  2009

我们迄今为止执行的操作返回新的DataFrame对象。Pandas 没有改变来自 CSV 文件的原始movies DataFrame。这些操作的非破坏性性质是有益的;它积极鼓励实验。我们总是在将其永久化之前确认结果是否正确。

1.3.3 在 Series 中计数值

让我们尝试一个更复杂的分析。如果我们想知道哪个电影工作室拥有最多的高票房电影,我们需要解决这个问题,我们需要计算每个工作室在 Studio 列中出现的次数。

我们可以从DataFrame中提取单个数据列作为Series。注意,pandas 保留了DataFrame的索引,即电影标题,在Series中:

In  [17] movies["Studio"]

Out [17] Title
         Avengers: Endgame                   Buena Vista
         Avatar                                      Fox
         Titanic                               Paramount
         Star Wars: The Force Awakens        Buena Vista
         Avengers: Infinity War              Buena Vista
                                              ...
         Yogi Bear                       Warner Brothers
         Garfield: The Movie                         Fox
         Cats & Dogs                     Warner Brothers
         The Hunt for Red October              Paramount
         Valkyrie                                    MGM
         Name: Studio, Length: 782, dtype: object

如果一个Series有大量的行,pandas 会截断数据集,只显示前五行和后五行。

现在我们已经隔离了 Studio 列,我们可以计算每个唯一值的出现次数。让我们将结果限制在前 10 个工作室:

In  [18] movies["Studio"].value_counts().head(10)

Out [18] Warner Brothers    132
         Buena Vista        125
         Fox                117
         Universal          109
         Sony                86
         Paramount           76
         Dreamworks          27
         Lionsgate           21
         New Line            16
         MGM                 11
         Name: Studio, dtype: int64

上面的返回值是另一个Series对象!这次,pandas 使用 Studio 列中的工作室作为索引标签,它们的计数作为Series的值。

1.3.4 通过一个或多个标准过滤列

你通常会想根据一个或多个标准提取行子集。Excel 提供了过滤工具来完成这个目的。

如果我们只想找到由环球公司发行的电影,我们可以用一行代码在 pandas 中完成这个任务:

In  [19] movies[movies["Studio"] == "Universal"]

Out [19]

                                Rank     Studio      Gross  Year
**Title** 
Jurassic World                     6  Universal  $1,671.70  2015
Furious 7                          8  Universal  $1,516.00  2015
Jurassic World: Fallen Kingdom    13  Universal  $1,309.50  2018
The Fate of the Furious           17  Universal  $1,236.00  2017
Minions                           19  Universal  $1,159.40  2015
    ...                          ...        ...        ...   ...
The Break-Up                     763  Universal    $205.00  2006
Everest                          766  Universal    $203.40  2015
Patch Adams                      772  Universal    $202.30  1998
Kindergarten Cop                 775  Universal    $202.00  1990
Straight Outta Compton           776  Universal    $201.60  2015

109 rows × 4 columns

我们可以将过滤条件分配给一个变量,为读者提供上下文:

In  [20] released_by_universal = (movies["Studio"] == "Universal")
         movies[released_by_universal].head()

Out [20]

                                Rank     Studio      Gross  Year
**Title** 
Jurassic World                     6  Universal  $1,671.70  2015
Furious 7                          8  Universal  $1,516.00  2015
Jurassic World: Fallen Kingdom    13  Universal  $1,309.50  2018
The Fate of the Furious           17  Universal  $1,236.00  2017
Minions                           19  Universal  $1,159.40  2015

我们还可以根据多个标准过滤DataFrame的行。下一个例子针对所有由环球公司发行且在 2015 年发行的电影:

In  [21] released_by_universal = movies["Studio"] == "Universal"
         released_in_2015 = movies["Year"] == 2015
         movies[released_by_universal & released_in_2015]

Out [21]

                       Rank     Studio       Gross  Year 
**Title** 
Jurassic World             6  Universal  $1,671.70  2015
Furious 7                  8  Universal  $1,516.00  2015
Minions                   19  Universal  $1,159.40  2015
Fifty Shades of Grey     165  Universal    $571.00  2015
Pitch Perfect 2          504  Universal    $287.50  2015
Ted 2                    702  Universal    $216.70  2015
Everest                  766  Universal    $203.40  2015
Straight Outta Compton   776  Universal    $201.60  2015

之前的例子包括了满足两个条件都成立的行。我们也可以筛选出符合任一条件的电影:由环球公司发行在 2015 年发行的。由于更多电影有机会满足这两个条件中的一个而不是两个,所以结果DataFrame更长:

In  [22] released_by_universal = movies["Studio"] == "Universal"
         released_in_2015 = movies["Year"] == 2015
         movies[released_by_universal | released_in_2015]

Out [22]

                                Rank       Studio      Gross  Year
**Title** 
Star Wars: The Force Awakens       4  Buena Vista  $2,068.20  2015
Jurassic World                     6    Universal  $1,671.70  2015
Furious 7                          8    Universal  $1,516.00  2015
Avengers: Age of Ultron            9  Buena Vista  $1,405.40  2015
Jurassic World: Fallen Kingdom    13    Universal  $1,309.50  2018
    ...                          ...          ...        ...   ...
The Break-Up                     763    Universal    $205.00  2006
Everest                          766    Universal    $203.40  2015
Patch Adams                      772    Universal    $202.30  1998
Kindergarten Cop                 775    Universal    $202.00  1990
Straight Outta Compton           776    Universal    $201.60  2015

140 rows × 4 columns

Pandas 提供了额外的过滤DataFrame的方法。例如,我们可以针对小于或大于特定值的列值进行目标定位。在这里,我们针对 1975 年之前发行的电影:

In  [23] before_1975 = movies["Year"] < 1975
         movies[before_1975]

Out [23]

                    Rank           Studio    Gross   Year
**Title** 
The Exorcist         252  Warner Brothers  $441.30   1973
Gone with the Wind   288              MGM  $402.40   1939
Bambi                540              RKO  $267.40   1942
The Godfather        604        Paramount  $245.10   1972
101 Dalmatians       708      Buena Vista  $215.90   1961
The Jungle Book      755      Buena Vista  $205.80   1967

我们还可以指定一个范围,所有值都必须落在这个范围内。下一个例子提取了 1983 年至 1986 年间发行的电影:

In  [24] mid_80s = movies["Year"].between(1983, 1986)
         movies[mid_80s]

Out [24]

                                      Rank     Studio     Gross   Year
**Title** 
Return of the Jedi                     222        Fox  $475.10   1983
Back to the Future                     311  Universal  $381.10   1985
Top Gun                                357  Paramount  $356.80   1986
Indiana Jones and the Temple of Doom   403  Paramount  $333.10   1984
Crocodile Dundee                       413  Paramount  $328.20   1986
Beverly Hills Cop                      432  Paramount  $316.40   1984
Rocky IV                               467        MGM  $300.50   1985
Rambo: First Blood Part II             469    TriStar  $300.40   1985
Ghostbusters                           485   Columbia  $295.20   1984
Out of Africa                          662  Universal  $227.50   1985

我们还可以使用DataFrame的索引来过滤行。下一个例子将索引中的电影标题转换为小写,并找到标题中包含单词"dark"的所有电影:

In  [25] has_dark_in_title = movies.index.str.lower().str.contains("dark")
         movies[has_dark_in_title]

Out [25]

                                Rank           Studio       Gross   Year
**Title** 
Transformers: Dark of the Moon    23        Paramount  $1,123.80   2011
The Dark Knight Rises             27  Warner Brothers  $1,084.90   2012
The Dark Knight                   39  Warner Brothers  $1,004.90   2008
Thor: The Dark World             132      Buena Vista    $644.60   2013
Star Trek Into Darkness          232        Paramount    $467.40   2013
Fifty Shades Darker              309        Universal    $381.50   2017
Dark Shadows                     600  Warner Brothers    $245.50   2012
Dark Phoenix                     603              Fox    $245.10   2019

注意,pandas 会找到所有包含单词"dark"的电影,无论该文本出现在标题的哪个位置。

1.3.5 数据分组

我们面临的下一个挑战是最复杂的。我们可能好奇哪个工作室在所有电影中的总收入最高。让我们按工作室对总收入列的值进行汇总。

我们面临的第一道难题是,总收入列的值存储为文本而不是数字。Pandas 将该列的值作为文本导入以保留原始 CSV 中的美元符号和逗号符号。我们可以将列的值转换为小数数字,但前提是必须删除这两个字符。下一个示例将所有出现的 "$""," 替换为空文本。这个操作类似于 Excel 中的查找和替换:

In  [26] movies["Gross"].str.replace(
             "$", "", regex = False
         ).str.replace(",", "", regex = False)

Out [26] Title
         Avengers: Endgame               2796.30
         Avatar                          2789.70
         Titanic                         2187.50
         Star Wars: The Force Awakens    2068.20
         Avengers: Infinity War          2048.40
                                           ...
         Yogi Bear                        201.60
         Garfield: The Movie              200.80
         Cats & Dogs                      200.70
         The Hunt for Red October         200.50
         Valkyrie                         200.30
         Name: Gross, Length: 782, dtype: object

去掉符号后,我们可以将总收入列的值从文本转换为浮点数:

In  [27] (
             movies["Gross"]
            .str.replace("$", "", regex = False)
            .str.replace(",", "", regex = False)
            .astype(float)
         )

Out [27] Title
         Avengers: Endgame               2796.3
         Avatar                          2789.7
         Titanic                         2187.5
         Star Wars: The Force Awakens    2068.2
         Avengers: Infinity War          2048.4
                                          ...
         Yogi Bear                        201.6
         Garfield: The Movie              200.8
         Cats & Dogs                      200.7
         The Hunt for Red October         200.5
         Valkyrie                         200.3
         Name: Gross, Length: 782, dtype: float64

这些操作是临时的,并不会修改原始的总收入 Series。在前面的所有示例中,pandas 创建了原始数据结构的副本,执行了操作,并返回了一个新对象。下一个示例明确地将 movies 中的总收入列覆盖为一个新的以小数点分隔的数字列。现在这个转换是永久的:

In  [28] movies["Gross"] = (
             movies["Gross"]
             .str.replace("$", "", regex = False)
             .str.replace(",", "", regex = False)
             .astype(float)
         )

我们的数据类型转换打开了更多计算和操作的大门。下一个示例计算电影的平均票房总收入:

In  [29] movies["Gross"].mean()

Out [29] 439.0308184143222

让我们回到最初的问题:计算每个电影工作室的总票房总收入。首先,我们需要识别工作室并将属于每个工作室的电影(或行)进行分类。这个过程称为“分组”。在下一个示例中,我们将 DataFrame 的行根据“工作室”列中的值进行分组:

In  [30] studios = movies.groupby("Studio")

我们可以要求 pandas 统计每个工作室的电影数量:

In  [31] studios["Gross"].count().head()

Out [31] Studio
         Artisan                     1
         Buena Vista               125
         CL                          1
         China Film Corporation      1
         Columbia                    5
         Name: Gross, dtype: int64

之前的结果是按工作室名称的字母顺序排序的。我们可以改为按电影数量排序 Series,从多到少:

In  [32] studios["Gross"].count().sort_values(ascending = False).head()

Out [32] Studio
         Warner Brothers    132
         Buena Vista        125
         Fox                117
         Universal          109
         Sony                86
         Name: Gross, dtype: int64

接下来,让我们为每个工作室添加总收入的值。Pandas 将识别属于每个工作室的电影子集,提取它们各自的总收入值,并将它们相加:

In  [33] studios["Gross"].sum().head()

Out [33] Studio
         Artisan                     248.6
         Buena Vista               73585.0
         CL                          228.1
         China Film Corporation      699.8
         Columbia                   1276.6
         Name: Gross, dtype: float64

再次,pandas 按工作室名称对结果进行排序。我们想要识别总收入最高的工作室,所以让我们按降序对 Series 中的值进行排序。以下是总收入最高的五个工作室:

In  [34] studios["Gross"].sum().sort_values(ascending = False).head()

Out [34] Studio
         Buena Vista        73585.0
         Warner Brothers    58643.8
         Fox                50420.8
         Universal          44302.3
         Sony               32822.5
         Name: Gross, dtype: float64

通过几行代码,我们可以从这个复杂的数据集中得出一些有趣的见解。例如,华纳兄弟工作室在列表中的电影比迪士尼工作室多,但迪士尼工作室所有电影的累计总收入更高。这一事实表明,迪士尼工作室电影的平均总收入高于华纳兄弟工作室电影。

我们只是刚刚触及 pandas 能够做到的事情的表面。我希望这些示例已经阐明了我们可以用这个强大的库以多种方式操纵和转换数据。我们将在整本书中更详细地讨论本章中使用的所有代码。接下来,我们将深入探讨 pandas 的核心构建块:Series 对象。

摘要

  • Pandas 是一个基于 Python 编程语言构建的数据分析库。

  • Pandas 在使用简洁的语法对大型数据集执行复杂操作方面表现出色。

  • Pandas 的竞争对手包括图形电子表格应用 Excel、统计编程语言 R 和 SAS 软件套件。

  • 编程所需的技能集合与使用 Excel 或 Sheets 工作不同。

  • Pandas 可以导入各种文件格式。一种流行的格式是 CSV,它使用换行符分隔行,使用逗号分隔行值。

  • DataFrame 是 pandas 中的主要数据结构。它实际上是一个具有多列的数据表。

  • Series 是一个一维带标签的数组。可以将其视为数据的一个单列。

  • 我们可以通过行号或索引标签访问 SeriesDataFrame 中的行。

  • 我们可以按一列或多列的值对 DataFrame 进行排序。

  • 我们可以使用逻辑条件从 DataFrame 中提取数据子集。

  • 我们可以根据列的值对 DataFrame 的行进行分组。我们还可以对结果组执行聚合操作,例如求和。


¹ 参见“pandas 库的未来是什么?”,Data School,www.dataschool.io/future-of-pandas

² 参见 Oliver Peckham 的文章,“TIOBE Index: Python Reaches Another All-Time High”,HPC Wire,mng.bz/w0XP

³ 参见 Andy Patrizio 的文章,“Excel: Your entry into the world of data analytics”,Computer World,mng.bz/qe6r

2 系列对象

本章节涵盖

  • 从列表、字典、元组等实例化 Series 对象

  • Series上设置自定义索引

  • 访问Series对象的属性和调用其方法

  • 对一个或多个 序列 执行数学运算

  • Series 传递给 Python 的内置函数

Pandas 的核心数据结构之一,Series是一个用于同质数据的单维标签数组。数组是有序值集合,类似于 Python 列表。术语同质意味着值具有相同的数据类型(例如,所有整数或所有布尔值)。

Pandas 为每个 Series 值分配一个 标签——一个我们可以用来定位值的标识符。该库还为每个 Series 值分配一个 顺序——一个在行中的位置。顺序从 0 开始计数;第一个 Series 值占据位置 0,第二个值占据位置 1,以此类推。Series 是一个一维数据结构,因为我们需要一个参考点来访问一个值:要么是一个标签,要么是一个位置。

系列结合并扩展了 Python 原生数据结构的最佳特性。就像列表一样,它以有序的方式存储其值。就像字典一样,它为每个值分配一个键/标签。我们获得了这两个对象的好处,以及超过 180 种数据操作方法。

在本章中,我们将熟悉Series对象的机制,学习如何计算Series值的总和和平均值,对每个Series值应用数学运算,以及更多内容。作为 pandas 的基石,Series是探索该库的完美起点。

2.1 系列概述

让我们创建一些 Series 对象,好吗?我们将从使用 import 关键字导入 pandas 和 NumPy 包开始;我们将在 2.1.4 节中使用后者库。pandasnumpy 的流行社区别名是 pdnp。我们可以使用 as 关键字给导入项指定别名:

In  [1] import pandas as pd
        import numpy as np

pd 命名空间包含 pandas 包的顶级导出,这是一个包含超过 100 个类、函数、异常、常量等功能的集合。有关这些概念的信息,请参阅附录 B。

pd视为图书馆的大厅——一个我们可以访问 pandas 可用功能的入口房间。图书馆的导出项作为pd的属性可用。我们可以使用点符号来访问一个属性:

pd.attribute

Jupyter Notebook 提供了一个方便的自动补全功能,用于搜索属性。输入库的名称,添加一个点,然后按 Tab 键以显示包的导出模态。随着你输入更多字符,笔记本会过滤结果以匹配你的搜索词。

图片

图 2.1 使用 Jupyter Notebook 的自动完成功能来显示以S开头的 pandas 导出

图 2.1 展示了自动完成功能在作用。在输入大写字母 S 后,我们可以按 Tab 键来显示所有以该字符开头的 pd 导出。请注意,搜索是区分大小写的。如果自动完成功能不起作用,请将以下代码添加到笔记本中的一个单元格中,执行它,然后再次尝试搜索:

%config Completer.use_jedi = False

我们可以使用键盘的上箭头和下箭头键在模式搜索结果中导航。幸运的是,Series 类是我们的第一个搜索结果。按 Enter 键来自动完成其名称。

2.1.1 类和实例

一个 是一个 Python 对象的蓝图。pd.Series 类是一个模板,下一步是创建它的具体实例。我们通过一对括号从类中实例化一个对象。让我们从 Series 类创建一个 Series 对象:

In  [2] pd.Series()

Out [2] Series([], dtype: float64)

可能会在输出旁边出现一个红色框中的警告:

DeprecationWarning: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.

因为我们没有提供任何要存储的值,pandas 无法推断 Series 应该持有的数据类型。无需担心;警告是预期行为。

我们已经成功创建了我们的第一个 Series 对象!不幸的是,它没有存储任何数据。让我们用一些值填充我们的 Series

2.1.2 用值填充 Series

一个 构造函数 是一个从类中构建对象的方法。当我们在第 2.1.1 节中写下 pd.Series() 时,我们使用了 Series 构造函数来创建一个新的 Series 对象。

当我们创建一个对象时,我们通常会想要定义其初始状态。我们可以将对象的初始状态视为其初始配置——其“设置”。我们通常可以通过传递给创建对象的构造函数的参数来设置状态。一个 参数 是我们传递给方法的输入。

让我们练习从手动数据创建一些 Series。目标是熟悉数据结构的外观和感觉。在未来,我们将使用导入的数据集来填充我们的 Series 的值。

Series 构造函数的第一个参数是一个可迭代对象,其值将填充 Series。我们可以传递各种输入,包括列表、字典、元组和 NumPy ndarrays。`

让我们使用 Python 列表中的数据创建一个 Series 对象。下一个示例声明了一个包含四个字符串的列表,将列表赋值给 ice_cream_flavors 变量,然后将列表传递给 Series 构造函数:

In  [3] ice_cream_flavors = [
            "Chocolate",
            "Vanilla",
            "Strawberry",
            "Rum Raisin",
        ]

        pd.Series(ice_cream_flavors)

Out [3] 0     Chocolate
        1       Vanilla
        2    Strawberry
        3    Rum Raisin
        dtype: object

极好——我们已经使用来自我们的 ice_cream_ flavors 列表的四个值创建了一个新的 Series。注意,pandas 保留了输入列表中字符串的顺序。我们稍后会回到 Series 左侧的数字。

一个 参数 是指给函数或方法的一个预期输入的名称。在幕后,Python 会将我们传递给构造函数的每个参数与一个参数匹配。我们可以在 Jupyter Notebook 中直接查看构造函数的参数。在一个新单元格中输入 pd.Series(),将鼠标光标放在括号之间,然后按 Shift+Tab。图 2.2 展示了出现的文档模式。

图片

图 2.2 一个带有 Series 构造函数参数和默认值的文档模式

重复按 Shift+Tab 键以显示更多信息。最终,Jupyter 将将文档面板固定在屏幕底部。

Series 构造函数定义了六个参数:dataindexdtypenamecopyfastpath。我们可以使用这些参数来设置对象的初始状态。我们可以将参数视为 Series 的配置选项。

文档显示每个参数及其默认值。默认值 是 Python 在我们没有为参数提供参数时使用的回退值。例如,如果我们不为 name 参数传递值,Python 将使用 None。具有默认值的参数本质上是可选的。它始终会有一些参数,无论是从其调用还是从其定义中隐式地获得。我们之前能够不带参数实例化一个 Series,因为其构造函数的所有六个参数都是可选的。

Series 构造函数的第一个参数 data 期望的是一个对象,其值将填充到 Series 中。如果我们向构造函数传递参数而没有参数名称,Python 将假设我们是按顺序传递它们的。在先前的代码示例中,我们将 ice_cream_flavors 列表作为构造函数的第一个参数传递;因此,Python 将其与第一个构造函数参数 data 匹配。Python 还将 indexdtypename 参数的默认值设置为 None,以及将 copyfastpath 参数的默认值设置为 False

我们可以使用关键字参数明确地将参数和参数连接起来(参见附录 B)。输入参数,后跟一个等号和其参数。在以下示例中,第一行使用位置参数,第二行使用关键字参数,但结果相同:

In  [4] # The two lines below are equivalent
        pd.Series(ice_cream_flavors)
        pd.Series(data = ice_cream_flavors)

Out [4] 0     Chocolate
        1       Vanilla
        2    Strawberry
        3    Rum Raisin
        dtype: object

关键字参数是有优势的,因为它们为每个构造函数参数提供了上下文。示例中的第二行更好地传达了 ice_cream_flavors 代表 Seriesdata

2.1.3 自定义 Series 索引

让我们更仔细地看看我们的 Series

0     Chocolate
1       Vanilla
2    Strawberry
3    Rum Raisin
dtype: object

之前我们提到,pandas 为每个 Series 值分配一个行位置。输出左侧的递增整数集合被称为索引。每个数字表示一个值在 Series 中的顺序。索引从 0 开始计数。字符串 "Chocolate" 占据索引 0,字符串 "Vanilla" 占据索引 1,以此类推。在图形电子表格应用程序中,数据的第一行从 1 开始计数——这是 pandas 和 Excel 之间的重要区别。

术语 index 既可以指代标识符的集合,也可以指代单个标识符。这两个表达都是有效的:“Series 的索引由整数组成”和“值 'Strawberry'Series 中的索引位置是 2。”

最后一个索引位置总是比值的总数少 1。当前的 Series 有四种冰淇淋口味,因此索引计数到 3。

除了索引位置之外,我们还可以为每个 Series 值分配一个索引标签。索引标签可以是任何不可变的数据类型:字符串、元组、日期时间等。这种灵活性使 Series 非常强大:我们可以通过其顺序或键/标签来引用值。从某种意义上说,每个值都有两个标识符。

Series 构造函数的第二个参数 index 用于设置 Series 的索引标签。如果我们不向该参数传递任何参数,pandas 默认使用从 0 开始的数值索引。这种类型的索引中,标签和位置标识符是相同的。

让我们构建一个具有自定义索引的 Series。我们可以向 dataindex 参数传递不同数据类型的对象,但它们的长度必须相同,这样 pandas 才能关联它们的值。下一个示例将字符串列表传递给 data 参数,并将字符串元组传递给 index 参数。列表和元组的长度都是 4

In  [5] ice_cream_flavors = [
            "Chocolate",
            "Vanilla",
            "Strawberry",
            "Rum Raisin",
        ]

        days_of_week = ("Monday", "Wednesday", "Friday", "Saturday")

        # The two lines below are equivalent
        pd.Series(ice_cream_flavors, days_of_week)
        pd.Series(data = ice_cream_flavors, index = days_of_week)

Out [5] Monday         Chocolate
        Wednesday        Vanilla
        Friday        Strawberry
        Saturday      Rum Raisin
        dtype: object

Pandas 使用共享索引位置将 ice_cream_flavors 列表和 days_of_week 元组中的值关联起来。例如,库在各自的对象中将 "Rum Raisin""Saturday" 视为索引位置 3;因此,它在 Series 中将它们关联起来。

即使索引由字符串标签组成,pandas 仍然为每个 Series 值分配一个索引位置。换句话说,我们可以通过索引标签 "Wednesday" 或索引位置 1 来访问值 "Vanilla"。我们将在第四章探讨如何通过行和标签访问 Series 元素。

索引允许重复,这是 Series 与 Python 字典区别的一个细节。在下一个示例中,字符串 "Wednesday"Series 的索引标签中出现了两次:

In  [6] ice_cream_flavors = [
            "Chocolate",
            "Vanilla",
            "Strawberry",
            "Rum Raisin",
        ]

        days_of_week = ("Monday", "Wednesday", "Friday", "Wednesday")

        # The two lines below are equivalent
        pd.Series(ice_cream_flavors, days_of_week)
        pd.Series(data = ice_cream_flavors, index = days_of_week)

Out [6] Monday        Chocolate
        Wednesday       Vanilla
        Friday       Strawberry
        Wednesday    Rum Raisin
        dtype: object

尽管 pandas 允许重复,但尽可能避免重复是理想的,因为唯一的索引允许库更快地定位索引标签。

关键字参数的一个额外优点是它们允许我们以任何顺序传递参数。相比之下,顺序/位置参数要求我们按照构造函数期望的顺序传递参数。下一个示例交换了 indexdata 关键字参数的顺序。Pandas 创建了相同的 Series

In  [7] pd.Series(index = days_of_week, data = ice_cream_flavors)

Out [7] Monday        Chocolate
        Wednesday       Vanilla
        Friday       Strawberry
        Wednesday    Rum Raisin
        dtype: object

我们还没有讨论输出中的一个部分:底部的 dtype 语句反映了 Series 中值的类型。对于大多数数据类型,pandas 将显示一个可预测的类型(例如 boolfloatint)。对于字符串和更复杂的对象(例如嵌套数据结构),pandas 将显示 dtype: object。¹

下面的示例从布尔值、整数和浮点数值的列表中创建 Series 对象。观察 Series 中的相似之处和不同之处:

In  [8] bunch_of_bools = [True, False, False]
        pd.Series(bunch_of_bools)

Out [8] 0     True
        1    False
        2    False
        dtype: bool

In  [9] stock_prices = [985.32, 950.44]
        time_of_day = ["Open", "Close"]
        pd.Series(data = stock_prices, index = time_of_day)

Out [9] Open     985.32
        Close    950.44
        dtype: float64

In  [10] lucky_numbers = [4, 8, 15, 16, 23, 42]
         pd.Series(lucky_numbers)

Out [10] 0     4
         1     8
         2    15
         3    16
         4    23
         5    42
         dtype: int64

float64int64 数据类型表明,Series 中的每个浮点数/整数值在您的计算机 RAM 中占用 64 位(8 字节)。位和字节是内存的存储单位。我们不需要深入探讨这些计算机科学概念,就可以有效地使用 pandas。

Pandas 会尽力从 data 参数的值推断出适合 Series 的数据类型。我们可以通过构造函数的 dtype 参数强制转换到不同的类型。下一个示例将整数列表传递给构造函数,但要求一个浮点数 Series

In  [11] lucky_numbers = [4, 8, 15, 16, 23, 42]
 pd.Series(lucky_numbers, dtype = "float")

Out [11] 0     4.0
         1     8.0
         2    15.0
         3    16.0
         4    23.0
         5    42.0
         dtype: float64

之前的示例同时使用了位置参数和关键字参数。我们按顺序将 lucky_numbers 列表传递给 data 参数。我们还通过关键字参数显式地传递了 dtype 参数。Series 构造函数期望 dtype 参数是第三个参数,因此我们不能直接在 lucky_numbers 之后传递它;我们必须使用关键字参数。

2.1.4 创建带有缺失值的 Series

到目前为止,一切顺利。我们到目前为止的 Series 都很简单且完整。当我们自己构建数据集时,拥有完美的数据很容易。在现实世界中,数据要复杂得多。分析师遇到的最常见问题可能是缺失值。

当 pandas 在文件导入期间看到缺失值时,库会替换 NumPy 的 nan 对象。nan 的缩写是 not a number,是一个用于未定义值的通用术语。换句话说,nan 是一个表示空缺或缺失的占位符对象。

让我们在 Series 中偷偷加入一个缺失值。当我们之前导入时,我们将 NumPy 库分配给别名 np。库的顶层导出中可用 nan 属性。下一个示例将 np.nan 嵌入我们传递给 Series 构造函数的温度列表中。注意输出中索引位置 2 的 NaN。习惯这三个字母的组合;我们将在整本书中经常看到它们:

In  [12] temperatures = [94, 88, np.nan, 91]
         pd.Series(data = temperatures)

Out [12] 0    94.0
         1    88.0
         2     NaN
         3    91.0
         dtype: float64

注意,Seriesdtypefloat64。当 Pandas 发现 nan 值时,会自动将数值从整数转换为浮点数;这个内部技术要求使得库能够将数值和缺失值存储在同一个同质的 Series 中。

2.2 从 Python 对象创建 Series

Series 构造函数的 data 参数接受各种输入,包括原生 Python 数据结构和来自其他库的对象。在本节中,我们将探讨 Series 构造函数如何处理字典、元组、集合和 NumPy 数组。pandas 返回的 Series 对象无论其数据源如何,操作方式都是相同的。

一个 字典 是一组键值对(参见附录 B)。当传递一个字典给构造函数时,它会将每个键设置为 Series 中相应的索引标签:

In  [13] calorie_info = {
             "Cereal": 125,
             "Chocolate Bar": 406,
             "Ice Cream Sundae": 342,
         }

         diet = pd.Series(calorie_info)
         diet

Out [13] Cereal              125
         Chocolate Bar       406
         Ice Cream Sundae    342
         dtype: int64

一个 元组 是一个不可变列表。在创建元组之后,我们无法添加、删除或替换其中的元素(参见附录 B)。当传递一个元组给构造函数时,它会以预期的方式填充 Series

In  [14] pd.Series(data = ("Red", "Green", "Blue"))

Out [14] 0      Red
         1    Green
         2     Blue
         dtype: object

要创建一个存储元组的 Series,请将元组包裹在一个列表中。元组非常适合由多个部分或组件组成的行值,例如地址:

In  [15] rgb_colors = [(120, 41, 26), (196, 165, 45)]
         pd.Series(data = rgb_colors)

Out [15] 0     (120, 41, 26)
         1    (196, 165, 45)
         dtype: object

一个 集合 是一个无序的唯一值集合。我们可以用一对花括号来声明它,就像字典一样。Python 使用键值对的存在来区分这两种数据结构(参见附录 B)。

如果我们将一个集合传递给 Series 构造函数,pandas 会引发一个 TypeError 异常。集合既没有顺序的概念(如列表)也没有关联的概念(如字典)。因此,库无法假设一个存储集合值的顺序:²

In  [16] my_set = {"Ricky", "Bobby"}
         pd.Series(my_set)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-16-bf85415a7772> in <module>
      1 my_set = { "Ricky", "Bobby" }
----> 2 pd.Series(my_set)

TypeError: 'set' type is unordered

如果你的程序涉及一个集合,在将其传递给 Series 构造函数之前,将其转换为有序数据结构。下一个示例通过使用 Python 的内置 list 函数将 my_set 转换为列表:

In  [17] pd.Series(list(my_set))

Out [17] 0    Ricky
         1    Bobby
         dtype: object

由于集合是无序的,我们无法保证列表元素(或 Series 元素)的顺序。

Series 构造函数的 data 参数也接受一个 NumPy ndarray 对象。许多数据科学库使用 NumPy 数组,这是移动数据时的常见存储格式。下一个示例通过 NumPy 的 randint 函数生成的 ndarray 来向 Series 构造函数提供数据(参见附录 C):

In  [18] random_data = np.random.randint(1, 101, 10)
         random_data

Out [18] array([27, 16, 13, 83,  3, 38, 34, 19, 27, 66])

In  [19] pd.Series(random_data)

Out [19] 0    27
         1    16
         2    13
         3    83
         4     3
         5    38
         6    34
         7    19
         8    27
         9    66
         dtype: int64

与所有其他输入一样,pandas 保留 ndarray 的值在 Series 中的顺序。

2.3 Series 属性

一个 属性 是属于一个对象的数据片段。属性揭示了关于对象内部状态的信息。一个属性的值可能是一个对象。参见附录 B 以获取深入了解。

Series 由几个更小的对象组成。将这些对象想象成拼图碎片,它们组合在一起形成一个更大的整体。考虑第 2.2 节中的 calorie_info Series

Cereal              125
Chocolate Bar       406
Ice Cream Sundae    342
dtype: int64

这个 Series 使用 NumPy 库的 ndarray 对象来存储卡路里计数,以及 pandas 库的 Index 对象来存储食品名称作为索引。我们可以通过 Series 属性访问这些嵌套对象。例如,values 属性暴露了存储值的 ndarray 对象:

In  [20] diet.values

Out [20] array([125, 406, 342])

如果我们不确定对象的类型或它来自哪个库,我们可以将对象传递给 Python 的内置type函数。该函数将返回对象实例化的类:

In  [21] type(diet.values)

Out [21] numpy.ndarray

让我们在这里暂停一下,稍作思考。Pandas 将存储Series值的责任委托给来自不同库的对象。这就是为什么 NumPy 是 pandas 的依赖项。ndarray对象通过依赖底层 C 编程语言进行许多计算,从而优化速度和效率。在许多方面,Series是一个包装器——围绕核心 NumPy 库对象的额外功能层。

当然,Pandas 有其自己的对象。例如,index属性返回存储Series标签的Index对象:

In  [22] diet.index

Out [22] Index(['Cereal', 'Chocolate Bar', 'Ice Cream Sundae'],
         dtype='object')

索引对象,例如Index,是内置到 pandas 中的:

In  [23] type(diet.index)

Out [23] pandas.core.indexes.base.Index

一些属性揭示了关于对象的有用细节。例如,dtype返回Series值的类型:

In  [24] diet.dtype

Out [24] dtype('int64')

size属性返回Series中的值数:

In  [25] diet.size

Out [25] 3

补充的shape属性返回一个元组,包含 pandas 数据结构的维度。对于一维Series,元组的唯一值将是Series的大小。Python 中一个元素元组的逗号后是一个标准的视觉输出:

In  [26] diet.shape
Out [26] (3,)

is_unique属性在所有Series值都是唯一的时返回True

In  [27] diet.is_unique

Out [27] True

is_unique属性在Series包含重复项时返回False

In  [28] pd.Series(data = [3, 3]).is_unique

Out [28] False

is_monotonic属性在Series的每个值都大于前一个值时返回True。值之间的增量不必相等:

In  [29] pd.Series(data = [1, 3, 6]).is_monotonic

Out [29] True

is_monotonic属性在任意元素小于前一个元素时返回False

In  [30] pd.Series(data = [1, 6, 3]).is_monotonic

Out [30] False

总结来说,属性询问对象关于其内部状态的信息。属性揭示了嵌套对象,它们可以有自己的功能。在 Python 中,一切都是对象,包括整数、字符串和布尔值。因此,返回数字的属性在技术上与返回复杂对象(如ndarray)的属性没有区别。

2.4 获取第一行和最后一行

到现在为止,你应该已经能够舒适地创建Series对象。如果技术术语有点令人不知所措,那也无可厚非;我们在一开始就提供了大量信息,并在整本书中多次回顾。在本节中,我们将开始探索我们可以用Series对象做什么。

Python 对象既有属性也有方法。一个属性是属于对象的数据片段——数据结构可以揭示的关于自身的特征或细节。在第 2.3 节中,我们访问了Series的属性,如sizeshapevaluesindex

相比之下,一个方法是属于对象的功能——我们要求对象执行的动作或命令。方法通常涉及对对象属性的分析、计算或操作。属性定义了对象的状态,而方法定义了对象的行为

让我们创建我们迄今为止最大的Series。我们将使用 Python 的内置range函数生成一个从起点到终点的所有数字的序列。range函数的三个参数是下限、上限和步进序列(每两个数字之间的间隔)。

下一个示例生成一个介于 0 到 500 之间,增量为 5 的 100 个值的范围,然后将范围对象传递给Series构造函数:

In  [31] values = range(0, 500, 5)
         nums = pd.Series(data = values)
         nums

Out [31] 0       0
         1       5
         2      10
         3      15
         4      20
               ...
         95    475
         96    480
         97    485
         98    490
         99    495
         Length: 100, dtype: int64

现在我们有一个包含 100 个值的Series。真棒!注意输出中间出现的省略号(三个点)。Pandas 正在告诉我们它通过隐藏一些行来压缩了输出。这个库方便地截断了Series,只显示前五行和后五行。打印过多的数据行可能会减慢 Jupyter Notebook。

我们在方法名称后面调用一对括号。让我们调用一些简单的Series方法。我们将从head方法开始,它返回数据集的开始或顶部的行。它接受一个参数n,该参数设置要提取的行数:

In  [32] nums.head(3)

Out [32] 0     0
         1     5
         2    10
         dtype: int64

我们可以在方法调用中传递关键字参数,就像在构造函数和函数中一样。以下代码产生与前面代码相同的结果:

In  [33] nums.head(n = 3)

Out [33] 0     0
         1     5
         2    10
         dtype: int64

和函数一样,方法可以为它们的参数声明默认参数。head方法的n参数有一个默认参数为5。如果我们没有为n传递显式参数,pandas 将返回五行(这是 pandas 开发团队的设计决策):

In  [34] nums.head()

Out [34] 0     0
         1     5
         2    10
         3    15
         4    20
         dtype: int64

补充的tail方法返回Series的底部或末尾的行:

In  [35] nums.tail(6)

Out [35] 94    470
         95    475
         96    480
         97    485
         98    490
         99    495
         dtype: int64

tail方法的n参数也有一个默认参数为5

In  [36] nums.tail()

Out [36] 95    475
         96    480
         97    485
         98    490
         99    495
         dtype: int64

headtail是我最常用的两个方法;我们可以使用它们快速预览数据集的开始和结束。接下来,让我们深入了解一些更高级的Series方法。

2.5 数学运算

Series对象包含大量的统计和数学方法。让我们看看这些方法在实际中的应用。您可以随意快速浏览这一节,并在需要查找特定函数时再回来。

2.5.1 统计操作

我们将从一个升序数字列表开始创建一个Series,在中间偷偷加入一个np.nan值。记住,如果一个数据源有缺失值,pandas 会将整数强制转换为浮点值:

In  [37] numbers = pd.Series([1, 2, 3, np.nan, 4, 5])
         numbers

Out [37] 0    1.0
         1    2.0
         2    3.0
         3    NaN
         4    4.0
         5    5.0
         dtype: float64

count方法计算非空值的数量:

In  [38] numbers.count()

Out [38] 5

sum方法将Series的值相加:

In  [39] numbers.sum()

Out [39] 15.0

默认情况下,大多数数学方法忽略缺失值。我们可以通过将skipna参数的值设置为False来强制包含缺失值。

下一个示例使用带有参数的sum方法。Pandas 返回一个nan,因为它无法将索引3处的未知nan值加到累积和中:

In  [40] numbers.sum(skipna = False)

Out [40] nan

sum 方法的 min_count 参数设置了一个 序列 必须持有的有效值的最小数量,以便 pandas 计算其总和。我们的六个元素 numbers 序列 包含五个有效值和一个 nan

在下一个示例中,序列 达到了三个有效值的阈值,因此 pandas 返回总和:

In  [41] numbers.sum(min_count = 3)

Out [41] 15.0

相比之下,下一个调用要求 pandas 计算总和至少有六个值。阈值未满足,因此 sum 方法返回 nan

In  [42] numbers.sum(min_count = 6)

Out [42] nan

提示:如果您对某个方法的参数感到好奇,请在 Jupyter Notebook 中方法括号之间按 Shift+Tab 键,以显示文档。

product 方法将所有 序列 值相乘:

In  [43] numbers.product()

Out [43] 120.0

该方法还接受 skipnamin_count 参数。在这里,我们要求 pandas 将 nan 值包含在计算中:

In  [44] numbers.product(skipna = False)

Out [44] nan

在下一个示例中,如果 序列 至少有三个有效值,则请求所有 序列 值的乘积:

In  [45] numbers.product(min_count = 3)

Out [45] 120.0

cumsum(累积和)方法返回一个新的 序列,其中包含值的滚动总和。每个索引位置都包含从该索引开始并包括该索引的值的总和。累积和有助于确定哪些值对总和的贡献最大:

In  [46] numbers

Out [46] 0    1.0
         1    2.0
         2    3.0
         3    NaN
         4    4.0
         5    5.0
         dtype: float64

In  [47] numbers.cumsum()

Out [47] 0     1.0
         1     3.0
         2     6.0
         3     NaN
         4    10.0
         5    15.0
         dtype: float64

让我们来看看结果中的一些计算:

  • 索引 0 的累积和为 1.0,这是 numbers 序列 中的第一个值。目前还没有任何值可以相加。

  • 索引 1 的累积和为 3.0,是索引 0 的 1.0 和索引位置 1 的 2.0 的总和。

  • 索引 2 的累积和为 6.0,是 1.0、2.0 和 3.0 的总和。

  • numbers 序列 在索引 3 处有一个 nan。Pandas 无法将缺失值添加到累积和中,因此在返回的 序列 中相同索引处放置一个 nan

  • 索引 4 的累积和为 10.0。Pandas 将前一个累积和与当前索引的值(1.0 + 2.0 + 3.0 + 4.0)相加。

如果我们传递 skipna 的参数为 False,则 序列 将列出直到第一个缺失值的累积和,然后对于剩余的值使用 NaN

In  [48] numbers.cumsum(skipna = False)

Out [48] 0    1.0
         1    3.0
         2    6.0
         3    NaN
         4    NaN
         5    NaN
         dtype: float64

pct_change(百分比变化)方法返回一个 序列 中一个值与下一个值之间的百分比差异。在每个索引处,pandas 将最后一个索引的值和当前索引的值相加,然后将总和除以最后一个索引的值。Pandas 只能在两个索引都有有效值的情况下计算百分比差异。

pct_change 方法默认使用 前向填充 策略处理缺失值。使用此策略,pandas 将最后一个有效值替换为 nan。让我们调用该方法,然后了解计算过程:

In  [49] numbers

Out [49] 0    1.0
         1    2.0
         2    3.0
         3    NaN
         4    4.0
         5    5.0
         dtype: float64

In  [50] numbers.pct_change()

Out [50] 0         NaN
         1    1.000000
         2    0.500000
         3    0.000000
         4    0.333333
         5    0.250000
         dtype: float64

这是 pandas 的工作方式:

  • 在索引 0 处,pandas 无法将 numbers 序列 中的值 1.0 与任何先前值进行比较。因此,返回 序列 中的索引 0 有一个 NaN 值。

  • 在索引 1 处,pandas 将索引 1 的值 2.0 与索引 0 的值 1.0 进行比较。2.0 和 1.0 之间的百分比变化是 100(翻倍),这在返回的 序列 中转换为索引 1 的 1.00000。

  • 在索引 2 处,pandas 重复相同的操作。

  • 在索引 3 处,numbers Series有一个缺失的NaN值。Pandas 用最后遇到的值(索引 2 处的 3.0)来替代它。替代的 3.0 在索引 3 处和索引 2 处的 3.0 之间的百分比变化是 0。

  • 在索引 4 处,pandas 将索引 4 的值 4.0 与前一行的值进行比较。它再次用它看到的最后一个有效值 3.0 来替代nan。4 和 3 之间的百分比变化是 0.333333(增加了 33%)。

图 2.3 展示了前向填充百分比变化计算的视觉表示。左侧的Series是起点。中间的Series显示了 pandas 执行的中间计算。右侧的Series是最终结果。

Paskhaver 的图片

图 2.3:pct_change方法如何使用前向填充解决方案计算值的过程

fill_method参数自定义了pct_change用何种协议来替代NaN值。此参数在许多方法中都可用,因此花时间熟悉它是值得的。如前所述,使用默认的前向填充策略,pandas 用最后一个有效的观察值来替代nan值。我们可以传递一个显式的参数"pad""ffill"fill_method参数以实现相同的结果:

In  [51] # The three lines below are equivalent
         numbers.pct_change()
         numbers.pct_change(fill_method = "pad")
         numbers.pct_change(fill_method = "ffill")

Out [51] 0         NaN
         1    1.000000
         2    0.500000
         3    0.000000
         4    0.333333
         5    0.250000
         dtype: float64

处理缺失值的另一种策略是后向填充解决方案。使用此选项,pandas 用下一个有效的观察值来替代nan值。让我们传递一个值为"bfill"fill_method参数来查看结果,然后逐步进行说明:

In  [52] # The two lines below are equivalent
         numbers.pct_change(fill_method = "bfill")
         numbers.pct_change(fill_method = "backfill")

Out [52] 0         NaN
         1    1.000000
         2    0.500000
         3    0.333333
         4    0.000000
         5    0.250000
         dtype: float64

注意,在索引位置 3 和 4 之间,前向填充和后向填充解决方案的值不同。以下是 pandas 如何得出先前计算的方法:

  • 在索引 0 处,pandas 无法将numbers Series中的值 1.0 与任何先前值进行比较。因此,返回的Series中的索引 0 有一个NaN值。

  • 在索引 3 处,pandas 在numbers Series中遇到了一个NaN。Pandas 用下一个有效值(索引 4 处的 4.0)来替代它。numbers中索引 3 处的 4.0 和索引 2 处的 3.0 之间的百分比变化是 0.33333。

  • 在索引 4 处,pandas 将 4.0 与索引 3 的值进行比较。它再次用numbers Series中下一个有效的值 4.0 来替代索引 3 处的NaN。4 和 4 之间的百分比变化是 0.0。

图 2.4 展示了后向填充百分比变化计算的视觉表示。左侧的Series是起点。中间的Series显示了 pandas 执行的中间计算。右侧的Series是最终结果。

Paskhaver 的图片

图 2.4:pct_change方法如何使用后向填充解决方案计算值的过程

mean方法返回Series中值的平均值。平均值是值总和除以值计数的结果:

In  [53] numbers.mean()

Out [53] 3.0

median 方法返回排序后的 Series 值中的中间数。Series 值中有一半会低于中位数,另一半会高于中位数:

In  [54] numbers.median()

Out [54] 3.0

std 方法返回 标准差,这是衡量数据变异程度的指标:

In  [55] numbers.std()

Out [55] 1.5811388300841898

maxmin 方法从 Series 中检索最大和最小值:

In  [56] numbers.max()

Out [56] 5.0

In  [57] numbers.min()

Out [57] 1.0

Pandas 按字母顺序对字符串 Series 进行排序。最小的字符串是接近字母表开头的字符串,最大的字符串是接近字母表末尾的字符串。以下是一个简单的示例,使用了一个小的 Series

In  [58] animals = pd.Series(["koala", "aardvark", "zebra"])
         animals

Out [58] 0       koala
         1    aardvark
         2       zebra
         dtype: object

In  [59] animals.max()

Out [59] 'zebra'

In  [60] animals.min()

Out [60] 'aardvark'

如果你正在寻找一个可以有效地总结 Series 的单一方法,强大的 describe 方法可以做到这一点。它返回一个包含计数、平均值和标准差的统计评估 Series

In  [61] numbers.describe()

Out [61] count    5.000000
         mean     3.000000
         std      1.581139
         min      1.000000
         25%      2.000000
         50%      3.000000
         75%      4.000000
         max      5.000000
         dtype: float64

sample 方法从 Series 中选择一组随机的值。新 Series 和原始 Series 中的值顺序可能不同。在下一个示例中,请注意,随机选择中没有 NaN 值允许 pandas 返回一个整数的 Series。如果 NaN 是其中的任何一个值,pandas 将返回一个浮点数的 Series

In  [62] numbers.sample(3)

Out [62] 1    2
         3    4
         2    3
         dtype: int64

unique 方法返回 Series 中唯一值的 NumPy ndarray。在下一个示例中,字符串 "Orwell"authors Series 中出现两次,但在返回的 ndarray 中只出现一次:

In  [63] authors = pd.Series(
             ["Hemingway", "Orwell", "Dostoevsky", "Fitzgerald", "Orwell"]
         )

         authors.unique()

Out [63] array(['Hemingway', 'Orwell', 'Dostoevsky', 'Fitzgerald'],
         dtype=object)

补充的 nunique 方法返回 Series 中唯一值的数量:

In  [64] authors.nunique()

Out [64] 4

nunique 方法的返回值将等于 unique 方法返回的数组的长度。

2.5.2 算术运算

在 2.5.1 节中,我们练习了在 Series 对象上调用多个数学方法。Pandas 给我们提供了执行算术计算的其他方法。让我们首先创建一个包含一个缺失值的整数 Series

In  [65] s1 = pd.Series(data = [5, np.nan, 15], index = ["A", "B", "C"])
         s1
Out [65] A     5.0
         B     NaN
         C    15.0
         dtype: float64

我们可以使用 Python 的标准数学运算符对 Series 进行算术运算:

  • + 用于加法

  • - 用于减法

  • * 用于乘法

  • / 用于除法

语法直观:将 Series 视为数学运算符一侧的常规操作数。将补充值放在运算符的另一侧。请注意,任何涉及 nan 的数学运算都会产生另一个 nan。下一个示例将 3 添加到 s1 Series 中的每个值:

In  [66] s1 + 3

Out [66] A     8.0
         B     NaN
         C    18.0
         dtype: float64

一些软件开发人员可能会对结果感到惊讶。我们如何将一个整数添加到数据结构中?看起来这些类型是不兼容的。在幕后,pandas 足够智能,能够解析我们的语法,并理解我们想要将整数添加到 Series 中的每个值,而不是添加到 Series 对象本身。

如果你更喜欢基于方法的方法,add 方法可以达到相同的结果:

In  [67] s1.add(3)

Out [67] A     8.0
         B     NaN
         C    18.0
         dtype: float64

下面的三个示例展示了减法(-)、乘法(*)和除法(/)的补充语法选项。在 pandas 中,通常有多种方式可以完成相同的操作:

In  [68] # The three lines below are equivalent
         s1 - 5
         s1.sub(5)
         s1.subtract(5)

Out [68] A     0.0
         B     NaN
         C    10.0
         dtype: float64

In  [69] # The three lines below are equivalent
         s1 * 2
         s1.mul(2)
         s1.multiply(2)

Out [69] A    10.0
         B     NaN
         C    30.0
         dtype: float64

In  [70] # The three lines below are equivalent
         s1 / 2
         s1.div(2)
         s1.divide(2)

Out [70] A    2.5
         B    NaN
         C    7.5
         dtype: float64

向下取整除法运算符 (//) 执行除法并从结果中移除小数点后的任何数字。例如,15 除以 4 的常规除法得到 3.75,而向下取整除法得到 3。我们可以将此运算符应用于 Series;另一种选择是调用 floordiv 方法:

In  [71] # The two lines below are equivalent
         s1 // 4
         s1.floordiv(4)

Out [71] A    1.0
         B    NaN
         C    3.0
         dtype: float64

取模运算符 (%) 返回除法的余数。以下是一个例子:

In  [72] # The two lines below are equivalent
         s1 % 3
         s1.mod(3)

Out [72] A    2.0
         B    NaN
         C    0.0
         dtype: float64

在前面的例子中,

  • Pandas 将索引标签 A 上的值 5.0 除以 3,余数为 2.0。

  • Pandas 不能将索引标签 B 上的 NaN 进行除法。

  • Pandas 将索引标签 C 上的值 15.0 除以 3,余数为 0.0。

2.5.3 广播

回想一下,pandas 在底层使用 NumPy ndarray 存储其 Series 值。当我们使用 s1 + 3s1 - 5 这样的语法时,pandas 将数学计算委托给 NumPy。

NumPy 文档使用术语 广播 来描述从一个数组派生另一个数组的过程。不深入技术细节(您不需要理解 NumPy 的复杂性就能有效地使用 pandas),术语 广播 来自一个广播塔,它向所有收听的人发送相同的信号。像 s1 + 3 这样的语法意味着“将相同的操作(加 3)应用于 Series 中的每个值。”每个 Series 值都收到相同的信息,就像同时收听同一电台的每个人都能听到相同的歌曲一样。

广播还描述了多个 Series 对象之间的数学运算。作为一个经验法则,pandas 使用共享索引标签在不同数据结构之间对齐值。让我们通过一个例子来演示这个概念。让我们实例化两个具有相同三个元素索引的 Series

In  [73] s1 = pd.Series([1, 2, 3], index = ["A", "B", "C"])
         s2 = pd.Series([4, 5, 6], index = ["A", "B", "C"])

当我们使用 + 运算符并将两个 Series 作为操作数时,pandas 会将相同索引位置的值相加:

  • 在索引 A 处,pandas 将值 1 和 4 相加得到 5。

  • 在索引 B 处,pandas 将值 2 和 5 相加得到 7。

  • 在索引 C 处,pandas 将值 3 和 6 相加得到 9。

In  [74] s1 + s2

Out [74] A    5
         B    7
         C    9
         dtype: int64

图 2.5 展示了 pandas 如何对齐两个 Series

图片

图 2.5 展示了 pandas 在执行数学运算时通过共享索引标签对齐 Series

这里是 pandas 使用共享索引标签对齐数据的另一个例子。让我们创建另外两个具有标准数值索引的 Series。我们将在每个集合中添加一个缺失值:

In  [75] s1 = pd.Series(data = [3, 6, np.nan, 12])
         s2 = pd.Series(data = [2, 6, np.nan, 12])

Python 的等号运算符 (==) 用于比较两个对象的相等性。我们可以使用这个运算符来比较两个 Series 中的值,如下例所示。请注意,pandas 认为 nan 值与另一个 nan 不相等;它不能假设缺失的值与另一个缺失的值相等。等号运算符的等效方法是 eq

In  [76] # The two lines below are equivalent
         s1 == s2
         s1.eq(2)

Out [76] 0    False
         1     True
         2    False
         3     True
         dtype: bool

不等号运算符 (!=) 确认两个值是否不相等。它的等效方法是 ne

In  [77] # The two lines below are equivalent
         s1 != s2
         s1.ne(s2)

Out [77] 0     True
         1    False
         2     True
         3    False
         dtype: bool

当索引不同时,Series之间的比较操作会变得复杂。一个索引可能包含更多或更少的标签,或者标签本身可能存在不匹配。

下一个示例创建了两个只共享两个索引标签 B 和 C 的Series

In  [78] s1 = pd.Series(
             data = [5, 10, 15], index = ["A", "B", "C"]
         )

         s2 = pd.Series(
             data = [4, 8, 12, 14], index = ["B", "C", "D", "E"]
         )

当我们尝试将s1s2相加时会发生什么?Pandas 会在 B 和 C 标签处添加值,并在剩余的索引(A、D 和 E)处返回NaN值。提醒一下,任何与NaN值进行的算术运算总是结果为NaN

In  [79] s1 + s2

Out [79] A     NaN
         B    14.0
         C    23.0
         D     NaN
         E     NaN
         dtype: float64

图 2.6 显示了 pandas 如何对齐s1s2 Series,然后添加它们相关的索引值。

图片

图 2.6 Pandas 在Series不共享索引标签时返回NaN

总结来说,pandas 通过在两个Series之间共享索引标签来对齐数据,并在需要的地方用NaN替换。

2.6 将Series传递给 Python 的内置函数

Python 的开发者社区喜欢围绕某些设计原则进行团结,以确保代码库之间的连贯性。一个例子是库对象与 Python 内置函数的无缝集成。Pandas 也不例外。我们可以将Series传递给 Python 的任何内置函数,并得到可预测的结果。让我们创建一个包含美国城市的Series

In  [80] cities = pd.Series(
             data = ["San Francisco", "Los Angeles", "Las  Vegas", np.nan]
         )

len函数返回Series中的行数。计数包括缺失值(NaNs):

In  [81] len(cities)

Out [81] 4

如我们之前看到的,type函数返回对象的类。当你不确定你正在处理的数据结构或其来源的库时,请使用此函数:

In  [82] type(cities)

Out [82] pandas.core.series.Series

dir函数返回一个对象属性和方法作为字符串的列表。注意,下一个示例显示了输出的简略版本:

In  [83] dir(cities)

Out [83] ['T',
          '_AXIS_ALIASES',
          '_AXIS_IALIASES',
          '_AXIS_LEN',
          '_AXIS_NAMES',
          '_AXIS_NUMBERS',
          '_AXIS_ORDERS',
          '_AXIS_REVERSED',
          '_HANDLED_TYPES',
          '__abs__',
          '__add__',
          '__and__',
          '__annotations__',
          '__array__',
          '__array_priority__',
          #...
         ]

Series的值可以填充一个原生的 Python 数据结构。下一个示例通过使用 Python 的list函数从我们的cities Series创建一个列表:

In  [84] list(cities)

Out [84] ['San Francisco', 'Los Angeles', 'Las  Vegas', nan]

我们可以将Series传递给 Python 的内置dict函数来创建一个字典。Pandas 将Series的索引标签和值映射到字典的键和值:

In  [85] dict(cities)

Out [85] {0: 'San Francisco', 1: 'Los Angeles', 2: 'Las  Vegas', 3: nan}

在 Python 中,我们使用in关键字来检查包含。在 pandas 中,我们可以使用in关键字来检查给定值是否存在于Series的索引中。这里是一个关于cities的提醒:

In  [86] cities

Out [86] 0    San Francisco
         1      Los Angeles
         2       Las  Vegas
         3              NaN
         dtype: object

下两个示例在Series的索引中查询"Las Vegas"2

In  [87] "Las Vegas" in cities

Out [87] False

In  [88] 2 in cities

Out [88] True

要检查Series值中的包含情况,我们可以将in关键字与values属性配对。记住,values暴露了包含实际数据的ndarray对象:

In  [89] "Las Vegas" in cities.values

Out [89] True

我们可以使用逆not in运算符来检查排除。如果 pandas 在Series中找不到该值,则该运算符返回True

In  [90] 100 not in cities

Out [90] True

In  [91] "Paris" not in cities.values

Out [91] True

pandas 对象通常会与 Python 的内置函数集成,并提供自己的属性/方法来返回相同的数据。选择最适合你的语法选项。

2.7 编程挑战

欢迎来到本书的第一个编码挑战!这些练习的目的是帮助你应用和复习本章中介绍的概念。你将在问题之后立即找到解决方案。祝你好运!

2.7.1 问题

假设你被给出了这两个数据结构:

In  [92] superheroes = [
             "Batman",
             "Superman",
             "Spider-Man",
             "Iron Man",
             "Captain America",
             "Wonder Woman"
         ]

In  [93] strength_levels = (100, 120, 90, 95, 110, 120)

这里是你的挑战:

  1. 使用超级英雄列表填充一个新的Series对象。

  2. 使用力量值的元组填充一个新的Series对象。

  3. 创建一个以超级英雄为索引标签,力量等级为值的Series。将Series赋值给heroes变量。

  4. 提取heroes Series的前两行。

  5. 提取heroes Series的最后四行。

  6. 确定你的heroes Series中唯一值的数量。

  7. 计算heroes中超级英雄的平均力量。

  8. 计算heroes中的最大力量和最小力量。

  9. 计算如果力量等级翻倍,每个超级英雄的力量等级会是多少。

  10. heroes Series转换为 Python 字典。

2.7.2 解决方案

让我们探索 2.7.1 节中问题的解决方案:

  1. 要创建一个新的Series对象,我们可以在 pandas 库的顶层使用Series构造函数。将数据源作为第一个位置参数传入:

    In  [94] pd.Series(superheroes)
    
    Out [94] 0             Batman
             1           Superman
             2         Spider-Man
             3           Iron Man
             4    Captain America
             5       Wonder Woman
             dtype: object
    
  2. 这个问题的解决方案与上一个相同;我们只需要将我们的力量值元组传递给Series构造函数。这次,让我们明确写出data关键字参数:

    In  [95] pd.Series(data = strength_levels)
    
    Out [95] 0    100
             1    120
             2     90
             3     95
             4    110
             5    120
             dtype: int64
    
    
  3. 要创建具有自定义索引的Series,我们可以将index参数传递给构造函数。在这里,我们将力量等级设置为Series的值,将超级英雄名称设置为索引标签:

    In  [96] heroes = pd.Series(
                 data = strength_levels, index = superheroes
             )
    
             heroes
    
    Out [96] Batman             100
             Superman           120
             Spider-Man          90
             Iron Man            95
             Captain America    110
             Wonder Woman       120
             dtype: int64
    
    
  4. 作为提醒,方法是我们可以向对象发出的动作或命令。我们可以使用head方法从 pandas 数据结构的顶部提取行。方法的唯一参数n设置了要提取的行数。head方法返回一个新的Series

    In  [97] heroes.head(2)
    
    Out [97] Batman      100
             Superman    120
             dtype: int64
    
    
  5. 补充的tail方法从 pandas 数据结构的末尾提取行。为了定位最后四行,我们将传递一个参数4

    In  [98] heroes.tail(4)
    
    Out [98] Spider-Man          90
             Iron Man            95
             Captain America    110
             Wonder Woman       120
             dtype: int64
    
  6. 要识别Series中的唯一值数量,我们可以调用nunique方法。heroes Series总共有六个值,其中五个是唯一的;值120出现了两次:

    In  [99] heroes.nunique()
    
    Out [99] 5
    
    
  7. 要计算Series值的平均值,我们可以调用mean方法:

    In  [100] heroes.mean()
    
    Out [100] 105.83333333333333
    
    
  8. 下一个挑战是确定Series中的最大值和最小值。maxmin方法可以解决这个问题:

    In  [101] heroes.max()
    
    Out [101] 120
    
    In  [102] heroes.min()
    
    Out [102] 90
    
    
  9. 我们如何将每个超级英雄的力量等级翻倍呢?我们可以将每个Series值乘以 2。以下解决方案使用了乘法运算符,但mulmultiply方法也是合适的选择:

    In  [103] heroes * 2
    
    Out [103] Batman             200
              Superman           240
              Spider-Man         180
              Iron Man           190
              Captain America    220
              Wonder Woman       240
              dtype: int64
    
    
  10. 最后的挑战是将heroes Series转换为 Python 字典。为了解决这个问题,我们可以将数据结构传递给 Python 的dict构造函数/函数。Pandas 将索引标签设置为字典键,将Series值设置为字典值:

    In  [104] dict(heroes)
    
    Out [104] {'Batman': 100,
               'Superman': 120,
               'Spider-Man': 90,
               'Iron Man': 95,
               'Captain America': 110,
               'Wonder Woman': 120}
    
    

恭喜您完成了您的第一个编码挑战!

摘要

  • Series 是一个一维同构标签数组,用于存储值和索引。

  • Series 的值可以是任何数据类型。索引标签可以是任何不可变的数据类型。

  • Pandas 为每个 Series 值分配一个索引 位置 和一个索引 标签

  • 我们可以使用列表、字典、元组、NumPy 数组等数据源填充 Series

  • head 方法用于检索 Series 的第一行。

  • tail 方法用于检索 Series 的最后几行。

  • Series 支持常见的统计操作,如总和、平均值、中位数和标准差。

  • Pandas 使用共享索引标签在多个 Series 上应用算术运算。

  • Series 与 Python 的内置函数(包括 dictlistlen)友好地协同工作。


¹ 请参阅mng.bz/7j6v以了解为什么 pandas 将字符串的 dtype 列为“object”的讨论。

² 请参阅“使用集合构造序列返回集合而不是序列”,github.com/pandas-dev/pandas/issues/1913

3 个 Series 方法

本章涵盖

  • 使用 read_csv 函数导入 CSV 数据集

  • 按升序和降序排序 Series

  • Series 中检索最大和最小值

  • Series 中计数唯一值的出现次数

  • Series 的每个值上调用函数

在第二章中,我们开始探索 Series 对象,这是一个一维的、带有同质值的标签数组。我们从不同的来源填充了我们的 Series,包括列表、字典和 NumPy ndarrays。我们观察了 pandas 如何为每个 Series 值分配一个索引标签和一个索引位置。我们学习了如何对 Series 应用数学运算。

在掌握基础知识后,我们准备探索一些真实世界的数据集!在本章中,我们将介绍许多高级 Series 操作,包括排序、计数和分桶。我们还将开始看到这些方法如何帮助我们从数据中得出见解。让我们深入探讨。

3.1 使用 read_csv 函数导入数据集

CSV 是一个纯文本文件,它使用换行符分隔每行数据,使用逗号分隔每行值。文件的第一行包含数据的列标题。本章为我们提供了三个 CSV 文件来操作:

  • pokemon.csv—一个包含超过 800 种宝可梦的列表,这些宝可梦是任天堂流行媒体系列的卡通怪物。每种宝可梦都关联一个或多个类型,例如火、水、草。

  • google_stock.csv—从 2004 年 8 月上市到 2019 年 10 月的谷歌科技公司每日美元股价集合。

  • revolutionary_war.csv—美国独立战争期间战斗的记录。每次小冲突都与一个开始日期和一个美国州相关联。

让我们先导入数据集。在继续的过程中,我们将讨论我们可以进行的优化,以简化分析。

我们的第一步是启动一个新的 Jupyter Notebook 并导入 pandas 库。确保在 CSV 文件所在的目录中创建笔记本:

In [1] import pandas as pd

Pandas 有十几个导入函数来加载各种文件格式。这些函数在库的最高级别可用,并以前缀 read. 开头。在我们的情况下,要导入 CSV,我们想要 read_csv 函数。该函数的第一个参数 filepath_or_buffer 期望一个包含文件名的字符串。确保该字符串包含 .csv 扩展名(例如 "pokemon.csv",而不是 "pokemon")。默认情况下,pandas 在笔记本所在的目录中查找文件:

In  [2] # The two lines below are equivalent
        pd.read_csv(filepath_or_buffer = "pokemon.csv")
        pd.read_csv("pokemon.csv")

Out [2]

 **Pokemon                Type**
0       Bulbasaur      Grass / Poison
1         Ivysaur      Grass / Poison
2        Venusaur      Grass / Poison
3      Charmander                Fire
4      Charmeleon                Fire
...           ...                 ...
804     Stakataka        Rock / Steel
805   Blacephalon        Fire / Ghost
806       Zeraora            Electric
807        Meltan               Steel
808      Melmetal               Steel

809 rows × 2 columns

无论数据集中有多少列,read_csv 函数总是将数据导入一个 DataFrame,这是一个支持多行多列的二维 pandas 数据结构。我们将在第四章中详细介绍这个对象。使用 DataFrame 没有问题,但我们想更多地练习 Series,所以让我们将 CSV 的数据存储在较小的数据结构中。

我们遇到的第一问题是数据集有两个列(宝可梦和类型),但 Series 只支持一列数据。一个简单的解决方案是将数据集的一个列设置为 Series 索引。我们可以使用 index_col 参数来设置索引列。请注意大小写敏感性:字符串必须与数据集中的标题匹配。让我们将 "Pokemon" 作为 index_col 的参数传递:

In  [3] pd.read_csv("pokemon.csv", index_col = "Pokemon")

Out [3]

                       Type
**Pokemon** 
Bulbasaur    Grass / Poison
Ivysaur      Grass / Poison
Venusaur     Grass / Poison
Charmander             Fire
Charmeleon             Fire
     ...                ...
Stakataka      Rock / Steel
Blacephalon    Fire / Ghost
Zeraora            Electric
Meltan                Steel
Melmetal              Steel

809 rows × 1 columns

我们已经成功将 Pokemon 列设置为 Series 索引,但 pandas 仍然默认将数据导入到 DataFrame 中。毕竟,一个能够容纳多列数据的容器在技术上也可以容纳一列数据。要强制 pandas 使用 Series,我们需要添加另一个名为 squeeze 的参数,并传递一个值为 True 的参数。squeeze 参数将一列 DataFrame 强制转换为 Series

In  [4] pd.read_csv("pokemon.csv", index_col = "Pokemon", squeeze = True)

Out [4] Pokemon
        Bulbasaur      Grass / Poison
        Ivysaur        Grass / Poison
        Venusaur       Grass / Poison
        Charmander               Fire
        Charmeleon               Fire
                            ...
        Stakataka        Rock / Steel
        Blacephalon      Fire / Ghost
        Zeraora              Electric
        Meltan                  Steel
        Melmetal                Steel
        Name: Type, Length: 809, dtype: object

我们正式拥有了一个 Series。太好了!索引标签是宝可梦的名字,值是宝可梦的类型。

值下面的输出揭示了某些重要细节:

  • Pandas 将 Series 命名为 Type,这是 CSV 文件中的列名。

  • Series 有 809 个值。

  • dtype: object 告诉我们这是一个字符串值的 Seriesobject 是 pandas 对字符串和更复杂数据结构的内部术语。

最后一步是将 Series 赋值给一个变量。在这里 pokemon 感觉很合适:

In  [5] pokemon = pd.read_csv(
            "pokemon.csv", index_col = "Pokemon", squeeze = True
        )

剩下的两个数据集有一些额外的复杂性。让我们看一下 google_stock.csv:

In  [6] pd.read_csv("google_stocks.csv").head()

Out [6]

 **Date  Close**
0  2004-08-19  49.98
1  2004-08-20  53.95
2  2004-08-23  54.50
3  2004-08-24  52.24
4  2004-08-25  52.80

当导入数据集时,pandas 会推断每个列最适合的数据类型。有时,库会采取保守的做法,避免对我们的数据进行假设。例如,google_stocks.csv 包含一个日期列,其值为 YYYY-MM-DD 格式的日期时间(例如 2010-08-04)。除非我们告诉 pandas 将这些值视为日期时间,否则库默认将它们导入为字符串。字符串是一种更通用和灵活的数据类型;它可以表示任何值。

让我们明确告诉 pandas 将日期列中的值转换为日期时间。虽然我们不会在第十一章介绍日期时间,但将每列数据存储在最准确的数据类型中被认为是最佳实践。当 pandas 知道它有日期时间时,它将启用在普通字符串上不可用的额外方法,例如计算日期的星期几。

read_csv 函数的 parse_dates 参数接受一个字符串列表,表示 pandas 应将其文本值转换为日期时间的列。下一个示例传递了一个包含 "Date" 的列表:

In  [7] pd.read_csv("google_stocks.csv", parse_dates = ["Date"]).head()

Out [7]

 **Date  Close**
0  2004-08-19  49.98
1  2004-08-20  53.95
2  2004-08-23  54.50
3  2004-08-24  52.24
4  2004-08-25  52.80

输出中没有视觉差异,但 pandas 在底层为日期列存储了不同的数据类型。让我们使用 index_col 参数将日期列设置为 Series 索引;Series 与日期索引配合得很好。最后,让我们添加 squeeze 参数来强制使用 Series 对象而不是 DataFrame

In  [8] pd.read_csv(
            "google_stocks.csv",
            parse_dates = ["Date"],
            index_col = "Date",
            squeeze = True
        ).head()

Out [8] Date
        2004-08-19    49.98
        2004-08-20    53.95
        2004-08-23    54.50
        2004-08-24    52.24
        2004-08-25    52.80
        Name: Close, dtype: float64

看起来不错。我们有一个由日期时间索引标签和浮点值组成的Series。让我们将这个Series保存到google变量中:

In  [9] google = pd.read_csv(
            "google_stocks.csv",
            parse_dates = ["Date"],
            index_col = "Date",
            squeeze = True
        )

我们还有一个数据集要导入:美国独立战争战役。这次,让我们在导入时预览最后五行。我们将tail方法链接到read_csv函数返回的DataFrame

In  [10] pd.read_csv("revolutionary_war.csv").tail()

Out [10]

 **Battle  Start Date     State**
227         Siege of Fort Henry   9/11/1782  Virginia
228  Grand Assault on Gibraltar   9/13/1782       NaN
229   Action of 18 October 1782  10/18/1782       NaN
230   Action of 6 December 1782   12/6/1782       NaN
231   Action of 22 January 1783   1/22/1783  Virginia

看一下“州”列。哎呀——这个数据集有一些缺失值。提醒一下,pandas 使用NaN(不是一个数字)来标记缺失值。NaN是一个 NumPy 对象,用于表示无或值的缺失。这个数据集包含没有确定开始日期的战役或在美国领土外作战的战役的缺失/不存在值。

让我们将“开始日期”列设置为索引。我们再次使用index_col参数来设置索引,并使用parse_dates参数将开始日期字符串转换为日期时间值。Pandas 可以识别这个数据集的日期格式(M/D/YYYY):

In  [11] pd.read_csv(
             "revolutionary_war.csv",
             index_col = "Start Date",
             parse_dates = ["Start Date"],
         ).tail()

Out [11]

                                Battle     State
**Start Date** 
1782-09-11         Siege of Fort Henry  Virginia
1782-09-13  Grand Assault on Gibraltar       NaN
1782-10-18   Action of 18 October 1782       NaN
1782-12-06   Action of 6 December 1782       NaN
1783-01-22   Action of 22 January 1783  Virginia

默认情况下,read_csv函数从 CSV 文件中导入所有列。如果我们想得到一个Series,我们必须限制导入到两列:一列作为索引,另一列作为值。在这种情况下,squeeze参数本身是不够的;如果有多于一列的数据,pandas 将忽略该参数。

read_csv函数的usecols参数接受一个列列表,pandas 应该导入这些列。让我们只包括开始日期和州:

In  [12] pd.read_csv(
             "revolutionary_war.csv",
             index_col = "Start Date",
             parse_dates = ["Start Date"],
             usecols = ["State", "Start Date"],
             squeeze = True
         ).tail()

Out [12] Start Date
         1782-09-11    Virginia
         1782-09-13         NaN
         1782-10-18         NaN
         1782-12-06         NaN
         1783-01-22    Virginia
         Name: State, dtype: object

完美!我们有一个由日期时间索引和字符串值组成的Series。让我们将其分配给一个battles变量:

In  [13] battles = pd.read_csv(
             "revolutionary_war.csv",
             index_col = "Start Date",
             parse_dates = ["Start Date"],
             usecols = ["State", "Start Date"],
             squeeze = True
         )

现在我们已经将数据集导入到Series对象中,让我们看看我们可以用它们做什么。

3.2 对 Series 进行排序

我们可以按值或索引对Series进行排序,按升序或降序排序。

3.2.1 使用sort_values方法按值排序

假设我们想知道谷歌公司最低和最高的股价。sort_values方法返回一个新Series,其中的值按升序排序。升序意味着大小增加——换句话说,从小到大。索引标签与其值对应移动:

In  [14] google.sort_values()

Out [14] Date
         2004-09-03      49.82
         2004-09-01      49.94
         2004-08-19      49.98
         2004-09-02      50.57
         2004-09-07      50.60
                        ...
         2019-04-23    1264.55
         2019-10-25    1265.13
         2018-07-26    1268.33
         2019-04-26    1272.18
         2019-04-29    1287.58
         Name: Close, Length: 3824, dtype: float64

Pandas 按字母顺序对字符串Series进行排序。升序意味着从字母表的开始到结束:

In  [15] pokemon.sort_values()

Out [15] Pokemon
         Illumise                Bug
         Silcoon                 Bug
         Pinsir                  Bug
         Burmy                   Bug
         Wurmple                 Bug
                           ...
         Tirtouga       Water / Rock
         Relicanth      Water / Rock
         Corsola        Water / Rock
         Carracosta     Water / Rock
         Empoleon      Water / Steel
         Name: Type, Length: 809, dtype: object

Pandas 在排序时将大写字母排在小写字母之前。因此,大写字母"Z"在小写字母"a"之前。在下一个例子中,请注意字符串"adam"出现在"Ben"之后:

In  [16] pd.Series(data = ["Adam", "adam", "Ben"]).sort_values()

Out [16] 0    Adam
         2     Ben
         1    adam
         dtype: object

ascending参数设置排序顺序,默认参数为True。要按降序(从大到小)排序Series值,将参数传递一个False的参数:

In  [17] google.sort_values(ascending = False).head()

Out [17] Date
         2019-04-29    1287.58
         2019-04-26    1272.18
         2018-07-26    1268.33
         2019-10-25    1265.13
         2019-04-23    1264.55
         Name: Close, dtype: float64

降序排序将字符串Series按逆字母顺序排列。降序意味着从字母表的末尾到开头:

In  [18] pokemon.sort_values(ascending = False).head()

Out [18] Pokemon
         Empoleon      Water / Steel
         Carracosta     Water / Rock
         Corsola        Water / Rock
         Relicanth      Water / Rock
         Tirtouga       Water / Rock
         Name: Type, dtype: object

na_position 参数配置返回的 SeriesNaN 值的位置,其默认参数为 "last"。默认情况下,pandas 将缺失值放置在排序后的 Series 的末尾:

In  [19] # The two lines below are equivalent
         battles.sort_values()
         battles.sort_values(na_position = "last")

Out [19] Start Date
         1781-09-06    Connecticut
         1779-07-05    Connecticut
         1777-04-27    Connecticut
         1777-09-03       Delaware
         1777-05-17        Florida
                          ...
         1782-08-08            NaN
         1782-08-25            NaN
         1782-09-13            NaN
         1782-10-18            NaN
         1782-12-06            NaN
         Name: State, Length: 232, dtype: object

要首先显示缺失值,可以将 na_position 参数的值设置为 "first"。结果 Series 首先显示所有 NaN,然后是排序后的值:

In  [20] battles.sort_values(na_position = "first")

Out [20] Start Date
         1775-09-17         NaN
         1775-12-31         NaN
         1776-03-03         NaN
         1776-03-25         NaN
         1776-05-18         NaN
                         ...
         1781-07-06    Virginia
         1781-07-01    Virginia
         1781-06-26    Virginia
         1781-04-25    Virginia
         1783-01-22    Virginia
         Name: State, Length: 232, dtype: object

如果我们想移除 NaN 值呢?dropna 方法返回一个没有缺失值的 Series。注意,该方法仅针对 Series 中的 NaN 值,而不是索引。下一个示例过滤出具有当前位置的战斗:

In  [21] battles.dropna().sort_values()

Out [21] Start Date
         1781-09-06    Connecticut
         1779-07-05    Connecticut
         1777-04-27    Connecticut
         1777-09-03       Delaware
         1777-05-17        Florida
                             ...
         1782-08-19       Virginia
         1781-03-16       Virginia
         1781-04-25       Virginia
         1778-09-07       Virginia
         1783-01-22       Virginia
         Name: State, Length: 162, dtype: object

之前的 Series 比预期的 battles 要短。Pandas 从 battles 中移除了 70 个 NaN 值。

3.2.2 使用 sort_index 方法按索引排序

有时候,我们的关注点可能在于索引而不是值。幸运的是,我们可以使用 sort_index 方法按索引对 Series 进行排序。使用此选项,值会与其索引对应项一起移动。与 sort_values 类似,sort_index 也接受一个 ascending 参数,其默认参数也是 True

In  [22] # The two lines below are equivalent
         pokemon.sort_index()
         pokemon.sort_index(ascending = True)

Out [22] Pokemon
         Abomasnow        Grass / Ice
         Abra                 Psychic
         Absol                   Dark
         Accelgor                 Bug
         Aegislash      Steel / Ghost
                           ...
         Zoroark                 Dark
         Zorua                   Dark
         Zubat        Poison / Flying
         Zweilous       Dark / Dragon
         Zygarde      Dragon / Ground
         Name: Type, Length: 809, dtype: object

当按升序对日期时间集合进行排序时,pandas 从最早的日期排序到最新的日期。battles Series 提供了一个很好的机会来观察这个排序的实际应用:

In  [23] battles.sort_index()

Out [23] Start Date
         1774-09-01    Massachusetts
         1774-12-14    New Hampshire
         1775-04-19    Massachusetts
         1775-04-19    Massachusetts
         1775-04-20         Virginia
                           ...
         1783-01-22         Virginia
         NaT              New Jersey
         NaT                Virginia
         NaT                     NaN
         NaT                     NaN
         Name: State, Length: 232, dtype: object

在排序后的 Series 的末尾,我们看到了一种新的值类型。Pandas 使用另一个 NumPy 对象 NaT(代表 not a time)来代替缺失的日期值。NaT 对象与索引的日期时间类型保持数据完整性。

sort_index 方法还包括 na_position 参数,用于改变 NaN 值的位置。下一个示例首先显示缺失值,然后是排序后的日期时间:

In  [24] battles.sort_index(na_position = "first").head()

Out [24] Start Date
         NaT              New Jersey
         NaT                Virginia
         NaT                     NaN
         NaT                     NaN
         1774-09-01    Massachusetts
         Name: State, dtype: object

要按降序排序,我们可以将 ascending 参数的值设置为 False。降序排序显示从最新到最早的日期:

In  [25] battles.sort_index(ascending = False).head()

Out [25] Start Date
         1783-01-22    Virginia
         1782-12-06         NaN
         1782-10-18         NaN
         1782-09-13         NaN
         1782-09-11    Virginia
         Name: State, dtype: object

数据集最早的战斗发生在 1783 年 1 月 22 日,在弗吉尼亚州。

3.2.3 使用 nsmallest 和 nlargest 方法检索最小和最大值

假设我们想找到谷歌股票表现最佳的五个日期。一个选项是将 Series 按降序排序,然后限制结果为前五行:

In  [26] google.sort_values(ascending = False).head()

Out [26] Date
         2019-04-29    1287.58
         2019-04-26    1272.18
         2018-07-26    1268.33
         2019-10-25    1265.13
         2019-04-23    1264.55
         Name: Close, dtype: float64

这个操作相当常见,因此 pandas 提供了一个辅助方法来节省我们一些字符。nlargest 方法返回 Series 中的最大值。它的第一个参数 n 设置要返回的记录数。n 参数的默认参数为 5。Pandas 在返回的 Series 中按降序排序值:

In  [27] # The two lines below are equivalent
         google.nlargest(n = 5)
         google.nlargest()

Out [27] Date
         2019-04-29    1287.58
         2019-04-26    1272.18
         2018-07-26    1268.33
         2019-10-25    1265.13
         2019-04-23    1264.55
         Name: Close, dtype: float64

补充的 nsmallest 方法返回 Series 中的最小值,并按升序排序。它的 n 参数也有一个默认参数 5

In  [28] # The two lines below are equivalent
         google.nsmallest(n = 5)
         google.nsmallest(5)

Out [28] Date
         2004-09-03    49.82
         2004-09-01    49.94
         2004-08-19    49.98
         2004-09-02    50.57
         2004-09-07    50.60
         2004-08-30    50.81
         Name: Close, dtype: float64

注意,这两个方法都不适用于字符串 Series

3.3 使用 inplace 参数覆盖 Series

我们在本章中调用的所有方法都会返回新的 Series 对象。我们用 pokemongooglebattles 变量引用的原始 Series 对象在我们的操作过程中始终保持未受影响。作为一个例子,让我们观察方法调用前后的 battlesSeries 并没有改变:

In  [29] battles.head(3)

Out [29] Start Date
         1774-09-01    Massachusetts
         1774-12-14    New Hampshire
         1775-04-19    Massachusetts
         Name: State, dtype: object

In  [30] battles.sort_values().head(3)

Out [30] Start Date
         1781-09-06    Connecticut
         1779-07-05    Connecticut
         1777-04-27    Connecticut
         Name: State, dtype: object

In  [31] battles.head(3)

Out [31] Start Date
         1774-09-01    Massachusetts
         1774-12-14    New Hampshire
         1775-04-19    Massachusetts
         Name: State, dtype: object

如果我们想修改 battles Series 呢?pandas 中的许多方法包括一个 inplace 参数,当传递 True 作为参数时,它似乎会修改被调用的对象。

将上一个例子与下一个例子进行比较。在这里,我们再次调用 sort_values 方法,但这次我们传递 True 作为 inplace 参数的参数。如果我们使用 inplace,该方法将返回 None,导致 Jupyter Notebook 中没有输出。当我们输出 battles 时,我们可以看到它已经改变了:

In  [32] battles.head(3)

Out [32] Start Date
         1774-09-01    Massachusetts
         1774-12-14    New Hampshire
         1775-04-19    Massachusetts
         Name: State, dtype: object
In  [33] battles.sort_values(inplace = True)

In  [34] battles.head(3)

Out [34] Start Date
         1781-09-06    Connecticut
         1779-07-05    Connecticut
         1777-04-27    Connecticut
         Name: State, dtype: object

inplace 参数是一个常见的混淆点。它的名字暗示它修改或突变现有对象,而不是创建一个副本。开发者可能会被 inplace 诱惑,因为减少我们创建的副本数量可以减少内存使用。但即使有 inplace 参数,pandas 在我们调用方法时总是会创建一个对象的副本。库始终创建一个副本;inplace 参数将我们的现有变量重新分配给新对象。因此,与普遍看法相反,inplace 参数并不提供任何性能优势。这两行在技术上等效:

battles.sort_values(inplace = True)
battles = battles.sort_values()

为什么 pandas 开发者选择了这种实现?我们总是创建副本我们能获得什么优势?你可以在网上找到更详细的解释,但简短的答案是不可变数据结构往往导致更少的错误。记住,不可变对象无法改变。我们可以复制一个不可变对象并操作副本,但我们不能改变原始对象。Python 字符串就是一个例子。不可变对象不太可能进入损坏或无效的状态;它也更容易测试。

pandas 开发团队已经讨论过在未来的版本中从库中移除 inplace 参数。我的建议是如果可能的话避免使用它。替代方案是将方法的返回值重新分配给相同的变量或创建一个更具有描述性的变量。例如,我们可以将 sort_values 方法的返回值分配给一个如 sorted_battles 的变量。

3.4 使用 value_counts 方法计数值

这里是一个关于 pokemon Series 的提醒:

In  [35] pokemon.head()

Out [35] Pokemon
         Bulbasaur     Grass / Poison
         Ivysaur       Grass / Poison
         Venusaur      Grass / Poison
         Charmander              Fire
         Charmeleon              Fire
         Name: Type, dtype: object

我们如何找出最常见的宝可梦类型?我们需要将值分组到桶中,并计算每个桶中元素的数量。value_counts 方法,它计算每个 Series 值出现的次数,完美地解决了这个问题:

In  [36] pokemon.value_counts()

Out [36] Normal            65
         Water             61
         Grass             38
         Psychic           35
         Fire              30
                   ..
         Fire / Dragon      1
         Dark / Ghost       1
         Steel / Ground     1
         Fire / Psychic     1
         Dragon / Ice       1
         Name: Type, Length: 159, dtype: int64

value_counts方法返回一个新的Series对象。索引标签是pokemon Series的值,值是它们各自的计数。有 65 只宝可梦被归类为正常,61 只被归类为水,等等。对于那些好奇的人,“正常”宝可梦是那些在物理攻击方面表现出色的宝可梦。

value_counts Series的长度等于pokemon Series中唯一值的数量。作为提醒,nunique方法返回此信息:

In  [37] len(pokemon.value_counts())

Out [37] 159

In  [38] pokemon.nunique()

Out [38] 159

在这种情况下,数据完整性至关重要。额外的空格或字符的不同大小写会导致 pandas 认为两个值不相等,并将它们分别计数。我们将在第六章中讨论数据清理。

value_counts方法的ascending参数的默认参数为False。Pandas 按降序对值进行排序,从出现次数最多到最少。要按升序排序值,将ascending参数传递一个值为True

In  [39] pokemon.value_counts(ascending = True)

Out [39] Rock / Poison        1
         Ghost / Dark         1
         Ghost / Dragon       1
         Fighting / Steel     1
         Rock / Fighting      1
                             ..
         Fire                30
         Psychic             35
         Grass               38
         Water               61
         Normal              65

我们可能对宝可梦类型相对于所有类型的比率更感兴趣。将value_counts方法的normalize参数设置为True以返回每个唯一值的频率。一个值的频率是该值在数据集中所占的比例:

In  [40] pokemon.value_counts(normalize = True).head()

Out [40] Normal            0.080346
         Water             0.075402
         Grass             0.046972
         Psychic           0.043263
         Fire              0.037083

我们可以将频率Series中的值乘以 100,以得到每种宝可梦类型对整体贡献的百分比。你还记得第二章中的语法吗?我们可以使用像乘号这样的普通数学运算符与Series一起使用。Pandas 将对每个值应用此操作:

In  [41] pokemon.value_counts(normalize = True).head() * 100

Out [41] Normal            8.034611
         Water             7.540173
         Grass             4.697157
         Psychic           4.326329
         Fire              3.708282

正常宝可梦占数据集的 8.034611%,水宝可梦占 7.540173%,等等。真有趣!

假设我们想要限制百分比的精度。我们可以使用round方法对Series的值进行四舍五入。该方法的第一参数decimals设置小数点后保留的位数。下一个示例将值四舍五入到两位数字;它将前一个示例中的代码用括号括起来以避免语法错误。我们想要确保 pandas 首先将每个值乘以 100,然后对结果Series调用round方法:

In  [42] (pokemon.value_counts(normalize = True) * 100).round(2)

Out [42] Normal              8.03
         Water               7.54
         Grass               4.70
         Psychic             4.33
         Fire                3.71
                             ...
         Rock / Fighting     0.12
         Fighting / Steel    0.12
         Ghost / Dragon      0.12
         Ghost / Dark        0.12
         Rock / Poison       0.12
         Name: Type, Length: 159, dtype: float64

value_counts方法在数值Series上操作相同。下一个示例计算google Series中每个唯一股票价格的出现次数。结果发现,数据集中没有股票价格出现超过三次:

In  [43] google.value_counts().head()

Out [43] 237.04     3
         288.92     3
         287.68     3
         290.41     3
         194.27     3

为了识别数值数据集中的趋势,将值分组到预定义的区间中可能比计数唯一值更有益。让我们首先确定google Series中最小值和最大值之间的差异。Seriesmaxmin方法在这里工作得很好。另一个选择是将Series传递给 Python 内置的maxmin函数:

In  [44] google.max()

Out [44] 1287.58

In  [45] google.min()

Out [45] 49.82

最小值和最大值之间有大约 1,250 的范围。让我们将股价分成 200 的桶,从 0 开始,一直工作到 1,400。我们可以将这些区间定义为列表中的值,并将列表传递给value_counts方法的bins参数。Pandas 将使用列表中每两个后续值作为区间的下限和上限:

In  [46] buckets = [0, 200, 400, 600, 800, 1000, 1200, 1400]
         google.value_counts(bins = buckets)

Out [46] (200.0, 400.0]      1568
         (-0.001, 200.0]      595
         (400.0, 600.0]       575
         (1000.0, 1200.0]     406
         (600.0, 800.0]       380
         (800.0, 1000.0]      207
         (1200.0, 1400.0]      93
         Name: Close, dtype: int64

输出告诉我们,在数据集中,谷歌的股价在$200 到$400 之间有 1,568 个值。

注意,pandas 按每个桶中的值的数量降序对之前的Series进行了排序。如果我们想按区间排序结果呢?我们只需要混合匹配几个 pandas 方法。这些区间是返回的Series中的索引标签,因此我们可以使用sort_index方法来排序它们。这种连续调用多个方法的技巧称为方法链

In  [47] google.value_counts(bins = buckets).sort_index()

Out [47] (-0.001, 200.0]      595
         (200.0, 400.0]      1568
         (400.0, 600.0]       575
         (600.0, 800.0]       380
         (800.0, 1000.0]      207
         (1000.0, 1200.0]     406
         (1200.0, 1400.0]      93
         Name: Close, dtype: int64

我们可以通过将False传递给value_counts方法的sort参数来获得相同的结果:

In  [48] google.value_counts(bins = buckets, sort = False)

Out [48] (-0.001, 200.0]      595
         (200.0, 400.0]      1568
         (400.0, 600.0]       575
         (600.0, 800.0]       380
         (800.0, 1000.0]      207
         (1000.0, 1200.0]     406
         (1200.0, 1400.0]      93
         Name: Close, dtype: int64

注意,第一个区间包含-0.001 而不是 0。当 pandas 将Series的值组织到桶中时,它可能将任何桶的范围扩展到.1%的任一方向。区间周围的符号具有意义:

  • 一个括号标记一个值作为区间外的排除值。

  • 一个方括号标记一个值作为区间内的包含值。

考虑区间 (-0.001, 200.0]。-0.001 被排除,200 被包含。因此,该区间捕获了所有大于-0.001 且小于或等于 200.0 的值。

一个闭区间包含两个端点。例如 [5, 10](大于等于 5,小于等于 10)。

一个开区间不包含两个端点。例如 (5, 10)(大于 5,小于 10)。

带有bin参数的value_counts方法返回半开区间。Pandas 将包含一个端点并排除另一个端点。

value_counts方法的bins参数还接受一个整数参数。Pandas 将自动计算Series中最大值和最小值之间的差值,并将范围分成指定的数量个桶。下一个示例将google中的股价分成六个桶。请注意,桶的大小可能不完全相等(由于任何方向上任何区间的可能.1%扩展),但将非常接近:

In  [49] google.value_counts(bins = 6, sort = False)

Out [49] (48.581, 256.113]      1204
         (256.113, 462.407]     1104
         (462.407, 668.7]        507
         (668.7, 874.993]        380
         (874.993, 1081.287]     292
         (1081.287, 1287.58]     337
         Name: Close, dtype: int64

我们的battles数据集怎么样了?我们有一段时间没看到它了:

In  [50] battles.head()

Out [50] Start Date
         1781-09-06    Connecticut
         1779-07-05    Connecticut
         1777-04-27    Connecticut
         1777-09-03       Delaware
         1777-05-17        Florida
         Name: State, dtype: object

我们可以使用value_counts方法查看在独立战争中哪个州发生了最多的战斗:

In  [51] battles.value_counts().head()

Out [51] South Carolina    31
         New York          28
         New Jersey        24
         Virginia          21
         Massachusetts     11
         Name: State, dtype: int64

Pandas 默认会从value_counts Series中排除NaN值。将dropna参数的值设置为False以将空值计为一个不同的类别:

In  [52] battles.value_counts(dropna = False).head()

Out [52] NaN               70
         South Carolina    31
         New York          28
         New Jersey        24
         Virginia          21
         Name: State, dtype: int64

Series索引也支持value_counts方法。在调用方法之前,我们必须通过index属性访问索引对象。让我们找出在独立战争中哪一天发生了最多的战斗:

In  [53] battles.index

Out [53]

DatetimeIndex(['1774-09-01', '1774-12-14', '1775-04-19', '1775-04-19',
               '1775-04-20', '1775-05-10', '1775-05-27', '1775-06-11',
               '1775-06-17', '1775-08-08',
               ...
               '1782-08-08', '1782-08-15', '1782-08-19', '1782-08-26',
               '1782-08-25', '1782-09-11', '1782-09-13', '1782-10-18',
               '1782-12-06', '1783-01-22'],
              dtype='datetime64[ns]', name='Start Date', length=232,
              freq=None)

In  [54] battles.index.value_counts()

Out [54] 1775-04-19    2
         1781-05-22    2
         1781-04-15    2
         1782-01-11    2
         1780-05-25    2
                      ..
         1778-05-20    1
         1776-06-28    1
         1777-09-19    1
         1778-08-29    1
         1777-05-17    1
         Name: Start Date, Length: 217, dtype: int64

看起来没有哪一天发生了超过两场战斗。

3.5 使用 apply 方法对每个 Series 值调用函数

在 Python 中,函数是一个 一等对象,这意味着语言将其视为任何其他数据类型。函数可能感觉像是一个更抽象的实体,但它与其他任何数据结构一样有效。

关于一等对象的简单思考方式是:你可以用数字做的任何事情,你都可以用函数做。例如,你可以做所有以下事情:

  • 将函数存储在列表中。

  • 将函数分配为字典键的值。

  • 将一个函数作为参数传递给另一个函数。

  • 从另一个函数中返回一个函数。

区分函数和函数调用非常重要。一个 函数 是一系列产生输出的指令;它是一个“食谱”,尚未烹饪。相比之下,一个 函数调用 是指令的实际执行;它是食谱的烹饪。

下一个示例声明了一个 funcs 列表,该列表存储了三个 Python 内置函数。lenmaxmin 函数在列表中没有被调用。列表存储了函数本身的引用:

In  [55] funcs = [len, max, min]

下一个示例使用 for 循环遍历 funcs 列表。在三次迭代中,current_func 迭代变量代表未调用的 lenmaxmin 函数。在每次迭代中,循环调用动态的 current_func 函数,传入 google Series 并打印返回值:

In  [56] for current_func in funcs:
             print(current_func(google))

Out [56] 3824
         1287.58
         49.82

输出包括三个函数的连续返回值:Series 的长度、Series 中的最大值和最小值。

这里的关键点是我们可以像对待 Python 中的任何其他对象一样对待函数。那么这个事实如何应用到 pandas 中呢?假设我们想要将 google Series 中的每个浮点值向上或向下舍入到最接近的整数。Python 有一个方便的 round 函数来完成这个任务。该函数将大于 0.5 的值向上舍入,将小于 0.5 的值向下舍入:

In  [57] round(99.2)

Out [57] 99

In  [58] round(99.49)

Out [58] 99

In  [59] round(99.5)

Out [59] 100

如果我们能够将这个 round 函数应用到我们的 Series 中的每个值上,那岂不是很好?我们很幸运。Series 有一个名为 apply 的方法,它为每个 Series 值调用一次函数,并返回一个由函数调用返回值组成的新 Seriesapply 方法期望它将调用的函数作为其第一个参数 func。下一个示例传递了 Python 的内置 round 函数:

In  [60] # The two lines below are equivalent
         google.apply(func = round)
         google.apply(round)

Out [60] Date
         2004-08-19      50
         2004-08-20      54
         2004-08-23      54
         2004-08-24      52
         2004-08-25      53
                       ...
         2019-10-21    1246
         2019-10-22    1243
         2019-10-23    1259
         2019-10-24    1261
         2019-10-25    1265
         Name: Close, Length: 3824, dtype: int64

我们已经将每个 Series 的值都四舍五入过了!

再次请注意,我们正在将未调用的 round 函数传递给 apply 方法。我们传递的是食谱。在 pandas 的内部某个地方,apply 方法知道要在每个 Series 值上调用我们的函数。Pandas 抽象掉了操作的复杂性。

apply方法也接受自定义函数。定义一个函数,它接受一个参数并返回你希望 pandas 存储在聚合Series中的值。

假设我们想知道有多少宝可梦是单一类型的(例如火)以及有多少宝可梦是两种或更多类型的。我们需要将相同的逻辑,即宝可梦的分类,应用于每个Series值。函数是一个封装该逻辑的理想容器。让我们定义一个名为single_or_multi的实用函数,它接受一个宝可梦类型并确定它是一个或多个类型。如果一个宝可梦有多个类型,字符串会使用斜杠("Fire / Ghost")分隔它们。我们可以使用 Python 的in运算符来检查参数字符串中是否存在前斜杠。if语句仅在条件评估为True时执行一个块。在我们的情况下,如果存在/,则函数将返回字符串"Multi";否则,它将返回"Single"

In  [61] def single_or_multi(pokemon_type):
             if "/" in pokemon_type:
                 return "Multi"

             return "Single"

现在我们可以将single_or_multi函数传递给apply方法。以下是对pokemon外观的快速回顾:

In  [62] pokemon.head(4)

Out [62] Pokemon
         Bulbasaur     Grass / Poison
         Ivysaur       Grass / Poison
         Venusaur      Grass / Poison
         Charmander              Fire
         Name: Type, dtype: object

下一个示例调用apply方法,将single_or_multi函数作为其参数。Pandas 会对每个Series值调用single_or_multi函数:

In  [63] pokemon.apply(single_or_multi)

Out [63] Pokemon
         Bulbasaur       Multi
         Ivysaur         Multi
         Venusaur        Multi
         Charmander     Single
         Charmeleon     Single
                         ...
         Stakataka       Multi
         Blacephalon     Multi
         Zeraora        Single
         Meltan         Single
         Melmetal       Single
         Name: Type, Length: 809, dtype: object

我们的第一种样本,妙蛙种子,被归类为草/毒宝可梦,因此single_or_multi函数返回"Multi"。相比之下,我们的第四种样本,小火龙,被归类为火宝可梦,因此函数返回"Single"。对于剩余的pokemon值,相同的逻辑重复出现。

我们有一个新的Series对象!让我们通过调用value_counts来找出有多少宝可梦属于每个分类:

In  [64] pokemon.apply(single_or_multi).value_counts()

Out [64] Multi     405
         Single    404
         Name: Type, dtype: int64

单一能量和多能量宝可梦的分布相当均匀。我希望这些知识在某个时刻对你的生活有所帮助。

3.6 编程挑战

让我们解决一个结合了本章和第二章中介绍的一些想法的挑战。

3.6.1 问题

假设一位历史学家向我们求助,要求我们确定在革命战争中哪一天发生了最多的战斗。最终输出应该是一个Series,其中包含星期(星期日、星期一等)作为索引标签,以及每天战斗的数量作为值。从头开始,导入 revolutionary_war.csv 数据集,并执行必要的操作以获得以下数据:

Saturday     39
Friday       39
Wednesday    32
Thursday     31
Sunday       31
Tuesday      29
Monday       27

解决这个问题你需要额外的 Python 知识。如果你有一个单独的 datetime 对象,你可以使用strftime方法并传入参数"%A"来返回日期所在的星期几(例如"Sunday")。参见以下示例和附录 B,以获取关于 datetime 对象的更全面概述:

In  [65] import datetime as dt
         today = dt.datetime(2020, 12, 26)
         today.strftime("%A")

Out [65] 'Saturday'

提示:声明一个自定义函数来计算日期的星期几可能很有帮助。

祝你好运!

3.6.2 解决方案

让我们重新导入 revolutionary_war.csv 数据集,并提醒自己其原始形状:

In  [66] pd.read_csv("revolutionary_war.csv").head()

Out [66]
 **Battle  Start Date          State**
0                       Powder Alarm    9/1/1774  Massachusetts
1  Storming of Fort William and Mary  12/14/1774  New Hampshire
2   Battles of Lexington and Concord   4/19/1775  Massachusetts
3                    Siege of Boston   4/19/1775  Massachusetts
4                 Gunpowder Incident   4/20/1775       Virginia

我们不需要分析中的战斗和状态列。你可以使用任一列作为索引,或者坚持使用默认的数字索引。

关键步骤是将开始日期列中的字符串值强制转换为 datetime。如果我们处理的是日期,我们可以调用与日期相关的函数,如 strftime。对于普通字符串,我们没有同样的能力。让我们使用 usecols 参数选择开始日期列,并使用 parse_dates 参数将其值转换为 datetime。最后,记得将 True 传递给 squeeze 参数以创建一个 Series 而不是 DataFrame

In  [67] days_of_war = pd.read_csv(
             "revolutionary_war.csv",
             usecols = ["Start Date"],
             parse_dates = ["Start Date"],
             squeeze = True,
         )

         days_of_war.head()

Out [67] 0   1774-09-01
         1   1774-12-14
         2   1775-04-19
         3   1775-04-19
         4   1775-04-20
         Name: Start Date, dtype: datetime64[ns]

我们下一个挑战是提取每个日期的一周中的某一天。一个解决方案(仅使用我们目前所知的工具)是将每个 Series 值传递给一个函数,该函数将返回该日期的一周中的某一天。现在让我们声明这个函数:

In  [68] def day_of_week(date):
             return date.strftime("%A")

我们如何为每个 Series 值调用一次 day_of_week 函数?我们可以将 day_of_week 函数作为参数传递给 apply 方法。我们期望得到一周中的某一天,但结果却...

In  [69] days_of_war.apply(day_of_week)

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-411-c133befd2940> in <module>
----> 1 days_of_war.apply(day_of_week)

ValueError: NaTType does not support strftime

哎呀——我们的开始日期列有缺失值。与 datetime 对象不同,NaT 对象没有 strftime 方法,所以当将其传递给 day_of_week 函数时,pandas 会遇到麻烦。简单的解决方案是在调用 apply 方法之前从 Series 中删除所有缺失的 datetime 值。我们可以使用 dropna 方法做到这一点:

In  [70] days_of_war.dropna().apply(day_of_week)

Out [70] 0       Thursday
         1      Wednesday
         2      Wednesday
         3      Wednesday
         4       Thursday
                  ...
         227    Wednesday
         228       Friday
         229       Friday
         230       Friday
         231    Wednesday
         Name: Start Date, Length: 228, dtype: object

现在我们有进展了!我们需要一种方法来计算每个工作日的出现次数。value_counts 方法就做到了这一点:

In  [71] days_of_war.dropna().apply(day_of_week).value_counts()

Out [71] Saturday     39
         Friday       39
         Wednesday    32
         Thursday     31
         Sunday       31
         Tuesday      29
         Monday       27
         Name: Start Date, dtype: int64

完美!结果是周五和周六打平。恭喜你完成编码挑战!

摘要

  • read_csv 函数将 CSV 的内容导入 pandas 数据结构中。

  • read_csv 函数的参数可以自定义导入的列、索引、数据类型等。

  • sort_values 方法按升序或降序对 Series 的值进行排序。

  • sort_index 方法按升序或降序对 Series 的索引进行排序。

  • 我们可以使用 inplace 参数将方法返回的副本重新分配给原始变量。使用 inplace 没有性能上的好处。

  • value_counts 方法计算 Series 中每个唯一值的出现次数。

  • apply 方法在 Series 的每个值上调用一个函数,并将结果返回到一个新的 Series 中。

4 DataFrame 对象

本章涵盖

  • 从字典和 NumPy ndarrays 实例化 DataFrame 对象

  • 使用 read_csv 函数从 CSV 文件导入 DataFrame

  • 排序 DataFrame

  • 访问 DataFrame 中的行和列

  • 设置和重置 DataFrame 索引

  • DataFrame 中重命名列和索引标签

pandas 的 DataFrame 是一个二维数据表,具有行和列。与 Series 一样,pandas 为每个 DataFrame 行分配一个索引标签和索引位置。Pandas 还为每个列分配一个标签和位置。DataFrame 是二维的,因为它需要两个参考点——行和列——来从数据集中隔离一个值。图 4.1 显示了 pandas DataFrame 的视觉示例。

图 4.1 pandas DataFrame 的五行两列的视觉表示

DataFrame 是 pandas 库中的工作马,是你每天都会大量使用的数据结构,因此我们将在这本书的剩余部分探索其丰富的功能。

4.1 DataFrame 概述

如往常一样,让我们启动一个新的 Jupyter Notebook 并导入 pandas。我们还需要 NumPy 库,我们将在 4.1.2 节中使用它来生成随机数据。NumPy 通常被分配别名 np

In  [1] import pandas as pd
        import numpy as np

DataFrame 类构造函数位于 pandas 的顶层。实例化 DataFrame 对象的语法与实例化 Series 的语法相同。我们通过一对括号访问 DataFrame 类并实例化:pd.DataFrame()

4.1.1 从字典创建 DataFrame

构造函数的第一个参数 data 期望填充 DataFrame 的数据。一个合适的输入是键为列名、值为列值的 Python 字典。下一个示例传递一个键为字符串、值为列表的字典。Pandas 返回一个包含三个列的 DataFrame。每个列表元素成为其相应列的值:

In  [2] city_data = {
            "City": ["New York City", "Paris", "Barcelona", "Rome"],
            "Country": ["United States", "France", "Spain", "Italy"],
            "Population": [8600000, 2141000, 5515000, 2873000]
        }

        cities = pd.DataFrame(city_data)
        cities

Out [2]

 **City         Country   Population**
0  New York City   United States      8600000
1          Paris          France      2141000
2      Barcelona           Spain      5515000
3           Rome           Italy      2873000

我们正式拥有了一个 DataFrame!注意,数据结构与 Series 的渲染方式不同。

A DataFrame 包含行标签的索引。我们没有向构造函数提供自定义索引,因此 pandas 从 0 开始生成一个数字索引。逻辑操作与 Series 上的操作相同。

DataFrame 可以包含多个数据列。将列标题视为第二个索引是有帮助的。City、Country 和 Population 是列轴上的三个索引标签;pandas 分别将它们分配索引位置 0、1 和 2。

如果我们想交换列标题和索引标签呢?这里有两种选择。我们可以在 DataFrame 上调用 transpose 方法,或者访问其 T 属性:

In  [3] # The two lines below are equivalent
        cities.transpose()
        cities.T

Out [3]

 **0        1          2        3**
City        New York City    Paris  Barcelona     Rome
Country     United States   France      Spain    Italy
Population        8600000  2141000    5515000  2873000

之前的例子提醒我们,pandas 可以存储不同数据类型的索引标签。在上一个输出中,列使用相同的值作为索引标签和索引位置。行有不同的标签(城市、国家、人口)和位置(0、1 和 2)。

4.1.2 从 NumPy ndarray 创建 DataFrame

让我们再试一个例子。DataFrame 构造函数的 data 参数也接受 NumPy 的 ndarray。我们可以使用 NumPy 的 random 模块中的 randint 函数生成任何大小的 ndarray。下一个示例创建一个介于 1 和 101(不包括 101)之间的整数的 3 x 5 ndarray

In  [4] random_data = np.random.randint(1, 101, [3, 5])
        random_data

Out [4] array([[25, 22, 80, 43, 42],
              [40, 89,  7, 21, 25],
              [89, 71, 32, 28, 39]])

如果你想了解更多关于 NumPy 中随机数据生成的信息,请参阅附录 C。

接下来,让我们将我们的 ndarray 传递给 DataFrame 构造函数。ndarray 没有行标签也没有列标签。因此,pandas 使用数字索引作为行轴和列轴:

In  [5] pd.DataFrame(data = random_data)

Out [5]

 **0   1   2   3   4**
0  25  22  80  43  42
1  40  89   7  21  25
2  89  71  32  28  39

我们可以使用 DataFrame 构造函数的 index 参数手动设置行标签,该参数接受任何迭代对象,包括列表、元组或 ndarray。请注意,迭代器的长度必须等于数据集的行数。我们传递一个 3 x 5 的 ndarray,因此我们必须提供三个行标签:

In  [6] row_labels = ["Morning", "Afternoon", "Evening"]
        temperatures = pd.DataFrame(
            data = random_data, index = row_labels
        )
        temperatures

Out [6]

 **0   1   2   3   4**
Morning    25  22  80  43  42
Afternoon  40  89   7  21  25
Evening    89  71  32  28  39

我们可以使用构造函数的 columns 参数设置列名。ndarray 包含五个列,因此我们必须传递一个包含五个元素的迭代器。下一个示例通过元组传递列名:

In  [7] row_labels = ["Morning", "Afternoon", "Evening"]
        column_labels = (
            "Monday",
            "Tuesday",
            "Wednesday",
            "Thursday",
            "Friday",
        )

        pd.DataFrame(
            data = random_data,
            index = row_labels,
            columns = column_labels,
        )

Out [7]

 **Monday  Tuesday  Wednesday  Thursday  Friday**
Morning        25       22         80        43      42
Afternoon      40       89          7        21      25
Evening        89       71         32        28      39

Pandas 允许行和列索引有重复。在下一个示例中,"Morning" 在行索引标签中出现了两次,而 "Tuesday" 在列索引标签中出现了两次:

In  [8] row_labels = ["Morning", "Afternoon", "Morning"]
        column_labels = [
            "Monday",
            "Tuesday",
            "Wednesday",
            "Tuesday",
            "Friday"
        ]

        pd.DataFrame(
            data = random_data,
            index = row_labels,
            columns = column_labels,
        )

Out [8]

 **Monday  Tuesday  Wednesday  Tuesday  Friday**
Morning        25       22         80       43      42
Afternoon      40       89          7       21      25
Evening        89       71         32       28      39

正如我们在前面的章节中提到的,当可能时,拥有唯一的索引是理想的。如果没有重复,pandas 提取特定行或列会更容易。

4.2 Series 和 DataFrame 之间的相似性

许多 Series 属性和方法也适用于 DataFrames。它们的实现可能有所不同;pandas 必须现在考虑多列和两个独立的轴。

4.2.1 使用 read_csv 函数导入 DataFrame

nba.csv 数据集是 2019-20 赛季美国国家篮球协会(NBA)职业篮球运动员的列表。每一行包括一名球员的姓名、球队、位置、生日和薪水。数据类型的好组合散布在整个数据集中,这使得这个数据集非常适合探索 DataFrame 的基础知识。

让我们使用 pandas 的顶级 read_csv 函数导入文件(我们在第三章介绍了这个函数)。该函数接受一个文件名作为其第一个参数,默认返回一个 DataFrame。在执行以下代码之前,请确保数据集与你的 Jupyter Notebook 在同一目录下:

In  [9] pd.read_csv("nba.csv")

Out [9]

 **Name                 Team    Position  Birthday     Salary**
0      Shake Milton   Philadelphia 76ers          SG   9/26/96    1445697
1    Christian Wood      Detroit Pistons          PF   9/27/95    1645357
2     PJ Washington    Charlotte Hornets          PF   8/23/98    3831840
3      Derrick Rose      Detroit Pistons          PG   10/4/88    7317074
4     Marial Shayok   Philadelphia 76ers           G   7/26/95      79568
 ...       ...                ...                 ...    ...         ...
445   Austin Rivers      Houston Rockets          PG    8/1/92    2174310
446     Harry Giles     Sacramento Kings          PF   4/22/98    2578800
447     Robin Lopez      Milwaukee Bucks           C    4/1/88    4767000
448   Collin Sexton  Cleveland Cavaliers          PG    1/4/99    4764960
449     Ricky Rubio         Phoenix Suns          PG  10/21/90   16200000

450 rows × 5 columns

在输出的底部,pandas 通知我们数据有 450 行和 5 列。

在我们将 DataFrame 赋值给变量之前,让我们进行一个优化。Pandas 将生日列的值作为字符串而不是日期时间值导入,这限制了我们可以对这些值执行的操作数量。我们可以使用 parse_dates 参数将值强制转换为日期时间:

In  [10] pd.read_csv("nba.csv", parse_dates = ["Birthday"])

Out [10]

 **Name                 Team Position   Birthday    Salary**
0      Shake Milton   Philadelphia 76ers       SG 1996-09-26   1445697
1    Christian Wood      Detroit Pistons       PF 1995-09-27   1645357
2     PJ Washington    Charlotte Hornets       PF 1998-08-23   3831840
3      Derrick Rose      Detroit Pistons       PG 1988-10-04   7317074
4     Marial Shayok   Philadelphia 76ers        G 1995-07-26     79568
 ...     ...                ...         ...          ...         ...
445   Austin Rivers      Houston Rockets       PG 1992-08-01   2174310
446     Harry Giles     Sacramento Kings       PF 1998-04-22   2578800
447     Robin Lopez      Milwaukee Bucks        C 1988-04-01   4767000
448   Collin Sexton  Cleveland Cavaliers       PG 1999-01-04   4764960
449     Ricky Rubio         Phoenix Suns       PG 1990-10-21  16200000

450 rows × 5 columns

现在好多了!现在我们有一个日期时间列。Pandas 以传统的 YYYY-MM-DD 格式显示日期时间值。我很高兴导入成功,因此我们可以将 DataFrame 赋值给变量,比如 nba

In  [11] nba = pd.read_csv("nba.csv", parse_dates = ["Birthday"])

DataFrame 想象成一系列具有公共索引的 Series 对象是有帮助的。在这个例子中,nba 中的五个列(姓名、球队、位置、生日和薪水)共享相同的行索引。让我们开始探索 DataFrame 吧。

4.2.2 SeriesDataFrames 的共享和独有属性

SeriesDataFrames 的属性和方法可能在名称和实现上有所不同。以下是一个例子。Series 有一个 dtype 属性,它揭示了其值的类型(见第二章)。请注意,dtype 属性是单数的,因为 Series 只能存储一种数据类型:

In  [12] pd.Series([1, 2, 3]).dtype

Out [12] dtype('int64')

相比之下,DataFrame 可以存储异构数据。异构 意味着混合或不同。一列可以存储整数,另一列可以存储字符串。DataFrame 有一个独特的 dtypes 属性。(注意名称是复数。)该属性返回一个以 DataFrame 的列作为索引标签,列的数据类型作为值的 Series

In  [13] nba.dtypes

Out [13] Name                object
         Team                object
         Position            object
         Birthday    datetime64[ns]
         Salary               int64
         dtype: object

姓名、球队和位置列将 object 列为其数据类型。object 数据类型是 pandas 对复杂对象的术语,包括字符串。因此,nba DataFrame 有三个字符串列,一个日期时间列和一个整数列。

我们可以在 Series 上调用 value_counts 方法来计算存储每种数据类型的列数:

In  [14] nba.dtypes.value_counts()

Out [14] object            3
         datetime64[ns]    1
         int64             1
         dtype: int64

dtypedtypesSeriesDataFrames 之间不同属性的一个例子。但这两个数据结构也有许多共同的属性和方法。

一个 DataFrame 由几个较小的对象组成:一个包含行标签的索引,一个包含列标签的索引,以及一个包含值的值容器。index 属性暴露了 DataFrame 的索引:

In  [15] nba.index

Out [15] RangeIndex(start=0, stop=450, step=1)

这里,我们有一个 RangeIndex,这是一个优化存储一系列数值的索引。RangeIndex 对象包括三个属性:start(包含的下限),stop(排除的上限),和 step(每两个值之间的间隔或步长)。上面的输出告诉我们 nba 的索引从 0 开始计数,以 1 为增量递增到 450。

Pandas 使用一个单独的索引对象来存储 DataFrame 的列。我们可以通过 columns 属性访问它:

In  [16] nba.columns

Out [16] Index(['Name', 'Team', 'Position', 'Birthday', 'Salary'],
         dtype='object'

此对象是另一种索引对象:Index。Pandas 在索引由文本值组成时使用此选项。

index属性是一个DataFrameSeries共享的属性的例子。columns属性是一个仅属于DataFrame的属性的例子。Series没有列的概念。

ndim属性返回 pandas 对象中的维度数。DataFrame有两个:

In  [17] nba.ndim

Out [17] 2

shape属性以元组的形式返回DataFrame的维度。nba数据集有 450 行和 5 列:

In  [18] nba.shape

Out [18] (450, 5)

size属性计算数据集中的总值数。缺失值(如NaNs)包含在计数中:

In  [19] nba.size

Out [19] 2250

如果我们想要排除缺失值,count方法返回一个包含每列当前值计数的Series

In  [20] nba.count()

Out [20] Name        450
         Team        450
         Position    450
         Birthday    450
         Salary      450
         dtype: int64

我们可以使用sum方法将这些Series值相加,以得到DataFrame中非空值的数量。nba DataFrame数据集没有缺失值,所以size属性和sum方法返回相同的结果:

In  [21] nba.count().sum()

Out [21] 2250

这里有一个示例,说明了size属性和count方法之间的区别。让我们创建一个包含缺失值的DataFrame。我们可以将nan作为 NumPy 包的顶层属性访问:

In  [22] data = {
             "A": [1, np.nan],
             "B": [2, 3]
         }

         df = pd.DataFrame(data)
         df

Out [22]

 **A  B**
0  1.0  2
1  NaN  3

size属性返回4,因为DataFrame有四个单元格:

In  [23] df.size

Out [23] 4

相比之下,sum方法返回3,因为DataFrame有三个非空值:

In  [24] df.count()

Out [24] A    1
         B    2
         dtype: int64

In  [25] df.count().sum()

Out [25] 3

A 列有一个当前值,B 列有两个当前值。

4.2.3 Series 和 DataFrame 的共享方法

DataFrameSeries也有共同的方法。我们可以使用head方法从DataFrame的顶部提取行,例如:

In  [26] nba.head(2)

Out [26]

 **Name                Team Position   Birthday   Salary**
0    Shake Milton  Philadelphia 76ers       SG 1996-09-26  1445697
1  Christian Wood     Detroit Pistons       PF 1995-09-27  1645357

tail方法返回DataFrame底部的行:

In  [27] nba.tail(n = 3)

Out [27]

 **Name                 Team Position   Birthday    Salary**
447    Robin Lopez      Milwaukee Bucks        C 1988-04-01   4767000
448  Collin Sexton  Cleveland Cavaliers       PG 1999-01-04   4764960
449    Ricky Rubio         Phoenix Suns       PG 1990-10-21  16200000

这两个方法在没有参数的情况下默认返回五行:

In  [28] nba.tail()

Out [28]

 **Name                 Team Position   Birthday    Salary**
445  Austin Rivers      Houston Rockets       PG 1992-08-01   2174310
446    Harry Giles     Sacramento Kings       PF 1998-04-22   2578800
447    Robin Lopez      Milwaukee Bucks        C 1988-04-01   4767000
448  Collin Sexton  Cleveland Cavaliers       PG 1999-01-04   4764960
449    Ricky Rubio         Phoenix Suns       PG 1990-10-21  16200000

sample方法从DataFrame中提取随机行。它的第一个参数指定了行数:

In  [29] nba.sample(3)

Out [29]

 **Name                 Team Position    Birthday    Salary**
225     Tomas Satoransky        Chicago Bulls       PG  1991-10-30  10000000
201        Javonte Green       Boston Celtics       SF  1993-07-23    898310
310  Matthew Dellavedova  Cleveland Cavaliers       PG  1990-09-08   9607500

假设我们想要找出这个数据集中存在多少支队伍、薪资和职位。在第二章中,我们使用了nunique方法来计算Series中唯一值的数量。当我们对DataFrame调用相同的方法时,它返回一个包含每列唯一值计数的Series对象:

In  [30] nba.nunique()

Out [30] Name        450
         Team         30
         Position      9
         Birthday    430
         Salary      269
         dtype: int64

nba中,有 30 个唯一的队伍,269 个唯一的薪资和 9 个唯一的职位。

你可能还记得maxmin方法。在DataFrame上,max方法返回一个包含每列最大值的Series。文本列中的最大值是字母表中接近末尾的字符串。日期时间列中的最大值是按时间顺序最晚的日期:

In  [31] nba.max()

Out [31] Name             Zylan Cheatham
         Team         Washington Wizards
         Position                     SG
         Birthday    2000-12-23 00:00:00
         Salary                 40231758
         dtype: object

min方法返回一个包含每列最小值的Series(最小的数字,字母表中接近开头的字符串,最早的日期,等等):

In  [32] nba.min()

Out [32] Name               Aaron Gordon
         Team              Atlanta Hawks
         Position                      C
         Birthday    1977-01-26 00:00:00
         Salary                    79568
         dtype: object

如果我们想要识别多个最大值,例如数据集中的四位最高薪球员?nlargest方法可以检索具有DataFrame中给定列最大值的行子集。我们通过将其n参数传递要提取的行数,并通过其columns参数传递用于排序的列:

In  [33] nba.nlargest(n = 4, columns = "Salary")

Out [33]

 **Name                   Team Position   Birthday    Salary**
205      Stephen Curry  Golden State Warriors       PG 1988-03-14  40231758
38          Chris Paul  Oklahoma City Thunder       PG 1985-05-06  38506482
219  Russell Westbrook        Houston Rockets       PG 1988-11-12  38506482
251          John Wall     Washington Wizards       PG 1990-09-06  38199000

我们下一个挑战是找到联盟中最年长的三位球员。我们可以通过获取生日列中的三个最早日期来完成这项任务。nsmallest方法可以帮助我们;它返回具有数据集中给定列最小值的行子集。最小的日期时间值是那些在时间顺序中最早发生的。请注意,nlargestnsmallest方法只能用于数值或日期时间列:

In  [34] nba.nsmallest(n = 3, columns = ["Birthday"])

Out [34]

 **Name             Team Position   Birthday   Salary**
98    Vince Carter    Atlanta Hawks       PF 1977-01-26  2564753
196  Udonis Haslem       Miami Heat        C 1980-06-09  2564753
262    Kyle Korver  Milwaukee Bucks       PF 1981-03-17  6004753

如果我们想要计算所有 NBA 球员的薪资总和呢?DataFrame包含一个sum方法用于此目的:

In  [35] nba.sum()

Out [35] Name        Shake MiltonChristian WoodPJ WashingtonDerrick...
         Team        Philadelphia 76ersDetroit PistonsCharlotte Hor...
         Position    SGPFPFPGGPFSGSFCSFPGPGFCPGSGPFCCPFPFSGPFPGSGSF...
         Salary                                             3444112694
         dtype: object

我们确实得到了我们想要的答案,但输出有点杂乱。默认情况下,pandas 会在每个列中添加值。对于文本列,库将所有字符串连接成一个。要限制添加到数值总量,我们可以将True传递给sum方法的numeric_only参数:

In  [36] nba.sum(numeric_only = True)

Out [36] Salary    3444112694
         dtype: int64

这 450 名 NBA 球员的总薪资高达 34 亿美元。我们可以使用mean方法来计算平均薪资。该方法接受相同的numeric_only参数,以仅针对数值列:

In  [37] nba.mean(numeric_only = True)

Out [37] Salary    7.653584e+06
         dtype: float64

DataFrame还包括用于统计计算的诸如中位数、众数和标准差的方法:

In  [38] nba.median(numeric_only = True)

Out [38] Salary    3303074.5
         dtype: float64

In  [39] nba.mode(numeric_only = True)

Out [39]

 **Salary**
0   79568

In  [40] nba.std(numeric_only = True)

Out [40] Salary    9.288810e+06
         dtype: float64

对于高级统计方法,请查看官方的Series文档(mng.bz/myDa)。

4.3 对 DataFrame 进行排序

我们的数据集行以混乱、随机的顺序到达,但这不是问题!我们可以通过使用sort_values方法按一个或多个列对DataFrame进行排序。

4.3.1 按单列排序

让我们先按姓名的字母顺序对球员进行排序。sort_values方法的第一参数by接受 pandas 应使用的列以对DataFrame进行排序。让我们将名称列作为字符串传递:

In  [41] # The two lines below are equivalent
         nba.sort_values("Name")
         nba.sort_values(by = "Name")

Out [41]

 **Name                   Team Position   Birthday    Salary**
52        Aaron Gordon          Orlando Magic       PF 1995-09-16  19863636
101      Aaron Holiday         Indiana Pacers       PG 1996-09-30   2239200
437        Abdel Nader  Oklahoma City Thunder       SF 1993-09-25   1618520
81         Adam Mokoka          Chicago Bulls        G 1998-07-18     79568
399  Admiral Schofield     Washington Wizards       SF 1997-03-30   1000000
...                ...                    ...      ...        ...       ...
159        Zach LaVine          Chicago Bulls       PG 1995-03-10  19500000
302       Zach Norvell     Los Angeles Lakers       SG 1997-12-09     79568
312       Zhaire Smith     Philadelphia 76ers       SG 1999-06-04   3058800
137    Zion Williamson   New Orleans Pelicans        F 2000-07-06   9757440
248     Zylan Cheatham   New Orleans Pelicans       SF 1995-11-17     79568

450 rows × 5 columns

sort_values方法的ascending参数确定排序顺序;它有一个默认参数为True。默认情况下,pandas 将按升序对数字列进行排序,对字符串列按字母顺序排序,对日期时间列按时间顺序排序。

如果我们想要按逆字母顺序对名称进行排序,我们可以将ascending参数传递为False

In  [42] nba.sort_values("Name", ascending = False).head()

Out [42]

 **Name                  Team Position   Birthday    Salary**
248   Zylan Cheatham  New Orleans Pelicans       SF 1995-11-17     79568
137  Zion Williamson  New Orleans Pelicans        F 2000-07-06   9757440
312     Zhaire Smith    Philadelphia 76ers       SG 1999-06-04   3058800
302     Zach Norvell    Los Angeles Lakers       SG 1997-12-09     79568
159      Zach LaVine         Chicago Bulls       PG 1995-03-10  19500000

这里还有一个例子:如果我们想在不使用nsmallest方法的情况下找到nba中的五位最年轻球员怎么办?我们可以通过使用sort_values方法并将ascending设置为False来按逆时间顺序对生日列进行排序,然后使用head方法取前五行:

In  [43] nba.sort_values("Birthday", ascending = False).head()

Out [43]

 **Name                  Team Position   Birthday   Salary**
136      Sekou Doumbouya       Detroit Pistons       SF 2000-12-23  3285120
432  Talen Horton-Tucker    Los Angeles Lakers       GF 2000-11-25   898310
137      Zion Williamson  New Orleans Pelicans        F 2000-07-06  9757440
313           RJ Barrett       New York Knicks       SG 2000-06-14  7839960
392         Jalen Lecque          Phoenix Suns        G 2000-06-13   898310

nba 中最年轻球员在输出中排在第一位。这位球员是 Sekou Doumbouya,他出生于 2000 年 12 月 23 日。

4.3.2 按多列排序

我们可以通过将列表传递给 sort_values 方法的 by 参数来按多个列对 DataFrame 进行排序。Pandas 将按列表中出现的顺序依次对 DataFrame 的列进行排序。下一个示例首先按 Team 列排序,然后按 Name 列排序。Pandas 默认对所有列进行升序排序:

In  [44] nba.sort_values(by = ["Team", "Name"])

Out [44]

 **Name                Team Position   Birthday    Salary**
359         Alex Len       Atlanta Hawks        C 1993-06-16   4160000
167     Allen Crabbe       Atlanta Hawks       SG 1992-04-09  18500000
276  Brandon Goodwin       Atlanta Hawks       PG 1995-10-02     79568
438   Bruno Fernando       Atlanta Hawks        C 1998-08-15   1400000
194      Cam Reddish       Atlanta Hawks       SF 1999-09-01   4245720
...              ...                 ...      ...        ...       ...
418     Jordan McRae  Washington Wizards       PG 1991-03-28   1645357
273  Justin Robinson  Washington Wizards       PG 1997-10-12    898310
428    Moritz Wagner  Washington Wizards        C 1997-04-26   2063520
21     Rui Hachimura  Washington Wizards       PF 1998-02-08   4469160
36     Thomas Bryant  Washington Wizards        C 1997-07-31   8000000

450 rows × 5 columns

这是您读取输出的方式。当按字母顺序对球队进行排序时,亚特兰大老鹰队是数据集中的第一个球队。在亚特兰大老鹰队中,Alex Len 的名字排在第一位,其次是 Allen Crabbe 和 Brandon Goodwin。Pandas 对剩余的球队和名字重复此排序逻辑。

我们可以将单个布尔值传递给 ascending 参数,以将相同的排序顺序应用于每一列。下一个示例传递 False,因此 pandas 首先按降序排序 Team 列,然后按降序排序 Name 列:

In  [45] nba.sort_values(["Team", "Name"], ascending = False)

Out [45]

 **Name                Team Position   Birthday    Salary**
36     Thomas Bryant  Washington Wizards        C 1997-07-31   8000000
21     Rui Hachimura  Washington Wizards       PF 1998-02-08   4469160
428    Moritz Wagner  Washington Wizards        C 1997-04-26   2063520
273  Justin Robinson  Washington Wizards       PG 1997-10-12    898310
418     Jordan McRae  Washington Wizards       PG 1991-03-28   1645357
...              ...                 ...      ...        ...       ...
194      Cam Reddish       Atlanta Hawks       SF 1999-09-01   4245720
438   Bruno Fernando       Atlanta Hawks        C 1998-08-15   1400000
276  Brandon Goodwin       Atlanta Hawks       PG 1995-10-02     79568
167     Allen Crabbe       Atlanta Hawks       SG 1992-04-09  18500000
359         Alex Len       Atlanta Hawks        C 1993-06-16   4160000

450 rows × 5 columns

如果我们想按不同的顺序对每一列进行排序呢?例如,我们可能希望按升序对球队进行排序,然后按降序对那些球队中的薪水进行排序。为了完成这个任务,我们可以将布尔值列表传递给 ascending 参数。传递给 byascending 参数的列表长度必须相等。Pandas 将使用两个列表之间的共享索引位置来匹配每一列与其关联的排序顺序。在下一个示例中,Team 列在 by 列中占据索引位置 0;Pandas 将它与 ascending 列中索引位置 0 的 True 匹配,因此按升序排序该列。Pandas 对 Salary 列应用相同的逻辑,并按降序排序:

In  [46] nba.sort_values(
             by = ["Team", "Salary"], ascending = [True, False]
         )

Out [46]

 **Name                Team Position   Birthday    Salary**
111   Chandler Parsons       Atlanta Hawks       SF 1988-10-25  25102512
28         Evan Turner       Atlanta Hawks       PG 1988-10-27  18606556
167       Allen Crabbe       Atlanta Hawks       SG 1992-04-09  18500000
213    De'Andre Hunter       Atlanta Hawks       SF 1997-12-02   7068360
339      Jabari Parker       Atlanta Hawks       PF 1995-03-15   6500000
...                ...                 ...      ...        ...       ...
80         Isaac Bonga  Washington Wizards       PG 1999-11-08   1416852
399  Admiral Schofield  Washington Wizards       SF 1997-03-30   1000000
273    Justin Robinson  Washington Wizards       PG 1997-10-12    898310
283   Garrison Mathews  Washington Wizards       SG 1996-10-24     79568
353      Chris Chiozza  Washington Wizards       PG 1995-11-21     79568

450 rows × 5 columns

数据看起来不错,所以让我们使排序永久化。sort_values 方法支持 inplace 参数,但我们将明确地将返回的 DataFrame 重新赋值给 nba 变量(有关 inplace 参数的不足之处,请参阅第三章):

In  [47] nba = nba.sort_values(
             by = ["Team", "Salary"],
             ascending = [True, False]
         )

欢呼——我们已经按 TeamSalary 列的值对 DataFrame 进行了排序。现在我们可以找出每个球队中哪位球员的薪水最高。

4.4 按索引排序

在我们的永久排序中,DataFrame 的顺序与到达时不同:

In  [48] nba.head()

Out [48]

 **Name           Team Position   Birthday    Salary**
111  Chandler Parsons  Atlanta Hawks       SF 1988-10-25  25102512
28        Evan Turner  Atlanta Hawks       PG 1988-10-27  18606556
167      Allen Crabbe  Atlanta Hawks       SG 1992-04-09  18500000
213   De'Andre Hunter  Atlanta Hawks       SF 1997-12-02   7068360
339     Jabari Parker  Atlanta Hawks       PF 1995-03-15   6500000

我们如何将其恢复到原始形式?

4.4.1 按行索引排序

我们的 nba DataFrame 仍然具有其数值索引。如果我们能按索引位置而不是按列值对数据集进行排序,我们就可以将其恢复到原始形状。sort_index 方法正是这样做的:

In  [49] # The two lines below are equivalent
         nba.sort_index().head()
         nba.sort_index(ascending = True).head()

Out [49]

 **Name                Team Position   Birthday   Salary**
0    Shake Milton  Philadelphia 76ers       SG 1996-09-26  1445697
1  Christian Wood     Detroit Pistons       PF 1995-09-27  1645357
2   PJ Washington   Charlotte Hornets       PF 1998-08-23  3831840
3    Derrick Rose     Detroit Pistons       PG 1988-10-04  7317074
4   Marial Shayok  Philadelphia 76ers        G 1995-07-26    79568

我们还可以通过将 False 传递给方法的 ascending 参数来反转排序顺序。下一个示例首先显示最大的索引位置:

In  [50] nba.sort_index(ascending = False).head()

Out [50]

 **Name                 Team Position   Birthday    Salary**
449    Ricky Rubio         Phoenix Suns       PG 1990-10-21  16200000
448  Collin Sexton  Cleveland Cavaliers       PG 1999-01-04   4764960
447    Robin Lopez      Milwaukee Bucks        C 1988-04-01   4767000
446    Harry Giles     Sacramento Kings       PF 1998-04-22   2578800
445  Austin Rivers      Houston Rockets       PG 1992-08-01   2174310

我们回到了起点,DataFrame 已按索引位置排序。现在,让我们将这个 DataFrame 重新赋值给 nba 变量:

In  [51] nba = nba.sort_index()

接下来,让我们探索如何按 nba 的其他轴进行排序。

4.4.2 按列索引排序

DataFrame 是一个二维数据结构。我们可以对额外的轴进行排序:垂直轴。

要按顺序对 DataFrame 列进行排序,我们再次依赖 sort_index 方法。然而,这次我们需要添加一个 axis 参数,并将其参数传递为 "columns" 或 1。下一个示例按升序排序列:

In  [52] # The two lines below are equivalent
         nba.sort_index(axis = "columns").head()
         nba.sort_index(axis = 1).head()

Out [52]

 **Birthday            Name Position   Salary                Team**
0 1996-09-26    Shake Milton       SG  1445697  Philadelphia 76ers
1 1995-09-27  Christian Wood       PF  1645357     Detroit Pistons
2 1998-08-23   PJ Washington       PF  3831840   Charlotte Hornets
3 1988-10-04    Derrick Rose       PG  7317074     Detroit Pistons
4 1995-07-26   Marial Shayok        G    79568  Philadelphia 76ers

按照逆字母顺序排序列怎么样?这个任务很简单:我们可以将 ascending 参数的参数设置为 False。下一个示例调用 sort_index 方法,使用 axis 参数指定列,并通过 ascending 参数按降序排序:

In  [53] nba.sort_index(axis = "columns", ascending = False).head()

Out [53]

 **Team   Salary Position            Name   Birthday**
0  Philadelphia 76ers  1445697       SG    Shake Milton 1996-09-26
1     Detroit Pistons  1645357       PF  Christian Wood 1995-09-27
2   Charlotte Hornets  3831840       PF   PJ Washington 1998-08-23
3     Detroit Pistons  7317074       PG    Derrick Rose 1988-10-04
4  Philadelphia 76ers    79568        G   Marial Shayok 1995-07-26

让我们花点时间来反思 pandas 的强大功能。通过两种方法和几个参数,我们能够对 DataFrame 在两个轴上、按一列、按多列、按升序、按降序或按多顺序进行排序。pandas 非常灵活。我们只需要将正确的方法与正确的参数结合起来。

4.5 设置新的索引

在本质上,我们的数据集是玩家集合。因此,使用 Name 列的值作为 DataFrame 的索引标签似乎是合适的。Name 还有一个好处,即它是唯一具有唯一值的列。

set_index 方法返回一个新的 DataFrame,其中指定的列被设置为索引。它的第一个参数 keys 接受列名作为字符串:

In  [54] # The two lines below are equivalent
         nba.set_index(keys = "Name")
         nba.set_index("Name")

Out [54]

                               Team Position   Birthday    Salary
**Name** 
Shake Milton     Philadelphia 76ers       SG 1996-09-26   1445697
Christian Wood      Detroit Pistons       PF 1995-09-27   1645357
PJ Washington     Charlotte Hornets       PF 1998-08-23   3831840
Derrick Rose        Detroit Pistons       PG 1988-10-04   7317074
Marial Shayok    Philadelphia 76ers        G 1995-07-26     79568
    ...                    ...           ...        ...       ...
Austin Rivers       Houston Rockets       PG 1992-08-01   2174310
Harry Giles        Sacramento Kings       PF 1998-04-22   2578800
Robin Lopez         Milwaukee Bucks        C 1988-04-01   4767000
Collin Sexton   Cleveland Cavaliers       PG 1999-01-04   4764960
Ricky Rubio            Phoenix Suns       PG 1990-10-21  16200000

450 rows × 4 columns

看起来不错!让我们覆盖我们的 nba 变量:

In  [55] nba = nba.set_index(keys = "Name")

作为旁注,我们可以在导入数据集时设置索引。将列名作为字符串传递给 read_csv 函数的 index_col 参数。以下代码导致相同的 DataFrame

In  [56] nba = pd.read_csv(
             "nba.csv", parse_dates = ["Birthday"], index_col = "Name"
         )

接下来,我们将讨论如何从我们的 DataFrame 中选择行和列。

4.6 从 DataFrame 中选择列和行

DataFrame 是具有共同索引的 Series 对象的集合。有多个语法选项可用于从 DataFrame 中提取一个或多个这些 Series

4.6.1 从 DataFrame 中选择单个列

每个 Series 列都作为 DataFrame 的属性可用。我们使用点语法来访问对象属性。例如,我们可以使用 nba.Salary 提取 Salary 列。注意,索引从 DataFrame 传递到 Series

In  [57] nba.Salary

Out [57] Name
         Shake Milton       1445697
         Christian Wood     1645357
         PJ Washington      3831840
         Derrick Rose       7317074
         Marial Shayok        79568
                             ...
         Austin Rivers      2174310
         Harry Giles        2578800
         Robin Lopez        4767000
         Collin Sexton      4764960
         Ricky Rubio       16200000
         Name: Salary, Length: 450, dtype: int64

我们也可以通过在 DataFrame 后方传递其名称来提取一个列,使用方括号:

In  [58] nba["Position"]

Out [58] Name
         Shake Milton      SG
         Christian Wood    PF
         PJ Washington     PF
         Derrick Rose      PG
         Marial Shayok      G
                           ..
         Austin Rivers     PG
         Harry Giles       PF
         Robin Lopez        C
         Collin Sexton     PG
         Ricky Rubio       PG
         Name: Position, Length: 450, dtype: object

方括号语法的优点是它支持有空格的列名。如果我们的列名为 "Player Position",我们只能通过方括号提取它:

nba["Player Position"]

属性语法会引发异常。Python 没有办法知道空格的意义,它会假设我们正在尝试访问一个 Player 列:

nba.Player Position

虽然意见不同,但我建议使用方括号语法进行提取。我喜欢那种 100%有效率的解决方案,即使它们需要多输入几个字符。

4.6.2 从 DataFrame 中选择多个列

要提取多个 DataFrame 列,声明一对开闭方括号;然后传递列名列表。结果将是一个新的 DataFrame,其列的顺序与列表元素相同。下一个示例目标是薪资和生日列:

In  [59] nba[["Salary", "Birthday"]]

Out [59]

                  Salary   Birthday
**Name** 
Shake Milton     1445697 1996-09-26
Christian Wood   1645357 1995-09-27
PJ Washington    3831840 1998-08-23
Derrick Rose     7317074 1988-10-04
Marial Shayok      79568 1995-07-26

Pandas 将根据列表中的顺序提取列:

In  [60] nba[["Birthday", "Salary"]].head()

Out [60]

                 Birthday   Salary
**Name** 
Shake Milton   1996-09-26  1445697
Christian Wood 1995-09-27  1645357
PJ Washington  1998-08-23  3831840
Derrick Rose   1988-10-04  7317074
Marial Shayok  1995-07-26    79568

我们可以使用 select_dtypes 方法根据数据类型选择列。该方法接受两个参数,includeexclude。这些参数接受单个字符串或列表,表示 pandas 应该保留或丢弃的列类型。提醒一下,如果您想查看每列的数据类型,可以访问 dtypes 属性。下一个示例从 nba 中选择仅包含字符串列:

In  [61] nba.select_dtypes(include = "object")

Out [61]

                               Team Position
**Name** 
Shake Milton     Philadelphia 76ers       SG
Christian Wood      Detroit Pistons       PF
PJ Washington     Charlotte Hornets       PF
Derrick Rose        Detroit Pistons       PG
Marial Shayok    Philadelphia 76ers        G
      ...                    ...         ...
Austin Rivers       Houston Rockets       PG
Harry Giles        Sacramento Kings       PF
Robin Lopez         Milwaukee Bucks        C
Collin Sexton   Cleveland Cavaliers       PG
Ricky Rubio            Phoenix Suns       PG

450 rows × 2 columns

下一个示例选择除了字符串和整数列之外的所有列:

In  [62] nba.select_dtypes(exclude = ["object", "int"])

Out [62]

                 Birthday
**Name** 
Shake Milton   1996-09-26
Christian Wood 1995-09-27
PJ Washington  1998-08-23
Derrick Rose   1988-10-04
Marial Shayok  1995-07-26
      ...          ...
Austin Rivers  1992-08-01
Harry Giles    1998-04-22
Robin Lopez    1988-04-01
Collin Sexton  1999-01-04
Ricky Rubio    1990-10-21

450 rows × 1 columns

生日列是 nba 中唯一一个既不包含字符串也不包含整数值的列。要包含或排除日期时间列,我们可以将 "datetime" 参数传递给正确的参数。

4.7 从 DataFrame 中选择行

现在我们已经练习了提取列,让我们学习如何通过索引标签或位置提取 DataFrame 行。

4.7.1 通过索引标签提取行

loc 属性通过标签提取一行。我们称像 loc 这样的属性为访问器,因为它们可以访问数据的一部分。在 loc 后面立即输入一对方括号,并传递目标索引标签。下一个示例通过索引标签 "LeBron James" 提取了 nba 行。Pandas 以 Series 的形式返回行的值。一如既往,请注意大小写敏感性:

In  [63] nba.loc["LeBron James"]

Out [63] Team         Los Angeles Lakers
         Position                     PF
         Birthday    1984-12-30 00:00:00
         Salary                 37436858
         Name: LeBron James, dtype: object

我们可以在方括号之间传递一个列表来提取多行。当结果集包含多个记录时,pandas 将结果存储在一个 DataFrame 中:

In  [64] nba.loc[["Kawhi Leonard", "Paul George"]]

Out [64]

                               Team Position   Birthday    Salary
**Name** 
Kawhi Leonard  Los Angeles Clippers       SF 1991-06-29  32742000
Paul George    Los Angeles Clippers       SF 1990-05-02  33005556

Pandas 按照索引标签在列表中出现的顺序组织行。下一个示例交换了上一个示例中的字符串顺序:

In  [65] nba.loc[["Paul George", "Kawhi Leonard"]]

Out [65]

                               Team Position   Birthday    Salary
**Name** 
Paul George    Los Angeles Clippers       SF 1990-05-02  33005556
Kawhi Leonard  Los Angeles Clippers       SF 1991-06-29  32742000

我们可以使用 loc 提取一系列索引标签。语法与 Python 的列表切片语法相似。我们提供起始值,一个冒号,以及结束值。对于此类提取,我强烈建议首先对索引进行排序,因为这可以加快 pandas 查找值的速度。

假设我们想要定位在 Otto Porter 和 Patrick Beverley 之间的所有球员。我们可以对 DataFrame 的索引进行排序,以按字母顺序获取球员姓名,然后向 loc 访问器提供这两个球员姓名。"Otto Porter" 代表我们的下限,而 "Patrick Beverley" 代表上限:

In  [66] nba.sort_index().loc["Otto Porter":"Patrick Beverley"]

Out [66]

                                  Team Position   Birthday    Salary
**Name** 
Otto Porter              Chicago Bulls       SF 1993-06-03  27250576
PJ Dozier               Denver Nuggets       PG 1996-10-25     79568
PJ Washington        Charlotte Hornets       PF 1998-08-23   3831840
Pascal Siakam          Toronto Raptors       PF 1994-04-02   2351838
Pat Connaughton        Milwaukee Bucks       SG 1993-01-06   1723050
Patrick Beverley  Los Angeles Clippers       PG 1988-07-12  12345680

注意,pandas 的 loc 访问器与 Python 的列表切片语法有一些不同。首先,loc 访问器包括上限的标签,而 Python 的列表切片语法不包括上限的值。

这里有一个快速示例来提醒你。下一个示例使用列表切片语法从包含三个元素的列表中提取索引 0 到索引 2 的元素。索引 2("PJ Washington")是排他的,所以 Python 将其省略:

In  [67] players = ["Otto Porter", "PJ Dozier", "PJ Washington"]
         players[0:2]

Out [67] ['Otto Porter', 'PJ Dozier']

我们可以使用 locDataFrame 的中间拉取行到其末尾。传递方括号起始索引标签和冒号:

In  [68] nba.sort_index().loc["Zach Collins":]

Out [68]

                                   Team Position   Birthday    Salary
**Name** 
Zach Collins     Portland Trail Blazers        C 1997-11-19   4240200
Zach LaVine               Chicago Bulls       PG 1995-03-10  19500000
Zach Norvell         Los Angeles Lakers       SG 1997-12-09     79568
Zhaire Smith         Philadelphia 76ers       SG 1999-06-04   3058800
Zion Williamson    New Orleans Pelicans        F 2000-07-06   9757440
Zylan Cheatham     New Orleans Pelicans       SF 1995-11-17     79568

向另一个方向转动,我们可以使用 loc 切片从 DataFrame 的开始处拉取行到特定的索引标签。从冒号开始,然后输入要提取到的索引标签。下一个示例返回从开始到数据集中的阿尔·霍福德的所有球员:

In  [69] nba.sort_index().loc[:"Al Horford"]

Out [69]

                                    Team Position   Birthday    Salary
**Name** 
Aaron Gordon               Orlando Magic       PF 1995-09-16  19863636
Aaron Holiday             Indiana Pacers       PG 1996-09-30   2239200
Abdel Nader        Oklahoma City Thunder       SF 1993-09-25   1618520
Adam Mokoka                Chicago Bulls        G 1998-07-18     79568
Admiral Schofield     Washington Wizards       SF 1997-03-30   1000000
Al Horford            Philadelphia 76ers        C 1986-06-03  28000000

如果索引标签在 DataFrame 中不存在,Pandas 将引发异常:

In  [70] nba.loc["Bugs Bunny"]

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)

KeyError: 'Bugs Bunny'

如其名称所示,KeyError 异常表示在给定的数据结构中不存在键。

4.7.2 通过索引位置提取行

iloc(索引位置)访问器通过索引位置提取行,这在我们的数据集中行的位置有重要意义时很有用。语法与 loc 我们使用的语法相似。在 iloc 后面输入一对方括号,并传递一个整数。Pandas 将提取该索引处的行:

In  [71] nba.iloc[300]

Out [71] Team             Denver Nuggets
         Position                     PF
         Birthday    1999-04-03 00:00:00
         Salary                  1416852
         Name: Jarred Vanderbilt, dtype: object

iloc 访问器也接受一个索引位置的列表来针对多个记录。下一个示例从索引位置 100、200、300 和 400 提取球员:

In  [72] nba.iloc[[100, 200, 300, 400]]

Out [72]

                                Team Position   Birthday   Salary
**Name** 
Brian Bowen           Indiana Pacers       SG 1998-10-02    79568
Marco Belinelli    San Antonio Spurs       SF 1986-03-25  5846154
Jarred Vanderbilt     Denver Nuggets       PF 1999-04-03  1416852
Louis King           Detroit Pistons        F 1999-04-06    79568

我们也可以使用列表切片语法与 iloc 访问器一起使用。请注意,然而,pandas 排除了冒号之后的索引位置。下一个示例传递 400:404 的切片。Pandas 包括索引位置为 400、401、402 和 403 的行,并排除了索引 404 的行:

In  [73] nba.iloc[400:404]

Out [73]

                                    Team Position   Birthday    Salary
**Name** 
Louis King               Detroit Pistons        F 1999-04-06     79568
Kostas Antetokounmpo  Los Angeles Lakers       PF 1997-11-20     79568
Rodions Kurucs             Brooklyn Nets       PF 1998-02-05   1699236
Spencer Dinwiddie          Brooklyn Nets       PG 1993-04-06  10605600

我们可以省略冒号前的数字,以从 DataFrame 的开始处拉取。在这里,我们针对从 nba 开始到(但不包括)索引位置 2 的行:

In  [74] nba.iloc[:2]

Out [74]

                              Team Position   Birthday   Salary
**Name** 
Shake Milton    Philadelphia 76ers       SG 1996-09-26  1445697
Christian Wood     Detroit Pistons       PF 1995-09-27  1645357

类似地,我们可以移除冒号后面的数字,以拉取到 DataFrame 的末尾。在这里,我们针对从索引位置 447 到 nba 末尾的行:

In  [75] nba.iloc[447:]

Out [75]

                              Team Position   Birthday    Salary
**Name** 
Robin Lopez        Milwaukee Bucks        C 1988-04-01   4767000
Collin Sexton  Cleveland Cavaliers       PG 1999-01-04   4764960
Ricky Rubio           Phoenix Suns       PG 1990-10-21  16200000

我们也可以传递负数,无论是单个值还是两个值。下一个示例从倒数第 10 行提取到(但不包括)倒数第 6 行:

In  [76] nba.iloc[-10:-6]

Out [76]

                                    Team Position   Birthday   Salary
**Name** 
Jared Dudley          Los Angeles Lakers       PF 1985-07-10  2564753
Max Strus                  Chicago Bulls       SG 1996-03-28    79568
Kevon Looney       Golden State Warriors        C 1996-02-06  4464286
Willy Hernangomez      Charlotte Hornets        C 1994-05-27  1557250

我们可以在方括号内提供第三个数字来创建步进序列,在两个索引位置之间创建一个间隔。下一个示例以 2 的增量拉取前 10 个 nba 行。结果 DataFrame 包含索引位置为 0、2、4、6 和 8 的行:

In  [77] nba.iloc[0:10:2]

Out [77]

                             Team Position   Birthday    Salary
**Name** 
Shake Milton   Philadelphia 76ers       SG 1996-09-26   1445697
PJ Washington   Charlotte Hornets       PF 1998-08-23   3831840
Marial Shayok  Philadelphia 76ers        G 1995-07-26     79568
Kendrick Nunn          Miami Heat       SG 1995-08-03   1416852
Brook Lopez       Milwaukee Bucks        C 1988-04-01  12093024

当我们想要提取每隔一行时,这种切片技术特别有效。

4.7.3 从特定列提取值

lociloc 属性都接受一个表示要提取的列(s)的第二个参数。如果我们使用 loc,我们必须提供列名。如果我们使用 iloc,我们必须提供列位置。下一个示例使用 loc"Giannis Antetokounmpo" 行和 Team 列的交点提取值:

In  [78] nba.loc["Giannis Antetokounmpo", "Team"]

Out [78] 'Milwaukee Bucks'

要指定多个值,我们可以为loc访问器的其中一个或两个参数传递一个列表。下一个示例提取具有"James Harden"索引标签的行以及 Position 和 Birthday 列的值。Pandas 返回一个Series

In  [79] nba.loc["James Harden", ["Position", "Birthday"]]

Out [79] Position                     PG
         Birthday    1989-08-26 00:00:00
         Name: James Harden, dtype: object

下一个示例提供了多个行标签和多个列:

In  [80] nba.loc[
             ["Russell Westbrook", "Anthony Davis"],
             ["Team", "Salary"]
         ]

Out [80]

                                 Team    Salary
**Name** 
Russell Westbrook     Houston Rockets  38506482
Anthony Davis      Los Angeles Lakers  27093019

我们还可以使用列表切片语法来提取多个列,而无需明确写出它们的名称。在我们的数据集中有四个列(Team、Position、Birthday 和 Salary)。让我们从 Position 到 Salary 提取所有列。Pandas 在loc切片中包含两个端点:

In  [81] nba.loc["Joel Embiid", "Position":"Salary"]

Out [81] Position                      C
         Birthday    1994-03-16 00:00:00
         Salary                 27504630
         Name: Joel Embiid, dtype: object

我们必须以DataFrame中列出现的顺序传递列名。下一个示例返回一个空结果,因为 Salary 列在 Position 列之后。Pandas 无法识别要提取哪些列:

In  [82] nba.loc["Joel Embiid", "Salary":"Position"]

Out [82] Series([], Name: Joel Embiid, dtype: object)

假设我们想要通过列的顺序而不是通过列名来定位列。记住,pandas 会给每个DataFrame列分配一个索引位置。在nba中,Team 列的索引为 0,Position 列的索引为 1,以此类推。我们可以将列的索引作为iloc的第二个参数传递。下一个示例定位到索引为 57 的行和索引为 3 的列(Salary)的交叉值:

In  [83] nba.iloc[57, 3]

Out [83] 796806

我们也可以在这里使用列表切片语法。下一个示例从索引位置 100 提取所有行,直到但不包括索引位置 104。它还包括从列的开始到但不包括索引位置 3 的列(Salary)的所有列:

In  [84] nba.iloc[100:104, :3]

Out [84]

                             Team Position   Birthday
**Name** 
Brian Bowen        Indiana Pacers       SG 1998-10-02
Aaron Holiday      Indiana Pacers       PG 1996-09-30
Troy Daniels   Los Angeles Lakers       SG 1991-07-15
Buddy Hield      Sacramento Kings       SG 1992-12-17

ilocloc访问器非常灵活。它们的方括号可以接受单个值、值列表、列表切片等。这种灵活性的缺点是它需要额外的开销;pandas 必须确定我们给ilocloc提供了什么类型的输入。

我们可以在知道要从DataFrame中提取单个值时使用两个可选属性,atiat。这两个属性更快,因为 pandas 可以在寻找单个值时优化其搜索算法。

语法类似。at属性接受行和列标签:

In  [85] nba.at["Austin Rivers", "Birthday"]

Out [85] Timestamp('1992-08-01 00:00:00')

iat属性接受行和列索引:

In  [86] nba.iat[263, 1]

Out [86] 'PF'

Jupyter Notebook 包含几个魔法方法来帮助我们增强开发者体验。我们使用%%前缀声明魔法方法,并将它们与常规 Python 代码一起输入。一个例子是%%timeit,它在单元格中运行代码并计算执行的平均时间。%%timeit有时会运行单元格高达 100,000 次!下一个示例使用魔法方法来比较我们迄今为止探索的访问器的速度:

In  [87] %%timeit
         nba.at["Austin Rivers", "Birthday"]

6.38 µs ± 53.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In  [88] %%timeit
         nba.loc["Austin Rivers", "Birthday"]

9.12 µs ± 53.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In  [89] %%timeit
         nba.iat[263, 1]

4.7 µs ± 27.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In  [90] %%timeit
         nba.iloc[263, 1]

7.41 µs ± 39.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

结果在不同计算机之间可能会有所不同,但显示了atiat相对于lociloc的明显速度优势。

4.8 从 Series 中提取值

locilocatiat访问器也适用于Series对象。我们可以在DataFrame的样本Series上练习,例如 Salary:

In  [91] nba["Salary"].loc["Damian Lillard"]

Out [91] 29802321

In  [92] nba["Salary"].at["Damian Lillard"]

Out [92] 29802321

In  [93] nba["Salary"].iloc[234]

Out [93] 2033160

In  [94] nba["Salary"].iat[234]

Out [94] 2033160

随意使用最适合你的访问器。

4.9 重命名列或行

你还记得columns属性吗?它暴露了存储DataFrame列名的Index对象:

In  [95] nba.columns

Out [95] Index(['Team', 'Position', 'Birthday', 'Salary'], dtype='object')

我们可以通过将新名字的列表赋给属性来重命名DataFrame的任何或所有列。下一个例子将 Salary 列的名称更改为 Pay:

In  [96] nba.columns = ["Team", "Position", "Date of Birth", "Pay"]
         nba.head(1)

Out [96]

                            Team Position Date of Birth      Pay
**Name** 
Shake Milton  Philadelphia 76ers       SG    1996-09-26  1445697

rename方法是一个替代选项,可以完成相同的结果。我们可以将其columns参数传递一个字典,其中键是现有列名,值是它们的新名称。下一个例子将出生日期列的名称更改为 Birthday:

In  [97] nba.rename(columns = { "Date of Birth": "Birthday" })

Out [97]

                               Team Position   Birthday       Pay
**Name** 
Shake Milton     Philadelphia 76ers       SG 1996-09-26   1445697
Christian Wood      Detroit Pistons       PF 1995-09-27   1645357
PJ Washington     Charlotte Hornets       PF 1998-08-23   3831840
Derrick Rose        Detroit Pistons       PG 1988-10-04   7317074
Marial Shayok    Philadelphia 76ers        G 1995-07-26     79568
 ...               ...                   ...      ...        ...
Austin Rivers       Houston Rockets       PG 1992-08-01   2174310
Harry Giles        Sacramento Kings       PF 1998-04-22   2578800
Robin Lopez         Milwaukee Bucks        C 1988-04-01   4767000
Collin Sexton   Cleveland Cavaliers       PG 1999-01-04   4764960
Ricky Rubio            Phoenix Suns       PG 1990-10-21  16200000

450 rows × 4 columns

让我们通过将返回的DataFrame赋给nba变量来使操作永久化:

In  [98] nba = nba.rename(columns = { "Date of Birth": "Birthday" })

我们还可以通过传递字典到方法的index参数来重命名索引标签。相同的逻辑适用;键是旧标签,值是新标签。以下示例将 "Giannis" Antetokounmpo" 与他的昵称 "Greek" Freak" 交换:

In  [99] nba.loc["Giannis Antetokounmpo"]

Out [99] Team                 Milwaukee Bucks
         Position                          PF
         Birthday         1994-12-06 00:00:00
         Pay                         25842697

         Name: Giannis Antetokounmpo, dtype: object

In  [100] nba = nba.rename(
              index = { "Giannis Antetokounmpo": "Greek Freak" }
          )

让我们尝试通过其新标签查找行:

In  [101] nba.loc["Greek Freak"]

Out [101] Team                 Milwaukee Bucks
          Position                          PF
          Birthday         1994-12-06 00:00:00
          Pay                         25842697
          Name: Greek Freak, dtype: object

我们已经成功更改了行标签!

4.10 重置索引

有时候,我们想要将另一列设置为DataFrame的索引。假设我们想要将 Team 设置为nba的索引。我们可以使用本章早些时候引入的set_index方法,但我们会失去当前球员名字的索引。看看这个例子:

In  [102] nba.set_index("Team").head()

Out [102]

                   Position   Birthday   Salary
**Team** 
Philadelphia 76ers       SG 1996-09-26  1445697
Detroit Pistons          PF 1995-09-27  1645357
Charlotte Hornets        PF 1998-08-23  3831840
Detroit Pistons          PG 1988-10-04  7317074
Philadelphia 76ers        G 1995-07-26    79568

为了保留球员的名字,我们首先必须将现有的索引重新集成到DataFrame的常规列中。reset_index方法将当前索引移动到DataFrame列中,并用 pandas 的数字索引替换原来的索引:

In  [103] nba.reset_index().head()

Out [103]

 **Name                 Team Position   Birthday    Salary**
0      Shake Milton   Philadelphia 76ers       SG 1996-09-26   1445697
1    Christian Wood      Detroit Pistons       PF 1995-09-27   1645357
2     PJ Washington    Charlotte Hornets       PF 1998-08-23   3831840
3      Derrick Rose      Detroit Pistons       PG 1988-10-04   7317074
4     Marial Shayok   Philadelphia 76ers        G 1995-07-26     79568

现在我们可以使用set_index方法将 Team 列移动到索引,而不会丢失数据:

In  [104] nba.reset_index().set_index("Team").head()

Out [104]

                              Name Position   Birthday   Salary
**Team** 
Philadelphia 76ers    Shake Milton       SG 1996-09-26  1445697
Detroit Pistons     Christian Wood       PF 1995-09-27  1645357
Charlotte Hornets    PJ Washington       PF 1998-08-23  3831840
Detroit Pistons       Derrick Rose       PG 1988-10-04  7317074
Philadelphia 76ers   Marial Shayok        G 1995-07-26    79568

避免使用inplace参数的一个优点是我们可以链式调用多个方法。让我们链式调用reset_indexset_index方法,并用结果覆盖nba变量:

In  [105] nba = nba.reset_index().set_index("Team")

这就是所有需要涵盖的内容。你现在已经熟悉了DataFrame,它是 pandas 库的核心工作马。

4.11 编程挑战

现在我们已经探讨了 NBA 的财务状况,让我们将本章的概念应用于不同的体育联盟。

4.11.1 问题

nfl.csv 文件包含了一份国家橄榄球联盟球员名单,其中包含类似的名字、球队、位置、生日和薪水列。看看你是否能回答这些问题:

  1. 我们如何导入 nfl.csv 文件?有什么有效的方法将其生日列的值转换为日期时间?

  2. 我们可以用哪两种方式将DataFrame的索引设置为存储球员名字?

  3. 我们如何计算这个数据集中每个球队的球员数量?

  4. 谁是收入最高的五位球员?

  5. 我们如何首先按球队字母顺序排序数据集,然后按薪水降序排序?

  6. 纽约喷气机队名单中最年长的球员是谁,他的生日是什么?

4.11.2 解决方案

让我们一步一步地走过挑战:

  1. 我们可以使用 read_csv 函数导入 CSV 文件。要将 Birthday 列的值存储为日期时间,我们将列传递给 parse_dates 参数的列表中:

    In  [106] nfl = pd.read_csv("nfl.csv", parse_dates = ["Birthday"])
              nfl
    
    Out [106]
    
     **Name                  Team Position   Birthday   Salary**
    0           Tremon Smith   Philadelphia Eagles       RB 1996-07-20   570000
    1         Shawn Williams    Cincinnati Bengals       SS 1991-05-13  3500000
    2            Adam Butler  New England Patriots       DT 1994-04-12   645000
    3            Derek Wolfe        Denver Broncos       DE 1990-02-24  8000000
    4              Jake Ryan  Jacksonville Jaguars      OLB 1992-02-27  1000000
       ...                   ...                     ...        ...          ...        ...
    1650    Bashaud Breeland    Kansas City Chiefs       CB 1992-01-30   805000
    1651         Craig James   Philadelphia Eagles       CB 1996-04-29   570000
    1652  Jonotthan Harrison         New York Jets        C 1991-08-25  1500000
    1653         Chuma Edoga         New York Jets       OT 1997-05-25   495000
    1654        Tajae Sharpe      Tennessee Titans       WR 1994-12-23  2025000
    
    1655 rows × 5 columns
    
  2. 我们下一个挑战是将球员名称设置为索引标签。我们的选择是调用 set_index 方法并将新的 DataFrame 赋值给 nfl 变量:

    In  [107] nfl = nfl.set_index("Name")
    

    另一个选项是在导入数据集时向 read_csv 函数提供 index_col 参数:

    In  [108] nfl = pd.read_csv(
                  "nfl.csv", index_col = "Name", parse_dates = ["Birthday"]
              )
    
    

    无论哪种方式,结果都将相同:

    In  [109] nfl.head()
    
    Out [109]
    
                                    Team Position   Birthday   Salary
    **Name** 
    Tremon Smith     Philadelphia Eagles       RB 1996-07-20   570000
    Shawn Williams    Cincinnati Bengals       SS 1991-05-13  3500000
    Adam Butler     New England Patriots       DT 1994-04-12   645000
    Derek Wolfe           Denver Broncos       DE 1990-02-24  8000000
    Jake Ryan       Jacksonville Jaguars      OLB 1992-02-27  1000000
    
    
  3. 要计算每个队伍的球员数量,我们可以在 Team 列上调用 value_counts 方法。首先,我们需要使用点语法或方括号提取 Team Series

    In  [110] # The two lines below are equivalent
              nfl.Team.value_counts().head()
              nfl["Team"].value_counts().head()
    
    Out [110] New York Jets           58
              Washington Redskins     56
              Kansas City Chiefs      56
              San Francisco 49Ers     55
              New Orleans Saints      55
    
    
  4. 要识别五位最高薪球员,我们可以使用 sort_values 方法对 Salary 列进行排序。要告诉 pandas 按降序排序,我们可以将 ascending 参数传递 False。另一个选项是 nlargest 方法:

    In  [111] nfl.sort_values("Salary", ascending = False).head()
    
    Out [111]
    
                                     Team Position   Birthday    Salary
    **Name** 
    Kirk Cousins        Minnesota Vikings       QB 1988-08-19  27500000
    Jameis Winston   Tampa Bay Buccaneers       QB 1994-01-06  20922000
    Marcus Mariota       Tennessee Titans       QB 1993-10-30  20922000
    Derek Carr            Oakland Raiders       QB 1991-03-28  19900000
    Jimmy Garoppolo   San Francisco 49Ers       QB 1991-11-02  17200000
    
    
  5. 要按多个列排序,我们需要将参数传递给 sort_values 方法的 byascending 参数。以下代码按升序排序 Team 列,然后按降序排序 Salary 列:

    In  [112] nfl.sort_values(
                  by = ["Team", "Salary"],
                  ascending = [True, False]
              )
    
    Out [112]
    
                                       Team Position   Birthday    Salary
    **Name** 
    Chandler Jones        Arizona Cardinals      OLB 1990-02-27  16500000
    Patrick Peterson      Arizona Cardinals       CB 1990-07-11  11000000
    Larry Fitzgerald      Arizona Cardinals       WR 1983-08-31  11000000
    David Johnson         Arizona Cardinals       RB 1991-12-16   5700000
    Justin Pugh           Arizona Cardinals        G 1990-08-15   5000000
     ...                       ...               ...     ...         ...
    Ross Pierschbacher  Washington Redskins        C 1995-05-05    495000
    Kelvin Harmon       Washington Redskins       WR 1996-12-15    495000
    Wes Martin          Washington Redskins        G 1996-05-09    495000
    Jimmy Moreland      Washington Redskins       CB 1995-08-26    495000
    Jeremy Reaves       Washington Redskins       SS 1996-08-29    495000
    
    1655 rows × 4 columns
    
    
  6. 最后一个挑战是有点棘手的:我们必须找到纽约喷气机阵容中最老的球员。鉴于我们目前可用的工具,我们可以将 Team 列设置为 DataFrame 索引,以便轻松提取所有喷气机球员。为了保留我们索引中当前球员的名称,我们首先使用 reset_index 方法将它们移回 DataFrame 作为常规列:

    In  [113] nfl = nfl.reset_index().set_index(keys = "Team")
              nfl.head(3)
    
    Out [113]
    
                                    Name Position   Birthday   Salary
    **Team** 
    Philadelphia Eagles     Tremon Smith       RB 1996-07-20   570000
    Cincinnati Bengals    Shawn Williams       SS 1991-05-13  3500000
    New England Patriots     Adam Butler       DT 1994-04-12   645000
    
    

    接下来,我们可以使用 loc 属性来隔离所有纽约喷气机球员:

    In  [114] nfl.loc["New York Jets"].head()
    
    Out [114]
                               Name Position   Birthday   Salary
    **Team** 
    New York Jets   Bronson Kaufusi       DE 1991-07-06   645000
    New York Jets    Darryl Roberts       CB 1990-11-26  1000000
    New York Jets     Jordan Willis       DE 1995-05-02   754750
    New York Jets  Quinnen Williams       DE 1997-12-21   495000
    New York Jets        Sam Ficken        K 1992-12-14   495000
    
    

    最后一步是对 Birthday 列进行排序并提取最高记录。这种排序之所以可能,仅仅是因为我们将列的值转换为日期时间:

    In  [115] nfl.loc["New York Jets"].sort_values("Birthday").head(1)
    
    Out [115]
    
                         Name Position   Birthday   Salary
    **Team** 
    New York Jets  Ryan Kalil        C 1985-03-29  2400000
    
    

在这个数据集中,纽约喷气机的最老球员是 Ryan Kalil。他的生日是 1985 年 3 月 29 日。

恭喜你完成编码挑战!

摘要

  • DataFrame 是由行和列组成的二维数据结构。

  • DataFrameSeries 共享属性和方法。由于这两个对象之间的维度差异,许多属性和方法操作方式不同。

  • sort_values 方法对一或多个 DataFrame 列进行排序。我们可以为每个列分配不同的排序顺序(升序或降序)。

  • loc 属性通过索引标签提取行或列。at 属性是定位单个值的便捷快捷方式。

  • iloc 属性通过索引位置提取行或列。iat 属性是定位单个值的便捷快捷方式。

  • reset_index 方法将索引恢复为 DataFrame 中的常规列。

  • rename 方法为一个或多个列或行设置不同的名称。

5. 过滤 DataFrame

本章涵盖

  • 减少 DataFrame 的内存使用

  • 通过一个或多个条件提取DataFrame

  • 过滤包含或排除空值的DataFrame

  • 选择介于某个范围内的列值

  • DataFrame中删除重复和空值

在第四章中,我们学习了如何通过使用lociloc访问器从DataFrame中提取行、列和单元格值。这些访问器在我们知道要针对的行/列的索引标签和位置时工作得很好。有时,我们可能想要通过条件或标准而不是标识符来定位行。例如,我们可能想要提取一个列包含特定值的行子集。

在本章中,我们将学习如何声明包含和排除DataFrame行的逻辑条件。我们将看到如何通过使用ANDOR逻辑结合多个条件。最后,我们将介绍一些 pandas 实用方法,这些方法简化了过滤过程。前方有很多乐趣,让我们开始吧。

5.1 优化数据集以减少内存使用

在我们过渡到过滤之前,让我们快速谈谈在 pandas 中减少内存使用。每次导入数据集时,考虑每个列是否以最优化类型存储其数据都很重要。“最佳”数据类型是消耗最少内存或提供最多效用的一种类型。例如,在大多数计算机上,整数比浮点数占用更少的内存,所以如果你的数据集包含整数,理想的情况是将它们导入为整数而不是浮点数。作为另一个例子,如果你的数据集包含日期,理想的情况是将它们导入为日期时间而不是字符串,这允许进行日期时间特定的操作。在本节中,我们将学习一些技巧和窍门,通过将列数据转换为不同类型来减少内存消耗,这将有助于后续的快速过滤。让我们从导入我们最喜欢的数据分析库的常规操作开始:

In  [1] import pandas as pd

本章的 employees.csv 数据集是一个虚构的公司员工集合。每条记录包括员工的姓氏、性别、在公司的工作开始日期、薪水、经理状态(TrueFalse)和团队。让我们用read_csv函数看一下数据集:

In  [2] pd.read_csv("employees.csv")

Out [2]

 **First Name  Gender Start Date    Salary   Mgmt          Team**
0       Douglas    Male     8/6/93       NaN   True     Marketing
1        Thomas    Male    3/31/96   61933.0   True           NaN
2         Maria  Female        NaN  130590.0  False       Finance
3         Jerry     NaN     3/4/05  138705.0   True       Finance
4         Larry    Male    1/24/98  101004.0   True            IT
...         ...     ...        ...       ...    ...           ...
996     Phillip    Male    1/31/84   42392.0  False       Finance
997     Russell    Male    5/20/13   96914.0  False       Product
998       Larry    Male    4/20/13   60500.0  False  Business Dev
999      Albert    Male    5/15/12  129949.0   True         Sales
1000        NaN     NaN        NaN       NaN    NaN           NaN

1001 rows × 6 columns

请花点时间注意输出中散布的NaNs。每一列都有缺失值。实际上,最后一行只包含NaNs。在现实世界中,像这样的不完整数据很常见。数据集可能包含空白行、空白列等。

我们如何提高数据集的效用?我们的第一个优化是我们现在应该感到舒适的。我们可以使用parse_dates参数将开始日期列中的文本值转换为日期时间:

In  [3] pd.read_csv("employees.csv", parse_dates = ["Start Date"]).head()

Out [3]

 **First Name  Gender  Start Date    Salary   Mgmt       Team**
0    Douglas    Male  1993-08-06       NaN   True  Marketing
1     Thomas    Male  1996-03-31   61933.0   True        NaN
2      Maria  Female         NaT  130590.0  False    Finance
3      Jerry     NaN  2005-03-04  138705.0   True    Finance
4      Larry    Male  1998-01-24  101004.0   True         IT

CSV 导入进展顺利,所以让我们将DataFrame对象分配给一个描述性的变量,例如employees

In  [4] employees = pd.read_csv(
            "employees.csv", parse_dates = ["Start Date"]
        )

有几种选项可以提高DataFrame操作的速度和效率。首先,让我们总结当前的数据集。我们可以调用info方法来查看列列表、它们的数据类型、缺失值的计数以及DataFrame的总内存消耗:

In  [5] employees.info()

Out [5]

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1001 entries, 0 to 1000
Data columns (total 6 columns):
 #   Column      Non-Null Count  Dtype
---  ------      --------------  -----
 0   First Name  933 non-null    object
 1   Gender      854 non-null    object
 2   Start Date  999 non-null    datetime64[ns]
 3   Salary      999 non-null    float64
 4   Mgmt        933 non-null    object
 5   Team        957 non-null    object
dtypes: datetime64ns, float64(1), object(4)
message usage: 47.0+ KB

让我们从上到下浏览输出结果。我们有一个包含 1,001 行的DataFrame,从索引 0 开始,到索引 1000 结束。有四个字符串列,一个日期时间列和一个浮点列。所有六列都有缺失数据。

当前内存使用量约为 47 KB——对于现代计算机来说是一个小数字,但让我们尝试将其数量减少。在阅读以下示例时,请更多地关注百分比减少而不是数值减少。随着数据集的增长,性能改进将更加显著。

5.1.1 使用 astype 方法转换数据类型

你注意到 pandas 将 Mgmt 列的值作为字符串导入了吗?该列只存储两个值:TrueFalse。我们可以通过将值转换为更轻量级的布尔数据类型来减少内存使用。

astype方法将Series的值转换为不同的数据类型。它接受一个参数:新的数据类型。我们可以传递数据类型或其名称的字符串。

下一个示例从employees中提取 Mgmt Series并使用bool参数调用其astype方法。Pandas 返回一个新的布尔Series对象。请注意,库将NaNs转换为True值。我们将在 5.5.4 节中讨论删除缺失值。

In  [6] employees["Mgmt"].astype(bool)

Out [6] 0        True
        1        True
        2       False
        3        True
        4        True
                ...
        996     False
        997     False
        998     False
        999      True
        1000     True
        Name: Mgmt, Length: 1001, dtype: bool

看起来不错!现在我们已经预览了Series将如何显示,我们可以覆盖employees中现有的 Mgmt 列。更新DataFrame列的工作方式与在字典中设置键值对类似。如果存在具有指定名称的列,pandas 会用新的Series覆盖它。如果不存在具有该名称的列,pandas 会创建一个新的Series并将其追加到DataFrame的右侧。库通过共享索引标签来匹配SeriesDataFrame中的行。

下一个代码示例使用我们的新布尔Series覆盖 Mgmt 列。作为提醒,Python 首先评估赋值运算符(=)的右侧。首先,我们创建一个新的Series,然后我们覆盖现有的 Mgmt 列:

In  [7] employees["Mgmt"] = employees["Mgmt"].astype(bool)

列赋值不会产生返回值,因此在 Jupyter Notebook 中代码不会输出任何内容。让我们再次查看DataFrame以查看结果:

In  [8] employees.tail()

Out [8]

 **First Name Gender Start Date    Salary   Mgmt          Team**
996     Phillip   Male 1984-01-31   42392.0  False       Finance
997     Russell   Male 2013-05-20   96914.0  False       Product
998       Larry   Male 2013-04-20   60500.0  False  Business Dev
999      Albert   Male 2012-05-15  129949.0   True         Sales
1000        NaN    NaN        NaT       NaN   True           NaN

除了缺失值最后一行的True之外,DataFrame看起来没有不同。但我们的内存使用量如何呢?让我们再次调用info方法来查看差异:

In  [9] employees.info()

Out [9]

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1001 entries, 0 to 1000
Data columns (total 6 columns):
 #   Column      Non-Null Count  Dtype
---  ------      --------------  -----
 0   First Name  933 non-null    object
 1   Gender      854 non-null    object
 2   Start Date  999 non-null    datetime64[ns]
 3   Salary      999 non-null    float64
 4   Mgmt        1001 non-null   bool
 5   Team        957 non-null    object
dtypes: bool(1), datetime64ns, float64(1), object(3)
memory usage: 40.2+ KB

我们将employees的内存使用量减少了近 15%,从 47 KB 降至 40.2 KB。这是一个相当不错的开始!

接下来,让我们过渡到 Salary 列。如果我们打开原始 CSV 文件,我们可以看到其值存储为整数:

First Name,Gender,Start Date,Salary,Mgmt,Team
Douglas,Male,8/6/93,,True,Marketing
Thomas,Male,3/31/96,61933,True,
Maria,Female,,130590,False,Finance
Jerry,,3/4/05,138705,True,Finance

employees中,然而,pandas 将 Salary 值存储为浮点数。为了在整个列中支持NaNs,pandas 将整数转换为浮点数——这是我们在前面的章节中观察到的库的技术要求。

在我们之前的布尔示例之后,我们可能会尝试使用astype方法将列的值强制转换为整数。不幸的是,pandas 会引发一个ValueError异常:

In  [10] employees["Salary"].astype(int)

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-99-b148c8b8be90> in *<module>*
----> 1 employees["Salary"].astype(int)

ValueError: Cannot convert non-finite values (NA or inf) to integer

Pandas 无法将NaN值转换为整数。我们可以通过用常数值替换NaN值来解决这个问题。fillna方法用我们传入的参数替换Series的空值。下一个示例提供了一个填充值为 0。请注意,您选择的价值可能会扭曲数据;0 仅用于示例。

我们知道原始的 Salary 列在其最后一行有一个缺失值。让我们在调用fillna方法后查看最后一行:

In  [11] employees["Salary"].fillna(0).tail()

Out [11] 996      42392.0
         997      96914.0
         998      60500.0
         999     129949.0
         1000         0.0
         Name: Salary, dtype: float64

太好了。现在,由于 Salary 列没有缺失值,我们可以使用astype方法将其值转换为整数:

In  [12] employees["Salary"].fillna(0).astype(int).tail()

Out [12] 996      42392
         997      96914
         998      60500
         999     129949
         1000         0
         Name: Salary, dtype: int64

接下来,我们可以覆盖employees中现有的 Salary Series

In  [13] employees["Salary"] = employees["Salary"].fillna(0).astype(int)

我们可以做出一个额外的优化。Pandas 包括一个称为类别的特殊数据类型,这对于相对于其总大小具有少量唯一值的列来说非常理想。一些具有有限数量值的日常数据点示例包括性别、工作日、血型、行星和收入群体。在幕后,pandas 只为每个分类值存储一个副本,而不是在行之间存储重复的副本。

nunique方法可以揭示每个DataFrame列中唯一值的数量。请注意,它默认排除计数中的缺失值(NaN):

In  [14] employees.nunique()

Out [14] First Name    200
         Gender          2
         Start Date    971
         Salary        995
         Mgmt            2
         Team           10
         dtype: int64

Gender 和 Team 列是存储分类值的良好候选。在 1,001 行数据中,Gender 只有两个唯一值,而 Team 只有十个唯一值。

让我们再次使用astype方法。首先,我们将通过将"category"作为参数传递给方法将 Gender 列的值转换为类别:

In  [15] employees["Gender"].astype("category")

Out [15] 0         Male
         1         Male
         2       Female
         3          NaN
         4         Male
                  ...
         996       Male
         997       Male
         998       Male
         999       Male
         1000       NaN
         Name: Gender, Length: 1001, dtype: category
         Categories (2, object): [Female, Male]

Pandas 已经识别出两个独特的类别:"Female""Male"。我们可以覆盖现有的 Gender 列:

In  [16] employees["Gender"] = employees["Gender"].astype("category")

让我们通过调用info方法检查内存使用情况。由于 pandas 只需要跟踪两个值而不是 1,001 个,内存使用量再次显著下降:

In  [17] employees.info()

Out [17]

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1001 entries, 0 to 1000
Data columns (total 6 columns):
 #   Column      Non-Null Count  Dtype
---  ------      --------------  -----
 0   First Name  933 non-null    object
 1   Gender      854 non-null    category
 2   Start Date  999 non-null    datetime64[ns]
 3   Salary      1001 non-null   int64
 4   Mgmt        1001 non-null   bool
 5   Team        957 non-null    object
dtypes: bool(1), category(1), datetime64ns, int64(1), object(2)
memory usage: 33.5+ KB

让我们为只有十个唯一值的 Team 列重复相同的流程:

In  [18] employees["Team"] = employees["Team"].astype("category")

In  [19] employees.info()

Out [19]

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1001 entries, 0 to 1000
Data columns (total 6 columns):
 #   Column      Non-Null Count  Dtype
---  ------      --------------  -----
 0   First Name  933 non-null    object
 1   Gender      854 non-null    category
 2   Start Date  999 non-null    datetime64[ns]
 3   Salary      1001 non-null   int64
 4   Mgmt        1001 non-null   bool
 5   Team        957 non-null    category
dtypes: bool(1), category(2)
memory usage: 27.0+ KB

在不到十行代码的情况下,我们已经将DataFrame的内存消耗减少了 40%以上。想象一下对拥有数百万行数据集的影响!

5.2 通过单个条件进行过滤

提取数据子集可能是数据分析中最常见的操作。一个子集是符合某种条件的大数据集的一部分。

假设我们想要生成所有名为"Maria"的员工的列表。为了完成这个任务,我们需要根据“First Name”列中的值过滤我们的员工数据集。名为 Maria 的员工列表是所有员工的一个子集。

首先,简要回顾一下 Python 中相等性的工作原理。在 Python 中,相等运算符(==)比较两个对象是否相等,如果对象相等则返回True,如果不相等则返回False。(有关详细解释,请参阅附录 B。)以下是一个简单的示例:

In  [20] "Maria" == "Maria"

Out [20] True

In  [21] "Maria" == "Taylor"

Out [21] False

要比较每个Series条目与一个常量值,我们将Series放在等号的一侧,而将值放在另一侧:

Series == value

有些人可能会认为这种语法会导致错误,但 pandas 足够智能,能够识别出我们想要比较的是每个Series值与指定字符串的相等性,而不是与Series本身进行比较。我们在第二章中探讨了类似的想法,当时我们将Series与数学运算符(如加号)配对。

当我们将Series与相等运算符结合时,pandas 返回一个布尔Series。下一个示例比较每个“First Name”列的值与"Maria"True值表示字符串"Maria"确实出现在该索引处,而False值表示没有。以下输出表明索引 2 存储的值是"Maria"

In  [22] employees["First Name"] == "Maria"

Out [22] 0       False
         1       False
         2        True
         3       False
         4       False
                 ...
         996     False
         997     False
         998     False
         999     False
         1000    False
         Name: First Name, Length: 1001, dtype: bool

如果我们只能提取具有 True 值的行,那么我们的 employees DataFrame 将包含数据集中所有的"Maria"记录。幸运的是,pandas 提供了一个方便的语法,通过布尔Series提取行。要过滤行,我们在 DataFrame 后提供布尔Series,并用方括号括起来:

In  [23] employees[employees["First Name"] == "Maria"]

Out [23]

 **First Name    Gender   Start Date   Salary     Mgmt            Team**
2        Maria  Female        NaT  130590  False       Finance
198      Maria  Female 1990-12-27   36067   True       Product
815      Maria     NaN 1986-01-18  106562  False            HR
844      Maria     NaN 1985-06-19  148857  False         Legal
936      Maria  Female 2003-03-14   96250  False  Business Dev
984      Maria  Female 2011-10-15   43455  False   Engineering

大获成功!我们已经使用布尔Series过滤了“First Name”列中值为"Maria"的行。

如果使用多个方括号令人困惑,您可以将布尔Series分配给一个描述性变量,然后通过方括号传递该变量。以下代码产生与前面代码相同的行子集:

In  [24] marias = employees["First Name"] == "Maria"
         employees[marias]

Out [24]

 **First      Name  Gender   Start  Date   Salary    Mgmt            Team**
2        Maria  Female        NaT  130590  False       Finance
198      Maria  Female 1990-12-27   36067   True       Product
815      Maria     NaN 1986-01-18  106562  False            HR
844      Maria     NaN 1985-06-19  148857  False         Legal
936      Maria  Female 2003-03-14   96250  False  Business Dev
984      Maria  Female 2011-10-15   43455  False   Engineering

初学者在比较值相等性时最常见的错误是使用一个等号而不是两个。请记住,单个等号将对象分配给变量,而两个等号检查对象之间的相等性。如果我们在这个例子中不小心使用了单个等号,我们将所有“First Name”列的值覆盖为字符串"Maria"。这可不是什么好事。

让我们再举一个例子。如果我们想提取不属于财务团队的员工子集,协议保持不变,但略有变化。我们需要生成一个布尔Series,检查团队列的哪些值不等于"Finance"。然后我们可以使用布尔Series来过滤employees。Python 的不等运算符在两个值不相等时返回True,在相等时返回False

In  [25] "Finance" != "Engineering"

Out [25] True

Series 对象也与不等式运算符友好地配合。以下示例比较团队列中的值与字符串 "Finance"True 表示给定索引的 Team 值不是 "Finance",而 False 表示 Team 值是 "Finance"

In  [26] employees["Team"] != "Finance"

Out [26] 0        True
         1        True
         2       False
         3       False
         4        True
                 ...
         996     False
         997      True
         998      True
         999      True
         1000     True
         Name: Team, Length: 1001, dtype: bool

现在我们有了布尔 Series,我们可以将其放入方括号中,以提取值为 True的 DataFrame 行。在以下输出中,我们看到 pandas 排除了索引 2 和 3 的行,因为那里的Team值为"Finance"`:

In  [27] employees[employees["Team"] != "Finance"]

Out [27]

 **First Name  Gender Start Date  Salary   Mgmt          Team**
0       Douglas    Male 1993-08-06       0   True     Marketing
1        Thomas    Male 1996-03-31   61933   True           NaN
4         Larry    Male 1998-01-24  101004   True            IT
5        Dennis    Male 1987-04-18  115163  False         Legal
6          Ruby  Female 1987-08-17   65476   True       Product
...       ...      ...     ...        ...      ...         ...
995       Henry     NaN 2014-11-23  132483  False  Distribution
997     Russell    Male 2013-05-20   96914  False       Product
998       Larry    Male 2013-04-20   60500  False  Business Dev
999      Albert    Male 2012-05-15  129949   True         Sales
1000        NaN     NaN        NaT       0   True           NaN

899 rows × 6 columns

注意,结果包括具有缺失值的行。我们可以在索引 1000 处看到一个例子。在这种情况下,pandas 将 NaN 视为不等于字符串 "Finance"

如果我们想要检索公司中的所有经理?经理在 Mgmt 列中的值为 True。我们可以执行 employees["Mgmt"] == True,但不需要,因为 Mgmt 已经是一个布尔 SeriesTrue 值和 False 值已经表明 pandas 应该保留还是丢弃一行。因此,我们可以将 Mgmt 列本身放入方括号中:

In  [28] employees[employees["Mgmt"]].head()

Out [28]

 **First Name  Gender Start Date  Salary  Mgmt       Team**
0    Douglas    Male 1993-08-06       0  True  Marketing
1     Thomas    Male 1996-03-31   61933  True        NaN
3      Jerry     NaN 2005-03-04  138705  True    Finance
4      Larry    Male 1998-01-24  101004  True         IT
6       Ruby  Female 1987-08-17   65476  True    Product

我们也可以使用算术运算符根据数学条件来过滤列。以下示例生成一个布尔 Series,用于筛选薪资值大于 $100,000 的记录(有关此语法的更多信息,请参阅第二章):

In  [29] high_earners = employees["Salary"] > 100000
         high_earners.head()

Out [29] 0    False
         1    False
         2     True
         3     True
         4     True
         Name: Salary, dtype: bool

让我们看看哪些员工的薪资超过 $100,000:

In  [30] employees[high_earners].head()

Out [30]

 **First Name  Gender Start Date  Salary   Mgmt          Team**
2      Maria  Female        NaT  130590  False       Finance
3      Jerry     NaN 2005-03-04  138705   True       Finance
4      Larry    Male 1998-01-24  101004   True            IT
5     Dennis    Male 1987-04-18  115163  False         Legal
9    Frances  Female 2002-08-08  139852   True  Business Dev

尝试在 employees 的其他列上练习语法。只要提供布尔 Series,pandas 就能够过滤 DataFrame

5.3 多条件过滤

我们可以通过创建两个独立的布尔 Series 并声明 pandas 应在它们之间应用的逻辑标准来使用多个条件过滤 DataFrame

5.3.1 AND 条件

假设我们想要找到所有在业务发展团队工作的女性员工。现在 pandas 必须查找两个条件来选择行:性别列中的值为 "Female",团队列中的值为 "Business Dev"。这两个标准是独立的,但都必须满足。以下是一个关于如何使用 AND 逻辑与两个条件快速提醒的例子:

条件 1 条件 2 评估
True True True
True False False
False True False
False False False

让我们一次构建一个布尔 Series。我们可以从隔离性别列中的 "Female" 值开始:

In  [31] is_female = employees["Gender"] == "Female"

接下来,我们将针对所有在 "Business Dev" 团队工作的员工:

In  [32] in_biz_dev = employees["Team"] == "Business Dev"

最后,我们需要计算两个 Series 的交集,即 is_femalein_biz_dev Series 都有 True 值的行。将两个 Series 都放入方括号中,并在它们之间放置一个 & 符号。& 符号声明了一个 AND 逻辑标准。is_female Series 必须为 True,并且 in_biz_dev Series 也必须为 True

In  [33] employees[is_female & in_biz_dev].head()

Out [33]

 **First Name  Gender Start Date  Salary   Mgmt          Team**
9     Frances  Female 2002-08-08  139852   True  Business Dev
33       Jean  Female 1993-12-18  119082  False  Business Dev
36     Rachel  Female 2009-02-16  142032  False  Business Dev
38  Stephanie  Female 1986-09-13   36844   True  Business Dev
61     Denise  Female 2001-11-06  106862  False  Business Dev

只要我们用&符号分隔连续的两个Series,我们就可以在方括号内包含任意数量的Series。以下示例添加了一个第三个标准来识别业务发展团队的女性经理:

In  [34] is_manager = employees["Mgmt"]
         employees[is_female & in_biz_dev & is_manager].head()

Out [34]

 **First Name  Gender Start Date  Salary  Mgmt          Team**
9      Frances  Female 2002-08-08  139852  True  Business Dev
38   Stephanie  Female 1986-09-13   36844  True  Business Dev
66       Nancy  Female 2012-12-15  125250  True  Business Dev
92       Linda  Female 2000-05-25  119009  True  Business Dev
111     Bonnie  Female 1999-12-17   42153  True  Business Dev

总结来说,&符号选择符合所有条件的行。声明两个或多个布尔Series,然后使用&符号将它们编织在一起。

5.3.2 OR 条件

我们也可以提取符合几个条件之一的行。并非所有条件都必须为真,但至少有一个条件必须满足。以下是一个关于如何使用两个条件进行OR逻辑的快速提醒:

条件 1 条件 2 评估
True True True
True False True
False True True
False False False

假设我们想要识别所有薪资低于 4 万美元或开始日期在 2015 年 1 月 1 日之后的员工。我们可以使用数学运算符如<和>来得到这两个条件的两个单独的布尔Series

In  [35] earning_below_40k = employees["Salary"] < 40000
 started_after_2015 = employees["Start Date"] > "2015-01-01"

我们在布尔Series之间使用管道符号(|)来声明OR标准。在下一个示例中,我们选择任一布尔Series持有True值的行:

In  [36] employees[earning_below_40k | started_after_2015].tail()

Out [36]

 **First Name  Gender Start Date  Salary   Mgmt         Team**
958      Gloria  Female 1987-10-24   39833  False  Engineering
964       Bruce    Male 1980-05-07   35802   True        Sales
967      Thomas    Male 2016-03-12  105681  False  Engineering
989      Justin     NaN 1991-02-10   38344  False        Legal
1000        NaN     NaN        NaT       0   True          NaN

索引位置 958、964、989 和 1000 的行符合薪资条件,索引位置 967 的行符合开始日期条件。Pandas 还会包括符合这两个条件的行。

5.3.3 使用 ~ 进行反转

波浪线符号(~)反转布尔Series中的值。所有True值变为False,所有False值变为True。以下是一个简单的示例,使用了一个小的Series

In  [37] my_series = pd.Series([True, False, True])
         my_series

Out [37] 0     True
         1    False
         2     True
         dtype: bool

In  [38] ~my_series

Out [38] 0    False
         1     True
         2    False
         dtype: bool

当我们想要反转一个条件时,反转非常有用。假设我们想要识别薪资低于 10 万美元的员工。我们可以使用两种方法,第一种是编写employees["Salary"] < 100000

In  [39] employees[employees["Salary"] < 100000].head()

Out [39]

 **First Name  Gender Start Date  Salary  Mgmt         Team**
0    Douglas    Male 1993-08-06       0  True    Marketing
1     Thomas    Male 1996-03-31   61933  True          NaN
6       Ruby  Female 1987-08-17   65476  True      Product
7        NaN  Female 2015-07-20   45906  True      Finance
8     Angela  Female 2005-11-22   95570  True  Engineering

或者,我们可以反转收入超过或等于 10 万美元的员工的结果集。生成的DataFrames将是相同的。在下一个示例中,我们将大于操作放在括号内。这种语法确保 pandas 在反转其值之前生成布尔Series。一般来说,当评估顺序可能对 pandas 不清楚时,你应该使用括号:

In  [40] employees[~(employees["Salary"] >= 100000)].head()

Out [40]

 **First Name  Gender Start Date  Salary  Mgmt         Team**
0    Douglas    Male 1993-08-06       0  True    Marketing
1     Thomas    Male 1996-03-31   61933  True          NaN
6       Ruby  Female 1987-08-17   65476  True      Product
7        NaN  Female 2015-07-20   45906  True      Finance
8     Angela  Female 2005-11-22   95570  True  Engineering

TIP 对于像这样复杂的提取,考虑将布尔Series分配给一个描述性变量。

5.3.4 布尔方法

Pandas 为喜欢方法而不是运算符的分析师提供了一个替代语法。以下表格显示了相等、不等式和其他算术运算的方法替代方案:

操作 算术语法 方法语法
相等 employees["Team"] == "Marketing" employees["Team"].eq("Marketing")
不等式 employees["Team"] != "Marketing" employees["Team"].ne("Marketing")
小于 employees["Salary"] < 100000 employees["Salary"].lt(100000)
小于或等于 employees["Salary"] <= 100000 employees["Salary"].le(100000)
大于 employees["Salary"] > 100000 employees["Salary"].gt(100000)
大于等于 employees["Salary"] >= 100000 employees["Salary"].ge(100000)

关于使用&|符号进行AND/OR逻辑的规则同样适用。

5.4 通过条件过滤

一些过滤操作比简单的相等或不等式检查更复杂。幸运的是,pandas 提供了许多辅助方法,这些方法为这些类型的提取生成布尔Series

5.4.1 isin方法

如果我们想隔离属于销售、法律或市场营销团队的员工呢?我们可以在方括号内提供三个单独的布尔Series,并添加|符号来声明OR条件:

In  [41] sales = employees["Team"] == "Sales"
         legal = employees["Team"] == "Legal"
         mktg  = employees["Team"] == "Marketing"
         employees[sales | legal | mktg].head()

Out [41]

 **First Name  Gender Start Date  Salary   Mgmt       Team**
0     Douglas    Male 1993-08-06       0   True  Marketing
5      Dennis    Male 1987-04-18  115163  False      Legal
11      Julie  Female 1997-10-26  102508   True      Legal
13       Gary    Male 2008-01-27  109831  False      Sales
20       Lois     NaN 1995-04-22   64714   True      Legal

虽然这个解决方案是可行的,但它并不具有可扩展性。如果我们的下一个报告要求从 15 个团队而不是三个团队中获取员工呢?为每个条件声明一个Series是费时的。

一个更好的解决方案是isin方法,它接受一个元素的可迭代序列(列表、元组、Series等)并返回一个布尔SeriesTrue表示 pandas 在可迭代序列的值中找到了行的值,而False表示没有找到。当我们有了Series,我们可以用它以通常的方式过滤DataFrame。下一个示例达到相同的结果集:

In  [42] all_star_teams = ["Sales", "Legal", "Marketing"]
         on_all_star_teams = employees["Team"].isin(all_star_teams)
         employees[on_all_star_teams].head()

Out [42]

 **First Name  Gender Start Date  Salary   Mgmt       Team**
0     Douglas    Male 1993-08-06       0   True  Marketing
5      Dennis    Male 1987-04-18  115163  False      Legal
11      Julie  Female 1997-10-26  102508   True      Legal
13       Gary    Male 2008-01-27  109831  False      Sales
20       Lois     NaN 1995-04-22   64714   True      Legal

使用isin方法的最佳情况是我们事先不知道比较集合,例如当它是动态生成的时候。

5.4.2 between方法

当处理数字或日期时,我们经常想要提取位于某个范围内的值。假设我们想要识别所有薪资在$80,000 和$90,000 之间的员工。我们可以创建两个布尔Series,一个用于声明下限,一个用于声明上限。然后我们可以使用&运算符强制两个条件都为True

In  [43] higher_than_80 = employees["Salary"] >= 80000
         lower_than_90 = employees["Salary"] < 90000
         employees[higher_than_80 & lower_than_90].head()

Out [43]

 **First Name  Gender Start Date  Salary   Mgmt         Team**
19      Donna  Female 2010-07-22   81014  False      Product
31      Joyce     NaN 2005-02-20   88657  False      Product
35    Theresa  Female 2006-10-10   85182  False        Sales
45      Roger    Male 1980-04-17   88010   True        Sales
54       Sara  Female 2007-08-15   83677  False  Engineering

一个稍微更简洁的解决方案是使用一个名为between的方法,它接受一个下限和一个上限;它返回一个布尔Series,其中True表示行的值位于指定的区间内。请注意,第一个参数,即下限,是包含的,而第二个参数,即上限,是排除的。以下代码返回与前面代码相同的DataFrame,过滤出在$80,000 和$90,000 之间的薪资:

In  [44] between_80k_and_90k = employees["Salary"].between(80000, 90000)
         employees[between_80k_and_90k].head()

Out [44]

 **First Name  Gender Start Date  Salary   Mgmt         Team**
19      Donna  Female 2010-07-22   81014  False      Product
31      Joyce     NaN 2005-02-20   88657  False      Product
35    Theresa  Female 2006-10-10   85182  False        Sales
45      Roger    Male 1980-04-17   88010   True        Sales
54       Sara  Female 2007-08-15   83677  False  Engineering

between方法也适用于其他数据类型的列。要过滤日期时间,我们可以传递时间范围的起始和结束日期的字符串。该方法的第一个和第二个参数的关键字参数是leftright。在这里,我们找到所有在 20 世纪 80 年代开始与公司合作的员工:

In  [45] eighties_folk = employees["Start Date"].between(
             left = "1980-01-01",
             right = "1990-01-01"
         )

         employees[eighties_folk].head()

Out [45]

 **First Name  Gender Start Date  Salary   Mgmt     Team**
5      Dennis    Male 1987-04-18  115163  False    Legal
6        Ruby  Female 1987-08-17   65476   True  Product
10     Louise  Female 1980-08-12   63241   True      NaN
12    Brandon    Male 1980-12-01  112807   True       HR
17      Shawn    Male 1986-12-07  111737  False  Product

我们还可以将between方法应用于字符串列。让我们提取所有名字以字母"R"开头的员工。我们将以大写字母"R"作为包含的下限,并向上到非包含上限"S"

In  [46] name_starts_with_r = employees["First Name"].between("R", "S")
         employees[name_starts_with_r].head()

Out [46]

 **First Name  Gender Start Date  Salary   Mgmt          Team**
6        Ruby  Female 1987-08-17   65476   True       Product
36     Rachel  Female 2009-02-16  142032  False  Business Dev
45      Roger    Male 1980-04-17   88010   True         Sales
67     Rachel  Female 1999-08-16   51178   True       Finance
78      Robin  Female 1983-06-04  114797   True         Sales

像往常一样,在处理字符和字符串时,请注意大小写敏感性。

5.4.3 isnullnotnull方法

员工数据集包含大量的缺失值。我们可以在前五行中看到一些缺失值:

In  [47] employees.head()

Out [47]

 **First Name  Gender Start Date  Salary   Mgmt       Team**
0    Douglas    Male 1993-08-06       0   True  Marketing
1     Thomas    Male 1996-03-31   61933   True        NaN
2      Maria  Female        NaT  130590  False    Finance
3      Jerry     NaN 2005-03-04  138705   True    Finance
4      Larry    Male 1998-01-24  101004   True         IT

Pandas 用NaN(不是一个数字)标记缺失的文本值和缺失的数值,并用NaT(不是一个时间)标记缺失的日期时间值。我们可以在开始日期列的索引位置 2 看到一个示例。

我们可以使用几个 pandas 方法来隔离给定列中具有 null 或现有值的行。isnull方法返回一个布尔Series,其中True表示某行的值缺失:

In  [48] employees["Team"].isnull().head()

Out [48] 0    False
         1     True
         2    False
         3    False
         4    False
         Name: Team, dtype: bool

Pandas 将NaTNone值视为 null。下一个示例在开始日期列上调用isnull方法:

In  [49] employees["Start Date"].isnull().head()

Out [49] 0    False
         1    False
         2     True
         3    False
         4    False
         Name: Start Date, dtype: bool

notnull方法返回其逆Series,其中True表示某行的值存在。以下输出表明索引 0、2、3 和 4 没有缺失值:

In  [50] employees["Team"].notnull().head()

Out [50] 0     True
         1    False
         2     True
         3     True
         4     True
         Name: Team, dtype: bool

我们可以通过反转isnull方法返回的Series来产生相同的结果集。提醒一下,我们使用波浪符号(~)来反转布尔Series

In  [51] (~employees["Team"].isnull()).head()

Out [51] 0     True
         1    False
         2     True
         3     True
         4     True
         Name: Team, dtype: bool

两种方法都有效,但notnull更具有描述性,因此推荐使用。

和往常一样,我们可以使用这些布尔Series来提取特定的DataFrame行。在这里,我们提取了所有缺少团队值的员工:

In  [52] no_team = employees["Team"].isnull()
         employees[no_team].head()

Out [52]

 **First Name  Gender Start Date  Salary   Mgmt Team**
1      Thomas    Male 1996-03-31   61933   True  NaN
10     Louise  Female 1980-08-12   63241   True  NaN
23        NaN    Male 2012-06-14  125792   True  NaN
32        NaN    Male 1998-08-21  122340   True  NaN
91      James     NaN 2005-01-26  128771  False  NaN

下一个示例提取了具有现有姓氏值的员工:

In  [53] has_name = employees["First Name"].notnull()
         employees[has_name].tail()

Out [53]

 **First Name Gender Start Date  Salary   Mgmt          Team**
995      Henry    NaN 2014-11-23  132483  False  Distribution
996    Phillip   Male 1984-01-31   42392  False       Finance
997    Russell   Male 2013-05-20   96914  False       Product
998      Larry   Male 2013-04-20   60500  False  Business Dev
999     Albert   Male 2012-05-15  129949   True         Sales

isnullnotnull方法是快速过滤一个或多个行中现有和缺失值的最优方式。

5.4.4 处理 null 值

在讨论缺失值的话题上,让我们讨论一些处理它们的方法。在 5.2 节中,我们学习了如何使用fillna方法用常数值替换NaNs。我们也可以删除它们。

让我们通过将数据集恢复到其原始形状来开始本节。我们将使用read_csv函数重新导入 CSV 文件:

In  [54] employees = pd.read_csv(
             "employees.csv", parse_dates = ["Start Date"]
         )

这里是一个提醒,它看起来是这样的:

In  [55] employees

Out [55]

 **First Name  Gender Start Date    Salary   Mgmt          Team**
0       Douglas    Male 1993-08-06       NaN   True     Marketing
1        Thomas    Male 1996-03-31   61933.0   True           NaN
2         Maria  Female        NaT  130590.0  False       Finance
3         Jerry     NaN 2005-03-04  138705.0   True       Finance
4         Larry    Male 1998-01-24  101004.0   True            IT
 ...       ...     ...     ...          ...     ...           ...
996     Phillip    Male 1984-01-31   42392.0  False       Finance
997     Russell    Male 2013-05-20   96914.0  False       Product
998       Larry    Male 2013-04-20   60500.0  False  Business Dev
999      Albert    Male 2012-05-15  129949.0   True         Sales
1000        NaN     NaN        NaT       NaN    NaN           NaN

1001 rows × 6 columns

dropna方法删除包含任何NaN值的DataFrame行。一行缺失多少个值无关紧要;如果存在单个NaN,则方法会排除该行。员工DataFrame在薪资列的索引 0、团队列的索引 1、开始日期列的索引 2 和性别列的索引 3 处有缺失值。注意,pandas 在以下输出中排除了所有这些行:

In  [56] employees.dropna()

Out [56]

 **First Name  Gender Start Date    Salary   Mgmt          Team**
4        Larry    Male 1998-01-24  101004.0   True            IT
5       Dennis    Male 1987-04-18  115163.0  False         Legal
6         Ruby  Female 1987-08-17   65476.0   True       Product
8       Angela  Female 2005-11-22   95570.0   True   Engineering
9      Frances  Female 2002-08-08  139852.0   True  Business Dev
 ...      ...     ...     ...          ...     ...           ...
994     George    Male 2013-06-21   98874.0   True     Marketing
996    Phillip    Male 1984-01-31   42392.0  False       Finance
997    Russell    Male 2013-05-20   96914.0  False       Product
998      Larry    Male 2013-04-20   60500.0  False  Business Dev
999     Albert    Male 2012-05-15  129949.0   True         Sales

761 rows × 6 columns

我们可以将how参数传递一个"all"的参数来删除所有值都缺失的行。数据集中只有最后一行满足这个条件:

In  [57] employees.dropna(how = "all").tail()

Out [57]

 **First Name Gender Start Date    Salary   Mgmt          Team**
995      Henry    NaN 2014-11-23  132483.0  False  Distribution
996    Phillip   Male 1984-01-31   42392.0  False       Finance
997    Russell   Male 2013-05-20   96914.0  False       Product
998      Larry   Male 2013-04-20   60500.0  False  Business Dev
999     Albert   Male 2012-05-15  129949.0   True         Sales

how参数的默认参数是"any"。一个"any"的参数会在某行的任何值缺失时删除该行。注意,在先前的输出中,索引标签 995 的性别列中有NaN。将此输出与以下输出进行比较,其中第 995 行不存在;pandas 仍然删除了最后一行,因为它至少有一个NaN值:

In  [58] employees.dropna(how = "any").tail()

Out [58]

 **First Name Gender Start Date    Salary   Mgmt          Team**
994     George   Male 2013-06-21   98874.0   True     Marketing
996    Phillip   Male 1984-01-31   42392.0  False       Finance
997    Russell   Male 2013-05-20   96914.0  False       Product
998      Larry   Male 2013-04-20   60500.0  False  Business Dev
999     Albert   Male 2012-05-15  129949.0   True         Sales

我们可以使用 subset 参数来针对具有特定列中缺失值的行。下一个示例删除性别列中具有缺失值的行:

In  [59] employees.dropna(subset = ["Gender"]).tail()

Out [59]

 **First Name Gender Start Date    Salary   Mgmt          Team**
994     George   Male 2013-06-21   98874.0   True     Marketing
996    Phillip   Male 1984-01-31   42392.0  False       Finance
997    Russell   Male 2013-05-20   96914.0  False       Product
998      Larry   Male 2013-04-20   60500.0  False  Business Dev
999     Albert   Male 2012-05-15  129949.0   True         Sales

我们还可以将 subset 参数传递给列的列表。如果行在指定的任何列中具有缺失值,pandas 将删除该行。下一个示例删除具有缺失值的开始日期列、薪资列或两者的行:

In  [60] employees.dropna(subset = ["Start Date", "Salary"]).head()

Out [60]

 **First Name  Gender Start Date    Salary   Mgmt     Team**
1     Thomas    Male 1996-03-31   61933.0   True      NaN
3      Jerry     NaN 2005-03-04  138705.0   True  Finance
4      Larry    Male 1998-01-24  101004.0   True       IT
5     Dennis    Male 1987-04-18  115163.0  False    Legal
6       Ruby  Female 1987-08-17   65476.0   True  Product

thresh 参数指定一行必须具有的最小非空值阈值,以便 pandas 保留该行。下一个示例筛选 employees 以获取至少有四个现有值的行:

In  [61] employees.dropna(how = "any", thresh = 4).head()

Out [61]

 **First Name  Gender Start Date    Salary   Mgmt       Team**
0    Douglas    Male 1993-08-06       NaN   True  Marketing
1     Thomas    Male 1996-03-31   61933.0   True        NaN
2      Maria  Female        NaT  130590.0  False    Finance
3      Jerry     NaN 2005-03-04  138705.0   True    Finance
4      Larry    Male 1998-01-24  101004.0   True         IT

当一定数量的缺失值使行对分析无用时,thresh 参数非常出色。

5.5 处理重复项

缺失值在杂乱的数据集中很常见,重复值也是如此。幸运的是,pandas 包含了几个用于识别和排除重复值的方法。

5.5.1 重复项方法

首先,这是一个关于团队列前五行的快速提醒。注意,值 "Finance" 出现在索引位置 2 和 3:

In  [62] employees["Team"].head()

Out [62] 0    Marketing
         1          NaN
         2      Finance
         3      Finance
         4           IT
         Name: Team, dtype: object

duplicated 方法返回一个布尔 Series,用于识别列中的重复值。Pandas 在 Series 中遇到之前遇到的任何值时返回 True。考虑下一个示例。duplicated 方法将团队列中 "Finance" 的第一次出现标记为非重复(False)。它将 "Finance" 的所有后续出现标记为重复(True)。相同的逻辑适用于所有其他团队值:

In  [63] employees["Team"].duplicated().head()

Out [63] 0    False
         1    False
         2    False
         3     True
         4    False
         Name: Team, dtype: bool

duplicated 方法的 keep 参数告诉 pandas 保留哪个重复出现的值。它的默认参数 "first" 保留每个重复值的第一次出现。以下代码与前面的代码等价:

In  [64] employees["Team"].duplicated(keep = "first").head()

Out [64] 0    False
         1    False
         2    False
         3     True
         4    False
         Name: Team, dtype: bool

我们还可以要求 pandas 将列中值的最后一次出现标记为非重复。将 "last" 字符串传递给 keep 参数:

In  [65] employees["Team"].duplicated(keep = "last")

Out [65] 0        True
         1        True
         2        True
         3        True
         4        True
                 ...
         996     False
         997     False
         998     False
         999     False
         1000    False
         Name: Team, Length: 1001, dtype: bool

假设我们想要从每个团队中提取一名员工。我们可以使用的策略是提取每个独特团队在团队列中的第一行。我们现有的 duplicated 方法返回一个布尔 SeriesTrue 识别第一次出现后的所有重复值。如果我们反转这个 Series,我们将得到一个 Series,其中 True 表示 pandas 首次遇到该值:

In  [66] (~employees["Team"].duplicated()).head()

Out [66] 0     True
         1     True
         2     True
         3    False
         4     True
         Name: Team, dtype: bool

现在,我们可以通过传递方括号内的布尔 Series 来按团队提取一名员工。Pandas 将包含具有团队列中值第一次出现的行。请注意,库将 NaNs 视为唯一值:

In  [67] first_one_in_team = ~employees["Team"].duplicated()
         employees[first_one_in_team]

Out [67]

 **First Name  Gender Start Date    Salary   Mgmt          Team**
0     Douglas    Male 1993-08-06       NaN   True     Marketing
1      Thomas    Male 1996-03-31   61933.0   True           NaN
2       Maria  Female        NaT  130590.0  False       Finance
4       Larry    Male 1998-01-24  101004.0   True            IT
5      Dennis    Male 1987-04-18  115163.0  False         Legal
6        Ruby  Female 1987-08-17   65476.0   True       Product
8      Angela  Female 2005-11-22   95570.0   True   Engineering
9     Frances  Female 2002-08-08  139852.0   True  Business Dev
12    Brandon    Male 1980-12-01  112807.0   True            HR
13       Gary    Male 2008-01-27  109831.0  False         Sales
40    Michael    Male 2008-10-10   99283.0   True  Distribution

这个输出告诉我们,Douglas 是数据集中营销团队的第一个员工,Thomas 是第一个缺少团队的人,Maria 是财务团队的第一个员工,等等。

5.5.2 删除重复项方法

DataFramedrop_duplicates 方法提供了一个方便的快捷方式来完成 5.5.1 节中的操作。默认情况下,该方法会删除所有值都等于之前遇到的一行中的值的行。没有所有六个行值都相等的 employees 行,所以使用标准调用方法,该方法对我们来说并没有做什么:

In  [68] employees.drop_duplicates()

Out [68]

 **First Name  Gender Start Date    Salary   Mgmt          Team**
0       Douglas    Male 1993-08-06       NaN   True     Marketing
1        Thomas    Male 1996-03-31   61933.0   True           NaN
2         Maria  Female        NaT  130590.0  False       Finance
3         Jerry     NaN 2005-03-04  138705.0   True       Finance
4         Larry    Male 1998-01-24  101004.0   True            IT
 ...       ...     ...     ...          ...     ...           ...
996     Phillip    Male 1984-01-31   42392.0  False       Finance
997     Russell    Male 2013-05-20   96914.0  False       Product
998       Larry    Male 2013-04-20   60500.0  False  Business Dev
999      Albert    Male 2012-05-15  129949.0   True         Sales
1000        NaN     NaN        NaT       NaN    NaN           NaN

1001 rows × 6 columns

但我们可以向该方法传递一个 subset 参数,其中包含 pandas 应该用来确定行唯一性的列的列表。下一个示例找到 Team 列中每个唯一值的第一个出现。换句话说,pandas 只保留具有 Team 值(例如 "Marketing")的第一个出现的行。它排除了第一个之后具有重复 Team 值的所有行:

In  [69] employees.drop_duplicates(subset = ["Team"])

Out [69]

 **First Name  Gender Start Date    Salary   Mgmt          Team**
0     Douglas    Male 1993-08-06       NaN   True     Marketing
1      Thomas    Male 1996-03-31   61933.0   True           NaN
2       Maria  Female        NaT  130590.0  False       Finance
4       Larry    Male 1998-01-24  101004.0   True            IT
5      Dennis    Male 1987-04-18  115163.0  False         Legal
6        Ruby  Female 1987-08-17   65476.0   True       Product
8      Angela  Female 2005-11-22   95570.0   True   Engineering
9     Frances  Female 2002-08-08  139852.0   True  Business Dev
12    Brandon    Male 1980-12-01  112807.0   True            HR
13       Gary    Male 2008-01-27  109831.0  False         Sales
40    Michael    Male 2008-10-10   99283.0   True  Distribution

drop_duplicates 方法还接受一个 keep 参数。我们可以传递一个 "last" 参数来保留每个重复值的最后出现行。这些行可能更接近数据集的末尾。在下面的示例中,Alice 是数据集中 HR 团队中的最后一名员工,Justin 是 Legal 团队中的最后一名员工,依此类推:

In  [70] employees.drop_duplicates(subset = ["Team"], keep = "last")

Out [70]

 **First Name  Gender Start Date    Salary   Mgmt          Team**
988       Alice  Female 2004-10-05   47638.0  False            HR
989      Justin     NaN 1991-02-10   38344.0  False         Legal
990       Robin  Female 1987-07-24  100765.0   True            IT
993        Tina  Female 1997-05-15   56450.0   True   Engineering
994      George    Male 2013-06-21   98874.0   True     Marketing
995       Henry     NaN 2014-11-23  132483.0  False  Distribution
996     Phillip    Male 1984-01-31   42392.0  False       Finance
997     Russell    Male 2013-05-20   96914.0  False       Product
998       Larry    Male 2013-04-20   60500.0  False  Business Dev
999      Albert    Male 2012-05-15  129949.0   True         Sales
1000        NaN     NaN        NaT       NaN    NaN           NaN

对于 keep 参数还有一个额外的选项。我们可以传递一个 False 参数来排除所有具有重复值的行。如果存在任何其他具有相同值的行,Pandas 将拒绝该行。下一个示例筛选出 employees 中在 First Name 列中具有唯一值的行。换句话说,这些名字在 DataFrame 中只出现一次:

In  [71] employees.drop_duplicates(subset = ["First Name"], keep = False)

Out [71]

 **First Name  Gender Start Date    Salary   Mgmt          Team**
5       Dennis    Male 1987-04-18  115163.0  False         Legal
8       Angela  Female 2005-11-22   95570.0   True   Engineering
33        Jean  Female 1993-12-18  119082.0  False  Business Dev
190      Carol  Female 1996-03-19   57783.0  False       Finance
291      Tammy  Female 1984-11-11  132839.0   True            IT
495     Eugene    Male 1984-05-24   81077.0  False         Sales
688      Brian    Male 2007-04-07   93901.0   True         Legal
832      Keith    Male 2003-02-12  120672.0  False         Legal
887      David    Male 2009-12-05   92242.0  False         Legal

假设我们想要通过多个列的组合来识别重复项。我们可能想要找到数据集中具有唯一 First Name 和 Gender 组合的每个员工的第一个出现,例如。为了参考,这里是一个具有 "Douglas" 名字和 "Male" 性别的所有员工的子集:

In  [72] name_is_douglas = employees["First Name"] == "Douglas"
         is_male = employees["Gender"] == "Male"
         employees[name_is_douglas & is_male]

Out [72]

 **First Name Gender Start Date    Salary   Mgmt         Team**
0      Douglas   Male 1993-08-06       NaN   True    Marketing
217    Douglas   Male 1999-09-03   83341.0   True           IT
322    Douglas   Male 2002-01-08   41428.0  False      Product
835    Douglas   Male 2007-08-04  132175.0  False  Engineering

我们可以将列的列表传递给 drop_duplicates 方法的 subset 参数。Pandas 将使用这些列来确定重复项的存在。下一个示例使用性别和 Team 列中的值的组合来识别重复项:

In  [73] employees.drop_duplicates(subset = ["Gender", "Team"]).head()

Out [73]

 **First Name  Gender Start Date    Salary   Mgmt       Team**
0    Douglas    Male 1993-08-06       NaN   True  Marketing
1     Thomas    Male 1996-03-31   61933.0   True        NaN
2      Maria  Female        NaT  130590.0  False    Finance
3      Jerry     NaN 2005-03-04  138705.0   True    Finance
4      Larry    Male 1998-01-24  101004.0   True         IT

让我们浏览一下输出。索引为 0 的行持有 employees 数据集中 "Douglas""Male" 性别的第一个出现。Pandas 将排除任何具有相同两个值的其他行。为了澄清,如果某行具有 "Douglas" 名字和不同的 "Male" 性别,库仍然会包括该行。同样,它也会包括具有 "Male" 性别和不同的 "Douglas" 名字的行。Pandas 使用两个列的值组合来识别重复项。

5.6 编程挑战

这是您练习本章引入的概念的机会。

5.6.1 问题

netflix.csv 数据集是 Netflix 视频流媒体服务在 2019 年 11 月可观看的近 6,000 个标题的集合。它包括四个列:视频的标题、导演、Netflix 添加它的日期以及它的类型/类别。导演和 date_added 列包含缺失值。我们可以在以下输出的索引位置 0、2 和 5836 中看到示例:

In  [74] pd.read_csv("netflix.csv")

Out [74]

 **title        director date_added     type**
0               Alias Grace             NaN   3-Nov-17  TV Show
1            A Patch of Fog  Michael Lennox  15-Apr-17    Movie
2                  Lunatics             NaN  19-Apr-19  TV Show
3                 Uriyadi 2     Vijay Kumar   2-Aug-19    Movie
4         Shrek the Musical     Jason Moore  29-Dec-13    Movie
   ...            ...               ...          ...        ...
5832            The Pursuit     John Papola   7-Aug-19    Movie
5833       Hurricane Bianca   Matt Kugelman   1-Jan-17    Movie
5834           Amar's Hands  Khaled Youssef  26-Apr-19    Movie
5835  Bill Nye: Science Guy  Jason Sussberg  25-Apr-18    Movie
5836           Age of Glory             NaN        NaN  TV Show

5837 rows × 4 columns

使用本章学到的技能,解决以下挑战:

  1. 优化数据集以减少内存使用并最大化实用性。

  2. 查找所有标题为"Limitless"的行。

  3. 查找所有导演为"Robert Rodriguez"且类型为"Movie"的行。

  4. 查找所有date_added值为"2019-07-31"或导演为"Robert" Altman的行。

  5. 查找所有导演为"Orson Welles""Aditya" Kripalani"Sam Raimi"`的行。

  6. 查找所有date_added值在 2019 年 5 月 1 日至 2019 年 6 月 1 日之间的行。

  7. 删除导演列中所有包含NaN值的行。

  8. 确定 Netflix 在其目录中仅添加了一部电影的日子。

5.6.2 解决方案

让我们解决这些问题!

  1. 为了优化数据集以节省内存和提高实用性,我们首先可以将date_added列的值转换为日期时间格式。我们可以在导入时通过将parse_dates参数传递给read_csv函数来强制类型转换:

    In  [75] netflix = pd.read_csv("netflix.csv", parse_dates = ["date_added"])
    

    保持基准很重要,让我们看看当前的内存使用情况:

    In  [76] netflix.info()
    
    Out [76]
    
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 5837 entries, 0 to 5836
    Data columns (total 4 columns):
     #   Column      Non-Null Count  Dtype
    ---  ------      --------------  -----
     0   title       5837 non-null   object
     1   director    3936 non-null   object
     2   date_added  5195 non-null   datetime64[ns]
     3   type        5837 non-null   object
    dtypes: datetime64ns, object(3)
    memory usage: 182.5+ KB
    
    

    我们能否将任何列的值转换为不同的数据类型?比如分类值?让我们使用nunique方法来计算每列的唯一值数量:

    In  [77] netflix.nunique()
    
    Out [77] title         5780
             director      3024
             date_added    1092
             type             2
             dtype: int64
    
    

    类型列是分类值的完美候选者。在一个包含 5,837 行的数据集中,它只有两个唯一值:"Movie""TV Show"。我们可以使用astype方法转换其值。请记住覆盖原始Series

    In  [78] netflix["type"] = netflix["type"].astype("category")
    

    将数据转换为分类数据后,我们的内存使用减少了多少?惊人的 22%:

    In  [79] netflix.info()
    
    Out [79]
    
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 5837 entries, 0 to 5836
    Data columns (total 4 columns):
     #   Column      Non-Null Count  Dtype
    ---  ------      --------------  -----
     0   title       5837 non-null   object
     1   director    3936 non-null   object
     2   date_added  5195 non-null   datetime64[ns]
     3   type        5837 non-null   category
    dtypes: category(1), datetime64ns, object(2)
    memory usage: 142.8+ KB
    
    
  2. 我们需要使用等号运算符来比较每个标题列的值与字符串"Limitless"。之后,我们可以使用布尔Seriesnetflix中提取返回True的行:

    In  [80] netflix[netflix["title"] == "Limitless"]
    
    Out [80]
    
     **title         director date_added     type**
    1559  Limitless      Neil Burger 2019-05-16    Movie
    2564  Limitless              NaN 2016-07-01  TV Show
    4579  Limitless  Vrinda Samartha 2019-10-01    Movie
    
    
  3. 为了提取由 Robert Rodriguez 执导的电影,我们需要两个布尔Series,一个比较导演列的值与"Robert Rodriguez",另一个比较类型列的值与"Movie"&符号应用于两个布尔SeriesAND逻辑:

    In  [81] directed_by_robert_rodriguez = (
                 netflix["director"] == "Robert Rodriguez"
             )
             is_movie = netflix["type"] == "Movie"
             netflix[directed_by_robert_rodriguez & is_movie]
    
    Out [81]
    
     **title          director date_added     type**
    1384    Spy Kids: All the Time in the ...  Robert Rodriguez 2019-02-19  Movie
    1416                  Spy Kids 3: Game...  Robert Rodriguez 2019-04-01  Movie
    1460  Spy Kids 2: The Island of Lost D...  Robert Rodriguez 2019-03-08  Movie
    2890                           Sin City    Robert Rodriguez 2019-10-01  Movie
    3836                             Shorts    Robert Rodriguez 2019-07-01  Movie
    3883                           Spy Kids    Robert Rodriguez 2019-04-01  Movie
    
    
  4. 下一个问题要求所有标题为"2019-07-31"或导演为"Robert Altman"的标题。这个问题与上一个问题类似,但需要|符号进行OR逻辑:

    In  [82] added_on_july_31 = netflix["date_added"] == "2019-07-31"
             directed_by_altman = netflix["director"] == "Robert Altman"
             netflix[added_on_july_31 | directed_by_altman]
    
    Out [82]
     **title       director date_added    type**
    611                            Popeye  Robert Altman 2019-11-24   Movie
    1028        The Red Sea Diving Resort    Gideon Raff 2019-07-31   Movie
    1092                     Gosford Park  Robert Altman 2019-11-01   Movie
    3473  Bangkok Love Stories: Innocence            NaN 2019-07-31 TV Show
    5117                       Ramen Shop      Eric Khoo 2019-07-31   Movie
    
    
  5. 下一个挑战要求找到导演为"Orson Welles""Aditya" Kripalani"Sam Raimi"的条目。一个选项是创建三个布尔Series,每个导演一个,然后使用|运算符。但生成布尔Series的更简洁和可扩展的方法是在导演列上调用isin`方法并传入导演列表:

    In  [83] directors = ["Orson Welles", "Aditya Kripalani", "Sam Raimi"]
             target_directors = netflix["director"].isin(directors)
             netflix[target_directors]
    
    Out [83]
    
     **title          director date_added   type**
    946                 The Stranger      Orson Welles 2018-07-19  Movie
    1870                    The Gift         Sam Raimi 2019-11-20  Movie
    3706                Spider-Man 3         Sam Raimi 2019-11-01  Movie
    4243        Tikli and Laxmi Bomb  Aditya Kripalani 2018-08-01  Movie
    4475  The Other Side of the Wind      Orson Welles 2018-11-02  Movie
    5115    Tottaa Pataaka Item Maal  Aditya Kripalani 2019-06-25  Movie
    
    
  6. 要找到所有date_added值在 2019 年 5 月 1 日和 2019 年 6 月 1 日之间的行,最简洁的方法是使用between方法。我们可以提供这两个日期作为下限和上限。这种方法消除了需要两个单独的布尔Series的需求:

    In  [84] may_movies = netflix["date_added"].between(
                 "2019-05-01", "2019-06-01"
             )
    
             netflix[may_movies].head()
    
    Out [84]
    
     **title      director date_added     type**
    29            Chopsticks  Sachin Yardi 2019-05-31    Movie
    60        Away From Home           NaN 2019-05-08  TV Show
    82   III Smoking Barrels    Sanjib Dey 2019-06-01    Movie
    108            Jailbirds           NaN 2019-05-10  TV Show
    124              Pegasus       Han Han 2019-05-31    Movie
    
    
  7. dropna方法删除具有缺失值的DataFrame行。我们必须包含subset参数以限制 pandas 应查找空值的列。对于这个问题,我们将针对导演列中的NaN:

    In  [85] netflix.dropna(subset = ["director"]).head()
    
    Out [85]
    
     **title        director date_added   type**
    1                      A Patch of Fog  Michael Lennox 2017-04-15  Movie
    3                           Uriyadi 2     Vijay Kumar 2019-08-02  Movie
    4                   Shrek the Musical     Jason Moore 2013-12-29  Movie
    5                    Schubert In Love     Lars Büchel 2018-03-01  Movie
    6  We Have Always Lived in the Castle   Stacie Passon 2019-09-14  Movie
    
    
  8. 最后的挑战要求识别 Netflix 仅在服务中添加一部电影的日子。一个解决方案是认识到date_added列包含同一日添加的标题的重复日期值。我们可以使用drop_duplicates方法,并设置subsetdate_added以及keep参数为False。Pandas 将删除date_added列中任何重复条目的行。结果DataFrame将包含在其各自日期上仅添加的标题:

    In  [86] netflix.drop_duplicates(subset = ["date_added"], keep = False)
    
    Out [86]
    
     **title         director date_added   type**
    4                      Shrek the Musical      Jason Moore 2013-12-29  Movie
    12                         Without Gorky   Cosima Spender 2017-05-31  Movie
    30            Anjelah Johnson: Not Fancy        Jay Karas 2015-10-02  Movie
    38                        One Last Thing      Tim Rouhana 2019-08-25  Movie
    70    Marvel's Iron Man & Hulk: Heroes ...      Leo Riley 2014-02-16  Movie
     ...                              ...             ...          ...      ...
    5748                             Menorca     John Barnard 2017-08-27  Movie
    5749                          Green Room  Jeremy Saulnier 2018-11-12  Movie
    5788     Chris Brown: Welcome to My Life   Andrew Sandler 2017-10-07  Movie
    5789             A Very Murray Christmas    Sofia Coppola 2015-12-04  Movie
    5812            Little Singham in London    Prakash Satam 2019-04-22  Movie
    
    391 rows × 4 columns
    
    

恭喜你完成了编码挑战!

摘要

  • astype方法将Series的值转换为另一种数据类型。

  • 当一个Series具有少量唯一值时,category数据类型是理想的。

  • Pandas 可以根据一个或多个条件从DataFrame中提取数据子集。

  • 将一个布尔Series放在方括号内以提取DataFrame的子集。

  • 使用相等、不等和数学运算符将每个Series条目与一个常数值进行比较。

  • &符号强制要求满足多个条件才能提取一行。

  • |符号强制要求满足任一条件才能提取一行。

  • isnullnotnullbetweenduplicated这样的辅助方法返回布尔Series,我们可以使用它们来过滤数据集。

  • fillna方法用常数值替换NaNs

  • dropna方法删除具有空值的行。我们可以自定义其参数以针对所有或某些列中的缺失值。

第二部分. 应用 pandas

在第一部分,我们为掌握 pandas 打下了基础。现在我们已经熟悉了SeriesDataFrame的使用,我们可以拓宽视野,学习如何解决数据分析中的常见问题。第六章直接进入处理杂乱文本数据,包括处理空白字符和不一致的字符大小写。在第七章中,我们学习如何使用强大的MultiIndex来存储和提取层次数据。第八章和第九章专注于聚合:旋转我们的DataFrame,将数据分组到桶中,总结数据等等。在第十章中,我们探索了如何使用各种连接来合并数据集。紧接着,在第十一章中,我们学习如何处理另一种常见的数据类型,即日期时间。在第十二章中,我们查看如何将数据集导入和导出到 pandas。第十三章涵盖了如何调整库的配置设置。最后,第十四章提供了一个从我们的DataFrame创建可视化的教程。

在这个过程中,我们将使用超过 30 个数据集来练习 pandas 概念,这些数据集涵盖了从婴儿名字到早餐谷物,从《财富》1000 强公司到诺贝尔奖获得者的一切内容。欢迎您按章节顺序阅读,或者探索您最感兴趣的主题。请将这里的每一章视为一个新专业,以添加到您的 pandas 工具箱中。祝您好运!

6 处理文本数据

本章涵盖

  • 从字符串中移除空白字符

  • 将字符串转换为大写或小写

  • 在字符串中查找和替换字符

  • 通过字符索引位置切片字符串

  • 通过分隔符拆分文本

文本数据可能会非常混乱。现实世界的数据集充满了错误的字符、不正确的字母大小写、空白字符等等。清理数据的过程被称为整理修补。通常,我们的大部分数据分析都致力于修补。我们可能一开始就知道我们想要得出的见解,但困难在于将数据整理成适合操作的形式。幸运的是,pandas 背后的一个主要动机是简化清理格式不正确的文本值的过程。这个库经过实战检验且灵活。在本章中,我们将学习如何使用 pandas 来修复我们文本数据集中的各种缺陷。有很多内容要介绍,所以让我们直接进入正题。

6.1 字母大小写和空白字符

我们将首先在新的 Jupyter Notebook 中导入 pandas:

In  [1] import pandas as pd

本章的第一个数据集,chicago_food_inspections.csv,是芝加哥市进行的超过 150,000 次食品检查的列表。CSV 文件只包含两列:一列是机构的名称,另一列是风险评级。四个风险等级是风险 1(高)、风险 2(中)、风险 3(低),以及针对最严重违规者的特殊等级“所有”:

In  [2] inspections = pd.read_csv("chicago_food_inspections.csv")
        inspections

Out [2]

 **Name             Risk**
0                  MARRIOT MARQUIS CHICAGO    Risk 1 (High)
1                               JETS PIZZA  Risk 2 (Medium)
2                                ROOM 1520     Risk 3 (Low)
3                  MARRIOT MARQUIS CHICAGO    Risk 1 (High)
4                               CHARTWELLS    Risk 1 (High)
     ...                                  ...                 ...
153805                           WOLCOTT'S    Risk 1 (High)
153806        DUNKIN DONUTS/BASKIN-ROBBINS  Risk 2 (Medium)
153807                            Cafe 608    Risk 1 (High)
153808                         mr.daniel's    Risk 1 (High)
153809                          TEMPO CAFE    Risk 1 (High)

153810 rows × 2 columns

注意:chicago_food_inspections.csv 是芝加哥市提供的数据集的一个修改版本(mng.bz/9N60)。数据中存在拼写错误和不一致性;我们保留了它们,以便您可以看到现实世界中出现的各种数据不规则性。我鼓励您考虑如何使用本章中学习的技巧来优化这些数据。

我们立即在名称列中看到一个问题:字母大小写不一致。大多数行值是大写的,一些是小写的("mr.daniel's"),还有一些是正常情况("Café 608")。

前面的输出没有显示inspections中隐藏的另一个问题:名称列的值被空白字符包围。如果我们使用方括号语法单独检查名称Series,我们可以更容易地发现额外的间隔。注意行尾没有对齐:

In  [3] inspections["Name"].head()

Out [3] 0     MARRIOT MARQUIS CHICAGO   
        1                    JETS PIZZA 
        2                     ROOM 1520 
        3      MARRIOT MARQUIS CHICAGO  
        4                  CHARTWELLS   
        Name: Name, dtype: object

我们可以使用Series上的values属性来获取存储值的底层 NumPy ndarray。空白字符出现在值的开始和结束处:

In  [4] inspections["Name"].head().values

Out [4] array([' MARRIOT MARQUIS CHICAGO   ', ' JETS PIZZA ',
               '   ROOM 1520 ', '  MARRIOT MARQUIS CHICAGO  ',
               ' CHARTWELLS   '], dtype=object)

让我们先关注空白字符。我们稍后会处理字母大小写问题。

Series对象的str属性暴露了一个StringMethods对象,这是一个强大的字符串处理方法工具箱:

In  [5] inspections["Name"].str
Out [5] <pandas.core.strings.StringMethods at 0x122ad8510>

每当我们想要执行字符串操作时,我们都会在StringMethods对象上调用一个方法,而不是在Series本身上。一些方法类似于 Python 的本地字符串方法,而其他方法则是 pandas 独有的。有关 Python 字符串方法的全面回顾,请参阅附录 B。

我们可以使用strip方法族来从字符串中删除空白字符。lstrip(左删除)方法从字符串的开始处删除空白字符。以下是一个基本示例:

In  [6] dessert = "  cheesecake  "
        dessert.lstrip()

Out [6] 'cheesecake  '

rstrip(右删除)方法从字符串的末尾删除空白字符:

In  [7] dessert.rstrip()

Out [7] '  cheesecake'

strip方法从字符串的两端删除空白字符:

In  [8] dessert.strip()

Out [8] 'cheesecake'

这三个strip方法都可在StringMethods对象上使用。每个方法都会返回一个新的Series对象,其中每个列值都应用了该操作。让我们调用它们:

In  [9] inspections["Name"].str.lstrip().head()

Out [9] 0    MARRIOT MARQUIS CHICAGO   
        1                   JETS PIZZA 
        2                    ROOM 1520 
        3     MARRIOT MARQUIS CHICAGO  
        4                 CHARTWELLS   
        Name: Name, dtype: object
In  [10] inspections["Name"].str.rstrip().head()

Out [10] 0      MARRIOT MARQUIS CHICAGO
         1                   JETS PIZZA
         2                    ROOM 1520
         3      MARRIOT MARQUIS CHICAGO
         4                   CHARTWELLS
         Name: Name, dtype: object

In  [11] inspections["Name"].str.strip().head()

Out [11] 0    MARRIOT MARQUIS CHICAGO
         1                 JETS PIZZA
         2                  ROOM 1520
         3    MARRIOT MARQUIS CHICAGO
         4                 CHARTWELLS
         Name: Name, dtype: object

现在,我们可以用没有额外空白的新Series覆盖现有的Series。在等号的右侧,我们将使用strip代码创建新的Series。在等号的左侧,我们将使用方括号语法来表示我们想要覆盖的列。Python 首先处理等号右侧的部分。总之,我们使用“名称”列创建一个没有空白的新Series,然后用这个新Series覆盖“名称”列:

In  [12] inspections["Name"] = inspections["Name"].str.strip()

这个一行解决方案适用于小数据集,但对于拥有大量列的数据集来说可能会很快变得繁琐。我们如何快速将相同的逻辑应用到所有DataFrame列上?你可能还记得columns属性,它公开了包含DataFrame列名的可迭代Index对象:

In  [13] inspections.columns

Out [13] Index(['Name', 'Risk'], dtype='object')

我们可以使用 Python 的for循环遍历每一列,从DataFrame中动态提取它,调用str.strip方法返回一个新的Series,并覆盖原始列。这个逻辑只需要两行代码:

In  [14] for column in inspections.columns:
             inspections[column] = inspections[column].str.strip()

Python 的所有字符大小写方法都可在StringMethods对象上使用。例如,lower方法会将所有字符串字符转换为小写:

In  [15] inspections["Name"].str.lower().head()

Out [15] 0    marriot marquis chicago
         1                 jets pizza
         2                  room 1520
         3    marriot marquis chicago
         4                 chartwells
         Name: Name, dtype: object

相反的str.upper方法返回一个包含大写字符串的Series。下一个示例在另一个Series上调用该方法,因为“名称”列已经大多是 uppercase 的:

In  [16] steaks = pd.Series(["porterhouse", "filet mignon", "ribeye"])
         steaks

Out [16] 0     porterhouse
         1    filet mignon
         2          ribeye
         dtype: object

In  [17] steaks.str.upper()

Out [17] 0     PORTERHOUSE
         1    FILET MIGNON
         2          RIBEYE
         dtype: object

假设我们想要以更标准、可读的格式获取机构的名称。我们可以使用str.capitalize方法将Series中每个字符串的第一个字母大写:

In  [18] inspections["Name"].str.capitalize().head()

Out [18] 0    Marriot marquis chicago
         1                 Jets pizza
         2                  Room 1520
         3    Marriot marquis chicago
         4                 Chartwells
         Name: Name, dtype: object

这是一个正确的步骤,但可能最好的方法是str.title,它将每个单词的第一个字母大写。Pandas 使用空格来识别一个单词的结束和下一个单词的开始:

In  [19] inspections["Name"].str.title().head()

Out [19] 0    Marriot Marquis Chicago
         1                 Jets Pizza
         2                  Room 1520
         3    Marriot Marquis Chicago
         4                 Chartwells
         Name: Name, dtype: object

title方法是一个处理地点、国家、城市和人们全名的绝佳选项。

6.2 字符串切片

让我们把注意力转向“风险”列。每一行的值都包含风险的数值和分类表示(例如 1 和"高")。以下是该列的提醒:

In  [20] inspections["Risk"].head()

Out [20]

0      Risk 1 (High)
1    Risk 2 (Medium)
2       Risk 3 (Low)
3      Risk 1 (High)
4      Risk 1 (High)
Name: Risk, dtype: object

假设我们想从每一行中提取数值风险值。鉴于每行看似一致的格式,这个操作可能看起来很简单,但我们必须小心行事。在一个如此大的数据集中,总会有欺骗的空间:

In  [21] len(inspections)

Out [21] 153810

所有行都遵循"风险" 数字 "(风险 等级)"格式吗?我们可以通过调用unique方法来找出答案,该方法返回一个包含列唯一值的 NumPy ndarray

In  [22] inspections["Risk"].unique()

Out [22] array(['Risk 1 (High)', 'Risk 2 (Medium)', 'Risk 3 (Low)', 'All',
                nan], dtype=object)

我们必须考虑两个额外的值:缺失的NaNs'全部'字符串。我们如何处理这些值最终取决于分析师和业务。这些值是否重要,或者可以丢弃?在这种情况下,让我们提出一个折衷方案:我们将删除缺失的NaN值,并将"全部"值替换为"风险 4 (极端)"。我们将选择这种方法以确保所有风险值具有一致的格式。

我们可以使用第五章中引入的dropna方法从Series中删除缺失值。我们将传递其subset参数一个包含DataFrame列的列表,pandas 将在这个列表中查找NaNs。下一个示例从inspections中删除风险列中包含NaN值的行:

In  [23] inspections = inspections.dropna(subset = ["Risk"])

让我们检查风险列中的唯一值:

In  [24] inspections["Risk"].unique()

Out [24] array(['Risk 1 (High)', 'Risk 2 (Medium)', 'Risk 3 (Low)', 'All'],
                dtype=object)

我们可以使用DataFrame的有用replace方法将一个值的所有出现替换为另一个值。该方法的第一参数to_replace设置要搜索的值,第二个参数value指定将每个出现替换为什么值。下一个示例将"全部"字符串值替换为"风险 4 (极端)"

In  [25] inspections = inspections.replace(
             to_replace = "All", value = "Risk 4 (Extreme)"
         )

现在风险列中的所有值都有了一致的格式:

In  [26] inspections["Risk"].unique()

Out [26] array(['Risk 1 (High)', 'Risk 2 (Medium)', 'Risk 3 (Low)',
                'Risk 4 (Extreme)'], dtype=object)

接下来,让我们继续我们的原始目标,即提取每一行的风险数字。

6.3 字符串切片和字符替换

我们可以在StringMethods对象上使用slice方法通过索引位置提取字符串的子串。该方法接受起始索引和结束索引作为参数。下限(起点)是包含的,而上限(终点)是排除的。

我们的风险数字从每个字符串的索引位置 5 开始。下一个示例从索引位置 5 开始提取字符,直到(但不包括)索引位置 6:

In  [27] inspections["Risk"].str.slice(5, 6).head()

Out [27] 0    1
         1    2
         2    3
         3    1
         4    1
         Name: Risk, dtype: object

我们还可以用 Python 的列表切片语法(见附录 B)替换slice方法。以下代码返回与前面代码相同的结果:

In  [28] inspections["Risk"].str[5:6].head()

Out [28] 0    1
         1    2
         2    3
         3    1
         4    1
         Name: Risk, dtype: object

如果我们想从每一行中提取分类排名("高""中""低""全部"),这个挑战由于单词长度的不同而变得困难;我们不能从起始索引位置提取相同数量的字符。有几个解决方案可用。我们将讨论最健壮的选项,正则表达式,在第 6.7 节中。

现在,让我们一步一步地解决这个问题。我们可以从使用slice方法提取每行的风险类别开始。如果我们向slice方法传递一个单一值,pandas 将使用它作为下限,并提取到字符串的末尾。

以下示例从每个字符串的索引位置 8 开始提取字符,直到字符串的末尾。索引位置 8 的字符是每种风险类型的第一个字母(例如,"High"中的"H",“Medium”中的"M""Low"中的"L",以及"Extreme"中的"E"):

In  [29] inspections["Risk"].str.slice(8).head()

Out [29] 0      High)
         1    Medium)
         2       Low)
         3      High)
         4      High)
         Name: Risk, dtype: object

我们也可以使用 Python 的列表切片语法。在方括号内,提供一个起始索引位置,后跟一个单冒号。结果是相同的:

In  [30] inspections["Risk"].str[8:].head()

Out [30] 0      High)
         1    Medium)
         2       Low)
         3      High)
         4      High)
         Name: Risk, dtype: object

我们仍然需要处理那些讨厌的闭合括号。这里有一个酷解决方案:将负数参数传递给 str.slice 方法。负数参数设置索引界限相对于字符串的末尾:-1 提取到最后一个字符,-2 提取到倒数第二个字符,依此类推。让我们从索引位置 8 提取到每个字符串的最后一个字符:

In  [31] inspections["Risk"].str.slice(8, -1).head()

Out [31] 0      High
         1    Medium
         2       Low
         3      High
         4      High
         Name: Risk, dtype: object

我们做到了!如果你更喜欢列表切片语法,你可以在方括号内冒号后面传递 -1:

In  [32] inspections["Risk"].str[8:-1].head()

Out [32] 0      High
         1    Medium
         2       Low
         3      High
         4      High
         Name: Risk, dtype: object

另一种移除闭合括号的策略是使用 str.replace 方法。我们可以将每个闭合括号替换为一个空字符串——一个没有字符的字符串。

每个 str 方法都会返回一个新的 Series 对象,并具有自己的 str 属性。这一特性允许我们在调用每个方法时引用 str 属性,从而按顺序链式调用多个字符串方法。以下示例展示了如何链式调用 slicereplace 方法:

In  [33] inspections["Risk"].str.slice(8).str.replace(")", "").head()

Out [33] 0      High
         1    Medium
         2       Low
         3      High
         4      High
         Name: Risk, dtype: object

通过从中间索引位置切片并移除结束括号,我们能够隔离每行的风险级别。

6.4 布尔方法

第 6.3 节介绍了 upperslice 等返回字符串 Series 的方法。StringMethods 对象上可用的其他方法返回布尔值 Series。这些方法在过滤 DataFrame 时可能特别有用。

假设我们想要隔离所有名称中包含单词 "Pizza" 的机构。在纯 Python 中,我们使用 in 操作符来搜索字符串中的子字符串:

In  [34] "Pizza" in "Jets Pizza"

Out [34] True

字符串匹配的最大挑战是大小写敏感性。例如,Python 不会在 "Jets Pizza" 中找到字符串 "pizza",因为 "p" 字符的大小写不匹配:

In  [35] "pizza" in "Jets Pizza"

Out [35] False

为了解决这个问题,我们需要在检查子字符串的存在之前确保所有列值的大小写一致。我们可以在全小写的 Series 中查找 "pizza",或者在全部大写的 Series 中查找 "PIZZA"。让我们选择前者。

contains 方法检查每个 Series 值中是否包含子字符串。当 pandas 在行的字符串中找到方法参数时,该方法返回 True;如果没有找到,则返回 False。以下示例首先使用 lower 方法将名称列转换为小写,然后在每行中搜索 "pizza"

In  [36] inspections["Name"].str.lower().str.contains("pizza").head()

Out [36] 0    False
         1     True
         2    False
         3    False
         4    False
         Name: Name, dtype: bool

我们有一个布尔 Series,我们可以用它来提取所有名称中包含 "Pizza" 的机构:

In  [37] has_pizza = inspections["Name"].str.lower().str.contains("pizza")
         inspections[has_pizza]

Out [37]

 **Name             Risk**
1                             JETS PIZZA  Risk 2 (Medium)
19         NANCY'S HOME OF STUFFED PIZZA    Risk 1 (High)
27            NARY'S GRILL & PIZZA ,INC.    Risk 1 (High)
29                   NARYS GRILL & PIZZA    Risk 1 (High)
68                         COLUTAS PIZZA    Risk 1 (High)
 ...                              ...                ...
153756       ANGELO'S STUFFED PIZZA CORP    Risk 1 (High)
153764                COCHIAROS PIZZA #2    Risk 1 (High)
153772  FERNANDO'S MEXICAN GRILL & PIZZA    Risk 1 (High)
153788            REGGIO'S PIZZA EXPRESS    Risk 1 (High)
153801        State Street Pizza Company    Risk 1 (High)

3992 rows × 2 columns

注意到 pandas 保留了姓名中的原始字母大小写。inspections DataFrame 从未改变。lower 方法返回一个新的 Series,而我们对其调用的 contains 方法返回另一个新的 Series,pandas 使用它来从原始 DataFrame 中过滤行。

如果我们想要在目标上更加精确,也许提取所有以字符串 "tacos" 开头的机构?现在我们关注子字符串在每个字符串中的位置。str.startswith 方法解决了这个问题,如果字符串以它的参数开头则返回 True

In  [38] inspections["Name"].str.lower().str.startswith("tacos").head()

Out [38] 0    False
         1    False
         2    False
         3    False
         4    False
         Name: Name, dtype: bool

In  [39] starts_with_tacos = (
             inspections["Name"].str.lower().str.startswith("tacos")
         )

         inspections[starts_with_tacos]
Out [39]

 **Name           Risk**
69               TACOS NIETOS  Risk 1 (High)
556       TACOS EL TIO 2 INC.  Risk 1 (High)
675          TACOS DON GABINO  Risk 1 (High)
958       TACOS EL TIO 2 INC.  Risk 1 (High)
1036      TACOS EL TIO 2 INC.  Risk 1 (High)
...                       ...            ...
143587          TACOS DE LUNA  Risk 1 (High)
144026           TACOS GARCIA  Risk 1 (High)
146174        Tacos Place's 1  Risk 1 (High)
147810  TACOS MARIO'S LIMITED  Risk 1 (High)
151191            TACOS REYNA  Risk 1 (High)

105 rows × 2 columns

补充的 str.endswith 方法检查每个 Series 字符串的末尾是否有子字符串:

In  [40] ends_with_tacos = (
             inspections["Name"].str.lower().str.endswith("tacos")
         )

         inspections[ends_with_tacos]

Out [40]

 **Name           Risk**
382        LAZO'S TACOS  Risk 1 (High)
569        LAZO'S TACOS  Risk 1 (High)
2652       FLYING TACOS   Risk 3 (Low)
3250       JONY'S TACOS  Risk 1 (High)
3812       PACO'S TACOS  Risk 1 (High)
...                 ...            ...
151121      REYES TACOS  Risk 1 (High)
151318   EL MACHO TACOS  Risk 1 (High)
151801   EL MACHO TACOS  Risk 1 (High)
153087  RAYMOND'S TACOS  Risk 1 (High)
153504        MIS TACOS  Risk 1 (High)

304 rows × 2 columns

无论您是在寻找字符串的开头、中间还是结尾的文本,StringMethods 对象都有一个辅助方法来帮助您。

6.5 分割字符串

我们下一个数据集是一组虚构的客户。每一行包括客户的姓名和地址。让我们使用 read_csv 函数导入 customers.csv 文件,并将 DataFrame 赋值给 customers 变量:

In  [41] customers = pd.read_csv("customers.csv")
         customers.head()

Out [41]
 **Name                                              Address**
0        Frank Manning  6461 Quinn Groves, East Matthew, New Hampshire,166...
1    Elizabeth Johnson   1360 Tracey Ports Apt. 419, Kyleport, Vermont,319...
2      Donald Stephens   19120 Fleming Manors, Prestonstad, Montana, 23495
3  Michael Vincent III        441 Olivia Creek, Jimmymouth, Georgia, 82991
4       Jasmine Zamora     4246 Chelsey Ford Apt. 310, Karamouth, Utah, 76...

我们可以使用 str.len 方法来返回每行字符串的长度。例如,行 0 的 "Frank Manning" 值的长度为 13 个字符:

In  [42] customers["Name"].str.len().head()

Out [42] 0    13
         1    17
         2    15
         3    19
         4    14
         Name: Name, dtype: int64

假设我们想要将每个客户的第一个和最后一个名字分别放在两个单独的列中。您可能熟悉 Python 的 split 方法,该方法使用指定的分隔符来分割字符串。该方法返回一个由所有分割后的子字符串组成的列表。下一个示例使用连字符分隔符将电话号码分割成三个字符串列表:

In  [43] phone_number = "555-123-4567"
         phone_number.split("-")

Out [43] ['555', '123', '4567']

str.split 方法对 Series 中的每一行执行相同的操作;它的返回值是一个列表的 Series。我们将分隔符传递给方法的第一个参数,pat(代表 pattern)。下一个示例通过空格的存在来分割 Name 中的值:

In  [44] # The two lines below are equivalent
         customers["Name"].str.split(pat = " ").head()
         customers["Name"].str.split(" ").head()

Out [44] 0           [Frank, Manning]
         1       [Elizabeth, Johnson]
         2         [Donald, Stephens]
         3    [Michael, Vincent, III]
         4          [Jasmine, Zamora]
         Name: Name, dtype: object

接下来,让我们重新调用这个新的列表 Series 上的 str.len 方法来获取每个列表的长度。Pandas 会根据 Series 存储的数据类型动态反应:

In  [45] customers["Name"].str.split(" ").str.len().head()

Out [45] 0    2
         1    2
         2    2
         3    3
         4    2
         Name: Name, dtype: int64

我们有一个小问题。由于 "MD""Jr" 这样的后缀,一些名字有多于两个单词。我们可以在索引位置 3 看到一个例子:Michael Vincent III,pandas 会将其分割成三个元素的列表。为了确保每个列表有相同数量的元素,我们可以限制分割的数量。如果我们设置一个最大分割阈值为一次,pandas 将会在第一个空格处分割字符串并停止。然后我们将有一个由两个元素的列表组成的 Series。每个列表将包含客户的第一个名字以及随后的任何内容。

下一个示例将 1 作为参数传递给 split 方法的 n 参数,这设置了最大分割次数。看看 pandas 如何处理索引 3 的 "Michael Vincent III"

In  [46] customers["Name"].str.split(pat = " ", n = 1).head()

Out [46] 0          [Frank, Manning]
         1      [Elizabeth, Johnson]
         2        [Donald, Stephens]
         3    [Michael, Vincent III]
         4         [Jasmine, Zamora]
         Name: Name, dtype: object

现在我们所有的列表长度都相等了。我们可以使用 str.get 来根据每行的列表的索引位置提取一个值。例如,我们可以定位到索引 0,来提取每个列表的第一个元素,也就是客户的第一个名字:

In  [47] customers["Name"].str.split(pat = " ", n = 1).str.get(0).head()

Out [47] 0        Frank
         1    Elizabeth
         2       Donald
         3      Michael
         4      Jasmine
         Name: Name, dtype: object

要从每个列表中提取姓氏,我们可以将 get 方法传递一个索引位置为 1:

In  [48] customers["Name"].str.split(pat = " ", n = 1).str.get(1).head()

Out [48] 0        Manning
         1        Johnson
         2       Stephens
         3    Vincent III
         4         Zamora
         Name: Name, dtype: object

get 方法也支持负参数。参数 -1 从每行的列表中提取最后一个元素,无论列表有多少元素。以下代码产生与前面代码相同的结果,并且在列表长度不同的情况下更灵活:

In  [49] customers["Name"].str.split(pat = " ", n = 1).str.get(-1).head()

Out [49] 0        Manning
         1        Johnson
         2       Stephens
         3    Vincent III
         4         Zamora
         Name: Name, dtype: object

到目前为止,一切顺利。我们已经使用两个单独的 get 方法调用来提取两个单独的 Series 中的第一个和最后一个名字。不是很好吗?在单个方法调用中执行相同的逻辑?幸运的是,str.split 方法接受一个 expand 参数,当我们传递一个 True 参数时,该方法返回一个新的 DataFrame 而不是列表的 Series

In  [50] customers["Name"].str.split(
             pat = " ", n = 1, expand = True
         ).head()

Out [50]

 **0            1**
0      Frank      Manning
1  Elizabeth      Johnson
2     Donald     Stephens
3    Michael  Vincent III
4    Jasmine       Zamora

我们得到了一个新的 DataFrame!因为我们没有为列提供自定义名称,pandas 默认在列轴上使用数字索引。

在这些情况下要小心。如果我们不使用 n 参数限制拆分的次数,pandas 将在元素不足的行中放置 None 值:

In  [51] customers["Name"].str.split(pat = " ", expand = True).head()

Out [51]

 **0         1     2**
0      Frank   Manning  None
1  Elizabeth   Johnson  None
2     Donald  Stephens  None
3    Michael   Vincent   III
4    Jasmine    Zamora  None

现在我们已经隔离了客户的姓名,让我们将新的两列 DataFrame 附接到现有的客户 DataFrame 上。在等号的右边,我们将使用 split 代码来创建 DataFrame。在等号的左边,我们将提供一个列名列表,放在一对方括号内。Pandas 将将这些列附加到客户上。下一个示例添加了两个新列,即“First Name”和“Last Name”,并用 split 方法返回的 DataFrame 来填充它们:

In  [52] customers[["First Name", "Last Name"]] = customers[
             "Name"
         ].str.split(pat = " ", n = 1, expand = True)

让我们看看结果:

In  [53] customers

Out [53]

 **Name                   Address    First Name     Last Name**
0           Frank Manning  6461 Quinn Groves, E...       Frank      Manning
1       Elizabeth Johnson  1360 Tracey Ports Ap...   Elizabeth      Johnson
2         Donald Stephens  19120 Fleming Manors...      Donald     Stephens
3     Michael Vincent III  441 Olivia Creek, Ji...     Michael  Vincent III
4          Jasmine Zamora  4246 Chelsey Ford Ap...     Jasmine       Zamora
 ...              ...                 ...                ...            ...
9956        Dana Browning  762 Andrew Views Apt...        Dana     Browning
9957      Amanda Anderson  44188 Day Crest Apt ...      Amanda     Anderson
9958           Eric Davis  73015 Michelle Squar...        Eric        Davis
9959     Taylor Hernandez  129 Keith Greens, Ha...      Taylor    Hernandez
9960     Sherry Nicholson  355 Griffin Valley, ...      Sherry    Nicholson

9961 rows × 4 columns

太棒了!现在我们已经将客户的姓名提取到单独的列中,我们可以删除原始的姓名列。一种方法是使用我们客户的 DataFrame 上的 drop 方法。我们将传递列的名称到 labels 参数,并将 "columns" 作为 axis 参数的参数。我们需要包含 axis 参数来告诉 pandas 在列中而不是行中查找姓名标签:

In  [54] customers = customers.drop(labels = "Name", axis = "columns")

记住,突变操作在 Jupyter Notebook 中不会产生输出。我们必须打印 DataFrame 来查看结果:

In  [55] customers.head()

Out [55]

 **Address   First Name     Last Name**
0  6461 Quinn Groves, East Matthew, New Hampshire...       Frank      Manning
1   1360 Tracey Ports Apt. 419, Kyleport, Vermont...   Elizabeth      Johnson
2      19120 Fleming Manors, Prestonstad, Montana...      Donald     Stephens
3           441 Olivia Creek, Jimmymouth, Georgia...     Michael  Vincent III
4     4246 Chelsey Ford Apt. 310, Karamouth, Utah...     Jasmine       Zamora

好了,姓名列已经消失了,我们已经将其内容拆分到了两个新的列中。

6.6 编程挑战

现在是你练习本章引入的概念的机会。

6.6.1 问题

我们的客户数据集包括一个地址列。每个地址由街道、城市、州和邮政编码组成。你的挑战是将这四个值分开;将它们分配给新的街道、城市、州和邮政编码列;然后删除地址列。尝试解决这个问题,然后查看解决方案。

6.6.2 解决方案

我们的第一步是使用 split 方法通过分隔符拆分地址字符串。单独的逗号似乎是一个好的参数:

In  [56] customers["Address"].str.split(",").head()

Out [56] 0    [6461 Quinn Groves,  East Matthew,  New Hampsh...
         1    [1360 Tracey Ports Apt. 419,  Kyleport,  Vermo...
         2    [19120 Fleming Manors,  Prestonstad,  Montana,...
         3    [441 Olivia Creek,  Jimmymouth,  Georgia,  82991]
         4    [4246 Chelsey Ford Apt. 310,  Karamouth,  Utah...
         Name: Address, dtype: object

不幸的是,这个分割操作保留了逗号后面的空格。我们可以通过使用 strip 等方法进行额外的清理,但有一个更好的解决方案。如果我们仔细思考,地址的每一部分都是由逗号和空格分隔的。因此,我们可以将这两个字符作为分隔符传递给 split 方法:

In  [57] customers["Address"].str.split(", ").head()

Out [57] 0    [6461 Quinn Groves, East Matthew, New Hampshir...
         1    [1360 Tracey Ports Apt. 419, Kyleport, Vermont...
         2    [19120 Fleming Manors, Prestonstad, Montana, 2...
         3       [441 Olivia Creek, Jimmymouth, Georgia, 82991]
         4    [4246 Chelsey Ford Apt. 310, Karamouth, Utah, ...
         Name: Address, dtype: object

现在列表中的每个子字符串开头都没有多余的空格了。

默认情况下,split 方法返回一个列表的 Series。我们可以通过传递 expand 参数一个值为 True 的参数来使方法返回一个 DataFrame

In  [58] customers["Address"].str.split(", ", expand = True).head()

Out [58]

 **0             1              2      3**
0           6461 Quinn Groves  East Matthew  New Hampshire  16656
1  1360 Tracey Ports Apt. 419      Kyleport        Vermont  31924
2        19120 Fleming Manors   Prestonstad        Montana  23495
3            441 Olivia Creek    Jimmymouth        Georgia  82991
4  4246 Chelsey Ford Apt. 310     Karamouth           Utah  76252

我们还有一些步骤要做。让我们将新的四列 DataFrame 添加到现有的客户 DataFrame 中。我们将定义一个包含新列名称的列表。这次,让我们将列表赋给一个变量以简化可读性。接下来,我们将在等号前传递列表,并在等号右侧使用前面的代码创建新的 DataFrame

In  [59] new_cols = ["Street", "City", "State", "Zip"]

         customers[new_cols] = customers["Address"].str.split(
              pat = ", ", expand = True   
         )

最后一步是删除原始的 Address 列。drop 方法在这里是一个很好的解决方案。为了永久更改 DataFrame,请确保用返回的 DataFrame 覆盖 customers:

In  [60] customers.drop(labels = "Address", axis = "columns").head()

Out [60]

 **First Name    Last Name        Street          City         State    Zip**
0      Frank      Manning  6461 Quin...  East Matthew  New Hamps...  16656
1  Elizabeth      Johnson  1360 Trac...      Kyleport       Vermont  31924
2     Donald     Stephens  19120 Fle...   Prestonstad       Montana  23495
3    Michael  Vincent III  441 Olivi...    Jimmymouth       Georgia  82991
4    Jasmine       Zamora  4246 Chel...     Karamouth          Utah  76252

另一个选项是在目标列之前使用 Python 的内置 del 关键字。这个语法会修改 DataFrame

In  [61] del customers["Address"]

让我们看看最终产品:

In  [62] customers.tail()

Out [62]

 **First Name  Last Name        Street         City            State    Zip**
9956       Dana   Browning  762 Andrew ...   North Paul     New Mexico  28889
9957     Amanda   Anderson  44188 Day C...  Lake Marcia          Maine  37378
9958       Eric      Davis  73015 Miche...  Watsonville  West Virginia  03933
9959     Taylor  Hernandez  129 Keith G...    Haleyfurt       Oklahoma  98916
9960     Sherry  Nicholson  355 Griffin...    Davidtown     New Mexico  17581

我们已经成功地将地址列的内容提取到了四个新的列中。恭喜你完成了编码挑战!

6.7 关于正则表达式的说明

任何关于处理文本数据的讨论如果没有提到正则表达式(也称为 RegEx)都是不完整的。正则表达式是一种搜索模式,用于在字符串中查找字符序列。

我们使用特殊的语法来声明正则表达式,该语法由符号和字符组成。例如,\d 匹配介于 0 和 9 之间的任何数字。通过正则表达式,我们可以通过针对小写字母、大写字母、数字、斜杠、空白、字符串边界等来定义复杂的搜索模式。

假设一个像 555-555-5555 这样的电话号码隐藏在一个更长的字符串中。我们可以使用正则表达式来定义一个搜索算法,提取由三个连续数字、一个破折号、三个连续数字、另一个破折号和四个更多连续数字组成的序列。这种粒度级别赋予了正则表达式其强大的功能。

下面是一个快速示例,展示了语法在实际中的应用。接下来的代码示例使用 Street 列上的 replace 方法,将所有连续四个数字替换为星号字符:

In  [63] customers["Street"].head()

Out [63]  0             6461 Quinn Groves
          1    1360 Tracey Ports Apt. 419
          2          19120 Fleming Manors
          3              441 Olivia Creek
          4    4246 Chelsey Ford Apt. 310
          Name: Street, dtype: object

In  [64] customers["Street"].str.replace(
             "\d{4,}", "*", regex = True
         ).head()

Out [64] 0             * Quinn Groves
         1    * Tracey Ports Apt. 419
         2           * Fleming Manors
         3           441 Olivia Creek
         4    * Chelsey Ford Apt. 310
         Name: Street, dtype: object

正则表达式是一个高度专业化的技术主题。关于正则表达式的复杂性,已经有许多书籍被撰写。目前,重要的是要注意,pandas 支持大多数字符串方法的正则表达式参数。你可以查看附录 E 以获得该领域的更全面介绍。

摘要

  • str 属性包含一个 StringMethods 对象,该对象具有对 Series 值执行字符串操作的方法。

  • strip 方法族用于从字符串的开始、结束或两侧移除空白字符。

  • upperlowercapitalizetitle 等方法用于修改字符串字符的大小写。

  • contains 方法用于检查另一个字符串中是否存在子字符串。

  • startswith 方法用于检查字符串开头是否存在子字符串。

  • 补充的 endswith 方法用于检查字符串末尾是否存在子字符串。

  • split 方法通过使用指定的分隔符将字符串分割成列表。我们可以用它来分割 DataFrame 列中的文本,跨越多个 Series

7 MultiIndex DataFrames

本章涵盖了

  • 创建 MultiIndex

  • MultiIndex DataFrame 中选择行和列

  • MultiIndex DataFrame 中提取横截面

  • 交换 MultiIndex 级别

到目前为止,在我们的 pandas 之旅中,我们已经探索了一维的 Series 和二维的 DataFrame。维数的数量是我们从数据结构中提取值所需的参考点数量。在 Series 中定位一个值只需要一个标签或一个索引位置。在 DataFrame 中定位一个值需要两个参考点:行和列的标签/索引。我们能否超越二维?绝对可以!Pandas 通过使用 MultiIndex 支持任何数量的维度的数据集。

MultiIndex 是一个包含多个级别的索引对象。每个级别存储行的值。当值的组合为数据行提供最佳标识符时,使用 MultiIndex 是最理想的。考虑图 7.1 中的数据集,它存储了多个日期的股票价格。

图 7.1 示例数据集,包含股票、日期和价格列

假设我们想要为每个价格找到一个唯一标识符。仅股票名称或日期本身都不足以作为标识,但两者的组合则是一个很好的选择。股票 "MSFT" 出现了两次,日期 "02/08/2021" 也出现了两次,但 "MSFT""02/08/2021" 的组合只出现了一次。存储股票和日期列值的 MultiIndex 对这个数据集非常适合。

MultiIndex 也非常适合层次数据——其中一列的值是另一列值的子类别的数据。考虑图 7.2 中的数据集。

图 7.2 示例数据集,包含组、项目和卡路里列

项目列的值是组列值的子类别。苹果是一种水果,西兰花是一种蔬菜。因此,组和项目列可以作为 MultiIndex 组合。

MultiIndex 是 pandas 中一个不太为人所知的功能,但值得花时间去学习。引入多个索引级别为我们如何切片和切块数据集增加了许多灵活性。

7.1 MultiIndex 对象

让我们打开一个新的 Jupyter Notebook,导入 pandas 库,并将其分配别名 pd

In  [1] import pandas as pd

为了保持简单,我们将从零开始创建一个 MultiIndex 对象。在第 7.2 节中,我们将在导入的数据集上练习这些概念。

你还记得 Python 的内置元组对象吗?元组是一个不可变的数据结构,它按顺序存储一系列值。元组实际上是一个在创建后不能修改的列表。要深入了解这个数据结构,请参阅附录 B。

假设我们想要模拟一个街道地址。地址通常包括街道名称、城市、镇和邮政编码。我们可以将这些四个元素存储在一个元组中:

In  [2] address = ("8809 Flair Square", "Toddside", "IL", "37206")
        address

Out [2] ('8809 Underwood Squares', 'Toddside', 'IL', '37206')

SeriesDataFrame的索引可以存储各种数据类型:字符串、数字、日期和时间等。但所有这些对象在每个索引位置只能存储一个值,每行一个标签。元组没有这个限制。

如果我们在一个列表中收集多个元组呢?列表看起来会是这样:

In  [3] addresses = [
            ("8809 Flair Square", "Toddside", "IL", "37206"),
            ("9901 Austin Street", "Toddside", "IL", "37206"),
            ("905 Hogan Quarter", "Franklin", "IL", "37206"),
        ]

现在想象这些元组作为DataFrame的索引标签。我希望这个想法不会太令人困惑。所有操作都保持不变。我们仍然可以通过索引标签来引用一行,但每个索引标签都是一个包含多个元素的容器。这是一个很好的方式来开始思考MultiIndex对象——作为一个每个标签可以存储多个数据的索引。

我们可以独立于SeriesDataFrame创建MultiIndex对象。MultiIndex类作为 pandas 库的一个顶级属性可用。它包括一个from_tuples类方法,可以从元组列表中实例化一个MultiIndex类方法是我们对一个类而不是一个实例调用的方法。下一个例子调用了from_tuples类方法,并传递了addresses列表:

In  [4] # The two lines below are equivalent
        pd.MultiIndex.from_tuples(addresses)
        pd.MultiIndex.from_tuples(tuples = addresses)

Out [4] MultiIndex([( '8809 Flair Square',   'Toddside', 'IL', '37206'),
                    ('9901 Austin Street',   'Toddside', 'IL', '37206'),
                    ( '905 Hogan Quarter',   'Franklin', 'IL', '37206')],
                   )

我们有了第一个MultiIndex,它存储了三个包含四个元素的元组。每个元组的元素都有一个一致的规律:

  • 第一个值是地址。

  • 第二个值是城市。

  • 第三个值是州。

  • 第四个值是邮政编码。

在 pandas 术语中,相同位置上的元组值的集合形成MultiIndex的一个level。在之前的例子中,第一个MultiIndex级别由值"8809 Flair Square""9901 Austin Street""905 Hogan Quarter"组成。同样,第二个MultiIndex级别由"Toddside""Toddside""Franklin"组成。

我们可以通过传递一个列表给from_tuples方法的names参数来给每个MultiIndex级别分配一个名称。在这里,我们分配了名称"Street""City""State""Zip"

In  [5] row_index = pd.MultiIndex.from_tuples(
            tuples = addresses,
            names = ["Street", "City", "State", "Zip"]
        )

        row_index

Out [5] MultiIndex([( '8809 Flair Square',   'Toddside', 'IL', '37206'),
                    ('9901 Austin Street',   'Toddside', 'IL', '37206'),
                    ( '905 Hogan Quarter',   'Franklin', 'IL', '37206')],
                    names=['Street', 'City', 'State', 'Zip'])

总结一下,MultiIndex是一个存储容器,其中每个标签包含多个值。一个级别由标签中相同位置的值组成。

现在我们有了MultiIndex,让我们将其附加到一个DataFrame上。最简单的方法是使用DataFrame构造函数的index参数。我们在前面的章节中传递了这个参数一个字符串列表,但它也接受任何有效的索引对象。让我们传递给它分配给row_index变量的MultiIndex。因为我们的MultiIndex有三个元组(或者说,相当于三个标签),我们需要提供三行数据:

In  [6] data = [
            ["A", "B+"],
            ["C+", "C"],
            ["D-", "A"],
        ]

        columns = ["Schools", "Cost of Living"]

        area_grades = pd.DataFrame(
            data = data, index = row_index, columns = columns
        )

        area_grades

Out [6]

 **Schools Cost of Living**
Street             City     State Zip
8809 Flair Square  Toddside IL    37206       A             B+
9901 Austin Street Toddside IL    37206      C+              C
905 Hogan Quarter  Franklin IL    37206      D-              A

我们有一个在行轴上有MultiIndexDataFrame。每一行的标签包含四个值:街道、城市、州和邮政编码。

让我们把注意力转向列轴。Pandas 将DataFrame的列标题存储在一个索引对象中。我们可以通过columns属性访问该索引:

In  [7] area_grades.columns

Out [7] Index(['Schools', 'Cost of Living'], dtype='object')

Pandas 目前将两个列名存储在单级Index对象中。让我们创建第二个MultiIndex并将其附加到列轴。下一个示例再次调用from_tuples类方法,传递一个包含四个元组的列表。每个元组包含两个字符串:

In  [8] column_index = pd.MultiIndex.from_tuples(
             [
                 ("Culture", "Restaurants"),
                 ("Culture", "Museums"),
                 ("Services", "Police"),
                 ("Services", "Schools"),
             ]
         )

         column_index

Out [8] MultiIndex([( 'Culture', 'Restaurants'),
                    ( 'Culture',     'Museums'),
                    ('Services',      'Police'),
                    ('Services',     'Schools')],
                   )

让我们将两个MultiIndex都附加到一个DataFrame上。行轴的MultiIndexrow_index)要求数据集包含三行。列轴的MultiIndexcolumn_index)要求数据集包含四列。因此,我们的数据集必须具有 3 x 4 的形状。让我们创建这个样本数据。下一个示例声明了一个包含三个列表的列表。每个嵌套列表存储四个字符串:

In  [9] data = [
            ["C-", "B+", "B-", "A"],
            ["D+", "C", "A", "C+"],
            ["A-", "A", "D+", "F"]
        ]

我们已经准备好将各个部分组合起来,创建一个在行和列轴上都有MultiIndexDataFrame。在DataFrame构造函数中,我们将各自的MultiIndex变量传递给indexcolumns参数:

In  [10] pd.DataFrame(
             data = data, index = row_index, columns = column_index
         )

Out [10]

                                    Culture               Services
                                    Restaurants Museums   Police Schools
**Street       City       State Zip** 
8809 Flai... Toddside   IL    37206          C-      B+       B-       A
9901 Aust... Toddside   IL    37206          D+       C        A      C+
905 Hogan... Franklin   IL    37206          A-       A       D+       F

欢呼!我们已经成功创建了一个具有四级行MultiIndex和二级列MultiIndexDataFrameMultiIndex是一个可以存储多个级别、多个层级的索引。每个索引标签由多个组件组成。这就是全部内容。

7.2 多级索引 DataFrame

让我们稍微扩大一下范围。neighborhoods.csv 数据集与我们第 7.1 节中创建的数据集类似;它列出了美国各地约 250 个虚构地址。每个地址根据四个宜居特性进行评级:餐馆、博物馆、警察局和学校。这四个评级分为两个父类别:文化和服务。

这是原始 CSV 文件前几行的预览。在 CSV 中,逗号分隔数据行中的每两个后续值。因此,连续逗号之间没有内容表示缺失值:

,,,Culture,Culture,Services,Services
,,,Restaurants,Museums,Police,Schools
State,City,Street,,,,
MO,Fisherborough,244 Tracy View,C+,F,D-,A+

Pandas 如何导入这个 CSV 文件的数据?让我们用read_csv函数来找出答案:

In  [11] neighborhoods = pd.read_csv("neighborhoods.csv")
         neighborhoods.head()

Out [11]

 **Unnamed: 0  Unnamed: 1  Unnamed: 2    Culture Culture.1 Services Services.1**
0        NaN         NaN         NaN  Restau...   Museums   Police    Schools
1      State        City      Street        NaN       NaN      NaN        NaN
2         MO   Fisher...   244 Tr...         C+         F       D-         A+
3         SD   Port C...   446 Cy...         C-         B        B         D+
4         WV   Jimene...   432 Jo...          A        A+        F          B

这里有些不对劲!首先,我们有三个未命名的列,每个列都以不同的数字结尾。当导入 CSV 时,pandas 假设文件的第一行包含列名,也称为标题。如果一个标题槽没有值,pandas 会将其分配一个标题为“未命名”的列名。同时,库试图避免重复的列名。为了区分多个缺失的标题,库会给每个标题添加一个数字索引。因此,我们有三个未命名的列:未命名:0、未命名:1 和未命名:2。

右侧的四列也存在相同的命名问题。注意,pandas 将标题为“文化”的列分配给索引 3,并将其后的列命名为“文化 1”。CSV 文件在行中有两个标题单元格具有相同的“文化”值,然后是两行标题单元格具有相同的“服务”值。

不幸的是,这还不是我们的问题的终点。在行 0 中,前三个列都包含一个 NaN 值。在行 1 中,最后四个列都存在 NaN 值。问题是 CSV 正在尝试模拟一个多级行索引和多级列索引,但 read_csv 函数参数的默认值不识别它。幸运的是,我们可以通过更改 read_csv 参数的值来解决这个问题。

首先,我们必须告诉 pandas,前三个列应该作为 DataFrame 的索引。我们可以通过将一个包含数字的列表传递给 index_col 参数来实现,每个数字代表一个列的索引(或数字位置),该列应包含在 DataFrame 的索引中。索引从 0 开始计数。因此,前三个列(未命名的列)将具有索引位置 0、1 和 2。当我们传递一个包含多个值的 index_col 列表时,pandas 会自动为 DataFrame 创建一个 MultiIndex

In  [12] neighborhoods = pd.read_csv(
             "neighborhoods.csv",
             index_col = [0, 1, 2]
         )

         neighborhoods.head()

Out [12]
 **Culture Culture.1 Services Services.1**
NaN   NaN           NaN           Restaurants   Museums   Police    Schools
State City          Street                NaN       NaN      NaN        NaN
MO    Fisherbor...  244 Tracy...           C+         F       D-         A+
SD    Port Curt...  446 Cynth...           C-         B        B         D+
WV    Jimenezview   432 John ...            A        A+        F          B

我们已经完成了一半。接下来,我们需要告诉 pandas 我们想要用于 DataFrame 标题的数据集行。read_csv 函数假设只有第一行将包含标题。在这个数据集中,前两行将包含标题。我们可以使用 read_csv 函数的 header 参数自定义 DataFrame 的标题,该参数接受一个整数列表,表示 pandas 应将其设置为列标题的 rows。如果我们提供一个包含多个元素的列表,pandas 将将一个 MultiIndex 分配给列。下一个示例将前两行(索引 0 和 1)设置为列标题:

In  [13] neighborhoods = pd.read_csv(
             "neighborhoods.csv",
             index_col = [0, 1, 2],
             header = [0, 1]
         )

         neighborhoods.head()

Out [13]
                                           Culture         Services
                                      Restaurants Museums   Police Schools
**State City             Street** 
MO    Fisherborough    244 Tracy View           C+       F       D-      A+
SD    Port Curtisv...  446 Cynthia ...          C-       B        B      D+
WV    Jimenezview      432 John Common           A      A+        F       B
AK    Stevenshire      238 Andrew Rue           D-       A       A-      A-
ND    New Joshuaport   877 Walter Neck          D+      C-        B       B

现在我们有了一些可以操作的东西!

如前所述,数据集将四个可居住性特征(餐馆、博物馆、警察和学校)分为两类(文化和服务)。当我们有一个包含较小子类别的父类别时,创建一个 MultiIndex 是实现快速切片的最佳方式。

让我们调用一些熟悉的方法来观察 MultiIndex DataFrame 的输出如何变化。info 方法是一个很好的起点:

In  [14] neighborhoods.info()

Out [14]

<class 'pandas.core.frame.DataFrame'>
MultiIndex: 251 entries, ('MO', 'Fisherborough', '244 Tracy View') to ('NE', 'South Kennethmouth', '346 Wallace Pass')
Data columns (total 4 columns):
 #   Column                  Non-Null Count  Dtype
---  ------                  --------------  -----
 0   (Culture, Restaurants)  251 non-null    object
 1   (Culture, Museums)      251 non-null    object
 2   (Services, Police)      251 non-null    object
 3   (Services, Schools)     251 non-null    object
dtypes: object(4)
memory use: 27.2+ KB

注意,pandas 将每个列名打印为一个包含两个元素的元组,例如 (Culture, Restaurants)。同样,库将每行的标签存储为一个包含三个元素的元组,例如 ('MO', 'Fisherborough', '244 Tracy View')

我们可以通过熟悉的 index 属性访问行的 MultiIndex 对象。输出使我们能够看到包含每行值的元组:

In  [15] neighborhoods.index

Out [15] MultiIndex([
            ('MO',       'Fisherborough',        '244 Tracy View'),
            ('SD',    'Port Curtisville',     '446 Cynthia Inlet'),
            ('WV',         'Jimenezview',       '432 John Common'),
            ('AK',         'Stevenshire',        '238 Andrew Rue'),
            ('ND',      'New Joshuaport',       '877 Walter Neck'),
            ('ID',          'Wellsville',   '696 Weber Stravenue'),
            ('TN',           'Jodiburgh',    '285 Justin Corners'),
            ('DC',    'Lake Christopher',   '607 Montoya Harbors'),
            ('OH',           'Port Mike',      '041 Michael Neck'),
            ('ND',          'Hardyburgh', '550 Gilmore Mountains'),
            ...
            ('AK', 'South Nicholasshire',      '114 Jones Garden'),
            ('IA',     'Port Willieport',  '320 Jennifer Mission'),
            ('ME',          'Port Linda',        '692 Hill Glens'),
            ('KS',          'Kaylamouth',       '483 Freeman Via'),
            ('WA',      'Port Shawnfort',    '691 Winters Bridge'),
            ('MI',       'North Matthew',      '055 Clayton Isle'),
            ('MT',             'Chadton',     '601 Richards Road'),
            ('SC',           'Diazmouth',     '385 Robin Harbors'),
            ('VA',          'Laurentown',     '255 Gonzalez Land'),
            ('NE',  'South Kennethmouth',      '346 Wallace Pass')],
           names=['State', 'City', 'Street'], length=251)

我们可以通过 columns 属性访问列的 MultiIndex 对象,该属性也使用元组来存储嵌套的列标签:

In  [16] neighborhoods.columns

Out [16] MultiIndex([( 'Culture', 'Restaurants'),
            ( 'Culture',     'Museums'),
            ('Services',      'Police'),
            ('Services',     'Schools')],
           )

在其内部,pandas 从多个 Index 对象中组合一个 MultiIndex。在导入数据集时,库为每个 Index 从 CSV 标题分配了一个名称。我们可以通过 MultiIndex 对象上的 names 属性访问索引名称列表。州、市和街道是成为我们索引的三个 CSV 列的名称:

In  [17] neighborhoods.index.names

Out [17] FrozenList(['State', 'City', 'Street'])

Pandas 为 MultiIndex 中的每个嵌套层级分配一个顺序。在我们的当前 neighborhoods DataFrame 中,

  • 州层级有一个索引位置为 0。

  • 城市层级有一个索引位置为 1。

  • 街道层级有一个索引位置为 2。

get_level_values 方法从 MultiIndex 的给定层级中提取 Index 对象。我们可以传递层级的索引位置或层级的名称给方法的第一个也是唯一的参数 level

In  [18] # The two lines below are equivalent
         neighborhoods.index.get_level_values(1)
         neighborhoods.index.get_level_values("City")

Out [18] Index(['Fisherborough', 'Port Curtisville', 'Jimenezview',
                'Stevenshire', 'New Joshuaport', 'Wellsville', 'Jodiburgh',
                'Lake Christopher', 'Port Mike', 'Hardyburgh',
                ...
                'South Nicholasshire', 'Port Willieport', 'Port Linda',
                'Kaylamouth', 'Port Shawnfort', 'North Matthew', 'Chadton',
                'Diazmouth', 'Laurentown', 'South Kennethmouth'],
               dtype='object', name='City', length=251)

列的 MultiIndex 层级没有名称,因为 CSV 没有提供任何:

In  [19] neighborhoods.columns.names

Out [19] FrozenList([None, None])

让我们解决这个问题。我们可以使用 columns 属性访问列的 MultiIndex。然后我们可以将新的列名列表分配给 MultiIndex 对象的 names 属性。名称 "Category""Subcategory" 似乎很适合这里:

In  [20] neighborhoods.columns.names = ["Category", "Subcategory"]
         neighborhoods.columns.names

Out [20] FrozenList(['Category', 'Subcategory'])

层级名称将出现在输出中的列标题左侧。让我们调用 head 方法来看看区别:

In  [21] neighborhoods.head(3)

Out [21]

Category                            Culture         Services
Subcategory                     Restaurants Museums   Police Schools
**State City         Street** 
MO    Fisherbor... 244 Tracy...          C+       F       D-      A+
SD    Port Curt... 446 Cynth...          C-       B        B      D+
WV    Jimenezview  432 John ...           A      A+        F       B

现在我们已经为层级分配了名称,我们可以使用 get_level_values 方法从列的 MultiIndex 中检索任何 Index。记住,我们可以传递列的索引位置或列的名称给该方法:

In  [22] # The two lines below are equivalent
         neighborhoods.columns.get_level_values(0)
         neighborhoods.columns.get_level_values("Category")

Out [22] Index(['Culture', 'Culture', 'Services', 'Services'],
         dtype='object', name='Category')

MultiIndex 将延续到从数据集派生的新对象。索引可以根据操作切换轴。考虑 DataFramenunique 方法,它返回一个 Series,其中包含每列唯一值的计数。如果我们对 neighborhoods 调用 nuniqueDataFrame 的列 MultiIndex 将交换轴,并在结果 Series 中作为行的 MultiIndex

In  [23] neighborhoods.head(1)

Out [23]

Category                                 Culture         Services
Subcategory                          Restaurants Museums   Police Schools
**State City           Street** 
AK    Rowlandchester 386 Rebecca ...          C-      A-       A+       C

In  [24] neighborhoods.nunique()

Out [24] Culture   Restaurants    13
                   Museums        13
         Services  Police         13
                   Schools        13
         dtype: int64

MultiIndex Series 告诉我们 Pandas 在每个四个列中找到了多少唯一值。在这种情况下,值是相等的,因为所有四个列都包含了 13 种可能的等级(A+ 到 F)。

7.3 对 MultiIndex 排序

Pandas 在有序集合中查找值比在杂乱无章的集合中快得多。一个很好的类似例子是在字典中查找单词。当单词按字母顺序排列时,比随机序列更容易找到单词。因此,在从 DataFrame 中选择任何行和列之前对索引进行排序是最佳的。

第四章介绍了用于对 DataFrame 排序的 sort_index 方法。当我们对 MultiIndex DataFrame 调用该方法时,pandas 按升序对所有层级进行排序,并从外部开始进行。在下一个示例中,pandas 首先对州级值进行排序,然后对城市级值进行排序,最后对街道级值进行排序:

In  [25] neighborhoods.sort_index()

Out [25]

Category                                  Culture         Services
Subcategory                           Restaurants Museums   Police Schools
**State City            Street** 
AK    Rowlandchester 386 Rebecca ...          C-      A-       A+        C
      Scottstad      082 Leblanc ...           D      C-        D       B+
                     114 Jones Ga...          D-      D-        D        D
      Stevenshire    238 Andrew Rue           D-       A       A-       A-
AL    Clarkland      430 Douglas ...           A       F       C+       B+
 ...       ...           ...                  ...     ...      ...     ...
WY    Lake Nicole    754 Weaver T...           B      D-        B        D
                     933 Jennifer...           C      A+       A-        C
      Martintown     013 Bell Mills           C-       D       A-       B-
      Port Jason     624 Faulkner...          A-       F       C+       C+
      Reneeshire     717 Patel Sq...           B      B+        D        A

251 rows × 4 columns

让我们确保我们理解输出。首先,pandas 针对州层级,在 "AK""AL" 之间对值 "AK" 进行排序。然后,在 "AK" 州内,pandas 在 "Rowlandchester""Scottstad" 之间对城市进行排序。它将相同的逻辑应用于最终层级,街道。

sort_values 方法包含一个 ascending 参数。我们可以传递一个布尔值给该参数,以对所有 MultiIndex 层次应用一致的排序顺序。下一个示例提供了一个 False 参数。Pandas 将州值按逆字母顺序排序,然后是城市值按逆字母顺序排序,最后是街道值按逆字母顺序排序:

In  [26] neighborhoods.sort_index(ascending = False).head()

Out [26]

Category                              Culture         Services
Subcategory                       Restaurants Museums   Police Schools
**State City        Street** 
WY    Reneeshire  717 Patel Sq...           B      B+        D       A
      Port Jason  624 Faulkner...          A-       F       C+      C+
      Martintown  013 Bell Mills           C-       D       A-      B-
      Lake Nicole 933 Jennifer...           C      A+       A-       C
                  754 Weaver T...           B      D-        B       D

假设我们想要为不同的层次改变排序顺序。我们可以将布尔值列表传递给 ascending 参数。每个布尔值设置下一个 MultiIndex 层次的排序顺序,从最外层开始,向内进行。例如,[True, False, True] 参数将按升序对州层次进行排序,按降序对城市层次进行排序,按升序对街道层次进行排序:

In  [27] neighborhoods.sort_index(ascending = [True, False, True]).head()

Out [27]

Category                                 Culture         Services
Subcategory                          Restaurants Museums   Police Schools
**State City           Street** 
AK    Stevenshire    238 Andrew Rue           D-       A       A-      A-
      Scottstad      082 Leblanc ...           D      C-        D      B+
                     114 Jones Ga...          D-      D-        D       D
      Rowlandchester 386 Rebecca ...          C-      A-       A+       C
AL    Vegaside       191 Mindy Me...          B+      A-       A+      D+

我们还可以单独对 MultiIndex 层次进行排序。假设我们想要按第二个 MultiIndex 层次,即城市中的值对行进行排序。我们可以将层次索引位置或其名称传递给 sort_index 方法的 level 参数。Pandas 在排序时会忽略剩余的层次:

In  [28] # The two lines below are equivalent
         neighborhoods.sort_index(level = 1)
         neighborhoods.sort_index(level = "City")

Out [28]

Category                                Culture         Services
Subcategory                         Restaurants Museums   Police Schools
**State City          Street** 
AR    Allisonland   124 Diaz Brooks          C-      A+        F      C+
GA    Amyburgh      941 Brian Ex...           B       B       D-      C+
IA    Amyburgh      163 Heather ...           F       D       A+      A-
ID    Andrewshire   952 Ellis Drive          C+      A-       C+       A
UT    Baileyfort    919 Stewart ...          D+      C+        A       C
 ...      ...           ...                 ...     ...      ...     ...
NC    West Scott    348 Jack Branch          A-      D-       A-       A
SD    West Scott    139 Hardy Vista          C+      A-       D+      B-
IN    Wilsonborough 066 Carr Road            A+      C-        B       F
NC    Wilsonshire   871 Christop...          B+       B       D+       F
NV    Wilsonshire   542 Jessica ...           A      A+       C-      C+

251 rows × 4 columns

level 参数也接受一个层次列表。下一个示例首先按城市层次排序,然后按街道层次排序。州层次的值对排序没有任何影响:

In  [29] # The two lines below are equivalent
         neighborhoods.sort_index(level = [1, 2]).head()
         neighborhoods.sort_index(level = ["City", "Street"]).head()

Out [29]

Category                              Culture         Services
Subcategory                       Restaurants Museums   Police Schools
**State City        Street** 
AR    Allisonland 124 Diaz Brooks          C-      A+        F      C+
IA    Amyburgh    163 Heather ...           F       D       A+      A-
GA    Amyburgh    941 Brian Ex...           B       B       D-      C+
ID    Andrewshire 952 Ellis Drive          C+      A-       C+       A
VT    Baileyfort  831 Norma Cove            B      D+       A+      D+

我们还可以组合 ascendinglevel 参数。注意在前面的示例中,pandas 按字母/升序对艾姆伯赫市("163 Heather Neck" 和 "941 Brian Expressway")的两个街道值进行了排序。下一个示例按升序对城市层次进行排序,按降序对街道层次进行排序,从而交换了两个艾姆伯赫街道值的位置:

In  [30] neighborhoods.sort_index(
             level = ["City", "Street"], ascending = [True, False]
         ).head()

Out [30]

Category                              Culture         Services
Subcategory                       Restaurants Museums   Police Schools
**State City        Street** 
AR    Allisonland 124 Diaz Brooks          C-      A+        F      C+
GA    Amyburgh    941 Brian Ex...           B       B       D-      C+
IA    Amyburgh    163 Heather ...           F       D       A+      A-
ID    Andrewshire 952 Ellis Drive          C+      A-       C+       A
UT    Baileyfort  919 Stewart ...          D+      C+        A       C

我们还可以通过向 sort_index 方法提供 axis 参数来对列的 MultiIndex 进行排序。参数的默认值是 0,代表行索引。要排序列,我们可以传递数字 1 或字符串 "columns"。在下一个示例中,pandas 首先按类别层次排序,然后按子类别层次排序。文化值在服务之前。在文化层次内,博物馆值在餐馆之前。在服务中,警察值在学校之前:

In  [31] # The two lines below are equivalent
         neighborhoods.sort_index(axis = 1).head(3)
         neighborhoods.sort_index(axis = "columns").head(3)

Out [31]

Category                              Culture             Services
Subcategory                           Museums Restaurants   Police Schools
**State City            Street** 
MO    Fisherborough   244 Tracy View        F          C+       D-      A+
SD    Port Curtisv... 446 Cynthia ...       B          C-        B      D+
WV    Jimenezview     432 John Common      A+           A        F       B

我们可以将 levelascending 参数与 axis 参数结合使用,以进一步自定义列的排序顺序。下一个示例按降序对子类别层次值进行排序。Pandas 忽略类别层次中的值。子类别("Schools"、"Restaurants"、"Police" 和 "Museums")的逆字母顺序强制视觉上分割类别组。因此,输出会多次打印服务和文化列标题:

In  [32] neighborhoods.sort_index(
             axis = 1, level = "Subcategory", ascending = False
         ).head(3)

Out [32]

Category                              Services     Culture Services Culture
Subcategory                            Schools Restaurants   Police Museums
**State City            Street** 
MO    Fisherborough   244 Tracy View        A+          C+       D-       F
SD    Port Curtisv... 446 Cynthia ...       D+          C-        B       B
WV    Jimenezview     432 John Common        B           A        F      A+

在第 7.4 节中,我们将学习如何使用熟悉的访问器属性(如 lociloc)从 MultiIndex DataFrame 中提取行和列。如前所述,在我们查找任何行之前对索引进行排序是最佳做法。让我们按升序排序 MultiIndex 级别,并覆盖我们的 neighborhoods DataFrame

In  [33] neighborhoods = neighborhoods.sort_index(ascending = True)

这是结果:

In  [34] neighborhoods.head(3)

Out [34]

Category                                 Culture         Services
Subcategory                          Restaurants Museums   Police Schools
**State City           Street** 
AK    Rowlandchester 386 Rebecca ...          C-      A-       A+       C
      Scottstad      082 Leblanc ...           D      C-        D      B+
                     114 Jones Ga...          D-      D-        D       D

看起来不错。我们已经对 MultiIndex 中的每个级别进行了排序,可以继续进行。

7.4 使用多级索引进行选择

当涉及多个级别时,从 DataFrame 中提取行和列会变得复杂。在编写任何代码之前,我们需要问的关键问题是我们要提取什么。

第四章介绍了从 DataFrame 中选择列的方括号语法。这里有一个快速提醒。以下代码创建了一个包含两行两列的 DataFrame

In  [35] data = [
             [1, 2],
             [3, 4]
         ]

         df = pd.DataFrame(
             data = data, index = ["A", "B"], columns = ["X", "Y"]
         )

         df

Out [35]

 **X  Y**
A  1  2
B  3  4

方括号语法从 DataFrame 中提取一列作为 Series

In  [36] df["X"]

Out [36] A    1
         B    3
         Name: X, dtype: int64

假设我们想从 neighborhoods 中提取一列。DataFrame 中的四个列都需要两个标识符的组合:一个类别和一个子类别。如果我们只传递一个标识符会发生什么?

7.4.1 提取一个或多个列

如果我们在方括号中传递单个值,pandas 将在列的 MultiIndex 的最外层级别中查找它。以下示例搜索 "Services",这是类别级别中的一个有效值:

In  [37] neighborhoods["Services"]

Out [37]

Subcategory                               Police Schools
**State City           Street** 
AK    Rowlandchester 386 Rebecca Cove         A+       C
      Scottstad      082 Leblanc Freeway       D      B+
                     114 Jones Garden          D       D
      Stevenshire    238 Andrew Rue           A-      A-
AL    Clarkland      430 Douglas Mission      C+      B+
 ...       ...       ...        ...          ...     ...
WY    Lake Nicole    754 Weaver Turnpike       B       D
                     933 Jennifer Burg        A-       C
      Martintown     013 Bell Mills           A-      B-
      Port Jason     624 Faulkner Orchard     C+      C+
      Reneeshire     717 Patel Square          D       A

251 rows × 2 columns

注意,新的 DataFrame 没有类别级别。它有一个简单的 Index,包含两个值:"Police" 和 "Schools"。不再需要 MultiIndex;在这个 DataFrame 中的两列是隶属于服务值的子类别。类别级别不再有任何值得列出的变化。

如果值不存在于列的 MultiIndex 的最外层级别,Pandas 将引发 KeyError 异常:

In  [38] neighborhoods["Schools"]

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)

KeyError: 'Schools'

如果我们想针对特定的类别,然后在该类别中针对子类别,我们应该怎么办?为了在列的 MultiIndex 的多个级别中指定值,我们可以将它们放在一个元组中。下一个示例针对类别级别中的值为 "Services" 和子类别级别中的值为 "Schools" 的列:

In  [39] neighborhoods[("Services", "Schools")]

Out [39] State  City            Street
         AK     Rowlandchester  386 Rebecca Cove         C
                Scottstad       082 Leblanc Freeway     B+
                                114 Jones Garden         D
                Stevenshire     238 Andrew Rue          A-
         AL     Clarkland       430 Douglas Mission     B+
                                                        ..
         WY     Lake Nicole     754 Weaver Turnpike      D
                                933 Jennifer Burg        C
                Martintown      013 Bell Mills          B-
                Port Jason      624 Faulkner Orchard    C+
                Reneeshire      717 Patel Square         A
         Name: (Services, Schools), Length: 251, dtype: object

该方法返回一个不带列索引的 Series!再次强调,当我们为 MultiIndex 级别提供一个值时,我们消除了该级别存在的必要性。我们明确告诉 pandas 在类别和子类别级别中要针对哪些值,因此库从列索引中删除了这两个级别。因为 ("Services", "Schools") 组合产生了一个数据列,所以 pandas 返回了一个 Series 对象。

要提取多个 DataFrame 列,我们需要向方括号传递一个元组列表。列表中的每个元组应指定一个列的级别值。列表中元组的顺序设置了结果 DataFrame 中列的顺序。下一个示例从 neighborhoods 中提取两列:

In  [40] neighborhoods[[("Services", "Schools"), ("Culture", "Museums")]]

Out [40]

Category                                  Services Culture
Subcategory                                Schools Museums
**State City           Street** 
AK    Rowlandchester 386 Rebecca Cove            C      A-
      Scottstad      082 Leblanc Freeway        B+      C-
                     114 Jones Garden            D      D-
      Stevenshire    238 Andrew Rue             A-       A
AL    Clarkland      430 Douglas Mission        B+       F
 ...       ...       ...          ...          ...     ...
WY    Lake Nicole    754 Weaver Turnpike         D      D-
                     933 Jennifer Burg           C      A+
      Martintown     013 Bell Mills             B-       D
      Port Jason     624 Faulkner Orchard       C+       F
      Reneeshire     717 Patel Square            A      B+

251 rows × 2 columns

Syntax tends to become confusing and error-prone when it involves multiple parentheses and brackets. We can simplify the preceding code by assigning the list to a variable and breaking its tuples across several lines:

In  [41] columns = [
             ("Services", "Schools"),
             ("Culture", "Museums")
         ]

         neighborhoods[columns]

Out [41]

Category                                  Services Culture
Subcategory                                Schools Museums
**State City           Street** 
AK    Rowlandchester 386 Rebecca Cove            C      A-
      Scottstad      082 Leblanc Freeway        B+      C-
                     114 Jones Garden            D      D-
      Stevenshire    238 Andrew Rue             A-       A
AL    Clarkland      430 Douglas Mission        B+       F
 ...        ...      ...      ...               ...    ...
WY    Lake Nicole    754 Weaver Turnpike         D      D-
                     933 Jennifer Burg           C      A+
      Martintown     013 Bell Mills             B-       D
      Port Jason     624 Faulkner Orchard       C+       F
      Reneeshire     717 Patel Square            A      B+

251 rows × 2 columns

The previous two examples accomplish the same result, but this code is significantly easier to read; its syntax clearly identifies where each tuple begins and ends.

7.4.2 提取一个或多个行使用 loc

Chapter 4 introduced the loc and iloc accessors for selecting rows and columns from a DataFrame. The loc accessor extracts by index label, and the iloc accessor extracts by index position. Here’s a quick review, using the df DataFrame we declared in section 7.4.1:

In  [42] df

Out [42]

 **X  Y**
A  1  2
B  3  4

The next example uses loc to select the row with an index label of "A":

In  [43] df.loc["A"]

Out [43] X    1
         Y    2
         Name: A, dtype: int64

下一个示例使用 iloc 来选择索引位置为 1 的行:

In  [44] df.iloc[1]

Out [44] X    3
         Y    4
         Name: B, dtype: int64

We can use the loc and iloc accessors to pull rows from a MultiIndex DataFrame. Let’s start slow and work our way up.

The neighborhoods DataFrame’s MultiIndex has three levels: State, City, and Address. If we know the values to target in each level, we can pass them in a tuple within the square brackets. When we provide a value for a level, we remove the need for the level to exist in the result. The next example provides "TX" for the State level, "Kingchester" for the City level, and "534 Gordon Falls" for the Address level. Pandas returns a Series object with an index constructed from the column headers in neighborhoods:

In  [45] neighborhoods.loc[("TX", "Kingchester", "534 Gordon Falls")]

Out [45] Category  Subcategory
         Culture   Restaurants     C
                   Museums        D+
         Services  Police          B
                   Schools         B
         Name: (TX, Kingchester, 534 Gordon Falls), dtype: object

If we pass a single label in the square brackets, pandas looks for it in the outermost MultiIndex level. The next example selects the rows with a State value of "CA". State is the first level of the rows’ MultiIndex:

In  [46] neighborhoods.loc["CA"]

Out [46]

Category                           Culture         Services
Subcategory                    Restaurants Museums   Police Schools
**City           Street** 
Dustinmouth    793 Cynthia ...          A-      A+       C-       A
North Jennifer 303 Alisha Road          D-      C+       C+      A+
Ryanfort       934 David Run             F      B+        F      D-

Pandas returns a DataFrame with a two-level MultiIndex. Notice that the State level is not present. There is no longer a need for it because all three rows belong to that level; there is no longer any variation to display.

Usually, the second argument to the square brackets denotes the column(s) we’d like to extract, but we can also provide the value to look for in the next MultiIndex level. The next example targets rows with a State value of "CA" and a City value of "Dustinmouth". Once again, pandas returns a DataFrame with one fewer level. Because only one level is left, pandas falls back to a plain Index object to store the row labels from the Street level:

In  [47] neighborhoods.loc["CA", "Dustinmouth"]

Out [47]

Category               Culture         Services
Subcategory        Restaurants Museums   Police Schools
**Street** 
793 Cynthia Square          A-      A+       C-       A

We can still use the second argument to loc to declare the column(s) to extract. The next example extracts rows with a State value of "CA" in the row MultiIndex and a Category value of "Culture" in the column MultiIndex:

In  [48] neighborhoods.loc["CA", "Culture"]

Out [48]

Subcategory                       Restaurants Museums
**City           Street** 
Dustinmouth    793 Cynthia Square          A-      A+
North Jennifer 303 Alisha Road             D-      C+
Ryanfort       934 David Run                F      B+

The syntax in the previous two examples is not ideal because of its ambiguity. The second argument to loc can represent either a value from the second level of the rows’ MultiIndex or a value from the 第一级 of the columns’ MultiIndex:

pandas 文档 ¹ 建议以下索引策略以避免不确定性。使用 loc 的第一个参数作为行索引标签,第二个参数作为列索引标签。将给定索引的所有参数都包裹在一个元组中。按照这个标准,我们应该将我们的行级别值放在一个元组中,同样,我们的列级别值也应该放在一个元组中。访问具有 "CA" 状态值和 "Dustinmouth" 市值的行的推荐方式如下:

In  [49] neighborhoods.loc[("CA", "Dustinmouth")]

Out [49]

Category               Culture         Services
Subcategory        Restaurants Museums   Police Schools
**Street** 
793 Cynthia Square          A-      A+       C-       A

这个语法更直接、更一致;它允许 loc 的第二个参数始终代表列的索引标签以进行定位。下一个示例提取了相同州 "CA" 和市 "Dustinmouth" 的服务列。我们在元组内传递 "Services"。一个元素的元组需要一个逗号,以便 Python 能够将其识别为元组:

In  [50] neighborhoods.loc[("CA", "Dustinmouth"), ("Services",)]

Out [50]

Subcategory        Police Schools
**Street** 
793 Cynthia Square     C-       A

这里还有一个有用的提示:pandas 区分列表和元组参数以访问器。使用列表来存储多个键。使用元组来存储一个多级键的组成部分。

我们可以将元组作为 loc 的第二个参数传递,为列的 MultiIndex 级别提供值。下一个示例针对

  • 行的 MultiIndex 级别中的 "CA""Dustinmouth"

  • 列的 MultiIndex 级别中的 "Services""Schools"

"Services""Schools" 放在一个元组中,告诉 pandas 将它们视为构成单个标签的组成部分。"Services" 是类别级别的值,而 "Schools" 是子类别级别的值:

In  [51] neighborhoods.loc[("CA", "Dustinmouth"), ("Services", "Schools")]

Out [51] Street
         793 Cynthia Square    A
         Name: (Services, Schools), dtype: object

关于选择连续行怎么办?我们可以使用 Python 的列表切片语法。我们在起点和终点之间放置一个冒号。下一个代码示例提取了所有在 "NE""NH" 之间的连续行。在 pandas 切片中,终点(冒号后的值)是包含的:

In  [52] neighborhoods["NE":"NH"]

Out [52]

Category                                  Culture         Services
Subcategory                           Restaurants Museums   Police Schools
**State City            Street** 
NE    Barryborough    460 Anna Tunnel          A+      A+        B       A
      Shawnchester    802 Cook Cliff           D-      D+        D       A
      South Kennet... 346 Wallace ...          C-      B-        A      A-
      South Nathan    821 Jake Fork            C+       D       D+       A
NH    Courtneyfort    697 Spencer ...          A+      A+       C+      A+
      East Deborah... 271 Ryan Mount            B       C       D+      B-
      Ingramton       430 Calvin U...          C+      D+        C      C-
      North Latoya    603 Clark Mount          D-      A-       B+      B-
      South Tara      559 Michael ...          C-      C-        F       B

我们可以将列表切片语法与元组参数结合使用。下一个示例提取所有行,这些行

  • 从州级别 "NE" 和市级别 "Shawnchester" 的值开始

  • 在州级别以 "NH" 结尾,在市级别以 "North Latoya" 结尾

In  [53] neighborhoods.loc[("NE", "Shawnchester"):("NH", "North Latoya")]

Out [53]

Category                                  Culture         Services
Subcategory                           Restaurants Museums   Police Schools
**State City            Street** 
NE    Shawnchester    802 Cook Cliff           D-      D+        D       A
      South Kennet... 346 Wallace ...          C-      B-        A      A-
      South Nathan    821 Jake Fork            C+       D       D+       A
NH    Courtneyfort    697 Spencer ...          A+      A+       C+      A+
      East Deborah... 271 Ryan Mount            B       C       D+      B-
      Ingramton       430 Calvin U...          C+      D+        C      C-
      North Latoya    603 Clark Mount          D-      A-       B+      B-

注意这个语法;单个缺失的括号或逗号都可能引发异常。我们可以通过将元组分配给描述性变量并将提取分解成更小的部分来简化代码。下一个示例返回相同的结果集,但更容易阅读:

In  [54] start = ("NE", "Shawnchester")
         end   = ("NH", "North Latoya")
         neighborhoods.loc[start:end]

Out [54]

Category                                  Culture         Services
Subcategory                           Restaurants Museums   Police Schools
**State City            Street** 
NE    Shawnchester    802 Cook Cliff           D-      D+        D       A
      South Kennet... 346 Wallace ...          C-      B-        A      A-
      South Nathan    821 Jake Fork            C+       D       D+       A
NH    Courtneyfort    697 Spencer ...          A+      A+       C+      A+
      East Deborah... 271 Ryan Mount            B       C       D+      B-
      Ingramton       430 Calvin U...          C+      D+        C      C-
      North Latoya    603 Clark Mount          D-      A-       B+      B-

我们不必为每个级别提供每个元组的值。下一个示例没有为第二个元组包含市级别的值:

In  [55] neighborhoods.loc[("NE", "Shawnchester"):("NH")]

Out [55]

Category                                  Culture         Services
Subcategory                           Restaurants Museums   Police Schools
**State City            Street** 
NE    Shawnchester    802 Cook Cliff           D-      D+        D       A
      South Kennet... 346 Wallace ...          C-      B-        A      A-
      South Nathan    821 Jake Fork            C+       D       D+       A
NH    Courtneyfort    697 Spencer ...          A+      A+       C+      A+
      East Deborah... 271 Ryan Mount            B       C       D+      B-
      Ingramton       430 Calvin U...          C+      D+        C      C-
      North Latoya    603 Clark Mount          D-      A-       B+      B-
      South Tara      559 Michael ...          C-      C-        F       B

Pandas 从 ("NE", "Shawnchester") 开始提取行,直到遇到所有具有 "NH" 状态值的行末尾。

7.4.3 使用 iloc 提取一个或多个行

iloc 访问器通过索引位置提取行和列。以下示例应该会帮助你回顾第四章中介绍的概念。我们可以向 iloc 传递一个索引位置来提取单行:

In  [56] neighborhoods.iloc[25]

Out [56] Category  Subcategory
         Culture   Restaurants    A+
                   Museums         A
         Services  Police         A+
                   Schools        C+
         Name: (CT, East Jessicaland, 208 Todd Knolls), dtype: object

我们可以向 iloc 传递两个参数来表示行和列索引。下一个示例针对索引位置为 25 的行和索引位置为 2 的列:

In  [57] neighborhoods.iloc[25, 2]

Out [57] 'A+'

我们可以通过将它们的索引位置包裹在列表中来提取多行:

In  [58] neighborhoods.iloc[[25, 30]]

Out [58]

Category                                  Culture         Services
Subcategory                           Restaurants Museums   Police Schools
**State City            Street** 
CT    East Jessica... 208 Todd Knolls          A+       A       A+      C+
DC    East Lisaview   910 Sandy Ramp           A-      A+        B       B

在切片方面,lociloc 之间有很大的区别。当我们使用 iloc 进行索引切片时,终点是排他的。在前面的例子中,街道为 "910 Sandy Ramp" 的记录的索引位置是 30。当我们提供 30 作为下一个示例中 iloc 终点的值时,pandas 提取到该索引,但不包括它:

In  [59] neighborhoods.iloc[25:30]

Out [59]

Category                                  Culture         Services
Subcategory                           Restaurants Museums   Police Schools
**State City            Street** 
CT    East Jessica... 208 Todd Knolls          A+       A       A+      C+
      New Adrianhaven 048 Brian Cove           A-      C+       A+      D-
      Port Mike       410 Keith Lodge          D-       A       B+       D
      Sethstad        139 Bailey G...           C      C-       C+      A+
DC    East Jessica    149 Norman C...          A-      C-       C+      A-

列切片遵循相同的原理。下一个示例从索引位置 1 到 3(不包括)提取列:

In  [60] neighborhoods.iloc[25:30, 1:3]

Out [60]

Category                              Culture Services
Subcategory                           Museums   Police
**State City            Street** 
CT    East Jessica... 208 Todd Knolls       A       A+
      New Adrianhaven 048 Brian Cove       C+       A+
      Port Mike       410 Keith Lodge       A       B+
      Sethstad        139 Bailey G...      C-       C+
DC    East Jessica    149 Norman C...      C-       C+

Pandas 还允许使用负切片。下一个示例从倒数第四行开始提取行,从倒数第二列开始提取列:

In  [61] neighborhoods.iloc[-4:, -2:]

Out [61]

Category                          Services
Subcategory                         Police Schools
**State City        Street** 
WY    Lake Nicole 933 Jennifer...       A-       C
      Martintown  013 Bell Mills        A-      B-
      Port Jason  624 Faulkner...       C+      C+
      Reneeshire  717 Patel Sq...        D       A

Pandas 为每个 DataFrame 行分配一个索引位置,而不是给定索引级别中的每个值。因此,我们无法使用 iloc 在连续的 MultiIndex 级别之间进行索引。这种限制是 pandas 开发团队有意设计的一个决策。正如开发者 Jeff Reback 所说,iloc 作为“严格的位置索引器”,“根本不考虑 DataFrame 的结构。” ²

7.5 横截面

xs 方法允许我们通过提供一个 MultiIndex 级别的值来提取行。我们向该方法传递一个带有要查找的值的 key 参数。我们传递 level 参数为要查找值的数字位置或索引级别的名称。例如,假设我们想要找到湖妮可市的所有地址,无论州或街道。城市是 MultiIndex 的第二级;它在级别层次结构中的索引位置为 1:

In  [62] # The two lines below are equivalent
         neighborhoods.xs(key = "Lake Nicole", level = 1)
         neighborhoods.xs(key = "Lake Nicole", level = "City")

Out [62]

Category                      Culture         Services
Subcategory               Restaurants Museums   Police Schools
**State Street** 
OR    650 Angela Track              D      C-        D       F
WY    754 Weaver Turnpike           B      D-        B       D
      933 Jennifer Burg             C      A+       A-       C

在两个州中,湖妮可市有三个地址。注意,pandas 从新的 DataFrameMultiIndex 中移除了城市级别。City 值是固定的("Lake Nicole"),因此 pandas 没有必要包含它。

我们可以通过将 axis 参数的参数设置为 "columns" 来将相同的提取技术应用于列。下一个示例选择具有 Subcategory 级别中 "Museums" 键的列 MultiIndex。只有一个列符合这个描述:

In  [63] neighborhoods.xs(
             axis = "columns", key = "Museums", level = "Subcategory"
         ).head()

Out [63]
Category                                 Culture
**State City           Street** 
AK    Rowlandchester 386 Rebecca Cove         A-
      Scottstad      082 Leblanc Freeway      C-
                     114 Jones Garden         D-
      Stevenshire    238 Andrew Rue            A
AL    Clarkland      430 Douglas Mission       F

注意,子类别级别在返回的 DataFrame 中不存在,但类别级别仍然存在。Pandas 包含它是因为类别级别仍然存在变化的可能性(如多个值)。当我们从中间级别提取值时,它们可能属于多个顶级标签。

我们也可以向 xs 方法提供跨越非连续 MultiIndex 级别的键。我们将它们作为一个元组传递。假设我们想要找到具有 "238 Andrew Rue" 街道值和 "AK" 州值的行,无论城市值如何。这对 xs 来说不是问题:

In  [64] # The two lines below are equivalent
         neighborhoods.xs(
             key = ("AK", "238 Andrew Rue"), level = ["State", "Street"]
         )

         neighborhoods.xs(
             key = ("AK", "238 Andrew Rue"), level = [0, 2]
         )

Out [64]

Category        Culture         Services
Subcategory Restaurants Museums   Police Schools
**City** 
Stevenshire          D-       A       A-      A-

能够仅针对一个级别的值进行操作是 MultiIndex 的一个强大功能。

7.6 索引操作

在本章的开头,我们通过改变 read_csv 函数的参数,将我们的邻里数据集扭曲成当前的形状。Pandas 也允许我们操作现有的 DataFrame 上的索引。让我们看一下。

7.6.1 重置索引

邻里 DataFrame 当前具有 State 作为其最外层的 MultiIndex 级别,后面跟着 City 和 Street:

In  [65] neighborhoods.head()

Out [65]

Category                                   Culture         Services
Subcategory                            Restaurants Museums   Police Schools
**State City           Street** 
AK    Rowlandchester 386 Rebecca Cove           C-      A-       A+       C
      Scottstad      082 Leblanc Fr...           D      C-        D      B+
                     114 Jones Garden           D-      D-        D       D
      Stevenshire    238 Andrew Rue             D-       A       A-      A-
AL    Clarkland      430 Douglas Mi...           A       F       C+      B+

reorder_levels 方法按照指定的顺序排列 MultiIndex 级别。我们向其 order 参数传递一个所需顺序的级别列表。下一个示例交换了 City 和 State 级别的位置:

In  [66] new_order = ["City", "State", "Street"]
         neighborhoods.reorder_levels(order = new_order).head()

Out [66]

Category                                 Culture         Services
Subcategory                          Restaurants Museums   Police Schools
**City           State Street** 
Rowlandchester AK    386 Rebecca ...          C-      A-       A+       C
Scottstad      AK    082 Leblanc ...           D      C-        D      B+
                     114 Jones Ga...          D-      D-        D       D
Stevenshire    AK    238 Andrew Rue           D-       A       A-      A-
Clarkland      AL    430 Douglas ...           A       F       C+      B+

我们也可以向 order 参数传递一个整数列表。这些数字必须代表 MultiIndex 级别的当前索引位置。如果我们想 State 成为新的 MultiIndex 中的第一个级别,例如,我们必须以 1 开始列表——这是 State 级别在当前 MultiIndex 中的索引位置。下一个代码示例返回与上一个相同的结果:

In  [67] neighborhoods.reorder_levels(order = [1, 0, 2]).head()

Out [67]

Category                                 Culture         Services
Subcategory                          Restaurants Museums   Police Schools
**City           State Street** 
Rowlandchester AK    386 Rebecca ...          C-      A-       A+       C
Scottstad      AK    082 Leblanc ...           D      C-        D      B+
                     114 Jones Ga...          D-      D-        D       D
Stevenshire    AK    238 Andrew Rue           D-       A       A-      A-
Clarkland      AL    430 Douglas ...           A       F       C+      B+

如果我们想去除索引呢?也许我们想设置不同组合的列作为索引标签。reset_index 方法返回一个新的 DataFrame,它将之前的 MultiIndex 级别作为列整合。Pandas 将之前的 MultiIndex 替换为其标准的数值索引:

In  [68] neighborhoods.reset_index().tail()

Out [68]

Category    State     City   Street     Culture         Services
Subcategory                         Restaurants Museums   Police Schools
246            WY  Lake...  754 ...        B         D-        B       D
247            WY  Lake...  933 ...        C         A+       A-       C
248            WY  Mart...  013 ...       C-          D       A-      B-
249            WY  Port...  624 ...       A-          F       C+      C+
250            WY  Rene...  717 ...        B         B+        D       A

注意,三个新列(State、City 和 Street)成为列的 MultiIndex 的最外层级别 Category 的值。为了确保列之间的一致性(使每一列都成为两个值的元组),pandas 将三个新列分配一个空字符串的子类别值。

我们可以将三个列添加到另一个 MultiIndex 级别。将所需级别的索引位置或名称传递给 reset_index 方法的 col_level 参数。下一个示例将 State、City 和 Street 列整合到列的 MultiIndex 的子类别级别:

In  [69] # The two lines below are equivalent
         neighborhoods.reset_index(col_level = 1).tail()
         neighborhoods.reset_index(col_level = "Subcategory").tail()

Out [69]

Category                                Culture         Services
Subcategory State     City   Street Restaurants Museums   Police Schools
246            WY  Lake...  754 ...        B         D-        B       D
247            WY  Lake...  933 ...        C         A+       A-       C
248            WY  Mart...  013 ...       C-          D       A-      B-
249            WY  Port...  624 ...       A-          F       C+      C+
250            WY  Rene...  717 ...        B         B+        D       A

现在 pandas 将默认为空字符串用于类别,这是包含 State、City 和 Street 的子类别级别的父级别。我们可以通过传递一个参数到 col_fill 参数来用我们选择的价值替换空字符串。在下一个示例中,我们在地址父级别下对三个新列进行分组。现在外部的类别级别包含三个不同的值:地址、文化和服务:

In  [70] neighborhoods.reset_index(
             col_fill = "Address", col_level = "Subcategory"
         ).tail()

Out [70]

Category    Address                       Culture         Services
Subcategory   State     City   Street Restaurants Museums   Police Schools
246              WY  Lake...  754 ...        B         D-        B       D
247              WY  Lake...  933 ...        C         A+       A-       C
248              WY  Mart...  013 ...       C-          D       A-      B-
249              WY  Port...  624 ...       A-          F       C+      C+
250              WY  Rene...  717 ...        B         B+        D       A

reset_index 的标准调用将所有索引级别转换为常规列。我们也可以通过传递其名称到 levels 参数来移动单个索引级别。下一个示例将 Street 级别从 MultiIndex 移动到常规 DataFrame 列:

In  [71] neighborhoods.reset_index(level = "Street").tail()

Out [71]

Category                      Street     Culture         Services
Subcategory                          Restaurants Museums   Police Schools
**State City** 
WY    Lake Nicole  754 Weaver Tur...           B      D-        B       D
      Lake Nicole  933 Jennifer Burg           C      A+       A-       C
      Martintown      013 Bell Mills          C-       D       A-      B-
      Port Jason   624 Faulkner O...          A-       F       C+      C+
      Reneeshire    717 Patel Square           B      B+        D       A

我们可以通过传递一个列表来移动多个索引级别:

In  [72] neighborhoods.reset_index(level = ["Street", "City"]).tail()

Out [72]

Category            City       Street     Culture         Services
Subcategory                           Restaurants Museums   Police Schools
**State** 
WY           Lake Nicole  754 Weav...           B      D-        B       D
WY           Lake Nicole  933 Jenn...           C      A+       A-       C
WY            Martintown  013 Bell...          C-       D       A-      B-
WY            Port Jason  624 Faul...          A-       F       C+      C+
WY            Reneeshire  717 Pate...           B      B+        D       A

如果我们要从 MultiIndex 中移除一个级别怎么办?如果我们向 reset_index 方法的 drop 参数传递一个值为 True,pandas 将删除指定的级别而不是将其添加到列中。下一个 reset_index 示例移除了 Street 级别:

In  [73] neighborhoods.reset_index(level = "Street", drop = True).tail()

Out [73]

Category              Culture         Services
Subcategory       Restaurants Museums   Police Schools
**State City** 
WY    Lake Nicole           B      D-        B       D
      Lake Nicole           C      A+       A-       C
      Martintown           C-       D       A-      B-
      Port Jason           A-       F       C+      C+
      Reneeshire            B      B+        D       A

为了为 7.6.2 节做准备,让我们通过用新的DataFrame覆盖neighborhoods变量来使索引重置永久化。此操作将所有三个索引级别移动到DataFrame的列中:

In  [74] neighborhoods = neighborhoods.reset_index()

现在我们有七个列在社区中,只有列轴上的MultiIndex

7.6.2 设置索引

让我们检查一下我们的DataFrame以唤醒我们的记忆:

In  [75] neighborhoods.head(3)

Out [75]

Category    State     City   Street     Culture         Services
Subcategory                         Restaurants Museums   Police Schools
0              AK  Rowl...  386 ...       C-         A-       A+       C
1              AK  Scot...  082 ...        D         C-        D      B+
2              AK  Scot...  114 ...       D-         D-        D       D

set_index方法将一个或多个DataFrame列设置为新的索引。我们可以将列传递给其keys参数:

In  [76] neighborhoods.set_index(keys = "City").head()

Out [76]

Category       State          Street     Culture         Services
Subcategory                          Restaurants Museums   Police Schools
**City** 
Rowlandchester    AK  386 Rebecca...          C-      A-       A+       C
Scottstad         AK  082 Leblanc...           D      C-        D      B+
Scottstad         AK  114 Jones G...          D-      D-        D       D
Stevenshire       AK  238 Andrew Rue          D-       A       A-      A-
Clarkland         AL  430 Douglas...           A       F       C+      B+

如果我们想让最后四个列中的一个作为索引怎么办?下一个示例通过将包含要针对每个MultiIndex级别值的元组传递给keys参数:

In  [77] neighborhoods.set_index(keys = ("Culture", "Museums")).head()

Out [77]

Category    State       City     Street     Culture Services
Subcategory                             Restaurants   Police Schools
**(Cultur...** 
A-             AK  Rowlan...  386 Re...         C-        A+       C
C-             AK  Scottstad  082 Le...          D         D      B+
D-             AK  Scottstad  114 Jo...         D-         D       D
A              AK  Steven...  238 An...         D-        A-      A-
F              AL  Clarkland  430 Do...          A        C+      B+

要在行轴上创建MultiIndex,我们可以将包含多个列的列表传递给keys参数:

In  [78] neighborhoods.set_index(keys = ["State", "City"]).head()

Out [78]

Category                      Street     Culture         Services 
Subcategory                          Restaurants Museums   Police Schools
**State City** 
AK    Rowlandchester  386 Rebecca...          C-      A-       A+       C
      Scottstad       082 Leblanc...           D      C-        D      B+
      Scottstad       114 Jones G...          D-      D-        D       D
      Stevenshire     238 Andrew Rue          D-       A       A-      A-
AL    Clarkland       430 Douglas...           A       F       C+      B+

正如在 pandas 中经常看到的那样,有许多排列和组合可以用于分析数据集。在定义DataFrame的索引时,问问自己当前问题中最重要的是哪些值。关键信息是什么?几份数据是否本质上相互关联?你希望将哪些数据点存储为行或列?行或列是否构成一个组或类别?对于许多这些问题,MultiIndex可以提供有效的解决方案来存储你的数据。

7.7 编码挑战

这是练习本章引入的概念的机会。

7.7.1 问题

investments.csv 数据集包含来自网站 Crunchbase 的超过 27,000 条创业投资记录。每个初创公司都有一个名称、一个市场、一个状态、一个运营状态和一个融资轮次数量:

In  [79] investments = pd.read_csv("investments.csv")
         investments.head()

Out [79]

 **Name      Market     Status State  Funding Rounds**
0            #waywire       News    Acquired    NY               1
1  &TV Communications      Games   Operating    CA               2
2  -R- Ranch and Mine    Tourism   Operating    TX               2
3    004 Technologies   Software   Operating    IL               1
4             1-4 All   Software   Operating    NC               1

让我们在DataFrame上添加一个MultiIndex。我们可以通过使用nunique方法来识别每个列中唯一值的数量。具有少量唯一项的列通常表示分类数据,并且是索引级别的良好候选者:

In  [80] investments.nunique()

Out [80] Name              27763
         Market              693
         Status                3
         State                61
         Funding Rounds       16
         dtype: int64

让我们创建一个包含状态、融资轮次和状态列的三级MultiIndex。我们将按列中值数量最少的原则排序列。级别中的唯一值越少,pandas 提取其行就越快。我们还将对DataFrame索引进行排序以加速查找时间:

In  [81] investments = investments.set_index(
             keys = ["Status", "Funding Rounds", "State"]
         ).sort_index()

这是投资目前的样子:

In  [82] investments.head()

Out [82]
                                              Name               Market
**Status   Funding Rounds State** 
Acquired 1              AB          Hallpass Media                Games
                        AL               EnteGreat   Enterprise Soft...
                        AL     Onward Behaviora...        Biotechnology
                        AL                 Proxsys        Biotechnology
                        AZ             Envox Group     Public Relations

这里是该节的一些挑战:

  1. 提取所有状态为"Closed"的行。

  2. 提取所有状态为"Acquired"并且有十个融资轮次的行。

  3. 提取所有状态为"Operating"、六个融资轮次和状态为"NJ"的行。

  4. 提取所有状态为"Closed"并且有八个融资轮次的行,只提取名称列。

  5. 提取所有状态为"NJ"的行,无论状态和融资轮次级别的值如何。

  6. MultiIndex级别重新整合回DataFrame作为列。

7.7.2 解决方案

让我们逐一解决这些问题:

  1. 要提取所有状态为 "Closed" 的行,我们可以使用 loc 访问器。我们将传递一个包含单个值 "Closed" 的元组。请记住,单元素元组需要逗号:

    In  [83] investments.loc[("Closed",)].head()
    
    Out [83]
    
                                                Name                 Market
    **Funding Rounds State** 
    1              AB    Cardinal Media Technologies   Social Network Media
                   AB               Easy Bill Online               Tracking
                   AB                  Globel Direct       Public Relations
                   AB              Ph03nix New Media                  Games
                   AL                          Naubo                   News
    
  2. 接下来,我们需要提取符合两个条件的行:状态值为 "Acquired" 和融资轮次值为 10。这些是 MultiIndex 中的连续级别。我们可以将包含正确值的元组传递给 loc 访问器:

    In  [84] investments.loc[("Acquired", 10)]
    
    Out [84]
    
                       Name        Market
    **State** 
    NY     Genesis Networks   Web Hosting
    TX       ACTIVE Network      Software
    
  3. 我们可以使用之前两个问题中使用的相同解决方案。这次,我们需要提供一个包含三个值的元组,每个值对应于 MultiIndex 的一个级别:

    In  [85] investments.loc[("Operating", 6, "NJ")]
    
    Out [85]
    
                                                  Name              Market
    **Status    Funding Rounds State** 
    Operating 6              NJ     Agile Therapeutics       Biotechnology
                             NJ               Agilence   Retail Technology
                             NJ      Edge Therapeutics       Biotechnology
                             NJ                Nistica         Web Hosting
    
  4. 要提取 DataFrame 列,我们可以向 loc 访问器传递第二个参数。对于这个问题,我们将传递一个包含名称列的单元素元组。第一个参数仍然包含状态和融资轮次级别的值:

    In  [86] investments.loc[("Closed", 8), ("Name",)]
    
    Out [86]
    
                          Name
    **State** 
    CA               CipherMax
    CA      Dilithium Networks
    CA                 Moblyng
    CA                SolFocus
    CA                Solyndra
    FL     Extreme Enterprises
    GA                MedShape
    NC     Biolex Therapeutics
    WA              Cozi Group
    
  5. 下一个挑战要求我们提取在州级别具有 "NJ" 值的行。我们可以使用 xs 方法,将级别索引位置或级别名称传递给 level 参数:

    In  [87] # The two lines below are equivalent
             investments.xs(key = "NJ", level = 2).head()
             investments.xs(key = "NJ", level = "State").head()
    
    Out [87]
    
                                        Name               Market
    **Status   Funding Rounds** 
    Acquired 1                         AkaRx        Biotechnology
             1                Aptalis Pharma        Biotechnology
             1                        Cadent             Software
             1               Cancer Genetics  Health And Wellness
             1                     Clacendix           E-Commerce
    
  6. 最后,我们希望将 MultiIndex 级别作为列添加回 DataFrame。我们将调用 reset_index 方法来重新整合索引级别,并覆盖 investments DataFrame 以使更改永久:

    In  [88] investments = investments.reset_index()
             investments.head()
    
    Out [88]
    
     **Status  Funding Rounds State                 Name               Market**
    0  Acquired               1    AB       Hallpass Media                Games
    1  Acquired               1    AL            EnteGreat  Enterprise Software
    2  Acquired               1    AL  Onward Behaviora...        Biotechnology
    3  Acquired               1    AL              Proxsys        Biotechnology
    4  Acquired               1    AZ          Envox Group     Public Relations
    

恭喜您完成编码挑战!

摘要

  • MultiIndex 是由多个级别组成的索引。

  • MultiIndex 使用值的元组来存储其标签。

  • DataFrame 可以在其行和列轴上存储 MultiIndex

  • sort_index 方法对 MultiIndex 级别进行排序。Pandas 可以单独或作为一组对索引级别进行排序。

  • 基于标签的 loc 和基于位置的 iloc 访问器需要额外的参数来提取正确的行和列组合。

  • lociloc 访问器传递元组以避免歧义。

  • reset_index 方法将索引级别作为 DataFrame 列整合。

  • set_index 方法传递列的列表,以从现有的 DataFrame 列构建 MultiIndex


¹ 请参阅“使用分层索引的高级索引”,mng.bz/5WJO

² 请参阅 Jeff Reback 的“loc 和 iloc 在 MultiIndex 中的不一致行为”,github.com/pandas-dev/pandas/issues/15228

8 重塑和交叉

本章涵盖

  • 比较宽格式和窄格式数据

  • DataFrame 生成交叉表

  • 通过求和、平均值、计数等方式聚合值

  • 堆叠和取消堆叠 DataFrame 索引级别

  • DataFrame 熔化

一个数据集可能以不适合我们对其进行分析的格式出现。有时,问题局限于特定的列、行或单元格。一列可能有错误的数据类型,一行可能有缺失值,或一个单元格可能有错误的字符大小写。在其他时候,数据集可能存在更大的结构问题,这些问题超出了数据本身。也许数据集存储其值的方式使得提取单行变得容易,但聚合数据变得困难。

重塑 数据集意味着将其操纵成不同的形状,这种形状可以讲述一个从其原始展示中无法获取的故事。重塑提供了对数据的新视角或观点。这项技能至关重要;一项研究估计,80% 的数据分析都包括清理数据和将其扭曲成正确的形状。¹

在本章中,我们将探索新的 pandas 技巧,以将数据集塑造成我们想要的形状。首先,我们将看看如何通过简洁的交叉表来总结更大的数据集。然后我们将朝相反的方向前进,学习如何拆分聚合数据集。到那时,你将成为一个能够将数据扭曲成最适合你分析展示的大师。

8.1 宽格式与窄格式数据

在我们深入探讨更多方法之前,让我们简要地谈谈数据集结构。数据集可以以宽格式或窄格式存储其值。一个窄数据集也被称为长数据集或高数据集。这些名称反映了当我们向数据集中添加更多值时数据集扩展的方向。宽数据集在宽度上增加;它向外扩展。窄/长/高数据集在高度上增加;它向下扩展。

看一下以下表格,它测量了两个城市在两天内的温度:

 **Weekday  Miami  New York**
0   Monday    100        65
1  Tuesday    105        70

考虑到 变量,即变化的测量值。有人可能会认为这个数据集中的唯一变量是星期几和温度。但还有一个额外的变量隐藏在列名中:城市。这个数据集在两列中存储了相同的变量——温度,而不是一列。迈阿密和纽约的标题并没有描述它们所存储的数据——也就是说,100 并不是 Miami 的一种类型,就像 MondayWeekday 的一种类型一样。数据集通过将变量存储在列标题中隐藏了变化的城市的变量。我们可以将这个表格归类为宽数据集。宽数据集在水平方向上扩展。

假设我们为另外两个城市引入了温度测量值。我们不得不为相同的变量添加两个新列:温度。注意数据集扩展的方向。数据变宽了,而不是变高:

 **Weekday  Miami  New York  Chicago  San Francisco**
0   Monday    100        65       50             60
1  Tuesday    105        70       58             62

水平扩展是坏事吗?不一定。宽数据集非常适合查看汇总图——完整的故事。如果我们关心的是周一和周二的温度,数据集很容易阅读和理解。但宽格式也有其缺点。随着我们添加更多列,数据集变得难以处理。假设我们编写了计算所有天数平均温度的代码。现在温度存储在四个列中。如果我们添加另一个城市列,我们就必须修改我们的计算逻辑以包含它。设计变得不那么灵活。

窄数据集垂直增长。窄格式使得操作现有数据并添加新记录变得更容易。每个变量都被隔离到单个列中。比较本节中的第一个表格和以下表格:

 **Weekday      City  Temperature**
0   Monday     Miami          100
1   Monday  New York           65
2  Tuesday     Miami          105
3  Tuesday  New York           70

要包括两个更多城市的温度,我们应该添加行而不是列。数据会变得更长,而不是更宽:

 **Weekday           City  Temperature**
0   Monday          Miami          100
1   Monday       New York           65
2   Monday        Chicago           50
3   Monday  San Francisco           60
4  Tuesday          Miami          105
5  Tuesday       New York           70
6  Tuesday        Chicago           58
7  Tuesday  San Francisco           62

在周一定位城市的温度是否更容易?我会说不是,因为现在数据分散在四行中。但计算平均温度更容易,因为我们已经将温度值隔离到单个列中。随着我们添加更多行,平均计算逻辑保持不变。

数据集的最佳存储格式取决于我们试图从中获得的洞察力。Pandas 提供了将 DataFrames 从窄格式转换为宽格式以及相反的工具。我们将在本章的其余部分学习如何应用这两种转换。

8.2 从 DataFrame 创建透视表

我们的第一份数据集,sales_by_employee.csv,是一家虚构公司的业务交易列表。每一行包括销售的日期、销售人员的姓名、客户以及交易的收入和支出:

In  [1] import pandas as pd

In  [2] pd.read_csv("sales_by_employee.csv").head()

Out [2]

 **Date   Name       Customer  Revenue  Expenses**
0  1/1/20  Oscar  Logistics XYZ     5250       531
1  1/1/20  Oscar    Money Corp.     4406       661
2  1/2/20  Oscar     PaperMaven     8661      1401
3  1/3/20  Oscar    PaperGenius     7075       906
4  1/4/20  Oscar    Paper Pound     2524      1767

为了方便起见,让我们使用 read_csv 函数的 parse_dates 参数将日期列中的字符串转换为 datetime 对象。在此更改之后,此导入看起来很好。我们可以将 DataFrame 赋值给 sales 变量:

In  [3] sales = pd.read_csv(
            "sales_by_employee.csv", parse_dates = ["Date"]
        )

        sales.tail()

Out [3]

 **Date   Name           Customer  Revenue  Expenses**
21 2020-01-01  Creed        Money Corp.     4430       548
22 2020-01-02  Creed  Average Paper Co.     8026      1906
23 2020-01-02  Creed  Average Paper Co.     5188      1768
24 2020-01-04  Creed         PaperMaven     3144      1314
25 2020-01-05  Creed        Money Corp.      938      1053

在加载数据集后,让我们探索如何使用透视表聚合其数据。

8.2.1 pivot_table 方法

透视表 通过聚合列的值并使用其他列的值对结果进行分组。聚合 一词描述了涉及多个值的汇总计算。示例聚合包括平均、总和、中位数和计数。Pandas 中的透视表类似于 Microsoft Excel 中的透视表功能。

像往常一样,一个例子最有帮助,所以让我们解决我们的第一个挑战。多个销售人员在同一天完成了交易。此外,相同的销售人员在同一天完成了多个交易。如果我们想按日期汇总收入并查看每个销售人员对每日总量的贡献是多少?

我们遵循四个步骤来创建透视表:

  1. 选择我们想要聚合的列(s)。

  2. 选择要应用到列(s)上的聚合操作。

  3. 选择将分组聚合数据为类别的列(s)。

  4. 确定是否将组放在行轴、列轴或两个轴上。

我们一步一步来。首先,我们需要在我们的现有sales DataFrame上调用pivot_table方法。该方法index参数接受将构成数据透视表索引标签的列。Pandas 将使用该列的唯一值来分组结果。

下一个示例使用日期列的值作为数据透视表的索引标签。日期列包含五个唯一的日期。Pandas 对其sales中的所有数值列(支出和收入)应用其默认的聚合操作,即平均值:

In  [4] sales.pivot_table(index = "Date")

Out [4]

               Expenses      Revenue
**Date** 
2020-01-01   637.500000  4293.500000
2020-01-02  1244.400000  7303.000000
2020-01-03  1313.666667  4865.833333
2020-01-04  1450.600000  3948.000000
2020-01-05  1196.250000  4834.750000

该方法返回一个常规的DataFrame对象。它可能有点令人失望,但这个 DataFrame 是一个数据透视表!该表显示了按日期列中的五个唯一日期组织的平均支出和平均收入。

我们使用aggfunc参数声明聚合函数;其默认参数是"mean"。以下代码产生与前面代码相同的结果:

In  [5] sales.pivot_table(index = "Date", aggfunc = "mean")

Out [5]

               Expenses      Revenue
**Date** 
2020-01-01   637.500000  4293.500000
2020-01-02  1244.400000  7303.000000
2020-01-03  1313.666667  4865.833333
2020-01-04  1450.600000  3948.000000
2020-01-05  1196.250000  4834.750000

我们需要修改一些方法参数以达到我们的原始目标:按销售人员组织每一天的收入总和。首先,让我们将aggfunc参数的参数更改为"sum"以在支出和收入中添加值:

In  [6] sales.pivot_table(index = "Date", aggfunc = "sum")

Out [6]

            Expenses  Revenue
**Date** 
2020-01-01      3825    25761
2020-01-02      6222    36515
2020-01-03      7882    29195
2020-01-04      7253    19740
2020-01-05      4785    19339

目前,我们只关心对收入列中的值求和。values参数接受 pandas 将聚合的DataFrame列(s)。要仅聚合一个列的值,我们可以传递一个包含列名的字符串参数:

In  [7] sales.pivot_table(
            index = "Date", values = "Revenue", aggfunc = "sum"
        )

Out [7]

            Revenue
**Date** 
2020-01-01    25761
2020-01-02    36515
2020-01-03    29195
2020-01-04    19740
2020-01-05    19339

要跨多列聚合值,我们可以将values传递一个列的列表。

我们有一个按日期分组的收入总和。我们的最终步骤是传达每位销售人员对每日总收入的贡献。似乎最优的展示方式是将每位销售人员的名字放在单独的列中。换句话说,我们希望使用名称列的唯一值作为数据透视表的列标题。让我们在方法调用中添加一个columns参数,并传递参数"Name"

In  [8] sales.pivot_table(
            index = "Date",
            columns = "Name",
            values = "Revenue",
            aggfunc = "sum"
        )

Out [8]

Name          Creed   Dwight     Jim  Michael   Oscar
**Date** 
2020-01-01   4430.0   2639.0  1864.0   7172.0  9656.0
2020-01-02  13214.0      NaN  8278.0   6362.0  8661.0
2020-01-03      NaN  11912.0  4226.0   5982.0  7075.0
2020-01-04   3144.0      NaN  6155.0   7917.0  2524.0
2020-01-05    938.0   7771.0     NaN   7837.0  2793.0

就这样!我们已经按照日期和销售人员组织了收入的总和。注意数据集中存在NaNNaN表示销售人员在某一天没有sales中的行,并且没有收入值。例如,Dwight 没有 2020-01-02 日期的任何sales行。数据透视表需要存在 2020-01-02 的索引标签,以便为那天有收入值的四位销售人员。Pandas 用NaN填充缺失的空白。NaN值的存在还迫使整数转换为浮点数。

我们可以使用fill_value参数将所有数据透视表中的NaN替换为固定值。让我们用零来填补数据空白:

In  [9] sales.pivot_table(
            index = "Date",
            columns = "Name",
            values = "Revenue",
            aggfunc = "sum",
            fill_value = 0
        )

Out [9]

Name        Creed  Dwight   Jim  Michael  Oscar
**Date** 
2020-01-01   4430    2639  1864     7172   9656
2020-01-02  13214       0  8278     6362   8661
2020-01-03      0   11912  4226     5982   7075
2020-01-04   3144       0  6155     7917   2524
2020-01-05    938    7771     0     7837   2793

我们还可能想查看每个日期和销售人员的收入小计。我们可以将margins参数的参数设置为True来为每一行和每一列添加总计:

In  [10] sales.pivot_table(
             index = "Date",
             columns = "Name",
             values = "Revenue",
             aggfunc = "sum",
             fill_value = 0,
             margins = True
         )

Out [10]

Name                 Creed  Dwight    Jim  Michael  Oscar     All
**Date** 
2020-01-01 00:00:00   4430    2639   1864     7172   9656   25761
2020-01-02 00:00:00  13214       0   8278     6362   8661   36515
2020-01-03 00:00:00      0   11912   4226     5982   7075   29195
2020-01-04 00:00:00   3144       0   6155     7917   2524   19740
2020-01-05 00:00:00    938    7771      0     7837   2793   19339
All                  21726   22322  20523    35270  30709  130550

注意,将"All"包含在行标签中会改变日期的可视表示,现在包括小时、分钟和秒。Pandas 需要支持日期和字符串索引标签。字符串是唯一可以表示日期或文本值的数据类型。因此,库将索引从日期的DatetimeIndex转换为字符串的普通Index。当将日期对象转换为字符串表示时,Pandas 包括时间;它还假设没有时间的日期从一天的开始。

我们可以使用margins_name参数来自定义小计标签。以下示例将标签从"All"更改为"Total"

In  [11] sales.pivot_table(
             index = "Date",
             columns = "Name",
             values = "Revenue",
             aggfunc = "sum",
             fill_value = 0,
             margins = True,
             margins_name = "Total"
         )

Out [11]

Name                 Creed  Dwight    Jim  Michael  Oscar   Total
**Date** 
2020-01-01 00:00:00   4430    2639   1864     7172   9656   25761
2020-01-02 00:00:00  13214       0   8278     6362   8661   36515
2020-01-03 00:00:00      0   11912   4226     5982   7075   29195
2020-01-04 00:00:00   3144       0   6155     7917   2524   19740
2020-01-05 00:00:00    938    7771      0     7837   2793   19339
Total                21726   22322  20523    35270  30709  130550

理想情况下,Excel 用户会感到非常熟悉这些选项。

8.2.2 交叉表的附加选项

交叉表支持各种聚合操作。假设我们感兴趣的是每天完成的企业交易数量。我们可以将aggfunc的参数设置为"count"来计算每个日期和员工组合的sales行数:

In  [12] sales.pivot_table(
             index = "Date",
             columns = "Name",
             values = "Revenue",
             aggfunc = "count"
         )

Out [12]

Name        Creed  Dwight  Jim  Michael  Oscar
**Date** 
2020-01-01    1.0     1.0  1.0      1.0    2.0
2020-01-02    2.0     NaN  1.0      1.0    1.0
2020-01-03    NaN     3.0  1.0      1.0    1.0
2020-01-04    1.0     NaN  2.0      1.0    1.0
2020-01-05    1.0     1.0  NaN      1.0    1.0

再次强调,一个NaN值表示销售人员在某一天没有进行销售。例如,Creed 在 2020-01-03 没有完成任何销售,而 Dwight 完成了三次。以下表格列出了aggfunc参数的一些其他选项:

参数 描述
max 分组中的最大值
min 分组中的最小值
std 分组中值的标准差
median 分组中值的中间值(中位数)
size 分组中的值的数量(等同于count

我们还可以向pivot_table函数的aggfunc参数传递一个聚合函数的列表。交叉表将在列轴上创建一个MultiIndex,并将聚合存储在其最外层级别。以下示例聚合了按日期的收入的总和以及按日期的收入计数:

In  [13] sales.pivot_table(
             index = "Date",
             columns = "Name",
             values = "Revenue",
             aggfunc = ["sum", "count"],
             fill_value = 0
         )

Out [13]

              sum                            count
Name        Creed Dwight   Jim Michael Oscar  Creed Dwight Jim Michael Oscar
**Date** 
2020-01-01   4430   2639  1864    7172  9656      1      1   1       1     2
2020-01-02  13214      0  8278    6362  8661      2      0   1       1     1
2020-01-03      0  11912  4226    5982  7075      0      3   1       1     1
2020-01-04   3144      0  6155    7917  2524      1      0   2       1     1
2020-01-05    938   7771     0    7837  2793      1      1   0       1     1

我们可以通过向aggfunc参数传递一个字典来对不同列应用不同的聚合操作。使用字典的键来识别DataFrame列,并使用值来设置聚合操作。以下示例提取了每个日期和销售人员的组合的最小收入和最大支出:

In  [14] sales.pivot_table(
             index = "Date",
             columns = "Name",
             values = ["Revenue", "Expenses"],
             fill_value = 0,
             aggfunc = { "Revenue": "min", "Expenses": "max" }
         )

Out [14]

      Expenses                           Revenue
Name     Creed Dwight Jim Michael Oscar  Creed  Dwight    Jim Michael Oscar
**Date** 
20...   548      368  1305    412   531   4430    2639   1864    7172  5250
20...  1768        0   462    685  1401   8026       0   8278    6362  8661
20...     0      758  1923   1772   906      0    4951   4226    5982  7075
20...  1314        0   426   1857  1767   3144       0   3868    7917  2524
20...  1053     1475     0   1633   624    938    7771      0    7837  2793

我们还可以通过将列的列表传递给index参数,在单个轴上堆叠多个分组。以下示例在行轴上聚合了按销售人员和日期的支出总和。Pandas 返回一个具有两级MultiIndexDataFrame

In  [15] sales.pivot_table(
             index = ["Name", "Date"], values = "Revenue", aggfunc = "sum"
         ).head(10)

Out [15]

                   Revenue
**Name   Date** 
Creed  2020-01-01     4430
       2020-01-02    13214
       2020-01-04     3144
       2020-01-05      938
Dwight 2020-01-01     2639
       2020-01-03    11912
       2020-01-05     7771
Jim    2020-01-01     1864
       2020-01-02     8278
       2020-01-03     4226

通过切换index列表中字符串的顺序来重新排列交叉表MultiIndex中的级别。以下示例交换了名称和日期的位置:

In  [16] sales.pivot_table(
             index = ["Date", "Name"], values = "Revenue", aggfunc = "sum"
         ).head(10)

Out [16]

                    Revenue
**Date       Name** 
2020-01-01 Creed       4430
           Dwight      2639
           Jim         1864
           Michael     7172
           Oscar       9656
2020-01-02 Creed      13214
           Jim         8278
           Michael     6362
           Oscar       8661
2020-01-03 Dwight     11912

交叉表首先组织和排序日期值,然后在每个日期内组织和排序名称值。

8.3 索引级别的堆叠和取消堆叠

这里是当前销售情况的提醒:

In  [17] sales.head()

Out [17]

 **Date   Name       Customer  Revenue  Expenses**
0 2020-01-01  Oscar  Logistics XYZ     5250       531
1 2020-01-01  Oscar    Money Corp.     4406       661
2 2020-01-02  Oscar     PaperMaven     8661      1401
3 2020-01-03  Oscar    PaperGenius     7075       906
4 2020-01-04  Oscar    Paper Pound     2524      1767

让我们将销售数据按照员工姓名和日期来组织收入。我们将日期放在列轴上,姓名放在行轴上:

In  [18] by_name_and_date = sales.pivot_table(
             index = "Name",
             columns = "Date",
             values = "Revenue",
             aggfunc = "sum"
         )

         by_name_and_date.head(2)

Out [18]

Date    2020-01-01  2020-01-02  2020-01-03  2020-01-04  2020-01-05
**Name** 
Creed       4430.0     13214.0         NaN      3144.0       938.0
Dwight      2639.0         NaN     11912.0         NaN      7771.0

有时,我们可能想要将一个索引级别从一个轴移动到另一个轴。这种变化提供了数据的不同展示方式,我们可以决定我们更喜欢哪种视图。

stack 方法将一个索引级别从列轴移动到行轴。接下来的示例将日期索引级别从列轴移动到行轴。Pandas 创建一个 MultiIndex 来存储两个行级别:姓名和日期。因为只剩下一列的值,pandas 返回一个 Series

In  [19] by_name_and_date.stack().head(7)

Out [19]

Name    Date
Creed   2020-01-01     4430.0
        2020-01-02    13214.0
        2020-01-04     3144.0
        2020-01-05      938.0
Dwight  2020-01-01     2639.0
        2020-01-03    11912.0
        2020-01-05     7771.0
dtype: float64

注意到 DataFrame 中的 NaN 值在 Series 中不存在。Pandas 在 by_name_and_date 交叉表中保留了 NaN 值的单元格,以保持行和列的结构完整性。这个 MultiIndex Series 的形状允许 pandas 丢弃 NaN 值。

相补的 unstack 方法将一个索引级别从行轴移动到列轴。考虑以下交叉表,它按客户和销售人员分组收入。行轴有一个两级的 MultiIndex,列轴有一个常规索引:

In  [20] sales_by_customer = sales.pivot_table(
             index = ["Customer", "Name"],
             values = "Revenue",
             aggfunc = "sum"
         )

         sales_by_customer.head()

Out [20]

                           Revenue
**Customer          Name** 
Average Paper Co. Creed      13214
                  Jim         2287
Best Paper Co.    Dwight      2703
                  Michael    15754
Logistics XYZ     Dwight      9209

unstack 方法将行索引的最内层级别移动到列索引:

In  [21] sales_by_customer.unstack()

Out [21]

                   Revenue
Name                 Creed  Dwight     Jim  Michael   Oscar
**Customer** 
Average Paper Co.  13214.0     NaN  2287.0      NaN     NaN
Best Paper Co.         NaN  2703.0     NaN  15754.0     NaN
Logistics XYZ          NaN  9209.0     NaN   7172.0  5250.0
Money Corp.         5368.0     NaN  8278.0      NaN  4406.0
Paper Pound            NaN  7771.0  4226.0      NaN  5317.0
PaperGenius            NaN  2639.0  1864.0  12344.0  7075.0
PaperMaven          3144.0     NaN  3868.0      NaN  8661.0

在新的 DataFrame 中,列轴现在有一个两级的 MultiIndex,行轴有一个常规的一级索引。

8.4 熔化数据集

交叉表聚合数据集中的值。在本节中,我们将学习如何做相反的事情:将聚合的数据集合分解为非聚合的数据集合。

让我们将宽格式与窄格式框架应用于销售 DataFrame。这里有一个有效的策略来确定数据集是否为窄格式:导航到一行值,询问每个单元格其值是否是列标题所描述变量的单个测量值。以下是销售的第一行:

In  [22] sales.head(1)

Out [22]

 **Date   Name       Customer  Revenue  Expenses**
0 2020-01-01  Oscar  Logistics XYZ     5250       531

在前面的例子中,"2020-01-01" 是日期,"Oscar" 是姓名,"Logistics XYZ" 是客户,5250 是收入金额,531 是支出金额。销售 DataFrame 是一个窄数据集的例子。每一行的值代表一个给定变量的单个观察结果。没有变量在多列中重复。

当我们在宽格式或窄格式中操作数据时,我们经常需要在灵活性和可读性之间做出选择。我们可以将最后四列(姓名、客户、收入、支出)表示为单个类别列中的字段(以下示例),但并没有真正的益处,因为四个变量是独立且分开的。当数据以这种格式存储时,聚合数据会更困难:

 **Date  Category          Value**
0 2020-01-01      Name          Oscar
1 2020-01-01  Customer  Logistics XYZ
2 2020-01-01   Revenue           5250
3 2020-01-01  Expenses            531

下一个数据集,video_game_sales.csv,是超过 16,000 个视频游戏区域销售的列表。每一行包括游戏名称以及在美国(NA)、欧洲(EU)、日本(JP)和其他(其他)地区销售的单位数量(以百万计):

In  [23] video_game_sales = pd.read_csv("video_game_sales.csv")
         video_game_sales.head()

Out [23]

 **Name     NA     EU     JP  Other**
0           Wii Sports  41.49  29.02   3.77   8.46
1    Super Mario Bros.  29.08   3.58   6.81   0.77
2       Mario Kart Wii  15.85  12.88   3.79   3.31
3    Wii Sports Resort  15.75  11.01   3.28   2.96
4  Pokemon Red/Poke...  11.27   8.89  10.22   1.00

再次,让我们遍历一个样本行,并询问每个单元格是否包含正确的信息。这是 video_game_sales 的第一行:

In  [24] video_game_sales.head(1)

Out [24]

 **Name     NA     EU    JP  Other**
0  Wii Sports  41.49  29.02  3.77   8.46

第一个单元格是好的;"Wii Sports"是名称的一个例子。接下来的四个单元格有问题。41.49 不是 NA 的类型或 NA 的度量。NA(北美)不是一个其值在其列中变化的变量。NA 列的实际变量数据是销售数字。NA 代表这些销售数字的区域——一个单独且不同的变量。

因此,video_game_sales 以宽格式存储其数据。四个列(NA、EU、JP 和其他)存储相同的数据点:销售数量。如果我们添加更多的区域销售列,数据集将水平增长。如果我们可以在一个共同类别中分组多个列标题,那么这是一个提示,表明数据集正在以宽格式存储其数据。

假设我们将值"NA"、"EU"、"JP"和"Other"移动到新的区域列中。将前面的表示与以下表示进行比较:

 **Name Region  Sales**
0  Wii Sports     NA  41.49
1  Wii Sports     EU  29.02
2  Wii Sports     JP   3.77
3  Wii Sports  Other   8.46

从某种意义上说,我们正在对 video_game_sales DataFrame进行逆透视。我们正在将数据的汇总、概览视图转换为每个列存储一个变量信息的视图。

Pandas 使用melt方法熔化DataFrame。(熔化是将宽数据集转换为窄数据集的过程。)该方法接受两个主要参数:

  • id_vars参数设置标识符列,宽数据集聚合数据的列。Name 是 video_game_sales 中的标识符列。数据集按视频游戏汇总销售。

  • value_vars参数接受 pandas 将熔化和存储在新列中的值所在的列(s)。

让我们从简单开始,仅熔化 NA 列的值。在下一个示例中,pandas 遍历每个 NA 列的值,并将其分配给新DataFrame中的单独一行。库将前一个列名(NA)存储在一个新的变量列中:

In  [25] video_game_sales.melt(id_vars = "Name", value_vars = "NA").head()

Out [25]

 **Name variable  value**
0                Wii Sports       NA  41.49
1         Super Mario Bros.       NA  29.08
2            Mario Kart Wii       NA  15.85
3         Wii Sports Resort       NA  15.75
4  Pokemon Red/Pokemon Blue       NA  11.27

接下来,让我们熔化所有四个区域销售列。下一个代码示例将value_vars参数传递给来自 video_game_sales 的四个区域销售列的列表:

In  [26] regional_sales_columns = ["NA", "EU", "JP", "Other"]

         video_game_sales.melt(
             id_vars = "Name", value_vars = regional_sales_columns
         )

Out [26]

 **Name   variable  value**
0                                            Wii Sports       NA  41.49
1                                     Super Mario Bros.       NA  29.08
2                                        Mario Kart Wii       NA  15.85
3                                     Wii Sports Resort       NA  15.75
4                              Pokemon Red/Pokemon Blue       NA  11.27
 ...                                            ...          ...    ...
66259                Woody Woodpecker in Crazy Castle 5    Other   0.00
66260                     Men in Black II: Alien Escape    Other   0.00
66261  SCORE International Baja 1000: The Official Game    Other   0.00
66262                                        Know How 2    Other   0.00
66263                                  Spirits & Spells    Other   0.00

66264 rows × 3 columns

melt方法返回一个包含 66,264 行的DataFrame!相比之下,video_game_sales 有 16,566 行。新的数据集是原来的四倍长,因为它为 video_games_sales 中的每一行有四行数据。数据集存储

  • 每个视频游戏及其相应的北美销售数量为 16,566 行

  • 每个视频游戏及其相应的欧洲销售数量为 16,566 行

  • 每个视频游戏及其相应的日本销售数量为 16,566 行

  • 每个视频游戏及其相应的其他销售数量为 16,566 行

变量列包含来自 video_game_sales 的四个区域列名。值列包含来自这四个区域销售列的值。在上一个输出中,数据显示视频游戏"Woodpecker in Crazy Castle 5"在 video_game_sales 的其他列中的值为0.00

我们可以通过传递var_namevalue_name参数的参数来自定义熔融DataFrame的列名。下一个示例使用“区域”作为变量列和“销售额”作为值列:

In  [27] video_game_sales_by_region = video_game_sales.melt(
             id_vars = "Name",
             value_vars = regional_sales_columns,
             var_name = "Region",
             value_name = "Sales"
         )

         video_game_sales_by_region.head()

Out [27]

 **Name Region  Sales**
0                Wii Sports     NA  41.49
1         Super Mario Bros.     NA  29.08
2            Mario Kart Wii     NA  15.85
3         Wii Sports Resort     NA  15.75
4  Pokemon Red/Pokemon Blue     NA  11.27

窄数据比宽数据更容易聚合。假设我们想要找出所有地区每款视频游戏销售额的总和。给定熔融数据集,我们可以使用pivot_table方法通过几行代码来完成这个任务:

In  [28] video_game_sales_by_region.pivot_table(
             index = "Name", values = "Sales", aggfunc = "sum"
         ).head()

Out [28]

                               Sales
**Name** 
'98 Koshien                     0.40
.hack//G.U. Vol.1//Rebirth      0.17
.hack//G.U. Vol.2//Reminisce    0.23
.hack//G.U. Vol.3//Redemption   0.17
.hack//Infection Part 1         1.26

数据集的窄形状简化了转置过程。

8.5 爆炸值列表

有时,数据集会在同一单元格中存储多个值。我们可能想要拆分数据簇,以便每行只存储一个值。考虑 recipes.csv,这是一个包含三个菜谱的集合,每个菜谱都有一个名称和成分列表。成分存储在一个逗号分隔的字符串中:

In  [29] recipes = pd.read_csv("recipes.csv")
         recipes

Out [29]

 **Recipe                              Ingredients**
0   Cashew Crusted Chicken  Apricot preserves, Dijon mustard, cu...
1      Tomato Basil Salmon  Salmon filets, basil, tomato, olive ...
2  Parmesan Cheese Chicken  Bread crumbs, Parmesan cheese, Itali...

你还记得我们在第六章中介绍的str.split方法吗?此方法使用分隔符将字符串拆分为子字符串。我们可以通过逗号的存在来拆分每个“成分”字符串。在下一个示例中,pandas 返回一个列表的Series。每个列表存储该行的成分:

In  [30] recipes["Ingredients"].str.split(",")

Out [30]

0    [Apricot preserves,  Dijon mustard,  curry pow...
1    [Salmon filets,  basil,  tomato,  olive oil,  ...
2    [Bread crumbs,  Parmesan cheese,  Italian seas...
Name: Ingredients, dtype: object

让我们用新的列覆盖原始的“成分”列:

In  [31] recipes["Ingredients"] = recipes["Ingredients"].str.split(",")
         recipes

Out [31]

 **Recipe                              Ingredients**
0   Cashew Crusted Chicken  [Apricot preserves,  Dijon mustard, ...
1      Tomato Basil Salmon  [Salmon filets,  basil,  tomato,  ol...
2  Parmesan Cheese Chicken  [Bread crumbs,  Parmesan cheese,  It...

现在,我们如何将每个列表的值分散到多行中?explode方法为Series中的每个列表元素创建一个单独的行。我们在DataFrame上调用此方法,并传入包含列表的列:

In  [32] recipes.explode("Ingredients")

Out [32]

 **Recipe         Ingredients**
0  Cashew Crusted Chicken   Apricot preserves
0  Cashew Crusted Chicken       Dijon mustard
0  Cashew Crusted Chicken        curry powder
0  Cashew Crusted Chicken     chicken breasts
0  Cashew Crusted Chicken             cashews
1     Tomato Basil Salmon       Salmon filets
1     Tomato Basil Salmon               basil
1     Tomato Basil Salmon              tomato
1     Tomato Basil Salmon           olive oil
1     Tomato Basil Salmon     Parmesan cheese
2  Simply Parmesan Cheese        Bread crumbs
2  Simply Parmesan Cheese     Parmesan cheese
2  Simply Parmesan Cheese   Italian seasoning
2  Simply Parmesan Cheese                 egg
2  Simply Parmesan Cheese     chicken breasts

真棒!我们已经将每个成分隔离到单独的一行。请注意,explode方法需要一个列表的Series才能正常工作。

8.6 编码挑战

这是一个练习本章中介绍的重塑、转置和熔融概念的机会。

8.6.1 问题

我们为你提供了两个数据集进行操作。used_cars.csv 文件是 Craigslist 分类网站上出售的二手车的列表。每一行包括汽车的制造商、生产年份、燃料类型、传动类型和价格:

In  [33] cars = pd.read_csv("used_cars.csv")
         cars.head()

Out [33]

 **Manufacturer  Year Fuel Transmission  Price**
0        Acura  2012  Gas    Automatic  10299
1       Jaguar  2011  Gas    Automatic   9500
2        Honda  2004  Gas    Automatic   3995
3    Chevrolet  2016  Gas    Automatic  41988
4          Kia  2015  Gas    Automatic  12995

minimum_wage.csv 数据集是美国最低工资的集合。数据集有一个“州”列和多个年份列:

In  [34] min_wage = pd.read_csv("minimum_wage.csv")
         min_wage.head()

Out [34]

 **State  2010  2011  2012  2013  2014  2015   2016   2017**
0     Alabama  0.00  0.00  0.00  0.00  0.00  0.00   0.00   0.00
1      Alaska  8.90  8.63  8.45  8.33  8.20  9.24  10.17  10.01
2     Arizona  8.33  8.18  8.34  8.38  8.36  8.50   8.40  10.22
3    Arkansas  7.18  6.96  6.82  6.72  6.61  7.92   8.35   8.68
4  California  9.19  8.91  8.72  8.60  9.52  9.51  10.43  10.22

这里有一些挑战:

  1. 对 cars 中的汽车价格总和进行汇总。按燃料类型在行轴上对结果进行分组。

  2. 对 cars 中的汽车数量进行汇总。按索引轴上的制造商和列轴上的传动类型对结果进行分组。显示行和列的子总计。

  3. 对 cars 中的汽车价格的平均值进行汇总。按年份和燃料类型在索引轴上,按传动类型在列轴上对结果进行分组。

  4. 给定前一个挑战中的DataFrame,将传输级别从列轴移动到行轴。

  5. min_wage从宽格式转换为窄格式。换句话说,将数据从八个年份的列(2010-17)移动到单个列。

8.6.2 解决方案

让我们逐一解决这些问题:

  1. pivot_table 方法是添加价格列中的值并按燃料类型组织总计的最佳解决方案。我们可以使用方法的 index 参数设置数据透视表的索引标签;我们将传递一个 "Fuel" 的参数。我们将指定聚合操作为 "sum",使用 aggfunc 参数:

    In  [35] cars.pivot_table(
                 values = "Price", index = "Fuel", aggfunc = "sum"
             )
    
    Out [35]
    
                    Price
    **Fuel** 
    Diesel      986177143
    Electric     18502957
    Gas       86203853926
    Hybrid       44926064
    Other       242096286
    
  2. 我们还可以使用 pivot_table 方法按制造商和传动类型计数汽车。我们将使用 columns 参数设置传动列的值作为数据透视表的列标签。记得传递 margins 参数一个 True 的参数以显示行和列的小计:

    In  [36] cars.pivot_table(
                 values = "Price",
                 index = "Manufacturer",
                 columns = "Transmission",
                 aggfunc = "count",
                 margins = True
             ).tail()
    
    Out [36]
    
    Transmission  Automatic   Manual    Other     All
    **Manufacturer** 
    Tesla             179.0      NaN     59.0     238
    Toyota          31480.0   1367.0   2134.0   34981
    Volkswagen       7985.0   1286.0    236.0    9507
    Volvo            2665.0    155.0     50.0    2870
    All            398428.0  21005.0  21738.0  441171
    
  3. 要按年份和燃料类型在数据透视表的行轴上组织平均汽车价格,我们可以将一个字符串列表传递给 pivot_table 函数的 index 参数:

       In  [37] cars.pivot_table(
                    values = "Price",
                    index = ["Year", "Fuel"],
                    columns = ["Transmission"],
                    aggfunc = "mean"
                )
    
    Out [37]
    
    Transmission      Automatic        Manual         Other
    **Year Fuel** 
    2000 Diesel    11326.176962  14010.164021  11075.000000
         Electric   1500.000000           NaN           NaN
         Gas        4314.675996   6226.140327   3203.538462
         Hybrid     2600.000000   2400.000000           NaN
         Other     16014.918919  11361.952381  12984.642857
     ...  ...          ...           ...           ...
    2020 Diesel    63272.595930      1.000000   1234.000000
         Electric   8015.166667   2200.000000  20247.500000
         Gas       34925.857933  36007.270833  20971.045455
         Hybrid    35753.200000           NaN   1234.000000
         Other     22210.306452           NaN   2725.925926
    
    102 rows × 3 columns
    

    让我们将之前的透视表分配给 report 变量以进行下一个挑战:

    In  [38] report = cars.pivot_table(
                 values = "Price",
                 index = ["Year", "Fuel"],
                 columns = ["Transmission"],
                 aggfunc = "mean"
             )
    
  4. 下一个练习是将传动类型从列索引移动到行索引。这里 stack 方法派上用场。该方法返回一个 MultiIndex SeriesSeries 有三个级别:年份、燃料和新增的传动:

    In  [39] report.stack()
    
    Out [39]
    
    Year  Fuel      Transmission
    2000  Diesel    Automatic       11326.176962
                    Manual          14010.164021
                    Other           11075.000000
          Electric  Automatic        1500.000000
          Gas       Automatic        4314.675996
                                        ...
    2020  Gas       Other           20971.045455
          Hybrid    Automatic       35753.200000
                    Other            1234.000000
          Other     Automatic       22210.306452
                    Other            2725.925926
    Length: 274, dtype: float64 
    
  5. 接下来,我们希望将 min_wage 数据集从宽格式转换为窄格式。八个列存储相同的变量:工资本身。解决方案是 melt 方法。我们可以将 State 列声明为标识列,将八个年份列声明为变量列:

    In  [40] year_columns = [
                 "2010", "2011", "2012", "2013",
                 "2014", "2015", "2016", "2017"
             ]
    
             min_wage.melt(id_vars = "State", value_vars = year_columns)
    
    Out [40]
    
     **State variable  value**
    0          Alabama     2010   0.00
    1           Alaska     2010   8.90
    2          Arizona     2010   8.33
    3         Arkansas     2010   7.18
    4       California     2010   9.19
     ...        ...        ...     ...
    435       Virginia     2017   7.41
    436     Washington     2017  11.24
    437  West Virginia     2017   8.94
    438      Wisconsin     2017   7.41
    439        Wyoming     2017   5.26
    
    440 rows × 3 columns
    

    这里有一个额外的技巧:我们可以在调用 melt 方法时移除 value_vars 参数,仍然得到相同的 DataFrame。默认情况下,pandas 从除了我们传递给 id_vars 参数的列之外的所有列熔化数据:

    In  [41] min_wage.melt(id_vars = "State")
    
    Out [41]
    
     **State variable  value**
    0          Alabama     2010   0.00
    1           Alaska     2010   8.90
    2          Arizona     2010   8.33
    3         Arkansas     2010   7.18
    4       California     2010   9.19
     ...        ...        ...     ...
    435       Virginia     2017   7.41
    436     Washington     2017  11.24
    437  West Virginia     2017   8.94
    438      Wisconsin     2017   7.41
    439        Wyoming     2017   5.26
    
    440 rows × 3 columns
    

    我们还可以使用 var_namevalue_name 参数自定义列名。下一个示例使用 "Year""Wage" 来更好地解释每一列代表的内容:

    In  [42] min_wage.melt(
                 id_vars = "State", var_name = "Year", value_name = "Wage"
             )
    
    Out [42]
    
     **State  Year   Wage**
    0          Alabama  2010   0.00
    1           Alaska  2010   8.90
    2          Arizona  2010   8.33
    3         Arkansas  2010   7.18
    4       California  2010   9.19
     ...         ...     ...    ...
    435       Virginia  2017   7.41
    436     Washington  2017  11.24
    437  West Virginia  2017   8.94
    438      Wisconsin  2017   7.41
    439        Wyoming  2017   5.26
    
    440 rows × 3 columns
    

恭喜你完成了编码挑战!

摘要

  • pivot_table 方法聚合 DataFrame 的数据。

  • 数据透视表的聚合包括求和、计数和平均值。

  • 我们可以自定义数据透视表的行标签和列标签。

  • 我们可以使用一个或多个列的值作为数据透视表的索引标签。

  • stack 方法将一个索引级别从列索引移动到行索引。

  • unstack 方法将一个索引级别从行索引移动到列索引。

  • melt 方法通过将数据分布到各个单独的行来“取消透视”一个汇总表。这个过程将宽数据集转换为窄数据集。

  • explode 方法为列表中的每个元素创建一个单独的行条目;它需要一个列表的 Series


¹ 参见 Hadley Wickham 的文章“Tidy Data”,发表在《统计软件杂志》上,vita.had.co.nz/papers/tidy-data.pdf

9 GroupBy 对象

本章涵盖

  • 使用groupby方法按组拆分DataFrame

  • GroupBy对象中的组中提取第一行和最后一行

  • GroupBy组上执行聚合操作

  • 遍历GroupBy对象中的DataFrame

pandas 库的GroupBy对象是一个用于将DataFrame行分组到桶中的存储容器。它提供了一套方法来聚合和分析集合中的每个独立组。它允许我们在每个组中提取特定索引位置的行。它还提供了一个方便的方式来遍历行的组。GroupBy对象中包含了很多强大的功能,所以让我们看看它能做什么。

9.1 从头创建一个 GroupBy 对象

让我们创建一个新的 Jupyter Notebook 并导入 pandas 库:

In  [1] import pandas as pd

我们将从一个小的例子开始,并在第 9.2 节中深入更多技术细节。让我们首先创建一个DataFrame,它存储了超市中水果和蔬菜的价格:

In  [2] food_data = {
          "Item": ["Banana", "Cucumber", "Orange", "Tomato", "Watermelon"],
          "Type": ["Fruit", "Vegetable", "Fruit", "Vegetable", "Fruit"],
          "Price": [0.99, 1.25, 0.25, 0.33, 3.00]
        }

        supermarket = pd.DataFrame(data = food_data)

        supermarket

Out [2]

 **Item       Type  Price**
0      Banana      Fruit   0.99
1    Cucumber  Vegetable   1.25
2      Orange      Fruit   0.25
3      Tomato  Vegetable   0.33
4  Watermelon      Fruit   3.00

类型列标识一个项目所属的组。在超市数据集中有两种项目组:水果和蔬菜。我们可以使用诸如等术语来互换地描述相同的概念。多行可能属于同一类别。

GroupBy对象根据列中的共享值将DataFrame行组织到桶中。假设我们感兴趣的是水果的平均价格和蔬菜的平均价格。如果我们能将"Fruit"行和"Vegetable"行隔离到不同的组中,那么进行计算会更容易。

让我们从在超市DataFrame上调用groupby方法开始。我们需要传递给它 pandas 将用于创建组的列。下一个示例提供了类型列。该方法返回一个我们尚未见过的对象:一个DataFrameGroupByDataFrameGroupBy对象与DataFrame是分开且独立的:

In  [3] groups = supermarket.groupby("Type")
        groups

Out [3] <pandas.core.groupby.generic.DataFrameGroupBy object at
        0x114f2db90>

类型列有两个唯一值,因此GroupBy对象将存储两个组。get_group方法接受一个组名并返回一个包含相应行的DataFrame。让我们提取出"Fruit"行:

In  [4] groups.get_group("Fruit")

Out [4]

 **Item   Type  Price**
0      Banana  Fruit   0.99
2      Orange  Fruit   0.25
4  Watermelon  Fruit   3.00

我们还可以提取出"Vegetable"行:

In  [5] groups.get_group("Vegetable")

Out [5]

 **Item       Type  Price**
1  Cucumber  Vegetable   1.25
3    Tomato  Vegetable   0.33

GroupBy对象在聚合操作方面表现出色。我们的原始目标是计算超市中水果和蔬菜的平均价格。我们可以在groups上调用mean方法来计算每个组内项目的平均价格。通过几行代码,我们已经成功拆分、聚合和分析了一个数据集:

In  [6] groups.mean()

Out [6]

              Price
**Type** 
Fruit      1.413333
Vegetable  0.790000

在掌握了基础知识之后,让我们继续到一个更复杂的数据集。

9.2 从数据集中创建一个 GroupBy 对象

《财富》1000 强是美国按收入排名的前 1,000 家大公司的名单。《财富》杂志每年更新一次这个名单。fortune1000.csv 文件是 2018 年的《财富》1000 强公司的集合。每一行包括公司的名称、收入、利润、员工人数、行业和子行业:

In  [7] fortune = pd.read_csv("fortune1000.csv")
        fortune

Out [7]

 **Company  Revenues  Profits  Employees        Sector      Industry**
0         Walmart  500343.0   9862.0    2300000     Retailing  General M...
1     Exxon Mobil  244363.0  19710.0      71200        Energy  Petroleum...
2    Berkshire...  242137.0  44940.0     377000    Financials  Insurance...
3           Apple  229234.0  48351.0     123000    Technology  Computers...
4    UnitedHea...  201159.0  10558.0     260000   Health Care  Health Ca...
...       ...         ...       ...       ...           ...             ...
995  SiteOne L...    1862.0     54.6       3664   Wholesalers  Wholesale...
996  Charles R...    1858.0    123.4      11800   Health Care  Health Ca...
997     CoreLogic    1851.0    152.2       5900  Business ...  Financial...
998  Ensign Group    1849.0     40.5      21301   Health Care  Health Ca...
999           HCP    1848.0    414.2        190    Financials   Real estate

1000 rows × 6 columns

一个行业可以拥有许多公司。例如,苹果和亚马逊都属于“技术”行业。

行业是某个行业内的子类别。例如,“管道”和“石油精炼”行业属于“能源”行业。

“行业”列包含 21 个独特的行业。假设我们想要找到每个行业内的公司的平均收入。在我们使用GroupBy对象之前,让我们通过一个替代方法来解决这个问题。第五章向我们展示了如何创建一个布尔Series来从DataFrame中提取子集。下一个示例提取了所有“零售”行业的公司:

In  [8] in_retailing = fortune["Sector"] == "Retailing"
        retail_companies = fortune[in_retailing]
        retail_companies.head()

Out [8]

 **Company  Revenues  Profits  Employees     Sector           Industry**
0      Walmart  500343.0   9862.0    2300000  Retailing  General Mercha...
7   Amazon.com  177866.0   3033.0     566000  Retailing  Internet Servi...
14      Costco  129025.0   2679.0     182000  Retailing  General Mercha...
22  Home Depot  100904.0   8630.0     413000  Retailing  Specialty Reta...
38      Target   71879.0   2934.0     345000  Retailing  General Mercha...

我们可以通过使用方括号从子集中提取出收入列:

In  [9] retail_companies["Revenues"].head()

Out [9] 0     500343.0
        7     177866.0
        14    129025.0
        22    100904.0
        38     71879.0
        Name: Revenues, dtype: float64

最后,我们可以通过在收入列上调用mean方法来计算“零售”行业的平均收入:

In  [10] retail_companies["Revenues"].mean()

Out [10] 21874.714285714286

上述代码适用于计算一个行业的平均收入。然而,我们需要编写大量的额外代码,才能将相同的逻辑应用于《财富》中的其他 20 个行业。代码的可扩展性不高。Python 可以自动化一些重复性工作,但GroupBy对象提供了现成的最佳解决方案。pandas 开发者已经为我们解决了这个问题。

让我们在fortuneDataFrame上调用groupby方法。该方法接受一个列,pandas 将使用该列的值来分组行。如果列存储了行的分类数据,则列是分组的良好候选。确保有多个行属于其下的父类别。例如,数据集有 1,000 个独特的公司,但只有 21 个独特的行业,因此行业列非常适合进行汇总分析:

In  [11] sectors = fortune.groupby("Sector")

让我们输出sectors变量,看看我们正在处理什么类型的对象:

In  [12] sectors

Out [12] <pandas.core.groupby.generic.DataFrameGroupBy object at
         0x1235b1d10>

DataFrameGroupBy对象是一组DataFrame。在幕后,pandas 重复了我们用于“零售”行业的提取过程,但针对“行业”列中的所有 21 个值。

我们可以通过将GroupBy对象传递给 Python 的内置len函数来计算sectors中的组数:

In  [13] len(sectors)

Out [13] 21

sectors GroupBy对象有 21 个DataFrame。这个数字等于fortune的“行业”列中唯一值的数量,我们可以通过调用nunique方法来发现这一点:

In  [14] fortune["Sector"].nunique()

Out [14] 21

有哪些 21 个行业,以及《财富》杂志中每个行业有多少家公司?在GroupBy对象上的size方法返回一个包含按字母顺序排列的组和它们行数的Series。以下输出告诉我们,有 25 家《财富》公司属于“航空航天与国防”行业,14 家属于“服装”行业,等等:

In  [15] sectors.size()

Out [15] Sector
         Aerospace & Defense               25
         Apparel                           14
         Business Services                 53
         Chemicals                         33
         Energy                           107
         Engineering & Construction        27
         Financials                       155
         Food &  Drug Stores               12
         Food, Beverages & Tobacco         37
         Health Care                       71
         Hotels, Restaurants & Leisure     26
         Household Products                28
         Industrials                       49
         Materials                         45
         Media                             25
         Motor Vehicles & Parts            19
         Retailing                         77
         Technology                       103
         Telecommunications                10
         Transportation                    40
         Wholesalers                       44
         dtype: int64

现在我们已经将财富行分桶,让我们探索我们可以用GroupBy对象做什么。

9.3 GroupBy 对象的属性和方法

将我们的GroupBy对象可视化为字典,将 21 个行业映射到每个行业所属的财富行集合。groups属性存储一个字典,其中包含这些组到行的关联;其键是行业名称,其值是存储来自财富DataFrame的行索引位置的Index对象。该字典共有 21 个键值对,但我已将以下输出限制在前两个对以节省空间:

In  [16] sectors.groups

Out [16]

'Aerospace &  Defense': Int64Index([ 26,  50,  58,  98, 117, 118, 207, 224,
                                     275, 380, 404, 406, 414, 540, 660,
                                     661, 806, 829, 884, 930, 954, 955,
                                     959, 975, 988], dtype='int64'),
 'Apparel': Int64Index([88, 241, 331, 420, 432, 526, 529, 554, 587, 678,
                        766, 774, 835, 861], dtype='int64'),

输出告诉我们,索引位置为 26、50、58、98 等行的财富中的“Sector”列的值为"Aerospace & Defense"

第四章介绍了用于通过索引标签提取DataFrame行和列的loc访问器。其第一个参数是行索引标签,第二个参数是列索引标签。让我们提取一个样本财富行以确认 pandas 是否将其拉入正确的行业组。我们将尝试26,这是"Aerospace & Defense"组中列出的第一个索引位置:

In  [17] fortune.loc[26, "Sector"]

Out [17] 'Aerospace &  Defense'

如果我们想找到每个行业中表现最好的公司(按收入计算)怎么办?GroupBy对象的first方法提取了财富中每个行业的第一个列表。因为我们的财富DataFrame按收入排序,所以每个行业提取出的第一家公司将是该行业表现最好的公司。first的返回值是一个 21 行的DataFrame(每个行业一家公司):

In  [18] sectors.first()

Out [18]

                      Company  Revenues  Profits  Employees       Industry
**Sector** 
Aerospace &...         Boeing   93392.0   8197.0     140800  Aerospace ...
Apparel                  Nike   34350.0   4240.0      74400        Apparel
Business Se...  ManpowerGroup   21034.0    545.4      29000  Temporary ...
Chemicals           DowDuPont   62683.0   1460.0      98000      Chemicals
Energy            Exxon Mobil  244363.0  19710.0      71200  Petroleum ...
 ...                ...          ...        ...        ...             ...
Retailing             Walmart  500343.0   9862.0    2300000  General Me...
Technology              Apple  229234.0  48351.0     123000  Computers,...
Telecommuni...           AT&T  160546.0  29450.0     254000  Telecommun...
Transportation            UPS   65872.0   4910.0     346415  Mail, Pack...
Wholesalers          McKesson  198533.0   5070.0      64500  Wholesaler...

相补的last方法从属于每个行业的财富中提取最后一家公司。同样,pandas 按照它们在DataFrame中出现的顺序提取行。因为财富按收入降序排列公司,所以以下结果揭示了每个行业中收入最低的公司:

In  [19] sectors.last()

Out [19]

                      Company  Revenues  Profits  Employees       Industry
**Sector** 
Aerospace &...  Aerojet Ro...    1877.0     -9.2       5157  Aerospace ...
Apparel         Wolverine ...    2350.0      0.3       3700        Apparel
Business Se...      CoreLogic    1851.0    152.2       5900  Financial ...
Chemicals              Stepan    1925.0     91.6       2096      Chemicals
Energy          Superior E...    1874.0   -205.9       6400  Oil and Ga...
 ...                      ...      ...      ...         ...            ...
Retailing       Childrens ...    1870.0     84.7       9800  Specialty ...
Technology      VeriFone S...    1871.0   -173.8       5600  Financial ...
Telecommuni...  Zayo Group...    2200.0     85.7       3794  Telecommun...
Transportation  Echo Globa...    1943.0     12.6       2453  Transporta...
Wholesalers     SiteOne La...    1862.0     54.6       3664  Wholesaler...

GroupBy对象为每个行业组中的行分配索引位置。"Aerospace & Defense"行业的第一个财富行在其组中的索引位置为 0。同样,"Apparel"行业的第一个财富行在其组中的索引位置为 0。索引位置在组之间是独立的。

n种方法从其组中提取给定索引位置的行。如果我们用0作为参数调用nth方法,我们将得到每个行业中的第一家公司。下一个DataFramefirst方法返回的相同:

In  [20] sectors.nth(0)

Out [20]

                      Company  Revenues  Profits  Employees       Industry
**Sector** 
Aerospace &...         Boeing   93392.0   8197.0     140800  Aerospace ...
Apparel                  Nike   34350.0   4240.0      74400        Apparel
Business Se...  ManpowerGroup   21034.0    545.4      29000  Temporary ...
Chemicals           DowDuPont   62683.0   1460.0      98000      Chemicals
Energy            Exxon Mobil  244363.0  19710.0      71200  Petroleum ...
 ...              ...            ...       ...          ...            ...
Retailing             Walmart  500343.0   9862.0    2300000  General Me...
Technology              Apple  229234.0  48351.0     123000  Computers,...
Telecommuni...           AT&T  160546.0  29450.0     254000  Telecommun...
Transportation            UPS   65872.0   4910.0     346415  Mail, Pack...
Wholesalers          McKesson  198533.0   5070.0      64500  Wholesaler...

下一个示例将3作为参数传递给nth方法,以从fortune DataFrame中的每个行业提取第四行。结果包括在其行业中按收入排名第四的 21 家公司:

In  [21] sectors.nth(3)

Out [21]

                      Company  Revenues  Profits  Employees       Industry
**Sector** 
Aerospace &...  General Dy...   30973.0   2912.0      98600  Aerospace ...
Apparel          Ralph Lauren    6653.0    -99.3      18250        Apparel
Business Se...        Aramark   14604.0    373.9     215000  Diversifie...
Chemicals            Monsanto   14640.0   2260.0      21900      Chemicals
Energy          Valero Energy   88407.0   4065.0      10015  Petroleum ...
 ...               ...            ...       ...        ...             ...
Retailing          Home Depot  100904.0   8630.0     413000  Specialty ...
Technology                IBM   79139.0   5753.0     397800  Informatio...
Telecommuni...  Charter Co...   41581.0   9895.0      94800  Telecommun...
Transportation  Delta Air ...   41244.0   3577.0      86564       Airlines
Wholesalers             Sysco   55371.0   1142.5      66500  Wholesaler...

注意到"Apparel"行业的值是"Ralph Lauren"。我们可以通过在财富中过滤"Apparel"行来确认输出是否正确。注意"Ralph Lauren"是第四行:

In  [22] fortune[fortune["Sector"] == "Apparel"].head()

Out [22]

 **Company  Revenues  Profits  Employees   Sector Industry**
88           Nike   34350.0   4240.0      74400  Apparel  Apparel
241            VF   12400.0    614.9      69000  Apparel  Apparel
331           PVH    8915.0    537.8      28050  Apparel  Apparel
420  Ralph Lauren    6653.0    -99.3      18250  Apparel  Apparel
432   Hanesbrands    6478.0     61.9      67200  Apparel  Apparel

head方法从每个组中提取多行。在下一个示例中,head(2)提取每个部门在财富中的前两行。结果是包含 42 行的DataFrame(21 个独特部门,每个部门两行)。不要将GroupBy对象上的此head方法与DataFrame对象上的head方法混淆:

In  [23] sectors.head(2)

Out [23]

 **Company  Revenues  Profits  Employees        Sector      Industry**
0         Walmart  500343.0   9862.0    2300000     Retailing  General M...
1     Exxon Mobil  244363.0  19710.0      71200        Energy  Petroleum...
2    Berkshire...  242137.0  44940.0     377000    Financials  Insurance...
3           Apple  229234.0  48351.0     123000    Technology  Computers...
4    UnitedHea...  201159.0  10558.0     260000   Health Care  Health Ca...
  ...         ...     ...       ...        ...          ...             ...
160          Visa   18358.0   6699.0      15000  Business ...  Financial...
162  Kimberly-...   18259.0   2278.0      42000  Household...  Household...
163         AECOM   18203.0    339.4      87000  Engineeri...  Engineeri...
189  Sherwin-W...   14984.0   1772.3      52695     Chemicals     Chemicals
241            VF   12400.0    614.9      69000       Apparel       Apparel

相补的tail方法从每个组中提取最后一行。例如,tail(3)提取每个部门的最后三行。结果是 63 行的DataFrame(21 个部门 x 3 行):

In  [24] sectors.tail(3)

Out [24]

 **Company  Revenues  Profits  Employees        Sector      Industry**
473  Windstrea...    5853.0  -2116.6      12979  Telecommu...  Telecommu...
520  Telephone...    5044.0    153.0       9900  Telecommu...  Telecommu...
667  Weis Markets    3467.0     98.4      23000  Food &  D...  Food and ...
759  Hain Cele...    2853.0     67.4       7825  Food, Bev...  Food Cons...
774  Fossil Group    2788.0   -478.2      12300       Apparel       Apparel
  ...         ...      ...      ...        ...          ...             ...
995  SiteOne L...    1862.0     54.6       3664   Wholesalers  Wholesale...
996  Charles R...    1858.0    123.4      11800   Health Care  Health Ca...
997     CoreLogic    1851.0    152.2       5900  Business ...  Financial...
998  Ensign Group    1849.0     40.5      21301   Health Care  Health Ca...
999           HCP    1848.0    414.2        190    Financials   Real estate

63 rows × 6 columns

我们可以使用get_group方法提取给定组中的所有行。该方法返回包含行的DataFrame。下一个示例显示"Energy"部门中的所有公司:

In  [25] sectors.get_group("Energy").head()

Out [25]

 **Company  Revenues  Profits  Employees  Sector        Industry**
1      Exxon Mobil  244363.0  19710.0      71200  Energy  Petroleum R...
12         Chevron  134533.0   9195.0      51900  Energy  Petroleum R...
27     Phillips 66   91568.0   5106.0      14600  Energy  Petroleum R...
30   Valero Energy   88407.0   4065.0      10015  Energy  Petroleum R...
40  Marathon Pe...   67610.0   3432.0      43800  Energy  Petroleum R...

现在我们已经了解了GroupBy对象的机制,让我们讨论如何对每个嵌套组中的值进行聚合。

9.4 聚合操作

我们可以通过在GroupBy对象上调用方法来对每个嵌套组应用聚合操作。例如,sum方法将每个组中的列值相加。默认情况下,pandas 针对原始DataFrame中的所有数值列。在下一个示例中,sum方法计算了财富DataFrame中三个数值列(收入、利润和员工)的每个部门的总和。我们在GroupBy对象上调用sum方法:

In  [26] sectors.sum().head(10)

Out [26]

                             Revenues   Profits  Employees
**Sector** 
Aerospace & Defense          383835.0   26733.5    1010124
Apparel                      101157.3    6350.7     355699
Business Services            316090.0   37179.2    1593999
Chemicals                    251151.0   20475.0     474020
Energy                      1543507.2   85369.6     981207
Engineering & Construction   172782.0    7121.0     420745
Financials                  2442480.0  264253.5    3500119
Food &  Drug Stores          405468.0    8440.3    1398074
Food, Beverages & Tobacco    510232.0   54902.5    1079316
Health Care                 1507991.4   92791.1    2971189

让我们检查一个样本计算。Pandas 将公司在"Aerospace & Defense"中的收入总和列示为$383,835。我们可以使用get_group方法检索嵌套的"Aerospace & Defense" DataFrame,针对其收入列,并使用sum方法计算其总和:

In  [27] sectors.get_group("Aerospace & Defense").head()

Out [27]

 **Company  Revenues  Profits  Employees        Sector      Industry**
26         Boeing   93392.0   8197.0     140800  Aerospace...  Aerospace...
50   United Te...   59837.0   4552.0     204700  Aerospace...  Aerospace...
58   Lockheed ...   51048.0   2002.0     100000  Aerospace...  Aerospace...
98   General D...   30973.0   2912.0      98600  Aerospace...  Aerospace...
117  Northrop ...   25803.0   2015.0      70000  Aerospace...  Aerospace...

In  [28] sectors.get_group("Aerospace & Defense").loc[:,"Revenues"].head()

Out [28] 26     93392.0
         50     59837.0
         58     51048.0
         98     30973.0
         117    25803.0
         Name: Revenues, dtype: float64

In  [29] sectors.get_group("Aerospace & Defense").loc[:, "Revenues"].sum()

Out [29] 383835.0

值是相等的。Pandas 是正确的!通过单个sum方法调用,库将计算逻辑应用于sectors GroupBy对象中的每个嵌套DataFrame。我们用最少的代码对列的所有分组进行了聚合分析。

GroupBy对象支持许多其他聚合方法。下一个示例调用mean方法计算每个部门的收入、利润和员工列的平均值。同样,pandas 只包括其计算中的数值列:

In  [30] sectors.mean().head()

Out [30]

                         Revenues      Profits     Employees
**Sector** 
Aerospace & Defense  15353.400000  1069.340000  40404.960000
Apparel               7225.521429   453.621429  25407.071429
Business Services     5963.962264   701.494340  30075.452830
Chemicals             7610.636364   620.454545  14364.242424
Energy               14425.300935   805.373585   9170.158879

我们可以通过在GroupBy对象后面传递其名称并在方括号内传递来针对单个财富列。Pandas 返回一个新的对象,一个SeriesGroupBy

In  [31] sectors["Revenues"]

Out [31] <pandas.core.groupby.generic.SeriesGroupBy object at 0x114778210>

在底层,DataFrameGroupBy对象存储了一个SeriesGroupBy对象的集合。SeriesGroupBy对象可以对财富中的单个列执行聚合操作。Pandas 将结果按部门组织。下一个示例按部门计算收入总和:

In  [32] sectors["Revenues"].sum().head()

Out [32] Sector
         Aerospace & Defense     383835.0
         Apparel                 101157.3
         Business Services       316090.0
         Chemicals               251151.0
         Energy                 1543507.2
         Name: Revenues, dtype: float64

下一个示例计算每个部门的平均员工数量:

In  [33] sectors["Employees"].mean().head()

Out [33] Sector
         Aerospace & Defense    40404.960000
         Apparel                25407.071429
         Business Services      30075.452830
         Chemicals              14364.242424
         Energy                  9170.158879
         Name: Employees, dtype: float64

max方法从给定列返回最大值。在下一个示例中,我们提取每个部门的最高利润列值。在"Aerospace & Defense"部门表现最好的公司利润为$8,197:

In  [34] sectors["Profits"].max().head()

Out [34] Sector
         Aerospace & Defense     8197.0
         Apparel                 4240.0
         Business Services       6699.0
         Chemicals               3000.4
         Energy                 19710.0
         Name: Profits, dtype: float64

相补的 min 方法返回给定列中的最小值。下一个示例显示每个部门的最低员工人数。在 "Aerospace & Defense" 部门中,公司最少的员工人数是 5,157:

In  [35] sectors["Employees"].min().head()

Out [35] Sector
         Aerospace & Defense    5157
         Apparel                3700        
         Business Services      2338
         Chemicals              1931
         Energy                  593
         Name: Employees, dtype: int64

agg 方法将多个聚合操作应用于不同的列,并接受一个字典作为其参数。在每一对键值中,键表示 DataFrame 的列,而值指定要应用于该列的聚合操作。下一个示例提取每个部门的最低收入、最高利润和平均员工人数:

In  [36] aggregations = {
             "Revenues": "min",
             "Profits": "max",
             "Employees": "mean"
         }

         sectors.agg(aggregations).head()

Out [36]

                     Revenues  Profits     Employees
**Sector** 
Aerospace & Defense    1877.0   8197.0  40404.960000
Apparel                2350.0   4240.0  25407.071429
Business Services      1851.0   6699.0  30075.452830
Chemicals              1925.0   3000.4  14364.242424
Energy                 1874.0  19710.0   9170.158879

Pandas 返回一个 DataFrame,其中聚合字典的键作为列标题。部门仍然是索引标签。

9.5 对所有组应用自定义操作

假设我们想要对 GroupBy 对象中的每个嵌套组应用一个自定义操作。在第 9.4 节中,我们使用了 GroupBy 对象的 max 方法来找到每个部门的最高收入。假设我们想要识别每个部门收入最高的公司。我们之前已经解决了这个问题,但现在假设财富是无序的。

DataFramenlargest 方法提取给定列中值最大的行。这里有一个快速回顾。下一个示例返回利润列中值最大的五个财富行:

In  [37] fortune.nlargest(n = 5, columns = "Profits")

Out [37]

 **Company  Revenues  Profits  Employees        Sector      Industry**
3          Apple  229234.0  48351.0     123000    Technology  Computers...
2   Berkshire...  242137.0  44940.0     377000    Financials  Insurance...
15       Verizon  126034.0  30101.0     155400  Telecommu...  Telecommu...
8           AT&T  160546.0  29450.0     254000  Telecommu...  Telecommu...
19  JPMorgan ...  113899.0  24441.0     252539    Financials  Commercia...

如果我们能在 sectors 中的每个嵌套 DataFrame 上调用 nlargest 方法,我们就能得到我们想要的结果。我们将在每个部门中得到收入最高的公司。

我们可以使用 GroupBy 对象的 apply 方法在这里。该方法期望一个函数作为参数。它对 GroupBy 对象中的每个组调用一次函数。然后它收集函数调用的返回值,并将它们以新的 DataFrame 的形式返回。

首先,让我们定义一个 get_largest_row 函数,它接受一个参数:一个 DataFrame。该函数将返回 Revenues 列中值最大的 DataFrame 行。该函数是动态的;只要它有一个 Revenues 列,它就可以对任何 DataFrame 执行逻辑:

In  [38] def get_largest_row(df):
             return df.nlargest(1, "Revenues")

接下来,我们可以调用 apply 方法,并传入未调用的 get_largest_row 函数。Pandas 对每个部门调用一次 get_largest_row,并返回一个 DataFrame,其中包含每个部门收入最高的公司:

In  [39] sectors.apply(get_largest_row).head()

Out [39]

                        Company  Revenues  Profits  Employees      Industry
**Sector** 
Aerospace ... 26         Boeing   93392.0   8197.0     140800  Aerospace...
Apparel       88           Nike   34350.0   4240.0      74400       Apparel
Business S... 142  ManpowerG...   21034.0    545.4      29000  Temporary...
Chemicals     46      DowDuPont   62683.0   1460.0      98000     Chemicals
Energy        1     Exxon Mobil  244363.0  19710.0      71200  Petroleum...

当 Pandas 不支持您想要应用于每个嵌套组的自定义聚合时,请使用 apply 方法。

9.6 按多个列分组

我们可以使用来自多个 DataFrame 列的值来创建一个 GroupBy 对象。当列值的组合是最佳标识符时,此操作是最佳的。下一个示例将两个字符串的列表传递给 groupby 方法。Pandas 首先按 Sector 列的值分组,然后按 Industry 列的值分组。请记住,公司的行业是更大部门内的一个子类别:

In  [40] sector_and_industry = fortune.groupby(by = ["Sector", "Industry"])

GroupBy对象的size方法现在返回一个包含每个内部组行数的MultiIndex Series。这个GroupBy对象长度为 82,这意味着fortune有 82 个独特的部门与行业组合:

In  [41] sector_and_industry.size()

Out [41]

**Sector               Industry** 
Aerospace & Defense  Aerospace and Defense                            25
Apparel              Apparel                                          14
Business Services    Advertising, marketing                            2
                     Diversified Outsourcing Services                 14
                     Education                                         2
                                                                      ..
Transportation       Trucking, Truck Leasing                          11
Wholesalers          Wholesalers: Diversified                         24
                     Wholesalers: Electronics and Office Equipment     8
                     Wholesalers: Food and Grocery                     6
                     Wholesalers: Health Care                          6
Length: 82, dtype: int64

get_group方法需要一个值元组来从GroupBy集合中提取嵌套DataFrame。下一个示例针对部门为"Business Services"和行业为"Education"的行:

In  [42] sector_and_industry.get_group(("Business Services", "Education"))

Out [42]

 **Company  Revenues  Profits  Employees        Sector   Industry**
567  Laureate ...    4378.0     91.5      54500  Business ...  Education
810  Graham Ho...    2592.0    302.0      16153  Business ...  Education

对于所有聚合操作,pandas 返回一个包含计算的MultiIndex DataFrame。下一个示例计算了fortune中三个数值列(收入、利润和员工人数)的总和,首先按部门分组,然后按每个部门内的行业分组:

In  [43] sector_and_industry.sum().head()

Out [43]

                                          Revenues  Profits  Employees
**Sector              Industry** 
Aerospace & Defense Aerospace and Def...  383835.0  26733.5    1010124
Apparel             Apparel               101157.3   6350.7     355699
Business Services   Advertising, mark...   23156.0   1667.4     127500
                    Diversified Outso...   74175.0   5043.7     858600
                    Education               6970.0    393.5      70653

我们可以使用与第 9.5 节相同的语法来针对单个fortune列进行聚合。在GroupBy对象后输入列名,然后调用聚合方法。下一个示例计算了每个部门/行业组合中公司的平均收入:

In  [44] sector_and_industry["Revenues"].mean().head(5)

Out [44]

**Sector               Industry** 
Aerospace & Defense  Aerospace and Defense               15353.400000
Apparel              Apparel                              7225.521429
Business Services    Advertising, marketing              11578.000000
                     Diversified Outsourcing Services     5298.214286
                     Education                            3485.000000
Name: Revenues, dtype: float64

总结来说,GroupBy对象是一个用于分割、组织和聚合DataFrame值的最佳数据结构。如果你需要使用多个列来识别分组,可以将列的列表传递给groupby方法。

9.7 编码挑战

这个编码挑战的数据集,cereals.csv,是 80 种流行早餐谷物的列表。每一行包括谷物的名称、制造商、类型、卡路里、纤维克数和糖克数。让我们看一下:

In  [45] cereals = pd.read_csv("cereals.csv")
         cereals.head()

Out [45]

 **Name    Manufacturer  Type  Calories  Fiber  Sugars**
0            100% Bran         Nabisco  Cold        70   10.0       6
1    100% Natural Bran     Quaker Oats  Cold       120    2.0       8
2             All-Bran       Kellogg's  Cold        70    9.0       5
3  All-Bran with Ex...       Kellogg's  Cold        50   14.0       0
4       Almond Delight  Ralston Purina  Cold       110    1.0       8

祝你好运!

9.7.1 问题

这里是挑战:

  1. 使用制造商列的值对谷物进行分组。

  2. 确定总组数和每组中的谷物数量。

  3. 提取属于制造商/组"Nabisco"的谷物。

  4. 计算每个制造商卡路里、纤维和糖列值的平均值。

  5. 找到每个制造商糖列中的最大值。

  6. 找到每个制造商在纤维列中的最小值。

  7. 从每个制造商中提取糖含量最低的谷物到一个新的DataFrame中。

9.7.2 解决方案

让我们深入解决方案:

  1. 要按制造商对谷物进行分组,我们可以在谷物的DataFrame上调用groupby方法,并传入制造商列。Pandas 将使用列的唯一值来组织分组:

    In  [46] manufacturers = cereals.groupby("Manufacturer")
    
  2. 要找到组/制造商的总数,我们可以将GroupBy对象传递给 Python 的内置len函数:

    In  [47] len(manufacturers)
    
    Out [47] 7
    

    如果你好奇,GroupBy对象的size方法返回一个包含每组分谷物数量的Series

    In  [48] manufacturers.size()
    
    Out [48] Manufacturer
             American Home Food Products     1
             General Mills                  22
             Kellogg's                      23
             Nabisco                         6
             Post                            9
             Quaker Oats                     8
             Ralston Purina                  8
             dtype: int64
    
  3. 要识别属于"Nabisco"组的谷物,我们可以在我们的GroupBy对象上调用get_group方法。Pandas 将返回包含"Nabisco"行的嵌套DataFrame

    In  [49] manufacturers.get_group("Nabisco")
    
    Out [49]
    
     **Name Manufacturer  Type Calories  Fiber Sugars**
    0                  100% Bran      Nabisco  Cold       70   10.0      6
    20    Cream of Wheat (Quick)      Nabisco   Hot      100    1.0      0
    63            Shredded Wheat      Nabisco  Cold       80    3.0      0
    64    Shredded Wheat 'n'Bran      Nabisco  Cold       90    4.0      0
    65  Shredded Wheat spoon ...      Nabisco  Cold       90    3.0      0
    68   Strawberry Fruit Wheats      Nabisco  Cold       90    3.0      5
    
  4. 要计算cereals中数值列的平均值,我们可以在manufacturersGroupBy对象上调用mean方法。Pandas 默认会聚合cereals中的所有数值列:

    In  [50] manufacturers.mean()
    
    Out [50]
    
                                   Calories     Fiber    Sugars
    **Manufacturer** 
    American Home Food Products  100.000000  0.000000  3.000000
    General Mills                111.363636  1.272727  7.954545
    Kellogg's                    108.695652  2.739130  7.565217
    Nabisco                       86.666667  4.000000  1.833333
    Post                         108.888889  2.777778  8.777778
    Quaker Oats                   95.000000  1.337500  5.250000
    Ralston Purina               115.000000  1.875000  6.125000
    
  5. 接下来,我们的任务是找到每个制造商的最大糖分值。我们可以在GroupBy对象后面使用方括号来标识要聚合的列的值。然后我们提供正确的聚合方法,在这种情况下是max

    In  [51] manufacturers["Sugars"].max()
    
    Out [51] Manufacturer
             American Home Food Products     3
             General Mills                  14
             Kellogg's                      15
             Nabisco                         6
             Post                           15
             Quaker Oats                    12
             Ralston Purina                 11
             Name: Sugars, dtype: int64
    
  6. 要找到每个制造商的最小纤维值,我们可以将列交换为纤维并调用min方法:

    In  [52] manufacturers["Fiber"].min()
    
    Out [52] Manufacturer
             American Home Food Products    0.0
             General Mills                  0.0
             Kellogg's                      0.0
             Nabisco                        1.0
             Post                           0.0
             Quaker Oats                    0.0
             Ralston Purina                 0.0
             Name: Fiber, dtype: float64
    
  7. 最后,我们需要为每个制造商识别出糖分列中值最低的谷物行。我们可以通过使用apply方法和自定义函数来解决这个问题。smallest_sugar_row函数使用nsmallest方法来提取糖分列中值最小的DataFrame行。然后我们使用apply在每一个GroupBy组上调用自定义函数:

    In  [53] def smallest_sugar_row(df):
                 return df.nsmallest(1, "Sugars")
    
    In  [54] manufacturers.apply(smallest_sugar_row)
    
    Out [54]
    
                              Name  Manufacturer Type Calories Fiber Sugars
    **Manufacturer** 
    American H... 43             Maypo  American ...   Hot       100   0.0      3
    General Mills 11          Cheerios  General M...  Cold       110   2.0      0
    Nabisco       20      Cream of ...       Nabisco   Hot       100   1.0      0
    Post          33        Grape-Nuts          Post  Cold       110   3.0      3
    Quaker Oats   57      Quaker Oa...   Quaker Oats   Hot       100   2.7     -1
    Ralston Pu... 61         Rice Chex  Ralston P...  Cold       110   0.0      2
    

恭喜你完成了编码挑战!

摘要

  • GroupBy对象是一个DataFrame的容器。

  • Pandas 通过使用一个或多个列的值将行划分到GroupBy DataFrame中。

  • firstlast方法从每个GroupBy组中返回第一行和最后一行。原始DataFrame中的行顺序决定了每个组中的行顺序。

  • headtail方法根据行在原始DataFrame中的位置从GroupBy对象中的每个组中提取多行。

  • nth方法通过索引位置从每个GroupBy组中提取一行。

  • Pandas 可以通过GroupBy对象对每个组执行聚合计算,如求和、平均值、最大值和最小值。

  • agg方法将不同的聚合操作应用于不同的列。我们传递一个字典,其中列作为键,聚合作为值。

  • apply方法在GroupBy对象中的每个DataFrame上调用一个函数。

10 合并、连接和连接

本章涵盖

  • 在垂直和水平轴上连接DataFrames

  • 使用内连接、外连接和左连接合并DataFrames

  • DataFrames之间查找唯一和共享值

  • 通过索引标签连接DataFrames

随着业务领域的复杂性增长,将所有数据存储在单个集合中变得越来越困难。为了解决这个问题,数据管理员将数据分散到多个表中。然后他们将这些表相互关联,以便容易识别它们之间的关系。

你可能之前使用过像 PostgreSQL、MySQL 或 Oracle 这样的数据库。关系数据库管理系统(RDBMS)遵循前面段落中描述的范式。数据库由表组成。一个表包含一个领域模型的记录。一个表由行和列组成。一行存储一个记录的信息。一列存储该记录的属性。表通过列键连接。如果你之前没有使用过数据库,你可以将表视为与 pandas DataFrame等效。

这里有一个现实世界的例子。想象一下,我们正在构建一个电子商务网站,并希望创建一个users表来存储网站的注册用户。遵循关系数据库的惯例,我们将为每条记录分配一个唯一的数字标识符。我们将值存储在id列中。id列的值被称为主键,因为它们是特定行的唯一标识符。

用户
id
1
2

让我们设想我们的下一个目标是跟踪我们网站上用户的订单。我们将创建一个orders表来存储订单详情,例如项目名称和价格。但我们如何将每个订单与其下单的用户关联起来?看看下面的表格:

订单
id
1
2

为了在两个表之间建立关系,数据库管理员创建一个外键列。外键是对另一个表中记录的引用。它被标记为外键,因为键存在于当前表的作用域之外。

每个orders表行存储了在user_id列中下单的用户的 ID。因此,user_id列存储外键;其值是对另一个表,即users表中记录的引用。使用两个表之间建立的关系,我们可以确定订单 1 是由 ID 为 1 的 Homer Simpson 用户下的。

外键的优势在于减少了数据重复。例如,orders 表不需要为每个订单复制用户的姓名、姓氏和电子邮件。相反,它只需要存储对正确的 users 记录的单个引用。用户和订单的业务实体分别存在,但我们在需要时可以将它们连接起来。

当需要合并表时,我们总是可以转向 pandas。这个库在追加、连接、连接、合并和垂直和水平方向上合并 DataFrames 方面表现出色。它可以识别 DataFrames 之间的唯一和共享记录。它可以执行 SQL 操作,如内连接、外连接、左连接和右连接。在本章中,我们将探讨这些连接之间的差异以及每种连接可以证明是有益的情况。

10.1 数据集介绍

让我们导入 pandas 库并将其分配一个别名 pd

In  [1] import pandas as pd

本章的数据集来自在线社交服务 Meetup,这是一个用户加入具有共同兴趣如远足、文学和桌面游戏的组网站。组组织者安排远程或现场活动,组员参加。Meetup 的域有几个数据模型,包括组、分类和城市。

Meetup 目录包含本章的所有数据集。让我们通过导入 groups1.csv 和 groups2.csv 文件开始我们的探索。这些文件包含 Meetup 注册组的样本。每个组包括一个 ID、名称、关联的分类 ID 和关联的城市 ID。以下是 groups1 的样子:

In  [2] groups1 = pd.read_csv("meetup/groups1.csv")
        groups1.head()

Out [2]

 **group_id                           name  category_id  city_id**
0      6388         Alternative Health NYC           14    10001
1      6510      Alternative Energy Meetup            4    10001
2      8458              NYC Animal Rights           26    10001
3      8940  The New York City Anime Group           29    10001
4     10104             NYC Pit Bull Group           26    10001

同时导入 groups2.csv 文件。注意,这两个 CSV 文件都有相同的四个列。我们可以想象,groups 数据是以某种方式分割并存储在两个文件中而不是一个文件中:

In  [3] groups2 = pd.read_csv("meetup/groups2.csv")
        groups2.head()

Out [3]

 **group_id                                      name  category_id  city_id**
0  18879327                              BachataMania            5    10001
1  18880221  Photoshoot Chicago - Photography and ...           27    60601
2  18880426  Chicago Adult Push / Kick Scooter Gro...           31    60601
3  18880495         Chicago International Soccer Club           32    60601
4  18880695          Impact.tech San Francisco Meetup            2    94101

每个组还有一个 category_id 外键。我们可以在 categories.csv 文件中找到有关分类的信息。该文件中的每一行存储分类的 ID 和名称:

In  [4] categories = pd.read_csv("meetup/categories.csv")
        categories.head()

Out [4]

 **category_id            category_name**
0            1           Arts & Culture
1            3       Cars & Motorcycles
2            4  Community & Environment
3            5                  Dancing
4            6     Education & Learning

每个组还有一个 city_id 外键。cities.csv 数据集存储城市信息。一个城市有一个唯一的 ID、名称、州和邮政编码。让我们看一下:

In  [5] pd.read_csv("meetup/cities.csv").head()

Out [5]

 **id            city state    zip**
0   7093   West New York    NJ   7093
1  10001        New York    NY  10001
2  13417  New York Mills    NY  13417
3  46312    East Chicago    IN  46312
4  56567  New York Mills    MN  56567

cities 数据集有一个小问题。看看第一行的 zip 值。7093 是一个无效的邮政编码;CSV 中的值实际上是 07093。邮政编码可以以一个前导零开头。不幸的是,pandas 假设邮政编码是整数,因此从值中删除了前导零。为了解决这个问题,我们可以在 read_csv 函数中添加 dtype 参数。dtype 接受一个字典,其中键表示列名,值表示要分配给该列的数据类型。让我们确保 pandas 将 zip 列的值作为字符串导入:

In  [6] cities = pd.read_csv(
            "meetup/cities.csv", dtype = {"zip": "string"}
        )
        cities.head()

Out [6]

 **id            city state    zip**
0   7093   West New York    NJ  07093
1  10001        New York    NY  10001
2  13417  New York Mills    NY  13417
3  46312    East Chicago    IN  46312
4  56567  New York Mills    MN  56567

优秀;我们准备继续。为了总结,groups1 和 groups2 中的每个组都属于一个类别和一个城市。category_id 和 group_id 列存储外键。category_id 列的值映射到 categories 中的 category_id 列。city_id 列的值映射到 cities 中的 id 列。在我们的数据表加载到 Jupyter 中后,我们准备开始连接它们。

10.2 连接数据集

将两个数据集合并的最简单方法是使用连接——将一个 DataFrame 添加到另一个 DataFrame 的末尾。

groups1 和 groups2 DataFrames 都有相同的四个列名。让我们假设它们是一个更大整体的两半。我们希望将它们的行合并到一个 DataFrame 中。Pandas 库的顶层有一个方便的 concat 函数。我们可以传递一个 DataFrames 列表给它的 objs 参数。Pandas 将按照 objs 列表中出现的顺序连接对象。下一个示例将 groups2 的行连接到 groups1 的末尾:

In  [7] pd.concat(objs = [groups1, groups2])

Out [7]

 **group_id                                   name  category_id  city_id**
0         6388                 Alternative Health NYC           14    10001
1         6510              Alternative Energy Meetup            4    10001
2         8458                      NYC Animal Rights           26    10001
3         8940          The New York City Anime Group           29    10001
4        10104                     NYC Pit Bull Group           26    10001
 ...      ...                                      ...         ...      ...
8326  26377464                                Shinect           34    94101
8327  26377698  The art of getting what you want [...           14    94101
8328  26378067            Streeterville Running Group            9    60601
8329  26378128                         Just Dance NYC           23    10001
8330  26378470  FREE Arabic Chicago Evanston North...           31    60601

16330 rows × 4 columns

连接后的 DataFrame 有 16,330 行!正如你可能猜到的,它的长度等于 groups1 和 groups2 DataFrames 长度的总和:

In  [8] len(groups1)

Out [8] 7999

In  [9] len(groups2)

Out [9] 8331

In  [10] len(groups1) + len(groups2)

Out [10] 16330

Pandas 在连接中保留了两个 DataFrame 的原始索引标签,这就是为什么我们看到连接后的 DataFrame 中有一个最终的索引位置为 8,330,尽管它有超过 16,000 行。我们看到的是 groups2 DataFrame 末尾的 8,330 索引。Pandas 不关心相同的索引号是否出现在 groups1 和 groups2 中。因此,连接后的索引有重复的索引标签。

我们可以将 concat 函数的 ignore_index 参数设置为 True 以生成 pandas 的标准数值索引。连接后的 DataFrame 将丢弃原始的索引标签:

In  [11] pd.concat(objs = [groups1, groups2], ignore_index = True)

Out [11]

 **group_id                                  name  category_id  city_id**
0          6388                Alternative Health NYC           14    10001
1          6510             Alternative Energy Meetup            4    10001
2          8458                     NYC Animal Rights           26    10001
3          8940         The New York City Anime Group           29    10001
4         10104                    NYC Pit Bull Group           26    10001
 ...        ...                                   ...          ...      ...
16325  26377464                               Shinect           34    94101
16326  26377698  The art of getting what you want ...           14    94101
16327  26378067           Streeterville Running Group            9    60601
16328  26378128                        Just Dance NYC           23    10001
16329  26378470  FREE Arabic Chicago Evanston Nort...           31    60601

16330 rows × 4 columns

如果我们想要两者兼得:创建一个非重复索引,同时保留每行数据来自哪个 DataFrame?一个解决方案是添加一个 keys 参数,并传递一个字符串列表。Pandas 将将 keys 列表中的每个字符串与 objs 列表中相同索引位置的 DataFrame 关联起来。keysobjs 列表必须具有相同的长度。

下一个示例将 groups1 DataFrame 分配一个键 "G1",将 groups2 DataFrame 分配一个键 "G2"concat 函数返回一个 MultiIndex DataFrameMultiIndex 的第一级存储键,第二级存储来自相应 DataFrame 的索引标签:

In  [12] pd.concat(objs = [groups1, groups2], keys = ["G1", "G2"])

Out [12]

 **group_id                                name  category_id  city_id**
G1 0         6388              Alternative Health NYC           14    10001
   1         6510           Alternative Energy Meetup            4    10001
   2         8458                   NYC Animal Rights           26    10001
   3         8940       The New York City Anime Group           29    10001
   4        10104                  NYC Pit Bull Group           26    10001
... ...       ...                                 ...          ...      ...
G2 8326  26377464                             Shinect           34    94101
   8327  26377698  The art of getting what you wan...           14    94101
   8328  26378067         Streeterville Running Group            9    60601
   8329  26378128                      Just Dance NYC           23    10001
   8330  26378470  FREE Arabic Chicago Evanston No...           31    60601

16330 rows × 4 columns

我们可以通过访问 MultiIndex 第一级的 G1G2 键来提取原始 DataFrames。(参见第七章以复习在 MultiIndex DataFrames 上使用 loc 访问器。)在我们继续之前,让我们将连接后的 DataFrame 分配给一个 groups 变量:

In  [13] groups = pd.concat(objs = [groups1, groups2], ignore_index = True)

我们将在第 10.4 节回到 groups

10.3 连接后的 DataFrames 中的缺失值

当连接两个DataFrame时,pandas 会在数据集不共享的行标签和列标签的交叉处放置NaN。考虑以下两个DataFrame,它们都有一个足球列。sports_champions_A DataFrame有一个独有的棒球列,而sports_champions_B DataFrame有一个独有的曲棍球列:

In  [14] sports_champions_A = pd.DataFrame(
             data = [
                 ["New England Patriots", "Houston Astros"],
                 ["Philadelphia Eagles", "Boston Red Sox"]
             ],
             columns = ["Football", "Baseball"],
             index = [2017, 2018]
         )

        sports_champions_A

Out [14]

 **Football        Baseball**
2017  New England Patriots  Houston Astros
2018   Philadelphia Eagles  Boston Red Sox

In  [15] sports_champions_B = pd.DataFrame(
             data = [
                 ["New England Patriots", "St. Louis Blues"],
                 ["Kansas City Chiefs", "Tampa Bay Lightning"]
             ],
             columns = ["Football", "Hockey"],
             index = [2019, 2020]
         )

         sports_champions_B

Out [15]

 **Football               Hockey**
2019  New England Patriots      St. Louis Blues
2020    Kansas City Chiefs  Tampa Bay Lightning

如果我们将DataFrames连接起来,将在棒球和曲棍球列中创建缺失值。sports_champions_A DataFrame在曲棍球列中没有值可以放置,而sports_champions_B DataFrame在棒球列中没有值可以放置:

In  [16] pd.concat(objs = [sports_champions_A, sports_champions_B])

Out [16]

 **Football        Baseball               Hockey**
2017  New England Patriots  Houston Astros                  NaN
2018   Philadelphia Eagles  Boston Red Sox                  NaN
2019  New England Patriots             NaN      St. Louis Blues
2020    Kansas City Chiefs             NaN  Tampa Bay Lightning

默认情况下,pandas 在水平轴上连接行。有时,我们希望垂直轴上追加行。考虑sports_champions_C DataFrame,它具有与sports_champions_A相同的两个索引标签(2017 年和 2018 年),但有两列不同的数据,分别是曲棍球和篮球:

In  [17] sports_champions_C = pd.DataFrame(
             data = [
                 ["Pittsburgh Penguins", "Golden State Warriors"],
                 ["Washington Capitals", "Golden State Warriors"]
         ],
             columns = ["Hockey", "Basketball"],
             index = [2017, 2018]
         )

         sports_champions_C

Out [17]

 **Hockey             Basketball**
2017  Pittsburgh Penguins  Golden State Warriors
2018  Washington Capitals  Golden State Warriors

当我们将sports_champions_Asports_champions_C连接起来时,pandas 会将第二个DataFrame的行追加到第一个DataFrame的末尾。这个过程会创建重复的 2017 年和 2018 年索引标签:

In  [18] pd.concat(objs = [sports_champions_A, sports_champions_C])

Out [18]

 **Football        Baseball            Hockey        Basketball**
2017  New England P...  Houston Astros               NaN               NaN
2018  Philadelphia ...  Boston Red Sox               NaN               NaN
2017               NaN             NaN  Pittsburgh Pe...  Golden State ...
2018               NaN             NaN  Washington Ca...  Golden State ...

这个结果并不是我们想要的。相反,我们希望对齐重复的索引标签(2017 年和 2018 年),使得列没有缺失值。

concat函数包含一个axis参数。我们可以传递该参数一个1"columns"的参数,以在列轴上连接DataFrame

In  [19] # The two lines below are equivalent
         pd.concat(
             objs = [sports_champions_A, sports_champions_C],
             axis = 1
         )
         pd.concat(
             objs = [sports_champions_A, sports_champions_C],
             axis = "columns"
         )

Out [19]

 **Football        Baseball            Hockey        Basketball**
2017  New England P...  Houston Astros  Pittsburgh Pe...  Golden State ...
2018  Philadelphia ...  Boston Red Sox  Washington Ca...  Golden State ...

太好了!

总结来说,concat函数通过将一个DataFrame追加到另一个的末尾,在水平轴或垂直轴上组合两个DataFrame。我喜欢将这个过程描述为“将两个数据集粘合在一起”。

10.4 左连接

与连接相比,连接使用逻辑标准来确定两个数据集之间合并的行或列。例如,连接可以仅针对两个数据集之间具有共享值的行。以下几节将介绍三种类型的连接:左连接、内连接和外连接。让我们逐一了解它们。

左连接使用一个数据集的键来从另一个数据集中提取值。它在 Excel 中的VLOOKUP操作中是等效的。当一个数据集是分析的重点时,左连接是最优的。我们引入第二个数据集以提供与主要数据集相关的补充信息。考虑图 10.1 中的图表。将每个圆圈视为一个DataFrame。左边的DataFrame是分析的重点。

图 10.1 左连接图

这里快速回顾一下我们的组数据集的样子:

In  [20] groups.head(3)

Out [20]

 **group_id                       name  category_id  city_id**
0      6388     Alternative Health NYC           14    10001
1      6510  Alternative Energy Meetup            4    10001
2      8458          NYC Animal Rights           26    10001

category_id列中的外键引用了categories数据集中的 ID:

In  [21] categories.head(3)

Out [21]

 **category_id            category_name**
0            1           Arts & Culture
1            3       Cars & Motorcycles
2            4  Community & Environment

让我们在 groups 上执行左连接,为每个组添加类别信息。我们将使用merge方法将一个DataFrame合并到另一个中。该方法的第一参数right接受一个DataFrame。这个术语来自之前的图表。右边的DataFrame是右边的圆圈,即“第二个”数据集。我们可以将表示连接类型的字符串传递给方法的how参数;我们将传递"left"。我们还必须告诉 pandas 使用哪些列来匹配两个DataFrame之间的值。让我们添加一个on参数,其值为"category_id"。我们只能在两个DataFrame的列名相等时使用on参数。在我们的情况下,groups 和 categories DataFrames 都有 category_id 列:

In  [22] groups.merge(categories, how = "left", on = "category_id").head()

Out [22]

 **group_id                 name  category_id  city_id        category_name**
0      6388  Alternative Heal...           14    10001   Health & Wellbeing
1      6510  Alternative Ener...            4    10001  Community & Envi...
2      8458    NYC Animal Rights           26    10001                  NaN
3      8940  The New York Cit...           29    10001     Sci-Fi & Fantasy
4     10104   NYC Pit Bull Group           26    10001                  NaN

就在这里!当 Pandas 找到与 groups 中的 category_id 值匹配时,它会拉入 categories 表的列。唯一的例外是 category_id 列,它只列了一次。请注意,当库在 categories 中找不到 category_id 时,它会在 categories 的 category_name 列中显示NaN值。我们可以在上一个输出的第 2 行和第 4 行看到一个例子。

10.5 内连接

内连接的目标是存在于两个DataFrame中的值。考虑图 10.2;内连接的目标是两个圆圈中间的彩色重叠部分。

图 10.2 内连接图

在内连接中,pandas 排除了只存在于第一个DataFrame和只存在于第二个DataFrame中的值。

这里是一个提醒,关于 groups 和 categories 数据集看起来是什么样子:

In  [23] groups.head(3)

Out [23]

 **group_id                       name  category_id  city_id**
0      6388     Alternative Health NYC           14    10001
1      6510  Alternative Energy Meetup            4    10001
2      8458          NYC Animal Rights           26    10001

In  [24] categories.head(3)

Out [24]

 **category_id            category_name**
0            1           Arts & Culture
1            3       Cars & Motorcycles
2            4  Community & Environment

让我们确定存在于两个数据集中的类别。从技术角度来看,我们再次想要针对两个DataFrame中 category_id 列值相等的行。在这种情况下,我们是否在 group 或 categories 上调用merge方法无关紧要。内连接确定两个数据集中的共同元素;结果将是一样的。对于下一个示例,让我们在 groups 上调用merge方法:

In  [25] groups.merge(categories, how = "inner", on = "category_id")

Out [25]

 **group_id               name  category_id  city_id      category_name**
0         6388  Alternative He...           14    10001  Health & Wellb...
1        54126  Energy Healers...           14    10001  Health & Wellb...
2        67776  Flourishing Li...           14    10001  Health & Wellb...
3       111855  Hypnosis & NLP...           14    10001  Health & Wellb...
4       129277  The Live Food ...           14    60601  Health & Wellb...
 ...      ...                 ...          ...      ...                ...
8032  25536270  New York Cucko...           17    10001          Lifestyle
8033  25795045  Pagans Paradis...           17    10001          Lifestyle
8034  25856573  Fuck Yeah Femm...           17    94101          Lifestyle
8035  26158102  Chicago Crossd...           17    60601          Lifestyle
8036  26219043  Corporate Goes...           17    10001          Lifestyle

8037 rows × 5 columns

合并的DataFrame包括 groups 和 categories DataFrames 的所有列。category_id 列的值在 groups 和 categories 中都出现。category_id 列只列了一次。我们不需要重复的列,因为在内连接中,category_id 的值对于 groups 和 categories 是相同的。

让我们添加一些上下文来解释 pandas 做了什么。合并的DataFrame的前四行有一个 category_id 值为 14。我们可以在 groups 和 categories DataFrames 中过滤出这个 ID:

In  [26] groups[groups["category_id"] == 14]

Out [26]

 **group_id                                  name  category_id  city_id**
0          6388                Alternative Health NYC           14    10001
52        54126                    Energy Healers NYC           14    10001
78        67776               Flourishing Life Meetup           14    10001
121      111855  Hypnosis & NLP NYC - Update Your ...           14    10001
136      129277       The Live Food Chicago Community           14    60601
 ...       ...                                    ...          ...      ...
16174  26291539  The Transformation Project: Colla...           14    94101
16201  26299876  Cognitive Empathy, How To Transla...           14    10001
16248  26322976         Contemplative Practices Group           14    94101
16314  26366221  The art of getting what you want:...           14    94101
16326  26377698  The art of getting what you want ...           14    94101

870 rows × 4 columns

In  [27] categories[categories["category_id"] == 14]

Out [27]

 **category_id       category_name**
8           14  Health & Wellbeing

合并后的DataFrame为两个DataFrame之间的每个group_id匹配创建一行。组中有 870 行,类别中有 1 行,group_id为 14。Pandas 将组中的 870 行与类别中的单行配对,并在合并的DataFrame中创建总共 870 行。因为内连接为每个值匹配创建新行,所以合并的DataFrame可以比原始的DataFrame大得多。例如,如果有三个 ID 为 14 的类别,pandas 将创建 2610 行(870 x 3)。

10.6 外连接

外连接将两个数据集中的所有记录组合在一起。在外连接中,唯一性并不重要。图 10.3 显示了外连接的结果;pandas 包括所有值,无论它们是否属于一个数据集或两个数据集。

图 10.3 外连接图

图 10.3 外连接图

这里是关于组和城市DataFrame的提醒:

In  [28] groups.head(3)

Out [28]

 **group_id                       name  category_id  city_id**
0      6388     Alternative Health NYC           14    10001
1      6510  Alternative Energy Meetup            4    10001
2      8458          NYC Animal Rights           26    10001

In  [29] cities.head(3)

Out [29]

 **id            city state    zip**
0   7093   West New York    NJ  07093
1  10001        New York    NY  10001
2  13417  New York Mills    NY  13417

让我们使用外连接合并组和城市。我们将拉入所有城市:仅属于组的城市,仅属于城市的城市,以及两者都有的城市。

到目前为止,我们只使用共享列名来合并数据集。当数据集之间的列名不同时,我们必须向merge方法传递不同的参数。而不是使用on参数,我们可以使用merge方法的left_onright_on参数。我们将left_on传递给左边的DataFrame中的列名,将right_on传递给右边的DataFrame中的列名。在这里,我们执行外连接以将城市信息合并到组DataFrame中:

In  [30] groups.merge(
             cities, how = "outer", left_on = "city_id", right_on = "id"
         )

Out [30]

 **group_id       name  category_id  city_id       city state    zip**
0         6388.0  Altern...       14.0    10001.0   New York    NY  10001
1         6510.0  Altern...        4.0    10001.0   New York    NY  10001
2         8458.0  NYC An...       26.0    10001.0   New York    NY  10001
3         8940.0  The Ne...       29.0    10001.0   New York    NY  10001
4        10104.0  NYC Pi...       26.0    10001.0   New York    NY  10001
 ...         ...        ...        ...        ...        ...   ...    ...
16329  243034...  Midwes...       34.0    60064.0  North ...    IL  60064
16330        NaN        NaN        NaN        NaN  New Yo...    NY  13417
16331        NaN        NaN        NaN        NaN  East C...    IN  46312
16332        NaN        NaN        NaN        NaN  New Yo...    MN  56567
16333        NaN        NaN        NaN        NaN  Chicag...    CA  95712

16334 rows × 8 columns

最终的DataFrame包含来自两个数据集的所有城市 ID。如果 pandas 在city_idid列之间找到值匹配,它将在单行中合并两个DataFrame的列。我们可以在前五行中看到一些示例。city_id列存储共同的 id。

如果一个DataFrame有一个另一个DataFrame没有的值,pandas 将在city_id列中放置一个NaN值。我们可以在数据集的末尾看到一些示例。这种放置将不受组或城市是否有唯一值的影响。

我们可以将True传递给merge方法的indicator参数,以识别一个值属于哪个DataFrame。合并后的DataFrame将包含一个_merge列,该列存储值"both""left_only""right_only"

In  [31] groups.merge(
             cities,
             how = "outer",
             left_on = "city_id",
             right_on = "id",
             indicator = True
         )

Out [31]

 **group_id    name  category_id  city_id    city state    zip  _merge**
0      6388.0    Alt...    14.0       100...   New...    NY  10001    both
1      6510.0    Alt...     4.0       100...   New...    NY  10001    both
2      8458.0    NYC...    26.0       100...   New...    NY  10001    both
3      8940.0    The...    29.0       100...   New...    NY  10001    both
4      101...    NYC...    26.0       100...   New...    NY  10001    both
...       ...       ...     ...          ...      ...   ...    ...     ...
16329  243...    Mid...    34.0       600...   Nor...    IL  60064    both
16330     NaN       NaN     NaN          NaN   New...    NY  13417  rig...
16331     NaN       NaN     NaN          NaN   Eas...    IN  46312  rig...
16332     NaN       NaN     NaN          NaN   New...    MN  56567  rig...
16333     NaN       NaN     NaN          NaN   Chi...    CA  95712  rig...

16334 rows × 9 columns

我们可以使用_merge列来过滤属于任一DataFrame的行。下一个示例提取_merge列中值为"right_only"的行,或者等价地,仅存在于城市中的城市 ID,即右边的DataFrame

In  [32] outer_join = groups.merge(
             cities,
             how = "outer",
             left_on = "city_id",
             right_on = "id",
             indicator = True
         )
         in_right_only = outer_join["_merge"] == "right_only"

         outer_join[in_right_only].head()

Out [32]

 **group_id name  category_id  city_id      city state    zip    _merge**
16330       NaN  NaN       NaN         NaN  New Y...    NY  13417  right...
16331       NaN  NaN       NaN         NaN  East ...    IN  46312  right...
16332       NaN  NaN       NaN         NaN  New Y...    MN  56567  right...
16333       NaN  NaN       NaN         NaN  Chica...    CA  95712  right...

通过几行代码,我们可以轻松过滤出每个数据集中的唯一值。

10.7 在索引标签上合并

想象一下,我们想要连接的DataFrame将其主键存储在其索引中。让我们模拟这种情况。我们可以在城市上调用set_index方法,将其 id 列设置为DataFrame的索引:

In  [33] cities.head(3)

Out [33]

 **id            city state    zip**
0   7093   West New York    NJ  07093
1  10001        New York    NY  10001
2  13417  New York Mills    NY  13417

In  [34] cities = cities.set_index("id")

In  [35] cities.head(3)

Out [35]

                 city state    zip
**id** 
7093    West New York    NJ  07093
10001        New York    NY  10001
13417  New York Mills    NY  13417

让我们再次使用左连接将城市合并到组中。这里是一个关于 groups 的快速提醒:

In  [36] groups.head(3)

Out [36]

 **group_id                       name  category_id  city_id**
0      6388     Alternative Health NYC           14    10001
1      6510  Alternative Energy Meetup            4    10001
2      8458          NYC Animal Rights           26    10001

现在我们想比较组中 city_id 列的值与城市索引标签。当我们调用 merge 方法时,我们将 how 参数的参数设置为 "left" 以进行左连接。我们将使用 left_on 参数告诉 pandas 在组中的 city_id 列中查找匹配项,在左 DataFrame 中。为了在右 DataFrame 的索引中查找匹配项,我们可以提供一个不同的参数,right_index,并将其设置为 True。该参数告诉 pandas 在右 DataFrame 的索引中查找 city_id 匹配项:

In  [37] groups.merge(
             cities,
             how = "left",
             left_on = "city_id",
             right_index = True
         )

Out [37]

 **group_id        name  category_id  city_id        city state    zip**
0          6388  Alterna...          14     10001    New York    NY  10001
1          6510  Alterna...           4     10001    New York    NY  10001
2          8458  NYC Ani...          26     10001    New York    NY  10001
3          8940  The New...          29     10001    New York    NY  10001
4         10104  NYC Pit...          26     10001    New York    NY  10001
...         ...         ...         ...       ...         ...   ...    ...
16325  26377464     Shinect          34     94101  San Fra...    CA  94101
16326  26377698  The art...          14     94101  San Fra...    CA  94101
16327  26378067  Streete...           9     60601     Chicago    IL  60290
16328  26378128  Just Da...          23     10001    New York    NY  10001
16329  26378470  FREE Ar...          31     60601     Chicago    IL  60290

16330 rows × 7 columns

该方法还支持一个互补的 left_index 参数。将参数传递一个 True 的参数,告诉 pandas 在左 DataFrame 的索引中查找匹配项。左 DataFrame 是我们调用 merge 方法的那个。

10.8 编程挑战

我们的探索已经结束;感谢您的参与(有意为之!)让我们练习本章介绍的概念。

这个编程挑战的表格总结了虚构餐厅的销售情况。week_1_sales.csv 和 week_2_sales.csv 文件包含每周交易的列表。每个餐厅订单包括下订单的客户的 ID 和他们购买的食品项的 ID。以下是 week_1_sales 的前五行预览:

In  [38] pd.read_csv("restaurant/week_1_sales.csv").head()

Out [38]

 **Customer ID  Food ID**
0          537        9
1           97        4
2          658        1
3          202        2
4          155        9

week_2_sales 数据集具有相同的形状。让我们导入这两个 CSV 文件,并将它们分配给 week1week2 变量:

In  [39] week1 = pd.read_csv("restaurant/week_1_sales.csv")
         week2 = pd.read_csv("restaurant/week_2_sales.csv")

客户 ID 列包含外键,它们引用 customers.csv 中的 ID 列的值。customers.csv 中的每条记录都包含一个客户的姓氏、名字、性别、公司和职业。让我们使用 read_csv 函数导入该数据集,并使用 index_col 参数将其 ID 列设置为 DataFrame 的索引:

In  [40] pd.read_csv("restaurant/customers.csv", index_col = "ID").head()

Out [40]

   First Name Last Name  Gender  Company                     Occupation
**ID** 
1      Joseph   Perkins    Male  Dynazzy  Community Outreach Specialist
2    Jennifer   Alvarez  Female     DabZ        Senior Quality Engineer
3       Roger     Black    Male  Tagfeed              Account Executive
4      Steven     Evans    Male     Fatz               Registered Nurse
5        Judy  Morrison  Female  Demivee                Legal Assistant

In  [41] customers = pd.read_csv(
             "restaurant/customers.csv", index_col = "ID"
         )

weeks1 和 weeks2 DataFrames 中还有另一列外键。食品 ID 外键连接到 foods.csv 中的 ID 列。一个食品项目包括一个 ID、一个名称和一个价格。当我们导入这个数据集时,让我们将其食品 ID 列设置为 DataFrame 的索引:

In  [42] pd.read_csv("restaurant/foods.csv", index_col = "Food ID")

Out [42]

          Food Item  Price
**Food ID** 
1             Sushi   3.99
2           Burrito   9.99
3              Taco   2.99
4        Quesadilla   4.25
5             Pizza   2.49
6             Pasta  13.99
7             Steak  24.99
8             Salad  11.25
9             Donut   0.99
10            Drink   1.75

In  [43] foods = pd.read_csv("restaurant/foods.csv", index_col = "Food ID")

数据集导入后,我们就可以开始解决练习了。

10.8.1 问题

这里是挑战:

  1. 将两周的销售数据合并到一个 DataFrame 中。将 week1 DataFrame 分配一个键 "Week 1",将 week2 DataFrame 分配一个键 "Week 2"

  2. 找出两个星期都在餐厅就餐的客户。

  3. 找出两个星期都在餐厅就餐且每周都订购相同食品的客户。

    提示:您可以通过传递一个列列表给 on 参数来在多个列上连接数据集。

  4. 识别哪些客户只在第 1 周和只在第 2 周来过。

  5. week1 DataFrame 中的每一行都标识了一个购买食品项目的客户。对于每一行,从 customers DataFrame 中提取客户信息。

10.8.2 解决方案

让我们探索解决方案:

  1. 我们的首要挑战是将两周的餐厅销售数据合并到一个DataFrame中。pandas 顶级层的concat函数提供了一个完美的解决方案。我们可以将两个DataFrame作为一个列表传递给函数的objs参数。为了将MultiIndex级别分配给结果中的每个DataFrame,我们还将提供keys参数一个包含级别标签的列表:

    In  [44] pd.concat(objs = [week1, week2], keys = ["Week 1", "Week 2"])
    
    Out [44]
    
     **Customer ID  Food ID**
    Week 1 0            537        9
           1             97        4
           2            658        1
           3            202        2
           4            155        9
     ...  ...           ...      ...
    Week 2 245          783       10
           246          556       10
           247          547        9
           248          252        9
           249          249        6
    
    500 rows × 2 columns
    
  2. 接下来,我们希望识别出在两周内都访问过餐厅的客户。从技术角度来看,我们需要找到存在于 week1 和 week2 DataFrame中的客户 ID。这里我们需要的是内连接。让我们在 week1 上调用merge方法,并将 week2 作为右边的DataFrame传入。我们将连接类型声明为"inner",并告诉 pandas 在客户 ID 列中查找共享值:

    In  [45] week1.merge(
                 right = week2, how = "inner", on = "Customer ID"
             ).head()
    
    Out [45]
    
     **Customer ID  Food ID_x  Food ID_y**
    0          537          9          5
    1          155          9          3
    2          155          1          3
    3          503          5          8
    4          503          5          9
    

    记住,内连接显示了 week1 和 week2 DataFrames中所有客户 ID 的匹配。因此,结果中有重复(客户 155 和 503)。如果我们想删除重复项,我们可以调用第五章中引入的drop_duplicates方法:

    In  [46] week1.merge(
                 right = week2, how = "inner", on = "Customer ID"
             ).drop_duplicates(subset = ["Customer ID"]).head()
    
    Out [46]
    
     **Customer ID  Food ID_x  Food ID_y**
    0          537          9          5
    1          155          9          3
    3          503          5          8
    5          550          6          7
    6          101          7          4
    
  3. 第三个挑战要求找到在两周内都访问过餐厅并且点了相同菜品的客户。同样,内连接是找到左和右DataFrame中存在的值的正确选项。然而,这一次,我们必须将on参数传递为一个包含两个列的列表。在 week1 和 week2 的客户 ID 和食品 ID 列中的值必须匹配:

    In  [47] week1.merge(
                 right = week2,
                 how = "inner",
                 on = ["Customer ID", "Food ID"]
             )
    
    Out [47]
    
     **Customer ID  Food ID**
    0          304        3
    1          540        3
    2          937       10
    3          233        3
    4           21        4
    5           21        4
    6          922        1
    7          578        5
    8          578        5s
    
  4. 识别只在一周内来过的客户的一个解决方案是使用外连接。我们可以通过客户 ID 列中的值在两个DataFrame之间匹配记录。让我们将indicator参数的值设置为True以添加一个 _merge 列。Pandas 将指示客户 ID 是否仅存在于左表("left_only")、仅存在于右表("right_only"),还是两个表都存在("both"):

    In  [48] week1.merge(
                 right = week2,
                 how = "outer",
                 on = "Customer ID",
                 indicator = True
             ).head()
    
    Out [48]
    
     **Customer ID  Food ID_x  Food ID_y     _merge**
    0          537        9.0        5.0       both
    1           97        4.0        NaN  left_only
    2          658        1.0        NaN  left_only
    3          202        2.0        NaN  left_only
    4          155        9.0        3.0       both
    
    
  5. 最后一个挑战要求将客户信息拉入 week1 表中。左连接是一个最优解。在 week1 DataFrame上调用merge方法,传入客户DataFrame作为右数据集。将how参数的值传递为"left"

    这个挑战的难点在于,week1 DataFrame将其客户 ID 存储在其客户 ID 列中,而客户DataFrame将其存储在其索引标签中。为了解决这个问题,我们可以将left_on参数传递给 week1 DataFrame中的列名,并将right_index参数的值设置为True

    In  [49] week1.merge(
                 right = customers,
                 how = "left",
                 left_on = "Customer ID",
                 right_index = True
             ).head()
    
    Out [49]
    
     **Customer ID  Food ID First Name Last Name  Gender    Company Occupation**
    0        537          9     Cheryl   Carroll  Female   Zoombeat  Regist...
    1         97          4     Amanda   Watkins  Female        Ozu  Accoun...
    2        658          1    Patrick      Webb    Male  Browsebug  Commun...
    3        202          2      Louis  Campbell    Male  Rhynoodle  Accoun...
    4        155          9    Carolyn      Diaz  Female   Gigazoom  Databa...
    

恭喜你完成了编码挑战!

摘要

  • 主键是数据集中记录的唯一标识符。

  • 外键是另一个数据集中记录的引用。

  • concat函数在水平或垂直轴上连接DataFrame

  • merge方法基于某些逻辑标准连接两个DataFrame

  • 内连接(inner join)识别两个 DataFrame 之间的共同值。对于任何匹配项,pandas 会将右侧 DataFrame 的所有列拉入左侧 DataFrame

  • 外连接(outer join)合并两个 DataFrame。Pandas 包含那些仅属于一个数据集或共享的值。

  • 左连接(left join)在右侧 DataFrame 的值存在于左侧 DataFrame 时,会拉入列。这个操作在 Excel 中相当于 VLOOKUP

  • 当第二个 DataFrame 包含我们希望附加到主 DataFrame 的补充信息时,左连接是理想的。

11 处理日期和时间

本章涵盖

  • 将字符串 Series 转换为日期时间

  • 从日期时间对象检索日期和时间信息

  • 将日期四舍五入到周、月和季度末

  • 在日期时间之间添加和减去

一个 datetime 是用于存储日期和时间的数据类型。它可以表示一个特定的日期(例如 2021 年 10 月 4 日),一个特定的时间(例如上午 11:50),或者两者(例如 2021 年 10 月 4 日上午 11:50)。日期时间非常有价值,因为它们允许我们跟踪时间趋势。一个金融分析师可能会使用日期时间来确定股票表现最佳的星期几。一个餐馆老板可能会使用它们来发现顾客光顾业务的高峰时段。一个运营经理可能会使用它们来识别生产中造成瓶颈的过程部分。数据集中的 何时 常常可以引导到 为什么

在本章中,我们将回顾 Python 的内置 datetime 对象,并了解 pandas 如何通过其 TimestampTimedelta 对象来改进它们。我们还将学习如何使用该库将字符串转换为日期,添加和减去时间偏移量,计算持续时间,等等。没有时间可以浪费(这里有个双关语),让我们开始吧。

11.1 介绍 Timestamp 对象

一个 模块 是一个包含 Python 代码的文件。Python 的标准库是语言中内置的超过 250 个模块的集合,它们为常见问题提供经过实战检验的解决方案,例如数据库连接、数学和测试。标准库的存在是为了让开发者能够编写使用核心语言特性的软件,而不是安装额外的依赖项。常有人说 Python “自带电池”;就像玩具一样,语言可以直接使用。

11.1.1 Python 如何处理日期时间

为了减少内存消耗,Python 默认不会自动加载其标准库模块。相反,我们必须明确地将任何所需的模块导入到我们的项目中。与外部包(如 pandas)一样,我们可以使用 import 关键字导入一个模块,并使用 as 关键字为其分配别名。标准库的 datetime 模块是我们的目标;它存储用于处理日期和时间的类。dtdatetime 模块的流行别名。让我们启动一个新的 Jupyter Notebook 并导入 datetime 以及 pandas 库:

In  [1] import datetime as dt
        import pandas as pd

让我们回顾模块中的四个类:datetimedatetimetimedelta。(有关类和对象的更多详细信息,请参阅附录 B。)

date 模型历史中的一个单独的一天。该对象不存储任何时间。date 类构造函数接受顺序的 yearmonthday 参数。所有参数都期望是整数。下一个示例为我的生日,1991 年 4 月 12 日,实例化一个 date 对象:

In  [2] # The two lines below are equivalent
        birthday = dt.date(1991, 4, 12)
        birthday = dt.date(year = 1991, month = 4, day = 12)
        birthday

Out [2] datetime.date(1991, 4, 12)

date 对象将构造函数的参数保存为对象属性。我们可以通过 yearmonthday 属性来访问它们的值:

In  [3] birthday.year

Out [3] 1991

In  [4] birthday.month

Out [4] 4

In  [5] birthday.day

Out [5] 12

date对象是不可变的——我们创建后不能更改其内部状态。如果我们尝试覆盖任何date属性,Python 将引发AttributeError异常:

In  [6] birthday.month = 10

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-15-2690a31d7b19> in <module>
----> 1 birthday.month = 10

AttributeError: attribute 'month' of 'datetime.date' objects is not writable

补充的time类表示一天中的特定时间。日期无关紧要。time构造函数的前三个参数接受整数参数,用于hourminutesecond。与date对象一样,time对象是不可变的。下一个示例实例化一个time对象,表示上午 6:43:25:

In  [7] # The two lines below are equivalent
        alarm_clock = dt.time(6, 43, 25)
        alarm_clock = dt.time(hour = 6, minute = 43, second = 25)
        alarm_clock

Out [7] datetime.time(6, 43, 25)

所有三个参数的默认值都是 0。如果我们不带参数实例化一个time对象,它将代表午夜(凌晨 12:00:00)。午夜是一天的 0 小时、0 分钟和 0 秒:

In  [8] dt.time()

Out [8] datetime.time(0, 0)

下一个示例将 9 传递给hour参数,42 传递给second参数,没有为minute参数传递值。time对象将minutes值替换为 0。得到的时间是上午 9:00:42:

In  [9] dt.time(hour = 9, second = 42)

Out [9] datetime.time(9, 0, 42)

time构造函数使用 24 小时制时钟;我们可以传递大于或等于 12 的hour值来表示下午或晚上的时间。下一个示例表示 19:43:22 或等价于晚上 7:43:22:

In  [10] dt.time(hour = 19, minute = 43, second = 22)

Out [10] datetime.time(19, 43, 22)

time对象将构造函数参数保存为对象属性。我们可以使用hourminutesecond属性访问它们的值:

In  [11] alarm_clock.hour

Out [11] 6

In  [12] alarm_clock.minute

Out [12] 43

In  [13] alarm_clock.second

Out [13] 25

接下来是datetime对象,它包含日期和时间。它的前六个参数是yearmonthdayhourminutesecond

In  [14] # The two lines below are equivalent
         moon_landing = dt.datetime(1969, 7, 20, 22, 56, 20)
         moon_landing = dt.datetime(
             year = 1969,
             month = 7,
             day = 20,
             hour = 22,
             minute = 56,
             second = 20
         )
         moon_landing

Out [14] datetime.datetime(1969, 7, 20, 22, 56, 20)

yearmonthday参数是必需的。与时间相关的属性是可选的,默认值为0。下一个示例表示 2020 年 1 月 1 日凌晨(12:00:00 a.m.)。我们明确传递了yearmonthday参数;hourminutesecond参数隐式地回退到0

In  [15] dt.datetime(2020, 1, 1)

Out [15] datetime.datetime(2020, 1, 1, 0, 0)

我们从datetime模块中的最后一个值得注意的对象是timedelta,它表示一个持续时间——时间的长度。其构造函数的参数包括weeksdayshours。所有参数都是可选的,默认值为0。构造函数将时间长度相加以计算总持续时间。在下一个示例中,我们添加了 8 周和 6 天,总共 62 天(8 周 * 7 天 + 6 天)。Python 还添加了 3 小时、58 分钟和 12 秒,总共有 14,292 秒(238 分钟 * 60 秒 + 12 秒):

In  [16] dt.timedelta(
             weeks = 8,
             days = 6,
             hours = 3,
             minutes = 58,
             seconds = 12
         )

Out [16] datetime.timedelta(days=62, seconds=14292)

现在我们已经熟悉了 Python 如何表示日期、时间和持续时间,让我们来探索 pandas 如何在此基础上构建这些概念。

11.1.2 Pandas 如何处理日期时间

Python 的datetime模块受到了一些批评。一些常见的投诉包括

  • 需要跟踪大量的模块。我们只在本章中介绍了datetime,但还有其他模块可用于日历、时间转换、实用函数等。

  • 需要记住大量的课程。

  • 复杂、困难的对象 API 用于时区逻辑。

Pandas 引入了 Timestamp 对象作为 Python 的 datetime 对象的替代品。我们可以将 Timestampdatetime 对象视为兄弟;在 pandas 生态系统中,它们经常可以互换,例如作为方法参数传递。就像 Series 扩展了 Python 列表一样,Timestamp 为更原始的 datetime 对象添加了功能。随着我们进入本章,我们将看到一些这些特性:

Timestamp 构造函数在 pandas 的顶层可用;它接受与 datetime 构造函数相同的参数。三个与日期相关的参数(yearmonthday)是必需的。与时间相关的参数是可选的,默认为 0. 这里,我们再次模拟 1991 年 4 月 12 日,一个辉煌的日子:

In  [17] # The two lines below are equivalent
         pd.Timestamp(1991, 4, 12)
         pd.Timestamp(year = 1991, month = 4, day = 12)

Out [17] Timestamp('1991-04-12 00:00:00')

Pandas 认为如果两个对象存储相同的信息,则 Timestamp 等于 date/datetime。我们可以使用 == 符号来比较对象相等性:

In  [18] (pd.Timestamp(year = 1991, month = 4, day = 12)
            == dt.date(year = 1991, month = 4, day = 12))

Out [18] True

In  [19] (pd.Timestamp(year = 1991, month = 4, day = 12, minute = 2)
            == dt.datetime(year = 1991, month = 4, day = 12, minute = 2))

Out [19] True

如果日期或时间有任何差异,两个对象将不相等。下一个示例使用 minute 值为 2Timestampminute 值为 1datetime 实例化。相等比较的结果为 False

In  [20] (pd.Timestamp(year = 1991, month = 4, day = 12, minute = 2)
            == dt.datetime(year = 1991, month = 4, day = 12, minute = 1))

Out [20] False

Timestamp 构造函数非常灵活,接受各种输入。下一个示例将字符串传递给构造函数而不是整数序列。文本存储了一个日期,格式为 YYYY-MM-DD(四位年份,两位月份,两位日期)。Pandas 正确地解析了输入中的月份、日期和年份:

In  [21] pd.Timestamp("2015-03-31")

Out [21] Timestamp('2015-03-31 00:00:00')

Pandas 识别许多标准的 datetime 字符串格式。下一个示例将日期字符串中的破折号替换为斜杠:

In  [22] pd.Timestamp("2015/03/31")

Out [22] Timestamp('2015-03-31 00:00:00')

下一个示例传递一个 MM/DD/YYYY 格式的字符串,这对 pandas 来说没问题:

In  [23] pd.Timestamp("03/31/2015")

Out [23] Timestamp('2015-03-31 00:00:00')

我们还可以以各种书面格式包含时间:

In  [24] pd.Timestamp("2021-03-08 08:35:15")

Out [24] Timestamp('2021-03-08 08:35:15')

In  [25] pd.Timestamp("2021-03-08 6:13:29 PM")

Out [25] Timestamp('2021-03-08 18:13:29')

最后,Timestamp 构造函数接受 Python 的原生 datetimedatetime 对象。下一个示例从 datetime 对象解析数据:

In  [26] pd.Timestamp(dt.datetime(2000, 2, 3, 21, 35, 22))

Out [26] Timestamp('2000-02-03 21:35:22')

Timestamp 对象实现了所有 datetime 属性,如 hourminutesecond。下一个示例将之前的 Timestamp 保存到变量中,然后输出几个属性:

In  [27] my_time = pd.Timestamp(dt.datetime(2000, 2, 3, 21, 35, 22))
         print(my_time.year)
         print(my_time.month)
         print(my_time.day)
         print(my_time.hour)
         print(my_time.minute)
         print(my_time.second)
Out [27] 2000
         2
         3
         21
         35
         22

Pandas 尽力确保其 datetime 对象与 Python 内置的类似。我们可以认为这些对象在 pandas 操作中是有效可互换的。

11.2 在 DatetimeIndex 中存储多个时间戳

索引 是附加到 pandas 数据结构上的标识标签集合。我们迄今为止遇到的最常见的索引是 RangeIndex,它是一系列升序或降序的数值。我们可以通过 index 属性访问 SeriesDataFrame 的索引:

In  [28] pd.Series([1, 2, 3]).index

Out [28] RangeIndex(start=0, stop=3, step=1)

Pandas 使用一个 Index 对象来存储一系列字符串标签。在下一个示例中,请注意 pandas 附加到 Series 的索引对象会根据其内容而变化:

In  [29] pd.Series([1, 2, 3], index = ["A", "B", "C"]).index

Out [29] Index(['A', 'B', 'C'], dtype='object')

DatetimeIndex 是用于存储 Timestamp 对象的索引。如果我们向 Series 构造函数的 index 参数传递一个 Timestamps 的列表,pandas 将将 DatetimeIndex 附加到 Series

In  [30] timestamps = [
             pd.Timestamp("2020-01-01"),
             pd.Timestamp("2020-02-01"),
             pd.Timestamp("2020-03-01"),
         ]

         pd.Series([1, 2, 3], index = timestamps).index

Out [30] DatetimeIndex(['2020-01-01', '2020-02-01', '2020-03-01'],
         dtype='datetime64[ns]', freq=None)

如果我们传递一个 Python datetime 对象的列表,Pandas 也会使用 DatetimeIndex

In  [31] datetimes = [
             dt.datetime(2020, 1, 1),
             dt.datetime(2020, 2, 1),
             dt.datetime(2020, 3, 1),
         ]

         pd.Series([1, 2, 3], index = datetimes).index

Out [31] DatetimeIndex(['2020-01-01', '2020-02-01', '2020-03-01'],
         dtype='datetime64[ns]', freq=None)

我们也可以从头创建一个 DatetimeIndex。其构造函数位于 pandas 的顶层。构造函数的 data 参数接受任何日期的可迭代集合。我们可以将日期作为字符串、datetimes、Timestamps 或甚至数据类型的混合传递。Pandas 将将所有值转换为等效的 Timestamps 并存储在索引中:

In  [32] string_dates = ["2018/01/02", "2016/04/12", "2009/09/07"]
         pd.DatetimeIndex(data = string_dates)

Out [32] DatetimeIndex(['2018-01-02', '2016-04-12', '2009-09-07'],
         dtype='datetime64[ns]', freq=None)

In  [33] mixed_dates = [
             dt.date(2018, 1, 2),
             "2016/04/12",
             pd.Timestamp(2009, 9, 7)
         ]

         dt_index = pd.DatetimeIndex(mixed_dates)
         dt_index

Out [33] DatetimeIndex(['2018-01-02', '2016-04-12', '2009-09-07'],
         dtype='datetime64[ns]', freq=None)

现在我们已经将 DatetimeIndex 分配给 dt_index 变量,让我们将其附加到 pandas 数据结构中。下一个示例将索引连接到一个样本 Series

In  [34] s = pd.Series(data = [100, 200, 300], index = dt_index)
         s

Out [34] 2018-01-02    100
         2016-04-12    200
         2009-09-07    300
         dtype: int64

只有当我们将值存储为 Timestamps 而不是字符串时,pandas 才能执行日期和时间相关的操作。Pandas 无法从像 "2018-01-02" 这样的字符串中推断出星期几,因为它将其视为数字和短划线的集合,而不是实际的日期。这就是为什么在第一次导入数据集时,将所有相关字符串列转换为日期时间至关重要:

我们可以使用 sort_index 方法按升序或降序排序 DatetimeIndex。下一个示例按升序(从最早到最新)排序索引日期:

In  [35] s.sort_index()

Out [35] 2009-09-07    300
         2016-04-12    200
         2018-01-02    100
         dtype: int64

Pandas 在排序或比较日期时间时考虑日期和时间。如果两个 Timestamps 使用相同的日期,pandas 将比较它们的时、分、秒等:

对于 Timestamps,有各种排序和比较操作可用。例如,小于符号(<)检查一个 Timestamp 是否早于另一个:

In  [36] morning = pd.Timestamp("2020-01-01 11:23:22 AM")
         evening = pd.Timestamp("2020-01-01 11:23:22 PM")

         morning < evening

Out [36] True

在 11.7 节中,我们将学习如何将这些类型的比较应用于 Series 中的所有值。

11.3 将列或索引值转换为日期时间

我们本章的第一个数据集,disney.csv,包含了华特迪士尼公司近 60 年的股价,这是世界上最知名娱乐品牌之一。每一行包括一个日期,该日股票的最高价和最低价,以及开盘价和收盘价:

In  [37] disney = pd.read_csv("disney.csv")
         disney.head()

Out [37]

 ** Date      High       Low      Open     Close**
0  1962-01-02  0.096026  0.092908  0.092908  0.092908
1  1962-01-03  0.094467  0.092908  0.092908  0.094155
2  1962-01-04  0.094467  0.093532  0.094155  0.094155
3  1962-01-05  0.094779  0.093844  0.094155  0.094467
4  1962-01-08  0.095714  0.092285  0.094467  0.094155

read_csv 函数默认将非数字列的所有值导入为字符串。我们可以通过 DataFramedtypes 属性来查看列的数据类型。注意,日期列的数据类型为 "object",这是 pandas 对字符串的指定:

In  [38] disney.dtypes

Out [38] Date      object
         High     float64
         Low      float64
         Open     float64
         Close    float64
         dtype: object

我们必须明确告诉 pandas 哪些列的值要转换为日期时间。我们之前看到的一个选项是 read_csv 函数的 parse_dates 参数,它在第三章中引入。我们可以将参数传递给一个列表,其中包含 pandas 应将其值转换为日期时间的列:

In  [39] disney = pd.read_csv("disney.csv", parse_dates = ["Date"])

另一个解决方案是 pandas 顶层中的to_datetime转换函数。该函数接受一个可迭代对象(例如 Python 列表、元组、Series或索引),将其值转换为日期时间,并返回新的值在一个DatetimeIndex中。以下是一个小例子:

In  [40] string_dates = ["2015-01-01", "2016-02-02", "2017-03-03"]
         dt_index = pd.to_datetime(string_dates)
         dt_index

Out [40] DatetimeIndex(['2015-01-01', '2016-02-02', '2017-03-03'],
         dtype='datetime64[ns]', freq=None)

让我们把来自 disney DataFrame的日期Series传递给to_datetime函数:

In  [41] pd.to_datetime(disney["Date"]).head()

Out [41] 0   1962-01-02
         1   1962-01-03
         2   1962-01-04
         3   1962-01-05
         4   1962-01-08
         Name: Date, dtype: datetime64[ns]

我们有一个日期时间的Series,所以让我们覆盖原始DataFrame。接下来的代码示例将原始日期列替换为新的日期时间Series。记住,Python 首先评估等号右侧的表达式:

In  [42] disney["Date"] = pd.to_datetime(disney["Date"])

让我们再次通过dtypes属性检查日期列:

In  [43] disney.dtypes

Out [43] Date     datetime64[ns]
         High            float64
         Low             float64
         Open            float64
         Close           float64
         dtype: object

太好了;我们有一个日期时间列!我们的日期值存储正确后,我们可以探索 pandas 提供的强大的内置日期时间功能。

11.4 使用 DatetimeProperties 对象

一个日期时间Series包含一个特殊的dt属性,它暴露了一个DatetimeProperties对象:

In  [44] disney["Date"].dt

Out [44] <pandas.core.indexes.accessors.DatetimeProperties object at
         0x116247950>

我们可以在DatetimeProperties对象上访问属性并调用方法来从列的日期时间值中提取信息。dt属性对于日期时间就像str属性对于字符串一样。(参见第六章对str的回顾。)这两个属性都专门用于特定类型数据的操作。

让我们从DatetimeProperties对象的day属性开始探索,该属性从每个日期中提取出天。Pandas 返回值在一个新的Series中:

In  [45] disney["Date"].head(3)

Out [45] 0   1962-01-02
         1   1962-01-03
         2   1962-01-04
         Name: Date, dtype: datetime64[ns]

In  [46] disney["Date"].dt.day.head(3)

Out [46] 0    2
         1    3
         2    4
         Name: Date, dtype: int64

month属性返回一个包含月份数字的Series。1 月有month值为1,2 月有month值为2,依此类推。需要注意的是,这与我们在 Python/pandas 中通常的计数方式不同,在那里我们给第一个元素分配值为0

In  [47] disney["Date"].dt.month.head(3)

Out [47] 0    1
         1    1
         2    1
         Name: Date, dtype: int64

year属性返回一个新的包含年份的Series

In  [48] disney["Date"].dt.year.head(3)

Out [48] 0    1962
         1    1962
         2    1962
         Name: Date, dtype: int64

之前的属性相当简单。我们可以要求 pandas 提取更有趣的信息。一个例子是dayofweek属性,它返回每个日期星期数的Series0表示星期一,1表示星期二,以此类推,直到6表示星期日。在以下输出中,索引位置 0 处的1值表示 1962 年 1 月 2 日是星期二:

In  [49] disney["Date"].dt.dayofweek.head()

Out [49] 0    1
         1    2
         2    3
         3    4
         4    0
         Name: Date, dtype: int64

如果我们想要的是星期的名称而不是数字,那么day_name方法就能派上用场。注意语法。我们是在dt对象上调用这个方法,而不是在Series本身上:

In  [50] disney["Date"].dt.day_name().head()

Out [50] 0      Tuesday
         1    Wednesday
         2     Thursday
         3       Friday
         4       Monday
         Name: Date, dtype: object

我们可以将这些dt属性和方法与其他 pandas 功能结合使用进行高级分析。以下是一个例子。让我们计算迪士尼股票按星期的平均表现。我们将首先将dt.day_name方法返回的Series附加到 disney DataFrame上:

In  [51] disney["Day of Week"] = disney["Date"].dt.day_name()

我们可以根据新星期几列的值对行进行分组(这是一种在第七章中介绍的技术):

In  [52] group = disney.groupby("Day of Week")

我们可以调用GroupBy对象的mean方法来计算每个分组的值的平均值:

In  [53] group.mean()

Out [53]

                  High        Low       Open      Close
**Day of Week** 
Friday       23.767304  23.318898  23.552872  23.554498
Monday       23.377271  22.930606  23.161392  23.162543
Thursday     23.770234  23.288687  23.534561  23.540359
Tuesday      23.791234  23.335267  23.571755  23.562907
Wednesday    23.842743  23.355419  23.605618  23.609873

在三行代码中,我们计算了按周计算的平均股票表现。

让我们回到dt对象方法。补充的month_name方法返回包含日期月份名称的Series

In  [54] disney["Date"].dt.month_name().head()

Out [54] 0    January
         1    January
         2    January
         3    January
         4    January
         Name: Date, dtype: object

dt对象上的一些属性返回布尔值。假设我们想探索迪士尼在其历史中每个季度的股票表现。商业年的四个季度分别从 1 月 1 日、4 月 1 日、7 月 1 日和 10 月 1 日开始。is_quarter_start属性返回一个布尔Series,其中True表示该行的日期落在季度开始日:

In  [55] disney["Date"].dt.is_quarter_start.tail()

Out [55] 14722    False
         14723    False
         14724    False
         14725     True
         14726    False
         Name: Date, dtype: bool

我们可以使用布尔Series来提取在季度开始时掉落的迪士尼行。下一个示例使用熟悉的方括号语法来提取行:

In  [56] disney[disney["Date"].dt.is_quarter_start].head()

Out [56]

 **Date      High       Low      Open     Close Day of Week**
189 1962-10-01  0.064849  0.062355  0.063913  0.062355      Monday
314 1963-04-01  0.087989  0.086704  0.087025  0.086704      Monday
377 1963-07-01  0.096338  0.095053  0.096338  0.095696      Monday
441 1963-10-01  0.110467  0.107898  0.107898  0.110467     Tuesday
565 1964-04-01  0.116248  0.112394  0.112394  0.116248   Wednesday

我们可以使用is_quarter_end属性来提取在季度结束时掉落的日期:

In  [57] disney[disney["Date"].dt.is_quarter_end].head()

Out [57]

 **Date      High       Low      Open     Close Day of Week**
251 1962-12-31  0.074501  0.071290  0.074501  0.072253      Monday
440 1963-09-30  0.109825  0.105972  0.108541  0.107577      Monday
502 1963-12-31  0.101476  0.096980  0.097622  0.101476     Tuesday
564 1964-03-31  0.115605  0.112394  0.114963  0.112394     Tuesday
628 1964-06-30  0.101476  0.100191  0.101476  0.100834     Tuesday

补充的is_month_startis_month_end属性确认日期是在月份的开始或结束:

In  [58] disney[disney["Date"].dt.is_month_start].head()

Out [58]

 **Date      High       Low      Open     Close Day of Week**
22  1962-02-01  0.096338  0.093532  0.093532  0.094779    Thursday
41  1962-03-01  0.095714  0.093532  0.093532  0.095714    Thursday
83  1962-05-01  0.087296  0.085426  0.085738  0.086673     Tuesday
105 1962-06-01  0.079814  0.077943  0.079814  0.079814      Friday
147 1962-08-01  0.068590  0.068278  0.068590  0.068590   Wednesday
In  [59] disney[disney["Date"].dt.is_month_end].head()

Out [59]

 **Date      High       Low      Open     Close Day of Week**
21  1962-01-31  0.093844  0.092908  0.093532  0.093532   Wednesday
40  1962-02-28  0.094779  0.093220  0.094155  0.093220   Wednesday
82  1962-04-30  0.087608  0.085738  0.087608  0.085738      Monday
104 1962-05-31  0.082308  0.079814  0.079814  0.079814    Thursday
146 1962-07-31  0.069214  0.068278  0.068278  0.068590     Tuesday

is_year_start属性如果日期在年初,则返回True。下一个示例返回一个空的DataFrame;由于新年那天股市关闭,数据集中的日期都不符合标准:

In  [60] disney[disney["Date"].dt.is_year_start].head()

Out [60]

 **Date      High       Low      Open     Close Day of Week**

补充的is_year_end属性如果日期在年底,则返回True

In  [61] disney[disney["Date"].dt.is_year_end].head()

Out [61]

 **Date      High       Low      Open     Close Day of Week**
251  1962-12-31  0.074501  0.071290  0.074501  0.072253      Monday
502  1963-12-31  0.101476  0.096980  0.097622  0.101476     Tuesday
755  1964-12-31  0.117853  0.116890  0.116890  0.116890    Thursday
1007 1965-12-31  0.154141  0.150929  0.153498  0.152214      Friday
1736 1968-12-31  0.439301  0.431594  0.434163  0.436732     Tuesday

无论属性如何,过滤过程都保持不变:创建一个布尔Series,然后将其传递到DataFrame后面的方括号内。

11.5 添加和减去时间持续时间

我们可以使用DateOffset对象添加或减去一致的时间持续时间。其构造函数在 pandas 的顶层可用。构造函数接受yearsmonthsdays等参数。下一个示例模拟了三年、四个月和三天的时间:

In  [62] pd.DateOffset(years = 3, months = 4, days = 5)

Out [62] <DateOffset: days=5, months=4, years=3>

这里是迪士尼DataFrame前五行的提醒:

In  [63] disney["Date"].head()

Out [63] 0   1962-01-02
         1   1962-01-03
         2   1962-01-04
         3   1962-01-05
         4   1962-01-08
         Name: Date, dtype: datetime64[ns]

为了举例,让我们假设我们的记录系统出现故障,日期列中的日期偏差了五天。我们可以使用加号(+)和DateOffset对象向日期时间Series中的每个日期添加一个一致的时间量。加号表示“向前移动”或“进入未来。”下一个示例将日期列中的每个日期增加五天:

In  [64] (disney["Date"] + pd.DateOffset(days = 5)).head()

Out [64] 0   1962-01-07
         1   1962-01-08
         2   1962-01-09
         3   1962-01-10
         4   1962-01-13
         Name: Date, dtype: datetime64[ns]

当与DateOffset一起使用时,减号(-)从日期Series中的每个日期减去一个持续时间。减号表示“向后移动”或“进入过去。”下一个示例将每个日期向后移动三天:

In  [65] (disney["Date"] - pd.DateOffset(days = 3)).head()

Out [65] 0   1961-12-30
         1   1961-12-31
         2   1962-01-01
         3   1962-01-02
         4   1962-01-05
         Name: Date, dtype: datetime64[ns]

尽管前面的输出没有显示,但Timestamp对象确实内部存储时间。当我们将日期列的值转换为日期时间时,pandas 假设每个日期的时间为午夜。下一个示例向DateOffset构造函数添加一个hours参数,以向日期中的每个日期时间添加一个一致的时间。结果Series显示日期和时间:

In  [66] (disney["Date"] + pd.DateOffset(days = 10, hours = 6)).head()

Out [66] 0   1962-01-12 06:00:00
         1   1962-01-13 06:00:00
         2   1962-01-14 06:00:00
         3   1962-01-15 06:00:00
         4   1962-01-18 06:00:00
         Name: Date, dtype: datetime64[ns]

Pandas 在减去持续时间时应用相同的逻辑。下一个示例从每个日期减去一年、三个月、十天、六小时和三分钟:

In  [67] (
             disney["Date"]
             - pd.DateOffset(
                 years = 1, months = 3, days = 10, hours = 6, minutes = 3
             )
         ).head()

Out [67] 0   1960-09-21 17:57:00
         1   1960-09-22 17:57:00
         2   1960-09-23 17:57:00
         3   1960-09-24 17:57:00
         4   1960-09-27 17:57:00
         Name: Date, dtype: datetime64[ns]

DateOffset构造函数支持额外的秒、微秒和纳秒关键字参数。有关更多信息,请参阅 pandas 文档。

11.6 日期偏移

DateOffset对象对于向每个日期添加或减去固定的时间量是最优的。现实世界的分析通常需要更动态的计算。假设我们想要将每个日期四舍五入到当前月的月底。每个日期距离其月底的天数不同,因此一致的DateOffset添加是不够的。

Pandas 提供了预构建的偏移对象,用于动态的时间计算。这些对象定义在库中的offsets.py模块中。在我们的代码中,我们必须使用它们的完整路径作为前缀:pd.offsets

一个示例偏移量是MonthEnd,它将每个日期四舍五入到下个月的月底。这里是对日期列最后五行的复习:

In  [68] disney["Date"].tail()

Out [68] 14722   2020-06-26
         14723   2020-06-29
         14724   2020-06-30
         14725   2020-07-01
         14726   2020-07-02
         Name: Date, dtype: datetime64[ns]

我们可以将第 11.5 节中的加法和减法语法应用于 pandas 的偏移对象。下一个示例返回一个新的Series,将每个日期时间四舍五入到月底。加号表示时间向前移动,因此我们移动到下个月的月底:

In  [69] (disney["Date"] + pd.offsets.MonthEnd()).tail()

Out [69] 14722   2020-06-30
         14723   2020-06-30
         14724   2020-07-31
         14725   2020-07-31
         14726   2020-07-31
         Name: Date, dtype: datetime64[ns]

必须在预期的方向上有所移动。Pandas 不能将日期四舍五入到相同的日期。因此,如果一个日期位于月底,库会将其四舍五入到下个月的月底。Pandas 将索引位置 14724 处的 2020-06-30 四舍五入到 2020-07-31,即下一个可用的月底。

减号将每个日期向后移动。下一个示例使用MonthEnd偏移量将日期四舍五入到上个月的月底。Pandas 将前三个日期(2020-06-26、2020-06-29 和 2020-06-30)四舍五入到 5 月的最后一天,即 2020-05-31。它将最后两个日期(2020-07-01 和 2020-07-02)四舍五入到 6 月的最后一天,即 2020-06-30:

In  [70] (disney["Date"] - pd.offsets.MonthEnd()).tail()

Out [70] 14722   2020-05-31
         14723   2020-05-31
         14724   2020-05-31
         14725   2020-06-30
         14726   2020-06-30
         Name: Date, dtype: datetime64[ns]

相补的MonthBegin偏移量将四舍五入到一个月的第一天。下一个示例使用加号+将每个日期四舍五入到下个月的第一天。Pandas 将前三个日期(2020-06-26、2020-06-29 和 2020-06-30)四舍五入到 7 月的开始,即 2020-07-01。Pandas 将剩下的两个 7 月日期(2020-07-01 和 2020-07-02)四舍五入到 8 月的第一天,即 2020-08-01:

In  [71] (disney["Date"] + pd.offsets.MonthBegin()).tail()

Out [71] 14722   2020-07-01
         14723   2020-07-01
         14724   2020-07-01
         14725   2020-08-01
         14726   2020-08-01
         Name: Date, dtype: datetime64[ns]

我们可以将MonthBegin偏移量与减号结合使用,将日期回滚到月份的开始。在下一个示例中,pandas 将前三个日期(2020-06-26、2020-06-29 和 2020-06-30)回滚到 2020 年 6 月 1 日,即 6 月的开始。它将最后一个日期,2020-07-02,回滚到 7 月的开始,即 2020-07-01。一个有趣的情况是索引位置 14725 处的 2020-07-01。正如我们之前提到的,pandas 不能将日期回滚到相同的日期。因此,必须有一些回滚的动作,所以 pandas 将其回滚到上个月的开头,即 2020-06-01:

In  [72] (disney["Date"] - pd.offsets.MonthBegin()).tail()

Out [72] 14722   2020-06-01
         14723   2020-06-01
         14724   2020-06-01
         14725   2020-06-01
         14726   2020-07-01
         Name: Date, dtype: datetime64[ns]

对于商业时间计算,有一组特殊的偏移量可用;它们的名称以大写 "B" 开头。例如,商业月末 (BMonthEnd) 偏移量将四舍五入到该月的最后工作日。这五个工作日是星期一、星期二、星期三、星期四和星期五。

考虑以下三个日期时间的 Series。这三个日期分别落在星期四、星期五和星期六:

In  [73] may_dates = ["2020-05-28", "2020-05-29", "2020-05-30"]
         end_of_may = pd.Series(pd.to_datetime(may_dates))
         end_of_may

Out [73] 0   2020-05-28
         1   2020-05-29
         2   2020-05-30
         dtype: datetime64[ns]

让我们比较 MonthEndBMonthEnd 偏移量。当我们用加号搭配 MonthEnd 偏移量时,pandas 将所有三个日期四舍五入到 2020 年 5 月的最后一天,即 2020-05-31。无论这个日期是否是工作日或周末都无关紧要:

In  [74] end_of_may + pd.offsets.MonthEnd()

Out [74] 0   2020-05-31
         1   2020-05-31
         2   2020-05-31
         dtype: datetime64[ns]

BMonthEnd 偏移量返回不同的结果集。2020 年 5 月的最后工作日是星期五,5 月 29 日。Pandas 将 Series 中的第一个日期,2020-05-28,四舍五入到 29 日。下一个日期,2020-05-29,正好是该月的最后工作日。Pandas 不能将日期四舍五入到相同的日期,所以它将 2020-05-29 四舍五入到 6 月的最后工作日,即 2020-06-30,星期二。Series 中的最后一个日期,2020-05-30,是星期六。5 月没有剩余的工作日,所以 pandas 同样将日期四舍五入到 6 月的最后工作日,即 2020-06-30:

In  [75] end_of_may + pd.offsets.BMonthEnd()

Out [75] 0   2020-05-29
         1   2020-06-30
         2   2020-06-30
         dtype: datetime64[ns]

pd.offsets 模块包括额外的偏移量,用于四舍五入到季度、商业季度、年份、商业年份等的开始和结束。请在你的空闲时间自由探索它们。

11.7 Timedelta 对象

你可能还记得本章前面提到的 Python 的原生 timedelta 对象。timedelta 模型了持续时间——两个时间点之间的距离。像一小时这样的持续时间表示时间的长度;它没有特定的日期或时间。Pandas 使用自己的 Timedelta 对象来模型持续时间。

注意:这两个对象很容易混淆。timedelta 是 Python 内置的,而 Timedelta 是 pandas 内置的。当与 pandas 操作一起使用时,这两个对象是可以互换的。

Timedelta 构造函数在 pandas 顶层可用。它接受时间单位的键控参数,如 dayshoursminutesseconds。下一个示例实例化了一个 Timedelta,它模型了八天、七小时、六分钟和五秒:

In  [76] duration = pd.Timedelta(
            days = 8,
            hours = 7,
            minutes = 6,
            seconds = 5
        )

         duration

Out [76] Timedelta('8 days 07:06:05')

pandas 顶层中的 to_timedelta 函数将它的参数转换为 Timedelta 对象。我们可以传递一个字符串,如下一个示例所示:

In  [77] duration = pd.to_timedelta("3 hours, 5 minutes, 12 seconds")

Out [77] Timedelta('0 days 03:05:12')

我们还可以将一个整数和一个 unit 参数一起传递给 to_timedelta 函数。unit 参数声明了数字所代表的时间单位。接受的参数包括 "hour""day""minute"。下一个示例中的 Timedelta 模型了一个五小时的持续时间:

In  [78] pd.to_timedelta(5, unit = "hour")

Out [78] Timedelta('0 days 05:00:00')

我们可以将一个可迭代对象,如列表,传递给 to_timedelta 函数,将其值转换为 Timedeltas。Pandas 将 Timedeltas 存储在 TimedeltaIndex 中,这是一个用于存储持续时间的 pandas 索引:

In  [79] pd.to_timedelta([5, 10, 15], unit = "day")

Out [79] TimedeltaIndex(['5 days', '10 days', '15 days'],
         dtype='timedelta64[ns]', freq=None)

通常,Timedelta 对象是从头创建的,而不是从头创建的。例如,从一个 Timestamp 减去另一个 Timestamp 会自动返回一个 Timedelta

In  [80] pd.Timestamp("1999-02-05") - pd.Timestamp("1998-05-24")

Out [80] Timedelta('257 days 00:00:00')

现在我们已经熟悉了 Timedelta,让我们导入本章的第二个数据集:deliveries.csv。CSV 跟踪一家虚构公司的产品运输。每一行包括订单日期和交货日期:

In  [81] deliveries = pd.read_csv("deliveries.csv")
         deliveries.head()

Out [81]

 **order_date delivery_date**
0    5/24/98        2/5/99
1    4/22/92        3/6/98
2    2/10/91       8/26/92
3    7/21/92      11/20/97
4     9/2/93       6/10/98

让我们练习将两个列中的值转换为日期时间。是的,我们可以使用 parse_dates 参数,但让我们尝试另一种方法。一个选项是两次调用 to_datetime 函数,一次用于订单日期列,一次用于交货日期列,并覆盖现有的 DataFrame 列:

In  [82] deliveries["order_date"] = pd.to_datetime(
             deliveries["order_date"]
         )

         deliveries["delivery_date"] = pd.to_datetime(
             deliveries["delivery_date"]
         )

一个更可扩展的解决方案是使用 for 循环遍历列名。我们可以动态地引用一个交货列,使用 to_datetime 从它创建一个 TimestampsDatetimeIndex,然后覆盖原始列:

In  [83] for column in ["order_date", "delivery_date"]:
             deliveries[column] = pd.to_datetime(deliveries[column])

让我们看看交货情况。新的列格式确认我们已经将字符串转换为日期时间:

In  [84] deliveries.head()

Out [84]

 **order_date delivery_date**
0 1998-05-24    1999-02-05
1 1992-04-22    1998-03-06
2 1991-02-10    1992-08-26
3 1992-07-21    1997-11-20
4 1993-09-02    1998-06-10

让我们来计算每批货物的持续时间。使用 pandas,这个计算就像从订单日期列减去交货日期列一样简单:

In  [85] (deliveries["delivery_date"] - deliveries["order_date"]).head()

Out [85] 0    257 days
         1   2144 days
         2    563 days
         3   1948 days
         4   1742 days
         dtype: timedelta64[ns]

Pandas 返回一个 timedeltaSeries。让我们将这个新的 Series附加到交货 DataFrame 的末尾:

In  [86] deliveries["duration"] = (
             deliveries["delivery_date"] - deliveries["order_date"]
         )
         deliveries.head()

Out [86]

 **order_date delivery_date  duration**
0 1998-05-24    1999-02-05  257 days
1 1992-04-22    1998-03-06 2144 days
2 1991-02-10    1992-08-26  563 days
3 1992-07-21    1997-11-20 1948 days
4 1993-09-02    1998-06-10 1742 days

现在我们有两个 Timestamp 列和一个 Timedelta 列:

In  [87] deliveries.dtypes

Out [87] order_date        datetime64[ns]
         delivery_date     datetime64[ns]
         duration         timedelta64[ns]
         dtype: object

我们可以从 Timestamp 对象中添加或减去 Timedelta。下一个示例从每行的持续时间中减去交货日期列。可预测的是,新 Series 中的值与订单日期列中的值相同:

In  [88] (deliveries["delivery_date"] - deliveries["duration"]).head()

Out [88] 0   1998-05-24
         1   1992-04-22
         2   1991-02-10
         3   1992-07-21
         4   1993-09-02
         dtype: datetime64[ns]

加号符号将 Timedelta 添加到 Timestamp。假设我们想找到每个包裹需要两倍时间到达的交货日期。我们可以将持续时间列中的 Timedelta 值添加到交货日期列中的 Timestamp 值:

In  [89] (deliveries["delivery_date"] + deliveries["duration"]).head()

Out [89] 0   1999-10-20
         1   2004-01-18
         2   1994-03-12
         3   2003-03-22
         4   2003-03-18
         dtype: datetime64[ns]

sort_values 方法与 Timedelta Series 一起工作。下一个示例按升序对持续时间列进行排序,从最短的交货到最长的交货:

In  [90] deliveries.sort_values("duration")

Out [90]

 **order_date delivery_date  duration**
454 1990-05-24    1990-06-01    8 days
294 1994-08-11    1994-08-20    9 days
10  1998-05-10    1998-05-19    9 days
499 1993-06-03    1993-06-13   10 days
143 1997-09-20    1997-10-06   16 days
...        ...           ...       ...
152 1990-09-18    1999-12-19 3379 days
62  1990-04-02    1999-08-16 3423 days
458 1990-02-13    1999-11-15 3562 days
145 1990-03-07    1999-12-25 3580 days
448 1990-01-20    1999-11-12 3583 days

501 rows × 3 columns

数学方法也适用于 Timedelta Series。接下来的几个示例突出了我们在本书中使用的三种方法:max 用于最大值,min 用于最小值,mean 用于平均值:

In  [91] deliveries["duration"].max()

Out [91] Timedelta('3583 days 00:00:00')

In  [92] deliveries["duration"].min()

Out [92] Timedelta('8 days 00:00:00')

In  [93] deliveries["duration"].mean()

Out [93] Timedelta('1217 days 22:53:53.532934')

这是下一个挑战。让我们过滤 DataFrame,以找到交货时间超过一年的包裹。我们可以使用大于符号 (>) 来比较每个持续时间列的值与固定持续时间。我们可以将时间长度指定为 Timedelta 或字符串。下一个示例使用 "365 days"

In  [94] # The two lines below are equivalent
         (deliveries["duration"] > pd.Timedelta(days = 365)).head()
         (deliveries["duration"] > "365 days").head()

Out [94] 0      False
         1       True
         2       True
         3       True
         4       True
         Name: Delivery Time, dtype: bool

让我们使用布尔 Series 过滤出交货时间超过 365 天的 deliveries 行:

In  [95] deliveries[deliveries["duration"] > "365 days"].head()

Out [95]

 **order_date delivery_date  duration**
1 1992-04-22    1998-03-06 2144 days
2 1991-02-10    1992-08-26  563 days
3 1992-07-21    1997-11-20 1948 days
4 1993-09-02    1998-06-10 1742 days
6 1990-01-25    1994-10-02 1711 days

我们可以根据需要将比较持续时间细化。下一个示例包括字符串中的天数、小时和分钟,用逗号分隔时间单位:

In  [96] long_time = (
             deliveries["duration"] > "2000 days, 8 hours, 4 minutes"
         )

         deliveries[long_time].head()

Out [96]

 ** order_date delivery_date  duration**
1  1992-04-22    1998-03-06 2144 days
7  1992-02-23    1998-12-30 2502 days
11 1992-10-17    1998-10-06 2180 days
12 1992-05-30    1999-08-15 2633 days
15 1990-01-20    1998-07-24 3107 days

作为提醒,Pandas 可以对 Timedelta 列进行排序。要发现最长或最短时长,我们可以在时长 Series 上调用 sort_values 方法。

11.8 编程挑战

这是练习本章引入的概念的机会。

11.8.1 问题

Citi Bike NYC 是纽约市的官方自行车共享计划。居民和游客可以在城市数百个地点取车和还车。骑行数据是公开的,由市政府每月发布,网址为 www.citibikenyc.com/system-data。citibike.csv 是 2020 年 6 月骑行的约 190 万次骑行数据的集合。为了简化,数据集已从原始版本修改,仅包括两个列:每次骑行的开始时间和结束时间。让我们导入数据集并将其分配给 citi_bike 变量:

In  [97] citi_bike = pd.read_csv("citibike.csv")
         citi_bike.head()

Out [97]
 ** start_time                 stop_time**
0  2020-06-01 00:00:03.3720  2020-06-01 00:17:46.2080
1  2020-06-01 00:00:03.5530  2020-06-01 01:03:33.9360
2  2020-06-01 00:00:09.6140  2020-06-01 00:17:06.8330
3  2020-06-01 00:00:12.1780  2020-06-01 00:03:58.8640
4  2020-06-01 00:00:21.2550  2020-06-01 00:24:18.9650

start_time 和 stop_time 列中的 datetime 条目包括年、月、日、时、分、秒和微秒。(微秒是等于一百万分之一秒的时间单位。)

我们可以使用 info 方法打印一个摘要,包括 DataFrame 的长度、列的数据类型和内存使用情况。注意,pandas 已经将两个列的值作为字符串导入:

In  [98] citi_bike.info()

Out [98]

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1882273 entries, 0 to 1882272
Data columns (total 2 columns):
 #   Column      Dtype
---  ------      -----
 0   start_time  object
 1   stop_time   object
dtypes: object(2)
memory usage: 28.7+ MB

这是练习本章引入的概念的机会。

  1. 将 start_time 和 stop_time 列转换为存储 datetime (Timestamp) 值而不是字符串。

  2. 计算每周每天(周一、周二等)发生的骑行次数。哪一天是骑行最受欢迎的工作日?以 start_time 列作为起点。

  3. 计算每月每周内每周的骑行次数。为此,将 start_time 列中的每个日期四舍五入到其前一个或当前的周一。假设每周从周一开始,周日结束。因此,六月的第一个星期是 6 月 1 日星期一到 6 月 7 日星期日。

  4. 计算每次骑行的时长,并将结果保存到一个新的时长列中。

  5. 查找一次骑行的平均时长。

  6. 从数据集中提取按时长排序的前五次最长骑行。

11.8.2 解决方案

让我们逐个解决这些问题:

  1. pandas 顶层中的 to_datetime 转换函数可以很好地将 start_time 和 end_time 列的值转换为 Timestamps。下面的代码示例使用 for 循环遍历列名列表,将每个列传递给 to_datetime 函数,并用新的 datetime Series 覆盖现有的字符串列:

    In  [99] for column in ["start_time", "stop_time"]:
                 citi_bike[column] = pd.to_datetime(citi_bike[column])
    

    让我们再次调用 info 方法来确认这两个列存储的是 datetime 值:

    In  [100] citi_bike.info()
    
    Out [100]
    
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 1882273 entries, 0 to 1882272
    Data columns (total 2 columns):
     #   Column      Dtype
    ---  ------      -----
     0   start_time  datetime64[ns]
     1   stop_time   datetime64[ns]
    dtypes: datetime64ns
    memory usage: 28.7 MB
    
  2. 我们需要分两步来计算每周的骑行次数。首先,我们从 start_time 列中的每个 datetime 提取星期几;然后计算星期几的出现次数。dt.day_name 方法返回一个包含每个日期星期几名称的 Series

    In  [101] citi_bike["start_time"].dt.day_name().head()
    
    Out [101] 0    Monday
              1    Monday
              2    Monday
              3    Monday
              4    Monday
              Name: start_time, dtype: object
    

    然后,我们可以在返回的 Series 上调用可靠的 value_counts 方法来计算工作日。在 2020 年 6 月,星期二是最受欢迎的骑行日:

    In  [102] citi_bike["start_time"].dt.day_name().value_counts()
    
    Out [102] Tuesday      305833
              Sunday       301482
              Monday       292690
              Saturday     285966
              Friday       258479
              Wednesday    222647
              Thursday     215176
              Name: start_time, dtype: int64
    
  3. 下一个挑战要求我们将每个日期分组到其对应的周桶中。我们可以通过将日期四舍五入到其前一个或当前星期一来实现。这里有一个巧妙的解决方案:我们可以使用 dayofweek 属性来返回一个数字 Series0 表示星期一,1 表示星期二,6 表示星期日,依此类推:

    In  [103] citi_bike["start_time"].dt.dayofweek.head()
    
    Out [103] 0    0
              1    0
              2    0
              3    0
              4    0
                 Name: start_time, dtype: int64
    

    星期几的数字也代表从最近的星期一到当前日期的天数距离。例如,6 月 1 日的星期一有 dayofweek 值为 0。该日期距离最近的星期一有 0 天。同样,6 月 2 日的星期二有 dayofweek 值为 1。该日期距离最近的星期一(6 月 1 日)有 1 天。让我们将这个 Series 保存到 days_away_from_monday 变量中:

    In  [104] days_away_from_monday = citi_bike["start_time"].dt.dayofweek
    

    如果我们从日期中减去 dayofweek 值,我们将有效地将每个日期四舍五入到其前一个星期一。我们可以将 dayofweek Series 传递给 to_timedelta 函数以将其转换为时长 Series。我们将传递一个设置为 "day" 的单位参数,告诉 pandas 将数值视为天数:

    In  [105] citi_bike["start_time"] - pd.to_timedelta(
                  days_away_from_monday, unit = "day"
              )
    
    Out [105] 0         2020-06-01 00:00:03.372
              1         2020-06-01 00:00:03.553
              2         2020-06-01 00:00:09.614
              3         2020-06-01 00:00:12.178
              4         2020-06-01 00:00:21.255
                                  ...
              1882268   2020-06-29 23:59:41.116
              1882269   2020-06-29 23:59:46.426
              1882270   2020-06-29 23:59:47.477
              1882271   2020-06-29 23:59:53.395
              1882272   2020-06-29 23:59:53.901
              Name: start_time, Length: 1882273, dtype: datetime64[ns]
    

    让我们将新的 Series 保存到 dates_rounded_to_monday 变量中:

    In  [106] dates_rounded_to_monday = citi_bike[
                  "start_time"
              ] - pd.to_timedelta(days_away_from_monday, unit = "day")
    

    我们已经完成了一半。我们已经将日期四舍五入到正确的星期一,但 value_counts 方法还不能使用。日期之间的时间差异会导致 pandas 认为它们不相等:

    In  [107] dates_rounded_to_monday.value_counts().head()
    
    Out [107] 2020-06-22 20:13:36.208    3
              2020-06-08 17:17:26.335    3
              2020-06-08 16:50:44.596    3
              2020-06-15 19:24:26.737    3
              2020-06-08 19:49:21.686    3
              Name: start_time, dtype: int64
    

    让我们使用 dt.date 属性来返回一个包含每个 datetime 的 Series

    In  [108] dates_rounded_to_monday.dt.date.head()
    
    Out [108] 0    2020-06-01
              1    2020-06-01
              2    2020-06-01
              3    2020-06-01
              4    2020-06-01
              Name: start_time, dtype: object
    

    现在我们已经隔离了日期,我们可以调用 value_counts 方法来计算每个值的出现次数。从 6 月 15 日星期一到 6 月 21 日星期日的这一周,是整个月自行车骑行次数最多的一周:

    In  [109] dates_rounded_to_monday.dt.date.value_counts()
    
    Out [109] 2020-06-15    481211
              2020-06-08    471384
              2020-06-22    465412
              2020-06-01    337590
              2020-06-29    126676
              Name: start_time, dtype: int64
    
  4. 要计算每段骑行的时长,我们可以从停止时间列减去开始时间列。Pandas 将返回一个 Timedeltas 的 Series。我们需要将这个 Series 保存到下一个示例中,所以让我们将其附加到 DataFrame 作为一个名为 duration 的新列:

    In  [110] citi_bike["duration"] = (
                  citi_bike["stop_time"] - citi_bike["start_time"]
              )
    
              citi_bike.head()
    
    Out [110]
    
     ** start_time               stop_time               duration**
    0 2020-06-01 00:00:03.372 2020-06-01 00:17:46.208 0 days 00:17:42.836000
    1 2020-06-01 00:00:03.553 2020-06-01 01:03:33.936 0 days 01:03:30.383000
    2 2020-06-01 00:00:09.614 2020-06-01 00:17:06.833 0 days 00:16:57.219000
    3 2020-06-01 00:00:12.178 2020-06-01 00:03:58.864 0 days 00:03:46.686000
    4 2020-06-01 00:00:21.255 2020-06-01 00:24:18.965 0 days 00:23:57.710000
    

    注意,如果列存储的是字符串,之前的减法操作将引发错误;这就是为什么在转换它们为日期时间之前,这是强制性的。

  5. 接下来,我们必须找出所有自行车骑行平均时长。这个过程很简单:我们可以在新的时长列上调用 mean 方法进行计算。平均骑行时长为 27 分钟和 19 秒:

    In  [111] citi_bike["duration"].mean()
    
    Out [111] Timedelta('0 days 00:27:19.590506853')
    
  6. 最后一个问题要求识别数据集中最长的五段自行车骑行。一个解决方案是使用 sort_values 方法按降序排序时长列的值,然后使用 head 方法查看前五行。这些会话可能属于那些在完成骑行后忘记检查自行车的人:

    In  [112] citi_bike["duration"].sort_values(ascending = False).head()
    
    Out [112] 50593    32 days 15:01:54.940000
              98339    31 days 01:47:20.632000
              52306    30 days 19:32:20.696000
              15171    30 days 04:26:48.424000
              149761   28 days 09:24:50.696000
              Name: duration, dtype: timedelta64[ns]
    

    另一个选项是 nlargest 方法。我们可以在时长 Series 或整个 DataFrame 上调用此方法。让我们选择后者:

    In  [113] citi_bike.nlargest(n = 5, columns = "duration")
    
    Out [113]
    
     ** start_time              stop_time               duration**
    50593  2020-06-01 21:30:17... 2020-07-04 12:32:12... 32 days 15:01:54.94...
    98339  2020-06-02 19:41:39... 2020-07-03 21:29:00... 31 days 01:47:20.63...
    52306  2020-06-01 22:17:10... 2020-07-02 17:49:31... 30 days 19:32:20.69...
    15171  2020-06-01 13:01:41... 2020-07-01 17:28:30... 30 days 04:26:48.42...
    149761 2020-06-04 14:36:53... 2020-07-03 00:01:44... 28 days 09:24:50.69...
    

这就是数据集中最长的五次骑行记录。恭喜您完成编码挑战!

摘要

  • Pandas 的 Timestamp 对象是一个灵活、强大的替代品,用于 Python 的原生 datetime 对象。

  • 在 datetime Series 上的 dt 访问器揭示了一个具有属性和方法以提取日期、月份、星期名称等属性的 DatetimeProperties 对象。

  • Timedelta 对象表示一个持续时间。

  • 当我们从两个 Timestamp 对象中减去时,Pandas 创建一个 Timedelta 对象。

  • pd.offsets 包中的偏移量动态地将日期四舍五入到最近的周、月、季度等。我们可以用加号向前四舍五入,用减号向后四舍五入。

  • DatetimeIndexTimestamp 值的容器。我们可以将其作为索引或列添加到 Pandas 数据结构中。

  • TimedeltaIndexTimedelta 对象的容器。

  • 最高级的 to_datetime 函数将值的可迭代序列转换为 TimestampDatetimeIndex

12 导入和导出

本章涵盖

  • 导入 JSON 数据

  • 展平嵌套的记录集合

  • 从在线网站下载 CSV 文件

  • 从 Excel 工作簿中读取和写入

数据集以各种文件格式存在:逗号分隔值(CSV)、制表符分隔值(TSV)、Excel 工作簿(XLSX)等。一些数据格式不存储在表格格式中;相反,它们在键值存储中嵌套相关数据的集合。考虑以下两个例子。图 12.1 以表格形式存储数据,而图 12.2 以 Python 字典的形式存储相同的数据。

图 12.1 奥斯卡获奖者表格

Python 的字典是一个键值数据结构的例子:

{
    2000: [
        {
            "Award": "Best Actor",
            "Winner": "Russell Crowe"
        },
        {
            "Award": "Best Actress",
            "Winner": "Julia Roberts"
        }
    ],
    2001: [
        {
            "Award": "Best Actor",
            "Winner": "Denzel Washington"
        },
        {
            "Award": "Best Actress",
            "Winner": "Halle Berry"
        }
    ]
}

图 12.2 具有相同数据的 Python 字典(键值存储)

Pandas 附带用于将键值数据转换为表格数据及其相反操作的实用函数。当我们有DataFrame中的数据时,我们可以应用所有我们喜欢的技巧。但将数据扭曲成正确的形状通常证明是分析中最具挑战性的部分。在本章中,我们将学习如何解决数据导入中的常见问题。我们还将探索等式的另一面:将DataFrame导出为各种文件类型和数据结构。

12.1 从 JSON 文件中读取和写入

让我们从谈论 JSON 开始,可能是今天最受欢迎的键值存储格式。JavaScript 对象表示法(JSON)是一种用于存储和传输文本数据的格式。尽管 JavaScript 编程语言启发了其语法,但 JSON 本身是语言无关的。今天的大多数语言,包括 Python,都可以生成和解析 JSON。

一个 JSON 响应由键值对组成,其中键作为值的唯一标识符。冒号符号(:)将键与值连接起来:

"name":"Harry Potter"

键必须是字符串。值可以是任何数据类型,包括字符串、数字和布尔值。JSON 类似于 Python 的字典对象。

JSON 是许多现代应用程序编程接口(API)的流行响应格式,例如网站服务器。API 的原始 JSON 响应看起来像是一个普通的字符串。以下是一个响应可能的样子:

{"name":"Harry Potter","age":17,"wizard":true}

被称为linters的软件程序通过将每个键值对放在单独的一行上格式化 JSON 响应。一个流行的例子是 JSONLint (jsonlint.com)。将之前的 JSON 通过 JSONLint 运行会产生以下输出:

{
            "name": "Harry Potter",
            "age": 17,
            "wizard": true,
}

这两个先前的代码样本之间没有技术差异,但后者更易读。

JSON 响应包含三个键值对:

  • "name"键的字符串值为"Harry Potter"

  • "age"键的整数值为 17。

  • "wizard"键的布尔值为true。在 JSON 中,布尔值以小写形式书写。这个概念与 Python 布尔值相同。

键也可以指向一个数组,这是一个有序的元素集合,相当于 Python 列表。以下 JSON 示例中的"friends"键映射到一个包含两个字符串的数组:

{
    "name": "Harry Potter",
            age": 17,
    "wizard": true,
    "friends": ["Ron Weasley", "Hermione Granger"],
}

JSON 可以在嵌套对象中存储额外的键值对,例如以下示例中的"address"。用 Python 的说法,我们可以将"address"视为嵌套在另一个字典中的字典:

{
    "name": "Harry Potter",
    "age": 17,
    "wizard": true,    
    "friends": ["Ron Weasley", "Hermione Granger"],
    "address": {
        "street": "4 Privet Drive",
        "town": "Little Whinging"
    }
}

嵌套的键值对存储有助于通过分组相关字段来简化数据。

12.1.1 将 JSON 文件加载到 DataFrame 中

让我们创建一个新的 Jupyter Notebook 并导入 pandas 库。确保在包含本章数据文件的同一目录中创建 Notebook:

In  [1] import pandas as pd

JSON 可以存储在一个以.json 扩展名的纯文本文件中。本章的 prizes.json 文件是从诺贝尔奖 API 保存的 JSON 响应。API 存储了从 1901 年以来的诺贝尔奖获得者。您可以通过在网页浏览器中导航到api.nobelprize.org/v1/prize.json来查看原始 JSON 响应。以下是 JSON 形状的预览:

{
  "prizes": [
    {
      "year": "2019",
      "category": "chemistry",
      "laureates": [
        {
          "id": "976",
          "firstname": "John",
          "surname": "Goodenough",
          "motivation": "\"for the development of lithium-ion batteries\"",
          "share": "3"
        },
        {
          "id": "977",
          "firstname": "M. Stanley",
          "surname": "Whittingham",
          "motivation": "\"for the development of lithium-ion batteries\"",
          "share": "3"
        },
        {
          "id": "978",
          "firstname": "Akira",
          "surname": "Yoshino",
          "motivation": "\"for the development of lithium-ion batteries\"",
          "share": "3"
        }
      ]
    },

JSON 包含一个顶层prizes键,它映射到一个字典数组,每个字典对应于年份和类别的组合(例如"chemistry""physics""literature"等)。对于所有获奖者,"year""category"键都存在,而"laureates""overallMotivation"键只存在于某些获奖者中。以下是一个包含"overallMotivation"键的示例字典:

{
    year: "1972",
    category: "peace",
    overallMotivation: "No Nobel Prize was awarded this year. The prize
    money for 1972 was allocated to the Main Fund."
}

"laureates"键连接到一个字典数组,每个字典都有自己的"id""firstname""surname""motivation""share"键。"laureates"键存储一个数组,以容纳在相同类别中获得诺贝尔奖的多个年份。即使某一年只有一个获奖者,"laureates"键也会使用列表。以下是一个示例:

{
    year: "2019",
    category: "literature",
    laureates: [
        {
             id: "980",
             firstname: "Peter",
             surname: "Handke",
             motivation: "for an influential work that with linguistic
             ingenuity has explored the periphery and the specificity of
             human experience",
             share: "1"
        }
    ]
},

pandas 中的导入函数有一个一致的命名方案;每个函数都由一个read前缀后跟文件类型组成。例如,我们已经在整本书中多次使用了read_csv函数。要导入 JSON 文件,我们将使用互补的read_json函数。它的第一个参数是文件路径。以下示例传递了 nobel.json 文件。Pandas 返回一个包含奖项列的单列DataFrame

In  [2] nobel = pd.read_json("nobel.json")
        nobel.head()

Out [2]

 **prizes**
0  {'year': '2019', 'category': 'chemistry', 'laureates': [{'id': '97...
1  {'year': '2019', 'category': 'economics', 'laureates': [{'id': '98...
2  {'year': '2019', 'category': 'literature', 'laureates': [{'id': '9...
3  {'year': '2019', 'category': 'peace', 'laureates': [{'id': '981', ...
4  {'year': '2019', 'category': 'physics', 'overallMotivation': '"for...

我们已经成功地将文件导入到 pandas 中,但不幸的是,它不是分析的理想格式。Pandas 将 JSON 的顶层prizes键设置为列名,并为从 JSON 中解析的每个键值对创建了一个 Python 字典。以下是一个示例行值:

In  [3] nobel.loc[2, "prizes"]

Out [3] {'year': '2019',
         'category': 'literature',
         'laureates': [{'id': '980',
           'firstname': 'Peter',
           'surname': 'Handke',
           'motivation': '"for an influential work that with linguistic
             ingenuity has explored the periphery and the specificity of
             human experience"',
           'share': '1'}]}

以下示例将行值传递给 Python 的内置type函数。我们确实有一个字典的Series

In  [4] type(nobel.loc[2, "prizes"])

Out [4] dict

我们的目标是将数据转换为表格格式。为此,我们需要提取 JSON 的顶级键值对(年份,类别)到单独的 DataFrame 列。我们还需要遍历 "laureates" 列表中的每个字典并提取其嵌套信息。我们的目标是每个诺贝尔奖获得者一行,与他们所在的年份和类别相关联。我们追求的 DataFrame 看起来像这样:

 **id   firstname      surname           motivation share  year   category**
0  976        John   Goodenough  "for the develop...     3  2019  chemistry
1  977  M. Stanley  Whittingham  "for the develop...     3  2019  chemistry
2  978       Akira      Yoshino  "for the develop...     3  2019  chemistry

将嵌套数据记录移动到单个一维列表的过程称为 展平归一化。pandas 库包含一个内置的 json_normalize 函数来处理繁重的工作。让我们在一个小例子上尝试它:来自 nobel DataFrame 的一个样本字典。我们将使用 loc 访问器来访问第一行的字典并将其分配给一个 chemistry_2019 变量:

In  [5] chemistry_2019 = nobel.loc[0, "prizes"]
        chemistry_2019

Out [5] {'year': '2019',
         'category': 'chemistry',
         'laureates': [{'id': '976',
           'firstname': 'John',
           'surname': 'Goodenough',
           'motivation': '"for the development of lithium-ion batteries"',
           'share': '3'},
          {'id': '977',
           'firstname': 'M. Stanley',
           'surname': 'Whittingham',
           'motivation': '"for the development of lithium-ion batteries"',
           'share': '3'},
          {'id': '978',
           'firstname': 'Akira',
           'surname': 'Yoshino',
           'motivation': '"for the development of lithium-ion batteries"',
           'share': '3'}]}

让我们将 chemistry_2019 字典传递给 json_normalize 函数的 data 参数。好消息是 pandas 提取了三个顶级字典键 ("year", "category", 和 "laureates") 并将它们作为新 DataFrame 中的单独列。不幸的是,库仍然保留了 "laureates" 列表中的嵌套字典。最终,我们希望将数据存储在单独的列中。

In  [6] pd.json_normalize(data = chemistry_2019)

Out [6]

 ** year   category                                          laureates**
0  2019  chemistry  [{'id': '976', 'firstname': 'John', 'surname':...

我们可以使用 json_normalize 函数的 record_path 参数来归一化嵌套的 "laureates" 记录。我们将传递一个表示字典中哪个键持有嵌套记录的字符串。让我们传递 "laureates"

In  [7] pd.json_normalize(data = chemistry_2019, record_path = "laureates")

Out [7]

 **id   firstname      surname                     motivation share**
0  976        John   Goodenough  "for the development of li...     3
1  977  M. Stanley  Whittingham  "for the development of li...     3
2  978       Akira      Yoshino  "for the development of li...     3

前进一步,退一步。Pandas 将嵌套的 "laureates" 字典扩展到新的列中,但现在我们失去了原始的年份和类别列。为了保留这些顶级键值对,我们可以将它们的名称列表传递给一个名为 meta 的参数:

In  [8] pd.json_normalize(
            data = chemistry_2019,
            record_path = "laureates",
            meta = ["year", "category"],
        )

Out [8]

 **id   firstname      surname           motivation share  year   category**
0  976        John   Goodenough  "for the develop...     3  2019  chemistry
1  977  M. Stanley  Whittingham  "for the develop...     3  2019  chemistry
2  978       Akira      Yoshino  "for the develop...     3  2019  chemistry

这正是我们想要的 DataFrame。我们的归一化策略已经成功应用于奖项列中的单个字典。幸运的是,json_normalize 函数足够智能,可以接受字典的 Series 并为每个条目重复提取逻辑。让我们看看当我们传递 prizes Series 时会发生什么:

In  [9] pd.json_normalize(
            data = nobel["prizes"],
            record_path = "laureates",
            meta = ["year", "category"]
        )

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-49-e09a24c19e5b> in <module>
      2     data = nobel["prizes"],
      3     record_path = "laureates",
----> 4     meta = ["year", "category"]
      5 )

KeyError: 'laureates'

不幸的是,Pandas 抛出了一个 KeyError 异常。奖项 Series 中的一些字典没有 "laureates" 键。json_normalize 函数无法从一个不存在的列表中提取嵌套的获奖者信息。我们可以通过识别缺少 "laureates" 键的字典并手动分配它们键值来解决这个问题。在这些情况下,我们可以为 "laureates" 键提供一个空列表的值。

让我们花点时间回顾一下 Python 字典中的 setdefault 方法。考虑以下字典:

In  [10] cheese_consumption = {
             "France": 57.9,
             "Germany": 53.2,
             "Luxembourg": 53.2
         }

setdefault 方法将键值对分配给字典,但只有当字典中没有该键时。如果键已存在,该方法返回其现有值。该方法的第一参数是键,第二参数是值。

以下示例尝试将键 "France" 添加到 cheese_consumption 字典中,其值为 100。键存在,因此没有变化。Python 保留了原始值 57.9

In  [11] cheese_consumption.setdefault("France", 100)

Out [11] 57.9

In  [12] cheese_consumption["France"]

Out [12] 57.9

与之相比,下一个示例使用 "Italy" 参数调用 setdefault。字典中不存在 "Italy" 键,因此 Python 添加它并将其赋值为 48

In  [13] cheese_consumption.setdefault("Italy", 48)

Out [13] 48

In  [14] cheese_consumption

Out [14] {'France': 57.9, 'Germany': 53.2, 'Luxembourg': 53.2, 'Italy': 48}

让我们将此技术应用于奖项中的每个嵌套字典。如果一个字典没有 laureates 键,我们将使用 setdefault 方法添加键,其值为空列表。提醒一下,我们可以使用 apply 方法逐个遍历每个 Series 元素。该方法在第三章中引入,接受一个函数作为参数,并将每个 Series 行按顺序传递给该函数。下一个示例定义了一个 add_laureates_key 函数来更新单个字典,然后将该函数作为参数传递给 apply 方法:

In  [15] def add_laureates_key(entry):
             entry.setdefault("laureates", [])

         nobel["prizes"].apply(add_laureates_key)

Out [15] 0      [{'id': '976', 'firstname': 'John', 'surname':...
         1      [{'id': '982', 'firstname': 'Abhijit', 'surnam...
         2      [{'id': '980', 'firstname': 'Peter', 'surname'...
         3      [{'id': '981', 'firstname': 'Abiy', 'surname':...
         4      [{'id': '973', 'firstname': 'James', 'surname'...
                                      ...
         641    [{'id': '160', 'firstname': 'Jacobus H.', 'sur...
         642    [{'id': '569', 'firstname': 'Sully', 'surname'...
         643    [{'id': '462', 'firstname': 'Henry', 'surname'...
         644    [{'id': '1', 'firstname': 'Wilhelm Conrad', 's...
         645    [{'id': '293', 'firstname': 'Emil', 'surname':...
         Name: prizes, Length: 646, dtype: object

setdefault 方法会修改奖项中的字典,因此不需要覆盖原始 Series

现在所有嵌套字典都有一个 laureates 键,我们可以重新调用 json_normalize 函数。再次,我们将包含我们希望保留的两个顶级字典键的列表传递给 meta 参数。我们还将使用 record_path 来指定具有嵌套记录列表的顶级属性:

In  [16] winners = pd.json_normalize(
             data = nobel["prizes"],
             record_path = "laureates",
             meta = ["year", "category"]
         )

         winners

Out [16]

 **id     firstname      surname      motivation share  year    category**
0    976          John   Goodenough  "for the de...     3  2019   chemistry
1    977    M. Stanley  Whittingham  "for the de...     3  2019   chemistry
2    978         Akira      Yoshino  "for the de...     3  2019   chemistry
3    982       Abhijit     Banerjee  "for their ...     3  2019   economics
4    983        Esther        Duflo  "for their ...     3  2019   economics
...  ...             ...        ...             ...    ...  ...         ...
945  569         Sully    Prudhomme  "in special...     1  1901  literature
946  462         Henry       Dunant  "for his hu...     2  1901       peace
947  463      Frédéric        Passy  "for his li...     2  1901       peace
948    1  Wilhelm Con...    Röntgen  "in recogni...     1  1901     physics
949  293          Emil  von Behring  "for his wo...     1  1901    medicine

950 rows × 7 columns

成功!我们已经将 JSON 数据标准化,转换为表格格式,并将其存储在一个二维的 DataFrame 中。

12.1.2 将 DataFrame 导出为 JSON 文件

现在我们尝试逆向过程:将 DataFrame 转换为 JSON 表示形式并将其写入 JSON 文件。to_json 方法从 pandas 数据结构创建 JSON 字符串;其 orient 参数自定义 pandas 返回数据时的格式。下一个示例使用 "records" 参数来返回一个键值对象的 JSON 数组。Pandas 将列名存储为字典键,这些键指向行的相应值。以下是一个示例,展示了 winners 的前两行,这是我们在 12.1.1 节中创建的 DataFrame

In  [17] winners.head(2)

Out [17]

 **id   firstname      surname           motivation share  year   category**
0  976        John   Goodenough  "for the develop...     3  2019  chemistry
1  977  M. Stanley  Whittingham  "for the develop...     3  2019  chemistry

In  [18] winners.head(2).to_json(orient = "records")

Out [18]
  '[{"id":"976","firstname":"John","surname":"Goodenough","motivation":"\\
    "for the development of lithium-ion batteries\\"","share":"3","year":"2019","category":"chemistry"},
    {"id":"977","firstname":"M. Stanley","surname":"Whittingham","motivation":"\\"
    for the development of lithium-ion batteries\\"","share":"3","year":"2019","category":"chemistry"}]'

与之相比,我们可以传递一个 "split" 参数来返回一个包含单独的 columnsindexdata 键的字典。此选项防止了每行条目中列名的重复:

In  [19] winners.head(2).to_json(orient = "split")

Out [19]

'{"columns":["id","firstname","surname","motivation","share","year","category"],
    "index":[0,1],"data":[["976","John","Goodenough","\\"for the development of 
    lithium-ion batteries\\"","3","2019","chemistry"],["977","M. Stanley","Whittingham",
    "\\"for the development of lithium-ion batteries\\"","3","2019","chemistry"]]}'

orient 参数的其他可用参数包括 "index""columns""values""table"

当 JSON 格式符合你的预期时,将 JSON 文件名作为 to_json 方法的第一个参数传递。Pandas 将字符串写入与 Jupyter Notebook 相同目录的 JSON 文件中:

In  [20] winners.to_json("winners.json", orient = "records")

警告:执行相同的单元格两次时要小心。如果在目录中存在 winners.json 文件,pandas 将在执行上一个单元格时覆盖它。库不会警告我们正在替换文件。因此,我强烈建议为输出文件和输入文件使用不同的名称。

12.2 从 CSV 文件读取和写入

我们下一个数据集是纽约市婴儿名字的集合。每一行包括名字、出生年份、性别、种族、计数和流行排名。CSV 文件托管在纽约市政府网站上,可在mng.bz/MgzQ找到。

我们可以在我们的网络浏览器中访问该网站,并将 CSV 文件下载到我们的计算机上进行本地存储。作为替代方案,我们可以将 URL 作为read_csv函数的第一个参数传递。Pandas 将自动获取数据集并将其导入到DataFrame中。硬编码的 URL 在处理实时数据且数据频繁变化时非常有用,因为它们可以节省我们每次重新运行分析时手动下载数据集的工作:

In  [21] url = "https://data.cityofnewyork.us/api/views/25th-nujf/rows.csv"
         baby_names = pd.read_csv(url)
         baby_names.head()

Out [21]

 ** Year of Birth  Gender Ethnicity Child's First Name  Count  Rank**
0           2011  FEMALE  HISPANIC          GERALDINE     13    75
1           2011  FEMALE  HISPANIC                GIA     21    67
2           2011  FEMALE  HISPANIC             GIANNA     49    42
3           2011  FEMALE  HISPANIC            GISELLE     38    51
4           2011  FEMALE  HISPANIC              GRACE     36    53

注意,如果链接无效,pandas 将引发 HTTPError 异常。

让我们尝试使用to_csv方法将 baby_names DataFrame写入一个普通的 CSV 文件。如果没有提供参数,该方法将直接在 Jupyter Notebook 中输出 CSV 字符串。遵循 CSV 约定,pandas 使用换行符分隔行,使用逗号分隔行值。作为提醒,Python 中\n字符标记行断。以下是方法输出前十个行的预览:

In  [22] baby_names.head(10).to_csv()

Out [22]

",Year of Birth,Gender,Ethnicity,Child's First Name,Count,Rank\n0,2011,FEMALE,HISPANIC,
    GERALDINE,13,75\n1,2011,FEMALE,HISPANIC,GIA,21,67\n2,2011,FEMALE,HISPANIC,
    GIANNA,49,42\n3,2011,FEMALE,HISPANIC,
    GISELLE,38,51\n4,2011,FEMALE,HISPANIC,GRACE,36,53\n5,2011,FEMALE,HISPANIC,
    GUADALUPE,26,62\n6,2011,FEMALE,HISPANIC,
    HAILEY,126,8\n7,2011,FEMALE,HISPANIC,HALEY,14,74\n8,2011,FEMALE,HISPANIC,
    HANNAH,17,71\n9,2011,FEMALE,HISPANIC,HAYLEE,17,71\n"

默认情况下,pandas 将DataFrame索引包含在 CSV 字符串中。注意字符串开头的逗号和每个\n符号后面的数值(0、1、2 等等)。图 12.3 突出了to_csv方法输出中的逗号。

图 12.3 CSV 输出,箭头突出显示索引标签

我们可以通过将index参数的参数设置为False来排除索引:

In  [23] baby_names.head(10).to_csv(index = False)

Out [23]

"Year of Birth,Gender,Ethnicity,Child's First Name,Count,Rank\n2011,FEMALE,HISPANIC,
    GERALDINE,13,75\n2011,FEMALE,HISPANIC,
    GIA,21,67\n2011,FEMALE,HISPANIC,GIANNA,49,42\n2011,FEMALE,HISPANIC,
    GISELLE,38,51\n2011,FEMALE,HISPANIC,
    GRACE,36,53\n2011,FEMALE,HISPANIC,
    GUADALUPE,26,62\n2011,FEMALE,HISPANIC,
    HAILEY,126,8\n2011,FEMALE,HISPANIC,
    HALEY,14,74\n2011,FEMALE,HISPANIC,
    HANNAH,17,71\n2011,FEMALE,HISPANIC,
    HAYLEE,17,71\n"

要将字符串写入 CSV 文件,我们可以将所需的文件名作为第一个参数传递给to_csv方法。确保在字符串中包含.csv 扩展名。如果我们不提供特定路径,pandas 将把文件写入与 Jupyter Notebook 相同的目录:

In  [24] baby_names.to_csv("NYC_Baby_Names.csv", index = False)

该方法在笔记本单元下方不产生输出。然而,如果我们切换回 Jupyter Notebook 导航界面,我们会看到 pandas 已创建了 CSV 文件。图 12.4 显示了保存的 NYC_Baby_Names.csv 文件。

图 12.4 NYC_Baby_Names.csv 文件已保存到与 Jupyter Notebook 相同的目录中

默认情况下,pandas 将所有DataFrame列写入 CSV 文件。我们可以通过传递一个列名列表到columns参数来选择要导出的列。下一个示例创建了一个只包含性别、孩子的名字和计数列的 CSV 文件:

In  [25] baby_names.to_csv(
             "NYC_Baby_Names.csv",
             index = False,
             columns = ["Gender", "Child's First Name", "Count"]
         )

请注意,如果目录中存在 NYC_Baby_Names.csv 文件,pandas 将覆盖现有文件。

12.3 从 Excel 工作簿读取和写入

Excel 是目前使用最广泛的电子表格应用程序。Pandas 使得从 Excel 工作簿和特定工作表中读取和写入变得容易。但首先,我们需要做一些整理工作以集成这两款软件。

12.3.1 在 Anaconda 环境中安装 xlrd 和 openpyxl 库

Pandas 需要xlrdopenpyxl库来与 Excel 交互。这些包是连接 Python 和 Excel 的粘合剂。

这里是关于在 Anaconda 环境中安装包的复习。要获取更深入的概述,请参阅附录 A。如果您已经在您的 Anaconda 环境中安装了这些库,请自由跳转到第 12.3.2 节。

  1. 启动终端(macOS)或 Anaconda Prompt(Windows)应用程序。

  2. 使用conda info --envs命令查看您的可用 Anaconda 环境:

    $ conda info --envs
    
    # conda environments:
    #
    base                  *  /opt/anaconda3
    pandas_in_action         /opt/anaconda3/envs/pandas_in_action
    
  3. 激活您想要安装库的 Anaconda 环境。附录 A 展示了如何为本书创建一个pandas_in_action环境。如果您选择了不同的环境名称,请在以下命令中将pandas_in_action替换为它:

    $ conda activate pandas_in_action
    
  4. 使用conda install命令安装xlrdopenpyxl库:

    (pandas_in_action) $ conda install xlrd openpyxl
    
  5. 当 Anaconda 列出所需的包依赖关系时,输入"Y"并按 Enter 键开始安装。

  6. 安装完成后,执行jupyter notebook以再次启动 Jupyter 服务器,并导航回该章节的 Jupyter Notebook。

    不要忘记执行顶部的import pandas as pd命令的单元格。

12.3.2 导入 Excel 工作簿

pandas 顶层中的read_excel函数将 Excel 工作簿导入到DataFrame中。其第一个参数io接受一个包含工作簿路径的字符串。确保在文件名中包含.xlsx 扩展名。默认情况下,pandas 将只导入工作簿中的第一个工作表。

单个工作表的 Excel 工作簿 Single Worksheet.xlsx 是一个很好的起点,因为它包含一个单独的数据工作表:

In  [26] pd.read_excel("Single Worksheet.xlsx")

Out [26]

 **First Name Last Name           City Gender**
0    Brandon     James          Miami      M
1       Sean   Hawkins         Denver      M
2       Judy       Day    Los Angeles      F
3     Ashley      Ruiz  San Francisco      F
4  Stephanie     Gomez       Portland      F

read_excel函数支持与read_csv相同的许多参数,包括index_col来设置索引列,usecols来选择列,以及squeeze将一列DataFrame强制转换为Series对象。下一个示例将 City 列设置为索引并仅保留数据集的四列中的三列。请注意,如果我们传递一个列到index_col参数,我们必须也在usecols列表中包含该列:

In  [27] pd.read_excel(
             io = "Single Worksheet.xlsx",
             usecols = ["City", "First Name", "Last Name"],
             index_col = "City"
         )

Out [27]

              First Name Last Name
**City** 
Miami            Brandon     James
Denver              Sean   Hawkins
Los Angeles         Judy       Day
San Francisco     Ashley      Ruiz
Portland       Stephanie     Gomez

当工作簿包含多个工作表时,复杂性略有增加。Multiple Worksheets.xlsx 工作簿包含三个工作表:Data 1、Data 2 和 Data 3。默认情况下,pandas 只导入工作簿中的第一个工作表:

In  [28] pd.read_excel("Multiple Worksheets.xlsx")

Out [28]

 **First Name Last Name           City Gender**
0    Brandon     James          Miami      M
1       Sean   Hawkins         Denver      M
2       Judy       Day    Los Angeles      F
3     Ashley      Ruiz  San Francisco      F
4  Stephanie     Gomez       Portland      F

在导入过程中,pandas 将每个工作表分配一个从 0 开始的索引位置。我们可以通过传递工作表的索引位置或其名称到sheet_name参数来导入特定的工作表。该参数的默认参数是0(第一个工作表)。因此,以下两个语句返回相同的DataFrame

In  [29] # The two lines below are equivalent
         pd.read_excel("Multiple Worksheets.xlsx", sheet_name = 0)
         pd.read_excel("Multiple Worksheets.xlsx", sheet_name = "Data 1")

Out [29]

 **First Name Last Name           City Gender**
0    Brandon     James          Miami      M
1       Sean   Hawkins         Denver      M
2       Judy       Day    Los Angeles      F
3     Ashley      Ruiz  San Francisco      F
4  Stephanie     Gomez       Portland      F

要导入所有工作表,我们可以将None作为参数传递给sheet_name。Pandas 将每个工作表存储在一个单独的DataFrame中。read_excel函数返回一个字典,其键是工作表名称,相应的值是DataFrames:

In  [30] workbook = pd.read_excel(
             "Multiple Worksheets.xlsx", sheet_name = None
         )

         workbook

Out [30] {'Data 1':   First Name Last Name           City Gender
          0    Brandon     James          Miami      M
          1       Sean   Hawkins         Denver      M
          2       Judy       Day    Los Angeles      F
          3     Ashley      Ruiz  San Francisco      F
          4  Stephanie     Gomez       Portland      F,
          'Data 2':   First Name Last Name           City Gender
          0     Parker     Power        Raleigh      F
          1    Preston  Prescott   Philadelphia      F
          2    Ronaldo   Donaldo         Bangor      M
          3      Megan   Stiller  San Francisco      M
          4     Bustin    Jieber         Austin      F,
          'Data 3':   First Name  Last Name     City Gender
          0     Robert     Miller  Seattle      M
          1       Tara     Garcia  Phoenix      F
          2    Raphael  Rodriguez  Orlando      M}

In  [31] type(workbook)

Out [31] dict

要访问DataFrame/工作表,我们通过字典中的一个键来访问。在这里,我们访问 Data 2 工作表的DataFrame

In  [32] workbook["Data 2"]

Out [32]

 **First Name Last Name           City Gender**
0     Parker     Power        Raleigh      F
1    Preston  Prescott   Philadelphia      F
2    Ronaldo   Donaldo         Bangor      M
3      Megan   Stiller  San Francisco      M
4     Bustin    Jieber         Austin      F

要指定要导入的工作表子集,我们可以将sheet_name参数传递为一个索引位置或工作表名称的列表。Pandas 仍然返回一个字典。该字典的键将与sheet_name列表中的字符串相匹配。下一个示例仅导入 Data 1 和 Data 3 工作表:

In  [33] pd.read_excel(
             "Multiple Worksheets.xlsx",
             sheet_name = ["Data 1", "Data 3"]
         )

Out [33] {'Data 1':   First Name Last Name           City Gender
          0    Brandon     James          Miami      M
          1       Sean   Hawkins         Denver      M
          2       Judy       Day    Los Angeles      F
          3     Ashley      Ruiz  San Francisco      F
          4  Stephanie     Gomez       Portland      F,
          'Data 3':   First Name  Last Name     City Gender
          0     Robert     Miller  Seattle      M
          1       Tara     Garcia  Phoenix      F
          2    Raphael  Rodriguez  Orlando      M}

下一个示例针对索引位置 1 和 2,或者等价地,第二和第三个工作表:

In  [34] pd.read_excel("Multiple Worksheets.xlsx", sheet_name = [1, 2])

Out [34] {1:   First Name Last Name           City Gender
          0     Parker     Power        Raleigh      F
          1    Preston  Prescott   Philadelphia      F
          2    Ronaldo   Donaldo         Bangor      M
          3      Megan   Stiller  San Francisco      M
          4     Bustin    Jieber         Austin      F,
          2:   First Name  Last Name     City Gender
          0     Robert     Miller  Seattle      M
          1       Tara     Garcia  Phoenix      F
          2    Raphael  Rodriguez  Orlando      M}

在我们导入DataFrame之后,我们可以自由地对其调用任何我们喜欢的操作。原始数据源对我们的可用操作没有影响。

12.3.3 导出 Excel 工作簿

让我们回到我们从纽约市下载的baby_names DataFrame。这是它的一个提醒:

In  [35] baby_names.head()

Out [35]

 ** Year of Birth  Gender Ethnicity Child's First Name  Count  Rank**
0           2011  FEMALE  HISPANIC          GERALDINE     13    75
1           2011  FEMALE  HISPANIC                GIA     21    67
2           2011  FEMALE  HISPANIC             GIANNA     49    42
3           2011  FEMALE  HISPANIC            GISELLE     38    51
4           2011  FEMALE  HISPANIC              GRACE     36    53

假设我们想要将数据集分成两个DataFrame,一个用于每个性别。然后我们希望将每个DataFrame写入一个新的 Excel 工作簿中的单独工作表中。我们可以从通过使用性别列中的值过滤baby_names DataFrame开始。第五章介绍了以下语法:

In  [36] girls = baby_names[baby_names["Gender"] == "FEMALE"]
         boys = baby_names[baby_names["Gender"] == "MALE"]

将数据写入 Excel 工作簿比写入 CSV 需要更多步骤。首先,我们需要创建一个ExcelWriter对象。该对象作为工作簿的基础。我们将在稍后将其附加到单独的工作表上。

ExcelWriter构造函数作为 pandas 库的一个顶级属性可用。它的第一个参数path接受新的工作簿文件名作为字符串。如果我们不提供一个目录的路径,pandas 将在 Jupyter Notebook 相同的目录中创建 Excel 文件。确保将ExcelWriter对象保存到变量中。以下示例使用excel_file

In  [37] excel_file = pd.ExcelWriter("Baby_Names.xlsx")
         excel_file

Out [37] <pandas.io.excel._openpyxl._OpenpyxlWriter at 0x118a7bf90>

接下来,我们需要将我们的女孩和男孩DataFrames 连接到工作簿中的单独工作表。让我们从前者开始。

DataFrame包含一个用于写入 Excel 工作簿的to_excel方法。该方法的第一参数excel_writer接受一个ExcelWriter对象,就像前面示例中创建的那样。该方法的sheet_name参数接受工作表名称作为字符串。最后,我们可以通过将index参数的值设置为False来排除DataFrame索引:

In  [38] girls.to_excel(
             excel_writer = excel_file, sheet_name = "Girls", index = False
         )

注意,我们还没有创建 Excel 工作簿。相反,我们在创建工作簿时将ExcelWriter对象连接到包括女孩DataFrame

接下来,让我们将我们的 boys DataFrame连接到 Excel 工作簿。我们将在 boys 上调用to_excel方法,将excel_writer参数传递给相同的ExcelWriter对象。现在 pandas 知道它应该将两个数据集写入同一个工作簿。让我们也修改sheet_name参数的字符串参数。为了只导出子集的列,我们将传递一个自定义列表到columns参数。下一个例子指示 pandas 在将 boys DataFrame写入工作簿中的“Boys”工作表时只包含 Child’s First Name,Count 和 Rank 列:

In  [39] boys.to_excel(
             excel_file,
             sheet_name = "Boys",
             index = False,
             columns = ["Child's First Name", "Count", "Rank"]
         )

现在我们已经配置了 Excel 工作簿的管道,我们可以将其写入磁盘。在excel_file ExcelWriter对象上调用save方法来完成这个过程:

In  [40] excel_file.save()

查看 Jupyter Notebook 界面以查看结果。图 12.5 显示了同一文件夹中的新 Baby_Names.xlsx 文件。

图 12.5 将 XLSX Excel 文件保存到与 Jupyter Notebook 相同的目录中

现在你已经知道了如何从 pandas 导出 JSON、CSV 和 XLSX 文件。该库提供了将数据结构导出到其他文件格式的附加功能。

12.4 编程挑战

让我们练习本章介绍的概念。tv_shows.json 文件是从 Episodate.com API(见www.episodate.com/api)拉取的电视剧集的聚合集合。JSON 包含了三部电视剧的数据:《X 档案》,《迷失》,和《吸血鬼猎人巴菲》

In  [41] tv_shows_json = pd.read_json("tv_shows.json")
         tv_shows_json

Out [41]

 ** shows**
0  {'show': 'The X-Files', 'runtime': 60, 'network': 'FOX',...
1  {'show': 'Lost', 'runtime': 60, 'network': 'ABC', 'episo...
2  {'show': 'Buffy the Vampire Slayer', 'runtime': 60, 'net...

JSON 包含一个顶层的"shows"键,它连接到一个包含三个字典的列表,每个字典对应三个节目中的一个:

{
    "shows": [{}, {}, {}]
}

每个嵌套的电视剧字典包括"show""runtime""network",和"episodes"键。以下是第一行字典的截断预览:

In  [42] tv_shows_json.loc[0, "shows"]

Out [42] {'show': 'The X-Files',
          'runtime': 60,
          'network': 'FOX',
          'episodes': [{'season': 1,
            'episode': 1,
            'name': 'Pilot',
            'air_date': '1993-09-11 01:00:00'},
           {'season': 1,
            'episode': 2,
            'name': 'Deep Throat',
            'air_date': '1993-09-18 01:00:00'},

"episodes"键映射到一个字典列表。每个字典包含一个电视剧集的数据。在之前的例子中,我们看到的是《X 档案》第一季前两集的数据。

12.4.1 问题

你的挑战是

  1. 将每个字典中的嵌套剧集数据规范化,目标是创建一个DataFrame,其中每行代表一个剧集。每行应包括剧集的相关元数据(seasonepisodename,和air_date)以及电视剧的顶级信息(showruntime,和network)。

  2. 将规范化数据集过滤成三个单独的DataFrame,每个对应一个节目("The X-Files""Lost",和"Buffy the Vampire Slayer")。

  3. 将三个DataFrame写入一个 episodes.xlsx Excel 工作簿,并将每个电视剧的剧集数据保存到单独的工作表中。(工作表名称由你决定。)

12.4.2 解决方案

让我们解决这些问题:

  1. 我们可以使用 json_normalize 函数提取每个电视节目的嵌套剧集批次。剧集位于 "episodes" 键下,我们可以将其传递给方法的 record_path 参数。为了保留顶级节目数据,我们可以将 meta 参数传递一个包含要保留的顶级键的列表:

    In  [43] tv_shows = pd.json_normalize(
                 data = tv_shows_json["shows"],
                 record_path = "episodes",
                 meta = ["show", "runtime", "network"]
             )
    
             tv_shows
    
    Out [43]
    
     **season  episode          name      air_date          show runtime network**
    0      1        1         Pilot  1993-09-1...   The X-Files      60     FOX
    1      1        2   Deep Throat  1993-09-1...   The X-Files      60     FOX
    2      1        3       Squeeze  1993-09-2...   The X-Files      60     FOX
    3      1        4       Conduit  1993-10-0...   The X-Files      60     FOX
    4      1        5  The Jerse...  1993-10-0...   The X-Files      60     FOX
     ...  ...     ...           ...           ...           ...     ...     ...
    477    7       18   Dirty Girls  2003-04-1...  Buffy the...      60     UPN
    478    7       19  Empty Places  2003-04-3...  Buffy the...      60     UPN
    479    7       20       Touched  2003-05-0...  Buffy the...      60     UPN
    480    7       21   End of Days  2003-05-1...  Buffy the...      60     UPN
    481    7       22        Chosen  2003-05-2...  Buffy the...      60     UPN
    
    482 rows × 7 columns
    
  2. 我们接下来的挑战是将数据集拆分为三个 DataFrame,每个 DataFrame 对应一个电视节目。我们可以根据 show 列中的值在 tv_shows 中过滤行:

    In  [44] xfiles = tv_shows[tv_shows["show"] == "The X-Files"]
             lost = tv_shows[tv_shows["show"] == "Lost"]
             buffy = tv_shows[tv_shows["show"] == "Buffy the Vampire Slayer"]
    
  3. 最后,让我们将三个 DataFrame 写入 Excel 工作簿。我们首先实例化一个 ExcelWriter 对象并将其保存到一个变量中。我们可以将工作簿名称作为第一个参数传入。我选择将其命名为 episodes.xlsx:

    In  [45] episodes = pd.ExcelWriter("episodes.xlsx")
             episodes
    
    Out [45] <pandas.io.excel._openpyxl._OpenpyxlWriter at 0x11e5cd3d0>
    

    接下来,我们必须在三个 DataFrame 上调用 to_excel 方法,将它们连接到工作簿中的单独工作表。我们将相同的 episodes ExcelWriter 对象传递给每个调用中的 excel_writer 参数。我们确保通过 sheet_name 参数为每个工作表提供唯一的名称。最后,我们将 index 参数的值设置为 False 以排除 DataFrame 的索引:

    In  [46] xfiles.to_excel(
                 excel_writer = episodes, sheet_name = "X-Files", index = False
             )
    
    In  [47] lost.to_excel(
                 excel_writer = episodes, sheet_name = "Lost", index = False
             )
    
    In  [48] buffy.to_excel(
                 excel_writer = episodes,
                 sheet_name = "Buffy the Vampire Slayer",
                 index = False
             )
    

    在工作表连接好之后,我们可以在 episodes ExcelWriter 对象上调用 save 方法来创建 episodes.xlsx 工作簿:

    In  [49] episodes.save()
    

恭喜你完成了编码挑战!

摘要

  • read_json 函数将 JSON 文件解析为 DataFrame

  • json_normalize 函数将嵌套 JSON 数据转换为表格 DataFrame

  • 我们可以将 URL 传递给导入函数,如 read_csvread_jsonread_excel。Pandas 将从提供的链接下载数据集。

  • read_excel 函数导入 Excel 工作簿。方法中的 sheet_name 参数设置要导入的工作表。当我们导入多个工作表时,pandas 将结果 DataFrame 存储在字典中。

  • 要将一个或多个 DataFrame 写入 Excel 工作簿,需要实例化一个 ExcelWriter 对象,通过 to_excel 方法将其 DataFrame 附加到该对象上,然后在该 ExcelWriter 对象上调用 save 方法。

13 配置 pandas

本章涵盖

  • 配置 pandas 显示设置以同时适用于笔记本和单个单元格

  • 限制打印的 DataFrame 行和列的数量

  • 修改小数点数字的精度

  • 截断单元格的文本内容

  • 当数值低于某个下限时进行数值舍入

在我们处理本书的数据集的过程中,我们看到了 pandas 如何通过在数据展示上做出合理的决策来改善我们的用户体验。例如,当我们输出一个 1,000 行的 DataFrame 时,该库假设我们更愿意看到开始和结束部分的 30 行,而不是整个数据集,这可能会使屏幕变得杂乱。有时,我们可能想要打破 pandas 的假设并调整其设置以适应我们的自定义显示需求。幸运的是,该库公开了许多其内部设置供我们修改。在本章中,我们将学习如何配置选项,例如行和列限制、浮点精度和值舍入。让我们动手看看我们如何可以改变这些设置。

13.1 获取和设置 pandas 选项

我们将首先导入 pandas 库并将其分配一个别名 pd

In  [1] import pandas as pd

本章的数据集,happiness.csv,是根据幸福感对世界各国进行排名。民意调查公司盖洛普在联合国的支持下收集了这些数据。每一行都包含一个国家的总幸福感得分以及人均国内生产总值(GDP)、社会支持、预期寿命和慷慨程度的个人得分。该数据集包含 6 列和 156 行:

In  [2] happiness = pd.read_csv("happiness.csv")
        happiness.head()

Out [2]

 ** Country  Score  GDP per cap...  Social sup...  Life expect...   Generosity**
0      Finland  7.769         1.340          1.587           0.986          0.153
1      Denmark  7.600         1.383          1.573           0.996          0.252
2       Norway  7.554         1.488          1.582           1.028          0.271
3      Iceland  7.494         1.380          1.624           1.026          0.354
4  Netherlands  7.488         1.396          1.522           0.999          0.322

Pandas 将其设置存储在库顶层的单个 options 对象中。每个选项都属于一个父类别。让我们从 display 类别开始,它包含 pandas 数据结构的打印表示的设置。

最高级别的 describe_option 函数返回给定设置的文档。我们可以传递一个包含设置名称的字符串。让我们看看 max_rows 选项,它嵌套在 display 父类别中。max_rows 设置配置了 pandas 在截断 DataFrame 之前打印的最大行数:

In  [3] pd.describe_option("display.max_rows")

Out [3]

        display.max_rows : int
            If max_rows is exceeded, switch to truncate view. Depending on
            `large_repr`, objects are either centrally truncated or printed
            as a summary view. 'None' value means unlimited.

            In case python/IPython is running in a terminal and
            `large_repr` equals 'truncate' this can be set to 0 and pandas
            will auto-detect the height of the terminal and print a
            truncated object which fits the screen height. The IPython
            notebook, IPython qtconsole, or IDLE do not run in a terminal
            and hence it is not possible to do correct auto-detection.
           [default: 60] [currently: 60]

注意,文档的末尾包括设置的默认值和当前值。

Pandas 将打印出所有与字符串参数匹配的库选项。该库使用正则表达式来比较 describe_option 的参数与其可用设置。提醒一下,正则表达式 是一种用于文本的搜索模式;请参阅附录 E 以获取详细概述。下一个示例传递了一个参数 "max_col"。Pandas 打印出与该术语匹配的两个设置的文档:

In  [4] pd.describe_option("max_col")

Out [4]

display.max_columns : int
    If max_cols is exceeded, switch to truncate view. Depending on
    `large_repr`, objects are either centrally truncated or printed as
    a summary view. 'None' value means unlimited.

    In case python/IPython is running in a terminal and `large_repr`
    equals 'truncate' this can be set to 0 and pandas will auto-detect
    the width of the terminal and print a truncated object which fits
    the screen width. The IPython notebook, IPython qtconsole, or IDLE
    do not run in a terminal and hence it is not possible to do
    correct auto-detection.
    [default: 20] [currently: 5]
display.max_colwidth : int or None
    The maximum width in characters of a column in the repr of
    a pandas data structure. When the column overflows, a "..."
    placeholder is embedded in the output. A 'None' value means unlimited.
    [default: 50] [currently: 9]

虽然正则表达式很有吸引力,但我建议写出设置的完整名称,包括其父类别。明确的代码往往会导致错误更少。

有两种方式来获取设置的当前值。第一种方式是 pandas 顶级层的 get_option 函数;像 describe_option 一样,它接受一个字符串参数,包含设置的名称。第二种方法是访问父类别和特定设置,作为顶级 pd.options 对象上的属性。

以下示例显示了两种策略的语法。这两行代码都返回 max_rows 设置的 60,这意味着 pandas 将截断任何长度超过 60 行的 DataFrame 输出:

In  [5] # The two lines below are equivalent
        pd.get_option("display.max_rows")
        pd.options.display.max_rows

Out [5] 60

同样,有两种方式可以设置配置设置的新的值。pandas 顶级层的 set_option 函数接受设置作为其第一个参数,其新值作为第二个参数。或者,我们可以通过 pd.options 对象上的属性访问选项,并用等号分配新值:

In  [6] # The two lines below are equivalent
        pd.set_option("display.max_rows", 6)
        pd.options.display.max_rows = 6

我们已指示 pandas 如果 DataFrame 输出超过六行,则截断输出:

In  [7] pd.options.display.max_rows

Out [7] 6

让我们看看实际的变化。下一个示例要求 pandas 打印幸福数据的头六行。六行最大行的阈值没有被越过,所以 pandas 没有截断地输出了 DataFrame

In  [8] happiness.head(6)

Out [8]

 ** Country  Score  GDP per cap...  Social sup...  Life expect...   Generosity**
0      Finland  7.769         1.340          1.587           0.986          0.153
1      Denmark  7.600         1.383          1.573           0.996          0.252
2       Norway  7.554         1.488          1.582           1.028          0.271
3      Iceland  7.494         1.380          1.624           1.026          0.354
4  Netherlands  7.488         1.396          1.522           0.999          0.322
5  Switzerland  7.480         1.452          1.526           1.052          0.263

现在让我们越过阈值,让 pandas 打印幸福数据的头七行。库总是旨在在截断前后打印相同数量的行。在下一个示例中,它从输出开始打印三行,从输出末尾打印三行,截断中间的行(索引 3):

In  [9] happiness.head(7)

Out [9]

 ** Country  Score  GDP per cap...  Social sup...  Life expect...   Generosity**
0      Finland  7.769         1.340            1.587         0.986          0.153
1      Denmark  7.600         1.383            1.573         0.996          0.252
2       Norway  7.554         1.488            1.582         1.028          0.271
...        ...    ...           ...              ...           ...            ...
4  Netherlands  7.488         1.396            1.522         0.999          0.322
5  Switzerland  7.480         1.452            1.526         1.052          0.263
6       Sweden  7.343         1.387            1.487         1.009          0.267

7 rows × 6 columns

max_rows 设置声明了打印的行数。互补的 display.max_columns 选项设置了打印的最大列数。默认值是 20

In  [10] # The two lines below are equivalent
         pd.get_option("display.max_columns")
         pd.options.display.max_columns

Out [10] 20

再次,要分配新值,我们可以使用 set_option 函数或直接访问嵌套的 max_columns 属性:

In  [11] # The two lines below are equivalent
         pd.set_option("display.max_columns", 2)
         pd.options.display.max_columns = 2

如果我们设置偶数个最大列数,pandas 将从其最大列数中排除截断列。幸福 DataFrame 有六列,但以下输出只显示了其中两列。Pandas 包含第一列和最后一列,即国家和国民慷慨度,并在两者之间放置一个截断列:

In  [12] happiness.head(7)

Out [12]

 ** Country  ...  Generosity**
0      Finland            0.153
1      Denmark  ...       0.252
2       Norway  ...       0.271
...        ...  ...         ...
4  Netherlands  ...       0.322
5  Switzerland  ...       0.263
6       Sweden  ...       0.267

7 rows × 6 columns

如果我们设置奇数个最大列数,pandas 将包括截断列在其列计数中。奇数确保 pandas 可以在截断的两侧放置相等数量的列。下一个示例将 max_columns 值设置为 5。幸福输出显示了最左边的两列(国家得分),截断列,以及最右边的两列(预期寿命和国民慷慨度)。Pandas 打印了原始六列中的四列:

In  [13] # The two lines below are equivalent
         pd.set_option("display.max_columns", 5)
         pd.options.display.max_columns = 5

In  [14] happiness.head(7)

Out [14]

 ** Country  Score  ...  Life expectancy   Generosity**
0      Finland  7.769  ...            0.986        0.153
1      Denmark  7.600  ...            0.996        0.252
2       Norway  7.554  ...            1.028        0.271
...        ...    ...  ...              ...          ...
4  Netherlands  7.488  ...            0.999        0.322
5  Switzerland  7.480  ...            1.052        0.263
6       Sweden  7.343  ...            1.009        0.267

5 rows × 6 columns

要将设置恢复到其原始值,请将名称传递给 pandas 顶级层的 reset_option 函数。下一个示例重置了 max_rows 设置:

In  [15] pd.reset_option("display.max_rows")

我们可以通过再次调用 get_option 函数来确认更改:

In  [16] pd.get_option("display.max_rows")

Out [16] 60

Pandas 已将 max_rows 设置重置为其默认值 60

13.2 精度

现在我们已经熟悉了 pandas 更改设置的 API,让我们来了解一下几个流行的配置选项。

display.precision 选项设置浮点数后的数字位数。默认值是 6

In  [17] pd.describe_option("display.precision")

Out [17]

         display.precision : int
             Floating point output precision (number of significant
             digits). This is only a suggestion
             [default: 6] [currently: 6]

下一个示例将精度设置为 2。该设置影响幸福感中所有四个浮点列的值:

In  [18] # The two lines below are equivalent
         pd.set_option("display.precision", 2)
         pd.options.display.precision = 2

In  [19] happiness.head()

Out [19]

 ** Country  Score  ...  Life expectancy  Generosity**
0      Finland   7.77  ...             1.34        0.15
1      Denmark   7.60  ...             1.38        0.25
2       Norway   7.55  ...             1.49        0.27
3      Iceland   7.49  ...             1.38        0.35
4  Netherlands   7.49  ...             1.40        0.32

5 rows × 6 columns

precision 设置仅改变浮点数的表示形式。Pandas 在 DataFrame 中保留原始值,我们可以通过使用 loc 访问器从浮点列(如分数)中提取样本值来证明这一点:

In  [20] happiness.loc[0, "Score"]

Out [20] 7.769

分数列的原始值,7.769,仍然存在。当 pandas 打印 DataFrame 时,它会将值的表示形式更改为 7.77

13.3 最大列宽

display.max_colwidth 设置设置 pandas 在截断单元格文本之前打印的最大字符数:

In  [21] pd.describe_option("display.max_colwidth")

Out [21]

         display.max_colwidth : int or None
             The maximum width in characters of a column in the repr of
             a pandas data structure. When the column overflows, a "..."
             placeholder is embedded in the output. A 'None' value means
             unlimited.
            [default: 50] [currently: 50]

下一个示例要求 pandas 在文本长度超过九个字符时截断文本:

In  [22] # The two lines below are equivalent
         pd.set_option("display.max_colwidth", 9)
         pd.options.display.max_colwidth = 9

让我们看看当我们输出幸福感时会发生什么:

In  [23] happiness.tail()

Out [23]

 **Country    Score  ...  Life expectancy  Generosity**
151          Rwanda     3.33  ...             0.61        0.22
152        Tanzania     3.23  ...             0.50        0.28
153          Afgha...   3.20  ...             0.36        0.16
154    Central Afr...   3.08  ...             0.10        0.23
155          South...   2.85  ...             0.29        0.20

5 rows × 6 columns

Pandas 缩短了最后三个国家值(阿富汗、中非共和国和南苏丹)。输出中的前两个值(卢旺达六个字符和坦桑尼亚八个字符)不受影响。

13.4 截断阈值

在某些分析中,如果值合理接近 0,我们可能会认为它们是无意义的。例如,您的业务领域可能认为值 0.10 是“与 0 一样好”或“实际上是 0”。display.chop_threshold 选项设置浮点值必须跨越的底限才能打印。Pandas 将显示低于阈值的任何值作为 0

In  [24] pd.describe_option("display.chop_threshold")

Out [24]

         display.chop_threshold : float or None
             if set to a float value, all float values smaller then the
             given threshold will be displayed as exactly 0 by repr and
             friends.
            [default: None] [currently: None]

此示例将 0.25 设置为截断阈值:

In  [25] pd.set_option("display.chop_threshold", 0.25)

在下一个输出中,请注意,pandas 将寿命和慷慨列的索引 154(分别为 0.1050.235)打印为输出中的 0.00

In  [26] happiness.tail()

Out [26]

 **Country  Score  ...  Life expectancy  Generosity**
151          Rwanda   3.33  ...             0.61        0.00
152        Tanzania   3.23  ...             0.50        0.28
153     Afghanistan   3.20  ...             0.36        0.00
154  Central Afr...   3.08  ...             0.00        0.00
155     South Sudan   2.85  ...             0.29        0.00

5 rows × 6 columns

precision 设置类似,chop_threshold 不会更改 DataFrame 中的底层值——只会更改它们的打印表示形式。

13.5 选项上下文

我们迄今为止更改的设置都是全局的。当我们更改它们时,我们会改变之后执行的 Jupyter Notebook 单元格的输出。全局设置持续到我们为其分配新值为止。例如,如果我们将 display.max_columns 设置为 6,那么 Jupyter 将为所有未来的单元格执行输出最多六列的 DataFrame

有时,我们可能希望为单个单元格自定义表示选项。我们可以使用 pandas 的顶级 option_context 函数来完成此任务。我们将该函数与 Python 的内置 with 关键字配对以创建上下文块。将 上下文块 想象为一个临时的执行环境。option_context 函数在代码块执行时设置 pandas 选项的临时值;全局 pandas 设置不受影响。

我们将设置作为顺序参数传递给 option_context 函数。下一个示例打印了幸福感 DataFrame

  • display.max_columns 设置为 5

  • display.max_rows 设置为 10

  • display.precision 设置为 3

Jupyter 不识别 with 块的内容作为笔记本单元的最终语句。因此,我们需要使用名为 display 的笔记本函数来手动输出 DataFrame

In  [27] with pd.option_context(
             "display.max_columns", 5,
             "display.max_rows", 10,
             "display.precision", 3
         ):
            display(happiness)

Out [27]

 **Country  Score  ...  Life expectancy  Generosity**
0           Finland  7.769  ...            0.986       0.153
1           Denmark  7.600  ...            0.996       0.252
2            Norway  7.554  ...            1.028       0.271
3           Iceland  7.494  ...            1.026       0.354
4       Netherlands  7.488  ...            0.999       0.322
...             ...    ...  ...              ...         ...
151          Rwanda  3.334  ...            0.614       0.217
152        Tanzania  3.231  ...            0.499       0.276
153     Afghanistan  3.203  ...            0.361       0.158
154  Central Afr...  3.083  ...            0.105       0.235
155     South Sudan  2.853  ...            0.295       0.202

156 rows × 6 columns

由于我们使用了 with 关键字,我们没有更改这三个选项的全局笔记本设置;它们保留了它们的原始值。

option_context 函数对于为不同的单元执行分配不同的选项很有帮助。如果您希望所有输出都有一致的表现,我建议在 Jupyter Notebook 的顶部单元中一次性设置选项。

摘要

  • describe_option 函数返回 pandas 设置的文档。

  • set_option 函数设置了一个设置的新的值。

  • 我们也可以通过访问和覆盖 pd.options 对象上的属性来更改设置。

  • reset_option 函数将 pandas 设置改回其默认值。

  • display.max_rowsdisplay.max_columns 选项设置 pandas 在输出中显示的最大行/列数。

  • display.precision 设置改变小数点后的数字位数。

  • display.max_colwidth 选项设置了一个数值阈值,当 pandas 打印字符时,超过此阈值的字符将被截断。

  • display.chop_threshold 选项设置了一个数值下限。如果值没有超过阈值,pandas 将它们打印为零。

  • option_context 函数和 with 关键字配对,以创建一个完全的临时执行上下文块。

14 可视化

本章涵盖

  • 安装 Matplotlib 库进行数据可视化

  • 使用 pandas 和 Matplotlib 渲染图表和图形

  • 将颜色模板应用于可视化

文本形式的 DataFrame 摘要很有帮助,但很多时候,一个故事最好通过可视化来讲述。折线图可以快速传达趋势;条形图可以清晰地识别独特的类别及其计数;饼图可以以易于消化的方式表示比例,等等。幸运的是,pandas 与许多流行的 Python 数据可视化库无缝集成,包括 Matplotlib、seaborn 和 ggplot。在本章中,我们将学习如何使用 Matplotlib 从我们的 SeriesDataFrames 中渲染动态图表。我希望这些可视化能帮助您在数据展示中添加一点火花。

14.1 安装 matplotlib

默认情况下,pandas 依赖于开源的 Matplotlib 包来渲染图表和图形。让我们在我们的 Anaconda 环境中安装它。

首先,启动您操作系统的终端(macOS)或 Anaconda Prompt(Windows)应用程序。默认的 Anaconda 环境 base 应该列在左侧括号内。base 是当前活动环境。

当我们安装 Anaconda(见附录 A)时,我们创建了一个名为 pandas_in_action 的环境。让我们执行 conda activate 命令来激活它。如果您选择了不同的环境名称,请将 pandas_in_action 替换为该名称,如下所示:

(base) ~$ conda activate pandas_in_action

括号应反映活动环境。执行命令 conda install matplotlibpandas_in_action 环境中安装 Matplotlib 库:

(pandas_in_action) ~$ conda install matplotlib

当提示确认时,输入 'Y' 表示是并按 Enter 键。安装完成后,执行 jupyter notebook 并创建一个新的 Notebook。

14.2 折线图

如往常一样,让我们首先导入 pandas 库。我们还将从 Matplotlib 库中导入 pyplot 包。在这个上下文中,一个 指的是顶级库中的一个嵌套文件夹。我们可以使用点符号访问 pyplot 包,就像访问任何库属性一样。pyplot 的一个常见社区别名是 plt

默认情况下,Jupyter Notebook 会将每个 Matplotlib 可视化渲染在单独的浏览器窗口中,就像网站上的弹出窗口一样。窗口可能会有些令人震惊,尤其是当屏幕上有多个图表时。我们可以添加一个额外的行——%matplotlib inline——来强制 Jupyter 在单元格下方直接渲染可视化。%matplotlib inline 是一个魔法函数,是设置 Notebook 配置选项的语法快捷方式:

In  [1] import pandas as pd
        import matplotlib.pyplot as plt
        %matplotlib inline

现在来看数据!本章的数据集,space_missions.csv,包含了 2019 年和 2020 年间的 100 多次太空飞行记录。每条记录包括任务日期、赞助公司、地点、成本和状态("成功"或"失败"):

In  [2] pd.read_csv("space_missions.csv").head()

Out [2]

 **Date Company Name Location    Cost   Status**
0   2/5/19  Arianespace   France  200.00  Success
1  2/22/19       SpaceX      USA   50.00  Success
2   3/2/19       SpaceX      USA   50.00  Success
3   3/9/19         CASC    China   29.15  Success
4  3/22/19  Arianespace   France   37.00  Success

在我们将导入的DataFrame分配给space变量之前,让我们调整两个设置。首先,我们将使用parse_dates参数将日期列中的值导入为日期时间。接下来,我们将日期列设置为DataFrame的索引:

In  [3] space = pd.read_csv(
            "space_missions.csv",
            parse_dates = ["Date"],
            index_col = "Date"
        )

        space.head()

Out [3]

 ** Company Name Location    Cost   Status**
Date
2019-02-05  Arianespace   France  200.00  Success
2019-02-22       SpaceX      USA   50.00  Success
2019-03-02       SpaceX      USA   50.00  Success
2019-03-09         CASC    China   29.15  Success
2019-03-22  Arianespace   France   37.00  Success

假设我们想要绘制这个数据集中两年内的飞行成本。时间序列图是观察随时间趋势的最佳图表。我们可以在 x 轴上绘制时间,在 y 轴上绘制值。首先,让我们从DataFrame的空间中提取成本列。结果是包含数值和日期时间索引的Series

In  [4] space["Cost"].head()

Out [4] Date
        2019-02-05    200.00
        2019-02-22     50.00
        2019-03-02     50.00
        2019-03-09     29.15
        2019-03-22     37.00
        Name: Cost, dtype: float64

要渲染一个可视化,请在 pandas 数据结构上调用plot方法。默认情况下,Matplotlib 绘制一个折线图。Jupyter 还会打印出图表对象在计算机内存中的位置。这个位置会随着每个单元格的执行而不同,所以请随意忽略它:

In  [5] space["Cost"].plot()

Out [5] <matplotlib.axes._subplots.AxesSubplot at 0x11e1c4650>

真的很花哨!我们已经使用 pandas 的值使用 Matplotlib 渲染了一个折线图。默认情况下,库在 x 轴上绘制索引标签(在这种情况下,日期时间),在 y 轴上绘制Series的值。Matplotlib 还计算了两个轴上值范围的合理间隔。

我们也可以在空间DataFrame本身上调用plot方法。在这种情况下,pandas 产生相同的输出,但仅因为数据集只有一个数值列:


In  [6] space.plot()

Out [6] <matplotlib.axes._subplots.AxesSubplot at 0x11ea18790>

如果DataFrame包含多个数值列,Matplotlib 将为每个列绘制一条单独的线。请注意:如果列之间值的幅度存在较大差距(例如,如果一个数值列的值在百万级别,而另一个在百级别),较大的值可能会轻易地掩盖较小的值。考虑以下DataFrame

In  [7] data = [
            [2000, 3000000],
            [5000, 5000000]
        ]

        df = pd.DataFrame(data = data, columns = ["Small", "Large"])
        df

Out [7]

   Small    Large
0   2000  3000000
1   5000  5000000

当我们绘制 df DataFrame时,Matplotlib 调整图表比例以适应大型列的值。小型列的值趋势变得难以看到:

In  [8] df.plot()

Out [8] <matplotlib.axes._subplots.AxesSubplot at 0x7fc48279b6d0>

让我们回到空间。plot方法接受一个y参数来标识 Matplotlib 应该绘制其值的DataFrame列。下一个示例传递了成本列,这是渲染相同时间序列图的另一种方式:

In  [9] space.plot(y = "Cost")

Out [9] <matplotlib.axes._subplots.AxesSubplot at 0x11eb0b990>

我们可以使用colormap参数来改变可视化的外观。将这个过程想象成设置图表的颜色主题。该参数接受一个字符串,该字符串来自 Matplotlib 库的预定义调色板。以下示例使用了一个"gray"主题,该主题将折线图渲染为黑白:

In  [10] space.plot(y = "Cost", colormap = "gray")

Out [10] <matplotlib.axes._subplots.AxesSubplot at 0x11ebef350>

要查看colormaps参数的有效输入列表,请在pyplot库(在我们的笔记本中别名为plt)上调用colormaps方法。请注意,我们只能在某些条件满足的情况下应用一些主题,例如最小数量的图表线条:

In  [11] print(plt.colormaps())

Out [11] ['Accent', 'Accent_r', 'Blues', 'Blues_r', 'BrBG', 'BrBG_r',
          'BuGn', 'BuGn_r', 'BuPu', 'BuPu_r', 'CMRmap', 'CMRmap_r',
          'Dark2', 'Dark2_r', 'GnBu', 'GnBu_r', 'Greens', 'Greens_r',
          'Greys', 'Greys_r', 'OrRd', 'OrRd_r', 'Oranges', 'Oranges_r',
          'PRGn', 'PRGn_r', 'Paired', 'Paired_r', 'Pastel1', 'Pastel1_r',
          'Pastel2', 'Pastel2_r', 'PiYG', 'PiYG_r', 'PuBu', 'PuBuGn',
          'PuBuGn_r', 'PuBu_r', 'PuOr', 'PuOr_r', 'PuRd', 'PuRd_r',
          'Purples', 'Purples_r', 'RdBu', 'RdBu_r', 'RdGy', 'RdGy_r',
          'RdPu', 'RdPu_r', 'RdYlBu', 'RdYlBu_r', 'RdYlGn', 'RdYlGn_r',
          'Reds', 'Reds_r', 'Set1', 'Set1_r', 'Set2', 'Set2_r', 'Set3',
          'Set3_r', 'Spectral', 'Spectral_r', 'Wistia', 'Wistia_r', 'YlGn',
          'YlGnBu', 'YlGnBu_r', 'YlGn_r', 'YlOrBr', 'YlOrBr_r', 'YlOrRd',
          'YlOrRd_r', 'afmhot', 'afmhot_r', 'autumn', 'autumn_r', 'binary',
          'binary_r', 'bone', 'bone_r', 'brg', 'brg_r', 'bwr', 'bwr_r',
          'cividis', 'cividis_r', 'cool', 'cool_r', 'coolwarm',
          'coolwarm_r', 'copper', 'copper_r', 'cubehelix', 'cubehelix_r',
          'flag', 'flag_r', 'gist_earth', 'gist_earth_r', 'gist_gray',
          'gist_gray_r', 'gist_heat', 'gist_heat_r', 'gist_ncar',
          'gist_ncar_r', 'gist_rainbow', 'gist_rainbow_r', 'gist_stern',
          'gist_stern_r', 'gist_yarg', 'gist_yarg_r', 'gnuplot',
          'gnuplot2', 'gnuplot2_r', 'gnuplot_r', 'gray', 'gray_r', 'hot',
          'hot_r', 'hsv', 'hsv_r', 'inferno', 'inferno_r', 'jet', 'jet_r',
          'magma', 'magma_r', 'nipy_spectral', 'nipy_spectral_r', 'ocean',
          'ocean_r', 'pink', 'pink_r', 'plasma', 'plasma_r', 'prism',
          'prism_r', 'rainbow', 'rainbow_r', 'seismic', 'seismic_r',
          'spring', 'spring_r', 'summer', 'summer_r', 'tab10', 'tab10_r',
          'tab20', 'tab20_r', 'tab20b', 'tab20b_r', 'tab20c', 'tab20c_r',
          'terrain', 'terrain_r', 'twilight', 'twilight_r',
          'twilight_shifted', 'twilight_shifted_r', 'viridis', 'viridis_r',
          'winter', 'winter_r']

Matplotlib 提供了超过 150 种可用的颜色图可供选择。该库还提供了手动自定义图表的方法。

14.3 柱状图

plot 方法的 kind 参数会改变 Matplotlib 渲染的图表类型。柱状图是展示数据集中唯一值计数的绝佳选择,因此我们可以用它来可视化每家公司赞助的太空飞行次数。

首先,我们将针对“公司名称”列并调用 value_counts 方法来返回按公司计算的使命计数的 Series

In  [12] space["Company Name"].value_counts()

Out [12] CASC            35
         SpaceX          25
         Roscosmos       12
         Arianespace     10
         Rocket Lab       9
         VKS RF           6
         ULA              6
         Northrop         5
         ISRO             5
         MHI              3
         Virgin Orbit     1
         JAXA             1
         ILS              1
         ExPace           1
         Name: Company Name, dtype: int64

接下来,让我们在 Series 上调用 plot 方法,将 kind 参数的参数传递为 "bar"。Matplotlib 再次在 x 轴上绘制索引标签,在 y 轴上绘制值。看起来 CASC 在数据集中有最多的条目,其次是 SpaceX:

In  [13] space["Company Name"].value_counts().plot(kind = "bar")

Out [13] <matplotlib.axes._subplots.AxesSubplot at 0x11ecd6310>

图表是一个良好的开始,但我们必须转动头部才能读取标签。哎呀。让我们将 kind 参数更改为 "barh" 以渲染一个水平柱状图:

In  [14] space["Company Name"].value_counts().plot(kind = "barh")

Out [14] <matplotlib.axes._subplots.AxesSubplot at 0x11edf0190>

现在好多了!现在我们可以轻松地识别哪些公司在数据集中有最多的太空飞行次数。

14.4 饼图

饼图是一种可视化,其中彩色切片组合形成一个完整的圆形饼图(就像披萨的片状)。每一部分都直观地表示它对总量的贡献比例。

让我们使用饼图来比较成功任务与失败任务的比例。状态列只有两个唯一值:“成功”和“失败”。首先,我们将使用 value_counts 方法来计算每个值的出现次数:

In  [15] space["Status"].value_counts()

Out [15] Success    114
         Failure      6
         Name: Status, dtype: int64

让我们再次调用 plot 方法。这次,我们将 kind 参数的参数设置为 "pie"

In  [16] space["Status"].value_counts().plot(kind = "pie")

Out [16] <matplotlib.axes._subplots.AxesSubplot at 0x11ef9ea90>

好消息!看起来大多数太空飞行都是成功的。

要向此类可视化添加图例,我们可以将 legend 参数的参数设置为 True

In  [17] space["Status"].value_counts().plot(kind = "pie", legend = True)

Out [17] <matplotlib.axes._subplots.AxesSubplot at 0x11eac1a10>

Matplotlib 支持广泛的附加图表和图形,包括直方图、散点图和箱线图。我们可以包括额外的参数来自定义这些可视化的美学、标签、图例和交互性。我们只是触及了这个强大库可以渲染的表面。

概述

  • Pandas 与 Matplotlib 库无缝集成,用于数据可视化。它还与 Python 数据科学生态系统中其他绘图库兼容得很好。

  • SeriesDataFrame 上的 plot 方法会渲染一个包含 pandas 数据结构数据的可视化。

  • Matplotlib 默认的图表是折线图。

  • plot 方法的 kind 参数会改变渲染的可视化类型。选项包括折线图、柱状图和饼图。

  • colormap参数会改变渲染图形的颜色方案。Matplotlib 有数十个预定义的模板,用户也可以通过调整方法参数来创建自己的模板。

附录 A. 安装和设置

欢迎来到补充材料!本附录将指导您在 macOS 和 Windows 操作系统上安装 Python 编程语言和 pandas 库。(也称为)是一系列功能,它扩展了核心编程语言的功能——一个扩展包或附加组件,它为开发者在使用该语言时遇到的常见挑战提供解决方案。Python 生态系统包括数千个针对统计、HTTP 请求和数据库管理等领域的包。

依赖项是我们需要安装以运行其他软件的软件组件。Pandas 不是一个独立的包;它包含一系列依赖项,包括 NumPy 和 pytz 库。这些库可能需要它们自己的依赖项。我们不需要了解所有这些其他包的功能,但我们需要安装它们,以便 pandas 能够正常工作。

A.1 Anaconda 发行版

开源库通常由不同时间线的独立贡献者团队开发。不幸的是,隔离的开发周期可能会在库版本之间引入兼容性问题。在不升级其依赖项的情况下安装库的最新版本可能会导致其无法正常工作,例如。

为了简化 pandas 及其依赖项的安装和管理,我们将依赖一个名为 Anaconda 的 Python 发行版。发行版是一组软件,它将多个应用程序及其依赖项捆绑在一个简单的安装程序中。Anaconda 拥有超过 2000 万用户,是 Python 中进行数据科学的最受欢迎的发行版。

Anaconda 安装 Python 和一个名为conda的强大环境管理系统。环境是一个独立的代码执行沙盒——一种可以安装 Python 和一系列包的游乐场。为了实验不同的 Python 版本、不同的 pandas 版本、不同的包组合或任何介于两者之间的东西,我们创建一个新的conda环境。图 A.1 展示了三个假设的conda环境,每个环境都有不同的 Python 版本。

图片

图 A.1 具有不同 Python 版本和不同包的三个 Anaconda 环境

环境的优势在于隔离。一个环境中的更改不会影响其他任何环境,因为conda将它们存储在不同的文件夹中。因此,我们可以轻松地处理多个项目,每个项目都需要不同的配置。当您将包安装到环境中时,conda也会安装适当的依赖项,并确保不同库版本之间的兼容性。简而言之,conda是使您的计算机上能够进行多个 Python 工具安装和配置的有效方式。

这只是一个宏观介绍!现在让我们开始实际操作,安装 Anaconda。前往 www.anaconda.com/products/individual,找到适用于您的操作系统的安装程序下载部分。您可能会看到多个 Anaconda 安装程序的版本:

  • 如果您在图形安装程序和命令行安装程序之间有选择,请选择图形安装程序。

  • 如果您可以选择 Python 版本,请选择最新的版本。与大多数软件一样,较大的版本号表示较新的发布。Python 3 比 Python 2 新,Python 3.9 比 Python 3.8 新。在学习新技术时,最好从最新版本开始。不用担心;conda 允许您创建使用较早版本 Python 的环境,如果您需要的话。

  • 如果您是 Windows 用户,您可能会在 64 位和 32 位安装程序之间进行选择。我们将在 A.3 节中讨论选择哪一个。

到此为止,macOS 和 Windows 操作系统的设置过程将有所不同。在本附录中找到适当的子节,并从那里继续。

A.2 macOS 安装过程

让我们一步步在 macOS 计算机上安装 Anaconda。

A.2.1 在 macOS 中安装 Anaconda

您的 Anaconda 下载将包含一个单独的 .pkg 安装程序文件。文件名可能包含 Anaconda 版本号和操作系统(例如 Anaconda3-2021.05-MacOSX-x86_64)。在文件系统中定位安装程序,并双击它以开始安装。

在第一个屏幕上点击继续按钮。在 README 屏幕上,安装程序提供了 Anaconda 的快速概述,值得浏览(见图 A.2)。

图 A.2 macOS 计算机上的 Anaconda 安装屏幕

安装会创建一个名为 base 的起始 conda 环境,其中包含超过 250 个预选的数据分析包。您稍后可以创建额外的环境。安装程序还会通知您,每次您启动 shell 时,它都会激活此 base 环境;我们将在 A.2.2 节中讨论此过程是如何工作的。现在,请相信这部分安装过程是必需的,并继续前进。

继续浏览任何剩余的屏幕。接受许可协议和空间要求。您将获得自定义安装目录的选项;是否进行自定义完全取决于您。请注意,该发行版是自包含的;Anaconda 会将其自身安装到您的计算机上的一个目录中。因此,如果您想卸载 Anaconda,您可以删除该目录。

安装可能需要几分钟。完成安装后,点击下一步直到退出安装程序。

A.2.2 启动终端

Anaconda 随附一个名为 Navigator 的图形程序,它使得创建和管理 conda 环境变得容易。然而,在我们启动它之前,我们将使用更传统的终端应用程序向 conda 环境管理器发出命令。

终端 是一个用于向 macOS 操作系统发出命令的应用程序。在现代图形用户界面(GUI)存在之前,用户完全依赖于基于文本的应用程序来与计算机交互。在终端中,你输入文本然后按 Enter 键执行它。我建议我们在掌握 Anaconda Navigator 之前先掌握终端,因为了解软件为我们抽象的复杂性是很重要的,在我们依赖其快捷方式之前。

打开一个 Finder 窗口,导航到应用程序目录,你将在实用工具文件夹中找到终端应用程序。启动应用程序。我还建议将终端应用程序的图标拖到 Dock 中以便于访问。

终端应在闪烁提示符之前,在括号内列出激活的 conda 环境名称。作为提醒,Anaconda 在安装期间创建了一个 base 起始环境。图 A.3 显示了一个激活了 base 环境的示例终端窗口。

图片

图 A.3 macOS 机器上的终端。当前激活的 conda 环境是 base

每次我们启动终端时,Anaconda 都会激活 conda 环境管理器和 base 环境。

A.2.3 常见终端命令

我们只需要记住少数几个命令就可以有效地使用终端。在终端中,我们可以像在 Finder 中一样在计算机的目录中导航。pwd(打印工作目录)命令输出我们所在的文件夹:

(base) ~$ pwd
/Users/boris

ls(列表)命令列出当前目录内的文件和文件夹:

(base) ~$ ls
Applications Documents    Google Drive Movies       Pictures     anaconda3
Desktop      Downloads    Library      Music        Public

一些命令接受标志。标志 是我们添加到命令后面的配置选项,以修改其执行方式。其语法由一系列短横线和文本字符组成。这里有一个例子。单独的 ls 命令只显示公共文件和文件夹。我们可以向命令中添加 --all 标志来显示隐藏的文件。一些标志支持多个语法选项。例如,ls -als --all 的快捷方式。请亲自尝试这两个命令。

cd(更改目录)命令导航到指定的目录。在命令后立即输入目录名,确保包含一个空格。在下一个示例中,我们将导航到桌面目录:

(base) ~$ cd Desktop

我们可以使用 pwd(打印工作目录)命令输出我们的当前位置:

(base) ~/Desktop$ pwd
/Users/boris/Desktop

cd 命令后跟一对点号可以向上导航文件夹层次结构:

(base) ~/Desktop$ cd ..

(base) ~$ pwd
/Users/boris

终端具有强大的自动完成功能。在您的用户目录中,输入 cd Des 并按 Tab 键以自动完成到 cd Desktop。终端查看可用的文件和文件夹列表,并确定只有 Desktop 与我们输入的 Des 模式匹配。如果有多个匹配项,终端将完成名称的一部分。如果一个目录包含两个文件夹,AnacondaAnalytics,并且您输入字母 A,终端将自动完成 Ana,这是两个选项中的常见字母。您需要输入额外的字母并再次按 Tab 键,以便终端自动完成名称的其余部分。

到目前为止,我们已经获得了开始使用 conda 环境管理器所需的所有知识。跳转到 A.4 部分,我们将与我们的 Windows 朋友们见面,并设置我们的第一个 conda 环境!

A.3 Windows 设置过程

让我们一起来了解一下如何在 Windows 计算机上安装 Anaconda。

A.3.1 在 Windows 中安装 Anaconda

Windows 的 Anaconda 安装程序提供 32 位和 64 位版本。这些选项描述了与您的计算机一起安装的处理器类型。如果您不确定要下载哪个版本,请打开开始菜单,并选择系统信息应用。在应用的主屏幕上,您将看到一个由“项目”和“值”列组成的表格。查找“系统”“类型”项目;其值将包括 x64 如果您的计算机运行的是 64 位版本的 Windows,或者 x86 如果您的计算机运行的是 32 位版本的 Windows。图 A.4 显示了具有突出显示的系统类型行的 Windows 计算机上的系统信息应用。

图片

图 A.4 64 位 Windows 计算机上的系统信息应用

您的 Anaconda 下载将包含一个单独的 .exe 安装程序文件。文件名将包括 Anaconda 版本号和操作系统(例如 Anaconda3-2021.05-Windows-x86_64)。在您的文件系统中定位该文件,并双击它以启动安装程序。

通过前几个安装屏幕。您将被提示接受许可协议,选择是否为单个或所有用户安装 Anaconda,以及选择安装目录。选择默认选项是可以的。

当您到达高级安装选项屏幕时,如果您已经在计算机上安装了 Python,取消选中“将 Anaconda 注册为我的默认 Python”复选框可能是个好主意。取消选中该选项可以防止安装将 Anaconda 设置为计算机上的默认 Python 版本。如果您是第一次安装 Python,保留该选项选中应该没问题。

安装创建了一个名为 base 的起始 conda 环境,其中包含超过 250 个预先选择的数据分析包。您稍后可以创建额外的环境。

安装可能需要几分钟。图 A.5 显示了安装过程的示例。当安装完成后,退出安装程序。

图片

![图 A.5 Windows 电脑上的 Anaconda 安装过程]

如果你想要卸载 Anaconda,请启动开始菜单,并选择“添加或删除程序”。找到 Anaconda 程序,点击“卸载”按钮,然后按照提示中的步骤从计算机中删除该分布。请注意,此过程将删除所有 conda 环境、它们安装的包以及 Python 版本。

A.3.2 启动 Anaconda Prompt

Anaconda 随附一个名为 Navigator 的图形程序,它使得创建和管理 conda 环境变得容易。然而,在我们启动它之前,我们将使用一个更传统的命令行应用程序来向 conda 环境管理器发出命令。在依赖其快捷方式之前,理解 Navigator 为我们解决的问题很重要。

Anaconda Prompt 是一个用于向 Windows 操作系统发出文本命令的应用程序。我们输入一个命令,然后按 Enter 键执行它。在现代 GUI 存在之前,用户完全依赖于像这样一个基于命令的应用程序来与计算机交互。打开开始菜单,找到 Anaconda Prompt,然后启动应用程序。

Anaconda Prompt 应始终在其闪烁的提示符之前用一对括号列出活动的 conda 环境。目前,你应该看到 base,这是 Anaconda 在安装期间创建的起始环境。图 A.6 显示了具有活动 base 环境的 Anaconda Prompt。

图片

图 A.6 Windows 机器上的 Anaconda Prompt

当 Anaconda Prompt 启动时,将激活 base 环境。在 A.3.4 节中,我们将介绍如何使用 conda 创建和激活新环境。

A.3.3 常见的 Anaconda Prompt 命令

我们只需要记住少数几个命令,就可以有效地使用 Anaconda Prompt。我们可以像在 Windows 资源管理器中一样在计算机的目录中导航。dir(目录)命令列出当前目录中的所有文件和文件夹:

(base) C:\Users\Boris>dir
 Volume in drive C is OS
 Volume Serial Number is 6AAC-5705

 Directory of C:\Users\Boris

08/15/2019 03:16 PM <DIR> .
08/15/2019 03:16 PM <DIR> ..
09/20/2017 02:45 PM <DIR> Contacts
08/18/2019 11:21 AM <DIR> Desktop
08/13/2019 03:50 PM <DIR> Documents
08/15/2019 02:51 PM <DIR> Downloads
09/20/2017 02:45 PM <DIR> Favorites
05/07/2015 09:56 PM <DIR> Intel
06/25/2018 03:35 PM <DIR> Links
09/20/2017 02:45 PM <DIR> Music
09/20/2017 02:45 PM <DIR> Pictures
09/20/2017 02:45 PM <DIR> Saved Games
09/20/2017 02:45 PM <DIR> Searches
09/20/2017 02:45 PM <DIR> Videos
              1 File(s) 91 bytes
             26 Dir(s) 577,728,139,264 bytes free

cd(更改目录)命令可以导航到指定的目录。在命令后立即输入目录名,确保包含一个空格。在下一个示例中,我们将导航到桌面目录:

(base) C:\Users\Boris>cd Desktop

(base) C:\Users\Boris\Desktop>

cd 后跟一对点号可以向上导航文件夹层次结构:

(base) C:\Users\Boris\Desktop>cd ..

(base) C:\Users\Boris>

Anaconda Prompt 具有强大的自动完成功能。在你的用户目录中,输入cd Des并按 Tab 键以自动完成它为cd Desktop。Anaconda Prompt 查看可用的文件和文件夹列表,并确定只有Desktop与我们所输入的Des模式匹配。如果有多个匹配项,Anaconda Prompt 将完成名称的一部分。如果一个目录包含两个文件夹,AnacondaAnalytics,而你输入字母A,Anaconda Prompt 将自动完成Ana,这是两个选项中的共同字母。你将需要输入额外的字母并再次按 Tab 键,以便 Prompt 自动完成名称的其余部分。

到目前为止,我们已经拥有了开始使用conda环境管理器所需的所有知识。让我们创建我们的第一个conda环境!

A.4 创建新的 Anaconda 环境

恭喜你——你已在 macOS 或 Windows 机器上成功安装了 Anaconda 发行版。现在让我们创建一个示例conda环境,我们将用它来学习本书。请注意,本节中的代码示例来自 macOS 计算机。尽管两个操作系统之间的输出可能略有不同,但 Anaconda 命令保持不变。

打开终端(macOS)或 Anaconda Prompt(Windows)。Anaconda 的默认base环境应该处于激活状态。查看提示符左侧是否有包含base一词的括号,以确认其存在。

首先,让我们通过发出一个示例命令来确认我们已成功安装了conda环境管理器。这里有一个简单的命令:让conda显示其版本号。请注意,你的版本可能与以下输出中的版本不同,但只要命令返回任何数字,就表示conda已成功安装:

(base) ~$ conda –-version
conda 4.10.1

conda info命令返回有关conda的技术细节列表。输出包括当前活动环境和它在硬盘上的位置。以下是输出摘要:

(base) ~$ conda info

     active environment : base
    active env location : /opt/anaconda3
            shell level : 1
       user config file : /Users/boris/.condarc
 populated config files : /Users/boris/.condarc
          conda version : 4.10.1
    conda-build version : 3.18.9
         python version : 3.7.4.final.0

我们可以使用标志来自定义和配置conda命令。标志是我们添加到命令之后的一个配置选项,用于修改其执行方式。其语法由一系列短横线和文本字符组成。--envs标志用于info命令,列出所有环境和它们在计算机上的位置。星号(*)标记了活动环境:

(base) ~$ conda info --envs
# conda environments:
#
base                  *  /Users/boris/anaconda3

每个conda命令都支持--help标志,该标志会输出命令的文档。让我们将此标志添加到conda info命令中:

(base) ~$ conda info --help
usage: conda info [-h] [--json] [-v] [-q] [-a] [--base] [-e] [-s]
                  [--unsafe-channels]

Display information about current conda install.

Options:

optional arguments:
  -h, --help         Show this help message and exit.
  -a, --all          Show all information.
  --base             Display base environment path.
  -e, --envs         List all known conda environments.
  -s, --system       List environment variables.
  --unsafe-channels  Display list of channels with tokens exposed.

Output, Prompt, and Flow Control Options:
  --json             Report all output as json. Suitable for using conda
                     programmatically.
  -v, --verbose      Use once for info, twice for debug, three times for
                     trace.
  -q, --quiet        Do not display progress bar.

让我们创建一个新的游乐场来玩耍。conda create命令生成一个新的conda环境。我们必须使用--name标志为环境提供名称。我选择了一个合适的标题pandas_in_action;你可以选择你喜欢的任何环境名称。当conda提示确认时,输入y(代表是)并按 Enter 键确认:

(base) ~$ conda create --name pandas_in_action
Collecting package metadata (current_repodata.json): done
Solving environment: done

## Package Plan ##

  environment location: /opt/anaconda3/envs/pandas_in_action

Proceed ([y]/n)? y

Preparing transaction: done
Verifying transaction: done
Executing transaction: done
#
# To activate this environment, use
#
#     $ conda activate pandas_in_action
#
# To deactivate an active environment, use
#
#     $ conda deactivate

默认情况下,conda在新环境中安装最新的 Python 版本。要自定义语言版本,请在命令末尾添加关键字python,输入一个等号,并声明所需的版本。以下示例展示了如何创建一个名为sample的环境,并使用 Python 3.7:

(base) ~$ conda create --name sample python=3.7

使用conda env remove命令删除一个环境。提供--name标志,指定您想要删除的环境。以下代码示例删除了我们创建的sample环境:

(base) ~$ conda env remove --name sample

现在已经创建了pandas_in_action环境,我们可以激活它。conda activate命令在终端或 Anaconda 提示符中设置活动环境。提示符前的文本将更改为反映新的活动环境:

(base) ~$ conda activate pandas_in_action

(pandas_in_action) ~$

所有conda命令都在活动环境中执行。如果我们要求conda安装一个 Python 软件包,例如,conda现在将在pandas_in_action中安装它。我们想要安装以下软件包:

  • 核心库pandas

  • 我们将在此处编写代码的jupyter开发环境

  • 用于速度加速的bottlenecknumexpr

conda install 命令在活动的conda环境中下载并安装软件包。在命令后立即添加四个软件包,用空格分隔:

(pandas_in_action) ~$ conda install pandas jupyter bottleneck numexpr

如前所述,这四个库有依赖关系。conda环境管理器将输出所有需要安装的软件包的列表。以下是输出的一部分。如果您看到不同的库列表或版本号,这是正常的;conda会处理兼容性问题。

Collecting package metadata (repodata.json): done
Solving environment: done

## Package Plan ##

  environment location: /opt/anaconda3/envs/pandas_in_action

  added / updated specs:
    - bottleneck
    - jupyter
    - numexpr
    - pandas

The following packages will be downloaded:

    package                    |            build
    ---------------------------|-----------------
    appnope-0.1.2              |py38hecd8cb5_1001          10 KB
    argon2-cffi-20.1.0         |   py38haf1e3a3_1          44 KB
    async_generator-1.10       |             py_0          24 KB
    certifi-2020.12.5          |   py38hecd8cb5_0         141 KB
    cffi-1.14.4                |   py38h2125817_0         217 KB
    ipython-7.19.0             |   py38h01d92e1_0         982 KB
    jedi-0.18.0                |   py38hecd8cb5_0         906 KB
    #... more libraries

输入y并按 Enter 键安装所有软件包及其依赖项。

如果您忘记了环境中安装的软件包,请使用conda list命令查看完整的列表。输出包括每个库的版本:

(pandas_in_action) ~$ conda list

# packages in environment at /Users/boris/anaconda3/envs/pandas_in_action:
#
# Name                    Version             Build  Channel
jupyter                   1.0.0           py39hecd8cb5_7
pandas                    1.2.4           py39h23ab428_0

如果您想从一个环境中删除软件包,请使用conda uninstall命令。以下是一个使用 pandas 的示例:

(pandas_in_action) ~$ conda uninstall pandas

我们准备好探索我们的开发环境了。我们可以使用命令jupyter notebook启动 Jupyter Notebook 应用程序:

(pandas_in_action) ~$ jupyter notebook

Jupyter Notebook 在您的计算机上启动一个本地服务器以运行核心 Jupyter 应用程序。我们需要一个持续运行的服务器,以便它可以观察我们编写的 Python 代码并立即执行它。

Jupyter Notebook 应用程序应在您的系统默认网页浏览器中打开。您也可以通过在地址栏中导航到 localhost:8888/来访问应用程序;localhost 指的是您的计算机,8888 是应用程序运行的端口。就像码头包括多个港口以欢迎多艘船只一样,您的计算机(localhost)也有多个端口,以便在您的本地服务器上运行多个程序。图 A.7 显示了 Jupyter Notebook 的主界面,列出了当前目录中的文件和文件夹。

图 A.7 Jupyter Notebook 的主界面

Jupyter Notebook 界面类似于 Finder(macOS)或 Windows 资源管理器(Windows)。文件夹和文件按字母顺序组织。您可以点击文件夹来导航到下一个目录,并使用顶部的导航栏向上导航。探索几秒钟。当您熟悉导航后,关闭浏览器。

注意,关闭浏览器并不会关闭正在运行的 Jupyter 服务器。我们需要在终端或 Anaconda Prompt 中按两次键盘快捷键 Ctrl-C 来终止 Jupyter 服务器。

注意,每次您启动终端(macOS)或 Anaconda Prompt(Windows)时,您都需要再次激活 pandas_in_action 环境。尽管 Anaconda 的 base 环境包括 pandas,但我建议为每本 Python 书籍或教程创建一个新的环境。多个环境确保了不同项目之间 Python 依赖项的分离。例如,一个教程可能使用 pandas 1.1.3,而另一个可能使用 pandas 1.2.0。当您单独安装、升级和处理依赖项时,技术错误的可能性更小。

这里提醒您每次启动终端或 Anaconda Prompt 时需要做什么:

(base) ~$ conda activate pandas_in_action

(pandas_in_action) ~$ jupyter notebook

第一个命令激活 conda 环境,第二个命令启动 Jupyter Notebook。

A.5 Anaconda 导航器

Anaconda Navigator 是一个用于管理 conda 环境的图形程序。尽管其功能集不如 conda 命令行工具全面,但 Anaconda Navigator 提供了一种视觉友好、适合初学者的方式来使用 conda 创建和管理环境。您可以在 Finder(macOS)中的应用程序文件夹或 Windows 的开始菜单中找到 Anaconda Navigator。图 A.8 显示了 Anaconda 导航器应用程序的主界面。

图 A.8 Anaconda 导航器主界面

在左侧菜单中点击环境选项卡以显示所有环境的列表。选择一个 conda 环境,以查看其已安装的包,包括它们的描述和版本号。

在底部面板中,点击创建按钮以启动新的环境创建提示。给环境命名,并选择要安装的 Python 版本。结果对话框显示 conda 将创建环境的位置(图 A.9)。

图 A.9 创建新的 Anaconda 环境

要安装包,在左侧列表中选择一个环境。在包列表上方,点击下拉菜单并选择所有以查看所有包(图 A.10)。

图 A.10 Anaconda 包搜索

在右侧的搜索框中,搜索一个示例库,例如 pandas。在搜索结果中找到它,并选择相应的复选框(图 A.11)。

图 A.11 在 Anaconda 导航器中搜索并选择 pandas 包

最后,点击右下角的绿色应用按钮来安装库。

让我们删除我们创建的 pandas_playbox 环境。因为我们已经在终端或 Anaconda Prompt 中创建了一个 pandas_in_action 环境,所以我们不再需要它。请确保在左侧环境列表中选择 pandas_playbox。然后点击底部面板上的删除按钮,并在确认对话框中再次点击(图 A.12)。

图片

图 A.12 在 Anaconda Navigator 中删除我们创建的环境

要从 Anaconda Navigator 启动 Jupyter Notebook,点击左侧导航菜单的“主页”标签。在此屏幕上,你会看到当前环境中安装的应用程序的磁贴。屏幕顶部有一个下拉菜单,你可以从中选择活动的 conda 环境。请确保选择我们为本书创建的 pandas_in_action 环境。然后你可以通过点击其应用程序磁贴来启动 Jupyter Notebook。此操作等同于在终端或 Anaconda Prompt 中执行 jupyter notebook

A.6 Jupyter Notebook 的基础知识

Jupyter Notebook 是一个用于 Python 的交互式开发环境,由一个或多个单元格组成,每个单元格包含 Python 代码或 Markdown。Markdown 是一种文本格式化标准,我们可以用它来在笔记本中添加标题、文本段落、项目符号列表、嵌入图像等。我们用 Python 编写我们的逻辑,用 Markdown 组织我们的思想。在阅读本书的过程中,请随时使用 Markdown 来记录材料。Markdown 的完整文档可在 daringfireball.net/projects/markdown/syntax 找到。

在 Jupyter 启动屏幕上,点击右侧菜单中的新建按钮,然后选择 Python 3 来创建一个新的 Notebook(图 A.13)。

图片

图 A.13 创建 Jupyter Notebook

要给 Notebook 命名,点击顶部的“未命名”文本,并在对话框中输入一个名称。Jupyter Notebook 使用 .ipynb 扩展名保存其文件,这是 IPython Notebooks 的简称,是 Jupyter Notebooks 的前身。你可以导航回 Jupyter Notebook 选项卡,查看目录中的新 .ipynb 文件。

Notebook 运行在两种模式:命令和编辑。当单元格被选中时点击单元格或按 Enter 键将触发编辑模式。Jupyter 会用绿色边框突出显示单元格。在编辑模式下,Jupyter 会逐字解释你的键盘输入。我们使用此模式在选定的单元格中输入字符。图 A.14 显示了编辑模式下的一个示例 Jupyter 单元格。

图片

图 A.14 编辑模式下的空 Jupyter Notebook 单元格

在笔记本的导航菜单下方,你可以找到一个用于常见快捷键的工具栏。工具栏右端的下拉菜单显示了当前单元格的类型。点击下拉菜单以显示可用单元格选项的列表,并选择代码或 Markdown 来将单元格更改为该类型(图 A.15)。

图 A.15 更改 Jupyter Notebook 单元格的类型

Jupyter Notebook 最好的特性之一是其试错式开发方法。我们在 代码 单元格中输入 Python 代码然后执行它。Jupyter 在单元格下方输出结果。我们检查结果是否与预期相符,并继续这个过程。这种方法鼓励积极实验;我们总是通过键盘敲击就能看到代码行带来的差异。

让我们执行一些基本的 Python 代码。在笔记本的第一个单元格中输入以下数学表达式,然后点击工具栏上的运行按钮来执行它:

In  [1]: 1 + 1

Out [1]: 2

代码左侧的框(在先前的例子中显示数字 1)标记了单元格相对于 Jupyter Notebook 的启动或重启的执行顺序。你可以按任何顺序执行单元格,并且可以多次执行相同的单元格。

随着你阅读这本书,我鼓励你通过在 Jupyter 单元格中执行不同的代码片段进行实验。因此,如果你的执行次数与文本中的不一致是正常的。

如果单元格包含多行代码,Jupyter 将输出最后一个表达式的评估。请注意,Python 仍然会运行单元格中的所有代码;我们只看到最后一个表达式。

In  [2]: 1 + 1
         3 + 2

Out [2]: 5

解释器 是解析你的 Python 源代码并执行它的软件。Jupyter Notebook 依赖于 IPython(交互式 Python),这是一个增强的解释器,具有提高开发者生产力的额外功能。例如,你可以使用 Tab 键来显示任何 Python 对象上的可用方法和属性。下一个例子显示了 Python 字符串上的可用方法。输入任何字符串和一个点;然后按 Tab 来查看对话框。图 A.16 展示了一个字符串的例子。如果你不熟悉 Python 的核心数据结构,请参阅附录 B 以获得对语言的全面介绍。

图 A.16 Jupyter Notebook 的自动完成功能

你可以在 代码 单元格中输入任何数量的 Python 代码,但最好保持单元格的大小合理,以提高可读性和理解性。如果你的逻辑很复杂,可以将操作拆分到几个单元格中。

你可以使用两种键盘快捷键之一在 Jupyter Notebook 中执行单元格。按 Shift-Enter 来执行单元格并将焦点移动到下一个单元格,按 Ctrl-Enter 来执行单元格并保持焦点在原始单元格上。练习重新执行前两个单元格以查看这种差异的实际效果。

按下 Esc 键以激活命令模式,这是笔记本的管理模式。在此模式下可用的操作更全局;它们影响整个笔记本而不是单个特定单元格。在此模式下,键盘字符作为快捷键。以下是当笔记本处于命令模式时一些有用的键盘快捷键:

键盘快捷键 描述
上箭头键和下箭头键 在笔记本单元格之间导航。
a 在所选单元格上方创建一个新单元格。
b 在所选单元格下方创建一个新单元格。
c 复制单元格的内容。
x 剪切单元格的内容。
v 将复制的或剪切的单元格粘贴到所选单元格下方的单元格中。
d+d 删除单元格。
z 撤销删除操作。
y 将单元格类型更改为“代码”。
m 将单元格类型更改为Markdown
h 显示帮助菜单,其中包含完整的键盘快捷键列表。
Command-S(macOS)或 Ctrl-S(Windows) 保存笔记本。请注意,Jupyter Notebook 还具有自动保存功能。

为了清除笔记本内存中的所有内容,请从顶层菜单中选择“内核”,然后选择“重启”。还有其他选项可供清除单元格输出和重新运行笔记本中的所有单元格。

假设我们今天已经玩够了笔记本,决定是时候退出。即使我们关闭了浏览器标签页,笔记本也会在后台继续运行。要关闭它,导航到 Jupyter 启动屏幕顶部菜单中的“运行”标签,然后点击笔记本旁边的“关闭”按钮(图 A.17)。

图片

图 A.17 关闭 Jupyter Notebook

关闭所有笔记本后,我们必须终止 Jupyter Notebook 应用程序。关闭带有 Jupyter 应用程序的浏览器标签页。在终端或 Anaconda 提示符中,按 Ctrl+C 两次以终止本地 Jupyter 服务器。

到目前为止,你已经准备好开始在 Jupyter 中编写 Python 和 pandas 代码了。祝你好运!

附录 B. Python 入门教程

pandas 库建立在 Python 之上,Python 是一种流行的编程语言,由荷兰开发者 Guido van Rossum 于 1991 年首次发布。(也称为)是一组功能工具箱,它扩展了编程语言的核心功能。库通过提供解决方案来加速开发者的生产力,例如数据库连接、代码质量和测试。大多数 Python 项目都使用库。毕竟,如果有人已经解决了问题,为什么还要从头开始解决问题呢?Python 包索引(PyPi)是一个集中在线仓库,有超过 300,000 个库可供下载。Pandas 是这 300,000 个库之一;它实现了复杂的数据结构,擅长存储和操作多维数据。在我们探索 pandas 为 Python 添加了什么之前,了解基础语言中有什么是很重要的。

Python 是一种面向对象编程(OOP)语言。面向对象范式将软件程序视为一组相互通信的对象。对象是一个数字数据结构,用于存储信息并提供访问和操作该信息的方式。每个对象都有其存在的原因或目的。我们可以将每个对象想象成戏剧中的一个演员,而软件程序则是一场表演。

将对象视为数字构建块是一种有用的思考方式。以电子表格软件如 Excel 为例。作为用户,我们可以区分工作簿、工作表和单元格之间的差异。工作簿包含工作表,工作表包含单元格,单元格包含值。我们将这三个实体视为三个不同的业务逻辑容器,每个容器都有指定的职责,并且我们以不同的方式与之交互。当构建面向对象的计算机程序时,开发者会以同样的方式思考,识别并构建程序运行所需的“块”。

在 Python 社区中,你经常会听到“万物皆对象”的表达。这个说法意味着该语言实现了所有数据类型,即使是像数字和文本这样的简单类型,也作为对象实现。像 pandas 这样的库添加了新的对象集合——一组额外的构建块——到语言中。

作为一名从数据分析师转变为软件工程师的人,我见证了行业内许多角色的 Python 熟练度要求。我可以从经验中提出,你不需要成为一名高级程序员就能有效地使用 pandas。然而,对 Python 核心机制的基本理解将显著加快你掌握该库的速度。本附录强调了你需要了解的关键语言要素,以便取得成功。

B.1 简单数据类型

数据有多种类型。例如,整数 5 与十进制数 8.46 的类型不同。5 和 8.46 都与文本值"Bob"不同。

让我们从探索 Python 中内置的核心数据类型开始。确保您已安装 Anaconda 发行版,并设置了一个包含 Jupyter Notebook 编码环境的 conda 环境。如果您需要帮助,请参阅附录 A 中的安装说明。激活为本书创建的 conda 环境,执行命令 jupyter notebook,并创建一个新的 Notebook。

在我们开始之前的一个快速提示:在 Python 中,井号符号(#)创建一个注释。一个注释是 Python 在处理代码时忽略的文本行。开发者使用注释为他们的代码提供内联文档。以下是一个示例:

# Adds two numbers together
1 + 1

我们也可以在一段代码后面添加注释。Python 会忽略井号符号之后的所有内容。该行的其余部分会正常执行:

1 + 1 # Adds two numbers together

虽然前面的例子计算结果为 2,但下一个例子不会产生任何输出。注释有效地禁用了该行,因此 Python 忽略了加法操作:

# 1 + 1

我在本书中的代码单元中使用了注释,以提供对当前操作补充说明。您不需要将注释复制到您的 Jupyter Notebook 中。

B.1.1 数字

一个整数是一个整数;它没有分数或小数部分。20 是一个例子:

In  [1] 20

Out [1] 20

一个整数可以是任何正数、负数或零。负数前面有一个负号(-):

In  [2] -13

Out [2] -13

一个浮点数(俗称浮点)是一个带有分数或小数部分的数字。我们使用点来声明小数点。7.349 是一个浮点数的例子:

In  [3] 7.349

Out [3] 7.349

整数和浮点数在 Python 中代表不同的数据类型,或者说代表不同的对象。通过查找小数点的存在来区分这两个。例如,5.0 是一个浮点对象,而 5 是一个整数对象。

B.1.2 字符串

一个字符串是由零个或多个文本字符组成的集合。我们通过将一段文本包裹在单引号、双引号或三重引号中来声明一个字符串。这三个选项之间有区别,但对于初学者来说并不重要。本书中我们将坚持使用双引号。Jupyter Notebook 对三种语法选项的输出是相同的:

In  [4] 'Good morning'

Out [4] 'Good morning'

In  [5] "Good afternoon"

Out [5] 'Good afternoon'

In  [6] """Good night"""

Out [6] 'Good night'

字符串不仅限于字母字符;它们可以包括数字、空格和符号。考虑下一个例子,它包括七个字母字符、一个美元符号、两个数字、一个空格和一个感叹号:

In  [7] "$15 dollars!"

Out [7] '$15 dollars!'

通过引号的存在来识别字符串。许多初学者对像 "5" 这样的值感到困惑,这是一个包含单个数字字符的字符串。"5" 不是一个整数。

一个空字符串没有字符。我们通过在两个引号之间不放置任何内容来创建它:

In  [8] ""

Out [8] ''

字符串的长度指的是其字符的数量。例如,字符串 "Monkey business" 的长度为 15 个字符;其中 Monkey 有六个字符,business 有八个字符,两个单词之间有一个空格。

Python 根据每个字符串字符在行中的顺序为其分配一个数字。这个数字称为索引,它从 0 开始计数。在字符串 "car" 中,

  • "c" 位于索引位置 0。

  • "a" 位于索引位置 1。

  • "r" 位于索引位置 2。

字符串的最后一个索引位置总是比其长度少 1。字符串 "car" 的长度为 3,因此其最后一个索引位置是 2。基于 0 的索引可能会让新开发者感到困惑;这是一个难以进行的心智转变,因为我们从小学就被教导从 1 开始计数。

我们可以通过索引位置提取字符串中的任何字符。在字符串后,输入一对带有索引值的方括号。下一个示例提取了 "Python" 中的 "h" 字符。"h" 字符是序列中的第四个字符,因此它的索引是 3:

In  [9] "Python"[3]

Out [9] 'h'

要从字符串的末尾提取,在方括号内提供一个负值。-1 的值提取最后一个字符,-2 提取倒数第二个字符,依此类推。下一个示例针对 Python 中的第四个倒数字符,即 "t"

In  [10] "Python"[-4]

Out [10] 't'

在前面的示例中,"Python"[2] 会产生相同的 "t" 输出。

我们可以使用特殊的语法从字符串中提取多个字符。这个过程称为切片。在方括号内放置两个数字,用冒号分隔。左侧的值设置起始索引。右侧的值设置最终索引。起始索引是包含的;Python 包含该索引处的字符。结束索引是不包含的;Python 不包含该索引处的字符。我知道这很棘手。

下一个示例从索引位置 2(包含)到索引位置 5(不包含)提取所有字符。切片包括索引位置 2 的 "t"、索引位置 3 的 "h" 和索引位置 4 的 "o"

In  [11] "Python"[2:5]

Out [11] 'tho'

如果 0 是起始索引,我们可以从方括号中移除它并得到相同的结果。选择最适合你的语法选项:

In  [12] # The two lines below are equivalent
         "Python"[0:4]
         "Python"[:4]

Out [12] 'Pyth'

这里还有一个快捷方式:要从一个索引提取到字符串的末尾,移除结束索引。以下示例显示了两种从 "h"(索引 3)提取到 "Python" 字符串末尾字符的方法:

In  [13] # The two lines below are equivalent
         "Python"[3:6]
         "Python"[3:]

Out [13] 'hon'

我们还可以移除两个数字。单个冒号告诉 Python “从开头到结尾。”结果是字符串的副本:

In  [14] "Python"[:]

Out [14] 'Python'

我们可以在字符串切片中混合使用正索引和负索引位置。让我们从索引 1("y") 提取到最后一个字符("n"):

In  [15] "Python"[1:-1]

Out [15] 'ytho'

我们还可以传递一个可选的第三个数字来设置步长间隔——两个索引位置之间的间隔。下一个示例以 2 的间隔从索引位置 0(包含)到 6(不包含)提取字符。这个切片包括索引位置 0 的 "P"、索引位置 2 的 "t" 和索引位置 4 的 "o"

In  [16] "Python"[0:6:2]

Out [16] 'Pto'

这里有一个酷技巧:我们可以将-1 作为第三个数字传递,以便从列表的末尾向前推进到开头。结果是反转的字符串:

In  [17] "Python"[::-1]

Out [17] 'nohtyP'

切片对于从较大的字符串中提取文本片段非常有用——这是我们在第六章中详细讨论的主题。

B.1.3 布尔

布尔 数据类型代表逻辑上的真值概念。它只能有两个值之一:TrueFalse。布尔数据类型是以英国数学家和哲学家乔治·布尔的名字命名的。它通常表示一种非此即彼的关系:是或否,开或关,有效或无效,活跃或非活跃,等等。

In  [18] True

Out [18] True

In  [19] False

Out [19] False

我们通常通过计算或比较得到布尔数据类型,我们将在 B.2.2 节中看到。

B.1.4 None 对象

None 对象代表无或值的缺失。与布尔类型一样,这是一个难以理解的概念,因为它比整数等具体值更抽象。

假设我们决定测量我们城镇一周的每日气温,但忘记了在星期五进行测量。七天的气温中有六天是整数。我们如何记录缺失的那一天的气温?我们可能会输入“缺失”、“未知”或“null”。在 Python 中,None 对象模拟了同样的概念。语言需要某种东西来传达值的缺失。它需要一个对象来代表并宣布值是缺失的、不存在的或不需要的。当我们执行包含 None 的单元格时,Jupyter Notebook 不会输出任何内容:

In  [20] None

与布尔类型一样,我们通常会得到一个 None 值,而不是手动创建它。随着我们阅读本书,我们将更详细地探讨该对象。

B.2 运算符

运算符 是执行操作的符号。一个经典的例子来自小学的加法运算符:加号 (+)。运算符作用的值称为 操作数。在表达式 3 + 5 中,

    • 是运算符。
  • 3 和 5 是操作数。

在本节中,我们将探讨 Python 中内置的各种数学和逻辑运算符。

B.2.1 数学运算符

让我们写出介绍中的数学表达式。Jupyter 将直接在单元格下方输出计算结果:

In  [21] 3 + 5

Out [21] 8

在运算符两侧添加空格是一种惯例,可以使代码更容易阅读。接下来的两个示例说明了减法 (-) 和乘法 (*):

In  [22] 3 - 5

Out [22] -2

In  [23] 3 * 5

Out [23] 15

** 是指数运算符。下一个示例将 3 提到 5 次幂(3 自身乘以 5 次):

In  [24] 3 ** 5

Out [24] 243

/ 符号执行除法。下一个示例将 3 除以 5:

In  [25] 3 / 5

Out [25] 0.6

在数学术语中, 是一个数除以另一个数的结果。使用 / 运算符的除法始终返回一个浮点数商,即使除数可以整除被除数:

In  [26] 18 / 6

Out [26] 3.0

地板除法 是一种替代的除法类型,它会从商中移除小数余数。它需要两个正斜杠 (//),并返回一个整数商。下一个示例演示了这两个运算符之间的区别:

In  [27] 8 / 3

Out [27] 2.6666666666666665

In  [28] 8 // 3

Out [28] 2

取模 运算符 (%) 返回除法的结果余数。当 5 除以 3 时,2 是余数:

In  [29] 5 % 3

Out [29] 2

我们还可以使用加法和乘法运算符与字符串。加号用于连接两个字符串。这个过程的技术术语是 连接

In  [30] "race" + "car"

Out [30] 'racecar'

乘号会重复字符串给定次数:

In  [31] "Mahi" * 2

Out [31] 'MahiMahi'

对象的类型决定了它支持的运算符和操作。例如,我们可以除以整数,但不能除以字符串。面向对象编程的主要技能是识别你正在处理的对象以及它可以执行的操作。

我们可以将一个字符串连接到另一个字符串,我们也可以将一个数字加到另一个数字上。但当我们尝试将一个字符串和一个数字相加时会发生什么?

In  [32] 3 + "5"

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-9-d4e36ca990f8> in <module>
----> 1 3 + "5"

TypeError: unsupported operand type(s) for +: 'int' and 'str'

哎呀。这是我们第一次接触到 Python 错误——语言中内置的几十种错误之一。错误的技术名称是 异常。像 Python 中的其他一切一样,异常也是一个对象。每当我们在语法或逻辑上犯错误时,Jupyter Notebook 会显示一个分析,其中包括错误的名称和触发它的行号。技术术语 raise 常用来表示 Python 遇到了异常。我们可以这样说:“我尝试将一个数字和一个字符串相加,Python 抛出了一个异常。”

当我们在操作中使用错误的数据类型时,Python 会抛出一个 TypeError 异常。在上面的示例中,Python 观察到一个数字和一个加号,并假设将跟随另一个数字。然而,它接收到一个字符串,它无法将字符串添加到整数中。我们将在 B.4.1 节中看到如何将整数转换为字符串(反之亦然)。

B.2.2 等于和不等于运算符

Python 认为两个对象相等,如果它们持有相同的值。我们可以通过将它们放在等于运算符的两侧来比较两个对象的相等性(==)。如果两个对象相等,则运算符返回 True。提醒一下,True 是一个布尔值。

In  [33] 10 == 10

Out [33] True

小心:等于运算符有两个等号。Python 为一个完全不同的操作保留了单个等号,我们将在 B.3 节中介绍。

如果两个对象不相等,等于运算符返回 FalseTrueFalse 是布尔值的有效值:

In  [34] 10 == 20

Out [34] False

这里有一些等于运算符与字符串的示例:

In  [35] "Hello" == "Hello"

Out [35] True

In  [36] "Hello" == "Goodbye"

Out [36] False

在比较两个字符串时,大小写敏感很重要。在下一个示例中,一个字符串以大写 "H" 开头,而另一个以小写 "h" 开头,因此 Python 认为这两个字符串不相等:

In  [37] "Hello" == "hello"

Out [37] False

不等于运算符(!=)是等于运算符的逆运算;如果两个对象不相等,则返回 True。例如,10 不等于 20:

In  [38] 10 != 20

Out [38] True

同样,字符串 "Hello" 不等于字符串 "Goodbye"

In  [39] "Hello" != "Goodbye"

Out [39] True

如果两个对象相等,不等于运算符返回 False

In  [40] 10 != 10

Out [40] False

In  [41] "Hello" != "Hello"

Out [41] False

Python 支持数字之间的数学比较。< 运算符检查左侧的操作数是否小于右侧的操作数。以下示例检查 -5 是否小于 3:

In  [42] -5 < 3

Out [42] True

>运算符检查左侧的操作数是否大于右侧的操作数。下一个示例评估 5 是否大于 7;结果是False

In  [43] 5 > 7

Out [43] False

<=操作符检查左侧操作数是否小于或等于右侧操作数。在这里,我们检查 11 是否小于或等于 11:

In  [44] 11 <= 11

Out [44] True

相补的>=操作符检查左侧操作数是否大于或等于右侧操作数。下一个示例检查 4 是否大于或等于 5:

In  [45] 4 >= 5

Out [45] False

Pandas 使我们能够将这些比较应用于整个数据列,这是我们在第五章中讨论的主题。

B.3 变量

变量是我们分配给对象的名称;我们可以将其与房屋地址进行比较,因为它是标签、引用和标识符。变量名应该是清晰且描述性的,描述对象存储的数据以及它在我们的应用程序中扮演的用途。例如,revenues_for_quarter4rr4更好的变量名。

我们使用赋值运算符(单个等号=)将变量赋给对象。下一个示例将四个变量(nameagehigh_school_gpais_handsome)赋给四种不同的数据类型(字符串、整数、浮点数和布尔值):

In  [46] name = "Boris"
         age = 28
         high_school_gpa = 3.7
         is_handsome = True

在 Jupyter Notebook 中,对带有变量赋值的单元格执行不会产生任何输出,但之后我们可以在笔记本中的任何单元格中使用该变量。变量是它所持有值的替代品:

In  [47] name

Out [47] 'Boris'

变量名必须以字母或下划线开头。在第一个字母之后,它只能包含字母、数字或下划线。

正如它们的名称所暗示的,变量可以存储在程序执行过程中变化的值。让我们将age变量重新赋值为新的值35。在我们执行单元格之后,age变量对其先前值28的引用将丢失:

In  [48] age = 35
         age

Out [48] 35

我们可以在赋值运算符的两侧使用相同的变量。Python 始终首先评估等号右侧的值。在下一个示例中,Python 将单元格执行开始时age的值35加到10上。得到的总和45被保存到age变量中:

In  [49] age = age + 10
         age

Out [49] 45

Python 是一种动态类型语言,这意味着变量对数据类型一无所知。变量是程序中任何对象的占位符名称。只有对象知道它的数据类型。因此,我们可以将变量从一种类型重新赋值到另一种类型。下一个示例将high_school_gpa变量从其原始的浮点值3.7重新赋值为字符串"A+"

In  [50] high_school_gpa = "A+"

当程序中不存在变量时,Python 会引发NameError异常:

In  [51] last_name

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-5-e1aeda7b4fde> in <module>
----> 1 last_name

NameError: name 'last_name' is not defined

当你误拼变量名时,通常会遇到NameError异常。这种异常不必害怕;更正拼写,然后再次执行单元格。

B.4 函数

一个 函数 是由一个或多个步骤组成的程序。将函数想象成编程语言中的烹饪食谱——一系列产生一致结果的指令。函数使软件具有可重用性。因为函数从开始到结束捕获了一部分业务逻辑,所以当我们需要多次执行相同的操作时,我们可以重用它。

我们声明一个函数然后执行它。在声明中,我们写下函数应该采取的步骤。在执行中,我们运行函数。按照我们的烹饪类比,声明一个函数相当于写下食谱,执行一个函数相当于烹饪食谱。执行函数的技术术语是 调用调用

B.4.1 参数和返回值

Python 随带提供了超过 65 个内置函数。我们也可以声明我们自己的自定义函数。让我们深入一个例子。内置的 len 函数返回给定对象的长度。长度的概念因数据类型而异;对于字符串,它是字符的计数。

我们通过输入函数名和一对开闭括号来调用一个函数。就像烹饪食谱可以接受配料一样,函数调用可以接受称为 参数 的输入。我们按顺序在括号内传递参数,参数之间用逗号分隔。

len 函数期望一个参数:它应该计算长度的对象。下一个示例将 "Python is fun" 字符串参数传递给函数:

In  [52] len("Python is fun")

Out [52] 13

烹饪食谱产生最终输出——一顿饭。同样,Python 函数产生一个称为 返回值 的最终输出。在上一个示例中,len 是被调用的函数,"Python is fun" 是它的单个参数,13 是返回值。

就这些了!函数是一个可以调用零个或多个参数并产生返回值的程序。

这里是 Python 中三个更受欢迎的内置函数:

  • int,它将它的参数转换为整数

  • float,它将它的参数转换为浮点数

  • str,它将它的参数转换为字符串

下三个示例展示了这些函数的实际应用。第一个示例使用 "20" 字符串参数调用 int 函数,并产生返回值 20。你能识别剩余两个函数的参数和返回值吗?

In  [53] int("20")

Out [53] 20

In  [54] float("14.3")

Out [54] 14.3

In  [55] str(5)

Out [55] '5'

这里还有一个常见的错误:当函数接收到正确数据类型但不适用的值时,Python 会抛出一个 ValueError 异常。在下一个示例中,int 函数接收了一个字符串(一个合适的类型),但这个字符串无法从中提取出整数:

In  [56] int("xyz")

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-6-ed77017b9e49> in <module>
----> 1 int("xyz")

ValueError: invalid literal for int() with base 10: 'xyz'

另一个流行的内置函数是 print,它将文本输出到屏幕。它接受任意数量的参数。当我们在程序执行过程中想要观察变量的值时,该函数通常非常有用。下一个示例四次调用 print 函数,使用 value 变量,其值变化了几次:

In  [57] value = 10
         print(value)

         value = value - 3
         print(value)

         value = value * 4
         print(value)

         value = value / 2
         print(value)

Out [57] 10
         7
         28
         14.0

如果一个函数接受多个参数,我们必须用逗号分隔每两个后续参数。开发者经常在逗号后添加一个空格以提高可读性。

当我们向 print 函数传递多个参数时,它会按顺序输出所有参数。在下一个示例中,请注意 Python 使用空格分隔三个打印的元素:

In  [58] print("Cherry", "Strawberry", "Key Lime")

Out [58] Cherry Strawberry Key Lime

参数是为预期函数参数赋予的名称。调用中的每个参数都对应一个参数。在之前的示例中,我们向 print 函数传递了参数,但没有指定它们的参数。

对于某些参数,我们必须明确写出参数名称。例如,print 函数的 sep(分隔符)参数自定义 Python 在每个打印值之间插入的字符串。如果我们想传递一个自定义参数,我们必须明确写出 sep 参数。我们使用等号将参数分配给函数的关键字参数。下一个示例输出相同的三个字符串,但指示 print 函数用感叹号分隔它们:

In  [59] print("Cherry", "Strawberry", "Key Lime", sep = "!")

Out [59] Cherry!Strawberry!Key Lime

让我们回到上一个示例之前。为什么三个值之间用空格隔开?

默认参数是一个后备值,当函数调用没有明确提供一个值时,Python 会将它传递给参数。print 函数的 sep 参数有一个默认参数 " "。如果我们调用 print 函数而没有为 sep 参数提供参数,Python 将自动传递一个包含一个空格的字符串。以下两行代码产生相同的输出:

In  [60] # The two lines below are equivalent
         print("Cherry", "Strawberry", "Key Lime")
         print("Cherry", "Strawberry", "Key Lime", sep=" ")

Out [60] Cherry Strawberry Key Lime
         Cherry Strawberry Key Lime

我们称像 sep 这样的参数为关键字参数。在传递参数时,我们必须写出它们的特定参数名称。Python 要求我们在传递顺序参数之后传递关键字参数。以下是一个 print 函数调用的另一个示例,它向 sep 参数传递了不同的字符串参数:

In  [61] print("Cherry", "Strawberry", "Key Lime", sep="*!*")

Out [61] Cherry*!*Strawberry*!*Key Lime

print 函数的 end 参数自定义 Python 添加到所有输出末尾的字符串。该参数的默认参数是 "\n",这是一个 Python 识别为换行符的特殊字符。在下一个示例中,我们明确地将相同的 "\n" 参数传递给 end 参数:

In  [62] print("Cherry", "Strawberry", "Key Lime", end="\n")
         print("Peach Cobbler")

Out [62] Cherry Strawberry Key Lime
         Peach Cobbler

我们可以在函数调用中传递多个关键字参数。技术规则仍然适用:用逗号分隔每两个参数。下一个示例两次调用了 print 函数。第一次调用用 "!" 分隔其三个参数,并以 "***" 结束输出。因为第一次调用没有强制换行,所以第二次调用的输出从第一个调用结束的地方继续:

In  [63] print("Cherry", "Strawberry", "Key Lime", sep="!", end="***")
         print("Peach Cobbler")

Out [63] Cherry!Strawberry!Key Lime***Peach Cobbler

请花点时间思考一下前面示例中的代码格式。长行代码可能难以阅读,尤其是当我们把多个参数放在一起时。Python 社区倾向于几种格式化解决方案。一个选项是将所有参数放在单独的一行上:

In  [64] print(
             "Cherry", "Strawberry", "Key Lime", sep="!", end="***"
         )

Out [64] Cherry!Strawberry!Key Lime***

另一个选项是在参数之间添加换行符:

In  [65] print(
             "Cherry",
             "Strawberry",
             "Key Lime",
             sep="!",
             end="***",
         )

Out [65] Cherry!Strawberry!Key Lime***

这三个代码示例在技术上都是有效的。Python 代码的格式化有多种方式。我在整本书中使用了多种格式化选项。我的最终目标是可读性。你不必遵循我使用的格式化约定。我会尽我所能说明哪些差异是技术性的,哪些是美学的。

B.4.2 自定义函数

我们可以在程序中声明自定义函数。函数的目标是将一个独特的业务逻辑捕获在单一、可重用的过程中。软件工程领域的一个常见格言是 DRY,它是 不要重复自己 的缩写。这个缩写是一个警告,表明重复相同的逻辑或行为可能导致程序不稳定。你重复代码的地方越多,如果需求发生变化,你需要编辑的地方就越多。函数解决了 DRY 问题。

让我们来看一个示例。假设我们是气象学家,正在处理天气数据。我们的工作要求我们在程序中将温度从华氏度转换为摄氏度。转换有一个简单、一致的公式。编写一个函数来将 一个 温度从华氏度转换为摄氏度是个好主意,因为我们可以隔离转换逻辑并在需要时重复使用它。

我们用一个 def 关键字开始函数定义。我们在 def 后面跟函数的名称,一对开括号和闭括号,以及一个冒号。多单词的函数名和变量名遵循 snake_case 命名约定。这个约定将每两个单词用下划线分隔,使得名称看起来像蛇。让我们称我们的函数为 convert_to_fahrenheit

def convert_to_fahrenheit():

为了复习,一个 参数 是一个预期函数参数的名称。我们希望 convert_to_fahrenheit 函数接受一个单一参数:摄氏温度。让我们称这个参数为 celsius_temp

def convert_to_fahrenheit(celsius_temp):

如果我们在声明函数时定义了一个参数,那么在调用它时必须为该参数传递一个参数。因此,每次运行 convert_to_fahrenheit 时,我们都必须为 celsius_temp 提供一个值。

我们下一步是定义函数的功能。我们在函数体中声明函数的步骤,这是位于其名称下方的一个缩进代码部分。Python 使用缩进来建立程序中构造之间的关系。函数体是 的一个例子,它是嵌套在另一个代码部分中的代码段。根据 PEP-8¹,Python 社区的风格指南,我们应该使用四个空格来缩进块中的每一行:

def convert_to_fahrenheit(celsius_temp):
    # This indented line belongs to the function
    # So does this indented line

# This line is not indented, so it does not belong to convert_to_fahrenheit

我们可以在函数体中使用函数的参数。在我们的例子中,我们可以在 convert_to_fahrenheit 函数的任何地方使用 celsius_temp 参数。

我们可以在函数体中声明变量。这些变量被称为 局部变量,因为它们绑定到函数执行的范围内。Python 在函数运行完成后立即将局部变量从内存中移除。

让我们写出转换的逻辑!将摄氏温度转换为华氏温度的公式是将它乘以 9/5 并加 32:

def convert_to_fahrenheit(celsius_temp):
    first_step = celsius_temp * (9 / 5)
    fahrenheit_temperature = first_step + 32

在这个阶段,我们的函数正确地计算了华氏温度,但它并没有将评估结果发送回主程序。我们需要使用 return 关键字来标记华氏温度为函数的最终输出。我们将它返回到外部世界:

In  [66] def convert_to_fahrenheit(celsius_temp):
             first_step = celsius_temp * (9 / 5)
             fahrenheit_temperature = first_step + 32
             return fahrenheit_temperature

我们的功能已经完成,现在让我们来测试它!我们使用一对括号来调用自定义函数,这与我们用于 Python 内置函数的语法相同。下一个示例使用 10 作为样本参数调用了 convert_to_fahrenheit 函数。Python 将 celsius_temp 参数设置为 10 并运行函数体。该函数返回值为 50.0

In  [67] convert_to_fahrenheit(10)

Out [67] 50.0

我们可以提供关键字参数而不是位置参数。下一个示例明确写出了 celsius_temp 参数的名称。以下代码与前面的代码等效:

In  [68] convert_to_fahrenheit(celsius_temp = 10)

Out [68] 50.0

虽然它们不是必需的,但关键字参数有助于使我们的程序更清晰。前面的示例更好地说明了 convert_to_fahrenheit 函数的输入代表什么。

B.5 模块

一个 模块 是一个单独的 Python 文件。Python 的 标准库 是一个包含超过 250 个模块的语言集合,这些模块内置到语言中以加速生产力。这些模块帮助进行技术操作,如数学、音频分析和 URL 请求。为了减少程序的内存消耗,Python 默认不会加载这些模块。当我们的程序需要时,我们必须手动导入我们想要的特定模块。

导入内置模块和外部包的语法是相同的:输入 import 关键字,然后是模块或包的名称。让我们导入 Python 的 datetime 模块,它帮助我们处理日期和时间:

In  [69] import datetime

别名 是导入的替代名称——一个我们可以分配给模块的快捷方式,这样在引用它时就不必写出其完整名称。别名实际上取决于我们,但某些昵称已经在 Python 开发者中确立了自己作为最受欢迎的。例如,datetime 模块的流行别名是 dt。我们使用 as 关键字来分配别名:

In  [70] import datetime as dt

现在我们可以用 dt 而不是 datetime 来引用模块。

B.6 类和对象

我们迄今为止探索的所有数据类型——整数、浮点数、布尔值、字符串、异常、函数,甚至模块——都是对象。对象 是一种数字数据结构,用于存储、访问和操作一种类型的数据。

是创建对象的蓝图。将其视为一个图表或模板,Python 从中构建对象。

我们称从类构建的对象为该类的 实例。从类创建对象的行为称为 实例化

Python 的内置 type 函数返回我们作为参数传递给它的对象的类。下一个例子两次调用 type 函数,使用两个不同的字符串:"peanut butter""jelly"。尽管它们的内容不同,但这两个字符串是由相同的蓝图、相同的类、str 类构建的。它们都是字符串:

In  [71] type("peanut butter")

Out [71] str

In  [72] type("jelly")

Out [72] str

这些例子相当简单。当我们不确定正在处理什么类型的对象时,type 函数很有帮助。如果我们调用一个自定义函数并且不确定它返回什么类型的对象,我们可以将它的返回值传递给 type 来找出。

字面量 是创建从类创建对象的简写语法。我们迄今为止遇到的一个例子是双引号,它创建字符串("hello")。对于更复杂的对象,我们需要使用不同的创建过程。

在 B.5 节中导入的 datetime 模块有一个 date 类,它模拟时间中的日期。假设我们正在尝试将列奥纳多·达·芬奇的生日,1452 年 4 月 15 日,表示为一个 date 对象。

要从类创建一个实例,写上类名后跟一对括号。例如,date() 创建了一个来自 date 类的 date 对象。语法与调用函数相同。在实例化对象时,我们有时可以向构造函数传递参数,即创建对象的函数。date 构造函数的前三个参数代表 date 对象将包含的年、月和日。这三个参数是必需的:

In  [73] da_vinci_birthday = dt.date(1452, 4, 15)
         da_vinci_birthday

Out [73] datetime.date(1452, 4, 15)

现在我们有一个 da_vinci_birthday 变量,它包含一个代表 1452 年 4 月 15 日的 date 对象。

B.7 属性和方法

属性 是属于对象、特征或细节的内部数据片段,它揭示了关于对象的信息。我们使用点符号来访问对象的属性。一个 date 对象上的三个示例属性是 daymonthyear

In  [74] da_vinci_birthday.day

Out [74] 15

In  [75] da_vinci_birthday.month

Out [75] 4

In  [76] da_vinci_birthday.year

Out [76] 1452

方法是我们可以向对象发出的动作或命令。将方法视为属于对象的函数。属性构成了对象的状态,而方法代表了对象的行为。像函数一样,方法可以接受参数并产生返回值。

我们在方法名称后面使用一对括号来调用方法。确保在对象和方法名称之间添加一个点。date对象的一个示例方法是一个weekdayweekday方法返回日期的星期几作为整数。0表示星期日,6表示星期六:

In  [77] da_vinci_birthday.weekday()

Out [77] 3

莱昂纳多出生于星期三!

weekday等方法的简便性和可重用性是date对象存在的原因。想象一下,如果用文本字符串来模拟日期逻辑会有多困难。想象一下,如果每个开发者都构建他们自己的定制解决方案。哎呀。Python 的开发者预计用户将需要处理日期,因此他们构建了一个可重用的date类来模拟这个现实世界的结构。

关键要点是 Python 标准库为开发者提供了许多实用类和函数来解决常见问题。然而,随着程序复杂性的增加,仅使用 Python 的核心对象来模拟现实世界思想变得困难。为了解决这个问题,开发者向语言中添加自定义对象。这些对象模拟特定领域的业务逻辑。开发者将这些对象打包成库。这就是 pandas 的全部:一组用于解决数据分析领域特定问题的额外类。

B.8 字符串方法

字符串对象有一套自己的方法。这里有一些例子。

upper方法返回一个所有字符都为大写的新的字符串:

In  [78] "Hello".upper()

Out [78] "HELLO"

我们可以在变量上调用方法。回想一下,变量是对象的占位符名称。Python 会将变量替换为它引用的对象。下一个示例在greeting变量引用的字符串上调用upper方法。输出与前面的代码示例相同:

In  [79] greeting = "Hello"
         greeting.upper()

Out [79] "HELLO"

有两种类型的对象:可变和不可变。可变对象可以改变。不可变对象不能改变。字符串、数字和布尔值是不可变对象的例子;我们创建后不能修改它们。字符串"Hello"始终是字符串"Hello"。数字 5 始终是数字 5。

在前面的例子中,upper方法调用没有修改分配给greeting变量的原始"Hello"字符串。相反,方法调用返回了一个所有字母都为大写的新的字符串。我们可以输出greeting变量来确认字符保留了原始的大小写:

In  [80] greeting

Out [80] 'Hello'

字符串是不可变的,所以它的方法不会修改原始对象。我们将在 B.9 节开始探索一些可变对象。

相补的lower方法返回一个所有字符都转换为小写的新的字符串:

In  [81] "1611 BROADWAY".lower()

Out [81] '1611 broadway'

甚至还有一个 swapcase 方法,它返回一个新字符串,其中每个字符的大小写都被反转。大写字母变为小写,小写字母变为大写:

In  [82] "uPsIdE dOwN".swapcase()

Out [82] 'UpSiDe DoWn'

一个方法可以接受参数。让我们看看 replace 方法,它将所有子字符串的出现次数与指定的字符序列交换。该功能类似于文字处理程序中的查找和替换功能。replace 方法接受两个参数:

  • 要查找的子字符串

  • 要替换的值

下一个示例将所有 "S" 出现替换为 "$"

In  [83] "Sally Sells Seashells by the Seashore".replace("S", "$")

Out [83] '$ally $ells $eashells by the $eashore'

在这个例子中,

  • "Sally Sells Seashells by the Seashore" 是原始的字符串 对象

  • replace 是对字符串调用的 方法

  • "S" 是传递给 replace 方法调用的 第一个参数

  • "$" 是传递给 replace 方法调用的 第二个参数

  • "$ally $ells $eashells by the $eashore"replace 方法的 返回值

一个方法返回值的数据类型可以与原始对象不同。例如,isspace 方法作用于字符串,但返回一个布尔值。如果字符串仅由空格组成,则方法返回 True;否则,返回 False

In  [84] "  ".isspace()

Out [84] True

In  [85] "3 Amigos".isspace()

Out [85] False

字符串有一系列用于删除空白的方法。rstrip(右删除)方法从字符串的末尾删除空白:

In  [86] data = "    10/31/2019  "
         data.rstrip()

Out [86] '    10/31/2019'

lstrip(左删除)方法从字符串的开始删除空白:

In  [87] data.lstrip()

Out [87] '10/31/2019  '

strip 方法从字符串的两端删除空白:

In  [88] data.strip()

Out [88] '10/31/2019'

capitalize 方法将字符串的第一个字符大写。此方法通常在处理小写名称、地点或组织时非常有用:

In  [89] "robert".capitalize()

Out [89] 'Robert'

title 方法将字符串中每个单词的首字母大写,使用空格来标识每个单词的开始和结束位置:

In  [90] "once upon a time".title()

Out [90] 'Once Upon A Time'

我们可以在一行中连续调用多个方法。这种技术称为 方法链。在下一个示例中,lower 方法返回一个新的字符串对象,然后我们调用 title 方法。title 的返回值又是另一个新的字符串对象:

In  [91] "BENJAMIN FRANKLIN".lower().title()

Out [91] 'Benjamin Franklin'

in 关键字检查子字符串是否存在于另一个字符串中。在关键字之前输入要搜索的字符串,在关键字之后输入要搜索的字符串。操作返回一个布尔值:

In  [92] "tuna" in "fortunate"

Out [92] True

In  [93] "salmon" in "fortunate"

Out [93] False

startswith 方法检查子字符串是否存在于字符串的开头:

In  [94] "factory".startswith("fact")

Out [94] True

endswith 方法检查子字符串是否存在于字符串的末尾:

In  [95] "garage".endswith("rage")

Out [95] True

count 方法计算字符串中子字符串的出现次数。下一个示例计算 "celebrate""e" 字符的数量:

In  [96] "celebrate".count("e")

Out [96] 3

findindex 方法定位字符或子字符串的索引位置。这些方法返回参数首次出现的位置索引。回想一下,索引位置从 0 开始计数。下一个示例搜索 "celebrate" 中第一个 "e" 的索引。Python 在索引 1 处定位它:

In  [97] "celebrate".find("e")

Out [97] 1

In  [98] "celebrate".index("e")

Out [98] 1

findindex 方法有什么区别?如果字符串不包含参数,find 将返回 -1,而 index 将引发 ValueError 异常:

In  [99] "celebrate".find("z")

Out [99] -1

In  [100] "celebrate".index("z")

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-5-bf78a69262aa> in <module>
----> 1 "celebrate".index("z")

ValueError: substring not found

每种方法都适用于特定的情况;两种选择没有哪种比另一种更好。如果你的程序依赖于一个子字符串存在于更大的字符串中,例如,你可以使用 index 方法并处理错误。相比之下,如果子字符串的缺失不会阻止你的程序执行,你可以使用 find 方法来避免崩溃。

B.9 列表

列表 是一个按顺序存储对象的容器。列表的目的是双重的:提供一个“盒子”来存储值,并保持它们的顺序。我们称列表中的项目为 元素。在其他编程语言中,这种数据结构通常被称为 数组

我们使用一对开方括号和闭方括号来声明一个列表。我们在方括号内写下我们的元素,每两个元素之间用逗号分隔。下一个示例创建了一个包含五个字符串的列表:

In  [101] backstreet_boys = ["Nick", "AJ", "Brian", "Howie", "Kevin"]

列表的长度等于其元素的数量。还记得那个可靠的 len 函数吗?它可以帮助我们确定史上最伟大的男孩乐队有多少成员:

In  [102] len(backstreet_boys)

Out [102] 5

空列表 是一个没有元素的列表。它的长度为 0

In  [103] []

Out [103] []

列表可以存储任何数据类型的元素:字符串、数字、浮点数、布尔值等等。一个 同质 列表是指所有元素都具有相同类型的列表。以下三个列表是同质的。第一个包含整数,第二个包含浮点数,第三个包含布尔值:

In  [104] prime_numbers = [2, 3, 5, 7, 11]

In  [105] stock_prices_for_last_four_days = [99.93, 105.23, 102.18, 94.45]

In  [106] settings = [True, False, False, True, True, False]

列表也可以存储不同数据类型的元素。一个 异质 列表是指元素具有不同数据类型的列表。以下列表包含一个字符串、一个整数、一个布尔值和一个浮点数:

In  [107] motley_crew = ["rhinoceros", 42, False, 100.05]

就像 Python 为字符串中的每个字符分配索引位置一样,Python 为列表中的每个元素分配一个索引位置。索引表示元素在行中的位置,并从 0 开始计数。在以下三个元素的 favorite_foods 列表中,

  • "Sushi" 占据索引位置 0。

  • "Steak" 占据索引位置 1。

  • "Barbeque" 占据索引位置 2。

In  [108] favorite_foods = ["Sushi", "Steak", "Barbeque"]

关于列表格式的两个快速说明。首先,Python 允许我们在列表的最后一个元素后插入一个逗号。逗号根本不影响列表;它是一种替代语法:

In  [109] favorite_foods = ["Sushi", "Steak", "Barbeque",]

其次,一些 Python 风格指南建议将长列表拆分,以便每个元素占据一行。这种格式也不会以任何技术方式影响列表。语法看起来是这样的:

In  [110] favorite_foods = [
              "Sushi",
              "Steak",
              "Barbeque",
          ]

在本书的示例中,我使用了我认为最能增强可读性的格式化风格。你可以使用你觉得最舒服的格式。

我们可以通过索引位置访问列表元素。在列表(或引用它的变量)后面传递索引,放在一对方括号中:

In  [111] favorite_foods[1]

Out [111] 'Steak'

在 B.1.2 节中,我们介绍了一种切片语法来从字符串中提取字符。我们可以使用相同的语法从列表中提取元素。下一个示例从索引位置 1 到 3 提取元素。记住,在列表切片中,起始索引是包含的,而结束索引是不包含的:

In  [112] favorite_foods[1:3]

Out [112] ['Steak', 'Barbeque']

我们可以移除冒号前的数字来从列表的开头提取。下一个示例从列表的开头提取到索引 2(不包括)的元素:

In  [113] favorite_foods[:2]

Out [113] ['Sushi', 'Steak']

我们可以移除冒号后的数字来提取到列表的末尾。下一个示例从索引 2 提取到列表的末尾:

In  [114] favorite_foods[2:]

Out [114] ['Barbeque']

忽略两个数字以创建列表的副本:

In  [115] favorite_foods[:]

Out [115] ['Sushi', 'Steak', 'Barbeque']

最后,我们可以在方括号中提供一个可选的第三个数字来以间隔提取元素。下一个示例以 2 的增量从索引位置 0(包含)到索引位置 3(不包含)提取元素:

In  [116] favorite_foods[0:3:2]

Out [116] ['Sushi', 'Barbeque']

所有切片选项都返回一个新的列表。

让我们逐一了解一些列表方法。append 方法将新元素添加到列表的末尾:

In  [117] favorite_foods.append("Burrito")
          favorite_foods

Out [117] ['Sushi', 'Steak', 'Barbeque', 'Burrito']

你还记得我们关于可变性和不可变性的讨论吗?列表是一个可变对象的例子,是一个能够改变的对象。在创建列表后,我们可以添加、删除或替换列表中的元素。在前面的例子中,append 方法修改了由 favorite_foods 变量引用的列表。我们没有创建一个新的列表。

相比之下,字符串是一个不可变对象的例子。当我们调用像 upper 这样的方法时,Python 返回一个新的字符串;原始字符串保持不变。不可变对象不能改变。

列表包含多种变异方法。extend 方法将多个元素添加到列表的末尾。它接受一个参数,即要添加值的列表:

In  [118] favorite_foods.extend(["Tacos", "Pizza", "Cheeseburger"])
          favorite_foods

Out [118] ['Sushi', 'Steak', 'Barbeque', 'Burrito', 'Tacos', 'Pizza',
          'Cheeseburger']

insert 方法将元素添加到列表的特定索引位置。它的第一个参数是我们想要插入元素的位置,第二个参数是新的元素。Python 会将指定索引位置及其后的值推到下一个槽位。下一个示例将字符串 "Pasta" 插入到索引位置 2。列表将 "Barbeque" 和所有后续元素向上移动一个索引位置:

In  [119] favorite_foods.insert(2, "Pasta")
          favorite_foods

Out [119] ['Sushi',
           'Steak',
           'Pasta',
           'Barbeque',
           'Burrito',
           'Tacos',
           'Pizza',
           'Cheeseburger']

in 关键字可以检查列表是否包含一个元素。"Pizza" 在我们的 favorite_foods 列表中存在,而 "Caviar" 不存在:

In  [120] "Pizza" in favorite_foods

Out [120] True

In  [121] "Caviar" in favorite_foods

Out [121] False

not in 操作符确认列表中不存在一个元素。它返回 in 操作符的逆布尔值:

In  [122] "Pizza" not in favorite_foods

Out [122] False

In  [123] "Caviar" not in favorite_foods

Out [123] True

count 方法计算元素在列表中出现的次数:

In  [124] favorite_foods.append("Pasta")
          favorite_foods

Out [124] ['Sushi',
           'Steak',
           'Pasta',
           'Barbeque',
           'Burrito',
           'Tacos',
           'Pizza',
           'Cheeseburger',
           'Pasta']

In  [125] favorite_foods.count("Pasta")

Out [125] 2

remove 方法从列表中删除第一个出现的元素。注意,Python 不会删除该元素的后续出现:

In  [126] favorite_foods.remove("Pasta")
          favorite_foods

Out [126] ['Sushi',
           'Steak',
           'Barbeque',
           'Burrito',
           'Tacos',
           'Pizza',
           'Cheeseburger',
           'Pasta']

让我们去除列表末尾的其他 "Pasta" 字符串。pop 方法从列表中移除并返回最后一个元素:

In  [127] favorite_foods.pop()

Out [127] 'Pasta'

In  [128] favorite_foods

Out [128] ['Sushi', 'Steak', 'Barbeque', 'Burrito', 'Tacos', 'Pizza',
           'Cheeseburger']

pop方法也接受一个整数参数,表示 Python 应该删除的值的索引位置。在下一个示例中,我们删除了索引位置 2 的"Barbeque"值。"Burrito"字符串滑入索引位置 2,并且它后面的元素也向下移动一个索引:

In  [129] favorite_foods.pop(2)

Out [129] 'Barbeque'

In  [130] favorite_foods

Out [130] ['Sushi', 'Steak', 'Burrito', 'Tacos', 'Pizza', 'Cheeseburger']

列表可以存储任何对象,包括其他列表。在下一个示例中,我们声明了一个包含三个嵌套列表的列表。每个嵌套列表包含三个整数:

In  [131] spreadsheet = [
              [1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]
          ]

让我们花点时间反思一下前面的视觉表示。你能看到任何与电子表格的相似之处吗?嵌套列表是我们表示多维、表格化数据集合的一种方式。我们可以将最外层的列表视为工作表,每个内部列表视为数据行。

B.9.1 列表迭代

列表是集合对象的一个例子。它能够存储多个值——一个集合的值。迭代意味着逐个移动集合对象的元素。

遍历列表项最常见的方式是使用for循环。其语法看起来像这样:

for variable_name in some_list:
    # Do something

for循环由几个组件组成:

  • for关键字。

  • 一个变量名,在迭代过程中将逐个存储列表元素。

  • in关键字。

  • 要迭代的列表。

  • Python 在每次迭代期间将运行的代码块。我们可以在代码块中使用变量名。

作为提醒,一个是缩进代码的部分。Python 使用缩进来关联程序中的结构。位于函数名下方的块定义了函数的功能。同样,位于for循环下方的块定义了每次迭代期间发生的事情。

在下一个示例中,我们迭代一个包含四个字符串的列表,打印出每个字符串的长度:

In  [132] for season in ["Winter", "Spring", "Summer", "Fall"]:
              print(len(season))

Out [132] 6
          6
          6
          4

前面的迭代包含四个循环。season变量按顺序持有"Winter""Spring""Summer""Fall"的值。在每次迭代中,我们将当前字符串传递给len函数。len函数返回一个数字,我们将它打印出来。

假设我们想要将字符串的长度相加。我们必须将for循环与其他 Python 概念结合起来。在下一个示例中,我们首先初始化一个letter_count变量来保存累积总和。在for循环块内部,我们使用len函数计算当前字符串的长度,然后覆盖运行总和。最后,在循环完成后输出letter_count的值:

In  [133] letter_count = 0

          for season in ["Winter", "Spring", "Summer", "Fall"]:
              letter_count = letter_count + len(season)

          letter_count

Out [133] 22

for循环是迭代列表最传统的方法。Python 还支持另一种语法,我们将在 B.9.2 节中讨论。

B.9.2 列表推导式

列表 推导式是创建列表的简写语法,从集合对象中生成。假设我们有一个包含六个数字的列表:

In  [134] numbers = [4, 8, 15, 16, 23, 42]

假设我们想要创建一个包含那些数字平方的新列表。换句话说,我们想要对原始列表中的每个元素应用一个一致的运算。一个解决方案是遍历numbers中的每个整数,取其平方,并将结果添加到新列表中。提醒一下,append方法将元素添加到列表的末尾:

In  [135] squares = []

          for number in numbers:
              squares.append(number ** 2)

          squares

Out [135] [16, 64, 225, 256, 529, 1764]

列表推导可以单行生成相同的平方列表。其语法需要一个成对的开放和闭合方括号。在括号内,我们首先描述我们想要对迭代的每个元素做什么,然后是从中获取可迭代项的集合。

下一个示例仍然遍历numbers列表,并将每个列表元素分配给一个number变量。我们在for关键字之前声明我们想要对每个number做什么。我们将number ** 2计算移到开始,将for in逻辑移到末尾:

In  [136] squares = [number ** 2 for number in numbers]
          squares

Out [136] [16, 64, 225, 256, 529, 1764]

列表推导被认为是创建新列表的更 Pythonic 方式,从现有的数据结构中创建。Pythonic 方式描述了 Python 开发者随着时间的推移所采用的推荐实践集合。

B.9.3 将字符串转换为列表及其相反操作

我们现在熟悉列表和字符串了,让我们看看我们如何可以将它们一起使用。假设我们的程序中有一个包含地址的字符串:

In  [137] empire_state_bldg = "20 West 34th Street, New York, NY, 10001"

如果我们想要将地址分解成更小的组成部分:街道、城市、州和邮政编码呢?请注意,该字符串使用逗号来分隔这四个部分。

字符串的split方法通过使用分隔符,一个或多个字符的序列来标记边界,将字符串分解。下一个示例要求split方法在empire_state_building的每个逗号出现处进行分割。该方法返回一个由较小的字符串组成的列表:

In  [138] empire_state_bldg.split(",")

Out [138] ['20 West 34th Street', ' New York', ' NY', ' 10001']

这段代码是朝着正确方向迈出的一步。但请注意,列表中的最后三个元素前面有一个前置空格。虽然我们可以遍历列表的元素并对每个元素调用strip方法来移除其空白字符,但一个更优的解决方案是将空格添加到split方法的分隔符参数中:

In  [139] empire_state_bldg.split(", ")

Out [139] ['20 West 34th Street', 'New York', 'NY', '10001']

我们已经成功将字符串分解成字符串列表。

该过程也可以反向进行。假设我们将地址存储在一个列表中,并希望将列表的元素连接成一个单独的字符串:

In  [140] chrysler_bldg = ["405 Lexington Ave", "New York", "NY", "10174"]

首先,我们必须声明我们希望 Python 在两个列表元素之间注入的字符串。然后我们可以对字符串调用join方法,并将列表作为参数传入。Python 会将列表的元素连接起来,每个元素之间用分隔符分隔。下一个示例使用逗号和空格作为分隔符:

In  [141] ", ".join(chrysler_bldg)

Out [141] '405 Lexington Ave, New York, NY, 10174'

splitjoin方法对于处理文本数据很有帮助,这些数据通常需要被分离和重新合并。

B.10 元组

一个 元组 与 Python 列表类似的数据结构。元组也按顺序存储元素,但与列表不同,它是不可变的。一旦创建了元组,我们就不能添加、删除或替换其中的元素。

定义元组的唯一技术要求是声明多个元素,并且用逗号分隔每个后续的两个元素。以下示例声明了一个包含三个元素的元组:

In  [142] "Rock", "Pop", "Country"

Out [142] ('Rock', 'Pop', 'Country')

然而,通常我们使用一对括号来声明元组。这种语法使得从视觉上识别对象变得更容易:

In  [143] music_genres = ("Rock", "Pop", "Country")
          music_genres

Out [143] ('Rock', 'Pop', 'Country')

len 函数返回元组的长度:

In  [144] len(music_genres)

Out [144] 3

要声明一个只有一个元素的元组,我们必须在元素后面包含一个逗号。Python 需要逗号来识别元组。比较以下两个输出的差异。第一个示例没有使用逗号;Python 将值读取为字符串。

In  [145] one_hit_wonders = ("Never Gonna Give You Up")
          one_hit_wonders

Out [145] 'Never Gonna Give You Up'

相比之下,这里的语法返回一个元组。是的,一个符号在 Python 中可以产生巨大的差异:

In  [146] one_hit_wonders = ("Never Gonna Give You Up",)
          one_hit_wonders

Out [146] ('Never Gonna Give You Up',)

使用 tuple 函数创建一个 空元组,即一个没有元素的元组:

In  [147] empty_tuple = tuple()
          empty_tuple

Out [147] ()

In  [148] len(empty_tuple)

Out [148] 0

与列表一样,你可以通过索引位置访问元组元素。与列表一样,你可以使用 for 循环遍历元组元素。唯一不能做的是修改元组。由于其不可变性,元组不包含如 appendpopinsert 这样的突变方法。

如果你有一组有序的元素,并且知道它不会改变,你可以选择使用元组而不是列表来存储它。

B.11 字典

列表和元组是存储有序对象的最佳数据结构。我们需要另一种数据结构来解决不同类型的问题:在对象之间建立关联。

考虑一家餐厅的菜单。每个菜单项都是一个唯一的标识符,我们用它来查找相应的价格。菜单项和其成本是关联的。项目的顺序并不重要;重要的是两份数据之间的 联系

一个 字典 是一个可变、无序的键值对集合。一对由一个键和一个值组成。每个键都是一个值的标识符。键必须是唯一的。值可以包含重复项。

我们使用一对花括号 ({}) 声明字典。以下示例创建了一个空字典:

In  [149] {}

Out [149] {}

让我们在 Python 中模拟一个示例餐厅菜单。在花括号内,我们使用冒号 (:) 为其值分配一个键。以下示例声明了一个包含一个键值对的字典。字符串键 "Cheeseburger" 被分配了浮点值 7.99

In  [150] { "Cheeseburger": 7.99 }
Out [150] {'Cheeseburger': 7.99}

当声明包含多个键值对的字典时,用逗号分隔每个两个键值对。让我们扩展我们的 menu 字典以包含三个键值对。注意 "French Fries""Soda" 键的值是相同的:

In  [151] menu = {"Cheeseburger": 7.99, "French Fries": 2.99, "Soda": 2.99}
          menu

Out [151] {'Cheeseburger': 7.99, 'French Fries': 2.99, 'Soda': 2.99}

我们可以通过将其传递给 Python 的内置 len 函数来计算字典中键值对的数量:

In  [152] len(menu)

Out [152] 3

我们使用键从字典中检索值。在字典后面立即放置一对方括号,并紧跟键。语法与通过索引位置访问列表元素相同。以下示例提取了 "French Fries" 键的值:

In  [153] menu["French Fries"]

Out [153] 2.99

在列表中,索引位置始终是数字。在字典中,键可以是任何不可变的数据类型:整数、浮点数、字符串、布尔值等等。

如果键在字典中不存在,Python 将引发一个 KeyError 异常。KeyError 是原生 Python 错误的另一个例子:

In  [154] menu["Steak"]

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-19-0ad3e3ec4cd7> in <module>
----> 1 menu["Steak"]

KeyError: 'Steak'

总是注意大小写敏感。如果单个字符不匹配,Python 将无法找到键。在我们的字典中不存在 "soda" 这个键。只有 "Soda"

In  [155] menu["soda"]

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-20-47940ceca824> in <module>
----> 1 menu["soda"]

KeyError: 'soda'

get 方法也可以通过键提取字典值:

In  [156] menu.get("French Fries")

Out [156] 2.99

get 方法的优势是,如果键不存在,它返回 None 而不是引发错误。记住,None 是 Python 用来表示不存在或空值的概念的一个对象。None 值在 Jupyter Notebook 中不会产生任何视觉输出。但我们可以用 print 函数包裹调用,强制 Python 打印 None 的字符串表示:

In  [157] print(menu.get("Steak"))

Out [157] None

get 方法的第二个参数是在字典中键不存在时返回的自定义值。在下一个示例中,字符串 "Steak" 并不是 menu 字典中的键,所以 Python 返回 99.99

In  [158] menu.get("Steak", 99.99)

Out [158] 99.99

字典是一个可变的数据结构。在创建字典后,我们可以向其中添加键值对或从字典中删除键值对。要添加一个新的键值对,提供键并用赋值运算符(=)为其赋值:

In  [159] menu["Taco"] = 0.99
          menu

Out [159] {'Cheeseburger': 7.99, 'French Fries': 2.99, 'Soda': 1.99,
          'Taco': 0.99}

如果键已经存在于字典中,Python 将覆盖其原始值。在下一个示例中,将 "Cheeseburger" 键的值从 7.99 更改为 9.99

In  [160] print(menu["Cheeseburger"])
          menu["Cheeseburger"] = 9.99
          print(menu["Cheeseburger"])

Out [160] 7.99
          9.99

pop 方法从一个字典中移除一个键值对;它接受一个键作为参数并返回其值。如果键在字典中不存在,Python 将引发一个 KeyError 异常:

In  [161] menu.pop("French Fries")

Out [161] 2.99

In  [162] menu

Out [162] {'Cheeseburger': 9.99, 'Soda': 1.99, 'Taco': 0.99}

in 关键字检查一个元素是否存在于字典的键中:

In  [163] "Soda" in menu

Out [163] True

In  [164] "Spaghetti" in menu

Out [164] False

要检查字典值中的包含情况,在字典上调用 values 方法。该方法返回一个类似列表的对象,包含字典的值。我们可以将 in 操作符与 values 方法的返回值结合使用:

In  [165] 1.99 in menu.values()

Out [165] True

In  [166] 499.99 in menu.values()

Out [166] False

values 方法返回的对象类型与我们之前看到的列表、元组和字典不同。我们不一定需要知道这个对象是什么,然而。我们只关心我们如何与之交互。in 操作符检查一个值是否在对象中,而 values 方法返回的对象知道如何处理它。

B.11.1 字典迭代

我们应该始终假设字典的键值对是无序的。如果您需要一个保持顺序的数据结构,请使用列表或元组。如果您需要创建对象之间的关联,请使用字典。

即使我们不能保证迭代顺序的确定性,我们仍然可以使用 for 循环一次迭代一个键值对来遍历字典。字典的 items 方法在每次迭代时返回一个包含两个元素的元组。该元组包含一个键及其相应的值。我们可以在 for 关键字之后声明多个变量来存储每个键和值。在下一个示例中,state 变量包含每个字典键,而 capital 变量包含每个值:

In  [167] capitals = {
              "New York": "Albany",
              "Florida": "Tallahassee",
              "California": "Sacramento"
          }

          for state, capital in capitals.items():
              print("The capital of " + state + " is " + capital + ".")

          The capital of New York is Albany.
          The capital of Florida is Tallahassee.
          The capital of California is Sacramento.

在第一次迭代中,Python 返回一个包含 ("New York", "Albany") 的元组。在第二次迭代中,它返回一个包含 ("Florida", "Tallahassee") 的元组,依此类推。

B.12 集合

列表和字典对象有助于解决顺序和关联的问题。集合帮助解决另一个常见需求:唯一性。一个 集合 是一个无序、可变且元素唯一的集合。它禁止重复。

我们使用一对花括号声明一个集合。我们在花括号中填充元素,每两个元素之间用逗号分隔。下一个示例声明了一个包含六个数字的集合:

In  [168] favorite_numbers = { 4, 8, 15, 16, 23, 42 }

眼光敏锐的读者可能会注意到,声明集合的花括号语法与声明字典的语法相同。Python 可以根据键值对的存在与否来区分这两种类型的对象。

因为 Python 将空的一对花括号解释为空字典,所以创建空集合的唯一方法是使用内置的 set 函数:

In  [169] set()

Out [169] set()

这里有一些有用的集合方法。add 方法向集合中添加一个新元素:

In  [170] favorite_numbers.add(100)
          favorite_numbers

Out [170] {4, 8, 15, 16, 23, 42, 100}

Python 只会在集合中不存在该元素的情况下向集合中添加一个元素。下一个示例尝试将 15 添加到 favorite_numbers 中。Python 发现 15 已经存在于集合中,因此对象保持不变:

In  [171] favorite_numbers.add(15)
          favorite_numbers

Out [171] {4, 8, 15, 16, 23, 42, 100}

集合没有顺序的概念。如果我们尝试通过索引位置访问集合元素,Python 会引发 TypeError 异常:

In  [172] favorite_numbers[2]

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-17-e392cd51c821> in <module>
----> 1 favorite_numbers[2]

TypeError: 'set' object is not subscriptable

当我们尝试对一个无效对象应用操作时,Python 会引发 TypeError 异常。集合元素是无序的,因此元素没有索引位置。

除了防止重复外,集合非常适合识别两个数据集合之间的相似性和差异性。让我们定义两个字符串集合:

In  [173] candy_bars = { "Milky Way", "Snickers", "100 Grand" }
          sweet_things = { "Sour Patch Kids", "Reeses Pieces", "Snickers" }

intersection 方法返回一个新集合,其中包含在两个原始集合中都找到的元素。& 符号执行相同的逻辑。在下一个示例中,"Snickers"candy_barssweet_things 之间唯一的字符串:

In  [174] candy_bars.intersection(sweet_things)

Out [174] {'Snickers'}

In  [175] candy_bars & sweet_things

Out [175] {'Snickers'}

union 方法返回一个集合,该集合包含两个集合的所有元素。| 符号执行相同的逻辑。请注意,像 "Snickers" 这样的重复值只会出现一次:

In  [176] candy_bars.union(sweet_things)

Out [176] {'100 Grand', 'Milky Way', 'Reeses Pieces', 'Snickers', 'Sour
          Patch Kids'}

In  [177] candy_bars | sweet_things

Out [177] {'100 Grand', 'Milky Way', 'Reeses Pieces', 'Snickers', 'Sour
          Patch Kids'}

difference方法返回一个集合,其中包含在调用该方法时在集合中存在的元素,但不包含在作为参数传递的集合中。我们可以使用-符号作为快捷方式。在下一个例子中,"100 Grand""Milky Way"存在于candy_bars中,但不在sweet_things中:

In  [178] candy_bars.difference(sweet_things)

Out [178] {'100 Grand', 'Milky Way'}

In  [179] candy_bars - sweet_things

Out [179] {'100 Grand', 'Milky Way'}

symmetric_difference方法返回一个集合,其中包含在任一集合中找到的元素,但不包含在两个集合中。^语法可以达到相同的结果:

In  [180] candy_bars.symmetric_difference(sweet_things)

Out [180] {'100 Grand', 'Milky Way', 'Reeses Pieces', 'Sour Patch Kids'}

In  [181] candy_bars ^ sweet_things

Out [181] {'100 Grand', 'Milky Way', 'Reeses Pieces', 'Sour Patch Kids'}

就这些了!我们已经学到了很多 Python 知识:数据类型、函数、迭代等等。如果你记不住所有细节也没关系。相反,当你需要复习 Python 核心机制时,随时回到这个附录。在我们使用 pandas 库的过程中,我们会用到并回顾很多这些想法。


¹ 请参阅“PEP 8—Python 代码风格指南”,www.python.org/dev/peps/pep-0008

附录 C. NumPy 快速入门

开源 NumPy(数值 Python)库是 pandas 的依赖项,它公开了一个强大的ndarray对象,用于存储同质、n-维数组。这听起来有点复杂,所以让我们分解一下。数组是有序值集合,类似于 Python 列表。同质意味着数组中的值具有相同的数据类型。n-维意味着数组可以持有任意数量的维度。(我们将在 C.1 节中讨论维度。)NumPy 是由数据科学家 Travis Oliphant 开发的,他创立了 Anaconda 公司,该公司构建了我们用来设置开发环境的 Python 发行版。

我们可以使用 NumPy 生成任何大小和形状的随机数据集;事实上,官方的 pandas 文档广泛地这样做。对库的基本了解将有助于我们更好地理解 pandas 的底层机制。

C.1 维度

维度指的是从数据结构中提取单个值所需的参考点数量。考虑一个给定日期多个城市温度的集合:

温度
纽约 38
芝加哥 36
旧金山 51
迈阿密 73

如果我要求您在这个数据集中查找一个特定的温度,您只需要一个参考点:城市的名称(例如“旧金山”)或其顺序(例如“列表中的第三个城市”)。因此,这个表格描述了一个一维数据集。

将此表与多个城市在多个日子温度的数据集进行比较:

星期一 星期二 星期三 星期四 星期五
纽约 38 41 35 32 35
芝加哥 36 39 31 27 25
旧金山 51 52 50 49 53
迈阿密 73 74 72 71 74

现在您需要多少个参考点才能从这个数据集中提取一个特定的值?答案是 2。我们需要一个城市和一周中的某一天(例如“星期四的旧金山”)或行号和列号(例如“第 3 行第 4 列”)。城市或工作日本身不足以作为标识符,因为它们都与数据集中的多个值相关联。城市和工作日的组合(或等价的行和列)将结果过滤到一个值;因此,这个数据集是二维的。

数据集的行数和列数不影响其维度数。一个有 100 万行和 100 万列的表格仍然是二维的。我们仍然需要一个行位置和列位置的组合来提取一个值。

每增加一个参考点,就增加一个维度。我们可能会收集两周的温度数据:

第一周

星期一 星期二 星期三 星期四 星期五
纽约 38 41 35 32 35
芝加哥 36 39 31 27 25
旧金山 51 52 50 49 53
迈阿密 73 74 72 71 74

第二周

星期一 星期二 星期三 星期四 星期五
纽约 40 42 38 36 28
芝加哥 32 28 25 31 25
旧金山 49 55 54 51 48
迈阿密 75 78 73 76 71

城市和星期几已不再足够提取单个值。我们现在需要三个参考点(星期、城市和日期),因此我们可以将这个数据集分类为三维的。

C.2 ndarray 对象

让我们先创建一个新的 Jupyter Notebook 并导入 NumPy 库,该库通常被分配别名np

In  [1] import numpy as np

NumPy 擅长生成随机和非随机数据。让我们从一项简单的挑战开始:创建一个连续的数字范围。

C.2.1 使用 arange 方法生成数字范围

arange函数返回一个一维ndarray对象,包含一系列连续的数值。当我们用单个参数调用arange时,NumPy 将0设置为下限,范围开始的位置。第一个参数将设置上限,范围结束的数字。上限是不包含的;NumPy 将到达那个值但不包括它。例如,一个3的参数将产生一个包含值012ndarray

In  [2] np.arange(3)

Out [2] array([0, 1, 2])

我们也可以向arange传递两个参数,这将声明范围的上下限。下限是包含的;范围将包括其值。端点是不包含的。在下一个示例中,请注意 NumPy 包含2但不包含6

In  [3] np.arange(2, 6)

Out [3] array([2, 3, 4, 5])

arange的前两个参数对应于startstop关键字参数。我们可以明确写出关键字参数。前面的和后面的代码示例产生相同的数组:

In  [4] np.arange(start = 2, stop = 6)

Out [4] array([2, 3, 4, 5])

arange函数的可选第三个参数step设置两个值之间的间隔。从数学的角度来考虑这个概念可能会有所帮助。从下限开始,并加上间隔值,直到达到上限。下一个示例创建一个从0111(不包含)的间隔为 10 的范围:

In  [5] np.arange(start = 0, stop = 111, step = 10)

Out [5] array([  0,  10,  20,  30,  40,  50,  60,  70,  80,  90, 100, 110])

让我们将最后一个ndarray保存到tens变量中:

In  [6] tens = np.arange(start = 0, stop = 111, step = 10)

现在tens变量指向一个包含 12 个数字的ndarray对象。

C.2.2 ndarray 对象上的属性

NumPy 库的ndarray对象有一套自己的属性和方法。作为提醒,属性是属于对象的数据的一部分。方法是我们可以向对象发送的命令。

shape属性返回一个元组,包含数组的维度。shape元组的长度等于ndarray的维度数。以下输出表明tens是一个包含 12 个值的一维数组:

In  [7] tens.shape

Out [7] (12,)

我们还可以使用ndim属性来询问ndarray的维度数:

In  [8] tens.ndim

Out [8] 1

size属性返回数组中的元素数量:

In  [9] tens.size

Out [9] 12

接下来,让我们看看我们如何可以操作数组中 12 个元素的形状。

C.2.3 reshape 方法

目前,我们的 12 个元素的tens ndarray是一维的。我们可以通过一个参考点访问任何元素,即它在数组中的位置:

In  [10] tens

Out [10] array([  0,  10,  20,  30,  40,  50,  60,  70,  80,  90, 100,
         110])

我们可能想要将现有的一个维度数组转换成具有不同形状的多维度数组。假设我们的 12 个值代表在 4 天内捕获的 3 次每日测量值。在 4 x 3 的形状中思考数据比在 12 x 1 的形状中更容易。

reshape 方法使用其参数返回具有指定形状的新 ndarray 对象。下一个示例将 tens 变形为一个新的二维数组,具有 4 行 3 列:

In  [11] tens.reshape(4, 3)

Out [11] array([[  0,  10,  20],
                [ 30,  40,  50],
                [ 60,  70,  80],
                [ 90, 100, 110]])

传递给 reshape 的参数数量将等于新 ndarray 的维度数:

In  [12] tens.reshape(4, 3).ndim

Out [12] 2

我们必须确保参数的乘积等于原始数组中的元素数量。值 4 和 3 是有效的参数,因为它们的乘积是 12,而 tens 有 12 个值。另一个有效示例是具有 2 行 6 列的二维数组:

In  [13] tens.reshape(2, 6)

Out [13] array([[  5,  15,  25,  35,  45,  55],
                [ 65,  75,  85,  95, 105, 115]])

如果 NumPy 无法将原始数组变形为所需的形状,则会引发 ValueError 异常。在下一个示例中,库无法将 tens 中的 12 个值拟合到 2 x 5 的数组中:

In  [14] tens.reshape(2, 5)

Out [14]

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-68-5b9588276555> in <module>
----> 1 tens.reshape(2, 5)

ValueError: cannot reshape array of size 12 into shape (2,5)

ndarray 能否存储超过两个维度的数据?当然可以。让我们通过传递第三个参数给 reshape 来看看它是如何工作的。下一个示例将一维的 tens 数组形状调整为 2 x 3 x 2 的三维数组:

In  [15] tens.reshape(2, 3, 2)

Out [15] array([[[  5,  15],
                 [ 25,  35],
                 [ 45,  55]],

                [[ 65,  75],
                 [ 85,  95],
                 [105, 115]]])

让我们访问新数组上的 ndim 属性。数据结构确实有三个维度:

In  [16] tens.reshape(2, 3, 2).ndim

Out [16] 3

我们也可以传递 -1 作为 reshape 的参数来表示一个未知维度。NumPy 将推断出在该维度中填充的正确值数。下一个示例传递了 2-1 作为参数。NumPy 计算出新的二维数组应该具有 2 x 6 的形状:

In  [17] tens.reshape(2, -1)

Out [17] array([[  0,  10,  20,  30,  40,  50],
                [ 60,  70,  80,  90, 100, 110]])

在下一个示例中,库计算返回的 ndarray 应该具有 2 x 3 x 2 的形状:

In  [18] tens.reshape(2, -1, 2)

Out [18] array([[[  0,  10],
                 [ 20,  30],
                 [ 40,  50]],

                [[ 60,  70],
                 [ 80,  90],
                 [100, 110]]])

我们只能将一个未知维度传递给 reshape 方法调用。

reshape 方法返回一个新的 ndarray 对象。原始数组不会被修改。因此,我们的 tens 数组仍然保持其原始的 1 x 12 形状。

C.2.4 randint 函数

randint 函数生成一个或多个介于某个范围内的随机数。当传递单个参数时,它返回一个介于 0 到该值之间的随机整数。下一个示例返回一个介于 05(不包括 5)之间的随机值:

In  [19] np.random.randint(5)

Out [19] 3

我们可以向 randint 传递两个参数来声明一个包含下限(包括)和上限(不包括)的范围。NumPy 将在该范围内选择一个数字:

In  [20] np.random.randint(1, 10)

Out [20] 9

如果我们想要生成一个随机整数数组呢?我们可以传递第三个参数给 randint 来指定所需的数组形状。我们可以传递一个单个整数或一个单元素列表来创建一个一维数组:

In  [21] np.random.randint(1, 10, 3)

Out [21] array([4, 6, 3])

In  [22] np.random.randint(1, 10, [3])

Out [22] array([9, 1, 6])

要创建一个多维 ndarray,我们传递一个列表来指定每个维度中的值数。以下示例填充了一个 3 x 5 的二维数组,其中的值介于 110(不包括 10)之间:

In  [23] np.random.randint(1, 10, [3, 5])

Out [23] array([[2, 9, 8, 8, 7],
                [9, 8, 7, 3, 2],
                [4, 4, 5, 3, 9]])

你可以在列表中提供任意数量的值来创建具有更多维度的 ndarrays。例如,一个包含三个值的列表将创建一个三维数组。

C.2.5 randn 函数

randn 函数返回一个包含从标准正态分布中随机值的 ndarray。函数的每个连续参数设置一个维度中要存储的值的数量。如果我们传递一个参数,ndarray 将有一个维度。下一个示例创建了一个 1 x 3(1 行 3 列)的数组:

In  [24] np.random.randn(3)

Out [24] array([-1.04474993,  0.46965268, -0.74204863])

如果我们向 randn 函数传递两个参数,ndarray 将有两个维度,依此类推。下一个示例创建了一个 2 x 4 的二维数组:

In  [25] np.random.randn(2, 4)

Out [25] array([[-0.35139565,  1.15677736,  1.90854535,  0.66070779],
                [-0.02940895, -0.86612595,  1.41188378, -1.20965709]])

下一个示例创建了一个形状为 2 x 4 x 3 的三维数组。我们可以将这个形状视为两个数据集,每个数据集都有四行和三列:

In  [26] np.random.randn(2, 4, 3)

Out [26] array([[[ 0.38281118,  0.54459183,  1.49719148],
                 [-0.03987083,  0.42543538,  0.11534431],
                 [-1.38462105,  1.54316814,  1.26342648],
                 [ 0.6256691 ,  0.51487132,  0.40268548]],

                [[-0.24774185, -0.64730832,  1.65089833],
                 [ 0.30635744,  0.21157744, -0.5644958 ],
                 [ 0.35393732,  1.80357335,  0.63604068],
                 [-1.5123853 ,  1.20420021,  0.22183476]]])

rand 函数族是生成虚假数值数据的一个惊人的方法。我们还可以创建不同类型和类别的虚假数据,例如姓名、地址或信用卡。有关该主题的更多信息,请参阅附录 D。

C.3 nan 对象

NumPy 库使用一个特殊的 nan 对象来表示缺失或无效的值。缩写 nan 代表 not a number,这是一个通用的通用术语,用于表示缺失数据。在我们将包含缺失值的数据集导入 pandas 的过程中,本书中会频繁地遇到 nan。目前,我们可以直接通过 np 包的顶层属性访问 nan 对象:

In  [27] np.nan

Out [27] nan

一个 nan 对象不等于任何值:

In  [28] np.nan == 5

Out [28] False

一个 nan 值也不等于另一个 nan。从 NumPy 的角度来看,nan 值是缺失或缺失的。我们不能确定它们是否相同,所以我们假设它们是不同的。

In  [29] np.nan == np.nan

Out [29] False

就这些了!这些是关于 NumPy 库最重要的细节,pandas 在其底层使用它。

在你的空闲时间,浏览一下 pandas 文档(pandas.pydata.org/docs/user_guide/10min.html)。你可能会看到许多使用 NumPy 生成随机数据的示例。

附录 D. 使用 Faker 生成假数据

Faker 是一个用于生成假数据的 Python 库。它专门用于创建姓名、电话号码、街道地址、电子邮件等列表。结合可以生成随机数值数据的 NumPy,它可以快速创建任何大小、形状和类型的数据集。如果你想要练习 pandas 概念但找不到合适的数据集来应用,Faker 提供了一个绝佳的解决方案。在本附录中,我们将介绍你需要了解的所有内容以开始使用这个库。

D.1 安装 Faker

首先,让我们在我们的 conda 环境中安装 Faker 库。在终端(macOS)或 Anaconda Prompt(Windows)中,激活为这本书设置的 conda 环境。当我为附录 A 创建环境时,我将其命名为 pandas_in_action

conda activate pandas_in_action

如果你忘记了可用的 Anaconda 环境,你可以执行 conda info --envs 来查看它们的列表。当环境处于活动状态时,使用 conda install 命令安装 Faker 库:

conda install faker

当提示确认时,输入 "Y" 表示是并按 Enter 键。Anaconda 将下载并安装库。当过程完成后,启动 Jupyter Notebook 并创建一个新的笔记本。

D.2 使用 Faker 入门

让我们探索 Faker 的核心功能,然后将其与 NumPy 配对以生成一个 1,000 行的 DataFrame。首先,我们将导入 pandas 和 NumPy 库并将它们分配给各自的别名(pdnp)。同时,我们也将导入 faker 库:

In  [1] import pandas as pd
        import numpy as np
        import faker

faker 包导出了一个 Faker 类(注意大写的 F)。作为提醒,一个 是对象的蓝图——数据结构的模板。SeriesDataFrame 是 pandas 库中的两个示例类,而 Faker 是 Faker 库中的一个示例类。

让我们使用一对括号创建一个 Faker 类的实例,并将生成的 Faker 对象分配给 fake 变量:

In  [2] fake = faker.Faker()

一个 Faker 对象包含许多实例方法,每个方法都从给定类别返回一个随机值。例如,name 实例方法返回一个包含人名的字符串:

In  [3] fake.name()

Out [3] 'David Lee'

由于 Faker 的固有随机性,当你在自己的计算机上执行代码时,返回值可能会变化。这是完全可以接受的。

我们可以调用互补的 name_malename_female 方法来根据性别返回全名:

In  [4] fake.name_male()

Out [4] 'James Arnold'

In  [5] fake.name_female()

Out [5] 'Brianna Hall'

使用 first_namelast_name 方法来返回仅包含名字或姓氏:

In  [6] fake.first_name()

Out [6] 'Kevin'

In  [7] fake.last_name()

Out [7] 'Soto'

同样,也有针对特定性别的 first_name_malefirst_name_female 方法:

In  [8] fake.first_name_male()

Out [8] 'Brian'
In  [9] fake.first_name_female()

Out [9] 'Susan'

如您所见,Faker 的语法简单但功能强大。这里有一个另一个例子。假设我们想要为数据集生成一些随机的位置。address 方法返回一个包含街道、城市、州和邮政编码的完整地址的字符串:

In  [10] fake.address()

Out [10] '6162 Chase Corner\nEast Ronald, SC 68701'

注意,地址完全是假的;它不是地图上的实际位置。Faker 只是遵循了地址通常看起来像的惯例。

注意到 Faker 使用换行符 (\n) 将街道和地址的其余部分分开。您可以将返回值包裹在一个 print 函数调用中,以将地址拆分到多行:

In  [11] print(fake.address())

Out [11] 602 Jason Ways Apt. 358
         Hoganville, NV 37296

我们可以使用 street_addresscitystatepostcode 等方法生成地址的各个组成部分:

In  [12] fake.street_address()

Out [12] '58229 Heather Walk'

In  [13] fake.city()

Out [13] 'North Kristinside'

In  [14] fake.state()

Out [14] 'Oklahoma'

In  [15] fake.postcode()

Out [15] '94631'

我们可以使用另一批方法生成与商业相关的数据。以下方法返回一个随机公司、标语、职位和 URL:

In  [16] fake.company()

Out [16] 'Parker, Harris and Sutton'

In  [17] fake.catch_phrase()

Out [17] 'Switchable systematic task-force'

In  [18] fake.job()

Out [18] 'Copywriter, advertising'

In  [19] fake.url()

Out [19] 'https://www.gutierrez.com/'

Faker 还支持电子邮件地址、电话号码和信用卡号码:

In  [20] fake.email()

Out [20] 'sharon13@taylor.com'

In  [21] fake.phone_number()

Out [21] '680.402.4787'

In  [22] fake.credit_card_number()

Out [22] '4687538791240162'

Faker 网站 (faker.readthedocs.io/en/master) 为 Faker 对象的实例方法提供了完整的文档。该库将方法分组到父类别中,如地址、汽车和银行。图 D.1 显示了 Faker 文档的一个示例页面。

图 D.1 Faker 官方网站上的一个示例文档页面

花些时间探索 Faker 可用的类别。一点多样性可以帮助使你生成的下一个假数据集更加引人入胜。

D.3 使用假值填充 DataFrame

现在我们已经熟悉了使用 Faker 生成一个假值,让我们使用它来填充整个数据集。我们的目标是创建一个包含四个列:姓名、公司、电子邮件和薪水的 1,000 行 DataFrame

我们将如何解决这个问题:我们将使用一个 for 循环来迭代 1,000 次,并在每次迭代中要求 Faker 生成一个假名字、公司和一个电子邮件地址。我们还将要求 NumPy 生成一个随机数来表示薪水。

我们可以使用 Python 的 range 函数进行迭代。该函数接受一个整数参数。它返回一个升序的迭代序列,从 0 开始,直到但不包括参数。在下一个示例中,我们使用一个 for 循环来迭代从 0(包含)到 5(不包含)的值范围:

In  [23] for i in range(5):
             print(i)

Out [23] 0
         1
         2
         3
         4

为了生成我们的数据集,我们将使用 range(1000) 来迭代 1,000 次。

DataFrame 类的构造函数接受各种输入作为其 data 参数,包括字典列表。Pandas 将每个字典键映射到 DataFrame 的一个列,并将每个值映射到该列的行值。以下是我们想要的输入的预览:

[
    {
         'Name': 'Ashley Anderson',
         'Company': 'Johnson Group',
         'Email': 'jessicabrooks@whitaker-crawford.biz',
         'Salary': 62883
    },
    {
         'Name': 'Katie Lee',
         'Company': 'Ward-Aguirre',
         'Email': 'kennethbowman@fletcher.com',
         'Salary': 102971
    }
    # ... and 998 more dictionaries
]

您会注意到 Faker 生成的数据中存在一些逻辑不一致。例如,第一个人的名字是 Ashley Anderson,但电子邮件是 jessicabrooks@whitaker-crawford.biz。这种不一致是由于 Faker 的随机性。对于以下示例,我们不会担心这些不完美之处。然而,如果我们想使我们的数据集更加“准确”,我们可以将 Faker 与常规 Python 代码结合使用来生成我们想要的任何值。例如,我们可以要求 Faker 提供一个名字("Morgan")和一个姓氏("Robinson"),然后将这两个字符串连接起来形成一个更真实的电子邮件地址("MorganRobinson@gmail.com"):

In  [24] first_name = fake.first_name_female()
         last_name = fake.last_name()
         email = first_name + last_name + "@gmail.com"
         email

Out [24] 'MorganRobinson@gmail.com'

回到正题。让我们使用列表推导式和range函数来创建一个包含 1,000 个字典的列表。在每个字典中,我们将声明相同的四个键:"Name""Company""Email""Salary"。对于前三个值,我们将在我们的Faker对象上调用namecompanyemail实例方法。记住,Python 将在每次迭代时调用这些方法,因此值将每次都不同。对于"Salary"值,我们将使用 NumPy 的randint函数来返回一个介于 50,000 和 200,000 之间的随机整数。有关 NumPy 函数的更深入教程,请参阅附录 C。

In  [25] data = [
             { "Name": fake.name(),
               "Company": fake.company(),
               "Email": fake.email(),
               "Salary": np.random.randint(50000, 200000)
             }
             for i in range(1000)
         ]

我们的data变量包含一个包含 1,000 个字典的列表。最后一步是将字典列表传递给 pandas 顶层中的DataFrame构造函数:

In  [26] df = pd.DataFrame(data = data)
         df

Out [26]

 **Name              Company                Email  Salary**
0       Deborah Lowe       Williams Group  ballbenjamin@gra...  147540
1     Jennifer Black          Johnson Inc  bryannash@carlso...  135992
2          Amy Reese  Mitchell, Hughes...   ajames@hotmail.com  101703
3     Danielle Moore       Porter-Stevens     logan76@ward.com  133189
4        Jennifer Wu        Goodwin Group    vray@boyd-lee.biz   57486
...              ...                  ...                  ...     ...
995   Joseph Stewart  Rangel, Garcia a...     sbrown@yahoo.com  123897
996   Deborah Curtis  Rodriguez, River...  smithedward@yaho...   51908
997  Melissa Simmons        Stevenson Ltd  frederick96@hous...  108791
998  Tracie Martinez       Morales-Moreno  caseycurry@lopez...  181615
999  Phillip Andrade    Anderson and Sons  anthony23@glover...  198586

1000 rows × 4 columns

由此,你得到了一个包含 1,000 行随机数据的DataFrame,用于练习。请随意探索 Faker 和 NumPy 的文档,看看你可以生成哪些其他类型的随机数据。

附录 E. 正则表达式

一个正则 表达式(通常缩写为RegEx)是文本的搜索模式。它定义了计算机应在字符串中查找的逻辑字符序列。

这里有一个简单的例子。你可能在某个时候使用过网页浏览器中的查找功能。在大多数网页浏览器中,你可以通过按 Windows 上的 Ctrl-F 或 macOS 上的 Command-F 来访问这个功能。浏览器会弹出一个对话框,我们在其中输入一串字符。然后浏览器会在网页上搜索这些字符。图 E.1 展示了浏览器在页面内容中搜索并找到romance的例子。

图 E.1 使用 Google Chrome 的查找功能搜索文本romance

Chrome 的查找功能是正则表达式在行动中的简单例子。这个工具确实有其局限性。例如,我们只能按字符出现的顺序搜索字符。我们可以搜索字符序列"cat",但不能声明一个条件,比如字母"c""a""t"。正则表达式使得这种动态搜索成为可能。

正则表达式描述了如何在一段文本中查找内容。我们可以搜索字母、数字或空格等字符,但也可以使用特殊符号来声明条件。以下是一些我们可以搜索的内容示例:

  • 连续的两个数字

  • 由三个或更多字母组成的序列,后面跟一个空格

  • 字符s,但仅限于单词的开头

在本附录中,我们将探讨正则表达式在 Python 中的工作原理,然后应用我们的知识来处理 pandas 中的数据集。关于正则表达式,有整本教科书和大学课程,所以我们在这里的目的是触及这个复杂研究领域的一角。正则表达式易于入门,但难以精通。

E.1 Python 的 re 模块简介

让我们从创建一个新的 Jupyter Notebook 开始。我们将导入 pandas 和一个名为re的特殊模块。re(正则表达式)模块是 Python 标准库的一部分,并内置于语言中:

In  [1] import re
        import pandas as pd

re模块有一个search函数,用于在字符串中查找子字符串。该函数接受两个参数:一个搜索序列和一个要查找的字符串。下一个例子是在字符串"field of flowers"中查找字符串"flower"

In  [2] re.search("flower", "field of flowers")

Out [2] <re.Match object; span=(9, 15), match='flower'>

search函数如果 Python 在目标字符串中找到字符序列,则返回一个Match对象。Match对象存储有关与搜索模式匹配的内容以及它在目标字符串中的位置的信息。前面的输出表明"flower"在从索引位置 9 到 15 的字符范围内被找到。第一个索引是包含的,第二个索引是排除的。如果我们计算"field of flowers"中的字符索引位置,我们会看到索引 9 是"flowers"中的小写"f",而索引 15 是"flowers"中的"s"

如果搜索模式在目标字符串中不存在,search 函数返回 None。默认情况下,Jupyter Notebook 不会为 None 值输出任何内容。但我们可以将 search 调用包裹在 print 函数中,强制 Jupyter 打印值:

In  [3] print(re.search("flower", "Barney the Dinosaur"))

Out [3] None

search 函数只返回目标字符串中的第一个匹配项。我们可以使用 findall 函数来找到所有匹配项。此函数接受相同的两个参数——一个搜索序列和一个目标字符串——并返回一个字符串列表,其中包含与搜索序列匹配的字符串。在下一个示例中,Python 在 "Picking flowers in the flower field" 中找到了 "flower" 这个搜索模式两次:

In  [4] re.findall("flower", "Picking flowers in the flower field")

Out [4] ['flower', 'flower']

注意,搜索是区分大小写的。

E.2 元字符

现在,让我们使用正则表达式声明一个更复杂的搜索模式。我们将首先将一个长字符串赋值给 sentence 变量。下一个代码示例将字符串拆分为多行以提高可读性,但您也可以在 Jupyter Notebook 中将其输入为单行:

In  [5] sentence = "I went to the store and bought " \
                   "5 apples, 4 oranges, and 15 plums."

        sentence

Out [5] 'I went to the store and bought 5 apples, 4 oranges, and 15 plums.'

在正则表达式中,我们可以声明 元字符——定义搜索模式的特殊符号。例如,\d 元字符指示 Python 匹配任何数字。假设我们想要识别 sentence 字符串中的所有数字。下一个示例使用正则表达式 "\d" 作为搜索模式调用 findall 函数:

In  [6] re.findall("\d", sentence)

Out [6] ['5', '4', '1', '5']

函数的返回值是按出现顺序排列的 sentence 中的四个数字列表:

  • "5""5 apples"

  • "4""4 oranges"

  • "1""15 plums"

  • "5""15 plums"

我们已经学会了我们的第一个元字符!通过简单的 \d 符号,我们创建了一个匹配目标字符串中任何数字的搜索模式。

在我们继续之前,有两点值得注意:

  • 当一个列表包含许多元素时,Jupyter Notebook 喜欢将每个元素打印在单独的一行上。这种风格方法使得输出更容易阅读,但也导致输出占用大量空间。为了强制 Jupyter 正常打印列表——只在输出了一定数量的字符后添加换行符——从现在起,我们将把 findall 函数调用包裹在 Python 内置的 print 函数中。

  • 我们将把 RegEx 参数作为原始字符串传递给 findall 函数。Python 将原始字符串中的每个字符按字面意思解释。此解析选项防止正则表达式与转义序列之间的冲突。考虑字符序列 \b。它在普通 Python 字符串中有符号意义,在正则表达式中有不同的意义。当我们使用原始字符串时,我们指示 Python 将 \b 解释为字面意义上的反斜杠字符后跟字面意义上的 b 字符。这种语法保证了 Python 将正确解析正则表达式的元字符。

我们使用双引号前的 "r" 字符来声明一个原始字符串。让我们用 print 函数调用和原始字符串重写前面的示例:

In  [7] print(re.findall(r"\d", sentence))

Out [7] ['5', '4', '1', '5']

要声明操作的逆操作,我们交换元字符的大小写。例如,如果 \d 表示“匹配任何数字”,那么 \D 表示“匹配任何非数字”。非数字字符包括字母、空格、逗号和符号。在下一个示例中,我们使用 \D 来识别 sentence 中的所有非数字字符:

In  [8] print(re.findall(r"\D", sentence))

Out [8] ['I', ' ', 'w', 'e', 'n', 't', ' ', 't', 'o', ' ', 't', 'h', 'e', '
        ', 's', 't', 'o', 'r', 'e', ' ', 'a', 'n', 'd', ' ', 'b', 'o',
        'u', 'g', 'h', 't', ' ', ' ', 'a', 'p', 'p', 'l', 'e', 's', ',', '
        ', ' ', 'o', 'r', 'a', 'n', 'g', 'e', 's', ',', ' ', 'a', 'n',
        'd', ' ', ' ', 'p', 'l', 'u', 'm', 's', '.']

现在你已经了解了正则表达式的基础知识,下一步是学习更多元字符并构建复杂的搜索查询。以下是一个示例。\w 元字符匹配任何单词字符,这个类别包括字母、数字和下划线:

In  [9] print(re.findall(r"\w", sentence))

Out [9] ['I', 'w', 'e', 'n', 't', 't', 'o', 't', 'h', 'e', 's', 't', 'o',
         'r', 'e', 'a', 'n', 'd', 'b', 'o', 'u', 'g', 'h', 't', '5', 'a',
         'p', 'p', 'l', 'e', 's', '4', 'o', 'r', 'a', 'n', 'g', 'e', 's',
         'a', 'n', 'd', '1', '5', 'p', 'l', 'u', 'm', 's']

\W 元字符匹配任何非单词字符。非单词字符包括空格、逗号和句号:

In  [10] print(re.findall(r"\W", sentence))

Out [10] [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ',', ' ', ' ', ',', ' ',
         ' ', ' ', '.']

\s 元字符搜索任何空白字符:

In  [11] print(re.findall(r"\s", sentence))

Out [11] [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']

\S 元字符搜索任何非空白字符:

In  [12] print(re.findall(r"\S", sentence))

Out [12] ['I', 'w', 'e', 'n', 't', 't', 'o', 't', 'h', 'e', 's', 't', 'o',
         'r', 'e', 'a', 'n', 'd', 'b', 'o', 'u', 'g', 'h', 't', '5', 'a',
         'p', 'p', 'l', 'e', 's', ',', '4', 'o', 'r', 'a', 'n', 'g', 'e',
         's', ',', 'a', 'n', 'd', '1', '5', 'p', 'l', 'u', 'm', 's', '.']

要搜索特定字符,请在搜索模式中直接声明该字符。以下示例搜索所有出现的字母 "t"。此语法与我们在此附录的第一个示例中使用的是相同的:

In  [13] print(re.findall(r"t", sentence))

Out [13] ['t', 't', 't', 't', 't']

要搜索字符序列,请按顺序在搜索模式中写出这些字符。以下示例在 sentence 字符串中搜索字母 "to"。Python 找到了两次(单词 "to""store" 中的 "to"):

In  [14] print(re.findall(r"to", sentence))

Out [14] ['to', 'to']

\b 元字符声明单词边界。单词边界强制字符相对于空格的位置。以下示例搜索 "\bt"。逻辑可以翻译为“任何在单词边界之后的 t 字符”或等价地,“任何在空格之后的 t 字符”。该模式匹配 "to""the" 中的 "t" 字符:

In  [15] print(re.findall(r"\bt", sentence))

Out [15] ['t', 't']

让我们翻转符号。如果我们使用 "t\b",我们搜索“任何在单词边界之前的 t 字符”或等价地,“任何在空格之前的 t 字符”。Python 匹配的 "t" 字符不同。这些是 "went""bought" 结尾的 "t" 字符:

In  [16] print(re.findall(r"t\b", sentence))

Out [16] ['t', 't']

\B 元字符声明非单词边界。例如,"\Bt" 表示“任何不在单词边界之后的 t 字符”或等价地,“任何不在空格之后的 t 字符”:

In  [17] print(re.findall(r"\Bt", sentence))

Out [17] ['t', 't', 't']

上述示例匹配了 "went""store""bought" 中的 "t" 字符。Python 忽略了 "to""the" 中的 "t" 字符,因为它们出现在单词边界之后。

E.3 高级搜索模式

回顾一下,元字符是在正则表达式中指定搜索序列的符号。第 E.2 节探讨了 \d\w\s\b 元字符,用于数字、单词字符、空格和单词边界。让我们学习一些新的元字符,然后将它们组合成复杂的搜索查询。

点 (.) 元字符匹配任何字符:

In  [18] soda = "coca cola."
         soda

Out [18] 'coca cola.'

In  [19] print(re.findall(r".", soda))

Out [19] ['c', 'o', 'c', 'a', ' ', 'c', 'o', 'l', 'a', '.']

乍一看,这个元字符可能看起来并不特别有用,但与其他符号搭配使用时却效果神奇。例如,正则表达式 "c." 搜索的是紧跟在 "c" 字符后面的任何字符。在我们的字符串中存在三个这样的匹配项:

In  [20] print(re.findall(r"c.", soda))

Out [20] ['co', 'ca', 'co']

如果我们想在字符串中搜索一个字面上的点,该怎么办?在这种情况下,我们必须在正则表达式中用反斜杠转义它。下一个示例中的 "\." 定位到 soda 字符串的末尾:

In  [21] print(re.findall(r"\.", soda))

Out [21] ['.']

之前,我们看到了如何将字符组合起来,以便在目标字符串中按顺序搜索它们。这里,我们搜索 "co" 的确切序列:

In  [22] print(re.findall(r"co", soda))

Out [22] ['co', 'co']

如果我们想搜索字符 "c" 或字符 "o" 中的任何一个,怎么办?为了做到这一点,我们可以将字符包裹在一对方括号中。匹配项将包括目标字符串中 "c""o" 的任何出现:

In  [23] print(re.findall(r"[co]", soda))

Out [23] ['c', 'o', 'c', 'c', 'o']

方括号中字符的顺序不会影响结果:

In  [24] print(re.findall(r"[oc]", soda))

Out [24] ['c', 'o', 'c', 'c', 'o']

假设我们想目标化 "c""l" 之间的任何字符。一个选项是写出方括号内完整的字母字符序列:

In  [25] print(re.findall(r"[cdefghijkl]", soda))

Out [25] ['c', 'c', 'c', 'l']

一个更好的解决方案是使用破折号符号(-)来声明字符的范围。下面的代码示例产生与前面代码相同的列表:

In  [26] print(re.findall(r"[c-l]", soda))

Out [26] ['c', 'c', 'c', 'l']

接下来,让我们探索如何目标化字符的连续多个出现。考虑字符串 "bookkeeper"

In  [27] word = "bookkeeper"
         word

Out [27] 'bookkeeper'

要搜索连续的两个 "e" 字符,我们可以在搜索序列中将它们配对:

In  [28] print(re.findall(r"ee", word))

Out [28] ['ee']

我们还可以使用一对大括号来搜索字符的多个出现。在大括号内,我们声明要匹配的出现次数。在下一个示例中,我们在 "bookkeeper" 中搜索连续的两个 "e" 字符:

In  [29] print(re.findall(r"e{2}", word))

Out [29] ['ee']

如果我们使用 "e{3}" 搜索连续的三个 "e" 字符,返回值将是一个空列表,因为在 "bookkeeper" 中没有三个连续的 "e" 字符序列:

In  [30] print(re.findall(r"e{3}", word))

Out [30] []

我们还可以在大括号内输入两个数字,用逗号分隔。第一个值设置出现次数的下限,第二个值设置出现次数的上限。下一个示例搜索连续出现一次到三次的 "e" 字符。第一个匹配项是 "keeper" 中的连续 "ee" 字符,第二个匹配项是 "keeper" 中的最后一个 "e"

In  [31] print(re.findall(r"e{1,3}", word))

Out [31] ['ee', 'e']

让我们更详细地走一遍这个例子。该模式搜索一行中的一到三个 "e" 字符。当 Python 找到匹配项时,它会继续遍历字符串,直到搜索模式被违反。正则表达式首先单独查看字母 "bookk"。这些字母中没有哪一个符合搜索模式,所以 Python 继续前进。然后模式定位到它的第一个 "e"。Python 还不能将这个匹配项标记为最终匹配,因为下一个字符也可能是一个 "e",所以它检查下一个字符。那个字符确实又是另一个 "e",这符合原始搜索标准。Python 继续到 "p",它不匹配模式,并宣布匹配项是 "ee" 而不是两个单独的 "e" 字符。相同的逻辑重复应用于字符串末尾更接近的 "e"

我们正在取得良好的进展,但所有之前的例子都主要是理论性的。我们在处理现实世界的数据集时如何使用正则表达式?

想象一下,我们正在运行一个客户支持热线并存储电话通话的转录。我们可能有一个这样的消息:

In  [32] transcription = "I can be reached at 555-123-4567\. "\
                         "Look forward to talking to you soon."

         transcription

Out [32] 'I can be reached at 555-123-4567\. Look forward to talking to you 
          soon.'

假设我们想从每个人的消息中提取电话号码,但每个转录都是独特的。然而,我们可以假设电话号码有一个一致的格式,由

  1. 三个数字

  2. 一个破折号

  3. 三个数字

  4. 一个破折号

  5. 四个数字

正则表达式的美在于它可以不受字符串内容的影响地识别这种搜索模式。下一个示例声明了我们迄今为止最复杂的正则表达式。我们只是将元字符和符号组合起来,以描述上述逻辑:

  1. \d{3} 搜索恰好三个数字。

    • 搜索一个破折号。
  2. \d{3} 搜索恰好三个数字。

    • 搜索一个破折号。
  3. \d{4} 搜索恰好四个数字。

    In  [33] print(re.findall(r"\d{3}-\d{3}-\d{4}", transcription))
    
    Out [33] ['555-123-4567']
    

哇!

此外,还有一个方便的 + 元字符,表示“一个或多个”前面的字符或元字符。例如,\d+ 搜索一行中的一或多个数字。我们可以使用 + 符号简化前面的代码。下一个正则表达式持有不同的搜索模式,但返回相同的结果:

  1. 一个或多个连续的数字

  2. 一个破折号

  3. 一个或多个连续的数字

  4. 一个破折号

  5. 一个或多个连续的数字

    In  [34] print(re.findall(r"\d+-\d+-\d+", transcription))
    
    Out [34] ['555-123-4567']
    

我们可以用一行代码从一个动态文本片段中提取电话号码——相当强大的功能。

E.4 正则表达式和 pandas

在第六章中,我们介绍了用于操作字符串 SeriesStringMethods 对象。该对象通过 str 属性提供,并且它的许多方法支持正则表达式参数,这极大地扩展了它们的功能。让我们在实际数据集上练习这些正则表达式概念。

ice_cream.csv 数据集是四个流行品牌(Ben & Jerry’s、Haagen-Dazs、Breyers 和 Talenti)的冰淇淋口味的集合。每一行包括一个品牌、一个口味和描述:

In  [35] ice_cream = pd.read_csv("ice_cream.csv")
         ice_cream.head()

Out [35]

 **Brand               Flavor                         Description**
0  Ben and Jerry's  Salted Caramel Core  Sweet Cream Ice Cream with Blon...
1  Ben and Jerry's  Netflix & Chilll'd™  Peanut Butter Ice Cream with Sw...
2  Ben and Jerry's         Chip Happens  A Cold Mess of Chocolate Ice Cr...
3  Ben and Jerry's              Cannoli  Mascarpone Ice Cream with Fudge...
4  Ben and Jerry's       Gimme S’more!™  Toasted Marshmallow Ice Cream w...

注意:ice_cream 是从 Kaggle 可用的数据集的一个修改版本(www.kaggle.com/tysonpo/ice-cream-dataset)。数据中存在拼写错误和不一致性;我们保留了它们,以便你可以看到在现实世界中出现的真实数据不规则性。我鼓励你考虑如何使用本章中学习的技巧来优化这些数据。

我很好奇我们能在口味中找到多少种不同的巧克力美食。我们的挑战是在描述列中找到紧跟在字符串 "Chocolate" 后面的所有单词。我们可以使用 Series 上的 str.extract 方法来完成这个任务。该方法接受一个正则表达式模式,并返回一个包含匹配项的 DataFrame

让我们构建我们的正则表达式。我们将从一个单词边界 (\b) 开始。然后我们将目标定位在文本 "Chocolate" 上。接下来,我们将强制一个单独的空白字符 (\s)。最后,我们将匹配一行中的一或多个单词字符 (\w+),以捕获所有直到 Python 遇到空格或句点为止的字母数字字母。因此,最终的表达式是 "\bChocolate\s\w+)"

由于技术原因,我们必须在将正则表达式传递给 str.extract 方法时将其括起来。该方法支持高级语法,可以搜索多个正则表达式,而括号将其限制为一个:

In  [36] ice_cream["Description"].str.extract(r"(\bChocolate\s\w+)").head()

Out [36]

 **0**
0               NaN
1               NaN
2     Chocolate Ice
3               NaN
4  Chocolate Cookie

到目前为止,一切顺利。我们的 Series 包含了像索引位置 2 的 "Chocolate Ice" 和索引位置 4 的 "Chocolate Cookie" 这样的匹配项;它还在找不到搜索模式的地方存储了 NaN 值。让我们调用 dropna 方法来删除有缺失值的行:

In  [37] (
             ice_cream["Description"]
             .str.extract(r"(\bChocolate\s\w+)")
             .dropna()
             .head()
         )

Out [37]

 **0**
2      Chocolate Ice
4   Chocolate Cookie
8      Chocolate Ice
9      Chocolate Ice
13  Chocolate Cookie

我们越来越接近了。

接下来,让我们将 DataFrame 转换为 Series。默认情况下,str.extract 方法返回一个 DataFrame 以支持潜在的多个搜索模式。我们可以使用 squeeze 方法将单列 DataFrame 转换为 Series。你可能还记得从 read_csv 导入函数中的相关 squeeze 参数;squeeze 方法可以达到相同的效果:

In  [38] (
             ice_cream["Description"]
             .str.extract(r"(\bChocolate\s\w+)")
             .dropna()
             .squeeze()
             .head()
         )

Out [38] 2        Chocolate Ice
         4     Chocolate Cookie
         8        Chocolate Ice
         9        Chocolate Ice
         13    Chocolate Cookie
         Name: Chocolate, dtype: object

我们的方法链变得越来越长,所以让我们将当前的 Series 赋值给一个 chocolate_flavors 变量:

In  [39] chocolate_flavors = (
            ice_cream["Description"]
            .str.extract(r"(\bChocolate\s\w+)")
            .dropna()
            .squeeze()
        )

我们最终想要识别在 "Chocolate" 之后是什么成分。让我们调用 str.split 方法通过空格的出现来分割每个字符串。在这里,我们不会传递一个包含单个空格的字符串,而是提供一个正则表达式的参数。提醒一下,"\s" 元字符查找单个空白:

In  [40] chocolate_flavors.str.split(r"\s").head()

Out [40] 2        [Chocolate, Ice]
         4     [Chocolate, Cookie]
         8        [Chocolate, Ice]
         9        [Chocolate, Ice]
         13    [Chocolate, Cookie]
         Name: 0, dtype: object

str.get 方法从 Series 中的每个列表中检索一个在一致索引位置的值。在下一个示例中,我们从每个列表中检索第二个元素(索引位置 1),或者等价地,检索原始字符串中 "Chocolate" 后面的单词:

In  [41] chocolate_flavors.str.split(r"\s").str.get(1).head()

Out [41] 2        Ice
         4     Cookie
         8        Ice
         9        Ice
         13    Cookie
         Name: Chocolate, dtype: object

出于好奇,让我们调用 value_counts 方法来看看在所有冰淇淋口味中紧跟 "Chocolate" 的最频繁出现的单词。不出所料, "Ice" 是赢家,而 "Cookie" 则位居第二:

In  [42] chocolate_flavors.str.split(r"\s").str.get(1).value_counts()

Out [42] Ice         11
         Cookie       4
         Chip         3
         Cookies      2
         Sandwich     2
         Malt         1
         Mint         1
         Name: Chocolate, dtype: int64

正则表达式提供了一种在文本中搜索模式的高级方法。我希望你已经对正则表达式的益处以及如何在 pandas 的各种方法中应用它有了更深入的理解。

posted @ 2025-11-17 09:51  绝不原创的飞龙  阅读(11)  评论(0)    收藏  举报