附录
附录 A:高级 NumPy
原文:
wesmckinney.com/book/advanced-numpy译者:飞龙
此开放访问网络版本的《Python 数据分析第三版》现已作为印刷版和数字版的伴侣提供。如果您发现任何勘误,请在此处报告。请注意,由 Quarto 生成的本站点的某些方面与 O’Reilly 的印刷版和电子书版本的格式不同。
如果您发现本书的在线版本有用,请考虑订购纸质版或无 DRM 的电子书以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。
在这个附录中,我将深入探讨 NumPy 库的数组计算。这将包括有关 ndarray 类型的更多内部细节以及更高级的数组操作和算法。
这个附录包含各种主题,不一定需要按顺序阅读。在各章节中,我将为许多示例生成随机数据,这些示例将使用numpy.random模块中的默认随机数生成器:
In [11]: rng = np.random.default_rng(seed=12345)
A.1 ndarray 对象内部
NumPy ndarray 提供了一种将块状同类型数据(连续或分步)解释为多维数组对象的方法。数据类型,或dtype,决定了数据被解释为浮点数、整数、布尔值或我们一直在查看的其他类型之一。
ndarray 灵活的部分之一是每个数组对象都是对数据块的步进视图。例如,您可能想知道,例如,数组视图arr[::2, ::-1]如何不复制任何数据。原因是 ndarray 不仅仅是一块内存和一个数据类型;它还具有步进信息,使数组能够以不同的步长在内存中移动。更准确地说,ndarray 内部包含以下内容:
- 一个数据指针—即 RAM 中的数据块或内存映射文件
- 描述数组中固定大小值单元的数据类型或 dtype
- 一个指示数组形状的元组
- 一个步长元组—表示在一个维度上前进一个元素所需的字节数
请参见图 A.1 以查看 ndarray 内部的简单模拟。
图 A.1:NumPy ndarray 对象
例如,一个 10×5 的数组将具有形状(10, 5):
In [12]: np.ones((10, 5)).shape Out[12]: (10, 5)
一个典型的(C 顺序)3×4×5 的float64(8 字节)值数组具有步长(160, 40, 8)(了解步长可以是有用的,因为一般来说,特定轴上的步长越大,沿着该轴执行计算的成本就越高):
In [13]: np.ones((3, 4, 5), dtype=np.float64).strides Out[13]: (160, 40, 8)
虽然典型的 NumPy 用户很少会对数组的步长感兴趣,但它们需要用来构建“零拷贝”数组视图。步长甚至可以是负数,这使得数组可以在内存中“向后”移动(例如,在像obj[::-1]或obj[:, ::-1]这样的切片中)。
NumPy 数据类型层次结构
您可能偶尔需要检查代码是否包含整数、浮点数、字符串或 Python 对象的数组。由于有多种浮点数类型(float16到float128),检查数据类型是否在类型列表中会非常冗长。幸运的是,数据类型有超类,如np.integer和np.floating,可以与np.issubdtype函数一起使用:
In [14]: ints = np.ones(10, dtype=np.uint16) In [15]: floats = np.ones(10, dtype=np.float32) In [16]: np.issubdtype(ints.dtype, np.integer) Out[16]: True In [17]: np.issubdtype(floats.dtype, np.floating) Out[17]: True
您可以通过调用类型的mro方法查看特定数据类型的所有父类:
In [18]: np.float64.mro() Out[18]: [numpy.float64, numpy.floating, numpy.inexact, numpy.number, numpy.generic, float, object]
因此,我们还有:
In [19]: np.issubdtype(ints.dtype, np.number) Out[19]: True
大多数 NumPy 用户永远不需要了解这一点,但有时会有用。请参见图 A.2 以查看数据类型层次结构和父-子类关系的图表。¹
图 A.2:NumPy 数据类型类层次结构
A.2 高级数组操作
除了花式索引、切片和布尔子集之外,还有许多处理数组的方法。虽然大部分数据分析应用程序的繁重工作由 pandas 中的高级函数处理,但您可能在某个时候需要编写一个在现有库中找不到的数据算法。
重新塑形数组
在许多情况下,您可以将一个数组从一种形状转换为另一种形状而不复制任何数据。为此,将表示新形状的元组传递给 reshape 数组实例方法。例如,假设我们有一个希望重新排列成矩阵的值的一维数组(这在图 A.3 中有说明):
In [20]: arr = np.arange(8) In [21]: arr Out[21]: array([0, 1, 2, 3, 4, 5, 6, 7]) In [22]: arr.reshape((4, 2)) Out[22]: array([[0, 1], [2, 3], [4, 5], [6, 7]])
图 A.3:按 C(行主要)或 FORTRAN(列主要)顺序重新塑形
多维数组也可以被重新塑形:
In [23]: arr.reshape((4, 2)).reshape((2, 4)) Out[23]: array([[0, 1, 2, 3], [4, 5, 6, 7]])
传递的形状维度中可以有一个为 -1,在这种情况下,该维度的值将从数据中推断出来:
In [24]: arr = np.arange(15) In [25]: arr.reshape((5, -1)) Out[25]: array([[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11], [12, 13, 14]])
由于数组的 shape 属性是一个元组,它也可以传递给 reshape:
In [26]: other_arr = np.ones((3, 5)) In [27]: other_arr.shape Out[27]: (3, 5) In [28]: arr.reshape(other_arr.shape) Out[28]: array([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14]])
从一维到更高维的 reshape 的相反操作通常称为 展平 或 raveling:
In [29]: arr = np.arange(15).reshape((5, 3)) In [30]: arr Out[30]: array([[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11], [12, 13, 14]]) In [31]: arr.ravel() Out[31]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
如果结果中的值在原始数组中是连续的,ravel 不会生成基础值的副本。
flatten 方法的行为类似于 ravel,只是它总是返回数据的副本:
In [32]: arr.flatten() Out[32]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
数据可以以不同的顺序被重新塑形或展开。这对于新的 NumPy 用户来说是一个略微微妙的主题,因此是下一个子主题。
C 与 FORTRAN 顺序
NumPy 能够适应内存中数据的许多不同布局。默认情况下,NumPy 数组是按 行主要 顺序创建的。从空间上讲,这意味着如果您有一个二维数据数组,数组中每行的项都存储在相邻的内存位置上。与行主要顺序相反的是 列主要 顺序,这意味着数据中每列的值都存储在相邻的内存位置上。
出于历史原因,行和列主要顺序也被称为 C 和 FORTRAN 顺序。在 FORTRAN 77 语言中,矩阵都是列主要的。
像 reshape 和 ravel 这样的函数接受一个 order 参数,指示数组中使用数据的顺序。在大多数情况下,这通常设置为 'C' 或 'F'(还有一些不常用的选项 'A' 和 'K';请参阅 NumPy 文档,并参考图 A.3 以了解这些选项的说明):
In [33]: arr = np.arange(12).reshape((3, 4)) In [34]: arr Out[34]: array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) In [35]: arr.ravel() Out[35]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) In [36]: arr.ravel('F') Out[36]: array([ 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11])
使用超过两个维度的数组进行重新塑形可能有点令人费解(参见图 A.3)。C 和 FORTRAN 顺序之间的关键区别在于维度的遍历方式:
C/行主要顺序
在遍历更高维度时,首先 遍历(例如,先在轴 1 上再在轴 0 上前进)。
FORTRAN/列主要顺序
在遍历更高维度时,最后 遍历(例如,先在轴 0 上再在轴 1 上前进)。
连接和分割数组
numpy.concatenate 接受一个数组序列(元组,列表等),并按顺序沿着输入轴连接它们:
In [37]: arr1 = np.array([[1, 2, 3], [4, 5, 6]]) In [38]: arr2 = np.array([[7, 8, 9], [10, 11, 12]]) In [39]: np.concatenate([arr1, arr2], axis=0) Out[39]: array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]]) In [40]: np.concatenate([arr1, arr2], axis=1) Out[40]: array([[ 1, 2, 3, 7, 8, 9], [ 4, 5, 6, 10, 11, 12]])
有一些便利函数,如 vstack 和 hstack,用于常见类型的连接。前面的操作可以表示为:
In [41]: np.vstack((arr1, arr2)) Out[41]: array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]]) In [42]: np.hstack((arr1, arr2)) Out[42]: array([[ 1, 2, 3, 7, 8, 9], [ 4, 5, 6, 10, 11, 12]])
另一方面,split 将数组沿着一个轴分割成多个数组:
In [43]: arr = rng.standard_normal((5, 2)) In [44]: arr Out[44]: array([[-1.4238, 1.2637], [-0.8707, -0.2592], [-0.0753, -0.7409], [-1.3678, 0.6489], [ 0.3611, -1.9529]]) In [45]: first, second, third = np.split(arr, [1, 3]) In [46]: first Out[46]: array([[-1.4238, 1.2637]]) In [47]: second Out[47]: array([[-0.8707, -0.2592], [-0.0753, -0.7409]]) In [48]: third Out[48]: array([[-1.3678, 0.6489], [ 0.3611, -1.9529]])
传递给 np.split 的值 [1, 3] 指示在哪些索引处将数组分割成片段。
请参见表 A.1 以获取所有相关连接和分割函数的列表,其中一些仅作为非常通用的 concatenate 的便利。
表 A.1:数组连接函数
| 函数 | 描述 |
concatenate |
最通用的函数,沿一个轴连接数组集合 |
vstack, row_stack |
按行堆叠数组(沿轴 0) |
hstack |
按列堆叠数组(沿轴 1) |
column_stack |
类似于hstack,但首先将 1D 数组转换为 2D 列向量 |
dstack |
按“深度”(沿轴 2)堆叠数组 |
split |
沿特定轴在传递位置分割数组 |
hsplit/vsplit |
在轴 0 和 1 上分割的便利函数 |
堆叠助手:r_ 和 c_
NumPy 命名空间中有两个特殊对象,r_和c_,使堆叠数组更简洁:
In [49]: arr = np.arange(6) In [50]: arr1 = arr.reshape((3, 2)) In [51]: arr2 = rng.standard_normal((3, 2)) In [52]: np.r_[arr1, arr2] Out[52]: array([[ 0. , 1. ], [ 2. , 3. ], [ 4. , 5. ], [ 2.3474, 0.9685], [-0.7594, 0.9022], [-0.467 , -0.0607]]) In [53]: np.c_[np.r_[arr1, arr2], arr] Out[53]: array([[ 0. , 1. , 0. ], [ 2. , 3. , 1. ], [ 4. , 5. , 2. ], [ 2.3474, 0.9685, 3. ], [-0.7594, 0.9022, 4. ], [-0.467 , -0.0607, 5. ]])
这些还可以将切片转换为数组:
In [54]: np.c_[1:6, -10:-5] Out[54]: array([[ 1, -10], [ 2, -9], [ 3, -8], [ 4, -7], [ 5, -6]])
查看文档字符串以了解您可以使用c_和r_做什么。
重复元素:tile 和 repeat
用于重复或复制数组以生成更大数组的两个有用工具是repeat和tile函数。repeat将数组中的每个元素重复若干次,生成一个更大的数组:
In [55]: arr = np.arange(3) In [56]: arr Out[56]: array([0, 1, 2]) In [57]: arr.repeat(3) Out[57]: array([0, 0, 0, 1, 1, 1, 2, 2, 2])
注意
需要复制或重复数组的情况在 NumPy 中可能不像其他数组编程框架(如 MATLAB)中那样常见。其中一个原因是广播通常更好地满足这种需求,这是下一节的主题。
默认情况下,如果传递一个整数,每个元素将重复该次数。如果传递一个整数数组,每个元素可以重复不同次数:
In [58]: arr.repeat([2, 3, 4]) Out[58]: array([0, 0, 1, 1, 1, 2, 2, 2, 2])
多维数组可以沿特定轴重复其元素:
In [59]: arr = rng.standard_normal((2, 2)) In [60]: arr Out[60]: array([[ 0.7888, -1.2567], [ 0.5759, 1.399 ]]) In [61]: arr.repeat(2, axis=0) Out[61]: array([[ 0.7888, -1.2567], [ 0.7888, -1.2567], [ 0.5759, 1.399 ], [ 0.5759, 1.399 ]])
请注意,如果没有传递轴,数组将首先被展平,这可能不是您想要的。同样,当重复多维数组以不同次数重复给定切片时,可以传递整数数组:
In [62]: arr.repeat([2, 3], axis=0) Out[62]: array([[ 0.7888, -1.2567], [ 0.7888, -1.2567], [ 0.5759, 1.399 ], [ 0.5759, 1.399 ], [ 0.5759, 1.399 ]]) In [63]: arr.repeat([2, 3], axis=1) Out[63]: array([[ 0.7888, 0.7888, -1.2567, -1.2567, -1.2567], [ 0.5759, 0.5759, 1.399 , 1.399 , 1.399 ]])
另一方面,tile是一个沿轴堆叠数组副本的快捷方式。在视觉上,您可以将其视为类似于“铺设瓷砖”:
In [64]: arr Out[64]: array([[ 0.7888, -1.2567], [ 0.5759, 1.399 ]]) In [65]: np.tile(arr, 2) Out[65]: array([[ 0.7888, -1.2567, 0.7888, -1.2567], [ 0.5759, 1.399 , 0.5759, 1.399 ]])
第二个参数是瓷砖的数量;对于标量,瓦片是按行而不是按列进行的。tile的第二个参数可以是一个元组,指示“瓦片”的布局:
In [66]: arr Out[66]: array([[ 0.7888, -1.2567], [ 0.5759, 1.399 ]]) In [67]: np.tile(arr, (2, 1)) Out[67]: array([[ 0.7888, -1.2567], [ 0.5759, 1.399 ], [ 0.7888, -1.2567], [ 0.5759, 1.399 ]]) In [68]: np.tile(arr, (3, 2)) Out[68]: array([[ 0.7888, -1.2567, 0.7888, -1.2567], [ 0.5759, 1.399 , 0.5759, 1.399 ], [ 0.7888, -1.2567, 0.7888, -1.2567], [ 0.5759, 1.399 , 0.5759, 1.399 ], [ 0.7888, -1.2567, 0.7888, -1.2567], [ 0.5759, 1.399 , 0.5759, 1.399 ]])
花式索引等效:take 和 put
正如您可能从 Ch 4:NumPy 基础:数组和矢量化计算中记得的那样,通过使用整数数组进行花式索引来获取和设置数组的子集是一种方法:
In [69]: arr = np.arange(10) * 100 In [70]: inds = [7, 1, 2, 6] In [71]: arr[inds] Out[71]: array([700, 100, 200, 600])
在仅在单个轴上进行选择的特殊情况下,有一些替代的 ndarray 方法是有用的:
In [72]: arr.take(inds) Out[72]: array([700, 100, 200, 600]) In [73]: arr.put(inds, 42) In [74]: arr Out[74]: array([ 0, 42, 42, 300, 400, 500, 42, 42, 800, 900]) In [75]: arr.put(inds, [40, 41, 42, 43]) In [76]: arr Out[76]: array([ 0, 41, 42, 300, 400, 500, 43, 40, 800, 900])
要在其他轴上使用take,可以传递axis关键字:
In [77]: inds = [2, 0, 2, 1] In [78]: arr = rng.standard_normal((2, 4)) In [79]: arr Out[79]: array([[ 1.3223, -0.2997, 0.9029, -1.6216], [-0.1582, 0.4495, -1.3436, -0.0817]]) In [80]: arr.take(inds, axis=1) Out[80]: array([[ 0.9029, 1.3223, 0.9029, -0.2997], [-1.3436, -0.1582, -1.3436, 0.4495]])
put不接受axis参数,而是索引到数组的展平(一维,C 顺序)版本。因此,当您需要使用索引数组在其他轴上设置元素时,最好使用基于[]的索引。
A.3 广播
广播规定了不同形状数组之间的操作方式。它可以是一个强大的功能,但即使对于有经验的用户也可能会引起混淆。广播的最简单示例是将标量值与数组组合时发生:
In [81]: arr = np.arange(5) In [82]: arr Out[82]: array([0, 1, 2, 3, 4]) In [83]: arr * 4 Out[83]: array([ 0, 4, 8, 12, 16])
在这里,我们说标量值 4 已经广播到乘法操作中的所有其他元素。
例如,我们可以通过减去列均值来对数组的每一列进行去均值处理。在这种情况下,只需要减去包含每列均值的数组即可:
In [84]: arr = rng.standard_normal((4, 3)) In [85]: arr.mean(0) Out[85]: array([0.1206, 0.243 , 0.1444]) In [86]: demeaned = arr - arr.mean(0) In [87]: demeaned Out[87]: array([[ 1.6042, 2.3751, 0.633 ], [ 0.7081, -1.202 , -1.3538], [-1.5329, 0.2985, 0.6076], [-0.7793, -1.4717, 0.1132]]) In [88]: demeaned.mean(0) Out[88]: array([ 0., -0., 0.])
请参见图 A.4 以了解此操作的示例。将行作为广播操作去均值需要更多的注意。幸运的是,跨任何数组维度广播潜在较低维值(例如从二维数组的每列中减去行均值)是可能的,只要遵循规则。
这将我们带到了广播规则。
两个数组在广播时兼容,如果对于每个尾部维度(即,从末尾开始),轴的长度匹配,或者长度中的任何一个为 1。然后在缺失或长度为 1 的维度上执行广播。
图 A.4:在 1D 数组的轴 0 上进行广播
即使作为一个经验丰富的 NumPy 用户,我经常发现自己在思考广播规则时不得不停下来画图。考虑最后一个示例,假设我们希望减去每行的平均值。由于arr.mean(0)的长度为 3,它在轴 0 上是兼容的进行广播,因为arr中的尾部维度为 3,因此匹配。根据规则,要在轴 1 上进行减法(即,从每行减去行均值),较小的数组必须具有形状(4, 1):
In [89]: arr Out[89]: array([[ 1.7247, 2.6182, 0.7774], [ 0.8286, -0.959 , -1.2094], [-1.4123, 0.5415, 0.7519], [-0.6588, -1.2287, 0.2576]]) In [90]: row_means = arr.mean(1) In [91]: row_means.shape Out[91]: (4,) In [92]: row_means.reshape((4, 1)) Out[92]: array([[ 1.7068], [-0.4466], [-0.0396], [-0.5433]]) In [93]: demeaned = arr - row_means.reshape((4, 1)) In [94]: demeaned.mean(1) Out[94]: array([-0., 0., 0., 0.])
查看图 A.5 以了解此操作的示例。
图 A.5:在 2D 数组的轴 1 上进行广播
查看图 A.6 以获得另一个示例,这次是在轴 0 上将二维数组添加到三维数组中。
图 A.6:在 3D 数组的轴 0 上进行广播
Python 数据分析(PYDA)第三版(七)(2)https://developer.aliyun.com/article/1482402