现代数据科学家-Python-与-R-指南-一-

76 阅读1小时+

现代数据科学家 Python 与 R 指南(一)

原文:zh.annas-archive.org/md5/7d2468ffba808f16fc49f377a169de98

译者:飞龙

协议:CC BY-NC-SA 4.0

第一部分:发现新语言

为了开始我们的探索,我们将回顾 Python 和 R 的历史。通过比较和对比这些起源故事,您将更好地理解数据科学领域每种语言的当前状态。如果您想开始编码,请随意跳到第二部分。

Python 和 R 适用于现代数据科学家

最佳结合

作者:Rick J. Scavetta 和 Boyan Angelov

Python 和 R 适用于现代数据科学家

作者:Rick J. Scavetta 和 Boyan Angelov

版权所有 © 2021 Boyan Angelov 和 Rick Scavetta。

印刷于美利坚合众国。

出版:O’Reilly Media, Inc.,1005 Gravenstein Highway North, Sebastopol, CA 95472。

O’Reilly 图书可供教育、商业或销售促销使用。大多数书籍也提供在线版(oreilly.com)。有关更多信息,请联系我们的公司/机构销售部门:800-998-9938 或corporate@oreilly.com

  • 编辑:Angela Rufino 和 Michelle Smith

  • 制作编辑:Katherine Tozer

  • 内部设计:David Futato

  • 封面设计:Karen Montgomery

  • 插图:Kate Dullea

  • 2021 年 9 月:第一版

第一版修订历史

  • 2020-11-06:首次发布

  • 2021-01-26:第二次发布

  • 2021-03-16:第三次发布

  • 2021-05-19:第四次发布

查看oreilly.com/catalog/errata.csp?isbn=9781492093404以获取发布详细信息。

O’Reilly 标志是 O’Reilly Media, Inc. 的注册商标。Python 和 R 适用于现代数据科学家,封面图片及相关商标也是 O’Reilly Media, Inc. 的商标。

本作品所表达的观点属于作者个人观点,不代表出版商的观点。尽管出版商和作者已尽力确保本作品中的信息和指导准确无误,但出版商和作者不对任何错误或遗漏承担责任,包括但不限于因使用或依赖本作品而导致的损害。使用本作品中包含或描述的任何代码示例或其他技术如受开源许可证或他人的知识产权的限制,您有责任确保您的使用符合这些许可证和/或权利。

978-1-492-09340-4

致谢

致我的父母,为我提供了人生中最好的开端。致我的妻子,是我坚实的支柱。致我的孩子们,拥有未来最光明的梦想。

—Boyan Angelov

对于我们所有准备好、愿意和能够通过更广泛的视角看待世界,消除我们中间的“他者”。

—Rick Scavetta

前言

为什么我们写了这本书

我们想向您展示,对工具更加敏感、知情和有目的地的选择是提高生产力的最佳策略。出于这个目标,我们并没有写一本双语词典(好吧,不仅仅是那样,你将在附录 A 中找到这个方便的资源)。关于 Python 与 R 的持续讨论(所谓的“语言战争”)早已不再具有生产力。这让我们想起了马斯洛的锤子:“如果你手头只有一把锤子,看什么都像钉子”。这是一个设定在绝对中的幻想世界观,在那里工具提供了全面解决方案。现实世界的情况是依赖于背景的,一个工匠知道应该适当地选择工具。我们的目标是通过利用所有出色的数据科学工具展示一种新的工作方式。因此,我们旨在开发现代数据科学家思考和工作的方式。

我们选择标题中的“现代”,不仅仅是为了表明我们方法的新颖性。它使我们能够在讨论工具时采取更加细致的立场。什么是现代数据科学?现代数据科学是:

  • 集体性。它不是孤立存在的。它融入到更广泛的网络中,如团队或组织。我们避免术语,当它构建桥梁时采纳它(见“技术交互”,下文)。

  • 简单性。我们的目标是减少在方法、代码和沟通中的不必要的复杂性。

  • 可访问性。这是一个可以评估、理解和优化的开放设计过程。

  • 可推广性。它的基本工具和概念适用于许多领域。

  • 面向外部。它结合了其他领域的发展,并受其启发和影响。

  • 道德诚实。它以人为本。它采纳了道德工作的最佳实践,并考虑了对社区和社会的更广泛视角的后果。我们避免只为短期利益服务的炒作、潮流和趋势。

然而,未来几年内数据科学家的实际工作描述如何演变,我们可以期待这些永恒原则将为其提供坚实的基础。

技术交互

接受世界比任何单一工具能够服务的更广泛、更多样化和更复杂的事实,这提出了一个挑战,最好是直接和早期地加以解决。

这种广阔的视角导致了技术交互的增加。我们必须考虑编程语言、包、命名约定、项目文件架构、集成开发环境(IDE)、文本编辑器等等,这些都将最适合情况。多样性导致复杂性和混乱。

我们的生态系统变得越来越多样化,因此考虑我们的选择是否作为桥梁障碍变得更加重要。我们必须始终努力做出能够与同事和社区建立桥梁的选择,避免那些会使我们孤立和僵化的障碍。我们将会遇到各种选择的多样性,每种情况的挑战在于在个人偏好和共同体可访问性之间找到平衡。

这种挑战存在于所有技术交互中。除了工具选择(一种“硬”技能)外,还包括沟通(一种“软”技能)。内容、风格和传播媒介等因素,仅举几例,也充当特定观众的桥梁障碍

成为 Python 和 R 的双语者是向更广泛数据科学社区成员建立桥梁的一步。

本书适合谁阅读

本书旨在面向职业生涯中级阶段的数据科学家。因此,它并不试图教授数据科学。尽管如此,初入职业生涯的数据科学家也能从本书中受益,了解现代数据科学环境中各种主题、工具或语言的可能性。

我们的目标是弥合 Python 和 R 社区之间的差距。我们希望摆脱部落式的“我们对抗他们”的心态,转向一个统一、高效的社区。因此,本书适合那些看到扩展技能组合、视角和他们的工作能够为各种数据科学项目增加价值的数据科学家。

忽视我们可以利用的强大工具是不负责任的。我们努力开放心态,接受实现编程目标的新的、高效的方法,并鼓励同事们走出舒适区。

此外,第一部分和附录 A 也可作为在需要快速将某个熟悉的概念在不同语言之间进行映射时的有用参考。

先决条件

为了从本书中获得最大的价值,我们假设读者至少熟悉数据科学中的一种主要编程语言,如 Python 和 R。对于了解相关语言(例如 Julia 或 Ruby)的读者,也能够获得良好的价值。

对数据科学工作的一般领域(如数据整理、数据可视化和机器学习)有基本了解是有益的,但不是必须的,以理解例子、工作流场景和案例研究。

本书的组织方式

我们将本书组织成为成年人学习第二门口语的方式。

在第 I 部分,我们从两种语言的起源开始回顾,然后展示这些起源如何影响当前状态,涵盖关键突破。如果你想直接进入语言内容,可以跳到第 II 部分。

在我们与口语语言的类比中,这有助于解释为什么会有不规则动词和复数形式等怪癖。词源学^(1)是有趣的,有助于你欣赏一门语言,比如德语中看似无穷无尽的复数名词形式,但对于口语来说并非必需。

第二部分深入探讨了两种语言的方言,通过提供一个镜像的视角。首先,我们将讨论 Python 用户如何处理与 R 的工作,然后反过来。这不仅会扩展你的技能集,还会扩展你的思维方式,因为你会体会到每种语言的运作方式。

在这部分中,我们将单独对待每种语言,因为我们开始变得双语。就像掌握口语双语一样,我们需要抵制两种战胜冲动。第一个冲动是指出我们母语中某些东西有多么简单、优雅或以某种方式“更好”。祝贺你,但学习新语言不就是这个意思吧?我们要单独学习每种语言。尽管我们将随着学习指出比较,但它们将帮助我们处理母语的包袱。

第二个冲动是不断尝试在两种语言之间进行逐字字面的解释。这阻止了我们用新语言思考(甚至梦想)。有时这是不可能的!我喜欢用的例子是德语中的das schmeckt mir,或意大利语中的ho fame,它们的字面翻译非常糟糕,比如“那对我来说尝起来很好”和“我饿了”。关键在于,不同的语言允许不同的结构。这为我们提供了新的工具和新的思考方式,一旦我们意识到不能将一切都简单地映射到我们以前的知识上。把这些章节看作我们将你对一种语言的知识映射到另一种语言的第一步。

第三部分涵盖了语言应用的现代背景。这包括对开源软件包的广泛生态系统的审查,以及工作流特定方法的多样性。本部分将展示为什么在某些情况下更喜欢一种语言,尽管它们在这一点上仍然是独立的语言。这将帮助你决定在大型数据科学项目的各个部分中使用哪种语言。

在口语中,“lost in translation” 是真实存在的。有些事情在某种语言中表达得更好。在德语中,“mir ist heiß” 和 “ich bin heiß” 在英语中都是 “I’m hot”,但德语使用者会区分天气热和身体热。其他词如 Schadenfreude,由 “Schaden”(损害)和 “Freude”(快乐)组成,意味着因他人困境而感到快乐,或者 Kummerspeck,由 “Kummer”(忧虑)和 “Speck”(培根)组成,指因情感进食而增加的体重,是如此完美,以至于没有必要翻译它们。

第 IV 部分详细介绍了存在于语言之间的现代接口。首先,我们成为了双语者,独立使用每种语言。然后,我们确定如何在两种语言之间选择一种。现在,我们将探讨工具,将我们从分离的、互连的 Python 和 R 脚本转变为单一脚本,以一个工作流程将这两种语言交织在一起。

当你不仅仅是双语者,而是在一个双语社区中工作时,真正的乐趣就开始了。你不仅可以独立使用每种语言进行交流,还可以以其他双语者才能理解和欣赏的新颖方式将它们结合起来。双语不仅仅提供了进入新社区的机会,而且本身就创造了一个新的社区。对于纯粹主义者来说,这纯属折磨,但我希望我们已经超越了那个阶段。双语者可以理解这样的警告:“Ordnungsamt 今天正在监控 Bergmannkiez”。理想情况下,你不是因为忘记了单词而替换它们,而是因为这是情境中的最佳选择。并非所有单词都能很好地翻译,比如 Ordnungsamt(管理局?),而 Bergmannkiez 则是柏林的一个社区,本身不应被翻译。有时,一种语言中的词汇更容易传达信息,比如 Mundschutzpflicht,即在冠状病毒大流行期间强制佩戴口罩。

最后,第七章包含一个案例研究,将详细说明如何根据本书内容实施现代数据科学项目。在这里,我们将看到所有先前的部分如何在一个工作流程中结合起来。

让我们来谈谈

数据科学领域不断发展,我们希望本书能帮助您轻松地在 Python 和 R 之间导航。我们很期待听到您的想法,所以请告诉我们您的工作如何改变!您可以通过本书的伴随网站 moderndata.design/ 联系我们。在那里,您将找到更新的额外内容和一张方便的 Python/R 双语速查表。

本书使用的约定

本书使用以下印刷约定:

Italic

表示新术语、URL、电子邮件地址、文件名和文件扩展名。

Constant width

用于程序列表以及段落内引用程序元素,例如变量或函数名、数据库、数据类型、环境变量、语句和关键字。

Constant width bold

显示用户应按照字面意思输入的命令或其他文本。

Constant width italic

显示应替换为用户提供的值或由上下文确定的值的文本。

小贴士

该元素表示提示或建议。

注意

该元素表示一般注意事项。

警告

该元素指示警告或注意事项。

使用代码示例

补充材料(代码示例、练习等)可在https://github.com/moderndatadesign/PyR4MDS下载。

如果您有技术问题或使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com

本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,您可以在自己的程序和文档中使用它。除非您要复制大量代码,否则无需征得我们的许可。例如,编写一个使用本书中多个代码片段的程序无需许可。销售或分发 O’Reilly 图书示例需要许可。引用本书并引用示例代码回答问题无需许可。将本书大量示例代码整合到您产品的文档中需要许可。

我们感谢您的理解,但通常不需要署名。署名通常包括标题、作者、出版商和 ISBN 号。例如:“书名 by Some Author (O’Reilly). Copyright 2012 Some Copyright Holder, 978-0-596-xxxx-x.”

如果您觉得您使用的代码示例不属于合理使用范围或上述许可权限,请随时联系我们,邮件至permissions@oreilly.com

O’Reilly 在线学习

注意

超过 40 年来,O’Reilly Media一直致力于为公司提供技术和业务培训、知识和见解,帮助其取得成功。

我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专业技能。O’Reilly 的在线学习平台让您随时访问现场培训课程、深度学习路径、交互式编码环境,以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频。更多信息,请访问http://oreilly.com

如何联系我们

有关本书的评论和问题,请联系出版社:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • Sebastopol, CA 95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104 (传真)

我们为这本书创建了一个网页,列出勘误、示例和任何额外信息。您可以在http://www.oreilly.com/catalog/catalogpage上访问此页面。

电邮至bookquestions@oreilly.com发表评论或提出有关本书的技术问题。

欲了解我们的图书和课程的新闻和信息,请访问http://oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

在 Twitter 上关注我们:http://twitter.com/oreillymedia

在 YouTube 上观看我们:http://www.youtube.com/oreillymedia

致谢

致我的父母,给予我最好的人生起点。致我的妻子,作为我的支柱。致我的孩子,拥有最光明的未来梦想。

Boyan Angelov

^(1) 单词起源和含义的研究。

第一章:初生牛犊不怕虎

里克·J·斯卡维塔

博扬·安吉洛夫

我们希望以一句伟大的开场白开始,比如“这是最好的时代,也是最坏的时代……”,但老实说,这只是最好的时代 —— 数据科学正在蓬勃发展!随着它继续成熟,它已经开始分裂成各自的专业领域,就像许多学科随着时间的推移一样。这种成熟是科学计算早期就开始的漫长旅程的结果。我们相信,了解一些 Python 和 R 的起源故事将帮助您更好地理解它们在当今环境中的区别,从而更好地利用它们。

我们不会假装成为科学历史学家,那些追溯伟大发现和人物背后情况的学术人员。我们能做的是为您提供 Python 和 R 的起源及其如何引导我们走向当前局面的精彩摘要。

R 语言的起源

每当我想到 R,我就会想起 FUBU,这是一家成立在 90 年代的街头服装公司。这个名字是一个缩写词,我立即爱上了它:“For Us, By Us”。FUBU 意味着社区,意味着理解你的人民的需求和欲望,并确保你为他们提供了良好的服务。R 就是 FUBU。^(1) 在本章结束时,我确信你会有同样的感觉。一旦我们承认 R 就是 FUBU,它开始变得更有意义了。

我们可以追溯 R 语言的起源到如今传奇般的新泽西贝尔实验室。1976 年,统计编程语言 S 的开发由约翰·钱伯斯领导。一年后,钱伯斯出版了《数据分析的计算方法》,他的同事约翰·图基(也在贝尔实验室)出版了《探索性数据分析》。1983 年,钱伯斯出版了《数据分析的图形方法》。这些书籍为开发计算系统提供了框架,不仅允许统计学家探索、理解和分析数据,还能够传达他们的结果。我们在谈论一个由 FUBU 明星阵容组成的全明星阵容!钱伯斯的合著者包括图基的表亲保罗·A·图基和威廉·克利夫兰。克利夫兰的感知经验实验总结在两本富有洞见的书籍中,至今对数据可视化领域有所启发。在科学计算和统计学的诸多贡献中,图基开发了新颖的可视化方法,如常常被误解的箱线图和克利夫兰为非参数平滑开发了 LOESS 方法。

我们从 S 开始,因为它奠定了最终成为 R 的基础。前一段的信息告诉我们很多关于 S 和 R 的基础知识。首先,统计学家是非常直白的人(S,你懂的吧?)。这是一个非常有用的特征。其次,统计学家需要一种专门用于数据分析的 FUBU 编程语言。他们对制作通用编程语言或操作系统不感兴趣。第三,这些早期关于计算统计学和可视化的书籍简直就是教学美的杰作和精确表达的例子^(2)。尽管技术明显过时,但它们的质量仍然出色。我认为这些书籍为统计学家,尤其是 R 社区,以开放、清晰和包容的方式进行技术交流种下了种子。我相信,这是 R 社区的一大特点,具有深厚的根基。第四,早期对图形方法的强调告诉我们,S 已经关注灵活和高效的数据可视化,这对于理解数据和传达结果都是必要的。因此,S 是关于尽可能轻松地完成最重要的事情,并以真正的 FUBU 方式进行。

最初的 S 发行版在 Unix 上运行,并且是免费提供的。最终,S 成为了 S-PLUS 的实现,并获得了许可。这促使奥克兰大学的 Ross Ihaka 和 Robert Gentleman 于 1991 年发布了另一个开源免费的 S 实现。他们将这个实现命名为 R,以他们的名字首字母命名,这是对 S 名称的戏仿,并且符合使用单个字母命名编程语言的传统。R 的第一个官方稳定 beta 版本v1.0.0于 2000 年 2 月 29 日发布。在此期间,发生了两个重要的发展。首先,建立了 CRAN(综合 R 档案网络),用于在镜像服务器上托管和归档 R 软件包。其次,还成立了 R 核心团队。这个由志愿者组成的团队(目前包括20 名成员)实现了基本的 R 功能,包括文档、构建、测试和发布,以及支持这一切的基础设施。值得注意的是,一些最初的成员仍然参与其中,包括 John Chambers、Ross Ihaka 和 Robert Gentleman。

自 2000 年 R v1.0.0以来发生了很多事情,但迄今为止的故事应该已经让你对 R 作为 FUBU 统计计算工具的独特背景有了初步了解。在我们继续 R 的故事之前,让我们先来看看 Python。

Python 的起源

1991 年,随着 Ross Ihaka 和 Robert Gentleman 开始着手开发将成为 R 的项目,荷兰程序员 Guido van Rossum 发布了 Python。Python 的核心愿景实际上是一个人旨在解决当时常见计算问题的愿景。事实上,van Rossum 多年来被亲切地称为终身仁慈独裁者(BDFL),他在 2018 年退出 Python 领导委员会时放弃了这一头衔。

我们看到 S 是从统计学家进行数据分析的需求中产生的,R 是从开源实现的需求中产生的,那么 Python 解决了什么问题?嗯,并不是数据分析 —— 这一点要晚得多。当 Python 出现时,C 和 C++,两种低级编程语言,非常流行。Python 慢慢成为一种解释型、高级别的替代方案,特别是在 2000 年发布 Python v2(与 R v1.0.0 同年发布)之后。Python 的明确目标是成为一种易于使用和学习的广泛采用的编程语言,具有简单的语法。在这个角色中,它取得了非常好的成功!

你会注意到,与 R 相比,Python 无处不在并且非常多才多艺。你会在网站开发、游戏、系统管理、桌面应用程序、数据科学等领域看到它的身影。当然,R 能做的远不止数据分析,但请记住,R 是 FUBU。如果 R 是 FUBU,Python 则是瑞士军刀。它无处不在,每个人都有一把,但即使它有很多工具,大多数人也只是定期使用单一工具。虽然使用 Python 的数据科学家工作在一个广阔而多样的景观中,他们倾向于找到自己的利基,并专注于他们工作所需的软件包和工作流程,而不是利用这个通用语言的所有方面。

Python 在数据科学领域的广泛流行并非完全因为其数据科学能力。我认为 Python 部分是通过作为通用编程语言的现有用途进入数据科学的。毕竟,探入门槛是成功的一半。分析师和数据科学家在与系统管理和网站开发相关的同事共享和实施脚本时会更容易,因为他们已经知道如何使用 Python 脚本。这在 Python 的广泛采纳中起到了重要作用。Python 很适合利用高性能计算,并有效地实现深度学习算法。R 曾经是,并且可能仍然是,一门较为小众和有些陌生的语言,广泛的计算世界并不真正了解它。

尽管 Python v2 于 2000 年发布,但直到 2005 年,处理数组数据的广泛采用的包才开始萌芽,即NumPy的发布。此时,SciPy,自 2001 年以来提供数据科学基础算法(如优化、积分、微分方程等),开始依赖于NumPy的数据结构。SciPy还提供了特殊的数据结构,如k维树。

一旦核心数据结构和算法的标准包问题解决了,Python 开始在科学计算中广泛应用。低级别的NumPySciPy包为高级包如pandas(2009 年)打下了基础,提供了数据操作和数据结构(如DataFrames)的工具。有时称为 pyData 栈,从此 Python 开始飞速发展。

语言战争开始

早期的 2000 年为后来被称为“语言之战”的舞台铺平了道路。随着 pyData 栈的形成,Python 和 R 的里程碑开始加热竞争。特别突出的四个事件。

首先,2002 年,BioConductor成立为一个新的 R 包仓库和框架,用于处理不断增长的各种生物数据。在此之前,生物信息学家依赖于诸如 MatLab 和 Perl(以及经典的命令行工具和一些手动 Web 接口工具)。MatLab 在特定学科如神经科学中仍然受到青睐。然而,Perl 已经大多被BioConductor取代。BioConductor对生物信息学的影响难以估量。它不仅提供了处理远程基因序列数据库、表达数据、微阵列等的包仓库,还提供了新的数据结构来处理遗传序列。BioConductor持续扩展并深深植根于生物信息学社区。

其次,2006 年发布了IPython包。这是一种在交互式笔记本环境中工作的突破性方式。从 2012 年开始的各种资助后,IPython 最终在 2014 年成熟为Jupyter 项目,现在包括 JupyterLab IDE。用户经常忘记 Jupyter 是“Julia、Python 和 R”的缩写,因为它非常以 Python 为中心。笔记本已经成为 Python 中进行数据科学的主要方式,在 2018 年,Google 发布了Google Colab,一个免费的在线笔记本工具。我们将在第三章中详细探讨这个工具。

第三,2007 年,Hadley Wickham 发表了他的博士论文,其中包括两个会从根本上改变 R 景观的 R 包。第一个叫做reshape,为后来形成的Tidyverse奠定了基础(稍后会详细讨论)。虽然reshape早已退役,但它是我们理解数据结构如何影响我们如何思考和处理数据的第一个窥视。第二个ggplot2则是 Leland Wilkinson 的里程碑之作《图形语法》,提供了直观、高级别的绘图,极大简化了 R 中现有的工具(关于这点详见第五章)。

最后,Python v3 在 2008 年发布。多年来,关于使用 Python 的哪个版本,v2 还是 v3 的问题一直存在。这是因为 Python v3 是不兼容的。幸运的是,自从 2020 年 Python v2 被弃用后,这个问题已经为你解决了。令人惊讶的是,尽管如此,2020 年后你仍然可以购买到预装有 Python 2 的新款 MacBook Pro,因为一些遗留脚本仍然依赖于它。所以 Python 2 仍然存在。

数据科学主导地位之争

到此时,Python 和 R 都已经有了广泛应用于各种数据科学应用的强大工具。随着所谓的“语言战争”持续进行,其他关键发展使每种语言找到了自己的领域。

Python 和 R 都包含在特定的构建中。对于 Python,这是 Anaconda 发行版,目前仍然广泛使用(详见第三章)。对于 R,则是 Revolution Analytics 公司推出的Revolution R Open。尽管他们的 R 构建从未被社区广泛接受,但该公司被微软收购,显示了对 R 语言的强大企业支持。

2011 年,Python 社区预见到机器学习的繁荣,推出了scikit-learn包。2016 年,随着tensorflowkeras用于深度学习的发布,同时得到了健康的企业支持。这也突显了 Python 作为一个高级解释器坐在高性能平台上的优势。例如,你会发现 AWS lambda 用于大规模高并发编程,Numba 用于高性能计算,以及前面提到的TensorFlow用于高度优化的 C++。除了数据科学之外,Python 在部署模型方面的声誉超越了 R,这一点也不足为奇。

2011 年还见证了RStudio IDE 的发布,由同名公司推出,未来几年内,R 社区开始集中在这个工具上。在很多方面,使用 R 就意味着使用 RStudio。RStudio 在推广 R 作为适用于各种数据中心用途的编程语言方面的影响也很重要。

当所有这些都在发生的同时,R 社区的一个不断增长的部分开始转向一套包,其中许多包由 Hadley Wickham 编写或领导,这些包开始重新构思和简化典型的数据工作流程。这些包的大部分工作是标准化 R 函数语法以及输入和输出数据存储结构。最终,这套包开始被非正式地称为“Hadleyverse”。在斯坦福大学 UseR! 2016 年会议的主题演讲中,Wickham 放弃了这一说法,点燃了数字火焰以消除他的名字,并创造了“Tidyverse”这一术语。自加入 RStudio 以来,该公司一直在积极开发和推广 Tidyverse 生态系统,这显然已成为 R 中占主导地位的方言。我们将在第二章中更深入地探讨这一点。

我们可以想象 R 至少包含 2 种“范式”或“方言”。它们可以混合使用,但每种都有自己独特的风格。基础 R^(3)是大多数 R 所采用的,并且可能仍然是如此。Tidyverse重新构想了基础 R,在一个广泛的包和函数组成的宇宙中发挥作用,这些包和函数能很好地协同工作,通常依赖于管道并且偏爱数据框。我认为BioConductor提供了另一种方言,专注于特定学科,即生物信息学。你毫无疑问会发现一些大型包可能包含足够多的特殊性,以至于你可能认为它们是自成一体的方言,但我们不要深究这个问题。R 现在正处于阈值,一些用户只了解(或被教导)Tidyverse的做事方式。基础 R 和 Tidyverse R 之间的区别似乎微不足道,但我见过许多新的 R 学习者努力理解为什么 Tidyverse 存在。部分原因是多年来基础 R 代码仍在积极使用且不可忽视。尽管 Tidyverse 的支持者认为这些包让初学者的生活更加轻松,但竞争的方言可能造成不必要的困惑。

我们也可以想象 Python 包含不同的方言。Python 的“原生”安装是最基本的安装,与导入 pyData 堆栈的环境运行方式不同。大多数数据科学家在 pyData 堆栈中操作,因此方言之间的混淆较少。

关于合作和社区建设的融合

有一段时间,语言之争中主导的态度似乎是“我们对他们”。看着别人电脑屏幕的不屑表情。Python 或 R 似乎最终会在数据科学领域中消失。你好,单一文化!一些数据科学家仍然支持这一点,但我们猜想你不是他们中的一员。也有一段时间,Python 和 R 试图彼此模仿,只是将工作流迁移,使语言不再重要。幸运的是,这些努力并未成功。Python 和 R 都有独特的优势;试图互相模仿似乎忽略了这一点。

今天,许多 Python 和 R 社区的数据科学家认识到这两种语言都非常优秀、有用且互补。回到前言中的一个关键点,数据科学社区已经达成了合作和社区建设的共识,这对每个人都有益处。

我们准备迎接一个新的双语数据科学家社区。挑战在于,许多使用一种语言的用户不太清楚它们如何互补,以及何时使用哪种语言。多年来已经出现了一些解决方案,但我们将在第四部分详细探讨这一点。

总结思考

到目前为止,你应该对我们在 2021 年的位置及其背后的原因有了一个很好的了解。在下一部分中,我们将向每组用户介绍一种新的语言。

最后一点说明:Python 用户称自己为 Pythonistas,这是一个非常酷的名字!R 没有真正的对应,他们也没有得到一个非常酷的动物,但这就是单字母语言的生活。R 用户通常被称为……等等……用 R 用户!(叹号可选)事实上,官方的年度会议称为 useR!(叹号是必须的),出版商 Springer 也有一系列名为同名的持续更新的很出色的书籍。我们将使用这些名称。

图 1-1 概述了本章我们重点介绍的一些重要事件,以及其他一些值得关注的里程碑。

图 1-1. Python 和 R 数据科学里程碑的时间线。

^(1) 好吧,更像是统计学家们的统计学家,但是“FSBS”听起来没有那么响亮。

^(2) 或许除了用于数据分析的计算方法,我承认我没有阅读过。

^(3) Python 用户可能不熟悉术语“基础”。这指的是语言的内置功能,没有任何额外的包安装。Python 本身在数据分析方面已经非常强大。数据科学家在 Python 中默认会导入 PyData 栈。

第二部分:双语主题 I: 学习一门新语言

在这一部分,我将介绍数据科学的两种核心语言:Python 和 R。与其他介绍不同的是,在介绍另一种语言之前,我期望读者对其中一种语言有一定的熟悉度。简而言之,我希望你带着行李来。我想建议你把行李放在门口,但行李是设计用来搬运的,所以这有点难做到。相反,让我们接纳你的行李吧!认识到 Python 和 R 的运作方式有很大不同,你可能并不总能找到一对一的翻译。那没关系!

当我教完全初学者使用 R 和 Python 时,每节课都是一个元素,是整体的基本组成部分。前四个元素是

  1. 函数 - 如何执行动作,即动词。

  2. 对象 - 如何存储信息,即名词。

  3. 逻辑表达式 - 如何提出问题。

  4. 索引 - 如何查找信息。

这四个元素之外还有许多层次,但这些是核心、基本的。一旦你对这些元素有了扎实的掌握,你就有了自行深入的工具。我的目标是让你达到这一点。因此,接下来的章节并不是对每种语言的彻底介绍。

附录 A 包含一个快速参考的 Python/R 双语词典。它将帮助你将你熟悉的代码翻译成你尚不熟悉的新语言。

第二章

如果你是一位 Python 精神的 Pythonista 希望进入 useR 精神,请从这里开始。

第三章

如果你是一位使用者希望进入 Python 精神的 useR,请从这里开始。

一旦你熟悉了你的新语言,请继续学习第三部分,以了解在什么情况下使用每种语言最为适合。

第二章:R for Pythonistas

Rick J. Scavetta

欢迎,勇敢的 Python 程序员,来到 useR 的世界^(1)!在本章中,我们的目标是介绍 R 的核心特性,并试图解决你在学习过程中可能遇到的一些困惑。因此,提到我们打算做什么是有用的。

首先,我们不是为天真的数据科学家编写的。如果你想从零开始学习 R,有很多优秀的资源可供选择;太多了,无法一一列举。我们鼓励你去探索它们,并选择适合你需求和学习风格的资源。在这里,我们将提出可能会让完全的新手感到困惑的话题和问题。我们会绕道解释一些话题,希望能够帮助友好的 Python 程序员更轻松地适应 R。

其次,这不是一本双语词典,你可以在附录 A 中找到它,但是没有上下文,它并不是真正有用的。在这里,我们希望带领你踏上一段探索和理解的旅程。我们希望你能够对 R 有一种感觉,以便开始思考 R,变得双语。因此,为了叙述的完整性,我们可能会在写给完全新手的时候介绍一些项目远比其他时候晚得多的东西。尽管如此,我们希望当你需要在新语言中执行熟悉的任务时,能够回到这一章。

第三,这不是一本全面的指南。一旦你掌握了 R,你就会发现探索更深层次的语言以满足你特定需求的乐趣无穷。正如我们在本书的第一部分中提到的那样,R 社区是多样化的,友好的,欢迎你的——而且乐于助人!我们相信这是少数不那么技术兄弟的文化之一。要了解社区的情况,你可以在 Twitter 上关注 #rstats

运行 R

要在本章中完成练习,你可以在 RStudio Cloud 中访问 R,也可以在本地安装 R 和 RStudio。RStudio Cloud 是一个提供访问 R 实例(通过 RStudio IDE)的平台,它允许你上传自己的数据并分享项目。我们将在接下来的段落中介绍这两种方法。

要使用 RStudio Cloud,请在http://rstudio.cloud/ 上创建一个账户,然后转到我们的公开项目。确保在你的工作空间中保存项目的副本,这样你就有了自己的副本,你会在页眉中看到链接。

你的 RStudio 会话应该看起来像图 2-1 中的样子。打开ch02-r4py/r4py.R,就这样!你已经准备好跟着所有的例子了。要执行命令,请按下ctrl + enter(或 cmd + enter)。

图 2-1. 我们在 RStudio Cloud 中的项目。

要在本地运行 R,您会发现它可以在 Anaconda 发行版中使用,如果您使用的是该发行版,否则您可以直接安装它。首先从https://www.r-project.org/为您的操作系统下载并安装 R。R v4.0 于 2020 年 6 月发布,与 Python v3.x 相比,它具有向后兼容性,但也有一些显著的例外。我们假设您至少正在运行 R 4.0.0:“再次起飞”。每个发布版都得到一个受花生漫画(经典的漫画和电影系列,由查理·布朗,史努比等主演)启发的名称。我认为这是一个不错的个人化的触摸。接下来,从https://rstudio.com/安装 RStudio Desktop IDE。

最后,设置一个要处理的项目。这与虚拟环境有些不同,我们稍后会讨论。有两种典型的方式可以使用预先存在的文件创建项目。

首先,如果您使用 git,您会很高兴地知道 RStudio 也是一个基本的 git 图形用户界面客户端。在 RStudio 中,选择 File > New project > Version Control > Git 并输入存储库 URL [*https://github.com/moderndatadesign/PyR4MDS*](https://github.com/moderndatadesign/PyR4MDS)。项目目录名称将自动使用存储库名称。选择您希望存储存储库的位置,然后单击“创建项目”。

其次,如果您不使用 git,您可以直接下载并解压来自https://github.com/moderndatadesign/PyR4MDS的存储库。在 RStudio 中,选择 File > Existing Directory 并导航到下载的目录。在该目录中将创建一个新的 R 项目文件 *.Rproj

您的 RStudio 会话应该看起来像图 图 2-2。打开 ch02-r4py/r4py.R,就这样!您已准备好跟随所有示例进行操作。要执行命令,请按 ctrl + enter(或 cmd + enter)。

图 2-2. 我们在 RStudio 中的项目。

项目和包

我们可以通过使用内置数据集开始探索 R,并直接深入 Tidyverse(在第一章介绍),但我想稍作停顿,深呼吸,并从头开始我们的故事。让我们先读取一个简单的 csv 文件。为此,我们将使用 ggplot2 包中实际上已经可用的数据集。对于我们的目的,我们对实际分析不太关心,而是关心它在 R 中的执行方式。我已在书籍的存储库中提供了数据集作为文件。

如果您正确设置了项目(参见上文),您只需要执行以下命令即可。如果此命令不起作用,请不要担心,我们很快会回到这个问题。

diamonds <- read.csv("ch02-r4py/data/diamonds.csv")

就像在 Python 中一样,单引号 ('') 和双引号 ("") 是可以互换的,尽管双引号更受欢迎。

现在,文件已导入并作为对象出现在你的全局环境中,你的自定义对象也在其中。你会注意到的第一件事是 RStudio 的环境面板将显示该对象并提供一些摘要信息。这种可爱而简单的设计类似于 VScode 的 Jupyter 笔记本扩展(参见 Chapter 3),它也允许你查看你的环境。虽然这在 RStudio 中是一个标准功能,但在 Python 编写脚本时查看对象列表,或者许多其他语言中,这并不常见。点击对象名称旁边的小蓝箭头将显示文本描述(参见图 Figure 2-3)。

图 2-3. 数据框的下拉菜单。

点击名称将在类似 Excel 的查看器中打开它(参见图 Figure 2-4)。

图 2-4. 表格视图中的数据框。
注意

RStudio 的查看器比 Excel 更好,因为它只在内存中加载你屏幕上看到的内容。你可以搜索特定文本并筛选数据,因此这是一个方便的工具,用来窥探你的数据。

尽管这些功能很好,一些用户认为它们有点过于 GUI^(2) 且有点过于不够 IDE^(3)。Python 爱好者大多数会同意,有些人因此批评 RStudio 的用户体验。我部分同意,因为我看到它可能会鼓励一些不良实践。例如,要导入数据集,你也可以点击“导入数据集…”按钮。如果你在解析文件结构时遇到困难,这可能很方便,但会导致未记录的、不可重现的操作,这非常令人沮丧,因为脚本/项目将不是自包含的。执行导入文件的命令将在控制台中执行,并在历史面板中可见,但除非你明确复制它,否则它不会出现在脚本中。这会导致环境中存在脚本未定义的对象。然而,请记住,RStudio 不等同于 R。你可以在其他文本编辑器中使用 R(例如 ESS(“emacs speaks statistics”)扩展的 emacs)。

如果你无法使用上述命令导入数据,要么(i)该文件不存在于该目录中,要么(ii)你正在错误的 工作目录 中工作,后者更有可能。你可能会被诱导写出像这样糟糕的东西:

diamonds <- read.csv("ch02-r4py/data/diamonds.csv")

在使用 Python 的虚拟环境时,避免使用硬编码路径会更加熟悉。像我们之前所做的那样使用相对路径,确保我们的文件目录包含所有必要的数据文件。工作目录和项目都不是虚拟环境,但它们仍然非常方便,所以让我们来了解一下吧!

工作目录是 R 查找文件的第一个地方。当您使用 R 项目时,工作目录就是您拥有 *.Rproj 文件的位置。因此,ch02-r4py 是我们工作目录中的一个子目录。工作目录的名称或位置无关紧要。只要在 RStudio 中打开项目(*.Rproj 文件),整个项目可以移动到计算机上的任何位置,它依然能够正常工作

警告

如果您没有使用 R 项目,那么您的工作目录可能是您的主目录,在 RStudio 中显示为 project: (None)。这很糟糕,因为您将不得不指定文件的完整路径,而不仅仅是项目中的子目录。您会发现命令 getwd() 获取setwd() 设置工作目录在许多过时的教程中。请不要使用这些命令!它们会导致硬编码完整文件路径的相同问题。

让我们回到我们的命令 diamonds <- read.csv("ch02-r4py/data/diamonds.csv")。您已经注意到一些会让经验丰富的 Python 爱好者感到困惑或激怒的事情。有三件事情特别显眼。

首先,请注意,在 R 中通常使用 <- 作为赋值操作符是普遍且甚至更受欢迎的。您可以像在 Python 中那样使用 =,事实上您会看到一些有经验的用户这样做,但 <- 更加明确,因为 = 也用于在函数调用中为参数分配值,而 Python 爱好者都喜欢明确!

<- 赋值操作符实际上是一个传统操作符,起源于标准化前的 QWERTY 键盘,其中 <- 并不意味着将光标向左移动一个空格,而是字面上使 <- 出现。

其次,请注意函数名称是 read.csv(),不,这不是打字错误。csv() 不是对象 read方法,也不是 read 模块函数。如果这是一个 Python 命令,这两种解释都是完全可以接受的。在 R 中,除了一些明显的例外,. 并不具有特殊意义。如果您习惯于更面向对象的语言,其中 . 是一个特殊字符,这有点令人恼火。

最后,请注意,我们没有初始化任何包来完成此任务。read.*() 函数变体是基本 R 的一部分。有趣的是,如果这些函数不能满足您的需求,还有更新更便捷的读取文件的方法。例如 read_csv() 函数在 readr 包中。我们知道您很兴奋看到那个 _

一般来说,当您看到带有 . 的简单函数时,这些都是旧的基础 R 函数,创建时没有人担心名称中包含 . 会引起混淆。来自较新 Tidyverse 包的函数,例如 readr,倾向于使用 _(见第一章)。它们基本上做同样的事情,但稍作调整,使它们更加用户友好。

让我们看看如何使用readr实现这一点。就像在 Python 中一样,你需要安装这个包。这通常是在 R 控制台中直接完成的,R 中没有pip的等价物。

使用以下命令:

install.packages("tidyverse")
注意

在 RStudio 中,你可以通过在右下角面板中的“Packages”面板上点击“Install”按钮来安装包。输入tidyverse,确保选中“install all dependencies”框,并点击 OK。如果你选择这种方式,请不要点击已安装包名称旁边的复选框。这将初始化包,但不会将其记录在你的脚本中。

默认情况下,这将从 CRAN 安装包及其依赖项,CRAN 是官方 R 包的存储库。官方包经过质量控制,并托管在全球镜像服务器上。第一次这样做时,系统将要求你选择一个镜像站点进行安装。大部分情况下,选择哪一个并不重要。在核心 Tidyverse 包和它们的所有依赖项安装过程中,你会看到很多红色文本。这主要是一种方便的方式,一次性安装许多有用的包。

安装包最常见的问题是在包目录中没有写权限。这会提示你创建一个个人图书馆。你可以随时通过以下方式检查你的包安装在哪里

.libPaths()
[1] "/Library/Frameworks/R.framework/Versions/4.0/Resources/library"

如果你有一个个人图书馆,它将显示在第二个位置。

注意

与 Pythonistas 相比,他们倾向于使用虚拟环境,而 R 用户通常只安装一次包,使其在整个系统范围内可用。在尝试为 R 实现项目特定库的解决方案多次失败后,当前的首选是renv ,即R 环境

与 Python 类似,在安装包后,需要在每个新的 R 会话中初始化它。当我们说初始化加载一个包时,我们实际上是指“使用library()函数加载一个已安装的包,然后附加它到命名空间,即全局环境”。所有你的包组成你的,因此使用library()。可以使用library(tidyverse)加载 Tidyverse 的核心套件包。这是常见的做法,大部分时间没有问题,但你可能希望养成只加载实际需要的包而不是不必要地填充环境的习惯。让我们从包含read_csv()函数的readr开始。

# R
library(readr)

这相当于:

# Python equivalent
import readr

尽管 R 使用面向对象编程(OOP),但大部分时间是在后台操作,因此你永远不会看到像这样的包的奇怪别名:

import readr as rr

这在 R 中只是一个外来概念。在附加包之后,该包中的所有函数和数据集都可在全局环境中使用。

警告

这让我想起另一个可能会漂浮的遗留函数。你必须绝对避免使用 attach()(以及大部分情况下的配对函数 detach())。这个函数允许你将一个对象 attach 到你的全局环境中,就像我们附加一个包一样。因此,你可以直接调用对象中的元素,而不必显式指定对象名称,就像我们在不必每次显式调用包名称的情况下调用包中的函数一样。这种方法不再流行的原因是你可能会有许多想要访问的数据对象,因此可能会出现名称冲突问题(即对象 masking)。另外,这种方法也不够明确。

在继续之前,我想解决另一个加载包的问题。你经常会看到:

require(readr)

require() 函数将加载一个已安装的包,并基于成功与否返回 TRUE/FALSE。这对于测试包是否存在很有用,因此应该仅在必要时使用。大多数情况下,你应该使用 library()

好了,让我们再次读取我们的数据集,这次使用 read_csv() 进行一些简单的比较。

> diamonds_2 <- read_csv("R4Py/diamonds.csv")
Parsed with column specification:
cols(
  carat = col_double(),
  cut = col_character(),
  color = col_character(),
  clarity = col_character(),
  depth = col_double(),
  table = col_double(),
  price = col_double(),
  x = col_double(),
  y = col_double(),
  z = col_double()
)

你会注意到,我们得到了一个更详细的描述发生了什么。

正如我们之前提到的,Tidyverse 的设计选择往往比其更新的老流程更加用户友好。此输出告诉我们表格数据的列名及其类型(参见 Table 2-2)。

R 语言当前的趋势是使用蛇形命名法,单词间用下划线(“_”)分隔,并且只使用小写字母。尽管在 R 中一直以来都存在风格指南的不严格遵循,但是 Advanced R 书籍 提供了很好的建议。Google 也试图推广一个 R 风格指南,但似乎社区对此并不十分严格。这与 Python 的 PEP 8 风格指南形成对比,后者由 Python 创始人 Guido van Rossum 在 Python 早期发布时编写。

Tibbles 的胜利

到目前为止,我们已经两次导入了我们的数据,使用了两种不同的命令。这样做是为了让你看到 R 在幕后的工作方式以及 Tidyverse 与基础包的一些典型行为。我们已经提到,你可以点击环境查看器中的对象来查看它,但通常也会将其打印到控制台。你可能会试图执行:

> print(diamonds)

print() 函数在特定情况下是不必要的,比如在 for 循环内。与 Jupyter 笔记本类似,你可以直接执行对象名称,例如:

> diamonds

这将把对象打印到控制台。我们不会在这里复制它,但如果你执行上面的命令,你会注意到这不是一个好的输出!确实,人们会想知道为什么默认输出在交互模式下允许如此多的内容打印到控制台。现在尝试使用read_csv()读取的数据帧:

> diamonds_2
# A tibble: 53,940 x 10
   carat cut       color clarity depth table price     x     y     z
   <dbl> <chr>     <chr> <chr>   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1 0.23  Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
 2 0.21  Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
 3 0.23  Good      E     VS1      56.9    65   327  4.05  4.07  2.31
 4 0.290 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
 5 0.31  Good      J     SI2      63.3    58   335  4.34  4.35  2.75
 6 0.24  Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
 7 0.24  Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
 8 0.26  Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
 9 0.22  Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
10 0.23  Very Good H     VS1      59.4    61   338  4     4.05  2.39
# … with 53,930 more rows

哇!这比默认的基本 R 版本要好得多。我们有一个整齐的小表,第一行是列名,下面是<>中的数据类型的 3 个字母代码。我们只看到前 10 行,然后有一个告诉我们有多少行未显示的注释。如果我们的屏幕有太多列,我们将在底部看到它们列出来。试试吧,将你的控制台输出设置得非常窄,然后再次执行命令:

# A tibble: 53,940 x 10
   carat cut     color clarity
   <dbl> <chr>   <chr> <chr>
 1 0.23  Ideal   E     SI2
 2 0.21  Premium E     SI1
 3 0.23  Good    E     VS1
 4 0.290 Premium I     VS2
 5 0.31  Good    J     SI2
 6 0.24  Very G… J     VVS2
 7 0.24  Very G… I     VVS1
 8 0.26  Very G… H     SI1
 9 0.22  Fair    E     VS2
10 0.23  Very G… H     VS1
# … with 53,930 more rows,
#   and 6 more variables:
#   depth <dbl>, table <dbl>,
#   price <dbl>, x <dbl>,
#   y <dbl>, z <dbl>

基本的 R 已经对探索性数据分析(EDA)非常好了,但这是下一个级别的便利。那么发生了什么?实际理解这一点非常重要,但首先我们想要强调另外两个有趣的点。

首先,请注意,我们不需要加载整个readr包来访问read_csv()函数。我们可以省略library(readr),直接使用:

> diamonds_2 <- readr::read_csv("R4Py/diamonds.csv")

双冒号操作符::用于访问包中的函数。类似于:

from pandas import read_csv

当用户知道他们只需要一个非常特定的函数来自一个包时,或者两个包中的函数可能会冲突时,他们会使用::。这样可以避免将整个包附加到其命名空间中。

其次,这是我们第一次在 R 中看到实际数据,并且我们立即可以看到编号从 1 开始!(为什么不呢?)。

注意

顺便说一下,打印对象到屏幕上时经常会看到整个表达式周围有圆括号。这只是执行表达式并将对象打印到屏幕上的意思。

(aa <- 8)

它大多数时候只是混乱了命令。除非有必要,否则就直接调用对象。

aa <- 8
aa

此外,只需注释掉(在 RStudio 中使用 ctrl+shift+c)打印行,而不是回去删除所有这些额外的括号,会更容易一些。

好的,让我们来看看这里发生了什么。为什么在打印到控制台时diamondsdiamonds_2看起来如此不同。回答这个问题将帮助我们理解一下 R 如何处理对象。为了回答这个问题,让我们看看这些对象的类:

class(diamonds)
[1] "data.frame"
class(diamonds_2)
[1] "spec_tbl_df" "tbl_df"      "tbl"         "data.frame"

你会从pandas.DataFrame中熟悉data.frame(好吧,我们能承认pandasDataFrame只是 R 中data.frame的一个 Python 实现吗?)。但使用 Tidyverse 的read_csv()函数生成了一个具有三个额外类的对象。在这里要提到的两个是子类tbl_df和类tbl,这两者共同定义了一个tibble(因此tbl),它具有data.frame结构的特性tbl_df

Tibbles 是 Tidyverse 的核心特性,在基本的 R 对象上有很多优势。例如,打印到控制台。回想一下,调用对象名称只是调用 print() 的快捷方式。print() 反过来又有一个处理数据框的方法,现在我们已经加载了 readr 包,它现在有一个处理 tbl_df 类对象的方法。

因此,在后台,我们看到 OOP 原则隐式地处理对象类并调用适用于给定类的方法。方便!令人困惑吗?隐式!我能理解为什么 Python 程序员会感到恼火,但一旦你克服了这个问题,你会发现你可以轻松地继续工作,而不会遇到太多麻烦。

有关类型和探索的一些说明

让我们更深入地研究一下我们的数据,看看 R 是如何存储和处理数据的。数据框是一个二维异构数据结构。听起来很简单,但让我们进一步解释一下。

表 2-1. 示例数据框

名称维度数数据类型
向量1同质
列表1异构
数据框2异构
矩阵2同质
数组n同质

向量是最基本的数据存储形式。它们是一维的和同质的。也就是说,一个接一个地存储每个元素,每个元素都是相同类型的。这就像是一个一维的 numpy 数组,由标量组成。我们不在 R 中引用标量,那只是一个长度为 1 的向量。R 中有许多 类型,以及 4 个常用的“用户定义的原子向量类型”。术语“原子”已经告诉我们,这已经是我们在 Table 2-2 中找到的最基本的东西了。

表 2-2. 数据类型

类型数据框简写Tibble 简写描述
逻辑logi二进制 TRUE/FALSE, T/F, 1/0
整数int整数从 [-Inf,Inf]
双精度num实数从 [-Inf,Inf]
字符串chr所有字母数字字符,包括空格。

另外两种较不常见的用户定义的原子向量类型是 rawcomplex

向量是基本的构建块。关于向量有几件事情需要注意,所以让我们先搞清楚这些再回到数据科学的工作马车,那就是心爱的数据框。

根据信息内容的递增顺序排列了 Table 2-2 中列出的四种用户定义的原子向量类型。当你创建一个向量时,R 会尝试找到能够包含该向量中所有信息的最低信息内容类型。例如,逻辑:

> a <- c(TRUE, FALSE)
> typeof(a)
[1] "logical"

logical 是 R 中的 bool 等价物,但很少被称为布尔或二进制。另外,请注意 TF 本身不是 R 中的保留术语,因此不建议用于逻辑向量,尽管它们是有效的。请使用 TRUEFALSE。让我们来看一下数字:

> b <- c(1, 2)
> typeof(b)
[1] "double"
> c <- c(3.14, 6.8)
> typeof(c)
[1] "double"

R 将根据需要自动在双精度和整数之间进行转换。主要使用双精度进行数学运算,这在数据框中用 numeric 的简写显示出来。除非您明确需要将数字限制为真正的整数,否则 numeric/double 就足够了。如果确实需要将值限制为整数,可以使用 as.*() 函数强制转换为特定类型,或者使用 L 后缀指定数字必须是整数。

> b <- as.integer(c(1, 2))
> typeof(b)
[1] "integer"
> b <- c(1L, 2L)
> typeof(b)
[1] "integer"

字符串是 R 中的字符串版本。你可能知道 Python 中的 str,这在 R 中是一个常见的函数 str(),它显示对象的结构。字符在 R 中也经常被称为字符串,包括在参数和包名称中,这是一个不幸的不一致性。

> d <- c("a", "b")
> typeof(d)
[1] "character"

将它们放在使用 data.frame() 创建的基本数据框中,或者使用使用 tibble() 创建的更近期开发的 tibble 中,得到:

my_tibble <- tibble(a = c(T, F),
                    b = c(1L, 2L),
                    c = c(3.14, 6.8),
                    d = c("a", "b"))
my_tibble
# A tibble: 2 x 4
  a         b     c d
  <lgl> <int> <dbl> <chr>
1 TRUE      1  3.14 a
2 FALSE     2  6.8  b

注意,由于它是一个 tibble,我们从 print() 中得到了漂亮的输出。当我们查看结构时,我们会看到一些令人困惑的特性:

> str(my_tibble)
tibble [2 × 4] (S3: tbl_df/tbl/data.frame)
 $ a: logi [1:2] TRUE FALSE
 $ b: int [1:2] 1 2
 $ c: num [1:2] 3.14 6.8
 $ d: chr [1:2] "a" "b"

str() 是一个经典的基础包函数,提供一些基本的输出,类似于在环境面板中点击对象名称旁边的显示箭头时看到的内容。第一行给出了对象的类别(我们上面已经看到了)。S3 指的是此对象使用的特定面向对象编程系统,本例中是最基本和最宽松的面向对象编程系统。

或者,我们也可以使用 Tidyverse 中 dplyr 包的 glimpse() 函数。

> library(dplyr)
> glimpse(my_tibble)
Rows: 2
Columns: 4
$ a <lgl> TRUE, FALSE
$ b <int> 1, 2
$ c <dbl> 3.14, 6.80
$ d <chr> "a", "b"

注意,表 2-2 还指出了缩写 num,在 glimpse() 的输出中并没有出现。这指的是“numeric”类别,它可以是双精度浮点数或整数类型。

上述示例告诉我们,data.frame 是一个异构的、二维的、每个长度相同的同质一维向量集合。我们将解释为什么 R 在下面打印所有这些美元符号(不,这与你的工资无关!)

命名(内部)事物

我们已经提到,蛇形命名法是目前 R 中命名对象的当前趋势。但是,命名数据框中的列名则完全不同,因为我们只是从源文件的第一行继承了名称。基础 R 中的数据框,例如使用 read.*() 函数系列获取的或者使用 data.frame() 函数手动创建的,不允许使用任何“非法”字符。非法字符包括所有空格和所有在 R 中保留的字符,例如:

  • 算术运算符(+, -, /, *, 等),

  • 逻辑运算符(&, |, 等),

  • 关系运算符(==, !=, >, <, 等)

  • 括号 ([, (, {, < 及其关闭符)

此外,尽管它们可以包含数字,但不能以数字开头。让我们看看会发生什么:

# Base package version
data.frame("Weight (g)" = 15,
           "Group" = "trt1",
           "5-day check" = TRUE)
  Weight..g. Group X5.day.check
1         15  trt1         TRUE

所有非法字符都被替换为!我知道,没错吧?R 真的很喜欢嘲笑你这些 OOP 狂热者!此外,以数字开头的任何变量现在都以X开头。

那么如何导入没有标题的文件?

> diamonds_base_nohead <- read.csv("ch02-r4py/data/diamonds_noheader.csv", header = F)
> names(diamonds_base_nohead)
 [1] "V1"  "V2"  "V3"  "V4"  "V5"  "V6"  "V7"  "V8"  "V9"  "V10"

在 base R 中,如果没有任何标题,给定的名称是V,表示“变量”,后面是该列的数字。

通过readr::read_*()函数中的一个或使用tibble()创建的相同文件将保留非法字符!这似乎微不足道,但实际上这是对 Tidyverse 的严肃批评,如果你开始干预别人的脚本,这是需要密切关注的问题。让我们看看:

> tibble("Weight (g)" = 15,
+            "Group" = "trt1",
+            "5-day check" = TRUE)
# A tibble: 1 x 3
  `Weight (g)` Group `5-day check`
         <dbl> <chr> <lgl>
1           15 trt1  TRUE

注意到用于列Weight (g)5-day check的反引号了吗?现在您需要使用它来转义非法字符。也许这会使命令更具信息性,因为您有完整的名称,但无论如何,您可能仍希望保持简短和具有信息性的列名。关于单位(例如重量的克)的信息是不必要的附加信息,应包含在数据集的图例中。

不仅如此,无头数据集的名称也不同:

> diamonds_tidy_nohead <- read_csv("ch02-r4py/data/diamonds_noheader.csv", col_names = F)
> names(diamonds_tidy_nohead)
 [1] "X1"  "X2"  "X3"  "X4"  "X5"  "X6"  "X7"  "X8"  "X9"  "X10"

不再使用V,而是使用X!这将我们带回到 Tidyverse 作为 R 中的一个独特方言。如果您继承了一个完全基于 base R 的脚本,如果您随意开始添加 Tidyverse 函数,那么您会遇到麻烦。这就像在柏林面包店要求柏林人^(5)一样!

列表

列表是另一种常见的数据结构,但它们与 Python 列表并不完全相同,因此命名可能会令人困惑。事实上,在我们非常短暂的 R 之旅中,我们已经遇到了列表。这是因为``data.framelist`类型的特定类。是的,你没听错。

> typeof(my_tibble)
[1] "list"

表 2-1 告诉我们,列表是一种一维的、异质的对象。这意味着这个一维对象中的每个元素可以是不同类型的,实际上列表不仅可以包含向量,还可以包含其他列表、数据框、矩阵等等。如果每个元素是相同长度的向量,我们最终得到的是类data.frame的表格数据。非常方便,对吧?通常,您会遇到列表作为统计测试的输出,让我们来看看。

PlantGrowth数据框是 R 中的内置对象。它包含两个变量(即列表中的元素,也称为表格数据中的列):weightgroup

> glimpse(PlantGrowth)
Rows: 30
Columns: 2
$ weight <dbl> 4.17, 5.58, 5.18, 6.11, 4.50, 4.6…
$ group  <fct> ctrl, ctrl, ctrl, ctrl, ctrl, ctr…

数据集描述了 30 个观察值(即单个植物,也称为表格数据中的行)在groups描述的三种条件下生长的干燥植物weight(以克计,感谢数据图例)。方便的glimpse()函数并不显示这三组,但经典的str()函数却能显示:

> str(PlantGrowth)
'data.frame':	30 obs. of  2 variables:
 $ weight: num  4.17 5.58 5.18 6.11 4.5 4.61 5.17 4.53 5.33 5.14 ...
 $ group : Factor w/ 3 levels "ctrl","trt1",..: 1 1 1 1 1 1 1 1 1 1 ...

如果您对<fct>Factor w/ 3 levels感到紧张,那么请耐心等待,我们将在讨论列表之后讨论这个问题。

好的,让我们进行一些测试。我们可能想要为weightgroup描述定义一个线性模型:

pg_lm <- lm(weight ~ group, data = PlantGrowth)

lm()是在 R 中定义线性模型的基础性和灵活性函数。我们的模型以公式符号书写,其中weight ~ groupy ~ x。你会认出~作为统计学中“由...描述”的标准符号。输出是一个类lmlist类型:

> typeof(pg_lm)
[1] "list"
> class(pg_lm)
[1] "lm"

在这里,有两件事情我们想要提醒你并进行拓展。

首先,记住我们提到过数据框是相同长度的向量集合?现在我们看到,这只是意味着它是一个特殊类型的列表,其中每个元素都是相同长度的向量。我们可以使用$符号访问列表中的命名元素:

> names(PlantGrowth)
[1] "weight" "group"
> PlantGrowth$weight
 [1] 4.17 5.58 5.18 6.11 4.50 4.61 5.17 4.53 5.33 5.14 4.81 4.17 4.41 3.59
[15] 5.87 3.83 6.03 4.89 4.32 4.69 6.31 5.12 5.54 5.50 5.37 5.29 4.92 6.15
[29] 5.80 5.26

注意它的打印方式,沿着一行,并且每行的开头都以[]开头,其中包含一个索引位置。(我们已经提到过 R 从 1 开始索引了,对吗?)在 RStudio 中,键入$后,你将得到一个列名的自动完成列表。

我们也可以使用相同的符号访问列表中的命名元素:

> names(pg_lm)
 [1] "coefficients"  "residuals"     "effects"       "rank"
 [5] "fitted.values" "assign"        "qr"            "df.residual"
 [9] "contrasts"     "xlevels"       "call"          "terms"
[13] "model"

你可以看到列表是存储统计测试结果的一种很好的方式,因为我们有很多不同类型的输出。例如系数

> pg_lm$coefficients
(Intercept)   grouptrt1   grouptrt2
      5.032      -0.371       0.494

是一个命名的 3 元素长的数值向量。(虽然它的元素被命名了,但是对于原子向量,$运算符无效,但是我们当然还有其他的技巧——见下面的用[]进行索引)。我们没有深入讨论细节,但你可能意识到,鉴于我们的数据,我们期望在我们的模型中有三个系数(估计)。

考虑残差

> pg_lm$residuals
     1      2      3      4      5      6      7      8      9     10
-0.862  0.548  0.148  1.078 -0.532 -0.422  0.138 -0.502  0.298  0.108
    11     12     13     14     15     16     17     18     19     20
 0.149 -0.491 -0.251 -1.071  1.209 -0.831  1.369  0.229 -0.341  0.029
    21     22     23     24     25     26     27     28     29     30
 0.784 -0.406  0.014 -0.026 -0.156 -0.236 -0.606  0.624  0.274 -0.266

它们存储在一个命名的 30 元素长的数值向量中(记住我们有 30 个观测)。所以列表对于存储异构数据非常方便,在 R 中你会经常见到它们,尽管 Tidyverse 致力于数据框架及其变体。

其次,记住我们提到.在大多数情况下并没有特殊含义。嗯,这是一个例外,.确实有含义的一个例外。可能最常见的用法是在定义模型时指定所有。在这里,除了weight列外,PlantGrowth只有一个其他列,我们可以写成:

lm(weight ~ ., data = PlantGrowth)
注意

关于变量类型的说明。通过使用y ~ x的公式,我们在说 x 是“独立”的或者“预测”变量,而 y 则是依赖于 x,或者是对预测的“响应”。

这并不是真正必要的,因为我们这里只有一个自变量,但在某些情况下很方便。ToothGrowth数据集有一个类似的实验设置,但我们在两种条件下测量牙齿生长的长度,一种是特定的补充剂(supp)和其剂量(dose)。

lm(len ~ ., data = ToothGrowth)
# is the same as
lm(len ~ supp + dose, data = ToothGrowth)

但像以往一样,明确地表达有其优点,比如定义更精确的模型:

lm(len ~ supp * dose, data = ToothGrowth)

你能发现两个输出之间的区别吗?指定相互作用是用*进行的^(6)

关于因子的事实

好吧,我们在继续之前需要澄清的最后一件事是因素的现象。因素类似于 Python 中的 pandas category 类型。它们是 R 中一个精彩且有用的类。在大多数情况下,它们存在,你不需要担心它们,但要注意,因为它们的使用和误用会使你的生活成为梦想或悲惨。让我们来看看。

名称“因素”在统计学术语中非常常见,我们可以将它们称为分类变量,就像 Python 一样,但你也会看到它们被称为定性和离散变量,无论是在教科书中还是在特定的 R 包中,比如RColorBrewerggplot2。虽然这些术语都指的是相同类型的变量,但当我们在 R 中说因素时,我们指的是整数类型的类。这就像data.frame 是列表类型的类一样。观察:

> typeof(PlantGrowth$group)
[1] "integer"
> class(PlantGrowth$group)
[1] "factor"

你可以轻易地识别因素,因为在str()的输出(见上文)和普通的向量格式化中,级别会被说明:

> PlantGrowth$group
 [1] ctrl ctrl ctrl ctrl ctrl ctrl ctrl ctrl ctrl ctrl
 [11] trt1 trt1 trt1 trt1 trt1 trt1 trt1 trt1 trt1 trt1
 [21] trt2 trt2 trt2 trt2 trt2 trt2 trt2 trt2 trt2 trt2
Levels: ctrl trt1 trt2

级别是统计学家们称为“组”的名称。另一个特征是,虽然我们有字符,但它们没有用引号括起来!这非常奇怪,因为我们实际上可以将它们视为字符,即使它们是整数类型的(参见表 2-2)。您可能会对使用dput()查看对象的内部结构感兴趣。在这里,我们可以看到我们有一个整数向量c(1L, ...)和两个属性,标签和类。

> dput(PlantGrowth$group)
structure(c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L,
            2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L,
            3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L),
          .Label = c("ctrl", "trt1", "trt2"),
          class = "factor")

标签定义了因素中每个级别的名称,并映射到整数,1ctrl,依此类推。所以当我们打印到屏幕上时,我们只看到名称,而不是整数。这通常被接受为是从内存昂贵的时代遗留下来的用例,当时多次保存整数而不是潜在的长字符向量是有意义的。

到目前为止,我们看到的唯一一种因素实际上是描述名义变量的(没有顺序的分类变量),但我们也有一个很好的解决方案来处理序数变量。看看来自钻石数据集的这个变量:

> diamonds$color
[1] E E E I J J I H E H ..
Levels: D < E < F < G < H < I < J

级别有一定的顺序,即 D 在 E 之前,依此类推。

如何找… 东西

好吧,到目前为止,我们已经看到了 R 如何存储数据以及您需要牢记的各种微妙之处,特别是可能会让 Python 开发者出错的事情。让我们继续逻辑表达式和索引,也就是说:如何找… 东西?

逻辑表达式是关系运算符的组合,它们询问是/否的比较问题,以及逻辑运算符,它们组合这些是/否的问题。

让我们从一个向量开始:

> diamonds$price > 18000
   [1] FALSE FALSE FALSE FALSE FALSE FALSE
   ...

这只是问我们的钻石中哪些比$18,000 更贵。这里有三个关键要记住的事情。

首先,较短对象的长度,这里是未分配的数值向量18000(1 个元素长),将“循环使用”整个较长向量的长度,这里是通过$符号访问的diamonds数据框中的price列(53,940 个元素)。在 Python 中,您可能将其称为广播,当使用numpy数组时,并将向量化作为一个单独的函数。在 R 中,我们简单地将这两者都称为向量化,或向量循环。

第二,这意味着输出向量的长度与最长向量的长度相同,这里是53,940个元素。

第三,每当你看到关系运算符或逻辑运算符时,你知道输出向量将始终是逻辑向量。记住逻辑值是TRUE/FALSE,不是像斯波克先生那样的逻辑。

如果你想结合问题,你需要结合两个完整的问题,比如真的昂贵和小的钻石(高雅!):

> diamonds$price > 18000 & diamonds$carat < 1.5
   [1] FALSE FALSE FALSE FALSE FALSE FALSE
   ...

请注意,上述所有三个关键点仍然成立。当我介绍原子向量类型时,我没有提到逻辑也可以被定义为10。这意味着我们可以对逻辑向量进行数学运算,这非常方便。我们有多少昂贵的小钻石?

> sum(diamonds$price > 18000 & diamonds$carat < 1.5)
[1] 9

(老实说还不够)。它们代表我的数据集的比例是多少?只需除以总观察数即可。

> sum(diamonds$price > 18000 & diamonds$carat < 1.5)/nrow(diamonds)
[1] 0.0001668521

所以这是询问和结合问题。让我们来看一下使用[]进行索引。你已经很熟悉[],但我们觉得在 R 中它们更直接。以下是总结:

表 2-3. 索引

用途数据对象结果
xx[i]向量只包含i个元素的向量
xx列表、数据框、tibble从列表中提取的第i个元素
xx[i]列表、数据框、tibble维持原始结构的第i个元素
xx[i,j]数据框、tibble 或矩阵数据框、tibble 或矩阵的i行和j
xx[i,j,k]数组数组的i行、j列和k维度

ijk是可以在[]内部使用的三种不同类型的向量之一:

  1. 整数向量

  2. 一个逻辑向量,或者

  3. 如果元素有名称,则包含名称的字符向量。

这对你来说应该已经很熟悉了,就像在 Python 中一样。对于整数和逻辑向量,它们可以是未分配的向量,或者是解析为整数或逻辑向量的对象或函数。数字不需要是整数类型,尽管整数更清晰。使用数字/双倍会向下舍入到最接近的整数,但除非有目的,否则尽量避免在索引时使用实数。

让我们从整数开始。我们在这里又稍作一小跳,讨论一下无处不在的:运算符,它不会像你的 Pythonista 大脑告诉你的那样运行。我们将从内置字符向量letters开始,它与在数据框中拥有列一样,例如PlantGrowth$weight

> letters[1] # The 1st element (indexing begins at 1)
[1] "a"

所以这非常直接了。如何倒数计数呢?

> letters[-4] # Everything except the 4th element,
> # (*not* the fourth element, counting backwards!)
 [1] "a" "b" "c" "e" "f" "g" "h" ...

不,这不会发生,-意味着排除一个元素,而不是倒数计数,但这是一个不错的尝试。我们也可以排除一系列的值:

> letters[-(1:20)] # Exclude elements 1 through 20
[1] "u" "v" "w" "x" "y" "z"

当然可以索引一系列的值:

> letters[23:26] # The 23rd to the 26th element
[1] "w" "x" "y" "z"

并且记住,我们可以将这个方法与任何可以给我们整数向量的东西结合起来。length()会告诉我们向量中有多少元素,而lhs:rhs是函数seq(from = lhs, to = rhs, by = 1)的简写,它会创建一系列的值,增量步长为by,在这种情况下默认为 1。

>    # The 23rd to the last element
[1] "w" "x" "y" "z"

这意味着在使用:时,你总是需要一个lhs和一个rhs。很遗憾,但这并不起作用:

> letters[23:] # error

在 R 中,错误地使用[]会产生一个传奇而神秘的错误消息:

> df[1]
Error in df[1] : object of type 'closure' is not subsettable
> t[6]
Error in t[6] : object of type 'closure' is not subsettable

你能告诉我们哪里出错了吗?dft不是定义的数据存储对象,我们可以对其进行索引!它们是函数,因此必须跟随(),其中我们提供参数。[]总是用于子集,而这些函数df()t()是闭包类型的函数,不能进行子集操作。所以实际上这是一个非常明确的错误消息,并且提醒我们不要使用含糊不清的简短名称来调用对象,或者混淆函数和数据存储对象。

这一切都很好,但你可能知道索引的真正威力来自于使用逻辑向量来索引特定的TRUE元素,就像在 Python 中使用类型bool一样。获取用于索引的逻辑向量的最常见方法是使用逻辑表达式(见上文)。这正是numpy掩码的用法。

那么这些花式钻石的颜色是什么?

> diamonds$color[diamonds$price > 18000 & diamonds$carat < 1.5]
[1] D D D D F D F F E
Levels: D < E < F < G < H < I < J

在这里,我们使用价格和克拉来找到我们感兴趣的钻石的颜色。毫不奇怪,它们是最好的颜色分类。你可能会觉得反复写diamonds$很烦人,但我们会认为这样做更明确,这就是我们在 Python 中引用pandasSeries时发生的事情。由于我们正在索引一个向量,我们得到的输出也是一个向量。让我们转向数据框架。我们可以把上面的索引命令写成:

> diamonds[diamonds$price > 18000 & diamonds$carat < 1.5, "color"]
# A tibble: 9 x 1
  color
  <ord>
1 D
2 D
3 D
4 D
5 F
6 D
7 F
8 F
9 E

正如你所期望的,在`` [i,j] 中,i总是指*行*(观察值),而j总是指*列*(变量)。请注意,我们还混合了两种不同类型的输入,但它们在表达式的不同部分工作是因为它们。我们使用一个逻辑向量,其长度与数据框架的观测数相同(感谢向量回收),以获取所有TRUE行,然后我们使用一个字符向量来提取一个命名元素,回想一下数据框架中的每一列都是一个命名元素。这在 R 中是一个非常典型的表达方式。输出是一个数据框架,具体来说是一个 tibble,因为我们在钻石数据框架上使用了索引,而不是在特定的一维向量上。不要被这个主题困扰,但值得注意的是,如果我们没有一个 tibble,对单列进行索引(在j`中)会返回一个向量:

> class(diamonds)
[1] "data.frame"
> diamonds[diamonds$price > 18000 & diamonds$carat < 1.5, "color"]
[1] D D D D F D F F E
Levels: D < E < F < G < H < I < J

这确实令人困惑,并突显了我们始终要意识到数据对象的类别的必要性。Tidyverse 试图通过保持数据框架来解决其中一些问题,即使在基础 R 更倾向于回退到向量的情况下也是如此。下面显示的 Tidyverse 索引函数使事情变得更简单(基础包的简写subset()在大致上也是这样,但在 Tidyverse 上下文中使用filter()更有效)。

> diamonds %>%
+   filter(price > 18000, carat < 1.5) %>%
+   select(color)
# A tibble: 9 x 1
  color
  <ord>
1 D
2 D
3 D
4 D
5 F
6 D
7 F
8 F
9 E

我们在书的第一部分介绍了 Tidyverse 的原理,现在我们看到它的实际应用。上面的%>%允许我们展开对象和函数。例如,我们可以这样写:

> select(filter(diamonds, price > 18000, carat < 1.5), color)

这是一个长而嵌套的函数格式,很难理解。我们可以把%>%读作“然后”,因此可以将上面的整个命令读作“取出 diamonds 数据集,然后根据这些条件进行过滤,然后仅选择这些列”。这在帮助我们理解和阅读代码方面有很大帮助,这也是为什么dplyr被描述为数据分析的语法。对象,如 tibbles,是名词,%>%是我们的标点符号,函数是动词。

表 2-4. 函数描述

函数作用对象描述
select()使用名称或辅助函数提取仅这些列
filter()使用逻辑向量保留仅为 TRUE 的行
arrange()根据特定列中的值重新排序行
---------
summarise()对列应用聚合函数
mutate()对列应用转换函数

dplyr中五个最重要的动词在表 2-4 中列出。我们已经看到filter()select()在实际操作中的应用,接下来让我们看看如何使用summarise()mutate()应用函数。summarise()用于应用聚合函数,返回单个值,如平均值mean()或标准偏差sd()。通常会看到summarise()group_by()函数结合使用。在我们的语法元素类比中,group_by()是一个副词,它修改动词的操作方式。在下面的例子中,我们使用group_by()向我们的数据框添加一个Group属性,因此在summarise中应用的函数是特定于组的。就像pandasDataFrame中的.groupby()方法一样!

> PlantGrowth %>%
+   group_by(group) %>%
+   summarise(avg = mean(weight),
+             stdev = sd(weight))
`summarise()` ungrouping output (override with `.groups` argument)
# A tibble: 3 x 3
  group   avg stdev
  <fct> <dbl> <dbl>
1 ctrl   5.03 0.583
2 trt1   4.66 0.794
3 trt2   5.53 0.443

mutate() 用于应用转换函数,该函数返回与输入相同数量的输出。在这些情况下,结合使用 Tidyverse 语法和原生的[]索引特定值并不罕见。例如,该数据集包含了世界不同地区在四个不同时间点的灌溉面积(千公顷)。

> irrigation <- read_csv("R4Py/irrigation.csv")
Parsed with column specification:
cols(
  region = col_character(),
  year = col_double(),
  area = col_double()
)
> irrigation
# A tibble: 16 x 3
   region      year  area
   <chr>      <dbl> <dbl>
 1 Africa      1980   9.3
 2 Africa      1990  11
 3 Africa      2000  13.2
 4 Africa      2007  13.6
 5 Europe      1980  18.8
 6 Europe      1990  25.3
 7 Europe      2000  26.7
 8 Europe      2007  26.3
...

我们可能希望针对每个地区相对于 1980 年的面积折变进行测量。

irrigation %>%
  group_by(region) %>%
  mutate(area_std_1980 = area/area[year == 1980])
# A tibble: 16 x 4
# Groups:   region [4]
   region      year  area area_std_1980
   <chr>      <dbl> <dbl>         <dbl>
 1 Africa      1980   9.3          1
 2 Africa      1990  11            1.18
 3 Africa      2000  13.2          1.42
 4 Africa      2007  13.6          1.46
 5 Europe      1980  18.8          1
 6 Europe      1990  25.3          1.35
 7 Europe      2000  26.7          1.42
 8 Europe      2007  26.3          1.40
 ...

就像mutate()一样,我们可以添加更多的转换,比如每个时间点的百分比变化:

> irrigation <- irrigation %>%
+   group_by(region) %>%
+   mutate(area_std_1980 = area/area[year == 1980],
+          area_per_change = c(0, diff(area)/area[-length(area)] * 100))
> irrigation
# A tibble: 16 x 5
# Groups:   region [4]
   region      year  area area_std_1980 area_per_change
   <chr>      <dbl> <dbl>         <dbl>           <dbl>
 1 Africa      1980   9.3          1               0
 2 Africa      1990  11            1.18           18.3
 3 Africa      2000  13.2          1.42           20.0
 4 Africa      2007  13.6          1.46            3.03
 5 Europe      1980  18.8          1               0
 6 Europe      1990  25.3          1.35           34.6
 7 Europe      2000  26.7          1.42            5.53
 8 Europe      2007  26.3          1.40           -1.50
 ...

重复

注意,在上述示例中我们没有需要循环的地方。你可能本能地想应用for 循环来计算每个区域的聚合或转换函数,但这并不必要。在 R 中避免 for 循环有点像过去的一种消遣,并且在基础包中使用apply家族的函数时可以发现这一点。

由于向量化对于 R 非常重要,因此有一个非正式的竞赛,看看你能写多少少的 for 循环。我们想象一些用户可能有一个墙上的标志:“距上次 for 循环的天数:”就像工厂对事故的记录一样。

这意味着有一些非常古老的方法来重复任务,以及一些更新的方法使这一过程更加方便。

表 2-5。基础包apply家族

功能用途
apply()将函数应用于矩阵或数据框的每一行或列
lapply()将函数应用于列表中的每个元素
sapply()简化lapply()的输出
mapply()sapply()的多变量版本
tapply()对由索引定义的值应用函数
emapply()在环境中应用函数

老派方法依赖于apply家族的函数,列在表 2-5 中。除了apply(),其他的发音都是第一个字母然后再加上 apply,因此是“t apply”而不是“tapply”。有一点趋势是摒弃这些重复性的工作马,但你仍然会经常看到它们,因此熟悉它们是值得的。这样做还将帮助您理解为什么 Tidyverse 会出现。例如,让我们回到我们对上面的PlantGrowth数据框应用的聚合函数。在 apply 函数族中,我们可以使用:

> tapply(PlantGrowth$weight, PlantGrowth$group, mean)
 ctrl  trt1  trt2
5.032 4.661 5.526
> tapply(PlantGrowth$weight, PlantGrowth$group, sd)
     ctrl      trt1      trt2
0.5830914 0.7936757 0.4425733

你可以想象这样读取,“从PlantGrowth数据集中取出重量列,根据组列中的标签分割值,然后对每组值应用均值函数,然后返回一个命名向量”。

如果你想在那里添加更多的功能,你可以看到这是多么繁琐吗?命名向量可能很方便,但它们并不是你想要存储重要数据的典型方式。

试图简化这一过程的一种尝试在plyr中实现,这是dplyr的前身。plyr的发音类似于多功能手持工具“plyer”。我们使用它如下:

library(plyr)

ddply(PlantGrowth, "group", summarize,
      avg = mean(weight))

尽管这些方法今天仍然有时会被使用,但它们大多已被数据框架为中心的包所取代,因此在dplyr中有个 d(读作 d-plyer):

library(dplyr)
PlantGrowth %>%
  group_by(group) %>%
  summarize(avg = mean(weight))

但是要明确,我们也可以使用其他非常古老的函数返回一个数据框:

> aggregate(weight ~ group, PlantGrowth, mean)
  group weight
1  ctrl  5.032
2  trt1  4.661
3  trt2  5.526

哇,这是一个很棒的函数,不是吗?这个东西真的很古老!你仍然会看到它的身影,为什么不呢,一旦你掌握了它,它就是优雅而有效的,即使它仍然只应用一个函数。然而,倾向于使用更统一的 Tidyverse 框架来进行阅读和学习,这意味着古老的方法正在逐渐淡出。

这些函数自 R 早期就存在,直观地反映了统计学家们 一直在做的事情。它们将数据分割成由某些属性定义的块(行、列、分类变量、对象),然后应用某种操作(绘图、假设检验、建模等),最后以某种方式组合输出(数据框、列表等)。这个过程有时被称为“分割-应用-组合”。意识到这个过程的重复性开始让社区清楚如何开始思考数据,以及如何组织数据。从而诞生了“整洁”数据的概念^(7)。

作为迭代的最后一个例子,你可能熟悉 Python 中的 map() 函数。在 Tidyverse 的 purrr 包中可以找到类似的函数。这对于在列表或向量中重复迭代非常方便,但超出了本书的范围。

最后思考

在 Python 中,你经常听到 Pythonic 这个词。这意味着适当的 Python 语法和执行特定操作的首选方法。这在 R 中并不存在;有很多方法可以达到同样的效果,人们会使用各种方案!此外,他们经常混合使用不同的方言。尽管有些方言比其他方言更容易阅读,但这种混合可能会增加语言的学习难度。

加之不断调整的扩展 Tidyverse。函数被标记为实验性的、休眠的、成熟的、稳定的、有问题的、被取代的和归档的。再加上项目特定包管理或虚拟环境使用的相对宽松标准,你可以想象出一定程度的不满和挫折感。

R 正式在 2020 年庆祝其成立 20 周年,其根源比这要古老得多。然而,有时候感觉 R 正处于青少年的快速成长期。它正试图弄清楚自己是如何突然变得更加庞大的,同时也可能显得既笨拙又酷。融合不同的 R 方言将帮助你更深入地发现其全部潜力。

^(1) useR! 是每年一度的 R 会议,也是由 Springer 出版的一系列书籍。

^(2) “图形用户界面”

^(3) “集成开发环境”

^(4) 面向对象编程

^(5) 柏林人(名词):在柏林,是该市的居民。在其他地方:一种美味的果冻填充、糖粉涂抹的甜甜圈。

^(6) 但我们将详细阐述模型定义的内容留给有兴趣的读者去探索。

^(7) 如果你想进一步了解这个主题,可以查阅 Hadley Wickham 的论文 这里

第三章:面向使用者的 Python

瑞克·J·斯卡维塔

欢迎,勇敢的使用者,来到 Pythonista 的美妙世界!对于许多使用者来说,这个新世界可能看起来更加多样化 —— 因此也更加不一致和令人困惑 —— 比他们在 R 中习惯的要多。但不要因为多样性而担忧 —— 应该庆祝它!在本章中,我将帮助您导航通过丰富多样的 Python 丛林,突出您的 Python 同事可能采取的各种路径(工作流),您以后也可以选择探索。同时,请知道,您最终会找到最适合您和您的工作环境的路径,这会随时间而变化,可能并不是这里所概述的那一条。就像任何好的远足一样,把这条路线当作指南,而不是规则书。

我将讨论这一部分介绍中提到的“四要素”的基本内容:函数对象逻辑表达式索引。但我首先要回答三个问题。

问题 1:要使用哪个版本和构建(分发)?与 R 不同,Python 有几个不同的版本和构建可供选择。

问题 2:要使用哪些工具?广泛的集成开发环境(IDE)、文本编辑器和笔记本,加上许多实现虚拟环境的方法,增加了更多选择。

问题 3:Python 语言 与 R 语言 相比如何?理解面向对象编程为主导的世界,以及大量的类、方法、函数和关键字,是另一个入门的障碍。

我将依次回答这些问题。我的目标是让您足够熟悉阅读和编写 Python,以便您可以在 第三部分 和 第四部分 继续您的双语旅程。我并不打算为数据科学提供一个全面深入的 Python 入门。为此,请访问 O'Reilly 的 Python 数据分析Python 数据科学手册;这一章将帮助您更好地理解那些书籍。

如果你急于开始使用 Python,可以跳过到关于笔记本的部分,“笔记本”,并访问Google Colab 笔记本来学习 Python 的课程,或者在我们的书库的 GitHub 上访问本章的脚本。

版本和构建

尽管 R 有几种不同的分发版本,但大多数用户仍然使用从 r-project.org^(1) 获取的基本版本。对于 Python,至少有四种常见的 Python 构建(即分发版本)供选择。在每种情况下,您还需要考虑 Python 版本

首先,你可能已经注意到系统已经安装了 Python 的一个版本。在我使用的 macOS Big Sur (v11.1) 上,可以通过以下终端命令查看 Python 的版本:

---
$ python --version
Python 2.7.16
---

有趣的是,masOS 也内置了 python3

---
$ python3 --version
Python 3.8.6
---

这些是 macOS 内部使用的 Python 安装;不需要触碰它们。

其次,我们有 原始的 Python —— 纯净的、直接来自源代码的 Python 版本。在撰写本文时,这是版本 3.9。2.x 版本已不再受支持,您应该使用 3.x 版本进行未来的数据科学项目。在您确定您将使用的所有软件包与最新版本兼容之前,最好坚持使用最后一个小更新,本例中为 3.8 版本。事实上,您的系统上可能有多个小版本。

要安装您想要的特定版本,请访问 Python 网站 并按照 下载页面 上的说明操作。

安装方式因系统而异。因此,官方 Python 使用和安装指南 是权威资源。如果遇到安装问题,一个好的起点是对错误消息的通用部分进行文字搜索(用双引号括起来)。

表 3-1 提供了其他信息源,但您最好直接访问源头^(2)。

表 3-1. 安装 Python

平台站点替代方案
Linuxpython.orgPython 3 已经安装好了。
macOSpython.org在终端中使用 brew install python3
Windowspython.org从 Windows Store 安装 Python。

其次,有两种常见的 Conda 构建:Anaconda(又称 Conda)和 minicondaConda 提供了多种编程语言(包括 Python 和 R)的软件包、依赖项和环境管理,尽管它很少用于 R。这些开源构建包括 Python、一套对数据科学有用的软件包以及一系列集成开发环境(包括 RStudio)。Anaconda 包括免费的个人版本和各种商业版本。顾名思义,miniconda 是一个最小的安装程序。我们将在本书的最后部分再次看到 miniconda 的出现。

Anaconda 网站 上有详细的安装说明。您会注意到,Anaconda 可能不会打包最新版本的 Python。例如,在撰写本文时,Anaconda 打包的是 Python 3.8,而不是 3.9。因此,这为我们上面提到的偏爱原始 Python 3.8 提供了一些理由。Anaconda 是一个流行的构建,但对于我们的目的,我们将坚持使用原始的 Python,以避免那些在这一点上只会分散我们注意力的额外功能。因此,我不会进一步考虑这个选项,但如果您选择这条路,我会在需要时提到一些重要的区别。

第四,您可能决定不使用本地 Python 安装,而是使用由Google Colab提供的流行的在线 Python 版本的 Notebooks 接口^(3). 还有其他在线 Notebook 工具,但详细介绍超出了本书的范围。Notebooks 类似于 RMarkdown 文档,但基于 JSON。我们将在后面更详细地讨论它们。

我敢打赌你已经能够猜到,这种早期阶段的多样性可能会在安装特定问题出现时导致混乱。未来,我们将假设您已经准备好本地或云端安装的 Python。

标准工具

与 R 类似,访问 Python 的方式有很多种。常见的方法包括:在命令行上、IDE、基于云的 IDE、文本编辑器和 Notebooks。为简单起见,我不打算专注于在命令行上执行 Python。如果您熟悉在命令行上执行脚本,这是熟悉的领域。如果不熟悉,待会儿您会遇到的。

IDE 包括 JupyterLab、Spyder、PyCharm 和我们心爱的 RStudio。云原生 IDE 包括 AWS Cloud9。这些都是主题的变体,在我的经验中通常不受 Python 爱好者青睐,尽管有使用云端工具的趋势。听起来很奇怪,IDE 并不那么受欢迎,如果有一个很好的 IDE 为什么不使用呢?我认为答案有两个方面。首先,没有一个 IDE 像 RStudio 在用户中那样成为事实上的首选。其次,由于 Python 的使用案例如此广泛,通常甚至在命令行本身执行,对于许多 Python 爱好者来说,使用 IDE 编码并不那么吸引,特别是如果他们来自编码背景并且在没有 IDE 的情况下感觉很舒适。对我来说,这在某种程度上反映了 Python 比 R 更难但更好的叙述。这两种说法都是错误的!抱歉 :/ 尽管如此,您可能会因为看起来舒适的 IDE 而开始使用 Python。在这里,我们认为文本编辑器从长远来看会更好地为您服务。在书的最后部分,当我们将 Python 和 R 合并到一个脚本中时,我们将回到 RStudio。目前,试着抵制默认使用 IDE 的冲动,但请关注可能引导未来趋势的云平台的发展。

文本编辑器

文本编辑器是编写纯 Python 脚本最常见且看似首选的工具。有许多出色的文本编辑器可供选择,每年都在人气上升和下降。SublimeAtomVisual Studio Code(VS Code)甚至是古老的编辑器 vimemacs,以及许多其他编辑器,都在广泛使用中。尽管如此,VS Code,这款由微软开发和强力支持的开源编辑器,在过去几年中已成为首选。扩展市场意味着编辑器为包括 Python 和 R 在内的多种语言提供了强大且简便的支持^(4)。因此,我们将专注于 VS Code。你的第一个练习是获取并安装 VS Code

当你第一次打开 VS Code 时,将会看到欢迎屏幕,如图 3-1)所示。

图 3-1. VS Code 欢迎屏幕。

当你点击左上角的空白文档图标时,将要求你打开一个文件夹或克隆一个 git 仓库(例如来自 GitHub)。我们将选择一个名为 Intro_python 的空文件夹,这个文件夹我们已经创建好了。打开这个文件夹就像在 RStudio 中打开一个项目一样。在这里,我们可以点击新文档图标,并将被要求为新文档命名。就像在图 3-2 中一样,文件名为 helloworld.py

图 3-2. 我们的第一个 Python 脚本。

由于文件扩展名,VS Code 自动检测到你希望在此文档中使用 Python 解释器。与许多其他文本编辑器一样,如果它知道要使用的解释器,VS Code 可以直接执行文档中的代码。请注意图 3-2(右下角),VS Code 将自动要求你安装适用于 Python 的适当扩展,因为我们尚未安装它们(并且页脚从紫色变为蓝色,表示正在使用 Python 解释器)。鼓励你访问市场并考虑自行选择其他扩展,但大多数情况下“少即是多”。在图 3-3 中,显示了扩展主页在安装包时的状态。请注意,这个扩展是由微软直接开发和维护的,就像 VS Code 本身一样,所以我们是在可靠的手中^(5)。

图 3-3. 安装 VS Code Python 扩展。

安装扩展后,将会看到扩展的欢迎页面,显示在图 3-4 中。蓝色页脚现在显示了你正在使用的实际 Python 版本。请记住,你的系统上可能安装了许多不同的版本,在这里我使用的是 v3.8.6

图 3-4. VS Code Python 扩展的欢迎屏幕。

扩展程序欢迎页面的第一项是“创建一个 Jupyter Notebook”。我们很快就会讨论到这一点;现在值得注意的是,我们可以在 VS Code 中同时使用脚本和 Notebooks。还要注意该项目的第一个要点告诉我们,要打开 Notebook,我们应该在命令面板中运行一个命令,您可以通过 Mac 上的键盘快捷键shift + cmd + P(或 PC 上的shift + ctrl + P)来访问该面板^(6)。返回到helloworld.py文件并使用此键盘快捷键来打开命令面板。这是您执行各种命令以使您的 Python 生活更轻松的地方。命令面板是 RStudio 中相对较新的功能,但长期以来一直是导航文本编辑器的标准方式。从市场安装的每个扩展程序都将添加更多您可以在此处访问的命令。我们的第一个命令将是Create New Integrated Terminal (in Active Workspace)。您只需开始输入命令,然后让自动完成完成其工作即可。确保选择(in Active Workspace)选项。请记住,这就像是一个 RStudio 项目,所以我们希望保持在我们的 Active Workspace 中。

这会在屏幕底部打开一个新的终端窗格(图 3-5)。好吧,我们承认,这开始看起来越来越像一个集成开发环境(IDE),但我们不要太兴奋!

图 3-5. 带有命令面板和集成终端窗格的 VS Code。

到目前为止,我们已经选定了一个文本编辑器,并且有了我们的第一个(空的)Python 脚本。看起来我们已经准备好了 — 但还不完全!每当你想创建一个新的 Python 项目时,必须解决两个关键因素:

  • 虚拟(开发)环境,以及

  • 安装包

虚拟环境

大多数用户习惯于使用 RStudio 项目,这些项目将工作目录绑定到项目目录。这些项目非常方便,因为我们无需硬编码路径,并且被鼓励将所有数据和脚本保存在一个目录中。当您在 VS Code 工作空间中打开整个文件夹时,您已经拥有了这一点。

R 项目和 VS Code 工作空间的一个主要缺点是它们无法提供便携、可重现的开发环境!许多用户只有每个包的单个全局安装(参见.libPaths()),很少指定特定的 R 版本。

现在,亲爱的用户,让我们彼此诚实吧:如果你还没有遇到过包版本冲突的问题,那么在某个时候你会遇到的。你已经全局更新了一个包,现在一个旧脚本无法工作,因为它调用了一个不推荐使用的函数,或者使用了函数的默认参数,而这些参数已经改变,或者由于包版本冲突的任何其他原因。这在 R 中是一个令人惊讶地常见的问题,在长时间或协作工作中,这实际上是一种真正令人沮丧的做法。多年来,有许多尝试在 R 中实现某种受控开发环境。最近的尝试,也希望这将最终解决问题的方法,是 renv。如果你没有关注这方面的发展,请访问 RStudio 的 包网站

Python 爱好者长期以来一直使用虚拟环境来保持其项目的未来兼容性,这是 Python 起源于编程优先方法的标志。在这里,虚拟环境只是项目文件夹中一个隐藏的子目录,例如 .venv. 是使它隐藏的关键。你的计算机上有很多隐藏的文件和目录,大部分时间它们是隐藏的,因为你没有在那里乱动的理由。在 .venv 内部,你会找到用于 这个 特定项目的包以及关于 这个 项目使用的 Python 版本的信息。由于每个项目现在都包含一个虚拟环境,其中包含所有的包和适当的包版本(!),只要虚拟环境存在,你就保证项目将持续无限期地工作。我们可以像 Figure 3-6 中可视化不同机器之间的潜在依赖问题,这突显了拥有关于包版本的单一“真相源”的好处。

Figure 3-6. Python 环境中的冲突来源。

就像 Python 中的一切一样,创建虚拟环境有很多方法。我们可以使用 venvvirtualenv 包。如果你在使用 Anaconda,你将使用 conda 的替代方法,这里我们不涉及。venvvirtualenv 之间有一些细微的差异,但在这个故事的这一点上它们是无关紧要的;让我们只使用 venv。在你的新终端窗口中执行 Table 3-2 中的一个命令,就像我在 Figure 3-7 中所做的那样。

Table 3-2. 使用 venv 创建(和激活)虚拟环境。

平台创建激活(最好使用 VS Code 的自动激活)
macOS X & Linuxpython3 -m venv .venvsource .venv/bin/activate
Windowspy -3 -m venv .venv.venv\scripts\activate

Figure 3-7. 在我们的活动工作区创建一个新的虚拟环境。

创建虚拟环境之后,您必须激活它。关于此的终端命令在表 3-2 中给出,但让 VS Code 做它最擅长的事情更方便。它将自动检测到您的新虚拟环境,并询问您是否要激活它(见图 3-8)。走吧!注意左下角的 Python 解释器也会明确提到(.venv): venv(见图 3-9)。

图 3-8. 在 VS Code 中激活虚拟环境。

如果要求安装 Linter pylint,请继续确认。这是 VS Code 将能够在您的脚本中发现错误的方式。

我们马上就会开始安装包,现在让我们试着执行我们的第一个“Hello, world!”命令。返回到你的空helloworld.py文件并输入:

#%%
print('Hello, world!')

实际上,打印是不必要的,但它使我们尝试做的事情变得明确。这看起来很像一个简单的 R 函数,对吧?#%%也不是必需的,但它是 VS Code 中 Python 扩展的一个可爱功能,强烈推荐使用!键入#%%允许我们将长脚本分成可执行的块。这类似于 R Markdown 块,但更简单,并且用于普通的 Python 脚本。要执行命令,请按shift + enter或单击run cell文本,如图 3-9 所示。

图 3-9. 在 Python 中执行您的第一个代码。

您将立即被要求安装 ipyKernel,见图图 3-9(右下角)。确认后,您将获得新窗口中的输出,可见于图 3-10。

图 3-10. 在交互式 Python 查看器中查看命令输出。

好的,现在我们开始做生意了。看起来工作量很大,但是做几次后,你会形成一种常规并掌握要领!

安装软件包

到目前为止,在这个故事中,我们安装了某个版本的 Python,并在 VS Code 中访问了一个工作区,就像在 R 项目中一样。我们还创建了一个虚拟环境,现在准备用我们喜爱的数据科学包来填充它。如果你选择了conda这条路线,你会使用不同的命令,但也将预装最常见的数据科学包。这听起来非常不错,但你可能会发现,当你需要与其他 Python 开发人员合作时,比如数据工程师或系统管理员,他们可能不会使用 Anaconda。我们觉得,了解 Python 的核心,而不是 Anaconda 提供的所有花里胡哨的东西,也有其优点。因此,我们选择了纯粹的路线。

在我们开始讨论包之前,让我们复习一些必要的术语。在 R 中,库是一组单独的包。对于 Python 也是如此,但是 的使用并不那么严格。例如,pandas 这个包提供了 DataFrame 类的对象,它在 pandas 网站上被称为库和包。这种术语混用在 Python 程序员中很常见,所以如果你是一个严谨的命名者,不要被困扰。不过,模块是值得注意的。包是模块的集合。这是有用的知识,因为我们可以加载整个包或其中的一个特定模块。因此,一般而言:库 > 包 > 模块。

在 R 中,你可以使用 install.packages() 函数从 CRAN 安装包。在 Python 中,有两个等效的 CRAN,即 PyPI(Python 包安装器)用于使用原始 Python,以及 conda,用于使用 Anaconda 或 miniconda(稍后我们还将看到如何直接在在线笔记本中在 Google Colab 中安装包)。要使用原始 Python 从 PyPI 安装包,你需要在终端中执行命令。回想一下,我们之前还有一个活动的 VS Code 终端窗口。在终端中执行命令 pip install matplotlib 以在你的虚拟环境中安装 matplotlib 包,如图 3-11 所示。pip 是 Python 的包安装器,也有各种版本。

!

图 3-11. 使用命令行将包安装到虚拟环境中。

你几乎在每个虚拟环境中都会安装的包包括 numpypandasmatplotlibseabornscipy。你不需要一直安装它们,因为它们的包依赖项会自动处理。如果它们已经安装,pip 会告诉你,并且不会安装任何其他内容。你在这里遇到的最常见的错误消息是你的包版本与你的 Python 版本不兼容。对于这个问题,你可以为你的项目使用不同的 Python 内核^(7),或者指定你想要安装的确切包版本。就像在 R 中一样,你只需要安装包一次,但是每次你激活你的环境时都需要 导入 它(即 初始化 它)(见上文)。包安装与在脚本中导入分开似乎很方便。你可能在 R 脚本中看到许多单独的 install.packages() 函数,这有点烦人。

我想提及另外两个重要点。首先,在终端中使用以下命令检查环境中安装的所有包及其版本:

---
$ pip freeze
---

其次,通过执行以下命令将此输出导出到名为 requirements.txt 的文件中:

---
$ pip freeze > requirements.txt
---

其他用户现在可以使用 requirements.txt 文件通过以下命令安装所有必要的包:

---
$ pip install -r requirements.txt
---

笔记本

如果到目前为止您已经按照教程操作,那么您已经准备好进入第三个问题并开始探索 Python 语言。尽管如此,复习笔记本仍然是值得的,所以请继续阅读。如果您在本地设置 Python 时遇到困难,不要担心!Jupyter Notebooks 是一个让您可以松一口气,将安装问题搁置一旁并重新开始的地方。

Jupyter Notebooks 建立在 IPython 的基础上,IPython 起源于 2001 年。Jupyter 代表 JUlia、PYThon 和 R,现在支持数十种编程语言,并且可以在 JupyterLab IDE 或 Jupyter 中直接使用笔记本。笔记本允许您使用 Markdown 编写文本,添加代码块并查看内联输出。这听起来很像 R Markdown!嗯,是和不是。在底层,R Markdown 是一个平面文本文件,可以呈现为 HTML、doc 或 pdf。笔记本专门基于 JSON 的 HTML,可以原生地处理交互式组件。对于使用 R 的人来说,默认情况下,这有点像带有shiny运行时的交互式 R Markdown。这意味着您不会将笔记本作为平面文本文件来构成,这在考虑编辑潜力时是一个重要的区别。

在 Python 编码中,通常会使用纯粹的笔记本。例如,如果你涉足处理大数据用于机器学习的云平台,比如 AWS Sagemaker、Google AI 平台或 Azure ML Studio,你将从笔记本开始。正如我们已经看到的,它们受到 VS Code 的支持。其他在线版本包括 Kaggle 竞赛和发布的 Jupyter Notebooks。另一种在线笔记本的变体可以在 Google Colab 服务中找到。这使您能够使用 Python 后端生成和分发在线笔记本,这也是我们用来探索笔记本的工具。

要熟悉使用笔记本,请使用 Jupyter 的这个在线教程这里。只需点击Notebook Basics panel,特别注意键盘快捷键。

如果您想跟进,您可以在这一章节找到 Google Colab 笔记本这里

图 3-12。使用 Google Colab 开始使用 Python 笔记本的示例。

Python 语言与 R 语言有什么区别呢?

到目前为止,您应该已经选择了两条路中的一条。如果您已经在本地安装了 Python,您应该已经:

  1. 一个项目目录,您将在其中存储数据和脚本文件。

  2. 在该目录中设置一个虚拟环境。

  3. 在该虚拟环境中安装数据科学的典型软件包。

如果您决定使用 Google Colab,您应该已经访问了本章的笔记本(见上文)。

现在是时候通过导入我们将使用的包来开始我们的项目了。在这里,我们会再次看到有多种方法可以做到这一点,但大多数方法都是标准的。让我们来看看。在书的存储库中,您会找到一个练习脚本,其中包含以下命令,或者您可以在 Google Colab Notebook 中跟随。

随着我们逐步介绍这些命令,我们将引入更多新术语——关键字、方法和属性——并讨论它们在 Python 上下文中的含义。

首先,我们可以导入整个包:

import math # Functions beyond the basic maths

这允许我们使用 math 包中的函数。math 包已经安装好了,所以我们不需要使用 pip,但我们确实需要导入它。

这是我们第一次接触 Python 语言的一个常见而重要的方面:关键字,它们类似于 R 语言中的保留字,但数量更多。现在 Python 中有 35 个关键字,可以分成不同的组(见附录 A)。在这里,import 是一个导入关键字。作为习惯于函数式编程的 useR,在 R 中你会使用 library(math)。因此,在这种情况下,你可以把关键字看作是函数的快捷方式,实际上在很多情况下确实是这样。这就像 R 中的运算符(比如 <-+==& 等),它们在底层也是函数的快捷方式。它们不是用经典的函数格式写的,但它们本质上确实是函数。

简而言之,关键字是具有非常具体含义的保留字。在这种情况下,import 代表着一个函数,用于导入 math 包中的所有函数。很多关键字都是这样的,但不是全部。我们稍后会看到一些例子。

但首先,既然我们已经从 math 包中得到了函数,让我们试试这个:

math.log(8, 2)

在这里,我们看到 . 具有特定的含义:在 math 包内部,访问 log() 函数。两个参数分别是数字和基数。所以你可以看到为什么 R Tidyverse 倾向于使用 _ 而不是 . 符号,以及为什么许多 R 函数中无意义的 . 符号会让许多从面向对象编程语言转来的用户感到沮丧。

其次,我们可以导入整个包并给它一个特定的、通常是标准化的别名

import pandas as pd      # For DataFrame and handling
import numpy as np       # Array and numerical processing
import seaborn as sns    # High level Plotting

这里是我们的第二个关键字,as。请注意,它实际上并没有像一个函数一样起作用,除非我们记得 <- 也是一个函数。如果我们发挥一下想象力,我们可以把它想象成在 R 中的以下用法:

dp <- library(dplyr)       # nonsense, but just as an idea

useR 不会这样做,但这是与之最接近的类似命令^(8)。as 关键字总是与 import 一起使用,为访问包或模块的函数提供了一个便捷的别名^(9)。因此,它是调用我们想要的确切函数的明确方式。执行此函数以导入数据集以供将来使用:

plant_growth = pd.read_csv('ch03-py4r/data/plant_growth.csv')

注意再次出现的 .?上面的命令相当于在 R 中执行以下命令:

plant_growth <- readr::read_csv("ch03-py4r/data/plant_growth.csv")

第三,我们可以从一个包中导入一个特定的模块

from scipy import stats # e.g. for t-test function

这里是我们的第三个关键字from。它允许我们进入scipy包并导入stats模块。

第四,我们可以从包中导入特定模块,并为其指定一个通常标准化的别名。

import matplotlib.pyplot as plt # Low level plotting
import statsmodels.api as sm    # Modeling, e.g. ANOVA

最后,我们还可以仅从包中导入特定函数:

from statsmodels.formula.api import ols # For ordinary least squares regression

导入数据集

上面,我们看到如何使用pandas包的函数导入数据集:

plant_growth = pd.read_csv('ch03-py4r/data/plant_growth.csv')

检查数据

在我们开始处理数据之前,查看数据总是一个良好的做法。在 R 中,我们会使用summary()str(),或者如果我们初始化了dplyr,则会使用glimpse()。让我们看看这在 Python 中是如何工作的。

plant_growth.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 30 entries, 0 to 29
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   weight  30 non-null     float64
 1   group   30 non-null     object
dtypes: float64(1), object(1)
memory usage: 608.0+ bytes
plant_growth.describe()

        weight
count	30.000000
 mean	5.073000
  std	0.701192
  min	3.590000
  25%	4.550000
  50%	5.155000
  75%	5.530000
  max	6.310000
plant_growth.head()

  weight	group
0	4.17	ctrl
1	5.58	ctrl
2	5.18	ctrl
3	6.11	ctrl
4	4.50	ctrl

什么??这是我们第一次遇到这个术语,而且又出现了那个无处不在的点符号!info()describe()head()函数是plant_growth对象的方法。方法是由对象调用的函数。与其他函数一样,我们也可以提供特定的参数,尽管在这些情况下,我们将坚持使用默认值。

特别注意info()方法的输出。在这里,我们首次看到 Python 中索引从 0 开始,这与许多编程语言相同 —— 那又为什么不呢?这是 Python 编程的一个重要方面。当我们进行索引时,稍后我们将看到这会产生什么后果。

.info()的输出还告诉我们,我们有一个pandas DataFrame。我们很快会探索不同的对象类。

如何查看plant_growth对象的形状(即维度)和列名?

plant_growth.shape

(30, 2)
plant_growth.columns

Index(['weight', 'group'], dtype='object')

在这种情况下,我们正在调用对象的属性,它们不接收任何括号。因此,我们可以看到,任何给定对象都可以调用其类允许的可允许的方法和属性。您将从 R 中知道这一点,当对象的类允许它在具体函数中使用时。在底层,相同的魔术正在发生。R 首先是函数,其次是 OOP。它在那里,只是我们在函数式编程中不需要太多担心它。为了让您了解在 R 中这是如何工作的,请考虑内置数据集sunspots。它是一个ts类对象(即时间序列)。

# in R
class(sunspots)
[1] "ts"
plot(sunspots)

您可以使用以下代码查找绘图函数的方法:

# in R
methods(plot)

在这里,您将看到plot.ts()方法,当您为plot()函数提供一个ts类对象时,实际上调用的就是它。

最后,您可能会错过实际查看数据集的能力,就像我们可以使用 RStudio 的查看选项一样。别担心!您可以点击交互式 Python 内核中的表格图标,并查看环境中的所有内容。如果您单击DataFrame,它将为您打开一个视图以便您查看它。

数据结构与描述性统计

好了,现在我们已经掌握了方法和属性,让我们看看如何生成一些描述性统计数据。pandas DataFrame非常类似于 R 中的data.frametbl。它是一个二维表格,其中每一列都是一个Series,就像在 R 数据框中列是相同长度的向量一样。就像DataFrame本身一样,Series也有方法和属性。记住group列是分类的。到现在为止,这个命令应该对你有意义:

plant_growth['group'].value_counts()

trt2    10
trt1    10
ctrl    10
Name: group, dtype: int64

[[]]对你来说可能很熟悉,它们按列名索引。然后,这个单列调用一个方法.value_counts(),在本例中计算每个值的观察次数。

这样如何:

np.mean(plant_growth['weight'])

np表示我们将使用我们之前导入的numpy包中的一个函数。在该函数内部,我们提供数值,即plant_growth DataFrame 的weight Series。

关于一些汇总统计数据,你能猜到这个方法会做什么吗?

# summary statistics
plant_growth.groupby(['group']).describe()

就像dplyrgroup_by()函数一样,groupby()方法将允许按照分类变量对每个子集应用下游方法,在本例中是group Series。describe()方法将为每个子集提供一系列汇总统计信息。

这个版本更加具体:

plant_growth.groupby(['group']).agg({'weight':['mean','std']})

你可能猜到.agg()方法代表聚合。聚合函数返回一个单一值(通常)在 R 中,我们会使用summarise()函数来指定它。

对于.agg()方法的输入,{'weight':['mean','std']},这是一个字典(类dict)。你可以将其看作是一个键值对,在这里使用{}定义:

{'weight':['mean','std']}

我们也可以使用dict()函数来达到同样的目的:

dict([('weight', ['mean','std'])])

字典是数据存储对象本身,是标准的纯粹 Python 的一部分,并且正如我们在这里看到的,被用作输入方法和函数中的参数。这与 R 中列表的使用方式相似,在特定情况下用于数据存储和作为参数列表。然而,字典更适合被视为关联数组,因为索引仅由键而不是数字完成。我可以说字典更像是 R 中的环境,因为它包含了许多对象但没有索引,但这可能有点牵强。

让我们深入挖掘一下。以下命令产生相同的输出,但格式不同!

# Produces Pandas Series
plant_growth.groupby('group')['weight'].mean()
# Produces Pandas DataFrame
plant_growth.groupby('group')[['weight']].mean()

注意[[]][]的区别吗?它提醒了你在处理不是 tibbles 的数据框时可能遇到的差异。

数据结构:回到基础

在 Python 中,我们已经看到了三种常见的数据存储对象,pandas DataFramepandas Seriesdict。只有dict是来自原始 Python 的,因此在我们进一步探讨之前,我想看看其他一些基本结构:liststuplesNumPy arrays。我选择在比预期晚得多的时间介绍它们,因为我希望从直觉和频繁使用的数据框架开始。所以在结束之前,让我们确保掌握了基础知识:

首先,就像在 R 中一样,你会看到 Python 中的四种关键数据类型:

表 3-3. Python 中的数据类型。

类型名称示例
bool二进制TrueFalse
int整数7,9,2,-4
float实数3.14, 2.78, 6.45
str字符串所有字母数字字符和特殊字符

接下来,你将遇到列表,这是一维对象。与 R 中的向量不同,每个元素可以是不同的对象,例如另一个一维列表。这里是两个简单的列表:

cities = ['Munich', 'Paris', 'Amsterdam']
dist = [584, 1054, 653]

注意,[]定义了一个列表。实际上,我们在前面定义dict时已经看到了这一点:

{'weight':['mean','std']}

因此,[]{}单独在 Python 中是有效的并且具有不同的行为,与 R 中不同。但请记住,我们之前使用[]来索引数据框,这与 R 非常相似。

plant_growth['weight']

最后,我们有元组,它们类似于列表,但是不可变,即不能更改,它们由()定义,如下所示:

('Munich', 'Paris', 'Amsterdam')

元组的常见用法是函数返回多个值时使用。例如,divmod()函数返回两个数的整数除法和模数的结果:

>>>   divmod(10, 3)
(3, 1)

结果是一个元组,但我们可以解包这个元组,并将每个输出分配给单独的对象:

int, mod = divmod(10, 3)

当定义自定义函数时,这非常方便。在 R 中的等价操作是将输出保存到列表中。

精明的用户可能熟悉由zeallot包引入并由keras流行化的多重赋值操作符%<-%

我想提到的最后一个数据结构是 NumPy 数组。这与一维列表非常相似,但允许进行向量化等操作。例如:

# A list of distances
>>> dist
[584, 1054, 653]
# Some transformation function
>>> dist * 2
[584, 1054, 653, 584, 1054, 653]

这与使用 R 的用法非常不同。如果我们正在处理 NumPy 数组,我们将回到熟悉的领域:

# Make a numpy array
>>> dist_array = np.array(dist)
>>> dist_array * 2
array([1168, 2108, 1306])

索引和逻辑表达式

现在我们有了各种对象,让我们看看如何对它们进行索引。我们已经看到可以使用[]甚至[[]],就像在 R 中看到的那样,但是有几个有趣的区别。请记住,Python 中的索引始终从 0 开始!另外,请注意,在 R 中最常见的操作符之一,即:,在 Python 中以稍微不同的形式再次出现,这里是[start:end]

>>> dist_array
array([ 584, 1054,  653])
>>>  dist_array[:2]
array([ 584, 1054])
>>>  dist_array()
array([1054,  653])

:运算符不需要左右两侧。如果一侧为空,则索引从开始或继续到末尾。起始处是包含的,如果指定了结束,那么结束是不包含的。因此:2获取索引 0 和 1,1:获取从索引 1 到最后一个未指定的元素,因此是包含的。

对于二维数据框架,我们遇到了 pandas 的.iloc,“索引位置”和.loc“位置”方法。

# Rows: 0th and 2nd
>>> plant_growth.iloc[[0,2]]
  weight	group
0	4.17	ctrl
2	5.18	ctrl

# Rows: 0th to 5th, exclusive
# Cols: 1st
>>> plant_growth.iloc[:5, 0]
0    4.17
1    5.58
2    5.18
3    6.11
4    4.50

对于.loc(),我们可以引入逻辑表达式,即关系运算符和逻辑运算符的组合,以提出和组合True/False问题。

>>> plant_growth.loc[(plant_growth.weight <=  4)]
   weight	group
13	3.59	trt1
15	3.83	trt1

对于有关索引和逻辑表达式的更多详细信息,请参阅附录中的笔记。

绘图

好的,让我们来看看描述由组别权重的一些数据可视化。在这里,我们有一个箱线图:

sns.boxplot(x='group', y='weight', data=plant_growth)
plt.show()

只是这些点:

sns.catplot(x="group", y="weight", data=plant_growth)
plt.show()

只是均值和它们的标准偏差:

sns.catplot(x="group", y="weight", data=plant_growth, kind="point")
plt.show()

请注意,我正在使用seaborn包(别名为sns)进行数据可视化,然后使用matplotlibshow()函数将可视化结果打印到屏幕上。

推断统计

在这个数据集中,我们有一个特定的设置,即我们有三个组,并且我们对两个特定的两组比较感兴趣。我们可以通过建立线性模型来实现这一点。

# fit a linear model
# specify model
model = ols("weight ~ group", plant_growth)

# fit model
results = model.fit()

我们可以直接获取模型的系数:

# extract coefficients
results.params.Intercept
results.params["group[T.trt1]"]
results.params["group[T.trt2]"]

最后,让我们来看看我们模型的摘要:

# Explore model results
results.summary()

好的,让我们通过使用这种类型数据的典型统计测试来结束:单因素方差分析。请注意,我们正在使用我们上面拟合的模型results

# ANOVA
# compute anova
aov_table = sm.stats.anova_lm(results, typ=2)

# explore anova results
aov_table
print(aov_table)

如果我们想要进行所有成对比较,我们可以转向图基的 Tukey 显著性差异(HSD)事后检验:

from statsmodels.stats.multicomp import pairwise_tukeyhsd
print(pairwise_tukeyhsd(plant_growth['weight'], plant_growth['group']))

在这个例子中,我们从statsmodel库开始,使用其中的stats包和multicomp模块,并从中提取特定的pairwise_tukeyhsd()函数以导入。在第二行,我们使用连续变量作为第一个参数和分组变量作为第二个参数执行该函数。

最后的想法

在 R 中,自 2016 年以来,已经形成了关于常见实践和工作流程的共识。在 Python 中,有更多的多样性可以在一开始就上手。这种多样性可能看起来令人生畏,但它只是 Python 起源故事和现实世界用例的反映。

如果您是习惯于函数式编程世界的用户,那么掌握面向对象的方法也可能会显得非常困难,但一旦您跨越了这个障碍,您就可以开始利用 Python 真正闪耀的地方,即第三部分的话题。

^(1) 实际上,其他构建很少在合适的公司提到

^(2) 如果您想通过这种方式进入 macOS,请安装homebrew

^(3) 您需要一个 Google 账户来访问这个免费资源。

^(4) 尽管 R 得到支持,但使用者很少在 VS Code 中工作。

^(5) 好吧,至少我们是某人的手下!

^(6) 敏锐的用户可能已经注意到,在 2020 年底,RStudio v1.4 增加了使用相同键盘快捷键调用的命令面板。

^(7) Python 执行后端。

^(8) 但是请记住,当我们开始同时使用 Python 和 R 时,因为我们会看到非常相似的东西。

^(9) 回顾一下log(),你更有可能使用np.log()而不是math.log(),因为它接受更广泛的输入类型。