Python-数据科学手册第二版-一-

312 阅读20分钟

Python 数据科学手册第二版(一)

原文:zh.annas-archive.org/md5/051facaf2908ae8198253e3a14b09ec1

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

什么是数据科学?

这是一本关于使用 Python 进行数据科学的书籍,这立即引发了一个问题:什么是数据科学?这是一个令人惊讶地难以定义的问题,特别是考虑到这个术语已经如此普遍。激烈的批评者们曾将其视为一个多余的标签(毕竟,哪门科学不涉及数据?)或者一个简单的流行语,只存在于简历中或吸引过度热衷的技术招聘者的眼球。

在我看来,这些批评忽略了一些重要的东西。尽管数据科学带有被夸大的光环,但它或许是我们在工业和学术界越来越重要的许多应用中拥有的跨学科技能集合的最佳标签。跨学科的部分至关重要:在我看来,关于数据科学最好的现有定义可以通过德鲁·康威于 2010 年 9 月在他的博客上首次发布的数据科学维恩图来说明(图 P-1)。

虽然一些交集标签有些玩味,但这张图表捕捉到了人们说“数据科学”时我认为的本质:它本质上是一门跨学科的学科。数据科学包括三个明确且有重叠的领域:统计学家的技能,他们知道如何对数据集进行建模和总结(这些数据集越来越大);计算机科学家的技能,能够设计和使用算法来高效存储、处理和可视化这些数据;以及领域专业知识,我们可以称之为在一个学科中的“经典”训练,既需要制定正确的问题,又需要将其答案放入背景中。

在这种情况下,我鼓励您把数据科学看作不是要学习的新知识领域,而是可以应用于您当前专业领域的一组新技能。无论您是在报告选举结果、预测股票回报、优化在线广告点击、识别显微镜照片中的微生物、寻找新的天文物体类别,还是在任何其他领域处理数据,本书的目标是赋予您提出和回答有关所选主题领域的新问题的能力。

数据科学 VD

图 P-1. 德鲁·康威的数据科学维恩图(来源:德鲁·康威,获得授权使用)

这本书是为谁写的?

在我在华盛顿大学以及各种技术会议和聚会上的教学中,我经常听到一个最常见的问题:“我应该如何学习 Python?” 提问的人通常是技术背景的学生、开发人员或研究人员,他们通常已经具有编写代码和使用计算和数值工具的坚实背景。大多数这些人不是想学习 Python 本身,而是想学习这门语言,以便将其作为数据密集型和计算科学的工具使用。尽管针对这一受众的大量视频、博客文章和教程在线上都可以找到,但长期以来我一直对这个问题缺乏一个单一的好答案感到沮丧;这就是这本书的灵感来源。

本书不旨在介绍 Python 或编程的基础知识;我假设读者已经熟悉 Python 语言,包括定义函数、赋值变量、调用对象的方法、控制程序流程以及其他基本任务。相反,它旨在帮助 Python 用户学习如何有效地使用 Python 的数据科学堆栈——如以下章节中提到的库和相关工具——来有效地存储、操作和从数据中获取见解。

为什么选择 Python?

过去几十年间,Python 已经成为科学计算任务的一流工具,包括大数据集的分析和可视化。这可能让 Python 语言的早期支持者感到惊讶:语言本身并不是专门设计用于数据分析或科学计算。Python 在数据科学中的实用性主要源于第三方包的大规模和活跃生态系统:NumPy用于处理同质数组数据,Pandas用于处理异构和标记数据,SciPy用于常见的科学计算任务,Matplotlib用于出版质量的可视化,IPython用于交互式执行和代码共享,Scikit-Learn用于机器学习,以及许多其他工具,这些工具将在接下来的页面中提到。

如果您正在寻找关于 Python 语言本身的指南,我建议参考本书的姊妹项目 Python 旋风之旅。这份简短的报告为熟悉一种或多种其他编程语言的数据科学家提供了 Python 语言基本功能的概览。

书籍大纲

本书的每个编号部分都专注于贡献 Python 数据科学故事基础部分的特定包或工具,并分为短小而自包含的章节,每章讨论一个单一概念:

  • 第一部分,“Jupyter: 超越普通 Python” 介绍了 IPython 和 Jupyter。这些包提供了许多使用 Python 的数据科学家工作的计算环境。

  • 第二部分,“NumPy 简介”专注于 NumPy 库,它提供了ndarray,用于 Python 中密集数据数组的高效存储和操作。

  • 第三部分,“使用 Pandas 进行数据操作”介绍了 Pandas 库,它提供了DataFrame,用于 Python 中标记/列数据的高效存储和操作。

  • 第四部分,“使用 Matplotlib 进行可视化”专注于 Matplotlib,这是一个提供灵活数据可视化能力的 Python 库。

  • 第五部分,“机器学习”侧重于 Scikit-Learn 库,它提供了最重要和已建立的机器学习算法的高效且干净的 Python 实现。

PyData 世界显然比这六个软件包要大得多,而且它每天都在增长。考虑到这一点,我在本书中的每一个地方都尽力提供有关其他有趣工作、项目和软件包的参考信息,它们正在推动 Python 所能做的事情的边界。尽管如此,我专注的软件包目前对 Python 数据科学领域的许多工作至关重要,我预计它们在生态系统继续围绕它们增长的同时将继续保持重要性。

安装考虑事项

安装 Python 及其科学计算库套件是很简单的。本节将概述在设置计算机时需要考虑的一些事项。

尽管有各种安装 Python 的方式,但我建议用于数据科学的一个是 Anaconda 发行版,无论您使用 Windows、Linux 还是 macOS,它的工作方式都类似。Anaconda 发行版有两种版本:

  • Miniconda提供了 Python 解释器本身,以及一个名为conda的命令行工具,它作为一个跨平台的 Python 软件包管理器,类似于 Linux 用户熟悉的 apt 或 yum 工具。

  • Anaconda包含 Python 和 conda,并且还捆绑了一套其他预装的面向科学计算的软件包。由于这一捆绑包的大小,安装过程将消耗几个千兆字节的磁盘空间。

Anaconda 中包含的任何软件包也可以手动安装在 Miniconda 之上;因此,我建议从 Miniconda 开始。

要开始,请下载并安装 Miniconda 包 —— 确保选择带有 Python 3 的版本 —— 然后安装本书中使用的核心包:

[~]$ conda install numpy pandas scikit-learn matplotlib seaborn jupyter

在整个文本中,我们还将使用 Python 科学生态系统中的其他更专业的工具;安装通常只需输入**conda install *packagename***。如果您遇到默认 conda 频道中不可用的软件包,请务必查看conda-forge,这是一个广泛的、社区驱动的 conda 软件包存储库。

要了解 conda 的更多信息,包括关于创建和使用 conda 环境的信息(我强烈推荐),请参阅conda 的在线文档

本书中使用的约定

本书使用以下排版约定:

斜体

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

常量宽度

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

常量宽度粗体

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

常量宽度斜体

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

注意

这个元素表示一个一般的注意事项。

使用代码示例

补充材料(代码示例、图表等)可在http://github.com/jakevdp/PythonDataScienceHandbook下载。

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

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

我们感谢您的致谢,但一般情况下不需要署名。致谢通常包括书名、作者、出版社和 ISBN。例如:"Python 数据科学手册,第 2 版,作者 Jake VanderPlas(O'Reilly)。版权所有 2023 年 Jake VanderPlas,978-1-098-12122-8。"

如果您觉得您对代码示例的使用超出了公平使用范围或上述许可,请随时通过permissions@oreilly.com与我们联系。

O'Reilly 在线学习

注意

超过 40 年来,O'Reilly Media已经为企业提供技术和商业培训、知识和见解,帮助公司取得成功。

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

如何联系我们

请将有关本书的评论和问题发送至出版商:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • Sebastopol, CA 95472

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

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

  • 707-829-0104(传真)

我们为本书建立了一个网页,列出勘误、示例以及任何额外信息。您可以访问该网页:https://oreil.ly/python-data-science-handbook

发送电子邮件至bookquestions@oreilly.com以评论或询问有关本书的技术问题。

关于我们的书籍和课程的新闻和信息,请访问https://oreilly.com

在 LinkedIn 上找到我们:https://linkedin.com/company/oreilly-media

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

在 YouTube 上关注我们:https://youtube.com/oreillymedia

第一部分:Jupyter:超越普通 Python

Python 有很多开发环境的选择,人们经常问我在自己的工作中使用哪一个。我的答案有时会让人惊讶:我偏爱的环境是IPython加上一个文本编辑器(在我这里,取决于我的心情,可能是 Emacs 或 VSCode)。Jupyter 起源于 IPython shell,由 Fernando Perez 在 2001 年创建,作为增强版 Python 解释器,并已发展成为一个旨在提供“研究计算整个生命周期工具”的项目,用 Perez 的话来说。如果 Python 是我们数据科学任务的引擎,你可以把 Jupyter 看作是交互控制面板。

作为 Python 的一个有用的交互界面,Jupyter还为语言提供了一些有用的语法增强;我们将在这里介绍其中最有用的一些增强功能。也许 Jupyter 项目提供的最熟悉的界面是 Jupyter Notebook,这是一个基于浏览器的环境,非常适合开发、协作、共享,甚至是发布数据科学结果。作为笔记本格式有用性的一个例子,无需看得更远,就在你正在阅读的这页:这本书的整个手稿都是作为一组 Jupyter 笔记本来撰写的。

本书的这一部分将首先逐步介绍一些对数据科学实践有用的 Jupyter 和 IPython 特性,特别是它们提供的超出 Python 标准特性的语法。接下来,我们将更深入地讨论一些更有用的魔术命令,这些命令可以加快创建和使用数据科学代码中的常见任务。最后,我们将涉及一些使笔记本在理解数据和共享结果方面变得有用的特性。

第一章:在 IPython 和 Jupyter 中开始

在编写数据科学的 Python 代码时,我通常会在三种工作模式之间切换:我使用 IPython shell 尝试短命令序列,使用 Jupyter Notebook 进行更长时间的交互分析和与他人共享内容,并使用诸如 Emacs 或 VSCode 的交互式开发环境(IDE)创建可重复使用的 Python 包。本章重点介绍前两种模式:IPython shell 和 Jupyter Notebook。虽然软件开发中使用 IDE 是数据科学家工具箱中的重要第三工具,但我们在此不会直接讨论它。

启动 IPython Shell

此书大部分的文本,包括本部分,都不是设计用于被被动吸收的。我建议您在阅读时跟随并尝试所涵盖的工具和语法:通过这样做建立的肌肉记忆将比仅仅阅读要有用得多。首先通过在命令行上键入 ipython 来启动 IPython 解释器;或者,如果您安装了像 Anaconda 或 EPD 这样的发行版,可能会有一个特定于您系统的启动器。

一旦完成此操作,您应该看到如下提示:

Python 3.9.2 (v3.9.2:1a79785e3e, Feb 19 2021, 09:06:10)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.21.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]:

准备好了,现在可以跟着进行。

启动 Jupyter Notebook

Jupyter Notebook 是一个基于浏览器的图形界面,用于 IPython shell,并在此基础上提供了丰富的动态显示功能。除了执行 Python/IPython 语句外,笔记本还允许用户包括格式化文本、静态和动态可视化、数学方程、JavaScript 小部件等。此外,这些文档可以以一种方式保存,让其他人能够打开它们并在自己的系统上执行代码。

尽管您将通过 Web 浏览器窗口查看和编辑 Jupyter 笔记本,但它们必须连接到正在运行的 Python 进程以执行代码。您可以通过在系统 shell 中运行以下命令来启动此进程(称为“内核”):

$ jupyter lab

此命令启动一个本地 Web 服务器,该服务器将对您的浏览器可见。它立即输出一个显示其正在执行的日志;该日志看起来会像这样:

$ jupyter lab
[ServerApp] Serving notebooks from local directory: /Users/jakevdp/ \
PythonDataScienceHandbook
[ServerApp] Jupyter Server 1.4.1 is running at:
[ServerApp] http://localhost:8888/lab?token=dd852649
[ServerApp] Use Control-C to stop this server and shut down all kernels
(twice to skip confirmation).

执行命令后,默认浏览器应自动打开并导航到列出的本地 URL;确切的地址将取决于您的系统。如果浏览器未自动打开,您可以手动打开一个窗口并输入此地址(http://localhost:8888/lab/ 作为示例)。

IPython 中的帮助和文档

如果您在本章中不阅读其他部分,请阅读此部分:我发现在我的日常工作流程中,讨论的工具是 IPython 最具变革性的贡献。

当技术上熟悉的人被要求帮助朋友、家人或同事解决计算机问题时,大多数时候这不是知道答案的问题,而是知道如何快速找到未知答案的问题。在数据科学中也是如此:可搜索的网络资源,如在线文档、邮件列表线程和 Stack Overflow 答案,包含了丰富的信息,甚至(尤其是?)关于您之前搜索过的主题。成为数据科学的有效从业者,不仅仅是记住每种可能情况下应该使用的工具或命令,更重要的是学会如何有效地查找您不知道的信息,无论是通过网络搜索引擎还是其他方式。

IPython/Jupyter 最有用的功能之一是缩小用户与文档之间的差距,帮助用户有效地完成工作。尽管网络搜索在回答复杂问题方面仍然发挥着作用,但仅通过 IPython 就能找到大量信息。IPython 可以在几个按键中帮助回答的问题的一些示例包括:

  • 我如何调用这个函数?它有哪些参数和选项?

  • Python 对象的源代码是什么样子的?

  • 我导入的这个包里面有什么?

  • 这个对象有哪些属性或方法?

在 IPython shell 和 Jupyter Notebook 中提供的工具可以快速访问这些信息,主要包括使用 ? 字符查看文档、使用 ?? 字符查看源代码,以及使用 Tab 键进行自动完成。

使用 ? 访问文档

Python 语言及其数据科学生态系统是为用户设计的,其中一个重要部分是访问文档。每个 Python 对象都包含对称为 docstring 的字符串的引用,大多数情况下将包含对象的简明摘要及其使用方法。Python 有一个内置的 help 函数,可以访问这些信息并打印结果。例如,要查看内置 len 函数的文档,请执行以下操作:

In [1]: help(len)
Help on built-in function len in module builtins:

len(obj, /)
    Return the number of items in a container.

根据您的解释器不同,此信息可能会显示为内联文本或在单独的弹出窗口中。

因为查找对象帮助是如此常见和有用,IPython 和 Jupyter 引入了 ? 字符作为访问此文档和其他相关信息的简写:

In [2]: len?
Signature: len(obj, /)
Docstring: Return the number of items in a container.
Type:      builtin_function_or_method

此表示法适用于几乎任何内容,包括对象方法:

In [3]: L = [1, 2, 3]
In [4]: L.insert?
Signature: L.insert(index, object, /)
Docstring: Insert object before index.
Type:      builtin_function_or_method

或者甚至是对象本身,具有其类型的文档:

In [5]: L?
Type:        list
String form: [1, 2, 3]
Length:      3
Docstring:
Built-in mutable sequence.

If no argument is given, the constructor creates a new empty list.
The argument must be an iterable if specified.

更重要的是,即使是您自己创建的函数或其他对象,这也同样适用!在这里,我们将定义一个带有文档字符串的小函数:

In [6]: def square(a):
  ....:     """Return the square of a."""
  ....:     return a ** 2
  ....:

请注意,为我们的函数创建文档字符串时,我们只需将字符串字面值放在第一行。由于文档字符串通常是多行的,按照惯例,我们使用了 Python 的三引号符号来表示多行字符串。

现在我们将使用 ? 来找到这个文档字符串:

In [7]: square?
Signature: square(a)
Docstring: Return the square of a.
File:      <ipython-input-6>
Type:      function

通过 docstrings 快速访问文档是你应该养成的习惯之一,这是你应该始终将这样的内联文档添加到你编写的代码中的原因之一。

使用 ?? 访问源代码

因为 Python 语言非常易读,通过阅读你感兴趣的对象的源代码通常可以获得更深入的见解。IPython 和 Jupyter 通过双问号 (??) 提供了直接查看源代码的快捷方式:

In [8]: square??
Signature: square(a)
Source:
def square(a):
    """Return the square of a."""
    return a ** 2
File:      <ipython-input-6>
Type:      function

对于像这样的简单函数,双问号可以快速了解其底层细节。

如果你经常使用这个功能,你会注意到有时 ?? 后缀不会显示任何源代码:这通常是因为所讨论的对象不是用 Python 实现的,而是用 C 或其他编译扩展语言实现的。如果是这种情况,?? 后缀会给出与 ? 后缀相同的输出。你会在许多 Python 内置对象和类型中特别发现这种情况,包括前面提到的 len 函数:

In [9]: len??
Signature: len(obj, /)
Docstring: Return the number of items in a container.
Type:      builtin_function_or_method

使用 ? 和/或 ?? 是查找任何 Python 函数或模块功能的强大快速方式。

使用 Tab 补全探索模块

另一个有用的界面是使用 Tab 键自动完成和探索对象、模块和命名空间的内容。在接下来的示例中,我将使用 <TAB> 来指示何时按 Tab 键。

对象内容的 Tab 补全

每个 Python 对象都有与之关联的各种属性和方法。与前面提到的 help 函数类似,Python 有一个内置的 dir 函数,返回这些属性和方法的列表,但实际使用中,Tab 补全接口更容易使用。要查看对象的所有可用属性列表,可以输入对象名称,后跟句点 (.) 字符和 Tab 键:

In [10]: L.<TAB>
            append() count    insert   reverse
            clear    extend   pop      sort
            copy     index    remove

要缩小列表,可以输入名称的第一个或几个字符,然后按 Tab 键查找匹配的属性和方法:

In [10]: L.c<TAB>
             clear() count()
             copy()

In [10]: L.co<TAB>
              copy()  count()

如果只有一个选项,按 Tab 键将为您完成该行。例如,以下内容将立即替换为 L.count

In [10]: L.cou<TAB>

尽管 Python 没有严格强制区分公共/外部属性和私有/内部属性的区别,按照惯例,前置下划线用于表示后者。为了清晰起见,默认情况下省略了这些私有方法和特殊方法,但可以通过显式输入下划线来列出它们:

In [10]: L._<TAB>
           __add__             __delattr__     __eq__
           __class__           __delitem__     __format__()
           __class_getitem__() __dir__()       __ge__            >
           __contains__        __doc__         __getattribute__

为简洁起见,我只显示了输出的前几列。大多数都是 Python 的特殊双下划线方法(通常被昵称为“dunder”方法)。

导入时的 Tab 补全

在从包中导入对象时,Tab 补全也非常有用。在这里,我们将用它来查找以 co 开头的 itertools 包中的所有可能导入:

In [10]: from itertools import co<TAB>
         combinations()                  compress()
         combinations_with_replacement() count()

同样,您可以使用 tab 补全来查看系统上可用的导入(这将根据哪些第三方脚本和模块对您的 Python 会话可见而变化):

In [10]: import <TAB>
            abc                 anyio
            activate_this       appdirs
            aifc                appnope        >
            antigravity         argon2

In [10]: import h<TAB>
            hashlib html
            heapq   http
            hmac

超出 tab 补全:通配符匹配

如果您知道对象或属性名称的前几个字符,tab 补全是有用的,但如果您想要匹配名称中间或末尾的字符,则帮助不大。对于这种用例,IPython 和 Jupyter 提供了使用*字符进行名称通配符匹配的方法。

例如,我们可以使用这个来列出命名空间中名称以Warning结尾的每个对象:

In [10]: *Warning?
BytesWarning                  RuntimeWarning
DeprecationWarning            SyntaxWarning
FutureWarning                 UnicodeWarning
ImportWarning                 UserWarning
PendingDeprecationWarning     Warning
ResourceWarning

注意,* 字符匹配任何字符串,包括空字符串。

同样,假设我们正在寻找一个字符串方法,其中包含单词find在其名称中的某处。我们可以这样搜索:

In [11]: str.*find*?
str.find
str.rfind

我发现这种灵活的通配符搜索可以帮助我在了解新包或重新熟悉熟悉的包时找到特定的命令很有用。

IPython Shell 中的键盘快捷键

如果您在计算机上花费了任何时间,您可能已经发现在工作流程中使用键盘快捷键的用途。最熟悉的可能是 Cmd-c 和 Cmd-v(或 Ctrl-c 和 Ctrl-v),用于在各种程序和系统中复制和粘贴。高级用户往往会走得更远:流行的文本编辑器如 Emacs、Vim 和其他编辑器通过复杂的按键组合为用户提供了一系列不可思议的操作。

IPython shell 不会走这么远,但在输入命令时提供了许多快速导航的键盘快捷键。虽然其中一些快捷键确实在基于浏览器的笔记本中起作用,但本节主要讨论 IPython shell 中的快捷键。

一旦您习惯了这些,它们可以非常有用,可以快速执行某些命令,而无需将手从“home”键盘位置移开。如果您是 Emacs 用户或者有 Linux 风格 shell 的使用经验,则以下内容将非常熟悉。我将这些快捷键分为几个类别:导航快捷键文本输入快捷键命令历史快捷键其他快捷键

导航快捷键

虽然使用左右箭头键在行内向前向后移动是很明显的,但还有其他选项不需要移动手从键盘的“home”位置:

按键动作
Ctrl-a将光标移动到行首
Ctrl-e将光标移动到行尾
Ctrl-b 或左箭头键将光标向后移动一个字符
Ctrl-f 或右箭头键将光标向前移动一个字符

文本输入快捷键

虽然每个人都习惯使用退格键删除前一个字符,但经常需要稍微扭动手指才能按到该键,而且它一次只能删除一个字符。在 IPython 中,有几个快捷键可用于删除您正在输入的文本的某些部分;其中最有用的是删除整行文本的命令。如果你发现自己使用 Ctrl-b 和 Ctrl-d 的组合来删除前一个字符,而不是按退格键,那么你将会知道这些快捷键已经变得本能了!

按键动作
退格键删除行内前一个字符
Ctrl-d删除行内下一个字符
Ctrl-k从光标处剪切文本到行尾
Ctrl-u从行首剪切文本到光标处
Ctrl-y拷贝(即粘贴)之前被剪切的文本
Ctrl-t转置(即交换)前两个字符

命令历史快捷键

或许在这里讨论的最有影响力的快捷键是 IPython 提供的用于导航命令历史记录的快捷键。此命令历史记录超出了您当前的 IPython 会话:您的整个命令历史记录存储在 IPython 配置文件目录中的 SQLite 数据库中。

访问先前的命令最简单的方法是使用上箭头和下箭头键来浏览历史记录,但还有其他选项:

按键动作
Ctrl-p(或向上箭头键)访问历史记录中的上一个命令
Ctrl-n(或向下箭头键)访问历史记录中的下一个命令
Ctrl-r通过命令历史记录进行逆向搜索

逆向搜索选项可能特别有用。回想一下,早些时候我们定义了一个名为 square 的函数。让我们从一个新的 IPython shell 中反向搜索我们的 Python 历史记录,并再次找到这个定义。当您在 IPython 终端中按 Ctrl-r 时,您将看到以下提示:

In [1]:
(reverse-i-search)`':

如果您在此提示符下开始键入字符,IPython 将自动填充最近的命令(如果有),与这些字符匹配的:

In [1]:
(reverse-i-search)`sqa': square??

在任何时候,您都可以添加更多字符以完善搜索,或者再次按 Ctrl-r 以进一步搜索与查询匹配的另一个命令。如果您之前跟随操作,再按两次 Ctrl-r 会得到以下结果:

In [1]:
(reverse-i-search)`sqa': def square(a):
    """Return the square of a"""
    return a ** 2

找到您要查找的命令后,按回车键搜索将结束。然后,您可以使用检索到的命令并继续会话:

In [1]: def square(a):
    """Return the square of a"""
    return a ** 2

In [2]: square(2)
Out[2]: 4

请注意,您可以使用 Ctrl-p/Ctrl-n 或上/下箭头键以类似的方式搜索历史记录,但只能通过匹配行开头的字符来搜索。也就是说,如果您键入 def 然后按 Ctrl-p,它将找到您历史记录中以字符 def 开头的最近的命令(如果有的话)。

杂项快捷键

最后,还有一些杂项快捷键不属于前面提到的任何类别,但仍然值得知道:

按键动作
Ctrl-l清除终端屏幕
Ctrl-c中断当前 Python 命令
Ctrl-d退出 IPython 会话

特别是 Ctrl-c 快捷键在你不小心启动了一个运行时间非常长的任务时非常有用。

虽然这里讨论的一些快捷键起初可能显得有点晦涩,但是通过实践,它们很快就会变得自动化。一旦你培养了那种肌肉记忆,我相信你甚至会希望它们在其他情境中也能用到。

第二章:增强交互功能

IPython 和 Jupyter 的大部分功能来自它们提供的额外交互工具。本章将涵盖其中一些工具,包括所谓的魔术命令,用于探索输入和输出历史记录的工具,以及与 Shell 交互的工具。

IPython 魔术命令

前一章展示了 IPython 如何让您高效、交互地使用和探索 Python。在这里,我们将开始讨论 IPython 在正常 Python 语法之上添加的一些增强功能。这些在 IPython 中被称为魔术命令,以%字符为前缀。这些魔术命令设计精炼地解决标准数据分析中的各种常见问题。魔术命令有两种类型:行魔术,以单个%前缀表示,作用于单行输入;单元格魔术,以双%%前缀表示,作用于多行输入。我将在这里展示和讨论一些简短的示例,并稍后回到更专注地讨论几个有用的魔术命令。

运行外部代码:%run

随着您开始开发更多的代码,您可能会发现自己在 IPython 中进行交互式探索,以及在文本编辑器中存储希望重用的代码。与其在新窗口中运行此代码,不如在您的 IPython 会话中运行更方便。可以通过%run魔术命令完成此操作。

例如,假设您创建了一个myscript.py文件,其中包含以下内容:

# file: myscript.py

def square(x):
    """square a number"""
    return x ** 2

for N in range(1, 4):
    print(f"{N} squared is {square(N)}")

你可以从你的 IPython 会话中执行以下操作:

In [1]: %run myscript.py
1 squared is 1
2 squared is 4
3 squared is 9

还要注意,在运行此脚本后,其中定义的任何函数都可以在您的 IPython 会话中使用:

In [2]: square(5)
Out[2]: 25

有几种选项可以微调代码的运行方式;您可以通过在 IPython 解释器中键入**%run?**来查看正常的文档。

代码执行时间:%timeit

另一个有用的魔术函数示例是%timeit,它将自动确定其后的单行 Python 语句的执行时间。例如,我们可能希望检查列表理解的性能:

In [3]: %timeit L = [n ** 2 for n in range(1000)]
430 µs ± 3.21 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit 的好处在于对于短命令,它会自动执行多次运行以获得更稳健的结果。对于多行语句,添加第二个%符号将其转换为可以处理多行输入的单元格魔术。例如,这里是使用for循环的等效构造:

In [4]: %%timeit
   ...: L = []
   ...: for n in range(1000):
   ...:     L.append(n ** 2)
   ...:
484 µs ± 5.67 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

我们可以立即看到,在这种情况下,列表理解比等效的for循环结构快大约 10%。我们将在“分析和计时代码”中探索%timeit和其他计时和分析代码的方法。

魔术函数帮助:?,%magic 和%lsmagic

与普通的 Python 函数类似,IPython 的魔术函数有文档字符串,可以以标准方式访问这些有用的文档。例如,要查阅 %timeit 魔术函数的文档,只需输入以下内容:

In [5]: %timeit?

类似地,还可以访问其他函数的文档。要访问可用魔术函数的一般描述,包括一些示例,请输入以下内容:

In [6]: %magic

要快速简单地列出所有可用的魔术函数,请输入以下内容:

In [7]: %lsmagic

最后,我将提到,如果你愿意的话,定义自己的魔术函数非常简单。我不会在这里讨论它,但如果你有兴趣,可以参考 “更多 IPython 资源” 中列出的参考资料。

输入和输出历史

正如之前所见,IPython shell 允许你使用上下箭头键(或等效的 Ctrl-p/Ctrl-n 快捷键)访问先前的命令。此外,在 shell 和笔记本中,IPython 还提供了几种获取先前命令输出以及命令字符串版本的方法。我们将在这里探讨这些方法。

IPython 的 In 和 Out 对象

现在我想象你已经开始熟悉 IPython 使用的 In [1]:/Out[1]: 样式的提示了。但事实证明,这些不仅仅是漂亮的装饰:它们提供了一种方法,让你可以访问当前会话中的先前输入和输出。假设我们开始一个看起来像这样的会话:

In [1]: import math

In [2]: math.sin(2)
Out[2]: 0.9092974268256817

In [3]: math.cos(2)
Out[3]: -0.4161468365471424

我们已经导入了内置的 math 包,然后计算了数字 2 的正弦和余弦。这些输入和输出显示在带有 In/Out 标签的 shell 中,但是 IPython 实际上创建了一些名为 InOut 的 Python 变量,这些变量会自动更新以反映这些历史记录:

In [4]: In
Out[4]: ['', 'import math', 'math.sin(2)', 'math.cos(2)', 'In']

In [5]: Out
Out[5]:
{2: 0.9092974268256817,
 3: -0.4161468365471424,
 4: ['', 'import math', 'math.sin(2)', 'math.cos(2)', 'In', 'Out']}

In 对象是一个列表,按顺序保存命令(列表的第一项是一个占位符,以便 In [1] 可以引用第一条命令):

In [6]: print(In[1])
import math

Out 对象不是一个列表,而是一个将输入编号映射到它们的输出(如果有的话)的字典:

In [7]: print(Out[2])
.9092974268256817

注意,并非所有操作都有输出:例如,import 语句和 print 语句不会影响输出。后者可能会令人惊讶,但如果考虑到 print 是一个返回 None 的函数,这是有道理的;为简洁起见,任何返回 None 的命令都不会添加到 Out 中。

当你想要与过去的结果交互时,这可能会很有用。例如,让我们使用先前计算的结果来检查 sin(2) ** 2cos(2) ** 2 的总和:

In [8]: Out[2] ** 2 + Out[3] ** 2
Out[8]: 1.0

结果是 1.0,这符合我们对这个著名的三角恒等式的预期。在这种情况下,使用这些先前的结果可能是不必要的,但如果你执行了一个非常昂贵的计算并且忘记将结果赋给一个变量,这可能会变得非常方便。

下划线快捷键和先前的输出

标准 Python shell 仅包含一种简单的快捷方式用于访问先前的输出:变量 _(即一个下划线)会随先前的输出更新。这在 IPython 中也适用:

In [9]: print(_)
.0

但是 IPython 进一步扩展了这一点 — 您可以使用双下划线访问倒数第二个输出,并使用三个下划线访问倒数第三个输出(跳过任何没有输出的命令):

In [10]: print(__)
-0.4161468365471424

In [11]: print(___)
.9092974268256817

IPython 到此为止:超过三个下划线开始变得有点难以计数,此时通过行号更容易引用输出。

我还应该提一下另一个快捷方式 — Out[*X*] 的简写是 _*X*(即一个下划线后跟行号):

In [12]: Out[2]
Out[12]: 0.9092974268256817

In [13]: _2
Out[13]: 0.9092974268256817

禁止输出

有时候,您可能希望抑制语句的输出(这在我们将在第四部分探索的绘图命令中可能最常见)。或者,您执行的命令会产生一个您不希望存储在输出历史记录中的结果,也许是因为在其他引用被移除时它可以被释放。抑制命令的输出最简单的方法是在行尾添加分号:

In [14]: math.sin(2) + math.cos(2);

结果将在不显示在屏幕上或存储在 Out 字典中的情况下计算:

In [15]: 14 in Out
Out[15]: False

相关的魔术命令

要一次访问一批先前的输入,%history 魔术命令非常有帮助。以下是如何打印前四个输入:

In [16]: %history -n 1-3
   1: import math
   2: math.sin(2)
   3: math.cos(2)

如往常一样,您可以输入 %history? 以获取更多信息并查看可用选项的描述(有关 ? 功能的详细信息,请参见第一章)。其他有用的魔术命令包括 %rerun,它将重新执行命令历史记录的某些部分,以及 %save,它将命令历史记录的某个集合保存到文件中。

IPython 和 Shell 命令

在与标准 Python 解释器的交互工作时,一个令人沮丧的地方是需要在多个窗口之间切换以访问 Python 工具和系统命令行工具。IPython 弥合了这一差距,并为您提供了在 IPython 终端内直接执行 shell 命令的语法。这是通过感叹号实现的:在 ! 后出现的任何内容将不会由 Python 内核执行,而是由系统命令行执行。

以下讨论假定您正在使用类 Unix 系统,如 Linux 或 macOS。接下来的一些示例在 Windows 上会失败,因为 Windows 默认使用不同类型的 shell,但如果您使用Windows 子系统 for Linux,这里的示例应该能正常运行。如果您对 shell 命令不熟悉,我建议您查看由始终优秀的软件教程基金会组织的Unix shell 教程

Shell 快速入门

对于如何使用 shell/终端/命令行的全面介绍远远超出了本章的范围,但对于未接触过的人,我将在这里进行一个快速介绍。shell 是一种与计算机进行文本交互的方式。自从 20 世纪 80 年代中期以来,当微软和苹果推出了他们现在无处不在的图形操作系统的第一个版本时,大多数计算机用户通过熟悉的菜单选择和拖放操作与他们的操作系统交互。但是操作系统在这些图形用户界面之前就存在,主要通过文本输入的序列来控制:在提示符下,用户会输入一个命令,计算机会执行用户告诉它做的事情。那些早期的提示系统是今天大多数数据科学家仍在使用的 shell 和终端的前身。

对于不熟悉 shell 的人可能会问,为什么要费这个劲,当很多相同的结果可以通过简单点击图标和菜单来实现呢?一个 shell 用户可能会反问:为什么要去寻找图标和菜单项,而不是通过输入命令来更轻松地完成任务呢?虽然这听起来像是典型的技术偏好僵局,但当超越基本任务时,很快就会明显地感觉到 shell 在控制高级任务方面提供了更多的控制权——尽管学习曲线确实可能令人望而生畏。

例如,这里是一个 Linux/macOS shell 会话的示例,用户在其系统上探索、创建和修改目录和文件(osx:~ $是提示符,$之后的所有内容是键入的命令;以#开头的文本仅用作描述,而不是您实际要键入的内容):

osx:~ $ echo "hello world"            # echo is like Python's print function
hello world

osx:~ $ pwd                            # pwd = print working directory
/home/jake                             # This is the "path" that we're sitting in

osx:~ $ ls                             # ls = list working directory contents
notebooks  projects

osx:~ $ cd projects/                   # cd = change directory

osx:projects $ pwd
/home/jake/projects

osx:projects $ ls
datasci_book   mpld3   myproject.txt

osx:projects $ mkdir myproject          # mkdir = make new directory

osx:projects $ cd myproject/

osx:myproject $ mv ../myproject.txt ./  # mv = move file. Here we're moving the
                                        # file myproject.txt from one directory
                                        # up (../) to the current directory (./).
osx:myproject $ ls
myproject.txt

请注意,所有这些只是通过键入命令而不是点击图标和菜单来执行熟悉操作(导航目录结构、创建目录、移动文件等)的一种紧凑方式。仅仅几个命令(pwdlscdmkdircp)就可以完成许多最常见的文件操作,但当您超越这些基础操作时,shell 方法真正显示其强大之处。

IPython 中的 Shell 命令

任何标准的 shell 命令都可以通过在其前面加上!字符直接在 IPython 中使用。例如,lspwdecho命令可以如下运行:

In [1]: !ls
myproject.txt

In [2]: !pwd
/home/jake/projects/myproject

In [3]: !echo "printing from the shell"
printing from the shell

向 Shell 传递值和从 Shell 获取值

Shell 命令不仅可以从 IPython 中调用,还可以与 IPython 命名空间交互。例如,您可以使用赋值操作符=将任何 shell 命令的输出保存到 Python 列表中:

In [4]: contents = !ls

In [5]: print(contents)
['myproject.txt']

In [6]: directory = !pwd

In [7]: print(directory)
['/Users/jakevdp/notebooks/tmp/myproject']

这些结果不会以列表的形式返回,而是以 IPython 中定义的特殊的 shell 返回类型返回:

In [8]: type(directory)
IPython.utils.text.SList

这看起来和行为很像 Python 列表,但具有额外的功能,比如grepfields方法以及允许您以方便的方式搜索、过滤和显示结果的snp属性。有关这些信息,您可以使用 IPython 内置的帮助功能。

可以使用 {*varname*} 语法将 Python 变量传递到 shell 中,实现双向通信:

In [9]: message = "hello from Python"

In [10]: !echo {message}
hello from Python

大括号中包含变量名,这个变量名在 shell 命令中会被替换为变量的内容。

与 Shell 相关的魔术命令

如果你在 IPython 的 shell 命令中尝试了一段时间,你可能会注意到无法使用 !cd 来导航文件系统:

In [11]: !pwd
/home/jake/projects/myproject

In [12]: !cd ..

In [13]: !pwd
/home/jake/projects/myproject

这是因为笔记本中的 shell 命令是在一个临时子 shell 中执行的,这个 shell 并不保留命令之间的状态。如果你想更持久地改变工作目录,可以使用 %cd 魔术命令:

In [14]: %cd ..
/home/jake/projects

实际上,默认情况下你甚至可以不带 % 符号使用这些功能:

In [15]: cd myproject
/home/jake/projects/myproject

这被称为自动魔术函数,可以通过 %automagic 魔术函数切换执行这些命令时是否需要显式 % 符号。

除了 %cd,还有其他可用的类似 shell 的魔术函数,如 %cat%cp%env%ls%man%mkdir%more%mv%pwd%rm%rmdir,如果 automagic 打开,这些命令都可以不带 % 符号使用。这使得你几乎可以像处理普通 shell 一样处理 IPython 提示符:

In [16]: mkdir tmp

In [17]: ls
myproject.txt  tmp/

In [18]: cp myproject.txt tmp/

In [19]: ls tmp
myproject.txt

In [20]: rm -r tmp

在同一个终端窗口中访问 shell,与你的 Python 会话结合得更自然,减少了上下文切换。

第三章:调试和性能分析

除了前一章中讨论的增强交互工具外,Jupyter 还提供了许多探索和理解正在运行的代码的方式,例如通过跟踪逻辑错误或意外的慢执行。本章将讨论其中一些工具。

错误和调试

代码开发和数据分析始终需要一些试验和错误,而 IPython 包含了简化此过程的工具。本节将简要介绍控制 Python 异常报告的一些选项,然后探讨代码中调试错误的工具。

控制异常:%xmode

大多数情况下,当 Python 脚本失败时,它会引发异常。当解释器遇到这些异常时,可以从 Python 内部访问导致错误的原因,该信息可以在 traceback 中找到。通过 %xmode 魔术函数,IPython 允许您控制引发异常时打印的信息量。考虑以下代码:

In [1]: def func1(a, b):
            return a / b

        def func2(x):
            a = x
            b = x - 1
            return func1(a, b)

In [2]: func2(1)
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-2-b2e110f6fc8f> in <module>()
----> 1 func2(1)

<ipython-input-1-d849e34d61fb> in func2(x)
      5     a = x
      6     b = x - 1
----> 7     return func1(a, b)

<ipython-input-1-d849e34d61fb> in func1(a, b)
      1 def func1(a, b):
----> 2     return a / b
      3
      4 def func2(x):
      5     a = x

ZeroDivisionError: division by zero

调用 func2 导致错误,并阅读打印的跟踪让我们能够准确地看到发生了什么。在默认模式下,此跟踪包括几行显示导致错误的每个步骤的上下文。使用 %xmode 魔术函数(缩写为 exception mode),我们可以更改打印的信息内容。

%xmode 接受一个参数,即模式,有三种可能性:PlainContextVerbose。默认是 Context,给出类似刚才显示的输出。Plain 更紧凑,提供较少信息:

In [3]: %xmode Plain
Out[3]: Exception reporting mode: Plain

In [4]: func2(1)
Traceback (most recent call last):

  File "<ipython-input-4-b2e110f6fc8f>", line 1, in <module>
    func2(1)

  File "<ipython-input-1-d849e34d61fb>", line 7, in func2
    return func1(a, b)

  File "<ipython-input-1-d849e34d61fb>", line 2, in func1
    return a / b

ZeroDivisionError: division by zero

Verbose 模式添加了一些额外信息,包括调用的任何函数的参数:

In [5]: %xmode Verbose
Out[5]: Exception reporting mode: Verbose

In [6]: func2(1)
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-6-b2e110f6fc8f> in <module>()
----> 1 func2(1)
        global func2 = <function func2 at 0x103729320>

<ipython-input-1-d849e34d61fb> in func2(x=1)
      5     a = x
      6     b = x - 1
----> 7     return func1(a, b)
        global func1 = <function func1 at 0x1037294d0>
        a = 1
        b = 0

<ipython-input-1-d849e34d61fb> in func1(a=1, b=0)
      1 def func1(a, b):
----> 2     return a / b
        a = 1
        b = 0
      3
      4 def func2(x):
      5     a = x

ZeroDivisionError: division by zero

这些额外信息可以帮助您更准确地找出异常发生的原因。那么为什么不始终使用 Verbose 模式?随着代码变得复杂,这种回溯可能会变得非常长。根据情况,有时 PlainContext 模式的简洁性更容易处理。

调试:当仅仅阅读跟踪不足以解决问题时

用于交互式调试的标准 Python 工具是 pdb,即 Python 调试器。此调试器允许用户逐行步进代码,以查看可能导致更复杂错误的原因。其 IPython 增强版本是 ipdb,即 IPython 调试器。

启动和使用这两个调试器有许多方法;我们在此处不会全面涵盖它们。请参考这两个实用工具的在线文档以了解更多信息。

在 IPython 中,也许最方便的调试接口是 %debug 魔术命令。如果在异常发生后调用它,它将自动在异常点打开一个交互式调试提示符。ipdb 提示符允许您查看堆栈的当前状态,探索可用变量,甚至运行 Python 命令!

让我们查看最近的异常,然后执行一些基本任务。我们将打印 ab 的值,然后输入 quit 退出调试会话:

In [7]: %debug <ipython-input-1-d849e34d61fb>(2)func1()
      1 def func1(a, b):
----> 2     return a / b
      3

ipdb> print(a)
1
ipdb> print(b)
0
ipdb> quit

交互式调试器提供的远不止这些——我们甚至可以在堆栈中上下跳转,并探索那里的变量值:

In [8]: %debug <ipython-input-1-d849e34d61fb>(2)func1()
      1 def func1(a, b):
----> 2     return a / b
      3

ipdb> up <ipython-input-1-d849e34d61fb>(7)func2()
      5     a = x
      6     b = x - 1
----> 7     return func1(a, b)

ipdb> print(x)
1
ipdb> up <ipython-input-6-b2e110f6fc8f>(1)<module>()
----> 1 func2(1)

ipdb> down <ipython-input-1-d849e34d61fb>(7)func2()
      5     a = x
      6     b = x - 1
----> 7     return func1(a, b)

ipdb> quit

这使我们不仅可以快速找出错误的原因,还可以查看导致错误的函数调用。

如果您希望调试器在引发异常时自动启动,可以使用 %pdb 魔术函数来打开这种自动行为:

In [9]: %xmode Plain
        %pdb on
        func2(1)
Exception reporting mode: Plain
Automatic pdb calling has been turned ON
ZeroDivisionError: division by zero <ipython-input-1-d849e34d61fb>(2)func1()
      1 def func1(a, b):
----> 2     return a / b
      3

ipdb> print(b)
0
ipdb> quit

最后,如果您有一个希望以交互模式从头开始运行的脚本,可以使用命令 %run -d 运行它,并使用 next 命令逐行交互式地执行代码。

除了我在这里展示的常见和有用的命令外,还有许多可用于交互式调试的命令。表 3-1 包含了一些更多的描述。

表 3-1. 调试命令的部分列表

命令描述
l(ist)显示文件中的当前位置
h(elp)显示命令列表,或查找特定命令的帮助信息
q(uit)退出调试器和程序
c(ontinue)退出调试器,继续程序执行
n(ext)进入程序的下一步
<enter>重复上一个命令
p(rint)打印变量
s(tep)进入子例程
r(eturn)从子例程中返回

欲了解更多信息,请在调试器中使用 help 命令,或查看 ipdb在线文档

代码分析和计时

在开发代码和创建数据处理流水线的过程中,通常可以在各种实现之间做出权衡。在开发算法的早期阶段,过于担心这些事情可能会适得其反。正如唐纳德·克努特所说,“我们应该忘记小的效率,大约 97%的时间:过早优化是一切邪恶的根源。”

但是一旦您的代码运行正常,深入了解其效率可能会有所帮助。有时检查特定命令或一系列命令的执行时间很有用;其他时候,检查多行过程并确定复杂操作系列中的瓶颈位置也很有用。IPython 提供了广泛的功能来进行此类代码的时间和性能分析。这里我们将讨论以下 IPython 魔术命令:

%time

时间单个语句的执行时间

%timeit

多次执行单个语句以获得更准确的时间

%prun

使用分析器运行代码

%lprun

使用逐行分析器运行代码

%memit

测量单个语句的内存使用情况

%mprun

使用逐行内存分析器运行代码

最后四个命令未捆绑在 IPython 中;要使用它们,您需要获取line_profilermemory_profiler扩展,我们将在以下部分讨论它们。

计时代码片段:%timeit%time

我们在“IPython 魔法命令”的介绍中看到了%timeit行魔法和%%timeit单元魔法;这些可以用来计时代码片段的重复执行。

In [1]: %timeit sum(range(100))
1.53 µs ± 47.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

请注意,由于此操作非常快,%timeit会自动执行大量重复。对于较慢的命令,%timeit会自动调整并执行较少的重复:

In [2]: %%timeit
        total = 0
        for i in range(1000):
            for j in range(1000):
                total += i * (-1) ** j
536 ms ± 15.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

有时重复操作并不是最佳选择。例如,如果我们有一个需要排序的列表,我们可能会被重复的操作误导;排序一个已经排序好的列表比排序一个未排序的列表快得多,因此重复操作会扭曲结果:

In [3]: import random
        L = [random.random() for i in range(100000)]
        %timeit L.sort()
Out[3]: 1.71 ms ± 334 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

对于此操作,%time魔法函数可能是一个更好的选择。当命令运行时间较长时,短暂的系统相关延迟不太可能影响结果。让我们计时一个未排序和一个已排序列表的排序过程:

In [4]: import random
        L = [random.random() for i in range(100000)]
        print("sorting an unsorted list:")
        %time L.sort()
Out[4]: sorting an unsorted list:
        CPU times: user 31.3 ms, sys: 686 µs, total: 32 ms
        Wall time: 33.3 ms
In [5]: print("sorting an already sorted list:")
        %time L.sort()
Out[5]: sorting an already sorted list:
        CPU times: user 5.19 ms, sys: 268 µs, total: 5.46 ms
        Wall time: 14.1 ms

注意排序好的列表比无序列表排序快得多,但请注意,即使对于排序好的列表,使用%time%timeit花费的时间也更长!这是因为%timeit在幕后做了一些聪明的事情,防止系统调用干扰计时。例如,它阻止了未使用的 Python 对象的清理(称为垃圾收集),否则可能会影响计时。因此,%timeit的结果通常明显比%time的结果快。

对于%time,与%timeit一样,使用%%单元魔法语法允许对多行脚本进行计时:

In [6]: %%time
        total = 0
        for i in range(1000):
            for j in range(1000):
                total += i * (-1) ** j
CPU times: user 655 ms, sys: 5.68 ms, total: 661 ms
Wall time: 710 ms

要获取有关%time%timeit以及它们可用选项的更多信息,请使用 IPython 帮助功能(例如,在 IPython 提示符处键入%time?)。

全脚本分析: %prun

一个程序由许多单个语句组成,有时在上下文中计时这些语句比单独计时它们更重要。Python 包含一个内置的代码分析器(可以在 Python 文档中了解),但 IPython 提供了一个更方便的方式来使用此分析器,即魔法函数%prun

举例来说,我们定义一个简单的函数来进行一些计算:

In [7]: def sum_of_lists(N):
            total = 0
            for i in range(5):
                L = [j ^ (j >> i) for j in range(N)]
                total += sum(L)
            return total

现在我们可以调用一个函数调用来查看%prun的分析结果:

In [8]: %prun sum_of_lists(1000000)
14 function calls in 0.932 seconds
Ordered by: internal time
ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     5    0.808    0.162    0.808  0.162 <ipython-input-7-f105717832a2>:4(<listcomp>)
     5    0.066    0.013    0.066  0.013 {built-in method builtins.sum}
     1    0.044    0.044    0.918  0.918 <ipython-input-7-f105717832a2>:1
     > (sum_of_lists)
     1    0.014    0.014    0.932  0.932 <string>:1(<module>)
     1    0.000    0.000    0.932  0.932 {built-in method builtins.exec}
     1    0.000    0.000    0.000  0.000 {method 'disable' of '_lsprof.Profiler'
     > objects}

结果是一个表,按每次函数调用的总时间顺序显示执行在哪里花费了最多的时间。在这种情况下,大部分执行时间都花费在sum_of_lists内的列表推导式中。从这里开始,我们可以考虑如何改进算法的性能。

要获取有关%prun以及其可用选项的更多信息,请使用 IPython 帮助功能(即,在 IPython 提示符处键入%prun?)。

逐行分析:%lprun

%prun的函数级性能分析很有用,但有时逐行分析报告更方便。这不是 Python 或 IPython 内置的功能,但可以通过安装line_profiler包来实现这一点。首先使用 Python 的包装工具pip安装line_profiler包:

$ pip install line_profiler

接下来,您可以使用 IPython 来加载line_profiler IPython 扩展,这是该软件包的一部分:

In [9]: %load_ext line_profiler

现在,%lprun命令将对任何函数进行逐行分析。在这种情况下,我们需要明确告诉它我们感兴趣的是哪些函数:

In [10]: %lprun -f sum_of_lists sum_of_lists(5000)
Timer unit: 1e-06 s

Total time: 0.014803 s
File: <ipython-input-7-f105717832a2>
Function: sum_of_lists at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
      1                                           def sum_of_lists(N):
      2         1          6.0      6.0      0.0      total = 0
      3         6         13.0      2.2      0.1      for i in range(5):
      4         5      14242.0   2848.4     96.2          L = [j ^ (j >> i) for j
      5         5        541.0    108.2      3.7          total += sum(L)
      6         1          1.0      1.0      0.0      return total

顶部的信息为我们提供了阅读结果的关键:时间以微秒为单位报告,我们可以看到程序在哪里花费了最多的时间。此时,我们可以使用这些信息来修改脚本的某些方面,使其在我们所需的用例中表现更好。

要获取关于%lprun及其可用选项的更多信息,请使用 IPython 的帮助功能(例如,在 IPython 提示符处键入%lprun?)。

内存使用分析:%memit%mprun

分析的另一个方面是操作使用的内存量。可以使用另一个 IPython 扩展memory_profiler来评估这一点。与line_profiler一样,我们首先使用pip安装该扩展:

$ pip install memory_profiler

然后我们可以使用 IPython 来加载它:

In [11]: %load_ext memory_profiler

内存分析扩展包含两个有用的魔术函数:%memit(提供了%timeit的内存测量等价物)和%mprun(提供了%lprun的内存测量等价物)。%memit魔术函数可以非常简单地使用:

In [12]: %memit sum_of_lists(1000000)
peak memory: 141.70 MiB, increment: 75.65 MiB

我们看到这个函数使用了约 140 MB 的内存。

对于逐行描述内存使用情况,我们可以使用%mprun魔术函数。不幸的是,这仅适用于在单独模块中定义的函数,而不适用于笔记本本身,因此我们将首先使用%%file单元格魔术创建一个简单的模块mprun_demo.py,其中包含我们的sum_of_lists函数,并添加一个额外的功能,以使我们的内存分析结果更清晰:

In [13]: %%file mprun_demo.py
         def sum_of_lists(N):
             total = 0
             for i in range(5):
                 L = [j ^ (j >> i) for j in range(N)]
                 total += sum(L)
                 del L # remove reference to L
             return total
Overwriting mprun_demo.py

现在我们可以导入这个函数的新版本并运行内存行分析:

In [14]: from mprun_demo import sum_of_lists
         %mprun -f sum_of_lists sum_of_lists(1000000)

Filename: /Users/jakevdp/github/jakevdp/PythonDataScienceHandbook/notebooks_v2/
> m prun_demo.py

Line #    Mem usage    Increment  Occurrences   Line Contents
============================================================
     1     66.7 MiB     66.7 MiB           1   def sum_of_lists(N):
     2     66.7 MiB      0.0 MiB           1       total = 0
     3     75.1 MiB      8.4 MiB           6       for i in range(5):
     4    105.9 MiB     30.8 MiB     5000015           L = [j ^ (j >> i) for j
     5    109.8 MiB      3.8 MiB           5           total += sum(L)
     6     75.1 MiB    -34.6 MiB           5           del L # remove reference to L
     7     66.9 MiB     -8.2 MiB           1       return total

在这里,Increment列告诉我们每行如何影响总内存预算:注意,当我们创建和删除列表L时,我们增加了约 30 MB 的内存使用量。这是在 Python 解释器本身的后台内存使用量之上的额外内存使用。

要获取关于%memit%mprun及其可用选项的更多信息,请使用 IPython 的帮助功能(例如,在 IPython 提示符处键入%memit?)。

更多 IPython 资源

在这一系列章节中,我们只是初步介绍了使用 IPython 来支持数据科学任务的表面知识。更多信息可以在印刷品和网络上找到,这里我将列出一些其他可能对你有帮助的资源。

网络资源

IPython 网站

IPython 网站提供了文档、示例、教程以及各种其他资源的链接。

nbviewer 网站

此网站显示任何可在互联网上找到的 Jupyter 笔记本的静态渲染。首页展示了一些示例笔记本,您可以浏览看看其他人是如何使用 IPython 的!

Jupyter 笔记本的精选集

由 nbviewer 提供支持的这个不断增长的笔记本列表展示了您可以用 IPython 进行的数值分析的深度和广度。它涵盖了从简短的示例和教程到完整的课程和笔记本格式的书籍!

视频教程

在互联网上搜索,您会发现许多关于 IPython 的视频教程。我特别推荐从 PyCon、SciPy 和 PyData 大会上获取教程,由 IPython 和 Jupyter 的两位主要创作者和维护者 Fernando Perez 和 Brian Granger 提供。

书籍

Python 数据分析(O’Reilly)

Wes McKinney 的书包括一章介绍如何将 IPython 用作数据科学家的工具。尽管其中很多内容与我们在这里讨论的内容重叠,但另一个视角总是有帮助的。

学习 IPython 进行交互计算与数据可视化(Packt)

Cyrille Rossant 编写的这本简短书籍,为使用 IPython 进行数据分析提供了良好的入门。

IPython 交互计算与可视化食谱(Packt)

同样由 Cyrille Rossant 编写,这本书是更长、更高级的 IPython 数据科学使用指南。尽管书名中提到的是 IPython,但它也深入探讨了广泛的数据科学主题。

最后,提醒您可以自己寻找帮助:IPython 基于 ? 的帮助功能(详见第一章)在您善加利用且经常使用时非常有用。当您浏览这里和其他地方的示例时,可以用它来熟悉 IPython 提供的所有工具。

第二部分:NumPy 入门

本书的这一部分,连同第三部分,概述了在 Python 中有效加载、存储和操作内存中数据的技术。这个主题非常广泛:数据集可以来自各种来源,以及各种格式,包括文档集合、图像集合、声音片段集合、数字测量集合,或者几乎任何其他类型的数据。尽管看似多样化,但许多数据集本质上都可以表示为数字数组。

例如,图像——特别是数字图像——可以简单地被视为表示整个区域内像素亮度的二维数组。声音片段可以被视为与时间的强度一维数组。文本可以以各种方式转换为数字表示,比如表示特定单词或词组频率的二进制数字。无论数据是什么,使其可分析的第一步将是将其转换为数字数组。(我们将在第四十章中讨论此过程的一些具体示例。)

因此,对数字数组的有效存储和操作对于进行数据科学的过程是绝对基本的。我们现在将看看 Python 专门用于处理这种数字数组的专用工具:NumPy 软件包和 Pandas 软件包(在第三部分中讨论)。

本书的这一部分将详细介绍 NumPy。NumPy(缩写为 Numerical Python)提供了一个高效的接口来存储和操作密集数据缓冲区。在某些方面,NumPy 数组类似于 Python 的内置 list 类型,但是随着数组大小的增长,NumPy 数组提供了更高效的存储和数据操作。NumPy 数组几乎构成了 Python 数据科学工具生态系统的全部核心,因此花时间学习如何有效地使用 NumPy 对无论你感兴趣的数据科学的哪个方面都是有价值的。

如果你遵循了前言中的建议并安装了 Anaconda 栈,那么你已经安装并准备好使用 NumPy 了。如果你更喜欢自己动手,你可以访问NumPy.org并按照那里的安装说明进行操作。一旦你安装完成,你就可以导入 NumPy 并检查版本:

In [1]: import numpy
        numpy.__version__
Out[1]: '1.21.2'

对于本章讨论的软件包的组件,我建议使用 NumPy 版本 1.8 或更高版本。按照惯例,在 SciPy/PyData 世界中,大多数人都会使用 np 作为 NumPy 的别名导入:

In [2]: import numpy as np

在本章以及书中的其余部分中,你会发现这是我们导入和使用 NumPy 的方式。

第四章:理解 Python 中的数据类型

有效的数据驱动科学和计算需要理解数据如何存储和操作。本章概述了在 Python 语言本身中如何处理数据数组,并介绍了 NumPy 在此基础上的改进。理解这种差异对于理解本书其余部分的材料至关重要。

Python 的用户经常被其易用性所吸引,其中一个因素是动态类型。而静态类型语言如 C 或 Java 要求每个变量都必须显式声明,而像 Python 这样的动态类型语言则跳过了这个规范。例如,在 C 语言中,您可能会这样指定特定的操作:

/* C code */
int result = 0;
for(int i=0; i<100; i++){
    result += i;
}

而在 Python 中,等价的操作可以这样写:

# Python code
result = 0
for i in range(100):
    result += i

注意一个主要区别:在 C 语言中,每个变量的数据类型都是显式声明的,而在 Python 中类型是动态推断的。这意味着,例如,我们可以将任何类型的数据分配给任何变量:

# Python code
x = 4
x = "four"

在这里,我们已经将x的内容从整数变为字符串。在 C 语言中,相同操作会导致(依赖于编译器设置)编译错误或其他意外后果:

/* C code */
int x = 4;
x = "four";  // FAILS

这种灵活性是 Python 和其他动态类型语言方便和易于使用的一个要素。理解这是如何工作是学习如何有效和有效地分析数据的重要一环。但是,这种类型灵活性也指出了 Python 变量不仅仅是它们的值;它们还包含关于值的类型的额外信息。我们将在接下来的章节中更多地探讨这一点。

Python 整数不仅仅是一个整数

标准的 Python 实现是用 C 编写的。这意味着每个 Python 对象实际上是一个巧妙伪装的 C 结构,不仅包含其值,还包含其他信息。例如,当我们在 Python 中定义一个整数,如x = 10000x不只是一个“原始”整数。它实际上是一个指向复合 C 结构的指针,该结构包含几个值。浏览 Python 3.10 源代码时,我们发现整数(long)类型定义实际上看起来是这样的(一旦 C 宏被展开):

struct _longobject {
    long ob_refcnt;
    PyTypeObject *ob_type;
    size_t ob_size;
    long ob_digit[1];
};

在 Python 3.10 中,一个单独的整数实际上包含四个部分:

  • ob_refcnt,一个引用计数,帮助 Python 静默处理内存分配和释放

  • ob_type,编码变量的类型

  • ob_size指定了接下来的数据成员的大小。

  • ob_digit包含了我们期望 Python 变量表示的实际整数值。

这意味着在 Python 中存储整数与在编译语言如 C 中的存储相比,会有一些额外开销,正如图 4-1 所示。

cint vs pyint

图 4-1. C 和 Python 整数的差异

这里,PyObject_HEAD 是包含引用计数、类型代码和之前提到的其他部分的结构的一部分。

注意这里的区别:C 整数本质上是一个标签,指向内存中的一个位置,其字节编码包含一个整数值。Python 整数是指向内存中包含所有 Python 对象信息的位置的指针,包括包含整数值的字节。Python 整数结构中的这些额外信息是允许 Python 如此自由和动态编码的原因。然而,Python 类型中的所有这些额外信息都是有代价的,特别是在结合许多这些对象的结构中尤为明显。

Python 列表不仅仅是一个列表

现在让我们考虑当我们使用一个包含许多 Python 对象的 Python 数据结构时会发生什么。Python 中标准的可变多元素容器是列表。我们可以按照以下方式创建一个整数列表:

In [1]: L = list(range(10))
        L
Out[1]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [2]: type(L[0])
Out[2]: int

或者,类似地,一个字符串列表:

In [3]: L2 = [str(c) for c in L]
        L2
Out[3]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
In [4]: type(L2[0])
Out[4]: str

由于 Python 的动态类型,我们甚至可以创建异构列表:

In [5]: L3 = [True, "2", 3.0, 4]
        [type(item) for item in L3]
Out[5]: [bool, str, float, int]

但这种灵活性是有代价的:为了允许这些灵活的类型,列表中的每个项目都必须包含自己的类型、引用计数和其他信息。也就是说,每个项目都是一个完整的 Python 对象。在所有变量都是相同类型的特殊情况下,大部分信息是冗余的,因此将数据存储在固定类型的数组中可能更有效。动态类型列表和固定类型(NumPy 风格)数组之间的区别在 图 4-2 中有所说明。

array vs list

图 4-2. C 和 Python 列表之间的区别

在实现级别上,数组基本上包含一个指向一个连续数据块的单个指针。另一方面,Python 列表包含一个指向指针块的指针,每个指针又指向一个完整的 Python 对象,就像我们之前看到的 Python 整数一样。再次强调列表的优势在于灵活性:因为每个列表元素都是一个包含数据和类型信息的完整结构,所以列表可以填充任何所需类型的数据。固定类型的 NumPy 风格数组缺乏这种灵活性,但对于存储和操作数据来说更加高效。

Python 中的固定类型数组

Python 提供了几种不同的选项来在高效的固定类型数据缓冲区中存储数据。内置的 array 模块(自 Python 3.3 起可用)可用于创建统一类型的密集数组:

In [6]: import array
        L = list(range(10))
        A = array.array('i', L)
        A
Out[6]: array('i', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

这里,'i' 是指示内容为整数的类型代码。

然而,更有用的是 NumPy 包的 ndarray 对象。虽然 Python 的 array 对象提供了对基于数组的数据的有效存储,但 NumPy 在此基础上添加了对该数据的有效操作。我们将在后面的章节中探讨这些操作;接下来,我将向您展示创建 NumPy 数组的几种不同方法。

从 Python 列表创建数组

我们将从标准的 NumPy 导入开始,使用别名 np

In [7]: import numpy as np

现在我们可以使用 np.array 来从 Python 列表创建数组:

In [8]: # Integer array
        np.array([1, 4, 2, 5, 3])
Out[8]: array([1, 4, 2, 5, 3])

请记住,与 Python 列表不同,NumPy 数组只能包含相同类型的数据。如果类型不匹配,NumPy 将根据其类型提升规则进行类型提升;在这里,整数被提升为浮点数:

In [9]: np.array([3.14, 4, 2, 3])
Out[9]: array([3.14, 4.  , 2.  , 3.  ])

如果我们想要显式地设置结果数组的数据类型,可以使用 dtype 关键字:

In [10]: np.array([1, 2, 3, 4], dtype=np.float32)
Out[10]: array([1., 2., 3., 4.], dtype=float32)

最后,与 Python 列表不同,NumPy 数组可以是多维的。以下是使用列表的列表初始化多维数组的一种方法:

In [11]: # Nested lists result in multidimensional arrays
         np.array([range(i, i + 3) for i in [2, 4, 6]])
Out[11]: array([[2, 3, 4],
                [4, 5, 6],
                [6, 7, 8]])

内部列表被视为生成的二维数组的行。

从头创建数组

特别是对于较大的数组,使用 NumPy 内置的函数从头创建数组更有效率。以下是几个示例:

In [12]: # Create a length-10 integer array filled with 0s
         np.zeros(10, dtype=int)
Out[12]: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
In [13]: # Create a 3x5 floating-point array filled with 1s
         np.ones((3, 5), dtype=float)
Out[13]: array([[1., 1., 1., 1., 1.],
                [1., 1., 1., 1., 1.],
                [1., 1., 1., 1., 1.]])
In [14]: # Create a 3x5 array filled with 3.14
         np.full((3, 5), 3.14)
Out[14]: array([[3.14, 3.14, 3.14, 3.14, 3.14],
                [3.14, 3.14, 3.14, 3.14, 3.14],
                [3.14, 3.14, 3.14, 3.14, 3.14]])
In [15]: # Create an array filled with a linear sequence
         # starting at 0, ending at 20, stepping by 2
         # (this is similar to the built-in range function)
         np.arange(0, 20, 2)
Out[15]: array([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18])
In [16]: # Create an array of five values evenly spaced between 0 and 1
         np.linspace(0, 1, 5)
Out[16]: array([0.  , 0.25, 0.5 , 0.75, 1.  ])
In [17]: # Create a 3x3 array of uniformly distributed
         # pseudorandom values between 0 and 1
         np.random.random((3, 3))
Out[17]: array([[0.09610171, 0.88193001, 0.70548015],
                [0.35885395, 0.91670468, 0.8721031 ],
                [0.73237865, 0.09708562, 0.52506779]])
In [18]: # Create a 3x3 array of normally distributed pseudorandom
         # values with mean 0 and standard deviation 1
         np.random.normal(0, 1, (3, 3))
Out[18]: array([[-0.46652655, -0.59158776, -1.05392451],
                [-1.72634268,  0.03194069, -0.51048869],
                [ 1.41240208,  1.77734462, -0.43820037]])
In [19]: # Create a 3x3 array of pseudorandom integers in the interval [0, 10)
         np.random.randint(0, 10, (3, 3))
Out[19]: array([[4, 3, 8],
                [6, 5, 0],
                [1, 1, 4]])
In [20]: # Create a 3x3 identity matrix
         np.eye(3)
Out[20]: array([[1., 0., 0.],
                [0., 1., 0.],
                [0., 0., 1.]])
In [21]: # Create an uninitialized array of three integers; the values will be
         # whatever happens to already exist at that memory location
         np.empty(3)
Out[21]: array([1., 1., 1.])

NumPy 标准数据类型

NumPy 数组包含单一类型的值,因此对这些类型及其限制有详细了解很重要。因为 NumPy 是用 C 语言构建的,所以这些类型对于使用 C、Fortran 和其他相关语言的用户来说应该很熟悉。

标准的 NumPy 数据类型列在 表 4-1 中。注意,在构建数组时,它们可以使用字符串来指定:

np.zeros(10, dtype='int16')

或者使用相关的 NumPy 对象:

np.zeros(10, dtype=np.int16)

可以进行更高级的类型规定,例如指定大端或小端数字;有关更多信息,请参阅 NumPy 文档。NumPy 还支持复合数据类型,这将在 第 12 章 中介绍。

表 4-1. 标准的 NumPy 数据类型

数据类型描述
bool_存储为字节的布尔值(True 或 False)
int_默认整数类型(与 C 语言的 long 相同;通常是 int64int32
intc与 C 语言 int 相同(通常是 int32int64
intp用于索引的整数(与 C 语言的 ssize_t 相同;通常是 int32int64
int8字节(–128 到 127)
int16整数(–32768 到 32767)
int32整数(–2147483648 到 2147483647)
int64整数(–9223372036854775808 到 9223372036854775807)
uint8无符号整数(0 到 255)
uint16无符号整数(0 到 65535)
uint32无符号整数(0 到 4294967295)
uint64无符号整数(0 到 18446744073709551615)
float_float64 的简写
float16半精度浮点数:符号位,5 位指数,10 位尾数
float32单精度浮点数:符号位,8 位指数,23 位尾数
float64双精度浮点数:符号位,11 位指数,52 位尾数
complex_complex128 的简写
complex64复数,由两个 32 位浮点数表示
complex128复数,由两个 64 位浮点数表示

第五章:NumPy 数组的基础知识

在 Python 中对数据进行操作几乎等同于对 NumPy 数组进行操作:即使是像 Pandas(第 III 部分)这样的较新工具也是围绕 NumPy 数组构建的。本章将介绍使用 NumPy 数组操作来访问数据和子数组、拆分、重塑和连接数组的几个示例。虽然这里展示的操作类型可能看起来有些枯燥和迂腐,但它们构成了本书中许多其他示例的基础。好好地了解它们!

这里将涵盖一些基本数组操作的类别:

数组的属性

确定数组的大小、形状、内存消耗和数据类型

数组的索引

获取和设置单个数组元素的值

数组的切片

获取和设置大数组中的较小子数组

数组的重塑

更改给定数组的形状

数组的连接和拆分

将多个数组组合成一个数组,并将一个数组拆分成多个数组

NumPy 数组属性

首先让我们讨论一些有用的数组属性。我们将从定义一维、二维和三维随机数组开始。我们将使用 NumPy 的随机数生成器,我们将使用一个固定值来seed,以确保每次运行此代码时都生成相同的随机数组:

In [1]: import numpy as np
        rng = np.random.default_rng(seed=1701)  # seed for reproducibility

        x1 = rng.integers(10, size=6)  # one-dimensional array
        x2 = rng.integers(10, size=(3, 4))  # two-dimensional array
        x3 = rng.integers(10, size=(3, 4, 5))  # three-dimensional array

每个数组都有一些属性,包括ndim(维数)、shape(每个维度的大小)、size(数组的总大小)和dtype(每个元素的类型):

In [2]: print("x3 ndim: ", x3.ndim)
        print("x3 shape:", x3.shape)
        print("x3 size: ", x3.size)
        print("dtype:   ", x3.dtype)
Out[2]: x3 ndim:  3
        x3 shape: (3, 4, 5)
        x3 size:  60
        dtype:    int64

有关数据类型的更多讨论,请参阅第四章。

数组索引:访问单个元素

如果你熟悉 Python 标准列表的索引,那么在 NumPy 中进行索引将会感觉非常熟悉。在一维数组中,第i个值(从零开始计数)可以通过在方括号中指定所需索引来访问,就像在 Python 列表中一样:

In [3]: x1
Out[3]: array([9, 4, 0, 3, 8, 6])
In [4]: x1[0]
Out[4]: 9
In [5]: x1[4]
Out[5]: 8

要从数组的末尾进行索引,可以使用负索引:

In [6]: x1[-1]
Out[6]: 6
In [7]: x1[-2]
Out[7]: 8

在多维数组中,可以使用逗号分隔的(*行*, *列*)元组来访问项目:

In [8]: x2
Out[8]: array([[3, 1, 3, 7],
               [4, 0, 2, 3],
               [0, 0, 6, 9]])
In [9]: x2[0, 0]
Out[9]: 3
In [10]: x2[2, 0]
Out[10]: 0
In [11]: x2[2, -1]
Out[11]: 9

值也可以使用任何前述的索引表示进行修改:

In [12]: x2[0, 0] = 12
         x2
Out[12]: array([[12,  1,  3,  7],
                [ 4,  0,  2,  3],
                [ 0,  0,  6,  9]])

请记住,与 Python 列表不同,NumPy 数组具有固定的类型。这意味着,例如,如果你尝试将浮点值插入到整数数组中,该值将被静默截断。不要被这种行为所困扰!

In [13]: x1[0] = 3.14159  # this will be truncated!
         x1
Out[13]: array([3, 4, 0, 3, 8, 6])

数组切片:访问子数组

正如我们可以使用方括号访问单个数组元素一样,我们也可以使用切片符号(由冒号(:)字符标记)来访问子数组。NumPy 的切片语法遵循标准 Python 列表的语法;要访问数组x的一个切片,使用以下方法:

x[start:stop:step]

如果其中任何一个未指定,它们将默认为start=0stop=<size of dimension>step=1的值。让我们看一些在一维和多维数组中访问子数组的例子。

一维子数组

这里是一些访问一维子数组中元素的示例:

In [14]: x1
Out[14]: array([3, 4, 0, 3, 8, 6])
In [15]: x1[:3]  # first three elements
Out[15]: array([3, 4, 0])
In [16]: x1[3:]  # elements after index 3
Out[16]: array([3, 8, 6])
In [17]: x1[1:4]  # middle subarray
Out[17]: array([4, 0, 3])
In [18]: x1[::2]  # every second element
Out[18]: array([3, 0, 8])
In [19]: x1[1::2]  # every second element, starting at index 1
Out[19]: array([4, 3, 6])

一个可能令人困惑的情况是当step值为负时。在这种情况下,startstop的默认值会被交换。这成为反转数组的便捷方式:

In [20]: x1[::-1]  # all elements, reversed
Out[20]: array([6, 8, 3, 0, 4, 3])
In [21]: x1[4::-2]  # every second element from index 4, reversed
Out[21]: array([8, 0, 3])

多维子数组

多维切片的工作方式相同,使用逗号分隔的多个切片。例如:

In [22]: x2
Out[22]: array([[12,  1,  3,  7],
                [ 4,  0,  2,  3],
                [ 0,  0,  6,  9]])
In [23]: x2[:2, :3]  # first two rows & three columns
Out[23]: array([[12,  1,  3],
                [ 4,  0,  2]])
In [24]: x2[:3, ::2]  # three rows, every second column
Out[24]: array([[12,  3],
                [ 4,  2],
                [ 0,  6]])
In [25]: x2[::-1, ::-1]  # all rows & columns, reversed
Out[25]: array([[ 9,  6,  0,  0],
                [ 3,  2,  0,  4],
                [ 7,  3,  1, 12]])

一个常见的例程是访问数组的单行或单列。这可以通过组合索引和切片来完成,使用一个冒号(:)标记的空切片:

In [26]: x2[:, 0]  # first column of x2
Out[26]: array([12,  4,  0])
In [27]: x2[0, :]  # first row of x2
Out[27]: array([12,  1,  3,  7])

在行访问的情况下,可以省略空切片以获得更紧凑的语法:

In [28]: x2[0]  # equivalent to x2[0, :]
Out[28]: array([12,  1,  3,  7])

子数组作为非副本视图

与 Python 列表切片不同,NumPy 数组切片返回的是数组数据的视图而不是副本。考虑我们之前的二维数组:

In [29]: print(x2)
Out[29]: [[12  1  3  7]
          [ 4  0  2  3]
          [ 0  0  6  9]]

让我们从中提取一个2 × 2子数组:

In [30]: x2_sub = x2[:2, :2]
         print(x2_sub)
Out[30]: [[12  1]
          [ 4  0]]

现在,如果我们修改这个子数组,我们会看到原始数组也发生了改变!观察:

In [31]: x2_sub[0, 0] = 99
         print(x2_sub)
Out[31]: [[99  1]
          [ 4  0]]
In [32]: print(x2)
Out[32]: [[99  1  3  7]
          [ 4  0  2  3]
          [ 0  0  6  9]]

一些用户可能会觉得这很奇怪,但这可能是有利的:例如,在处理大型数据集时,我们可以访问和处理这些数据集的部分而无需复制底层数据缓冲区。

创建数组的副本

尽管数组视图具有这些特性,但有时将数据明确复制到数组或子数组中更有用。这可以使用copy方法最容易地完成:

In [33]: x2_sub_copy = x2[:2, :2].copy()
         print(x2_sub_copy)
Out[33]: [[99  1]
          [ 4  0]]

如果我们现在修改这个子数组,原始数组不会受到影响:

In [34]: x2_sub_copy[0, 0] = 42
         print(x2_sub_copy)
Out[34]: [[42  1]
          [ 4  0]]
In [35]: print(x2)
Out[35]: [[99  1  3  7]
          [ 4  0  2  3]
          [ 0  0  6  9]]

数组的重塑

另一种有用的操作类型是数组的重塑,可以使用reshape方法完成。例如,如果你想将数字 1 到 9 放在一个3 × 3网格中,你可以这样做:

In [36]: grid = np.arange(1, 10).reshape(3, 3)
         print(grid)
Out[36]: [[1 2 3]
          [4 5 6]
          [7 8 9]]

请注意,为了使其工作,初始数组的大小必须与重塑后数组的大小匹配,在大多数情况下,reshape方法将返回初始数组的非副本视图。

一个常见的重塑操作是将一维数组转换为二维行或列矩阵:

In [37]: x = np.array([1, 2, 3])
         x.reshape((1, 3))  # row vector via reshape
Out[37]: array([[1, 2, 3]])
In [38]: x.reshape((3, 1))  # column vector via reshape
Out[38]: array([[1],
                [2],
                [3]])

这样做的一个便利的简写是在切片语法中使用np.newaxis

In [39]: x[np.newaxis, :]  # row vector via newaxis
Out[39]: array([[1, 2, 3]])
In [40]: x[:, np.newaxis]  # column vector via newaxis
Out[40]: array([[1],
                [2],
                [3]])

这是本书剩余部分我们经常会利用的模式。

数组连接和拆分

所有前述的例程都在单个数组上工作。NumPy 还提供了将多个数组合并成一个数组的工具,以及将单个数组拆分成多个数组的工具。

数组的连接

在 NumPy 中,数组的连接或组合主要通过np.concatenatenp.vstacknp.hstack这些例程来实现。np.concatenate将一个数组的元组或列表作为其第一个参数,如下所示:

In [41]: x = np.array([1, 2, 3])
         y = np.array([3, 2, 1])
         np.concatenate([x, y])
Out[41]: array([1, 2, 3, 3, 2, 1])

你也可以一次连接多个数组:

In [42]: z = np.array([99, 99, 99])
         print(np.concatenate([x, y, z]))
Out[42]: [ 1  2  3  3  2  1 99 99 99]

它也可以用于二维数组:

In [43]: grid = np.array([[1, 2, 3],
                          [4, 5, 6]])
In [44]: # concatenate along the first axis
         np.concatenate([grid, grid])
Out[44]: array([[1, 2, 3],
                [4, 5, 6],
                [1, 2, 3],
                [4, 5, 6]])
In [45]: # concatenate along the second axis (zero-indexed)
         np.concatenate([grid, grid], axis=1)
Out[45]: array([[1, 2, 3, 1, 2, 3],
                [4, 5, 6, 4, 5, 6]])

对于处理混合维度数组,使用 np.vstack(垂直堆叠)和 np.hstack(水平堆叠)函数可能更清晰:

In [46]: # vertically stack the arrays
         np.vstack([x, grid])
Out[46]: array([[1, 2, 3],
                [1, 2, 3],
                [4, 5, 6]])
In [47]: # horizontally stack the arrays
         y = np.array([[99],
                       [99]])
         np.hstack([grid, y])
Out[47]: array([[ 1,  2,  3, 99],
                [ 4,  5,  6, 99]])

类似地,对于高维数组,np.dstack 将沿第三轴堆叠数组。

数组的分割

连接的反操作是分割,由函数 np.splitnp.hsplitnp.vsplit 实现。对于每一个,我们可以传递一个给定分割点的索引列表:

In [48]: x = [1, 2, 3, 99, 99, 3, 2, 1]
         x1, x2, x3 = np.split(x, [3, 5])
         print(x1, x2, x3)
Out[48]: [1 2 3] [99 99] [3 2 1]

注意,N 个分割点会导致 N + 1 个子数组。相关的函数 np.hsplitnp.vsplit 类似:

In [49]: grid = np.arange(16).reshape((4, 4))
         grid
Out[49]: array([[ 0,  1,  2,  3],
                [ 4,  5,  6,  7],
                [ 8,  9, 10, 11],
                [12, 13, 14, 15]])
In [50]: upper, lower = np.vsplit(grid, [2])
         print(upper)
         print(lower)
Out[50]: [[0 1 2 3]
          [4 5 6 7]]
         [[ 8  9 10 11]
          [12 13 14 15]]
In [51]: left, right = np.hsplit(grid, [2])
         print(left)
         print(right)
Out[51]: [[ 0  1]
          [ 4  5]
          [ 8  9]
          [12 13]]
         [[ 2  3]
          [ 6  7]
          [10 11]
          [14 15]]

类似地,对于高维数组,np.dsplit 将沿第三轴分割数组。